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(¤ciesModel, 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(¶metersModel, 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"