1 /***************************************************************************
2 * SPDX-FileCopyrightText: 2021 S. MANKOWSKI stephane@mankowski.fr
3 * SPDX-FileCopyrightText: 2021 G. DE BURE support@mankowski.fr
4 * SPDX-License-Identifier: GPL-3.0-or-later
5 ***************************************************************************/
6 /** @file
7 * This file implements classes SKGAccountObject.
8 *
9 * @author Stephane MANKOWSKI / Guillaume DE BURE
10 */
11 #include "skgaccountobject.h"
12
13 #include <klocalizedstring.h>
14
15 #include "skgbankobject.h"
16 #include "skgdocumentbank.h"
17 #include "skginterestobject.h"
18 #include "skgoperationobject.h"
19 #include "skgpayeeobject.h"
20 #include "skgsuboperationobject.h"
21 #include "skgtraces.h"
22 #include "skgunitobject.h"
23
factorial(int n)24 int factorial(int n)
25 {
26 return (n == 1 || n == 0) ? 1 : factorial(n - 1) * n;
27 }
28
SKGAccountObject()29 SKGAccountObject::SKGAccountObject() : SKGAccountObject(nullptr, 0) {}
30
SKGAccountObject(SKGDocument * iDocument,int iID)31 SKGAccountObject::SKGAccountObject(SKGDocument* iDocument, int iID) : SKGNamedObject(iDocument, QStringLiteral("v_account"), iID) {}
32
33 SKGAccountObject::~SKGAccountObject() = default;
34
35 SKGAccountObject::SKGAccountObject(const SKGAccountObject& iObject)
36 = default;
37
SKGAccountObject(const SKGNamedObject & iObject)38 SKGAccountObject::SKGAccountObject(const SKGNamedObject& iObject)
39 : SKGNamedObject(iObject.getDocument(), QStringLiteral("v_account"), iObject.getID())
40 {
41 if (iObject.getRealTable() == QStringLiteral("account")) {
42 copyFrom(iObject);
43 } else {
44 *this = SKGNamedObject(iObject.getDocument(), QStringLiteral("v_account"), iObject.getID());
45 }
46 }
47
SKGAccountObject(const SKGObjectBase & iObject)48 SKGAccountObject::SKGAccountObject(const SKGObjectBase& iObject)
49 {
50 if (iObject.getRealTable() == QStringLiteral("account")) {
51 copyFrom(iObject);
52 } else {
53 *this = SKGNamedObject(iObject.getDocument(), QStringLiteral("v_account"), iObject.getID());
54 }
55 }
56
operator =(const SKGObjectBase & iObject)57 SKGAccountObject& SKGAccountObject::operator= (const SKGObjectBase& iObject)
58 {
59 copyFrom(iObject);
60 return *this;
61 }
62
operator =(const SKGAccountObject & iObject)63 SKGAccountObject& SKGAccountObject::operator= (const SKGAccountObject& iObject)
64 {
65 copyFrom(iObject);
66 return *this;
67 }
68
setInitialBalance(double iBalance,const SKGUnitObject & iUnit)69 SKGError SKGAccountObject::setInitialBalance(double iBalance, const SKGUnitObject& iUnit)
70 {
71 SKGError err;
72 SKGTRACEINFUNCRC(10, err)
73 if (getDocument() != nullptr) {
74 // Delete previous initial balance for this account
75 err = getDocument()->executeSqliteOrder("DELETE FROM operation WHERE d_date='0000-00-00' AND rd_account_id=" % SKGServices::intToString(getID()));
76
77 // Creation of new initial balance
78 IFOK(err) {
79 SKGOperationObject initialBalanceOp;
80 err = addOperation(initialBalanceOp, true);
81 IFOKDO(err, initialBalanceOp.setAttribute(QStringLiteral("d_date"), QStringLiteral("0000-00-00")))
82 IFOKDO(err, initialBalanceOp.setUnit(iUnit))
83 IFOKDO(err, initialBalanceOp.setStatus(SKGOperationObject::CHECKED))
84 IFOKDO(err, initialBalanceOp.save())
85
86 SKGSubOperationObject initialBalanceSubOp;
87 IFOKDO(err, initialBalanceOp.addSubOperation(initialBalanceSubOp))
88 IFOKDO(err, initialBalanceSubOp.setAttribute(QStringLiteral("d_date"), QStringLiteral("0000-00-00")))
89 IFOKDO(err, initialBalanceSubOp.setQuantity(iBalance))
90 IFOKDO(err, initialBalanceSubOp.save())
91 }
92 }
93 return err;
94 }
95
getInitialBalance(double & oBalance,SKGUnitObject & oUnit)96 SKGError SKGAccountObject::getInitialBalance(double& oBalance, SKGUnitObject& oUnit)
97 {
98 SKGError err;
99 SKGTRACEINFUNCRC(10, err)
100 // Initialisation
101 oBalance = 0;
102 oUnit = SKGUnitObject();
103 QString unitName = qobject_cast<SKGDocumentBank*>(getDocument())->getPrimaryUnit().Symbol;
104
105 // Get initial balance
106 SKGStringListList listTmp;
107 err = getDocument()->executeSelectSqliteOrder("SELECT f_QUANTITY, t_UNIT FROM v_operation_tmp1 WHERE d_date='0000-00-00' AND rd_account_id=" % SKGServices::intToString(getID()), listTmp);
108 if (!err && listTmp.count() > 1) {
109 oBalance = SKGServices::stringToDouble(listTmp.at(1).at(0));
110 unitName = listTmp.at(1).at(1);
111
112 oUnit = SKGUnitObject(getDocument());
113 err = oUnit.setSymbol(unitName);
114 IFOKDO(err, oUnit.load())
115 }
116 return err;
117 }
118
setBank(const SKGBankObject & iBank)119 SKGError SKGAccountObject::setBank(const SKGBankObject& iBank)
120 {
121 return setAttribute(QStringLiteral("rd_bank_id"), SKGServices::intToString(iBank.getID()));
122 }
123
getBank(SKGBankObject & oBank) const124 SKGError SKGAccountObject::getBank(SKGBankObject& oBank) const
125 {
126 SKGError err = getDocument()->getObject(QStringLiteral("v_bank"), "id=" % getAttribute(QStringLiteral("rd_bank_id")), oBank);
127 return err;
128 }
129
setLinkedAccount(const SKGAccountObject & iAccount)130 SKGError SKGAccountObject::setLinkedAccount(const SKGAccountObject& iAccount)
131 {
132 return setAttribute(QStringLiteral("r_account_id"), SKGServices::intToString(iAccount.getID()));
133 }
134
getLinkedAccount(SKGAccountObject & oAccount) const135 SKGError SKGAccountObject::getLinkedAccount(SKGAccountObject& oAccount) const
136 {
137 SKGError err = getDocument()->getObject(QStringLiteral("v_account"), "id=" % getAttribute(QStringLiteral("r_account_id")), oAccount);
138 return err;
139 }
140
getLinkedByAccounts(SKGListSKGObjectBase & oAccounts) const141 SKGError SKGAccountObject::getLinkedByAccounts(SKGListSKGObjectBase& oAccounts) const
142 {
143 SKGError err;
144 if (getDocument() != nullptr) {
145 err = getDocument()->getObjects(QStringLiteral("v_account"),
146 "r_account_id=" % SKGServices::intToString(getID()),
147 oAccounts);
148 }
149 return err;
150 }
151
setNumber(const QString & iNumber)152 SKGError SKGAccountObject::setNumber(const QString& iNumber)
153 {
154 return setAttribute(QStringLiteral("t_number"), iNumber);
155 }
156
getNumber() const157 QString SKGAccountObject::getNumber() const
158 {
159 return getAttribute(QStringLiteral("t_number"));
160 }
161
setComment(const QString & iComment)162 SKGError SKGAccountObject::setComment(const QString& iComment)
163 {
164 return setAttribute(QStringLiteral("t_comment"), iComment);
165 }
166
getComment() const167 QString SKGAccountObject::getComment() const
168 {
169 return getAttribute(QStringLiteral("t_comment"));
170 }
171
setAgencyNumber(const QString & iNumber)172 SKGError SKGAccountObject::setAgencyNumber(const QString& iNumber)
173 {
174 return setAttribute(QStringLiteral("t_agency_number"), iNumber);
175 }
176
getAgencyNumber() const177 QString SKGAccountObject::getAgencyNumber() const
178 {
179 return getAttribute(QStringLiteral("t_agency_number"));
180 }
181
setAgencyAddress(const QString & iAddress)182 SKGError SKGAccountObject::setAgencyAddress(const QString& iAddress)
183 {
184 return setAttribute(QStringLiteral("t_agency_address"), iAddress);
185 }
186
getAgencyAddress() const187 QString SKGAccountObject::getAgencyAddress() const
188 {
189 return getAttribute(QStringLiteral("t_agency_address"));
190 }
191
addOperation(SKGOperationObject & oOperation,bool iForce)192 SKGError SKGAccountObject::addOperation(SKGOperationObject& oOperation, bool iForce)
193 {
194 SKGError err;
195 if (getID() == 0) {
196 err = SKGError(ERR_FAIL, i18nc("Error message", "%1 failed because linked object is not yet saved in the database.", QStringLiteral("SKGAccountObject::addOperation")));
197 } else {
198 oOperation = SKGOperationObject(getDocument());
199 err = oOperation.setParentAccount(*this, iForce);
200 }
201 return err;
202 }
203
getNbOperation() const204 int SKGAccountObject::getNbOperation() const
205 {
206 int nb = 0;
207 if (getDocument() != nullptr) {
208 getDocument()->getNbObjects(QStringLiteral("operation"), "rd_account_id=" % SKGServices::intToString(getID()), nb);
209 }
210 return nb;
211 }
212
getOperations(SKGListSKGObjectBase & oOperations) const213 SKGError SKGAccountObject::getOperations(SKGListSKGObjectBase& oOperations) const
214 {
215 SKGError err;
216 if (getDocument() != nullptr) {
217 err = getDocument()->getObjects(QStringLiteral("v_operation"),
218 "rd_account_id=" % SKGServices::intToString(getID()),
219 oOperations);
220 }
221 return err;
222 }
223
getCurrentAmount() const224 double SKGAccountObject::getCurrentAmount() const
225 {
226 return SKGServices::stringToDouble(getAttributeFromView(QStringLiteral("v_account_amount"), QStringLiteral("f_CURRENTAMOUNT")));
227 }
228
getAmount(QDate iDate,bool iOnlyCurrencies) const229 double SKGAccountObject::getAmount(QDate iDate, bool iOnlyCurrencies) const
230 {
231 SKGTRACEINFUNC(10)
232 double output = 0;
233 if (getDocument() != nullptr) {
234 // Search result in cache
235 QString ids = SKGServices::intToString(getID());
236 QString dates = SKGServices::dateToSqlString(iDate);
237 QString key = "getamount-" % ids % '-' % dates;
238 QString val = getDocument()->getCachedValue(key);
239 if (val.isEmpty()) {
240 SKGStringListList listTmp;
241 SKGError err = getDocument()->executeSelectSqliteOrder("SELECT TOTAL(f_QUANTITY), rc_unit_id FROM v_operation_tmp1 WHERE "
242 "d_date<='" % dates % "' AND t_template='N' AND rd_account_id=" % ids %
243 (iOnlyCurrencies ? " AND t_TYPEUNIT IN ('1', '2', 'C')" : "") %
244 " GROUP BY rc_unit_id",
245 listTmp);
246 int nb = listTmp.count();
247 for (int i = 1; !err && i < nb ; ++i) {
248 QString quantity = listTmp.at(i).at(0);
249 QString unitid = listTmp.at(i).at(1);
250
251 double coef = 1;
252 QString val2 = getDocument()->getCachedValue("unitvalue-" % unitid);
253 if (!val2.isEmpty()) {
254 // Yes
255 coef = SKGServices::stringToDouble(val2);
256 } else {
257 // No
258 SKGUnitObject unit(getDocument(), SKGServices::stringToInt(unitid));
259 if (unit.getType() != SKGUnitObject::PRIMARY) {
260 coef = unit.getAmount(iDate);
261 }
262 }
263
264 output += coef * SKGServices::stringToDouble(quantity);
265 }
266 getDocument()->addValueInCache(key, SKGServices::doubleToString(output));
267 } else {
268 output = SKGServices::stringToDouble(val);
269 }
270 }
271 return output;
272 }
273
setType(SKGAccountObject::AccountType iType)274 SKGError SKGAccountObject::setType(SKGAccountObject::AccountType iType)
275 {
276 return setAttribute(QStringLiteral("t_type"), (iType == CURRENT ? QStringLiteral("C") :
277 (iType == CREDITCARD ? QStringLiteral("D") :
278 (iType == ASSETS ? QStringLiteral("A") :
279 (iType == INVESTMENT ? QStringLiteral("I") :
280 (iType == WALLET ? QStringLiteral("W") :
281 (iType == PENSION ? QStringLiteral("P") :
282 (iType == LOAN ? QStringLiteral("L") :
283 (iType == SAVING ? QStringLiteral("S") :
284 QStringLiteral("O"))))))))));
285 }
286
getType() const287 SKGAccountObject::AccountType SKGAccountObject::getType() const
288 {
289 QString typeString = getAttribute(QStringLiteral("t_type"));
290 return (typeString == QStringLiteral("C") ? CURRENT :
291 (typeString == QStringLiteral("D") ? CREDITCARD :
292 (typeString == QStringLiteral("A") ? ASSETS :
293 (typeString == QStringLiteral("I") ? INVESTMENT :
294 (typeString == QStringLiteral("W") ? WALLET :
295 (typeString == QStringLiteral("P") ? PENSION :
296 (typeString == QStringLiteral("L") ? LOAN :
297 (typeString == QStringLiteral("S") ? SAVING : OTHER))))))));
298 }
299
setClosed(bool iClosed)300 SKGError SKGAccountObject::setClosed(bool iClosed)
301 {
302 return setAttribute(QStringLiteral("t_close"), iClosed ? QStringLiteral("Y") : QStringLiteral("N"));
303 }
304
isClosed() const305 bool SKGAccountObject::isClosed() const
306 {
307 return (getAttribute(QStringLiteral("t_close")) == QStringLiteral("Y"));
308 }
309
bookmark(bool iBookmark)310 SKGError SKGAccountObject::bookmark(bool iBookmark)
311 {
312 return setAttribute(QStringLiteral("t_bookmarked"), iBookmark ? QStringLiteral("Y") : QStringLiteral("N"));
313 }
314
isBookmarked() const315 bool SKGAccountObject::isBookmarked() const
316 {
317 return (getAttribute(QStringLiteral("t_bookmarked")) == QStringLiteral("Y"));
318 }
319
maxLimitAmountEnabled(bool iEnabled)320 SKGError SKGAccountObject::maxLimitAmountEnabled(bool iEnabled)
321 {
322 return setAttribute(QStringLiteral("t_maxamount_enabled"), iEnabled ? QStringLiteral("Y") : QStringLiteral("N"));
323 }
324
isMaxLimitAmountEnabled() const325 bool SKGAccountObject::isMaxLimitAmountEnabled() const
326 {
327 return (getAttribute(QStringLiteral("t_maxamount_enabled")) == QStringLiteral("Y"));
328 }
329
setMaxLimitAmount(double iAmount)330 SKGError SKGAccountObject::setMaxLimitAmount(double iAmount)
331 {
332 SKGError err = setAttribute(QStringLiteral("f_maxamount"), SKGServices::doubleToString(iAmount));
333 if (!err && getMinLimitAmount() > iAmount) {
334 err = setMinLimitAmount(iAmount);
335 }
336 return err;
337 }
338
getMaxLimitAmount() const339 double SKGAccountObject::getMaxLimitAmount() const
340 {
341 return SKGServices::stringToDouble(getAttribute(QStringLiteral("f_maxamount")));
342 }
343
minLimitAmountEnabled(bool iEnabled)344 SKGError SKGAccountObject::minLimitAmountEnabled(bool iEnabled)
345 {
346 return setAttribute(QStringLiteral("t_minamount_enabled"), iEnabled ? QStringLiteral("Y") : QStringLiteral("N"));
347 }
348
isMinLimitAmountEnabled() const349 bool SKGAccountObject::isMinLimitAmountEnabled() const
350 {
351 return (getAttribute(QStringLiteral("t_minamount_enabled")) == QStringLiteral("Y"));
352 }
353
setMinLimitAmount(double iAmount)354 SKGError SKGAccountObject::setMinLimitAmount(double iAmount)
355 {
356 SKGError err = setAttribute(QStringLiteral("f_minamount"), SKGServices::doubleToString(iAmount));
357 if (!err && getMaxLimitAmount() < iAmount) {
358 err = setMaxLimitAmount(iAmount);
359 }
360 return err;
361 }
362
getMinLimitAmount() const363 double SKGAccountObject::getMinLimitAmount() const
364 {
365 return SKGServices::stringToDouble(getAttribute(QStringLiteral("f_minamount")));
366 }
367
setReconciliationDate(QDate iDate)368 SKGError SKGAccountObject::setReconciliationDate(QDate iDate)
369 {
370 return setAttribute(QStringLiteral("d_reconciliationdate"), SKGServices::dateToSqlString(iDate));
371 }
372
getReconciliationDate() const373 QDate SKGAccountObject::getReconciliationDate() const
374 {
375 return SKGServices::stringToTime(getAttribute(QStringLiteral("d_reconciliationdate"))).date();
376 }
377
setReconciliationBalance(double iAmount)378 SKGError SKGAccountObject::setReconciliationBalance(double iAmount)
379 {
380 return setAttribute(QStringLiteral("f_reconciliationbalance"), SKGServices::doubleToString(iAmount));
381 }
382
getReconciliationBalance() const383 double SKGAccountObject::getReconciliationBalance() const
384 {
385 return SKGServices::stringToDouble(getAttribute(QStringLiteral("f_reconciliationbalance")));
386 }
387
getUnit(SKGUnitObject & oUnit) const388 SKGError SKGAccountObject::getUnit(SKGUnitObject& oUnit) const
389 {
390 // Get initial amount
391 SKGStringListList listTmp;
392 SKGError err = getDocument()->executeSelectSqliteOrder("SELECT t_UNIT FROM v_suboperation_consolidated WHERE d_date='0000-00-00' AND rd_account_id=" % SKGServices::intToString(getID()), listTmp);
393 IFOK(err) {
394 // Is initial amount existing ?
395 if (listTmp.count() > 1) {
396 // Yes ==> then the amount is the amount of the initial value
397 oUnit = SKGUnitObject(getDocument());
398 err = oUnit.setSymbol(listTmp.at(1).at(0));
399 IFOKDO(err, oUnit.load())
400 } else {
401 // No ==> we get the preferred unit
402 SKGObjectBase::SKGListSKGObjectBase units;
403 err = getDocument()->getObjects(QStringLiteral("v_unit"),
404 "t_type IN ('1', '2', 'C') AND EXISTS(SELECT 1 FROM operation WHERE rc_unit_id=v_unit.id AND rd_account_id=" % SKGServices::intToString(getID()) % ") ORDER BY t_type", units);
405 int nb = units.count();
406 if (nb != 0) {
407 oUnit = units.at(0);
408 }
409 }
410 }
411 return err;
412 }
413
addInterest(SKGInterestObject & oInterest)414 SKGError SKGAccountObject::addInterest(SKGInterestObject& oInterest)
415 {
416 SKGError err;
417 if (getID() == 0) {
418 err = SKGError(ERR_FAIL, i18nc("Error message", "%1 failed because linked object is not yet saved in the database.", QStringLiteral("SKGAccountObject::addInterest")));
419 } else {
420 oInterest = SKGInterestObject(qobject_cast<SKGDocumentBank*>(getDocument()));
421 err = oInterest.setAccount(*this);
422 }
423 return err;
424 }
425
getInterests(SKGListSKGObjectBase & oInterestList) const426 SKGError SKGAccountObject::getInterests(SKGListSKGObjectBase& oInterestList) const
427 {
428 SKGError err = getDocument()->getObjects(QStringLiteral("v_interest"),
429 "rd_account_id=" % SKGServices::intToString(getID()),
430 oInterestList);
431 return err;
432 }
433
getInterest(QDate iDate,SKGInterestObject & oInterest) const434 SKGError SKGAccountObject::getInterest(QDate iDate, SKGInterestObject& oInterest) const
435 {
436 QString ids = SKGServices::intToString(getID());
437 QString dates = SKGServices::dateToSqlString(iDate);
438 SKGError err = SKGObjectBase::getDocument()->getObject(QStringLiteral("v_interest"),
439 "rd_account_id=" % ids % " AND d_date<='" % dates %
440 "' AND ABS(strftime('%s','" % dates %
441 "')-strftime('%s',d_date))=(SELECT MIN(ABS(strftime('%s','" % dates %
442 "')-strftime('%s',u2.d_date))) FROM interest u2 WHERE u2.rd_account_id=" % ids %
443 " AND u2.d_date<='" % dates % "')",
444 oInterest);
445
446 // If not found then get first
447 IFKO(err) err = SKGObjectBase::getDocument()->getObject(QStringLiteral("v_interest"),
448 "rd_account_id=" % SKGServices::intToString(getID()) % " AND d_date=(SELECT MIN(d_date) FROM interest WHERE rd_account_id=" %
449 SKGServices::intToString(getID()) % ')',
450 oInterest);
451 return err;
452 }
453
getInterestItems(SKGAccountObject::SKGInterestItemList & oInterestList,double & oInterests,int iYear) const454 SKGError SKGAccountObject::getInterestItems(SKGAccountObject::SKGInterestItemList& oInterestList, double& oInterests, int iYear) const
455 {
456 oInterestList.clear();
457 SKGError err;
458
459 // Initial date
460 int y = iYear;
461 if (y == 0) {
462 y = QDate::currentDate().year();
463 }
464 QDate initialDate = QDate(y, 1, 1);
465 QDate lastDate = QDate(y, 12, 31);
466
467 oInterests = 0;
468 bool computationNeeded = false;
469
470 // Add operations
471 SKGObjectBase::SKGListSKGObjectBase items;
472 err = getDocument()->getObjects(QStringLiteral("v_operation"), "rd_account_id=" % SKGServices::intToString(getID()) %
473 " AND t_template='N' AND t_TYPEUNIT IN ('1', '2', 'C')"
474 " AND d_date>='" % SKGServices::dateToSqlString(initialDate) % "' "
475 " AND d_date<='" % SKGServices::dateToSqlString(lastDate) % "' ORDER BY d_date", items);
476 int nb = items.count();
477 for (int i = 0; !err && i < nb; ++i) {
478 SKGOperationObject ob(items.at(i));
479
480 SKGInterestItem itemI;
481 itemI.object = ob;
482 itemI.date = ob.getDate();
483 itemI.valueDate = itemI.date;
484 itemI.rate = 0;
485 itemI.base = 0;
486 itemI.coef = 0;
487 itemI.annualInterest = 0;
488 itemI.accruedInterest = 0;
489 itemI.amount = ob.getCurrentAmount();
490
491 oInterestList.push_back(itemI);
492 }
493
494 // Add interest
495 IFOK(err) {
496 err = getDocument()->getObjects(QStringLiteral("v_interest"), "rd_account_id=" % SKGServices::intToString(getID()) %
497 " AND d_date>='" % SKGServices::dateToSqlString(initialDate) % "' "
498 " AND d_date<='" % SKGServices::dateToSqlString(lastDate) % "' ORDER BY d_date", items);
499
500 int pos = 0;
501 int nb2 = items.count();
502 for (int i = 0; !err && i < nb2; ++i) {
503 SKGInterestObject ob(items.at(i));
504
505 SKGInterestItem itemI;
506 itemI.object = ob;
507 itemI.date = ob.getDate();
508 itemI.valueDate = itemI.date;
509 itemI.rate = ob.getRate();
510 itemI.base = SKGServices::stringToInt(ob.getAttribute(QStringLiteral("t_base")));
511 itemI.coef = 0;
512 itemI.annualInterest = 0;
513 itemI.accruedInterest = 0;
514 itemI.amount = 0;
515
516 int nb3 = oInterestList.count();
517 for (int j = pos; !err && j < nb3; ++j) {
518 if (itemI.date <= oInterestList.at(j).date) {
519 break;
520 }
521 ++pos;
522 }
523
524 oInterestList.insert(pos, itemI);
525 computationNeeded = true;
526 }
527 }
528
529 // Get first interest
530 IFOK(err) {
531 SKGInterestObject firstInterest;
532 if (getInterest(initialDate, firstInterest).isSucceeded()) {
533 if (firstInterest.getDate() < initialDate) {
534 SKGInterestItem itemI;
535 itemI.object = firstInterest;
536 itemI.date = initialDate;
537 itemI.valueDate = initialDate;
538 itemI.rate = firstInterest.getRate();
539 itemI.base = 0;
540 itemI.coef = 0;
541 itemI.annualInterest = 0;
542 itemI.accruedInterest = 0;
543 itemI.amount = 0;
544
545 oInterestList.insert(0, itemI);
546 computationNeeded = true;
547 }
548 }
549 }
550
551 // Launch computation
552 IFOK(err) {
553 if (computationNeeded) {
554 err = computeInterestItems(oInterestList, oInterests, y);
555 } else {
556 // Drop temporary table
557 IFOKDO(err, getDocument()->executeSqliteOrder(QStringLiteral("DROP TABLE IF EXISTS interest_result")))
558 // Create fake table
559 IFOKDO(err, getDocument()->executeSqliteOrder(QStringLiteral("CREATE TEMP TABLE interest_result(a)")))
560 }
561 }
562 return err;
563 }
564
computeInterestItems(SKGAccountObject::SKGInterestItemList & ioInterestList,double & oInterests,int iYear) const565 SKGError SKGAccountObject::computeInterestItems(SKGAccountObject::SKGInterestItemList& ioInterestList, double& oInterests, int iYear) const
566 {
567 SKGError err;
568
569 // Sum annual interest
570 oInterests = 0;
571
572 // Initial date
573 int y = iYear;
574 if (y == 0) {
575 y = QDate::currentDate().year();
576 }
577 QDate initialDate = QDate(y, 1, 1);
578
579 // Default interest item
580 SKGInterestItem currentInterest;
581 currentInterest.date = initialDate;
582 currentInterest.valueDate = currentInterest.date;
583 currentInterest.rate = 0;
584 currentInterest.coef = 0;
585 currentInterest.annualInterest = 0;
586 currentInterest.accruedInterest = 0;
587
588 int nb = ioInterestList.count();
589 for (int i = 0; !err && i < nb; ++i) {
590 SKGInterestItem tmp = ioInterestList.at(i);
591 SKGObjectBase object = tmp.object;
592 if (object.getRealTable() == QStringLiteral("operation")) {
593 // Get operations
594 SKGOperationObject op(object);
595
596 // Get current amount
597 tmp.amount = op.getCurrentAmount();
598
599 // Get value date computation mode
600 SKGInterestObject::ValueDateMode valueMode = SKGInterestObject::FIFTEEN;
601 SKGInterestObject::InterestMode baseMode = SKGInterestObject::FIFTEEN24;
602 if (currentInterest.object.getRealTable() == QStringLiteral("interest")) {
603 SKGInterestObject interestObj(currentInterest.object);
604 valueMode = (tmp.amount >= 0 ? interestObj.getIncomeValueDateMode() : interestObj.getExpenditueValueDateMode());
605 baseMode = interestObj.getInterestComputationMode();
606
607 tmp.rate = interestObj.getRate();
608 }
609
610 // Compute value date
611 if (object.getRealTable() == QStringLiteral("operation")) {
612 if (valueMode == SKGInterestObject::FIFTEEN) {
613 if (tmp.amount >= 0) {
614 if (tmp.date.day() <= 15) {
615 tmp.valueDate = tmp.date.addDays(16 - tmp.date.day());
616 } else {
617 tmp.valueDate = tmp.date.addMonths(1).addDays(1 - tmp.date.day());
618 }
619 } else {
620 if (tmp.date.day() <= 15) {
621 tmp.valueDate = tmp.date.addDays(1 - tmp.date.day());
622 } else {
623 tmp.valueDate = tmp.date.addDays(16 - tmp.date.day());
624 }
625 }
626 } else {
627 tmp.valueDate = tmp.date.addDays(tmp.amount >= 0 ? (static_cast<int>(valueMode)) - 1 : - (static_cast<int>(valueMode)) + 1);
628 }
629 }
630
631 // Compute coef
632 if (baseMode == SKGInterestObject::DAYS365) {
633 QDate last(tmp.date.year(), 12, 31);
634 tmp.coef = tmp.valueDate.daysTo(last) + 1;
635 tmp.coef /= 365;
636 } else if (baseMode == SKGInterestObject::DAYS360) {
637 QDate last(tmp.date.year(), 12, 31);
638 tmp.coef = 360 * (last.year() - tmp.valueDate.year()) + 30 * (last.month() - tmp.valueDate.month()) + (last.day() - tmp.valueDate.day());
639 tmp.coef /= 360;
640 } else {
641 tmp.coef = 2 * (12 - tmp.valueDate.month()) + (tmp.valueDate.day() <= 15 ? 2 : 1);
642 tmp.coef /= 24;
643 }
644 if (tmp.valueDate.year() != iYear) {
645 tmp.coef = 0;
646 }
647
648 // Compute annual interest
649 tmp.annualInterest = tmp.amount * tmp.coef * tmp.rate / 100;
650
651 } else if (object.getRealTable() == QStringLiteral("interest")) {
652 // Compute coef
653 if (tmp.base == 365) {
654 QDate last(tmp.date.year(), 12, 31);
655 tmp.coef = tmp.valueDate.daysTo(last) + 1;
656 tmp.coef /= 365;
657 } else if (tmp.base == 360) {
658 QDate last(tmp.date.year(), 12, 31);
659 tmp.coef = 360 * (last.year() - tmp.valueDate.year()) + 30 * (last.month() - tmp.valueDate.month()) + (last.day() - tmp.valueDate.day());
660 tmp.coef /= 360;
661 } else {
662 tmp.coef = 2 * (12 - tmp.valueDate.month()) + (tmp.valueDate.day() <= 15 ? 2 : 1);
663 tmp.coef /= 24;
664 }
665 if (tmp.valueDate.year() != iYear) {
666 tmp.coef = 0;
667 }
668
669 // Compute annual interest
670 // BUG 329568: We must ignore operations of the day
671 tmp.amount = getAmount(tmp.valueDate.addDays(-1), true);
672 tmp.annualInterest = tmp.amount * tmp.coef * (tmp.rate - currentInterest.rate) / 100;
673
674 currentInterest = tmp;
675 }
676
677 // Compute sum
678 oInterests += tmp.annualInterest;
679
680 // Compute accrued interest
681 tmp.accruedInterest = oInterests - getAmount(tmp.date, true) * tmp.coef * tmp.rate / 100;
682
683 ioInterestList[i] = tmp;
684 }
685
686 // Create temporary table
687 IFOK(err) {
688 QStringList sqlOrders;
689 sqlOrders << QStringLiteral("DROP TABLE IF EXISTS interest_result")
690 << QStringLiteral("CREATE TEMP TABLE interest_result("
691 "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,"
692 "d_date DATE NOT NULL,"
693 "d_valuedate DATE NOT NULL,"
694 "t_comment TEXT NOT NULL DEFAULT '',"
695 "f_currentamount FLOAT NOT NULL DEFAULT 0,"
696 "f_coef FLOAT NOT NULL DEFAULT 0,"
697 "f_rate FLOAT NOT NULL DEFAULT 0,"
698 "f_annual_interest FLOAT NOT NULL DEFAULT 0,"
699 "f_accrued_interest FLOAT NOT NULL DEFAULT 0"
700 ")");
701 err = getDocument()->executeSqliteOrders(sqlOrders);
702
703 // Fill table
704 int nb2 = ioInterestList.count();
705 for (int i = 0; !err && i < nb2; ++i) {
706 SKGInterestItem interest = ioInterestList.at(i);
707 SKGObjectBase object = interest.object;
708 QString sqlinsert =
709 "INSERT INTO interest_result (d_date,d_valuedate,t_comment,f_currentamount,f_coef,f_rate,f_annual_interest,f_accrued_interest) "
710 " VALUES ('" % SKGServices::dateToSqlString(interest.date) %
711 "','" % SKGServices::dateToSqlString(interest.valueDate) %
712 "','" % SKGServices::stringToSqlString(object.getRealTable() == QStringLiteral("operation") ? i18nc("Noun", "Relative to operation '%1'", SKGOperationObject(object).getDisplayName()) : i18nc("Noun", "Rate change")) %
713 "'," % SKGServices::doubleToString(interest.amount) %
714 ',' % SKGServices::doubleToString(interest.coef) %
715 ',' % SKGServices::doubleToString(interest.rate) %
716 ',' % SKGServices::doubleToString(interest.annualInterest) %
717 ',' % SKGServices::doubleToString(interest.accruedInterest) %
718 ")";
719
720 err = getDocument()->executeSqliteOrder(sqlinsert);
721 }
722 }
723 return err;
724 }
725
transferDeferredOperations(const SKGAccountObject & iTargetAccount,QDate iDate)726 SKGError SKGAccountObject::transferDeferredOperations(const SKGAccountObject& iTargetAccount, QDate iDate)
727 {
728 SKGError err;
729 SKGTRACEINFUNCRC(10, err)
730 //
731 auto* doc = qobject_cast<SKGDocumentBank*>(getDocument());
732 if (doc != nullptr) {
733 // Get pointed operations
734 SKGObjectBase::SKGListSKGObjectBase operations;
735 IFOKDO(err, getDocument()->getObjects(QStringLiteral("v_operation"), "rd_account_id=" % SKGServices::intToString(getID()) % " AND t_status='P'", operations))
736 int nb = operations.count();
737 if (nb != 0) {
738 SKGOperationObject mergedOperations;
739 SKGOperationObject balancedOperations;
740 for (int i = 0; !err && i < nb; ++i) {
741 SKGOperationObject op(operations.at(i));
742
743 // Create the balance operation
744 SKGOperationObject opdup;
745 IFOKDO(err, op.duplicate(opdup, iDate))
746
747 SKGListSKGObjectBase subops;
748 IFOKDO(err, opdup.getSubOperations(subops))
749 int nbsupops = subops.count();
750 for (int j = 0; !err && j < nbsupops; ++j) {
751 SKGSubOperationObject subop(subops.at(j));
752 IFOKDO(err, subop.setDate(op.getDate()))
753 IFOKDO(err, subop.setQuantity(-subop.getQuantity()))
754 IFOKDO(err, subop.save())
755 }
756
757 if (i == 0) {
758 mergedOperations = opdup;
759 } else {
760 IFOKDO(err, mergedOperations.mergeSuboperations(opdup))
761 }
762
763 // Create the duplicate in target account
764 SKGOperationObject opduptarget;
765 IFOKDO(err, op.duplicate(opduptarget))
766 IFOKDO(err, opduptarget.setDate(op.getDate()))
767 IFOKDO(err, opduptarget.setParentAccount(iTargetAccount))
768 IFOKDO(err, opduptarget.setImported(op.isImported()))
769 IFOKDO(err, opduptarget.setImportID(op.getImportID()))
770 IFOKDO(err, opduptarget.setGroupOperation(mergedOperations))
771 IFOKDO(err, opduptarget.setStatus(SKGOperationObject::POINTED))
772 IFOKDO(err, opduptarget.save())
773 IFOKDO(err, mergedOperations.load()) // To reload the modif done by the setGroupOperation
774
775 // Check the operation
776 IFOKDO(err, op.setStatus(SKGOperationObject::CHECKED))
777 IFOKDO(err, op.save())
778 }
779
780 // Check the balance operation
781 IFOKDO(err, mergedOperations.setPayee(SKGPayeeObject()))
782 IFOKDO(err, mergedOperations.setStatus(SKGOperationObject::CHECKED))
783 IFOKDO(err, mergedOperations.save())
784 }
785 }
786
787 return err;
788 }
789
getPossibleReconciliations(double iTargetBalance,bool iSearchAllPossibleReconciliation) const790 QVector< QVector<SKGOperationObject> > SKGAccountObject::getPossibleReconciliations(double iTargetBalance, bool iSearchAllPossibleReconciliation) const
791 {
792 SKGTRACEINFUNC(5)
793 QVector< QVector<SKGOperationObject> > output;
794 auto* doc = qobject_cast<SKGDocumentBank*>(getDocument());
795 if (doc != nullptr) {
796 // Get unit
797 SKGServices::SKGUnitInfo unit1 = doc->getPrimaryUnit();
798 SKGUnitObject unitAccount;
799 if (getUnit(unitAccount).isSucceeded()) {
800 if (!unitAccount.getSymbol().isEmpty()) {
801 unit1.Symbol = unitAccount.getSymbol();
802 unit1.Value = SKGServices::stringToDouble(unitAccount.getAttribute(QStringLiteral("f_CURRENTAMOUNT")));
803 }
804 }
805 SKGTRACEL(5) << "iTargetBalance=" << doc->formatMoney(iTargetBalance, unit1, false) << SKGENDL;
806
807 // Get balance of checked operations
808 QString balanceString;
809 getDocument()->executeSingleSelectSqliteOrder("SELECT f_CHECKED from v_account_display WHERE id=" % SKGServices::intToString(getID()), balanceString);
810 double balance = SKGServices::stringToDouble(balanceString);
811 SKGTRACEL(5) << "balance=" << doc->formatMoney(balance, unit1, false) << SKGENDL;
812
813
814 QString zero = doc->formatMoney(0, unit1, false);
815 QString negativezero = doc->formatMoney(-EPSILON, unit1, false);
816 QString sdiff = doc->formatMoney(balance - iTargetBalance * unit1.Value, unit1, false);
817 if (sdiff == zero || sdiff == negativezero) {
818 // This is an empty soluce
819 output.push_back(QVector<SKGOperationObject>());
820 SKGTRACEL(5) << "empty solution found !!!" << SKGENDL;
821 } else {
822 // Get all imported operation
823 SKGObjectBase::SKGListSKGObjectBase operations;
824 getDocument()->getObjects(QStringLiteral("v_operation"), "rd_account_id=" % SKGServices::intToString(getID()) % " AND t_status!='Y' AND t_template='N' AND t_imported IN ('Y','P') ORDER BY d_date, id", operations);
825 int nb = operations.count();
826
827 if (!iSearchAllPossibleReconciliation) {
828 // Check if all operations are a solution
829 double amount = 0.0;
830 QVector<SKGOperationObject> list;
831 list.reserve(nb);
832 for (int i = 0; i < nb; ++i) {
833 SKGOperationObject op(operations.at(i));
834 amount += op.getCurrentAmount();
835 list.push_back(op);
836 }
837 QString sdiff = doc->formatMoney(amount + balance - iTargetBalance * unit1.Value, unit1, false);
838 if (sdiff == zero || sdiff == negativezero) {
839 SKGTRACEL(5) << "all operations are a solution !!!" << SKGENDL;
840 output.push_back(list);
841 return output;
842 }
843 }
844
845 // Search
846 int nbmax = 500;
847 SKGTRACEL(5) << "Nb operations:" << nb << SKGENDL;
848 if (nb > nbmax) {
849 SKGTRACEL(5) << "Too many operations (" << nb << ") ==> Reducing the size of the computation" << SKGENDL;
850 for (int i = 0; i < nb - nbmax; ++i) {
851 SKGOperationObject op(operations.at(0));
852 auto amount = op.getCurrentAmount();
853 balance += amount;
854 operations.removeFirst();
855 }
856 }
857 output = getPossibleReconciliations(operations, balance, iTargetBalance, unit1, iSearchAllPossibleReconciliation);
858 }
859 }
860 return output;
861 }
862
getPossibleReconciliations(const SKGObjectBase::SKGListSKGObjectBase & iOperations,double iBalance,double iTargetBalance,const SKGServices::SKGUnitInfo & iUnit,bool iSearchAllPossibleReconciliation) const863 QVector< QVector<SKGOperationObject> > SKGAccountObject::getPossibleReconciliations(const SKGObjectBase::SKGListSKGObjectBase& iOperations, double iBalance, double iTargetBalance, const SKGServices::SKGUnitInfo& iUnit, bool iSearchAllPossibleReconciliation) const
864 {
865 SKGTRACEINFUNC(5)
866 QVector< QVector<SKGOperationObject> > output;
867 output.reserve(5);
868 auto* doc = qobject_cast<SKGDocumentBank*>(getDocument());
869 if (doc != nullptr) {
870 SKGTRACEL(5) << "iTargetBalance=" << doc->formatMoney(iTargetBalance, iUnit, false) << SKGENDL;
871
872 // Comparison
873 QString zero = doc->formatMoney(0, iUnit, false);
874 QString negativezero = doc->formatMoney(-EPSILON, iUnit, false);
875
876 // Check operations list
877 int nb = iOperations.count();
878 if (nb > 0) {
879 // Get all operations of the next date
880 QVector<SKGOperationObject> nextOperations;
881 nextOperations.reserve(iOperations.count());
882 QString date = iOperations.at(0).getAttribute(QStringLiteral("d_date"));
883 for (int i = 0; i < nb; ++i) {
884 SKGOperationObject op(iOperations.at(i));
885 if (op.getAttribute(QStringLiteral("d_date")) == date) {
886 nextOperations.push_back(op);
887 } else {
888 break;
889 }
890 }
891
892 // Get all combination of operations
893 int nbNext = nextOperations.count();
894 SKGTRACEL(5) << date << ":" << nbNext << " operations found" << SKGENDL;
895 std::vector<int> v(nbNext);
896 for (int i = 0; i < nbNext; ++i) {
897 v[i] = i;
898 }
899
900 double nextBalance = iBalance;
901 int index = 0;
902 if (nbNext > 7) {
903 SKGTRACEL(5) << "Too many combination: " << factorial(nbNext) << " ==> limited to 5040" << SKGENDL;
904 nbNext = 7;
905 }
906 nb = factorial(nbNext);
907 bool stopTests = false;
908 QVector<SKGOperationObject> combi;
909 QVector<double> combiAmount;
910 combi.reserve(nbNext);
911 combiAmount.reserve(nbNext);
912 do {
913 // Build the next combination
914 combi.resize(0);
915 combiAmount.resize(0);
916 double sumOperationPositives = 0.0;
917 double sumOperationsNegatives = 0.0;
918 for (int i = 0; i < nbNext; ++i) {
919 const SKGOperationObject& op = nextOperations.at(v[i]);
920 combi.push_back(op);
921 auto amount = op.getCurrentAmount();
922 combiAmount.push_back(amount);
923 if (Q_LIKELY(amount < 0)) {
924 sumOperationsNegatives += amount;
925 } else if (amount > 0) {
926 sumOperationPositives += amount;
927 }
928 }
929
930 // Test the combination
931 double diff = iBalance - iTargetBalance * iUnit.Value;
932 double previousDiff = diff;
933 SKGTRACEL(5) << "Check combination " << (index + 1) << "/" << nb << ": Diff=" << doc->formatMoney(diff, iUnit, false) << SKGENDL;
934
935 // Try to find an immediate soluce
936 int nbop = combi.count();
937 for (int j = 0; j < nbop; ++j) {
938 auto amount = combiAmount.at(j);
939 diff += amount;
940 if (Q_UNLIKELY(index == 0)) {
941 nextBalance += amount;
942 }
943 QString sdiff = doc->formatMoney(diff, iUnit, false);
944 SKGTRACEL(5) << (j + 1) << "/" << nbop << ": Amount=" << amount << " / New diff=" << sdiff << SKGENDL;
945 if (sdiff == zero || sdiff == negativezero) {
946 // This is a soluce
947 auto s = combi.mid(0, j + 1);
948 if (output.contains(s)) {
949 SKGTRACEL(5) << "found but already existing !!!" << SKGENDL;
950 } else {
951 output.push_back(s);
952 SKGTRACEL(5) << "found !!!" << SKGENDL;
953 if (j == nbop - 1 || iSearchAllPossibleReconciliation) {
954 // No need to test all combinations
955 SKGTRACEL(5) << "No need to test all combinations" << SKGENDL;
956 stopTests = true;
957 }
958 }
959 }
960 }
961
962 // Check if tests of all combinations can be cancelled
963 if ((previousDiff > 0 && previousDiff + sumOperationsNegatives > 0) || (previousDiff < 0 && previousDiff + sumOperationPositives < 0)) {
964 SKGTRACEL(5) << "No need to test all combinations due to signs of operations and diffs" << SKGENDL;
965 stopTests = true;
966 }
967
968 ++index;
969 } while (index < nb && std::next_permutation(v.begin(), v.end()) && !stopTests);
970
971 // Try to find next solutions
972 auto reconciliations = getPossibleReconciliations(iOperations.mid(nbNext), nextBalance, iTargetBalance, iUnit, iSearchAllPossibleReconciliation);
973 int nbReconciliations = reconciliations.count();
974 output.reserve(nbReconciliations + 5);
975 for (int i = 0; i < nbReconciliations; ++i) {
976 QVector<SKGOperationObject> output2 = nextOperations;
977 output2 = output2 << reconciliations.at(i);
978 output.push_back(output2);
979 }
980 }
981 }
982
983 SKGTRACEL(5) << output.count() << " soluces found" << SKGENDL;
984 if (!output.isEmpty()) {
985 SKGTRACEL(5) << "Size of the first soluce: " << output.at(0).count() << SKGENDL;
986 }
987 return output;
988 }
989
autoReconcile(double iBalance)990 SKGError SKGAccountObject::autoReconcile(double iBalance)
991 {
992 SKGError err;
993 SKGTRACEINFUNCRC(5, err)
994
995 // Soluces
996 auto soluces = getPossibleReconciliations(iBalance);
997 int nbSoluces = soluces.count();
998 if (nbSoluces > 0) {
999 if (nbSoluces > 1) {
1000 err = getDocument()->sendMessage(i18nc("An information message", "More than one solution is possible for this auto reconciliation."));
1001 }
1002
1003 // Choose the longest solution
1004 QVector<SKGOperationObject> soluce;
1005 int length = 0;
1006 for (int i = 0; i < nbSoluces; ++i) {
1007 const auto& s = soluces.at(i);
1008 int l = s.count();
1009 if (l > length) {
1010 soluce = s;
1011 length = l;
1012 }
1013 }
1014
1015 // Check all
1016 SKGTRACEL(5) << length << " operations pointed" << SKGENDL;
1017 for (int i = 0; i < length; ++i) {
1018 SKGOperationObject op(soluce.at(i));
1019 err = op.setStatus(SKGOperationObject::POINTED);
1020 IFOKDO(err, op.save(true, false))
1021 }
1022 } else {
1023 err = SKGError(ERR_FAIL, i18nc("Error message", "Can not find the imported operations for obtaining the expected final balance"),
1024 QString("skg://skrooge_operation_plugin/?title_icon=quickopen&title=" % SKGServices::encodeForUrl(i18nc("Noun, a list of items", "Operations of account \"%1\" used for auto reconciliation", getDisplayName())) %
1025 "&operationWhereClause=" % SKGServices::encodeForUrl("rd_account_id=" + SKGServices::intToString(getID()) + " AND t_template='N' AND ((t_status='N' AND t_imported IN ('Y','P')) OR t_status='Y')")));
1026 }
1027
1028 return err;
1029 }
1030
merge(const SKGAccountObject & iAccount,bool iMergeInitalBalance)1031 SKGError SKGAccountObject::merge(const SKGAccountObject& iAccount, bool iMergeInitalBalance)
1032 {
1033 SKGError err;
1034 SKGTRACEINFUNCRC(10, err)
1035
1036 // Get initial balances
1037 double balance1 = 0.0;
1038 SKGUnitObject unit1;
1039 err = getInitialBalance(balance1, unit1);
1040
1041 double balance2 = 0.0;
1042 SKGUnitObject unit2;
1043 if (iMergeInitalBalance) {
1044 IFOKDO(err, const_cast<SKGAccountObject*>(&iAccount)->getInitialBalance(balance2, unit2))
1045 }
1046
1047 // Transfer operations
1048 SKGObjectBase::SKGListSKGObjectBase ops;
1049 IFOKDO(err, iAccount.getOperations(ops))
1050 int nb = ops.count();
1051 for (int i = 0; !err && i < nb; ++i) {
1052 SKGOperationObject op(ops.at(i));
1053 err = op.setParentAccount(*this);
1054 IFOKDO(err, op.save(true, false))
1055 }
1056
1057 // Set initial balance
1058 SKGUnitObject unit = unit1;
1059 if (!unit1.exist()) {
1060 unit = unit2;
1061 }
1062 if (unit.exist() && balance2 != 0.0) {
1063 double balance = balance1 + SKGUnitObject::convert(balance2, unit2, unit);
1064 IFOKDO(err, setInitialBalance(balance, unit))
1065 }
1066 // Remove account
1067 IFOKDO(err, iAccount.remove(false))
1068 return err;
1069 }
1070
1071
1072