1 /*******************************************************
2  Copyright (C) 2013,2014 Guan Lisheng (guanlisheng@gmail.com)
3 
4  This program is free software; you can redistribute it and/or modify
5  it under the terms of the GNU General Public License as published by
6  the Free Software Foundation; either version 2 of the License, or
7  (at your option) any later version.
8 
9  This program is distributed in the hope that it will be useful,
10  but WITHOUT ANY WARRANTY; without even the implied warranty of
11  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  GNU General Public License for more details.
13 
14  You should have received a copy of the GNU General Public License
15  along with this program; if not, write to the Free Software
16  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
17  ********************************************************/
18 
19 #include "mmOption.h"
20 #include "Model_Checking.h"
21 #include "Model_Account.h"
22 #include "Model_Payee.h"
23 #include "Model_Category.h"
24 #include <queue>
25 
26 const std::vector<std::pair<Model_Checking::TYPE, wxString> > Model_Checking::TYPE_CHOICES =
27 {
28     std::make_pair(Model_Checking::WITHDRAWAL, wxTRANSLATE("Withdrawal"))
29     , std::make_pair(Model_Checking::DEPOSIT, wxTRANSLATE("Deposit"))
30     , std::make_pair(Model_Checking::TRANSFER, wxTRANSLATE("Transfer"))
31 };
32 
33 const std::vector<std::pair<Model_Checking::STATUS_ENUM, wxString> > Model_Checking::STATUS_ENUM_CHOICES =
34 {
35     std::make_pair(Model_Checking::NONE, wxTRANSLATE("None"))
36     , std::make_pair(Model_Checking::RECONCILED, wxTRANSLATE("Reconciled"))
37     , std::make_pair(Model_Checking::VOID_, wxTRANSLATE("Void"))
38     , std::make_pair(Model_Checking::FOLLOWUP, wxTRANSLATE("Follow up"))
39     , std::make_pair(Model_Checking::DUPLICATE_, wxTRANSLATE("Duplicate"))
40 };
41 
Model_Checking()42 Model_Checking::Model_Checking(): Model<DB_Table_CHECKINGACCOUNT_V1>()
43 {
44 }
45 
~Model_Checking()46 Model_Checking::~Model_Checking()
47 {
48 }
49 
all_type()50 wxArrayString Model_Checking::all_type()
51 {
52     wxArrayString types;
53     for (const auto& r : TYPE_CHOICES) types.Add(r.second);
54 
55     return types;
56 }
57 
all_status()58 wxArrayString Model_Checking::all_status()
59 {
60     wxArrayString status;
61     for (const auto& r : STATUS_ENUM_CHOICES) status.Add(r.second);
62 
63     return status;
64 }
65 
66 /**
67 * Initialize the global Model_Checking table.
68 * Reset the Model_Checking table or create the table if it does not exist.
69 */
instance(wxSQLite3Database * db)70 Model_Checking& Model_Checking::instance(wxSQLite3Database* db)
71 {
72     Model_Checking& ins = Singleton<Model_Checking>::instance();
73     ins.db_ = db;
74     ins.destroy_cache();
75     ins.ensure(db);
76 
77     return ins;
78 }
79 
80 /** Return the static instance of Model_Checking table */
instance()81 Model_Checking& Model_Checking::instance()
82 {
83     return Singleton<Model_Checking>::instance();
84 }
85 
remove(int id)86 bool Model_Checking::remove(int id)
87 {
88     //TODO: remove all split at once
89     //Model_Splittransaction::instance().remove(Model_Splittransaction::instance().find(Model_Splittransaction::TRANSID(id)));
90     for (const auto& r : Model_Splittransaction::instance().find(Model_Splittransaction::TRANSID(id)))
91         Model_Splittransaction::instance().remove(r.SPLITTRANSID);
92     return this->remove(id, db_);
93 }
94 
splittransaction(const Data * r)95 const Model_Splittransaction::Data_Set Model_Checking::splittransaction(const Data* r)
96 {
97     return Model_Splittransaction::instance().find(Model_Splittransaction::TRANSID(r->TRANSID));
98 }
99 
splittransaction(const Data & r)100 const Model_Splittransaction::Data_Set Model_Checking::splittransaction(const Data& r)
101 {
102     return Model_Splittransaction::instance().find(Model_Splittransaction::TRANSID(r.TRANSID));
103 }
104 
TRANSDATE(const wxDate & date,OP op)105 DB_Table_CHECKINGACCOUNT_V1::TRANSDATE Model_Checking::TRANSDATE(const wxDate& date, OP op)
106 {
107     return DB_Table_CHECKINGACCOUNT_V1::TRANSDATE(date.FormatISODate(), op);
108 }
109 
STATUS(STATUS_ENUM status,OP op)110 DB_Table_CHECKINGACCOUNT_V1::STATUS Model_Checking::STATUS(STATUS_ENUM status, OP op)
111 {
112     return DB_Table_CHECKINGACCOUNT_V1::STATUS(toShortStatus(all_status()[status]), op);
113 }
114 
TRANSCODE(TYPE type,OP op)115 DB_Table_CHECKINGACCOUNT_V1::TRANSCODE Model_Checking::TRANSCODE(TYPE type, OP op)
116 {
117     return DB_Table_CHECKINGACCOUNT_V1::TRANSCODE(all_type()[type], op);
118 }
119 
TRANSDATE(const Data * r)120 wxDate Model_Checking::TRANSDATE(const Data* r)
121 {
122     return Model::to_date(r->TRANSDATE);
123 }
124 
TRANSDATE(const Data & r)125 wxDate Model_Checking::TRANSDATE(const Data& r)
126 {
127     return Model::to_date(r.TRANSDATE);
128 }
129 
type(const wxString & r)130 Model_Checking::TYPE Model_Checking::type(const wxString& r)
131 {
132     if (r.empty()) return TYPE::WITHDRAWAL;
133     static std::map<wxString, TYPE> cache;
134     const auto it = cache.find(r);
135     if (it != cache.end()) return it->second;
136 
137     for (const auto& t : TYPE_CHOICES)
138     {
139         if (r.CmpNoCase(t.second) == 0)
140         {
141             cache.insert(std::make_pair(r, t.first));
142             return t.first;
143         }
144     }
145 
146     cache.insert(std::make_pair(r, TYPE::WITHDRAWAL));
147     return TYPE::WITHDRAWAL;
148 }
type(const Data & r)149 Model_Checking::TYPE Model_Checking::type(const Data& r)
150 {
151     return type(r.TRANSCODE);
152 }
type(const Data * r)153 Model_Checking::TYPE Model_Checking::type(const Data* r)
154 {
155     return type(r->TRANSCODE);
156 }
157 
status(const wxString & r)158 Model_Checking::STATUS_ENUM Model_Checking::status(const wxString& r)
159 {
160     static std::map<wxString, STATUS_ENUM> cache;
161     const auto it = cache.find(r);
162     if (it != cache.end()) return it->second;
163 
164     for (const auto & s : STATUS_ENUM_CHOICES)
165     {
166         if (r.CmpNoCase(s.second) == 0)
167         {
168             cache.insert(std::make_pair(r, s.first));
169             return s.first;
170         }
171     }
172 
173     STATUS_ENUM ret = NONE;
174     if (r.CmpNoCase("R") == 0) ret = RECONCILED;
175     else if (r.CmpNoCase("V") == 0) ret = VOID_;
176     else if (r.CmpNoCase("F") == 0) ret = FOLLOWUP;
177     else if (r.CmpNoCase("D") == 0) ret = DUPLICATE_;
178     cache.insert(std::make_pair(r, ret));
179 
180     return ret;
181 }
status(const Data & r)182 Model_Checking::STATUS_ENUM Model_Checking::status(const Data& r)
183 {
184     return status(r.STATUS);
185 }
status(const Data * r)186 Model_Checking::STATUS_ENUM Model_Checking::status(const Data* r)
187 {
188     return status(r->STATUS);
189 }
190 
amount(const Data * r,int account_id)191 double Model_Checking::amount(const Data* r, int account_id)
192 {
193     double sum = 0;
194     switch (type(r->TRANSCODE))
195     {
196     case WITHDRAWAL:
197         sum -= r->TRANSAMOUNT;
198         break;
199     case DEPOSIT:
200         sum += r->TRANSAMOUNT;
201         break;
202     case TRANSFER:
203         if (account_id == r->ACCOUNTID)
204             sum -= r->TRANSAMOUNT;
205         else
206             sum += r->TOTRANSAMOUNT;
207         break;
208     default:
209         break;
210     }
211     return sum;
212 }
213 
amount(const Data & r,int account_id)214 double Model_Checking::amount(const Data&r, int account_id)
215 {
216     return amount(&r, account_id);
217 }
218 
balance(const Data * r,int account_id)219 double Model_Checking::balance(const Data* r, int account_id)
220 {
221     if (Model_Checking::status(r->STATUS) == Model_Checking::VOID_) return 0;
222     return amount(r, account_id);
223 }
224 
balance(const Data & r,int account_id)225 double Model_Checking::balance(const Data& r, int account_id)
226 {
227     return balance(&r, account_id);
228 }
229 
withdrawal(const Data * r,int account_id)230 double Model_Checking::withdrawal(const Data* r, int account_id)
231 {
232     double bal = balance(r, account_id);
233     return bal <= 0 ? -bal : 0;
234 }
235 
withdrawal(const Data & r,int account_id)236 double Model_Checking::withdrawal(const Data& r, int account_id)
237 {
238     return withdrawal(&r, account_id);
239 }
240 
deposit(const Data * r,int account_id)241 double Model_Checking::deposit(const Data* r, int account_id)
242 {
243     double bal = balance(r, account_id);
244     return bal > 0 ? bal : 0;
245 }
246 
deposit(const Data & r,int account_id)247 double Model_Checking::deposit(const Data& r, int account_id)
248 {
249     return deposit(&r, account_id);
250 }
251 
reconciled(const Data * r,int account_id)252 double Model_Checking::reconciled(const Data* r, int account_id)
253 {
254     return (Model_Checking::status(r->STATUS) == Model_Checking::RECONCILED) ? balance(r, account_id) : 0;
255 }
256 
reconciled(const Data & r,int account_id)257 double Model_Checking::reconciled(const Data& r, int account_id)
258 {
259     return reconciled(&r, account_id);
260 }
261 
is_transfer(const wxString & r)262 bool Model_Checking::is_transfer(const wxString& r)
263 {
264     return type(r) == Model_Checking::TRANSFER;
265 }
is_transfer(const Data * r)266 bool Model_Checking::is_transfer(const Data* r)
267 {
268     return is_transfer(r->TRANSCODE);
269 }
is_deposit(const wxString & r)270 bool Model_Checking::is_deposit(const wxString& r)
271 {
272     return type(r) == Model_Checking::DEPOSIT;
273 }
is_deposit(const Data * r)274 bool Model_Checking::is_deposit(const Data* r)
275 {
276     return is_deposit(r->TRANSCODE);
277 }
278 
toShortStatus(const wxString & fullStatus)279 wxString Model_Checking::toShortStatus(const wxString& fullStatus)
280 {
281     wxString s = fullStatus.Left(1);
282     s.Replace("N", "");
283     return s;
284 }
285 
Full_Data()286 Model_Checking::Full_Data::Full_Data() : Data(0), BALANCE(0), AMOUNT(0)
287 {
288 }
289 
Full_Data(const Data & r)290 Model_Checking::Full_Data::Full_Data(const Data& r) : Data(r), BALANCE(0), AMOUNT(0)
291     , m_splits(Model_Splittransaction::instance().find(Model_Splittransaction::TRANSID(r.TRANSID)))
292 {
293     ACCOUNTNAME = Model_Account::get_account_name(r.ACCOUNTID);
294 
295     if (Model_Checking::type(r) == Model_Checking::TRANSFER)
296     {
297         TOACCOUNTNAME = Model_Account::get_account_name(r.TOACCOUNTID);
298         PAYEENAME = TOACCOUNTNAME;
299     }
300     else
301     {
302         PAYEENAME = Model_Payee::get_payee_name(r.PAYEEID);
303     }
304 
305     if (!m_splits.empty())
306     {
307         for (const auto& entry : m_splits)
308             this->CATEGNAME += (this->CATEGNAME.empty() ? " * " : ", ")
309                 + Model_Category::full_name(entry.CATEGID, entry.SUBCATEGID);
310     }
311     else
312     {
313         this->CATEGNAME = Model_Category::instance().full_name(r.CATEGID, r.SUBCATEGID);
314     }
315 }
316 
Full_Data(const Data & r,const std::map<int,Model_Splittransaction::Data_Set> & splits)317 Model_Checking::Full_Data::Full_Data(const Data& r
318     , const std::map<int /*trans id*/, Model_Splittransaction::Data_Set /*split trans*/ > & splits)
319     : Data(r), BALANCE(0), AMOUNT(0)
320 {
321     const auto it = splits.find(this->id());
322     if (it != splits.end()) m_splits = it->second;
323 
324     ACCOUNTNAME = Model_Account::get_account_name(r.ACCOUNTID);
325     if (Model_Checking::type(r) == Model_Checking::TRANSFER)
326     {
327         TOACCOUNTNAME = Model_Account::get_account_name(r.TOACCOUNTID);
328         PAYEENAME = TOACCOUNTNAME;
329     }
330     else
331     {
332         PAYEENAME = Model_Payee::get_payee_name(r.PAYEEID);
333     }
334 
335     if (!m_splits.empty())
336     {
337         for (const auto& entry : m_splits)
338             this->CATEGNAME += (this->CATEGNAME.empty() ? " * " : ", ")
339                 + Model_Category::full_name(entry.CATEGID, entry.SUBCATEGID);
340     }
341     else
342     {
343         CATEGNAME = Model_Category::full_name(r.CATEGID, r.SUBCATEGID);
344     }
345 }
346 
~Full_Data()347 Model_Checking::Full_Data::~Full_Data()
348 {
349 }
350 
real_payee_name(int account_id) const351 wxString Model_Checking::Full_Data::real_payee_name(int account_id) const
352 {
353     if (TYPE::TRANSFER == type(this->TRANSCODE))
354     {
355         if (this->ACCOUNTID == account_id || account_id == -1)
356             return ("> " + this->TOACCOUNTNAME);
357         else
358             return ("< " + this->ACCOUNTNAME);
359     }
360 
361     return this->PAYEENAME;
362 }
363 
has_split() const364 bool Model_Checking::Full_Data::has_split() const
365 {
366     return !this->m_splits.empty();
367 }
368 
info() const369 wxString Model_Checking::Full_Data::info() const
370 {
371     // TODO more info
372     wxDate date = Model_Checking::TRANSDATE(this);
373     wxString info = wxGetTranslation(date.GetWeekDayName(date.GetWeekDay()));
374     return info;
375 }
376 
getFrequentUsedNotes(std::vector<wxString> & frequentNotes,int accountID)377 void Model_Checking::getFrequentUsedNotes(std::vector<wxString> &frequentNotes, int accountID)
378 {
379     frequentNotes.clear();
380     int max = 20;
381 
382     const auto notes = instance().find(NOTES("", NOT_EQUAL)
383         , accountID > 0 ? ACCOUNTID(accountID) : ACCOUNTID(-1, NOT_EQUAL));
384 
385     std::map <wxString, int> counterMap;
386     for (const auto& entry : notes)
387         counterMap[wxString(entry.NOTES).Trim()]--;
388 
389     std::priority_queue<std::pair<int, wxString> > q; // largest element to appear as the top
390     for (const auto & kv : counterMap)
391     {
392         q.push(std::make_pair(kv.second, kv.first));
393         if (q.size() > max) q.pop(); // keep fixed queue as max
394     }
395 
396     while (!q.empty())
397     {
398         const auto & kv = q.top();
399         frequentNotes.push_back(kv.second);
400         q.pop();
401     }
402 }
403 
getEmptyTransaction(Data & data,int accountID)404 void Model_Checking::getEmptyTransaction(Data &data, int accountID)
405 {
406     data.TRANSID = -1;
407     wxDateTime trx_date = wxDateTime::Today();
408     if (mmIniOptions::instance().transDateDefault_ != 0)
409     {
410         auto trans = instance().find(ACCOUNTID(accountID), TRANSDATE(trx_date, LESS_OR_EQUAL));
411         std::stable_sort(trans.begin(), trans.end(), SorterByTRANSDATE());
412         std::reverse(trans.begin(), trans.end());
413         if (!trans.empty())
414             trx_date = to_date(trans.begin()->TRANSDATE);
415 
416         wxDateTime trx_date_b = wxDateTime::Today();
417         auto trans_b = instance().find(TOACCOUNTID(accountID), TRANSDATE(trx_date_b, LESS_OR_EQUAL));
418         std::stable_sort(trans_b.begin(), trans_b.end(), SorterByTRANSDATE());
419         std::reverse(trans_b.begin(), trans_b.end());
420         if (!trans_b.empty())
421         {
422             trx_date_b = to_date(trans_b.begin()->TRANSDATE);
423             if (!trans.empty() && (trx_date_b > trx_date))
424                 trx_date = trx_date_b;
425         }
426     }
427 
428     data.TRANSDATE = trx_date.FormatISODate();
429     data.ACCOUNTID = accountID;
430     data.STATUS = toShortStatus(all_status()[mmIniOptions::instance().transStatusReconciled_]);
431     data.TRANSCODE = all_type()[WITHDRAWAL];
432     data.CATEGID = -1;
433     data.SUBCATEGID = -1;
434     data.FOLLOWUPID = -1;
435     data.TRANSAMOUNT = 0;
436     data.TOTRANSAMOUNT = 0;
437     data.TRANSACTIONNUMBER = "";
438     if (mmIniOptions::instance().transPayeeSelectionNone_ != 0)
439     {
440         auto trx = instance().find(TRANSCODE(TRANSFER, NOT_EQUAL)
441             , ACCOUNTID(accountID, EQUAL), TRANSDATE(trx_date, LESS_OR_EQUAL));
442 
443         if (!trx.empty())
444         {
445             std::stable_sort(trx.begin(), trx.end(), SorterByTRANSDATE());
446             Model_Payee::Data* payee = Model_Payee::instance().get(trx.rbegin()->PAYEEID);
447             if (payee) data.PAYEEID = payee->PAYEEID;
448             if (payee && mmIniOptions::instance().transCategorySelectionNone_ != 0)
449             {
450                 data.CATEGID = payee->CATEGID;
451                 data.SUBCATEGID = payee->SUBCATEGID;
452             }
453         }
454     }
455 }
456 
getTransactionData(Data & data,const Data * r)457 bool Model_Checking::getTransactionData(Data &data, const Data* r)
458 {
459     if (r) {
460         data.TRANSDATE = r->TRANSDATE;
461         data.STATUS = r->STATUS;
462         data.ACCOUNTID = r->ACCOUNTID;
463         data.TOACCOUNTID = r->TOACCOUNTID;
464         data.TRANSCODE = r->TRANSCODE;
465         data.CATEGID = r->CATEGID;
466         data.SUBCATEGID = r->SUBCATEGID;
467         data.TRANSAMOUNT = r->TRANSAMOUNT;
468         data.TOTRANSAMOUNT = r->TOTRANSAMOUNT;
469         data.FOLLOWUPID = r->FOLLOWUPID;
470         data.NOTES = r->NOTES;
471         data.TRANSACTIONNUMBER = r->TRANSACTIONNUMBER;
472         data.PAYEEID = r->PAYEEID;
473         data.TRANSID = r->TRANSID;
474     }
475     return r ? true : false;
476 }
477 
putDataToTransaction(Data * r,const Data & data)478 void Model_Checking::putDataToTransaction(Data *r, const Data &data)
479 {
480     r->STATUS = data.STATUS;
481     r->TRANSCODE = data.TRANSCODE;
482     r->TRANSDATE = data.TRANSDATE;
483     r->PAYEEID = data.PAYEEID;
484     r->ACCOUNTID = data.ACCOUNTID;
485     r->TRANSAMOUNT = data.TRANSAMOUNT;
486     r->CATEGID = data.CATEGID;
487     r->SUBCATEGID = data.SUBCATEGID;
488     r->TOACCOUNTID = data.TOACCOUNTID;
489     r->TOTRANSAMOUNT = data.TOTRANSAMOUNT;
490     r->NOTES = data.NOTES;
491     r->TRANSACTIONNUMBER = data.TRANSACTIONNUMBER;
492     r->FOLLOWUPID = data.FOLLOWUPID;
493 }
494 
to_json()495 const wxString Model_Checking::Full_Data::to_json()
496 {
497     json::Object o;
498     Model_Checking::Data::to_json(o);
499     o[L"ACCOUNTNAME"] = json::String(this->ACCOUNTNAME.ToStdWstring());
500     if (is_transfer(this))
501         o[L"TOACCOUNTNAME"] = json::String(this->TOACCOUNTNAME.ToStdWstring());
502     else
503         o[L"PAYEENAME"] = json::String(this->PAYEENAME.ToStdWstring());
504 
505     if (this->has_split())
506     {
507         json::Array a;
508         for (const auto & item : m_splits)
509         {
510             json::Object s;
511             const std::wstring categ = Model_Category::full_name(item.CATEGID, item.SUBCATEGID).ToStdWstring();
512             s[categ] = json::Number(item.SPLITTRANSAMOUNT);
513             a.Insert(s);
514         }
515         o[L"CATEGS"] = json::Array(a);
516     }
517     else
518         o[L"CATEG"] = json::String(Model_Category::full_name(this->CATEGID, this->SUBCATEGID).ToStdWstring());
519 
520     std::wstringstream ss;
521     json::Writer::Write(o, ss);
522 
523     return ss.str();
524 }
525