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