1 /*
2  * Copyright 2000-2003  Michael Edwardes <mte@users.sourceforge.net>
3  * Copyright 2001-2002  Felix Rodriguez <frodriguez@users.sourceforge.net>
4  * Copyright 2002-2004  Kevin Tambascio <ktambascio@users.sourceforge.net>
5  * Copyright 2004-2005  Ace Jones <acejones@users.sourceforge.net>
6  * Copyright 2006-2019  Thomas Baumgart <tbaumgart@kde.org>
7  * Copyright 2006       Darren Gould <darren_gould@gmx.de>
8  * Copyright 2017-2018  Łukasz Wojniłowicz <lukasz.wojnilowicz@gmail.com>
9  *
10  * This program is free software; you can redistribute it and/or
11  * modify it under the terms of the GNU General Public License as
12  * published by the Free Software Foundation; either version 2 of
13  * the License, or (at your option) any later version.
14  *
15  * This program is distributed in the hope that it will be useful,
16  * but WITHOUT ANY WARRANTY; without even the implied warranty of
17  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18  * GNU General Public License for more details.
19  *
20  * You should have received a copy of the GNU General Public License
21  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
22  */
23 
24 #include "mymoneyfile.h"
25 
26 #include <utility>
27 
28 // ----------------------------------------------------------------------------
29 // QT Includes
30 
31 #include <QString>
32 #include <QList>
33 #include <QUuid>
34 #include <QLocale>
35 #include <QBitArray>
36 #include <QDebug>
37 
38 // ----------------------------------------------------------------------------
39 // KDE Includes
40 
41 #include <KLocalizedString>
42 
43 // ----------------------------------------------------------------------------
44 // Project Includes
45 
46 #include "mymoneystoragemgr.h"
47 #include "mymoneyinstitution.h"
48 #include "mymoneyaccount.h"
49 #include "mymoneyaccountloan.h"
50 #include "mymoneysecurity.h"
51 #include "mymoneyreport.h"
52 #include "mymoneybalancecache.h"
53 #include "mymoneybudget.h"
54 #include "mymoneyprice.h"
55 #include "mymoneypayee.h"
56 #include "mymoneytag.h"
57 #include "mymoneyschedule.h"
58 #include "mymoneysplit.h"
59 #include "mymoneytransaction.h"
60 #include "mymoneycostcenter.h"
61 #include "mymoneyexception.h"
62 #include "onlinejob.h"
63 #include "storageenums.h"
64 #include "mymoneyenums.h"
65 
66 // include the following line to get a 'cout' for debug purposes
67 // #include <iostream>
68 
69 using namespace eMyMoney;
70 
71 const QString MyMoneyFile::AccountSeparator = QChar(':');
72 
73 MyMoneyFile MyMoneyFile::file;
74 
75 typedef QList<std::pair<QString, QDate> > BalanceNotifyList;
76 typedef QMap<QString, bool> CacheNotifyList;
77 
78 /// @todo make this template based
79 class MyMoneyNotification
80 {
81 public:
MyMoneyNotification(File::Mode mode,const MyMoneyTransaction & t)82   MyMoneyNotification(File::Mode mode, const MyMoneyTransaction& t) :
83       m_objType(File::Object::Transaction),
84       m_notificationMode(mode),
85       m_id(t.id()) {
86   }
87 
MyMoneyNotification(File::Mode mode,const MyMoneyAccount & acc)88   MyMoneyNotification(File::Mode mode, const MyMoneyAccount& acc) :
89       m_objType(File::Object::Account),
90       m_notificationMode(mode),
91       m_id(acc.id()) {
92   }
93 
MyMoneyNotification(File::Mode mode,const MyMoneyInstitution & institution)94   MyMoneyNotification(File::Mode mode, const MyMoneyInstitution& institution) :
95       m_objType(File::Object::Institution),
96       m_notificationMode(mode),
97       m_id(institution.id()) {
98   }
99 
MyMoneyNotification(File::Mode mode,const MyMoneyPayee & payee)100   MyMoneyNotification(File::Mode mode, const MyMoneyPayee& payee) :
101       m_objType(File::Object::Payee),
102       m_notificationMode(mode),
103       m_id(payee.id()) {
104   }
105 
MyMoneyNotification(File::Mode mode,const MyMoneyTag & tag)106   MyMoneyNotification(File::Mode mode, const MyMoneyTag& tag) :
107       m_objType(File::Object::Tag),
108       m_notificationMode(mode),
109       m_id(tag.id()) {
110   }
111 
MyMoneyNotification(File::Mode mode,const MyMoneySchedule & schedule)112   MyMoneyNotification(File::Mode mode, const MyMoneySchedule& schedule) :
113       m_objType(File::Object::Schedule),
114       m_notificationMode(mode),
115       m_id(schedule.id()) {
116   }
117 
MyMoneyNotification(File::Mode mode,const MyMoneySecurity & security)118   MyMoneyNotification(File::Mode mode, const MyMoneySecurity& security) :
119       m_objType(File::Object::Security),
120       m_notificationMode(mode),
121       m_id(security.id()) {
122   }
123 
MyMoneyNotification(File::Mode mode,const onlineJob & job)124   MyMoneyNotification(File::Mode mode, const onlineJob& job) :
125       m_objType(File::Object::OnlineJob),
126       m_notificationMode(mode),
127       m_id(job.id()) {
128   }
129 
objectType() const130   File::Object objectType() const {
131     return m_objType;
132   }
notificationMode() const133   File::Mode notificationMode() const {
134     return m_notificationMode;
135   }
id() const136   const QString& id() const {
137     return m_id;
138   }
139 
140 protected:
MyMoneyNotification(File::Object obj,File::Mode mode,const QString & id)141   MyMoneyNotification(File::Object obj,
142                       File::Mode mode,
143                       const QString& id) :
144       m_objType(obj),
145       m_notificationMode(mode),
146       m_id(id) {}
147 
148 private:
149   File::Object   m_objType;
150   File::Mode     m_notificationMode;
151   QString        m_id;
152 };
153 
154 
155 
156 
157 
158 class MyMoneyFile::Private
159 {
160 public:
Private()161   Private() :
162       m_storage(0),
163       m_inTransaction(false) {}
164 
~Private()165   ~Private() {
166     delete m_storage;
167   }
168   /**
169     * This method is used to add an id to the list of objects
170     * to be removed from the cache. If id is empty, then nothing is added to the list.
171     *
172     * @param id id of object to be notified
173     * @param reload reload the object (@c true) or not (@c false). The default is @c true
174     * @see attach, detach
175     */
addCacheNotification(const QString & id,const QDate & date)176   void addCacheNotification(const QString& id, const QDate& date) {
177     if (!id.isEmpty())
178       m_balanceNotifyList.append(std::make_pair(id, date));
179   }
180 
181   /**
182     * This method is used to clear the notification list
183     */
clearCacheNotification()184   void clearCacheNotification() {
185     // reset list to be empty
186     m_balanceNotifyList.clear();
187   }
188 
189   /**
190     * This method is used to clear all
191     * objects mentioned in m_notificationList from the cache.
192     */
notify()193   void notify() {
194     foreach (const BalanceNotifyList::value_type & i, m_balanceNotifyList) {
195       m_balanceChangedSet += i.first;
196       if (i.second.isValid()) {
197         m_balanceCache.clear(i.first, i.second);
198       } else {
199         m_balanceCache.clear(i.first);
200       }
201     }
202 
203     clearCacheNotification();
204   }
205 
206   /**
207     * This method checks if a storage object is attached and
208     * throws and exception if not.
209     */
checkStorage() const210   inline void checkStorage() const {
211     if (m_storage == 0)
212       throw MYMONEYEXCEPTION_CSTRING("No storage object attached to MyMoneyFile");
213   }
214 
215   /**
216     * This method checks that a transaction has been started with
217     * startTransaction() and throws an exception otherwise. Calls
218     * checkStorage() to make sure a storage object is present and attached.
219     */
checkTransaction(const char * txt) const220   void checkTransaction(const char* txt) const {
221     checkStorage();
222     if (!m_inTransaction)
223       throw MYMONEYEXCEPTION(QString::fromLatin1("No transaction started for %1").arg(QString::fromLatin1(txt)));
224   }
225 
priceChanged(const MyMoneyFile & file,const MyMoneyPrice price)226   void priceChanged(const MyMoneyFile& file, const MyMoneyPrice price) {
227     // get all affected accounts and add them to the m_valueChangedSet
228     QList<MyMoneyAccount> accList;
229     file.accountList(accList);
230     QList<MyMoneyAccount>::const_iterator account_it;
231     for (account_it = accList.constBegin(); account_it != accList.constEnd(); ++account_it) {
232       QString currencyId = account_it->currencyId();
233       if (currencyId != file.baseCurrency().id() && (currencyId == price.from() || currencyId == price.to())) {
234         // this account is not in the base currency and the price affects it's value
235         m_valueChangedSet.insert(account_it->id());
236       }
237     }
238   }
239 
240   /**
241     * This member points to the storage strategy
242     */
243   MyMoneyStorageMgr *m_storage;
244 
245 
246   bool                   m_inTransaction;
247   MyMoneySecurity        m_baseCurrency;
248 
249   /**
250    * @brief Cache for MyMoneyObjects
251    *
252    * It is also used to emit the objectAdded() and objectModified() signals.
253    * => If one of these signals is used, you must use this cache.
254    */
255   MyMoneyPriceList       m_priceCache;
256   MyMoneyBalanceCache    m_balanceCache;
257 
258   /**
259     * This member keeps a list of account ids to notify
260     * after a single operation is completed. The balance cache
261     * is cleared for that account and all dates on or after
262     * the one supplied. If the date is invalid, the entire
263     * balance cache is cleared for that account.
264     */
265   BalanceNotifyList m_balanceNotifyList;
266 
267   /**
268     * This member keeps a list of account ids for which
269     * a balanceChanged() signal needs to be emitted when
270     * a set of operations has been committed.
271     *
272     * @sa MyMoneyFile::commitTransaction()
273     */
274   QSet<QString>     m_balanceChangedSet;
275 
276   /**
277     * This member keeps a list of account ids for which
278     * a valueChanged() signal needs to be emitted when
279     * a set of operations has been committed.
280     *
281     * @sa MyMoneyFile::commitTransaction()
282     */
283   QSet<QString>     m_valueChangedSet;
284 
285   /**
286     * This member keeps the list of changes in the engine
287     * in historical order. The type can be 'added', 'modified'
288     * or removed.
289     */
290   QList<MyMoneyNotification> m_changeSet;
291 };
292 
293 
294 class MyMoneyNotifier
295 {
296 public:
MyMoneyNotifier(MyMoneyFile::Private * file)297   MyMoneyNotifier(MyMoneyFile::Private* file) {
298     m_file = file; m_file->clearCacheNotification();
299   }
~MyMoneyNotifier()300   ~MyMoneyNotifier() {
301     m_file->notify();
302   }
303 private:
304   MyMoneyFile::Private* m_file;
305 };
306 
307 
308 
MyMoneyFile()309 MyMoneyFile::MyMoneyFile() :
310     d(new Private)
311 {
312 }
313 
~MyMoneyFile()314 MyMoneyFile::~MyMoneyFile()
315 {
316   delete d;
317 }
318 
MyMoneyFile(MyMoneyStorageMgr * storage)319 MyMoneyFile::MyMoneyFile(MyMoneyStorageMgr *storage) :
320     d(new Private)
321 {
322   attachStorage(storage);
323 }
324 
instance()325 MyMoneyFile* MyMoneyFile::instance()
326 {
327   return &file;
328 }
329 
attachStorage(MyMoneyStorageMgr * const storage)330 void MyMoneyFile::attachStorage(MyMoneyStorageMgr* const storage)
331 {
332   if (d->m_storage != 0)
333     throw MYMONEYEXCEPTION_CSTRING("Storage already attached");
334 
335   if (storage == 0)
336     throw MYMONEYEXCEPTION_CSTRING("Storage must not be 0");
337 
338   d->m_storage = storage;
339 
340   // force reload of base currency
341   d->m_baseCurrency = MyMoneySecurity();
342 
343   // and the whole cache
344   d->m_balanceCache.clear();
345   d->m_priceCache.clear();
346 
347   // notify application about new data availability
348   emit beginChangeNotification();
349   emit dataChanged();
350   emit endChangeNotification();
351 }
352 
detachStorage(MyMoneyStorageMgr * const)353 void MyMoneyFile::detachStorage(MyMoneyStorageMgr* const /* storage */)
354 {
355   d->m_balanceCache.clear();
356   d->m_priceCache.clear();
357   d->m_storage = nullptr;
358 }
359 
storage() const360 MyMoneyStorageMgr* MyMoneyFile::storage() const
361 {
362   return d->m_storage;
363 }
364 
storageAttached() const365 bool MyMoneyFile::storageAttached() const
366 {
367   return d->m_storage != 0;
368 }
369 
startTransaction()370 void MyMoneyFile::startTransaction()
371 {
372   d->checkStorage();
373   if (d->m_inTransaction) {
374     throw MYMONEYEXCEPTION_CSTRING("Already started a transaction!");
375   }
376 
377   d->m_storage->startTransaction();
378   d->m_inTransaction = true;
379   d->m_changeSet.clear();
380 }
381 
hasTransaction() const382 bool MyMoneyFile::hasTransaction() const
383 {
384   return d->m_inTransaction;
385 }
386 
commitTransaction()387 void MyMoneyFile::commitTransaction()
388 {
389   d->checkTransaction(Q_FUNC_INFO);
390 
391   // commit the transaction in the storage
392   const auto changed = d->m_storage->commitTransaction();
393   d->m_inTransaction = false;
394 
395   // collect notifications about removed objects
396   QStringList removedObjects;
397   const auto& set = d->m_changeSet;
398   for (const auto& change : set) {
399     switch (change.notificationMode()) {
400       case File::Mode::Remove:
401         removedObjects += change.id();
402         break;
403       default:
404         break;
405     }
406   }
407 
408   // inform the outside world about the beginning of notifications
409   emit beginChangeNotification();
410 
411   // Now it's time to send out some signals to the outside world
412   // First we go through the d->m_changeSet and emit respective
413   // signals about addition, modification and removal of engine objects
414   const auto& changes = d->m_changeSet;
415   for (const auto& change : changes) {
416     switch (change.notificationMode()) {
417       case File::Mode::Remove:
418         emit objectRemoved(change.objectType(), change.id());
419         // if there is a balance change recorded for this account remove it since the account itself will be removed
420         // this can happen when deleting categories that have transactions and the reassign category feature was used
421         d->m_balanceChangedSet.remove(change.id());
422         break;
423       case File::Mode::Add:
424         if (!removedObjects.contains(change.id())) {
425           emit objectAdded(change.objectType(), change.id());
426         }
427         break;
428       case File::Mode::Modify:
429         if (!removedObjects.contains(change.id())) {
430           emit objectModified(change.objectType(), change.id());
431         }
432         break;
433     }
434   }
435 
436   // we're done with the change set, so we clear it
437   d->m_changeSet.clear();
438 
439   // now send out the balanceChanged signal for all those
440   // accounts for which we have an indication about a possible
441   // change.
442   const auto& balanceChanges = d->m_balanceChangedSet;
443   for (const auto& id : balanceChanges) {
444     if (!removedObjects.contains(id)) {
445       // if we notify about balance change we don't need to notify about value change
446       // for the same account since a balance change implies a value change
447       d->m_valueChangedSet.remove(id);
448       emit balanceChanged(account(id));
449     }
450   }
451   d->m_balanceChangedSet.clear();
452 
453   // now notify about the remaining value changes
454   const auto& m_valueChanges = d->m_valueChangedSet;
455   for (const auto& id : m_valueChanges) {
456     if (!removedObjects.contains(id)) {
457       emit valueChanged(account(id));
458     }
459   }
460 
461   d->m_valueChangedSet.clear();
462 
463   // as a last action, send out the global dataChanged signal
464   if (changed)
465     emit dataChanged();
466 
467   // inform the outside world about the end of notifications
468   emit endChangeNotification();
469 }
470 
rollbackTransaction()471 void MyMoneyFile::rollbackTransaction()
472 {
473   d->checkTransaction(Q_FUNC_INFO);
474 
475   d->m_storage->rollbackTransaction();
476   d->m_inTransaction = false;
477   d->m_balanceChangedSet.clear();
478   d->m_valueChangedSet.clear();
479   d->m_changeSet.clear();
480 }
481 
addInstitution(MyMoneyInstitution & institution)482 void MyMoneyFile::addInstitution(MyMoneyInstitution& institution)
483 {
484   // perform some checks to see that the institution stuff is OK. For
485   // now we assume that the institution must have a name, the ID is not set
486   // and it does not have a parent (MyMoneyFile).
487 
488   if (institution.name().length() == 0
489       || institution.id().length() != 0)
490     throw MYMONEYEXCEPTION_CSTRING("Not a new institution");
491 
492   d->checkTransaction(Q_FUNC_INFO);
493 
494   d->m_storage->addInstitution(institution);
495   d->m_changeSet += MyMoneyNotification(File::Mode::Add, institution);
496 }
497 
modifyInstitution(const MyMoneyInstitution & institution)498 void MyMoneyFile::modifyInstitution(const MyMoneyInstitution& institution)
499 {
500   d->checkTransaction(Q_FUNC_INFO);
501 
502   d->m_storage->modifyInstitution(institution);
503   d->m_changeSet += MyMoneyNotification(File::Mode::Modify, institution);
504 }
505 
modifyTransaction(const MyMoneyTransaction & transaction)506 void MyMoneyFile::modifyTransaction(const MyMoneyTransaction& transaction)
507 {
508   d->checkTransaction(Q_FUNC_INFO);
509 
510   MyMoneyTransaction tCopy(transaction);
511 
512   // now check the splits
513   bool loanAccountAffected = false;
514   const auto splits1 = transaction.splits();
515   for (const auto& split : splits1) {
516     // the following line will throw an exception if the
517     // account does not exist
518     auto acc = MyMoneyFile::account(split.accountId());
519     if (acc.id().isEmpty())
520       throw MYMONEYEXCEPTION_CSTRING("Cannot store split with no account assigned");
521     if (isStandardAccount(split.accountId()))
522       throw MYMONEYEXCEPTION_CSTRING("Cannot store split referencing standard account");
523     if (acc.isLoan() && (split.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::Transfer)))
524       loanAccountAffected = true;
525   }
526 
527   // change transfer splits between asset/liability and loan accounts
528   // into amortization splits
529   if (loanAccountAffected) {
530     const auto splits = transaction.splits();
531     for (const auto& split : splits) {
532       if (split.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::Transfer)) {
533         auto acc = MyMoneyFile::account(split.accountId());
534 
535         if (acc.isAssetLiability()) {
536           MyMoneySplit s = split;
537           s.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::Amortization));
538           tCopy.modifySplit(s);
539         }
540       }
541     }
542   }
543 
544   // clear all changed objects from cache
545   MyMoneyNotifier notifier(d);
546 
547   // get the current setting of this transaction
548   MyMoneyTransaction tr = MyMoneyFile::transaction(transaction.id());
549 
550   // scan the splits again to update notification list
551   // and mark all accounts that are referenced
552   const auto splits2 = tr.splits();
553   foreach (const auto& split, splits2)
554     d->addCacheNotification(split.accountId(), tr.postDate());
555 
556   // make sure the value is rounded to the accounts precision
557   fixSplitPrecision(tCopy);
558 
559   // perform modification
560   d->m_storage->modifyTransaction(tCopy);
561 
562   // and mark all accounts that are referenced
563   const auto splits3 = tCopy.splits();
564   for (const auto& split : splits3)
565     d->addCacheNotification(split.accountId(), tCopy.postDate());
566 
567   d->m_changeSet += MyMoneyNotification(File::Mode::Modify, transaction);
568 }
569 
modifyAccount(const MyMoneyAccount & _account)570 void MyMoneyFile::modifyAccount(const MyMoneyAccount& _account)
571 {
572   d->checkTransaction(Q_FUNC_INFO);
573 
574   MyMoneyAccount account(_account);
575 
576   auto acc = MyMoneyFile::account(account.id());
577 
578   // check that for standard accounts only specific parameters are changed
579   if (isStandardAccount(account.id())) {
580     // make sure to use the stuff we found on file
581     account = acc;
582 
583     // and only use the changes that are allowed
584     account.setName(_account.name());
585     account.setCurrencyId(_account.currencyId());
586 
587     // now check that it is the same
588     if (!(account == _account))
589       throw MYMONEYEXCEPTION_CSTRING("Unable to modify the standard account groups");
590   }
591 
592   if (account.accountType() != acc.accountType() &&
593       !account.isLiquidAsset() && !acc.isLiquidAsset())
594     throw MYMONEYEXCEPTION_CSTRING("Unable to change account type");
595 
596   // if the account was moved to another institution, we notify
597   // the old one as well as the new one and the structure change
598   if (acc.institutionId() != account.institutionId()) {
599     MyMoneyInstitution inst;
600     if (!acc.institutionId().isEmpty()) {
601       inst = institution(acc.institutionId());
602       inst.removeAccountId(acc.id());
603       modifyInstitution(inst);
604       // modifyInstitution updates d->m_changeSet already
605     }
606     if (!account.institutionId().isEmpty()) {
607       inst = institution(account.institutionId());
608       inst.addAccountId(acc.id());
609       modifyInstitution(inst);
610       // modifyInstitution updates d->m_changeSet already
611     }
612   }
613 
614   d->m_storage->modifyAccount(account);
615   d->m_changeSet += MyMoneyNotification(File::Mode::Modify, account);
616 }
617 
reparentAccount(MyMoneyAccount & acc,MyMoneyAccount & parent)618 void MyMoneyFile::reparentAccount(MyMoneyAccount &acc, MyMoneyAccount& parent)
619 {
620   d->checkTransaction(Q_FUNC_INFO);
621 
622   // check that it's not one of the standard account groups
623   if (isStandardAccount(acc.id()))
624     throw MYMONEYEXCEPTION_CSTRING("Unable to reparent the standard account groups");
625 
626   if (acc.accountGroup() == parent.accountGroup()
627       || (acc.accountType() == Account::Type::Income && parent.accountType() == Account::Type::Expense)
628       || (acc.accountType() == Account::Type::Expense && parent.accountType() == Account::Type::Income)) {
629 
630     if (acc.isInvest() && parent.accountType() != Account::Type::Investment)
631       throw MYMONEYEXCEPTION_CSTRING("Unable to reparent Stock to non-investment account");
632 
633     if (parent.accountType() == Account::Type::Investment && !acc.isInvest())
634       throw MYMONEYEXCEPTION_CSTRING("Unable to reparent non-stock to investment account");
635 
636     // keep a notification of the current parent
637     MyMoneyAccount curParent = account(acc.parentAccountId());
638 
639     d->m_storage->reparentAccount(acc, parent);
640 
641     d->m_changeSet += MyMoneyNotification(File::Mode::Modify, curParent);
642     d->m_changeSet += MyMoneyNotification(File::Mode::Modify, parent);
643     d->m_changeSet += MyMoneyNotification(File::Mode::Modify, acc);
644 
645   } else
646     throw MYMONEYEXCEPTION_CSTRING("Unable to reparent to different account type");
647 }
648 
institution(const QString & id) const649 MyMoneyInstitution MyMoneyFile::institution(const QString& id) const
650 {
651   return d->m_storage->institution(id);
652 }
653 
account(const QString & id) const654 MyMoneyAccount MyMoneyFile::account(const QString& id) const
655 {
656   if (Q_UNLIKELY(id.isEmpty())) // FIXME: Stop requesting accounts with empty id
657     return MyMoneyAccount();
658 
659   return d->m_storage->account(id);
660 }
661 
subAccountByName(const MyMoneyAccount & account,const QString & name) const662 MyMoneyAccount MyMoneyFile::subAccountByName(const MyMoneyAccount& account, const QString& name) const
663 {
664   static MyMoneyAccount nullAccount;
665 
666   const auto accounts = account.accountList();
667   for (const auto& acc : accounts) {
668     const auto sacc = MyMoneyFile::account(acc);
669     if (sacc.name().compare(name) == 0)
670       return sacc;
671   }
672   return nullAccount;
673 }
674 
accountByName(const QString & name) const675 MyMoneyAccount MyMoneyFile::accountByName(const QString& name) const
676 {
677   try {
678     return d->m_storage->accountByName(name);
679   } catch (const MyMoneyException &) {
680   }
681   return MyMoneyAccount();
682 }
683 
removeTransaction(const MyMoneyTransaction & transaction)684 void MyMoneyFile::removeTransaction(const MyMoneyTransaction& transaction)
685 {
686   d->checkTransaction(Q_FUNC_INFO);
687 
688   // clear all changed objects from cache
689   MyMoneyNotifier notifier(d);
690 
691   // get the engine's idea about this transaction
692   MyMoneyTransaction tr = MyMoneyFile::transaction(transaction.id());
693 
694   // scan the splits again to update notification list
695   const auto splits = tr.splits();
696   for (const auto& split : splits) {
697     auto acc = account(split.accountId());
698     if (acc.isClosed())
699       throw MYMONEYEXCEPTION(QString::fromLatin1("Cannot remove transaction that references a closed account."));
700     d->addCacheNotification(split.accountId(), tr.postDate());
701     //FIXME-ALEX Do I need to add d->addCacheNotification(split.tagList()); ??
702   }
703 
704   d->m_storage->removeTransaction(transaction);
705 
706   // remove a possible notification of that same object from the changeSet
707   QList<MyMoneyNotification>::iterator it;
708   for(it = d->m_changeSet.begin(); it != d->m_changeSet.end();) {
709     if((*it).id() == transaction.id()) {
710       it = d->m_changeSet.erase(it);
711     } else {
712       ++it;
713     }
714   }
715 
716   d->m_changeSet += MyMoneyNotification(File::Mode::Remove, transaction);
717 }
718 
719 
hasActiveSplits(const QString & id) const720 bool MyMoneyFile::hasActiveSplits(const QString& id) const
721 {
722   d->checkStorage();
723 
724   return d->m_storage->hasActiveSplits(id);
725 }
726 
isStandardAccount(const QString & id) const727 bool MyMoneyFile::isStandardAccount(const QString& id) const
728 {
729   d->checkStorage();
730 
731   return d->m_storage->isStandardAccount(id);
732 }
733 
setAccountName(const QString & id,const QString & name) const734 void MyMoneyFile::setAccountName(const QString& id, const QString& name) const
735 {
736   d->checkTransaction(Q_FUNC_INFO);
737 
738   auto acc = account(id);
739   d->m_storage->setAccountName(id, name);
740   d->m_changeSet += MyMoneyNotification(File::Mode::Modify, acc);
741 }
742 
removeAccount(const MyMoneyAccount & account)743 void MyMoneyFile::removeAccount(const MyMoneyAccount& account)
744 {
745   d->checkTransaction(Q_FUNC_INFO);
746 
747   MyMoneyAccount parent;
748   MyMoneyAccount acc;
749   MyMoneyInstitution institution;
750 
751   // check that the account and its parent exist
752   // this will throw an exception if the id is unknown
753   acc = MyMoneyFile::account(account.id());
754   parent = MyMoneyFile::account(account.parentAccountId());
755   if (!acc.institutionId().isEmpty())
756     institution = MyMoneyFile::institution(acc.institutionId());
757 
758   // check that it's not one of the standard account groups
759   if (isStandardAccount(account.id()))
760     throw MYMONEYEXCEPTION_CSTRING("Unable to remove the standard account groups");
761 
762   if (hasActiveSplits(account.id())) {
763     throw MYMONEYEXCEPTION_CSTRING("Unable to remove account with active splits");
764   }
765 
766   // collect all sub-ordinate accounts for notification
767   const auto accounts = acc.accountList();
768   for (const auto& id : accounts)
769     d->m_changeSet += MyMoneyNotification(File::Mode::Modify, MyMoneyFile::account(id));
770 
771   // don't forget the parent and a possible institution
772 
773   if (!institution.id().isEmpty()) {
774     institution.removeAccountId(account.id());
775     d->m_storage->modifyInstitution(institution);
776     d->m_changeSet += MyMoneyNotification(File::Mode::Modify, institution);
777   }
778   acc.setInstitutionId(QString());
779 
780   d->m_storage->removeAccount(acc);
781 
782   d->m_balanceCache.clear(acc.id());
783 
784   d->m_changeSet += MyMoneyNotification(File::Mode::Modify, parent);
785   d->m_changeSet += MyMoneyNotification(File::Mode::Remove, acc);
786 }
787 
removeAccountList(const QStringList & account_list,unsigned int level)788 void MyMoneyFile::removeAccountList(const QStringList& account_list, unsigned int level)
789 {
790   if (level > 100)
791     throw MYMONEYEXCEPTION_CSTRING("Too deep recursion in [MyMoneyFile::removeAccountList]!");
792 
793   d->checkTransaction(Q_FUNC_INFO);
794 
795   // upon entry, we check that we could proceed with the operation
796   if (!level) {
797     if (!hasOnlyUnusedAccounts(account_list, 0)) {
798       throw MYMONEYEXCEPTION_CSTRING("One or more accounts cannot be removed");
799     }
800   }
801 
802   // process all accounts in the list and test if they have transactions assigned
803   foreach (const auto sAccount, account_list) {
804     auto a = d->m_storage->account(sAccount);
805     //qDebug() << "Deleting account '"<< a.name() << "'";
806 
807     // first remove all sub-accounts
808     if (!a.accountList().isEmpty()) {
809       removeAccountList(a.accountList(), level + 1);
810 
811       // then remove account itself, but we first have to get
812       // rid of the account list that is still stored in
813       // the MyMoneyAccount object. Easiest way is to get a fresh copy.
814       a = d->m_storage->account(sAccount);
815     }
816 
817     // make sure to remove the item from the cache
818     removeAccount(a);
819   }
820 }
821 
hasOnlyUnusedAccounts(const QStringList & account_list,unsigned int level)822 bool MyMoneyFile::hasOnlyUnusedAccounts(const QStringList& account_list, unsigned int level)
823 {
824   if (level > 100)
825     throw MYMONEYEXCEPTION_CSTRING("Too deep recursion in [MyMoneyFile::hasOnlyUnusedAccounts]!");
826   // process all accounts in the list and test if they have transactions assigned
827   for (const auto& sAccount : account_list) {
828     if (transactionCount(sAccount) != 0)
829       return false; // the current account has a transaction assigned
830     if (!hasOnlyUnusedAccounts(account(sAccount).accountList(), level + 1))
831       return false; // some sub-account has a transaction assigned
832   }
833   return true; // all subaccounts unused
834 }
835 
836 
removeInstitution(const MyMoneyInstitution & institution)837 void MyMoneyFile::removeInstitution(const MyMoneyInstitution& institution)
838 {
839   d->checkTransaction(Q_FUNC_INFO);
840 
841   MyMoneyInstitution inst = MyMoneyFile::institution(institution.id());
842 
843   bool blocked = signalsBlocked();
844   blockSignals(true);
845   const auto accounts = inst.accountList();
846   for (const auto& acc : accounts) {
847     auto a = account(acc);
848     a.setInstitutionId(QString());
849     modifyAccount(a);
850     d->m_changeSet += MyMoneyNotification(File::Mode::Modify, a);
851   }
852   blockSignals(blocked);
853 
854   d->m_storage->removeInstitution(institution);
855 
856   d->m_changeSet += MyMoneyNotification(File::Mode::Remove, institution);
857 }
858 
createAccount(MyMoneyAccount & newAccount,MyMoneyAccount & parentAccount,MyMoneyAccount & brokerageAccount,MyMoneyMoney openingBal)859 void MyMoneyFile::createAccount(MyMoneyAccount& newAccount, MyMoneyAccount& parentAccount, MyMoneyAccount& brokerageAccount, MyMoneyMoney openingBal)
860 {
861   // make sure we have a currency. If none is assigned, we assume base currency
862   if (newAccount.currencyId().isEmpty())
863     newAccount.setCurrencyId(baseCurrency().id());
864 
865   MyMoneyFileTransaction ft;
866   try {
867     int pos;
868     // check for ':' in the name and use it as separator for a hierarchy
869     while ((pos = newAccount.name().indexOf(MyMoneyFile::AccountSeparator)) != -1) {
870       QString part = newAccount.name().left(pos);
871       QString remainder = newAccount.name().mid(pos + 1);
872       const MyMoneyAccount& existingAccount = subAccountByName(parentAccount, part);
873       if (existingAccount.id().isEmpty()) {
874         newAccount.setName(part);
875 
876         addAccount(newAccount, parentAccount);
877         parentAccount = newAccount;
878       } else {
879         parentAccount = existingAccount;
880       }
881       newAccount.setParentAccountId(QString());  // make sure, there's no parent
882       newAccount.clearId();                       // and no id set for adding
883       newAccount.removeAccountIds();              // and no sub-account ids
884       newAccount.setName(remainder);
885     }
886 
887     addAccount(newAccount, parentAccount);
888 
889     // in case of a loan account, we add the initial payment
890     if ((newAccount.accountType() == Account::Type::Loan
891          || newAccount.accountType() == Account::Type::AssetLoan)
892         && !newAccount.value("kmm-loan-payment-acc").isEmpty()
893         && !newAccount.value("kmm-loan-payment-date").isEmpty()) {
894       MyMoneyAccountLoan acc(newAccount);
895       MyMoneyTransaction t;
896       MyMoneySplit a, b;
897       a.setAccountId(acc.id());
898       b.setAccountId(acc.value("kmm-loan-payment-acc"));
899       a.setValue(acc.loanAmount());
900       if (acc.accountType() == Account::Type::Loan)
901         a.setValue(-a.value());
902 
903       a.setShares(a.value());
904       b.setValue(-a.value());
905       b.setShares(b.value());
906       a.setMemo(i18n("Loan payout"));
907       b.setMemo(i18n("Loan payout"));
908       t.setPostDate(QDate::fromString(acc.value("kmm-loan-payment-date"), Qt::ISODate));
909       newAccount.deletePair("kmm-loan-payment-acc");
910       newAccount.deletePair("kmm-loan-payment-date");
911       MyMoneyFile::instance()->modifyAccount(newAccount);
912 
913       t.addSplit(a);
914       t.addSplit(b);
915       addTransaction(t);
916       createOpeningBalanceTransaction(newAccount, openingBal);
917 
918       // in case of an investment account we check if we should create
919       // a brokerage account
920     } else if (newAccount.accountType() == Account::Type::Investment
921                && !brokerageAccount.name().isEmpty()) {
922       addAccount(brokerageAccount, parentAccount);
923 
924       // set a link from the investment account to the brokerage account
925       modifyAccount(newAccount);
926       createOpeningBalanceTransaction(brokerageAccount, openingBal);
927 
928     } else
929       createOpeningBalanceTransaction(newAccount, openingBal);
930 
931     ft.commit();
932   } catch (const MyMoneyException &e) {
933     qWarning("Unable to create account: %s", e.what());
934     throw;
935   }
936 }
937 
addAccount(MyMoneyAccount & account,MyMoneyAccount & parent)938 void MyMoneyFile::addAccount(MyMoneyAccount& account, MyMoneyAccount& parent)
939 {
940   d->checkTransaction(Q_FUNC_INFO);
941 
942   MyMoneyInstitution institution;
943 
944   // perform some checks to see that the account stuff is OK. For
945   // now we assume that the account must have a name, has no
946   // transaction and sub-accounts and parent account
947   // it's own ID is not set and it does not have a pointer to (MyMoneyFile)
948 
949   if (account.name().length() == 0)
950     throw MYMONEYEXCEPTION_CSTRING("Account has no name");
951 
952   if (account.id().length() != 0)
953     throw MYMONEYEXCEPTION_CSTRING("New account must have no id");
954 
955   if (account.accountList().count() != 0)
956     throw MYMONEYEXCEPTION_CSTRING("New account must have no sub-accounts");
957 
958   if (!account.parentAccountId().isEmpty())
959     throw MYMONEYEXCEPTION_CSTRING("New account must have no parent-id");
960 
961   if (account.accountType() == Account::Type::Unknown)
962     throw MYMONEYEXCEPTION_CSTRING("Account has invalid type");
963 
964   // make sure, that the parent account exists
965   // if not, an exception is thrown. If it exists,
966   // get a copy of the current data
967   auto acc = MyMoneyFile::account(parent.id());
968 
969 #if 0
970   // TODO: remove the following code as we now can have multiple accounts
971   // with the same name even in the same hierarchy position of the account tree
972   //
973   // check if the selected name is currently not among the child accounts
974   // if we find one, then return it as the new account
975   QStringList::const_iterator it_a;
976   foreach (const auto accountID, acc.accountList()) {
977     MyMoneyAccount a = MyMoneyFile::account(accountID);
978     if (account.name() == a.name()) {
979       account = a;
980       return;
981     }
982   }
983 #endif
984 
985   // FIXME: make sure, that the parent has the same type
986   // I left it out here because I don't know, if there is
987   // a tight coupling between e.g. checking accounts and the
988   // class asset. It certainly does not make sense to create an
989   // expense account under an income account. Maybe it does, I don't know.
990 
991   // We enforce, that a stock account can never be a parent and
992   // that the parent for a stock account must be an investment. Also,
993   // an investment cannot have another investment account as it's parent
994   if (parent.isInvest())
995     throw MYMONEYEXCEPTION_CSTRING("Stock account cannot be parent account");
996 
997   if (account.isInvest() && parent.accountType() != Account::Type::Investment)
998     throw MYMONEYEXCEPTION_CSTRING("Stock account must have investment account as parent ");
999 
1000   if (!account.isInvest() && parent.accountType() == Account::Type::Investment)
1001     throw MYMONEYEXCEPTION_CSTRING("Investment account can only have stock accounts as children");
1002 
1003   // if an institution is set, verify that it exists
1004   if (account.institutionId().length() != 0) {
1005     // check the presence of the institution. if it
1006     // does not exist, an exception is thrown
1007     institution = MyMoneyFile::institution(account.institutionId());
1008   }
1009 
1010   // if we don't have a valid opening date use today
1011   if (!account.openingDate().isValid()) {
1012     account.setOpeningDate(QDate::currentDate());
1013   }
1014 
1015   // make sure to set the opening date for categories to a
1016   // fixed date (1900-1-1). See #313793 on b.k.o for details
1017   if (account.isIncomeExpense()) {
1018     account.setOpeningDate(QDate(1900, 1, 1));
1019   }
1020 
1021   // if we don't have a currency assigned use the base currency
1022   if (account.currencyId().isEmpty()) {
1023     account.setCurrencyId(baseCurrency().id());
1024   }
1025 
1026   // make sure the parent id is setup
1027   account.setParentAccountId(parent.id());
1028 
1029   d->m_storage->addAccount(account);
1030   d->m_changeSet += MyMoneyNotification(File::Mode::Add, account);
1031 
1032   d->m_storage->addAccount(parent, account);
1033   d->m_changeSet += MyMoneyNotification(File::Mode::Modify, parent);
1034 
1035   if (account.institutionId().length() != 0) {
1036     institution.addAccountId(account.id());
1037     d->m_storage->modifyInstitution(institution);
1038     d->m_changeSet += MyMoneyNotification(File::Mode::Modify, institution);
1039   }
1040 }
1041 
createOpeningBalanceTransaction(const MyMoneyAccount & acc,const MyMoneyMoney & balance)1042 MyMoneyTransaction MyMoneyFile::createOpeningBalanceTransaction(const MyMoneyAccount& acc, const MyMoneyMoney& balance)
1043 {
1044   MyMoneyTransaction t;
1045   // if the opening balance is not zero, we need
1046   // to create the respective transaction
1047   if (!balance.isZero()) {
1048     d->checkTransaction(Q_FUNC_INFO);
1049 
1050     MyMoneySecurity currency = security(acc.currencyId());
1051     MyMoneyAccount openAcc = openingBalanceAccount(currency);
1052 
1053     if (openAcc.openingDate() > acc.openingDate()) {
1054       openAcc.setOpeningDate(acc.openingDate());
1055       modifyAccount(openAcc);
1056     }
1057 
1058     MyMoneySplit s;
1059 
1060     t.setPostDate(acc.openingDate());
1061     t.setCommodity(acc.currencyId());
1062 
1063     s.setAccountId(acc.id());
1064     s.setShares(balance);
1065     s.setValue(balance);
1066     t.addSplit(s);
1067 
1068     s.clearId();
1069     s.setAccountId(openAcc.id());
1070     s.setShares(-balance);
1071     s.setValue(-balance);
1072     t.addSplit(s);
1073 
1074     addTransaction(t);
1075   }
1076   return t;
1077 }
1078 
openingBalanceTransaction(const MyMoneyAccount & acc) const1079 QString MyMoneyFile::openingBalanceTransaction(const MyMoneyAccount& acc) const
1080 {
1081   QString result;
1082 
1083   MyMoneySecurity currency = security(acc.currencyId());
1084   MyMoneyAccount openAcc;
1085 
1086   try {
1087     openAcc = openingBalanceAccount(currency);
1088   } catch (const MyMoneyException &) {
1089     return result;
1090   }
1091 
1092   // Iterate over all the opening balance transactions for this currency
1093   MyMoneyTransactionFilter filter;
1094   filter.addAccount(openAcc.id());
1095   QList<MyMoneyTransaction> transactions = transactionList(filter);
1096   QList<MyMoneyTransaction>::const_iterator it_t = transactions.constBegin();
1097   while (it_t != transactions.constEnd()) {
1098     try {
1099       // Test whether the transaction also includes a split into
1100       // this account
1101       (*it_t).splitByAccount(acc.id(), true /*match*/);
1102 
1103       // If so, we have a winner!
1104       result = (*it_t).id();
1105       break;
1106     } catch (const MyMoneyException &) {
1107       // If not, keep searching
1108       ++it_t;
1109     }
1110   }
1111 
1112   return result;
1113 }
1114 
openingBalanceAccount(const MyMoneySecurity & security)1115 MyMoneyAccount MyMoneyFile::openingBalanceAccount(const MyMoneySecurity& security)
1116 {
1117   if (!security.isCurrency())
1118     throw MYMONEYEXCEPTION_CSTRING("Opening balance for non currencies not supported");
1119 
1120   try {
1121     return openingBalanceAccount_internal(security);
1122   } catch (const MyMoneyException &) {
1123     MyMoneyFileTransaction ft;
1124     MyMoneyAccount acc;
1125 
1126     try {
1127       acc = createOpeningBalanceAccount(security);
1128       ft.commit();
1129 
1130     } catch (const MyMoneyException &) {
1131       qDebug("Unable to create opening balance account for security %s", qPrintable(security.id()));
1132     }
1133     return acc;
1134   }
1135 }
1136 
openingBalanceAccount(const MyMoneySecurity & security) const1137 MyMoneyAccount MyMoneyFile::openingBalanceAccount(const MyMoneySecurity& security) const
1138 {
1139   return openingBalanceAccount_internal(security);
1140 }
1141 
openingBalanceAccount_internal(const MyMoneySecurity & security) const1142 MyMoneyAccount MyMoneyFile::openingBalanceAccount_internal(const MyMoneySecurity& security) const
1143 {
1144   if (!security.isCurrency())
1145     throw MYMONEYEXCEPTION_CSTRING("Opening balance for non currencies not supported");
1146 
1147   MyMoneyAccount acc;
1148   QList<MyMoneyAccount> accounts;
1149   QList<MyMoneyAccount>::ConstIterator it;
1150 
1151   accountList(accounts, equity().accountList(), true);
1152 
1153   for (it = accounts.constBegin(); it != accounts.constEnd(); ++it) {
1154     if (it->value("OpeningBalanceAccount") == QLatin1String("Yes")
1155         && it->currencyId() == security.id()) {
1156       acc = *it;
1157       break;
1158     }
1159   }
1160 
1161   if (acc.id().isEmpty()) {
1162     for (it = accounts.constBegin(); it != accounts.constEnd(); ++it) {
1163       if (it->name().startsWith(MyMoneyFile::openingBalancesPrefix())
1164           && it->currencyId() == security.id()) {
1165         acc = *it;
1166         break;
1167       }
1168     }
1169   }
1170 
1171   if (acc.id().isEmpty())
1172     throw MYMONEYEXCEPTION(QString::fromLatin1("No opening balance account for %1").arg(security.tradingSymbol()));
1173 
1174   return acc;
1175 }
1176 
createOpeningBalanceAccount(const MyMoneySecurity & security)1177 MyMoneyAccount MyMoneyFile::createOpeningBalanceAccount(const MyMoneySecurity& security)
1178 {
1179   d->checkTransaction(Q_FUNC_INFO);
1180 
1181   MyMoneyAccount acc;
1182   QList<MyMoneyAccount> accounts;
1183   QList<MyMoneyAccount>::ConstIterator it;
1184 
1185   accountList(accounts, equity().accountList(), true);
1186 
1187   // find present opening balance accounts without containing '('
1188   QString name;
1189   QString parentAccountId;
1190   QRegExp exp(QString("\\([A-Z]{3}\\)"));
1191 
1192   for (it = accounts.constBegin(); it != accounts.constEnd(); ++it) {
1193     if (it->value("OpeningBalanceAccount") == QLatin1String("Yes")
1194         && exp.indexIn(it->name()) == -1) {
1195       name = it->name();
1196       parentAccountId = it->parentAccountId();
1197       break;
1198     }
1199   }
1200 
1201   if (name.isEmpty())
1202     name = MyMoneyFile::openingBalancesPrefix();
1203   if (security.id() != baseCurrency().id()) {
1204     name += QString(" (%1)").arg(security.id());
1205   }
1206   acc.setName(name);
1207   acc.setAccountType(Account::Type::Equity);
1208   acc.setCurrencyId(security.id());
1209   acc.setValue("OpeningBalanceAccount", "Yes");
1210 
1211   MyMoneyAccount parent = !parentAccountId.isEmpty() ? account(parentAccountId) : equity();
1212   this->addAccount(acc, parent);
1213   return acc;
1214 }
1215 
addTransaction(MyMoneyTransaction & transaction)1216 void MyMoneyFile::addTransaction(MyMoneyTransaction& transaction)
1217 {
1218   d->checkTransaction(Q_FUNC_INFO);
1219 
1220   // clear all changed objects from cache
1221   MyMoneyNotifier notifier(d);
1222 
1223   // perform some checks to see that the transaction stuff is OK. For
1224   // now we assume that
1225   // * no ids are assigned
1226   // * the date valid (must not be empty)
1227   // * the referenced accounts in the splits exist
1228 
1229   // first perform all the checks
1230   if (!transaction.id().isEmpty())
1231     throw MYMONEYEXCEPTION_CSTRING("Unable to add transaction with id set");
1232   if (!transaction.postDate().isValid())
1233     throw MYMONEYEXCEPTION_CSTRING("Unable to add transaction with invalid postdate");
1234 
1235   // now check the splits
1236   auto loanAccountAffected = false;
1237   const auto splits1 = transaction.splits();
1238   for (const auto& split : splits1) {
1239     // the following line will throw an exception if the
1240     // account does not exist or is one of the standard accounts
1241     auto acc = MyMoneyFile::account(split.accountId());
1242     if (acc.id().isEmpty())
1243       throw MYMONEYEXCEPTION_CSTRING("Cannot add split with no account assigned");
1244     if (acc.isLoan())
1245       loanAccountAffected = true;
1246     if (isStandardAccount(split.accountId()))
1247       throw MYMONEYEXCEPTION_CSTRING("Cannot add split referencing standard account");
1248   }
1249 
1250   // change transfer splits between asset/liability and loan accounts
1251   // into amortization splits
1252   if (loanAccountAffected) {
1253     foreach (const auto split, transaction.splits()) {
1254       if (split.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::Transfer)) {
1255         auto acc = MyMoneyFile::account(split.accountId());
1256 
1257         if (acc.isAssetLiability()) {
1258           MyMoneySplit s = split;
1259           s.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::Amortization));
1260           transaction.modifySplit(s);
1261         }
1262       }
1263     }
1264   }
1265 
1266   // check that we have a commodity
1267   if (transaction.commodity().isEmpty()) {
1268     transaction.setCommodity(baseCurrency().id());
1269   }
1270 
1271   // make sure the value is rounded to the accounts precision
1272   fixSplitPrecision(transaction);
1273 
1274   // then add the transaction to the file global pool
1275   d->m_storage->addTransaction(transaction);
1276 
1277   // scan the splits again to update notification list
1278   const auto splits2 = transaction.splits();
1279   for (const auto& split : splits2)
1280     d->addCacheNotification(split.accountId(), transaction.postDate());
1281 
1282   d->m_changeSet += MyMoneyNotification(File::Mode::Add, transaction);
1283 }
1284 
transaction(const QString & id) const1285 MyMoneyTransaction MyMoneyFile::transaction(const QString& id) const
1286 {
1287   d->checkStorage();
1288 
1289   return d->m_storage->transaction(id);
1290 }
1291 
transaction(const QString & account,const int idx) const1292 MyMoneyTransaction MyMoneyFile::transaction(const QString& account, const int idx) const
1293 {
1294   d->checkStorage();
1295 
1296   return d->m_storage->transaction(account, idx);
1297 }
1298 
addPayee(MyMoneyPayee & payee)1299 void MyMoneyFile::addPayee(MyMoneyPayee& payee)
1300 {
1301   d->checkTransaction(Q_FUNC_INFO);
1302 
1303   d->m_storage->addPayee(payee);
1304   d->m_changeSet += MyMoneyNotification(File::Mode::Add, payee);
1305 }
1306 
payee(const QString & id) const1307 MyMoneyPayee MyMoneyFile::payee(const QString& id) const
1308 {
1309   if (Q_UNLIKELY(id.isEmpty()))
1310     return MyMoneyPayee();
1311 
1312   return d->m_storage->payee(id);
1313 }
1314 
payeeByName(const QString & name) const1315 MyMoneyPayee MyMoneyFile::payeeByName(const QString& name) const
1316 {
1317   d->checkStorage();
1318 
1319   return d->m_storage->payeeByName(name);
1320 }
1321 
modifyPayee(const MyMoneyPayee & payee)1322 void MyMoneyFile::modifyPayee(const MyMoneyPayee& payee)
1323 {
1324   d->checkTransaction(Q_FUNC_INFO);
1325 
1326   d->m_storage->modifyPayee(payee);
1327   d->m_changeSet += MyMoneyNotification(File::Mode::Modify, payee);
1328 }
1329 
removePayee(const MyMoneyPayee & payee)1330 void MyMoneyFile::removePayee(const MyMoneyPayee& payee)
1331 {
1332   d->checkTransaction(Q_FUNC_INFO);
1333 
1334   // FIXME we need to make sure, that the payee is not referenced anymore
1335   d->m_storage->removePayee(payee);
1336   d->m_changeSet += MyMoneyNotification(File::Mode::Remove, payee);
1337 }
1338 
addTag(MyMoneyTag & tag)1339 void MyMoneyFile::addTag(MyMoneyTag& tag)
1340 {
1341   d->checkTransaction(Q_FUNC_INFO);
1342 
1343   d->m_storage->addTag(tag);
1344   d->m_changeSet += MyMoneyNotification(File::Mode::Add, tag);
1345 }
1346 
tag(const QString & id) const1347 MyMoneyTag MyMoneyFile::tag(const QString& id) const
1348 {
1349   return d->m_storage->tag(id);
1350 }
1351 
tagByName(const QString & name) const1352 MyMoneyTag MyMoneyFile::tagByName(const QString& name) const
1353 {
1354   d->checkStorage();
1355 
1356   return d->m_storage->tagByName(name);
1357 }
1358 
modifyTag(const MyMoneyTag & tag)1359 void MyMoneyFile::modifyTag(const MyMoneyTag& tag)
1360 {
1361   d->checkTransaction(Q_FUNC_INFO);
1362 
1363   d->m_storage->modifyTag(tag);
1364   d->m_changeSet += MyMoneyNotification(File::Mode::Modify, tag);
1365 }
1366 
removeTag(const MyMoneyTag & tag)1367 void MyMoneyFile::removeTag(const MyMoneyTag& tag)
1368 {
1369   d->checkTransaction(Q_FUNC_INFO);
1370 
1371   // FIXME we need to make sure, that the tag is not referenced anymore
1372   d->m_storage->removeTag(tag);
1373   d->m_changeSet += MyMoneyNotification(File::Mode::Remove, tag);
1374 }
1375 
accountList(QList<MyMoneyAccount> & list,const QStringList & idlist,const bool recursive) const1376 void MyMoneyFile::accountList(QList<MyMoneyAccount>& list, const QStringList& idlist, const bool recursive) const
1377 {
1378   d->checkStorage();
1379 
1380   if (idlist.isEmpty()) {
1381     d->m_storage->accountList(list);
1382 
1383 #if 0
1384     // TODO: I have no idea what this was good for, but it caused the networth report
1385     //       to show double the numbers so I commented it out (ipwizard, 2008-05-24)
1386     if (d->m_storage && (list.isEmpty() || list.size() != d->m_storage->accountCount())) {
1387       d->m_storage->accountList(list);
1388       d->m_cache.preloadAccount(list);
1389     }
1390 #endif
1391 
1392     QList<MyMoneyAccount>::Iterator it;
1393     for (it = list.begin(); it != list.end();) {
1394       if (isStandardAccount((*it).id())) {
1395         it = list.erase(it);
1396       } else {
1397         ++it;
1398       }
1399     }
1400   } else {
1401     QList<MyMoneyAccount>::ConstIterator it;
1402     QList<MyMoneyAccount> list_a;
1403     d->m_storage->accountList(list_a);
1404 
1405     for (it = list_a.constBegin(); it != list_a.constEnd(); ++it) {
1406       if (!isStandardAccount((*it).id())) {
1407         if (idlist.indexOf((*it).id()) != -1) {
1408           list.append(*it);
1409           if (recursive == true && !(*it).accountList().isEmpty()) {
1410             accountList(list, (*it).accountList(), true);
1411           }
1412         }
1413       }
1414     }
1415   }
1416 }
1417 
institutionList() const1418 QList<MyMoneyInstitution> MyMoneyFile::institutionList() const
1419 {
1420   return d->m_storage->institutionList();
1421 }
1422 
1423 // general get functions
user() const1424 MyMoneyPayee MyMoneyFile::user() const
1425 {
1426   d->checkStorage();
1427   return d->m_storage->user();
1428 }
1429 
1430 // general set functions
setUser(const MyMoneyPayee & user)1431 void MyMoneyFile::setUser(const MyMoneyPayee& user)
1432 {
1433   d->checkTransaction(Q_FUNC_INFO);
1434 
1435   d->m_storage->setUser(user);
1436 }
1437 
dirty() const1438 bool MyMoneyFile::dirty() const
1439 {
1440   if (!d->m_storage)
1441     return false;
1442 
1443   return d->m_storage->dirty();
1444 }
1445 
setDirty() const1446 void MyMoneyFile::setDirty() const
1447 {
1448   d->checkStorage();
1449 
1450   d->m_storage->setDirty();
1451 }
1452 
accountCount() const1453 unsigned int MyMoneyFile::accountCount() const
1454 {
1455   d->checkStorage();
1456 
1457   return d->m_storage->accountCount();
1458 }
1459 
ensureDefaultCurrency(MyMoneyAccount & acc) const1460 void MyMoneyFile::ensureDefaultCurrency(MyMoneyAccount& acc) const
1461 {
1462   if (acc.currencyId().isEmpty()) {
1463     if (!baseCurrency().id().isEmpty())
1464       acc.setCurrencyId(baseCurrency().id());
1465   }
1466 }
1467 
liability() const1468 MyMoneyAccount MyMoneyFile::liability() const
1469 {
1470   d->checkStorage();
1471 
1472   return account(MyMoneyAccount::stdAccName(eMyMoney::Account::Standard::Liability));
1473 }
1474 
asset() const1475 MyMoneyAccount MyMoneyFile::asset() const
1476 {
1477   d->checkStorage();
1478 
1479   return account(MyMoneyAccount::stdAccName(eMyMoney::Account::Standard::Asset));
1480 }
1481 
expense() const1482 MyMoneyAccount MyMoneyFile::expense() const
1483 {
1484   d->checkStorage();
1485 
1486   return account(MyMoneyAccount::stdAccName(eMyMoney::Account::Standard::Expense));
1487 }
1488 
income() const1489 MyMoneyAccount MyMoneyFile::income() const
1490 {
1491   d->checkStorage();
1492 
1493   return account(MyMoneyAccount::stdAccName(eMyMoney::Account::Standard::Income));
1494 }
1495 
equity() const1496 MyMoneyAccount MyMoneyFile::equity() const
1497 {
1498   d->checkStorage();
1499 
1500   return account(MyMoneyAccount::stdAccName(eMyMoney::Account::Standard::Equity));
1501 }
1502 
transactionCount(const QString & account) const1503 unsigned int MyMoneyFile::transactionCount(const QString& account) const
1504 {
1505   d->checkStorage();
1506 
1507   return d->m_storage->transactionCount(account);
1508 }
1509 
transactionCount() const1510 unsigned int MyMoneyFile::transactionCount() const
1511 {
1512   return transactionCount(QString());
1513 }
1514 
transactionCountMap() const1515 QMap<QString, unsigned long> MyMoneyFile::transactionCountMap() const
1516 {
1517   d->checkStorage();
1518 
1519   return d->m_storage->transactionCountMap();
1520 }
1521 
institutionCount() const1522 unsigned int MyMoneyFile::institutionCount() const
1523 {
1524   d->checkStorage();
1525 
1526   return d->m_storage->institutionCount();
1527 }
1528 
balance(const QString & id,const QDate & date) const1529 MyMoneyMoney MyMoneyFile::balance(const QString& id, const QDate& date) const
1530 {
1531   if (date.isValid()) {
1532     MyMoneyBalanceCacheItem bal = d->m_balanceCache.balance(id, date);
1533     if (bal.isValid())
1534       return bal.balance();
1535   }
1536 
1537   d->checkStorage();
1538 
1539   MyMoneyMoney returnValue = d->m_storage->balance(id, date);
1540 
1541   if (date.isValid()) {
1542     d->m_balanceCache.insert(id, date, returnValue);
1543   }
1544 
1545   return returnValue;
1546 }
1547 
balance(const QString & id) const1548 MyMoneyMoney MyMoneyFile::balance(const QString& id) const
1549 {
1550   return balance(id, QDate());
1551 }
1552 
clearedBalance(const QString & id,const QDate & date) const1553 MyMoneyMoney MyMoneyFile::clearedBalance(const QString &id, const QDate& date) const
1554 {
1555   MyMoneyMoney cleared;
1556   QList<MyMoneyTransaction> list;
1557 
1558   cleared = balance(id, date);
1559 
1560   MyMoneyAccount account = this->account(id);
1561   MyMoneyMoney factor(1, 1);
1562   if (account.accountGroup() == Account::Type::Liability || account.accountGroup() == Account::Type::Equity)
1563     factor = -factor;
1564 
1565   MyMoneyTransactionFilter filter;
1566   filter.addAccount(id);
1567   filter.setDateFilter(QDate(), date);
1568   filter.setReportAllSplits(false);
1569   filter.addState((int)TransactionFilter::State::NotReconciled);
1570   transactionList(list, filter);
1571 
1572   for (QList<MyMoneyTransaction>::const_iterator it_t = list.constBegin(); it_t != list.constEnd(); ++it_t) {
1573     const QList<MyMoneySplit>& splits = (*it_t).splits();
1574     for (QList<MyMoneySplit>::const_iterator it_s = splits.constBegin(); it_s != splits.constEnd(); ++it_s) {
1575       const MyMoneySplit &split = (*it_s);
1576       if (split.accountId() != id)
1577         continue;
1578       cleared -= split.shares();
1579     }
1580   }
1581   return cleared * factor;
1582 }
1583 
totalBalance(const QString & id,const QDate & date) const1584 MyMoneyMoney MyMoneyFile::totalBalance(const QString& id, const QDate& date) const
1585 {
1586   d->checkStorage();
1587 
1588   return d->m_storage->totalBalance(id, date);
1589 }
1590 
totalBalance(const QString & id) const1591 MyMoneyMoney MyMoneyFile::totalBalance(const QString& id) const
1592 {
1593   return totalBalance(id, QDate());
1594 }
1595 
warningMissingRate(const QString & fromId,const QString & toId) const1596 void MyMoneyFile::warningMissingRate(const QString& fromId, const QString& toId) const
1597 {
1598   MyMoneySecurity from, to;
1599   try {
1600     from = security(fromId);
1601     to = security(toId);
1602     qWarning("Missing price info for conversion from %s to %s", qPrintable(from.name()), qPrintable(to.name()));
1603 
1604   } catch (const MyMoneyException &e) {
1605     qWarning("Missing security caught in MyMoneyFile::warningMissingRate(). %s", e.what());
1606   }
1607 }
1608 
transactionList(QList<QPair<MyMoneyTransaction,MyMoneySplit>> & list,MyMoneyTransactionFilter & filter) const1609 void MyMoneyFile::transactionList(QList<QPair<MyMoneyTransaction, MyMoneySplit> >& list, MyMoneyTransactionFilter& filter) const
1610 {
1611   d->checkStorage();
1612   d->m_storage->transactionList(list, filter);
1613 }
1614 
transactionList(QList<MyMoneyTransaction> & list,MyMoneyTransactionFilter & filter) const1615 void MyMoneyFile::transactionList(QList<MyMoneyTransaction>& list, MyMoneyTransactionFilter& filter) const
1616 {
1617   d->checkStorage();
1618   d->m_storage->transactionList(list, filter);
1619 }
1620 
transactionList(MyMoneyTransactionFilter & filter) const1621 QList<MyMoneyTransaction> MyMoneyFile::transactionList(MyMoneyTransactionFilter& filter) const
1622 {
1623   d->checkStorage();
1624   return d->m_storage->transactionList(filter);
1625 }
1626 
payeeList() const1627 QList<MyMoneyPayee> MyMoneyFile::payeeList() const
1628 {
1629   return d->m_storage->payeeList();
1630 }
1631 
tagList() const1632 QList<MyMoneyTag> MyMoneyFile::tagList() const
1633 {
1634   return d->m_storage->tagList();
1635 }
1636 
accountToCategory(const QString & accountId,bool includeStandardAccounts) const1637 QString MyMoneyFile::accountToCategory(const QString& accountId, bool includeStandardAccounts) const
1638 {
1639   MyMoneyAccount acc;
1640   QString rc;
1641 
1642   if (!accountId.isEmpty()) {
1643     acc = account(accountId);
1644     do {
1645       if (!rc.isEmpty())
1646         rc = AccountSeparator + rc;
1647       rc = acc.name() + rc;
1648       acc = account(acc.parentAccountId());
1649     } while (!acc.id().isEmpty() && (includeStandardAccounts || !isStandardAccount(acc.id())));
1650   }
1651   return rc;
1652 }
1653 
categoryToAccount(const QString & category,Account::Type type) const1654 QString MyMoneyFile::categoryToAccount(const QString& category, Account::Type type) const
1655 {
1656   QString id;
1657 
1658   // search the category in the expense accounts and if it is not found, try
1659   // to locate it in the income accounts
1660   if (type == Account::Type::Unknown
1661       || type == Account::Type::Expense) {
1662     id = locateSubAccount(MyMoneyFile::instance()->expense(), category);
1663   }
1664 
1665   if ((id.isEmpty() && type == Account::Type::Unknown)
1666       || type == Account::Type::Income) {
1667     id = locateSubAccount(MyMoneyFile::instance()->income(), category);
1668   }
1669 
1670   return id;
1671 }
1672 
categoryToAccount(const QString & category) const1673 QString MyMoneyFile::categoryToAccount(const QString& category) const
1674 {
1675   return categoryToAccount(category, Account::Type::Unknown);
1676 }
1677 
nameToAccount(const QString & name) const1678 QString MyMoneyFile::nameToAccount(const QString& name) const
1679 {
1680   QString id;
1681 
1682   // search the category in the asset accounts and if it is not found, try
1683   // to locate it in the liability accounts
1684   id = locateSubAccount(MyMoneyFile::instance()->asset(), name);
1685   if (id.isEmpty())
1686     id = locateSubAccount(MyMoneyFile::instance()->liability(), name);
1687 
1688   return id;
1689 }
1690 
parentName(const QString & name) const1691 QString MyMoneyFile::parentName(const QString& name) const
1692 {
1693   return name.section(AccountSeparator, 0, -2);
1694 }
1695 
locateSubAccount(const MyMoneyAccount & base,const QString & category) const1696 QString MyMoneyFile::locateSubAccount(const MyMoneyAccount& base, const QString& category) const
1697 {
1698   MyMoneyAccount nextBase;
1699   QString level, remainder;
1700   level = category.section(AccountSeparator, 0, 0);
1701   remainder = category.section(AccountSeparator, 1);
1702 
1703   foreach (const auto sAccount, base.accountList()) {
1704     nextBase = account(sAccount);
1705     if (nextBase.name() == level) {
1706       if (remainder.isEmpty()) {
1707         return nextBase.id();
1708       }
1709       return locateSubAccount(nextBase, remainder);
1710     }
1711   }
1712   return QString();
1713 }
1714 
value(const QString & key) const1715 QString MyMoneyFile::value(const QString& key) const
1716 {
1717   d->checkStorage();
1718 
1719   return d->m_storage->value(key);
1720 }
1721 
setValue(const QString & key,const QString & val)1722 void MyMoneyFile::setValue(const QString& key, const QString& val)
1723 {
1724   d->checkTransaction(Q_FUNC_INFO);
1725 
1726   d->m_storage->setValue(key, val);
1727 }
1728 
deletePair(const QString & key)1729 void MyMoneyFile::deletePair(const QString& key)
1730 {
1731   d->checkTransaction(Q_FUNC_INFO);
1732 
1733   d->m_storage->deletePair(key);
1734 }
1735 
addSchedule(MyMoneySchedule & sched)1736 void MyMoneyFile::addSchedule(MyMoneySchedule& sched)
1737 {
1738   d->checkTransaction(Q_FUNC_INFO);
1739 
1740   const auto splits = sched.transaction().splits();
1741   for (const auto& split : splits) {
1742     // the following line will throw an exception if the
1743     // account does not exist or is one of the standard accounts
1744     const auto acc = account(split.accountId());
1745     if (acc.id().isEmpty())
1746       throw MYMONEYEXCEPTION_CSTRING("Cannot add split with no account assigned");
1747     if (isStandardAccount(split.accountId()))
1748       throw MYMONEYEXCEPTION_CSTRING("Cannot add split referencing standard account");
1749   }
1750 
1751   d->m_storage->addSchedule(sched);
1752   d->m_changeSet += MyMoneyNotification(File::Mode::Add, sched);
1753 }
1754 
modifySchedule(const MyMoneySchedule & sched)1755 void MyMoneyFile::modifySchedule(const MyMoneySchedule& sched)
1756 {
1757   d->checkTransaction(Q_FUNC_INFO);
1758 
1759   foreach (const auto split, sched.transaction().splits()) {
1760     // the following line will throw an exception if the
1761     // account does not exist or is one of the standard accounts
1762     auto acc = MyMoneyFile::account(split.accountId());
1763     if (acc.id().isEmpty())
1764       throw MYMONEYEXCEPTION_CSTRING("Cannot store split with no account assigned");
1765     if (isStandardAccount(split.accountId()))
1766       throw MYMONEYEXCEPTION_CSTRING("Cannot store split referencing standard account");
1767   }
1768 
1769   d->m_storage->modifySchedule(sched);
1770   d->m_changeSet += MyMoneyNotification(File::Mode::Modify, sched);
1771 }
1772 
removeSchedule(const MyMoneySchedule & sched)1773 void MyMoneyFile::removeSchedule(const MyMoneySchedule& sched)
1774 {
1775   d->checkTransaction(Q_FUNC_INFO);
1776 
1777   d->m_storage->removeSchedule(sched);
1778   d->m_changeSet += MyMoneyNotification(File::Mode::Remove, sched);
1779 }
1780 
schedule(const QString & id) const1781 MyMoneySchedule MyMoneyFile::schedule(const QString& id) const
1782 {
1783   return d->m_storage->schedule(id);
1784 }
1785 
scheduleList(const QString & accountId,const Schedule::Type type,const Schedule::Occurrence occurrence,const Schedule::PaymentType paymentType,const QDate & startDate,const QDate & endDate,const bool overdue) const1786 QList<MyMoneySchedule> MyMoneyFile::scheduleList(
1787   const QString& accountId,
1788   const Schedule::Type type,
1789   const Schedule::Occurrence occurrence,
1790   const Schedule::PaymentType paymentType,
1791   const QDate& startDate,
1792   const QDate& endDate,
1793   const bool overdue) const
1794 {
1795   d->checkStorage();
1796 
1797   return d->m_storage->scheduleList(accountId, type, occurrence, paymentType, startDate, endDate, overdue);
1798 }
1799 
scheduleList(const QString & accountId) const1800 QList<MyMoneySchedule> MyMoneyFile::scheduleList(
1801     const QString& accountId) const
1802 {
1803   return scheduleList(accountId, Schedule::Type::Any, Schedule::Occurrence::Any, Schedule::PaymentType::Any,
1804                       QDate(), QDate(), false);
1805 }
1806 
scheduleList() const1807 QList<MyMoneySchedule> MyMoneyFile::scheduleList() const
1808 {
1809   return scheduleList(QString(), Schedule::Type::Any, Schedule::Occurrence::Any, Schedule::PaymentType::Any,
1810                       QDate(), QDate(), false);
1811 }
1812 
consistencyCheck()1813 QStringList MyMoneyFile::consistencyCheck()
1814 {
1815   QList<MyMoneyAccount> list;
1816   QList<MyMoneyAccount>::Iterator it_a;
1817   QList<MyMoneySchedule>::Iterator it_sch;
1818   QList<MyMoneyPayee>::Iterator it_p;
1819   QList<MyMoneyTransaction>::Iterator it_t;
1820   QList<MyMoneyReport>::Iterator it_r;
1821   QStringList accountRebuild;
1822 
1823   QMap<QString, bool> interestAccounts;
1824 
1825   MyMoneyAccount parent;
1826   MyMoneyAccount child;
1827   MyMoneyAccount toplevel;
1828 
1829   QString parentId;
1830   QStringList rc;
1831 
1832   int problemCount = 0;
1833   int unfixedCount = 0;
1834   QString problemAccount;
1835 
1836   // check that we have a storage object
1837   d->checkTransaction(Q_FUNC_INFO);
1838 
1839   // get the current list of accounts
1840   accountList(list);
1841   // add the standard accounts
1842   list << MyMoneyFile::instance()->asset();
1843   list << MyMoneyFile::instance()->liability();
1844   list << MyMoneyFile::instance()->income();
1845   list << MyMoneyFile::instance()->expense();
1846 
1847   for (it_a = list.begin(); it_a != list.end(); ++it_a) {
1848     // no more checks for standard accounts
1849     if (isStandardAccount((*it_a).id())) {
1850       continue;
1851     }
1852 
1853     switch ((*it_a).accountGroup()) {
1854       case Account::Type::Asset:
1855         toplevel = asset();
1856         break;
1857       case Account::Type::Liability:
1858         toplevel = liability();
1859         break;
1860       case Account::Type::Expense:
1861         toplevel = expense();
1862         break;
1863       case Account::Type::Income:
1864         toplevel = income();
1865         break;
1866       case Account::Type::Equity:
1867         toplevel = equity();
1868         break;
1869       default:
1870         qWarning("%s:%d This should never happen!", __FILE__ , __LINE__);
1871         break;
1872     }
1873 
1874     // check for loops in the hierarchy
1875     parentId = (*it_a).parentAccountId();
1876     try {
1877       bool dropOut = false;
1878       while (!isStandardAccount(parentId) && !dropOut) {
1879         parent = account(parentId);
1880         if (parent.id() == (*it_a).id()) {
1881           // parent loops, so we need to re-parent to toplevel account
1882           // find parent account in our list
1883           problemCount++;
1884           QList<MyMoneyAccount>::Iterator it_b;
1885           for (it_b = list.begin(); it_b != list.end(); ++it_b) {
1886             if ((*it_b).id() == parent.id()) {
1887               if (problemAccount != (*it_a).name()) {
1888                 problemAccount = (*it_a).name();
1889                 rc << i18n("* Problem with account '%1'", problemAccount);
1890                 rc << i18n("  * Loop detected between this account and account '%1'.", (*it_b).name());
1891                 rc << i18n("    Reparenting account '%2' to top level account '%1'.", toplevel.name(), (*it_a).name());
1892                 (*it_a).setParentAccountId(toplevel.id());
1893                 if (accountRebuild.contains(toplevel.id()) == 0)
1894                   accountRebuild << toplevel.id();
1895                 if (accountRebuild.contains((*it_a).id()) == 0)
1896                   accountRebuild << (*it_a).id();
1897                 dropOut = true;
1898                 break;
1899               }
1900             }
1901           }
1902         }
1903         parentId = parent.parentAccountId();
1904       }
1905 
1906     } catch (const MyMoneyException &) {
1907       // if we don't know about a parent, we catch it later
1908     }
1909 
1910     // check that the parent exists
1911     parentId = (*it_a).parentAccountId();
1912     try {
1913       parent = account(parentId);
1914       if ((*it_a).accountGroup() != parent.accountGroup()) {
1915         problemCount++;
1916         if (problemAccount != (*it_a).name()) {
1917           problemAccount = (*it_a).name();
1918           rc << i18n("* Problem with account '%1'", problemAccount);
1919         }
1920         // the parent belongs to a different group, so we reconnect to the
1921         // master group account (asset, liability, etc) to which this account
1922         // should belong and update it in the engine.
1923         rc << i18n("  * Parent account '%1' belongs to a different group.", parent.name());
1924         rc << i18n("    New parent account is the top level account '%1'.", toplevel.name());
1925         (*it_a).setParentAccountId(toplevel.id());
1926 
1927         // make sure to rebuild the sub-accounts of the top account
1928         // and the one we removed this account from
1929         if (accountRebuild.contains(toplevel.id()) == 0)
1930           accountRebuild << toplevel.id();
1931         if (accountRebuild.contains(parent.id()) == 0)
1932           accountRebuild << parent.id();
1933       } else if (!parent.accountList().contains((*it_a).id())) {
1934         problemCount++;
1935         if (problemAccount != (*it_a).name()) {
1936           problemAccount = (*it_a).name();
1937           rc << i18n("* Problem with account '%1'", problemAccount);
1938         }
1939         // parent exists, but does not have a reference to the account
1940         rc << i18n("  * Parent account '%1' does not contain '%2' as sub-account.", parent.name(), problemAccount);
1941         if (accountRebuild.contains(parent.id()) == 0)
1942           accountRebuild << parent.id();
1943       }
1944     } catch (const MyMoneyException &) {
1945       // apparently, the parent does not exist anymore. we reconnect to the
1946       // master group account (asset, liability, etc) to which this account
1947       // should belong and update it in the engine.
1948       problemCount++;
1949       if (problemAccount != (*it_a).name()) {
1950         problemAccount = (*it_a).name();
1951         rc << i18n("* Problem with account '%1'", problemAccount);
1952       }
1953       rc << i18n("  * The parent with id %1 does not exist anymore.", parentId);
1954       rc << i18n("    New parent account is the top level account '%1'.", toplevel.name());
1955       (*it_a).setParentAccountId(toplevel.id());
1956 
1957       // make sure to rebuild the sub-accounts of the top account
1958       if (accountRebuild.contains(toplevel.id()) == 0)
1959         accountRebuild << toplevel.id();
1960     }
1961 
1962     // now check that all the children exist and have the correct type
1963     foreach (const auto accountID, (*it_a).accountList()) {
1964       // check that the child exists
1965       try {
1966         child = account(accountID);
1967         if (child.parentAccountId() != (*it_a).id()) {
1968           throw MYMONEYEXCEPTION_CSTRING("Child account has a different parent");
1969         }
1970       } catch (const MyMoneyException &) {
1971         problemCount++;
1972         if (problemAccount != (*it_a).name()) {
1973           problemAccount = (*it_a).name();
1974           rc << i18n("* Problem with account '%1'", problemAccount);
1975         }
1976         rc << i18n("  * Child account with id %1 does not exist anymore.", accountID);
1977         rc << i18n("    The child account list will be reconstructed.");
1978         if (accountRebuild.contains((*it_a).id()) == 0)
1979           accountRebuild << (*it_a).id();
1980       }
1981     }
1982 
1983     // see if it is a loan account. if so, remember the assigned interest account
1984     if ((*it_a).isLoan()) {
1985       MyMoneyAccountLoan loan(*it_a);
1986       if (!loan.interestAccountId().isEmpty()) {
1987         interestAccounts[loan.interestAccountId()] = true;
1988       }
1989       try {
1990         payee(loan.payee());
1991       } catch (const MyMoneyException &) {
1992         problemCount++;
1993         if (problemAccount != (*it_a).name()) {
1994           problemAccount = (*it_a).name();
1995           rc << i18n("* Problem with account '%1'", problemAccount);
1996         }
1997         rc << i18n("  * The payee with id %1 referenced by the loan does not exist anymore.", loan.payee());
1998         rc << i18n("    The payee will be removed.");
1999         // remove the payee - the account will be modified in the engine later
2000         (*it_a).deletePair("payee");
2001       }
2002     }
2003 
2004     // check if it is a category and set the date to 1900-01-01 if different
2005     if ((*it_a).isIncomeExpense()) {
2006       if (((*it_a).openingDate().isValid() == false) || ((*it_a).openingDate() != QDate(1900, 1, 1))) {
2007         (*it_a).setOpeningDate(QDate(1900, 1, 1));
2008       }
2009     }
2010 
2011     // check for clear text online password in the online settings
2012     if (!(*it_a).onlineBankingSettings().value("password").isEmpty()) {
2013       if (problemAccount != (*it_a).name()) {
2014         problemAccount = (*it_a).name();
2015         rc << i18n("* Problem with account '%1'", problemAccount);
2016       }
2017       rc << i18n("  * Older versions of KMyMoney stored an OFX password for this account in cleartext.");
2018       rc << i18n("    Please open it in the account editor (Account/Edit account) once and press OK.");
2019       rc << i18n("    This will store the password in the KDE wallet and remove the cleartext version.");
2020       ++unfixedCount;
2021     }
2022 
2023     // if the account was modified, we need to update it in the engine
2024     if (!(d->m_storage->account((*it_a).id()) == (*it_a))) {
2025       try {
2026         d->m_storage->modifyAccount(*it_a, true);
2027       } catch (const MyMoneyException &) {
2028         rc << i18n("  * Unable to update account data in engine.");
2029         return rc;
2030       }
2031     }
2032   }
2033 
2034   if (accountRebuild.count() != 0) {
2035     rc << i18n("* Reconstructing the child lists for");
2036   }
2037 
2038   // clear the affected lists
2039   for (it_a = list.begin(); it_a != list.end(); ++it_a) {
2040     if (accountRebuild.contains((*it_a).id())) {
2041       rc << QString("  %1").arg((*it_a).name());
2042       // clear the account list
2043       (*it_a).removeAccountIds();
2044     }
2045   }
2046 
2047   // reconstruct the lists
2048   for (it_a = list.begin(); it_a != list.end(); ++it_a) {
2049     QList<MyMoneyAccount>::Iterator it;
2050     parentId = (*it_a).parentAccountId();
2051     if (accountRebuild.contains(parentId)) {
2052       for (it = list.begin(); it != list.end(); ++it) {
2053         if ((*it).id() == parentId) {
2054           (*it).addAccountId((*it_a).id());
2055           break;
2056         }
2057       }
2058     }
2059   }
2060 
2061   // update the engine objects
2062   for (it_a = list.begin(); it_a != list.end(); ++it_a) {
2063     if (accountRebuild.contains((*it_a).id())) {
2064       try {
2065         d->m_storage->modifyAccount(*it_a, true);
2066       } catch (const MyMoneyException &) {
2067         rc << i18n("  * Unable to update account data for account %1 in engine", (*it_a).name());
2068       }
2069     }
2070   }
2071 
2072   // For some reason, files exist with invalid ids. This has been found in the payee id
2073   // so we fix them here
2074   QList<MyMoneyPayee> pList = payeeList();
2075   QMap<QString, QString>payeeConversionMap;
2076 
2077   for (it_p = pList.begin(); it_p != pList.end(); ++it_p) {
2078     if ((*it_p).id().length() > 7) {
2079       // found one of those with an invalid ids
2080       // create a new one and store it in the map.
2081       MyMoneyPayee payee = (*it_p);
2082       payee.clearId();
2083       d->m_storage->addPayee(payee);
2084       payeeConversionMap[(*it_p).id()] = payee.id();
2085       rc << i18n("  * Payee %1 recreated with fixed id", payee.name());
2086       ++problemCount;
2087     }
2088   }
2089 
2090   // Fix the transactions
2091   MyMoneyTransactionFilter filter;
2092   filter.setReportAllSplits(false);
2093   const auto tList = d->m_storage->transactionList(filter);
2094   // Generate the list of interest accounts
2095   for (const auto& transaction : tList) {
2096     const auto splits = transaction.splits();
2097     for (const auto& split : splits) {
2098       if (split.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::Interest))
2099         interestAccounts[split.accountId()] = true;
2100     }
2101   }
2102   QSet<Account::Type> supportedAccountTypes;
2103   supportedAccountTypes << Account::Type::Checkings
2104   << Account::Type::Savings
2105   << Account::Type::Cash
2106   << Account::Type::CreditCard
2107   << Account::Type::Asset
2108   << Account::Type::Liability;
2109   QSet<QString> reportedUnsupportedAccounts;
2110 
2111   for (const auto& transaction : tList) {
2112     MyMoneyTransaction t = transaction;
2113     bool tChanged = false;
2114     QDate accountOpeningDate;
2115     QStringList accountList;
2116     const auto splits = t.splits();
2117     foreach (const auto split, splits) {
2118       bool sChanged = false;
2119       MyMoneySplit s = split;
2120       if (payeeConversionMap.find(split.payeeId()) != payeeConversionMap.end()) {
2121         s.setPayeeId(payeeConversionMap[s.payeeId()]);
2122         sChanged = true;
2123         rc << i18n("  * Payee id updated in split of transaction '%1'.", t.id());
2124         ++problemCount;
2125       }
2126 
2127       try {
2128         const auto acc = this->account(s.accountId());
2129         // compute the newest opening date of all accounts involved in the transaction
2130         // in case the newest opening date is newer than the transaction post date, do one
2131         // of the following:
2132         //
2133         // a) for category and stock accounts: update the opening date of the account
2134         // b) for account types where the user cannot modify the opening date through
2135         //    the UI issue a warning (for each account only once)
2136         // c) others will be caught later
2137         if (!acc.isIncomeExpense() && !acc.isInvest()) {
2138           if (acc.openingDate() > t.postDate()) {
2139             if (!accountOpeningDate.isValid() || acc.openingDate() > accountOpeningDate) {
2140               accountOpeningDate = acc.openingDate();
2141             }
2142             accountList << this->accountToCategory(acc.id());
2143             if (!supportedAccountTypes.contains(acc.accountType())
2144                 && !reportedUnsupportedAccounts.contains(acc.id())) {
2145               rc << i18n("  * Opening date of Account '%1' cannot be changed to support transaction '%2' post date.",
2146                          this->accountToCategory(acc.id()), t.id());
2147               reportedUnsupportedAccounts << acc.id();
2148               ++unfixedCount;
2149             }
2150           }
2151         } else {
2152           if (acc.openingDate() > t.postDate()) {
2153             rc << i18n("  * Transaction '%1' post date '%2' is older than opening date '%4' of account '%3'.",
2154                        t.id(), t.postDate().toString(Qt::ISODate), this->accountToCategory(acc.id()), acc.openingDate().toString(Qt::ISODate));
2155 
2156             rc << i18n("    Account opening date updated.");
2157             MyMoneyAccount newAcc = acc;
2158             newAcc.setOpeningDate(t.postDate());
2159             this->modifyAccount(newAcc);
2160             ++problemCount;
2161           }
2162         }
2163 
2164         // make sure, that shares and value have the same number if they
2165         // represent the same currency.
2166         if (t.commodity() == acc.currencyId() && s.shares().reduce() != s.value().reduce()) {
2167           // use the value as master if the transaction is balanced
2168           if (t.splitSum().isZero()) {
2169             s.setShares(s.value());
2170             rc << i18n("  * shares set to value in split of transaction '%1'.", t.id());
2171           } else {
2172             s.setValue(s.shares());
2173             rc << i18n("  * value set to shares in split of transaction '%1'.", t.id());
2174           }
2175           sChanged = true;
2176           ++problemCount;
2177         }
2178       } catch (const MyMoneyException &) {
2179         rc << i18n("  * Split %2 in transaction '%1' contains a reference to invalid account %3. Please fix manually.", t.id(), split.id(), split.accountId());
2180         ++unfixedCount;
2181       }
2182 
2183       // make sure the interest splits are marked correct as such
2184       if (interestAccounts.find(s.accountId()) != interestAccounts.end()
2185           && s.action() != MyMoneySplit::actionName(eMyMoney::Split::Action::Interest)) {
2186         s.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::Interest));
2187         sChanged = true;
2188         rc << i18n("  * action marked as interest in split of transaction '%1'.", t.id());
2189         ++problemCount;
2190       }
2191 
2192       if (sChanged) {
2193         tChanged = true;
2194         t.modifySplit(s);
2195       }
2196     }
2197 
2198     // make sure that the transaction's post date is valid
2199     if (!t.postDate().isValid()) {
2200       tChanged = true;
2201       t.setPostDate(t.entryDate().isValid() ? t.entryDate() : QDate::currentDate());
2202       rc << i18n("  * Transaction '%1' has an invalid post date.", t.id());
2203       rc << i18n("    The post date was updated to '%1'.", QLocale().toString(t.postDate(), QLocale::ShortFormat));
2204       ++problemCount;
2205     }
2206     // check if the transaction's post date is after the opening date
2207     // of all accounts involved in the transaction. In case it is not,
2208     // issue a warning with the details about the transaction incl.
2209     // the account names and dates involved
2210     if (accountOpeningDate.isValid() && t.postDate() < accountOpeningDate) {
2211       QDate originalPostDate = t.postDate();
2212 #if 0
2213       // for now we do not activate the logic to move the post date to a later
2214       // point in time. This could cause some severe trouble if you have lots
2215       // of ancient data collected with older versions of KMyMoney that did not
2216       // enforce certain conditions like we do now.
2217       t.setPostDate(accountOpeningDate);
2218       tChanged = true;
2219       // copy the price information for investments to the new date
2220       QList<MyMoneySplit>::const_iterator it_t;
2221       foreach (const auto split, t.splits()) {
2222         if ((split.action() != "Buy") &&
2223             (split.action() != "Reinvest")) {
2224           continue;
2225         }
2226         QString id = split.accountId();
2227         auto acc = this->account(id);
2228         MyMoneySecurity sec = this->security(acc.currencyId());
2229         MyMoneyPrice price(acc.currencyId(),
2230                            sec.tradingCurrency(),
2231                            t.postDate(),
2232                            split.price(), "Transaction");
2233         this->addPrice(price);
2234         break;
2235       }
2236 #endif
2237       rc << i18n("  * Transaction '%1' has a post date '%2' before one of the referenced account's opening date.", t.id(), QLocale().toString(originalPostDate, QLocale::ShortFormat));
2238       rc << i18n("    Referenced accounts: %1", accountList.join(","));
2239       rc << i18n("    The post date was not updated to '%1'.", QLocale().toString(accountOpeningDate, QLocale::ShortFormat));
2240       ++unfixedCount;
2241     }
2242 
2243     if (tChanged) {
2244       d->m_storage->modifyTransaction(t);
2245     }
2246   }
2247 
2248   // Fix the schedules
2249   QList<MyMoneySchedule> schList = scheduleList();
2250   for (it_sch = schList.begin(); it_sch != schList.end(); ++it_sch) {
2251     MyMoneySchedule sch = (*it_sch);
2252     MyMoneyTransaction t = sch.transaction();
2253     auto tChanged = false;
2254     foreach (const auto split, t.splits()) {
2255       MyMoneySplit s = split;
2256       bool sChanged = false;
2257       if (payeeConversionMap.find(split.payeeId()) != payeeConversionMap.end()) {
2258         s.setPayeeId(payeeConversionMap[s.payeeId()]);
2259         sChanged = true;
2260         rc << i18n("  * Payee id updated in split of schedule '%1'.", (*it_sch).name());
2261         ++problemCount;
2262       }
2263       if (!split.value().isZero() && split.shares().isZero()) {
2264         s.setShares(s.value());
2265         sChanged = true;
2266         rc << i18n("  * Split in scheduled transaction '%1' contained value != 0 and shares == 0.", (*it_sch).name());
2267         rc << i18n("    Shares set to value.");
2268         ++problemCount;
2269       }
2270 
2271       // make sure, we don't have a bankid stored with a split in a schedule
2272       if (!split.bankID().isEmpty()) {
2273         s.setBankID(QString());
2274         sChanged = true;
2275         rc << i18n("  * Removed bankid from split in scheduled transaction '%1'.", (*it_sch).name());
2276         ++problemCount;
2277       }
2278 
2279       // make sure, that shares and value have the same number if they
2280       // represent the same currency.
2281       try {
2282         const auto acc = this->account(s.accountId());
2283         if (t.commodity() == acc.currencyId()
2284             && s.shares().reduce() != s.value().reduce()) {
2285           // use the value as master if the transaction is balanced
2286           if (t.splitSum().isZero()) {
2287             s.setShares(s.value());
2288             rc << i18n("  * shares set to value in split in schedule '%1'.", (*it_sch).name());
2289           } else {
2290             s.setValue(s.shares());
2291             rc << i18n("  * value set to shares in split in schedule '%1'.", (*it_sch).name());
2292           }
2293           sChanged = true;
2294           ++problemCount;
2295         }
2296       } catch (const MyMoneyException &) {
2297         rc << i18n("  * Split %2 in schedule '%1' contains a reference to invalid account %3. Please fix manually.", (*it_sch).name(), split.id(), split.accountId());
2298         ++unfixedCount;
2299       }
2300       if (sChanged) {
2301         t.modifySplit(s);
2302         tChanged = true;
2303       }
2304     }
2305     if (tChanged) {
2306       sch.setTransaction(t);
2307       d->m_storage->modifySchedule(sch);
2308     }
2309   }
2310 
2311   // Fix the reports
2312   QList<MyMoneyReport> rList = reportList();
2313   for (it_r = rList.begin(); it_r != rList.end(); ++it_r) {
2314     MyMoneyReport r = *it_r;
2315     QStringList payeeList;
2316     (*it_r).payees(payeeList);
2317     bool rChanged = false;
2318     for (auto it_payee = payeeList.begin(); it_payee != payeeList.end(); ++it_payee) {
2319       if (payeeConversionMap.find(*it_payee) != payeeConversionMap.end()) {
2320         rc << i18n("  * Payee id updated in report '%1'.", (*it_r).name());
2321         ++problemCount;
2322         r.removeReference(*it_payee);
2323         r.addPayee(payeeConversionMap[*it_payee]);
2324         rChanged = true;
2325       }
2326     }
2327     if (rChanged) {
2328       d->m_storage->modifyReport(r);
2329     }
2330   }
2331 
2332   // erase old payee ids
2333   QMap<QString, QString>::Iterator it_m;
2334   for (it_m = payeeConversionMap.begin(); it_m != payeeConversionMap.end(); ++it_m) {
2335     MyMoneyPayee payee = this->payee(it_m.key());
2336     removePayee(payee);
2337     rc << i18n("  * Payee '%1' removed.", payee.id());
2338     ++problemCount;
2339   }
2340 
2341   //look for accounts which have currencies other than the base currency but no price on the opening date
2342   //all accounts using base currency are excluded, since that's the base used for foreign currency calculation
2343   //thus it is considered as always present
2344   //accounts that represent Income/Expense categories are also excluded as price is irrelevant for their
2345   //fake opening date since a forex rate is required for all multi-currency transactions
2346 
2347   //get all currencies in use
2348   QStringList currencyList;
2349   QList<MyMoneyAccount> accountForeignCurrency;
2350   QList<MyMoneyAccount> accList;
2351   accountList(accList);
2352   QList<MyMoneyAccount>::const_iterator account_it;
2353   for (account_it = accList.constBegin(); account_it != accList.constEnd(); ++account_it) {
2354     MyMoneyAccount account = *account_it;
2355     if (!account.isIncomeExpense()
2356         && !currencyList.contains(account.currencyId())
2357         && account.currencyId() != baseCurrency().id()
2358         && !account.currencyId().isEmpty()) {
2359       //add the currency and the account-currency pair
2360       currencyList.append(account.currencyId());
2361       accountForeignCurrency.append(account);
2362     }
2363   }
2364 
2365   MyMoneyPriceList pricesList = priceList();
2366   QMap<MyMoneySecurityPair, QDate> securityPriceDate;
2367 
2368   //get the first date of the price for each security
2369   MyMoneyPriceList::const_iterator prices_it;
2370   for (prices_it = pricesList.constBegin(); prices_it != pricesList.constEnd(); ++prices_it) {
2371     MyMoneyPrice firstPrice = (*((*prices_it).constBegin()));
2372 
2373     //only check the price if the currency is in use
2374     if (currencyList.contains(firstPrice.from()) || currencyList.contains(firstPrice.to())) {
2375       //check the security in the from field
2376       //if it is there, check if it is older
2377       QPair<QString, QString> pricePair = qMakePair(firstPrice.from(), firstPrice.to());
2378       securityPriceDate[pricePair] = firstPrice.date();
2379     }
2380   }
2381 
2382   //compare the dates with the opening dates of the accounts using each currency
2383   QList<MyMoneyAccount>::const_iterator accForeignList_it;
2384   bool firstInvProblem = true;
2385   for (accForeignList_it = accountForeignCurrency.constBegin(); accForeignList_it != accountForeignCurrency.constEnd(); ++accForeignList_it) {
2386     //setup the price pair correctly
2387     QPair<QString, QString> pricePair;
2388     //setup the reverse, which can also be used for rate conversion
2389     QPair<QString, QString> reversePricePair;
2390     if ((*accForeignList_it).isInvest()) {
2391       //if it is a stock, we have to search for a price from its stock to the currency of the account
2392       QString securityId = (*accForeignList_it).currencyId();
2393       QString tradingCurrencyId = security(securityId).tradingCurrency();
2394       pricePair = qMakePair(securityId, tradingCurrencyId);
2395       reversePricePair = qMakePair(tradingCurrencyId, securityId);
2396     } else {
2397       //if it is a regular account we search for a price from the currency of the account to the base currency
2398       QString currency = (*accForeignList_it).currencyId();
2399       QString baseCurrencyId = baseCurrency().id();
2400       pricePair = qMakePair(currency, baseCurrencyId);
2401       reversePricePair = qMakePair(baseCurrencyId, currency);
2402     }
2403 
2404     //compare the first price with the opening date of the account
2405     if ((!securityPriceDate.contains(pricePair) || securityPriceDate.value(pricePair) > (*accForeignList_it).openingDate())
2406         && (!securityPriceDate.contains(reversePricePair) || securityPriceDate.value(reversePricePair) > (*accForeignList_it).openingDate())) {
2407       if (firstInvProblem) {
2408         firstInvProblem = false;
2409         rc << i18n("* Potential problem with securities/currencies");
2410       }
2411       QDate openingDate = (*accForeignList_it).openingDate();
2412       MyMoneySecurity secError = security((*accForeignList_it).currencyId());
2413       if (!(*accForeignList_it).isInvest()) {
2414         rc << i18n("  * The account '%1' in currency '%2' has no price set for the opening date '%3'.", (*accForeignList_it).name(), secError.name(), openingDate.toString(Qt::ISODate));
2415         rc << i18n("    Please enter a price for the currency on or before the opening date.");
2416       } else {
2417         rc << i18n("  * The security '%1' has no price set for the opening date '%2'.", (*accForeignList_it).name(), openingDate.toString(Qt::ISODate));
2418         rc << i18n("    Please enter a price for the security on or before the opening date.");
2419       }
2420       ++unfixedCount;
2421     }
2422   }
2423 
2424   // Fix the budgets that somehow still reference invalid accounts
2425   QString problemBudget;
2426   QList<MyMoneyBudget> bList = budgetList();
2427   for (QList<MyMoneyBudget>::const_iterator it_b = bList.constBegin(); it_b != bList.constEnd(); ++it_b) {
2428     MyMoneyBudget b = *it_b;
2429     QList<MyMoneyBudget::AccountGroup> baccounts = b.getaccounts();
2430     bool bChanged = false;
2431     for (QList<MyMoneyBudget::AccountGroup>::const_iterator it_bacc = baccounts.constBegin(); it_bacc != baccounts.constEnd(); ++it_bacc) {
2432       try {
2433         account((*it_bacc).id());
2434       } catch (const MyMoneyException &) {
2435         problemCount++;
2436         if (problemBudget != b.name()) {
2437           problemBudget = b.name();
2438           rc << i18n("* Problem with budget '%1'", problemBudget);
2439         }
2440         rc << i18n("  * The account with id %1 referenced by the budget does not exist anymore.", (*it_bacc).id());
2441         rc << i18n("    The account reference will be removed.");
2442         // remove the reference to the account
2443         b.removeReference((*it_bacc).id());
2444         bChanged = true;
2445       }
2446     }
2447     if (bChanged) {
2448       d->m_storage->modifyBudget(b);
2449     }
2450   }
2451 
2452   // add more checks here
2453 
2454   if (problemCount == 0 && unfixedCount == 0) {
2455     rc << i18n("Finished: data is consistent.");
2456   } else {
2457     const QString problemsCorrected = i18np("%1 problem corrected.", "%1 problems corrected.", problemCount);
2458     const QString problemsRemaining = i18np("%1 problem still present.", "%1 problems still present.", unfixedCount);
2459 
2460     rc << QString();
2461     rc << i18nc("%1 is a string, e.g. 7 problems corrected; %2 is a string, e.g. 3 problems still present", "Finished: %1 %2", problemsCorrected, problemsRemaining);
2462   }
2463   return rc;
2464 }
2465 
createCategory(const MyMoneyAccount & base,const QString & name)2466 QString MyMoneyFile::createCategory(const MyMoneyAccount& base, const QString& name)
2467 {
2468   d->checkTransaction(Q_FUNC_INFO);
2469 
2470   MyMoneyAccount parent = base;
2471   QString categoryText;
2472 
2473   if (base.id() != expense().id() && base.id() != income().id())
2474     throw MYMONEYEXCEPTION_CSTRING("Invalid base category");
2475 
2476   QStringList subAccounts = name.split(AccountSeparator);
2477   QStringList::Iterator it;
2478   for (it = subAccounts.begin(); it != subAccounts.end(); ++it) {
2479     MyMoneyAccount categoryAccount;
2480 
2481     categoryAccount.setName(*it);
2482     categoryAccount.setAccountType(base.accountType());
2483 
2484     if (it == subAccounts.begin())
2485       categoryText += *it;
2486     else
2487       categoryText += (AccountSeparator + *it);
2488 
2489     // Only create the account if it doesn't exist
2490     try {
2491       QString categoryId = categoryToAccount(categoryText);
2492       if (categoryId.isEmpty())
2493         addAccount(categoryAccount, parent);
2494       else {
2495         categoryAccount = account(categoryId);
2496       }
2497     } catch (const MyMoneyException &e) {
2498       qDebug("Unable to add account %s, %s, %s: %s",
2499              qPrintable(categoryAccount.name()),
2500              qPrintable(parent.name()),
2501              qPrintable(categoryText),
2502              e.what());
2503     }
2504 
2505     parent = categoryAccount;
2506   }
2507 
2508   return categoryToAccount(name);
2509 }
2510 
checkCategory(const QString & name,const MyMoneyMoney & value,const MyMoneyMoney & value2)2511 QString MyMoneyFile::checkCategory(const QString& name, const MyMoneyMoney& value, const MyMoneyMoney& value2)
2512 {
2513   QString accountId;
2514   MyMoneyAccount newAccount;
2515   bool found = true;
2516 
2517   if (!name.isEmpty()) {
2518     // The category might be constructed with an arbitrary depth (number of
2519     // colon delimited fields). We try to find a parent account within this
2520     // hierarchy by searching the following sequence:
2521     //
2522     //    aaaa:bbbb:cccc:ddddd
2523     //
2524     // 1. search aaaa:bbbb:cccc:dddd, create nothing
2525     // 2. search aaaa:bbbb:cccc     , create dddd
2526     // 3. search aaaa:bbbb          , create cccc:dddd
2527     // 4. search aaaa               , create bbbb:cccc:dddd
2528     // 5. don't search              , create aaaa:bbbb:cccc:dddd
2529 
2530     newAccount.setName(name);
2531     QString accName;      // part to be created (right side in above list)
2532     QString parent(name); // a possible parent part (left side in above list)
2533     do {
2534       accountId = categoryToAccount(parent);
2535       if (accountId.isEmpty()) {
2536         found = false;
2537         // prepare next step
2538         if (!accName.isEmpty())
2539           accName.prepend(':');
2540         accName.prepend(parent.section(':', -1));
2541         newAccount.setName(accName);
2542         parent = parent.section(':', 0, -2);
2543       } else if (!accName.isEmpty()) {
2544         newAccount.setParentAccountId(accountId);
2545       }
2546     } while (!parent.isEmpty() && accountId.isEmpty());
2547 
2548     // if we did not find the category, we create it
2549     if (!found) {
2550         MyMoneyAccount parentAccount;
2551       if (newAccount.parentAccountId().isEmpty()) {
2552         if (!value.isNegative() && value2.isNegative())
2553           parentAccount = income();
2554         else
2555           parentAccount = expense();
2556       } else {
2557         parentAccount = account(newAccount.parentAccountId());
2558       }
2559       newAccount.setAccountType((!value.isNegative() && value2.isNegative()) ? Account::Type::Income : Account::Type::Expense);
2560       MyMoneyAccount brokerage;
2561       // clear out the parent id, because createAccount() does not like that
2562       newAccount.setParentAccountId(QString());
2563       createAccount(newAccount, parentAccount, brokerage, MyMoneyMoney());
2564       accountId = newAccount.id();
2565     }
2566   }
2567 
2568   return accountId;
2569 }
2570 
addSecurity(MyMoneySecurity & security)2571 void MyMoneyFile::addSecurity(MyMoneySecurity& security)
2572 {
2573   d->checkTransaction(Q_FUNC_INFO);
2574 
2575   d->m_storage->addSecurity(security);
2576   d->m_changeSet += MyMoneyNotification(File::Mode::Add, security);
2577 }
2578 
modifySecurity(const MyMoneySecurity & security)2579 void MyMoneyFile::modifySecurity(const MyMoneySecurity& security)
2580 {
2581   d->checkTransaction(Q_FUNC_INFO);
2582 
2583   d->m_storage->modifySecurity(security);
2584   d->m_changeSet += MyMoneyNotification(File::Mode::Modify, security);
2585 }
2586 
removeSecurity(const MyMoneySecurity & security)2587 void MyMoneyFile::removeSecurity(const MyMoneySecurity& security)
2588 {
2589   d->checkTransaction(Q_FUNC_INFO);
2590 
2591   // FIXME check that security is not referenced by other object
2592 
2593   d->m_storage->removeSecurity(security);
2594   d->m_changeSet += MyMoneyNotification(File::Mode::Remove, security);
2595 }
2596 
security(const QString & id) const2597 MyMoneySecurity MyMoneyFile::security(const QString& id) const
2598 {
2599   if (Q_UNLIKELY(id.isEmpty()))
2600     return baseCurrency();
2601 
2602   return d->m_storage->security(id);
2603 }
2604 
securityList() const2605 QList<MyMoneySecurity> MyMoneyFile::securityList() const
2606 {
2607   d->checkStorage();
2608 
2609   return d->m_storage->securityList();
2610 }
2611 
addCurrency(const MyMoneySecurity & currency)2612 void MyMoneyFile::addCurrency(const MyMoneySecurity& currency)
2613 {
2614   d->checkTransaction(Q_FUNC_INFO);
2615 
2616   d->m_storage->addCurrency(currency);
2617   d->m_changeSet += MyMoneyNotification(File::Mode::Add, currency);
2618 }
2619 
modifyCurrency(const MyMoneySecurity & currency)2620 void MyMoneyFile::modifyCurrency(const MyMoneySecurity& currency)
2621 {
2622   d->checkTransaction(Q_FUNC_INFO);
2623 
2624   // force reload of base currency object
2625   if (currency.id() == d->m_baseCurrency.id())
2626     d->m_baseCurrency.clearId();
2627 
2628   d->m_storage->modifyCurrency(currency);
2629   d->m_changeSet += MyMoneyNotification(File::Mode::Modify, currency);
2630 }
2631 
removeCurrency(const MyMoneySecurity & currency)2632 void MyMoneyFile::removeCurrency(const MyMoneySecurity& currency)
2633 {
2634   d->checkTransaction(Q_FUNC_INFO);
2635 
2636   if (currency.id() == d->m_baseCurrency.id())
2637     throw MYMONEYEXCEPTION_CSTRING("Cannot delete base currency.");
2638 
2639   // FIXME check that security is not referenced by other object
2640 
2641   d->m_storage->removeCurrency(currency);
2642   d->m_changeSet += MyMoneyNotification(File::Mode::Remove, currency);
2643 }
2644 
currency(const QString & id) const2645 MyMoneySecurity MyMoneyFile::currency(const QString& id) const
2646 {
2647   if (id.isEmpty())
2648     return baseCurrency();
2649 
2650   try {
2651     const auto currency = d->m_storage->currency(id);
2652     if (currency.id().isEmpty())
2653       throw MYMONEYEXCEPTION(QString::fromLatin1("Currency '%1' not found.").arg(id));
2654 
2655     return currency;
2656 
2657   } catch (const MyMoneyException &) {
2658     const auto security = d->m_storage->security(id);
2659     if (security.id().isEmpty()) {
2660       throw MYMONEYEXCEPTION(QString::fromLatin1("Security '%1' not found.").arg(id));
2661     }
2662     return security;
2663   }
2664 }
2665 
ancientCurrencies() const2666 QMap<MyMoneySecurity, MyMoneyPrice> MyMoneyFile::ancientCurrencies() const
2667 {
2668   QMap<MyMoneySecurity, MyMoneyPrice> ancientCurrencies;
2669 
2670   ancientCurrencies.insert(MyMoneySecurity("ATS", i18n("Austrian Schilling"), QString::fromUtf8("ÖS")),     MyMoneyPrice("ATS", "EUR", QDate(1998, 12, 31), MyMoneyMoney(10000, 137603), QLatin1Literal("KMyMoney")));
2671   ancientCurrencies.insert(MyMoneySecurity("DEM", i18n("German Mark"), "DM"),            MyMoneyPrice("ATS", "EUR", QDate(1998, 12, 31), MyMoneyMoney(100000, 195583), QLatin1Literal("KMyMoney")));
2672   ancientCurrencies.insert(MyMoneySecurity("FRF", i18n("French Franc"), "FF"),           MyMoneyPrice("FRF", "EUR", QDate(1998, 12, 31), MyMoneyMoney(100000, 655957), QLatin1Literal("KMyMoney")));
2673   ancientCurrencies.insert(MyMoneySecurity("ITL", i18n("Italian Lira"), QChar(0x20A4)),  MyMoneyPrice("ITL", "EUR", QDate(1998, 12, 31), MyMoneyMoney(100, 193627), QLatin1Literal("KMyMoney")));
2674   ancientCurrencies.insert(MyMoneySecurity("ESP", i18n("Spanish Peseta"), QString()),    MyMoneyPrice("ESP", "EUR", QDate(1998, 12, 31), MyMoneyMoney(1000, 166386), QLatin1Literal("KMyMoney")));
2675   ancientCurrencies.insert(MyMoneySecurity("NLG", i18n("Dutch Guilder"), QString()),     MyMoneyPrice("NLG", "EUR", QDate(1998, 12, 31), MyMoneyMoney(100000, 220371), QLatin1Literal("KMyMoney")));
2676   ancientCurrencies.insert(MyMoneySecurity("BEF", i18n("Belgian Franc"), "Fr"),          MyMoneyPrice("BEF", "EUR", QDate(1998, 12, 31), MyMoneyMoney(10000, 403399), QLatin1Literal("KMyMoney")));
2677   ancientCurrencies.insert(MyMoneySecurity("LUF", i18n("Luxembourg Franc"), "Fr"),       MyMoneyPrice("LUF", "EUR", QDate(1998, 12, 31), MyMoneyMoney(10000, 403399), QLatin1Literal("KMyMoney")));
2678   ancientCurrencies.insert(MyMoneySecurity("PTE", i18n("Portuguese Escudo"), QString()), MyMoneyPrice("PTE", "EUR", QDate(1998, 12, 31), MyMoneyMoney(1000, 200482), QLatin1Literal("KMyMoney")));
2679   ancientCurrencies.insert(MyMoneySecurity("IEP", i18n("Irish Pound"), QChar(0x00A3)),   MyMoneyPrice("IEP", "EUR", QDate(1998, 12, 31), MyMoneyMoney(1000000, 787564), QLatin1Literal("KMyMoney")));
2680   ancientCurrencies.insert(MyMoneySecurity("FIM", i18n("Finnish Markka"), QString()),    MyMoneyPrice("FIM", "EUR", QDate(1998, 12, 31), MyMoneyMoney(100000, 594573), QLatin1Literal("KMyMoney")));
2681   ancientCurrencies.insert(MyMoneySecurity("GRD", i18n("Greek Drachma"), QChar(0x20AF)), MyMoneyPrice("GRD", "EUR", QDate(1998, 12, 31), MyMoneyMoney(100, 34075), QLatin1Literal("KMyMoney")));
2682 
2683     // https://en.wikipedia.org/wiki/Bulgarian_lev
2684   ancientCurrencies.insert(MyMoneySecurity("BGL", i18n("Bulgarian Lev"), "BGL"), MyMoneyPrice("BGL", "BGN", QDate(1999, 7, 5), MyMoneyMoney(1, 1000), QLatin1Literal("KMyMoney")));
2685 
2686   ancientCurrencies.insert(MyMoneySecurity("ROL", i18n("Romanian Leu"), "ROL"), MyMoneyPrice("ROL", "RON", QDate(2005, 6, 30), MyMoneyMoney(1, 10000), QLatin1Literal("KMyMoney")));
2687 
2688   ancientCurrencies.insert(MyMoneySecurity("RUR", i18n("Russian Ruble (old)"), "RUR"), MyMoneyPrice("RUR", "RUB", QDate(1998, 1, 1), MyMoneyMoney(1, 1000), QLatin1Literal("KMyMoney")));
2689 
2690   ancientCurrencies.insert(MyMoneySecurity("SIT", i18n("Slovenian Tolar"), "SIT"), MyMoneyPrice("SIT", "EUR", QDate(2006, 12, 31), MyMoneyMoney(1, 23964), QLatin1Literal("KMyMoney")));
2691 
2692     // Source: https://en.wikipedia.org/wiki/Turkish_lira
2693   ancientCurrencies.insert(MyMoneySecurity("TRL", i18n("Turkish Lira (old)"), "TL"), MyMoneyPrice("TRL", "TRY", QDate(2004, 12, 31), MyMoneyMoney(1, 1000000), QLatin1Literal("KMyMoney")));
2694 
2695     // Source: https://www.focus.de/finanzen/news/malta-und-zypern_aid_66058.html
2696   ancientCurrencies.insert(MyMoneySecurity("MTL", i18n("Maltese Lira"), "MTL"), MyMoneyPrice("MTL", "EUR", QDate(2008, 1, 1), MyMoneyMoney(429300, 1000000), QLatin1Literal("KMyMoney")));
2697   ancientCurrencies.insert(MyMoneySecurity("CYP", i18n("Cyprus Pound"), QString("C%1").arg(QChar(0x00A3))), MyMoneyPrice("CYP", "EUR", QDate(2008, 1, 1), MyMoneyMoney(585274, 1000000), QLatin1Literal("KMyMoney")));
2698 
2699     // Source: https://www.focus.de/finanzen/news/waehrungszone-slowakei-ist-neuer-euro-staat_aid_359025.html
2700   ancientCurrencies.insert(MyMoneySecurity("SKK", i18n("Slovak Koruna"), "SKK"), MyMoneyPrice("SKK", "EUR", QDate(2008, 12, 31), MyMoneyMoney(1000, 30126), QLatin1Literal("KMyMoney")));
2701 
2702     // Source: https://en.wikipedia.org/wiki/Mozambican_metical
2703   ancientCurrencies.insert(MyMoneySecurity("MZM", i18n("Mozambique Metical"), "MT"), MyMoneyPrice("MZM", "MZN", QDate(2006, 7, 1), MyMoneyMoney(1, 1000), QLatin1Literal("KMyMoney")));
2704 
2705     // Source https://en.wikipedia.org/wiki/Azerbaijani_manat
2706   ancientCurrencies.insert(MyMoneySecurity("AZM", i18n("Azerbaijani Manat"), "m."), MyMoneyPrice("AZM", "AZN", QDate(2006, 1, 1), MyMoneyMoney(1, 5000), QLatin1Literal("KMyMoney")));
2707 
2708     // Source: https://en.wikipedia.org/wiki/Litas
2709   ancientCurrencies.insert(MyMoneySecurity("LTL", i18n("Lithuanian Litas"), "Lt"), MyMoneyPrice("LTL", "EUR", QDate(2015, 1, 1), MyMoneyMoney(100000, 345280), QLatin1Literal("KMyMoney")));
2710 
2711     // Source: https://en.wikipedia.org/wiki/Belarusian_ruble
2712   ancientCurrencies.insert(MyMoneySecurity("BYR", i18n("Belarusian Ruble (old)"), "BYR"), MyMoneyPrice("BYR", "BYN", QDate(2016, 7, 1), MyMoneyMoney(1, 10000), QLatin1Literal("KMyMoney")));
2713 
2714   // Source: https://en.wikipedia.org/wiki/Zambian_kwacha, triggered by b.k.o ticket #425530
2715   ancientCurrencies.insert(MyMoneySecurity("ZMK", i18n("Zambian Kwacha (old)"), "K"), MyMoneyPrice("ZMK", "ZMW", QDate(2013, 1, 1), MyMoneyMoney(1, 1000), QLatin1Literal("KMyMoney")));
2716 
2717   return ancientCurrencies;
2718 }
2719 
availableCurrencyList() const2720 QList<MyMoneySecurity> MyMoneyFile::availableCurrencyList() const
2721 {
2722   QList<MyMoneySecurity> currencyList;
2723   currencyList.append(MyMoneySecurity("AFA", i18n("Afghanistan Afghani")));
2724   currencyList.append(MyMoneySecurity("ALL", i18n("Albanian Lek")));
2725   currencyList.append(MyMoneySecurity("ANG", i18n("Netherland Antillian Guilder")));
2726   currencyList.append(MyMoneySecurity("DZD", i18n("Algerian Dinar")));
2727   currencyList.append(MyMoneySecurity("ADF", i18n("Andorran Franc")));
2728   currencyList.append(MyMoneySecurity("ADP", i18n("Andorran Peseta")));
2729   currencyList.append(MyMoneySecurity("AOA", i18n("Angolan Kwanza"),         "Kz"));
2730   currencyList.append(MyMoneySecurity("ARS", i18n("Argentine Peso"),         "$"));
2731   currencyList.append(MyMoneySecurity("AWG", i18n("Aruban Florin")));
2732   currencyList.append(MyMoneySecurity("AUD", i18n("Australian Dollar"),      "$"));
2733   currencyList.append(MyMoneySecurity("AZN", i18n("Azerbaijani Manat"),      "m."));
2734   currencyList.append(MyMoneySecurity("BSD", i18n("Bahamian Dollar"),        "$"));
2735   currencyList.append(MyMoneySecurity("BHD", i18n("Bahraini Dinar"),         "BHD", 1000));
2736   currencyList.append(MyMoneySecurity("BDT", i18n("Bangladeshi Taka")));
2737   currencyList.append(MyMoneySecurity("BBD", i18n("Barbados Dollar"),        "$"));
2738   currencyList.append(MyMoneySecurity("BTC", i18n("Bitcoin"),                "BTC", 100000000, 100000000));
2739   currencyList.append(MyMoneySecurity("BYN", i18n("Belarusian Ruble"),       "Br"));
2740   currencyList.append(MyMoneySecurity("BZD", i18n("Belize Dollar"),          "$"));
2741   currencyList.append(MyMoneySecurity("BMD", i18n("Bermudian Dollar"),       "$"));
2742   currencyList.append(MyMoneySecurity("BTN", i18n("Bhutan Ngultrum")));
2743   currencyList.append(MyMoneySecurity("BOB", i18n("Bolivian Boliviano")));
2744   currencyList.append(MyMoneySecurity("BAM", i18n("Bosnian Convertible Mark")));
2745   currencyList.append(MyMoneySecurity("BWP", i18n("Botswana Pula")));
2746   currencyList.append(MyMoneySecurity("BRL", i18n("Brazilian Real"),         "R$"));
2747   currencyList.append(MyMoneySecurity("GBP", i18n("British Pound"),          QChar(0x00A3)));
2748   currencyList.append(MyMoneySecurity("BND", i18n("Brunei Dollar"),          "$"));
2749   currencyList.append(MyMoneySecurity("BGN", i18n("Bulgarian Lev (new)")));
2750   currencyList.append(MyMoneySecurity("BIF", i18n("Burundi Franc")));
2751   currencyList.append(MyMoneySecurity("XAF", i18n("CFA Franc BEAC")));
2752   currencyList.append(MyMoneySecurity("XOF", i18n("CFA Franc BCEAO")));
2753   currencyList.append(MyMoneySecurity("XPF", i18n("CFP Franc Pacifique"), "F", 1, 100));
2754   currencyList.append(MyMoneySecurity("KHR", i18n("Cambodia Riel")));
2755   currencyList.append(MyMoneySecurity("CAD", i18n("Canadian Dollar"),        "$"));
2756   currencyList.append(MyMoneySecurity("CVE", i18n("Cape Verde Escudo")));
2757   currencyList.append(MyMoneySecurity("KYD", i18n("Cayman Islands Dollar"),  "$"));
2758   currencyList.append(MyMoneySecurity("CLP", i18n("Chilean Peso")));
2759   currencyList.append(MyMoneySecurity("CNY", i18n("Chinese Yuan Renminbi")));
2760   currencyList.append(MyMoneySecurity("COP", i18n("Colombian Peso")));
2761   currencyList.append(MyMoneySecurity("KMF", i18n("Comoros Franc")));
2762   currencyList.append(MyMoneySecurity("CRC", i18n("Costa Rican Colon"),      QChar(0x20A1)));
2763   currencyList.append(MyMoneySecurity("HRK", i18n("Croatian Kuna")));
2764   currencyList.append(MyMoneySecurity("CUP", i18n("Cuban Peso")));
2765   currencyList.append(MyMoneySecurity("CUC", i18n("Cuban Convertible Peso")));
2766   currencyList.append(MyMoneySecurity("CZK", i18n("Czech Koruna")));
2767   currencyList.append(MyMoneySecurity("DKK", i18n("Danish Krone"),           "kr"));
2768   currencyList.append(MyMoneySecurity("DJF", i18n("Djibouti Franc")));
2769   currencyList.append(MyMoneySecurity("DOP", i18n("Dominican Peso")));
2770   currencyList.append(MyMoneySecurity("XCD", i18n("East Caribbean Dollar"),  "$"));
2771   currencyList.append(MyMoneySecurity("EGP", i18n("Egyptian Pound"),         QChar(0x00A3)));
2772   currencyList.append(MyMoneySecurity("SVC", i18n("El Salvador Colon")));
2773   currencyList.append(MyMoneySecurity("ERN", i18n("Eritrean Nakfa")));
2774   currencyList.append(MyMoneySecurity("EEK", i18n("Estonian Kroon")));
2775   currencyList.append(MyMoneySecurity("ETB", i18n("Ethiopian Birr")));
2776   currencyList.append(MyMoneySecurity("EUR", i18n("Euro"),                   QChar(0x20ac)));
2777   currencyList.append(MyMoneySecurity("FKP", i18n("Falkland Islands Pound"), QChar(0x00A3)));
2778   currencyList.append(MyMoneySecurity("FJD", i18n("Fiji Dollar"),            "$"));
2779   currencyList.append(MyMoneySecurity("GMD", i18n("Gambian Dalasi")));
2780   currencyList.append(MyMoneySecurity("GEL", i18n("Georgian Lari")));
2781   currencyList.append(MyMoneySecurity("GHC", i18n("Ghanaian Cedi")));
2782   currencyList.append(MyMoneySecurity("GIP", i18n("Gibraltar Pound"),        QChar(0x00A3)));
2783   currencyList.append(MyMoneySecurity("GTQ", i18n("Guatemalan Quetzal")));
2784   currencyList.append(MyMoneySecurity("GWP", i18n("Guinea-Bissau Peso")));
2785   currencyList.append(MyMoneySecurity("GYD", i18n("Guyanan Dollar"),         "$"));
2786   currencyList.append(MyMoneySecurity("HTG", i18n("Haitian Gourde")));
2787   currencyList.append(MyMoneySecurity("HNL", i18n("Honduran Lempira")));
2788   currencyList.append(MyMoneySecurity("HKD", i18n("Hong Kong Dollar"),       "$"));
2789   currencyList.append(MyMoneySecurity("HUF", i18n("Hungarian Forint"),       "HUF", 1, 100));
2790   currencyList.append(MyMoneySecurity("ISK", i18n("Iceland Krona")));
2791   currencyList.append(MyMoneySecurity("INR", i18n("Indian Rupee"),           QChar(0x20B9)));
2792   currencyList.append(MyMoneySecurity("IDR", i18n("Indonesian Rupiah"),      "IDR", 1, 0, 10));
2793   currencyList.append(MyMoneySecurity("IRR", i18n("Iranian Rial"),           "IRR", 1));
2794   currencyList.append(MyMoneySecurity("IQD", i18n("Iraqi Dinar"),            "IQD", 1000));
2795   currencyList.append(MyMoneySecurity("ILS", i18n("Israeli New Shekel"),     QChar(0x20AA)));
2796   currencyList.append(MyMoneySecurity("JMD", i18n("Jamaican Dollar"),        "$"));
2797   currencyList.append(MyMoneySecurity("JPY", i18n("Japanese Yen"),           QChar(0x00A5), 1));
2798   currencyList.append(MyMoneySecurity("JOD", i18n("Jordanian Dinar"),        "JOD", 1000));
2799   currencyList.append(MyMoneySecurity("KZT", i18n("Kazakhstan Tenge")));
2800   currencyList.append(MyMoneySecurity("KES", i18n("Kenyan Shilling")));
2801   currencyList.append(MyMoneySecurity("KWD", i18n("Kuwaiti Dinar"),          "KWD", 1000));
2802   currencyList.append(MyMoneySecurity("KGS", i18n("Kyrgyzstan Som")));
2803   currencyList.append(MyMoneySecurity("LAK", i18n("Laos Kip"),               QChar(0x20AD)));
2804   currencyList.append(MyMoneySecurity("LVL", i18n("Latvian Lats")));
2805   currencyList.append(MyMoneySecurity("LBP", i18n("Lebanese Pound"),         QChar(0x00A3)));
2806   currencyList.append(MyMoneySecurity("LSL", i18n("Lesotho Loti")));
2807   currencyList.append(MyMoneySecurity("LRD", i18n("Liberian Dollar"),        "$"));
2808   currencyList.append(MyMoneySecurity("LYD", i18n("Libyan Dinar"),           "LYD", 1000));
2809   currencyList.append(MyMoneySecurity("MOP", i18n("Macau Pataca")));
2810   currencyList.append(MyMoneySecurity("MKD", i18n("Macedonian Denar")));
2811   currencyList.append(MyMoneySecurity("MGF", i18n("Malagasy Franc"),         "MGF", 500));
2812   currencyList.append(MyMoneySecurity("MWK", i18n("Malawi Kwacha")));
2813   currencyList.append(MyMoneySecurity("MYR", i18n("Malaysian Ringgit")));
2814   currencyList.append(MyMoneySecurity("MVR", i18n("Maldive Rufiyaa")));
2815   currencyList.append(MyMoneySecurity("MLF", i18n("Mali Republic Franc")));
2816   currencyList.append(MyMoneySecurity("MRO", i18n("Mauritanian Ouguiya"),    "MRO", 5));
2817   currencyList.append(MyMoneySecurity("MUR", i18n("Mauritius Rupee")));
2818   currencyList.append(MyMoneySecurity("MXN", i18n("Mexican Peso"),           "$"));
2819   currencyList.append(MyMoneySecurity("MDL", i18n("Moldavian Leu")));
2820   currencyList.append(MyMoneySecurity("MNT", i18n("Mongolian Tugrik"),       QChar(0x20AE)));
2821   currencyList.append(MyMoneySecurity("MAD", i18n("Moroccan Dirham")));
2822   currencyList.append(MyMoneySecurity("MZN", i18n("Mozambique Metical"),     "MT"));
2823   currencyList.append(MyMoneySecurity("MMK", i18n("Myanmar Kyat")));
2824   currencyList.append(MyMoneySecurity("NAD", i18n("Namibian Dollar"),        "$"));
2825   currencyList.append(MyMoneySecurity("NPR", i18n("Nepalese Rupee")));
2826   currencyList.append(MyMoneySecurity("NZD", i18n("New Zealand Dollar"),     "$"));
2827   currencyList.append(MyMoneySecurity("NIC", i18n("Nicaraguan Cordoba Oro")));
2828   currencyList.append(MyMoneySecurity("NGN", i18n("Nigerian Naira"),         QChar(0x20A6)));
2829   currencyList.append(MyMoneySecurity("KPW", i18n("North Korean Won"),       QChar(0x20A9)));
2830   currencyList.append(MyMoneySecurity("NOK", i18n("Norwegian Kroner"),       "kr"));
2831   currencyList.append(MyMoneySecurity("OMR", i18n("Omani Rial"),             "OMR", 1000));
2832   currencyList.append(MyMoneySecurity("PKR", i18n("Pakistan Rupee")));
2833   currencyList.append(MyMoneySecurity("PAB", i18n("Panamanian Balboa")));
2834   currencyList.append(MyMoneySecurity("PGK", i18n("Papua New Guinea Kina")));
2835   currencyList.append(MyMoneySecurity("PYG", i18n("Paraguay Guarani")));
2836   currencyList.append(MyMoneySecurity("PEN", i18n("Peruvian Nuevo Sol")));
2837   currencyList.append(MyMoneySecurity("PHP", i18n("Philippine Peso"),        QChar(0x20B1)));
2838   currencyList.append(MyMoneySecurity("PLN", i18n("Polish Zloty")));
2839   currencyList.append(MyMoneySecurity("QAR", i18n("Qatari Rial")));
2840   currencyList.append(MyMoneySecurity("RON", i18n("Romanian Leu (new)")));
2841   currencyList.append(MyMoneySecurity("RUB", i18n("Russian Ruble")));
2842   currencyList.append(MyMoneySecurity("RWF", i18n("Rwanda Franc")));
2843   currencyList.append(MyMoneySecurity("WST", i18n("Samoan Tala")));
2844   currencyList.append(MyMoneySecurity("STD", i18n("Sao Tome and Principe Dobra")));
2845   currencyList.append(MyMoneySecurity("SAR", i18n("Saudi Riyal")));
2846   currencyList.append(MyMoneySecurity("RSD", i18n("Serbian Dinar")));
2847   currencyList.append(MyMoneySecurity("SCR", i18n("Seychelles Rupee")));
2848   currencyList.append(MyMoneySecurity("SLL", i18n("Sierra Leone Leone")));
2849   currencyList.append(MyMoneySecurity("SGD", i18n("Singapore Dollar"),       "$"));
2850   currencyList.append(MyMoneySecurity("SBD", i18n("Solomon Islands Dollar"), "$"));
2851   currencyList.append(MyMoneySecurity("SOS", i18n("Somali Shilling")));
2852   currencyList.append(MyMoneySecurity("ZAR", i18n("South African Rand")));
2853   currencyList.append(MyMoneySecurity("KRW", i18n("South Korean Won"),       QChar(0x20A9), 1));
2854   currencyList.append(MyMoneySecurity("LKR", i18n("Sri Lanka Rupee")));
2855   currencyList.append(MyMoneySecurity("SHP", i18n("St. Helena Pound"),       QChar(0x00A3)));
2856   currencyList.append(MyMoneySecurity("SDD", i18n("Sudanese Dinar")));
2857   currencyList.append(MyMoneySecurity("SRG", i18n("Suriname Guilder")));
2858   currencyList.append(MyMoneySecurity("SZL", i18n("Swaziland Lilangeni")));
2859   currencyList.append(MyMoneySecurity("SEK", i18n("Swedish Krona")));
2860   currencyList.append(MyMoneySecurity("CHF", i18n("Swiss Franc"),            "SFr"));
2861   currencyList.append(MyMoneySecurity("SYP", i18n("Syrian Pound"),           QChar(0x00A3)));
2862   currencyList.append(MyMoneySecurity("TWD", i18n("Taiwan Dollar"),          "$"));
2863   currencyList.append(MyMoneySecurity("TJS", i18n("Tajikistan Somoni")));
2864   currencyList.append(MyMoneySecurity("TZS", i18n("Tanzanian Shilling")));
2865   currencyList.append(MyMoneySecurity("THB", i18n("Thai Baht"),              QChar(0x0E3F)));
2866   currencyList.append(MyMoneySecurity("TOP", i18n("Tongan Pa'anga")));
2867   currencyList.append(MyMoneySecurity("TTD", i18n("Trinidad and Tobago Dollar"), "$"));
2868   currencyList.append(MyMoneySecurity("TND", i18n("Tunisian Dinar"),         "TND", 1000));
2869   currencyList.append(MyMoneySecurity("TRY", i18n("Turkish Lira"), QChar(0x20BA)));
2870   currencyList.append(MyMoneySecurity("TMM", i18n("Turkmenistan Manat")));
2871   currencyList.append(MyMoneySecurity("USD", i18n("US Dollar"),              "$"));
2872   currencyList.append(MyMoneySecurity("UGX", i18n("Uganda Shilling")));
2873   currencyList.append(MyMoneySecurity("UAH", i18n("Ukraine Hryvnia")));
2874   currencyList.append(MyMoneySecurity("CLF", i18n("Unidad de Fometo")));
2875   currencyList.append(MyMoneySecurity("AED", i18n("United Arab Emirates Dirham")));
2876   currencyList.append(MyMoneySecurity("UYU", i18n("Uruguayan Peso")));
2877   currencyList.append(MyMoneySecurity("UZS", i18n("Uzbekistani Sum")));
2878   currencyList.append(MyMoneySecurity("VUV", i18n("Vanuatu Vatu")));
2879   currencyList.append(MyMoneySecurity("VEB", i18n("Venezuelan Bolivar")));
2880   currencyList.append(MyMoneySecurity("VND", i18n("Vietnamese Dong"),        QChar(0x20AB)));
2881   currencyList.append(MyMoneySecurity("ZMW", i18n("Zambian Kwacha"),         "K"));
2882   currencyList.append(MyMoneySecurity("ZWD", i18n("Zimbabwe Dollar"),        "$"));
2883 
2884   currencyList.append(ancientCurrencies().keys());
2885 
2886   // sort the currencies ...
2887   qSort(currencyList.begin(), currencyList.end(),
2888         [] (const MyMoneySecurity& c1, const MyMoneySecurity& c2)
2889         {
2890           return c1.name().compare(c2.name()) < 0;
2891         });
2892 
2893   // ... and add a few precious metals at the ned
2894   currencyList.append(MyMoneySecurity("XAU", i18n("Gold"),       "XAU", 1000000));
2895   currencyList.append(MyMoneySecurity("XPD", i18n("Palladium"),  "XPD", 1000000));
2896   currencyList.append(MyMoneySecurity("XPT", i18n("Platinum"),   "XPT", 1000000));
2897   currencyList.append(MyMoneySecurity("XAG", i18n("Silver"),     "XAG", 1000000));
2898 
2899   return currencyList;
2900 }
2901 
currencyList() const2902 QList<MyMoneySecurity> MyMoneyFile::currencyList() const
2903 {
2904   d->checkStorage();
2905 
2906   return d->m_storage->currencyList();
2907 }
2908 
foreignCurrency(const QString & first,const QString & second) const2909 QString MyMoneyFile::foreignCurrency(const QString& first, const QString& second) const
2910 {
2911   if (baseCurrency().id() == second)
2912     return first;
2913   return second;
2914 }
2915 
baseCurrency() const2916 MyMoneySecurity MyMoneyFile::baseCurrency() const
2917 {
2918   if (d->m_baseCurrency.id().isEmpty()) {
2919     QString id = QString(value("kmm-baseCurrency"));
2920     if (!id.isEmpty())
2921       d->m_baseCurrency = currency(id);
2922   }
2923 
2924   return d->m_baseCurrency;
2925 }
2926 
setBaseCurrency(const MyMoneySecurity & curr)2927 void MyMoneyFile::setBaseCurrency(const MyMoneySecurity& curr)
2928 {
2929   // make sure the currency exists
2930   MyMoneySecurity c = currency(curr.id());
2931 
2932   if (c.id() != d->m_baseCurrency.id()) {
2933     setValue("kmm-baseCurrency", curr.id());
2934     // force reload of base currency cache
2935     d->m_baseCurrency = MyMoneySecurity();
2936   }
2937 }
2938 
addPrice(const MyMoneyPrice & price)2939 void MyMoneyFile::addPrice(const MyMoneyPrice& price)
2940 {
2941   if (price.rate(QString()).isZero())
2942     return;
2943 
2944   d->checkTransaction(Q_FUNC_INFO);
2945 
2946   // store the account's which are affected by this price regarding their value
2947   d->priceChanged(*this, price);
2948   d->m_storage->addPrice(price);
2949 }
2950 
removePrice(const MyMoneyPrice & price)2951 void MyMoneyFile::removePrice(const MyMoneyPrice& price)
2952 {
2953   d->checkTransaction(Q_FUNC_INFO);
2954 
2955   // store the account's which are affected by this price regarding their value
2956   d->priceChanged(*this, price);
2957   d->m_storage->removePrice(price);
2958 }
2959 
price(const QString & fromId,const QString & toId,const QDate & date,const bool exactDate) const2960 MyMoneyPrice MyMoneyFile::price(const QString& fromId, const QString& toId, const QDate& date, const bool exactDate) const
2961 {
2962   d->checkStorage();
2963 
2964   QString to(toId);
2965   if (to.isEmpty())
2966     to = value("kmm-baseCurrency");
2967   // if some id is missing, we can return an empty price object
2968   if (fromId.isEmpty() || to.isEmpty())
2969     return MyMoneyPrice();
2970 
2971   // we don't search our tables if someone asks stupid stuff
2972   if (fromId == toId) {
2973     return MyMoneyPrice(fromId, toId, date, MyMoneyMoney::ONE, "KMyMoney");
2974   }
2975 
2976   // if not asking for exact date, try to find the exact date match first,
2977   // either the requested price or its reciprocal value. If unsuccessful, it will move
2978   // on and look for prices of previous dates
2979   MyMoneyPrice rc = d->m_storage->price(fromId, to, date, true);
2980   if (!rc.isValid()) {
2981     // not found, search 'to-from' rate and use reciprocal value
2982     rc = d->m_storage->price(to, fromId, date, true);
2983 
2984     // not found, search previous dates, if exact date is not needed
2985     if (!exactDate && !rc.isValid()) {
2986       // search 'from-to' and 'to-from', select the most recent one
2987       MyMoneyPrice fromPrice = d->m_storage->price(fromId, to, date, exactDate);
2988       MyMoneyPrice toPrice = d->m_storage->price(to, fromId, date, exactDate);
2989 
2990       // check first whether both prices are valid
2991       if (fromPrice.isValid() && toPrice.isValid()) {
2992         if (fromPrice.date() >= toPrice.date()) {
2993           // if 'from-to' is newer or the same date, prefer that one
2994           rc = fromPrice;
2995         } else {
2996           // otherwise, use the reciprocal price
2997           rc = toPrice;
2998         }
2999       } else if (fromPrice.isValid()) { // check if any of the prices is valid, return that one
3000         rc = fromPrice;
3001       } else if (toPrice.isValid()) {
3002         rc = toPrice;
3003       }
3004     }
3005   }
3006   return rc;
3007 }
3008 
price(const QString & fromId,const QString & toId) const3009 MyMoneyPrice MyMoneyFile::price(const QString& fromId, const QString& toId) const
3010 {
3011   return price(fromId, toId, QDate::currentDate(), false);
3012 }
3013 
price(const QString & fromId) const3014 MyMoneyPrice MyMoneyFile::price(const QString& fromId) const
3015 {
3016   return price(fromId, QString(), QDate::currentDate(), false);
3017 }
3018 
priceList() const3019 MyMoneyPriceList MyMoneyFile::priceList() const
3020 {
3021   d->checkStorage();
3022 
3023   return d->m_storage->priceList();
3024 }
3025 
hasAccount(const QString & id,const QString & name) const3026 bool MyMoneyFile::hasAccount(const QString& id, const QString& name) const
3027 {
3028   const auto accounts = account(id).accountList();
3029   for (const auto& acc : accounts) {
3030     if (account(acc).name().compare(name) == 0)
3031       return true;
3032   }
3033   return false;
3034 }
3035 
reportList() const3036 QList<MyMoneyReport> MyMoneyFile::reportList() const
3037 {
3038   d->checkStorage();
3039 
3040   return d->m_storage->reportList();
3041 }
3042 
addReport(MyMoneyReport & report)3043 void MyMoneyFile::addReport(MyMoneyReport& report)
3044 {
3045   d->checkTransaction(Q_FUNC_INFO);
3046 
3047   d->m_storage->addReport(report);
3048 }
3049 
modifyReport(const MyMoneyReport & report)3050 void MyMoneyFile::modifyReport(const MyMoneyReport& report)
3051 {
3052   d->checkTransaction(Q_FUNC_INFO);
3053 
3054   d->m_storage->modifyReport(report);
3055 }
3056 
countReports() const3057 unsigned MyMoneyFile::countReports() const
3058 {
3059   d->checkStorage();
3060 
3061   return d->m_storage->countReports();
3062 }
3063 
report(const QString & id) const3064 MyMoneyReport MyMoneyFile::report(const QString& id) const
3065 {
3066   d->checkStorage();
3067 
3068   return d->m_storage->report(id);
3069 }
3070 
removeReport(const MyMoneyReport & report)3071 void MyMoneyFile::removeReport(const MyMoneyReport& report)
3072 {
3073   d->checkTransaction(Q_FUNC_INFO);
3074 
3075   d->m_storage->removeReport(report);
3076 }
3077 
3078 
budgetList() const3079 QList<MyMoneyBudget> MyMoneyFile::budgetList() const
3080 {
3081   d->checkStorage();
3082 
3083   return d->m_storage->budgetList();
3084 }
3085 
addBudget(MyMoneyBudget & budget)3086 void MyMoneyFile::addBudget(MyMoneyBudget &budget)
3087 {
3088   d->checkTransaction(Q_FUNC_INFO);
3089 
3090   d->m_storage->addBudget(budget);
3091 }
3092 
budgetByName(const QString & name) const3093 MyMoneyBudget MyMoneyFile::budgetByName(const QString& name) const
3094 {
3095   d->checkStorage();
3096 
3097   return d->m_storage->budgetByName(name);
3098 }
3099 
modifyBudget(const MyMoneyBudget & budget)3100 void MyMoneyFile::modifyBudget(const MyMoneyBudget& budget)
3101 {
3102   d->checkTransaction(Q_FUNC_INFO);
3103 
3104   d->m_storage->modifyBudget(budget);
3105 }
3106 
countBudgets() const3107 unsigned MyMoneyFile::countBudgets() const
3108 {
3109   d->checkStorage();
3110 
3111   return d->m_storage->countBudgets();
3112 }
3113 
budget(const QString & id) const3114 MyMoneyBudget MyMoneyFile::budget(const QString& id) const
3115 {
3116   d->checkStorage();
3117 
3118   return d->m_storage->budget(id);
3119 }
3120 
removeBudget(const MyMoneyBudget & budget)3121 void MyMoneyFile::removeBudget(const MyMoneyBudget& budget)
3122 {
3123   d->checkTransaction(Q_FUNC_INFO);
3124 
3125   d->m_storage->removeBudget(budget);
3126 }
3127 
addOnlineJob(onlineJob & job)3128 void MyMoneyFile::addOnlineJob(onlineJob& job)
3129 {
3130   d->checkTransaction(Q_FUNC_INFO);
3131 
3132   d->m_storage->addOnlineJob(job);
3133   d->m_changeSet += MyMoneyNotification(File::Mode::Add, job);
3134 }
3135 
modifyOnlineJob(const onlineJob job)3136 void MyMoneyFile::modifyOnlineJob(const onlineJob job)
3137 {
3138   d->checkTransaction(Q_FUNC_INFO);
3139   d->m_storage->modifyOnlineJob(job);
3140   d->m_changeSet += MyMoneyNotification(File::Mode::Modify, job);
3141 }
3142 
getOnlineJob(const QString & jobId) const3143 onlineJob MyMoneyFile::getOnlineJob(const QString &jobId) const
3144 {
3145   d->checkStorage();
3146   return d->m_storage->getOnlineJob(jobId);
3147 }
3148 
onlineJobList() const3149 QList<onlineJob> MyMoneyFile::onlineJobList() const
3150 {
3151   d->checkStorage();
3152   return d->m_storage->onlineJobList();
3153 }
3154 
3155 /** @todo improve speed by passing count job to m_storage */
countOnlineJobs() const3156 int MyMoneyFile::countOnlineJobs() const
3157 {
3158   return onlineJobList().count();
3159 }
3160 
3161 /**
3162  * @brief Remove onlineJob
3163  * @param job onlineJob to remove
3164  */
removeOnlineJob(const onlineJob & job)3165 void MyMoneyFile::removeOnlineJob(const onlineJob& job)
3166 {
3167   d->checkTransaction(Q_FUNC_INFO);
3168 
3169   // clear all changed objects from cache
3170   if (job.isLocked()) {
3171     return;
3172   }
3173   d->m_changeSet += MyMoneyNotification(File::Mode::Remove, job);
3174   d->m_storage->removeOnlineJob(job);
3175 }
3176 
removeOnlineJob(const QStringList onlineJobIds)3177 void MyMoneyFile::removeOnlineJob(const QStringList onlineJobIds)
3178 {
3179   foreach (QString jobId, onlineJobIds) {
3180     removeOnlineJob(getOnlineJob(jobId));
3181   }
3182 }
3183 
costCenterList(QList<MyMoneyCostCenter> & list) const3184 void MyMoneyFile::costCenterList(QList< MyMoneyCostCenter >& list) const
3185 {
3186   d->checkStorage();
3187   list = d->m_storage->costCenterList();
3188 }
3189 
updateVAT(MyMoneyTransaction & transaction) const3190 void MyMoneyFile::updateVAT(MyMoneyTransaction& transaction) const
3191 {
3192   // check if transaction qualifies
3193   const auto splitCount = transaction.splits().count();
3194   if (splitCount > 1 && splitCount <= 3) {
3195     MyMoneyMoney amount;
3196     MyMoneyAccount assetLiability;
3197     MyMoneyAccount category;
3198     MyMoneySplit taxSplit;
3199     const QString currencyId = transaction.commodity();
3200     foreach (const auto& split, transaction.splits()) {
3201       const auto acc = account(split.accountId());
3202       // all splits must reference accounts denoted in the same currency
3203       if (acc.currencyId() != currencyId) {
3204         return;
3205       }
3206       if (acc.isAssetLiability() && assetLiability.id().isEmpty()) {
3207         amount = split.shares();
3208         assetLiability = acc;
3209         continue;
3210       }
3211       if (acc.isAssetLiability()) {
3212         return;
3213       }
3214       if (category.id().isEmpty() && !acc.value("VatAccount").isEmpty()) {
3215         category = acc;
3216         continue;
3217       } else if(taxSplit.id().isEmpty() && !acc.value("Tax").toLower().compare(QLatin1String("yes"))) {
3218         taxSplit = split;
3219         continue;
3220       }
3221       return;
3222     }
3223     if (!category.id().isEmpty()) {
3224       // remove a possibly found tax split - we create a new one
3225       // but only if it is the same tax category
3226       if (!taxSplit.id().isEmpty()) {
3227         if (category.value("VatAccount").compare(taxSplit.accountId()))
3228           return;
3229         transaction.removeSplit(taxSplit);
3230       }
3231       addVATSplit(transaction, assetLiability, category, amount);
3232     }
3233   }
3234 }
3235 
addVATSplit(MyMoneyTransaction & transaction,const MyMoneyAccount & acc,const MyMoneyAccount & category,const MyMoneyMoney & amount) const3236 bool MyMoneyFile::addVATSplit(MyMoneyTransaction& transaction, const MyMoneyAccount& acc, const MyMoneyAccount& category, const MyMoneyMoney& amount) const
3237 {
3238   bool rc = false;
3239 
3240   try {
3241     MyMoneySplit cat;  // category
3242     MyMoneySplit tax;  // tax
3243 
3244     if (category.value("VatAccount").isEmpty())
3245       return false;
3246     MyMoneyAccount vatAcc = account(category.value("VatAccount"));
3247     const MyMoneySecurity& asec = security(acc.currencyId());
3248     const MyMoneySecurity& csec = security(category.currencyId());
3249     const MyMoneySecurity& vsec = security(vatAcc.currencyId());
3250     if (asec.id() != csec.id() || asec.id() != vsec.id()) {
3251       qDebug("Auto VAT assignment only works if all three accounts use the same currency.");
3252       return false;
3253     }
3254 
3255     MyMoneyMoney vatRate(vatAcc.value("VatRate"));
3256     MyMoneyMoney gv, nv;    // gross value, net value
3257     int fract = acc.fraction();
3258 
3259     if (!vatRate.isZero()) {
3260 
3261       tax.setAccountId(vatAcc.id());
3262 
3263       // qDebug("vat amount is '%s'", category.value("VatAmount").toLatin1());
3264       if (category.value("VatAmount").toLower() != QString("net")) {
3265         // split value is the gross value
3266         gv = amount;
3267         nv = (gv / (MyMoneyMoney::ONE + vatRate)).convert(fract);
3268         MyMoneySplit catSplit = transaction.splitByAccount(acc.id(), false);
3269         catSplit.setShares(-nv);
3270         catSplit.setValue(catSplit.shares());
3271         transaction.modifySplit(catSplit);
3272 
3273       } else {
3274         // split value is the net value
3275         nv = amount;
3276         gv = (nv * (MyMoneyMoney::ONE + vatRate)).convert(fract);
3277         MyMoneySplit accSplit = transaction.splitByAccount(acc.id());
3278         accSplit.setValue(gv.convert(fract));
3279         accSplit.setShares(accSplit.value());
3280         transaction.modifySplit(accSplit);
3281       }
3282 
3283       tax.setValue(-(gv - nv).convert(fract));
3284       tax.setShares(tax.value());
3285       transaction.addSplit(tax);
3286       rc = true;
3287     }
3288   } catch (const MyMoneyException &) {
3289   }
3290   return rc;
3291 }
3292 
isReferenced(const MyMoneyObject & obj,const QBitArray & skipChecks) const3293 bool MyMoneyFile::isReferenced(const MyMoneyObject& obj, const QBitArray& skipChecks) const
3294 {
3295   d->checkStorage();
3296   return d->m_storage->isReferenced(obj, skipChecks);
3297 }
3298 
isReferenced(const MyMoneyObject & obj) const3299 bool MyMoneyFile::isReferenced(const MyMoneyObject& obj) const
3300 {
3301   return isReferenced(obj, QBitArray((int)eStorage::Reference::Count));
3302 }
3303 
checkNoUsed(const QString & accId,const QString & no) const3304 bool MyMoneyFile::checkNoUsed(const QString& accId, const QString& no) const
3305 {
3306   // by definition, an empty string or a non-numeric string is not used
3307   QRegExp exp(QString("(.*\\D)?(\\d+)(\\D.*)?"));
3308   if (no.isEmpty() || exp.indexIn(no) == -1)
3309     return false;
3310 
3311   MyMoneyTransactionFilter filter;
3312   filter.addAccount(accId);
3313   QList<MyMoneyTransaction> transactions = transactionList(filter);
3314   QList<MyMoneyTransaction>::ConstIterator it_t = transactions.constBegin();
3315   while (it_t != transactions.constEnd()) {
3316     try {
3317       MyMoneySplit split;
3318       // Test whether the transaction also includes a split into
3319       // this account
3320       split = (*it_t).splitByAccount(accId, true /*match*/);
3321       if (!split.number().isEmpty() && split.number() == no)
3322         return true;
3323     } catch (const MyMoneyException &) {
3324     }
3325     ++it_t;
3326   }
3327   return false;
3328 }
3329 
highestCheckNo(const QString & accId) const3330 QString MyMoneyFile::highestCheckNo(const QString& accId) const
3331 {
3332   unsigned64 lno = 0;
3333   unsigned64 cno;
3334   QString no;
3335   MyMoneyTransactionFilter filter;
3336   filter.addAccount(accId);
3337   QList<MyMoneyTransaction> transactions = transactionList(filter);
3338   QList<MyMoneyTransaction>::ConstIterator it_t = transactions.constBegin();
3339   while (it_t != transactions.constEnd()) {
3340     try {
3341       // Test whether the transaction also includes a split into
3342       // this account
3343       MyMoneySplit split = (*it_t).splitByAccount(accId, true /*match*/);
3344       if (!split.number().isEmpty()) {
3345         // non-numerical values stored in number will return 0 in the next line
3346         cno = split.number().toULongLong();
3347         if (cno > lno) {
3348           lno = cno;
3349           no = split.number();
3350         }
3351       }
3352     } catch (const MyMoneyException &) {
3353     }
3354     ++it_t;
3355   }
3356   return no;
3357 }
3358 
hasNewerTransaction(const QString & accId,const QDate & date) const3359 bool MyMoneyFile::hasNewerTransaction(const QString& accId, const QDate& date) const
3360 {
3361   MyMoneyTransactionFilter filter;
3362   filter.addAccount(accId);
3363   filter.setDateFilter(date.addDays(+1), QDate());
3364   return !transactionList(filter).isEmpty();
3365 }
3366 
clearCache()3367 void MyMoneyFile::clearCache()
3368 {
3369   d->checkStorage();
3370   d->m_balanceCache.clear();
3371 }
3372 
forceDataChanged()3373 void MyMoneyFile::forceDataChanged()
3374 {
3375   emit dataChanged();
3376 }
3377 
isTransfer(const MyMoneyTransaction & t) const3378 bool MyMoneyFile::isTransfer(const MyMoneyTransaction& t) const
3379 {
3380   auto rc = true;
3381   if (t.splitCount() == 2) {
3382     foreach (const auto split, t.splits()) {
3383       auto acc = account(split.accountId());
3384       if (acc.isIncomeExpense()) {
3385         rc = false;
3386         break;
3387       }
3388     }
3389   }
3390   return rc;
3391 }
3392 
referencesClosedAccount(const MyMoneyTransaction & t) const3393 bool MyMoneyFile::referencesClosedAccount(const MyMoneyTransaction& t) const
3394 {
3395   auto ret = false;
3396   foreach (const auto split, t.splits()) {
3397     if (referencesClosedAccount(split)) {
3398       ret = true;
3399       break;
3400     }
3401   }
3402   return ret;
3403 }
3404 
referencesClosedAccount(const MyMoneySplit & s) const3405 bool MyMoneyFile::referencesClosedAccount(const MyMoneySplit& s) const
3406 {
3407   if (s.accountId().isEmpty())
3408     return false;
3409 
3410   try {
3411     return account(s.accountId()).isClosed();
3412   } catch (const MyMoneyException &) {
3413   }
3414   return false;
3415 }
3416 
storageId()3417 QString MyMoneyFile::storageId()
3418 {
3419   QString id = value("kmm-id");
3420   if (id.isEmpty()) {
3421     MyMoneyFileTransaction ft;
3422     try {
3423       QUuid uid = QUuid::createUuid();
3424       setValue("kmm-id", uid.toString());
3425       ft.commit();
3426       id = uid.toString();
3427     } catch (const MyMoneyException &) {
3428       qDebug("Unable to setup UID for new storage object");
3429     }
3430   }
3431   return id;
3432 }
3433 
openingBalancesPrefix()3434 QString MyMoneyFile::openingBalancesPrefix()
3435 {
3436     return i18n("Opening Balances");
3437 }
3438 
hasMatchingOnlineBalance(const MyMoneyAccount & _acc) const3439 bool MyMoneyFile::hasMatchingOnlineBalance(const MyMoneyAccount& _acc) const
3440 {
3441   // get current values
3442   auto acc = account(_acc.id());
3443 
3444   // if there's no last transaction import data we are done
3445   if (acc.value("lastImportedTransactionDate").isEmpty()
3446       || acc.value("lastStatementBalance").isEmpty())
3447     return false;
3448 
3449   // otherwise, we compare the balances
3450   MyMoneyMoney balance(acc.value("lastStatementBalance"));
3451   MyMoneyMoney accBalance = this->balance(acc.id(), QDate::fromString(acc.value("lastImportedTransactionDate"), Qt::ISODate));
3452 
3453   return balance == accBalance;
3454 }
3455 
countTransactionsWithSpecificReconciliationState(const QString & accId,TransactionFilter::State state) const3456 int MyMoneyFile::countTransactionsWithSpecificReconciliationState(const QString& accId, TransactionFilter::State state) const
3457 {
3458   MyMoneyTransactionFilter filter;
3459   filter.addAccount(accId);
3460   filter.addState((int)state);
3461   return transactionList(filter).count();
3462 }
3463 
countTransactionsWithSpecificReconciliationState() const3464 QMap<QString, QVector<int> > MyMoneyFile::countTransactionsWithSpecificReconciliationState() const
3465 {
3466   QMap<QString, QVector<int> > result;
3467   MyMoneyTransactionFilter filter;
3468   filter.setReportAllSplits(false);
3469 
3470   d->checkStorage();
3471 
3472   QList<MyMoneyAccount> list;
3473   accountList(list);
3474   for (const auto& account : list) {
3475     result[account.id()] = QVector<int>((int)eMyMoney::Split::State::MaxReconcileState, 0);
3476   }
3477 
3478   const auto transactions = d->m_storage->transactionList(filter);
3479   for (const auto& transaction : transactions) {
3480     const auto& splits = transaction.splits();
3481     for (const auto& split : splits) {
3482       if (!result.contains(split.accountId())) {
3483         result[split.accountId()] = QVector<int>((int)eMyMoney::Split::State::MaxReconcileState, 0);
3484       }
3485       const auto flag = split.reconcileFlag();
3486       switch(flag) {
3487         case eMyMoney::Split::State::NotReconciled:
3488         case eMyMoney::Split::State::Cleared:
3489         case eMyMoney::Split::State::Reconciled:
3490         case eMyMoney::Split::State::Frozen:
3491           result[split.accountId()][(int)flag]++;
3492           break;
3493         default:
3494           break;
3495       }
3496 
3497     }
3498   }
3499   return result;
3500 }
3501 
3502   /**
3503    * Make sure that the splits value has the precision of the corresponding account
3504    */
fixSplitPrecision(MyMoneyTransaction & t) const3505 void MyMoneyFile::fixSplitPrecision(MyMoneyTransaction& t) const
3506 {
3507   auto transactionSecurity = security(t.commodity());
3508   auto transactionFraction = transactionSecurity.smallestAccountFraction();
3509 
3510   for (auto& split : t.splits()) {
3511     auto acc = account(split.accountId());
3512     auto fraction = acc.fraction();
3513     if(fraction == -1) {
3514       auto sec = security(acc.currencyId());
3515       fraction = acc.fraction(sec);
3516     }
3517     // Don't do any rounding on a split factor
3518     if (split.action() != MyMoneySplit::actionName(eMyMoney::Split::Action::SplitShares)) {
3519       split.setShares(static_cast<const MyMoneyMoney>(split.shares().convertDenominator(fraction).canonicalize()));
3520       split.setValue(static_cast<const MyMoneyMoney>(split.value().convertDenominator(transactionFraction).canonicalize()));
3521     }
3522   }
3523 }
3524 
3525 class MyMoneyFileTransactionPrivate
3526 {
3527   Q_DISABLE_COPY(MyMoneyFileTransactionPrivate)
3528 
3529 public:
MyMoneyFileTransactionPrivate()3530   MyMoneyFileTransactionPrivate() :
3531     m_isNested(MyMoneyFile::instance()->hasTransaction()),
3532     m_needRollback(!m_isNested)
3533   {
3534   }
3535 
3536 public:
3537   bool m_isNested;
3538   bool m_needRollback;
3539 
3540 };
3541 
3542 
MyMoneyFileTransaction()3543 MyMoneyFileTransaction::MyMoneyFileTransaction() :
3544   d_ptr(new MyMoneyFileTransactionPrivate)
3545 {
3546   Q_D(MyMoneyFileTransaction);
3547   if (!d->m_isNested)
3548     MyMoneyFile::instance()->startTransaction();
3549 }
3550 
~MyMoneyFileTransaction()3551 MyMoneyFileTransaction::~MyMoneyFileTransaction()
3552 {
3553   try {
3554     rollback();
3555   } catch (const MyMoneyException &e) {
3556     qDebug() << e.what();
3557   }
3558   Q_D(MyMoneyFileTransaction);
3559   delete d;
3560 }
3561 
restart()3562 void MyMoneyFileTransaction::restart()
3563 {
3564   rollback();
3565 
3566   Q_D(MyMoneyFileTransaction);
3567   d->m_needRollback = !d->m_isNested;
3568   if (!d->m_isNested)
3569     MyMoneyFile::instance()->startTransaction();
3570 }
3571 
commit()3572 void MyMoneyFileTransaction::commit()
3573 {
3574   Q_D(MyMoneyFileTransaction);
3575   if (!d->m_isNested)
3576     MyMoneyFile::instance()->commitTransaction();
3577   d->m_needRollback = false;
3578 }
3579 
rollback()3580 void MyMoneyFileTransaction::rollback()
3581 {
3582   Q_D(MyMoneyFileTransaction);
3583   if (d->m_needRollback)
3584     MyMoneyFile::instance()->rollbackTransaction();
3585   d->m_needRollback = false;
3586 }
3587