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