File indexing completed on 2024-05-12 16:42:36

0001 /*
0002     SPDX-FileCopyrightText: 2000-2003 Michael Edwardes <mte@users.sourceforge.net>
0003     SPDX-FileCopyrightText: 2001-2002 Felix Rodriguez <frodriguez@users.sourceforge.net>
0004     SPDX-FileCopyrightText: 2002-2004 Kevin Tambascio <ktambascio@users.sourceforge.net>
0005     SPDX-FileCopyrightText: 2004-2005 Ace Jones <acejones@users.sourceforge.net>
0006     SPDX-FileCopyrightText: 2006-2019 Thomas Baumgart <tbaumgart@kde.org>
0007     SPDX-FileCopyrightText: 2006 Darren Gould <darren_gould@gmx.de>
0008     SPDX-FileCopyrightText: 2017-2018 Łukasz Wojniłowicz <lukasz.wojnilowicz@gmail.com>
0009     SPDX-License-Identifier: GPL-2.0-or-later
0010 */
0011 
0012 #include "mymoneyfile.h"
0013 
0014 #include <utility>
0015 
0016 // ----------------------------------------------------------------------------
0017 // QT Includes
0018 
0019 #include <QString>
0020 #include <QList>
0021 #include <QUuid>
0022 #include <QLocale>
0023 #include <QBitArray>
0024 #include <QDebug>
0025 
0026 // ----------------------------------------------------------------------------
0027 // KDE Includes
0028 
0029 #include <KLocalizedString>
0030 
0031 // ----------------------------------------------------------------------------
0032 // Project Includes
0033 
0034 #include "mymoneystoragemgr.h"
0035 #include "mymoneyinstitution.h"
0036 #include "mymoneyaccount.h"
0037 #include "mymoneyaccountloan.h"
0038 #include "mymoneysecurity.h"
0039 #include "mymoneyreport.h"
0040 #include "mymoneybalancecache.h"
0041 #include "mymoneybudget.h"
0042 #include "mymoneyprice.h"
0043 #include "mymoneypayee.h"
0044 #include "mymoneytag.h"
0045 #include "mymoneyschedule.h"
0046 #include "mymoneysplit.h"
0047 #include "mymoneytransaction.h"
0048 #include "mymoneycostcenter.h"
0049 #include "mymoneyexception.h"
0050 #include "onlinejob.h"
0051 #include "storageenums.h"
0052 #include "mymoneyenums.h"
0053 
0054 // include the following line to get a 'cout' for debug purposes
0055 // #include <iostream>
0056 
0057 using namespace eMyMoney;
0058 
0059 const QString MyMoneyFile::AccountSeparator = QChar(':');
0060 
0061 MyMoneyFile MyMoneyFile::file;
0062 
0063 typedef QList<std::pair<QString, QDate> > BalanceNotifyList;
0064 typedef QMap<QString, bool> CacheNotifyList;
0065 
0066 /// @todo make this template based
0067 class MyMoneyNotification
0068 {
0069 public:
0070     MyMoneyNotification(File::Mode mode, const MyMoneyTransaction& t) :
0071         m_objType(File::Object::Transaction),
0072         m_notificationMode(mode),
0073         m_id(t.id()) {
0074     }
0075 
0076     MyMoneyNotification(File::Mode mode, const MyMoneyAccount& acc) :
0077         m_objType(File::Object::Account),
0078         m_notificationMode(mode),
0079         m_id(acc.id()) {
0080     }
0081 
0082     MyMoneyNotification(File::Mode mode, const MyMoneyInstitution& institution) :
0083         m_objType(File::Object::Institution),
0084         m_notificationMode(mode),
0085         m_id(institution.id()) {
0086     }
0087 
0088     MyMoneyNotification(File::Mode mode, const MyMoneyPayee& payee) :
0089         m_objType(File::Object::Payee),
0090         m_notificationMode(mode),
0091         m_id(payee.id()) {
0092     }
0093 
0094     MyMoneyNotification(File::Mode mode, const MyMoneyTag& tag) :
0095         m_objType(File::Object::Tag),
0096         m_notificationMode(mode),
0097         m_id(tag.id()) {
0098     }
0099 
0100     MyMoneyNotification(File::Mode mode, const MyMoneySchedule& schedule) :
0101         m_objType(File::Object::Schedule),
0102         m_notificationMode(mode),
0103         m_id(schedule.id()) {
0104     }
0105 
0106     MyMoneyNotification(File::Mode mode, const MyMoneySecurity& security) :
0107         m_objType(File::Object::Security),
0108         m_notificationMode(mode),
0109         m_id(security.id()) {
0110     }
0111 
0112     MyMoneyNotification(File::Mode mode, const onlineJob& job) :
0113         m_objType(File::Object::OnlineJob),
0114         m_notificationMode(mode),
0115         m_id(job.id()) {
0116     }
0117 
0118     File::Object objectType() const {
0119         return m_objType;
0120     }
0121     File::Mode notificationMode() const {
0122         return m_notificationMode;
0123     }
0124     const QString& id() const {
0125         return m_id;
0126     }
0127 
0128 protected:
0129     MyMoneyNotification(File::Object obj,
0130                         File::Mode mode,
0131                         const QString& id) :
0132         m_objType(obj),
0133         m_notificationMode(mode),
0134         m_id(id) {}
0135 
0136 private:
0137     File::Object   m_objType;
0138     File::Mode     m_notificationMode;
0139     QString        m_id;
0140 };
0141 
0142 
0143 
0144 
0145 
0146 class MyMoneyFile::Private
0147 {
0148 public:
0149     Private() :
0150         m_storage(0),
0151         m_inTransaction(false) {}
0152 
0153     ~Private() {
0154         delete m_storage;
0155     }
0156     /**
0157       * This method is used to add an id to the list of objects
0158       * to be removed from the cache. If id is empty, then nothing is added to the list.
0159       *
0160       * @param id id of object to be notified
0161       * @param reload reload the object (@c true) or not (@c false). The default is @c true
0162       * @see attach, detach
0163       */
0164     void addCacheNotification(const QString& id, const QDate& date) {
0165         if (!id.isEmpty())
0166             m_balanceNotifyList.append(std::make_pair(id, date));
0167     }
0168 
0169     /**
0170       * This method is used to clear the notification list
0171       */
0172     void clearCacheNotification() {
0173         // reset list to be empty
0174         m_balanceNotifyList.clear();
0175     }
0176 
0177     /**
0178       * This method is used to clear all
0179       * objects mentioned in m_notificationList from the cache.
0180       */
0181     void notify() {
0182         foreach (const BalanceNotifyList::value_type & i, m_balanceNotifyList) {
0183             m_balanceChangedSet += i.first;
0184             if (i.second.isValid()) {
0185                 m_balanceCache.clear(i.first, i.second);
0186             } else {
0187                 m_balanceCache.clear(i.first);
0188             }
0189         }
0190 
0191         clearCacheNotification();
0192     }
0193 
0194     /**
0195       * This method checks if a storage object is attached and
0196       * throws and exception if not.
0197       */
0198     inline void checkStorage() const {
0199         if (m_storage == 0)
0200             throw MYMONEYEXCEPTION_CSTRING("No storage object attached to MyMoneyFile");
0201     }
0202 
0203     /**
0204       * This method checks that a transaction has been started with
0205       * startTransaction() and throws an exception otherwise. Calls
0206       * checkStorage() to make sure a storage object is present and attached.
0207       */
0208     void checkTransaction(const char* txt) const {
0209         checkStorage();
0210         if (!m_inTransaction)
0211             throw MYMONEYEXCEPTION(QString::fromLatin1("No transaction started for %1").arg(QString::fromLatin1(txt)));
0212     }
0213 
0214     void priceChanged(const MyMoneyFile& file, const MyMoneyPrice price) {
0215         // get all affected accounts and add them to the m_valueChangedSet
0216         QList<MyMoneyAccount> accList;
0217         file.accountList(accList);
0218         QList<MyMoneyAccount>::const_iterator account_it;
0219         for (account_it = accList.constBegin(); account_it != accList.constEnd(); ++account_it) {
0220             QString currencyId = account_it->currencyId();
0221             if (currencyId != file.baseCurrency().id() && (currencyId == price.from() || currencyId == price.to())) {
0222                 // this account is not in the base currency and the price affects it's value
0223                 m_valueChangedSet.insert(account_it->id());
0224             }
0225         }
0226     }
0227 
0228     /**
0229       * This member points to the storage strategy
0230       */
0231     MyMoneyStorageMgr *m_storage;
0232 
0233 
0234     bool                   m_inTransaction;
0235     MyMoneySecurity        m_baseCurrency;
0236 
0237     /**
0238      * @brief Cache for MyMoneyObjects
0239      *
0240      * It is also used to emit the objectAdded() and objectModified() signals.
0241      * => If one of these signals is used, you must use this cache.
0242      */
0243     MyMoneyPriceList       m_priceCache;
0244     MyMoneyBalanceCache    m_balanceCache;
0245 
0246     /**
0247       * This member keeps a list of account ids to notify
0248       * after a single operation is completed. The balance cache
0249       * is cleared for that account and all dates on or after
0250       * the one supplied. If the date is invalid, the entire
0251       * balance cache is cleared for that account.
0252       */
0253     BalanceNotifyList m_balanceNotifyList;
0254 
0255     /**
0256       * This member keeps a list of account ids for which
0257       * a balanceChanged() signal needs to be emitted when
0258       * a set of operations has been committed.
0259       *
0260       * @sa MyMoneyFile::commitTransaction()
0261       */
0262     QSet<QString>     m_balanceChangedSet;
0263 
0264     /**
0265       * This member keeps a list of account ids for which
0266       * a valueChanged() signal needs to be emitted when
0267       * a set of operations has been committed.
0268       *
0269       * @sa MyMoneyFile::commitTransaction()
0270       */
0271     QSet<QString>     m_valueChangedSet;
0272 
0273     /**
0274       * This member keeps the list of changes in the engine
0275       * in historical order. The type can be 'added', 'modified'
0276       * or removed.
0277       */
0278     QList<MyMoneyNotification> m_changeSet;
0279 };
0280 
0281 
0282 class MyMoneyNotifier
0283 {
0284 public:
0285     MyMoneyNotifier(MyMoneyFile::Private* file) {
0286         m_file = file;
0287         m_file->clearCacheNotification();
0288     }
0289     ~MyMoneyNotifier() {
0290         m_file->notify();
0291     }
0292 private:
0293     MyMoneyFile::Private* m_file;
0294 };
0295 
0296 
0297 
0298 MyMoneyFile::MyMoneyFile() :
0299     d(new Private)
0300 {
0301 }
0302 
0303 MyMoneyFile::~MyMoneyFile()
0304 {
0305     delete d;
0306 }
0307 
0308 MyMoneyFile::MyMoneyFile(MyMoneyStorageMgr *storage) :
0309     d(new Private)
0310 {
0311     attachStorage(storage);
0312 }
0313 
0314 MyMoneyFile* MyMoneyFile::instance()
0315 {
0316     return &file;
0317 }
0318 
0319 void MyMoneyFile::attachStorage(MyMoneyStorageMgr* const storage)
0320 {
0321     if (d->m_storage != 0)
0322         throw MYMONEYEXCEPTION_CSTRING("Storage already attached");
0323 
0324     if (storage == 0)
0325         throw MYMONEYEXCEPTION_CSTRING("Storage must not be 0");
0326 
0327     d->m_storage = storage;
0328 
0329     // force reload of base currency
0330     d->m_baseCurrency = MyMoneySecurity();
0331 
0332     // and the whole cache
0333     d->m_balanceCache.clear();
0334     d->m_priceCache.clear();
0335 
0336     // notify application about new data availability
0337     emit beginChangeNotification();
0338     emit dataChanged();
0339     emit endChangeNotification();
0340 }
0341 
0342 void MyMoneyFile::detachStorage(MyMoneyStorageMgr* const /* storage */)
0343 {
0344     d->m_balanceCache.clear();
0345     d->m_priceCache.clear();
0346     d->m_storage = nullptr;
0347 }
0348 
0349 MyMoneyStorageMgr* MyMoneyFile::storage() const
0350 {
0351     return d->m_storage;
0352 }
0353 
0354 bool MyMoneyFile::storageAttached() const
0355 {
0356     return d->m_storage != 0;
0357 }
0358 
0359 void MyMoneyFile::startTransaction()
0360 {
0361     d->checkStorage();
0362     if (d->m_inTransaction) {
0363         throw MYMONEYEXCEPTION_CSTRING("Already started a transaction!");
0364     }
0365 
0366     d->m_storage->startTransaction();
0367     d->m_inTransaction = true;
0368     d->m_changeSet.clear();
0369 }
0370 
0371 bool MyMoneyFile::hasTransaction() const
0372 {
0373     return d->m_inTransaction;
0374 }
0375 
0376 void MyMoneyFile::commitTransaction()
0377 {
0378     d->checkTransaction(Q_FUNC_INFO);
0379 
0380     // commit the transaction in the storage
0381     const auto changed = d->m_storage->commitTransaction();
0382     d->m_inTransaction = false;
0383 
0384     // collect notifications about removed objects
0385     QStringList removedObjects;
0386     const auto& set = d->m_changeSet;
0387     for (const auto& change : set) {
0388         switch (change.notificationMode()) {
0389         case File::Mode::Remove:
0390             removedObjects += change.id();
0391             break;
0392         default:
0393             break;
0394         }
0395     }
0396 
0397     // inform the outside world about the beginning of notifications
0398     emit beginChangeNotification();
0399 
0400     // Now it's time to send out some signals to the outside world
0401     // First we go through the d->m_changeSet and emit respective
0402     // signals about addition, modification and removal of engine objects
0403     const auto& changes = d->m_changeSet;
0404     for (const auto& change : changes) {
0405         switch (change.notificationMode()) {
0406         case File::Mode::Remove:
0407             emit objectRemoved(change.objectType(), change.id());
0408             // if there is a balance change recorded for this account remove it since the account itself will be removed
0409             // this can happen when deleting categories that have transactions and the reassign category feature was used
0410             d->m_balanceChangedSet.remove(change.id());
0411             break;
0412         case File::Mode::Add:
0413             if (!removedObjects.contains(change.id())) {
0414                 emit objectAdded(change.objectType(), change.id());
0415             }
0416             break;
0417         case File::Mode::Modify:
0418             if (!removedObjects.contains(change.id())) {
0419                 emit objectModified(change.objectType(), change.id());
0420             }
0421             break;
0422         }
0423     }
0424 
0425     // we're done with the change set, so we clear it
0426     d->m_changeSet.clear();
0427 
0428     // now send out the balanceChanged signal for all those
0429     // accounts for which we have an indication about a possible
0430     // change.
0431     const auto& balanceChanges = d->m_balanceChangedSet;
0432     for (const auto& id : balanceChanges) {
0433         if (!removedObjects.contains(id)) {
0434             // if we notify about balance change we don't need to notify about value change
0435             // for the same account since a balance change implies a value change
0436             d->m_valueChangedSet.remove(id);
0437             emit balanceChanged(account(id));
0438         }
0439     }
0440     d->m_balanceChangedSet.clear();
0441 
0442     // now notify about the remaining value changes
0443     const auto& m_valueChanges = d->m_valueChangedSet;
0444     for (const auto& id : m_valueChanges) {
0445         if (!removedObjects.contains(id)) {
0446             emit valueChanged(account(id));
0447         }
0448     }
0449 
0450     d->m_valueChangedSet.clear();
0451 
0452     // as a last action, send out the global dataChanged signal
0453     if (changed)
0454         emit dataChanged();
0455 
0456     // inform the outside world about the end of notifications
0457     emit endChangeNotification();
0458 }
0459 
0460 void MyMoneyFile::rollbackTransaction()
0461 {
0462     d->checkTransaction(Q_FUNC_INFO);
0463 
0464     d->m_storage->rollbackTransaction();
0465     d->m_inTransaction = false;
0466     d->m_balanceChangedSet.clear();
0467     d->m_valueChangedSet.clear();
0468     d->m_changeSet.clear();
0469 }
0470 
0471 void MyMoneyFile::addInstitution(MyMoneyInstitution& institution)
0472 {
0473     // perform some checks to see that the institution stuff is OK. For
0474     // now we assume that the institution must have a name, the ID is not set
0475     // and it does not have a parent (MyMoneyFile).
0476 
0477     if (institution.name().length() == 0
0478             || institution.id().length() != 0)
0479         throw MYMONEYEXCEPTION_CSTRING("Not a new institution");
0480 
0481     d->checkTransaction(Q_FUNC_INFO);
0482 
0483     d->m_storage->addInstitution(institution);
0484     d->m_changeSet += MyMoneyNotification(File::Mode::Add, institution);
0485 }
0486 
0487 void MyMoneyFile::modifyInstitution(const MyMoneyInstitution& institution)
0488 {
0489     d->checkTransaction(Q_FUNC_INFO);
0490 
0491     d->m_storage->modifyInstitution(institution);
0492     d->m_changeSet += MyMoneyNotification(File::Mode::Modify, institution);
0493 }
0494 
0495 void MyMoneyFile::modifyTransaction(const MyMoneyTransaction& transaction)
0496 {
0497     d->checkTransaction(Q_FUNC_INFO);
0498 
0499     MyMoneyTransaction tCopy(transaction);
0500 
0501     // now check the splits
0502     bool loanAccountAffected = false;
0503     const auto splits1 = transaction.splits();
0504     for (const auto& split : splits1) {
0505         // the following line will throw an exception if the
0506         // account does not exist
0507         auto acc = MyMoneyFile::account(split.accountId());
0508         if (acc.id().isEmpty())
0509             throw MYMONEYEXCEPTION_CSTRING("Cannot store split with no account assigned");
0510         if (isStandardAccount(split.accountId()))
0511             throw MYMONEYEXCEPTION_CSTRING("Cannot store split referencing standard account");
0512         if (acc.isLoan() && (split.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::Transfer)))
0513             loanAccountAffected = true;
0514     }
0515 
0516     // change transfer splits between asset/liability and loan accounts
0517     // into amortization splits
0518     if (loanAccountAffected) {
0519         const auto splits = transaction.splits();
0520         for (const auto& split : splits) {
0521             if (split.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::Transfer)) {
0522                 auto acc = MyMoneyFile::account(split.accountId());
0523 
0524                 if (acc.isAssetLiability()) {
0525                     MyMoneySplit s = split;
0526                     s.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::Amortization));
0527                     tCopy.modifySplit(s);
0528                 }
0529             }
0530         }
0531     }
0532 
0533     // clear all changed objects from cache
0534     MyMoneyNotifier notifier(d);
0535 
0536     // get the current setting of this transaction
0537     MyMoneyTransaction tr = MyMoneyFile::transaction(transaction.id());
0538 
0539     // scan the splits again to update notification list
0540     // and mark all accounts that are referenced
0541     const auto splits2 = tr.splits();
0542     foreach (const auto& split, splits2)
0543         d->addCacheNotification(split.accountId(), tr.postDate());
0544 
0545     // make sure the value is rounded to the accounts precision
0546     fixSplitPrecision(tCopy);
0547 
0548     // perform modification
0549     d->m_storage->modifyTransaction(tCopy);
0550 
0551     // and mark all accounts that are referenced
0552     const auto splits3 = tCopy.splits();
0553     for (const auto& split : splits3)
0554         d->addCacheNotification(split.accountId(), tCopy.postDate());
0555 
0556     d->m_changeSet += MyMoneyNotification(File::Mode::Modify, transaction);
0557 }
0558 
0559 void MyMoneyFile::modifyAccount(const MyMoneyAccount& _account)
0560 {
0561     d->checkTransaction(Q_FUNC_INFO);
0562 
0563     MyMoneyAccount account(_account);
0564 
0565     auto acc = MyMoneyFile::account(account.id());
0566 
0567     // check that for standard accounts only specific parameters are changed
0568     if (isStandardAccount(account.id())) {
0569         // make sure to use the stuff we found on file
0570         account = acc;
0571 
0572         // and only use the changes that are allowed
0573         account.setName(_account.name());
0574         account.setCurrencyId(_account.currencyId());
0575 
0576         // now check that it is the same
0577         if (!(account == _account))
0578             throw MYMONEYEXCEPTION_CSTRING("Unable to modify the standard account groups");
0579     }
0580 
0581     if (account.accountType() != acc.accountType() //
0582              && !account.isLiquidAsset() && !acc.isLiquidAsset())
0583         throw MYMONEYEXCEPTION_CSTRING("Unable to change account type");
0584 
0585     // if the account was moved to another institution, we notify
0586     // the old one as well as the new one and the structure change
0587     if (acc.institutionId() != account.institutionId()) {
0588         MyMoneyInstitution inst;
0589         if (!acc.institutionId().isEmpty()) {
0590             inst = institution(acc.institutionId());
0591             inst.removeAccountId(acc.id());
0592             modifyInstitution(inst);
0593             // modifyInstitution updates d->m_changeSet already
0594         }
0595         if (!account.institutionId().isEmpty()) {
0596             inst = institution(account.institutionId());
0597             inst.addAccountId(acc.id());
0598             modifyInstitution(inst);
0599             // modifyInstitution updates d->m_changeSet already
0600         }
0601     }
0602 
0603     // check if account can be closed
0604     if (account.isClosed() && !acc.isClosed()) {
0605         // balance must be zero
0606         if (!account.balance().isZero())
0607             throw MYMONEYEXCEPTION_CSTRING("Cannot close account with balance unequal to zero");
0608         if (account.hasOnlineMapping())
0609             throw MYMONEYEXCEPTION_CSTRING("Cannot close account with active online mapping");
0610 
0611         // all children must be closed already
0612         const auto accList = account.accountList();
0613         for (const auto& sAccount : accList) {
0614             const auto subAccount = MyMoneyFile::instance()->account(sAccount);
0615             if (!subAccount.isClosed()) {
0616                 throw MYMONEYEXCEPTION_CSTRING("Cannot close account with open sub-account");
0617             }
0618         }
0619 
0620         // there must be no unfinished schedule referencing the account
0621         QList<MyMoneySchedule> list = scheduleList();
0622         QList<MyMoneySchedule>::const_iterator it_l;
0623         for (it_l = list.constBegin(); it_l != list.constEnd(); ++it_l) {
0624             if ((*it_l).isFinished())
0625                 continue;
0626             if ((*it_l).hasReferenceTo(acc.id())) {
0627                 throw MYMONEYEXCEPTION_CSTRING("Cannot close account referenced in schedule");
0628             }
0629         }
0630     }
0631 
0632     d->m_storage->modifyAccount(account);
0633     d->m_changeSet += MyMoneyNotification(File::Mode::Modify, account);
0634 }
0635 
0636 void MyMoneyFile::reparentAccount(MyMoneyAccount &acc, MyMoneyAccount& parent)
0637 {
0638     d->checkTransaction(Q_FUNC_INFO);
0639 
0640     // check that it's not one of the standard account groups
0641     if (isStandardAccount(acc.id()))
0642         throw MYMONEYEXCEPTION_CSTRING("Unable to reparent the standard account groups");
0643 
0644     if (acc.accountGroup() == parent.accountGroup()
0645             || (acc.accountType() == Account::Type::Income && parent.accountType() == Account::Type::Expense)
0646             || (acc.accountType() == Account::Type::Expense && parent.accountType() == Account::Type::Income)) {
0647 
0648         if (acc.isInvest() && parent.accountType() != Account::Type::Investment)
0649             throw MYMONEYEXCEPTION_CSTRING("Unable to reparent Stock to non-investment account");
0650 
0651         if (parent.accountType() == Account::Type::Investment && !acc.isInvest())
0652             throw MYMONEYEXCEPTION_CSTRING("Unable to reparent non-stock to investment account");
0653 
0654         // keep a notification of the current parent
0655         MyMoneyAccount curParent = account(acc.parentAccountId());
0656 
0657         d->m_storage->reparentAccount(acc, parent);
0658 
0659         d->m_changeSet += MyMoneyNotification(File::Mode::Modify, curParent);
0660         d->m_changeSet += MyMoneyNotification(File::Mode::Modify, parent);
0661         d->m_changeSet += MyMoneyNotification(File::Mode::Modify, acc);
0662 
0663     } else
0664         throw MYMONEYEXCEPTION_CSTRING("Unable to reparent to different account type");
0665 }
0666 
0667 MyMoneyInstitution MyMoneyFile::institution(const QString& id) const
0668 {
0669     return d->m_storage->institution(id);
0670 }
0671 
0672 MyMoneyAccount MyMoneyFile::account(const QString& id) const
0673 {
0674     if (Q_UNLIKELY(id.isEmpty())) // FIXME: Stop requesting accounts with empty id
0675         return MyMoneyAccount();
0676 
0677     return d->m_storage->account(id);
0678 }
0679 
0680 MyMoneyAccount MyMoneyFile::subAccountByName(const MyMoneyAccount& account, const QString& name) const
0681 {
0682     static MyMoneyAccount nullAccount;
0683 
0684     const auto accounts = account.accountList();
0685     for (const auto& acc : accounts) {
0686         const auto sacc = MyMoneyFile::account(acc);
0687         if (sacc.name().compare(name) == 0)
0688             return sacc;
0689     }
0690     return nullAccount;
0691 }
0692 
0693 MyMoneyAccount MyMoneyFile::accountByName(const QString& name) const
0694 {
0695     try {
0696         return d->m_storage->accountByName(name);
0697     } catch (const MyMoneyException &) {
0698     }
0699     return MyMoneyAccount();
0700 }
0701 
0702 void MyMoneyFile::removeTransaction(const MyMoneyTransaction& transaction)
0703 {
0704     d->checkTransaction(Q_FUNC_INFO);
0705 
0706     // clear all changed objects from cache
0707     MyMoneyNotifier notifier(d);
0708 
0709     // get the engine's idea about this transaction
0710     MyMoneyTransaction tr = MyMoneyFile::transaction(transaction.id());
0711 
0712     // scan the splits again to update notification list
0713     const auto splits = tr.splits();
0714     for (const auto& split : splits) {
0715         auto acc = account(split.accountId());
0716         if (acc.isClosed())
0717             throw MYMONEYEXCEPTION(QString::fromLatin1("Cannot remove transaction that references a closed account."));
0718         d->addCacheNotification(split.accountId(), tr.postDate());
0719         //FIXME-ALEX Do I need to add d->addCacheNotification(split.tagList()); ??
0720     }
0721 
0722     d->m_storage->removeTransaction(transaction);
0723 
0724     // remove a possible notification of that same object from the changeSet
0725     QList<MyMoneyNotification>::iterator it;
0726     for(it = d->m_changeSet.begin(); it != d->m_changeSet.end();) {
0727         if((*it).id() == transaction.id()) {
0728             it = d->m_changeSet.erase(it);
0729         } else {
0730             ++it;
0731         }
0732     }
0733 
0734     d->m_changeSet += MyMoneyNotification(File::Mode::Remove, transaction);
0735 }
0736 
0737 
0738 bool MyMoneyFile::hasActiveSplits(const QString& id) const
0739 {
0740     d->checkStorage();
0741 
0742     return d->m_storage->hasActiveSplits(id);
0743 }
0744 
0745 bool MyMoneyFile::isStandardAccount(const QString& id) const
0746 {
0747     d->checkStorage();
0748 
0749     return d->m_storage->isStandardAccount(id);
0750 }
0751 
0752 void MyMoneyFile::setAccountName(const QString& id, const QString& name) const
0753 {
0754     d->checkTransaction(Q_FUNC_INFO);
0755 
0756     auto acc = account(id);
0757     d->m_storage->setAccountName(id, name);
0758     d->m_changeSet += MyMoneyNotification(File::Mode::Modify, acc);
0759 }
0760 
0761 void MyMoneyFile::removeAccount(const MyMoneyAccount& account)
0762 {
0763     d->checkTransaction(Q_FUNC_INFO);
0764 
0765     MyMoneyAccount parent;
0766     MyMoneyAccount acc;
0767     MyMoneyInstitution institution;
0768 
0769     // check that the account and its parent exist
0770     // this will throw an exception if the id is unknown
0771     acc = MyMoneyFile::account(account.id());
0772     parent = MyMoneyFile::account(account.parentAccountId());
0773     if (!acc.institutionId().isEmpty())
0774         institution = MyMoneyFile::institution(acc.institutionId());
0775 
0776     // check that it's not one of the standard account groups
0777     if (isStandardAccount(account.id()))
0778         throw MYMONEYEXCEPTION_CSTRING("Unable to remove the standard account groups");
0779 
0780     if (hasActiveSplits(account.id())) {
0781         throw MYMONEYEXCEPTION_CSTRING("Unable to remove account with active splits");
0782     }
0783 
0784     // collect all sub-ordinate accounts for notification
0785     const auto accounts = acc.accountList();
0786     for (const auto& id : accounts)
0787         d->m_changeSet += MyMoneyNotification(File::Mode::Modify, MyMoneyFile::account(id));
0788 
0789     // don't forget the parent and a possible institution
0790 
0791     if (!institution.id().isEmpty()) {
0792         institution.removeAccountId(account.id());
0793         d->m_storage->modifyInstitution(institution);
0794         d->m_changeSet += MyMoneyNotification(File::Mode::Modify, institution);
0795     }
0796     acc.setInstitutionId(QString());
0797 
0798     d->m_storage->removeAccount(acc);
0799 
0800     d->m_balanceCache.clear(acc.id());
0801 
0802     d->m_changeSet += MyMoneyNotification(File::Mode::Modify, parent);
0803     d->m_changeSet += MyMoneyNotification(File::Mode::Remove, acc);
0804 }
0805 
0806 void MyMoneyFile::removeAccountList(const QStringList& account_list, unsigned int level)
0807 {
0808     if (level > 100)
0809         throw MYMONEYEXCEPTION_CSTRING("Too deep recursion in [MyMoneyFile::removeAccountList]!");
0810 
0811     d->checkTransaction(Q_FUNC_INFO);
0812 
0813     // upon entry, we check that we could proceed with the operation
0814     if (!level) {
0815         if (!hasOnlyUnusedAccounts(account_list, 0)) {
0816             throw MYMONEYEXCEPTION_CSTRING("One or more accounts cannot be removed");
0817         }
0818     }
0819 
0820     // process all accounts in the list and test if they have transactions assigned
0821     foreach (const auto sAccount, account_list) {
0822         auto a = d->m_storage->account(sAccount);
0823         //qDebug() << "Deleting account '"<< a.name() << "'";
0824 
0825         // first remove all sub-accounts
0826         if (!a.accountList().isEmpty()) {
0827             removeAccountList(a.accountList(), level + 1);
0828 
0829             // then remove account itself, but we first have to get
0830             // rid of the account list that is still stored in
0831             // the MyMoneyAccount object. Easiest way is to get a fresh copy.
0832             a = d->m_storage->account(sAccount);
0833         }
0834 
0835         // make sure to remove the item from the cache
0836         removeAccount(a);
0837     }
0838 }
0839 
0840 bool MyMoneyFile::hasOnlyUnusedAccounts(const QStringList& account_list, unsigned int level)
0841 {
0842     if (level > 100)
0843         throw MYMONEYEXCEPTION_CSTRING("Too deep recursion in [MyMoneyFile::hasOnlyUnusedAccounts]!");
0844     // process all accounts in the list and test if they have transactions assigned
0845     for (const auto& sAccount : account_list) {
0846         if (transactionCount(sAccount) != 0)
0847             return false; // the current account has a transaction assigned
0848         if (!hasOnlyUnusedAccounts(account(sAccount).accountList(), level + 1))
0849             return false; // some sub-account has a transaction assigned
0850     }
0851     return true; // all subaccounts unused
0852 }
0853 
0854 
0855 void MyMoneyFile::removeInstitution(const MyMoneyInstitution& institution)
0856 {
0857     d->checkTransaction(Q_FUNC_INFO);
0858 
0859     MyMoneyInstitution inst = MyMoneyFile::institution(institution.id());
0860 
0861     bool blocked = signalsBlocked();
0862     blockSignals(true);
0863     const auto accounts = inst.accountList();
0864     for (const auto& acc : accounts) {
0865         auto a = account(acc);
0866         a.setInstitutionId(QString());
0867         modifyAccount(a);
0868         d->m_changeSet += MyMoneyNotification(File::Mode::Modify, a);
0869     }
0870     blockSignals(blocked);
0871 
0872     d->m_storage->removeInstitution(institution);
0873 
0874     d->m_changeSet += MyMoneyNotification(File::Mode::Remove, institution);
0875 }
0876 
0877 void MyMoneyFile::createAccount(MyMoneyAccount& newAccount, MyMoneyAccount& parentAccount, MyMoneyAccount& brokerageAccount, MyMoneyMoney openingBal)
0878 {
0879     // make sure we have a currency. If none is assigned, we assume base currency
0880     if (newAccount.currencyId().isEmpty())
0881         newAccount.setCurrencyId(baseCurrency().id());
0882 
0883     MyMoneyFileTransaction ft;
0884     try {
0885         int pos;
0886         // check for ':' in the name and use it as separator for a hierarchy
0887         while ((pos = newAccount.name().indexOf(MyMoneyFile::AccountSeparator)) != -1) {
0888             QString part = newAccount.name().left(pos);
0889             QString remainder = newAccount.name().mid(pos + 1);
0890             const MyMoneyAccount& existingAccount = subAccountByName(parentAccount, part);
0891             if (existingAccount.id().isEmpty()) {
0892                 newAccount.setName(part);
0893 
0894                 addAccount(newAccount, parentAccount);
0895                 parentAccount = newAccount;
0896             } else {
0897                 parentAccount = existingAccount;
0898             }
0899             newAccount.setParentAccountId(QString());  // make sure, there's no parent
0900             newAccount.clearId();                       // and no id set for adding
0901             newAccount.removeAccountIds();              // and no sub-account ids
0902             newAccount.setName(remainder);
0903         }
0904 
0905         addAccount(newAccount, parentAccount);
0906 
0907         // in case of a loan account, we add the initial payment
0908         if ((newAccount.accountType() == Account::Type::Loan
0909                 || newAccount.accountType() == Account::Type::AssetLoan)
0910                 && !newAccount.value("kmm-loan-payment-acc").isEmpty()
0911                 && !newAccount.value("kmm-loan-payment-date").isEmpty()) {
0912             MyMoneyAccountLoan acc(newAccount);
0913             MyMoneyTransaction t;
0914             MyMoneySplit a, b;
0915             a.setAccountId(acc.id());
0916             b.setAccountId(acc.value("kmm-loan-payment-acc"));
0917             a.setValue(acc.loanAmount());
0918             if (acc.accountType() == Account::Type::Loan)
0919                 a.setValue(-a.value());
0920 
0921             a.setShares(a.value());
0922             b.setValue(-a.value());
0923             b.setShares(b.value());
0924             a.setMemo(i18n("Loan payout"));
0925             b.setMemo(i18n("Loan payout"));
0926             t.setPostDate(QDate::fromString(acc.value("kmm-loan-payment-date"), Qt::ISODate));
0927             newAccount.deletePair("kmm-loan-payment-acc");
0928             newAccount.deletePair("kmm-loan-payment-date");
0929             MyMoneyFile::instance()->modifyAccount(newAccount);
0930 
0931             t.addSplit(a);
0932             t.addSplit(b);
0933             addTransaction(t);
0934             createOpeningBalanceTransaction(newAccount, openingBal);
0935 
0936             // in case of an investment account we check if we should create
0937             // a brokerage account
0938         } else if (newAccount.accountType() == Account::Type::Investment
0939                    && !brokerageAccount.name().isEmpty()) {
0940             addAccount(brokerageAccount, parentAccount);
0941 
0942             // set a link from the investment account to the brokerage account
0943             modifyAccount(newAccount);
0944             createOpeningBalanceTransaction(brokerageAccount, openingBal);
0945 
0946         } else
0947             createOpeningBalanceTransaction(newAccount, openingBal);
0948 
0949         ft.commit();
0950     } catch (const MyMoneyException &e) {
0951         qWarning("Unable to create account: %s", e.what());
0952         throw;
0953     }
0954 }
0955 
0956 void MyMoneyFile::addAccount(MyMoneyAccount& account, MyMoneyAccount& parent)
0957 {
0958     d->checkTransaction(Q_FUNC_INFO);
0959 
0960     MyMoneyInstitution institution;
0961 
0962     // perform some checks to see that the account stuff is OK. For
0963     // now we assume that the account must have a name, has no
0964     // transaction and sub-accounts and parent account
0965     // it's own ID is not set and it does not have a pointer to (MyMoneyFile)
0966 
0967     if (account.name().length() == 0)
0968         throw MYMONEYEXCEPTION_CSTRING("Account has no name");
0969 
0970     if (account.id().length() != 0)
0971         throw MYMONEYEXCEPTION_CSTRING("New account must have no id");
0972 
0973     if (account.accountList().count() != 0)
0974         throw MYMONEYEXCEPTION_CSTRING("New account must have no sub-accounts");
0975 
0976     if (!account.parentAccountId().isEmpty())
0977         throw MYMONEYEXCEPTION_CSTRING("New account must have no parent-id");
0978 
0979     if (account.accountType() == Account::Type::Unknown)
0980         throw MYMONEYEXCEPTION_CSTRING("Account has invalid type");
0981 
0982     // make sure, that the parent account exists
0983     // if not, an exception is thrown. If it exists,
0984     // get a copy of the current data
0985     auto acc = MyMoneyFile::account(parent.id());
0986 
0987 #if 0
0988     // TODO: remove the following code as we now can have multiple accounts
0989     // with the same name even in the same hierarchy position of the account tree
0990     //
0991     // check if the selected name is currently not among the child accounts
0992     // if we find one, then return it as the new account
0993     QStringList::const_iterator it_a;
0994     foreach (const auto accountID, acc.accountList()) {
0995         MyMoneyAccount a = MyMoneyFile::account(accountID);
0996         if (account.name() == a.name()) {
0997             account = a;
0998             return;
0999         }
1000     }
1001 #endif
1002 
1003     // FIXME: make sure, that the parent has the same type
1004     // I left it out here because I don't know, if there is
1005     // a tight coupling between e.g. checking accounts and the
1006     // class asset. It certainly does not make sense to create an
1007     // expense account under an income account. Maybe it does, I don't know.
1008 
1009     // We enforce, that a stock account can never be a parent and
1010     // that the parent for a stock account must be an investment. Also,
1011     // an investment cannot have another investment account as it's parent
1012     if (parent.isInvest())
1013         throw MYMONEYEXCEPTION_CSTRING("Stock account cannot be parent account");
1014 
1015     if (account.isInvest() && parent.accountType() != Account::Type::Investment)
1016         throw MYMONEYEXCEPTION_CSTRING("Stock account must have investment account as parent ");
1017 
1018     if (!account.isInvest() && parent.accountType() == Account::Type::Investment)
1019         throw MYMONEYEXCEPTION_CSTRING("Investment account can only have stock accounts as children");
1020 
1021     // if an institution is set, verify that it exists
1022     if (account.institutionId().length() != 0) {
1023         // check the presence of the institution. if it
1024         // does not exist, an exception is thrown
1025         institution = MyMoneyFile::institution(account.institutionId());
1026     }
1027 
1028     // if we don't have a valid opening date use today
1029     if (!account.openingDate().isValid()) {
1030         account.setOpeningDate(QDate::currentDate());
1031     }
1032 
1033     // make sure to set the opening date for categories to a
1034     // fixed date (1900-1-1). See #313793 on b.k.o for details
1035     if (account.isIncomeExpense()) {
1036         account.setOpeningDate(QDate(1900, 1, 1));
1037     }
1038 
1039     // if we don't have a currency assigned use the base currency
1040     if (account.currencyId().isEmpty()) {
1041         account.setCurrencyId(baseCurrency().id());
1042     }
1043 
1044     // make sure the parent id is setup
1045     account.setParentAccountId(parent.id());
1046 
1047     d->m_storage->addAccount(account);
1048     d->m_changeSet += MyMoneyNotification(File::Mode::Add, account);
1049 
1050     d->m_storage->addAccount(parent, account);
1051     d->m_changeSet += MyMoneyNotification(File::Mode::Modify, parent);
1052 
1053     if (account.institutionId().length() != 0) {
1054         institution.addAccountId(account.id());
1055         d->m_storage->modifyInstitution(institution);
1056         d->m_changeSet += MyMoneyNotification(File::Mode::Modify, institution);
1057     }
1058 }
1059 
1060 MyMoneyTransaction MyMoneyFile::createOpeningBalanceTransaction(const MyMoneyAccount& acc, const MyMoneyMoney& balance)
1061 {
1062     MyMoneyTransaction t;
1063     // if the opening balance is not zero, we need
1064     // to create the respective transaction
1065     if (!balance.isZero()) {
1066         d->checkTransaction(Q_FUNC_INFO);
1067 
1068         MyMoneySecurity currency = security(acc.currencyId());
1069         MyMoneyAccount openAcc = openingBalanceAccount(currency);
1070 
1071         if (openAcc.openingDate() > acc.openingDate()) {
1072             openAcc.setOpeningDate(acc.openingDate());
1073             modifyAccount(openAcc);
1074         }
1075 
1076         MyMoneySplit s;
1077 
1078         t.setPostDate(acc.openingDate());
1079         t.setCommodity(acc.currencyId());
1080 
1081         s.setAccountId(acc.id());
1082         s.setShares(balance);
1083         s.setValue(balance);
1084         t.addSplit(s);
1085 
1086         s.clearId();
1087         s.setAccountId(openAcc.id());
1088         s.setShares(-balance);
1089         s.setValue(-balance);
1090         t.addSplit(s);
1091 
1092         addTransaction(t);
1093     }
1094     return t;
1095 }
1096 
1097 QString MyMoneyFile::openingBalanceTransaction(const MyMoneyAccount& acc) const
1098 {
1099     QString result;
1100 
1101     MyMoneySecurity currency = security(acc.currencyId());
1102     MyMoneyAccount openAcc;
1103 
1104     try {
1105         openAcc = openingBalanceAccount(currency);
1106     } catch (const MyMoneyException &) {
1107         return result;
1108     }
1109 
1110     // Iterate over all the opening balance transactions for this currency
1111     MyMoneyTransactionFilter filter;
1112     filter.addAccount(openAcc.id());
1113     QList<MyMoneyTransaction> transactions = transactionList(filter);
1114     QList<MyMoneyTransaction>::const_iterator it_t = transactions.constBegin();
1115     while (it_t != transactions.constEnd()) {
1116         try {
1117             // Test whether the transaction also includes a split into
1118             // this account
1119             (*it_t).splitByAccount(acc.id(), true /*match*/);
1120 
1121             // If so, we have a winner!
1122             result = (*it_t).id();
1123             break;
1124         } catch (const MyMoneyException &) {
1125             // If not, keep searching
1126             ++it_t;
1127         }
1128     }
1129 
1130     return result;
1131 }
1132 
1133 MyMoneyAccount MyMoneyFile::openingBalanceAccount(const MyMoneySecurity& security)
1134 {
1135     if (!security.isCurrency())
1136         throw MYMONEYEXCEPTION_CSTRING("Opening balance for non currencies not supported");
1137 
1138     try {
1139         return openingBalanceAccount_internal(security);
1140     } catch (const MyMoneyException &) {
1141         MyMoneyFileTransaction ft;
1142         MyMoneyAccount acc;
1143 
1144         try {
1145             acc = createOpeningBalanceAccount(security);
1146             ft.commit();
1147 
1148         } catch (const MyMoneyException &) {
1149             qDebug("Unable to create opening balance account for security %s", qPrintable(security.id()));
1150         }
1151         return acc;
1152     }
1153 }
1154 
1155 MyMoneyAccount MyMoneyFile::openingBalanceAccount(const MyMoneySecurity& security) const
1156 {
1157     return openingBalanceAccount_internal(security);
1158 }
1159 
1160 MyMoneyAccount MyMoneyFile::openingBalanceAccount_internal(const MyMoneySecurity& security) const
1161 {
1162     if (!security.isCurrency())
1163         throw MYMONEYEXCEPTION_CSTRING("Opening balance for non currencies not supported");
1164 
1165     MyMoneyAccount acc;
1166     QList<MyMoneyAccount> accounts;
1167     QList<MyMoneyAccount>::ConstIterator it;
1168 
1169     accountList(accounts, equity().accountList(), true);
1170 
1171     // If an account is clearly marked as an opening balance account
1172     // for a specific currency we use it
1173     for (it = accounts.constBegin(); it != accounts.constEnd(); ++it) {
1174         if ((it->value("OpeningBalanceAccount") == QLatin1String("Yes")) && (it->currencyId() == security.id())) {
1175             acc = *it;
1176             break;
1177         }
1178     }
1179 
1180     // If we did not find one, then we look for one with a
1181     // name starting with the opening balances prefix.
1182     if (acc.id().isEmpty()) {
1183         for (it = accounts.constBegin(); it != accounts.constEnd(); ++it) {
1184             if (it->name().startsWith(MyMoneyFile::openingBalancesPrefix()) && (it->currencyId() == security.id())) {
1185                 acc = *it;
1186                 break;
1187             }
1188         }
1189     }
1190 
1191     // If we still don't have an opening balances account, we
1192     // see if we can find one of type equity with the currency
1193     // in question. This is needed, in case we have an old file
1194     // which does not have the marker of step 1 above because
1195     // is not (yet) present, the name did not match because it
1196     // has been replaced (e.g. anonymization or the language
1197     // of the application has been changed)
1198     if (acc.id().isEmpty()) {
1199         for (it = accounts.constBegin(); it != accounts.constEnd(); ++it) {
1200             if ((it->accountType() == eMyMoney::Account::Type::Equity) && (it->currencyId() == security.id())) {
1201                 acc = *it;
1202                 break;
1203             }
1204         }
1205     }
1206 
1207     if (acc.id().isEmpty())
1208         throw MYMONEYEXCEPTION(QString::fromLatin1("No opening balance account for %1").arg(security.tradingSymbol()));
1209 
1210     return acc;
1211 }
1212 
1213 MyMoneyAccount MyMoneyFile::createOpeningBalanceAccount(const MyMoneySecurity& security)
1214 {
1215     d->checkTransaction(Q_FUNC_INFO);
1216 
1217     MyMoneyAccount acc;
1218     QList<MyMoneyAccount> accounts;
1219     QList<MyMoneyAccount>::ConstIterator it;
1220 
1221     accountList(accounts, equity().accountList(), true);
1222 
1223     // find present opening balance accounts without containing '('
1224     QString name;
1225     QString parentAccountId;
1226     QRegExp exp(QString("\\([A-Z]{3}\\)"));
1227 
1228     for (it = accounts.constBegin(); it != accounts.constEnd(); ++it) {
1229         if (it->value("OpeningBalanceAccount") == QLatin1String("Yes")
1230                 && exp.indexIn(it->name()) == -1) {
1231             name = it->name();
1232             parentAccountId = it->parentAccountId();
1233             break;
1234         }
1235     }
1236 
1237     if (name.isEmpty())
1238         name = MyMoneyFile::openingBalancesPrefix();
1239     if (security.id() != baseCurrency().id()) {
1240         name += QString(" (%1)").arg(security.id());
1241     }
1242     acc.setName(name);
1243     acc.setAccountType(Account::Type::Equity);
1244     acc.setCurrencyId(security.id());
1245     acc.setValue("OpeningBalanceAccount", "Yes");
1246 
1247     MyMoneyAccount parent = !parentAccountId.isEmpty() ? account(parentAccountId) : equity();
1248     this->addAccount(acc, parent);
1249     return acc;
1250 }
1251 
1252 void MyMoneyFile::addTransaction(MyMoneyTransaction& transaction)
1253 {
1254     d->checkTransaction(Q_FUNC_INFO);
1255 
1256     // clear all changed objects from cache
1257     MyMoneyNotifier notifier(d);
1258 
1259     // perform some checks to see that the transaction stuff is OK. For
1260     // now we assume that
1261     // * no ids are assigned
1262     // * the date valid (must not be empty)
1263     // * the referenced accounts in the splits exist
1264 
1265     // first perform all the checks
1266     if (!transaction.id().isEmpty())
1267         throw MYMONEYEXCEPTION_CSTRING("Unable to add transaction with id set");
1268     if (!transaction.postDate().isValid())
1269         throw MYMONEYEXCEPTION_CSTRING("Unable to add transaction with invalid postdate");
1270 
1271     // now check the splits
1272     auto loanAccountAffected = false;
1273     const auto splits1 = transaction.splits();
1274     for (const auto& split : splits1) {
1275         // the following line will throw an exception if the
1276         // account does not exist or is one of the standard accounts
1277         auto acc = MyMoneyFile::account(split.accountId());
1278         if (acc.id().isEmpty())
1279             throw MYMONEYEXCEPTION_CSTRING("Cannot add split with no account assigned");
1280         if (acc.isLoan())
1281             loanAccountAffected = true;
1282         if (isStandardAccount(split.accountId()))
1283             throw MYMONEYEXCEPTION_CSTRING("Cannot add split referencing standard account");
1284     }
1285 
1286     // change transfer splits between asset/liability and loan accounts
1287     // into amortization splits
1288     if (loanAccountAffected) {
1289         foreach (const auto split, transaction.splits()) {
1290             if (split.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::Transfer)) {
1291                 auto acc = MyMoneyFile::account(split.accountId());
1292 
1293                 if (acc.isAssetLiability()) {
1294                     MyMoneySplit s = split;
1295                     s.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::Amortization));
1296                     transaction.modifySplit(s);
1297                 }
1298             }
1299         }
1300     }
1301 
1302     // check that we have a commodity
1303     if (transaction.commodity().isEmpty()) {
1304         transaction.setCommodity(baseCurrency().id());
1305     }
1306 
1307     // make sure the value is rounded to the accounts precision
1308     fixSplitPrecision(transaction);
1309 
1310     // then add the transaction to the file global pool
1311     d->m_storage->addTransaction(transaction);
1312 
1313     // scan the splits again to update notification list
1314     const auto splits2 = transaction.splits();
1315     for (const auto& split : splits2)
1316         d->addCacheNotification(split.accountId(), transaction.postDate());
1317 
1318     d->m_changeSet += MyMoneyNotification(File::Mode::Add, transaction);
1319 }
1320 
1321 MyMoneyTransaction MyMoneyFile::transaction(const QString& id) const
1322 {
1323     d->checkStorage();
1324 
1325     return d->m_storage->transaction(id);
1326 }
1327 
1328 MyMoneyTransaction MyMoneyFile::transaction(const QString& account, const int idx) const
1329 {
1330     d->checkStorage();
1331 
1332     return d->m_storage->transaction(account, idx);
1333 }
1334 
1335 void MyMoneyFile::addPayee(MyMoneyPayee& payee)
1336 {
1337     d->checkTransaction(Q_FUNC_INFO);
1338 
1339     d->m_storage->addPayee(payee);
1340     d->m_changeSet += MyMoneyNotification(File::Mode::Add, payee);
1341 }
1342 
1343 MyMoneyPayee MyMoneyFile::payee(const QString& id) const
1344 {
1345     if (Q_UNLIKELY(id.isEmpty()))
1346         return MyMoneyPayee();
1347 
1348     return d->m_storage->payee(id);
1349 }
1350 
1351 MyMoneyPayee MyMoneyFile::payeeByName(const QString& name) const
1352 {
1353     d->checkStorage();
1354 
1355     return d->m_storage->payeeByName(name);
1356 }
1357 
1358 void MyMoneyFile::modifyPayee(const MyMoneyPayee& payee)
1359 {
1360     d->checkTransaction(Q_FUNC_INFO);
1361 
1362     d->m_storage->modifyPayee(payee);
1363     d->m_changeSet += MyMoneyNotification(File::Mode::Modify, payee);
1364 }
1365 
1366 void MyMoneyFile::removePayee(const MyMoneyPayee& payee)
1367 {
1368     d->checkTransaction(Q_FUNC_INFO);
1369 
1370     // FIXME we need to make sure, that the payee is not referenced anymore
1371     d->m_storage->removePayee(payee);
1372     d->m_changeSet += MyMoneyNotification(File::Mode::Remove, payee);
1373 }
1374 
1375 void MyMoneyFile::addTag(MyMoneyTag& tag)
1376 {
1377     d->checkTransaction(Q_FUNC_INFO);
1378 
1379     d->m_storage->addTag(tag);
1380     d->m_changeSet += MyMoneyNotification(File::Mode::Add, tag);
1381 }
1382 
1383 MyMoneyTag MyMoneyFile::tag(const QString& id) const
1384 {
1385     return d->m_storage->tag(id);
1386 }
1387 
1388 MyMoneyTag MyMoneyFile::tagByName(const QString& name) const
1389 {
1390     d->checkStorage();
1391 
1392     return d->m_storage->tagByName(name);
1393 }
1394 
1395 void MyMoneyFile::modifyTag(const MyMoneyTag& tag)
1396 {
1397     d->checkTransaction(Q_FUNC_INFO);
1398 
1399     d->m_storage->modifyTag(tag);
1400     d->m_changeSet += MyMoneyNotification(File::Mode::Modify, tag);
1401 }
1402 
1403 void MyMoneyFile::removeTag(const MyMoneyTag& tag)
1404 {
1405     d->checkTransaction(Q_FUNC_INFO);
1406 
1407     // FIXME we need to make sure, that the tag is not referenced anymore
1408     d->m_storage->removeTag(tag);
1409     d->m_changeSet += MyMoneyNotification(File::Mode::Remove, tag);
1410 }
1411 
1412 void MyMoneyFile::accountList(QList<MyMoneyAccount>& list, const QStringList& idlist, const bool recursive) const
1413 {
1414     d->checkStorage();
1415 
1416     if (idlist.isEmpty()) {
1417         d->m_storage->accountList(list);
1418 
1419 #if 0
1420         // TODO: I have no idea what this was good for, but it caused the networth report
1421         //       to show double the numbers so I commented it out (ipwizard, 2008-05-24)
1422         if (d->m_storage && (list.isEmpty() || list.size() != d->m_storage->accountCount())) {
1423             d->m_storage->accountList(list);
1424             d->m_cache.preloadAccount(list);
1425         }
1426 #endif
1427 
1428         QList<MyMoneyAccount>::Iterator it;
1429         for (it = list.begin(); it != list.end();) {
1430             if (isStandardAccount((*it).id())) {
1431                 it = list.erase(it);
1432             } else {
1433                 ++it;
1434             }
1435         }
1436     } else {
1437         QList<MyMoneyAccount>::ConstIterator it;
1438         QList<MyMoneyAccount> list_a;
1439         d->m_storage->accountList(list_a);
1440 
1441         for (it = list_a.constBegin(); it != list_a.constEnd(); ++it) {
1442             if (!isStandardAccount((*it).id())) {
1443                 if (idlist.indexOf((*it).id()) != -1) {
1444                     list.append(*it);
1445                     if (recursive == true && !(*it).accountList().isEmpty()) {
1446                         accountList(list, (*it).accountList(), true);
1447                     }
1448                 }
1449             }
1450         }
1451     }
1452 }
1453 
1454 QList<MyMoneyInstitution> MyMoneyFile::institutionList() const
1455 {
1456     return d->m_storage->institutionList();
1457 }
1458 
1459 // general get functions
1460 MyMoneyPayee MyMoneyFile::user() const
1461 {
1462     d->checkStorage();
1463     return d->m_storage->user();
1464 }
1465 
1466 // general set functions
1467 void MyMoneyFile::setUser(const MyMoneyPayee& user)
1468 {
1469     d->checkTransaction(Q_FUNC_INFO);
1470 
1471     d->m_storage->setUser(user);
1472 }
1473 
1474 bool MyMoneyFile::dirty() const
1475 {
1476     if (!d->m_storage)
1477         return false;
1478 
1479     return d->m_storage->dirty();
1480 }
1481 
1482 void MyMoneyFile::setDirty() const
1483 {
1484     d->checkStorage();
1485 
1486     d->m_storage->setDirty();
1487 }
1488 
1489 unsigned int MyMoneyFile::accountCount() const
1490 {
1491     d->checkStorage();
1492 
1493     return d->m_storage->accountCount();
1494 }
1495 
1496 void MyMoneyFile::ensureDefaultCurrency(MyMoneyAccount& acc) const
1497 {
1498     if (acc.currencyId().isEmpty()) {
1499         if (!baseCurrency().id().isEmpty())
1500             acc.setCurrencyId(baseCurrency().id());
1501     }
1502 }
1503 
1504 MyMoneyAccount MyMoneyFile::liability() const
1505 {
1506     d->checkStorage();
1507 
1508     return account(MyMoneyAccount::stdAccName(eMyMoney::Account::Standard::Liability));
1509 }
1510 
1511 MyMoneyAccount MyMoneyFile::asset() const
1512 {
1513     d->checkStorage();
1514 
1515     return account(MyMoneyAccount::stdAccName(eMyMoney::Account::Standard::Asset));
1516 }
1517 
1518 MyMoneyAccount MyMoneyFile::expense() const
1519 {
1520     d->checkStorage();
1521 
1522     return account(MyMoneyAccount::stdAccName(eMyMoney::Account::Standard::Expense));
1523 }
1524 
1525 MyMoneyAccount MyMoneyFile::income() const
1526 {
1527     d->checkStorage();
1528 
1529     return account(MyMoneyAccount::stdAccName(eMyMoney::Account::Standard::Income));
1530 }
1531 
1532 MyMoneyAccount MyMoneyFile::equity() const
1533 {
1534     d->checkStorage();
1535 
1536     return account(MyMoneyAccount::stdAccName(eMyMoney::Account::Standard::Equity));
1537 }
1538 
1539 unsigned int MyMoneyFile::transactionCount(const QString& account) const
1540 {
1541     d->checkStorage();
1542 
1543     return d->m_storage->transactionCount(account);
1544 }
1545 
1546 unsigned int MyMoneyFile::transactionCount() const
1547 {
1548     return transactionCount(QString());
1549 }
1550 
1551 QMap<QString, unsigned long> MyMoneyFile::transactionCountMap() const
1552 {
1553     d->checkStorage();
1554 
1555     return d->m_storage->transactionCountMap();
1556 }
1557 
1558 unsigned int MyMoneyFile::institutionCount() const
1559 {
1560     d->checkStorage();
1561 
1562     return d->m_storage->institutionCount();
1563 }
1564 
1565 MyMoneyMoney MyMoneyFile::balance(const QString& id, const QDate& date) const
1566 {
1567     if (date.isValid()) {
1568         MyMoneyBalanceCacheItem bal = d->m_balanceCache.balance(id, date);
1569         if (bal.isValid())
1570             return bal.balance();
1571     }
1572 
1573     d->checkStorage();
1574 
1575     MyMoneyMoney returnValue = d->m_storage->balance(id, date);
1576 
1577     if (date.isValid()) {
1578         d->m_balanceCache.insert(id, date, returnValue);
1579     }
1580 
1581     return returnValue;
1582 }
1583 
1584 MyMoneyMoney MyMoneyFile::balance(const QString& id) const
1585 {
1586     return balance(id, QDate());
1587 }
1588 
1589 MyMoneyMoney MyMoneyFile::clearedBalance(const QString &id, const QDate& date) const
1590 {
1591     MyMoneyMoney cleared;
1592     QList<MyMoneyTransaction> list;
1593 
1594     cleared = balance(id, date);
1595 
1596     MyMoneyAccount account = this->account(id);
1597     MyMoneyMoney factor(1, 1);
1598     if (account.accountGroup() == Account::Type::Liability || account.accountGroup() == Account::Type::Equity)
1599         factor = -factor;
1600 
1601     MyMoneyTransactionFilter filter;
1602     filter.addAccount(id);
1603     filter.setDateFilter(QDate(), date);
1604     filter.setReportAllSplits(false);
1605     filter.addState((int)TransactionFilter::State::NotReconciled);
1606     transactionList(list, filter);
1607 
1608     for (QList<MyMoneyTransaction>::const_iterator it_t = list.constBegin(); it_t != list.constEnd(); ++it_t) {
1609         const QList<MyMoneySplit>& splits = (*it_t).splits();
1610         for (QList<MyMoneySplit>::const_iterator it_s = splits.constBegin(); it_s != splits.constEnd(); ++it_s) {
1611             const MyMoneySplit &split = (*it_s);
1612             if (split.accountId() != id)
1613                 continue;
1614             cleared -= split.shares();
1615         }
1616     }
1617     return cleared * factor;
1618 }
1619 
1620 MyMoneyMoney MyMoneyFile::totalBalance(const QString& id, const QDate& date) const
1621 {
1622     d->checkStorage();
1623 
1624     return d->m_storage->totalBalance(id, date);
1625 }
1626 
1627 MyMoneyMoney MyMoneyFile::totalBalance(const QString& id) const
1628 {
1629     return totalBalance(id, QDate());
1630 }
1631 
1632 void MyMoneyFile::warningMissingRate(const QString& fromId, const QString& toId) const
1633 {
1634     MyMoneySecurity from, to;
1635     try {
1636         from = security(fromId);
1637         to = security(toId);
1638         qWarning("Missing price info for conversion from %s to %s", qPrintable(from.name()), qPrintable(to.name()));
1639 
1640     } catch (const MyMoneyException &e) {
1641         qWarning("Missing security caught in MyMoneyFile::warningMissingRate(). %s", e.what());
1642     }
1643 }
1644 
1645 void MyMoneyFile::transactionList(QList<QPair<MyMoneyTransaction, MyMoneySplit> >& list, MyMoneyTransactionFilter& filter) const
1646 {
1647     d->checkStorage();
1648     d->m_storage->transactionList(list, filter);
1649 }
1650 
1651 void MyMoneyFile::transactionList(QList<MyMoneyTransaction>& list, MyMoneyTransactionFilter& filter) const
1652 {
1653     d->checkStorage();
1654     d->m_storage->transactionList(list, filter);
1655 }
1656 
1657 QList<MyMoneyTransaction> MyMoneyFile::transactionList(MyMoneyTransactionFilter& filter) const
1658 {
1659     d->checkStorage();
1660     return d->m_storage->transactionList(filter);
1661 }
1662 
1663 QList<MyMoneyPayee> MyMoneyFile::payeeList() const
1664 {
1665     return d->m_storage->payeeList();
1666 }
1667 
1668 QList<MyMoneyTag> MyMoneyFile::tagList() const
1669 {
1670     return d->m_storage->tagList();
1671 }
1672 
1673 QString MyMoneyFile::accountToCategory(const QString& accountId, bool includeStandardAccounts) const
1674 {
1675     MyMoneyAccount acc;
1676     QString rc;
1677 
1678     if (!accountId.isEmpty()) {
1679         acc = account(accountId);
1680         do {
1681             if (!rc.isEmpty())
1682                 rc = AccountSeparator + rc;
1683             rc = acc.name() + rc;
1684             acc = account(acc.parentAccountId());
1685         } while (!acc.id().isEmpty() && (includeStandardAccounts || !isStandardAccount(acc.id())));
1686     }
1687     return rc;
1688 }
1689 
1690 QString MyMoneyFile::categoryToAccount(const QString& category, Account::Type type) const
1691 {
1692     QString id;
1693 
1694     // search the category in the expense accounts and if it is not found, try
1695     // to locate it in the income accounts
1696     if (type == Account::Type::Unknown
1697             || type == Account::Type::Expense) {
1698         id = locateSubAccount(MyMoneyFile::instance()->expense(), category);
1699     }
1700 
1701     if ((id.isEmpty() && type == Account::Type::Unknown)
1702             || type == Account::Type::Income) {
1703         id = locateSubAccount(MyMoneyFile::instance()->income(), category);
1704     }
1705 
1706     return id;
1707 }
1708 
1709 QString MyMoneyFile::categoryToAccount(const QString& category) const
1710 {
1711     return categoryToAccount(category, Account::Type::Unknown);
1712 }
1713 
1714 QString MyMoneyFile::nameToAccount(const QString& name) const
1715 {
1716     QString id;
1717 
1718     // search the category in the asset accounts and if it is not found, try
1719     // to locate it in the liability accounts
1720     id = locateSubAccount(MyMoneyFile::instance()->asset(), name);
1721     if (id.isEmpty())
1722         id = locateSubAccount(MyMoneyFile::instance()->liability(), name);
1723 
1724     return id;
1725 }
1726 
1727 QString MyMoneyFile::parentName(const QString& name) const
1728 {
1729     return name.section(AccountSeparator, 0, -2);
1730 }
1731 
1732 QString MyMoneyFile::locateSubAccount(const MyMoneyAccount& base, const QString& category) const
1733 {
1734     MyMoneyAccount nextBase;
1735     QString level, remainder;
1736     level = category.section(AccountSeparator, 0, 0);
1737     remainder = category.section(AccountSeparator, 1);
1738 
1739     foreach (const auto sAccount, base.accountList()) {
1740         nextBase = account(sAccount);
1741         if (nextBase.name() == level) {
1742             if (remainder.isEmpty()) {
1743                 return nextBase.id();
1744             }
1745             return locateSubAccount(nextBase, remainder);
1746         }
1747     }
1748     return QString();
1749 }
1750 
1751 QString MyMoneyFile::value(const QString& key) const
1752 {
1753     d->checkStorage();
1754 
1755     return d->m_storage->value(key);
1756 }
1757 
1758 void MyMoneyFile::setValue(const QString& key, const QString& val)
1759 {
1760     d->checkTransaction(Q_FUNC_INFO);
1761 
1762     d->m_storage->setValue(key, val);
1763 }
1764 
1765 void MyMoneyFile::deletePair(const QString& key)
1766 {
1767     d->checkTransaction(Q_FUNC_INFO);
1768 
1769     d->m_storage->deletePair(key);
1770 }
1771 
1772 void MyMoneyFile::addSchedule(MyMoneySchedule& sched)
1773 {
1774     d->checkTransaction(Q_FUNC_INFO);
1775 
1776     const auto splits = sched.transaction().splits();
1777     for (const auto& split : splits) {
1778         // the following line will throw an exception if the
1779         // account does not exist or is one of the standard accounts
1780         const auto acc = account(split.accountId());
1781         if (acc.id().isEmpty())
1782             throw MYMONEYEXCEPTION_CSTRING("Cannot add split with no account assigned");
1783         if (isStandardAccount(split.accountId()))
1784             throw MYMONEYEXCEPTION_CSTRING("Cannot add split referencing standard account");
1785     }
1786 
1787     d->m_storage->addSchedule(sched);
1788     d->m_changeSet += MyMoneyNotification(File::Mode::Add, sched);
1789 }
1790 
1791 void MyMoneyFile::modifySchedule(const MyMoneySchedule& sched)
1792 {
1793     d->checkTransaction(Q_FUNC_INFO);
1794 
1795     foreach (const auto split, sched.transaction().splits()) {
1796         // the following line will throw an exception if the
1797         // account does not exist or is one of the standard accounts
1798         auto acc = MyMoneyFile::account(split.accountId());
1799         if (acc.id().isEmpty())
1800             throw MYMONEYEXCEPTION_CSTRING("Cannot store split with no account assigned");
1801         if (isStandardAccount(split.accountId()))
1802             throw MYMONEYEXCEPTION_CSTRING("Cannot store split referencing standard account");
1803     }
1804 
1805     d->m_storage->modifySchedule(sched);
1806     d->m_changeSet += MyMoneyNotification(File::Mode::Modify, sched);
1807 }
1808 
1809 void MyMoneyFile::removeSchedule(const MyMoneySchedule& sched)
1810 {
1811     d->checkTransaction(Q_FUNC_INFO);
1812 
1813     d->m_storage->removeSchedule(sched);
1814     d->m_changeSet += MyMoneyNotification(File::Mode::Remove, sched);
1815 }
1816 
1817 MyMoneySchedule MyMoneyFile::schedule(const QString& id) const
1818 {
1819     return d->m_storage->schedule(id);
1820 }
1821 
1822 QList<MyMoneySchedule> MyMoneyFile::scheduleList(
1823     const QString& accountId,
1824     const Schedule::Type type,
1825     const Schedule::Occurrence occurrence,
1826     const Schedule::PaymentType paymentType,
1827     const QDate& startDate,
1828     const QDate& endDate,
1829     const bool overdue) const
1830 {
1831     d->checkStorage();
1832 
1833     return d->m_storage->scheduleList(accountId, type, occurrence, paymentType, startDate, endDate, overdue);
1834 }
1835 
1836 QList<MyMoneySchedule> MyMoneyFile::scheduleList(
1837     const QString& accountId) const
1838 {
1839     return scheduleList(accountId, Schedule::Type::Any, Schedule::Occurrence::Any, Schedule::PaymentType::Any,
1840                         QDate(), QDate(), false);
1841 }
1842 
1843 QList<MyMoneySchedule> MyMoneyFile::scheduleList() const
1844 {
1845     return scheduleList(QString(), Schedule::Type::Any, Schedule::Occurrence::Any, Schedule::PaymentType::Any,
1846                         QDate(), QDate(), false);
1847 }
1848 
1849 QStringList MyMoneyFile::consistencyCheck()
1850 {
1851     QList<MyMoneyAccount> list;
1852     QList<MyMoneyAccount>::Iterator it_a;
1853     QList<MyMoneySchedule>::Iterator it_sch;
1854     QList<MyMoneyPayee>::Iterator it_p;
1855     QList<MyMoneyTransaction>::Iterator it_t;
1856     QList<MyMoneyReport>::Iterator it_r;
1857     QStringList accountRebuild;
1858 
1859     QMap<QString, bool> interestAccounts;
1860 
1861     MyMoneyAccount parent;
1862     MyMoneyAccount child;
1863     MyMoneyAccount toplevel;
1864 
1865     QString parentId;
1866     QStringList rc;
1867 
1868     int problemCount = 0;
1869     int unfixedCount = 0;
1870     QString problemAccount;
1871 
1872     // check that we have a storage object
1873     d->checkTransaction(Q_FUNC_INFO);
1874 
1875     // get the current list of accounts
1876     accountList(list);
1877     // add the standard accounts
1878     list << MyMoneyFile::instance()->asset();
1879     list << MyMoneyFile::instance()->liability();
1880     list << MyMoneyFile::instance()->income();
1881     list << MyMoneyFile::instance()->expense();
1882 
1883     for (it_a = list.begin(); it_a != list.end(); ++it_a) {
1884         // no more checks for standard accounts
1885         if (isStandardAccount((*it_a).id())) {
1886             continue;
1887         }
1888 
1889         switch ((*it_a).accountGroup()) {
1890         case Account::Type::Asset:
1891             toplevel = asset();
1892             break;
1893         case Account::Type::Liability:
1894             toplevel = liability();
1895             break;
1896         case Account::Type::Expense:
1897             toplevel = expense();
1898             break;
1899         case Account::Type::Income:
1900             toplevel = income();
1901             break;
1902         case Account::Type::Equity:
1903             toplevel = equity();
1904             break;
1905         default:
1906             qWarning("%s:%d This should never happen!", __FILE__, __LINE__);
1907             break;
1908         }
1909 
1910         // check for loops in the hierarchy
1911         parentId = (*it_a).parentAccountId();
1912         try {
1913             bool dropOut = false;
1914             while (!isStandardAccount(parentId) && !dropOut) {
1915                 parent = account(parentId);
1916                 if (parent.id() == (*it_a).id()) {
1917                     // parent loops, so we need to re-parent to toplevel account
1918                     // find parent account in our list
1919                     problemCount++;
1920                     QList<MyMoneyAccount>::Iterator it_b;
1921                     for (it_b = list.begin(); it_b != list.end(); ++it_b) {
1922                         if ((*it_b).id() == parent.id()) {
1923                             if (problemAccount != (*it_a).name()) {
1924                                 problemAccount = (*it_a).name();
1925                                 rc << i18n("* Problem with account '%1'", problemAccount);
1926                                 rc << i18n("  * Loop detected between this account and account '%1'.", (*it_b).name());
1927                                 rc << i18n("    Reparenting account '%2' to top level account '%1'.", toplevel.name(), (*it_a).name());
1928                                 (*it_a).setParentAccountId(toplevel.id());
1929                                 if (accountRebuild.contains(toplevel.id()) == 0)
1930                                     accountRebuild << toplevel.id();
1931                                 if (accountRebuild.contains((*it_a).id()) == 0)
1932                                     accountRebuild << (*it_a).id();
1933                                 dropOut = true;
1934                                 break;
1935                             }
1936                         }
1937                     }
1938                 }
1939                 parentId = parent.parentAccountId();
1940             }
1941 
1942         } catch (const MyMoneyException &) {
1943             // if we don't know about a parent, we catch it later
1944         }
1945 
1946         // check that the parent exists
1947         parentId = (*it_a).parentAccountId();
1948         try {
1949             parent = account(parentId);
1950             if ((*it_a).accountGroup() != parent.accountGroup()) {
1951                 problemCount++;
1952                 if (problemAccount != (*it_a).name()) {
1953                     problemAccount = (*it_a).name();
1954                     rc << i18n("* Problem with account '%1'", problemAccount);
1955                 }
1956                 // the parent belongs to a different group, so we reconnect to the
1957                 // master group account (asset, liability, etc) to which this account
1958                 // should belong and update it in the engine.
1959                 rc << i18n("  * Parent account '%1' belongs to a different group.", parent.name());
1960                 rc << i18n("    New parent account is the top level account '%1'.", toplevel.name());
1961                 (*it_a).setParentAccountId(toplevel.id());
1962 
1963                 // make sure to rebuild the sub-accounts of the top account
1964                 // and the one we removed this account from
1965                 if (accountRebuild.contains(toplevel.id()) == 0)
1966                     accountRebuild << toplevel.id();
1967                 if (accountRebuild.contains(parent.id()) == 0)
1968                     accountRebuild << parent.id();
1969             } else if (!parent.accountList().contains((*it_a).id())) {
1970                 problemCount++;
1971                 if (problemAccount != (*it_a).name()) {
1972                     problemAccount = (*it_a).name();
1973                     rc << i18n("* Problem with account '%1'", problemAccount);
1974                 }
1975                 // parent exists, but does not have a reference to the account
1976                 rc << i18n("  * Parent account '%1' does not contain '%2' as sub-account.", parent.name(), problemAccount);
1977                 if (accountRebuild.contains(parent.id()) == 0)
1978                     accountRebuild << parent.id();
1979             }
1980         } catch (const MyMoneyException &) {
1981             // apparently, the parent does not exist anymore. we reconnect to the
1982             // master group account (asset, liability, etc) to which this account
1983             // should belong and update it in the engine.
1984             problemCount++;
1985             if (problemAccount != (*it_a).name()) {
1986                 problemAccount = (*it_a).name();
1987                 rc << i18n("* Problem with account '%1'", problemAccount);
1988             }
1989             rc << i18n("  * The parent with id %1 does not exist anymore.", parentId);
1990             rc << i18n("    New parent account is the top level account '%1'.", toplevel.name());
1991             (*it_a).setParentAccountId(toplevel.id());
1992 
1993             // make sure to rebuild the sub-accounts of the top account
1994             if (accountRebuild.contains(toplevel.id()) == 0)
1995                 accountRebuild << toplevel.id();
1996         }
1997 
1998         // now check that all the children exist and have the correct type
1999         foreach (const auto accountID, (*it_a).accountList()) {
2000             // check that the child exists
2001             try {
2002                 child = account(accountID);
2003                 if (child.parentAccountId() != (*it_a).id()) {
2004                     throw MYMONEYEXCEPTION_CSTRING("Child account has a different parent");
2005                 }
2006             } catch (const MyMoneyException &) {
2007                 problemCount++;
2008                 if (problemAccount != (*it_a).name()) {
2009                     problemAccount = (*it_a).name();
2010                     rc << i18n("* Problem with account '%1'", problemAccount);
2011                 }
2012                 rc << i18n("  * Child account with id %1 does not exist anymore.", accountID);
2013                 rc << i18n("    The child account list will be reconstructed.");
2014                 if (accountRebuild.contains((*it_a).id()) == 0)
2015                     accountRebuild << (*it_a).id();
2016             }
2017         }
2018 
2019         // see if it is a loan account. if so, remember the assigned interest account
2020         if ((*it_a).isLoan()) {
2021             MyMoneyAccountLoan loan(*it_a);
2022             if (!loan.interestAccountId().isEmpty()) {
2023                 interestAccounts[loan.interestAccountId()] = true;
2024             }
2025             try {
2026                 payee(loan.payee());
2027             } catch (const MyMoneyException &) {
2028                 problemCount++;
2029                 if (problemAccount != (*it_a).name()) {
2030                     problemAccount = (*it_a).name();
2031                     rc << i18n("* Problem with account '%1'", problemAccount);
2032                 }
2033                 rc << i18n("  * The payee with id %1 referenced by the loan does not exist anymore.", loan.payee());
2034                 rc << i18n("    The payee will be removed.");
2035                 // remove the payee - the account will be modified in the engine later
2036                 (*it_a).deletePair("payee");
2037             }
2038         }
2039 
2040         // check if it is a category and set the date to 1900-01-01 if different
2041         if ((*it_a).isIncomeExpense()) {
2042             if (((*it_a).openingDate().isValid() == false) || ((*it_a).openingDate() != QDate(1900, 1, 1))) {
2043                 (*it_a).setOpeningDate(QDate(1900, 1, 1));
2044             }
2045         }
2046 
2047         // check for clear text online password in the online settings
2048         if (!(*it_a).onlineBankingSettings().value("password").isEmpty()) {
2049             if (problemAccount != (*it_a).name()) {
2050                 problemAccount = (*it_a).name();
2051                 rc << i18n("* Problem with account '%1'", problemAccount);
2052             }
2053             rc << i18n("  * Older versions of KMyMoney stored an OFX password for this account in cleartext.");
2054             rc << i18n("    Please open it in the account editor (Account/Edit account) once and press OK.");
2055             rc << i18n("    This will store the password in the KDE wallet and remove the cleartext version.");
2056             ++unfixedCount;
2057         }
2058 
2059         // if the account was modified, we need to update it in the engine
2060         if (!(d->m_storage->account((*it_a).id()) == (*it_a))) {
2061             try {
2062                 d->m_storage->modifyAccount(*it_a, true);
2063             } catch (const MyMoneyException &) {
2064                 rc << i18n("  * Unable to update account data in engine.");
2065                 return rc;
2066             }
2067         }
2068     }
2069 
2070     if (accountRebuild.count() != 0) {
2071         rc << i18n("* Reconstructing the child lists for");
2072     }
2073 
2074     // clear the affected lists
2075     for (it_a = list.begin(); it_a != list.end(); ++it_a) {
2076         if (accountRebuild.contains((*it_a).id())) {
2077             rc << QString("  %1").arg((*it_a).name());
2078             // clear the account list
2079             (*it_a).removeAccountIds();
2080         }
2081     }
2082 
2083     // reconstruct the lists
2084     for (it_a = list.begin(); it_a != list.end(); ++it_a) {
2085         QList<MyMoneyAccount>::Iterator it;
2086         parentId = (*it_a).parentAccountId();
2087         if (accountRebuild.contains(parentId)) {
2088             for (it = list.begin(); it != list.end(); ++it) {
2089                 if ((*it).id() == parentId) {
2090                     (*it).addAccountId((*it_a).id());
2091                     break;
2092                 }
2093             }
2094         }
2095     }
2096 
2097     // update the engine objects
2098     for (it_a = list.begin(); it_a != list.end(); ++it_a) {
2099         if (accountRebuild.contains((*it_a).id())) {
2100             try {
2101                 d->m_storage->modifyAccount(*it_a, true);
2102             } catch (const MyMoneyException &) {
2103                 rc << i18n("  * Unable to update account data for account %1 in engine", (*it_a).name());
2104             }
2105         }
2106     }
2107 
2108     // For some reason, files exist with invalid ids. This has been found in the payee id
2109     // so we fix them here
2110     QList<MyMoneyPayee> pList = payeeList();
2111     QMap<QString, QString>payeeConversionMap;
2112 
2113     for (it_p = pList.begin(); it_p != pList.end(); ++it_p) {
2114         if ((*it_p).id().length() > 7) {
2115             // found one of those with an invalid ids
2116             // create a new one and store it in the map.
2117             MyMoneyPayee payee = (*it_p);
2118             payee.clearId();
2119             d->m_storage->addPayee(payee);
2120             payeeConversionMap[(*it_p).id()] = payee.id();
2121             rc << i18n("  * Payee %1 recreated with fixed id", payee.name());
2122             ++problemCount;
2123         }
2124     }
2125 
2126     // Fix the transactions
2127     MyMoneyTransactionFilter filter;
2128     filter.setReportAllSplits(false);
2129     const auto tList = d->m_storage->transactionList(filter);
2130     // Generate the list of interest accounts
2131     for (const auto& transaction : tList) {
2132         const auto splits = transaction.splits();
2133         for (const auto& split : splits) {
2134             if (split.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::Interest))
2135                 interestAccounts[split.accountId()] = true;
2136         }
2137     }
2138     QSet<Account::Type> supportedAccountTypes;
2139     supportedAccountTypes << Account::Type::Checkings
2140                           << Account::Type::Savings
2141                           << Account::Type::Cash
2142                           << Account::Type::CreditCard
2143                           << Account::Type::Asset
2144                           << Account::Type::Liability;
2145     QSet<QString> reportedUnsupportedAccounts;
2146 
2147     for (const auto& transaction : tList) {
2148         MyMoneyTransaction t = transaction;
2149         bool tChanged = false;
2150         QDate accountOpeningDate;
2151         QStringList accountList;
2152         const auto splits = t.splits();
2153         foreach (const auto split, splits) {
2154             bool sChanged = false;
2155             MyMoneySplit s = split;
2156             if (payeeConversionMap.find(split.payeeId()) != payeeConversionMap.end()) {
2157                 s.setPayeeId(payeeConversionMap[s.payeeId()]);
2158                 sChanged = true;
2159                 rc << i18n("  * Payee id updated in split of transaction '%1'.", t.id());
2160                 ++problemCount;
2161             }
2162 
2163             try {
2164                 const auto acc = this->account(s.accountId());
2165                 // compute the newest opening date of all accounts involved in the transaction
2166                 // in case the newest opening date is newer than the transaction post date, do one
2167                 // of the following:
2168                 //
2169                 // a) for category and stock accounts: update the opening date of the account
2170                 // b) for account types where the user cannot modify the opening date through
2171                 //    the UI issue a warning (for each account only once)
2172                 // c) others will be caught later
2173                 if (!acc.isIncomeExpense() && !acc.isInvest()) {
2174                     if (acc.openingDate() > t.postDate()) {
2175                         if (!accountOpeningDate.isValid() || acc.openingDate() > accountOpeningDate) {
2176                             accountOpeningDate = acc.openingDate();
2177                         }
2178                         accountList << this->accountToCategory(acc.id());
2179                         if (!supportedAccountTypes.contains(acc.accountType())
2180                                 && !reportedUnsupportedAccounts.contains(acc.id())) {
2181                             rc << i18n("  * Opening date of Account '%1' cannot be changed to support transaction '%2' post date.",
2182                                        this->accountToCategory(acc.id()), t.id());
2183                             reportedUnsupportedAccounts << acc.id();
2184                             ++unfixedCount;
2185                         }
2186                     }
2187                 } else {
2188                     if (acc.openingDate() > t.postDate()) {
2189                         rc << i18n("  * Transaction '%1' post date '%2' is older than opening date '%4' of account '%3'.",
2190                                    t.id(), t.postDate().toString(Qt::ISODate), this->accountToCategory(acc.id()), acc.openingDate().toString(Qt::ISODate));
2191 
2192                         rc << i18n("    Account opening date updated.");
2193                         MyMoneyAccount newAcc = acc;
2194                         newAcc.setOpeningDate(t.postDate());
2195                         this->modifyAccount(newAcc);
2196                         ++problemCount;
2197                     }
2198                 }
2199 
2200                 // make sure, that shares and value have the same number if they
2201                 // represent the same currency.
2202                 if (t.commodity() == acc.currencyId() && s.shares().reduce() != s.value().reduce()) {
2203                     // use the value as master if the transaction is balanced
2204                     if (t.splitSum().isZero()) {
2205                         s.setShares(s.value());
2206                         rc << i18n("  * shares set to value in split of transaction '%1'.", t.id());
2207                     } else {
2208                         s.setValue(s.shares());
2209                         rc << i18n("  * value set to shares in split of transaction '%1'.", t.id());
2210                     }
2211                     sChanged = true;
2212                     ++problemCount;
2213                 }
2214             } catch (const MyMoneyException &) {
2215                 rc << i18n("  * Split %2 in transaction '%1' contains a reference to invalid account %3. Please fix manually.", t.id(), split.id(), split.accountId());
2216                 ++unfixedCount;
2217             }
2218 
2219             // make sure the interest splits are marked correct as such
2220             if (interestAccounts.find(s.accountId()) != interestAccounts.end()
2221                     && s.action() != MyMoneySplit::actionName(eMyMoney::Split::Action::Interest)) {
2222                 s.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::Interest));
2223                 sChanged = true;
2224                 rc << i18n("  * action marked as interest in split of transaction '%1'.", t.id());
2225                 ++problemCount;
2226             }
2227 
2228             if (sChanged) {
2229                 tChanged = true;
2230                 t.modifySplit(s);
2231             }
2232         }
2233 
2234         // make sure that the transaction's post date is valid
2235         if (!t.postDate().isValid()) {
2236             tChanged = true;
2237             t.setPostDate(t.entryDate().isValid() ? t.entryDate() : QDate::currentDate());
2238             rc << i18n("  * Transaction '%1' has an invalid post date.", t.id());
2239             rc << i18n("    The post date was updated to '%1'.", QLocale().toString(t.postDate(), QLocale::ShortFormat));
2240             ++problemCount;
2241         }
2242         // check if the transaction's post date is after the opening date
2243         // of all accounts involved in the transaction. In case it is not,
2244         // issue a warning with the details about the transaction incl.
2245         // the account names and dates involved
2246         if (accountOpeningDate.isValid() && t.postDate() < accountOpeningDate) {
2247             QDate originalPostDate = t.postDate();
2248 #if 0
2249             // for now we do not activate the logic to move the post date to a later
2250             // point in time. This could cause some severe trouble if you have lots
2251             // of ancient data collected with older versions of KMyMoney that did not
2252             // enforce certain conditions like we do now.
2253             t.setPostDate(accountOpeningDate);
2254             tChanged = true;
2255             // copy the price information for investments to the new date
2256             QList<MyMoneySplit>::const_iterator it_t;
2257             foreach (const auto split, t.splits()) {
2258                 if ((split.action() != "Buy") &&
2259                         (split.action() != "Reinvest")) {
2260                     continue;
2261                 }
2262                 QString id = split.accountId();
2263                 auto acc = this->account(id);
2264                 MyMoneySecurity sec = this->security(acc.currencyId());
2265                 MyMoneyPrice price(acc.currencyId(),
2266                                    sec.tradingCurrency(),
2267                                    t.postDate(),
2268                                    split.price(), "Transaction");
2269                 this->addPrice(price);
2270                 break;
2271             }
2272 #endif
2273             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));
2274             rc << i18n("    Referenced accounts: %1", accountList.join(","));
2275             rc << i18n("    The post date was not updated to '%1'.", QLocale().toString(accountOpeningDate, QLocale::ShortFormat));
2276             ++unfixedCount;
2277         }
2278 
2279         if (tChanged) {
2280             d->m_storage->modifyTransaction(t);
2281         }
2282     }
2283 
2284     // Fix the schedules
2285     QList<MyMoneySchedule> schList = scheduleList();
2286     for (it_sch = schList.begin(); it_sch != schList.end(); ++it_sch) {
2287         MyMoneySchedule sch = (*it_sch);
2288         MyMoneyTransaction t = sch.transaction();
2289         auto tChanged = false;
2290         foreach (const auto split, t.splits()) {
2291             MyMoneySplit s = split;
2292             bool sChanged = false;
2293             if (payeeConversionMap.find(split.payeeId()) != payeeConversionMap.end()) {
2294                 s.setPayeeId(payeeConversionMap[s.payeeId()]);
2295                 sChanged = true;
2296                 rc << i18n("  * Payee id updated in split of schedule '%1'.", (*it_sch).name());
2297                 ++problemCount;
2298             }
2299             if (!split.value().isZero() && split.shares().isZero()) {
2300                 s.setShares(s.value());
2301                 sChanged = true;
2302                 rc << i18n("  * Split in scheduled transaction '%1' contained value != 0 and shares == 0.", (*it_sch).name());
2303                 rc << i18n("    Shares set to value.");
2304                 ++problemCount;
2305             }
2306 
2307             // make sure, we don't have a bankid stored with a split in a schedule
2308             if (!split.bankID().isEmpty()) {
2309                 s.setBankID(QString());
2310                 sChanged = true;
2311                 rc << i18n("  * Removed bankid from split in scheduled transaction '%1'.", (*it_sch).name());
2312                 ++problemCount;
2313             }
2314 
2315             // make sure, that shares and value have the same number if they
2316             // represent the same currency.
2317             try {
2318                 const auto acc = this->account(s.accountId());
2319                 if (t.commodity() == acc.currencyId()
2320                         && s.shares().reduce() != s.value().reduce()) {
2321                     // use the value as master if the transaction is balanced
2322                     if (t.splitSum().isZero()) {
2323                         s.setShares(s.value());
2324                         rc << i18n("  * shares set to value in split in schedule '%1'.", (*it_sch).name());
2325                     } else {
2326                         s.setValue(s.shares());
2327                         rc << i18n("  * value set to shares in split in schedule '%1'.", (*it_sch).name());
2328                     }
2329                     sChanged = true;
2330                     ++problemCount;
2331                 }
2332             } catch (const MyMoneyException &) {
2333                 rc << i18n("  * Split %2 in schedule '%1' contains a reference to invalid account %3. Please fix manually.", (*it_sch).name(), split.id(), split.accountId());
2334                 ++unfixedCount;
2335             }
2336             if (sChanged) {
2337                 t.modifySplit(s);
2338                 tChanged = true;
2339             }
2340         }
2341         if (tChanged) {
2342             sch.setTransaction(t);
2343             d->m_storage->modifySchedule(sch);
2344         }
2345     }
2346 
2347     // Fix the reports
2348     QList<MyMoneyReport> rList = reportList();
2349     for (it_r = rList.begin(); it_r != rList.end(); ++it_r) {
2350         MyMoneyReport r = *it_r;
2351         QStringList payeeList;
2352         (*it_r).payees(payeeList);
2353         bool rChanged = false;
2354         for (auto it_payee = payeeList.begin(); it_payee != payeeList.end(); ++it_payee) {
2355             if (payeeConversionMap.find(*it_payee) != payeeConversionMap.end()) {
2356                 rc << i18n("  * Payee id updated in report '%1'.", (*it_r).name());
2357                 ++problemCount;
2358                 r.removeReference(*it_payee);
2359                 r.addPayee(payeeConversionMap[*it_payee]);
2360                 rChanged = true;
2361             }
2362         }
2363         if (rChanged) {
2364             d->m_storage->modifyReport(r);
2365         }
2366     }
2367 
2368     // erase old payee ids
2369     QMap<QString, QString>::Iterator it_m;
2370     for (it_m = payeeConversionMap.begin(); it_m != payeeConversionMap.end(); ++it_m) {
2371         MyMoneyPayee payee = this->payee(it_m.key());
2372         removePayee(payee);
2373         rc << i18n("  * Payee '%1' removed.", payee.id());
2374         ++problemCount;
2375     }
2376 
2377     //look for accounts which have currencies other than the base currency but no price on the opening date
2378     //all accounts using base currency are excluded, since that's the base used for foreign currency calculation
2379     //thus it is considered as always present
2380     //accounts that represent Income/Expense categories are also excluded as price is irrelevant for their
2381     //fake opening date since a forex rate is required for all multi-currency transactions
2382 
2383     //get all currencies in use
2384     QStringList currencyList;
2385     QList<MyMoneyAccount> accountForeignCurrency;
2386     QList<MyMoneyAccount> accList;
2387     accountList(accList);
2388     QList<MyMoneyAccount>::const_iterator account_it;
2389     for (account_it = accList.constBegin(); account_it != accList.constEnd(); ++account_it) {
2390         MyMoneyAccount account = *account_it;
2391         if (!account.isIncomeExpense()
2392                 && !currencyList.contains(account.currencyId())
2393                 && account.currencyId() != baseCurrency().id()
2394                 && !account.currencyId().isEmpty()) {
2395             //add the currency and the account-currency pair
2396             currencyList.append(account.currencyId());
2397             accountForeignCurrency.append(account);
2398         }
2399     }
2400 
2401     MyMoneyPriceList pricesList = priceList();
2402     QMap<MyMoneySecurityPair, QDate> securityPriceDate;
2403 
2404     //get the first date of the price for each security
2405     MyMoneyPriceList::const_iterator prices_it;
2406     for (prices_it = pricesList.constBegin(); prices_it != pricesList.constEnd(); ++prices_it) {
2407         MyMoneyPrice firstPrice = (*((*prices_it).constBegin()));
2408 
2409         //only check the price if the currency is in use
2410         if (currencyList.contains(firstPrice.from()) || currencyList.contains(firstPrice.to())) {
2411             //check the security in the from field
2412             //if it is there, check if it is older
2413             QPair<QString, QString> pricePair = qMakePair(firstPrice.from(), firstPrice.to());
2414             securityPriceDate[pricePair] = firstPrice.date();
2415         }
2416     }
2417 
2418     //compare the dates with the opening dates of the accounts using each currency
2419     QList<MyMoneyAccount>::const_iterator accForeignList_it;
2420     bool firstInvProblem = true;
2421     for (accForeignList_it = accountForeignCurrency.constBegin(); accForeignList_it != accountForeignCurrency.constEnd(); ++accForeignList_it) {
2422         //setup the price pair correctly
2423         QPair<QString, QString> pricePair;
2424         //setup the reverse, which can also be used for rate conversion
2425         QPair<QString, QString> reversePricePair;
2426         if ((*accForeignList_it).isInvest()) {
2427             //if it is a stock, we have to search for a price from its stock to the currency of the account
2428             QString securityId = (*accForeignList_it).currencyId();
2429             QString tradingCurrencyId = security(securityId).tradingCurrency();
2430             pricePair = qMakePair(securityId, tradingCurrencyId);
2431             reversePricePair = qMakePair(tradingCurrencyId, securityId);
2432         } else {
2433             //if it is a regular account we search for a price from the currency of the account to the base currency
2434             QString currency = (*accForeignList_it).currencyId();
2435             QString baseCurrencyId = baseCurrency().id();
2436             pricePair = qMakePair(currency, baseCurrencyId);
2437             reversePricePair = qMakePair(baseCurrencyId, currency);
2438         }
2439 
2440         //compare the first price with the opening date of the account
2441         if ((!securityPriceDate.contains(pricePair) || securityPriceDate.value(pricePair) > (*accForeignList_it).openingDate())
2442                 && (!securityPriceDate.contains(reversePricePair) || securityPriceDate.value(reversePricePair) > (*accForeignList_it).openingDate())) {
2443             if (firstInvProblem) {
2444                 firstInvProblem = false;
2445                 rc << i18n("* Potential problem with securities/currencies");
2446             }
2447             QDate openingDate = (*accForeignList_it).openingDate();
2448             MyMoneySecurity secError = security((*accForeignList_it).currencyId());
2449             if (!(*accForeignList_it).isInvest()) {
2450                 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));
2451                 rc << i18n("    Please enter a price for the currency on or before the opening date.");
2452             } else {
2453                 rc << i18n("  * The security '%1' has no price set for the opening date '%2'.", (*accForeignList_it).name(), openingDate.toString(Qt::ISODate));
2454                 rc << i18n("    Please enter a price for the security on or before the opening date.");
2455             }
2456             ++unfixedCount;
2457         }
2458     }
2459 
2460     // Fix the budgets that somehow still reference invalid accounts
2461     QString problemBudget;
2462     QList<MyMoneyBudget> bList = budgetList();
2463     for (QList<MyMoneyBudget>::const_iterator it_b = bList.constBegin(); it_b != bList.constEnd(); ++it_b) {
2464         MyMoneyBudget b = *it_b;
2465         QList<MyMoneyBudget::AccountGroup> baccounts = b.getaccounts();
2466         bool bChanged = false;
2467         for (QList<MyMoneyBudget::AccountGroup>::const_iterator it_bacc = baccounts.constBegin(); it_bacc != baccounts.constEnd(); ++it_bacc) {
2468             try {
2469                 account((*it_bacc).id());
2470             } catch (const MyMoneyException &) {
2471                 problemCount++;
2472                 if (problemBudget != b.name()) {
2473                     problemBudget = b.name();
2474                     rc << i18n("* Problem with budget '%1'", problemBudget);
2475                 }
2476                 rc << i18n("  * The account with id %1 referenced by the budget does not exist anymore.", (*it_bacc).id());
2477                 rc << i18n("    The account reference will be removed.");
2478                 // remove the reference to the account
2479                 b.removeReference((*it_bacc).id());
2480                 bChanged = true;
2481             }
2482         }
2483         if (bChanged) {
2484             d->m_storage->modifyBudget(b);
2485         }
2486     }
2487 
2488     // add more checks here
2489 
2490     if (problemCount == 0 && unfixedCount == 0) {
2491         rc << i18n("Finished: data is consistent.");
2492     } else {
2493         const QString problemsCorrected = i18np("%1 problem corrected.", "%1 problems corrected.", problemCount);
2494         const QString problemsRemaining = i18np("%1 problem still present.", "%1 problems still present.", unfixedCount);
2495 
2496         rc << QString();
2497         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);
2498     }
2499     return rc;
2500 }
2501 
2502 QString MyMoneyFile::createCategory(const MyMoneyAccount& base, const QString& name)
2503 {
2504     d->checkTransaction(Q_FUNC_INFO);
2505 
2506     MyMoneyAccount parent = base;
2507     QString categoryText;
2508 
2509     if (base.id() != expense().id() && base.id() != income().id())
2510         throw MYMONEYEXCEPTION_CSTRING("Invalid base category");
2511 
2512     QStringList subAccounts = name.split(AccountSeparator);
2513     QStringList::Iterator it;
2514     for (it = subAccounts.begin(); it != subAccounts.end(); ++it) {
2515         MyMoneyAccount categoryAccount;
2516 
2517         categoryAccount.setName(*it);
2518         categoryAccount.setAccountType(base.accountType());
2519 
2520         if (it == subAccounts.begin())
2521             categoryText += *it;
2522         else
2523             categoryText += (AccountSeparator + *it);
2524 
2525         // Only create the account if it doesn't exist
2526         try {
2527             QString categoryId = categoryToAccount(categoryText);
2528             if (categoryId.isEmpty())
2529                 addAccount(categoryAccount, parent);
2530             else {
2531                 categoryAccount = account(categoryId);
2532             }
2533         } catch (const MyMoneyException &e) {
2534             qDebug("Unable to add account %s, %s, %s: %s",
2535                    qPrintable(categoryAccount.name()),
2536                    qPrintable(parent.name()),
2537                    qPrintable(categoryText),
2538                    e.what());
2539         }
2540 
2541         parent = categoryAccount;
2542     }
2543 
2544     return categoryToAccount(name);
2545 }
2546 
2547 QString MyMoneyFile::checkCategory(const QString& name, const MyMoneyMoney& value, const MyMoneyMoney& value2)
2548 {
2549     QString accountId;
2550     MyMoneyAccount newAccount;
2551     bool found = true;
2552 
2553     if (!name.isEmpty()) {
2554         // The category might be constructed with an arbitrary depth (number of
2555         // colon delimited fields). We try to find a parent account within this
2556         // hierarchy by searching the following sequence:
2557         //
2558         //    aaaa:bbbb:cccc:ddddd
2559         //
2560         // 1. search aaaa:bbbb:cccc:dddd, create nothing
2561         // 2. search aaaa:bbbb:cccc     , create dddd
2562         // 3. search aaaa:bbbb          , create cccc:dddd
2563         // 4. search aaaa               , create bbbb:cccc:dddd
2564         // 5. don't search              , create aaaa:bbbb:cccc:dddd
2565 
2566         newAccount.setName(name);
2567         QString accName;      // part to be created (right side in above list)
2568         QString parent(name); // a possible parent part (left side in above list)
2569         do {
2570             accountId = categoryToAccount(parent);
2571             if (accountId.isEmpty()) {
2572                 found = false;
2573                 // prepare next step
2574                 if (!accName.isEmpty())
2575                     accName.prepend(':');
2576                 accName.prepend(parent.section(':', -1));
2577                 newAccount.setName(accName);
2578                 parent = parent.section(':', 0, -2);
2579             } else if (!accName.isEmpty()) {
2580                 newAccount.setParentAccountId(accountId);
2581             }
2582         } while (!parent.isEmpty() && accountId.isEmpty());
2583 
2584         // if we did not find the category,
2585         // but have a name for it, we create it
2586         if (!found && !accName.isEmpty()) {
2587             MyMoneyAccount parentAccount;
2588             if (newAccount.parentAccountId().isEmpty()) {
2589                 if (!value.isNegative() && value2.isNegative())
2590                     parentAccount = income();
2591                 else
2592                     parentAccount = expense();
2593             } else {
2594                 parentAccount = account(newAccount.parentAccountId());
2595             }
2596             newAccount.setAccountType((!value.isNegative() && value2.isNegative()) ? Account::Type::Income : Account::Type::Expense);
2597             MyMoneyAccount brokerage;
2598             // clear out the parent id, because createAccount() does not like that
2599             newAccount.setParentAccountId(QString());
2600             createAccount(newAccount, parentAccount, brokerage, MyMoneyMoney());
2601             accountId = newAccount.id();
2602         }
2603     }
2604 
2605     return accountId;
2606 }
2607 
2608 void MyMoneyFile::addSecurity(MyMoneySecurity& security)
2609 {
2610     d->checkTransaction(Q_FUNC_INFO);
2611 
2612     d->m_storage->addSecurity(security);
2613     d->m_changeSet += MyMoneyNotification(File::Mode::Add, security);
2614 }
2615 
2616 void MyMoneyFile::modifySecurity(const MyMoneySecurity& security)
2617 {
2618     d->checkTransaction(Q_FUNC_INFO);
2619 
2620     d->m_storage->modifySecurity(security);
2621     d->m_changeSet += MyMoneyNotification(File::Mode::Modify, security);
2622 }
2623 
2624 void MyMoneyFile::removeSecurity(const MyMoneySecurity& security)
2625 {
2626     d->checkTransaction(Q_FUNC_INFO);
2627 
2628     // FIXME check that security is not referenced by other object
2629 
2630     d->m_storage->removeSecurity(security);
2631     d->m_changeSet += MyMoneyNotification(File::Mode::Remove, security);
2632 }
2633 
2634 MyMoneySecurity MyMoneyFile::security(const QString& id) const
2635 {
2636     if (Q_UNLIKELY(id.isEmpty()))
2637         return baseCurrency();
2638 
2639     return d->m_storage->security(id);
2640 }
2641 
2642 QList<MyMoneySecurity> MyMoneyFile::securityList() const
2643 {
2644     d->checkStorage();
2645 
2646     return d->m_storage->securityList();
2647 }
2648 
2649 void MyMoneyFile::addCurrency(const MyMoneySecurity& currency)
2650 {
2651     d->checkTransaction(Q_FUNC_INFO);
2652 
2653     d->m_storage->addCurrency(currency);
2654     d->m_changeSet += MyMoneyNotification(File::Mode::Add, currency);
2655 }
2656 
2657 void MyMoneyFile::modifyCurrency(const MyMoneySecurity& currency)
2658 {
2659     d->checkTransaction(Q_FUNC_INFO);
2660 
2661     // force reload of base currency object
2662     if (currency.id() == d->m_baseCurrency.id())
2663         d->m_baseCurrency.clearId();
2664 
2665     d->m_storage->modifyCurrency(currency);
2666     d->m_changeSet += MyMoneyNotification(File::Mode::Modify, currency);
2667 }
2668 
2669 void MyMoneyFile::removeCurrency(const MyMoneySecurity& currency)
2670 {
2671     d->checkTransaction(Q_FUNC_INFO);
2672 
2673     if (currency.id() == d->m_baseCurrency.id())
2674         throw MYMONEYEXCEPTION_CSTRING("Cannot delete base currency.");
2675 
2676     // FIXME check that security is not referenced by other object
2677 
2678     d->m_storage->removeCurrency(currency);
2679     d->m_changeSet += MyMoneyNotification(File::Mode::Remove, currency);
2680 }
2681 
2682 MyMoneySecurity MyMoneyFile::currency(const QString& id) const
2683 {
2684     if (id.isEmpty())
2685         return baseCurrency();
2686 
2687     try {
2688         const auto currency = d->m_storage->currency(id);
2689         if (currency.id().isEmpty())
2690             throw MYMONEYEXCEPTION(QString::fromLatin1("Currency '%1' not found.").arg(id));
2691 
2692         return currency;
2693 
2694     } catch (const MyMoneyException &) {
2695         const auto security = d->m_storage->security(id);
2696         if (security.id().isEmpty()) {
2697             throw MYMONEYEXCEPTION(QString::fromLatin1("Security '%1' not found.").arg(id));
2698         }
2699         return security;
2700     }
2701 }
2702 
2703 QMap<MyMoneySecurity, MyMoneyPrice> MyMoneyFile::ancientCurrencies() const
2704 {
2705     QMap<MyMoneySecurity, MyMoneyPrice> ancientCurrencies;
2706 
2707     ancientCurrencies.insert(MyMoneySecurity("ATS", i18n("Austrian Schilling"), QString::fromUtf8("ÖS")),     MyMoneyPrice("ATS", "EUR", QDate(1998, 12, 31), MyMoneyMoney(10000, 137603), QLatin1Literal("KMyMoney")));
2708     ancientCurrencies.insert(MyMoneySecurity("DEM", i18n("German Mark"), "DM"),            MyMoneyPrice("ATS", "EUR", QDate(1998, 12, 31), MyMoneyMoney(100000, 195583), QLatin1Literal("KMyMoney")));
2709     ancientCurrencies.insert(MyMoneySecurity("FRF", i18n("French Franc"), "FF"),           MyMoneyPrice("FRF", "EUR", QDate(1998, 12, 31), MyMoneyMoney(100000, 655957), QLatin1Literal("KMyMoney")));
2710     ancientCurrencies.insert(MyMoneySecurity("ITL", i18n("Italian Lira"), QChar(0x20A4)),  MyMoneyPrice("ITL", "EUR", QDate(1998, 12, 31), MyMoneyMoney(100, 193627), QLatin1Literal("KMyMoney")));
2711     ancientCurrencies.insert(MyMoneySecurity("ESP", i18n("Spanish Peseta"), QString()),    MyMoneyPrice("ESP", "EUR", QDate(1998, 12, 31), MyMoneyMoney(1000, 166386), QLatin1Literal("KMyMoney")));
2712     ancientCurrencies.insert(MyMoneySecurity("NLG", i18n("Dutch Guilder"), QString()),     MyMoneyPrice("NLG", "EUR", QDate(1998, 12, 31), MyMoneyMoney(100000, 220371), QLatin1Literal("KMyMoney")));
2713     ancientCurrencies.insert(MyMoneySecurity("BEF", i18n("Belgian Franc"), "Fr"),          MyMoneyPrice("BEF", "EUR", QDate(1998, 12, 31), MyMoneyMoney(10000, 403399), QLatin1Literal("KMyMoney")));
2714     ancientCurrencies.insert(MyMoneySecurity("LUF", i18n("Luxembourg Franc"), "Fr"),       MyMoneyPrice("LUF", "EUR", QDate(1998, 12, 31), MyMoneyMoney(10000, 403399), QLatin1Literal("KMyMoney")));
2715     ancientCurrencies.insert(MyMoneySecurity("PTE", i18n("Portuguese Escudo"), QString()), MyMoneyPrice("PTE", "EUR", QDate(1998, 12, 31), MyMoneyMoney(1000, 200482), QLatin1Literal("KMyMoney")));
2716     ancientCurrencies.insert(MyMoneySecurity("IEP", i18n("Irish Pound"), QChar(0x00A3)),   MyMoneyPrice("IEP", "EUR", QDate(1998, 12, 31), MyMoneyMoney(1000000, 787564), QLatin1Literal("KMyMoney")));
2717     ancientCurrencies.insert(MyMoneySecurity("FIM", i18n("Finnish Markka"), QString()),    MyMoneyPrice("FIM", "EUR", QDate(1998, 12, 31), MyMoneyMoney(100000, 594573), QLatin1Literal("KMyMoney")));
2718     ancientCurrencies.insert(MyMoneySecurity("GRD", i18n("Greek Drachma"), QChar(0x20AF)), MyMoneyPrice("GRD", "EUR", QDate(1998, 12, 31), MyMoneyMoney(100, 34075), QLatin1Literal("KMyMoney")));
2719 
2720     // https://en.wikipedia.org/wiki/Bulgarian_lev
2721     ancientCurrencies.insert(MyMoneySecurity("BGL", i18n("Bulgarian Lev"), "BGL"), MyMoneyPrice("BGL", "BGN", QDate(1999, 7, 5), MyMoneyMoney(1, 1000), QLatin1Literal("KMyMoney")));
2722 
2723     ancientCurrencies.insert(MyMoneySecurity("ROL", i18n("Romanian Leu"), "ROL"), MyMoneyPrice("ROL", "RON", QDate(2005, 6, 30), MyMoneyMoney(1, 10000), QLatin1Literal("KMyMoney")));
2724 
2725     ancientCurrencies.insert(MyMoneySecurity("RUR", i18n("Russian Ruble (old)"), "RUR"), MyMoneyPrice("RUR", "RUB", QDate(1998, 1, 1), MyMoneyMoney(1, 1000), QLatin1Literal("KMyMoney")));
2726 
2727     ancientCurrencies.insert(MyMoneySecurity("SIT", i18n("Slovenian Tolar"), "SIT"), MyMoneyPrice("SIT", "EUR", QDate(2006, 12, 31), MyMoneyMoney(1, 23964), QLatin1Literal("KMyMoney")));
2728 
2729     // Source: https://en.wikipedia.org/wiki/Turkish_lira
2730     ancientCurrencies.insert(MyMoneySecurity("TRL", i18n("Turkish Lira (old)"), "TL"), MyMoneyPrice("TRL", "TRY", QDate(2004, 12, 31), MyMoneyMoney(1, 1000000), QLatin1Literal("KMyMoney")));
2731 
2732     // Source: https://www.focus.de/finanzen/news/malta-und-zypern_aid_66058.html
2733     ancientCurrencies.insert(MyMoneySecurity("MTL", i18n("Maltese Lira"), "MTL"), MyMoneyPrice("MTL", "EUR", QDate(2008, 1, 1), MyMoneyMoney(429300, 1000000), QLatin1Literal("KMyMoney")));
2734     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")));
2735 
2736     // Source: https://www.focus.de/finanzen/news/waehrungszone-slowakei-ist-neuer-euro-staat_aid_359025.html
2737     ancientCurrencies.insert(MyMoneySecurity("SKK", i18n("Slovak Koruna"), "SKK"), MyMoneyPrice("SKK", "EUR", QDate(2008, 12, 31), MyMoneyMoney(1000, 30126), QLatin1Literal("KMyMoney")));
2738 
2739     // Source: https://en.wikipedia.org/wiki/Mozambican_metical
2740     ancientCurrencies.insert(MyMoneySecurity("MZM", i18n("Mozambique Metical"), "MT"), MyMoneyPrice("MZM", "MZN", QDate(2006, 7, 1), MyMoneyMoney(1, 1000), QLatin1Literal("KMyMoney")));
2741 
2742     // Source https://en.wikipedia.org/wiki/Azerbaijani_manat
2743     ancientCurrencies.insert(MyMoneySecurity("AZM", i18n("Azerbaijani Manat"), "m."), MyMoneyPrice("AZM", "AZN", QDate(2006, 1, 1), MyMoneyMoney(1, 5000), QLatin1Literal("KMyMoney")));
2744 
2745     // Source: https://en.wikipedia.org/wiki/Litas
2746     ancientCurrencies.insert(MyMoneySecurity("LTL", i18n("Lithuanian Litas"), "Lt"), MyMoneyPrice("LTL", "EUR", QDate(2015, 1, 1), MyMoneyMoney(100000, 345280), QLatin1Literal("KMyMoney")));
2747 
2748     // Source: https://en.wikipedia.org/wiki/Belarusian_ruble
2749     ancientCurrencies.insert(MyMoneySecurity("BYR", i18n("Belarusian Ruble (old)"), "BYR"), MyMoneyPrice("BYR", "BYN", QDate(2016, 7, 1), MyMoneyMoney(1, 10000), QLatin1Literal("KMyMoney")));
2750 
2751     // Source: https://en.wikipedia.org/wiki/Zambian_kwacha, triggered by b.k.o ticket #425530
2752     ancientCurrencies.insert(MyMoneySecurity("ZMK", i18n("Zambian Kwacha (old)"), "K"), MyMoneyPrice("ZMK", "ZMW", QDate(2013, 1, 1), MyMoneyMoney(1, 1000), QLatin1Literal("KMyMoney")));
2753 
2754     // Source: https://www.ecb.europa.eu/press/pr/date/2022/html/ecb.pr220712~b97dd38de3.en.html
2755     ancientCurrencies.insert(MyMoneySecurity("HRK", i18n("Croatian Kuna")),
2756                              MyMoneyPrice("HRK", "EUR", QDate(2023, 1, 1), MyMoneyMoney(100000, 753450), QLatin1Literal("KMyMoney")));
2757 
2758     return ancientCurrencies;
2759 }
2760 
2761 QList<MyMoneySecurity> MyMoneyFile::availableCurrencyList() const
2762 {
2763     QList<MyMoneySecurity> currencyList;
2764     currencyList.append(MyMoneySecurity("AFA", i18n("Afghanistan Afghani")));
2765     currencyList.append(MyMoneySecurity("ALL", i18n("Albanian Lek")));
2766     currencyList.append(MyMoneySecurity("ANG", i18n("Netherland Antillian Guilder")));
2767     currencyList.append(MyMoneySecurity("DZD", i18n("Algerian Dinar")));
2768     currencyList.append(MyMoneySecurity("ADF", i18n("Andorran Franc")));
2769     currencyList.append(MyMoneySecurity("ADP", i18n("Andorran Peseta")));
2770     currencyList.append(MyMoneySecurity("AOA", i18n("Angolan Kwanza"),         "Kz"));
2771     currencyList.append(MyMoneySecurity("ARS", i18n("Argentine Peso"),         "$"));
2772     currencyList.append(MyMoneySecurity("AWG", i18n("Aruban Florin")));
2773     currencyList.append(MyMoneySecurity("AUD", i18n("Australian Dollar"),      "$"));
2774     currencyList.append(MyMoneySecurity("AZN", i18n("Azerbaijani Manat"),      "m."));
2775     currencyList.append(MyMoneySecurity("BSD", i18n("Bahamian Dollar"),        "$"));
2776     currencyList.append(MyMoneySecurity("BHD", i18n("Bahraini Dinar"),         "BHD", 1000));
2777     currencyList.append(MyMoneySecurity("BDT", i18n("Bangladeshi Taka")));
2778     currencyList.append(MyMoneySecurity("BBD", i18n("Barbados Dollar"),        "$"));
2779     currencyList.append(MyMoneySecurity("BTC", i18n("Bitcoin"),                "BTC", 100000000, 100000000));
2780     currencyList.append(MyMoneySecurity("BYN", i18n("Belarusian Ruble"),       "Br"));
2781     currencyList.append(MyMoneySecurity("BZD", i18n("Belize Dollar"),          "$"));
2782     currencyList.append(MyMoneySecurity("BMD", i18n("Bermudian Dollar"),       "$"));
2783     currencyList.append(MyMoneySecurity("BTN", i18n("Bhutan Ngultrum")));
2784     currencyList.append(MyMoneySecurity("BOB", i18n("Bolivian Boliviano")));
2785     currencyList.append(MyMoneySecurity("BAM", i18n("Bosnian Convertible Mark")));
2786     currencyList.append(MyMoneySecurity("BWP", i18n("Botswana Pula")));
2787     currencyList.append(MyMoneySecurity("BRL", i18n("Brazilian Real"),         "R$"));
2788     currencyList.append(MyMoneySecurity("GBP", i18n("British Pound"),          QChar(0x00A3)));
2789     currencyList.append(MyMoneySecurity("BND", i18n("Brunei Dollar"),          "$"));
2790     currencyList.append(MyMoneySecurity("BGN", i18n("Bulgarian Lev (new)")));
2791     currencyList.append(MyMoneySecurity("BIF", i18n("Burundi Franc")));
2792     currencyList.append(MyMoneySecurity("XAF", i18n("CFA Franc BEAC")));
2793     currencyList.append(MyMoneySecurity("XOF", i18n("CFA Franc BCEAO")));
2794     currencyList.append(MyMoneySecurity("XPF", i18n("CFP Franc Pacifique"), "F", 1, 100));
2795     currencyList.append(MyMoneySecurity("KHR", i18n("Cambodia Riel")));
2796     currencyList.append(MyMoneySecurity("CAD", i18n("Canadian Dollar"),        "$"));
2797     currencyList.append(MyMoneySecurity("CVE", i18n("Cape Verde Escudo")));
2798     currencyList.append(MyMoneySecurity("KYD", i18n("Cayman Islands Dollar"),  "$"));
2799     currencyList.append(MyMoneySecurity("CLP", i18n("Chilean Peso")));
2800     currencyList.append(MyMoneySecurity("CNY", i18n("Chinese Yuan Renminbi")));
2801     currencyList.append(MyMoneySecurity("COP", i18n("Colombian Peso")));
2802     currencyList.append(MyMoneySecurity("KMF", i18n("Comoros Franc")));
2803     currencyList.append(MyMoneySecurity("CRC", i18n("Costa Rican Colon"),      QChar(0x20A1)));
2804     currencyList.append(MyMoneySecurity("CUP", i18n("Cuban Peso")));
2805     currencyList.append(MyMoneySecurity("CUC", i18n("Cuban Convertible Peso")));
2806     currencyList.append(MyMoneySecurity("CZK", i18n("Czech Koruna")));
2807     currencyList.append(MyMoneySecurity("DKK", i18n("Danish Krone"),           "kr"));
2808     currencyList.append(MyMoneySecurity("DJF", i18n("Djibouti Franc")));
2809     currencyList.append(MyMoneySecurity("DOP", i18n("Dominican Peso")));
2810     currencyList.append(MyMoneySecurity("XCD", i18n("East Caribbean Dollar"),  "$"));
2811     currencyList.append(MyMoneySecurity("EGP", i18n("Egyptian Pound"),         QChar(0x00A3)));
2812     currencyList.append(MyMoneySecurity("SVC", i18n("El Salvador Colon")));
2813     currencyList.append(MyMoneySecurity("ERN", i18n("Eritrean Nakfa")));
2814     currencyList.append(MyMoneySecurity("EEK", i18n("Estonian Kroon")));
2815     currencyList.append(MyMoneySecurity("ETB", i18n("Ethiopian Birr")));
2816     currencyList.append(MyMoneySecurity("EUR", i18n("Euro"),                   QChar(0x20ac)));
2817     currencyList.append(MyMoneySecurity("FKP", i18n("Falkland Islands Pound"), QChar(0x00A3)));
2818     currencyList.append(MyMoneySecurity("FJD", i18n("Fiji Dollar"),            "$"));
2819     currencyList.append(MyMoneySecurity("GMD", i18n("Gambian Dalasi")));
2820     currencyList.append(MyMoneySecurity("GEL", i18n("Georgian Lari")));
2821     currencyList.append(MyMoneySecurity("GHC", i18n("Ghanaian Cedi")));
2822     currencyList.append(MyMoneySecurity("GIP", i18n("Gibraltar Pound"),        QChar(0x00A3)));
2823     currencyList.append(MyMoneySecurity("GTQ", i18n("Guatemalan Quetzal")));
2824     currencyList.append(MyMoneySecurity("GWP", i18n("Guinea-Bissau Peso")));
2825     currencyList.append(MyMoneySecurity("GYD", i18n("Guyanan Dollar"),         "$"));
2826     currencyList.append(MyMoneySecurity("HTG", i18n("Haitian Gourde")));
2827     currencyList.append(MyMoneySecurity("HNL", i18n("Honduran Lempira")));
2828     currencyList.append(MyMoneySecurity("HKD", i18n("Hong Kong Dollar"),       "$"));
2829     currencyList.append(MyMoneySecurity("HUF", i18n("Hungarian Forint"),       "HUF", 1, 100));
2830     currencyList.append(MyMoneySecurity("ISK", i18n("Iceland Krona")));
2831     currencyList.append(MyMoneySecurity("INR", i18n("Indian Rupee"),           QChar(0x20B9)));
2832     currencyList.append(MyMoneySecurity("IDR", i18n("Indonesian Rupiah"),      "IDR", 1, 0, 10));
2833     currencyList.append(MyMoneySecurity("IRR", i18n("Iranian Rial"),           "IRR", 1));
2834     currencyList.append(MyMoneySecurity("IQD", i18n("Iraqi Dinar"),            "IQD", 1000));
2835     currencyList.append(MyMoneySecurity("ILS", i18n("Israeli New Shekel"),     QChar(0x20AA)));
2836     currencyList.append(MyMoneySecurity("JMD", i18n("Jamaican Dollar"),        "$"));
2837     currencyList.append(MyMoneySecurity("JPY", i18n("Japanese Yen"),           QChar(0x00A5), 1));
2838     currencyList.append(MyMoneySecurity("JOD", i18n("Jordanian Dinar"),        "JOD", 1000));
2839     currencyList.append(MyMoneySecurity("KZT", i18n("Kazakhstan Tenge")));
2840     currencyList.append(MyMoneySecurity("KES", i18n("Kenyan Shilling")));
2841     currencyList.append(MyMoneySecurity("KWD", i18n("Kuwaiti Dinar"),          "KWD", 1000));
2842     currencyList.append(MyMoneySecurity("KGS", i18n("Kyrgyzstan Som")));
2843     currencyList.append(MyMoneySecurity("LAK", i18n("Laos Kip"),               QChar(0x20AD)));
2844     currencyList.append(MyMoneySecurity("LVL", i18n("Latvian Lats")));
2845     currencyList.append(MyMoneySecurity("LBP", i18n("Lebanese Pound"),         QChar(0x00A3)));
2846     currencyList.append(MyMoneySecurity("LSL", i18n("Lesotho Loti")));
2847     currencyList.append(MyMoneySecurity("LRD", i18n("Liberian Dollar"),        "$"));
2848     currencyList.append(MyMoneySecurity("LYD", i18n("Libyan Dinar"),           "LYD", 1000));
2849     currencyList.append(MyMoneySecurity("MOP", i18n("Macau Pataca")));
2850     currencyList.append(MyMoneySecurity("MKD", i18n("Macedonian Denar")));
2851     currencyList.append(MyMoneySecurity("MGF", i18n("Malagasy Franc"),         "MGF", 500));
2852     currencyList.append(MyMoneySecurity("MWK", i18n("Malawi Kwacha")));
2853     currencyList.append(MyMoneySecurity("MYR", i18n("Malaysian Ringgit")));
2854     currencyList.append(MyMoneySecurity("MVR", i18n("Maldive Rufiyaa")));
2855     currencyList.append(MyMoneySecurity("MLF", i18n("Mali Republic Franc")));
2856     currencyList.append(MyMoneySecurity("MRO", i18n("Mauritanian Ouguiya"),    "MRO", 5));
2857     currencyList.append(MyMoneySecurity("MUR", i18n("Mauritius Rupee")));
2858     currencyList.append(MyMoneySecurity("MXN", i18n("Mexican Peso"),           "$"));
2859     currencyList.append(MyMoneySecurity("MDL", i18n("Moldavian Leu")));
2860     currencyList.append(MyMoneySecurity("MNT", i18n("Mongolian Tugrik"),       QChar(0x20AE)));
2861     currencyList.append(MyMoneySecurity("MAD", i18n("Moroccan Dirham")));
2862     currencyList.append(MyMoneySecurity("MZN", i18n("Mozambique Metical"),     "MT"));
2863     currencyList.append(MyMoneySecurity("MMK", i18n("Myanmar Kyat")));
2864     currencyList.append(MyMoneySecurity("NAD", i18n("Namibian Dollar"),        "$"));
2865     currencyList.append(MyMoneySecurity("NPR", i18n("Nepalese Rupee")));
2866     currencyList.append(MyMoneySecurity("NZD", i18n("New Zealand Dollar"),     "$"));
2867     currencyList.append(MyMoneySecurity("NIC", i18n("Nicaraguan Cordoba Oro")));
2868     currencyList.append(MyMoneySecurity("NGN", i18n("Nigerian Naira"),         QChar(0x20A6)));
2869     currencyList.append(MyMoneySecurity("KPW", i18n("North Korean Won"),       QChar(0x20A9)));
2870     currencyList.append(MyMoneySecurity("NOK", i18n("Norwegian Kroner"),       "kr"));
2871     currencyList.append(MyMoneySecurity("OMR", i18n("Omani Rial"),             "OMR", 1000));
2872     currencyList.append(MyMoneySecurity("PKR", i18n("Pakistan Rupee")));
2873     currencyList.append(MyMoneySecurity("PAB", i18n("Panamanian Balboa")));
2874     currencyList.append(MyMoneySecurity("PGK", i18n("Papua New Guinea Kina")));
2875     currencyList.append(MyMoneySecurity("PYG", i18n("Paraguay Guarani")));
2876     currencyList.append(MyMoneySecurity("PEN", i18n("Peruvian Nuevo Sol")));
2877     currencyList.append(MyMoneySecurity("PHP", i18n("Philippine Peso"),        QChar(0x20B1)));
2878     currencyList.append(MyMoneySecurity("PLN", i18n("Polish Zloty")));
2879     currencyList.append(MyMoneySecurity("QAR", i18n("Qatari Rial")));
2880     currencyList.append(MyMoneySecurity("RON", i18n("Romanian Leu (new)")));
2881     currencyList.append(MyMoneySecurity("RUB", i18n("Russian Ruble")));
2882     currencyList.append(MyMoneySecurity("RWF", i18n("Rwanda Franc")));
2883     currencyList.append(MyMoneySecurity("WST", i18n("Samoan Tala")));
2884     currencyList.append(MyMoneySecurity("STD", i18n("Sao Tome and Principe Dobra")));
2885     currencyList.append(MyMoneySecurity("SAR", i18n("Saudi Riyal")));
2886     currencyList.append(MyMoneySecurity("RSD", i18n("Serbian Dinar")));
2887     currencyList.append(MyMoneySecurity("SCR", i18n("Seychelles Rupee")));
2888     currencyList.append(MyMoneySecurity("SLL", i18n("Sierra Leone Leone")));
2889     currencyList.append(MyMoneySecurity("SGD", i18n("Singapore Dollar"),       "$"));
2890     currencyList.append(MyMoneySecurity("SBD", i18n("Solomon Islands Dollar"), "$"));
2891     currencyList.append(MyMoneySecurity("SOS", i18n("Somali Shilling")));
2892     currencyList.append(MyMoneySecurity("ZAR", i18n("South African Rand")));
2893     currencyList.append(MyMoneySecurity("KRW", i18n("South Korean Won"),       QChar(0x20A9), 1));
2894     currencyList.append(MyMoneySecurity("LKR", i18n("Sri Lanka Rupee")));
2895     currencyList.append(MyMoneySecurity("SHP", i18n("St. Helena Pound"),       QChar(0x00A3)));
2896     currencyList.append(MyMoneySecurity("SDD", i18n("Sudanese Dinar")));
2897     currencyList.append(MyMoneySecurity("SRG", i18n("Suriname Guilder")));
2898     currencyList.append(MyMoneySecurity("SZL", i18n("Swaziland Lilangeni")));
2899     currencyList.append(MyMoneySecurity("SEK", i18n("Swedish Krona")));
2900     currencyList.append(MyMoneySecurity("CHF", i18n("Swiss Franc"),            "SFr"));
2901     currencyList.append(MyMoneySecurity("SYP", i18n("Syrian Pound"),           QChar(0x00A3)));
2902     currencyList.append(MyMoneySecurity("TWD", i18n("Taiwan Dollar"),          "$"));
2903     currencyList.append(MyMoneySecurity("TJS", i18n("Tajikistan Somoni")));
2904     currencyList.append(MyMoneySecurity("TZS", i18n("Tanzanian Shilling")));
2905     currencyList.append(MyMoneySecurity("THB", i18n("Thai Baht"),              QChar(0x0E3F)));
2906     currencyList.append(MyMoneySecurity("TOP", i18n("Tongan Pa'anga")));
2907     currencyList.append(MyMoneySecurity("TTD", i18n("Trinidad and Tobago Dollar"), "$"));
2908     currencyList.append(MyMoneySecurity("TND", i18n("Tunisian Dinar"),         "TND", 1000));
2909     currencyList.append(MyMoneySecurity("TRY", i18n("Turkish Lira"), QChar(0x20BA)));
2910     currencyList.append(MyMoneySecurity("TMM", i18n("Turkmenistan Manat")));
2911     currencyList.append(MyMoneySecurity("USD", i18n("US Dollar"),              "$"));
2912     currencyList.append(MyMoneySecurity("UGX", i18n("Uganda Shilling")));
2913     currencyList.append(MyMoneySecurity("UAH", i18n("Ukraine Hryvnia")));
2914     currencyList.append(MyMoneySecurity("CLF", i18n("Unidad de Fometo")));
2915     currencyList.append(MyMoneySecurity("AED", i18n("United Arab Emirates Dirham")));
2916     currencyList.append(MyMoneySecurity("UYU", i18n("Uruguayan Peso")));
2917     currencyList.append(MyMoneySecurity("UZS", i18n("Uzbekistani Sum")));
2918     currencyList.append(MyMoneySecurity("VUV", i18n("Vanuatu Vatu")));
2919     currencyList.append(MyMoneySecurity("VEB", i18n("Venezuelan Bolivar")));
2920     currencyList.append(MyMoneySecurity("VND", i18n("Vietnamese Dong"),        QChar(0x20AB)));
2921     currencyList.append(MyMoneySecurity("ZMW", i18n("Zambian Kwacha"),         "K"));
2922     currencyList.append(MyMoneySecurity("ZWD", i18n("Zimbabwe Dollar"),        "$"));
2923 
2924     currencyList.append(ancientCurrencies().keys());
2925 
2926     // sort the currencies ...
2927     qSort(currencyList.begin(), currencyList.end(),
2928           [] (const MyMoneySecurity& c1, const MyMoneySecurity& c2)
2929     {
2930         return c1.name().compare(c2.name()) < 0;
2931     });
2932 
2933     // ... and add a few precious metals at the ned
2934     currencyList.append(MyMoneySecurity("XAU", i18n("Gold"),       "XAU", 1000000));
2935     currencyList.append(MyMoneySecurity("XPD", i18n("Palladium"),  "XPD", 1000000));
2936     currencyList.append(MyMoneySecurity("XPT", i18n("Platinum"),   "XPT", 1000000));
2937     currencyList.append(MyMoneySecurity("XAG", i18n("Silver"),     "XAG", 1000000));
2938 
2939     return currencyList;
2940 }
2941 
2942 QList<MyMoneySecurity> MyMoneyFile::currencyList() const
2943 {
2944     d->checkStorage();
2945 
2946     return d->m_storage->currencyList();
2947 }
2948 
2949 QString MyMoneyFile::foreignCurrency(const QString& first, const QString& second) const
2950 {
2951     if (baseCurrency().id() == second)
2952         return first;
2953     return second;
2954 }
2955 
2956 MyMoneySecurity MyMoneyFile::baseCurrency() const
2957 {
2958     if (d->m_baseCurrency.id().isEmpty()) {
2959         QString id = QString(value("kmm-baseCurrency"));
2960         if (!id.isEmpty())
2961             d->m_baseCurrency = currency(id);
2962     }
2963 
2964     return d->m_baseCurrency;
2965 }
2966 
2967 void MyMoneyFile::setBaseCurrency(const MyMoneySecurity& curr)
2968 {
2969     // make sure the currency exists
2970     MyMoneySecurity c = currency(curr.id());
2971 
2972     if (c.id() != d->m_baseCurrency.id()) {
2973         setValue("kmm-baseCurrency", curr.id());
2974         // force reload of base currency cache
2975         d->m_baseCurrency = MyMoneySecurity();
2976         emit dataChanged();
2977     }
2978 }
2979 
2980 void MyMoneyFile::addPrice(const MyMoneyPrice& price)
2981 {
2982     if (price.rate(QString()).isZero())
2983         return;
2984 
2985     d->checkTransaction(Q_FUNC_INFO);
2986 
2987     auto adjustedPrice(price);
2988     const auto fromSecurity = security(price.from());
2989     const auto toSecurity = security(price.to());
2990     // if fromSecurity is a currency and toSecurity is not a currency
2991     // we swap both of them so that it is a security -> currency
2992     // conversion
2993     if (fromSecurity.isCurrency() && !toSecurity.isCurrency()) {
2994         adjustedPrice = MyMoneyPrice(price.to(),
2995                                      price.from(),
2996                                      price.date(),
2997                                      price.rate(price.from()),
2998                                      price.source());
2999     }
3000     // store the account's which are affected by this price regarding their value
3001     d->priceChanged(*this, adjustedPrice);
3002     d->m_storage->addPrice(adjustedPrice);
3003 }
3004 
3005 void MyMoneyFile::removePrice(const MyMoneyPrice& price)
3006 {
3007     d->checkTransaction(Q_FUNC_INFO);
3008 
3009     // store the account's which are affected by this price regarding their value
3010     d->priceChanged(*this, price);
3011     d->m_storage->removePrice(price);
3012 }
3013 
3014 MyMoneyPrice MyMoneyFile::price(const QString& fromId, const QString& toId, const QDate& date, const bool exactDate) const
3015 {
3016     d->checkStorage();
3017 
3018     QString to(toId);
3019     if (to.isEmpty())
3020         to = value("kmm-baseCurrency");
3021     // if some id is missing, we can return an empty price object
3022     if (fromId.isEmpty() || to.isEmpty())
3023         return MyMoneyPrice();
3024 
3025     // we don't search our tables if someone asks stupid stuff
3026     if (fromId == toId) {
3027         return MyMoneyPrice(fromId, toId, date, MyMoneyMoney::ONE, "KMyMoney");
3028     }
3029 
3030     // if not asking for exact date, try to find the exact date match first,
3031     // either the requested price or its reciprocal value. If unsuccessful, it will move
3032     // on and look for prices of previous dates
3033     MyMoneyPrice rc = d->m_storage->price(fromId, to, date, true);
3034     if (!rc.isValid()) {
3035         // not found, search 'to-from' rate and use reciprocal value
3036         rc = d->m_storage->price(to, fromId, date, true);
3037 
3038         // not found, search previous dates, if exact date is not needed
3039         if (!exactDate && !rc.isValid()) {
3040             // search 'from-to' and 'to-from', select the most recent one
3041             MyMoneyPrice fromPrice = d->m_storage->price(fromId, to, date, exactDate);
3042             MyMoneyPrice toPrice = d->m_storage->price(to, fromId, date, exactDate);
3043 
3044             // check first whether both prices are valid
3045             if (fromPrice.isValid() && toPrice.isValid()) {
3046                 if (fromPrice.date() >= toPrice.date()) {
3047                     // if 'from-to' is newer or the same date, prefer that one
3048                     rc = fromPrice;
3049                 } else {
3050                     // otherwise, use the reciprocal price
3051                     rc = toPrice;
3052                 }
3053             } else if (fromPrice.isValid()) { // check if any of the prices is valid, return that one
3054                 rc = fromPrice;
3055             } else if (toPrice.isValid()) {
3056                 rc = toPrice;
3057             }
3058         }
3059     }
3060     return rc;
3061 }
3062 
3063 MyMoneyPrice MyMoneyFile::price(const QString& fromId, const QString& toId) const
3064 {
3065     return price(fromId, toId, QDate::currentDate(), false);
3066 }
3067 
3068 MyMoneyPrice MyMoneyFile::price(const QString& fromId) const
3069 {
3070     return price(fromId, QString(), QDate::currentDate(), false);
3071 }
3072 
3073 MyMoneyPriceList MyMoneyFile::priceList() const
3074 {
3075     d->checkStorage();
3076 
3077     return d->m_storage->priceList();
3078 }
3079 
3080 bool MyMoneyFile::hasAccount(const QString& id, const QString& name) const
3081 {
3082     const auto accounts = account(id).accountList();
3083     for (const auto& acc : accounts) {
3084         if (account(acc).name().compare(name) == 0)
3085             return true;
3086     }
3087     return false;
3088 }
3089 
3090 QList<MyMoneyReport> MyMoneyFile::reportList() const
3091 {
3092     d->checkStorage();
3093 
3094     return d->m_storage->reportList();
3095 }
3096 
3097 void MyMoneyFile::addReport(MyMoneyReport& report)
3098 {
3099     d->checkTransaction(Q_FUNC_INFO);
3100 
3101     d->m_storage->addReport(report);
3102 }
3103 
3104 void MyMoneyFile::modifyReport(const MyMoneyReport& report)
3105 {
3106     d->checkTransaction(Q_FUNC_INFO);
3107 
3108     d->m_storage->modifyReport(report);
3109 }
3110 
3111 unsigned MyMoneyFile::countReports() const
3112 {
3113     d->checkStorage();
3114 
3115     return d->m_storage->countReports();
3116 }
3117 
3118 MyMoneyReport MyMoneyFile::report(const QString& id) const
3119 {
3120     d->checkStorage();
3121 
3122     return d->m_storage->report(id);
3123 }
3124 
3125 void MyMoneyFile::removeReport(const MyMoneyReport& report)
3126 {
3127     d->checkTransaction(Q_FUNC_INFO);
3128 
3129     d->m_storage->removeReport(report);
3130 }
3131 
3132 
3133 QList<MyMoneyBudget> MyMoneyFile::budgetList() const
3134 {
3135     d->checkStorage();
3136 
3137     return d->m_storage->budgetList();
3138 }
3139 
3140 void MyMoneyFile::addBudget(MyMoneyBudget &budget)
3141 {
3142     d->checkTransaction(Q_FUNC_INFO);
3143 
3144     d->m_storage->addBudget(budget);
3145 }
3146 
3147 MyMoneyBudget MyMoneyFile::budgetByName(const QString& name) const
3148 {
3149     d->checkStorage();
3150 
3151     return d->m_storage->budgetByName(name);
3152 }
3153 
3154 void MyMoneyFile::modifyBudget(const MyMoneyBudget& budget)
3155 {
3156     d->checkTransaction(Q_FUNC_INFO);
3157 
3158     d->m_storage->modifyBudget(budget);
3159 }
3160 
3161 unsigned MyMoneyFile::countBudgets() const
3162 {
3163     d->checkStorage();
3164 
3165     return d->m_storage->countBudgets();
3166 }
3167 
3168 MyMoneyBudget MyMoneyFile::budget(const QString& id) const
3169 {
3170     d->checkStorage();
3171 
3172     return d->m_storage->budget(id);
3173 }
3174 
3175 void MyMoneyFile::removeBudget(const MyMoneyBudget& budget)
3176 {
3177     d->checkTransaction(Q_FUNC_INFO);
3178 
3179     d->m_storage->removeBudget(budget);
3180 }
3181 
3182 void MyMoneyFile::addOnlineJob(onlineJob& job)
3183 {
3184     d->checkTransaction(Q_FUNC_INFO);
3185 
3186     d->m_storage->addOnlineJob(job);
3187     d->m_changeSet += MyMoneyNotification(File::Mode::Add, job);
3188 }
3189 
3190 void MyMoneyFile::modifyOnlineJob(const onlineJob job)
3191 {
3192     d->checkTransaction(Q_FUNC_INFO);
3193     d->m_storage->modifyOnlineJob(job);
3194     d->m_changeSet += MyMoneyNotification(File::Mode::Modify, job);
3195 }
3196 
3197 onlineJob MyMoneyFile::getOnlineJob(const QString &jobId) const
3198 {
3199     d->checkStorage();
3200     return d->m_storage->getOnlineJob(jobId);
3201 }
3202 
3203 QList<onlineJob> MyMoneyFile::onlineJobList() const
3204 {
3205     d->checkStorage();
3206     return d->m_storage->onlineJobList();
3207 }
3208 
3209 /** @todo improve speed by passing count job to m_storage */
3210 int MyMoneyFile::countOnlineJobs() const
3211 {
3212     return onlineJobList().count();
3213 }
3214 
3215 /**
3216  * @brief Remove onlineJob
3217  * @param job onlineJob to remove
3218  */
3219 void MyMoneyFile::removeOnlineJob(const onlineJob& job)
3220 {
3221     d->checkTransaction(Q_FUNC_INFO);
3222 
3223     // clear all changed objects from cache
3224     if (job.isLocked()) {
3225         return;
3226     }
3227     d->m_changeSet += MyMoneyNotification(File::Mode::Remove, job);
3228     d->m_storage->removeOnlineJob(job);
3229 }
3230 
3231 void MyMoneyFile::removeOnlineJob(const QStringList onlineJobIds)
3232 {
3233     foreach (QString jobId, onlineJobIds) {
3234         removeOnlineJob(getOnlineJob(jobId));
3235     }
3236 }
3237 
3238 void MyMoneyFile::costCenterList(QList< MyMoneyCostCenter >& list) const
3239 {
3240     d->checkStorage();
3241     list = d->m_storage->costCenterList();
3242 }
3243 
3244 void MyMoneyFile::updateVAT(MyMoneyTransaction& transaction) const
3245 {
3246     // check if transaction qualifies
3247     const auto splitCount = transaction.splits().count();
3248     if (splitCount > 1 && splitCount <= 3) {
3249         MyMoneyMoney amount;
3250         MyMoneyAccount assetLiability;
3251         MyMoneyAccount category;
3252         MyMoneySplit taxSplit;
3253         const QString currencyId = transaction.commodity();
3254         foreach (const auto& split, transaction.splits()) {
3255             const auto acc = account(split.accountId());
3256             // all splits must reference accounts denoted in the same currency
3257             if (acc.currencyId() != currencyId) {
3258                 return;
3259             }
3260             if (acc.isAssetLiability() && assetLiability.id().isEmpty()) {
3261                 amount = split.shares();
3262                 assetLiability = acc;
3263                 continue;
3264             }
3265             if (acc.isAssetLiability()) {
3266                 return;
3267             }
3268             if (category.id().isEmpty() && !acc.value("VatAccount").isEmpty()) {
3269                 category = acc;
3270                 continue;
3271             } else if(taxSplit.id().isEmpty() && !acc.value("Tax").toLower().compare(QLatin1String("yes"))) {
3272                 taxSplit = split;
3273                 continue;
3274             }
3275             return;
3276         }
3277         if (!category.id().isEmpty()) {
3278             // remove a possibly found tax split - we create a new one
3279             // but only if it is the same tax category
3280             if (!taxSplit.id().isEmpty()) {
3281                 if (category.value("VatAccount").compare(taxSplit.accountId()))
3282                     return;
3283                 transaction.removeSplit(taxSplit);
3284             }
3285             addVATSplit(transaction, assetLiability, category, amount);
3286         }
3287     }
3288 }
3289 
3290 bool MyMoneyFile::addVATSplit(MyMoneyTransaction& transaction, const MyMoneyAccount& acc, const MyMoneyAccount& category, const MyMoneyMoney& amount) const
3291 {
3292     bool rc = false;
3293 
3294     try {
3295         MyMoneySplit cat;  // category
3296         MyMoneySplit tax;  // tax
3297 
3298         if (category.value("VatAccount").isEmpty())
3299             return false;
3300         MyMoneyAccount vatAcc = account(category.value("VatAccount"));
3301         const MyMoneySecurity& asec = security(acc.currencyId());
3302         const MyMoneySecurity& csec = security(category.currencyId());
3303         const MyMoneySecurity& vsec = security(vatAcc.currencyId());
3304         if (asec.id() != csec.id() || asec.id() != vsec.id()) {
3305             qDebug("Auto VAT assignment only works if all three accounts use the same currency.");
3306             return false;
3307         }
3308 
3309         MyMoneyMoney vatRate(vatAcc.value("VatRate"));
3310         MyMoneyMoney gv, nv;    // gross value, net value
3311         int fract = acc.fraction();
3312 
3313         if (!vatRate.isZero()) {
3314 
3315             tax.setAccountId(vatAcc.id());
3316 
3317             // qDebug("vat amount is '%s'", category.value("VatAmount").toLatin1());
3318             if (category.value("VatAmount").toLower() != QString("net")) {
3319                 // split value is the gross value
3320                 gv = amount;
3321                 nv = (gv / (MyMoneyMoney::ONE + vatRate)).convert(fract);
3322                 MyMoneySplit catSplit = transaction.splitByAccount(acc.id(), false);
3323                 catSplit.setShares(-nv);
3324                 catSplit.setValue(catSplit.shares());
3325                 transaction.modifySplit(catSplit);
3326 
3327             } else {
3328                 // split value is the net value
3329                 nv = amount;
3330                 gv = (nv * (MyMoneyMoney::ONE + vatRate)).convert(fract);
3331                 MyMoneySplit accSplit = transaction.splitByAccount(acc.id());
3332                 accSplit.setValue(gv.convert(fract));
3333                 accSplit.setShares(accSplit.value());
3334                 transaction.modifySplit(accSplit);
3335             }
3336 
3337             tax.setValue(-(gv - nv).convert(fract));
3338             tax.setShares(tax.value());
3339             transaction.addSplit(tax);
3340             rc = true;
3341         }
3342     } catch (const MyMoneyException &) {
3343     }
3344     return rc;
3345 }
3346 
3347 bool MyMoneyFile::isReferenced(const MyMoneyObject& obj, const QBitArray& skipChecks) const
3348 {
3349     d->checkStorage();
3350     return d->m_storage->isReferenced(obj, skipChecks);
3351 }
3352 
3353 bool MyMoneyFile::isReferenced(const MyMoneyObject& obj) const
3354 {
3355     return isReferenced(obj, QBitArray((int)eStorage::Reference::Count));
3356 }
3357 
3358 bool MyMoneyFile::checkNoUsed(const QString& accId, const QString& no) const
3359 {
3360     // by definition, an empty string or a non-numeric string is not used
3361     QRegExp exp(QString("(.*\\D)?(\\d+)(\\D.*)?"));
3362     if (no.isEmpty() || exp.indexIn(no) == -1)
3363         return false;
3364 
3365     MyMoneyTransactionFilter filter;
3366     filter.addAccount(accId);
3367     QList<MyMoneyTransaction> transactions = transactionList(filter);
3368     QList<MyMoneyTransaction>::ConstIterator it_t = transactions.constBegin();
3369     while (it_t != transactions.constEnd()) {
3370         try {
3371             MyMoneySplit split;
3372             // Test whether the transaction also includes a split into
3373             // this account
3374             split = (*it_t).splitByAccount(accId, true /*match*/);
3375             if (!split.number().isEmpty() && split.number() == no)
3376                 return true;
3377         } catch (const MyMoneyException &) {
3378         }
3379         ++it_t;
3380     }
3381     return false;
3382 }
3383 
3384 QString MyMoneyFile::highestCheckNo(const QString& accId) const
3385 {
3386     unsigned64 lno = 0;
3387     unsigned64 cno;
3388     QString no;
3389     MyMoneyTransactionFilter filter;
3390     filter.addAccount(accId);
3391     QList<MyMoneyTransaction> transactions = transactionList(filter);
3392     QList<MyMoneyTransaction>::ConstIterator it_t = transactions.constBegin();
3393     while (it_t != transactions.constEnd()) {
3394         try {
3395             // Test whether the transaction also includes a split into
3396             // this account
3397             MyMoneySplit split = (*it_t).splitByAccount(accId, true /*match*/);
3398             if (!split.number().isEmpty()) {
3399                 // non-numerical values stored in number will return 0 in the next line
3400                 cno = split.number().toULongLong();
3401                 if (cno > lno) {
3402                     lno = cno;
3403                     no = split.number();
3404                 }
3405             }
3406         } catch (const MyMoneyException &) {
3407         }
3408         ++it_t;
3409     }
3410     return no;
3411 }
3412 
3413 bool MyMoneyFile::hasNewerTransaction(const QString& accId, const QDate& date) const
3414 {
3415     MyMoneyTransactionFilter filter;
3416     filter.addAccount(accId);
3417     filter.setDateFilter(date.addDays(+1), QDate());
3418     return !transactionList(filter).isEmpty();
3419 }
3420 
3421 void MyMoneyFile::clearCache()
3422 {
3423     d->checkStorage();
3424     d->m_balanceCache.clear();
3425 }
3426 
3427 void MyMoneyFile::forceDataChanged()
3428 {
3429     emit dataChanged();
3430 }
3431 
3432 bool MyMoneyFile::isTransfer(const MyMoneyTransaction& t) const
3433 {
3434     auto rc = true;
3435     if (t.splitCount() == 2) {
3436         foreach (const auto split, t.splits()) {
3437             auto acc = account(split.accountId());
3438             if (acc.isIncomeExpense()) {
3439                 rc = false;
3440                 break;
3441             }
3442         }
3443     }
3444     return rc;
3445 }
3446 
3447 bool MyMoneyFile::referencesClosedAccount(const MyMoneyTransaction& t) const
3448 {
3449     auto ret = false;
3450     foreach (const auto split, t.splits()) {
3451         if (referencesClosedAccount(split)) {
3452             ret = true;
3453             break;
3454         }
3455     }
3456     return ret;
3457 }
3458 
3459 bool MyMoneyFile::referencesClosedAccount(const MyMoneySplit& s) const
3460 {
3461     if (s.accountId().isEmpty())
3462         return false;
3463 
3464     try {
3465         return account(s.accountId()).isClosed();
3466     } catch (const MyMoneyException &) {
3467     }
3468     return false;
3469 }
3470 
3471 QString MyMoneyFile::storageId()
3472 {
3473     QString id = value("kmm-id");
3474     if (id.isEmpty()) {
3475         MyMoneyFileTransaction ft;
3476         try {
3477             QUuid uid = QUuid::createUuid();
3478             setValue("kmm-id", uid.toString());
3479             ft.commit();
3480             id = uid.toString();
3481         } catch (const MyMoneyException &) {
3482             qDebug("Unable to setup UID for new storage object");
3483         }
3484     }
3485     return id;
3486 }
3487 
3488 QString MyMoneyFile::openingBalancesPrefix()
3489 {
3490     return i18n("Opening Balances");
3491 }
3492 
3493 bool MyMoneyFile::hasMatchingOnlineBalance(const MyMoneyAccount& _acc) const
3494 {
3495     // get current values
3496     auto acc = account(_acc.id());
3497 
3498     // if there's no last transaction import data we are done
3499     if (acc.value("lastImportedTransactionDate").isEmpty()
3500             || acc.value("lastStatementBalance").isEmpty())
3501         return false;
3502 
3503     // otherwise, we compare the balances
3504     MyMoneyMoney balance(acc.value("lastStatementBalance"));
3505     MyMoneyMoney accBalance = this->balance(acc.id(), QDate::fromString(acc.value("lastImportedTransactionDate"), Qt::ISODate));
3506 
3507     return balance == accBalance;
3508 }
3509 
3510 int MyMoneyFile::countTransactionsWithSpecificReconciliationState(const QString& accId, TransactionFilter::State state) const
3511 {
3512     MyMoneyTransactionFilter filter;
3513     filter.addAccount(accId);
3514     filter.addState((int)state);
3515     return transactionList(filter).count();
3516 }
3517 
3518 QMap<QString, QVector<int> > MyMoneyFile::countTransactionsWithSpecificReconciliationState() const
3519 {
3520     QMap<QString, QVector<int> > result;
3521     MyMoneyTransactionFilter filter;
3522     filter.setReportAllSplits(false);
3523 
3524     d->checkStorage();
3525 
3526     QList<MyMoneyAccount> list;
3527     accountList(list);
3528     for (const auto& account : list) {
3529         result[account.id()] = QVector<int>((int)eMyMoney::Split::State::MaxReconcileState, 0);
3530     }
3531 
3532     const auto transactions = d->m_storage->transactionList(filter);
3533     for (const auto& transaction : transactions) {
3534         const auto& splits = transaction.splits();
3535         for (const auto& split : splits) {
3536             if (!result.contains(split.accountId())) {
3537                 result[split.accountId()] = QVector<int>((int)eMyMoney::Split::State::MaxReconcileState, 0);
3538             }
3539             const auto flag = split.reconcileFlag();
3540             switch(flag) {
3541             case eMyMoney::Split::State::NotReconciled:
3542             case eMyMoney::Split::State::Cleared:
3543             case eMyMoney::Split::State::Reconciled:
3544             case eMyMoney::Split::State::Frozen:
3545                 result[split.accountId()][(int)flag]++;
3546                 break;
3547             default:
3548                 break;
3549             }
3550 
3551         }
3552     }
3553     return result;
3554 }
3555 
3556 /**
3557  * Make sure that the splits value has the precision of the corresponding account
3558  */
3559 void MyMoneyFile::fixSplitPrecision(MyMoneyTransaction& t) const
3560 {
3561     auto transactionSecurity = security(t.commodity());
3562     auto transactionFraction = transactionSecurity.smallestAccountFraction();
3563 
3564     for (auto& split : t.splits()) {
3565         auto acc = account(split.accountId());
3566         auto fraction = acc.fraction();
3567         if(fraction == -1) {
3568             auto sec = security(acc.currencyId());
3569             fraction = acc.fraction(sec);
3570         }
3571         // Don't do any rounding on a split factor
3572         if (split.action() != MyMoneySplit::actionName(eMyMoney::Split::Action::SplitShares)) {
3573             split.setShares(static_cast<const MyMoneyMoney>(split.shares().convertDenominator(fraction).canonicalize()));
3574             split.setValue(static_cast<const MyMoneyMoney>(split.value().convertDenominator(transactionFraction).canonicalize()));
3575         }
3576     }
3577 }
3578 
3579 MyMoneyMoney MyMoneyFile::stockSplit(const QString& accountId, MyMoneyMoney balance, MyMoneyMoney factor, eMyMoney::StockSplitDirection direction) const
3580 {
3581     d->checkStorage();
3582     return d->m_storage->stockSplit(accountId, balance, factor, direction);
3583 }
3584 
3585 class MyMoneyFileTransactionPrivate
3586 {
3587     Q_DISABLE_COPY(MyMoneyFileTransactionPrivate)
3588 
3589 public:
3590     MyMoneyFileTransactionPrivate() :
3591         m_isNested(MyMoneyFile::instance()->hasTransaction()),
3592         m_needRollback(!m_isNested)
3593     {
3594     }
3595 
3596 public:
3597     bool m_isNested;
3598     bool m_needRollback;
3599 
3600 };
3601 
3602 
3603 MyMoneyFileTransaction::MyMoneyFileTransaction() :
3604     d_ptr(new MyMoneyFileTransactionPrivate)
3605 {
3606     Q_D(MyMoneyFileTransaction);
3607     if (!d->m_isNested)
3608         MyMoneyFile::instance()->startTransaction();
3609 }
3610 
3611 MyMoneyFileTransaction::~MyMoneyFileTransaction()
3612 {
3613     try {
3614         rollback();
3615     } catch (const MyMoneyException &e) {
3616         qDebug() << e.what();
3617     }
3618     Q_D(MyMoneyFileTransaction);
3619     delete d;
3620 }
3621 
3622 void MyMoneyFileTransaction::restart()
3623 {
3624     rollback();
3625 
3626     Q_D(MyMoneyFileTransaction);
3627     d->m_needRollback = !d->m_isNested;
3628     if (!d->m_isNested)
3629         MyMoneyFile::instance()->startTransaction();
3630 }
3631 
3632 void MyMoneyFileTransaction::commit()
3633 {
3634     Q_D(MyMoneyFileTransaction);
3635     if (!d->m_isNested)
3636         MyMoneyFile::instance()->commitTransaction();
3637     d->m_needRollback = false;
3638 }
3639 
3640 void MyMoneyFileTransaction::rollback()
3641 {
3642     Q_D(MyMoneyFileTransaction);
3643     if (d->m_needRollback)
3644         MyMoneyFile::instance()->rollbackTransaction();
3645     d->m_needRollback = false;
3646 }