diff --git a/kmymoney/converter/mymoneystatementreader.cpp b/kmymoney/converter/mymoneystatementreader.cpp index 0d7da2ea0..b82e94a82 100644 --- a/kmymoney/converter/mymoneystatementreader.cpp +++ b/kmymoney/converter/mymoneystatementreader.cpp @@ -1,1576 +1,1585 @@ /*************************************************************************** mymoneystatementreader.cpp ------------------- begin : Mon Aug 30 2004 copyright : (C) 2000-2004 by Michael Edwardes email : mte@users.sourceforge.net Javier Campos Morales Felix Rodriguez John C Thomas Baumgart Kevin Tambascio Ace Jones ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "mymoneystatementreader.h" #include // ---------------------------------------------------------------------------- // QT Headers #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Headers #include #include #include #include #include #include // ---------------------------------------------------------------------------- // Project Headers #include "mymoneyfile.h" #include "mymoneyaccount.h" #include "mymoneyprice.h" #include "mymoneyexception.h" #include "mymoneytransactionfilter.h" #include "mymoneypayee.h" #include "mymoneystatement.h" #include "mymoneysecurity.h" #include "kmymoneysettings.h" #include "transactioneditor.h" #include "stdtransactioneditor.h" #include "kmymoneyedit.h" #include "kaccountselectdlg.h" #include "knewaccountwizard.h" #include "transactionmatcher.h" #include "kenterscheduledlg.h" #include "kmymoneyaccountcombo.h" #include "accountsmodel.h" #include "models.h" #include "existingtransactionmatchfinder.h" #include "scheduledtransactionmatchfinder.h" #include "dialogenums.h" #include "mymoneyenums.h" #include "modelenums.h" #include "kmymoneyutils.h" using namespace eMyMoney; bool matchNotEmpty(const QString &l, const QString &r) { return !l.isEmpty() && QString::compare(l, r, Qt::CaseInsensitive) == 0; } Q_GLOBAL_STATIC(QStringList, globalResultMessages); class MyMoneyStatementReader::Private { public: Private() : transactionsCount(0), transactionsAdded(0), transactionsMatched(0), transactionsDuplicate(0), m_skipCategoryMatching(true), m_progressCallback(nullptr), scannedCategories(false) {} const QString& feeId(const MyMoneyAccount& invAcc); const QString& interestId(const MyMoneyAccount& invAcc); QString interestId(const QString& name); QString expenseId(const QString& name); QString feeId(const QString& name); void assignUniqueBankID(MyMoneySplit& s, const MyMoneyStatement::Transaction& t_in); void setupPrice(MyMoneySplit &s, const MyMoneyAccount &splitAccount, const MyMoneyAccount &transactionAccount, const QDate &postDate); MyMoneyAccount lastAccount; MyMoneyAccount m_account; MyMoneyAccount m_brokerageAccount; QList transactions; QList payees; int transactionsCount; int transactionsAdded; int transactionsMatched; int transactionsDuplicate; QMap uniqIds; QMap securitiesBySymbol; QMap securitiesByName; bool m_skipCategoryMatching; void (*m_progressCallback)(int, int, const QString&); private: void scanCategories(QString& id, const MyMoneyAccount& invAcc, const MyMoneyAccount& parentAccount, const QString& defaultName); /** * This method tries to figure out the category to be used for fees and interest * from previous transactions in the given @a investmentAccount and returns the * ids of those categories in @a feesId and @a interestId. The last used category * will be returned. */ void previouslyUsedCategories(const QString& investmentAccount, QString& feesId, QString& interestId); QString nameToId(const QString&name, MyMoneyAccount& parent); private: QString m_feeId; QString m_interestId; bool scannedCategories; }; const QString& MyMoneyStatementReader::Private::feeId(const MyMoneyAccount& invAcc) { scanCategories(m_feeId, invAcc, MyMoneyFile::instance()->expense(), i18n("_Fees")); return m_feeId; } const QString& MyMoneyStatementReader::Private::interestId(const MyMoneyAccount& invAcc) { scanCategories(m_interestId, invAcc, MyMoneyFile::instance()->income(), i18n("_Dividend")); return m_interestId; } QString MyMoneyStatementReader::Private::nameToId(const QString& name, MyMoneyAccount& parent) { // Adapted from KMyMoneyApp::createAccount(MyMoneyAccount& newAccount, MyMoneyAccount& parentAccount, MyMoneyAccount& brokerageAccount, MyMoneyMoney openingBal) // Needed to find/create category:sub-categories MyMoneyFile* file = MyMoneyFile::instance(); QString id = file->categoryToAccount(name, Account::Type::Unknown); // if it does not exist, we have to create it if (id.isEmpty()) { MyMoneyAccount newAccount; MyMoneyAccount parentAccount = parent; newAccount.setName(name) ; int pos; // check for ':' in the name and use it as separator for a hierarchy while ((pos = newAccount.name().indexOf(MyMoneyFile::AccountSeparator)) != -1) { QString part = newAccount.name().left(pos); QString remainder = newAccount.name().mid(pos + 1); const MyMoneyAccount& existingAccount = file->subAccountByName(parentAccount, part); if (existingAccount.id().isEmpty()) { newAccount.setName(part); newAccount.setAccountType(parentAccount.accountType()); file->addAccount(newAccount, parentAccount); parentAccount = newAccount; } else { parentAccount = existingAccount; } newAccount.setParentAccountId(QString()); // make sure, there's no parent newAccount.clearId(); // and no id set for adding newAccount.removeAccountIds(); // and no sub-account ids newAccount.setName(remainder); }//end while newAccount.setAccountType(parentAccount.accountType()); // make sure we have a currency. If none is assigned, we assume base currency if (newAccount.currencyId().isEmpty()) newAccount.setCurrencyId(file->baseCurrency().id()); file->addAccount(newAccount, parentAccount); id = newAccount.id(); } return id; } QString MyMoneyStatementReader::Private::expenseId(const QString& name) { MyMoneyAccount parent = MyMoneyFile::instance()->expense(); return nameToId(name, parent); } QString MyMoneyStatementReader::Private::interestId(const QString& name) { MyMoneyAccount parent = MyMoneyFile::instance()->income(); return nameToId(name, parent); } QString MyMoneyStatementReader::Private::feeId(const QString& name) { MyMoneyAccount parent = MyMoneyFile::instance()->expense(); return nameToId(name, parent); } void MyMoneyStatementReader::Private::previouslyUsedCategories(const QString& investmentAccount, QString& feesId, QString& interestId) { feesId.clear(); interestId.clear(); MyMoneyFile* file = MyMoneyFile::instance(); try { MyMoneyAccount acc = file->account(investmentAccount); MyMoneyTransactionFilter filter(investmentAccount); filter.setReportAllSplits(false); // since we assume an investment account here, we need to collect the stock accounts as well filter.addAccount(acc.accountList()); QList< QPair > list; file->transactionList(list, filter); QList< QPair >::const_iterator it_t; for (it_t = list.constBegin(); it_t != list.constEnd(); ++it_t) { const MyMoneyTransaction& t = (*it_t).first; MyMoneySplit s = (*it_t).second; acc = file->account(s.accountId()); // stock split shouldn't be fee or interest bacause it won't play nice with dissectTransaction // it was caused by processTransactionEntry adding splits in wrong order != with manual transaction entering if (acc.accountGroup() == Account::Type::Expense || acc.accountGroup() == Account::Type::Income) { foreach (auto sNew , t.splits()) { acc = file->account(sNew.accountId()); if (acc.accountGroup() != Account::Type::Expense && // shouldn't be fee acc.accountGroup() != Account::Type::Income && // shouldn't be interest ((sNew.value() != sNew.shares()) || // shouldn't be checking account... (sNew.price() != MyMoneyMoney::ONE))) { // ...but sometimes it may look like checking account s = sNew; break; } } } MyMoneySplit assetAccountSplit; QList feeSplits; QList interestSplits; MyMoneySecurity security; MyMoneySecurity currency; eMyMoney::Split::InvestmentTransactionType transactionType; KMyMoneyUtils::dissectTransaction(t, s, assetAccountSplit, feeSplits, interestSplits, security, currency, transactionType); if (!feeSplits.isEmpty()) { feesId = feeSplits.first().accountId(); if (!interestId.isEmpty()) break; } if (!interestSplits.isEmpty()) { interestId = interestSplits.first().accountId(); if (!feesId.isEmpty()) break; } } } catch (const MyMoneyException &) { } } void MyMoneyStatementReader::Private::scanCategories(QString& id, const MyMoneyAccount& invAcc, const MyMoneyAccount& parentAccount, const QString& defaultName) { if (!scannedCategories) { previouslyUsedCategories(invAcc.id(), m_feeId, m_interestId); scannedCategories = true; } if (id.isEmpty()) { MyMoneyFile* file = MyMoneyFile::instance(); MyMoneyAccount acc = file->accountByName(defaultName); // if it does not exist, we have to create it if (acc.id().isEmpty()) { MyMoneyAccount parent = parentAccount; acc.setName(defaultName); acc.setAccountType(parent.accountType()); acc.setCurrencyId(parent.currencyId()); file->addAccount(acc, parent); } id = acc.id(); } } void MyMoneyStatementReader::Private::assignUniqueBankID(MyMoneySplit& s, const MyMoneyStatement::Transaction& t_in) { if (! t_in.m_strBankID.isEmpty()) { // make sure that id's are unique from this point on by appending a -# // postfix if needed QString base(t_in.m_strBankID); QString hash(base); int idx = 1; for (;;) { QMap::const_iterator it; it = uniqIds.constFind(hash); if (it == uniqIds.constEnd()) { uniqIds[hash] = true; break; } hash = QString("%1-%2").arg(base).arg(idx); ++idx; } s.setBankID(hash); } } void MyMoneyStatementReader::Private::setupPrice(MyMoneySplit &s, const MyMoneyAccount &splitAccount, const MyMoneyAccount &transactionAccount, const QDate &postDate) { if (transactionAccount.currencyId() != splitAccount.currencyId()) { // a currency converstion is needed assume that split has already a proper value MyMoneyFile* file = MyMoneyFile::instance(); MyMoneySecurity toCurrency = file->security(splitAccount.currencyId()); MyMoneySecurity fromCurrency = file->security(transactionAccount.currencyId()); // get the price for the transaction's date const MyMoneyPrice &price = file->price(fromCurrency.id(), toCurrency.id(), postDate); // if the price is valid calculate the shares if (price.isValid()) { const int fract = splitAccount.fraction(toCurrency); const MyMoneyMoney &shares = s.value() * price.rate(toCurrency.id()); s.setShares(shares.convert(fract)); qDebug("Setting second split shares to %s", qPrintable(s.shares().formatMoney(toCurrency.id(), 2))); } else { qDebug("No price entry was found to convert from '%s' to '%s' on '%s'", qPrintable(fromCurrency.tradingSymbol()), qPrintable(toCurrency.tradingSymbol()), qPrintable(postDate.toString(Qt::ISODate))); } } } MyMoneyStatementReader::MyMoneyStatementReader() : d(new Private), m_userAbort(false), m_autoCreatePayee(false), m_ft(0), m_progressCallback(0) { m_askPayeeCategory = KMyMoneySettings::askForPayeeCategory(); } MyMoneyStatementReader::~MyMoneyStatementReader() { delete d; } bool MyMoneyStatementReader::anyTransactionAdded() const { return (d->transactionsAdded != 0) ? true : false; } void MyMoneyStatementReader::setAutoCreatePayee(bool create) { m_autoCreatePayee = create; } void MyMoneyStatementReader::setAskPayeeCategory(bool ask) { m_askPayeeCategory = ask; } QStringList MyMoneyStatementReader::importStatement(const QString& url, bool silent, void(*callback)(int, int, const QString&)) { QStringList summary; MyMoneyStatement s; if (MyMoneyStatement::readXMLFile(s, url)) summary = MyMoneyStatementReader::importStatement(s, silent, callback); else KMessageBox::error(nullptr, i18n("Error importing %1: This file is not a valid KMM statement file.", url), i18n("Invalid Statement")); return summary; } QStringList MyMoneyStatementReader::importStatement(const MyMoneyStatement& s, bool silent, void(*callback)(int, int, const QString&)) { auto result = false; // keep a copy of the statement if (KMyMoneySettings::logImportedStatements()) { auto logFile = QString::fromLatin1("%1/kmm-statement-%2.txt").arg(KMyMoneySettings::logPath(), QDateTime::currentDateTimeUtc().toString(QStringLiteral("yyyy-MM-dd hh-mm-ss"))); MyMoneyStatement::writeXMLFile(s, logFile); } auto reader = new MyMoneyStatementReader; reader->setAutoCreatePayee(true); if (callback) reader->setProgressCallback(callback); QStringList messages; result = reader->import(s, messages); auto transactionAdded = reader->anyTransactionAdded(); delete reader; if (callback) callback(-1, -1, QString()); if (!silent && transactionAdded) { globalResultMessages()->append(messages); } if (!result) messages.clear(); return messages; } bool MyMoneyStatementReader::import(const MyMoneyStatement& s, QStringList& messages) { // // Select the account // d->m_account = MyMoneyAccount(); d->m_brokerageAccount = MyMoneyAccount(); m_ft = new MyMoneyFileTransaction(); d->m_skipCategoryMatching = s.m_skipCategoryMatching; // if the statement source left some information about // the account, we use it to get the current data of it if (!s.m_accountId.isEmpty()) { try { d->m_account = MyMoneyFile::instance()->account(s.m_accountId); } catch (const MyMoneyException &) { qDebug("Received reference '%s' to unknown account in statement", qPrintable(s.m_accountId)); } } if (d->m_account.id().isEmpty()) { d->m_account.setName(s.m_strAccountName); d->m_account.setNumber(s.m_strAccountNumber); switch (s.m_eType) { case eMyMoney::Statement::Type::Checkings: d->m_account.setAccountType(Account::Type::Checkings); break; case eMyMoney::Statement::Type::Savings: d->m_account.setAccountType(Account::Type::Savings); break; case eMyMoney::Statement::Type::Investment: //testing support for investment statements! //m_userAbort = true; //KMessageBox::error(kmymoney, i18n("This is an investment statement. These are not supported currently."), i18n("Critical Error")); d->m_account.setAccountType(Account::Type::Investment); break; case eMyMoney::Statement::Type::CreditCard: d->m_account.setAccountType(Account::Type::CreditCard); break; default: d->m_account.setAccountType(Account::Type::Unknown); break; } // we ask the user only if we have some transactions to process if (!m_userAbort && s.m_listTransactions.count() > 0) m_userAbort = ! selectOrCreateAccount(Select, d->m_account); } // see if we need to update some values stored with the account const auto statementEndDate = s.statementEndDate(); if (d->m_account.value("lastStatementBalance") != s.m_closingBalance.toString() || d->m_account.value("lastImportedTransactionDate") != statementEndDate.toString(Qt::ISODate)) { if (s.m_closingBalance != MyMoneyMoney::autoCalc) { d->m_account.setValue("lastStatementBalance", s.m_closingBalance.toString()); } if (statementEndDate.isValid()) { d->m_account.setValue("lastImportedTransactionDate", statementEndDate.toString(Qt::ISODate)); } try { MyMoneyFile::instance()->modifyAccount(d->m_account); } catch (const MyMoneyException &) { qDebug("Updating account in MyMoneyStatementReader::startImport failed"); } } if (!d->m_account.name().isEmpty()) messages += i18n("Importing statement for account %1", d->m_account.name()); else if (s.m_listTransactions.count() == 0) messages += i18n("Importing statement without transactions"); qDebug("Importing statement for '%s'", qPrintable(d->m_account.name())); // // Process the securities // signalProgress(0, s.m_listSecurities.count(), "Importing Statement ..."); int progress = 0; QList::const_iterator it_s = s.m_listSecurities.begin(); while (it_s != s.m_listSecurities.end()) { processSecurityEntry(*it_s); signalProgress(++progress, 0); ++it_s; } signalProgress(-1, -1); // // Process the transactions // if (!m_userAbort) { try { qDebug("Processing transactions (%s)", qPrintable(d->m_account.name())); signalProgress(0, s.m_listTransactions.count(), "Importing Statement ..."); progress = 0; QList::const_iterator it_t = s.m_listTransactions.begin(); while (it_t != s.m_listTransactions.end() && !m_userAbort) { processTransactionEntry(*it_t); signalProgress(++progress, 0); ++it_t; } qDebug("Processing transactions done (%s)", qPrintable(d->m_account.name())); } catch (const MyMoneyException &e) { if (QString::fromLatin1(e.what()).contains("USERABORT")) m_userAbort = true; else qDebug("Caught exception from processTransactionEntry() not caused by USERABORT: %s", e.what()); } signalProgress(-1, -1); } // // process price entries // if (!m_userAbort) { try { signalProgress(0, s.m_listPrices.count(), "Importing Statement ..."); KMyMoneyUtils::processPriceList(s); } catch (const MyMoneyException &e) { if (QString::fromLatin1(e.what()).contains("USERABORT")) m_userAbort = true; else qDebug("Caught exception from processPriceEntry() not caused by USERABORT: %s", e.what()); } signalProgress(-1, -1); } bool rc = false; // delete all payees created in vain int payeeCount = d->payees.count(); QList::const_iterator it_p; for (it_p = d->payees.constBegin(); it_p != d->payees.constEnd(); ++it_p) { try { MyMoneyFile::instance()->removePayee(*it_p); --payeeCount; } catch (const MyMoneyException &) { // if we can't delete it, it must be in use which is ok for us } } if (s.m_closingBalance.isAutoCalc()) { messages += i18n(" Statement balance is not contained in statement."); } else { messages += i18n(" Statement balance on %1 is reported to be %2", s.m_dateEnd.toString(Qt::ISODate), s.m_closingBalance.formatMoney("", 2)); } messages += i18n(" Transactions"); messages += i18np(" %1 processed", " %1 processed", d->transactionsCount); messages += i18ncp("x transactions have been added", " %1 added", " %1 added", d->transactionsAdded); messages += i18np(" %1 matched", " %1 matched", d->transactionsMatched); messages += i18np(" %1 duplicate", " %1 duplicates", d->transactionsDuplicate); messages += i18n(" Payees"); messages += i18ncp("x transactions have been created", " %1 created", " %1 created", payeeCount); messages += QString(); // remove the Don't ask again entries KSharedConfigPtr config = KSharedConfig::openConfig(); KConfigGroup grp = config->group(QString::fromLatin1("Notification Messages")); QStringList::ConstIterator it; for (it = m_dontAskAgain.constBegin(); it != m_dontAskAgain.constEnd(); ++it) { grp.deleteEntry(*it); } config->sync(); m_dontAskAgain.clear(); rc = !m_userAbort; // finish the transaction if (rc) m_ft->commit(); delete m_ft; m_ft = 0; qDebug("Importing statement for '%s' done", qPrintable(d->m_account.name())); return rc; } void MyMoneyStatementReader::processSecurityEntry(const MyMoneyStatement::Security& sec_in) { // For a security entry, we will just make sure the security exists in the // file. It will not get added to the investment account until it's called // for in a transaction. MyMoneyFile* file = MyMoneyFile::instance(); // check if we already have the security // In a statement, we do not know what type of security this is, so we will // not use type as a matching factor. MyMoneySecurity security; QList list = file->securityList(); QList::ConstIterator it = list.constBegin(); while (it != list.constEnd() && security.id().isEmpty()) { if (matchNotEmpty(sec_in.m_strSymbol, (*it).tradingSymbol()) || matchNotEmpty(sec_in.m_strName, (*it).name())) { security = *it; } ++it; } // if the security was not found, we have to create it while not forgetting // to setup the type if (security.id().isEmpty()) { security.setName(sec_in.m_strName); security.setTradingSymbol(sec_in.m_strSymbol); security.setTradingCurrency(file->baseCurrency().id()); security.setValue("kmm-security-id", sec_in.m_strId); security.setValue("kmm-online-source", "Stooq"); security.setSecurityType(Security::Type::Stock); MyMoneyFileTransaction ft; try { file->addSecurity(security); ft.commit(); qDebug() << "Created " << security.name() << " with id " << security.id(); } catch (const MyMoneyException &e) { KMessageBox::error(0, i18n("Error creating security record: %1", QString::fromLatin1(e.what())), i18n("Error")); } } else { qDebug() << "Found " << security.name() << " with id " << security.id(); } } void MyMoneyStatementReader::processTransactionEntry(const MyMoneyStatement::Transaction& statementTransactionUnderImport) { MyMoneyFile* file = MyMoneyFile::instance(); MyMoneyTransaction transactionUnderImport; QString dbgMsg; dbgMsg = QString("Process on: '%1', id: '%3', amount: '%2', fees: '%4'") .arg(statementTransactionUnderImport.m_datePosted.toString(Qt::ISODate)) .arg(statementTransactionUnderImport.m_amount.formatMoney("", 2)) .arg(statementTransactionUnderImport.m_strBankID) .arg(statementTransactionUnderImport.m_fees.formatMoney("", 2)); qDebug("%s", qPrintable(dbgMsg)); // mark it imported for the view transactionUnderImport.setImported(); // TODO (Ace) We can get the commodity from the statement!! // Although then we would need UI to verify transactionUnderImport.setCommodity(d->m_account.currencyId()); transactionUnderImport.setPostDate(statementTransactionUnderImport.m_datePosted); transactionUnderImport.setMemo(statementTransactionUnderImport.m_strMemo); MyMoneySplit s1; MyMoneySplit s2; MyMoneySplit sFees; MyMoneySplit sBrokerage; s1.setMemo(statementTransactionUnderImport.m_strMemo); s1.setValue(statementTransactionUnderImport.m_amount + statementTransactionUnderImport.m_fees); s1.setShares(s1.value()); s1.setNumber(statementTransactionUnderImport.m_strNumber); // set these values if a transfer split is needed at the very end. MyMoneyMoney transfervalue; // If the user has chosen to import into an investment account, determine the correct account to use MyMoneyAccount thisaccount = d->m_account; QString brokerageactid; if (thisaccount.accountType() == Account::Type::Investment) { // determine the brokerage account brokerageactid = d->m_account.value("kmm-brokerage-account").toUtf8(); if (brokerageactid.isEmpty()) { brokerageactid = file->accountByName(statementTransactionUnderImport.m_strBrokerageAccount).id(); } if (brokerageactid.isEmpty()) { brokerageactid = file->nameToAccount(statementTransactionUnderImport.m_strBrokerageAccount); } if (brokerageactid.isEmpty()) { brokerageactid = file->nameToAccount(thisaccount.brokerageName()); } if (brokerageactid.isEmpty()) { brokerageactid = SelectBrokerageAccount(); } // find the security transacted, UNLESS this transaction didn't // involve any security. if ((statementTransactionUnderImport.m_eAction != eMyMoney::Transaction::Action::None) // eaInterest transactions MAY have a security. // && (t_in.m_eAction != MyMoneyStatement::Transaction::eaInterest) && (statementTransactionUnderImport.m_eAction != eMyMoney::Transaction::Action::Fees)) { // the correct account is the stock account which matches two criteria: // (1) it is a sub-account of the selected investment account, and // (2a) the symbol of the underlying security matches the security of the // transaction, or // (2b) the name of the security matches the name of the security of the transaction. // search through each subordinate account auto found = false; QString currencyid; foreach (const auto sAccount, thisaccount.accountList()) { currencyid = file->account(sAccount).currencyId(); auto security = file->security(currencyid); if (matchNotEmpty(statementTransactionUnderImport.m_strSymbol, security.tradingSymbol()) || matchNotEmpty(statementTransactionUnderImport.m_strSecurity, security.name())) { thisaccount = file->account(sAccount); found = true; break; } } // If there was no stock account under the m_acccount investment account, // add one using the security. if (!found) { // The security should always be available, because the statement file // should separately list all the securities referred to in the file, // and when we found a security, we added it to the file. if (statementTransactionUnderImport.m_strSecurity.isEmpty()) { KMessageBox::information(0, i18n("This imported statement contains investment transactions with no security. These transactions will be ignored."), i18n("Security not found"), QString("BlankSecurity")); return; } else { MyMoneySecurity security; QList list = MyMoneyFile::instance()->securityList(); QList::ConstIterator it = list.constBegin(); while (it != list.constEnd() && security.id().isEmpty()) { if (matchNotEmpty(statementTransactionUnderImport.m_strSymbol, (*it).tradingSymbol()) || matchNotEmpty(statementTransactionUnderImport.m_strSecurity, (*it).name())) { security = *it; } ++it; } if (!security.id().isEmpty()) { thisaccount = MyMoneyAccount(); thisaccount.setName(security.name()); thisaccount.setAccountType(Account::Type::Stock); thisaccount.setCurrencyId(security.id()); currencyid = thisaccount.currencyId(); file->addAccount(thisaccount, d->m_account); qDebug() << Q_FUNC_INFO << ": created account " << thisaccount.id() << " for security " << statementTransactionUnderImport.m_strSecurity << " under account " << d->m_account.id(); } // this security does not exist in the file. else { // This should be rare. A statement should have a security entry for any // of the securities referred to in the transactions. The only way to get // here is if that's NOT the case. int ret = KMessageBox::warningContinueCancel(0, i18n("
This investment account does not contain the \"%1\" security.
" "
Transactions involving this security will be ignored.
", statementTransactionUnderImport.m_strSecurity), i18n("Security not found"), KStandardGuiItem::cont(), KStandardGuiItem::cancel()); if (ret == KMessageBox::Cancel) { m_userAbort = true; } return; } } } // Don't update price if there is no price information contained in the transaction if (statementTransactionUnderImport.m_eAction != eMyMoney::Transaction::Action::CashDividend && statementTransactionUnderImport.m_eAction != eMyMoney::Transaction::Action::Shrsin && statementTransactionUnderImport.m_eAction != eMyMoney::Transaction::Action::Shrsout) { // update the price, while we're here. in the future, this should be // an option QString basecurrencyid = file->baseCurrency().id(); const MyMoneyPrice &price = file->price(currencyid, basecurrencyid, statementTransactionUnderImport.m_datePosted, true); if (!price.isValid() && ((!statementTransactionUnderImport.m_amount.isZero() && !statementTransactionUnderImport.m_shares.isZero()) || !statementTransactionUnderImport.m_price.isZero())) { MyMoneyPrice newprice; if (!statementTransactionUnderImport.m_price.isZero()) { newprice = MyMoneyPrice(currencyid, basecurrencyid, statementTransactionUnderImport.m_datePosted, statementTransactionUnderImport.m_price.abs(), i18n("Statement Importer")); } else { newprice = MyMoneyPrice(currencyid, basecurrencyid, statementTransactionUnderImport.m_datePosted, (statementTransactionUnderImport.m_amount / statementTransactionUnderImport.m_shares).abs(), i18n("Statement Importer")); } file->addPrice(newprice); } } } s1.setAccountId(thisaccount.id()); d->assignUniqueBankID(s1, statementTransactionUnderImport); if (statementTransactionUnderImport.m_eAction == eMyMoney::Transaction::Action::ReinvestDividend) { s1.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::ReinvestDividend)); s1.setShares(statementTransactionUnderImport.m_shares); if (!statementTransactionUnderImport.m_price.isZero()) { s1.setPrice(statementTransactionUnderImport.m_price); } else { if (statementTransactionUnderImport.m_shares.isZero()) { KMessageBox::information(0, i18n("This imported statement contains investment transactions with no share amount. These transactions will be ignored."), i18n("No share amount provided"), QString("BlankAmount")); return; } MyMoneyMoney total = -statementTransactionUnderImport.m_amount - statementTransactionUnderImport.m_fees; s1.setPrice(MyMoneyMoney((total / statementTransactionUnderImport.m_shares).convertPrecision(file->security(thisaccount.currencyId()).pricePrecision()))); } s2.setMemo(statementTransactionUnderImport.m_strMemo); if (statementTransactionUnderImport.m_strInterestCategory.isEmpty()) s2.setAccountId(d->interestId(thisaccount)); else s2.setAccountId(d->interestId(statementTransactionUnderImport.m_strInterestCategory)); s2.setShares(-statementTransactionUnderImport.m_amount - statementTransactionUnderImport.m_fees); s2.setValue(s2.shares()); } else if (statementTransactionUnderImport.m_eAction == eMyMoney::Transaction::Action::CashDividend) { // Cash dividends require setting 2 splits to get all of the information // in. Split #1 will be the income split, and we'll set it to the first // income account. This is a hack, but it's needed in order to get the // amount into the transaction. if (statementTransactionUnderImport.m_strInterestCategory.isEmpty()) s1.setAccountId(d->interestId(thisaccount)); else {// Ensure category sub-accounts are dealt with properly s1.setAccountId(d->interestId(statementTransactionUnderImport.m_strInterestCategory)); } s1.setShares(-statementTransactionUnderImport.m_amount - statementTransactionUnderImport.m_fees); s1.setValue(-statementTransactionUnderImport.m_amount - statementTransactionUnderImport.m_fees); // Split 2 will be the zero-amount investment split that serves to // mark this transaction as a cash dividend and note which stock account // it belongs to. s2.setMemo(statementTransactionUnderImport.m_strMemo); s2.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::Dividend)); s2.setAccountId(thisaccount.id()); /* at this point any fees have been taken into account already * so don't deduct them again. * BUG 322381 */ transfervalue = statementTransactionUnderImport.m_amount; } else if (statementTransactionUnderImport.m_eAction == eMyMoney::Transaction::Action::Interest) { if (statementTransactionUnderImport.m_strInterestCategory.isEmpty()) s1.setAccountId(d->interestId(thisaccount)); else {// Ensure category sub-accounts are dealt with properly if (statementTransactionUnderImport.m_amount.isPositive()) s1.setAccountId(d->interestId(statementTransactionUnderImport.m_strInterestCategory)); else s1.setAccountId(d->expenseId(statementTransactionUnderImport.m_strInterestCategory)); } s1.setShares(-statementTransactionUnderImport.m_amount - statementTransactionUnderImport.m_fees); s1.setValue(-statementTransactionUnderImport.m_amount - statementTransactionUnderImport.m_fees); /// *********** Add split as per Div ********** // Split 2 will be the zero-amount investment split that serves to // mark this transaction as a cash dividend and note which stock account // it belongs to. s2.setMemo(statementTransactionUnderImport.m_strMemo); s2.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::InterestIncome)); s2.setAccountId(thisaccount.id()); transfervalue = statementTransactionUnderImport.m_amount; } else if (statementTransactionUnderImport.m_eAction == eMyMoney::Transaction::Action::Fees) { if (statementTransactionUnderImport.m_strInterestCategory.isEmpty()) s1.setAccountId(d->feeId(thisaccount)); else// Ensure category sub-accounts are dealt with properly s1.setAccountId(d->feeId(statementTransactionUnderImport.m_strInterestCategory)); s1.setShares(statementTransactionUnderImport.m_amount); s1.setValue(statementTransactionUnderImport.m_amount); transfervalue = statementTransactionUnderImport.m_amount; } else if ((statementTransactionUnderImport.m_eAction == eMyMoney::Transaction::Action::Buy) || (statementTransactionUnderImport.m_eAction == eMyMoney::Transaction::Action::Sell)) { s1.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::BuyShares)); if (!statementTransactionUnderImport.m_price.isZero()) { s1.setPrice(statementTransactionUnderImport.m_price.abs()); } else if (!statementTransactionUnderImport.m_shares.isZero()) { MyMoneyMoney total = statementTransactionUnderImport.m_amount + statementTransactionUnderImport.m_fees.abs(); s1.setPrice(MyMoneyMoney((total / statementTransactionUnderImport.m_shares).abs().convertPrecision(file->security(thisaccount.currencyId()).pricePrecision()))); } if (statementTransactionUnderImport.m_eAction == eMyMoney::Transaction::Action::Buy) s1.setShares(statementTransactionUnderImport.m_shares.abs()); else s1.setShares(-statementTransactionUnderImport.m_shares.abs()); s1.setValue(-(statementTransactionUnderImport.m_amount + statementTransactionUnderImport.m_fees.abs())); transfervalue = statementTransactionUnderImport.m_amount; } else if ((statementTransactionUnderImport.m_eAction == eMyMoney::Transaction::Action::Shrsin) || (statementTransactionUnderImport.m_eAction == eMyMoney::Transaction::Action::Shrsout)) { s1.setValue(MyMoneyMoney()); s1.setShares(statementTransactionUnderImport.m_shares); s1.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::AddShares)); } else if (statementTransactionUnderImport.m_eAction == eMyMoney::Transaction::Action::None) { // User is attempting to import a non-investment transaction into this // investment account. This is not supportable the way KMyMoney is // written. However, if a user has an associated brokerage account, // we can stuff the transaction there. brokerageactid = d->m_account.value("kmm-brokerage-account").toUtf8(); if (brokerageactid.isEmpty()) { brokerageactid = file->accountByName(d->m_account.brokerageName()).id(); } if (! brokerageactid.isEmpty()) { s1.setAccountId(brokerageactid); d->assignUniqueBankID(s1, statementTransactionUnderImport); // Needed to satisfy the bankid check below. thisaccount = file->account(brokerageactid); } else { // Warning!! Your transaction is being thrown away. } } if (!statementTransactionUnderImport.m_fees.isZero()) { sFees.setMemo(i18n("(Fees) %1", statementTransactionUnderImport.m_strMemo)); sFees.setValue(statementTransactionUnderImport.m_fees); sFees.setShares(statementTransactionUnderImport.m_fees); sFees.setAccountId(d->feeId(thisaccount)); } } else { // For non-investment accounts, just use the selected account // Note that it is perfectly reasonable to import an investment statement into a non-investment account // if you really want. The investment-specific information, such as number of shares and action will // be discarded in that case. s1.setAccountId(d->m_account.id()); d->assignUniqueBankID(s1, statementTransactionUnderImport); } QString payeename = statementTransactionUnderImport.m_strPayee; if (!payeename.isEmpty()) { qDebug() << QLatin1String("Start matching payee") << payeename; QString payeeid; try { QList pList = file->payeeList(); QList::const_iterator it_p; QMap matchMap; for (it_p = pList.constBegin(); it_p != pList.constEnd(); ++it_p) { bool ignoreCase; QStringList keys; QStringList::const_iterator it_s; const auto matchType = (*it_p).matchData(ignoreCase, keys); switch (matchType) { case eMyMoney::Payee::MatchType::Disabled: break; case eMyMoney::Payee::MatchType::Name: case eMyMoney::Payee::MatchType::NameExact: keys << QString("%1").arg(QRegExp::escape((*it_p).name())); if(matchType == eMyMoney::Payee::MatchType::NameExact) { keys.clear(); keys << QString("^%1$").arg(QRegExp::escape((*it_p).name())); } // intentional fall through case eMyMoney::Payee::MatchType::Key: for (it_s = keys.constBegin(); it_s != keys.constEnd(); ++it_s) { QRegExp exp(*it_s, ignoreCase ? Qt::CaseInsensitive : Qt::CaseSensitive); if (exp.indexIn(payeename) != -1) { qDebug("Found match with '%s' on '%s'", qPrintable(payeename), qPrintable((*it_p).name())); matchMap[exp.matchedLength()] = (*it_p).id(); } } break; } } // at this point we can have several scenarios: // a) multiple matches // b) a single match // c) no match at all // // for c) we just do nothing, for b) we take the one we found // in case of a) we take the one with the largest matchedLength() // which happens to be the last one in the map if (matchMap.count() > 1) { qDebug("Multiple matches"); QMap::const_iterator it_m = matchMap.constEnd(); --it_m; payeeid = *it_m; } else if (matchMap.count() == 1) { qDebug("Single matches"); payeeid = *(matchMap.constBegin()); } // if we did not find a matching payee, we throw an exception and try to create it if (payeeid.isEmpty()) throw MYMONEYEXCEPTION_CSTRING("payee not matched"); s1.setPayeeId(payeeid); } catch (const MyMoneyException &) { MyMoneyPayee payee; int rc = KMessageBox::Yes; if (m_autoCreatePayee == false) { // Ask the user if that is what he intended to do? QString msg = i18n("Do you want to add \"%1\" as payee/receiver?\n\n", payeename); msg += i18n("Selecting \"Yes\" will create the payee, \"No\" will skip " "creation of a payee record and remove the payee information " "from this transaction. Selecting \"Cancel\" aborts the import " "operation.\n\nIf you select \"No\" here and mark the \"Do not ask " "again\" checkbox, the payee information for all following transactions " "referencing \"%1\" will be removed.", payeename); QString askKey = QString("Statement-Import-Payee-") + payeename; if (!m_dontAskAgain.contains(askKey)) { m_dontAskAgain += askKey; } rc = KMessageBox::questionYesNoCancel(0, msg, i18n("New payee/receiver"), KStandardGuiItem::yes(), KStandardGuiItem::no(), KStandardGuiItem::cancel(), askKey); } if (rc == KMessageBox::Yes) { // for now, we just add the payee to the pool and turn // on simple name matching, so that future transactions // with the same name don't get here again. // // In the future, we could open a dialog and ask for // all the other attributes of the payee, but since this // is called in the context of an automatic procedure it // might distract the user. payee.setName(payeename); payee.setMatchData(eMyMoney::Payee::MatchType::Key, true, QStringList() << QString("^%1$").arg(QRegExp::escape(payeename))); if (m_askPayeeCategory) { // We use a QPointer because the dialog may get deleted // during exec() if the parent of the dialog gets deleted. // In that case the guarded ptr will reset to 0. QPointer dialog = new QDialog; dialog->setWindowTitle(i18n("Default Category for Payee")); dialog->setModal(true); QWidget *mainWidget = new QWidget; QVBoxLayout *topcontents = new QVBoxLayout(mainWidget); //add in caption? and account combo here QLabel *label1 = new QLabel(i18n("Please select a default category for payee '%1'", payeename)); topcontents->addWidget(label1); auto filterProxyModel = new AccountNamesFilterProxyModel(this); filterProxyModel->setHideEquityAccounts(!KMyMoneySettings::expertMode()); filterProxyModel->addAccountGroup(QVector {Account::Type::Asset, Account::Type::Liability, Account::Type::Equity, Account::Type::Income, Account::Type::Expense}); auto const model = Models::instance()->accountsModel(); filterProxyModel->setSourceModel(model); filterProxyModel->setSourceColumns(model->getColumns()); filterProxyModel->sort((int)eAccountsModel::Column::Account); QPointer accountCombo = new KMyMoneyAccountCombo(filterProxyModel); topcontents->addWidget(accountCombo); mainWidget->setLayout(topcontents); QVBoxLayout *mainLayout = new QVBoxLayout; QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Cancel|QDialogButtonBox::No|QDialogButtonBox::Yes); dialog->setLayout(mainLayout); mainLayout->addWidget(mainWidget); dialog->connect(buttonBox, SIGNAL(accepted()), dialog, SLOT(accept())); dialog->connect(buttonBox, SIGNAL(rejected()), dialog, SLOT(reject())); mainLayout->addWidget(buttonBox); KGuiItem::assign(buttonBox->button(QDialogButtonBox::Yes), KGuiItem(i18n("Save Category"))); KGuiItem::assign(buttonBox->button(QDialogButtonBox::No), KGuiItem(i18n("No Category"))); KGuiItem::assign(buttonBox->button(QDialogButtonBox::Cancel), KGuiItem(i18n("Abort"))); int result = dialog->exec(); QString accountId; if (accountCombo && !accountCombo->getSelected().isEmpty()) { accountId = accountCombo->getSelected(); } delete dialog; //if they hit yes instead of no, then grab setting of account combo if (result == QDialog::Accepted) { payee.setDefaultAccountId(accountId); } else if (result != QDialog::Rejected) { //add cancel button? and throw exception like below throw MYMONEYEXCEPTION_CSTRING("USERABORT"); } } try { file->addPayee(payee); qDebug("Payee '%s' created", qPrintable(payee.name())); d->payees << payee; payeeid = payee.id(); s1.setPayeeId(payeeid); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(nullptr, i18n("Unable to add payee/receiver"), QString::fromLatin1(e.what())); } } else if (rc == KMessageBox::No) { s1.setPayeeId(QString()); } else { throw MYMONEYEXCEPTION_CSTRING("USERABORT"); } } if (thisaccount.accountType() != Account::Type::Stock) { // // Fill in other side of the transaction (category/etc) based on payee // // Note, this logic is lifted from KLedgerView::slotPayeeChanged(), // however this case is more complicated, because we have an amount and // a memo. We just don't have the other side of the transaction. // // We'll search for the most recent transaction in this account with // this payee. If this reference transaction is a simple 2-split // transaction, it's simple. If it's a complex split, and the amounts // are different, we have a problem. Somehow we have to balance the // transaction. For now, we'll leave it unbalanced, and let the user // handle it. // const MyMoneyPayee& payeeObj = MyMoneyFile::instance()->payee(payeeid); if (statementTransactionUnderImport.m_listSplits.isEmpty() && payeeObj.defaultAccountEnabled()) { MyMoneyAccount splitAccount = file->account(payeeObj.defaultAccountId()); MyMoneySplit s; s.setReconcileFlag(eMyMoney::Split::State::Cleared); s.clearId(); s.setBankID(QString()); s.setShares(-s1.shares()); s.setValue(-s1.value()); s.setAccountId(payeeObj.defaultAccountId()); s.setMemo(transactionUnderImport.memo()); s.setPayeeId(payeeid); d->setupPrice(s, splitAccount, d->m_account, statementTransactionUnderImport.m_datePosted); transactionUnderImport.addSplit(s); - file->addVATSplit(transactionUnderImport, d->m_account, splitAccount, statementTransactionUnderImport.m_amount); + } else if (statementTransactionUnderImport.m_listSplits.isEmpty() && !d->m_skipCategoryMatching) { MyMoneyTransactionFilter filter(thisaccount.id()); filter.addPayee(payeeid); QList list = file->transactionList(filter); if (!list.empty()) { // Default to using the most recent transaction as the reference MyMoneyTransaction t_old = list.last(); // if there is more than one matching transaction, try to be a little // smart about which one we take. for now, we'll see if there's one // with the same VALUE as our imported transaction, and if so take that one. if (list.count() > 1) { QList::ConstIterator it_trans = list.constEnd(); if (it_trans != list.constBegin()) --it_trans; while (it_trans != list.constBegin()) { MyMoneySplit s = (*it_trans).splitByAccount(thisaccount.id()); if (s.value() == s1.value()) { // keep searching if this transaction references a closed account if (!MyMoneyFile::instance()->referencesClosedAccount(*it_trans)) { t_old = *it_trans; break; } } --it_trans; } // check constBegin, just in case if (it_trans == list.constBegin()) { MyMoneySplit s = (*it_trans).splitByAccount(thisaccount.id()); if (s.value() == s1.value()) { t_old = *it_trans; } } } // Only copy the splits if the transaction found does not reference a closed account if (!MyMoneyFile::instance()->referencesClosedAccount(t_old)) { foreach (const auto split, t_old.splits()) { // We don't need the split that covers this account, // we just need the other ones. if (split.accountId() != thisaccount.id()) { MyMoneySplit s(split); s.setReconcileFlag(eMyMoney::Split::State::NotReconciled); s.clearId(); s.setBankID(QString()); s.removeMatch(); + // in case the old transaction has two splits + // we simply inverse the amount of the current + // transaction found in s1. In other cases (more + // than two splits we copy all splits and don't + // modify the splits. This may lead to unbalanced + // transactions which the user has to fix manually if (t_old.splits().count() == 2) { s.setShares(-s1.shares()); s.setValue(-s1.value()); s.setMemo(s1.memo()); } MyMoneyAccount splitAccount = file->account(s.accountId()); qDebug("Adding second split to %s(%s)", qPrintable(splitAccount.name()), qPrintable(s.accountId())); d->setupPrice(s, splitAccount, d->m_account, statementTransactionUnderImport.m_datePosted); transactionUnderImport.addSplit(s); } } } } } } } s1.setReconcileFlag(statementTransactionUnderImport.m_reconcile); // Add the 'account' split if it's needed if (! transfervalue.isZero()) { // in case the transaction has a reference to the brokerage account, we use it // but if brokerageactid has already been set, keep that. if (!statementTransactionUnderImport.m_strBrokerageAccount.isEmpty() && brokerageactid.isEmpty()) { brokerageactid = file->nameToAccount(statementTransactionUnderImport.m_strBrokerageAccount); } if (brokerageactid.isEmpty()) { brokerageactid = file->accountByName(statementTransactionUnderImport.m_strBrokerageAccount).id(); } // There is no BrokerageAccount so have to nowhere to put this split. if (!brokerageactid.isEmpty()) { sBrokerage.setMemo(statementTransactionUnderImport.m_strMemo); sBrokerage.setValue(transfervalue); sBrokerage.setShares(transfervalue); sBrokerage.setAccountId(brokerageactid); sBrokerage.setReconcileFlag(statementTransactionUnderImport.m_reconcile); MyMoneyAccount splitAccount = file->account(sBrokerage.accountId()); d->setupPrice(sBrokerage, splitAccount, d->m_account, statementTransactionUnderImport.m_datePosted); } } if (!(sBrokerage == MyMoneySplit())) transactionUnderImport.addSplit(sBrokerage); if (!(sFees == MyMoneySplit())) transactionUnderImport.addSplit(sFees); if (!(s2 == MyMoneySplit())) transactionUnderImport.addSplit(s2); transactionUnderImport.addSplit(s1); + // check if we need to add/update a VAT assignment + file->updateVAT(transactionUnderImport); + if ((statementTransactionUnderImport.m_eAction != eMyMoney::Transaction::Action::ReinvestDividend) && (statementTransactionUnderImport.m_eAction != eMyMoney::Transaction::Action::CashDividend) && (statementTransactionUnderImport.m_eAction != eMyMoney::Transaction::Action::Interest) ) { //****************************************** // process splits //****************************************** QList::const_iterator it_s; for (it_s = statementTransactionUnderImport.m_listSplits.begin(); it_s != statementTransactionUnderImport.m_listSplits.end(); ++it_s) { MyMoneySplit s3; s3.setAccountId((*it_s).m_accountId); MyMoneyAccount acc = file->account(s3.accountId()); s3.setPayeeId(s1.payeeId()); s3.setMemo((*it_s).m_strMemo); s3.setShares((*it_s).m_amount); s3.setValue((*it_s).m_amount); s3.setReconcileFlag((*it_s).m_reconcile); d->setupPrice(s3, acc, d->m_account, statementTransactionUnderImport.m_datePosted); transactionUnderImport.addSplit(s3); } } // Add the transaction try { // check for matches already stored in the engine TransactionMatchFinder::MatchResult result; TransactionMatcher matcher(thisaccount); d->transactionsCount++; ExistingTransactionMatchFinder existingTrMatchFinder(KMyMoneySettings::matchInterval()); result = existingTrMatchFinder.findMatch(transactionUnderImport, s1); if (result != TransactionMatchFinder::MatchNotFound) { MyMoneyTransaction matchedTransaction = existingTrMatchFinder.getMatchedTransaction(); if (result == TransactionMatchFinder::MatchDuplicate || !matchedTransaction.isImported() || result == TransactionMatchFinder::MatchPrecise) { // don't match with just imported transaction MyMoneySplit matchedSplit = existingTrMatchFinder.getMatchedSplit(); handleMatchingOfExistingTransaction(matcher, matchedTransaction, matchedSplit, transactionUnderImport, s1, result); return; } } addTransaction(transactionUnderImport); ScheduledTransactionMatchFinder scheduledTrMatchFinder(thisaccount, KMyMoneySettings::matchInterval()); result = scheduledTrMatchFinder.findMatch(transactionUnderImport, s1); if (result != TransactionMatchFinder::MatchNotFound) { MyMoneySplit matchedSplit = scheduledTrMatchFinder.getMatchedSplit(); MyMoneySchedule matchedSchedule = scheduledTrMatchFinder.getMatchedSchedule(); handleMatchingOfScheduledTransaction(matcher, matchedSchedule, matchedSplit, transactionUnderImport, s1); return; } } catch (const MyMoneyException &e) { QString message(i18n("Problem adding or matching imported transaction with id '%1': %2", statementTransactionUnderImport.m_strBankID, e.what())); qDebug("%s", qPrintable(message)); int result = KMessageBox::warningContinueCancel(0, message); if (result == KMessageBox::Cancel) throw MYMONEYEXCEPTION_CSTRING("USERABORT"); } } QString MyMoneyStatementReader::SelectBrokerageAccount() { if (d->m_brokerageAccount.id().isEmpty()) { d->m_brokerageAccount.setAccountType(Account::Type::Checkings); if (!m_userAbort) m_userAbort = ! selectOrCreateAccount(Select, d->m_brokerageAccount); } return d->m_brokerageAccount.id(); } bool MyMoneyStatementReader::selectOrCreateAccount(const SelectCreateMode /*mode*/, MyMoneyAccount& account) { bool result = false; MyMoneyFile* file = MyMoneyFile::instance(); QString accountId; // Try to find an existing account in the engine which matches this one. // There are two ways to be a "matching account". The account number can // match the statement account OR the "StatementKey" property can match. // Either way, we'll update the "StatementKey" property for next time. QString accountNumber = account.number(); if (! accountNumber.isEmpty()) { // Get a list of all accounts QList accounts; file->accountList(accounts); // Iterate through them QList::const_iterator it_account = accounts.constBegin(); while (it_account != accounts.constEnd()) { if ( ((*it_account).value("StatementKey") == accountNumber) || ((*it_account).number() == accountNumber) ) { MyMoneyAccount newAccount((*it_account).id(), account); account = newAccount; accountId = (*it_account).id(); break; } ++it_account; } } QString msg = i18n("You have downloaded a statement for the following account:

"); msg += i18n(" - Account Name: %1", account.name()) + "
"; msg += i18n(" - Account Type: %1", MyMoneyAccount::accountTypeToString(account.accountType())) + "
"; msg += i18n(" - Account Number: %1", account.number()) + "
"; msg += "
"; if (!account.name().isEmpty()) { if (!accountId.isEmpty()) msg += i18n("Do you want to import transactions to this account?"); else msg += i18n("KMyMoney cannot determine which of your accounts to use. You can " "create a new account by pressing the Create button " "or select another one manually from the selection box below."); } else { msg += i18n("No account information has been found in the selected statement file. " "Please select an account using the selection box in the dialog or " "create a new account by pressing the Create button."); } eDialogs::Category type; if (account.accountType() == Account::Type::Checkings) { type = eDialogs::Category::checking; } else if (account.accountType() == Account::Type::Savings) { type = eDialogs::Category::savings; } else if (account.accountType() == Account::Type::Investment) { type = eDialogs::Category::investment; } else if (account.accountType() == Account::Type::CreditCard) { type = eDialogs::Category::creditCard; } else { type = static_cast(eDialogs::Category::asset | eDialogs::Category::liability); } QPointer accountSelect = new KAccountSelectDlg(type, "StatementImport", 0); connect(accountSelect, &KAccountSelectDlg::createAccount, this, &MyMoneyStatementReader::slotNewAccount); accountSelect->setHeader(i18n("Import transactions")); accountSelect->setDescription(msg); accountSelect->setAccount(account, accountId); accountSelect->setMode(false); accountSelect->showAbortButton(true); accountSelect->hideQifEntry(); QString accname; bool done = false; while (!done) { if (accountSelect->exec() == QDialog::Accepted && !accountSelect->selectedAccount().isEmpty()) { result = true; done = true; accountId = accountSelect->selectedAccount(); account = file->account(accountId); if (! accountNumber.isEmpty() && account.value("StatementKey") != accountNumber) { account.setValue("StatementKey", accountNumber); MyMoneyFileTransaction ft; try { MyMoneyFile::instance()->modifyAccount(account); ft.commit(); accname = account.name(); } catch (const MyMoneyException &) { qDebug("Updating account in MyMoneyStatementReader::selectOrCreateAccount failed"); } } } else { if (accountSelect->aborted()) //throw MYMONEYEXCEPTION_CSTRING("USERABORT"); done = true; else KMessageBox::error(0, QLatin1String("") + i18n("You must select an account, create a new one, or press the Abort button.") + QLatin1String("")); } } delete accountSelect; return result; } const MyMoneyAccount& MyMoneyStatementReader::account() const { return d->m_account; } void MyMoneyStatementReader::setProgressCallback(void(*callback)(int, int, const QString&)) { m_progressCallback = callback; } void MyMoneyStatementReader::signalProgress(int current, int total, const QString& msg) { if (m_progressCallback != 0) (*m_progressCallback)(current, total, msg); } void MyMoneyStatementReader::handleMatchingOfExistingTransaction(TransactionMatcher & matcher, MyMoneyTransaction matchedTransaction, MyMoneySplit matchedSplit, MyMoneyTransaction & importedTransaction, const MyMoneySplit & importedSplit, const TransactionMatchFinder::MatchResult & matchResult) { switch (matchResult) { case TransactionMatchFinder::MatchNotFound: break; case TransactionMatchFinder::MatchDuplicate: d->transactionsDuplicate++; qDebug("Detected transaction duplicate"); break; case TransactionMatchFinder::MatchImprecise: case TransactionMatchFinder::MatchPrecise: addTransaction(importedTransaction); qDebug("Detected as match to transaction '%s'", qPrintable(matchedTransaction.id())); matcher.match(matchedTransaction, matchedSplit, importedTransaction, importedSplit, true); d->transactionsMatched++; break; } } void MyMoneyStatementReader::handleMatchingOfScheduledTransaction(TransactionMatcher & matcher, MyMoneySchedule matchedSchedule, MyMoneySplit matchedSplit, const MyMoneyTransaction & importedTransaction, const MyMoneySplit & importedSplit) { QPointer editor; if (askUserToEnterScheduleForMatching(matchedSchedule, importedSplit, importedTransaction)) { KEnterScheduleDlg dlg(0, matchedSchedule); editor = dlg.startEdit(); if (editor) { MyMoneyTransaction torig; try { // in case the amounts of the scheduled transaction and the // imported transaction differ, we need to update the amount // using the transaction editor. if (matchedSplit.shares() != importedSplit.shares() && !matchedSchedule.isFixed()) { // for now this only works with regular transactions and not // for investment transactions. As of this, we don't have // scheduled investment transactions anyway. auto se = dynamic_cast(editor.data()); if (se) { // the following call will update the amount field in the // editor and also adjust a possible VAT assignment. Make // sure to use only the absolute value of the amount, because // the editor keeps the sign in a different position (deposit, // withdrawal tab) KMyMoneyEdit* amount = dynamic_cast(se->haveWidget("amount")); if (amount) { amount->setValue(importedSplit.shares().abs()); se->slotUpdateAmount(importedSplit.shares().abs().toString()); // we also need to update the matchedSplit variable to // have the modified share/value. matchedSplit.setShares(importedSplit.shares()); matchedSplit.setValue(importedSplit.value()); } } } editor->createTransaction(torig, dlg.transaction(), dlg.transaction().splits().isEmpty() ? MyMoneySplit() : dlg.transaction().splits().front(), true); QString newId; if (editor->enterTransactions(newId, false, true)) { if (!newId.isEmpty()) { torig = MyMoneyFile::instance()->transaction(newId); matchedSchedule.setLastPayment(torig.postDate()); } matchedSchedule.setNextDueDate(matchedSchedule.nextPayment(matchedSchedule.nextDueDate())); MyMoneyFile::instance()->modifySchedule(matchedSchedule); } // now match the two transactions matcher.match(torig, matchedSplit, importedTransaction, importedSplit); d->transactionsMatched++; } catch (const MyMoneyException &) { // make sure we get rid of the editor before // the KEnterScheduleDlg is destroyed delete editor; throw; // rethrow } } // delete the editor delete editor; } } void MyMoneyStatementReader::addTransaction(MyMoneyTransaction& transaction) { MyMoneyFile* file = MyMoneyFile::instance(); file->addTransaction(transaction); d->transactionsAdded++; } bool MyMoneyStatementReader::askUserToEnterScheduleForMatching(const MyMoneySchedule& matchedSchedule, const MyMoneySplit& importedSplit, const MyMoneyTransaction & importedTransaction) const { QString scheduleName = matchedSchedule.name(); int currencyDenom = d->m_account.fraction(MyMoneyFile::instance()->currency(d->m_account.currencyId())); QString splitValue = importedSplit.value().formatMoney(currencyDenom); QString payeeName = MyMoneyFile::instance()->payee(importedSplit.payeeId()).name(); QString questionMsg = i18n("KMyMoney has found a scheduled transaction which matches an imported transaction.
" "Schedule name: %1
" "Transaction: %2 %3
" "Do you want KMyMoney to enter this schedule now so that the transaction can be matched?", scheduleName, splitValue, payeeName); // check that dates are within user's setting const auto gap = static_cast(qAbs(matchedSchedule.transaction().postDate().toJulianDay() - importedTransaction.postDate().toJulianDay())); if (gap > KMyMoneySettings::matchInterval()) questionMsg = i18np("KMyMoney has found a scheduled transaction which matches an imported transaction.
" "Schedule name: %2
" "Transaction: %3 %4
" "The transaction dates are one day apart.
" "Do you want KMyMoney to enter this schedule now so that the transaction can be matched?", "KMyMoney has found a scheduled transaction which matches an imported transaction.
" "Schedule name: %2
" "Transaction: %3 %4
" "The transaction dates are %1 days apart.
" "Do you want KMyMoney to enter this schedule now so that the transaction can be matched?", gap ,scheduleName, splitValue, payeeName); const int userAnswer = KMessageBox::questionYesNo(0, QLatin1String("") + questionMsg + QLatin1String(""), i18n("Schedule found")); return (userAnswer == KMessageBox::Yes); } void MyMoneyStatementReader::slotNewAccount(const MyMoneyAccount& acc) { auto newAcc = acc; NewAccountWizard::Wizard::newAccount(newAcc); } void MyMoneyStatementReader::clearResultMessages() { globalResultMessages()->clear(); } QStringList MyMoneyStatementReader::resultMessages() { return *globalResultMessages(); } diff --git a/kmymoney/mymoney/mymoneyfile.cpp b/kmymoney/mymoney/mymoneyfile.cpp index 2b4f7ace4..64c381e75 100644 --- a/kmymoney/mymoney/mymoneyfile.cpp +++ b/kmymoney/mymoney/mymoneyfile.cpp @@ -1,3535 +1,3581 @@ /* * Copyright 2000-2003 Michael Edwardes * Copyright 2001-2002 Felix Rodriguez * Copyright 2002-2004 Kevin Tambascio * Copyright 2004-2005 Ace Jones * Copyright 2006-2019 Thomas Baumgart * Copyright 2006 Darren Gould * Copyright 2017-2018 Łukasz Wojniłowicz * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "mymoneyfile.h" #include // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include // ---------------------------------------------------------------------------- // Project Includes #include "mymoneystoragemgr.h" #include "mymoneyinstitution.h" #include "mymoneyaccount.h" #include "mymoneyaccountloan.h" #include "mymoneysecurity.h" #include "mymoneyreport.h" #include "mymoneybalancecache.h" #include "mymoneybudget.h" #include "mymoneyprice.h" #include "mymoneypayee.h" #include "mymoneytag.h" #include "mymoneyschedule.h" #include "mymoneysplit.h" #include "mymoneytransaction.h" #include "mymoneycostcenter.h" #include "mymoneyexception.h" #include "onlinejob.h" #include "storageenums.h" #include "mymoneyenums.h" // include the following line to get a 'cout' for debug purposes // #include using namespace eMyMoney; const QString MyMoneyFile::AccountSeparator = QChar(':'); MyMoneyFile MyMoneyFile::file; typedef QList > BalanceNotifyList; typedef QMap CacheNotifyList; /// @todo make this template based class MyMoneyNotification { public: MyMoneyNotification(File::Mode mode, const MyMoneyTransaction& t) : m_objType(File::Object::Transaction), m_notificationMode(mode), m_id(t.id()) { } MyMoneyNotification(File::Mode mode, const MyMoneyAccount& acc) : m_objType(File::Object::Account), m_notificationMode(mode), m_id(acc.id()) { } MyMoneyNotification(File::Mode mode, const MyMoneyInstitution& institution) : m_objType(File::Object::Institution), m_notificationMode(mode), m_id(institution.id()) { } MyMoneyNotification(File::Mode mode, const MyMoneyPayee& payee) : m_objType(File::Object::Payee), m_notificationMode(mode), m_id(payee.id()) { } MyMoneyNotification(File::Mode mode, const MyMoneyTag& tag) : m_objType(File::Object::Tag), m_notificationMode(mode), m_id(tag.id()) { } MyMoneyNotification(File::Mode mode, const MyMoneySchedule& schedule) : m_objType(File::Object::Schedule), m_notificationMode(mode), m_id(schedule.id()) { } MyMoneyNotification(File::Mode mode, const MyMoneySecurity& security) : m_objType(File::Object::Security), m_notificationMode(mode), m_id(security.id()) { } MyMoneyNotification(File::Mode mode, const onlineJob& job) : m_objType(File::Object::OnlineJob), m_notificationMode(mode), m_id(job.id()) { } File::Object objectType() const { return m_objType; } File::Mode notificationMode() const { return m_notificationMode; } const QString& id() const { return m_id; } protected: MyMoneyNotification(File::Object obj, File::Mode mode, const QString& id) : m_objType(obj), m_notificationMode(mode), m_id(id) {} private: File::Object m_objType; File::Mode m_notificationMode; QString m_id; }; class MyMoneyFile::Private { public: Private() : m_storage(0), m_inTransaction(false) {} ~Private() { delete m_storage; } /** * This method is used to add an id to the list of objects * to be removed from the cache. If id is empty, then nothing is added to the list. * * @param id id of object to be notified * @param reload reload the object (@c true) or not (@c false). The default is @c true * @see attach, detach */ void addCacheNotification(const QString& id, const QDate& date) { if (!id.isEmpty()) m_balanceNotifyList.append(std::make_pair(id, date)); } /** * This method is used to clear the notification list */ void clearCacheNotification() { // reset list to be empty m_balanceNotifyList.clear(); } /** * This method is used to clear all * objects mentioned in m_notificationList from the cache. */ void notify() { foreach (const BalanceNotifyList::value_type & i, m_balanceNotifyList) { m_balanceChangedSet += i.first; if (i.second.isValid()) { m_balanceCache.clear(i.first, i.second); } else { m_balanceCache.clear(i.first); } } clearCacheNotification(); } /** * This method checks if a storage object is attached and * throws and exception if not. */ inline void checkStorage() const { if (m_storage == 0) throw MYMONEYEXCEPTION_CSTRING("No storage object attached to MyMoneyFile"); } /** * This method checks that a transaction has been started with * startTransaction() and throws an exception otherwise. Calls * checkStorage() to make sure a storage object is present and attached. */ void checkTransaction(const char* txt) const { checkStorage(); if (!m_inTransaction) throw MYMONEYEXCEPTION(QString::fromLatin1("No transaction started for %1").arg(QString::fromLatin1(txt))); } void priceChanged(const MyMoneyFile& file, const MyMoneyPrice price) { // get all affected accounts and add them to the m_valueChangedSet QList accList; file.accountList(accList); QList::const_iterator account_it; for (account_it = accList.constBegin(); account_it != accList.constEnd(); ++account_it) { QString currencyId = account_it->currencyId(); if (currencyId != file.baseCurrency().id() && (currencyId == price.from() || currencyId == price.to())) { // this account is not in the base currency and the price affects it's value m_valueChangedSet.insert(account_it->id()); } } } /** * This member points to the storage strategy */ MyMoneyStorageMgr *m_storage; bool m_inTransaction; MyMoneySecurity m_baseCurrency; /** * @brief Cache for MyMoneyObjects * * It is also used to emit the objectAdded() and objectModified() signals. * => If one of these signals is used, you must use this cache. */ MyMoneyPriceList m_priceCache; MyMoneyBalanceCache m_balanceCache; /** * This member keeps a list of account ids to notify * after a single operation is completed. The balance cache * is cleared for that account and all dates on or after * the one supplied. If the date is invalid, the entire * balance cache is cleared for that account. */ BalanceNotifyList m_balanceNotifyList; /** * This member keeps a list of account ids for which * a balanceChanged() signal needs to be emitted when * a set of operations has been committed. * * @sa MyMoneyFile::commitTransaction() */ QSet m_balanceChangedSet; /** * This member keeps a list of account ids for which * a valueChanged() signal needs to be emitted when * a set of operations has been committed. * * @sa MyMoneyFile::commitTransaction() */ QSet m_valueChangedSet; /** * This member keeps the list of changes in the engine * in historical order. The type can be 'added', 'modified' * or removed. */ QList m_changeSet; }; class MyMoneyNotifier { public: MyMoneyNotifier(MyMoneyFile::Private* file) { m_file = file; m_file->clearCacheNotification(); } ~MyMoneyNotifier() { m_file->notify(); } private: MyMoneyFile::Private* m_file; }; MyMoneyFile::MyMoneyFile() : d(new Private) { } MyMoneyFile::~MyMoneyFile() { delete d; } MyMoneyFile::MyMoneyFile(MyMoneyStorageMgr *storage) : d(new Private) { attachStorage(storage); } MyMoneyFile* MyMoneyFile::instance() { return &file; } void MyMoneyFile::attachStorage(MyMoneyStorageMgr* const storage) { if (d->m_storage != 0) throw MYMONEYEXCEPTION_CSTRING("Storage already attached"); if (storage == 0) throw MYMONEYEXCEPTION_CSTRING("Storage must not be 0"); d->m_storage = storage; // force reload of base currency d->m_baseCurrency = MyMoneySecurity(); // and the whole cache d->m_balanceCache.clear(); d->m_priceCache.clear(); // notify application about new data availability emit beginChangeNotification(); emit dataChanged(); emit endChangeNotification(); } void MyMoneyFile::detachStorage(MyMoneyStorageMgr* const /* storage */) { d->m_balanceCache.clear(); d->m_priceCache.clear(); d->m_storage = nullptr; } MyMoneyStorageMgr* MyMoneyFile::storage() const { return d->m_storage; } bool MyMoneyFile::storageAttached() const { return d->m_storage != 0; } void MyMoneyFile::startTransaction() { d->checkStorage(); if (d->m_inTransaction) { throw MYMONEYEXCEPTION_CSTRING("Already started a transaction!"); } d->m_storage->startTransaction(); d->m_inTransaction = true; d->m_changeSet.clear(); } bool MyMoneyFile::hasTransaction() const { return d->m_inTransaction; } void MyMoneyFile::commitTransaction() { d->checkTransaction(Q_FUNC_INFO); // commit the transaction in the storage const auto changed = d->m_storage->commitTransaction(); d->m_inTransaction = false; // collect notifications about removed objects QStringList removedObjects; const auto& set = d->m_changeSet; for (const auto& change : set) { switch (change.notificationMode()) { case File::Mode::Remove: removedObjects += change.id(); break; default: break; } } // inform the outside world about the beginning of notifications emit beginChangeNotification(); // Now it's time to send out some signals to the outside world // First we go through the d->m_changeSet and emit respective // signals about addition, modification and removal of engine objects const auto& changes = d->m_changeSet; for (const auto& change : changes) { switch (change.notificationMode()) { case File::Mode::Remove: emit objectRemoved(change.objectType(), change.id()); // if there is a balance change recorded for this account remove it since the account itself will be removed // this can happen when deleting categories that have transactions and the reassign category feature was used d->m_balanceChangedSet.remove(change.id()); break; case File::Mode::Add: if (!removedObjects.contains(change.id())) { emit objectAdded(change.objectType(), change.id()); } break; case File::Mode::Modify: if (!removedObjects.contains(change.id())) { emit objectModified(change.objectType(), change.id()); } break; } } // we're done with the change set, so we clear it d->m_changeSet.clear(); // now send out the balanceChanged signal for all those // accounts for which we have an indication about a possible // change. const auto& balanceChanges = d->m_balanceChangedSet; for (const auto& id : balanceChanges) { if (!removedObjects.contains(id)) { // if we notify about balance change we don't need to notify about value change // for the same account since a balance change implies a value change d->m_valueChangedSet.remove(id); emit balanceChanged(account(id)); } } d->m_balanceChangedSet.clear(); // now notify about the remaining value changes const auto& m_valueChanges = d->m_valueChangedSet; for (const auto& id : m_valueChanges) { if (!removedObjects.contains(id)) { emit valueChanged(account(id)); } } d->m_valueChangedSet.clear(); // as a last action, send out the global dataChanged signal if (changed) emit dataChanged(); // inform the outside world about the end of notifications emit endChangeNotification(); } void MyMoneyFile::rollbackTransaction() { d->checkTransaction(Q_FUNC_INFO); d->m_storage->rollbackTransaction(); d->m_inTransaction = false; d->m_balanceChangedSet.clear(); d->m_valueChangedSet.clear(); d->m_changeSet.clear(); } void MyMoneyFile::addInstitution(MyMoneyInstitution& institution) { // perform some checks to see that the institution stuff is OK. For // now we assume that the institution must have a name, the ID is not set // and it does not have a parent (MyMoneyFile). if (institution.name().length() == 0 || institution.id().length() != 0) throw MYMONEYEXCEPTION_CSTRING("Not a new institution"); d->checkTransaction(Q_FUNC_INFO); d->m_storage->addInstitution(institution); d->m_changeSet += MyMoneyNotification(File::Mode::Add, institution); } void MyMoneyFile::modifyInstitution(const MyMoneyInstitution& institution) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->modifyInstitution(institution); d->m_changeSet += MyMoneyNotification(File::Mode::Modify, institution); } void MyMoneyFile::modifyTransaction(const MyMoneyTransaction& transaction) { d->checkTransaction(Q_FUNC_INFO); MyMoneyTransaction tCopy(transaction); // now check the splits bool loanAccountAffected = false; const auto splits1 = transaction.splits(); for (const auto& split : splits1) { // the following line will throw an exception if the // account does not exist auto acc = MyMoneyFile::account(split.accountId()); if (acc.id().isEmpty()) throw MYMONEYEXCEPTION_CSTRING("Cannot store split with no account assigned"); if (isStandardAccount(split.accountId())) throw MYMONEYEXCEPTION_CSTRING("Cannot store split referencing standard account"); if (acc.isLoan() && (split.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::Transfer))) loanAccountAffected = true; } // change transfer splits between asset/liability and loan accounts // into amortization splits if (loanAccountAffected) { const auto splits = transaction.splits(); for (const auto& split : splits) { if (split.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::Transfer)) { auto acc = MyMoneyFile::account(split.accountId()); if (acc.isAssetLiability()) { MyMoneySplit s = split; s.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::Amortization)); tCopy.modifySplit(s); } } } } // clear all changed objects from cache MyMoneyNotifier notifier(d); // get the current setting of this transaction MyMoneyTransaction tr = MyMoneyFile::transaction(transaction.id()); // scan the splits again to update notification list // and mark all accounts that are referenced const auto splits2 = tr.splits(); foreach (const auto& split, splits2) d->addCacheNotification(split.accountId(), tr.postDate()); // make sure the value is rounded to the accounts precision fixSplitPrecision(tCopy); // perform modification d->m_storage->modifyTransaction(tCopy); // and mark all accounts that are referenced const auto splits3 = tCopy.splits(); for (const auto& split : splits3) d->addCacheNotification(split.accountId(), tCopy.postDate()); d->m_changeSet += MyMoneyNotification(File::Mode::Modify, transaction); } void MyMoneyFile::modifyAccount(const MyMoneyAccount& _account) { d->checkTransaction(Q_FUNC_INFO); MyMoneyAccount account(_account); auto acc = MyMoneyFile::account(account.id()); // check that for standard accounts only specific parameters are changed if (isStandardAccount(account.id())) { // make sure to use the stuff we found on file account = acc; // and only use the changes that are allowed account.setName(_account.name()); account.setCurrencyId(_account.currencyId()); // now check that it is the same if (!(account == _account)) throw MYMONEYEXCEPTION_CSTRING("Unable to modify the standard account groups"); } if (account.accountType() != acc.accountType() && !account.isLiquidAsset() && !acc.isLiquidAsset()) throw MYMONEYEXCEPTION_CSTRING("Unable to change account type"); // if the account was moved to another institution, we notify // the old one as well as the new one and the structure change if (acc.institutionId() != account.institutionId()) { MyMoneyInstitution inst; if (!acc.institutionId().isEmpty()) { inst = institution(acc.institutionId()); inst.removeAccountId(acc.id()); modifyInstitution(inst); // modifyInstitution updates d->m_changeSet already } if (!account.institutionId().isEmpty()) { inst = institution(account.institutionId()); inst.addAccountId(acc.id()); modifyInstitution(inst); // modifyInstitution updates d->m_changeSet already } } d->m_storage->modifyAccount(account); d->m_changeSet += MyMoneyNotification(File::Mode::Modify, account); } void MyMoneyFile::reparentAccount(MyMoneyAccount &acc, MyMoneyAccount& parent) { d->checkTransaction(Q_FUNC_INFO); // check that it's not one of the standard account groups if (isStandardAccount(acc.id())) throw MYMONEYEXCEPTION_CSTRING("Unable to reparent the standard account groups"); if (acc.accountGroup() == parent.accountGroup() || (acc.accountType() == Account::Type::Income && parent.accountType() == Account::Type::Expense) || (acc.accountType() == Account::Type::Expense && parent.accountType() == Account::Type::Income)) { if (acc.isInvest() && parent.accountType() != Account::Type::Investment) throw MYMONEYEXCEPTION_CSTRING("Unable to reparent Stock to non-investment account"); if (parent.accountType() == Account::Type::Investment && !acc.isInvest()) throw MYMONEYEXCEPTION_CSTRING("Unable to reparent non-stock to investment account"); // keep a notification of the current parent MyMoneyAccount curParent = account(acc.parentAccountId()); d->m_storage->reparentAccount(acc, parent); d->m_changeSet += MyMoneyNotification(File::Mode::Modify, curParent); d->m_changeSet += MyMoneyNotification(File::Mode::Modify, parent); d->m_changeSet += MyMoneyNotification(File::Mode::Modify, acc); } else throw MYMONEYEXCEPTION_CSTRING("Unable to reparent to different account type"); } MyMoneyInstitution MyMoneyFile::institution(const QString& id) const { return d->m_storage->institution(id); } MyMoneyAccount MyMoneyFile::account(const QString& id) const { if (Q_UNLIKELY(id.isEmpty())) // FIXME: Stop requesting accounts with empty id return MyMoneyAccount(); return d->m_storage->account(id); } MyMoneyAccount MyMoneyFile::subAccountByName(const MyMoneyAccount& account, const QString& name) const { static MyMoneyAccount nullAccount; const auto accounts = account.accountList(); for (const auto& acc : accounts) { const auto sacc = MyMoneyFile::account(acc); if (sacc.name().compare(name) == 0) return sacc; } return nullAccount; } MyMoneyAccount MyMoneyFile::accountByName(const QString& name) const { try { return d->m_storage->accountByName(name); } catch (const MyMoneyException &) { } return MyMoneyAccount(); } void MyMoneyFile::removeTransaction(const MyMoneyTransaction& transaction) { d->checkTransaction(Q_FUNC_INFO); // clear all changed objects from cache MyMoneyNotifier notifier(d); // get the engine's idea about this transaction MyMoneyTransaction tr = MyMoneyFile::transaction(transaction.id()); // scan the splits again to update notification list const auto splits = tr.splits(); for (const auto& split : splits) { auto acc = account(split.accountId()); if (acc.isClosed()) throw MYMONEYEXCEPTION(QString::fromLatin1("Cannot remove transaction that references a closed account.")); d->addCacheNotification(split.accountId(), tr.postDate()); //FIXME-ALEX Do I need to add d->addCacheNotification(split.tagList()); ?? } d->m_storage->removeTransaction(transaction); // remove a possible notification of that same object from the changeSet QList::iterator it; for(it = d->m_changeSet.begin(); it != d->m_changeSet.end();) { if((*it).id() == transaction.id()) { it = d->m_changeSet.erase(it); } else { ++it; } } d->m_changeSet += MyMoneyNotification(File::Mode::Remove, transaction); } bool MyMoneyFile::hasActiveSplits(const QString& id) const { d->checkStorage(); return d->m_storage->hasActiveSplits(id); } bool MyMoneyFile::isStandardAccount(const QString& id) const { d->checkStorage(); return d->m_storage->isStandardAccount(id); } void MyMoneyFile::setAccountName(const QString& id, const QString& name) const { d->checkTransaction(Q_FUNC_INFO); auto acc = account(id); d->m_storage->setAccountName(id, name); d->m_changeSet += MyMoneyNotification(File::Mode::Modify, acc); } void MyMoneyFile::removeAccount(const MyMoneyAccount& account) { d->checkTransaction(Q_FUNC_INFO); MyMoneyAccount parent; MyMoneyAccount acc; MyMoneyInstitution institution; // check that the account and its parent exist // this will throw an exception if the id is unknown acc = MyMoneyFile::account(account.id()); parent = MyMoneyFile::account(account.parentAccountId()); if (!acc.institutionId().isEmpty()) institution = MyMoneyFile::institution(acc.institutionId()); // check that it's not one of the standard account groups if (isStandardAccount(account.id())) throw MYMONEYEXCEPTION_CSTRING("Unable to remove the standard account groups"); if (hasActiveSplits(account.id())) { throw MYMONEYEXCEPTION_CSTRING("Unable to remove account with active splits"); } // collect all sub-ordinate accounts for notification const auto accounts = acc.accountList(); for (const auto& id : accounts) d->m_changeSet += MyMoneyNotification(File::Mode::Modify, MyMoneyFile::account(id)); // don't forget the parent and a possible institution if (!institution.id().isEmpty()) { institution.removeAccountId(account.id()); d->m_storage->modifyInstitution(institution); d->m_changeSet += MyMoneyNotification(File::Mode::Modify, institution); } acc.setInstitutionId(QString()); d->m_storage->removeAccount(acc); d->m_balanceCache.clear(acc.id()); d->m_changeSet += MyMoneyNotification(File::Mode::Modify, parent); d->m_changeSet += MyMoneyNotification(File::Mode::Remove, acc); } void MyMoneyFile::removeAccountList(const QStringList& account_list, unsigned int level) { if (level > 100) throw MYMONEYEXCEPTION_CSTRING("Too deep recursion in [MyMoneyFile::removeAccountList]!"); d->checkTransaction(Q_FUNC_INFO); // upon entry, we check that we could proceed with the operation if (!level) { if (!hasOnlyUnusedAccounts(account_list, 0)) { throw MYMONEYEXCEPTION_CSTRING("One or more accounts cannot be removed"); } } // process all accounts in the list and test if they have transactions assigned foreach (const auto sAccount, account_list) { auto a = d->m_storage->account(sAccount); //qDebug() << "Deleting account '"<< a.name() << "'"; // first remove all sub-accounts if (!a.accountList().isEmpty()) { removeAccountList(a.accountList(), level + 1); // then remove account itself, but we first have to get // rid of the account list that is still stored in // the MyMoneyAccount object. Easiest way is to get a fresh copy. a = d->m_storage->account(sAccount); } // make sure to remove the item from the cache removeAccount(a); } } bool MyMoneyFile::hasOnlyUnusedAccounts(const QStringList& account_list, unsigned int level) { if (level > 100) throw MYMONEYEXCEPTION_CSTRING("Too deep recursion in [MyMoneyFile::hasOnlyUnusedAccounts]!"); // process all accounts in the list and test if they have transactions assigned for (const auto& sAccount : account_list) { if (transactionCount(sAccount) != 0) return false; // the current account has a transaction assigned if (!hasOnlyUnusedAccounts(account(sAccount).accountList(), level + 1)) return false; // some sub-account has a transaction assigned } return true; // all subaccounts unused } void MyMoneyFile::removeInstitution(const MyMoneyInstitution& institution) { d->checkTransaction(Q_FUNC_INFO); MyMoneyInstitution inst = MyMoneyFile::institution(institution.id()); bool blocked = signalsBlocked(); blockSignals(true); const auto accounts = inst.accountList(); for (const auto& acc : accounts) { auto a = account(acc); a.setInstitutionId(QString()); modifyAccount(a); d->m_changeSet += MyMoneyNotification(File::Mode::Modify, a); } blockSignals(blocked); d->m_storage->removeInstitution(institution); d->m_changeSet += MyMoneyNotification(File::Mode::Remove, institution); } void MyMoneyFile::createAccount(MyMoneyAccount& newAccount, MyMoneyAccount& parentAccount, MyMoneyAccount& brokerageAccount, MyMoneyMoney openingBal) { // make sure we have a currency. If none is assigned, we assume base currency if (newAccount.currencyId().isEmpty()) newAccount.setCurrencyId(baseCurrency().id()); MyMoneyFileTransaction ft; try { int pos; // check for ':' in the name and use it as separator for a hierarchy while ((pos = newAccount.name().indexOf(MyMoneyFile::AccountSeparator)) != -1) { QString part = newAccount.name().left(pos); QString remainder = newAccount.name().mid(pos + 1); const MyMoneyAccount& existingAccount = subAccountByName(parentAccount, part); if (existingAccount.id().isEmpty()) { newAccount.setName(part); addAccount(newAccount, parentAccount); parentAccount = newAccount; } else { parentAccount = existingAccount; } newAccount.setParentAccountId(QString()); // make sure, there's no parent newAccount.clearId(); // and no id set for adding newAccount.removeAccountIds(); // and no sub-account ids newAccount.setName(remainder); } addAccount(newAccount, parentAccount); // in case of a loan account, we add the initial payment if ((newAccount.accountType() == Account::Type::Loan || newAccount.accountType() == Account::Type::AssetLoan) && !newAccount.value("kmm-loan-payment-acc").isEmpty() && !newAccount.value("kmm-loan-payment-date").isEmpty()) { MyMoneyAccountLoan acc(newAccount); MyMoneyTransaction t; MyMoneySplit a, b; a.setAccountId(acc.id()); - b.setAccountId(acc.value("kmm-loan-payment-acc").toLatin1()); + b.setAccountId(acc.value("kmm-loan-payment-acc")); a.setValue(acc.loanAmount()); if (acc.accountType() == Account::Type::Loan) a.setValue(-a.value()); a.setShares(a.value()); b.setValue(-a.value()); b.setShares(b.value()); a.setMemo(i18n("Loan payout")); b.setMemo(i18n("Loan payout")); t.setPostDate(QDate::fromString(acc.value("kmm-loan-payment-date"), Qt::ISODate)); newAccount.deletePair("kmm-loan-payment-acc"); newAccount.deletePair("kmm-loan-payment-date"); MyMoneyFile::instance()->modifyAccount(newAccount); t.addSplit(a); t.addSplit(b); addTransaction(t); createOpeningBalanceTransaction(newAccount, openingBal); // in case of an investment account we check if we should create // a brokerage account } else if (newAccount.accountType() == Account::Type::Investment && !brokerageAccount.name().isEmpty()) { addAccount(brokerageAccount, parentAccount); // set a link from the investment account to the brokerage account modifyAccount(newAccount); createOpeningBalanceTransaction(brokerageAccount, openingBal); } else createOpeningBalanceTransaction(newAccount, openingBal); ft.commit(); } catch (const MyMoneyException &e) { qWarning("Unable to create account: %s", e.what()); throw; } } void MyMoneyFile::addAccount(MyMoneyAccount& account, MyMoneyAccount& parent) { d->checkTransaction(Q_FUNC_INFO); MyMoneyInstitution institution; // perform some checks to see that the account stuff is OK. For // now we assume that the account must have a name, has no // transaction and sub-accounts and parent account // it's own ID is not set and it does not have a pointer to (MyMoneyFile) if (account.name().length() == 0) throw MYMONEYEXCEPTION_CSTRING("Account has no name"); if (account.id().length() != 0) throw MYMONEYEXCEPTION_CSTRING("New account must have no id"); if (account.accountList().count() != 0) throw MYMONEYEXCEPTION_CSTRING("New account must have no sub-accounts"); if (!account.parentAccountId().isEmpty()) throw MYMONEYEXCEPTION_CSTRING("New account must have no parent-id"); if (account.accountType() == Account::Type::Unknown) throw MYMONEYEXCEPTION_CSTRING("Account has invalid type"); // make sure, that the parent account exists // if not, an exception is thrown. If it exists, // get a copy of the current data auto acc = MyMoneyFile::account(parent.id()); #if 0 // TODO: remove the following code as we now can have multiple accounts // with the same name even in the same hierarchy position of the account tree // // check if the selected name is currently not among the child accounts // if we find one, then return it as the new account QStringList::const_iterator it_a; foreach (const auto accountID, acc.accountList()) { MyMoneyAccount a = MyMoneyFile::account(accountID); if (account.name() == a.name()) { account = a; return; } } #endif // FIXME: make sure, that the parent has the same type // I left it out here because I don't know, if there is // a tight coupling between e.g. checking accounts and the // class asset. It certainly does not make sense to create an // expense account under an income account. Maybe it does, I don't know. // We enforce, that a stock account can never be a parent and // that the parent for a stock account must be an investment. Also, // an investment cannot have another investment account as it's parent if (parent.isInvest()) throw MYMONEYEXCEPTION_CSTRING("Stock account cannot be parent account"); if (account.isInvest() && parent.accountType() != Account::Type::Investment) throw MYMONEYEXCEPTION_CSTRING("Stock account must have investment account as parent "); if (!account.isInvest() && parent.accountType() == Account::Type::Investment) throw MYMONEYEXCEPTION_CSTRING("Investment account can only have stock accounts as children"); // if an institution is set, verify that it exists if (account.institutionId().length() != 0) { // check the presence of the institution. if it // does not exist, an exception is thrown institution = MyMoneyFile::institution(account.institutionId()); } // if we don't have a valid opening date use today if (!account.openingDate().isValid()) { account.setOpeningDate(QDate::currentDate()); } // make sure to set the opening date for categories to a // fixed date (1900-1-1). See #313793 on b.k.o for details if (account.isIncomeExpense()) { account.setOpeningDate(QDate(1900, 1, 1)); } // if we don't have a currency assigned use the base currency if (account.currencyId().isEmpty()) { account.setCurrencyId(baseCurrency().id()); } // make sure the parent id is setup account.setParentAccountId(parent.id()); d->m_storage->addAccount(account); d->m_changeSet += MyMoneyNotification(File::Mode::Add, account); d->m_storage->addAccount(parent, account); d->m_changeSet += MyMoneyNotification(File::Mode::Modify, parent); if (account.institutionId().length() != 0) { institution.addAccountId(account.id()); d->m_storage->modifyInstitution(institution); d->m_changeSet += MyMoneyNotification(File::Mode::Modify, institution); } } MyMoneyTransaction MyMoneyFile::createOpeningBalanceTransaction(const MyMoneyAccount& acc, const MyMoneyMoney& balance) { MyMoneyTransaction t; // if the opening balance is not zero, we need // to create the respective transaction if (!balance.isZero()) { d->checkTransaction(Q_FUNC_INFO); MyMoneySecurity currency = security(acc.currencyId()); MyMoneyAccount openAcc = openingBalanceAccount(currency); if (openAcc.openingDate() > acc.openingDate()) { openAcc.setOpeningDate(acc.openingDate()); modifyAccount(openAcc); } MyMoneySplit s; t.setPostDate(acc.openingDate()); t.setCommodity(acc.currencyId()); s.setAccountId(acc.id()); s.setShares(balance); s.setValue(balance); t.addSplit(s); s.clearId(); s.setAccountId(openAcc.id()); s.setShares(-balance); s.setValue(-balance); t.addSplit(s); addTransaction(t); } return t; } QString MyMoneyFile::openingBalanceTransaction(const MyMoneyAccount& acc) const { QString result; MyMoneySecurity currency = security(acc.currencyId()); MyMoneyAccount openAcc; try { openAcc = openingBalanceAccount(currency); } catch (const MyMoneyException &) { return result; } // Iterate over all the opening balance transactions for this currency MyMoneyTransactionFilter filter; filter.addAccount(openAcc.id()); QList transactions = transactionList(filter); QList::const_iterator it_t = transactions.constBegin(); while (it_t != transactions.constEnd()) { try { // Test whether the transaction also includes a split into // this account (*it_t).splitByAccount(acc.id(), true /*match*/); // If so, we have a winner! result = (*it_t).id(); break; } catch (const MyMoneyException &) { // If not, keep searching ++it_t; } } return result; } MyMoneyAccount MyMoneyFile::openingBalanceAccount(const MyMoneySecurity& security) { if (!security.isCurrency()) throw MYMONEYEXCEPTION_CSTRING("Opening balance for non currencies not supported"); try { return openingBalanceAccount_internal(security); } catch (const MyMoneyException &) { MyMoneyFileTransaction ft; MyMoneyAccount acc; try { acc = createOpeningBalanceAccount(security); ft.commit(); } catch (const MyMoneyException &) { qDebug("Unable to create opening balance account for security %s", qPrintable(security.id())); } return acc; } } MyMoneyAccount MyMoneyFile::openingBalanceAccount(const MyMoneySecurity& security) const { return openingBalanceAccount_internal(security); } MyMoneyAccount MyMoneyFile::openingBalanceAccount_internal(const MyMoneySecurity& security) const { if (!security.isCurrency()) throw MYMONEYEXCEPTION_CSTRING("Opening balance for non currencies not supported"); MyMoneyAccount acc; QList accounts; QList::ConstIterator it; accountList(accounts, equity().accountList(), true); for (it = accounts.constBegin(); it != accounts.constEnd(); ++it) { if (it->value("OpeningBalanceAccount") == QLatin1String("Yes") && it->currencyId() == security.id()) { acc = *it; break; } } if (acc.id().isEmpty()) { for (it = accounts.constBegin(); it != accounts.constEnd(); ++it) { if (it->name().startsWith(MyMoneyFile::openingBalancesPrefix()) && it->currencyId() == security.id()) { acc = *it; break; } } } if (acc.id().isEmpty()) throw MYMONEYEXCEPTION(QString::fromLatin1("No opening balance account for %1").arg(security.tradingSymbol())); return acc; } MyMoneyAccount MyMoneyFile::createOpeningBalanceAccount(const MyMoneySecurity& security) { d->checkTransaction(Q_FUNC_INFO); MyMoneyAccount acc; QList accounts; QList::ConstIterator it; accountList(accounts, equity().accountList(), true); // find present opening balance accounts without containing '(' QString name; QString parentAccountId; QRegExp exp(QString("\\([A-Z]{3}\\)")); for (it = accounts.constBegin(); it != accounts.constEnd(); ++it) { if (it->value("OpeningBalanceAccount") == QLatin1String("Yes") && exp.indexIn(it->name()) == -1) { name = it->name(); parentAccountId = it->parentAccountId(); break; } } if (name.isEmpty()) name = MyMoneyFile::openingBalancesPrefix(); if (security.id() != baseCurrency().id()) { name += QString(" (%1)").arg(security.id()); } acc.setName(name); acc.setAccountType(Account::Type::Equity); acc.setCurrencyId(security.id()); acc.setValue("OpeningBalanceAccount", "Yes"); MyMoneyAccount parent = !parentAccountId.isEmpty() ? account(parentAccountId) : equity(); this->addAccount(acc, parent); return acc; } void MyMoneyFile::addTransaction(MyMoneyTransaction& transaction) { d->checkTransaction(Q_FUNC_INFO); // clear all changed objects from cache MyMoneyNotifier notifier(d); // perform some checks to see that the transaction stuff is OK. For // now we assume that // * no ids are assigned // * the date valid (must not be empty) // * the referenced accounts in the splits exist // first perform all the checks if (!transaction.id().isEmpty()) throw MYMONEYEXCEPTION_CSTRING("Unable to add transaction with id set"); if (!transaction.postDate().isValid()) throw MYMONEYEXCEPTION_CSTRING("Unable to add transaction with invalid postdate"); // now check the splits auto loanAccountAffected = false; const auto splits1 = transaction.splits(); for (const auto& split : splits1) { // the following line will throw an exception if the // account does not exist or is one of the standard accounts auto acc = MyMoneyFile::account(split.accountId()); if (acc.id().isEmpty()) throw MYMONEYEXCEPTION_CSTRING("Cannot add split with no account assigned"); if (acc.isLoan()) loanAccountAffected = true; if (isStandardAccount(split.accountId())) throw MYMONEYEXCEPTION_CSTRING("Cannot add split referencing standard account"); } // change transfer splits between asset/liability and loan accounts // into amortization splits if (loanAccountAffected) { foreach (const auto split, transaction.splits()) { if (split.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::Transfer)) { auto acc = MyMoneyFile::account(split.accountId()); if (acc.isAssetLiability()) { MyMoneySplit s = split; s.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::Amortization)); transaction.modifySplit(s); } } } } // check that we have a commodity if (transaction.commodity().isEmpty()) { transaction.setCommodity(baseCurrency().id()); } // make sure the value is rounded to the accounts precision fixSplitPrecision(transaction); // then add the transaction to the file global pool d->m_storage->addTransaction(transaction); // scan the splits again to update notification list const auto splits2 = transaction.splits(); for (const auto& split : splits2) d->addCacheNotification(split.accountId(), transaction.postDate()); d->m_changeSet += MyMoneyNotification(File::Mode::Add, transaction); } MyMoneyTransaction MyMoneyFile::transaction(const QString& id) const { d->checkStorage(); return d->m_storage->transaction(id); } MyMoneyTransaction MyMoneyFile::transaction(const QString& account, const int idx) const { d->checkStorage(); return d->m_storage->transaction(account, idx); } void MyMoneyFile::addPayee(MyMoneyPayee& payee) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->addPayee(payee); d->m_changeSet += MyMoneyNotification(File::Mode::Add, payee); } MyMoneyPayee MyMoneyFile::payee(const QString& id) const { if (Q_UNLIKELY(id.isEmpty())) return MyMoneyPayee(); return d->m_storage->payee(id); } MyMoneyPayee MyMoneyFile::payeeByName(const QString& name) const { d->checkStorage(); return d->m_storage->payeeByName(name); } void MyMoneyFile::modifyPayee(const MyMoneyPayee& payee) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->modifyPayee(payee); d->m_changeSet += MyMoneyNotification(File::Mode::Modify, payee); } void MyMoneyFile::removePayee(const MyMoneyPayee& payee) { d->checkTransaction(Q_FUNC_INFO); // FIXME we need to make sure, that the payee is not referenced anymore d->m_storage->removePayee(payee); d->m_changeSet += MyMoneyNotification(File::Mode::Remove, payee); } void MyMoneyFile::addTag(MyMoneyTag& tag) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->addTag(tag); d->m_changeSet += MyMoneyNotification(File::Mode::Add, tag); } MyMoneyTag MyMoneyFile::tag(const QString& id) const { return d->m_storage->tag(id); } MyMoneyTag MyMoneyFile::tagByName(const QString& name) const { d->checkStorage(); return d->m_storage->tagByName(name); } void MyMoneyFile::modifyTag(const MyMoneyTag& tag) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->modifyTag(tag); d->m_changeSet += MyMoneyNotification(File::Mode::Modify, tag); } void MyMoneyFile::removeTag(const MyMoneyTag& tag) { d->checkTransaction(Q_FUNC_INFO); // FIXME we need to make sure, that the tag is not referenced anymore d->m_storage->removeTag(tag); d->m_changeSet += MyMoneyNotification(File::Mode::Remove, tag); } void MyMoneyFile::accountList(QList& list, const QStringList& idlist, const bool recursive) const { d->checkStorage(); if (idlist.isEmpty()) { d->m_storage->accountList(list); #if 0 // TODO: I have no idea what this was good for, but it caused the networth report // to show double the numbers so I commented it out (ipwizard, 2008-05-24) if (d->m_storage && (list.isEmpty() || list.size() != d->m_storage->accountCount())) { d->m_storage->accountList(list); d->m_cache.preloadAccount(list); } #endif QList::Iterator it; for (it = list.begin(); it != list.end();) { if (isStandardAccount((*it).id())) { it = list.erase(it); } else { ++it; } } } else { QList::ConstIterator it; QList list_a; d->m_storage->accountList(list_a); for (it = list_a.constBegin(); it != list_a.constEnd(); ++it) { if (!isStandardAccount((*it).id())) { if (idlist.indexOf((*it).id()) != -1) { list.append(*it); if (recursive == true && !(*it).accountList().isEmpty()) { accountList(list, (*it).accountList(), true); } } } } } } QList MyMoneyFile::institutionList() const { return d->m_storage->institutionList(); } // general get functions MyMoneyPayee MyMoneyFile::user() const { d->checkStorage(); return d->m_storage->user(); } // general set functions void MyMoneyFile::setUser(const MyMoneyPayee& user) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->setUser(user); } bool MyMoneyFile::dirty() const { if (!d->m_storage) return false; return d->m_storage->dirty(); } void MyMoneyFile::setDirty() const { d->checkStorage(); d->m_storage->setDirty(); } unsigned int MyMoneyFile::accountCount() const { d->checkStorage(); return d->m_storage->accountCount(); } void MyMoneyFile::ensureDefaultCurrency(MyMoneyAccount& acc) const { if (acc.currencyId().isEmpty()) { if (!baseCurrency().id().isEmpty()) acc.setCurrencyId(baseCurrency().id()); } } MyMoneyAccount MyMoneyFile::liability() const { d->checkStorage(); return account(MyMoneyAccount::stdAccName(eMyMoney::Account::Standard::Liability)); } MyMoneyAccount MyMoneyFile::asset() const { d->checkStorage(); return account(MyMoneyAccount::stdAccName(eMyMoney::Account::Standard::Asset)); } MyMoneyAccount MyMoneyFile::expense() const { d->checkStorage(); return account(MyMoneyAccount::stdAccName(eMyMoney::Account::Standard::Expense)); } MyMoneyAccount MyMoneyFile::income() const { d->checkStorage(); return account(MyMoneyAccount::stdAccName(eMyMoney::Account::Standard::Income)); } MyMoneyAccount MyMoneyFile::equity() const { d->checkStorage(); return account(MyMoneyAccount::stdAccName(eMyMoney::Account::Standard::Equity)); } unsigned int MyMoneyFile::transactionCount(const QString& account) const { d->checkStorage(); return d->m_storage->transactionCount(account); } unsigned int MyMoneyFile::transactionCount() const { return transactionCount(QString()); } QMap MyMoneyFile::transactionCountMap() const { d->checkStorage(); return d->m_storage->transactionCountMap(); } unsigned int MyMoneyFile::institutionCount() const { d->checkStorage(); return d->m_storage->institutionCount(); } MyMoneyMoney MyMoneyFile::balance(const QString& id, const QDate& date) const { if (date.isValid()) { MyMoneyBalanceCacheItem bal = d->m_balanceCache.balance(id, date); if (bal.isValid()) return bal.balance(); } d->checkStorage(); MyMoneyMoney returnValue = d->m_storage->balance(id, date); if (date.isValid()) { d->m_balanceCache.insert(id, date, returnValue); } return returnValue; } MyMoneyMoney MyMoneyFile::balance(const QString& id) const { return balance(id, QDate()); } MyMoneyMoney MyMoneyFile::clearedBalance(const QString &id, const QDate& date) const { MyMoneyMoney cleared; QList list; cleared = balance(id, date); MyMoneyAccount account = this->account(id); MyMoneyMoney factor(1, 1); if (account.accountGroup() == Account::Type::Liability || account.accountGroup() == Account::Type::Equity) factor = -factor; MyMoneyTransactionFilter filter; filter.addAccount(id); filter.setDateFilter(QDate(), date); filter.setReportAllSplits(false); filter.addState((int)TransactionFilter::State::NotReconciled); transactionList(list, filter); for (QList::const_iterator it_t = list.constBegin(); it_t != list.constEnd(); ++it_t) { const QList& splits = (*it_t).splits(); for (QList::const_iterator it_s = splits.constBegin(); it_s != splits.constEnd(); ++it_s) { const MyMoneySplit &split = (*it_s); if (split.accountId() != id) continue; cleared -= split.shares(); } } return cleared * factor; } MyMoneyMoney MyMoneyFile::totalBalance(const QString& id, const QDate& date) const { d->checkStorage(); return d->m_storage->totalBalance(id, date); } MyMoneyMoney MyMoneyFile::totalBalance(const QString& id) const { return totalBalance(id, QDate()); } void MyMoneyFile::warningMissingRate(const QString& fromId, const QString& toId) const { MyMoneySecurity from, to; try { from = security(fromId); to = security(toId); qWarning("Missing price info for conversion from %s to %s", qPrintable(from.name()), qPrintable(to.name())); } catch (const MyMoneyException &e) { qWarning("Missing security caught in MyMoneyFile::warningMissingRate(). %s", e.what()); } } void MyMoneyFile::transactionList(QList >& list, MyMoneyTransactionFilter& filter) const { d->checkStorage(); d->m_storage->transactionList(list, filter); } void MyMoneyFile::transactionList(QList& list, MyMoneyTransactionFilter& filter) const { d->checkStorage(); d->m_storage->transactionList(list, filter); } QList MyMoneyFile::transactionList(MyMoneyTransactionFilter& filter) const { d->checkStorage(); return d->m_storage->transactionList(filter); } QList MyMoneyFile::payeeList() const { return d->m_storage->payeeList(); } QList MyMoneyFile::tagList() const { return d->m_storage->tagList(); } QString MyMoneyFile::accountToCategory(const QString& accountId, bool includeStandardAccounts) const { MyMoneyAccount acc; QString rc; if (!accountId.isEmpty()) { acc = account(accountId); do { if (!rc.isEmpty()) rc = AccountSeparator + rc; rc = acc.name() + rc; acc = account(acc.parentAccountId()); } while (!acc.id().isEmpty() && (includeStandardAccounts || !isStandardAccount(acc.id()))); } return rc; } QString MyMoneyFile::categoryToAccount(const QString& category, Account::Type type) const { QString id; // search the category in the expense accounts and if it is not found, try // to locate it in the income accounts if (type == Account::Type::Unknown || type == Account::Type::Expense) { id = locateSubAccount(MyMoneyFile::instance()->expense(), category); } if ((id.isEmpty() && type == Account::Type::Unknown) || type == Account::Type::Income) { id = locateSubAccount(MyMoneyFile::instance()->income(), category); } return id; } QString MyMoneyFile::categoryToAccount(const QString& category) const { return categoryToAccount(category, Account::Type::Unknown); } QString MyMoneyFile::nameToAccount(const QString& name) const { QString id; // search the category in the asset accounts and if it is not found, try // to locate it in the liability accounts id = locateSubAccount(MyMoneyFile::instance()->asset(), name); if (id.isEmpty()) id = locateSubAccount(MyMoneyFile::instance()->liability(), name); return id; } QString MyMoneyFile::parentName(const QString& name) const { return name.section(AccountSeparator, 0, -2); } QString MyMoneyFile::locateSubAccount(const MyMoneyAccount& base, const QString& category) const { MyMoneyAccount nextBase; QString level, remainder; level = category.section(AccountSeparator, 0, 0); remainder = category.section(AccountSeparator, 1); foreach (const auto sAccount, base.accountList()) { nextBase = account(sAccount); if (nextBase.name() == level) { if (remainder.isEmpty()) { return nextBase.id(); } return locateSubAccount(nextBase, remainder); } } return QString(); } QString MyMoneyFile::value(const QString& key) const { d->checkStorage(); return d->m_storage->value(key); } void MyMoneyFile::setValue(const QString& key, const QString& val) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->setValue(key, val); } void MyMoneyFile::deletePair(const QString& key) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->deletePair(key); } void MyMoneyFile::addSchedule(MyMoneySchedule& sched) { d->checkTransaction(Q_FUNC_INFO); const auto splits = sched.transaction().splits(); for (const auto& split : splits) { // the following line will throw an exception if the // account does not exist or is one of the standard accounts const auto acc = account(split.accountId()); if (acc.id().isEmpty()) throw MYMONEYEXCEPTION_CSTRING("Cannot add split with no account assigned"); if (isStandardAccount(split.accountId())) throw MYMONEYEXCEPTION_CSTRING("Cannot add split referencing standard account"); } d->m_storage->addSchedule(sched); d->m_changeSet += MyMoneyNotification(File::Mode::Add, sched); } void MyMoneyFile::modifySchedule(const MyMoneySchedule& sched) { d->checkTransaction(Q_FUNC_INFO); foreach (const auto split, sched.transaction().splits()) { // the following line will throw an exception if the // account does not exist or is one of the standard accounts auto acc = MyMoneyFile::account(split.accountId()); if (acc.id().isEmpty()) throw MYMONEYEXCEPTION_CSTRING("Cannot store split with no account assigned"); if (isStandardAccount(split.accountId())) throw MYMONEYEXCEPTION_CSTRING("Cannot store split referencing standard account"); } d->m_storage->modifySchedule(sched); d->m_changeSet += MyMoneyNotification(File::Mode::Modify, sched); } void MyMoneyFile::removeSchedule(const MyMoneySchedule& sched) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->removeSchedule(sched); d->m_changeSet += MyMoneyNotification(File::Mode::Remove, sched); } MyMoneySchedule MyMoneyFile::schedule(const QString& id) const { return d->m_storage->schedule(id); } QList MyMoneyFile::scheduleList( const QString& accountId, const Schedule::Type type, const Schedule::Occurrence occurrence, const Schedule::PaymentType paymentType, const QDate& startDate, const QDate& endDate, const bool overdue) const { d->checkStorage(); return d->m_storage->scheduleList(accountId, type, occurrence, paymentType, startDate, endDate, overdue); } QList MyMoneyFile::scheduleList( const QString& accountId) const { return scheduleList(accountId, Schedule::Type::Any, Schedule::Occurrence::Any, Schedule::PaymentType::Any, QDate(), QDate(), false); } QList MyMoneyFile::scheduleList() const { return scheduleList(QString(), Schedule::Type::Any, Schedule::Occurrence::Any, Schedule::PaymentType::Any, QDate(), QDate(), false); } QStringList MyMoneyFile::consistencyCheck() { QList list; QList::Iterator it_a; QList::Iterator it_sch; QList::Iterator it_p; QList::Iterator it_t; QList::Iterator it_r; QStringList accountRebuild; QMap interestAccounts; MyMoneyAccount parent; MyMoneyAccount child; MyMoneyAccount toplevel; QString parentId; QStringList rc; int problemCount = 0; int unfixedCount = 0; QString problemAccount; // check that we have a storage object d->checkTransaction(Q_FUNC_INFO); // get the current list of accounts accountList(list); // add the standard accounts list << MyMoneyFile::instance()->asset(); list << MyMoneyFile::instance()->liability(); list << MyMoneyFile::instance()->income(); list << MyMoneyFile::instance()->expense(); for (it_a = list.begin(); it_a != list.end(); ++it_a) { // no more checks for standard accounts if (isStandardAccount((*it_a).id())) { continue; } switch ((*it_a).accountGroup()) { case Account::Type::Asset: toplevel = asset(); break; case Account::Type::Liability: toplevel = liability(); break; case Account::Type::Expense: toplevel = expense(); break; case Account::Type::Income: toplevel = income(); break; case Account::Type::Equity: toplevel = equity(); break; default: qWarning("%s:%d This should never happen!", __FILE__ , __LINE__); break; } // check for loops in the hierarchy parentId = (*it_a).parentAccountId(); try { bool dropOut = false; while (!isStandardAccount(parentId) && !dropOut) { parent = account(parentId); if (parent.id() == (*it_a).id()) { // parent loops, so we need to re-parent to toplevel account // find parent account in our list problemCount++; QList::Iterator it_b; for (it_b = list.begin(); it_b != list.end(); ++it_b) { if ((*it_b).id() == parent.id()) { if (problemAccount != (*it_a).name()) { problemAccount = (*it_a).name(); rc << i18n("* Problem with account '%1'", problemAccount); rc << i18n(" * Loop detected between this account and account '%1'.", (*it_b).name()); rc << i18n(" Reparenting account '%2' to top level account '%1'.", toplevel.name(), (*it_a).name()); (*it_a).setParentAccountId(toplevel.id()); if (accountRebuild.contains(toplevel.id()) == 0) accountRebuild << toplevel.id(); if (accountRebuild.contains((*it_a).id()) == 0) accountRebuild << (*it_a).id(); dropOut = true; break; } } } } parentId = parent.parentAccountId(); } } catch (const MyMoneyException &) { // if we don't know about a parent, we catch it later } // check that the parent exists parentId = (*it_a).parentAccountId(); try { parent = account(parentId); if ((*it_a).accountGroup() != parent.accountGroup()) { problemCount++; if (problemAccount != (*it_a).name()) { problemAccount = (*it_a).name(); rc << i18n("* Problem with account '%1'", problemAccount); } // the parent belongs to a different group, so we reconnect to the // master group account (asset, liability, etc) to which this account // should belong and update it in the engine. rc << i18n(" * Parent account '%1' belongs to a different group.", parent.name()); rc << i18n(" New parent account is the top level account '%1'.", toplevel.name()); (*it_a).setParentAccountId(toplevel.id()); // make sure to rebuild the sub-accounts of the top account // and the one we removed this account from if (accountRebuild.contains(toplevel.id()) == 0) accountRebuild << toplevel.id(); if (accountRebuild.contains(parent.id()) == 0) accountRebuild << parent.id(); } else if (!parent.accountList().contains((*it_a).id())) { problemCount++; if (problemAccount != (*it_a).name()) { problemAccount = (*it_a).name(); rc << i18n("* Problem with account '%1'", problemAccount); } // parent exists, but does not have a reference to the account rc << i18n(" * Parent account '%1' does not contain '%2' as sub-account.", parent.name(), problemAccount); if (accountRebuild.contains(parent.id()) == 0) accountRebuild << parent.id(); } } catch (const MyMoneyException &) { // apparently, the parent does not exist anymore. we reconnect to the // master group account (asset, liability, etc) to which this account // should belong and update it in the engine. problemCount++; if (problemAccount != (*it_a).name()) { problemAccount = (*it_a).name(); rc << i18n("* Problem with account '%1'", problemAccount); } rc << i18n(" * The parent with id %1 does not exist anymore.", parentId); rc << i18n(" New parent account is the top level account '%1'.", toplevel.name()); (*it_a).setParentAccountId(toplevel.id()); // make sure to rebuild the sub-accounts of the top account if (accountRebuild.contains(toplevel.id()) == 0) accountRebuild << toplevel.id(); } // now check that all the children exist and have the correct type foreach (const auto accountID, (*it_a).accountList()) { // check that the child exists try { child = account(accountID); if (child.parentAccountId() != (*it_a).id()) { throw MYMONEYEXCEPTION_CSTRING("Child account has a different parent"); } } catch (const MyMoneyException &) { problemCount++; if (problemAccount != (*it_a).name()) { problemAccount = (*it_a).name(); rc << i18n("* Problem with account '%1'", problemAccount); } rc << i18n(" * Child account with id %1 does not exist anymore.", accountID); rc << i18n(" The child account list will be reconstructed."); if (accountRebuild.contains((*it_a).id()) == 0) accountRebuild << (*it_a).id(); } } // see if it is a loan account. if so, remember the assigned interest account if ((*it_a).isLoan()) { MyMoneyAccountLoan loan(*it_a); if (!loan.interestAccountId().isEmpty()) { interestAccounts[loan.interestAccountId()] = true; } try { payee(loan.payee()); } catch (const MyMoneyException &) { problemCount++; if (problemAccount != (*it_a).name()) { problemAccount = (*it_a).name(); rc << i18n("* Problem with account '%1'", problemAccount); } rc << i18n(" * The payee with id %1 referenced by the loan does not exist anymore.", loan.payee()); rc << i18n(" The payee will be removed."); // remove the payee - the account will be modified in the engine later (*it_a).deletePair("payee"); } } // check if it is a category and set the date to 1900-01-01 if different if ((*it_a).isIncomeExpense()) { if (((*it_a).openingDate().isValid() == false) || ((*it_a).openingDate() != QDate(1900, 1, 1))) { (*it_a).setOpeningDate(QDate(1900, 1, 1)); } } // check for clear text online password in the online settings if (!(*it_a).onlineBankingSettings().value("password").isEmpty()) { if (problemAccount != (*it_a).name()) { problemAccount = (*it_a).name(); rc << i18n("* Problem with account '%1'", problemAccount); } rc << i18n(" * Older versions of KMyMoney stored an OFX password for this account in cleartext."); rc << i18n(" Please open it in the account editor (Account/Edit account) once and press OK."); rc << i18n(" This will store the password in the KDE wallet and remove the cleartext version."); ++unfixedCount; } // if the account was modified, we need to update it in the engine if (!(d->m_storage->account((*it_a).id()) == (*it_a))) { try { d->m_storage->modifyAccount(*it_a, true); } catch (const MyMoneyException &) { rc << i18n(" * Unable to update account data in engine."); return rc; } } } if (accountRebuild.count() != 0) { rc << i18n("* Reconstructing the child lists for"); } // clear the affected lists for (it_a = list.begin(); it_a != list.end(); ++it_a) { if (accountRebuild.contains((*it_a).id())) { rc << QString(" %1").arg((*it_a).name()); // clear the account list (*it_a).removeAccountIds(); } } // reconstruct the lists for (it_a = list.begin(); it_a != list.end(); ++it_a) { QList::Iterator it; parentId = (*it_a).parentAccountId(); if (accountRebuild.contains(parentId)) { for (it = list.begin(); it != list.end(); ++it) { if ((*it).id() == parentId) { (*it).addAccountId((*it_a).id()); break; } } } } // update the engine objects for (it_a = list.begin(); it_a != list.end(); ++it_a) { if (accountRebuild.contains((*it_a).id())) { try { d->m_storage->modifyAccount(*it_a, true); } catch (const MyMoneyException &) { rc << i18n(" * Unable to update account data for account %1 in engine", (*it_a).name()); } } } // For some reason, files exist with invalid ids. This has been found in the payee id // so we fix them here QList pList = payeeList(); QMappayeeConversionMap; for (it_p = pList.begin(); it_p != pList.end(); ++it_p) { if ((*it_p).id().length() > 7) { // found one of those with an invalid ids // create a new one and store it in the map. MyMoneyPayee payee = (*it_p); payee.clearId(); d->m_storage->addPayee(payee); payeeConversionMap[(*it_p).id()] = payee.id(); rc << i18n(" * Payee %1 recreated with fixed id", payee.name()); ++problemCount; } } // Fix the transactions MyMoneyTransactionFilter filter; filter.setReportAllSplits(false); const auto tList = d->m_storage->transactionList(filter); // Generate the list of interest accounts for (const auto& transaction : tList) { const auto splits = transaction.splits(); for (const auto& split : splits) { if (split.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::Interest)) interestAccounts[split.accountId()] = true; } } QSet supportedAccountTypes; supportedAccountTypes << Account::Type::Checkings << Account::Type::Savings << Account::Type::Cash << Account::Type::CreditCard << Account::Type::Asset << Account::Type::Liability; QSet reportedUnsupportedAccounts; for (const auto& transaction : tList) { MyMoneyTransaction t = transaction; bool tChanged = false; QDate accountOpeningDate; QStringList accountList; const auto splits = t.splits(); foreach (const auto split, splits) { bool sChanged = false; MyMoneySplit s = split; if (payeeConversionMap.find(split.payeeId()) != payeeConversionMap.end()) { s.setPayeeId(payeeConversionMap[s.payeeId()]); sChanged = true; rc << i18n(" * Payee id updated in split of transaction '%1'.", t.id()); ++problemCount; } try { const auto acc = this->account(s.accountId()); // compute the newest opening date of all accounts involved in the transaction // in case the newest opening date is newer than the transaction post date, do one // of the following: // // a) for category and stock accounts: update the opening date of the account // b) for account types where the user cannot modify the opening date through // the UI issue a warning (for each account only once) // c) others will be caught later if (!acc.isIncomeExpense() && !acc.isInvest()) { if (acc.openingDate() > t.postDate()) { if (!accountOpeningDate.isValid() || acc.openingDate() > accountOpeningDate) { accountOpeningDate = acc.openingDate(); } accountList << this->accountToCategory(acc.id()); if (!supportedAccountTypes.contains(acc.accountType()) && !reportedUnsupportedAccounts.contains(acc.id())) { rc << i18n(" * Opening date of Account '%1' cannot be changed to support transaction '%2' post date.", this->accountToCategory(acc.id()), t.id()); reportedUnsupportedAccounts << acc.id(); ++unfixedCount; } } } else { if (acc.openingDate() > t.postDate()) { rc << i18n(" * Transaction '%1' post date '%2' is older than opening date '%4' of account '%3'.", t.id(), t.postDate().toString(Qt::ISODate), this->accountToCategory(acc.id()), acc.openingDate().toString(Qt::ISODate)); rc << i18n(" Account opening date updated."); MyMoneyAccount newAcc = acc; newAcc.setOpeningDate(t.postDate()); this->modifyAccount(newAcc); ++problemCount; } } // make sure, that shares and value have the same number if they // represent the same currency. if (t.commodity() == acc.currencyId() && s.shares().reduce() != s.value().reduce()) { // use the value as master if the transaction is balanced if (t.splitSum().isZero()) { s.setShares(s.value()); rc << i18n(" * shares set to value in split of transaction '%1'.", t.id()); } else { s.setValue(s.shares()); rc << i18n(" * value set to shares in split of transaction '%1'.", t.id()); } sChanged = true; ++problemCount; } } catch (const MyMoneyException &) { rc << i18n(" * Split %2 in transaction '%1' contains a reference to invalid account %3. Please fix manually.", t.id(), split.id(), split.accountId()); ++unfixedCount; } // make sure the interest splits are marked correct as such if (interestAccounts.find(s.accountId()) != interestAccounts.end() && s.action() != MyMoneySplit::actionName(eMyMoney::Split::Action::Interest)) { s.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::Interest)); sChanged = true; rc << i18n(" * action marked as interest in split of transaction '%1'.", t.id()); ++problemCount; } if (sChanged) { tChanged = true; t.modifySplit(s); } } // make sure that the transaction's post date is valid if (!t.postDate().isValid()) { tChanged = true; t.setPostDate(t.entryDate().isValid() ? t.entryDate() : QDate::currentDate()); rc << i18n(" * Transaction '%1' has an invalid post date.", t.id()); rc << i18n(" The post date was updated to '%1'.", QLocale().toString(t.postDate(), QLocale::ShortFormat)); ++problemCount; } // check if the transaction's post date is after the opening date // of all accounts involved in the transaction. In case it is not, // issue a warning with the details about the transaction incl. // the account names and dates involved if (accountOpeningDate.isValid() && t.postDate() < accountOpeningDate) { QDate originalPostDate = t.postDate(); #if 0 // for now we do not activate the logic to move the post date to a later // point in time. This could cause some severe trouble if you have lots // of ancient data collected with older versions of KMyMoney that did not // enforce certain conditions like we do now. t.setPostDate(accountOpeningDate); tChanged = true; // copy the price information for investments to the new date QList::const_iterator it_t; foreach (const auto split, t.splits()) { if ((split.action() != "Buy") && (split.action() != "Reinvest")) { continue; } QString id = split.accountId(); auto acc = this->account(id); MyMoneySecurity sec = this->security(acc.currencyId()); MyMoneyPrice price(acc.currencyId(), sec.tradingCurrency(), t.postDate(), split.price(), "Transaction"); this->addPrice(price); break; } #endif rc << i18n(" * Transaction '%1' has a post date '%2' before one of the referenced account's opening date.", t.id(), QLocale().toString(originalPostDate, QLocale::ShortFormat)); rc << i18n(" Referenced accounts: %1", accountList.join(",")); rc << i18n(" The post date was not updated to '%1'.", QLocale().toString(accountOpeningDate, QLocale::ShortFormat)); ++unfixedCount; } if (tChanged) { d->m_storage->modifyTransaction(t); } } // Fix the schedules QList schList = scheduleList(); for (it_sch = schList.begin(); it_sch != schList.end(); ++it_sch) { MyMoneySchedule sch = (*it_sch); MyMoneyTransaction t = sch.transaction(); auto tChanged = false; foreach (const auto split, t.splits()) { MyMoneySplit s = split; bool sChanged = false; if (payeeConversionMap.find(split.payeeId()) != payeeConversionMap.end()) { s.setPayeeId(payeeConversionMap[s.payeeId()]); sChanged = true; rc << i18n(" * Payee id updated in split of schedule '%1'.", (*it_sch).name()); ++problemCount; } if (!split.value().isZero() && split.shares().isZero()) { s.setShares(s.value()); sChanged = true; rc << i18n(" * Split in scheduled transaction '%1' contained value != 0 and shares == 0.", (*it_sch).name()); rc << i18n(" Shares set to value."); ++problemCount; } // make sure, we don't have a bankid stored with a split in a schedule if (!split.bankID().isEmpty()) { s.setBankID(QString()); sChanged = true; rc << i18n(" * Removed bankid from split in scheduled transaction '%1'.", (*it_sch).name()); ++problemCount; } // make sure, that shares and value have the same number if they // represent the same currency. try { const auto acc = this->account(s.accountId()); if (t.commodity() == acc.currencyId() && s.shares().reduce() != s.value().reduce()) { // use the value as master if the transaction is balanced if (t.splitSum().isZero()) { s.setShares(s.value()); rc << i18n(" * shares set to value in split in schedule '%1'.", (*it_sch).name()); } else { s.setValue(s.shares()); rc << i18n(" * value set to shares in split in schedule '%1'.", (*it_sch).name()); } sChanged = true; ++problemCount; } } catch (const MyMoneyException &) { rc << i18n(" * Split %2 in schedule '%1' contains a reference to invalid account %3. Please fix manually.", (*it_sch).name(), split.id(), split.accountId()); ++unfixedCount; } if (sChanged) { t.modifySplit(s); tChanged = true; } } if (tChanged) { sch.setTransaction(t); d->m_storage->modifySchedule(sch); } } // Fix the reports QList rList = reportList(); for (it_r = rList.begin(); it_r != rList.end(); ++it_r) { MyMoneyReport r = *it_r; QStringList payeeList; (*it_r).payees(payeeList); bool rChanged = false; for (auto it_payee = payeeList.begin(); it_payee != payeeList.end(); ++it_payee) { if (payeeConversionMap.find(*it_payee) != payeeConversionMap.end()) { rc << i18n(" * Payee id updated in report '%1'.", (*it_r).name()); ++problemCount; r.removeReference(*it_payee); r.addPayee(payeeConversionMap[*it_payee]); rChanged = true; } } if (rChanged) { d->m_storage->modifyReport(r); } } // erase old payee ids QMap::Iterator it_m; for (it_m = payeeConversionMap.begin(); it_m != payeeConversionMap.end(); ++it_m) { MyMoneyPayee payee = this->payee(it_m.key()); removePayee(payee); rc << i18n(" * Payee '%1' removed.", payee.id()); ++problemCount; } //look for accounts which have currencies other than the base currency but no price on the opening date //all accounts using base currency are excluded, since that's the base used for foreing currency calculation //thus it is considered as always present //accounts that represent Income/Expense categories are also excluded as price is irrelevant for their //fake opening date since a forex rate is required for all multi-currency transactions //get all currencies in use QStringList currencyList; QList accountForeignCurrency; QList accList; accountList(accList); QList::const_iterator account_it; for (account_it = accList.constBegin(); account_it != accList.constEnd(); ++account_it) { MyMoneyAccount account = *account_it; if (!account.isIncomeExpense() && !currencyList.contains(account.currencyId()) && account.currencyId() != baseCurrency().id() && !account.currencyId().isEmpty()) { //add the currency and the account-currency pair currencyList.append(account.currencyId()); accountForeignCurrency.append(account); } } MyMoneyPriceList pricesList = priceList(); QMap securityPriceDate; //get the first date of the price for each security MyMoneyPriceList::const_iterator prices_it; for (prices_it = pricesList.constBegin(); prices_it != pricesList.constEnd(); ++prices_it) { MyMoneyPrice firstPrice = (*((*prices_it).constBegin())); //only check the price if the currency is in use if (currencyList.contains(firstPrice.from()) || currencyList.contains(firstPrice.to())) { //check the security in the from field //if it is there, check if it is older QPair pricePair = qMakePair(firstPrice.from(), firstPrice.to()); securityPriceDate[pricePair] = firstPrice.date(); } } //compare the dates with the opening dates of the accounts using each currency QList::const_iterator accForeignList_it; bool firstInvProblem = true; for (accForeignList_it = accountForeignCurrency.constBegin(); accForeignList_it != accountForeignCurrency.constEnd(); ++accForeignList_it) { //setup the price pair correctly QPair pricePair; //setup the reverse, which can also be used for rate conversion QPair reversePricePair; if ((*accForeignList_it).isInvest()) { //if it is a stock, we have to search for a price from its stock to the currency of the account QString securityId = (*accForeignList_it).currencyId(); QString tradingCurrencyId = security(securityId).tradingCurrency(); pricePair = qMakePair(securityId, tradingCurrencyId); reversePricePair = qMakePair(tradingCurrencyId, securityId); } else { //if it is a regular account we search for a price from the currency of the account to the base currency QString currency = (*accForeignList_it).currencyId(); QString baseCurrencyId = baseCurrency().id(); pricePair = qMakePair(currency, baseCurrencyId); reversePricePair = qMakePair(baseCurrencyId, currency); } //compare the first price with the opening date of the account if ((!securityPriceDate.contains(pricePair) || securityPriceDate.value(pricePair) > (*accForeignList_it).openingDate()) && (!securityPriceDate.contains(reversePricePair) || securityPriceDate.value(reversePricePair) > (*accForeignList_it).openingDate())) { if (firstInvProblem) { firstInvProblem = false; rc << i18n("* Potential problem with investments/currencies"); } QDate openingDate = (*accForeignList_it).openingDate(); MyMoneySecurity secError = security((*accForeignList_it).currencyId()); if (!(*accForeignList_it).isInvest()) { rc << i18n(" * The account '%1' in currency '%2' has no price set for the opening date '%3'.", (*accForeignList_it).name(), secError.name(), openingDate.toString(Qt::ISODate)); rc << i18n(" Please enter a price for the currency on or before the opening date."); } else { rc << i18n(" * The investment '%1' has no price set for the opening date '%2'.", (*accForeignList_it).name(), openingDate.toString(Qt::ISODate)); rc << i18n(" Please enter a price for the investment on or before the opening date."); } ++unfixedCount; } } // Fix the budgets that somehow still reference invalid accounts QString problemBudget; QList bList = budgetList(); for (QList::const_iterator it_b = bList.constBegin(); it_b != bList.constEnd(); ++it_b) { MyMoneyBudget b = *it_b; QList baccounts = b.getaccounts(); bool bChanged = false; for (QList::const_iterator it_bacc = baccounts.constBegin(); it_bacc != baccounts.constEnd(); ++it_bacc) { try { account((*it_bacc).id()); } catch (const MyMoneyException &) { problemCount++; if (problemBudget != b.name()) { problemBudget = b.name(); rc << i18n("* Problem with budget '%1'", problemBudget); } rc << i18n(" * The account with id %1 referenced by the budget does not exist anymore.", (*it_bacc).id()); rc << i18n(" The account reference will be removed."); // remove the reference to the account b.removeReference((*it_bacc).id()); bChanged = true; } } if (bChanged) { d->m_storage->modifyBudget(b); } } // add more checks here if (problemCount == 0 && unfixedCount == 0) { rc << i18n("Finished: data is consistent."); } else { const QString problemsCorrected = i18np("%1 problem corrected.", "%1 problems corrected.", problemCount); const QString problemsRemaining = i18np("%1 problem still present.", "%1 problems still present.", unfixedCount); rc << QString(); 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); } return rc; } QString MyMoneyFile::createCategory(const MyMoneyAccount& base, const QString& name) { d->checkTransaction(Q_FUNC_INFO); MyMoneyAccount parent = base; QString categoryText; if (base.id() != expense().id() && base.id() != income().id()) throw MYMONEYEXCEPTION_CSTRING("Invalid base category"); QStringList subAccounts = name.split(AccountSeparator); QStringList::Iterator it; for (it = subAccounts.begin(); it != subAccounts.end(); ++it) { MyMoneyAccount categoryAccount; categoryAccount.setName(*it); categoryAccount.setAccountType(base.accountType()); if (it == subAccounts.begin()) categoryText += *it; else categoryText += (AccountSeparator + *it); // Only create the account if it doesn't exist try { QString categoryId = categoryToAccount(categoryText); if (categoryId.isEmpty()) addAccount(categoryAccount, parent); else { categoryAccount = account(categoryId); } } catch (const MyMoneyException &e) { qDebug("Unable to add account %s, %s, %s: %s", qPrintable(categoryAccount.name()), qPrintable(parent.name()), qPrintable(categoryText), e.what()); } parent = categoryAccount; } return categoryToAccount(name); } QString MyMoneyFile::checkCategory(const QString& name, const MyMoneyMoney& value, const MyMoneyMoney& value2) { QString accountId; MyMoneyAccount newAccount; bool found = true; if (!name.isEmpty()) { // The category might be constructed with an arbitraty depth (number of // colon delimited fields). We try to find a parent account within this // hierarchy by searching the following sequence: // // aaaa:bbbb:cccc:ddddd // // 1. search aaaa:bbbb:cccc:dddd, create nothing // 2. search aaaa:bbbb:cccc , create dddd // 3. search aaaa:bbbb , create cccc:dddd // 4. search aaaa , create bbbb:cccc:dddd // 5. don't search , create aaaa:bbbb:cccc:dddd newAccount.setName(name); QString accName; // part to be created (right side in above list) QString parent(name); // a possible parent part (left side in above list) do { accountId = categoryToAccount(parent); if (accountId.isEmpty()) { found = false; // prepare next step if (!accName.isEmpty()) accName.prepend(':'); accName.prepend(parent.section(':', -1)); newAccount.setName(accName); parent = parent.section(':', 0, -2); } else if (!accName.isEmpty()) { newAccount.setParentAccountId(accountId); } } while (!parent.isEmpty() && accountId.isEmpty()); // if we did not find the category, we create it if (!found) { MyMoneyAccount parentAccount; if (newAccount.parentAccountId().isEmpty()) { if (!value.isNegative() && value2.isNegative()) parentAccount = income(); else parentAccount = expense(); } else { parentAccount = account(newAccount.parentAccountId()); } newAccount.setAccountType((!value.isNegative() && value2.isNegative()) ? Account::Type::Income : Account::Type::Expense); MyMoneyAccount brokerage; // clear out the parent id, because createAccount() does not like that newAccount.setParentAccountId(QString()); createAccount(newAccount, parentAccount, brokerage, MyMoneyMoney()); accountId = newAccount.id(); } } return accountId; } void MyMoneyFile::addSecurity(MyMoneySecurity& security) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->addSecurity(security); d->m_changeSet += MyMoneyNotification(File::Mode::Add, security); } void MyMoneyFile::modifySecurity(const MyMoneySecurity& security) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->modifySecurity(security); d->m_changeSet += MyMoneyNotification(File::Mode::Modify, security); } void MyMoneyFile::removeSecurity(const MyMoneySecurity& security) { d->checkTransaction(Q_FUNC_INFO); // FIXME check that security is not referenced by other object d->m_storage->removeSecurity(security); d->m_changeSet += MyMoneyNotification(File::Mode::Remove, security); } MyMoneySecurity MyMoneyFile::security(const QString& id) const { if (Q_UNLIKELY(id.isEmpty())) return baseCurrency(); return d->m_storage->security(id); } QList MyMoneyFile::securityList() const { d->checkStorage(); return d->m_storage->securityList(); } void MyMoneyFile::addCurrency(const MyMoneySecurity& currency) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->addCurrency(currency); d->m_changeSet += MyMoneyNotification(File::Mode::Add, currency); } void MyMoneyFile::modifyCurrency(const MyMoneySecurity& currency) { d->checkTransaction(Q_FUNC_INFO); // force reload of base currency object if (currency.id() == d->m_baseCurrency.id()) d->m_baseCurrency.clearId(); d->m_storage->modifyCurrency(currency); d->m_changeSet += MyMoneyNotification(File::Mode::Modify, currency); } void MyMoneyFile::removeCurrency(const MyMoneySecurity& currency) { d->checkTransaction(Q_FUNC_INFO); if (currency.id() == d->m_baseCurrency.id()) throw MYMONEYEXCEPTION_CSTRING("Cannot delete base currency."); // FIXME check that security is not referenced by other object d->m_storage->removeCurrency(currency); d->m_changeSet += MyMoneyNotification(File::Mode::Remove, currency); } MyMoneySecurity MyMoneyFile::currency(const QString& id) const { if (id.isEmpty()) return baseCurrency(); try { const auto currency = d->m_storage->currency(id); if (currency.id().isEmpty()) throw MYMONEYEXCEPTION(QString::fromLatin1("Currency '%1' not found.").arg(id)); return currency; } catch (const MyMoneyException &) { const auto security = d->m_storage->security(id); if (security.id().isEmpty()) { throw MYMONEYEXCEPTION(QString::fromLatin1("Security '%1' not found.").arg(id)); } return security; } } QMap MyMoneyFile::ancientCurrencies() const { QMap ancientCurrencies; ancientCurrencies.insert(MyMoneySecurity("ATS", i18n("Austrian Schilling"), QString::fromUtf8("ÖS")), MyMoneyPrice("ATS", "EUR", QDate(1998, 12, 31), MyMoneyMoney(10000, 137603), QLatin1Literal("KMyMoney"))); ancientCurrencies.insert(MyMoneySecurity("DEM", i18n("German Mark"), "DM"), MyMoneyPrice("ATS", "EUR", QDate(1998, 12, 31), MyMoneyMoney(100000, 195583), QLatin1Literal("KMyMoney"))); ancientCurrencies.insert(MyMoneySecurity("FRF", i18n("French Franc"), "FF"), MyMoneyPrice("FRF", "EUR", QDate(1998, 12, 31), MyMoneyMoney(100000, 655957), QLatin1Literal("KMyMoney"))); ancientCurrencies.insert(MyMoneySecurity("ITL", i18n("Italian Lira"), QChar(0x20A4)), MyMoneyPrice("ITL", "EUR", QDate(1998, 12, 31), MyMoneyMoney(100, 193627), QLatin1Literal("KMyMoney"))); ancientCurrencies.insert(MyMoneySecurity("ESP", i18n("Spanish Peseta"), QString()), MyMoneyPrice("ESP", "EUR", QDate(1998, 12, 31), MyMoneyMoney(1000, 166386), QLatin1Literal("KMyMoney"))); ancientCurrencies.insert(MyMoneySecurity("NLG", i18n("Dutch Guilder"), QString()), MyMoneyPrice("NLG", "EUR", QDate(1998, 12, 31), MyMoneyMoney(100000, 220371), QLatin1Literal("KMyMoney"))); ancientCurrencies.insert(MyMoneySecurity("BEF", i18n("Belgian Franc"), "Fr"), MyMoneyPrice("BEF", "EUR", QDate(1998, 12, 31), MyMoneyMoney(10000, 403399), QLatin1Literal("KMyMoney"))); ancientCurrencies.insert(MyMoneySecurity("LUF", i18n("Luxembourg Franc"), "Fr"), MyMoneyPrice("LUF", "EUR", QDate(1998, 12, 31), MyMoneyMoney(10000, 403399), QLatin1Literal("KMyMoney"))); ancientCurrencies.insert(MyMoneySecurity("PTE", i18n("Portuguese Escudo"), QString()), MyMoneyPrice("PTE", "EUR", QDate(1998, 12, 31), MyMoneyMoney(1000, 200482), QLatin1Literal("KMyMoney"))); ancientCurrencies.insert(MyMoneySecurity("IEP", i18n("Irish Pound"), QChar(0x00A3)), MyMoneyPrice("IEP", "EUR", QDate(1998, 12, 31), MyMoneyMoney(1000000, 787564), QLatin1Literal("KMyMoney"))); ancientCurrencies.insert(MyMoneySecurity("FIM", i18n("Finnish Markka"), QString()), MyMoneyPrice("FIM", "EUR", QDate(1998, 12, 31), MyMoneyMoney(100000, 594573), QLatin1Literal("KMyMoney"))); ancientCurrencies.insert(MyMoneySecurity("GRD", i18n("Greek Drachma"), QChar(0x20AF)), MyMoneyPrice("GRD", "EUR", QDate(1998, 12, 31), MyMoneyMoney(100, 34075), QLatin1Literal("KMyMoney"))); // http://en.wikipedia.org/wiki/Bulgarian_lev ancientCurrencies.insert(MyMoneySecurity("BGL", i18n("Bulgarian Lev"), "BGL"), MyMoneyPrice("BGL", "BGN", QDate(1999, 7, 5), MyMoneyMoney(1, 1000), QLatin1Literal("KMyMoney"))); ancientCurrencies.insert(MyMoneySecurity("ROL", i18n("Romanian Leu"), "ROL"), MyMoneyPrice("ROL", "RON", QDate(2005, 6, 30), MyMoneyMoney(1, 10000), QLatin1Literal("KMyMoney"))); ancientCurrencies.insert(MyMoneySecurity("RUR", i18n("Russian Ruble (old)"), "RUR"), MyMoneyPrice("RUR", "RUB", QDate(1998, 1, 1), MyMoneyMoney(1, 1000), QLatin1Literal("KMyMoney"))); ancientCurrencies.insert(MyMoneySecurity("SIT", i18n("Slovenian Tolar"), "SIT"), MyMoneyPrice("SIT", "EUR", QDate(2006, 12, 31), MyMoneyMoney(1, 23964), QLatin1Literal("KMyMoney"))); // Source: http://www.tf-portfoliosolutions.net/products/turkishlira.aspx ancientCurrencies.insert(MyMoneySecurity("TRL", i18n("Turkish Lira (old)"), "TL"), MyMoneyPrice("TRL", "TRY", QDate(2004, 12, 31), MyMoneyMoney(1, 1000000), QLatin1Literal("KMyMoney"))); // Source: http://www.focus.de/finanzen/news/malta-und-zypern_aid_66058.html ancientCurrencies.insert(MyMoneySecurity("MTL", i18n("Maltese Lira"), "MTL"), MyMoneyPrice("MTL", "EUR", QDate(2008, 1, 1), MyMoneyMoney(429300, 1000000), QLatin1Literal("KMyMoney"))); ancientCurrencies.insert(MyMoneySecurity("CYP", i18n("Cyprus Pound"), QString("C%1").arg(QChar(0x00A3))), MyMoneyPrice("CYP", "EUR", QDate(2008, 1, 1), MyMoneyMoney(585274, 1000000), QLatin1Literal("KMyMoney"))); // Source: http://www.focus.de/finanzen/news/waehrungszone-slowakei-ist-neuer-euro-staat_aid_359025.html ancientCurrencies.insert(MyMoneySecurity("SKK", i18n("Slovak Koruna"), "SKK"), MyMoneyPrice("SKK", "EUR", QDate(2008, 12, 31), MyMoneyMoney(1000, 30126), QLatin1Literal("KMyMoney"))); // Source: http://en.wikipedia.org/wiki/Mozambican_metical ancientCurrencies.insert(MyMoneySecurity("MZM", i18n("Mozambique Metical"), "MT"), MyMoneyPrice("MZM", "MZN", QDate(2006, 7, 1), MyMoneyMoney(1, 1000), QLatin1Literal("KMyMoney"))); // Source https://en.wikipedia.org/wiki/Azerbaijani_manat ancientCurrencies.insert(MyMoneySecurity("AZM", i18n("Azerbaijani Manat"), "m."), MyMoneyPrice("AZM", "AZN", QDate(2006, 1, 1), MyMoneyMoney(1, 5000), QLatin1Literal("KMyMoney"))); // Source: https://en.wikipedia.org/wiki/Litas ancientCurrencies.insert(MyMoneySecurity("LTL", i18n("Lithuanian Litas"), "Lt"), MyMoneyPrice("LTL", "EUR", QDate(2015, 1, 1), MyMoneyMoney(100000, 345280), QLatin1Literal("KMyMoney"))); // Source: https://en.wikipedia.org/wiki/Belarusian_ruble ancientCurrencies.insert(MyMoneySecurity("BYR", i18n("Belarusian Ruble (old)"), "BYR"), MyMoneyPrice("BYR", "BYN", QDate(2016, 7, 1), MyMoneyMoney(1, 10000), QLatin1Literal("KMyMoney"))); return ancientCurrencies; } QList MyMoneyFile::availableCurrencyList() const { QList currencyList; currencyList.append(MyMoneySecurity("AFA", i18n("Afghanistan Afghani"))); currencyList.append(MyMoneySecurity("ALL", i18n("Albanian Lek"))); currencyList.append(MyMoneySecurity("ANG", i18n("Netherland Antillian Guilder"))); currencyList.append(MyMoneySecurity("DZD", i18n("Algerian Dinar"))); currencyList.append(MyMoneySecurity("ADF", i18n("Andorran Franc"))); currencyList.append(MyMoneySecurity("ADP", i18n("Andorran Peseta"))); currencyList.append(MyMoneySecurity("AON", i18n("Angolan New Kwanza"))); currencyList.append(MyMoneySecurity("ARS", i18n("Argentine Peso"), "$")); currencyList.append(MyMoneySecurity("AWG", i18n("Aruban Florin"))); currencyList.append(MyMoneySecurity("AUD", i18n("Australian Dollar"), "$")); currencyList.append(MyMoneySecurity("AZN", i18n("Azerbaijani Manat"), "m.")); currencyList.append(MyMoneySecurity("BSD", i18n("Bahamian Dollar"), "$")); currencyList.append(MyMoneySecurity("BHD", i18n("Bahraini Dinar"), "BHD", 1000)); currencyList.append(MyMoneySecurity("BDT", i18n("Bangladeshi Taka"))); currencyList.append(MyMoneySecurity("BBD", i18n("Barbados Dollar"), "$")); currencyList.append(MyMoneySecurity("BTC", i18n("Bitcoin"), "BTC")); currencyList.append(MyMoneySecurity("BYN", i18n("Belarusian Ruble"), "Br")); currencyList.append(MyMoneySecurity("BZD", i18n("Belize Dollar"), "$")); currencyList.append(MyMoneySecurity("BMD", i18n("Bermudian Dollar"), "$")); currencyList.append(MyMoneySecurity("BTN", i18n("Bhutan Ngultrum"))); currencyList.append(MyMoneySecurity("BOB", i18n("Bolivian Boliviano"))); currencyList.append(MyMoneySecurity("BAM", i18n("Bosnian Convertible Mark"))); currencyList.append(MyMoneySecurity("BWP", i18n("Botswana Pula"))); currencyList.append(MyMoneySecurity("BRL", i18n("Brazilian Real"), "R$")); currencyList.append(MyMoneySecurity("GBP", i18n("British Pound"), QChar(0x00A3))); currencyList.append(MyMoneySecurity("BND", i18n("Brunei Dollar"), "$")); currencyList.append(MyMoneySecurity("BGN", i18n("Bulgarian Lev (new)"))); currencyList.append(MyMoneySecurity("BIF", i18n("Burundi Franc"))); currencyList.append(MyMoneySecurity("XAF", i18n("CFA Franc BEAC"))); currencyList.append(MyMoneySecurity("XOF", i18n("CFA Franc BCEAO"))); currencyList.append(MyMoneySecurity("XPF", i18n("CFP Franc Pacifique"), "F", 1, 100)); currencyList.append(MyMoneySecurity("KHR", i18n("Cambodia Riel"))); currencyList.append(MyMoneySecurity("CAD", i18n("Canadian Dollar"), "$")); currencyList.append(MyMoneySecurity("CVE", i18n("Cape Verde Escudo"))); currencyList.append(MyMoneySecurity("KYD", i18n("Cayman Islands Dollar"), "$")); currencyList.append(MyMoneySecurity("CLP", i18n("Chilean Peso"))); currencyList.append(MyMoneySecurity("CNY", i18n("Chinese Yuan Renminbi"))); currencyList.append(MyMoneySecurity("COP", i18n("Colombian Peso"))); currencyList.append(MyMoneySecurity("KMF", i18n("Comoros Franc"))); currencyList.append(MyMoneySecurity("CRC", i18n("Costa Rican Colon"), QChar(0x20A1))); currencyList.append(MyMoneySecurity("HRK", i18n("Croatian Kuna"))); currencyList.append(MyMoneySecurity("CUP", i18n("Cuban Peso"))); currencyList.append(MyMoneySecurity("CUC", i18n("Cuban Convertible Peso"))); currencyList.append(MyMoneySecurity("CZK", i18n("Czech Koruna"))); currencyList.append(MyMoneySecurity("DKK", i18n("Danish Krone"), "kr")); currencyList.append(MyMoneySecurity("DJF", i18n("Djibouti Franc"))); currencyList.append(MyMoneySecurity("DOP", i18n("Dominican Peso"))); currencyList.append(MyMoneySecurity("XCD", i18n("East Caribbean Dollar"), "$")); currencyList.append(MyMoneySecurity("EGP", i18n("Egyptian Pound"), QChar(0x00A3))); currencyList.append(MyMoneySecurity("SVC", i18n("El Salvador Colon"))); currencyList.append(MyMoneySecurity("ERN", i18n("Eritrean Nakfa"))); currencyList.append(MyMoneySecurity("EEK", i18n("Estonian Kroon"))); currencyList.append(MyMoneySecurity("ETB", i18n("Ethiopian Birr"))); currencyList.append(MyMoneySecurity("EUR", i18n("Euro"), QChar(0x20ac))); currencyList.append(MyMoneySecurity("FKP", i18n("Falkland Islands Pound"), QChar(0x00A3))); currencyList.append(MyMoneySecurity("FJD", i18n("Fiji Dollar"), "$")); currencyList.append(MyMoneySecurity("GMD", i18n("Gambian Dalasi"))); currencyList.append(MyMoneySecurity("GEL", i18n("Georgian Lari"))); currencyList.append(MyMoneySecurity("GHC", i18n("Ghanaian Cedi"))); currencyList.append(MyMoneySecurity("GIP", i18n("Gibraltar Pound"), QChar(0x00A3))); currencyList.append(MyMoneySecurity("GTQ", i18n("Guatemalan Quetzal"))); currencyList.append(MyMoneySecurity("GWP", i18n("Guinea-Bissau Peso"))); currencyList.append(MyMoneySecurity("GYD", i18n("Guyanan Dollar"), "$")); currencyList.append(MyMoneySecurity("HTG", i18n("Haitian Gourde"))); currencyList.append(MyMoneySecurity("HNL", i18n("Honduran Lempira"))); currencyList.append(MyMoneySecurity("HKD", i18n("Hong Kong Dollar"), "$")); currencyList.append(MyMoneySecurity("HUF", i18n("Hungarian Forint"), "HUF", 1, 100)); currencyList.append(MyMoneySecurity("ISK", i18n("Iceland Krona"))); currencyList.append(MyMoneySecurity("INR", i18n("Indian Rupee"), QChar(0x20A8))); currencyList.append(MyMoneySecurity("IDR", i18n("Indonesian Rupiah"), "IDR", 1)); currencyList.append(MyMoneySecurity("IRR", i18n("Iranian Rial"), "IRR", 1)); currencyList.append(MyMoneySecurity("IQD", i18n("Iraqi Dinar"), "IQD", 1000)); currencyList.append(MyMoneySecurity("ILS", i18n("Israeli New Shekel"), QChar(0x20AA))); currencyList.append(MyMoneySecurity("JMD", i18n("Jamaican Dollar"), "$")); currencyList.append(MyMoneySecurity("JPY", i18n("Japanese Yen"), QChar(0x00A5), 1)); currencyList.append(MyMoneySecurity("JOD", i18n("Jordanian Dinar"), "JOD", 1000)); currencyList.append(MyMoneySecurity("KZT", i18n("Kazakhstan Tenge"))); currencyList.append(MyMoneySecurity("KES", i18n("Kenyan Shilling"))); currencyList.append(MyMoneySecurity("KWD", i18n("Kuwaiti Dinar"), "KWD", 1000)); currencyList.append(MyMoneySecurity("KGS", i18n("Kyrgyzstan Som"))); currencyList.append(MyMoneySecurity("LAK", i18n("Laos Kip"), QChar(0x20AD))); currencyList.append(MyMoneySecurity("LVL", i18n("Latvian Lats"))); currencyList.append(MyMoneySecurity("LBP", i18n("Lebanese Pound"), QChar(0x00A3))); currencyList.append(MyMoneySecurity("LSL", i18n("Lesotho Loti"))); currencyList.append(MyMoneySecurity("LRD", i18n("Liberian Dollar"), "$")); currencyList.append(MyMoneySecurity("LYD", i18n("Libyan Dinar"), "LYD", 1000)); currencyList.append(MyMoneySecurity("MOP", i18n("Macau Pataca"))); currencyList.append(MyMoneySecurity("MKD", i18n("Macedonian Denar"))); currencyList.append(MyMoneySecurity("MGF", i18n("Malagasy Franc"), "MGF", 500)); currencyList.append(MyMoneySecurity("MWK", i18n("Malawi Kwacha"))); currencyList.append(MyMoneySecurity("MYR", i18n("Malaysian Ringgit"))); currencyList.append(MyMoneySecurity("MVR", i18n("Maldive Rufiyaa"))); currencyList.append(MyMoneySecurity("MLF", i18n("Mali Republic Franc"))); currencyList.append(MyMoneySecurity("MRO", i18n("Mauritanian Ouguiya"), "MRO", 5)); currencyList.append(MyMoneySecurity("MUR", i18n("Mauritius Rupee"))); currencyList.append(MyMoneySecurity("MXN", i18n("Mexican Peso"), "$")); currencyList.append(MyMoneySecurity("MDL", i18n("Moldavian Leu"))); currencyList.append(MyMoneySecurity("MNT", i18n("Mongolian Tugrik"), QChar(0x20AE))); currencyList.append(MyMoneySecurity("MAD", i18n("Moroccan Dirham"))); currencyList.append(MyMoneySecurity("MZN", i18n("Mozambique Metical"), "MT")); currencyList.append(MyMoneySecurity("MMK", i18n("Myanmar Kyat"))); currencyList.append(MyMoneySecurity("NAD", i18n("Namibian Dollar"), "$")); currencyList.append(MyMoneySecurity("NPR", i18n("Nepalese Rupee"))); currencyList.append(MyMoneySecurity("NZD", i18n("New Zealand Dollar"), "$")); currencyList.append(MyMoneySecurity("NIC", i18n("Nicaraguan Cordoba Oro"))); currencyList.append(MyMoneySecurity("NGN", i18n("Nigerian Naira"), QChar(0x20A6))); currencyList.append(MyMoneySecurity("KPW", i18n("North Korean Won"), QChar(0x20A9))); currencyList.append(MyMoneySecurity("NOK", i18n("Norwegian Kroner"), "kr")); currencyList.append(MyMoneySecurity("OMR", i18n("Omani Rial"), "OMR", 1000)); currencyList.append(MyMoneySecurity("PKR", i18n("Pakistan Rupee"))); currencyList.append(MyMoneySecurity("PAB", i18n("Panamanian Balboa"))); currencyList.append(MyMoneySecurity("PGK", i18n("Papua New Guinea Kina"))); currencyList.append(MyMoneySecurity("PYG", i18n("Paraguay Guarani"))); currencyList.append(MyMoneySecurity("PEN", i18n("Peruvian Nuevo Sol"))); currencyList.append(MyMoneySecurity("PHP", i18n("Philippine Peso"), QChar(0x20B1))); currencyList.append(MyMoneySecurity("PLN", i18n("Polish Zloty"))); currencyList.append(MyMoneySecurity("QAR", i18n("Qatari Rial"))); currencyList.append(MyMoneySecurity("RON", i18n("Romanian Leu (new)"))); currencyList.append(MyMoneySecurity("RUB", i18n("Russian Ruble"))); currencyList.append(MyMoneySecurity("RWF", i18n("Rwanda Franc"))); currencyList.append(MyMoneySecurity("WST", i18n("Samoan Tala"))); currencyList.append(MyMoneySecurity("STD", i18n("Sao Tome and Principe Dobra"))); currencyList.append(MyMoneySecurity("SAR", i18n("Saudi Riyal"))); currencyList.append(MyMoneySecurity("RSD", i18n("Serbian Dinar"))); currencyList.append(MyMoneySecurity("SCR", i18n("Seychelles Rupee"))); currencyList.append(MyMoneySecurity("SLL", i18n("Sierra Leone Leone"))); currencyList.append(MyMoneySecurity("SGD", i18n("Singapore Dollar"), "$")); currencyList.append(MyMoneySecurity("SBD", i18n("Solomon Islands Dollar"), "$")); currencyList.append(MyMoneySecurity("SOS", i18n("Somali Shilling"))); currencyList.append(MyMoneySecurity("ZAR", i18n("South African Rand"))); currencyList.append(MyMoneySecurity("KRW", i18n("South Korean Won"), QChar(0x20A9))); currencyList.append(MyMoneySecurity("LKR", i18n("Sri Lanka Rupee"))); currencyList.append(MyMoneySecurity("SHP", i18n("St. Helena Pound"), QChar(0x00A3))); currencyList.append(MyMoneySecurity("SDD", i18n("Sudanese Dinar"))); currencyList.append(MyMoneySecurity("SRG", i18n("Suriname Guilder"))); currencyList.append(MyMoneySecurity("SZL", i18n("Swaziland Lilangeni"))); currencyList.append(MyMoneySecurity("SEK", i18n("Swedish Krona"))); currencyList.append(MyMoneySecurity("CHF", i18n("Swiss Franc"), "SFr")); currencyList.append(MyMoneySecurity("SYP", i18n("Syrian Pound"), QChar(0x00A3))); currencyList.append(MyMoneySecurity("TWD", i18n("Taiwan Dollar"), "$")); currencyList.append(MyMoneySecurity("TJS", i18n("Tajikistan Somoni"))); currencyList.append(MyMoneySecurity("TZS", i18n("Tanzanian Shilling"))); currencyList.append(MyMoneySecurity("THB", i18n("Thai Baht"), QChar(0x0E3F))); currencyList.append(MyMoneySecurity("TOP", i18n("Tongan Pa'anga"))); currencyList.append(MyMoneySecurity("TTD", i18n("Trinidad and Tobago Dollar"), "$")); currencyList.append(MyMoneySecurity("TND", i18n("Tunisian Dinar"), "TND", 1000)); currencyList.append(MyMoneySecurity("TRY", i18n("Turkish Lira"), QChar(0x20BA))); currencyList.append(MyMoneySecurity("TMM", i18n("Turkmenistan Manat"))); currencyList.append(MyMoneySecurity("USD", i18n("US Dollar"), "$")); currencyList.append(MyMoneySecurity("UGX", i18n("Uganda Shilling"))); currencyList.append(MyMoneySecurity("UAH", i18n("Ukraine Hryvnia"))); currencyList.append(MyMoneySecurity("CLF", i18n("Unidad de Fometo"))); currencyList.append(MyMoneySecurity("AED", i18n("United Arab Emirates Dirham"))); currencyList.append(MyMoneySecurity("UYU", i18n("Uruguayan Peso"))); currencyList.append(MyMoneySecurity("UZS", i18n("Uzbekistani Sum"))); currencyList.append(MyMoneySecurity("VUV", i18n("Vanuatu Vatu"))); currencyList.append(MyMoneySecurity("VEB", i18n("Venezuelan Bolivar"))); currencyList.append(MyMoneySecurity("VND", i18n("Vietnamese Dong"), QChar(0x20AB))); currencyList.append(MyMoneySecurity("ZMK", i18n("Zambian Kwacha"))); currencyList.append(MyMoneySecurity("ZWD", i18n("Zimbabwe Dollar"), "$")); currencyList.append(ancientCurrencies().keys()); // sort the currencies ... qSort(currencyList.begin(), currencyList.end(), [] (const MyMoneySecurity& c1, const MyMoneySecurity& c2) { return c1.name().compare(c2.name()) < 0; }); // ... and add a few precious metals at the ned currencyList.append(MyMoneySecurity("XAU", i18n("Gold"), "XAU", 1000000)); currencyList.append(MyMoneySecurity("XPD", i18n("Palladium"), "XPD", 1000000)); currencyList.append(MyMoneySecurity("XPT", i18n("Platinum"), "XPT", 1000000)); currencyList.append(MyMoneySecurity("XAG", i18n("Silver"), "XAG", 1000000)); return currencyList; } QList MyMoneyFile::currencyList() const { d->checkStorage(); return d->m_storage->currencyList(); } QString MyMoneyFile::foreignCurrency(const QString& first, const QString& second) const { if (baseCurrency().id() == second) return first; return second; } MyMoneySecurity MyMoneyFile::baseCurrency() const { if (d->m_baseCurrency.id().isEmpty()) { QString id = QString(value("kmm-baseCurrency")); if (!id.isEmpty()) d->m_baseCurrency = currency(id); } return d->m_baseCurrency; } void MyMoneyFile::setBaseCurrency(const MyMoneySecurity& curr) { // make sure the currency exists MyMoneySecurity c = currency(curr.id()); if (c.id() != d->m_baseCurrency.id()) { setValue("kmm-baseCurrency", curr.id()); // force reload of base currency cache d->m_baseCurrency = MyMoneySecurity(); } } void MyMoneyFile::addPrice(const MyMoneyPrice& price) { if (price.rate(QString()).isZero()) return; d->checkTransaction(Q_FUNC_INFO); // store the account's which are affected by this price regarding their value d->priceChanged(*this, price); d->m_storage->addPrice(price); } void MyMoneyFile::removePrice(const MyMoneyPrice& price) { d->checkTransaction(Q_FUNC_INFO); // store the account's which are affected by this price regarding their value d->priceChanged(*this, price); d->m_storage->removePrice(price); } MyMoneyPrice MyMoneyFile::price(const QString& fromId, const QString& toId, const QDate& date, const bool exactDate) const { d->checkStorage(); QString to(toId); if (to.isEmpty()) to = value("kmm-baseCurrency"); // if some id is missing, we can return an empty price object if (fromId.isEmpty() || to.isEmpty()) return MyMoneyPrice(); // we don't search our tables if someone asks stupid stuff if (fromId == toId) { return MyMoneyPrice(fromId, toId, date, MyMoneyMoney::ONE, "KMyMoney"); } // if not asking for exact date, try to find the exact date match first, // either the requested price or its reciprocal value. If unsuccessful, it will move // on and look for prices of previous dates MyMoneyPrice rc = d->m_storage->price(fromId, to, date, true); if (!rc.isValid()) { // not found, search 'to-from' rate and use reciprocal value rc = d->m_storage->price(to, fromId, date, true); // not found, search previous dates, if exact date is not needed if (!exactDate && !rc.isValid()) { // search 'from-to' and 'to-from', select the most recent one MyMoneyPrice fromPrice = d->m_storage->price(fromId, to, date, exactDate); MyMoneyPrice toPrice = d->m_storage->price(to, fromId, date, exactDate); // check first whether both prices are valid if (fromPrice.isValid() && toPrice.isValid()) { if (fromPrice.date() >= toPrice.date()) { // if 'from-to' is newer or the same date, prefer that one rc = fromPrice; } else { // otherwise, use the reciprocal price rc = toPrice; } } else if (fromPrice.isValid()) { // check if any of the prices is valid, return that one rc = fromPrice; } else if (toPrice.isValid()) { rc = toPrice; } } } return rc; } MyMoneyPrice MyMoneyFile::price(const QString& fromId, const QString& toId) const { return price(fromId, toId, QDate::currentDate(), false); } MyMoneyPrice MyMoneyFile::price(const QString& fromId) const { return price(fromId, QString(), QDate::currentDate(), false); } MyMoneyPriceList MyMoneyFile::priceList() const { d->checkStorage(); return d->m_storage->priceList(); } bool MyMoneyFile::hasAccount(const QString& id, const QString& name) const { const auto accounts = account(id).accountList(); for (const auto& acc : accounts) { if (account(acc).name().compare(name) == 0) return true; } return false; } QList MyMoneyFile::reportList() const { d->checkStorage(); return d->m_storage->reportList(); } void MyMoneyFile::addReport(MyMoneyReport& report) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->addReport(report); } void MyMoneyFile::modifyReport(const MyMoneyReport& report) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->modifyReport(report); } unsigned MyMoneyFile::countReports() const { d->checkStorage(); return d->m_storage->countReports(); } MyMoneyReport MyMoneyFile::report(const QString& id) const { d->checkStorage(); return d->m_storage->report(id); } void MyMoneyFile::removeReport(const MyMoneyReport& report) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->removeReport(report); } QList MyMoneyFile::budgetList() const { d->checkStorage(); return d->m_storage->budgetList(); } void MyMoneyFile::addBudget(MyMoneyBudget &budget) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->addBudget(budget); } MyMoneyBudget MyMoneyFile::budgetByName(const QString& name) const { d->checkStorage(); return d->m_storage->budgetByName(name); } void MyMoneyFile::modifyBudget(const MyMoneyBudget& budget) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->modifyBudget(budget); } unsigned MyMoneyFile::countBudgets() const { d->checkStorage(); return d->m_storage->countBudgets(); } MyMoneyBudget MyMoneyFile::budget(const QString& id) const { d->checkStorage(); return d->m_storage->budget(id); } void MyMoneyFile::removeBudget(const MyMoneyBudget& budget) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->removeBudget(budget); } void MyMoneyFile::addOnlineJob(onlineJob& job) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->addOnlineJob(job); d->m_changeSet += MyMoneyNotification(File::Mode::Add, job); } void MyMoneyFile::modifyOnlineJob(const onlineJob job) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->modifyOnlineJob(job); d->m_changeSet += MyMoneyNotification(File::Mode::Modify, job); } onlineJob MyMoneyFile::getOnlineJob(const QString &jobId) const { d->checkStorage(); return d->m_storage->getOnlineJob(jobId); } QList MyMoneyFile::onlineJobList() const { d->checkStorage(); return d->m_storage->onlineJobList(); } /** @todo improve speed by passing count job to m_storage */ int MyMoneyFile::countOnlineJobs() const { return onlineJobList().count(); } /** * @brief Remove onlineJob * @param job onlineJob to remove */ void MyMoneyFile::removeOnlineJob(const onlineJob& job) { d->checkTransaction(Q_FUNC_INFO); // clear all changed objects from cache if (job.isLocked()) { return; } d->m_changeSet += MyMoneyNotification(File::Mode::Remove, job); d->m_storage->removeOnlineJob(job); } void MyMoneyFile::removeOnlineJob(const QStringList onlineJobIds) { foreach (QString jobId, onlineJobIds) { removeOnlineJob(getOnlineJob(jobId)); } } void MyMoneyFile::costCenterList(QList< MyMoneyCostCenter >& list) const { d->checkStorage(); list = d->m_storage->costCenterList(); } -bool MyMoneyFile::addVATSplit(MyMoneyTransaction& transaction, const MyMoneyAccount& account, const MyMoneyAccount& category, const MyMoneyMoney& amount) +void MyMoneyFile::updateVAT(MyMoneyTransaction& transaction) const +{ + // check if transaction qualifies + const auto splitCount = transaction.splits().count(); + if (splitCount > 1 && splitCount <= 3) { + MyMoneyMoney amount; + MyMoneyAccount assetLiability; + MyMoneyAccount category; + MyMoneySplit taxSplit; + const QString currencyId = transaction.commodity(); + foreach (const auto& split, transaction.splits()) { + const auto acc = account(split.accountId()); + // all splits must reference accounts denoted in the same currency + if (acc.currencyId() != currencyId) { + return; + } + if (acc.isAssetLiability() && assetLiability.id().isEmpty()) { + amount = split.shares(); + assetLiability = acc; + continue; + } + if (acc.isAssetLiability()) { + return; + } + if (category.id().isEmpty() && !acc.value("VatAccount").isEmpty()) { + category = acc; + continue; + } else if(taxSplit.id().isEmpty() && !acc.value("Tax").toLower().compare(QLatin1String("yes"))) { + taxSplit = split; + continue; + } + return; + } + if (!category.id().isEmpty()) { + // remove a possibly found tax split - we create a new one + // but only if it is the same tax category + if (!taxSplit.id().isEmpty()) { + if (category.value("VatAccount").compare(taxSplit.accountId())) + return; + transaction.removeSplit(taxSplit); + } + addVATSplit(transaction, assetLiability, category, amount); + } + } +} + +bool MyMoneyFile::addVATSplit(MyMoneyTransaction& transaction, const MyMoneyAccount& acc, const MyMoneyAccount& category, const MyMoneyMoney& amount) const { bool rc = false; try { MyMoneySplit cat; // category MyMoneySplit tax; // tax if (category.value("VatAccount").isEmpty()) return false; - MyMoneyAccount vatAcc = this->account(category.value("VatAccount").toLatin1()); - const MyMoneySecurity& asec = security(account.currencyId()); + MyMoneyAccount vatAcc = account(category.value("VatAccount")); + const MyMoneySecurity& asec = security(acc.currencyId()); const MyMoneySecurity& csec = security(category.currencyId()); const MyMoneySecurity& vsec = security(vatAcc.currencyId()); if (asec.id() != csec.id() || asec.id() != vsec.id()) { qDebug("Auto VAT assignment only works if all three accounts use the same currency."); return false; } MyMoneyMoney vatRate(vatAcc.value("VatRate")); MyMoneyMoney gv, nv; // gross value, net value - int fract = account.fraction(); + int fract = acc.fraction(); if (!vatRate.isZero()) { tax.setAccountId(vatAcc.id()); // qDebug("vat amount is '%s'", category.value("VatAmount").toLatin1()); if (category.value("VatAmount").toLower() != QString("net")) { // split value is the gross value gv = amount; nv = (gv / (MyMoneyMoney::ONE + vatRate)).convert(fract); - MyMoneySplit catSplit = transaction.splitByAccount(account.id(), false); + MyMoneySplit catSplit = transaction.splitByAccount(acc.id(), false); catSplit.setShares(-nv); catSplit.setValue(catSplit.shares()); transaction.modifySplit(catSplit); } else { // split value is the net value nv = amount; gv = (nv * (MyMoneyMoney::ONE + vatRate)).convert(fract); - MyMoneySplit accSplit = transaction.splitByAccount(account.id()); + MyMoneySplit accSplit = transaction.splitByAccount(acc.id()); accSplit.setValue(gv.convert(fract)); accSplit.setShares(accSplit.value()); transaction.modifySplit(accSplit); } tax.setValue(-(gv - nv).convert(fract)); tax.setShares(tax.value()); transaction.addSplit(tax); rc = true; } } catch (const MyMoneyException &) { } return rc; } bool MyMoneyFile::isReferenced(const MyMoneyObject& obj, const QBitArray& skipChecks) const { d->checkStorage(); return d->m_storage->isReferenced(obj, skipChecks); } bool MyMoneyFile::isReferenced(const MyMoneyObject& obj) const { return isReferenced(obj, QBitArray((int)eStorage::Reference::Count)); } bool MyMoneyFile::checkNoUsed(const QString& accId, const QString& no) const { // by definition, an empty string or a non-numeric string is not used QRegExp exp(QString("(.*\\D)?(\\d+)(\\D.*)?")); if (no.isEmpty() || exp.indexIn(no) == -1) return false; MyMoneyTransactionFilter filter; filter.addAccount(accId); QList transactions = transactionList(filter); QList::ConstIterator it_t = transactions.constBegin(); while (it_t != transactions.constEnd()) { try { MyMoneySplit split; // Test whether the transaction also includes a split into // this account split = (*it_t).splitByAccount(accId, true /*match*/); if (!split.number().isEmpty() && split.number() == no) return true; } catch (const MyMoneyException &) { } ++it_t; } return false; } QString MyMoneyFile::highestCheckNo(const QString& accId) const { unsigned64 lno = 0; unsigned64 cno; QString no; MyMoneyTransactionFilter filter; filter.addAccount(accId); QList transactions = transactionList(filter); QList::ConstIterator it_t = transactions.constBegin(); while (it_t != transactions.constEnd()) { try { // Test whether the transaction also includes a split into // this account MyMoneySplit split = (*it_t).splitByAccount(accId, true /*match*/); if (!split.number().isEmpty()) { // non-numerical values stored in number will return 0 in the next line cno = split.number().toULongLong(); if (cno > lno) { lno = cno; no = split.number(); } } } catch (const MyMoneyException &) { } ++it_t; } return no; } bool MyMoneyFile::hasNewerTransaction(const QString& accId, const QDate& date) const { MyMoneyTransactionFilter filter; filter.addAccount(accId); filter.setDateFilter(date.addDays(+1), QDate()); return !transactionList(filter).isEmpty(); } void MyMoneyFile::clearCache() { d->checkStorage(); d->m_balanceCache.clear(); } void MyMoneyFile::forceDataChanged() { emit dataChanged(); } bool MyMoneyFile::isTransfer(const MyMoneyTransaction& t) const { auto rc = true; if (t.splitCount() == 2) { foreach (const auto split, t.splits()) { auto acc = account(split.accountId()); if (acc.isIncomeExpense()) { rc = false; break; } } } return rc; } bool MyMoneyFile::referencesClosedAccount(const MyMoneyTransaction& t) const { auto ret = false; foreach (const auto split, t.splits()) { if (referencesClosedAccount(split)) { ret = true; break; } } return ret; } bool MyMoneyFile::referencesClosedAccount(const MyMoneySplit& s) const { if (s.accountId().isEmpty()) return false; try { return account(s.accountId()).isClosed(); } catch (const MyMoneyException &) { } return false; } QString MyMoneyFile::storageId() { QString id = value("kmm-id"); if (id.isEmpty()) { MyMoneyFileTransaction ft; try { QUuid uid = QUuid::createUuid(); setValue("kmm-id", uid.toString()); ft.commit(); id = uid.toString(); } catch (const MyMoneyException &) { qDebug("Unable to setup UID for new storage object"); } } return id; } QString MyMoneyFile::openingBalancesPrefix() { return i18n("Opening Balances"); } bool MyMoneyFile::hasMatchingOnlineBalance(const MyMoneyAccount& _acc) const { // get current values auto acc = account(_acc.id()); // if there's no last transaction import data we are done if (acc.value("lastImportedTransactionDate").isEmpty() || acc.value("lastStatementBalance").isEmpty()) return false; // otherwise, we compare the balances MyMoneyMoney balance(acc.value("lastStatementBalance")); MyMoneyMoney accBalance = this->balance(acc.id(), QDate::fromString(acc.value("lastImportedTransactionDate"), Qt::ISODate)); return balance == accBalance; } int MyMoneyFile::countTransactionsWithSpecificReconciliationState(const QString& accId, TransactionFilter::State state) const { MyMoneyTransactionFilter filter; filter.addAccount(accId); filter.addState((int)state); return transactionList(filter).count(); } QMap > MyMoneyFile::countTransactionsWithSpecificReconciliationState() const { QMap > result; MyMoneyTransactionFilter filter; filter.setReportAllSplits(false); d->checkStorage(); QList list; accountList(list); for (const auto account : list) { result[account.id()] = QVector((int)eMyMoney::Split::State::MaxReconcileState, 0); } const auto transactions = d->m_storage->transactionList(filter); for (const auto& transaction : transactions) { for (const auto& split : transaction.splits()) { if (!result.contains(split.accountId())) { result[split.accountId()] = QVector((int)eMyMoney::Split::State::MaxReconcileState, 0); } const auto flag = split.reconcileFlag(); switch(flag) { case eMyMoney::Split::State::NotReconciled: case eMyMoney::Split::State::Cleared: case eMyMoney::Split::State::Reconciled: case eMyMoney::Split::State::Frozen: result[split.accountId()][(int)flag]++; break; default: break; } } } return result; } /** * Make sure that the splits value has the precision of the corresponding account */ void MyMoneyFile::fixSplitPrecision(MyMoneyTransaction& t) const { auto transactionSecurity = security(t.commodity()); auto transactionFraction = transactionSecurity.smallestAccountFraction(); for (auto& split : t.splits()) { auto acc = account(split.accountId()); auto fraction = acc.fraction(); if(fraction == -1) { auto sec = security(acc.currencyId()); fraction = acc.fraction(sec); } // Don't do any rounding on a split factor if (split.action() != MyMoneySplit::actionName(eMyMoney::Split::Action::SplitShares)) { split.setShares(static_cast(split.shares().convertDenominator(fraction).canonicalize())); split.setValue(static_cast(split.value().convertDenominator(transactionFraction).canonicalize())); } } } class MyMoneyFileTransactionPrivate { Q_DISABLE_COPY(MyMoneyFileTransactionPrivate) public: MyMoneyFileTransactionPrivate() : m_isNested(MyMoneyFile::instance()->hasTransaction()), m_needRollback(!m_isNested) { } public: bool m_isNested; bool m_needRollback; }; MyMoneyFileTransaction::MyMoneyFileTransaction() : d_ptr(new MyMoneyFileTransactionPrivate) { Q_D(MyMoneyFileTransaction); if (!d->m_isNested) MyMoneyFile::instance()->startTransaction(); } MyMoneyFileTransaction::~MyMoneyFileTransaction() { try { rollback(); } catch (const MyMoneyException &e) { qDebug() << e.what(); } Q_D(MyMoneyFileTransaction); delete d; } void MyMoneyFileTransaction::restart() { rollback(); Q_D(MyMoneyFileTransaction); d->m_needRollback = !d->m_isNested; if (!d->m_isNested) MyMoneyFile::instance()->startTransaction(); } void MyMoneyFileTransaction::commit() { Q_D(MyMoneyFileTransaction); if (!d->m_isNested) MyMoneyFile::instance()->commitTransaction(); d->m_needRollback = false; } void MyMoneyFileTransaction::rollback() { Q_D(MyMoneyFileTransaction); if (d->m_needRollback) MyMoneyFile::instance()->rollbackTransaction(); d->m_needRollback = false; } diff --git a/kmymoney/mymoney/mymoneyfile.h b/kmymoney/mymoney/mymoneyfile.h index ec9e243d0..281beaed2 100644 --- a/kmymoney/mymoney/mymoneyfile.h +++ b/kmymoney/mymoney/mymoneyfile.h @@ -1,1709 +1,1723 @@ /* * Copyright 2000-2003 Michael Edwardes * Copyright 2001-2002 Felix Rodriguez * Copyright 2002-2004 Kevin Tambascio * Copyright 2004-2005 Ace Jones * Copyright 2006-2018 Thomas Baumgart * Copyright 2006 Darren Gould * Copyright 2017-2018 Łukasz Wojniłowicz * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef MYMONEYFILE_H #define MYMONEYFILE_H // ---------------------------------------------------------------------------- // QT Includes #include // ---------------------------------------------------------------------------- // Project Includes #include "kmm_mymoney_export.h" #include "mymoneyunittestable.h" /** * @author Thomas Baumgart, Michael Edwardes, Kevin Tambascio, Christian Dávid */ /** * This class represents the interface to the MyMoney engine. * For historical reasons it is still called MyMoneyFile. * It is implemented using the singleton pattern and thus only * exists once for each running instance of an application. * * The instance of the MyMoneyFile object is accessed as follows: * * @code * MyMoneyFile *file = MyMoneyFile::instance(); * file->anyMemberFunction(); * @endcode * * The first line of the above code creates a unique MyMoneyFile * object if it is called for the first time ever. All subsequent * calls to this functions return a pointer to the object created * during the first call. * * As the MyMoneyFile object represents the business logic, a storage * manager must be attached to it. This mechanism allows to use different * access methods to store the objects. The interface to access such an * storage manager is defined in the class MyMoneyStorageMgr. The methods * attachStorage() and detachStorage() are used to attach/detach a * storage manager object. The following code can be used to create a * functional MyMoneyFile instance: * * @code * MyMoneyStorageMgr *storage = .... * MyMoneyFile *file = MyMoneyFile::instance(); * file->attachStorage(storage); * @endcode * * The methods addAccount(), modifyAccount() and removeAccount() implement the * general account maintenance functions. The method reparentAccount() is * available to move an account from one superordinate account to another. * account() and accountList() are used to retrieve a single instance or a * QList of MyMoneyAccount objects. * * The methods addInstitution(), modifyInstitution() and removeInstitution() * implement the general institution maintenance functions. institution() and * institutionList() are used to retrieve a single instance or a * QList of MyMoneyInstitution objects. * * The methods addPayee(), modifyPayee() and removePayee() * implement the general payee maintenance functions. * payee() and payeeList() are used to retrieve a single instance or a * QList of MyMoneyPayee objects. * * The methods addTag(), modifyTag() and removeTag() * implement the general tag maintenance functions. * tag() and tagList() are used to retrieve a single instance or a * QList of MyMoneyTag objects. * * The methods addTransaction(), modifyTransaction() and removeTransaction() * implement the general transaction maintenance functions. * transaction() and transactionList() are used to retrieve * a single instance or a QList of MyMoneyTransaction objects. * * The methods addSecurity(), modifySecurity() and removeSecurity() * implement the general access to equities held in the engine. * * The methods addCurrency(), modifyCurrency() and removeCurrency() * implement the general access to multiple currencies held in the engine. * The methods baseCurrency() and setBaseCurrency() allow to retrieve/set * the currency selected by the user as base currency. If a currency * reference is emtpy, it will usually be interpreted as baseCurrency(). * * The methods liability(), asset(), expense(), income() and equity() are * used to retrieve the five standard accounts. isStandardAccount() * checks if a given accountId references one of the or not. * setAccountName() is used to specify a name for the standard accounts * from the GUI. * * The MyMoneyFile object emits the dataChanged() signal when data * has been changed. * * For abritrary values that have to be stored with the storage object * but are of importance to the application only, the object is derived * for MyMoneyKeyValueContainer which provides a container to store * these values indexed by an alphanumeric key. * * @exception MyMoneyException is thrown whenever an error occurs * while the engine code is running. The MyMoneyException:: object * describes the problem. */ template class QMap; class QString; class QStringList; class QBitArray; class MyMoneyStorageMgr; class MyMoneyCostCenter; class MyMoneyAccount; class MyMoneyInstitution; class MyMoneySecurity; class MyMoneyPrice; typedef QPair MyMoneySecurityPair; typedef QMap MyMoneyPriceEntries; typedef QMap MyMoneyPriceList; class MyMoneySchedule; class MyMoneyTag; class MyMoneyPayee; class MyMoneyBudget; class MyMoneyReport; class MyMoneyMoney; class MyMoneySplit; class MyMoneyObject; class MyMoneyTransaction; class MyMoneyTransactionFilter; class onlineJob; namespace eMyMoney { namespace Account { enum class Type; } namespace File { enum class Object; } namespace Schedule { enum class Type; enum class Occurrence; enum class PaymentType; } namespace TransactionFilter { enum class State; } } class KMM_MYMONEY_EXPORT MyMoneyFile : public QObject { Q_OBJECT KMM_MYMONEY_UNIT_TESTABLE public: friend class MyMoneyNotifier; /** * This is the function to access the MyMoneyFile object. * It returns a pointer to the single instance of the object. */ static MyMoneyFile* instance(); /** * This is the destructor for any MyMoneyFile object */ ~MyMoneyFile(); /** * @deprecated This is a convenience constructor. Do not use it anymore. * It will be deprecated in a future version of the engine. * * @param storage pointer to object that implements the MyMoneyStorageMgr * interface. */ explicit MyMoneyFile(MyMoneyStorageMgr *storage); // general get functions MyMoneyPayee user() const; // general set functions void setUser(const MyMoneyPayee& user); /** * This method is used to attach a storage object to the MyMoneyFile object * Without an attached storage object, the MyMoneyFile object is * of no use. * * After successful completion, the dataChanged() signal is emitted. * * In case of an error condition, an exception is thrown. * The following error conditions are checked: * * - @a storage is not equal to 0 * - there is no other @a storage object attached (use detachStorage() * to revert the attachStorage() operation. * * @param storage pointer to object that implements the MyMoneyStorageMgr * interface. * * @sa detachStorage() */ void attachStorage(MyMoneyStorageMgr* const storage); /** * This method is used to detach a previously attached storage * object from the MyMoneyFile object. If no storage object * is attached to the engine, this is a NOP. * * @param storage pointer to object that implements the MyMoneyStorageMgr * interface. * * @sa attachStorage() */ void detachStorage(MyMoneyStorageMgr* const storage = 0); /** * This method returns whether a storage is currently attached to * the engine or not. * * @return true if storage object is attached, false otherwise */ bool storageAttached() const; /** * This method returns a pointer to the storage object * * @return const pointer to the current attached storage object. * If no object is attached, returns 0. */ MyMoneyStorageMgr* storage() const; /** * This method must be called before any single change or a series of changes * in the underlying storage area is performed. * Once all changes are complete (i.e. the transaction is completed), * commitTransaction() must be called to finalize all changes. If an error occurs * during the processing of the changes call rollbackTransaction() to undo the * changes done so far. */ void startTransaction(); /** * This method returns whether a transaction has been started (@a true) * or not (@a false). */ bool hasTransaction() const; /** * @sa startTransaction() */ void commitTransaction(); /** * @sa startTransaction() */ void rollbackTransaction(); /** * This method is used to return the standard liability account * @return MyMoneyAccount liability account(group) */ MyMoneyAccount liability() const; /** * This method is used to return the standard asset account * @return MyMoneyAccount asset account(group) */ MyMoneyAccount asset() const; /** * This method is used to return the standard expense account * @return MyMoneyAccount expense account(group) */ MyMoneyAccount expense() const; /** * This method is used to return the standard income account * @return MyMoneyAccount income account(group) */ MyMoneyAccount income() const; /** * This method is used to return the standard equity account * @return MyMoneyAccount equity account(group) */ MyMoneyAccount equity() const; /** * This method returns the account information for the opening * balances account for the given @p security. If the respective * account does not exist, it will be created. The name is constructed * using MyMoneyFile::openingBalancesPrefix() and appending " (xxx)" in * case the @p security is not the baseCurrency(). The account created * will be a sub-account of the standard equity account provided by equity(). * * @param security Security for which the account is searched * * @return The opening balance account * * @note No notifications will be sent! */ MyMoneyAccount openingBalanceAccount(const MyMoneySecurity& security); /** * This method is essentially the same as the above, except it works on * const objects. If there is no opening balance account, this method * WILL NOT create one. Instead it will thrown an exception. * * @param security Security for which the account is searched * * @return The opening balance account * * @note No notifications will be sent! */ MyMoneyAccount openingBalanceAccount(const MyMoneySecurity& security) const; /** * Create an opening balance transaction for the account @p acc * with a value of @p balance. If the corresponding opening balance account * for the account's currency does not exist it will be created. If it exists * and it's opening date is later than the opening date of @p acc, * the opening date of the opening balances account will be adjusted to the * one of @p acc. * * @param acc reference to account for which the opening balance transaction * should be created * @param balance reference to the value of the opening balance transaction * * @returns The created MyMoneyTransaction object. In case no transaction has been * created, the id of the object is empty. */ MyMoneyTransaction createOpeningBalanceTransaction(const MyMoneyAccount& acc, const MyMoneyMoney& balance); /** * Retrieve the opening balance transaction for the account @p acc. * If there is no opening balance transaction, QString() will be returned. * * @param acc reference to account for which the opening balance transaction * should be retrieved * @return QString id for the transaction, or QString() if no transaction exists */ QString openingBalanceTransaction(const MyMoneyAccount& acc) const; /** * This method returns an indicator if the MyMoneyFile object has been * changed after it has last been saved to permanent storage. * * @return true if changed, false if not */ bool dirty() const; /** * This method is used to force the attached storage object to * be dirty. This is used by the application to re-set the dirty * flag after a failed upload to a server when the save operation * to a local temp file was OK. */ void setDirty() const; /** * Adds an institution to the file-global institution pool. A * respective institution-ID will be generated for this object. * The ID is stored as QString in the object passed as argument. * * An exception will be thrown upon error conditions. * @param institution The complete institution information in a * MyMoneyInstitution object */ void addInstitution(MyMoneyInstitution& institution); /** * Modifies an already existing institution in the file global * institution pool. * * An exception will be thrown upon error conditions. * * @param institution The complete new institution information */ void modifyInstitution(const MyMoneyInstitution& institution); /** * Deletes an existing institution from the file global institution pool * Also modifies the accounts that reference this institution as * their institution. * * An exception will be thrown upon error conditions. * * @param institution institution to be deleted. */ void removeInstitution(const MyMoneyInstitution& institution); void createAccount(MyMoneyAccount& newAccount, MyMoneyAccount& parentAccount, MyMoneyAccount& brokerageAccount, MyMoneyMoney openingBal); /** * Adds an account to the file-global account pool. A respective * account-ID will be generated within this record. The modified * members of @a account will be updated. * * A few parameters of the account to be added are checked against * the following conditions. If they do not match, an exception is * thrown. * * An account must match the following conditions: * * a) the account must have a name with length > 0 * b) the account must not have an id assigned * c) the transaction list must be empty * d) the account must not have any sub-ordinate accounts * e) the account must have no parent account * f) the account must not have any reference to a MyMoneyFile object * * An exception will be thrown upon error conditions. * * @param account The complete account information in a MyMoneyAccount object * @param parent The complete account information of the parent account */ void addAccount(MyMoneyAccount& account, MyMoneyAccount& parent); /** * Modifies an already existing account in the file global account pool. * * An exception will be thrown upon error conditions. * * @param account reference to the new account information */ void modifyAccount(const MyMoneyAccount& account); /** * This method re-parents an existing account * * An exception will be thrown upon error conditions. * * @param account MyMoneyAccount reference to account to be re-parented * @param parent MyMoneyAccount reference to new parent account */ void reparentAccount(MyMoneyAccount &account, MyMoneyAccount& parent); /** * moves splits from one account to another * * @param oldAccount id of the current account * @param newAccount if of the new account * * @return the number of modified splits */ unsigned int moveSplits(const QString& oldAccount, const QString& newAccount); /** * This method is used to determince, if the account with the * given ID is referenced by any split in m_transactionList. * * @param id id of the account to be checked for * @return true if account is referenced, false otherwise */ bool hasActiveSplits(const QString& id) const; /** * This method is used to check whether a given * account id references one of the standard accounts or not. * * An exception will be thrown upon error conditions. * * @param id account id * @return true if account-id is one of the standards, false otherwise */ bool isStandardAccount(const QString& id) const; /** * Returns @a true, if transaction @p t is a transfer transaction. * A transfer transaction has two splits, both referencing either * an asset, a liability or an equity account. */ bool isTransfer(const MyMoneyTransaction& t) const; /** * This method is used to set the name for the specified standard account * within the storage area. An exception will be thrown, if an error * occurs * * @param id QString reference to one of the standard accounts. * @param name QString reference to the name to be set * */ void setAccountName(const QString& id, const QString& name) const; /** * Deletes an existing account from the file global account pool * This method only allows to remove accounts that are not * referenced by any split. Use moveSplits() to move splits * to another account. An exception is thrown in case of a * problem. * * @param account reference to the account to be deleted. */ void removeAccount(const MyMoneyAccount& account); /** * Deletes existing accounts and their subaccounts recursivly * from the global account pool. * This method expects that all accounts and their subaccounts * are no longer assigned to any transactions or splits. * An exception is thrown in case of a problem deleting an account. * * The optional parameter level is used to keep track of the recursion level. * If the recursion level exceeds 100 (some arbitrary number which seems a good * maximum), an exception is thrown. * * @param account_list Reference to a list of account IDs to be deleted. * @param level Parameter to keep track of recursion level (do not pass a value here). */ void removeAccountList(const QStringList& account_list, unsigned int level = 0); /** * This member function checks all accounts identified by account_list * and their subaccounts whether they are assigned to transactions/splits or not. * The function calls itself recursively with the list of sub-accounts of * the currently processed account. * * The optional parameter level is used to keep track of the recursion level. * If the recursion level exceeds 100 (some arbitrary number which seems a good * maximum), an exception is thrown. * * @param account_list A QStringList with account IDs that need to be checked. * @param level (optional) Optional parameter to indicate recursion level. * @return Returns 'false' if at least one account has been found that * is still referenced by a transaction. */ bool hasOnlyUnusedAccounts(const QStringList& account_list, unsigned int level = 0); /** * Adds a transaction to the file-global transaction pool. A respective * transaction-ID will be generated for this object. The ID is stored * as QString in the object passed as argument. * Splits must reference valid accounts and valid payees. The payee * id can be empty. * * An exception will be thrown upon error conditions. * * @param transaction reference to the transaction */ void addTransaction(MyMoneyTransaction& transaction); /** * This method is used to update a specific transaction in the * transaction pool of the MyMoneyFile object. * Splits must reference valid accounts and valid payees. The payee * id can be empty. * * An exception will be thrown upon error conditions. * * @param transaction reference to transaction to be changed */ void modifyTransaction(const MyMoneyTransaction& transaction); /** * This method is used to extract a transaction from the file global * transaction pool through an id. In case of an invalid id, an * exception will be thrown. * * @param id id of transaction as QString. * @return reference to the requested transaction */ MyMoneyTransaction transaction(const QString& id) const; /** * This method is used to extract a transaction from the file global * transaction pool through an index into an account. * * @param account id of the account as QString * @param idx number of transaction in this account * @return reference to MyMoneyTransaction object */ MyMoneyTransaction transaction(const QString& account, const int idx) const; /** * This method is used to pull a list of transactions from the file * global transaction pool. It returns all those transactions * that match the filter passed as argument. If the filter is empty, * the whole journal will be returned. * The list returned is sorted according to the transactions posting date. * If more than one transaction exists for the same date, the order among * them is undefined. * * @param filter MyMoneyTransactionFilter object with the match criteria * * @return set of transactions in form of a QList */ QList transactionList(MyMoneyTransactionFilter& filter) const; void transactionList(QList& list, MyMoneyTransactionFilter& filter) const; void transactionList(QList >& list, MyMoneyTransactionFilter& filter) const; /** * This method is used to remove a transaction from the transaction * pool (journal). * * @param transaction const reference to transaction to be deleted */ void removeTransaction(const MyMoneyTransaction& transaction); /** * This method is used to return the actual balance of an account * without it's sub-ordinate accounts. If a @p date is presented, * the balance at the beginning of this date (not including any * transaction on this date) is returned. Otherwise all recorded * transactions are included in the balance. * * @param id id of the account in question * @param date return balance for specific date (default = QDate()) * @return balance of the account as MyMoneyMoney object */ MyMoneyMoney balance(const QString& id, const QDate& date) const; MyMoneyMoney balance(const QString& id) const; /** * This method is used to return the cleared balance of an account * without it's sub-ordinate accounts for a specific date. All * recorded transactions are included in the balance. * This method is used by the reconcialition functionality * * @param id id of the account in question * @param date return cleared balance for specific date * @return balance of the account as MyMoneyMoney object */ MyMoneyMoney clearedBalance(const QString& id, const QDate& date) const; /** * This method is used to return the actual balance of an account * including it's sub-ordinate accounts. If a @p date is presented, * the balance at the beginning of this date (not including any * transaction on this date) is returned. Otherwise all recorded * transactions are included in the balance. * * @param id id of the account in question * @param date return balance for specific date (default = QDate()) * @return balance of the account as MyMoneyMoney object */ MyMoneyMoney totalBalance(const QString& id, const QDate& date) const; MyMoneyMoney totalBalance(const QString& id) const; /** * This method returns the number of transactions currently known to file * in the range 0..MAXUINT * * @param account QString reference to account id. If account is empty + all transactions (the journal) will be counted. If account * is not empty it returns the number of transactions * that have splits in this account. * * @return number of transactions in journal/account */ unsigned int transactionCount(const QString& account) const; unsigned int transactionCount() const; /** * This method returns a QMap filled with the number of transactions * per account. The account id serves as index into the map. If one * needs to have all transactionCounts() for many accounts, this method * is faster than calling transactionCount(const QString& account) many * times. * * @return QMap with numbers of transactions per account */ QMap transactionCountMap() const; /** * This method returns the number of institutions currently known to file * in the range 0..MAXUINT * * @return number of institutions known to file */ unsigned int institutionCount() const; /** * This method returns the number of accounts currently known to file * in the range 0..MAXUINT * * @return number of accounts currently known inside a MyMoneyFile object */ unsigned int accountCount() const; /** * Returns the institution of a given ID * * @param id id of the institution to locate * @return MyMoneyInstitution object filled with data. If the institution * could not be found, an exception will be thrown */ MyMoneyInstitution institution(const QString& id) const; /** * This method returns a list of the institutions * inside a MyMoneyFile object. This is a convenience method * to the one above * * @return QList containing the institution objects */ QList institutionList() const; /** * Returns the account addressed by its id. * * @param id id of the account to locate. * @return MyMoneyAccount object carrying the @p id. An exception is thrown * if the id is unknown */ MyMoneyAccount account(const QString& id) const; /** * Returns the account addressed by its name. * * @param name name of the account to locate. * @return First MyMoneyAccount object found carrying the @p name. * An empty MyMoneyAccount object will be returned if the name is not found. */ MyMoneyAccount accountByName(const QString& name) const; /** * Returns the sub-account addressed by its name. * * @param acc account to search in * @param name name of the account to locate. * @return First MyMoneyAccount object found carrying the @p name. * An empty MyMoneyAccount object will be returned if the name is not found. */ MyMoneyAccount subAccountByName(const MyMoneyAccount& account, const QString& name) const; /** * This method returns a list of accounts inside a MyMoneyFile object. * An optional parameter is a list of id's. If this list is emtpy (the default) * the returned list contains all accounts, otherwise only those referenced * in the id-list. * * @param list reference to QList receiving the account objects * @param idlist QStringList of account ids of those accounts that * should be returned. If this list is empty, all accounts * currently known will be returned. * * @param recursive if @p true, then recurse in all found accounts. The default is @p false */ void accountList(QList& list, const QStringList& idlist = QStringList(), const bool recursive = false) const; /** * This method is used to convert an account id to a string representation * of the names which can be used as a category description. If the account * is part of a hierarchy, the category name will be the concatenation of * the single account names separated by MyMoneyFile::AccountSeparator. * * @param accountId QString reference of the account's id * @param includeStandardAccounts if true, the standard top account will be part * of the name, otherwise it will not be included (default is @c false) * * @return QString of the constructed name. */ QString accountToCategory(const QString& accountId, bool includeStandardAccounts = false) const; /** * This method is used to convert a string representing a category to * an account id. A category can be the concatenation of multiple accounts * representing a hierarchy of accounts. They have to be separated by * MyMoneyFile::AccountSeparator. * * @param category const reference to QString containing the category * @param type account type if a specific type is required (defaults to Unknown) * * @return QString of the corresponding account. If account was not found * the return value will be an empty string. */ QString categoryToAccount(const QString& category, eMyMoney::Account::Type type) const; QString categoryToAccount(const QString& category) const; /** * This method is used to convert a string representing an asset or * liability account to an account id. An account name can be the * concatenation of multiple accounts representing a hierarchy of * accounts. They have to be separated by MyMoneyFile::AccountSeparator. * * @param name const reference to QString containing the account name * * @return QString of the corresponding account. If account was not found * the return value will be an empty string. */ QString nameToAccount(const QString& name) const; /** * This method is used to extract the parent part of an account hierarchy * name who's parts are separated by MyMoneyFile::AccountSeparator. * * @param name full account name * @return parent name (full account name excluding the last part) */ QString parentName(const QString& name) const; /** * This method is used to create a new payee * * An exception will be thrown upon error conditions * * @param payee MyMoneyPayee reference to payee information */ void addPayee(MyMoneyPayee& payee); /** * This method is used to retrieve information about a payee * An exception will be thrown upon error conditions. * * @param id QString reference to id of payee * * @return MyMoneyPayee object of payee */ MyMoneyPayee payee(const QString& id) const; /** * This method is used to retrieve the id to a corresponding * name of a payee/receiver. * An exception will be thrown upon error conditions. * * @param payee QString reference to name of payee * * @return MyMoneyPayee object of payee */ MyMoneyPayee payeeByName(const QString& payee) const; /** * This method is used to modify an existing payee * * An exception will be thrown upon error conditions * * @param payee MyMoneyPayee reference to payee information */ void modifyPayee(const MyMoneyPayee& payee); /** * This method is used to remove an existing payee. * An error condition occurs, if the payee is still referenced * by a split. * * An exception will be thrown upon error conditions * * @param payee MyMoneyPayee reference to payee information */ void removePayee(const MyMoneyPayee& payee); /** * This method returns a list of the payees * inside a MyMoneyStorage object * * @return QList containing the payee information */ QList payeeList() const; /** * This method is used to create a new tag * * An exception will be thrown upon error conditions * * @param tag MyMoneyTag reference to tag information */ void addTag(MyMoneyTag& tag); /** * This method is used to retrieve information about a tag * An exception will be thrown upon error conditions. * * @param id QString reference to id of tag * * @return MyMoneyTag object of tag */ MyMoneyTag tag(const QString& id) const; /** * This method is used to retrieve the id to a corresponding * name of a tag. * An exception will be thrown upon error conditions. * * @param tag QString reference to name of tag * * @return MyMoneyTag object of tag */ MyMoneyTag tagByName(const QString& tag) const; /** * This method is used to modify an existing tag * * An exception will be thrown upon error conditions * * @param tag MyMoneyTag reference to tag information */ void modifyTag(const MyMoneyTag& tag); /** * This method is used to remove an existing tag. * An error condition occurs, if the tag is still referenced * by a split. * * An exception will be thrown upon error conditions * * @param tag MyMoneyTag reference to tag information */ void removeTag(const MyMoneyTag& tag); /** * This method returns a list of the tags * inside a MyMoneyStorage object * * @return QList containing the tag information */ QList tagList() const; /** * This method returns a list of the cost centers * inside a MyMoneyStorage object * * @return QList containing the cost center information */ void costCenterList(QList< MyMoneyCostCenter >& list) const; /** * This method is used to extract a value from the storage's * KeyValueContainer. For details see MyMoneyKeyValueContainer::value(). * @note Do not use this method to return the value of the key @p kmm-id. Use * storageId() instead. * * @param key const reference to QString containing the key * @return QString containing the value */ QString value(const QString& key) const; /** * This method is used to set a value in the storage's * KeyValueContainer. For details see MyMoneyKeyValueContainer::setValue(). * * @param key const reference to QString containing the key * @param val const reference to QString containing the value * * @note Keys starting with the leadin @p kmm- are reserved for internal use * by the MyMoneyFile object. */ void setValue(const QString& key, const QString& val); /** * This method returns the unique id of the attached storage object. * In case the storage object does not have an id yet, a new one will be * assigned. * * @return QString containing the value * * An exception is thrown if no storage object is attached. */ QString storageId(); /** * This method is used to delete a key-value-pair from the * storage's KeyValueContainer identified by the parameter * @p key. For details see MyMoneyKeyValueContainer::deletePair(). * * @param key const reference to QString containing the key */ void deletePair(const QString& key); /** * This method is used to add a scheduled transaction to the engine. * It must be sure, that the id of the object is not filled. When the * method returns to the caller, the id will be filled with the * newly created object id value. * * An exception will be thrown upon erroneous situations. * * @param sched reference to the MyMoneySchedule object */ void addSchedule(MyMoneySchedule& sched); /** * This method is used to modify an existing MyMoneySchedule * object. Therefor, the id attribute of the object must be set. * * An exception will be thrown upon erroneous situations. * * @param sched const reference to the MyMoneySchedule object to be updated */ void modifySchedule(const MyMoneySchedule& sched); /** * This method is used to remove an existing MyMoneySchedule object * from the engine. The id attribute of the object must be set. * * An exception will be thrown upon erroneous situations. * * @param sched const reference to the MyMoneySchedule object to be updated */ void removeSchedule(const MyMoneySchedule& sched); /** * This method is used to retrieve a single MyMoneySchedule object. * The id of the object must be supplied in the parameter @p id. * * An exception will be thrown upon erroneous situations. * * @param id QString containing the id of the MyMoneySchedule object * @return MyMoneySchedule object */ MyMoneySchedule schedule(const QString& id) const; /** * This method is used to extract a list of scheduled transactions * according to the filter criteria passed as arguments. * * @param accountId only search for scheduled transactions that reference * account @p accountId. If accountId is the empty string, * this filter is off. Default is @p QString(). * @param type only schedules of type @p type are searched for. * See eMyMoney::Schedule::Type for details. * Default is eMyMoney::Schedule::Type::Any * @param occurrence only schedules of occurrence type @p occurrence are searched for. * See eMyMoney::Schedule::Occurrence for details. * Default is eMyMoney::Schedule::Occurrence::Any * @param paymentType only schedules of payment method @p paymentType * are searched for. * See eMyMoney::Schedule::PaymentType for details. * Default is eMyMoney::Schedule::PaymentType::Any * @param startDate only schedules with payment dates after @p startDate * are searched for. Default is all dates (QDate()). * @param endDate only schedules with payment dates ending prior to @p endDate * are searched for. Default is all dates (QDate()). * @param overdue if true, only those schedules that are overdue are * searched for. Default is false (all schedules will be returned). * * @return const QList list of schedule objects. */ QList scheduleList(const QString& accountId, const eMyMoney::Schedule::Type type, const eMyMoney::Schedule::Occurrence occurrence, const eMyMoney::Schedule::PaymentType paymentType, const QDate& startDate, const QDate& endDate, const bool overdue) const; QList scheduleList(const QString& accountId) const; QList scheduleList() const; QStringList consistencyCheck(); /** * MyMoneyFile::openingBalancesPrefix() is a special string used * to generate the name for opening balances accounts. See openingBalanceAccount() * for details. */ static QString openingBalancesPrefix(); /** * MyMoneyFile::AccountSeparator is used as the separator * between account names to form a hierarchy. */ static const QString AccountSeparator; /** * createCategory creates a category from a text name. * * The whole account hierarchy is created if it does not * already exist. e.g if name = Bills:Credit Card and * base = expense(), Bills will first be checked to see if * it exists and created if not. Credit Card will then * be created with Bills as it's parent. The Credit Card account * will have it's id returned. * * @param base The base account (expense or income) * @param name The category to create * * @return The category account id or empty on error. * * @exception An exception will be thrown, if @p base is not equal * expense() or income(). **/ QString createCategory(const MyMoneyAccount& base, const QString& name); /** * This method is used to get the account id of the split for * a transaction from the text found in the QIF $ or L record. * If an account with the name is not found, the user is asked * if it should be created. * * @param name name of account as found in the QIF file * @param value value found in the T record * @param value2 value found in the $ record for split transactions * * @return id of the account for the split. If no name is specified * or the account was not found and not created the * return value will be "". */ QString checkCategory(const QString& name, const MyMoneyMoney& value, const MyMoneyMoney& value2); /** * This method is used to add a new security object to the engine. * The ID of the object is the trading symbol, so there is no need for an additional * ID since the symbol is guaranteed to be unique. * * An exception will be thrown upon erroneous situations. * * @param security reference to the MyMoneySecurity object */ void addSecurity(MyMoneySecurity& security); /** * This method is used to modify an existing MyMoneySchedule * object. * * An exception will be thrown upon erroneous situations. * * @param security reference to the MyMoneySecurity object to be updated */ void modifySecurity(const MyMoneySecurity& security); /** * This method is used to remove an existing MyMoneySecurity object * from the engine. * * An exception will be thrown upon erroneous situations. * * @param security reference to the MyMoneySecurity object to be removed */ void removeSecurity(const MyMoneySecurity& security); /** * This method is used to retrieve a single MyMoneySecurity object. * The id of the object must be supplied in the parameter @p id. * If no security with the given id is found, then a corresponding * currency is searched. If @p id is empty, the baseCurrency() is returned. * * An exception will be thrown upon erroneous situations. * * @param id QString containing the id of the MyMoneySecurity object * @return MyMoneySecurity object */ MyMoneySecurity security(const QString& id) const; /** * This method is used to retrieve a list of all MyMoneySecurity objects. */ QList securityList() const; /** * This method is used to add a new currency object to the engine. * The ID of the object is the trading symbol, so there is no need for an additional * ID since the symbol is guaranteed to be unique. * * An exception will be thrown upon erroneous situations. * * @param currency reference to the MyMoneySecurity object */ void addCurrency(const MyMoneySecurity& currency); /** * This method is used to modify an existing MyMoneySecurity * object. * * An exception will be thrown upon erroneous situations. * * @param currency reference to the MyMoneySecurity object */ void modifyCurrency(const MyMoneySecurity& currency); /** * This method is used to remove an existing MyMoneySecurity object * from the engine. * * An exception will be thrown upon erroneous situations. * * @param currency reference to the MyMoneySecurity object */ void removeCurrency(const MyMoneySecurity& currency); /** * This method is used to retrieve a single MyMoneySecurity object. * The id of the object must be supplied in the parameter @p id. * If @p id is empty, this method returns baseCurrency(). In case * no currency is found, @p id is searched in the loaded set of securities. * * An exception will be thrown upon erroneous situations. * * @param id QString containing the id of the MyMoneySchedule object * @return MyMoneySchedule object */ MyMoneySecurity currency(const QString& id) const; /** * This method is used to retrieve the map of ancient currencies (together with their last prices) * known to the engine. * * An exception will be thrown upon erroneous situations. * * @return QMap of all MyMoneySecurity and MyMoneyPrice objects. */ QMap ancientCurrencies() const; /** * This method is used to retrieve the list of available currencies * known to the engine. * * An exception will be thrown upon erroneous situations. * * @return QList of all MyMoneySecurity objects. */ QList availableCurrencyList() const; /** * This method is used to retrieve the list of stored currencies * known to the engine. * * An exception will be thrown upon erroneous situations. * * @return QList of all MyMoneySecurity objects. */ QList currencyList() const; /** * This method retrieves a MyMoneySecurity object representing * the selected base currency. If the base currency is not * selected (e.g. due to a previous call to setBaseCurrency()) * a standard MyMoneySecurity object will be returned. See * MyMoneySecurity() for details. * * An exception will be thrown upon erroneous situations. * * @return MyMoneySecurity describing base currency */ MyMoneySecurity baseCurrency() const; /** * This method returns the foreign currency of the given two * currency ids. If second is the base currency id then @a first * is returned otherwise @a second is returned. */ QString foreignCurrency(const QString& first, const QString& second) const; /** * This method allows to select the base currency. It does * not perform any changes to the data in the engine. It merely * stores a reference to the base currency. The currency * passed as argument must exist in the engine. * * An exception will be thrown upon erroneous situations. * * @param currency */ void setBaseCurrency(const MyMoneySecurity& currency); /** * This method adds/replaces a price to/from the price list */ void addPrice(const MyMoneyPrice& price); /** * This method removes a price from the price list */ void removePrice(const MyMoneyPrice& price); /** * This method is used to retrieve a price for a specific security * on a specific date. If there is no price for this date, the last * known price for this currency is used. If no price information * is available, 1.0 will be returned as price. * * @param fromId the id of the currency in question * @param toId the id of the currency to convert to (if emtpy, baseCurrency) * @param date the date for which the price should be returned (default = today) * @param exactDate if true, entry for date must exist, if false any price information * with a date less or equal to @p date will be returned * * @return price found as MyMoneyPrice object * @note This throws an exception when the base currency is not set and toId is empty */ MyMoneyPrice price(const QString& fromId, const QString& toId, const QDate& date, const bool exactDate = false) const; MyMoneyPrice price(const QString& fromId, const QString& toId) const; MyMoneyPrice price(const QString& fromId) const; /** * This method returns a list of all prices. * * @return MyMoneyPriceList of all MyMoneyPrice objects. */ MyMoneyPriceList priceList() const; /** * This method allows to interrogate the engine, if a known account * with id @p id has a subaccount with the name @p name. * * @param id id of the account to look at * @param name account name that needs to be searched force * @retval true account with name @p name found as subaccounts * @retval false no subaccount present with that name */ bool hasAccount(const QString& id, const QString& name) const; /** * This method is used to retrieve the list of all reports * known to the engine. * * An exception will be thrown upon erroneous situations. * * @return QList of all MyMoneyReport objects. */ QList reportList() const; /** * Adds a report to the file-global institution pool. A * respective report-ID will be generated for this object. * The ID is stored as QString in the object passed as argument. * * An exception will be thrown upon error conditions. * * @param report The complete report information in a * MyMoneyReport object */ void addReport(MyMoneyReport& report); /** * Modifies an already existing report in the file global * report pool. * * An exception will be thrown upon error conditions. * * @param report The complete new report information */ void modifyReport(const MyMoneyReport& report); /** * This method returns the number of reports currently known to file * in the range 0..MAXUINT * * @return number of reports known to file */ unsigned countReports() const; /** * This method is used to retrieve a single MyMoneyReport object. * The id of the object must be supplied in the parameter @p id. * * An exception will be thrown upon erroneous situations. * * @param id QString containing the id of the MyMoneyReport object * @return MyMoneyReport object */ MyMoneyReport report(const QString& id) const; /** * This method is used to remove an existing MyMoneyReport object * from the engine. The id attribute of the object must be set. * * An exception will be thrown upon erroneous situations. * * @param report const reference to the MyMoneyReport object to be updated */ void removeReport(const MyMoneyReport& report); /** * This method is used to retrieve the list of all budgets * known to the engine. * * An exception will be thrown upon erroneous situations. * * @return QList of all MyMoneyBudget objects. */ QList budgetList() const; /** * Adds a budget to the file-global institution pool. A * respective budget-ID will be generated for this object. * The ID is stored as QString in the object passed as argument. * * An exception will be thrown upon error conditions. * * @param budget The complete budget information in a * MyMoneyBudget object */ void addBudget(MyMoneyBudget &budget); /** * This method is used to retrieve the id to a corresponding * name of a budget. * An exception will be thrown upon error conditions. * * @param budget QString reference to name of budget * * @return MyMoneyBudget refernce to object of budget */ MyMoneyBudget budgetByName(const QString& budget) const; /** * Modifies an already existing budget in the file global * budget pool. * * An exception will be thrown upon error conditions. * * @param budget The complete new budget information */ void modifyBudget(const MyMoneyBudget& budget); /** * This method returns the number of budgets currently known to file * in the range 0..MAXUINT * * @return number of budgets known to file */ unsigned countBudgets() const; /** * This method is used to retrieve a single MyMoneyBudget object. * The id of the object must be supplied in the parameter @p id. * * An exception will be thrown upon erroneous situations. * * @param id QString containing the id of the MyMoneyBudget object * @return MyMoneyBudget object */ MyMoneyBudget budget(const QString& id) const; /** * This method is used to remove an existing MyMoneyBudget object * from the engine. The id attribute of the object must be set. * * An exception will be thrown upon erroneous situations. * * @param budget const reference to the MyMoneyBudget object to be updated */ void removeBudget(const MyMoneyBudget& budget); /** * This method is used to add a VAT split to a transaction. * * @param transaction reference to the transaction * @param account reference to the account * @param category reference to the category * @param amount reference to the amount of the VAT split * * @return true if a VAT split has been added */ - bool addVATSplit(MyMoneyTransaction& transaction, const MyMoneyAccount& account, const MyMoneyAccount& category, const MyMoneyMoney& amount); + bool addVATSplit(MyMoneyTransaction& transaction, const MyMoneyAccount& account, const MyMoneyAccount& category, const MyMoneyMoney& amount) const; + + /** + * This method checks if the @a transaction has two or three splits and + * one references an asset or liability, one references an income or expense and one + * references a tax account. All three accounts must be denominated in the same + * transaction.commodity(). Also, if there is a tax split it must reference the + * same account as the one that is assigned to the income/expense category. + * + * If that all matches, the @a transaction is updated such that the amount + * of the asset/liability is split according to the tax settings. + * + * @param transaction reference to the transaction + */ + void updateVAT(MyMoneyTransaction& transaction) const; /** * This method checks, if the given @p object is referenced * by another engine object. * * @param obj const reference to object to be checked * @param skipCheck QBitArray with eStorage::Reference bits set for which * the check should be skipped * * @retval false @p object is not referenced * @retval true @p institution is referenced */ bool isReferenced(const MyMoneyObject& obj, const QBitArray& skipCheck) const; bool isReferenced(const MyMoneyObject& obj) const; /** * Returns true if any of the accounts referenced by the splits * of transaction @a t is closed. */ bool referencesClosedAccount(const MyMoneyTransaction& t) const; /** * Returns true if the accounts referenced by the split @a s is closed. */ bool referencesClosedAccount(const MyMoneySplit& s) const; /** * This method checks if the given check no &p no is used in * a transaction referencing account &p accId. If @p accId is empty, * @p false is returned. * * @param accId id of account to checked * @param no check number to be verified if used or not * @retval false @p no is not in use * @retval true @p no is already assigned */ bool checkNoUsed(const QString& accId, const QString& no) const; /** * This method returns the highest assigned check no for * account @p accId. * * @param accId id of account to be scanned * @return highest check no. used */ QString highestCheckNo(const QString& accId) const; /** * This method checks if there is a transaction * after the date @p date for account @p accId. * * @param accId id of account to be scanned * @param date date to compare with * @retval false if there is no transaction after @p date * @retval true if there is a transaction after @p date */ bool hasNewerTransaction(const QString& accId, const QDate& date) const; /** * Clear all internal caches (used internally for performance measurements) */ void clearCache(); void forceDataChanged(); /** * This returns @p true if file and online balance of a specific * @p account are matching. Returns false if there is no online balance. * * @param account @p account to be checked * @retval false if @p account has balance mismatch or if there is no online balance. * @retval true if @p account has matching balances */ bool hasMatchingOnlineBalance(const MyMoneyAccount& account) const; /** * This returns the number of transactions of a specific reconciliation state @p state of account with id @p accId. * * @param accId @p accId is the account id of account to be checked * @param state @p state reconciliation state * @return number of transactions with state @p state */ int countTransactionsWithSpecificReconciliationState(const QString& accId, eMyMoney::TransactionFilter::State state) const; QMap< QString, QVector > countTransactionsWithSpecificReconciliationState() const; /** * @brief Saves a new onlineJob * @param job you stay owner of the object (a copy will be created) */ void addOnlineJob(onlineJob& job); /** * @brief Saves a onlineJob * @param job you stay owner of the object (a copy will be created) */ void modifyOnlineJob(const onlineJob job); /** * @brief Returns onlineJob identified by jobId * @param jobId * @return */ onlineJob getOnlineJob(const QString &jobId) const; /** * @brief Returns all onlineJobs * @return all online jobs, caller gains ownership */ QList onlineJobList() const; /** * @brief Returns the number of onlineJobs */ int countOnlineJobs() const; /** * @brief Remove onlineJob * * @note Removing an onlineJob fails if it is locked */ void removeOnlineJob(const onlineJob& job); /** * @brief Removes multiple onlineJobs by id * * @note Removing an onlineJob fails if it is locked */ void removeOnlineJob(const QStringList onlineJobIds); protected: /** * This is the constructor for a new empty file description */ MyMoneyFile(); Q_SIGNALS: /** * This signal is emitted when a transaction has been committed and * the notifications are about to be sent out. */ void beginChangeNotification(); /** * This signal is emitted when a transaction has been committed and * all notifications have been sent out. */ void endChangeNotification(); /** * This signal is emitted whenever any data has been changed in the engine * via any of the methods of this object */ void dataChanged(); /** * This signal is emitted by the engine whenever a new object * had been added. The data for the new object is contained in * @a obj. */ void objectAdded(eMyMoney::File::Object objType, const QString& id); /** * This signal is emitted by the engine whenever an object * had been removed. * * @note: The data contained in @a obj is only for reference * purposes and should not be used to call any MyMoneyFile * method anymore as the object is already deleted in the storage * when the signal is emitted. */ void objectRemoved(eMyMoney::File::Object objType, const QString& id); /** * This signal is emitted by the engine whenever an object * had been changed. The new state of the object is contained * in @a obj. */ void objectModified(eMyMoney::File::Object objType, const QString& id); /** * This signal is emitted by the engine whenever the balance * of an account had been changed by adding, modifying or * removing transactions from the MyMoneyFile object. */ void balanceChanged(const MyMoneyAccount& acc); /** * This signal is emitted by the engine whenever the value * of an account had been changed by adding or removing * prices from the MyMoneyFile object. */ void valueChanged(const MyMoneyAccount& acc); private: static MyMoneyFile file; MyMoneyFile& operator=(MyMoneyFile&); // not allowed for singleton MyMoneyFile(const MyMoneyFile&); // not allowed for singleton QString locateSubAccount(const MyMoneyAccount& base, const QString& category) const; void ensureDefaultCurrency(MyMoneyAccount& acc) const; void warningMissingRate(const QString& fromId, const QString& toId) const; /** * This method creates an opening balances account. The name is constructed * using MyMoneyFile::openingBalancesPrefix() and appending " (xxx)" in * case the @p security is not the baseCurrency(). The account created * will be a sub-account of the standard equity account provided by equity(). * * @param security Security for which the account is searched */ MyMoneyAccount createOpeningBalanceAccount(const MyMoneySecurity& security); MyMoneyAccount openingBalanceAccount_internal(const MyMoneySecurity& security) const; /** * Make sure that the splits value has the precision of the corresponding account */ void fixSplitPrecision(MyMoneyTransaction& t) const; private: /// \internal d-pointer class. class Private; /// \internal d-pointer instance. Private* const d; }; class MyMoneyFileTransactionPrivate; class KMM_MYMONEY_EXPORT MyMoneyFileTransaction { Q_DISABLE_COPY(MyMoneyFileTransaction) public: MyMoneyFileTransaction(); ~MyMoneyFileTransaction(); /** * Commit the current transaction. * * @warning Make sure not to use any variable that might have been altered by * the transaction. Please keep in mind, that changing transactions * can also affect account objects. If you still need those variables * just reload them from the engine. */ void commit(); void rollback(); void restart(); private: MyMoneyFileTransactionPrivate * const d_ptr; Q_DECLARE_PRIVATE(MyMoneyFileTransaction) }; #endif diff --git a/kmymoney/views/kgloballedgerview.cpp b/kmymoney/views/kgloballedgerview.cpp index 5a3bc30c9..2a0b8ce4e 100644 --- a/kmymoney/views/kgloballedgerview.cpp +++ b/kmymoney/views/kgloballedgerview.cpp @@ -1,2119 +1,2123 @@ /*************************************************************************** kgloballedgerview.cpp - description ------------------- begin : Wed Jul 26 2006 copyright : (C) 2006 by Thomas Baumgart email : Thomas Baumgart (C) 2017 by Łukasz Wojniłowicz ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "kgloballedgerview_p.h" #include // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "mymoneyaccount.h" #include "mymoneyfile.h" #include "kmymoneyaccountcombo.h" #include "kmymoneypayeecombo.h" #include "keditscheduledlg.h" #include "kendingbalancedlg.h" #include "register.h" #include "transactioneditor.h" #include "selectedtransactions.h" #include "kmymoneysettings.h" #include "registersearchline.h" #include "kfindtransactiondlg.h" #include "accountsmodel.h" #include "models.h" #include "mymoneyschedule.h" #include "mymoneysecurity.h" #include "mymoneytransaction.h" #include "mymoneytransactionfilter.h" #include "mymoneysplit.h" #include "transaction.h" #include "transactionform.h" #include "widgetenums.h" #include "mymoneyenums.h" #include "menuenums.h" using namespace eMenu; QDate KGlobalLedgerViewPrivate::m_lastPostDate; KGlobalLedgerView::KGlobalLedgerView(QWidget *parent) : KMyMoneyViewBase(*new KGlobalLedgerViewPrivate(this), parent) { typedef void(KGlobalLedgerView::*KGlobalLedgerViewFunc)(); const QHash actionConnections { {Action::NewTransaction, &KGlobalLedgerView::slotNewTransaction}, {Action::EditTransaction, &KGlobalLedgerView::slotEditTransaction}, {Action::DeleteTransaction, &KGlobalLedgerView::slotDeleteTransaction}, {Action::DuplicateTransaction, &KGlobalLedgerView::slotDuplicateTransaction}, {Action::EnterTransaction, &KGlobalLedgerView::slotEnterTransaction}, {Action::AcceptTransaction, &KGlobalLedgerView::slotAcceptTransaction}, {Action::CancelTransaction, &KGlobalLedgerView::slotCancelTransaction}, {Action::EditSplits, &KGlobalLedgerView::slotEditSplits}, {Action::CopySplits, &KGlobalLedgerView::slotCopySplits}, {Action::GoToPayee, &KGlobalLedgerView::slotGoToPayee}, {Action::GoToAccount, &KGlobalLedgerView::slotGoToAccount}, {Action::MatchTransaction, &KGlobalLedgerView::slotMatchTransactions}, {Action::CombineTransactions, &KGlobalLedgerView::slotCombineTransactions}, {Action::ToggleReconciliationFlag, &KGlobalLedgerView::slotToggleReconciliationFlag}, {Action::MarkCleared, &KGlobalLedgerView::slotMarkCleared}, {Action::MarkReconciled, &KGlobalLedgerView::slotMarkReconciled}, {Action::MarkNotReconciled, &KGlobalLedgerView::slotMarkNotReconciled}, {Action::SelectAllTransactions, &KGlobalLedgerView::slotSelectAllTransactions}, {Action::NewScheduledTransaction, &KGlobalLedgerView::slotCreateScheduledTransaction}, {Action::AssignTransactionsNumber, &KGlobalLedgerView::slotAssignNumber}, {Action::StartReconciliation, &KGlobalLedgerView::slotStartReconciliation}, {Action::FinishReconciliation, &KGlobalLedgerView::slotFinishReconciliation}, {Action::PostponeReconciliation, &KGlobalLedgerView::slotPostponeReconciliation}, {Action::OpenAccount, &KGlobalLedgerView::slotOpenAccount}, {Action::EditFindTransaction, &KGlobalLedgerView::slotFindTransaction}, }; for (auto a = actionConnections.cbegin(); a != actionConnections.cend(); ++a) connect(pActions[a.key()], &QAction::triggered, this, a.value()); Q_D(KGlobalLedgerView); d->m_balanceWarning.reset(new KBalanceWarning(this)); } KGlobalLedgerView::~KGlobalLedgerView() { } void KGlobalLedgerView::executeCustomAction(eView::Action action) { Q_D(KGlobalLedgerView); switch(action) { case eView::Action::Refresh: refresh(); break; case eView::Action::SetDefaultFocus: // delay the setFocus call until the event loop is running QMetaObject::invokeMethod(d->m_registerSearchLine->searchLine(), "setFocus", Qt::QueuedConnection); break; case eView::Action::DisableViewDepenedendActions: pActions[Action::SelectAllTransactions]->setEnabled(false); break; case eView::Action::InitializeAfterFileOpen: d->m_lastSelectedAccountID.clear(); d->m_currentAccount = MyMoneyAccount(); if (d->m_accountComboBox) { d->m_accountComboBox->setSelected(QString()); } break; case eView::Action::CleanupBeforeFileClose: if (d->m_inEditMode) { d->deleteTransactionEditor(); } break; default: break; } } void KGlobalLedgerView::refresh() { Q_D(KGlobalLedgerView); if (isVisible()) { if (!d->m_inEditMode) { setUpdatesEnabled(false); d->loadView(); setUpdatesEnabled(true); d->m_needsRefresh = false; // force a new account if the current one is empty d->m_newAccountLoaded = d->m_currentAccount.id().isEmpty(); } } else { d->m_needsRefresh = true; } } void KGlobalLedgerView::showEvent(QShowEvent* event) { if (MyMoneyFile::instance()->storageAttached()) { Q_D(KGlobalLedgerView); if (d->m_needLoad) d->init(); emit customActionRequested(View::Ledgers, eView::Action::AboutToShow); if (d->m_needsRefresh) { if (!d->m_inEditMode) { setUpdatesEnabled(false); d->loadView(); setUpdatesEnabled(true); d->m_needsRefresh = false; d->m_newAccountLoaded = false; } } else { if (!d->m_lastSelectedAccountID.isEmpty()) { try { const auto acc = MyMoneyFile::instance()->account(d->m_lastSelectedAccountID); slotSelectAccount(acc.id()); } catch (const MyMoneyException &) { d->m_lastSelectedAccountID.clear(); // account is invalid } } else { slotSelectAccount(d->m_accountComboBox->getSelected()); } KMyMoneyRegister::SelectedTransactions list(d->m_register); updateLedgerActions(list); emit selectByVariant(QVariantList {QVariant::fromValue(list)}, eView::Intent::SelectRegisterTransactions); } } pActions[Action::SelectAllTransactions]->setEnabled(true); // don't forget base class implementation QWidget::showEvent(event); } void KGlobalLedgerView::updateActions(const MyMoneyObject& obj) { Q_D(KGlobalLedgerView); // if (typeid(obj) != typeid(MyMoneyAccount) && // (obj.id().isEmpty() && d->m_currentAccount.id().isEmpty())) // do not disable actions that were already disabled)) // return; const auto& acc = static_cast(obj); const QVector actionsToBeDisabled { Action::StartReconciliation, Action::FinishReconciliation, Action::PostponeReconciliation, Action::OpenAccount, Action::NewTransaction }; for (const auto& a : actionsToBeDisabled) pActions[a]->setEnabled(false); auto b = acc.isClosed() ? false : true; pMenus[Menu::MoveTransaction]->setEnabled(b); QString tooltip; pActions[Action::NewTransaction]->setEnabled(canCreateTransactions(tooltip) || !isVisible()); pActions[Action::NewTransaction]->setToolTip(tooltip); const auto file = MyMoneyFile::instance(); if (!acc.id().isEmpty() && !file->isStandardAccount(acc.id())) { switch (acc.accountGroup()) { case eMyMoney::Account::Type::Asset: case eMyMoney::Account::Type::Liability: case eMyMoney::Account::Type::Equity: pActions[Action::OpenAccount]->setEnabled(true); if (acc.accountGroup() != eMyMoney::Account::Type::Equity) { if (d->m_reconciliationAccount.id().isEmpty()) { pActions[Action::StartReconciliation]->setEnabled(true); pActions[Action::StartReconciliation]->setToolTip(i18n("Reconcile")); } else { auto tip = i18n("Reconcile - disabled because you are currently reconciling %1", d->m_reconciliationAccount.name()); pActions[Action::StartReconciliation]->setToolTip(tip); if (!d->m_transactionEditor) { pActions[Action::FinishReconciliation]->setEnabled(acc.id() == d->m_reconciliationAccount.id()); pActions[Action::PostponeReconciliation]->setEnabled(acc.id() == d->m_reconciliationAccount.id()); } } } break; case eMyMoney::Account::Type::Income : case eMyMoney::Account::Type::Expense : pActions[Action::OpenAccount]->setEnabled(true); break; default: break; } } d->m_currentAccount = acc; // slotSelectAccount(acc); } void KGlobalLedgerView::updateLedgerActions(const KMyMoneyRegister::SelectedTransactions& list) { Q_D(KGlobalLedgerView); d->selectTransactions(list); updateLedgerActionsInternal(); } void KGlobalLedgerView::updateLedgerActionsInternal() { Q_D(KGlobalLedgerView); const QVector actionsToBeDisabled { Action::EditTransaction, Action::EditSplits, Action::EnterTransaction, Action::CancelTransaction, Action::DeleteTransaction, Action::MatchTransaction, Action::AcceptTransaction, Action::DuplicateTransaction, Action::ToggleReconciliationFlag, Action::MarkCleared, Action::GoToAccount, Action::GoToPayee, Action::AssignTransactionsNumber, Action::NewScheduledTransaction, Action::CombineTransactions, Action::CopySplits, }; for (const auto& a : actionsToBeDisabled) pActions[a]->setEnabled(false); const auto file = MyMoneyFile::instance(); pActions[Action::MatchTransaction]->setText(i18nc("Button text for match transaction", "Match")); // pActions[Action::TransactionNew]->setToolTip(i18n("Create a new transaction")); pMenus[Menu::MoveTransaction]->setEnabled(false); pMenus[Menu::MarkTransaction]->setEnabled(false); pMenus[Menu::MarkTransactionContext]->setEnabled(false); if (!d->m_selectedTransactions.isEmpty() && !d->m_selectedTransactions.first().isScheduled()) { // enable 'delete transaction' only if at least one of the // selected transactions does not reference a closed account bool enable = false; KMyMoneyRegister::SelectedTransactions::const_iterator it_t; for (it_t = d->m_selectedTransactions.constBegin(); (enable == false) && (it_t != d->m_selectedTransactions.constEnd()); ++it_t) { enable = !(*it_t).transaction().id().isEmpty() && !file->referencesClosedAccount((*it_t).transaction()); } pActions[Action::DeleteTransaction]->setEnabled(enable); if (!d->m_transactionEditor) { QString tooltip = i18n("Duplicate the current selected transactions"); pActions[Action::DuplicateTransaction]->setEnabled(canDuplicateTransactions(d->m_selectedTransactions, tooltip) && !d->m_selectedTransactions[0].transaction().id().isEmpty()); pActions[Action::DuplicateTransaction]->setToolTip(tooltip); if (canEditTransactions(d->m_selectedTransactions, tooltip)) { pActions[Action::EditTransaction]->setEnabled(true); // editing splits is allowed only if we have one transaction selected if (d->m_selectedTransactions.count() == 1) { pActions[Action::EditSplits]->setEnabled(true); } if (d->m_currentAccount.isAssetLiability() && d->m_currentAccount.accountType() != eMyMoney::Account::Type::Investment) { pActions[Action::NewScheduledTransaction]->setEnabled(d->m_selectedTransactions.count() == 1); } } pActions[Action::EditTransaction]->setToolTip(tooltip); if (!d->m_currentAccount.isClosed()) pMenus[Menu::MoveTransaction]->setEnabled(true); pMenus[Menu::MarkTransaction]->setEnabled(true); pMenus[Menu::MarkTransactionContext]->setEnabled(true); // Allow marking the transaction if at least one is selected pActions[Action::MarkCleared]->setEnabled(true); pActions[Action::MarkReconciled]->setEnabled(true); pActions[Action::MarkNotReconciled]->setEnabled(true); pActions[Action::ToggleReconciliationFlag]->setEnabled(true); if (!d->m_accountGoto.isEmpty()) pActions[Action::GoToAccount]->setEnabled(true); if (!d->m_payeeGoto.isEmpty()) pActions[Action::GoToPayee]->setEnabled(true); // Matching is enabled as soon as one regular and one imported transaction is selected int matchedCount = 0; int importedCount = 0; KMyMoneyRegister::SelectedTransactions::const_iterator it; for (it = d->m_selectedTransactions.constBegin(); it != d->m_selectedTransactions.constEnd(); ++it) { if ((*it).transaction().isImported()) ++importedCount; if ((*it).split().isMatched()) ++matchedCount; } if (d->m_selectedTransactions.count() == 2 /* && pActions[Action::TransactionEdit]->isEnabled() */) { pActions[Action::MatchTransaction]->setEnabled(true); } if (importedCount != 0 || matchedCount != 0) pActions[Action::AcceptTransaction]->setEnabled(true); if (matchedCount != 0) { pActions[Action::MatchTransaction]->setEnabled(true); pActions[Action::MatchTransaction]->setText(i18nc("Button text for unmatch transaction", "Unmatch")); pActions[Action::MatchTransaction]->setIcon(QIcon("process-stop")); } if (d->m_selectedTransactions.count() > 1) { pActions[Action::CombineTransactions]->setEnabled(true); } if (d->m_selectedTransactions.count() >= 2) { int singleSplitTransactions = 0; int multipleSplitTransactions = 0; foreach (const KMyMoneyRegister::SelectedTransaction& st, d->m_selectedTransactions) { switch (st.transaction().splitCount()) { case 0: break; case 1: singleSplitTransactions++; break; default: multipleSplitTransactions++; break; } } if (singleSplitTransactions > 0 && multipleSplitTransactions == 1) { pActions[Action::CopySplits]->setEnabled(true); } } if (d->m_selectedTransactions.count() >= 2) { int singleSplitTransactions = 0; int multipleSplitTransactions = 0; foreach(const KMyMoneyRegister::SelectedTransaction& st, d->m_selectedTransactions) { switch(st.transaction().splitCount()) { case 0: break; case 1: singleSplitTransactions++; break; default: multipleSplitTransactions++; break; } } if(singleSplitTransactions > 0 && multipleSplitTransactions == 1) { pActions[Action::CopySplits]->setEnabled(true); } } } else { pActions[Action::AssignTransactionsNumber]->setEnabled(d->m_transactionEditor->canAssignNumber()); pActions[Action::NewTransaction]->setEnabled(false); pActions[Action::DeleteTransaction]->setEnabled(false); QString reason; pActions[Action::EnterTransaction]->setEnabled(d->m_transactionEditor->isComplete(reason)); //FIXME: Port to KDE4 // the next line somehow worked in KDE3 but does not have // any influence under KDE4 /// Works for me when 'reason' is set. Allan pActions[Action::EnterTransaction]->setToolTip(reason); pActions[Action::CancelTransaction]->setEnabled(true); } } } void KGlobalLedgerView::slotAboutToSelectItem(KMyMoneyRegister::RegisterItem* item, bool& okToSelect) { Q_UNUSED(item); slotCancelOrEnterTransactions(okToSelect); } void KGlobalLedgerView::slotUpdateSummaryLine(const KMyMoneyRegister::SelectedTransactions& selection) { Q_D(KGlobalLedgerView); if (selection.count() > 1) { MyMoneyMoney balance; foreach (const KMyMoneyRegister::SelectedTransaction& t, selection) { if (!t.isScheduled()) { balance += t.split().shares(); } } d->m_rightSummaryLabel->setText(QString("%1: %2").arg(QChar(0x2211), balance.formatMoney("", d->m_precision))); } else { if (d->isReconciliationAccount()) { d->m_rightSummaryLabel->setText(i18n("Difference: %1", d->m_totalBalance.formatMoney("", d->m_precision))); } else { if (d->m_currentAccount.accountType() != eMyMoney::Account::Type::Investment) { d->m_rightSummaryLabel->setText(i18n("Balance: %1", d->m_totalBalance.formatMoney("", d->m_precision))); bool showNegative = d->m_totalBalance.isNegative(); if (d->m_currentAccount.accountGroup() == eMyMoney::Account::Type::Liability && !d->m_totalBalance.isZero()) showNegative = !showNegative; if (showNegative) { QPalette palette = d->m_rightSummaryLabel->palette(); palette.setColor(d->m_rightSummaryLabel->foregroundRole(), KMyMoneySettings::schemeColor(SchemeColor::Negative)); d->m_rightSummaryLabel->setPalette(palette); } } else { d->m_rightSummaryLabel->setText(i18n("Investment value: %1%2", d->m_balanceIsApproximated ? "~" : "", d->m_totalBalance.formatMoney(MyMoneyFile::instance()->baseCurrency().tradingSymbol(), d->m_precision))); } } } } void KGlobalLedgerView::resizeEvent(QResizeEvent* ev) { if (MyMoneyFile::instance()->storageAttached()) { Q_D(KGlobalLedgerView); if (d->m_needLoad) d->init(); d->m_register->resize((int)eWidgets::eTransaction::Column::Detail); d->m_form->resize((int)eWidgets::eTransactionForm::Column::Value1); } KMyMoneyViewBase::resizeEvent(ev); } void KGlobalLedgerView::slotSetReconcileAccount(const MyMoneyAccount& acc, const QDate& reconciliationDate, const MyMoneyMoney& endingBalance) { Q_D(KGlobalLedgerView); if(d->m_needLoad) d->init(); if (d->m_reconciliationAccount.id() != acc.id()) { // make sure the account is selected if (!acc.id().isEmpty()) slotSelectAccount(acc.id()); d->m_reconciliationAccount = acc; d->m_reconciliationDate = reconciliationDate; d->m_endingBalance = endingBalance; if (acc.accountGroup() == eMyMoney::Account::Type::Liability) d->m_endingBalance = -endingBalance; d->m_newAccountLoaded = true; if (acc.id().isEmpty()) { d->m_buttonbar->removeAction(pActions[Action::PostponeReconciliation]); d->m_buttonbar->removeAction(pActions[Action::FinishReconciliation]); } else { d->m_buttonbar->addAction(pActions[Action::PostponeReconciliation]); d->m_buttonbar->addAction(pActions[Action::FinishReconciliation]); // when we start reconciliation, we need to reload the view // because no data has been changed. When postponing or finishing // reconciliation, the data change in the engine takes care of updateing // the view. refresh(); } } } void KGlobalLedgerView::slotSetReconcileAccount(const MyMoneyAccount& acc, const QDate& reconciliationDate) { slotSetReconcileAccount(acc, reconciliationDate, MyMoneyMoney()); } void KGlobalLedgerView::slotSetReconcileAccount(const MyMoneyAccount& acc) { slotSetReconcileAccount(acc, QDate(), MyMoneyMoney()); } void KGlobalLedgerView::slotSetReconcileAccount() { slotSetReconcileAccount(MyMoneyAccount(), QDate(), MyMoneyMoney()); } void KGlobalLedgerView::slotShowTransactionMenu(const MyMoneySplit& sp) { Q_UNUSED(sp) pMenus[Menu::Transaction]->exec(QCursor::pos()); } void KGlobalLedgerView::slotContinueReconciliation() { Q_D(KGlobalLedgerView); const auto file = MyMoneyFile::instance(); MyMoneyAccount account; try { account = file->account(d->m_currentAccount.id()); // get rid of previous run. delete d->m_endingBalanceDlg; d->m_endingBalanceDlg = new KEndingBalanceDlg(account, this); if (account.isAssetLiability()) { if (d->m_endingBalanceDlg->exec() == QDialog::Accepted) { if (KMyMoneySettings::autoReconciliation()) { MyMoneyMoney startBalance = d->m_endingBalanceDlg->previousBalance(); MyMoneyMoney endBalance = d->m_endingBalanceDlg->endingBalance(); QDate endDate = d->m_endingBalanceDlg->statementDate(); QList > transactionList; MyMoneyTransactionFilter filter(account.id()); filter.addState((int)eMyMoney::TransactionFilter::State::Cleared); filter.addState((int)eMyMoney::TransactionFilter::State::NotReconciled); filter.setDateFilter(QDate(), endDate); filter.setConsiderCategory(false); filter.setReportAllSplits(true); file->transactionList(transactionList, filter); QList > result = d->automaticReconciliation(account, transactionList, endBalance - startBalance); if (!result.empty()) { QString message = i18n("KMyMoney has detected transactions matching your reconciliation data.\nWould you like KMyMoney to clear these transactions for you?"); if (KMessageBox::questionYesNo(this, message, i18n("Automatic reconciliation"), KStandardGuiItem::yes(), KStandardGuiItem::no(), "AcceptAutomaticReconciliation") == KMessageBox::Yes) { // mark the transactions cleared KMyMoneyRegister::SelectedTransactions oldSelection = d->m_selectedTransactions; d->m_selectedTransactions.clear(); QListIterator > itTransactionSplitResult(result); while (itTransactionSplitResult.hasNext()) { const QPair &transactionSplit = itTransactionSplitResult.next(); d->m_selectedTransactions.append(KMyMoneyRegister::SelectedTransaction(transactionSplit.first, transactionSplit.second, QString())); } // mark all transactions in d->m_selectedTransactions as 'Cleared' d->markTransaction(eMyMoney::Split::State::Cleared); d->m_selectedTransactions = oldSelection; } } } if (!file->isStandardAccount(account.id()) && account.isAssetLiability()) { if (!isVisible()) emit customActionRequested(View::Ledgers, eView::Action::SwitchView); Models::instance()->accountsModel()->slotReconcileAccount(account, d->m_endingBalanceDlg->statementDate(), d->m_endingBalanceDlg->endingBalance()); slotSetReconcileAccount(account, d->m_endingBalanceDlg->statementDate(), d->m_endingBalanceDlg->endingBalance()); // check if the user requests us to create interest // or charge transactions. auto ti = d->m_endingBalanceDlg->interestTransaction(); auto tc = d->m_endingBalanceDlg->chargeTransaction(); MyMoneyFileTransaction ft; try { if (ti != MyMoneyTransaction()) { MyMoneyFile::instance()->addTransaction(ti); } if (tc != MyMoneyTransaction()) { MyMoneyFile::instance()->addTransaction(tc); } ft.commit(); } catch (const MyMoneyException &e) { qWarning("interest transaction not stored: '%s'", e.what()); } // reload the account object as it might have changed in the meantime d->m_reconciliationAccount = file->account(account.id()); updateActions(d->m_currentAccount); updateLedgerActionsInternal(); // slotUpdateActions(); } } } } catch (const MyMoneyException &) { } } void KGlobalLedgerView::slotLedgerSelected(const QString& _accId, const QString& transaction) { auto acc = MyMoneyFile::instance()->account(_accId); QString accId(_accId); switch (acc.accountType()) { case Account::Type::Stock: // if a stock account is selected, we show the // the corresponding parent (investment) account acc = MyMoneyFile::instance()->account(acc.parentAccountId()); accId = acc.id(); // intentional fall through case Account::Type::Checkings: case Account::Type::Savings: case Account::Type::Cash: case Account::Type::CreditCard: case Account::Type::Loan: case Account::Type::Asset: case Account::Type::Liability: case Account::Type::AssetLoan: case Account::Type::Income: case Account::Type::Expense: case Account::Type::Investment: case Account::Type::Equity: if (!isVisible()) emit customActionRequested(View::Ledgers, eView::Action::SwitchView); slotSelectAccount(accId, transaction); break; case Account::Type::CertificateDep: case Account::Type::MoneyMarket: case Account::Type::Currency: qDebug("No ledger view available for account type %d", (int)acc.accountType()); break; default: qDebug("Unknown account type %d in KMyMoneyView::slotLedgerSelected", (int)acc.accountType()); break; } } void KGlobalLedgerView::slotSelectByObject(const MyMoneyObject& obj, eView::Intent intent) { switch(intent) { case eView::Intent::UpdateActions: updateActions(obj); break; case eView::Intent::FinishEnteringOverdueScheduledTransactions: slotContinueReconciliation(); break; case eView::Intent::SynchronizeAccountInLedgersView: slotSelectAccount(obj); break; default: break; } } void KGlobalLedgerView::slotSelectByVariant(const QVariantList& variant, eView::Intent intent) { switch(intent) { case eView::Intent::ShowTransaction: if (variant.count() == 2) slotLedgerSelected(variant.at(0).toString(), variant.at(1).toString()); break; case eView::Intent::SelectRegisterTransactions: if (variant.count() == 1) updateLedgerActions(variant.at(0).value()); break; default: break; } } void KGlobalLedgerView::slotSelectAccount(const MyMoneyObject& obj) { Q_D(KGlobalLedgerView); if (typeid(obj) != typeid(MyMoneyAccount)) return/* false */; d->m_lastSelectedAccountID = obj.id(); } void KGlobalLedgerView::slotSelectAccount(const QString& id) { slotSelectAccount(id, QString()); } bool KGlobalLedgerView::slotSelectAccount(const QString& id, const QString& transactionId) { Q_D(KGlobalLedgerView); auto rc = true; if (!id.isEmpty()) { if (d->m_currentAccount.id() != id) { try { d->m_currentAccount = MyMoneyFile::instance()->account(id); // if a stock account is selected, we show the // the corresponding parent (investment) account if (d->m_currentAccount.isInvest()) { d->m_currentAccount = MyMoneyFile::instance()->account(d->m_currentAccount.parentAccountId()); } d->m_lastSelectedAccountID = d->m_currentAccount.id(); d->m_newAccountLoaded = true; refresh(); } catch (const MyMoneyException &) { qDebug("Unable to retrieve account %s", qPrintable(id)); rc = false; } } else { // we need to refresh m_account.m_accountList, a child could have been deleted d->m_currentAccount = MyMoneyFile::instance()->account(id); emit selectByObject(d->m_currentAccount, eView::Intent::None); emit selectByObject(d->m_currentAccount, eView::Intent::SynchronizeAccountInInvestmentView); } d->selectTransaction(transactionId); } return rc; } bool KGlobalLedgerView::selectEmptyTransaction() { Q_D(KGlobalLedgerView); bool rc = false; if (!d->m_inEditMode) { // in case we don't know the type of transaction to be created, // have at least one selected transaction and the id of // this transaction is not empty, we take it as template for the // transaction to be created KMyMoneyRegister::SelectedTransactions list(d->m_register); if ((d->m_action == eWidgets::eRegister::Action::None) && (!list.isEmpty()) && (!list[0].transaction().id().isEmpty())) { // the new transaction to be created will have the same type // as the one that currently has the focus KMyMoneyRegister::Transaction* t = dynamic_cast(d->m_register->focusItem()); if (t) d->m_action = t->actionType(); d->m_register->clearSelection(); } // if we still don't have an idea which type of transaction // to create, we use the default. if (d->m_action == eWidgets::eRegister::Action::None) { d->setupDefaultAction(); } d->m_register->selectItem(d->m_register->lastItem()); d->m_register->updateRegister(); rc = true; } return rc; } TransactionEditor* KGlobalLedgerView::startEdit(const KMyMoneyRegister::SelectedTransactions& list) { Q_D(KGlobalLedgerView); // we use the warnlevel to keep track, if we have to warn the // user that some or all splits have been reconciled or if the // user cannot modify the transaction if at least one split // has the status frozen. The following value are used: // // 0 - no sweat, user can modify // 1 - user should be warned that at least one split has been reconciled // already // 2 - user will be informed, that this transaction cannot be changed anymore int warnLevel = list.warnLevel(); Q_ASSERT(warnLevel < 2); // otherwise the edit action should not be enabled switch (warnLevel) { case 0: break; case 1: if (KMessageBox::warningContinueCancel(this, i18n( "At least one split of the selected transactions has been reconciled. " "Do you wish to continue to edit the transactions anyway?" ), i18n("Transaction already reconciled"), KStandardGuiItem::cont(), KStandardGuiItem::cancel(), "EditReconciledTransaction") == KMessageBox::Cancel) { warnLevel = 2; } break; case 2: KMessageBox::sorry(this, i18n("At least one split of the selected transactions has been frozen. " "Editing the transactions is therefore prohibited."), i18n("Transaction already frozen")); break; case 3: KMessageBox::sorry(this, i18n("At least one split of the selected transaction references an account that has been closed. " "Editing the transactions is therefore prohibited."), i18n("Account closed")); break; } if (warnLevel > 1) { d->m_register->endEdit(); return 0; } TransactionEditor* editor = 0; KMyMoneyRegister::Transaction* item = dynamic_cast(d->m_register->focusItem()); if (item) { // in case the current focus item is not selected, we move the focus to the first selected transaction if (!item->isSelected()) { KMyMoneyRegister::RegisterItem* p; for (p = d->m_register->firstItem(); p; p = p->nextItem()) { KMyMoneyRegister::Transaction* t = dynamic_cast(p); if (t && t->isSelected()) { d->m_register->setFocusItem(t); item = t; break; } } } // decide, if we edit in the register or in the form TransactionEditorContainer* parent; if (d->m_formFrame->isVisible()) parent = d->m_form; else { parent = d->m_register; } editor = item->createEditor(parent, list, KGlobalLedgerViewPrivate::m_lastPostDate); // check that we use the same transaction commodity in all selected transactions // if not, we need to update this in the editor's list. The user can also bail out // of this operation which means that we have to stop editing here. if (editor) { if (!editor->fixTransactionCommodity(d->m_currentAccount)) { // if the user wants to quit, we need to destroy the editor // and bail out delete editor; editor = 0; } } if (editor) { if (parent == d->m_register) { // make sure, the height of the table is correct d->m_register->updateRegister(KMyMoneySettings::ledgerLens() | !KMyMoneySettings::transactionForm()); } d->m_inEditMode = true; connect(editor, &TransactionEditor::transactionDataSufficient, pActions[Action::EnterTransaction], &QAction::setEnabled); connect(editor, &TransactionEditor::returnPressed, pActions[Action::EnterTransaction], &QAction::trigger); connect(editor, &TransactionEditor::escapePressed, pActions[Action::CancelTransaction], &QAction::trigger); connect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, editor, &TransactionEditor::slotReloadEditWidgets); connect(editor, &TransactionEditor::finishEdit, this, &KGlobalLedgerView::slotLeaveEditMode); connect(editor, &TransactionEditor::objectCreation, d->m_mousePressFilter, &MousePressFilter::setFilterDeactive); connect(editor, &TransactionEditor::assignNumber, this, &KGlobalLedgerView::slotAssignNumber); connect(editor, &TransactionEditor::lastPostDateUsed, this, &KGlobalLedgerView::slotKeepPostDate); // create the widgets, place them in the parent and load them with data // setup tab order d->m_tabOrderWidgets.clear(); editor->setup(d->m_tabOrderWidgets, d->m_currentAccount, d->m_action); Q_ASSERT(!d->m_tabOrderWidgets.isEmpty()); // install event filter in all taborder widgets QWidgetList::const_iterator it_w = d->m_tabOrderWidgets.constBegin(); for (; it_w != d->m_tabOrderWidgets.constEnd(); ++it_w) { (*it_w)->installEventFilter(this); } // Install a filter that checks if a mouse press happened outside // of one of our own widgets. qApp->installEventFilter(d->m_mousePressFilter); // Check if the editor has some preference on where to set the focus // If not, set the focus to the first widget in the tab order QWidget* focusWidget = editor->firstWidget(); if (!focusWidget) focusWidget = d->m_tabOrderWidgets.first(); // for some reason, this only works reliably if delayed a bit QTimer::singleShot(10, focusWidget, SLOT(setFocus())); // preset to 'I have no idea which type to create' for the next round. d->m_action = eWidgets::eRegister::Action::None; } } return editor; } void KGlobalLedgerView::slotTransactionsContextMenuRequested() { Q_D(KGlobalLedgerView); auto transactions = d->m_selectedTransactions; updateLedgerActionsInternal(); // emit transactionsSelected(d->m_selectedTransactions); // that should select MyMoneySchedule in KScheduledView if (!transactions.isEmpty() && transactions.first().isScheduled()) emit selectByObject(MyMoneyFile::instance()->schedule(transactions.first().scheduleId()), eView::Intent::OpenContextMenu); else slotShowTransactionMenu(MyMoneySplit()); } void KGlobalLedgerView::slotLeaveEditMode(const KMyMoneyRegister::SelectedTransactions& list) { Q_D(KGlobalLedgerView); d->m_inEditMode = false; qApp->removeEventFilter(d->m_mousePressFilter); // a possible focusOut event may have removed the focus, so we // install it back again. d->m_register->focusItem()->setFocus(true); // if we come back from editing a new item, we make sure that // we always select the very last known transaction entry no // matter if the transaction has been created or not. if (list.count() && list[0].transaction().id().isEmpty()) { // block signals to prevent some infinite loops that might occur here. d->m_register->blockSignals(true); d->m_register->clearSelection(); KMyMoneyRegister::RegisterItem* p = d->m_register->lastItem(); if (p && p->prevItem()) p = p->prevItem(); d->m_register->selectItem(p); d->m_register->updateRegister(true); d->m_register->blockSignals(false); // we need to update the form manually as sending signals was blocked KMyMoneyRegister::Transaction* t = dynamic_cast(p); if (t) d->m_form->slotSetTransaction(t); } else { if (!KMyMoneySettings::transactionForm()) { // update the row height of the transactions because it might differ between viewing/editing mode when not using the transaction form d->m_register->blockSignals(true); d->m_register->updateRegister(true); d->m_register->blockSignals(false); } } d->m_needsRefresh = true; // TODO: Why transaction in view doesn't update without this? if (d->m_needsRefresh) refresh(); d->m_register->endEdit(); d->m_register->setFocus(); } bool KGlobalLedgerView::focusNextPrevChild(bool next) { Q_D(KGlobalLedgerView); bool rc = false; // qDebug() << "----------------------------------------------------------"; // qDebug() << "KGlobalLedgerView::focusNextPrevChild, editmode=" << d->m_inEditMode; if (d->m_inEditMode) { QWidget *w = 0; w = qApp->focusWidget(); int currentWidgetIndex = d->m_tabOrderWidgets.indexOf(w); const auto startIndex = currentWidgetIndex; // qDebug() << "Focus is at currentWidgetIndex" << currentWidgetIndex << w->objectName(); do { while (w && currentWidgetIndex == -1) { // qDebug() << w->objectName() << "not in list, use parent"; w = w->parentWidget(); currentWidgetIndex = d->m_tabOrderWidgets.indexOf(w); } // qDebug() << "Focus is at currentWidgetIndex" << currentWidgetIndex << w->objectName(); if (currentWidgetIndex != -1) { // if(w) qDebug() << "tab order is at" << w->objectName(); currentWidgetIndex += next ? 1 : -1; if (currentWidgetIndex < 0) currentWidgetIndex = d->m_tabOrderWidgets.size() - 1; else if (currentWidgetIndex >= d->m_tabOrderWidgets.size()) currentWidgetIndex = 0; w = d->m_tabOrderWidgets[currentWidgetIndex]; // qDebug() << "currentWidgetIndex" << currentWidgetIndex << w->objectName() << w->isVisible(); if (((w->focusPolicy() & Qt::TabFocus) == Qt::TabFocus) && w->isVisible() && w->isEnabled()) { // qDebug() << "Set focus to" << w->objectName(); w->setFocus(); rc = true; break; } } else { break; } } while(currentWidgetIndex != startIndex); } else rc = KMyMoneyViewBase::focusNextPrevChild(next); return rc; } bool KGlobalLedgerView::eventFilter(QObject* o, QEvent* e) { Q_D(KGlobalLedgerView); bool rc = false; // Need to capture mouse position here as QEvent::ToolTip is too slow d->m_tooltipPosn = QCursor::pos(); if (e->type() == QEvent::KeyPress) { if (d->m_inEditMode) { // qDebug("object = %s, key = %d", o->className(), k->key()); if (o == d->m_register) { // we hide all key press events from the register // while editing a transaction rc = true; } } } if (!rc) rc = KMyMoneyViewBase::eventFilter(o, e); return rc; } void KGlobalLedgerView::slotSortOptions() { Q_D(KGlobalLedgerView); QPointer dlg = new KSortOptionDlg(this); QString key; QString sortOrder, def; if (d->isReconciliationAccount()) { key = "kmm-sort-reconcile"; def = KMyMoneySettings::sortReconcileView(); } else { key = "kmm-sort-std"; def = KMyMoneySettings::sortNormalView(); } // check if we have an account override of the sort order if (!d->m_currentAccount.value(key).isEmpty()) sortOrder = d->m_currentAccount.value(key); QString oldOrder = sortOrder; dlg->setSortOption(sortOrder, def); if (dlg->exec() == QDialog::Accepted) { if (dlg != 0) { sortOrder = dlg->sortOption(); if (sortOrder != oldOrder) { if (sortOrder.isEmpty()) { d->m_currentAccount.deletePair(key); } else { d->m_currentAccount.setValue(key, sortOrder); } MyMoneyFileTransaction ft; try { MyMoneyFile::instance()->modifyAccount(d->m_currentAccount); ft.commit(); } catch (const MyMoneyException &e) { qDebug("Unable to update sort order for account '%s': %s", qPrintable(d->m_currentAccount.name()), e.what()); } } } } delete dlg; } void KGlobalLedgerView::slotToggleTransactionMark(KMyMoneyRegister::Transaction* /* t */) { Q_D(KGlobalLedgerView); if (!d->m_inEditMode) { slotToggleReconciliationFlag(); } } void KGlobalLedgerView::slotKeepPostDate(const QDate& date) { KGlobalLedgerViewPrivate::m_lastPostDate = date; } QString KGlobalLedgerView::accountId() const { Q_D(const KGlobalLedgerView); return d->m_currentAccount.id(); } bool KGlobalLedgerView::canCreateTransactions(QString& tooltip) const { Q_D(const KGlobalLedgerView); bool rc = true; if (d->m_currentAccount.id().isEmpty()) { tooltip = i18n("Cannot create transactions when no account is selected."); rc = false; } if (d->m_currentAccount.accountGroup() == eMyMoney::Account::Type::Income || d->m_currentAccount.accountGroup() == eMyMoney::Account::Type::Expense) { tooltip = i18n("Cannot create transactions in the context of a category."); d->showTooltip(tooltip); rc = false; } if (d->m_currentAccount.isClosed()) { tooltip = i18n("Cannot create transactions in a closed account."); d->showTooltip(tooltip); rc = false; } return rc; } bool KGlobalLedgerView::canModifyTransactions(const KMyMoneyRegister::SelectedTransactions& list, QString& tooltip) const { Q_D(const KGlobalLedgerView); return d->canProcessTransactions(list, tooltip) && list.canModify(); } bool KGlobalLedgerView::canDuplicateTransactions(const KMyMoneyRegister::SelectedTransactions& list, QString& tooltip) const { Q_D(const KGlobalLedgerView); return d->canProcessTransactions(list, tooltip) && list.canDuplicate(); } bool KGlobalLedgerView::canEditTransactions(const KMyMoneyRegister::SelectedTransactions& list, QString& tooltip) const { Q_D(const KGlobalLedgerView); // check if we can edit the list of transactions. We can edit, if // // a) no mix of standard and investment transactions exist // b) if a split transaction is selected, this is the only selection // c) none of the splits is frozen // d) the transaction having the current focus is selected // check for d) if (!d->canProcessTransactions(list, tooltip)) return false; // check for c) if (list.warnLevel() == 2) { tooltip = i18n("Cannot edit transactions with frozen splits."); d->showTooltip(tooltip); return false; } bool rc = true; int investmentTransactions = 0; int normalTransactions = 0; if (d->m_currentAccount.accountGroup() == eMyMoney::Account::Type::Income || d->m_currentAccount.accountGroup() == eMyMoney::Account::Type::Expense) { tooltip = i18n("Cannot edit transactions in the context of a category."); d->showTooltip(tooltip); rc = false; } if (d->m_currentAccount.isClosed()) { tooltip = i18n("Cannot create or edit any transactions in Account %1 as it is closed", d->m_currentAccount.name()); d->showTooltip(tooltip); rc = false; } KMyMoneyRegister::SelectedTransactions::const_iterator it_t; QString action; for (it_t = list.begin(); rc && it_t != list.end(); ++it_t) { if ((*it_t).transaction().id().isEmpty()) { tooltip.clear(); rc = false; continue; } if (KMyMoneyUtils::transactionType((*it_t).transaction()) == KMyMoneyUtils::InvestmentTransaction) { if (action.isEmpty()) { action = (*it_t).split().action(); continue; } if (action == (*it_t).split().action()) { continue; } else { tooltip = (i18n("Cannot edit mixed investment action/type transactions together.")); d->showTooltip(tooltip); rc = false; break; } } if (KMyMoneyUtils::transactionType((*it_t).transaction()) == KMyMoneyUtils::InvestmentTransaction) ++investmentTransactions; else ++normalTransactions; // check for a) if (investmentTransactions != 0 && normalTransactions != 0) { tooltip = i18n("Cannot edit investment transactions and non-investment transactions together."); d->showTooltip(tooltip); rc = false; break; } // check for b) but only for normalTransactions if ((*it_t).transaction().splitCount() > 2 && normalTransactions != 0) { if (list.count() > 1) { tooltip = i18n("Cannot edit multiple split transactions at once."); d->showTooltip(tooltip); rc = false; break; } } } // check for multiple transactions being selected in an investment account // we do not allow editing in this case: https://bugs.kde.org/show_bug.cgi?id=240816 // later on, we might allow to edit investment transactions of the same type /// Can now disable the following check. /* if (rc == true && investmentTransactions > 1) { tooltip = i18n("Cannot edit multiple investment transactions at once"); rc = false; }*/ // now check that we have the correct account type for investment transactions if (rc == true && investmentTransactions != 0) { if (d->m_currentAccount.accountType() != eMyMoney::Account::Type::Investment) { tooltip = i18n("Cannot edit investment transactions in the context of this account."); rc = false; } } return rc; } void KGlobalLedgerView::slotMoveToAccount(const QString& id) { Q_D(KGlobalLedgerView); // close the menu, if it is still open if (pMenus[Menu::Transaction]->isVisible()) pMenus[Menu::Transaction]->close(); if (!d->m_selectedTransactions.isEmpty()) { MyMoneyFileTransaction ft; try { foreach (const auto selection, d->m_selectedTransactions) { if (d->m_currentAccount.accountType() == eMyMoney::Account::Type::Investment) { d->moveInvestmentTransaction(d->m_currentAccount.id(), id, selection.transaction()); } else { auto changed = false; auto t = selection.transaction(); foreach (const auto split, selection.transaction().splits()) { if (split.accountId() == d->m_currentAccount.id()) { MyMoneySplit s = split; s.setAccountId(id); t.modifySplit(s); changed = true; } } if (changed) { MyMoneyFile::instance()->modifyTransaction(t); } } } ft.commit(); } catch (const MyMoneyException &) { } } } void KGlobalLedgerView::slotUpdateMoveToAccountMenu() { Q_D(KGlobalLedgerView); d->createTransactionMoveMenu(); // in case we were not able to create the selector, we // better get out of here. Anything else would cause // a crash later on (accountSet.load) if (!d->m_moveToAccountSelector) return; if (!d->m_currentAccount.id().isEmpty()) { AccountSet accountSet; if (d->m_currentAccount.accountType() == eMyMoney::Account::Type::Investment) { accountSet.addAccountType(eMyMoney::Account::Type::Investment); } else if (d->m_currentAccount.isAssetLiability()) { accountSet.addAccountType(eMyMoney::Account::Type::Checkings); accountSet.addAccountType(eMyMoney::Account::Type::Savings); accountSet.addAccountType(eMyMoney::Account::Type::Cash); accountSet.addAccountType(eMyMoney::Account::Type::AssetLoan); accountSet.addAccountType(eMyMoney::Account::Type::CertificateDep); accountSet.addAccountType(eMyMoney::Account::Type::MoneyMarket); accountSet.addAccountType(eMyMoney::Account::Type::Asset); accountSet.addAccountType(eMyMoney::Account::Type::Currency); accountSet.addAccountType(eMyMoney::Account::Type::CreditCard); accountSet.addAccountType(eMyMoney::Account::Type::Loan); accountSet.addAccountType(eMyMoney::Account::Type::Liability); } else if (d->m_currentAccount.isIncomeExpense()) { accountSet.addAccountType(eMyMoney::Account::Type::Income); accountSet.addAccountType(eMyMoney::Account::Type::Expense); } accountSet.load(d->m_moveToAccountSelector); // remove those accounts that we currently reference foreach (const auto selection, d->m_selectedTransactions) { foreach (const auto split, selection.transaction().splits()) { d->m_moveToAccountSelector->removeItem(split.accountId()); } } // remove those accounts from the list that are denominated // in a different currency auto list = d->m_moveToAccountSelector->accountList(); QList::const_iterator it_a; for (it_a = list.constBegin(); it_a != list.constEnd(); ++it_a) { auto acc = MyMoneyFile::instance()->account(*it_a); if (acc.currencyId() != d->m_currentAccount.currencyId()) d->m_moveToAccountSelector->removeItem((*it_a)); } } } void KGlobalLedgerView::slotObjectDestroyed(QObject* o) { Q_D(KGlobalLedgerView); if (o == d->m_moveToAccountSelector) { d->m_moveToAccountSelector = nullptr; } } void KGlobalLedgerView::slotCancelOrEnterTransactions(bool& okToSelect) { Q_D(KGlobalLedgerView); static bool oneTime = false; if (!oneTime) { oneTime = true; auto dontShowAgain = "CancelOrEditTransaction"; // qDebug("KMyMoneyApp::slotCancelOrEndEdit"); if (d->m_transactionEditor) { if (KMyMoneySettings::focusChangeIsEnter() && pActions[Action::EnterTransaction]->isEnabled()) { slotEnterTransaction(); if (d->m_transactionEditor) { // if at this stage the editor is still there that means that entering the transaction was cancelled // for example by pressing cancel on the exchange rate editor so we must stay in edit mode okToSelect = false; } } else { // okToSelect is preset to true if a cancel of the dialog is useful and false if it is not int rc; KGuiItem noGuiItem = KStandardGuiItem::save(); KGuiItem yesGuiItem = KStandardGuiItem::discard(); KGuiItem cancelGuiItem = KStandardGuiItem::cont(); // if the transaction can't be entered make sure that it can't be entered by pressing no either if (!pActions[Action::EnterTransaction]->isEnabled()) { noGuiItem.setEnabled(false); noGuiItem.setToolTip(pActions[Action::EnterTransaction]->toolTip()); } // in case we have a new transaction and cannot save it we simply cancel if (!pActions[Action::EnterTransaction]->isEnabled() && d->m_transactionEditor && d->m_transactionEditor->createNewTransaction()) { rc = KMessageBox::Yes; } else if (okToSelect == true) { rc = KMessageBox::warningYesNoCancel(this, i18n("

Please select what you want to do: discard the changes, save the changes or continue to edit the transaction.

You can also set an option to save the transaction automatically when e.g. selecting another transaction.

"), i18n("End transaction edit"), yesGuiItem, noGuiItem, cancelGuiItem, dontShowAgain); } else { rc = KMessageBox::warningYesNo(this, i18n("

Please select what you want to do: discard or save the changes.

You can also set an option to save the transaction automatically when e.g. selecting another transaction.

"), i18n("End transaction edit"), yesGuiItem, noGuiItem, dontShowAgain); } switch (rc) { case KMessageBox::Yes: slotCancelTransaction(); break; case KMessageBox::No: slotEnterTransaction(); // make sure that we'll see this message the next time no matter // if the user has chosen the 'Don't show again' checkbox KMessageBox::enableMessage(dontShowAgain); if (d->m_transactionEditor) { // if at this stage the editor is still there that means that entering the transaction was cancelled // for example by pressing cancel on the exchange rate editor so we must stay in edit mode okToSelect = false; } break; case KMessageBox::Cancel: // make sure that we'll see this message the next time no matter // if the user has chosen the 'Don't show again' checkbox KMessageBox::enableMessage(dontShowAgain); okToSelect = false; break; } } } oneTime = false; } } void KGlobalLedgerView::slotNewSchedule(const MyMoneyTransaction& _t, eMyMoney::Schedule::Occurrence occurrence) { KEditScheduleDlg::newSchedule(_t, occurrence); } void KGlobalLedgerView::slotNewTransactionForm(eWidgets::eRegister::Action id) { Q_D(KGlobalLedgerView); if (!d->m_inEditMode) { d->m_action = id; // since we jump here via code, we have to make sure to react only // if the action is enabled if (pActions[Action::NewTransaction]->isEnabled()) { if (d->createNewTransaction()) { d->m_transactionEditor = d->startEdit(d->m_selectedTransactions); if (d->m_transactionEditor) { KMyMoneyMVCCombo::setSubstringSearchForChildren(this/*d->m_myMoneyView*/, !KMyMoneySettings::stringMatchFromStart()); KMyMoneyPayeeCombo* payeeEdit = dynamic_cast(d->m_transactionEditor->haveWidget("payee")); if (payeeEdit && !d->m_lastPayeeEnteredId.isEmpty()) { // in case we entered a new transaction before and used a payee, // we reuse it here. Save the text to the edit widget, select it // so that hitting any character will start entering another payee. payeeEdit->setSelectedItem(d->m_lastPayeeEnteredId); payeeEdit->lineEdit()->selectAll(); } if (d->m_transactionEditor) { connect(d->m_transactionEditor.data(), &TransactionEditor::statusProgress, this, &KGlobalLedgerView::slotStatusProgress); connect(d->m_transactionEditor.data(), &TransactionEditor::statusMsg, this, &KGlobalLedgerView::slotStatusMsg); connect(d->m_transactionEditor.data(), &TransactionEditor::scheduleTransaction, this, &KGlobalLedgerView::slotNewSchedule); } updateLedgerActionsInternal(); // emit transactionsSelected(d->m_selectedTransactions); } } } } } void KGlobalLedgerView::slotNewTransaction() { // in case the view is not visible ... if (!isVisible()) { // we switch to it pActions[Action::ShowLedgersView]->activate(QAction::ActionEvent::Trigger); QString tooltip; if (!canCreateTransactions(tooltip)) { // and inform the user via a dialog about the reason // why a transaction cannot be created KMessageBox::sorry(this, tooltip); return; } } slotNewTransactionForm(eWidgets::eRegister::Action::None); } void KGlobalLedgerView::slotEditTransaction() { Q_D(KGlobalLedgerView); // qDebug("KMyMoneyApp::slotTransactionsEdit()"); // since we jump here via code, we have to make sure to react only // if the action is enabled if (pActions[Action::EditTransaction]->isEnabled()) { // as soon as we edit a transaction, we don't remember the last payee entered d->m_lastPayeeEnteredId.clear(); d->m_transactionEditor = d->startEdit(d->m_selectedTransactions); KMyMoneyMVCCombo::setSubstringSearchForChildren(this/*d->m_myMoneyView*/, !KMyMoneySettings::stringMatchFromStart()); updateLedgerActionsInternal(); } } void KGlobalLedgerView::slotDeleteTransaction() { Q_D(KGlobalLedgerView); // since we may jump here via code, we have to make sure to react only // if the action is enabled if (!pActions[Action::DeleteTransaction]->isEnabled()) return; if (d->m_selectedTransactions.isEmpty()) return; if (d->m_selectedTransactions.warnLevel() == 1) { if (KMessageBox::warningContinueCancel(this, i18n("At least one split of the selected transactions has been reconciled. " "Do you wish to delete the transactions anyway?"), i18n("Transaction already reconciled")) == KMessageBox::Cancel) return; } auto msg = i18np("Do you really want to delete the selected transaction?", "Do you really want to delete all %1 selected transactions?", d->m_selectedTransactions.count()); if (KMessageBox::questionYesNo(this, msg, i18n("Delete transaction")) == KMessageBox::Yes) { //KMSTATUS(i18n("Deleting transactions")); d->doDeleteTransactions(); } } void KGlobalLedgerView::slotDuplicateTransaction() { Q_D(KGlobalLedgerView); // since we may jump here via code, we have to make sure to react only // if the action is enabled if (pActions[Action::DuplicateTransaction]->isEnabled()) { KMyMoneyRegister::SelectedTransactions selectionList = d->m_selectedTransactions; KMyMoneyRegister::SelectedTransactions::iterator it_t; int i = 0; int cnt = d->m_selectedTransactions.count(); // KMSTATUS(i18n("Duplicating transactions")); emit selectByVariant(QVariantList {QVariant(0), QVariant(cnt)}, eView::Intent::ReportProgress); MyMoneyFileTransaction ft; MyMoneyTransaction lt; try { foreach (const auto selection, selectionList) { auto t = selection.transaction(); // wipe out any reconciliation information for (auto& split : t.splits()) { split.setReconcileFlag(eMyMoney::Split::State::NotReconciled); split.setReconcileDate(QDate()); split.setBankID(QString()); } // clear invalid data t.setEntryDate(QDate()); t.clearId(); // and set the post date to today t.setPostDate(QDate::currentDate()); MyMoneyFile::instance()->addTransaction(t); lt = t; emit selectByVariant(QVariantList {QVariant(i++), QVariant(0)}, eView::Intent::ReportProgress); } ft.commit(); // select the new transaction in the ledger if (!d->m_currentAccount.id().isEmpty()) slotLedgerSelected(d->m_currentAccount.id(), lt.id()); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(this, i18n("Unable to duplicate transaction(s)"), QString::fromLatin1(e.what())); } // switch off the progress bar emit selectByVariant(QVariantList {QVariant(-1), QVariant(-1)}, eView::Intent::ReportProgress); } } void KGlobalLedgerView::slotEnterTransaction() { Q_D(KGlobalLedgerView); // since we jump here via code, we have to make sure to react only // if the action is enabled if (pActions[Action::EnterTransaction]->isEnabled()) { // disable the action while we process it to make sure it's processed only once since // d->m_transactionEditor->enterTransactions(newId) will run QCoreApplication::processEvents // we could end up here twice which will cause a crash slotUpdateActions() will enable the action again pActions[Action::EnterTransaction]->setEnabled(false); if (d->m_transactionEditor) { QString accountId = d->m_currentAccount.id(); QString newId; connect(d->m_transactionEditor.data(), &TransactionEditor::balanceWarning, d->m_balanceWarning.data(), &KBalanceWarning::slotShowMessage); if (d->m_transactionEditor->enterTransactions(newId)) { KMyMoneyPayeeCombo* payeeEdit = dynamic_cast(d->m_transactionEditor->haveWidget("payee")); if (payeeEdit && !newId.isEmpty()) { d->m_lastPayeeEnteredId = payeeEdit->selectedItem(); } d->deleteTransactionEditor(); } if (!newId.isEmpty()) { slotLedgerSelected(accountId, newId); } } updateLedgerActionsInternal(); } } void KGlobalLedgerView::slotAcceptTransaction() { Q_D(KGlobalLedgerView); KMyMoneyRegister::SelectedTransactions list = d->m_selectedTransactions; KMyMoneyRegister::SelectedTransactions::const_iterator it_t; int cnt = list.count(); int i = 0; emit selectByVariant(QVariantList {QVariant(0), QVariant(cnt)}, eView::Intent::ReportProgress); MyMoneyFileTransaction ft; try { for (it_t = list.constBegin(); it_t != list.constEnd(); ++it_t) { // reload transaction in case it got changed during the course of this loop MyMoneyTransaction t = MyMoneyFile::instance()->transaction((*it_t).transaction().id()); if (t.isImported()) { t.setImported(false); if (!d->m_currentAccount.id().isEmpty()) { foreach (const auto split, t.splits()) { if (split.accountId() == d->m_currentAccount.id()) { if (split.reconcileFlag() == eMyMoney::Split::State::NotReconciled) { MyMoneySplit s = split; s.setReconcileFlag(eMyMoney::Split::State::Cleared); t.modifySplit(s); } } } } MyMoneyFile::instance()->modifyTransaction(t); } if ((*it_t).split().isMatched()) { // reload split in case it got changed during the course of this loop MyMoneySplit s = t.splitById((*it_t).split().id()); TransactionMatcher matcher(d->m_currentAccount); matcher.accept(t, s); } emit selectByVariant(QVariantList {QVariant(i++), QVariant(0)}, eView::Intent::ReportProgress); } emit selectByVariant(QVariantList {QVariant(-1), QVariant(-1)}, eView::Intent::ReportProgress); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(this, i18n("Unable to accept transaction"), QString::fromLatin1(e.what())); } } void KGlobalLedgerView::slotCancelTransaction() { Q_D(KGlobalLedgerView); // since we jump here via code, we have to make sure to react only // if the action is enabled if (pActions[Action::CancelTransaction]->isEnabled()) { // make sure, we block the enter function pActions[Action::EnterTransaction]->setEnabled(false); // qDebug("KMyMoneyApp::slotTransactionsCancel"); d->deleteTransactionEditor(); updateLedgerActions(d->m_selectedTransactions); emit selectByVariant(QVariantList {QVariant::fromValue(d->m_selectedTransactions)}, eView::Intent::SelectRegisterTransactions); } } void KGlobalLedgerView::slotEditSplits() { Q_D(KGlobalLedgerView); // since we jump here via code, we have to make sure to react only // if the action is enabled if (pActions[Action::EditSplits]->isEnabled()) { // as soon as we edit a transaction, we don't remember the last payee entered d->m_lastPayeeEnteredId.clear(); d->m_transactionEditor = d->startEdit(d->m_selectedTransactions); updateLedgerActions(d->m_selectedTransactions); emit selectByVariant(QVariantList {QVariant::fromValue(d->m_selectedTransactions)}, eView::Intent::SelectRegisterTransactions); if (d->m_transactionEditor) { KMyMoneyMVCCombo::setSubstringSearchForChildren(this/*d->m_myMoneyView*/, !KMyMoneySettings::stringMatchFromStart()); if (d->m_transactionEditor->slotEditSplits() == QDialog::Accepted) { MyMoneyFileTransaction ft; try { QString id; connect(d->m_transactionEditor.data(), &TransactionEditor::balanceWarning, d->m_balanceWarning.data(), &KBalanceWarning::slotShowMessage); d->m_transactionEditor->enterTransactions(id); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(this, i18n("Unable to modify transaction"), QString::fromLatin1(e.what())); } } } d->deleteTransactionEditor(); updateLedgerActions(d->m_selectedTransactions); emit selectByVariant(QVariantList {QVariant::fromValue(d->m_selectedTransactions)}, eView::Intent::SelectRegisterTransactions); } } void KGlobalLedgerView::slotCopySplits() { Q_D(KGlobalLedgerView); const auto file = MyMoneyFile::instance(); if (d->m_selectedTransactions.count() >= 2) { int singleSplitTransactions = 0; int multipleSplitTransactions = 0; KMyMoneyRegister::SelectedTransaction selectedSourceTransaction; foreach (const auto& st, d->m_selectedTransactions) { switch (st.transaction().splitCount()) { case 0: break; case 1: singleSplitTransactions++; break; default: selectedSourceTransaction = st; multipleSplitTransactions++; break; } } if (singleSplitTransactions > 0 && multipleSplitTransactions == 1) { MyMoneyFileTransaction ft; try { const auto& sourceTransaction = selectedSourceTransaction.transaction(); const auto& sourceSplit = selectedSourceTransaction.split(); foreach (const KMyMoneyRegister::SelectedTransaction& st, d->m_selectedTransactions) { auto t = st.transaction(); // don't process the source transaction if (sourceTransaction.id() == t.id()) { continue; } const auto& baseSplit = st.split(); if (t.splitCount() == 1) { foreach (const auto& split, sourceTransaction.splits()) { // Don't copy the source split, as we already have that // as part of the destination transaction if (split.id() == sourceSplit.id()) { continue; } MyMoneySplit sp(split); // clear the ID and reconciliation state sp.clearId(); sp.setReconcileFlag(eMyMoney::Split::State::NotReconciled); sp.setReconcileDate(QDate()); // in case it is a simple transaction consisting of two splits, // we can adjust the share and value part of the second split we // just created. We need to keep a possible price in mind in case // of different currencies if (sourceTransaction.splitCount() == 2) { sp.setValue(-baseSplit.value()); sp.setShares(-(baseSplit.shares() * baseSplit.price())); } t.addSplit(sp); } + // check if we need to add/update a VAT assignment + file->updateVAT(t); + + // and store the modified transaction file->modifyTransaction(t); } } ft.commit(); } catch (const MyMoneyException &) { qDebug() << "transactionCopySplits() failed"; } } } } void KGlobalLedgerView::slotGoToPayee() { Q_D(KGlobalLedgerView); if (!d->m_payeeGoto.isEmpty()) { try { QString transactionId; if (d->m_selectedTransactions.count() == 1) { transactionId = d->m_selectedTransactions[0].transaction().id(); } // make sure to pass copies, as d->myMoneyView->slotPayeeSelected() overrides // d->m_payeeGoto and d->m_currentAccount while calling slotUpdateActions() QString payeeId = d->m_payeeGoto; QString accountId = d->m_currentAccount.id(); emit selectByVariant(QVariantList {QVariant(payeeId), QVariant(accountId), QVariant(transactionId)}, eView::Intent::ShowPayee); // emit openPayeeRequested(payeeId, accountId, transactionId); } catch (const MyMoneyException &) { } } } void KGlobalLedgerView::slotGoToAccount() { Q_D(KGlobalLedgerView); if (!d->m_accountGoto.isEmpty()) { try { QString transactionId; if (d->m_selectedTransactions.count() == 1) { transactionId = d->m_selectedTransactions[0].transaction().id(); } // make sure to pass a copy, as d->myMoneyView->slotLedgerSelected() overrides // d->m_accountGoto while calling slotUpdateActions() slotLedgerSelected(d->m_accountGoto, transactionId); } catch (const MyMoneyException &) { } } } void KGlobalLedgerView::slotMatchTransactions() { Q_D(KGlobalLedgerView); // if the menu action is retrieved it can contain an '&' character for the accelerator causing the comparison to fail if not removed QString transactionActionText = pActions[Action::MatchTransaction]->text(); transactionActionText.remove('&'); if (transactionActionText == i18nc("Button text for match transaction", "Match")) d->transactionMatch(); else d->transactionUnmatch(); } void KGlobalLedgerView::slotCombineTransactions() { qDebug("slotTransactionCombine() not implemented yet"); } void KGlobalLedgerView::slotToggleReconciliationFlag() { Q_D(KGlobalLedgerView); d->markTransaction(eMyMoney::Split::State::Unknown); } void KGlobalLedgerView::slotMarkCleared() { Q_D(KGlobalLedgerView); d->markTransaction(eMyMoney::Split::State::Cleared); } void KGlobalLedgerView::slotMarkReconciled() { Q_D(KGlobalLedgerView); d->markTransaction(eMyMoney::Split::State::Reconciled); } void KGlobalLedgerView::slotMarkNotReconciled() { Q_D(KGlobalLedgerView); d->markTransaction(eMyMoney::Split::State::NotReconciled); } void KGlobalLedgerView::slotSelectAllTransactions() { Q_D(KGlobalLedgerView); if(d->m_needLoad) d->init(); d->m_register->clearSelection(); KMyMoneyRegister::RegisterItem* p = d->m_register->firstItem(); while (p) { KMyMoneyRegister::Transaction* t = dynamic_cast(p); if (t) { if (t->isVisible() && t->isSelectable() && !t->isScheduled() && !t->id().isEmpty()) { t->setSelected(true); } } p = p->nextItem(); } // this is here only to re-paint the items without selecting anything because the data (including the selection) is not really held in the model right now d->m_register->selectAll(); // inform everyone else about the selected items KMyMoneyRegister::SelectedTransactions list(d->m_register); updateLedgerActions(list); emit selectByVariant(QVariantList {QVariant::fromValue(list)}, eView::Intent::SelectRegisterTransactions); } void KGlobalLedgerView::slotCreateScheduledTransaction() { Q_D(KGlobalLedgerView); if (d->m_selectedTransactions.count() == 1) { // make sure to have the current selected split as first split in the schedule MyMoneyTransaction t = d->m_selectedTransactions[0].transaction(); MyMoneySplit s = d->m_selectedTransactions[0].split(); QString splitId = s.id(); s.clearId(); s.setReconcileFlag(eMyMoney::Split::State::NotReconciled); s.setReconcileDate(QDate()); t.removeSplits(); t.addSplit(s); foreach (const auto split, d->m_selectedTransactions[0].transaction().splits()) { if (split.id() != splitId) { MyMoneySplit s0 = split; s0.clearId(); s0.setReconcileFlag(eMyMoney::Split::State::NotReconciled); s0.setReconcileDate(QDate()); t.addSplit(s0); } } KEditScheduleDlg::newSchedule(t, eMyMoney::Schedule::Occurrence::Monthly); } } void KGlobalLedgerView::slotAssignNumber() { Q_D(KGlobalLedgerView); if (d->m_transactionEditor) d->m_transactionEditor->assignNextNumber(); } void KGlobalLedgerView::slotStartReconciliation() { Q_D(KGlobalLedgerView); // we cannot reconcile standard accounts if (!MyMoneyFile::instance()->isStandardAccount(d->m_currentAccount.id())) emit selectByObject(d->m_currentAccount, eView::Intent::StartEnteringOverdueScheduledTransactions); // asynchronous call to KScheduledView::slotEnterOverdueSchedules is made here // after that all activity should be continued in KGlobalLedgerView::slotContinueReconciliation() } void KGlobalLedgerView::slotFinishReconciliation() { Q_D(KGlobalLedgerView); const auto file = MyMoneyFile::instance(); if (!d->m_reconciliationAccount.id().isEmpty()) { // retrieve list of all transactions that are not reconciled or cleared QList > transactionList; MyMoneyTransactionFilter filter(d->m_reconciliationAccount.id()); filter.addState((int)eMyMoney::TransactionFilter::State::Cleared); filter.addState((int)eMyMoney::TransactionFilter::State::NotReconciled); filter.setDateFilter(QDate(), d->m_endingBalanceDlg->statementDate()); filter.setConsiderCategory(false); filter.setReportAllSplits(true); file->transactionList(transactionList, filter); auto balance = MyMoneyFile::instance()->balance(d->m_reconciliationAccount.id(), d->m_endingBalanceDlg->statementDate()); MyMoneyMoney actBalance, clearedBalance; actBalance = clearedBalance = balance; // walk the list of transactions to figure out the balance(s) for (auto it = transactionList.constBegin(); it != transactionList.constEnd(); ++it) { if ((*it).second.reconcileFlag() == eMyMoney::Split::State::NotReconciled) { clearedBalance -= (*it).second.shares(); } } if (d->m_endingBalanceDlg->endingBalance() != clearedBalance) { auto message = i18n("You are about to finish the reconciliation of this account with a difference between your bank statement and the transactions marked as cleared.\n" "Are you sure you want to finish the reconciliation?"); if (KMessageBox::questionYesNo(this, message, i18n("Confirm end of reconciliation"), KStandardGuiItem::yes(), KStandardGuiItem::no()) == KMessageBox::No) return; } MyMoneyFileTransaction ft; // refresh object d->m_reconciliationAccount = file->account(d->m_reconciliationAccount.id()); // Turn off reconciliation mode // Models::instance()->accountsModel()->slotReconcileAccount(MyMoneyAccount(), QDate(), MyMoneyMoney()); // slotSetReconcileAccount(MyMoneyAccount(), QDate(), MyMoneyMoney()); // d->m_myMoneyView->finishReconciliation(d->m_reconciliationAccount); // only update the last statement balance here, if we haven't a newer one due // to download of online statements. if (d->m_reconciliationAccount.value("lastImportedTransactionDate").isEmpty() || QDate::fromString(d->m_reconciliationAccount.value("lastImportedTransactionDate"), Qt::ISODate) < d->m_endingBalanceDlg->statementDate()) { d->m_reconciliationAccount.setValue("lastStatementBalance", d->m_endingBalanceDlg->endingBalance().toString()); // in case we override the last statement balance here, we have to make sure // that we don't show the online balance anymore, as it might be different d->m_reconciliationAccount.deletePair("lastImportedTransactionDate"); } d->m_reconciliationAccount.setLastReconciliationDate(d->m_endingBalanceDlg->statementDate()); // keep a record of this reconciliation d->m_reconciliationAccount.addReconciliation(d->m_endingBalanceDlg->statementDate(), d->m_endingBalanceDlg->endingBalance()); d->m_reconciliationAccount.deletePair("lastReconciledBalance"); d->m_reconciliationAccount.deletePair("statementBalance"); d->m_reconciliationAccount.deletePair("statementDate"); try { // update the account data file->modifyAccount(d->m_reconciliationAccount); /* // collect the list of cleared splits for this account filter.clear(); filter.addAccount(d->m_reconciliationAccount.id()); filter.addState(eMyMoney::TransactionFilter::Cleared); filter.setConsiderCategory(false); filter.setReportAllSplits(true); file->transactionList(transactionList, filter); */ // walk the list of transactions/splits and mark the cleared ones as reconciled for (auto it = transactionList.begin(); it != transactionList.end(); ++it) { MyMoneySplit sp = (*it).second; // skip the ones that are not marked cleared if (sp.reconcileFlag() != eMyMoney::Split::State::Cleared) continue; // always retrieve a fresh copy of the transaction because we // might have changed it already with another split MyMoneyTransaction t = file->transaction((*it).first.id()); sp.setReconcileFlag(eMyMoney::Split::State::Reconciled); sp.setReconcileDate(d->m_endingBalanceDlg->statementDate()); t.modifySplit(sp); // update the engine ... file->modifyTransaction(t); // ... and the list (*it) = qMakePair(t, sp); } ft.commit(); // reload account data from engine as the data might have changed in the meantime d->m_reconciliationAccount = file->account(d->m_reconciliationAccount.id()); /** * This signal is emitted when an account has been successfully reconciled * and all transactions are updated in the engine. It can be used by plugins * to create reconciliation reports. * * @param account the account data * @param date the reconciliation date as provided through the dialog * @param startingBalance the starting balance as provided through the dialog * @param endingBalance the ending balance as provided through the dialog * @param transactionList reference to QList of QPair containing all * transaction/split pairs processed by the reconciliation. */ emit selectByVariant(QVariantList { QVariant::fromValue(d->m_reconciliationAccount), QVariant::fromValue(d->m_endingBalanceDlg->statementDate()), QVariant::fromValue(d->m_endingBalanceDlg->previousBalance()), QVariant::fromValue(d->m_endingBalanceDlg->endingBalance()), QVariant::fromValue(transactionList) }, eView::Intent::AccountReconciled); } catch (const MyMoneyException &) { qDebug("Unexpected exception when setting cleared to reconcile"); } // Turn off reconciliation mode Models::instance()->accountsModel()->slotReconcileAccount(MyMoneyAccount(), QDate(), MyMoneyMoney()); slotSetReconcileAccount(MyMoneyAccount(), QDate(), MyMoneyMoney()); } // Turn off reconciliation mode d->m_reconciliationAccount = MyMoneyAccount(); updateActions(d->m_currentAccount); updateLedgerActionsInternal(); d->loadView(); // slotUpdateActions(); } void KGlobalLedgerView::slotPostponeReconciliation() { Q_D(KGlobalLedgerView); MyMoneyFileTransaction ft; const auto file = MyMoneyFile::instance(); if (!d->m_reconciliationAccount.id().isEmpty()) { // refresh object d->m_reconciliationAccount = file->account(d->m_reconciliationAccount.id()); // Turn off reconciliation mode // Models::instance()->accountsModel()->slotReconcileAccount(MyMoneyAccount(), QDate(), MyMoneyMoney()); // slotSetReconcileAccount(MyMoneyAccount(), QDate(), MyMoneyMoney()); // d->m_myMoneyView->finishReconciliation(d->m_reconciliationAccount); d->m_reconciliationAccount.setValue("lastReconciledBalance", d->m_endingBalanceDlg->previousBalance().toString()); d->m_reconciliationAccount.setValue("statementBalance", d->m_endingBalanceDlg->endingBalance().toString()); d->m_reconciliationAccount.setValue("statementDate", d->m_endingBalanceDlg->statementDate().toString(Qt::ISODate)); try { file->modifyAccount(d->m_reconciliationAccount); ft.commit(); d->m_reconciliationAccount = MyMoneyAccount(); updateActions(d->m_currentAccount); updateLedgerActionsInternal(); // slotUpdateActions(); } catch (const MyMoneyException &) { qDebug("Unexpected exception when setting last reconcile info into account"); ft.rollback(); d->m_reconciliationAccount = file->account(d->m_reconciliationAccount.id()); } // Turn off reconciliation mode Models::instance()->accountsModel()->slotReconcileAccount(MyMoneyAccount(), QDate(), MyMoneyMoney()); slotSetReconcileAccount(MyMoneyAccount(), QDate(), MyMoneyMoney()); d->loadView(); } } void KGlobalLedgerView::slotOpenAccount() { Q_D(KGlobalLedgerView); if (!MyMoneyFile::instance()->isStandardAccount(d->m_currentAccount.id())) slotLedgerSelected(d->m_currentAccount.id(), QString()); } void KGlobalLedgerView::slotFindTransaction() { Q_D(KGlobalLedgerView); if (!d->m_searchDlg) { d->m_searchDlg = new KFindTransactionDlg(this); connect(d->m_searchDlg, &QObject::destroyed, this, &KGlobalLedgerView::slotCloseSearchDialog); connect(d->m_searchDlg, &KFindTransactionDlg::transactionSelected, this, &KGlobalLedgerView::slotLedgerSelected); } d->m_searchDlg->show(); d->m_searchDlg->raise(); d->m_searchDlg->activateWindow(); } void KGlobalLedgerView::slotCloseSearchDialog() { Q_D(KGlobalLedgerView); if (d->m_searchDlg) d->m_searchDlg->deleteLater(); d->m_searchDlg = nullptr; } void KGlobalLedgerView::slotStatusMsg(const QString& txt) { emit selectByVariant(QVariantList {QVariant(txt)}, eView::Intent::ReportProgressMessage); } void KGlobalLedgerView::slotStatusProgress(int cnt, int base) { emit selectByVariant(QVariantList {QVariant(cnt), QVariant(base)}, eView::Intent::ReportProgress); } void KGlobalLedgerView::slotTransactionsSelected(const KMyMoneyRegister::SelectedTransactions& list) { updateLedgerActions(list); emit selectByVariant(QVariantList {QVariant::fromValue(list)}, eView::Intent::SelectRegisterTransactions); }