File indexing completed on 2024-05-19 05:07:17

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