diff --git a/kmymoney/converter/mymoneystatementreader.cpp b/kmymoney/converter/mymoneystatementreader.cpp index 89b38482c..eef7ee51b 100644 --- a/kmymoney/converter/mymoneystatementreader.cpp +++ b/kmymoney/converter/mymoneystatementreader.cpp @@ -1,1578 +1,1578 @@ /*************************************************************************** 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; } 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.value() == sNew.shares() && 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); } // we use an object on the heap here, so that we can check the presence // of it during slotUpdateActions() by looking at the pointer. auto reader = new MyMoneyStatementReader; reader->setAutoCreatePayee(true); if (callback) reader->setProgressCallback(callback); // disable all standard widgets during the import // setEnabled(false); QStringList messages; result = reader->import(s, messages); auto transactionAdded = reader->anyTransactionAdded(); // get rid of the statement reader and tell everyone else // about the destruction by setting the pointer to zero delete reader; if (callback) callback(-1, -1, QString()); // re-enable all standard widgets // setEnabled(true); if (!silent && transactionAdded) KMessageBox::informationList(nullptr, i18n("The statement has been processed with the following results:"), messages, i18n("Statement stats")); if (!result) messages.clear(); return messages; } bool MyMoneyStatementReader::import(const MyMoneyStatement& s, QStringList& messages) { // // For testing, save the statement to an XML file // (uncomment this line) // //MyMoneyStatement::writeXMLFile(s, "Imported.Xml"); // // 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 if (d->m_account.value("lastStatementBalance") != s.m_closingBalance.toString() || d->m_account.value("lastImportedTransactionDate") != s.m_dateEnd.toString(Qt::ISODate)) { if (s.m_closingBalance != MyMoneyMoney::autoCalc) { d->m_account.setValue("lastStatementBalance", s.m_closingBalance.toString()); if (s.m_dateEnd.isValid()) { d->m_account.setValue("lastImportedTransactionDate", s.m_dateEnd.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 MyMoneyPayee::payeeMatchType matchType = (*it_p).matchData(ignoreCase, keys); + const auto matchType = (*it_p).matchData(ignoreCase, keys); switch (matchType) { - case MyMoneyPayee::matchDisabled: + case eMyMoney::Payee::MatchType::Disabled: break; - case MyMoneyPayee::matchName: - case MyMoneyPayee::matchNameExact: + case eMyMoney::Payee::MatchType::Name: + case eMyMoney::Payee::MatchType::NameExact: keys << QString("%1").arg(QRegExp::escape((*it_p).name())); - if(matchType == MyMoneyPayee::matchNameExact) { + if(matchType == eMyMoney::Payee::MatchType::NameExact) { keys.clear(); keys << QString("^%1$").arg(QRegExp::escape((*it_p).name())); } // intentional fall through - case MyMoneyPayee::matchKey: + 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(MyMoneyPayee::matchKey, true, QStringList() << QString("^%1$").arg(QRegExp::escape(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(); 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); 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); } diff --git a/kmymoney/converter/tests/converter-test.cpp b/kmymoney/converter/tests/converter-test.cpp index acd2bd188..9fae5381e 100644 --- a/kmymoney/converter/tests/converter-test.cpp +++ b/kmymoney/converter/tests/converter-test.cpp @@ -1,200 +1,202 @@ /*************************************************************************** convertertest.cpp ------------------- copyright : (C) 2002 by Thomas Baumgart email : ipwizard@users.sourceforge.net 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 "converter-test.h" #include #include // uses helper functions from reports tests #include "reportstestcommon.h" using namespace test; #include "mymoneyinstitution.h" #include "mymoneysecurity.h" #include "mymoneyprice.h" #include "mymoneyreport.h" #include "mymoneypayee.h" #include "mymoneystatement.h" #include "mymoneyexception.h" #include "storage/mymoneystoragedump.h" #include "webpricequote.h" QTEST_GUILESS_MAIN(ConverterTest) using namespace convertertest; void ConverterTest::init() { storage = new MyMoneyStorageMgr; file = MyMoneyFile::instance(); file->attachStorage(storage); MyMoneyFileTransaction ft; file->addCurrency(MyMoneySecurity("CAD", "Canadian Dollar", "C$")); file->addCurrency(MyMoneySecurity("USD", "US Dollar", "$")); file->addCurrency(MyMoneySecurity("JPY", "Japanese Yen", QChar(0x00A5), 1)); file->addCurrency(MyMoneySecurity("GBP", "British Pound", "#")); file->setBaseCurrency(file->currency("USD")); - MyMoneyPayee payeeTest("Test Payee"); + MyMoneyPayee payeeTest; + payeeTest.setName("Test Payee"); file->addPayee(payeeTest); - MyMoneyPayee payeeTest2("Thomas Baumgart"); + MyMoneyPayee payeeTest2; + payeeTest2.setName("Thomas Baumgart"); file->addPayee(payeeTest2); acAsset = (MyMoneyFile::instance()->asset().id()); acLiability = (MyMoneyFile::instance()->liability().id()); acExpense = (MyMoneyFile::instance()->expense().id()); acIncome = (MyMoneyFile::instance()->income().id()); acChecking = makeAccount("Checking Account", eMyMoney::Account::Type::Checkings, moConverterCheckingOpen, QDate(2004, 5, 15), acAsset); acCredit = makeAccount("Credit Card", eMyMoney::Account::Type::CreditCard, moConverterCreditOpen, QDate(2004, 7, 15), acLiability); acSolo = makeAccount("Solo", eMyMoney::Account::Type::Expense, MyMoneyMoney(), QDate(2004, 1, 11), acExpense); acParent = makeAccount("Parent", eMyMoney::Account::Type::Expense, MyMoneyMoney(), QDate(2004, 1, 11), acExpense); acChild = makeAccount("Child", eMyMoney::Account::Type::Expense, MyMoneyMoney(), QDate(2004, 2, 11), acParent); acForeign = makeAccount("Foreign", eMyMoney::Account::Type::Expense, MyMoneyMoney(), QDate(2004, 1, 11), acExpense); MyMoneyInstitution i("Bank of the World", "", "", "", "", "", ""); file->addInstitution(i); inBank = i.id(); ft.commit(); } void ConverterTest::cleanup() { file->detachStorage(storage); delete storage; } void ConverterTest::testWebQuotes_data() { QTest::addColumn("symbol"); QTest::addColumn("testname"); QTest::addColumn("source"); QTest::newRow("Yahoo UK") << "VOD.L" << "test Yahoo UK" << "Yahoo UK"; QTest::newRow("Yahoo Currency") << "EUR > USD" << "test Yahoo Currency" << "Yahoo Currency"; QTest::newRow("Financial Express") << "0585239" << "test Financial Express" << "Financial Express"; QTest::newRow("Yahoo France") << "EAD.PA" << "test Yahoo France" << "Yahoo France"; QTest::newRow("Globe & Mail") << "50492" << "test Globe-Mail" << "Globe & Mail"; QTest::newRow("MSN Canada") << "TDB647" << "test MSN.CA" << "MSN.CA"; // QTest::newRow("Finanztreff") << "BASF.SE" << "test Finanztreff" << "Finanztreff"; // QTest::newRow("boerseonline") << "symbol" << "test boerseonline" << "boerseonline"; // QTest::newRow("Wallstreet-Online.DE (Default)") << "symbol" << "test Wallstreet-Online.DE (Default)" << "Wallstreet-Online.DE (Default)"; // QTest::newRow("Financial Times UK") << "DZGEAE" << "test Financial Times UK Funds" << "Financial Times UK Funds"); QTest::newRow("Yahoo Canada") << "UTS.TO" << "test Yahoo Canada" << "Yahoo Canada"; // QTest::newRow("Wallstreed-Online.DE (Hamburg)") << "TDB647" << "test Wallstreet-Online.DE (Hamburg)" << "Wallstreet-Online.DE (Hamburg)"; // QTest::newRow("Gielda Papierow Wartosciowych (GPW)") << "TDB647" << "test Gielda Papierow Wartosciowych (GPW)" << "Gielda Papierow Wartosciowych (GPW)"; // QTest::newRow("OMX Baltic") << "TDB647" << "test OMX Baltic funds" << "OMX Baltic funds"; QTest::newRow("Finance::Quote usa") << "DIS" << "test F::Q usa" << "Finance::Quote usa"; //UNTESTED: Other F::Q sources, local files, user custom sources } void ConverterTest::testWebQuotesDefault() { #ifdef PERFORM_ONLINE_TESTS try { WebPriceQuote q; QuoteReceiver qr(&q); q.launch("DIS", "test default"); // qDebug() << "ConverterTest::testWebQuotes(): quote for " << q.m_symbol << " on " << qr.m_date.toString() << " is " << qr.m_price.toString() << " errors(" << qr.m_errors.count() << "): " << qr.m_errors.join(" /// "); // No errors allowed QVERIFY(qr.m_errors.count() == 0); // Quote date should be within the last week, or something bad is going on. QVERIFY(qr.m_date <= QDate::currentDate()); QVERIFY(qr.m_date >= QDate::currentDate().addDays(-7)); // Quote value should at least be positive QVERIFY(qr.m_price.isPositive()); } catch (const MyMoneyException &e) { QFAIL(e.what()); } #endif } void ConverterTest::testWebQuotes() { #ifdef PERFORM_ONLINE_TESTS try { WebPriceQuote q; QuoteReceiver qr(&q); QFETCH(QString, symbol); QFETCH(QString, testname); QFETCH(QString, source); q.launch(symbol, testname, source); QVERIFY(qr.m_errors.count() == 0); QVERIFY(qr.m_date <= QDate::currentDate().addDays(1)); QVERIFY(qr.m_date >= QDate::currentDate().addDays(-7)); QVERIFY(qr.m_price.isPositive()); } catch (const MyMoneyException &e) { QFAIL(e.what()); } #endif } void ConverterTest::testDateFormat() { try { MyMoneyDateFormat format("%mm-%dd-%yyyy"); QVERIFY(format.convertString("1-5-2005") == QDate(2005, 1, 5)); QVERIFY(format.convertString("jan-15-2005") == QDate(2005, 1, 15)); QVERIFY(format.convertString("august-25-2005") == QDate(2005, 8, 25)); format = MyMoneyDateFormat("%mm/%dd/%yy"); QVERIFY(format.convertString("1/5/05") == QDate(2005, 1, 5)); QVERIFY(format.convertString("jan/15/05") == QDate(2005, 1, 15)); QVERIFY(format.convertString("august/25/05") == QDate(2005, 8, 25)); format = MyMoneyDateFormat("%d\\.%m\\.%yy"); QVERIFY(format.convertString("1.5.05") == QDate(2005, 5, 1)); QVERIFY(format.convertString("15.jan.05") == QDate(2005, 1, 15)); QVERIFY(format.convertString("25.august.05") == QDate(2005, 8, 25)); format = MyMoneyDateFormat("%yyyy\\\\%dddd\\\\%mmmmmmmmmmm"); QVERIFY(format.convertString("2005\\31\\12") == QDate(2005, 12, 31)); QVERIFY(format.convertString("2005\\15\\jan") == QDate(2005, 1, 15)); QVERIFY(format.convertString("2005\\25\\august") == QDate(2005, 8, 25)); format = MyMoneyDateFormat("%m %dd, %yyyy"); QVERIFY(format.convertString("jan 15, 2005") == QDate(2005, 1, 15)); QVERIFY(format.convertString("august 25, 2005") == QDate(2005, 8, 25)); QVERIFY(format.convertString("january 1st, 2005") == QDate(2005, 1, 1)); format = MyMoneyDateFormat("%m %d %y"); QVERIFY(format.convertString("12/31/50", false, 2000) == QDate(1950, 12, 31)); QVERIFY(format.convertString("1/1/90", false, 2000) == QDate(1990, 1, 1)); QVERIFY(format.convertString("december 31st, 5", false) == QDate(2005, 12, 31)); } catch (const MyMoneyException &e) { QFAIL(e.what()); } } diff --git a/kmymoney/dialogs/kbalancechartdlg.cpp b/kmymoney/dialogs/kbalancechartdlg.cpp index 1fe2312d3..9bd1346b4 100644 --- a/kmymoney/dialogs/kbalancechartdlg.cpp +++ b/kmymoney/dialogs/kbalancechartdlg.cpp @@ -1,164 +1,164 @@ /* * Copyright 2007-2011 Thomas Baumgart * Copyright 2017 Ł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 "kbalancechartdlg.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "mymoneyreport.h" #include "pivottable.h" #include "kreportchartview.h" #include "mymoneyenums.h" using namespace reports; KBalanceChartDlg::KBalanceChartDlg(const MyMoneyAccount& account, QWidget* parent) : QDialog(parent) { setWindowTitle(i18n("Balance of %1", account.name())); setSizeGripEnabled(true); setModal(true); // restore the last used dialog size winId(); // needs to be called to create the QWindow KConfigGroup grp = KSharedConfig::openConfig()->group("KBalanceChartDlg"); if (grp.isValid()) { KWindowConfig::restoreWindowSize(windowHandle(), grp); } // let the minimum size be 700x500 resize(QSize(700, 500).expandedTo(windowHandle() ? windowHandle()->size() : QSize())); QVBoxLayout *mainLayout = new QVBoxLayout; setLayout(mainLayout); //draw the chart and add it to the main layout KReportChartView* chartWidget = drawChart(account); mainLayout->addWidget(chartWidget); // add the buttons QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Close); connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); mainLayout->addWidget(buttonBox); } KBalanceChartDlg::~KBalanceChartDlg() { // store the last used dialog size KConfigGroup grp = KSharedConfig::openConfig()->group("KBalanceChartDlg"); if (grp.isValid()) { KWindowConfig::saveWindowSize(windowHandle(), grp); } } KReportChartView* KBalanceChartDlg::drawChart(const MyMoneyAccount& account) { MyMoneyReport reportCfg = MyMoneyReport( - MyMoneyReport::eAssetLiability, - MyMoneyReport::eMonths, + eMyMoney::Report::RowType::AssetLiability, + static_cast(eMyMoney::Report::ColumnType::Months), eMyMoney::TransactionFilter::Date::Last3ToNext3Months, - MyMoneyReport::eDetailTotal, + eMyMoney::Report::DetailLevel::Total, i18n("%1 Balance History", account.name()), i18n("Generated Report") ); reportCfg.setChartByDefault(true); reportCfg.setChartCHGridLines(false); reportCfg.setChartSVGridLines(false); reportCfg.setChartDataLabels(false); - reportCfg.setChartType(MyMoneyReport::eChartLine); + reportCfg.setChartType(eMyMoney::Report::ChartType::Line); reportCfg.setIncludingForecast(true); reportCfg.setIncludingBudgetActuals(true); if (account.accountType() == eMyMoney::Account::Type::Investment) { foreach (const auto accountID, account.accountList()) reportCfg.addAccount(accountID); } else reportCfg.addAccount(account.id()); reportCfg.setColumnsAreDays(true); reportCfg.setConvertCurrency(false); reportCfg.setMixedTime(true); reports::PivotTable table(reportCfg); reports::KReportChartView* chartWidget = new reports::KReportChartView(this); table.drawChart(*chartWidget); // add another row for limit bool needRow = false; bool haveMinBalance = false; bool haveMaxCredit = false; MyMoneyMoney minBalance, maxCredit; MyMoneyMoney factor(1, 1); if (account.accountGroup() == eMyMoney::Account::Type::Asset) factor = -factor; if (!account.value("maxCreditEarly").isEmpty()) { needRow = true; haveMaxCredit = true; maxCredit = MyMoneyMoney(account.value("maxCreditEarly")) * factor; } if (!account.value("maxCreditAbsolute").isEmpty()) { needRow = true; haveMaxCredit = true; maxCredit = MyMoneyMoney(account.value("maxCreditAbsolute")) * factor; } if (!account.value("minBalanceEarly").isEmpty()) { needRow = true; haveMinBalance = true; minBalance = MyMoneyMoney(account.value("minBalanceEarly")); } if (!account.value("minBalanceAbsolute").isEmpty()) { needRow = true; haveMinBalance = true; minBalance = MyMoneyMoney(account.value("minBalanceAbsolute")); } if (needRow) { if (haveMinBalance) { chartWidget->drawLimitLine(minBalance.toDouble()); } if (haveMaxCredit) { chartWidget->drawLimitLine(maxCredit.toDouble()); } } // always draw the y axis zero value line // TODO: port to KF5 - this crashes KChart //chartWidget->drawLimitLine(0); //remove the legend chartWidget->removeLegend(); return chartWidget; } diff --git a/kmymoney/mymoney/mymoneyaccount.cpp b/kmymoney/mymoney/mymoneyaccount.cpp index be2d35b1c..d515b28e4 100644 --- a/kmymoney/mymoney/mymoneyaccount.cpp +++ b/kmymoney/mymoney/mymoneyaccount.cpp @@ -1,730 +1,736 @@ /* * Copyright 2000-2002 Michael Edwardes * Copyright 2001 Felix Rodriguez * Copyright 2002-2003 Kevin Tambascio * Copyright 2006-2017 Thomas Baumgart * Copyright 2006 Ace Jones * 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 "mymoneyaccount.h" #include "mymoneyaccount_p.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include // ---------------------------------------------------------------------------- // Project Includes #include "mymoneyutils.h" #include "mymoneyexception.h" #include "mymoneysplit.h" #include "mymoneyfile.h" #include "mymoneysecurity.h" #include "mymoneyinstitution.h" #include "mymoneypayee.h" #include "payeeidentifier/payeeidentifiertyped.h" #include "payeeidentifier/ibanbic/ibanbic.h" #include "payeeidentifier/nationalaccount/nationalaccount.h" #include "mymoneystoragenames.h" #include "icons/icons.h" using namespace MyMoneyStorageNodes; using namespace Icons; MyMoneyAccount::MyMoneyAccount() : MyMoneyObject(*new MyMoneyAccountPrivate), MyMoneyKeyValueContainer() { } +MyMoneyAccount::MyMoneyAccount(const QString &id): + MyMoneyObject(*new MyMoneyAccountPrivate, id), + MyMoneyKeyValueContainer() +{ +} + MyMoneyAccount::MyMoneyAccount(const QDomElement& node) : MyMoneyObject(*new MyMoneyAccountPrivate, node), MyMoneyKeyValueContainer(node.elementsByTagName(nodeNames[nnKeyValuePairs]).item(0).toElement()) { if (nodeNames[nnAccount] != node.tagName()) throw MYMONEYEXCEPTION_CSTRING("Node was not ACCOUNT"); Q_D(MyMoneyAccount); setName(node.attribute(d->getAttrName(Account::Attribute::Name))); // qDebug("Reading information for account %s", acc.name().data()); setParentAccountId(MyMoneyUtils::QStringEmpty(node.attribute(d->getAttrName(Account::Attribute::ParentAccount)))); setLastModified(MyMoneyUtils::stringToDate(MyMoneyUtils::QStringEmpty(node.attribute(d->getAttrName(Account::Attribute::LastModified))))); setLastReconciliationDate(MyMoneyUtils::stringToDate(MyMoneyUtils::QStringEmpty(node.attribute(d->getAttrName(Account::Attribute::LastReconciled))))); if (!d->m_lastReconciliationDate.isValid()) { // for some reason, I was unable to access our own kvp at this point through // the value() method. It always returned empty strings. The workaround for // this is to construct a local kvp the same way as we have done before and // extract the value from it. // // Since we want to get rid of the lastStatementDate record anyway, this seems // to be ok for now. (ipwizard - 2008-08-14) QString txt = MyMoneyKeyValueContainer(node.elementsByTagName(nodeNames[nnKeyValuePairs]).item(0).toElement()).value("lastStatementDate"); if (!txt.isEmpty()) { setLastReconciliationDate(QDate::fromString(txt, Qt::ISODate)); } } setInstitutionId(MyMoneyUtils::QStringEmpty(node.attribute(d->getAttrName(Account::Attribute::Institution)))); setNumber(MyMoneyUtils::QStringEmpty(node.attribute(d->getAttrName(Account::Attribute::Number)))); setOpeningDate(MyMoneyUtils::stringToDate(MyMoneyUtils::QStringEmpty(node.attribute(d->getAttrName(Account::Attribute::Opened))))); setCurrencyId(MyMoneyUtils::QStringEmpty(node.attribute(d->getAttrName(Account::Attribute::Currency)))); QString tmp = MyMoneyUtils::QStringEmpty(node.attribute(d->getAttrName(Account::Attribute::Type))); bool bOK = false; int type = tmp.toInt(&bOK); if (bOK) { setAccountType(static_cast(type)); } else { qWarning("XMLREADER: Account %s had invalid or no account type information.", qPrintable(name())); } if (node.hasAttribute(d->getAttrName(Account::Attribute::OpeningBalance))) if (!MyMoneyMoney(node.attribute(d->getAttrName(Account::Attribute::OpeningBalance))).isZero()) throw MYMONEYEXCEPTION(QString::fromLatin1("Account %1 contains an opening balance. Please use KMyMoney version 0.8 or later and earlier than version 0.9 to correct the problem.").arg(d->m_name)); setDescription(node.attribute(d->getAttrName(Account::Attribute::Description))); d->m_id = MyMoneyUtils::QStringEmpty(node.attribute(d->getAttrName(Account::Attribute::ID))); // qDebug("Account %s has id of %s, type of %d, parent is %s.", acc.name().data(), id.data(), type, acc.parentAccountId().data()); // Process any Sub-Account information found inside the account entry. d->m_accountList.clear(); QDomNodeList nodeList = node.elementsByTagName(d->getElName(Account::Element::SubAccounts)); if (nodeList.count() > 0) { nodeList = nodeList.item(0).toElement().elementsByTagName(d->getElName(Account::Element::SubAccount)); for (int i = 0; i < nodeList.count(); ++i) { addAccountId(QString(nodeList.item(i).toElement().attribute(d->getAttrName(Account::Attribute::ID)))); } } nodeList = node.elementsByTagName(d->getElName(Account::Element::OnlineBanking)); if (nodeList.count() > 0) { QDomNamedNodeMap attributes = nodeList.item(0).toElement().attributes(); for (int i = 0; i < attributes.count(); ++i) { const QDomAttr& it_attr = attributes.item(i).toAttr(); d->m_onlineBankingSettings.setValue(it_attr.name(), it_attr.value()); } } // Up to and including version 4.6.6 the new account dialog stored the iban in the kvp-key "IBAN". // But the rest of the software uses "iban". So correct this: if (!value("IBAN").isEmpty()) { // If "iban" was not set, set it now. If it is set, the user reseted it already, so remove // the garbage. if (value(d->getAttrName(Account::Attribute::IBAN)).isEmpty()) setValue(d->getAttrName(Account::Attribute::IBAN), value("IBAN")); deletePair("IBAN"); } } MyMoneyAccount::MyMoneyAccount(const MyMoneyAccount& other) : MyMoneyObject(*new MyMoneyAccountPrivate(*other.d_func()), other.id()), MyMoneyKeyValueContainer(other) { } MyMoneyAccount::MyMoneyAccount(const QString& id, const MyMoneyAccount& other) : MyMoneyObject(*new MyMoneyAccountPrivate(*other.d_func()), id), MyMoneyKeyValueContainer(other) { } MyMoneyAccount::~MyMoneyAccount() { } void MyMoneyAccount::touch() { setLastModified(QDate::currentDate()); } eMyMoney::Account::Type MyMoneyAccount::accountType() const { Q_D(const MyMoneyAccount); return d->m_accountType; } void MyMoneyAccount::setAccountType(const Account::Type type) { Q_D(MyMoneyAccount); d->m_accountType = type; } QString MyMoneyAccount::institutionId() const { Q_D(const MyMoneyAccount); return d->m_institution; } void MyMoneyAccount::setInstitutionId(const QString& id) { Q_D(MyMoneyAccount); d->m_institution = id; } QString MyMoneyAccount::name() const { Q_D(const MyMoneyAccount); return d->m_name; } void MyMoneyAccount::setName(const QString& name) { Q_D(MyMoneyAccount); d->m_name = name; } QString MyMoneyAccount::number() const { Q_D(const MyMoneyAccount); return d->m_number; } void MyMoneyAccount::setNumber(const QString& number) { Q_D(MyMoneyAccount); d->m_number = number; } QString MyMoneyAccount::description() const { Q_D(const MyMoneyAccount); return d->m_description; } void MyMoneyAccount::setDescription(const QString& desc) { Q_D(MyMoneyAccount); d->m_description = desc; } QDate MyMoneyAccount::openingDate() const { Q_D(const MyMoneyAccount); return d->m_openingDate; } void MyMoneyAccount::setOpeningDate(const QDate& date) { Q_D(MyMoneyAccount); d->m_openingDate = date; } QDate MyMoneyAccount::lastReconciliationDate() const { Q_D(const MyMoneyAccount); return d->m_lastReconciliationDate; } void MyMoneyAccount::setLastReconciliationDate(const QDate& date) { Q_D(MyMoneyAccount); // FIXME: for a limited time (maybe until we delivered 1.0) we // keep the last reconciliation date also in the KVP for backward // compatibility. After that, the setValue() statemetn should be removed // and the XML ctor should remove the value completely from the KVP setValue("lastStatementDate", date.toString(Qt::ISODate)); d->m_lastReconciliationDate = date; } QDate MyMoneyAccount::lastModified() const { Q_D(const MyMoneyAccount); return d->m_lastModified; } void MyMoneyAccount::setLastModified(const QDate& date) { Q_D(MyMoneyAccount); d->m_lastModified = date; } QString MyMoneyAccount::parentAccountId() const { Q_D(const MyMoneyAccount); return d->m_parentAccount; } void MyMoneyAccount::setParentAccountId(const QString& parent) { Q_D(MyMoneyAccount); d->m_parentAccount = parent; } QStringList MyMoneyAccount::accountList() const { Q_D(const MyMoneyAccount); return d->m_accountList; } int MyMoneyAccount::accountCount() const { Q_D(const MyMoneyAccount); return d->m_accountList.count(); } void MyMoneyAccount::addAccountId(const QString& account) { Q_D(MyMoneyAccount); if (!d->m_accountList.contains(account)) d->m_accountList += account; } void MyMoneyAccount::removeAccountIds() { Q_D(MyMoneyAccount); d->m_accountList.clear(); } void MyMoneyAccount::removeAccountId(const QString& account) { Q_D(MyMoneyAccount); const auto pos = d->m_accountList.indexOf(account); if (pos != -1) d->m_accountList.removeAt(pos); } bool MyMoneyAccount::operator == (const MyMoneyAccount& right) const { Q_D(const MyMoneyAccount); auto d2 = static_cast(right.d_func()); return (MyMoneyKeyValueContainer::operator==(right) && MyMoneyObject::operator==(right) && (d->m_accountList == d2->m_accountList) && (d->m_accountType == d2->m_accountType) && (d->m_lastModified == d2->m_lastModified) && (d->m_lastReconciliationDate == d2->m_lastReconciliationDate) && ((d->m_name.length() == 0 && d2->m_name.length() == 0) || (d->m_name == d2->m_name)) && ((d->m_number.length() == 0 && d2->m_number.length() == 0) || (d->m_number == d2->m_number)) && ((d->m_description.length() == 0 && d2->m_description.length() == 0) || (d->m_description == d2->m_description)) && (d->m_openingDate == d2->m_openingDate) && (d->m_parentAccount == d2->m_parentAccount) && (d->m_currencyId == d2->m_currencyId) && (d->m_institution == d2->m_institution)); } Account::Type MyMoneyAccount::accountGroup() const { Q_D(const MyMoneyAccount); switch (d->m_accountType) { case Account::Type::Checkings: case Account::Type::Savings: case Account::Type::Cash: case Account::Type::Currency: case Account::Type::Investment: case Account::Type::MoneyMarket: case Account::Type::CertificateDep: case Account::Type::AssetLoan: case Account::Type::Stock: return Account::Type::Asset; case Account::Type::CreditCard: case Account::Type::Loan: return Account::Type::Liability; default: return d->m_accountType; } } QString MyMoneyAccount::currencyId() const { Q_D(const MyMoneyAccount); return d->m_currencyId; } void MyMoneyAccount::setCurrencyId(const QString& id) { Q_D(MyMoneyAccount); d->m_currencyId = id; } bool MyMoneyAccount::isAssetLiability() const { return accountGroup() == Account::Type::Asset || accountGroup() == Account::Type::Liability; } bool MyMoneyAccount::isIncomeExpense() const { return accountGroup() == Account::Type::Income || accountGroup() == Account::Type::Expense; } bool MyMoneyAccount::isLoan() const { return accountType() == Account::Type::Loan || accountType() == Account::Type::AssetLoan; } bool MyMoneyAccount::isInvest() const { return accountType() == Account::Type::Stock; } bool MyMoneyAccount::isLiquidAsset() const { return accountType() == Account::Type::Checkings || accountType() == Account::Type::Savings || accountType() == Account::Type::Cash; } bool MyMoneyAccount::isCostCenterRequired() const { return value("CostCenter").toLower() == QLatin1String("yes"); } void MyMoneyAccount::setCostCenterRequired(bool required) { if(required) { setValue("CostCenter", "yes"); } else { deletePair("CostCenter"); } } void MyMoneyAccount::writeXML(QDomDocument& document, QDomElement& parent) const { auto el = document.createElement(nodeNames[nnAccount]); Q_D(const MyMoneyAccount); d->writeBaseXML(document, el); el.setAttribute(d->getAttrName(Account::Attribute::ParentAccount), parentAccountId()); el.setAttribute(d->getAttrName(Account::Attribute::LastReconciled), MyMoneyUtils::dateToString(lastReconciliationDate())); el.setAttribute(d->getAttrName(Account::Attribute::LastModified), MyMoneyUtils::dateToString(lastModified())); el.setAttribute(d->getAttrName(Account::Attribute::Institution), institutionId()); el.setAttribute(d->getAttrName(Account::Attribute::Opened), MyMoneyUtils::dateToString(openingDate())); el.setAttribute(d->getAttrName(Account::Attribute::Number), number()); // el.setAttribute(getAttrName(anOpeningBalance), openingBalance().toString()); el.setAttribute(d->getAttrName(Account::Attribute::Type), (int)accountType()); el.setAttribute(d->getAttrName(Account::Attribute::Name), name()); el.setAttribute(d->getAttrName(Account::Attribute::Description), description()); if (!currencyId().isEmpty()) el.setAttribute(d->getAttrName(Account::Attribute::Currency), currencyId()); //Add in subaccount information, if this account has subaccounts. if (accountCount()) { QDomElement subAccounts = document.createElement(d->getElName(Account::Element::SubAccounts)); foreach (const auto accountID, accountList()) { QDomElement temp = document.createElement(d->getElName(Account::Element::SubAccount)); temp.setAttribute(d->getAttrName(Account::Attribute::ID), accountID); subAccounts.appendChild(temp); } el.appendChild(subAccounts); } // Write online banking settings if (d->m_onlineBankingSettings.pairs().count()) { QDomElement onlinesettings = document.createElement(d->getElName(Account::Element::OnlineBanking)); QMap::const_iterator it_key = d->m_onlineBankingSettings.pairs().constBegin(); while (it_key != d->m_onlineBankingSettings.pairs().constEnd()) { onlinesettings.setAttribute(it_key.key(), it_key.value()); ++it_key; } el.appendChild(onlinesettings); } // FIXME drop the lastStatementDate record from the KVP when it is // not stored there after setLastReconciliationDate() has been changed // See comment there when this will happen // deletePair("lastStatementDate"); //Add in Key-Value Pairs for accounts. MyMoneyKeyValueContainer::writeXML(document, el); parent.appendChild(el); } bool MyMoneyAccount::hasReferenceTo(const QString& id) const { Q_D(const MyMoneyAccount); return (id == d->m_institution) || (id == d->m_parentAccount) || (id == d->m_currencyId); } void MyMoneyAccount::setOnlineBankingSettings(const MyMoneyKeyValueContainer& values) { Q_D(MyMoneyAccount); d->m_onlineBankingSettings = values; } MyMoneyKeyValueContainer MyMoneyAccount::onlineBankingSettings() const { Q_D(const MyMoneyAccount); return d->m_onlineBankingSettings; } void MyMoneyAccount::setClosed(bool closed) { if (closed) setValue("mm-closed", "yes"); else deletePair("mm-closed"); } bool MyMoneyAccount::isClosed() const { return !(value("mm-closed").isEmpty()); } int MyMoneyAccount::fraction(const MyMoneySecurity& sec) const { Q_D(const MyMoneyAccount); int fraction; if (d->m_accountType == Account::Type::Cash) fraction = sec.smallestCashFraction(); else fraction = sec.smallestAccountFraction(); return fraction; } int MyMoneyAccount::fraction(const MyMoneySecurity& sec) { Q_D(MyMoneyAccount); if (d->m_accountType == Account::Type::Cash) d->m_fraction = sec.smallestCashFraction(); else d->m_fraction = sec.smallestAccountFraction(); return d->m_fraction; } int MyMoneyAccount::fraction() const { Q_D(const MyMoneyAccount); return d->m_fraction; } bool MyMoneyAccount::isCategory() const { Q_D(const MyMoneyAccount); return d->m_accountType == Account::Type::Income || d->m_accountType == Account::Type::Expense; } QString MyMoneyAccount::brokerageName() const { Q_D(const MyMoneyAccount); if (d->m_accountType == Account::Type::Investment) return QString("%1 (%2)").arg(d->m_name, i18nc("Brokerage (suffix for account names)", "Brokerage")); return d->m_name; } MyMoneyMoney MyMoneyAccount::balance() const { Q_D(const MyMoneyAccount); return d->m_balance; } void MyMoneyAccount::adjustBalance(const MyMoneySplit& s, bool reverse) { Q_D(MyMoneyAccount); if (s.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::SplitShares)) { if (reverse) d->m_balance = d->m_balance / s.shares(); else d->m_balance = d->m_balance * s.shares(); } else { if (reverse) d->m_balance -= s.shares(); else d->m_balance += s.shares(); } } void MyMoneyAccount::setBalance(const MyMoneyMoney& val) { Q_D(MyMoneyAccount); d->m_balance = val; } QPixmap MyMoneyAccount::accountPixmap(const bool reconcileFlag, const int size) const { static const QHash accToIco { {Account::Type::Asset, Icon::ViewAsset}, {Account::Type::Investment, Icon::ViewStock}, {Account::Type::Stock, Icon::ViewStock}, {Account::Type::MoneyMarket, Icon::ViewStock}, {Account::Type::Checkings, Icon::ViewChecking}, {Account::Type::Savings, Icon::ViewSaving}, {Account::Type::AssetLoan, Icon::ViewLoanAsset}, {Account::Type::Loan, Icon::ViewLoan}, {Account::Type::CreditCard, Icon::ViewCreditCard}, {Account::Type::Asset, Icon::ViewAsset}, {Account::Type::Cash, Icon::ViewCash}, {Account::Type::Income, Icon::ViewIncome}, {Account::Type::Expense, Icon::ViewExpense}, {Account::Type::Equity, Icon::ViewEquity} }; Icon ixIcon = accToIco.value(accountType(), Icon::ViewLiability); QString kyIcon = accountTypeToString(accountType()) + QString::number(size); QPixmap pxIcon; if (!QPixmapCache::find(kyIcon, pxIcon)) { pxIcon = Icons::get(ixIcon).pixmap(size); // Qt::AA_UseHighDpiPixmaps (in Qt 5.7) doesn't return highdpi pixmap QPixmapCache::insert(kyIcon, pxIcon); } if (isClosed()) ixIcon = Icon::AccountClosed; else if (reconcileFlag) ixIcon = Icon::FlagGreen; else if (hasOnlineMapping()) ixIcon = Icon::Download; else return pxIcon; QPixmap pxOverlay = Icons::get(ixIcon).pixmap(size); QPainter pxPainter(&pxIcon); const QSize szIcon = pxIcon.size(); pxPainter.drawPixmap(szIcon.width() / 2, szIcon.height() / 2, szIcon.width() / 2, szIcon.height() / 2, pxOverlay); return pxIcon; } QString MyMoneyAccount::accountTypeToString(const Account::Type accountType) { switch (accountType) { case Account::Type::Checkings: return i18nc("Account type", "Checking"); case Account::Type::Savings: return i18nc("Account type", "Savings"); case Account::Type::CreditCard: return i18nc("Account type", "Credit Card"); case Account::Type::Cash: return i18nc("Account type", "Cash"); case Account::Type::Loan: return i18nc("Account type", "Loan"); case Account::Type::CertificateDep: return i18nc("Account type", "Certificate of Deposit"); case Account::Type::Investment: return i18nc("Account type", "Investment"); case Account::Type::MoneyMarket: return i18nc("Account type", "Money Market"); case Account::Type::Asset: return i18nc("Account type", "Asset"); case Account::Type::Liability: return i18nc("Account type", "Liability"); case Account::Type::Currency: return i18nc("Account type", "Currency"); case Account::Type::Income: return i18nc("Account type", "Income"); case Account::Type::Expense: return i18nc("Account type", "Expense"); case Account::Type::AssetLoan: return i18nc("Account type", "Investment Loan"); case Account::Type::Stock: return i18nc("Account type", "Stock"); case Account::Type::Equity: return i18nc("Account type", "Equity"); default: return i18nc("Account type", "Unknown"); } } bool MyMoneyAccount::addReconciliation(const QDate& date, const MyMoneyMoney& amount) { Q_D(MyMoneyAccount); d->m_reconciliationHistory[date] = amount; QString history, sep; QMap::const_iterator it; for (it = d->m_reconciliationHistory.constBegin(); it != d->m_reconciliationHistory.constEnd(); ++it) { history += QString("%1%2:%3").arg(sep, it.key().toString(Qt::ISODate), (*it).toString()); sep = QLatin1Char(';'); } setValue("reconciliationHistory", history); return true; } QMap MyMoneyAccount::reconciliationHistory() { Q_D(MyMoneyAccount); // check if the internal history member is already loaded if (d->m_reconciliationHistory.count() == 0 && !value("reconciliationHistory").isEmpty()) { QStringList entries = value("reconciliationHistory").split(';'); foreach (const QString& entry, entries) { QStringList parts = entry.split(':'); QDate date = QDate::fromString(parts[0], Qt::ISODate); MyMoneyMoney amount(parts[1]); if (parts.count() == 2 && date.isValid()) { d->m_reconciliationHistory[date] = amount; } } } return d->m_reconciliationHistory; } /** * @todo Improve setting of country for nationalAccount */ QList< payeeIdentifier > MyMoneyAccount::payeeIdentifiers() const { Q_D(const MyMoneyAccount); QList< payeeIdentifier > list; MyMoneyFile* file = MyMoneyFile::instance(); // Iban & Bic if (!value(d->getAttrName(Account::Attribute::IBAN)).isEmpty()) { payeeIdentifierTyped iban(new payeeIdentifiers::ibanBic); iban->setIban(value(d->getAttrName(Account::Attribute::IBAN))); iban->setBic(file->institution(institutionId()).value(d->getAttrName(Account::Attribute::BIC))); iban->setOwnerName(file->user().name()); list.append(iban); } // National Account number if (!number().isEmpty()) { payeeIdentifierTyped national(new payeeIdentifiers::nationalAccount); national->setAccountNumber(number()); national->setBankCode(file->institution(institutionId()).sortcode()); if (file->user().state().length() == 2) national->setCountry(file->user().state()); national->setOwnerName(file->user().name()); list.append(national); } return list; } bool MyMoneyAccount::hasOnlineMapping() const { Q_D(const MyMoneyAccount); return !d->m_onlineBankingSettings.value(QLatin1String("provider")).isEmpty(); } diff --git a/kmymoney/mymoney/mymoneyaccount.h b/kmymoney/mymoney/mymoneyaccount.h index bcabcc753..13ab74bc1 100644 --- a/kmymoney/mymoney/mymoneyaccount.h +++ b/kmymoney/mymoney/mymoneyaccount.h @@ -1,600 +1,601 @@ /* * Copyright 2000-2002 Michael Edwardes * Copyright 2001 Felix Rodriguez * Copyright 2002-2003 Kevin Tambascio * Copyright 2006-2017 Thomas Baumgart * Copyright 2006 Ace Jones * 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 MYMONEYACCOUNT_H #define MYMONEYACCOUNT_H // ---------------------------------------------------------------------------- // QT Includes #include // ---------------------------------------------------------------------------- // Project Includes #include "mymoneykeyvaluecontainer.h" #include "mymoneyobject.h" #include "kmm_mymoney_export.h" #include "mymoneyunittestable.h" class QString; class QDate; class QPixmap; class QDomElement; class MyMoneySecurity; class MyMoneyMoney; class MyMoneySplit; class payeeIdentifier; namespace eMyMoney { namespace Account { enum class Type; } } template class payeeIdentifierTyped; /** * A representation of an account. * This object represents any type of account, those held at an * institution as well as the accounts used for double entry * accounting. * * Currently, the following account types are known: * * @li Unknown * @li Checkings * @li Savings * @li Cash * @li CreditCard * @li Loan (collected) * @li CertificateDep * @li Investment * @li MoneyMarket * @li Currency * @li Asset * @li Liability * @li Income * @li Expense * @li Loan (given) * @li Stock * @li Equity * * @see MyMoneyInstitution * @see MyMoneyFile * * @author Michael Edwardes 2000-2001 * @author Thomas Baumgart 2002 * @author Łukasz Wojniłowicz 2017 * **/ class MyMoneyAccountPrivate; class KMM_MYMONEY_EXPORT MyMoneyAccount : public MyMoneyObject, public MyMoneyKeyValueContainer /*, public MyMoneyPayeeIdentifierContainer */ { Q_DECLARE_PRIVATE_D(MyMoneyObject::d_ptr, MyMoneyAccount) KMM_MYMONEY_UNIT_TESTABLE public: /** * This is the constructor for a new empty account */ MyMoneyAccount(); + explicit MyMoneyAccount(const QString &id); /** * This is the constructor for a new account known to the current file * This is the only constructor that will set the attribute m_openingDate * to a correct value. * * @param id id assigned to the account * @param right account definition */ MyMoneyAccount(const QString& id, const MyMoneyAccount& other); /** * This is the constructor for an account that is described by a * QDomElement (e.g. from a file). * * @param el const reference to the QDomElement from which to * create the object */ explicit MyMoneyAccount(const QDomElement& el); MyMoneyAccount(const MyMoneyAccount & other); MyMoneyAccount(MyMoneyAccount && other); MyMoneyAccount & operator=(MyMoneyAccount other); friend void swap(MyMoneyAccount& first, MyMoneyAccount& second); /** * This is the destructor for any MyMoneyAccount object */ ~MyMoneyAccount(); /** * This operator tests for equality of two MyMoneyAccount objects */ bool operator == (const MyMoneyAccount &) const; /** * This converts the account type into one of the four * major account types liability, asset, expense or income. * * The current assignment is as follows: * * - Asset * - Asset * - Checkings * - Savings * - Cash * - Currency * - Investment * - MoneyMarket * - CertificateDep * - AssetLoan * - Stock * * - Liability * - Liability * - CreditCard * - Loan * * - Income * - Income * * - Expense * - Expense * * @return accountTypeE of major account type */ eMyMoney::Account::Type accountGroup() const; /** * This method returns the id of the MyMoneyInstitution object this account * belongs to. * @return id of MyMoneyInstitution object. QString() if it is * an internal account * @see setInstitution */ QString institutionId() const; /** * This method is used to set the id of the institution this account * belongs to. * * @param id id of the institution this account belongs to * @see institution */ void setInstitutionId(const QString& id); /** * This method returns the name of the account * @return name of account * @see setName() */ QString name() const; /** * This method is used to set the name of the account * @param name name of the account * @see name */ void setName(const QString& name); /** * This method returns the number of the account at the institution * @return number of account at the institution * @see setNumber */ QString number() const; /** * This method is used to set the number of the account at the institution * @param number number of the account * @see number */ void setNumber(const QString& number); /** * This method returns the descriptive text of the account. * @return description of account * @see setDescription */ QString description() const; /** * This method is used to set the descriptive text of the account * * @param desc text that serves as description * @see setDescription */ void setDescription(const QString& desc); /** * This method returns the opening date of this account * @return date of opening of this account as const QDate value * @see setOpeningDate() */ QDate openingDate() const; /** * This method is used to set the opening date information of an * account. * * @param date QDate of opening date * @see openingDate */ void setOpeningDate(const QDate& date); /** * This method returns the date of the last reconciliation of this account * @return date of last reconciliation as const QDate value * @see setLastReconciliationDate */ QDate lastReconciliationDate() const; /** * This method is used to set the date of the last reconciliation * of an account. * @param date QDate of last reconciliation * @see lastReconciliationDate */ void setLastReconciliationDate(const QDate& date); /** * This method returns the date the account was last modified * @return date of last modification as const QDate value * @see setLastModified */ QDate lastModified() const; /** * This method is used to modify the date of the last * modification access. * @param date date of last modification * @see lastModified */ void setLastModified(const QDate& date); /** * This method is used to return the ID of the parent account * @return QString with the ID of the parent of this account */ QString parentAccountId() const; /** * This method is used to set a new parent account id * @param parent QString reference to new parent account */ void setParentAccountId(const QString& parent); /** * This method returns the list of the account id's of * subordinate accounts * @return QStringList account ids */ QStringList accountList() const; /** * This method returns the number of entries in the m_accountList * @return number of entries in the accountList */ int accountCount() const; /** * This method is used to add an account id as sub-ordinate account * @param account const QString reference to account ID */ void addAccountId(const QString& account); /** * This method is used to remove an account from the list of * sub-ordinate accounts. * @param account const QString reference to account ID to be removed */ void removeAccountId(const QString& account); /** * This method is used to remove all accounts from the list of * sub-ordinate accounts. */ void removeAccountIds(); /** * Return the stored account identifiers * * @internal This method is temporary until MyMoneyAccount is a MyMoneyPayeeIdentifierContainer. But before this * can happen the account numbers and iban kvp data must be moved there. */ QList< payeeIdentifier > payeeIdentifiers() const; /** * This method is used to update m_lastModified to the current date */ void touch(); /** * This method returns the type of the account. */ eMyMoney::Account::Type accountType() const; /** * This method is used to change the account type * * @param type account type */ void setAccountType(const eMyMoney::Account::Type type); /** * This method retrieves the id of the currency used with this account. * If the return value is empty, the base currency should be used. * * @return id of currency */ QString currencyId() const; /** * This method sets the id of the currency used with this account. * * @param id ID of currency to be associated with this account. */ void setCurrencyId(const QString& id); void writeXML(QDomDocument& document, QDomElement& parent) const override; /** * This method checks if a reference to the given object exists. It returns, * a @p true if the object is referencing the one requested by the * parameter @p id. If it does not, this method returns @p false. * * @param id id of the object to be checked for references * @retval true This object references object with id @p id. * @retval false This object does not reference the object with id @p id. */ bool hasReferenceTo(const QString& id) const override; /** * This member returns the balance of this account based on * all transactions stored in the journal. */ MyMoneyMoney balance() const; /** * This method adjusts the balance of this account * according to the difference contained in the split @p s. * If the s.action() is MyMoneySplit::actionName(eMyMoney::Split::Action::SplitShares) then * the balance will be adjusted accordingly. * * @param s const reference to MyMoneySplit object containing the * value to be added/subtracted to/from the balance * @param reverse add (false) or subtract (true) the shares contained in the split. * It also affects the balance for share splits in the opposite direction. */ void adjustBalance(const MyMoneySplit& s, bool reverse = false); /** * This method sets the balance of this account * according to the value provided by @p val. * * @param val const reference to MyMoneyMoney object containing the * value to be assigned to the balance */ void setBalance(const MyMoneyMoney& val); /** * This method sets the kvp's for online banking with this account * * @param values The container of kvp's needed when connecting to this account */ void setOnlineBankingSettings(const MyMoneyKeyValueContainer& values); /** * This method retrieves the kvp's for online banking with this account * * @return The container of kvp's needed when connecting to this account */ MyMoneyKeyValueContainer onlineBankingSettings() const; /** * This method sets the closed flag for the account. This is just * an informational flag for the application. It has no other influence * on the behaviour of the account object. The default for * new objects @p open. * * @param isClosed mark the account closed (@p true) or open (@p false). */ void setClosed(bool isClosed); /** * Return the closed flag for the account. * * @retval false account is marked open (the default for new accounts) * @retval true account is marked closed */ bool isClosed() const; /** * returns the applicable smallest fraction for this account * for the given security based on the account type. At the same * time, m_fraction is updated to the value returned. * * @param sec const reference to currency (security) * * @retval sec.smallestCashFraction() for account type Cash * @retval sec.smallestAccountFraction() for all other account types */ int fraction(const MyMoneySecurity& sec); /** * Same as the above method, but does not modify m_fraction. */ int fraction(const MyMoneySecurity& sec) const; /** * This method returns the stored value for the fraction of this * account or -1 if not initialized. It can be initialized by * calling fraction(const MyMoneySecurity& sec) once. * * @note Don't use this method outside of KMyMoney application context (eg. testcases). * Use the above method instead. */ int fraction() const; /** * This method returns @a true if the account type is * either Income or Expense * * @retval true account is of type income or expense * @retval false for all other account types * * @deprecated use isIncomeExpense() instead */ KMM_MYMONEY_DEPRECATED bool isCategory() const; /** * This method returns @a true if the account type is * either Income or Expense * * @retval true account is of type income or expense * @retval false for all other account types */ bool isIncomeExpense() const; /** * This method returns @a true if the account type is * either Asset or Liability * * @retval true account is of type asset or liability * @retval false for all other account types */ bool isAssetLiability() const; /** * This method returns @a true if the account type is * either AssetLoan or Loan * * @retval true account is of type Loan or AssetLoan * @retval false for all other account types */ bool isLoan() const; /** * This method returns @a true if the account type is * Stock * * @retval true account is of type Stock * @retval false for all other account types */ bool isInvest() const; /** * This method returns @a true if the account type is * Checkings, Savings or Cash * * @retval true account is of type Checkings, Savings or Cash * @retval false for all other account types */ bool isLiquidAsset() const; /** * This method returns true if a costcenter assignment is required for this account */ bool isCostCenterRequired() const; /** * This method allows to control if a cost center assignment is required * for this account. It is if @a required is @c true (the default). */ void setCostCenterRequired(bool required = true); /** * This method returns a name that has a brokerage suffix of * the current name. It only works on investment accounts and * returns the name for all other cases. */ QString brokerageName() const; /** * @param reconcileFlag if set to @a true a reconcile overlay will be * added to the pixmap returned * @param size is a hint for the size of the icon * @return a pixmap using DesktopIcon for the account type */ QPixmap accountPixmap(const bool reconcileFlag = false, const int size = 64) const; /** * This method is used to convert the internal representation of * an account type into a human readable format * * @param accountType numerical representation of the account type. * For possible values, see eMyMoney::Account::Type * @return QString representing the human readable form */ static QString accountTypeToString(const eMyMoney::Account::Type accountType); /** * keeps a history record of a reconciliation for this account on @a date * with @a amount. * * @return @p true in case entry was added, @p false otherwise * * @sa reconciliationHistory() */ bool addReconciliation(const QDate& date, const MyMoneyMoney& amount); /** * @return QMap with the reconciliation history for the account * * @sa addReconciliation() */ QMap reconciliationHistory(); /** * @return @c true if account has an online mapping, @c false otherwise */ bool hasOnlineMapping() const; QDataStream &operator<<(const MyMoneyAccount &); QDataStream &operator>>(MyMoneyAccount &); }; inline void swap(MyMoneyAccount& first, MyMoneyAccount& second) // krazy:exclude=inline { using std::swap; swap(first.MyMoneyObject::d_ptr, second.MyMoneyObject::d_ptr); swap(first.MyMoneyKeyValueContainer::d_ptr, second.MyMoneyKeyValueContainer::d_ptr); } inline MyMoneyAccount::MyMoneyAccount(MyMoneyAccount && other) : MyMoneyAccount() // krazy:exclude=inline { swap(*this, other); } inline MyMoneyAccount & MyMoneyAccount::operator=(MyMoneyAccount other) // krazy:exclude=inline { swap(*this, other); return *this; } /** * Make it possible to hold @ref MyMoneyAccount objects, * @ref accountTypeE and @ref amountTypeE inside @ref QVariant objects. */ Q_DECLARE_METATYPE(MyMoneyAccount) Q_DECLARE_METATYPE(eMyMoney::Account::Type) #endif diff --git a/kmymoney/mymoney/mymoneybudget.cpp b/kmymoney/mymoney/mymoneybudget.cpp index 81538ac01..02e38c482 100644 --- a/kmymoney/mymoney/mymoneybudget.cpp +++ b/kmymoney/mymoney/mymoneybudget.cpp @@ -1,600 +1,596 @@ /* * Copyright 2006 Ace Jones * Copyright 2006 Darren Gould * Copyright 2017 Ł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 "mymoneybudget.h" #include "mymoneybudget_p.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes class MyMoneyBudget::PeriodGroupPrivate { public: QDate m_start; MyMoneyMoney m_amount; }; MyMoneyBudget::PeriodGroup::PeriodGroup() : d_ptr(new PeriodGroupPrivate) { } MyMoneyBudget::PeriodGroup::PeriodGroup(const MyMoneyBudget::PeriodGroup & other) : d_ptr(new PeriodGroupPrivate(*other.d_func())) { } MyMoneyBudget::PeriodGroup::~PeriodGroup() { Q_D(PeriodGroup); delete d; } QDate MyMoneyBudget::PeriodGroup::startDate() const { Q_D(const PeriodGroup); return d->m_start; } void MyMoneyBudget::PeriodGroup::setStartDate(const QDate& start) { Q_D(PeriodGroup); d->m_start = start; } MyMoneyMoney MyMoneyBudget::PeriodGroup::amount() const { Q_D(const PeriodGroup); return d->m_amount; } void MyMoneyBudget::PeriodGroup::setAmount(const MyMoneyMoney& amount) { Q_D(PeriodGroup); d->m_amount = amount; } bool MyMoneyBudget::PeriodGroup::operator == (const PeriodGroup& right) const { Q_D(const PeriodGroup); auto d2 = static_cast(right.d_func()); return (d->m_start == d2->m_start && d->m_amount == d2->m_amount); } -const QStringList MyMoneyBudget::AccountGroup::kBudgetLevelText = QString("none,monthly,monthbymonth,yearly,invalid").split(','); const int BUDGET_VERSION = 2; class MyMoneyBudget::AccountGroupPrivate { public: AccountGroupPrivate() : - m_budgetlevel(AccountGroup::eBudgetLevel::eNone), + m_budgetlevel(eMyMoney::Budget::Level::None), m_budgetsubaccounts(false) { } QString m_id; - AccountGroup::eBudgetLevel m_budgetlevel; + eMyMoney::Budget::Level m_budgetlevel; bool m_budgetsubaccounts; QMap m_periods; }; MyMoneyBudget::AccountGroup::AccountGroup() : d_ptr(new AccountGroupPrivate) { } MyMoneyBudget::AccountGroup::AccountGroup(const MyMoneyBudget::AccountGroup& other) : d_ptr(new AccountGroupPrivate(*other.d_func())) { } MyMoneyBudget::AccountGroup::~AccountGroup() { Q_D(AccountGroup); delete d; } bool MyMoneyBudget::AccountGroup::isZero() const { Q_D(const AccountGroup); - return (!d->m_budgetsubaccounts && d->m_budgetlevel == eMonthly && balance().isZero()); + return (!d->m_budgetsubaccounts && d->m_budgetlevel == eMyMoney::Budget::Level::Monthly && balance().isZero()); } void MyMoneyBudget::AccountGroup::convertToMonthly() { MyMoneyBudget::PeriodGroup period; Q_D(AccountGroup); switch (d->m_budgetlevel) { - case eYearly: - case eMonthByMonth: + case eMyMoney::Budget::Level::Yearly: + case eMyMoney::Budget::Level::MonthByMonth: period = d->m_periods.first(); // make him monthly period.setAmount(balance() / MyMoneyMoney(12, 1)); clearPeriods(); addPeriod(period.startDate(), period); break; default: break; } - d->m_budgetlevel = eMonthly; + d->m_budgetlevel = eMyMoney::Budget::Level::Monthly; } void MyMoneyBudget::AccountGroup::convertToYearly() { MyMoneyBudget::PeriodGroup period; Q_D(AccountGroup); switch (d->m_budgetlevel) { - case eMonthByMonth: - case eMonthly: + case eMyMoney::Budget::Level::MonthByMonth: + case eMyMoney::Budget::Level::Monthly: period = d->m_periods.first(); // make him monthly period.setAmount(totalBalance()); clearPeriods(); addPeriod(period.startDate(), period); break; default: break; } - d->m_budgetlevel = eYearly; + d->m_budgetlevel = eMyMoney::Budget::Level::Yearly; } void MyMoneyBudget::AccountGroup::convertToMonthByMonth() { MyMoneyBudget::PeriodGroup period; QDate date; Q_D(AccountGroup); switch (d->m_budgetlevel) { - case eYearly: - case eMonthly: + case eMyMoney::Budget::Level::Yearly: + case eMyMoney::Budget::Level::Monthly: period = d->m_periods.first(); period.setAmount(totalBalance() / MyMoneyMoney(12, 1)); clearPeriods(); date = period.startDate(); for (auto i = 0; i < 12; ++i) { addPeriod(date, period); date = date.addMonths(1); period.setStartDate(date); } break; default: break; } - d->m_budgetlevel = eMonthByMonth; + d->m_budgetlevel = eMyMoney::Budget::Level::MonthByMonth; } QString MyMoneyBudget::AccountGroup::id() const { Q_D(const AccountGroup); return d->m_id; } void MyMoneyBudget::AccountGroup::setId(const QString& id) { Q_D(AccountGroup); d->m_id = id; } bool MyMoneyBudget::AccountGroup::budgetSubaccounts() const { Q_D(const AccountGroup); return d->m_budgetsubaccounts; } void MyMoneyBudget::AccountGroup::setBudgetSubaccounts(bool budgetsubaccounts) { Q_D(AccountGroup); d->m_budgetsubaccounts = budgetsubaccounts; } -MyMoneyBudget::AccountGroup::eBudgetLevel MyMoneyBudget::AccountGroup::budgetLevel() const +eMyMoney::Budget::Level MyMoneyBudget::AccountGroup::budgetLevel() const { Q_D(const AccountGroup); return d->m_budgetlevel; } -void MyMoneyBudget::AccountGroup::setBudgetLevel(eBudgetLevel level) +void MyMoneyBudget::AccountGroup::setBudgetLevel(eMyMoney::Budget::Level level) { Q_D(AccountGroup); d->m_budgetlevel = level; } MyMoneyBudget::PeriodGroup MyMoneyBudget::AccountGroup::period(const QDate& date) const { Q_D(const AccountGroup); return d->m_periods[date]; } void MyMoneyBudget::AccountGroup::addPeriod(const QDate& date, PeriodGroup& period) { Q_D(AccountGroup); d->m_periods[date] = period; } const QMap MyMoneyBudget::AccountGroup::getPeriods() const { Q_D(const AccountGroup); return d->m_periods; } void MyMoneyBudget::AccountGroup::clearPeriods() { Q_D(AccountGroup); d->m_periods.clear(); } MyMoneyMoney MyMoneyBudget::AccountGroup::balance() const { Q_D(const AccountGroup); MyMoneyMoney balance; foreach (const auto period, d->m_periods) balance += period.amount(); return balance; } MyMoneyMoney MyMoneyBudget::AccountGroup::totalBalance() const { Q_D(const AccountGroup); auto bal = balance(); switch (d->m_budgetlevel) { default: break; - case eMonthly: + case eMyMoney::Budget::Level::Monthly: bal = bal * 12; break; } return bal; } MyMoneyBudget::AccountGroup MyMoneyBudget::AccountGroup::operator += (const MyMoneyBudget::AccountGroup& right) { Q_D(AccountGroup); auto d2 = static_cast(right.d_func()); // in case the right side is empty, we're done - if (d2->m_budgetlevel == eNone) + if (d2->m_budgetlevel == eMyMoney::Budget::Level::None) return *this; MyMoneyBudget::AccountGroup r(right); auto d3 = static_cast(r.d_func()); // make both operands based on the same budget level if (d->m_budgetlevel != d3->m_budgetlevel) { - if (d->m_budgetlevel == eMonthly) { // my budget is monthly - if (d3->m_budgetlevel == eYearly) { // his is yearly + if (d->m_budgetlevel == eMyMoney::Budget::Level::Monthly) { // my budget is monthly + if (d3->m_budgetlevel == eMyMoney::Budget::Level::Yearly) { // his is yearly r.convertToMonthly(); - } else if (d3->m_budgetlevel == eMonthByMonth) { // his is month by month + } else if (d3->m_budgetlevel == eMyMoney::Budget::Level::MonthByMonth) { // his is month by month convertToMonthByMonth(); } - } else if (d->m_budgetlevel == eYearly) { // my budget is yearly - if (d3->m_budgetlevel == eMonthly) { // his is monthly + } else if (d->m_budgetlevel == eMyMoney::Budget::Level::Yearly) { // my budget is yearly + if (d3->m_budgetlevel == eMyMoney::Budget::Level::Monthly) { // his is monthly r.convertToYearly(); - } else if (d3->m_budgetlevel == eMonthByMonth) { // his is month by month + } else if (d3->m_budgetlevel == eMyMoney::Budget::Level::MonthByMonth) { // his is month by month convertToMonthByMonth(); } - } else if (d->m_budgetlevel == eMonthByMonth) { // my budget is month by month + } else if (d->m_budgetlevel == eMyMoney::Budget::Level::MonthByMonth) { // my budget is month by month r.convertToMonthByMonth(); } } QMap rPeriods = d3->m_periods; QMap::const_iterator it_pr; // in case the left side is empty, we add empty periods // so that both budgets are identical - if (d->m_budgetlevel == eNone) { + if (d->m_budgetlevel == eMyMoney::Budget::Level::None) { it_pr = rPeriods.constBegin(); QDate date = (*it_pr).startDate(); while (it_pr != rPeriods.constEnd()) { MyMoneyBudget::PeriodGroup period = *it_pr; period.setAmount(MyMoneyMoney()); addPeriod(date, period); date = date.addMonths(1); ++it_pr; } d->m_budgetlevel = d3->m_budgetlevel; } QMap periods = d->m_periods; QMap::const_iterator it_p; // now both budgets should be of the same type and we simply need // to iterate over the period list and add the values d->m_periods.clear(); it_p = periods.constBegin(); it_pr = rPeriods.constBegin(); QDate date = (*it_p).startDate(); while (it_p != periods.constEnd()) { MyMoneyBudget::PeriodGroup period = *it_p; if (it_pr != rPeriods.constEnd()) { period.setAmount(period.amount() + (*it_pr).amount()); ++it_pr; } addPeriod(date, period); date = date.addMonths(1); ++it_p; } return *this; } bool MyMoneyBudget::AccountGroup::operator == (const AccountGroup& right) const { Q_D(const AccountGroup); auto d2 = static_cast(right.d_func()); return (d->m_id == d2->m_id && d->m_budgetlevel == d2->m_budgetlevel && d->m_budgetsubaccounts == d2->m_budgetsubaccounts && d->m_periods.keys() == d2->m_periods.keys() && d->m_periods.values() == d2->m_periods.values()); } MyMoneyBudget::MyMoneyBudget() : MyMoneyObject(*new MyMoneyBudgetPrivate) { Q_D(MyMoneyBudget); d->m_name = QLatin1Literal("Unconfigured Budget"); } -MyMoneyBudget::MyMoneyBudget(const QString& name) : - MyMoneyObject(*new MyMoneyBudgetPrivate) +MyMoneyBudget::MyMoneyBudget(const QString &id) : + MyMoneyObject(*new MyMoneyBudgetPrivate, id) { Q_D(MyMoneyBudget); - d->m_name = name; + d->m_name = QLatin1Literal("Unconfigured Budget"); } MyMoneyBudget::MyMoneyBudget(const QDomElement& node) : MyMoneyObject(*new MyMoneyBudgetPrivate, node) { if (!read(node)) clearId(); } MyMoneyBudget::MyMoneyBudget(const QString& id, const MyMoneyBudget& other) : MyMoneyObject(*new MyMoneyBudgetPrivate(*other.d_func()), id) { } MyMoneyBudget::MyMoneyBudget(const MyMoneyBudget& other) : MyMoneyObject(*new MyMoneyBudgetPrivate(*other.d_func()), other.id()) { } MyMoneyBudget::~MyMoneyBudget() { } bool MyMoneyBudget::operator == (const MyMoneyBudget& right) const { Q_D(const MyMoneyBudget); auto d2 = static_cast(right.d_func()); return (MyMoneyObject::operator==(right) && (d->m_accounts.count() == d2->m_accounts.count()) && (d->m_accounts.keys() == d2->m_accounts.keys()) && (d->m_accounts.values() == d2->m_accounts.values()) && (d->m_name == d2->m_name) && (d->m_start == d2->m_start)); } void MyMoneyBudget::write(QDomElement& e, QDomDocument *doc) const { Q_D(const MyMoneyBudget); d->writeBaseXML(*doc, e); e.setAttribute(d->getAttrName(Budget::Attribute::Name), d->m_name); e.setAttribute(d->getAttrName(Budget::Attribute::Start), d->m_start.toString(Qt::ISODate)); e.setAttribute(d->getAttrName(Budget::Attribute::Version), BUDGET_VERSION); QMap::const_iterator it; for (it = d->m_accounts.begin(); it != d->m_accounts.end(); ++it) { // only add the account if there is a budget entered // or it covers some sub accounts if (!(*it).balance().isZero() || (*it).budgetSubaccounts()) { QDomElement domAccount = doc->createElement(d->getElName(Budget::Element::Account)); domAccount.setAttribute(d->getAttrName(Budget::Attribute::ID), it.key()); - domAccount.setAttribute(d->getAttrName(Budget::Attribute::BudgetLevel), AccountGroup::kBudgetLevelText[it.value().budgetLevel()]); + domAccount.setAttribute(d->getAttrName(Budget::Attribute::BudgetLevel), d->budgetNames(it.value().budgetLevel())); domAccount.setAttribute(d->getAttrName(Budget::Attribute::BudgetSubAccounts), it.value().budgetSubaccounts()); const QMap periods = it.value().getPeriods(); QMap::const_iterator it_per; for (it_per = periods.begin(); it_per != periods.end(); ++it_per) { if (!(*it_per).amount().isZero()) { QDomElement domPeriod = doc->createElement(d->getElName(Budget::Element::Period)); domPeriod.setAttribute(d->getAttrName(Budget::Attribute::Amount), (*it_per).amount().toString()); domPeriod.setAttribute(d->getAttrName(Budget::Attribute::Start), (*it_per).startDate().toString(Qt::ISODate)); domAccount.appendChild(domPeriod); } } e.appendChild(domAccount); } } } bool MyMoneyBudget::read(const QDomElement& e) { // The goal of this reading method is 100% backward AND 100% forward // compatibility. Any Budget ever created with any version of KMyMoney // should be able to be loaded by this method (as long as it's one of the // Budget types supported in this version, of course) Q_D(MyMoneyBudget); auto result = false; if (d->getElName(Budget::Element::Budget) == e.tagName()) { result = true; d->m_name = e.attribute(d->getAttrName(Budget::Attribute::Name)); d->m_start = QDate::fromString(e.attribute(d->getAttrName(Budget::Attribute::Start)), Qt::ISODate); d->m_id = e.attribute(d->getAttrName(Budget::Attribute::ID)); QDomNode child = e.firstChild(); while (!child.isNull() && child.isElement()) { QDomElement c = child.toElement(); AccountGroup account; if (d->getElName(Budget::Element::Account) == c.tagName()) { if (c.hasAttribute(d->getAttrName(Budget::Attribute::ID))) account.setId(c.attribute(d->getAttrName(Budget::Attribute::ID))); - if (c.hasAttribute(d->getAttrName(Budget::Attribute::BudgetLevel))) { - int i = AccountGroup::kBudgetLevelText.indexOf(c.attribute(d->getAttrName(Budget::Attribute::BudgetLevel))); - if (i != -1) - account.setBudgetLevel(static_cast(i)); - } + if (c.hasAttribute(d->getAttrName(Budget::Attribute::BudgetLevel))) + account.setBudgetLevel(d->stringToBudgetLevel(c.attribute(d->getAttrName(Budget::Attribute::BudgetLevel)))); if (c.hasAttribute(d->getAttrName(Budget::Attribute::BudgetSubAccounts))) account.setBudgetSubaccounts(c.attribute(d->getAttrName(Budget::Attribute::BudgetSubAccounts)).toUInt()); } QDomNode period = c.firstChild(); while (!period.isNull() && period.isElement()) { QDomElement per = period.toElement(); PeriodGroup pGroup; if (d->getElName(Budget::Element::Period) == per.tagName() && per.hasAttribute(d->getAttrName(Budget::Attribute::Amount)) && per.hasAttribute(d->getAttrName(Budget::Attribute::Start))) { pGroup.setAmount(MyMoneyMoney(per.attribute(d->getAttrName(Budget::Attribute::Amount)))); pGroup.setStartDate(QDate::fromString(per.attribute(d->getAttrName(Budget::Attribute::Start)), Qt::ISODate)); account.addPeriod(pGroup.startDate(), pGroup); } period = period.nextSibling(); } d->m_accounts[account.id()] = account; child = child.nextSibling(); } } return result; } void MyMoneyBudget::writeXML(QDomDocument& document, QDomElement& parent) const { Q_D(const MyMoneyBudget); QDomElement el = document.createElement(d->getElName(Budget::Element::Budget)); write(el, &document); parent.appendChild(el); } bool MyMoneyBudget::hasReferenceTo(const QString& id) const { Q_D(const MyMoneyBudget); // return true if we have an assignment for this id return (d->m_accounts.contains(id)); } void MyMoneyBudget::removeReference(const QString& id) { Q_D(MyMoneyBudget); if (d->m_accounts.contains(id)) { d->m_accounts.remove(id); } } const MyMoneyBudget::AccountGroup& MyMoneyBudget::account(const QString& id) const { static AccountGroup empty; QMap::ConstIterator it; Q_D(const MyMoneyBudget); it = d->m_accounts.constFind(id); if (it != d->m_accounts.constEnd()) return it.value(); return empty; } void MyMoneyBudget::setAccount(const AccountGroup& account, const QString& id) { Q_D(MyMoneyBudget); if (account.isZero()) { d->m_accounts.remove(id); } else { // make sure we store a correct id AccountGroup acc(account); if (acc.id() != id) acc.setId(id); d->m_accounts[id] = acc; } } bool MyMoneyBudget::contains(const QString &id) const { Q_D(const MyMoneyBudget); return d->m_accounts.contains(id); } QList MyMoneyBudget::getaccounts() const { Q_D(const MyMoneyBudget); return d->m_accounts.values(); } QString MyMoneyBudget::name() const { Q_D(const MyMoneyBudget); return d->m_name; } void MyMoneyBudget::setName(const QString& name) { Q_D(MyMoneyBudget); d->m_name = name; } QDate MyMoneyBudget::budgetStart() const { Q_D(const MyMoneyBudget); return d->m_start; } void MyMoneyBudget::setBudgetStart(const QDate& start) { Q_D(MyMoneyBudget); auto oldDate = QDate(d->m_start.year(), d->m_start.month(), 1); d->m_start = QDate(start.year(), start.month(), 1); if (oldDate.isValid()) { int adjust = ((d->m_start.year() - oldDate.year()) * 12) + (d->m_start.month() - oldDate.month()); QMap::iterator it; for (it = d->m_accounts.begin(); it != d->m_accounts.end(); ++it) { const QMap periods = (*it).getPeriods(); QMap::const_iterator it_per; (*it).clearPeriods(); for (it_per = periods.begin(); it_per != periods.end(); ++it_per) { PeriodGroup pgroup = (*it_per); pgroup.setStartDate(pgroup.startDate().addMonths(adjust)); (*it).addPeriod(pgroup.startDate(), pgroup); } } } } diff --git a/kmymoney/mymoney/mymoneybudget.h b/kmymoney/mymoney/mymoneybudget.h index 3caf61f7a..c5fdde85b 100644 --- a/kmymoney/mymoney/mymoneybudget.h +++ b/kmymoney/mymoney/mymoneybudget.h @@ -1,306 +1,298 @@ /* * Copyright 2006 Ace Jones * Copyright 2006 Darren Gould * Copyright 2017 Ł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 MYMONEYBUDGET_H #define MYMONEYBUDGET_H // ---------------------------------------------------------------------------- // QT Includes #include // ---------------------------------------------------------------------------- // Project Includes #include "mymoneyobject.h" #include "kmm_mymoney_export.h" #include "mymoneyunittestable.h" class QString; class QDate; class QDomElement; class QDomDocument; class MyMoneyMoney; template class QList; template class QMap; +namespace eMyMoney { namespace Budget { enum class Level; } } + /** * This class defines a Budget within the MyMoneyEngine. The Budget class * contains all the configuration parameters needed to run a Budget, plus * XML serialization. * * As noted above, this class only provides a Budget DEFINITION. The * generation and presentation of the Budget itself are left to higher * level classes. * * @author Darren Gould */ class MyMoneyBudgetPrivate; class KMM_MYMONEY_EXPORT MyMoneyBudget: public MyMoneyObject { Q_DECLARE_PRIVATE(MyMoneyBudget) KMM_MYMONEY_UNIT_TESTABLE public: MyMoneyBudget(); - explicit MyMoneyBudget(const QString& name); + explicit MyMoneyBudget(const QString &id); + /** * This constructor creates an object based on the data found in the * QDomElement referenced by @p node. If problems arise, the @p id of * the object is cleared (see MyMoneyObject::clearId()). */ explicit MyMoneyBudget(const QDomElement& node); /** * This constructor creates an object based on the data found in the * MyMoneyBudget budget object. */ MyMoneyBudget(const QString& id, const MyMoneyBudget& other); MyMoneyBudget(const MyMoneyBudget& other); MyMoneyBudget(MyMoneyBudget && other); MyMoneyBudget & operator=(MyMoneyBudget other); friend void swap(MyMoneyBudget& first, MyMoneyBudget& second); ~MyMoneyBudget(); /** * Helper class for MyMoneyBudget * * This is an abstraction of the PERIOD stored in the BUDGET/ACCOUNT tag in XML * * @author Darren Gould */ class PeriodGroupPrivate; class KMM_MYMONEY_EXPORT PeriodGroup { Q_DECLARE_PRIVATE(PeriodGroup) PeriodGroupPrivate* d_ptr; public: PeriodGroup(); PeriodGroup(const PeriodGroup & other); PeriodGroup(PeriodGroup && other); PeriodGroup & operator=(PeriodGroup other); friend void swap(PeriodGroup& first, PeriodGroup& second); ~PeriodGroup(); QDate startDate() const; void setStartDate(const QDate& start); MyMoneyMoney amount() const; void setAmount(const MyMoneyMoney& amount); bool operator == (const PeriodGroup &right) const; }; /** * Helper class for MyMoneyBudget * * This is an abstraction of the Account Data stored in the BUDGET tag in XML * * @author Darren Gould */ class AccountGroupPrivate; class KMM_MYMONEY_EXPORT AccountGroup { Q_DECLARE_PRIVATE(AccountGroup) AccountGroupPrivate* d_ptr; - public: - typedef enum { - eNone = 0, - eMonthly, - eMonthByMonth, - eYearly, - eMax - } eBudgetLevel; - - static const QStringList kBudgetLevelText; - public: AccountGroup(); AccountGroup(const AccountGroup & other); AccountGroup(AccountGroup && other); AccountGroup & operator=(AccountGroup other); friend void swap(AccountGroup& first, AccountGroup& second); ~AccountGroup(); QString id() const; void setId(const QString& id); bool budgetSubaccounts() const; void setBudgetSubaccounts(bool budgetsubaccounts); - eBudgetLevel budgetLevel() const; - void setBudgetLevel(eBudgetLevel level); + eMyMoney::Budget::Level budgetLevel() const; + void setBudgetLevel(eMyMoney::Budget::Level level); PeriodGroup period(const QDate& date) const; void addPeriod(const QDate& date, PeriodGroup& period); const QMap getPeriods() const; void clearPeriods(); MyMoneyMoney balance() const; MyMoneyMoney totalBalance() const; // This member adds the value of another account group // m_budgetlevel is adjusted to the larger one of both // m_budgetsubaccounts remains unaffected AccountGroup operator += (const AccountGroup& right); bool operator == (const AccountGroup &right) const; bool isZero() const; protected: void convertToMonthly(); void convertToYearly(); void convertToMonthByMonth(); }; /** * This operator tests for equality of two MyMoneyBudget objects */ bool operator == (const MyMoneyBudget &) const; QString name() const; void setName(const QString& name); QDate budgetStart() const; void setBudgetStart(const QDate& start); const AccountGroup & account(const QString &id) const; void setAccount(const AccountGroup& account, const QString &id); bool contains(const QString &id) const; QList getaccounts() const; /** * This method writes this Budget to the DOM element @p e, * within the DOM document @p doc. * * @param e The element which should be populated with info from this Budget * @param doc The document which we can use to create new sub-elements * if needed */ void write(QDomElement& e, QDomDocument *doc) const; /** * This method reads a Budget from the DOM element @p e, and * populates this Budget with the results. * * @param e The element from which the Budget should be read * * @return bool True if a Budget was successfully loaded from the * element @p e. If false is returned, the contents of this Budget * object are undefined. */ bool read(const QDomElement& e); /** * This method creates a QDomElement for the @p document * under the parent node @p parent. (This version overwrites the * MMObject base class.) * * @param document reference to QDomDocument * @param parent reference to QDomElement parent node */ void writeXML(QDomDocument& document, QDomElement& parent) const override; /** * This method checks if a reference to the given object exists. It returns, * a @p true if the object is referencing the one requested by the * parameter @p id and the balance() returned is zero. * If it does not, this method returns @p false. * * @param id id of the object to be checked for references * @retval true This object references object with id @p id. * @retval false This object does not reference the object with id @p id. */ bool hasReferenceTo(const QString& id) const override; /** * This member removes all references to object identified by @p id. Used * to remove objects which are about to be removed from the engine. */ void removeReference(const QString& id); }; inline void swap(MyMoneyBudget::PeriodGroup& first, MyMoneyBudget::PeriodGroup& second) // krazy:exclude=inline { using std::swap; swap(first.d_ptr, second.d_ptr); } inline MyMoneyBudget::PeriodGroup::PeriodGroup(MyMoneyBudget::PeriodGroup && other) : PeriodGroup() // krazy:exclude=inline { swap(*this, other); } inline MyMoneyBudget::PeriodGroup & MyMoneyBudget::PeriodGroup::operator=(MyMoneyBudget::PeriodGroup other) // krazy:exclude=inline { swap(*this, other); return *this; } inline void swap(MyMoneyBudget::AccountGroup& first, MyMoneyBudget::AccountGroup& second) // krazy:exclude=inline { using std::swap; swap(first.d_ptr, second.d_ptr); } inline MyMoneyBudget::AccountGroup::AccountGroup(MyMoneyBudget::AccountGroup && other) : AccountGroup() // krazy:exclude=inline { swap(*this, other); } inline MyMoneyBudget::AccountGroup & MyMoneyBudget::AccountGroup::operator=(MyMoneyBudget::AccountGroup other) // krazy:exclude=inline { swap(*this, other); return *this; } inline void swap(MyMoneyBudget& first, MyMoneyBudget& second) // krazy:exclude=inline { using std::swap; swap(first.d_ptr, second.d_ptr); } inline MyMoneyBudget::MyMoneyBudget(MyMoneyBudget && other) : MyMoneyBudget() // krazy:exclude=inline { swap(*this, other); } inline MyMoneyBudget & MyMoneyBudget::operator=(MyMoneyBudget other) // krazy:exclude=inline { swap(*this, other); return *this; } /** * Make it possible to hold @ref MyMoneyBudget objects inside @ref QVariant objects. */ Q_DECLARE_METATYPE(MyMoneyBudget) #endif // MYMONEYBudget_H diff --git a/kmymoney/mymoney/mymoneybudget_p.h b/kmymoney/mymoney/mymoneybudget_p.h index fb7a7799f..5c03d94b0 100644 --- a/kmymoney/mymoney/mymoneybudget_p.h +++ b/kmymoney/mymoney/mymoneybudget_p.h @@ -1,105 +1,128 @@ /* * Copyright 2006 Ace Jones * Copyright 2006 Darren Gould * Copyright 2017 Ł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 MYMONEYBUDGET_P_H #define MYMONEYBUDGET_P_H // ---------------------------------------------------------------------------- // QT Includes #include #include #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes #include "mymoneyobject_p.h" #include "mymoneymoney.h" +#include "mymoneyenums.h" namespace Budget { enum class Element { Budget = 0, Account, Period }; uint qHash(const Element key, uint seed) { return ::qHash(static_cast(key), seed); } enum class Attribute { ID = 0, Name, Start, Version, BudgetLevel, BudgetSubAccounts, Amount, // insert new entries above this line LastAttribute }; uint qHash(const Attribute key, uint seed) { return ::qHash(static_cast(key), seed); } } class MyMoneyBudgetPrivate : public MyMoneyObjectPrivate { public: static QString getElName(const Budget::Element el) { static const QMap elNames { {Budget::Element::Budget, "BUDGET"}, {Budget::Element::Account, "ACCOUNT"}, {Budget::Element::Period, "PERIOD"} }; return elNames[el]; } static QString getAttrName(const Budget::Attribute attr) { static const QHash attrNames { {Budget::Attribute::ID, QStringLiteral("id")}, {Budget::Attribute::Name, QStringLiteral("name")}, {Budget::Attribute::Start, QStringLiteral("start")}, {Budget::Attribute::Version, QStringLiteral("version")}, {Budget::Attribute::BudgetLevel, QStringLiteral("budgetlevel")}, {Budget::Attribute::BudgetSubAccounts, QStringLiteral("budgetsubaccounts")}, {Budget::Attribute::Amount, QStringLiteral("amount")} }; return attrNames[attr]; } + static QHash budgetLevelLUT() + { + static const QHash lut { + {eMyMoney::Budget::Level::None, QStringLiteral("none")}, + {eMyMoney::Budget::Level::Monthly, QStringLiteral("monthly")}, + {eMyMoney::Budget::Level::MonthByMonth, QStringLiteral("monthbymonth")}, + {eMyMoney::Budget::Level::Yearly, QStringLiteral("yearly")}, + {eMyMoney::Budget::Level::Max, QStringLiteral("invalid")}, + }; + return lut; + } + + static QString budgetNames(eMyMoney::Budget::Level textID) + { + return budgetLevelLUT().value(textID); + } + + static eMyMoney::Budget::Level stringToBudgetLevel(const QString &text) + { + return budgetLevelLUT().key(text, eMyMoney::Budget::Level::Max); + } + /** * The user-assigned name of the Budget */ QString m_name; /** * The user-assigned year of the Budget */ QDate m_start; /** * Map the budgeted accounts * * Each account Id is stored against the AccountGroup information */ QMap m_accounts; }; #endif diff --git a/kmymoney/mymoney/mymoneycostcenter.cpp b/kmymoney/mymoney/mymoneycostcenter.cpp index 59e8c9877..94234c1a0 100644 --- a/kmymoney/mymoney/mymoneycostcenter.cpp +++ b/kmymoney/mymoney/mymoneycostcenter.cpp @@ -1,140 +1,138 @@ /* * Copyright 2012-2016 Thomas Baumgart * * 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 "mymoneycostcenter.h" // ---------------------------------------------------------------------------- // QT Includes #include #include // ---------------------------------------------------------------------------- // Project Includes #include "mymoneyobject_p.h" #include "mymoneyexception.h" #include "mymoneystoragenames.h" using namespace MyMoneyStorageNodes; MyMoneyCostCenter MyMoneyCostCenter::null; class MyMoneyCostCenterPrivate : public MyMoneyObjectPrivate { public: QString m_name; }; MyMoneyCostCenter::MyMoneyCostCenter() : MyMoneyObject(*new MyMoneyCostCenterPrivate) { } -MyMoneyCostCenter::MyMoneyCostCenter(const QString& name) : - MyMoneyObject(*new MyMoneyCostCenterPrivate) +MyMoneyCostCenter::MyMoneyCostCenter(const QString &id) : + MyMoneyObject(*new MyMoneyCostCenterPrivate, id) { - Q_D(MyMoneyCostCenter); - d->m_name = name; } MyMoneyCostCenter::MyMoneyCostCenter(const QDomElement& node) : MyMoneyObject(*new MyMoneyCostCenterPrivate, node) { if (nodeNames[nnCostCenter] != node.tagName()) throw MYMONEYEXCEPTION_CSTRING("Node was not COSTCENTER"); Q_D(MyMoneyCostCenter); d->m_name = node.attribute(getAttrName(Attribute::Name)); } MyMoneyCostCenter::MyMoneyCostCenter(const MyMoneyCostCenter& other) : MyMoneyObject(*new MyMoneyCostCenterPrivate(*other.d_func()), other.id()) { } MyMoneyCostCenter::MyMoneyCostCenter(const QString& id, const MyMoneyCostCenter& other) : MyMoneyObject(*new MyMoneyCostCenterPrivate(*other.d_func()), id) { } MyMoneyCostCenter::~MyMoneyCostCenter() { } bool MyMoneyCostCenter::operator == (const MyMoneyCostCenter& right) const { Q_D(const MyMoneyCostCenter); auto d2 = static_cast(right.d_func()); return (MyMoneyObject::operator==(right) && ((d->m_name.length() == 0 && d2->m_name.length() == 0) || (d->m_name == d2->m_name))); } bool MyMoneyCostCenter::operator < (const MyMoneyCostCenter& right) const { Q_D(const MyMoneyCostCenter); auto d2 = static_cast(right.d_func()); QCollator col; return col.compare(d->m_name, d2->m_name); } void MyMoneyCostCenter::writeXML(QDomDocument& document, QDomElement& parent) const { auto el = document.createElement(nodeNames[nnCostCenter]); Q_D(const MyMoneyCostCenter); d->writeBaseXML(document, el); el.setAttribute(getAttrName(Attribute::Name), d->m_name); parent.appendChild(el); } bool MyMoneyCostCenter::hasReferenceTo(const QString& /*id*/) const { return false; } QString MyMoneyCostCenter::name() const { Q_D(const MyMoneyCostCenter); return d->m_name; } void MyMoneyCostCenter::setName(const QString& val) { Q_D(MyMoneyCostCenter); d->m_name = val; } QString MyMoneyCostCenter::shortName() const { Q_D(const MyMoneyCostCenter); QRegExp shortNumberExp("^(\\d+)\\s.+"); if(shortNumberExp.exactMatch(d->m_name)) { return shortNumberExp.cap(1); } return d->m_name; } QString MyMoneyCostCenter::getAttrName(const Attribute attr) { static const QMap attrNames = { {Attribute::Name, QStringLiteral("name")}, }; return attrNames[attr]; } diff --git a/kmymoney/mymoney/mymoneycostcenter.h b/kmymoney/mymoney/mymoneycostcenter.h index 1da937110..b08ce17cc 100644 --- a/kmymoney/mymoney/mymoneycostcenter.h +++ b/kmymoney/mymoney/mymoneycostcenter.h @@ -1,133 +1,133 @@ /* * Copyright 2012-2016 Thomas Baumgart * * 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 MYMONEYCOSTCENTER_H #define MYMONEYCOSTCENTER_H #include "kmm_mymoney_export.h" // ---------------------------------------------------------------------------- // QT Includes #include #include // ---------------------------------------------------------------------------- // Project Includes #include "mymoneyobject.h" /** * This class represents a tag within the MyMoney engine. */ class MyMoneyCostCenterPrivate; class KMM_MYMONEY_EXPORT MyMoneyCostCenter : public MyMoneyObject { Q_DECLARE_PRIVATE(MyMoneyCostCenter) KMM_MYMONEY_UNIT_TESTABLE public: MyMoneyCostCenter(); - explicit MyMoneyCostCenter(const QString& name); + explicit MyMoneyCostCenter(const QString &id); /** * This is the constructor for a tag that is described by a * QDomElement (e.g. from a file). * * @param el const reference to the QDomElement from which to * create the object */ explicit MyMoneyCostCenter(const QDomElement& el); MyMoneyCostCenter(const QString& id, const MyMoneyCostCenter& other); MyMoneyCostCenter(const MyMoneyCostCenter & other); MyMoneyCostCenter(MyMoneyCostCenter && other); MyMoneyCostCenter & operator=(MyMoneyCostCenter other); friend void swap(MyMoneyCostCenter& first, MyMoneyCostCenter& second); ~MyMoneyCostCenter(); QString name() const; void setName(const QString& val); /** * This member returns a possible number leading the name. If there * is no number infront of the name, then the full name will be returned * @sa name() */ QString shortName() const; // Equality operator bool operator == (const MyMoneyCostCenter &) const; bool operator <(const MyMoneyCostCenter& right) const; void writeXML(QDomDocument& document, QDomElement& parent) const override; /** * This method checks if a reference to the given object exists. It returns, * a @p true if the object is referencing the one requested by the * parameter @p id. If it does not, this method returns @p false. * * @param id id of the object to be checked for references * @retval true This object references object with id @p id. * @retval false This object does not reference the object with id @p id. */ bool hasReferenceTo(const QString& id) const override; static MyMoneyCostCenter null; private: enum class Attribute { Name }; static QString getAttrName(const Attribute attr); friend uint qHash(const Attribute, uint seed); }; inline uint qHash(const MyMoneyCostCenter::Attribute key, uint seed) { return ::qHash(static_cast(key), seed); } // krazy:exclude=inline inline void swap(MyMoneyCostCenter& first, MyMoneyCostCenter& second) // krazy:exclude=inline { using std::swap; swap(first.d_ptr, second.d_ptr); } inline MyMoneyCostCenter::MyMoneyCostCenter(MyMoneyCostCenter && other) : MyMoneyCostCenter() // krazy:exclude=inline { swap(*this, other); } inline MyMoneyCostCenter & MyMoneyCostCenter::operator=(MyMoneyCostCenter other) // krazy:exclude=inline { swap(*this, other); return *this; } //inline bool operator==(const MyMoneyCostCenter& lhs, const QString& rhs) //{ // return lhs.id() == rhs; //} /** * Make it possible to hold @ref MyMoneyCostCenter objects inside @ref QVariant objects. */ Q_DECLARE_METATYPE(MyMoneyCostCenter) #endif diff --git a/kmymoney/mymoney/mymoneyenums.h b/kmymoney/mymoney/mymoneyenums.h index d4f253257..c195ed943 100644 --- a/kmymoney/mymoney/mymoneyenums.h +++ b/kmymoney/mymoney/mymoneyenums.h @@ -1,355 +1,410 @@ /* * 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 MYMONEYENUMS_H #define MYMONEYENUMS_H #include namespace eMyMoney { /** * Account types currently supported. */ namespace Account { enum class Type { Unknown = 0, /**< For error handling */ Checkings, /**< Standard checking account */ Savings, /**< Typical savings account */ Cash, /**< Denotes a shoe-box or pillowcase stuffed with cash */ CreditCard, /**< Credit card accounts */ Loan, /**< Loan and mortgage accounts (liability) */ CertificateDep, /**< Certificates of Deposit */ Investment, /**< Investment account */ MoneyMarket, /**< Money Market Account */ Asset, /**< Denotes a generic asset account.*/ Liability, /**< Denotes a generic liability account.*/ Currency, /**< Denotes a currency trading account. */ Income, /**< Denotes an income account */ Expense, /**< Denotes an expense account */ AssetLoan, /**< Denotes a loan (asset of the owner of this object) */ Stock, /**< Denotes an security account as sub-account for an investment */ Equity, /**< Denotes an equity account e.g. opening/closeing balance */ /* insert new account types above this line */ MaxAccountTypes /**< Denotes the number of different account types */ }; inline uint qHash(const Type key, uint seed) { return ::qHash(static_cast(key), seed); } } + namespace Payee { + enum class MatchType { + Disabled = 0, + Name, + Key, + NameExact + }; + } + namespace Security { enum class Type { Stock, MutualFund, Bond, Currency, None }; inline uint qHash(const Type key, uint seed) { return ::qHash(static_cast(key), seed); } } + namespace Report { + enum class RowType { NoRows = 0, AssetLiability, ExpenseIncome, Category, TopCategory, Account, Tag, Payee, Month, Week, TopAccount, AccountByTopAccount, EquityType, AccountType, Institution, Budget, BudgetActual, Schedule, AccountInfo, AccountLoanInfo, AccountReconcile, CashFlow, Invalid }; + inline uint qHash(const RowType key, uint seed) { return ::qHash(static_cast(key), seed); } + + enum class ColumnType { NoColumns = 0, Days = 1, Months = 1, BiMonths = 2, Quarters = 3, Weeks = 7, Years = 12, Invalid }; + inline uint qHash(const ColumnType key, uint seed) { return ::qHash(static_cast(key), seed); } + + enum class ReportType { NoReport = 0, PivotTable, QueryTable, InfoTable, Invalid }; + // if you add bits to this bitmask, start with the value currently assigned to QCend and update its value afterwards + // also don't forget to add column names to kQueryColumnsText in mymoneyreport.cpp + enum QueryColumn : int { None = 0x0, Begin = 0x1, Number = 0x1, Payee = 0x2, Category = 0x4, Tag = 0x8, Memo = 0x10, Account = 0x20, Reconciled = 0x40, Action = 0x80, Shares = 0x100, Price = 0x200, Performance = 0x400, Loan = 0x800, Balance = 0x1000, CapitalGain = 0x2000, End = 0x4000 }; + + enum class DetailLevel { None = 0, All, Top, Group, Total, End }; + inline uint qHash(const DetailLevel key, uint seed) { return ::qHash(static_cast(key), seed); } + + enum class InvestmentSum { Period = 0, OwnedAndSold, Owned, Sold, Bought}; + enum class ChartType { None = 0, Line, Bar, Pie, Ring, StackedBar, End }; + inline uint qHash(const ChartType key, uint seed) { return ::qHash(static_cast(key), seed); } + + enum class DataLock { Automatic = 0, UserDefined, DataOptionCount }; + inline uint qHash(const DataLock key, uint seed) { return ::qHash(static_cast(key), seed); } + } + namespace Schedule { /** * This enum is used to describe all the possible schedule frequencies. * The special entry, Any, is used to combine all the other types. */ enum class Occurrence { Any = 0, Once = 1, Daily = 2, Weekly = 4, Fortnightly = 8, EveryOtherWeek = 16, EveryHalfMonth = 18, EveryThreeWeeks = 20, EveryThirtyDays = 30, Monthly = 32, EveryFourWeeks = 64, EveryEightWeeks = 126, EveryOtherMonth = 128, EveryThreeMonths = 256, TwiceYearly = 1024, EveryOtherYear = 2048, Quarterly = 4096, EveryFourMonths = 8192, Yearly = 16384 }; /** * This enum is used to describe the schedule type. */ enum class Type { Any = 0, Bill = 1, Deposit = 2, Transfer = 4, LoanPayment = 5 }; /** * This enum is used to describe the schedule's payment type. */ enum class PaymentType { Any = 0, DirectDebit = 1, DirectDeposit = 2, ManualDeposit = 4, Other = 8, WriteChecque = 16, StandingOrder = 32, BankTransfer = 64 }; /** * This enum is used by the auto-commit functionality. * * Depending upon the value of m_weekendOption the schedule can * be entered on a different date **/ enum class WeekendOption { MoveBefore = 0, MoveAfter = 1, MoveNothing = 2 }; } + namespace Budget { + enum class Level { + None = 0, + Monthly, + MonthByMonth, + Yearly, + Max + }; + + inline uint qHash(const Level key, uint seed) { return ::qHash(static_cast(key), seed); } + } + namespace TransactionFilter { // Make sure to keep the following enum valus in sync with the values // used by the GUI (for KMyMoney in kfindtransactiondlgdecl.ui) enum class Type { All = 0, Payments, Deposits, Transfers, // insert new constants above of this line LastType }; // Make sure to keep the following enum valus in sync with the values // used by the GUI (for KMyMoney in kfindtransactiondlgdecl.ui) enum class State { All = 0, NotReconciled, Cleared, Reconciled, Frozen, // insert new constants above of this line LastState }; // Make sure to keep the following enum valus in sync with the values // used by the GUI (for KMyMoney in kfindtransactiondlgdecl.ui) enum class Validity { Any = 0, Valid, Invalid, // insert new constants above of this line LastValidity }; // Make sure to keep the following enum valus in sync with the values // used by the GUI (for KMyMoney in kfindtransactiondlgdecl.ui) enum class Date { All = 0, AsOfToday, CurrentMonth, CurrentYear, MonthToDate, YearToDate, YearToMonth, LastMonth, LastYear, Last7Days, Last30Days, Last3Months, Last6Months, Last12Months, Next7Days, Next30Days, Next3Months, Next6Months, Next12Months, UserDefined, Last3ToNext3Months, Last11Months, CurrentQuarter, LastQuarter, NextQuarter, CurrentFiscalYear, LastFiscalYear, Today, Next18Months, // insert new constants above of this line LastDateItem }; } namespace Split { /** * This enum defines the possible reconciliation states a split * can be in. Possible values are as follows: * * @li NotReconciled * @li Cleared * @li Reconciled * @li Frozen * * Whenever a new split is created, it has the status NotReconciled. It * can be set to cleared when the transaction has been performed. Once the * account is reconciled, cleared splits will be set to Reconciled. The * state Frozen will be used, when the concept of books is introduced into * the engine and a split must not be changed anymore. */ enum class State { Unknown = -1, NotReconciled = 0, Cleared, Reconciled, Frozen, // insert new values above MaxReconcileState }; enum class InvestmentTransactionType { UnknownTransactionType = -1, BuyShares = 0, SellShares, Dividend, ReinvestDividend, Yield, AddShares, RemoveShares, SplitShares, InterestIncome/// }; inline uint qHash(const InvestmentTransactionType key, uint seed) { return ::qHash(static_cast(key), seed); } enum class Action { Check, Deposit, Transfer, Withdrawal, ATM, Amortization, Interest, BuyShares, Dividend, ReinvestDividend, Yield, AddShares, SplitShares, InterestIncome }; inline uint qHash(const Action key, uint seed) { return ::qHash(static_cast(key), seed); } } namespace File { /** * notificationObject identifies the type of the object * for which this notification is stored */ enum class Object { Account = 1, Institution, Payee, Transaction, Tag, Schedule, Security, OnlineJob, CostCenter }; /** * notificationMode identifies the type of notifiation * (add, modify, remove) */ enum class Mode { Add = 1, Modify, Remove }; } /** * @brief Type of message * * An usually it is not easy to categorise log messages. This description is only a hint. */ namespace OnlineJob { enum class MessageType { Debug, /**< Just for debug purposes. In normal scenarios the user should not see this. No need to store this message. Plugins should not create them at all if debug mode is not enabled. */ Log, /**< A piece of information the user should not see during normal operation. It is not shown in any UI by default. It is stored persistantly. */ Information, /**< Information that should be kept but without the need to burden the user. The user can see this during normal operation. */ Warning, /**< A piece of information the user should see but not be enforced to do so (= no modal dialog). E.g. a task is expected to have direct effect but insted you have to wait a day (and that is commen behavior). */ Error /**< Important for the user - he must be warned. E.g. a task could unexpectedly not be executed */ }; + + /** + * @brief The state of a job given by the onlinePlugin + */ + enum sendingState { + noBankAnswer, /**< Used during or before sending or if sendDate().isValid() the job was successfully sent */ + acceptedByBank, /**< bank definetly confirmed the job */ + rejectedByBank, /**< bank definetly rejected this job */ + abortedByUser, /**< aborted by user during sending */ + sendingError /**< an error occurred, the job is certainly not executed by the bank */ + }; } namespace Statement { enum class Type { None = 0, Checkings, Savings, Investment, CreditCard, Invalid }; inline uint qHash(const Type key, uint seed) { return ::qHash(static_cast(key), seed); } } namespace Transaction { // the following members are only used for investment accounts (m_eType==etInvestment) // eaNone means the action, shares, and security can be ignored. enum class Action { None = 0, Buy, Sell, ReinvestDividend, CashDividend, Shrsin, Shrsout, Stksplit, Fees, Interest, Invalid }; inline uint qHash(const Action key, uint seed) { return ::qHash(static_cast(key), seed); } } namespace Money { enum signPosition : int { // keep those in sync with the ones defined in klocale.h ParensAround = 0, BeforeQuantityMoney = 1, AfterQuantityMoney = 2, BeforeMoney = 3, AfterMoney = 4 }; } } #endif diff --git a/kmymoney/mymoney/mymoneyforecast.cpp b/kmymoney/mymoney/mymoneyforecast.cpp index 3d7b9df2a..f6f18de5b 100644 --- a/kmymoney/mymoney/mymoneyforecast.cpp +++ b/kmymoney/mymoney/mymoneyforecast.cpp @@ -1,1710 +1,1710 @@ /* * Copyright 2007-2010 Alvaro Soliverez * 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 "mymoneyforecast.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes #include "mymoneyfile.h" #include "mymoneyaccount.h" #include "mymoneyaccountloan.h" #include "mymoneysecurity.h" #include "mymoneybudget.h" #include "mymoneyschedule.h" #include "mymoneyprice.h" #include "mymoneymoney.h" #include "mymoneysplit.h" #include "mymoneytransaction.h" #include "mymoneytransactionfilter.h" #include "mymoneyfinancialcalculator.h" #include "mymoneyexception.h" #include "mymoneyenums.h" enum class eForecastMethod {Scheduled = 0, Historic = 1 }; /** * daily balances of an account */ typedef QMap dailyBalances; /** * map of trends of an account */ typedef QMap trendBalances; class MyMoneyForecastPrivate { Q_DECLARE_PUBLIC(MyMoneyForecast) public: explicit MyMoneyForecastPrivate(MyMoneyForecast *qq) : q_ptr(qq), m_accountsCycle(30), m_forecastCycles(3), m_forecastDays(90), m_beginForecastDay(0), m_forecastMethod(eForecastMethod::Scheduled), m_historyMethod(1), m_skipOpeningDate(true), m_includeUnusedAccounts(false), m_forecastDone(false), m_includeFutureTransactions(true), m_includeScheduledTransactions(true) { } eForecastMethod forecastMethod() const { return m_forecastMethod; } /** * Returns the list of accounts to create a budget. Only Income and Expenses are returned. */ QList budgetAccountList() { auto file = MyMoneyFile::instance(); QList accList; QStringList emptyStringList; //Get all accounts from the file and check if they are of the right type to calculate forecast file->accountList(accList, emptyStringList, false); QList::iterator accList_t = accList.begin(); for (; accList_t != accList.end();) { auto acc = *accList_t; if (acc.isClosed() //check the account is not closed || (!acc.isIncomeExpense())) { //remove the account if it is not of the correct type accList_t = accList.erase(accList_t); } else { ++accList_t; } } return accList; } /** * calculate daily forecast balance based on historic transactions */ void calculateHistoricDailyBalances() { Q_Q(MyMoneyForecast); auto file = MyMoneyFile::instance(); calculateAccountTrendList(); //Calculate account daily balances QSet::ConstIterator it_n; for (it_n = m_forecastAccounts.constBegin(); it_n != m_forecastAccounts.constEnd(); ++it_n) { auto acc = file->account(*it_n); //set the starting balance of the account setStartingBalance(acc); switch (q->historyMethod()) { case 0: case 1: { for (QDate f_day = q->forecastStartDate(); f_day <= q->forecastEndDate();) { for (auto t_day = 1; t_day <= q->accountsCycle(); ++t_day) { MyMoneyMoney balanceDayBefore = m_accountList[acc.id()][(f_day.addDays(-1))];//balance of the day before MyMoneyMoney accountDailyTrend = m_accountTrendList[acc.id()][t_day]; //trend for that day //balance of the day is the balance of the day before multiplied by the trend for the day m_accountList[acc.id()][f_day] = balanceDayBefore; m_accountList[acc.id()][f_day] += accountDailyTrend; //movement trend for that particular day m_accountList[acc.id()][f_day] = m_accountList[acc.id()][f_day].convert(acc.fraction()); //m_accountList[acc.id()][f_day] += m_accountListPast[acc.id()][f_day.addDays(-q->historyDays())]; f_day = f_day.addDays(1); } } } break; case 2: { QDate baseDate = QDate::currentDate().addDays(-q->accountsCycle()); for (auto t_day = 1; t_day <= q->accountsCycle(); ++t_day) { auto f_day = 1; QDate fDate = baseDate.addDays(q->accountsCycle() + 1); while (fDate <= q->forecastEndDate()) { //the calculation is based on the balance for the last month, that is then multiplied by the trend m_accountList[acc.id()][fDate] = m_accountListPast[acc.id()][baseDate] + (m_accountTrendList[acc.id()][t_day] * MyMoneyMoney(f_day, 1)); m_accountList[acc.id()][fDate] = m_accountList[acc.id()][fDate].convert(acc.fraction()); ++f_day; fDate = baseDate.addDays(q->accountsCycle() * f_day); } baseDate = baseDate.addDays(1); } } } } } /** * calculate monthly budget balance based on historic transactions */ void calculateHistoricMonthlyBalances() { Q_Q(MyMoneyForecast); auto file = MyMoneyFile::instance(); //Calculate account monthly balances QSet::ConstIterator it_n; for (it_n = m_forecastAccounts.constBegin(); it_n != m_forecastAccounts.constEnd(); ++it_n) { auto acc = file->account(*it_n); for (QDate f_date = q->forecastStartDate(); f_date <= q->forecastEndDate();) { for (auto f_day = 1; f_day <= q->accountsCycle() && f_date <= q->forecastEndDate(); ++f_day) { MyMoneyMoney accountDailyTrend = m_accountTrendList[acc.id()][f_day]; //trend for that day //check for leap year if (f_date.month() == 2 && f_date.day() == 29) f_date = f_date.addDays(1); //skip 1 day m_accountList[acc.id()][QDate(f_date.year(), f_date.month(), 1)] += accountDailyTrend; //movement trend for that particular day f_date = f_date.addDays(1); } } } } /** * calculate monthly budget balance based on historic transactions */ void calculateScheduledMonthlyBalances() { Q_Q(MyMoneyForecast); auto file = MyMoneyFile::instance(); //Calculate account monthly balances QSet::ConstIterator it_n; for (it_n = m_forecastAccounts.constBegin(); it_n != m_forecastAccounts.constEnd(); ++it_n) { auto acc = file->account(*it_n); for (QDate f_date = q->forecastStartDate(); f_date <= q->forecastEndDate(); f_date = f_date.addDays(1)) { //get the trend for the day MyMoneyMoney accountDailyBalance = m_accountList[acc.id()][f_date]; //do not add if it is the beginning of the month //otherwise we end up with duplicated values as reported by Marko Käning if (f_date != QDate(f_date.year(), f_date.month(), 1)) m_accountList[acc.id()][QDate(f_date.year(), f_date.month(), 1)] += accountDailyBalance; } } } /** * calculate forecast based on future and scheduled transactions */ void doFutureScheduledForecast() { Q_Q(MyMoneyForecast); auto file = MyMoneyFile::instance(); if (q->isIncludingFutureTransactions()) addFutureTransactions(); if (q->isIncludingScheduledTransactions()) addScheduledTransactions(); //do not show accounts with no transactions if (!q->isIncludingUnusedAccounts()) purgeForecastAccountsList(m_accountList); //adjust value of investments to deep currency QSet::ConstIterator it_n; for (it_n = m_forecastAccounts.constBegin(); it_n != m_forecastAccounts.constEnd(); ++it_n) { auto acc = file->account(*it_n); if (acc.isInvest()) { //get the id of the security for that account MyMoneySecurity undersecurity = file->security(acc.currencyId()); //only do it if the security is not an actual currency if (! undersecurity.isCurrency()) { //set the default value MyMoneyMoney rate = MyMoneyMoney::ONE; for (QDate it_day = QDate::currentDate(); it_day <= q->forecastEndDate();) { //get the price for the tradingCurrency that day const MyMoneyPrice &price = file->price(undersecurity.id(), undersecurity.tradingCurrency(), it_day); if (price.isValid()) { rate = price.rate(undersecurity.tradingCurrency()); } //value is the amount of shares multiplied by the rate of the deep currency m_accountList[acc.id()][it_day] = m_accountList[acc.id()][it_day] * rate; it_day = it_day.addDays(1); } } } } } /** * add future transactions to forecast */ void addFutureTransactions() { Q_Q(MyMoneyForecast); MyMoneyTransactionFilter filter; auto file = MyMoneyFile::instance(); // collect and process all transactions that have already been entered but // are located in the future. filter.setDateFilter(q->forecastStartDate(), q->forecastEndDate()); filter.setReportAllSplits(false); foreach (const auto transaction, file->transactionList(filter)) { foreach (const auto split, transaction.splits()) { if (!split.shares().isZero()) { auto acc = file->account(split.accountId()); if (q->isForecastAccount(acc)) { dailyBalances balance; balance = m_accountList[acc.id()]; //if it is income, the balance is stored as negative number if (acc.accountType() == eMyMoney::Account::Type::Income) { balance[transaction.postDate()] += (split.shares() * MyMoneyMoney::MINUS_ONE); } else { balance[transaction.postDate()] += split.shares(); } m_accountList[acc.id()] = balance; } } } } #if 0 QFile trcFile("forecast.csv"); trcFile.open(QIODevice::WriteOnly); QTextStream s(&trcFile); { s << "Already present transactions\n"; QMap::Iterator it_a; QSet::ConstIterator it_n; for (it_n = m_nameIdx.begin(); it_n != m_nameIdx.end(); ++it_n) { auto acc = file->account(*it_n); it_a = m_accountList.find(*it_n); s << "\"" << acc.name() << "\","; for (auto i = 0; i < 90; ++i) { s << "\"" << (*it_a)[i].formatMoney("") << "\","; } s << "\n"; } } #endif } /** * add scheduled transactions to forecast */ void addScheduledTransactions() { Q_Q(MyMoneyForecast); auto file = MyMoneyFile::instance(); // now process all the schedules that may have an impact QList schedule; schedule = file->scheduleList(QString(), eMyMoney::Schedule::Type::Any, eMyMoney::Schedule::Occurrence::Any, eMyMoney::Schedule::PaymentType::Any, QDate(), q->forecastEndDate(), false); if (schedule.count() > 0) { QList::Iterator it; do { qSort(schedule); it = schedule.begin(); if (it == schedule.end()) break; if ((*it).isFinished()) { schedule.erase(it); continue; } QDate date = (*it).nextPayment((*it).lastPayment()); if (!date.isValid()) { schedule.erase(it); continue; } QDate nextDate = (*it).adjustedNextPayment((*it).adjustedDate((*it).lastPayment(), (*it).weekendOption())); if (nextDate > q->forecastEndDate()) { // We're done with this schedule, let's move on to the next schedule.erase(it); continue; } // found the next schedule. process it auto acc = (*it).account(); if (!acc.id().isEmpty()) { try { if (acc.accountType() != eMyMoney::Account::Type::Investment) { auto t = (*it).transaction(); // only process the entry, if it is still active if (!(*it).isFinished() && nextDate != QDate()) { // make sure we have all 'starting balances' so that the autocalc works QMap balanceMap; foreach (const auto split, t.splits()) { auto accountFromSplit = file->account(split.accountId()); if (q->isForecastAccount(accountFromSplit)) { // collect all overdues on the first day QDate forecastDate = nextDate; if (QDate::currentDate() >= nextDate) forecastDate = QDate::currentDate().addDays(1); dailyBalances balance; balance = m_accountList[accountFromSplit.id()]; for (QDate f_day = QDate::currentDate(); f_day < forecastDate;) { balanceMap[accountFromSplit.id()] += m_accountList[accountFromSplit.id()][f_day]; f_day = f_day.addDays(1); } } } // take care of the autoCalc stuff q->calculateAutoLoan(*it, t, balanceMap); // now add the splits to the balances foreach (const auto split, t.splits()) { auto accountFromSplit = file->account(split.accountId()); if (q->isForecastAccount(accountFromSplit)) { dailyBalances balance; balance = m_accountList[accountFromSplit.id()]; //auto offset = QDate::currentDate().daysTo(nextDate); //if(offset <= 0) { // collect all overdues on the first day // offset = 1; //} // collect all overdues on the first day QDate forecastDate = nextDate; if (QDate::currentDate() >= nextDate) forecastDate = QDate::currentDate().addDays(1); if (accountFromSplit.accountType() == eMyMoney::Account::Type::Income) { balance[forecastDate] += (split.shares() * MyMoneyMoney::MINUS_ONE); } else { balance[forecastDate] += split.shares(); } m_accountList[accountFromSplit.id()] = balance; } } } } (*it).setLastPayment(date); } catch (const MyMoneyException &e) { qDebug() << Q_FUNC_INFO << " Schedule " << (*it).id() << " (" << (*it).name() << "): " << e.what(); schedule.erase(it); } } else { // remove schedule from list schedule.erase(it); } } while (1); } #if 0 { s << "\n\nAdded scheduled transactions\n"; QMap::Iterator it_a; QSet::ConstIterator it_n; for (it_n = m_nameIdx.begin(); it_n != m_nameIdx.end(); ++it_n) { auto acc = file->account(*it_n); it_a = m_accountList.find(*it_n); s << "\"" << acc.name() << "\","; for (auto i = 0; i < 90; ++i) { s << "\"" << (*it_a)[i].formatMoney("") << "\","; } s << "\n"; } } #endif } /** * calculate daily forecast balance based on future and scheduled transactions */ void calculateScheduledDailyBalances() { Q_Q(MyMoneyForecast); auto file = MyMoneyFile::instance(); //Calculate account daily balances QSet::ConstIterator it_n; for (it_n = m_forecastAccounts.constBegin(); it_n != m_forecastAccounts.constEnd(); ++it_n) { auto acc = file->account(*it_n); //set the starting balance of the account setStartingBalance(acc); for (QDate f_day = q->forecastStartDate(); f_day <= q->forecastEndDate();) { MyMoneyMoney balanceDayBefore = m_accountList[acc.id()][(f_day.addDays(-1))];//balance of the day before m_accountList[acc.id()][f_day] += balanceDayBefore; //running sum f_day = f_day.addDays(1); } } } /** * set the starting balance for an accounts */ void setStartingBalance(const MyMoneyAccount& acc) { Q_Q(MyMoneyForecast); auto file = MyMoneyFile::instance(); //Get current account balance if (acc.isInvest()) { //investments require special treatment //get the security id of that account MyMoneySecurity undersecurity = file->security(acc.currencyId()); //only do it if the security is not an actual currency if (! undersecurity.isCurrency()) { //set the default value MyMoneyMoney rate = MyMoneyMoney::ONE; //get te const MyMoneyPrice &price = file->price(undersecurity.id(), undersecurity.tradingCurrency(), QDate::currentDate()); if (price.isValid()) { rate = price.rate(undersecurity.tradingCurrency()); } m_accountList[acc.id()][QDate::currentDate()] = file->balance(acc.id(), QDate::currentDate()) * rate; } } else { m_accountList[acc.id()][QDate::currentDate()] = file->balance(acc.id(), QDate::currentDate()); } //if the method is linear regression, we have to add the opening balance to m_accountListPast if (forecastMethod() == eForecastMethod::Historic && q->historyMethod() == 2) { //FIXME workaround for stock opening dates QDate openingDate; if (acc.accountType() == eMyMoney::Account::Type::Stock) { auto parentAccount = file->account(acc.parentAccountId()); openingDate = parentAccount.openingDate(); } else { openingDate = acc.openingDate(); } //add opening balance only if it opened after the history start if (openingDate >= q->historyStartDate()) { MyMoneyMoney openingBalance; openingBalance = file->balance(acc.id(), openingDate); //calculate running sum for (QDate it_date = openingDate; it_date <= q->historyEndDate(); it_date = it_date.addDays(1)) { //investments require special treatment if (acc.isInvest()) { //get the security id of that account MyMoneySecurity undersecurity = file->security(acc.currencyId()); //only do it if the security is not an actual currency if (! undersecurity.isCurrency()) { //set the default value MyMoneyMoney rate = MyMoneyMoney::ONE; //get the rate for that specific date const MyMoneyPrice &price = file->price(undersecurity.id(), undersecurity.tradingCurrency(), it_date); if (price.isValid()) { rate = price.rate(undersecurity.tradingCurrency()); } m_accountListPast[acc.id()][it_date] += openingBalance * rate; } } else { m_accountListPast[acc.id()][it_date] += openingBalance; } } } } } /** * Returns the day moving average for the account @a acc based on the daily balances of a given number of @p forecastTerms * It returns the moving average for a given @p trendDay of the forecastTerm * With a term of 1 month and 3 terms, it calculates the trend taking the transactions occurred * at that day and the day before,for the last 3 months */ MyMoneyMoney accountMovingAverage(const MyMoneyAccount& acc, const qint64 trendDay, const int forecastTerms) { Q_Q(MyMoneyForecast); //Calculate a daily trend for the account based on the accounts of a given number of terms //With a term of 1 month and 3 terms, it calculates the trend taking the transactions occurred at that day and the day before, //for the last 3 months MyMoneyMoney balanceVariation; for (auto it_terms = 0; (trendDay + (q->accountsCycle()*it_terms)) <= q->historyDays(); ++it_terms) { //sum for each term MyMoneyMoney balanceBefore = m_accountListPast[acc.id()][q->historyStartDate().addDays(trendDay+(q->accountsCycle()*it_terms)-2)]; //get balance for the day before MyMoneyMoney balanceAfter = m_accountListPast[acc.id()][q->historyStartDate().addDays(trendDay+(q->accountsCycle()*it_terms)-1)]; balanceVariation += (balanceAfter - balanceBefore); //add the balance variation between days } //calculate average of the variations return (balanceVariation / MyMoneyMoney(forecastTerms, 1)).convert(10000); } /** * Returns the weighted moving average for a given @p trendDay */ MyMoneyMoney accountWeightedMovingAverage(const MyMoneyAccount& acc, const qint64 trendDay, const int totalWeight) { Q_Q(MyMoneyForecast); MyMoneyMoney balanceVariation; for (auto it_terms = 0, weight = 1; (trendDay + (q->accountsCycle()*it_terms)) <= q->historyDays(); ++it_terms, ++weight) { //sum for each term multiplied by weight MyMoneyMoney balanceBefore = m_accountListPast[acc.id()][q->historyStartDate().addDays(trendDay+(q->accountsCycle()*it_terms)-2)]; //get balance for the day before MyMoneyMoney balanceAfter = m_accountListPast[acc.id()][q->historyStartDate().addDays(trendDay+(q->accountsCycle()*it_terms)-1)]; balanceVariation += ((balanceAfter - balanceBefore) * MyMoneyMoney(weight, 1)); //add the balance variation between days multiplied by its weight } //calculate average of the variations return (balanceVariation / MyMoneyMoney(totalWeight, 1)).convert(10000); } /** * Returns the linear regression for a given @p trendDay */ MyMoneyMoney accountLinearRegression(const MyMoneyAccount &acc, const qint64 trendDay, const qint64 actualTerms, const MyMoneyMoney& meanTerms) { Q_Q(MyMoneyForecast); MyMoneyMoney meanBalance, totalBalance, totalTerms; totalTerms = MyMoneyMoney(actualTerms, 1); //calculate mean balance for (auto it_terms = q->forecastCycles() - actualTerms; (trendDay + (q->accountsCycle()*it_terms)) <= q->historyDays(); ++it_terms) { //sum for each term totalBalance += m_accountListPast[acc.id()][q->historyStartDate().addDays(trendDay+(q->accountsCycle()*it_terms)-1)]; } meanBalance = totalBalance / MyMoneyMoney(actualTerms, 1); meanBalance = meanBalance.convert(10000); //calculate b1 //first calculate x - mean x multiplied by y - mean y MyMoneyMoney totalXY, totalSqX; auto term = 1; for (auto it_terms = q->forecastCycles() - actualTerms; (trendDay + (q->accountsCycle()*it_terms)) <= q->historyDays(); ++it_terms, ++term) { //sum for each term MyMoneyMoney balance = m_accountListPast[acc.id()][q->historyStartDate().addDays(trendDay+(q->accountsCycle()*it_terms)-1)]; MyMoneyMoney balMeanBal = balance - meanBalance; MyMoneyMoney termMeanTerm = (MyMoneyMoney(term, 1) - meanTerms); totalXY += (balMeanBal * termMeanTerm).convert(10000); totalSqX += (termMeanTerm * termMeanTerm).convert(10000); } totalXY = (totalXY / MyMoneyMoney(actualTerms, 1)).convert(10000); totalSqX = (totalSqX / MyMoneyMoney(actualTerms, 1)).convert(10000); //check zero if (totalSqX.isZero()) return MyMoneyMoney(); MyMoneyMoney linReg = (totalXY / totalSqX).convert(10000); return linReg; } /** * calculate daily forecast trend based on historic transactions */ void calculateAccountTrendList() { Q_Q(MyMoneyForecast); auto file = MyMoneyFile::instance(); qint64 auxForecastTerms; qint64 totalWeight = 0; //Calculate account trends QSet::ConstIterator it_n; for (it_n = m_forecastAccounts.begin(); it_n != m_forecastAccounts.end(); ++it_n) { auto acc = file->account(*it_n); m_accountTrendList[acc.id()][0] = MyMoneyMoney(); // for today, the trend is 0 auxForecastTerms = q->forecastCycles(); if (q->skipOpeningDate()) { QDate openingDate; if (acc.accountType() == eMyMoney::Account::Type::Stock) { auto parentAccount = file->account(acc.parentAccountId()); openingDate = parentAccount.openingDate(); } else { openingDate = acc.openingDate(); } if (openingDate > q->historyStartDate()) { //if acc opened after forecast period auxForecastTerms = 1 + ((openingDate.daysTo(q->historyEndDate()) + 1) / q->accountsCycle()); // set forecastTerms to a lower value, to calculate only based on how long this account was opened } } switch (q->historyMethod()) { //moving average case 0: { for (auto t_day = 1; t_day <= q->accountsCycle(); t_day++) m_accountTrendList[acc.id()][t_day] = accountMovingAverage(acc, t_day, auxForecastTerms); //moving average break; } //weighted moving average case 1: { //calculate total weight for moving average if (auxForecastTerms == q->forecastCycles()) { totalWeight = (auxForecastTerms * (auxForecastTerms + 1)) / 2; //totalWeight is the triangular number of auxForecastTerms } else { //if only taking a few periods, totalWeight is the sum of the weight for most recent periods auto i = 1; for (qint64 w = q->forecastCycles(); i <= auxForecastTerms; ++i, --w) totalWeight += w; } for (auto t_day = 1; t_day <= q->accountsCycle(); t_day++) m_accountTrendList[acc.id()][t_day] = accountWeightedMovingAverage(acc, t_day, totalWeight); break; } case 2: { //calculate mean term MyMoneyMoney meanTerms = MyMoneyMoney((auxForecastTerms * (auxForecastTerms + 1)) / 2, 1) / MyMoneyMoney(auxForecastTerms, 1); for (auto t_day = 1; t_day <= q->accountsCycle(); t_day++) m_accountTrendList[acc.id()][t_day] = accountLinearRegression(acc, t_day, auxForecastTerms, meanTerms); break; } default: break; } } } /** * set the internal list of accounts to be forecast */ void setForecastAccountList() { Q_Q(MyMoneyForecast); //get forecast accounts QList accList; accList = q->forecastAccountList(); QList::const_iterator accList_t = accList.constBegin(); for (; accList_t != accList.constEnd(); ++accList_t) { m_forecastAccounts.insert((*accList_t).id()); } } /** * set the internal list of accounts to create a budget */ void setBudgetAccountList() { //get budget accounts QList accList; accList = budgetAccountList(); QList::const_iterator accList_t = accList.constBegin(); for (; accList_t != accList.constEnd(); ++accList_t) { m_forecastAccounts.insert((*accList_t).id()); } } /** * get past transactions for the accounts to be forecast */ void pastTransactions() { Q_Q(MyMoneyForecast); auto file = MyMoneyFile::instance(); MyMoneyTransactionFilter filter; filter.setDateFilter(q->historyStartDate(), q->historyEndDate()); filter.setReportAllSplits(false); //Check past transactions foreach (const auto transaction, file->transactionList(filter)) { foreach (const auto split, transaction.splits()) { if (!split.shares().isZero()) { auto acc = file->account(split.accountId()); //workaround for stock accounts which have faulty opening dates QDate openingDate; if (acc.accountType() == eMyMoney::Account::Type::Stock) { auto parentAccount = file->account(acc.parentAccountId()); openingDate = parentAccount.openingDate(); } else { openingDate = acc.openingDate(); } if (q->isForecastAccount(acc) //If it is one of the accounts we are checking, add the amount of the transaction && ((openingDate < transaction.postDate() && q->skipOpeningDate()) || !q->skipOpeningDate())) { //don't take the opening day of the account to calculate balance dailyBalances balance; //FIXME deal with leap years balance = m_accountListPast[acc.id()]; if (acc.accountType() == eMyMoney::Account::Type::Income) {//if it is income, the balance is stored as negative number balance[transaction.postDate()] += (split.shares() * MyMoneyMoney::MINUS_ONE); } else { balance[transaction.postDate()] += split.shares(); } // check if this is a new account for us m_accountListPast[acc.id()] = balance; } } } } //purge those accounts with no transactions on the period if (q->isIncludingUnusedAccounts() == false) purgeForecastAccountsList(m_accountListPast); //calculate running sum QSet::ConstIterator it_n; for (it_n = m_forecastAccounts.begin(); it_n != m_forecastAccounts.end(); ++it_n) { auto acc = file->account(*it_n); m_accountListPast[acc.id()][q->historyStartDate().addDays(-1)] = file->balance(acc.id(), q->historyStartDate().addDays(-1)); for (QDate it_date = q->historyStartDate(); it_date <= q->historyEndDate();) { m_accountListPast[acc.id()][it_date] += m_accountListPast[acc.id()][it_date.addDays(-1)]; //Running sum it_date = it_date.addDays(1); } } //adjust value of investments to deep currency for (it_n = m_forecastAccounts.begin(); it_n != m_forecastAccounts.end(); ++it_n) { auto acc = file->account(*it_n); if (acc.isInvest()) { //get the id of the security for that account MyMoneySecurity undersecurity = file->security(acc.currencyId()); if (! undersecurity.isCurrency()) { //only do it if the security is not an actual currency MyMoneyMoney rate = MyMoneyMoney::ONE; //set the default value for (QDate it_date = q->historyStartDate().addDays(-1) ; it_date <= q->historyEndDate();) { //get the price for the tradingCurrency that day const MyMoneyPrice &price = file->price(undersecurity.id(), undersecurity.tradingCurrency(), it_date); if (price.isValid()) { rate = price.rate(undersecurity.tradingCurrency()); } //value is the amount of shares multiplied by the rate of the deep currency m_accountListPast[acc.id()][it_date] = m_accountListPast[acc.id()][it_date] * rate; it_date = it_date.addDays(1); } } } } } /** * calculate the day to start forecast and sets the begin date * The quantity of forecast days will be counted from this date * Depends on the values of begin day and accounts cycle * The rules to calculate begin day are as follows: * - if beginDay is 0, begin date is current date * - if the day of the month set by beginDay has not passed, that will be used * - if adding an account cycle to beginDay, will not go past the beginDay of next month, * that date will be used, otherwise it will add account cycle to beginDay until it is past current date * It returns the total amount of Forecast Days from current date. */ qint64 calculateBeginForecastDay() { Q_Q(MyMoneyForecast); auto fDays = q->forecastDays(); auto beginDay = q->beginForecastDay(); auto accCycle = q->accountsCycle(); QDate beginDate; //if 0, beginDate is current date and forecastDays remains unchanged if (beginDay == 0) { q->setBeginForecastDate(QDate::currentDate()); return fDays; } //adjust if beginDay more than days of current month if (QDate::currentDate().daysInMonth() < beginDay) beginDay = QDate::currentDate().daysInMonth(); //if beginDay still to come, calculate and return if (QDate::currentDate().day() <= beginDay) { beginDate = QDate(QDate::currentDate().year(), QDate::currentDate().month(), beginDay); fDays += QDate::currentDate().daysTo(beginDate); q->setBeginForecastDate(beginDate); return fDays; } //adjust beginDay for next month if (QDate::currentDate().addMonths(1).daysInMonth() < beginDay) beginDay = QDate::currentDate().addMonths(1).daysInMonth(); //if beginDay of next month comes before 1 interval, use beginDay next month if (QDate::currentDate().addDays(accCycle) >= (QDate(QDate::currentDate().addMonths(1).year(), QDate::currentDate().addMonths(1).month(), 1).addDays(beginDay - 1))) { beginDate = QDate(QDate::currentDate().addMonths(1).year(), QDate::currentDate().addMonths(1).month(), 1).addDays(beginDay - 1); fDays += QDate::currentDate().daysTo(beginDate); } else { //add intervals to current beginDay and take the first after current date beginDay = ((((QDate::currentDate().day() - beginDay) / accCycle) + 1) * accCycle) + beginDay; beginDate = QDate::currentDate().addDays(beginDay - QDate::currentDate().day()); fDays += QDate::currentDate().daysTo(beginDate); } q->setBeginForecastDate(beginDate); return fDays; } /** * remove accounts from the list if the accounts has no transactions in the forecast timeframe. * Used for scheduled-forecast method. */ void purgeForecastAccountsList(QMap& accountList) { m_forecastAccounts.intersect(accountList.keys().toSet()); } MyMoneyForecast *q_ptr; /** * daily forecast balance of accounts */ QMap m_accountList; /** * daily past balance of accounts */ QMap m_accountListPast; /** * daily forecast trends of accounts */ QMap m_accountTrendList; /** * list of forecast account ids. */ QSet m_forecastAccounts; /** * cycle of accounts in days */ qint64 m_accountsCycle; /** * number of cycles to use in forecast */ qint64 m_forecastCycles; /** * number of days to forecast */ qint64 m_forecastDays; /** * date to start forecast */ QDate m_beginForecastDate; /** * day to start forecast */ qint64 m_beginForecastDay; /** * forecast method */ eForecastMethod m_forecastMethod; /** * history method */ int m_historyMethod; /** * start date of history */ QDate m_historyStartDate; /** * end date of history */ QDate m_historyEndDate; /** * start date of forecast */ QDate m_forecastStartDate; /** * end date of forecast */ QDate m_forecastEndDate; /** * skip opening date when fetching transactions of an account */ bool m_skipOpeningDate; /** * include accounts with no transactions in the forecast timeframe. default is false. */ bool m_includeUnusedAccounts; /** * forecast already done */ bool m_forecastDone; /** * include future transactions when doing a scheduled-based forecast */ bool m_includeFutureTransactions; /** * include scheduled transactions when doing a scheduled-based forecast */ bool m_includeScheduledTransactions; }; MyMoneyForecast::MyMoneyForecast() : d_ptr(new MyMoneyForecastPrivate(this)) { setHistoryStartDate(QDate::currentDate().addDays(-forecastCycles()*accountsCycle())); setHistoryEndDate(QDate::currentDate().addDays(-1)); } MyMoneyForecast::MyMoneyForecast(const MyMoneyForecast& other) : d_ptr(new MyMoneyForecastPrivate(*other.d_func())) { this->d_ptr->q_ptr = this; } void swap(MyMoneyForecast& first, MyMoneyForecast& second) { using std::swap; swap(first.d_ptr, second.d_ptr); swap(first.d_ptr->q_ptr, second.d_ptr->q_ptr); } MyMoneyForecast::MyMoneyForecast(MyMoneyForecast && other) : MyMoneyForecast() { swap(*this, other); } MyMoneyForecast & MyMoneyForecast::operator=(MyMoneyForecast other) { swap(*this, other); return *this; } MyMoneyForecast::~MyMoneyForecast() { Q_D(MyMoneyForecast); delete d; } void MyMoneyForecast::doForecast() { Q_D(MyMoneyForecast); auto fDays = d->calculateBeginForecastDay(); auto fMethod = d->forecastMethod(); auto fAccCycle = accountsCycle(); auto fCycles = forecastCycles(); //validate settings if (fAccCycle < 1 || fCycles < 1 || fDays < 1) { throw MYMONEYEXCEPTION_CSTRING("Illegal settings when calling doForecast. Settings must be higher than 0"); } //initialize global variables setForecastDays(fDays); setForecastStartDate(QDate::currentDate().addDays(1)); setForecastEndDate(QDate::currentDate().addDays(fDays)); setAccountsCycle(fAccCycle); setForecastCycles(fCycles); setHistoryStartDate(forecastCycles() * accountsCycle()); setHistoryEndDate(QDate::currentDate().addDays(-1)); //yesterday //clear all data before calculating d->m_accountListPast.clear(); d->m_accountList.clear(); d->m_accountTrendList.clear(); //set forecast accounts d->setForecastAccountList(); switch (fMethod) { case eForecastMethod::Scheduled: d->doFutureScheduledForecast(); d->calculateScheduledDailyBalances(); break; case eForecastMethod::Historic: d->pastTransactions(); d->calculateHistoricDailyBalances(); break; default: break; } //flag the forecast as done d->m_forecastDone = true; } bool MyMoneyForecast::isForecastAccount(const MyMoneyAccount& acc) { Q_D(MyMoneyForecast); if (d->m_forecastAccounts.isEmpty()) { d->setForecastAccountList(); } return d->m_forecastAccounts.contains(acc.id()); } QList MyMoneyForecast::accountList() { auto file = MyMoneyFile::instance(); QList accList; QStringList emptyStringList; //Get all accounts from the file and check if they are present file->accountList(accList, emptyStringList, false); QList::iterator accList_t = accList.begin(); for (; accList_t != accList.end();) { auto acc = *accList_t; if (!isForecastAccount(acc)) { accList_t = accList.erase(accList_t); //remove the account } else { ++accList_t; } } return accList; } MyMoneyMoney MyMoneyForecast::calculateAccountTrend(const MyMoneyAccount& acc, qint64 trendDays) { auto file = MyMoneyFile::instance(); MyMoneyTransactionFilter filter; MyMoneyMoney netIncome; QDate startDate; QDate openingDate = acc.openingDate(); //validate arguments if (trendDays < 1) { throw MYMONEYEXCEPTION_CSTRING("Illegal arguments when calling calculateAccountTrend. trendDays must be higher than 0"); } //If it is a new account, we don't take into account the first day //because it is usually a weird one and it would mess up the trend if (openingDate.daysTo(QDate::currentDate()) < trendDays) { startDate = (acc.openingDate()).addDays(1); } else { startDate = QDate::currentDate().addDays(-trendDays); } //get all transactions for the period filter.setDateFilter(startDate, QDate::currentDate()); if (acc.accountGroup() == eMyMoney::Account::Type::Income || acc.accountGroup() == eMyMoney::Account::Type::Expense) { filter.addCategory(acc.id()); } else { filter.addAccount(acc.id()); } filter.setReportAllSplits(false); //add all transactions for that account foreach (const auto transaction, file->transactionList(filter)) { foreach (const auto split, transaction.splits()) { if (!split.shares().isZero()) { if (acc.id() == split.accountId()) netIncome += split.value(); } } } //calculate trend of the account in the past period MyMoneyMoney accTrend; //don't take into account the first day of the account if (openingDate.daysTo(QDate::currentDate()) < trendDays) { accTrend = netIncome / MyMoneyMoney(openingDate.daysTo(QDate::currentDate()) - 1, 1); } else { accTrend = netIncome / MyMoneyMoney(trendDays, 1); } return accTrend; } MyMoneyMoney MyMoneyForecast::forecastBalance(const MyMoneyAccount& acc, const QDate &forecastDate) { Q_D(MyMoneyForecast); dailyBalances balance; MyMoneyMoney MM_amount = MyMoneyMoney(); //Check if acc is not a forecast account, return 0 if (!isForecastAccount(acc)) { return MM_amount; } if (d->m_accountList.contains(acc.id())) { balance = d->m_accountList.value(acc.id()); } if (balance.contains(forecastDate)) { //if the date is not in the forecast, it returns 0 MM_amount = balance.value(forecastDate); } return MM_amount; } /** * Returns the forecast balance trend for account @a acc for offset @p int * offset is days from current date, inside forecast days. * Returns 0 if offset not in range of forecast days. */ MyMoneyMoney MyMoneyForecast::forecastBalance(const MyMoneyAccount& acc, qint64 offset) { QDate forecastDate = QDate::currentDate().addDays(offset); return forecastBalance(acc, forecastDate); } qint64 MyMoneyForecast::daysToMinimumBalance(const MyMoneyAccount& acc) { Q_D(MyMoneyForecast); QString minimumBalance = acc.value("minBalanceAbsolute"); MyMoneyMoney minBalance = MyMoneyMoney(minimumBalance); dailyBalances balance; //Check if acc is not a forecast account, return -1 if (!isForecastAccount(acc)) { return -1; } balance = d->m_accountList[acc.id()]; for (QDate it_day = QDate::currentDate() ; it_day <= forecastEndDate();) { if (minBalance > balance[it_day]) { return QDate::currentDate().daysTo(it_day); } it_day = it_day.addDays(1); } return -1; } qint64 MyMoneyForecast::daysToZeroBalance(const MyMoneyAccount& acc) { Q_D(MyMoneyForecast); dailyBalances balance; //Check if acc is not a forecast account, return -1 if (!isForecastAccount(acc)) { return -2; } balance = d->m_accountList[acc.id()]; if (acc.accountGroup() == eMyMoney::Account::Type::Asset) { for (QDate it_day = QDate::currentDate() ; it_day <= forecastEndDate();) { if (balance[it_day] < MyMoneyMoney()) { return QDate::currentDate().daysTo(it_day); } it_day = it_day.addDays(1); } } else if (acc.accountGroup() == eMyMoney::Account::Type::Liability) { for (QDate it_day = QDate::currentDate() ; it_day <= forecastEndDate();) { if (balance[it_day] > MyMoneyMoney()) { return QDate::currentDate().daysTo(it_day); } it_day = it_day.addDays(1); } } return -1; } MyMoneyMoney MyMoneyForecast::accountCycleVariation(const MyMoneyAccount& acc) { Q_D(MyMoneyForecast); MyMoneyMoney cycleVariation; if (d->forecastMethod() == eForecastMethod::Historic) { switch (historyMethod()) { case 0: case 1: { for (auto t_day = 1; t_day <= accountsCycle() ; ++t_day) { cycleVariation += d->m_accountTrendList[acc.id()][t_day]; } } break; case 2: { cycleVariation = d->m_accountList[acc.id()][QDate::currentDate().addDays(accountsCycle())] - d->m_accountList[acc.id()][QDate::currentDate()]; break; } } } return cycleVariation; } MyMoneyMoney MyMoneyForecast::accountTotalVariation(const MyMoneyAccount& acc) { MyMoneyMoney totalVariation; totalVariation = forecastBalance(acc, forecastEndDate()) - forecastBalance(acc, QDate::currentDate()); return totalVariation; } QList MyMoneyForecast::accountMinimumBalanceDateList(const MyMoneyAccount& acc) { QList minBalanceList; qint64 daysToBeginDay; daysToBeginDay = QDate::currentDate().daysTo(beginForecastDate()); for (auto t_cycle = 0; ((t_cycle * accountsCycle()) + daysToBeginDay) < forecastDays() ; ++t_cycle) { MyMoneyMoney minBalance = forecastBalance(acc, (t_cycle * accountsCycle() + daysToBeginDay)); QDate minDate = QDate::currentDate().addDays(t_cycle * accountsCycle() + daysToBeginDay); for (auto t_day = 1; t_day <= accountsCycle() ; ++t_day) { if (minBalance > forecastBalance(acc, (t_cycle * accountsCycle()) + daysToBeginDay + t_day)) { minBalance = forecastBalance(acc, (t_cycle * accountsCycle()) + daysToBeginDay + t_day); minDate = QDate::currentDate().addDays((t_cycle * accountsCycle()) + daysToBeginDay + t_day); } } minBalanceList.append(minDate); } return minBalanceList; } QList MyMoneyForecast::accountMaximumBalanceDateList(const MyMoneyAccount& acc) { QList maxBalanceList; qint64 daysToBeginDay; daysToBeginDay = QDate::currentDate().daysTo(beginForecastDate()); for (auto t_cycle = 0; ((t_cycle * accountsCycle()) + daysToBeginDay) < forecastDays() ; ++t_cycle) { MyMoneyMoney maxBalance = forecastBalance(acc, ((t_cycle * accountsCycle()) + daysToBeginDay)); QDate maxDate = QDate::currentDate().addDays((t_cycle * accountsCycle()) + daysToBeginDay); for (auto t_day = 0; t_day < accountsCycle() ; ++t_day) { if (maxBalance < forecastBalance(acc, (t_cycle * accountsCycle()) + daysToBeginDay + t_day)) { maxBalance = forecastBalance(acc, (t_cycle * accountsCycle()) + daysToBeginDay + t_day); maxDate = QDate::currentDate().addDays((t_cycle * accountsCycle()) + daysToBeginDay + t_day); } } maxBalanceList.append(maxDate); } return maxBalanceList; } MyMoneyMoney MyMoneyForecast::accountAverageBalance(const MyMoneyAccount& acc) { MyMoneyMoney totalBalance; for (auto f_day = 1; f_day <= forecastDays() ; ++f_day) { totalBalance += forecastBalance(acc, f_day); } return totalBalance / MyMoneyMoney(forecastDays(), 1); } void MyMoneyForecast::createBudget(MyMoneyBudget& budget, QDate historyStart, QDate historyEnd, QDate budgetStart, QDate budgetEnd, const bool returnBudget) { Q_D(MyMoneyForecast); // clear all data except the id and name QString name = budget.name(); budget = MyMoneyBudget(budget.id(), MyMoneyBudget()); budget.setName(name); //check parameters if (historyStart > historyEnd || budgetStart > budgetEnd || budgetStart <= historyEnd) { throw MYMONEYEXCEPTION_CSTRING("Illegal parameters when trying to create budget"); } //get forecast method auto fMethod = d->forecastMethod(); //set start date to 1st of month and end dates to last day of month, since we deal with full months in budget historyStart = QDate(historyStart.year(), historyStart.month(), 1); historyEnd = QDate(historyEnd.year(), historyEnd.month(), historyEnd.daysInMonth()); budgetStart = QDate(budgetStart.year(), budgetStart.month(), 1); budgetEnd = QDate(budgetEnd.year(), budgetEnd.month(), budgetEnd.daysInMonth()); //set forecast parameters setHistoryStartDate(historyStart); setHistoryEndDate(historyEnd); setForecastStartDate(budgetStart); setForecastEndDate(budgetEnd); setForecastDays(budgetStart.daysTo(budgetEnd) + 1); if (budgetStart.daysTo(budgetEnd) > historyStart.daysTo(historyEnd)) { //if history period is shorter than budget, use that one as the trend length setAccountsCycle(historyStart.daysTo(historyEnd)); //we set the accountsCycle to the base timeframe we will use to calculate the average (eg. 180 days, 365, etc) } else { //if one timeframe is larger than the other, but not enough to be 1 time larger, we take the lowest value setAccountsCycle(budgetStart.daysTo(budgetEnd)); } setForecastCycles((historyStart.daysTo(historyEnd) / accountsCycle())); if (forecastCycles() == 0) //the cycles must be at least 1 setForecastCycles(1); //do not skip opening date setSkipOpeningDate(false); //clear and set accounts list we are going to use. Categories, in this case d->m_forecastAccounts.clear(); d->setBudgetAccountList(); //calculate budget according to forecast method switch (fMethod) { case eForecastMethod::Scheduled: d->doFutureScheduledForecast(); d->calculateScheduledMonthlyBalances(); break; case eForecastMethod::Historic: d->pastTransactions(); //get all transactions for history period d->calculateAccountTrendList(); d->calculateHistoricMonthlyBalances(); //add all balances of each month and put at the 1st day of each month break; default: break; } //flag the forecast as done d->m_forecastDone = true; //only fill the budget if it is going to be used if (returnBudget) { //setup the budget itself auto file = MyMoneyFile::instance(); budget.setBudgetStart(budgetStart); //go through all the accounts and add them to budget for (auto it_nc = d->m_forecastAccounts.constBegin(); it_nc != d->m_forecastAccounts.constEnd(); ++it_nc) { auto acc = file->account(*it_nc); MyMoneyBudget::AccountGroup budgetAcc; budgetAcc.setId(acc.id()); - budgetAcc.setBudgetLevel(MyMoneyBudget::AccountGroup::eMonthByMonth); + budgetAcc.setBudgetLevel(eMyMoney::Budget::Level::MonthByMonth); for (QDate f_date = forecastStartDate(); f_date <= forecastEndDate();) { MyMoneyBudget::PeriodGroup period; //add period to budget account period.setStartDate(f_date); period.setAmount(forecastBalance(acc, f_date)); budgetAcc.addPeriod(f_date, period); //next month f_date = f_date.addMonths(1); } //add budget account to budget budget.setAccount(budgetAcc, acc.id()); } } } qint64 MyMoneyForecast::historyDays() const { Q_D(const MyMoneyForecast); return (d->m_historyStartDate.daysTo(d->m_historyEndDate) + 1); } void MyMoneyForecast::setAccountsCycle(qint64 accountsCycle) { Q_D(MyMoneyForecast); d->m_accountsCycle = accountsCycle; } void MyMoneyForecast::setForecastCycles(qint64 forecastCycles) { Q_D(MyMoneyForecast); d->m_forecastCycles = forecastCycles; } void MyMoneyForecast::setForecastDays(qint64 forecastDays) { Q_D(MyMoneyForecast); d->m_forecastDays = forecastDays; } void MyMoneyForecast::setBeginForecastDate(const QDate &beginForecastDate) { Q_D(MyMoneyForecast); d->m_beginForecastDate = beginForecastDate; } void MyMoneyForecast::setBeginForecastDay(qint64 beginDay) { Q_D(MyMoneyForecast); d->m_beginForecastDay = beginDay; } void MyMoneyForecast::setForecastMethod(qint64 forecastMethod) { Q_D(MyMoneyForecast); d->m_forecastMethod = static_cast(forecastMethod); } void MyMoneyForecast::setHistoryStartDate(const QDate &historyStartDate) { Q_D(MyMoneyForecast); d->m_historyStartDate = historyStartDate; } void MyMoneyForecast::setHistoryEndDate(const QDate &historyEndDate) { Q_D(MyMoneyForecast); d->m_historyEndDate = historyEndDate; } void MyMoneyForecast::setHistoryStartDate(qint64 daysToStartDate) { setHistoryStartDate(QDate::currentDate().addDays(-daysToStartDate)); } void MyMoneyForecast::setHistoryEndDate(qint64 daysToEndDate) { setHistoryEndDate(QDate::currentDate().addDays(-daysToEndDate)); } void MyMoneyForecast::setForecastStartDate(const QDate &_startDate) { Q_D(MyMoneyForecast); d->m_forecastStartDate = _startDate; } void MyMoneyForecast::setForecastEndDate(const QDate &_endDate) { Q_D(MyMoneyForecast); d->m_forecastEndDate = _endDate; } void MyMoneyForecast::setSkipOpeningDate(bool _skip) { Q_D(MyMoneyForecast); d->m_skipOpeningDate = _skip; } void MyMoneyForecast::setHistoryMethod(int historyMethod) { Q_D(MyMoneyForecast); d->m_historyMethod = historyMethod; } void MyMoneyForecast::setIncludeUnusedAccounts(bool _bool) { Q_D(MyMoneyForecast); d->m_includeUnusedAccounts = _bool; } void MyMoneyForecast::setForecastDone(bool _bool) { Q_D(MyMoneyForecast); d->m_forecastDone = _bool; } void MyMoneyForecast::setIncludeFutureTransactions(bool _bool) { Q_D(MyMoneyForecast); d->m_includeFutureTransactions = _bool; } void MyMoneyForecast::setIncludeScheduledTransactions(bool _bool) { Q_D(MyMoneyForecast); d->m_includeScheduledTransactions = _bool; } qint64 MyMoneyForecast::accountsCycle() const { Q_D(const MyMoneyForecast); return d->m_accountsCycle; } qint64 MyMoneyForecast::forecastCycles() const { Q_D(const MyMoneyForecast); return d->m_forecastCycles; } qint64 MyMoneyForecast::forecastDays() const { Q_D(const MyMoneyForecast); return d->m_forecastDays; } QDate MyMoneyForecast::beginForecastDate() const { Q_D(const MyMoneyForecast); return d->m_beginForecastDate; } qint64 MyMoneyForecast::beginForecastDay() const { Q_D(const MyMoneyForecast); return d->m_beginForecastDay; } QDate MyMoneyForecast::historyStartDate() const { Q_D(const MyMoneyForecast); return d->m_historyStartDate; } QDate MyMoneyForecast::historyEndDate() const { Q_D(const MyMoneyForecast); return d->m_historyEndDate; } QDate MyMoneyForecast::forecastStartDate() const { Q_D(const MyMoneyForecast); return d->m_forecastStartDate; } QDate MyMoneyForecast::forecastEndDate() const { Q_D(const MyMoneyForecast); return d->m_forecastEndDate; } bool MyMoneyForecast::skipOpeningDate() const { Q_D(const MyMoneyForecast); return d->m_skipOpeningDate; } int MyMoneyForecast::historyMethod() const { Q_D(const MyMoneyForecast); return d->m_historyMethod; } bool MyMoneyForecast::isIncludingUnusedAccounts() const { Q_D(const MyMoneyForecast); return d->m_includeUnusedAccounts; } bool MyMoneyForecast::isForecastDone() const { Q_D(const MyMoneyForecast); return d->m_forecastDone; } bool MyMoneyForecast::isIncludingFutureTransactions() const { Q_D(const MyMoneyForecast); return d->m_includeFutureTransactions; } bool MyMoneyForecast::isIncludingScheduledTransactions() const { Q_D(const MyMoneyForecast); return d->m_includeScheduledTransactions; } void MyMoneyForecast::calculateAutoLoan(const MyMoneySchedule& schedule, MyMoneyTransaction& transaction, const QMap& balances) { if (schedule.type() == eMyMoney::Schedule::Type::LoanPayment) { //get amortization and interest autoCalc splits MyMoneySplit amortizationSplit = transaction.amortizationSplit(); MyMoneySplit interestSplit = transaction.interestSplit(); const bool interestSplitValid = !interestSplit.id().isEmpty(); if (!amortizationSplit.id().isEmpty()) { MyMoneyAccountLoan acc(MyMoneyFile::instance()->account(amortizationSplit.accountId())); MyMoneyFinancialCalculator calc; QDate dueDate; // FIXME: setup dueDate according to when the interest should be calculated // current implementation: take the date of the next payment according to // the schedule. If the calculation is based on the payment reception, and // the payment is overdue then take the current date dueDate = schedule.nextDueDate(); if (acc.interestCalculation() == MyMoneyAccountLoan::paymentReceived) { if (dueDate < QDate::currentDate()) dueDate = QDate::currentDate(); } // we need to calculate the balance at the time the payment is due MyMoneyMoney balance; if (balances.count() == 0) balance = MyMoneyFile::instance()->balance(acc.id(), dueDate.addDays(-1)); else balance = balances[acc.id()]; // FIXME: for now, we only support interest calculation at the end of the period calc.setBep(); // FIXME: for now, we only support periodic compounding calc.setDisc(); calc.setPF(MyMoneySchedule::eventsPerYear(schedule.occurrence())); eMyMoney::Schedule::Occurrence compoundingOccurrence = static_cast(acc.interestCompounding()); if (compoundingOccurrence == eMyMoney::Schedule::Occurrence::Any) compoundingOccurrence = schedule.occurrence(); calc.setCF(MyMoneySchedule::eventsPerYear(compoundingOccurrence)); calc.setPv(balance.toDouble()); calc.setIr(acc.interestRate(dueDate).abs().toDouble()); calc.setPmt(acc.periodicPayment().toDouble()); MyMoneyMoney interest(calc.interestDue(), 100), amortization; interest = interest.abs(); // make sure it's positive for now amortization = acc.periodicPayment() - interest; if (acc.accountType() == eMyMoney::Account::Type::AssetLoan) { interest = -interest; amortization = -amortization; } amortizationSplit.setShares(amortization); if (interestSplitValid) interestSplit.setShares(interest); // FIXME: for now we only assume loans to be in the currency of the transaction amortizationSplit.setValue(amortization); if (interestSplitValid) interestSplit.setValue(interest); transaction.modifySplit(amortizationSplit); if (interestSplitValid) transaction.modifySplit(interestSplit); } } } QList MyMoneyForecast::forecastAccountList() { auto file = MyMoneyFile::instance(); QList accList; //Get all accounts from the file and check if they are of the right type to calculate forecast file->accountList(accList); QList::iterator accList_t = accList.begin(); for (; accList_t != accList.end();) { auto acc = *accList_t; if (acc.isClosed() //check the account is not closed || (!acc.isAssetLiability())) { //|| (acc.accountType() == eMyMoney::Account::Type::Investment) ) {//check that it is not an Investment account and only include Stock accounts //remove the account if it is not of the correct type accList_t = accList.erase(accList_t); } else { ++accList_t; } } return accList; } diff --git a/kmymoney/mymoney/mymoneyinstitution.cpp b/kmymoney/mymoney/mymoneyinstitution.cpp index 922d20eca..b94786d2d 100644 --- a/kmymoney/mymoney/mymoneyinstitution.cpp +++ b/kmymoney/mymoney/mymoneyinstitution.cpp @@ -1,326 +1,332 @@ /* * Copyright 2000-2001 Michael Edwardes * Copyright 2002-2017 Thomas Baumgart * Copyright 2003 Kevin Tambascio * Copyright 2004-2006 Ace Jones * 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 "mymoneyinstitution.h" #include "mymoneyinstitution_p.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes #include "icons.h" #include #include "mymoneystoragenames.h" using namespace MyMoneyStorageNodes; using namespace Icons; MyMoneyInstitution::MyMoneyInstitution() : MyMoneyObject(*new MyMoneyInstitutionPrivate), MyMoneyKeyValueContainer() { } +MyMoneyInstitution::MyMoneyInstitution(const QString &id) : + MyMoneyObject(*new MyMoneyInstitutionPrivate, id), + MyMoneyKeyValueContainer() +{ +} + MyMoneyInstitution::MyMoneyInstitution(const QString& name, const QString& town, const QString& street, const QString& postcode, const QString& telephone, const QString& manager, const QString& sortcode) : MyMoneyObject(*new MyMoneyInstitutionPrivate), MyMoneyKeyValueContainer() { Q_D(MyMoneyInstitution); clearId(); d->m_name = name; d->m_town = town; d->m_street = street; d->m_postcode = postcode; d->m_telephone = telephone; d->m_manager = manager; d->m_sortcode = sortcode; } MyMoneyInstitution::MyMoneyInstitution(const QDomElement& node) : MyMoneyObject(*new MyMoneyInstitutionPrivate, node), MyMoneyKeyValueContainer(node.elementsByTagName(nodeNames[nnKeyValuePairs]).item(0).toElement()) { if (nodeNames[nnInstitution] != node.tagName()) throw MYMONEYEXCEPTION_CSTRING("Node was not INSTITUTION"); Q_D(MyMoneyInstitution); d->m_sortcode = node.attribute(d->getAttrName(Institution::Attribute::SortCode)); d->m_name = node.attribute(d->getAttrName(Institution::Attribute::Name)); d->m_manager = node.attribute(d->getAttrName(Institution::Attribute::Manager)); QDomNodeList nodeList = node.elementsByTagName(d->getElName(Institution::Element::Address)); if (nodeList.isEmpty()) throw MYMONEYEXCEPTION(QString::fromLatin1("No ADDRESS in institution %1").arg(d->m_name)); QDomElement addrNode = nodeList.item(0).toElement(); d->m_street = addrNode.attribute(d->getAttrName(Institution::Attribute::Street)); d->m_town = addrNode.attribute(d->getAttrName(Institution::Attribute::City)); d->m_postcode = addrNode.attribute(d->getAttrName(Institution::Attribute::Zip)); d->m_telephone = addrNode.attribute(d->getAttrName(Institution::Attribute::Telephone)); d->m_accountList.clear(); nodeList = node.elementsByTagName(d->getElName(Institution::Element::AccountIDS)); if (nodeList.count() > 0) { nodeList = nodeList.item(0).toElement().elementsByTagName(d->getElName(Institution::Element::AccountID)); for (int i = 0; i < nodeList.count(); ++i) { d->m_accountList << nodeList.item(i).toElement().attribute(d->getAttrName(Institution::Attribute::ID)); } } } MyMoneyInstitution::MyMoneyInstitution(const MyMoneyInstitution& other) : MyMoneyObject(*new MyMoneyInstitutionPrivate(*other.d_func()), other.id()), MyMoneyKeyValueContainer(other) { } MyMoneyInstitution::MyMoneyInstitution(const QString& id, const MyMoneyInstitution& other) : MyMoneyObject(*new MyMoneyInstitutionPrivate(*other.d_func()), id), MyMoneyKeyValueContainer(other) { } MyMoneyInstitution::~MyMoneyInstitution() { } QString MyMoneyInstitution::manager() const { Q_D(const MyMoneyInstitution); return d->m_manager; } void MyMoneyInstitution::setManager(const QString& manager) { Q_D(MyMoneyInstitution); d->m_manager = manager; } QString MyMoneyInstitution::name() const { Q_D(const MyMoneyInstitution); return d->m_name; } void MyMoneyInstitution::setName(const QString& name) { Q_D(MyMoneyInstitution); d->m_name = name; } QString MyMoneyInstitution::postcode() const { Q_D(const MyMoneyInstitution); return d->m_postcode; } void MyMoneyInstitution::setPostcode(const QString& code) { Q_D(MyMoneyInstitution); d->m_postcode = code; } QString MyMoneyInstitution::street() const { Q_D(const MyMoneyInstitution); return d->m_street; } void MyMoneyInstitution::setStreet(const QString& street) { Q_D(MyMoneyInstitution); d->m_street = street; } QString MyMoneyInstitution::telephone() const { Q_D(const MyMoneyInstitution); return d->m_telephone; } void MyMoneyInstitution::setTelephone(const QString& tel) { Q_D(MyMoneyInstitution); d->m_telephone = tel; } QString MyMoneyInstitution::town() const { Q_D(const MyMoneyInstitution); return d->m_town; } void MyMoneyInstitution::setTown(const QString& town) { Q_D(MyMoneyInstitution); d->m_town = town; } QString MyMoneyInstitution::city() const { return town(); } void MyMoneyInstitution::setCity(const QString& town) { setTown(town); } QString MyMoneyInstitution::sortcode() const { Q_D(const MyMoneyInstitution); return d->m_sortcode; } void MyMoneyInstitution::setSortcode(const QString& code) { Q_D(MyMoneyInstitution); d->m_sortcode = code; } void MyMoneyInstitution::addAccountId(const QString& account) { Q_D(MyMoneyInstitution); // only add this account if it is not yet presently in the list if (d->m_accountList.contains(account) == 0) d->m_accountList.append(account); } QString MyMoneyInstitution::removeAccountId(const QString& account) { Q_D(MyMoneyInstitution); QString rc; auto pos = d->m_accountList.indexOf(account); if (pos != -1) { d->m_accountList.removeAt(pos); rc = account; } return rc; } QStringList MyMoneyInstitution::accountList() const { Q_D(const MyMoneyInstitution); return d->m_accountList; } /** * This method returns the number of accounts known to * this institution * @return number of accounts */ unsigned int MyMoneyInstitution::accountCount() const { Q_D(const MyMoneyInstitution); return d->m_accountList.count(); } bool MyMoneyInstitution::operator < (const MyMoneyInstitution& right) const { Q_D(const MyMoneyInstitution); auto d2 = static_cast(right.d_func()); return d->m_name < d2->m_name; } bool MyMoneyInstitution::operator == (const MyMoneyInstitution& right) const { Q_D(const MyMoneyInstitution); auto d2 = static_cast(right.d_func()); if (MyMoneyObject::operator==(right) && ((d->m_name.length() == 0 && d2->m_name.length() == 0) || (d->m_name == d2->m_name)) && ((d->m_town.length() == 0 && d2->m_town.length() == 0) || (d->m_town == d2->m_town)) && ((d->m_street.length() == 0 && d2->m_street.length() == 0) || (d->m_street == d2->m_street)) && ((d->m_postcode.length() == 0 && d2->m_postcode.length() == 0) || (d->m_postcode == d2->m_postcode)) && ((d->m_telephone.length() == 0 && d2->m_telephone.length() == 0) || (d->m_telephone == d2->m_telephone)) && ((d->m_sortcode.length() == 0 && d2->m_sortcode.length() == 0) || (d->m_sortcode == d2->m_sortcode)) && ((d->m_manager.length() == 0 && d2->m_manager.length() == 0) || (d->m_manager == d2->m_manager)) && (d->m_accountList == d2->m_accountList)) { return true; } else return false; } void MyMoneyInstitution::writeXML(QDomDocument& document, QDomElement& parent) const { Q_D(const MyMoneyInstitution); auto el = document.createElement(nodeNames[nnInstitution]); d->writeBaseXML(document, el); el.setAttribute(d->getAttrName(Institution::Attribute::Name), d->m_name); el.setAttribute(d->getAttrName(Institution::Attribute::Manager), d->m_manager); el.setAttribute(d->getAttrName(Institution::Attribute::SortCode), d->m_sortcode); auto address = document.createElement(d->getElName(Institution::Element::Address)); address.setAttribute(d->getAttrName(Institution::Attribute::Street), d->m_street); address.setAttribute(d->getAttrName(Institution::Attribute::City), d->m_town); address.setAttribute(d->getAttrName(Institution::Attribute::Zip), d->m_postcode); address.setAttribute(d->getAttrName(Institution::Attribute::Telephone), d->m_telephone); el.appendChild(address); auto accounts = document.createElement(d->getElName(Institution::Element::AccountIDS)); foreach (const auto accountID, accountList()) { auto temp = document.createElement(d->getElName(Institution::Element::AccountID)); temp.setAttribute(d->getAttrName(Institution::Attribute::ID), accountID); accounts.appendChild(temp); } el.appendChild(accounts); //Add in Key-Value Pairs for institutions. MyMoneyKeyValueContainer::writeXML(document, el); parent.appendChild(el); } bool MyMoneyInstitution::hasReferenceTo(const QString& /* id */) const { return false; } QPixmap MyMoneyInstitution::pixmap(const int size) { QPixmap pxIcon; auto kyIcon = QString::fromLatin1("view_institution%1").arg(QString::number(size)); if (!QPixmapCache::find(kyIcon, pxIcon)) { pxIcon = Icons::get(Icon::ViewInstitutions).pixmap(size); QPixmapCache::insert(kyIcon, pxIcon); } return pxIcon; } diff --git a/kmymoney/mymoney/mymoneyinstitution.h b/kmymoney/mymoney/mymoneyinstitution.h index 9511dc870..9d8e5dc61 100644 --- a/kmymoney/mymoney/mymoneyinstitution.h +++ b/kmymoney/mymoney/mymoneyinstitution.h @@ -1,199 +1,200 @@ /* * Copyright 2000-2001 Michael Edwardes * Copyright 2002-2017 Thomas Baumgart * Copyright 2003 Kevin Tambascio * Copyright 2004-2006 Ace Jones * 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 MYMONEYINSTITUTION_H #define MYMONEYINSTITUTION_H // ---------------------------------------------------------------------------- // QT Includes #include // ---------------------------------------------------------------------------- // Project Includes #include "mymoneyobject.h" #include "mymoneykeyvaluecontainer.h" #include "kmm_mymoney_export.h" class QString; class QStringList; class QPixmap; /** * This class represents a Bank contained within a MyMoneyFile object * * @author Thomas Baumgart * @author Łukasz Wojniłowicz */ class MyMoneyInstitutionPrivate; class KMM_MYMONEY_EXPORT MyMoneyInstitution : public MyMoneyObject, public MyMoneyKeyValueContainer { Q_DECLARE_PRIVATE_D(MyMoneyObject::d_ptr, MyMoneyInstitution) KMM_MYMONEY_UNIT_TESTABLE public: /** * This is the constructor for a new empty institution description */ MyMoneyInstitution(); + explicit MyMoneyInstitution(const QString &id); /** * This is the constructor used by an application to fill the * values required for a new institution. This object should then be * passed to @see MyMoneyFile::addInstitution */ explicit MyMoneyInstitution(const QString& name, const QString& city, const QString& street, const QString& postcode, const QString& telephone, const QString& manager, const QString& sortCode); MyMoneyInstitution(const MyMoneyInstitution & other); MyMoneyInstitution(MyMoneyInstitution && other); MyMoneyInstitution & operator=(MyMoneyInstitution other); friend void swap(MyMoneyInstitution& first, MyMoneyInstitution& second); /** * This is the destructor for any MyMoneyInstitution object */ ~MyMoneyInstitution(); /** * This is the constructor for a new institution known to the current file * * @param id id assigned to the new institution object * @param right institution definition */ MyMoneyInstitution(const QString& id, const MyMoneyInstitution& other); /** * This is the constructor for an institution that is described by a * QDomElement (e.g. from a file). * * @param el const reference to the QDomElement from which to * create the object */ explicit MyMoneyInstitution(const QDomElement& el); QString manager() const; void setManager(const QString& manager); QString name() const; void setName(const QString& name); QString postcode() const; void setPostcode(const QString& code); QString street() const; void setStreet(const QString& street); QString telephone() const; void setTelephone(const QString& tel); QString town() const; void setTown(const QString& town); QString city() const; void setCity(const QString& town); QString sortcode() const; void setSortcode(const QString& code); /** * This method adds the id of an account to the account list of * this institution It is verified, that the account is only * mentioned once. * * @param account id of the account to be added */ void addAccountId(const QString& account); /** * This method deletes the id of an account from the account list * of this institution * * @param account id of the account to be deleted * @return id of account deleted, otherwise empty string */ QString removeAccountId(const QString& account); /** * This method is used to return the set of accounts known to * this institution * return QStringList of account ids */ QStringList accountList() const; /** * This method returns the number of accounts known to * this institution * @return number of accounts */ unsigned int accountCount() const; bool operator == (const MyMoneyInstitution&) const; bool operator < (const MyMoneyInstitution& right) const; void writeXML(QDomDocument& document, QDomElement& parent) const override; /** * This method checks if a reference to the given object exists. It returns, * a @p true if the object is referencing the one requested by the * parameter @p id. If it does not, this method returns @p false. * * @param id id of the object to be checked for references * @retval true This object references object with id @p id. * @retval false This object does not reference the object with id @p id. */ bool hasReferenceTo(const QString& id) const override; static QPixmap pixmap(const int size = 64); }; inline void swap(MyMoneyInstitution& first, MyMoneyInstitution& second) // krazy:exclude=inline { using std::swap; swap(first.MyMoneyObject::d_ptr, second.MyMoneyObject::d_ptr); swap(first.MyMoneyKeyValueContainer::d_ptr, second.MyMoneyKeyValueContainer::d_ptr); } inline MyMoneyInstitution::MyMoneyInstitution(MyMoneyInstitution && other) : MyMoneyInstitution() // krazy:exclude=inline { swap(*this, other); } inline MyMoneyInstitution & MyMoneyInstitution::operator=(MyMoneyInstitution other) // krazy:exclude=inline { swap(*this, other); return *this; } /** * Make it possible to hold @ref MyMoneyInstitution objects inside @ref QVariant objects. */ Q_DECLARE_METATYPE(MyMoneyInstitution) #endif diff --git a/kmymoney/mymoney/mymoneypayee.cpp b/kmymoney/mymoney/mymoneypayee.cpp index 11e9c8217..bb4ddba56 100644 --- a/kmymoney/mymoney/mymoneypayee.cpp +++ b/kmymoney/mymoney/mymoneypayee.cpp @@ -1,396 +1,402 @@ /* * Copyright 2000-2001 Michael Edwardes * Copyright 2002-2017 Thomas Baumgart * Copyright 2003 Kevin Tambascio * Copyright 2006 Ace Jones * 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 "mymoneypayee.h" #include "mymoneypayee_p.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include #include "mymoneystoragenames.h" +#include "mymoneyenums.h" using namespace MyMoneyStorageNodes; MyMoneyPayee MyMoneyPayee::null; MyMoneyPayee::MyMoneyPayee() : MyMoneyObject(*new MyMoneyPayeePrivate) { } +MyMoneyPayee::MyMoneyPayee(const QString &id): + MyMoneyObject(*new MyMoneyPayeePrivate, id) +{ +} + MyMoneyPayee::MyMoneyPayee(const QString& name, const QString& address, const QString& city, const QString& state, const QString& postcode, const QString& telephone, const QString& email) : MyMoneyObject(*new MyMoneyPayeePrivate) { Q_D(MyMoneyPayee); d->m_name = name; d->m_address = address; d->m_city = city; d->m_state = state; d->m_postcode = postcode; d->m_telephone = telephone; d->m_email = email; d->m_matchingEnabled = false; d->m_usingMatchKey = false; d->m_matchKeyIgnoreCase = true; } MyMoneyPayee::MyMoneyPayee(const QDomElement& node) : MyMoneyObject(*new MyMoneyPayeePrivate, node) { if (nodeNames[nnPayee] != node.tagName()) { throw MYMONEYEXCEPTION_CSTRING("Node was not PAYEE"); } Q_D(MyMoneyPayee); d->m_name = node.attribute(d->getAttrName(Payee::Attribute::Name)); d->m_reference = node.attribute(d->getAttrName(Payee::Attribute::Reference)); d->m_email = node.attribute(d->getAttrName(Payee::Attribute::Email)); d->m_matchingEnabled = node.attribute(d->getAttrName(Payee::Attribute::MatchingEnabled), "0").toUInt(); if (d->m_matchingEnabled) { - setMatchData((node.attribute(d->getAttrName(Payee::Attribute::UsingMatchKey), "0").toUInt() != 0) ? matchKey : matchName, + setMatchData((node.attribute(d->getAttrName(Payee::Attribute::UsingMatchKey), "0").toUInt() != 0) ? eMyMoney::Payee::MatchType::Key : eMyMoney::Payee::MatchType::Name, node.attribute(d->getAttrName(Payee::Attribute::MatchIgnoreCase), "0").toUInt(), node.attribute(d->getAttrName(Payee::Attribute::MatchKey))); } if (node.hasAttribute(d->getAttrName(Payee::Attribute::Notes))) { d->m_notes = node.attribute(d->getAttrName(Payee::Attribute::Notes)); } if (node.hasAttribute(d->getAttrName(Payee::Attribute::DefaultAccountID))) { d->m_defaultAccountId = node.attribute(d->getAttrName(Payee::Attribute::DefaultAccountID)); } // Load Address QDomNodeList nodeList = node.elementsByTagName(d->getElName(Payee::Element::Address)); if (nodeList.isEmpty()) throw MYMONEYEXCEPTION(QString::fromLatin1("No ADDRESS in payee %1").arg(d->m_name)); QDomElement addrNode = nodeList.item(0).toElement(); d->m_address = addrNode.attribute(d->getAttrName(Payee::Attribute::Street)); d->m_city = addrNode.attribute(d->getAttrName(Payee::Attribute::City)); d->m_postcode = addrNode.attribute(d->getAttrName(Payee::Attribute::PostCode)); d->m_state = addrNode.attribute(d->getAttrName(Payee::Attribute::State)); d->m_telephone = addrNode.attribute(d->getAttrName(Payee::Attribute::Telephone)); MyMoneyPayeeIdentifierContainer::loadXML(node); } MyMoneyPayee::MyMoneyPayee(const MyMoneyPayee& other) : MyMoneyObject(*new MyMoneyPayeePrivate(*other.d_func()), other.id()), MyMoneyPayeeIdentifierContainer(other) { } MyMoneyPayee::MyMoneyPayee(const QString& id, const MyMoneyPayee& other) : MyMoneyObject(*new MyMoneyPayeePrivate(*other.d_func()), id), MyMoneyPayeeIdentifierContainer(other) { } MyMoneyPayee::~MyMoneyPayee() { } bool MyMoneyPayee::operator == (const MyMoneyPayee& right) const { Q_D(const MyMoneyPayee); auto d2 = static_cast(right.d_func()); return (MyMoneyObject::operator==(right) && ((d->m_name.length() == 0 && d2->m_name.length() == 0) || (d->m_name == d2->m_name)) && ((d->m_address.length() == 0 && d2->m_address.length() == 0) || (d->m_address == d2->m_address)) && ((d->m_city.length() == 0 && d2->m_city.length() == 0) || (d->m_city == d2->m_city)) && ((d->m_state.length() == 0 && d2->m_state.length() == 0) || (d->m_state == d2->m_state)) && ((d->m_postcode.length() == 0 && d2->m_postcode.length() == 0) || (d->m_postcode == d2->m_postcode)) && ((d->m_telephone.length() == 0 && d2->m_telephone.length() == 0) || (d->m_telephone == d2->m_telephone)) && ((d->m_email.length() == 0 && d2->m_email.length() == 0) || (d->m_email == d2->m_email)) && (d->m_matchingEnabled == d2->m_matchingEnabled) && (d->m_usingMatchKey == d2->m_usingMatchKey) && (d->m_matchKeyIgnoreCase == d2->m_matchKeyIgnoreCase) && ((d->m_matchKey.length() == 0 && d2->m_matchKey.length() == 0) || d->m_matchKey == d2->m_matchKey) && ((d->m_reference.length() == 0 && d2->m_reference.length() == 0) || (d->m_reference == d2->m_reference)) && ((d->m_defaultAccountId.length() == 0 && d2->m_defaultAccountId.length() == 0) || d->m_defaultAccountId == d2->m_defaultAccountId)); } //bool MyMoneyPayee::operator == (const MyMoneyPayee& lhs, const QString& rhs) const //{ // Q_D(const MyMoneyPayee); // return lhs.id() == rhs; //} bool MyMoneyPayee::operator < (const MyMoneyPayee& right) const { Q_D(const MyMoneyPayee); auto d2 = static_cast(right.d_func()); return d->m_name < d2->m_name; } void MyMoneyPayee::writeXML(QDomDocument& document, QDomElement& parent) const { auto el = document.createElement(nodeNames[nnPayee]); Q_D(const MyMoneyPayee); d->writeBaseXML(document, el); el.setAttribute(d->getAttrName(Payee::Attribute::Name), d->m_name); el.setAttribute(d->getAttrName(Payee::Attribute::Reference), d->m_reference); el.setAttribute(d->getAttrName(Payee::Attribute::Email), d->m_email); if (!d->m_notes.isEmpty()) el.setAttribute(d->getAttrName(Payee::Attribute::Notes), d->m_notes); el.setAttribute(d->getAttrName(Payee::Attribute::MatchingEnabled), d->m_matchingEnabled); if (d->m_matchingEnabled) { el.setAttribute(d->getAttrName(Payee::Attribute::UsingMatchKey), d->m_usingMatchKey); el.setAttribute(d->getAttrName(Payee::Attribute::MatchIgnoreCase), d->m_matchKeyIgnoreCase); el.setAttribute(d->getAttrName(Payee::Attribute::MatchKey), d->m_matchKey); } if (!d->m_defaultAccountId.isEmpty()) { el.setAttribute(d->getAttrName(Payee::Attribute::DefaultAccountID), d->m_defaultAccountId); } // Save address QDomElement address = document.createElement(d->getElName(Payee::Element::Address)); address.setAttribute(d->getAttrName(Payee::Attribute::Street), d->m_address); address.setAttribute(d->getAttrName(Payee::Attribute::City), d->m_city); address.setAttribute(d->getAttrName(Payee::Attribute::PostCode), d->m_postcode); address.setAttribute(d->getAttrName(Payee::Attribute::State), d->m_state); address.setAttribute(d->getAttrName(Payee::Attribute::Telephone), d->m_telephone); el.appendChild(address); // Save payeeIdentifiers (account numbers) MyMoneyPayeeIdentifierContainer::writeXML(document, el); parent.appendChild(el); } bool MyMoneyPayee::hasReferenceTo(const QString& id) const { Q_D(const MyMoneyPayee); return id == d->m_defaultAccountId; } QString MyMoneyPayee::name() const { Q_D(const MyMoneyPayee); return d->m_name; } void MyMoneyPayee::setName(const QString& val) { Q_D(MyMoneyPayee); d->m_name = val; } QString MyMoneyPayee::address() const { Q_D(const MyMoneyPayee); return d->m_address; } void MyMoneyPayee::setAddress(const QString& val) { Q_D(MyMoneyPayee); d->m_address = val; } QString MyMoneyPayee::city() const { Q_D(const MyMoneyPayee); return d->m_city; } void MyMoneyPayee::setCity(const QString& val) { Q_D(MyMoneyPayee); d->m_city = val; } QString MyMoneyPayee::state() const { Q_D(const MyMoneyPayee); return d->m_state; } void MyMoneyPayee::setState(const QString& val) { Q_D(MyMoneyPayee); d->m_state = val; } QString MyMoneyPayee::postcode() const { Q_D(const MyMoneyPayee); return d->m_postcode; } void MyMoneyPayee::setPostcode(const QString& val) { Q_D(MyMoneyPayee); d->m_postcode = val; } QString MyMoneyPayee::telephone() const { Q_D(const MyMoneyPayee); return d->m_telephone; } void MyMoneyPayee::setTelephone(const QString& val) { Q_D(MyMoneyPayee); d->m_telephone = val; } QString MyMoneyPayee::email() const { Q_D(const MyMoneyPayee); return d->m_email; } void MyMoneyPayee::setEmail(const QString& val) { Q_D(MyMoneyPayee); d->m_email = val; } QString MyMoneyPayee::notes() const { Q_D(const MyMoneyPayee); return d->m_notes; } void MyMoneyPayee::setNotes(const QString& val) { Q_D(MyMoneyPayee); d->m_notes = val; } QString MyMoneyPayee::reference() const { Q_D(const MyMoneyPayee); return d->m_reference; } void MyMoneyPayee::setReference(const QString& ref) { Q_D(MyMoneyPayee); d->m_reference = ref; } -MyMoneyPayee::payeeMatchType MyMoneyPayee::matchData(bool& ignorecase, QStringList& keys) const +eMyMoney::Payee::MatchType MyMoneyPayee::matchData(bool& ignorecase, QStringList& keys) const { - payeeMatchType type = matchDisabled; + auto type = eMyMoney::Payee::MatchType::Disabled; keys.clear(); Q_D(const MyMoneyPayee); ignorecase = d->m_matchKeyIgnoreCase; if (d->m_matchingEnabled) { - type = d->m_usingMatchKey ? matchKey : matchName; - if (type == matchKey) { + type = d->m_usingMatchKey ? eMyMoney::Payee::MatchType::Key : eMyMoney::Payee::MatchType::Name; + if (type == eMyMoney::Payee::MatchType::Key) { if (d->m_matchKey.contains(QLatin1Char('\n'))) keys = d->m_matchKey.split(QLatin1Char('\n')); else keys = d->m_matchKey.split(QLatin1Char(';')); // for compatibility with 4.8.0 } else if (d->m_matchKey.compare(QLatin1String("^$")) == 0) { - type = matchNameExact; + type = eMyMoney::Payee::MatchType::NameExact; } } return type; } -MyMoneyPayee::payeeMatchType MyMoneyPayee::matchData(bool& ignorecase, QString& keyString) const +eMyMoney::Payee::MatchType MyMoneyPayee::matchData(bool& ignorecase, QString& keyString) const { QStringList keys; - payeeMatchType type = matchData(ignorecase, keys); + auto type = matchData(ignorecase, keys); keyString = keys.join(QLatin1Char('\n')); return type; } -void MyMoneyPayee::setMatchData(payeeMatchType type, bool ignorecase, const QStringList& keys) +void MyMoneyPayee::setMatchData(eMyMoney::Payee::MatchType type, bool ignorecase, const QStringList& keys) { Q_D(MyMoneyPayee); - d->m_matchingEnabled = (type != matchDisabled); + d->m_matchingEnabled = (type != eMyMoney::Payee::MatchType::Disabled); d->m_matchKeyIgnoreCase = ignorecase; d->m_matchKey.clear(); if (d->m_matchingEnabled) { - d->m_usingMatchKey = (type == matchKey); + d->m_usingMatchKey = (type == eMyMoney::Payee::MatchType::Key); if (d->m_usingMatchKey) { QRegExp validKeyRegExp("[^ ]"); QStringList filteredKeys = keys.filter(validKeyRegExp); d->m_matchKey = filteredKeys.join(QLatin1Char('\n')); - } else if(type == matchNameExact) { + } else if(type == eMyMoney::Payee::MatchType::NameExact) { d->m_matchKey = QLatin1String("^$"); } } } -void MyMoneyPayee::setMatchData(payeeMatchType type, bool ignorecase, const QString& keys) +void MyMoneyPayee::setMatchData(eMyMoney::Payee::MatchType type, bool ignorecase, const QString& keys) { if (keys.contains(QLatin1Char('\n'))) setMatchData(type, ignorecase, keys.split(QLatin1Char('\n'))); else setMatchData(type, ignorecase, keys.split(QLatin1Char(';'))); // for compatibility with 4.8.0 } bool MyMoneyPayee::defaultAccountEnabled() const { Q_D(const MyMoneyPayee); return !d->m_defaultAccountId.isEmpty(); } QString MyMoneyPayee::defaultAccountId() const { Q_D(const MyMoneyPayee); return d->m_defaultAccountId; } void MyMoneyPayee::setDefaultAccountId(const QString& id) { Q_D(MyMoneyPayee); d->m_defaultAccountId = id; } void MyMoneyPayee::setDefaultAccountId() { setDefaultAccountId(QString()); } // vim:cin:si:ai:et:ts=2:sw=2: diff --git a/kmymoney/mymoney/mymoneypayee.h b/kmymoney/mymoney/mymoneypayee.h index ca2f39132..48992d85a 100644 --- a/kmymoney/mymoney/mymoneypayee.h +++ b/kmymoney/mymoney/mymoneypayee.h @@ -1,209 +1,205 @@ /* * Copyright 2000-2001 Michael Edwardes * Copyright 2002-2017 Thomas Baumgart * Copyright 2003 Kevin Tambascio * Copyright 2006 Ace Jones * 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 MYMONEYPAYEE_H #define MYMONEYPAYEE_H // ---------------------------------------------------------------------------- // QT Includes #include // ---------------------------------------------------------------------------- // Project Includes #include "kmm_mymoney_export.h" #include "mymoneyobject.h" #include "mymoneypayeeidentifiercontainer.h" class QString; class QStringList; +namespace eMyMoney { namespace Payee { enum class MatchType; } } + /** * This class represents a payee or receiver within the MyMoney engine. * Since it is not payee-specific, it is also used as a generic address * book entry. * * @author Thomas Baumgart */ class MyMoneyPayeePrivate; class KMM_MYMONEY_EXPORT MyMoneyPayee : public MyMoneyObject, public MyMoneyPayeeIdentifierContainer { Q_DECLARE_PRIVATE(MyMoneyPayee) KMM_MYMONEY_UNIT_TESTABLE public: - typedef enum { - matchDisabled = 0, - matchName, - matchKey, - matchNameExact - } payeeMatchType; - MyMoneyPayee(); + explicit MyMoneyPayee(const QString &id); explicit MyMoneyPayee(const QString& name, const QString& address = QString(), const QString& city = QString(), const QString& state = QString(), const QString& postcode = QString(), const QString& telephone = QString(), const QString& email = QString()); /** * This is the constructor for a payee that is described by a * QDomElement (e.g. from a file). * * @param el const reference to the QDomElement from which to * create the object */ explicit MyMoneyPayee(const QDomElement& node); MyMoneyPayee(const QString& id, const MyMoneyPayee& other); MyMoneyPayee(const MyMoneyPayee & other); MyMoneyPayee(MyMoneyPayee && other); MyMoneyPayee & operator=(MyMoneyPayee other); friend void swap(MyMoneyPayee& first, MyMoneyPayee& second); ~MyMoneyPayee() override; QString name() const; void setName(const QString& val); QString address() const; void setAddress(const QString& val); QString city() const; void setCity(const QString& val); QString state() const; void setState(const QString& val); QString postcode() const; void setPostcode(const QString& val); QString telephone() const; void setTelephone(const QString& val); QString email() const; void setEmail(const QString& val); QString notes() const; void setNotes(const QString& val); QString reference() const; void setReference(const QString& ref); /** * Get all match data in one call * * @param ignorecase Bool which will be replaced to indicate whether the match is * case-sensitive (false) or case-insensitive (true) * @param keys List of strings which will be replaced by the match key to use for this payee * * @return the matching type (see payeeMatchType for details) */ - payeeMatchType matchData(bool& ignorecase, QStringList& keys) const; + eMyMoney::Payee::MatchType matchData(bool& ignorecase, QStringList& keys) const; /** * Set all match data in one call * * @param type matching type (see payeeMatchType for details) * @param ignorecase Whether case should be ignored for the key/name match * @param keys A list of keys themselves, if applicable */ - void setMatchData(payeeMatchType type, bool ignorecase, const QStringList& keys); + void setMatchData(eMyMoney::Payee::MatchType type, bool ignorecase, const QStringList& keys); /** * Get all match data in one call (overloaded version for database module) * * @param ignorecase Bool which will be replaced to indicate whether the match is * case-sensitive (false) or case-insensitive (true) * @param keyString A list of keys in single-string format, if applicable * * @return the matching type (see payeeMatchType for details) */ - payeeMatchType matchData(bool& ignorecase, QString& keyString) const; + eMyMoney::Payee::MatchType matchData(bool& ignorecase, QString& keyString) const; /** * Set all match data in one call (overloaded version for database module) * * @param type matching type (see payeeMatchType for details) * @param ignorecase Whether case should be ignored for the key/name match * @param keys A list of keys in single-string format, if applicable */ - void setMatchData(payeeMatchType type, bool ignorecase, const QString& keys); + void setMatchData(eMyMoney::Payee::MatchType type, bool ignorecase, const QString& keys); bool defaultAccountEnabled() const; QString defaultAccountId() const; void setDefaultAccountId(const QString& id); void setDefaultAccountId(); // Equality operator bool operator == (const MyMoneyPayee &) const; // bool operator == (const MyMoneyPayee& lhs, const QString& rhs) const; bool operator <(const MyMoneyPayee& right) const; void writeXML(QDomDocument& document, QDomElement& parent) const override; /** * This method checks if a reference to the given object exists. It returns, * a @p true if the object is referencing the one requested by the * parameter @p id. If it does not, this method returns @p false. * * @param id id of the object to be checked for references * @retval true This object references object with id @p id. * @retval false This object does not reference the object with id @p id. */ bool hasReferenceTo(const QString& id) const override; static MyMoneyPayee null; }; inline void swap(MyMoneyPayee& first, MyMoneyPayee& second) // krazy:exclude=inline { using std::swap; swap(first.d_ptr, second.d_ptr); swap(first.m_payeeIdentifiers, second.m_payeeIdentifiers); } inline MyMoneyPayee::MyMoneyPayee(MyMoneyPayee && other) : MyMoneyPayee() // krazy:exclude=inline { swap(*this, other); } inline MyMoneyPayee & MyMoneyPayee::operator=(MyMoneyPayee other) // krazy:exclude=inline { swap(*this, other); return *this; } /** * Make it possible to hold @ref MyMoneyPayee objects inside @ref QVariant objects. */ Q_DECLARE_METATYPE(MyMoneyPayee) #endif diff --git a/kmymoney/mymoney/mymoneyreport.cpp b/kmymoney/mymoney/mymoneyreport.cpp index 818fdf80e..72989f2c7 100644 --- a/kmymoney/mymoney/mymoneyreport.cpp +++ b/kmymoney/mymoney/mymoneyreport.cpp @@ -1,1635 +1,1527 @@ /* * Copyright 2004-2006 Ace Jones * Copyright 2006 Darren Gould * Copyright 2007-2010 Alvaro Soliverez * 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 "mymoneyreport_p.h" // ---------------------------------------------------------------------------- // QT Includes #include #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes #include "mymoneymoney.h" #include "mymoneyfile.h" #include "mymoneyaccount.h" #include "mymoneystoragenames.h" #include "mymoneytransaction.h" #include "mymoneytransactionfilter.h" #include "mymoneyexception.h" using namespace MyMoneyStorageNodes; -// define this to debug reports -// #define DEBUG_REPORTS - -const QStringList MyMoneyReport::kRowTypeText = QString("none,assetliability,expenseincome,category,topcategory,account,tag,payee,month,week,topaccount,topaccount-account,equitytype,accounttype,institution,budget,budgetactual,schedule,accountinfo,accountloaninfo,accountreconcile,cashflow").split(','); -const QStringList MyMoneyReport::kColumnTypeText = QString("none,months,bimonths,quarters,4,5,6,weeks,8,9,10,11,years").split(','); - -// if you add names here, don't forget to update the bitmap for EQueryColumns -// and shift the bit for eQCend one position to the left -const QStringList MyMoneyReport::kQueryColumnsText = QString("none,number,payee,category,tag,memo,account,reconcileflag,action,shares,price,performance,loan,balance,capitalgain").split(','); - -const MyMoneyReport::EReportType MyMoneyReport::kTypeArray[] = { eNoReport, ePivotTable, ePivotTable, eQueryTable, eQueryTable, eQueryTable, eQueryTable, eQueryTable, eQueryTable, eQueryTable, eQueryTable, eQueryTable, eQueryTable, eQueryTable, eQueryTable, ePivotTable, ePivotTable, eInfoTable, eInfoTable, eInfoTable, eQueryTable, eQueryTable, eNoReport }; -const QStringList MyMoneyReport::kDetailLevelText = QString("none,all,top,group,total,invalid").split(','); -const QStringList MyMoneyReport::kChartTypeText = QString("none,line,bar,pie,ring,stackedbar").split(','); - -// This should live in mymoney/mymoneytransactionfilter.h -const QStringList kTypeText = QString("all,payments,deposits,transfers,none").split(','); -const QStringList kStateText = QString("all,notreconciled,cleared,reconciled,frozen,none").split(','); -const QStringList kDateLockText = QString("alldates,untiltoday,currentmonth,currentyear,monthtodate,yeartodate,yeartomonth,lastmonth,lastyear,last7days,last30days,last3months,last6months,last12months,next7days,next30days,next3months,next6months,next12months,userdefined,last3tonext3months,last11Months,currentQuarter,lastQuarter,nextQuarter,currentFiscalYear,lastFiscalYear,today,next18months").split(','); -const QStringList kDataLockText = QString("automatic,userdefined").split(','); -const QStringList kAccountTypeText = QString("unknown,checkings,savings,cash,creditcard,loan,certificatedep,investment,moneymarket,asset,liability,currency,income,expense,assetloan,stock,equity,invalid").split(','); - MyMoneyReport::MyMoneyReport() : MyMoneyObject(*new MyMoneyReportPrivate) { - Q_D(MyMoneyReport); - d->m_name = "Unconfigured Pivot Table Report"; - d->m_detailLevel = eDetailNone; - d->m_investmentSum = eSumSold; - d->m_hideTransactions = false; - d->m_convertCurrency = true; - d->m_favorite = false; - d->m_tax = false; - d->m_investments = false; - d->m_loans = false; - d->m_reportType = kTypeArray[eExpenseIncome]; - d->m_rowType = eExpenseIncome; - d->m_columnType = eMonths; - d->m_columnsAreDays = false; - d->m_queryColumns = eQCnone; - d->m_dateLock = TransactionFilter::Date::UserDefined; - d->m_accountGroupFilter = false; - d->m_chartType = eChartLine; - d->m_chartDataLabels = true; - d->m_chartCHGridLines = true; - d->m_chartSVGridLines = true; - d->m_chartByDefault = false; - d->m_logYaxis = false; - d->m_dataRangeStart = '0'; - d->m_dataRangeEnd = '0'; - d->m_dataMajorTick = '0'; - d->m_dataMinorTick = '0'; - d->m_yLabelsPrecision = 2; - d->m_dataLock = MyMoneyReport::automatic; - d->m_includeSchedules = false; - d->m_includeTransfers = false; - d->m_includeBudgetActuals = false; - d->m_includeUnusedAccounts = false; - d->m_showRowTotals = false; - d->m_showColumnTotals = true; - d->m_includeForecast = false; - d->m_includeMovingAverage = false; - d->m_movingAverageDays = 0; - d->m_includePrice = false; - d->m_includeAveragePrice = false; - d->m_mixedTime = false; - d->m_currentDateColumn = 0; - d->m_settlementPeriod = 3; - d->m_showSTLTCapitalGains = false; - d->m_tseparator = QDate::currentDate().addYears(-1); - d->m_skipZero = false; - d->m_chartLineWidth = m_lineWidth; } -MyMoneyReport::MyMoneyReport(ERowType rt, +MyMoneyReport::MyMoneyReport(const QString &id) : + MyMoneyObject(*new MyMoneyReportPrivate, id) +{ +} + +MyMoneyReport::MyMoneyReport(eMyMoney::Report::RowType rt, unsigned ct, - TransactionFilter::Date dl, - EDetailLevel ss, + eMyMoney::TransactionFilter::Date dl, + eMyMoney::Report::DetailLevel ss, const QString& name, const QString& comment) : MyMoneyObject(*new MyMoneyReportPrivate) { Q_D(MyMoneyReport); d->m_name = name; d->m_comment = comment; d->m_detailLevel = ss; - d->m_investmentSum = ct & eQCcapitalgain ? eSumSold : eSumPeriod; - d->m_hideTransactions = false; - d->m_convertCurrency = true; - d->m_favorite = false; - d->m_tax = false; - d->m_investments = false; - d->m_loans = false; - d->m_reportType = kTypeArray[rt]; + d->m_investmentSum = ct & eMyMoney::Report::QueryColumn::CapitalGain ? eMyMoney::Report::InvestmentSum::Sold : eMyMoney::Report::InvestmentSum::Period; + d->m_reportType = d->rowTypeToReportType(rt); d->m_rowType = rt; - d->m_columnType = eMonths; - d->m_columnsAreDays = false; - d->m_queryColumns = eQCnone; d->m_dateLock = dl; - d->m_accountGroupFilter = false; - d->m_chartType = eChartLine; - d->m_chartDataLabels = true; - d->m_chartCHGridLines = true; - d->m_chartSVGridLines = true; - d->m_chartByDefault = false; - d->m_logYaxis = false; - d->m_dataRangeStart = '0'; - d->m_dataRangeEnd = '0'; - d->m_dataMajorTick = '0'; - d->m_dataMinorTick = '0'; - d->m_yLabelsPrecision = 2; - d->m_dataLock = MyMoneyReport::automatic; - d->m_includeSchedules = false; - d->m_includeTransfers = false; - d->m_includeBudgetActuals = false; - d->m_includeUnusedAccounts = false; - d->m_showRowTotals = false; - d->m_showColumnTotals = true; - d->m_includeForecast = false; - d->m_includeMovingAverage = false; - d->m_movingAverageDays = 0; - d->m_includePrice = false; - d->m_includeAveragePrice = false; - d->m_mixedTime = false; - d->m_currentDateColumn = 0; - d->m_settlementPeriod = 3; - d->m_showSTLTCapitalGains = false; - d->m_tseparator = QDate::currentDate().addYears(-1); - d->m_skipZero = false; - - //set initial values - d->m_chartLineWidth = m_lineWidth; //set report type - if (d->m_reportType == ePivotTable) - d->m_columnType = static_cast(ct); - if (d->m_reportType == eQueryTable) - d->m_queryColumns = static_cast(ct); + if (d->m_reportType == eMyMoney::Report::ReportType::PivotTable) + d->m_columnType = static_cast(ct); + if (d->m_reportType == eMyMoney::Report::ReportType::QueryTable) + d->m_queryColumns = static_cast(ct); setDateFilter(dl); //throw exception if the type is inconsistent - if ((rt > static_cast(sizeof(kTypeArray) / sizeof(kTypeArray[0]))) - || (d->m_reportType == eNoReport)) + if (d->rowTypeToReportType(rt) == eMyMoney::Report::ReportType::Invalid || + d->m_reportType == eMyMoney::Report::ReportType::NoReport) throw MYMONEYEXCEPTION_CSTRING("Invalid report type"); //add the corresponding account groups - if (rt == MyMoneyReport::eAssetLiability) { - addAccountGroup(Account::Type::Asset); - addAccountGroup(Account::Type::Liability); + if (rt == eMyMoney::Report::RowType::AssetLiability) { + addAccountGroup(eMyMoney::Account::Type::Asset); + addAccountGroup(eMyMoney::Account::Type::Liability); d->m_showRowTotals = true; } - if (rt == MyMoneyReport::eAccount) { - addAccountGroup(Account::Type::Asset); - addAccountGroup(Account::Type::AssetLoan); - addAccountGroup(Account::Type::Cash); - addAccountGroup(Account::Type::Checkings); - addAccountGroup(Account::Type::CreditCard); + if (rt == eMyMoney::Report::RowType::Account) { + addAccountGroup(eMyMoney::Account::Type::Asset); + addAccountGroup(eMyMoney::Account::Type::AssetLoan); + addAccountGroup(eMyMoney::Account::Type::Cash); + addAccountGroup(eMyMoney::Account::Type::Checkings); + addAccountGroup(eMyMoney::Account::Type::CreditCard); if (m_expertMode) - addAccountGroup(Account::Type::Equity); - addAccountGroup(Account::Type::Expense); - addAccountGroup(Account::Type::Income); - addAccountGroup(Account::Type::Liability); - addAccountGroup(Account::Type::Loan); - addAccountGroup(Account::Type::Savings); - addAccountGroup(Account::Type::Stock); + addAccountGroup(eMyMoney::Account::Type::Equity); + addAccountGroup(eMyMoney::Account::Type::Expense); + addAccountGroup(eMyMoney::Account::Type::Income); + addAccountGroup(eMyMoney::Account::Type::Liability); + addAccountGroup(eMyMoney::Account::Type::Loan); + addAccountGroup(eMyMoney::Account::Type::Savings); + addAccountGroup(eMyMoney::Account::Type::Stock); d->m_showRowTotals = true; } - if (rt == MyMoneyReport::eExpenseIncome) { - addAccountGroup(Account::Type::Expense); - addAccountGroup(Account::Type::Income); + if (rt == eMyMoney::Report::RowType::ExpenseIncome) { + addAccountGroup(eMyMoney::Account::Type::Expense); + addAccountGroup(eMyMoney::Account::Type::Income); d->m_showRowTotals = true; } //FIXME take this out once we have sorted out all issues regarding budget of assets and liabilities -- asoliverez@gmail.com - if (rt == MyMoneyReport::eBudget || rt == MyMoneyReport::eBudgetActual) { - addAccountGroup(Account::Type::Expense); - addAccountGroup(Account::Type::Income); + if (rt == eMyMoney::Report::RowType::Budget || rt == eMyMoney::Report::RowType::BudgetActual) { + addAccountGroup(eMyMoney::Account::Type::Expense); + addAccountGroup(eMyMoney::Account::Type::Income); } - if (rt == MyMoneyReport::eAccountInfo) { - addAccountGroup(Account::Type::Asset); - addAccountGroup(Account::Type::Liability); + if (rt == eMyMoney::Report::RowType::AccountInfo) { + addAccountGroup(eMyMoney::Account::Type::Asset); + addAccountGroup(eMyMoney::Account::Type::Liability); } //cash flow reports show splits for all account groups - if (rt == MyMoneyReport::eCashFlow) { - addAccountGroup(Account::Type::Expense); - addAccountGroup(Account::Type::Income); - addAccountGroup(Account::Type::Asset); - addAccountGroup(Account::Type::Liability); + if (rt == eMyMoney::Report::RowType::CashFlow) { + addAccountGroup(eMyMoney::Account::Type::Expense); + addAccountGroup(eMyMoney::Account::Type::Income); + addAccountGroup(eMyMoney::Account::Type::Asset); + addAccountGroup(eMyMoney::Account::Type::Liability); } #ifdef DEBUG_REPORTS QDebug out = qDebug(); out << _name << toString(_rt) << toString(m_reportType); - foreach(const Account::Type accountType, m_accountGroups) - out << MyMoneyAccount::accountTypeToString(accountType); + foreach(const eMyMoney::Account::Type accountType, m_accountGroups) + out << MyMoneyeMyMoney::Account::accountTypeToString(accountType); if (m_accounts.size() > 0) out << m_accounts; #endif } MyMoneyReport::MyMoneyReport(const QDomElement& node) : MyMoneyObject(*new MyMoneyReportPrivate, node) { Q_D(MyMoneyReport); - d->m_currentDateColumn = 0; // properly initialize the object before reading it *this = MyMoneyReport(); if (!read(node)) clearId(); } MyMoneyReport::MyMoneyReport(const MyMoneyReport& other) : MyMoneyObject(*new MyMoneyReportPrivate(*other.d_func()), other.id()), MyMoneyTransactionFilter(other) { } MyMoneyReport::MyMoneyReport(const QString& id, const MyMoneyReport& other) : MyMoneyObject(*new MyMoneyReportPrivate(*other.d_func()), id), MyMoneyTransactionFilter(other) { Q_D(MyMoneyReport); d->m_movingAverageDays = 0; d->m_currentDateColumn = 0; } MyMoneyReport::~MyMoneyReport() { } -MyMoneyReport::EReportType MyMoneyReport::reportType() const +eMyMoney::Report::ReportType MyMoneyReport::reportType() const { Q_D(const MyMoneyReport); return d->m_reportType; } QString MyMoneyReport::name() const { Q_D(const MyMoneyReport); return d->m_name; } void MyMoneyReport::setName(const QString& s) { Q_D(MyMoneyReport); d->m_name = s; } bool MyMoneyReport::isShowingRowTotals() const { Q_D(const MyMoneyReport); return (d->m_showRowTotals); } void MyMoneyReport::setShowingRowTotals(bool f) { Q_D(MyMoneyReport); d->m_showRowTotals = f; } bool MyMoneyReport::isShowingColumnTotals() const { Q_D(const MyMoneyReport); return d->m_showColumnTotals; } void MyMoneyReport::setShowingColumnTotals(bool f) { Q_D(MyMoneyReport); d->m_showColumnTotals = f; } -MyMoneyReport::ERowType MyMoneyReport::rowType() const +eMyMoney::Report::RowType MyMoneyReport::rowType() const { Q_D(const MyMoneyReport); return d->m_rowType; } -void MyMoneyReport::setRowType(ERowType rt) +void MyMoneyReport::setRowType(eMyMoney::Report::RowType rt) { Q_D(MyMoneyReport); d->m_rowType = rt; - d->m_reportType = kTypeArray[rt]; + d->m_reportType = d->rowTypeToReportType(rt); d->m_accountGroupFilter = false; d->m_accountGroups.clear(); - if (rt == MyMoneyReport::eAssetLiability) { - addAccountGroup(Account::Type::Asset); - addAccountGroup(Account::Type::Liability); + if (rt == eMyMoney::Report::RowType::AssetLiability) { + addAccountGroup(eMyMoney::Account::Type::Asset); + addAccountGroup(eMyMoney::Account::Type::Liability); } - if (rt == MyMoneyReport::eExpenseIncome) { - addAccountGroup(Account::Type::Expense); - addAccountGroup(Account::Type::Income); + if (rt == eMyMoney::Report::RowType::ExpenseIncome) { + addAccountGroup(eMyMoney::Account::Type::Expense); + addAccountGroup(eMyMoney::Account::Type::Income); } } bool MyMoneyReport::isRunningSum() const { Q_D(const MyMoneyReport); - return (d->m_rowType == eAssetLiability); + return (d->m_rowType == eMyMoney::Report::RowType::AssetLiability); } -MyMoneyReport::EColumnType MyMoneyReport::columnType() const +eMyMoney::Report::ColumnType MyMoneyReport::columnType() const { Q_D(const MyMoneyReport); return d->m_columnType; } -void MyMoneyReport::setColumnType(EColumnType ct) +void MyMoneyReport::setColumnType(eMyMoney::Report::ColumnType ct) { Q_D(MyMoneyReport); d->m_columnType = ct; } bool MyMoneyReport::isConvertCurrency() const { Q_D(const MyMoneyReport); return d->m_convertCurrency; } void MyMoneyReport::setConvertCurrency(bool f) { Q_D(MyMoneyReport); d->m_convertCurrency = f; } uint MyMoneyReport::columnPitch() const { Q_D(const MyMoneyReport); return static_cast(d->m_columnType); } QString MyMoneyReport::comment() const { Q_D(const MyMoneyReport); return d->m_comment; } void MyMoneyReport::setComment(const QString& comment) { Q_D(MyMoneyReport); d->m_comment = comment; } -MyMoneyReport::EQueryColumns MyMoneyReport::queryColumns() const +eMyMoney::Report::QueryColumn MyMoneyReport::queryColumns() const { Q_D(const MyMoneyReport); return d->m_queryColumns; } -void MyMoneyReport::setQueryColumns(EQueryColumns qc) +void MyMoneyReport::setQueryColumns(eMyMoney::Report::QueryColumn qc) { Q_D(MyMoneyReport); d->m_queryColumns = qc; } QString MyMoneyReport::group() const { Q_D(const MyMoneyReport); return d->m_group; } void MyMoneyReport::setGroup(const QString& group) { Q_D(MyMoneyReport); d->m_group = group; } bool MyMoneyReport::isFavorite() const { Q_D(const MyMoneyReport); return d->m_favorite; } void MyMoneyReport::setFavorite(bool f) { Q_D(MyMoneyReport); d->m_favorite = f; } bool MyMoneyReport::isTax() const { Q_D(const MyMoneyReport); return d->m_tax; } void MyMoneyReport::setTax(bool f) { Q_D(MyMoneyReport); d->m_tax = f; } bool MyMoneyReport::isInvestmentsOnly() const { Q_D(const MyMoneyReport); return d->m_investments; } void MyMoneyReport::setInvestmentsOnly(bool f) { Q_D(MyMoneyReport); d->m_investments = f; if (f) d->m_loans = false; } bool MyMoneyReport::isLoansOnly() const { Q_D(const MyMoneyReport); return d->m_loans; } void MyMoneyReport::setLoansOnly(bool f) { Q_D(MyMoneyReport); d->m_loans = f; if (f) d->m_investments = false; } -MyMoneyReport::EDetailLevel MyMoneyReport::detailLevel() const +eMyMoney::Report::DetailLevel MyMoneyReport::detailLevel() const { Q_D(const MyMoneyReport); return d->m_detailLevel; } -void MyMoneyReport::setDetailLevel(EDetailLevel detail) +void MyMoneyReport::setDetailLevel(eMyMoney::Report::DetailLevel detail) { Q_D(MyMoneyReport); d->m_detailLevel = detail; } -MyMoneyReport::EInvestmentSum MyMoneyReport::investmentSum() const +eMyMoney::Report::InvestmentSum MyMoneyReport::investmentSum() const { Q_D(const MyMoneyReport); return d->m_investmentSum; } -void MyMoneyReport::setInvestmentSum(EInvestmentSum sum) +void MyMoneyReport::setInvestmentSum(eMyMoney::Report::InvestmentSum sum) { Q_D(MyMoneyReport); d->m_investmentSum = sum; } bool MyMoneyReport::isHideTransactions() const { Q_D(const MyMoneyReport); return d->m_hideTransactions; } void MyMoneyReport::setHideTransactions(bool f) { Q_D(MyMoneyReport); d->m_hideTransactions = f; } -MyMoneyReport::EChartType MyMoneyReport::chartType() const +eMyMoney::Report::ChartType MyMoneyReport::chartType() const { Q_D(const MyMoneyReport); return d->m_chartType; } -void MyMoneyReport::setChartType(EChartType type) +void MyMoneyReport::setChartType(eMyMoney::Report::ChartType type) { Q_D(MyMoneyReport); d->m_chartType = type; } bool MyMoneyReport::isChartDataLabels() const { Q_D(const MyMoneyReport); return d->m_chartDataLabels; } void MyMoneyReport::setChartDataLabels(bool f) { Q_D(MyMoneyReport); d->m_chartDataLabels = f; } bool MyMoneyReport::isChartCHGridLines() const { Q_D(const MyMoneyReport); return d->m_chartCHGridLines; } void MyMoneyReport::setChartCHGridLines(bool f) { Q_D(MyMoneyReport); d->m_chartCHGridLines = f; } bool MyMoneyReport::isChartSVGridLines() const { Q_D(const MyMoneyReport); return d->m_chartSVGridLines; } void MyMoneyReport::setChartSVGridLines(bool f) { Q_D(MyMoneyReport); d->m_chartSVGridLines = f; } bool MyMoneyReport::isChartByDefault() const { Q_D(const MyMoneyReport); return d->m_chartByDefault; } void MyMoneyReport::setChartByDefault(bool f) { Q_D(MyMoneyReport); d->m_chartByDefault = f; } uint MyMoneyReport::chartLineWidth() const { Q_D(const MyMoneyReport); return d->m_chartLineWidth; } void MyMoneyReport::setChartLineWidth(uint f) { Q_D(MyMoneyReport); d->m_chartLineWidth = f; } bool MyMoneyReport::isLogYAxis() const { Q_D(const MyMoneyReport); return d->m_logYaxis; } void MyMoneyReport::setLogYAxis(bool f) { Q_D(MyMoneyReport); d->m_logYaxis = f; } QString MyMoneyReport::dataRangeStart() const { Q_D(const MyMoneyReport); return d->m_dataRangeStart; } void MyMoneyReport::setDataRangeStart(const QString& f) { Q_D(MyMoneyReport); d->m_dataRangeStart = f; } QString MyMoneyReport::dataRangeEnd() const { Q_D(const MyMoneyReport); return d->m_dataRangeEnd; } void MyMoneyReport::setDataRangeEnd(const QString& f) { Q_D(MyMoneyReport); d->m_dataRangeEnd = f; } QString MyMoneyReport::dataMajorTick() const { Q_D(const MyMoneyReport); return d->m_dataMajorTick; } void MyMoneyReport::setDataMajorTick(const QString& f) { Q_D(MyMoneyReport); d->m_dataMajorTick = f; } QString MyMoneyReport::dataMinorTick() const { Q_D(const MyMoneyReport); return d->m_dataMinorTick; } void MyMoneyReport::setDataMinorTick(const QString& f) { Q_D(MyMoneyReport); d->m_dataMinorTick = f; } uint MyMoneyReport::yLabelsPrecision() const { Q_D(const MyMoneyReport); return d->m_yLabelsPrecision; } void MyMoneyReport::setYLabelsPrecision(int f) { Q_D(MyMoneyReport); d->m_yLabelsPrecision = f; } bool MyMoneyReport::isIncludingSchedules() const { Q_D(const MyMoneyReport); return d->m_includeSchedules; } void MyMoneyReport::setIncludingSchedules(bool f) { Q_D(MyMoneyReport); d->m_includeSchedules = f; } bool MyMoneyReport::isColumnsAreDays() const { Q_D(const MyMoneyReport); return d->m_columnsAreDays; } void MyMoneyReport::setColumnsAreDays(bool f) { Q_D(MyMoneyReport); d->m_columnsAreDays = f; } bool MyMoneyReport::isIncludingTransfers() const { Q_D(const MyMoneyReport); return d->m_includeTransfers; } void MyMoneyReport::setIncludingTransfers(bool f) { Q_D(MyMoneyReport); d->m_includeTransfers = f; } bool MyMoneyReport::isIncludingUnusedAccounts() const { Q_D(const MyMoneyReport); return d->m_includeUnusedAccounts; } void MyMoneyReport::setIncludingUnusedAccounts(bool f) { Q_D(MyMoneyReport); d->m_includeUnusedAccounts = f; } bool MyMoneyReport::hasBudget() const { Q_D(const MyMoneyReport); return !d->m_budgetId.isEmpty(); } QString MyMoneyReport::budget() const { Q_D(const MyMoneyReport); return d->m_budgetId; } /** * Sets the budget used for this report * * @param budget The ID of the budget to use, or an empty string * to indicate a budget is NOT included * @param fa Whether to display actual data alongside the budget. * Setting to false means the report displays ONLY the budget itself. * @warning For now, the budget ID is ignored. The budget id is * simply checked for any non-empty string, and if so, hasBudget() * will return true. */ void MyMoneyReport::setBudget(const QString& budget, bool fa) { Q_D(MyMoneyReport); d->m_budgetId = budget; d->m_includeBudgetActuals = fa; } bool MyMoneyReport::isIncludingBudgetActuals() const { Q_D(const MyMoneyReport); return d->m_includeBudgetActuals; } void MyMoneyReport::setIncludingBudgetActuals(bool f) { Q_D(MyMoneyReport); d->m_includeBudgetActuals = f; } bool MyMoneyReport::isIncludingForecast() const { Q_D(const MyMoneyReport); return d->m_includeForecast; } void MyMoneyReport::setIncludingForecast(bool f) { Q_D(MyMoneyReport); d->m_includeForecast = f; } bool MyMoneyReport::isIncludingMovingAverage() const { Q_D(const MyMoneyReport); return d->m_includeMovingAverage; } void MyMoneyReport::setIncludingMovingAverage(bool f) { Q_D(MyMoneyReport); d->m_includeMovingAverage = f; } int MyMoneyReport::movingAverageDays() const { Q_D(const MyMoneyReport); return d->m_movingAverageDays; } void MyMoneyReport::setMovingAverageDays(int days) { Q_D(MyMoneyReport); d->m_movingAverageDays = days; } bool MyMoneyReport::isIncludingPrice() const { Q_D(const MyMoneyReport); return d->m_includePrice; } void MyMoneyReport::setIncludingPrice(bool f) { Q_D(MyMoneyReport); d->m_includePrice = f; } bool MyMoneyReport::isIncludingAveragePrice() const { Q_D(const MyMoneyReport); return d->m_includeAveragePrice; } void MyMoneyReport::setIncludingAveragePrice(bool f) { Q_D(MyMoneyReport); d->m_includeAveragePrice = f; } -MyMoneyReport::dataOptionE MyMoneyReport::dataFilter() const +eMyMoney::Report::DataLock MyMoneyReport::dataFilter() const { Q_D(const MyMoneyReport); return d->m_dataLock; } bool MyMoneyReport::isDataUserDefined() const { Q_D(const MyMoneyReport); - return d->m_dataLock == MyMoneyReport::userDefined; + return d->m_dataLock == eMyMoney::Report::DataLock::UserDefined; } -void MyMoneyReport::setDataFilter(dataOptionE u) +void MyMoneyReport::setDataFilter(eMyMoney::Report::DataLock u) { Q_D(MyMoneyReport); d->m_dataLock = u; } eMyMoney::TransactionFilter::Date MyMoneyReport::dateRange() const { Q_D(const MyMoneyReport); return d->m_dateLock; } bool MyMoneyReport::isDateUserDefined() const { Q_D(const MyMoneyReport); - return d->m_dateLock == TransactionFilter::Date::UserDefined; + return d->m_dateLock == eMyMoney::TransactionFilter::Date::UserDefined; } /** * Set the underlying date filter and LOCK that filter to the specified * range. For example, if @p _u is "CurrentMonth", this report should always * be updated to the current month no matter when the report is run. * * This updating is not entirely automatic, you should update it yourself by * calling updateDateFilter. * * @param _u The date range constant (MyMoneyTransactionFilter::dateRangeE) * which this report should be locked to. */ -void MyMoneyReport::setDateFilter(TransactionFilter::Date u) +void MyMoneyReport::setDateFilter(eMyMoney::TransactionFilter::Date u) { Q_D(MyMoneyReport); d->m_dateLock = u; - if (u != TransactionFilter::Date::UserDefined) + if (u != eMyMoney::TransactionFilter::Date::UserDefined) MyMoneyTransactionFilter::setDateFilter(u); } void MyMoneyReport::setDateFilter(const QDate& db, const QDate& de) { MyMoneyTransactionFilter::setDateFilter(db, de); } void MyMoneyReport::updateDateFilter() { Q_D(MyMoneyReport); - if (d->m_dateLock != TransactionFilter::Date::UserDefined) MyMoneyTransactionFilter::setDateFilter(d->m_dateLock); + if (d->m_dateLock != eMyMoney::TransactionFilter::Date::UserDefined) MyMoneyTransactionFilter::setDateFilter(d->m_dateLock); } bool MyMoneyReport::isMixedTime() const { Q_D(const MyMoneyReport); return d->m_mixedTime; } void MyMoneyReport::setMixedTime(bool f) { Q_D(MyMoneyReport); d->m_mixedTime = f; } int MyMoneyReport::currentDateColumn() const { Q_D(const MyMoneyReport); return d->m_currentDateColumn; } void MyMoneyReport::setCurrentDateColumn(int f) { Q_D(MyMoneyReport); d->m_currentDateColumn = f; } uint MyMoneyReport::settlementPeriod() const { Q_D(const MyMoneyReport); return d->m_settlementPeriod; } void MyMoneyReport::setSettlementPeriod(uint days) { Q_D(MyMoneyReport); d->m_settlementPeriod = days; } bool MyMoneyReport::isShowingSTLTCapitalGains() const { Q_D(const MyMoneyReport); return d->m_showSTLTCapitalGains; } void MyMoneyReport::setShowSTLTCapitalGains(bool f) { Q_D(MyMoneyReport); d->m_showSTLTCapitalGains = f; } QDate MyMoneyReport::termSeparator() const { Q_D(const MyMoneyReport); return d->m_tseparator; } void MyMoneyReport::setTermSeparator(const QDate& date) { Q_D(MyMoneyReport); d->m_tseparator = date; } bool MyMoneyReport::isSkippingZero() const { Q_D(const MyMoneyReport); return d->m_skipZero; } void MyMoneyReport::setSkipZero(int f) { Q_D(MyMoneyReport); d->m_skipZero = f; } void MyMoneyReport::clearTransactionFilter() { Q_D(MyMoneyReport); d->m_accountGroupFilter = false; d->m_accountGroups.clear(); MyMoneyTransactionFilter::clear(); } void MyMoneyReport::assignFilter(const MyMoneyTransactionFilter& filter) { MyMoneyTransactionFilter::operator=(filter); } void MyMoneyReport::validDateRange(QDate& db, QDate& de) { db = fromDate(); de = toDate(); // if either begin or end date are invalid we have one of the following // possible date filters: // // a) begin date not set - first transaction until given end date // b) end date not set - from given date until last transaction // c) both not set - first transaction until last transaction // // If there is no transaction in the engine at all, we use the current // year as the filter criteria. if (!db.isValid() || !de.isValid()) { QList list = MyMoneyFile::instance()->transactionList(*this); QDate tmpBegin, tmpEnd; if (!list.isEmpty()) { qSort(list); // try to use the post dates tmpBegin = list.front().postDate(); tmpEnd = list.back().postDate(); // if the post dates are not valid try the entry dates if (!tmpBegin.isValid()) tmpBegin = list.front().entryDate(); if (!tmpEnd.isValid()) tmpEnd = list.back().entryDate(); } // make sure that we leave this function with valid dates no mather what if (!tmpBegin.isValid() || !tmpEnd.isValid() || tmpBegin > tmpEnd) { tmpBegin = QDate(QDate::currentDate().year(), 1, 1); // the first date in the file tmpEnd = QDate(QDate::currentDate().year(), 12, 31); // the last date in the file } if (!db.isValid()) db = tmpBegin; if (!de.isValid()) de = tmpEnd; } if (db > de) db = de; } -bool MyMoneyReport::accountGroups(QList& list) const +bool MyMoneyReport::accountGroups(QList& list) const { Q_D(const MyMoneyReport); bool result = d->m_accountGroupFilter; if (result) { - QList::const_iterator it_group = d->m_accountGroups.begin(); + QList::const_iterator it_group = d->m_accountGroups.begin(); while (it_group != d->m_accountGroups.end()) { list += (*it_group); ++it_group; } } return result; } -void MyMoneyReport::addAccountGroup(Account::Type type) +void MyMoneyReport::addAccountGroup(eMyMoney::Account::Type type) { Q_D(MyMoneyReport); - if (!d->m_accountGroups.isEmpty() && type != Account::Type::Unknown) { + if (!d->m_accountGroups.isEmpty() && type != eMyMoney::Account::Type::Unknown) { if (d->m_accountGroups.contains(type)) return; } d->m_accountGroupFilter = true; - if (type != Account::Type::Unknown) + if (type != eMyMoney::Account::Type::Unknown) d->m_accountGroups.push_back(type); } -bool MyMoneyReport::includesAccountGroup(Account::Type type) const +bool MyMoneyReport::includesAccountGroup(eMyMoney::Account::Type type) const { Q_D(const MyMoneyReport); bool result = (! d->m_accountGroupFilter) - || (isIncludingTransfers() && d->m_rowType == MyMoneyReport::eExpenseIncome) + || (isIncludingTransfers() && d->m_rowType == eMyMoney::Report::RowType::ExpenseIncome) || d->m_accountGroups.contains(type); return result; } bool MyMoneyReport::includes(const MyMoneyAccount& acc) const { Q_D(const MyMoneyReport); auto result = false; if (includesAccountGroup(acc.accountGroup())) { switch (acc.accountGroup()) { - case Account::Type::Income: - case Account::Type::Expense: + case eMyMoney::Account::Type::Income: + case eMyMoney::Account::Type::Expense: if (isTax()) result = (acc.value("Tax") == "Yes") && includesCategory(acc.id()); else result = includesCategory(acc.id()); break; - case Account::Type::Asset: - case Account::Type::Liability: + case eMyMoney::Account::Type::Asset: + case eMyMoney::Account::Type::Liability: if (isLoansOnly()) result = acc.isLoan() && includesAccount(acc.id()); else if (isInvestmentsOnly()) result = acc.isInvest() && includesAccount(acc.id()); - else if (isIncludingTransfers() && d->m_rowType == MyMoneyReport::eExpenseIncome) + else if (isIncludingTransfers() && d->m_rowType == eMyMoney::Report::RowType::ExpenseIncome) // If transfers are included, ONLY include this account if it is NOT // included in the report itself!! result = ! includesAccount(acc.id()); else result = includesAccount(acc.id()); break; - case Account::Type::Equity: + case eMyMoney::Account::Type::Equity: if (isInvestmentsOnly()) result = (isIncludingPrice() || isIncludingAveragePrice()) && acc.isInvest() && includesAccount(acc.id()); break; default: result = includesAccount(acc.id()); } } return result; } void MyMoneyReport::write(QDomElement& e, QDomDocument *doc, bool anonymous) const { Q_D(const MyMoneyReport); // No matter what changes, be sure to have a 'type' attribute. Only change // the major type if it becomes impossible to maintain compatibility with // older versions of the program as new features are added to the reports. // Feel free to change the minor type every time a change is made here. // write report's internals - if (d->m_reportType == ePivotTable) + if (d->m_reportType == eMyMoney::Report::ReportType::PivotTable) e.setAttribute(d->getAttrName(Report::Attribute::Type), "pivottable 1.15"); - else if (d->m_reportType == eQueryTable) + else if (d->m_reportType == eMyMoney::Report::ReportType::QueryTable) e.setAttribute(d->getAttrName(Report::Attribute::Type), "querytable 1.14"); - else if (d->m_reportType == eInfoTable) + else if (d->m_reportType == eMyMoney::Report::ReportType::InfoTable) e.setAttribute(d->getAttrName(Report::Attribute::Type), "infotable 1.0"); e.setAttribute(d->getAttrName(Report::Attribute::Group), d->m_group); e.setAttribute(d->getAttrName(Report::Attribute::ID), d->m_id); // write general tab if (anonymous) { e.setAttribute(d->getAttrName(Report::Attribute::Name), d->m_id); e.setAttribute(d->getAttrName(Report::Attribute::Comment), QString(d->m_comment).fill('x')); } else { e.setAttribute(d->getAttrName(Report::Attribute::Name), d->m_name); e.setAttribute(d->getAttrName(Report::Attribute::Comment), d->m_comment); } e.setAttribute(d->getAttrName(Report::Attribute::ConvertCurrency), d->m_convertCurrency); e.setAttribute(d->getAttrName(Report::Attribute::Favorite), d->m_favorite); e.setAttribute(d->getAttrName(Report::Attribute::SkipZero), d->m_skipZero); - e.setAttribute(d->getAttrName(Report::Attribute::DateLock), kDateLockText[(int)d->m_dateLock]); + e.setAttribute(d->getAttrName(Report::Attribute::DateLock), d->dateLockAttributeToString(static_cast(d->m_dateLock))); - if (d->m_reportType == ePivotTable) { + if (d->m_reportType == eMyMoney::Report::ReportType::PivotTable) { // write report's internals e.setAttribute(d->getAttrName(Report::Attribute::IncludesActuals), d->m_includeBudgetActuals); e.setAttribute(d->getAttrName(Report::Attribute::IncludesForecast), d->m_includeForecast); e.setAttribute(d->getAttrName(Report::Attribute::IncludesPrice), d->m_includePrice); e.setAttribute(d->getAttrName(Report::Attribute::IncludesAveragePrice), d->m_includeAveragePrice); e.setAttribute(d->getAttrName(Report::Attribute::MixedTime), d->m_mixedTime); e.setAttribute(d->getAttrName(Report::Attribute::Investments), d->m_investments); // it's setable in rows/columns tab of querytable, but here it is internal setting // write rows/columns tab if (!d->m_budgetId.isEmpty()) e.setAttribute(d->getAttrName(Report::Attribute::Budget), d->m_budgetId); - e.setAttribute(d->getAttrName(Report::Attribute::RowType), kRowTypeText[d->m_rowType]); + e.setAttribute(d->getAttrName(Report::Attribute::RowType), d->reportNames(d->m_rowType)); e.setAttribute(d->getAttrName(Report::Attribute::ShowRowTotals), d->m_showRowTotals); e.setAttribute(d->getAttrName(Report::Attribute::ShowColumnTotals), d->m_showColumnTotals); - e.setAttribute(d->getAttrName(Report::Attribute::Detail), kDetailLevelText[d->m_detailLevel]); + e.setAttribute(d->getAttrName(Report::Attribute::Detail), d->reportNames(d->m_detailLevel)); e.setAttribute(d->getAttrName(Report::Attribute::IncludesMovingAverage), d->m_includeMovingAverage); if (d->m_includeMovingAverage) e.setAttribute(d->getAttrName(Report::Attribute::MovingAverageDays), d->m_movingAverageDays); e.setAttribute(d->getAttrName(Report::Attribute::IncludesSchedules), d->m_includeSchedules); e.setAttribute(d->getAttrName(Report::Attribute::IncludesTransfers), d->m_includeTransfers); e.setAttribute(d->getAttrName(Report::Attribute::IncludesUnused), d->m_includeUnusedAccounts); e.setAttribute(d->getAttrName(Report::Attribute::ColumnsAreDays), d->m_columnsAreDays); - - // write chart tab - if (d->m_chartType < 0 || d->m_chartType >= kChartTypeText.size()) { - qDebug("m_chartType out of bounds with %d on report of type %d. Default to none.", d->m_chartType, d->m_reportType); - e.setAttribute(d->getAttrName(Report::Attribute::ChartType), kChartTypeText[eChartNone]); - } else - e.setAttribute(d->getAttrName(Report::Attribute::ChartType), kChartTypeText[d->m_chartType]); - + e.setAttribute(d->getAttrName(Report::Attribute::ChartType), d->reportNames(d->m_chartType)); e.setAttribute(d->getAttrName(Report::Attribute::ChartCHGridLines), d->m_chartCHGridLines); e.setAttribute(d->getAttrName(Report::Attribute::ChartSVGridLines), d->m_chartSVGridLines); e.setAttribute(d->getAttrName(Report::Attribute::ChartDataLabels), d->m_chartDataLabels); e.setAttribute(d->getAttrName(Report::Attribute::ChartByDefault), d->m_chartByDefault); e.setAttribute(d->getAttrName(Report::Attribute::LogYAxis), d->m_logYaxis); e.setAttribute(d->getAttrName(Report::Attribute::ChartLineWidth), d->m_chartLineWidth); - e.setAttribute(d->getAttrName(Report::Attribute::ColumnType), kColumnTypeText[d->m_columnType]); - e.setAttribute(d->getAttrName(Report::Attribute::DataLock), kDataLockText[d->m_dataLock]); + e.setAttribute(d->getAttrName(Report::Attribute::ColumnType), d->reportNames(d->m_columnType)); + e.setAttribute(d->getAttrName(Report::Attribute::DataLock), d->reportNames(d->m_dataLock)); e.setAttribute(d->getAttrName(Report::Attribute::DataRangeStart), d->m_dataRangeStart); e.setAttribute(d->getAttrName(Report::Attribute::DataRangeEnd), d->m_dataRangeEnd); e.setAttribute(d->getAttrName(Report::Attribute::DataMajorTick), d->m_dataMajorTick); e.setAttribute(d->getAttrName(Report::Attribute::DataMinorTick), d->m_dataMinorTick); e.setAttribute(d->getAttrName(Report::Attribute::YLabelsPrecision), d->m_yLabelsPrecision); - } else if (d->m_reportType == eQueryTable) { + } else if (d->m_reportType == eMyMoney::Report::ReportType::QueryTable) { // write rows/columns tab - e.setAttribute(d->getAttrName(Report::Attribute::RowType), kRowTypeText[d->m_rowType]); + e.setAttribute(d->getAttrName(Report::Attribute::RowType), d->reportNames(d->m_rowType)); QStringList columns; unsigned qc = d->m_queryColumns; - unsigned it_qc = eQCbegin; + unsigned it_qc = eMyMoney::Report::QueryColumn::Begin; unsigned index = 1; - while (it_qc != eQCend) { + while (it_qc != eMyMoney::Report::QueryColumn::End) { if (qc & it_qc) - columns += kQueryColumnsText[index]; + columns += d->reportNamesForQC(static_cast(it_qc)); it_qc *= 2; index++; } e.setAttribute(d->getAttrName(Report::Attribute::QueryColumns), columns.join(",")); e.setAttribute(d->getAttrName(Report::Attribute::Tax), d->m_tax); e.setAttribute(d->getAttrName(Report::Attribute::Investments), d->m_investments); e.setAttribute(d->getAttrName(Report::Attribute::Loans), d->m_loans); e.setAttribute(d->getAttrName(Report::Attribute::HideTransactions), d->m_hideTransactions); e.setAttribute(d->getAttrName(Report::Attribute::ShowColumnTotals), d->m_showColumnTotals); - e.setAttribute(d->getAttrName(Report::Attribute::Detail), kDetailLevelText[d->m_detailLevel]); + e.setAttribute(d->getAttrName(Report::Attribute::Detail), d->reportNames(d->m_detailLevel)); // write performance tab - if (d->m_queryColumns & eQCperformance || d->m_queryColumns & eQCcapitalgain) - e.setAttribute(d->getAttrName(Report::Attribute::InvestmentSum), d->m_investmentSum); + if (d->m_queryColumns & eMyMoney::Report::QueryColumn::Performance || d->m_queryColumns & eMyMoney::Report::QueryColumn::CapitalGain) + e.setAttribute(d->getAttrName(Report::Attribute::InvestmentSum), static_cast(d->m_investmentSum)); // write capital gains tab - if (d->m_queryColumns & eQCcapitalgain) { - if (d->m_investmentSum == MyMoneyReport::eSumSold) { + if (d->m_queryColumns & eMyMoney::Report::QueryColumn::CapitalGain) { + if (d->m_investmentSum == eMyMoney::Report::InvestmentSum::Sold) { e.setAttribute(d->getAttrName(Report::Attribute::SettlementPeriod), d->m_settlementPeriod); e.setAttribute(d->getAttrName(Report::Attribute::ShowSTLTCapitalGains), d->m_showSTLTCapitalGains); e.setAttribute(d->getAttrName(Report::Attribute::TermsSeparator), d->m_tseparator.toString(Qt::ISODate)); } } - } else if (d->m_reportType == eInfoTable) + } else if (d->m_reportType == eMyMoney::Report::ReportType::InfoTable) e.setAttribute(d->getAttrName(Report::Attribute::ShowRowTotals), d->m_showRowTotals); // // Text Filter // QRegExp textfilter; if (textFilter(textfilter)) { QDomElement f = doc->createElement(d->getElName(Report::Element::Text)); f.setAttribute(d->getAttrName(Report::Attribute::Pattern), textfilter.pattern()); f.setAttribute(d->getAttrName(Report::Attribute::CaseSensitive), (textfilter.caseSensitivity() == Qt::CaseSensitive) ? 1 : 0); f.setAttribute(d->getAttrName(Report::Attribute::RegEx), (textfilter.patternSyntax() == QRegExp::Wildcard) ? 1 : 0); f.setAttribute(d->getAttrName(Report::Attribute::InvertText), MyMoneyTransactionFilter::isInvertingText()); e.appendChild(f); } // // Type & State Filters // QList typelist; if (types(typelist) && ! typelist.empty()) { // iterate over payees, and add each one QList::const_iterator it_type = typelist.constBegin(); while (it_type != typelist.constEnd()) { QDomElement p = doc->createElement(d->getElName(Report::Element::Type)); - p.setAttribute(d->getAttrName(Report::Attribute::Type), kTypeText[*it_type]); + p.setAttribute(d->getAttrName(Report::Attribute::Type), d->typeAttributeToString(*it_type)); e.appendChild(p); ++it_type; } } QList statelist; if (states(statelist) && ! statelist.empty()) { // iterate over payees, and add each one QList::const_iterator it_state = statelist.constBegin(); while (it_state != statelist.constEnd()) { QDomElement p = doc->createElement(d->getElName(Report::Element::State)); - p.setAttribute(d->getAttrName(Report::Attribute::State), kStateText[*it_state]); + p.setAttribute(d->getAttrName(Report::Attribute::State), d->stateAttributeToString(*it_state)); e.appendChild(p); ++it_state; } } // // Number Filter // QString nrFrom, nrTo; if (numberFilter(nrFrom, nrTo)) { QDomElement f = doc->createElement(d->getElName(Report::Element::Number)); f.setAttribute(d->getAttrName(Report::Attribute::From), nrFrom); f.setAttribute(d->getAttrName(Report::Attribute::To), nrTo); e.appendChild(f); } // // Amount Filter // MyMoneyMoney from, to; if (amountFilter(from, to)) { // bool getAmountFilter(MyMoneyMoney&,MyMoneyMoney&); QDomElement f = doc->createElement(d->getElName(Report::Element::Amount)); f.setAttribute(d->getAttrName(Report::Attribute::From), from.toString()); f.setAttribute(d->getAttrName(Report::Attribute::To), to.toString()); e.appendChild(f); } // // Payees Filter // QStringList payeelist; if (payees(payeelist)) { if (payeelist.empty()) { QDomElement p = doc->createElement(d->getElName(Report::Element::Payee)); e.appendChild(p); } else { // iterate over payees, and add each one QStringList::const_iterator it_payee = payeelist.constBegin(); while (it_payee != payeelist.constEnd()) { QDomElement p = doc->createElement(d->getElName(Report::Element::Payee)); p.setAttribute(d->getAttrName(Report::Attribute::ID), *it_payee); e.appendChild(p); ++it_payee; } } } // // Tags Filter // QStringList taglist; if (tags(taglist)) { if (taglist.empty()) { QDomElement p = doc->createElement(d->getElName(Report::Element::Tag)); e.appendChild(p); } else { // iterate over tags, and add each one QStringList::const_iterator it_tag = taglist.constBegin(); while (it_tag != taglist.constEnd()) { QDomElement p = doc->createElement(d->getElName(Report::Element::Tag)); p.setAttribute(d->getAttrName(Report::Attribute::ID), *it_tag); e.appendChild(p); ++it_tag; } } } // // Account Groups Filter // - QList accountgrouplist; + QList accountgrouplist; if (accountGroups(accountgrouplist)) { // iterate over accounts, and add each one - QList::const_iterator it_group = accountgrouplist.constBegin(); + QList::const_iterator it_group = accountgrouplist.constBegin(); while (it_group != accountgrouplist.constEnd()) { QDomElement p = doc->createElement(d->getElName(Report::Element::AccountGroup)); - p.setAttribute(d->getAttrName(Report::Attribute::Group), kAccountTypeText[(int)*it_group]); + p.setAttribute(d->getAttrName(Report::Attribute::Group), d->accountTypeAttributeToString(static_cast(*it_group))); e.appendChild(p); ++it_group; } } // // Accounts Filter // QStringList accountlist; if (accounts(accountlist)) { // iterate over accounts, and add each one QStringList::const_iterator it_account = accountlist.constBegin(); while (it_account != accountlist.constEnd()) { QDomElement p = doc->createElement(d->getElName(Report::Element::Account)); p.setAttribute(d->getAttrName(Report::Attribute::ID), *it_account); e.appendChild(p); ++it_account; } } // // Categories Filter // accountlist.clear(); if (categories(accountlist)) { // iterate over accounts, and add each one QStringList::const_iterator it_account = accountlist.constBegin(); while (it_account != accountlist.constEnd()) { QDomElement p = doc->createElement(d->getElName(Report::Element::Category)); p.setAttribute(d->getAttrName(Report::Attribute::ID), *it_account); e.appendChild(p); ++it_account; } } // // Date Filter // - if (d->m_dateLock == TransactionFilter::Date::UserDefined) { + if (d->m_dateLock == eMyMoney::TransactionFilter::Date::UserDefined) { QDate dateFrom, dateTo; if (dateFilter(dateFrom, dateTo)) { QDomElement f = doc->createElement(d->getElName(Report::Element::Dates)); if (dateFrom.isValid()) f.setAttribute(d->getAttrName(Report::Attribute::From), dateFrom.toString(Qt::ISODate)); if (dateTo.isValid()) f.setAttribute(d->getAttrName(Report::Attribute::To), dateTo.toString(Qt::ISODate)); e.appendChild(f); } } } bool MyMoneyReport::read(const QDomElement& e) { Q_D(MyMoneyReport); // The goal of this reading method is 100% backward AND 100% forward // compatibility. Any report ever created with any version of KMyMoney // should be able to be loaded by this method (as long as it's one of the // report types supported in this version, of course) if (e.tagName().compare(nodeNames[nnReport]) != 0) return false; // read report's internals QString type = e.attribute(d->getAttrName(Report::Attribute::Type)); if (type.startsWith(QLatin1String("pivottable"))) - d->m_reportType = ePivotTable; + d->m_reportType = eMyMoney::Report::ReportType::PivotTable; else if (type.startsWith(QLatin1String("querytable"))) - d->m_reportType = eQueryTable; + d->m_reportType = eMyMoney::Report::ReportType::QueryTable; else if (type.startsWith(QLatin1String("infotable"))) - d->m_reportType = eInfoTable; + d->m_reportType = eMyMoney::Report::ReportType::InfoTable; else return false; d->m_group = e.attribute(d->getAttrName(Report::Attribute::Group)); d->m_id = e.attribute(d->getAttrName(Report::Attribute::ID)); clearTransactionFilter(); // read date tab QString datelockstr = e.attribute(d->getAttrName(Report::Attribute::DateLock), "userdefined"); // Handle the pivot 1.2/query 1.1 case where the values were saved as // numbers bool ok = false; int i = datelockstr.toUInt(&ok); if (!ok) { - i = kDateLockText.indexOf(datelockstr); + i = d->stringToDateLockAttribute(datelockstr); if (i == -1) - i = (int)TransactionFilter::Date::UserDefined; + i = (int)eMyMoney::TransactionFilter::Date::UserDefined; } - setDateFilter(static_cast(i)); + setDateFilter(static_cast(i)); // read general tab d->m_name = e.attribute(d->getAttrName(Report::Attribute::Name)); d->m_comment = e.attribute(d->getAttrName(Report::Attribute::Comment), "Extremely old report"); d->m_convertCurrency = e.attribute(d->getAttrName(Report::Attribute::ConvertCurrency), "1").toUInt(); d->m_favorite = e.attribute(d->getAttrName(Report::Attribute::Favorite), "0").toUInt(); d->m_skipZero = e.attribute(d->getAttrName(Report::Attribute::SkipZero), "0").toUInt(); - if (d->m_reportType == ePivotTable) { + if (d->m_reportType == eMyMoney::Report::ReportType::PivotTable) { // read report's internals d->m_includeBudgetActuals = e.attribute(d->getAttrName(Report::Attribute::IncludesActuals), "0").toUInt(); d->m_includeForecast = e.attribute(d->getAttrName(Report::Attribute::IncludesForecast), "0").toUInt(); d->m_includePrice = e.attribute(d->getAttrName(Report::Attribute::IncludesPrice), "0").toUInt(); d->m_includeAveragePrice = e.attribute(d->getAttrName(Report::Attribute::IncludesAveragePrice), "0").toUInt(); d->m_mixedTime = e.attribute(d->getAttrName(Report::Attribute::MixedTime), "0").toUInt(); d->m_investments = e.attribute(d->getAttrName(Report::Attribute::Investments), "0").toUInt(); // read rows/columns tab if (e.hasAttribute(d->getAttrName(Report::Attribute::Budget))) d->m_budgetId = e.attribute(d->getAttrName(Report::Attribute::Budget)); - i = kRowTypeText.indexOf(e.attribute(d->getAttrName(Report::Attribute::RowType))); - if (i != -1) - setRowType(static_cast(i)); + const auto rowTypeFromXML = d->stringToRowType(e.attribute(d->getAttrName(Report::Attribute::RowType))); + if (rowTypeFromXML != eMyMoney::Report::RowType::Invalid) + setRowType(rowTypeFromXML); else - setRowType(eExpenseIncome); + setRowType(eMyMoney::Report::RowType::ExpenseIncome); if (e.hasAttribute(d->getAttrName(Report::Attribute::ShowRowTotals))) d->m_showRowTotals = e.attribute(d->getAttrName(Report::Attribute::ShowRowTotals)).toUInt(); - else if (rowType() == eExpenseIncome) // for backward compatibility + else if (rowType() == eMyMoney::Report::RowType::ExpenseIncome) // for backward compatibility d->m_showRowTotals = true; d->m_showColumnTotals = e.attribute(d->getAttrName(Report::Attribute::ShowColumnTotals), "1").toUInt(); //check for reports with older settings which didn't have the detail attribute - i = kDetailLevelText.indexOf(e.attribute(d->getAttrName(Report::Attribute::Detail))); - if (i != -1) - d->m_detailLevel = static_cast(i); + const auto detailLevelFromXML = d->stringToDetailLevel(e.attribute(d->getAttrName(Report::Attribute::Detail))); + if (detailLevelFromXML != eMyMoney::Report::DetailLevel::End) + d->m_detailLevel = detailLevelFromXML; else - d->m_detailLevel = eDetailAll; + d->m_detailLevel = eMyMoney::Report::DetailLevel::All; d->m_includeMovingAverage = e.attribute(d->getAttrName(Report::Attribute::IncludesMovingAverage), "0").toUInt(); if (d->m_includeMovingAverage) d->m_movingAverageDays = e.attribute(d->getAttrName(Report::Attribute::MovingAverageDays), "1").toUInt(); d->m_includeSchedules = e.attribute(d->getAttrName(Report::Attribute::IncludesSchedules), "0").toUInt(); d->m_includeTransfers = e.attribute(d->getAttrName(Report::Attribute::IncludesTransfers), "0").toUInt(); d->m_includeUnusedAccounts = e.attribute(d->getAttrName(Report::Attribute::IncludesUnused), "0").toUInt(); d->m_columnsAreDays = e.attribute(d->getAttrName(Report::Attribute::ColumnsAreDays), "0").toUInt(); // read chart tab - i = kChartTypeText.indexOf(e.attribute(d->getAttrName(Report::Attribute::ChartType))); - if (i != -1) - d->m_chartType = static_cast(i); + const auto chartTypeFromXML = d->stringToChartType(e.attribute(d->getAttrName(Report::Attribute::ChartType))); + if (chartTypeFromXML != eMyMoney::Report::ChartType::End) + d->m_chartType = chartTypeFromXML; else - d->m_chartType = eChartNone; + d->m_chartType = eMyMoney::Report::ChartType::None; d->m_chartCHGridLines = e.attribute(d->getAttrName(Report::Attribute::ChartCHGridLines), "1").toUInt(); d->m_chartSVGridLines = e.attribute(d->getAttrName(Report::Attribute::ChartSVGridLines), "1").toUInt(); d->m_chartDataLabels = e.attribute(d->getAttrName(Report::Attribute::ChartDataLabels), "1").toUInt(); d->m_chartByDefault = e.attribute(d->getAttrName(Report::Attribute::ChartByDefault), "0").toUInt(); d->m_logYaxis = e.attribute(d->getAttrName(Report::Attribute::LogYAxis), "0").toUInt(); d->m_chartLineWidth = e.attribute(d->getAttrName(Report::Attribute::ChartLineWidth), QString(m_lineWidth)).toUInt(); // read range tab - i = kColumnTypeText.indexOf(e.attribute(d->getAttrName(Report::Attribute::ColumnType))); - if (i != -1) - setColumnType(static_cast(i)); + const auto columnTypeFromXML = d->stringToColumnType(e.attribute(d->getAttrName(Report::Attribute::ColumnType))); + if (columnTypeFromXML != eMyMoney::Report::ColumnType::Invalid) + setColumnType(columnTypeFromXML); else - setColumnType(eMonths); + setColumnType(eMyMoney::Report::ColumnType::Months); - i = kDataLockText.indexOf(e.attribute(d->getAttrName(Report::Attribute::DataLock))); - if (i != -1) - setDataFilter(static_cast(i)); + const auto dataLockFromXML = d->stringToDataLockAttribute(e.attribute(d->getAttrName(Report::Attribute::DataLock))); + if (dataLockFromXML != eMyMoney::Report::DataLock::DataOptionCount) + setDataFilter(dataLockFromXML); else - setDataFilter(MyMoneyReport::automatic); + setDataFilter(eMyMoney::Report::DataLock::Automatic); d->m_dataRangeStart = e.attribute(d->getAttrName(Report::Attribute::DataRangeStart), "0"); d->m_dataRangeEnd= e.attribute(d->getAttrName(Report::Attribute::DataRangeEnd), "0"); d->m_dataMajorTick = e.attribute(d->getAttrName(Report::Attribute::DataMajorTick), "0"); d->m_dataMinorTick = e.attribute(d->getAttrName(Report::Attribute::DataMinorTick), "0"); d->m_yLabelsPrecision = e.attribute(d->getAttrName(Report::Attribute::YLabelsPrecision), "2").toUInt(); - } else if (d->m_reportType == eQueryTable) { + } else if (d->m_reportType == eMyMoney::Report::ReportType::QueryTable) { // read rows/columns tab - i = kRowTypeText.indexOf(e.attribute(d->getAttrName(Report::Attribute::RowType))); - if (i != -1) - setRowType(static_cast(i)); + const auto rowTypeFromXML = d->stringToRowType(e.attribute(d->getAttrName(Report::Attribute::RowType))); + if (rowTypeFromXML != eMyMoney::Report::RowType::Invalid) + setRowType(rowTypeFromXML); else - setRowType(eAccount); + setRowType(eMyMoney::Report::RowType::Account); unsigned qc = 0; QStringList columns = e.attribute(d->getAttrName(Report::Attribute::QueryColumns), "none").split(','); foreach (const auto column, columns) { - i = kQueryColumnsText.indexOf(column); - if (i > 0) - qc |= (1 << (i - 1)); + const int queryColumnFromXML = d->stringToQueryColumn(column); + i = d->stringToQueryColumn(column); + if (queryColumnFromXML != eMyMoney::Report::QueryColumn::End) + qc |= queryColumnFromXML; } - setQueryColumns(static_cast(qc)); + setQueryColumns(static_cast(qc)); d->m_tax = e.attribute(d->getAttrName(Report::Attribute::Tax), "0").toUInt(); d->m_investments = e.attribute(d->getAttrName(Report::Attribute::Investments), "0").toUInt(); d->m_loans = e.attribute(d->getAttrName(Report::Attribute::Loans), "0").toUInt(); d->m_hideTransactions = e.attribute(d->getAttrName(Report::Attribute::HideTransactions), "0").toUInt(); d->m_showColumnTotals = e.attribute(d->getAttrName(Report::Attribute::ShowColumnTotals), "1").toUInt(); - d->m_detailLevel = kDetailLevelText.indexOf(e.attribute(d->getAttrName(Report::Attribute::Detail), "none")) == eDetailAll ? eDetailAll : eDetailNone; + const auto detailLevelFromXML = d->stringToDetailLevel(e.attribute(d->getAttrName(Report::Attribute::Detail), "none")); + if (detailLevelFromXML == eMyMoney::Report::DetailLevel::All) + d->m_detailLevel = detailLevelFromXML; + else + d->m_detailLevel = eMyMoney::Report::DetailLevel::None; // read performance or capital gains tab - if (d->m_queryColumns & eQCperformance) - d->m_investmentSum = static_cast(e.attribute(d->getAttrName(Report::Attribute::InvestmentSum), QString().setNum(MyMoneyReport::eSumPeriod)).toInt()); + if (d->m_queryColumns & eMyMoney::Report::QueryColumn::Performance) + d->m_investmentSum = static_cast(e.attribute(d->getAttrName(Report::Attribute::InvestmentSum), QString::number(static_cast(eMyMoney::Report::InvestmentSum::Period))).toInt()); // read capital gains tab - if (d->m_queryColumns & eQCcapitalgain) { - d->m_investmentSum = static_cast(e.attribute(d->getAttrName(Report::Attribute::InvestmentSum), QString().setNum(MyMoneyReport::eSumSold)).toInt()); - if (d->m_investmentSum == MyMoneyReport::eSumSold) { + if (d->m_queryColumns & eMyMoney::Report::QueryColumn::CapitalGain) { + d->m_investmentSum = static_cast(e.attribute(d->getAttrName(Report::Attribute::InvestmentSum), QString::number(static_cast(eMyMoney::Report::InvestmentSum::Sold))).toInt()); + if (d->m_investmentSum == eMyMoney::Report::InvestmentSum::Sold) { d->m_showSTLTCapitalGains = e.attribute(d->getAttrName(Report::Attribute::ShowSTLTCapitalGains), "0").toUInt(); d->m_settlementPeriod = e.attribute(d->getAttrName(Report::Attribute::SettlementPeriod), "3").toUInt(); d->m_tseparator = QDate::fromString(e.attribute(d->getAttrName(Report::Attribute::TermsSeparator), QDate::currentDate().addYears(-1).toString(Qt::ISODate)),Qt::ISODate); } } - } else if (d->m_reportType == eInfoTable) { + } else if (d->m_reportType == eMyMoney::Report::ReportType::InfoTable) { if (e.hasAttribute(d->getAttrName(Report::Attribute::ShowRowTotals))) d->m_showRowTotals = e.attribute(d->getAttrName(Report::Attribute::ShowRowTotals)).toUInt(); else d->m_showRowTotals = true; } QDomNode child = e.firstChild(); while (!child.isNull() && child.isElement()) { QDomElement c = child.toElement(); if (d->getElName(Report::Element::Text) == c.tagName() && c.hasAttribute(d->getAttrName(Report::Attribute::Pattern))) { setTextFilter(QRegExp(c.attribute(d->getAttrName(Report::Attribute::Pattern)), c.attribute(d->getAttrName(Report::Attribute::CaseSensitive), "1").toUInt() ? Qt::CaseSensitive : Qt::CaseInsensitive, c.attribute(d->getAttrName(Report::Attribute::RegEx), "1").toUInt() ? QRegExp::Wildcard : QRegExp::RegExp), c.attribute(d->getAttrName(Report::Attribute::InvertText), "0").toUInt()); } if (d->getElName(Report::Element::Type) == c.tagName() && c.hasAttribute(d->getAttrName(Report::Attribute::Type))) { - i = kTypeText.indexOf(c.attribute(d->getAttrName(Report::Attribute::Type))); + i = d->stringToTypeAttribute(c.attribute(d->getAttrName(Report::Attribute::Type))); if (i != -1) addType(i); } if (d->getElName(Report::Element::State) == c.tagName() && c.hasAttribute(d->getAttrName(Report::Attribute::State))) { - i = kStateText.indexOf(c.attribute(d->getAttrName(Report::Attribute::State))); + i = d->stringToStateAttribute(c.attribute(d->getAttrName(Report::Attribute::State))); if (i != -1) addState(i); } if (d->getElName(Report::Element::Number) == c.tagName()) setNumberFilter(c.attribute(d->getAttrName(Report::Attribute::From)), c.attribute(d->getAttrName(Report::Attribute::To))); if (d->getElName(Report::Element::Amount) == c.tagName()) setAmountFilter(MyMoneyMoney(c.attribute(d->getAttrName(Report::Attribute::From), "0/100")), MyMoneyMoney(c.attribute(d->getAttrName(Report::Attribute::To), "0/100"))); if (d->getElName(Report::Element::Dates) == c.tagName()) { QDate from, to; if (c.hasAttribute(d->getAttrName(Report::Attribute::From))) from = QDate::fromString(c.attribute(d->getAttrName(Report::Attribute::From)), Qt::ISODate); if (c.hasAttribute(d->getAttrName(Report::Attribute::To))) to = QDate::fromString(c.attribute(d->getAttrName(Report::Attribute::To)), Qt::ISODate); MyMoneyTransactionFilter::setDateFilter(from, to); } if (d->getElName(Report::Element::Payee) == c.tagName()) addPayee(c.attribute(d->getAttrName(Report::Attribute::ID))); if (d->getElName(Report::Element::Tag) == c.tagName()) addTag(c.attribute(d->getAttrName(Report::Attribute::ID))); if (d->getElName(Report::Element::Category) == c.tagName() && c.hasAttribute(d->getAttrName(Report::Attribute::ID))) addCategory(c.attribute(d->getAttrName(Report::Attribute::ID))); if (d->getElName(Report::Element::Account) == c.tagName() && c.hasAttribute(d->getAttrName(Report::Attribute::ID))) addAccount(c.attribute(d->getAttrName(Report::Attribute::ID))); if (d->getElName(Report::Element::AccountGroup) == c.tagName() && c.hasAttribute(d->getAttrName(Report::Attribute::Group))) { - i = kAccountTypeText.indexOf(c.attribute(d->getAttrName(Report::Attribute::Group))); + i = d->stringToAccountTypeAttribute(c.attribute(d->getAttrName(Report::Attribute::Group))); if (i != -1) - addAccountGroup(static_cast(i)); + addAccountGroup(static_cast(i)); } child = child.nextSibling(); } return true; } void MyMoneyReport::writeXML(QDomDocument& document, QDomElement& parent) const { QDomElement el = document.createElement(nodeNames[nnReport]); write(el, &document, false); parent.appendChild(el); } bool MyMoneyReport::hasReferenceTo(const QString& id) const { QStringList list; // collect all ids accounts(list); categories(list); payees(list); tags(list); return list.contains(id); } int MyMoneyReport::m_lineWidth = 2; bool MyMoneyReport::m_expertMode = false; void MyMoneyReport::setLineWidth(int width) { m_lineWidth = width; } void MyMoneyReport::setExpertMode(bool expertMode) { m_expertMode = expertMode; } -QString MyMoneyReport::toString(ERowType type) +QString MyMoneyReport::toString(eMyMoney::Report::RowType type) { switch(type) { - case eNoRows : return "eNoRows"; - case eAssetLiability : return "eAssetLiability"; - case eExpenseIncome : return "eExpenseIncome"; - case eCategory : return "eCategory"; - case eTopCategory : return "eTopCategory"; - case eAccount : return "eAccount"; - case eTag : return "eTag"; - case ePayee : return "ePayee"; - case eMonth : return "eMonth"; - case eWeek : return "eWeek"; - case eTopAccount : return "eTopAccount"; - case eAccountByTopAccount: return "eAccountByTopAccount"; - case eEquityType : return "eEquityType"; - case eAccountType : return "eAccountType"; - case eInstitution : return "eInstitution"; - case eBudget : return "eBudget"; - case eBudgetActual : return "eBudgetActual"; - case eSchedule : return "eSchedule"; - case eAccountInfo : return "eAccountInfo"; - case eAccountLoanInfo : return "eAccountLoanInfo"; - case eAccountReconcile : return "eAccountReconcile"; - case eCashFlow : return "eCashFlow"; + case eMyMoney::Report::RowType::NoRows : return "eMyMoney::Report::RowType::NoRows"; + case eMyMoney::Report::RowType::AssetLiability : return "eMyMoney::Report::RowType::AssetLiability"; + case eMyMoney::Report::RowType::ExpenseIncome : return "eMyMoney::Report::RowType::ExpenseIncome"; + case eMyMoney::Report::RowType::Category : return "eMyMoney::Report::RowType::Category"; + case eMyMoney::Report::RowType::TopCategory : return "eTopCategory"; + case eMyMoney::Report::RowType::Account : return "eAccount"; + case eMyMoney::Report::RowType::Tag : return "eTag"; + case eMyMoney::Report::RowType::Payee : return "ePayee"; + case eMyMoney::Report::RowType::Month : return "eMonth"; + case eMyMoney::Report::RowType::Week : return "eWeek"; + case eMyMoney::Report::RowType::TopAccount : return "eTopAccount"; + case eMyMoney::Report::RowType::AccountByTopAccount: return "eAccountByTopAccount"; + case eMyMoney::Report::RowType::EquityType : return "eEquityType"; + case eMyMoney::Report::RowType::AccountType : return "eAccountType"; + case eMyMoney::Report::RowType::Institution : return "eInstitution"; + case eMyMoney::Report::RowType::Budget : return "eBudget"; + case eMyMoney::Report::RowType::BudgetActual : return "eBudgetActual"; + case eMyMoney::Report::RowType::Schedule : return "eSchedule"; + case eMyMoney::Report::RowType::AccountInfo : return "eAccountInfo"; + case eMyMoney::Report::RowType::AccountLoanInfo : return "eAccountLoanInfo"; + case eMyMoney::Report::RowType::AccountReconcile : return "eAccountReconcile"; + case eMyMoney::Report::RowType::CashFlow : return "eCashFlow"; default : return "undefined"; } } -QString MyMoneyReport::toString(MyMoneyReport::EReportType type) +QString MyMoneyReport::toString(eMyMoney::Report::ReportType type) { switch(type) { - case eNoReport: return "eNoReport"; - case ePivotTable: return "ePivotTable"; - case eQueryTable: return "eQueryTable"; - case eInfoTable: return "eInfoTable"; + case eMyMoney::Report::ReportType::NoReport: return "eNoReport"; + case eMyMoney::Report::ReportType::PivotTable: return "ePivotTable"; + case eMyMoney::Report::ReportType::QueryTable: return "eQueryTable"; + case eMyMoney::Report::ReportType::InfoTable: return "eInfoTable"; default: return "undefined"; } } diff --git a/kmymoney/mymoney/mymoneyreport.h b/kmymoney/mymoney/mymoneyreport.h index 1eb7d4f6e..48274bc79 100644 --- a/kmymoney/mymoney/mymoneyreport.h +++ b/kmymoney/mymoney/mymoneyreport.h @@ -1,493 +1,478 @@ /* * Copyright 2004-2006 Ace Jones * Copyright 2006 Darren Gould * Copyright 2007-2010 Alvaro Soliverez * 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 MYMONEYREPORT_H #define MYMONEYREPORT_H // ---------------------------------------------------------------------------- // QT Includes // ---------------------------------------------------------------------------- // Project Includes #include "mymoneyobject.h" #include "mymoneytransactionfilter.h" #include "kmm_mymoney_export.h" #include "mymoneyunittestable.h" class QString; class QDomElement; class QDomDocument; class MyMoneyAccount; template class QList; namespace eMyMoney { namespace Account { enum class Type; } namespace TransactionFilter { enum class Date; } } +namespace eMyMoney { namespace Report { enum class RowType; } } +namespace eMyMoney { namespace Report { enum class ReportType; } } +namespace eMyMoney { namespace Report { enum class ColumnType; } } +namespace eMyMoney { namespace Report { enum QueryColumn : int; } } +namespace eMyMoney { namespace Report { enum class DetailLevel; } } +namespace eMyMoney { namespace Report { enum class InvestmentSum; } } +namespace eMyMoney { namespace Report { enum class ChartType; } } +namespace eMyMoney { namespace Report { enum class DataLock; } } + /** * This class defines a report within the MyMoneyEngine. The report class * contains all the configuration parameters needed to run a report, plus * XML serialization. * * A report is a transactionfilter, so any report can specify which * transactions it's interested down to the most minute level of detail. * It extends the transactionfilter by providing identification (name, * comments, group type, etc) as well as layout information (what kind * of layout should be used, how the rows & columns should be presented, * currency converted, etc.) * * As noted above, this class only provides a report DEFINITION. The * generation and presentation of the report itself are left to higher * level classes. * * @author Ace Jones */ class MyMoneyReportPrivate; class KMM_MYMONEY_EXPORT MyMoneyReport: public MyMoneyObject, public MyMoneyTransactionFilter { Q_DECLARE_PRIVATE_D(MyMoneyObject::d_ptr, MyMoneyReport) KMM_MYMONEY_UNIT_TESTABLE -//protected: -// MyMoneyReport(MyMoneyReportPrivate &dd); - -public: - // When adding a new row type, be sure to add a corresponding entry in kTypeArray - enum ERowType { eNoRows = 0, eAssetLiability, eExpenseIncome, eCategory, eTopCategory, eAccount, eTag, ePayee, eMonth, eWeek, eTopAccount, eAccountByTopAccount, eEquityType, eAccountType, eInstitution, eBudget, eBudgetActual, eSchedule, eAccountInfo, eAccountLoanInfo, eAccountReconcile, eCashFlow}; - enum EReportType { eNoReport = 0, ePivotTable, eQueryTable, eInfoTable }; - enum EColumnType { eNoColumns = 0, eDays = 1, eMonths = 1, eBiMonths = 2, eQuarters = 3, eWeeks = 7, eYears = 12 }; - - // if you add bits to this bitmask, start with the value currently assigned to eQCend and update its value afterwards - // also don't forget to add column names to kQueryColumnsText in mymoneyreport.cpp - enum EQueryColumns { eQCnone = 0x0, eQCbegin = 0x1, eQCnumber = 0x1, eQCpayee = 0x2, eQCcategory = 0x4, eQCtag = 0x8, eQCmemo = 0x10, eQCaccount = 0x20, eQCreconciled = 0x40, eQCaction = 0x80, eQCshares = 0x100, eQCprice = 0x200, eQCperformance = 0x400, eQCloan = 0x800, eQCbalance = 0x1000, eQCcapitalgain = 0x2000, eQCend = 0x4000 }; - - enum EDetailLevel { eDetailNone = 0, eDetailAll, eDetailTop, eDetailGroup, eDetailTotal, eDetailEnd }; - enum EInvestmentSum { eSumPeriod = 0, eSumOwnedAndSold, eSumOwned, eSumSold, eSumBought}; - enum EChartType { eChartNone = 0, eChartLine, eChartBar, eChartPie, eChartRing, eChartStackedBar, eChartEnd }; - - enum dataOptionE { automatic = 0, userDefined, dataOptionCount }; - - static const QStringList kRowTypeText; - static const QStringList kColumnTypeText; - static const QStringList kQueryColumnsText; - static const QStringList kDetailLevelText; - static const QStringList kChartTypeText; - static const EReportType kTypeArray[]; - public: MyMoneyReport(); - explicit MyMoneyReport(ERowType rt, + explicit MyMoneyReport(const QString &id); + + explicit MyMoneyReport(eMyMoney::Report::RowType rt, unsigned ct, eMyMoney::TransactionFilter::Date dl, - EDetailLevel ss, + eMyMoney::Report::DetailLevel ss, const QString& name, const QString& comment); /** * This constructor creates an object based on the data found in the * QDomElement referenced by @p node. If problems arise, the @p id of * the object is cleared (see MyMoneyObject::clearId()). */ explicit MyMoneyReport(const QDomElement& node); MyMoneyReport(const QString& id, const MyMoneyReport& other); MyMoneyReport(const MyMoneyReport & other); MyMoneyReport(MyMoneyReport && other); MyMoneyReport & operator=(MyMoneyReport other); friend void swap(MyMoneyReport& first, MyMoneyReport& second); ~MyMoneyReport(); - EReportType reportType() const; + eMyMoney::Report::ReportType reportType() const; QString name() const; void setName(const QString& s); bool isShowingRowTotals() const; void setShowingRowTotals(bool f); bool isShowingColumnTotals() const; void setShowingColumnTotals(bool f); - ERowType rowType() const; - void setRowType(ERowType rt); + eMyMoney::Report::RowType rowType() const; + void setRowType(eMyMoney::Report::RowType rt); bool isRunningSum() const; - EColumnType columnType() const; - void setColumnType(EColumnType ct); + eMyMoney::Report::ColumnType columnType() const; + void setColumnType(eMyMoney::Report::ColumnType ct); bool isConvertCurrency() const; void setConvertCurrency(bool f); uint columnPitch() const; QString comment() const; void setComment(const QString& comment); - EQueryColumns queryColumns() const; - void setQueryColumns(EQueryColumns qc); + eMyMoney::Report::QueryColumn queryColumns() const; + void setQueryColumns(eMyMoney::Report::QueryColumn qc); QString group() const; void setGroup(const QString& group); bool isFavorite() const; void setFavorite(bool f); bool isTax() const; void setTax(bool f); bool isInvestmentsOnly() const; void setInvestmentsOnly(bool f); bool isLoansOnly() const; void setLoansOnly(bool f); - EDetailLevel detailLevel() const; - void setDetailLevel(EDetailLevel detail); + eMyMoney::Report::DetailLevel detailLevel() const; + void setDetailLevel(eMyMoney::Report::DetailLevel detail); - EInvestmentSum investmentSum() const; - void setInvestmentSum(EInvestmentSum sum); + eMyMoney::Report::InvestmentSum investmentSum() const; + void setInvestmentSum(eMyMoney::Report::InvestmentSum sum); bool isHideTransactions() const; void setHideTransactions(bool f); - EChartType chartType() const; - void setChartType(EChartType type); + eMyMoney::Report::ChartType chartType() const; + void setChartType(eMyMoney::Report::ChartType type); bool isChartDataLabels() const; void setChartDataLabels(bool f); bool isChartCHGridLines() const; void setChartCHGridLines(bool f); bool isChartSVGridLines() const; void setChartSVGridLines(bool f); bool isChartByDefault() const; void setChartByDefault(bool f); uint chartLineWidth() const; void setChartLineWidth(uint f); bool isLogYAxis() const; void setLogYAxis(bool f); QString dataRangeStart() const; void setDataRangeStart(const QString& f); QString dataRangeEnd() const; void setDataRangeEnd(const QString& f); QString dataMajorTick() const; void setDataMajorTick(const QString& f); QString dataMinorTick() const; void setDataMinorTick(const QString& f); uint yLabelsPrecision() const; void setYLabelsPrecision(int f); bool isIncludingSchedules() const; void setIncludingSchedules(bool f); bool isColumnsAreDays() const; void setColumnsAreDays(bool f); bool isIncludingTransfers() const; void setIncludingTransfers(bool f); bool isIncludingUnusedAccounts() const; void setIncludingUnusedAccounts(bool f); bool hasBudget() const; QString budget() const; /** * Sets the budget used for this report * * @param budget The ID of the budget to use, or an empty string * to indicate a budget is NOT included * @param fa Whether to display actual data alongside the budget. * Setting to false means the report displays ONLY the budget itself. * @warning For now, the budget ID is ignored. The budget id is * simply checked for any non-empty string, and if so, hasBudget() * will return true. */ void setBudget(const QString& budget, bool fa = true); bool isIncludingBudgetActuals() const; void setIncludingBudgetActuals(bool f); bool isIncludingForecast() const; void setIncludingForecast(bool f); bool isIncludingMovingAverage() const; void setIncludingMovingAverage(bool f); int movingAverageDays() const; void setMovingAverageDays(int days); bool isIncludingPrice() const; void setIncludingPrice(bool f); bool isIncludingAveragePrice() const; void setIncludingAveragePrice(bool f); - dataOptionE dataFilter() const; + eMyMoney::Report::DataLock dataFilter() const; bool isDataUserDefined() const; - void setDataFilter(dataOptionE u); + void setDataFilter(eMyMoney::Report::DataLock u); eMyMoney::TransactionFilter::Date dateRange() const; bool isDateUserDefined() const; /** * Set the underlying date filter and LOCK that filter to the specified * range. For example, if @p _u is "CurrentMonth", this report should always * be updated to the current month no matter when the report is run. * * This updating is not entirely automatic, you should update it yourself by * calling updateDateFilter. * * @param _u The date range constant (MyMoneyTransactionFilter::dateRangeE) * which this report should be locked to. */ void setDateFilter(eMyMoney::TransactionFilter::Date u); /** * Set the underlying date filter using the start and end dates provided. * Note that this does not LOCK to any range like setDateFilter(unsigned) * above. It is just a reimplementation of the MyMoneyTransactionFilter * version. * * @param _db The inclusive begin date of the date range * @param _de The inclusive end date of the date range */ void setDateFilter(const QDate& db, const QDate& de); /** * Set the underlying date filter using the 'date lock' property. * * Always call this function before executing the report to be sure that * the date filters properly match the plain-language 'date lock'. * * For example, if the report is date-locked to "Current Month", and the * last time you loaded or ran the report was in August, but it's now * September, this function will update the date range to be September, * as is proper. */ void updateDateFilter(); bool isMixedTime() const; void setMixedTime(bool f); int currentDateColumn() const; void setCurrentDateColumn(int f); uint settlementPeriod() const; void setSettlementPeriod(uint days); bool isShowingSTLTCapitalGains() const; void setShowSTLTCapitalGains(bool f); QDate termSeparator() const; void setTermSeparator(const QDate& date); bool isSkippingZero() const; void setSkipZero(int f); /** * This method allows you to clear the underlying transaction filter */ void clearTransactionFilter(); /** * This method allows you to set the underlying transaction filter * * @param _filter The filter which should replace the existing transaction * filter. */ void assignFilter(const MyMoneyTransactionFilter& filter); /** * Retrieves a VALID beginning & ending date for this report. * * The underlying date filter can return en empty QDate() for either the * begin or end date or both. This is typically unacceptable for reports, * which need the REAL begin and end date. * * This function gets the underlying date filter range, and if either is * an empty QDate(), it determines the missing date from looking at all * the transactions which match the underlying filter, and returning the * date of the first or last transaction (as appropriate). * * @param _db The inclusive begin date of the date range * @param _de The inclusive end date of the date range */ void validDateRange(QDate &db, QDate &de); /** * This method turns on the account group filter and adds the * @p type to the list of allowed groups. * * Note that account group filtering is handled differently * than all the filters of the underlying class. This filter * is meant to be applied to individual splits of matched * transactions AFTER the underlying filter is used to find * the matching transactions. * * @param type the account group to add to the allowed groups list */ void addAccountGroup(eMyMoney::Account::Type type); /** * This method returns whether an account group filter has been set, * and if so, it returns all the account groups set in the filter. * * @param list list to append account groups into * @return return true if an account group filter has been set */ bool accountGroups(QList& list) const; /** * This method returns whether the specified account group * is allowed by the account groups filter. * * @param type group to append account groups into * @return return true if an account group filter has been set */ bool includesAccountGroup(eMyMoney::Account::Type type) const; /** * This method is used to test whether a specific account * passes the accountGroup test and either the Account or * Category test, depending on which sort of Account it is. * * The m_tax and m_investments properties are also considered. * * @param acc the account in question * @return true if account is in filter set, false otherwise */ bool includes(const MyMoneyAccount& acc) const; /** * This method writes this report to the DOM element @p e, * within the DOM document @p doc. * * @param e The element which should be populated with info from this report * @param doc The document which we can use to create new sub-elements * if needed * @param anonymous Whether the sensitive parts of the report should be * masked */ void write(QDomElement& e, QDomDocument *doc, bool anonymous = false) const; /** * This method reads a report from the DOM element @p e, and * populates this report with the results. * * @param e The element from which the report should be read * * @return bool True if a report was successfully loaded from the * element @p e. If false is returned, the contents of this report * object are undefined. */ bool read(const QDomElement& e); /** * This method creates a QDomElement for the @p document * under the parent node @p parent. (This version overwrites the * MMObject base class.) * * @param document reference to QDomDocument * @param parent reference to QDomElement parent node */ void writeXML(QDomDocument& document, QDomElement& parent) const override; /** * This method checks if a reference to the given object exists. It returns, * a @p true if the object is referencing the one requested by the * parameter @p id. If it does not, this method returns @p false. * * @param id id of the object to be checked for references * @retval true This object references object with id @p id. * @retval false This object does not reference the object with id @p id. */ bool hasReferenceTo(const QString& id) const override; /** * This method allows to modify the default lineWidth for graphs. * The default is 2. */ static void setLineWidth(int width); /** * This member keeps the current setting for line graphs lineWidth. * @sa setLineWidth() */ static int m_lineWidth; static void setExpertMode(bool expertMode); static bool m_expertMode; /** * Return row type as string. * * @param type type to get string for * @return row type converted to string */ - static QString toString(ERowType type); + static QString toString(eMyMoney::Report::RowType type); /** * Return report type as string. * * @param type report type to get string for * @return report type converted to string */ - static QString toString(EReportType type); + static QString toString(eMyMoney::Report::ReportType type); }; inline void swap(MyMoneyReport& first, MyMoneyReport& second) // krazy:exclude=inline { using std::swap; swap(first.MyMoneyObject::d_ptr, second.MyMoneyObject::d_ptr); swap(first.MyMoneyTransactionFilter::d_ptr, second.MyMoneyTransactionFilter::d_ptr); } inline MyMoneyReport::MyMoneyReport(MyMoneyReport && other) : MyMoneyReport() // krazy:exclude=inline { swap(*this, other); } inline MyMoneyReport & MyMoneyReport::operator=(MyMoneyReport other) // krazy:exclude=inline { swap(*this, other); return *this; } /** * Make it possible to hold @ref MyMoneyReport objects inside @ref QVariant objects. */ Q_DECLARE_METATYPE(MyMoneyReport) #endif // MYMONEYREPORT_H diff --git a/kmymoney/mymoney/mymoneyreport_p.h b/kmymoney/mymoney/mymoneyreport_p.h index 130f7a63f..afc40484d 100644 --- a/kmymoney/mymoney/mymoneyreport_p.h +++ b/kmymoney/mymoney/mymoneyreport_p.h @@ -1,414 +1,782 @@ /* * Copyright 2004-2006 Ace Jones * Copyright 2006 Darren Gould * Copyright 2007-2010 Alvaro Soliverez * 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 MYMONEYREPORT_P_H #define MYMONEYREPORT_P_H #include "mymoneyreport.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes #include "mymoneyobject_p.h" #include "mymoneyenums.h" -using namespace eMyMoney; - namespace Report { enum class Element { Payee, Tag, Account, Text, Type, State, Number, Amount, Dates, Category, AccountGroup }; uint qHash(const Element key, uint seed) { return ::qHash(static_cast(key), seed); } enum class Attribute { ID, Group, Type, Name, Comment, ConvertCurrency, Favorite, SkipZero, DateLock, DataLock, MovingAverageDays, IncludesActuals, IncludesForecast, IncludesPrice, IncludesAveragePrice, IncludesMovingAverage, IncludesSchedules, IncludesTransfers, IncludesUnused, MixedTime, Investments, Budget, ShowRowTotals, ShowColumnTotals, Detail, ColumnsAreDays, ChartType, ChartCHGridLines, ChartSVGridLines, ChartDataLabels, ChartByDefault, LogYAxis, ChartLineWidth, ColumnType, RowType, DataRangeStart, DataRangeEnd, DataMajorTick, DataMinorTick, YLabelsPrecision, QueryColumns, Tax, Loans, HideTransactions, InvestmentSum, SettlementPeriod, ShowSTLTCapitalGains, TermsSeparator, Pattern, CaseSensitive, RegEx, InvertText, State, From, To, // insert new entries above this line LastAttribute }; uint qHash(const Attribute key, uint seed) { return ::qHash(static_cast(key), seed); } } class MyMoneyReportPrivate : public MyMoneyObjectPrivate { public: + MyMoneyReportPrivate() : + m_name(QStringLiteral("Unconfigured Pivot Table Report")), + m_detailLevel(eMyMoney::Report::DetailLevel::None), + m_investmentSum(eMyMoney::Report::InvestmentSum::Sold), + m_hideTransactions(false), + m_convertCurrency(true), + m_favorite(false), + m_tax(false), + m_investments(false), + m_loans(false), + m_reportType(rowTypeToReportType(eMyMoney::Report::RowType::ExpenseIncome)), + m_rowType(eMyMoney::Report::RowType::ExpenseIncome), + m_columnType(eMyMoney::Report::ColumnType::Months), + m_columnsAreDays(false), + m_queryColumns(eMyMoney::Report::QueryColumn::None), + m_dateLock(eMyMoney::TransactionFilter::Date::UserDefined), + m_accountGroupFilter(false), + m_chartType(eMyMoney::Report::ChartType::Line), + m_chartDataLabels(true), + m_chartCHGridLines(true), + m_chartSVGridLines(true), + m_chartByDefault(false), + m_chartLineWidth(MyMoneyReport::m_lineWidth), + m_logYaxis(false), + m_dataRangeStart('0'), + m_dataRangeEnd('0'), + m_dataMajorTick('0'), + m_dataMinorTick('0'), + m_yLabelsPrecision(2), + m_dataLock(eMyMoney::Report::DataLock::Automatic), + m_includeSchedules(false), + m_includeTransfers(false), + m_includeBudgetActuals(false), + m_includeUnusedAccounts(false), + m_showRowTotals(false), + m_showColumnTotals(true), + m_includeForecast(false), + m_includeMovingAverage(false), + m_movingAverageDays(0), + m_includePrice(false), + m_includeAveragePrice(false), + m_mixedTime(false), + m_currentDateColumn(0), + m_settlementPeriod(3), + m_showSTLTCapitalGains(false), + m_tseparator(QDate::currentDate().addYears(-1)), + m_skipZero(false) + { + } static QString getElName(const Report::Element el) { static const QHash elNames { {Report::Element::Payee, QStringLiteral("PAYEE")}, {Report::Element::Tag, QStringLiteral("TAG")}, {Report::Element::Account, QStringLiteral("ACCOUNT")}, {Report::Element::Text, QStringLiteral("TEXT")}, {Report::Element::Type, QStringLiteral("TYPE")}, {Report::Element::State, QStringLiteral("STATE")}, {Report::Element::Number, QStringLiteral("NUMBER")}, {Report::Element::Amount, QStringLiteral("AMOUNT")}, {Report::Element::Dates, QStringLiteral("DATES")}, {Report::Element::Category, QStringLiteral("CATEGORY")}, {Report::Element::AccountGroup, QStringLiteral("ACCOUNTGROUP")} }; return elNames[el]; } static QString getAttrName(const Report::Attribute attr) { static const QHash attrNames { {Report::Attribute::ID, QStringLiteral("id")}, {Report::Attribute::Group, QStringLiteral("group")}, {Report::Attribute::Type, QStringLiteral("type")}, {Report::Attribute::Name, QStringLiteral("name")}, {Report::Attribute::Comment, QStringLiteral("comment")}, {Report::Attribute::ConvertCurrency, QStringLiteral("convertcurrency")}, {Report::Attribute::Favorite, QStringLiteral("favorite")}, {Report::Attribute::SkipZero, QStringLiteral("skipZero")}, {Report::Attribute::DateLock, QStringLiteral("datelock")}, {Report::Attribute::DataLock, QStringLiteral("datalock")}, {Report::Attribute::MovingAverageDays, QStringLiteral("movingaveragedays")}, {Report::Attribute::IncludesActuals, QStringLiteral("includesactuals")}, {Report::Attribute::IncludesForecast, QStringLiteral("includesforecast")}, {Report::Attribute::IncludesPrice, QStringLiteral("includesprice")}, {Report::Attribute::IncludesAveragePrice, QStringLiteral("includesaverageprice")}, {Report::Attribute::IncludesMovingAverage, QStringLiteral("includesmovingaverage")}, {Report::Attribute::IncludesSchedules, QStringLiteral("includeschedules")}, {Report::Attribute::IncludesTransfers, QStringLiteral("includestransfers")}, {Report::Attribute::IncludesUnused, QStringLiteral("includeunused")}, {Report::Attribute::MixedTime, QStringLiteral("mixedtime")}, {Report::Attribute::Investments, QStringLiteral("investments")}, {Report::Attribute::Budget, QStringLiteral("budget")}, {Report::Attribute::ShowRowTotals, QStringLiteral("showrowtotals")}, {Report::Attribute::ShowColumnTotals, QStringLiteral("showcolumntotals")}, {Report::Attribute::Detail, QStringLiteral("detail")}, {Report::Attribute::ColumnsAreDays, QStringLiteral("columnsaredays")}, {Report::Attribute::ChartType, QStringLiteral("charttype")}, {Report::Attribute::ChartCHGridLines, QStringLiteral("chartchgridlines")}, {Report::Attribute::ChartSVGridLines, QStringLiteral("chartsvgridlines")}, {Report::Attribute::ChartDataLabels, QStringLiteral("chartdatalabels")}, {Report::Attribute::ChartByDefault, QStringLiteral("chartbydefault")}, {Report::Attribute::LogYAxis, QStringLiteral("logYaxis")}, {Report::Attribute::ChartLineWidth, QStringLiteral("chartlinewidth")}, {Report::Attribute::ColumnType, QStringLiteral("columntype")}, {Report::Attribute::RowType, QStringLiteral("rowtype")}, {Report::Attribute::DataRangeStart, QStringLiteral("dataRangeStart")}, {Report::Attribute::DataRangeEnd, QStringLiteral("dataRangeEnd")}, {Report::Attribute::DataMajorTick, QStringLiteral("dataMajorTick")}, {Report::Attribute::DataMinorTick, QStringLiteral("dataMinorTick")}, {Report::Attribute::YLabelsPrecision, QStringLiteral("yLabelsPrecision")}, {Report::Attribute::QueryColumns, QStringLiteral("querycolumns")}, {Report::Attribute::Tax, QStringLiteral("tax")}, {Report::Attribute::Loans, QStringLiteral("loans")}, {Report::Attribute::HideTransactions, QStringLiteral("hidetransactions")}, {Report::Attribute::InvestmentSum, QStringLiteral("investmentsum")}, {Report::Attribute::SettlementPeriod, QStringLiteral("settlementperiod")}, {Report::Attribute::ShowSTLTCapitalGains, QStringLiteral("showSTLTCapitalGains")}, {Report::Attribute::TermsSeparator, QStringLiteral("tseparator")}, {Report::Attribute::Pattern, QStringLiteral("pattern")}, {Report::Attribute::CaseSensitive, QStringLiteral("casesensitive")}, {Report::Attribute::RegEx, QStringLiteral("regex")}, {Report::Attribute::InvertText, QStringLiteral("inverttext")}, {Report::Attribute::State, QStringLiteral("state")}, {Report::Attribute::From, QStringLiteral("from")}, {Report::Attribute::To, QStringLiteral("to")} }; return attrNames[attr]; } + static QHash rowTypesLUT() + { + static const QHash lut { + {eMyMoney::Report::RowType::NoRows, QStringLiteral("none")}, + {eMyMoney::Report::RowType::AssetLiability, QStringLiteral("assetliability")}, + {eMyMoney::Report::RowType::ExpenseIncome, QStringLiteral("expenseincome")}, + {eMyMoney::Report::RowType::Category, QStringLiteral("category")}, + {eMyMoney::Report::RowType::TopCategory, QStringLiteral("topcategory")}, + {eMyMoney::Report::RowType::Account, QStringLiteral("account")}, + {eMyMoney::Report::RowType::Tag, QStringLiteral("tag")}, + {eMyMoney::Report::RowType::Payee, QStringLiteral("payee")}, + {eMyMoney::Report::RowType::Month, QStringLiteral("month")}, + {eMyMoney::Report::RowType::Week, QStringLiteral("week")}, + {eMyMoney::Report::RowType::TopAccount, QStringLiteral("topaccount")}, + {eMyMoney::Report::RowType::AccountByTopAccount, QStringLiteral("topaccount-account")}, + {eMyMoney::Report::RowType::EquityType, QStringLiteral("equitytype")}, + {eMyMoney::Report::RowType::AccountType, QStringLiteral("accounttype")}, + {eMyMoney::Report::RowType::Institution, QStringLiteral("institution")}, + {eMyMoney::Report::RowType::Budget, QStringLiteral("budget")}, + {eMyMoney::Report::RowType::BudgetActual, QStringLiteral("budgetactual")}, + {eMyMoney::Report::RowType::Schedule, QStringLiteral("schedule")}, + {eMyMoney::Report::RowType::AccountInfo, QStringLiteral("accountinfo")}, + {eMyMoney::Report::RowType::AccountLoanInfo, QStringLiteral("accountloaninfo")}, + {eMyMoney::Report::RowType::AccountReconcile, QStringLiteral("accountreconcile")}, + {eMyMoney::Report::RowType::CashFlow, QStringLiteral("cashflow")}, + }; + return lut; + } + + static QString reportNames(eMyMoney::Report::RowType textID) + { + return rowTypesLUT().value(textID); + } + + static eMyMoney::Report::RowType stringToRowType(const QString &text) + { + return rowTypesLUT().key(text, eMyMoney::Report::RowType::Invalid); + } + + static QHash columTypesLUT() + { + static const QHash lut { + {eMyMoney::Report::ColumnType::NoColumns, QStringLiteral("none")}, + {eMyMoney::Report::ColumnType::Months, QStringLiteral("months")}, + {eMyMoney::Report::ColumnType::BiMonths, QStringLiteral("bimonths")}, + {eMyMoney::Report::ColumnType::Quarters, QStringLiteral("quarters")}, +// {eMyMoney::Report::ColumnType::, QStringLiteral("4")} +// {eMyMoney::Report::ColumnType::, QStringLiteral("5")} +// {eMyMoney::Report::ColumnType::, QStringLiteral("6")} + {eMyMoney::Report::ColumnType::Weeks, QStringLiteral("weeks")}, +// {eMyMoney::Report::ColumnType::, QStringLiteral("8")} +// {eMyMoney::Report::ColumnType::, QStringLiteral("9")} +// {eMyMoney::Report::ColumnType::, QStringLiteral("10")} +// {eMyMoney::Report::ColumnType::, QStringLiteral("11")} + {eMyMoney::Report::ColumnType::Years, QStringLiteral("years")} + }; + return lut; + } + + static QString reportNames(eMyMoney::Report::ColumnType textID) + { + return columTypesLUT().value(textID); + } + + static eMyMoney::Report::ColumnType stringToColumnType(const QString &text) + { + return columTypesLUT().key(text, eMyMoney::Report::ColumnType::Invalid); + } + + static QHash queryColumnsLUT() + { + static const QHash lut { + {eMyMoney::Report::QueryColumn::None, QStringLiteral("none")}, + {eMyMoney::Report::QueryColumn::Number, QStringLiteral("number")}, + {eMyMoney::Report::QueryColumn::Payee, QStringLiteral("payee")}, + {eMyMoney::Report::QueryColumn::Category, QStringLiteral("category")}, + {eMyMoney::Report::QueryColumn::Tag, QStringLiteral("tag")}, + {eMyMoney::Report::QueryColumn::Memo, QStringLiteral("memo")}, + {eMyMoney::Report::QueryColumn::Account, QStringLiteral("account")}, + {eMyMoney::Report::QueryColumn::Reconciled, QStringLiteral("reconcileflag")}, + {eMyMoney::Report::QueryColumn::Action, QStringLiteral("action")}, + {eMyMoney::Report::QueryColumn::Shares, QStringLiteral("shares")}, + {eMyMoney::Report::QueryColumn::Price, QStringLiteral("price")}, + {eMyMoney::Report::QueryColumn::Performance, QStringLiteral("performance")}, + {eMyMoney::Report::QueryColumn::Loan, QStringLiteral("loan")}, + {eMyMoney::Report::QueryColumn::Balance, QStringLiteral("balance")}, + {eMyMoney::Report::QueryColumn::CapitalGain, QStringLiteral("capitalgain")} + }; + return lut; + } + + static QString reportNamesForQC(eMyMoney::Report::QueryColumn textID) + { + return queryColumnsLUT().value(textID); + } + + static eMyMoney::Report::QueryColumn stringToQueryColumn(const QString &text) + { + return queryColumnsLUT().key(text, eMyMoney::Report::QueryColumn::End); + } + + static QHash detailLevelLUT() + { + static const QHash lut { + {eMyMoney::Report::DetailLevel::None, QStringLiteral("none")}, + {eMyMoney::Report::DetailLevel::All, QStringLiteral("all")}, + {eMyMoney::Report::DetailLevel::Top, QStringLiteral("top")}, + {eMyMoney::Report::DetailLevel::Group, QStringLiteral("group")}, + {eMyMoney::Report::DetailLevel::Total, QStringLiteral("total")}, + {eMyMoney::Report::DetailLevel::End, QStringLiteral("invalid")} + }; + return lut; + } + + static QString reportNames(eMyMoney::Report::DetailLevel textID) + { + return detailLevelLUT().value(textID); + } + + static eMyMoney::Report::DetailLevel stringToDetailLevel(const QString &text) + { + return detailLevelLUT().key(text, eMyMoney::Report::DetailLevel::End); + } + + static QHash chartTypeLUT() + { + static const QHash lut { + {eMyMoney::Report::ChartType::None, QStringLiteral("none")}, + {eMyMoney::Report::ChartType::Line, QStringLiteral("line")}, + {eMyMoney::Report::ChartType::Bar, QStringLiteral("bar")}, + {eMyMoney::Report::ChartType::Pie, QStringLiteral("pie")}, + {eMyMoney::Report::ChartType::Ring, QStringLiteral("ring")}, + {eMyMoney::Report::ChartType::StackedBar, QStringLiteral("stackedbar")} + }; + return lut; + } + + static QString reportNames(eMyMoney::Report::ChartType textID) + { + return chartTypeLUT().value(textID); + } + + static eMyMoney::Report::ChartType stringToChartType(const QString &text) + { + return chartTypeLUT().key(text, eMyMoney::Report::ChartType::End); + } + + static QHash typeAttributeLUT() + { + static const QHash lut { + {0, QStringLiteral("all")}, + {1, QStringLiteral("payments")}, + {2, QStringLiteral("deposits")}, + {3, QStringLiteral("transfers")}, + {4, QStringLiteral("none")}, + }; + return lut; + } + + static QString typeAttributeToString(int textID) + { + return typeAttributeLUT().value(textID); + } + + static int stringToTypeAttribute(const QString &text) + { + return typeAttributeLUT().key(text, 4); + } + + static QHash stateAttributeLUT() + { + static const QHash lut { + {0, QStringLiteral("all")}, + {1, QStringLiteral("notreconciled")}, + {2, QStringLiteral("cleared")}, + {3, QStringLiteral("reconciled")}, + {4, QStringLiteral("frozen")}, + {5, QStringLiteral("none")} + }; + return lut; + } + + static QString stateAttributeToString(int textID) + { + return stateAttributeLUT().value(textID); + } + + static int stringToStateAttribute(const QString &text) + { + return stateAttributeLUT().key(text, 5); + } + + static QHash dateLockLUT() + { + static const QHash lut { + {0, QStringLiteral("alldates")}, + {1, QStringLiteral("untiltoday")}, + {2, QStringLiteral("currentmonth")}, + {3, QStringLiteral("currentyear")}, + {4, QStringLiteral("monthtodate")}, + {5, QStringLiteral("yeartodate")}, + {6, QStringLiteral("yeartomonth")}, + {7, QStringLiteral("lastmonth")}, + {8, QStringLiteral("lastyear")}, + {9, QStringLiteral("last7days")}, + {10, QStringLiteral("last30days")}, + {11, QStringLiteral("last3months")}, + {12, QStringLiteral("last6months")}, + {13, QStringLiteral("last12months")}, + {14, QStringLiteral("next7days")}, + {15, QStringLiteral("next30days")}, + {16, QStringLiteral("next3months")}, + {17, QStringLiteral("next6months")}, + {18, QStringLiteral("next12months")}, + {19, QStringLiteral("userdefined")}, + {20, QStringLiteral("last3tonext3months")}, + {21, QStringLiteral("last11Months")}, + {22, QStringLiteral("currentQuarter")}, + {23, QStringLiteral("lastQuarter")}, + {24, QStringLiteral("nextQuarter")}, + {25, QStringLiteral("currentFiscalYear")}, + {26, QStringLiteral("lastFiscalYear")}, + {27, QStringLiteral("today")}, + {28, QStringLiteral("next18months")} + }; + return lut; + } + + static QString dateLockAttributeToString(int textID) + { + return dateLockLUT().value(textID); + } + + static int stringToDateLockAttribute(const QString &text) + { + return dateLockLUT().key(text, 0); + } + + static QHash dataLockLUT() + { + static const QHash lut { + {eMyMoney::Report::DataLock::Automatic, QStringLiteral("automatic")}, + {eMyMoney::Report::DataLock::UserDefined, QStringLiteral("userdefined")} + }; + return lut; + } + + static QString reportNames(eMyMoney::Report::DataLock textID) + { + return dataLockLUT().value(textID); + } + + static eMyMoney::Report::DataLock stringToDataLockAttribute(const QString &text) + { + return dataLockLUT().key(text, eMyMoney::Report::DataLock::DataOptionCount); + } + + static QHash accountTypeAttributeLUT() + { + static const QHash lut { + {0, QStringLiteral("unknown")}, + {1, QStringLiteral("checkings")}, + {2, QStringLiteral("savings")}, + {3, QStringLiteral("cash")}, + {4, QStringLiteral("creditcard")}, + {5, QStringLiteral("loan")}, + {6, QStringLiteral("certificatedep")}, + {7, QStringLiteral("investment")}, + {8, QStringLiteral("moneymarket")}, + {10, QStringLiteral("asset")}, + {11, QStringLiteral("liability")}, + {12, QStringLiteral("currency")}, + {13, QStringLiteral("income")}, + {14, QStringLiteral("expense")}, + {15, QStringLiteral("assetloan")}, + {16, QStringLiteral("stock")}, + {17, QStringLiteral("equity")}, + {18, QStringLiteral("invalid")} + }; + return lut; + } + + static QString accountTypeAttributeToString(int textID) + { + return accountTypeAttributeLUT().value(textID); + } + + static int stringToAccountTypeAttribute(const QString &text) + { + return accountTypeAttributeLUT().key(text, 0); + } + + static eMyMoney::Report::ReportType rowTypeToReportType(eMyMoney::Report::RowType rowType) + { + static const QHash reportTypes { + {eMyMoney::Report::RowType::NoRows, eMyMoney::Report::ReportType::NoReport}, + {eMyMoney::Report::RowType::AssetLiability, eMyMoney::Report::ReportType::PivotTable}, + {eMyMoney::Report::RowType::ExpenseIncome, eMyMoney::Report::ReportType::PivotTable}, + {eMyMoney::Report::RowType::Category, eMyMoney::Report::ReportType::QueryTable}, + {eMyMoney::Report::RowType::TopCategory, eMyMoney::Report::ReportType::QueryTable}, + {eMyMoney::Report::RowType::Account, eMyMoney::Report::ReportType::QueryTable}, + {eMyMoney::Report::RowType::Tag, eMyMoney::Report::ReportType::QueryTable}, + {eMyMoney::Report::RowType::Payee, eMyMoney::Report::ReportType::QueryTable}, + {eMyMoney::Report::RowType::Month, eMyMoney::Report::ReportType::QueryTable}, + {eMyMoney::Report::RowType::Week, eMyMoney::Report::ReportType::QueryTable}, + {eMyMoney::Report::RowType::TopAccount, eMyMoney::Report::ReportType::QueryTable}, + {eMyMoney::Report::RowType::AccountByTopAccount, eMyMoney::Report::ReportType::QueryTable}, + {eMyMoney::Report::RowType::EquityType, eMyMoney::Report::ReportType::QueryTable}, + {eMyMoney::Report::RowType::AccountType, eMyMoney::Report::ReportType::QueryTable}, + {eMyMoney::Report::RowType::Institution, eMyMoney::Report::ReportType::QueryTable}, + {eMyMoney::Report::RowType::Budget, eMyMoney::Report::ReportType::PivotTable}, + {eMyMoney::Report::RowType::BudgetActual, eMyMoney::Report::ReportType::PivotTable}, + {eMyMoney::Report::RowType::Schedule, eMyMoney::Report::ReportType::InfoTable}, + {eMyMoney::Report::RowType::AccountInfo, eMyMoney::Report::ReportType::InfoTable}, + {eMyMoney::Report::RowType::AccountLoanInfo, eMyMoney::Report::ReportType::InfoTable}, + {eMyMoney::Report::RowType::AccountReconcile, eMyMoney::Report::ReportType::QueryTable}, + {eMyMoney::Report::RowType::CashFlow, eMyMoney::Report::ReportType::QueryTable}, + }; + return reportTypes.value(rowType, eMyMoney::Report::ReportType::Invalid); + } + /** * The user-assigned name of the report */ QString m_name; /** * The user-assigned comment for the report, in case they want to make * additional notes for themselves about the report. */ QString m_comment; /** * Where to group this report amongst the others in the UI view. This * should be assigned by the UI system. */ QString m_group; /** * How much detail to show in the accounts */ - MyMoneyReport::EDetailLevel m_detailLevel; + eMyMoney::Report::DetailLevel m_detailLevel; /** * Whether to sum: all, sold, bought or owned value */ - MyMoneyReport::EInvestmentSum m_investmentSum; + eMyMoney::Report::InvestmentSum m_investmentSum; /** * Whether to show transactions or just totals. */ bool m_hideTransactions; /** * Whether to convert all currencies to the base currency of the file (true). * If this is false, it's up to the report generator to decide how to handle * the currency. */ bool m_convertCurrency; /** * Whether this is one of the users' favorite reports */ bool m_favorite; /** * Whether this report should only include categories marked as "Tax"="Yes" */ bool m_tax; /** * Whether this report should only include investment accounts */ bool m_investments; /** * Whether this report should only include loan accounts * Applies only to querytable reports. Mutually exclusive with * m_investments. */ bool m_loans; /** * What sort of algorithm should be used to run the report */ - MyMoneyReport::EReportType m_reportType; + eMyMoney::Report::ReportType m_reportType; /** * What sort of values should show up on the ROWS of this report */ - MyMoneyReport::ERowType m_rowType; + eMyMoney::Report::RowType m_rowType; /** * What sort of values should show up on the COLUMNS of this report, * in the case of a 'PivotTable' report. Really this is used more as a * QUANTITY of months or days. Whether it's months or days is determined * by m_columnsAreDays. */ - MyMoneyReport::EColumnType m_columnType; + eMyMoney::Report::ColumnType m_columnType; /** * Whether the base unit of columns of this report is days. Only applies to * 'PivotTable' reports. If false, then columns are months or multiples thereof. */ bool m_columnsAreDays; /** * What sort of values should show up on the COLUMNS of this report, * in the case of a 'QueryTable' report */ - MyMoneyReport::EQueryColumns m_queryColumns; + eMyMoney::Report::QueryColumn m_queryColumns; /** * The plain-language description of what the date range should be locked * to. 'userDefined' means NO locking, in any other case, the report * will be adjusted to match the date lock. So if the date lock is * 'currentMonth', the start and end dates of the underlying filter will * be updated to whatever the current month is. This updating happens * automatically when the report is loaded, and should also be done * manually by calling updateDateFilter() before generating the report */ eMyMoney::TransactionFilter::Date m_dateLock; /** * Which account groups should be included in the report. This filter * is applied to the individual splits AFTER a transaction has been * matched using the underlying filter. */ - QList m_accountGroups; + QList m_accountGroups; /** * Whether an account group filter has been set (see m_accountGroups) */ bool m_accountGroupFilter; /** * What format should be used to draw this report as a chart */ - MyMoneyReport::EChartType m_chartType; + eMyMoney::Report::ChartType m_chartType; /** * Whether the value of individual data points should be drawn on the chart */ bool m_chartDataLabels; /** * Whether grid lines should be drawn on the chart */ bool m_chartCHGridLines; bool m_chartSVGridLines; /** * Whether this report should be shown as a chart by default (otherwise it * should be shown as a textual report) */ bool m_chartByDefault; /** * Width of the chart lines */ uint m_chartLineWidth; /** * Whether Y axis is logarithmic or linear */ bool m_logYaxis; /** * Y data range */ QString m_dataRangeStart; QString m_dataRangeEnd; /** * Y data range division */ QString m_dataMajorTick; QString m_dataMinorTick; /** * Y labels precision */ uint m_yLabelsPrecision; /** * Whether data range should be calculated automatically or is user defined */ - MyMoneyReport::dataOptionE m_dataLock; + eMyMoney::Report::DataLock m_dataLock; /** * Whether to include scheduled transactions */ bool m_includeSchedules; /** * Whether to include transfers. Only applies to Income/Expense reports */ bool m_includeTransfers; /** * The id of the budget associated with this report. */ QString m_budgetId; /** * Whether this report should print the actual data to go along with * the budget. This is only valid if the report has a budget. */ bool m_includeBudgetActuals; /** * Whether this report should include all accounts and not only * accounts with transactions. */ bool m_includeUnusedAccounts; /** * Whether this report should include columns for row totals */ bool m_showRowTotals; /** * Whether this report should include rows for column totals */ bool m_showColumnTotals; /** * Whether this report should include forecast balance */ bool m_includeForecast; /** * Whether this report should include moving average */ bool m_includeMovingAverage; /** * The amount of days that spans each moving average */ int m_movingAverageDays; /** * Whether this report should include prices */ bool m_includePrice; /** * Whether this report should include moving average prices */ bool m_includeAveragePrice; /** * Make the actual and forecast lines display as one */ bool m_mixedTime; /** * This stores the column for the current date * This value is calculated dinamically and thus it is not saved in the file */ int m_currentDateColumn; /** * Time in days between the settlement date and the transaction date. */ uint m_settlementPeriod; /** * Controls showing short-term and long-term capital gains. */ bool m_showSTLTCapitalGains; /** * Date separating shot-term from long-term gains. */ QDate m_tseparator; /** * This option is for investments reports only which * show prices instead of balances as all other reports do. *

* Select this option to include prices for the given period (week, month, * quarter, ...) only. *

*

* If this option is off the last existing price is shown for a period, if * it is on, in a table the value is '0' shown and in a chart a linear * interpolation for the missing values will be performed. *
Example: *
There are prices for January and March, but there is no price for * February. *

    *
  • OFF: shows the price for February as the last price of * January *
  • ON: in a table the value is '0', in a chart a linear * interpolation for the February-price will be performed * (so it makes a kind of average-value using the January- and the * March-price in the chart) *
*

*/ bool m_skipZero; }; #endif diff --git a/kmymoney/mymoney/mymoneyschedule.cpp b/kmymoney/mymoney/mymoneyschedule.cpp index d0945cf29..7c729900b 100644 --- a/kmymoney/mymoney/mymoneyschedule.cpp +++ b/kmymoney/mymoney/mymoneyschedule.cpp @@ -1,1552 +1,1557 @@ /* * Copyright 2000-2004 Michael Edwardes * Copyright 2002-2018 Thomas Baumgart * Copyright 2005 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. * * 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 "mymoneyschedule.h" #include "mymoneyschedule_p.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include // ---------------------------------------------------------------------------- // Project Includes #include "mymoneyutils.h" #include "mymoneyexception.h" #include "mymoneyfile.h" #include "mymoneyaccount.h" #include "mymoneysplit.h" #include "imymoneyprocessingcalendar.h" #include "mymoneystoragenames.h" using namespace MyMoneyStorageNodes; using namespace eMyMoney; static IMyMoneyProcessingCalendar* processingCalendarPtr = 0; MyMoneySchedule::MyMoneySchedule() : MyMoneyObject(*new MyMoneySchedulePrivate) { } +MyMoneySchedule::MyMoneySchedule(const QString &id) : + MyMoneyObject(*new MyMoneySchedulePrivate, id) +{ +} + MyMoneySchedule::MyMoneySchedule(const QString& name, Schedule::Type type, Schedule::Occurrence occurrence, int occurrenceMultiplier, Schedule::PaymentType paymentType, const QDate& /* startDate */, const QDate& endDate, bool fixed, bool autoEnter) : MyMoneyObject(*new MyMoneySchedulePrivate) { Q_D(MyMoneySchedule); // Set up the values possibly differeing from defaults d->m_name = name; d->m_occurrence = occurrence; d->m_occurrenceMultiplier = occurrenceMultiplier; simpleToCompoundOccurrence(d->m_occurrenceMultiplier, d->m_occurrence); d->m_type = type; d->m_paymentType = paymentType; d->m_fixed = fixed; d->m_autoEnter = autoEnter; d->m_endDate = endDate; } MyMoneySchedule::MyMoneySchedule(const QDomElement& node) : MyMoneyObject(*new MyMoneySchedulePrivate, node) { if (nodeNames[nnScheduleTX] != node.tagName()) throw MYMONEYEXCEPTION_CSTRING("Node was not SCHEDULED_TX"); Q_D(MyMoneySchedule); d->m_name = node.attribute(d->getAttrName(Schedule::Attribute::Name)); d->m_startDate = MyMoneyUtils::stringToDate(node.attribute(d->getAttrName(Schedule::Attribute::StartDate))); d->m_endDate = MyMoneyUtils::stringToDate(node.attribute(d->getAttrName(Schedule::Attribute::EndDate))); d->m_lastPayment = MyMoneyUtils::stringToDate(node.attribute(d->getAttrName(Schedule::Attribute::LastPayment))); d->m_type = static_cast(node.attribute(d->getAttrName(Schedule::Attribute::Type)).toInt()); d->m_paymentType = static_cast(node.attribute(d->getAttrName(Schedule::Attribute::PaymentType)).toInt()); d->m_occurrence = static_cast(node.attribute(d->getAttrName(Schedule::Attribute::Occurrence)).toInt()); d->m_occurrenceMultiplier = node.attribute(d->getAttrName(Schedule::Attribute::OccurrenceMultiplier), "1").toInt(); // Convert to compound occurrence simpleToCompoundOccurrence(d->m_occurrenceMultiplier, d->m_occurrence); d->m_lastDayInMonth = static_cast(node.attribute("lastDayInMonth").toInt()); d->m_autoEnter = static_cast(node.attribute(d->getAttrName(Schedule::Attribute::AutoEnter)).toInt()); d->m_fixed = static_cast(node.attribute(d->getAttrName(Schedule::Attribute::Fixed)).toInt()); d->m_weekendOption = static_cast(node.attribute(d->getAttrName(Schedule::Attribute::WeekendOption)).toInt()); // read in the associated transaction QDomNodeList nodeList = node.elementsByTagName(nodeNames[nnTransaction]); if (nodeList.count() == 0) throw MYMONEYEXCEPTION_CSTRING("SCHEDULED_TX has no TRANSACTION node"); setTransaction(MyMoneyTransaction(nodeList.item(0).toElement(), false), true); // some old versions did not remove the entry date and post date fields // in the schedule. So if this is the case, we deal with a very old transaction // and can't use the post date field as next due date. Hence, we wipe it out here if (d->m_transaction.entryDate().isValid()) { d->m_transaction.setPostDate(QDate()); d->m_transaction.setEntryDate(QDate()); } // readin the recorded payments nodeList = node.elementsByTagName(d->getElName(Schedule::Element::Payments)); if (nodeList.count() > 0) { nodeList = nodeList.item(0).toElement().elementsByTagName(d->getElName(Schedule::Element::Payment)); for (int i = 0; i < nodeList.count(); ++i) { d->m_recordedPayments << MyMoneyUtils::stringToDate(nodeList.item(i).toElement().attribute(d->getAttrName(Schedule::Attribute::Date))); } } // if the next due date is not set (comes from old version) // then set it up the old way if (!nextDueDate().isValid() && !d->m_lastPayment.isValid()) { d->m_transaction.setPostDate(d->m_startDate); // clear it, because the schedule has never been used d->m_startDate = QDate(); } // There are reports that lastPayment and nextDueDate are identical or // that nextDueDate is older than lastPayment. This could // be caused by older versions of the application. In this case, we just // clear out the nextDueDate and let it calculate from the lastPayment. if (nextDueDate().isValid() && nextDueDate() <= d->m_lastPayment) { d->m_transaction.setPostDate(QDate()); } if (!nextDueDate().isValid()) { d->m_transaction.setPostDate(d->m_startDate); d->m_transaction.setPostDate(nextPayment(d->m_lastPayment.addDays(1))); } } MyMoneySchedule::MyMoneySchedule(const MyMoneySchedule& other) : MyMoneyObject(*new MyMoneySchedulePrivate(*other.d_func()), other.id()) { } MyMoneySchedule::MyMoneySchedule(const QString& id, const MyMoneySchedule& other) : MyMoneyObject(*new MyMoneySchedulePrivate(*other.d_func()), id) { } MyMoneySchedule::~MyMoneySchedule() { } Schedule::Occurrence MyMoneySchedule::occurrence() const { Q_D(const MyMoneySchedule); Schedule::Occurrence occ = d->m_occurrence; int mult = d->m_occurrenceMultiplier; compoundToSimpleOccurrence(mult, occ); return occ; } int MyMoneySchedule::occurrenceMultiplier() const { Q_D(const MyMoneySchedule); return d->m_occurrenceMultiplier; } eMyMoney::Schedule::Type MyMoneySchedule::type() const { Q_D(const MyMoneySchedule); return d->m_type; } eMyMoney::Schedule::Occurrence MyMoneySchedule::occurrencePeriod() const { Q_D(const MyMoneySchedule); return d->m_occurrence; } void MyMoneySchedule::setStartDate(const QDate& date) { Q_D(MyMoneySchedule); d->m_startDate = date; } void MyMoneySchedule::setPaymentType(Schedule::PaymentType type) { Q_D(MyMoneySchedule); d->m_paymentType = type; } void MyMoneySchedule::setFixed(bool fixed) { Q_D(MyMoneySchedule); d->m_fixed = fixed; } void MyMoneySchedule::setTransaction(const MyMoneyTransaction& transaction) { setTransaction(transaction, false); } void MyMoneySchedule::setTransaction(const MyMoneyTransaction& transaction, bool noDateCheck) { auto t = transaction; Q_D(MyMoneySchedule); if (!noDateCheck) { // don't allow a transaction that has no due date // if we get something like that, then we use the // the current next due date. If that is also invalid // we can't help it. if (!t.postDate().isValid()) { t.setPostDate(d->m_transaction.postDate()); } if (!t.postDate().isValid()) return; } // make sure to clear out some unused information in scheduled transactions // we need to do this for the case that the transaction passed as argument // is a matched or imported transaction. auto firstSplit = true; foreach (const auto split, t.splits()) { MyMoneySplit s = split; // clear out the bankID if (!split.bankID().isEmpty()) { s.setBankID(QString()); t.modifySplit(s); } // only clear payees from second split onwards if (firstSplit) { firstSplit = false; continue; } if (!split.payeeId().isEmpty()) { // but only if the split references an income/expense category auto file = MyMoneyFile::instance(); // some unit tests don't have a storage attached, so we // simply skip the test // Don't check for accounts with an id of 'Phony-ID' which is used // internally for non-existing accounts (during creation of accounts) if (file->storageAttached() && s.accountId() != QString("Phony-ID")) { auto acc = file->account(s.accountId()); if (acc.isIncomeExpense()) { s.setPayeeId(QString()); t.modifySplit(s); } } } } d->m_transaction = t; // make sure that the transaction does not have an id so that we can enter // it into the engine d->m_transaction.clearId(); } void MyMoneySchedule::setEndDate(const QDate& date) { Q_D(MyMoneySchedule); d->m_endDate = date; } void MyMoneySchedule::setLastDayInMonth(bool state) { Q_D(MyMoneySchedule); d->m_lastDayInMonth = state; } void MyMoneySchedule::setAutoEnter(bool autoenter) { Q_D(MyMoneySchedule); d->m_autoEnter = autoenter; } QDate MyMoneySchedule::startDate() const { Q_D(const MyMoneySchedule); if (d->m_startDate.isValid()) return d->m_startDate; return nextDueDate(); } eMyMoney::Schedule::PaymentType MyMoneySchedule::paymentType() const { Q_D(const MyMoneySchedule); return d->m_paymentType; } /** * Simple get method that returns true if the schedule is fixed. * * @return bool To indicate whether the instance is fixed. */ bool MyMoneySchedule::isFixed() const { Q_D(const MyMoneySchedule); return d->m_fixed; } /** * Simple get method that returns true if the schedule will end * at some time. * * @return bool Indicates whether the instance will end. */ bool MyMoneySchedule::willEnd() const { Q_D(const MyMoneySchedule); return d->m_endDate.isValid(); } QDate MyMoneySchedule::nextDueDate() const { Q_D(const MyMoneySchedule); return d->m_transaction.postDate(); } QDate MyMoneySchedule::adjustedNextDueDate() const { if (isFinished()) return QDate(); if (lastDayInMonth()) { QDate date = nextDueDate(); return adjustedDate(QDate(date.year(), date.month(), date.daysInMonth()), weekendOption()); } return adjustedDate(nextDueDate(), weekendOption()); } QDate MyMoneySchedule::adjustedDate(QDate date, Schedule::WeekendOption option) const { if (!date.isValid() || option == Schedule::WeekendOption::MoveNothing || isProcessingDate(date)) return date; int step = 1; if (option == Schedule::WeekendOption::MoveBefore) step = -1; while (!isProcessingDate(date)) date = date.addDays(step); return date; } void MyMoneySchedule::setNextDueDate(const QDate& date) { Q_D(MyMoneySchedule); if (date.isValid()) { d->m_transaction.setPostDate(date); // m_startDate = date; } } void MyMoneySchedule::setLastPayment(const QDate& date) { Q_D(MyMoneySchedule); // Delete all payments older than date QList::Iterator it; QList delList; for (it = d->m_recordedPayments.begin(); it != d->m_recordedPayments.end(); ++it) { if (*it < date || !date.isValid()) delList.append(*it); } for (it = delList.begin(); it != delList.end(); ++it) { d->m_recordedPayments.removeAll(*it); } d->m_lastPayment = date; if (!d->m_startDate.isValid()) d->m_startDate = date; } QString MyMoneySchedule::name() const { Q_D(const MyMoneySchedule); return d->m_name; } void MyMoneySchedule::setName(const QString& nm) { Q_D(MyMoneySchedule); d->m_name = nm; } eMyMoney::Schedule::WeekendOption MyMoneySchedule::weekendOption() const { Q_D(const MyMoneySchedule); return d->m_weekendOption; } void MyMoneySchedule::setOccurrence(Schedule::Occurrence occ) { auto occ2 = occ; auto mult = 1; simpleToCompoundOccurrence(mult, occ2); setOccurrencePeriod(occ2); setOccurrenceMultiplier(mult); } void MyMoneySchedule::setOccurrencePeriod(Schedule::Occurrence occ) { Q_D(MyMoneySchedule); d->m_occurrence = occ; } void MyMoneySchedule::setOccurrenceMultiplier(int occmultiplier) { Q_D(MyMoneySchedule); d->m_occurrenceMultiplier = occmultiplier < 1 ? 1 : occmultiplier; } void MyMoneySchedule::setType(Schedule::Type type) { Q_D(MyMoneySchedule); d->m_type = type; } void MyMoneySchedule::validate(bool id_check) const { /* Check the supplied instance is valid... * * To be valid it must not have the id set and have the following fields set: * * m_occurrence * m_type * m_startDate * m_paymentType * m_transaction * the transaction must contain at least one split (two is better ;-) ) */ Q_D(const MyMoneySchedule); if (id_check && !d->m_id.isEmpty()) throw MYMONEYEXCEPTION_CSTRING("ID for schedule not empty when required"); if (d->m_occurrence == Schedule::Occurrence::Any) throw MYMONEYEXCEPTION_CSTRING("Invalid occurrence type for schedule"); if (d->m_type == Schedule::Type::Any) throw MYMONEYEXCEPTION_CSTRING("Invalid type for schedule"); if (!nextDueDate().isValid()) throw MYMONEYEXCEPTION_CSTRING("Invalid next due date for schedule"); if (d->m_paymentType == Schedule::PaymentType::Any) throw MYMONEYEXCEPTION_CSTRING("Invalid payment type for schedule"); if (d->m_transaction.splitCount() == 0) throw MYMONEYEXCEPTION_CSTRING("Scheduled transaction does not contain splits"); // Check the payment types switch (d->m_type) { case Schedule::Type::Bill: if (d->m_paymentType == Schedule::PaymentType::DirectDeposit || d->m_paymentType == Schedule::PaymentType::ManualDeposit) throw MYMONEYEXCEPTION_CSTRING("Invalid payment type for bills"); break; case Schedule::Type::Deposit: if (d->m_paymentType == Schedule::PaymentType::DirectDebit || d->m_paymentType == Schedule::PaymentType::WriteChecque) throw MYMONEYEXCEPTION_CSTRING("Invalid payment type for deposits"); break; case Schedule::Type::Any: throw MYMONEYEXCEPTION_CSTRING("Invalid type ANY"); break; case Schedule::Type::Transfer: // if (m_paymentType == DirectDeposit || m_paymentType == ManualDeposit) // return false; break; case Schedule::Type::LoanPayment: break; } } QDate MyMoneySchedule::adjustedNextPayment(const QDate& refDate) const { return nextPaymentDate(true, refDate); } QDate MyMoneySchedule::adjustedNextPayment() const { return adjustedNextPayment(QDate::currentDate()); } QDate MyMoneySchedule::nextPayment(const QDate& refDate) const { return nextPaymentDate(false, refDate); } QDate MyMoneySchedule::nextPayment() const { return nextPayment(QDate::currentDate()); } QDate MyMoneySchedule::nextPaymentDate(const bool& adjust, const QDate& refDate) const { Schedule::WeekendOption option(adjust ? weekendOption() : Schedule::WeekendOption::MoveNothing); Q_D(const MyMoneySchedule); QDate adjEndDate(adjustedDate(d->m_endDate, option)); // if the enddate is valid and it is before the reference date, // then there will be no more payments. if (adjEndDate.isValid() && adjEndDate < refDate) { return QDate(); } QDate dueDate(nextDueDate()); QDate paymentDate(adjustedDate(dueDate, option)); if (paymentDate.isValid() && (paymentDate <= refDate || d->m_recordedPayments.contains(dueDate))) { switch (d->m_occurrence) { case Schedule::Occurrence::Once: // If the lastPayment is already set or the payment should have been // prior to the reference date then invalidate the payment date. if (d->m_lastPayment.isValid() || paymentDate <= refDate) paymentDate = QDate(); break; case Schedule::Occurrence::Daily: { int step = d->m_occurrenceMultiplier; do { dueDate = dueDate.addDays(step); paymentDate = adjustedDate(dueDate, option); } while (paymentDate.isValid() && (paymentDate <= refDate || d->m_recordedPayments.contains(dueDate))); } break; case Schedule::Occurrence::Weekly: { int step = 7 * d->m_occurrenceMultiplier; do { dueDate = dueDate.addDays(step); paymentDate = adjustedDate(dueDate, option); } while (paymentDate.isValid() && (paymentDate <= refDate || d->m_recordedPayments.contains(dueDate))); } break; case Schedule::Occurrence::EveryHalfMonth: do { dueDate = addHalfMonths(dueDate, d->m_occurrenceMultiplier); paymentDate = adjustedDate(dueDate, option); } while (paymentDate.isValid() && (paymentDate <= refDate || d->m_recordedPayments.contains(dueDate))); break; case Schedule::Occurrence::Monthly: do { dueDate = dueDate.addMonths(d->m_occurrenceMultiplier); fixDate(dueDate); paymentDate = adjustedDate(dueDate, option); } while (paymentDate.isValid() && (paymentDate <= refDate || d->m_recordedPayments.contains(dueDate))); break; case Schedule::Occurrence::Yearly: do { dueDate = dueDate.addYears(d->m_occurrenceMultiplier); fixDate(dueDate); paymentDate = adjustedDate(dueDate, option); } while (paymentDate.isValid() && (paymentDate <= refDate || d->m_recordedPayments.contains(dueDate))); break; case Schedule::Occurrence::Any: default: paymentDate = QDate(); break; } } if (paymentDate.isValid() && adjEndDate.isValid() && paymentDate > adjEndDate) paymentDate = QDate(); return paymentDate; } QDate MyMoneySchedule::nextPaymentDate(const bool& adjust) const { return nextPaymentDate(adjust, QDate::currentDate()); } QList MyMoneySchedule::paymentDates(const QDate& _startDate, const QDate& _endDate) const { QDate paymentDate(nextDueDate()); QList theDates; Schedule::WeekendOption option(weekendOption()); Q_D(const MyMoneySchedule); QDate endDate(_endDate); if (willEnd() && d->m_endDate < endDate) { // consider the adjusted end date instead of the plain end date endDate = adjustedDate(d->m_endDate, option); } QDate start_date(adjustedDate(startDate(), option)); // if the period specified by the parameters and the adjusted period // defined for this schedule don't overlap, then the list remains empty if ((willEnd() && adjustedDate(d->m_endDate, option) < _startDate) || start_date > endDate) return theDates; QDate date(adjustedDate(paymentDate, option)); switch (d->m_occurrence) { case Schedule::Occurrence::Once: if (start_date >= _startDate && start_date <= endDate) theDates.append(start_date); break; case Schedule::Occurrence::Daily: while (date.isValid() && (date <= endDate)) { if (date >= _startDate) theDates.append(date); paymentDate = paymentDate.addDays(d->m_occurrenceMultiplier); date = adjustedDate(paymentDate, option); } break; case Schedule::Occurrence::Weekly: { int step = 7 * d->m_occurrenceMultiplier; while (date.isValid() && (date <= endDate)) { if (date >= _startDate) theDates.append(date); paymentDate = paymentDate.addDays(step); date = adjustedDate(paymentDate, option); } } break; case Schedule::Occurrence::EveryHalfMonth: while (date.isValid() && (date <= endDate)) { if (date >= _startDate) theDates.append(date); paymentDate = addHalfMonths(paymentDate, d->m_occurrenceMultiplier); date = adjustedDate(paymentDate, option); } break; case Schedule::Occurrence::Monthly: while (date.isValid() && (date <= endDate)) { if (date >= _startDate) theDates.append(date); paymentDate = paymentDate.addMonths(d->m_occurrenceMultiplier); fixDate(paymentDate); date = adjustedDate(paymentDate, option); } break; case Schedule::Occurrence::Yearly: while (date.isValid() && (date <= endDate)) { if (date >= _startDate) theDates.append(date); paymentDate = paymentDate.addYears(d->m_occurrenceMultiplier); fixDate(paymentDate); date = adjustedDate(paymentDate, option); } break; case Schedule::Occurrence::Any: default: break; } return theDates; } bool MyMoneySchedule::operator <(const MyMoneySchedule& right) const { return adjustedNextDueDate() < right.adjustedNextDueDate(); } bool MyMoneySchedule::operator ==(const MyMoneySchedule& right) const { Q_D(const MyMoneySchedule); auto d2 = static_cast(right.d_func()); if (MyMoneyObject::operator==(right) && d->m_occurrence == d2->m_occurrence && d->m_occurrenceMultiplier == d2->m_occurrenceMultiplier && d->m_type == d2->m_type && d->m_startDate == d2->m_startDate && d->m_paymentType == d2->m_paymentType && d->m_fixed == d2->m_fixed && d->m_transaction == d2->m_transaction && d->m_endDate == d2->m_endDate && d->m_lastDayInMonth == d2->m_lastDayInMonth && d->m_autoEnter == d2->m_autoEnter && d->m_lastPayment == d2->m_lastPayment && ((d->m_name.length() == 0 && d2->m_name.length() == 0) || (d->m_name == d2->m_name))) return true; return false; } bool MyMoneySchedule::operator !=(const MyMoneySchedule& right) const { return ! operator==(right); } int MyMoneySchedule::transactionsRemaining() const { Q_D(const MyMoneySchedule); return transactionsRemainingUntil(adjustedDate(d->m_endDate, weekendOption())); } int MyMoneySchedule::transactionsRemainingUntil(const QDate& endDate) const { auto counter = 0; Q_D(const MyMoneySchedule); QDate startDate = d->m_lastPayment.isValid() ? d->m_lastPayment : d->m_startDate; if (startDate.isValid() && endDate.isValid()) { QList dates = paymentDates(startDate, endDate); counter = dates.count(); } return counter; } QDate MyMoneySchedule::endDate() const { Q_D(const MyMoneySchedule); return d->m_endDate; } bool MyMoneySchedule::autoEnter() const { Q_D(const MyMoneySchedule); return d->m_autoEnter; } bool MyMoneySchedule::lastDayInMonth() const { Q_D(const MyMoneySchedule); return d->m_lastDayInMonth; } MyMoneyTransaction MyMoneySchedule::transaction() const { Q_D(const MyMoneySchedule); return d->m_transaction; } QDate MyMoneySchedule::lastPayment() const { Q_D(const MyMoneySchedule); return d->m_lastPayment; } MyMoneyAccount MyMoneySchedule::account(int cnt) const { Q_D(const MyMoneySchedule); QList splits = d->m_transaction.splits(); QList::ConstIterator it; auto file = MyMoneyFile::instance(); MyMoneyAccount acc; // search the first asset or liability account for (it = splits.constBegin(); it != splits.constEnd() && (acc.id().isEmpty() || cnt); ++it) { try { acc = file->account((*it).accountId()); if (acc.isAssetLiability()) --cnt; if (!cnt) return acc; } catch (const MyMoneyException &) { qWarning("Schedule '%s' references unknown account '%s'", qPrintable(id()), qPrintable((*it).accountId())); return MyMoneyAccount(); } } return MyMoneyAccount(); } MyMoneyAccount MyMoneySchedule::transferAccount() const { return account(2); } QDate MyMoneySchedule::dateAfter(int transactions) const { auto counter = 1; QDate paymentDate(startDate()); if (transactions <= 0) return paymentDate; Q_D(const MyMoneySchedule); switch (d->m_occurrence) { case Schedule::Occurrence::Once: break; case Schedule::Occurrence::Daily: while (counter++ < transactions) paymentDate = paymentDate.addDays(d->m_occurrenceMultiplier); break; case Schedule::Occurrence::Weekly: { int step = 7 * d->m_occurrenceMultiplier; while (counter++ < transactions) paymentDate = paymentDate.addDays(step); } break; case Schedule::Occurrence::EveryHalfMonth: paymentDate = addHalfMonths(paymentDate, d->m_occurrenceMultiplier * (transactions - 1)); break; case Schedule::Occurrence::Monthly: while (counter++ < transactions) paymentDate = paymentDate.addMonths(d->m_occurrenceMultiplier); break; case Schedule::Occurrence::Yearly: while (counter++ < transactions) paymentDate = paymentDate.addYears(d->m_occurrenceMultiplier); break; case Schedule::Occurrence::Any: default: break; } return paymentDate; } bool MyMoneySchedule::isOverdue() const { if (isFinished()) return false; if (adjustedNextDueDate() >= QDate::currentDate()) return false; return true; } bool MyMoneySchedule::isFinished() const { Q_D(const MyMoneySchedule); if (!d->m_lastPayment.isValid()) return false; if (d->m_endDate.isValid()) { if (d->m_lastPayment >= d->m_endDate || !nextDueDate().isValid() || nextDueDate() > d->m_endDate) return true; } // Check to see if its a once off payment if (d->m_occurrence == Schedule::Occurrence::Once) return true; return false; } bool MyMoneySchedule::hasRecordedPayment(const QDate& date) const { Q_D(const MyMoneySchedule); // m_lastPayment should always be > recordedPayments() if (d->m_lastPayment.isValid() && d->m_lastPayment >= date) return true; if (d->m_recordedPayments.contains(date)) return true; return false; } void MyMoneySchedule::recordPayment(const QDate& date) { Q_D(MyMoneySchedule); d->m_recordedPayments.append(date); } QList MyMoneySchedule::recordedPayments() const { Q_D(const MyMoneySchedule); return d->m_recordedPayments; } void MyMoneySchedule::setWeekendOption(const Schedule::WeekendOption option) { Q_D(MyMoneySchedule); // make sure only valid values are used. Invalid defaults to MoveNothing. switch (option) { case Schedule::WeekendOption::MoveBefore: case Schedule::WeekendOption::MoveAfter: d->m_weekendOption = option; break; default: d->m_weekendOption = Schedule::WeekendOption::MoveNothing; break; } } void MyMoneySchedule::fixDate(QDate& date) const { Q_D(const MyMoneySchedule); QDate fixDate(d->m_startDate); if (fixDate.isValid() && date.day() != fixDate.day() && QDate::isValid(date.year(), date.month(), fixDate.day())) { date = QDate(date.year(), date.month(), fixDate.day()); } } void MyMoneySchedule::writeXML(QDomDocument& document, QDomElement& parent) const { auto el = document.createElement(nodeNames[nnScheduleTX]); Q_D(const MyMoneySchedule); d->writeBaseXML(document, el); el.setAttribute(d->getAttrName(Schedule::Attribute::Name), d->m_name); el.setAttribute(d->getAttrName(Schedule::Attribute::Type), (int)d->m_type); el.setAttribute(d->getAttrName(Schedule::Attribute::Occurrence), (int)d->m_occurrence); el.setAttribute(d->getAttrName(Schedule::Attribute::OccurrenceMultiplier), d->m_occurrenceMultiplier); el.setAttribute(d->getAttrName(Schedule::Attribute::PaymentType), (int)d->m_paymentType); el.setAttribute(d->getAttrName(Schedule::Attribute::StartDate), MyMoneyUtils::dateToString(d->m_startDate)); el.setAttribute(d->getAttrName(Schedule::Attribute::EndDate), MyMoneyUtils::dateToString(d->m_endDate)); el.setAttribute(d->getAttrName(Schedule::Attribute::Fixed), d->m_fixed); el.setAttribute(d->getAttrName(Schedule::Attribute::LastDayInMonth), d->m_lastDayInMonth); el.setAttribute(d->getAttrName(Schedule::Attribute::AutoEnter), d->m_autoEnter); el.setAttribute(d->getAttrName(Schedule::Attribute::LastPayment), MyMoneyUtils::dateToString(d->m_lastPayment)); el.setAttribute(d->getAttrName(Schedule::Attribute::WeekendOption), (int)d->m_weekendOption); //store the payment history for this scheduled task. QList payments = recordedPayments(); QList::ConstIterator it; QDomElement paymentsElement = document.createElement(d->getElName(Schedule::Element::Payments)); for (it = payments.constBegin(); it != payments.constEnd(); ++it) { QDomElement paymentEntry = document.createElement(d->getElName(Schedule::Element::Payment)); paymentEntry.setAttribute(d->getAttrName(Schedule::Attribute::Date), MyMoneyUtils::dateToString(*it)); paymentsElement.appendChild(paymentEntry); } el.appendChild(paymentsElement); //store the transaction data for this task. d->m_transaction.writeXML(document, el); parent.appendChild(el); } bool MyMoneySchedule::hasReferenceTo(const QString& id) const { Q_D(const MyMoneySchedule); return d->m_transaction.hasReferenceTo(id); } QString MyMoneySchedule::occurrenceToString() const { return occurrenceToString(occurrenceMultiplier(), occurrencePeriod()); } QString MyMoneySchedule::occurrenceToString(Schedule::Occurrence occurrence) { QString occurrenceString = I18N_NOOP2("Frequency of schedule", "Any"); if (occurrence == Schedule::Occurrence::Once) occurrenceString = I18N_NOOP2("Frequency of schedule", "Once"); else if (occurrence == Schedule::Occurrence::Daily) occurrenceString = I18N_NOOP2("Frequency of schedule", "Daily"); else if (occurrence == Schedule::Occurrence::Weekly) occurrenceString = I18N_NOOP2("Frequency of schedule", "Weekly"); else if (occurrence == Schedule::Occurrence::Fortnightly) occurrenceString = I18N_NOOP2("Frequency of schedule", "Fortnightly"); else if (occurrence == Schedule::Occurrence::EveryOtherWeek) occurrenceString = I18N_NOOP2("Frequency of schedule", "Every other week"); else if (occurrence == Schedule::Occurrence::EveryHalfMonth) occurrenceString = I18N_NOOP2("Frequency of schedule", "Every half month"); else if (occurrence == Schedule::Occurrence::EveryThreeWeeks) occurrenceString = I18N_NOOP2("Frequency of schedule", "Every three weeks"); else if (occurrence == Schedule::Occurrence::EveryFourWeeks) occurrenceString = I18N_NOOP2("Frequency of schedule", "Every four weeks"); else if (occurrence == Schedule::Occurrence::EveryThirtyDays) occurrenceString = I18N_NOOP2("Frequency of schedule", "Every thirty days"); else if (occurrence == Schedule::Occurrence::Monthly) occurrenceString = I18N_NOOP2("Frequency of schedule", "Monthly"); else if (occurrence == Schedule::Occurrence::EveryEightWeeks) occurrenceString = I18N_NOOP2("Frequency of schedule", "Every eight weeks"); else if (occurrence == Schedule::Occurrence::EveryOtherMonth) occurrenceString = I18N_NOOP2("Frequency of schedule", "Every two months"); else if (occurrence == Schedule::Occurrence::EveryThreeMonths) occurrenceString = I18N_NOOP2("Frequency of schedule", "Every three months"); else if (occurrence == Schedule::Occurrence::Quarterly) occurrenceString = I18N_NOOP2("Frequency of schedule", "Quarterly"); else if (occurrence == Schedule::Occurrence::EveryFourMonths) occurrenceString = I18N_NOOP2("Frequency of schedule", "Every four months"); else if (occurrence == Schedule::Occurrence::TwiceYearly) occurrenceString = I18N_NOOP2("Frequency of schedule", "Twice yearly"); else if (occurrence == Schedule::Occurrence::Yearly) occurrenceString = I18N_NOOP2("Frequency of schedule", "Yearly"); else if (occurrence == Schedule::Occurrence::EveryOtherYear) occurrenceString = I18N_NOOP2("Frequency of schedule", "Every other year"); return occurrenceString; } QString MyMoneySchedule::occurrenceToString(int mult, Schedule::Occurrence type) { QString occurrenceString = I18N_NOOP2("Frequency of schedule", "Any"); if (type == Schedule::Occurrence::Once) switch (mult) { case 1: occurrenceString = I18N_NOOP2("Frequency of schedule", "Once"); break; default: occurrenceString = I18N_NOOP2("Frequency of schedule", QString("%1 times").arg(mult)); } else if (type == Schedule::Occurrence::Daily) switch (mult) { case 1: occurrenceString = I18N_NOOP2("Frequency of schedule", "Daily"); break; case 30: occurrenceString = I18N_NOOP2("Frequency of schedule", "Every thirty days"); break; default: occurrenceString = I18N_NOOP2("Frequency of schedule", QString("Every %1 days").arg(mult)); } else if (type == Schedule::Occurrence::Weekly) switch (mult) { case 1: occurrenceString = I18N_NOOP2("Frequency of schedule", "Weekly"); break; case 2: occurrenceString = I18N_NOOP2("Frequency of schedule", "Every other week"); break; case 3: occurrenceString = I18N_NOOP2("Frequency of schedule", "Every three weeks"); break; case 4: occurrenceString = I18N_NOOP2("Frequency of schedule", "Every four weeks"); break; case 8: occurrenceString = I18N_NOOP2("Frequency of schedule", "Every eight weeks"); break; default: occurrenceString = I18N_NOOP2("Frequency of schedule", QString("Every %1 weeks").arg(mult)); } else if (type == Schedule::Occurrence::EveryHalfMonth) switch (mult) { case 1: occurrenceString = I18N_NOOP2("Frequency of schedule", "Every half month"); break; default: occurrenceString = I18N_NOOP2("Frequency of schedule", QString("Every %1 half months").arg(mult)); } else if (type == Schedule::Occurrence::Monthly) switch (mult) { case 1: occurrenceString = I18N_NOOP2("Frequency of schedule", "Monthly"); break; case 2: occurrenceString = I18N_NOOP2("Frequency of schedule", "Every two months"); break; case 3: occurrenceString = I18N_NOOP2("Frequency of schedule", "Every three months"); break; case 4: occurrenceString = I18N_NOOP2("Frequency of schedule", "Every four months"); break; case 6: occurrenceString = I18N_NOOP2("Frequency of schedule", "Twice yearly"); break; default: occurrenceString = I18N_NOOP2("Frequency of schedule", QString("Every %1 months").arg(mult)); } else if (type == Schedule::Occurrence::Yearly) switch (mult) { case 1: occurrenceString = I18N_NOOP2("Frequency of schedule", "Yearly"); break; case 2: occurrenceString = I18N_NOOP2("Frequency of schedule", "Every other year"); break; default: occurrenceString = I18N_NOOP2("Frequency of schedule", QString("Every %1 years").arg(mult)); } return occurrenceString; } QString MyMoneySchedule::occurrencePeriodToString(Schedule::Occurrence type) { QString occurrenceString = I18N_NOOP2("Schedule occurrence period", "Any"); if (type == Schedule::Occurrence::Once) occurrenceString = I18N_NOOP2("Schedule occurrence period", "Once"); else if (type == Schedule::Occurrence::Daily) occurrenceString = I18N_NOOP2("Schedule occurrence period", "Day"); else if (type == Schedule::Occurrence::Weekly) occurrenceString = I18N_NOOP2("Schedule occurrence period", "Week"); else if (type == Schedule::Occurrence::EveryHalfMonth) occurrenceString = I18N_NOOP2("Schedule occurrence period", "Half-month"); else if (type == Schedule::Occurrence::Monthly) occurrenceString = I18N_NOOP2("Schedule occurrence period", "Month"); else if (type == Schedule::Occurrence::Yearly) occurrenceString = I18N_NOOP2("Schedule occurrence period", "Year"); return occurrenceString; } QString MyMoneySchedule::scheduleTypeToString(Schedule::Type type) { QString text; switch (type) { case Schedule::Type::Bill: text = I18N_NOOP2("Scheduled transaction type", "Bill"); break; case Schedule::Type::Deposit: text = I18N_NOOP2("Scheduled transaction type", "Deposit"); break; case Schedule::Type::Transfer: text = I18N_NOOP2("Scheduled transaction type", "Transfer"); break; case Schedule::Type::LoanPayment: text = I18N_NOOP2("Scheduled transaction type", "Loan payment"); break; case Schedule::Type::Any: default: text = I18N_NOOP2("Scheduled transaction type", "Unknown"); } return text; } QString MyMoneySchedule::paymentMethodToString(Schedule::PaymentType paymentType) { QString text; switch (paymentType) { case Schedule::PaymentType::DirectDebit: text = I18N_NOOP2("Scheduled Transaction payment type", "Direct debit"); break; case Schedule::PaymentType::DirectDeposit: text = I18N_NOOP2("Scheduled Transaction payment type", "Direct deposit"); break; case Schedule::PaymentType::ManualDeposit: text = I18N_NOOP2("Scheduled Transaction payment type", "Manual deposit"); break; case Schedule::PaymentType::Other: text = I18N_NOOP2("Scheduled Transaction payment type", "Other"); break; case Schedule::PaymentType::WriteChecque: text = I18N_NOOP2("Scheduled Transaction payment type", "Write check"); break; case Schedule::PaymentType::StandingOrder: text = I18N_NOOP2("Scheduled Transaction payment type", "Standing order"); break; case Schedule::PaymentType::BankTransfer: text = I18N_NOOP2("Scheduled Transaction payment type", "Bank transfer"); break; case Schedule::PaymentType::Any: text = I18N_NOOP2("Scheduled Transaction payment type", "Any (Error)"); break; } return text; } QString MyMoneySchedule::weekendOptionToString(Schedule::WeekendOption weekendOption) { QString text; switch (weekendOption) { case Schedule::WeekendOption::MoveBefore: text = I18N_NOOP("Change the date to the previous processing day"); break; case Schedule::WeekendOption::MoveAfter: text = I18N_NOOP("Change the date to the next processing day"); break; case Schedule::WeekendOption::MoveNothing: text = I18N_NOOP("Do not change the date"); break; } return text; } // until we don't have the means to store the value // of the variation, we default to 10% in case this // scheduled transaction is marked 'not fixed'. // // ipwizard 2009-04-18 int MyMoneySchedule::variation() const { int rc = 0; if (!isFixed()) { rc = 10; #if 0 QString var = value("kmm-variation"); if (!var.isEmpty()) rc = var.toInt(); #endif } return rc; } void MyMoneySchedule::setVariation(int var) { Q_UNUSED(var) #if 0 deletePair("kmm-variation"); if (var != 0) setValue("kmm-variation", QString("%1").arg(var)); #endif } int MyMoneySchedule::eventsPerYear(Schedule::Occurrence occurrence) { int rc = 0; switch (occurrence) { case Schedule::Occurrence::Daily: rc = 365; break; case Schedule::Occurrence::Weekly: rc = 52; break; case Schedule::Occurrence::Fortnightly: rc = 26; break; case Schedule::Occurrence::EveryOtherWeek: rc = 26; break; case Schedule::Occurrence::EveryHalfMonth: rc = 24; break; case Schedule::Occurrence::EveryThreeWeeks: rc = 17; break; case Schedule::Occurrence::EveryFourWeeks: rc = 13; break; case Schedule::Occurrence::Monthly: case Schedule::Occurrence::EveryThirtyDays: rc = 12; break; case Schedule::Occurrence::EveryEightWeeks: rc = 6; break; case Schedule::Occurrence::EveryOtherMonth: rc = 6; break; case Schedule::Occurrence::EveryThreeMonths: case Schedule::Occurrence::Quarterly: rc = 4; break; case Schedule::Occurrence::EveryFourMonths: rc = 3; break; case Schedule::Occurrence::TwiceYearly: rc = 2; break; case Schedule::Occurrence::Yearly: rc = 1; break; default: qWarning("Occurrence not supported by financial calculator"); } return rc; } int MyMoneySchedule::daysBetweenEvents(Schedule::Occurrence occurrence) { int rc = 0; switch (occurrence) { case Schedule::Occurrence::Daily: rc = 1; break; case Schedule::Occurrence::Weekly: rc = 7; break; case Schedule::Occurrence::Fortnightly: rc = 14; break; case Schedule::Occurrence::EveryOtherWeek: rc = 14; break; case Schedule::Occurrence::EveryHalfMonth: rc = 15; break; case Schedule::Occurrence::EveryThreeWeeks: rc = 21; break; case Schedule::Occurrence::EveryFourWeeks: rc = 28; break; case Schedule::Occurrence::EveryThirtyDays: rc = 30; break; case Schedule::Occurrence::Monthly: rc = 30; break; case Schedule::Occurrence::EveryEightWeeks: rc = 56; break; case Schedule::Occurrence::EveryOtherMonth: rc = 60; break; case Schedule::Occurrence::EveryThreeMonths: case Schedule::Occurrence::Quarterly: rc = 90; break; case Schedule::Occurrence::EveryFourMonths: rc = 120; break; case Schedule::Occurrence::TwiceYearly: rc = 180; break; case Schedule::Occurrence::Yearly: rc = 360; break; default: qWarning("Occurrence not supported by financial calculator"); } return rc; } QDate MyMoneySchedule::addHalfMonths(QDate date, int mult) const { QDate newdate = date; int d, dm; if (mult > 0) { d = newdate.day(); if (d <= 12) { if (mult % 2 == 0) newdate = newdate.addMonths(mult >> 1); else newdate = newdate.addMonths(mult >> 1).addDays(15); } else for (int i = 0; i < mult; i++) { if (d <= 13) newdate = newdate.addDays(15); else { dm = newdate.daysInMonth(); if (d == 14) newdate = newdate.addDays((dm < 30) ? dm - d : 15); else if (d == 15) newdate = newdate.addDays(dm - d); else if (d == dm) newdate = newdate.addDays(15 - d).addMonths(1); else newdate = newdate.addDays(-15).addMonths(1); } d = newdate.day(); } } else if (mult < 0) // Go backwards for (int i = 0; i > mult; i--) { d = newdate.day(); dm = newdate.daysInMonth(); if (d > 15) { dm = newdate.daysInMonth(); newdate = newdate.addDays((d == dm) ? 15 - dm : -15); } else if (d <= 13) newdate = newdate.addMonths(-1).addDays(15); else if (d == 15) newdate = newdate.addDays(-15); else { // 14 newdate = newdate.addMonths(-1); dm = newdate.daysInMonth(); newdate = newdate.addDays((dm < 30) ? dm - d : 15); } } return newdate; } /** * Helper method to convert simple occurrence to compound occurrence + multiplier * * @param multiplier Returned by reference. Adjusted multiplier * @param occurrence Returned by reference. Occurrence type */ void MyMoneySchedule::simpleToCompoundOccurrence(int& multiplier, Schedule::Occurrence& occurrence) { Schedule::Occurrence newOcc = occurrence; int newMulti = 1; if (occurrence == Schedule::Occurrence::Once || occurrence == Schedule::Occurrence::Daily || occurrence == Schedule::Occurrence::Weekly || occurrence == Schedule::Occurrence::EveryHalfMonth || occurrence == Schedule::Occurrence::Monthly || occurrence == Schedule::Occurrence::Yearly) { // Already a base occurrence and multiplier } else if (occurrence == Schedule::Occurrence::Fortnightly || occurrence == Schedule::Occurrence::EveryOtherWeek) { newOcc = Schedule::Occurrence::Weekly; newMulti = 2; } else if (occurrence == Schedule::Occurrence::EveryThreeWeeks) { newOcc = Schedule::Occurrence::Weekly; newMulti = 3; } else if (occurrence == Schedule::Occurrence::EveryFourWeeks) { newOcc = Schedule::Occurrence::Weekly; newMulti = 4; } else if (occurrence == Schedule::Occurrence::EveryThirtyDays) { newOcc = Schedule::Occurrence::Daily; newMulti = 30; } else if (occurrence == Schedule::Occurrence::EveryEightWeeks) { newOcc = Schedule::Occurrence::Weekly; newMulti = 8; } else if (occurrence == Schedule::Occurrence::EveryOtherMonth) { newOcc = Schedule::Occurrence::Monthly; newMulti = 2; } else if (occurrence == Schedule::Occurrence::EveryThreeMonths || occurrence == Schedule::Occurrence::Quarterly) { newOcc = Schedule::Occurrence::Monthly; newMulti = 3; } else if (occurrence == Schedule::Occurrence::EveryFourMonths) { newOcc = Schedule::Occurrence::Monthly; newMulti = 4; } else if (occurrence == Schedule::Occurrence::TwiceYearly) { newOcc = Schedule::Occurrence::Monthly; newMulti = 6; } else if (occurrence == Schedule::Occurrence::EveryOtherYear) { newOcc = Schedule::Occurrence::Yearly; newMulti = 2; } else { // Unknown newOcc = Schedule::Occurrence::Any; newMulti = 1; } if (newOcc != occurrence) { occurrence = newOcc; multiplier = newMulti == 1 ? multiplier : newMulti * multiplier; } } /** * Helper method to convert compound occurrence + multiplier to simple occurrence * * @param multiplier Returned by reference. Adjusted multiplier * @param occurrence Returned by reference. Occurrence type */ void MyMoneySchedule::compoundToSimpleOccurrence(int& multiplier, Schedule::Occurrence& occurrence) { Schedule::Occurrence newOcc = occurrence; if (occurrence == Schedule::Occurrence::Once) { // Nothing to do } else if (occurrence == Schedule::Occurrence::Daily) { switch (multiplier) { case 1: break; case 30: newOcc = Schedule::Occurrence::EveryThirtyDays; break; } } else if (newOcc == Schedule::Occurrence::Weekly) { switch (multiplier) { case 1: break; case 2: newOcc = Schedule::Occurrence::EveryOtherWeek; break; case 3: newOcc = Schedule::Occurrence::EveryThreeWeeks; break; case 4: newOcc = Schedule::Occurrence::EveryFourWeeks; break; case 8: newOcc = Schedule::Occurrence::EveryEightWeeks; break; } } else if (occurrence == Schedule::Occurrence::Monthly) switch (multiplier) { case 1: break; case 2: newOcc = Schedule::Occurrence::EveryOtherMonth; break; case 3: newOcc = Schedule::Occurrence::EveryThreeMonths; break; case 4: newOcc = Schedule::Occurrence::EveryFourMonths; break; case 6: newOcc = Schedule::Occurrence::TwiceYearly; break; } else if (occurrence == Schedule::Occurrence::EveryHalfMonth) switch (multiplier) { case 1: break; } else if (occurrence == Schedule::Occurrence::Yearly) { switch (multiplier) { case 1: break; case 2: newOcc = Schedule::Occurrence::EveryOtherYear; break; } } if (occurrence != newOcc) { // Changed to derived type occurrence = newOcc; multiplier = 1; } } void MyMoneySchedule::setProcessingCalendar(IMyMoneyProcessingCalendar* pc) { processingCalendarPtr = pc; } bool MyMoneySchedule::isProcessingDate(const QDate& date) const { if (processingCalendarPtr) return processingCalendarPtr->isProcessingDate(date); /// @todo test against m_processingDays instead? (currently only for tests) return date.dayOfWeek() < Qt::Saturday; } IMyMoneyProcessingCalendar* MyMoneySchedule::processingCalendar() const { return processingCalendarPtr; } bool MyMoneySchedule::replaceId(const QString& newId, const QString& oldId) { Q_D(MyMoneySchedule); return d->m_transaction.replaceId(newId, oldId); } diff --git a/kmymoney/mymoney/mymoneyschedule.h b/kmymoney/mymoney/mymoneyschedule.h index 826728e8b..66c43a4a0 100644 --- a/kmymoney/mymoney/mymoneyschedule.h +++ b/kmymoney/mymoney/mymoneyschedule.h @@ -1,733 +1,734 @@ /* * Copyright 2000-2004 Michael Edwardes * Copyright 2002-2018 Thomas Baumgart * Copyright 2005 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. * * 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 MYMONEYSCHEDULE_H #define MYMONEYSCHEDULE_H // ---------------------------------------------------------------------------- // QT Includes #include // ---------------------------------------------------------------------------- // Project Includes #include "kmm_mymoney_export.h" #include "mymoneyunittestable.h" #include "mymoneyobject.h" class QString; class QDate; class IMyMoneyProcessingCalendar; class MyMoneyAccount; class MyMoneyTransaction; namespace eMyMoney { namespace Schedule { enum class Type; enum class Occurrence; enum class PaymentType; enum class WeekendOption; } } template class QList; /** * @author Michael Edwardes */ /** * This class represents a schedule. (A series of bills, deposits or * transfers). * * @short A class to represent a schedule. * @see MyMoneyScheduled */ class MyMoneySchedulePrivate; class KMM_MYMONEY_EXPORT MyMoneySchedule : public MyMoneyObject { Q_DECLARE_PRIVATE(MyMoneySchedule) friend class MyMoneyStorageANON; KMM_MYMONEY_UNIT_TESTABLE public: /** * Standard constructor */ MyMoneySchedule(); + explicit MyMoneySchedule(const QString &id); /** * Constructor for initialising the object. * * Please note that the optional fields are not set and the transaction * MUST be set before it can be used. * * @a startDate is not used anymore and internally set to QDate() */ explicit MyMoneySchedule(const QString& name, eMyMoney::Schedule::Type type, eMyMoney::Schedule::Occurrence occurrence, int occurrenceMultiplier, eMyMoney::Schedule::PaymentType paymentType, const QDate& startDate, const QDate& endDate, bool fixed, bool autoEnter); explicit MyMoneySchedule(const QDomElement& node); MyMoneySchedule(const QString& id, const MyMoneySchedule& other); MyMoneySchedule(const MyMoneySchedule & other); MyMoneySchedule(MyMoneySchedule && other); MyMoneySchedule & operator=(MyMoneySchedule other); friend void swap(MyMoneySchedule& first, MyMoneySchedule& second); /** * Standard destructor */ ~MyMoneySchedule(); /** * Simple get method that returns the occurrence frequency. * * @return eMyMoney::Schedule::Occurrence The instance frequency. */ eMyMoney::Schedule::Occurrence occurrence() const; /** * Simple get method that returns the occurrence period * multiplier and occurrence * * @return eMyMoney::Schedule::Occurrence The instance period * */ eMyMoney::Schedule::Occurrence occurrencePeriod() const; /** * Simple get method that returns the occurrence period multiplier. * * @return int The frequency multiplier */ int occurrenceMultiplier() const; /** * Simple get method that returns the schedule type. * * @return eMyMoney::Schedule::Type The instance type. */ eMyMoney::Schedule::Type type() const; /** * Simple get method that returns the schedule startDate. If * the schedule has been executed once, the date of the first * execution is returned. Otherwise, the next due date is * returned. * * @return reference to QDate containing the start date. */ QDate startDate() const; /** * Simple get method that returns the schedule paymentType. * * @return eMyMoney::Schedule::PaymentType The instance paymentType. */ eMyMoney::Schedule::PaymentType paymentType() const; /** * Simple get method that returns true if the schedule is fixed. * * @return bool To indicate whether the instance is fixed. */ bool isFixed() const; /** * Simple get method that returns true if the schedule will end * at some time. * * @return bool Indicates whether the instance will end. */ bool willEnd() const; /** * Simple get method that returns the number of transactions remaining. * * @return int The number of transactions remaining for the instance. */ int transactionsRemaining() const; /** * Simple method that returns the number of transactions remaining * until a given date. * * @param endDate Date to count transactions to. * @return int The number of transactions remaining for the instance. */ int transactionsRemainingUntil(const QDate& endDate) const; /** * Simple get method that returns the schedule end date. * * @return QDate The end date for the instance. */ QDate endDate() const; /** * Get the state if the schedule should be processed at the last day * of a month * * @return state of the flag */ bool lastDayInMonth() const; /** * Simple get method that returns true if the transaction should be * automatically entered into the register. * * @return bool Indicates whether the instance will be automatically entered. */ bool autoEnter() const; /** * Simple get method that returns the transaction data for the schedule. * * @return MyMoneyTransaction The transaction data for the instance. */ MyMoneyTransaction transaction() const; /** * Simple method that returns the schedules last payment. If the * schedule has never been executed, QDate() will be returned. * * @return QDate The last payment for the schedule. */ QDate lastPayment() const; /** * Simple method that returns the next due date for the schedule. * * @return reference to QDate containing the next due date. * * @note The date returned can represent a value that is past * a possible end of the schedule. Make sure to consider * the return value of isFinished() when using the value returned. */ QDate nextDueDate() const; /** * This method returns the next due date adjusted * according to the rules specified by the schedule's weekend option. * * @return QDate containing the adjusted next due date. If the * schedule is finished (@sa isFinished()) then the method * returns an invalid QDate. * * @sa weekendOption() * @sa adjustedDate() */ QDate adjustedNextDueDate() const; /** * This method adjusts returns the date adjusted according to the * rules specified by the schedule's weekend option. * * @return QDate containing the adjusted date. */ QDate adjustedDate(QDate date, eMyMoney::Schedule::WeekendOption option) const; /** * Get the weekendOption that determines how the schedule check code * will enter transactions that occur on a non-processing day (usually * a weekend). * * This not used by MyMoneySchedule but by the support code. **/ eMyMoney::Schedule::WeekendOption weekendOption() const; /** * Simple method that sets the frequency for the schedule. * * @param occ The new occurrence (frequency). * @return none */ void setOccurrence(eMyMoney::Schedule::Occurrence occ); /** * Simple method that sets the schedule period * * @param occ The new occurrence period (frequency) * @return none */ void setOccurrencePeriod(eMyMoney::Schedule::Occurrence occ); /** * Simple method that sets the frequency multiplier for the schedule. * * @param occmultiplier The new occurrence (frequency) multiplier. * @return none */ void setOccurrenceMultiplier(int occmultiplier); /** * Simple method that sets the type for the schedule. * * @param type The new type. * @return none */ void setType(eMyMoney::Schedule::Type type); /** * Simple method that sets the start date for the schedule. * * @param date The new start date. * @return none */ void setStartDate(const QDate& date); /** * Simple method that sets the payment type for the schedule. * * @param type The new payment type. * @return none */ void setPaymentType(eMyMoney::Schedule::PaymentType type); /** * Simple method to set whether the schedule is fixed or not. * * @param fixed boolean to indicate whether the instance is fixed. * @return none */ void setFixed(bool fixed); /** * Simple method that sets the transaction for the schedule. * The transaction must have a valid postDate set, otherwise * it will not be accepted. * * @param transaction The new transaction. * @return none */ void setTransaction(const MyMoneyTransaction& transaction); /** * Simple set method to set the end date for the schedule. * * @param date The new end date. * @return none */ void setEndDate(const QDate& date); /** * Simple method to set whether the schedule should be performed at * the last day of a month. * * @param state boolean The state to set * @return none */ void setLastDayInMonth(bool state); /** * Simple set method to set whether this transaction should be automatically * entered into the journal whenever it is due. * * @param autoenter boolean to indicate whether we need to automatically * enter the transaction. * @return none */ void setAutoEnter(bool autoenter); /** * Simple set method to set the schedule's next payment date. * * @param date The next payment date. * @return none */ void setNextDueDate(const QDate& date); /** * Simple set method to set the schedule's last payment. If * this method is called for the first time on the object, * the @a m_startDate member will be set to @a date as well. * * This method should be called whenever a schedule is entered or skipped. * * @param date The last payment date. * @return none */ void setLastPayment(const QDate& date); /** * Set the weekendOption that determines how the schedule check code * will enter transactions that occur on a non-processing day (usually * a weekend). The following values * are valid: * * - MoveNothing: don't modify date * - MoveBefore: modify the date to the previous processing day * - MoveAfter: modify the date to the next processing day * * If an invalid option is given, the option is set to MoveNothing. * * @param option See list in description * @return none * * @note This not used by MyMoneySchedule but by the support code. **/ void setWeekendOption(const eMyMoney::Schedule::WeekendOption option); /** * Validates the schedule instance. * * Makes sure the paymentType matches the type and that the required * fields have been set. * * @param id_check if @p true, the method will check for an empty id. * if @p false, this check is skipped. Default is @p true. * * @return If this method returns, all checks are passed. Otherwise, * it will throw a MyMoneyException object. * * @exception MyMoneyException with detailed error information is thrown * in case of failure of any check. */ void validate(bool id_check = true) const; /** * Calculates the date of the next payment adjusted according to the * rules specified by the schedule's weekend option. * * @param refDate The reference date from which the next payment * date will be calculated (defaults to current date) * * @return QDate The adjusted date the next payment is due. This date is * always past @a refDate. In case of an error or if there * are no more payments then an empty/invalid QDate() will * be returned. */ QDate adjustedNextPayment(const QDate& refDate) const; QDate adjustedNextPayment() const; /** * Calculates the date of the next payment. * * @param refDate The reference date from which the next payment * date will be calculated (defaults to current date) * * @return QDate The date the next payment is due. This date is * always past @a refDate. In case of an error or * if there are no more payments then an empty/invalid QDate() * will be returned. */ QDate nextPayment(const QDate& refDate) const; QDate nextPayment() const; /** * Calculates the date of the next payment and adjusts if asked. * * @param adjust Whether to adjust the calculated date according to the * rules specified by the schedule's weekend option. * @param refDate The reference date from which the next payment * date will be calculated (defaults to current date) * * @return QDate The date the next payment is due. This date is * always past @a refDate. In case of an error or * if there is no more payments then an empty/invalid QDate() * will be returned. */ QDate nextPaymentDate(const bool& adjust, const QDate& refDate) const; QDate nextPaymentDate(const bool& adjust) const; /** * Calculates the dates of the payment over a certain period of time. * * An empty list is returned for no payments or error. * * @param startDate The start date for the range calculations * @param endDate The end date for the range calculations. * @return QList The dates on which the payments are due. */ QList paymentDates(const QDate& startDate, const QDate& endDate) const; /** * Returns the instances name * * @return The name */ QString name() const; /** * Changes the instance name * * @param nm The new name * @return none */ void setName(const QString& nm); bool operator ==(const MyMoneySchedule& right) const; bool operator !=(const MyMoneySchedule& right) const; bool operator <(const MyMoneySchedule& right) const; MyMoneyAccount account(int cnt = 1) const; MyMoneyAccount transferAccount() const; QDate dateAfter(int transactions) const; bool isOverdue() const; bool isFinished() const; bool hasRecordedPayment(const QDate&) const; void recordPayment(const QDate&); QList recordedPayments() const; void writeXML(QDomDocument& document, QDomElement& parent) const final override; /** * This method checks if a reference to the given object exists. It returns, * a @p true if the object is referencing the one requested by the * parameter @p id. If it does not, this method returns @p false. * * @param id id of the object to be checked for references * @retval true This object references object with id @p id. * @retval false This object does not reference the object with id @p id. */ virtual bool hasReferenceTo(const QString& id) const final override; /** * This method replaces all occurrences of id @a oldId with * @a newId. All other ids are not changed. * * @return true if any change has been performed * @return false if nothing has been modified */ bool replaceId(const QString& newId, const QString& oldId); /** * Returns the human-readable format of Schedule's occurrence * * @return QString representing the human readable format */ QString occurrenceToString() const; /** * This method is used to convert the occurrence type from its * internal representation into a human readable format. * * @param type numerical representation of the MyMoneySchedule * occurrence type * * @return QString representing the human readable format */ static QString occurrenceToString(eMyMoney::Schedule::Occurrence type); /** * This method is used to convert a multiplier and base occurrence type * from its internal representation into a human readable format. * When multiplier * occurrence is equivalent to a simple occurrence * the method returns the same as occurrenceToString of the simple occurrence * * @param mult occurrence multiplier * @param type occurrence period * * @return QString representing the human readable format */ static QString occurrenceToString(int mult, eMyMoney::Schedule::Occurrence type); /** * This method is used to convert an occurrence period from * its internal representation into a human-readable format. * * @param type numerical representation of the MyMoneySchedule * occurrence type * * @return QString representing the human readable format */ static QString occurrencePeriodToString(eMyMoney::Schedule::Occurrence type); /** * This method is used to convert the payment type from its * internal representation into a human readable format. * * @param paymentType numerical representation of the MyMoneySchedule * payment type * * @return QString representing the human readable format */ static QString paymentMethodToString(eMyMoney::Schedule::PaymentType paymentType); /** * This method is used to convert the schedule weekend option from its * internal representation into a human readable format. * * @param weekendOption numerical representation of the MyMoneySchedule * weekend option * * @return QString representing the human readable format */ static QString weekendOptionToString(eMyMoney::Schedule::WeekendOption weekendOption); /** * This method is used to convert the schedule type from its * internal representation into a human readable format. * * @param type numerical representation of the MyMoneySchedule * schedule type * * @return QString representing the human readable format */ static QString scheduleTypeToString(eMyMoney::Schedule::Type type); int variation() const; void setVariation(int var); /** * * Convert an occurrence to the maximum number of events possible during a single * calendar year. * A fortnight is treated as 15 days. * * @param occurrence The occurrence * * @return int Number of days between events */ static int eventsPerYear(eMyMoney::Schedule::Occurrence occurrence); /** * * Convert an occurrence to the number of days between events * Treats a month as 30 days. * Treats a fortnight as 15 days. * * @param occurrence The occurrence * * @return int Number of days between events */ static int daysBetweenEvents(eMyMoney::Schedule::Occurrence occurrence); /** * Helper method to convert simple occurrence to compound occurrence + multiplier * * @param multiplier Returned by reference. Adjusted multiplier * @param occurrence Returned by reference. Occurrence type */ static void simpleToCompoundOccurrence(int& multiplier, eMyMoney::Schedule::Occurrence& occurrence); /** * Helper method to convert compound occurrence + multiplier to simple occurrence * * @param multiplier Returned by reference. Adjusted multiplier * @param occurrence Returned by reference. Occurrence type */ static void compoundToSimpleOccurrence(int& multiplier, eMyMoney::Schedule::Occurrence& occurrence); /** * This method is used to set the static point to relevant * IMyMoneyProcessingCalendar. */ static void setProcessingCalendar(IMyMoneyProcessingCalendar* pc); private: /** * This method returns a pointer to the processing calendar object. * * @return const pointer to the current attached processing calendar object. * If no object is attached, returns 0. */ IMyMoneyProcessingCalendar* processingCalendar() const; /** * This method forces the day of the passed @p date to * be the day of the start date of this schedule kept * in m_startDate. It is internally used when calculating * the payment dates over several periods. * * @param date reference to QDate object to be checked and adjusted */ void fixDate(QDate& date) const; /** * Simple method that sets the transaction for the schedule. * The transaction must have a valid postDate set, otherwise * it will not be accepted. This test is bypassed, if @a noDateCheck * is set to true * * @param transaction The new transaction. * @param noDateCheck if @a true, the date check is bypassed * @return none */ void setTransaction(const MyMoneyTransaction& transaction, bool noDateCheck); /** * This method adds a number of Half Months to the given Date. * This is used for EveryHalfMonth occurrences. * The addition uses the following rules to add a half month: * Day 1-13: add 15 days * Day 14: add 15 days (except February: the last day of the month) * Day 15: last day of the month * Day 16-29 (not last day in February): subtract 15 days and add 1 month * 30 and last day: 15th of next month * * This calculation pairs days 1 to 12 with 16 to 27. * Day 15 is paired with the last day of every month. * Repeated addition has issues in the following cases: * - Days 13 to 14 are paired with 28 to 29 until addition hits the last day of February * after which the (15,last) pair will be used. * - Addition from Day 30 leads immediately to the (15th,last) day pair. * * @param date The date * @param mult The number of half months to add. Default is 1. * * @return QDate date with mult half months added */ QDate addHalfMonths(QDate date, int mult = 1) const; /** * Checks if a given date should be considered a processing day * based on a calendar. See @a IMyMoneyProcessingCalendar and * setProcessingCalendar(). If no processingCalendar has been * setup using setProcessingCalendar it returns @c true on Mon..Fri * and @c false on Sat..Sun. */ bool isProcessingDate(const QDate& date) const; }; inline void swap(MyMoneySchedule& first, MyMoneySchedule& second) // krazy:exclude=inline { using std::swap; swap(first.d_ptr, second.d_ptr); } inline MyMoneySchedule::MyMoneySchedule(MyMoneySchedule && other) : MyMoneySchedule() // krazy:exclude=inline { swap(*this, other); } inline MyMoneySchedule & MyMoneySchedule::operator=(MyMoneySchedule other) // krazy:exclude=inline { swap(*this, other); return *this; } /** * Make it possible to hold @ref MyMoneySchedule objects inside @ref QVariant objects. */ Q_DECLARE_METATYPE(MyMoneySchedule) #endif diff --git a/kmymoney/mymoney/mymoneysecurity.cpp b/kmymoney/mymoney/mymoneysecurity.cpp index b6eb53ee3..f58333cf4 100644 --- a/kmymoney/mymoney/mymoneysecurity.cpp +++ b/kmymoney/mymoney/mymoneysecurity.cpp @@ -1,347 +1,352 @@ /* * Copyright 2005-2017 Thomas Baumgart * 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 "mymoneysecurity.h" #include "mymoneysecurity_p.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include // ---------------------------------------------------------------------------- // Project Includes #include "mymoneyexception.h" #include "mymoneystoragenames.h" using namespace eMyMoney; using namespace MyMoneyStorageNodes; MyMoneySecurity::MyMoneySecurity() : MyMoneyObject(*new MyMoneySecurityPrivate) { } +MyMoneySecurity::MyMoneySecurity(const QString &id) : + MyMoneyObject(*new MyMoneySecurityPrivate, id) +{ +} + MyMoneySecurity::MyMoneySecurity(const QString& id, const QString& name, const QString& symbol, const int smallestCashFraction, const int smallestAccountFraction, const int pricePrecision) : MyMoneyObject(*new MyMoneySecurityPrivate, id), MyMoneyKeyValueContainer() { Q_D(MyMoneySecurity); d->m_name = name; d->m_smallestCashFraction = smallestCashFraction; d->m_pricePrecision = pricePrecision; d->m_securityType = eMyMoney::Security::Type::Currency; if (symbol.isEmpty()) d->m_tradingSymbol = id; else d->m_tradingSymbol = symbol; if (smallestAccountFraction) d->m_smallestAccountFraction = smallestAccountFraction; else d->m_smallestAccountFraction = smallestCashFraction; } MyMoneySecurity::MyMoneySecurity(const QDomElement& node) : MyMoneyObject(*new MyMoneySecurityPrivate, node), MyMoneyKeyValueContainer(node.elementsByTagName(nodeNames[nnKeyValuePairs]).item(0).toElement()) { { const auto tag = node.tagName(); if ((nodeNames[nnSecurity] != tag) && (nodeNames[nnEquity] != tag) && (nodeNames[nnCurrency] != tag)) throw MYMONEYEXCEPTION_CSTRING("Node was not SECURITY or CURRENCY"); } Q_D(MyMoneySecurity); d->m_name = node.attribute(d->getAttrName(Security::Attribute::Name)); d->m_tradingSymbol = node.attribute(d->getAttrName(Security::Attribute::Symbol)); d->m_securityType = static_cast(node.attribute(d->getAttrName(Security::Attribute::Type)).toInt()); d->m_roundingMethod = static_cast(node.attribute(d->getAttrName(Security::Attribute::RoundingMethod)).toInt()); d->m_smallestAccountFraction = node.attribute(d->getAttrName(Security::Attribute::SAF)).toUInt(); d->m_pricePrecision = node.attribute(d->getAttrName(Security::Attribute::PP)).toUInt(); if (d->m_smallestAccountFraction == 0) d->m_smallestAccountFraction = 100; if (d->m_pricePrecision == 0 || d->m_pricePrecision > 10) d->m_pricePrecision = 4; if (isCurrency()) { d->m_smallestCashFraction = node.attribute(d->getAttrName(Security::Attribute::SCF)).toUInt(); if (d->m_smallestCashFraction == 0) d->m_smallestCashFraction = 100; } else { d->m_tradingCurrency = node.attribute(d->getAttrName(Security::Attribute::TradingCurrency)); d->m_tradingMarket = node.attribute(d->getAttrName(Security::Attribute::TradingMarket)); } } MyMoneySecurity::MyMoneySecurity(const MyMoneySecurity& other) : MyMoneyObject(*new MyMoneySecurityPrivate(*other.d_func()), other.id()), MyMoneyKeyValueContainer(other) { } MyMoneySecurity::MyMoneySecurity(const QString& id, const MyMoneySecurity& other) : MyMoneyObject(*new MyMoneySecurityPrivate(*other.d_func()), id), MyMoneyKeyValueContainer(other) { } MyMoneySecurity::~MyMoneySecurity() { } bool MyMoneySecurity::operator == (const MyMoneySecurity& right) const { Q_D(const MyMoneySecurity); auto d2 = static_cast(right.d_func()); return (d->m_id == d2->m_id) && (d->m_name == d2->m_name) && (d->m_tradingSymbol == d2->m_tradingSymbol) && (d->m_tradingMarket == d2->m_tradingMarket) && (d->m_roundingMethod == d2->m_roundingMethod) && (d->m_tradingSymbol == d2->m_tradingSymbol) && (d->m_tradingCurrency == d2->m_tradingCurrency) && (d->m_securityType == d2->m_securityType) && (d->m_smallestAccountFraction == d2->m_smallestAccountFraction) && (d->m_smallestCashFraction == d2->m_smallestCashFraction) && (d->m_pricePrecision == d2->m_pricePrecision) && this->MyMoneyKeyValueContainer::operator == (right); } bool MyMoneySecurity::operator < (const MyMoneySecurity& right) const { Q_D(const MyMoneySecurity); auto d2 = static_cast(right.d_func()); if (d->m_securityType == d2->m_securityType) return d->m_name < d2->m_name; return d->m_securityType < d2->m_securityType; } QString MyMoneySecurity::name() const { Q_D(const MyMoneySecurity); return d->m_name; } void MyMoneySecurity::setName(const QString& str) { Q_D(MyMoneySecurity); d->m_name = str; } QString MyMoneySecurity::tradingSymbol() const { Q_D(const MyMoneySecurity); return d->m_tradingSymbol; } void MyMoneySecurity::setTradingSymbol(const QString& str) { Q_D(MyMoneySecurity); d->m_tradingSymbol = str; } QString MyMoneySecurity::tradingMarket() const { Q_D(const MyMoneySecurity); return d->m_tradingMarket; } void MyMoneySecurity::setTradingMarket(const QString& str) { Q_D(MyMoneySecurity); d->m_tradingMarket = str; } QString MyMoneySecurity::tradingCurrency() const { Q_D(const MyMoneySecurity); return d->m_tradingCurrency; } void MyMoneySecurity::setTradingCurrency(const QString& str) { Q_D(MyMoneySecurity); d->m_tradingCurrency = str; } bool MyMoneySecurity::operator != (const MyMoneySecurity& r) const { return !(*this == r); } eMyMoney::Security::Type MyMoneySecurity::securityType() const { Q_D(const MyMoneySecurity); return d->m_securityType; } void MyMoneySecurity::setSecurityType(const eMyMoney::Security::Type s) { Q_D(MyMoneySecurity); d->m_securityType = s; } bool MyMoneySecurity::isCurrency() const { Q_D(const MyMoneySecurity); return d->m_securityType == eMyMoney::Security::Type::Currency; } AlkValue::RoundingMethod MyMoneySecurity::roundingMethod() const { Q_D(const MyMoneySecurity); return d->m_roundingMethod; } void MyMoneySecurity::setRoundingMethod(const AlkValue::RoundingMethod rnd) { Q_D(MyMoneySecurity); d->m_roundingMethod = rnd; } int MyMoneySecurity::smallestAccountFraction() const { Q_D(const MyMoneySecurity); return d->m_smallestAccountFraction; } void MyMoneySecurity::setSmallestAccountFraction(const int sf) { Q_D(MyMoneySecurity); d->m_smallestAccountFraction = sf; } int MyMoneySecurity::smallestCashFraction() const { Q_D(const MyMoneySecurity); return d->m_smallestCashFraction; } void MyMoneySecurity::setSmallestCashFraction(const int cf) { Q_D(MyMoneySecurity); d->m_smallestCashFraction = cf; } int MyMoneySecurity::pricePrecision() const { Q_D(const MyMoneySecurity); return d->m_pricePrecision; } void MyMoneySecurity::setPricePrecision(const int pp) { Q_D(MyMoneySecurity); d->m_pricePrecision = pp; } bool MyMoneySecurity::hasReferenceTo(const QString& id) const { Q_D(const MyMoneySecurity); return (id == d->m_tradingCurrency); } void MyMoneySecurity::writeXML(QDomDocument& document, QDomElement& parent) const { QDomElement el; if (isCurrency()) el = document.createElement(nodeNames[nnCurrency]); else el = document.createElement(nodeNames[nnSecurity]); Q_D(const MyMoneySecurity); d->writeBaseXML(document, el); el.setAttribute(d->getAttrName(Security::Attribute::Name), d->m_name); el.setAttribute(d->getAttrName(Security::Attribute::Symbol),d->m_tradingSymbol); el.setAttribute(d->getAttrName(Security::Attribute::Type), static_cast(d->m_securityType)); el.setAttribute(d->getAttrName(Security::Attribute::RoundingMethod), static_cast(d->m_roundingMethod)); el.setAttribute(d->getAttrName(Security::Attribute::SAF), d->m_smallestAccountFraction); el.setAttribute(d->getAttrName(Security::Attribute::PP), d->m_pricePrecision); if (isCurrency()) el.setAttribute(d->getAttrName(Security::Attribute::SCF), d->m_smallestCashFraction); else { el.setAttribute(d->getAttrName(Security::Attribute::TradingCurrency), d->m_tradingCurrency); el.setAttribute(d->getAttrName(Security::Attribute::TradingMarket), d->m_tradingMarket); } //Add in Key-Value Pairs for securities. MyMoneyKeyValueContainer::writeXML(document, el); parent.appendChild(el); } QString MyMoneySecurity::securityTypeToString(const eMyMoney::Security::Type securityType) { switch (securityType) { case eMyMoney::Security::Type::Stock: return i18nc("Security type", "Stock"); case eMyMoney::Security::Type::MutualFund: return i18nc("Security type", "Mutual Fund"); case eMyMoney::Security::Type::Bond: return i18nc("Security type", "Bond"); case eMyMoney::Security::Type::Currency: return i18nc("Security type", "Currency"); case eMyMoney::Security::Type::None: return i18nc("Security type", "None"); default: return i18nc("Security type", "Unknown"); } } QString MyMoneySecurity::roundingMethodToString(const AlkValue::RoundingMethod roundingMethod) { switch (roundingMethod) { case AlkValue::RoundNever: return I18N_NOOP("Never"); case AlkValue::RoundFloor: return I18N_NOOP("Floor"); case AlkValue::RoundCeil: return I18N_NOOP("Ceil"); case AlkValue::RoundTruncate: return I18N_NOOP("Truncate"); case AlkValue::RoundPromote: return I18N_NOOP("Promote"); case AlkValue::RoundHalfDown: return I18N_NOOP("HalfDown"); case AlkValue::RoundHalfUp: return I18N_NOOP("HalfUp"); case AlkValue::RoundRound: return I18N_NOOP("Round"); default: return I18N_NOOP("Unknown"); } } diff --git a/kmymoney/mymoney/mymoneysecurity.h b/kmymoney/mymoney/mymoneysecurity.h index 5514d668b..afff5fd9f 100644 --- a/kmymoney/mymoney/mymoneysecurity.h +++ b/kmymoney/mymoney/mymoneysecurity.h @@ -1,185 +1,187 @@ /* * Copyright 2005-2017 Thomas Baumgart * 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 MYMONEYSECURITY_H #define MYMONEYSECURITY_H #include "kmm_mymoney_export.h" // ---------------------------------------------------------------------------- // QT Includes #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes #include #include "mymoneyobject.h" #include "mymoneykeyvaluecontainer.h" class QString; namespace eMyMoney { namespace Security { enum class Type; } } /** * Class that holds all the required information about a security that the user * has entered information about. A security can be a stock, a mutual fund, a bond * or a currency. * * @author Kevin Tambascio * @author Thomas Baumgart * @author Łukasz Wojniłowicz */ class MyMoneySecurityPrivate; class KMM_MYMONEY_EXPORT MyMoneySecurity : public MyMoneyObject, public MyMoneyKeyValueContainer { Q_DECLARE_PRIVATE_D(MyMoneyObject::d_ptr, MyMoneySecurity) KMM_MYMONEY_UNIT_TESTABLE public: MyMoneySecurity(); + explicit MyMoneySecurity(const QString &id); + explicit MyMoneySecurity(const QString& id, const QString& name, const QString& symbol = QString(), const int smallestCashFraction = 100, const int smallestAccountFraction = 100, const int pricePrecision = 4); explicit MyMoneySecurity(const QDomElement& node); MyMoneySecurity(const QString& id, const MyMoneySecurity& other); MyMoneySecurity(const MyMoneySecurity & other); MyMoneySecurity(MyMoneySecurity && other); MyMoneySecurity & operator=(MyMoneySecurity other); friend void swap(MyMoneySecurity& first, MyMoneySecurity& second); ~MyMoneySecurity(); bool operator < (const MyMoneySecurity&) const; /** * This operator tests for equality of two MyMoneySecurity objects */ bool operator == (const MyMoneySecurity&) const; /** * This operator tests for inequality of this MyMoneySecurity object * and the one passed by @p r * * @param r the right side of the comparison */ bool operator != (const MyMoneySecurity& r) const; QString name() const; void setName(const QString& str); QString tradingSymbol() const; void setTradingSymbol(const QString& str); eMyMoney::Security::Type securityType() const; void setSecurityType(const eMyMoney::Security::Type s); bool isCurrency() const; AlkValue::RoundingMethod roundingMethod() const; void setRoundingMethod(const AlkValue::RoundingMethod rnd); QString tradingMarket() const; void setTradingMarket(const QString& str); QString tradingCurrency() const; void setTradingCurrency(const QString& str); int smallestAccountFraction() const; void setSmallestAccountFraction(const int sf); int smallestCashFraction() const; void setSmallestCashFraction(const int cf); int pricePrecision() const; void setPricePrecision(const int pp); void writeXML(QDomDocument& document, QDomElement& parent) const override; /** * This method checks if a reference to the given object exists. It returns, * a @p true if the object is referencing the one requested by the * parameter @p id. If it does not, this method returns @p false. * * @param id id of the object to be checked for references * @retval true This object references object with id @p id. * @retval false This object does not reference the object with id @p id. */ bool hasReferenceTo(const QString& id) const override; /** * This method is used to convert the internal representation of * an security type into a human readable format * * @param securityType enumerated representation of the security type. * For possible values, see MyMoneySecurity::eSECURITYTYPE * * @return QString representing the human readable form */ static QString securityTypeToString(const eMyMoney::Security::Type securityType); /** * This method is used to convert the internal representation of * an rounding method into a human readable format * * @param roundingMethod enumerated representation of the rouding method. * For possible values, see AlkValue::RoundingMethod * * @return QString representing the human readable form */ static QString roundingMethodToString(const AlkValue::RoundingMethod roundingMethod); }; inline void swap(MyMoneySecurity& first, MyMoneySecurity& second) // krazy:exclude=inline { using std::swap; swap(first.MyMoneyObject::d_ptr, second.MyMoneyObject::d_ptr); swap(first.MyMoneyKeyValueContainer::d_ptr, second.MyMoneyKeyValueContainer::d_ptr); } inline MyMoneySecurity::MyMoneySecurity(MyMoneySecurity && other) : MyMoneySecurity() // krazy:exclude=inline { swap(*this, other); } inline MyMoneySecurity & MyMoneySecurity::operator=(MyMoneySecurity other) // krazy:exclude=inline { swap(*this, other); return *this; } /** * Make it possible to hold @ref MyMoneySecurity objects inside @ref QVariant objects. */ Q_DECLARE_METATYPE(MyMoneySecurity) #endif diff --git a/kmymoney/mymoney/mymoneytag.cpp b/kmymoney/mymoney/mymoneytag.cpp index 43b8464ee..25042bbf7 100644 --- a/kmymoney/mymoney/mymoneytag.cpp +++ b/kmymoney/mymoney/mymoneytag.cpp @@ -1,176 +1,173 @@ /* * Copyright 2012 Alessandro Russo * 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 "mymoneytag.h" #include "mymoneytag_p.h" // ---------------------------------------------------------------------------- // QT Includes #include #include // ---------------------------------------------------------------------------- // Project Includes #include "mymoneyexception.h" #include "mymoneystoragenames.h" using namespace MyMoneyStorageNodes; MyMoneyTag MyMoneyTag::null; MyMoneyTag::MyMoneyTag() : MyMoneyObject(*new MyMoneyTagPrivate) { } -MyMoneyTag::MyMoneyTag(const QString& name) : - MyMoneyObject(*new MyMoneyTagPrivate) +MyMoneyTag::MyMoneyTag(const QString &id) : + MyMoneyObject(*new MyMoneyTagPrivate, id) { - Q_D(MyMoneyTag); - d->m_name = name; - d->m_tag_color = QColor(); } MyMoneyTag::MyMoneyTag(const QString& name, const QColor& tabColor) : MyMoneyObject(*new MyMoneyTagPrivate) { Q_D(MyMoneyTag); d->m_name = name; d->m_tag_color = tabColor; } MyMoneyTag::MyMoneyTag(const QDomElement& node) : MyMoneyObject(*new MyMoneyTagPrivate, node) { if (nodeNames[nnTag] != node.tagName()) { throw MYMONEYEXCEPTION_CSTRING("Node was not TAG"); } Q_D(MyMoneyTag); d->m_name = node.attribute(d->getAttrName(Tag::Attribute::Name)); if (node.hasAttribute(d->getAttrName(Tag::Attribute::TagColor))) { d->m_tag_color.setNamedColor(node.attribute(d->getAttrName(Tag::Attribute::TagColor))); } if (node.hasAttribute(d->getAttrName(Tag::Attribute::Notes))) { d->m_notes = node.attribute(d->getAttrName(Tag::Attribute::Notes)); } d->m_closed = node.attribute(d->getAttrName(Tag::Attribute::Closed), "0").toUInt(); } MyMoneyTag::MyMoneyTag(const MyMoneyTag& other) : MyMoneyObject(*new MyMoneyTagPrivate(*other.d_func()), other.id()) { } MyMoneyTag::MyMoneyTag(const QString& id, const MyMoneyTag& other) : MyMoneyObject(*new MyMoneyTagPrivate(*other.d_func()), id) { Q_D(MyMoneyTag); d->m_tag_color = QColor("black"); } MyMoneyTag::~MyMoneyTag() { } QString MyMoneyTag::name() const { Q_D(const MyMoneyTag); return d->m_name; } void MyMoneyTag::setName(const QString& val) { Q_D(MyMoneyTag); d->m_name = val; } bool MyMoneyTag::isClosed() const { Q_D(const MyMoneyTag); return d->m_closed; } void MyMoneyTag::setClosed(bool val) { Q_D(MyMoneyTag); d->m_closed = val; } QColor MyMoneyTag::tagColor() const { Q_D(const MyMoneyTag); return d->m_tag_color; } void MyMoneyTag::setTagColor(const QColor& val) { Q_D(MyMoneyTag); d->m_tag_color = val; } QString MyMoneyTag::notes() const { Q_D(const MyMoneyTag); return d->m_notes; } void MyMoneyTag::setNotes(const QString& val) { Q_D(MyMoneyTag); d->m_notes = val; } bool MyMoneyTag::operator == (const MyMoneyTag& right) const { Q_D(const MyMoneyTag); auto d2 = static_cast(right.d_func()); return (MyMoneyObject::operator==(right) && ((d->m_name.length() == 0 && d2->m_name.length() == 0) || (d->m_name == d2->m_name)) && ((d->m_tag_color.isValid() == false && d2->m_tag_color.isValid() == false) || (d->m_tag_color.name() == d2->m_tag_color.name())) && (d->m_closed == d2->m_closed)); } bool MyMoneyTag::operator < (const MyMoneyTag& right) const { Q_D(const MyMoneyTag); auto d2 = static_cast(right.d_func()); return d->m_name < d2->m_name; } void MyMoneyTag::writeXML(QDomDocument& document, QDomElement& parent) const { auto el = document.createElement(nodeNames[nnTag]); Q_D(const MyMoneyTag); d->writeBaseXML(document, el); el.setAttribute(d->getAttrName(Tag::Attribute::Name), d->m_name); el.setAttribute(d->getAttrName(Tag::Attribute::Closed), d->m_closed); if (d->m_tag_color.isValid()) el.setAttribute(d->getAttrName(Tag::Attribute::TagColor), d->m_tag_color.name()); if (!d->m_notes.isEmpty()) el.setAttribute(d->getAttrName(Tag::Attribute::Notes), d->m_notes); parent.appendChild(el); } bool MyMoneyTag::hasReferenceTo(const QString& /*id*/) const { return false; } diff --git a/kmymoney/mymoney/mymoneytag.h b/kmymoney/mymoney/mymoneytag.h index d1940f291..02c38a7f0 100644 --- a/kmymoney/mymoney/mymoneytag.h +++ b/kmymoney/mymoney/mymoneytag.h @@ -1,133 +1,132 @@ /* * Copyright 2012 Alessandro Russo * 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 MYMONEYTAG_H #define MYMONEYTAG_H // ---------------------------------------------------------------------------- // QT Includes #include // ---------------------------------------------------------------------------- // Project Includes #include "kmm_mymoney_export.h" #include "mymoneyobject.h" class QString; class QColor; class QDomDocument; /** * This class represents a tag within the MyMoney engine. */ class MyMoneyTagPrivate; class KMM_MYMONEY_EXPORT MyMoneyTag : public MyMoneyObject { Q_DECLARE_PRIVATE(MyMoneyTag) KMM_MYMONEY_UNIT_TESTABLE public: MyMoneyTag(); - - explicit MyMoneyTag(const QString& name); + explicit MyMoneyTag(const QString &id); explicit MyMoneyTag(const QString& name, const QColor& tagColor ); /** * This is the constructor for a tag that is described by a * QDomElement (e.g. from a file). * * @param el const reference to the QDomElement from which to * create the object */ explicit MyMoneyTag(const QDomElement& node); MyMoneyTag(const QString& id, const MyMoneyTag& tag); MyMoneyTag(const MyMoneyTag & other); MyMoneyTag(MyMoneyTag && other); MyMoneyTag & operator=(MyMoneyTag other); friend void swap(MyMoneyTag& first, MyMoneyTag& second); ~MyMoneyTag(); QString name() const; void setName(const QString& val); bool isClosed() const; void setClosed(bool val); QColor tagColor() const; void setTagColor(const QColor& val); QString notes() const; void setNotes(const QString& val); // Equality operator bool operator == (const MyMoneyTag &) const; bool operator <(const MyMoneyTag& right) const; void writeXML(QDomDocument& document, QDomElement& parent) const override; /** * This method checks if a reference to the given object exists. It returns, * a @p true if the object is referencing the one requested by the * parameter @p id. If it does not, this method returns @p false. * * @param id id of the object to be checked for references * @retval true This object references object with id @p id. * @retval false This object does not reference the object with id @p id. */ bool hasReferenceTo(const QString& id) const override; static MyMoneyTag null; }; inline void swap(MyMoneyTag& first, MyMoneyTag& second) // krazy:exclude=inline { using std::swap; swap(first.d_ptr, second.d_ptr); } inline MyMoneyTag::MyMoneyTag(MyMoneyTag && other) : MyMoneyTag() // krazy:exclude=inline { swap(*this, other); } inline MyMoneyTag & MyMoneyTag::operator=(MyMoneyTag other) // krazy:exclude=inline { swap(*this, other); return *this; } //inline bool operator==(const MyMoneyTag& lhs, const QString& rhs) //{ // return lhs.id() == rhs; //} /** * Make it possible to hold @ref MyMoneyTag objects inside @ref QVariant objects. */ Q_DECLARE_METATYPE(MyMoneyTag) #endif diff --git a/kmymoney/mymoney/mymoneytransaction.cpp b/kmymoney/mymoney/mymoneytransaction.cpp index 93beef9b4..039a343db 100644 --- a/kmymoney/mymoney/mymoneytransaction.cpp +++ b/kmymoney/mymoney/mymoneytransaction.cpp @@ -1,519 +1,528 @@ /* * Copyright 2000-2002 Michael Edwardes * Copyright 2001-2017 Thomas Baumgart * Copyright 2001 Felix Rodriguez * Copyright 2003 Kevin Tambascio * Copyright 2004-2005 Ace Jones * Copyright 2006 Darren Gould * * 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 "mymoneytransaction.h" #include "mymoneytransaction_p.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "mymoneystoragenames.h" #include "mymoneyutils.h" #include "mymoneymoney.h" #include "mymoneyexception.h" #include "mymoneyenums.h" using namespace MyMoneyStorageNodes; MyMoneyTransaction::MyMoneyTransaction() : MyMoneyObject(*new MyMoneyTransactionPrivate) { Q_D(MyMoneyTransaction); d->m_nextSplitID = 1; d->m_entryDate = QDate(); d->m_postDate = QDate(); } +MyMoneyTransaction::MyMoneyTransaction(const QString &id) : + MyMoneyObject(*new MyMoneyTransactionPrivate, id) +{ + Q_D(MyMoneyTransaction); + d->m_nextSplitID = 1; + d->m_entryDate = QDate(); + d->m_postDate = QDate(); +} + MyMoneyTransaction::MyMoneyTransaction(const QDomElement& node, const bool forceId) : MyMoneyObject(*new MyMoneyTransactionPrivate, node, forceId) { Q_D(MyMoneyTransaction); if (nodeNames[nnTransaction] != node.tagName()) throw MYMONEYEXCEPTION_CSTRING("Node was not TRANSACTION"); d->m_nextSplitID = 1; d->m_postDate = MyMoneyUtils::stringToDate(node.attribute(d->getAttrName(Transaction::Attribute::PostDate))); d->m_entryDate = MyMoneyUtils::stringToDate(node.attribute(d->getAttrName(Transaction::Attribute::EntryDate))); d->m_bankID = MyMoneyUtils::QStringEmpty(node.attribute(d->getAttrName(Transaction::Attribute::BankID))); d->m_memo = MyMoneyUtils::QStringEmpty(node.attribute(d->getAttrName(Transaction::Attribute::Memo))); d->m_commodity = MyMoneyUtils::QStringEmpty(node.attribute(d->getAttrName(Transaction::Attribute::Commodity))); QDomNode child = node.firstChild(); while (!child.isNull() && child.isElement()) { QDomElement c = child.toElement(); if (c.tagName() == d->getElName(Transaction::Element::Splits)) { // Process any split information found inside the transaction entry. QDomNodeList nodeList = c.elementsByTagName(d->getElName(Transaction::Element::Split)); for (int i = 0; i < nodeList.count(); ++i) { MyMoneySplit s(nodeList.item(i).toElement()); if (!d->m_bankID.isEmpty()) s.setBankID(d->m_bankID); if (!s.accountId().isEmpty()) addSplit(s); else qDebug("Dropped split because it did not have an account id"); } } else if (c.tagName() == nodeNames[nnKeyValuePairs]) { MyMoneyKeyValueContainer kvp(c); setPairs(kvp.pairs()); } child = child.nextSibling(); } d->m_bankID.clear(); } MyMoneyTransaction::MyMoneyTransaction(const MyMoneyTransaction& other) : MyMoneyObject(*new MyMoneyTransactionPrivate(*other.d_func()), other.id()), MyMoneyKeyValueContainer(other) { } MyMoneyTransaction::MyMoneyTransaction(const QString& id, const MyMoneyTransaction& other) : MyMoneyObject(*new MyMoneyTransactionPrivate(*other.d_func()), id), MyMoneyKeyValueContainer(other) { Q_D(MyMoneyTransaction); if (d->m_entryDate == QDate()) d->m_entryDate = QDate::currentDate(); foreach (auto split, d->m_splits) split.setTransactionId(id); } MyMoneyTransaction::~MyMoneyTransaction() { } QDate MyMoneyTransaction::entryDate() const { Q_D(const MyMoneyTransaction); return d->m_entryDate; } void MyMoneyTransaction::setEntryDate(const QDate& date) { Q_D(MyMoneyTransaction); d->m_entryDate = date; } QDate MyMoneyTransaction::postDate() const { Q_D(const MyMoneyTransaction); return d->m_postDate; } void MyMoneyTransaction::setPostDate(const QDate& date) { Q_D(MyMoneyTransaction); d->m_postDate = date; } QString MyMoneyTransaction::memo() const { Q_D(const MyMoneyTransaction); return d->m_memo; } void MyMoneyTransaction::setMemo(const QString& memo) { Q_D(MyMoneyTransaction); d->m_memo = memo; } QList MyMoneyTransaction::splits() const { Q_D(const MyMoneyTransaction); return d->m_splits; } QList& MyMoneyTransaction::splits() { Q_D(MyMoneyTransaction); return d->m_splits; } MyMoneySplit MyMoneyTransaction::firstSplit() const { Q_D(const MyMoneyTransaction); return d->m_splits.first(); } uint MyMoneyTransaction::splitCount() const { Q_D(const MyMoneyTransaction); return d->m_splits.count(); } QString MyMoneyTransaction::commodity() const { Q_D(const MyMoneyTransaction); return d->m_commodity; } void MyMoneyTransaction::setCommodity(const QString& commodityId) { Q_D(MyMoneyTransaction); d->m_commodity = commodityId; } QString MyMoneyTransaction::bankID() const { Q_D(const MyMoneyTransaction); return d->m_bankID; } void MyMoneyTransaction::setBankID(const QString& bankID) { Q_D(MyMoneyTransaction); d->m_bankID = bankID; } bool MyMoneyTransaction::operator == (const MyMoneyTransaction& right) const { Q_D(const MyMoneyTransaction); auto d2 = static_cast(right.d_func()); return (MyMoneyObject::operator==(right) && MyMoneyKeyValueContainer::operator==(right) && (d->m_commodity == d2->m_commodity) && ((d->m_memo.length() == 0 && d2->m_memo.length() == 0) || (d->m_memo == d2->m_memo)) && (d->m_splits == d2->m_splits) && (d->m_entryDate == d2->m_entryDate) && (d->m_postDate == d2->m_postDate)); } bool MyMoneyTransaction::operator != (const MyMoneyTransaction& r) const { return !(*this == r); } bool MyMoneyTransaction::operator< (const MyMoneyTransaction& r) const { return postDate() < r.postDate(); } bool MyMoneyTransaction::operator<= (const MyMoneyTransaction& r) const { return postDate() <= r.postDate(); } bool MyMoneyTransaction::operator> (const MyMoneyTransaction& r) const { return postDate() > r.postDate(); } bool MyMoneyTransaction::accountReferenced(const QString& id) const { Q_D(const MyMoneyTransaction); foreach (const auto split, d->m_splits) { if (split.accountId() == id) return true; } return false; } void MyMoneyTransaction::addSplit(MyMoneySplit &split) { if (!split.id().isEmpty()) throw MYMONEYEXCEPTION(QString::fromLatin1("Cannot add split with assigned id '%1'").arg(split.id())); if (split.accountId().isEmpty()) throw MYMONEYEXCEPTION(QString::fromLatin1("Cannot add split that does not contain an account reference")); Q_D(MyMoneyTransaction); MyMoneySplit newSplit(d->nextSplitID(), split); split = newSplit; split.setTransactionId(id()); d->m_splits.append(split); } void MyMoneyTransaction::modifySplit(const MyMoneySplit& split) { // This is the other version which allows having more splits referencing // the same account. if (split.accountId().isEmpty()) throw MYMONEYEXCEPTION_CSTRING("Cannot modify split that does not contain an account reference"); Q_D(MyMoneyTransaction); for (auto& it_split : d->m_splits) { if (split.id() == it_split.id()) { it_split = split; return; } } throw MYMONEYEXCEPTION(QString::fromLatin1("Invalid split id '%1'").arg(split.id())); } void MyMoneyTransaction::removeSplit(const MyMoneySplit& split) { Q_D(MyMoneyTransaction); for (auto end = d->m_splits.size(), i = 0; i < end; ++i) { if (split.id() == d->m_splits.at(i).id()) { d->m_splits.removeAt(i); return; } } throw MYMONEYEXCEPTION(QString::fromLatin1("Invalid split id '%1'").arg(split.id())); } void MyMoneyTransaction::removeSplits() { Q_D(MyMoneyTransaction); d->m_splits.clear(); d->m_nextSplitID = 1; } MyMoneySplit MyMoneyTransaction::splitByPayee(const QString& payeeId) const { Q_D(const MyMoneyTransaction); foreach (const auto split, d->m_splits) { if (split.payeeId() == payeeId) return split; } throw MYMONEYEXCEPTION(QString::fromLatin1("Split not found for payee '%1'").arg(QString(payeeId))); } MyMoneySplit MyMoneyTransaction::splitByAccount(const QString& accountId, const bool match) const { Q_D(const MyMoneyTransaction); foreach (const auto split, d->m_splits) { if ((match == true && split.accountId() == accountId) || (match == false && split.accountId() != accountId)) return split; } throw MYMONEYEXCEPTION(QString::fromLatin1("Split not found for account %1%2").arg(match ? "" : "!").arg(QString(accountId))); } MyMoneySplit MyMoneyTransaction::splitByAccount(const QStringList& accountIds, const bool match) const { Q_D(const MyMoneyTransaction); foreach (const auto split, d->m_splits) { if ((match == true && accountIds.contains(split.accountId())) || (match == false && !accountIds.contains(split.accountId()))) return split; } throw MYMONEYEXCEPTION(QString::fromLatin1("Split not found for account %1%1...%2").arg(match ? "" : "!").arg(accountIds.front(), accountIds.back())); } MyMoneySplit MyMoneyTransaction::splitById(const QString& splitId) const { Q_D(const MyMoneyTransaction); foreach (const auto split, d->m_splits) { if (split.id() == splitId) return split; } throw MYMONEYEXCEPTION(QString::fromLatin1("Split not found for id '%1'").arg(QString(splitId))); } QString MyMoneyTransaction::firstSplitID() { QString id; id = 'S' + id.setNum(1).rightJustified(MyMoneyTransactionPrivate::SPLIT_ID_SIZE, '0'); return id; } MyMoneyMoney MyMoneyTransaction::splitSum() const { MyMoneyMoney result; Q_D(const MyMoneyTransaction); foreach (const auto split, d->m_splits) result += split.value(); return result; } bool MyMoneyTransaction::isLoanPayment() const { try { Q_D(const MyMoneyTransaction); foreach (const auto split, d->m_splits) { if (split.isAmortizationSplit()) return true; } } catch (const MyMoneyException &) { } return false; } MyMoneySplit MyMoneyTransaction::amortizationSplit() const { static MyMoneySplit nullSplit; Q_D(const MyMoneyTransaction); foreach (const auto split, d->m_splits) { if (split.isAmortizationSplit() && split.isAutoCalc()) return split; } return nullSplit; } MyMoneySplit MyMoneyTransaction::interestSplit() const { static MyMoneySplit nullSplit; Q_D(const MyMoneyTransaction); foreach (const auto split, d->m_splits) { if (split.isInterestSplit() && split.isAutoCalc()) return split; } return nullSplit; } unsigned long MyMoneyTransaction::hash(const QString& txt, unsigned long h) { unsigned long g; for (int i = 0; i < txt.length(); ++i) { unsigned short uc = txt[i].unicode(); for (unsigned j = 0; j < 2; ++j) { unsigned char c = uc & 0xff; // if either the cell or the row of the Unicode char is 0, stop processing if (!c) break; h = (h << 4) + c; if ((g = (h & 0xf0000000))) { h = h ^(g >> 24); h = h ^ g; } uc >>= 8; } } return h; } bool MyMoneyTransaction::isStockSplit() const { Q_D(const MyMoneyTransaction); return (d->m_splits.count() == 1 && d->m_splits.first().action() == MyMoneySplit::actionName(eMyMoney::Split::Action::SplitShares)); } bool MyMoneyTransaction::isImported() const { return value("Imported").toLower() == QString("true"); } void MyMoneyTransaction::setImported(bool state) { if (state) setValue("Imported", "true"); else deletePair("Imported"); } void MyMoneyTransaction::writeXML(QDomDocument& document, QDomElement& parent) const { Q_D(const MyMoneyTransaction); auto el = document.createElement(nodeNames[nnTransaction]); d->writeBaseXML(document, el); el.setAttribute(d->getAttrName(Transaction::Attribute::PostDate), MyMoneyUtils::dateToString(d->m_postDate)); el.setAttribute(d->getAttrName(Transaction::Attribute::Memo), d->m_memo); el.setAttribute(d->getAttrName(Transaction::Attribute::EntryDate), MyMoneyUtils::dateToString(d->m_entryDate)); el.setAttribute(d->getAttrName(Transaction::Attribute::Commodity), d->m_commodity); auto splits = document.createElement(d->getElName(Transaction::Element::Splits)); foreach (const auto split, d->m_splits) split.writeXML(document, splits); el.appendChild(splits); MyMoneyKeyValueContainer::writeXML(document, el); parent.appendChild(el); } bool MyMoneyTransaction::hasReferenceTo(const QString& id) const { Q_D(const MyMoneyTransaction); if (id == d->m_commodity) return true; foreach (const auto split, d->m_splits) { if (split.hasReferenceTo(id)) return true; } return false; } bool MyMoneyTransaction::hasAutoCalcSplit() const { Q_D(const MyMoneyTransaction); foreach (const auto split, d->m_splits) if (split.isAutoCalc()) return true; return false; } QString MyMoneyTransaction::accountSignature(bool includeSplitCount) const { Q_D(const MyMoneyTransaction); QMap accountList; foreach (const auto split, d->m_splits) accountList[split.accountId()] += 1; QMap::const_iterator it_a; QString rc; for (it_a = accountList.constBegin(); it_a != accountList.constEnd(); ++it_a) { if (it_a != accountList.constBegin()) rc += '-'; rc += it_a.key(); if (includeSplitCount) rc += QString("*%1").arg(*it_a); } return rc; } QString MyMoneyTransaction::uniqueSortKey() const { Q_D(const MyMoneyTransaction); QString year, month, day, key; const auto postdate = postDate(); year = year.setNum(postdate.year()).rightJustified(MyMoneyTransactionPrivate::YEAR_SIZE, '0'); month = month.setNum(postdate.month()).rightJustified(MyMoneyTransactionPrivate::MONTH_SIZE, '0'); day = day.setNum(postdate.day()).rightJustified(MyMoneyTransactionPrivate::DAY_SIZE, '0'); key = QString::fromLatin1("%1-%2-%3-%4").arg(year, month, day, d->m_id); return key; } bool MyMoneyTransaction::replaceId(const QString& newId, const QString& oldId) { auto changed = false; Q_D(MyMoneyTransaction); for (MyMoneySplit& split : d->m_splits) changed |= split.replaceId(newId, oldId); return changed; } diff --git a/kmymoney/mymoney/mymoneytransaction.h b/kmymoney/mymoney/mymoneytransaction.h index 8d508a1a5..6dbf809a9 100644 --- a/kmymoney/mymoney/mymoneytransaction.h +++ b/kmymoney/mymoney/mymoneytransaction.h @@ -1,335 +1,337 @@ /* * Copyright 2000-2002 Michael Edwardes * Copyright 2001-2017 Thomas Baumgart * Copyright 2001 Felix Rodriguez * Copyright 2003 Kevin Tambascio * Copyright 2004-2005 Ace Jones * Copyright 2006 Darren Gould * * 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 MYMONEYTRANSACTION_H #define MYMONEYTRANSACTION_H // ---------------------------------------------------------------------------- // QT Includes #include // ---------------------------------------------------------------------------- // Project Includes #include "mymoneykeyvaluecontainer.h" #include "mymoneyobject.h" #include "kmm_mymoney_export.h" #include "mymoneyunittestable.h" class QString; class QDate; class MyMoneyMoney; class MyMoneySplit; class QStringList; template class QList; /** * This class represents a transaction within the MyMoneyEngine. A transaction * contains none, one or more splits of type MyMoneySplit. They are stored in * a QList within this object. A transaction containing only * a single split with an amount not equal to 0 is an unbalanced transaction. It * is tolerated by the engine, but in general not a good idea as it is financially * wrong. */ class MyMoneyTransactionPrivate; class KMM_MYMONEY_EXPORT MyMoneyTransaction : public MyMoneyObject, public MyMoneyKeyValueContainer { Q_DECLARE_PRIVATE_D(MyMoneyObject::d_ptr, MyMoneyTransaction) KMM_MYMONEY_UNIT_TESTABLE public: MyMoneyTransaction(); + explicit MyMoneyTransaction(const QString &id); + /** * @param node reference to QDomNode * @param forceId see MyMoneyObject(const QDomElement&, const bool) */ explicit MyMoneyTransaction(const QDomElement& node, const bool forceId = true); MyMoneyTransaction(const QString& id, const MyMoneyTransaction& other); MyMoneyTransaction(const MyMoneyTransaction & other); MyMoneyTransaction(MyMoneyTransaction && other); MyMoneyTransaction & operator=(MyMoneyTransaction other); friend void swap(MyMoneyTransaction& first, MyMoneyTransaction& second); ~MyMoneyTransaction(); friend QDataStream &operator<<(QDataStream &, MyMoneyTransaction &); friend QDataStream &operator>>(QDataStream &, MyMoneyTransaction &); QDate entryDate() const; void setEntryDate(const QDate& date); QDate postDate() const; void setPostDate(const QDate& date); QString memo() const; void setMemo(const QString& memo); QList splits() const; QList& splits(); MyMoneySplit firstSplit() const; uint splitCount() const; QString commodity() const; void setCommodity(const QString& commodityId); QString bankID() const; void setBankID(const QString& bankID); bool operator == (const MyMoneyTransaction& right) const; bool operator != (const MyMoneyTransaction& r) const; bool operator < (const MyMoneyTransaction& r) const; bool operator <= (const MyMoneyTransaction& r) const; bool operator > (const MyMoneyTransaction& r) const; /** * This method is used to extract a split for a given accountId * from a transaction. A parameter controls, whether the accountId * should match or not. In case of 'not match', the first not-matching * split is returned. * * @param accountId the account to look for * @param match if true, the account Id must match * if false, the account Id must not match * * @return reference to split within the transaction is returned */ MyMoneySplit splitByAccount(const QString& accountId, const bool match = true) const; /** * This method is essentially the same as the previous method, except that * takes a list of accounts instead of just one. * * @param accountIds the list of accounts to look for * @param match if true, the account Id must match * if false, the account Id must not match * * @return reference to split within the transaction is returned */ MyMoneySplit splitByAccount(const QStringList& accountIds, const bool match = true) const; /** * This method is used to extract a split from a transaction. * * @param splitId the split to look for * * @return reference to split within the transaction is returned */ MyMoneySplit splitById(const QString& splitId) const; /** * This method is used to extract a split for a given payeeId * from a transaction. * * @param payeeId the payee to look for * * @return reference to split within the transaction is returned */ MyMoneySplit splitByPayee(const QString& payeeId) const; /** * This method is used to check if the given account is used * in any of the splits of this transaction * * @param id account id that should be checked for usage */ bool accountReferenced(const QString& id) const; /** * This method is used to add a split to the transaction. The split * will be assigned an id. The id member must be empty and the * accountId member must be filled. * * @param split reference to the split that should be added * */ void addSplit(MyMoneySplit &split); /** * This method is used to modify a split in a transaction */ void modifySplit(const MyMoneySplit& split); /** * This method is used to remove a split from a transaction */ void removeSplit(const MyMoneySplit& split); /** * This method is used to remove all splits from a transaction */ void removeSplits(); /** * This method is used to return the sum of all splits of this transaction * * @return MyMoneyMoney value of sum of all splits */ MyMoneyMoney splitSum() const; /** * This method returns information if the transaction * contains information of a loan payment or not. * Loan payment transactions have at least one * split that is identified with a MyMoneySplit::action() of type * MyMoneySplit::actionName(eMyMoney::Split::Action::Amortization). * * @retval false transaction is no loan payment transaction * @retval true transaction is a loan payment transaction * * @note Upon internal failures, the return value @p false will be used. */ bool isLoanPayment() const; /** * This method returns a const reference to the amortization split. * In case none is found, a reference to an empty split will be returned. */ MyMoneySplit amortizationSplit() const; /** * This method returns a const reference to the interest split. * In case none is found, a reference to an empty split will be returned. */ MyMoneySplit interestSplit() const; /** * returns @a true if this is a stock split transaction */ bool isStockSplit() const; /** * returns @a true if this is an imported transaction */ bool isImported() const; /** * Sets the imported state of this transaction to be the value of @a state . * @p state defaults to @p true. */ void setImported(bool state = true); /** * This static method returns the id which will be assigned to the * first split added to a transaction. This ID can be used to figure * out the split that references the account through which a transaction * was entered. * * @return QString with ID of the first split of transactions */ static QString firstSplitID(); void writeXML(QDomDocument& document, QDomElement& parent) const override; /** * This method checks if a reference to the given object exists. It returns, * a @p true if the object is referencing the one requested by the * parameter @p id. If it does not, this method returns @p false. * * @param id id of the object to be checked for references * @retval true This object references object with id @p id. * @retval false This object does not reference the object with id @p id. */ bool hasReferenceTo(const QString& id) const override; /** * Checks whether any split contains an autocalc split. * * @retval true at least one split has an autocalc value * @retval false all splits have fixed values */ bool hasAutoCalcSplit() const; /** * Returns a signature consisting of the account ids and the * number of times they occur in the transaction if @a includeSplitCount * is @a true. The signature is independent from the order of splits. * * Example: Having splits referencing the account B, A and B, the returned * value will be "A-B" if @p includeSplitCount is @p false or A*1-B*2 if it * is @p true. * * The same result will be returned if the list of splits is A, B, B. * * @param includeSplitCount if @p true, the string @p *n with @p n being * the number of splits referencing this account. The default for * this parameter is @p false. */ QString accountSignature(bool includeSplitCount = false) const; QString uniqueSortKey() const; /** * This module implements an algorithm used by P.J. Weinberger * for fast hashing. Source: COMPILERS by Alfred V. Aho, * pages 435-437. * * It converts the string passed in @p txt into a non-unique * unsigned long integer value. * * @param txt the text to be hashed * @param h initial hash value (default 0) * @return non-unique hash value of the text @p txt */ static unsigned long hash(const QString& txt, unsigned long h = 0); /** * This method replaces all occurrences of id @a oldId with * @a newId. All other ids are not changed. * * @return true if any change has been performed * @return false if nothing has been modified */ bool replaceId(const QString& newId, const QString& oldId); }; inline void swap(MyMoneyTransaction& first, MyMoneyTransaction& second) // krazy:exclude=inline { using std::swap; swap(first.MyMoneyObject::d_ptr, second.MyMoneyObject::d_ptr); swap(first.MyMoneyKeyValueContainer::d_ptr, second.MyMoneyKeyValueContainer::d_ptr); } inline MyMoneyTransaction::MyMoneyTransaction(MyMoneyTransaction && other) : MyMoneyTransaction() // krazy:exclude=inline { swap(*this, other); } inline MyMoneyTransaction & MyMoneyTransaction::operator=(MyMoneyTransaction other) // krazy:exclude=inline { swap(*this, other); return *this; } /** * Make it possible to hold @ref MyMoneyTransaction objects inside @ref QVariant objects. */ Q_DECLARE_METATYPE(MyMoneyTransaction) #endif diff --git a/kmymoney/mymoney/onlinejob.cpp b/kmymoney/mymoney/onlinejob.cpp index d6375f719..7a04fd3f4 100644 --- a/kmymoney/mymoney/onlinejob.cpp +++ b/kmymoney/mymoney/onlinejob.cpp @@ -1,322 +1,339 @@ /* * Copyright 2013-2015 Christian Dávid * * 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 "onlinejob.h" #include "onlinejob_p.h" #include #include #include "mymoneyfile.h" #include "mymoneyaccount.h" #include "tasks/onlinetask.h" #include "onlinejobadministration.h" #include "mymoneystoragenames.h" using namespace MyMoneyStorageNodes; onlineJob::onlineJob() : MyMoneyObject(*new onlineJobPrivate), m_task(0) { Q_D(onlineJob); d->m_jobSend = QDateTime(); d->m_jobBankAnswerDate = QDateTime(); d->m_jobBankAnswerState = noBankAnswer; d->m_messageList = QList(); d->m_locked = false; } +onlineJob::onlineJob(const QString &id) : + MyMoneyObject(*new onlineJobPrivate, id), + m_task(nullptr) +{ +} + onlineJob::onlineJob(onlineTask* onlinetask, const QString &id) : MyMoneyObject(*new onlineJobPrivate, id), m_task(onlinetask) { Q_D(onlineJob); d->m_jobSend = QDateTime(); d->m_jobBankAnswerDate = QDateTime(); d->m_jobBankAnswerState = noBankAnswer; d->m_messageList = QList(); d->m_locked = false; } onlineJob::onlineJob(onlineTask* onlinetask) : MyMoneyObject(*new onlineJobPrivate, QString()), m_task(onlinetask) { Q_D(onlineJob); d->m_jobSend = QDateTime(); d->m_jobBankAnswerDate = QDateTime(); d->m_jobBankAnswerState = noBankAnswer; d->m_messageList = QList(); d->m_locked = false; } onlineJob::onlineJob(onlineJob const& other) : MyMoneyObject(*new onlineJobPrivate(*other.d_func()), other.id()), m_task(0) { copyPointerFromOtherJob(other); } onlineJob::onlineJob(const QString &id, const onlineJob& other) : MyMoneyObject(*new onlineJobPrivate(*other.d_func()), id), m_task() { Q_D(onlineJob); d->m_jobSend = QDateTime(); d->m_jobBankAnswerDate = QDateTime(); d->m_jobBankAnswerState = noBankAnswer; d->m_messageList = QList(); d->m_locked = false; copyPointerFromOtherJob(other); } onlineJob::onlineJob(const QDomElement& element) : MyMoneyObject(*new onlineJobPrivate, element, true) { Q_D(onlineJob); d->m_messageList = QList(); d->m_locked = false; d->m_jobSend = QDateTime::fromString(element.attribute(d->getAttrName(OnlineJob::Attribute::Send)), Qt::ISODate); d->m_jobBankAnswerDate = QDateTime::fromString(element.attribute(d->getAttrName(OnlineJob::Attribute::BankAnswerDate)), Qt::ISODate); QString state = element.attribute(d->getAttrName(OnlineJob::Attribute::BankAnswerState)); if (state == d->getAttrName(OnlineJob::Attribute::AbortedByUser)) d->m_jobBankAnswerState = abortedByUser; else if (state == d->getAttrName(OnlineJob::Attribute::AcceptedByBank)) d->m_jobBankAnswerState = acceptedByBank; else if (state == d->getAttrName(OnlineJob::Attribute::RejectedByBank)) d->m_jobBankAnswerState = rejectedByBank; else if (state == d->getAttrName(OnlineJob::Attribute::SendingError)) d->m_jobBankAnswerState = sendingError; else d->m_jobBankAnswerState = noBankAnswer; QDomElement taskElem = element.firstChildElement(d->getElName(OnlineJob::Element::OnlineTask)); m_task = onlineJobAdministration::instance()->createOnlineTaskByXml(taskElem.attribute(d->getAttrName(OnlineJob::Attribute::IID)), taskElem); } void onlineJob::copyPointerFromOtherJob(const onlineJob &other) { if (!other.isNull()) m_task = other.constTask()->clone(); } void onlineJob::reset() { Q_D(onlineJob); clearId(); d->m_jobSend = QDateTime(); d->m_jobBankAnswerDate = QDateTime(); d->m_jobBankAnswerState = noBankAnswer; d->m_locked = false; } onlineJob::~onlineJob() { delete m_task; } +void onlineJob::setTask(onlineTask *task) +{ + m_task = task; +} + onlineTask* onlineJob::task() { if (m_task == 0) throw EMPTYTASKEXCEPTION; return m_task; } const onlineTask* onlineJob::task() const { if (m_task == 0) throw EMPTYTASKEXCEPTION; return m_task; } const onlineTask* onlineJob::constTask() const { return task(); } QString onlineJob::taskIid() const { try { return task()->taskName(); } catch (const emptyTask&) { } return QString(); } QString onlineJob::responsibleAccount() const { try { return task()->responsibleAccount(); } catch (const emptyTask&) { } return QString(); } MyMoneyAccount onlineJob::responsibleMyMoneyAccount() const { QString accountId = responsibleAccount(); if (!accountId.isEmpty()) return MyMoneyFile::instance()->account(accountId); return MyMoneyAccount(); } bool onlineJob::setLock(bool enable) { Q_D(onlineJob); d->m_locked = enable; return true; } bool onlineJob::isLocked() const { Q_D(const onlineJob); return d->m_locked; } bool onlineJob::isEditable() const { Q_D(const onlineJob); return (!isLocked() && sendDate().isNull() && (d->m_jobBankAnswerState == noBankAnswer || d->m_jobBankAnswerState == sendingError)); } bool onlineJob::isNull() const { return (m_task == 0); } void onlineJob::setJobSend(const QDateTime &dateTime) { Q_D(onlineJob); d->m_jobSend = dateTime; } void onlineJob::setJobSend() { setJobSend(QDateTime::currentDateTime()); } void onlineJob::setBankAnswer(const onlineJob::sendingState state, const QDateTime &dateTime) { Q_D(onlineJob); d->m_jobBankAnswerState = state; d->m_jobBankAnswerDate = dateTime; } void onlineJob::setBankAnswer(const onlineJob::sendingState state) { setBankAnswer(state, QDateTime::currentDateTime()); } QDateTime onlineJob::bankAnswerDate() const { Q_D(const onlineJob); return d->m_jobBankAnswerDate; } onlineJob::sendingState onlineJob::bankAnswerState() const { Q_D(const onlineJob); return d->m_jobBankAnswerState; } void onlineJob::addJobMessage(const onlineJobMessage& message) { Q_D(onlineJob); d->m_messageList.append(message); } void onlineJob::addJobMessage(const eMyMoney::OnlineJob::MessageType& type, const QString& sender, const QString& message, const QString& errorCode, const QDateTime& timestamp) { Q_D(onlineJob); onlineJobMessage logMessage(type, sender, message, timestamp); logMessage.setSenderErrorCode(errorCode); d->m_messageList.append(logMessage); } void onlineJob::addJobMessage(const eMyMoney::OnlineJob::MessageType& type, const QString& sender, const QString& message, const QString& errorCode) { addJobMessage(type, sender, message, errorCode, QDateTime::currentDateTime()); } void onlineJob::addJobMessage(const eMyMoney::OnlineJob::MessageType& type, const QString& sender, const QString& message) { addJobMessage(type, sender, message, QString(), QDateTime::currentDateTime()); } QList onlineJob::jobMessageList() const { Q_D(const onlineJob); return d->m_messageList; } +void onlineJob::clearJobMessageList() +{ + Q_D(onlineJob); + d->m_messageList = QList(); +} + /** @todo give life */ void onlineJob::writeXML(QDomDocument &document, QDomElement &parent) const { auto el = document.createElement(nodeNames[nnOnlineJob]); Q_D(const onlineJob); d->writeBaseXML(document, el); if (!d->m_jobSend.isNull()) el.setAttribute(d->getAttrName(OnlineJob::Attribute::Send), d->m_jobSend.toString(Qt::ISODate)); if (!d->m_jobBankAnswerDate.isNull()) el.setAttribute(d->getAttrName(OnlineJob::Attribute::BankAnswerDate), d->m_jobBankAnswerDate.toString(Qt::ISODate)); switch (d->m_jobBankAnswerState) { case abortedByUser: el.setAttribute(d->getAttrName(OnlineJob::Attribute::BankAnswerState), d->getAttrName(OnlineJob::Attribute::AbortedByUser)); break; case acceptedByBank: el.setAttribute(d->getAttrName(OnlineJob::Attribute::BankAnswerState), d->getAttrName(OnlineJob::Attribute::AcceptedByBank)); break; case rejectedByBank: el.setAttribute(d->getAttrName(OnlineJob::Attribute::BankAnswerState), d->getAttrName(OnlineJob::Attribute::RejectedByBank)); break; case sendingError: el.setAttribute(d->getAttrName(OnlineJob::Attribute::BankAnswerState), d->getAttrName(OnlineJob::Attribute::SendingError)); break; case noBankAnswer: default: void(); } QDomElement taskEl = document.createElement(d->getElName(OnlineJob::Element::OnlineTask)); taskEl.setAttribute(d->getAttrName(OnlineJob::Attribute::IID), taskIid()); try { task()->writeXML(document, taskEl); // throws execption if there is no task el.appendChild(taskEl); // only append child if there is something to append } catch (const emptyTask&) { } parent.appendChild(el); } bool onlineJob::isValid() const { if (m_task != 0) return m_task->isValid(); return false; } QDateTime onlineJob::sendDate() const { Q_D(const onlineJob); return d->m_jobSend; } bool onlineJob::hasReferenceTo(const QString& id) const { if (m_task != 0) return m_task->hasReferenceTo(id); return false; } diff --git a/kmymoney/mymoney/onlinejob.h b/kmymoney/mymoney/onlinejob.h index bbe811bc4..8fbf00e20 100644 --- a/kmymoney/mymoney/onlinejob.h +++ b/kmymoney/mymoney/onlinejob.h @@ -1,356 +1,362 @@ /* * Copyright 2013-2015 Christian Dávid * * 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 ONLINEJOB_H #define ONLINEJOB_H #define STRINGIFY(x) #x #define TOSTRING(x) STRINGIFY(x) #define BADTASKEXCEPTION badTaskCast("Casted onlineTask with wrong type. " __FILE__ ":" TOSTRING(__LINE__)) #define EMPTYTASKEXCEPTION emptyTask("Requested onlineTask of onlineJob without any task. " __FILE__ ":" TOSTRING(__LINE__)) #include #include #include "mymoneyobject.h" #include "mymoneyexception.h" #include "onlinejobmessage.h" class onlineTask; class MyMoneyAccount; /** * @brief Class to share jobs which can be procceded by an online banking plugin * * This class stores only the status information and a pointer to an @r onlineTask which stores * the real data. So onlineJob is similar to an shared pointer. * * If you know the type of the onlineTask, @r onlineJobTyped is the first choice to use. * * It is save to use because accesses to pointers (e.g. task() ) throw an execption if onlineJob is null. * * Online jobs are usually not created directly but over @r onlineJobAdministration::createOnlineJob. This is * required to allow loading of onlineTasks at runtime and only if needed. * * This class was created to help writing stable and reliable code. Before an unsafe structure (= pointer) * is accessed it is checked. Exceptions are thrown if the content is unsafe. * * @see onlineTask * @see onlineJobTyped * @todo LOW make data implicitly shared */ class onlineJobPrivate; class KMM_MYMONEY_EXPORT onlineJob : public MyMoneyObject { Q_DECLARE_PRIVATE(onlineJob) KMM_MYMONEY_UNIT_TESTABLE public: /** * @brief Contructor for null onlineJobs * * A onlineJob which is null cannot become valid again. * @see isNull() */ onlineJob(); + explicit onlineJob(const QString &id); /** * @brief Default construtor * * The onlineJob takes ownership of the task. The task is deleted in the destructor. */ onlineJob(onlineTask* task, const QString& id); // krazy:exclude=explicit onlineJob(onlineTask* task); // krazy:exclude=explicit /** @brief Contruct from xml */ explicit onlineJob(const QDomElement&); /** * @brief Create new onlineJob as copy of other * * This constructor does not copy the status information but the task only. */ onlineJob(const QString &id, const onlineJob& other); onlineJob(const onlineJob & other); onlineJob(onlineJob && other); onlineJob & operator=(onlineJob other); friend void swap(onlineJob& first, onlineJob& second); virtual ~onlineJob(); + void setTask(onlineTask *task); + /** * @brief Returns task attached to this onlineJob * * You should not store this pointer but use onlineJob::task() (or @r onlineJobTyped::task()) * every time you access it. * * @note The return type may change in future (e.g. to an atomic pointer). But you can always expect * the operator @c -> to work like it does for onlineTask*. * * @throws emptyTask if isNull() */ onlineTask* task(); /** @copydoc task(); */ const onlineTask* task() const; /** * @brief Returns task attached to this onlineJob as const * @throws emptyTask if isNull() */ const onlineTask* constTask() const; /** * @brief Returns task of type T attached to this onlineJob * * Internaly a dynamic_cast is done and the result is checked. * * @throws emptyTask if isNull() * @throws badTaskCast if attached task cannot be casted to T */ template T* task(); /** @copydoc task() */ template const T* task() const; template const T* constTask() const { return task(); } template bool canTaskCast() const; QString taskIid() const; /** @todo implement */ bool hasReferenceTo(const QString &id) const override; void writeXML(QDomDocument &document, QDomElement &parent) const override; /** * @brief The state of a job given by the onlinePlugin */ enum sendingState { noBankAnswer, /**< Used during or before sending or if sendDate().isValid() the job was successfully sent */ acceptedByBank, /**< bank definetly confirmed the job */ rejectedByBank, /**< bank definetly rejected this job */ abortedByUser, /**< aborted by user during sending */ sendingError /**< an error occurred, the job is certainly not executed by the bank */ }; /** * @brief Account this job is related to * * Each job must have an account on which the job operates. This is used to determine * the correct onlinePlugin which can execute this job. If the job is related to more * than one account (e.g. a password change) select a random one. * * @return accountId or QString() if none is set or job isNull. */ virtual QString responsibleAccount() const; /** * @brief Returns the MyMoneyAccount this job is related to * @see responsibleAccount() */ MyMoneyAccount responsibleMyMoneyAccount() const; /** * @brief Check if this onlineJob is editable by the user * * A job is no longer editable by the user if it is used for documentary purposes * e.g. the job was sent to the bank. In that case create a new job based on the * old one. * * @todo make it possible to use onlineJobs as templates */ virtual bool isEditable() const; /** * @brief Checks if this onlineJob has an attached task * * @return true if no task is attached to this job */ virtual bool isNull() const; /** * @brief Checks if an valid onlineTask is attached * * @return true if task().isValid(), false if isNull() or !task.isValid() */ virtual bool isValid() const; /** * @brief DateTime the job was sent to the bank * * A valid return does not mean that this job was accepted by the bank. * * @return A valid QDateTime if send to bank, an QDateTime() if not send. */ virtual QDateTime sendDate() const; /** * @brief Mark this job as send * * To be used by online plugin only! * * Set dateTime to QDateTime to mark unsend. */ virtual void setJobSend(const QDateTime &dateTime); virtual void setJobSend(); /** * @brief The bank's answer to this job * * To be used by online plugin only! * * Set dateTime to QDateTime() and bankAnswer to noState to mark unsend. If bankAnswer == noState dateTime.isNull() must be true! */ void setBankAnswer(const sendingState state, const QDateTime &dateTime); void setBankAnswer(const sendingState state); /** * @brief DateTime of the last status update by the bank * */ QDateTime bankAnswerDate() const; /** * @brief Returns last status sand by bank * @return */ sendingState bankAnswerState() const; /** * @brief locks the onlineJob for sending it * * Used when the job is in sending process by the online plugin. * * A locked onlineJob cannot be removed from the storage. * * @note The onlineJob can still be edited and stored. But it should be done by * the one how owns the lock only. * * @todo Enforce the lock somehow? Note: the onlinePlugin must still be able to * write to the job. * * @param enable true locks the job, false unlocks the job */ virtual bool setLock(bool enable = true); /** * @brief Get lock status */ virtual bool isLocked() const; /** * @brief Make this onlineJob a "new" onlineJob * * Removes all status information, log, and the id. Only * the task is keept. */ virtual void reset(); /** * @brief addJobMessage * * To be used by online plugin only. * @param message */ void addJobMessage(const onlineJobMessage &message); /** * @brief Convenient method to set add a log message */ void addJobMessage(const eMyMoney::OnlineJob::MessageType& type, const QString& sender, const QString& message, const QString& errorCode, const QDateTime& timestamp); void addJobMessage(const eMyMoney::OnlineJob::MessageType& type, const QString& sender, const QString& message, const QString& errorCode); void addJobMessage(const eMyMoney::OnlineJob::MessageType& type, const QString& sender, const QString& message); /** * @brief jobMessageList * @return */ virtual QList jobMessageList() const; + + void clearJobMessageList(); + /** * @brief Thrown if a cast of a task fails * * This is inspired by std::bad_cast */ class badTaskCast : public std::runtime_error { public: explicit badTaskCast(const char *msg) : std::runtime_error(msg) {} }; /** * @brief Thrown if a task of an invalid onlineJob is requested */ class emptyTask : public std::runtime_error { public: explicit emptyTask(const char *msg) : std::runtime_error(msg) {} }; /** @brief onlineTask attatched to this job */ onlineTask* m_task; private: /** @brief Copies stored pointers (used by copy constructors) */ inline void copyPointerFromOtherJob(const onlineJob& other); }; inline void swap(onlineJob& first, onlineJob& second) // krazy:exclude=inline { using std::swap; swap(first.d_ptr, second.d_ptr); swap(first.m_task, second.m_task); } inline onlineJob::onlineJob(onlineJob && other) : onlineJob() // krazy:exclude=inline { swap(*this, other); } inline onlineJob & onlineJob::operator=(onlineJob other) // krazy:exclude=inline { swap(*this, other); return *this; } template T* onlineJob::task() { T* ret = dynamic_cast(m_task); if (ret == 0) throw EMPTYTASKEXCEPTION; return ret; } template const T* onlineJob::task() const { const T* ret = dynamic_cast(m_task); if (ret == 0) throw BADTASKEXCEPTION; return ret; } Q_DECLARE_METATYPE(onlineJob) #endif // ONLINEJOB_H diff --git a/kmymoney/mymoney/tests/mymoneybudget-test.cpp b/kmymoney/mymoney/tests/mymoneybudget-test.cpp index 2b1567244..b608f84e3 100644 --- a/kmymoney/mymoney/tests/mymoneybudget-test.cpp +++ b/kmymoney/mymoney/tests/mymoneybudget-test.cpp @@ -1,296 +1,296 @@ /* * Copyright 2010-2011 Thomas Baumgart * * 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 "mymoneybudget-test.h" #include #define KMM_MYMONEY_UNIT_TESTABLE friend class MyMoneyBudgetTest; #include "mymoneymoney.h" #include "mymoneybudget.h" #include "mymoneybudget_p.h" QTEST_GUILESS_MAIN(MyMoneyBudgetTest) void MyMoneyBudgetTest::init() { } void MyMoneyBudgetTest::cleanup() { } void MyMoneyBudgetTest::addMonthlyToMonthly() { MyMoneyBudget::AccountGroup a0, a1; - a0.setBudgetLevel(MyMoneyBudget::AccountGroup::eMonthly); - a1.setBudgetLevel(MyMoneyBudget::AccountGroup::eMonthly); + a0.setBudgetLevel(eMyMoney::Budget::Level::Monthly); + a1.setBudgetLevel(eMyMoney::Budget::Level::Monthly); MyMoneyBudget::PeriodGroup period; period.setStartDate(QDate(2010, 1, 1)); period.setAmount(MyMoneyMoney(100, 1)); a0.addPeriod(QDate(2010, 1, 1), period); a1.addPeriod(QDate(2010, 1, 1), period); a0 += a1; - QVERIFY(a0.budgetLevel() == MyMoneyBudget::AccountGroup::eMonthly); - QVERIFY(a1.budgetLevel() == MyMoneyBudget::AccountGroup::eMonthly); + QVERIFY(a0.budgetLevel() == eMyMoney::Budget::Level::Monthly); + QVERIFY(a1.budgetLevel() == eMyMoney::Budget::Level::Monthly); QVERIFY(a0.getPeriods().count() == 1); QVERIFY(a1.getPeriods().count() == 1); QVERIFY(a0.balance() == MyMoneyMoney(200, 1)); QVERIFY(a1.balance() == MyMoneyMoney(100, 1)); } void MyMoneyBudgetTest::addMonthlyToYearly() { MyMoneyBudget::AccountGroup a0, a1; - a0.setBudgetLevel(MyMoneyBudget::AccountGroup::eYearly); - a1.setBudgetLevel(MyMoneyBudget::AccountGroup::eMonthly); + a0.setBudgetLevel(eMyMoney::Budget::Level::Yearly); + a1.setBudgetLevel(eMyMoney::Budget::Level::Monthly); MyMoneyBudget::PeriodGroup period; period.setStartDate(QDate(2010, 1, 1)); period.setAmount(MyMoneyMoney(100, 1)); a0.addPeriod(QDate(2010, 1, 1), period); a1.addPeriod(QDate(2010, 1, 1), period); QVERIFY(a0.totalBalance() == MyMoneyMoney(100, 1)); QVERIFY(a1.totalBalance() == MyMoneyMoney(1200, 1)); a0 += a1; - QVERIFY(a0.budgetLevel() == MyMoneyBudget::AccountGroup::eYearly); - QVERIFY(a1.budgetLevel() == MyMoneyBudget::AccountGroup::eMonthly); + QVERIFY(a0.budgetLevel() == eMyMoney::Budget::Level::Yearly); + QVERIFY(a1.budgetLevel() == eMyMoney::Budget::Level::Monthly); QVERIFY(a0.getPeriods().count() == 1); QVERIFY(a1.getPeriods().count() == 1); QVERIFY(a0.totalBalance() == MyMoneyMoney(1300, 1)); QVERIFY(a1.totalBalance() == MyMoneyMoney(1200, 1)); } void MyMoneyBudgetTest::addMonthlyToMonthByMonth() { MyMoneyBudget::AccountGroup a0, a1; - a0.setBudgetLevel(MyMoneyBudget::AccountGroup::eMonthByMonth); - a1.setBudgetLevel(MyMoneyBudget::AccountGroup::eMonthly); + a0.setBudgetLevel(eMyMoney::Budget::Level::MonthByMonth); + a1.setBudgetLevel(eMyMoney::Budget::Level::Monthly); MyMoneyBudget::PeriodGroup period; period.setStartDate(QDate(2010, 1, 1)); period.setAmount(MyMoneyMoney(100, 1)); a1.addPeriod(QDate(2010, 1, 1), period); QDate date(2010, 1, 1); for (int i = 0; i < 12; ++i) { period.setAmount(MyMoneyMoney((i + 1) * 100, 1)); a0.addPeriod(date, period); date = date.addMonths(1); period.setStartDate(date); } QVERIFY(a0.getPeriods().count() == 12); QVERIFY(a0.totalBalance() == MyMoneyMoney(7800, 1)); QVERIFY(a1.totalBalance() == MyMoneyMoney(1200, 1)); a0 += a1; - QVERIFY(a0.budgetLevel() == MyMoneyBudget::AccountGroup::eMonthByMonth); - QVERIFY(a1.budgetLevel() == MyMoneyBudget::AccountGroup::eMonthly); + QVERIFY(a0.budgetLevel() == eMyMoney::Budget::Level::MonthByMonth); + QVERIFY(a1.budgetLevel() == eMyMoney::Budget::Level::Monthly); QVERIFY(a0.getPeriods().count() == 12); QVERIFY(a1.getPeriods().count() == 1); QVERIFY(a0.totalBalance() == MyMoneyMoney(9000, 1)); QVERIFY(a1.totalBalance() == MyMoneyMoney(1200, 1)); } void MyMoneyBudgetTest::addYearlyToMonthly() { MyMoneyBudget::AccountGroup a0, a1; - a0.setBudgetLevel(MyMoneyBudget::AccountGroup::eYearly); - a1.setBudgetLevel(MyMoneyBudget::AccountGroup::eMonthly); + a0.setBudgetLevel(eMyMoney::Budget::Level::Yearly); + a1.setBudgetLevel(eMyMoney::Budget::Level::Monthly); MyMoneyBudget::PeriodGroup period; period.setStartDate(QDate(2010, 1, 1)); period.setAmount(MyMoneyMoney(100, 1)); a0.addPeriod(QDate(2010, 1, 1), period); a1.addPeriod(QDate(2010, 1, 1), period); QVERIFY(a0.totalBalance() == MyMoneyMoney(100, 1)); QVERIFY(a1.totalBalance() == MyMoneyMoney(1200, 1)); a1 += a0; - QVERIFY(a0.budgetLevel() == MyMoneyBudget::AccountGroup::eYearly); - QVERIFY(a1.budgetLevel() == MyMoneyBudget::AccountGroup::eMonthly); + QVERIFY(a0.budgetLevel() == eMyMoney::Budget::Level::Yearly); + QVERIFY(a1.budgetLevel() == eMyMoney::Budget::Level::Monthly); QVERIFY(a0.getPeriods().count() == 1); QVERIFY(a1.getPeriods().count() == 1); QVERIFY(a0.totalBalance() == MyMoneyMoney(100, 1)); QVERIFY(a1.totalBalance() == MyMoneyMoney(1300, 1)); } void MyMoneyBudgetTest::addYearlyToYearly() { MyMoneyBudget::AccountGroup a0, a1; - a0.setBudgetLevel(MyMoneyBudget::AccountGroup::eYearly); - a1.setBudgetLevel(MyMoneyBudget::AccountGroup::eYearly); + a0.setBudgetLevel(eMyMoney::Budget::Level::Yearly); + a1.setBudgetLevel(eMyMoney::Budget::Level::Yearly); MyMoneyBudget::PeriodGroup period; period.setStartDate(QDate(2010, 1, 1)); period.setAmount(MyMoneyMoney(100, 1)); a0.addPeriod(QDate(2010, 1, 1), period); a1.addPeriod(QDate(2010, 1, 1), period); a0 += a1; - QVERIFY(a0.budgetLevel() == MyMoneyBudget::AccountGroup::eYearly); - QVERIFY(a1.budgetLevel() == MyMoneyBudget::AccountGroup::eYearly); + QVERIFY(a0.budgetLevel() == eMyMoney::Budget::Level::Yearly); + QVERIFY(a1.budgetLevel() == eMyMoney::Budget::Level::Yearly); QVERIFY(a0.getPeriods().count() == 1); QVERIFY(a1.getPeriods().count() == 1); QVERIFY(a0.balance() == MyMoneyMoney(200, 1)); QVERIFY(a1.balance() == MyMoneyMoney(100, 1)); } void MyMoneyBudgetTest::addYearlyToMonthByMonth() { MyMoneyBudget::AccountGroup a0, a1; - a0.setBudgetLevel(MyMoneyBudget::AccountGroup::eMonthByMonth); - a1.setBudgetLevel(MyMoneyBudget::AccountGroup::eYearly); + a0.setBudgetLevel(eMyMoney::Budget::Level::MonthByMonth); + a1.setBudgetLevel(eMyMoney::Budget::Level::Yearly); MyMoneyBudget::PeriodGroup period; period.setStartDate(QDate(2010, 1, 1)); period.setAmount(MyMoneyMoney(100, 1)); a1.addPeriod(QDate(2010, 1, 1), period); QDate date(2010, 1, 1); for (int i = 0; i < 12; ++i) { period.setAmount(MyMoneyMoney((i + 1) * 100, 1)); a0.addPeriod(date, period); date = date.addMonths(1); period.setStartDate(date); } QVERIFY(a0.getPeriods().count() == 12); QVERIFY(a0.totalBalance() == MyMoneyMoney(7800, 1)); QVERIFY(a1.totalBalance() == MyMoneyMoney(100, 1)); a0 += a1; - QVERIFY(a0.budgetLevel() == MyMoneyBudget::AccountGroup::eMonthByMonth); - QVERIFY(a1.budgetLevel() == MyMoneyBudget::AccountGroup::eYearly); + QVERIFY(a0.budgetLevel() == eMyMoney::Budget::Level::MonthByMonth); + QVERIFY(a1.budgetLevel() == eMyMoney::Budget::Level::Yearly); QVERIFY(a0.getPeriods().count() == 12); QVERIFY(a1.getPeriods().count() == 1); QVERIFY(a0.totalBalance() == MyMoneyMoney(7900, 1)); QVERIFY(a1.totalBalance() == MyMoneyMoney(100, 1)); } void MyMoneyBudgetTest::addMonthByMonthToMonthly() { MyMoneyBudget::AccountGroup a0, a1; - a0.setBudgetLevel(MyMoneyBudget::AccountGroup::eMonthByMonth); - a1.setBudgetLevel(MyMoneyBudget::AccountGroup::eMonthly); + a0.setBudgetLevel(eMyMoney::Budget::Level::MonthByMonth); + a1.setBudgetLevel(eMyMoney::Budget::Level::Monthly); MyMoneyBudget::PeriodGroup period; period.setStartDate(QDate(2010, 1, 1)); period.setAmount(MyMoneyMoney(100, 1)); a1.addPeriod(QDate(2010, 1, 1), period); QDate date(2010, 1, 1); for (int i = 0; i < 12; ++i) { period.setAmount(MyMoneyMoney((i + 1) * 100, 1)); a0.addPeriod(date, period); date = date.addMonths(1); period.setStartDate(date); } QVERIFY(a0.getPeriods().count() == 12); QVERIFY(a0.totalBalance() == MyMoneyMoney(7800, 1)); QVERIFY(a1.totalBalance() == MyMoneyMoney(1200, 1)); a1 += a0; - QVERIFY(a0.budgetLevel() == MyMoneyBudget::AccountGroup::eMonthByMonth); - QVERIFY(a1.budgetLevel() == MyMoneyBudget::AccountGroup::eMonthByMonth); + QVERIFY(a0.budgetLevel() == eMyMoney::Budget::Level::MonthByMonth); + QVERIFY(a1.budgetLevel() == eMyMoney::Budget::Level::MonthByMonth); QVERIFY(a0.getPeriods().count() == 12); QVERIFY(a1.getPeriods().count() == 12); QVERIFY(a0.totalBalance() == MyMoneyMoney(7800, 1)); QVERIFY(a1.totalBalance() == MyMoneyMoney(9000, 1)); } void MyMoneyBudgetTest::addMonthByMonthToYearly() { MyMoneyBudget::AccountGroup a0, a1; - a0.setBudgetLevel(MyMoneyBudget::AccountGroup::eMonthByMonth); - a1.setBudgetLevel(MyMoneyBudget::AccountGroup::eYearly); + a0.setBudgetLevel(eMyMoney::Budget::Level::MonthByMonth); + a1.setBudgetLevel(eMyMoney::Budget::Level::Yearly); MyMoneyBudget::PeriodGroup period; period.setStartDate(QDate(2010, 1, 1)); period.setAmount(MyMoneyMoney(100, 1)); a1.addPeriod(QDate(2010, 1, 1), period); QDate date(2010, 1, 1); for (int i = 0; i < 12; ++i) { period.setAmount(MyMoneyMoney((i + 1) * 100, 1)); a0.addPeriod(date, period); date = date.addMonths(1); period.setStartDate(date); } QVERIFY(a0.getPeriods().count() == 12); QVERIFY(a0.totalBalance() == MyMoneyMoney(7800, 1)); QVERIFY(a1.totalBalance() == MyMoneyMoney(100, 1)); a1 += a0; - QVERIFY(a0.budgetLevel() == MyMoneyBudget::AccountGroup::eMonthByMonth); - QVERIFY(a1.budgetLevel() == MyMoneyBudget::AccountGroup::eMonthByMonth); + QVERIFY(a0.budgetLevel() == eMyMoney::Budget::Level::MonthByMonth); + QVERIFY(a1.budgetLevel() == eMyMoney::Budget::Level::MonthByMonth); QVERIFY(a0.getPeriods().count() == 12); QVERIFY(a1.getPeriods().count() == 12); QVERIFY(a0.totalBalance() == MyMoneyMoney(7800, 1)); QVERIFY(a1.totalBalance() == MyMoneyMoney(7900, 1)); } void MyMoneyBudgetTest::addMonthByMonthToMonthByMonth() { MyMoneyBudget::AccountGroup a0, a1; - a0.setBudgetLevel(MyMoneyBudget::AccountGroup::eMonthByMonth); - a1.setBudgetLevel(MyMoneyBudget::AccountGroup::eMonthByMonth); + a0.setBudgetLevel(eMyMoney::Budget::Level::MonthByMonth); + a1.setBudgetLevel(eMyMoney::Budget::Level::MonthByMonth); MyMoneyBudget::PeriodGroup period; QDate date(2010, 1, 1); for (int i = 0; i < 12; ++i) { period.setStartDate(date); period.setAmount(MyMoneyMoney((i + 1) * 100, 1)); a0.addPeriod(date, period); period.setAmount(MyMoneyMoney((i + 1) * 200, 1)); a1.addPeriod(date, period); date = date.addMonths(1); } QVERIFY(a0.getPeriods().count() == 12); QVERIFY(a1.getPeriods().count() == 12); QVERIFY(a0.totalBalance() == MyMoneyMoney(7800, 1)); QVERIFY(a1.totalBalance() == MyMoneyMoney(15600, 1)); a0 += a1; - QVERIFY(a0.budgetLevel() == MyMoneyBudget::AccountGroup::eMonthByMonth); - QVERIFY(a1.budgetLevel() == MyMoneyBudget::AccountGroup::eMonthByMonth); + QVERIFY(a0.budgetLevel() == eMyMoney::Budget::Level::MonthByMonth); + QVERIFY(a1.budgetLevel() == eMyMoney::Budget::Level::MonthByMonth); QVERIFY(a0.getPeriods().count() == 12); QVERIFY(a1.getPeriods().count() == 12); QVERIFY(a0.totalBalance() == MyMoneyMoney(23400, 1)); QVERIFY(a1.totalBalance() == MyMoneyMoney(15600, 1)); } void MyMoneyBudgetTest::testElementNames() { for (auto i = (int)Budget::Element::Budget; i <= (int)Budget::Element::Period; ++i) { auto isEmpty = MyMoneyBudgetPrivate::getElName(static_cast(i)).isEmpty(); if (isEmpty) qWarning() << "Empty element's name " << i; QVERIFY(!isEmpty); } } void MyMoneyBudgetTest::testAttributeNames() { for (auto i = (int)Budget::Attribute::ID; i < (int)Budget::Attribute::LastAttribute; ++i) { auto isEmpty = MyMoneyBudgetPrivate::getAttrName(static_cast(i)).isEmpty(); if (isEmpty) qWarning() << "Empty attribute's name " << i; QVERIFY(!isEmpty); } } diff --git a/kmymoney/mymoney/tests/mymoneyforecast-test.cpp b/kmymoney/mymoney/tests/mymoneyforecast-test.cpp index c4803c863..a3531a7d3 100644 --- a/kmymoney/mymoney/tests/mymoneyforecast-test.cpp +++ b/kmymoney/mymoney/tests/mymoneyforecast-test.cpp @@ -1,990 +1,992 @@ /* * Copyright 2007-2010 Alvaro Soliverez * * 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 "mymoneyforecast-test.h" #include #include #include #include "mymoneybudget.h" #include "mymoneyexception.h" #include "mymoneystoragedump.h" #include "reportstestcommon.h" #include "mymoneyinstitution.h" #include "mymoneysecurity.h" #include "mymoneymoney.h" #include "mymoneysplit.h" #include "mymoneyschedule.h" #include "mymoneypayee.h" #include "mymoneyenums.h" using namespace eMyMoney; using namespace test; QTEST_GUILESS_MAIN(MyMoneyForecastTest) MyMoneyForecastTest::MyMoneyForecastTest() : m(nullptr), storage(nullptr), file(nullptr) { try { this->moT1 = MyMoneyMoney(57, 1); this->moT2 = MyMoneyMoney(63, 1); this->moT3 = MyMoneyMoney(84, 1); this->moT4 = MyMoneyMoney(62, 1); this->moT5 = MyMoneyMoney(104, 1); } catch (const MyMoneyException &e) { qDebug() << e.what(); } } void MyMoneyForecastTest::init() { //all this has been taken from pivottabletest.cpp, by Thomas Baumgart and Ace Jones storage = new MyMoneyStorageMgr; file = MyMoneyFile::instance(); file->attachStorage(storage); MyMoneyFileTransaction ft; file->addCurrency(MyMoneySecurity("CAD", "Canadian Dollar", "C$")); file->addCurrency(MyMoneySecurity("USD", "US Dollar", "$")); file->addCurrency(MyMoneySecurity("JPY", "Japanese Yen", QChar(0x00A5), 1)); file->addCurrency(MyMoneySecurity("GBP", "British Pound", "#")); file->setBaseCurrency(file->currency("USD")); - MyMoneyPayee payeeTest("Test Payee"); + MyMoneyPayee payeeTest; + payeeTest.setName("Test Payee"); file->addPayee(payeeTest); - MyMoneyPayee payeeTest2("Alvaro Soliverez"); + MyMoneyPayee payeeTest2; + payeeTest2.setName("Alvaro Soliverez"); file->addPayee(payeeTest2); acAsset = (MyMoneyFile::instance()->asset().id()); acLiability = (MyMoneyFile::instance()->liability().id()); acExpense = (MyMoneyFile::instance()->expense().id()); acIncome = (MyMoneyFile::instance()->income().id()); acChecking = makeAccount(QString("Checking Account"), Account::Type::Checkings, moCheckingOpen, QDate(2004, 5, 15), acAsset, "USD"); acCredit = makeAccount(QString("Credit Card"), Account::Type::CreditCard, moCreditOpen, QDate(2004, 7, 15), acLiability, "USD"); acSolo = makeAccount(QString("Solo"), Account::Type::Expense, MyMoneyMoney(), QDate(2004, 1, 11), acExpense, "USD"); acParent = makeAccount(QString("Parent"), Account::Type::Expense, MyMoneyMoney(), QDate(2004, 1, 11), acExpense, "USD"); acChild = makeAccount(QString("Child"), Account::Type::Expense, MyMoneyMoney(), QDate(2004, 2, 11), acParent, "USD"); acForeign = makeAccount(QString("Foreign"), Account::Type::Expense, MyMoneyMoney(), QDate(2004, 1, 11), acExpense, "USD"); acInvestment = makeAccount("Investment", Account::Type::Investment, moZero, QDate(2004, 1, 1), acAsset, "USD"); acSecondChild = makeAccount(QString("Second Child"), Account::Type::Expense, MyMoneyMoney(), QDate(2004, 2, 11), acParent, "USD"); acGrandChild1 = makeAccount(QString("Grand Child 1"), Account::Type::Expense, MyMoneyMoney(), QDate(2004, 2, 11), acChild, "USD"); acGrandChild2 = makeAccount(QString("Grand Child 2"), Account::Type::Expense, MyMoneyMoney(), QDate(2004, 2, 11), acChild, "USD"); //this account added to have an account to test opening date calculations acCash = makeAccount(QString("Cash"), Account::Type::Cash, moCreditOpen, QDate::currentDate().addDays(-2), acAsset, "USD"); MyMoneyInstitution i("Bank of the World", "", "", "", "", "", ""); file->addInstitution(i); inBank = i.id(); ft.commit(); } void MyMoneyForecastTest::cleanup() { file->detachStorage(storage); delete storage; } void MyMoneyForecastTest::testEmptyConstructor() { MyMoneyForecast a; MyMoneyAccount b; QVERIFY(a.forecastBalance(b, QDate::currentDate()).isZero()); QVERIFY(!a.isForecastAccount(b)); QVERIFY(a.forecastBalance(b, QDate::currentDate()) == MyMoneyMoney()); QVERIFY(a.daysToMinimumBalance(b) == -1); QVERIFY(a.daysToZeroBalance(b) == -2); QVERIFY(a.forecastDays() == 90); QVERIFY(a.accountsCycle() == 30); QVERIFY(a.forecastCycles() == 3); QVERIFY(a.historyStartDate() == QDate::currentDate().addDays(-3*30)); QVERIFY(a.historyEndDate() == QDate::currentDate().addDays(-1)); QVERIFY(a.historyDays() == 30 * 3); } void MyMoneyForecastTest::testDoForecastInit() { MyMoneyForecast a; a.doForecast(); /* //check the illegal argument validation try { KMyMoneySettings::setForecastDays(-10); a.doForecast(); } catch (const MyMoneyException &e) { QFAIL("Unexpected exception"); } try { KMyMoneySettings::setForecastAccountCycle(-20); a.doForecast(); } catch (const MyMoneyException &e) { QFAIL("Unexpected exception"); } try { KMyMoneySettings::setForecastCycles(-10); a.doForecast(); } catch (const MyMoneyException &e) { QFAIL("Unexpected exception"); } try { KMyMoneySettings::setForecastAccountCycle(0); a.doForecast(); } catch (const MyMoneyException &e) { QFAIL("Unexpected exception"); } try { KMyMoneySettings::setForecastDays(0); KMyMoneySettings::setForecastCycles(0); KMyMoneySettings::setForecastAccountCycle(0); a.doForecast(); } catch (const MyMoneyException &e) { QVERIFY("Unexpected exception"); }*/ } //test that it forecasts correctly with transactions in the period of forecast void MyMoneyForecastTest::testDoForecast() { //set up environment MyMoneyForecast a; MyMoneyAccount a_checking = file->account(acChecking); MyMoneyAccount a_credit = file->account(acCredit); //test empty forecast a.doForecast(); //this is just to check nothing goes wrong if forecast is run agains an empty template //setup some transactions TransactionHelper t1(QDate::currentDate().addDays(-1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), this->moT1, acChecking, acSolo); TransactionHelper t2(QDate::currentDate().addDays(-1), MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit), -(this->moT2), acCredit, acParent); TransactionHelper t3(QDate::currentDate().addDays(-1), MyMoneySplit::actionName(eMyMoney::Split::Action::Transfer), this->moT1, acCredit, acChecking); a.setForecastMethod(1); a.setForecastDays(3); a.setAccountsCycle(1); a.setForecastCycles(1); a.setBeginForecastDay(0); a.setHistoryMethod(0); //moving average a.doForecast(); //checking didn't have balance variations, so the forecast should be equal to the current balance MyMoneyMoney b_checking = file->balance(a_checking.id(), QDate::currentDate()); QVERIFY(a.forecastBalance(a_checking, QDate::currentDate().addDays(1)) == b_checking); QVERIFY(a.forecastBalance(a_checking, QDate::currentDate().addDays(2)) == b_checking); QVERIFY(a.forecastBalance(a_checking, QDate::currentDate().addDays(3)) == b_checking); QVERIFY(a.forecastBalance(a_checking, QDate::currentDate()) == b_checking); //credit had a variation so the forecast should be different for each day MyMoneyMoney b_credit = file->balance(a_credit.id(), QDate::currentDate()); QVERIFY(a.forecastBalance(a_credit, 0) == b_credit); QVERIFY(a.forecastBalance(a_credit, QDate::currentDate().addDays(1)) == (b_credit + (moT2 - moT1))); QVERIFY(a.forecastBalance(a_credit, QDate::currentDate().addDays(2)) == (b_credit + ((moT2 - moT1)*2))); QVERIFY(a.forecastBalance(a_credit, QDate::currentDate().addDays(3)) == b_credit + ((moT2 - moT1)*3)); a.setHistoryMethod(1); //weighted moving average a.doForecast(); QVERIFY(a.forecastBalance(a_credit, 0) == b_credit); QVERIFY(a.forecastBalance(a_credit, QDate::currentDate().addDays(1)) == (b_credit + (moT2 - moT1))); QVERIFY(a.forecastBalance(a_credit, QDate::currentDate().addDays(2)) == (b_credit + ((moT2 - moT1)*2))); QVERIFY(a.forecastBalance(a_credit, QDate::currentDate().addDays(3)) == b_credit + ((moT2 - moT1)*3)); //insert transactions outside the forecast period. The calculation should be the same. TransactionHelper t4(QDate::currentDate().addDays(-2), MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit), -moT2, acCredit, acParent); TransactionHelper t5(QDate::currentDate().addDays(-10), MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit), -moT2, acCredit, acParent); TransactionHelper t6(QDate::currentDate().addDays(-3), MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit), -moT2, acCredit, acParent); a.setForecastMethod(1); a.setForecastDays(3); a.setAccountsCycle(1); a.setForecastCycles(1); a.setBeginForecastDay(0); a.setHistoryMethod(0); //moving average a.doForecast(); //check forecast b_credit = file->balance(a_credit.id(), QDate::currentDate()); MyMoneyMoney b_credit_1_exp = (b_credit + ((moT2 - moT1))); MyMoneyMoney b_credit_2 = a.forecastBalance(a_credit, QDate::currentDate().addDays(2)); MyMoneyMoney b_credit_2_exp = (b_credit + ((moT2 - moT1) * 2)); QVERIFY(a.forecastBalance(a_credit, QDate::currentDate()) == file->balance(a_credit.id(), QDate::currentDate())); QVERIFY(a.forecastBalance(a_credit, QDate::currentDate().addDays(1)) == b_credit + (moT2 - moT1)); QVERIFY(a.forecastBalance(a_credit, QDate::currentDate().addDays(2)) == b_credit + ((moT2 - moT1)*2)); QVERIFY(a.forecastBalance(a_credit, QDate::currentDate().addDays(3)) == b_credit + ((moT2 - moT1)*3)); //test weighted moving average a.setForecastMethod(1); a.setForecastDays(3); a.setAccountsCycle(1); a.setForecastCycles(3); a.setBeginForecastDay(0); a.setHistoryMethod(1); a.doForecast(); QVERIFY(a.forecastBalance(a_credit, 0) == b_credit); QVERIFY(a.forecastBalance(a_credit, QDate::currentDate().addDays(1)) == (b_credit + (((moT2 - moT1)*3 + moT2*2 + moT2) / MyMoneyMoney(6, 1)))); } void MyMoneyForecastTest::testGetForecastAccountList() { MyMoneyForecast a; MyMoneyAccount a_checking = file->account(acChecking); MyMoneyAccount a_parent = file->account(acParent); QList b; b = a.forecastAccountList(); //check that it contains asset account, but not expense accounts QVERIFY(b.contains(a_checking)); QVERIFY(!b.contains(a_parent)); } void MyMoneyForecastTest::testCalculateAccountTrend() { //set up environment TransactionHelper t1(QDate::currentDate().addDays(-3), MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit), -moT2, acChecking, acSolo); MyMoneyAccount a_checking = file->account(acChecking); //test invalid arguments try { MyMoneyForecast::calculateAccountTrend(a_checking, 0); } catch (const MyMoneyException &e) { QVERIFY(QString::fromLatin1(e.what()).startsWith("Illegal arguments when calling calculateAccountTrend. trendDays must be higher than 0")); } try { MyMoneyForecast::calculateAccountTrend(a_checking, -10); } catch (const MyMoneyException &e) { QVERIFY(QString::fromLatin1(e.what()).startsWith("Illegal arguments when calling calculateAccountTrend. trendDays must be higher than 0")); } //test that it calculates correctly QVERIFY(MyMoneyForecast::calculateAccountTrend(a_checking , 3) == moT2 / MyMoneyMoney(3, 1)); //test that it works for all kind of accounts MyMoneyAccount a_solo = file->account(acSolo); MyMoneyMoney soloTrend = MyMoneyForecast::calculateAccountTrend(a_solo, 3); MyMoneyMoney soloTrendExp = -moT2 / MyMoneyMoney(3, 1); QVERIFY(MyMoneyForecast::calculateAccountTrend(a_solo, 3) == -moT2 / MyMoneyMoney(3, 1)); //test that it does not take into account the transactions of the opening date of the account MyMoneyAccount a_cash = file->account(acCash); TransactionHelper t2(QDate::currentDate().addDays(-2), MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit), moT2, acCash, acParent); TransactionHelper t3(QDate::currentDate().addDays(-1), MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit), moT1, acCash, acParent); QVERIFY(MyMoneyForecast::calculateAccountTrend(a_cash, 3) == -moT1); } void MyMoneyForecastTest::testGetForecastBalance() { //set up environment MyMoneyForecast a; TransactionHelper t1(QDate::currentDate().addDays(-1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), this->moT1, acChecking, acSolo); TransactionHelper t2(QDate::currentDate().addDays(-1), MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit), -(this->moT2), acCredit, acParent); TransactionHelper t3(QDate::currentDate().addDays(-1), MyMoneySplit::actionName(eMyMoney::Split::Action::Transfer), this->moT1, acCredit, acChecking); a.setForecastMethod(1); a.setForecastDays(3); a.setAccountsCycle(1); a.setForecastCycles(1); a.setHistoryMethod(0); a.doForecast(); MyMoneyAccount a_checking = file->account(acChecking); MyMoneyAccount a_credit = file->account(acCredit); //test invalid arguments QVERIFY(a.forecastBalance(a_checking, QDate::currentDate().addDays(-1)) == MyMoneyMoney()); QVERIFY(a.forecastBalance(a_checking, QDate::currentDate().addDays(-10)) == MyMoneyMoney()); QVERIFY(a.forecastBalance(a_checking, -1) == MyMoneyMoney()); QVERIFY(a.forecastBalance(a_checking, -100) == MyMoneyMoney()); //test a date outside the forecast days QVERIFY(a.forecastBalance(a_checking, QDate::currentDate().addDays(4)) == MyMoneyMoney()); QVERIFY(a.forecastBalance(a_checking, 4) == MyMoneyMoney()); QVERIFY(a.forecastBalance(a_checking, QDate::currentDate().addDays(10)) == MyMoneyMoney()); QVERIFY(a.forecastBalance(a_checking, 10) == MyMoneyMoney()); //test it returns valid results MyMoneyMoney b_credit = file->balance(a_credit.id(), QDate::currentDate()); QVERIFY(a.forecastBalance(a_credit, QDate::currentDate()) == file->balance(a_credit.id(), QDate::currentDate())); QVERIFY(a.forecastBalance(a_credit, QDate::currentDate().addDays(1)) == b_credit + (moT2 - moT1)); QVERIFY(a.forecastBalance(a_credit, QDate::currentDate().addDays(2)) == b_credit + ((moT2 - moT1)*2)); QVERIFY(a.forecastBalance(a_credit, QDate::currentDate().addDays(3)) == b_credit + ((moT2 - moT1)*3)); } void MyMoneyForecastTest::testIsForecastAccount() { MyMoneyForecast a; MyMoneyAccount a_checking = file->account(acChecking); MyMoneyAccount a_solo = file->account(acSolo); MyMoneyAccount a_investment = file->account(acInvestment); //test an invalid account QVERIFY(a.isForecastAccount(a_solo) == false); QVERIFY(a.isForecastAccount(a_investment) == true); //test a valid account QVERIFY(a.isForecastAccount(a_checking) == true); } void MyMoneyForecastTest::testDoFutureScheduledForecast() { //set up future transactions MyMoneyForecast a; MyMoneyAccount a_cash = file->account(acCash); TransactionHelper t1(QDate::currentDate().addDays(1), MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit), -moT1, acCash, acParent); TransactionHelper t2(QDate::currentDate().addDays(2), MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit), -moT2, acCash, acParent); TransactionHelper t3(QDate::currentDate().addDays(3), MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit), -moT3, acCash, acParent); TransactionHelper t4(QDate::currentDate().addDays(10), MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit), -moT4, acCash, acParent); a.setForecastMethod(0); a.setForecastDays(3); a.setAccountsCycle(1); a.setForecastCycles(1); a.doForecast(); MyMoneyMoney b_cash = file->balance(a_cash.id(), QDate::currentDate()); //test valid results QVERIFY(a.forecastBalance(a_cash, QDate::currentDate()) == b_cash); QVERIFY(a.forecastBalance(a_cash, QDate::currentDate().addDays(1)) == b_cash + moT1); QVERIFY(a.forecastBalance(a_cash, QDate::currentDate().addDays(2)) == b_cash + moT1 + moT2); QVERIFY(a.forecastBalance(a_cash, QDate::currentDate().addDays(3)) == b_cash + moT1 + moT2 + moT3); } void MyMoneyForecastTest::testScheduleForecast() { //set up schedule environment for testing MyMoneyAccount a_cash = file->account(acCash); MyMoneyAccount a_parent = file->account(acParent); MyMoneyFileTransaction ft; MyMoneySchedule sch("A Name", Schedule::Type::Bill, Schedule::Occurrence::Weekly, 1, Schedule::PaymentType::DirectDebit, QDate::currentDate().addDays(1), QDate(), true, true); MyMoneyTransaction t; t.setPostDate(QDate::currentDate().addDays(1)); t.setEntryDate(QDate::currentDate().addDays(1)); //t.setId("T000000000000000001"); t.setBankID("BID"); t.setMemo("Wohnung:Miete"); t.setCommodity("USD"); t.setValue("key", "value"); MyMoneySplit s; s.setPayeeId("P000001"); s.setShares(moT2); s.setValue(moT2); s.setAccountId(a_parent.id()); s.setBankID("SPID1"); s.setReconcileFlag(eMyMoney::Split::State::Reconciled); t.addSplit(s); s.setPayeeId("P000001"); s.setShares(-moT2); s.setValue(-moT2); s.setAccountId(a_cash.id()); s.setBankID("SPID2"); s.setReconcileFlag(eMyMoney::Split::State::Cleared); s.clearId(); t.addSplit(s); sch.setTransaction(t); file->addSchedule(sch); ft.commit(); MyMoneyFileTransaction ft3; MyMoneySchedule sch3("A Name1", Schedule::Type::Bill, Schedule::Occurrence::Weekly, 1, Schedule::PaymentType::DirectDebit, QDate::currentDate().addDays(5), QDate(), true, true); //sch.setLastPayment(QDate::currentDate()); //sch.recordPayment(QDate::currentDate().addDays(1)); //sch.setId("SCH0001"); MyMoneyTransaction t3; t3.setPostDate(QDate::currentDate().addDays(5)); t3.setEntryDate(QDate::currentDate().addDays(5)); //t.setId("T000000000000000001"); t3.setBankID("BID"); t3.setMemo("Wohnung:Miete"); t3.setCommodity("USD"); t3.setValue("key", "value"); MyMoneySplit s3; s3.setPayeeId("P000001"); s3.setShares(moT2); s3.setValue(moT2); s3.setAccountId(a_parent.id()); s3.setBankID("SPID1"); s3.setReconcileFlag(eMyMoney::Split::State::Reconciled); t3.addSplit(s3); s3.setPayeeId("P000001"); s3.setShares(-moT2); s3.setValue(-moT2); s3.setAccountId(a_cash.id()); s3.setBankID("SPID2"); s3.setReconcileFlag(eMyMoney::Split::State::Cleared); s3.clearId(); t3.addSplit(s3); sch3.setTransaction(t3); file->addSchedule(sch3); ft3.commit(); MyMoneyFileTransaction ft2; MyMoneySchedule sch2("A Name2", Schedule::Type::Bill, Schedule::Occurrence::Weekly, 1, Schedule::PaymentType::DirectDebit, QDate::currentDate().addDays(2), QDate(), true, true); //sch.setLastPayment(QDate::currentDate()); //sch.recordPayment(QDate::currentDate().addDays(1)); //sch.setId("SCH0001"); MyMoneyTransaction t2; t2.setPostDate(QDate::currentDate().addDays(2)); t2.setEntryDate(QDate::currentDate().addDays(2)); //t.setId("T000000000000000001"); t2.setBankID("BID"); t2.setMemo("Wohnung:Miete"); t2.setCommodity("USD"); t2.setValue("key", "value"); MyMoneySplit s2; s2.setPayeeId("P000001"); s2.setShares(moT1); s2.setValue(moT1); s2.setAccountId(a_parent.id()); s2.setBankID("SPID1"); s2.setReconcileFlag(eMyMoney::Split::State::Reconciled); t2.addSplit(s2); s2.setPayeeId("P000001"); s2.setShares(-moT1); s2.setValue(-moT1); s2.setAccountId(a_cash.id()); s2.setBankID("SPID2"); s2.setReconcileFlag(eMyMoney::Split::State::Cleared); s2.clearId(); t2.addSplit(s2); sch2.setTransaction(t2); file->addSchedule(sch2); ft2.commit(); //run forecast MyMoneyForecast a; a.setForecastMethod(0); a.setForecastDays(3); a.setAccountsCycle(1); a.setForecastCycles(1); a.doForecast(); //check result for single schedule MyMoneyMoney b_cash = file->balance(a_cash.id(), QDate::currentDate()); MyMoneyMoney b_cash1 = a.forecastBalance(a_cash, QDate::currentDate().addDays(1)); //test valid results QVERIFY(a.forecastBalance(a_cash, QDate::currentDate()) == b_cash); QVERIFY(a.forecastBalance(a_cash, QDate::currentDate().addDays(1)) == b_cash - moT2); QVERIFY(a.forecastBalance(a_cash, QDate::currentDate().addDays(2)) == b_cash - moT2 - moT1); } void MyMoneyForecastTest::testDaysToMinimumBalance() { //setup environment MyMoneyForecast a; MyMoneyAccount a_cash = file->account(acCash); MyMoneyAccount a_credit = file->account(acCredit); MyMoneyAccount a_parent = file->account(acParent); a_cash.setValue("minBalanceAbsolute", "50"); a_credit.setValue("minBalanceAbsolute", "50"); TransactionHelper t1(QDate::currentDate().addDays(-1), MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit), -moT1, acCash, acParent); TransactionHelper t2(QDate::currentDate().addDays(2), MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit), moT2, acCash, acParent); TransactionHelper t3(QDate::currentDate().addDays(-1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), -moT1, acCredit, acParent); TransactionHelper t4(QDate::currentDate().addDays(4), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moT5, acCredit, acParent); a.setForecastMethod(0); a.setForecastDays(3); a.setAccountsCycle(1); a.setForecastCycles(1); a.setBeginForecastDay(0); a.doForecast(); //test invalid arguments MyMoneyAccount nullAcc; QVERIFY(a.daysToMinimumBalance(nullAcc) == -1); //test when not a forecast account QVERIFY(a.daysToMinimumBalance(a_parent) == -1); //test it warns when inside the forecast period QVERIFY(a.daysToMinimumBalance(a_cash) == 2); //test it does not warn when it will be outside of the forecast period QVERIFY(a.daysToMinimumBalance(a_credit) == -1); } void MyMoneyForecastTest::testDaysToZeroBalance() { //set up environment MyMoneyAccount a_Solo = file->account(acSolo); MyMoneyAccount a_Cash = file->account(acCash); MyMoneyAccount a_Credit = file->account(acCredit); //MyMoneyFileTransaction ft; TransactionHelper t1(QDate::currentDate().addDays(2), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), -moT1, acChecking, acSolo); TransactionHelper t2(QDate::currentDate().addDays(2), MyMoneySplit::actionName(eMyMoney::Split::Action::Transfer), (moT5), acCash, acCredit); TransactionHelper t3(QDate::currentDate().addDays(2), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), (moT5*100), acCredit, acParent); //ft.commit(); MyMoneyForecast a; a.setForecastMethod(0); a.setForecastDays(30); a.setAccountsCycle(1); a.setForecastCycles(3); a.doForecast(); //test invalid arguments MyMoneyAccount nullAcc; try { auto days = a.daysToZeroBalance(nullAcc); Q_UNUSED(days) } catch (const MyMoneyException &) { QFAIL("Unexpected exception"); } //test when not a forecast account MyMoneyAccount a_solo = file->account(acSolo); auto iSolo = a.daysToZeroBalance(a_Solo); QVERIFY(iSolo == -2); //test it warns when inside the forecast period MyMoneyMoney fCash = a.forecastBalance(a_Cash, QDate::currentDate().addDays(2)); QVERIFY(a.daysToZeroBalance(a_Cash) == 2); //test it does not warn when it will be outside of the forecast period } void MyMoneyForecastTest::testSkipOpeningDate() { //set up environment MyMoneyForecast a; TransactionHelper t1(QDate::currentDate().addDays(-2), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), this->moT1, acCash, acSolo); TransactionHelper t2(QDate::currentDate().addDays(-1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), this->moT2, acCash, acSolo); a.setForecastMethod(1); a.setForecastDays(3); a.setAccountsCycle(2); a.setForecastCycles(1); a.setHistoryMethod(0); a.doForecast(); MyMoneyAccount a_cash = file->account(acCash); //test it has no variation because it skipped the variation of the opening date MyMoneyMoney b_cash = file->balance(a_cash.id(), QDate::currentDate()); QVERIFY(a.skipOpeningDate() == true); QVERIFY(a.forecastBalance(a_cash, QDate::currentDate()) == b_cash); QVERIFY(a.forecastBalance(a_cash, QDate::currentDate().addDays(1)) == b_cash); QVERIFY(a.forecastBalance(a_cash, QDate::currentDate().addDays(2)) == b_cash - moT2); QVERIFY(a.forecastBalance(a_cash, QDate::currentDate().addDays(3)) == b_cash - moT2); } void MyMoneyForecastTest::testAccountMinimumBalanceDateList() { //set up environment MyMoneyForecast a; TransactionHelper t1(QDate::currentDate().addDays(-2), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), this->moT1, acCash, acSolo); TransactionHelper t2(QDate::currentDate().addDays(-1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), this->moT2, acCash, acSolo); a.setForecastMethod(1); a.setForecastDays(6); a.setAccountsCycle(2); a.setForecastCycles(3); a.setHistoryMethod(0); a.setBeginForecastDay(QDate::currentDate().addDays(1).day()); a.doForecast(); MyMoneyAccount a_cash = file->account(acCash); //test QList dateList; dateList = a.accountMinimumBalanceDateList(a_cash); QList::iterator it = dateList.begin(); QDate minDate = *it; QVERIFY(minDate == QDate::currentDate().addDays(2)); it++; minDate = *it; QVERIFY(minDate == QDate::currentDate().addDays(4)); it++; minDate = *it; QVERIFY(minDate == QDate::currentDate().addDays(6)); } void MyMoneyForecastTest::testAccountMaximumBalanceDateList() { //set up environment MyMoneyForecast a; TransactionHelper t1(QDate::currentDate().addDays(-2), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), this->moT1, acCash, acSolo); TransactionHelper t2(QDate::currentDate().addDays(-1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), this->moT2, acCash, acSolo); a.setForecastMethod(1); a.setForecastDays(6); a.setAccountsCycle(2); a.setForecastCycles(3); a.setHistoryMethod(0); a.setBeginForecastDay(QDate::currentDate().addDays(1).day()); a.doForecast(); MyMoneyAccount a_cash = file->account(acCash); //test QList dateList; dateList = a.accountMaximumBalanceDateList(a_cash); QList::iterator it = dateList.begin(); QDate maxDate = *it; QVERIFY(maxDate == QDate::currentDate().addDays(1)); it++; maxDate = *it; QVERIFY(maxDate == QDate::currentDate().addDays(3)); it++; maxDate = *it; QVERIFY(maxDate == QDate::currentDate().addDays(5)); } void MyMoneyForecastTest::testAccountAverageBalance() { //set up environment MyMoneyForecast a; TransactionHelper t1(QDate::currentDate().addDays(-2), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), this->moT1, acCash, acSolo); TransactionHelper t2(QDate::currentDate().addDays(-1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), this->moT2, acCash, acSolo); a.setForecastMethod(1); a.setForecastDays(3); a.setAccountsCycle(2); a.setForecastCycles(1); a.setBeginForecastDay(0); a.doForecast(); MyMoneyAccount a_cash = file->account(acCash); //test MyMoneyMoney b_cash1 = a.forecastBalance(a_cash, QDate::currentDate().addDays(1)); MyMoneyMoney b_cash2 = a.forecastBalance(a_cash, QDate::currentDate().addDays(2)); MyMoneyMoney b_cash3 = a.forecastBalance(a_cash, QDate::currentDate().addDays(3)); MyMoneyMoney average = (b_cash1 + b_cash2 + b_cash3) / MyMoneyMoney(3, 1); QVERIFY(a.accountAverageBalance(a_cash) == average); } void MyMoneyForecastTest::testBeginForecastDate() { //set up environment MyMoneyForecast a; QDate beginDate; qint64 beginDay; a.setForecastMethod(1); a.setForecastDays(90); a.setAccountsCycle(14); a.setForecastCycles(3); a.setBeginForecastDay(0); a.doForecast(); //test when using old method without begin day QVERIFY(QDate::currentDate() == a.beginForecastDate()); //setup begin to last day of month a.setBeginForecastDay(31); beginDay = a.beginForecastDay(); a.doForecast(); //test if (QDate::currentDate().day() < beginDay) { if (QDate::currentDate().daysInMonth() < beginDay) beginDay = QDate::currentDate().daysInMonth(); beginDate = QDate(QDate::currentDate().year(), QDate::currentDate().month(), beginDay); QVERIFY(beginDate == a.beginForecastDate()); } //setup begin day to same date a.setBeginForecastDay(QDate::currentDate().day()); beginDay = a.beginForecastDay(); a.doForecast(); QVERIFY(QDate::currentDate() == a.beginForecastDate()); //setup to first day of month with small interval a.setBeginForecastDay(1); a.setAccountsCycle(1); beginDay = a.beginForecastDay(); a.doForecast(); //test if (QDate::currentDate() == a.beginForecastDate()) { QVERIFY(QDate::currentDate() == a.beginForecastDate()); } else { beginDay = ((((QDate::currentDate().day() - beginDay) / a.accountsCycle()) + 1) * a.accountsCycle()) + beginDay; if (beginDay > QDate::currentDate().daysInMonth()) beginDay = QDate::currentDate().daysInMonth(); beginDate = QDate(QDate::currentDate().year(), QDate::currentDate().month(), beginDay); if (QDate::currentDate().day() == QDate::currentDate().daysInMonth()) { std::cout << std::endl << "testBeginForecastDate(): test of first day of month with small interval skipped because it is the last day of month" << std::endl; } else { QVERIFY(beginDate == a.beginForecastDate()); } } //setup to test when current date plus cycle equals begin day a.setAccountsCycle(14); beginDay = QDate::currentDate().addDays(14).day(); a.setBeginForecastDay(beginDay); beginDate = QDate::currentDate().addDays(14); a.doForecast(); //test QVERIFY(beginDate == a.beginForecastDate()); //setup to test when the begin day will be next month a.setBeginForecastDay(1); a.setAccountsCycle(40); a.doForecast(); beginDate = QDate(QDate::currentDate().addMonths(1).year(), QDate::currentDate().addMonths(1).month(), 1); //test if (QDate::currentDate().day() > 1) { QVERIFY(beginDate == a.beginForecastDate()); } else { //test is not valid if today is 1st of month std::cout << std::endl << "testBeginForecastDate(): test of first day of month skipped because current day is 1st of month" << std::endl; } } void MyMoneyForecastTest::testHistoryDays() { MyMoneyForecast a; QVERIFY(a.historyStartDate() == QDate::currentDate().addDays(-a.forecastCycles()*a.accountsCycle())); QVERIFY(a.historyEndDate() == QDate::currentDate().addDays(-1)); QVERIFY(a.historyDays() == a.forecastCycles()*a.accountsCycle()); a.setForecastMethod(1); a.setForecastDays(90); a.setAccountsCycle(14); a.setForecastCycles(3); a.setBeginForecastDay(0); a.doForecast(); QVERIFY(a.historyStartDate() == QDate::currentDate().addDays(-14*3)); QVERIFY(a.historyDays() == (14*3)); QVERIFY(a.historyEndDate() == (QDate::currentDate().addDays(-1))); } void MyMoneyForecastTest::testCreateBudget() { //set up environment MyMoneyForecast a; MyMoneyForecast b; MyMoneyBudget budget; TransactionHelper t1(QDate(2005, 1, 3), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), this->moT1, acCash, acSolo); TransactionHelper t2(QDate(2005, 1, 15), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), this->moT2, acCash, acParent); TransactionHelper t3(QDate(2005, 1, 30), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), this->moT3, acCash, acSolo); TransactionHelper t4(QDate(2006, 1, 25), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), this->moT4, acCash, acParent); TransactionHelper t5(QDate(2005, 4, 3), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), this->moT1, acCash, acSolo); TransactionHelper t6(QDate(2006, 5, 15), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), this->moT2, acCash, acParent); TransactionHelper t7(QDate(2005, 8, 3), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), this->moT3, acCash, acSolo); TransactionHelper t8(QDate(2006, 9, 15), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), this->moT4, acCash, acParent); a.setHistoryMethod(0); a.setForecastMethod(1); a.createBudget(budget, QDate(2005, 1, 1), QDate(2006, 12, 31), QDate(2007, 1, 1), QDate(2007, 12, 31), true); //test MyMoneyAccount a_solo = file->account(acSolo); MyMoneyAccount a_parent = file->account(acParent); //test it has no variation because it skipped the variation of the opening date QVERIFY(a.forecastBalance(a_solo, QDate(2007, 1, 1)) == ((moT1 + moT3) / MyMoneyMoney(2, 1))); QVERIFY(a.forecastBalance(a_parent, QDate(2007, 1, 1)) == ((moT2 + moT4) / MyMoneyMoney(2, 1))); QVERIFY(a.forecastBalance(a_solo, QDate(2007, 4, 1)) == ((moT1) / MyMoneyMoney(2, 1))); QVERIFY(a.forecastBalance(a_parent, QDate(2007, 5, 1)) == ((moT2) / MyMoneyMoney(2, 1))); QVERIFY(a.forecastBalance(a_solo, QDate(2007, 8, 1)) == ((moT3) / MyMoneyMoney(2, 1))); QVERIFY(a.forecastBalance(a_parent, QDate(2007, 9, 1)) == ((moT4) / MyMoneyMoney(2, 1))); //test the budget object returned by the method QVERIFY(budget.account(a_parent.id()).period(QDate(2007, 9, 1)).amount() == ((moT4) / MyMoneyMoney(2, 1))); //setup test for a length lower than a year b.setForecastMethod(1); b.setHistoryMethod(0); b.createBudget(budget, QDate(2005, 1, 1), QDate(2005, 6, 30), QDate(2007, 1, 1), QDate(2007, 6, 30), true); //test QVERIFY(b.forecastBalance(a_solo, QDate(2007, 1, 1)) == (moT1 + moT3)); QVERIFY(b.forecastBalance(a_parent, QDate(2007, 1, 1)) == (moT2)); QVERIFY(b.forecastBalance(a_solo, QDate(2007, 4, 1)) == (moT1)); QVERIFY(b.forecastBalance(a_parent, QDate(2007, 5, 1)) == (MyMoneyMoney())); //set up schedule environment for testing MyMoneyAccount a_cash = file->account(acCash); MyMoneyFileTransaction ft; MyMoneySchedule sch("A Name", Schedule::Type::Bill, Schedule::Occurrence::Monthly, 1, Schedule::PaymentType::DirectDebit, QDate::currentDate(), QDate(), true, true); MyMoneyTransaction t10; t10.setPostDate(QDate::currentDate().addMonths(1)); t10.setEntryDate(QDate::currentDate().addMonths(1)); //t.setId("T000000000000000001"); t10.setBankID("BID"); t10.setMemo("Wohnung:Miete"); t10.setCommodity("USD"); t10.setValue("key", "value"); MyMoneySplit s; s.setPayeeId("P000001"); s.setShares(moT2); s.setValue(moT2); s.setAccountId(a_parent.id()); s.setBankID("SPID1"); s.setReconcileFlag(eMyMoney::Split::State::Reconciled); t10.addSplit(s); s.setPayeeId("P000001"); s.setShares(-moT2); s.setValue(-moT2); s.setAccountId(a_cash.id()); s.setBankID("SPID2"); s.setReconcileFlag(eMyMoney::Split::State::Cleared); s.clearId(); t10.addSplit(s); sch.setTransaction(t10); file->addSchedule(sch); ft.commit(); //run forecast MyMoneyForecast c; c.setForecastMethod(0); c.setForecastCycles(1); c.createBudget(budget, QDate::currentDate().addYears(-2), QDate::currentDate().addYears(-1), QDate::currentDate().addMonths(-2), QDate::currentDate().addMonths(6), true); MyMoneyMoney c_parent = c.forecastBalance(a_parent, QDate(QDate::currentDate().addMonths(1).year(), QDate::currentDate().addMonths(1).month(), 1)); //test valid results QVERIFY(c.forecastBalance(a_parent, QDate(QDate::currentDate().addMonths(1).year(), QDate::currentDate().addMonths(1).month(), 1)) == (moT2)); } void MyMoneyForecastTest::testLinearRegression() { //set up environment MyMoneyForecast a; MyMoneyAccount a_checking = file->account(acChecking); MyMoneyAccount a_credit = file->account(acCredit); //setup some transactions TransactionHelper t1(QDate::currentDate().addDays(-1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), this->moT1, acChecking, acSolo); TransactionHelper t2(QDate::currentDate().addDays(-1), MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit), -(this->moT2), acCredit, acParent); TransactionHelper t3(QDate::currentDate().addDays(-1), MyMoneySplit::actionName(eMyMoney::Split::Action::Transfer), this->moT1, acCredit, acChecking); //TODO Add tests specific for linear regression } diff --git a/kmymoney/mymoney/tests/mymoneypayee-test.cpp b/kmymoney/mymoney/tests/mymoneypayee-test.cpp index cb858f9ce..23c3358e9 100644 --- a/kmymoney/mymoney/tests/mymoneypayee-test.cpp +++ b/kmymoney/mymoney/tests/mymoneypayee-test.cpp @@ -1,236 +1,237 @@ /* * Copyright 2009-2014 Cristian Oneț * Copyright 2009-2017 Thomas Baumgart * Copyright 2016 Christian Dávid * 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 "mymoneypayee-test.h" #include #include #include #define KMM_MYMONEY_UNIT_TESTABLE friend class MyMoneyPayeeTest; #include "mymoneypayee.h" #include "mymoneypayee_p.h" +#include "mymoneyenums.h" using namespace std; QTEST_GUILESS_MAIN(MyMoneyPayeeTest) void MyMoneyPayeeTest::testXml() { QDomDocument doc; QDomElement parent = doc.createElement("Test"); doc.appendChild(parent); MyMoneyPayee payee1; payee1.d_func()->m_id = "some random id";//if the ID isn't set, w ethrow an exception payee1.writeXML(doc, parent); QString temp1 = "Account1"; payee1.setDefaultAccountId(temp1); payee1.writeXML(doc, parent); QString temp2 = "Account2"; payee1.setDefaultAccountId(temp2); payee1.writeXML(doc, parent); payee1.setDefaultAccountId(); payee1.writeXML(doc, parent); QDomElement el = parent.firstChild().toElement(); QVERIFY(!el.isNull()); MyMoneyPayee payee2(el); QVERIFY(!payee2.defaultAccountEnabled()); QVERIFY(payee2.defaultAccountId().isEmpty()); el = el.nextSibling().toElement(); QVERIFY(!el.isNull()); MyMoneyPayee payee3(el); QVERIFY(payee3.defaultAccountEnabled()); QVERIFY(payee3.defaultAccountId() == temp1); el = el.nextSibling().toElement(); QVERIFY(!el.isNull()); MyMoneyPayee payee4(el); QVERIFY(payee4.defaultAccountEnabled()); QVERIFY(payee4.defaultAccountId() == temp2); el = el.nextSibling().toElement(); QVERIFY(!el.isNull()); MyMoneyPayee payee5(el); QVERIFY(!payee5.defaultAccountEnabled()); QVERIFY(payee5.defaultAccountId().isEmpty()); } void MyMoneyPayeeTest::testDefaultAccount() { MyMoneyPayee payee; QVERIFY(!payee.defaultAccountEnabled()); QVERIFY(payee.defaultAccountId().isEmpty()); QString temp = "Account1"; payee.setDefaultAccountId(temp); QVERIFY(payee.defaultAccountEnabled()); QVERIFY(payee.defaultAccountId() == temp); payee.setDefaultAccountId(); QVERIFY(!payee.defaultAccountEnabled()); QVERIFY(payee.defaultAccountId().isEmpty()); } void MyMoneyPayeeTest::testEmptyMatchKeyBegin() { MyMoneyPayee payee; QString keys; bool ignoreCase; - payee.setMatchData(MyMoneyPayee::matchKey, false, "\ntest1\ntest2"); - QVERIFY(payee.matchData(ignoreCase, keys) == MyMoneyPayee::matchKey); + payee.setMatchData(eMyMoney::Payee::MatchType::Key, false, "\ntest1\ntest2"); + QVERIFY(payee.matchData(ignoreCase, keys) == eMyMoney::Payee::MatchType::Key); QVERIFY(ignoreCase == false); QVERIFY(keys == QLatin1String("test1\ntest2")); - payee.setMatchData(MyMoneyPayee::matchKey, false, "\n\ntest1\ntest2"); - QVERIFY(payee.matchData(ignoreCase, keys) == MyMoneyPayee::matchKey); + payee.setMatchData(eMyMoney::Payee::MatchType::Key, false, "\n\ntest1\ntest2"); + QVERIFY(payee.matchData(ignoreCase, keys) == eMyMoney::Payee::MatchType::Key); QVERIFY(ignoreCase == false); QVERIFY(keys == QLatin1String("test1\ntest2")); } void MyMoneyPayeeTest::testEmptyMatchKeyEnd() { MyMoneyPayee payee; QString keys; bool ignoreCase; - payee.setMatchData(MyMoneyPayee::matchKey, false, "test1\ntest2\n"); - QVERIFY(payee.matchData(ignoreCase, keys) == MyMoneyPayee::matchKey); + payee.setMatchData(eMyMoney::Payee::MatchType::Key, false, "test1\ntest2\n"); + QVERIFY(payee.matchData(ignoreCase, keys) == eMyMoney::Payee::MatchType::Key); QVERIFY(ignoreCase == false); QVERIFY(keys == QLatin1String("test1\ntest2")); - payee.setMatchData(MyMoneyPayee::matchKey, false, "test1\ntest2\n\n"); - QVERIFY(payee.matchData(ignoreCase, keys) == MyMoneyPayee::matchKey); + payee.setMatchData(eMyMoney::Payee::MatchType::Key, false, "test1\ntest2\n\n"); + QVERIFY(payee.matchData(ignoreCase, keys) == eMyMoney::Payee::MatchType::Key); QVERIFY(ignoreCase == false); QVERIFY(keys == QLatin1String("test1\ntest2")); } void MyMoneyPayeeTest::testEmptyMatchKeyMiddle() { MyMoneyPayee payee; QString keys; bool ignoreCase; - payee.setMatchData(MyMoneyPayee::matchKey, false, "test1\n\ntest2"); - QVERIFY(payee.matchData(ignoreCase, keys) == MyMoneyPayee::matchKey); + payee.setMatchData(eMyMoney::Payee::MatchType::Key, false, "test1\n\ntest2"); + QVERIFY(payee.matchData(ignoreCase, keys) == eMyMoney::Payee::MatchType::Key); QVERIFY(ignoreCase == false); QVERIFY(keys == QLatin1String("test1\ntest2")); - payee.setMatchData(MyMoneyPayee::matchKey, false, "test1\n\n\ntest2"); - QVERIFY(payee.matchData(ignoreCase, keys) == MyMoneyPayee::matchKey); + payee.setMatchData(eMyMoney::Payee::MatchType::Key, false, "test1\n\n\ntest2"); + QVERIFY(payee.matchData(ignoreCase, keys) == eMyMoney::Payee::MatchType::Key); QVERIFY(ignoreCase == false); QVERIFY(keys == QLatin1String("test1\ntest2")); } void MyMoneyPayeeTest::testEmptyMatchKeyMix() { MyMoneyPayee payee; QString keys; bool ignoreCase; - payee.setMatchData(MyMoneyPayee::matchKey, false, "\ntest1\n\ntest2\n"); - QVERIFY(payee.matchData(ignoreCase, keys) == MyMoneyPayee::matchKey); + payee.setMatchData(eMyMoney::Payee::MatchType::Key, false, "\ntest1\n\ntest2\n"); + QVERIFY(payee.matchData(ignoreCase, keys) == eMyMoney::Payee::MatchType::Key); QVERIFY(ignoreCase == false); QVERIFY(keys == QLatin1String("test1\ntest2")); - payee.setMatchData(MyMoneyPayee::matchKey, false, "\n\ntest1\n\n\ntest2\n\n"); - QVERIFY(payee.matchData(ignoreCase, keys) == MyMoneyPayee::matchKey); + payee.setMatchData(eMyMoney::Payee::MatchType::Key, false, "\n\ntest1\n\n\ntest2\n\n"); + QVERIFY(payee.matchData(ignoreCase, keys) == eMyMoney::Payee::MatchType::Key); QVERIFY(ignoreCase == false); QVERIFY(keys == QLatin1String("test1\ntest2")); } void MyMoneyPayeeTest::testMatchKeyDisallowSingleSpace() { MyMoneyPayee payee; QString keys; bool ignoreCase; - payee.setMatchData(MyMoneyPayee::matchKey, false, " "); - QVERIFY(payee.matchData(ignoreCase, keys) == MyMoneyPayee::matchKey); + payee.setMatchData(eMyMoney::Payee::MatchType::Key, false, " "); + QVERIFY(payee.matchData(ignoreCase, keys) == eMyMoney::Payee::MatchType::Key); QVERIFY(ignoreCase == false); QVERIFY(keys == QLatin1String("")); } void MyMoneyPayeeTest::testMatchKeyDisallowMultipleSpace() { MyMoneyPayee payee; QString keys; bool ignoreCase; - payee.setMatchData(MyMoneyPayee::matchKey, false, " "); - QVERIFY(payee.matchData(ignoreCase, keys) == MyMoneyPayee::matchKey); + payee.setMatchData(eMyMoney::Payee::MatchType::Key, false, " "); + QVERIFY(payee.matchData(ignoreCase, keys) == eMyMoney::Payee::MatchType::Key); QVERIFY(ignoreCase == false); QVERIFY(keys == QLatin1String("")); } void MyMoneyPayeeTest::testMatchKeyAllowSpaceAtStart() { MyMoneyPayee payee; QString keys; bool ignoreCase; - payee.setMatchData(MyMoneyPayee::matchKey, false, " payee"); - QVERIFY(payee.matchData(ignoreCase, keys) == MyMoneyPayee::matchKey); + payee.setMatchData(eMyMoney::Payee::MatchType::Key, false, " payee"); + QVERIFY(payee.matchData(ignoreCase, keys) == eMyMoney::Payee::MatchType::Key); QVERIFY(ignoreCase == false); QVERIFY(keys == QLatin1String(" payee")); } void MyMoneyPayeeTest::testMatchKeyAllowSpaceAtEnd() { MyMoneyPayee payee; QString keys; bool ignoreCase; - payee.setMatchData(MyMoneyPayee::matchKey, false, "payee "); - QVERIFY(payee.matchData(ignoreCase, keys) == MyMoneyPayee::matchKey); + payee.setMatchData(eMyMoney::Payee::MatchType::Key, false, "payee "); + QVERIFY(payee.matchData(ignoreCase, keys) == eMyMoney::Payee::MatchType::Key); QVERIFY(ignoreCase == false); QVERIFY(keys == QLatin1String("payee ")); } void MyMoneyPayeeTest::testMatchNameExact() { MyMoneyPayee payee; QString keys; bool ignoreCase; - payee.setMatchData(MyMoneyPayee::matchNameExact, false, keys); + payee.setMatchData(eMyMoney::Payee::MatchType::NameExact, false, keys); keys = QLatin1String("payee "); - QCOMPARE(payee.matchData(ignoreCase, keys), MyMoneyPayee::matchNameExact); + QCOMPARE(payee.matchData(ignoreCase, keys), eMyMoney::Payee::MatchType::NameExact); QCOMPARE(ignoreCase, false); QVERIFY(keys.isEmpty()); } void MyMoneyPayeeTest::testElementNames() { for (auto i = (int)Payee::Element::Address; i <= (int)Payee::Element::Address; ++i) { auto isEmpty = MyMoneyPayeePrivate::getElName(static_cast(i)).isEmpty(); if (isEmpty) qWarning() << "Empty element's name " << i; QVERIFY(!isEmpty); } } void MyMoneyPayeeTest::testAttributeNames() { for (auto i = (int)Payee::Attribute::Name; i < (int)Payee::Attribute::LastAttribute; ++i) { auto isEmpty = MyMoneyPayeePrivate::getAttrName(static_cast(i)).isEmpty(); if (isEmpty) qWarning() << "Empty attribute's name " << i; QVERIFY(!isEmpty); } } diff --git a/kmymoney/plugins/sql/mymoneystoragesql.cpp b/kmymoney/plugins/sql/mymoneystoragesql.cpp index 2d1ca57fe..f5c2ec704 100644 --- a/kmymoney/plugins/sql/mymoneystoragesql.cpp +++ b/kmymoney/plugins/sql/mymoneystoragesql.cpp @@ -1,2856 +1,2856 @@ /*************************************************************************** mymoneystoragesql.cpp --------------------- begin : 11 November 2005 copyright : (C) 2005 by Tony Bloomfield email : tonybloom@users.sourceforge.net : Fernando Vilas : Christian Dávid (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 "mymoneystoragesql_p.h" // ---------------------------------------------------------------------------- // System Includes // ---------------------------------------------------------------------------- // QT Includes // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes //************************ Constructor/Destructor ***************************** MyMoneyStorageSql::MyMoneyStorageSql(MyMoneyStorageMgr *storage, const QUrl &url) : QSqlDatabase(QUrlQuery(url).queryItemValue("driver")), d_ptr(new MyMoneyStorageSqlPrivate(this)) { Q_D(MyMoneyStorageSql); d->m_storage = storage; } MyMoneyStorageSql::~MyMoneyStorageSql() { try { close(true); } catch (const MyMoneyException &e) { qDebug() << "Caught Exception in MMStorageSql dtor: " << e.what(); } Q_D(MyMoneyStorageSql); delete d; } uint MyMoneyStorageSql::currentVersion() const { Q_D(const MyMoneyStorageSql); return (d->m_db.currentVersion()); } int MyMoneyStorageSql::open(const QUrl &url, int openMode, bool clear) { Q_D(MyMoneyStorageSql); try { int rc = 0; d->m_driver = MyMoneyDbDriver::create(QUrlQuery(url).queryItemValue("driver")); //get the input options QStringList options = QUrlQuery(url).queryItemValue("options").split(','); d->m_loadAll = true; // force loading whole database into memory since unification of storages // options.contains("loadAll")/*|| m_mode == 0*/; d->m_override = options.contains("override"); // create the database connection // regarding the construction of the database name see the discussion on // https://phabricator.kde.org/D12681. In case of a local file based DB // driver we cut off the leading slash only in those cases, where we // a) have a file based DB on Windows systems and // b) have a server based DB. // so that we do not modify the absolute path on *nix based systems // in case of a DB based driver QString dbName = url.path(); if(d->m_driver->requiresExternalFile()) { #ifdef Q_OS_WIN dbName = url.path().remove(0, 1); // remove separator slash for files on Windows #endif } else { dbName = url.path().remove(0, 1); // remove separator slash for server based databases } setDatabaseName(dbName); setHostName(url.host()); setUserName(url.userName()); setPassword(url.password()); if (QUrlQuery(url).queryItemValue("driver").contains("QMYSQL")) { setConnectOptions("MYSQL_OPT_RECONNECT=1"); } switch (openMode) { case QIODevice::ReadOnly: // OpenDatabase menu entry (or open last file) case QIODevice::ReadWrite: // Save menu entry with database open // this may be a sqlite file opened from the recently used list // but which no longer exists. In that case, open will work but create an empty file. // This is not what the user's after; he may accuse KMM of deleting all his data! if (d->m_driver->requiresExternalFile()) { if (!d->fileExists(dbName)) { rc = 1; break; } } if (!QSqlDatabase::open()) { d->buildError(QSqlQuery(*this), Q_FUNC_INFO, "opening database"); rc = 1; } else { rc = d->createTables(); // check all tables are present, create if not } break; case QIODevice::WriteOnly: // SaveAs Database - if exists, must be empty, if not will create // Try to open the database. // If that fails, try to create the database, then try to open it again. d->m_newDatabase = true; if (!QSqlDatabase::open()) { if (!d->createDatabase(url)) { rc = 1; } else { if (!QSqlDatabase::open()) { d->buildError(QSqlQuery(*this), Q_FUNC_INFO, "opening new database"); rc = 1; } else { rc = d->createTables(); } } } else { rc = d->createTables(); if (rc == 0) { if (clear) { d->clean(); } else { rc = d->isEmpty(); } } } break; default: qWarning("%s", qPrintable(QString("%1 - unknown open mode %2").arg(Q_FUNC_INFO).arg(openMode))); } if (rc != 0) return (rc); // bypass logon check if we are creating a database if (d->m_newDatabase) return(0); // check if the database is locked, if not lock it d->readFileInfo(); if (!d->m_logonUser.isEmpty() && (!d->m_override)) { d->m_error = i18n("Database apparently in use\nOpened by %1 on %2 at %3.\nOpen anyway?", d->m_logonUser, d->m_logonAt.date().toString(Qt::ISODate), d->m_logonAt.time().toString("hh.mm.ss")); qDebug("%s", qPrintable(d->m_error)); close(false); rc = -1; // retryable error } else { d->m_logonUser = url.userName() + '@' + url.host(); d->m_logonAt = QDateTime::currentDateTime(); d->writeFileInfo(); } return(rc); } catch (const QString& s) { qDebug("%s", qPrintable(s)); return (1); } } void MyMoneyStorageSql::close(bool logoff) { Q_D(MyMoneyStorageSql); if (QSqlDatabase::isOpen()) { if (logoff) { MyMoneyDbTransaction t(*this, Q_FUNC_INFO); d->m_logonUser.clear(); d->writeFileInfo(); } QSqlDatabase::close(); QSqlDatabase::removeDatabase(connectionName()); } } ulong MyMoneyStorageSql::getRecCount(const QString& table) const { Q_D(const MyMoneyStorageSql); QSqlQuery q(*const_cast (this)); q.prepare(QString("SELECT COUNT(*) FROM %1;").arg(table)); if ((!q.exec()) || (!q.next())) { // krazy:exclude=crashy d->buildError(q, Q_FUNC_INFO, "error retrieving record count"); qFatal("Error retrieving record count"); // definitely shouldn't happen } return ((ulong) q.value(0).toULongLong()); } ////////////////////////////////////////////////////////////////// bool MyMoneyStorageSql::readFile() { Q_D(MyMoneyStorageSql); d->m_displayStatus = true; try { d->readFileInfo(); d->readInstitutions(); if (d->m_loadAll) { readPayees(); } else { QList user; user.append(QString("USER")); readPayees(user); } readTags(); d->readCurrencies(); d->readSecurities(); d->readAccounts(); if (d->m_loadAll) { d->readTransactions(); } else { if (d->m_preferred.filterSet().singleFilter.accountFilter) readTransactions(d->m_preferred); } d->readSchedules(); d->readPrices(); d->readReports(); d->readBudgets(); d->readOnlineJobs(); //FIXME - ?? if (m_mode == 0) //m_storage->rebuildAccountBalances(); // this seems to be nonsense, but it clears the dirty flag // as a side-effect. d->m_storage->setLastModificationDate(d->m_storage->lastModificationDate()); // FIXME?? if (m_mode == 0) m_storage = NULL; // make sure the progress bar is not shown any longer d->signalProgress(-1, -1); d->m_displayStatus = false; //MyMoneySqlQuery::traceOn(); return true; } catch (const QString &) { return false; } // this seems to be nonsense, but it clears the dirty flag // as a side-effect. } // The following is called from 'SaveAsDatabase' bool MyMoneyStorageSql::writeFile() { Q_D(MyMoneyStorageSql); // initialize record counts and hi ids d->m_institutions = d->m_accounts = d->m_payees = d->m_tags = d->m_transactions = d->m_splits = d->m_securities = d->m_prices = d->m_currencies = d->m_schedules = d->m_reports = d->m_kvps = d->m_budgets = 0; d->m_hiIdInstitutions = d->m_hiIdPayees = d->m_hiIdTags = d->m_hiIdAccounts = d->m_hiIdTransactions = d->m_hiIdSchedules = d->m_hiIdSecurities = d->m_hiIdReports = d->m_hiIdBudgets = 0; d->m_onlineJobs = d->m_payeeIdentifier = 0; d->m_displayStatus = true; try { if (this->driverName().compare(QLatin1String("QSQLITE")) == 0) { QSqlQuery query(*this); query.exec("PRAGMA foreign_keys = ON"); // this is needed for "ON UPDATE" and "ON DELETE" to work } MyMoneyDbTransaction t(*this, Q_FUNC_INFO); d->writeInstitutions(); d->writePayees(); d->writeTags(); d->writeAccounts(); d->writeTransactions(); d->writeSchedules(); d->writeSecurities(); d->writePrices(); d->writeCurrencies(); d->writeReports(); d->writeBudgets(); d->writeOnlineJobs(); d->writeFileInfo(); // this seems to be nonsense, but it clears the dirty flag // as a side-effect. //m_storage->setLastModificationDate(m_storage->lastModificationDate()); // FIXME?? if (m_mode == 0) m_storage = NULL; // make sure the progress bar is not shown any longer d->signalProgress(-1, -1); d->m_displayStatus = false; // this seems to be nonsense, but it clears the dirty flag // as a side-effect. d->m_storage->setLastModificationDate(d->m_storage->lastModificationDate()); return true; } catch (const QString &) { return false; } } QString MyMoneyStorageSql::lastError() const { Q_D(const MyMoneyStorageSql); return d->m_error; } // --------------- SQL Transaction (commit unit) handling ----------------------------------- void MyMoneyStorageSql::startCommitUnit(const QString& callingFunction) { Q_D(MyMoneyStorageSql); if (d->m_commitUnitStack.isEmpty()) { if (!transaction()) throw MYMONEYEXCEPTION(d->buildError(QSqlQuery(), callingFunction, "starting commit unit")); } d->m_commitUnitStack.push(callingFunction); } bool MyMoneyStorageSql::endCommitUnit(const QString& callingFunction) { Q_D(MyMoneyStorageSql); // for now, we don't know if there were any changes made to the data so // we expect the data to have changed. This assumption causes some unnecessary // repaints of the UI here and there, but for now it's ok. If we can determine // that the commit() really changes the data, we can return that information // as value of this method. bool rc = true; if (d->m_commitUnitStack.isEmpty()) { throw MYMONEYEXCEPTION_CSTRING("Empty commit unit stack while trying to commit"); } if (callingFunction != d->m_commitUnitStack.top()) qDebug("%s", qPrintable(QString("%1 - %2 s/be %3").arg(Q_FUNC_INFO).arg(callingFunction).arg(d->m_commitUnitStack.top()))); d->m_commitUnitStack.pop(); if (d->m_commitUnitStack.isEmpty()) { //qDebug() << "Committing with " << QSqlQuery::refCount() << " queries"; if (!commit()) throw MYMONEYEXCEPTION(d->buildError(QSqlQuery(), callingFunction, "ending commit unit")); } return rc; } void MyMoneyStorageSql::cancelCommitUnit(const QString& callingFunction) { Q_D(MyMoneyStorageSql); if (d->m_commitUnitStack.isEmpty()) return; if (callingFunction != d->m_commitUnitStack.top()) qDebug("%s", qPrintable(QString("%1 - %2 s/be %3").arg(Q_FUNC_INFO).arg(callingFunction).arg(d->m_commitUnitStack.top()))); d->m_commitUnitStack.clear(); if (!rollback()) throw MYMONEYEXCEPTION(d->buildError(QSqlQuery(), callingFunction, "cancelling commit unit") + ' ' + callingFunction); } ///////////////////////////////////////////////////////////////////// void MyMoneyStorageSql::fillStorage() { Q_D(MyMoneyStorageSql); // if (!m_transactionListRead) // make sure we have loaded everything d->readTransactions(); // if (!m_payeeListRead) readPayees(); } //------------------------------ Write SQL routines ---------------------------------------- // **** Institutions **** void MyMoneyStorageSql::addInstitution(const MyMoneyInstitution& inst) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QSqlQuery q(*this); q.prepare(d->m_db.m_tables["kmmInstitutions"].insertString()); QList iList; iList << inst; d->writeInstitutionList(iList , q); ++d->m_institutions; d->writeFileInfo(); } void MyMoneyStorageSql::modifyInstitution(const MyMoneyInstitution& inst) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QSqlQuery q(*this); q.prepare(d->m_db.m_tables["kmmInstitutions"].updateString()); QVariantList kvpList; kvpList << inst.id(); d->deleteKeyValuePairs("OFXSETTINGS", kvpList); QList iList; iList << inst; d->writeInstitutionList(iList , q); d->writeFileInfo(); } void MyMoneyStorageSql::removeInstitution(const MyMoneyInstitution& inst) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QVariantList kvpList; kvpList << inst.id(); d->deleteKeyValuePairs("OFXSETTINGS", kvpList); QSqlQuery query(*this); query.prepare(d->m_db.m_tables["kmmInstitutions"].deleteString()); query.bindValue(":id", inst.id()); if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("deleting Institution")); // krazy:exclude=crashy --d->m_institutions; d->writeFileInfo(); } void MyMoneyStorageSql::addPayee(const MyMoneyPayee& payee) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QSqlQuery query(*this); query.prepare(d->m_db.m_tables["kmmPayees"].insertString()); d->writePayee(payee, query); ++d->m_payees; QVariantList identIds; QList idents = payee.payeeIdentifiers(); // Store ids which have to be stored in the map table identIds.reserve(idents.count()); foreach (payeeIdentifier ident, idents) { try { // note: this changes ident addPayeeIdentifier(ident); identIds.append(ident.idString()); } catch (const payeeIdentifier::empty &) { } } if (!identIds.isEmpty()) { // Create lists for batch processing QVariantList order; QVariantList payeeIdList; order.reserve(identIds.size()); payeeIdList.reserve(identIds.size()); for (int i = 0; i < identIds.size(); ++i) { order << i; payeeIdList << payee.id(); } query.prepare("INSERT INTO kmmPayeesPayeeIdentifier (payeeId, identifierId, userOrder) VALUES(?, ?, ?)"); query.bindValue(0, payeeIdList); query.bindValue(1, identIds); query.bindValue(2, order); if (!query.execBatch()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("writing payee's identifiers")); // krazy:exclude=crashy } d->writeFileInfo(); } void MyMoneyStorageSql::modifyPayee(MyMoneyPayee payee) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QSqlQuery query(*this); query.prepare(d->m_db.m_tables["kmmPayees"].updateString()); d->writePayee(payee, query); // Get a list of old identifiers first query.prepare("SELECT identifierId FROM kmmPayeesPayeeIdentifier WHERE payeeId = ?"); query.bindValue(0, payee.id()); if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("modifying payee's identifiers (getting old values failed)")); // krazy:exclude=crashy QStringList oldIdentIds; oldIdentIds.reserve(query.numRowsAffected()); while (query.next()) oldIdentIds << query.value(0).toString(); // Add new and modify old payeeIdentifiers foreach (payeeIdentifier ident, payee.payeeIdentifiers()) { if (ident.idString().isEmpty()) { payeeIdentifier oldIdent(ident); addPayeeIdentifier(ident); // addPayeeIdentifier could fail (throws an exception then) only remove old // identifier if new one is stored correctly payee.removePayeeIdentifier(oldIdent); payee.addPayeeIdentifier(ident); } else { modifyPayeeIdentifier(ident); payee.modifyPayeeIdentifier(ident); oldIdentIds.removeAll(ident.idString()); } } // Remove identifiers which are not used anymore foreach (QString idToRemove, oldIdentIds) { payeeIdentifier ident(fetchPayeeIdentifier(idToRemove)); removePayeeIdentifier(ident); } // Update relation table query.prepare("DELETE FROM kmmPayeesPayeeIdentifier WHERE payeeId = ?"); query.bindValue(0, payee.id()); if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("modifying payee's identifiers (delete from mapping table)")); // krazy:exclude=crashy // Get list again because modifiyPayeeIdentifier which is used above may change the id QList idents(payee.payeeIdentifiers()); QVariantList order; QVariantList payeeIdList; QVariantList identIdList; order.reserve(idents.size()); payeeIdList.reserve(idents.size()); identIdList.reserve(idents.size()); { QList::const_iterator end = idents.constEnd(); int i = 0; for (QList::const_iterator iter = idents.constBegin(); iter != end; ++iter, ++i) { order << i; payeeIdList << payee.id(); identIdList << iter->idString(); } } query.prepare("INSERT INTO kmmPayeesPayeeIdentifier (payeeId, userOrder, identifierId) VALUES(?, ?, ?)"); query.bindValue(0, payeeIdList); query.bindValue(1, order); query.bindValue(2, identIdList); if (!query.execBatch()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("writing payee's identifiers during modify")); // krazy:exclude=crashy d->writeFileInfo(); } void MyMoneyStorageSql::modifyUserInfo(const MyMoneyPayee& payee) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QSqlQuery q(*this); q.prepare(d->m_db.m_tables["kmmPayees"].updateString()); d->writePayee(payee, q, true); d->writeFileInfo(); } void MyMoneyStorageSql::removePayee(const MyMoneyPayee& payee) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QSqlQuery query(*this); // Get identifiers first so we know which to delete query.prepare("SELECT identifierId FROM kmmPayeesPayeeIdentifier WHERE payeeId = ?"); query.bindValue(0, payee.id()); if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("removing payee's identifiers (getting old values failed)")); // krazy:exclude=crashy QStringList identIds; while (query.next()) identIds << query.value(0).toString(); QMap idents = fetchPayeeIdentifiers(identIds); foreach (payeeIdentifier ident, idents) { removePayeeIdentifier(ident); } // Delete entries from mapping table query.prepare("DELETE FROM kmmPayeesPayeeIdentifier WHERE payeeId = ?"); query.bindValue(0, payee.id()); if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("removing payee's identifiers (delete from mapping table)")); // krazy:exclude=crashy // Delete payee query.prepare(d->m_db.m_tables["kmmPayees"].deleteString()); query.bindValue(":id", payee.id()); if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("deleting Payee")); // krazy:exclude=crashy --d->m_payees; d->writeFileInfo(); } // **** Tags **** void MyMoneyStorageSql::addTag(const MyMoneyTag& tag) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QSqlQuery q(*this); q.prepare(d->m_db.m_tables["kmmTags"].insertString()); d->writeTag(tag, q); ++d->m_tags; d->writeFileInfo(); } void MyMoneyStorageSql::modifyTag(const MyMoneyTag& tag) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QSqlQuery q(*this); q.prepare(d->m_db.m_tables["kmmTags"].updateString()); d->writeTag(tag, q); d->writeFileInfo(); } void MyMoneyStorageSql::removeTag(const MyMoneyTag& tag) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QSqlQuery query(*this); query.prepare(d->m_db.m_tables["kmmTags"].deleteString()); query.bindValue(":id", tag.id()); if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("deleting Tag")); // krazy:exclude=crashy --d->m_tags; d->writeFileInfo(); } // **** Accounts **** void MyMoneyStorageSql::addAccount(const MyMoneyAccount& acc) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QSqlQuery q(*this); q.prepare(d->m_db.m_tables["kmmAccounts"].insertString()); QList aList; aList << acc; d->writeAccountList(aList, q); ++d->m_accounts; d->writeFileInfo(); } void MyMoneyStorageSql::modifyAccount(const MyMoneyAccount& acc) { QList aList; aList << acc; modifyAccountList(aList); } void MyMoneyStorageSql::modifyAccountList(const QList& acc) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QSqlQuery q(*this); q.prepare(d->m_db.m_tables["kmmAccounts"].updateString()); QVariantList kvpList; foreach (const MyMoneyAccount& a, acc) { kvpList << a.id(); } d->deleteKeyValuePairs("ACCOUNT", kvpList); d->deleteKeyValuePairs("ONLINEBANKING", kvpList); d->writeAccountList(acc, q); d->writeFileInfo(); } void MyMoneyStorageSql::removeAccount(const MyMoneyAccount& acc) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QVariantList kvpList; kvpList << acc.id(); d->deleteKeyValuePairs("ACCOUNT", kvpList); d->deleteKeyValuePairs("ONLINEBANKING", kvpList); QSqlQuery query(*this); query.prepare(d->m_db.m_tables["kmmAccounts"].deleteString()); query.bindValue(":id", acc.id()); if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("deleting Account")); // krazy:exclude=crashy --d->m_accounts; d->writeFileInfo(); } // **** Transactions and Splits **** void MyMoneyStorageSql::addTransaction(const MyMoneyTransaction& tx) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); // add the transaction and splits QSqlQuery q(*this); q.prepare(d->m_db.m_tables["kmmTransactions"].insertString()); d->writeTransaction(tx.id(), tx, q, "N"); ++d->m_transactions; QList aList; // for each split account, update lastMod date, balance, txCount foreach (const MyMoneySplit& it_s, tx.splits()) { MyMoneyAccount acc = d->m_storage->account(it_s.accountId()); ++d->m_transactionCountMap[acc.id()]; aList << acc; } modifyAccountList(aList); // in the fileinfo record, update lastMod, txCount, next TxId d->writeFileInfo(); } void MyMoneyStorageSql::modifyTransaction(const MyMoneyTransaction& tx) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); // remove the splits of the old tx from the count table QSqlQuery query(*this); query.prepare("SELECT accountId FROM kmmSplits WHERE transactionId = :txId;"); query.bindValue(":txId", tx.id()); if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("retrieving old splits")); while (query.next()) { QString id = query.value(0).toString(); --d->m_transactionCountMap[id]; } // add the transaction and splits query.prepare(d->m_db.m_tables["kmmTransactions"].updateString()); d->writeTransaction(tx.id(), tx, query, "N"); QList aList; // for each split account, update lastMod date, balance, txCount foreach (const MyMoneySplit& it_s, tx.splits()) { MyMoneyAccount acc = d->m_storage->account(it_s.accountId()); ++d->m_transactionCountMap[acc.id()]; aList << acc; } modifyAccountList(aList); //writeSplits(tx.id(), "N", tx.splits()); // in the fileinfo record, update lastMod d->writeFileInfo(); } void MyMoneyStorageSql::removeTransaction(const MyMoneyTransaction& tx) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); d->deleteTransaction(tx.id()); --d->m_transactions; QList aList; // for each split account, update lastMod date, balance, txCount foreach (const MyMoneySplit& it_s, tx.splits()) { MyMoneyAccount acc = d->m_storage->account(it_s.accountId()); --d->m_transactionCountMap[acc.id()]; aList << acc; } modifyAccountList(aList); // in the fileinfo record, update lastModDate, txCount d->writeFileInfo(); } // **** Schedules **** void MyMoneyStorageSql::addSchedule(const MyMoneySchedule& sched) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QSqlQuery q(*this); q.prepare(d->m_db.m_tables["kmmSchedules"].insertString()); d->writeSchedule(sched, q, true); ++d->m_schedules; d->writeFileInfo(); } void MyMoneyStorageSql::modifySchedule(const MyMoneySchedule& sched) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QSqlQuery q(*this); q.prepare(d->m_db.m_tables["kmmSchedules"].updateString()); d->writeSchedule(sched, q, false); d->writeFileInfo(); } void MyMoneyStorageSql::removeSchedule(const MyMoneySchedule& sched) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); d->deleteSchedule(sched.id()); --d->m_schedules; d->writeFileInfo(); } // **** Securities **** void MyMoneyStorageSql::addSecurity(const MyMoneySecurity& sec) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QSqlQuery q(*this); q.prepare(d->m_db.m_tables["kmmSecurities"].insertString()); d->writeSecurity(sec, q); ++d->m_securities; d->writeFileInfo(); } void MyMoneyStorageSql::modifySecurity(const MyMoneySecurity& sec) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QVariantList kvpList; kvpList << sec.id(); d->deleteKeyValuePairs("SECURITY", kvpList); QSqlQuery q(*this); q.prepare(d->m_db.m_tables["kmmSecurities"].updateString()); d->writeSecurity(sec, q); d->writeFileInfo(); } void MyMoneyStorageSql::removeSecurity(const MyMoneySecurity& sec) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QVariantList kvpList; kvpList << sec.id(); d->deleteKeyValuePairs("SECURITY", kvpList); QSqlQuery query(*this); query.prepare(d->m_db.m_tables["kmmSecurities"].deleteString()); query.bindValue(":id", kvpList); if (!query.execBatch()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("deleting Security")); --d->m_securities; d->writeFileInfo(); } // **** Prices **** void MyMoneyStorageSql::addPrice(const MyMoneyPrice& p) { Q_D(MyMoneyStorageSql); if (d->m_readingPrices) return; // the app always calls addPrice, whether or not there is already one there MyMoneyDbTransaction t(*this, Q_FUNC_INFO); bool newRecord = false; QSqlQuery query(*this); QString s = d->m_db.m_tables["kmmPrices"].selectAllString(false); s += " WHERE fromId = :fromId AND toId = :toId AND priceDate = :priceDate;"; query.prepare(s); query.bindValue(":fromId", p.from()); query.bindValue(":toId", p.to()); query.bindValue(":priceDate", p.date().toString(Qt::ISODate)); if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("finding Price")); // krazy:exclude=crashy if (query.next()) { query.prepare(d->m_db.m_tables["kmmPrices"].updateString()); } else { query.prepare(d->m_db.m_tables["kmmPrices"].insertString()); ++d->m_prices; newRecord = true; } query.bindValue(":fromId", p.from()); query.bindValue(":toId", p.to()); query.bindValue(":priceDate", p.date().toString(Qt::ISODate)); query.bindValue(":price", p.rate(QString()).toString()); const MyMoneySecurity sec = d->m_storage->security(p.to()); query.bindValue(":priceFormatted", p.rate(QString()).formatMoney("", sec.pricePrecision())); query.bindValue(":priceSource", p.source()); if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("writing Price")); // krazy:exclude=crashy if (newRecord) d->writeFileInfo(); } void MyMoneyStorageSql::removePrice(const MyMoneyPrice& p) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QSqlQuery query(*this); query.prepare(d->m_db.m_tables["kmmPrices"].deleteString()); query.bindValue(":fromId", p.from()); query.bindValue(":toId", p.to()); query.bindValue(":priceDate", p.date().toString(Qt::ISODate)); if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("deleting Price")); // krazy:exclude=crashy --d->m_prices; d->writeFileInfo(); } // **** Currencies **** void MyMoneyStorageSql::addCurrency(const MyMoneySecurity& sec) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QSqlQuery q(*this); q.prepare(d->m_db.m_tables["kmmCurrencies"].insertString()); d->writeCurrency(sec, q); ++d->m_currencies; d->writeFileInfo(); } void MyMoneyStorageSql::modifyCurrency(const MyMoneySecurity& sec) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QSqlQuery q(*this); q.prepare(d->m_db.m_tables["kmmCurrencies"].updateString()); d->writeCurrency(sec, q); d->writeFileInfo(); } void MyMoneyStorageSql::removeCurrency(const MyMoneySecurity& sec) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QSqlQuery query(*this); query.prepare(d->m_db.m_tables["kmmCurrencies"].deleteString()); query.bindValue(":ISOcode", sec.id()); if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("deleting Currency")); // krazy:exclude=crashy --d->m_currencies; d->writeFileInfo(); } void MyMoneyStorageSql::addReport(const MyMoneyReport& rep) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QSqlQuery q(*this); q.prepare(d->m_db.m_tables["kmmReportConfig"].insertString()); d->writeReport(rep, q); ++d->m_reports; d->writeFileInfo(); } void MyMoneyStorageSql::modifyReport(const MyMoneyReport& rep) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QSqlQuery q(*this); q.prepare(d->m_db.m_tables["kmmReportConfig"].updateString()); d->writeReport(rep, q); d->writeFileInfo(); } void MyMoneyStorageSql::removeReport(const MyMoneyReport& rep) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QSqlQuery query(*this); query.prepare("DELETE FROM kmmReportConfig WHERE id = :id"); query.bindValue(":id", rep.id()); if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("deleting Report")); // krazy:exclude=crashy --d->m_reports; d->writeFileInfo(); } void MyMoneyStorageSql::addBudget(const MyMoneyBudget& bud) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QSqlQuery q(*this); q.prepare(d->m_db.m_tables["kmmBudgetConfig"].insertString()); d->writeBudget(bud, q); ++d->m_budgets; d->writeFileInfo(); } void MyMoneyStorageSql::modifyBudget(const MyMoneyBudget& bud) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QSqlQuery q(*this); q.prepare(d->m_db.m_tables["kmmBudgetConfig"].updateString()); d->writeBudget(bud, q); d->writeFileInfo(); } void MyMoneyStorageSql::removeBudget(const MyMoneyBudget& bud) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QSqlQuery query(*this); query.prepare(d->m_db.m_tables["kmmBudgetConfig"].deleteString()); query.bindValue(":id", bud.id()); if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("deleting Budget")); // krazy:exclude=crashy --d->m_budgets; d->writeFileInfo(); } void MyMoneyStorageSql::addOnlineJob(const onlineJob& job) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QSqlQuery query(*this); query.prepare("INSERT INTO kmmOnlineJobs (id, type, jobSend, bankAnswerDate, state, locked) VALUES(:id, :type, :jobSend, :bankAnswerDate, :state, :locked);"); d->writeOnlineJob(job, query); if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("writing onlineJob")); // krazy:exclude=crashy ++d->m_onlineJobs; try { // Save online task d->actOnOnlineJobInSQL(MyMoneyStorageSqlPrivate::SQLAction::Save, *job.constTask(), job.id()); } catch (onlineJob::emptyTask&) { } } void MyMoneyStorageSql::modifyOnlineJob(const onlineJob& job) { Q_D(MyMoneyStorageSql); Q_ASSERT(!job.id().isEmpty()); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QSqlQuery query(*this); query.prepare(QLatin1String( "UPDATE kmmOnlineJobs SET " " type = :type, " " jobSend = :jobSend, " " bankAnswerDate = :bankAnswerDate, " " state = :state, " " locked = :locked " " WHERE id = :id" )); d->writeOnlineJob(job, query); if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("writing onlineJob")); // krazy:exclude=crashy try { // Modify online task d->actOnOnlineJobInSQL(MyMoneyStorageSqlPrivate::SQLAction::Modify, *job.constTask(), job.id()); } catch (onlineJob::emptyTask&) { // If there is no task attached this is fine as well } } void MyMoneyStorageSql::removeOnlineJob(const onlineJob& job) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); // Remove onlineTask first, because it could have a contraint // which could block the removal of the onlineJob try { // Remove task d->actOnOnlineJobInSQL(MyMoneyStorageSqlPrivate::SQLAction::Remove, *job.constTask(), job.id()); } catch (onlineJob::emptyTask&) { } QSqlQuery query(*this); query.prepare(d->m_db.m_tables["kmmOnlineJobs"].deleteString()); query.bindValue(":id", job.id()); if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("deleting onlineJob")); // krazy:exclude=crashy --d->m_onlineJobs; } void MyMoneyStorageSql::addPayeeIdentifier(payeeIdentifier& ident) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); ident = payeeIdentifier(incrementPayeeIdentfierId(), ident); QSqlQuery q(*this); q.prepare("INSERT INTO kmmPayeeIdentifier (id, type) VALUES(:id, :type)"); d->writePayeeIdentifier(ident, q); ++d->m_payeeIdentifier; try { d->actOnPayeeIdentifierObjectInSQL(MyMoneyStorageSqlPrivate::SQLAction::Save, ident); } catch (const payeeIdentifier::empty &) { } } void MyMoneyStorageSql::modifyPayeeIdentifier(const payeeIdentifier& ident) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QSqlQuery query(*this); query.prepare("SELECT type FROM kmmPayeeIdentifier WHERE id = ?"); query.bindValue(0, ident.idString()); if (!query.exec() || !query.next()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("modifying payeeIdentifier")); // krazy:exclude=crashy bool typeChanged = (query.value(0).toString() != ident.iid()); if (typeChanged) { // Delete old identifier if type changed const payeeIdentifier oldIdent(fetchPayeeIdentifier(ident.idString())); try { d->actOnPayeeIdentifierObjectInSQL(MyMoneyStorageSqlPrivate::SQLAction::Modify, oldIdent); } catch (const payeeIdentifier::empty &) { // Note: this should not happen because the ui does not offer a way to change // the type of an payeeIdentifier if it was not correctly loaded. throw MYMONEYEXCEPTION((QString::fromLatin1("Could not modify payeeIdentifier '") + ident.idString() + QLatin1String("' because type changed and could not remove identifier of old type. Maybe a plugin is missing?")) ); // krazy:exclude=crashy } } query.prepare("UPDATE kmmPayeeIdentifier SET type = :type WHERE id = :id"); d->writePayeeIdentifier(ident, query); try { if (typeChanged) d->actOnPayeeIdentifierObjectInSQL(MyMoneyStorageSqlPrivate::SQLAction::Save, ident); else d->actOnPayeeIdentifierObjectInSQL(MyMoneyStorageSqlPrivate::SQLAction::Modify, ident); } catch (const payeeIdentifier::empty &) { } } void MyMoneyStorageSql::removePayeeIdentifier(const payeeIdentifier& ident) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); // Remove first, the table could have a contraint which prevents removal // of row in kmmPayeeIdentifier try { d->actOnPayeeIdentifierObjectInSQL(MyMoneyStorageSqlPrivate::SQLAction::Remove, ident); } catch (const payeeIdentifier::empty &) { } QSqlQuery query(*this); query.prepare(d->m_db.m_tables["kmmPayeeIdentifier"].deleteString()); query.bindValue(":id", ident.idString()); if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("deleting payeeIdentifier")); // krazy:exclude=crashy --d->m_payeeIdentifier; } // **** Key/value pairs **** //******************************** read SQL routines ************************************** /*void MyMoneyStorageSql::setVersion (const QString& version) { m_dbVersion = version.section('.', 0, 0).toUInt(); m_minorVersion = version.section('.', 1, 1).toUInt(); // Okay, I made a cockup by forgetting to include a fixversion in the database // design, so we'll use the minor version as fix level (similar to VERSION // and FIXVERSION in XML file format). A second mistake was setting minor version to 1 // in the first place, so we need to subtract one on reading and add one on writing (sigh)!! m_storage->setFileFixVersion( m_minorVersion - 1); }*/ QMap MyMoneyStorageSql::fetchInstitutions(const QStringList& idList, bool forUpdate) const { Q_D(const MyMoneyStorageSql); int institutionsNb = (idList.isEmpty() ? d->m_institutions : idList.size()); d->signalProgress(0, institutionsNb, QObject::tr("Loading institutions...")); int progress = 0; QMap iList; ulong lastId = 0; const MyMoneyDbTable& t = d->m_db.m_tables["kmmInstitutions"]; QSqlQuery sq(*const_cast (this)); sq.prepare("SELECT id from kmmAccounts where institutionId = :id"); QSqlQuery query(*const_cast (this)); QString queryString(t.selectAllString(false)); // Use bind variables, instead of just inserting the values in the queryString, // so that values containing a ':' will work. if (! idList.empty()) { queryString += " WHERE"; for (int i = 0; i < idList.count(); ++i) queryString += QString(" id = :id%1 OR").arg(i); queryString = queryString.left(queryString.length() - 2); } if (forUpdate) queryString += d->m_driver->forUpdateString(); queryString += ';'; query.prepare(queryString); if (! idList.empty()) { QStringList::ConstIterator bindVal = idList.constBegin(); for (int i = 0; bindVal != idList.constEnd(); ++i, ++bindVal) { query.bindValue(QString(":id%1").arg(i), *bindVal); } } if (!query.exec()) throw MYMONEYEXCEPTION(d->buildError(query, Q_FUNC_INFO, QString::fromLatin1("reading Institution"))); // krazy:exclude=crashy int idCol = t.fieldNumber("id"); int nameCol = t.fieldNumber("name"); int managerCol = t.fieldNumber("manager"); int routingCodeCol = t.fieldNumber("routingCode"); int addressStreetCol = t.fieldNumber("addressStreet"); int addressCityCol = t.fieldNumber("addressCity"); int addressZipcodeCol = t.fieldNumber("addressZipcode"); int telephoneCol = t.fieldNumber("telephone"); while (query.next()) { MyMoneyInstitution inst; QString iid = GETSTRING(idCol); inst.setName(GETSTRING(nameCol)); inst.setManager(GETSTRING(managerCol)); inst.setSortcode(GETSTRING(routingCodeCol)); inst.setStreet(GETSTRING(addressStreetCol)); inst.setCity(GETSTRING(addressCityCol)); inst.setPostcode(GETSTRING(addressZipcodeCol)); inst.setTelephone(GETSTRING(telephoneCol)); // get list of subaccounts sq.bindValue(":id", iid); if (!sq.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("reading Institution AccountList")); // krazy:exclude=crashy QStringList aList; while (sq.next()) aList.append(sq.value(0).toString()); foreach (const QString& it, aList) inst.addAccountId(it); iList[iid] = MyMoneyInstitution(iid, inst); ulong id = MyMoneyUtils::extractId(iid); if (id > lastId) lastId = id; d->signalProgress(++progress, 0); } return iList; } QMap MyMoneyStorageSql::fetchInstitutions() const { return fetchInstitutions(QStringList(), false); } void MyMoneyStorageSql::readPayees(const QString& id) { QList list; list.append(id); readPayees(list); } void MyMoneyStorageSql::readPayees(const QList& pid) { Q_D(MyMoneyStorageSql); try { d->m_storage->loadPayees(fetchPayees(pid)); } catch (const MyMoneyException &) { } // if (pid.isEmpty()) m_payeeListRead = true; } void MyMoneyStorageSql::readPayees() { readPayees(QList()); } QMap MyMoneyStorageSql::fetchPayees(const QStringList& idList, bool /*forUpdate*/) const { Q_D(const MyMoneyStorageSql); MyMoneyDbTransaction trans(const_cast (*this), Q_FUNC_INFO); if (d->m_displayStatus) { int payeesNb = (idList.isEmpty() ? d->m_payees : idList.size()); d->signalProgress(0, payeesNb, QObject::tr("Loading payees...")); } int progress = 0; QMap pList; QSqlQuery query(*const_cast (this)); QString queryString = QLatin1String("SELECT kmmPayees.id AS id, kmmPayees.name AS name, kmmPayees.reference AS reference, " " kmmPayees.email AS email, kmmPayees.addressStreet AS addressStreet, kmmPayees.addressCity AS addressCity, kmmPayees.addressZipcode AS addressZipcode, " " kmmPayees.addressState AS addressState, kmmPayees.telephone AS telephone, kmmPayees.notes AS notes, " " kmmPayees.defaultAccountId AS defaultAccountId, kmmPayees.matchData AS matchData, kmmPayees.matchIgnoreCase AS matchIgnoreCase, " " kmmPayees.matchKeys AS matchKeys, " " kmmPayeesPayeeIdentifier.identifierId AS identId " " FROM ( SELECT * FROM kmmPayees "); if (!idList.isEmpty()) { // Create WHERE clause if needed queryString += QLatin1String(" WHERE id IN ("); queryString += QString("?, ").repeated(idList.length()); queryString.chop(2); // remove ", " from end queryString += QLatin1Char(')'); } queryString += QLatin1String( " ) kmmPayees " " LEFT OUTER JOIN kmmPayeesPayeeIdentifier ON kmmPayees.Id = kmmPayeesPayeeIdentifier.payeeId " // The order is used below " ORDER BY kmmPayees.id, kmmPayeesPayeeIdentifier.userOrder;"); query.prepare(queryString); if (!idList.isEmpty()) { // Bind values QStringList::const_iterator end = idList.constEnd(); for (QStringList::const_iterator iter = idList.constBegin(); iter != end; ++iter) { query.addBindValue(*iter); } } if (!query.exec()) throw MYMONEYEXCEPTION(d->buildError(query, Q_FUNC_INFO, QString::fromLatin1("reading Payee"))); // krazy:exclude=crashy const QSqlRecord record = query.record(); const int idCol = record.indexOf("id"); const int nameCol = record.indexOf("name"); const int referenceCol = record.indexOf("reference"); const int emailCol = record.indexOf("email"); const int addressStreetCol = record.indexOf("addressStreet"); const int addressCityCol = record.indexOf("addressCity"); const int addressZipcodeCol = record.indexOf("addressZipcode"); const int addressStateCol = record.indexOf("addressState"); const int telephoneCol = record.indexOf("telephone"); const int notesCol = record.indexOf("notes"); const int defaultAccountIdCol = record.indexOf("defaultAccountId"); const int matchDataCol = record.indexOf("matchData"); const int matchIgnoreCaseCol = record.indexOf("matchIgnoreCase"); const int matchKeysCol = record.indexOf("matchKeys"); const int identIdCol = record.indexOf("identId"); if (query.next()) { while (query.isValid()) { QString pid; QString boolChar; MyMoneyPayee payee; uint type; bool ignoreCase; QString matchKeys; pid = GETSTRING(idCol); payee.setName(GETSTRING(nameCol)); payee.setReference(GETSTRING(referenceCol)); payee.setEmail(GETSTRING(emailCol)); payee.setAddress(GETSTRING(addressStreetCol)); payee.setCity(GETSTRING(addressCityCol)); payee.setPostcode(GETSTRING(addressZipcodeCol)); payee.setState(GETSTRING(addressStateCol)); payee.setTelephone(GETSTRING(telephoneCol)); payee.setNotes(GETSTRING(notesCol)); payee.setDefaultAccountId(GETSTRING(defaultAccountIdCol)); type = GETINT(matchDataCol); ignoreCase = (GETSTRING(matchIgnoreCaseCol) == "Y"); matchKeys = GETSTRING(matchKeysCol); - payee.setMatchData(static_cast(type), ignoreCase, matchKeys); + payee.setMatchData(static_cast(type), ignoreCase, matchKeys); // Get payeeIdentifier ids QStringList identifierIds; do { identifierIds.append(GETSTRING(identIdCol)); } while (query.next() && GETSTRING(idCol) == pid); // as long as the payeeId is unchanged // Fetch and save payeeIdentifier if (!identifierIds.isEmpty()) { QList< ::payeeIdentifier > identifier = fetchPayeeIdentifiers(identifierIds).values(); payee.resetPayeeIdentifiers(identifier); } if (pid == "USER") d->m_storage->setUser(payee); else pList[pid] = MyMoneyPayee(pid, payee); if (d->m_displayStatus) d->signalProgress(++progress, 0); } } return pList; } QMap MyMoneyStorageSql::fetchPayees() const { return fetchPayees(QStringList(), false); } void MyMoneyStorageSql::readTags(const QString& id) { QList list; list.append(id); readTags(list); } void MyMoneyStorageSql::readTags(const QList& pid) { Q_D(MyMoneyStorageSql); try { d->m_storage->loadTags(fetchTags(pid)); d->readFileInfo(); } catch (const MyMoneyException &) { } // if (pid.isEmpty()) m_tagListRead = true; } void MyMoneyStorageSql::readTags() { readTags(QList()); } QMap MyMoneyStorageSql::fetchOnlineJobs(const QStringList& idList, bool forUpdate) const { Q_D(const MyMoneyStorageSql); Q_UNUSED(forUpdate); MyMoneyDbTransaction trans(const_cast (*this), Q_FUNC_INFO); if (d->m_displayStatus) d->signalProgress(0, idList.isEmpty() ? d->m_onlineJobs : idList.size(), QObject::tr("Loading online banking data...")); // Create query QSqlQuery query(*const_cast (this)); if (idList.isEmpty()) { query.prepare("SELECT id, type, jobSend, bankAnswerDate, state, locked FROM kmmOnlineJobs;"); } else { QString queryIdSet = QString("?, ").repeated(idList.length()); queryIdSet.chop(2); query.prepare(QLatin1String("SELECT id, type, jobSend, bankAnswerDate, state, locked FROM kmmOnlineJobs WHERE id IN (") + queryIdSet + QLatin1String(");")); QStringList::const_iterator end = idList.constEnd(); for (QStringList::const_iterator iter = idList.constBegin(); iter != end; ++iter) { query.addBindValue(*iter); } } if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("reading onlineJobs")); // krazy:exclude=crashy // Create onlineJobs int progress = 0; QMap jobList; while (query.next()) { const QString& id = query.value(0).toString(); onlineTask *const task = d->createOnlineTaskObject(query.value(1).toString(), id, *this); onlineJob job = onlineJob(task, id); job.setJobSend(query.value(2).toDateTime()); onlineJob::sendingState state; const QString stateString = query.value(4).toString(); if (stateString == "acceptedByBank") state = onlineJob::acceptedByBank; else if (stateString == "rejectedByBank") state = onlineJob::rejectedByBank; else if (stateString == "abortedByUser") state = onlineJob::abortedByUser; else if (stateString == "sendingError") state = onlineJob::sendingError; else // includes: stateString == "noBankAnswer" state = onlineJob::noBankAnswer; job.setBankAnswer(state, query.value(4).toDateTime()); job.setLock(query.value(5).toString() == QLatin1String("Y") ? true : false); jobList.insert(job.id(), job); if (d->m_displayStatus) d->signalProgress(++progress, 0); } return jobList; } QMap MyMoneyStorageSql::fetchOnlineJobs() const { return fetchOnlineJobs(QStringList(), false); } payeeIdentifier MyMoneyStorageSql::fetchPayeeIdentifier(const QString& id) const { QMap list = fetchPayeeIdentifiers(QStringList(id)); QMap::const_iterator iter = list.constFind(id); if (iter == list.constEnd()) throw MYMONEYEXCEPTION(QString::fromLatin1("payeeIdentifier with id '%1' not found").arg(id)); // krazy:exclude=crashy return *iter; } QMap< QString, payeeIdentifier > MyMoneyStorageSql::fetchPayeeIdentifiers(const QStringList& idList) const { Q_D(const MyMoneyStorageSql); MyMoneyDbTransaction trans(const_cast (*this), Q_FUNC_INFO); // Create query QSqlQuery query(*const_cast (this)); if (idList.isEmpty()) { query.prepare("SELECT id, type FROM kmmPayeeIdentifier;"); } else { QString queryIdSet = QString("?, ").repeated(idList.length()); queryIdSet.chop(2); // remove ", " from end query.prepare(QLatin1String("SELECT id, type FROM kmmPayeeIdentifier WHERE id IN (") + queryIdSet + QLatin1String(");")); QStringList::const_iterator end = idList.constEnd(); for (QStringList::const_iterator iter = idList.constBegin(); iter != end; ++iter) { query.addBindValue(*iter); } } if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("reading payee identifiers")); // krazy:exclude=crashy QMap identList; while (query.next()) { const auto id = query.value(0).toString(); identList.insert(id, d->createPayeeIdentifierObject(*this, query.value(1).toString(), id)); } return identList; } QMap< QString, payeeIdentifier > MyMoneyStorageSql::fetchPayeeIdentifiers() const { return fetchPayeeIdentifiers(QStringList()); } QMap MyMoneyStorageSql::fetchTags(const QStringList& idList, bool /*forUpdate*/) const { Q_D(const MyMoneyStorageSql); MyMoneyDbTransaction trans(const_cast (*this), Q_FUNC_INFO); if (d->m_displayStatus) { int tagsNb = (idList.isEmpty() ? d->m_tags : idList.size()); d->signalProgress(0, tagsNb, QObject::tr("Loading tags...")); } else { // if (m_tagListRead) return; } int progress = 0; QMap taList; //ulong lastId; const MyMoneyDbTable& t = d->m_db.m_tables["kmmTags"]; QSqlQuery query(*const_cast (this)); if (idList.isEmpty()) { query.prepare(t.selectAllString()); } else { QString whereClause = " where ("; QString itemConnector = ""; foreach (const QString& it, idList) { whereClause.append(QString("%1id = '%2'").arg(itemConnector).arg(it)); itemConnector = " or "; } whereClause += ')'; query.prepare(t.selectAllString(false) + whereClause); } if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("reading Tag")); // krazy:exclude=crashy int idCol = t.fieldNumber("id"); int nameCol = t.fieldNumber("name"); int notesCol = t.fieldNumber("notes"); int tagColorCol = t.fieldNumber("tagColor"); int closedCol = t.fieldNumber("closed"); while (query.next()) { QString pid; QString boolChar; MyMoneyTag tag; pid = GETSTRING(idCol); tag.setName(GETSTRING(nameCol)); tag.setNotes(GETSTRING(notesCol)); tag.setClosed((GETSTRING(closedCol) == "Y")); tag.setTagColor(QColor(GETSTRING(tagColorCol))); taList[pid] = MyMoneyTag(pid, tag); if (d->m_displayStatus) d->signalProgress(++progress, 0); } return taList; } QMap MyMoneyStorageSql::fetchTags() const { return fetchTags(QStringList(), false); } QMap MyMoneyStorageSql::fetchAccounts(const QStringList& idList, bool forUpdate) const { Q_D(const MyMoneyStorageSql); int accountsNb = (idList.isEmpty() ? d->m_accounts : idList.size()); d->signalProgress(0, accountsNb, QObject::tr("Loading accounts...")); int progress = 0; QMap accList; QStringList kvpAccountList(idList); const MyMoneyDbTable& t = d->m_db.m_tables["kmmAccounts"]; QSqlQuery query(*const_cast (this)); QSqlQuery sq(*const_cast (this)); QString childQueryString = "SELECT id, parentId FROM kmmAccounts WHERE "; QString queryString(t.selectAllString(false)); // Use bind variables, instead of just inserting the values in the queryString, // so that values containing a ':' will work. if (! idList.empty()) { queryString += " WHERE id IN ("; childQueryString += " parentId IN ("; QString inString; for (int i = 0; i < idList.count(); ++i) { inString += QString(":id%1, ").arg(i); } inString = inString.left(inString.length() - 2) + ')'; queryString += inString; childQueryString += inString; } else { childQueryString += " NOT parentId IS NULL"; } queryString += " ORDER BY id"; childQueryString += " ORDER BY parentid, id"; if (forUpdate) { queryString += d->m_driver->forUpdateString(); childQueryString += d->m_driver->forUpdateString(); } query.prepare(queryString); sq.prepare(childQueryString); if (! idList.empty()) { QStringList::ConstIterator bindVal = idList.constBegin(); for (int i = 0; bindVal != idList.constEnd(); ++i, ++bindVal) { query.bindValue(QString(":id%1").arg(i), *bindVal); sq.bindValue(QString(":id%1").arg(i), *bindVal); } } if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("reading Account")); // krazy:exclude=crashy if (!sq.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("reading subAccountList")); // krazy:exclude=crashy // Reserve enough space for all values. Approximate it with the size of the // idList in case the db doesn't support reporting the size of the // resultset to the caller. //FIXME: this is for if/when there is a QHash conversion //accList.reserve(q.size() > 0 ? q.size() : idList.size()); static const int idCol = t.fieldNumber("id"); static const int institutionIdCol = t.fieldNumber("institutionId"); static const int parentIdCol = t.fieldNumber("parentId"); static const int lastReconciledCol = t.fieldNumber("lastReconciled"); static const int lastModifiedCol = t.fieldNumber("lastModified"); static const int openingDateCol = t.fieldNumber("openingDate"); static const int accountNumberCol = t.fieldNumber("accountNumber"); static const int accountTypeCol = t.fieldNumber("accountType"); static const int accountNameCol = t.fieldNumber("accountName"); static const int descriptionCol = t.fieldNumber("description"); static const int currencyIdCol = t.fieldNumber("currencyId"); static const int balanceCol = t.fieldNumber("balance"); static const int transactionCountCol = t.fieldNumber("transactionCount"); while (query.next()) { QString aid; QString balance; MyMoneyAccount acc; aid = GETSTRING(idCol); acc.setInstitutionId(GETSTRING(institutionIdCol)); acc.setParentAccountId(GETSTRING(parentIdCol)); acc.setLastReconciliationDate(GETDATE_D(lastReconciledCol)); acc.setLastModified(GETDATE_D(lastModifiedCol)); acc.setOpeningDate(GETDATE_D(openingDateCol)); acc.setNumber(GETSTRING(accountNumberCol)); acc.setAccountType(static_cast(GETINT(accountTypeCol))); acc.setName(GETSTRING(accountNameCol)); acc.setDescription(GETSTRING(descriptionCol)); acc.setCurrencyId(GETSTRING(currencyIdCol)); acc.setBalance(MyMoneyMoney(GETSTRING(balanceCol))); const_cast (this)->d_func()->m_transactionCountMap[aid] = (ulong) GETULL(transactionCountCol); // Process any key value pair if (idList.empty()) kvpAccountList.append(aid); accList.insert(aid, MyMoneyAccount(aid, acc)); if (acc.value("PreferredAccount") == "Yes") { const_cast (this)->d_func()->m_preferred.addAccount(aid); } d->signalProgress(++progress, 0); } QMap::Iterator it_acc; QMap::Iterator accListEnd = accList.end(); while (sq.next()) { it_acc = accList.find(sq.value(1).toString()); if (it_acc != accListEnd && it_acc.value().id() == sq.value(1).toString()) { while (sq.isValid() && it_acc != accListEnd && it_acc.value().id() == sq.value(1).toString()) { it_acc.value().addAccountId(sq.value(0).toString()); if (!sq.next()) break; } sq.previous(); } } //TODO: There should be a better way than this. What's below is O(n log n) or more, // where it may be able to be done in O(n), if things are just right. // The operator[] call in the loop is the most expensive call in this function, according // to several profile runs. QHash kvpResult = d->readKeyValuePairs("ACCOUNT", kvpAccountList); QHash ::const_iterator kvp_end = kvpResult.constEnd(); for (QHash ::const_iterator it_kvp = kvpResult.constBegin(); it_kvp != kvp_end; ++it_kvp) { accList[it_kvp.key()].setPairs(it_kvp.value().pairs()); } kvpResult = d->readKeyValuePairs("ONLINEBANKING", kvpAccountList); kvp_end = kvpResult.constEnd(); for (QHash ::const_iterator it_kvp = kvpResult.constBegin(); it_kvp != kvp_end; ++it_kvp) { accList[it_kvp.key()].setOnlineBankingSettings(it_kvp.value()); } return accList; } QMap MyMoneyStorageSql::fetchAccounts() const { return fetchAccounts(QStringList(), false); } QMap MyMoneyStorageSql::fetchBalance(const QStringList& idList, const QDate& date) const { Q_D(const MyMoneyStorageSql); QMap returnValue; QSqlQuery query(*const_cast (this)); QString queryString = "SELECT action, shares, accountId, postDate " "FROM kmmSplits WHERE txType = 'N'"; if (idList.count() > 0) { queryString += "AND accountId in ("; for (int i = 0; i < idList.count(); ++i) { queryString += QString(":id%1, ").arg(i); } queryString = queryString.left(queryString.length() - 2) + ')'; } // SQLite stores dates as YYYY-MM-DDTHH:mm:ss with 0s for the time part. This makes // the <= operator misbehave when the date matches. To avoid this, add a day to the // requested date and use the < operator. if (date.isValid() && !date.isNull()) queryString += QString(" AND postDate < '%1'").arg(date.addDays(1).toString(Qt::ISODate)); queryString += " ORDER BY accountId, postDate;"; //DBG(queryString); query.prepare(queryString); int i = 0; foreach (const QString& bindVal, idList) { query.bindValue(QString(":id%1").arg(i), bindVal); ++i; } if (!query.exec()) // krazy:exclude=crashy throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("fetching balance")); QString id; QString oldId; MyMoneyMoney temp; while (query.next()) { id = query.value(2).toString(); // If the old ID does not match the new ID, then the account being summed has changed. // Write the balance into the returnValue map and update the oldId to the current one. if (id != oldId) { if (!oldId.isEmpty()) { returnValue.insert(oldId, temp); temp = 0; } oldId = id; } if (MyMoneySplit::actionName(eMyMoney::Split::Action::SplitShares) == query.value(0).toString()) temp *= MyMoneyMoney(query.value(1).toString()); else temp += MyMoneyMoney(query.value(1).toString()); } // Do not forget the last id in the list. returnValue.insert(id, temp); // Return the map. return returnValue; } void MyMoneyStorageSql::readTransactions(const MyMoneyTransactionFilter& filter) { Q_D(MyMoneyStorageSql); try { d->m_storage->loadTransactions(fetchTransactions(filter)); } catch (const MyMoneyException &) { throw; } } QMap MyMoneyStorageSql::fetchTransactions(const QString& tidList, const QString& dateClause, bool /*forUpdate*/) const { Q_D(const MyMoneyStorageSql); // if (m_transactionListRead) return; // all list already in memory if (d->m_displayStatus) { int transactionsNb = (tidList.isEmpty() ? d->m_transactions : tidList.size()); d->signalProgress(0, transactionsNb, QObject::tr("Loading transactions...")); } int progress = 0; // m_payeeList.clear(); QString whereClause = " WHERE txType = 'N' "; if (! tidList.isEmpty()) { whereClause += " AND id IN " + tidList; } if (!dateClause.isEmpty()) whereClause += " and " + dateClause; const MyMoneyDbTable& t = d->m_db.m_tables["kmmTransactions"]; QSqlQuery query(*const_cast (this)); query.prepare(t.selectAllString(false) + whereClause + " ORDER BY id;"); if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("reading Transaction")); // krazy:exclude=crashy const MyMoneyDbTable& ts = d->m_db.m_tables["kmmSplits"]; whereClause = " WHERE txType = 'N' "; if (! tidList.isEmpty()) { whereClause += " AND transactionId IN " + tidList; } if (!dateClause.isEmpty()) whereClause += " and " + dateClause; QSqlQuery qs(*const_cast (this)); QString splitQuery = ts.selectAllString(false) + whereClause + " ORDER BY transactionId, splitId;"; qs.prepare(splitQuery); if (!qs.exec()) throw MYMONEYEXCEPTION(d->buildError(qs, Q_FUNC_INFO, "reading Splits")); // krazy:exclude=crashy QString splitTxId = "ZZZ"; MyMoneySplit s; if (qs.next()) { splitTxId = qs.value(0).toString(); d->readSplit(s, qs); } else { splitTxId = "ZZZ"; } QMap txMap; QStringList txList; int idCol = t.fieldNumber("id"); int postDateCol = t.fieldNumber("postDate"); int memoCol = t.fieldNumber("memo"); int entryDateCol = t.fieldNumber("entryDate"); int currencyIdCol = t.fieldNumber("currencyId"); int bankIdCol = t.fieldNumber("bankId"); while (query.next()) { MyMoneyTransaction tx; QString txId = GETSTRING(idCol); tx.setPostDate(GETDATE_D(postDateCol)); tx.setMemo(GETSTRING(memoCol)); tx.setEntryDate(GETDATE_D(entryDateCol)); tx.setCommodity(GETSTRING(currencyIdCol)); tx.setBankID(GETSTRING(bankIdCol)); // skip all splits while the transaction id of the split is less than // the transaction id of the current transaction. Don't forget to check // for the ZZZ flag for the end of the list. while (txId < splitTxId && splitTxId != "ZZZ") { if (qs.next()) { splitTxId = qs.value(0).toString(); d->readSplit(s, qs); } else { splitTxId = "ZZZ"; } } // while the split transaction id matches the current transaction id, // add the split to the current transaction. Set the ZZZ flag if // all splits for this transaction have been read. while (txId == splitTxId) { tx.addSplit(s); if (qs.next()) { splitTxId = qs.value(0).toString(); d->readSplit(s, qs); } else { splitTxId = "ZZZ"; } } // Process any key value pair if (! txId.isEmpty()) { txList.append(txId); tx = MyMoneyTransaction(txId, tx); txMap.insert(tx.uniqueSortKey(), tx); } } // get the kvps QHash kvpMap = d->readKeyValuePairs("TRANSACTION", txList); QMap::Iterator txMapEnd = txMap.end(); for (QMap::Iterator i = txMap.begin(); i != txMapEnd; ++i) { i.value().setPairs(kvpMap[i.value().id()].pairs()); if (d->m_displayStatus) d->signalProgress(++progress, 0); } if ((tidList.isEmpty()) && (dateClause.isEmpty())) { //qDebug("setting full list read"); } return txMap; } QMap MyMoneyStorageSql::fetchTransactions(const QString& tidList) const { return fetchTransactions(tidList, QString(), false); } QMap MyMoneyStorageSql::fetchTransactions() const { return fetchTransactions(QString(), QString(), false); } QMap MyMoneyStorageSql::fetchTransactions(const MyMoneyTransactionFilter& filter) const { Q_D(const MyMoneyStorageSql); // analyze the filter // if (m_transactionListRead) return; // all list already in memory // if the filter is restricted to certain accounts/categories // check if we already have them all in memory QStringList accounts; QString inQuery; filter.accounts(accounts); filter.categories(accounts); // QStringList::iterator it; // bool allAccountsLoaded = true; // for (it = accounts.begin(); it != accounts.end(); ++it) { // if (m_accountsLoaded.find(*it) == m_accountsLoaded.end()) { // allAccountsLoaded = false; // break; // } // } // if (allAccountsLoaded) return; /* Some filter combinations do not lend themselves to implementation * in SQL, or are likely to require such extensive reading of the database * as to make it easier to just read everything into memory. */ bool canImplementFilter = true; MyMoneyMoney m1, m2; if (filter.amountFilter(m1, m2)) { d->alert("Amount Filter Set"); canImplementFilter = false; } QString n1, n2; if (filter.numberFilter(n1, n2)) { d->alert("Number filter set"); canImplementFilter = false; } int t1; if (filter.firstType(t1)) { d->alert("Type filter set"); canImplementFilter = false; } // int s1; // if (filter.firstState(s1)) { // alert("State filter set"); // canImplementFilter = false; // } QRegExp t2; if (filter.textFilter(t2)) { d->alert("text filter set"); canImplementFilter = false; } MyMoneyTransactionFilter::FilterSet s = filter.filterSet(); if (s.singleFilter.validityFilter) { d->alert("Validity filter set"); canImplementFilter = false; } if (!canImplementFilter) { QMap transactionList = fetchTransactions(); QMap::ConstIterator it_t; std::remove_if(transactionList.begin(), transactionList.end(), FilterFail(filter)); return transactionList; } bool splitFilterActive = false; // the split filter is active if we are selecting on fields in the split table // get start and end dates QDate start = filter.fromDate(); QDate end = filter.toDate(); // not entirely sure if the following is correct, but at best, saves a lot of reads, at worst // it only causes us to read a few more transactions that strictly necessary (I think...) if (start == MyMoneyStorageSqlPrivate::m_startDate) start = QDate(); bool txFilterActive = ((start != QDate()) || (end != QDate())); // and this for fields in the transaction table QString whereClause = ""; QString subClauseconnector = " where txType = 'N' and "; // payees QStringList payees; if (filter.payees(payees)) { QString itemConnector = "payeeId in ("; QString payeesClause = ""; foreach (const QString& it, payees) { payeesClause.append(QString("%1'%2'") .arg(itemConnector).arg(it)); itemConnector = ", "; } if (!payeesClause.isEmpty()) { whereClause += subClauseconnector + payeesClause + ')'; subClauseconnector = " and "; } splitFilterActive = true; } //tags QStringList tags; if (filter.tags(tags)) { QString itemConnector = "splitId in ( SELECT splitId from kmmTagSplits where kmmTagSplits.transactionId = kmmSplits.transactionId and tagId in ("; QString tagsClause = ""; foreach (const QString& it, tags) { tagsClause.append(QString("%1'%2'") .arg(itemConnector).arg(it)); itemConnector = ", "; } if (!tagsClause.isEmpty()) { whereClause += subClauseconnector + tagsClause + ')'; subClauseconnector = " and "; } splitFilterActive = true; } // accounts and categories if (!accounts.isEmpty()) { splitFilterActive = true; QString itemConnector = "accountId in ("; QString accountsClause = ""; foreach (const QString& it, accounts) { accountsClause.append(QString("%1 '%2'") .arg(itemConnector).arg(it)); itemConnector = ", "; } if (!accountsClause.isEmpty()) { whereClause += subClauseconnector + accountsClause + ')'; subClauseconnector = " and ("; } } // split states QList splitStates; if (filter.states(splitStates)) { splitFilterActive = true; QString itemConnector = " reconcileFlag IN ("; QString statesClause = ""; foreach (int it, splitStates) { statesClause.append(QString(" %1 '%2'").arg(itemConnector) .arg(d->splitState(TransactionFilter::State(it)))); itemConnector = ','; } if (!statesClause.isEmpty()) { whereClause += subClauseconnector + statesClause + ')'; subClauseconnector = " and ("; } } // I've given up trying to work out the logic. we keep getting the wrong number of close brackets int obc = whereClause.count('('); int cbc = whereClause.count(')'); if (cbc > obc) { qDebug() << "invalid where clause " << whereClause; qFatal("aborting"); } while (cbc < obc) { whereClause.append(')'); cbc++; } // if the split filter is active, but the where clause and the date filter is empty // it means we already have all the transactions for the specified filter // in memory, so just exit if ((splitFilterActive) && (whereClause.isEmpty()) && (!txFilterActive)) { qDebug("all transactions already in storage"); return fetchTransactions(); } // if we have neither a split filter, nor a tx (date) filter // it's effectively a read all if ((!splitFilterActive) && (!txFilterActive)) { //qDebug("reading all transactions"); return fetchTransactions(); } // build a date clause for the transaction table QString dateClause; QString connector = ""; if (end != QDate()) { dateClause = QString("(postDate < '%1')").arg(end.addDays(1).toString(Qt::ISODate)); connector = " and "; } if (start != QDate()) { dateClause += QString("%1 (postDate >= '%2')").arg(connector).arg(start.toString(Qt::ISODate)); } // now get a list of transaction ids // if we have only a date filter, we need to build the list from the tx table // otherwise we need to build from the split table if (splitFilterActive) { inQuery = QString("(select distinct transactionId from kmmSplits %1)").arg(whereClause); } else { inQuery = QString("(select distinct id from kmmTransactions where %1)").arg(dateClause); txFilterActive = false; // kill off the date filter now } return fetchTransactions(inQuery, dateClause); //FIXME: if we have an accounts-only filter, recalc balances on loaded accounts } ulong MyMoneyStorageSql::transactionCount(const QString& aid) const { Q_D(const MyMoneyStorageSql); if (aid.isEmpty()) return d->m_transactions; else return d->m_transactionCountMap[aid]; } QHash MyMoneyStorageSql::transactionCountMap() const { Q_D(const MyMoneyStorageSql); return d->m_transactionCountMap; } bool MyMoneyStorageSql::isReferencedByTransaction(const QString& id) const { Q_D(const MyMoneyStorageSql); //FIXME-ALEX should I add sub query for kmmTagSplits here? QSqlQuery q(*const_cast (this)); q.prepare("SELECT COUNT(*) FROM kmmTransactions " "INNER JOIN kmmSplits ON kmmTransactions.id = kmmSplits.transactionId " "WHERE kmmTransactions.currencyId = :ID OR kmmSplits.payeeId = :ID " "OR kmmSplits.accountId = :ID OR kmmSplits.costCenterId = :ID"); q.bindValue(":ID", id); if ((!q.exec()) || (!q.next())) { // krazy:exclude=crashy d->buildError(q, Q_FUNC_INFO, "error retrieving reference count"); qFatal("Error retrieving reference count"); // definitely shouldn't happen } return (0 != q.value(0).toULongLong()); } QMap MyMoneyStorageSql::fetchSchedules(const QStringList& idList, bool forUpdate) const { Q_D(const MyMoneyStorageSql); int schedulesNb = (idList.isEmpty() ? d->m_schedules : idList.size()); d->signalProgress(0, schedulesNb, QObject::tr("Loading schedules...")); int progress = 0; const MyMoneyDbTable& t = d->m_db.m_tables["kmmSchedules"]; QSqlQuery query(*const_cast (this)); QMap sList; //ulong lastId = 0; const MyMoneyDbTable& ts = d->m_db.m_tables["kmmSplits"]; QSqlQuery qs(*const_cast (this)); qs.prepare(ts.selectAllString(false) + " WHERE transactionId = :id ORDER BY splitId;"); QSqlQuery sq(*const_cast (this)); sq.prepare("SELECT payDate from kmmSchedulePaymentHistory where schedId = :id"); QString queryString(t.selectAllString(false)); // Use bind variables, instead of just inserting the values in the queryString, // so that values containing a ':' will work. if (! idList.empty()) { queryString += " WHERE"; for (int i = 0; i < idList.count(); ++i) queryString += QString(" id = :id%1 OR").arg(i); queryString = queryString.left(queryString.length() - 2); } queryString += " ORDER BY id"; if (forUpdate) queryString += d->m_driver->forUpdateString(); query.prepare(queryString); if (! idList.empty()) { QStringList::ConstIterator bindVal = idList.constBegin(); for (int i = 0; bindVal != idList.constEnd(); ++i, ++bindVal) { query.bindValue(QString(":id%1").arg(i), *bindVal); } } if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("reading Schedules")); // krazy:exclude=crashy int idCol = t.fieldNumber("id"); int nameCol = t.fieldNumber("name"); int typeCol = t.fieldNumber("type"); int occurrenceCol = t.fieldNumber("occurence"); // krazy:exclude=spelling int occurrenceMultiplierCol = t.fieldNumber("occurenceMultiplier"); // krazy:exclude=spelling int paymentTypeCol = t.fieldNumber("paymentType"); int startDateCol = t.fieldNumber("startDate"); int endDateCol = t.fieldNumber("endDate"); int fixedCol = t.fieldNumber("fixed"); int lastDayInMonthCol = t.fieldNumber("lastDayInMonth"); int autoEnterCol = t.fieldNumber("autoEnter"); int lastPaymentCol = t.fieldNumber("lastPayment"); int weekendOptionCol = t.fieldNumber("weekendOption"); int nextPaymentDueCol = t.fieldNumber("nextPaymentDue"); while (query.next()) { MyMoneySchedule s; QString boolChar; QString sId = GETSTRING(idCol); s.setName(GETSTRING(nameCol)); s.setType(static_cast(GETINT(typeCol))); s.setOccurrencePeriod(static_cast(GETINT(occurrenceCol))); s.setOccurrenceMultiplier(GETINT(occurrenceMultiplierCol)); s.setPaymentType(static_cast(GETINT(paymentTypeCol))); s.setStartDate(GETDATE_D(startDateCol)); s.setEndDate(GETDATE_D(endDateCol)); boolChar = GETSTRING(fixedCol); s.setFixed(boolChar == "Y"); boolChar = GETSTRING(lastDayInMonthCol); s.setLastDayInMonth(boolChar == "Y"); boolChar = GETSTRING(autoEnterCol); s.setAutoEnter(boolChar == "Y"); s.setLastPayment(GETDATE_D(lastPaymentCol)); s.setWeekendOption(static_cast(GETINT(weekendOptionCol))); QDate nextPaymentDue = GETDATE_D(nextPaymentDueCol); // convert simple occurrence to compound occurrence int mult = s.occurrenceMultiplier(); Schedule::Occurrence occ = s.occurrencePeriod(); MyMoneySchedule::simpleToCompoundOccurrence(mult, occ); s.setOccurrencePeriod(occ); s.setOccurrenceMultiplier(mult); // now assign the id to the schedule MyMoneySchedule _s(sId, s); s = _s; // read the associated transaction // m_payeeList.clear(); const MyMoneyDbTable& transactionTable = d->m_db.m_tables["kmmTransactions"]; QSqlQuery q2(*const_cast (this)); q2.prepare(transactionTable.selectAllString(false) + " WHERE id = :id;"); q2.bindValue(":id", s.id()); if (!q2.exec()) throw MYMONEYEXCEPTION(d->buildError(q2, Q_FUNC_INFO, QString("reading Scheduled Transaction"))); // krazy:exclude=crashy QSqlRecord rec = q2.record(); if (!q2.next()) throw MYMONEYEXCEPTION(d->buildError(q2, Q_FUNC_INFO, QString("retrieving scheduled transaction"))); MyMoneyTransaction tx(s.id(), MyMoneyTransaction()); tx.setPostDate(d->GETDATE(q2.value(transactionTable.fieldNumber("postDate")).toString())); tx.setMemo(q2.value(transactionTable.fieldNumber("memo")).toString()); tx.setEntryDate(d->GETDATE(q2.value(transactionTable.fieldNumber("entryDate")).toString())); tx.setCommodity(q2.value(transactionTable.fieldNumber("currencyId")).toString()); tx.setBankID(q2.value(transactionTable.fieldNumber("bankId")).toString()); qs.bindValue(":id", s.id()); if (!qs.exec()) throw MYMONEYEXCEPTION(d->buildError(qs, Q_FUNC_INFO, "reading Scheduled Splits")); // krazy:exclude=crashy while (qs.next()) { MyMoneySplit sp; d->readSplit(sp, qs); tx.addSplit(sp); } // if (!m_payeeList.isEmpty()) // readPayees(m_payeeList); // Process any key value pair tx.setPairs(d->readKeyValuePairs("TRANSACTION", s.id()).pairs()); // If the transaction doesn't have a post date, setTransaction will reject it. // The old way of handling things was to store the next post date in the schedule object // and set the transaction post date to QDate(). // For compatibility, if this is the case, copy the next post date from the schedule object // to the transaction object post date. if (!tx.postDate().isValid()) { tx.setPostDate(nextPaymentDue); } s.setTransaction(tx); // read in the recorded payments sq.bindValue(":id", s.id()); if (!sq.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("reading schedule payment history")); // krazy:exclude=crashy while (sq.next()) s.recordPayment(sq.value(0).toDate()); sList[s.id()] = s; //FIXME: enable when schedules have KVPs. // s.setPairs(readKeyValuePairs("SCHEDULE", s.id()).pairs()); //ulong id = MyMoneyUtils::extractId(s.id().data()); //if(id > lastId) // lastId = id; d->signalProgress(++progress, 0); } return sList; } QMap MyMoneyStorageSql::fetchSchedules() const { return fetchSchedules(QStringList(), false); } QMap MyMoneyStorageSql::fetchSecurities(const QStringList& /*idList*/, bool /*forUpdate*/) const { Q_D(const MyMoneyStorageSql); d->signalProgress(0, d->m_securities, QObject::tr("Loading securities...")); int progress = 0; QMap sList; ulong lastId = 0; const MyMoneyDbTable& t = d->m_db.m_tables["kmmSecurities"]; QSqlQuery query(*const_cast (this)); query.prepare(t.selectAllString(false) + " ORDER BY id;"); if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("reading Securities")); // krazy:exclude=crashy int idCol = t.fieldNumber("id"); int nameCol = t.fieldNumber("name"); int symbolCol = t.fieldNumber("symbol"); int typeCol = t.fieldNumber("type"); int roundingMethodCol = t.fieldNumber("roundingMethod"); int smallestAccountFractionCol = t.fieldNumber("smallestAccountFraction"); int pricePrecisionCol = t.fieldNumber("pricePrecision"); int tradingCurrencyCol = t.fieldNumber("tradingCurrency"); int tradingMarketCol = t.fieldNumber("tradingMarket"); while (query.next()) { MyMoneySecurity e; QString eid; eid = GETSTRING(idCol); e.setName(GETSTRING(nameCol)); e.setTradingSymbol(GETSTRING(symbolCol)); e.setSecurityType(static_cast(GETINT(typeCol))); e.setRoundingMethod(static_cast(GETINT(roundingMethodCol))); int saf = GETINT(smallestAccountFractionCol); int pp = GETINT(pricePrecisionCol); e.setTradingCurrency(GETSTRING(tradingCurrencyCol)); e.setTradingMarket(GETSTRING(tradingMarketCol)); if (e.tradingCurrency().isEmpty()) e.setTradingCurrency(d->m_storage->pairs()["kmm-baseCurrency"]); if (saf == 0) saf = 100; if (pp == 0 || pp > 10) pp = 4; e.setSmallestAccountFraction(saf); e.setPricePrecision(pp); // Process any key value pairs e.setPairs(d->readKeyValuePairs("SECURITY", eid).pairs()); //tell the storage objects we have a new security object. // FIXME: Adapt to new interface make sure, to take care of the currencies as well // see MyMoneyStorageXML::readSecurites() MyMoneySecurity security(eid, e); sList[security.id()] = security; ulong id = MyMoneyUtils::extractId(security.id()); if (id > lastId) lastId = id; d->signalProgress(++progress, 0); } return sList; } QMap MyMoneyStorageSql::fetchSecurities() const { return fetchSecurities(QStringList(), false); } MyMoneyPrice MyMoneyStorageSql::fetchSinglePrice(const QString& fromId, const QString& toId, const QDate& date_, bool exactDate, bool /*forUpdate*/) const { Q_D(const MyMoneyStorageSql); const MyMoneyDbTable& t = d->m_db.m_tables["kmmPrices"]; static const int priceDateCol = t.fieldNumber("priceDate"); static const int priceCol = t.fieldNumber("price"); static const int priceSourceCol = t.fieldNumber("priceSource"); QSqlQuery query(*const_cast (this)); // Use bind variables, instead of just inserting the values in the queryString, // so that values containing a ':' will work. // See balance query for why the date logic seems odd. QString queryString = t.selectAllString(false) + " WHERE fromId = :fromId AND toId = :toId AND priceDate < :priceDate "; if (exactDate) queryString += "AND priceDate > :exactDate "; queryString += "ORDER BY priceDate DESC;"; query.prepare(queryString); QDate date(date_); if (!date.isValid()) date = QDate::currentDate(); query.bindValue(":fromId", fromId); query.bindValue(":toId", toId); query.bindValue(":priceDate", date.addDays(1).toString(Qt::ISODate)); if (exactDate) query.bindValue(":exactDate", date.toString(Qt::ISODate)); if (! query.exec()) return MyMoneyPrice(); // krazy:exclude=crashy if (query.next()) { return MyMoneyPrice(fromId, toId, GETDATE_D(priceDateCol), MyMoneyMoney(GETSTRING(priceCol)), GETSTRING(priceSourceCol)); } return MyMoneyPrice(); } MyMoneyPriceList MyMoneyStorageSql::fetchPrices(const QStringList& fromIdList, const QStringList& toIdList, bool forUpdate) const { Q_D(const MyMoneyStorageSql); int pricesNb = (fromIdList.isEmpty() ? d->m_prices : fromIdList.size()); d->signalProgress(0, pricesNb, QObject::tr("Loading prices...")); int progress = 0; const_cast (this)->d_func()->m_readingPrices = true; MyMoneyPriceList pList; const MyMoneyDbTable& t = d->m_db.m_tables["kmmPrices"]; QSqlQuery query(*const_cast (this)); QString queryString = t.selectAllString(false); // Use bind variables, instead of just inserting the values in the queryString, // so that values containing a ':' will work. if (! fromIdList.empty()) { queryString += " WHERE ("; for (int i = 0; i < fromIdList.count(); ++i) { queryString += QString(" fromId = :fromId%1 OR").arg(i); } queryString = queryString.left(queryString.length() - 2) + ')'; } if (! toIdList.empty()) { queryString += " AND ("; for (int i = 0; i < toIdList.count(); ++i) { queryString += QString(" toId = :toId%1 OR").arg(i); } queryString = queryString.left(queryString.length() - 2) + ')'; } if (forUpdate) queryString += d->m_driver->forUpdateString(); queryString += ';'; query.prepare(queryString); if (! fromIdList.empty()) { QStringList::ConstIterator bindVal = fromIdList.constBegin(); for (int i = 0; bindVal != fromIdList.constEnd(); ++i, ++bindVal) { query.bindValue(QString(":fromId%1").arg(i), *bindVal); } } if (! toIdList.empty()) { QStringList::ConstIterator bindVal = toIdList.constBegin(); for (int i = 0; bindVal != toIdList.constEnd(); ++i, ++bindVal) { query.bindValue(QString(":toId%1").arg(i), *bindVal); } } if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("reading Prices")); // krazy:exclude=crashy static const int fromIdCol = t.fieldNumber("fromId"); static const int toIdCol = t.fieldNumber("toId"); static const int priceDateCol = t.fieldNumber("priceDate"); static const int priceCol = t.fieldNumber("price"); static const int priceSourceCol = t.fieldNumber("priceSource"); while (query.next()) { QString from = GETSTRING(fromIdCol); QString to = GETSTRING(toIdCol); QDate date = GETDATE_D(priceDateCol); pList [MyMoneySecurityPair(from, to)].insert(date, MyMoneyPrice(from, to, date, MyMoneyMoney(GETSTRING(priceCol)), GETSTRING(priceSourceCol))); d->signalProgress(++progress, 0); } const_cast (this)->d_func()->m_readingPrices = false; return pList; } MyMoneyPriceList MyMoneyStorageSql::fetchPrices() const { return fetchPrices(QStringList(), QStringList(), false); } QMap MyMoneyStorageSql::fetchCurrencies(const QStringList& idList, bool forUpdate) const { Q_D(const MyMoneyStorageSql); int currenciesNb = (idList.isEmpty() ? d->m_currencies : idList.size()); d->signalProgress(0, currenciesNb, QObject::tr("Loading currencies...")); int progress = 0; QMap cList; const MyMoneyDbTable& t = d->m_db.m_tables["kmmCurrencies"]; QSqlQuery query(*const_cast (this)); QString queryString(t.selectAllString(false)); // Use bind variables, instead of just inserting the values in the queryString, // so that values containing a ':' will work. if (! idList.empty()) { queryString += " WHERE"; for (int i = 0; i < idList.count(); ++i) queryString += QString(" isocode = :id%1 OR").arg(i); queryString = queryString.left(queryString.length() - 2); } queryString += " ORDER BY ISOcode"; if (forUpdate) queryString += d->m_driver->forUpdateString(); queryString += ';'; query.prepare(queryString); if (! idList.empty()) { QStringList::ConstIterator bindVal = idList.constBegin(); for (int i = 0; bindVal != idList.end(); ++i, ++bindVal) { query.bindValue(QString(":id%1").arg(i), *bindVal); } } if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("reading Currencies")); // krazy:exclude=crashy int ISOcodeCol = t.fieldNumber("ISOcode"); int nameCol = t.fieldNumber("name"); int typeCol = t.fieldNumber("type"); int symbol1Col = t.fieldNumber("symbol1"); int symbol2Col = t.fieldNumber("symbol2"); int symbol3Col = t.fieldNumber("symbol3"); int smallestCashFractionCol = t.fieldNumber("smallestCashFraction"); int smallestAccountFractionCol = t.fieldNumber("smallestAccountFraction"); int pricePrecisionCol = t.fieldNumber("pricePrecision"); while (query.next()) { QString id; MyMoneySecurity c; QChar symbol[3]; id = GETSTRING(ISOcodeCol); c.setName(GETSTRING(nameCol)); c.setSecurityType(static_cast(GETINT(typeCol))); symbol[0] = QChar(GETINT(symbol1Col)); symbol[1] = QChar(GETINT(symbol2Col)); symbol[2] = QChar(GETINT(symbol3Col)); c.setSmallestCashFraction(GETINT(smallestCashFractionCol)); c.setSmallestAccountFraction(GETINT(smallestAccountFractionCol)); c.setPricePrecision(GETINT(pricePrecisionCol)); c.setTradingSymbol(QString(symbol, 3).trimmed()); cList[id] = MyMoneySecurity(id, c); d->signalProgress(++progress, 0); } return cList; } QMap MyMoneyStorageSql::fetchCurrencies() const { return fetchCurrencies(QStringList(), false); } QMap MyMoneyStorageSql::fetchReports(const QStringList& /*idList*/, bool /*forUpdate*/) const { Q_D(const MyMoneyStorageSql); d->signalProgress(0, d->m_reports, QObject::tr("Loading reports...")); int progress = 0; const MyMoneyDbTable& t = d->m_db.m_tables["kmmReportConfig"]; QSqlQuery query(*const_cast (this)); query.prepare(t.selectAllString(true)); if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("reading reports")); // krazy:exclude=crashy int xmlCol = t.fieldNumber("XML"); QMap rList; while (query.next()) { QDomDocument dom; dom.setContent(GETSTRING(xmlCol), false); QDomNode child = dom.firstChild(); child = child.firstChild(); MyMoneyReport report; if (report.read(child.toElement())) rList[report.id()] = report; d->signalProgress(++progress, 0); } return rList; } QMap MyMoneyStorageSql::fetchReports() const { return fetchReports(QStringList(), false); } QMap MyMoneyStorageSql::fetchBudgets(const QStringList& idList, bool forUpdate) const { Q_D(const MyMoneyStorageSql); int budgetsNb = (idList.isEmpty() ? d->m_budgets : idList.size()); d->signalProgress(0, budgetsNb, QObject::tr("Loading budgets...")); int progress = 0; const MyMoneyDbTable& t = d->m_db.m_tables["kmmBudgetConfig"]; QSqlQuery query(*const_cast (this)); QString queryString(t.selectAllString(false)); if (! idList.empty()) { queryString += " WHERE id = '" + idList.join("' OR id = '") + '\''; } if (forUpdate) queryString += d->m_driver->forUpdateString(); queryString += ';'; query.prepare(queryString); if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("reading budgets")); // krazy:exclude=crashy QMap budgets; int xmlCol = t.fieldNumber("XML"); while (query.next()) { QDomDocument dom; dom.setContent(GETSTRING(xmlCol), false); QDomNode child = dom.firstChild(); child = child.firstChild(); MyMoneyBudget budget(child.toElement()); budgets.insert(budget.id(), budget); d->signalProgress(++progress, 0); } return budgets; } QMap MyMoneyStorageSql::fetchBudgets() const { return fetchBudgets(QStringList(), false); } ulong MyMoneyStorageSql::getNextBudgetId() const { Q_D(const MyMoneyStorageSql); return d->getNextId<&MyMoneyStorageSqlPrivate::m_hiIdBudgets>(QLatin1String("kmmBudgetConfig"), QLatin1String("id"), 1); } ulong MyMoneyStorageSql::getNextAccountId() const { Q_D(const MyMoneyStorageSql); return d->getNextId<&MyMoneyStorageSqlPrivate::m_hiIdAccounts>(QLatin1String("kmmAccounts"), QLatin1String("id"), 1); } ulong MyMoneyStorageSql::getNextInstitutionId() const { Q_D(const MyMoneyStorageSql); return d->getNextId<&MyMoneyStorageSqlPrivate::m_hiIdInstitutions>(QLatin1String("kmmInstitutions"), QLatin1String("id"), 1); } ulong MyMoneyStorageSql::getNextPayeeId() const { Q_D(const MyMoneyStorageSql); return d->getNextId<&MyMoneyStorageSqlPrivate::m_hiIdPayees>(QLatin1String("kmmPayees"), QLatin1String("id"), 1); } ulong MyMoneyStorageSql::getNextTagId() const { Q_D(const MyMoneyStorageSql); return d->getNextId<&MyMoneyStorageSqlPrivate::m_hiIdTags>(QLatin1String("kmmTags"), QLatin1String("id"), 1); } ulong MyMoneyStorageSql::getNextReportId() const { Q_D(const MyMoneyStorageSql); return d->getNextId<&MyMoneyStorageSqlPrivate::m_hiIdReports>(QLatin1String("kmmReportConfig"), QLatin1String("id"), 1); } ulong MyMoneyStorageSql::getNextScheduleId() const { Q_D(const MyMoneyStorageSql); return d->getNextId<&MyMoneyStorageSqlPrivate::m_hiIdSchedules>(QLatin1String("kmmSchedules"), QLatin1String("id"), 3); } ulong MyMoneyStorageSql::getNextSecurityId() const { Q_D(const MyMoneyStorageSql); return d->getNextId<&MyMoneyStorageSqlPrivate::m_hiIdSecurities>(QLatin1String("kmmSecurities"), QLatin1String("id"), 1); } ulong MyMoneyStorageSql::getNextTransactionId() const { Q_D(const MyMoneyStorageSql); return d->getNextId<&MyMoneyStorageSqlPrivate::m_hiIdTransactions>(QLatin1String("kmmTransactions"), QLatin1String("id"), 1); } ulong MyMoneyStorageSql::getNextOnlineJobId() const { Q_D(const MyMoneyStorageSql); return d->getNextId<&MyMoneyStorageSqlPrivate::m_hiIdOnlineJobs>(QLatin1String("kmmOnlineJobs"), QLatin1String("id"), 1); } ulong MyMoneyStorageSql::getNextPayeeIdentifierId() const { Q_D(const MyMoneyStorageSql); return d->getNextId<&MyMoneyStorageSqlPrivate::m_hiIdPayeeIdentifier>(QLatin1String("kmmPayeeIdentifier"), QLatin1String("id"), 5); } ulong MyMoneyStorageSql::getNextCostCenterId() const { Q_D(const MyMoneyStorageSql); return d->getNextId<&MyMoneyStorageSqlPrivate::m_hiIdCostCenter>(QLatin1String("kmmCostCenterIdentifier"), QLatin1String("id"), 5); } ulong MyMoneyStorageSql::incrementBudgetId() { Q_D(MyMoneyStorageSql); d->m_hiIdBudgets = getNextBudgetId() + 1; return (d->m_hiIdBudgets - 1); } /** * @warning This method uses getNextAccountId() internaly. The database is not informed which can cause issues * when the database is accessed concurrently. Then maybe a single id is used twice but the RDBMS will detect the * issue and KMyMoney crashes. This issue can only occour when two instances of KMyMoney access the same database. * But in this unlikley case MyMoneyStorageSql will have a lot more issues, I think. */ ulong MyMoneyStorageSql::incrementAccountId() { Q_D(MyMoneyStorageSql); d->m_hiIdAccounts = getNextAccountId() + 1; return (d->m_hiIdAccounts - 1); } ulong MyMoneyStorageSql::incrementInstitutionId() { Q_D(MyMoneyStorageSql); d->m_hiIdInstitutions = getNextInstitutionId() + 1; return (d->m_hiIdInstitutions - 1); } ulong MyMoneyStorageSql::incrementPayeeId() { Q_D(MyMoneyStorageSql); d->m_hiIdPayees = getNextPayeeId() + 1; return (d->m_hiIdPayees - 1); } ulong MyMoneyStorageSql::incrementTagId() { Q_D(MyMoneyStorageSql); d->m_hiIdTags = getNextTagId() + 1; return (d->m_hiIdTags - 1); } ulong MyMoneyStorageSql::incrementReportId() { Q_D(MyMoneyStorageSql); d->m_hiIdReports = getNextReportId() + 1; return (d->m_hiIdReports - 1); } ulong MyMoneyStorageSql::incrementScheduleId() { Q_D(MyMoneyStorageSql); d->m_hiIdSchedules = getNextScheduleId() + 1; return (d->m_hiIdSchedules - 1); } ulong MyMoneyStorageSql::incrementSecurityId() { Q_D(MyMoneyStorageSql); d->m_hiIdSecurities = getNextSecurityId() + 1; return (d->m_hiIdSecurities - 1); } ulong MyMoneyStorageSql::incrementTransactionId() { Q_D(MyMoneyStorageSql); d->m_hiIdTransactions = getNextTransactionId() + 1; return (d->m_hiIdTransactions - 1); } ulong MyMoneyStorageSql::incrementOnlineJobId() { Q_D(MyMoneyStorageSql); d->m_hiIdOnlineJobs = getNextOnlineJobId() + 1; return (d->m_hiIdOnlineJobs - 1); } ulong MyMoneyStorageSql::incrementPayeeIdentfierId() { Q_D(MyMoneyStorageSql); d->m_hiIdPayeeIdentifier = getNextPayeeIdentifierId() + 1; return (d->m_hiIdPayeeIdentifier - 1); } ulong MyMoneyStorageSql::incrementCostCenterId() { Q_D(MyMoneyStorageSql); d->m_hiIdCostCenter = getNextCostCenterId() + 1; return (d->m_hiIdCostCenter - 1); } void MyMoneyStorageSql::loadAccountId(ulong id) { Q_D(MyMoneyStorageSql); d->m_hiIdAccounts = id; d->writeFileInfo(); } void MyMoneyStorageSql::loadTransactionId(ulong id) { Q_D(MyMoneyStorageSql); d->m_hiIdTransactions = id; d->writeFileInfo(); } void MyMoneyStorageSql::loadPayeeId(ulong id) { Q_D(MyMoneyStorageSql); d->m_hiIdPayees = id; d->writeFileInfo(); } void MyMoneyStorageSql::loadTagId(ulong id) { Q_D(MyMoneyStorageSql); d->m_hiIdTags = id; d->writeFileInfo(); } void MyMoneyStorageSql::loadInstitutionId(ulong id) { Q_D(MyMoneyStorageSql); d->m_hiIdInstitutions = id; d->writeFileInfo(); } void MyMoneyStorageSql::loadScheduleId(ulong id) { Q_D(MyMoneyStorageSql); d->m_hiIdSchedules = id; d->writeFileInfo(); } void MyMoneyStorageSql::loadSecurityId(ulong id) { Q_D(MyMoneyStorageSql); d->m_hiIdSecurities = id; d->writeFileInfo(); } void MyMoneyStorageSql::loadReportId(ulong id) { Q_D(MyMoneyStorageSql); d->m_hiIdReports = id; d->writeFileInfo(); } void MyMoneyStorageSql::loadBudgetId(ulong id) { Q_D(MyMoneyStorageSql); d->m_hiIdBudgets = id; d->writeFileInfo(); } void MyMoneyStorageSql::loadOnlineJobId(ulong id) { Q_D(MyMoneyStorageSql); d->m_hiIdOnlineJobs = id; d->writeFileInfo(); } void MyMoneyStorageSql::loadPayeeIdentifierId(ulong id) { Q_D(MyMoneyStorageSql); d->m_hiIdPayeeIdentifier = id; d->writeFileInfo(); } //**************************************************** void MyMoneyStorageSql::setProgressCallback(void(*callback)(int, int, const QString&)) { Q_D(MyMoneyStorageSql); d->m_progressCallback = callback; } void MyMoneyStorageSql::readFile(QIODevice* s, MyMoneyStorageMgr* storage) { Q_UNUSED(s); Q_UNUSED(storage) } void MyMoneyStorageSql::writeFile(QIODevice* s, MyMoneyStorageMgr* storage) { Q_UNUSED(s); Q_UNUSED(storage) } // **************************** Error display routine ******************************* QDate MyMoneyStorageSqlPrivate::m_startDate = QDate(1900, 1, 1); void MyMoneyStorageSql::setStartDate(const QDate& startDate) { MyMoneyStorageSqlPrivate::m_startDate = startDate; } QMap< QString, MyMoneyCostCenter > MyMoneyStorageSql::fetchCostCenters(const QStringList& idList, bool forUpdate) const { Q_D(const MyMoneyStorageSql); Q_UNUSED(forUpdate); MyMoneyDbTransaction trans(const_cast (*this), Q_FUNC_INFO); if (d->m_displayStatus) { int costCenterNb = (idList.isEmpty() ? 100 : idList.size()); d->signalProgress(0, costCenterNb, QObject::tr("Loading cost center...")); } int progress = 0; QMap costCenterList; //ulong lastId; const MyMoneyDbTable& t = d->m_db.m_tables["kmmCostCenter"]; QSqlQuery query(*const_cast (this)); if (idList.isEmpty()) { query.prepare(t.selectAllString()); } else { QString whereClause = " where ("; QString itemConnector = ""; foreach (const QString& it, idList) { whereClause.append(QString("%1id = '%2'").arg(itemConnector).arg(it)); itemConnector = " or "; } whereClause += ')'; query.prepare(t.selectAllString(false) + whereClause); } if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("reading CostCenter")); // krazy:exclude=crashy const int idCol = t.fieldNumber("id"); const int nameCol = t.fieldNumber("name"); while (query.next()) { MyMoneyCostCenter costCenter; QString pid = GETSTRING(idCol); costCenter.setName(GETSTRING(nameCol)); costCenterList[pid] = MyMoneyCostCenter(pid, costCenter); if (d->m_displayStatus) d->signalProgress(++progress, 0); } return costCenterList; } QMap< QString, MyMoneyCostCenter > MyMoneyStorageSql::fetchCostCenters() const { return fetchCostCenters(QStringList(), false); } diff --git a/kmymoney/plugins/sql/mymoneystoragesql_p.h b/kmymoney/plugins/sql/mymoneystoragesql_p.h index 773749e2b..4ab0ef374 100644 --- a/kmymoney/plugins/sql/mymoneystoragesql_p.h +++ b/kmymoney/plugins/sql/mymoneystoragesql_p.h @@ -1,3124 +1,3124 @@ /*************************************************************************** mymoneystoragesql.cpp --------------------- begin : 11 November 2005 copyright : (C) 2005 by Tony Bloomfield email : tonybloom@users.sourceforge.net : Fernando Vilas : Christian Dávid (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. * * * ***************************************************************************/ #ifndef MYMONEYSTORAGESQL_P_H #define MYMONEYSTORAGESQL_P_H #include "mymoneystoragesql.h" // ---------------------------------------------------------------------------- // System Includes #include // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include // ---------------------------------------------------------------------------- // Project Includes #include "mymoneystoragemgr.h" #include "kmymoneystorageplugin.h" #include "onlinejobadministration.h" #include "payeeidentifier/payeeidentifierloader.h" #include "onlinetasks/interfaces/tasks/onlinetask.h" #include "mymoneycostcenter.h" #include "mymoneyexception.h" #include "mymoneyinstitution.h" #include "mymoneyaccount.h" #include "mymoneysecurity.h" #include "mymoneymoney.h" #include "mymoneyschedule.h" #include "mymoneypayee.h" #include "mymoneytag.h" #include "mymoneysplit.h" #include "mymoneytransaction.h" #include "mymoneytransactionfilter.h" #include "mymoneybudget.h" #include "mymoneyreport.h" #include "mymoneyprice.h" #include "mymoneyutils.h" #include "mymoneydbdef.h" #include "mymoneydbdriver.h" #include "payeeidentifierdata.h" #include "payeeidentifier.h" #include "payeeidentifiertyped.h" #include "payeeidentifier/ibanbic/ibanbic.h" #include "payeeidentifier/nationalaccount/nationalaccount.h" #include "onlinetasks/sepa/sepaonlinetransferimpl.h" #include "mymoneyenums.h" #include "mymoneystoragenames.h" using namespace eMyMoney; using namespace MyMoneyStandardAccounts; class FilterFail { public: explicit FilterFail(const MyMoneyTransactionFilter& filter) : m_filter(filter) {} inline bool operator()(const QPair& transactionPair) { return (*this)(transactionPair.second); } inline bool operator()(const MyMoneyTransaction& transaction) { return !m_filter.match(transaction); } private: MyMoneyTransactionFilter m_filter; }; //***************************************************************************** // Create a class to handle db transactions using scope // // Don't let the database object get destroyed while this object exists, // that would result in undefined behavior. class MyMoneyDbTransaction { public: explicit MyMoneyDbTransaction(MyMoneyStorageSql& db, const QString& name) : m_db(db), m_name(name) { db.startCommitUnit(name); } ~MyMoneyDbTransaction() { if (std::uncaught_exception()) { m_db.cancelCommitUnit(m_name); } else { try{ m_db.endCommitUnit(m_name); } catch (const MyMoneyException &) { try { m_db.cancelCommitUnit(m_name); } catch (const MyMoneyException &e) { qDebug() << e.what(); } } } } private: MyMoneyStorageSql& m_db; QString m_name; }; /** * The MyMoneySqlQuery class is derived from QSqlQuery to provide * a way to adjust some queries based on database type and make * debugging easier by providing a place to put debug statements. */ class MyMoneySqlQuery : public QSqlQuery { public: explicit MyMoneySqlQuery(MyMoneyStorageSql* db = 0) : QSqlQuery(*db) { } virtual ~MyMoneySqlQuery() { } bool exec() { qDebug() << "start sql:" << lastQuery(); bool rc = QSqlQuery::exec(); qDebug() << "end sql:" << QSqlQuery::executedQuery(); qDebug() << "***Query returned:" << rc << ", row count:" << numRowsAffected(); return (rc); } bool exec(const QString & query) { qDebug() << "start sql:" << query; bool rc = QSqlQuery::exec(query); qDebug() << "end sql:" << QSqlQuery::executedQuery(); qDebug() << "***Query returned:" << rc << ", row count:" << numRowsAffected(); return rc; } bool prepare(const QString & query) { return (QSqlQuery::prepare(query)); } }; #define GETSTRING(a) query.value(a).toString() #define GETDATE(a) getDate(GETSTRING(a)) #define GETDATE_D(a) d->getDate(GETSTRING(a)) #define GETDATETIME(a) getDateTime(GETSTRING(a)) #define GETINT(a) query.value(a).toInt() #define GETULL(a) query.value(a).toULongLong() #define MYMONEYEXCEPTIONSQL(exceptionMessage) MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, exceptionMessage)) #define MYMONEYEXCEPTIONSQL_D(exceptionMessage) MYMONEYEXCEPTION(d->buildError(query, Q_FUNC_INFO, exceptionMessage)) class MyMoneyStorageSqlPrivate { Q_DISABLE_COPY(MyMoneyStorageSqlPrivate) Q_DECLARE_PUBLIC(MyMoneyStorageSql) public: explicit MyMoneyStorageSqlPrivate(MyMoneyStorageSql* qq) : q_ptr(qq), m_dbVersion(0), m_storage(nullptr), m_loadAll(false), m_override(false), m_institutions(0), m_accounts(0), m_payees(0), m_tags(0), m_transactions(0), m_splits(0), m_securities(0), m_prices(0), m_currencies(0), m_schedules(0), m_reports(0), m_kvps(0), m_budgets(0), m_onlineJobs(0), m_payeeIdentifier(0), m_hiIdInstitutions(0), m_hiIdPayees(0), m_hiIdTags(0), m_hiIdAccounts(0), m_hiIdTransactions(0), m_hiIdSchedules(0), m_hiIdSecurities(0), m_hiIdReports(0), m_hiIdBudgets(0), m_hiIdOnlineJobs(0), m_hiIdPayeeIdentifier(0), m_hiIdCostCenter(0), m_displayStatus(false), m_readingPrices(false), m_newDatabase(false), m_progressCallback(nullptr) { m_preferred.setReportAllSplits(false); } ~MyMoneyStorageSqlPrivate() { } enum class SQLAction { Save, Modify, Remove }; /** * MyMoneyStorageSql get highest ID number from the database * * @return : highest ID number */ ulong highestNumberFromIdString(QString tableName, QString tableField, int prefixLength) { Q_Q(MyMoneyStorageSql); MyMoneyDbTransaction t(*q, Q_FUNC_INFO); QSqlQuery query(*q); if (!query.exec(m_driver->highestNumberFromIdString(tableName, tableField, prefixLength)) || !query.next()) throw MYMONEYEXCEPTIONSQL("retrieving highest ID number"); return query.value(0).toULongLong(); } /** * @name writeFromStorageMethods * @{ * These method write all data from m_storage to the database. Data which is * stored in the database is deleted. */ void writeUserInformation(); void writeInstitutions() { Q_Q(MyMoneyStorageSql); // first, get a list of what's on the database // anything not in the list needs to be inserted // anything which is will be updated and removed from the list // anything left over at the end will need to be deleted // this is an expensive and inconvenient way to do things; find a better way // one way would be to build the lists when reading the db // unfortunately this object does not persist between read and write // it would also be nice if we could tell which objects had been updated since we read them in QList dbList; QSqlQuery query(*q); query.prepare("SELECT id FROM kmmInstitutions;"); if (!query.exec()) throw MYMONEYEXCEPTIONSQL("building Institution list"); // krazy:exclude=crashy while (query.next()) dbList.append(query.value(0).toString()); const QList list = m_storage->institutionList(); QList insertList; QList updateList; QSqlQuery query2(*q); query.prepare(m_db.m_tables["kmmInstitutions"].updateString()); query2.prepare(m_db.m_tables["kmmInstitutions"].insertString()); signalProgress(0, list.count(), "Writing Institutions..."); foreach (const MyMoneyInstitution& i, list) { if (dbList.contains(i.id())) { dbList.removeAll(i.id()); updateList << i; } else { insertList << i; } signalProgress(++m_institutions, 0); } if (!insertList.isEmpty()) writeInstitutionList(insertList, query2); if (!updateList.isEmpty()) writeInstitutionList(updateList, query); if (!dbList.isEmpty()) { QVariantList deleteList; // qCopy segfaults here, so do it with a hand-rolled loop foreach (const QString& it, dbList) { deleteList << it; } query.prepare("DELETE FROM kmmInstitutions WHERE id = :id"); query.bindValue(":id", deleteList); if (!query.execBatch()) throw MYMONEYEXCEPTIONSQL("deleting Institution"); deleteKeyValuePairs("OFXSETTINGS", deleteList); } } void writePayees() { Q_Q(MyMoneyStorageSql); // first, get a list of what's on the database (see writeInstitutions) QSqlQuery query(*q); query.prepare("SELECT id FROM kmmPayees;"); if (!query.exec()) throw MYMONEYEXCEPTIONSQL("building Payee list"); // krazy:exclude=crashy QList dbList; dbList.reserve(query.numRowsAffected()); while (query.next()) dbList.append(query.value(0).toString()); QList list = m_storage->payeeList(); MyMoneyPayee user(QString("USER"), m_storage->user()); list.prepend(user); signalProgress(0, list.count(), "Writing Payees..."); Q_FOREACH(const MyMoneyPayee& it, list) { if (dbList.contains(it.id())) { dbList.removeAll(it.id()); q->modifyPayee(it); } else { q->addPayee(it); } signalProgress(++m_payees, 0); } if (!dbList.isEmpty()) { QMap payeesToDelete = q->fetchPayees(dbList, true); Q_FOREACH(const MyMoneyPayee& payee, payeesToDelete) { q->removePayee(payee); } } } void writeTags() { Q_Q(MyMoneyStorageSql); // first, get a list of what's on the database (see writeInstitutions) QList dbList; QSqlQuery query(*q); query.prepare("SELECT id FROM kmmTags;"); if (!query.exec()) throw MYMONEYEXCEPTIONSQL("building Tag list"); // krazy:exclude=crashy while (query.next()) dbList.append(query.value(0).toString()); QList list = m_storage->tagList(); signalProgress(0, list.count(), "Writing Tags..."); QSqlQuery query2(*q); query.prepare(m_db.m_tables["kmmTags"].updateString()); query2.prepare(m_db.m_tables["kmmTags"].insertString()); foreach (const MyMoneyTag& it, list) { if (dbList.contains(it.id())) { dbList.removeAll(it.id()); writeTag(it, query); } else { writeTag(it, query2); } signalProgress(++m_tags, 0); } if (!dbList.isEmpty()) { QVariantList deleteList; // qCopy segfaults here, so do it with a hand-rolled loop foreach (const QString& it, dbList) { deleteList << it; } query.prepare(m_db.m_tables["kmmTags"].deleteString()); query.bindValue(":id", deleteList); if (!query.execBatch()) throw MYMONEYEXCEPTIONSQL("deleting Tag"); m_tags -= query.numRowsAffected(); } } void writeAccounts() { Q_Q(MyMoneyStorageSql); // first, get a list of what's on the database (see writeInstitutions) QList dbList; QSqlQuery query(*q); query.prepare("SELECT id FROM kmmAccounts;"); if (!query.exec()) throw MYMONEYEXCEPTIONSQL("building Account list"); // krazy:exclude=crashy while (query.next()) dbList.append(query.value(0).toString()); QList list; m_storage->accountList(list); unsigned progress = 0; signalProgress(0, list.count(), "Writing Accounts..."); if (dbList.isEmpty()) { // new table, insert standard accounts query.prepare(m_db.m_tables["kmmAccounts"].insertString()); } else { query.prepare(m_db.m_tables["kmmAccounts"].updateString()); } // Attempt to write the standard accounts. For an empty db, this will fail. try { QList stdList; stdList << m_storage->asset(); stdList << m_storage->liability(); stdList << m_storage->expense(); stdList << m_storage->income(); stdList << m_storage->equity(); writeAccountList(stdList, query); m_accounts += stdList.size(); } catch (const MyMoneyException &) { // If the above failed, assume that the database is empty and create // the standard accounts by hand before writing them. MyMoneyAccount acc_l; acc_l.setAccountType(Account::Type::Liability); acc_l.setName("Liability"); MyMoneyAccount liability(stdAccNames[stdAccLiability], acc_l); MyMoneyAccount acc_a; acc_a.setAccountType(Account::Type::Asset); acc_a.setName("Asset"); MyMoneyAccount asset(stdAccNames[stdAccAsset], acc_a); MyMoneyAccount acc_e; acc_e.setAccountType(Account::Type::Expense); acc_e.setName("Expense"); MyMoneyAccount expense(stdAccNames[stdAccExpense], acc_e); MyMoneyAccount acc_i; acc_i.setAccountType(Account::Type::Income); acc_i.setName("Income"); MyMoneyAccount income(stdAccNames[stdAccIncome], acc_i); MyMoneyAccount acc_q; acc_q.setAccountType(Account::Type::Equity); acc_q.setName("Equity"); MyMoneyAccount equity(stdAccNames[stdAccEquity], acc_q); QList stdList; stdList << asset; stdList << liability; stdList << expense; stdList << income; stdList << equity; writeAccountList(stdList, query); m_accounts += stdList.size(); } QSqlQuery query2(*q); query.prepare(m_db.m_tables["kmmAccounts"].updateString()); query2.prepare(m_db.m_tables["kmmAccounts"].insertString()); QList updateList; QList insertList; // Update the accounts that exist; insert the ones that do not. foreach (const MyMoneyAccount& it, list) { m_transactionCountMap[it.id()] = m_storage->transactionCount(it.id()); if (dbList.contains(it.id())) { dbList.removeAll(it.id()); updateList << it; } else { insertList << it; } signalProgress(++progress, 0); ++m_accounts; } writeAccountList(updateList, query); writeAccountList(insertList, query2); // Delete the accounts that are in the db but no longer in memory. if (!dbList.isEmpty()) { QVariantList kvpList; query.prepare("DELETE FROM kmmAccounts WHERE id = :id"); foreach (const QString& it, dbList) { if (!m_storage->isStandardAccount(it)) { kvpList << it; } } query.bindValue(":id", kvpList); if (!query.execBatch()) throw MYMONEYEXCEPTIONSQL("deleting Account"); deleteKeyValuePairs("ACCOUNT", kvpList); deleteKeyValuePairs("ONLINEBANKING", kvpList); } } void writeTransactions() { Q_Q(MyMoneyStorageSql); // first, get a list of what's on the database (see writeInstitutions) QList dbList; QSqlQuery query(*q); query.prepare("SELECT id FROM kmmTransactions WHERE txType = 'N';"); if (!query.exec()) throw MYMONEYEXCEPTIONSQL("building Transaction list"); // krazy:exclude=crashy while (query.next()) dbList.append(query.value(0).toString()); MyMoneyTransactionFilter filter; filter.setReportAllSplits(false); QList list; m_storage->transactionList(list, filter); signalProgress(0, list.count(), "Writing Transactions..."); QSqlQuery q2(*q); query.prepare(m_db.m_tables["kmmTransactions"].updateString()); q2.prepare(m_db.m_tables["kmmTransactions"].insertString()); foreach (const MyMoneyTransaction& it, list) { if (dbList.contains(it.id())) { dbList.removeAll(it.id()); writeTransaction(it.id(), it, query, "N"); } else { writeTransaction(it.id(), it, q2, "N"); } signalProgress(++m_transactions, 0); } if (!dbList.isEmpty()) { foreach (const QString& it, dbList) { deleteTransaction(it); } } } void writeSchedules() { Q_Q(MyMoneyStorageSql); // first, get a list of what's on the database (see writeInstitutions) QList dbList; QSqlQuery query(*q); query.prepare("SELECT id FROM kmmSchedules;"); if (!query.exec()) throw MYMONEYEXCEPTIONSQL("building Schedule list"); // krazy:exclude=crashy while (query.next()) dbList.append(query.value(0).toString()); const auto list = m_storage->scheduleList(QString(), Schedule::Type::Any, Schedule::Occurrence::Any, Schedule::PaymentType::Any, QDate(), QDate(), false); QSqlQuery query2(*q); //TODO: find a way to prepare the queries outside of the loop. writeSchedule() // modifies the query passed to it, so they have to be re-prepared every pass. signalProgress(0, list.count(), "Writing Schedules..."); foreach (const MyMoneySchedule& it, list) { query.prepare(m_db.m_tables["kmmSchedules"].updateString()); query2.prepare(m_db.m_tables["kmmSchedules"].insertString()); bool insert = true; if (dbList.contains(it.id())) { dbList.removeAll(it.id()); insert = false; writeSchedule(it, query, insert); } else { writeSchedule(it, query2, insert); } signalProgress(++m_schedules, 0); } if (!dbList.isEmpty()) { foreach (const QString& it, dbList) { deleteSchedule(it); } } } void writeSecurities() { Q_Q(MyMoneyStorageSql); // first, get a list of what's on the database (see writeInstitutions) QList dbList; QSqlQuery query(*q); QSqlQuery query2(*q); query.prepare("SELECT id FROM kmmSecurities;"); if (!query.exec()) throw MYMONEYEXCEPTIONSQL("building security list"); // krazy:exclude=crashy while (query.next()) dbList.append(query.value(0).toString()); const QList securityList = m_storage->securityList(); signalProgress(0, securityList.count(), "Writing Securities..."); query.prepare(m_db.m_tables["kmmSecurities"].updateString()); query2.prepare(m_db.m_tables["kmmSecurities"].insertString()); foreach (const MyMoneySecurity& it, securityList) { if (dbList.contains(it.id())) { dbList.removeAll(it.id()); writeSecurity(it, query); } else { writeSecurity(it, query2); } signalProgress(++m_securities, 0); } if (!dbList.isEmpty()) { QVariantList idList; // qCopy segfaults here, so do it with a hand-rolled loop foreach (const QString& it, dbList) { idList << it; } query.prepare("DELETE FROM kmmSecurities WHERE id = :id"); query2.prepare("DELETE FROM kmmPrices WHERE fromId = :id OR toId = :id"); query.bindValue(":id", idList); if (!query.execBatch()) throw MYMONEYEXCEPTIONSQL("deleting Security"); query2.bindValue(":fromId", idList); query2.bindValue(":toId", idList); if (!query2.execBatch()) throw MYMONEYEXCEPTIONSQL("deleting Security"); deleteKeyValuePairs("SECURITY", idList); } } void writePrices() { Q_Q(MyMoneyStorageSql); // due to difficulties in matching and determining deletes // easiest way is to delete all and re-insert QSqlQuery query(*q); query.prepare("DELETE FROM kmmPrices"); if (!query.exec()) throw MYMONEYEXCEPTIONSQL("deleting Prices"); // krazy:exclude=crashy m_prices = 0; const MyMoneyPriceList list = m_storage->priceList(); signalProgress(0, list.count(), "Writing Prices..."); MyMoneyPriceList::ConstIterator it; for (it = list.constBegin(); it != list.constEnd(); ++it) { writePricePair(*it); } } void writeCurrencies() { Q_Q(MyMoneyStorageSql); // first, get a list of what's on the database (see writeInstitutions) QList dbList; QSqlQuery query(*q); QSqlQuery query2(*q); query.prepare("SELECT ISOCode FROM kmmCurrencies;"); if (!query.exec()) throw MYMONEYEXCEPTIONSQL("building Currency list"); // krazy:exclude=crashy while (query.next()) dbList.append(query.value(0).toString()); const QList currencyList = m_storage->currencyList(); signalProgress(0, currencyList.count(), "Writing Currencies..."); query.prepare(m_db.m_tables["kmmCurrencies"].updateString()); query2.prepare(m_db.m_tables["kmmCurrencies"].insertString()); foreach (const MyMoneySecurity& it, currencyList) { if (dbList.contains(it.id())) { dbList.removeAll(it.id()); writeCurrency(it, query); } else { writeCurrency(it, query2); } signalProgress(++m_currencies, 0); } if (!dbList.isEmpty()) { QVariantList isoCodeList; query.prepare("DELETE FROM kmmCurrencies WHERE ISOCode = :ISOCode"); // qCopy segfaults here, so do it with a hand-rolled loop foreach (const QString& it, dbList) { isoCodeList << it; } query.bindValue(":ISOCode", isoCodeList); if (!query.execBatch()) throw MYMONEYEXCEPTIONSQL("deleting Currency"); } } void writeFileInfo() { Q_Q(MyMoneyStorageSql); // we have no real way of knowing when these change, so re-write them every time QVariantList kvpList; kvpList << ""; QList > pairs; pairs << m_storage->pairs(); deleteKeyValuePairs("STORAGE", kvpList); writeKeyValuePairs("STORAGE", kvpList, pairs); QSqlQuery query(*q); query.prepare("SELECT count(*) FROM kmmFileInfo;"); if (!query.exec() || !query.next()) throw MYMONEYEXCEPTIONSQL("checking fileinfo"); // krazy:exclude=crashy if (query.value(0).toInt() == 0) { // Cannot use "INSERT INTO kmmFileInfo DEFAULT VALUES;" because it is not supported by MySQL query.prepare(QLatin1String("INSERT INTO kmmFileInfo (version) VALUES (null);")); if (!query.exec()) throw MYMONEYEXCEPTIONSQL("inserting fileinfo"); // krazy:exclude=crashy } query.prepare(QLatin1String( "UPDATE kmmFileInfo SET " "version = :version, " "fixLevel = :fixLevel, " "created = :created, " "lastModified = :lastModified, " "baseCurrency = :baseCurrency, " "dateRangeStart = :dateRangeStart, " "dateRangeEnd = :dateRangeEnd, " "hiInstitutionId = :hiInstitutionId, " "hiPayeeId = :hiPayeeId, " "hiTagId = :hiTagId, " "hiAccountId = :hiAccountId, " "hiTransactionId = :hiTransactionId, " "hiScheduleId = :hiScheduleId, " "hiSecurityId = :hiSecurityId, " "hiReportId = :hiReportId, " "hiBudgetId = :hiBudgetId, " "hiOnlineJobId = :hiOnlineJobId, " "hiPayeeIdentifierId = :hiPayeeIdentifierId, " "encryptData = :encryptData, " "updateInProgress = :updateInProgress, " "logonUser = :logonUser, " "logonAt = :logonAt, " //! @todo The following updates are for backwards compatibility only //! remove backwards compatibility in a later version "institutions = :institutions, " "accounts = :accounts, " "payees = :payees, " "tags = :tags, " "transactions = :transactions, " "splits = :splits, " "securities = :securities, " "prices = :prices, " "currencies = :currencies, " "schedules = :schedules, " "reports = :reports, " "kvps = :kvps, " "budgets = :budgets; " ) ); query.bindValue(":version", m_dbVersion); query.bindValue(":fixLevel", m_storage->fileFixVersion()); query.bindValue(":created", m_storage->creationDate().toString(Qt::ISODate)); //q.bindValue(":lastModified", m_storage->lastModificationDate().toString(Qt::ISODate)); query.bindValue(":lastModified", QDate::currentDate().toString(Qt::ISODate)); query.bindValue(":baseCurrency", m_storage->pairs()["kmm-baseCurrency"]); query.bindValue(":dateRangeStart", QDate()); query.bindValue(":dateRangeEnd", QDate()); //FIXME: This modifies all m_ used in this function. // Sometimes the memory has been updated. // Should most of these be tracked in a view? // Variables actually needed are: version, fileFixVersion, creationDate, // baseCurrency, encryption, update info, and logon info. //try { //readFileInfo(); //} catch (...) { //q->startCommitUnit(Q_FUNC_INFO); //} //! @todo The following bindings are for backwards compatibility only //! remove backwards compatibility in a later version query.bindValue(":hiInstitutionId", QVariant::fromValue(q->getNextInstitutionId())); query.bindValue(":hiPayeeId", QVariant::fromValue(q->getNextPayeeId())); query.bindValue(":hiTagId", QVariant::fromValue(q->getNextTagId())); query.bindValue(":hiAccountId", QVariant::fromValue(q->getNextAccountId())); query.bindValue(":hiTransactionId", QVariant::fromValue(q->getNextTransactionId())); query.bindValue(":hiScheduleId", QVariant::fromValue(q->getNextScheduleId())); query.bindValue(":hiSecurityId", QVariant::fromValue(q->getNextSecurityId())); query.bindValue(":hiReportId", QVariant::fromValue(q->getNextReportId())); query.bindValue(":hiBudgetId", QVariant::fromValue(q->getNextBudgetId())); query.bindValue(":hiOnlineJobId", QVariant::fromValue(q->getNextOnlineJobId())); query.bindValue(":hiPayeeIdentifierId", QVariant::fromValue(q->getNextPayeeIdentifierId())); query.bindValue(":encryptData", m_encryptData); query.bindValue(":updateInProgress", "N"); query.bindValue(":logonUser", m_logonUser); query.bindValue(":logonAt", m_logonAt.toString(Qt::ISODate)); //! @todo The following bindings are for backwards compatibility only //! remove backwards compatibility in a later version query.bindValue(":institutions", (unsigned long long) m_institutions); query.bindValue(":accounts", (unsigned long long) m_accounts); query.bindValue(":payees", (unsigned long long) m_payees); query.bindValue(":tags", (unsigned long long) m_tags); query.bindValue(":transactions", (unsigned long long) m_transactions); query.bindValue(":splits", (unsigned long long) m_splits); query.bindValue(":securities", (unsigned long long) m_securities); query.bindValue(":prices", (unsigned long long) m_prices); query.bindValue(":currencies", (unsigned long long) m_currencies); query.bindValue(":schedules", (unsigned long long) m_schedules); query.bindValue(":reports", (unsigned long long) m_reports); query.bindValue(":kvps", (unsigned long long) m_kvps); query.bindValue(":budgets", (unsigned long long) m_budgets); if (!query.exec()) throw MYMONEYEXCEPTIONSQL("writing FileInfo"); // krazy:exclude=crashy } void writeReports() { Q_Q(MyMoneyStorageSql); // first, get a list of what's on the database (see writeInstitutions) QList dbList; QSqlQuery query(*q); QSqlQuery query2(*q); query.prepare("SELECT id FROM kmmReportConfig;"); if (!query.exec()) throw MYMONEYEXCEPTIONSQL("building Report list"); // krazy:exclude=crashy while (query.next()) dbList.append(query.value(0).toString()); QList list = m_storage->reportList(); signalProgress(0, list.count(), "Writing Reports..."); query.prepare(m_db.m_tables["kmmReportConfig"].updateString()); query2.prepare(m_db.m_tables["kmmReportConfig"].insertString()); foreach (const MyMoneyReport& it, list) { if (dbList.contains(it.id())) { dbList.removeAll(it.id()); writeReport(it, query); } else { writeReport(it, query2); } signalProgress(++m_reports, 0); } if (!dbList.isEmpty()) { QVariantList idList; query.prepare("DELETE FROM kmmReportConfig WHERE id = :id"); // qCopy segfaults here, so do it with a hand-rolled loop foreach (const QString& it, dbList) { idList << it; } query.bindValue(":id", idList); if (!query.execBatch()) throw MYMONEYEXCEPTIONSQL("deleting Report"); } } void writeBudgets() { Q_Q(MyMoneyStorageSql); // first, get a list of what's on the database (see writeInstitutions) QList dbList; QSqlQuery query(*q); QSqlQuery query2(*q); query.prepare("SELECT name FROM kmmBudgetConfig;"); if (!query.exec()) throw MYMONEYEXCEPTIONSQL("building Budget list"); // krazy:exclude=crashy while (query.next()) dbList.append(query.value(0).toString()); QList list = m_storage->budgetList(); signalProgress(0, list.count(), "Writing Budgets..."); query.prepare(m_db.m_tables["kmmBudgetConfig"].updateString()); query2.prepare(m_db.m_tables["kmmBudgetConfig"].insertString()); foreach (const MyMoneyBudget& it, list) { if (dbList.contains(it.name())) { dbList.removeAll(it.name()); writeBudget(it, query); } else { writeBudget(it, query2); } signalProgress(++m_budgets, 0); } if (!dbList.isEmpty()) { QVariantList idList; query.prepare("DELETE FROM kmmBudgetConfig WHERE id = :id"); // qCopy segfaults here, so do it with a hand-rolled loop foreach (const QString& it, dbList) { idList << it; } query.bindValue(":name", idList); if (!query.execBatch()) throw MYMONEYEXCEPTIONSQL("deleting Budget"); } } void writeOnlineJobs() { Q_Q(MyMoneyStorageSql); QSqlQuery query(*q); if (!query.exec("DELETE FROM kmmOnlineJobs;")) throw MYMONEYEXCEPTIONSQL("Clean kmmOnlineJobs table"); const QList jobs(m_storage->onlineJobList()); signalProgress(0, jobs.count(), i18n("Inserting online jobs.")); // Create list for onlineJobs which failed and the reason therefor QList > failedJobs; int jobCount = 0; foreach (const onlineJob& job, jobs) { try { q->addOnlineJob(job); } catch (const MyMoneyException &e) { // Do not save e as this may point to an inherited class failedJobs.append(QPair(job, e.what())); qDebug() << "Failed to save onlineJob" << job.id() << "Reson:" << e.what(); } signalProgress(++jobCount, 0); } if (!failedJobs.isEmpty()) { /** @todo Improve error message */ throw MYMONEYEXCEPTION_CSTRING("Could not save onlineJob."); } } /** @} */ /** * @name writeMethods * @{ * These methods bind the data fields of MyMoneyObjects to a given query and execute the query. * This is helpfull as the query has usually an update and a insert format. */ void writeInstitutionList(const QList& iList, QSqlQuery& query) { QVariantList idList; QVariantList nameList; QVariantList managerList; QVariantList routingCodeList; QVariantList addressStreetList; QVariantList addressCityList; QVariantList addressZipcodeList; QVariantList telephoneList; QList > kvpPairsList; foreach (const MyMoneyInstitution& i, iList) { idList << i.id(); nameList << i.name(); managerList << i.manager(); routingCodeList << i.sortcode(); addressStreetList << i.street(); addressCityList << i.city(); addressZipcodeList << i.postcode(); telephoneList << i.telephone(); kvpPairsList << i.pairs(); } query.bindValue(":id", idList); query.bindValue(":name", nameList); query.bindValue(":manager", managerList); query.bindValue(":routingCode", routingCodeList); query.bindValue(":addressStreet", addressStreetList); query.bindValue(":addressCity", addressCityList); query.bindValue(":addressZipcode", addressZipcodeList); query.bindValue(":telephone", telephoneList); if (!query.execBatch()) throw MYMONEYEXCEPTIONSQL("writing Institution"); writeKeyValuePairs("OFXSETTINGS", idList, kvpPairsList); // Set m_hiIdInstitutions to 0 to force recalculation the next time it is requested m_hiIdInstitutions = 0; } void writePayee(const MyMoneyPayee& p, QSqlQuery& query, bool isUserInfo = false) { if (isUserInfo) { query.bindValue(":id", "USER"); } else { query.bindValue(":id", p.id()); } query.bindValue(":name", p.name()); query.bindValue(":reference", p.reference()); query.bindValue(":email", p.email()); query.bindValue(":addressStreet", p.address()); query.bindValue(":addressCity", p.city()); query.bindValue(":addressZipcode", p.postcode()); query.bindValue(":addressState", p.state()); query.bindValue(":telephone", p.telephone()); query.bindValue(":notes", p.notes()); query.bindValue(":defaultAccountId", p.defaultAccountId()); bool ignoreCase; QString matchKeys; - MyMoneyPayee::payeeMatchType type = p.matchData(ignoreCase, matchKeys); + auto type = p.matchData(ignoreCase, matchKeys); query.bindValue(":matchData", static_cast(type)); if (ignoreCase) query.bindValue(":matchIgnoreCase", "Y"); else query.bindValue(":matchIgnoreCase", "N"); query.bindValue(":matchKeys", matchKeys); if (!query.exec()) // krazy:exclude=crashy throw MYMONEYEXCEPTIONSQL("writing Payee"); // krazy:exclude=crashy if (!isUserInfo) m_hiIdPayees = 0; } void writeTag(const MyMoneyTag& ta, QSqlQuery& query) { query.bindValue(":id", ta.id()); query.bindValue(":name", ta.name()); query.bindValue(":tagColor", ta.tagColor().name()); if (ta.isClosed()) query.bindValue(":closed", "Y"); else query.bindValue(":closed", "N"); query.bindValue(":notes", ta.notes()); if (!query.exec()) throw MYMONEYEXCEPTIONSQL("writing Tag"); // krazy:exclude=crashy m_hiIdTags = 0; } void writeAccountList(const QList& accList, QSqlQuery& query) { //MyMoneyMoney balance = m_storagePtr->balance(acc.id(), QDate()); QVariantList idList; QVariantList institutionIdList; QVariantList parentIdList; QVariantList lastReconciledList; QVariantList lastModifiedList; QVariantList openingDateList; QVariantList accountNumberList; QVariantList accountTypeList; QVariantList accountTypeStringList; QVariantList isStockAccountList; QVariantList accountNameList; QVariantList descriptionList; QVariantList currencyIdList; QVariantList balanceList; QVariantList balanceFormattedList; QVariantList transactionCountList; QList > pairs; QList > onlineBankingPairs; foreach (const MyMoneyAccount& a, accList) { idList << a.id(); institutionIdList << a.institutionId(); parentIdList << a.parentAccountId(); if (a.lastReconciliationDate() == QDate()) lastReconciledList << a.lastReconciliationDate(); else lastReconciledList << a.lastReconciliationDate().toString(Qt::ISODate); lastModifiedList << a.lastModified(); if (a.openingDate() == QDate()) openingDateList << a.openingDate(); else openingDateList << a.openingDate().toString(Qt::ISODate); accountNumberList << a.number(); accountTypeList << (int)a.accountType(); accountTypeStringList << MyMoneyAccount::accountTypeToString(a.accountType()); if (a.accountType() == Account::Type::Stock) isStockAccountList << "Y"; else isStockAccountList << "N"; accountNameList << a.name(); descriptionList << a.description(); currencyIdList << a.currencyId(); // This section attempts to get the balance from the database, if possible // That way, the balance fields are kept in sync. If that fails, then // It is assumed that the account actually knows its correct balance. //FIXME: Using exceptions for branching always feels like a kludge. // Look for a better way. try { MyMoneyMoney bal = m_storage->balance(a.id(), QDate()); balanceList << bal.toString(); balanceFormattedList << bal.formatMoney("", -1, false); } catch (const MyMoneyException &) { balanceList << a.balance().toString(); balanceFormattedList << a.balance().formatMoney("", -1, false); } transactionCountList << quint64(m_transactionCountMap[a.id()]); //MMAccount inherits from KVPContainer AND has a KVPContainer member //so handle both pairs << a.pairs(); onlineBankingPairs << a.onlineBankingSettings().pairs(); } query.bindValue(":id", idList); query.bindValue(":institutionId", institutionIdList); query.bindValue(":parentId", parentIdList); query.bindValue(":lastReconciled", lastReconciledList); query.bindValue(":lastModified", lastModifiedList); query.bindValue(":openingDate", openingDateList); query.bindValue(":accountNumber", accountNumberList); query.bindValue(":accountType", accountTypeList); query.bindValue(":accountTypeString", accountTypeStringList); query.bindValue(":isStockAccount", isStockAccountList); query.bindValue(":accountName", accountNameList); query.bindValue(":description", descriptionList); query.bindValue(":currencyId", currencyIdList); query.bindValue(":balance", balanceList); query.bindValue(":balanceFormatted", balanceFormattedList); query.bindValue(":transactionCount", transactionCountList); if (!query.execBatch()) throw MYMONEYEXCEPTIONSQL("writing Account"); //Add in Key-Value Pairs for accounts. writeKeyValuePairs("ACCOUNT", idList, pairs); writeKeyValuePairs("ONLINEBANKING", idList, onlineBankingPairs); m_hiIdAccounts = 0; } void writeTransaction(const QString& txId, const MyMoneyTransaction& tx, QSqlQuery& query, const QString& type) { query.bindValue(":id", txId); query.bindValue(":txType", type); query.bindValue(":postDate", tx.postDate().toString(Qt::ISODate)); query.bindValue(":memo", tx.memo()); query.bindValue(":entryDate", tx.entryDate().toString(Qt::ISODate)); query.bindValue(":currencyId", tx.commodity()); query.bindValue(":bankId", tx.bankID()); if (!query.exec()) // krazy:exclude=crashy throw MYMONEYEXCEPTIONSQL("writing Transaction"); // krazy:exclude=crashy m_txPostDate = tx.postDate(); // FIXME: TEMP till Tom puts date in split object QList splitList = tx.splits(); writeSplits(txId, type, splitList); //Add in Key-Value Pairs for transactions. QVariantList idList; idList << txId; deleteKeyValuePairs("TRANSACTION", idList); QList > pairs; pairs << tx.pairs(); writeKeyValuePairs("TRANSACTION", idList, pairs); m_hiIdTransactions = 0; } void writeSplits(const QString& txId, const QString& type, const QList& splitList) { Q_Q(MyMoneyStorageSql); // first, get a list of what's on the database (see writeInstitutions) QList dbList; QList insertList; QList updateList; QList insertIdList; QList updateIdList; QSqlQuery query(*q); query.prepare("SELECT splitId FROM kmmSplits where transactionId = :id;"); query.bindValue(":id", txId); if (!query.exec()) throw MYMONEYEXCEPTIONSQL("building Split list"); // krazy:exclude=crashy while (query.next()) dbList.append(query.value(0).toUInt()); QSqlQuery query2(*q); query.prepare(m_db.m_tables["kmmSplits"].updateString()); query2.prepare(m_db.m_tables["kmmSplits"].insertString()); auto i = 0; for (auto it = splitList.constBegin(); it != splitList.constEnd(); ++it) { if (dbList.contains(i)) { dbList.removeAll(i); updateList << *it; updateIdList << i; } else { ++m_splits; insertList << *it; insertIdList << i; } ++i; } if (!insertList.isEmpty()) { writeSplitList(txId, insertList, type, insertIdList, query2); writeTagSplitsList(txId, insertList, insertIdList); } if (!updateList.isEmpty()) { writeSplitList(txId, updateList, type, updateIdList, query); deleteTagSplitsList(txId, updateIdList); writeTagSplitsList(txId, updateList, updateIdList); } if (!dbList.isEmpty()) { QVector txIdList(dbList.count(), txId); QVariantList splitIdList; query.prepare("DELETE FROM kmmSplits WHERE transactionId = :txId AND splitId = :splitId"); // qCopy segfaults here, so do it with a hand-rolled loop foreach (int it, dbList) { splitIdList << it; } query.bindValue(":txId", txIdList.toList()); query.bindValue(":splitId", splitIdList); if (!query.execBatch()) throw MYMONEYEXCEPTIONSQL("deleting Splits"); } } void writeTagSplitsList (const QString& txId, const QList& splitList, const QList& splitIdList) { Q_Q(MyMoneyStorageSql); MyMoneyDbTransaction t(*q, Q_FUNC_INFO); QVariantList tagIdList; QVariantList txIdList; QVariantList splitIdList_TagSplits; QVariantList tagSplitsIdList; int i = 0, l = 0; foreach (const MyMoneySplit& s, splitList) { for (l = 0; l < s.tagIdList().size(); ++l) { tagIdList << s.tagIdList()[l]; splitIdList_TagSplits << splitIdList[i]; txIdList << txId; } i++; } QSqlQuery query(*q); query.prepare(m_db.m_tables["kmmTagSplits"].insertString()); query.bindValue(":tagId", tagIdList); query.bindValue(":splitId", splitIdList_TagSplits); query.bindValue(":transactionId", txIdList); if (!query.execBatch()) throw MYMONEYEXCEPTIONSQL("writing tagSplits"); } void writeSplitList (const QString& txId, const QList& splitList, const QString& type, const QList& splitIdList, QSqlQuery& query) { QVariantList txIdList; QVariantList typeList; QVariantList payeeIdList; QVariantList reconcileDateList; QVariantList actionList; QVariantList reconcileFlagList; QVariantList valueList; QVariantList valueFormattedList; QVariantList sharesList; QVariantList sharesFormattedList; QVariantList priceList; QVariantList priceFormattedList; QVariantList memoList; QVariantList accountIdList; QVariantList costCenterIdList; QVariantList checkNumberList; QVariantList postDateList; QVariantList bankIdList; QVariantList kvpIdList; QList > kvpPairsList; int i = 0; foreach (const MyMoneySplit& s, splitList) { txIdList << txId; typeList << type; payeeIdList << s.payeeId(); if (s.reconcileDate() == QDate()) reconcileDateList << s.reconcileDate(); else reconcileDateList << s.reconcileDate().toString(Qt::ISODate); actionList << s.action(); reconcileFlagList << (int)s.reconcileFlag(); valueList << s.value().toString(); valueFormattedList << s.value().formatMoney("", -1, false).replace(QChar(','), QChar('.')); sharesList << s.shares().toString(); MyMoneyAccount acc = m_storage->account(s.accountId()); MyMoneySecurity sec = m_storage->security(acc.currencyId()); sharesFormattedList << s.price(). formatMoney("", MyMoneyMoney::denomToPrec(sec.smallestAccountFraction()), false). replace(QChar(','), QChar('.')); MyMoneyMoney price = s.actualPrice(); if (!price.isZero()) { priceList << price.toString(); priceFormattedList << price.formatMoney ("", sec.pricePrecision(), false) .replace(QChar(','), QChar('.')); } else { priceList << QString(); priceFormattedList << QString(); } memoList << s.memo(); accountIdList << s.accountId(); costCenterIdList << s.costCenterId(); checkNumberList << s.number(); postDateList << m_txPostDate.toString(Qt::ISODate); // FIXME: when Tom puts date into split object bankIdList << s.bankID(); kvpIdList << QString(txId + QString::number(splitIdList[i])); kvpPairsList << s.pairs(); ++i; } query.bindValue(":transactionId", txIdList); query.bindValue(":txType", typeList); QVariantList iList; // qCopy segfaults here, so do it with a hand-rolled loop foreach (int it_s, splitIdList) { iList << it_s; } query.bindValue(":splitId", iList); query.bindValue(":payeeId", payeeIdList); query.bindValue(":reconcileDate", reconcileDateList); query.bindValue(":action", actionList); query.bindValue(":reconcileFlag", reconcileFlagList); query.bindValue(":value", valueList); query.bindValue(":valueFormatted", valueFormattedList); query.bindValue(":shares", sharesList); query.bindValue(":sharesFormatted", sharesFormattedList); query.bindValue(":price", priceList); query.bindValue(":priceFormatted", priceFormattedList); query.bindValue(":memo", memoList); query.bindValue(":accountId", accountIdList); query.bindValue(":costCenterId", costCenterIdList); query.bindValue(":checkNumber", checkNumberList); query.bindValue(":postDate", postDateList); query.bindValue(":bankId", bankIdList); if (!query.execBatch()) throw MYMONEYEXCEPTIONSQL("writing Split"); deleteKeyValuePairs("SPLIT", kvpIdList); writeKeyValuePairs("SPLIT", kvpIdList, kvpPairsList); } void writeSchedule(const MyMoneySchedule& sch, QSqlQuery& query, bool insert) { query.bindValue(":id", sch.id()); query.bindValue(":name", sch.name()); query.bindValue(":type", (int)sch.type()); query.bindValue(":typeString", MyMoneySchedule::scheduleTypeToString(sch.type())); query.bindValue(":occurence", (int)sch.occurrencePeriod()); // krazy:exclude=spelling query.bindValue(":occurenceMultiplier", sch.occurrenceMultiplier()); // krazy:exclude=spelling query.bindValue(":occurenceString", sch.occurrenceToString()); // krazy:exclude=spelling query.bindValue(":paymentType", (int)sch.paymentType()); query.bindValue(":paymentTypeString", MyMoneySchedule::paymentMethodToString(sch.paymentType())); query.bindValue(":startDate", sch.startDate().toString(Qt::ISODate)); query.bindValue(":endDate", sch.endDate().toString(Qt::ISODate)); if (sch.isFixed()) { query.bindValue(":fixed", "Y"); } else { query.bindValue(":fixed", "N"); } if (sch.lastDayInMonth()) { query.bindValue(":lastDayInMonth", "Y"); } else { query.bindValue(":lastDayInMonth", "N"); } if (sch.autoEnter()) { query.bindValue(":autoEnter", "Y"); } else { query.bindValue(":autoEnter", "N"); } query.bindValue(":lastPayment", sch.lastPayment()); query.bindValue(":nextPaymentDue", sch.nextDueDate().toString(Qt::ISODate)); query.bindValue(":weekendOption", (int)sch.weekendOption()); query.bindValue(":weekendOptionString", MyMoneySchedule::weekendOptionToString(sch.weekendOption())); if (!query.exec()) throw MYMONEYEXCEPTIONSQL("writing Schedules"); // krazy:exclude=crashy //store the payment history for this scheduled task. //easiest way is to delete all and re-insert; it's not a high use table query.prepare("DELETE FROM kmmSchedulePaymentHistory WHERE schedId = :id;"); query.bindValue(":id", sch.id()); if (!query.exec()) throw MYMONEYEXCEPTIONSQL("deleting Schedule Payment History"); // krazy:exclude=crashy query.prepare(m_db.m_tables["kmmSchedulePaymentHistory"].insertString()); foreach (const QDate& it, sch.recordedPayments()) { query.bindValue(":schedId", sch.id()); query.bindValue(":payDate", it.toString(Qt::ISODate)); if (!query.exec()) throw MYMONEYEXCEPTIONSQL("writing Schedule Payment History"); // krazy:exclude=crashy } //store the transaction data for this task. if (!insert) { query.prepare(m_db.m_tables["kmmTransactions"].updateString()); } else { query.prepare(m_db.m_tables["kmmTransactions"].insertString()); } writeTransaction(sch.id(), sch.transaction(), query, "S"); //FIXME: enable when schedules have KVPs. //Add in Key-Value Pairs for transactions. //deleteKeyValuePairs("SCHEDULE", sch.id()); //writeKeyValuePairs("SCHEDULE", sch.id(), sch.pairs()); } void writeSecurity(const MyMoneySecurity& security, QSqlQuery& query) { query.bindValue(":id", security.id()); query.bindValue(":name", security.name()); query.bindValue(":symbol", security.tradingSymbol()); query.bindValue(":type", static_cast(security.securityType())); query.bindValue(":typeString", MyMoneySecurity::securityTypeToString(security.securityType())); query.bindValue(":roundingMethod", static_cast(security.roundingMethod())); query.bindValue(":smallestAccountFraction", security.smallestAccountFraction()); query.bindValue(":pricePrecision", security.pricePrecision()); query.bindValue(":tradingCurrency", security.tradingCurrency()); query.bindValue(":tradingMarket", security.tradingMarket()); if (!query.exec()) throw MYMONEYEXCEPTIONSQL("writing Securities"); // krazy:exclude=crashy //Add in Key-Value Pairs for security QVariantList idList; idList << security.id(); QList > pairs; pairs << security.pairs(); writeKeyValuePairs("SECURITY", idList, pairs); m_hiIdSecurities = 0; } void writePricePair(const MyMoneyPriceEntries& p) { MyMoneyPriceEntries::ConstIterator it; for (it = p.constBegin(); it != p.constEnd(); ++it) { writePrice(*it); signalProgress(++m_prices, 0); } } void writePrice(const MyMoneyPrice& p) { Q_Q(MyMoneyStorageSql); QSqlQuery query(*q); query.prepare(m_db.m_tables["kmmPrices"].insertString()); query.bindValue(":fromId", p.from()); query.bindValue(":toId", p.to()); query.bindValue(":priceDate", p.date().toString(Qt::ISODate)); query.bindValue(":price", p.rate(QString()).toString()); query.bindValue(":priceFormatted", p.rate(QString()).formatMoney("", 2)); query.bindValue(":priceSource", p.source()); if (!query.exec()) throw MYMONEYEXCEPTIONSQL("writing Prices"); // krazy:exclude=crashy } void writeCurrency(const MyMoneySecurity& currency, QSqlQuery& query) { query.bindValue(":ISOcode", currency.id()); query.bindValue(":name", currency.name()); query.bindValue(":type", static_cast(currency.securityType())); query.bindValue(":typeString", MyMoneySecurity::securityTypeToString(currency.securityType())); // writing the symbol as three short ints is a PITA, but the // problem is that database drivers have incompatible ways of declaring UTF8 QString symbol = currency.tradingSymbol() + " "; const ushort* symutf = symbol.utf16(); //int ix = 0; //while (x[ix] != '\0') qDebug() << "symbol" << symbol << "char" << ix << "=" << x[ix++]; //q.bindValue(":symbol1", symbol.mid(0,1).unicode()->unicode()); //q.bindValue(":symbol2", symbol.mid(1,1).unicode()->unicode()); //q.bindValue(":symbol3", symbol.mid(2,1).unicode()->unicode()); query.bindValue(":symbol1", symutf[0]); query.bindValue(":symbol2", symutf[1]); query.bindValue(":symbol3", symutf[2]); query.bindValue(":symbolString", symbol); query.bindValue(":smallestCashFraction", currency.smallestCashFraction()); query.bindValue(":smallestAccountFraction", currency.smallestAccountFraction()); query.bindValue(":pricePrecision", currency.pricePrecision()); if (!query.exec()) throw MYMONEYEXCEPTIONSQL("writing Currencies"); // krazy:exclude=crashy } void writeReport(const MyMoneyReport& rep, QSqlQuery& query) { QDomDocument d; // create a dummy XML document QDomElement e = d.createElement("REPORTS"); d.appendChild(e); rep.writeXML(d, e); // write the XML to document query.bindValue(":id", rep.id()); query.bindValue(":name", rep.name()); query.bindValue(":XML", d.toString()); if (!query.exec()) throw MYMONEYEXCEPTIONSQL("writing Reports"); // krazy:exclude=crashy } void writeBudget(const MyMoneyBudget& bud, QSqlQuery& query) { QDomDocument d; // create a dummy XML document QDomElement e = d.createElement("BUDGETS"); d.appendChild(e); bud.writeXML(d, e); // write the XML to document query.bindValue(":id", bud.id()); query.bindValue(":name", bud.name()); query.bindValue(":start", bud.budgetStart()); query.bindValue(":XML", d.toString()); if (!query.exec()) // krazy:exclude=crashy throw MYMONEYEXCEPTIONSQL("writing Budgets"); // krazy:exclude=crashy } void writeKeyValuePairs(const QString& kvpType, const QVariantList& kvpId, const QList >& pairs) { Q_Q(MyMoneyStorageSql); if (pairs.empty()) return; QVariantList type; QVariantList id; QVariantList key; QVariantList value; int pairCount = 0; for (int i = 0; i < kvpId.size(); ++i) { QMap::ConstIterator it; for (it = pairs[i].constBegin(); it != pairs[i].constEnd(); ++it) { type << kvpType; id << kvpId[i]; key << it.key(); value << it.value(); } pairCount += pairs[i].size(); } QSqlQuery query(*q); query.prepare(m_db.m_tables["kmmKeyValuePairs"].insertString()); query.bindValue(":kvpType", type); query.bindValue(":kvpId", id); query.bindValue(":kvpKey", key); query.bindValue(":kvpData", value); if (!query.execBatch()) throw MYMONEYEXCEPTIONSQL("writing KVP"); m_kvps += pairCount; } void writeOnlineJob(const onlineJob& job, QSqlQuery& query) { Q_ASSERT(job.id().startsWith('O')); query.bindValue(":id", job.id()); query.bindValue(":type", job.taskIid()); query.bindValue(":jobSend", job.sendDate()); query.bindValue(":bankAnswerDate", job.bankAnswerDate()); switch (job.bankAnswerState()) { case onlineJob::acceptedByBank: query.bindValue(":state", QLatin1String("acceptedByBank")); break; case onlineJob::rejectedByBank: query.bindValue(":state", QLatin1String("rejectedByBank")); break; case onlineJob::abortedByUser: query.bindValue(":state", QLatin1String("abortedByUser")); break; case onlineJob::sendingError: query.bindValue(":state", QLatin1String("sendingError")); break; case onlineJob::noBankAnswer: default: query.bindValue(":state", QLatin1String("noBankAnswer")); } query.bindValue(":locked", QVariant::fromValue(job.isLocked() ? QLatin1String("Y") : QLatin1String("N"))); } void writePayeeIdentifier(const payeeIdentifier& pid, QSqlQuery& query) { query.bindValue(":id", pid.idString()); query.bindValue(":type", pid.iid()); if (!query.exec()) { // krazy:exclude=crashy qWarning() << buildError(query, Q_FUNC_INFO, QString("modifying payeeIdentifier")); throw MYMONEYEXCEPTIONSQL("modifying payeeIdentifier"); // krazy:exclude=crashy } } /** @} */ /** * @name readMethods * @{ */ void readFileInfo() { Q_Q(MyMoneyStorageSql); signalProgress(0, 1, QObject::tr("Loading file information...")); QSqlQuery query(*q); query.prepare( "SELECT " " created, lastModified, " " encryptData, logonUser, logonAt, " " (SELECT count(*) FROM kmmInstitutions) AS institutions, " " (SELECT count(*) from kmmAccounts) AS accounts, " " (SELECT count(*) FROM kmmCurrencies) AS currencies, " " (SELECT count(*) FROM kmmPayees) AS payees, " " (SELECT count(*) FROM kmmTags) AS tags, " " (SELECT count(*) FROM kmmTransactions) AS transactions, " " (SELECT count(*) FROM kmmSplits) AS splits, " " (SELECT count(*) FROM kmmSecurities) AS securities, " " (SELECT count(*) FROM kmmCurrencies) AS currencies, " " (SELECT count(*) FROM kmmSchedules) AS schedules, " " (SELECT count(*) FROM kmmPrices) AS prices, " " (SELECT count(*) FROM kmmKeyValuePairs) AS kvps, " " (SELECT count(*) FROM kmmReportConfig) AS reports, " " (SELECT count(*) FROM kmmBudgetConfig) AS budgets, " " (SELECT count(*) FROM kmmOnlineJobs) AS onlineJobs, " " (SELECT count(*) FROM kmmPayeeIdentifier) AS payeeIdentifier " "FROM kmmFileInfo;" ); if (!query.exec()) throw MYMONEYEXCEPTIONSQL("reading FileInfo"); // krazy:exclude=crashy if (!query.next()) throw MYMONEYEXCEPTIONSQL("retrieving FileInfo"); QSqlRecord rec = query.record(); m_storage->setCreationDate(GETDATE(rec.indexOf("created"))); m_storage->setLastModificationDate(GETDATE(rec.indexOf("lastModified"))); m_institutions = (ulong) GETULL(rec.indexOf("institutions")); m_accounts = (ulong) GETULL(rec.indexOf("accounts")); m_payees = (ulong) GETULL(rec.indexOf("payees")); m_tags = (ulong) GETULL(rec.indexOf("tags")); m_transactions = (ulong) GETULL(rec.indexOf("transactions")); m_splits = (ulong) GETULL(rec.indexOf("splits")); m_securities = (ulong) GETULL(rec.indexOf("securities")); m_currencies = (ulong) GETULL(rec.indexOf("currencies")); m_schedules = (ulong) GETULL(rec.indexOf("schedules")); m_prices = (ulong) GETULL(rec.indexOf("prices")); m_kvps = (ulong) GETULL(rec.indexOf("kvps")); m_reports = (ulong) GETULL(rec.indexOf("reports")); m_budgets = (ulong) GETULL(rec.indexOf("budgets")); m_onlineJobs = (ulong) GETULL(rec.indexOf("onlineJobs")); m_payeeIdentifier = (ulong) GETULL(rec.indexOf("payeeIdentifier")); m_encryptData = GETSTRING(rec.indexOf("encryptData")); m_logonUser = GETSTRING(rec.indexOf("logonUser")); m_logonAt = GETDATETIME(rec.indexOf("logonAt")); signalProgress(1, 0); m_storage->setPairs(readKeyValuePairs("STORAGE", QString("")).pairs()); } void readLogonData(); void readUserInformation(); void readInstitutions() { Q_Q(MyMoneyStorageSql); try { QMap iList = q->fetchInstitutions(); m_storage->loadInstitutions(iList); readFileInfo(); } catch (const MyMoneyException &) { throw; } } void readAccounts() { Q_Q(MyMoneyStorageSql); m_storage->loadAccounts(q->fetchAccounts()); } void readTransactions(const QString& tidList, const QString& dateClause) { Q_Q(MyMoneyStorageSql); try { m_storage->loadTransactions(q->fetchTransactions(tidList, dateClause)); } catch (const MyMoneyException &) { throw; } } void readTransactions() { readTransactions(QString(), QString()); } void readSplit(MyMoneySplit& s, const QSqlQuery& query) const { Q_Q(const MyMoneyStorageSql); // Set these up as statics, since the field numbers should not change // during execution. static const MyMoneyDbTable& t = m_db.m_tables["kmmSplits"]; static const int splitIdCol = t.fieldNumber("splitId"); static const int transactionIdCol = t.fieldNumber("transactionId"); static const int payeeIdCol = t.fieldNumber("payeeId"); static const int reconcileDateCol = t.fieldNumber("reconcileDate"); static const int actionCol = t.fieldNumber("action"); static const int reconcileFlagCol = t.fieldNumber("reconcileFlag"); static const int valueCol = t.fieldNumber("value"); static const int sharesCol = t.fieldNumber("shares"); static const int priceCol = t.fieldNumber("price"); static const int memoCol = t.fieldNumber("memo"); static const int accountIdCol = t.fieldNumber("accountId"); static const int costCenterIdCol = t.fieldNumber("costCenterId"); static const int checkNumberCol = t.fieldNumber("checkNumber"); // static const int postDateCol = t.fieldNumber("postDate"); // FIXME - when Tom puts date into split object static const int bankIdCol = t.fieldNumber("bankId"); s.clearId(); QList tagIdList; QSqlQuery query1(*const_cast (q)); query1.prepare("SELECT tagId from kmmTagSplits where splitId = :id and transactionId = :transactionId"); query1.bindValue(":id", GETSTRING(splitIdCol)); query1.bindValue(":transactionId", GETSTRING(transactionIdCol)); if (!query1.exec()) throw MYMONEYEXCEPTIONSQL("reading tagId in Split"); // krazy:exclude=crashy while (query1.next()) tagIdList << query1.value(0).toString(); s.setTagIdList(tagIdList); s.setPayeeId(GETSTRING(payeeIdCol)); s.setReconcileDate(GETDATE(reconcileDateCol)); s.setAction(GETSTRING(actionCol)); s.setReconcileFlag(static_cast(GETINT(reconcileFlagCol))); s.setValue(MyMoneyMoney(MyMoneyUtils::QStringEmpty(GETSTRING(valueCol)))); s.setShares(MyMoneyMoney(MyMoneyUtils::QStringEmpty(GETSTRING(sharesCol)))); s.setPrice(MyMoneyMoney(MyMoneyUtils::QStringEmpty(GETSTRING(priceCol)))); s.setMemo(GETSTRING(memoCol)); s.setAccountId(GETSTRING(accountIdCol)); s.setCostCenterId(GETSTRING(costCenterIdCol)); s.setNumber(GETSTRING(checkNumberCol)); //s.setPostDate(GETDATETIME(postDateCol)); // FIXME - when Tom puts date into split object s.setBankID(GETSTRING(bankIdCol)); return; } const MyMoneyKeyValueContainer readKeyValuePairs(const QString& kvpType, const QString& kvpId) const { Q_Q(const MyMoneyStorageSql); MyMoneyKeyValueContainer list; QSqlQuery query(*const_cast (q)); query.prepare("SELECT kvpKey, kvpData from kmmKeyValuePairs where kvpType = :type and kvpId = :id;"); query.bindValue(":type", kvpType); query.bindValue(":id", kvpId); if (!query.exec()) throw MYMONEYEXCEPTIONSQL(QString::fromLatin1("reading Kvp for %1 %2").arg(kvpType) // krazy:exclude=crashy .arg(kvpId)); while (query.next()) list.setValue(query.value(0).toString(), query.value(1).toString()); return (list); } const QHash readKeyValuePairs(const QString& kvpType, const QStringList& kvpIdList) const { Q_Q(const MyMoneyStorageSql); QHash retval; QSqlQuery query(*const_cast (q)); QString idList; if (!kvpIdList.empty()) { idList = QString(" and kvpId IN ('%1')").arg(kvpIdList.join("', '")); } QString sQuery = QString("SELECT kvpId, kvpKey, kvpData from kmmKeyValuePairs where kvpType = :type %1 order by kvpId;").arg(idList); query.prepare(sQuery); query.bindValue(":type", kvpType); if (!query.exec()) throw MYMONEYEXCEPTIONSQL(QString::fromLatin1("reading Kvp List for %1").arg(kvpType)); // krazy:exclude=crashy // Reserve enough space for all values. retval.reserve(kvpIdList.size()); // The loop below is designed to limit the number of calls to // QHash::operator[] in order to speed up calls to this function. This // assumes that QString::operator== is faster. /* if (q.next()) { QString oldkey = q.value(0).toString(); MyMoneyKeyValueContainer& kvpc = retval[oldkey]; kvpc.setValue(q.value(1).toString(), q.value(2).toString()); while (q.next()) { if (q.value(0).toString() != oldkey) { oldkey = q.value(0).toString(); kvpc = retval[oldkey]; } kvpc.setValue(q.value(1).toString(), q.value(2).toString()); } } */ while (query.next()) { retval[query.value(0).toString()].setValue(query.value(1).toString(), query.value(2).toString()); } return (retval); } void readSchedules() { Q_Q(MyMoneyStorageSql); try { m_storage->loadSchedules(q->fetchSchedules()); } catch (const MyMoneyException &) { throw; } } void readSecurities() { Q_Q(MyMoneyStorageSql); try { m_storage->loadSecurities(q->fetchSecurities()); } catch (const MyMoneyException &) { throw; } } void readPrices() { // try { // m_storage->addPrice(MyMoneyPrice(from, to, date, rate, source)); // } catch (const MyMoneyException &) { // throw; // } } void readCurrencies() { Q_Q(MyMoneyStorageSql); try { m_storage->loadCurrencies(q->fetchCurrencies()); } catch (const MyMoneyException &) { throw; } } void readReports() { Q_Q(MyMoneyStorageSql); try { m_storage->loadReports(q->fetchReports()); } catch (const MyMoneyException &) { throw; } } void readBudgets() { Q_Q(MyMoneyStorageSql); m_storage->loadBudgets(q->fetchBudgets()); } void readOnlineJobs() { Q_Q(MyMoneyStorageSql); m_storage->loadOnlineJobs(q->fetchOnlineJobs()); } /** @} */ void deleteTransaction(const QString& id) { Q_Q(MyMoneyStorageSql); MyMoneyDbTransaction t(*q, Q_FUNC_INFO); QSqlQuery query(*q); QVariantList idList; idList << id; query.prepare("DELETE FROM kmmSplits WHERE transactionId = :transactionId;"); query.bindValue(":transactionId", idList); if (!query.execBatch()) throw MYMONEYEXCEPTIONSQL("deleting Splits"); query.prepare("DELETE FROM kmmKeyValuePairs WHERE kvpType = 'SPLIT' " "AND kvpId LIKE '?%'"); query.bindValue(1, idList); if (!query.execBatch()) throw MYMONEYEXCEPTIONSQL("deleting Splits KVP"); m_splits -= query.numRowsAffected(); deleteKeyValuePairs("TRANSACTION", idList); query.prepare(m_db.m_tables["kmmTransactions"].deleteString()); query.bindValue(":id", idList); if (!query.execBatch()) throw MYMONEYEXCEPTIONSQL("deleting Transaction"); } void deleteTagSplitsList(const QString& txId, const QList& splitIdList) { Q_Q(MyMoneyStorageSql); MyMoneyDbTransaction t(*q, Q_FUNC_INFO); QVariantList iList; QVariantList transactionIdList; // qCopy segfaults here, so do it with a hand-rolled loop foreach (int it_s, splitIdList) { iList << it_s; transactionIdList << txId; } QSqlQuery query(*q); query.prepare("DELETE FROM kmmTagSplits WHERE transactionId = :transactionId AND splitId = :splitId"); query.bindValue(":splitId", iList); query.bindValue(":transactionId", transactionIdList); if (!query.execBatch()) throw MYMONEYEXCEPTIONSQL("deleting tagSplits"); } void deleteSchedule(const QString& id) { Q_Q(MyMoneyStorageSql); deleteTransaction(id); QSqlQuery query(*q); query.prepare("DELETE FROM kmmSchedulePaymentHistory WHERE schedId = :id"); query.bindValue(":id", id); if (!query.exec()) throw MYMONEYEXCEPTIONSQL("deleting Schedule Payment History"); // krazy:exclude=crashy query.prepare(m_db.m_tables["kmmSchedules"].deleteString()); query.bindValue(":id", id); if (!query.exec()) throw MYMONEYEXCEPTIONSQL("deleting Schedule"); // krazy:exclude=crashy //FIXME: enable when schedules have KVPs. //deleteKeyValuePairs("SCHEDULE", id); } void deleteKeyValuePairs(const QString& kvpType, const QVariantList& idList) { Q_Q(MyMoneyStorageSql); QSqlQuery query(*q); query.prepare("DELETE FROM kmmKeyValuePairs WHERE kvpType = :kvpType AND kvpId = :kvpId;"); QVariantList typeList; for (int i = 0; i < idList.size(); ++i) { typeList << kvpType; } query.bindValue(":kvpType", typeList); query.bindValue(":kvpId", idList); if (!query.execBatch()) { QString idString; for (int i = 0; i < idList.size(); ++i) { idString.append(idList[i].toString() + ' '); } throw MYMONEYEXCEPTIONSQL(QString::fromLatin1("deleting kvp for %1 %2").arg(kvpType).arg(idString)); } m_kvps -= query.numRowsAffected(); } ulong calcHighId(ulong i, const QString& id) { QString nid = id; ulong high = (ulong) nid.remove(QRegExp("[A-Z]*")).toULongLong(); return std::max(high, i); } void setVersion(const QString& version); int splitState(const TransactionFilter::State& state) const { auto rc = (int)Split::State::NotReconciled; switch (state) { default: case TransactionFilter::State::NotReconciled: break; case TransactionFilter::State::Cleared: rc = (int)Split::State::Cleared; break; case TransactionFilter::State::Reconciled: rc = (int)Split::State::Reconciled; break; case TransactionFilter::State::Frozen: rc = (int)Split::State::Frozen; break; } return rc; } QDate getDate(const QString& date) const { return (date.isNull() ? QDate() : QDate::fromString(date, Qt::ISODate)); } QDateTime getDateTime(const QString& date) const { return (date.isNull() ? QDateTime() : QDateTime::fromString(date, Qt::ISODate)); } bool fileExists(const QString& dbName) { QFile f(dbName); if (!f.exists()) { m_error = i18n("SQLite file %1 does not exist", dbName); return (false); } return (true); } /** @brief a function to build a comprehensive error message for an SQL error */ QString& buildError(const QSqlQuery& query, const QString& function, const QString& messageb) const { Q_Q(const MyMoneyStorageSql); return (buildError(query, function, messageb, q)); } QString& buildError(const QSqlQuery& query, const QString& function, const QString& message, const QSqlDatabase* db) const { Q_Q(const MyMoneyStorageSql); QString s = QString("Error in function %1 : %2").arg(function).arg(message); s += QString("\nDriver = %1, Host = %2, User = %3, Database = %4") .arg(db->driverName()).arg(db->hostName()).arg(db->userName()).arg(db->databaseName()); QSqlError e = db->lastError(); s += QString("\nDriver Error: %1").arg(e.driverText()); s += QString("\nDatabase Error No %1: %2").arg(e.number()).arg(e.databaseText()); s += QString("\nText: %1").arg(e.text()); s += QString("\nError type %1").arg(e.type()); e = query.lastError(); s += QString("\nExecuted: %1").arg(query.executedQuery()); s += QString("\nQuery error No %1: %2").arg(e.number()).arg(e.text()); s += QString("\nError type %1").arg(e.type()); const_cast (q)->d_func()->m_error = s; qDebug("%s", qPrintable(s)); const_cast (q)->cancelCommitUnit(function); return (const_cast (q)->d_func()->m_error); } /** * MyMoneyStorageSql create database * * @param url pseudo-URL of database to be opened * * @return true - creation successful * @return false - could not create * */ bool createDatabase(const QUrl &url) { Q_Q(MyMoneyStorageSql); int rc = true; if (!m_driver->requiresCreation()) return(true); // not needed for sqlite QString dbName = url.path().right(url.path().length() - 1); // remove separator slash if (!m_driver->canAutocreate()) { m_error = i18n("Automatic database creation for type %1 is not currently implemented.\n" "Please create database %2 manually", q->driverName(), dbName); return (false); } // create the database (only works for mysql and postgre at present) { // for this code block, see QSqlDatabase API re removeDatabase QSqlDatabase maindb = QSqlDatabase::addDatabase(q->driverName(), "main"); maindb.setDatabaseName(m_driver->defaultDbName()); maindb.setHostName(url.host()); maindb.setUserName(url.userName()); maindb.setPassword(url.password()); if (!maindb.open()) { throw MYMONEYEXCEPTION(QString::fromLatin1("opening database %1 in function %2") .arg(maindb.databaseName()).arg(Q_FUNC_INFO)); } else { QSqlQuery qm(maindb); QString qs = m_driver->createDbString(dbName) + ';'; if (!qm.exec(qs)) { // krazy:exclude=crashy buildError(qm, Q_FUNC_INFO, i18n("Error in create database %1; do you have create permissions?", dbName), &maindb); rc = false; } maindb.close(); } } QSqlDatabase::removeDatabase("main"); return (rc); } int upgradeDb() { Q_Q(MyMoneyStorageSql); //signalProgress(0, 1, QObject::tr("Upgrading database...")); QSqlQuery query(*q); query.prepare("SELECT version FROM kmmFileInfo;"); if (!query.exec() || !query.next()) { // krazy:exclude=crashy if (!m_newDatabase) { buildError(query, Q_FUNC_INFO, "Error retrieving file info (version)"); return(1); } else { m_dbVersion = m_db.currentVersion(); m_storage->setFileFixVersion(m_storage->currentFixVersion()); QSqlQuery query2(*q); query2.prepare("UPDATE kmmFileInfo SET version = :version, \ fixLevel = :fixLevel;"); query2.bindValue(":version", m_dbVersion); query2.bindValue(":fixLevel", m_storage->currentFixVersion()); if (!query2.exec()) { // krazy:exclude=crashy buildError(query2, Q_FUNC_INFO, "Error updating file info(version)"); return(1); } return (0); } } // prior to dbv6, 'version' format was 'dbversion.fixLevel+1' // as of dbv6, these are separate fields QString version = query.value(0).toString(); if (version.contains('.')) { m_dbVersion = query.value(0).toString().section('.', 0, 0).toUInt(); m_storage->setFileFixVersion(query.value(0).toString().section('.', 1, 1).toUInt() - 1); } else { m_dbVersion = version.toUInt(); query.prepare("SELECT fixLevel FROM kmmFileInfo;"); if (!query.exec() || !query.next()) { // krazy:exclude=crashy buildError(query, Q_FUNC_INFO, "Error retrieving file info (fixLevel)"); return(1); } m_storage->setFileFixVersion(query.value(0).toUInt()); } if (m_dbVersion == m_db.currentVersion()) return 0; int rc = 0; // Drop VIEWs QStringList lowerTables = tables(QSql::AllTables); for (QStringList::iterator i = lowerTables.begin(); i != lowerTables.end(); ++i) { (*i) = (*i).toLower(); } for (QMap::ConstIterator tt = m_db.viewBegin(); tt != m_db.viewEnd(); ++tt) { if (lowerTables.contains(tt.key().toLower())) { if (!query.exec("DROP VIEW " + tt.value().name() + ';')) // krazy:exclude=crashy throw MYMONEYEXCEPTIONSQL(QString::fromLatin1("dropping view %1").arg(tt.key())); } } while ((m_dbVersion < m_db.currentVersion()) && (rc == 0)) { switch (m_dbVersion) { case 0: if ((rc = upgradeToV1()) != 0) return (1); ++m_dbVersion; break; case 1: if ((rc = upgradeToV2()) != 0) return (1); ++m_dbVersion; break; case 2: if ((rc = upgradeToV3()) != 0) return (1); ++m_dbVersion; break; case 3: if ((rc = upgradeToV4()) != 0) return (1); ++m_dbVersion; break; case 4: if ((rc = upgradeToV5()) != 0) return (1); ++m_dbVersion; break; case 5: if ((rc = upgradeToV6()) != 0) return (1); ++m_dbVersion; break; case 6: if ((rc = upgradeToV7()) != 0) return (1); ++m_dbVersion; break; case 7: if ((rc = upgradeToV8()) != 0) return (1); ++m_dbVersion; break; case 8: if ((rc = upgradeToV9()) != 0) return (1); ++m_dbVersion; break; case 9: if ((rc = upgradeToV10()) != 0) return (1); ++m_dbVersion; break; case 10: if ((rc = upgradeToV11()) != 0) return (1); ++m_dbVersion; break; case 11: if ((rc = upgradeToV12()) != 0) return (1); ++m_dbVersion; break; default: qWarning("Unknown version number in database - %d", m_dbVersion); } } // restore VIEWs lowerTables = tables(QSql::AllTables); for (QStringList::iterator i = lowerTables.begin(); i != lowerTables.end(); ++i) { (*i) = (*i).toLower(); } for (QMap::ConstIterator tt = m_db.viewBegin(); tt != m_db.viewEnd(); ++tt) { if (!lowerTables.contains(tt.key().toLower())) { if (!query.exec(tt.value().createString())) // krazy:exclude=crashy throw MYMONEYEXCEPTIONSQL(QString::fromLatin1("creating view %1").arg(tt.key())); } } // write updated version to DB //setVersion(QString("%1.%2").arg(m_dbVersion).arg(m_minorVersion)) query.prepare(QString("UPDATE kmmFileInfo SET version = :version;")); query.bindValue(":version", m_dbVersion); if (!query.exec()) { // krazy:exclude=crashy buildError(query, Q_FUNC_INFO, "Error updating db version"); return (1); } //signalProgress(-1,-1); return (0); } int upgradeToV1() { Q_Q(MyMoneyStorageSql); MyMoneyDbTransaction t(*q, Q_FUNC_INFO); QSqlQuery query(*q); // change kmmSplits pkey to (transactionId, splitId) if (!query.exec("ALTER TABLE kmmSplits ADD PRIMARY KEY (transactionId, splitId);")) { // krazy:exclude=crashy buildError(query, Q_FUNC_INFO, "Error updating kmmSplits pkey"); return (1); } // change kmmSplits alter checkNumber varchar(32) if (!query.exec(m_db.m_tables["kmmSplits"].modifyColumnString(m_driver, "checkNumber", // krazy:exclude=crashy MyMoneyDbColumn("checkNumber", "varchar(32)")))) { buildError(query, Q_FUNC_INFO, "Error expanding kmmSplits.checkNumber"); return (1); } // change kmmSplits add postDate datetime if (!alterTable(m_db.m_tables["kmmSplits"], m_dbVersion)) return (1); // initialize it to same value as transaction (do it the long way round) query.prepare("SELECT id, postDate FROM kmmTransactions WHERE txType = 'N';"); if (!query.exec()) { // krazy:exclude=crashy buildError(query, Q_FUNC_INFO, "Error priming kmmSplits.postDate"); return (1); } QMap tids; while (query.next()) tids[query.value(0).toString()] = query.value(1).toDateTime(); QMap::ConstIterator it; for (it = tids.constBegin(); it != tids.constEnd(); ++it) { query.prepare("UPDATE kmmSplits SET postDate=:postDate WHERE transactionId = :id;"); query.bindValue(":postDate", it.value().toString(Qt::ISODate)); query.bindValue(":id", it.key()); if (!query.exec()) { // krazy:exclude=crashy buildError(query, Q_FUNC_INFO, "priming kmmSplits.postDate"); return(1); } } // add index to kmmKeyValuePairs to (kvpType,kvpId) QStringList list; list << "kvpType" << "kvpId"; if (!query.exec(MyMoneyDbIndex("kmmKeyValuePairs", "kmmKVPtype_id", list, false).generateDDL(m_driver) + ';')) { buildError(query, Q_FUNC_INFO, "Error adding kmmKeyValuePairs index"); return (1); } // add index to kmmSplits to (accountId, txType) list.clear(); list << "accountId" << "txType"; if (!query.exec(MyMoneyDbIndex("kmmSplits", "kmmSplitsaccount_type", list, false).generateDDL(m_driver) + ';')) { buildError(query, Q_FUNC_INFO, "Error adding kmmSplits index"); return (1); } // change kmmSchedulePaymentHistory pkey to (schedId, payDate) if (!query.exec("ALTER TABLE kmmSchedulePaymentHistory ADD PRIMARY KEY (schedId, payDate);")) { buildError(query, Q_FUNC_INFO, "Error updating kmmSchedulePaymentHistory pkey"); return (1); } // change kmmPrices pkey to (fromId, toId, priceDate) if (!query.exec("ALTER TABLE kmmPrices ADD PRIMARY KEY (fromId, toId, priceDate);")) { buildError(query, Q_FUNC_INFO, "Error updating kmmPrices pkey"); return (1); } // change kmmReportConfig pkey to (name) // There wasn't one previously, so no need to drop it. if (!query.exec("ALTER TABLE kmmReportConfig ADD PRIMARY KEY (name);")) { buildError(query, Q_FUNC_INFO, "Error updating kmmReportConfig pkey"); return (1); } // change kmmFileInfo add budgets, hiBudgetId unsigned bigint // change kmmFileInfo add logonUser // change kmmFileInfo add logonAt datetime if (!alterTable(m_db.m_tables["kmmFileInfo"], m_dbVersion)) return (1); // change kmmAccounts add transactionCount unsigned bigint as last field if (!alterTable(m_db.m_tables["kmmAccounts"], m_dbVersion)) return (1); // calculate the transaction counts. the application logic defines an account's tx count // in such a way as to count multiple splits in a tx which reference the same account as one. // this is the only way I can think of to do this which will work in sqlite too. // inefficient, but it only gets done once... // get a list of all accounts so we'll get a zero value for those without txs query.prepare("SELECT id FROM kmmAccounts"); if (!query.exec()) { // krazy:exclude=crashy buildError(query, Q_FUNC_INFO, "Error retrieving accounts for transaction counting"); return(1); } while (query.next()) { m_transactionCountMap[query.value(0).toString()] = 0; } query.prepare("SELECT accountId, transactionId FROM kmmSplits WHERE txType = 'N' ORDER BY 1, 2"); if (!query.exec()) { // krazy:exclude=crashy buildError(query, Q_FUNC_INFO, "Error retrieving splits for transaction counting"); return(1); } QString lastAcc, lastTx; while (query.next()) { QString thisAcc = query.value(0).toString(); QString thisTx = query.value(1).toString(); if ((thisAcc != lastAcc) || (thisTx != lastTx)) ++m_transactionCountMap[thisAcc]; lastAcc = thisAcc; lastTx = thisTx; } QHash::ConstIterator itm; query.prepare("UPDATE kmmAccounts SET transactionCount = :txCount WHERE id = :id;"); for (itm = m_transactionCountMap.constBegin(); itm != m_transactionCountMap.constEnd(); ++itm) { query.bindValue(":txCount", QString::number(itm.value())); query.bindValue(":id", itm.key()); if (!query.exec()) { // krazy:exclude=crashy buildError(query, Q_FUNC_INFO, "Error updating transaction count"); return (1); } } m_transactionCountMap.clear(); return (0); } int upgradeToV2() { Q_Q(MyMoneyStorageSql); MyMoneyDbTransaction t(*q, Q_FUNC_INFO); QSqlQuery query(*q); // change kmmSplits add price, priceFormatted fields if (!alterTable(m_db.m_tables["kmmSplits"], m_dbVersion)) return (1); return (0); } int upgradeToV3() { Q_Q(MyMoneyStorageSql); MyMoneyDbTransaction t(*q, Q_FUNC_INFO); QSqlQuery query(*q); // kmmSchedules - add occurrenceMultiplier // The default value is given here to populate the column. if (!query.exec("ALTER TABLE kmmSchedules ADD COLUMN " + MyMoneyDbIntColumn("occurenceMultiplier", MyMoneyDbIntColumn::SMALL, false, false, true) .generateDDL(m_driver) + " DEFAULT 0;")) { buildError(query, Q_FUNC_INFO, "Error adding kmmSchedules.occurenceMultiplier"); return (1); } //The default is less than any useful value, so as each schedule is hit, it will update //itself to the appropriate value. return 0; } int upgradeToV4() { Q_Q(MyMoneyStorageSql); MyMoneyDbTransaction t(*q, Q_FUNC_INFO); QSqlQuery query(*q); // kmmSplits - add index on transactionId + splitId QStringList list; list << "transactionId" << "splitId"; if (!query.exec(MyMoneyDbIndex("kmmSplits", "kmmTx_Split", list, false).generateDDL(m_driver) + ';')) { buildError(query, Q_FUNC_INFO, "Error adding kmmSplits index on (transactionId, splitId)"); return (1); } return 0; } int upgradeToV5() { Q_Q(MyMoneyStorageSql); MyMoneyDbTransaction dbtrans(*q, Q_FUNC_INFO); QSqlQuery query(*q); // kmmSplits - add bankId if (!alterTable(m_db.m_tables["kmmSplits"], m_dbVersion)) return (1); //kmmPayees - add columns "notes" "defaultAccountId" "matchData" "matchIgnoreCase" "matchKeys"; if (!alterTable(m_db.m_tables["kmmPayees"], m_dbVersion)) return (1); // kmmReportConfig - drop primary key on name since duplicate names are allowed if (!alterTable(m_db.m_tables["kmmReportConfig"], m_dbVersion)) return (1); //} return 0; } int upgradeToV6() { Q_Q(MyMoneyStorageSql); q->startCommitUnit(Q_FUNC_INFO); QSqlQuery query(*q); // kmmFileInfo - add fixLevel if (!alterTable(m_db.m_tables["kmmFileInfo"], m_dbVersion)) return (1); // upgrade Mysql to InnoDB transaction-safe engine // the following is not a good way to test for mysql - think of a better way if (!m_driver->tableOptionString().isEmpty()) { for (QMap::ConstIterator tt = m_db.tableBegin(); tt != m_db.tableEnd(); ++tt) { if (!query.exec(QString("ALTER TABLE %1 ENGINE = InnoDB;").arg(tt.value().name()))) { buildError(query, Q_FUNC_INFO, "Error updating to InnoDB"); return (1); } } } // the alterTable function really doesn't work too well // with adding a new column which is also to be primary key // so add the column first if (!query.exec("ALTER TABLE kmmReportConfig ADD COLUMN " + MyMoneyDbColumn("id", "varchar(32)").generateDDL(m_driver) + ';')) { buildError(query, Q_FUNC_INFO, "adding id to report table"); return(1); } QMap reportList = q->fetchReports(); // the V5 database allowed lots of duplicate reports with no // way to distinguish between them. The fetchReports call // will have effectively removed all duplicates // so we now delete from the db and re-write them if (!query.exec("DELETE FROM kmmReportConfig;")) { buildError(query, Q_FUNC_INFO, "Error deleting reports"); return (1); } // add unique id to reports table if (!alterTable(m_db.m_tables["kmmReportConfig"], m_dbVersion)) return(1); QMap::const_iterator it_r; for (it_r = reportList.constBegin(); it_r != reportList.constEnd(); ++it_r) { MyMoneyReport r = *it_r; query.prepare(m_db.m_tables["kmmReportConfig"].insertString()); writeReport(*it_r, query); } q->endCommitUnit(Q_FUNC_INFO); return 0; } int upgradeToV7() { Q_Q(MyMoneyStorageSql); MyMoneyDbTransaction dbtrans(*q, Q_FUNC_INFO); QSqlQuery query(*q); // add tags support // kmmFileInfo - add tags and hiTagId if (!alterTable(m_db.m_tables["kmmFileInfo"], m_dbVersion)) return (1); m_tags = 0; return 0; } int upgradeToV8() { Q_Q(MyMoneyStorageSql); MyMoneyDbTransaction dbtrans(*q, Q_FUNC_INFO); // Added onlineJobs and payeeIdentifier if (!alterTable(m_db.m_tables["kmmFileInfo"], m_dbVersion)) return (1); return 0; } int upgradeToV9() { Q_Q(MyMoneyStorageSql); MyMoneyDbTransaction dbtrans(*q, Q_FUNC_INFO); QSqlQuery query(*q); // kmmSplits - add bankId if (!alterTable(m_db.m_tables["kmmSplits"], m_dbVersion)) return (1); return 0; } int upgradeToV10() { Q_Q(MyMoneyStorageSql); MyMoneyDbTransaction dbtrans(*q, Q_FUNC_INFO); QSqlQuery query(*q); if (!alterTable(m_db.m_tables["kmmPayeesPayeeIdentifier"], m_dbVersion)) return (1); if (!alterTable(m_db.m_tables["kmmAccountsPayeeIdentifier"], m_dbVersion)) return (1); return 0; } int upgradeToV11() { Q_Q(MyMoneyStorageSql); MyMoneyDbTransaction dbtrans(*q, Q_FUNC_INFO); QSqlQuery query(*q); // add column roundingMethodCol to kmmSecurities if (!alterTable(m_db.m_tables["kmmSecurities"], m_dbVersion)) return 1; // add column pricePrecision to kmmCurrencies if (!alterTable(m_db.m_tables["kmmCurrencies"], m_dbVersion)) return 1; return 0; } int upgradeToV12() { Q_Q(MyMoneyStorageSql); MyMoneyDbTransaction dbtrans(*q, Q_FUNC_INFO); switch(haveColumnInTable(QLatin1String("kmmSchedules"), QLatin1String("lastDayInMonth"))) { case -1: return 1; case 1: // column exists, nothing to do break; case 0: // need update of kmmSchedules // add column lastDayInMonth. Simply redo the update for 10 .. 11 if (!alterTable(m_db.m_tables["kmmSchedules"], m_dbVersion-1)) return 1; break; } switch(haveColumnInTable(QLatin1String("kmmSecurities"), QLatin1String("roundingMethod"))) { case -1: return 1; case 1: // column exists, nothing to do break; case 0: // need update of kmmSecurities and kmmCurrencies // add column roundingMethodCol to kmmSecurities. Simply redo the update for 10 .. 11 if (!alterTable(m_db.m_tables["kmmSecurities"], m_dbVersion-1)) return 1; // add column pricePrecision to kmmCurrencies. Simply redo the update for 10 .. 11 if (!alterTable(m_db.m_tables["kmmCurrencies"], m_dbVersion-1)) return 1; break; } return 0; } int createTables() { Q_Q(MyMoneyStorageSql); // check tables, create if required // convert everything to lower case, since SQL standard is case insensitive // table and column names (when not delimited), but some DBMSs disagree. QStringList lowerTables = tables(QSql::AllTables); for (QStringList::iterator i = lowerTables.begin(); i != lowerTables.end(); ++i) { (*i) = (*i).toLower(); } for (QMap::ConstIterator tt = m_db.tableBegin(); tt != m_db.tableEnd(); ++tt) { if (!lowerTables.contains(tt.key().toLower())) { createTable(tt.value()); } } QSqlQuery query(*q); for (QMap::ConstIterator tt = m_db.viewBegin(); tt != m_db.viewEnd(); ++tt) { if (!lowerTables.contains(tt.key().toLower())) { if (!query.exec(tt.value().createString())) throw MYMONEYEXCEPTIONSQL(QString::fromLatin1("creating view %1").arg(tt.key())); } } // The columns to store version info changed with version 6. Prior versions are not supported here but an error is prevented and // an old behaviour is used: call upgradeDb(). m_dbVersion = m_db.currentVersion(); if (m_dbVersion >= 6) { query.prepare(QLatin1String("INSERT INTO kmmFileInfo (version, fixLevel) VALUES(?,?);")); query.bindValue(0, m_dbVersion); query.bindValue(1, m_storage->fileFixVersion()); if (!query.exec()) throw MYMONEYEXCEPTIONSQL(QString::fromLatin1("Saving database version")); } return upgradeDb(); } void createTable(const MyMoneyDbTable& t, int version = std::numeric_limits::max()) { Q_Q(MyMoneyStorageSql); // create the tables QStringList ql = t.generateCreateSQL(m_driver, version).split('\n', QString::SkipEmptyParts); QSqlQuery query(*q); foreach (const QString& i, ql) { if (!query.exec(i)) throw MYMONEYEXCEPTIONSQL(QString::fromLatin1("creating table/index %1").arg(t.name())); } } bool alterTable(const MyMoneyDbTable& t, int fromVersion) { Q_Q(MyMoneyStorageSql); const int toVersion = fromVersion + 1; QString tempTableName = t.name(); tempTableName.replace("kmm", "kmmtmp"); QSqlQuery query(*q); // drop primary key if it has one (and driver supports it) if (t.hasPrimaryKey(fromVersion)) { QString dropString = m_driver->dropPrimaryKeyString(t.name()); if (!dropString.isEmpty()) { if (!query.exec(dropString)) { buildError(query, Q_FUNC_INFO, QString("Error dropping old primary key from %1").arg(t.name())); return false; } } } for (MyMoneyDbTable::index_iterator i = t.indexBegin(); i != t.indexEnd(); ++i) { QString indexName = t.name() + '_' + i->name() + "_idx"; if (!query.exec(m_driver->dropIndexString(t.name(), indexName))) { buildError(query, Q_FUNC_INFO, QString("Error dropping index from %1").arg(t.name())); return false; } } if (!query.exec(QString("ALTER TABLE " + t.name() + " RENAME TO " + tempTableName + ';'))) { buildError(query, Q_FUNC_INFO, QString("Error renaming table %1").arg(t.name())); return false; } createTable(t, toVersion); if (q->getRecCount(tempTableName) > 0) { query.prepare(QString("INSERT INTO " + t.name() + " (" + t.columnList(fromVersion) + ") SELECT " + t.columnList(fromVersion) + " FROM " + tempTableName + ';')); if (!query.exec()) { // krazy:exclude=crashy buildError(query, Q_FUNC_INFO, QString("Error inserting into new table %1").arg(t.name())); return false; } } if (!query.exec(QString("DROP TABLE " + tempTableName + ';'))) { buildError(query, Q_FUNC_INFO, QString("Error dropping old table %1").arg(t.name())); return false; } return true; } void clean() { Q_Q(MyMoneyStorageSql); // delete all existing records QMap::ConstIterator it = m_db.tableBegin(); QSqlQuery query(*q); while (it != m_db.tableEnd()) { query.prepare(QString("DELETE from %1;").arg(it.key())); if (!query.exec()) throw MYMONEYEXCEPTIONSQL("cleaning database"); // krazy:exclude=crashy ++it; } } int isEmpty() { Q_Q(MyMoneyStorageSql); // check all tables are empty QMap::ConstIterator tt = m_db.tableBegin(); int recordCount = 0; QSqlQuery query(*q); while ((tt != m_db.tableEnd()) && (recordCount == 0)) { query.prepare(QString("select count(*) from %1;").arg((*tt).name())); if (!query.exec()) throw MYMONEYEXCEPTIONSQL("getting record count"); // krazy:exclude=crashy if (!query.next()) throw MYMONEYEXCEPTIONSQL("retrieving record count"); recordCount += query.value(0).toInt(); ++tt; } // a fresh created database contains at least one record (see createTables()) in // the kmmFileInfo table providing file and fix version. So we report empty // even if there is a recordCount of 1 if (recordCount > 1) { return -1; // not empty } else { return 0; } } // for bug 252841 QStringList tables(QSql::TableType tt) { Q_Q(MyMoneyStorageSql); return (m_driver->tables(tt, static_cast(*q))); } //! Returns 1 in case the @a column exists in @a table, 0 if not. In case of error, -1 is returned. int haveColumnInTable(const QString& table, const QString& column) { Q_Q(MyMoneyStorageSql); QSqlQuery query(*q); QString cmd = QString("SELECT * FROM %1 LIMIT 1").arg(table); if(!query.exec(cmd)) { buildError(query, Q_FUNC_INFO, QString("Error detecting if %1 exists in %2").arg(column).arg(table)); return -1; } QSqlRecord rec = query.record(); return (rec.indexOf(column) != -1) ? 1 : 0; } /** * @brief Ensure the storagePlugin with iid was setup * * @throws MyMoneyException in case of an error which makes the use * of the plugin unavailable. */ bool setupStoragePlugin(QString iid) { Q_Q(MyMoneyStorageSql); // setupDatabase has to be called every time because this simple technique to check if was updated already // does not work if a user opens another file // also the setup is removed if the current database transaction is rolled back if (iid.isEmpty() /*|| m_loadedStoragePlugins.contains(iid)*/) return false; QString sqlIID; if (iid == payeeIdentifiers::ibanBic::staticPayeeIdentifierIid()) sqlIID = QString::fromLatin1("org.kmymoney.payeeIdentifier.ibanbic.sqlStoragePlugin"); else if (iid == payeeIdentifiers::nationalAccount::staticPayeeIdentifierIid()) sqlIID = QLatin1String("org.kmymoney.payeeIdentifier.nationalAccount.sqlStoragePlugin"); else if (iid == sepaOnlineTransferImpl::name()) sqlIID = QLatin1String("org.kmymoney.creditTransfer.sepa.sqlStoragePlugin"); else return false; QString errorMsg; KMyMoneyPlugin::storagePlugin* plugin = KServiceTypeTrader::createInstanceFromQuery( QLatin1String("KMyMoney/sqlStoragePlugin"), QString("'%1' ~in [X-KMyMoney-PluginIid]").arg(sqlIID.replace(QLatin1Char('\''), QLatin1String("\\'"))), 0, QVariantList(), &errorMsg ); if (plugin == 0) throw MYMONEYEXCEPTION(QString::fromLatin1("Could not load sqlStoragePlugin '%1', (error: %2)").arg(sqlIID, errorMsg)); MyMoneyDbTransaction t(*q, Q_FUNC_INFO); if (plugin->setupDatabase(*q)) { m_loadedStoragePlugins.insert(sqlIID); return true; } throw MYMONEYEXCEPTION(QString::fromLatin1("Could not install sqlStoragePlugin '%1' in database.").arg(sqlIID)); } bool actOnIBANBICObjectInSQL(SQLAction action, const payeeIdentifier &obj) { payeeIdentifierTyped payeeIdentifier = payeeIdentifierTyped(obj); Q_Q(MyMoneyStorageSql); QSqlQuery query(*q); auto writeQuery = [&]() { query.bindValue(":id", obj.idString()); query.bindValue(":iban", payeeIdentifier->electronicIban()); const auto bic = payeeIdentifier->fullStoredBic(); query.bindValue(":bic", (bic.isEmpty()) ? QVariant(QVariant::String) : bic); query.bindValue(":name", payeeIdentifier->ownerName()); if (!query.exec()) { // krazy:exclude=crashy qWarning("Error while saving ibanbic data for '%s': %s", qPrintable(obj.idString()), qPrintable(query.lastError().text())); return false; } return true; }; switch(action) { case SQLAction::Save: query.prepare("INSERT INTO kmmIbanBic " " ( id, iban, bic, name )" " VALUES( :id, :iban, :bic, :name ) " ); return writeQuery(); case SQLAction::Modify: query.prepare("UPDATE kmmIbanBic SET iban = :iban, bic = :bic, name = :name WHERE id = :id;"); return writeQuery(); case SQLAction::Remove: query.prepare("DELETE FROM kmmIbanBic WHERE id = ?;"); query.bindValue(0, obj.idString()); if (!query.exec()) { qWarning("Error while deleting ibanbic data '%s': %s", qPrintable(obj.idString()), qPrintable(query.lastError().text())); return false; } return true; } return false; } bool actOnNationalAccountObjectInSQL(SQLAction action, const payeeIdentifier &obj) { payeeIdentifierTyped payeeIdentifier = payeeIdentifierTyped(obj); Q_Q(MyMoneyStorageSql); QSqlQuery query(*q); auto writeQuery = [&]() { query.bindValue(":id", obj.idString()); query.bindValue(":countryCode", payeeIdentifier->country()); query.bindValue(":accountNumber", payeeIdentifier->accountNumber()); query.bindValue(":bankCode", (payeeIdentifier->bankCode().isEmpty()) ? QVariant(QVariant::String) : payeeIdentifier->bankCode()); query.bindValue(":name", payeeIdentifier->ownerName()); if (!query.exec()) { // krazy:exclude=crashy qWarning("Error while saving national account number for '%s': %s", qPrintable(obj.idString()), qPrintable(query.lastError().text())); return false; } return true; }; switch(action) { case SQLAction::Save: query.prepare("INSERT INTO kmmNationalAccountNumber " " ( id, countryCode, accountNumber, bankCode, name )" " VALUES( :id, :countryCode, :accountNumber, :bankCode, :name ) " ); return writeQuery(); case SQLAction::Modify: query.prepare("UPDATE kmmNationalAccountNumber SET countryCode = :countryCode, accountNumber = :accountNumber, bankCode = :bankCode, name = :name WHERE id = :id;"); return writeQuery(); case SQLAction::Remove: query.prepare("DELETE FROM kmmNationalAccountNumber WHERE id = ?;"); query.bindValue(0, obj.idString()); if (!query.exec()) { qWarning("Error while deleting national account number '%s': %s", qPrintable(obj.idString()), qPrintable(query.lastError().text())); return false; } return true; } return false; } bool actOnSepaOnlineTransferObjectInSQL(SQLAction action, const onlineTask &obj, const QString& id) { Q_Q(MyMoneyStorageSql); QSqlQuery query(*q); const auto& task = dynamic_cast(obj); auto bindValuesToQuery = [&]() { query.bindValue(":id", id); query.bindValue(":originAccount", task.responsibleAccount()); query.bindValue(":value", task.value().toString()); query.bindValue(":purpose", task.purpose()); query.bindValue(":endToEndReference", (task.endToEndReference().isEmpty()) ? QVariant() : QVariant::fromValue(task.endToEndReference())); query.bindValue(":beneficiaryName", task.beneficiaryTyped().ownerName()); query.bindValue(":beneficiaryIban", task.beneficiaryTyped().electronicIban()); query.bindValue(":beneficiaryBic", (task.beneficiaryTyped().storedBic().isEmpty()) ? QVariant() : QVariant::fromValue(task.beneficiaryTyped().storedBic())); query.bindValue(":textKey", task.textKey()); query.bindValue(":subTextKey", task.subTextKey()); }; switch(action) { case SQLAction::Save: query.prepare("INSERT INTO kmmSepaOrders (" " id, originAccount, value, purpose, endToEndReference, beneficiaryName, beneficiaryIban, " " beneficiaryBic, textKey, subTextKey) " " VALUES( :id, :originAccount, :value, :purpose, :endToEndReference, :beneficiaryName, :beneficiaryIban, " " :beneficiaryBic, :textKey, :subTextKey ) " ); bindValuesToQuery(); if (!query.exec()) { qWarning("Error while saving sepa order '%s': %s", qPrintable(id), qPrintable(query.lastError().text())); return false; } return true; case SQLAction::Modify: query.prepare( "UPDATE kmmSepaOrders SET" " originAccount = :originAccount," " value = :value," " purpose = :purpose," " endToEndReference = :endToEndReference," " beneficiaryName = :beneficiaryName," " beneficiaryIban = :beneficiaryIban," " beneficiaryBic = :beneficiaryBic," " textKey = :textKey," " subTextKey = :subTextKey " " WHERE id = :id"); bindValuesToQuery(); if (!query.exec()) { qWarning("Could not modify sepaOnlineTransfer '%s': %s", qPrintable(id), qPrintable(query.lastError().text())); return false; } return true; case SQLAction::Remove: query.prepare("DELETE FROM kmmSepaOrders WHERE id = ?"); query.bindValue(0, id); return query.exec(); } return false; } void actOnPayeeIdentifierObjectInSQL(SQLAction action, const payeeIdentifier& obj) { setupStoragePlugin(obj->payeeIdentifierId()); auto isSuccessfull = false; if (obj->payeeIdentifierId() == payeeIdentifiers::ibanBic::staticPayeeIdentifierIid()) isSuccessfull = actOnIBANBICObjectInSQL(action, obj); else if (obj->payeeIdentifierId() == payeeIdentifiers::nationalAccount::staticPayeeIdentifierIid()) isSuccessfull = actOnNationalAccountObjectInSQL(action, obj); if (!isSuccessfull) { switch (action) { case SQLAction::Save: throw MYMONEYEXCEPTION(QString::fromLatin1("Could not save object with id '%1' in database (plugin failed).").arg(obj.idString())); case SQLAction::Modify: throw MYMONEYEXCEPTION(QString::fromLatin1("Could not modify object with id '%1' in database (plugin failed).").arg(obj.idString())); case SQLAction::Remove: throw MYMONEYEXCEPTION(QString::fromLatin1("Could not remove object with id '%1' from database (plugin failed).").arg(obj.idString())); } } } void actOnOnlineJobInSQL(SQLAction action, const onlineTask& obj, const QString& id) { setupStoragePlugin(obj.taskName()); auto isSuccessfull = false; if (obj.taskName() == sepaOnlineTransferImpl::name()) isSuccessfull = actOnSepaOnlineTransferObjectInSQL(action, obj, id); if (!isSuccessfull) { switch (action) { case SQLAction::Save: throw MYMONEYEXCEPTION(QString::fromLatin1("Could not save object with id '%1' in database (plugin failed).").arg(id)); case SQLAction::Modify: throw MYMONEYEXCEPTION(QString::fromLatin1("Could not modify object with id '%1' in database (plugin failed).").arg(id)); case SQLAction::Remove: throw MYMONEYEXCEPTION(QString::fromLatin1("Could not remove object with id '%1' from database (plugin failed).").arg(id)); } } } payeeIdentifierData* createIBANBICObject(QSqlDatabase db, const QString& identId) const { QSqlQuery query(db); query.prepare("SELECT iban, bic, name FROM kmmIbanBic WHERE id = ?;"); query.bindValue(0, identId); if (!query.exec() || !query.next()) { qWarning("Could load iban bic identifier from database"); return nullptr; } payeeIdentifiers::ibanBic *const ident = new payeeIdentifiers::ibanBic; ident->setIban(query.value(0).toString()); ident->setBic(query.value(1).toString()); ident->setOwnerName(query.value(2).toString()); return ident; } payeeIdentifierData* createNationalAccountObject(QSqlDatabase db, const QString& identId) const { QSqlQuery query(db); query.prepare("SELECT countryCode, accountNumber, bankCode, name FROM kmmNationalAccountNumber WHERE id = ?;"); query.bindValue(0, identId); if (!query.exec() || !query.next()) { qWarning("Could load national account number from database"); return nullptr; } payeeIdentifiers::nationalAccount *const ident = new payeeIdentifiers::nationalAccount; ident->setCountry(query.value(0).toString()); ident->setAccountNumber(query.value(1).toString()); ident->setBankCode(query.value(2).toString()); ident->setOwnerName(query.value(3).toString()); return ident; } payeeIdentifier createPayeeIdentifierObject(QSqlDatabase db, const QString& identifierType, const QString& identifierId) const { payeeIdentifierData* identData = nullptr; if (identifierType == payeeIdentifiers::ibanBic::staticPayeeIdentifierIid()) identData = createIBANBICObject(db, identifierId); else if (identifierType == payeeIdentifiers::nationalAccount::staticPayeeIdentifierIid()) identData = createNationalAccountObject(db, identifierId); return payeeIdentifier(identifierId, identData); } onlineTask* createSepaOnlineTransferObject(QSqlDatabase connection, const QString& onlineJobId) const { Q_ASSERT(!onlineJobId.isEmpty()); Q_ASSERT(connection.isOpen()); QSqlQuery query = QSqlQuery( "SELECT originAccount, value, purpose, endToEndReference, beneficiaryName, beneficiaryIban, " " beneficiaryBic, textKey, subTextKey FROM kmmSepaOrders WHERE id = ?", connection ); query.bindValue(0, onlineJobId); if (query.exec() && query.next()) { sepaOnlineTransferImpl* task = new sepaOnlineTransferImpl(); task->setOriginAccount(query.value(0).toString()); task->setValue(MyMoneyMoney(query.value(1).toString())); task->setPurpose(query.value(2).toString()); task->setEndToEndReference(query.value(3).toString()); task->setTextKey(query.value(7).toUInt()); task->setSubTextKey(query.value(8).toUInt()); payeeIdentifiers::ibanBic beneficiary; beneficiary.setOwnerName(query.value(4).toString()); beneficiary.setIban(query.value(5).toString()); beneficiary.setBic(query.value(6).toString()); task->setBeneficiary(beneficiary); return task; } return nullptr; } onlineTask* createOnlineTaskObject(const QString& iid, const QString& onlineTaskId, QSqlDatabase connection) const { onlineTask* taskOnline = nullptr; if (iid == sepaOnlineTransferImpl::name()) { // @todo This is probably memory leak but for now it works alike to original code onlineJobAdministration::instance()->registerOnlineTask(new sepaOnlineTransferImpl); taskOnline = createSepaOnlineTransferObject(connection, onlineTaskId); } if (!taskOnline) qWarning("In the file is a onlineTask for which I could not find the plugin ('%s')", qPrintable(iid)); return taskOnline; } void alert(QString s) const // FIXME: remove... { qDebug() << s; } void signalProgress(qint64 current, qint64 total, const QString& msg) const { if (m_progressCallback != 0) (*m_progressCallback)(current, total, msg); } void signalProgress(qint64 current, qint64 total) const { signalProgress(current, total, QString()); } template ulong getNextId(const QString& table, const QString& id, const int prefixLength) const { Q_CHECK_PTR(cache); if (this->*cache == 0) { MyMoneyStorageSqlPrivate* nonConstThis = const_cast(this); nonConstThis->*cache = 1 + nonConstThis->highestNumberFromIdString(table, id, prefixLength); } Q_ASSERT(this->*cache > 0); // everything else is never a valid id return this->*cache; } //void startCommitUnit (const QString& callingFunction); //void endCommitUnit (const QString& callingFunction); //void cancelCommitUnit (const QString& callingFunction); MyMoneyStorageSql *q_ptr; // data QExplicitlySharedDataPointer m_driver; MyMoneyDbDef m_db; uint m_dbVersion; MyMoneyStorageMgr *m_storage; // input options bool m_loadAll; // preload all data bool m_override; // override open if already in use // error message QString m_error; // record counts ulong m_institutions; ulong m_accounts; ulong m_payees; ulong m_tags; ulong m_transactions; ulong m_splits; ulong m_securities; ulong m_prices; ulong m_currencies; ulong m_schedules; ulong m_reports; ulong m_kvps; ulong m_budgets; ulong m_onlineJobs; ulong m_payeeIdentifier; // Cache for next id to use // value 0 means data is not available and has to be loaded from the database ulong m_hiIdInstitutions; ulong m_hiIdPayees; ulong m_hiIdTags; ulong m_hiIdAccounts; ulong m_hiIdTransactions; ulong m_hiIdSchedules; ulong m_hiIdSecurities; ulong m_hiIdReports; ulong m_hiIdBudgets; ulong m_hiIdOnlineJobs; ulong m_hiIdPayeeIdentifier; ulong m_hiIdCostCenter; // encrypt option - usage TBD QString m_encryptData; /** * This variable is used to suppress status messages except during * initial data load and final write */ bool m_displayStatus; /** The following keeps track of commitment units (known as transactions in SQL * though it would be confusing to use that term within KMM). It is implemented * as a stack for debug purposes. Long term, probably a count would suffice */ QStack m_commitUnitStack; /** * This member variable is used to preload transactions for preferred accounts */ MyMoneyTransactionFilter m_preferred; /** * This member variable is used because reading prices from a file uses the 'add...' function rather than a * 'load...' function which other objects use. Having this variable allows us to avoid needing to check the * database to see if this really is a new or modified price */ bool m_readingPrices; /** * This member variable holds a map of transaction counts per account, indexed by * the account id. It is used * to avoid having to scan all transactions whenever a count is needed. It should * probably be moved into the MyMoneyAccount object; maybe we will do that once * the database code has been properly checked out */ QHash m_transactionCountMap; /** * These member variables hold the user name and date/time of logon */ QString m_logonUser; QDateTime m_logonAt; QDate m_txPostDate; // FIXME: remove when Tom puts date into split object bool m_newDatabase; /** * This member keeps the current precision to be used fro prices. * @sa setPrecision() */ static int m_precision; /** * This member keeps the current start date used for transaction retrieval. * @sa setStartDate() */ static QDate m_startDate; /** * */ QSet m_loadedStoragePlugins; void (*m_progressCallback)(int, int, const QString&); }; #endif diff --git a/kmymoney/plugins/sql/tests/mymoneydatabasemgr-test.cpp b/kmymoney/plugins/sql/tests/mymoneydatabasemgr-test.cpp index 34bba1843..6fcb51975 100644 --- a/kmymoney/plugins/sql/tests/mymoneydatabasemgr-test.cpp +++ b/kmymoney/plugins/sql/tests/mymoneydatabasemgr-test.cpp @@ -1,2553 +1,2553 @@ /*************************************************************************** mymoneygenericstoragetest.cpp ------------------- copyright : (C) 2008 by Fernando Vilas email : fvilas@iname.com ***************************************************************************/ /*************************************************************************** * * * 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 "mymoneydatabasemgr-test.h" #include #include #include "mymoneystoragemgr_p.h" #include "../mymoneystoragesql.h" #include "../mymoneystoragesql_p.h" #include "mymoneytestutils.h" #include "mymoneymoney.h" #include "mymoneyfile.h" #include "mymoneyinstitution.h" #include "mymoneyaccount.h" #include "mymoneysecurity.h" #include "mymoneytag.h" #include "mymoneypayee.h" #include "mymoneyschedule.h" #include "mymoneyschedule_p.h" #include "mymoneyreport.h" #include "mymoneysplit.h" #include "mymoneysplit_p.h" #include "mymoneytransaction.h" #include "mymoneybudget.h" #include "onlinetasks/dummy/tasks/dummytask.h" #include "misc/platformtools.h" #include "mymoneyenums.h" using namespace eMyMoney; QTEST_GUILESS_MAIN(MyMoneyStorageMgrTest) MyMoneyStorageMgrTest::MyMoneyStorageMgrTest() : m(nullptr), m_dbAttached(false), m_canOpen(true), m_haveEmptyDataBase(false), m_file(this), m_emptyFile(this) { // Open and close the temp file so that it exists m_file.open(); m_file.close(); // The same with the empty db file m_emptyFile.open(); m_emptyFile.close(); testCaseTimer.start(); } MyMoneyStorageMgrTest::~MyMoneyStorageMgrTest() { m_sql.release(); } void MyMoneyStorageMgrTest::init() { testStepTimer.start(); m = new MyMoneyStorageMgr; // Create file and close it to release possible read-write locks m_file.open(); m_file.close(); } void MyMoneyStorageMgrTest::cleanup() { if (m_canOpen) { // All transactions should have already been committed. //m->commitTransaction(); } if (MyMoneyFile::instance()->storageAttached()) { MyMoneyFile::instance()->detachStorage(m); m_dbAttached = false; } delete m; qDebug() << "teststep" << testStepTimer.elapsed() << "msec, total" << testCaseTimer.elapsed() << "msec"; } void MyMoneyStorageMgrTest::testEmptyConstructor() { MyMoneyPayee user = m->user(); QVERIFY(user.name().isEmpty()); QVERIFY(user.address().isEmpty()); QVERIFY(user.city().isEmpty()); QVERIFY(user.state().isEmpty()); QVERIFY(user.postcode().isEmpty()); QVERIFY(user.telephone().isEmpty()); QVERIFY(user.email().isEmpty()); // QVERIFY(m->nextInstitutionID().isEmpty()); // QVERIFY(m->nextAccountID().isEmpty()); // QVERIFY(m->nextTransactionID().isEmpty()); // QVERIFY(m->nextPayeeID().isEmpty()); // QVERIFY(m->nextScheduleID().isEmpty()); // QVERIFY(m->nextReportID().isEmpty()); // QVERIFY(m->nextOnlineJobID().isEmpty()); QCOMPARE(m->institutionList().count(), 0); QList accList; m->accountList(accList); QCOMPARE(accList.count(), 0); MyMoneyTransactionFilter f; QCOMPARE(m->transactionList(f).count(), 0); QCOMPARE(m->payeeList().count(), 0); QCOMPARE(m->tagList().count(), 0); QCOMPARE(m->scheduleList(QString(), Schedule::Type::Any, Schedule::Occurrence::Any, Schedule::PaymentType::Any, QDate(), QDate(), false).count(), 0); QCOMPARE(m->creationDate(), QDate::currentDate()); } void MyMoneyStorageMgrTest::setupUrl(const QString& fname) { QString m_userName = platformTools::osUsername(); QString m_mode = //"QPSQL&mode=single"; //"QMYSQL&mode=single"; "QSQLITE&mode=single"; m_url = QUrl(QString("sql://%1@localhost/%2?driver=%3").arg(m_userName, fname, m_mode)); } void MyMoneyStorageMgrTest::copyDatabaseFile(QFile& src, QFile& dest) { if (src.open(QIODevice::ReadOnly)) { if (dest.open(QIODevice::WriteOnly | QIODevice::Truncate)) { dest.write(src.readAll()); dest.close(); } src.close(); } } void MyMoneyStorageMgrTest::testBadConnections() { // Check a connection that exists but has empty tables setupUrl(m_file.fileName()); try { m_sql.release(); m_sql = std::make_unique(m, m_url); // QExplicitlySharedDataPointer sql = m->connectToDatabase(m_url); QVERIFY(m_sql != nullptr); QEXPECT_FAIL("", "Will fix when correct behaviour in this case is clear.", Continue); QVERIFY(m_sql->open(m_url, QIODevice::ReadWrite) != 0); } catch (const MyMoneyException &e) { unexpectedException(e); } } void MyMoneyStorageMgrTest::testCreateDb() { try { // Fetch the list of available drivers QStringList list = QSqlDatabase::drivers(); QStringList::Iterator it = list.begin(); if (it == list.end()) { m_canOpen = false; } else { setupUrl(m_file.fileName()); m_sql.release(); m_sql = std::make_unique(m, m_url); // QExplicitlySharedDataPointer sql = m->connectToDatabase(m_url); QVERIFY(0 != m_sql); //qDebug("Database driver is %s", qPrintable(sql->driverName())); // Clear the database, so there is a fresh start on each run. if (0 == m_sql->open(m_url, QIODevice::WriteOnly, true)) { MyMoneyFile::instance()->attachStorage(m); QVERIFY(m_sql->writeFile()); m_sql->close(); copyDatabaseFile(m_file, m_emptyFile); m_haveEmptyDataBase = true; } else { m_canOpen = false; } } } catch (const MyMoneyException &e) { unexpectedException(e); } } void MyMoneyStorageMgrTest::testAttachDb() { if (!m_dbAttached) { if (!m_haveEmptyDataBase) { testCreateDb(); } else { // preload database file with empty set copyDatabaseFile(m_emptyFile, m_file); } if (m_canOpen) { try { MyMoneyFile::instance()->detachStorage(); m_sql.release(); m_sql = std::make_unique(m, m_url); // QExplicitlySharedDataPointer sql = m->connectToDatabase(m_url); QVERIFY(m_sql != nullptr); int openStatus = m_sql->open(m_url, QIODevice::ReadWrite); QCOMPARE(openStatus, 0); MyMoneyFile::instance()->attachStorage(m); m_dbAttached = true; } catch (const MyMoneyException &e) { unexpectedException(e); } } } } void MyMoneyStorageMgrTest::testDisconnection() { testAttachDb(); if (!m_canOpen) QSKIP("Database test skipped because no database could be opened.", SkipAll); try { m_sql.get()->QSqlDatabase::close(); // (QSqlDatabase*)(QSqlDatabase::m_sql.pointer->close())->close(); QList accList; m->accountList(accList); } catch (const MyMoneyException &e) { unexpectedException(e); } } void MyMoneyStorageMgrTest::testSetFunctions() { testAttachDb(); if (!m_canOpen) QSKIP("Database test skipped because no database could be opened.", SkipAll); MyMoneyPayee user = m->user(); user.setName("Name"); m->setUser(user); user.setAddress("Street"); m->setUser(user); user.setCity("Town"); m->setUser(user); user.setState("County"); m->setUser(user); user.setPostcode("Postcode"); m->setUser(user); user.setTelephone("Telephone"); m->setUser(user); user.setEmail("Email"); m->setUser(user); m->setValue("key", "value"); user = m->user(); QVERIFY(user.name() == "Name"); QVERIFY(user.address() == "Street"); QVERIFY(user.city() == "Town"); QVERIFY(user.state() == "County"); QVERIFY(user.postcode() == "Postcode"); QVERIFY(user.telephone() == "Telephone"); QVERIFY(user.email() == "Email"); QVERIFY(m->value("key") == "value"); m->setDirty(); m->deletePair("key"); QVERIFY(m->dirty() == true); } void MyMoneyStorageMgrTest::testSupportFunctions() { testAttachDb(); if (!m_canOpen) QSKIP("Database test skipped because no database could be opened.", SkipAll); try { QCOMPARE(m->d_func()->nextInstitutionID(), QLatin1String("I000001")); QCOMPARE(m->d_func()->nextAccountID(), QLatin1String("A000001")); QCOMPARE(m->d_func()->nextTransactionID(), QLatin1String("T000000000000000001")); QCOMPARE(m->d_func()->nextPayeeID(), QLatin1String("P000001")); QCOMPARE(m->d_func()->nextTagID(), QLatin1String("G000001")); QCOMPARE(m->d_func()->nextScheduleID(), QLatin1String("SCH000001")); QCOMPARE(m->d_func()->nextReportID(), QLatin1String("R000001")); QCOMPARE(m->d_func()->nextOnlineJobID(), QLatin1String("O000001")); QCOMPARE(m->liability().name(), QLatin1String("Liability")); QCOMPARE(m->asset().name(), QLatin1String("Asset")); QCOMPARE(m->expense().name(), QLatin1String("Expense")); QCOMPARE(m->income().name(), QLatin1String("Income")); QCOMPARE(m->equity().name(), QLatin1String("Equity")); QCOMPARE(m->dirty(), true); } catch (const MyMoneyException &e) { unexpectedException(e); } } void MyMoneyStorageMgrTest::testIsStandardAccount() { testAttachDb(); if (!m_canOpen) QSKIP("Database test skipped because no database could be opened.", SkipAll); QVERIFY(m->isStandardAccount(stdAccNames[stdAccLiability]) == true); QVERIFY(m->isStandardAccount(stdAccNames[stdAccAsset]) == true); QVERIFY(m->isStandardAccount(stdAccNames[stdAccExpense]) == true); QVERIFY(m->isStandardAccount(stdAccNames[stdAccIncome]) == true); QVERIFY(m->isStandardAccount(stdAccNames[stdAccEquity]) == true); QVERIFY(m->isStandardAccount("A0002") == false); } void MyMoneyStorageMgrTest::testNewAccount() { testAttachDb(); if (!m_canOpen) QSKIP("Database test skipped because no database could be opened.", SkipAll); MyMoneyAccount a; a.setName("AccountName"); a.setNumber("AccountNumber"); a.setValue("Key", "Value"); MyMoneyFileTransaction ft; m->addAccount(a); ft.commit(); QCOMPARE(m->d_func()->m_nextAccountID, 1ul); QList accList; m->accountList(accList); QCOMPARE(accList.count(), 1); QCOMPARE((*(accList.begin())).name(), QLatin1String("AccountName")); QCOMPARE((*(accList.begin())).id(), QLatin1String("A000001")); QCOMPARE((*(accList.begin())).value("Key"), QLatin1String("Value")); } void MyMoneyStorageMgrTest::testAccount() { testNewAccount(); if (!m_canOpen) QSKIP("Database test skipped because no database could be opened.", SkipAll); m->setDirty(); MyMoneyAccount a; // make sure that an invalid ID causes an exception try { a = m->account("Unknown ID"); QFAIL("Exception expected"); } catch (const MyMoneyException &) { } QVERIFY(m->dirty() == true); // now make sure, that a real ID works try { a = m->account("A000001"); QVERIFY(a.name() == "AccountName"); QVERIFY(a.id() == "A000001"); } catch (const MyMoneyException &e) { unexpectedException(e); } } void MyMoneyStorageMgrTest::testAddNewAccount() { testNewAccount(); if (!m_canOpen) QSKIP("Database test skipped because no database could be opened.", SkipAll); MyMoneyAccount a, b; b.setName("Account2"); b.setNumber("Acc2"); MyMoneyFileTransaction ft; m->addAccount(b); ft.commit(); m->setDirty(); QVERIFY(m->d_func()->m_nextAccountID == 2); QList accList; m->accountList(accList); QVERIFY(accList.count() == 2); // try to add account to undefined account try { MyMoneyAccount c("UnknownID", b); ft.restart(); m->addAccount(c, a); ft.commit(); QFAIL("Exception expected"); } catch (const MyMoneyException &) { } QVERIFY(m->dirty() == true); // now try to add account 1 as sub-account to account 2 try { a = m->account("A000001"); QVERIFY(m->asset().accountList().count() == 0); ft.restart(); m->addAccount(b, a); ft.commit(); MyMoneyAccount acc(m->account("A000002")); QVERIFY(acc.accountList()[0] == "A000001"); QVERIFY(acc.accountList().count() == 1); QVERIFY(m->asset().accountList().count() == 0); } catch (const MyMoneyException &e) { unexpectedException(e); } } void MyMoneyStorageMgrTest::testAddInstitution() { testAttachDb(); if (!m_canOpen) QSKIP("Database test skipped because no database could be opened.", SkipAll); MyMoneyInstitution i; i.setName("Inst Name"); MyMoneyFileTransaction ft; m->addInstitution(i); ft.commit(); QVERIFY(m->institutionList().count() == 1); QVERIFY(m->d_func()->m_nextInstitutionID == 1); QVERIFY((*(m->institutionList().begin())).name() == "Inst Name"); QVERIFY((*(m->institutionList().begin())).id() == "I000001"); } void MyMoneyStorageMgrTest::testInstitution() { testAddInstitution(); if (!m_canOpen) QSKIP("Database test skipped because no database could be opened.", SkipAll); MyMoneyInstitution i; m->setDirty(); // try to find unknown institution try { i = m->institution("Unknown ID"); QFAIL("Exception expected"); } catch (const MyMoneyException &) { } QVERIFY(m->dirty() == true); // now try to find real institution try { i = m->institution("I000001"); QVERIFY(i.name() == "Inst Name"); QVERIFY(m->dirty() == true); } catch (const MyMoneyException &e) { unexpectedException(e); } } void MyMoneyStorageMgrTest::testAccount2Institution() { testAttachDb(); if (!m_canOpen) QSKIP("Database test skipped because no database could be opened.", SkipAll); testAddInstitution(); testAddNewAccount(); MyMoneyInstitution i; MyMoneyAccount a, b; try { i = m->institution("I000001"); a = m->account("A000001"); } catch (const MyMoneyException &e) { unexpectedException(e); } m->setDirty(); // try to add to a false institution MyMoneyInstitution fake("Unknown ID", i); a.setInstitutionId(fake.id()); try { MyMoneyFileTransaction ft; m->modifyAccount(a); ft.commit(); QFAIL("Exception expected"); } catch (const MyMoneyException &) { } QVERIFY(m->dirty() == true); // now try to do it with a real institution try { QVERIFY(i.accountList().count() == 0); a.setInstitutionId(i.id()); MyMoneyFileTransaction ft; m->modifyAccount(a); ft.commit(); QVERIFY(a.institutionId() == i.id()); b = m->account("A000001"); QVERIFY(b.institutionId() == i.id()); QVERIFY(i.accountList().count() == 0); } catch (const MyMoneyException &e) { unexpectedException(e); } } void MyMoneyStorageMgrTest::testModifyAccount() { testAttachDb(); if (!m_canOpen) QSKIP("Database test skipped because no database could be opened.", SkipAll); testAccount2Institution(); // test the OK case // //FIXME: modify 2 accounts simultaneously to trip a write error MyMoneyAccount a = m->account("A000001"); a.setName("New account name"); m->setDirty(); try { MyMoneyFileTransaction ft; m->modifyAccount(a); ft.commit(); MyMoneyAccount b = m->account("A000001"); QVERIFY(b.parentAccountId() == a.parentAccountId()); QVERIFY(b.name() == "New account name"); QVERIFY(b.value("Key") == "Value"); } catch (const MyMoneyException &e) { unexpectedException(e); } // modify institution to unknown id MyMoneyAccount c("Unknown ID", a); m->setDirty(); try { m->modifyAccount(c); QFAIL("Exception expected"); } catch (const MyMoneyException &) { } // use different account type MyMoneyAccount d; d.setAccountType(Account::Type::CreditCard); MyMoneyAccount f("A000001", d); try { MyMoneyFileTransaction ft; m->modifyAccount(f); ft.commit(); QFAIL("Exception expected"); } catch (const MyMoneyException &) { } // use different parent a.setParentAccountId("A000002"); try { MyMoneyFileTransaction ft; m->modifyAccount(c); ft.commit(); QFAIL("Exception expected"); } catch (const MyMoneyException &) { } } void MyMoneyStorageMgrTest::testModifyInstitution() { testAddInstitution(); if (!m_canOpen) QSKIP("Database test skipped because no database could be opened.", SkipAll); MyMoneyInstitution i = m->institution("I000001"); m->setDirty(); i.setName("New inst name"); try { MyMoneyFileTransaction ft; m->modifyInstitution(i); ft.commit(); i = m->institution("I000001"); QVERIFY(i.name() == "New inst name"); } catch (const MyMoneyException &e) { unexpectedException(e); } // try to modify an institution that does not exist MyMoneyInstitution f("Unknown ID", i); try { MyMoneyFileTransaction ft; m->modifyInstitution(f); ft.commit(); QFAIL("Exception expected"); } catch (const MyMoneyException &) { } } void MyMoneyStorageMgrTest::testReparentAccount() { testAttachDb(); if (!m_canOpen) QSKIP("Database test skipped because no database could be opened.", SkipAll); // this one adds some accounts to the database MyMoneyAccount ex1; ex1.setAccountType(Account::Type::Expense); MyMoneyAccount ex2; ex2.setAccountType(Account::Type::Expense); MyMoneyAccount ex3; ex3.setAccountType(Account::Type::Expense); MyMoneyAccount ex4; ex4.setAccountType(Account::Type::Expense); MyMoneyAccount in; in.setAccountType(Account::Type::Income); MyMoneyAccount ch; ch.setAccountType(Account::Type::Checkings); ex1.setName("Sales Tax"); ex2.setName("Sales Tax 16%"); ex3.setName("Sales Tax 7%"); ex4.setName("Grosseries"); in.setName("Salary"); ch.setName("My checkings account"); ch.setValue("Key", "Value"); try { MyMoneyFileTransaction ft; m->addAccount(ex1); m->addAccount(ex2); m->addAccount(ex3); m->addAccount(ex4); m->addAccount(in); m->addAccount(ch); ft.commit(); QVERIFY(ex1.id() == "A000001"); QVERIFY(ex2.id() == "A000002"); QVERIFY(ex3.id() == "A000003"); QVERIFY(ex4.id() == "A000004"); QVERIFY(in.id() == "A000005"); QVERIFY(ch.id() == "A000006"); QVERIFY(ch.value("Key") == "Value"); MyMoneyAccount parent = m->expense(); ft.restart(); m->addAccount(parent, ex1); m->addAccount(ex1, ex2); m->addAccount(parent, ex3); m->addAccount(parent, ex4); ft.commit(); parent = m->income(); ft.restart(); m->addAccount(parent, in); ft.commit(); parent = m->asset(); ft.restart(); m->addAccount(parent, ch); ft.commit(); QVERIFY(ch.value("Key") == "Value"); // MyMoneyFile::instance()->preloadCache(); QVERIFY(m->expense().accountCount() == 3); QVERIFY(m->account(ex1.id()).accountCount() == 1); QVERIFY(ex3.parentAccountId() == stdAccNames[stdAccExpense]); //for (int i = 0; i < 100; ++i) { ft.restart(); m->reparentAccount(ex3, ex1); ft.commit(); //} // MyMoneyFile::instance()->preloadCache(); QVERIFY(m->expense().accountCount() == 2); QVERIFY(m->account(ex1.id()).accountCount() == 2); QVERIFY(ex3.parentAccountId() == ex1.id()); } catch (const MyMoneyException &e) { unexpectedException(e); } } void MyMoneyStorageMgrTest::testAddTransactions() { testAttachDb(); if (!m_canOpen) QSKIP("Database test skipped because no database could be opened.", SkipAll); testReparentAccount(); MyMoneyAccount ch; MyMoneyTransaction t1, t2; MyMoneySplit s; try { // I made some money, great s.setAccountId("A000006"); // Checkings s.setShares(MyMoneyMoney(100000, 100)); s.setValue(MyMoneyMoney(100000, 100)); QVERIFY(s.id().isEmpty()); t1.addSplit(s); s.clearId(); // enable re-usage of split variable s.setAccountId("A000005"); // Salary s.setShares(MyMoneyMoney(-100000, 100)); s.setValue(MyMoneyMoney(-100000, 100)); QVERIFY(s.id().isEmpty()); t1.addSplit(s); t1.setPostDate(QDate(2002, 5, 10)); } catch (const MyMoneyException &e) { unexpectedException(e); } m->setDirty(); try { ch = m->account("A000006"); QVERIFY(ch.value("Key") == "Value"); MyMoneyFileTransaction ft; m->addTransaction(t1); ft.commit(); QVERIFY(t1.id() == "T000000000000000001"); QVERIFY(t1.splitCount() == 2); QVERIFY(m->transactionCount(QString()) == 1); ch = m->account("A000006"); QVERIFY(ch.value("Key") == "Value"); } catch (const MyMoneyException &e) { unexpectedException(e); } try { // I spent some money, not so great s.clearId(); // enable re-usage of split variable s.setAccountId("A000004"); // Grosseries s.setShares(MyMoneyMoney(10000, 100)); s.setValue(MyMoneyMoney(10000, 100)); QVERIFY(s.id().isEmpty()); t2.addSplit(s); s.clearId(); // enable re-usage of split variable s.setAccountId("A000002"); // 16% sales tax s.setShares(MyMoneyMoney(1200, 100)); s.setValue(MyMoneyMoney(1200, 100)); QVERIFY(s.id().isEmpty()); t2.addSplit(s); s.clearId(); // enable re-usage of split variable s.setAccountId("A000003"); // 7% sales tax s.setShares(MyMoneyMoney(400, 100)); s.setValue(MyMoneyMoney(400, 100)); QVERIFY(s.id().isEmpty()); t2.addSplit(s); s.clearId(); // enable re-usage of split variable s.setAccountId("A000006"); // Checkings account s.setShares(MyMoneyMoney(-11600, 100)); s.setValue(MyMoneyMoney(-11600, 100)); QVERIFY(s.id().isEmpty()); t2.addSplit(s); t2.setPostDate(QDate(2002, 5, 9)); } catch (const MyMoneyException &e) { unexpectedException(e); } m->setDirty(); try { MyMoneyFileTransaction ft; m->addTransaction(t2); ft.commit(); QVERIFY(t2.id() == "T000000000000000002"); QVERIFY(t2.splitCount() == 4); QVERIFY(m->transactionCount(QString()) == 2); //QMap::ConstIterator it_k; MyMoneyTransactionFilter f; QList transactionList(m->transactionList(f)); QList::ConstIterator it_t(transactionList.constBegin()); QCOMPARE((*it_t).id(), QLatin1String("T000000000000000002")); ++it_t; ++it_t; ++it_t; ++it_t; ++it_t; QCOMPARE((*it_t).id(), QLatin1String("T000000000000000001")); ++it_t; QCOMPARE(it_t, transactionList.constEnd()); ch = m->account("A000006"); QCOMPARE(ch.value("Key"), QLatin1String("Value")); // check that the account's transaction list is updated QList list; MyMoneyTransactionFilter filter("A000006"); list = m->transactionList(filter); QCOMPARE(list.size(), 2); QList::ConstIterator it; it = list.constBegin(); //QVERIFY((*it).id() == "T000000000000000002"); QCOMPARE((*it), t2); ++it; QCOMPARE((*it), t1); ++it; QCOMPARE(it, list.constEnd()); } catch (const MyMoneyException &e) { unexpectedException(e); } } void MyMoneyStorageMgrTest::testTransactionCount() { testAttachDb(); if (!m_canOpen) QSKIP("Database test skipped because no database could be opened.", SkipAll); testAddTransactions(); QCOMPARE(m->transactionCount("A000001"), 0u); QCOMPARE(m->transactionCount("A000002"), 1u); QCOMPARE(m->transactionCount("A000003"), 1u); QCOMPARE(m->transactionCount("A000004"), 1u); QCOMPARE(m->transactionCount("A000005"), 1u); QCOMPARE(m->transactionCount("A000006"), 2u); } void MyMoneyStorageMgrTest::testAddBudget() { testAttachDb(); if (!m_canOpen) QSKIP("Database test skipped because no database could be opened.", SkipAll); MyMoneyBudget budget; budget.setName("TestBudget"); budget.setBudgetStart(QDate::currentDate()); MyMoneyFileTransaction ft; m->addBudget(budget); ft.commit(); QCOMPARE(m->budgetList().count(), 1); QCOMPARE(m->d_func()->m_nextBudgetID, 1ul); MyMoneyBudget newBudget = m->budgetByName("TestBudget"); QCOMPARE(budget.budgetStart(), newBudget.budgetStart()); QCOMPARE(budget.name(), newBudget.name()); } void MyMoneyStorageMgrTest::testCopyBudget() { testAddBudget(); if (!m_canOpen) QSKIP("Database test skipped because no database could be opened.", SkipAll); try { MyMoneyBudget oldBudget = m->budgetByName("TestBudget"); MyMoneyBudget newBudget = oldBudget; newBudget.clearId(); newBudget.setName(QString("Copy of %1").arg(oldBudget.name())); MyMoneyFileTransaction ft; m->addBudget(newBudget); ft.commit(); QCOMPARE(m->budgetList().count(), 2); QCOMPARE(m->d_func()->m_nextBudgetID, 2ul); MyMoneyBudget testBudget = m->budgetByName("TestBudget"); QCOMPARE(oldBudget.budgetStart(), testBudget.budgetStart()); QCOMPARE(oldBudget.name(), testBudget.name()); testBudget = m->budgetByName("Copy of TestBudget"); QCOMPARE(testBudget.budgetStart(), newBudget.budgetStart()); QCOMPARE(testBudget.name(), newBudget.name()); } catch (QString& s) { QFAIL(qPrintable(QString("Error in testCopyBudget(): %1").arg(qPrintable(s)))); } } void MyMoneyStorageMgrTest::testModifyBudget() { testAddBudget(); if (!m_canOpen) QSKIP("Database test skipped because no database could be opened.", SkipAll); MyMoneyBudget budget = m->budgetByName("TestBudget"); budget.setBudgetStart(QDate::currentDate().addDays(-1)); MyMoneyFileTransaction ft; m->modifyBudget(budget); ft.commit(); MyMoneyBudget newBudget = m->budgetByName("TestBudget"); QCOMPARE(budget.id(), newBudget.id()); QCOMPARE(budget.budgetStart(), newBudget.budgetStart()); QCOMPARE(budget.name(), newBudget.name()); } void MyMoneyStorageMgrTest::testRemoveBudget() { testAddBudget(); if (!m_canOpen) QSKIP("Database test skipped because no database could be opened.", SkipAll); MyMoneyBudget budget = m->budgetByName("TestBudget"); MyMoneyFileTransaction ft; m->removeBudget(budget); ft.commit(); try { budget = m->budgetByName("TestBudget"); // exception should be thrown if budget not found. QFAIL("Missing expected exception."); } catch (const MyMoneyException &) { QVERIFY(true); } } void MyMoneyStorageMgrTest::testBalance() { testAttachDb(); if (!m_canOpen) QSKIP("Database test skipped because no database could be opened.", SkipAll); testAddTransactions(); try { QVERIFY(m->balance("A000001", QDate()).isZero()); QCOMPARE(m->balance("A000002", QDate()), MyMoneyMoney(1200, 100)); QCOMPARE(m->balance("A000003", QDate()), MyMoneyMoney(400, 100)); //Add a transaction to zero account A000003 MyMoneyTransaction t1; MyMoneySplit s; s.setAccountId("A000003"); s.setShares(MyMoneyMoney(-400, 100)); s.setValue(MyMoneyMoney(-400, 100)); QVERIFY(s.id().isEmpty()); t1.addSplit(s); s.clearId(); // enable re-usage of split variable s.setAccountId("A000002"); s.setShares(MyMoneyMoney(400, 100)); s.setValue(MyMoneyMoney(400, 100)); QVERIFY(s.id().isEmpty()); t1.addSplit(s); t1.setPostDate(QDate(2007, 5, 10)); MyMoneyFileTransaction ft; m->addTransaction(t1); ft.commit(); QVERIFY(m->balance("A000003", QDate()).isZero()); QCOMPARE(m->totalBalance("A000001", QDate()), MyMoneyMoney(1600, 100)); QCOMPARE(m->balance("A000006", QDate(2002, 5, 9)), MyMoneyMoney(-11600, 100)); QCOMPARE(m->balance("A000005", QDate(2002, 5, 10)), MyMoneyMoney(-100000, 100)); QCOMPARE(m->balance("A000006", QDate(2002, 5, 10)), MyMoneyMoney(88400, 100)); } catch (const MyMoneyException &e) { unexpectedException(e); } } void MyMoneyStorageMgrTest::testModifyTransaction() { testAttachDb(); if (!m_canOpen) QSKIP("Database test skipped because no database could be opened.", SkipAll); testAddTransactions(); MyMoneyTransaction t = m->transaction("T000000000000000002"); MyMoneySplit s; MyMoneyAccount ch; // just modify simple stuff (splits) QVERIFY(t.splitCount() == 4); ch = m->account("A000006"); QVERIFY(ch.value("Key") == "Value"); s = t.splits()[0]; s.setShares(MyMoneyMoney(11000, 100)); s.setValue(MyMoneyMoney(11000, 100)); t.modifySplit(s); QVERIFY(t.splitCount() == 4); s = t.splits()[3]; s.setShares(MyMoneyMoney(-12600, 100)); s.setValue(MyMoneyMoney(-12600, 100)); t.modifySplit(s); ch = m->account("A000006"); QVERIFY(ch.value("Key") == "Value"); try { QVERIFY(m->balance("A000004", QDate()) == MyMoneyMoney(10000, 100)); QVERIFY(m->balance("A000006", QDate()) == MyMoneyMoney(100000 - 11600, 100)); QVERIFY(m->totalBalance("A000001", QDate()) == MyMoneyMoney(1600, 100)); MyMoneyFileTransaction ft; m->modifyTransaction(t); ft.commit(); ch = m->account("A000006"); QVERIFY(ch.value("Key") == "Value"); QVERIFY(m->balance("A000004", QDate()) == MyMoneyMoney(11000, 100)); QVERIFY(m->balance("A000006", QDate()) == MyMoneyMoney(100000 - 12600, 100)); QVERIFY(m->totalBalance("A000001", QDate()) == MyMoneyMoney(1600, 100)); } catch (const MyMoneyException &e) { unexpectedException(e); } // now modify the date t.setPostDate(QDate(2002, 5, 11)); try { ch = m->account("A000006"); QVERIFY(ch.value("Key") == "Value"); MyMoneyFileTransaction ft; m->modifyTransaction(t); ft.commit(); QVERIFY(m->balance("A000004", QDate()) == MyMoneyMoney(11000, 100)); QVERIFY(m->balance("A000006", QDate()) == MyMoneyMoney(100000 - 12600, 100)); QVERIFY(m->totalBalance("A000001", QDate()) == MyMoneyMoney(1600, 100)); //QMap::ConstIterator it_k; MyMoneyTransactionFilter f; QList transactionList(m->transactionList(f)); QList::ConstIterator it_t(transactionList.constBegin()); //it_k = m->m_transactionKeys.begin(); //QVERIFY((*it_k) == "2002-05-10-T000000000000000001"); QVERIFY((*it_t).id() == "T000000000000000001"); //++it_k; ++it_t; ++it_t; ++it_t; ++it_t; ++it_t; //QVERIFY((*it_k) == "2002-05-11-T000000000000000002"); QVERIFY((*it_t).id() == "T000000000000000002"); //++it_k; ++it_t; //QVERIFY(it_k == m->m_transactionKeys.end()); QVERIFY(it_t == transactionList.constEnd()); ch = m->account("A000006"); QVERIFY(ch.value("Key") == "Value"); // check that the account's transaction list is updated QList list; MyMoneyTransactionFilter filter("A000006"); list = m->transactionList(filter); QVERIFY(list.size() == 2); QList::ConstIterator it; it = list.constBegin(); QVERIFY((*it).id() == "T000000000000000001"); ++it; QVERIFY((*it).id() == "T000000000000000002"); ++it; QVERIFY(it == list.constEnd()); } catch (const MyMoneyException &e) { unexpectedException(e); } // Create another transaction MyMoneyTransaction t1; try { s.clearId(); // enable re-usage of split variable s.setAccountId("A000006"); // Checkings s.setShares(MyMoneyMoney(10000, 100)); s.setValue(MyMoneyMoney(10000, 100)); QVERIFY(s.id().isEmpty()); t1.addSplit(s); s.clearId(); // enable re-usage of split variable s.setAccountId("A000005"); // Salary s.setShares(MyMoneyMoney(-10000, 100)); s.setValue(MyMoneyMoney(-10000, 100)); QVERIFY(s.id().isEmpty()); t1.addSplit(s); t1.setPostDate(QDate(2002, 5, 10)); } catch (const MyMoneyException &e) { unexpectedException(e); } // Add it to the database MyMoneyFileTransaction ft; m->addTransaction(t1); ft.commit(); ch = m->account("A000005"); QVERIFY(ch.balance() == MyMoneyMoney(-100000 - 10000, 100)); QVERIFY(m->balance("A000005", QDate()) == MyMoneyMoney(-100000 - 10000, 100)); ch = m->account("A000006"); QVERIFY(ch.balance() == MyMoneyMoney(100000 - 12600 + 10000, 100)); QVERIFY(m->balance("A000006", QDate()) == MyMoneyMoney(100000 - 12600 + 10000, 100)); // Oops, the income was classified as Salary, but should have been // a refund from the grocery store. t1.splits()[1].setAccountId("A000004"); ft.restart(); m->modifyTransaction(t1); ft.commit(); // Make sure the account balances got updated correctly. ch = m->account("A000004"); QVERIFY(ch.balance() == MyMoneyMoney(11000 - 10000, 100)); QVERIFY(m->balance("A000004", QDate()) == MyMoneyMoney(11000 - 10000, 100)); ch = m->account("A000005"); QVERIFY(m->balance("A000005", QDate()) == MyMoneyMoney(-100000, 100)); QVERIFY(ch.balance() == MyMoneyMoney(-100000, 100)); ch = m->account("A000006"); QVERIFY(ch.balance() == MyMoneyMoney(100000 - 12600 + 10000, 100)); QVERIFY(m->balance("A000006", QDate()) == MyMoneyMoney(100000 - 12600 + 10000, 100)); } void MyMoneyStorageMgrTest::testRemoveUnusedAccount() { testAttachDb(); if (!m_canOpen) QSKIP("Database test skipped because no database could be opened.", SkipAll); testAccount2Institution(); MyMoneyAccount a = m->account("A000001"); MyMoneyInstitution i = m->institution("I000001"); m->setDirty(); // make sure, we cannot remove the standard account groups try { MyMoneyFileTransaction ft; m->removeAccount(m->liability()); ft.commit(); QFAIL("Exception expected"); } catch (const MyMoneyException &) { } try { MyMoneyFileTransaction ft; m->removeAccount(m->asset()); ft.commit(); QFAIL("Exception expected"); } catch (const MyMoneyException &) { } try { MyMoneyFileTransaction ft; m->removeAccount(m->expense()); ft.commit(); QFAIL("Exception expected"); } catch (const MyMoneyException &) { } try { MyMoneyFileTransaction ft; m->removeAccount(m->income()); ft.commit(); QFAIL("Exception expected"); } catch (const MyMoneyException &) { } // try to remove the account still attached to the institution try { MyMoneyFileTransaction ft; m->removeAccount(a); ft.commit(); QFAIL("Exception expected"); } catch (const MyMoneyException &) { } // now really remove an account try { // MyMoneyFile::instance()->preloadCache(); i = m->institution("I000001"); QVERIFY(i.accountCount() == 0); // QVERIFY(i.accountCount() == 1); QVERIFY(m->accountCount() == 7); a.setInstitutionId(QString()); MyMoneyFileTransaction ft; m->modifyAccount(a); ft.commit(); ft.restart(); m->removeAccount(a); ft.commit(); QVERIFY(m->accountCount() == 6); i = m->institution("I000001"); QVERIFY(i.accountCount() == 0); } catch (const MyMoneyException &e) { unexpectedException(e); } } void MyMoneyStorageMgrTest::testRemoveUsedAccount() { testAttachDb(); if (!m_canOpen) QSKIP("Database test skipped because no database could be opened.", SkipAll); testAddTransactions(); MyMoneyAccount a = m->account("A000006"); try { m->removeAccount(a); QFAIL("Exception expected"); } catch (const MyMoneyException &) { } } void MyMoneyStorageMgrTest::testRemoveInstitution() { testAttachDb(); if (!m_canOpen) QSKIP("Database test skipped because no database could be opened.", SkipAll); testModifyInstitution(); testReparentAccount(); MyMoneyInstitution i; MyMoneyAccount a; // assign the checkings account to the institution try { i = m->institution("I000001"); a = m->account("A000006"); a.setInstitutionId(i.id()); MyMoneyFileTransaction ft; m->modifyAccount(a); ft.commit(); QVERIFY(i.accountCount() == 0); } catch (const MyMoneyException &e) { unexpectedException(e); } m->setDirty(); // now remove the institution and see if the account survived ;-) try { MyMoneyFileTransaction ft; m->removeInstitution(i); a.setInstitutionId(QString()); m->modifyAccount(a); ft.commit(); a = m->account("A000006"); QVERIFY(a.institutionId().isEmpty()); QVERIFY(m->institutionCount() == 0); } catch (const MyMoneyException &e) { unexpectedException(e); } } void MyMoneyStorageMgrTest::testRemoveTransaction() { testAttachDb(); if (!m_canOpen) QSKIP("Database test skipped because no database could be opened.", SkipAll); testAddTransactions(); MyMoneyTransaction t = m->transaction("T000000000000000002"); m->setDirty(); try { MyMoneyFileTransaction ft; m->removeTransaction(t); ft.commit(); QVERIFY(m->transactionCount(QString()) == 1); } catch (const MyMoneyException &e) { unexpectedException(e); } } void MyMoneyStorageMgrTest::testTransactionList() { testAttachDb(); if (!m_canOpen) QSKIP("Database test skipped because no database could be opened.", SkipAll); testAddTransactions(); QList list; MyMoneyTransactionFilter filter("A000006"); list = m->transactionList(filter); QVERIFY(list.count() == 2); QVERIFY(list.at(0).id() == "T000000000000000002"); QVERIFY(list.at(1).id() == "T000000000000000001"); filter.clear(); filter.addAccount(QString("A000003")); list = m->transactionList(filter); QVERIFY(list.count() == 1); QVERIFY(list.at(0).id() == "T000000000000000002"); filter.clear(); list = m->transactionList(filter); QVERIFY(list.count() == 2); QVERIFY(list.at(0).id() == "T000000000000000002"); QVERIFY(list.at(1).id() == "T000000000000000001"); // test the date filtering while split filtering is active but with an empty filter filter.clear(); filter.addPayee(QString()); filter.setDateFilter(QDate(2002, 5, 10), QDate(2002, 5, 10)); list = m->transactionList(filter); QVERIFY(list.count() == 1); QVERIFY(list.at(0).id() == "T000000000000000001"); filter.clear(); filter.addAccount(QString()); filter.setDateFilter(QDate(2002, 5, 9), QDate(2002, 5, 9)); list = m->transactionList(filter); QVERIFY(list.count() == 0); // QVERIFY(list.at(0).id() == "T000000000000000002"); } void MyMoneyStorageMgrTest::testAddPayee() { testAttachDb(); if (!m_canOpen) QSKIP("Database test skipped because no database could be opened.", SkipAll); MyMoneyPayee p; p.setName("THB"); m->setDirty(); try { QVERIFY(m->d_func()->m_nextPayeeID == 0); MyMoneyFileTransaction ft; m->addPayee(p); ft.commit(); QVERIFY(m->d_func()->m_nextPayeeID == 1); MyMoneyPayee p1 = m->payeeByName("THB"); QVERIFY(p.id() == p1.id()); QVERIFY(p.name() == p1.name()); QVERIFY(p.address() == p1.address()); QVERIFY(p.city() == p1.city()); QVERIFY(p.state() == p1.state()); QVERIFY(p.postcode() == p1.postcode()); QVERIFY(p.telephone() == p1.telephone()); QVERIFY(p.email() == p1.email()); - MyMoneyPayee::payeeMatchType m1, m2; + eMyMoney::Payee::MatchType m1, m2; bool ignore, ignore1; QStringList keys, keys1; m1 = p.matchData(ignore, keys); m2 = p1.matchData(ignore1, keys1); QVERIFY(m1 == m2); QVERIFY(ignore == ignore1); QVERIFY(keys == keys1); QVERIFY(p.reference() == p1.reference()); QVERIFY(p.defaultAccountEnabled() == p1.defaultAccountEnabled()); QVERIFY(p.defaultAccountId() == p1.defaultAccountId()); QVERIFY(p == p1); } catch (const MyMoneyException &e) { unexpectedException(e); } } void MyMoneyStorageMgrTest::testSetAccountName() { testAttachDb(); if (!m_canOpen) QSKIP("Database test skipped because no database could be opened.", SkipAll); try { MyMoneyFileTransaction ft; m->setAccountName(stdAccNames[stdAccLiability], "Verbindlichkeiten"); ft.commit(); } catch (const MyMoneyException &e) { unexpectedException(e); } try { MyMoneyFileTransaction ft; m->setAccountName(stdAccNames[stdAccAsset], "Verm�gen"); ft.commit(); } catch (const MyMoneyException &e) { unexpectedException(e); } try { MyMoneyFileTransaction ft; m->setAccountName(stdAccNames[stdAccExpense], "Ausgaben"); ft.commit(); } catch (const MyMoneyException &e) { unexpectedException(e); } try { MyMoneyFileTransaction ft; m->setAccountName(stdAccNames[stdAccIncome], "Einnahmen"); ft.commit(); } catch (const MyMoneyException &e) { unexpectedException(e); } // MyMoneyFile::instance()->preloadCache(); try { QVERIFY(m->liability().name() == "Verbindlichkeiten"); QVERIFY(m->asset().name() == "Verm�gen"); QVERIFY(m->expense().name() == "Ausgaben"); QVERIFY(m->income().name() == "Einnahmen"); } catch (const MyMoneyException &e) { unexpectedException(e); } try { MyMoneyFileTransaction ft; m->setAccountName("A000001", "New account name"); ft.commit(); QFAIL("Exception expected"); } catch (const MyMoneyException &) { } } void MyMoneyStorageMgrTest::testModifyPayee() { testAttachDb(); if (!m_canOpen) QSKIP("Database test skipped because no database could be opened.", SkipAll); MyMoneyPayee p; testAddPayee(); p = m->payee("P000001"); p.setName("New name"); m->setDirty(); try { MyMoneyFileTransaction ft; m->modifyPayee(p); ft.commit(); p = m->payee("P000001"); QVERIFY(p.name() == "New name"); } catch (const MyMoneyException &e) { unexpectedException(e); } } void MyMoneyStorageMgrTest::testRemovePayee() { testAttachDb(); if (!m_canOpen) QSKIP("Database test skipped because no database could be opened.", SkipAll); testAddPayee(); m->setDirty(); // check that we can remove an unreferenced payee MyMoneyPayee p = m->payee("P000001"); try { QVERIFY(m->payeeList().count() == 1); MyMoneyFileTransaction ft; m->removePayee(p); ft.commit(); QVERIFY(m->payeeList().count() == 0); } catch (const MyMoneyException &e) { unexpectedException(e); } // add transaction testAddTransactions(); MyMoneyTransaction tr = m->transaction("T000000000000000001"); MyMoneySplit sp; sp = tr.splits()[0]; sp.setPayeeId("P000001"); tr.modifySplit(sp); // check that we cannot add a transaction referencing // an unknown payee try { MyMoneyFileTransaction ft; m->modifyTransaction(tr); ft.commit(); QFAIL("Expected exception"); } catch (const MyMoneyException &) { } // reset here, so that the // testAddPayee will not fail m->d_func()->m_nextPayeeID = 0; testAddPayee(); // check that it works when the payee exists try { MyMoneyFileTransaction ft; m->modifyTransaction(tr); ft.commit(); } catch (const MyMoneyException &e) { unexpectedException(e); } m->setDirty(); // now check, that we cannot remove the payee try { MyMoneyFileTransaction ft; m->removePayee(p); ft.commit(); QFAIL("Expected exception"); } catch (const MyMoneyException &) { } QVERIFY(m->payeeList().count() == 1); } void MyMoneyStorageMgrTest::testAddTag() { testAttachDb(); if (!m_canOpen) QSKIP("Database test skipped because no database could be opened.", SkipAll); MyMoneyTag ta; ta.setName("THB"); m->setDirty(); try { QVERIFY(m->d_func()->m_nextTagID == 0); MyMoneyFileTransaction ft; m->addTag(ta); ft.commit(); QVERIFY(m->d_func()->m_nextTagID == 1); MyMoneyTag ta1 = m->tagByName("THB"); QVERIFY(ta.id() == ta1.id()); QVERIFY(ta.name() == ta1.name()); QVERIFY(ta.isClosed() == ta1.isClosed()); QVERIFY(ta.tagColor().name() == ta1.tagColor().name()); QVERIFY(ta.notes() == ta1.notes()); QVERIFY(ta == ta1); } catch (const MyMoneyException &e) { unexpectedException(e); } } void MyMoneyStorageMgrTest::testModifyTag() { testAttachDb(); if (!m_canOpen) QSKIP("Database test skipped because no database could be opened.", SkipAll); MyMoneyTag ta; testAddTag(); ta = m->tag("G000001"); ta.setName("New name"); m->setDirty(); try { MyMoneyFileTransaction ft; m->modifyTag(ta); ft.commit(); ta = m->tag("G000001"); QVERIFY(ta.name() == "New name"); } catch (const MyMoneyException &e) { unexpectedException(e); } } void MyMoneyStorageMgrTest::testRemoveTag() { testAttachDb(); if (!m_canOpen) QSKIP("Database test skipped because no database could be opened.", SkipAll); testAddTag(); m->setDirty(); // check that we can remove an unreferenced tag MyMoneyTag ta = m->tag("G000001"); try { QVERIFY(m->tagList().count() == 1); MyMoneyFileTransaction ft; m->removeTag(ta); ft.commit(); QVERIFY(m->tagList().count() == 0); } catch (const MyMoneyException &e) { unexpectedException(e); } // add transaction testAddTransactions(); MyMoneyTransaction tr = m->transaction("T000000000000000001"); MyMoneySplit sp; sp = tr.splits()[0]; QList tagIdList; tagIdList << "G000001"; sp.setTagIdList(tagIdList); tr.modifySplit(sp); // check that we cannot add a transaction referencing // an unknown tag try { MyMoneyFileTransaction ft; m->modifyTransaction(tr); ft.commit(); QFAIL("Expected exception"); } catch (const MyMoneyException &) { } // reset here, so that the // testAddTag will not fail m->d_func()->m_nextTagID = 0; testAddTag(); // check that it works when the tag exists try { MyMoneyFileTransaction ft; m->modifyTransaction(tr); ft.commit(); } catch (const MyMoneyException &e) { unexpectedException(e); } m->setDirty(); // now check, that we cannot remove the tag try { MyMoneyFileTransaction ft; m->removeTag(ta); ft.commit(); QFAIL("Expected exception"); } catch (const MyMoneyException &) { } QVERIFY(m->tagList().count() == 1); } void MyMoneyStorageMgrTest::testRemoveAccountFromTree() { testAttachDb(); if (!m_canOpen) QSKIP("Database test skipped because no database could be opened.", SkipAll); MyMoneyAccount a, b, c; a.setName("Acc A"); b.setName("Acc B"); c.setName("Acc C"); // build a tree A -> B -> C, remove B and see if A -> C // remains in the storage manager try { MyMoneyFileTransaction ft; m->addAccount(a); m->addAccount(b); m->addAccount(c); m->reparentAccount(b, a); m->reparentAccount(c, b); ft.commit(); QVERIFY(a.accountList().count() == 1); QVERIFY(m->account(a.accountList()[0]).name() == "Acc B"); QVERIFY(b.accountList().count() == 1); QVERIFY(m->account(b.accountList()[0]).name() == "Acc C"); QVERIFY(c.accountList().count() == 0); ft.restart(); m->removeAccount(b); ft.commit(); // reload account info from titutionIDtorage a = m->account(a.id()); c = m->account(c.id()); try { b = m->account(b.id()); QFAIL("Exception expected"); } catch (const MyMoneyException &) { } QVERIFY(a.accountList().count() == 1); QVERIFY(m->account(a.accountList()[0]).name() == "Acc C"); QVERIFY(c.accountList().count() == 0); } catch (const MyMoneyException &e) { unexpectedException(e); } } void MyMoneyStorageMgrTest::testPayeeName() { testAttachDb(); if (!m_canOpen) QSKIP("Database test skipped because no database could be opened.", SkipAll); testAddPayee(); MyMoneyPayee p; QString name("THB"); // OK case try { p = m->payeeByName(name); QVERIFY(p.name() == "THB"); QVERIFY(p.id() == "P000001"); } catch (const MyMoneyException &e) { unexpectedException(e); } // Not OK case name = "Thb"; try { p = m->payeeByName(name); QFAIL("Exception expected"); } catch (const MyMoneyException &) { } } void MyMoneyStorageMgrTest::testTagName() { testAttachDb(); if (!m_canOpen) QSKIP("Database test skipped because no database could be opened.", SkipAll); testAddTag(); MyMoneyTag ta; QString name("THB"); // OK case try { ta = m->tagByName(name); QVERIFY(ta.name() == "THB"); QVERIFY(ta.id() == "G000001"); } catch (const MyMoneyException &e) { unexpectedException(e); } // Not OK case name = "Thb"; try { ta = m->tagByName(name); QFAIL("Exception expected"); } catch (const MyMoneyException &) { } } // disabled because of no real world use case //void MyMoneyStorageMgrTest::testAssignment() //{ // testAttachDb(); // if (!m_canOpen) // QSKIP("Database test skipped because no database could be opened.", SkipAll); // testAddTransactions(); // MyMoneyPayee user; // user.setName("Thomas"); // m->setUser(user); // MyMoneyStorageMgr test = *m; // testEquality(&test); //} void MyMoneyStorageMgrTest::testEquality(const MyMoneyStorageMgr *t) { testAttachDb(); if (!m_canOpen) QSKIP("Database test skipped because no database could be opened.", SkipAll); QVERIFY(m->user().name() == t->user().name()); QVERIFY(m->user().address() == t->user().address()); QVERIFY(m->user().city() == t->user().city()); QVERIFY(m->user().state() == t->user().state()); QVERIFY(m->user().postcode() == t->user().postcode()); QVERIFY(m->user().telephone() == t->user().telephone()); QVERIFY(m->user().email() == t->user().email()); //QVERIFY(m->nextInstitutionID() == t->nextInstitutionID()); //QVERIFY(m->nextAccountID() == t->nextAccountID()); //QVERIFY(m->m_nextTransactionID == t->m_nextTransactionID); //QVERIFY(m->nextPayeeID() == t->nextPayeeID()); //QVERIFY(m->m_nextScheduleID == t->m_nextScheduleID); QVERIFY(m->dirty() == t->dirty()); QVERIFY(m->creationDate() == t->creationDate()); QVERIFY(m->lastModificationDate() == t->lastModificationDate()); /* * make sure, that the keys and values are the same * on the left and the right side */ //QVERIFY(m->payeeList().keys() == t->payeeList().keys()); //QVERIFY(m->payeeList().values() == t->payeeList().values()); QVERIFY(m->payeeList() == t->payeeList()); QVERIFY(m->tagList() == t->tagList()); //QVERIFY(m->m_transactionKeys.keys() == t->m_transactionKeys.keys()); //QVERIFY(m->m_transactionKeys.values() == t->m_transactionKeys.values()); //QVERIFY(m->institutionList().keys() == t->institutionList().keys()); //QVERIFY(m->institutionList().values() == t->institutionList().values()); //QVERIFY(m->m_accountList.keys() == t->m_accountList.keys()); //QVERIFY(m->m_accountList.values() == t->m_accountList.values()); //QVERIFY(m->m_transactionList.keys() == t->m_transactionList.keys()); //QVERIFY(m->m_transactionList.values() == t->m_transactionList.values()); //QVERIFY(m->m_balanceCache.keys() == t->m_balanceCache.keys()); //QVERIFY(m->m_balanceCache.values() == t->m_balanceCache.values()); // QVERIFY(m->scheduleList().keys() == t->scheduleList().keys()); // QVERIFY(m->scheduleList().values() == t->scheduleList().values()); } // disabled because of no real world use case //void MyMoneyStorageMgrTest::testDuplicate() //{ // testAttachDb(); // if (!m_canOpen) // QSKIP("Database test skipped because no database could be opened.", SkipAll); // const MyMoneyStorageMgr* t; // testModifyTransaction(); // t = m->duplicate(); // testEquality(t); // delete t; //} void MyMoneyStorageMgrTest::testAddSchedule() { /* Note addSchedule() now calls validate as it should * so we need an account id. Later this will * be checked to make sure its a valid account id. The * tests currently fail because no splits are defined * for the schedules transaction. */ testAttachDb(); if (!m_canOpen) QSKIP("Database test skipped because no database could be opened.", SkipAll); // put some accounts in the db, so the tests don't break testReparentAccount(); try { QVERIFY(m->scheduleList(QString(), Schedule::Type::Any, Schedule::Occurrence::Any, Schedule::PaymentType::Any, QDate(), QDate(), false).count() == 0); MyMoneyTransaction t1; MyMoneySplit s1, s2; s1.setAccountId("A000001"); t1.addSplit(s1); s2.setAccountId("A000002"); t1.addSplit(s2); MyMoneySchedule schedule("Sched-Name", Schedule::Type::Deposit, Schedule::Occurrence::Daily, 1, Schedule::PaymentType::ManualDeposit, QDate(), QDate(), true, false); t1.setPostDate(QDate(2003, 7, 10)); schedule.setTransaction(t1); MyMoneyFileTransaction ft; m->addSchedule(schedule); ft.commit(); QVERIFY(m->scheduleList(QString(), Schedule::Type::Any, Schedule::Occurrence::Any, Schedule::PaymentType::Any, QDate(), QDate(), false).count() == 1); QVERIFY(schedule.id() == "SCH000001"); //MyMoneyFile::instance()->clearCache(); // test passes without this, so why is it here for? QVERIFY(m->schedule("SCH000001").id() == "SCH000001"); } catch (const MyMoneyException &e) { unexpectedException(e); } try { MyMoneySchedule schedule("Sched-Name", Schedule::Type::Deposit, Schedule::Occurrence::Daily, 1, Schedule::PaymentType::ManualDeposit, QDate(), QDate(), true, false); MyMoneyFileTransaction ft; m->addSchedule(schedule); ft.commit(); QFAIL("Exception expected"); } catch (const MyMoneyException &) { } QVERIFY(m->scheduleList(QString(), Schedule::Type::Any, Schedule::Occurrence::Any, Schedule::PaymentType::Any, QDate(), QDate(), false).count() == 1); // now try with a bad account, so this should cause an exception try { MyMoneyTransaction t1; MyMoneySplit s1, s2; s1.setAccountId("Abadaccount1"); t1.addSplit(s1); s2.setAccountId("Abadaccount2"); //t1.addSplit(s2); MyMoneySchedule schedule("Sched-Name", Schedule::Type::Deposit, Schedule::Occurrence::Daily, 1, Schedule::PaymentType::ManualDeposit, QDate(), QDate(), true, false); t1.setPostDate(QDate(2003, 7, 10)); schedule.setTransaction(t1); MyMoneyFileTransaction ft; m->addSchedule(schedule); ft.commit(); QFAIL("Exception expected, but not thrown"); } catch (const MyMoneyException &) { // Exception caught as expected. } QVERIFY(m->scheduleList(QString(), Schedule::Type::Any, Schedule::Occurrence::Any, Schedule::PaymentType::Any, QDate(), QDate(), false).count() == 1); } void MyMoneyStorageMgrTest::testSchedule() { testAttachDb(); if (!m_canOpen) QSKIP("Database test skipped because no database could be opened.", SkipAll); testAddSchedule(); MyMoneySchedule sched; sched = m->schedule("SCH000001"); QVERIFY(sched.name() == "Sched-Name"); QVERIFY(sched.id() == "SCH000001"); try { m->schedule("SCH000003"); QFAIL("Exception expected"); } catch (const MyMoneyException &) { } } void MyMoneyStorageMgrTest::testModifySchedule() { testAttachDb(); if (!m_canOpen) QSKIP("Database test skipped because no database could be opened.", SkipAll); testAddSchedule(); MyMoneySchedule sched; sched = m->schedule("SCH000001"); sched.d_func()->setId("SCH000002"); try { MyMoneyFileTransaction ft; m->modifySchedule(sched); ft.commit(); QFAIL("Exception expected"); } catch (const MyMoneyException &) { } sched = m->schedule("SCH000001"); sched.setName("New Sched-Name"); try { MyMoneyFileTransaction ft; m->modifySchedule(sched); ft.commit(); auto sch = m->scheduleList(QString(), Schedule::Type::Any, Schedule::Occurrence::Any, Schedule::PaymentType::Any, QDate(), QDate(), false); QVERIFY(sch.count() == 1); QVERIFY((*(sch.begin())).name() == "New Sched-Name"); QVERIFY((*(sch.begin())).id() == "SCH000001"); } catch (const MyMoneyException &e) { unexpectedException(e); } } void MyMoneyStorageMgrTest::testRemoveSchedule() { testAttachDb(); if (!m_canOpen) QSKIP("Database test skipped because no database could be opened.", SkipAll); testAddSchedule(); MyMoneySchedule sched; sched = m->schedule("SCH000001"); sched.d_func()->setId("SCH000003"); try { MyMoneyFileTransaction ft; m->removeSchedule(sched); ft.commit(); QFAIL("Exception expected"); } catch (const MyMoneyException &) { } sched = m->schedule("SCH000001"); try { MyMoneyFileTransaction ft; m->removeSchedule(sched); ft.commit(); QVERIFY(m->scheduleList(QString(), Schedule::Type::Any, Schedule::Occurrence::Any, Schedule::PaymentType::Any, QDate(), QDate(), false).count() == 0); } catch (const MyMoneyException &e) { unexpectedException(e); } } void MyMoneyStorageMgrTest::testScheduleList() { testAttachDb(); if (!m_canOpen) QSKIP("Database test skipped because no database could be opened.", SkipAll); // put some accounts in the db, so the tests don't break testReparentAccount(); QDate testDate = QDate::currentDate(); QDate notOverdue = testDate.addDays(2); QDate overdue = testDate.addDays(-2); MyMoneyTransaction t1; MyMoneySplit s1, s2; s1.setAccountId("A000001"); t1.addSplit(s1); s2.setAccountId("A000002"); t1.addSplit(s2); MyMoneySchedule schedule1("Schedule 1", Schedule::Type::Bill, Schedule::Occurrence::Once, 1, Schedule::PaymentType::DirectDebit, QDate(), QDate(), false, false); t1.setPostDate(notOverdue); schedule1.setTransaction(t1); schedule1.setLastPayment(notOverdue); MyMoneyTransaction t2; MyMoneySplit s3, s4; s3.setAccountId("A000001"); t2.addSplit(s3); s4.setAccountId("A000003"); t2.addSplit(s4); MyMoneySchedule schedule2("Schedule 2", Schedule::Type::Deposit, Schedule::Occurrence::Daily, 1, Schedule::PaymentType::DirectDeposit, QDate(), QDate(), false, false); t2.setPostDate(notOverdue.addDays(1)); schedule2.setTransaction(t2); schedule2.setLastPayment(notOverdue.addDays(1)); MyMoneyTransaction t3; MyMoneySplit s5, s6; s5.setAccountId("A000005"); t3.addSplit(s5); s6.setAccountId("A000006"); t3.addSplit(s6); MyMoneySchedule schedule3("Schedule 3", Schedule::Type::Transfer, Schedule::Occurrence::Weekly, 1, Schedule::PaymentType::Other, QDate(), QDate(), false, false); t3.setPostDate(notOverdue.addDays(2)); schedule3.setTransaction(t3); schedule3.setLastPayment(notOverdue.addDays(2)); MyMoneyTransaction t4; MyMoneySplit s7, s8; s7.setAccountId("A000005"); t4.addSplit(s7); s8.setAccountId("A000006"); t4.addSplit(s8); MyMoneySchedule schedule4("Schedule 4", Schedule::Type::Bill, Schedule::Occurrence::Weekly, 1, Schedule::PaymentType::WriteChecque, QDate(), notOverdue.addDays(31), false, false); t4.setPostDate(overdue.addDays(-7)); schedule4.setTransaction(t4); try { MyMoneyFileTransaction ft; m->addSchedule(schedule1); m->addSchedule(schedule2); m->addSchedule(schedule3); m->addSchedule(schedule4); ft.commit(); } catch (const MyMoneyException &e) { unexpectedException(e); } QList list; // no filter list = m->scheduleList(QString(), Schedule::Type::Any, Schedule::Occurrence::Any, Schedule::PaymentType::Any, QDate(), QDate(), false); QVERIFY(list.count() == 4); // filter by type list = m->scheduleList(QString(), Schedule::Type::Bill, Schedule::Occurrence::Any, Schedule::PaymentType::Any, QDate(), QDate(), false); QVERIFY(list.count() == 2); QVERIFY(list[0].name() == "Schedule 1"); QVERIFY(list[1].name() == "Schedule 4"); // filter by occurrence list = m->scheduleList(QString(), Schedule::Type::Any, Schedule::Occurrence::Daily, Schedule::PaymentType::Any, QDate(), QDate(), false); QVERIFY(list.count() == 1); QVERIFY(list[0].name() == "Schedule 2"); // filter by payment type list = m->scheduleList(QString(), Schedule::Type::Any, Schedule::Occurrence::Any, Schedule::PaymentType::DirectDeposit, QDate(), QDate(), false); QVERIFY(list.count() == 1); QVERIFY(list[0].name() == "Schedule 2"); // filter by account list = m->scheduleList("A01", Schedule::Type::Any, Schedule::Occurrence::Any, Schedule::PaymentType::Any, QDate(), QDate(), false); QVERIFY(list.count() == 0); list = m->scheduleList("A000001", Schedule::Type::Any, Schedule::Occurrence::Any, Schedule::PaymentType::Any, QDate(), QDate(), false); QVERIFY(list.count() == 2); list = m->scheduleList("A000002", Schedule::Type::Any, Schedule::Occurrence::Any, Schedule::PaymentType::Any, QDate(), QDate(), false); QVERIFY(list.count() == 1); // filter by start date list = m->scheduleList(QString(), Schedule::Type::Any, Schedule::Occurrence::Any, Schedule::PaymentType::Any, notOverdue.addDays(31), QDate(), false); QVERIFY(list.count() == 3); QVERIFY(list[0].name() == "Schedule 2"); QVERIFY(list[1].name() == "Schedule 3"); QVERIFY(list[2].name() == "Schedule 4"); // filter by end date list = m->scheduleList(QString(), Schedule::Type::Any, Schedule::Occurrence::Any, Schedule::PaymentType::Any, QDate(), notOverdue.addDays(1), false); QVERIFY(list.count() == 3); QVERIFY(list[0].name() == "Schedule 1"); QVERIFY(list[1].name() == "Schedule 2"); QVERIFY(list[2].name() == "Schedule 4"); // filter by start and end date list = m->scheduleList(QString(), Schedule::Type::Any, Schedule::Occurrence::Any, Schedule::PaymentType::Any, notOverdue.addDays(-1), notOverdue.addDays(1), false); QVERIFY(list.count() == 2); QVERIFY(list[0].name() == "Schedule 1"); QVERIFY(list[1].name() == "Schedule 2"); // filter by overdue status list = m->scheduleList(QString(), Schedule::Type::Any, Schedule::Occurrence::Any, Schedule::PaymentType::Any, QDate(), QDate(), true); QVERIFY(list.count() == 1); QVERIFY(list[0].name() == "Schedule 4"); } void MyMoneyStorageMgrTest::testAddCurrency() { testAttachDb(); if (!m_canOpen) QSKIP("Database test skipped because no database could be opened.", SkipAll); MyMoneySecurity curr("EUR", "Euro", "?", 100, 100); QVERIFY(m->currencyList().count() == 0); m->setDirty(); try { MyMoneyFileTransaction ft; m->addCurrency(curr); ft.commit(); QVERIFY(m->currencyList().count() == 1); QVERIFY((*(m->currencyList().begin())).name() == "Euro"); QVERIFY((*(m->currencyList().begin())).id() == "EUR"); } catch (const MyMoneyException &e) { unexpectedException(e); } m->setDirty(); try { MyMoneyFileTransaction ft; m->addCurrency(curr); ft.commit(); QFAIL("Expected exception missing"); } catch (const MyMoneyException &) { QVERIFY(m->dirty() == true); } } void MyMoneyStorageMgrTest::testModifyCurrency() { testAttachDb(); if (!m_canOpen) QSKIP("Database test skipped because no database could be opened.", SkipAll); MyMoneySecurity curr("EUR", "Euro", "?", 100, 100); testAddCurrency(); m->setDirty(); curr.setName("EURO"); try { MyMoneyFileTransaction ft; m->modifyCurrency(curr); ft.commit(); QVERIFY(m->currencyList().count() == 1); QVERIFY((*(m->currencyList().begin())).name() == "EURO"); QVERIFY((*(m->currencyList().begin())).id() == "EUR"); } catch (const MyMoneyException &e) { unexpectedException(e); } m->setDirty(); MyMoneySecurity unknownCurr("DEM", "Deutsche Mark", "DM", 100, 100); try { m->modifyCurrency(unknownCurr); QFAIL("Expected exception missing"); } catch (const MyMoneyException &) { QVERIFY(m->dirty() == true); } } void MyMoneyStorageMgrTest::testRemoveCurrency() { testAttachDb(); if (!m_canOpen) QSKIP("Database test skipped because no database could be opened.", SkipAll); MyMoneySecurity curr("EUR", "Euro", "?", 100, 100); testAddCurrency(); m->setDirty(); try { MyMoneyFileTransaction ft; m->removeCurrency(curr); ft.commit(); QVERIFY(m->currencyList().count() == 0); } catch (const MyMoneyException &e) { unexpectedException(e); } m->setDirty(); MyMoneySecurity unknownCurr("DEM", "Deutsche Mark", "DM", 100, 100); try { MyMoneyFileTransaction ft; m->removeCurrency(unknownCurr); ft.commit(); QFAIL("Expected exception missing"); } catch (const MyMoneyException &) { QVERIFY(m->dirty() == true); } } void MyMoneyStorageMgrTest::testCurrency() { testAttachDb(); if (!m_canOpen) QSKIP("Database test skipped because no database could be opened.", SkipAll); MyMoneySecurity curr("EUR", "Euro", "?", 100, 100); MyMoneySecurity newCurr; testAddCurrency(); m->setDirty(); try { newCurr = m->currency("EUR"); QVERIFY(m->dirty() == true); QVERIFY(newCurr.id() == curr.id()); QVERIFY(newCurr.name() == curr.name()); } catch (const MyMoneyException &e) { unexpectedException(e); } try { m->currency("DEM"); QFAIL("Expected exception missing"); } catch (const MyMoneyException &) { QVERIFY(m->dirty() == true); } } void MyMoneyStorageMgrTest::testCurrencyList() { testAttachDb(); if (!m_canOpen) QSKIP("Database test skipped because no database could be opened.", SkipAll); QVERIFY(m->currencyList().count() == 0); testAddCurrency(); QVERIFY(m->currencyList().count() == 1); MyMoneySecurity unknownCurr("DEM", "Deutsche Mark", "DM", 100, 100); try { MyMoneyFileTransaction ft; m->addCurrency(unknownCurr); ft.commit(); m->setDirty(); QVERIFY(m->currencyList().count() == 2); QVERIFY(m->currencyList().count() == 2); QVERIFY(m->dirty() == true); } catch (const MyMoneyException &e) { unexpectedException(e); } } void MyMoneyStorageMgrTest::testAccountList() { testAttachDb(); if (!m_canOpen) QSKIP("Database test skipped because no database could be opened.", SkipAll); QList accounts; m->accountList(accounts); QVERIFY(accounts.count() == 0); testAddNewAccount(); accounts.clear(); m->accountList(accounts); QVERIFY(accounts.count() == 2); MyMoneyAccount a = m->account("A000001"); MyMoneyAccount b = m->account("A000002"); MyMoneyFileTransaction ft; m->reparentAccount(b, a); ft.commit(); accounts.clear(); m->accountList(accounts); QVERIFY(accounts.count() == 2); } void MyMoneyStorageMgrTest::testAddOnlineJob() { testAttachDb(); if (!m_canOpen) QSKIP("Database test skipped because no database could be opened.", SkipAll); // Add a onlineJob onlineJob job(new dummyTask()); QCOMPARE(m->onlineJobList().count(), 0); m->setDirty(); QSKIP("Test not fully implemented, yet.", SkipAll); #if 0 try { m->addOnlineJob(job); QCOMPARE(m->onlineJobList().count(), 1); QCOMPARE((*(m->onlineJobList().begin())).id(), QLatin1String("O00000001")); } catch (const MyMoneyException &e) { unexpectedException(e); } // Try to re-add the same job. It should fail. m->setDirty(); try { MyMoneyFileTransaction ft; m->addOnlineJob(job); ft.commit(); QFAIL("Expected exception missing"); } catch (const MyMoneyException &) { QCOMPARE(m->dirty(), false); } #endif } void MyMoneyStorageMgrTest::testModifyOnlineJob() { testAttachDb(); if (!m_canOpen) QSKIP("Database test skipped because no database could be opened.", SkipAll); onlineJob job(new dummyTask()); testAddOnlineJob(); m->setDirty(); QSKIP("Test not fully implemented, yet.", SkipAll); #if 0 // update online job try { MyMoneyFileTransaction ft; m->modifyOnlineJob(job); ft.commit(); QVERIFY(m->onlineJobList().count() == 1); //QVERIFY((*(m->onlineJobList().begin())).name() == "EURO"); QVERIFY((*(m->onlineJobList().begin())).id() == "O00000001"); } catch (const MyMoneyException &e) { unexpectedException(e); } m->setDirty(); onlineJob unknownJob(new dummyTask()); try { MyMoneyFileTransaction ft; m->modifyOnlineJob(unknownJob); ft.commit(); QFAIL("Expected exception missing"); } catch (const MyMoneyException &) { QVERIFY(m->dirty() == false); } #endif } void MyMoneyStorageMgrTest::testRemoveOnlineJob() { testAttachDb(); if (!m_canOpen) QSKIP("Database test skipped because no database could be opened.", SkipAll); onlineJob job(new dummyTask()); testAddOnlineJob(); m->setDirty(); QSKIP("Test not fully implemented, yet.", SkipAll); #if 0 try { MyMoneyFileTransaction ft; m->removeOnlineJob(job); ft.commit(); QVERIFY(m->onlineJobList().count() == 0); } catch (const MyMoneyException &e) { unexpectedException(e); } m->setDirty(); onlineJob unknownJob(new dummyTask()); try { MyMoneyFileTransaction ft; m->removeOnlineJob(unknownJob); ft.commit(); QFAIL("Expected exception missing"); } catch (const MyMoneyException &) { QVERIFY(m->dirty() == false); } #endif } void MyMoneyStorageMgrTest::testHighestNumberFromIdString() { testAttachDb(); if (!m_canOpen) QSKIP("Database test skipped because no database could be opened.", SkipAll); testAddTransactions(); // disabled since unification of storages // QCOMPARE(m->d_func()->m_sql->d_func()->highestNumberFromIdString(QLatin1String("kmmTransactions"), QLatin1String("id"), 1), 2ul); // QCOMPARE(m->d_func()->m_sql->d_func()->highestNumberFromIdString(QLatin1String("kmmAccounts"), QLatin1String("id"), 1), 6ul); } diff --git a/kmymoney/plugins/views/budget/kbudgetvalues.cpp b/kmymoney/plugins/views/budget/kbudgetvalues.cpp index 44508ff83..5aa172952 100644 --- a/kmymoney/plugins/views/budget/kbudgetvalues.cpp +++ b/kmymoney/plugins/views/budget/kbudgetvalues.cpp @@ -1,386 +1,387 @@ /*************************************************************************** kbudgetvalues - description ------------------- begin : Wed Nov 28 2007 copyright : (C) 2007 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 "kbudgetvalues.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "ui_kbudgetvalues.h" #include "mymoneybudget.h" #include "kmymoneyedit.h" #include "kmymoneysettings.h" +#include "mymoneyenums.h" class KBudgetValuesPrivate { Q_DISABLE_COPY(KBudgetValuesPrivate) public: KBudgetValuesPrivate() : ui(new Ui::KBudgetValues), m_currentTab(nullptr) { for (int i = 0; i < 12; ++i) { m_label[i] = nullptr; m_field[i] = nullptr; } } ~KBudgetValuesPrivate() { delete ui; } void enableMonths(bool enabled) { for (int i = 1; i < 12; ++i) { m_label[i]->setEnabled(enabled); m_field[i]->setEnabled(enabled); } } void fillMonthLabels() { QDate date(m_budgetDate); for (auto i = 0; i < 12; ++i) { m_label[i]->setText(QLocale().standaloneMonthName(date.month(), QLocale::ShortFormat)); date = date.addMonths(1); } } Ui::KBudgetValues *ui; KMyMoneyEdit* m_field[12]; QLabel* m_label[12]; QWidget* m_currentTab; QDate m_budgetDate; }; KBudgetValues::KBudgetValues(QWidget* parent) : QWidget(parent), d_ptr(new KBudgetValuesPrivate) { Q_D(KBudgetValues); d->ui->setupUi(this); d->m_currentTab = d->ui->m_monthlyButton; d->m_budgetDate = QDate(2007, KMyMoneySettings::firstFiscalMonth(), KMyMoneySettings::firstFiscalDay()); d->m_field[0] = d->ui->m_amount1; d->m_field[1] = d->ui->m_amount2; d->m_field[2] = d->ui->m_amount3; d->m_field[3] = d->ui->m_amount4; d->m_field[4] = d->ui->m_amount5; d->m_field[5] = d->ui->m_amount6; d->m_field[6] = d->ui->m_amount7; d->m_field[7] = d->ui->m_amount8; d->m_field[8] = d->ui->m_amount9; d->m_field[9] = d->ui->m_amount10; d->m_field[10] = d->ui->m_amount11; d->m_field[11] = d->ui->m_amount12; d->m_label[0] = d->ui->m_label1; d->m_label[1] = d->ui->m_label2; d->m_label[2] = d->ui->m_label3; d->m_label[3] = d->ui->m_label4; d->m_label[4] = d->ui->m_label5; d->m_label[5] = d->ui->m_label6; d->m_label[6] = d->ui->m_label7; d->m_label[7] = d->ui->m_label8; d->m_label[8] = d->ui->m_label9; d->m_label[9] = d->ui->m_label10; d->m_label[10] = d->ui->m_label11; d->m_label[11] = d->ui->m_label12; // fill with standard labels d->ui->m_monthlyButton->setChecked(true); d->ui->m_periodGroup->setId(d->ui->m_monthlyButton, 0); d->ui->m_periodGroup->setId(d->ui->m_yearlyButton, 1); d->ui->m_periodGroup->setId(d->ui->m_individualButton, 2); slotChangePeriod(d->ui->m_periodGroup->id(d->ui->m_monthlyButton)); connect(d->ui->m_amountMonthly, &KMyMoneyEdit::valueChanged, this, &KBudgetValues::slotNeedUpdate); connect(d->ui->m_amountYearly, &KMyMoneyEdit::valueChanged, this, &KBudgetValues::slotNeedUpdate); d->ui->m_amountMonthly->installEventFilter(this); d->ui->m_amountYearly->installEventFilter(this); for (auto i = 0; i < 12; ++i) { connect(d->m_field[i], &KMyMoneyEdit::valueChanged, this, &KBudgetValues::slotNeedUpdate); d->m_field[i]->installEventFilter(this); } connect(d->ui->m_clearButton, &QAbstractButton::clicked, this, &KBudgetValues::slotClearAllValues); connect(d->ui->m_periodGroup, static_cast(&QButtonGroup::buttonClicked), this, &KBudgetValues::slotChangePeriod); connect(this, &KBudgetValues::valuesChanged, this, &KBudgetValues::slotUpdateClearButton); KGuiItem clearItem(KStandardGuiItem::clear()); KGuiItem::assign(d->ui->m_clearButton, clearItem); d->ui->m_clearButton->setText(QString()); d->ui->m_clearButton->setToolTip(clearItem.toolTip()); } KBudgetValues::~KBudgetValues() { Q_D(KBudgetValues); delete d; } bool KBudgetValues::eventFilter(QObject* o, QEvent* e) { auto rc = false; if (o->isWidgetType() && (e->type() == QEvent::KeyPress)) { if (auto k = dynamic_cast(e)) { if ((k->modifiers() & Qt::KeyboardModifierMask) == 0 || (k->modifiers() & Qt::KeypadModifier) != 0) { QKeyEvent evt(e->type(), Qt::Key_Tab, k->modifiers(), QString(), k->isAutoRepeat(), k->count()); switch (k->key()) { case Qt::Key_Return: case Qt::Key_Enter: // send out a TAB key event QApplication::sendEvent(o, &evt); // don't process this one any further rc = true; break; default: break; } } } } return rc; } void KBudgetValues::clear() { Q_D(KBudgetValues); blockSignals(true); for (auto i = 0; i < 12; ++i) d->m_field[i]->setValue(MyMoneyMoney()); d->ui->m_amountMonthly->setValue(MyMoneyMoney()); d->ui->m_amountYearly->setValue(MyMoneyMoney()); blockSignals(false); } void KBudgetValues::slotClearAllValues() { Q_D(KBudgetValues); int tab = d->ui->m_periodGroup->checkedId(); if (tab == d->ui->m_periodGroup->id(d->ui->m_monthlyButton)) { d->ui->m_amountMonthly->setValue(MyMoneyMoney()); } else if (tab == d->ui->m_periodGroup->id(d->ui->m_yearlyButton)) { d->ui->m_amountYearly->setValue(MyMoneyMoney()); } else if (tab == d->ui->m_periodGroup->id(d->ui->m_individualButton)) { for (auto i = 0; i < 12; ++i) d->m_field[i]->setValue(MyMoneyMoney()); } emit valuesChanged(); } void KBudgetValues::slotChangePeriod(int id) { Q_D(KBudgetValues); // Prevent a recursive entry of this method due to widget changes // performed during execution of this method static bool inside = false; if (inside) return; inside = true; QWidget *tab = d->ui->m_periodGroup->button(id); d->fillMonthLabels(); MyMoneyMoney newValue; if (tab == d->ui->m_monthlyButton) { d->ui->m_firstItemStack->setCurrentIndex(d->ui->m_firstItemStack->indexOf(d->ui->m_monthlyPage)); d->enableMonths(false); d->m_label[0]->setText(" "); if (d->ui->m_amountMonthly->value().isZero()) { if (d->m_currentTab == d->ui->m_yearlyButton) { newValue = (d->ui->m_amountYearly->value() / MyMoneyMoney(12, 1)).convert(); } else if (d->m_currentTab == d->ui->m_individualButton) { for (auto i = 0; i < 12; ++i) newValue += d->m_field[i]->value(); newValue = (newValue / MyMoneyMoney(12, 1)).convert(); } if (!newValue.isZero()) { if (KMessageBox::questionYesNo(this, QString("") + i18n("You have entered budget values using a different base which would result in a monthly budget of %1. Should this value be used to fill the monthly budget?", newValue.formatMoney(QString(), 2)) + QString(""), i18nc("Auto assignment (caption)", "Auto assignment"), KStandardGuiItem::yes(), KStandardGuiItem::no(), "use_previous_budget_values") == KMessageBox::Yes) { d->ui->m_amountMonthly->setValue(newValue); } } } } else if (tab == d->ui->m_yearlyButton) { d->ui->m_firstItemStack->setCurrentIndex(d->ui->m_firstItemStack->indexOf(d->ui->m_yearlyPage)); d->enableMonths(false); d->m_label[0]->setText(" "); if (d->ui->m_amountYearly->value().isZero()) { if (d->m_currentTab == d->ui->m_monthlyButton) { newValue = (d->ui->m_amountMonthly->value() * MyMoneyMoney(12, 1)).convert(); } else if (d->m_currentTab == d->ui->m_individualButton) { for (auto i = 0; i < 12; ++i) newValue += d->m_field[i]->value(); } if (!newValue.isZero()) { if (KMessageBox::questionYesNo(this, QString("") + i18n("You have entered budget values using a different base which would result in a yearly budget of %1. Should this value be used to fill the monthly budget?", newValue.formatMoney(QString(), 2)) + QString(""), i18nc("Auto assignment (caption)", "Auto assignment"), KStandardGuiItem::yes(), KStandardGuiItem::no(), "use_previous_budget_values") == KMessageBox::Yes) { d->ui->m_amountYearly->setValue(newValue); } } } } else if (tab == d->ui->m_individualButton) { d->ui->m_firstItemStack->setCurrentIndex(d->ui->m_firstItemStack->indexOf(d->ui->m_individualPage)); d->enableMonths(true); for (auto i = 0; i < 12; ++i) newValue += d->m_field[i]->value(); if (newValue.isZero()) { if (d->m_currentTab == d->ui->m_monthlyButton) { newValue = d->ui->m_amountMonthly->value(); } else if (d->m_currentTab == d->ui->m_yearlyButton) { newValue = (d->ui->m_amountYearly->value() / MyMoneyMoney(12, 1)).convert(); } if (!newValue.isZero()) { if (KMessageBox::questionYesNo(this, QString("") + i18n("You have entered budget values using a different base which would result in an individual monthly budget of %1. Should this value be used to fill the monthly budgets?", newValue.formatMoney(QString(), 2)) + QString(""), i18nc("Auto assignment (caption)", "Auto assignment"), KStandardGuiItem::yes(), KStandardGuiItem::no(), "use_previous_budget_values") == KMessageBox::Yes) { for (auto i = 0; i < 12; ++i) d->m_field[i]->setValue(newValue); } } } } slotNeedUpdate(); d->m_currentTab = tab; inside = false; } void KBudgetValues::slotNeedUpdate() { if (!signalsBlocked()) QTimer::singleShot(0, this, SIGNAL(valuesChanged())); } void KBudgetValues::setBudgetValues(const MyMoneyBudget& budget, const MyMoneyBudget::AccountGroup& budgetAccount) { Q_D(KBudgetValues); MyMoneyBudget::PeriodGroup period; d->m_budgetDate = budget.budgetStart(); QDate date; // make sure all values are zero so that slotChangePeriod() // doesn't check for anything. clear(); blockSignals(true); switch (budgetAccount.budgetLevel()) { - case MyMoneyBudget::AccountGroup::eMonthly: + case eMyMoney::Budget::Level::Monthly: default: d->ui->m_monthlyButton->setChecked(true); slotChangePeriod(d->ui->m_periodGroup->id(d->ui->m_monthlyButton)); d->ui->m_amountMonthly->setValue(budgetAccount.period(d->m_budgetDate).amount()); break; - case MyMoneyBudget::AccountGroup::eYearly: + case eMyMoney::Budget::Level::Yearly: d->ui->m_yearlyButton->setChecked(true); slotChangePeriod(d->ui->m_periodGroup->id(d->ui->m_yearlyButton)); d->ui->m_amountYearly->setValue(budgetAccount.period(d->m_budgetDate).amount()); break; - case MyMoneyBudget::AccountGroup::eMonthByMonth: + case eMyMoney::Budget::Level::MonthByMonth: d->ui->m_individualButton->setChecked(true); slotChangePeriod(d->ui->m_periodGroup->id(d->ui->m_individualButton)); date.setDate(d->m_budgetDate.year(), d->m_budgetDate.month(), d->m_budgetDate.day()); for (auto i = 0; i < 12; ++i) { d->m_field[i]->setValue(budgetAccount.period(date).amount()); date = date.addMonths(1); } break; } slotUpdateClearButton(); blockSignals(false); } void KBudgetValues::budgetValues(const MyMoneyBudget& budget, MyMoneyBudget::AccountGroup& budgetAccount) { Q_D(KBudgetValues); MyMoneyBudget::PeriodGroup period; d->m_budgetDate = budget.budgetStart(); period.setStartDate(d->m_budgetDate); QDate date; budgetAccount.clearPeriods(); int tab = d->ui->m_periodGroup->checkedId(); if (tab == d->ui->m_periodGroup->id(d->ui->m_monthlyButton)) { - budgetAccount.setBudgetLevel(MyMoneyBudget::AccountGroup::eMonthly); + budgetAccount.setBudgetLevel(eMyMoney::Budget::Level::Monthly); period.setAmount(d->ui->m_amountMonthly->value()); budgetAccount.addPeriod(d->m_budgetDate, period); } else if (tab == d->ui->m_periodGroup->id(d->ui->m_yearlyButton)) { - budgetAccount.setBudgetLevel(MyMoneyBudget::AccountGroup::eYearly); + budgetAccount.setBudgetLevel(eMyMoney::Budget::Level::Yearly); period.setAmount(d->ui->m_amountYearly->value()); budgetAccount.addPeriod(d->m_budgetDate, period); } else if (tab == d->ui->m_periodGroup->id(d->ui->m_individualButton)) { - budgetAccount.setBudgetLevel(MyMoneyBudget::AccountGroup::eMonthByMonth); + budgetAccount.setBudgetLevel(eMyMoney::Budget::Level::MonthByMonth); date.setDate(d->m_budgetDate.year(), d->m_budgetDate.month(), d->m_budgetDate.day()); for (auto i = 0; i < 12; ++i) { period.setStartDate(date); period.setAmount(d->m_field[i]->value()); budgetAccount.addPeriod(date, period); date = date.addMonths(1); } } } void KBudgetValues::slotUpdateClearButton() { Q_D(KBudgetValues); auto rc = false; int tab = d->ui->m_periodGroup->checkedId(); if (tab == d->ui->m_periodGroup->id(d->ui->m_monthlyButton)) { rc = !d->ui->m_amountMonthly->value().isZero(); } else if (tab == d->ui->m_periodGroup->id(d->ui->m_yearlyButton)) { rc = !d->ui->m_amountYearly->value().isZero(); } else if (tab == d->ui->m_periodGroup->id(d->ui->m_individualButton)) { for (auto i = 0; (i < 12) && (rc == false); ++i) { rc |= !d->m_field[i]->value().isZero(); } } d->ui->m_clearButton->setEnabled(rc); } diff --git a/kmymoney/plugins/views/budget/kbudgetview.cpp b/kmymoney/plugins/views/budget/kbudgetview.cpp index 709bc7365..119294f3b 100644 --- a/kmymoney/plugins/views/budget/kbudgetview.cpp +++ b/kmymoney/plugins/views/budget/kbudgetview.cpp @@ -1,539 +1,539 @@ /*************************************************************************** kbudgetview.cpp --------------- begin : Thu Jan 10 2006 copyright : (C) 2006 by Darren Gould email : darren_gould@gmx.de Alvaro Soliverez (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 "kbudgetview_p.h" #include // ---------------------------------------------------------------------------- // QT Includes #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes #include "mymoneyexception.h" using namespace Icons; KBudgetView::KBudgetView(QWidget *parent) : KMyMoneyAccountsViewBase(*new KBudgetViewPrivate(this), parent) { Q_D(KBudgetView); d->m_inSelection = false; } KBudgetView::KBudgetView(KBudgetViewPrivate &dd, QWidget *parent) : KMyMoneyAccountsViewBase(dd, parent) { } KBudgetView::~KBudgetView() { } void KBudgetView::showEvent(QShowEvent * event) { Q_D(KBudgetView); if (!d->m_proxyModel) d->init(); emit customActionRequested(View::Budget, eView::Action::AboutToShow); if (d->m_needsRefresh) refresh(); // don't forget base class implementation QWidget::showEvent(event); } void KBudgetView::executeCustomAction(eView::Action action) { switch(action) { case eView::Action::Refresh: refresh(); break; case eView::Action::SetDefaultFocus: { Q_D(KBudgetView); QTimer::singleShot(0, d->ui->m_budgetList, SLOT(setFocus())); } break; default: break; } } void KBudgetView::refresh() { Q_D(KBudgetView); if (isVisible()) { if (d->m_inSelection) QTimer::singleShot(0, this, SLOT(refresh())); else { d->loadBudgets(); d->m_needsRefresh = false; } } else { d->m_needsRefresh = true; } } void KBudgetView::slotNewBudget() { Q_D(KBudgetView); d->askSave(); auto date = QDate::currentDate(); date.setDate(date.year(), KMyMoneySettings::firstFiscalMonth(), KMyMoneySettings::firstFiscalDay()); auto newname = i18n("Budget %1", date.year()); MyMoneyBudget budget; // make sure we have a unique name try { int i = 1; // Exception thrown when the name is not found while (1) { MyMoneyFile::instance()->budgetByName(newname); newname = i18n("Budget %1 %2", date.year(), i++); } } catch (const MyMoneyException &) { // all ok, the name is unique } MyMoneyFileTransaction ft; try { budget.setName(newname); budget.setBudgetStart(date); MyMoneyFile::instance()->addBudget(budget); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(this, i18n("Unable to add budget"), QString::fromLatin1(e.what())); } } void KBudgetView::slotDeleteBudget() { Q_D(KBudgetView); if (d->m_budgetList.isEmpty()) return; // shouldn't happen auto file = MyMoneyFile::instance(); // get confirmation from user QString prompt; if (d->m_budgetList.size() == 1) prompt = i18n("

Do you really want to remove the budget %1?

", d->m_budgetList.front().name()); else prompt = i18n("Do you really want to remove all selected budgets?"); if (KMessageBox::questionYesNo(this, prompt, i18n("Remove Budget")) == KMessageBox::No) return; try { MyMoneyFileTransaction ft; // now loop over all selected d->m_budgetList and remove them for (const auto& budget : d->m_budgetList) file->removeBudget(budget); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(this, i18n("Unable to remove budget."), QString::fromLatin1(e.what())); } } void KBudgetView::slotCopyBudget() { Q_D(KBudgetView); if (d->m_budgetList.size() == 1) { MyMoneyFileTransaction ft; try { MyMoneyBudget budget = d->m_budgetList.first(); budget.clearId(); budget.setName(i18n("Copy of %1", budget.name())); MyMoneyFile::instance()->addBudget(budget); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(this, i18n("Unable to add budget"), QString::fromLatin1(e.what())); } } } void KBudgetView::slotChangeBudgetYear() { Q_D(KBudgetView); if (d->m_budgetList.size() == 1) { QStringList years; int current = 0; bool haveCurrent = false; MyMoneyBudget budget = *(d->m_budgetList.begin()); for (int i = (QDate::currentDate().year() - 3); i < (QDate::currentDate().year() + 5); ++i) { years << QString::fromLatin1("%1").arg(i); if (i == budget.budgetStart().year()) { haveCurrent = true; } if (!haveCurrent) ++current; } if (!haveCurrent) current = 0; bool ok = false; auto yearString = QInputDialog::getItem(this, i18n("Select year"), i18n("Budget year"), years, current, false, &ok); if (ok) { int year = yearString.toInt(0, 0); QDate newYear = QDate(year, budget.budgetStart().month(), budget.budgetStart().day()); if (newYear != budget.budgetStart()) { MyMoneyFileTransaction ft; try { budget.setBudgetStart(newYear); MyMoneyFile::instance()->modifyBudget(budget); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(this, i18n("Unable to modify budget."), QString::fromLatin1(e.what())); } } } } } void KBudgetView::slotBudgetForecast() { Q_D(KBudgetView); if (d->m_budgetList.size() == 1) { MyMoneyFileTransaction ft; try { MyMoneyBudget budget = d->m_budgetList.first(); bool calcBudget = budget.getaccounts().count() == 0; if (!calcBudget) { if (KMessageBox::warningContinueCancel(0, i18n("The current budget already contains data. Continuing will replace all current values of this budget."), i18nc("Warning message box", "Warning")) == KMessageBox::Continue) calcBudget = true; } if (calcBudget) { QDate historyStart; QDate historyEnd; QDate budgetStart; QDate budgetEnd; budgetStart = budget.budgetStart(); budgetEnd = budgetStart.addYears(1).addDays(-1); historyStart = budgetStart.addYears(-1); historyEnd = budgetEnd.addYears(-1); MyMoneyForecast forecast = KMyMoneyUtils::forecast(); forecast.createBudget(budget, historyStart, historyEnd, budgetStart, budgetEnd, true); MyMoneyFile::instance()->modifyBudget(budget); ft.commit(); } } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(this, i18n("Unable to modify budget."), QString::fromLatin1(e.what())); } } } void KBudgetView::slotResetBudget() { Q_D(KBudgetView); try { d->m_budget = MyMoneyFile::instance()->budget(d->m_budget.id()); d->loadAccounts(); const auto index = d->ui->m_accountTree->currentIndex(); if (index.isValid()) { const auto acc = d->ui->m_accountTree->model()->data(index, (int)eAccountsModel::Role::Account).value(); slotSelectAccount(acc, eView::Intent::None); } else { d->ui->m_budgetValue->clear(); } } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(this, i18n("Unable to reset budget"), QString::fromLatin1(e.what())); } } void KBudgetView::slotUpdateBudget() { Q_D(KBudgetView); MyMoneyFileTransaction ft; try { MyMoneyFile::instance()->modifyBudget(d->m_budget); ft.commit(); d->refreshHideUnusedButton(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(this, i18n("Unable to modify budget"), QString::fromLatin1(e.what())); } } void KBudgetView::slotStartRename() { Q_D(KBudgetView); QTreeWidgetItemIterator it_l(d->ui->m_budgetList, QTreeWidgetItemIterator::Selected); QTreeWidgetItem* it_v; if ((it_v = *it_l) != 0) { d->ui->m_budgetList->editItem(it_v, 0); } } void KBudgetView::slotOpenContextMenu(const QPoint&) { Q_D(KBudgetView); typedef void(KBudgetView::*KBudgetViewFunc)(); struct actionInfo { KBudgetViewFunc callback; QString text; Icon icon; bool enabled; }; const auto actionStates = d->actionStates(); const QVector actionInfos { {&KBudgetView::slotNewBudget, i18n("New budget"), Icon::BudgetNew, actionStates[eMenu::Action::NewBudget]}, {&KBudgetView::slotStartRename, i18n("Rename budget"), Icon::BudgetRename, actionStates[eMenu::Action::RenameBudget]}, {&KBudgetView::slotDeleteBudget, i18n("Delete budget"), Icon::BudgetDelete, actionStates[eMenu::Action::DeleteBudget]}, {&KBudgetView::slotCopyBudget, i18n("Copy budget"), Icon::BudgetCopy, actionStates[eMenu::Action::CopyBudget]}, {&KBudgetView::slotChangeBudgetYear, i18n("Change budget year"), Icon::ViewCalendar, actionStates[eMenu::Action::ChangeBudgetYear]}, {&KBudgetView::slotBudgetForecast, i18n("Budget based on forecast"), Icon::ViewForecast, actionStates[eMenu::Action::BudgetForecast]} }; auto menu = new QMenu(i18nc("Menu header", "Budget options")); for (const auto& info : actionInfos) { auto a = menu->addAction(Icons::get(info.icon), info.text, this, info.callback); a->setEnabled(info.enabled); } menu->exec(QCursor::pos()); } void KBudgetView::slotItemChanged(QTreeWidgetItem* p, int col) { // if we don't have an item we actually don't care about it if (!p) return; auto pBudget = dynamic_cast(p); if (!pBudget) return; if (col == 1) { pBudget->setText(1, QString().setNum(pBudget->budget().budgetStart().year())); return; } // create a copy of the new name without leading and trailing whitespaces QString new_name = p->text(0).trimmed(); if (pBudget->budget().name() != new_name) { MyMoneyFileTransaction ft; try { // check if we already have a budget with the new name try { // this function call will throw an exception, if the budget // hasn't been found. MyMoneyFile::instance()->budgetByName(new_name); // the name already exists, ask the user whether he's sure to keep the name if (KMessageBox::questionYesNo(this, i18n("A budget with the name '%1' already exists. It is not advisable to have " "multiple budgets with the same identification name. Are you sure you would like " "to rename the budget?", new_name)) != KMessageBox::Yes) { p->setText(0, pBudget->budget().name()); return; } } catch (const MyMoneyException &) { // all ok, the name is unique } MyMoneyBudget b = pBudget->budget(); b.setName(new_name); // don't use pBudget beyond this point as it will change due to call to modifyBudget pBudget = 0; MyMoneyFile::instance()->modifyBudget(b); // the above call to modifyBudget will reload the view so // all references and pointers to the view have to be // re-established. You cannot use pBudget beyond this point!!! ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(this, i18n("Unable to modify budget"), QString::fromLatin1(e.what())); } } else { pBudget->setText(0, new_name); } } void KBudgetView::slotSelectAccount(const MyMoneyObject &obj, eView::Intent intent) { Q_UNUSED(intent) Q_D(KBudgetView); d->ui->m_assignmentBox->setEnabled(false); if (typeid(obj) != typeid(MyMoneyAccount)) return; const MyMoneyAccount& acc = dynamic_cast(obj); d->ui->m_assignmentBox->setEnabled(true); if (d->m_budget.id().isEmpty()) return; QString id = acc.id(); d->ui->m_leAccounts->setText(MyMoneyFile::instance()->accountToCategory(id)); d->ui->m_cbBudgetSubaccounts->setChecked(d->m_budget.account(id).budgetSubaccounts()); d->ui->m_accountTotal->setValue(d->m_budget.account(id).totalBalance()); MyMoneyBudget::AccountGroup budgetAccount = d->m_budget.account(id); if (id != budgetAccount.id()) { - budgetAccount.setBudgetLevel(MyMoneyBudget::AccountGroup::eMonthly); + budgetAccount.setBudgetLevel(eMyMoney::Budget::Level::Monthly); } d->ui->m_budgetValue->setBudgetValues(d->m_budget, budgetAccount); } void KBudgetView::slotBudgetedAmountChanged() { Q_D(KBudgetView); if (d->m_budget.id().isEmpty()) return; const auto indexes = d->ui->m_accountTree->selectionModel()->selectedIndexes(); if (indexes.empty()) return; QString accountID = indexes.front().data((int)eAccountsModel::Role::ID).toString(); MyMoneyBudget::AccountGroup accountGroup = d->m_budget.account(accountID); accountGroup.setId(accountID); d->ui->m_budgetValue->budgetValues(d->m_budget, accountGroup); d->m_budget.setAccount(accountGroup, accountID); d->m_budgetProxyModel->setBudget(d->m_budget); d->ui->m_accountTotal->setValue(accountGroup.totalBalance()); d->ui->m_updateButton->setEnabled(!(d->selectedBudget() == d->m_budget)); d->ui->m_resetButton->setEnabled(!(d->selectedBudget() == d->m_budget)); } void KBudgetView::cb_includesSubaccounts_clicked() { Q_D(KBudgetView); if (d->m_budget.id().isEmpty()) return; QModelIndexList indexes = d->ui->m_accountTree->selectionModel()->selectedIndexes(); if (!indexes.empty()) { QString accountID = indexes.front().data((int)eAccountsModel::Role::ID).toString(); // now, we get a reference to the accountgroup, to modify its attribute, // and then put the resulting account group instead of the original MyMoneyBudget::AccountGroup auxAccount = d->m_budget.account(accountID); auxAccount.setBudgetSubaccounts(d->ui->m_cbBudgetSubaccounts->isChecked()); // in case we turn the option on, we check that no subordinate account // has a budget. If we find some, we ask the user if he wants to move it // to the current account or leave things as they are if (d->ui->m_cbBudgetSubaccounts->isChecked()) { // TODO: asking the user needs to be added. So long, we assume yes if (1) { MyMoneyBudget::AccountGroup subAccount; if (d->collectSubBudgets(subAccount, indexes.front())) { // we found a sub-budget somewhere // so we add those figures found and // clear the subaccounts auxAccount += subAccount; d->clearSubBudgets(indexes.front()); } - if (auxAccount.budgetLevel() == MyMoneyBudget::AccountGroup::eNone) { + if (auxAccount.budgetLevel() == eMyMoney::Budget::Level::None) { MyMoneyBudget::PeriodGroup period; auxAccount.addPeriod(d->m_budget.budgetStart(), period); - auxAccount.setBudgetLevel(MyMoneyBudget::AccountGroup::eMonthly); + auxAccount.setBudgetLevel(eMyMoney::Budget::Level::Monthly); } } } d->m_budget.setAccount(auxAccount, accountID); d->m_budgetProxyModel->setBudget(d->m_budget); d->ui->m_budgetValue->setBudgetValues(d->m_budget, auxAccount); d->loadAccounts(); } } void KBudgetView::slotBudgetBalanceChanged(const MyMoneyMoney &balance) { Q_D(KBudgetView); d->netBalProChanged(balance, d->ui->m_balanceLabel, View::Budget); } void KBudgetView::slotSelectBudget() { Q_D(KBudgetView); d->askSave(); KBudgetListItem* item; QTreeWidgetItemIterator widgetIt = QTreeWidgetItemIterator(d->ui->m_budgetList); if (d->m_budget.id().isEmpty()) { item = dynamic_cast(*widgetIt); if (item) { d->ui->m_budgetList->blockSignals(true); d->ui->m_budgetList->setCurrentItem(item, QItemSelectionModel::ClearAndSelect); d->ui->m_budgetList->blockSignals(false); } } d->ui->m_accountTree->setEnabled(false); d->ui->m_assignmentBox->setEnabled(false); d->m_budget = MyMoneyBudget(); QTreeWidgetItemIterator it_l(d->ui->m_budgetList, QTreeWidgetItemIterator::Selected); item = dynamic_cast(*it_l); if (item) { d->m_budget = item->budget(); d->ui->m_accountTree->setEnabled(true); } d->refreshHideUnusedButton(); d->loadAccounts(); const auto index = d->ui->m_accountTree->currentIndex(); if (index.isValid()) { const auto acc = d->ui->m_accountTree->model()->data(index, (int)eAccountsModel::Role::Account).value(); slotSelectAccount(acc, eView::Intent::None); } else { d->ui->m_budgetValue->clear(); } d->m_budgetList.clear(); if (!d->m_budget.id().isEmpty()) d->m_budgetList << d->m_budget; d->actionStates(); d->updateButtonStates(); } void KBudgetView::slotHideUnused(bool toggled) { Q_D(KBudgetView); // make sure we show all items for an empty budget const auto prevState = !toggled; d->refreshHideUnusedButton(); if (prevState != d->ui->m_hideUnusedButton->isChecked()) d->m_budgetProxyModel->setHideUnusedIncomeExpenseAccounts(d->ui->m_hideUnusedButton->isChecked()); } diff --git a/kmymoney/plugins/views/budget/kbudgetview_p.h b/kmymoney/plugins/views/budget/kbudgetview_p.h index eab28dbb8..e6a5aebd4 100644 --- a/kmymoney/plugins/views/budget/kbudgetview_p.h +++ b/kmymoney/plugins/views/budget/kbudgetview_p.h @@ -1,386 +1,387 @@ /*************************************************************************** kbudgetview.cpp --------------- begin : Thu Jan 10 2006 copyright : (C) 2006 by Darren Gould email : darren_gould@gmx.de Alvaro Soliverez (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. * * * ***************************************************************************/ #ifndef KBUDGETVIEW_P_H #define KBUDGETVIEW_P_H #include "kbudgetview.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "ui_kbudgetview.h" #include "kmymoneyaccountsviewbase_p.h" #include "mymoneyfile.h" #include "mymoneyaccount.h" #include "mymoneyforecast.h" #include "kmymoneyedit.h" #include "kbudgetvalues.h" #include "kmymoneyutils.h" #include "budgetviewproxymodel.h" #include "kmymoneysettings.h" #include "icons.h" #include "modelenums.h" #include "menuenums.h" +#include "mymoneyenums.h" using namespace Icons; /** * @author Darren Gould * @author Thomas Baumgart * * This class represents an item in the budgets list view. */ class KBudgetListItem : public QTreeWidgetItem { public: /** * Constructor to be used to construct a budget entry object. * * @param parent pointer to the QTreeWidget object this entry should be * added to. * @param budget const reference to MyMoneyBudget for which * the QTreeWidget entry is constructed */ explicit KBudgetListItem(QTreeWidget *parent, const MyMoneyBudget& budget): QTreeWidgetItem(parent), m_budget(budget) { setText(0, budget.name()); setText(1, QString::fromLatin1("%1").arg(budget.budgetStart().year())); setFlags(flags() | Qt::ItemIsEditable); } ~KBudgetListItem() {} const MyMoneyBudget& budget() { return m_budget; } void setBudget(const MyMoneyBudget& budget) { m_budget = budget; } private: MyMoneyBudget m_budget; }; class KBudgetViewPrivate : public KMyMoneyAccountsViewBasePrivate { Q_DECLARE_PUBLIC(KBudgetView) public: explicit KBudgetViewPrivate(KBudgetView *qq) : KMyMoneyAccountsViewBasePrivate(), q_ptr(qq), ui(new Ui::KBudgetView), m_budgetProxyModel(nullptr), m_inSelection(false), m_budgetInEditing(false) { } ~KBudgetViewPrivate() { if(m_proxyModel) { // remember the splitter settings for startup KConfigGroup grp = KSharedConfig::openConfig()->group("Last Use Settings"); grp.writeEntry("KBudgetViewSplitterSize", ui->m_splitter->saveState()); grp.sync(); } delete ui; } void init() { Q_Q(KBudgetView); ui->setupUi(q); m_accountTree = &ui->m_accountTree; ui->m_budgetList->setContextMenuPolicy(Qt::CustomContextMenu); ui->m_newButton->setIcon(Icons::get(Icon::BudgetNew)); ui->m_renameButton->setIcon(Icons::get(Icon::BudgetRename)); ui->m_deleteButton->setIcon(Icons::get(Icon::BudgetDelete)); ui->m_updateButton->setIcon(Icons::get(Icon::DocumentSave)); ui->m_resetButton->setIcon(Icons::get(Icon::EditUndo)); ui->m_collapseButton->setIcon(Icons::get(Icon::ListCollapse)); ui->m_expandButton->setIcon(Icons::get(Icon::ListExpand)); m_budgetProxyModel = qobject_cast(ui->m_accountTree->init(View::Budget)); m_proxyModel = m_budgetProxyModel; q->connect(m_budgetProxyModel, &BudgetViewProxyModel::balanceChanged, q, &KBudgetView::slotBudgetBalanceChanged); q->connect(ui->m_accountTree, &KMyMoneyAccountTreeView::selectByObject, q, &KBudgetView::slotSelectAccount); q->connect(ui->m_budgetList, &QTreeWidget::customContextMenuRequested, q, &KBudgetView::slotOpenContextMenu); q->connect(ui->m_budgetList->selectionModel(), &QItemSelectionModel::selectionChanged, q, &KBudgetView::slotSelectBudget); q->connect(ui->m_budgetList, &QTreeWidget::itemChanged, q, &KBudgetView::slotItemChanged); q->connect(ui->m_cbBudgetSubaccounts, &QAbstractButton::clicked, q, &KBudgetView::cb_includesSubaccounts_clicked); // connect the buttons to the actions. Make sure the enabled state // of the actions is reflected by the buttons q->connect(ui->m_renameButton, &QAbstractButton::clicked, q, &KBudgetView::slotStartRename); q->connect(ui->m_deleteButton, &QAbstractButton::clicked, q, &KBudgetView::slotDeleteBudget); q->connect(ui->m_budgetValue, &KBudgetValues::valuesChanged, q, &KBudgetView::slotBudgetedAmountChanged); q->connect(ui->m_newButton, &QAbstractButton::clicked, q, &KBudgetView::slotNewBudget); q->connect(ui->m_updateButton, &QAbstractButton::clicked, q, &KBudgetView::slotUpdateBudget); q->connect(ui->m_resetButton, &QAbstractButton::clicked, q, &KBudgetView::slotResetBudget); q->connect(ui->m_hideUnusedButton, &QAbstractButton::toggled, q, &KBudgetView::slotHideUnused); q->connect(ui->m_searchWidget, &QLineEdit::textChanged, m_budgetProxyModel, &QSortFilterProxyModel::setFilterFixedString); q->connect(ui->m_accountTree, &KMyMoneyAccountTreeView::selectByObject, q, &KBudgetView::selectByObject); q->connect(ui->m_accountTree, &KMyMoneyAccountTreeView::selectByVariant, q, &KBudgetView::selectByVariant); q->connect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, q, &KBudgetView::refresh); // setup initial state updateButtonStates(); auto grp = KSharedConfig::openConfig()->group("Last Use Settings"); ui->m_splitter->restoreState(grp.readEntry("KBudgetViewSplitterSize", QByteArray())); ui->m_splitter->setChildrenCollapsible(false); } QHash actionStates() { QHash actionStates; actionStates[eMenu::Action::NewBudget] = true; auto b = m_budgetList.size() >= 1 ? true : false; actionStates[eMenu::Action::DeleteBudget] = b; b = m_budgetList.size() == 1 ? true : false; actionStates[eMenu::Action::ChangeBudgetYear] = b; actionStates[eMenu::Action::CopyBudget] = b; actionStates[eMenu::Action::RenameBudget] = b; actionStates[eMenu::Action::BudgetForecast] = b; return actionStates; } void updateButtonStates() { const auto actionStates = KBudgetViewPrivate::actionStates(); ui->m_newButton->setEnabled(actionStates[eMenu::Action::NewBudget]); ui->m_renameButton->setEnabled(actionStates[eMenu::Action::RenameBudget]); ui->m_deleteButton->setEnabled(actionStates[eMenu::Action::DeleteBudget]); } void askSave() { Q_Q(KBudgetView); // check if the content of a currently selected budget was modified // and ask to store the data if (ui->m_updateButton->isEnabled()) { if (KMessageBox::questionYesNo(q, i18n("Do you want to save the changes for %1?", m_budget.name()), i18n("Save changes")) == KMessageBox::Yes) { m_inSelection = true; q->slotUpdateBudget(); m_inSelection = false; } } } void refreshHideUnusedButton() { ui->m_hideUnusedButton->setDisabled(m_budget.getaccounts().isEmpty()); } void loadAccounts() { // if no budgets are selected, don't load the accounts // and clear out the previously shown list if (m_budget.id().isEmpty()) { ui->m_budgetValue->clear(); ui->m_updateButton->setEnabled(false); ui->m_resetButton->setEnabled(false); return; } ui->m_updateButton->setEnabled(!(selectedBudget() == m_budget)); ui->m_resetButton->setEnabled(!(selectedBudget() == m_budget)); m_budgetProxyModel->setBudget(m_budget); } const MyMoneyBudget& selectedBudget() const { static MyMoneyBudget nullBudget; QTreeWidgetItemIterator it_l(ui->m_budgetList, QTreeWidgetItemIterator::Selected); KBudgetListItem* item = dynamic_cast(*it_l); if (item) { return item->budget(); } return nullBudget; } void AccountEnter() { if (m_budget.id().isEmpty()) return; } void clearSubBudgets(const QModelIndex &index) { const auto children = ui->m_accountTree->model()->rowCount(index); for (auto i = 0; i < children; ++i) { const auto childIdx = index.child(i, 0); const auto accountID = childIdx.data((int)eAccountsModel::Role::ID).toString(); m_budget.removeReference(accountID); clearSubBudgets(childIdx); } } bool collectSubBudgets(MyMoneyBudget::AccountGroup &destination, const QModelIndex &index) const { auto rc = false; const auto children = ui->m_accountTree->model()->rowCount(index); for (auto i = 0; i < children; ++i) { auto childIdx = index.child(i, 0); auto accountID = childIdx.data((int)eAccountsModel::Role::ID).toString(); MyMoneyBudget::AccountGroup auxAccount = m_budget.account(accountID); - if (auxAccount.budgetLevel() != MyMoneyBudget::AccountGroup::eNone + if (auxAccount.budgetLevel() != eMyMoney::Budget::Level::None && !auxAccount.isZero()) { rc = true; // add the subaccount // TODO: deal with budgets in different currencies // https://bugs.kde.org/attachment.cgi?id=54813 contains a demo file destination += auxAccount; } rc |= collectSubBudgets(destination, childIdx); } return rc; } /** * This method loads all available budgets into the budget list widget. If a budget is * currently selected it remains selected if it is still present. */ void loadBudgets() { Q_Q(KBudgetView); m_budgetProxyModel->invalidate(); // remember which item is currently selected QString id = m_budget.id(); // clear the budget list ui->m_budgetList->clear(); // add the correct years to the drop down list QDate date = QDate::currentDate(); int iStartYear = date.year() - m_iBudgetYearsBack; m_yearList.clear(); for (int i = 0; i < m_iBudgetYearsAhead + m_iBudgetYearsBack; i++) m_yearList += QString::number(iStartYear + i); KBudgetListItem* currentItem = 0; QList list = MyMoneyFile::instance()->budgetList(); QList::ConstIterator it; for (it = list.constBegin(); it != list.constEnd(); ++it) { KBudgetListItem* item = new KBudgetListItem(ui->m_budgetList, *it); // create a list of unique years if (m_yearList.indexOf(QString::number((*it).budgetStart().year())) == -1) m_yearList += QString::number((*it).budgetStart().year()); //sort the list by name ui->m_budgetList->sortItems((int)eAccountsModel::Column::Account, Qt::AscendingOrder); if (item->budget().id() == id) { m_budget = (*it); currentItem = item; item->setSelected(true); } } m_yearList.sort(); if (currentItem) { ui->m_budgetList->setCurrentItem(currentItem); } // reset the status of the buttons ui->m_updateButton->setEnabled(false); ui->m_resetButton->setEnabled(false); // make sure the world around us knows what we have selected q->slotSelectBudget(); } void ensureBudgetVisible(const QString& id) { const auto widgetIt = QTreeWidgetItemIterator(ui->m_budgetList); while (*widgetIt) { const auto p = dynamic_cast(*widgetIt); if ((p)->budget().id() == id) { ui->m_budgetList->scrollToItem((p), QAbstractItemView::PositionAtCenter); ui->m_budgetList->setCurrentItem(p, 0, QItemSelectionModel::ClearAndSelect); // active item and deselect all others } } } KBudgetView *q_ptr; Ui::KBudgetView *ui; BudgetViewProxyModel *m_budgetProxyModel; MyMoneyBudget m_budget; QMap m_transactionCountMap; QStringList m_yearList; QList m_budgetList; /** * Set if we are in the selection of a different budget **/ bool m_inSelection; void adaptHideUnusedButton(); static const int m_iBudgetYearsAhead = 5; static const int m_iBudgetYearsBack = 3; /** * This signals whether a budget is being edited **/ bool m_budgetInEditing; }; #endif diff --git a/kmymoney/plugins/views/forecast/kforecastview_p.h b/kmymoney/plugins/views/forecast/kforecastview_p.h index 2255d9e29..08113b707 100644 --- a/kmymoney/plugins/views/forecast/kforecastview_p.h +++ b/kmymoney/plugins/views/forecast/kforecastview_p.h @@ -1,1046 +1,1046 @@ /*************************************************************************** kforecastview.cpp ------------------- copyright : (C) 2007 by Alvaro Soliverez email : asoliverez@gmail.com (C) 2017 Ł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. * * * ***************************************************************************/ #ifndef KFORECASTVIEW_P_H #define KFORECASTVIEW_P_H #include "kforecastview.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "ui_kforecastview.h" #include "forecastviewsettings.h" #include "kmymoneyviewbase_p.h" #include "mymoneymoney.h" #include "mymoneyforecast.h" #include "mymoneyprice.h" #include "mymoneyutils.h" #include "mymoneyfile.h" #include "mymoneyaccount.h" #include "mymoneyexception.h" #include "mymoneysecurity.h" #include "kmymoneysettings.h" #include "mymoneybudget.h" #include "pivottable.h" #include "fixedcolumntreeview.h" #include "kreportchartview.h" #include "reportaccount.h" #include "icons.h" #include "mymoneyenums.h" #include "kmymoneyutils.h" using namespace reports; using namespace Icons; typedef enum { SummaryView = 0, ListView, AdvancedView, BudgetView, ChartView, // insert new values above this line MaxViewTabs } ForecastViewTab; enum ForecastViewRoles { ForecastRole = Qt::UserRole, /**< The forecast is held in this role.*/ AccountRole = Qt::UserRole + 1, /**< The MyMoneyAccount is stored in this role in column 0.*/ AmountRole = Qt::UserRole + 2, /**< The amount.*/ ValueRole = Qt::UserRole + 3, /**< The value.*/ }; enum EForecastViewType { eSummary = 0, eDetailed, eAdvanced, eBudget, eUndefined }; class KForecastViewPrivate : public KMyMoneyViewBasePrivate { Q_DECLARE_PUBLIC(KForecastView) public: explicit KForecastViewPrivate(KForecastView *qq) : KMyMoneyViewBasePrivate(), q_ptr(qq), ui(new Ui::KForecastView), m_needLoad(true), m_totalItem(0), m_assetItem(0), m_liabilityItem(0), m_incomeItem(0), m_expenseItem(0), m_chartLayout(0), m_forecastChart(0) { } ~KForecastViewPrivate() { delete ui; } void init() { Q_Q(KForecastView); m_needLoad = false; ui->setupUi(q); m_forecastChart = new KReportChartView(ui->m_tabChart); for (int i = 0; i < MaxViewTabs; ++i) m_needReload[i] = false; KSharedConfigPtr config = KSharedConfig::openConfig(); KConfigGroup grp = config->group("Last Use Settings"); ui->m_tab->setCurrentIndex(grp.readEntry("KForecastView_LastType", 0)); ui->m_forecastButton->setIcon(Icons::get(Icon::ViewForecast)); q->connect(ui->m_tab, &QTabWidget::currentChanged, q, &KForecastView::slotTabChanged); q->connect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, q, &KForecastView::refresh); q->connect(ui->m_forecastButton, &QAbstractButton::clicked, q, &KForecastView::slotManualForecast); ui->m_forecastList->setUniformRowHeights(true); ui->m_forecastList->setAllColumnsShowFocus(true); ui->m_summaryList->setAllColumnsShowFocus(true); ui->m_budgetList->setAllColumnsShowFocus(true); ui->m_advancedList->setAlternatingRowColors(true); q->connect(ui->m_forecastList, &QTreeWidget::itemExpanded, q, &KForecastView::itemExpanded); q->connect(ui->m_forecastList, &QTreeWidget::itemCollapsed, q, &KForecastView::itemCollapsed); q->connect(ui->m_summaryList, &QTreeWidget::itemExpanded, q, &KForecastView::itemExpanded); q->connect(ui->m_summaryList, &QTreeWidget::itemCollapsed, q, &KForecastView::itemCollapsed); q->connect(ui->m_budgetList, &QTreeWidget::itemExpanded, q, &KForecastView::itemExpanded); q->connect(ui->m_budgetList, &QTreeWidget::itemCollapsed, q, &KForecastView::itemCollapsed); m_forecastChart->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); m_chartLayout = ui->m_tabChart->layout(); m_chartLayout->setSpacing(6); m_chartLayout->addWidget(m_forecastChart); loadForecastSettings(); } void loadForecast(ForecastViewTab tab) { if (m_needReload[tab]) { switch (tab) { case ListView: loadListView(); break; case SummaryView: loadSummaryView(); break; case AdvancedView: loadAdvancedView(); break; case BudgetView: loadBudgetView(); break; case ChartView: loadChartView(); break; default: break; } m_needReload[tab] = false; } } void loadListView() { MyMoneyForecast forecast = KMyMoneyUtils::forecast(); const auto file = MyMoneyFile::instance(); //get the settings from current page forecast.setForecastDays(ui->m_forecastDays->value()); forecast.setAccountsCycle(ui->m_accountsCycle->value()); forecast.setBeginForecastDay(ui->m_beginDay->value()); forecast.setForecastCycles(ui->m_forecastCycles->value()); forecast.setHistoryMethod(ui->m_historyMethod->checkedId()); forecast.doForecast(); ui->m_forecastList->clear(); ui->m_forecastList->setColumnCount(0); ui->m_forecastList->setIconSize(QSize(22, 22)); ui->m_forecastList->setSortingEnabled(true); ui->m_forecastList->sortByColumn(0, Qt::AscendingOrder); //add columns QStringList headerLabels; headerLabels << i18n("Account"); //add cycle interval columns headerLabels << i18nc("Today's forecast", "Current"); for (int i = 1; i <= forecast.forecastDays(); ++i) { QDate forecastDate = QDate::currentDate().addDays(i); headerLabels << QLocale().toString(forecastDate, QLocale::LongFormat); } //add variation columns headerLabels << i18n("Total variation"); //set the columns ui->m_forecastList->setHeaderLabels(headerLabels); //add default rows addTotalRow(ui->m_forecastList, forecast); addAssetLiabilityRows(forecast); //load asset and liability forecast accounts loadAccounts(forecast, file->asset(), m_assetItem, eDetailed); loadAccounts(forecast, file->liability(), m_liabilityItem, eDetailed); adjustHeadersAndResizeToContents(ui->m_forecastList); // add the fixed column only if the horizontal scroll bar is visible m_fixedColumnView.reset(ui->m_forecastList->horizontalScrollBar()->isVisible() ? new FixedColumnTreeView(ui->m_forecastList) : 0); } void loadAccounts(MyMoneyForecast& forecast, const MyMoneyAccount& account, QTreeWidgetItem* parentItem, int forecastType) { QMap nameIdx; const auto file = MyMoneyFile::instance(); QTreeWidgetItem *forecastItem = 0; //Get all accounts of the right type to calculate forecast const auto accList = account.accountList(); if (accList.isEmpty()) return; foreach (const auto sAccount, accList) { auto subAccount = file->account(sAccount); //only add the account if it is a forecast account or the parent of a forecast account if (includeAccount(forecast, subAccount)) { nameIdx[subAccount.id()] = subAccount.id(); } } QMap::ConstIterator it_nc; for (it_nc = nameIdx.constBegin(); it_nc != nameIdx.constEnd(); ++it_nc) { const MyMoneyAccount subAccount = file->account(*it_nc); MyMoneySecurity currency; if (subAccount.isInvest()) { MyMoneySecurity underSecurity = file->security(subAccount.currencyId()); currency = file->security(underSecurity.tradingCurrency()); } else { currency = file->security(subAccount.currencyId()); } forecastItem = new QTreeWidgetItem(parentItem); forecastItem->setText(0, subAccount.name()); forecastItem->setIcon(0, subAccount.accountPixmap()); forecastItem->setData(0, ForecastRole, QVariant::fromValue(forecast)); forecastItem->setData(0, AccountRole, QVariant::fromValue(subAccount)); forecastItem->setExpanded(true); switch (forecastType) { case eSummary: updateSummary(forecastItem); break; case eDetailed: updateDetailed(forecastItem); break; case EForecastViewType::eBudget: updateBudget(forecastItem); break; default: break; } loadAccounts(forecast, subAccount, forecastItem, forecastType); } } void loadSummaryView() { MyMoneyForecast forecast = KMyMoneyUtils::forecast(); QList accList; const auto file = MyMoneyFile::instance(); //get the settings from current page forecast.setForecastDays(ui->m_forecastDays->value()); forecast.setAccountsCycle(ui->m_accountsCycle->value()); forecast.setBeginForecastDay(ui->m_beginDay->value()); forecast.setForecastCycles(ui->m_forecastCycles->value()); forecast.setHistoryMethod(ui->m_historyMethod->checkedId()); forecast.doForecast(); //add columns QStringList headerLabels; headerLabels << i18n("Account"); headerLabels << i18nc("Today's forecast", "Current"); //if beginning of forecast is today, set the begin day to next cycle to avoid repeating the first cycle qint64 daysToBeginDay; if (QDate::currentDate() < forecast.beginForecastDate()) { daysToBeginDay = QDate::currentDate().daysTo(forecast.beginForecastDate()); } else { daysToBeginDay = forecast.accountsCycle(); } for (auto i = 0; ((i*forecast.accountsCycle()) + daysToBeginDay) <= forecast.forecastDays(); ++i) { auto intervalDays = ((i * forecast.accountsCycle()) + daysToBeginDay); headerLabels << i18np("1 day", "%1 days", intervalDays); } //add variation columns headerLabels << i18n("Total variation"); ui->m_summaryList->clear(); //set the columns ui->m_summaryList->setHeaderLabels(headerLabels); ui->m_summaryList->setIconSize(QSize(22, 22)); ui->m_summaryList->setSortingEnabled(true); ui->m_summaryList->sortByColumn(0, Qt::AscendingOrder); //add default rows addTotalRow(ui->m_summaryList, forecast); addAssetLiabilityRows(forecast); loadAccounts(forecast, file->asset(), m_assetItem, eSummary); loadAccounts(forecast, file->liability(), m_liabilityItem, eSummary); adjustHeadersAndResizeToContents(ui->m_summaryList); //Add comments to the advice list ui->m_adviceText->clear(); //Get all accounts of the right type to calculate forecast m_nameIdx.clear(); accList = forecast.accountList(); QList::const_iterator accList_t = accList.constBegin(); for (; accList_t != accList.constEnd(); ++accList_t) { MyMoneyAccount acc = *accList_t; if (m_nameIdx[acc.id()] != acc.id()) { //Check if the account is there m_nameIdx[acc.id()] = acc.id(); } } QMap::ConstIterator it_nc; for (it_nc = m_nameIdx.constBegin(); it_nc != m_nameIdx.constEnd(); ++it_nc) { const MyMoneyAccount& acc = file->account(*it_nc); MyMoneySecurity currency; //change currency to deep currency if account is an investment if (acc.isInvest()) { MyMoneySecurity underSecurity = file->security(acc.currencyId()); currency = file->security(underSecurity.tradingCurrency()); } else { currency = file->security(acc.currencyId()); } //Check if the account is going to be below zero or below the minimal balance in the forecast period QString minimumBalance = acc.value("minimumBalance"); MyMoneyMoney minBalance = MyMoneyMoney(minimumBalance); //Check if the account is going to be below minimal balance auto dropMinimum = forecast.daysToMinimumBalance(acc); //Check if the account is going to be below zero in the future auto dropZero = forecast.daysToZeroBalance(acc); // spit out possible warnings QString msg; // if a minimum balance has been specified, an appropriate warning will // only be shown, if the drop below 0 is on a different day or not present if (dropMinimum != -1 && !minBalance.isZero() && (dropMinimum < dropZero || dropZero == -1)) { switch (dropMinimum) { case 0: msg = QString("").arg(KMyMoneySettings::schemeColor(SchemeColor::Negative).name()); msg += i18n("The balance of %1 is below the minimum balance %2 today.", acc.name(), MyMoneyUtils::formatMoney(minBalance, acc, currency)); msg += QString(""); break; default: msg = QString("").arg(KMyMoneySettings::schemeColor(SchemeColor::Negative).name()); msg += i18np("The balance of %2 will drop below the minimum balance %3 in %1 day.", "The balance of %2 will drop below the minimum balance %3 in %1 days.", dropMinimum - 1, acc.name(), MyMoneyUtils::formatMoney(minBalance, acc, currency)); msg += QString(""); } if (!msg.isEmpty()) { ui->m_adviceText->append(msg); } } // a drop below zero is always shown msg.clear(); switch (dropZero) { case -1: break; case 0: if (acc.accountGroup() == eMyMoney::Account::Type::Asset) { msg = QString("").arg(KMyMoneySettings::schemeColor(SchemeColor::Negative).name()); msg += i18n("The balance of %1 is below %2 today.", acc.name(), MyMoneyUtils::formatMoney(MyMoneyMoney(), acc, currency)); msg += QString(""); break; } if (acc.accountGroup() == eMyMoney::Account::Type::Liability) { msg = i18n("The balance of %1 is above %2 today.", acc.name(), MyMoneyUtils::formatMoney(MyMoneyMoney(), acc, currency)); break; } break; default: if (acc.accountGroup() == eMyMoney::Account::Type::Asset) { msg = QString("").arg(KMyMoneySettings::schemeColor(SchemeColor::Negative).name()); msg += i18np("The balance of %2 will drop below %3 in %1 day.", "The balance of %2 will drop below %3 in %1 days.", dropZero, acc.name(), MyMoneyUtils::formatMoney(MyMoneyMoney(), acc, currency)); msg += QString(""); break; } if (acc.accountGroup() == eMyMoney::Account::Type::Liability) { msg = i18np("The balance of %2 will raise above %3 in %1 day.", "The balance of %2 will raise above %3 in %1 days.", dropZero, acc.name(), MyMoneyUtils::formatMoney(MyMoneyMoney(), acc, currency)); break; } } if (!msg.isEmpty()) { ui->m_adviceText->append(msg); } //advice about trends msg.clear(); MyMoneyMoney accCycleVariation = forecast.accountCycleVariation(acc); if (accCycleVariation < MyMoneyMoney()) { msg = QString("").arg(KMyMoneySettings::schemeColor(SchemeColor::Negative).name()); msg += i18n("The account %1 is decreasing %2 per cycle.", acc.name(), MyMoneyUtils::formatMoney(accCycleVariation, acc, currency)); msg += QString(""); } if (!msg.isEmpty()) { ui->m_adviceText->append(msg); } } ui->m_adviceText->show(); } void loadAdvancedView() { const auto file = MyMoneyFile::instance(); QList accList; MyMoneySecurity baseCurrency = file->baseCurrency(); MyMoneyForecast forecast = KMyMoneyUtils::forecast(); qint64 daysToBeginDay; //get the settings from current page forecast.setForecastDays(ui->m_forecastDays->value()); forecast.setAccountsCycle(ui->m_accountsCycle->value()); forecast.setBeginForecastDay(ui->m_beginDay->value()); forecast.setForecastCycles(ui->m_forecastCycles->value()); forecast.setHistoryMethod(ui->m_historyMethod->checkedId()); forecast.doForecast(); //Get all accounts of the right type to calculate forecast m_nameIdx.clear(); accList = forecast.accountList(); QList::const_iterator accList_t = accList.constBegin(); for (; accList_t != accList.constEnd(); ++accList_t) { MyMoneyAccount acc = *accList_t; if (m_nameIdx[acc.id()] != acc.id()) { //Check if the account is there m_nameIdx[acc.id()] = acc.id(); } } //clear the list, including columns ui->m_advancedList->clear(); ui->m_advancedList->setColumnCount(0); ui->m_advancedList->setIconSize(QSize(22, 22)); QStringList headerLabels; //add first column of both lists headerLabels << i18n("Account"); //if beginning of forecast is today, set the begin day to next cycle to avoid repeating the first cycle if (QDate::currentDate() < forecast.beginForecastDate()) { daysToBeginDay = QDate::currentDate().daysTo(forecast.beginForecastDate()); } else { daysToBeginDay = forecast.accountsCycle(); } //add columns for (auto i = 1; ((i * forecast.accountsCycle()) + daysToBeginDay) <= forecast.forecastDays(); ++i) { headerLabels << i18n("Min Bal %1", i); headerLabels << i18n("Min Date %1", i); } for (auto i = 1; ((i * forecast.accountsCycle()) + daysToBeginDay) <= forecast.forecastDays(); ++i) { headerLabels << i18n("Max Bal %1", i); headerLabels << i18n("Max Date %1", i); } headerLabels << i18nc("Average balance", "Average"); ui->m_advancedList->setHeaderLabels(headerLabels); QTreeWidgetItem *advancedItem = 0; QMap::ConstIterator it_nc; for (it_nc = m_nameIdx.constBegin(); it_nc != m_nameIdx.constEnd(); ++it_nc) { const MyMoneyAccount& acc = file->account(*it_nc); QString amount; MyMoneyMoney amountMM; MyMoneySecurity currency; //change currency to deep currency if account is an investment if (acc.isInvest()) { MyMoneySecurity underSecurity = file->security(acc.currencyId()); currency = file->security(underSecurity.tradingCurrency()); } else { currency = file->security(acc.currencyId()); } advancedItem = new QTreeWidgetItem(ui->m_advancedList, advancedItem, false); advancedItem->setText(0, acc.name()); advancedItem->setIcon(0, acc.accountPixmap()); auto it_c = 1; // iterator for the columns of the listview //get minimum balance list QList minBalanceList = forecast.accountMinimumBalanceDateList(acc); QList::Iterator t_min; for (t_min = minBalanceList.begin(); t_min != minBalanceList.end() ; ++t_min) { QDate minDate = *t_min; amountMM = forecast.forecastBalance(acc, minDate); amount = MyMoneyUtils::formatMoney(amountMM, acc, currency); advancedItem->setText(it_c, amount); advancedItem->setTextAlignment(it_c, Qt::AlignRight | Qt::AlignVCenter); if (amountMM.isNegative()) { advancedItem->setForeground(it_c, KMyMoneySettings::schemeColor(SchemeColor::Negative)); } it_c++; QString dateString = QLocale().toString(minDate, QLocale::ShortFormat); advancedItem->setText(it_c, dateString); advancedItem->setTextAlignment(it_c, Qt::AlignRight | Qt::AlignVCenter); if (amountMM.isNegative()) { advancedItem->setForeground(it_c, KMyMoneySettings::schemeColor(SchemeColor::Negative)); } it_c++; } //get maximum balance list QList maxBalanceList = forecast.accountMaximumBalanceDateList(acc); QList::Iterator t_max; for (t_max = maxBalanceList.begin(); t_max != maxBalanceList.end() ; ++t_max) { QDate maxDate = *t_max; amountMM = forecast.forecastBalance(acc, maxDate); amount = MyMoneyUtils::formatMoney(amountMM, acc, currency); advancedItem->setText(it_c, amount); advancedItem->setTextAlignment(it_c, Qt::AlignRight | Qt::AlignVCenter); if (amountMM.isNegative()) { advancedItem->setForeground(it_c, KMyMoneySettings::schemeColor(SchemeColor::Negative)); } it_c++; QString dateString = QLocale().toString(maxDate, QLocale::ShortFormat); advancedItem->setText(it_c, dateString); advancedItem->setTextAlignment(it_c, Qt::AlignRight | Qt::AlignVCenter); if (amountMM.isNegative()) { advancedItem->setForeground(it_c, KMyMoneySettings::schemeColor(SchemeColor::Negative)); } it_c++; } //get average balance amountMM = forecast.accountAverageBalance(acc); amount = MyMoneyUtils::formatMoney(amountMM, acc, currency); advancedItem->setText(it_c, amount); advancedItem->setTextAlignment(it_c, Qt::AlignRight | Qt::AlignVCenter); if (amountMM.isNegative()) { advancedItem->setForeground(it_c, KMyMoneySettings::schemeColor(SchemeColor::Negative)); } it_c++; } // make sure all data is shown adjustHeadersAndResizeToContents(ui->m_advancedList); ui->m_advancedList->show(); } void loadBudgetView() { const auto file = MyMoneyFile::instance(); MyMoneyForecast forecast = KMyMoneyUtils::forecast(); //get the settings from current page and calculate this year based on last year QDate historyEndDate = QDate(QDate::currentDate().year() - 1, 12, 31); QDate historyStartDate = historyEndDate.addDays(-ui->m_accountsCycle->value() * ui->m_forecastCycles->value()); QDate forecastStartDate = QDate(QDate::currentDate().year(), 1, 1); QDate forecastEndDate = QDate::currentDate().addDays(ui->m_forecastDays->value()); forecast.setHistoryMethod(ui->m_historyMethod->checkedId()); MyMoneyBudget budget; forecast.createBudget(budget, historyStartDate, historyEndDate, forecastStartDate, forecastEndDate, false); ui->m_budgetList->clear(); ui->m_budgetList->setIconSize(QSize(22, 22)); ui->m_budgetList->setSortingEnabled(true); ui->m_budgetList->sortByColumn(0, Qt::AscendingOrder); //add columns QStringList headerLabels; headerLabels << i18n("Account"); { forecastStartDate = forecast.forecastStartDate(); forecastEndDate = forecast.forecastEndDate(); //add cycle interval columns QDate f_date = forecastStartDate; for (; f_date <= forecastEndDate; f_date = f_date.addMonths(1)) { headerLabels << QDate::longMonthName(f_date.month()); } } //add total column headerLabels << i18nc("Total balance", "Total"); //set the columns ui->m_budgetList->setHeaderLabels(headerLabels); //add default rows addTotalRow(ui->m_budgetList, forecast); addIncomeExpenseRows(forecast); //load income and expense budget accounts loadAccounts(forecast, file->income(), m_incomeItem, EForecastViewType::eBudget); loadAccounts(forecast, file->expense(), m_expenseItem, EForecastViewType::eBudget); adjustHeadersAndResizeToContents(ui->m_budgetList); } void loadChartView() { - MyMoneyReport::EDetailLevel detailLevel[4] = { MyMoneyReport::eDetailAll, MyMoneyReport::eDetailTop, MyMoneyReport::eDetailGroup, MyMoneyReport::eDetailTotal }; + eMyMoney::Report::DetailLevel detailLevel[4] = { eMyMoney::Report::DetailLevel::All, eMyMoney::Report::DetailLevel::Top, eMyMoney::Report::DetailLevel::Group, eMyMoney::Report::DetailLevel::Total }; MyMoneyReport reportCfg = MyMoneyReport( - MyMoneyReport::eAssetLiability, - MyMoneyReport::eMonths, + eMyMoney::Report::RowType::AssetLiability, + static_cast(eMyMoney::Report::ColumnType::Months), eMyMoney::TransactionFilter::Date::UserDefined, // overridden by the setDateFilter() call below detailLevel[ui->m_comboDetail->currentIndex()], i18n("Net Worth Forecast"), i18n("Generated Report")); reportCfg.setChartByDefault(true); reportCfg.setChartCHGridLines(false); reportCfg.setChartSVGridLines(false); - reportCfg.setChartType(MyMoneyReport::eChartLine); + reportCfg.setChartType(eMyMoney::Report::ChartType::Line); reportCfg.setIncludingSchedules(false); // FIXME: this causes a crash //reportCfg.setColumnsAreDays( true ); reportCfg.setChartDataLabels(false); reportCfg.setConvertCurrency(true); reportCfg.setIncludingForecast(true); reportCfg.setDateFilter(QDate::currentDate(), QDate::currentDate().addDays(ui->m_forecastDays->value())); reports::PivotTable table(reportCfg); table.drawChart(*m_forecastChart); // Adjust the size m_forecastChart->resize(ui->m_tab->width() - 10, ui->m_tab->height()); //m_forecastChart->show(); m_forecastChart->update(); } void loadForecastSettings() { //fill the settings controls ui->m_forecastDays->setValue(KMyMoneySettings::forecastDays()); ui->m_accountsCycle->setValue(KMyMoneySettings::forecastAccountCycle()); ui->m_beginDay->setValue(KMyMoneySettings::beginForecastDay()); ui->m_forecastCycles->setValue(KMyMoneySettings::forecastCycles()); ui->m_historyMethod->setId(ui->radioButton11, 0); // simple moving avg ui->m_historyMethod->setId(ui->radioButton12, 1); // weighted moving avg ui->m_historyMethod->setId(ui->radioButton13, 2); // linear regression ui->m_historyMethod->button(KMyMoneySettings::historyMethod())->setChecked(true); switch (KMyMoneySettings::forecastMethod()) { case 0: ui->m_forecastMethod->setText(i18nc("Scheduled method", "Scheduled")); ui->m_forecastCycles->setDisabled(true); ui->m_historyMethodGroupBox->setDisabled(true); break; case 1: ui->m_forecastMethod->setText(i18nc("History-based method", "History")); ui->m_forecastCycles->setEnabled(true); ui->m_historyMethodGroupBox->setEnabled(true); break; default: ui->m_forecastMethod->setText(i18nc("Unknown forecast method", "Unknown")); break; } } void addAssetLiabilityRows(const MyMoneyForecast& forecast) { const auto file = MyMoneyFile::instance(); m_assetItem = new QTreeWidgetItem(m_totalItem); m_assetItem->setText(0, file->asset().name()); m_assetItem->setIcon(0, file->asset().accountPixmap()); m_assetItem->setData(0, ForecastRole, QVariant::fromValue(forecast)); m_assetItem->setData(0, AccountRole, QVariant::fromValue(file->asset())); m_assetItem->setExpanded(true); m_liabilityItem = new QTreeWidgetItem(m_totalItem); m_liabilityItem->setText(0, file->liability().name()); m_liabilityItem->setIcon(0, file->liability().accountPixmap()); m_liabilityItem->setData(0, ForecastRole, QVariant::fromValue(forecast)); m_liabilityItem->setData(0, AccountRole, QVariant::fromValue(file->liability())); m_liabilityItem->setExpanded(true); } void addIncomeExpenseRows(const MyMoneyForecast& forecast) { const auto file = MyMoneyFile::instance(); m_incomeItem = new QTreeWidgetItem(m_totalItem); m_incomeItem->setText(0, file->income().name()); m_incomeItem->setIcon(0, file->income().accountPixmap()); m_incomeItem->setData(0, ForecastRole, QVariant::fromValue(forecast)); m_incomeItem->setData(0, AccountRole, QVariant::fromValue(file->income())); m_incomeItem->setExpanded(true); m_expenseItem = new QTreeWidgetItem(m_totalItem); m_expenseItem->setText(0, file->expense().name()); m_expenseItem->setIcon(0, file->expense().accountPixmap()); m_expenseItem->setData(0, ForecastRole, QVariant::fromValue(forecast)); m_expenseItem->setData(0, AccountRole, QVariant::fromValue(file->expense())); m_expenseItem->setExpanded(true); } void addTotalRow(QTreeWidget* forecastList, const MyMoneyForecast& forecast) { const auto file = MyMoneyFile::instance(); m_totalItem = new QTreeWidgetItem(forecastList); QFont font; font.setBold(true); m_totalItem->setFont(0, font); m_totalItem->setText(0, i18nc("Total balance", "Total")); m_totalItem->setIcon(0, file->asset().accountPixmap()); m_totalItem->setData(0, ForecastRole, QVariant::fromValue(forecast)); m_totalItem->setData(0, AccountRole, QVariant::fromValue(file->asset())); m_totalItem->setExpanded(true); } bool includeAccount(MyMoneyForecast& forecast, const MyMoneyAccount& acc) { const auto file = MyMoneyFile::instance(); if (forecast.isForecastAccount(acc)) return true; foreach (const auto sAccount, acc.accountList()) { auto account = file->account(sAccount); if (includeAccount(forecast, account)) return true; } return false; } void adjustHeadersAndResizeToContents(QTreeWidget *widget) { QSize sizeHint(0, widget->sizeHintForRow(0)); QTreeWidgetItem *header = widget->headerItem(); for (int i = 0; i < header->columnCount(); ++i) { if (i > 0) { header->setData(i, Qt::TextAlignmentRole, Qt::AlignRight); // make sure that the row height stays the same even when the column that has icons is not visible if (m_totalItem) { m_totalItem->setSizeHint(i, sizeHint); } } widget->resizeColumnToContents(i); } } void setNegative(QTreeWidgetItem *item, bool isNegative) { if (isNegative) { for (int i = 0; i < item->columnCount(); ++i) { item->setForeground(i, KMyMoneySettings::schemeColor(SchemeColor::Negative)); } } } void showAmount(QTreeWidgetItem* item, int column, const MyMoneyMoney& amount, const MyMoneySecurity& security) { item->setText(column, MyMoneyUtils::formatMoney(amount, security)); item->setTextAlignment(column, Qt::AlignRight | Qt::AlignVCenter); item->setFont(column, item->font(0)); if (amount.isNegative()) { item->setForeground(column, KMyMoneySettings::schemeColor(SchemeColor::Negative)); } } void adjustParentValue(QTreeWidgetItem *item, int column, const MyMoneyMoney& value) { if (!item) return; item->setData(column, ValueRole, QVariant::fromValue(item->data(column, ValueRole).value() + value)); item->setData(column, ValueRole, QVariant::fromValue(item->data(column, ValueRole).value().convert(MyMoneyFile::instance()->baseCurrency().smallestAccountFraction()))); // if the entry has no children, // or it is the top entry // or it is currently not open // we need to display the value of it if (item->childCount() == 0 || !item->parent() || (!item->isExpanded() && item->childCount() > 0) || (item->parent() && !item->parent()->parent())) { if (item->childCount() > 0) item->setText(column, " "); MyMoneyMoney amount = item->data(column, ValueRole).value(); showAmount(item, column, amount, MyMoneyFile::instance()->baseCurrency()); } // now make sure, the upstream accounts also get notified about the value change adjustParentValue(item->parent(), column, value); } void setValue(QTreeWidgetItem* item, int column, const MyMoneyMoney& amount, const QDate& forecastDate) { MyMoneyAccount account = item->data(0, AccountRole).value(); //calculate the balance in base currency for the total row if (account.currencyId() != MyMoneyFile::instance()->baseCurrency().id()) { ReportAccount repAcc = ReportAccount(account.id()); MyMoneyMoney curPrice = repAcc.baseCurrencyPrice(forecastDate); MyMoneyMoney baseAmountMM = amount * curPrice; MyMoneyMoney value = baseAmountMM.convert(MyMoneyFile::instance()->baseCurrency().smallestAccountFraction()); item->setData(column, ValueRole, QVariant::fromValue(value)); adjustParentValue(item->parent(), column, value); } else { item->setData(column, ValueRole, QVariant::fromValue(item->data(column, ValueRole).value() + amount)); adjustParentValue(item->parent(), column, amount); } } void setAmount(QTreeWidgetItem* item, int column, const MyMoneyMoney& amount) { item->setData(column, AmountRole, QVariant::fromValue(amount)); item->setTextAlignment(column, Qt::AlignRight | Qt::AlignVCenter); } void updateSummary(QTreeWidgetItem *item) { MyMoneyMoney amountMM; auto it_c = 1; // iterator for the columns of the listview const auto file = MyMoneyFile::instance(); qint64 daysToBeginDay; MyMoneyForecast forecast = item->data(0, ForecastRole).value(); if (QDate::currentDate() < forecast.beginForecastDate()) { daysToBeginDay = QDate::currentDate().daysTo(forecast.beginForecastDate()); } else { daysToBeginDay = forecast.accountsCycle(); } MyMoneyAccount account = item->data(0, AccountRole).value(); MyMoneySecurity currency; if (account.isInvest()) { MyMoneySecurity underSecurity = file->security(account.currencyId()); currency = file->security(underSecurity.tradingCurrency()); } else { currency = file->security(account.currencyId()); } //add current balance column QDate summaryDate = QDate::currentDate(); amountMM = forecast.forecastBalance(account, summaryDate); //calculate the balance in base currency for the total row setAmount(item, it_c, amountMM); setValue(item, it_c, amountMM, summaryDate); showAmount(item, it_c, amountMM, currency); it_c++; //iterate through all other columns for (summaryDate = QDate::currentDate().addDays(daysToBeginDay); summaryDate <= forecast.forecastEndDate(); summaryDate = summaryDate.addDays(forecast.accountsCycle()), ++it_c) { amountMM = forecast.forecastBalance(account, summaryDate); //calculate the balance in base currency for the total row setAmount(item, it_c, amountMM); setValue(item, it_c, amountMM, summaryDate); showAmount(item, it_c, amountMM, currency); } //calculate and add variation per cycle setNegative(item, forecast.accountTotalVariation(account).isNegative()); setAmount(item, it_c, forecast.accountTotalVariation(account)); setValue(item, it_c, forecast.accountTotalVariation(account), forecast.forecastEndDate()); showAmount(item, it_c, forecast.accountTotalVariation(account), currency); } void updateDetailed(QTreeWidgetItem *item) { QString amount; QString vAmount; MyMoneyMoney vAmountMM; const auto file = MyMoneyFile::instance(); MyMoneyAccount account = item->data(0, AccountRole).value(); MyMoneySecurity currency; if (account.isInvest()) { MyMoneySecurity underSecurity = file->security(account.currencyId()); currency = file->security(underSecurity.tradingCurrency()); } else { currency = file->security(account.currencyId()); } int it_c = 1; // iterator for the columns of the listview MyMoneyForecast forecast = item->data(0, ForecastRole).value(); for (QDate forecastDate = QDate::currentDate(); forecastDate <= forecast.forecastEndDate(); ++it_c, forecastDate = forecastDate.addDays(1)) { MyMoneyMoney amountMM = forecast.forecastBalance(account, forecastDate); //calculate the balance in base currency for the total row setAmount(item, it_c, amountMM); setValue(item, it_c, amountMM, forecastDate); showAmount(item, it_c, amountMM, currency); } //calculate and add variation per cycle vAmountMM = forecast.accountTotalVariation(account); setAmount(item, it_c, vAmountMM); setValue(item, it_c, vAmountMM, forecast.forecastEndDate()); showAmount(item, it_c, vAmountMM, currency); } void updateBudget(QTreeWidgetItem *item) { MyMoneySecurity currency; MyMoneyMoney tAmountMM; MyMoneyForecast forecast = item->data(0, ForecastRole).value(); const auto file = MyMoneyFile::instance(); int it_c = 1; // iterator for the columns of the listview QDate forecastDate = forecast.forecastStartDate(); MyMoneyAccount account = item->data(0, AccountRole).value(); if (account.isInvest()) { MyMoneySecurity underSecurity = file->security(account.currencyId()); currency = file->security(underSecurity.tradingCurrency()); } else { currency = file->security(account.currencyId()); } //iterate columns for (; forecastDate <= forecast.forecastEndDate(); forecastDate = forecastDate.addMonths(1), ++it_c) { MyMoneyMoney amountMM; amountMM = forecast.forecastBalance(account, forecastDate); if (account.accountType() == eMyMoney::Account::Type::Expense) amountMM = -amountMM; tAmountMM += amountMM; setAmount(item, it_c, amountMM); setValue(item, it_c, amountMM, forecastDate); showAmount(item, it_c, amountMM, currency); } //set total column setAmount(item, it_c, tAmountMM); setValue(item, it_c, tAmountMM, forecast.forecastEndDate()); showAmount(item, it_c, tAmountMM, currency); } /** * Get the list of prices for an account * This is used later to create an instance of KMyMoneyAccountTreeForecastItem * */ // QList getAccountPrices(const MyMoneyAccount& acc) // { // const auto file = MyMoneyFile::instance(); // QList prices; // MyMoneySecurity security = file->baseCurrency(); // try { // if (acc.isInvest()) { // security = file->security(acc.currencyId()); // if (security.tradingCurrency() != file->baseCurrency().id()) { // MyMoneySecurity sec = file->security(security.tradingCurrency()); // prices += file->price(sec.id(), file->baseCurrency().id()); // } // } else if (acc.currencyId() != file->baseCurrency().id()) { // if (acc.currencyId() != file->baseCurrency().id()) { // security = file->security(acc.currencyId()); // prices += file->price(acc.currencyId(), file->baseCurrency().id()); // } // } // } catch (const MyMoneyException &e) { // qDebug() << Q_FUNC_INFO << " caught exception while adding " << acc.name() << "[" << acc.id() << "]: " << e.what(); // } // return prices; // } KForecastView *q_ptr; Ui::KForecastView *ui; bool m_needReload[MaxViewTabs]; /** * This member holds the load state of page */ bool m_needLoad; QTreeWidgetItem* m_totalItem; QTreeWidgetItem* m_assetItem; QTreeWidgetItem* m_liabilityItem; QTreeWidgetItem* m_incomeItem; QTreeWidgetItem* m_expenseItem; QLayout* m_chartLayout; reports::KReportChartView* m_forecastChart; QScopedPointer m_fixedColumnView; QMap m_nameIdx; }; #endif diff --git a/kmymoney/plugins/views/reports/kreportconfigurationfilterdlg.cpp b/kmymoney/plugins/views/reports/kreportconfigurationfilterdlg.cpp index a67d86086..ca10e0d3b 100644 --- a/kmymoney/plugins/views/reports/kreportconfigurationfilterdlg.cpp +++ b/kmymoney/plugins/views/reports/kreportconfigurationfilterdlg.cpp @@ -1,660 +1,660 @@ /*************************************************************************** kreportconfigurationdlg.cpp - description ------------------- begin : Mon Jun 21 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 (C) 2017, 2018 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 "kreportconfigurationfilterdlg.h" // ---------------------------------------------------------------------------- // QT Includes #include // ---------------------------------------------------------------------------- // KDE Includes #include #include // ---------------------------------------------------------------------------- // Project Includes #include "ktransactionfilter.h" #include "kmymoneyaccountselector.h" #include "mymoneyfile.h" #include "mymoneyexception.h" #include "mymoneybudget.h" #include "mymoneyreport.h" #include "daterangedlg.h" #include "reporttabimpl.h" #include "mymoneyenums.h" #include #include #include #include #include #include #include #include class KReportConfigurationFilterDlgPrivate { Q_DISABLE_COPY(KReportConfigurationFilterDlgPrivate) public: KReportConfigurationFilterDlgPrivate(KReportConfigurationFilterDlg *qq) : q_ptr(qq), ui(new Ui::KReportConfigurationFilterDlg), m_tabRowColPivot(nullptr), m_tabRowColQuery(nullptr), m_tabChart(nullptr), m_tabRange(nullptr), m_dateRange(nullptr) { } ~KReportConfigurationFilterDlgPrivate() { delete ui; } KReportConfigurationFilterDlg *q_ptr; Ui::KReportConfigurationFilterDlg *ui; QPointer m_tabGeneral; QPointer m_tabRowColPivot; QPointer m_tabRowColQuery; QPointer m_tabChart; QPointer m_tabRange; QPointer m_tabCapitalGain; QPointer m_tabPerformance; QPointer m_tabFilters; MyMoneyReport m_initialState; MyMoneyReport m_currentState; QVector m_budgets; DateRangeDlg *m_dateRange; }; KReportConfigurationFilterDlg::KReportConfigurationFilterDlg(MyMoneyReport report, QWidget *parent) : QDialog(parent), d_ptr(new KReportConfigurationFilterDlgPrivate(this)) { Q_D(KReportConfigurationFilterDlg); d->ui->setupUi(this); d->m_initialState = report; d->m_currentState = report; // // Rework labeling // setWindowTitle(i18n("Report Configuration")); // // Rework the buttons // // the Apply button is always enabled d->ui->buttonBox->button(QDialogButtonBox::Apply)->setEnabled(true); d->ui->buttonBox->button(QDialogButtonBox::Apply)->setToolTip(i18nc("@info:tooltip for report configuration apply button", "Apply the configuration changes to the report")); connect(d->ui->buttonBox->button(QDialogButtonBox::Apply), &QAbstractButton::clicked, this, &KReportConfigurationFilterDlg::slotSearch); connect(d->ui->buttonBox->button(QDialogButtonBox::Close), &QAbstractButton::clicked, this, &QDialog::close); connect(d->ui->buttonBox->button(QDialogButtonBox::Reset), &QAbstractButton::clicked, this, &KReportConfigurationFilterDlg::slotReset); connect(d->ui->buttonBox->button(QDialogButtonBox::Help), &QAbstractButton::clicked, this, &KReportConfigurationFilterDlg::slotShowHelp); // // Add new tabs // - if (d->m_initialState.reportType() == MyMoneyReport::ePivotTable) { + if (d->m_initialState.reportType() == eMyMoney::Report::ReportType::PivotTable) { // we will use date range together with data range - d->m_tabFilters = new KTransactionFilter(this, (report.rowType() == MyMoneyReport::eAccount), false); + d->m_tabFilters = new KTransactionFilter(this, (report.rowType() == eMyMoney::Report::RowType::Account), false); } else { - d->m_tabFilters = new KTransactionFilter(this, (report.rowType() == MyMoneyReport::eAccount)); + d->m_tabFilters = new KTransactionFilter(this, (report.rowType() == eMyMoney::Report::RowType::Account)); d->m_dateRange = d->m_tabFilters->dateRange(); } d->ui->m_tabWidget->addTab(d->m_tabFilters, i18nc("Filters tab", "Filters")); d->m_tabGeneral = new ReportTabGeneral(d->ui->m_criteriaTab); d->ui->m_criteriaTab->insertTab(0, d->m_tabGeneral, i18nc("General tab", "General")); - if (d->m_initialState.reportType() == MyMoneyReport::ePivotTable) { + if (d->m_initialState.reportType() == eMyMoney::Report::ReportType::PivotTable) { int tabNr = 1; if (!(d->m_initialState.isIncludingPrice() || d->m_initialState.isIncludingAveragePrice())) { d->m_tabRowColPivot = new ReportTabRowColPivot(d->ui->m_criteriaTab); d->ui->m_criteriaTab->insertTab(tabNr++, d->m_tabRowColPivot, i18n("Rows/Columns")); connect(d->m_tabRowColPivot->ui->m_comboRows, static_cast(&QComboBox::activated), this, static_cast(&KReportConfigurationFilterDlg::slotRowTypeChanged)); connect(d->m_tabRowColPivot->ui->m_comboRows, static_cast(&QComboBox::activated), this, static_cast(&KReportConfigurationFilterDlg::slotUpdateColumnsCombo)); //control the state of the includeTransfer check connect(d->m_tabFilters->categoriesView(), &KMyMoneySelector::stateChanged, this, &KReportConfigurationFilterDlg::slotUpdateCheckTransfers); } d->m_tabChart = new ReportTabChart(d->ui->m_criteriaTab); d->ui->m_criteriaTab->insertTab(tabNr++, d->m_tabChart, i18n("Chart")); d->m_tabRange = new ReportTabRange(d->ui->m_criteriaTab); d->ui->m_criteriaTab->insertTab(tabNr++, d->m_tabRange, i18n("Range")); d->m_dateRange = d->m_tabRange->m_dateRange; if (!(d->m_initialState.isIncludingPrice() || d->m_initialState.isIncludingAveragePrice())) { connect(d->m_tabRange->ui->m_comboColumns, static_cast(&QComboBox::activated), this, &KReportConfigurationFilterDlg::slotColumnTypeChanged); connect(d->m_tabRange->ui->m_comboColumns, static_cast(&QComboBox::activated), this, static_cast(&KReportConfigurationFilterDlg::slotUpdateColumnsCombo)); } connect(d->m_tabChart->ui->m_logYaxis, &QCheckBox::stateChanged, this, &KReportConfigurationFilterDlg::slotLogAxisChanged); - } else if (d->m_initialState.reportType() == MyMoneyReport::eQueryTable) { + } else if (d->m_initialState.reportType() == eMyMoney::Report::ReportType::QueryTable) { // eInvestmentHoldings is a special-case report, and you cannot configure the // rows & columns of that report. - if (d->m_initialState.rowType() < MyMoneyReport::eAccountByTopAccount) { + if (d->m_initialState.rowType() < eMyMoney::Report::RowType::AccountByTopAccount) { d->m_tabRowColQuery = new ReportTabRowColQuery(d->ui->m_criteriaTab); d->ui->m_criteriaTab->insertTab(1, d->m_tabRowColQuery, i18n("Rows/Columns")); } - if (d->m_initialState.queryColumns() & MyMoneyReport::eQCcapitalgain) { + if (d->m_initialState.queryColumns() & eMyMoney::Report::QueryColumn::CapitalGain) { d->m_tabCapitalGain = new ReportTabCapitalGain(d->ui->m_criteriaTab); d->ui->m_criteriaTab->insertTab(1, d->m_tabCapitalGain, i18n("Report")); } - if (d->m_initialState.queryColumns() & MyMoneyReport::eQCperformance) { + if (d->m_initialState.queryColumns() & eMyMoney::Report::QueryColumn::Performance) { d->m_tabPerformance = new ReportTabPerformance(d->ui->m_criteriaTab); d->ui->m_criteriaTab->insertTab(1, d->m_tabPerformance, i18n("Report")); } } d->ui->m_criteriaTab->setCurrentIndex(d->ui->m_criteriaTab->indexOf(d->m_tabGeneral)); d->ui->m_criteriaTab->setMinimumSize(500, 200); QList list = MyMoneyFile::instance()->budgetList(); QList::const_iterator it_b; for (it_b = list.constBegin(); it_b != list.constEnd(); ++it_b) { d->m_budgets.push_back(*it_b); } // // Now set up the widgets with proper values // slotReset(); } KReportConfigurationFilterDlg::~KReportConfigurationFilterDlg() { } MyMoneyReport KReportConfigurationFilterDlg::getConfig() const { Q_D(const KReportConfigurationFilterDlg); return d->m_currentState; } void KReportConfigurationFilterDlg::slotSearch() { Q_D(KReportConfigurationFilterDlg); // setup the filter from the dialog widgets auto filter = d->m_tabFilters->setupFilter(); // Copy the m_filter over to the filter part of m_currentConfig. d->m_currentState.assignFilter(filter); // Then extract the report properties d->m_currentState.setName(d->m_tabGeneral->ui->m_editName->text()); d->m_currentState.setComment(d->m_tabGeneral->ui->m_editComment->text()); d->m_currentState.setConvertCurrency(d->m_tabGeneral->ui->m_checkCurrency->isChecked()); d->m_currentState.setFavorite(d->m_tabGeneral->ui->m_checkFavorite->isChecked()); d->m_currentState.setSkipZero(d->m_tabGeneral->ui->m_skipZero->isChecked()); if (d->m_tabRowColPivot) { - MyMoneyReport::EDetailLevel dl[4] = { MyMoneyReport::eDetailAll, MyMoneyReport::eDetailTop, MyMoneyReport::eDetailGroup, MyMoneyReport::eDetailTotal }; + eMyMoney::Report::DetailLevel dl[4] = { eMyMoney::Report::DetailLevel::All, eMyMoney::Report::DetailLevel::Top, eMyMoney::Report::DetailLevel::Group, eMyMoney::Report::DetailLevel::Total }; d->m_currentState.setDetailLevel(dl[d->m_tabRowColPivot->ui->m_comboDetail->currentIndex()]); // modify the rowtype only if the widget is enabled if (d->m_tabRowColPivot->ui->m_comboRows->isEnabled()) { - MyMoneyReport::ERowType rt[2] = { MyMoneyReport::eExpenseIncome, MyMoneyReport::eAssetLiability }; + eMyMoney::Report::RowType rt[2] = { eMyMoney::Report::RowType::ExpenseIncome, eMyMoney::Report::RowType::AssetLiability }; d->m_currentState.setRowType(rt[d->m_tabRowColPivot->ui->m_comboRows->currentIndex()]); } d->m_currentState.setShowingRowTotals(false); if (d->m_tabRowColPivot->ui->m_comboRows->currentIndex() == 0) d->m_currentState.setShowingRowTotals(d->m_tabRowColPivot->ui->m_checkTotalColumn->isChecked()); d->m_currentState.setShowingColumnTotals(d->m_tabRowColPivot->ui->m_checkTotalRow->isChecked()); d->m_currentState.setIncludingSchedules(d->m_tabRowColPivot->ui->m_checkScheduled->isChecked()); d->m_currentState.setIncludingTransfers(d->m_tabRowColPivot->ui->m_checkTransfers->isChecked()); d->m_currentState.setIncludingUnusedAccounts(d->m_tabRowColPivot->ui->m_checkUnused->isChecked()); if (d->m_tabRowColPivot->ui->m_comboBudget->isEnabled()) { - d->m_currentState.setBudget(d->m_budgets[d->m_tabRowColPivot->ui->m_comboBudget->currentItem()].id(), d->m_initialState.rowType() == MyMoneyReport::eBudgetActual); + d->m_currentState.setBudget(d->m_budgets[d->m_tabRowColPivot->ui->m_comboBudget->currentItem()].id(), d->m_initialState.rowType() == eMyMoney::Report::RowType::BudgetActual); } else { d->m_currentState.setBudget(QString(), false); } //set moving average days if (d->m_tabRowColPivot->ui->m_movingAverageDays->isEnabled()) { d->m_currentState.setMovingAverageDays(d->m_tabRowColPivot->ui->m_movingAverageDays->value()); } } else if (d->m_tabRowColQuery) { - MyMoneyReport::ERowType rtq[8] = { MyMoneyReport::eCategory, MyMoneyReport::eTopCategory, MyMoneyReport::eTag, MyMoneyReport::ePayee, MyMoneyReport::eAccount, MyMoneyReport::eTopAccount, MyMoneyReport::eMonth, MyMoneyReport::eWeek }; + eMyMoney::Report::RowType rtq[8] = { eMyMoney::Report::RowType::Category, eMyMoney::Report::RowType::TopCategory, eMyMoney::Report::RowType::Tag, eMyMoney::Report::RowType::Payee, eMyMoney::Report::RowType::Account, eMyMoney::Report::RowType::TopAccount, eMyMoney::Report::RowType::Month, eMyMoney::Report::RowType::Week }; d->m_currentState.setRowType(rtq[d->m_tabRowColQuery->ui->m_comboOrganizeBy->currentIndex()]); - unsigned qc = MyMoneyReport::eQCnone; + unsigned qc = eMyMoney::Report::QueryColumn::None; - if (d->m_currentState.queryColumns() & MyMoneyReport::eQCloan) + if (d->m_currentState.queryColumns() & eMyMoney::Report::QueryColumn::Loan) // once a loan report, always a loan report - qc = MyMoneyReport::eQCloan; + qc = eMyMoney::Report::QueryColumn::Loan; if (d->m_tabRowColQuery->ui->m_checkNumber->isChecked()) - qc |= MyMoneyReport::eQCnumber; + qc |= eMyMoney::Report::QueryColumn::Number; if (d->m_tabRowColQuery->ui->m_checkPayee->isChecked()) - qc |= MyMoneyReport::eQCpayee; + qc |= eMyMoney::Report::QueryColumn::Payee; if (d->m_tabRowColQuery->ui->m_checkTag->isChecked()) - qc |= MyMoneyReport::eQCtag; + qc |= eMyMoney::Report::QueryColumn::Tag; if (d->m_tabRowColQuery->ui->m_checkCategory->isChecked()) - qc |= MyMoneyReport::eQCcategory; + qc |= eMyMoney::Report::QueryColumn::Category; if (d->m_tabRowColQuery->ui->m_checkMemo->isChecked()) - qc |= MyMoneyReport::eQCmemo; + qc |= eMyMoney::Report::QueryColumn::Memo; if (d->m_tabRowColQuery->ui->m_checkAccount->isChecked()) - qc |= MyMoneyReport::eQCaccount; + qc |= eMyMoney::Report::QueryColumn::Account; if (d->m_tabRowColQuery->ui->m_checkReconciled->isChecked()) - qc |= MyMoneyReport::eQCreconciled; + qc |= eMyMoney::Report::QueryColumn::Reconciled; if (d->m_tabRowColQuery->ui->m_checkAction->isChecked()) - qc |= MyMoneyReport::eQCaction; + qc |= eMyMoney::Report::QueryColumn::Action; if (d->m_tabRowColQuery->ui->m_checkShares->isChecked()) - qc |= MyMoneyReport::eQCshares; + qc |= eMyMoney::Report::QueryColumn::Shares; if (d->m_tabRowColQuery->ui->m_checkPrice->isChecked()) - qc |= MyMoneyReport::eQCprice; + qc |= eMyMoney::Report::QueryColumn::Price; if (d->m_tabRowColQuery->ui->m_checkBalance->isChecked()) - qc |= MyMoneyReport::eQCbalance; + qc |= eMyMoney::Report::QueryColumn::Balance; - d->m_currentState.setQueryColumns(static_cast(qc)); + d->m_currentState.setQueryColumns(static_cast(qc)); d->m_currentState.setTax(d->m_tabRowColQuery->ui->m_checkTax->isChecked()); d->m_currentState.setInvestmentsOnly(d->m_tabRowColQuery->ui->m_checkInvestments->isChecked()); d->m_currentState.setLoansOnly(d->m_tabRowColQuery->ui->m_checkLoans->isChecked()); d->m_currentState.setDetailLevel(d->m_tabRowColQuery->ui->m_checkHideSplitDetails->isChecked() ? - MyMoneyReport::eDetailNone : MyMoneyReport::eDetailAll); + eMyMoney::Report::DetailLevel::None : eMyMoney::Report::DetailLevel::All); d->m_currentState.setHideTransactions(d->m_tabRowColQuery->ui->m_checkHideTransactions->isChecked()); d->m_currentState.setShowingColumnTotals(!d->m_tabRowColQuery->ui->m_checkHideTotals->isChecked()); } if (d->m_tabChart) { - MyMoneyReport::EChartType ct[5] = { MyMoneyReport::eChartLine, MyMoneyReport::eChartBar, MyMoneyReport::eChartStackedBar, MyMoneyReport::eChartPie, MyMoneyReport::eChartRing }; + eMyMoney::Report::ChartType ct[5] = { eMyMoney::Report::ChartType::Line, eMyMoney::Report::ChartType::Bar, eMyMoney::Report::ChartType::StackedBar, eMyMoney::Report::ChartType::Pie, eMyMoney::Report::ChartType::Ring }; d->m_currentState.setChartType(ct[d->m_tabChart->ui->m_comboType->currentIndex()]); d->m_currentState.setChartCHGridLines(d->m_tabChart->ui->m_checkCHGridLines->isChecked()); d->m_currentState.setChartSVGridLines(d->m_tabChart->ui->m_checkSVGridLines->isChecked()); d->m_currentState.setChartDataLabels(d->m_tabChart->ui->m_checkValues->isChecked()); d->m_currentState.setChartByDefault(d->m_tabChart->ui->m_checkShowChart->isChecked()); d->m_currentState.setChartLineWidth(d->m_tabChart->ui->m_lineWidth->value()); d->m_currentState.setLogYAxis(d->m_tabChart->ui->m_logYaxis->isChecked()); } if (d->m_tabRange) { d->m_currentState.setDataRangeStart(d->m_tabRange->ui->m_dataRangeStart->text()); d->m_currentState.setDataRangeEnd(d->m_tabRange->ui->m_dataRangeEnd->text()); d->m_currentState.setDataMajorTick(d->m_tabRange->ui->m_dataMajorTick->text()); d->m_currentState.setDataMinorTick(d->m_tabRange->ui->m_dataMinorTick->text()); d->m_currentState.setYLabelsPrecision(d->m_tabRange->ui->m_yLabelsPrecision->value()); - d->m_currentState.setDataFilter((MyMoneyReport::dataOptionE)d->m_tabRange->ui->m_dataLock->currentIndex()); + d->m_currentState.setDataFilter((eMyMoney::Report::DataLock)d->m_tabRange->ui->m_dataLock->currentIndex()); - MyMoneyReport::EColumnType ct[6] = { MyMoneyReport::eDays, MyMoneyReport::eWeeks, MyMoneyReport::eMonths, MyMoneyReport::eBiMonths, MyMoneyReport::eQuarters, MyMoneyReport::eYears }; + eMyMoney::Report::ColumnType ct[6] = { eMyMoney::Report::ColumnType::Days, eMyMoney::Report::ColumnType::Weeks, eMyMoney::Report::ColumnType::Months, eMyMoney::Report::ColumnType::BiMonths, eMyMoney::Report::ColumnType::Quarters, eMyMoney::Report::ColumnType::Years }; bool dy[6] = { true, true, false, false, false, false }; d->m_currentState.setColumnType(ct[d->m_tabRange->ui->m_comboColumns->currentIndex()]); //TODO (Ace) This should be implicit in the call above. MMReport needs fixin' d->m_currentState.setColumnsAreDays(dy[d->m_tabRange->ui->m_comboColumns->currentIndex()]); d->m_currentState.setDateFilter(d->m_dateRange->fromDate(), d->m_dateRange->toDate()); } // setup the date lock eMyMoney::TransactionFilter::Date range = d->m_dateRange->dateRange(); d->m_currentState.setDateFilter(range); if (d->m_tabCapitalGain) { d->m_currentState.setTermSeparator(d->m_tabCapitalGain->ui->m_termSeparator->date()); d->m_currentState.setShowSTLTCapitalGains(d->m_tabCapitalGain->ui->m_showSTLTCapitalGains->isChecked()); d->m_currentState.setSettlementPeriod(d->m_tabCapitalGain->ui->m_settlementPeriod->value()); d->m_currentState.setShowingColumnTotals(!d->m_tabCapitalGain->ui->m_checkHideTotals->isChecked()); - d->m_currentState.setInvestmentSum(static_cast(d->m_tabCapitalGain->ui->m_investmentSum->currentData().toInt())); + d->m_currentState.setInvestmentSum(static_cast(d->m_tabCapitalGain->ui->m_investmentSum->currentData().toInt())); } if (d->m_tabPerformance) { d->m_currentState.setShowingColumnTotals(!d->m_tabPerformance->ui->m_checkHideTotals->isChecked()); - d->m_currentState.setInvestmentSum(static_cast(d->m_tabPerformance->ui->m_investmentSum->currentData().toInt())); + d->m_currentState.setInvestmentSum(static_cast(d->m_tabPerformance->ui->m_investmentSum->currentData().toInt())); } done(true); } void KReportConfigurationFilterDlg::slotRowTypeChanged(int row) { Q_D(KReportConfigurationFilterDlg); d->m_tabRowColPivot->ui->m_checkTotalColumn->setEnabled(row == 0); } void KReportConfigurationFilterDlg::slotColumnTypeChanged(int row) { Q_D(KReportConfigurationFilterDlg); if ((d->m_tabRowColPivot->ui->m_comboBudget->isEnabled() && row < 2)) { d->m_tabRange->ui->m_comboColumns->setCurrentItem(i18nc("@item the columns will display monthly data", "Monthly"), false); } } void KReportConfigurationFilterDlg::slotUpdateColumnsCombo() { Q_D(KReportConfigurationFilterDlg); const int monthlyIndex = 2; const int incomeExpenseIndex = 0; const bool isIncomeExpenseForecast = d->m_currentState.isIncludingForecast() && d->m_tabRowColPivot->ui->m_comboRows->currentIndex() == incomeExpenseIndex; if (isIncomeExpenseForecast && d->m_tabRange->ui->m_comboColumns->currentIndex() != monthlyIndex) { d->m_tabRange->ui->m_comboColumns->setCurrentItem(i18nc("@item the columns will display monthly data", "Monthly"), false); } } void KReportConfigurationFilterDlg::slotUpdateColumnsCombo(int) { slotUpdateColumnsCombo(); } void KReportConfigurationFilterDlg::slotLogAxisChanged(int state) { Q_D(KReportConfigurationFilterDlg); if (state == Qt::Checked) d->m_tabRange->setRangeLogarythmic(true); else d->m_tabRange->setRangeLogarythmic(false); } void KReportConfigurationFilterDlg::slotReset() { Q_D(KReportConfigurationFilterDlg); // // Set up the widget from the initial filter // d->m_currentState = d->m_initialState; // // Report Properties // d->m_tabGeneral->ui->m_editName->setText(d->m_initialState.name()); d->m_tabGeneral->ui->m_editComment->setText(d->m_initialState.comment()); d->m_tabGeneral->ui->m_checkCurrency->setChecked(d->m_initialState.isConvertCurrency()); d->m_tabGeneral->ui->m_checkFavorite->setChecked(d->m_initialState.isFavorite()); if (d->m_initialState.isIncludingPrice() || d->m_initialState.isSkippingZero()) { d->m_tabGeneral->ui->m_skipZero->setChecked(d->m_initialState.isSkippingZero()); } else { d->m_tabGeneral->ui->m_skipZero->setEnabled(false); } if (d->m_tabRowColPivot) { KComboBox *combo = d->m_tabRowColPivot->ui->m_comboDetail; switch (d->m_initialState.detailLevel()) { - case MyMoneyReport::eDetailNone: - case MyMoneyReport::eDetailEnd: - case MyMoneyReport::eDetailAll: + case eMyMoney::Report::DetailLevel::None: + case eMyMoney::Report::DetailLevel::End: + case eMyMoney::Report::DetailLevel::All: combo->setCurrentItem(i18nc("All accounts", "All"), false); break; - case MyMoneyReport::eDetailTop: + case eMyMoney::Report::DetailLevel::Top: combo->setCurrentItem(i18n("Top-Level"), false); break; - case MyMoneyReport::eDetailGroup: + case eMyMoney::Report::DetailLevel::Group: combo->setCurrentItem(i18n("Groups"), false); break; - case MyMoneyReport::eDetailTotal: + case eMyMoney::Report::DetailLevel::Total: combo->setCurrentItem(i18n("Totals"), false); break; } combo = d->m_tabRowColPivot->ui->m_comboRows; switch (d->m_initialState.rowType()) { - case MyMoneyReport::eExpenseIncome: - case MyMoneyReport::eBudget: - case MyMoneyReport::eBudgetActual: + case eMyMoney::Report::RowType::ExpenseIncome: + case eMyMoney::Report::RowType::Budget: + case eMyMoney::Report::RowType::BudgetActual: combo->setCurrentItem(i18n("Income & Expenses"), false); // income / expense break; default: combo->setCurrentItem(i18n("Assets & Liabilities"), false); // asset / liability break; } d->m_tabRowColPivot->ui->m_checkTotalColumn->setChecked(d->m_initialState.isShowingRowTotals()); d->m_tabRowColPivot->ui->m_checkTotalRow->setChecked(d->m_initialState.isShowingColumnTotals()); slotRowTypeChanged(combo->currentIndex()); //load budgets combo - if (d->m_initialState.rowType() == MyMoneyReport::eBudget - || d->m_initialState.rowType() == MyMoneyReport::eBudgetActual) { + if (d->m_initialState.rowType() == eMyMoney::Report::RowType::Budget + || d->m_initialState.rowType() == eMyMoney::Report::RowType::BudgetActual) { d->m_tabRowColPivot->ui->m_comboRows->setEnabled(false); d->m_tabRowColPivot->ui->m_budgetFrame->setEnabled(!d->m_budgets.empty()); auto i = 0; for (QVector::const_iterator it_b = d->m_budgets.constBegin(); it_b != d->m_budgets.constEnd(); ++it_b) { d->m_tabRowColPivot->ui->m_comboBudget->insertItem((*it_b).name(), i); //set the current selected item if ((d->m_initialState.budget() == "Any" && (*it_b).budgetStart().year() == QDate::currentDate().year()) || d->m_initialState.budget() == (*it_b).id()) d->m_tabRowColPivot->ui->m_comboBudget->setCurrentItem(i); i++; } } //set moving average days spinbox QSpinBox *spinbox = d->m_tabRowColPivot->ui->m_movingAverageDays; spinbox->setEnabled(d->m_initialState.isIncludingMovingAverage()); if (d->m_initialState.isIncludingMovingAverage()) { spinbox->setValue(d->m_initialState.movingAverageDays()); } d->m_tabRowColPivot->ui->m_checkScheduled->setChecked(d->m_initialState.isIncludingSchedules()); d->m_tabRowColPivot->ui->m_checkTransfers->setChecked(d->m_initialState.isIncludingTransfers()); d->m_tabRowColPivot->ui->m_checkUnused->setChecked(d->m_initialState.isIncludingUnusedAccounts()); } else if (d->m_tabRowColQuery) { KComboBox *combo = d->m_tabRowColQuery->ui->m_comboOrganizeBy; switch (d->m_initialState.rowType()) { - case MyMoneyReport::eNoRows: - case MyMoneyReport::eCategory: + case eMyMoney::Report::RowType::NoRows: + case eMyMoney::Report::RowType::Category: combo->setCurrentItem(i18n("Categories"), false); break; - case MyMoneyReport::eTopCategory: + case eMyMoney::Report::RowType::TopCategory: combo->setCurrentItem(i18n("Top Categories"), false); break; - case MyMoneyReport::eTag: + case eMyMoney::Report::RowType::Tag: combo->setCurrentItem(i18n("Tags"), false); break; - case MyMoneyReport::ePayee: + case eMyMoney::Report::RowType::Payee: combo->setCurrentItem(i18n("Payees"), false); break; - case MyMoneyReport::eAccount: + case eMyMoney::Report::RowType::Account: combo->setCurrentItem(i18n("Accounts"), false); break; - case MyMoneyReport::eTopAccount: + case eMyMoney::Report::RowType::TopAccount: combo->setCurrentItem(i18n("Top Accounts"), false); break; - case MyMoneyReport::eMonth: + case eMyMoney::Report::RowType::Month: combo->setCurrentItem(i18n("Month"), false); break; - case MyMoneyReport::eWeek: + case eMyMoney::Report::RowType::Week: combo->setCurrentItem(i18n("Week"), false); break; default: throw MYMONEYEXCEPTION_CSTRING("KReportConfigurationFilterDlg::slotReset(): QueryTable report has invalid rowtype"); } unsigned qc = d->m_initialState.queryColumns(); - d->m_tabRowColQuery->ui->m_checkNumber->setChecked(qc & MyMoneyReport::eQCnumber); - d->m_tabRowColQuery->ui->m_checkPayee->setChecked(qc & MyMoneyReport::eQCpayee); - d->m_tabRowColQuery->ui->m_checkTag->setChecked(qc & MyMoneyReport::eQCtag); - d->m_tabRowColQuery->ui->m_checkCategory->setChecked(qc & MyMoneyReport::eQCcategory); - d->m_tabRowColQuery->ui->m_checkMemo->setChecked(qc & MyMoneyReport::eQCmemo); - d->m_tabRowColQuery->ui->m_checkAccount->setChecked(qc & MyMoneyReport::eQCaccount); - d->m_tabRowColQuery->ui->m_checkReconciled->setChecked(qc & MyMoneyReport::eQCreconciled); - d->m_tabRowColQuery->ui->m_checkAction->setChecked(qc & MyMoneyReport::eQCaction); - d->m_tabRowColQuery->ui->m_checkShares->setChecked(qc & MyMoneyReport::eQCshares); - d->m_tabRowColQuery->ui->m_checkPrice->setChecked(qc & MyMoneyReport::eQCprice); - d->m_tabRowColQuery->ui->m_checkBalance->setChecked(qc & MyMoneyReport::eQCbalance); + d->m_tabRowColQuery->ui->m_checkNumber->setChecked(qc & eMyMoney::Report::QueryColumn::Number); + d->m_tabRowColQuery->ui->m_checkPayee->setChecked(qc & eMyMoney::Report::QueryColumn::Payee); + d->m_tabRowColQuery->ui->m_checkTag->setChecked(qc & eMyMoney::Report::QueryColumn::Tag); + d->m_tabRowColQuery->ui->m_checkCategory->setChecked(qc & eMyMoney::Report::QueryColumn::Category); + d->m_tabRowColQuery->ui->m_checkMemo->setChecked(qc & eMyMoney::Report::QueryColumn::Memo); + d->m_tabRowColQuery->ui->m_checkAccount->setChecked(qc & eMyMoney::Report::QueryColumn::Account); + d->m_tabRowColQuery->ui->m_checkReconciled->setChecked(qc & eMyMoney::Report::QueryColumn::Reconciled); + d->m_tabRowColQuery->ui->m_checkAction->setChecked(qc & eMyMoney::Report::QueryColumn::Action); + d->m_tabRowColQuery->ui->m_checkShares->setChecked(qc & eMyMoney::Report::QueryColumn::Shares); + d->m_tabRowColQuery->ui->m_checkPrice->setChecked(qc & eMyMoney::Report::QueryColumn::Price); + d->m_tabRowColQuery->ui->m_checkBalance->setChecked(qc & eMyMoney::Report::QueryColumn::Balance); d->m_tabRowColQuery->ui->m_checkTax->setChecked(d->m_initialState.isTax()); d->m_tabRowColQuery->ui->m_checkInvestments->setChecked(d->m_initialState.isInvestmentsOnly()); d->m_tabRowColQuery->ui->m_checkLoans->setChecked(d->m_initialState.isLoansOnly()); d->m_tabRowColQuery->ui->m_checkHideTransactions->setChecked(d->m_initialState.isHideTransactions()); d->m_tabRowColQuery->ui->m_checkHideTotals->setChecked(!d->m_initialState.isShowingColumnTotals()); d->m_tabRowColQuery->ui->m_checkHideSplitDetails->setEnabled(!d->m_initialState.isHideTransactions()); d->m_tabRowColQuery->ui->m_checkHideSplitDetails->setChecked - (d->m_initialState.detailLevel() == MyMoneyReport::eDetailNone || d->m_initialState.isHideTransactions()); + (d->m_initialState.detailLevel() == eMyMoney::Report::DetailLevel::None || d->m_initialState.isHideTransactions()); } if (d->m_tabChart) { KMyMoneyGeneralCombo* combo = d->m_tabChart->ui->m_comboType; switch (d->m_initialState.chartType()) { - case MyMoneyReport::eChartNone: - combo->setCurrentItem(MyMoneyReport::eChartLine); + case eMyMoney::Report::ChartType::None: + combo->setCurrentItem(static_cast(eMyMoney::Report::ChartType::Line)); break; - case MyMoneyReport::eChartLine: - case MyMoneyReport::eChartBar: - case MyMoneyReport::eChartStackedBar: - case MyMoneyReport::eChartPie: - case MyMoneyReport::eChartRing: - combo->setCurrentItem(d->m_initialState.chartType()); + case eMyMoney::Report::ChartType::Line: + case eMyMoney::Report::ChartType::Bar: + case eMyMoney::Report::ChartType::StackedBar: + case eMyMoney::Report::ChartType::Pie: + case eMyMoney::Report::ChartType::Ring: + combo->setCurrentItem(static_cast(d->m_initialState.chartType())); break; default: throw MYMONEYEXCEPTION_CSTRING("KReportConfigurationFilterDlg::slotReset(): Report has invalid charttype"); } d->m_tabChart->ui->m_checkCHGridLines->setChecked(d->m_initialState.isChartCHGridLines()); d->m_tabChart->ui->m_checkSVGridLines->setChecked(d->m_initialState.isChartSVGridLines()); d->m_tabChart->ui->m_checkValues->setChecked(d->m_initialState.isChartDataLabels()); d->m_tabChart->ui->m_checkShowChart->setChecked(d->m_initialState.isChartByDefault()); d->m_tabChart->ui->m_lineWidth->setValue(d->m_initialState.chartLineWidth()); d->m_tabChart->ui->m_logYaxis->setChecked(d->m_initialState.isLogYAxis()); } if (d->m_tabRange) { d->m_tabRange->ui->m_dataRangeStart->setText(d->m_initialState.dataRangeStart()); d->m_tabRange->ui->m_dataRangeEnd->setText(d->m_initialState.dataRangeEnd()); d->m_tabRange->ui->m_dataMajorTick->setText(d->m_initialState.dataMajorTick()); d->m_tabRange->ui->m_dataMinorTick->setText(d->m_initialState.dataMinorTick()); d->m_tabRange->ui->m_yLabelsPrecision->setValue(d->m_initialState.yLabelsPrecision()); d->m_tabRange->ui->m_dataLock->setCurrentIndex((int)d->m_initialState.dataFilter()); KComboBox *combo = d->m_tabRange->ui->m_comboColumns; if (d->m_initialState.isColumnsAreDays()) { switch (d->m_initialState.columnType()) { - case MyMoneyReport::eNoColumns: - case MyMoneyReport::eDays: + case eMyMoney::Report::ColumnType::NoColumns: + case eMyMoney::Report::ColumnType::Days: combo->setCurrentItem(i18nc("@item the columns will display daily data", "Daily"), false); break; - case MyMoneyReport::eWeeks: + case eMyMoney::Report::ColumnType::Weeks: combo->setCurrentItem(i18nc("@item the columns will display weekly data", "Weekly"), false); break; default: break; } } else { switch (d->m_initialState.columnType()) { - case MyMoneyReport::eNoColumns: - case MyMoneyReport::eMonths: + case eMyMoney::Report::ColumnType::NoColumns: + case eMyMoney::Report::ColumnType::Months: combo->setCurrentItem(i18nc("@item the columns will display monthly data", "Monthly"), false); break; - case MyMoneyReport::eBiMonths: + case eMyMoney::Report::ColumnType::BiMonths: combo->setCurrentItem(i18nc("@item the columns will display bi-monthly data", "Bi-Monthly"), false); break; - case MyMoneyReport::eQuarters: + case eMyMoney::Report::ColumnType::Quarters: combo->setCurrentItem(i18nc("@item the columns will display quarterly data", "Quarterly"), false); break; - case MyMoneyReport::eYears: + case eMyMoney::Report::ColumnType::Years: combo->setCurrentItem(i18nc("@item the columns will display yearly data", "Yearly"), false); break; default: break; } } } if (d->m_tabCapitalGain) { d->m_tabCapitalGain->ui->m_termSeparator->setDate(d->m_initialState.termSeparator()); d->m_tabCapitalGain->ui->m_showSTLTCapitalGains->setChecked(d->m_initialState.isShowingSTLTCapitalGains()); d->m_tabCapitalGain->ui->m_settlementPeriod->setValue(d->m_initialState.settlementPeriod()); d->m_tabCapitalGain->ui->m_checkHideTotals->setChecked(!d->m_initialState.isShowingColumnTotals()); d->m_tabCapitalGain->ui->m_investmentSum->blockSignals(true); d->m_tabCapitalGain->ui->m_investmentSum->clear(); - d->m_tabCapitalGain->ui->m_investmentSum->addItem(i18n("Only owned"), MyMoneyReport::eSumOwned); - d->m_tabCapitalGain->ui->m_investmentSum->addItem(i18n("Only sold"), MyMoneyReport::eSumSold); + d->m_tabCapitalGain->ui->m_investmentSum->addItem(i18n("Only owned"), static_cast(eMyMoney::Report::InvestmentSum::Owned)); + d->m_tabCapitalGain->ui->m_investmentSum->addItem(i18n("Only sold"), static_cast(eMyMoney::Report::InvestmentSum::Sold)); d->m_tabCapitalGain->ui->m_investmentSum->blockSignals(false); - d->m_tabCapitalGain->ui->m_investmentSum->setCurrentIndex(d->m_tabCapitalGain->ui->m_investmentSum->findData(d->m_initialState.investmentSum())); + d->m_tabCapitalGain->ui->m_investmentSum->setCurrentIndex(d->m_tabCapitalGain->ui->m_investmentSum->findData(static_cast(d->m_initialState.investmentSum()))); } if (d->m_tabPerformance) { d->m_tabPerformance->ui->m_checkHideTotals->setChecked(!d->m_initialState.isShowingColumnTotals()); d->m_tabPerformance->ui->m_investmentSum->blockSignals(true); d->m_tabPerformance->ui->m_investmentSum->clear(); - d->m_tabPerformance->ui->m_investmentSum->addItem(i18n("From period"), MyMoneyReport::eSumPeriod); - d->m_tabPerformance->ui->m_investmentSum->addItem(i18n("Owned and sold"), MyMoneyReport::eSumOwnedAndSold); - d->m_tabPerformance->ui->m_investmentSum->addItem(i18n("Only owned"), MyMoneyReport::eSumOwned); - d->m_tabPerformance->ui->m_investmentSum->addItem(i18n("Only sold"), MyMoneyReport::eSumSold); + d->m_tabPerformance->ui->m_investmentSum->addItem(i18n("From period"), static_cast(eMyMoney::Report::InvestmentSum::Period)); + d->m_tabPerformance->ui->m_investmentSum->addItem(i18n("Owned and sold"), static_cast(eMyMoney::Report::InvestmentSum::OwnedAndSold)); + d->m_tabPerformance->ui->m_investmentSum->addItem(i18n("Only owned"), static_cast(eMyMoney::Report::InvestmentSum::Owned)); + d->m_tabPerformance->ui->m_investmentSum->addItem(i18n("Only sold"), static_cast(eMyMoney::Report::InvestmentSum::Sold)); d->m_tabPerformance->ui->m_investmentSum->blockSignals(false); - d->m_tabPerformance->ui->m_investmentSum->setCurrentIndex(d->m_tabPerformance->ui->m_investmentSum->findData(d->m_initialState.investmentSum())); + d->m_tabPerformance->ui->m_investmentSum->setCurrentIndex(d->m_tabPerformance->ui->m_investmentSum->findData(static_cast(d->m_initialState.investmentSum()))); } d->m_tabFilters->resetFilter(d->m_initialState); if (d->m_dateRange) { d->m_initialState.updateDateFilter(); QDate dateFrom, dateTo; if (d->m_initialState.dateFilter(dateFrom, dateTo)) { if (d->m_initialState.isDateUserDefined()) { d->m_dateRange->setDateRange(dateFrom, dateTo); } else { d->m_dateRange->setDateRange(d->m_initialState.dateRange()); } } else { d->m_dateRange->setDateRange(eMyMoney::TransactionFilter::Date::All); } } } void KReportConfigurationFilterDlg::slotShowHelp() { Q_D(KReportConfigurationFilterDlg); if (d->ui->m_tabWidget->currentIndex() == 1) d->m_tabFilters->slotShowHelp(); else KHelpClient::invokeHelp("details.reports.config"); } //TODO Fix the reports and engine to include transfers even if categories are filtered - bug #1523508 void KReportConfigurationFilterDlg::slotUpdateCheckTransfers() { Q_D(KReportConfigurationFilterDlg); auto cb = d->m_tabRowColPivot->ui->m_checkTransfers; if (!d->m_tabFilters->categoriesView()->allItemsSelected()) { cb->setChecked(false); cb->setDisabled(true); } else { cb->setEnabled(true); } } diff --git a/kmymoney/plugins/views/reports/kreportsview.cpp b/kmymoney/plugins/views/reports/kreportsview.cpp index 27d71a7ee..d7ba8400b 100644 --- a/kmymoney/plugins/views/reports/kreportsview.cpp +++ b/kmymoney/plugins/views/reports/kreportsview.cpp @@ -1,710 +1,710 @@ /*************************************************************************** kreportsview.cpp - description ------------------- begin : Sat Mar 27 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 (C) 2017 Ł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 "kreportsview_p.h" #include // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef ENABLE_WEBENGINE #include #else #include #endif // ---------------------------------------------------------------------------- // KDE Includes #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "ui_reportcontrol.h" #include "mymoneyfile.h" #include "mymoneyreport.h" #include "mymoneyexception.h" #include "kmymoneysettings.h" #include "querytable.h" #include "objectinfotable.h" #include "kreportconfigurationfilterdlg.h" #include "icons/icons.h" #include #include "tocitem.h" #include "tocitemgroup.h" #include "tocitemreport.h" #include "kreportchartview.h" #include "pivottable.h" #include "reporttable.h" #include "reportcontrolimpl.h" #include "mymoneyenums.h" #include "menuenums.h" using namespace reports; using namespace eMyMoney; using namespace Icons; #define VIEW_LEDGER "ledger" #define VIEW_SCHEDULE "schedule" #define VIEW_WELCOME "welcome" #define VIEW_HOME "home" #define VIEW_REPORTS "reports" /** * KReportsView Implementation */ KReportsView::KReportsView(QWidget *parent) : KMyMoneyViewBase(*new KReportsViewPrivate(this), parent) { connect(pActions[eMenu::Action::ReportAccountTransactions], &QAction::triggered, this, &KReportsView::slotReportAccountTransactions); } KReportsView::~KReportsView() { } void KReportsView::executeCustomAction(eView::Action action) { switch(action) { case eView::Action::Refresh: refresh(); break; case eView::Action::SetDefaultFocus: { Q_D(KReportsView); QTimer::singleShot(0, d->m_tocTreeWidget, SLOT(setFocus())); } break; case eView::Action::Print: slotPrintView(); break; case eView::Action::CleanupBeforeFileClose: slotCloseAll(); break; default: break; } } void KReportsView::refresh() { Q_D(KReportsView); if (isVisible()) { d->loadView(); d->m_needsRefresh = false; } else { d->m_needsRefresh = true; } } void KReportsView::showEvent(QShowEvent * event) { Q_D(KReportsView); if (d->m_needLoad) d->init(); emit customActionRequested(View::Reports, eView::Action::AboutToShow); if (d->m_needsRefresh) refresh(); if (auto tab = dynamic_cast(d->m_reportTabWidget->currentWidget())) emit reportSelected(tab->report()); else emit reportSelected(MyMoneyReport()); // don't forget base class implementation QWidget::showEvent(event); } void KReportsView::updateActions(const MyMoneyObject& obj) { Q_D(KReportsView); 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); bool b; if (MyMoneyFile::instance()->isStandardAccount(acc.id())) { b = false; } else { switch (acc.accountType()) { case eMyMoney::Account::Type::Asset: case eMyMoney::Account::Type::Liability: case eMyMoney::Account::Type::Equity: case eMyMoney::Account::Type::Checkings: b = true; break; default: b = false; break; } } pActions[eMenu::Action::ReportAccountTransactions]->setEnabled(b); d->m_currentAccount = acc; } void KReportsView::slotOpenUrl(const QUrl &url) { QString view = url.fileName(); if (view.isEmpty()) return; QString command = QUrlQuery(url).queryItemValue("command"); QString id = QUrlQuery(url).queryItemValue("id"); QString tid = QUrlQuery(url).queryItemValue("tid"); if (view == VIEW_REPORTS) { if (command.isEmpty()) { // slotRefreshView(); } else if (command == QLatin1String("print")) slotPrintView(); else if (command == QLatin1String("copy")) slotCopyView(); else if (command == QLatin1String("save")) slotSaveView(); else if (command == QLatin1String("configure")) slotConfigure(); else if (command == QLatin1String("duplicate")) slotDuplicate(); else if (command == QLatin1String("close")) slotCloseCurrent(); else if (command == QLatin1String("delete")) slotDelete(); else qWarning() << i18n("Unknown command '%1' in KReportsView::slotOpenUrl()", qPrintable(command)); } else if (view == VIEW_LEDGER) { emit selectByVariant(QVariantList {QVariant(id), QVariant(tid)}, eView::Intent::ShowTransaction); } else { qWarning() << i18n("Unknown view '%1' in KReportsView::slotOpenUrl()", qPrintable(view)); } } void KReportsView::slotPrintView() { Q_D(KReportsView); if (auto tab = dynamic_cast(d->m_reportTabWidget->currentWidget())) tab->print(); } void KReportsView::slotCopyView() { Q_D(KReportsView); if (auto tab = dynamic_cast(d->m_reportTabWidget->currentWidget())) tab->copyToClipboard(); } void KReportsView::slotSaveView() { Q_D(KReportsView); if (auto tab = dynamic_cast(d->m_reportTabWidget->currentWidget())) { QString filterList = i18nc("CSV (Filefilter)", "CSV files") + QLatin1String(" (*.csv);;") + i18nc("HTML (Filefilter)", "HTML files") + QLatin1String(" (*.html)"); QUrl newURL = QFileDialog::getSaveFileUrl(this, i18n("Export as"), QUrl::fromLocalFile(KRecentDirs::dir(":kmymoney-export")), filterList, &d->m_selectedExportFilter); if (!newURL.isEmpty()) { KRecentDirs::add(":kmymoney-export", newURL.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).path()); QString newName = newURL.toDisplayString(QUrl::PreferLocalFile); try { tab->saveAs(newName, true); } catch (const MyMoneyException &e) { KMessageBox::error(this, i18n("Failed to save: %1", QString::fromLatin1(e.what()))); } } } } void KReportsView::slotConfigure() { Q_D(KReportsView); QString cm = "KReportsView::slotConfigure"; auto tab = dynamic_cast(d->m_reportTabWidget->currentWidget()); if (!tab) // nothing to do return; int tabNr = d->m_reportTabWidget->currentIndex(); tab->updateDataRange(); // range will be needed during configuration, but cannot be obtained earlier MyMoneyReport report = tab->report(); if (report.comment() == i18n("Default Report") || report.comment() == i18n("Generated Report")) { report.setComment(i18n("Custom Report")); report.setName(i18n("%1 (Customized)", report.name())); } QPointer dlg = new KReportConfigurationFilterDlg(report); if (dlg->exec()) { MyMoneyReport newreport = dlg->getConfig(); // If this report has an ID, then MODIFY it, otherwise ADD it MyMoneyFileTransaction ft; try { if (! newreport.id().isEmpty()) { MyMoneyFile::instance()->modifyReport(newreport); ft.commit(); tab->modifyReport(newreport); d->m_reportTabWidget->setTabText(tabNr, newreport.name()); d->m_reportTabWidget->setCurrentIndex(tabNr) ; } else { MyMoneyFile::instance()->addReport(newreport); ft.commit(); QString reportGroupName = newreport.group(); // find report group TocItemGroup* tocItemGroup = d->m_allTocItemGroups[reportGroupName]; if (!tocItemGroup) { QString error = i18n("Could not find reportgroup \"%1\" for report \"%2\".\nPlease report this error to the developer's list: kmymoney-devel@kde.org", reportGroupName, newreport.name()); // write to messagehandler qWarning() << cm << error; // also inform user KMessageBox::error(d->m_reportTabWidget, error, i18n("Critical Error")); // cleanup delete dlg; return; } // do not add TocItemReport to TocItemGroup here, // this is done in loadView d->addReportTab(newreport); } } catch (const MyMoneyException &e) { KMessageBox::error(this, i18n("Failed to configure report: %1", QString::fromLatin1(e.what()))); } } delete dlg; } void KReportsView::slotDuplicate() { Q_D(KReportsView); QString cm = "KReportsView::slotDuplicate"; auto tab = dynamic_cast(d->m_reportTabWidget->currentWidget()); if (!tab) { // nothing to do return; } MyMoneyReport dupe = tab->report(); dupe.setName(i18n("Copy of %1", dupe.name())); if (dupe.comment() == i18n("Default Report")) dupe.setComment(i18n("Custom Report")); dupe.clearId(); QPointer dlg = new KReportConfigurationFilterDlg(dupe); if (dlg->exec()) { MyMoneyReport newReport = dlg->getConfig(); MyMoneyFileTransaction ft; try { MyMoneyFile::instance()->addReport(newReport); ft.commit(); QString reportGroupName = newReport.group(); // find report group TocItemGroup* tocItemGroup = d->m_allTocItemGroups[reportGroupName]; if (!tocItemGroup) { QString error = i18n("Could not find reportgroup \"%1\" for report \"%2\".\nPlease report this error to the developer's list: kmymoney-devel@kde.org", reportGroupName, newReport.name()); // write to messagehandler qWarning() << cm << error; // also inform user KMessageBox::error(d->m_reportTabWidget, error, i18n("Critical Error")); // cleanup delete dlg; return; } // do not add TocItemReport to TocItemGroup here, // this is done in loadView d->addReportTab(newReport); } catch (const MyMoneyException &e) { QString error = i18n("Cannot add report, reason: \"%1\"", e.what()); // write to messagehandler qWarning() << cm << error; // also inform user KMessageBox::error(d->m_reportTabWidget, error, i18n("Critical Error")); } } delete dlg; } void KReportsView::slotDelete() { Q_D(KReportsView); auto tab = dynamic_cast(d->m_reportTabWidget->currentWidget()); if (!tab) { // nothing to do return; } MyMoneyReport report = tab->report(); if (! report.id().isEmpty()) { if (KMessageBox::Continue == d->deleteReportDialog(report.name())) { // close the tab and then remove the report so that it is not // generated again during the following loadView() call slotClose(d->m_reportTabWidget->currentIndex()); MyMoneyFileTransaction ft; MyMoneyFile::instance()->removeReport(report); ft.commit(); } } else { KMessageBox::information(this, QString("") + i18n("%1 is a default report, so it cannot be deleted.", report.name()) + QString(""), i18n("Delete Report?")); } } void KReportsView::slotOpenReport(const QString& id) { Q_D(KReportsView); if (id.isEmpty()) { // nothing to do return; } KReportTab* page = 0; // Find the tab which contains the report int index = 1; while (index < d->m_reportTabWidget->count()) { auto current = dynamic_cast(d->m_reportTabWidget->widget(index)); if (current && current->report().id() == id) { page = current; break; } ++index; } // Show the tab, or create a new one, as needed if (page) d->m_reportTabWidget->setCurrentIndex(index); else d->addReportTab(MyMoneyFile::instance()->report(id)); } void KReportsView::slotOpenReport(const MyMoneyReport& report) { Q_D(KReportsView); if (d->m_needLoad) d->init(); qDebug() << Q_FUNC_INFO << " " << report.name(); KReportTab* page = 0; // Find the tab which contains the report indicated by this list item int index = 1; while (index < d->m_reportTabWidget->count()) { auto current = dynamic_cast(d->m_reportTabWidget->widget(index)); if (current && current->report().name() == report.name()) { page = current; break; } ++index; } // Show the tab, or create a new one, as needed if (page) d->m_reportTabWidget->setCurrentIndex(index); else d->addReportTab(report); if (!isVisible()) emit switchViewRequested(View::Reports); } void KReportsView::slotItemDoubleClicked(QTreeWidgetItem* item, int) { Q_D(KReportsView); auto tocItem = dynamic_cast(item); if (tocItem && !tocItem->isReport()) { // toggle the expanded-state for reportgroup-items item->setExpanded(item->isExpanded() ? false : true); // nothing else to do for reportgroup-items return; } TocItemReport* reportTocItem = dynamic_cast(tocItem); MyMoneyReport& report = reportTocItem->getReport(); KReportTab* page = 0; // Find the tab which contains the report indicated by this list item int index = 1; while (index < d->m_reportTabWidget->count()) { auto current = dynamic_cast(d->m_reportTabWidget->widget(index)); if (current) { // If this report has an ID, we'll use the ID to match if (! report.id().isEmpty()) { if (current->report().id() == report.id()) { page = current; break; } } // Otherwise, use the name to match. THIS ASSUMES that no 2 default reports // have the same name...but that would be pretty a boneheaded thing to do. else { if (current->report().name() == report.name()) { page = current; break; } } } ++index; } // Show the tab, or create a new one, as needed if (page) d->m_reportTabWidget->setCurrentIndex(index); else d->addReportTab(report); } void KReportsView::slotToggleChart() { Q_D(KReportsView); if (auto tab = dynamic_cast(d->m_reportTabWidget->currentWidget())) tab->toggleChart(); } void KReportsView::slotCloseCurrent() { Q_D(KReportsView); slotClose(d->m_reportTabWidget->currentIndex()); } void KReportsView::slotClose(int index) { Q_D(KReportsView); if (auto tab = dynamic_cast(d->m_reportTabWidget->widget(index))) { d->m_reportTabWidget->removeTab(index); tab->setReadyToDelete(true); } } void KReportsView::slotCloseAll() { Q_D(KReportsView); if(!d->m_needLoad) { while (true) { if (auto tab = dynamic_cast(d->m_reportTabWidget->widget(1))) { d->m_reportTabWidget->removeTab(1); tab->setReadyToDelete(true); } else break; } } } void KReportsView::slotListContextMenu(const QPoint & p) { Q_D(KReportsView); QTreeWidgetItem *item = d->m_tocTreeWidget->itemAt(p); if (!item) { return; } auto tocItem = dynamic_cast(item); if (tocItem && !tocItem->isReport()) { // currently there is no context menu for reportgroup items return; } QMenu* contextmenu = new QMenu(this); contextmenu->addAction(i18nc("To open a new report", "&Open"), this, SLOT(slotOpenFromList())); contextmenu->addAction(i18nc("Configure a report", "&Configure"), this, SLOT(slotConfigureFromList())); contextmenu->addAction(i18n("&New report"), this, SLOT(slotNewFromList())); // Only add this option if it's a custom report. Default reports cannot be deleted auto reportTocItem = dynamic_cast(tocItem); if (reportTocItem) { MyMoneyReport& report = reportTocItem->getReport(); if (! report.id().isEmpty()) { contextmenu->addAction(i18n("&Delete"), this, SLOT(slotDeleteFromList())); } } contextmenu->popup(d->m_tocTreeWidget->mapToGlobal(p)); } void KReportsView::slotOpenFromList() { Q_D(KReportsView); if (auto tocItem = dynamic_cast(d->m_tocTreeWidget->currentItem())) slotItemDoubleClicked(tocItem, 0); } void KReportsView::slotConfigureFromList() { Q_D(KReportsView); if (auto tocItem = dynamic_cast(d->m_tocTreeWidget->currentItem())) { slotItemDoubleClicked(tocItem, 0); slotConfigure(); } } void KReportsView::slotNewFromList() { Q_D(KReportsView); if (auto tocItem = dynamic_cast(d->m_tocTreeWidget->currentItem())) { slotItemDoubleClicked(tocItem, 0); slotDuplicate(); } } void KReportsView::slotDeleteFromList() { Q_D(KReportsView); if (auto tocItem = dynamic_cast(d->m_tocTreeWidget->currentItem())) { if (auto reportTocItem = dynamic_cast(tocItem)) { MyMoneyReport& report = reportTocItem->getReport(); // If this report does not have an ID, it's a default report and cannot be deleted if (! report.id().isEmpty() && KMessageBox::Continue == d->deleteReportDialog(report.name())) { // check if report's tab is open; start from 1 because 0 is toc tab for (int i = 1; i < d->m_reportTabWidget->count(); ++i) { auto tab = dynamic_cast(d->m_reportTabWidget->widget(i)); if (tab && tab->report().id() == report.id()) { slotClose(i); // if open, close it, so no crash when switching to it break; } } MyMoneyFileTransaction ft; MyMoneyFile::instance()->removeReport(report); ft.commit(); } } } } void KReportsView::slotSelectByObject(const MyMoneyObject& obj, eView::Intent intent) { switch(intent) { case eView::Intent::UpdateActions: updateActions(obj); break; case eView::Intent::OpenObject: slotOpenReport(static_cast(obj)); default: break; } } void KReportsView::slotReportAccountTransactions() { Q_D(KReportsView); // Generate a transaction report that contains transactions for only the // currently selected account. if (!d->m_currentAccount.id().isEmpty()) { MyMoneyReport report( - MyMoneyReport::eAccount, - MyMoneyReport::eQCnumber | MyMoneyReport::eQCpayee | MyMoneyReport::eQCcategory, + eMyMoney::Report::RowType::Account, + eMyMoney::Report::QueryColumn::Number | eMyMoney::Report::QueryColumn::Payee | eMyMoney::Report::QueryColumn::Category, eMyMoney::TransactionFilter::Date::YearToDate, - MyMoneyReport::eDetailAll, + eMyMoney::Report::DetailLevel::All, i18n("%1 YTD Account Transactions", d->m_currentAccount.name()), i18n("Generated Report") ); report.setGroup(i18n("Transactions")); report.addAccount(d->m_currentAccount.id()); emit customActionRequested(View::Reports, eView::Action::SwitchView); slotOpenReport(report); } } // Make sure, that these definitions are only used within this file // this does not seem to be necessary, but when building RPMs the // build option 'final' is used and all CPP files are concatenated. // So it could well be, that in another CPP file these definitions // are also used. #undef VIEW_LEDGER #undef VIEW_SCHEDULE #undef VIEW_WELCOME #undef VIEW_HOME #undef VIEW_REPORTS diff --git a/kmymoney/plugins/views/reports/kreportsview_p.h b/kmymoney/plugins/views/reports/kreportsview_p.h index f78c7beb7..40c6d699d 100644 --- a/kmymoney/plugins/views/reports/kreportsview_p.h +++ b/kmymoney/plugins/views/reports/kreportsview_p.h @@ -1,1453 +1,1453 @@ /*************************************************************************** kreportsview_p.h - description ------------------- begin : Sat Mar 27 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 (C) 2017 Ł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. * * * ***************************************************************************/ #ifndef KREPORTSVIEW_P_H #define KREPORTSVIEW_P_H #include "kreportsview.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef ENABLE_WEBENGINE #include #else #include #endif // ---------------------------------------------------------------------------- // KDE Includes #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "ui_reportcontrol.h" #include "kmymoneyviewbase_p.h" #include "kreportconfigurationfilterdlg.h" #include "mymoneyfile.h" #include "mymoneyreport.h" #include "mymoneyexception.h" #include "kmymoneysettings.h" #include "querytable.h" #include "objectinfotable.h" #include "icons/icons.h" #include #include "tocitem.h" #include "tocitemgroup.h" #include "tocitemreport.h" #include "kreportchartview.h" #include "pivottable.h" #include "reporttable.h" #include "reportcontrolimpl.h" #include "mymoneyenums.h" using namespace reports; using namespace eMyMoney; using namespace Icons; #define VIEW_LEDGER "ledger" #define VIEW_SCHEDULE "schedule" #define VIEW_WELCOME "welcome" #define VIEW_HOME "home" #define VIEW_REPORTS "reports" /** * Helper class for KReportView. * * This is the widget which displays a single report in the TabWidget that comprises this view. * * @author Ace Jones */ class KReportTab: public QWidget { private: #ifdef ENABLE_WEBENGINE QWebEngineView *m_tableView; #else KWebView *m_tableView; #endif reports::KReportChartView *m_chartView; ReportControl *m_control; QVBoxLayout *m_layout; QPrinter *m_currentPrinter; MyMoneyReport m_report; bool m_deleteMe; bool m_chartEnabled; bool m_showingChart; bool m_needReload; bool m_isChartViewValid; bool m_isTableViewValid; QPointer m_table; /** * Users character set encoding. */ QByteArray m_encoding; public: KReportTab(QTabWidget* parent, const MyMoneyReport& report, const KReportsView *eventHandler); ~KReportTab(); const MyMoneyReport& report() const { return m_report; } void print(); void toggleChart(); /** * Updates information about ploted chart in report's data */ void updateDataRange(); void copyToClipboard(); void saveAs(const QString& filename, bool includeCSS = false); void updateReport(); QString createTable(const QString& links = QString()); const ReportControl* control() const { return m_control; } bool isReadyToDelete() const { return m_deleteMe; } void setReadyToDelete(bool f) { m_deleteMe = f; } void modifyReport(const MyMoneyReport& report) { m_report = report; } void showEvent(QShowEvent * event) final override; void loadTab(); }; /** * Helper class for KReportView. * * This is a named list of reports, which will be one section * in the list of default reports * * @author Ace Jones */ class ReportGroup: public QList { private: QString m_name; ///< the title of the group in non-translated form QString m_title; ///< the title of the group in i18n-ed form public: ReportGroup() {} ReportGroup(const QString& name, const QString& title): m_name(name), m_title(title) {} const QString& name() const { return m_name; } const QString& title() const { return m_title; } }; /** * KReportTab Implementation */ KReportTab::KReportTab(QTabWidget* parent, const MyMoneyReport& report, const KReportsView* eventHandler): QWidget(parent), #ifdef ENABLE_WEBENGINE m_tableView(new QWebEngineView(this)), #else m_tableView(new KWebView(this)), #endif m_chartView(new KReportChartView(this)), m_control(new ReportControl(this)), m_layout(new QVBoxLayout(this)), m_currentPrinter(nullptr), m_report(report), m_deleteMe(false), m_chartEnabled(false), m_showingChart(report.isChartByDefault()), m_needReload(true), m_isChartViewValid(false), m_isTableViewValid(false), m_table(0) { m_layout->setSpacing(6); m_tableView->setPage(new MyQWebEnginePage(m_tableView)); m_tableView->setZoomFactor(KMyMoneySettings::zoomFactor()); //set button icons m_control->ui->buttonChart->setIcon(Icons::get(Icon::OfficeChartLine)); m_control->ui->buttonClose->setIcon(Icons::get(Icon::DocumentClose)); m_control->ui->buttonConfigure->setIcon(Icons::get(Icon::Configure)); m_control->ui->buttonCopy->setIcon(Icons::get(Icon::EditCopy)); m_control->ui->buttonDelete->setIcon(Icons::get(Icon::EditDelete)); m_control->ui->buttonExport->setIcon(Icons::get(Icon::DocumentExport)); m_control->ui->buttonNew->setIcon(Icons::get(Icon::DocumentNew)); m_chartView->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); m_chartView->hide(); m_tableView->hide(); m_layout->addWidget(m_control); m_layout->addWidget(m_tableView); m_layout->addWidget(m_chartView); m_layout->setStretch(1, 10); m_layout->setStretch(2, 10); connect(m_control->ui->buttonChart, &QAbstractButton::clicked, eventHandler, &KReportsView::slotToggleChart); connect(m_control->ui->buttonConfigure, &QAbstractButton::clicked, eventHandler, &KReportsView::slotConfigure); connect(m_control->ui->buttonNew, &QAbstractButton::clicked, eventHandler, &KReportsView::slotDuplicate); connect(m_control->ui->buttonCopy, &QAbstractButton::clicked, eventHandler, &KReportsView::slotCopyView); connect(m_control->ui->buttonExport, &QAbstractButton::clicked, eventHandler, &KReportsView::slotSaveView); connect(m_control->ui->buttonDelete, &QAbstractButton::clicked, eventHandler, &KReportsView::slotDelete); connect(m_control->ui->buttonClose, &QAbstractButton::clicked, eventHandler, &KReportsView::slotCloseCurrent); #ifdef ENABLE_WEBENGINE connect(m_tableView->page(), &QWebEnginePage::urlChanged, eventHandler, &KReportsView::slotOpenUrl); #else m_tableView->page()->setLinkDelegationPolicy(QWebPage::DelegateAllLinks); connect(m_tableView->page(), &KWebPage::linkClicked, eventHandler, &KReportsView::slotOpenUrl); #endif // if this is a default report, then you can't delete it! if (report.id().isEmpty()) m_control->ui->buttonDelete->setEnabled(false); int tabNr = parent->addTab(this, Icons::get(Icon::Spreadsheet), report.name()); parent->setTabEnabled(tabNr, true); parent->setCurrentIndex(tabNr); // get users character set encoding m_encoding = QTextCodec::codecForLocale()->name(); } KReportTab::~KReportTab() { delete m_table; } void KReportTab::print() { if (m_tableView) { m_currentPrinter = new QPrinter(); QPointer dialog = new QPrintDialog(m_currentPrinter, this); dialog->setWindowTitle(QString()); if (dialog->exec() != QDialog::Accepted) { delete m_currentPrinter; m_currentPrinter = nullptr; return; } #ifdef ENABLE_WEBENGINE m_tableView->page()->print(m_currentPrinter, [=] (bool) {delete m_currentPrinter; m_currentPrinter = nullptr;}); #else m_tableView->print(m_currentPrinter); #endif } } void KReportTab::copyToClipboard() { QMimeData* pMimeData = new QMimeData(); pMimeData->setHtml(m_table->renderReport(QLatin1String("html"), m_encoding, m_report.name(), true)); QApplication::clipboard()->setMimeData(pMimeData); } void KReportTab::saveAs(const QString& filename, bool includeCSS) { QFile file(filename); if (file.open(QIODevice::WriteOnly)) { if (QFileInfo(filename).suffix().toLower() == QLatin1String("csv")) { QTextStream(&file) << m_table->renderReport(QLatin1String("csv"), m_encoding, QString()); } else { QString table = m_table->renderReport(QLatin1String("html"), m_encoding, m_report.name(), includeCSS); QTextStream stream(&file); stream << table; } file.close(); } } void KReportTab::loadTab() { m_needReload = true; if (isVisible()) { m_needReload = false; updateReport(); } } void KReportTab::showEvent(QShowEvent * event) { if (m_needReload) { m_needReload = false; updateReport(); } QWidget::showEvent(event); } void KReportTab::updateReport() { m_isChartViewValid = false; m_isTableViewValid = false; // reload the report from the engine. It might have // been changed by the user try { // Don't try to reload default reports from the engine if (!m_report.id().isEmpty()) m_report = MyMoneyFile::instance()->report(m_report.id()); } catch (const MyMoneyException &) { } delete m_table; m_table = 0; - if (m_report.reportType() == MyMoneyReport::ePivotTable) { + if (m_report.reportType() == eMyMoney::Report::ReportType::PivotTable) { m_table = new PivotTable(m_report); m_chartEnabled = true; - } else if (m_report.reportType() == MyMoneyReport::eQueryTable) { + } else if (m_report.reportType() == eMyMoney::Report::ReportType::QueryTable) { m_table = new QueryTable(m_report); m_chartEnabled = false; - } else if (m_report.reportType() == MyMoneyReport::eInfoTable) { + } else if (m_report.reportType() == eMyMoney::Report::ReportType::InfoTable) { m_table = new ObjectInfoTable(m_report); m_chartEnabled = false; } m_control->ui->buttonChart->setEnabled(m_chartEnabled); m_showingChart = !m_showingChart; toggleChart(); } void KReportTab::toggleChart() { // for now it will just SHOW the chart. In the future it actually has to toggle it. if (m_showingChart) { if (!m_isTableViewValid) { m_tableView->setHtml(m_table->renderReport(QLatin1String("html"), m_encoding, m_report.name()), QUrl("file://")); // workaround for access permission to css file } m_isTableViewValid = true; m_tableView->show(); m_chartView->hide(); m_control->ui->buttonChart->setText(i18n("Chart")); m_control->ui->buttonChart->setToolTip(i18n("Show the chart version of this report")); m_control->ui->buttonChart->setIcon(Icons::get(Icon::OfficeChartLine)); } else { if (!m_isChartViewValid) m_table->drawChart(*m_chartView); m_isChartViewValid = true; m_tableView->hide(); m_chartView->show(); m_control->ui->buttonChart->setText(i18n("Report")); m_control->ui->buttonChart->setToolTip(i18n("Show the report version of this chart")); m_control->ui->buttonChart->setIcon(Icons::get(Icon::ViewFinancialList)); } m_showingChart = ! m_showingChart; } void KReportTab::updateDataRange() { QList grids = m_chartView->coordinatePlane()->gridDimensionsList(); // get dimmensions of ploted graph if (grids.isEmpty()) return; QChar separator = locale().groupSeparator(); QChar decimalPoint = locale().decimalPoint(); int precision = m_report.yLabelsPrecision(); QList> dims; // create list of dimension values in string and qreal // get qreal values dims.append(qMakePair(QString(), grids.at(1).start)); dims.append(qMakePair(QString(), grids.at(1).end)); dims.append(qMakePair(QString(), grids.at(1).stepWidth)); dims.append(qMakePair(QString(), grids.at(1).subStepWidth)); // convert qreal values to string variables for (int i = 0; i < 4; ++i) { if (i > 2) ++precision; if (precision == 0) dims[i].first = locale().toString(qRound(dims.at(i).second)); else dims[i].first = locale().toString(dims.at(i).second, 'f', precision).remove(separator).remove(QRegularExpression("0+$")).remove(QRegularExpression("\\" + decimalPoint + "$")); } // save string variables in report's data m_report.setDataRangeStart(dims.at(0).first); m_report.setDataRangeEnd(dims.at(1).first); m_report.setDataMajorTick(dims.at(2).first); m_report.setDataMinorTick(dims.at(3).first); } class KReportsViewPrivate : public KMyMoneyViewBasePrivate { Q_DECLARE_PUBLIC(KReportsView) public: explicit KReportsViewPrivate(KReportsView *qq): q_ptr(qq), m_needLoad(true), m_reportListView(nullptr), m_reportTabWidget(nullptr), m_listTab(nullptr), m_listTabLayout(nullptr), m_tocTreeWidget(nullptr), m_columnsAlreadyAdjusted(false) { } ~KReportsViewPrivate() { } void init() { Q_Q(KReportsView); m_needLoad = false; auto vbox = new QVBoxLayout(q); q->setLayout(vbox); vbox->setSpacing(6); vbox->setMargin(0); // build reports toc setColumnsAlreadyAdjusted(false); m_reportTabWidget = new QTabWidget(q); vbox->addWidget(m_reportTabWidget); m_reportTabWidget->setTabsClosable(true); m_listTab = new QWidget(m_reportTabWidget); m_listTabLayout = new QVBoxLayout(m_listTab); m_listTabLayout->setSpacing(6); m_tocTreeWidget = new QTreeWidget(m_listTab); // report-group items have only 1 column (name of group), // report items have 2 columns (report name and comment) m_tocTreeWidget->setColumnCount(2); // headers QStringList headers; headers << i18n("Reports") << i18n("Comment"); m_tocTreeWidget->setHeaderLabels(headers); m_tocTreeWidget->setAlternatingRowColors(true); m_tocTreeWidget->setSortingEnabled(true); m_tocTreeWidget->sortByColumn(0, Qt::AscendingOrder); // for report group items: // doubleclick toggles the expand-state, // so avoid any further action in case of doubleclick // (see slotItemDoubleClicked) m_tocTreeWidget->setExpandsOnDoubleClick(false); m_tocTreeWidget->setContextMenuPolicy(Qt::CustomContextMenu); m_tocTreeWidget->setSelectionMode(QAbstractItemView::SingleSelection); m_listTabLayout->addWidget(m_tocTreeWidget); m_reportTabWidget->addTab(m_listTab, i18n("Reports")); q->connect(m_reportTabWidget, &QTabWidget::tabCloseRequested, q, &KReportsView::slotClose); q->connect(m_tocTreeWidget, &QTreeWidget::itemActivated, q, &KReportsView::slotItemDoubleClicked); q->connect(m_tocTreeWidget, &QWidget::customContextMenuRequested, q, &KReportsView::slotListContextMenu); q->connect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, q, &KReportsView::refresh); } void restoreTocExpandState(QMap& expandStates) { for (auto i = 0; i < m_tocTreeWidget->topLevelItemCount(); ++i) { QTreeWidgetItem* item = m_tocTreeWidget->topLevelItem(i); if (item) { QString itemLabel = item->text(0); if (expandStates.contains(itemLabel)) { item->setExpanded(expandStates[itemLabel]); } else { item->setExpanded(false); } } } } /** * Display a dialog to confirm report deletion */ int deleteReportDialog(const QString &reportName) { Q_Q(KReportsView); return KMessageBox::warningContinueCancel(q, i18n("Are you sure you want to delete report %1? There is no way to recover it.", reportName), i18n("Delete Report?")); } void addReportTab(const MyMoneyReport& report) { Q_Q(KReportsView); new KReportTab(m_reportTabWidget, report, q); } void loadView() { // remember the id of the current selected item QTreeWidgetItem* item = m_tocTreeWidget->currentItem(); QString selectedItem = (item) ? item->text(0) : QString(); // save expand states of all top-level items QMap expandStates; for (int i = 0; i < m_tocTreeWidget->topLevelItemCount(); ++i) { item = m_tocTreeWidget->topLevelItem(i); if (item) { QString itemLabel = item->text(0); if (item->isExpanded()) { expandStates.insert(itemLabel, true); } else { expandStates.insert(itemLabel, false); } } } // find the item visible on top QTreeWidgetItem* visibleTopItem = m_tocTreeWidget->itemAt(0, 0); // text of column 0 identifies the item visible on top QString visibleTopItemText; bool visibleTopItemFound = true; if (visibleTopItem == NULL) { visibleTopItemFound = false; } else { // this assumes, that all item-texts in column 0 are unique, // no matter, whether the item is a report- or a group-item visibleTopItemText = visibleTopItem->text(0); } // turn off updates to avoid flickering during reload //m_reportListView->setUpdatesEnabled(false); // // Rebuild the list page // m_tocTreeWidget->clear(); // Default Reports QList defaultreports; defaultReports(defaultreports); QList::const_iterator it_group = defaultreports.constBegin(); // the item to be set as current item QTreeWidgetItem* currentItem = 0L; // group number, this will be used as sort key for reportgroup items // we have: // 1st some default groups // 2nd a chart group // 3rd maybe a favorite group // 4th maybe an orphan group (for old reports) int defaultGroupNo = 1; int chartGroupNo = defaultreports.size() + 1; // group for diagrams QString groupName = I18N_NOOP("Charts"); TocItemGroup* chartTocItemGroup = new TocItemGroup(m_tocTreeWidget, chartGroupNo, i18n(groupName.toLatin1().data())); m_allTocItemGroups.insert(groupName, chartTocItemGroup); while (it_group != defaultreports.constEnd()) { groupName = (*it_group).name(); TocItemGroup* defaultTocItemGroup = new TocItemGroup(m_tocTreeWidget, defaultGroupNo++, i18n(groupName.toLatin1().data())); m_allTocItemGroups.insert(groupName, defaultTocItemGroup); if (groupName == selectedItem) { currentItem = defaultTocItemGroup; } QList::const_iterator it_report = (*it_group).begin(); while (it_report != (*it_group).end()) { MyMoneyReport report = *it_report; report.setGroup(groupName); TocItemReport* reportTocItemReport = new TocItemReport(defaultTocItemGroup, report); if (report.name() == selectedItem) { currentItem = reportTocItemReport; } // ALSO place it into the Charts list if it's displayed as a chart by default if (report.isChartByDefault()) { new TocItemReport(chartTocItemGroup, report); } ++it_report; } ++it_group; } // group for custom (favorite) reports int favoriteGroupNo = chartGroupNo + 1; groupName = I18N_NOOP("Favorite Reports"); TocItemGroup* favoriteTocItemGroup = new TocItemGroup(m_tocTreeWidget, favoriteGroupNo, i18n(groupName.toLatin1().data())); m_allTocItemGroups.insert(groupName, favoriteTocItemGroup); TocItemGroup* orphanTocItemGroup = 0; QList customreports = MyMoneyFile::instance()->reportList(); QList::const_iterator it_report = customreports.constBegin(); while (it_report != customreports.constEnd()) { MyMoneyReport report = *it_report; groupName = (*it_report).group(); // If this report is in a known group, place it there // KReportGroupListItem* groupnode = groupitems[(*it_report).group()]; TocItemGroup* groupNode = m_allTocItemGroups[groupName]; if (groupNode) { new TocItemReport(groupNode, report); } else { // otherwise, place it in the orphanage if (!orphanTocItemGroup) { // group for orphaned reports int orphanGroupNo = favoriteGroupNo + 1; groupName = I18N_NOOP("Old Customized Reports"); orphanTocItemGroup = new TocItemGroup(m_tocTreeWidget, orphanGroupNo, i18n(groupName.toLatin1().data())); m_allTocItemGroups.insert(groupName, orphanTocItemGroup); } new TocItemReport(orphanTocItemGroup, report); } // ALSO place it into the Favorites list if it's a favorite if ((*it_report).isFavorite()) { new TocItemReport(favoriteTocItemGroup, report); } // ALSO place it into the Charts list if it's displayed as a chart by default if ((*it_report).isChartByDefault()) { new TocItemReport(chartTocItemGroup, report); } ++it_report; } // // Go through the tabs to set their update flag or delete them if needed // int index = 1; while (index < m_reportTabWidget->count()) { // TODO: Find some way of detecting the file is closed and kill these tabs!! if (auto tab = dynamic_cast(m_reportTabWidget->widget(index))) { if (tab->isReadyToDelete() /* || ! reports.count() */) { delete tab; --index; } else { tab->loadTab(); } } ++index; } if (visibleTopItemFound) { // try to find the visibleTopItem that we had at the start of this method // intentionally not using 'Qt::MatchCaseSensitive' here // to avoid 'item not found' if someone corrected a typo only QList visibleTopItemList = m_tocTreeWidget->findItems(visibleTopItemText, Qt::MatchFixedString | Qt::MatchRecursive); if (visibleTopItemList.isEmpty()) { // the item could not be found, it was deleted or renamed visibleTopItemFound = false; } else { visibleTopItem = visibleTopItemList.at(0); if (visibleTopItem == NULL) { visibleTopItemFound = false; } } } // adjust column widths, // but only the first time when the view is loaded, // maybe the user sets other column widths later, // so don't disturb him if (columnsAlreadyAdjusted()) { // restore expand states of all top-level items restoreTocExpandState(expandStates); // restore current item m_tocTreeWidget->setCurrentItem(currentItem); // try to scroll to the item visible on top // when this method started if (visibleTopItemFound) { m_tocTreeWidget->scrollToItem(visibleTopItem); } else { m_tocTreeWidget->scrollToTop(); } return; } // avoid flickering m_tocTreeWidget->setUpdatesEnabled(false); // expand all top-level items m_tocTreeWidget->expandAll(); // resize columns m_tocTreeWidget->resizeColumnToContents(0); m_tocTreeWidget->resizeColumnToContents(1); // restore expand states of all top-level items restoreTocExpandState(expandStates); // restore current item m_tocTreeWidget->setCurrentItem(currentItem); // try to scroll to the item visible on top // when this method started if (visibleTopItemFound) { m_tocTreeWidget->scrollToItem(visibleTopItem); } else { m_tocTreeWidget->scrollToTop(); } setColumnsAlreadyAdjusted(true); m_tocTreeWidget->setUpdatesEnabled(true); } void defaultReports(QList& groups) { { ReportGroup list("Income and Expenses", i18n("Income and Expenses")); list.push_back(MyMoneyReport( - MyMoneyReport::eExpenseIncome, - MyMoneyReport::eMonths, + eMyMoney::Report::RowType::ExpenseIncome, + static_cast(eMyMoney::Report::ColumnType::Months), TransactionFilter::Date::CurrentMonth, - MyMoneyReport::eDetailAll, + eMyMoney::Report::DetailLevel::All, i18n("Income and Expenses This Month"), i18n("Default Report") )); list.push_back(MyMoneyReport( - MyMoneyReport::eExpenseIncome, - MyMoneyReport::eMonths, + eMyMoney::Report::RowType::ExpenseIncome, + static_cast(eMyMoney::Report::ColumnType::Months), TransactionFilter::Date::YearToDate, - MyMoneyReport::eDetailAll, + eMyMoney::Report::DetailLevel::All, i18n("Income and Expenses This Year"), i18n("Default Report") )); list.push_back(MyMoneyReport( - MyMoneyReport::eExpenseIncome, - MyMoneyReport::eYears, + eMyMoney::Report::RowType::ExpenseIncome, + static_cast(eMyMoney::Report::ColumnType::Years), TransactionFilter::Date::All, - MyMoneyReport::eDetailAll, + eMyMoney::Report::DetailLevel::All, i18n("Income and Expenses By Year"), i18n("Default Report") )); list.push_back(MyMoneyReport( - MyMoneyReport::eExpenseIncome, - MyMoneyReport::eMonths, + eMyMoney::Report::RowType::ExpenseIncome, + static_cast(eMyMoney::Report::ColumnType::Months), TransactionFilter::Date::Last12Months, - MyMoneyReport::eDetailTop, + eMyMoney::Report::DetailLevel::Top, i18n("Income and Expenses Graph"), i18n("Default Report") )); list.back().setChartByDefault(true); - list.back().setChartType(MyMoneyReport::eChartLine); + list.back().setChartType(eMyMoney::Report::ChartType::Line); list.back().setChartDataLabels(false); list.push_back(MyMoneyReport( - MyMoneyReport::eExpenseIncome, - MyMoneyReport::eMonths, + eMyMoney::Report::RowType::ExpenseIncome, + static_cast(eMyMoney::Report::ColumnType::Months), TransactionFilter::Date::YearToDate, - MyMoneyReport::eDetailGroup, + eMyMoney::Report::DetailLevel::Group, i18n("Income and Expenses Pie Chart"), i18n("Default Report") )); list.back().setChartByDefault(true); - list.back().setChartType(MyMoneyReport::eChartPie); + list.back().setChartType(eMyMoney::Report::ChartType::Pie); list.back().setShowingRowTotals(false); groups.push_back(list); } { ReportGroup list("Net Worth", i18n("Net Worth")); list.push_back(MyMoneyReport( - MyMoneyReport::eAssetLiability, - MyMoneyReport::eMonths, + eMyMoney::Report::RowType::AssetLiability, + static_cast(eMyMoney::Report::ColumnType::Months), TransactionFilter::Date::YearToDate, - MyMoneyReport::eDetailTop, + eMyMoney::Report::DetailLevel::Top, i18n("Net Worth By Month"), i18n("Default Report") )); list.push_back(MyMoneyReport( - MyMoneyReport::eAssetLiability, - MyMoneyReport::eMonths, + eMyMoney::Report::RowType::AssetLiability, + static_cast(eMyMoney::Report::ColumnType::Months), TransactionFilter::Date::Today, - MyMoneyReport::eDetailTop, + eMyMoney::Report::DetailLevel::Top, i18n("Net Worth Today"), i18n("Default Report") )); list.push_back(MyMoneyReport( - MyMoneyReport::eAssetLiability, - MyMoneyReport::eYears, + eMyMoney::Report::RowType::AssetLiability, + static_cast(eMyMoney::Report::ColumnType::Years), TransactionFilter::Date::All, - MyMoneyReport::eDetailTop, + eMyMoney::Report::DetailLevel::Top, i18n("Net Worth By Year"), i18n("Default Report") )); list.push_back(MyMoneyReport( - MyMoneyReport::eAssetLiability, - MyMoneyReport::eMonths, + eMyMoney::Report::RowType::AssetLiability, + static_cast(eMyMoney::Report::ColumnType::Months), TransactionFilter::Date::Next7Days, - MyMoneyReport::eDetailTop, + eMyMoney::Report::DetailLevel::Top, i18n("7-day Cash Flow Forecast"), i18n("Default Report") )); list.back().setIncludingSchedules(true); list.back().setColumnsAreDays(true); list.push_back(MyMoneyReport( - MyMoneyReport::eAssetLiability, - MyMoneyReport::eMonths, + eMyMoney::Report::RowType::AssetLiability, + static_cast(eMyMoney::Report::ColumnType::Months), TransactionFilter::Date::Last12Months, - MyMoneyReport::eDetailTotal, + eMyMoney::Report::DetailLevel::Total, i18n("Net Worth Graph"), i18n("Default Report") )); list.back().setChartByDefault(true); list.back().setChartCHGridLines(false); list.back().setChartSVGridLines(false); - list.back().setChartType(MyMoneyReport::eChartLine); + list.back().setChartType(eMyMoney::Report::ChartType::Line); list.push_back(MyMoneyReport( - MyMoneyReport::eInstitution, - MyMoneyReport::eQCnone, + eMyMoney::Report::RowType::Institution, + eMyMoney::Report::QueryColumn::None, TransactionFilter::Date::YearToDate, - MyMoneyReport::eDetailTop, + eMyMoney::Report::DetailLevel::Top, i18n("Account Balances by Institution"), i18n("Default Report") )); list.push_back(MyMoneyReport( - MyMoneyReport::eAccountType, - MyMoneyReport::eQCnone, + eMyMoney::Report::RowType::AccountType, + eMyMoney::Report::QueryColumn::None, TransactionFilter::Date::YearToDate, - MyMoneyReport::eDetailTop, + eMyMoney::Report::DetailLevel::Top, i18n("Account Balances by Type"), i18n("Default Report") )); groups.push_back(list); } { ReportGroup list("Transactions", i18n("Transactions")); list.push_back(MyMoneyReport( - MyMoneyReport::eAccount, - MyMoneyReport::eQCnumber | MyMoneyReport::eQCpayee | MyMoneyReport::eQCcategory | MyMoneyReport::eQCtag | MyMoneyReport::eQCbalance, + eMyMoney::Report::RowType::Account, + eMyMoney::Report::QueryColumn::Number | eMyMoney::Report::QueryColumn::Payee | eMyMoney::Report::QueryColumn::Category | eMyMoney::Report::QueryColumn::Tag | eMyMoney::Report::QueryColumn::Balance, TransactionFilter::Date::YearToDate, - MyMoneyReport::eDetailAll, + eMyMoney::Report::DetailLevel::All, i18n("Transactions by Account"), i18n("Default Report") )); //list.back().setConvertCurrency(false); list.push_back(MyMoneyReport( - MyMoneyReport::eCategory, - MyMoneyReport::eQCnumber | MyMoneyReport::eQCpayee | MyMoneyReport::eQCaccount | MyMoneyReport::eQCtag, + eMyMoney::Report::RowType::Category, + eMyMoney::Report::QueryColumn::Number | eMyMoney::Report::QueryColumn::Payee | eMyMoney::Report::QueryColumn::Account | eMyMoney::Report::QueryColumn::Tag, TransactionFilter::Date::YearToDate, - MyMoneyReport::eDetailAll, + eMyMoney::Report::DetailLevel::All, i18n("Transactions by Category"), i18n("Default Report") )); list.push_back(MyMoneyReport( - MyMoneyReport::ePayee, - MyMoneyReport::eQCnumber | MyMoneyReport::eQCcategory | MyMoneyReport::eQCtag, + eMyMoney::Report::RowType::Payee, + eMyMoney::Report::QueryColumn::Number | eMyMoney::Report::QueryColumn::Category | eMyMoney::Report::QueryColumn::Tag, TransactionFilter::Date::YearToDate, - MyMoneyReport::eDetailAll, + eMyMoney::Report::DetailLevel::All, i18n("Transactions by Payee"), i18n("Default Report") )); list.push_back(MyMoneyReport( - MyMoneyReport::eTag, - MyMoneyReport::eQCnumber | MyMoneyReport::eQCcategory, + eMyMoney::Report::RowType::Tag, + eMyMoney::Report::QueryColumn::Number | eMyMoney::Report::QueryColumn::Category, TransactionFilter::Date::YearToDate, - MyMoneyReport::eDetailAll, + eMyMoney::Report::DetailLevel::All, i18n("Transactions by Tag"), i18n("Default Report") )); list.push_back(MyMoneyReport( - MyMoneyReport::eMonth, - MyMoneyReport::eQCnumber | MyMoneyReport::eQCpayee | MyMoneyReport::eQCcategory | MyMoneyReport::eQCtag, + eMyMoney::Report::RowType::Month, + eMyMoney::Report::QueryColumn::Number | eMyMoney::Report::QueryColumn::Payee | eMyMoney::Report::QueryColumn::Category | eMyMoney::Report::QueryColumn::Tag, TransactionFilter::Date::YearToDate, - MyMoneyReport::eDetailAll, + eMyMoney::Report::DetailLevel::All, i18n("Transactions by Month"), i18n("Default Report") )); list.push_back(MyMoneyReport( - MyMoneyReport::eWeek, - MyMoneyReport::eQCnumber | MyMoneyReport::eQCpayee | MyMoneyReport::eQCcategory | MyMoneyReport::eQCtag, + eMyMoney::Report::RowType::Week, + eMyMoney::Report::QueryColumn::Number | eMyMoney::Report::QueryColumn::Payee | eMyMoney::Report::QueryColumn::Category | eMyMoney::Report::QueryColumn::Tag, TransactionFilter::Date::YearToDate, - MyMoneyReport::eDetailAll, + eMyMoney::Report::DetailLevel::All, i18n("Transactions by Week"), i18n("Default Report") )); list.push_back(MyMoneyReport( - MyMoneyReport::eAccount, - MyMoneyReport::eQCloan, + eMyMoney::Report::RowType::Account, + eMyMoney::Report::QueryColumn::Loan, TransactionFilter::Date::All, - MyMoneyReport::eDetailAll, + eMyMoney::Report::DetailLevel::All, i18n("Loan Transactions"), i18n("Default Report") )); list.back().setLoansOnly(true); list.push_back(MyMoneyReport( - MyMoneyReport::eAccountReconcile, - MyMoneyReport::eQCnumber | MyMoneyReport::eQCpayee | MyMoneyReport::eQCcategory | MyMoneyReport::eQCbalance, + eMyMoney::Report::RowType::AccountReconcile, + eMyMoney::Report::QueryColumn::Number | eMyMoney::Report::QueryColumn::Payee | eMyMoney::Report::QueryColumn::Category | eMyMoney::Report::QueryColumn::Balance, TransactionFilter::Date::Last3Months, - MyMoneyReport::eDetailAll, + eMyMoney::Report::DetailLevel::All, i18n("Transactions by Reconciliation Status"), i18n("Default Report") )); groups.push_back(list); } { ReportGroup list("CashFlow", i18n("Cash Flow")); list.push_back(MyMoneyReport( - MyMoneyReport::eCashFlow, - MyMoneyReport::eQCnumber | MyMoneyReport::eQCpayee | MyMoneyReport::eQCaccount, + eMyMoney::Report::RowType::CashFlow, + eMyMoney::Report::QueryColumn::Number | eMyMoney::Report::QueryColumn::Payee | eMyMoney::Report::QueryColumn::Account, TransactionFilter::Date::YearToDate, - MyMoneyReport::eDetailAll, + eMyMoney::Report::DetailLevel::All, i18n("Cash Flow Transactions This Month"), i18n("Default Report") )); groups.push_back(list); } { ReportGroup list("Investments", i18n("Investments")); list.push_back(MyMoneyReport( - MyMoneyReport::eTopAccount, - MyMoneyReport::eQCaction | MyMoneyReport::eQCshares | MyMoneyReport::eQCprice, + eMyMoney::Report::RowType::TopAccount, + eMyMoney::Report::QueryColumn::Action | eMyMoney::Report::QueryColumn::Shares | eMyMoney::Report::QueryColumn::Price, TransactionFilter::Date::YearToDate, - MyMoneyReport::eDetailAll, + eMyMoney::Report::DetailLevel::All, i18n("Investment Transactions"), i18n("Default Report") )); list.back().setInvestmentsOnly(true); list.push_back(MyMoneyReport( - MyMoneyReport::eAccountByTopAccount, - MyMoneyReport::eQCshares | MyMoneyReport::eQCprice, + eMyMoney::Report::RowType::AccountByTopAccount, + eMyMoney::Report::QueryColumn::Shares | eMyMoney::Report::QueryColumn::Price, TransactionFilter::Date::YearToDate, - MyMoneyReport::eDetailAll, + eMyMoney::Report::DetailLevel::All, i18n("Investment Holdings by Account"), i18n("Default Report") )); list.back().setInvestmentsOnly(true); list.push_back(MyMoneyReport( - MyMoneyReport::eEquityType, - MyMoneyReport::eQCshares | MyMoneyReport::eQCprice, + eMyMoney::Report::RowType::EquityType, + eMyMoney::Report::QueryColumn::Shares | eMyMoney::Report::QueryColumn::Price, TransactionFilter::Date::YearToDate, - MyMoneyReport::eDetailAll, + eMyMoney::Report::DetailLevel::All, i18n("Investment Holdings by Type"), i18n("Default Report") )); list.back().setInvestmentsOnly(true); list.push_back(MyMoneyReport( - MyMoneyReport::eAccountByTopAccount, - MyMoneyReport::eQCperformance, + eMyMoney::Report::RowType::AccountByTopAccount, + eMyMoney::Report::QueryColumn::Performance, TransactionFilter::Date::YearToDate, - MyMoneyReport::eDetailAll, + eMyMoney::Report::DetailLevel::All, i18n("Investment Performance by Account"), i18n("Default Report") )); list.back().setInvestmentsOnly(true); list.push_back(MyMoneyReport( - MyMoneyReport::eEquityType, - MyMoneyReport::eQCperformance, + eMyMoney::Report::RowType::EquityType, + eMyMoney::Report::QueryColumn::Performance, TransactionFilter::Date::YearToDate, - MyMoneyReport::eDetailAll, + eMyMoney::Report::DetailLevel::All, i18n("Investment Performance by Type"), i18n("Default Report") )); list.back().setInvestmentsOnly(true); list.push_back(MyMoneyReport( - MyMoneyReport::eAccountByTopAccount, - MyMoneyReport::eQCcapitalgain, + eMyMoney::Report::RowType::AccountByTopAccount, + eMyMoney::Report::QueryColumn::CapitalGain, TransactionFilter::Date::YearToDate, - MyMoneyReport::eDetailAll, + eMyMoney::Report::DetailLevel::All, i18n("Investment Capital Gains by Account"), i18n("Default Report") )); list.back().setInvestmentsOnly(true); list.push_back(MyMoneyReport( - MyMoneyReport::eEquityType, - MyMoneyReport::eQCcapitalgain, + eMyMoney::Report::RowType::EquityType, + eMyMoney::Report::QueryColumn::CapitalGain, TransactionFilter::Date::YearToDate, - MyMoneyReport::eDetailAll, + eMyMoney::Report::DetailLevel::All, i18n("Investment Capital Gains by Type"), i18n("Default Report") )); list.back().setInvestmentsOnly(true); list.push_back(MyMoneyReport( - MyMoneyReport::eAssetLiability, - MyMoneyReport::eMonths, + eMyMoney::Report::RowType::AssetLiability, + static_cast(eMyMoney::Report::ColumnType::Months), TransactionFilter::Date::Today, - MyMoneyReport::eDetailAll, + eMyMoney::Report::DetailLevel::All, i18n("Investment Holdings Pie"), i18n("Default Report") )); list.back().setChartByDefault(true); list.back().setChartCHGridLines(false); list.back().setChartSVGridLines(false); - list.back().setChartType(MyMoneyReport::eChartPie); + list.back().setChartType(eMyMoney::Report::ChartType::Pie); list.back().setInvestmentsOnly(true); list.push_back(MyMoneyReport( - MyMoneyReport::eAssetLiability, - MyMoneyReport::eMonths, + eMyMoney::Report::RowType::AssetLiability, + static_cast(eMyMoney::Report::ColumnType::Months), TransactionFilter::Date::Last12Months, - MyMoneyReport::eDetailAll, + eMyMoney::Report::DetailLevel::All, i18n("Investment Worth Graph"), i18n("Default Report") )); list.back().setChartByDefault(true); list.back().setChartCHGridLines(false); list.back().setChartSVGridLines(false); - list.back().setChartType(MyMoneyReport::eChartLine); + list.back().setChartType(eMyMoney::Report::ChartType::Line); list.back().setColumnsAreDays(true); list.back().setInvestmentsOnly(true); list.push_back(MyMoneyReport( - MyMoneyReport::eAssetLiability, - MyMoneyReport::eMonths, + eMyMoney::Report::RowType::AssetLiability, + static_cast(eMyMoney::Report::ColumnType::Months), TransactionFilter::Date::Last12Months, - MyMoneyReport::eDetailAll, + eMyMoney::Report::DetailLevel::All, i18n("Investment Price Graph"), i18n("Default Report") )); list.back().setChartByDefault(true); list.back().setChartCHGridLines(false); list.back().setChartSVGridLines(false); - list.back().setChartType(MyMoneyReport::eChartLine); + list.back().setChartType(eMyMoney::Report::ChartType::Line); list.back().setColumnsAreDays(true); list.back().setInvestmentsOnly(true); list.back().setIncludingBudgetActuals(false); list.back().setIncludingPrice(true); list.back().setConvertCurrency(true); list.back().setChartDataLabels(false); list.back().setSkipZero(true); list.back().setShowingColumnTotals(false); list.back().setShowingRowTotals(false); list.push_back(MyMoneyReport( - MyMoneyReport::eAssetLiability, - MyMoneyReport::eMonths, + eMyMoney::Report::RowType::AssetLiability, + static_cast(eMyMoney::Report::ColumnType::Months), TransactionFilter::Date::Last12Months, - MyMoneyReport::eDetailAll, + eMyMoney::Report::DetailLevel::All, i18n("Investment Moving Average Price Graph"), i18n("Default Report") )); list.back().setChartByDefault(true); list.back().setChartCHGridLines(false); list.back().setChartSVGridLines(false); - list.back().setChartType(MyMoneyReport::eChartLine); + list.back().setChartType(eMyMoney::Report::ChartType::Line); list.back().setColumnsAreDays(true); list.back().setInvestmentsOnly(true); list.back().setIncludingBudgetActuals(false); list.back().setIncludingAveragePrice(true); list.back().setMovingAverageDays(10); list.back().setConvertCurrency(true); list.back().setChartDataLabels(false); list.back().setShowingColumnTotals(false); list.back().setShowingRowTotals(false); list.push_back(MyMoneyReport( - MyMoneyReport::eAssetLiability, - MyMoneyReport::eMonths, + eMyMoney::Report::RowType::AssetLiability, + static_cast(eMyMoney::Report::ColumnType::Months), TransactionFilter::Date::Last30Days, - MyMoneyReport::eDetailAll, + eMyMoney::Report::DetailLevel::All, i18n("Investment Moving Average"), i18n("Default Report") )); list.back().setChartCHGridLines(false); list.back().setChartSVGridLines(false); - list.back().setChartType(MyMoneyReport::eChartLine); + list.back().setChartType(eMyMoney::Report::ChartType::Line); list.back().setColumnsAreDays(true); list.back().setInvestmentsOnly(true); list.back().setIncludingBudgetActuals(false); list.back().setIncludingMovingAverage(true); list.back().setMovingAverageDays(10); list.push_back(MyMoneyReport( - MyMoneyReport::eAssetLiability, - MyMoneyReport::eMonths, + eMyMoney::Report::RowType::AssetLiability, + static_cast(eMyMoney::Report::ColumnType::Months), TransactionFilter::Date::Last30Days, - MyMoneyReport::eDetailAll, + eMyMoney::Report::DetailLevel::All, i18n("Investment Moving Average vs Actual"), i18n("Default Report") )); list.back().setChartByDefault(true); list.back().setChartCHGridLines(false); list.back().setChartSVGridLines(false); - list.back().setChartType(MyMoneyReport::eChartLine); + list.back().setChartType(eMyMoney::Report::ChartType::Line); list.back().setColumnsAreDays(true); list.back().setInvestmentsOnly(true); list.back().setIncludingBudgetActuals(true); list.back().setIncludingMovingAverage(true); list.back().setMovingAverageDays(10); groups.push_back(list); } { ReportGroup list("Taxes", i18n("Taxes")); list.push_back(MyMoneyReport( - MyMoneyReport::eCategory, - MyMoneyReport::eQCnumber | MyMoneyReport::eQCpayee | MyMoneyReport::eQCaccount, + eMyMoney::Report::RowType::Category, + eMyMoney::Report::QueryColumn::Number | eMyMoney::Report::QueryColumn::Payee | eMyMoney::Report::QueryColumn::Account, TransactionFilter::Date::YearToDate, - MyMoneyReport::eDetailAll, + eMyMoney::Report::DetailLevel::All, i18n("Tax Transactions by Category"), i18n("Default Report") )); list.back().setTax(true); list.push_back(MyMoneyReport( - MyMoneyReport::ePayee, - MyMoneyReport::eQCnumber | MyMoneyReport::eQCcategory | MyMoneyReport::eQCaccount, + eMyMoney::Report::RowType::Payee, + eMyMoney::Report::QueryColumn::Number | eMyMoney::Report::QueryColumn::Category | eMyMoney::Report::QueryColumn::Account, TransactionFilter::Date::YearToDate, - MyMoneyReport::eDetailAll, + eMyMoney::Report::DetailLevel::All, i18n("Tax Transactions by Payee"), i18n("Default Report") )); list.back().setTax(true); list.push_back(MyMoneyReport( - MyMoneyReport::eCategory, - MyMoneyReport::eQCnumber | MyMoneyReport::eQCpayee | MyMoneyReport::eQCaccount, + eMyMoney::Report::RowType::Category, + eMyMoney::Report::QueryColumn::Number | eMyMoney::Report::QueryColumn::Payee | eMyMoney::Report::QueryColumn::Account, TransactionFilter::Date::LastFiscalYear, - MyMoneyReport::eDetailAll, + eMyMoney::Report::DetailLevel::All, i18n("Tax Transactions by Category Last Fiscal Year"), i18n("Default Report") )); list.back().setTax(true); list.push_back(MyMoneyReport( - MyMoneyReport::ePayee, - MyMoneyReport::eQCnumber | MyMoneyReport::eQCcategory | MyMoneyReport::eQCaccount, + eMyMoney::Report::RowType::Payee, + eMyMoney::Report::QueryColumn::Number | eMyMoney::Report::QueryColumn::Category | eMyMoney::Report::QueryColumn::Account, TransactionFilter::Date::LastFiscalYear, - MyMoneyReport::eDetailAll, + eMyMoney::Report::DetailLevel::All, i18n("Tax Transactions by Payee Last Fiscal Year"), i18n("Default Report") )); list.back().setTax(true); groups.push_back(list); } { ReportGroup list("Budgeting", i18n("Budgeting")); list.push_back(MyMoneyReport( - MyMoneyReport::eBudgetActual, - MyMoneyReport::eMonths, + eMyMoney::Report::RowType::BudgetActual, + static_cast(eMyMoney::Report::ColumnType::Months), TransactionFilter::Date::YearToDate, - MyMoneyReport::eDetailAll, + eMyMoney::Report::DetailLevel::All, i18n("Budgeted vs. Actual This Year"), i18n("Default Report") )); list.back().setShowingRowTotals(true); list.back().setBudget("Any", true); list.push_back(MyMoneyReport( - MyMoneyReport::eBudgetActual, - MyMoneyReport::eMonths, + eMyMoney::Report::RowType::BudgetActual, + static_cast(eMyMoney::Report::ColumnType::Months), TransactionFilter::Date::YearToMonth, - MyMoneyReport::eDetailAll, + eMyMoney::Report::DetailLevel::All, i18n("Budgeted vs. Actual This Year (YTM)"), i18n("Default Report") )); list.back().setShowingRowTotals(true); list.back().setBudget("Any", true); // in case we're in January, we show the last year if (QDate::currentDate().month() == 1) { list.back().setDateFilter(TransactionFilter::Date::LastYear); } list.push_back(MyMoneyReport( - MyMoneyReport::eBudgetActual, - MyMoneyReport::eMonths, + eMyMoney::Report::RowType::BudgetActual, + static_cast(eMyMoney::Report::ColumnType::Months), TransactionFilter::Date::CurrentMonth, - MyMoneyReport::eDetailAll, + eMyMoney::Report::DetailLevel::All, i18n("Monthly Budgeted vs. Actual"), i18n("Default Report") )); list.back().setBudget("Any", true); list.push_back(MyMoneyReport( - MyMoneyReport::eBudgetActual, - MyMoneyReport::eMonths, + eMyMoney::Report::RowType::BudgetActual, + static_cast(eMyMoney::Report::ColumnType::Months), TransactionFilter::Date::CurrentYear, - MyMoneyReport::eDetailAll, + eMyMoney::Report::DetailLevel::All, i18n("Yearly Budgeted vs. Actual"), i18n("Default Report") )); list.back().setBudget("Any", true); list.back().setShowingRowTotals(true); list.push_back(MyMoneyReport( - MyMoneyReport::eBudget, - MyMoneyReport::eMonths, + eMyMoney::Report::RowType::Budget, + static_cast(eMyMoney::Report::ColumnType::Months), TransactionFilter::Date::CurrentMonth, - MyMoneyReport::eDetailAll, + eMyMoney::Report::DetailLevel::All, i18n("Monthly Budget"), i18n("Default Report") )); list.back().setBudget("Any", false); list.push_back(MyMoneyReport( - MyMoneyReport::eBudget, - MyMoneyReport::eMonths, + eMyMoney::Report::RowType::Budget, + static_cast(eMyMoney::Report::ColumnType::Months), TransactionFilter::Date::CurrentYear, - MyMoneyReport::eDetailAll, + eMyMoney::Report::DetailLevel::All, i18n("Yearly Budget"), i18n("Default Report") )); list.back().setBudget("Any", false); list.back().setShowingRowTotals(true); list.push_back(MyMoneyReport( - MyMoneyReport::eBudgetActual, - MyMoneyReport::eMonths, + eMyMoney::Report::RowType::BudgetActual, + static_cast(eMyMoney::Report::ColumnType::Months), TransactionFilter::Date::CurrentYear, - MyMoneyReport::eDetailGroup, + eMyMoney::Report::DetailLevel::Group, i18n("Yearly Budgeted vs Actual Graph"), i18n("Default Report") )); list.back().setChartByDefault(true); list.back().setChartCHGridLines(false); list.back().setChartSVGridLines(false); list.back().setBudget("Any", true); - list.back().setChartType(MyMoneyReport::eChartLine); + list.back().setChartType(eMyMoney::Report::ChartType::Line); groups.push_back(list); } { ReportGroup list("Forecast", i18n("Forecast")); list.push_back(MyMoneyReport( - MyMoneyReport::eAssetLiability, - MyMoneyReport::eMonths, + eMyMoney::Report::RowType::AssetLiability, + static_cast(eMyMoney::Report::ColumnType::Months), TransactionFilter::Date::Next12Months, - MyMoneyReport::eDetailTop, + eMyMoney::Report::DetailLevel::Top, i18n("Forecast By Month"), i18n("Default Report") )); list.back().setIncludingForecast(true); list.push_back(MyMoneyReport( - MyMoneyReport::eAssetLiability, - MyMoneyReport::eMonths, + eMyMoney::Report::RowType::AssetLiability, + static_cast(eMyMoney::Report::ColumnType::Months), TransactionFilter::Date::NextQuarter, - MyMoneyReport::eDetailTop, + eMyMoney::Report::DetailLevel::Top, i18n("Forecast Next Quarter"), i18n("Default Report") )); list.back().setColumnsAreDays(true); list.back().setIncludingForecast(true); list.push_back(MyMoneyReport( - MyMoneyReport::eExpenseIncome, - MyMoneyReport::eMonths, + eMyMoney::Report::RowType::ExpenseIncome, + static_cast(eMyMoney::Report::ColumnType::Months), TransactionFilter::Date::CurrentYear, - MyMoneyReport::eDetailTop, + eMyMoney::Report::DetailLevel::Top, i18n("Income and Expenses Forecast This Year"), i18n("Default Report") )); list.back().setIncludingForecast(true); list.push_back(MyMoneyReport( - MyMoneyReport::eAssetLiability, - MyMoneyReport::eMonths, + eMyMoney::Report::RowType::AssetLiability, + static_cast(eMyMoney::Report::ColumnType::Months), TransactionFilter::Date::Next3Months, - MyMoneyReport::eDetailTotal, + eMyMoney::Report::DetailLevel::Total, i18n("Net Worth Forecast Graph"), i18n("Default Report") )); list.back().setColumnsAreDays(true); list.back().setIncludingForecast(true); list.back().setChartByDefault(true); list.back().setChartCHGridLines(false); list.back().setChartSVGridLines(false); - list.back().setChartType(MyMoneyReport::eChartLine); + list.back().setChartType(eMyMoney::Report::ChartType::Line); groups.push_back(list); } { ReportGroup list("Information", i18n("General Information")); list.push_back(MyMoneyReport( - MyMoneyReport::eSchedule, - MyMoneyReport::eMonths, + eMyMoney::Report::RowType::Schedule, + static_cast(eMyMoney::Report::ColumnType::Months), TransactionFilter::Date::Next12Months, - MyMoneyReport::eDetailAll, + eMyMoney::Report::DetailLevel::All, i18n("Schedule Information"), i18n("Default Report") )); - list.back().setDetailLevel(MyMoneyReport::eDetailAll); + list.back().setDetailLevel(eMyMoney::Report::DetailLevel::All); list.push_back(MyMoneyReport( - MyMoneyReport::eSchedule, - MyMoneyReport::eMonths, + eMyMoney::Report::RowType::Schedule, + static_cast(eMyMoney::Report::ColumnType::Months), TransactionFilter::Date::Next12Months, - MyMoneyReport::eDetailAll, + eMyMoney::Report::DetailLevel::All, i18n("Schedule Summary Information"), i18n("Default Report") )); - list.back().setDetailLevel(MyMoneyReport::eDetailTop); + list.back().setDetailLevel(eMyMoney::Report::DetailLevel::Top); list.push_back(MyMoneyReport( - MyMoneyReport::eAccountInfo, - MyMoneyReport::eMonths, + eMyMoney::Report::RowType::AccountInfo, + static_cast(eMyMoney::Report::ColumnType::Months), TransactionFilter::Date::Today, - MyMoneyReport::eDetailAll, + eMyMoney::Report::DetailLevel::All, i18n("Account Information"), i18n("Default Report") )); list.back().setConvertCurrency(false); list.push_back(MyMoneyReport( - MyMoneyReport::eAccountLoanInfo, - MyMoneyReport::eMonths, + eMyMoney::Report::RowType::AccountLoanInfo, + static_cast(eMyMoney::Report::ColumnType::Months), TransactionFilter::Date::Today, - MyMoneyReport::eDetailAll, + eMyMoney::Report::DetailLevel::All, i18n("Loan Information"), i18n("Default Report") )); list.back().setConvertCurrency(false); groups.push_back(list); } } bool columnsAlreadyAdjusted() const { return m_columnsAlreadyAdjusted; } void setColumnsAlreadyAdjusted(bool adjusted) { m_columnsAlreadyAdjusted = adjusted; } KReportsView *q_ptr; /** * This member holds the load state of page */ bool m_needLoad; QListWidget* m_reportListView; QTabWidget* m_reportTabWidget; QWidget* m_listTab; QVBoxLayout* m_listTabLayout; QTreeWidget* m_tocTreeWidget; QMap m_allTocItemGroups; QString m_selectedExportFilter; bool m_columnsAlreadyAdjusted; MyMoneyAccount m_currentAccount; }; #endif diff --git a/kmymoney/plugins/views/reports/reporttabimpl.cpp b/kmymoney/plugins/views/reports/reporttabimpl.cpp index 419e39073..e252f39b4 100644 --- a/kmymoney/plugins/views/reports/reporttabimpl.cpp +++ b/kmymoney/plugins/views/reports/reporttabimpl.cpp @@ -1,330 +1,331 @@ /* This file is part of the KDE project Copyright (C) 2009 Laurent Montel (C) 2017 by Łukasz Wojniłowicz This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "reporttabimpl.h" #include #include "daterangedlg.h" #include "ui_reporttabgeneral.h" #include "ui_reporttabrowcolpivot.h" #include "ui_reporttabrowcolquery.h" #include "ui_reporttabchart.h" #include "ui_reporttabrange.h" #include "ui_reporttabcapitalgain.h" #include "ui_reporttabperformance.h" #include "mymoney/mymoneyreport.h" +#include "mymoneyenums.h" ReportTabGeneral::ReportTabGeneral(QWidget *parent) : QWidget(parent) { ui = new Ui::ReportTabGeneral; ui->setupUi(this); } ReportTabGeneral::~ReportTabGeneral() { delete ui; } ReportTabRowColPivot::ReportTabRowColPivot(QWidget *parent) : QWidget(parent) { ui = new Ui::ReportTabRowColPivot; ui->setupUi(this); } ReportTabRowColPivot::~ReportTabRowColPivot() { delete ui; } ReportTabRowColQuery::ReportTabRowColQuery(QWidget *parent) : QWidget(parent) { ui = new Ui::ReportTabRowColQuery; ui->setupUi(this); ui->buttonGroup1->setExclusive(false); ui->buttonGroup1->setId(ui->m_checkMemo, 0); ui->buttonGroup1->setId(ui->m_checkShares, 1); ui->buttonGroup1->setId(ui->m_checkPrice, 2); ui->buttonGroup1->setId(ui->m_checkReconciled, 3); ui->buttonGroup1->setId(ui->m_checkAccount, 4); ui->buttonGroup1->setId(ui->m_checkNumber, 5); ui->buttonGroup1->setId(ui->m_checkPayee, 6); ui->buttonGroup1->setId(ui->m_checkCategory, 7); ui->buttonGroup1->setId(ui->m_checkAction, 8); ui->buttonGroup1->setId(ui->m_checkBalance, 9); connect(ui->m_checkHideTransactions, &QAbstractButton::toggled, this, &ReportTabRowColQuery::slotHideTransactionsChanged); } void ReportTabRowColQuery::slotHideTransactionsChanged(bool checked) { if (checked) // toggle m_checkHideSplitDetails only if it's mandatory ui->m_checkHideSplitDetails->setChecked(checked); ui->m_checkHideSplitDetails->setEnabled(!checked); // hiding transactions without hiding splits isn't allowed } ReportTabRowColQuery::~ReportTabRowColQuery() { delete ui; } ReportTabChart::ReportTabChart(QWidget *parent) : QWidget(parent) { ui = new Ui::ReportTabChart; ui->setupUi(this); - ui->m_comboType->addItem(i18nc("type of graphic chart", "Line"), MyMoneyReport::eChartLine); - ui->m_comboType->addItem(i18nc("type of graphic chart", "Bar"), MyMoneyReport::eChartBar); - ui->m_comboType->addItem(i18nc("type of graphic chart", "Stacked Bar"), MyMoneyReport::eChartStackedBar); - ui->m_comboType->addItem(i18nc("type of graphic chart", "Pie"), MyMoneyReport::eChartPie); - ui->m_comboType->addItem(i18nc("type of graphic chart", "Ring"), MyMoneyReport::eChartRing); + ui->m_comboType->addItem(i18nc("type of graphic chart", "Line"), static_cast(eMyMoney::Report::ChartType::Line)); + ui->m_comboType->addItem(i18nc("type of graphic chart", "Bar"), static_cast(eMyMoney::Report::ChartType::Bar)); + ui->m_comboType->addItem(i18nc("type of graphic chart", "Stacked Bar"), static_cast(eMyMoney::Report::ChartType::StackedBar)); + ui->m_comboType->addItem(i18nc("type of graphic chart", "Pie"), static_cast(eMyMoney::Report::ChartType::Pie)); + ui->m_comboType->addItem(i18nc("type of graphic chart", "Ring"), static_cast(eMyMoney::Report::ChartType::Ring)); connect(ui->m_comboType, static_cast(&QComboBox::currentIndexChanged), this, &ReportTabChart::slotChartTypeChanged); emit ui->m_comboType->currentIndexChanged(ui->m_comboType->currentIndex()); } ReportTabChart::~ReportTabChart() { delete ui; } void ReportTabChart::slotChartTypeChanged(int index) { - if (index == MyMoneyReport::EChartType::eChartPie || - index == MyMoneyReport::EChartType::eChartRing) { + if (index == static_cast(eMyMoney::Report::ChartType::Pie) || + index == static_cast(eMyMoney::Report::ChartType::Ring)) { ui->m_checkCHGridLines->setText(i18n("Show circular grid lines")); ui->m_checkSVGridLines->setText(i18n("Show sagittal grid lines")); ui->m_logYaxis->setChecked(false); ui->m_logYaxis->setEnabled(false); } else { ui->m_checkCHGridLines->setText(i18n("Show horizontal grid lines")); ui->m_checkSVGridLines->setText(i18n("Show vertical grid lines")); ui->m_logYaxis->setEnabled(true); } } ReportTabRange::ReportTabRange(QWidget *parent) : QWidget(parent), ui(new Ui::ReportTabRange) { ui->setupUi(this); m_dateRange = new DateRangeDlg; ui->dateRangeGrid->addWidget(m_dateRange, 0, 0, 1, 2); connect(ui->m_yLabelsPrecision, static_cast(&QSpinBox::valueChanged), this, &ReportTabRange::slotYLabelsPrecisionChanged); emit ui->m_yLabelsPrecision->valueChanged(ui->m_yLabelsPrecision->value()); connect(ui->m_dataRangeStart, &QLineEdit::editingFinished, this, &ReportTabRange::slotEditingFinishedStart); connect(ui->m_dataRangeEnd, &QLineEdit::editingFinished, this, &ReportTabRange::slotEditingFinishedEnd); connect(ui->m_dataMajorTick, &QLineEdit::editingFinished, this, &ReportTabRange::slotEditingFinishedMajor); connect(ui->m_dataMinorTick, &QLineEdit::editingFinished, this, &ReportTabRange::slotEditingFinishedMinor); connect(ui->m_dataLock, static_cast(&QComboBox::currentIndexChanged), this, &ReportTabRange::slotDataLockChanged); emit ui->m_dataLock->currentIndexChanged(ui->m_dataLock->currentIndex()); } ReportTabRange::~ReportTabRange() { delete ui; } void ReportTabRange::setRangeLogarythmic(bool set) { // major and minor tick have no influence if axis is logarithmic so hide them if (set) { ui->lblDataMajorTick->hide(); ui->lblDataMinorTick->hide(); ui->m_dataMajorTick->hide(); ui->m_dataMinorTick->hide(); } else { ui->lblDataMajorTick->show(); ui->lblDataMinorTick->show(); ui->m_dataMajorTick->show(); ui->m_dataMinorTick->show(); } } void ReportTabRange::slotEditingFinished(EDimension dim) { qreal dataRangeStart = locale().toDouble(ui->m_dataRangeStart->text()); qreal dataRangeEnd = locale().toDouble(ui->m_dataRangeEnd->text()); qreal dataMajorTick = locale().toDouble(ui->m_dataMajorTick->text()); qreal dataMinorTick = locale().toDouble(ui->m_dataMinorTick->text()); if (dataRangeEnd < dataRangeStart) { // end must be higher than start if (dim == eRangeEnd) { ui->m_dataRangeStart->setText(ui->m_dataRangeEnd->text()); dataRangeStart = dataRangeEnd; } else { ui->m_dataRangeEnd->setText(ui->m_dataRangeStart->text()); dataRangeEnd = dataRangeStart; } } if ((dataRangeStart != 0 || dataRangeEnd != 0)) { // if data range isn't going to be reset if ((dataRangeEnd - dataRangeStart) < dataMajorTick) // major tick cannot be greater than data range dataMajorTick = dataRangeEnd - dataRangeStart; if (dataMajorTick != 0 && // if major tick isn't going to be reset dataMajorTick < (dataRangeEnd - dataRangeStart) * 0.01) // constraint major tick to be greater or equal to 1% of data range dataMajorTick = (dataRangeEnd - dataRangeStart) * 0.01; // that should produce more than 256 Y labels in KReportChartView::slotNeedUpdate //set precision of major tick to be greater by 1 ui->m_dataMajorTick->setText(locale().toString(dataMajorTick, 'f', ui->m_yLabelsPrecision->value() + 1).remove(locale().groupSeparator()).remove(QRegularExpression("0+$")).remove(QRegularExpression("\\" + locale().decimalPoint() + "$"))); } if (dataMajorTick < dataMinorTick) { // major tick must be higher than minor if (dim == eMinorTick) { ui->m_dataMajorTick->setText(ui->m_dataMinorTick->text()); dataMajorTick = dataMinorTick; } else { ui->m_dataMinorTick->setText(ui->m_dataMajorTick->text()); dataMinorTick = dataMajorTick; } } if (dataMinorTick < dataMajorTick * 0.1) { // constraint minor tick to be greater or equal to 10% of major tick, and set precision to be greater by 1 dataMinorTick = dataMajorTick * 0.1; ui->m_dataMinorTick->setText(locale().toString(dataMinorTick, 'f', ui->m_yLabelsPrecision->value() + 1).remove(locale().groupSeparator()).remove(QRegularExpression("0+$")).remove(QRegularExpression("\\" + locale().decimalPoint() + "$"))); } } void ReportTabRange::slotEditingFinishedStart() { slotEditingFinished(eRangeStart); } void ReportTabRange::slotEditingFinishedEnd() { slotEditingFinished(eRangeEnd); } void ReportTabRange::slotEditingFinishedMajor() { slotEditingFinished(eMajorTick); } void ReportTabRange::slotEditingFinishedMinor() { slotEditingFinished(eMinorTick); } void ReportTabRange::slotYLabelsPrecisionChanged(const int& value) { ui->m_dataRangeStart->setValidator(0); ui->m_dataRangeEnd->setValidator(0); ui->m_dataMajorTick->setValidator(0); ui->m_dataMinorTick->setValidator(0); MyDoubleValidator *dblVal = new MyDoubleValidator(value); ui->m_dataRangeStart->setValidator(dblVal); ui->m_dataRangeEnd->setValidator(dblVal); MyDoubleValidator *dblVal2 = new MyDoubleValidator(value + 1); ui->m_dataMajorTick->setValidator(dblVal2); ui->m_dataMinorTick->setValidator(dblVal2); } void ReportTabRange::slotDataLockChanged(int index) { - if (index == MyMoneyReport::dataOptionE::automatic) { + if (index == static_cast(eMyMoney::Report::DataLock::Automatic)) { ui->m_dataRangeStart->setText(QStringLiteral("0")); ui->m_dataRangeEnd->setText(QStringLiteral("0")); ui->m_dataMajorTick->setText(QStringLiteral("0")); ui->m_dataMinorTick->setText(QStringLiteral("0")); ui->m_dataRangeStart->setEnabled(false); ui->m_dataRangeEnd->setEnabled(false); ui->m_dataMajorTick->setEnabled(false); ui->m_dataMinorTick->setEnabled(false); } else { ui->m_dataRangeStart->setEnabled(true); ui->m_dataRangeEnd->setEnabled(true); ui->m_dataMajorTick->setEnabled(true); ui->m_dataMinorTick->setEnabled(true); } } ReportTabCapitalGain::ReportTabCapitalGain(QWidget *parent) : QWidget(parent) { ui = new Ui::ReportTabCapitalGain; ui->setupUi(this); connect(ui->m_investmentSum, static_cast(&QComboBox::currentIndexChanged), this, &ReportTabCapitalGain::slotInvestmentSumChanged); } ReportTabCapitalGain::~ReportTabCapitalGain() { delete ui; } void ReportTabCapitalGain::slotInvestmentSumChanged(int index) { Q_UNUSED(index); - if (ui->m_investmentSum->currentData() == MyMoneyReport::eSumOwned) { + if (ui->m_investmentSum->currentData() == static_cast(eMyMoney::Report::InvestmentSum::Owned)) { ui->m_settlementPeriod->setValue(0); ui->m_settlementPeriod->setEnabled(false); ui->m_showSTLTCapitalGains->setChecked(false); ui->m_showSTLTCapitalGains->setEnabled(false); ui->m_termSeparator->setEnabled(false); } else { ui->m_settlementPeriod->setEnabled(true); ui->m_showSTLTCapitalGains->setEnabled(true); ui->m_termSeparator->setEnabled(true); } } ReportTabPerformance::ReportTabPerformance(QWidget *parent) : QWidget(parent) { ui = new Ui::ReportTabPerformance; ui->setupUi(this); } ReportTabPerformance::~ReportTabPerformance() { delete ui; } MyDoubleValidator::MyDoubleValidator(int decimals, QObject * parent) : QDoubleValidator(0, 0, decimals, parent) { } QValidator::State MyDoubleValidator::validate(QString &s, int &i) const { Q_UNUSED(i); if (s.isEmpty() || s == "-") { return QValidator::Intermediate; } QChar decimalPoint = locale().decimalPoint(); if(s.indexOf(decimalPoint) != -1) { int charsAfterPoint = s.length() - s.indexOf(decimalPoint) - 1; if (charsAfterPoint > decimals()) { return QValidator::Invalid; } } bool ok; locale().toDouble(s, &ok); if (ok) { return QValidator::Acceptable; } else { return QValidator::Invalid; } } diff --git a/kmymoney/plugins/xml/mymoneystorageanon.cpp b/kmymoney/plugins/xml/mymoneystorageanon.cpp index 8ecf3e49c..886b28d9c 100644 --- a/kmymoney/plugins/xml/mymoneystorageanon.cpp +++ b/kmymoney/plugins/xml/mymoneystorageanon.cpp @@ -1,323 +1,323 @@ /* * Copyright 2004-2006 Ace Jones * Copyright 2005-2017 Thomas Baumgart * * 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 "mymoneystorageanon.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes #include "mymoneystoragemgr.h" #include "mymoneyreport.h" #include "mymoneyschedule.h" #include "mymoneysplit.h" #include "mymoneybudget.h" #include "mymoneytransaction.h" #include "mymoneyinstitution.h" #include "mymoneyaccount.h" #include "mymoneysecurity.h" #include "mymoneypayee.h" #include "mymoneytag.h" #include "mymoneykeyvaluecontainer.h" #include "mymoneyexception.h" QStringList MyMoneyStorageANON::zKvpNoModify = QString("kmm-baseCurrency,OpeningBalanceAccount,PreferredAccount,Tax,fixed-interest,interest-calculation,payee,schedule,term,kmm-online-source,kmm-brokerage-account,lastStatementDate,kmm-sort-reconcile,kmm-sort-std,kmm-iconpos,mm-closed,payee,schedule,term,lastImportedTransactionDate,VatAccount,VatRate,kmm-matched-tx,Imported,priceMode").split(','); QStringList MyMoneyStorageANON::zKvpXNumber = QString("final-payment,loan-amount,periodic-payment,lastStatementBalance").split(','); MyMoneyStorageANON::MyMoneyStorageANON() : MyMoneyStorageXML() { // Choose a quasi-random 0.0-100.0 factor which will be applied to all splits this time // around. int msec; do { msec = QTime::currentTime().msec(); } while (msec == 0); m_factor = MyMoneyMoney(msec, 10).reduce(); } MyMoneyStorageANON::~MyMoneyStorageANON() { } void MyMoneyStorageANON::readFile(QIODevice* , MyMoneyStorageMgr*) { throw MYMONEYEXCEPTION_CSTRING("Cannot read a file through MyMoneyStorageANON!!"); } void MyMoneyStorageANON::writeUserInformation(QDomElement& userInfo) { MyMoneyPayee user = m_storage->user(); userInfo.setAttribute(QString("name"), hideString(user.name())); userInfo.setAttribute(QString("email"), hideString(user.email())); QDomElement address = m_doc->createElement("ADDRESS"); address.setAttribute(QString("street"), hideString(user.address())); address.setAttribute(QString("city"), hideString(user.city())); address.setAttribute(QString("county"), hideString(user.state())); address.setAttribute(QString("zipcode"), hideString(user.postcode())); address.setAttribute(QString("telephone"), hideString(user.telephone())); userInfo.appendChild(address); } void MyMoneyStorageANON::writeInstitution(QDomElement& institution, const MyMoneyInstitution& _i) { MyMoneyInstitution i(_i); // mangle fields i.setName(i.id()); i.setManager(hideString(i.manager())); i.setSortcode(hideString(i.sortcode())); i.setStreet(hideString(i.street())); i.setCity(hideString(i.city())); i.setPostcode(hideString(i.postcode())); i.setTelephone(hideString(i.telephone())); MyMoneyStorageXML::writeInstitution(institution, i); } void MyMoneyStorageANON::writePayee(QDomElement& payee, const MyMoneyPayee& _p) { MyMoneyPayee p(_p); p.setName(p.id()); p.setReference(hideString(p.reference())); p.setAddress(hideString(p.address())); p.setCity(hideString(p.city())); p.setPostcode(hideString(p.postcode())); p.setState(hideString(p.state())); p.setTelephone(hideString(p.telephone())); p.setNotes(hideString(p.notes())); bool ignoreCase; QStringList keys; - MyMoneyPayee::payeeMatchType matchType = p.matchData(ignoreCase, keys); + auto matchType = p.matchData(ignoreCase, keys); QRegExp exp("[A-Za-z]"); p.setMatchData(matchType, ignoreCase, keys.join(";").replace(exp, "x").split(';')); // Data from plugins cannot be estranged, yet. p.resetPayeeIdentifiers(); MyMoneyStorageXML::writePayee(payee, p); } void MyMoneyStorageANON::writeTag(QDomElement& tag, const MyMoneyTag& _ta) { MyMoneyTag ta(_ta); ta.setName(ta.id()); ta.setNotes(hideString(ta.notes())); MyMoneyStorageXML::writeTag(tag, ta); } void MyMoneyStorageANON::writeAccount(QDomElement& account, const MyMoneyAccount& _p) { MyMoneyAccount p(_p); p.setNumber(hideString(p.number())); p.setName(p.id()); p.setDescription(hideString(p.description())); fakeKeyValuePair(p); // Remove the online banking settings entirely. p.setOnlineBankingSettings(MyMoneyKeyValueContainer()); MyMoneyStorageXML::writeAccount(account, p); } void MyMoneyStorageANON::fakeTransaction(MyMoneyTransaction& tx) { MyMoneyTransaction tn = tx; // hide transaction data tn.setMemo(tx.id()); tn.setBankID(hideString(tx.bankID())); // hide split data foreach (const auto split, tx.splits()) { MyMoneySplit s = split; s.setMemo(QString("%1/%2").arg(tn.id()).arg(s.id())); if (s.value() != MyMoneyMoney::autoCalc) { s.setValue((s.value() * m_factor)); s.setShares((s.shares() * m_factor)); } s.setNumber(hideString(s.number())); // obfuscate a possibly matched transaction as well if (s.isMatched()) { MyMoneyTransaction t = s.matchedTransaction(); fakeTransaction(t); s.removeMatch(); s.addMatch(t); } tn.modifySplit(s); } tx = tn; fakeKeyValuePair(tx); } void MyMoneyStorageANON::fakeKeyValuePair(MyMoneyKeyValueContainer& kvp) { QMap pairs; QMap::const_iterator it; for (it = kvp.pairs().constBegin(); it != kvp.pairs().constEnd(); ++it) { if (zKvpXNumber.contains(it.key()) || it.key().left(3) == "ir-") pairs[it.key()] = hideNumber(MyMoneyMoney(it.value())).toString(); else if (zKvpNoModify.contains(it.key())) pairs[it.key()] = it.value(); else pairs[it.key()] = hideString(it.value()); } kvp.setPairs(pairs); } void MyMoneyStorageANON::writeTransaction(QDomElement& transactions, const MyMoneyTransaction& tx) { MyMoneyTransaction tn = tx; fakeTransaction(tn); MyMoneyStorageXML::writeTransaction(transactions, tn); } void MyMoneyStorageANON::writeSchedule(QDomElement& scheduledTx, const MyMoneySchedule& sx) { MyMoneySchedule sn = sx; MyMoneyTransaction tn = sn.transaction(); fakeTransaction(tn); sn.setName(sx.id()); sn.setTransaction(tn, true); MyMoneyStorageXML::writeSchedule(scheduledTx, sn); } void MyMoneyStorageANON::writeSecurity(QDomElement& securityElement, const MyMoneySecurity& security) { MyMoneySecurity s = security; s.setName(security.id()); fakeKeyValuePair(s); MyMoneyStorageXML::writeSecurity(securityElement, s); } QString MyMoneyStorageANON::hideString(const QString& _in) const { return QString(_in).fill('x'); } MyMoneyMoney MyMoneyStorageANON::hideNumber(const MyMoneyMoney& _in) const { MyMoneyMoney result; static MyMoneyMoney counter = MyMoneyMoney(100, 100); // preserve sign if (_in.isNegative()) result = MyMoneyMoney::MINUS_ONE; else result = MyMoneyMoney::ONE; result = result * counter; counter += MyMoneyMoney("10/100"); // preserve > 1000 if (_in >= MyMoneyMoney(1000, 1)) result = result * MyMoneyMoney(1000, 1); if (_in <= MyMoneyMoney(-1000, 1)) result = result * MyMoneyMoney(1000, 1); return result.convert(); } void MyMoneyStorageANON::fakeBudget(MyMoneyBudget& bx) { MyMoneyBudget bn; bn.setName(bx.id()); bn.setBudgetStart(bx.budgetStart()); bn = MyMoneyBudget(bx.id(), bn); QList list = bx.getaccounts(); QList::iterator it; for (it = list.begin(); it != list.end(); ++it) { // only add the account if there is a budget entered if (!(*it).balance().isZero()) { MyMoneyBudget::AccountGroup account; account.setId((*it).id()); account.setBudgetLevel((*it).budgetLevel()); account.setBudgetSubaccounts((*it).budgetSubaccounts()); QMap plist = (*it).getPeriods(); QMap::const_iterator it_p; for (it_p = plist.constBegin(); it_p != plist.constEnd(); ++it_p) { MyMoneyBudget::PeriodGroup pGroup; pGroup.setAmount((*it_p).amount() * m_factor); pGroup.setStartDate((*it_p).startDate()); account.addPeriod(pGroup.startDate(), pGroup); } bn.setAccount(account, account.id()); } } bx = bn; } void MyMoneyStorageANON::writeBudget(QDomElement& budgets, const MyMoneyBudget& b) { MyMoneyBudget bn = b; fakeBudget(bn); MyMoneyStorageXML::writeBudget(budgets, bn); } void MyMoneyStorageANON::writeReport(QDomElement& reports, const MyMoneyReport& r) { MyMoneyReport rn = r; rn.setName(rn.id()); rn.setComment(hideString(rn.comment())); MyMoneyStorageXML::writeReport(reports, rn); } void MyMoneyStorageANON::writeOnlineJob(QDomElement& onlineJobs, const onlineJob& job) { Q_UNUSED(onlineJobs); Q_UNUSED(job); } diff --git a/kmymoney/reports/kreportchartview.cpp b/kmymoney/reports/kreportchartview.cpp index a255a20fe..390819223 100644 --- a/kmymoney/reports/kreportchartview.cpp +++ b/kmymoney/reports/kreportchartview.cpp @@ -1,746 +1,746 @@ /* * Copyright 2005 Ace Jones * Copyright 2005-2018 Thomas Baumgart * 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 "kreportchartview.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include // ---------------------------------------------------------------------------- // Project Includes #include #include #include #include #include #include #include #include #include #include #include #include #include "kmymoneysettings.h" #include #include "mymoneyfile.h" #include "mymoneysecurity.h" #include "mymoneyenums.h" using namespace reports; KReportChartView::KReportChartView(QWidget* parent) : KChart::Chart(parent), m_accountSeries(0), m_seriesTotals(0), m_numColumns(0), m_skipZero(0), m_backgroundBrush(KColorScheme(QPalette::Current).background()), m_foregroundBrush(KColorScheme(QPalette::Current).foreground()), m_precision(2) { // ******************************************************************** // Set KMyMoney's Chart Parameter Defaults // ******************************************************************** //Set the background obtained from the color scheme BackgroundAttributes backAttr(backgroundAttributes()); backAttr.setBrush(m_backgroundBrush); backAttr.setVisible(true); setBackgroundAttributes(backAttr); } void KReportChartView::drawPivotChart(const PivotGrid &grid, const MyMoneyReport &config, int numberColumns, const QStringList& columnHeadings, const QList& rowTypeList, const QStringList& columnTypeHeaderList) { if (numberColumns == 0) return; //set the number of columns setNumColumns(numberColumns); //set skipZero m_skipZero = config.isSkippingZero(); //remove existing headers const HeaderFooterList hfList = headerFooters(); foreach (const auto hf, hfList) delete hf; //remove existing legends const LegendList lgList = legends(); foreach (const auto lg, lgList) delete lg; //make sure the model is clear m_model.clear(); const bool blocked = m_model.blockSignals(true); // don't emit dataChanged() signal during each drawPivotRowSet //set the new header HeaderFooter* header = new HeaderFooter; header->setText(config.name()); header->setType(HeaderFooter::Header); header->setPosition(Position::North); TextAttributes headerTextAttr(header->textAttributes()); headerTextAttr.setPen(m_foregroundBrush.color()); header->setTextAttributes(headerTextAttr); addHeaderFooter(header); // whether to limit the chart to use series totals only. Used for reports which only // show one dimension (pie). setSeriesTotals(false); // whether series (rows) are accounts (true) or months (false). This causes a lot // of complexity in the charts. The problem is that circular reports work best with // an account in a COLUMN, while line/bar prefer it in a ROW. setAccountSeries(true); switch (config.chartType()) { - case MyMoneyReport::eChartLine: - case MyMoneyReport::eChartBar: - case MyMoneyReport::eChartStackedBar: { + case eMyMoney::Report::ChartType::Line: + case eMyMoney::Report::ChartType::Bar: + case eMyMoney::Report::ChartType::StackedBar: { CartesianCoordinatePlane* cartesianPlane = new CartesianCoordinatePlane(this); cartesianPlane->setAutoAdjustVerticalRangeToData(2); cartesianPlane->setRubberBandZoomingEnabled(true); replaceCoordinatePlane(cartesianPlane); // set-up axis type if (config.isLogYAxis()) cartesianPlane->setAxesCalcModeY(KChart::AbstractCoordinatePlane::Logarithmic); else cartesianPlane->setAxesCalcModeY(KChart::AbstractCoordinatePlane::Linear); QLocale loc = locale(); // set-up grid GridAttributes ga = cartesianPlane->gridAttributes(Qt::Vertical); ga.setGridVisible(config.isChartCHGridLines()); ga.setGridStepWidth(config.isDataUserDefined() ? loc.toDouble(config.dataMajorTick()) : 0.0); ga.setGridSubStepWidth(config.isDataUserDefined() ? loc.toDouble(config.dataMinorTick()) : 0.0); cartesianPlane->setGridAttributes(Qt::Vertical, ga); ga = cartesianPlane->gridAttributes(Qt::Horizontal); ga.setGridVisible(config.isChartSVGridLines()); cartesianPlane->setGridAttributes(Qt::Horizontal, ga); // set-up data range cartesianPlane->setVerticalRange(qMakePair(config.isDataUserDefined() ? loc.toDouble(config.dataRangeStart()) : 0.0, config.isDataUserDefined() ? loc.toDouble(config.dataRangeEnd()) : 0.0)); //set-up x axis CartesianAxis *xAxis = new CartesianAxis(); xAxis->setPosition(CartesianAxis::Bottom); xAxis->setTitleText(i18n("Time")); TextAttributes xAxisTitleTextAttr(xAxis->titleTextAttributes()); xAxisTitleTextAttr.setMinimalFontSize(QFontDatabase::systemFont(QFontDatabase::GeneralFont).pointSize()); xAxisTitleTextAttr.setPen(m_foregroundBrush.color()); xAxis->setTitleTextAttributes(xAxisTitleTextAttr); TextAttributes xAxisTextAttr(xAxis->textAttributes()); xAxisTextAttr.setPen(m_foregroundBrush.color()); xAxis->setTextAttributes(xAxisTextAttr); RulerAttributes xAxisRulerAttr(xAxis->rulerAttributes()); xAxisRulerAttr.setTickMarkPen(m_foregroundBrush.color()); xAxisRulerAttr.setShowRulerLine(true); xAxis->setRulerAttributes(xAxisRulerAttr); //set-up y axis KBalanceAxis *yAxis = new KBalanceAxis(); yAxis->setPosition(CartesianAxis::Left); if (config.isIncludingPrice()) yAxis->setTitleText(i18n("Price")); else yAxis->setTitleText(i18n("Balance")); TextAttributes yAxisTitleTextAttr(yAxis->titleTextAttributes()); yAxisTitleTextAttr.setMinimalFontSize(QFontDatabase::systemFont(QFontDatabase::GeneralFont).pointSize()); yAxisTitleTextAttr.setPen(m_foregroundBrush.color()); yAxis->setTitleTextAttributes(yAxisTitleTextAttr); TextAttributes yAxisTextAttr(yAxis->textAttributes()); yAxisTextAttr.setPen(m_foregroundBrush.color()); yAxis->setTextAttributes(yAxisTextAttr); RulerAttributes yAxisRulerAttr(yAxis->rulerAttributes()); yAxisRulerAttr.setTickMarkPen(m_foregroundBrush.color()); yAxisRulerAttr.setShowRulerLine(true); yAxis->setRulerAttributes(yAxisRulerAttr); switch (config.chartType()) { - case MyMoneyReport::eChartEnd: - case MyMoneyReport::eChartLine: { + case eMyMoney::Report::ChartType::End: + case eMyMoney::Report::ChartType::Line: { KChart::LineDiagram* diagram = new KChart::LineDiagram(this, cartesianPlane); if (config.isSkippingZero()) { LineAttributes attributes = diagram->lineAttributes(); attributes.setMissingValuesPolicy(LineAttributes::MissingValuesAreBridged); diagram->setLineAttributes(attributes); } cartesianPlane->replaceDiagram(diagram); diagram->addAxis(xAxis); diagram->addAxis(yAxis); break; } - case MyMoneyReport::eChartBar: { + case eMyMoney::Report::ChartType::Bar: { KChart::BarDiagram* diagram = new KChart::BarDiagram(this, cartesianPlane); cartesianPlane->replaceDiagram(diagram); diagram->addAxis(xAxis); diagram->addAxis(yAxis); break; } - case MyMoneyReport::eChartStackedBar: { + case eMyMoney::Report::ChartType::StackedBar: { KChart::BarDiagram* diagram = new KChart::BarDiagram(this, cartesianPlane); diagram->setType(BarDiagram::Stacked); cartesianPlane->replaceDiagram(diagram); diagram->addAxis(xAxis); diagram->addAxis(yAxis); break; } default: break; } break; } - case MyMoneyReport::eChartPie: - case MyMoneyReport::eChartRing:{ + case eMyMoney::Report::ChartType::Pie: + case eMyMoney::Report::ChartType::Ring:{ PolarCoordinatePlane* polarPlane = new PolarCoordinatePlane(this); replaceCoordinatePlane(polarPlane); // set-up grid GridAttributes ga = polarPlane->gridAttributes(true); ga.setGridVisible(config.isChartCHGridLines()); polarPlane->setGridAttributes(true, ga); ga = polarPlane->gridAttributes(false); ga.setGridVisible(config.isChartSVGridLines()); polarPlane->setGridAttributes(false, ga); setAccountSeries(false); switch (config.chartType()) { - case MyMoneyReport::eChartPie: { + case eMyMoney::Report::ChartType::Pie: { KChart::PieDiagram* diagram = new KChart::PieDiagram(this, polarPlane); polarPlane->replaceDiagram(diagram); setSeriesTotals(true); break; } - case MyMoneyReport::eChartRing: { + case eMyMoney::Report::ChartType::Ring: { KChart::RingDiagram* diagram = new KChart::RingDiagram(this, polarPlane); polarPlane->replaceDiagram(diagram); break; } default: break; } break; } default: // no valid chart types return; } //get the coordinate plane and the diagram for later use AbstractCoordinatePlane* cPlane = coordinatePlane(); AbstractDiagram* planeDiagram = cPlane->diagram(); planeDiagram->setAntiAliasing(true); //the palette - we set it here because it is a property of the diagram switch (KMyMoneySettings::chartsPalette()) { case 0: planeDiagram->useDefaultColors(); break; case 1: planeDiagram->useRainbowColors(); break; case 2: default: planeDiagram->useSubduedColors(); break; } int eBudgetDiffIdx = rowTypeList.indexOf(eBudgetDiff); QList myRowTypeList = rowTypeList; myRowTypeList.removeAt(eBudgetDiffIdx); QStringList myColumnTypeHeaderList = columnTypeHeaderList; myColumnTypeHeaderList.removeAt(eBudgetDiffIdx); int myRowTypeListSize = myRowTypeList.size(); MyMoneyFile* file = MyMoneyFile::instance(); int precision = MyMoneyMoney::denomToPrec(file->baseCurrency().smallestAccountFraction()); int rowNum = 0; QStringList legendNames; QMap legendTotal; switch (config.detailLevel()) { - case MyMoneyReport::eDetailNone: - case MyMoneyReport::eDetailAll: { + case eMyMoney::Report::DetailLevel::None: + case eMyMoney::Report::DetailLevel::All: { // iterate over outer groups PivotGrid::const_iterator it_outergroup = grid.begin(); while (it_outergroup != grid.end()) { // iterate over inner groups PivotOuterGroup::const_iterator it_innergroup = (*it_outergroup).begin(); while (it_innergroup != (*it_outergroup).end()) { // iterate over accounts PivotInnerGroup::const_iterator it_row = (*it_innergroup).begin(); while (it_row != (*it_innergroup).end()) { //Do not include investments accounts in the chart because they are merely container of stock and other accounts if (it_row.key().accountType() != eMyMoney::Account::Type::Investment) { // get displayed precision int currencyPrecision = precision; int securityPrecision = precision; if (!it_row.key().id().isEmpty()) { const MyMoneyAccount acc = file->account(it_row.key().id()); if (acc.isInvest()) { securityPrecision = file->currency(acc.currencyId()).pricePrecision(); // stock account isn't eveluated in currency, so take investment account instead currencyPrecision = MyMoneyMoney::denomToPrec(file->account(acc.parentAccountId()).fraction()); } else currencyPrecision = MyMoneyMoney::denomToPrec(acc.fraction()); } // iterate row types for (int i = 0 ; i < myRowTypeListSize; ++i) { QString legendText; //only show the column type in the header if there is more than one type if (myRowTypeListSize > 1) legendText = QString(myColumnTypeHeaderList.at(i) + QLatin1Literal(" - ") + it_row.key().name()); else legendText = it_row.key().name(); //set the legend text legendNames.append(legendText); legendTotal.insertMulti(it_row.value().value(myRowTypeList.at(i)).m_total.abs(), rowNum); precision = myRowTypeList.at(i) == ePrice ? securityPrecision : currencyPrecision; //set the cell value and tooltip rowNum = drawPivotGridRow(rowNum, it_row.value().value(myRowTypeList.at(i)), config.isChartDataLabels() ? legendText : QString(), 0, numberColumns, precision); } } ++it_row; } ++it_innergroup; } ++it_outergroup; } } break; - case MyMoneyReport::eDetailTop: { + case eMyMoney::Report::DetailLevel::Top: { // iterate over outer groups PivotGrid::const_iterator it_outergroup = grid.begin(); while (it_outergroup != grid.end()) { // iterate over inner groups PivotOuterGroup::const_iterator it_innergroup = (*it_outergroup).begin(); while (it_innergroup != (*it_outergroup).end()) { // iterate row types for (int i = 0 ; i < myRowTypeListSize; ++i) { QString legendText; //only show the column type in the header if there is more than one type if (myRowTypeListSize > 1) legendText = QString(myColumnTypeHeaderList.at(i) + QLatin1Literal(" - ") + it_innergroup.key()); else legendText = it_innergroup.key(); //set the legend text legendNames.append(legendText); legendTotal.insertMulti((*it_innergroup).m_total.value(myRowTypeList.at(i)).m_total.abs(), rowNum); //set the cell value and tooltip rowNum = drawPivotGridRow(rowNum, (*it_innergroup).m_total.value(myRowTypeList.at(i)), config.isChartDataLabels() ? legendText : QString(), 0, numberColumns, precision); } ++it_innergroup; } ++it_outergroup; } } break; - case MyMoneyReport::eDetailGroup: { + case eMyMoney::Report::DetailLevel::Group: { // iterate over outer groups PivotGrid::const_iterator it_outergroup = grid.begin(); while (it_outergroup != grid.end()) { // iterate row types for (int i = 0 ; i < myRowTypeListSize; ++i) { QString legendText; //only show the column type in the header if there is more than one type if (myRowTypeListSize > 1) legendText = QString(myColumnTypeHeaderList.at(i) + QLatin1Literal(" - ") + it_outergroup.key()); else legendText = it_outergroup.key(); //set the legend text legendNames.append(legendText); legendTotal.insertMulti((*it_outergroup).m_total.value(myRowTypeList.at(i)).m_total.abs(), rowNum); //set the cell value and tooltip rowNum = drawPivotGridRow(rowNum, (*it_outergroup).m_total.value(myRowTypeList.at(i)), config.isChartDataLabels() ? legendText : QString(), 0, numberColumns, precision); } ++it_outergroup; } //if selected, show totals too if (config.isShowingRowTotals()) { // iterate row types for (int i = 0 ; i < myRowTypeListSize; ++i) { QString legendText; //only show the column type in the header if there is more than one type if (myRowTypeListSize > 1) legendText = QString(myColumnTypeHeaderList.at(i) + QLatin1Literal(" - ") + i18nc("Total balance", "Total")); else legendText = QString(i18nc("Total balance", "Total")); //set the legend text legendNames.append(legendText); legendTotal.insertMulti(grid.m_total.value(myRowTypeList.at(i)).m_total.abs(), rowNum); //set the cell value and tooltip rowNum = drawPivotGridRow(rowNum, grid.m_total.value(myRowTypeList.at(i)), config.isChartDataLabels() ? legendText : QString(), 0, numberColumns, precision); } } } break; - case MyMoneyReport::eDetailTotal: { + case eMyMoney::Report::DetailLevel::Total: { // iterate row types for (int i = 0 ; i < myRowTypeListSize; ++i) { QString legendText; //only show the column type in the header if there is more than one type if (myRowTypeListSize > 1) legendText = QString(myColumnTypeHeaderList.at(i) + QLatin1Literal(" - ") + i18nc("Total balance", "Total")); else legendText = QString(i18nc("Total balance", "Total")); //set the legend text legendNames.append(legendText); legendTotal.insertMulti(grid.m_total.value(myRowTypeList.at(i)).m_total.abs(), rowNum); //set the cell value and tooltip if (config.isMixedTime()) { if (myRowTypeList.at(i) == eActual) rowNum = drawPivotGridRow(rowNum, grid.m_total.value(myRowTypeList.at(i)), config.isChartDataLabels() ? legendText : QString(), 0, config.currentDateColumn(), precision); else if (myRowTypeList.at(i)== eForecast) { rowNum = drawPivotGridRow(rowNum, grid.m_total.value(myRowTypeList.at(i)), config.isChartDataLabels() ? legendText : QString(), config.currentDateColumn(), numberColumns - config.currentDateColumn(), precision); } else rowNum = drawPivotGridRow(rowNum, grid.m_total.value(myRowTypeList.at(i)), config.isChartDataLabels() ? legendText : QString(), 0, numberColumns, precision); } else rowNum = drawPivotGridRow(rowNum, grid.m_total.value(myRowTypeList.at(i)), config.isChartDataLabels() ? legendText : QString(), 0, numberColumns, precision); } } break; default: - case MyMoneyReport::eDetailEnd: + case eMyMoney::Report::DetailLevel::End: return; } auto legendRows = legendTotal.values(); // list of legend rows sorted ascending by total value for (auto i = 0; i < legendRows.count(); ++i) { const auto ixRow = legendRows.count() - 1 - i; // take row with the highest total value i.e. form the bottom const auto row = legendRows.at(ixRow); if ( row != i) { // if legend isn't sorted by total value, then rearrange model if ((accountSeries() && !seriesTotals()) || (seriesTotals() && !accountSeries())) m_model.insertColumn(i, m_model.takeColumn(row)); else m_model.insertRow(i, m_model.takeRow(row)); for (auto j = i; j < ixRow; ++j) { // fix invalid indexes after above move operation if (legendRows.at(j) < row) ++legendRows[j]; } legendRows[ixRow] = i; legendNames.move(row, i); } } // Set up X axis labels (ie "abscissa" to use the technical term) if (accountSeries()) { // if not, we will set these up while putting in the chart values. QStringList xLabels; foreach (const auto colHeading, columnHeadings) xLabels.append(QString(colHeading).replace(QLatin1String(" "), QLatin1String(" "))); m_model.setVerticalHeaderLabels(xLabels); } m_model.setHorizontalHeaderLabels(legendNames); // set line width for line chart - if (config.chartType() == MyMoneyReport::eChartLine) { + if (config.chartType() == eMyMoney::Report::ChartType::Line) { AttributesModel* diagramAttributes = planeDiagram->attributesModel(); int penWidth = config.chartLineWidth(); for (int i = 0 ; i < rowNum ; ++i) { QPen pen = diagramAttributes->headerData(i, Qt::Horizontal, DatasetPenRole).value< QPen >(); pen.setWidth(penWidth); m_model.setHeaderData(i, Qt::Horizontal, qVariantFromValue(pen), DatasetPenRole); } } // set the text attributes after calling replaceLegend() otherwise fon sizes will get overwritten qreal generalFontSize = QFontDatabase::systemFont(QFontDatabase::GeneralFont).pointSizeF(); if (generalFontSize == -1) generalFontSize = 8; // this is a fallback if the fontsize was specified in pixels // the legend is needed only if at least two data sets are rendered if (qMin(static_cast(KMyMoneySettings::maximumLegendItems()), rowNum) > 1) { //the legend will be used later Legend* legend = new Legend(planeDiagram, this); legend->setTitleText(i18nc("Chart legend title", "Legend")); //set the legend basic attributes //this is done after adding the legend because the values are overridden when adding the legend to the chart const auto maxLegendItems = KMyMoneySettings::maximumLegendItems(); auto legendItems = legendNames.count(); auto i = 0; while (legendItems > maxLegendItems) { legend->setDatasetHidden(legendRows.at(i++), true); --legendItems; } legend->setUseAutomaticMarkerSize(false); FrameAttributes legendFrameAttr(legend->frameAttributes()); legendFrameAttr.setPen(m_foregroundBrush.color()); // leave some space between the content and the frame legendFrameAttr.setPadding(2); legend->setFrameAttributes(legendFrameAttr); legend->setPosition(Position::East); legend->setTextAlignment(Qt::AlignLeft); if (config.isChartDataLabels()) legend->setLegendStyle(KChart::Legend::MarkersAndLines); else legend->setLegendStyle(KChart::Legend::LinesOnly); replaceLegend(legend); TextAttributes legendTextAttr(legend->textAttributes()); legendTextAttr.setPen(m_foregroundBrush.color()); legendTextAttr.setFontSize(KChart::Measure(generalFontSize, KChartEnums::MeasureCalculationModeAbsolute)); legend->setTextAttributes(legendTextAttr); TextAttributes legendTitleTextAttr(legend->titleTextAttributes()); legendTitleTextAttr.setPen(m_foregroundBrush.color()); legendTitleTextAttr.setFontSize(KChart::Measure(generalFontSize + 4, KChartEnums::MeasureCalculationModeAbsolute)); legend->setTitleTextAttributes(legendTitleTextAttr); } //set data value attributes //make sure to show only the required number of fractional digits on the labels of the graph DataValueAttributes dataValueAttr(planeDiagram->dataValueAttributes()); MarkerAttributes markerAttr(dataValueAttr.markerAttributes()); markerAttr.setVisible(true); markerAttr.setMarkerStyle(MarkerAttributes::MarkerCircle); dataValueAttr.setMarkerAttributes(markerAttr); TextAttributes dataValueTextAttr(dataValueAttr.textAttributes()); dataValueTextAttr.setPen(m_foregroundBrush.color()); dataValueTextAttr.setFontSize(KChart::Measure(generalFontSize, KChartEnums::MeasureCalculationModeAbsolute)); dataValueAttr.setTextAttributes(dataValueTextAttr); m_precision = config.yLabelsPrecision(); dataValueAttr.setDecimalDigits(config.yLabelsPrecision()); dataValueAttr.setVisible(config.isChartDataLabels()); planeDiagram->setDataValueAttributes(dataValueAttr); planeDiagram->setAllowOverlappingDataValueTexts(true); m_model.blockSignals(blocked); // reenable dataChanged() signal //assign model to the diagram planeDiagram->setModel(&m_model); // connect needLayoutPlanes, so dimension of chart can be known, so custom Y labels can be generated connect(cPlane, SIGNAL(needLayoutPlanes()), this, SLOT(slotNeedUpdate())); } void KReportChartView::slotNeedUpdate() { disconnect(coordinatePlane(), SIGNAL(needLayoutPlanes()), this, SLOT(slotNeedUpdate())); // this won't cause hang-up in KReportsView::slotConfigure QList grids = coordinatePlane()->gridDimensionsList(); if (grids.isEmpty()) // ring and pie charts have no dimensions return; if (grids.at(1).stepWidth == 0) // no labels? return; QLocale loc = locale(); QChar separator = loc.groupSeparator(); QChar decimalPoint = loc.decimalPoint(); QStringList labels; if (m_precision > 10 || m_precision <= 0) // assure that conversion through QLocale::toString() will always work m_precision = 1; CartesianCoordinatePlane* cartesianplane = qobject_cast(coordinatePlane()); if (cartesianplane) { if (cartesianplane->axesCalcModeY() == KChart::AbstractCoordinatePlane::Logarithmic) { qreal labelValue = qFloor(log10(grids.at(1).start)); // first label is 10 to power of labelValue int labelCount = qFloor(log10(grids.at(1).end)) - qFloor(log10(grids.at(1).start)) + 1; for (auto i = 0; i < labelCount; ++i) { labels.append(loc.toString(qPow(10.0, labelValue), 'f', m_precision).remove(separator).remove(QRegularExpression("0+$")).remove(QRegularExpression("\\" + decimalPoint + "$"))); ++labelValue; // next label is 10 to power of previous exponent + 1 } } else { qreal labelValue = grids.at(1).start; // first label is start value qreal step = grids.at(1).stepWidth; int labelCount = qFloor((grids.at(1).end - grids.at(1).start) / grids.at(1).stepWidth) + 1; for (auto i = 0; i < labelCount; ++i) { labels.append(loc.toString(labelValue, 'f', m_precision).remove(separator).remove(QRegularExpression("0+$")).remove(QRegularExpression("\\" + decimalPoint + "$"))); labelValue += step; // next label is previous value + step value } } } else return; // nothing but cartesian plane is handled KChart::LineDiagram* lineDiagram = qobject_cast(coordinatePlane()->diagram()); if (lineDiagram) lineDiagram->axes().at(1)->setLabels(labels); KChart::BarDiagram* barDiagram = qobject_cast(coordinatePlane()->diagram()); if (barDiagram) barDiagram->axes().at(1)->setLabels(labels); } int KReportChartView::drawPivotGridRow(int rowNum, const PivotGridRow& gridRow, const QString& legendText, const int startColumn, const int columnsToDraw, const int precision) { // Columns QString toolTip = QString(QLatin1Literal("

%1

%2
")).arg(legendText); bool isToolTip = !legendText.isEmpty(); if (seriesTotals()) { QStandardItem* item = new QStandardItem(); double value = gridRow.m_total.toDouble(); item->setData(QVariant(value), Qt::DisplayRole); if (isToolTip) item->setToolTip(toolTip.arg(value, 0, 'f', precision)); //set the cell value if (accountSeries()) { m_model.insertRows(rowNum, 1); m_model.setItem(rowNum, 0, item); } else { m_model.insertColumns(rowNum, 1); m_model.setItem(0, rowNum, item); } } else { QList itemList; for (int i = startColumn; i < columnsToDraw; ++i) { QStandardItem* item = new QStandardItem(); if (!m_skipZero || !gridRow.at(i).isZero()) { double value = gridRow.at(i).toDouble(); item->setData(QVariant(value), Qt::DisplayRole); if (isToolTip) item->setToolTip(toolTip.arg(value, 0, 'f', precision)); } itemList.append(item); } if (accountSeries()) m_model.appendColumn(itemList); else m_model.appendRow(itemList); } return ++rowNum; } void KReportChartView::setDataCell(int row, int column, const double value, QString tip) { QMap cellMap; cellMap.insert(Qt::DisplayRole, QVariant(value)); if (!tip.isEmpty()) cellMap.insert(Qt::ToolTipRole, QVariant(tip)); const QModelIndex index = m_model.index(row, column); m_model.setItemData(index, cellMap); } /** * Justifies the model, so that the given rows and columns fit into it. */ void KReportChartView::justifyModelSize(int rows, int columns) { const int currentRows = m_model.rowCount(); const int currentCols = m_model.columnCount(); if (currentCols < columns) if (! m_model.insertColumns(currentCols, columns - currentCols)) qDebug() << "justifyModelSize: could not increase model size."; if (currentRows < rows) if (! m_model.insertRows(currentRows, rows - currentRows)) qDebug() << "justifyModelSize: could not increase model size."; Q_ASSERT(m_model.rowCount() >= rows); Q_ASSERT(m_model.columnCount() >= columns); } void KReportChartView::setLineWidth(const int lineWidth) { LineDiagram* lineDiagram = qobject_cast(coordinatePlane()->diagram()); if (lineDiagram) { QList pens; pens = lineDiagram->datasetPens(); for (int i = 0; i < pens.count(); ++i) { pens[i].setWidth(lineWidth); lineDiagram->setPen(i, pens.at(i)); } } } void KReportChartView::drawLimitLine(const double limit) { if (coordinatePlane()->diagram()->datasetDimension() != 1) return; // temporarily disconnect the view from the model to aovid update of view on // emission of the dataChanged() signal for each call of setDataCell(). // This speeds up the runtime of drawLimitLine() by a factor of // approx. 60 on my box (1831ms vs. 31ms). AbstractDiagram* planeDiagram = coordinatePlane()->diagram(); planeDiagram->setModel(0); //we get the current number of rows and we add one after that int row = m_model.rowCount(); justifyModelSize(m_numColumns, row + 1); for (int col = 0; col < m_numColumns; ++col) { setDataCell(col, row, limit); } planeDiagram->setModel(&m_model); //TODO: add format to the line } void KReportChartView::removeLegend() { Legend* chartLegend = Chart::legend(); delete chartLegend; } diff --git a/kmymoney/reports/listtable.cpp b/kmymoney/reports/listtable.cpp index 3f0b02321..f27d85a42 100644 --- a/kmymoney/reports/listtable.cpp +++ b/kmymoney/reports/listtable.cpp @@ -1,742 +1,742 @@ /* * Copyright 2004-2005 Ace Jones * Copyright 2008-2011 Alvaro Soliverez * 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 "listtable.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes // This is just needed for i18n(). Once I figure out how to handle i18n // without using this macro directly, I'll be freed of KDE dependency. // ---------------------------------------------------------------------------- // Project Includes #include "mymoneymoney.h" #include "mymoneyfile.h" #include "mymoneyaccount.h" #include "mymoneysecurity.h" #include "mymoneysplit.h" #include "mymoneytransaction.h" #include "mymoneyreport.h" #include "kmymoneysettings.h" #include "mymoneyenums.h" namespace reports { QVector ListTable::TableRow::m_sortCriteria; // **************************************************************************** // // ListTable implementation // // **************************************************************************** bool ListTable::TableRow::operator< (const TableRow& _compare) const { bool result = false; foreach (const auto criterion, m_sortCriteria) { if (this->operator[](criterion) < _compare[criterion]) { result = true; break; } else if (this->operator[](criterion) > _compare[criterion]) { break; } } return result; } // needed for KDE < 3.2 implementation of qHeapSort bool ListTable::TableRow::operator<= (const TableRow& _compare) const { return (!(_compare < *this)); } bool ListTable::TableRow::operator== (const TableRow& _compare) const { return (!(*this < _compare) && !(_compare < *this)); } bool ListTable::TableRow::operator> (const TableRow& _compare) const { return (_compare < *this); } /** * TODO * * - Collapse 2- & 3- groups when they are identical * - Way more test cases (especially splits & transfers) * - Option to collapse splits * - Option to exclude transfers * */ ListTable::ListTable(const MyMoneyReport& _report): ReportTable(_report) { } void ListTable::render(QString& result, QString& csv) const { MyMoneyFile* file = MyMoneyFile::instance(); result.clear(); csv.clear(); // retrieve the configuration parameters from the report definition. // the things that we care about for query reports are: // how to group the rows, what columns to display, and what field // to subtotal on QList columns = m_columns; if (!m_subtotal.isEmpty() && m_subtotal.count() == 1) // constructPerformanceRow has subtotal columns already in columns columns.append(m_subtotal); QList postcolumns = m_postcolumns; if (!m_postcolumns.isEmpty()) // prevent creation of empty column columns.append(postcolumns); result.append(QLatin1String("\n")); // // Table header // foreach (const auto cellType, columns) { result.append(QString::fromLatin1("").arg(tableHeader(cellType))); csv.append(tableHeader(cellType) + QLatin1Char(',')); } csv.chop(1); // remove last ',' character result.append(QLatin1String("\n")); csv.append(QLatin1Char('\n')); // initialize group names to empty, so any group will have to display its header QStringList prevGrpNames; for (int i = 0; i < m_group.count(); ++i) { prevGrpNames.append(QString()); } // // Rows // bool row_odd = true; bool isLowestGroupTotal = true; // hack to inform whether to put separator line or not // ***DV*** MyMoneyMoney startingBalance; MyMoneyMoney balanceChange = MyMoneyMoney(); for (QList::ConstIterator it_row = m_rows.constBegin(); it_row != m_rows.constEnd(); ++it_row) { /* rank can be: * 0 - opening balance * 1 - major split of transaction * 2 - minor split of transaction * 3 - closing balance * 4 - first totals row * 5 - middle totals row */ const int rowRank = (*it_row).value(ctRank).toInt(); // detect whether any of groups changed and display new group header in that case for (int i = 0; i < m_group.count(); ++i) { QString curGrpName = (*it_row).value(m_group.at(i)); if (curGrpName.isEmpty()) // it could be grand total continue; if (prevGrpNames.at(i) != curGrpName) { // group header of lowest group doesn't bring any useful information // if hide transaction is enabled, so don't display it int lowestGroup = m_group.count() - 1; if (!m_config.isHideTransactions() || i != lowestGroup) { row_odd = true; result.append(QString::fromLatin1("" "\n").arg(QString::number(i), QString::number(columns.count()), curGrpName)); csv.append(QString::fromLatin1("\"%1\"\n").arg(curGrpName)); } if (i == lowestGroup) // lowest group has been switched... isLowestGroupTotal = true; // ...so expect lowest group total prevGrpNames.replace(i, curGrpName); } } bool need_label = true; QString tlink; // link information to account and transaction if (!m_config.isHideTransactions() || rowRank == 4 || rowRank == 5) { // if hide transaction is enabled display only total rows i.e. rank = 4 || rank = 5 if (rowRank == 0 || rowRank == 3) { // skip the opening and closing balance row, // if the balance column is not shown // rank = 0 for opening balance, rank = 3 for closing balance if (!columns.contains(ctBalance)) continue; result.append(QString::fromLatin1("").arg((*it_row).value(ctID))); // ***DV*** } else if (rowRank == 1) { row_odd = ! row_odd; tlink = QString::fromLatin1("id=%1&tid=%2").arg((*it_row).value(ctAccountID), (*it_row).value(ctID)); result.append(QString::fromLatin1("").arg(row_odd ? QLatin1String("odd") : QLatin1String("even"))); } else if (rowRank == 2) { result.append(QString::fromLatin1("").arg(row_odd ? QLatin1Char('1') : QLatin1Char('0'))); } else if (rowRank == 4 || rowRank == 5) { QList::const_iterator nextRow = std::next(it_row); - if ((m_config.rowType() == MyMoneyReport::eTag)) { //If we order by Tags don't show the Grand total as we can have multiple tags per transaction + if ((m_config.rowType() == eMyMoney::Report::RowType::Tag)) { //If we order by Tags don't show the Grand total as we can have multiple tags per transaction continue; } else if (rowRank == 4) { if (nextRow != m_rows.end()) { if (isLowestGroupTotal && m_config.isHideTransactions()) { result.append(QLatin1String("")); isLowestGroupTotal = false; } else if ((*nextRow).value(ctRank) == QLatin1String("5")) { result.append(QLatin1String("")); } else { result.append(QLatin1String("")); } } else { result.append(QLatin1String("")); } } else if (rowRank == 5) { if (nextRow != m_rows.end()) { if ((*nextRow).value(ctRank) == QLatin1String("5")) result.append(QLatin1String("")); else result.append(QLatin1String("")); } }/* else { dead code result.append(QLatin1String("")); }*/ } else { result.append(QString::fromLatin1("").arg(row_odd ? QLatin1String("odd") : QLatin1String("even"))); } } else { continue; } // // Columns // QList::ConstIterator it_column = columns.constBegin(); while (it_column != columns.constEnd()) { QString data = (*it_row).value(*it_column); // ***DV*** if (rowRank == 2) { if (*it_column == ctValue) data = (*it_row).value(ctSplit); else if (*it_column == ctPostDate || *it_column == ctNumber || *it_column == ctPayee || *it_column == ctTag || *it_column == ctAction || *it_column == ctShares || *it_column == ctPrice || *it_column == ctNextDueDate || *it_column == ctBalance || *it_column == ctAccount || *it_column == ctName) data.clear(); } // ***DV*** else if (rowRank == 0 || rowRank == 3) { if (*it_column == ctBalance) { data = (*it_row).value(ctBalance); if ((*it_row).value(ctID) == QLatin1String("A")) { // opening balance? startingBalance = MyMoneyMoney(data); balanceChange = MyMoneyMoney(); } } if (need_label) { if ((*it_column == ctPayee) || (*it_column == ctCategory) || (*it_column == ctMemo)) { if (!(*it_row).value(ctShares).isEmpty()) { data = ((*it_row).value(ctID) == QLatin1String("A")) ? i18n("Initial Market Value") : i18n("Ending Market Value"); } else { data = ((*it_row).value(ctID) == QLatin1String("A")) ? i18n("Opening Balance") : i18n("Closing Balance"); } need_label = false; } } } // The 'balance' column is calculated at render-time // but not printed on split lines else if (*it_column == ctBalance && rowRank == 1) { // Take the balance off the deepest group iterator balanceChange += MyMoneyMoney((*it_row).value(ctValue, QLatin1String("0"))); data = (balanceChange + startingBalance).toString(); } else if ((rowRank == 4 || rowRank == 5)) { // display total title but only if first column doesn't contain any data if (it_column == columns.constBegin() && data.isEmpty()) { result.append(QString::fromLatin1("")); ++it_column; continue; } else if (!m_subtotal.contains(*it_column)) { // don't display e.g. account in totals row result.append(QLatin1String("")); ++it_column; continue; } } // Figure out how to render the value in this column, depending on // what its properties are. // // TODO: This and the i18n headings are handled // as a set of parallel vectors. Would be much better to make a single // vector of a properties class. QString tlinkBegin, tlinkEnd; if (!tlink.isEmpty()) { tlinkBegin = QString::fromLatin1("").arg(tlink); tlinkEnd = QLatin1String(""); } QString currencyID = (*it_row).value(ctCurrency); if (currencyID.isEmpty()) currencyID = file->baseCurrency().id(); int fraction = file->currency(currencyID).smallestAccountFraction(); if (m_config.isConvertCurrency()) // don't show currency id, if there is only single currency currencyID.clear(); switch (cellGroup(*it_column)) { case cgMoney: if (data.isEmpty()) { result.append(QString::fromLatin1("") .arg((*it_column == ctValue) ? QLatin1String(" class=\"value\"") : QString())); csv.append(QLatin1String("\"\",")); } else if (MyMoneyMoney(data) == MyMoneyMoney::autoCalc) { result.append(QString::fromLatin1("%3%2%4") .arg((*it_column == ctValue) ? QLatin1String(" class=\"value\"") : QString(), i18n("Calculated"), tlinkBegin, tlinkEnd)); csv.append(QString::fromLatin1("\"%1\",").arg(i18n("Calculated"))); } else { auto value = MyMoneyMoney(data); auto valueStr = value.formatMoney(fraction); csv.append(QString::fromLatin1("\"%1 %2\",") .arg(currencyID, valueStr)); QString colorBegin; QString colorEnd; if ((rowRank == 4 || rowRank == 5) && value.isNegative()) { colorBegin = QString::fromLatin1("").arg(KMyMoneySettings::schemeColor(SchemeColor::Negative).name()); colorEnd = QLatin1String(""); } result.append(QString::fromLatin1("%4%6%2 %3%7%5") .arg((*it_column == ctValue) ? QLatin1String(" class=\"value\"") : QString(), currencyID, valueStr, tlinkBegin, tlinkEnd, colorBegin, colorEnd)); } break; case cgPercent: if (data.isEmpty()) { result.append(QLatin1String("")); csv.append(QLatin1String("\"\",")); } else { auto value = MyMoneyMoney(data) * MyMoneyMoney(100, 1); auto valueStr = value.formatMoney(fraction); csv.append(QString::fromLatin1("%1%,").arg(valueStr)); QString colorBegin; QString colorEnd; if ((rowRank == 4 || rowRank == 5) && value.isNegative()) { colorBegin = QString::fromLatin1("").arg(KMyMoneySettings::schemeColor(SchemeColor::Negative).name()); colorEnd = QLatin1String(""); } if ((rowRank == 4 || rowRank == 5) && value.isNegative()) valueStr = QString::fromLatin1("%2") .arg(KMyMoneySettings::schemeColor(SchemeColor::Negative).name(), valueStr); result.append(QString::fromLatin1("").arg(valueStr, tlinkBegin, tlinkEnd, colorBegin, colorEnd)); } break; case cgPrice: { int pricePrecision = file->security(file->account((*it_row).value(ctAccountID)).currencyId()).pricePrecision(); result.append(QString::fromLatin1("") .arg(MyMoneyMoney(data).formatMoney(QString(), pricePrecision), currencyID, tlinkBegin, tlinkEnd)); csv.append(QString::fromLatin1("\"%1 %2\",").arg(currencyID, MyMoneyMoney(data).formatMoney(QString(), pricePrecision, false))); } break; case cgShares: if (data.isEmpty()) { result.append(QLatin1String("")); csv.append(QLatin1String("\"\",")); } else { int sharesPrecision = MyMoneyMoney::denomToPrec(file->security(file->account((*it_row).value(ctAccountID)).currencyId()).smallestAccountFraction()); result += QString::fromLatin1("").arg(MyMoneyMoney(data).formatMoney(QString(), sharesPrecision), tlinkBegin, tlinkEnd); csv.append(QString::fromLatin1("\"%1\",").arg(MyMoneyMoney(data).formatMoney(QString(), sharesPrecision, false))); } break; case cgDate: // do this before we possibly change data csv.append(QString::fromLatin1("\"%1\",").arg(data)); // if we have a locale() then use its date formatter if (!data.isEmpty()) { QDate qd = QDate::fromString(data, Qt::ISODate); data = QLocale().toString(qd, QLocale::ShortFormat); } result.append(QString::fromLatin1("").arg(data, tlinkBegin, tlinkEnd, QString::number(prevGrpNames.count() - 1))); break; default: result.append(QString::fromLatin1("").arg(data, tlinkBegin, tlinkEnd, QString::number(prevGrpNames.count() - 1))); csv.append(QString::fromLatin1("\"%1\",").arg(data)); break; } ++it_column; tlink.clear(); } result.append(QLatin1String("\n")); csv.chop(1); // remove final comma csv.append(QLatin1Char('\n')); } result.append(QLatin1String("
%1
%3
").arg((*it_row).value(ctDepth))); if (rowRank == 4) { if (!(*it_row).value(ctDepth).isEmpty()) result += i18nc("Total balance", "Total") + QLatin1Char(' ') + prevGrpNames.at((*it_row).value(ctDepth).toInt()); else result += i18n("Grand Total"); } result.append(QLatin1String("%2%4%1%%5%3%3%2 %1%4%2%1%3%2%1%3%2%1%3
\n")); } QString ListTable::renderHTML() const { QString html, csv; render(html, csv); return html; } QString ListTable::renderCSV() const { QString html, csv; render(html, csv); return csv; } void ListTable::dump(const QString& file, const QString& context) const { QFile g(file); g.open(QIODevice::WriteOnly | QIODevice::Text); if (! context.isEmpty()) QTextStream(&g) << context.arg(renderHTML()); else QTextStream(&g) << renderHTML(); g.close(); } void ListTable::includeInvestmentSubAccounts() { // if we're not in expert mode, we need to make sure // that all stock accounts for the selected investment // account are also selected. // In case we get called for a non investment only report we quit if (KMyMoneySettings::expertMode() || !m_config.isInvestmentsOnly()) { return; } // get all investment subAccountsList but do not include those with zero balance // or those which had no transactions during the timeframe of the report QStringList accountIdList; QStringList subAccountsList; MyMoneyFile* file = MyMoneyFile::instance(); // get the report account filter if (!m_config.accounts(accountIdList) && m_config.isInvestmentsOnly()) { // this will only execute if this is an investment-only report QList accountList; file->accountList(accountList); QList::const_iterator it_ma; for (it_ma = accountList.constBegin(); it_ma != accountList.constEnd(); ++it_ma) { if ((*it_ma).accountType() == eMyMoney::Account::Type::Investment) { accountIdList.append((*it_ma).id()); } } } foreach (const auto sAccount, accountIdList) { auto acc = file->account(sAccount); if (acc.accountType() == eMyMoney::Account::Type::Investment) { foreach (const auto sSubAccount, acc.accountList()) { if (!accountIdList.contains(sSubAccount)) { subAccountsList.append(sSubAccount); } } } } if (m_config.isInvestmentsOnly() && !m_config.isIncludingUnusedAccounts()) { // if the balance is not zero at the end, include the subaccount QStringList::iterator it_balance; for (it_balance = subAccountsList.begin(); it_balance != subAccountsList.end();) { if (!file->balance((*it_balance), m_config.toDate()).isZero()) { m_config.addAccount((*it_balance)); it_balance = subAccountsList.erase((it_balance)); } else { ++it_balance; } } // if there are transactions for that subaccount, include them MyMoneyTransactionFilter filter; filter.setDateFilter(m_config.fromDate(), m_config.toDate()); filter.addAccount(subAccountsList); filter.setReportAllSplits(false); QList transactions = file->transactionList(filter); QList::const_iterator it_t = transactions.constBegin(); //Check each split for a matching account for (; it_t != transactions.constEnd(); ++it_t) { const QList& splits = (*it_t).splits(); foreach (const auto split, splits) { const QString& accountId = split.accountId(); if (!split.shares().isZero() && subAccountsList.contains(accountId)) { subAccountsList.removeOne(accountId); m_config.addAccount(accountId); } } } } else { // if not an investment-only report or explicitly including unused accounts // add all investment subaccounts m_config.addAccount(subAccountsList); } } ListTable::cellGroupE ListTable::cellGroup(const cellTypeE cellType) { switch (cellType) { // the list of columns which represent money, so we can display them correctly case ctValue: case ctNetInvValue: case ctMarketValue: case ctBuys: case ctSells: case ctBuysST: case ctSellsST: case ctBuysLT: case ctSellsLT: case ctCapitalGain: case ctCapitalGainST: case ctCapitalGainLT: case ctCashIncome: case ctReinvestIncome: case ctFees: case ctInterest: case ctStartingBalance: case ctEndingBalance: case ctBalance: case ctCurrentBalance: case ctBalanceWarning: case ctMaxBalanceLimit: case ctCreditWarning: case ctMaxCreditLimit: case ctLoanAmount: case ctPeriodicPayment: case ctFinalPayment: case ctPayment: return cgMoney; case ctPrice: case ctLastPrice: case ctBuyPrice: return cgPrice; /* the list of columns which represent shares, which is like money except the transaction currency will not be displayed*/ case ctShares: return cgShares; // the list of columns which represent a percentage, so we can display them correctly case ctReturn: case ctReturnInvestment: case ctInterestRate: case ctPercentageGain: return cgPercent; // the list of columns which represent dates, so we can display them correctly case ctPostDate: case ctEntryDate: case ctNextDueDate: case ctOpeningDate: case ctNextInterestChange: return cgDate; default: break; } return cgMisc; } QString ListTable::tableHeader(const cellTypeE cellType) { switch (cellType) { case ctPostDate: return i18n("Date"); case ctValue: return i18n("Amount"); case ctNumber: return i18n("Num"); case ctPayee: return i18n("Payee"); case ctTag: return i18n("Tags"); case ctCategory: return i18n("Category"); case ctAccount: return i18n("Account"); case ctMemo: return i18n("Memo"); case ctTopCategory: return i18n("Top Category"); case ctCategoryType: return i18n("Category Type"); case ctMonth: return i18n("Month"); case ctWeek: return i18n("Week"); case ctReconcileFlag: return i18n("Reconciled"); case ctAction: return i18n("Action"); case ctShares: return i18n("Shares"); case ctPrice: return i18n("Price"); case ctLastPrice: return i18n("Last Price"); case ctBuyPrice: return i18n("Buy Price"); case ctNetInvValue: return i18n("Net Value"); case ctBuys: return i18n("Buy Value"); case ctSells: return i18n("Sell Value"); case ctBuysST: return i18n("Short-term Buy Value"); case ctSellsST: return i18n("Short-term Sell Value"); case ctBuysLT: return i18n("Long-term Buy Value"); case ctSellsLT: return i18n("Long-term Sell Value"); case ctReinvestIncome: return i18n("Dividends Reinvested"); case ctCashIncome: return i18n("Dividends Paid Out"); case ctStartingBalance: return i18n("Starting Balance"); case ctEndingBalance: return i18n("Ending Balance"); case ctMarketValue: return i18n("Market Value"); case ctReturn: return i18n("Annualized Return"); case ctReturnInvestment: return i18n("Return On Investment"); case ctFees: return i18n("Fees"); case ctInterest: return i18n("Interest"); case ctPayment: return i18n("Payment"); case ctBalance: return i18n("Balance"); case ctType: return i18n("Type"); case ctName: return i18nc("Account name", "Name"); case ctNextDueDate: return i18n("Next Due Date"); case ctOccurrence: return i18n("Occurrence"); case ctPaymentType: return i18n("Payment Method"); case ctInstitution: return i18n("Institution"); case ctDescription: return i18n("Description"); case ctOpeningDate: return i18n("Opening Date"); case ctCurrencyName: return i18n("Currency"); case ctBalanceWarning: return i18n("Balance Early Warning"); case ctMaxBalanceLimit: return i18n("Balance Max Limit"); case ctCreditWarning: return i18n("Credit Early Warning"); case ctMaxCreditLimit: return i18n("Credit Max Limit"); case ctTax: return i18n("Tax"); case ctFavorite: return i18n("Preferred"); case ctLoanAmount: return i18n("Loan Amount"); case ctInterestRate: return i18n("Interest Rate"); case ctNextInterestChange: return i18n("Next Interest Change"); case ctPeriodicPayment: return i18n("Periodic Payment"); case ctFinalPayment: return i18n("Final Payment"); case ctCurrentBalance: return i18n("Current Balance"); case ctCapitalGain: return i18n("Capital Gain"); case ctPercentageGain: return i18n("Percentage Gain"); case ctCapitalGainST: return i18n("Short-term Gain"); case ctCapitalGainLT: return i18n("Long-term Gain"); default: break; } return QLatin1String("None"); } } diff --git a/kmymoney/reports/objectinfotable.cpp b/kmymoney/reports/objectinfotable.cpp index c10d8a6fb..b3fa80534 100644 --- a/kmymoney/reports/objectinfotable.cpp +++ b/kmymoney/reports/objectinfotable.cpp @@ -1,373 +1,373 @@ /* * Copyright 2004-2005 Ace Jones * Copyright 2008-2010 Alvaro Soliverez * * 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 "objectinfotable.h" // ---------------------------------------------------------------------------- // QT Includes #include #include // ---------------------------------------------------------------------------- // KDE Includes #include // ---------------------------------------------------------------------------- // Project Includes #include "mymoneyfile.h" #include "mymoneyinstitution.h" #include "mymoneyaccount.h" #include "mymoneyaccountloan.h" #include "mymoneysecurity.h" #include "mymoneyprice.h" #include "mymoneypayee.h" #include "mymoneymoney.h" #include "mymoneysplit.h" #include "mymoneytransaction.h" #include "mymoneyreport.h" #include "mymoneyschedule.h" #include "mymoneyexception.h" #include "kmymoneyutils.h" #include "reportaccount.h" #include "mymoneyenums.h" namespace reports { // **************************************************************************** // // ObjectInfoTable implementation // // **************************************************************************** /** * TODO * * - Collapse 2- & 3- groups when they are identical * - Way more test cases (especially splits & transfers) * - Option to collapse splits * - Option to exclude transfers * */ ObjectInfoTable::ObjectInfoTable(const MyMoneyReport& _report): ListTable(_report) { // separated into its own method to allow debugging (setting breakpoints // directly in ctors somehow does not work for me (ipwizard)) // TODO: remove the init() method and move the code back to the ctor init(); } void ObjectInfoTable::init() { m_columns.clear(); m_group.clear(); m_subtotal.clear(); switch (m_config.rowType()) { - case MyMoneyReport::eSchedule: + case eMyMoney::Report::RowType::Schedule: constructScheduleTable(); m_columns << ctNextDueDate << ctName; break; - case MyMoneyReport::eAccountInfo: + case eMyMoney::Report::RowType::AccountInfo: constructAccountTable(); m_columns << ctInstitution << ctType << ctName; break; - case MyMoneyReport::eAccountLoanInfo: + case eMyMoney::Report::RowType::AccountLoanInfo: constructAccountLoanTable(); m_columns << ctInstitution << ctType << ctName; break; default: break; } // Sort the data to match the report definition m_subtotal << ctValue; switch (m_config.rowType()) { - case MyMoneyReport::eSchedule: + case eMyMoney::Report::RowType::Schedule: m_group << ctType; m_subtotal << ctValue; break; - case MyMoneyReport::eAccountInfo: - case MyMoneyReport::eAccountLoanInfo: + case eMyMoney::Report::RowType::AccountInfo: + case eMyMoney::Report::RowType::AccountLoanInfo: m_group << ctTopCategory << ctInstitution; m_subtotal << ctCurrentBalance; break; default: throw MYMONEYEXCEPTION_CSTRING("ObjectInfoTable::ObjectInfoTable(): unhandled row type"); } QVector sort = QVector::fromList(m_group) << QVector::fromList(m_columns) << ctID << ctRank; switch (m_config.rowType()) { - case MyMoneyReport::eSchedule: - if (m_config.detailLevel() == MyMoneyReport::eDetailAll) { + case eMyMoney::Report::RowType::Schedule: + if (m_config.detailLevel() == eMyMoney::Report::DetailLevel::All) { m_columns << ctName << ctPayee << ctPaymentType << ctOccurrence << ctNextDueDate << ctCategory; } else { m_columns << ctName << ctPayee << ctPaymentType << ctOccurrence << ctNextDueDate; } break; - case MyMoneyReport::eAccountInfo: + case eMyMoney::Report::RowType::AccountInfo: m_columns << ctType << ctName << ctNumber << ctDescription << ctOpeningDate << ctCurrencyName << ctBalanceWarning << ctCreditWarning << ctMaxCreditLimit << ctTax << ctFavorite; break; - case MyMoneyReport::eAccountLoanInfo: + case eMyMoney::Report::RowType::AccountLoanInfo: m_columns << ctType << ctName << ctNumber << ctDescription << ctOpeningDate << ctCurrencyName << ctPayee << ctLoanAmount << ctInterestRate << ctNextInterestChange << ctPeriodicPayment << ctFinalPayment << ctFavorite; break; default: m_columns.clear(); } TableRow::setSortCriteria(sort); qSort(m_rows); } void ObjectInfoTable::constructScheduleTable() { MyMoneyFile* file = MyMoneyFile::instance(); QList schedules; schedules = file->scheduleList(QString(), eMyMoney::Schedule::Type::Any, eMyMoney::Schedule::Occurrence::Any, eMyMoney::Schedule::PaymentType::Any, m_config.fromDate(), m_config.toDate(), false); QList::const_iterator it_schedule = schedules.constBegin(); while (it_schedule != schedules.constEnd()) { MyMoneySchedule schedule = *it_schedule; ReportAccount account(schedule.account()); if (m_config.includes(account)) { //get fraction for account int fraction = account.fraction(); //use base currency fraction if not initialized if (fraction == -1) fraction = MyMoneyFile::instance()->baseCurrency().smallestAccountFraction(); TableRow scheduleRow; //convert to base currency if needed MyMoneyMoney xr = MyMoneyMoney::ONE; if (m_config.isConvertCurrency() && account.isForeignCurrency()) { xr = account.baseCurrencyPrice(QDate::currentDate()).reduce(); } // help for sort and render functions scheduleRow[ctRank] = QLatin1Char('0'); //schedule data scheduleRow[ctID] = schedule.id(); scheduleRow[ctName] = schedule.name(); scheduleRow[ctNextDueDate] = schedule.nextDueDate().toString(Qt::ISODate); scheduleRow[ctType] = KMyMoneyUtils::scheduleTypeToString(schedule.type()); scheduleRow[ctOccurrence] = i18nc("Frequency of schedule", schedule.occurrenceToString().toLatin1()); scheduleRow[ctPaymentType] = KMyMoneyUtils::paymentMethodToString(schedule.paymentType()); //scheduleRow["category"] = account.name(); //to get the payee we must look into the splits of the transaction MyMoneyTransaction transaction = schedule.transaction(); MyMoneySplit split = transaction.splitByAccount(account.id(), true); scheduleRow[ctValue] = (split.value() * xr).toString(); MyMoneyPayee payee = file->payee(split.payeeId()); scheduleRow[ctPayee] = payee.name(); m_rows += scheduleRow; //the text matches the main split bool transaction_text = m_config.match(split); - if (m_config.detailLevel() == MyMoneyReport::eDetailAll) { + if (m_config.detailLevel() == eMyMoney::Report::DetailLevel::All) { //get the information for all splits QList splits = transaction.splits(); QList::const_iterator split_it = splits.constBegin(); for (; split_it != splits.constEnd(); ++split_it) { TableRow splitRow; ReportAccount splitAcc((*split_it).accountId()); splitRow[ctRank] = QLatin1Char('1'); splitRow[ctID] = schedule.id(); splitRow[ctName] = schedule.name(); splitRow[ctType] = KMyMoneyUtils::scheduleTypeToString(schedule.type()); splitRow[ctNextDueDate] = schedule.nextDueDate().toString(Qt::ISODate); if ((*split_it).value() == MyMoneyMoney::autoCalc) { splitRow[ctSplit] = MyMoneyMoney::autoCalc.toString(); } else if (! splitAcc.isIncomeExpense()) { splitRow[ctSplit] = (*split_it).value().toString(); } else { splitRow[ctSplit] = (- (*split_it).value()).toString(); } //if it is an assett account, mark it as a transfer if (! splitAcc.isIncomeExpense()) { splitRow[ctCategory] = ((* split_it).value().isNegative()) ? i18n("Transfer from %1" , splitAcc.fullName()) : i18n("Transfer to %1" , splitAcc.fullName()); } else { splitRow [ctCategory] = splitAcc.fullName(); } //add the split only if it matches the text or it matches the main split if (m_config.match((*split_it)) || transaction_text) m_rows += splitRow; } } } ++it_schedule; } } void ObjectInfoTable::constructAccountTable() { MyMoneyFile* file = MyMoneyFile::instance(); //make sure we have all subaccounts of investment accounts includeInvestmentSubAccounts(); QList accounts; file->accountList(accounts); QList::const_iterator it_account = accounts.constBegin(); while (it_account != accounts.constEnd()) { TableRow accountRow; ReportAccount account(*it_account); if (m_config.includes(account) && account.accountType() != eMyMoney::Account::Type::Stock && !account.isClosed()) { MyMoneyMoney value; accountRow[ctRank] = QLatin1Char('0'); accountRow[ctTopCategory] = MyMoneyAccount::accountTypeToString(account.accountGroup()); accountRow[ctInstitution] = (file->institution(account.institutionId())).name(); accountRow[ctType] = MyMoneyAccount::accountTypeToString(account.accountType()); accountRow[ctName] = account.name(); accountRow[ctNumber] = account.number(); accountRow[ctDescription] = account.description(); accountRow[ctOpeningDate] = account.openingDate().toString(Qt::ISODate); //accountRow["currency"] = (file->currency(account.currencyId())).tradingSymbol(); accountRow[ctCurrencyName] = (file->currency(account.currencyId())).name(); accountRow[ctBalanceWarning] = account.value("minBalanceEarly"); accountRow[ctMaxBalanceLimit] = account.value("minBalanceAbsolute"); accountRow[ctCreditWarning] = account.value("maxCreditEarly"); accountRow[ctMaxCreditLimit] = account.value("maxCreditAbsolute"); accountRow[ctTax] = account.value("Tax") == QLatin1String("Yes") ? i18nc("Is this a tax account?", "Yes") : QString(); accountRow[ctOpeningBalance] = account.value("OpeningBalanceAccount") == QLatin1String("Yes") ? i18nc("Is this an opening balance account?", "Yes") : QString(); accountRow[ctFavorite] = account.value("PreferredAccount") == QLatin1String("Yes") ? i18nc("Is this a favorite account?", "Yes") : QString(); //investment accounts show the balances of all its subaccounts if (account.accountType() == eMyMoney::Account::Type::Investment) { value = investmentBalance(account); } else { value = file->balance(account.id()); } //convert to base currency if needed if (m_config.isConvertCurrency() && account.isForeignCurrency()) { MyMoneyMoney xr = account.baseCurrencyPrice(QDate::currentDate()).reduce(); value = value * xr; } accountRow[ctCurrentBalance] = value.toString(); m_rows += accountRow; } ++it_account; } } void ObjectInfoTable::constructAccountLoanTable() { MyMoneyFile* file = MyMoneyFile::instance(); QList accounts; file->accountList(accounts); QList::const_iterator it_account = accounts.constBegin(); while (it_account != accounts.constEnd()) { TableRow accountRow; ReportAccount account(*it_account); MyMoneyAccountLoan loan = *it_account; if (m_config.includes(account) && account.isLoan() && !account.isClosed()) { //convert to base currency if needed MyMoneyMoney xr = MyMoneyMoney::ONE; if (m_config.isConvertCurrency() && account.isForeignCurrency()) { xr = account.baseCurrencyPrice(QDate::currentDate()).reduce(); } accountRow[ctRank] = QLatin1Char('0'); accountRow[ctTopCategory] = MyMoneyAccount::accountTypeToString(account.accountGroup()); accountRow[ctInstitution] = (file->institution(account.institutionId())).name(); accountRow[ctType] = MyMoneyAccount::accountTypeToString(account.accountType()); accountRow[ctName] = account.name(); accountRow[ctNumber] = account.number(); accountRow[ctDescription] = account.description(); accountRow[ctOpeningDate] = account.openingDate().toString(Qt::ISODate); //accountRow["currency"] = (file->currency(account.currencyId())).tradingSymbol(); accountRow[ctCurrencyName] = (file->currency(account.currencyId())).name(); accountRow[ctPayee] = file->payee(loan.payee()).name(); accountRow[ctLoanAmount] = (loan.loanAmount() * xr).toString(); accountRow[ctInterestRate] = (loan.interestRate(QDate::currentDate()) / MyMoneyMoney(100, 1) * xr).toString(); accountRow[ctNextInterestChange] = loan.nextInterestChange().toString(Qt::ISODate); accountRow[ctPeriodicPayment] = (loan.periodicPayment() * xr).toString(); accountRow[ctFinalPayment] = (loan.finalPayment() * xr).toString(); accountRow[ctFavorite] = account.value("PreferredAccount") == QLatin1String("Yes") ? i18nc("Is this a favorite account?", "Yes") : QString(); MyMoneyMoney value = file->balance(account.id()); value = value * xr; accountRow[ctCurrentBalance] = value.toString(); m_rows += accountRow; } ++it_account; } } MyMoneyMoney ObjectInfoTable::investmentBalance(const MyMoneyAccount& acc) { MyMoneyFile* file = MyMoneyFile::instance(); MyMoneyMoney value = file->balance(acc.id()); foreach (const auto sAccount, acc.accountList()) { auto stock = file->account(sAccount); try { MyMoneyMoney val; MyMoneyMoney balance = file->balance(stock.id()); MyMoneySecurity security = file->security(stock.currencyId()); const MyMoneyPrice &price = file->price(stock.currencyId(), security.tradingCurrency()); val = balance * price.rate(security.tradingCurrency()); // adjust value of security to the currency of the account MyMoneySecurity accountCurrency = file->currency(acc.currencyId()); val = val * file->price(security.tradingCurrency(), accountCurrency.id()).rate(accountCurrency.id()); val = val.convert(acc.fraction()); value += val; } catch (const MyMoneyException &e) { qWarning("%s", qPrintable(QString("cannot convert stock balance of %1 to base currency: %2").arg(stock.name(), e.what()))); } } return value; } } diff --git a/kmymoney/reports/pivottable.cpp b/kmymoney/reports/pivottable.cpp index 29a296d41..e7256bf41 100644 --- a/kmymoney/reports/pivottable.cpp +++ b/kmymoney/reports/pivottable.cpp @@ -1,2355 +1,2355 @@ /* * Copyright 2005-2006 Ace Jones * Copyright 2005-2018 Thomas Baumgart * Copyright 2007-2014 Alvaro Soliverez * * 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 "pivottable.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include // ---------------------------------------------------------------------------- // Project Includes #include "pivotgrid.h" #include "reportdebug.h" #include "kreportchartview.h" #include "kmymoneysettings.h" #include "kmymoneyutils.h" #include "mymoneyforecast.h" #include "mymoneyprice.h" #include "mymoneyfile.h" #include "mymoneysecurity.h" #include "mymoneybudget.h" #include "mymoneyreport.h" #include "mymoneyschedule.h" #include "mymoneysplit.h" #include "mymoneytransaction.h" #include "mymoneyexception.h" #include "mymoneyenums.h" namespace KChart { class Widget; } namespace reports { using KChart::Widget; QString Debug::m_sTabs; bool Debug::m_sEnabled = DEBUG_ENABLED_BY_DEFAULT; QString Debug::m_sEnableKey; Debug::Debug(const QString& _name): m_methodName(_name), m_enabled(m_sEnabled) { if (!m_enabled && _name == m_sEnableKey) m_enabled = true; if (m_enabled) { qDebug("%s%s(): ENTER", qPrintable(m_sTabs), qPrintable(m_methodName)); m_sTabs.append("--"); } } Debug::~Debug() { if (m_enabled) { m_sTabs.remove(0, 2); qDebug("%s%s(): EXIT", qPrintable(m_sTabs), qPrintable(m_methodName)); if (m_methodName == m_sEnableKey) m_enabled = false; } } void Debug::output(const QString& _text) { if (m_enabled) qDebug("%s%s(): %s", qPrintable(m_sTabs), qPrintable(m_methodName), qPrintable(_text)); } PivotTable::PivotTable(const MyMoneyReport& _report): ReportTable(_report), m_runningSumsCalculated(false) { init(); } void PivotTable::init() { DEBUG_ENTER(Q_FUNC_INFO); // // Initialize locals // MyMoneyFile* file = MyMoneyFile::instance(); // // Initialize member variables // //make sure we have all subaccounts of investment accounts includeInvestmentSubAccounts(); m_config.validDateRange(m_beginDate, m_endDate); // If we need to calculate running sums, it does not make sense // to show a row total column if (m_config.isRunningSum()) m_config.setShowingRowTotals(false); if (m_config.isRunningSum() && !m_config.isIncludingPrice() && !m_config.isIncludingAveragePrice() && !m_config.isIncludingMovingAverage()) m_startColumn = 1; else m_startColumn = 0; m_numColumns = columnValue(m_endDate) - columnValue(m_beginDate) + 1 + m_startColumn; // 1 for m_beginDate values and m_startColumn for opening balance values //Load what types of row the report is going to show loadRowTypeList(); // // Initialize outer groups of the grid // - if (m_config.rowType() == MyMoneyReport::eAssetLiability) { + if (m_config.rowType() == eMyMoney::Report::RowType::AssetLiability) { m_grid.insert(MyMoneyAccount::accountTypeToString(eMyMoney::Account::Type::Asset), PivotOuterGroup(m_numColumns)); m_grid.insert(MyMoneyAccount::accountTypeToString(eMyMoney::Account::Type::Liability), PivotOuterGroup(m_numColumns, PivotOuterGroup::m_kDefaultSortOrder, true /* inverted */)); } else { m_grid.insert(MyMoneyAccount::accountTypeToString(eMyMoney::Account::Type::Income), PivotOuterGroup(m_numColumns, PivotOuterGroup::m_kDefaultSortOrder - 2)); m_grid.insert(MyMoneyAccount::accountTypeToString(eMyMoney::Account::Type::Expense), PivotOuterGroup(m_numColumns, PivotOuterGroup::m_kDefaultSortOrder - 1, true /* inverted */)); // // Create rows for income/expense reports with all accounts included // if (m_config.isIncludingUnusedAccounts()) createAccountRows(); } // // Initialize grid totals // m_grid.m_total = PivotGridRowSet(m_numColumns); // // Get opening balances // Only net worth report qualifies if (m_startColumn == 1) calculateOpeningBalances(); // // Calculate budget mapping // (for budget reports only) // if (m_config.hasBudget()) calculateBudgetMapping(); // prices report doesn't need transactions, but it needs account stub // otherwise fillBasePriceUnit won't do nothing if (m_config.isIncludingPrice() || m_config.isIncludingAveragePrice()) { QList accounts; file->accountList(accounts); foreach (const auto acc, accounts) { if (acc.isInvest()) { const ReportAccount repAcc(acc); if (m_config.includes(repAcc)) { const auto outergroup = acc.accountTypeToString(acc.accountType()); assignCell(outergroup, repAcc, 0, MyMoneyMoney(), false, false); // add account stub } } } } else { // // Populate all transactions into the row/column pivot grid // QList transactions; m_config.setReportAllSplits(false); m_config.setConsiderCategory(true); try { transactions = file->transactionList(m_config); } catch (const MyMoneyException &e) { qDebug("ERR: %s", e.what()); throw; } DEBUG_OUTPUT(QString("Found %1 matching transactions").arg(transactions.count())); // Include scheduled transactions if required if (m_config.isIncludingSchedules()) { // Create a custom version of the report filter, excluding date // We'll use this to compare the transaction against MyMoneyTransactionFilter schedulefilter(m_config); schedulefilter.setDateFilter(QDate(), QDate()); // Get the real dates from the config filter QDate configbegin, configend; m_config.validDateRange(configbegin, configend); QList schedules = file->scheduleList(); QList::const_iterator it_schedule = schedules.constBegin(); while (it_schedule != schedules.constEnd()) { // If the transaction meets the filter MyMoneyTransaction tx = (*it_schedule).transaction(); if (!(*it_schedule).isFinished() && schedulefilter.match(tx)) { // Keep the id of the schedule with the transaction so that // we can do the autocalc later on in case of a loan payment tx.setValue("kmm-schedule-id", (*it_schedule).id()); // Get the dates when a payment will be made within the report window QDate nextpayment = (*it_schedule).adjustedNextPayment(configbegin); if (nextpayment.isValid()) { // Add one transaction for each date QList paymentDates = (*it_schedule).paymentDates(nextpayment, configend); QList::const_iterator it_date = paymentDates.constBegin(); while (it_date != paymentDates.constEnd()) { //if the payment occurs in the past, enter it tomorrow if (QDate::currentDate() >= *it_date) { tx.setPostDate(QDate::currentDate().addDays(1)); } else { tx.setPostDate(*it_date); } if (tx.postDate() <= configend && tx.postDate() >= configbegin) { transactions += tx; } DEBUG_OUTPUT(QString("Added transaction for schedule %1 on %2").arg((*it_schedule).id()).arg((*it_date).toString())); ++it_date; } } } ++it_schedule; } } // whether asset & liability transactions are actually to be considered // transfers - bool al_transfers = (m_config.rowType() == MyMoneyReport::eExpenseIncome) && (m_config.isIncludingTransfers()); + bool al_transfers = (m_config.rowType() == eMyMoney::Report::RowType::ExpenseIncome) && (m_config.isIncludingTransfers()); //this is to store balance for loan accounts when not included in the report QMap loanBalances; QList::const_iterator it_transaction = transactions.constBegin(); int colofs = columnValue(m_beginDate) - m_startColumn; while (it_transaction != transactions.constEnd()) { MyMoneyTransaction tx = (*it_transaction); QDate postdate = tx.postDate(); if (postdate < m_beginDate) { qDebug("MyMoneyFile::transactionList returned a transaction that is outside the date filter, skipping it"); ++it_transaction; continue; } int column = columnValue(postdate) - colofs; // check if we need to call the autocalculation routine if (tx.isLoanPayment() && tx.hasAutoCalcSplit() && (tx.value("kmm-schedule-id").length() > 0)) { // make sure to consider any autocalculation for loan payments MyMoneySchedule sched = file->schedule(tx.value("kmm-schedule-id")); const MyMoneySplit& split = tx.amortizationSplit(); if (!split.id().isEmpty()) { ReportAccount splitAccount(file->account(split.accountId())); eMyMoney::Account::Type type = splitAccount.accountGroup(); QString outergroup = MyMoneyAccount::accountTypeToString(type); //if the account is included in the report, calculate the balance from the cells if (m_config.includes(splitAccount)) { loanBalances[splitAccount.id()] = cellBalance(outergroup, splitAccount, column, false); } else { //if it is not in the report and also not in loanBalances, get the balance from the file if (!loanBalances.contains(splitAccount.id())) { QDate dueDate = sched.nextDueDate(); //if the payment is overdue, use current date if (dueDate < QDate::currentDate()) dueDate = QDate::currentDate(); //get the balance from the file for the date loanBalances[splitAccount.id()] = file->balance(splitAccount.id(), dueDate.addDays(-1)); } } KMyMoneyUtils::calculateAutoLoan(sched, tx, loanBalances); //if the loan split is not included in the report, update the balance for the next occurrence if (!m_config.includes(splitAccount)) { foreach (const auto txsplit, tx.splits()) { if (txsplit.isAmortizationSplit() && txsplit.accountId() == splitAccount.id()) loanBalances[splitAccount.id()] = loanBalances[splitAccount.id()] + txsplit.shares(); } } } } QList splits = tx.splits(); QList::const_iterator it_split = splits.constBegin(); while (it_split != splits.constEnd()) { ReportAccount splitAccount((*it_split).accountId()); // Each split must be further filtered, because if even one split matches, // the ENTIRE transaction is returned with all splits (even non-matching ones) if (m_config.includes(splitAccount) && m_config.match((*it_split))) { // reverse sign to match common notation for cash flow direction, only for expense/income splits MyMoneyMoney reverse(splitAccount.isIncomeExpense() ? -1 : 1, 1); MyMoneyMoney value; // the outer group is the account class (major account type) eMyMoney::Account::Type type = splitAccount.accountGroup(); QString outergroup = MyMoneyAccount::accountTypeToString(type); value = (*it_split).shares(); bool stockSplit = tx.isStockSplit(); if (!stockSplit) { // retrieve the value in the account's underlying currency if (value != MyMoneyMoney::autoCalc) { value = value * reverse; } else { qDebug("PivotTable::PivotTable(): This must not happen"); value = MyMoneyMoney(); // keep it 0 so far } // Except in the case of transfers on an income/expense report if (al_transfers && (type == eMyMoney::Account::Type::Asset || type == eMyMoney::Account::Type::Liability)) { outergroup = i18n("Transfers"); value = -value; } } // add the value to its correct position in the pivot table assignCell(outergroup, splitAccount, column, value, false, stockSplit); } ++it_split; } ++it_transaction; } } // // Get forecast data // if (m_config.isIncludingForecast()) calculateForecast(); // //Insert Price data // if (m_config.isIncludingPrice()) fillBasePriceUnit(ePrice); // //Insert Average Price data // if (m_config.isIncludingAveragePrice()) { fillBasePriceUnit(eActual); calculateMovingAverage(); } // // Collapse columns to match column type // if (m_config.columnPitch() > 1) collapseColumns(); // // Calculate the running sums // (for running sum reports only) // if (m_config.isRunningSum()) calculateRunningSums(); // // Calculate Moving Average // if (m_config.isIncludingMovingAverage()) calculateMovingAverage(); // // Calculate Budget Difference // if (m_config.isIncludingBudgetActuals()) calculateBudgetDiff(); // // Convert all values to the deep currency // convertToDeepCurrency(); // // Convert all values to the base currency // if (m_config.isConvertCurrency()) convertToBaseCurrency(); // // Determine column headings // calculateColumnHeadings(); // // Calculate row and column totals // calculateTotals(); // // If using mixed time, calculate column for current date // m_config.setCurrentDateColumn(currentDateColumn()); } void PivotTable::collapseColumns() { DEBUG_ENTER(Q_FUNC_INFO); int columnpitch = m_config.columnPitch(); if (columnpitch != 1) { int sourcemonth = (m_config.isColumnsAreDays()) // use the user's locale to determine the week's start ? (m_beginDate.dayOfWeek() + 8 - QLocale().firstDayOfWeek()) % 7 : m_beginDate.month(); int sourcecolumn = m_startColumn; int destcolumn = m_startColumn; while (sourcecolumn < m_numColumns) { if (sourcecolumn != destcolumn) { #if 0 // TODO: Clean up this rather inefficient kludge. We really should jump by an entire // destcolumn at a time on RS reports, and calculate the proper sourcecolumn to use, // allowing us to clear and accumulate only ONCE per destcolumn if (m_config_f.isRunningSum()) clearColumn(destcolumn); #endif accumulateColumn(destcolumn, sourcecolumn); } if (++sourcecolumn < m_numColumns) { if ((sourcemonth++ % columnpitch) == 0) { if (sourcecolumn != ++destcolumn) clearColumn(destcolumn); } } } m_numColumns = destcolumn + 1; } } void PivotTable::accumulateColumn(int destcolumn, int sourcecolumn) { DEBUG_ENTER(Q_FUNC_INFO); DEBUG_OUTPUT(QString("From Column %1 to %2").arg(sourcecolumn).arg(destcolumn)); // iterate over outer groups PivotGrid::iterator it_outergroup = m_grid.begin(); while (it_outergroup != m_grid.end()) { // iterate over inner groups PivotOuterGroup::iterator it_innergroup = (*it_outergroup).begin(); while (it_innergroup != (*it_outergroup).end()) { // iterator over rows PivotInnerGroup::iterator it_row = (*it_innergroup).begin(); while (it_row != (*it_innergroup).end()) { if ((*it_row)[eActual].count() <= sourcecolumn) throw MYMONEYEXCEPTION(QString::fromLatin1("Sourcecolumn %1 out of grid range (%2) in PivotTable::accumulateColumn").arg(sourcecolumn).arg((*it_row)[eActual].count())); if ((*it_row)[eActual].count() <= destcolumn) throw MYMONEYEXCEPTION(QString::fromLatin1("Destcolumn %1 out of grid range (%2) in PivotTable::accumulateColumn").arg(sourcecolumn).arg((*it_row)[eActual].count())); (*it_row)[eActual][destcolumn] += (*it_row)[eActual][sourcecolumn]; ++it_row; } ++it_innergroup; } ++it_outergroup; } } void PivotTable::clearColumn(int column) { DEBUG_ENTER(Q_FUNC_INFO); DEBUG_OUTPUT(QString("Column %1").arg(column)); // iterate over outer groups PivotGrid::iterator it_outergroup = m_grid.begin(); while (it_outergroup != m_grid.end()) { // iterate over inner groups PivotOuterGroup::iterator it_innergroup = (*it_outergroup).begin(); while (it_innergroup != (*it_outergroup).end()) { // iterator over rows PivotInnerGroup::iterator it_row = (*it_innergroup).begin(); while (it_row != (*it_innergroup).end()) { if ((*it_row)[eActual].count() <= column) throw MYMONEYEXCEPTION(QString::fromLatin1("Column %1 out of grid range (%2) in PivotTable::accumulateColumn").arg(column).arg((*it_row)[eActual].count())); (*it_row++)[eActual][column] = PivotCell(); } ++it_innergroup; } ++it_outergroup; } } void PivotTable::calculateColumnHeadings() { DEBUG_ENTER(Q_FUNC_INFO); // one column for the opening balance if (m_startColumn == 1) m_columnHeadings.append("Opening"); int columnpitch = m_config.columnPitch(); if (columnpitch == 0) { // output the warning but don't crash by dividing with 0 qWarning("PivotTable::calculateColumnHeadings() Invalid column pitch"); return; } // if this is a days-based report if (m_config.isColumnsAreDays()) { if (columnpitch == 1) { QDate columnDate = m_beginDate; int column = m_startColumn; while (column++ < m_numColumns) { QString heading = QLocale().monthName(columnDate.month(), QLocale::ShortFormat) + ' ' + QString::number(columnDate.day()); columnDate = columnDate.addDays(1); m_columnHeadings.append(heading); } } else { QDate day = m_beginDate; QDate prv = m_beginDate; // use the user's locale to determine the week's start int dow = (day.dayOfWeek() + 8 - QLocale().firstDayOfWeek()) % 7; while (day <= m_endDate) { if (((dow % columnpitch) == 0) || (day == m_endDate)) { m_columnHeadings.append(QString("%1 %2 - %3 %4") .arg(QLocale().monthName(prv.month(), QLocale::ShortFormat)) .arg(prv.day()) .arg(QLocale().monthName(day.month(), QLocale::ShortFormat)) .arg(day.day())); prv = day.addDays(1); } day = day.addDays(1); dow++; } } } // else it's a months-based report else { if (columnpitch == 12) { int year = m_beginDate.year(); int column = m_startColumn; while (column++ < m_numColumns) m_columnHeadings.append(QString::number(year++)); } else { int year = m_beginDate.year(); bool includeyear = (m_beginDate.year() != m_endDate.year()); int segment = (m_beginDate.month() - 1) / columnpitch; int column = m_startColumn; while (column++ < m_numColumns) { QString heading = QLocale().monthName(1 + segment * columnpitch, QLocale::ShortFormat); if (columnpitch != 1) heading += '-' + QLocale().monthName((1 + segment) * columnpitch, QLocale::ShortFormat); if (includeyear) heading += ' ' + QString::number(year); m_columnHeadings.append(heading); if (++segment >= 12 / columnpitch) { segment -= 12 / columnpitch; ++year; } } } } } void PivotTable::createAccountRows() { DEBUG_ENTER(Q_FUNC_INFO); MyMoneyFile* file = MyMoneyFile::instance(); QList accounts; file->accountList(accounts); QList::const_iterator it_account = accounts.constBegin(); while (it_account != accounts.constEnd()) { ReportAccount account(*it_account); // only include this item if its account group is included in this report // and if the report includes this account if (m_config.includes(*it_account)) { DEBUG_OUTPUT(QString("Includes account %1").arg(account.name())); // the row group is the account class (major account type) QString outergroup = MyMoneyAccount::accountTypeToString(account.accountGroup()); // place into the 'opening' column... assignCell(outergroup, account, 0, MyMoneyMoney()); } ++it_account; } } void PivotTable::calculateOpeningBalances() { DEBUG_ENTER(Q_FUNC_INFO); // First, determine the inclusive dates of the report. Normally, that's just // the begin & end dates of m_config_f. However, if either of those dates are // blank, we need to use m_beginDate and/or m_endDate instead. QDate from = m_config.fromDate(); QDate to = m_config.toDate(); if (! from.isValid()) from = m_beginDate; if (! to.isValid()) to = m_endDate; MyMoneyFile* file = MyMoneyFile::instance(); QList accounts; file->accountList(accounts); QList::const_iterator it_account = accounts.constBegin(); while (it_account != accounts.constEnd()) { ReportAccount account(*it_account); // only include this item if its account group is included in this report // and if the report includes this account if (m_config.includes(*it_account)) { //do not include account if it is closed and it has no transactions in the report period if (account.isClosed()) { //check if the account has transactions for the report timeframe MyMoneyTransactionFilter filter; filter.addAccount(account.id()); filter.setDateFilter(m_beginDate, m_endDate); filter.setReportAllSplits(false); QList transactions = file->transactionList(filter); //if a closed account has no transactions in that timeframe, do not include it if (transactions.size() == 0) { DEBUG_OUTPUT(QString("DOES NOT INCLUDE account %1").arg(account.name())); ++it_account; continue; } } DEBUG_OUTPUT(QString("Includes account %1").arg(account.name())); // the row group is the account class (major account type) QString outergroup = MyMoneyAccount::accountTypeToString(account.accountGroup()); // extract the balance of the account for the given begin date, which is // the opening balance plus the sum of all transactions prior to the begin // date // this is in the underlying currency MyMoneyMoney value = file->balance(account.id(), from.addDays(-1)); // place into the 'opening' column... assignCell(outergroup, account, 0, value); } else { DEBUG_OUTPUT(QString("DOES NOT INCLUDE account %1").arg(account.name())); } ++it_account; } } void PivotTable::calculateRunningSums(PivotInnerGroup::iterator& it_row) { MyMoneyMoney runningsum = it_row.value()[eActual][0].calculateRunningSum(MyMoneyMoney()); int column = m_startColumn; while (column < m_numColumns) { if (it_row.value()[eActual].count() <= column) throw MYMONEYEXCEPTION(QString::fromLatin1("Column %1 out of grid range (%2) in PivotTable::calculateRunningSums").arg(column).arg(it_row.value()[eActual].count())); runningsum = it_row.value()[eActual][column].calculateRunningSum(runningsum); ++column; } } void PivotTable::calculateRunningSums() { DEBUG_ENTER(Q_FUNC_INFO); m_runningSumsCalculated = true; PivotGrid::iterator it_outergroup = m_grid.begin(); while (it_outergroup != m_grid.end()) { PivotOuterGroup::iterator it_innergroup = (*it_outergroup).begin(); while (it_innergroup != (*it_outergroup).end()) { PivotInnerGroup::iterator it_row = (*it_innergroup).begin(); while (it_row != (*it_innergroup).end()) { #if 0 MyMoneyMoney runningsum = it_row.value()[0]; int column = m_startColumn; while (column < m_numColumns) { if (it_row.value()[eActual].count() <= column) throw MYMONEYEXCEPTION(QString::fromLatin1("Column %1 out of grid range (%2) in PivotTable::calculateRunningSums").arg(column).arg(it_row.value()[eActual].count())); runningsum = (it_row.value()[eActual][column] += runningsum); ++column; } #endif calculateRunningSums(it_row); ++it_row; } ++it_innergroup; } ++it_outergroup; } } MyMoneyMoney PivotTable::cellBalance(const QString& outergroup, const ReportAccount& _row, int _column, bool budget) { if (m_runningSumsCalculated) { qDebug("You must not call PivotTable::cellBalance() after calling PivotTable::calculateRunningSums()"); throw MYMONEYEXCEPTION(QString::fromLatin1("You must not call PivotTable::cellBalance() after calling PivotTable::calculateRunningSums()")); } // for budget reports, if this is the actual value, map it to the account which // holds its budget ReportAccount row = _row; if (!budget && m_config.hasBudget()) { QString newrow = m_budgetMap[row.id()]; // if there was no mapping found, then the budget report is not interested // in this account. if (newrow.isEmpty()) return MyMoneyMoney(); row = ReportAccount(newrow); } // ensure the row already exists (and its parental hierarchy) createRow(outergroup, row, true); // Determine the inner group from the top-most parent account QString innergroup(row.topParentName()); if (m_numColumns <= _column) throw MYMONEYEXCEPTION(QString::fromLatin1("Column %1 out of m_numColumns range (%2) in PivotTable::cellBalance").arg(_column).arg(m_numColumns)); if (m_grid[outergroup][innergroup][row][eActual].count() <= _column) throw MYMONEYEXCEPTION(QString::fromLatin1("Column %1 out of grid range (%2) in PivotTable::cellBalance").arg(_column).arg(m_grid[outergroup][innergroup][row][eActual].count())); MyMoneyMoney balance; if (budget) balance = m_grid[outergroup][innergroup][row][eBudget][0].cellBalance(MyMoneyMoney()); else balance = m_grid[outergroup][innergroup][row][eActual][0].cellBalance(MyMoneyMoney()); int column = m_startColumn; while (column < _column) { if (m_grid[outergroup][innergroup][row][eActual].count() <= column) throw MYMONEYEXCEPTION(QString::fromLatin1("Column %1 out of grid range (%2) in PivotTable::cellBalance").arg(column).arg(m_grid[outergroup][innergroup][row][eActual].count())); balance = m_grid[outergroup][innergroup][row][eActual][column].cellBalance(balance); ++column; } return balance; } void PivotTable::calculateBudgetMapping() { DEBUG_ENTER(Q_FUNC_INFO); MyMoneyFile* file = MyMoneyFile::instance(); // Only do this if there is at least one budget in the file if (file->countBudgets()) { // Select a budget // // It will choose the first budget in the list for the start year of the report if no budget is selected MyMoneyBudget budget = MyMoneyBudget(); QList budgets = file->budgetList(); bool validBudget = false; //check that the selected budget is valid if (m_config.budget() != "Any") { QList::const_iterator budgets_it = budgets.constBegin(); while (budgets_it != budgets.constEnd()) { //pick the budget by id if ((*budgets_it).id() == m_config.budget()) { budget = file->budget((*budgets_it).id()); validBudget = true; break; } ++budgets_it; } } //if no valid budget has been selected if (!validBudget) { //if the budget list is empty, just return if (budgets.count() == 0) { return; } QList::const_iterator budgets_it = budgets.constBegin(); while (budgets_it != budgets.constEnd()) { //pick the first budget that matches the report start year if ((*budgets_it).budgetStart().year() == QDate::currentDate().year()) { budget = file->budget((*budgets_it).id()); break; } ++budgets_it; } //if it can't find a matching budget, take the first one on the list if (budget.id().isEmpty()) { budget = budgets[0]; } //assign the budget to the report m_config.setBudget(budget.id(), m_config.isIncludingBudgetActuals()); } // Dump the budget //qDebug() << "Budget " << budget.name() << ": "; // Go through all accounts in the system to build the mapping QList accounts; file->accountList(accounts); QList::const_iterator it_account = accounts.constBegin(); while (it_account != accounts.constEnd()) { //include only the accounts selected for the report if (m_config.includes(*it_account)) { QString id = (*it_account).id(); QString acid = id; // If the budget contains this account outright if (budget.contains(id)) { // Add it to the mapping m_budgetMap[acid] = id; // qDebug() << ReportAccount(acid).debugName() << " self-maps / type =" << budget.account(id).budgetLevel(); } // Otherwise, search for a parent account which includes sub-accounts else { //if includeBudgetActuals, include all accounts regardless of whether in budget or not if (m_config.isIncludingBudgetActuals()) { m_budgetMap[acid] = id; // qDebug() << ReportAccount(acid).debugName() << " maps to " << ReportAccount(id).debugName(); } do { id = file->account(id).parentAccountId(); if (budget.contains(id)) { if (budget.account(id).budgetSubaccounts()) { m_budgetMap[acid] = id; // qDebug() << ReportAccount(acid).debugName() << " maps to " << ReportAccount(id).debugName(); break; } } } while (! id.isEmpty()); } } ++it_account; } // end while looping through the accounts in the file // Place the budget values into the budget grid QList baccounts = budget.getaccounts(); QList::const_iterator it_bacc = baccounts.constBegin(); while (it_bacc != baccounts.constEnd()) { ReportAccount splitAccount((*it_bacc).id()); //include the budget account only if it is included in the report if (m_config.includes(splitAccount)) { eMyMoney::Account::Type type = splitAccount.accountGroup(); QString outergroup = MyMoneyAccount::accountTypeToString(type); // reverse sign to match common notation for cash flow direction, only for expense/income splits MyMoneyMoney reverse((splitAccount.accountType() == eMyMoney::Account::Type::Expense) ? -1 : 1, 1); const QMap& periods = (*it_bacc).getPeriods(); // skip the account if it has no periods if (periods.count() < 1) { ++it_bacc; continue; } MyMoneyMoney value = (*periods.begin()).amount() * reverse; int column = m_startColumn; // based on the kind of budget it is, deal accordingly switch ((*it_bacc).budgetLevel()) { - case MyMoneyBudget::AccountGroup::eYearly: + case eMyMoney::Budget::Level::Yearly: // divide the single yearly value by 12 and place it in each column value /= MyMoneyMoney(12, 1); // intentional fall through - case MyMoneyBudget::AccountGroup::eNone: - case MyMoneyBudget::AccountGroup::eMax: - case MyMoneyBudget::AccountGroup::eMonthly: + case eMyMoney::Budget::Level::None: + case eMyMoney::Budget::Level::Max: + case eMyMoney::Budget::Level::Monthly: // place the single monthly value in each column of the report // only add the value if columns are monthly or longer - if (m_config.columnType() == MyMoneyReport::eBiMonths - || m_config.columnType() == MyMoneyReport::eMonths - || m_config.columnType() == MyMoneyReport::eYears - || m_config.columnType() == MyMoneyReport::eQuarters) { + if (m_config.columnType() == eMyMoney::Report::ColumnType::BiMonths + || m_config.columnType() == eMyMoney::Report::ColumnType::Months + || m_config.columnType() == eMyMoney::Report::ColumnType::Years + || m_config.columnType() == eMyMoney::Report::ColumnType::Quarters) { QDate budgetDate = budget.budgetStart(); while (column < m_numColumns && budget.budgetStart().addYears(1) > budgetDate) { //only show budget values if the budget year and the column date match //no currency conversion is done here because that is done for all columns later if (budgetDate > columnDate(column)) { ++column; } else { if (budgetDate >= m_beginDate.addDays(-m_beginDate.day() + 1) && budgetDate <= m_endDate.addDays(m_endDate.daysInMonth() - m_endDate.day()) - && budgetDate > (columnDate(column).addMonths(-m_config.columnType()))) { + && budgetDate > (columnDate(column).addMonths(-static_cast(m_config.columnType())))) { assignCell(outergroup, splitAccount, column, value, true /*budget*/); } budgetDate = budgetDate.addMonths(1); } } } break; - case MyMoneyBudget::AccountGroup::eMonthByMonth: + case eMyMoney::Budget::Level::MonthByMonth: // place each value in the appropriate column // budget periods are supposed to come in order just like columns { QMap::const_iterator it_period = periods.begin(); while (it_period != periods.end() && column < m_numColumns) { if ((*it_period).startDate() > columnDate(column)) { ++column; } else { switch (m_config.columnType()) { - case MyMoneyReport::eYears: - case MyMoneyReport::eBiMonths: - case MyMoneyReport::eQuarters: - case MyMoneyReport::eMonths: { + case eMyMoney::Report::ColumnType::Years: + case eMyMoney::Report::ColumnType::BiMonths: + case eMyMoney::Report::ColumnType::Quarters: + case eMyMoney::Report::ColumnType::Months: { if ((*it_period).startDate() >= m_beginDate.addDays(-m_beginDate.day() + 1) && (*it_period).startDate() <= m_endDate.addDays(m_endDate.daysInMonth() - m_endDate.day()) - && (*it_period).startDate() > (columnDate(column).addMonths(-m_config.columnType()))) { + && (*it_period).startDate() > (columnDate(column).addMonths(-static_cast(m_config.columnType())))) { //no currency conversion is done here because that is done for all columns later value = (*it_period).amount() * reverse; assignCell(outergroup, splitAccount, column, value, true /*budget*/); } ++it_period; break; } default: break; } } } break; } } } ++it_bacc; } } // end if there was a budget } void PivotTable::convertToBaseCurrency() { DEBUG_ENTER(Q_FUNC_INFO); MyMoneyFile* file = MyMoneyFile::instance(); int fraction = file->baseCurrency().smallestAccountFraction(); QList rowTypeList = m_rowTypeList; rowTypeList.removeOne(eAverage); PivotGrid::iterator it_outergroup = m_grid.begin(); while (it_outergroup != m_grid.end()) { PivotOuterGroup::iterator it_innergroup = (*it_outergroup).begin(); while (it_innergroup != (*it_outergroup).end()) { PivotInnerGroup::iterator it_row = (*it_innergroup).begin(); while (it_row != (*it_innergroup).end()) { auto column = 0; while (column < m_numColumns) { if (it_row.value()[eActual].count() <= column) throw MYMONEYEXCEPTION(QString::fromLatin1("Column %1 out of grid range (%2) in PivotTable::convertToBaseCurrency").arg(column).arg(it_row.value()[eActual].count())); QDate valuedate = columnDate(column); //get base price for that date MyMoneyMoney conversionfactor = it_row.key().baseCurrencyPrice(valuedate, m_config.isSkippingZero()); int pricePrecision; if (it_row.key().isInvest()) pricePrecision = file->security(it_row.key().currencyId()).pricePrecision(); else pricePrecision = MyMoneyMoney::denomToPrec(fraction); foreach (const auto rowType, rowTypeList) { //calculate base value MyMoneyMoney oldval = it_row.value()[rowType][column]; MyMoneyMoney value = (oldval * conversionfactor).reduce(); //convert to lowest fraction if (rowType == ePrice) it_row.value()[rowType][column] = PivotCell(MyMoneyMoney(value.convertPrecision(pricePrecision))); else it_row.value()[rowType][column] = PivotCell(value.convert(fraction)); DEBUG_OUTPUT_IF(conversionfactor != MyMoneyMoney::ONE , QString("Factor of %1, value was %2, now %3").arg(conversionfactor).arg(DEBUG_SENSITIVE(oldval)).arg(DEBUG_SENSITIVE(it_row.value()[rowType][column].toDouble()))); } ++column; } ++it_row; } ++it_innergroup; } ++it_outergroup; } } void PivotTable::convertToDeepCurrency() { DEBUG_ENTER(Q_FUNC_INFO); MyMoneyFile* file = MyMoneyFile::instance(); PivotGrid::iterator it_outergroup = m_grid.begin(); while (it_outergroup != m_grid.end()) { PivotOuterGroup::iterator it_innergroup = (*it_outergroup).begin(); while (it_innergroup != (*it_outergroup).end()) { PivotInnerGroup::iterator it_row = (*it_innergroup).begin(); while (it_row != (*it_innergroup).end()) { auto column = 0; while (column < m_numColumns) { if (it_row.value()[eActual].count() <= column) throw MYMONEYEXCEPTION(QString::fromLatin1("Column %1 out of grid range (%2) in PivotTable::convertToDeepCurrency").arg(column).arg(it_row.value()[eActual].count())); QDate valuedate = columnDate(column); //get conversion factor for the account and date MyMoneyMoney conversionfactor = it_row.key().deepCurrencyPrice(valuedate, m_config.isSkippingZero()); //use the fraction relevant to the account at hand int fraction = it_row.key().currency().smallestAccountFraction(); //use base currency fraction if not initialized if (fraction == -1) fraction = file->baseCurrency().smallestAccountFraction(); //convert to deep currency MyMoneyMoney oldval = it_row.value()[eActual][column]; MyMoneyMoney value = (oldval * conversionfactor).reduce(); //reduce to lowest fraction it_row.value()[eActual][column] = PivotCell(value.convert(fraction)); //convert price data if (m_config.isIncludingPrice()) { MyMoneyMoney oldPriceVal = it_row.value()[ePrice][column]; MyMoneyMoney priceValue = (oldPriceVal * conversionfactor).reduce(); it_row.value()[ePrice][column] = PivotCell(priceValue.convert(10000)); } DEBUG_OUTPUT_IF(conversionfactor != MyMoneyMoney::ONE , QString("Factor of %1, value was %2, now %3").arg(conversionfactor).arg(DEBUG_SENSITIVE(oldval)).arg(DEBUG_SENSITIVE(it_row.value()[eActual][column].toDouble()))); ++column; } ++it_row; } ++it_innergroup; } ++it_outergroup; } } void PivotTable::calculateTotals() { //insert the row type that is going to be used for (int i = 0; i < m_rowTypeList.size(); ++i) { for (int k = 0; k < m_numColumns; ++k) { m_grid.m_total[ m_rowTypeList[i] ].append(PivotCell()); } } // // Outer groups // // iterate over outer groups PivotGrid::iterator it_outergroup = m_grid.begin(); while (it_outergroup != m_grid.end()) { for (int i = 0; i < m_rowTypeList.size(); ++i) { for (int k = 0; k < m_numColumns; ++k) { (*it_outergroup).m_total[ m_rowTypeList[i] ].append(PivotCell()); } } // // Inner Groups // PivotOuterGroup::iterator it_innergroup = (*it_outergroup).begin(); while (it_innergroup != (*it_outergroup).end()) { for (int i = 0; i < m_rowTypeList.size(); ++i) { for (int k = 0; k < m_numColumns; ++k) { (*it_innergroup).m_total[ m_rowTypeList[i] ].append(PivotCell()); } } // // Rows // PivotInnerGroup::iterator it_row = (*it_innergroup).begin(); while (it_row != (*it_innergroup).end()) { // // Columns // auto column = 0; while (column < m_numColumns) { for (int i = 0; i < m_rowTypeList.size(); ++i) { if (it_row.value()[ m_rowTypeList[i] ].count() <= column) throw MYMONEYEXCEPTION(QString::fromLatin1("Column %1 out of grid range (%2) in PivotTable::calculateTotals, row columns").arg(column).arg(it_row.value()[ m_rowTypeList[i] ].count())); if ((*it_innergroup).m_total[ m_rowTypeList[i] ].count() <= column) throw MYMONEYEXCEPTION(QString::fromLatin1("Column %1 out of grid range (%2) in PivotTable::calculateTotals, inner group totals").arg(column).arg((*it_innergroup).m_total[ m_rowTypeList[i] ].count())); //calculate total MyMoneyMoney value = it_row.value()[ m_rowTypeList[i] ][column]; (*it_innergroup).m_total[ m_rowTypeList[i] ][column] += value; (*it_row)[ m_rowTypeList[i] ].m_total += value; } ++column; } ++it_row; } // // Inner Row Group Totals // auto column = 0; while (column < m_numColumns) { for (int i = 0; i < m_rowTypeList.size(); ++i) { if ((*it_innergroup).m_total[ m_rowTypeList[i] ].count() <= column) throw MYMONEYEXCEPTION(QString::fromLatin1("Column %1 out of grid range (%2) in PivotTable::calculateTotals, inner group totals").arg(column).arg((*it_innergroup).m_total[ m_rowTypeList[i] ].count())); if ((*it_outergroup).m_total[ m_rowTypeList[i] ].count() <= column) throw MYMONEYEXCEPTION(QString::fromLatin1("Column %1 out of grid range (%2) in PivotTable::calculateTotals, outer group totals").arg(column).arg((*it_innergroup).m_total[ m_rowTypeList[i] ].count())); //calculate totals MyMoneyMoney value = (*it_innergroup).m_total[ m_rowTypeList[i] ][column]; (*it_outergroup).m_total[ m_rowTypeList[i] ][column] += value; (*it_innergroup).m_total[ m_rowTypeList[i] ].m_total += value; } ++column; } ++it_innergroup; } // // Outer Row Group Totals // - const bool isIncomeExpense = (m_config.rowType() == MyMoneyReport::eExpenseIncome); + const bool isIncomeExpense = (m_config.rowType() == eMyMoney::Report::RowType::ExpenseIncome); const bool invert_total = (*it_outergroup).m_inverted; auto column = 0; while (column < m_numColumns) { for (int i = 0; i < m_rowTypeList.size(); ++i) { if (m_grid.m_total[ m_rowTypeList[i] ].count() <= column) throw MYMONEYEXCEPTION(QString::fromLatin1("Column %1 out of grid range (%2) in PivotTable::calculateTotals, grid totals").arg(column).arg((*it_innergroup).m_total[ m_rowTypeList[i] ].count())); //calculate actual totals MyMoneyMoney value = (*it_outergroup).m_total[ m_rowTypeList[i] ][column]; (*it_outergroup).m_total[ m_rowTypeList[i] ].m_total += value; //so far the invert only applies to actual and budget if (invert_total && m_rowTypeList[i] != eBudgetDiff && m_rowTypeList[i] != eForecast) value = -value; // forecast income expense reports should be inverted as oposed to asset/liability reports if (invert_total && isIncomeExpense && m_rowTypeList[i] == eForecast) value = -value; m_grid.m_total[ m_rowTypeList[i] ][column] += value; } ++column; } ++it_outergroup; } // // Report Totals // auto totalcolumn = 0; while (totalcolumn < m_numColumns) { for (int i = 0; i < m_rowTypeList.size(); ++i) { if (m_grid.m_total[ m_rowTypeList[i] ].count() <= totalcolumn) throw MYMONEYEXCEPTION(QString::fromLatin1("Total column %1 out of grid range (%2) in PivotTable::calculateTotals, grid totals").arg(totalcolumn).arg(m_grid.m_total[ m_rowTypeList[i] ].count())); //calculate actual totals MyMoneyMoney value = m_grid.m_total[ m_rowTypeList[i] ][totalcolumn]; m_grid.m_total[ m_rowTypeList[i] ].m_total += value; } ++totalcolumn; } } void PivotTable::assignCell(const QString& outergroup, const ReportAccount& _row, int column, MyMoneyMoney value, bool budget, bool stockSplit) { DEBUG_ENTER(Q_FUNC_INFO); DEBUG_OUTPUT(QString("Parameters: %1,%2,%3,%4,%5").arg(outergroup).arg(_row.debugName()).arg(column).arg(DEBUG_SENSITIVE(value.toDouble())).arg(budget)); // for budget reports, if this is the actual value, map it to the account which // holds its budget ReportAccount row = _row; if (!budget && m_config.hasBudget()) { QString newrow = m_budgetMap[row.id()]; // if there was no mapping found, then the budget report is not interested // in this account. if (newrow.isEmpty()) return; row = ReportAccount(newrow); } // ensure the row already exists (and its parental hierarchy) createRow(outergroup, row, true); // Determine the inner group from the top-most parent account QString innergroup(row.topParentName()); if (m_numColumns <= column) throw MYMONEYEXCEPTION(QString::fromLatin1("Column %1 out of m_numColumns range (%2) in PivotTable::assignCell").arg(column).arg(m_numColumns)); if (m_grid[outergroup][innergroup][row][eActual].count() <= column) throw MYMONEYEXCEPTION(QString::fromLatin1("Column %1 out of grid range (%2) in PivotTable::assignCell").arg(column).arg(m_grid[outergroup][innergroup][row][eActual].count())); if (m_grid[outergroup][innergroup][row][eBudget].count() <= column) throw MYMONEYEXCEPTION(QString::fromLatin1("Column %1 out of grid range (%2) in PivotTable::assignCell").arg(column).arg(m_grid[outergroup][innergroup][row][eBudget].count())); if (!stockSplit) { // Determine whether the value should be inverted before being placed in the row if (m_grid[outergroup].m_inverted) value = -value; // Add the value to the grid cell if (budget) { m_grid[outergroup][innergroup][row][eBudget][column] += value; } else { // If it is loading an actual value for a budget report // check whether it is a subaccount of a budget account (include subaccounts) // If so, check if is the same currency and convert otherwise if (m_config.hasBudget() && row.id() != _row.id() && row.currencyId() != _row.currencyId()) { ReportAccount origAcc = _row; MyMoneyMoney rate = origAcc.foreignCurrencyPrice(row.currencyId(), columnDate(column), false); m_grid[outergroup][innergroup][row][eActual][column] += (value * rate).reduce(); } else { m_grid[outergroup][innergroup][row][eActual][column] += value; } } } else { m_grid[outergroup][innergroup][row][eActual][column] += PivotCell::stockSplit(value); } } void PivotTable::createRow(const QString& outergroup, const ReportAccount& row, bool recursive) { DEBUG_ENTER(Q_FUNC_INFO); // Determine the inner group from the top-most parent account QString innergroup(row.topParentName()); if (! m_grid.contains(outergroup)) { DEBUG_OUTPUT(QString("Adding group [%1]").arg(outergroup)); m_grid[outergroup] = PivotOuterGroup(m_numColumns); } if (! m_grid[outergroup].contains(innergroup)) { DEBUG_OUTPUT(QString("Adding group [%1][%2]").arg(outergroup).arg(innergroup)); m_grid[outergroup][innergroup] = PivotInnerGroup(m_numColumns); } if (! m_grid[outergroup][innergroup].contains(row)) { DEBUG_OUTPUT(QString("Adding row [%1][%2][%3]").arg(outergroup).arg(innergroup).arg(row.debugName())); m_grid[outergroup][innergroup][row] = PivotGridRowSet(m_numColumns); if (recursive && !row.isTopLevel()) createRow(outergroup, row.parent(), recursive); } } int PivotTable::columnValue(const QDate& _date) const { if (m_config.isColumnsAreDays()) return (static_cast(m_beginDate.daysTo(_date))); else return (_date.year() * 12 + _date.month()); } QDate PivotTable::columnDate(int column) const { if (m_config.isColumnsAreDays()) return m_beginDate.addDays(m_config.columnPitch() * column - m_startColumn); else return m_beginDate.addMonths(m_config.columnPitch() * column).addDays(-m_startColumn); } QString PivotTable::renderCSV() const { DEBUG_ENTER(Q_FUNC_INFO); MyMoneyFile* file = MyMoneyFile::instance(); int pricePrecision = 0; int currencyPrecision = 0; int precision = MyMoneyMoney::denomToPrec(file->baseCurrency().smallestAccountFraction()); bool isMultipleCurrencies = false; // // Table Header // QString result = i18n("Account"); auto column = 0; while (column < m_numColumns) { result += QString(",%1").arg(QString(m_columnHeadings[column++])); if (m_rowTypeList.size() > 1) { QString separator; separator = separator.fill(',', m_rowTypeList.size() - 1); result += separator; } } //show total columns if (m_config.isShowingRowTotals()) result += QString(",%1").arg(i18nc("Total balance", "Total")); result += '\n'; // Row Type Header if (m_rowTypeList.size() > 1) { column = 0; while (column < m_numColumns) { for (int i = 0; i < m_rowTypeList.size(); ++i) { result += QString(",%1").arg(m_columnTypeHeaderList[i]); } column++; } if (m_config.isShowingRowTotals()) { for (int i = 0; i < m_rowTypeList.size(); ++i) { result += QString(",%1").arg(m_columnTypeHeaderList[i]); } } result += '\n'; } // // Outer groups // // iterate over outer groups PivotGrid::const_iterator it_outergroup = m_grid.begin(); while (it_outergroup != m_grid.end()) { // // Outer Group Header // if (!(m_config.isIncludingPrice() || m_config.isIncludingAveragePrice())) result += it_outergroup.key() + '\n'; // // Inner Groups // PivotOuterGroup::const_iterator it_innergroup = (*it_outergroup).begin(); int rownum = 0; while (it_innergroup != (*it_outergroup).end()) { // // Rows // QString innergroupdata; PivotInnerGroup::const_iterator it_row = (*it_innergroup).begin(); while (it_row != (*it_innergroup).end()) { ReportAccount rowname = it_row.key(); // // Columns // QString rowdata; column = 0; bool isUsed = false; for (int i = 0; i < m_rowTypeList.size(); ++i) isUsed |= it_row.value()[ m_rowTypeList[i] ][0].isUsed(); if (it_row.key().accountType() != eMyMoney::Account::Type::Investment) { while (column < m_numColumns) { //show columns foreach (const auto rowType, m_rowTypeList) { if (rowType == ePrice) { if (pricePrecision == 0) { if (it_row.key().isInvest()) { pricePrecision = file->currency(it_row.key().currencyId()).pricePrecision(); precision = pricePrecision; } else precision = MyMoneyMoney::denomToPrec(file->baseCurrency().smallestAccountFraction()); } else precision = pricePrecision; } else { if (currencyPrecision == 0) { if (it_row.key().isInvest()) // stock account isn't eveluated in currency, so take investment account instead currencyPrecision = MyMoneyMoney::denomToPrec(it_row.key().parent().fraction()); else currencyPrecision = MyMoneyMoney::denomToPrec(it_row.key().fraction()); precision = currencyPrecision; } else precision = currencyPrecision; } rowdata += QString(",\"%1\"").arg(it_row.value()[rowType][column].formatMoney(QString(), precision, false)); isUsed |= it_row.value()[rowType][column].isUsed(); } column++; } if (m_config.isShowingRowTotals()) { for (int i = 0; i < m_rowTypeList.size(); ++i) rowdata += QString(",\"%1\"").arg((*it_row)[ m_rowTypeList[i] ].m_total.formatMoney(QString(), precision, false)); } } else { for (auto i = 0; i < m_numColumns + m_rowTypeList.size(); ++i) rowdata.append(',');; } // // Row Header // if (!rowname.isClosed() || isUsed) { innergroupdata += "\"" + QString().fill(' ', rowname.hierarchyDepth() - 1) + rowname.name(); // if we don't convert the currencies to the base currency and the // current row contains a foreign currency, then we append the currency // to the name of the account if (!m_config.isConvertCurrency() && rowname.isForeignCurrency()) innergroupdata += QString(" (%1)").arg(rowname.currencyId()); innergroupdata += '\"'; if (isUsed) innergroupdata += rowdata; innergroupdata += '\n'; if (!isMultipleCurrencies && rowname.isForeignCurrency()) isMultipleCurrencies = true; if (!m_containsNonBaseCurrency && rowname.isForeignCurrency()) m_containsNonBaseCurrency = true; } ++it_row; } // // Inner Row Group Totals // bool finishrow = true; QString finalRow; bool isUsed = false; - if (m_config.detailLevel() == MyMoneyReport::eDetailAll && ((*it_innergroup).size() > 1)) { + if (m_config.detailLevel() == eMyMoney::Report::DetailLevel::All && ((*it_innergroup).size() > 1)) { // Print the individual rows result += innergroupdata; if (m_config.isConvertCurrency() && m_config.isShowingColumnTotals()) { // Start the TOTALS row finalRow = i18nc("Total balance", "Total"); isUsed = true; } else { ++rownum; finishrow = false; } } else { // Start the single INDIVIDUAL ACCOUNT row ReportAccount rowname = (*it_innergroup).begin().key(); isUsed |= !rowname.isClosed(); finalRow = "\"" + QString().fill(' ', rowname.hierarchyDepth() - 1) + rowname.name(); if (!m_config.isConvertCurrency() && rowname.isForeignCurrency()) finalRow += QString(" (%1)").arg(rowname.currencyId()); finalRow += "\""; } // Finish the row started above, unless told not to if (finishrow) { column = 0; for (int i = 0; i < m_rowTypeList.size(); ++i) isUsed |= (*it_innergroup).m_total[ m_rowTypeList[i] ][0].isUsed(); while (column < m_numColumns) { for (int i = 0; i < m_rowTypeList.size(); ++i) { isUsed |= (*it_innergroup).m_total[ m_rowTypeList[i] ][column].isUsed(); finalRow += QString(",\"%1\"").arg((*it_innergroup).m_total[ m_rowTypeList[i] ][column].formatMoney(QString(), precision, false)); } column++; } if (m_config.isShowingRowTotals()) { for (int i = 0; i < m_rowTypeList.size(); ++i) finalRow += QString(",\"%1\"").arg((*it_innergroup).m_total[ m_rowTypeList[i] ].m_total.formatMoney(QString(), precision, false)); } finalRow += '\n'; } if (isUsed) { result += finalRow; ++rownum; } ++it_innergroup; } // // Outer Row Group Totals // if (m_config.isConvertCurrency() && m_config.isShowingColumnTotals()) { result += QString("%1 %2").arg(i18nc("Total balance", "Total")).arg(it_outergroup.key()); column = 0; while (column < m_numColumns) { for (int i = 0; i < m_rowTypeList.size(); ++i) result += QString(",\"%1\"").arg((*it_outergroup).m_total[ m_rowTypeList[i] ][column].formatMoney(QString(), precision, false)); column++; } if (m_config.isShowingRowTotals()) { for (int i = 0; i < m_rowTypeList.size(); ++i) result += QString(",\"%1\"").arg((*it_outergroup).m_total[ m_rowTypeList[i] ].m_total.formatMoney(QString(), precision, false)); } result += '\n'; } ++it_outergroup; } // // Report Totals // if (m_config.isConvertCurrency() && m_config.isShowingColumnTotals()) { result += i18n("Grand Total"); auto totalcolumn = 0; while (totalcolumn < m_numColumns) { for (int i = 0; i < m_rowTypeList.size(); ++i) result += QString(",\"%1\"").arg(m_grid.m_total[ m_rowTypeList[i] ][totalcolumn].formatMoney(QString(), precision, false)); totalcolumn++; } if (m_config.isShowingRowTotals()) { for (int i = 0; i < m_rowTypeList.size(); ++i) result += QString(",\"%1\"").arg(m_grid.m_total[ m_rowTypeList[i] ].m_total.formatMoney(QString(), precision, false)); } result += '\n'; } return result; } QString PivotTable::renderHTML() const { DEBUG_ENTER(Q_FUNC_INFO); MyMoneyFile* file = MyMoneyFile::instance(); int pricePrecision = 0; int currencyPrecision = 0; int precision = MyMoneyMoney::denomToPrec(file->baseCurrency().smallestAccountFraction()); QString colspan = QString(" colspan=\"%1\"").arg(m_numColumns + 1 + (m_config.isShowingRowTotals() ? 1 : 0)); // setup a leftborder for better readability of budget vs actual reports QString leftborder; if (m_rowTypeList.size() > 1) leftborder = " class=\"leftborder\""; // // Table Header // QString result = QString("\n\n\n" "\n").arg(i18n("Account")); QString headerspan; int span = m_rowTypeList.size(); headerspan = QString(" colspan=\"%1\"").arg(span); auto column = 0; while (column < m_numColumns) result += QString("%2").arg(headerspan, QString(m_columnHeadings[column++]).replace(QRegExp(" "), "
")); if (m_config.isShowingRowTotals()) result += QString("%2").arg(headerspan).arg(i18nc("Total balance", "Total")); result += "
\n"; // // Header for multiple columns // if (span > 1) { result += ""; column = 0; while (column < m_numColumns) { QString lb; if (column != 0) lb = leftborder; for (int i = 0; i < m_rowTypeList.size(); ++i) { result += QString("%1") .arg(m_columnTypeHeaderList[i]) .arg(i == 0 ? lb : QString()); } column++; } if (m_config.isShowingRowTotals()) { for (int i = 0; i < m_rowTypeList.size(); ++i) { result += QString("%1") .arg(m_columnTypeHeaderList[i]) .arg(i == 0 ? leftborder : QString()); } } result += ""; } // Skip the body of the report if the report only calls for totals to be shown - if (m_config.detailLevel() != MyMoneyReport::eDetailTotal) { + if (m_config.detailLevel() != eMyMoney::Report::DetailLevel::Total) { // // Outer groups // // Need to sort the outergroups. They can't always be sorted by name. So we create a list of // map iterators, and sort that. Then we'll iterate through the map iterators and use those as // before. // // I hope this doesn't bog the performance of reports, given that we're copying the entire report // data. If this is a perf hit, we could change to storing outergroup pointers, I think. QList outergroups; PivotGrid::const_iterator it_outergroup_map = m_grid.begin(); while (it_outergroup_map != m_grid.end()) { outergroups.push_back(it_outergroup_map.value()); // copy the name into the outergroup, because we will now lose any association with // the map iterator outergroups.back().m_displayName = it_outergroup_map.key(); ++it_outergroup_map; } qSort(outergroups.begin(), outergroups.end()); QList::const_iterator it_outergroup = outergroups.constBegin(); while (it_outergroup != outergroups.constEnd()) { // // Outer Group Header // if (!(m_config.isIncludingPrice() || m_config.isIncludingAveragePrice())) result += QString("\n").arg(colspan).arg((*it_outergroup).m_displayName); // Skip the inner groups if the report only calls for outer group totals to be shown - if (m_config.detailLevel() != MyMoneyReport::eDetailGroup) { + if (m_config.detailLevel() != eMyMoney::Report::DetailLevel::Group) { // // Inner Groups // PivotOuterGroup::const_iterator it_innergroup = (*it_outergroup).begin(); int rownum = 0; while (it_innergroup != (*it_outergroup).end()) { // // Rows // QString innergroupdata; PivotInnerGroup::const_iterator it_row = (*it_innergroup).begin(); while (it_row != (*it_innergroup).end()) { // // Columns // QString rowdata; column = 0; pricePrecision = 0; // new row => new account => new precision currencyPrecision = 0; bool isUsed = it_row.value()[eActual][0].isUsed(); if (it_row.key().accountType() != eMyMoney::Account::Type::Investment) { while (column < m_numColumns) { QString lb; if (column > 0) lb = leftborder; foreach (const auto rowType, m_rowTypeList) { if (rowType == ePrice) { if (pricePrecision == 0) { if (it_row.key().isInvest()) { pricePrecision = file->currency(it_row.key().currencyId()).pricePrecision(); precision = pricePrecision; } else precision = MyMoneyMoney::denomToPrec(file->baseCurrency().smallestAccountFraction()); } else precision = pricePrecision; } else { if (currencyPrecision == 0) { if (it_row.key().isInvest()) // stock account isn't eveluated in currency, so take investment account instead currencyPrecision = MyMoneyMoney::denomToPrec(it_row.key().parent().fraction()); else currencyPrecision = MyMoneyMoney::denomToPrec(it_row.key().fraction()); precision = currencyPrecision; } else precision = currencyPrecision; } rowdata += QString("%1") .arg(coloredAmount(it_row.value()[rowType][column], QString(), precision)) .arg(lb); lb.clear(); isUsed |= it_row.value()[rowType][column].isUsed(); } ++column; } if (m_config.isShowingRowTotals()) { for (int i = 0; i < m_rowTypeList.size(); ++i) { rowdata += QString("%1") .arg(coloredAmount(it_row.value()[ m_rowTypeList[i] ].m_total, QString(), precision)) .arg(i == 0 ? leftborder : QString()); } } } else rowdata += QString(QLatin1Literal("")).arg(m_numColumns + m_rowTypeList.size()); // // Row Header // ReportAccount rowname = it_row.key(); // don't show closed accounts if they have not been used if (!rowname.isClosed() || isUsed) { innergroupdata += QString("%5%6") .arg(rownum & 0x01 ? "even" : "odd") .arg(rowname.isTopLevel() ? " id=\"topparent\"" : "") .arg("") //.arg((*it_row).m_total.isZero() ? colspan : "") // colspan the distance if this row will be blank .arg(rowname.hierarchyDepth() - 1) .arg(rowname.name().replace(QRegExp(" "), " ")) .arg((m_config.isConvertCurrency() || !rowname.isForeignCurrency()) ? QString() : QString(" (%1)").arg(rowname.currency().id())); // Don't print this row if it's going to be all zeros // TODO: Uncomment this, and deal with the case where the data // is zero, but the budget is non-zero //if ( !(*it_row).m_total.isZero() ) innergroupdata += rowdata; innergroupdata += "\n"; if (!m_containsNonBaseCurrency && rowname.isForeignCurrency()) m_containsNonBaseCurrency = true; } ++it_row; } // // Inner Row Group Totals // bool finishrow = true; QString finalRow; bool isUsed = false; - if (m_config.detailLevel() == MyMoneyReport::eDetailAll && ((*it_innergroup).size() > 1)) { + if (m_config.detailLevel() == eMyMoney::Report::DetailLevel::All && ((*it_innergroup).size() > 1)) { // Print the individual rows result += innergroupdata; if (m_config.isConvertCurrency() && m_config.isShowingColumnTotals()) { // Start the TOTALS row finalRow = QString("") .arg(rownum & 0x01 ? "even" : "odd") .arg(i18nc("Total balance", "Total")); // don't suppress display of totals isUsed = true; } else { finishrow = false; ++rownum; } } else { // Start the single INDIVIDUAL ACCOUNT row // FIXME: There is a bit of a bug here with class=leftX. There's only a finite number // of classes I can define in the .CSS file, and the user can theoretically nest deeper. // The right solution is to use style=Xem, and calculate X. Let's see if anyone complains // first :) Also applies to the row header case above. // FIXED: I found it in one of my reports and changed it to the proposed method. // This works for me (ipwizard) ReportAccount rowname = (*it_innergroup).begin().key(); isUsed |= !rowname.isClosed(); finalRow = QString("") .arg(rownum & 0x01 ? "even" : "odd") - .arg(m_config.detailLevel() == MyMoneyReport::eDetailAll ? "id=\"solo\"" : "") + .arg(m_config.detailLevel() == eMyMoney::Report::DetailLevel::All ? "id=\"solo\"" : "") .arg(rowname.hierarchyDepth() - 1) .arg(rowname.name().replace(QRegExp(" "), " ")) .arg((m_config.isConvertCurrency() || !rowname.isForeignCurrency()) ? QString() : QString(" (%1)").arg(rowname.currency().id())); } // Finish the row started above, unless told not to if (finishrow) { column = 0; isUsed |= (*it_innergroup).m_total[eActual][0].isUsed(); while (column < m_numColumns) { QString lb; if (column != 0) lb = leftborder; for (int i = 0; i < m_rowTypeList.size(); ++i) { finalRow += QString("%1") .arg(coloredAmount((*it_innergroup).m_total[ m_rowTypeList[i] ][column], QString(), precision)) .arg(i == 0 ? lb : QString()); isUsed |= (*it_innergroup).m_total[ m_rowTypeList[i] ][column].isUsed(); } column++; } if (m_config.isShowingRowTotals()) { for (int i = 0; i < m_rowTypeList.size(); ++i) { finalRow += QString("%1") .arg(coloredAmount((*it_innergroup).m_total[ m_rowTypeList[i] ].m_total, QString(), precision)) .arg(i == 0 ? leftborder : QString()); } } finalRow += "\n"; if (isUsed) { result += finalRow; ++rownum; } } ++it_innergroup; } // end while iterating on the inner groups } // end if detail level is not "group" // // Outer Row Group Totals // if (m_config.isConvertCurrency() && m_config.isShowingColumnTotals()) { result += QString("").arg(i18nc("Total balance", "Total")).arg((*it_outergroup).m_displayName); column = 0; while (column < m_numColumns) { QString lb; if (column != 0) lb = leftborder; for (int i = 0; i < m_rowTypeList.size(); ++i) { result += QString("%1") .arg(coloredAmount((*it_outergroup).m_total[ m_rowTypeList[i] ][column], QString(), precision)) .arg(i == 0 ? lb : QString()); } column++; } if (m_config.isShowingRowTotals()) { for (int i = 0; i < m_rowTypeList.size(); ++i) { result += QString("%1") .arg(coloredAmount((*it_outergroup).m_total[ m_rowTypeList[i] ].m_total, QString(), precision)) .arg(i == 0 ? leftborder : QString()); } } result += "\n"; } ++it_outergroup; } // end while iterating on the outergroups } // end if detail level is not "total" // // Report Totals // if (m_config.isConvertCurrency() && m_config.isShowingColumnTotals()) { result += QString("\n"); result += QString("").arg(i18n("Grand Total")); auto totalcolumn = 0; while (totalcolumn < m_numColumns) { QString lb; if (totalcolumn != 0) lb = leftborder; for (int i = 0; i < m_rowTypeList.size(); ++i) { result += QString("%1") .arg(coloredAmount(m_grid.m_total[ m_rowTypeList[i] ][totalcolumn], QString(), precision)) .arg(i == 0 ? lb : QString()); } totalcolumn++; } if (m_config.isShowingRowTotals()) { for (int i = 0; i < m_rowTypeList.size(); ++i) { result += QString("%1") .arg(coloredAmount(m_grid.m_total[ m_rowTypeList[i] ].m_total, QString(), precision)) .arg(i == 0 ? leftborder : QString()); } } result += "\n"; } result += "
%1
%2
  %2
%5%6
%1 %2
 
%1
\n"; return result; } void PivotTable::dump(const QString& file, const QString& /* context */) const { QFile g(file); g.open(QIODevice::WriteOnly); QTextStream(&g) << renderHTML(); g.close(); } void PivotTable::drawChart(KReportChartView& chartView) const { chartView.drawPivotChart(m_grid, m_config, m_numColumns, m_columnHeadings, m_rowTypeList, m_columnTypeHeaderList); } QString PivotTable::coloredAmount(const MyMoneyMoney& amount, const QString& currencySymbol, int prec) const { const auto value = amount.formatMoney(currencySymbol, prec); if (amount.isNegative()) return QString::fromLatin1("%2") .arg(KMyMoneySettings::schemeColor(SchemeColor::Negative).name(), value); else return value; } void PivotTable::calculateBudgetDiff() { PivotGrid::iterator it_outergroup = m_grid.begin(); while (it_outergroup != m_grid.end()) { PivotOuterGroup::iterator it_innergroup = (*it_outergroup).begin(); while (it_innergroup != (*it_outergroup).end()) { PivotInnerGroup::iterator it_row = (*it_innergroup).begin(); while (it_row != (*it_innergroup).end()) { int column = m_startColumn; switch (it_row.key().accountGroup()) { case eMyMoney::Account::Type::Income: case eMyMoney::Account::Type::Asset: while (column < m_numColumns) { it_row.value()[eBudgetDiff][column] = PivotCell(it_row.value()[eActual][column] - it_row.value()[eBudget][column]); ++column; } break; case eMyMoney::Account::Type::Expense: case eMyMoney::Account::Type::Liability: while (column < m_numColumns) { it_row.value()[eBudgetDiff][column] = PivotCell(it_row.value()[eBudget][column] - it_row.value()[eActual][column]); ++column; } break; default: break; } ++it_row; } ++it_innergroup; } ++it_outergroup; } } void PivotTable::calculateForecast() { //setup forecast MyMoneyForecast forecast = KMyMoneyUtils::forecast(); //since this is a net worth forecast we want to include all account even those that are not in use forecast.setIncludeUnusedAccounts(true); //setup forecast dates if (m_endDate > QDate::currentDate()) { forecast.setForecastEndDate(m_endDate); forecast.setForecastStartDate(QDate::currentDate()); forecast.setForecastDays(QDate::currentDate().daysTo(m_endDate)); } else { forecast.setForecastStartDate(m_beginDate); forecast.setForecastEndDate(m_endDate); forecast.setForecastDays(m_beginDate.daysTo(m_endDate) + 1); } //adjust history dates if beginning date is before today if (m_beginDate < QDate::currentDate()) { forecast.setHistoryEndDate(m_beginDate.addDays(-1)); forecast.setHistoryStartDate(forecast.historyEndDate().addDays(-forecast.accountsCycle()*forecast.forecastCycles())); } //run forecast - if (m_config.rowType() == MyMoneyReport::eAssetLiability) { //asset and liability + if (m_config.rowType() == eMyMoney::Report::RowType::AssetLiability) { //asset and liability forecast.doForecast(); } else { //income and expenses MyMoneyBudget budget; forecast.createBudget(budget, m_beginDate.addYears(-1), m_beginDate.addDays(-1), m_beginDate, m_endDate, false); } // check if we need to copy the opening balances // the conditions might be too tight but those fix a reported // problem and should avoid side effects in other situations // see https://bugs.kde.org/show_bug.cgi?id=391961 const bool copyOpeningBalances = (m_startColumn == 1) && !m_config.isIncludingSchedules() && (m_config.isRunningSum()); //go through the data and add forecast PivotGrid::iterator it_outergroup = m_grid.begin(); while (it_outergroup != m_grid.end()) { PivotOuterGroup::iterator it_innergroup = (*it_outergroup).begin(); while (it_innergroup != (*it_outergroup).end()) { PivotInnerGroup::iterator it_row = (*it_innergroup).begin(); while (it_row != (*it_innergroup).end()) { int column = m_startColumn; QDate forecastDate = m_beginDate; // check whether the opening balance needs to be setup if (copyOpeningBalances) { it_row.value()[eForecast][0] += it_row.value()[eActual][0]; } //check whether columns are days or months if (m_config.isColumnsAreDays()) { while (column < m_numColumns) { it_row.value()[eForecast][column] = PivotCell(forecast.forecastBalance(it_row.key(), forecastDate)); forecastDate = forecastDate.addDays(1); ++column; } } else { //if columns are months while (column < m_numColumns) { // the forecast balance is on the first day of the month see MyMoneyForecast::calculateScheduledMonthlyBalances() forecastDate = QDate(forecastDate.year(), forecastDate.month(), 1); //check that forecastDate is not over ending date if (forecastDate > m_endDate) forecastDate = m_endDate; //get forecast balance and set the corresponding column it_row.value()[eForecast][column] = PivotCell(forecast.forecastBalance(it_row.key(), forecastDate)); forecastDate = forecastDate.addMonths(1); ++column; } } ++it_row; } ++it_innergroup; } ++it_outergroup; } } void PivotTable::loadRowTypeList() { if ((m_config.isIncludingBudgetActuals()) || (!m_config.hasBudget() && !m_config.isIncludingForecast() && !m_config.isIncludingMovingAverage() && !m_config.isIncludingPrice() && !m_config.isIncludingAveragePrice()) ) { m_rowTypeList.append(eActual); m_columnTypeHeaderList.append(i18n("Actual")); } if (m_config.hasBudget()) { m_rowTypeList.append(eBudget); m_columnTypeHeaderList.append(i18n("Budget")); } if (m_config.isIncludingBudgetActuals()) { m_rowTypeList.append(eBudgetDiff); m_columnTypeHeaderList.append(i18n("Difference")); } if (m_config.isIncludingForecast()) { m_rowTypeList.append(eForecast); m_columnTypeHeaderList.append(i18n("Forecast")); } if (m_config.isIncludingMovingAverage()) { m_rowTypeList.append(eAverage); m_columnTypeHeaderList.append(i18n("Moving Average")); } if (m_config.isIncludingAveragePrice()) { m_rowTypeList.append(eAverage); m_columnTypeHeaderList.append(i18n("Moving Average Price")); } if (m_config.isIncludingPrice()) { m_rowTypeList.append(ePrice); m_columnTypeHeaderList.append(i18n("Price")); } } void PivotTable::calculateMovingAverage() { int delta = m_config.movingAverageDays() / 2; //go through the data and add the moving average PivotGrid::iterator it_outergroup = m_grid.begin(); while (it_outergroup != m_grid.end()) { PivotOuterGroup::iterator it_innergroup = (*it_outergroup).begin(); while (it_innergroup != (*it_outergroup).end()) { PivotInnerGroup::iterator it_row = (*it_innergroup).begin(); while (it_row != (*it_innergroup).end()) { int column = m_startColumn; //check whether columns are days or months - if (m_config.columnType() == MyMoneyReport::eDays) { + if (m_config.columnType() == eMyMoney::Report::ColumnType::Days) { while (column < m_numColumns) { MyMoneyMoney totalPrice = MyMoneyMoney(); QDate averageStart = columnDate(column).addDays(-delta); QDate averageEnd = columnDate(column).addDays(delta); for (QDate averageDate = averageStart; averageDate <= averageEnd; averageDate = averageDate.addDays(1)) { if (m_config.isConvertCurrency()) { totalPrice += it_row.key().deepCurrencyPrice(averageDate) * it_row.key().baseCurrencyPrice(averageDate); } else { totalPrice += it_row.key().deepCurrencyPrice(averageDate); } totalPrice = totalPrice.convert(10000); } //calculate the average price MyMoneyMoney averagePrice = totalPrice / MyMoneyMoney((averageStart.daysTo(averageEnd) + 1), 1); //get the actual value, multiply by the average price and save that value MyMoneyMoney averageValue = it_row.value()[eActual][column] * averagePrice; it_row.value()[eAverage][column] = PivotCell(averageValue.convert(10000)); ++column; } } else { //if columns are months while (column < m_numColumns) { QDate averageStart = columnDate(column); //set the right start date depending on the column type switch (m_config.columnType()) { - case MyMoneyReport::eYears: { + case eMyMoney::Report::ColumnType::Years: { averageStart = QDate(columnDate(column).year(), 1, 1); break; } - case MyMoneyReport::eBiMonths: { + case eMyMoney::Report::ColumnType::BiMonths: { averageStart = QDate(columnDate(column).year(), columnDate(column).month(), 1).addMonths(-1); break; } - case MyMoneyReport::eQuarters: { + case eMyMoney::Report::ColumnType::Quarters: { averageStart = QDate(columnDate(column).year(), columnDate(column).month(), 1).addMonths(-1); break; } - case MyMoneyReport::eMonths: { + case eMyMoney::Report::ColumnType::Months: { averageStart = QDate(columnDate(column).year(), columnDate(column).month(), 1); break; } - case MyMoneyReport::eWeeks: { + case eMyMoney::Report::ColumnType::Weeks: { averageStart = columnDate(column).addDays(-columnDate(column).dayOfWeek() + 1); break; } default: break; } //gather the actual data and calculate the average MyMoneyMoney totalPrice = MyMoneyMoney(); QDate averageEnd = columnDate(column); for (QDate averageDate = averageStart; averageDate <= averageEnd; averageDate = averageDate.addDays(1)) { if (m_config.isConvertCurrency()) { totalPrice += it_row.key().deepCurrencyPrice(averageDate) * it_row.key().baseCurrencyPrice(averageDate); } else { totalPrice += it_row.key().deepCurrencyPrice(averageDate); } totalPrice = totalPrice.convert(10000); } MyMoneyMoney averagePrice = totalPrice / MyMoneyMoney((averageStart.daysTo(averageEnd) + 1), 1); MyMoneyMoney averageValue = it_row.value()[eActual][column] * averagePrice; //fill in the average it_row.value()[eAverage][column] = PivotCell(averageValue.convert(10000)); ++column; } } ++it_row; } ++it_innergroup; } ++it_outergroup; } } void PivotTable::fillBasePriceUnit(ERowType rowType) { MyMoneyFile* file = MyMoneyFile::instance(); QString baseCurrencyId = file->baseCurrency().id(); //get the first price date for securities QMap securityDates = securityFirstPrice(); //go through the data PivotGrid::iterator it_outergroup = m_grid.begin(); while (it_outergroup != m_grid.end()) { PivotOuterGroup::iterator it_innergroup = (*it_outergroup).begin(); while (it_innergroup != (*it_outergroup).end()) { PivotInnerGroup::iterator it_row = (*it_innergroup).begin(); while (it_row != (*it_innergroup).end()) { int column = m_startColumn; //if it is a base currency fill all the values bool firstPriceExists = false; if (it_row.key().currencyId() == baseCurrencyId) { firstPriceExists = true; } while (column < m_numColumns) { //check whether the date for that column is on or after the first price if (!firstPriceExists && securityDates.contains(it_row.key().currencyId()) && columnDate(column) >= securityDates.value(it_row.key().currencyId())) { firstPriceExists = true; } //only add the dummy value if there is a price for that date if (firstPriceExists) { //insert a unit of currency for each account it_row.value()[rowType][column] = PivotCell(MyMoneyMoney::ONE); } ++column; } ++it_row; } ++it_innergroup; } ++it_outergroup; } } QMap PivotTable::securityFirstPrice() { MyMoneyFile* file = MyMoneyFile::instance(); MyMoneyPriceList priceList = file->priceList(); QMap securityPriceDate; MyMoneyPriceList::const_iterator prices_it; for (prices_it = priceList.constBegin(); prices_it != priceList.constEnd(); ++prices_it) { MyMoneyPrice firstPrice = (*((*prices_it).constBegin())); //check the security in the from field //if it is there, check if it is older if (securityPriceDate.contains(firstPrice.from())) { if (securityPriceDate.value(firstPrice.from()) > firstPrice.date()) { securityPriceDate[firstPrice.from()] = firstPrice.date(); } } else { securityPriceDate.insert(firstPrice.from(), firstPrice.date()); } //check the security in the to field //if it is there, check if it is older if (securityPriceDate.contains(firstPrice.to())) { if (securityPriceDate.value(firstPrice.to()) > firstPrice.date()) { securityPriceDate[firstPrice.to()] = firstPrice.date(); } } else { securityPriceDate.insert(firstPrice.to(), firstPrice.date()); } } return securityPriceDate; } void PivotTable::includeInvestmentSubAccounts() { // if we're not in expert mode, we need to make sure // that all stock accounts for the selected investment // account are also selected QStringList accountList; if (m_config.accounts(accountList)) { if (!KMyMoneySettings::expertMode()) { foreach (const auto sAccount, accountList) { auto acc = MyMoneyFile::instance()->account(sAccount); if (acc.accountType() == eMyMoney::Account::Type::Investment) { foreach (const auto sSubAccount, acc.accountList()) { if (!accountList.contains(sSubAccount)) { m_config.addAccount(sSubAccount); } } } } } } } int PivotTable::currentDateColumn() { //return -1 if the columns do not include the current date if (m_beginDate > QDate::currentDate() || m_endDate < QDate::currentDate()) { return -1; } //check the date of each column and return if it is the one for the current date //if columns are not days, return the one for the current month or year int column = m_startColumn; while (column < m_numColumns) { if (columnDate(column) >= QDate::currentDate()) { break; } column++; } //if there is no column matching the current date, return -1 if (column == m_numColumns) { column = -1; } return column; } } // namespace diff --git a/kmymoney/reports/querytable.cpp b/kmymoney/reports/querytable.cpp index f8cfb8200..fffc198a1 100644 --- a/kmymoney/reports/querytable.cpp +++ b/kmymoney/reports/querytable.cpp @@ -1,2176 +1,2176 @@ /* * Copyright 2005 Ace Jones * 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 . */ /*************************************************************************** * * * 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. * * * ***************************************************************************/ /**************************************************************************** Contains code from the func_xirr and related methods of financial.cpp - KOffice 1.6 by Sascha Pfau. Sascha agreed to relicense those methods under GPLv2 or later. *****************************************************************************/ #include "querytable.h" #include // ---------------------------------------------------------------------------- // QT Includes #include #include // ---------------------------------------------------------------------------- // KDE Includes #include // ---------------------------------------------------------------------------- // Project Includes #include "mymoneyfile.h" #include "mymoneyaccount.h" #include "mymoneysecurity.h" #include "mymoneyinstitution.h" #include "mymoneyprice.h" #include "mymoneypayee.h" #include "mymoneytag.h" #include "mymoneysplit.h" #include "mymoneytransaction.h" #include "mymoneyreport.h" #include "mymoneyexception.h" #include "kmymoneyutils.h" #include "reportaccount.h" #include "mymoneyenums.h" namespace reports { // **************************************************************************** // // CashFlowListItem implementation // // Cash flow analysis tools for investment reports // // **************************************************************************** QDate CashFlowListItem::m_sToday = QDate::currentDate(); MyMoneyMoney CashFlowListItem::NPV(double _rate) const { double T = static_cast(m_sToday.daysTo(m_date)) / 365.0; MyMoneyMoney result(m_value.toDouble() / pow(1 + _rate, T), 100); //qDebug() << "CashFlowListItem::NPV( " << _rate << " ) == " << result; return result; } // **************************************************************************** // // CashFlowList implementation // // Cash flow analysis tools for investment reports // // **************************************************************************** CashFlowListItem CashFlowList::mostRecent() const { CashFlowList dupe(*this); qSort(dupe); //qDebug() << " CashFlowList::mostRecent() == " << dupe.back().date().toString(Qt::ISODate); return dupe.back(); } MyMoneyMoney CashFlowList::NPV(double _rate) const { MyMoneyMoney result; const_iterator it_cash = constBegin(); while (it_cash != constEnd()) { result += (*it_cash).NPV(_rate); ++it_cash; } //qDebug() << "CashFlowList::NPV( " << _rate << " ) == " << result << "------------------------" << endl; return result; } double CashFlowList::calculateXIRR() const { double resultRate = 0.00001; double resultZero = 0.00000; //if ( args.count() > 2 ) // resultRate = calc->conv()->asFloat ( args[2] ).asFloat(); // check pairs and count >= 2 and guess > -1.0 //if ( args[0].count() != args[1].count() || args[1].count() < 2 || resultRate <= -1.0 ) // return Value::errorVALUE(); // define max epsilon static const double maxEpsilon = 1e-5; // max number of iterations static const int maxIter = 50; // Newton's method - try to find a res, with a accuracy of maxEpsilon double rateEpsilon, newRate, resultValue; int i = 0; bool contLoop; do { resultValue = xirrResult(resultRate); double resultDerive = xirrResultDerive(resultRate); //check what happens if xirrResultDerive is zero //Don't know if it is correct to dismiss the result if (resultDerive != 0) { newRate = resultRate - resultValue / resultDerive; } else { newRate = resultRate - resultValue; } rateEpsilon = fabs(newRate - resultRate); resultRate = newRate; contLoop = (rateEpsilon > maxEpsilon) && (fabs(resultValue) > maxEpsilon); } while (contLoop && (++i < maxIter)); if (contLoop) return resultZero; return resultRate; } double CashFlowList::xirrResult(double& rate) const { QDate date; double r = rate + 1.0; double res = 0.00000;//back().value().toDouble(); QList::const_iterator list_it = constBegin(); while (list_it != constEnd()) { double e_i = ((* list_it).today().daysTo((* list_it).date())) / 365.0; MyMoneyMoney val = (* list_it).value(); if (e_i < 0) { res += val.toDouble() * pow(r, -e_i); } else { res += val.toDouble() / pow(r, e_i); } ++list_it; } return res; } double CashFlowList::xirrResultDerive(double& rate) const { QDate date; double r = rate + 1.0; double res = 0.00000; QList::const_iterator list_it = constBegin(); while (list_it != constEnd()) { double e_i = ((* list_it).today().daysTo((* list_it).date())) / 365.0; MyMoneyMoney val = (* list_it).value(); res -= e_i * val.toDouble() / pow(r, e_i + 1.0); ++list_it; } return res; } double CashFlowList::IRR() const { double result = 0.0; // set 'today', which is the most recent of all dates in the list CashFlowListItem::setToday(mostRecent().date()); result = calculateXIRR(); return result; } MyMoneyMoney CashFlowList::total() const { MyMoneyMoney result; const_iterator it_cash = constBegin(); while (it_cash != constEnd()) { result += (*it_cash).value(); ++it_cash; } return result; } void CashFlowList::dumpDebug() const { const_iterator it_item = constBegin(); while (it_item != constEnd()) { qDebug() << (*it_item).date().toString(Qt::ISODate) << " " << (*it_item).value().toString(); ++it_item; } } // **************************************************************************** // // QueryTable implementation // // **************************************************************************** /** * TODO * * - Collapse 2- & 3- groups when they are identical * - Way more test cases (especially splits & transfers) * - Option to collapse splits * - Option to exclude transfers * */ QueryTable::QueryTable(const MyMoneyReport& _report): ListTable(_report) { // separated into its own method to allow debugging (setting breakpoints // directly in ctors somehow does not work for me (ipwizard)) // TODO: remove the init() method and move the code back to the ctor init(); } void QueryTable::init() { m_columns.clear(); m_group.clear(); m_subtotal.clear(); m_postcolumns.clear(); switch (m_config.rowType()) { - case MyMoneyReport::eAccountByTopAccount: - case MyMoneyReport::eEquityType: - case MyMoneyReport::eAccountType: - case MyMoneyReport::eInstitution: + case eMyMoney::Report::RowType::AccountByTopAccount: + case eMyMoney::Report::RowType::EquityType: + case eMyMoney::Report::RowType::AccountType: + case eMyMoney::Report::RowType::Institution: constructAccountTable(); m_columns << ctAccount; break; - case MyMoneyReport::eAccount: + case eMyMoney::Report::RowType::Account: constructTransactionTable(); m_columns << ctAccountID << ctPostDate; break; - case MyMoneyReport::ePayee: - case MyMoneyReport::eTag: - case MyMoneyReport::eMonth: - case MyMoneyReport::eWeek: + case eMyMoney::Report::RowType::Payee: + case eMyMoney::Report::RowType::Tag: + case eMyMoney::Report::RowType::Month: + case eMyMoney::Report::RowType::Week: constructTransactionTable(); m_columns << ctPostDate << ctAccount; break; - case MyMoneyReport::eCashFlow: + case eMyMoney::Report::RowType::CashFlow: constructSplitsTable(); m_columns << ctPostDate; break; default: constructTransactionTable(); m_columns << ctPostDate; } // Sort the data to match the report definition m_subtotal << ctValue; switch (m_config.rowType()) { - case MyMoneyReport::eCashFlow: + case eMyMoney::Report::RowType::CashFlow: m_group << ctCategoryType << ctTopCategory << ctCategory; break; - case MyMoneyReport::eCategory: + case eMyMoney::Report::RowType::Category: m_group << ctCategoryType << ctTopCategory << ctCategory; break; - case MyMoneyReport::eTopCategory: + case eMyMoney::Report::RowType::TopCategory: m_group << ctCategoryType << ctTopCategory; break; - case MyMoneyReport::eTopAccount: + case eMyMoney::Report::RowType::TopAccount: m_group << ctTopAccount << ctAccount; break; - case MyMoneyReport::eAccount: + case eMyMoney::Report::RowType::Account: m_group << ctAccount; break; - case MyMoneyReport::eAccountReconcile: + case eMyMoney::Report::RowType::AccountReconcile: m_group << ctAccount << ctReconcileFlag; break; - case MyMoneyReport::ePayee: + case eMyMoney::Report::RowType::Payee: m_group << ctPayee; break; - case MyMoneyReport::eTag: + case eMyMoney::Report::RowType::Tag: m_group << ctTag; break; - case MyMoneyReport::eMonth: + case eMyMoney::Report::RowType::Month: m_group << ctMonth; break; - case MyMoneyReport::eWeek: + case eMyMoney::Report::RowType::Week: m_group << ctWeek; break; - case MyMoneyReport::eAccountByTopAccount: + case eMyMoney::Report::RowType::AccountByTopAccount: m_group << ctTopAccount; break; - case MyMoneyReport::eEquityType: + case eMyMoney::Report::RowType::EquityType: m_group << ctEquityType; break; - case MyMoneyReport::eAccountType: + case eMyMoney::Report::RowType::AccountType: m_group << ctType; break; - case MyMoneyReport::eInstitution: + case eMyMoney::Report::RowType::Institution: m_group << ctInstitution << ctTopAccount; break; default: throw MYMONEYEXCEPTION_CSTRING("QueryTable::QueryTable(): unhandled row type"); } QVector sort = QVector::fromList(m_group) << QVector::fromList(m_columns) << ctID << ctRank; m_columns.clear(); switch (m_config.rowType()) { - case MyMoneyReport::eAccountByTopAccount: - case MyMoneyReport::eEquityType: - case MyMoneyReport::eAccountType: - case MyMoneyReport::eInstitution: + case eMyMoney::Report::RowType::AccountByTopAccount: + case eMyMoney::Report::RowType::EquityType: + case eMyMoney::Report::RowType::AccountType: + case eMyMoney::Report::RowType::Institution: m_columns << ctAccount; break; default: m_columns << ctPostDate; } unsigned qc = m_config.queryColumns(); - if (qc & MyMoneyReport::eQCnumber) + if (qc & eMyMoney::Report::QueryColumn::Number) m_columns << ctNumber; - if (qc & MyMoneyReport::eQCpayee) + if (qc & eMyMoney::Report::QueryColumn::Payee) m_columns << ctPayee; - if (qc & MyMoneyReport::eQCtag) + if (qc & eMyMoney::Report::QueryColumn::Tag) m_columns << ctTag; - if (qc & MyMoneyReport::eQCcategory) + if (qc & eMyMoney::Report::QueryColumn::Category) m_columns << ctCategory; - if (qc & MyMoneyReport::eQCaccount) + if (qc & eMyMoney::Report::QueryColumn::Account) m_columns << ctAccount; - if (qc & MyMoneyReport::eQCreconciled) + if (qc & eMyMoney::Report::QueryColumn::Reconciled) m_columns << ctReconcileFlag; - if (qc & MyMoneyReport::eQCmemo) + if (qc & eMyMoney::Report::QueryColumn::Memo) m_columns << ctMemo; - if (qc & MyMoneyReport::eQCaction) + if (qc & eMyMoney::Report::QueryColumn::Action) m_columns << ctAction; - if (qc & MyMoneyReport::eQCshares) + if (qc & eMyMoney::Report::QueryColumn::Shares) m_columns << ctShares; - if (qc & MyMoneyReport::eQCprice) + if (qc & eMyMoney::Report::QueryColumn::Price) m_columns << ctPrice; - if (qc & MyMoneyReport::eQCperformance) { + if (qc & eMyMoney::Report::QueryColumn::Performance) { m_subtotal.clear(); switch (m_config.investmentSum()) { - case MyMoneyReport::eSumOwnedAndSold: + case eMyMoney::Report::InvestmentSum::OwnedAndSold: m_columns << ctBuys << ctSells << ctReinvestIncome << ctCashIncome << ctEndingBalance << ctReturn << ctReturnInvestment; m_subtotal << ctBuys << ctSells << ctReinvestIncome << ctCashIncome << ctEndingBalance << ctReturn << ctReturnInvestment; break; - case MyMoneyReport::eSumOwned: + case eMyMoney::Report::InvestmentSum::Owned: m_columns << ctBuys << ctReinvestIncome << ctMarketValue << ctReturn << ctReturnInvestment; m_subtotal << ctBuys << ctReinvestIncome << ctMarketValue << ctReturn << ctReturnInvestment; break; - case MyMoneyReport::eSumSold: + case eMyMoney::Report::InvestmentSum::Sold: m_columns << ctBuys << ctSells << ctCashIncome << ctReturn << ctReturnInvestment; m_subtotal << ctBuys << ctSells << ctCashIncome << ctReturn << ctReturnInvestment; break; - case MyMoneyReport::eSumPeriod: + case eMyMoney::Report::InvestmentSum::Period: default: m_columns << ctStartingBalance << ctBuys << ctSells << ctReinvestIncome << ctCashIncome << ctEndingBalance << ctReturn << ctReturnInvestment; m_subtotal << ctStartingBalance << ctBuys << ctSells << ctReinvestIncome << ctCashIncome << ctEndingBalance << ctReturn << ctReturnInvestment; break; } } - if (qc & MyMoneyReport::eQCcapitalgain) { + if (qc & eMyMoney::Report::QueryColumn::CapitalGain) { m_subtotal.clear(); switch (m_config.investmentSum()) { - case MyMoneyReport::eSumOwned: + case eMyMoney::Report::InvestmentSum::Owned: m_columns << ctShares << ctBuyPrice << ctLastPrice << ctBuys << ctMarketValue << ctPercentageGain << ctCapitalGain; m_subtotal << ctShares << ctBuyPrice << ctLastPrice << ctBuys << ctMarketValue << ctPercentageGain << ctCapitalGain; break; - case MyMoneyReport::eSumSold: + case eMyMoney::Report::InvestmentSum::Sold: default: m_columns << ctBuys << ctSells << ctCapitalGain; m_subtotal << ctBuys << ctSells << ctCapitalGain; if (m_config.isShowingSTLTCapitalGains()) { m_columns << ctBuysST << ctSellsST << ctCapitalGainST << ctBuysLT << ctSellsLT << ctCapitalGainLT; m_subtotal << ctBuysST << ctSellsST << ctCapitalGainST << ctBuysLT << ctSellsLT << ctCapitalGainLT; } break; } } - if (qc & MyMoneyReport::eQCloan) { + if (qc & eMyMoney::Report::QueryColumn::Loan) { m_columns << ctPayment << ctInterest << ctFees; m_postcolumns << ctBalance; } - if (qc & MyMoneyReport::eQCbalance) + if (qc & eMyMoney::Report::QueryColumn::Balance) m_postcolumns << ctBalance; TableRow::setSortCriteria(sort); qSort(m_rows); if (m_config.isShowingColumnTotals()) constructTotalRows(); // adds total rows to m_rows } void QueryTable::constructTotalRows() { if (m_rows.isEmpty()) return; // qSort places grand total at last position, because it doesn't belong to any group for (int i = 0; i < m_rows.count(); ++i) { if (m_rows.at(0)[ctRank] == QLatin1String("4") || m_rows.at(0)[ctRank] == QLatin1String("5")) // it should be unlikely that total row is at the top of rows, so... m_rows.move(0, m_rows.count() - 1 - i); // ...move it at the bottom else break; } MyMoneyFile* file = MyMoneyFile::instance(); QList subtotals = m_subtotal; QList groups = m_group; QList columns = m_columns; if (!m_subtotal.isEmpty() && subtotals.count() == 1) columns.append(m_subtotal); QList postcolumns = m_postcolumns; if (!m_postcolumns.isEmpty()) columns.append(postcolumns); QMap>> totalCurrency; QList> totalGroups; QMap totalsValues; // initialize all total values under summed columns to be zero foreach (auto subtotal, subtotals) { totalsValues.insert(subtotal, MyMoneyMoney()); } totalsValues.insert(ctRowsCount, MyMoneyMoney()); // create total groups containing totals row for each group totalGroups.append(totalsValues); // prepend with extra group for grand total for (int j = 0; j < groups.count(); ++j) { totalGroups.append(totalsValues); } QList stashedTotalRows; int iCurrentRow, iNextRow; for (iCurrentRow = 0; iCurrentRow < m_rows.count();) { iNextRow = iCurrentRow + 1; // total rows are useless at summing so remove whole block of them at once while (iNextRow != m_rows.count() && (m_rows.at(iNextRow).value(ctRank) == QLatin1String("4") || m_rows.at(iNextRow).value(ctRank) == QLatin1String("5"))) { stashedTotalRows.append(m_rows.takeAt(iNextRow)); // ...but stash them just in case } bool lastRow = (iNextRow == m_rows.count()); // sum all subtotal values for lowest group QString currencyID = m_rows.at(iCurrentRow).value(ctCurrency); if (m_rows.at(iCurrentRow).value(ctRank) == QLatin1String("1")) { // don't sum up on balance (rank = 0 || rank = 3) and minor split (rank = 2) foreach (auto subtotal, subtotals) { if (!totalCurrency.contains(currencyID)) totalCurrency[currencyID].append(totalGroups); totalCurrency[currencyID].last()[subtotal] += MyMoneyMoney(m_rows.at(iCurrentRow)[subtotal]); } totalCurrency[currencyID].last()[ctRowsCount] += MyMoneyMoney::ONE; } // iterate over groups from the lowest to the highest to find group change for (int i = groups.count() - 1; i >= 0 ; --i) { // if any of groups from next row changes (or next row is the last row), then it's time to put totals row if (lastRow || m_rows.at(iCurrentRow)[groups.at(i)] != m_rows.at(iNextRow)[groups.at(i)]) { bool isMainCurrencyTotal = true; QMap>>::iterator currencyGrp = totalCurrency.begin(); while (currencyGrp != totalCurrency.end()) { if (!MyMoneyMoney((*currencyGrp).at(i + 1).value(ctRowsCount)).isZero()) { // if no rows summed up, then no totals row TableRow totalsRow; // sum all subtotal values for higher groups (excluding grand total) and reset lowest group values QMap::iterator upperGrp = (*currencyGrp)[i].begin(); QMap::iterator lowerGrp = (*currencyGrp)[i + 1].begin(); while(upperGrp != (*currencyGrp)[i].end()) { totalsRow[lowerGrp.key()] = lowerGrp.value().toString(); // fill totals row with subtotal values... (*upperGrp) += (*lowerGrp); // (*lowerGrp) = MyMoneyMoney(); ++upperGrp; ++lowerGrp; } // custom total values calculations foreach (auto subtotal, subtotals) { if (subtotal == ctReturnInvestment) totalsRow[subtotal] = helperROI((*currencyGrp).at(i + 1).value(ctBuys) - (*currencyGrp).at(i + 1).value(ctReinvestIncome), (*currencyGrp).at(i + 1).value(ctSells), (*currencyGrp).at(i + 1).value(ctStartingBalance), (*currencyGrp).at(i + 1).value(ctEndingBalance) + (*currencyGrp).at(i + 1).value(ctMarketValue), (*currencyGrp).at(i + 1).value(ctCashIncome)).toString(); else if (subtotal == ctPercentageGain) totalsRow[subtotal] = (((*currencyGrp).at(i + 1).value(ctBuys) + (*currencyGrp).at(i + 1).value(ctMarketValue)) / (*currencyGrp).at(i + 1).value(ctBuys).abs()).toString(); else if (subtotal == ctPrice) totalsRow[subtotal] = MyMoneyMoney((*currencyGrp).at(i + 1).value(ctPrice) / (*currencyGrp).at(i + 1).value(ctRowsCount)).toString(); } // total values that aren't calculated here, but are taken untouched from external source, e.g. constructPerformanceRow if (!stashedTotalRows.isEmpty()) { for (int j = 0; j < stashedTotalRows.count(); ++j) { if (stashedTotalRows.at(j).value(ctCurrency) != currencyID) continue; foreach (auto subtotal, subtotals) { if (subtotal == ctReturn) totalsRow[ctReturn] = stashedTotalRows.takeAt(j)[ctReturn]; } break; } } (*currencyGrp).replace(i + 1, totalsValues); for (int j = 0; j < groups.count(); ++j) { totalsRow[groups.at(j)] = m_rows.at(iCurrentRow)[groups.at(j)]; // ...and identification } currencyID = currencyGrp.key(); if (currencyID.isEmpty() && totalCurrency.count() > 1) currencyID = file->baseCurrency().id(); totalsRow[ctCurrency] = currencyID; if (isMainCurrencyTotal) { totalsRow[ctRank] = QLatin1Char('4'); isMainCurrencyTotal = false; } else totalsRow[ctRank] = QLatin1Char('5'); totalsRow[ctDepth] = QString::number(i); totalsRow.remove(ctRowsCount); m_rows.insert(iNextRow++, totalsRow); // iCurrentRow and iNextRow can diverge here by more than one } ++currencyGrp; } } } // code to put grand total row if (lastRow) { bool isMainCurrencyTotal = true; QMap>>::iterator currencyGrp = totalCurrency.begin(); while (currencyGrp != totalCurrency.end()) { TableRow totalsRow; QMap::const_iterator grandTotalGrp = (*currencyGrp)[0].constBegin(); while(grandTotalGrp != (*currencyGrp)[0].constEnd()) { totalsRow[grandTotalGrp.key()] = grandTotalGrp.value().toString(); ++grandTotalGrp; } foreach (auto subtotal, subtotals) { if (subtotal == ctReturnInvestment) totalsRow[subtotal] = helperROI((*currencyGrp).at(0).value(ctBuys) - (*currencyGrp).at(0).value(ctReinvestIncome), (*currencyGrp).at(0).value(ctSells), (*currencyGrp).at(0).value(ctStartingBalance), (*currencyGrp).at(0).value(ctEndingBalance) + (*currencyGrp).at(0).value(ctMarketValue), (*currencyGrp).at(0).value(ctCashIncome)).toString(); else if (subtotal == ctPercentageGain) totalsRow[subtotal] = (((*currencyGrp).at(0).value(ctBuys) + (*currencyGrp).at(0).value(ctMarketValue)) / (*currencyGrp).at(0).value(ctBuys).abs()).toString(); else if (subtotal == ctPrice) totalsRow[subtotal] = MyMoneyMoney((*currencyGrp).at(0).value(ctPrice) / (*currencyGrp).at(0).value(ctRowsCount)).toString(); } if (!stashedTotalRows.isEmpty()) { for (int j = 0; j < stashedTotalRows.count(); ++j) { foreach (auto subtotal, subtotals) { if (subtotal == ctReturn) totalsRow[ctReturn] = stashedTotalRows.takeAt(j)[ctReturn]; } } } for (int j = 0; j < groups.count(); ++j) { totalsRow[groups.at(j)] = QString(); // no identification } currencyID = currencyGrp.key(); if (currencyID.isEmpty() && totalCurrency.count() > 1) currencyID = file->baseCurrency().id(); totalsRow[ctCurrency] = currencyID; if (isMainCurrencyTotal) { totalsRow[ctRank] = QLatin1Char('4'); isMainCurrencyTotal = false; } else totalsRow[ctRank] = QLatin1Char('5'); totalsRow[ctDepth] = QString(); m_rows.append(totalsRow); ++currencyGrp; } break; // no use to loop further } iCurrentRow = iNextRow; // iCurrent makes here a leap forward by at least one } } void QueryTable::constructTransactionTable() { MyMoneyFile* file = MyMoneyFile::instance(); //make sure we have all subaccounts of investment accounts includeInvestmentSubAccounts(); MyMoneyReport report(m_config); report.setReportAllSplits(false); report.setConsiderCategory(true); bool use_transfers; bool use_summary; bool hide_details; bool tag_special_case = false; switch (m_config.rowType()) { - case MyMoneyReport::eCategory: - case MyMoneyReport::eTopCategory: + case eMyMoney::Report::RowType::Category: + case eMyMoney::Report::RowType::TopCategory: use_summary = false; use_transfers = false; hide_details = false; break; - case MyMoneyReport::ePayee: + case eMyMoney::Report::RowType::Payee: use_summary = false; use_transfers = false; - hide_details = (m_config.detailLevel() == MyMoneyReport::eDetailNone); + hide_details = (m_config.detailLevel() == eMyMoney::Report::DetailLevel::None); break; - case MyMoneyReport::eTag: + case eMyMoney::Report::RowType::Tag: use_summary = false; use_transfers = false; - hide_details = (m_config.detailLevel() == MyMoneyReport::eDetailNone); + hide_details = (m_config.detailLevel() == eMyMoney::Report::DetailLevel::None); tag_special_case = true; break; default: use_summary = true; use_transfers = true; - hide_details = (m_config.detailLevel() == MyMoneyReport::eDetailNone); + hide_details = (m_config.detailLevel() == eMyMoney::Report::DetailLevel::None); break; } // support for opening and closing balances QMap accts; //get all transactions for this report QList transactions = file->transactionList(report); for (QList::const_iterator it_transaction = transactions.constBegin(); it_transaction != transactions.constEnd(); ++it_transaction) { TableRow qA, qS; QDate pd; QList tagIdListCache; qA[ctID] = qS[ctID] = (* it_transaction).id(); qA[ctEntryDate] = qS[ctEntryDate] = (* it_transaction).entryDate().toString(Qt::ISODate); qA[ctPostDate] = qS[ctPostDate] = (* it_transaction).postDate().toString(Qt::ISODate); qA[ctCommodity] = qS[ctCommodity] = (* it_transaction).commodity(); pd = (* it_transaction).postDate(); qA[ctMonth] = qS[ctMonth] = i18n("Month of %1", QDate(pd.year(), pd.month(), 1).toString(Qt::ISODate)); qA[ctWeek] = qS[ctWeek] = i18n("Week of %1", pd.addDays(1 - pd.dayOfWeek()).toString(Qt::ISODate)); if (!m_containsNonBaseCurrency && (*it_transaction).commodity() != file->baseCurrency().id()) m_containsNonBaseCurrency = true; if (report.isConvertCurrency()) qA[ctCurrency] = qS[ctCurrency] = file->baseCurrency().id(); else qA[ctCurrency] = qS[ctCurrency] = (*it_transaction).commodity(); // to handle splits, we decide on which account to base the split // (a reference point or point of view so to speak). here we take the // first account that is a stock account or loan account (or the first account // that is not an income or expense account if there is no stock or loan account) // to be the account (qA) that will have the sub-item "split" entries. we add // one transaction entry (qS) for each subsequent entry in the split. const QList& splits = (*it_transaction).splits(); QList::const_iterator myBegin, it_split; for (it_split = splits.constBegin(), myBegin = splits.constEnd(); it_split != splits.constEnd(); ++it_split) { ReportAccount splitAcc((* it_split).accountId()); // always put split with a "stock" account if it exists if (splitAcc.isInvest()) break; // prefer to put splits with a "loan" account if it exists if (splitAcc.isLoan()) myBegin = it_split; if ((myBegin == splits.end()) && ! splitAcc.isIncomeExpense()) { myBegin = it_split; } } // select our "reference" split if (it_split == splits.end()) { it_split = myBegin; } else { myBegin = it_split; } // skip this transaction if we didn't find a valid base account - see the above description // for the base account's description - if we don't find it avoid a crash by skipping the transaction if (myBegin == splits.end()) continue; // if the split is still unknown, use the first one. I have seen this // happen with a transaction that has only a single split referencing an income or expense // account and has an amount and value of 0. Such a transaction will fall through // the above logic and leave 'it_split' pointing to splits.end() which causes the remainder // of this to end in an infinite loop. if (it_split == splits.end()) { it_split = splits.begin(); } // for "loan" reports, the loan transaction gets special treatment. // the splits of a loan transaction are placed on one line in the // reference (loan) account (qA). however, we process the matching // split entries (qS) normally. bool loan_special_case = false; - if (m_config.queryColumns() & MyMoneyReport::eQCloan) { + if (m_config.queryColumns() & eMyMoney::Report::QueryColumn::Loan) { ReportAccount splitAcc((*it_split).accountId()); loan_special_case = splitAcc.isLoan(); } bool include_me = true; bool transaction_text = false; //indicates whether a text should be considered as a match for the transaction or for a split only QString a_fullname; QString a_memo; int pass = 1; QString myBeginCurrency; QString baseCurrency = file->baseCurrency().id(); QMap xrMap; // container for conversion rates from given currency to myBeginCurrency do { MyMoneyMoney xr; ReportAccount splitAcc((* it_split).accountId()); QString splitCurrency; if (splitAcc.isInvest()) splitCurrency = file->account(file->account((*it_split).accountId()).parentAccountId()).currencyId(); else splitCurrency = file->account((*it_split).accountId()).currencyId(); if (it_split == myBegin) myBeginCurrency = splitCurrency; //get fraction for account int fraction = splitAcc.currency().smallestAccountFraction(); //use base currency fraction if not initialized if (fraction == -1) fraction = file->baseCurrency().smallestAccountFraction(); QString institution = splitAcc.institutionId(); QString payee = (*it_split).payeeId(); const QList tagIdList = (*it_split).tagIdList(); //convert to base currency if (m_config.isConvertCurrency()) { xr = xrMap.value(splitCurrency, xr); // check if there is conversion rate to myBeginCurrency already stored... if (xr == MyMoneyMoney()) // ...if not... xr = (*it_split).price(); // ...take conversion rate to myBeginCurrency from split else if (splitAcc.isInvest()) // if it's stock split... xr *= (*it_split).price(); // ...multiply it by stock price stored in split if (!m_containsNonBaseCurrency && myBeginCurrency != baseCurrency) m_containsNonBaseCurrency = true; if (myBeginCurrency != baseCurrency) { // myBeginCurrency can differ from baseCurrency... MyMoneyPrice price = file->price(myBeginCurrency, baseCurrency, (*it_transaction).postDate()); // ...so check conversion rate... if (price.isValid()) { xr *= price.rate(baseCurrency); // ...and multiply it by current price... qA[ctCurrency] = qS[ctCurrency] = baseCurrency; } else qA[ctCurrency] = qS[ctCurrency] = myBeginCurrency; // ...and set information about non-baseCurrency } } else if (splitAcc.isInvest()) xr = (*it_split).price(); else xr = MyMoneyMoney::ONE; if (it_split == myBegin) { include_me = m_config.includes(splitAcc); if (include_me) // track accts that will need opening and closing balances //FIXME in some cases it will show the opening and closing //balances but no transactions if the splits are all filtered out -- asoliverez accts.insert(splitAcc.id(), splitAcc); qA[ctAccount] = splitAcc.name(); qA[ctAccountID] = splitAcc.id(); qA[ctTopAccount] = splitAcc.topParentName(); if (splitAcc.isInvest()) { // use the institution of the parent for stock accounts institution = splitAcc.parent().institutionId(); MyMoneyMoney shares = (*it_split).shares(); int pricePrecision = file->security(splitAcc.currencyId()).pricePrecision(); qA[ctAction] = (*it_split).action(); qA[ctShares] = shares.isZero() ? QString() : shares.toString(); qA[ctPrice] = shares.isZero() ? QString() : xr.convertPrecision(pricePrecision).toString(); if (((*it_split).action() == MyMoneySplit::actionName(eMyMoney::Split::Action::BuyShares)) && shares.isNegative()) qA[ctAction] = "Sell"; qA[ctInvestAccount] = splitAcc.parent().name(); MyMoneySplit stockSplit = (*it_split); MyMoneySplit assetAccountSplit; QList feeSplits; QList interestSplits; MyMoneySecurity currency; MyMoneySecurity security; eMyMoney::Split::InvestmentTransactionType transactionType; KMyMoneyUtils::dissectTransaction((*it_transaction), stockSplit, assetAccountSplit, feeSplits, interestSplits, security, currency, transactionType); if (!(assetAccountSplit == MyMoneySplit())) { for (it_split = splits.begin(); it_split != splits.end(); ++it_split) { if ((*it_split) == assetAccountSplit) { splitAcc = ReportAccount(assetAccountSplit.accountId()); // switch over from stock split to asset split because amount in stock split doesn't take fees/interests into account myBegin = it_split; // set myBegin to asset split, so stock split can be listed in details under splits myBeginCurrency = (file->account((*myBegin).accountId())).currencyId(); if (!m_containsNonBaseCurrency && myBeginCurrency != baseCurrency) m_containsNonBaseCurrency = true; if (m_config.isConvertCurrency()) { if (myBeginCurrency != baseCurrency) { MyMoneyPrice price = file->price(myBeginCurrency, baseCurrency, (*it_transaction).postDate()); if (price.isValid()) { xr = price.rate(baseCurrency); qA[ctCurrency] = qS[ctCurrency] = baseCurrency; } else qA[ctCurrency] = qS[ctCurrency] = myBeginCurrency; } else xr = MyMoneyMoney::ONE; qA[ctPrice] = shares.isZero() ? QString() : (stockSplit.price() * xr / (*it_split).price()).toString(); // put conversion rate for all splits with this currency, so... // every split of transaction have the same conversion rate xrMap.insert(splitCurrency, MyMoneyMoney::ONE / (*it_split).price()); } else xr = (*it_split).price(); break; } } } } else qA[ctPrice] = xr.toString(); a_fullname = splitAcc.fullName(); a_memo = (*it_split).memo(); transaction_text = m_config.match((*it_split)); qA[ctInstitution] = institution.isEmpty() ? i18n("No Institution") : file->institution(institution).name(); qA[ctPayee] = payee.isEmpty() ? i18n("[Empty Payee]") : file->payee(payee).name().simplified(); if (tag_special_case) { tagIdListCache = tagIdList; } else { QString delimiter; foreach(const auto tagId, tagIdList) { qA[ctTag] += delimiter + file->tag(tagId).name().simplified(); delimiter = QLatin1Char(','); } } qA[ctReconcileDate] = (*it_split).reconcileDate().toString(Qt::ISODate); qA[ctReconcileFlag] = KMyMoneyUtils::reconcileStateToString((*it_split).reconcileFlag(), true); qA[ctNumber] = (*it_split).number(); qA[ctMemo] = a_memo; qA[ctValue] = ((*it_split).shares() * xr).convert(fraction).toString(); qS[ctReconcileDate] = qA[ctReconcileDate]; qS[ctReconcileFlag] = qA[ctReconcileFlag]; qS[ctNumber] = qA[ctNumber]; qS[ctTopCategory] = splitAcc.topParentName(); qS[ctCategoryType] = i18n("Transfer"); // only include the configured accounts if (include_me) { if (loan_special_case) { // put the principal amount in the "value" column and convert to lowest fraction qA[ctValue] = (-(*it_split).shares() * xr).convert(fraction).toString(); qA[ctRank] = QLatin1Char('1'); qA[ctSplit].clear(); } else { if ((splits.count() > 2) && use_summary) { // add the "summarized" split transaction // this is the sub-total of the split detail // convert to lowest fraction qA[ctRank] = QLatin1Char('1'); qA[ctCategory] = i18n("[Split Transaction]"); qA[ctTopCategory] = i18nc("Split transaction", "Split"); qA[ctCategoryType] = i18nc("Split transaction", "Split"); m_rows += qA; } } } } else { if (include_me) { if (loan_special_case) { MyMoneyMoney value = (-(* it_split).shares() * xr).convert(fraction); if ((*it_split).action() == MyMoneySplit::actionName(eMyMoney::Split::Action::Amortization)) { // put the payment in the "payment" column and convert to lowest fraction qA[ctPayee] = value.toString(); } else if ((*it_split).action() == MyMoneySplit::actionName(eMyMoney::Split::Action::Interest)) { // put the interest in the "interest" column and convert to lowest fraction qA[ctInterest] = value.toString(); } else if (splits.count() > 2) { // [dv: This comment carried from the original code. I am // not exactly clear on what it means or why we do this.] // Put the initial pay-in nowhere (that is, ignore it). This // is dangerous, though. The only way I can tell the initial // pay-in apart from fees is if there are only 2 splits in // the transaction. I wish there was a better way. } else { // accumulate everything else in the "fees" column MyMoneyMoney n0 = MyMoneyMoney(qA[ctFees]); qA[ctFees] = (n0 + value).toString(); } // we don't add qA here for a loan transaction. we'll add one // qA afer all of the split components have been processed. // (see below) } //--- special case to hide split transaction details else if (hide_details && (splits.count() > 2)) { // essentially, don't add any qA entries } //--- default case includes all transaction details else { //this is when the splits are going to be shown as children of the main split if ((splits.count() > 2) && use_summary) { qA[ctValue].clear(); //convert to lowest fraction qA[ctSplit] = (-(*it_split).shares() * xr).convert(fraction).toString(); qA[ctRank] = QLatin1Char('2'); } else { //this applies when the transaction has only 2 splits, or each split is going to be //shown separately, eg. transactions by category switch (m_config.rowType()) { - case MyMoneyReport::eCategory: - case MyMoneyReport::eTopCategory: + case eMyMoney::Report::RowType::Category: + case eMyMoney::Report::RowType::TopCategory: if (splitAcc.isIncomeExpense()) qA[ctValue] = (-(*it_split).shares() * xr).convert(fraction).toString(); // needed for category reports, in case of multicurrency transaction it breaks it break; default: break; } qA[ctSplit].clear(); qA[ctRank] = QLatin1Char('1'); } qA [ctMemo] = (*it_split).memo(); if (!m_containsNonBaseCurrency && splitAcc.currencyId() != file->baseCurrency().id()) m_containsNonBaseCurrency = true; if (report.isConvertCurrency()) qS[ctCurrency] = file->baseCurrency().id(); else qS[ctCurrency] = splitAcc.currency().id(); if (! splitAcc.isIncomeExpense()) { qA[ctCategory] = ((*it_split).shares().isNegative()) ? i18n("Transfer from %1", splitAcc.fullName()) : i18n("Transfer to %1", splitAcc.fullName()); qA[ctTopCategory] = splitAcc.topParentName(); qA[ctCategoryType] = i18n("Transfer"); } else { qA [ctCategory] = splitAcc.fullName(); qA [ctTopCategory] = splitAcc.topParentName(); qA [ctCategoryType] = MyMoneyAccount::accountTypeToString(splitAcc.accountGroup()); } if (use_transfers || (splitAcc.isIncomeExpense() && m_config.includes(splitAcc))) { //if it matches the text of the main split of the transaction or //it matches this particular split, include it //otherwise, skip it //if the filter is "does not contain" exclude the split if it does not match //even it matches the whole split if ((m_config.isInvertingText() && m_config.match((*it_split))) || (!m_config.isInvertingText() && (transaction_text || m_config.match((*it_split))))) { if (tag_special_case) { if (!tagIdListCache.size()) qA[ctTag] = i18n("[No Tag]"); else for (int i = 0; i < tagIdListCache.size(); i++) { qA[ctTag] = file->tag(tagIdListCache[i]).name().simplified(); m_rows += qA; } } else { m_rows += qA; } } } } } if (m_config.includes(splitAcc) && use_transfers && !(splitAcc.isInvest() && include_me)) { // otherwise stock split is displayed twice in report if (! splitAcc.isIncomeExpense()) { //multiply by currency and convert to lowest fraction qS[ctValue] = ((*it_split).shares() * xr).convert(fraction).toString(); qS[ctRank] = QLatin1Char('1'); qS[ctAccount] = splitAcc.name(); qS[ctAccountID] = splitAcc.id(); qS[ctTopAccount] = splitAcc.topParentName(); qS[ctCategory] = ((*it_split).shares().isNegative()) ? i18n("Transfer to %1", a_fullname) : i18n("Transfer from %1", a_fullname); qS[ctInstitution] = institution.isEmpty() ? i18n("No Institution") : file->institution(institution).name(); qS[ctMemo] = (*it_split).memo().isEmpty() ? a_memo : (*it_split).memo(); //FIXME-ALEX When is used this? I can't find in which condition we arrive here... maybe this code is useless? QString delimiter; for (int i = 0; i < tagIdList.size(); i++) { qA[ctTag] += delimiter + file->tag(tagIdList[i]).name().simplified(); delimiter = '+'; } qS[ctPayee] = payee.isEmpty() ? qA[ctPayee] : file->payee(payee).name().simplified(); //check the specific split against the filter for text and amount //TODO this should be done at the engine, but I have no clear idea how -- asoliverez //if the filter is "does not contain" exclude the split if it does not match //even it matches the whole split if ((m_config.isInvertingText() && m_config.match((*it_split))) || (!m_config.isInvertingText() && (transaction_text || m_config.match((*it_split))))) { m_rows += qS; // track accts that will need opening and closing balances accts.insert(splitAcc.id(), splitAcc); } } } } ++it_split; // look for wrap-around if (it_split == splits.end()) it_split = splits.begin(); // but terminate if this transaction has only a single split if (splits.count() < 2) break; //check if there have been more passes than there are splits //this is to prevent infinite loops in cases of data inconsistency -- asoliverez ++pass; if (pass > splits.count()) break; } while (it_split != myBegin); if (loan_special_case) { m_rows += qA; } } // now run through our accts list and add opening and closing balances switch (m_config.rowType()) { - case MyMoneyReport::eAccount: - case MyMoneyReport::eTopAccount: + case eMyMoney::Report::RowType::Account: + case eMyMoney::Report::RowType::TopAccount: break; - // case MyMoneyReport::eCategory: + // case eMyMoney::Report::RowType::Category: // case MyMoneyReport::eTopCategory: // case MyMoneyReport::ePayee: // case MyMoneyReport::eMonth: // case MyMoneyReport::eWeek: default: return; } QDate startDate, endDate; report.validDateRange(startDate, endDate); QString strStartDate = startDate.toString(Qt::ISODate); QString strEndDate = endDate.toString(Qt::ISODate); startDate = startDate.addDays(-1); QMap::const_iterator it_account, accts_end; for (it_account = accts.constBegin(); it_account != accts.constEnd(); ++it_account) { TableRow qA; ReportAccount account(*it_account); //get fraction for account int fraction = account.currency().smallestAccountFraction(); //use base currency fraction if not initialized if (fraction == -1) fraction = file->baseCurrency().smallestAccountFraction(); QString institution = account.institutionId(); // use the institution of the parent for stock accounts if (account.isInvest()) institution = account.parent().institutionId(); MyMoneyMoney startBalance, endBalance, startPrice, endPrice; MyMoneyMoney startShares, endShares; //get price and convert currency if necessary if (m_config.isConvertCurrency()) { startPrice = (account.deepCurrencyPrice(startDate) * account.baseCurrencyPrice(startDate)).reduce(); endPrice = (account.deepCurrencyPrice(endDate) * account.baseCurrencyPrice(endDate)).reduce(); } else { startPrice = account.deepCurrencyPrice(startDate).reduce(); endPrice = account.deepCurrencyPrice(endDate).reduce(); } startShares = file->balance(account.id(), startDate); endShares = file->balance(account.id(), endDate); //get starting and ending balances startBalance = startShares * startPrice; endBalance = endShares * endPrice; //starting balance // don't show currency if we're converting or if it's not foreign if (!m_containsNonBaseCurrency && account.currency().id() != file->baseCurrency().id()) m_containsNonBaseCurrency = true; if (m_config.isConvertCurrency()) qA[ctCurrency] = file->baseCurrency().id(); else qA[ctCurrency] = account.currency().id(); qA[ctAccountID] = account.id(); qA[ctAccount] = account.name(); qA[ctTopAccount] = account.topParentName(); qA[ctInstitution] = institution.isEmpty() ? i18n("No Institution") : file->institution(institution).name(); qA[ctRank] = QLatin1Char('0'); qA[ctPrice] = startPrice.convertPrecision(account.currency().pricePrecision()).toString(); if (account.isInvest()) { qA[ctShares] = startShares.toString(); } qA[ctPostDate] = strStartDate; qA[ctBalance] = startBalance.convert(fraction).toString(); qA[ctValue].clear(); qA[ctID] = QLatin1Char('A'); m_rows += qA; //ending balance qA[ctPrice] = endPrice.convertPrecision(account.currency().pricePrecision()).toString(); if (account.isInvest()) { qA[ctShares] = endShares.toString(); } qA[ctPostDate] = strEndDate; qA[ctBalance] = endBalance.toString(); qA[ctRank] = QLatin1Char('3'); qA[ctID] = QLatin1Char('Z'); m_rows += qA; } } MyMoneyMoney QueryTable::helperROI(const MyMoneyMoney &buys, const MyMoneyMoney &sells, const MyMoneyMoney &startingBal, const MyMoneyMoney &endingBal, const MyMoneyMoney &cashIncome) const { MyMoneyMoney returnInvestment; if (!buys.isZero() || !startingBal.isZero()) { returnInvestment = (sells + buys + cashIncome + endingBal - startingBal) / (startingBal - buys); returnInvestment = returnInvestment.convert(10000); } else returnInvestment = MyMoneyMoney(); // if no investment then no return on investment return returnInvestment; } MyMoneyMoney QueryTable::helperIRR(const CashFlowList &all) const { MyMoneyMoney annualReturn; try { double irr = all.IRR(); #ifdef Q_CC_MSVC annualReturn = MyMoneyMoney(_isnan(irr) ? 0 : irr, 10000); #else annualReturn = MyMoneyMoney(std::isnan(irr) ? 0 : irr, 10000); #endif } catch (QString e) { qDebug() << e; } return annualReturn; } void QueryTable::sumInvestmentValues(const ReportAccount& account, QList& cfList, QList& shList) const { for (int i = InvestmentValue::Buys; i < InvestmentValue::End; ++i) cfList.append(CashFlowList()); for (int i = InvestmentValue::Buys; i <= InvestmentValue::BuysOfOwned; ++i) shList.append(MyMoneyMoney()); MyMoneyFile* file = MyMoneyFile::instance(); MyMoneyReport report = m_config; QDate startingDate; QDate endingDate; QDate newStartingDate; QDate newEndingDate; const bool isSTLT = report.isShowingSTLTCapitalGains(); const int settlementPeriod = report.settlementPeriod(); QDate termSeparator = report.termSeparator().addDays(-settlementPeriod); report.validDateRange(startingDate, endingDate); newStartingDate = startingDate; newEndingDate = endingDate; - if (report.queryColumns() & MyMoneyReport::eQCcapitalgain) { + if (report.queryColumns() & eMyMoney::Report::QueryColumn::CapitalGain) { // Saturday and Sunday aren't valid settlement dates if (endingDate.dayOfWeek() == Qt::Saturday) endingDate = endingDate.addDays(-1); else if (endingDate.dayOfWeek() == Qt::Sunday) endingDate = endingDate.addDays(-2); if (termSeparator.dayOfWeek() == Qt::Saturday) termSeparator = termSeparator.addDays(-1); else if (termSeparator.dayOfWeek() == Qt::Sunday) termSeparator = termSeparator.addDays(-2); if (startingDate.daysTo(endingDate) <= settlementPeriod) // no days to check for return; termSeparator = termSeparator.addDays(-settlementPeriod); newEndingDate = endingDate.addDays(-settlementPeriod); } shList[BuysOfOwned] = file->balance(account.id(), newEndingDate); // get how many shares there are at the end of period MyMoneyMoney stashedBuysOfOwned = shList.at(BuysOfOwned); bool reportedDateRange = true; // flag marking sell transactions between startingDate and endingDate report.setReportAllSplits(false); report.setConsiderCategory(true); report.clearAccountFilter(); report.addAccount(account.id()); report.setDateFilter(newStartingDate, newEndingDate); do { QList transactions = file->transactionList(report); for (QList::const_reverse_iterator it_t = transactions.crbegin(); it_t != transactions.crend(); ++it_t) { MyMoneySplit shareSplit = (*it_t).splitByAccount(account.id()); MyMoneySplit assetAccountSplit; QList feeSplits; QList interestSplits; MyMoneySecurity security; MyMoneySecurity currency; eMyMoney::Split::InvestmentTransactionType transactionType; KMyMoneyUtils::dissectTransaction((*it_t), shareSplit, assetAccountSplit, feeSplits, interestSplits, security, currency, transactionType); QDate postDate = (*it_t).postDate(); MyMoneyMoney price; //get price for the day of the transaction if we have to calculate base currency //we are using the value of the split which is in deep currency if (m_config.isConvertCurrency()) price = account.baseCurrencyPrice(postDate); //we only need base currency because the value is in deep currency else price = MyMoneyMoney::ONE; MyMoneyMoney value = assetAccountSplit.value() * price; MyMoneyMoney shares = shareSplit.shares(); if (transactionType == eMyMoney::Split::InvestmentTransactionType::BuyShares) { if (reportedDateRange) { cfList[Buys].append(CashFlowListItem(postDate, value)); shList[Buys] += shares; } if (shList.at(BuysOfOwned).isZero()) { // add sold shares if (shList.at(BuysOfSells) + shares > shList.at(Sells).abs()) { // add partially sold shares MyMoneyMoney tempVal = (((shList.at(Sells).abs() - shList.at(BuysOfSells))) / shares) * value; cfList[BuysOfSells].append(CashFlowListItem(postDate, tempVal)); shList[BuysOfSells] = shList.at(Sells).abs(); if (isSTLT && postDate < termSeparator) { cfList[LongTermBuysOfSells].append(CashFlowListItem(postDate, tempVal)); shList[LongTermBuysOfSells] = shList.at(BuysOfSells); } } else { // add wholly sold shares cfList[BuysOfSells].append(CashFlowListItem(postDate, value)); shList[BuysOfSells] += shares; if (isSTLT && postDate < termSeparator) { cfList[LongTermBuysOfSells].append(CashFlowListItem(postDate, value)); shList[LongTermBuysOfSells] += shares; } } } else if (shList.at(BuysOfOwned) >= shares) { // subtract not-sold shares shList[BuysOfOwned] -= shares; cfList[BuysOfOwned].append(CashFlowListItem(postDate, value)); } else { // subtract partially not-sold shares MyMoneyMoney tempVal = ((shares - shList.at(BuysOfOwned)) / shares) * value; MyMoneyMoney tempVal2 = (shares - shList.at(BuysOfOwned)); cfList[BuysOfSells].append(CashFlowListItem(postDate, tempVal)); shList[BuysOfSells] += tempVal2; if (isSTLT && postDate < termSeparator) { cfList[LongTermBuysOfSells].append(CashFlowListItem(postDate, tempVal)); shList[LongTermBuysOfSells] += tempVal2; } cfList[BuysOfOwned].append(CashFlowListItem(postDate, (shList.at(BuysOfOwned) / shares) * value)); shList[BuysOfOwned] = MyMoneyMoney(); } } else if (transactionType == eMyMoney::Split::InvestmentTransactionType::SellShares && reportedDateRange) { cfList[Sells].append(CashFlowListItem(postDate, value)); shList[Sells] += shares; } else if (transactionType == eMyMoney::Split::InvestmentTransactionType::SplitShares) { // shares variable is denominator of split ratio here for (int i = Buys; i <= InvestmentValue::BuysOfOwned; ++i) shList[i] /= shares; } else if (transactionType == eMyMoney::Split::InvestmentTransactionType::AddShares || // added shares, when sold give 100% capital gain transactionType == eMyMoney::Split::InvestmentTransactionType::ReinvestDividend) { if (shList.at(BuysOfOwned).isZero()) { // add added/reinvested shares if (shList.at(BuysOfSells) + shares > shList.at(Sells).abs()) { // add partially added/reinvested shares shList[BuysOfSells] = shList.at(Sells).abs(); if (postDate < termSeparator) shList[LongTermBuysOfSells] = shList[BuysOfSells]; } else { // add wholly added/reinvested shares shList[BuysOfSells] += shares; if (postDate < termSeparator) shList[LongTermBuysOfSells] += shares; } } else if (shList.at(BuysOfOwned) >= shares) { // subtract not-added/not-reinvested shares shList[BuysOfOwned] -= shares; cfList[BuysOfOwned].append(CashFlowListItem(postDate, value)); } else { // subtract partially not-added/not-reinvested shares MyMoneyMoney tempVal = (shares - shList.at(BuysOfOwned)); shList[BuysOfSells] += tempVal; if (postDate < termSeparator) shList[LongTermBuysOfSells] += tempVal; cfList[BuysOfOwned].append(CashFlowListItem(postDate, (shList.at(BuysOfOwned) / shares) * value)); shList[BuysOfOwned] = MyMoneyMoney(); } if (transactionType == eMyMoney::Split::InvestmentTransactionType::ReinvestDividend) { value = MyMoneyMoney(); foreach (const auto split, interestSplits) value += split.value(); value *= price; cfList[ReinvestIncome].append(CashFlowListItem(postDate, -value)); } } else if (transactionType == eMyMoney::Split::InvestmentTransactionType::RemoveShares && reportedDateRange) // removed shares give no value in return so no capital gain on them shList[Sells] += shares; else if (transactionType == eMyMoney::Split::InvestmentTransactionType::Dividend || transactionType == eMyMoney::Split::InvestmentTransactionType::Yield) cfList[CashIncome].append(CashFlowListItem(postDate, value)); } reportedDateRange = false; newEndingDate = newStartingDate; newStartingDate = newStartingDate.addYears(-1); report.setDateFilter(newStartingDate, newEndingDate); // search for matching buy transactions year earlier } while ( ( - (report.investmentSum() == MyMoneyReport::eSumOwned && !shList[BuysOfOwned].isZero()) || - (report.investmentSum() == MyMoneyReport::eSumSold && !shList.at(Sells).isZero() && shList.at(Sells).abs() > shList.at(BuysOfSells).abs()) || - (report.investmentSum() == MyMoneyReport::eSumOwnedAndSold && (!shList[BuysOfOwned].isZero() || (!shList.at(Sells).isZero() && shList.at(Sells).abs() > shList.at(BuysOfSells).abs()))) + (report.investmentSum() == eMyMoney::Report::InvestmentSum::Owned && !shList[BuysOfOwned].isZero()) || + (report.investmentSum() == eMyMoney::Report::InvestmentSum::Sold && !shList.at(Sells).isZero() && shList.at(Sells).abs() > shList.at(BuysOfSells).abs()) || + (report.investmentSum() == eMyMoney::Report::InvestmentSum::OwnedAndSold && (!shList[BuysOfOwned].isZero() || (!shList.at(Sells).isZero() && shList.at(Sells).abs() > shList.at(BuysOfSells).abs()))) ) && account.openingDate() <= newEndingDate ); // we've got buy value and no sell value of long-term shares, so get them if (isSTLT && !shList[LongTermBuysOfSells].isZero()) { newStartingDate = startingDate; newEndingDate = endingDate.addDays(-settlementPeriod); report.setDateFilter(newStartingDate, newEndingDate); // search for matching buy transactions year earlier QList transactions = file->transactionList(report); shList[BuysOfOwned] = shList[LongTermBuysOfSells]; foreach (const auto transaction, transactions) { MyMoneySplit shareSplit = transaction.splitByAccount(account.id()); MyMoneySplit assetAccountSplit; QList feeSplits; QList interestSplits; MyMoneySecurity security; MyMoneySecurity currency; eMyMoney::Split::InvestmentTransactionType transactionType; KMyMoneyUtils::dissectTransaction(transaction, shareSplit, assetAccountSplit, feeSplits, interestSplits, security, currency, transactionType); QDate postDate = transaction.postDate(); MyMoneyMoney price; if (m_config.isConvertCurrency()) price = account.baseCurrencyPrice(postDate); //we only need base currency because the value is in deep currency else price = MyMoneyMoney::ONE; MyMoneyMoney value = assetAccountSplit.value() * price; MyMoneyMoney shares = shareSplit.shares(); if (transactionType == eMyMoney::Split::InvestmentTransactionType::SellShares) { if ((shList.at(LongTermSellsOfBuys) + shares).abs() >= shList.at(LongTermBuysOfSells)) { // add partially sold long-term shares cfList[LongTermSellsOfBuys].append(CashFlowListItem(postDate, (shList.at(LongTermSellsOfBuys).abs() - shList.at(LongTermBuysOfSells)) / shares * value)); shList[LongTermSellsOfBuys] = shList.at(LongTermBuysOfSells); break; } else { // add wholly sold long-term shares cfList[LongTermSellsOfBuys].append(CashFlowListItem(postDate, value)); shList[LongTermSellsOfBuys] += shares; } } else if (transactionType == eMyMoney::Split::InvestmentTransactionType::RemoveShares) { if ((shList.at(LongTermSellsOfBuys) + shares).abs() >= shList.at(LongTermBuysOfSells)) { shList[LongTermSellsOfBuys] = shList.at(LongTermBuysOfSells); break; } else shList[LongTermSellsOfBuys] += shares; } } } shList[BuysOfOwned] = stashedBuysOfOwned; report.setDateFilter(startingDate, endingDate); // reset data filter for next security return; } void QueryTable::constructPerformanceRow(const ReportAccount& account, TableRow& result, CashFlowList &all) const { MyMoneyReport report = m_config; QDate startingDate; QDate endingDate; report.validDateRange(startingDate, endingDate); startingDate = startingDate.addDays(-1); MyMoneyFile* file = MyMoneyFile::instance(); //get fraction depending on type of account int fraction = account.currency().smallestAccountFraction(); MyMoneyMoney price; if (m_config.isConvertCurrency()) price = account.deepCurrencyPrice(startingDate) * account.baseCurrencyPrice(startingDate); else price = account.deepCurrencyPrice(startingDate); MyMoneyMoney startingBal = file->balance(account.id(), startingDate) * price; //convert to lowest fraction startingBal = startingBal.convert(fraction); //calculate ending balance if (m_config.isConvertCurrency()) price = account.deepCurrencyPrice(endingDate) * account.baseCurrencyPrice(endingDate); else price = account.deepCurrencyPrice(endingDate); MyMoneyMoney endingBal = file->balance((account).id(), endingDate) * price; //convert to lowest fraction endingBal = endingBal.convert(fraction); QList cfList; QList shList; sumInvestmentValues(account, cfList, shList); MyMoneyMoney buysTotal; MyMoneyMoney sellsTotal; MyMoneyMoney cashIncomeTotal; MyMoneyMoney reinvestIncomeTotal; switch (m_config.investmentSum()) { - case MyMoneyReport::eSumOwnedAndSold: + case eMyMoney::Report::InvestmentSum::OwnedAndSold: buysTotal = cfList.at(BuysOfSells).total() + cfList.at(BuysOfOwned).total(); sellsTotal = cfList.at(Sells).total(); cashIncomeTotal = cfList.at(CashIncome).total(); reinvestIncomeTotal = cfList.at(ReinvestIncome).total(); startingBal = MyMoneyMoney(); if (buysTotal.isZero() && sellsTotal.isZero() && cashIncomeTotal.isZero() && reinvestIncomeTotal.isZero()) return; all.append(cfList.at(BuysOfSells)); all.append(cfList.at(BuysOfOwned)); all.append(cfList.at(Sells)); all.append(cfList.at(CashIncome)); result[ctSells] = sellsTotal.toString(); result[ctCashIncome] = cashIncomeTotal.toString(); result[ctReinvestIncome] = reinvestIncomeTotal.toString(); result[ctEndingBalance] = endingBal.toString(); break; - case MyMoneyReport::eSumOwned: + case eMyMoney::Report::InvestmentSum::Owned: buysTotal = cfList.at(BuysOfOwned).total(); startingBal = MyMoneyMoney(); if (buysTotal.isZero() && endingBal.isZero()) return; all.append(cfList.at(BuysOfOwned)); all.append(CashFlowListItem(endingDate, endingBal)); result[ctReinvestIncome] = reinvestIncomeTotal.toString(); result[ctMarketValue] = endingBal.toString(); break; - case MyMoneyReport::eSumSold: + case eMyMoney::Report::InvestmentSum::Sold: buysTotal = cfList.at(BuysOfSells).total(); sellsTotal = cfList.at(Sells).total(); cashIncomeTotal = cfList.at(CashIncome).total(); startingBal = endingBal = MyMoneyMoney(); // check if there are any meaningfull values before adding them to results if (buysTotal.isZero() && sellsTotal.isZero() && cashIncomeTotal.isZero()) return; all.append(cfList.at(BuysOfSells)); all.append(cfList.at(Sells)); all.append(cfList.at(CashIncome)); result[ctSells] = sellsTotal.toString(); result[ctCashIncome] = cashIncomeTotal.toString(); break; - case MyMoneyReport::eSumPeriod: + case eMyMoney::Report::InvestmentSum::Period: default: buysTotal = cfList.at(Buys).total(); sellsTotal = cfList.at(Sells).total(); cashIncomeTotal = cfList.at(CashIncome).total(); reinvestIncomeTotal = cfList.at(ReinvestIncome).total(); if (buysTotal.isZero() && sellsTotal.isZero() && cashIncomeTotal.isZero() && reinvestIncomeTotal.isZero() && startingBal.isZero() && endingBal.isZero()) return; all.append(cfList.at(Buys)); all.append(cfList.at(Sells)); all.append(cfList.at(CashIncome)); all.append(CashFlowListItem(startingDate, -startingBal)); all.append(CashFlowListItem(endingDate, endingBal)); result[ctSells] = sellsTotal.toString(); result[ctCashIncome] = cashIncomeTotal.toString(); result[ctReinvestIncome] = reinvestIncomeTotal.toString(); result[ctStartingBalance] = startingBal.toString(); result[ctEndingBalance] = endingBal.toString(); break; } MyMoneyMoney returnInvestment = helperROI(buysTotal - reinvestIncomeTotal, sellsTotal, startingBal, endingBal, cashIncomeTotal); MyMoneyMoney annualReturn = helperIRR(all); result[ctBuys] = buysTotal.toString(); result[ctReturn] = annualReturn.toString(); result[ctReturnInvestment] = returnInvestment.toString(); result[ctEquityType] = MyMoneySecurity::securityTypeToString(file->security(account.currencyId()).securityType()); } void QueryTable::constructCapitalGainRow(const ReportAccount& account, TableRow& result) const { MyMoneyFile* file = MyMoneyFile::instance(); QList cfList; QList shList; sumInvestmentValues(account, cfList, shList); MyMoneyMoney buysTotal = cfList.at(BuysOfSells).total(); MyMoneyMoney sellsTotal = cfList.at(Sells).total(); MyMoneyMoney longTermBuysOfSellsTotal = cfList.at(LongTermBuysOfSells).total(); MyMoneyMoney longTermSellsOfBuys = cfList.at(LongTermSellsOfBuys).total(); switch (m_config.investmentSum()) { - case MyMoneyReport::eSumOwned: + case eMyMoney::Report::InvestmentSum::Owned: { if (shList.at(BuysOfOwned).isZero()) return; MyMoneyReport report = m_config; QDate startingDate; QDate endingDate; report.validDateRange(startingDate, endingDate); //get fraction depending on type of account int fraction = account.currency().smallestAccountFraction(); MyMoneyMoney price; //calculate ending balance if (m_config.isConvertCurrency()) price = account.deepCurrencyPrice(endingDate) * account.baseCurrencyPrice(endingDate); else price = account.deepCurrencyPrice(endingDate); MyMoneyMoney endingBal = shList.at(BuysOfOwned) * price; //convert to lowest fraction endingBal = endingBal.convert(fraction); buysTotal = cfList.at(BuysOfOwned).total() - cfList.at(ReinvestIncome).total(); int pricePrecision = file->security(account.currencyId()).pricePrecision(); result[ctBuys] = buysTotal.toString(); result[ctShares] = shList.at(BuysOfOwned).toString(); result[ctBuyPrice] = (buysTotal.abs() / shList.at(BuysOfOwned)).convertPrecision(pricePrecision).toString(); result[ctLastPrice] = price.toString(); result[ctMarketValue] = endingBal.toString(); result[ctCapitalGain] = (buysTotal + endingBal).toString(); result[ctPercentageGain] = ((buysTotal + endingBal)/buysTotal.abs()).toString(); break; } - case MyMoneyReport::eSumSold: + case eMyMoney::Report::InvestmentSum::Sold: default: buysTotal = cfList.at(BuysOfSells).total() - cfList.at(ReinvestIncome).total(); sellsTotal = cfList.at(Sells).total(); longTermBuysOfSellsTotal = cfList.at(LongTermBuysOfSells).total(); longTermSellsOfBuys = cfList.at(LongTermSellsOfBuys).total(); // check if there are any meaningfull values before adding them to results if (buysTotal.isZero() && sellsTotal.isZero() && longTermBuysOfSellsTotal.isZero() && longTermSellsOfBuys.isZero()) return; result[ctBuys] = buysTotal.toString(); result[ctSells] = sellsTotal.toString(); result[ctCapitalGain] = (buysTotal + sellsTotal).toString(); if (m_config.isShowingSTLTCapitalGains()) { result[ctBuysLT] = longTermBuysOfSellsTotal.toString(); result[ctSellsLT] = longTermSellsOfBuys.toString(); result[ctCapitalGainLT] = (longTermBuysOfSellsTotal + longTermSellsOfBuys).toString(); result[ctBuysST] = (buysTotal - longTermBuysOfSellsTotal).toString(); result[ctSellsST] = (sellsTotal - longTermSellsOfBuys).toString(); result[ctCapitalGainST] = ((buysTotal - longTermBuysOfSellsTotal) + (sellsTotal - longTermSellsOfBuys)).toString(); } break; } result[ctEquityType] = MyMoneySecurity::securityTypeToString(file->security(account.currencyId()).securityType()); } void QueryTable::constructAccountTable() { MyMoneyFile* file = MyMoneyFile::instance(); //make sure we have all subaccounts of investment accounts includeInvestmentSubAccounts(); QMap> currencyCashFlow; // for total calculation QList accounts; file->accountList(accounts); for (auto it_account = accounts.constBegin(); it_account != accounts.constEnd(); ++it_account) { // Note, "Investment" accounts are never included in account rows because // they don't contain anything by themselves. In reports, they are only // useful as a "topaccount" aggregator of stock accounts if ((*it_account).isAssetLiability() && m_config.includes((*it_account)) && (*it_account).accountType() != eMyMoney::Account::Type::Investment) { // don't add the account if it is closed. In fact, the business logic // should prevent that an account can be closed with a balance not equal // to zero, but we never know. MyMoneyMoney shares = file->balance((*it_account).id(), m_config.toDate()); if (shares.isZero() && (*it_account).isClosed()) continue; ReportAccount account(*it_account); TableRow qaccountrow; CashFlowList accountCashflow; // for total calculation switch(m_config.queryColumns()) { - case MyMoneyReport::eQCperformance: + case eMyMoney::Report::QueryColumn::Performance: { constructPerformanceRow(account, qaccountrow, accountCashflow); if (!qaccountrow.isEmpty()) { // assuming that that report is grouped by topaccount qaccountrow[ctTopAccount] = account.topParentName(); if (!m_containsNonBaseCurrency && account.currency().id() != file->baseCurrency().id()) m_containsNonBaseCurrency = true; if (m_config.isConvertCurrency()) qaccountrow[ctCurrency] = file->baseCurrency().id(); else qaccountrow[ctCurrency] = account.currency().id(); if (!currencyCashFlow.value(qaccountrow.value(ctCurrency)).contains(qaccountrow.value(ctTopAccount))) currencyCashFlow[qaccountrow.value(ctCurrency)].insert(qaccountrow.value(ctTopAccount), accountCashflow); // create cashflow for unknown account... else currencyCashFlow[qaccountrow.value(ctCurrency)][qaccountrow.value(ctTopAccount)] += accountCashflow; // ...or add cashflow for known account } break; } - case MyMoneyReport::eQCcapitalgain: + case eMyMoney::Report::QueryColumn::CapitalGain: constructCapitalGainRow(account, qaccountrow); break; default: { //get fraction for account int fraction = account.currency().smallestAccountFraction() != -1 ? account.currency().smallestAccountFraction() : file->baseCurrency().smallestAccountFraction(); MyMoneyMoney netprice = account.deepCurrencyPrice(m_config.toDate()); if (m_config.isConvertCurrency() && account.isForeignCurrency()) netprice *= account.baseCurrencyPrice(m_config.toDate()); // display currency is base currency, so set the price netprice = netprice.reduce(); shares = shares.reduce(); int pricePrecision = file->security(account.currencyId()).pricePrecision(); qaccountrow[ctPrice] = netprice.convertPrecision(pricePrecision).toString(); qaccountrow[ctValue] = (netprice * shares).convert(fraction).toString(); qaccountrow[ctShares] = shares.toString(); QString iid = account.institutionId(); // If an account does not have an institution, get it from the top-parent. if (iid.isEmpty() && !account.isTopLevel()) iid = account.topParent().institutionId(); if (iid.isEmpty()) qaccountrow[ctInstitution] = i18nc("No institution", "None"); else qaccountrow[ctInstitution] = file->institution(iid).name(); qaccountrow[ctType] = MyMoneyAccount::accountTypeToString(account.accountType()); } } if (qaccountrow.isEmpty()) // don't add the account if there are no calculated values continue; qaccountrow[ctRank] = QLatin1Char('1'); qaccountrow[ctAccount] = account.name(); qaccountrow[ctAccountID] = account.id(); qaccountrow[ctTopAccount] = account.topParentName(); if (!m_containsNonBaseCurrency && account.currency().id() != file->baseCurrency().id()) m_containsNonBaseCurrency = true; if (m_config.isConvertCurrency()) qaccountrow[ctCurrency] = file->baseCurrency().id(); else qaccountrow[ctCurrency] = account.currency().id(); m_rows.append(qaccountrow); } } - if (m_config.queryColumns() == MyMoneyReport::eQCperformance && m_config.isShowingColumnTotals()) { + if (m_config.queryColumns() == eMyMoney::Report::QueryColumn::Performance && m_config.isShowingColumnTotals()) { TableRow qtotalsrow; qtotalsrow[ctRank] = QLatin1Char('4'); // add identification of row as total QMap currencyGrandCashFlow; QMap>::iterator currencyAccGrp = currencyCashFlow.begin(); while (currencyAccGrp != currencyCashFlow.end()) { // convert map of top accounts with cashflows to TableRow for (QMap::iterator topAccount = (*currencyAccGrp).begin(); topAccount != (*currencyAccGrp).end(); ++topAccount) { qtotalsrow[ctTopAccount] = topAccount.key(); qtotalsrow[ctReturn] = helperIRR(topAccount.value()).toString(); qtotalsrow[ctCurrency] = currencyAccGrp.key(); currencyGrandCashFlow[currencyAccGrp.key()] += topAccount.value(); // cumulative sum of cashflows of each topaccount m_rows.append(qtotalsrow); // rows aren't sorted yet, so no problem with adding them randomly at the end } ++currencyAccGrp; } QMap::iterator currencyGrp = currencyGrandCashFlow.begin(); qtotalsrow[ctTopAccount].clear(); // empty topaccount because it's grand cashflow while (currencyGrp != currencyGrandCashFlow.end()) { qtotalsrow[ctReturn] = helperIRR(currencyGrp.value()).toString(); qtotalsrow[ctCurrency] = currencyGrp.key(); m_rows.append(qtotalsrow); ++currencyGrp; } } } void QueryTable::constructSplitsTable() { MyMoneyFile* file = MyMoneyFile::instance(); //make sure we have all subaccounts of investment accounts includeInvestmentSubAccounts(); MyMoneyReport report(m_config); report.setReportAllSplits(false); report.setConsiderCategory(true); // support for opening and closing balances QMap accts; //get all transactions for this report QList transactions = file->transactionList(report); for (QList::const_iterator it_transaction = transactions.constBegin(); it_transaction != transactions.constEnd(); ++it_transaction) { TableRow qA, qS; QDate pd; qA[ctID] = qS[ctID] = (* it_transaction).id(); qA[ctEntryDate] = qS[ctEntryDate] = (* it_transaction).entryDate().toString(Qt::ISODate); qA[ctPostDate] = qS[ctPostDate] = (* it_transaction).postDate().toString(Qt::ISODate); qA[ctCommodity] = qS[ctCommodity] = (* it_transaction).commodity(); pd = (* it_transaction).postDate(); qA[ctMonth] = qS[ctMonth] = i18n("Month of %1", QDate(pd.year(), pd.month(), 1).toString(Qt::ISODate)); qA[ctWeek] = qS[ctWeek] = i18n("Week of %1", pd.addDays(1 - pd.dayOfWeek()).toString(Qt::ISODate)); if (!m_containsNonBaseCurrency && (*it_transaction).commodity() != file->baseCurrency().id()) m_containsNonBaseCurrency = true; if (report.isConvertCurrency()) qA[ctCurrency] = qS[ctCurrency] = file->baseCurrency().id(); else qA[ctCurrency] = qS[ctCurrency] = (*it_transaction).commodity(); // to handle splits, we decide on which account to base the split // (a reference point or point of view so to speak). here we take the // first account that is a stock account or loan account (or the first account // that is not an income or expense account if there is no stock or loan account) // to be the account (qA) that will have the sub-item "split" entries. we add // one transaction entry (qS) for each subsequent entry in the split. const QList& splits = (*it_transaction).splits(); QList::const_iterator myBegin, it_split; //S_end = splits.end(); for (it_split = splits.constBegin(), myBegin = splits.constEnd(); it_split != splits.constEnd(); ++it_split) { ReportAccount splitAcc((* it_split).accountId()); // always put split with a "stock" account if it exists if (splitAcc.isInvest()) break; // prefer to put splits with a "loan" account if it exists if (splitAcc.isLoan()) myBegin = it_split; if ((myBegin == splits.end()) && ! splitAcc.isIncomeExpense()) { myBegin = it_split; } } // select our "reference" split if (it_split == splits.end()) { it_split = myBegin; } else { myBegin = it_split; } // if the split is still unknown, use the first one. I have seen this // happen with a transaction that has only a single split referencing an income or expense // account and has an amount and value of 0. Such a transaction will fall through // the above logic and leave 'it_split' pointing to splits.end() which causes the remainder // of this to end in an infinite loop. if (it_split == splits.end()) { it_split = splits.begin(); } // for "loan" reports, the loan transaction gets special treatment. // the splits of a loan transaction are placed on one line in the // reference (loan) account (qA). however, we process the matching // split entries (qS) normally. bool loan_special_case = false; - if (m_config.queryColumns() & MyMoneyReport::eQCloan) { + if (m_config.queryColumns() & eMyMoney::Report::QueryColumn::Loan) { ReportAccount splitAcc((*it_split).accountId()); loan_special_case = splitAcc.isLoan(); } // There is a slight chance that at this point myBegin is still pointing to splits.end() if the // transaction only has income and expense splits (which should not happen). In that case, point // it to the first split if (myBegin == splits.end()) { myBegin = splits.begin(); } //the account of the beginning splits ReportAccount myBeginAcc((*myBegin).accountId()); bool include_me = true; QString a_fullname; QString a_memo; int pass = 1; do { MyMoneyMoney xr; ReportAccount splitAcc((* it_split).accountId()); //get fraction for account int fraction = splitAcc.currency().smallestAccountFraction(); //use base currency fraction if not initialized if (fraction == -1) fraction = file->baseCurrency().smallestAccountFraction(); QString institution = splitAcc.institutionId(); QString payee = (*it_split).payeeId(); const QList tagIdList = (*it_split).tagIdList(); if (m_config.isConvertCurrency()) { xr = (splitAcc.deepCurrencyPrice((*it_transaction).postDate()) * splitAcc.baseCurrencyPrice((*it_transaction).postDate())).reduce(); } else { xr = splitAcc.deepCurrencyPrice((*it_transaction).postDate()).reduce(); } // reverse the sign of incomes and expenses to keep consistency in the way it is displayed in other reports if (splitAcc.isIncomeExpense()) { xr = -xr; } if (splitAcc.isInvest()) { // use the institution of the parent for stock accounts institution = splitAcc.parent().institutionId(); MyMoneyMoney shares = (*it_split).shares(); int pricePrecision = file->security(splitAcc.currencyId()).pricePrecision(); qA[ctAction] = (*it_split).action(); qA[ctShares] = shares.isZero() ? QString() : (*it_split).shares().toString(); qA[ctPrice] = shares.isZero() ? QString() : xr.convertPrecision(pricePrecision).toString(); if (((*it_split).action() == MyMoneySplit::actionName(eMyMoney::Split::Action::BuyShares)) && (*it_split).shares().isNegative()) qA[ctAction] = "Sell"; qA[ctInvestAccount] = splitAcc.parent().name(); } include_me = m_config.includes(splitAcc); a_fullname = splitAcc.fullName(); a_memo = (*it_split).memo(); int pricePrecision = file->security(splitAcc.currencyId()).pricePrecision(); qA[ctPrice] = xr.convertPrecision(pricePrecision).toString(); qA[ctAccount] = splitAcc.name(); qA[ctAccountID] = splitAcc.id(); qA[ctTopAccount] = splitAcc.topParentName(); qA[ctInstitution] = institution.isEmpty() ? i18n("No Institution") : file->institution(institution).name(); //FIXME-ALEX Is this useless? Isn't constructSplitsTable called only for cashflow type report? QString delimiter; foreach(const auto tagId, tagIdList) { qA[ctTag] += delimiter + file->tag(tagId).name().simplified(); delimiter = QLatin1Char(','); } qA[ctPayee] = payee.isEmpty() ? i18n("[Empty Payee]") : file->payee(payee).name().simplified(); qA[ctReconcileDate] = (*it_split).reconcileDate().toString(Qt::ISODate); qA[ctReconcileFlag] = KMyMoneyUtils::reconcileStateToString((*it_split).reconcileFlag(), true); qA[ctNumber] = (*it_split).number(); qA[ctMemo] = a_memo; qS[ctReconcileDate] = qA[ctReconcileDate]; qS[ctReconcileFlag] = qA[ctReconcileFlag]; qS[ctNumber] = qA[ctNumber]; qS[ctTopCategory] = splitAcc.topParentName(); // only include the configured accounts if (include_me) { // add the "summarized" split transaction // this is the sub-total of the split detail // convert to lowest fraction qA[ctValue] = ((*it_split).shares() * xr).convert(fraction).toString(); qA[ctRank] = QLatin1Char('1'); //fill in account information if (! splitAcc.isIncomeExpense() && it_split != myBegin) { qA[ctAccount] = ((*it_split).shares().isNegative()) ? i18n("Transfer to %1", myBeginAcc.fullName()) : i18n("Transfer from %1", myBeginAcc.fullName()); } else if (it_split == myBegin) { //handle the main split if ((splits.count() > 2)) { //if it is the main split and has multiple splits, note that qA[ctAccount] = i18n("[Split Transaction]"); } else { //fill the account name of the second split QList::const_iterator tempSplit = splits.constBegin(); //there are supposed to be only 2 splits if we ever get here if (tempSplit == myBegin && splits.count() > 1) ++tempSplit; //show the name of the category, or "transfer to/from" if it as an account ReportAccount tempSplitAcc((*tempSplit).accountId()); if (! tempSplitAcc.isIncomeExpense()) { qA[ctAccount] = ((*it_split).shares().isNegative()) ? i18n("Transfer to %1", tempSplitAcc.fullName()) : i18n("Transfer from %1", tempSplitAcc.fullName()); } else { qA[ctAccount] = tempSplitAcc.fullName(); } } } else { //in any other case, fill in the account name of the main split qA[ctAccount] = myBeginAcc.fullName(); } //category data is always the one of the split qA [ctCategory] = splitAcc.fullName(); qA [ctTopCategory] = splitAcc.topParentName(); qA [ctCategoryType] = MyMoneyAccount::accountTypeToString(splitAcc.accountGroup()); m_rows += qA; // track accts that will need opening and closing balances accts.insert(splitAcc.id(), splitAcc); } ++it_split; // look for wrap-around if (it_split == splits.end()) it_split = splits.begin(); //check if there have been more passes than there are splits //this is to prevent infinite loops in cases of data inconsistency -- asoliverez ++pass; if (pass > splits.count()) break; } while (it_split != myBegin); if (loan_special_case) { m_rows += qA; } } // now run through our accts list and add opening and closing balances switch (m_config.rowType()) { - case MyMoneyReport::eAccount: - case MyMoneyReport::eTopAccount: + case eMyMoney::Report::RowType::Account: + case eMyMoney::Report::RowType::TopAccount: break; - // case MyMoneyReport::eCategory: + // case eMyMoney::Report::RowType::Category: // case MyMoneyReport::eTopCategory: // case MyMoneyReport::ePayee: // case MyMoneyReport::eMonth: // case MyMoneyReport::eWeek: default: return; } QDate startDate, endDate; report.validDateRange(startDate, endDate); QString strStartDate = startDate.toString(Qt::ISODate); QString strEndDate = endDate.toString(Qt::ISODate); startDate = startDate.addDays(-1); QMap::const_iterator it_account, accts_end; for (it_account = accts.constBegin(); it_account != accts.constEnd(); ++it_account) { TableRow qA; ReportAccount account((* it_account)); //get fraction for account int fraction = account.currency().smallestAccountFraction(); //use base currency fraction if not initialized if (fraction == -1) fraction = file->baseCurrency().smallestAccountFraction(); QString institution = account.institutionId(); // use the institution of the parent for stock accounts if (account.isInvest()) institution = account.parent().institutionId(); MyMoneyMoney startBalance, endBalance, startPrice, endPrice; MyMoneyMoney startShares, endShares; //get price and convert currency if necessary if (m_config.isConvertCurrency()) { startPrice = (account.deepCurrencyPrice(startDate) * account.baseCurrencyPrice(startDate)).reduce(); endPrice = (account.deepCurrencyPrice(endDate) * account.baseCurrencyPrice(endDate)).reduce(); } else { startPrice = account.deepCurrencyPrice(startDate).reduce(); endPrice = account.deepCurrencyPrice(endDate).reduce(); } startShares = file->balance(account.id(), startDate); endShares = file->balance(account.id(), endDate); //get starting and ending balances startBalance = startShares * startPrice; endBalance = endShares * endPrice; //starting balance // don't show currency if we're converting or if it's not foreign if (!m_containsNonBaseCurrency && account.currency().id() != file->baseCurrency().id()) m_containsNonBaseCurrency = true; if (m_config.isConvertCurrency()) qA[ctCurrency] = file->baseCurrency().id(); else qA[ctCurrency] = account.currency().id(); qA[ctAccountID] = account.id(); qA[ctAccount] = account.name(); qA[ctTopAccount] = account.topParentName(); qA[ctInstitution] = institution.isEmpty() ? i18n("No Institution") : file->institution(institution).name(); qA[ctRank] = QLatin1Char('0'); int pricePrecision = file->security(account.currencyId()).pricePrecision(); qA[ctPrice] = startPrice.convertPrecision(pricePrecision).toString(); if (account.isInvest()) { qA[ctShares] = startShares.toString(); } qA[ctPostDate] = strStartDate; qA[ctBalance] = startBalance.convert(fraction).toString(); qA[ctValue].clear(); qA[ctID] = QLatin1Char('A'); m_rows += qA; qA[ctRank] = QLatin1Char('3'); //ending balance qA[ctPrice] = endPrice.convertPrecision(pricePrecision).toString(); if (account.isInvest()) { qA[ctShares] = endShares.toString(); } qA[ctPostDate] = strEndDate; qA[ctBalance] = endBalance.toString(); qA[ctID] = QLatin1Char('Z'); m_rows += qA; } } } diff --git a/kmymoney/reports/tests/pivotgrid-test.cpp b/kmymoney/reports/tests/pivotgrid-test.cpp index 1fe1abd08..68e4fb18f 100644 --- a/kmymoney/reports/tests/pivotgrid-test.cpp +++ b/kmymoney/reports/tests/pivotgrid-test.cpp @@ -1,176 +1,178 @@ /* * Copyright 2005-2018 Thomas Baumgart * Copyright 2005-2006 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. * * 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 "pivotgrid-test.h" #include #include "reportstestcommon.h" #include "pivotgrid.h" #include "mymoneyinstitution.h" #include "mymoneysecurity.h" #include "mymoneypayee.h" #include "mymoneyenums.h" using namespace reports; using namespace test; QTEST_GUILESS_MAIN(PivotGridTest) void PivotGridTest::init() { storage = new MyMoneyStorageMgr; file = MyMoneyFile::instance(); file->attachStorage(storage); MyMoneyFileTransaction ft; file->addCurrency(MyMoneySecurity("CAD", "Canadian Dollar", "C$")); file->addCurrency(MyMoneySecurity("USD", "US Dollar", "$")); file->addCurrency(MyMoneySecurity("JPY", "Japanese Yen", QChar(0x00A5), 1)); file->addCurrency(MyMoneySecurity("GBP", "British Pound", "#")); file->setBaseCurrency(file->currency("USD")); - MyMoneyPayee payeeTest("Test Payee"); + MyMoneyPayee payeeTest; + payeeTest.setName("Test Payee"); file->addPayee(payeeTest); - MyMoneyPayee payeeTest2("Thomas Baumgart"); + MyMoneyPayee payeeTest2; + payeeTest2.setName("Thomas Baumgart"); file->addPayee(payeeTest2); acAsset = (MyMoneyFile::instance()->asset().id()); acLiability = (MyMoneyFile::instance()->liability().id()); acExpense = (MyMoneyFile::instance()->expense().id()); acIncome = (MyMoneyFile::instance()->income().id()); acChecking = makeAccount(QString("Checking Account"), eMyMoney::Account::Type::Checkings, moCheckingOpen, QDate(2004, 5, 15), acAsset); acCredit = makeAccount(QString("Credit Card"), eMyMoney::Account::Type::CreditCard, moCreditOpen, QDate(2004, 7, 15), acLiability); acSolo = makeAccount(QString("Solo"), eMyMoney::Account::Type::Expense, MyMoneyMoney(), QDate(2004, 1, 11), acExpense); acParent = makeAccount(QString("Parent"), eMyMoney::Account::Type::Expense, MyMoneyMoney(), QDate(2004, 1, 11), acExpense); acChild = makeAccount(QString("Child"), eMyMoney::Account::Type::Expense, MyMoneyMoney(), QDate(2004, 2, 11), acParent); acForeign = makeAccount(QString("Foreign"), eMyMoney::Account::Type::Expense, MyMoneyMoney(), QDate(2004, 1, 11), acExpense); acSecondChild = makeAccount(QString("Second Child"), eMyMoney::Account::Type::Expense, MyMoneyMoney(), QDate(2004, 2, 11), acParent); acGrandChild1 = makeAccount(QString("Grand Child 1"), eMyMoney::Account::Type::Expense, MyMoneyMoney(), QDate(2004, 2, 11), acChild); acGrandChild2 = makeAccount(QString("Grand Child 2"), eMyMoney::Account::Type::Expense, MyMoneyMoney(), QDate(2004, 2, 11), acChild); MyMoneyInstitution i("Bank of the World", "", "", "", "", "", ""); file->addInstitution(i); inBank = i.id(); ft.commit(); } void PivotGridTest::cleanup() { file->detachStorage(storage); delete storage; } void PivotGridTest::testCellAddValue() { PivotCell a; QVERIFY(a == MyMoneyMoney()); QVERIFY(a.m_stockSplit == MyMoneyMoney::ONE); QVERIFY(a.m_postSplit == MyMoneyMoney()); QVERIFY(a.formatMoney("", 2) == MyMoneyMoney().formatMoney("", 2)); PivotCell b(MyMoneyMoney(13, 10)); QVERIFY(b == MyMoneyMoney(13, 10)); QVERIFY(b.m_stockSplit == MyMoneyMoney::ONE); QVERIFY(b.m_postSplit == MyMoneyMoney()); QVERIFY(b.formatMoney("", 2) == MyMoneyMoney(13, 10).formatMoney("", 2)); PivotCell s(b); QVERIFY(s == MyMoneyMoney(13, 10)); QVERIFY(s.m_stockSplit == MyMoneyMoney::ONE); QVERIFY(s.m_postSplit == MyMoneyMoney()); QVERIFY(s.formatMoney("", 2) == MyMoneyMoney(13, 10).formatMoney("", 2)); s = PivotCell::stockSplit(MyMoneyMoney(1, 2)); QVERIFY(s == MyMoneyMoney()); QVERIFY(s.m_stockSplit == MyMoneyMoney(1, 2)); QVERIFY(s.m_postSplit == MyMoneyMoney()); QVERIFY(s.formatMoney("", 2) == MyMoneyMoney().formatMoney("", 2)); a += MyMoneyMoney::ONE; a += MyMoneyMoney(2, 1); QVERIFY(a == MyMoneyMoney(3, 1)); QVERIFY(a.m_stockSplit == MyMoneyMoney::ONE); QVERIFY(a.m_postSplit == MyMoneyMoney()); QVERIFY(a.formatMoney("", 2) == MyMoneyMoney(3, 1).formatMoney("", 2)); a += s; QVERIFY(a == MyMoneyMoney(3, 1)); QVERIFY(a.m_stockSplit == MyMoneyMoney(1, 2)); QVERIFY(a.m_postSplit == MyMoneyMoney()); QVERIFY(a.formatMoney("", 2) == MyMoneyMoney(15, 10).formatMoney("", 2)); a += MyMoneyMoney(3, 1); a += MyMoneyMoney(3, 1); QVERIFY(a == MyMoneyMoney(3, 1)); QVERIFY(a.m_stockSplit == MyMoneyMoney(1, 2)); QVERIFY(a.m_postSplit == MyMoneyMoney(6, 1)); QVERIFY(a.formatMoney("", 2) == MyMoneyMoney(75, 10).formatMoney("", 2)); } void PivotGridTest::testCellAddCell() { PivotCell a, b; a += MyMoneyMoney(3, 1); a += PivotCell::stockSplit(MyMoneyMoney(2, 1)); a += MyMoneyMoney(4, 1); QVERIFY(a == MyMoneyMoney(3, 1)); QVERIFY(a.m_stockSplit == MyMoneyMoney(2, 1)); QVERIFY(a.m_postSplit == MyMoneyMoney(4, 1)); QVERIFY(a.formatMoney("", 2) == MyMoneyMoney(10, 1).formatMoney("", 2)); b += MyMoneyMoney(4, 1); b += PivotCell::stockSplit(MyMoneyMoney(4, 1)); b += MyMoneyMoney(16, 1); QVERIFY(b == MyMoneyMoney(4, 1)); QVERIFY(b.m_stockSplit == MyMoneyMoney(4, 1)); QVERIFY(b.m_postSplit == MyMoneyMoney(16, 1)); QVERIFY(b.formatMoney("", 2) == MyMoneyMoney(32, 1).formatMoney("", 2)); a += b; QVERIFY(a == MyMoneyMoney(3, 1)); QVERIFY(a.m_stockSplit == MyMoneyMoney(8, 1)); QVERIFY(a.m_postSplit == MyMoneyMoney(48, 1)); QVERIFY(a.formatMoney("", 2) == MyMoneyMoney(72, 1).formatMoney("", 2)); } void PivotGridTest::testCellRunningSum() { PivotCell a; MyMoneyMoney runningSum(12, 10); a += MyMoneyMoney(3, 1); a += PivotCell::stockSplit(MyMoneyMoney(125, 100)); a += MyMoneyMoney(134, 10); QVERIFY(a.m_stockSplit != MyMoneyMoney::ONE); QVERIFY(a.m_postSplit != MyMoneyMoney()); runningSum = a.calculateRunningSum(runningSum); QVERIFY(runningSum == MyMoneyMoney(1865, 100)); QVERIFY(a.formatMoney("", 2) == MyMoneyMoney(1865, 100).formatMoney("", 2)); QVERIFY(a.m_stockSplit == MyMoneyMoney::ONE); QVERIFY(a.m_postSplit == MyMoneyMoney()); } diff --git a/kmymoney/reports/tests/pivottable-test.cpp b/kmymoney/reports/tests/pivottable-test.cpp index 340fb1614..44abd6729 100644 --- a/kmymoney/reports/tests/pivottable-test.cpp +++ b/kmymoney/reports/tests/pivottable-test.cpp @@ -1,1077 +1,1079 @@ /* * Copyright 2005-2018 Thomas Baumgart * Copyright 2005-2006 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. * * 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 "pivottable-test.h" #include #include #include #include // DOH, mmreport.h uses this without including it!! #include "mymoneyinstitution.h" #include "mymoneyaccount.h" #include "mymoneysecurity.h" #include "mymoneyprice.h" #include "mymoneyreport.h" #include "mymoneystatement.h" #include "mymoneysplit.h" #include "mymoneypayee.h" #include "mymoneyexception.h" #include "mymoneystoragedump.h" #include "mymoneyenums.h" #include "pivottable.h" #include "reportstestcommon.h" #include "kmymoneysettings.h" using namespace reports; using namespace test; QTEST_GUILESS_MAIN(PivotTableTest) void PivotTableTest::setup() { } void PivotTableTest::init() { storage = new MyMoneyStorageMgr; file = MyMoneyFile::instance(); file->attachStorage(storage); MyMoneyFileTransaction ft; file->addCurrency(MyMoneySecurity("CAD", "Canadian Dollar", "C$")); file->addCurrency(MyMoneySecurity("USD", "US Dollar", "$")); file->addCurrency(MyMoneySecurity("JPY", "Japanese Yen", QChar(0x00A5), 1)); file->addCurrency(MyMoneySecurity("GBP", "British Pound", "#")); file->setBaseCurrency(file->currency("USD")); - MyMoneyPayee payeeTest("Test Payee"); + MyMoneyPayee payeeTest; + payeeTest.setName("Test Payee"); file->addPayee(payeeTest); - MyMoneyPayee payeeTest2("Thomas Baumgart"); + MyMoneyPayee payeeTest2; + payeeTest2.setName("Thomas Baumgart"); file->addPayee(payeeTest2); acAsset = (MyMoneyFile::instance()->asset().id()); acLiability = (MyMoneyFile::instance()->liability().id()); acExpense = (MyMoneyFile::instance()->expense().id()); acIncome = (MyMoneyFile::instance()->income().id()); acChecking = makeAccount(QString("Checking Account"), eMyMoney::Account::Type::Checkings, moCheckingOpen, QDate(2004, 5, 15), acAsset); acCredit = makeAccount(QString("Credit Card"), eMyMoney::Account::Type::CreditCard, moCreditOpen, QDate(2004, 7, 15), acLiability); acSolo = makeAccount(QString("Solo"), eMyMoney::Account::Type::Expense, MyMoneyMoney(), QDate(2004, 1, 11), acExpense); acParent = makeAccount(QString("Parent"), eMyMoney::Account::Type::Expense, MyMoneyMoney(), QDate(2004, 1, 11), acExpense); acChild = makeAccount(QString("Child"), eMyMoney::Account::Type::Expense, MyMoneyMoney(), QDate(2004, 2, 11), acParent); acForeign = makeAccount(QString("Foreign"), eMyMoney::Account::Type::Expense, MyMoneyMoney(), QDate(2004, 1, 11), acExpense); acSecondChild = makeAccount(QString("Second Child"), eMyMoney::Account::Type::Expense, MyMoneyMoney(), QDate(2004, 2, 11), acParent); acGrandChild1 = makeAccount(QString("Grand Child 1"), eMyMoney::Account::Type::Expense, MyMoneyMoney(), QDate(2004, 2, 11), acChild); acGrandChild2 = makeAccount(QString("Grand Child 2"), eMyMoney::Account::Type::Expense, MyMoneyMoney(), QDate(2004, 2, 11), acChild); MyMoneyInstitution i("Bank of the World", "", "", "", "", "", ""); file->addInstitution(i); inBank = i.id(); ft.commit(); } void PivotTableTest::cleanup() { file->detachStorage(storage); delete storage; } void PivotTableTest::testNetWorthSingle() { try { MyMoneyReport filter; - filter.setRowType(MyMoneyReport::eAssetLiability); + filter.setRowType(eMyMoney::Report::RowType::AssetLiability); filter.setDateFilter(QDate(2004, 1, 1), QDate(2004, 7, 1).addDays(-1)); XMLandback(filter); PivotTable networth_f(filter); writeTabletoCSV(networth_f); QVERIFY(networth_f.m_grid["Asset"]["Checking Account"][ReportAccount(acChecking)][eActual][5] == moCheckingOpen); QVERIFY(networth_f.m_grid["Asset"]["Checking Account"][ReportAccount(acChecking)][eActual][6] == moCheckingOpen); QVERIFY(networth_f.m_grid["Asset"]["Checking Account"].m_total[eActual][5] == moCheckingOpen); QVERIFY(networth_f.m_grid.m_total[eActual][0] == moZero); QVERIFY(networth_f.m_grid.m_total[eActual][4] == moZero); QVERIFY(networth_f.m_grid.m_total[eActual][5] == moCheckingOpen); QVERIFY(networth_f.m_grid.m_total[eActual][6] == moCheckingOpen); } catch (const MyMoneyException &e) { QFAIL(e.what()); } } void PivotTableTest::testNetWorthOfsetting() { // Test the net worth report to make sure it picks up the opening balance for two // accounts opened during the period of the report, one asset & one liability. Test // that it calculates the totals correctly. MyMoneyReport filter; - filter.setRowType(MyMoneyReport::eAssetLiability); + filter.setRowType(eMyMoney::Report::RowType::AssetLiability); filter.setDateFilter(QDate(2004, 1, 1), QDate(2005, 1, 1).addDays(-1)); XMLandback(filter); PivotTable networth_f(filter); QVERIFY(networth_f.m_grid["Liability"]["Credit Card"][ReportAccount(acCredit)][eActual][7] == -moCreditOpen); QVERIFY(networth_f.m_grid.m_total[eActual][0] == moZero); QVERIFY(networth_f.m_grid.m_total[eActual][12] == moCheckingOpen + moCreditOpen); } void PivotTableTest::testNetWorthOpeningPrior() { // Test the net worth report to make sure it's picking up opening balances PRIOR to // the period of the report. MyMoneyReport filter; - filter.setRowType(MyMoneyReport::eAssetLiability); + filter.setRowType(eMyMoney::Report::RowType::AssetLiability); filter.setDateFilter(QDate(2005, 8, 1), QDate(2005, 12, 31)); filter.setName("Net Worth Opening Prior 1"); XMLandback(filter); PivotTable networth_f(filter); writeTabletoCSV(networth_f); QVERIFY(networth_f.m_grid["Liability"]["Credit Card"].m_total[eActual][0] == -moCreditOpen); QVERIFY(networth_f.m_grid["Asset"]["Checking Account"].m_total[eActual][0] == moCheckingOpen); QVERIFY(networth_f.m_grid.m_total[eActual][0] == moCheckingOpen + moCreditOpen); QVERIFY(networth_f.m_grid.m_total[eActual][1] == moCheckingOpen + moCreditOpen); // Test the net worth report to make sure that transactions prior to the report // period are included in the opening balance TransactionHelper t1(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent1, acCredit, acParent); TransactionHelper t2(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent2, acCredit, acParent); TransactionHelper t3(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moChild, acChecking, acChild); filter.setName("Net Worth Opening Prior 2"); PivotTable networth_f2(filter); writeTabletoCSV(networth_f2); MyMoneyMoney m1 = (networth_f2.m_grid["Liability"]["Credit Card"].m_total[eActual][1]); MyMoneyMoney m2 = (-moCreditOpen + moParent); QVERIFY((networth_f2.m_grid["Liability"]["Credit Card"].m_total[eActual][1]) == (-moCreditOpen + moParent)); QVERIFY(networth_f2.m_grid["Asset"]["Checking Account"].m_total[eActual][1] == moCheckingOpen - moChild); QVERIFY(networth_f2.m_grid.m_total[eActual][1] == moCheckingOpen + moCreditOpen - moChild - moParent); } void PivotTableTest::testNetWorthDateFilter() { // Test a net worth report whose period is prior to the time any accounts are open, // so the report should be zero. MyMoneyReport filter; - filter.setRowType(MyMoneyReport::eAssetLiability); + filter.setRowType(eMyMoney::Report::RowType::AssetLiability); filter.setDateFilter(QDate(2004, 1, 1), QDate(2004, 2, 1).addDays(-1)); XMLandback(filter); PivotTable networth_f(filter); QVERIFY(networth_f.m_grid.m_total[eActual][1] == moZero); } void PivotTableTest::testNetWorthOpening() { MyMoneyMoney openingBalance(12000000); auto acBasicAccount = makeAccount(QString("Basic Account"), eMyMoney::Account::Type::Checkings, openingBalance, QDate(2016, 1, 1), acAsset); auto ctBasicIncome = makeAccount(QString("Basic Income"), eMyMoney::Account::Type::Income, MyMoneyMoney(), QDate(2016, 1, 1), acIncome); auto ctBasicExpense = makeAccount(QString("Basic Expense"), eMyMoney::Account::Type::Expense, MyMoneyMoney(), QDate(2016, 1, 1), acExpense); TransactionHelper t1(QDate(2016, 7, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit), MyMoneyMoney(-6200000), acBasicAccount, ctBasicIncome); TransactionHelper t2(QDate(2016, 8, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit), MyMoneyMoney(-200000), acBasicAccount, ctBasicIncome); TransactionHelper t3(QDate(2016, 9, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit), MyMoneyMoney(-200000), acBasicAccount, ctBasicIncome); TransactionHelper t4(QDate(2016, 10, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit), MyMoneyMoney(-100000), acBasicAccount, ctBasicIncome); TransactionHelper t5(QDate(2016, 11, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit), MyMoneyMoney(-100000), acBasicAccount, ctBasicIncome); TransactionHelper t6(QDate(2016, 12, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), MyMoneyMoney(100000), acBasicAccount, ctBasicExpense); TransactionHelper t7(QDate(2017, 1, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit), MyMoneyMoney(-100000), acBasicAccount, ctBasicIncome); TransactionHelper t8(QDate(2017, 2, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit), MyMoneyMoney(-100000), acBasicAccount, ctBasicIncome); TransactionHelper t9(QDate(2017, 3, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit), MyMoneyMoney(-100000), acBasicAccount, ctBasicIncome); TransactionHelper t10(QDate(2017, 4, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit), MyMoneyMoney(-100000), acBasicAccount, ctBasicIncome); TransactionHelper t11(QDate(2017, 5, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), MyMoneyMoney(4500000), acBasicAccount, ctBasicExpense); TransactionHelper t12(QDate(2017, 6, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit), MyMoneyMoney(-100000), acBasicAccount, ctBasicIncome); TransactionHelper t13(QDate(2017, 7, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit), MyMoneyMoney(-100000), acBasicAccount, ctBasicIncome); MyMoneyReport filter; - filter.setRowType(MyMoneyReport::eAssetLiability); + filter.setRowType(eMyMoney::Report::RowType::AssetLiability); filter.setDateFilter(QDate(2016, 1, 1), QDate(2017, 12, 31)); filter.addAccount(acBasicAccount); XMLandback(filter); PivotTable nt_opening1(filter); writeTabletoCSV(nt_opening1, "networth-opening-1.csv"); QVERIFY(nt_opening1.m_grid["Asset"]["Basic Account"][ReportAccount(acBasicAccount)][eActual][0] == MyMoneyMoney()); // opening value on 1st Jan 2016 is 12000000, but before that i.e. 31st Dec 2015 opening value is 0 for (auto i = 1; i <= 6; ++i) QVERIFY(nt_opening1.m_grid["Asset"]["Basic Account"][ReportAccount(acBasicAccount)][eActual][i] == openingBalance); QVERIFY(nt_opening1.m_grid["Asset"]["Basic Account"][ReportAccount(acBasicAccount)][eActual][7] == openingBalance + MyMoneyMoney(6200000)); QVERIFY(nt_opening1.m_grid["Asset"]["Basic Account"][ReportAccount(acBasicAccount)][eActual][12] == MyMoneyMoney(18700000)); // value after t6 transaction filter.setDateFilter(QDate(2017, 1, 1), QDate(2017, 12, 31)); XMLandback(filter); PivotTable nt_opening2(filter); writeTabletoCSV(nt_opening2, "networth-opening-2.csv"); QVERIFY(nt_opening2.m_grid["Asset"]["Basic Account"][ReportAccount(acBasicAccount)][eActual][0] == MyMoneyMoney(18700000)); // opening value is equall to the value after t6 transaction QVERIFY(nt_opening2.m_grid["Asset"]["Basic Account"][ReportAccount(acBasicAccount)][eActual][12] == MyMoneyMoney(14800000)); } void PivotTableTest::testSpendingEmpty() { // test a spending report with no entries MyMoneyReport filter; - filter.setRowType(MyMoneyReport::eExpenseIncome); + filter.setRowType(eMyMoney::Report::RowType::ExpenseIncome); XMLandback(filter); PivotTable spending_f1(filter); QVERIFY(spending_f1.m_grid.m_total[eActual].m_total == moZero); filter.setDateFilter(QDate(2004, 9, 1), QDate(2005, 1, 1).addDays(-1)); PivotTable spending_f2(filter); QVERIFY(spending_f2.m_grid.m_total[eActual].m_total == moZero); } void PivotTableTest::testSingleTransaction() { // Test a single transaction TransactionHelper t(QDate(2004, 10, 31), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moSolo, acChecking, acSolo); MyMoneyReport filter; - filter.setRowType(MyMoneyReport::eExpenseIncome); + filter.setRowType(eMyMoney::Report::RowType::ExpenseIncome); filter.setDateFilter(QDate(2004, 9, 1), QDate(2005, 1, 1).addDays(-1)); filter.setName("Spending with Single Transaction.html"); XMLandback(filter); PivotTable spending_f(filter); writeTabletoHTML(spending_f, "Spending with Single Transaction.html"); QVERIFY(spending_f.m_grid["Expense"]["Solo"][ReportAccount(acSolo)][eActual][1] == moSolo); QVERIFY(spending_f.m_grid["Expense"]["Solo"].m_total[eActual][1] == moSolo); QVERIFY(spending_f.m_grid["Expense"]["Solo"].m_total[eActual][0] == moZero); QVERIFY(spending_f.m_grid.m_total[eActual][1] == (-moSolo)); QVERIFY(spending_f.m_grid.m_total[eActual].m_total == (-moSolo)); filter.clearTransactionFilter(); - filter.setRowType(MyMoneyReport::eAssetLiability); + filter.setRowType(eMyMoney::Report::RowType::AssetLiability); filter.setDateFilter(QDate(2004, 9, 1), QDate(2005, 1, 1).addDays(-1)); XMLandback(filter); PivotTable networth_f(filter); QVERIFY(networth_f.m_grid["Asset"]["Checking Account"].m_total[eActual][2] == (moCheckingOpen - moSolo)); } void PivotTableTest::testSubAccount() { // Test a sub-account with a value, under an account with a value TransactionHelper t1(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent1, acCredit, acParent); TransactionHelper t2(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent2, acCredit, acParent); TransactionHelper t3(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moChild, acCredit, acChild); MyMoneyReport filter; - filter.setRowType(MyMoneyReport::eExpenseIncome); + filter.setRowType(eMyMoney::Report::RowType::ExpenseIncome); filter.setDateFilter(QDate(2004, 9, 1), QDate(2005, 1, 1).addDays(-1)); - filter.setDetailLevel(MyMoneyReport::eDetailAll); + filter.setDetailLevel(eMyMoney::Report::DetailLevel::All); filter.setName("Spending with Sub-Account"); XMLandback(filter); PivotTable spending_f(filter); writeTabletoHTML(spending_f, "Spending with Sub-Account.html"); QVERIFY(spending_f.m_grid["Expense"]["Parent"][ReportAccount(acParent)][eActual][2] == moParent); QVERIFY(spending_f.m_grid["Expense"]["Parent"][ReportAccount(acChild)][eActual][2] == moChild); QVERIFY(spending_f.m_grid["Expense"]["Parent"].m_total[eActual][2] == moParent + moChild); QVERIFY(spending_f.m_grid["Expense"]["Parent"].m_total[eActual][1] == moZero); QVERIFY(spending_f.m_grid["Expense"]["Parent"].m_total[eActual].m_total == moParent + moChild); QVERIFY(spending_f.m_grid.m_total[eActual][2] == (-moParent - moChild)); QVERIFY(spending_f.m_grid.m_total[eActual].m_total == (-moParent - moChild)); filter.clearTransactionFilter(); - filter.setRowType(MyMoneyReport::eAssetLiability); + filter.setRowType(eMyMoney::Report::RowType::AssetLiability); filter.setDateFilter(QDate(2004, 9, 1), QDate(2005, 1, 1).addDays(-1)); filter.setName("Net Worth with Sub-Account"); XMLandback(filter); PivotTable networth_f(filter); writeTabletoHTML(networth_f, "Net Worth with Sub-Account.html"); QVERIFY(networth_f.m_grid["Liability"]["Credit Card"].m_total[eActual][3] == moParent + moChild - moCreditOpen); QVERIFY(networth_f.m_grid.m_total[eActual][4] == -moParent - moChild + moCreditOpen + moCheckingOpen); } void PivotTableTest::testFilterIEvsIE() { // Test that removing an income/spending account will remove the entry from an income/spending report TransactionHelper t1(QDate(2004, 10, 31), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moSolo, acChecking, acSolo); TransactionHelper t2(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent1, acCredit, acParent); TransactionHelper t3(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moChild, acCredit, acChild); MyMoneyReport filter; - filter.setRowType(MyMoneyReport::eExpenseIncome); + filter.setRowType(eMyMoney::Report::RowType::ExpenseIncome); filter.setDateFilter(QDate(2004, 9, 1), QDate(2005, 1, 1).addDays(-1)); filter.addCategory(acChild); filter.addCategory(acSolo); XMLandback(filter); PivotTable spending_f(filter); QVERIFY(spending_f.m_grid["Expense"]["Parent"].m_total[eActual][2] == moChild); QVERIFY(spending_f.m_grid["Expense"].m_total[eActual][1] == moSolo); QVERIFY(spending_f.m_grid.m_total[eActual].m_total == -moSolo - moChild); } void PivotTableTest::testFilterALvsAL() { // Test that removing an asset/liability account will remove the entry from an asset/liability report TransactionHelper t1(QDate(2004, 10, 31), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moSolo, acChecking, acSolo); TransactionHelper t2(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent1, acCredit, acParent); TransactionHelper t3(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moChild, acCredit, acChild); MyMoneyReport filter; - filter.setRowType(MyMoneyReport::eAssetLiability); + filter.setRowType(eMyMoney::Report::RowType::AssetLiability); filter.setDateFilter(QDate(2004, 9, 1), QDate(2005, 1, 1).addDays(-1)); filter.addAccount(acChecking); filter.addCategory(acChild); filter.addCategory(acSolo); XMLandback(filter); PivotTable networth_f(filter); QVERIFY(networth_f.m_grid.m_total[eActual][2] == -moSolo + moCheckingOpen); } void PivotTableTest::testFilterALvsIE() { // Test that removing an asset/liability account will remove the entry from an income/spending report TransactionHelper t1(QDate(2004, 10, 31), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moSolo, acChecking, acSolo); TransactionHelper t2(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent1, acCredit, acParent); TransactionHelper t3(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moChild, acCredit, acChild); MyMoneyReport filter; - filter.setRowType(MyMoneyReport::eExpenseIncome); + filter.setRowType(eMyMoney::Report::RowType::ExpenseIncome); filter.setDateFilter(QDate(2004, 9, 1), QDate(2005, 1, 1).addDays(-1)); filter.addAccount(acChecking); QVERIFY(file->transactionList(filter).count() == 1); XMLandback(filter); PivotTable spending_f(filter); QVERIFY(spending_f.m_grid["Expense"].m_total[eActual][2] == moZero); QVERIFY(spending_f.m_grid["Expense"].m_total[eActual][1] == moSolo); QVERIFY(spending_f.m_grid.m_total[eActual].m_total == -moSolo); } void PivotTableTest::testFilterAllvsIE() { // Test that removing an asset/liability account AND an income/expense // category will remove the entry from an income/spending report TransactionHelper t1(QDate(2004, 10, 31), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moSolo, acChecking, acSolo); TransactionHelper t2(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent1, acCredit, acParent); TransactionHelper t3(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moChild, acCredit, acChild); MyMoneyReport filter; - filter.setRowType(MyMoneyReport::eExpenseIncome); + filter.setRowType(eMyMoney::Report::RowType::ExpenseIncome); filter.setDateFilter(QDate(2004, 9, 1), QDate(2005, 1, 1).addDays(-1)); filter.addAccount(acCredit); filter.addCategory(acChild); PivotTable spending_f(filter); QVERIFY(spending_f.m_grid["Expense"].m_total[eActual][1] == moZero); QVERIFY(spending_f.m_grid["Expense"].m_total[eActual][2] == moChild); QVERIFY(spending_f.m_grid.m_total[eActual].m_total == -moChild); } void PivotTableTest::testFilterBasics() { // Test that the filters are operating the way that the reports expect them to TransactionHelper t1(QDate(2004, 10, 31), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moSolo, acChecking, acSolo); TransactionHelper t2(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent1, acCredit, acParent); TransactionHelper t3(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent2, acCredit, acParent); TransactionHelper t4(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moChild, acCredit, acChild); MyMoneyTransactionFilter filter; filter.clear(); filter.setDateFilter(QDate(2004, 9, 1), QDate(2005, 1, 1).addDays(-1)); filter.addCategory(acSolo); filter.setReportAllSplits(false); filter.setConsiderCategory(true); QVERIFY(file->transactionList(filter).count() == 1); filter.addCategory(acParent); QVERIFY(file->transactionList(filter).count() == 3); filter.addAccount(acChecking); QVERIFY(file->transactionList(filter).count() == 1); filter.clear(); filter.setDateFilter(QDate(2004, 9, 1), QDate(2005, 1, 1).addDays(-1)); filter.addCategory(acParent); filter.addAccount(acCredit); filter.setReportAllSplits(false); filter.setConsiderCategory(true); QVERIFY(file->transactionList(filter).count() == 2); } void PivotTableTest::testMultipleCurrencies() { MyMoneyMoney moCanOpening(0.0, 10); MyMoneyMoney moJpyOpening(0.0, 10); MyMoneyMoney moCanPrice(0.75, 100); MyMoneyMoney moJpyPrice(0.010, 1000); MyMoneyMoney moJpyPrice2(0.011, 1000); MyMoneyMoney moJpyPrice3(0.014, 1000); MyMoneyMoney moJpyPrice4(0.0395, 10000); MyMoneyMoney moCanTransaction(100.0, 10); MyMoneyMoney moJpyTransaction(100.0, 10); QString acCanChecking = makeAccount(QString("Canadian Checking"), eMyMoney::Account::Type::Checkings, moCanOpening, QDate(2003, 11, 15), acAsset, "CAD"); QString acJpyChecking = makeAccount(QString("Japanese Checking"), eMyMoney::Account::Type::Checkings, moJpyOpening, QDate(2003, 11, 15), acAsset, "JPY"); QString acCanCash = makeAccount(QString("Canadian"), eMyMoney::Account::Type::Expense, MyMoneyMoney(), QDate(2004, 2, 11), acForeign, "CAD"); QString acJpyCash = makeAccount(QString("Japanese"), eMyMoney::Account::Type::Expense, MyMoneyMoney(), QDate(2004, 2, 11), acForeign, "JPY"); makePrice("CAD", QDate(2004, 1, 1), MyMoneyMoney(moCanPrice)); makePrice("JPY", QDate(2004, 1, 1), MyMoneyMoney(moJpyPrice)); makePrice("JPY", QDate(2004, 5, 1), MyMoneyMoney(moJpyPrice2)); makePrice("JPY", QDate(2004, 6, 30), MyMoneyMoney(moJpyPrice3)); makePrice("JPY", QDate(2004, 7, 15), MyMoneyMoney(moJpyPrice4)); TransactionHelper t1(QDate(2004, 2, 20), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), MyMoneyMoney(moJpyTransaction), acJpyChecking, acJpyCash, "JPY"); TransactionHelper t2(QDate(2004, 3, 20), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), MyMoneyMoney(moJpyTransaction), acJpyChecking, acJpyCash, "JPY"); TransactionHelper t3(QDate(2004, 4, 20), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), MyMoneyMoney(moJpyTransaction), acJpyChecking, acJpyCash, "JPY"); TransactionHelper t4(QDate(2004, 2, 20), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), MyMoneyMoney(moCanTransaction), acCanChecking, acCanCash, "CAD"); TransactionHelper t5(QDate(2004, 3, 20), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), MyMoneyMoney(moCanTransaction), acCanChecking, acCanCash, "CAD"); TransactionHelper t6(QDate(2004, 4, 20), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), MyMoneyMoney(moCanTransaction), acCanChecking, acCanCash, "CAD"); #if 0 QFile g("multicurrencykmy.xml"); g.open(QIODevice::WriteOnly); MyMoneyStorageXML xml; IMyMoneyOperationsFormat& interface = xml; interface.writeFile(&g, dynamic_cast(MyMoneyFile::instance()->storage())); g.close(); #endif MyMoneyReport filter; - filter.setRowType(MyMoneyReport::eExpenseIncome); + filter.setRowType(eMyMoney::Report::RowType::ExpenseIncome); filter.setDateFilter(QDate(2004, 1, 1), QDate(2005, 1, 1).addDays(-1)); - filter.setDetailLevel(MyMoneyReport::eDetailAll); + filter.setDetailLevel(eMyMoney::Report::DetailLevel::All); filter.setConvertCurrency(true); filter.setName("Multiple Currency Spending Rerport (with currency conversion)"); XMLandback(filter); PivotTable spending_f(filter); writeTabletoCSV(spending_f); // test single foreign currency QVERIFY(spending_f.m_grid["Expense"]["Foreign"][ReportAccount(acCanCash)][eActual][1] == (moCanTransaction*moCanPrice)); QVERIFY(spending_f.m_grid["Expense"]["Foreign"][ReportAccount(acCanCash)][eActual][2] == (moCanTransaction*moCanPrice)); QVERIFY(spending_f.m_grid["Expense"]["Foreign"][ReportAccount(acCanCash)][eActual][3] == (moCanTransaction*moCanPrice)); // test multiple foreign currencies under a common parent QVERIFY(spending_f.m_grid["Expense"]["Foreign"][ReportAccount(acJpyCash)][eActual][1] == (moJpyTransaction*moJpyPrice)); QVERIFY(spending_f.m_grid["Expense"]["Foreign"][ReportAccount(acJpyCash)][eActual][2] == (moJpyTransaction*moJpyPrice)); QVERIFY(spending_f.m_grid["Expense"]["Foreign"][ReportAccount(acJpyCash)][eActual][3] == (moJpyTransaction*moJpyPrice)); QVERIFY(spending_f.m_grid["Expense"]["Foreign"].m_total[eActual][1] == (moJpyTransaction*moJpyPrice + moCanTransaction*moCanPrice)); QVERIFY(spending_f.m_grid["Expense"]["Foreign"].m_total[eActual].m_total == (moJpyTransaction*moJpyPrice + moCanTransaction*moCanPrice + moJpyTransaction*moJpyPrice + moCanTransaction*moCanPrice + moJpyTransaction*moJpyPrice + moCanTransaction*moCanPrice)); // Test the report type where we DO NOT convert the currency filter.setConvertCurrency(false); - filter.setDetailLevel(MyMoneyReport::eDetailAll); + filter.setDetailLevel(eMyMoney::Report::DetailLevel::All); filter.setName("Multiple Currency Spending Report (WITHOUT currency conversion)"); XMLandback(filter); PivotTable spending_fnc(filter); writeTabletoCSV(spending_fnc); QVERIFY(spending_fnc.m_grid["Expense"]["Foreign"][ReportAccount(acCanCash)][eActual][1] == (moCanTransaction)); QVERIFY(spending_fnc.m_grid["Expense"]["Foreign"][ReportAccount(acCanCash)][eActual][2] == (moCanTransaction)); QVERIFY(spending_fnc.m_grid["Expense"]["Foreign"][ReportAccount(acCanCash)][eActual][3] == (moCanTransaction)); QVERIFY(spending_fnc.m_grid["Expense"]["Foreign"][ReportAccount(acJpyCash)][eActual][1] == (moJpyTransaction)); QVERIFY(spending_fnc.m_grid["Expense"]["Foreign"][ReportAccount(acJpyCash)][eActual][2] == (moJpyTransaction)); QVERIFY(spending_fnc.m_grid["Expense"]["Foreign"][ReportAccount(acJpyCash)][eActual][3] == (moJpyTransaction)); filter.setConvertCurrency(true); filter.clearTransactionFilter(); filter.setName("Multiple currency net worth"); - filter.setRowType(MyMoneyReport::eAssetLiability); + filter.setRowType(eMyMoney::Report::RowType::AssetLiability); filter.setDateFilter(QDate(2004, 1, 1), QDate(2005, 1, 1).addDays(-1)); XMLandback(filter); PivotTable networth_f(filter); writeTabletoCSV(networth_f); // test single foreign currency QVERIFY(networth_f.m_grid["Asset"]["Canadian Checking"][ReportAccount(acCanChecking)][eActual][1] == (moCanOpening*moCanPrice)); QVERIFY(networth_f.m_grid["Asset"]["Canadian Checking"][ReportAccount(acCanChecking)][eActual][2] == ((moCanOpening - moCanTransaction)*moCanPrice)); QVERIFY(networth_f.m_grid["Asset"]["Canadian Checking"][ReportAccount(acCanChecking)][eActual][3] == ((moCanOpening - moCanTransaction - moCanTransaction)*moCanPrice)); QVERIFY(networth_f.m_grid["Asset"]["Canadian Checking"][ReportAccount(acCanChecking)][eActual][4] == ((moCanOpening - moCanTransaction - moCanTransaction - moCanTransaction)*moCanPrice)); QVERIFY(networth_f.m_grid["Asset"]["Canadian Checking"][ReportAccount(acCanChecking)][eActual][12] == ((moCanOpening - moCanTransaction - moCanTransaction - moCanTransaction)*moCanPrice)); // test Stable currency price, fluctuating account balance QVERIFY(networth_f.m_grid["Asset"]["Japanese Checking"][ReportAccount(acJpyChecking)][eActual][1] == (moJpyOpening*moJpyPrice)); QVERIFY(networth_f.m_grid["Asset"]["Japanese Checking"][ReportAccount(acJpyChecking)][eActual][2] == ((moJpyOpening - moJpyTransaction)*moJpyPrice)); QVERIFY(networth_f.m_grid["Asset"]["Japanese Checking"][ReportAccount(acJpyChecking)][eActual][3] == ((moJpyOpening - moJpyTransaction - moJpyTransaction)*moJpyPrice)); QVERIFY(networth_f.m_grid["Asset"]["Japanese Checking"][ReportAccount(acJpyChecking)][eActual][4] == ((moJpyOpening - moJpyTransaction - moJpyTransaction - moJpyTransaction)*moJpyPrice)); // test Fluctuating currency price, stable account balance QVERIFY(networth_f.m_grid["Asset"]["Japanese Checking"][ReportAccount(acJpyChecking)][eActual][5] == ((moJpyOpening - moJpyTransaction - moJpyTransaction - moJpyTransaction)*moJpyPrice2)); QVERIFY(networth_f.m_grid["Asset"]["Japanese Checking"][ReportAccount(acJpyChecking)][eActual][6] == ((moJpyOpening - moJpyTransaction - moJpyTransaction - moJpyTransaction)*moJpyPrice3)); QVERIFY(networth_f.m_grid["Asset"]["Japanese Checking"][ReportAccount(acJpyChecking)][eActual][7] == ((moJpyOpening - moJpyTransaction - moJpyTransaction - moJpyTransaction)*moJpyPrice4)); // test multiple currencies totalled up QVERIFY(networth_f.m_grid["Asset"].m_total[eActual][4] == ((moCanOpening - moCanTransaction - moCanTransaction - moCanTransaction)*moCanPrice) + ((moJpyOpening - moJpyTransaction - moJpyTransaction - moJpyTransaction)*moJpyPrice)); QVERIFY(networth_f.m_grid["Asset"].m_total[eActual][5] == ((moCanOpening - moCanTransaction - moCanTransaction - moCanTransaction)*moCanPrice) + ((moJpyOpening - moJpyTransaction - moJpyTransaction - moJpyTransaction)*moJpyPrice2) + moCheckingOpen); } void PivotTableTest::testAdvancedFilter() { // test more advanced filtering capabilities // amount { TransactionHelper t1(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent1, acCredit, acParent); TransactionHelper t2(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moChild, acCredit, acChild); MyMoneyReport filter; - filter.setRowType(MyMoneyReport::eExpenseIncome); + filter.setRowType(eMyMoney::Report::RowType::ExpenseIncome); filter.setDateFilter(QDate(2004, 1, 1), QDate(2005, 1, 1).addDays(-1)); filter.setAmountFilter(moChild, moChild); XMLandback(filter); PivotTable spending_f(filter); QVERIFY(spending_f.m_grid.m_total[eActual].m_total == -moChild); } // payee (specific) { TransactionHelper t1(QDate(2004, 10, 31), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moSolo, acChecking, acSolo); TransactionHelper t2(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent1, acCredit, acParent); TransactionHelper t3(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moChild, acCredit, acChild); TransactionHelper t4(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moThomas, acCredit, acParent, QString(), "Thomas Baumgart"); MyMoneyReport filter; - filter.setRowType(MyMoneyReport::eExpenseIncome); + filter.setRowType(eMyMoney::Report::RowType::ExpenseIncome); filter.setDateFilter(QDate(2004, 1, 1), QDate(2005, 1, 1).addDays(-1)); filter.addPayee(MyMoneyFile::instance()->payeeByName("Thomas Baumgart").id()); filter.setName("Spending with Payee Filter"); XMLandback(filter); PivotTable spending_f(filter); writeTabletoHTML(spending_f, "Spending with Payee Filter.html"); QVERIFY(spending_f.m_grid["Expense"]["Parent"][ReportAccount(acParent)][eActual][10] == moThomas); QVERIFY(spending_f.m_grid.m_total[eActual].m_total == -moThomas); } // payee (no payee) { TransactionHelper t1(QDate(2004, 10, 31), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moSolo, acChecking, acSolo); TransactionHelper t2(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent1, acCredit, acParent); TransactionHelper t3(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moChild, acCredit, acChild); TransactionHelper t4(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moNoPayee, acCredit, acParent, QString(), QString()); MyMoneyReport filter; - filter.setRowType(MyMoneyReport::eExpenseIncome); + filter.setRowType(eMyMoney::Report::RowType::ExpenseIncome); filter.setDateFilter(QDate(2004, 1, 1), QDate(2005, 1, 1).addDays(-1)); filter.addPayee(QString()); XMLandback(filter); PivotTable spending_f(filter); QVERIFY(spending_f.m_grid["Expense"]["Parent"][ReportAccount(acParent)][eActual][10] == moNoPayee); QVERIFY(spending_f.m_grid.m_total[eActual].m_total == -moNoPayee); } // text { TransactionHelper t1(QDate(2004, 10, 31), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moSolo, acChecking, acSolo); TransactionHelper t2(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent1, acCredit, acParent); TransactionHelper t3(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moChild, acCredit, acChild); TransactionHelper t4(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moThomas, acCredit, acParent, QString(), "Thomas Baumgart"); MyMoneyReport filter; - filter.setRowType(MyMoneyReport::eExpenseIncome); + filter.setRowType(eMyMoney::Report::RowType::ExpenseIncome); filter.setDateFilter(QDate(2004, 1, 1), QDate(2005, 1, 1).addDays(-1)); filter.setTextFilter(QRegExp("Thomas")); XMLandback(filter); PivotTable spending_f(filter); } // type (payment, deposit, transfer) { TransactionHelper t1(QDate(2004, 1, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moSolo, acChecking, acSolo); TransactionHelper t2(QDate(2004, 2, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit), -moParent1, acCredit, acParent); TransactionHelper t3(QDate(2004, 11, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Transfer), moChild, acCredit, acChecking); MyMoneyReport filter; - filter.setRowType(MyMoneyReport::eExpenseIncome); + filter.setRowType(eMyMoney::Report::RowType::ExpenseIncome); filter.addType((int)eMyMoney::TransactionFilter::Type::Payments); XMLandback(filter); PivotTable spending_f(filter); QVERIFY(spending_f.m_grid.m_total[eActual].m_total == -moSolo); filter.clearTransactionFilter(); filter.addType((int)eMyMoney::TransactionFilter::Type::Deposits); XMLandback(filter); PivotTable spending_f2(filter); QVERIFY(spending_f2.m_grid.m_total[eActual].m_total == moParent1); filter.clearTransactionFilter(); filter.addType((int)eMyMoney::TransactionFilter::Type::Transfers); XMLandback(filter); PivotTable spending_f3(filter); QVERIFY(spending_f3.m_grid.m_total[eActual].m_total == moZero); - filter.setRowType(MyMoneyReport::eAssetLiability); + filter.setRowType(eMyMoney::Report::RowType::AssetLiability); filter.setDateFilter(QDate(2004, 1, 1), QDate(2004, 12, 31)); XMLandback(filter); PivotTable networth_f4(filter); QVERIFY(networth_f4.m_grid["Asset"].m_total[eActual][11] == moCheckingOpen + moChild); QVERIFY(networth_f4.m_grid["Liability"].m_total[eActual][11] == - moCreditOpen + moChild); QVERIFY(networth_f4.m_grid.m_total[eActual][9] == moCheckingOpen + moCreditOpen); QVERIFY(networth_f4.m_grid.m_total[eActual][10] == moCheckingOpen + moCreditOpen); } // state (reconciled, cleared, not) { TransactionHelper t1(QDate(2004, 1, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moSolo, acChecking, acSolo); TransactionHelper t2(QDate(2004, 2, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent1, acCredit, acParent); TransactionHelper t3(QDate(2004, 3, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent2, acCredit, acParent); TransactionHelper t4(QDate(2004, 4, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moChild, acCredit, acChild); QList splits = t1.splits(); splits[0].setReconcileFlag(eMyMoney::Split::State::Cleared); splits[1].setReconcileFlag(eMyMoney::Split::State::Cleared); t1.modifySplit(splits[0]); t1.modifySplit(splits[1]); t1.update(); splits.clear(); splits = t2.splits(); splits[0].setReconcileFlag(eMyMoney::Split::State::Reconciled); splits[1].setReconcileFlag(eMyMoney::Split::State::Reconciled); t2.modifySplit(splits[0]); t2.modifySplit(splits[1]); t2.update(); MyMoneyReport filter; - filter.setRowType(MyMoneyReport::eExpenseIncome); + filter.setRowType(eMyMoney::Report::RowType::ExpenseIncome); filter.setDateFilter(QDate(2004, 1, 1), QDate(2005, 1, 1).addDays(-1)); filter.addState((int)eMyMoney::TransactionFilter::State::Cleared); XMLandback(filter); PivotTable spending_f(filter); QVERIFY(spending_f.m_grid.m_total[eActual].m_total == -moSolo); filter.addState((int)eMyMoney::TransactionFilter::State::Reconciled); XMLandback(filter); PivotTable spending_f2(filter); QVERIFY(spending_f2.m_grid.m_total[eActual].m_total == -moSolo - moParent1); filter.clearTransactionFilter(); filter.addState((int)eMyMoney::TransactionFilter::State::NotReconciled); XMLandback(filter); PivotTable spending_f3(filter); QVERIFY(spending_f3.m_grid.m_total[eActual].m_total == -moChild - moParent2); } // number { TransactionHelper t1(QDate(2004, 10, 31), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moSolo, acChecking, acSolo); TransactionHelper t2(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent1, acCredit, acParent); TransactionHelper t3(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent2, acCredit, acParent); TransactionHelper t4(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moChild, acCredit, acChild); QList splits = t1.splits(); splits[0].setNumber("1"); splits[1].setNumber("1"); t1.modifySplit(splits[0]); t1.modifySplit(splits[1]); t1.update(); splits.clear(); splits = t2.splits(); splits[0].setNumber("2"); splits[1].setNumber("2"); t2.modifySplit(splits[0]); t2.modifySplit(splits[1]); t2.update(); splits.clear(); splits = t3.splits(); splits[0].setNumber("3"); splits[1].setNumber("3"); t3.modifySplit(splits[0]); t3.modifySplit(splits[1]); t3.update(); splits.clear(); splits = t2.splits(); splits[0].setNumber("4"); splits[1].setNumber("4"); t4.modifySplit(splits[0]); t4.modifySplit(splits[1]); t4.update(); MyMoneyReport filter; - filter.setRowType(MyMoneyReport::eExpenseIncome); + filter.setRowType(eMyMoney::Report::RowType::ExpenseIncome); filter.setDateFilter(QDate(2004, 1, 1), QDate(2005, 1, 1).addDays(-1)); filter.setNumberFilter("1", "3"); XMLandback(filter); PivotTable spending_f(filter); QVERIFY(spending_f.m_grid.m_total[eActual].m_total == -moSolo - moParent1 - moParent2); } // blank dates { TransactionHelper t1y1(QDate(2003, 10, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moSolo, acChecking, acSolo); TransactionHelper t2y1(QDate(2003, 11, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent1, acCredit, acParent); TransactionHelper t3y1(QDate(2003, 12, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent2, acCredit, acParent); TransactionHelper t1y2(QDate(2004, 4, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moSolo, acChecking, acSolo); TransactionHelper t2y2(QDate(2004, 5, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent1, acCredit, acParent); TransactionHelper t3y2(QDate(2004, 6, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent2, acCredit, acParent); TransactionHelper t1y3(QDate(2005, 1, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moSolo, acChecking, acSolo); TransactionHelper t2y3(QDate(2005, 5, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent1, acCredit, acParent); TransactionHelper t3y3(QDate(2005, 9, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent2, acCredit, acParent); MyMoneyReport filter; - filter.setRowType(MyMoneyReport::eExpenseIncome); + filter.setRowType(eMyMoney::Report::RowType::ExpenseIncome); filter.setDateFilter(QDate(), QDate(2004, 7, 1)); XMLandback(filter); PivotTable spending_f(filter); QVERIFY(spending_f.m_grid.m_total[eActual].m_total == -moSolo - moParent1 - moParent2 - moSolo - moParent1 - moParent2); filter.clearTransactionFilter(); XMLandback(filter); PivotTable spending_f2(filter); QVERIFY(spending_f2.m_grid.m_total[eActual].m_total == -moSolo - moParent1 - moParent2 - moSolo - moParent1 - moParent2 - moSolo - moParent1 - moParent2); } } void PivotTableTest::testColumnType() { // test column type values of other than 'month' TransactionHelper t1q1(QDate(2004, 1, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moSolo, acChecking, acSolo); TransactionHelper t2q1(QDate(2004, 2, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent1, acCredit, acParent); TransactionHelper t3q1(QDate(2004, 3, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent2, acCredit, acParent); TransactionHelper t1q2(QDate(2004, 4, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moSolo, acChecking, acSolo); TransactionHelper t2q2(QDate(2004, 5, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent1, acCredit, acParent); TransactionHelper t3q2(QDate(2004, 6, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent2, acCredit, acParent); TransactionHelper t1y2(QDate(2005, 1, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moSolo, acChecking, acSolo); TransactionHelper t2y2(QDate(2005, 5, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent1, acCredit, acParent); TransactionHelper t3y2(QDate(2005, 9, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent2, acCredit, acParent); MyMoneyReport filter; - filter.setRowType(MyMoneyReport::eExpenseIncome); + filter.setRowType(eMyMoney::Report::RowType::ExpenseIncome); filter.setDateFilter(QDate(2003, 12, 31), QDate(2005, 12, 31)); - filter.setRowType(MyMoneyReport::eExpenseIncome); - filter.setColumnType(MyMoneyReport::eBiMonths); + filter.setRowType(eMyMoney::Report::RowType::ExpenseIncome); + filter.setColumnType(eMyMoney::Report::ColumnType::BiMonths); XMLandback(filter); PivotTable spending_b(filter); QVERIFY(spending_b.m_grid.m_total[eActual][0] == moZero); QVERIFY(spending_b.m_grid.m_total[eActual][1] == -moParent1 - moSolo); QVERIFY(spending_b.m_grid.m_total[eActual][2] == -moParent2 - moSolo); QVERIFY(spending_b.m_grid.m_total[eActual][3] == -moParent); QVERIFY(spending_b.m_grid.m_total[eActual][4] == moZero); QVERIFY(spending_b.m_grid.m_total[eActual][5] == moZero); QVERIFY(spending_b.m_grid.m_total[eActual][6] == moZero); QVERIFY(spending_b.m_grid.m_total[eActual][7] == -moSolo); QVERIFY(spending_b.m_grid.m_total[eActual][8] == moZero); QVERIFY(spending_b.m_grid.m_total[eActual][9] == -moParent1); QVERIFY(spending_b.m_grid.m_total[eActual][10] == moZero); QVERIFY(spending_b.m_grid.m_total[eActual][11] == -moParent2); QVERIFY(spending_b.m_grid.m_total[eActual][12] == moZero); - filter.setColumnType(MyMoneyReport::eQuarters); + filter.setColumnType(eMyMoney::Report::ColumnType::Quarters); XMLandback(filter); PivotTable spending_q(filter); QVERIFY(spending_q.m_grid.m_total[eActual][0] == moZero); QVERIFY(spending_q.m_grid.m_total[eActual][1] == -moSolo - moParent); QVERIFY(spending_q.m_grid.m_total[eActual][2] == -moSolo - moParent); QVERIFY(spending_q.m_grid.m_total[eActual][3] == moZero); QVERIFY(spending_q.m_grid.m_total[eActual][4] == moZero); QVERIFY(spending_q.m_grid.m_total[eActual][5] == -moSolo); QVERIFY(spending_q.m_grid.m_total[eActual][6] == -moParent1); QVERIFY(spending_q.m_grid.m_total[eActual][7] == -moParent2); QVERIFY(spending_q.m_grid.m_total[eActual][8] == moZero); - filter.setRowType(MyMoneyReport::eAssetLiability); + filter.setRowType(eMyMoney::Report::RowType::AssetLiability); filter.setName("Net Worth by Quarter"); XMLandback(filter); PivotTable networth_q(filter); writeTabletoHTML(networth_q, "Net Worth by Quarter.html"); QVERIFY(networth_q.m_grid.m_total[eActual][1] == moZero); QVERIFY(networth_q.m_grid.m_total[eActual][2] == -moSolo - moParent); QVERIFY(networth_q.m_grid.m_total[eActual][3] == -moSolo - moParent - moSolo - moParent + moCheckingOpen); QVERIFY(networth_q.m_grid.m_total[eActual][4] == -moSolo - moParent - moSolo - moParent + moCheckingOpen + moCreditOpen); QVERIFY(networth_q.m_grid.m_total[eActual][5] == -moSolo - moParent - moSolo - moParent + moCheckingOpen + moCreditOpen); QVERIFY(networth_q.m_grid.m_total[eActual][6] == -moSolo - moSolo - moParent - moSolo - moParent + moCheckingOpen + moCreditOpen); QVERIFY(networth_q.m_grid.m_total[eActual][7] == -moParent1 - moSolo - moSolo - moParent - moSolo - moParent + moCheckingOpen + moCreditOpen); QVERIFY(networth_q.m_grid.m_total[eActual][8] == -moParent2 - moParent1 - moSolo - moSolo - moParent - moSolo - moParent + moCheckingOpen + moCreditOpen); QVERIFY(networth_q.m_grid.m_total[eActual][9] == -moParent2 - moParent1 - moSolo - moSolo - moParent - moSolo - moParent + moCheckingOpen + moCreditOpen); - filter.setRowType(MyMoneyReport::eExpenseIncome); - filter.setColumnType(MyMoneyReport::eYears); + filter.setRowType(eMyMoney::Report::RowType::ExpenseIncome); + filter.setColumnType(eMyMoney::Report::ColumnType::Years); XMLandback(filter); PivotTable spending_y(filter); QVERIFY(spending_y.m_grid.m_total[eActual][0] == moZero); QVERIFY(spending_y.m_grid.m_total[eActual][1] == -moSolo - moParent - moSolo - moParent); QVERIFY(spending_y.m_grid.m_total[eActual][2] == -moSolo - moParent); QVERIFY(spending_y.m_grid.m_total[eActual].m_total == -moSolo - moParent - moSolo - moParent - moSolo - moParent); - filter.setRowType(MyMoneyReport::eAssetLiability); + filter.setRowType(eMyMoney::Report::RowType::AssetLiability); XMLandback(filter); PivotTable networth_y(filter); QVERIFY(networth_y.m_grid.m_total[eActual][1] == moZero); QVERIFY(networth_y.m_grid.m_total[eActual][2] == -moSolo - moParent - moSolo - moParent + moCheckingOpen + moCreditOpen); QVERIFY(networth_y.m_grid.m_total[eActual][3] == -moSolo - moParent - moSolo - moParent - moSolo - moParent + moCheckingOpen + moCreditOpen); // Test days-based reports TransactionHelper t1d1(QDate(2004, 7, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moSolo, acChecking, acSolo); TransactionHelper t2d1(QDate(2004, 7, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent1, acCredit, acParent); TransactionHelper t3d1(QDate(2004, 7, 4), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent2, acCredit, acParent); TransactionHelper t1d2(QDate(2004, 7, 14), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moSolo, acChecking, acSolo); TransactionHelper t2d2(QDate(2004, 7, 15), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent1, acCredit, acParent); TransactionHelper t3d2(QDate(2004, 7, 20), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent2, acCredit, acParent); TransactionHelper t1d3(QDate(2004, 8, 2), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moSolo, acChecking, acSolo); TransactionHelper t2d3(QDate(2004, 8, 3), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent1, acCredit, acParent); TransactionHelper t3d3(QDate(2004, 8, 4), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent2, acCredit, acParent); filter.setDateFilter(QDate(2004, 7, 2), QDate(2004, 7, 14)); - filter.setRowType(MyMoneyReport::eExpenseIncome); - filter.setColumnType(MyMoneyReport::eMonths); + filter.setRowType(eMyMoney::Report::RowType::ExpenseIncome); + filter.setColumnType(eMyMoney::Report::ColumnType::Months); filter.setColumnsAreDays(true); filter.setName("Spending by Days"); XMLandback(filter); PivotTable spending_days(filter); writeTabletoHTML(spending_days, "Spending by Days.html"); QVERIFY(spending_days.m_grid.m_total[eActual][2] == -moParent2); QVERIFY(spending_days.m_grid.m_total[eActual][12] == -moSolo); QVERIFY(spending_days.m_grid.m_total[eActual].m_total == -moSolo - moParent2); // set the first day of the week to 1 QLocale::setDefault(QLocale(QLocale::English, QLocale::UnitedKingdom)); filter.setDateFilter(QDate(2004, 7, 2), QDate(2004, 8, 1)); - filter.setRowType(MyMoneyReport::eExpenseIncome); - filter.setColumnType(static_cast(7)); + filter.setRowType(eMyMoney::Report::RowType::ExpenseIncome); + filter.setColumnType(static_cast(7)); filter.setColumnsAreDays(true); filter.setName("Spending by Weeks"); XMLandback(filter); PivotTable spending_weeks(filter); writeTabletoHTML(spending_weeks, "Spending by Weeks.html"); // restore the locale QLocale::setDefault(QLocale::system()); QVERIFY(spending_weeks.m_grid.m_total[eActual][0] == -moParent2); QVERIFY(spending_weeks.m_grid.m_total[eActual][1] == moZero); QVERIFY(spending_weeks.m_grid.m_total[eActual][2] == -moSolo - moParent1); QVERIFY(spending_weeks.m_grid.m_total[eActual][3] == -moParent2); QVERIFY(spending_weeks.m_grid.m_total[eActual][4] == moZero); QVERIFY(spending_weeks.m_grid.m_total[eActual].m_total == -moSolo - moParent - moParent2); } void PivotTableTest::testInvestment() { try { // Equities eqStock1 = makeEquity("Stock1", "STK1"); eqStock2 = makeEquity("Stock2", "STK2"); // Accounts acInvestment = makeAccount("Investment", eMyMoney::Account::Type::Investment, moZero, QDate(2004, 1, 1), acAsset); acStock1 = makeAccount("Stock 1", eMyMoney::Account::Type::Stock, moZero, QDate(2004, 1, 1), acInvestment, eqStock1); acStock2 = makeAccount("Stock 2", eMyMoney::Account::Type::Stock, moZero, QDate(2004, 1, 1), acInvestment, eqStock2); acDividends = makeAccount("Dividends", eMyMoney::Account::Type::Income, moZero, QDate(2004, 1, 1), acIncome); // Transactions // Date Action Shares Price Stock Asset Income InvTransactionHelper s1b1(QDate(2004, 2, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::BuyShares), MyMoneyMoney(1000.00), MyMoneyMoney(100.00), acStock1, acChecking, QString()); InvTransactionHelper s1b2(QDate(2004, 3, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::BuyShares), MyMoneyMoney(1000.00), MyMoneyMoney(110.00), acStock1, acChecking, QString()); InvTransactionHelper s1s1(QDate(2004, 4, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::BuyShares), MyMoneyMoney(-200.00), MyMoneyMoney(120.00), acStock1, acChecking, QString()); InvTransactionHelper s1s2(QDate(2004, 5, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::BuyShares), MyMoneyMoney(-200.00), MyMoneyMoney(100.00), acStock1, acChecking, QString()); InvTransactionHelper s1r1(QDate(2004, 6, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::ReinvestDividend), MyMoneyMoney(50.00), MyMoneyMoney(100.00), acStock1, QString(), acDividends); InvTransactionHelper s1r2(QDate(2004, 7, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::ReinvestDividend), MyMoneyMoney(50.00), MyMoneyMoney(80.00), acStock1, QString(), acDividends); InvTransactionHelper s1c1(QDate(2004, 8, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Dividend), MyMoneyMoney(10.00), MyMoneyMoney(100.00), acStock1, acChecking, acDividends); InvTransactionHelper s1c2(QDate(2004, 9, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Dividend), MyMoneyMoney(10.00), MyMoneyMoney(120.00), acStock1, acChecking, acDividends); makeEquityPrice(eqStock1, QDate(2004, 10, 1), MyMoneyMoney(100.00)); // // Net Worth Report (with investments) // MyMoneyReport networth_r; - networth_r.setRowType(MyMoneyReport::eAssetLiability); + networth_r.setRowType(eMyMoney::Report::RowType::AssetLiability); networth_r.setDateFilter(QDate(2004, 1, 1), QDate(2004, 12, 31).addDays(-1)); XMLandback(networth_r); PivotTable networth(networth_r); networth.dump("networth_i.html"); QVERIFY(networth.m_grid["Asset"]["Investment"].m_total[eActual][1] == moZero); // 1000 shares @ $100.00 QVERIFY(networth.m_grid["Asset"]["Investment"].m_total[eActual][2] == MyMoneyMoney(100000.0)); // 2000 shares @ $110.00 QVERIFY(networth.m_grid["Asset"]["Investment"].m_total[eActual][3] == MyMoneyMoney(220000.0)); // 1800 shares @ $120.00 QVERIFY(networth.m_grid["Asset"]["Investment"].m_total[eActual][4] == MyMoneyMoney(216000.0)); // 1600 shares @ $100.00 QVERIFY(networth.m_grid["Asset"]["Investment"].m_total[eActual][5] == MyMoneyMoney(160000.0)); // 1650 shares @ $100.00 QVERIFY(networth.m_grid["Asset"]["Investment"].m_total[eActual][6] == MyMoneyMoney(165000.0)); // 1700 shares @ $ 80.00 QVERIFY(networth.m_grid["Asset"]["Investment"].m_total[eActual][7] == MyMoneyMoney(136000.0)); // 1700 shares @ $100.00 QVERIFY(networth.m_grid["Asset"]["Investment"].m_total[eActual][8] == MyMoneyMoney(170000.0)); // 1700 shares @ $120.00 QVERIFY(networth.m_grid["Asset"]["Investment"].m_total[eActual][9] == MyMoneyMoney(204000.0)); // 1700 shares @ $100.00 QVERIFY(networth.m_grid["Asset"]["Investment"].m_total[eActual][10] == MyMoneyMoney(170000.0)); #if 0 // Dump file & reports QFile g("investmentkmy.xml"); g.open(QIODevice::WriteOnly); MyMoneyStorageXML xml; IMyMoneyOperationsFormat& interface = xml; interface.writeFile(&g, dynamic_cast(MyMoneyFile::instance()->storage())); g.close(); invtran.dump("invtran.html", "%1"); invhold.dump("invhold.html", "%1"); #endif } catch (const MyMoneyException &e) { QFAIL(e.what()); } } void PivotTableTest::testBudget() { // 1. Budget on A, transactions on A { BudgetHelper budget; budget += BudgetEntryHelper(QDate(2006, 1, 1), acSolo, false, MyMoneyMoney(100.0)); - MyMoneyReport report(MyMoneyReport::eBudgetActual, - MyMoneyReport::eMonths, + MyMoneyReport report(eMyMoney::Report::RowType::BudgetActual, + static_cast(eMyMoney::Report::ColumnType::Months), eMyMoney::TransactionFilter::Date::YearToDate, - MyMoneyReport::eDetailTop, + eMyMoney::Report::DetailLevel::Top, "Yearly Budgeted vs. Actual", "Default Report"); PivotTable table(report); } // 2. Budget on B, not applying to sub accounts, transactions on B and B:1 { BudgetHelper budget; budget += BudgetEntryHelper(QDate(2006, 1, 1), acParent, false, MyMoneyMoney(100.0)); - MyMoneyReport report(MyMoneyReport::eBudgetActual, - MyMoneyReport::eMonths, + MyMoneyReport report(eMyMoney::Report::RowType::BudgetActual, + static_cast(eMyMoney::Report::ColumnType::Months), eMyMoney::TransactionFilter::Date::YearToDate, - MyMoneyReport::eDetailTop, + eMyMoney::Report::DetailLevel::Top, "Yearly Budgeted vs. Actual", "Default Report"); PivotTable table(report); } // - Both B and B:1 totals should show up // - B actuals compare against B budget // - B:1 actuals compare against 0 // 3. Budget on C, applying to sub accounts, transactions on C and C:1 and C:1:a { BudgetHelper budget; budget += BudgetEntryHelper(QDate(2006, 1, 1), acParent, true, MyMoneyMoney(100.0)); - MyMoneyReport report(MyMoneyReport::eBudgetActual, - MyMoneyReport::eMonths, + MyMoneyReport report(eMyMoney::Report::RowType::BudgetActual, + static_cast(eMyMoney::Report::ColumnType::Months), eMyMoney::TransactionFilter::Date::YearToDate, - MyMoneyReport::eDetailTop , + eMyMoney::Report::DetailLevel::Top , "Yearly Budgeted vs. Actual", "Default Report"); PivotTable table(report); } // - Only C totals show up, not C:1 or C:1:a totals // - C + C:1 totals compare against C budget // 4. Budget on D, not applying to sub accounts, budget on D:1 not applying, budget on D:2 applying. Transactions on D, D:1, D:2, D:2:a, D:2:b { BudgetHelper budget; budget += BudgetEntryHelper(QDate(2006, 1, 1), acParent, false, MyMoneyMoney(100.0)); budget += BudgetEntryHelper(QDate(2006, 1, 1), acChild, false, MyMoneyMoney(100.0)); budget += BudgetEntryHelper(QDate(2006, 1, 1), acSecondChild, true, MyMoneyMoney(100.0)); - MyMoneyReport report(MyMoneyReport::eBudgetActual, - MyMoneyReport::eMonths, + MyMoneyReport report(eMyMoney::Report::RowType::BudgetActual, + static_cast(eMyMoney::Report::ColumnType::Months), eMyMoney::TransactionFilter::Date::YearToDate, - MyMoneyReport::eDetailTop, + eMyMoney::Report::DetailLevel::Top, "Yearly Budgeted vs. Actual", "Default Report"); PivotTable table(report); } // - Totals for D, D:1, D:2 show up. D:2:a and D:2:b do not // - D actuals (only) compare against D budget // - Ditto for D:1 // - D:2 acutals and children compare against D:2 budget // 5. Budget on E, no transactions on E { BudgetHelper budget; budget += BudgetEntryHelper(QDate(2006, 1, 1), acSolo, false, MyMoneyMoney(100.0)); - MyMoneyReport report(MyMoneyReport::eBudgetActual, - MyMoneyReport::eMonths, + MyMoneyReport report(eMyMoney::Report::RowType::BudgetActual, + static_cast(eMyMoney::Report::ColumnType::Months), eMyMoney::TransactionFilter::Date::YearToDate, - MyMoneyReport::eDetailTop, + eMyMoney::Report::DetailLevel::Top, "Yearly Budgeted vs. Actual", "Default Report"); PivotTable table(report); } } void PivotTableTest::testHtmlEncoding() { MyMoneyReport filter; - filter.setRowType(MyMoneyReport::eAssetLiability); + filter.setRowType(eMyMoney::Report::RowType::AssetLiability); filter.setDateFilter(QDate(2004, 1, 1), QDate(2005, 1, 1).addDays(-1)); XMLandback(filter); PivotTable networth_f(filter); QByteArray encoding = QTextCodec::codecForLocale()->name(); QString html = networth_f.renderReport(QLatin1String("html"), encoding, filter.name(), false); QRegExp rx(QString::fromLatin1("**")); rx.setPatternSyntax(QRegExp::Wildcard); rx.setCaseSensitivity(Qt::CaseInsensitive); QVERIFY(rx.exactMatch(html)); } diff --git a/kmymoney/reports/tests/querytable-test.cpp b/kmymoney/reports/tests/querytable-test.cpp index a0dc6bfff..8bb6c1357 100644 --- a/kmymoney/reports/tests/querytable-test.cpp +++ b/kmymoney/reports/tests/querytable-test.cpp @@ -1,904 +1,906 @@ /* * Copyright 2005-2018 Thomas Baumgart * Copyright 2005-2006 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. * * 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 "querytable-test.h" #include #include #include #include "reportstestcommon.h" #include "querytable.h" #include "mymoneyinstitution.h" #include "mymoneyaccount.h" #include "mymoneysecurity.h" #include "mymoneyprice.h" #include "mymoneystoragedump.h" #include "mymoneyreport.h" #include "mymoneysplit.h" #include "mymoneypayee.h" #include "mymoneystatement.h" #include "mymoneyexception.h" #include "kmymoneysettings.h" using namespace reports; using namespace test; QTEST_GUILESS_MAIN(QueryTableTest) void QueryTableTest::setup() { } void QueryTableTest::init() { storage = new MyMoneyStorageMgr; file = MyMoneyFile::instance(); file->attachStorage(storage); MyMoneyFileTransaction ft; file->addCurrency(MyMoneySecurity("CAD", "Canadian Dollar", "C$")); file->addCurrency(MyMoneySecurity("USD", "US Dollar", "$")); file->addCurrency(MyMoneySecurity("JPY", "Japanese Yen", QChar(0x00A5), 1)); file->addCurrency(MyMoneySecurity("GBP", "British Pound", "#")); file->setBaseCurrency(file->currency("USD")); - MyMoneyPayee payeeTest("Test Payee"); + MyMoneyPayee payeeTest; + payeeTest.setName("Test Payee"); file->addPayee(payeeTest); - MyMoneyPayee payeeTest2("Thomas Baumgart"); + MyMoneyPayee payeeTest2; + payeeTest2.setName("Thomas Baumgart"); file->addPayee(payeeTest2); acAsset = (MyMoneyFile::instance()->asset().id()); acLiability = (MyMoneyFile::instance()->liability().id()); acExpense = (MyMoneyFile::instance()->expense().id()); acIncome = (MyMoneyFile::instance()->income().id()); acChecking = makeAccount(QString("Checking Account"), eMyMoney::Account::Type::Checkings, moCheckingOpen, QDate(2004, 5, 15), acAsset); acCredit = makeAccount(QString("Credit Card"), eMyMoney::Account::Type::CreditCard, moCreditOpen, QDate(2004, 7, 15), acLiability); acSolo = makeAccount(QString("Solo"), eMyMoney::Account::Type::Expense, MyMoneyMoney(), QDate(2004, 1, 11), acExpense); acParent = makeAccount(QString("Parent"), eMyMoney::Account::Type::Expense, MyMoneyMoney(), QDate(2004, 1, 11), acExpense); acChild = makeAccount(QString("Child"), eMyMoney::Account::Type::Expense, MyMoneyMoney(), QDate(2004, 2, 11), acParent); acForeign = makeAccount(QString("Foreign"), eMyMoney::Account::Type::Expense, MyMoneyMoney(), QDate(2004, 1, 11), acExpense); acTax = makeAccount(QString("Tax"), eMyMoney::Account::Type::Expense, MyMoneyMoney(), QDate(2005, 1, 11), acExpense, "", true); MyMoneyInstitution i("Bank of the World", "", "", "", "", "", ""); file->addInstitution(i); inBank = i.id(); ft.commit(); } void QueryTableTest::cleanup() { file->detachStorage(storage); delete storage; } void QueryTableTest::testQueryBasics() { try { TransactionHelper t1q1(QDate(2004, 1, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moSolo, acChecking, acSolo); TransactionHelper t2q1(QDate(2004, 2, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent1, acCredit, acParent); TransactionHelper t3q1(QDate(2004, 3, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent2, acCredit, acParent); TransactionHelper t4y1(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moChild, acCredit, acChild); TransactionHelper t1q2(QDate(2004, 4, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moSolo, acChecking, acSolo); TransactionHelper t2q2(QDate(2004, 5, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent1, acCredit, acParent); TransactionHelper t3q2(QDate(2004, 6, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent2, acCredit, acParent); TransactionHelper t4q2(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moChild, acCredit, acChild); TransactionHelper t1y2(QDate(2005, 1, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moSolo, acChecking, acSolo); TransactionHelper t2y2(QDate(2005, 5, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent1, acCredit, acParent); TransactionHelper t3y2(QDate(2005, 9, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent2, acCredit, acParent); TransactionHelper t4y2(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moChild, acCredit, acChild); unsigned cols; MyMoneyReport filter; - filter.setRowType(MyMoneyReport::eCategory); - cols = MyMoneyReport::eQCnumber | MyMoneyReport::eQCpayee | MyMoneyReport::eQCaccount; - filter.setQueryColumns(static_cast(cols)); // + filter.setRowType(eMyMoney::Report::RowType::Category); + cols = eMyMoney::Report::QueryColumn::Number | eMyMoney::Report::QueryColumn::Payee | eMyMoney::Report::QueryColumn::Account; + filter.setQueryColumns(static_cast(cols)); // filter.setName("Transactions by Category"); XMLandback(filter); QueryTable qtbl_1(filter); writeTabletoHTML(qtbl_1, "Transactions by Category.html"); QList rows = qtbl_1.rows(); QVERIFY(rows.count() == 19); QVERIFY(rows[0][ListTable::ctCategoryType] == "Expense"); QVERIFY(rows[0][ListTable::ctCategory] == "Parent"); QVERIFY(rows[0][ListTable::ctPostDate] == "2004-02-01"); QVERIFY(rows[14][ListTable::ctCategoryType] == "Expense"); QVERIFY(rows[14][ListTable::ctCategory] == "Solo"); QVERIFY(rows[14][ListTable::ctPostDate] == "2005-01-01"); QVERIFY(MyMoneyMoney(rows[6][ListTable::ctValue]) == -(moParent1 + moParent2) * 3); QVERIFY(MyMoneyMoney(rows[10][ListTable::ctValue]) == -(moChild) * 3); QVERIFY(MyMoneyMoney(rows[16][ListTable::ctValue]) == -(moSolo) * 3); QVERIFY(MyMoneyMoney(rows[17][ListTable::ctValue]) == -(moParent1 + moParent2 + moSolo + moChild) * 3); QVERIFY(MyMoneyMoney(rows[18][ListTable::ctValue]) == -(moParent1 + moParent2 + moSolo + moChild) * 3 + moCheckingOpen + moCreditOpen); - filter.setRowType(MyMoneyReport::eTopCategory); - cols = MyMoneyReport::eQCnumber | MyMoneyReport::eQCpayee | MyMoneyReport::eQCaccount; - filter.setQueryColumns(static_cast(cols)); // + filter.setRowType(eMyMoney::Report::RowType::TopCategory); + cols = eMyMoney::Report::QueryColumn::Number | eMyMoney::Report::QueryColumn::Payee | eMyMoney::Report::QueryColumn::Account; + filter.setQueryColumns(static_cast(cols)); // filter.setName("Transactions by Top Category"); XMLandback(filter); QueryTable qtbl_2(filter); writeTabletoHTML(qtbl_2, "Transactions by Top Category.html"); rows = qtbl_2.rows(); QVERIFY(rows.count() == 16); QVERIFY(rows[0][ListTable::ctCategoryType] == "Expense"); QVERIFY(rows[0][ListTable::ctTopCategory] == "Parent"); QVERIFY(rows[0][ListTable::ctPostDate] == "2004-02-01"); QVERIFY(rows[8][ListTable::ctCategoryType] == "Expense"); QVERIFY(rows[8][ListTable::ctTopCategory] == "Parent"); QVERIFY(rows[8][ListTable::ctPostDate] == "2005-09-01"); QVERIFY(rows[12][ListTable::ctCategoryType] == "Expense"); QVERIFY(rows[12][ListTable::ctTopCategory] == "Solo"); QVERIFY(rows[12][ListTable::ctPostDate] == "2005-01-01"); QVERIFY(MyMoneyMoney(rows[9][ListTable::ctValue]) == -(moParent1 + moParent2 + moChild) * 3); QVERIFY(MyMoneyMoney(rows[13][ListTable::ctValue]) == -(moSolo) * 3); QVERIFY(MyMoneyMoney(rows[14][ListTable::ctValue]) == -(moParent1 + moParent2 + moSolo + moChild) * 3); QVERIFY(MyMoneyMoney(rows[15][ListTable::ctValue]) == -(moParent1 + moParent2 + moSolo + moChild) * 3 + moCheckingOpen + moCreditOpen); - filter.setRowType(MyMoneyReport::eAccount); + filter.setRowType(eMyMoney::Report::RowType::Account); filter.setName("Transactions by Account"); - cols = MyMoneyReport::eQCnumber | MyMoneyReport::eQCpayee | MyMoneyReport::eQCcategory; - filter.setQueryColumns(static_cast(cols)); // + cols = eMyMoney::Report::QueryColumn::Number | eMyMoney::Report::QueryColumn::Payee | eMyMoney::Report::QueryColumn::Category; + filter.setQueryColumns(static_cast(cols)); // XMLandback(filter); QueryTable qtbl_3(filter); writeTabletoHTML(qtbl_3, "Transactions by Account.html"); rows = qtbl_3.rows(); #if 1 QVERIFY(rows.count() == 19); QVERIFY(rows[1][ListTable::ctAccount] == "Checking Account"); QVERIFY(rows[1][ListTable::ctCategory] == "Solo"); QVERIFY(rows[1][ListTable::ctPostDate] == "2004-01-01"); QVERIFY(rows[15][ListTable::ctAccount] == "Credit Card"); QVERIFY(rows[15][ListTable::ctCategory] == "Parent"); QVERIFY(rows[15][ListTable::ctPostDate] == "2005-09-01"); #else QVERIFY(rows.count() == 12); QVERIFY(rows[0][ListTable::ctAccount] == "Checking Account"); QVERIFY(rows[0][ListTable::ctCategory] == "Solo"); QVERIFY(rows[0][ListTable::ctPostDate] == "2004-01-01"); QVERIFY(rows[11][ListTable::ctAccount] == "Credit Card"); QVERIFY(rows[11][ListTable::ctCategory] == "Parent"); QVERIFY(rows[11][ListTable::ctPostDate] == "2005-09-01"); #endif QVERIFY(MyMoneyMoney(rows[5][ListTable::ctValue]) == -(moSolo) * 3 + moCheckingOpen); QVERIFY(MyMoneyMoney(rows[17][ListTable::ctValue]) == -(moParent1 + moParent2 + moChild) * 3 + moCreditOpen); QVERIFY(MyMoneyMoney(rows[18][ListTable::ctValue]) == -(moParent1 + moParent2 + moSolo + moChild) * 3 + moCheckingOpen + moCreditOpen); - filter.setRowType(MyMoneyReport::ePayee); + filter.setRowType(eMyMoney::Report::RowType::Payee); filter.setName("Transactions by Payee"); - cols = MyMoneyReport::eQCnumber | MyMoneyReport::eQCmemo | MyMoneyReport::eQCcategory; - filter.setQueryColumns(static_cast(cols)); // + cols = eMyMoney::Report::QueryColumn::Number | eMyMoney::Report::QueryColumn::Memo | eMyMoney::Report::QueryColumn::Category; + filter.setQueryColumns(static_cast(cols)); // XMLandback(filter); QueryTable qtbl_4(filter); writeTabletoHTML(qtbl_4, "Transactions by Payee.html"); rows = qtbl_4.rows(); QVERIFY(rows.count() == 14); QVERIFY(rows[0][ListTable::ctPayee] == "Test Payee"); QVERIFY(rows[0][ListTable::ctCategory] == "Solo"); QVERIFY(rows[0][ListTable::ctPostDate] == "2004-01-01"); QVERIFY(rows[7][ListTable::ctPayee] == "Test Payee"); QVERIFY(rows[7][ListTable::ctCategory] == "Parent: Child"); QVERIFY(rows[7][ListTable::ctPostDate] == "2004-11-07"); QVERIFY(rows[11][ListTable::ctPayee] == "Test Payee"); QVERIFY(rows[11][ListTable::ctCategory] == "Parent"); QVERIFY(rows[11][ListTable::ctPostDate] == "2005-09-01"); QVERIFY(MyMoneyMoney(rows[12][ListTable::ctValue]) == -(moParent1 + moParent2 + moSolo + moChild) * 3); QVERIFY(MyMoneyMoney(rows[13][ListTable::ctValue]) == -(moParent1 + moParent2 + moSolo + moChild) * 3 + moCheckingOpen + moCreditOpen); - filter.setRowType(MyMoneyReport::eMonth); + filter.setRowType(eMyMoney::Report::RowType::Month); filter.setName("Transactions by Month"); - cols = MyMoneyReport::eQCnumber | MyMoneyReport::eQCpayee | MyMoneyReport::eQCcategory; - filter.setQueryColumns(static_cast(cols)); // + cols = eMyMoney::Report::QueryColumn::Number | eMyMoney::Report::QueryColumn::Payee | eMyMoney::Report::QueryColumn::Category; + filter.setQueryColumns(static_cast(cols)); // XMLandback(filter); QueryTable qtbl_5(filter); writeTabletoHTML(qtbl_5, "Transactions by Month.html"); rows = qtbl_5.rows(); QVERIFY(rows.count() == 23); QVERIFY(rows[0][ListTable::ctPayee] == "Test Payee"); QVERIFY(rows[0][ListTable::ctCategory] == "Solo"); QVERIFY(rows[0][ListTable::ctPostDate] == "2004-01-01"); QVERIFY(rows[12][ListTable::ctPayee] == "Test Payee"); QVERIFY(rows[12][ListTable::ctCategory] == "Parent: Child"); QVERIFY(rows[12][ListTable::ctPostDate] == "2004-11-07"); QVERIFY(rows[20][ListTable::ctPayee] == "Test Payee"); QVERIFY(rows[20][ListTable::ctCategory] == "Parent"); QVERIFY(rows[20][ListTable::ctPostDate] == "2005-09-01"); QVERIFY(MyMoneyMoney(rows[1][ListTable::ctValue]) == -moSolo); QVERIFY(MyMoneyMoney(rows[15][ListTable::ctValue]) == -(moChild) * 3); QVERIFY(MyMoneyMoney(rows[9][ListTable::ctValue]) == -moParent1 + moCheckingOpen); QVERIFY(MyMoneyMoney(rows[22][ListTable::ctValue]) == -(moParent1 + moParent2 + moSolo + moChild) * 3 + moCheckingOpen + moCreditOpen); - filter.setRowType(MyMoneyReport::eWeek); + filter.setRowType(eMyMoney::Report::RowType::Week); filter.setName("Transactions by Week"); - cols = MyMoneyReport::eQCnumber | MyMoneyReport::eQCpayee | MyMoneyReport::eQCcategory; - filter.setQueryColumns(static_cast(cols)); // + cols = eMyMoney::Report::QueryColumn::Number | eMyMoney::Report::QueryColumn::Payee | eMyMoney::Report::QueryColumn::Category; + filter.setQueryColumns(static_cast(cols)); // XMLandback(filter); QueryTable qtbl_6(filter); writeTabletoHTML(qtbl_6, "Transactions by Week.html"); rows = qtbl_6.rows(); QVERIFY(rows.count() == 23); QVERIFY(rows[0][ListTable::ctPayee] == "Test Payee"); QVERIFY(rows[0][ListTable::ctCategory] == "Solo"); QVERIFY(rows[0][ListTable::ctPostDate] == "2004-01-01"); QVERIFY(rows[20][ListTable::ctPayee] == "Test Payee"); QVERIFY(rows[20][ListTable::ctCategory] == "Parent"); QVERIFY(rows[20][ListTable::ctPostDate] == "2005-09-01"); QVERIFY(MyMoneyMoney(rows[1][ListTable::ctValue]) == -moSolo); QVERIFY(MyMoneyMoney(rows[15][ListTable::ctValue]) == -(moChild) * 3); QVERIFY(MyMoneyMoney(rows[21][ListTable::ctValue]) == -moParent2); QVERIFY(MyMoneyMoney(rows[22][ListTable::ctValue]) == -(moParent1 + moParent2 + moSolo + moChild) * 3 + moCheckingOpen + moCreditOpen); } catch (const MyMoneyException &e) { QFAIL(e.what()); } // Test querytable::TableRow::operator> and operator== QueryTable::TableRow low; low[ListTable::ctPrice] = 'A'; low[ListTable::ctLastPrice] = 'B'; low[ListTable::ctBuyPrice] = 'C'; QueryTable::TableRow high; high[ListTable::ctPrice] = 'A'; high[ListTable::ctLastPrice] = 'C'; high[ListTable::ctBuyPrice] = 'B'; QueryTable::TableRow::setSortCriteria({ListTable::ctPrice, ListTable::ctLastPrice, ListTable::ctBuyPrice}); QVERIFY(low < high); QVERIFY(low <= high); QVERIFY(high > low); QVERIFY(high <= high); QVERIFY(high == high); } void QueryTableTest::testCashFlowAnalysis() { // // Test IRR calculations // CashFlowList list; list += CashFlowListItem(QDate(2004, 5, 3), MyMoneyMoney(1000.0)); list += CashFlowListItem(QDate(2004, 5, 20), MyMoneyMoney(59.0)); list += CashFlowListItem(QDate(2004, 6, 3), MyMoneyMoney(14.0)); list += CashFlowListItem(QDate(2004, 6, 24), MyMoneyMoney(92.0)); list += CashFlowListItem(QDate(2004, 7, 6), MyMoneyMoney(63.0)); list += CashFlowListItem(QDate(2004, 7, 25), MyMoneyMoney(15.0)); list += CashFlowListItem(QDate(2004, 8, 5), MyMoneyMoney(92.0)); list += CashFlowListItem(QDate(2004, 9, 2), MyMoneyMoney(18.0)); list += CashFlowListItem(QDate(2004, 9, 21), MyMoneyMoney(5.0)); list += CashFlowListItem(QDate(2004, 10, 16), MyMoneyMoney(-2037.0)); MyMoneyMoney IRR(list.IRR(), 1000); QVERIFY(IRR == MyMoneyMoney(1676, 1000)); list.pop_back(); list += CashFlowListItem(QDate(2004, 10, 16), MyMoneyMoney(-1358.0)); IRR = MyMoneyMoney(list.IRR(), 1000); QVERIFY(IRR.isZero()); } void QueryTableTest::testAccountQuery() { try { QString htmlcontext = QString("\n\n%1\n\n"); // // No transactions, opening balances only // MyMoneyReport filter; - filter.setRowType(MyMoneyReport::eInstitution); + filter.setRowType(eMyMoney::Report::RowType::Institution); filter.setName("Accounts by Institution (No transactions)"); XMLandback(filter); QueryTable qtbl_1(filter); writeTabletoHTML(qtbl_1, "Accounts by Institution (No transactions).html"); QList rows = qtbl_1.rows(); QVERIFY(rows.count() == 6); QVERIFY(rows[0][ListTable::ctAccount] == "Checking Account"); QVERIFY(MyMoneyMoney(rows[0][ListTable::ctValue]) == moCheckingOpen); QVERIFY(rows[0][ListTable::ctEquityType].isEmpty()); QVERIFY(rows[2][ListTable::ctAccount] == "Credit Card"); QVERIFY(MyMoneyMoney(rows[1][ListTable::ctValue]) == moCreditOpen); QVERIFY(rows[2][ListTable::ctEquityType].isEmpty()); QVERIFY(MyMoneyMoney(rows[4][ListTable::ctValue]) == moCheckingOpen + moCreditOpen); QVERIFY(MyMoneyMoney(rows[5][ListTable::ctValue]) == moCheckingOpen + moCreditOpen); // // Adding in transactions // TransactionHelper t1q1(QDate(2004, 1, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moSolo, acChecking, acSolo); TransactionHelper t2q1(QDate(2004, 2, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent1, acCredit, acParent); TransactionHelper t3q1(QDate(2004, 3, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent2, acCredit, acParent); TransactionHelper t4y1(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moChild, acCredit, acChild); TransactionHelper t1q2(QDate(2004, 4, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moSolo, acChecking, acSolo); TransactionHelper t2q2(QDate(2004, 5, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent1, acCredit, acParent); TransactionHelper t3q2(QDate(2004, 6, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent2, acCredit, acParent); TransactionHelper t4q2(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moChild, acCredit, acChild); TransactionHelper t1y2(QDate(2005, 1, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moSolo, acChecking, acSolo); TransactionHelper t2y2(QDate(2005, 5, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent1, acCredit, acParent); TransactionHelper t3y2(QDate(2005, 9, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent2, acCredit, acParent); TransactionHelper t4y2(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moChild, acCredit, acChild); - filter.setRowType(MyMoneyReport::eInstitution); + filter.setRowType(eMyMoney::Report::RowType::Institution); filter.setName("Accounts by Institution (With Transactions)"); XMLandback(filter); QueryTable qtbl_2(filter); rows = qtbl_2.rows(); QVERIFY(rows.count() == 6); QVERIFY(rows[0][ListTable::ctAccount] == "Checking Account"); QVERIFY(MyMoneyMoney(rows[0][ListTable::ctValue]) == (moCheckingOpen - moSolo*3)); QVERIFY(rows[2][ListTable::ctAccount] == "Credit Card"); QVERIFY(MyMoneyMoney(rows[2][ListTable::ctValue]) == (moCreditOpen - (moParent1 + moParent2 + moChild) * 3)); QVERIFY(MyMoneyMoney(rows[5][ListTable::ctValue]) == moCheckingOpen + moCreditOpen - (moParent1 + moParent2 + moSolo + moChild) * 3); // // Account TYPES // - filter.setRowType(MyMoneyReport::eAccountType); + filter.setRowType(eMyMoney::Report::RowType::AccountType); filter.setName("Accounts by Type"); XMLandback(filter); QueryTable qtbl_3(filter); rows = qtbl_3.rows(); QVERIFY(rows.count() == 5); QVERIFY(rows[0][ListTable::ctAccount] == "Checking Account"); QVERIFY(MyMoneyMoney(rows[0][ListTable::ctValue]) == (moCheckingOpen - moSolo * 3)); QVERIFY(rows[2][ListTable::ctAccount] == "Credit Card"); QVERIFY(MyMoneyMoney(rows[2][ListTable::ctValue]) == (moCreditOpen - (moParent1 + moParent2 + moChild) * 3)); QVERIFY(MyMoneyMoney(rows[1][ListTable::ctValue]) == moCheckingOpen - moSolo * 3); QVERIFY(MyMoneyMoney(rows[3][ListTable::ctValue]) == moCreditOpen - (moParent1 + moParent2 + moChild) * 3); QVERIFY(MyMoneyMoney(rows[4][ListTable::ctValue]) == moCheckingOpen + moCreditOpen - (moParent1 + moParent2 + moSolo + moChild) * 3); } catch (const MyMoneyException &e) { QFAIL(e.what()); } } void QueryTableTest::testInvestment() { try { // Equities eqStock1 = makeEquity("Stock1", "STK1"); eqStock2 = makeEquity("Stock2", "STK2"); eqStock3 = makeEquity("Stock3", "STK3"); eqStock4 = makeEquity("Stock4", "STK4"); // Accounts acInvestment = makeAccount("Investment", eMyMoney::Account::Type::Investment, moZero, QDate(2003, 11, 1), acAsset); acStock1 = makeAccount("Stock 1", eMyMoney::Account::Type::Stock, moZero, QDate(2004, 1, 1), acInvestment, eqStock1); acStock2 = makeAccount("Stock 2", eMyMoney::Account::Type::Stock, moZero, QDate(2004, 1, 1), acInvestment, eqStock2); acStock3 = makeAccount("Stock 3", eMyMoney::Account::Type::Stock, moZero, QDate(2003, 11, 1), acInvestment, eqStock3); acStock4 = makeAccount("Stock 4", eMyMoney::Account::Type::Stock, moZero, QDate(2004, 1, 1), acInvestment, eqStock4); acDividends = makeAccount("Dividends", eMyMoney::Account::Type::Income, moZero, QDate(2004, 1, 1), acIncome); acInterest = makeAccount("Interest", eMyMoney::Account::Type::Income, moZero, QDate(2004, 1, 1), acIncome); acFees = makeAccount("Fees", eMyMoney::Account::Type::Expense, moZero, QDate(2003, 11, 1), acExpense); // Transactions // Date Action Shares Price Stock Asset Income InvTransactionHelper s1b1(QDate(2003, 12, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::BuyShares), MyMoneyMoney(1000.00), MyMoneyMoney(100.00), acStock3, acChecking, QString()); InvTransactionHelper s1b2(QDate(2004, 1, 30), MyMoneySplit::actionName(eMyMoney::Split::Action::BuyShares), MyMoneyMoney(500.00), MyMoneyMoney(100.00), acStock4, acChecking, acFees, MyMoneyMoney(100.00)); InvTransactionHelper s1b3(QDate(2004, 1, 30), MyMoneySplit::actionName(eMyMoney::Split::Action::BuyShares), MyMoneyMoney(500.00), MyMoneyMoney(90.00), acStock4, acChecking, acFees, MyMoneyMoney(100.00)); InvTransactionHelper s1b4(QDate(2004, 2, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::BuyShares), MyMoneyMoney(1000.00), MyMoneyMoney(100.00), acStock1, acChecking, QString()); InvTransactionHelper s1b5(QDate(2004, 3, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::BuyShares), MyMoneyMoney(1000.00), MyMoneyMoney(110.00), acStock1, acChecking, QString()); InvTransactionHelper s1s1(QDate(2004, 4, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::BuyShares), MyMoneyMoney(-200.00), MyMoneyMoney(120.00), acStock1, acChecking, QString()); InvTransactionHelper s1s2(QDate(2004, 5, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::BuyShares), MyMoneyMoney(-200.00), MyMoneyMoney(100.00), acStock1, acChecking, QString()); InvTransactionHelper s1s3(QDate(2004, 5, 30), MyMoneySplit::actionName(eMyMoney::Split::Action::BuyShares), MyMoneyMoney(-1000.00), MyMoneyMoney(120.00), acStock4, acChecking, acFees, MyMoneyMoney(200.00)); InvTransactionHelper s1r1(QDate(2004, 6, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::ReinvestDividend), MyMoneyMoney(50.00), MyMoneyMoney(100.00), acStock1, QString(), acDividends); InvTransactionHelper s1r2(QDate(2004, 7, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::ReinvestDividend), MyMoneyMoney(50.00), MyMoneyMoney(80.00), acStock1, QString(), acDividends); InvTransactionHelper s1c1(QDate(2004, 8, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Dividend), MyMoneyMoney(10.00), MyMoneyMoney(100.00), acStock1, acChecking, acDividends); InvTransactionHelper s1c2(QDate(2004, 9, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Dividend), MyMoneyMoney(10.00), MyMoneyMoney(120.00), acStock1, acChecking, acDividends); InvTransactionHelper s1y1(QDate(2004, 9, 15), MyMoneySplit::actionName(eMyMoney::Split::Action::Yield), MyMoneyMoney(10.00), MyMoneyMoney(110.00), acStock1, acChecking, acInterest); makeEquityPrice(eqStock1, QDate(2004, 10, 1), MyMoneyMoney(100.00)); makeEquityPrice(eqStock3, QDate(2004, 10, 1), MyMoneyMoney(110.00)); makeEquityPrice(eqStock4, QDate(2004, 10, 1), MyMoneyMoney(110.00)); // // Investment Transactions Report // MyMoneyReport invtran_r( - MyMoneyReport::eTopAccount, - MyMoneyReport::eQCaction | MyMoneyReport::eQCshares | MyMoneyReport::eQCprice, + eMyMoney::Report::RowType::TopAccount, + eMyMoney::Report::QueryColumn::Action | eMyMoney::Report::QueryColumn::Shares | eMyMoney::Report::QueryColumn::Price, eMyMoney::TransactionFilter::Date::UserDefined, - MyMoneyReport::eDetailAll, + eMyMoney::Report::DetailLevel::All, i18n("Investment Transactions"), i18n("Test Report") ); invtran_r.setDateFilter(QDate(2004, 1, 1), QDate(2004, 12, 31)); invtran_r.setInvestmentsOnly(true); XMLandback(invtran_r); QueryTable invtran(invtran_r); #if 1 writeTabletoHTML(invtran, "investment_transactions_test.html"); QList rows = invtran.rows(); QVERIFY(rows.count() == 32); QVERIFY(MyMoneyMoney(rows[1][ListTable::ctValue]) == MyMoneyMoney(-100000.00)); QVERIFY(MyMoneyMoney(rows[2][ListTable::ctValue]) == MyMoneyMoney(-110000.00)); QVERIFY(MyMoneyMoney(rows[3][ListTable::ctValue]) == MyMoneyMoney(24000.00)); QVERIFY(MyMoneyMoney(rows[4][ListTable::ctValue]) == MyMoneyMoney(20000.00)); QVERIFY(MyMoneyMoney(rows[5][ListTable::ctValue]) == MyMoneyMoney(5000.00)); QVERIFY(MyMoneyMoney(rows[6][ListTable::ctValue]) == MyMoneyMoney(4000.00)); QVERIFY(MyMoneyMoney(rows[19][ListTable::ctValue]) == MyMoneyMoney(-50100.00)); QVERIFY(MyMoneyMoney(rows[22][ListTable::ctValue]) == MyMoneyMoney(-45100.00)); // need to fix these... fundamentally different from the original test //QVERIFY(MyMoneyMoney(invtran.m_rows[8][ListTable::ctValue])==MyMoneyMoney( -1000.00)); //QVERIFY(MyMoneyMoney(invtran.m_rows[11][ListTable::ctValue])==MyMoneyMoney( -1200.00)); //QVERIFY(MyMoneyMoney(invtran.m_rows[14][ListTable::ctValue])==MyMoneyMoney( -1100.00)); QVERIFY(MyMoneyMoney(rows[1][ListTable::ctPrice]) == MyMoneyMoney(100.00)); QVERIFY(MyMoneyMoney(rows[3][ListTable::ctPrice]) == MyMoneyMoney(120.00)); QVERIFY(MyMoneyMoney(rows[5][ListTable::ctPrice]) == MyMoneyMoney(100.00)); QVERIFY(MyMoneyMoney(rows[7][ListTable::ctPrice]) == MyMoneyMoney()); QVERIFY(MyMoneyMoney(rows[10][ListTable::ctPrice]) == MyMoneyMoney()); QVERIFY(MyMoneyMoney(rows[19][ListTable::ctPrice]) == MyMoneyMoney(100.00)); QVERIFY(MyMoneyMoney(rows[22][ListTable::ctPrice]) == MyMoneyMoney(90.00)); QVERIFY(MyMoneyMoney(rows[2][ListTable::ctShares]) == MyMoneyMoney(1000.00)); QVERIFY(MyMoneyMoney(rows[4][ListTable::ctShares]) == MyMoneyMoney(-200.00)); QVERIFY(MyMoneyMoney(rows[6][ListTable::ctShares]) == MyMoneyMoney(50.00)); QVERIFY(MyMoneyMoney(rows[8][ListTable::ctShares]) == MyMoneyMoney(0.00)); QVERIFY(MyMoneyMoney(rows[11][ListTable::ctShares]) == MyMoneyMoney(0.00)); QVERIFY(MyMoneyMoney(rows[19][ListTable::ctShares]) == MyMoneyMoney(500.00)); QVERIFY(MyMoneyMoney(rows[22][ListTable::ctShares]) == MyMoneyMoney(500.00)); QVERIFY(rows[1][ListTable::ctAction] == "Buy"); QVERIFY(rows[3][ListTable::ctAction] == "Sell"); QVERIFY(rows[5][ListTable::ctAction] == "Reinvest"); QVERIFY(rows[7][ListTable::ctAction] == "Dividend"); QVERIFY(rows[13][ListTable::ctAction] == "Yield"); QVERIFY(rows[19][ListTable::ctAction] == "Buy"); QVERIFY(rows[22][ListTable::ctAction] == "Buy"); #else QVERIFY(rows.count() == 9); QVERIFY(MyMoneyMoney(rows[0][ListTable::ctValue]) == MyMoneyMoney(100000.00)); QVERIFY(MyMoneyMoney(rows[1][ListTable::ctValue]) == MyMoneyMoney(110000.00)); QVERIFY(MyMoneyMoney(rows[2][ListTable::ctValue]) == MyMoneyMoney(-24000.00)); QVERIFY(MyMoneyMoney(rows[3][ListTable::ctValue]) == MyMoneyMoney(-20000.00)); QVERIFY(MyMoneyMoney(rows[4][ListTable::ctValue]) == MyMoneyMoney(5000.00)); QVERIFY(MyMoneyMoney(rows[5][ListTable::ctValue]) == MyMoneyMoney(4000.00)); QVERIFY(MyMoneyMoney(rows[6][ListTable::ctValue]) == MyMoneyMoney(-1000.00)); QVERIFY(MyMoneyMoney(rows[7][ListTable::ctValue]) == MyMoneyMoney(-1200.00)); QVERIFY(MyMoneyMoney(rows[8][ListTable::ctValue]) == MyMoneyMoney(-1100.00)); QVERIFY(MyMoneyMoney(rows[0][ListTable::ctPrice]) == MyMoneyMoney(100.00)); QVERIFY(MyMoneyMoney(rows[2][ListTable::ctPrice]) == MyMoneyMoney(120.00)); QVERIFY(MyMoneyMoney(rows[4][ListTable::ctPrice]) == MyMoneyMoney(100.00)); QVERIFY(MyMoneyMoney(rows[6][ListTable::ctPrice]) == MyMoneyMoney(0.00)); QVERIFY(MyMoneyMoney(rows[8][ListTable::ctPrice]) == MyMoneyMoney(0.00)); QVERIFY(MyMoneyMoney(rows[1][ListTable::ctShares]) == MyMoneyMoney(1000.00)); QVERIFY(MyMoneyMoney(rows[3][ListTable::ctShares]) == MyMoneyMoney(-200.00)); QVERIFY(MyMoneyMoney(rows[5][ListTable::ctShares]) == MyMoneyMoney(50.00)); QVERIFY(MyMoneyMoney(rows[7][ListTable::ctShares]) == MyMoneyMoney(0.00)); QVERIFY(MyMoneyMoney(rows[8][ListTable::ctShares]) == MyMoneyMoney(0.00)); QVERIFY(rows[0][ListTable::ctAction] == "Buy"); QVERIFY(rows[2][ListTable::ctAction] == "Sell"); QVERIFY(rows[4][ListTable::ctAction] == "Reinvest"); QVERIFY(rows[6][ListTable::ctAction] == "Dividend"); QVERIFY(rows[8][ListTable::ctAction] == "Yield"); #endif #if 1 // i think this is the correct amount. different treatment of dividend and yield QVERIFY(MyMoneyMoney(rows[17][ListTable::ctValue]) == MyMoneyMoney(-153700.00)); QVERIFY(MyMoneyMoney(rows[29][ListTable::ctValue]) == MyMoneyMoney(24600.00)); QVERIFY(MyMoneyMoney(rows[31][ListTable::ctValue]) == MyMoneyMoney(-129100.00)); #else QVERIFY(searchHTML(html, i18n("Total Stock 1")) == MyMoneyMoney(171700.00)); QVERIFY(searchHTML(html, i18n("Grand Total")) == MyMoneyMoney(171700.00)); #endif // // Investment Performance Report // MyMoneyReport invhold_r( - MyMoneyReport::eAccountByTopAccount, - MyMoneyReport::eQCperformance, + eMyMoney::Report::RowType::AccountByTopAccount, + eMyMoney::Report::QueryColumn::Performance, eMyMoney::TransactionFilter::Date::UserDefined, - MyMoneyReport::eDetailAll, + eMyMoney::Report::DetailLevel::All, i18n("Investment Performance by Account"), i18n("Test Report") ); invhold_r.setDateFilter(QDate(2004, 1, 1), QDate(2004, 10, 1)); invhold_r.setInvestmentsOnly(true); XMLandback(invhold_r); QueryTable invhold(invhold_r); writeTabletoHTML(invhold, "Investment Performance by Account.html"); rows = invhold.rows(); QVERIFY(rows.count() == 5); QVERIFY(MyMoneyMoney(rows[0][ListTable::ctReturn]) == MyMoneyMoney("669/10000")); QVERIFY(MyMoneyMoney(rows[0][ListTable::ctReturnInvestment]) == MyMoneyMoney("-39/5000")); QVERIFY(MyMoneyMoney(rows[0][ListTable::ctBuys]) == MyMoneyMoney(-210000.00)); QVERIFY(MyMoneyMoney(rows[0][ListTable::ctSells]) == MyMoneyMoney(44000.00)); QVERIFY(MyMoneyMoney(rows[0][ListTable::ctReinvestIncome]) == MyMoneyMoney(9000.00)); QVERIFY(MyMoneyMoney(rows[0][ListTable::ctCashIncome]) == MyMoneyMoney(3300.00)); QVERIFY(MyMoneyMoney(rows[1][ListTable::ctReturn]) == MyMoneyMoney("1349/10000")); QVERIFY(MyMoneyMoney(rows[1][ListTable::ctReturnInvestment]) == MyMoneyMoney("1/10")); QVERIFY(MyMoneyMoney(rows[1][ListTable::ctStartingBalance]) == MyMoneyMoney(100000.00)); // this should stay non-zero to check if investment performance is calculated at non-zero starting balance QVERIFY(MyMoneyMoney(rows[2][ListTable::ctReturn]) == MyMoneyMoney("2501/2500")); QVERIFY(MyMoneyMoney(rows[2][ListTable::ctReturnInvestment]) == MyMoneyMoney("323/1250")); QVERIFY(MyMoneyMoney(rows[2][ListTable::ctBuys]) == MyMoneyMoney(-95200.00)); QVERIFY(MyMoneyMoney(rows[2][ListTable::ctSells]) == MyMoneyMoney(119800.00)); QVERIFY(MyMoneyMoney(rows[2][ListTable::ctEndingBalance]) == MyMoneyMoney(0.00)); // this should stay zero to check if investment performance is calculated at zero ending balance QVERIFY(MyMoneyMoney(rows[4][ListTable::ctEndingBalance]) == MyMoneyMoney(280000.00)); #if 0 // Dump file & reports QFile g("investmentkmy.xml"); g.open(QIODevice::WriteOnly); MyMoneyStorageXML xml; IMyMoneyOperationsFormat& interface = xml; interface.writeFile(&g, dynamic_cast(MyMoneyFile::instance()->storage())); g.close(); invtran.dump("invtran.html", "%1"); invhold.dump("invhold.html", "%1"); #endif } catch (const MyMoneyException &e) { QFAIL(e.what()); } } // prevents bug #312135 void QueryTableTest::testSplitShares() { try { MyMoneyMoney firstSharesPurchase(16); MyMoneyMoney splitFactor(2); MyMoneyMoney secondSharesPurchase(1); MyMoneyMoney sharesAtTheEnd = firstSharesPurchase / splitFactor + secondSharesPurchase; MyMoneyMoney priceBeforeSplit(74.99, 100); MyMoneyMoney priceAfterSplit = splitFactor * priceBeforeSplit; // Equities eqStock1 = makeEquity("Stock1", "STK1"); // Accounts acInvestment = makeAccount("Investment", eMyMoney::Account::Type::Investment, moZero, QDate(2017, 8, 1), acAsset); acStock1 = makeAccount("Stock 1", eMyMoney::Account::Type::Stock, moZero, QDate(2017, 8, 1), acInvestment, eqStock1); // Transactions // Date Action Shares Price Stock Asset Income InvTransactionHelper s1b1(QDate(2017, 8, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::BuyShares), firstSharesPurchase, priceBeforeSplit, acStock1, acChecking, QString()); InvTransactionHelper s1s1(QDate(2017, 8, 2), MyMoneySplit::actionName(eMyMoney::Split::Action::SplitShares), splitFactor, MyMoneyMoney(), acStock1, QString(), QString()); InvTransactionHelper s1b2(QDate(2017, 8, 3), MyMoneySplit::actionName(eMyMoney::Split::Action::BuyShares), secondSharesPurchase, priceAfterSplit, acStock1, acChecking, QString()); // // Investment Performance Report // MyMoneyReport invhold_r( - MyMoneyReport::eAccountByTopAccount, - MyMoneyReport::eQCperformance, + eMyMoney::Report::RowType::AccountByTopAccount, + eMyMoney::Report::QueryColumn::Performance, eMyMoney::TransactionFilter::Date::UserDefined, - MyMoneyReport::eDetailAll, + eMyMoney::Report::DetailLevel::All, i18n("Investment Performance by Account"), i18n("Test Report") ); invhold_r.setDateFilter(QDate(2017, 8, 1), QDate(2017, 8, 3)); invhold_r.setInvestmentsOnly(true); XMLandback(invhold_r); QueryTable invhold(invhold_r); writeTabletoHTML(invhold, "Investment Performance by Account (with stock split).html"); const auto rows = invhold.rows(); QVERIFY(rows.count() == 3); QVERIFY(MyMoneyMoney(rows[0][ListTable::ctBuys]) == sharesAtTheEnd * priceAfterSplit * MyMoneyMoney(-1)); } catch (const MyMoneyException &e) { QFAIL(e.what()); } } // prevents bug #118159 void QueryTableTest::testConversionRate() { try { MyMoneyMoney firsConversionRate(1.1800, 10000); MyMoneyMoney secondConversionRate(1.1567, 10000); MyMoneyMoney amountWithdrawn(100); const auto acCadChecking = makeAccount(QString("Canadian Checking"), eMyMoney::Account::Type::Checkings, moZero, QDate(2017, 8, 1), acAsset, "CAD"); makePrice("CAD", QDate(2017, 8, 1), firsConversionRate); makePrice("CAD", QDate(2017, 8, 2), secondConversionRate); TransactionHelper t1(QDate(2017, 8, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), amountWithdrawn, acCadChecking, acSolo, "CAD"); MyMoneyReport filter; - filter.setRowType(MyMoneyReport::eAccount); + filter.setRowType(eMyMoney::Report::RowType::Account); filter.setDateFilter(QDate(2017, 8, 1), QDate(2017, 8, 2)); filter.setName("Transactions by Account"); - auto cols = MyMoneyReport::eQCnumber | MyMoneyReport::eQCpayee | MyMoneyReport::eQCcategory | MyMoneyReport::eQCbalance; - filter.setQueryColumns(static_cast(cols)); // + auto cols = eMyMoney::Report::QueryColumn::Number | eMyMoney::Report::QueryColumn::Payee | eMyMoney::Report::QueryColumn::Category | eMyMoney::Report::QueryColumn::Balance; + filter.setQueryColumns(static_cast(cols)); // XMLandback(filter); QueryTable qtbl(filter); writeTabletoHTML(qtbl, "Transactions by Account (conversion rate).html"); const auto rows = qtbl.rows(); QVERIFY(rows.count() == 5); QVERIFY(MyMoneyMoney(rows[1][ListTable::ctValue]) == amountWithdrawn * firsConversionRate * MyMoneyMoney(-1)); QVERIFY(MyMoneyMoney(rows[1][ListTable::ctPrice]) == firsConversionRate); QVERIFY(MyMoneyMoney(rows[2][ListTable::ctBalance]) == amountWithdrawn * secondConversionRate * MyMoneyMoney(-1)); QVERIFY(MyMoneyMoney(rows[2][ListTable::ctPrice]) == secondConversionRate); } catch (const MyMoneyException &e) { QFAIL(e.what()); } } //this is to prevent me from making mistakes again when modifying balances - asoliverez //this case tests only the opening and ending balance of the accounts void QueryTableTest::testBalanceColumn() { try { TransactionHelper t1q1(QDate(2004, 1, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moSolo, acChecking, acSolo); TransactionHelper t2q1(QDate(2004, 2, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent1, acCredit, acParent); TransactionHelper t3q1(QDate(2004, 3, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent2, acCredit, acParent); TransactionHelper t4y1(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moChild, acCredit, acChild); TransactionHelper t1q2(QDate(2004, 4, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moSolo, acChecking, acSolo); TransactionHelper t2q2(QDate(2004, 5, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent1, acCredit, acParent); TransactionHelper t3q2(QDate(2004, 6, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent2, acCredit, acParent); TransactionHelper t4q2(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moChild, acCredit, acChild); TransactionHelper t1y2(QDate(2005, 1, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moSolo, acChecking, acSolo); TransactionHelper t2y2(QDate(2005, 5, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent1, acCredit, acParent); TransactionHelper t3y2(QDate(2005, 9, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent2, acCredit, acParent); TransactionHelper t4y2(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moChild, acCredit, acChild); unsigned cols; MyMoneyReport filter; - filter.setRowType(MyMoneyReport::eAccount); + filter.setRowType(eMyMoney::Report::RowType::Account); filter.setName("Transactions by Account"); - cols = MyMoneyReport::eQCnumber | MyMoneyReport::eQCpayee | MyMoneyReport::eQCcategory | MyMoneyReport::eQCbalance; - filter.setQueryColumns(static_cast(cols)); // + cols = eMyMoney::Report::QueryColumn::Number | eMyMoney::Report::QueryColumn::Payee | eMyMoney::Report::QueryColumn::Category | eMyMoney::Report::QueryColumn::Balance; + filter.setQueryColumns(static_cast(cols)); // XMLandback(filter); QueryTable qtbl_3(filter); writeTabletoHTML(qtbl_3, "Transactions by Account.html"); QString html = qtbl_3.renderHTML(); QList rows = qtbl_3.rows(); QVERIFY(rows.count() == 19); //this is to make sure that the dates of closing and opening balances and the balance numbers are ok QString openingDate = QLocale().toString(QDate(2004, 1, 1), QLocale::ShortFormat); QString closingDate = QLocale().toString(QDate(2005, 9, 1), QLocale::ShortFormat); QVERIFY(html.indexOf(openingDate + "" + i18n("Opening Balance")) > 0); QVERIFY(html.indexOf(closingDate + "" + i18n("Closing Balance") + " -702.36") > 0); QVERIFY(html.indexOf(closingDate + "" + i18n("Closing Balance") + " -705.69") > 0); } catch (const MyMoneyException &e) { QFAIL(e.what()); } } void QueryTableTest::testBalanceColumnWithMultipleCurrencies() { try { MyMoneyMoney moJpyOpening(0.0, 1); MyMoneyMoney moJpyPrice(0.010, 100); MyMoneyMoney moJpyPrice2(0.011, 100); MyMoneyMoney moJpyPrice3(0.024, 100); MyMoneyMoney moTransaction(100, 1); MyMoneyMoney moJpyTransaction(100, 1); QString acJpyChecking = makeAccount(QString("Japanese Checking"), eMyMoney::Account::Type::Checkings, moJpyOpening, QDate(2003, 11, 15), acAsset, "JPY"); makePrice("JPY", QDate(2004, 1, 1), MyMoneyMoney(moJpyPrice)); makePrice("JPY", QDate(2004, 5, 1), MyMoneyMoney(moJpyPrice2)); makePrice("JPY", QDate(2004, 6, 30), MyMoneyMoney(moJpyPrice3)); QDate openingDate(2004, 2, 20); QDate intermediateDate(2004, 5, 20); QDate closingDate(2004, 7, 20); TransactionHelper t1(openingDate, MyMoneySplit::actionName(eMyMoney::Split::Action::Transfer), MyMoneyMoney(moJpyTransaction), acJpyChecking, acChecking, "JPY"); TransactionHelper t4(openingDate, MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit), MyMoneyMoney(moTransaction), acCredit, acChecking); TransactionHelper t2(intermediateDate, MyMoneySplit::actionName(eMyMoney::Split::Action::Transfer), MyMoneyMoney(moJpyTransaction), acJpyChecking, acChecking, "JPY"); TransactionHelper t5(intermediateDate, MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit), MyMoneyMoney(moTransaction), acCredit, acChecking); TransactionHelper t3(closingDate, MyMoneySplit::actionName(eMyMoney::Split::Action::Transfer), MyMoneyMoney(moJpyTransaction), acJpyChecking, acChecking, "JPY"); TransactionHelper t6(closingDate, MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit), MyMoneyMoney(moTransaction), acCredit, acChecking); // test that an income/expense transaction that involves a currency exchange is properly reported TransactionHelper t7(intermediateDate, MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), MyMoneyMoney(moJpyTransaction), acJpyChecking, acSolo, "JPY"); unsigned cols; MyMoneyReport filter; - filter.setRowType(MyMoneyReport::eAccount); + filter.setRowType(eMyMoney::Report::RowType::Account); filter.setName("Transactions by Account"); - cols = MyMoneyReport::eQCnumber | MyMoneyReport::eQCpayee | MyMoneyReport::eQCcategory | MyMoneyReport::eQCbalance; - filter.setQueryColumns(static_cast(cols)); + cols = eMyMoney::Report::QueryColumn::Number | eMyMoney::Report::QueryColumn::Payee | eMyMoney::Report::QueryColumn::Category | eMyMoney::Report::QueryColumn::Balance; + filter.setQueryColumns(static_cast(cols)); // don't convert values to the default currency filter.setConvertCurrency(false); XMLandback(filter); QueryTable qtbl_3(filter); writeTabletoHTML(qtbl_3, "Transactions by Account (multiple currencies).html"); QString html = qtbl_3.renderHTML(); QList rows = qtbl_3.rows(); QVERIFY(rows.count() == 24); //this is to make sure that the dates of closing and opening balances and the balance numbers are ok QString openingDateString = QLocale().toString(openingDate, QLocale::ShortFormat); QString intermediateDateString = QLocale().toString(intermediateDate, QLocale::ShortFormat); QString closingDateString = QLocale().toString(closingDate, QLocale::ShortFormat); // check the opening and closing balances QVERIFY(html.indexOf(openingDateString + "" + i18n("Opening Balance") + "USD 0.00") > 0); QVERIFY(html.indexOf(closingDateString + "" + i18n("Closing Balance") + "USD 304.00") > 0); QVERIFY(html.indexOf(closingDateString + "" + i18n("Closing Balance") + "USD -300.00") > 0); QVERIFY(html.indexOf(closingDateString + "" + i18n("Closing Balance") + "JPY -400.00") > 0); // after a transfer of 100 JPY the balance should be 1.00 - price is 0.010 (precision of 2) QVERIFY(html.indexOf("" + openingDateString + "Test PayeeTransfer from Japanese CheckingUSD 1.00USD 1.00") > 0); // after a transfer of 100 the balance should be 101.00 QVERIFY(html.indexOf("" + openingDateString + "Test PayeeTransfer from Credit CardUSD 100.00USD 101.00") > 0); // after a transfer of 100 JPY the balance should be 102.00 - price is 0.011 (precision of 2) QVERIFY(html.indexOf("" + intermediateDateString + "Test PayeeTransfer from Japanese CheckingUSD 1.00USD 102.00") > 0); // after a transfer of 100 the balance should be 202.00 QVERIFY(html.indexOf("" + intermediateDateString + "Test PayeeTransfer from Credit CardUSD 100.00USD 202.00") > 0); // after a transfer of 100 JPY the balance should be 204.00 - price is 0.024 (precision of 2) QVERIFY(html.indexOf("" + closingDateString + "Test PayeeTransfer from Japanese CheckingUSD 2.00USD 204.00") > 0); // after a transfer of 100 the balance should be 304.00 QVERIFY(html.indexOf("" + closingDateString + "Test PayeeTransfer from Credit CardUSD 100.00USD 304.00") > 0); // a 100.00 JPY withdrawal should be displayed as such even if the expense account uses another currency QVERIFY(html.indexOf("" + intermediateDateString + "Test PayeeSoloJPY -100.00JPY -300.00") > 0); // now run the same report again but this time convert all values to the base currency and make sure the values are correct filter.setConvertCurrency(true); XMLandback(filter); QueryTable qtbl_4(filter); writeTabletoHTML(qtbl_4, "Transactions by Account (multiple currencies converted to base).html"); html = qtbl_4.renderHTML(); rows = qtbl_4.rows(); QVERIFY(rows.count() == 23); // check the opening and closing balances QVERIFY(html.indexOf(openingDateString + "" + i18n("Opening Balance") + " 0.00") > 0); QVERIFY(html.indexOf(closingDateString + "" + i18n("Closing Balance") + " 304.00") > 0); QVERIFY(html.indexOf(closingDateString + "" + i18n("Closing Balance") + " -300.00") > 0); // although the balance should be -5.00 it's -8.00 because the foreign currency balance is converted using the closing date price (0.024) QVERIFY(html.indexOf(closingDateString + "" + i18n("Closing Balance") + " -8.00") > 0); // a 100.00 JPY transfer should be displayed as -1.00 when converted to the base currency using the opening date price QVERIFY(html.indexOf("" + openingDateString + "Test PayeeTransfer to Checking Account -1.00 -1.00") > 0); // a 100.00 JPY transfer should be displayed as -1.00 when converted to the base currency using the intermediate date price QVERIFY(html.indexOf("" + intermediateDateString + "Test PayeeTransfer to Checking Account -1.00 -2.00") > 0); // a 100.00 JPY transfer should be displayed as -2.00 when converted to the base currency using the closing date price (notice the balance is -5.00) QVERIFY(html.indexOf("" + closingDateString + "Test PayeeTransfer to Checking Account -2.00 -5.00") > 0); // a 100.00 JPY withdrawal should be displayed as -1.00 when converted to the base currency using the intermediate date price QVERIFY(html.indexOf("" + intermediateDateString + "Test PayeeSolo -1.00 -3.00") > 0); } catch (const MyMoneyException &e) { QFAIL(e.what()); } } void QueryTableTest::testTaxReport() { try { TransactionHelper t1q1(QDate(2004, 1, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moSolo, acChecking, acSolo); TransactionHelper t2q1(QDate(2004, 2, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent1, acChecking, acTax); unsigned cols; MyMoneyReport filter; - filter.setRowType(MyMoneyReport::eCategory); + filter.setRowType(eMyMoney::Report::RowType::Category); filter.setName("Tax Transactions"); - cols = MyMoneyReport::eQCnumber | MyMoneyReport::eQCpayee | MyMoneyReport::eQCaccount; - filter.setQueryColumns(static_cast(cols)); + cols = eMyMoney::Report::QueryColumn::Number | eMyMoney::Report::QueryColumn::Payee | eMyMoney::Report::QueryColumn::Account; + filter.setQueryColumns(static_cast(cols)); filter.setTax(true); XMLandback(filter); QueryTable qtbl_3(filter); writeTabletoHTML(qtbl_3, "Tax Transactions.html"); QList rows = qtbl_3.rows(); QString html = qtbl_3.renderHTML(); QVERIFY(rows.count() == 5); } catch (const MyMoneyException &e) { QFAIL(e.what()); } } diff --git a/kmymoney/views/khomeview_p.h b/kmymoney/views/khomeview_p.h index e2909f299..a8606353d 100644 --- a/kmymoney/views/khomeview_p.h +++ b/kmymoney/views/khomeview_p.h @@ -1,2001 +1,2001 @@ /*************************************************************************** khomeview_p.h - description ------------------- begin : Tue Jan 22 2002 copyright : (C) 2000-2002 by Michael Edwardes Javier Campos Morales Felix Rodriguez John C Thomas Baumgart Kevin Tambascio (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. * * * ***************************************************************************/ #ifndef KHOMEVIEW_P_H #define KHOMEVIEW_P_H #include "khomeview.h" #include // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include #include #include #include #include #ifdef ENABLE_WEBENGINE #include #else #include #include #endif // ---------------------------------------------------------------------------- // KDE Includes #include #include #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "kmymoneyviewbase_p.h" #include "mymoneyutils.h" #include "kmymoneyutils.h" #include "kwelcomepage.h" #include "kmymoneysettings.h" #include "mymoneyfile.h" #include "mymoneyaccount.h" #include "mymoneyprice.h" #include "mymoneyforecast.h" #include "kreportchartview.h" #include "pivottable.h" #include "pivotgrid.h" #include "reportaccount.h" #include "mymoneysplit.h" #include "mymoneytransaction.h" #include "icons.h" #include "kmymoneywebpage.h" #include "mymoneyschedule.h" #include "mymoneysecurity.h" #include "mymoneyexception.h" #include "mymoneyenums.h" #include "menuenums.h" #define VIEW_LEDGER "ledger" #define VIEW_SCHEDULE "schedule" #define VIEW_WELCOME "welcome" #define VIEW_HOME "home" #define VIEW_REPORTS "reports" using namespace Icons; using namespace eMyMoney; /** * @brief Converts a QPixmap to an data URI scheme * * According to RFC 2397 * * @param pixmap Source to convert * @return full data URI */ QString QPixmapToDataUri(const QPixmap& pixmap) { QImage image(pixmap.toImage()); QByteArray byteArray; QBuffer buffer(&byteArray); buffer.open(QIODevice::WriteOnly); image.save(&buffer, "PNG"); // writes the image in PNG format inside the buffer return QLatin1String("data:image/png;base64,") + QString(byteArray.toBase64()); } bool accountNameLess(const MyMoneyAccount &acc1, const MyMoneyAccount &acc2) { return acc1.name().localeAwareCompare(acc2.name()) < 0; } using namespace reports; class KHomeViewPrivate : public KMyMoneyViewBasePrivate { Q_DECLARE_PUBLIC(KHomeView) public: explicit KHomeViewPrivate(KHomeView *qq) : KMyMoneyViewBasePrivate(), q_ptr(qq), m_view(nullptr), m_showAllSchedules(false), m_needLoad(true), m_netWorthGraphLastValidSize(400, 300), m_currentPrinter(nullptr) { } ~KHomeViewPrivate() { // if user wants to remember the font size, store it here if (KMyMoneySettings::rememberZoomFactor() && m_view) { KMyMoneySettings::setZoomFactor(m_view->zoomFactor()); KMyMoneySettings::self()->save(); } } /** * Definition of bitmap used as argument for showAccounts(). */ enum paymentTypeE { Preferred = 1, ///< show preferred accounts Payment = 2 ///< show payment accounts }; void init() { Q_Q(KHomeView); m_needLoad = false; auto vbox = new QVBoxLayout(q); q->setLayout(vbox); vbox->setSpacing(6); vbox->setMargin(0); #ifdef ENABLE_WEBENGINE m_view = new QWebEngineView(q); #else m_view = new KWebView(q); #endif m_view->setPage(new MyQWebEnginePage(m_view)); vbox->addWidget(m_view); m_view->setHtml(KWelcomePage::welcomePage(), QUrl("file://")); #ifdef ENABLE_WEBENGINE q->connect(m_view->page(), &QWebEnginePage::urlChanged, q, &KHomeView::slotOpenUrl); #else m_view->page()->setLinkDelegationPolicy(QWebPage::DelegateAllLinks); q->connect(m_view->page(), &KWebPage::linkClicked, q, &KHomeView::slotOpenUrl); #endif q->connect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, q, &KHomeView::refresh); } /** * Print an account and its balance and limit */ void showAccountEntry(const MyMoneyAccount& acc, const MyMoneyMoney& value, const MyMoneyMoney& valueToMinBal, const bool showMinBal) { MyMoneyFile* file = MyMoneyFile::instance(); QString tmp; MyMoneySecurity currency = file->currency(acc.currencyId()); QString amount; QString amountToMinBal; //format amounts amount = MyMoneyUtils::formatMoney(value, acc, currency); amount.replace(QChar(' '), " "); if (showMinBal) { amountToMinBal = MyMoneyUtils::formatMoney(valueToMinBal, acc, currency); amountToMinBal.replace(QChar(' '), " "); } QString cellStatus, cellCounts, pathOK, pathTODO, pathNotOK; if (KMyMoneySettings::showBalanceStatusOfOnlineAccounts()) { //show account's online-status pathOK = QPixmapToDataUri(Icons::get(Icon::DialogOKApply).pixmap(QSize(16,16))); pathTODO = QPixmapToDataUri(Icons::get(Icon::MailReceive).pixmap(QSize(16,16))); pathNotOK = QPixmapToDataUri(Icons::get(Icon::DialogCancel).pixmap(QSize(16,16))); if (acc.value("lastImportedTransactionDate").isEmpty() || acc.value("lastStatementBalance").isEmpty()) cellStatus = '-'; else if (file->hasMatchingOnlineBalance(acc)) { if (file->hasNewerTransaction(acc.id(), QDate::fromString(acc.value("lastImportedTransactionDate"), Qt::ISODate))) cellStatus = QString("").arg(pathTODO); else cellStatus = QString("").arg(pathOK); } else cellStatus = QString("").arg(pathNotOK); tmp = QString("%1").arg(cellStatus); } tmp += QString("") + link(VIEW_LEDGER, QString("?id=%1").arg(acc.id())) + acc.name() + linkend() + ""; int countNotMarked = 0, countCleared = 0, countNotReconciled = 0; QString countStr; if (KMyMoneySettings::showCountOfUnmarkedTransactions() || KMyMoneySettings::showCountOfNotReconciledTransactions()) countNotMarked = file->countTransactionsWithSpecificReconciliationState(acc.id(), TransactionFilter::State::NotReconciled); if (KMyMoneySettings::showCountOfClearedTransactions() || KMyMoneySettings::showCountOfNotReconciledTransactions()) countCleared = file->countTransactionsWithSpecificReconciliationState(acc.id(), TransactionFilter::State::Cleared); if (KMyMoneySettings::showCountOfNotReconciledTransactions()) countNotReconciled = countNotMarked + countCleared; if (KMyMoneySettings::showCountOfUnmarkedTransactions()) { if (countNotMarked) countStr = QString("%1").arg(countNotMarked); else countStr = '-'; tmp += QString("%1").arg(countStr); } if (KMyMoneySettings::showCountOfClearedTransactions()) { if (countCleared) countStr = QString("%1").arg(countCleared); else countStr = '-'; tmp += QString("%1").arg(countStr); } if (KMyMoneySettings::showCountOfNotReconciledTransactions()) { if (countNotReconciled) countStr = QString("%1").arg(countNotReconciled); else countStr = '-'; tmp += QString("%1").arg(countStr); } if (KMyMoneySettings::showDateOfLastReconciliation()) { const auto lastReconciliationDate = acc.lastReconciliationDate().toString(Qt::SystemLocaleShortDate).replace(QChar(' '), " "); tmp += QString("%1").arg(lastReconciliationDate); } //show account balance tmp += QString("%1").arg(showColoredAmount(amount, value.isNegative())); //show minimum balance column if requested if (showMinBal) { //if it is an investment, show minimum balance empty if (acc.accountType() == Account::Type::Investment) { tmp += QString(" "); } else { //show minimum balance entry tmp += QString("%1").arg(showColoredAmount(amountToMinBal, valueToMinBal.isNegative())); } } // qDebug("accountEntry = '%s'", tmp.toLatin1()); m_html += tmp; } void showAccountEntry(const MyMoneyAccount& acc) { MyMoneyFile* file = MyMoneyFile::instance(); MyMoneySecurity currency = file->currency(acc.currencyId()); MyMoneyMoney value; bool showLimit = KMyMoneySettings::showLimitInfo(); if (acc.accountType() == Account::Type::Investment) { //investment accounts show the balances of all its subaccounts value = investmentBalance(acc); //investment accounts have no minimum balance showAccountEntry(acc, value, MyMoneyMoney(), showLimit); } else { //get balance for normal accounts value = file->balance(acc.id(), QDate::currentDate()); if (acc.currencyId() != file->baseCurrency().id()) { ReportAccount repAcc = ReportAccount(acc.id()); MyMoneyMoney curPrice = repAcc.baseCurrencyPrice(QDate::currentDate()); MyMoneyMoney baseValue = value * curPrice; baseValue = baseValue.convert(file->baseCurrency().smallestAccountFraction()); m_total += baseValue; } else { m_total += value; } //if credit card or checkings account, show maximum credit if (acc.accountType() == Account::Type::CreditCard || acc.accountType() == Account::Type::Checkings) { QString maximumCredit = acc.value("maxCreditAbsolute"); if (maximumCredit.isEmpty()) { maximumCredit = acc.value("minBalanceAbsolute"); } MyMoneyMoney maxCredit = MyMoneyMoney(maximumCredit); showAccountEntry(acc, value, value - maxCredit, showLimit); } else { //otherwise use minimum balance QString minimumBalance = acc.value("minBalanceAbsolute"); MyMoneyMoney minBalance = MyMoneyMoney(minimumBalance); showAccountEntry(acc, value, value - minBalance, showLimit); } } } /** * @param acc the investment account * @return the balance in the currency of the investment account */ MyMoneyMoney investmentBalance(const MyMoneyAccount& acc) { auto file = MyMoneyFile::instance(); auto value = file->balance(acc.id(), QDate::currentDate()); foreach (const auto accountID, acc.accountList()) { auto stock = file->account(accountID); if (!stock.isClosed()) { try { MyMoneyMoney val; MyMoneyMoney balance = file->balance(stock.id(), QDate::currentDate()); MyMoneySecurity security = file->security(stock.currencyId()); const MyMoneyPrice &price = file->price(stock.currencyId(), security.tradingCurrency()); val = (balance * price.rate(security.tradingCurrency())).convertPrecision(security.pricePrecision()); // adjust value of security to the currency of the account MyMoneySecurity accountCurrency = file->currency(acc.currencyId()); val = val * file->price(security.tradingCurrency(), accountCurrency.id()).rate(accountCurrency.id()); val = val.convert(acc.fraction()); value += val; } catch (const MyMoneyException &e) { qWarning("%s", qPrintable(QString("cannot convert stock balance of %1 to base currency: %2").arg(stock.name(), e.what()))); } } } return value; } /** * Print text in the color set for negative numbers, if @p amount is negative * abd @p isNegative is true */ QString showColoredAmount(const QString& amount, bool isNegative) { if (isNegative) { //if negative, get the settings for negative numbers return QString("%2").arg(KMyMoneySettings::schemeColor(SchemeColor::Negative).name(), amount); } //if positive, return the same string return amount; } /** * Run the forecast */ void doForecast() { //clear m_accountList because forecast is about to changed m_accountList.clear(); //reinitialize the object m_forecast = KMyMoneyUtils::forecast(); //If forecastDays lower than accountsCycle, adjust to the first cycle if (m_forecast.accountsCycle() > m_forecast.forecastDays()) m_forecast.setForecastDays(m_forecast.accountsCycle()); //Get all accounts of the right type to calculate forecast m_forecast.doForecast(); } /** * Calculate the forecast balance after a payment has been made */ MyMoneyMoney forecastPaymentBalance(const MyMoneyAccount& acc, const MyMoneyMoney& payment, QDate& paymentDate) { //if paymentDate before or equal to currentDate set it to current date plus 1 //so we get to accumulate forecast balance correctly if (paymentDate <= QDate::currentDate()) paymentDate = QDate::currentDate().addDays(1); //check if the account is already there if (m_accountList.find(acc.id()) == m_accountList.end() || m_accountList[acc.id()].find(paymentDate) == m_accountList[acc.id()].end()) { if (paymentDate == QDate::currentDate()) { m_accountList[acc.id()][paymentDate] = m_forecast.forecastBalance(acc, paymentDate); } else { m_accountList[acc.id()][paymentDate] = m_forecast.forecastBalance(acc, paymentDate.addDays(-1)); } } m_accountList[acc.id()][paymentDate] = m_accountList[acc.id()][paymentDate] + payment; return m_accountList[acc.id()][paymentDate]; } void loadView() { m_view->setZoomFactor(KMyMoneySettings::zoomFactor()); QList list; if (MyMoneyFile::instance()->storage()) MyMoneyFile::instance()->accountList(list); if (list.isEmpty()) { m_view->setHtml(KWelcomePage::welcomePage(), QUrl("file://")); } else { // keep current location on page int scrollBarPos = 0; #ifdef ENABLE_WEBENGINE /// @todo cannot test this #else scrollBarPos = m_view->page()->mainFrame()->scrollBarValue(Qt::Vertical); #endif //clear the forecast flag so it will be reloaded m_forecast.setForecastDone(false); const QString filename = QStandardPaths::locate(QStandardPaths::AppConfigLocation, "html/kmymoney.css"); QString header = QString("\n\n").arg(QUrl::fromLocalFile(filename).url()); header += KMyMoneyUtils::variableCSS(); header += "\n"; QString footer = "\n"; m_html.clear(); m_html += header; m_html += QString("
%1
").arg(i18n("Your Financial Summary")); QStringList settings = KMyMoneySettings::listOfItems(); QStringList::ConstIterator it; for (it = settings.constBegin(); it != settings.constEnd(); ++it) { int option = (*it).toInt(); if (option > 0) { switch (option) { case 1: // payments showPayments(); break; case 2: // preferred accounts showAccounts(Preferred, i18n("Preferred Accounts")); break; case 3: // payment accounts // Check if preferred accounts are shown separately if (settings.contains("2")) { showAccounts(static_cast(Payment | Preferred), i18n("Payment Accounts")); } else { showAccounts(Payment, i18n("Payment Accounts")); } break; case 4: // favorite reports showFavoriteReports(); break; case 5: // forecast showForecast(); break; case 6: // net worth graph over all accounts showNetWorthGraph(); break; case 8: // assets and liabilities showAssetsLiabilities(); break; case 9: // budget showBudget(); break; case 10: // cash flow summary showCashFlowSummary(); break; } m_html += "
 
\n"; } } m_html += "
"; m_html += link(VIEW_WELCOME, QString()) + i18n("Show KMyMoney welcome page") + linkend(); m_html += "
"; m_html += "
"; m_html += footer; m_view->setHtml(m_html, QUrl("file://")); if (scrollBarPos) { #ifdef ENABLE_WEBENGINE /// @todo cannot test this #else m_view->page()->mainFrame()->setScrollBarValue(Qt::Vertical, scrollBarPos); #endif } } } void showNetWorthGraph() { Q_Q(KHomeView); m_html += QString("
%1
\n
 
\n").arg(i18n("Net Worth Forecast")); MyMoneyReport reportCfg = MyMoneyReport( - MyMoneyReport::eAssetLiability, - MyMoneyReport::eMonths, + eMyMoney::Report::RowType::AssetLiability, + static_cast(eMyMoney::Report::ColumnType::Months), TransactionFilter::Date::UserDefined, // overridden by the setDateFilter() call below - MyMoneyReport::eDetailTotal, + eMyMoney::Report::DetailLevel::Total, i18n("Net Worth Forecast"), i18n("Generated Report")); reportCfg.setChartByDefault(true); reportCfg.setChartCHGridLines(false); reportCfg.setChartSVGridLines(false); reportCfg.setChartDataLabels(false); - reportCfg.setChartType(MyMoneyReport::eChartLine); + reportCfg.setChartType(eMyMoney::Report::ChartType::Line); reportCfg.setIncludingSchedules(false); reportCfg.addAccountGroup(Account::Type::Asset); reportCfg.addAccountGroup(Account::Type::Liability); reportCfg.setColumnsAreDays(true); reportCfg.setConvertCurrency(true); reportCfg.setIncludingForecast(true); reportCfg.setDateFilter(QDate::currentDate(), QDate::currentDate().addDays(+ 90)); reports::PivotTable table(reportCfg); reports::KReportChartView* chartWidget = new reports::KReportChartView(0); table.drawChart(*chartWidget); // Adjust the size QSize netWorthGraphSize = q->size(); netWorthGraphSize -= QSize(80, 30); // consider the computed size valid only if it's smaller on both axes that the applications size // if (netWorthGraphSize.width() < kmymoney->width() || netWorthGraphSize.height() < kmymoney->height()) { m_netWorthGraphLastValidSize = netWorthGraphSize; // } chartWidget->resize(m_netWorthGraphLastValidSize); //save the chart to an image QString chart = QPixmapToDataUri(chartWidget->coordinatePlane()->parent()->grab()); m_html += QString(""); m_html += QString(""); m_html += QString("").arg(chart); m_html += QString(""); m_html += QString("
\"Networth\"
"); //delete the widget since we no longer need it delete chartWidget; } void showPayments() { MyMoneyFile* file = MyMoneyFile::instance(); QList overdues; QList schedule; int i = 0; //if forecast has not been executed yet, do it. if (!m_forecast.isForecastDone()) doForecast(); schedule = file->scheduleList(QString(), Schedule::Type::Any, Schedule::Occurrence::Any, Schedule::PaymentType::Any, QDate::currentDate(), QDate::currentDate().addMonths(1), false); overdues = file->scheduleList(QString(), Schedule::Type::Any, Schedule::Occurrence::Any, Schedule::PaymentType::Any, QDate(), QDate(), true); if (schedule.empty() && overdues.empty()) return; // HACK // Remove the finished schedules QList::Iterator d_it; //regular schedules d_it = schedule.begin(); while (d_it != schedule.end()) { if ((*d_it).isFinished()) { d_it = schedule.erase(d_it); continue; } ++d_it; } //overdue schedules d_it = overdues.begin(); while (d_it != overdues.end()) { if ((*d_it).isFinished()) { d_it = overdues.erase(d_it); continue; } ++d_it; } m_html += "
"; m_html += QString("
%1
\n").arg(i18n("Payments")); if (!overdues.isEmpty()) { m_html += "
 
\n"; qSort(overdues); QList::Iterator it; QList::Iterator it_f; m_html += ""; m_html += QString("\n").arg(showColoredAmount(i18n("Overdue payments"), true)); m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; for (it = overdues.begin(); it != overdues.end(); ++it) { // determine number of overdue payments int cnt = (*it).transactionsRemainingUntil(QDate::currentDate().addDays(-1)); m_html += QString("").arg(i++ & 0x01 ? "even" : "odd"); showPaymentEntry(*it, cnt); m_html += ""; } m_html += "
%1
"; m_html += i18n("Date"); m_html += ""; m_html += i18n("Schedule"); m_html += ""; m_html += i18n("Account"); m_html += ""; m_html += i18n("Amount"); m_html += ""; m_html += i18n("Balance after"); m_html += "
"; } if (!schedule.isEmpty()) { qSort(schedule); // Extract todays payments if any QList todays; QList::Iterator t_it; for (t_it = schedule.begin(); t_it != schedule.end();) { if ((*t_it).adjustedNextDueDate() == QDate::currentDate()) { todays.append(*t_it); (*t_it).setNextDueDate((*t_it).nextPayment(QDate::currentDate())); // if adjustedNextDueDate is still currentDate then remove it from // scheduled payments if ((*t_it).adjustedNextDueDate() == QDate::currentDate()) { t_it = schedule.erase(t_it); continue; } } ++t_it; } if (todays.count() > 0) { m_html += "
 
\n"; m_html += ""; m_html += QString("\n").arg(i18n("Today's due payments")); m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; for (t_it = todays.begin(); t_it != todays.end(); ++t_it) { m_html += QString("").arg(i++ & 0x01 ? "even" : "odd"); showPaymentEntry(*t_it); m_html += ""; } m_html += "
%1
"; m_html += i18n("Date"); m_html += ""; m_html += i18n("Schedule"); m_html += ""; m_html += i18n("Account"); m_html += ""; m_html += i18n("Amount"); m_html += ""; m_html += i18n("Balance after"); m_html += "
"; } if (!schedule.isEmpty()) { m_html += "
 
\n"; QList::Iterator it; m_html += ""; m_html += QString("\n").arg(i18n("Future payments")); m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; // show all or the first 6 entries int cnt; cnt = (m_showAllSchedules) ? -1 : 6; bool needMoreLess = m_showAllSchedules; QDate lastDate = QDate::currentDate().addMonths(1); qSort(schedule); do { it = schedule.begin(); if (it == schedule.end()) break; // if the next due date is invalid (schedule is finished) // we remove it from the list QDate nextDate = (*it).nextDueDate(); if (!nextDate.isValid()) { schedule.erase(it); continue; } if (nextDate > lastDate) break; if (cnt == 0) { needMoreLess = true; break; } // in case we've shown the current recurrence as overdue, // we don't show it here again, but keep the schedule // as it might show up later in the list again if (!(*it).isOverdue()) { if (cnt > 0) --cnt; m_html += QString("").arg(i++ & 0x01 ? "even" : "odd"); showPaymentEntry(*it); m_html += ""; // for single occurrence we have reported everything so we // better get out of here. if ((*it).occurrence() == Schedule::Occurrence::Once) { schedule.erase(it); continue; } } // if nextPayment returns an invalid date, setNextDueDate will // just skip it, resulting in a loop // we check the resulting date and erase the schedule if invalid if (!((*it).nextPayment((*it).nextDueDate())).isValid()) { schedule.erase(it); continue; } (*it).setNextDueDate((*it).nextPayment((*it).nextDueDate())); qSort(schedule); } while (1); if (needMoreLess) { m_html += QString("").arg(i++ & 0x01 ? "even" : "odd"); m_html += ""; m_html += ""; } m_html += "
%1
"; m_html += i18n("Date"); m_html += ""; m_html += i18n("Schedule"); m_html += ""; m_html += i18n("Account"); m_html += ""; m_html += i18n("Amount"); m_html += ""; m_html += i18n("Balance after"); m_html += "
"; if (m_showAllSchedules) { m_html += link(VIEW_SCHEDULE, QString("?mode=%1").arg("reduced")) + i18nc("Less...", "Show fewer schedules on the list") + linkend(); } else { m_html += link(VIEW_SCHEDULE, QString("?mode=%1").arg("full")) + i18nc("More...", "Show more schedules on the list") + linkend(); } m_html += "
"; } } m_html += "
"; } void showPaymentEntry(const MyMoneySchedule& sched, int cnt = 1) { QString tmp; MyMoneyFile* file = MyMoneyFile::instance(); try { MyMoneyAccount acc = sched.account(); if (!acc.id().isEmpty()) { MyMoneyTransaction t = sched.transaction(); // only show the entry, if it is still active if (!sched.isFinished()) { MyMoneySplit sp = t.splitByAccount(acc.id(), true); QString pathEnter = QPixmapToDataUri(Icons::get(Icon::KeyEnter).pixmap(QSize(16,16))); QString pathSkip = QPixmapToDataUri(Icons::get(Icon::MediaSkipForward).pixmap(QSize(16,16))); //show payment date tmp = QString("") + QLocale().toString(sched.adjustedNextDueDate(), QLocale::ShortFormat) + ""; if (!pathEnter.isEmpty()) tmp += link(VIEW_SCHEDULE, QString("?id=%1&mode=enter").arg(sched.id()), i18n("Enter schedule")) + QString("").arg(pathEnter) + linkend(); if (!pathSkip.isEmpty()) tmp += " " + link(VIEW_SCHEDULE, QString("?id=%1&mode=skip").arg(sched.id()), i18n("Skip schedule")) + QString("").arg(pathSkip) + linkend(); tmp += QString(" "); tmp += link(VIEW_SCHEDULE, QString("?id=%1&mode=edit").arg(sched.id()), i18n("Edit schedule")) + sched.name() + linkend(); //show quantity of payments overdue if any if (cnt > 1) tmp += i18np(" (%1 payment)", " (%1 payments)", cnt); //show account of the main split tmp += ""; tmp += QString(file->account(acc.id()).name()); //show amount of the schedule tmp += ""; const MyMoneySecurity& currency = MyMoneyFile::instance()->currency(acc.currencyId()); MyMoneyMoney payment = MyMoneyMoney(sp.value(t.commodity(), acc.currencyId()) * cnt); QString amount = MyMoneyUtils::formatMoney(payment, acc, currency); amount.replace(QChar(' '), " "); tmp += showColoredAmount(amount, payment.isNegative()); tmp += ""; //show balance after payments tmp += ""; QDate paymentDate = QDate(sched.adjustedNextDueDate()); MyMoneyMoney balanceAfter = forecastPaymentBalance(acc, payment, paymentDate); QString balance = MyMoneyUtils::formatMoney(balanceAfter, acc, currency); balance.replace(QChar(' '), " "); tmp += showColoredAmount(balance, balanceAfter.isNegative()); tmp += ""; // qDebug("paymentEntry = '%s'", tmp.toLatin1()); m_html += tmp; } } } catch (const MyMoneyException &e) { qDebug("Unable to display schedule entry: %s", e.what()); } } void showAccounts(paymentTypeE type, const QString& header) { MyMoneyFile* file = MyMoneyFile::instance(); int prec = MyMoneyMoney::denomToPrec(file->baseCurrency().smallestAccountFraction()); QList accounts; auto showClosedAccounts = KMyMoneySettings::showAllAccounts(); // get list of all accounts file->accountList(accounts); for (QList::Iterator it = accounts.begin(); it != accounts.end();) { bool removeAccount = false; if (!(*it).isClosed() || showClosedAccounts) { switch ((*it).accountType()) { case Account::Type::Expense: case Account::Type::Income: // never show a category account // Note: This might be different in a future version when // the homepage also shows category based information removeAccount = true; break; // Asset and Liability accounts are only shown if they // have the preferred flag set case Account::Type::Asset: case Account::Type::Liability: case Account::Type::Investment: // if preferred accounts are requested, then keep in list if ((*it).value("PreferredAccount") != "Yes" || (type & Preferred) == 0) { removeAccount = true; } break; // Check payment accounts. If payment and preferred is selected, // then always show them. If only payment is selected, then // show only if preferred flag is not set. case Account::Type::Checkings: case Account::Type::Savings: case Account::Type::Cash: case Account::Type::CreditCard: switch (type & (Payment | Preferred)) { case Payment: if ((*it).value("PreferredAccount") == "Yes") removeAccount = true; break; case Preferred: if ((*it).value("PreferredAccount") != "Yes") removeAccount = true; break; case Payment | Preferred: break; default: removeAccount = true; break; } break; // filter all accounts that are not used on homepage views default: removeAccount = true; break; } } else if ((*it).isClosed() || (*it).isInvest()) { // don't show if closed or a stock account removeAccount = true; } if (removeAccount) it = accounts.erase(it); else ++it; } if (!accounts.isEmpty()) { // sort the accounts by name qStableSort(accounts.begin(), accounts.end(), accountNameLess); QString tmp; int i = 0; tmp = "
" + header + "
\n
 
\n"; m_html += tmp; m_html += ""; m_html += ""; if (KMyMoneySettings::showBalanceStatusOfOnlineAccounts()) { QString pathStatusHeader = QPixmapToDataUri(Icons::get(Icon::Download).pixmap(QSize(16,16))); m_html += QString("").arg(pathStatusHeader); } m_html += ""; if (KMyMoneySettings::showCountOfUnmarkedTransactions()) m_html += QString(""); if (KMyMoneySettings::showCountOfClearedTransactions()) m_html += QString(""); if (KMyMoneySettings::showCountOfNotReconciledTransactions()) m_html += QString(""); if (KMyMoneySettings::showDateOfLastReconciliation()) m_html += QString("").arg(i18n("Last Reconciled")); m_html += ""; //only show limit info if user chose to do so if (KMyMoneySettings::showLimitInfo()) { m_html += ""; } m_html += ""; m_total = 0; QList::const_iterator it_m; for (it_m = accounts.constBegin(); it_m != accounts.constEnd(); ++it_m) { m_html += QString("").arg(i++ & 0x01 ? "even" : "odd"); showAccountEntry(*it_m); m_html += ""; } m_html += QString("").arg(i++ & 0x01 ? "even" : "odd"); QString amount = m_total.formatMoney(file->baseCurrency().tradingSymbol(), prec); if (KMyMoneySettings::showBalanceStatusOfOnlineAccounts()) m_html += ""; m_html += QString("").arg(i18n("Total")); if (KMyMoneySettings::showCountOfUnmarkedTransactions()) m_html += ""; if (KMyMoneySettings::showCountOfClearedTransactions()) m_html += ""; if (KMyMoneySettings::showCountOfNotReconciledTransactions()) m_html += ""; if (KMyMoneySettings::showDateOfLastReconciliation()) m_html += ""; m_html += QString("").arg(showColoredAmount(amount, m_total.isNegative())); m_html += "
"; m_html += i18n("Account"); m_html += "!MC!R%1"; m_html += i18n("Current Balance"); m_html += ""; m_html += i18n("To Minimum Balance / Maximum Credit"); m_html += "
%1%1
"; } } void showFavoriteReports() { QList reports = MyMoneyFile::instance()->reportList(); if (!reports.isEmpty()) { bool firstTime = 1; int row = 0; QList::const_iterator it_report = reports.constBegin(); while (it_report != reports.constEnd()) { if ((*it_report).isFavorite()) { if (firstTime) { m_html += QString("
%1
\n
 
\n").arg(i18n("Favorite Reports")); m_html += ""; m_html += ""; firstTime = false; } m_html += QString("") .arg(row++ & 0x01 ? "even" : "odd") .arg(link(VIEW_REPORTS, QString("?id=%1").arg((*it_report).id()))) .arg((*it_report).name()) .arg(linkend()) .arg((*it_report).comment()); } ++it_report; } if (!firstTime) m_html += "
"; m_html += i18n("Report"); m_html += ""; m_html += i18n("Comment"); m_html += "
%2%3%4%5
"; } } void showForecast() { MyMoneyFile* file = MyMoneyFile::instance(); QList accList; //if forecast has not been executed yet, do it. if (!m_forecast.isForecastDone()) doForecast(); accList = m_forecast.accountList(); if (accList.count() > 0) { // sort the accounts by name qStableSort(accList.begin(), accList.end(), accountNameLess); auto i = 0; auto colspan = 1; //get begin day auto beginDay = QDate::currentDate().daysTo(m_forecast.beginForecastDate()); //if begin day is today skip to next cycle if (beginDay == 0) beginDay = m_forecast.accountsCycle(); // Now output header m_html += QString("
%1
\n
 
\n").arg(i18n("%1 Day Forecast", m_forecast.forecastDays())); m_html += ""; m_html += ""; auto colWidth = 55 / (m_forecast.forecastDays() / m_forecast.accountsCycle()); for (i = 0; (i*m_forecast.accountsCycle() + beginDay) <= m_forecast.forecastDays(); ++i) { m_html += QString(""; colspan++; } m_html += ""; // Now output entries i = 0; QList::ConstIterator it_account; for (it_account = accList.constBegin(); it_account != accList.constEnd(); ++it_account) { //MyMoneyAccount acc = (*it_n); m_html += QString("").arg(i++ & 0x01 ? "even" : "odd"); m_html += QString(""; qint64 dropZero = -1; //account dropped below zero qint64 dropMinimum = -1; //account dropped below minimum balance QString minimumBalance = (*it_account).value("minimumBalance"); MyMoneyMoney minBalance = MyMoneyMoney(minimumBalance); MyMoneySecurity currency; MyMoneyMoney forecastBalance; //change account to deep currency if account is an investment if ((*it_account).isInvest()) { MyMoneySecurity underSecurity = file->security((*it_account).currencyId()); currency = file->security(underSecurity.tradingCurrency()); } else { currency = file->security((*it_account).currencyId()); } for (auto f = beginDay; f <= m_forecast.forecastDays(); f += m_forecast.accountsCycle()) { forecastBalance = m_forecast.forecastBalance(*it_account, QDate::currentDate().addDays(f)); QString amount; amount = MyMoneyUtils::formatMoney(forecastBalance, *it_account, currency); amount.replace(QChar(' '), " "); m_html += QString("").arg(showColoredAmount(amount, forecastBalance.isNegative())); } m_html += ""; //Check if the account is going to be below zero or below the minimal balance in the forecast period //Check if the account is going to be below minimal balance dropMinimum = m_forecast.daysToMinimumBalance(*it_account); //Check if the account is going to be below zero in the future dropZero = m_forecast.daysToZeroBalance(*it_account); // spit out possible warnings QString msg; // if a minimum balance has been specified, an appropriate warning will // only be shown, if the drop below 0 is on a different day or not present if (dropMinimum != -1 && !minBalance.isZero() && (dropMinimum < dropZero || dropZero == -1)) { switch (dropMinimum) { case 0: msg = i18n("The balance of %1 is below the minimum balance %2 today.", (*it_account).name(), MyMoneyUtils::formatMoney(minBalance, *it_account, currency)); msg = showColoredAmount(msg, true); break; default: msg = i18np("The balance of %2 will drop below the minimum balance %3 in %1 day.", "The balance of %2 will drop below the minimum balance %3 in %1 days.", dropMinimum - 1, (*it_account).name(), MyMoneyUtils::formatMoney(minBalance, *it_account, currency)); msg = showColoredAmount(msg, true); break; } if (!msg.isEmpty()) { m_html += QString("").arg(msg).arg(colspan); } } // a drop below zero is always shown msg.clear(); switch (dropZero) { case -1: break; case 0: if ((*it_account).accountGroup() == Account::Type::Asset) { msg = i18n("The balance of %1 is below %2 today.", (*it_account).name(), MyMoneyUtils::formatMoney(MyMoneyMoney(), *it_account, currency)); msg = showColoredAmount(msg, true); break; } if ((*it_account).accountGroup() == Account::Type::Liability) { msg = i18n("The balance of %1 is above %2 today.", (*it_account).name(), MyMoneyUtils::formatMoney(MyMoneyMoney(), *it_account, currency)); break; } break; default: if ((*it_account).accountGroup() == Account::Type::Asset) { msg = i18np("The balance of %2 will drop below %3 in %1 day.", "The balance of %2 will drop below %3 in %1 days.", dropZero, (*it_account).name(), MyMoneyUtils::formatMoney(MyMoneyMoney(), *it_account, currency)); msg = showColoredAmount(msg, true); break; } if ((*it_account).accountGroup() == Account::Type::Liability) { msg = i18np("The balance of %2 will raise above %3 in %1 day.", "The balance of %2 will raise above %3 in %1 days.", dropZero, (*it_account).name(), MyMoneyUtils::formatMoney(MyMoneyMoney(), *it_account, currency)); break; } } if (!msg.isEmpty()) { m_html += QString("").arg(msg).arg(colspan); } } m_html += "
"; m_html += i18n("Account"); m_html += "").arg(colWidth); m_html += i18ncp("Forecast days", "%1 day", "%1 days", i * m_forecast.accountsCycle() + beginDay); m_html += "
") + link(VIEW_LEDGER, QString("?id=%1").arg((*it_account).id())) + (*it_account).name() + linkend() + "").arg(colWidth); m_html += QString("%1
%1
%1
"; } } QString link(const QString& view, const QString& query, const QString& _title = QString()) const { QString titlePart; QString title(_title); if (!title.isEmpty()) titlePart = QString(" title=\"%1\"").arg(title.replace(QLatin1Char(' '), " ")); return QString("").arg(view, query, titlePart); } QString linkend() const { return QStringLiteral(""); } void showAssetsLiabilities() { QList accounts; QList::ConstIterator it; QList assets; QList liabilities; MyMoneyMoney netAssets; MyMoneyMoney netLiabilities; QString fontStart, fontEnd; MyMoneyFile* file = MyMoneyFile::instance(); int prec = MyMoneyMoney::denomToPrec(file->baseCurrency().smallestAccountFraction()); int i = 0; // get list of all accounts file->accountList(accounts); for (it = accounts.constBegin(); it != accounts.constEnd();) { if (!(*it).isClosed()) { switch ((*it).accountType()) { // group all assets into one list but make sure that investment accounts always show up case Account::Type::Investment: assets << *it; break; case Account::Type::Checkings: case Account::Type::Savings: case Account::Type::Cash: case Account::Type::Asset: case Account::Type::AssetLoan: // list account if it's the last in the hierarchy or has transactions in it if ((*it).accountList().isEmpty() || (file->transactionCount((*it).id()) > 0)) { assets << *it; } break; // group the liabilities into the other case Account::Type::CreditCard: case Account::Type::Liability: case Account::Type::Loan: // list account if it's the last in the hierarchy or has transactions in it if ((*it).accountList().isEmpty() || (file->transactionCount((*it).id()) > 0)) { liabilities << *it; } break; default: break; } } ++it; } //only do it if we have assets or liabilities account if (assets.count() > 0 || liabilities.count() > 0) { // sort the accounts by name qStableSort(assets.begin(), assets.end(), accountNameLess); qStableSort(liabilities.begin(), liabilities.end(), accountNameLess); QString statusHeader; if (KMyMoneySettings::showBalanceStatusOfOnlineAccounts()) { QString pathStatusHeader; pathStatusHeader = QPixmapToDataUri(Icons::get(Icon::ViewOutbox).pixmap(QSize(16,16))); statusHeader = QString("").arg(pathStatusHeader); } //print header m_html += "
" + i18n("Assets and Liabilities Summary") + "
\n
 
\n"; m_html += ""; //column titles m_html += ""; if (KMyMoneySettings::showBalanceStatusOfOnlineAccounts()) { m_html += ""; } m_html += ""; if (KMyMoneySettings::showCountOfUnmarkedTransactions()) m_html += ""; if (KMyMoneySettings::showCountOfClearedTransactions()) m_html += ""; if (KMyMoneySettings::showCountOfNotReconciledTransactions()) m_html += ""; if (KMyMoneySettings::showDateOfLastReconciliation()) m_html += ""; m_html += ""; //intermediate row to separate both columns m_html += ""; if (KMyMoneySettings::showBalanceStatusOfOnlineAccounts()) { m_html += ""; } m_html += ""; if (KMyMoneySettings::showCountOfUnmarkedTransactions()) m_html += ""; if (KMyMoneySettings::showCountOfClearedTransactions()) m_html += ""; if (KMyMoneySettings::showCountOfNotReconciledTransactions()) m_html += ""; if (KMyMoneySettings::showDateOfLastReconciliation()) m_html += ""; m_html += ""; QString placeHolder_Status, placeHolder_Counts; if (KMyMoneySettings::showBalanceStatusOfOnlineAccounts()) placeHolder_Status = ""; if (KMyMoneySettings::showCountOfUnmarkedTransactions()) placeHolder_Counts = ""; if (KMyMoneySettings::showCountOfClearedTransactions()) placeHolder_Counts += ""; if (KMyMoneySettings::showCountOfNotReconciledTransactions()) placeHolder_Counts += ""; if (KMyMoneySettings::showDateOfLastReconciliation()) placeHolder_Counts += ""; //get asset and liability accounts QList::const_iterator asset_it = assets.constBegin(); QList::const_iterator liabilities_it = liabilities.constBegin(); for (; asset_it != assets.constEnd() || liabilities_it != liabilities.constEnd();) { m_html += QString("").arg(i++ & 0x01 ? "even" : "odd"); //write an asset account if we still have any if (asset_it != assets.constEnd()) { MyMoneyMoney value; //investment accounts consolidate the balance of its subaccounts if ((*asset_it).accountType() == Account::Type::Investment) { value = investmentBalance(*asset_it); } else { value = MyMoneyFile::instance()->balance((*asset_it).id(), QDate::currentDate()); } //calculate balance for foreign currency accounts if ((*asset_it).currencyId() != file->baseCurrency().id()) { ReportAccount repAcc = ReportAccount((*asset_it).id()); MyMoneyMoney curPrice = repAcc.baseCurrencyPrice(QDate::currentDate()); MyMoneyMoney baseValue = value * curPrice; baseValue = baseValue.convert(10000); netAssets += baseValue; } else { netAssets += value; } //show the account without minimum balance showAccountEntry(*asset_it, value, MyMoneyMoney(), false); ++asset_it; } else { //write a white space if we don't m_html += QString("%1%2").arg(placeHolder_Status).arg(placeHolder_Counts); } //leave the intermediate column empty m_html += ""; //write a liability account if (liabilities_it != liabilities.constEnd()) { MyMoneyMoney value; value = MyMoneyFile::instance()->balance((*liabilities_it).id(), QDate::currentDate()); //calculate balance if foreign currency if ((*liabilities_it).currencyId() != file->baseCurrency().id()) { ReportAccount repAcc = ReportAccount((*liabilities_it).id()); MyMoneyMoney curPrice = repAcc.baseCurrencyPrice(QDate::currentDate()); MyMoneyMoney baseValue = value * curPrice; baseValue = baseValue.convert(10000); netLiabilities += baseValue; } else { netLiabilities += value; } //show the account without minimum balance showAccountEntry(*liabilities_it, value, MyMoneyMoney(), false); ++liabilities_it; } else { //leave the space empty if we run out of liabilities m_html += QString("%1%2").arg(placeHolder_Status).arg(placeHolder_Counts); } m_html += ""; } //calculate net worth MyMoneyMoney netWorth = netAssets + netLiabilities; //format assets, liabilities and net worth QString amountAssets = netAssets.formatMoney(file->baseCurrency().tradingSymbol(), prec); QString amountLiabilities = netLiabilities.formatMoney(file->baseCurrency().tradingSymbol(), prec); QString amountNetWorth = netWorth.formatMoney(file->baseCurrency().tradingSymbol(), prec); amountAssets.replace(QChar(' '), " "); amountLiabilities.replace(QChar(' '), " "); amountNetWorth.replace(QChar(' '), " "); m_html += QString("").arg(i++ & 0x01 ? "even" : "odd"); //print total for assets m_html += QString("%1%3").arg(placeHolder_Status).arg(i18n("Total Assets")).arg(placeHolder_Counts).arg(showColoredAmount(amountAssets, netAssets.isNegative())); //leave the intermediate column empty m_html += ""; //print total liabilities m_html += QString("%1%3").arg(placeHolder_Status).arg(i18n("Total Liabilities")).arg(placeHolder_Counts).arg(showColoredAmount(amountLiabilities, netLiabilities.isNegative())); m_html += ""; //print net worth m_html += QString("").arg(i++ & 0x01 ? "even" : "odd"); m_html += QString("%1%2").arg(placeHolder_Status).arg(placeHolder_Counts); m_html += QString("%1%3").arg(placeHolder_Status).arg(i18n("Net Worth")).arg(placeHolder_Counts).arg(showColoredAmount(amountNetWorth, netWorth.isNegative())); m_html += ""; m_html += "
"; m_html += statusHeader; m_html += ""; m_html += i18n("Asset Accounts"); m_html += "!MC!R" + i18n("Last Reconciled") + ""; m_html += i18n("Current Balance"); m_html += ""; m_html += statusHeader; m_html += ""; m_html += i18n("Liability Accounts"); m_html += "!MC!R" + i18n("Last Reconciled") + ""; m_html += i18n("Current Balance"); m_html += "
%2%4%2%4
%2%4
"; m_html += "
"; } } void showBudget() { MyMoneyFile* file = MyMoneyFile::instance(); if (file->countBudgets()) { int prec = MyMoneyMoney::denomToPrec(file->baseCurrency().smallestAccountFraction()); bool isOverrun = false; int i = 0; //config report just like "Monthly Budgeted vs Actual MyMoneyReport reportCfg = MyMoneyReport( - MyMoneyReport::eBudgetActual, - MyMoneyReport::eMonths, + eMyMoney::Report::RowType::BudgetActual, + static_cast(eMyMoney::Report::ColumnType::Months), TransactionFilter::Date::CurrentMonth, - MyMoneyReport::eDetailAll, + eMyMoney::Report::DetailLevel::All, i18n("Monthly Budgeted vs. Actual"), i18n("Generated Report")); reportCfg.setBudget("Any", true); reports::PivotTable table(reportCfg); PivotGrid grid = table.grid(); //div header m_html += "
" + i18n("Budget") + "
\n
 
\n"; //display budget summary m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += QString(""); MyMoneyMoney totalBudgetValue = grid.m_total[eBudget].m_total; MyMoneyMoney totalActualValue = grid.m_total[eActual].m_total; MyMoneyMoney totalBudgetDiffValue = grid.m_total[eBudgetDiff].m_total; QString totalBudgetAmount = totalBudgetValue.formatMoney(file->baseCurrency().tradingSymbol(), prec); QString totalActualAmount = totalActualValue.formatMoney(file->baseCurrency().tradingSymbol(), prec); QString totalBudgetDiffAmount = totalBudgetDiffValue.formatMoney(file->baseCurrency().tradingSymbol(), prec); m_html += QString("").arg(showColoredAmount(totalBudgetAmount, totalBudgetValue.isNegative())); m_html += QString("").arg(showColoredAmount(totalActualAmount, totalActualValue.isNegative())); m_html += QString("").arg(showColoredAmount(totalBudgetDiffAmount, totalBudgetDiffValue.isNegative())); m_html += ""; m_html += "
"; m_html += i18n("Current Month Summary"); m_html += "
"; m_html += i18n("Budgeted"); m_html += ""; m_html += i18n("Actual"); m_html += ""; m_html += i18n("Difference"); m_html += "
%1%1%1
"; //budget overrun m_html += "
 
\n"; m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; PivotGrid::iterator it_outergroup = grid.begin(); while (it_outergroup != grid.end()) { i = 0; PivotOuterGroup::iterator it_innergroup = (*it_outergroup).begin(); while (it_innergroup != (*it_outergroup).end()) { PivotInnerGroup::iterator it_row = (*it_innergroup).begin(); while (it_row != (*it_innergroup).end()) { //column number is 1 because the report includes only current month if (it_row.value()[eBudgetDiff].value(1).isNegative()) { //get report account to get the name later ReportAccount rowname = it_row.key(); //write the outergroup if it is the first row of outergroup being shown if (i == 0) { m_html += ""; m_html += QString("").arg(MyMoneyAccount::accountTypeToString(rowname.accountType())); m_html += ""; } m_html += QString("").arg(i++ & 0x01 ? "even" : "odd"); //get values from grid MyMoneyMoney actualValue = it_row.value()[eActual][1]; MyMoneyMoney budgetValue = it_row.value()[eBudget][1]; MyMoneyMoney budgetDiffValue = it_row.value()[eBudgetDiff][1]; //format amounts QString actualAmount = actualValue.formatMoney(file->baseCurrency().tradingSymbol(), prec); QString budgetAmount = budgetValue.formatMoney(file->baseCurrency().tradingSymbol(), prec); QString budgetDiffAmount = budgetDiffValue.formatMoney(file->baseCurrency().tradingSymbol(), prec); //account name m_html += QString(""; //show amounts m_html += QString("").arg(showColoredAmount(budgetAmount, budgetValue.isNegative())); m_html += QString("").arg(showColoredAmount(actualAmount, actualValue.isNegative())); m_html += QString("").arg(showColoredAmount(budgetDiffAmount, budgetDiffValue.isNegative())); m_html += ""; //set the flag that there are overruns isOverrun = true; } ++it_row; } ++it_innergroup; } ++it_outergroup; } //if no negative differences are found, then inform that if (!isOverrun) { m_html += QString::fromLatin1("").arg(((i++ & 1) == 1) ? QLatin1String("even") : QLatin1String("odd")); m_html += QString::fromLatin1("").arg(i18n("No Budget Categories have been overrun")); m_html += ""; } m_html += "
"; m_html += i18n("Budget Overruns"); m_html += "
"; m_html += i18n("Account"); m_html += ""; m_html += i18n("Budgeted"); m_html += ""; m_html += i18n("Actual"); m_html += ""; m_html += i18n("Difference"); m_html += "
%1
") + link(VIEW_LEDGER, QString("?id=%1").arg(rowname.id())) + rowname.name() + linkend() + "%1%1%1
%1
"; } } void showCashFlowSummary() { MyMoneyTransactionFilter filter; MyMoneyMoney incomeValue; MyMoneyMoney expenseValue; MyMoneyFile* file = MyMoneyFile::instance(); int prec = MyMoneyMoney::denomToPrec(file->baseCurrency().smallestAccountFraction()); //set start and end of month dates QDate startOfMonth = QDate(QDate::currentDate().year(), QDate::currentDate().month(), 1); QDate endOfMonth = QDate(QDate::currentDate().year(), QDate::currentDate().month(), QDate::currentDate().daysInMonth()); //Add total income and expenses for this month //get transactions for current month filter.setDateFilter(startOfMonth, endOfMonth); filter.setReportAllSplits(false); QList transactions = file->transactionList(filter); //if no transaction then skip and print total in zero if (transactions.size() > 0) { //get all transactions for this month foreach (const auto transaction, transactions) { //get the splits for each transaction foreach (const auto split, transaction.splits()) { if (!split.shares().isZero()) { ReportAccount repSplitAcc = ReportAccount(split.accountId()); //only add if it is an income or expense if (repSplitAcc.isIncomeExpense()) { MyMoneyMoney value; //convert to base currency if necessary if (repSplitAcc.currencyId() != file->baseCurrency().id()) { MyMoneyMoney curPrice = repSplitAcc.baseCurrencyPrice(transaction.postDate()); value = (split.shares() * MyMoneyMoney::MINUS_ONE) * curPrice; value = value.convert(10000); } else { value = (split.shares() * MyMoneyMoney::MINUS_ONE); } //store depending on account type if (repSplitAcc.accountType() == Account::Type::Income) { incomeValue += value; } else { expenseValue += value; } } } } } } //format income and expenses QString amountIncome = incomeValue.formatMoney(file->baseCurrency().tradingSymbol(), prec); QString amountExpense = expenseValue.formatMoney(file->baseCurrency().tradingSymbol(), prec); amountIncome.replace(QChar(' '), " "); amountExpense.replace(QChar(' '), " "); //calculate schedules //Add all schedules for this month MyMoneyMoney scheduledIncome; MyMoneyMoney scheduledExpense; MyMoneyMoney scheduledLiquidTransfer; MyMoneyMoney scheduledOtherTransfer; //get overdues and schedules until the end of this month QList schedule = file->scheduleList(QString(), Schedule::Type::Any, Schedule::Occurrence::Any, Schedule::PaymentType::Any, QDate(), endOfMonth, false); //Remove the finished schedules QList::Iterator finished_it; for (finished_it = schedule.begin(); finished_it != schedule.end();) { if ((*finished_it).isFinished()) { finished_it = schedule.erase(finished_it); continue; } ++finished_it; } //add income and expenses QList::Iterator sched_it; for (sched_it = schedule.begin(); sched_it != schedule.end();) { QDate nextDate = (*sched_it).nextDueDate(); int cnt = 0; while (nextDate.isValid() && nextDate <= endOfMonth) { ++cnt; nextDate = (*sched_it).nextPayment(nextDate); // for single occurrence nextDate will not change, so we // better get out of here. if ((*sched_it).occurrence() == Schedule::Occurrence::Once) break; } MyMoneyAccount acc = (*sched_it).account(); if (!acc.id().isEmpty()) { MyMoneyTransaction transaction = (*sched_it).transaction(); // only show the entry, if it is still active MyMoneySplit sp = transaction.splitByAccount(acc.id(), true); // take care of the autoCalc stuff if ((*sched_it).type() == Schedule::Type::LoanPayment) { nextDate = (*sched_it).nextPayment((*sched_it).lastPayment()); //make sure we have all 'starting balances' so that the autocalc works QMap balanceMap; foreach (const auto split, transaction.splits()) { acc = file->account(split.accountId()); // collect all overdues on the first day QDate schedDate = nextDate; if (QDate::currentDate() >= nextDate) schedDate = QDate::currentDate().addDays(1); balanceMap[acc.id()] += file->balance(acc.id(), QDate::currentDate()); } KMyMoneyUtils::calculateAutoLoan(*sched_it, transaction, balanceMap); } //go through the splits and assign to liquid or other transfers const QList splits = transaction.splits(); QList::const_iterator split_it; for (split_it = splits.constBegin(); split_it != splits.constEnd(); ++split_it) { if ((*split_it).accountId() != acc.id()) { ReportAccount repSplitAcc = ReportAccount((*split_it).accountId()); //get the shares and multiply by the quantity of occurrences in the period MyMoneyMoney value = (*split_it).shares() * cnt; //convert to foreign currency if needed if (repSplitAcc.currencyId() != file->baseCurrency().id()) { MyMoneyMoney curPrice = repSplitAcc.baseCurrencyPrice(QDate::currentDate()); value = value * curPrice; value = value.convert(10000); } if ((repSplitAcc.isLiquidLiability() || repSplitAcc.isLiquidAsset()) && acc.accountGroup() != repSplitAcc.accountGroup()) { scheduledLiquidTransfer += value; } else if (repSplitAcc.isAssetLiability() && !repSplitAcc.isLiquidLiability() && !repSplitAcc.isLiquidAsset()) { scheduledOtherTransfer += value; } else if (repSplitAcc.isIncomeExpense()) { //income and expenses are stored as negative values if (repSplitAcc.accountType() == Account::Type::Income) scheduledIncome -= value; if (repSplitAcc.accountType() == Account::Type::Expense) scheduledExpense -= value; } } } } ++sched_it; } //format the currency strings QString amountScheduledIncome = scheduledIncome.formatMoney(file->baseCurrency().tradingSymbol(), prec); QString amountScheduledExpense = scheduledExpense.formatMoney(file->baseCurrency().tradingSymbol(), prec); QString amountScheduledLiquidTransfer = scheduledLiquidTransfer.formatMoney(file->baseCurrency().tradingSymbol(), prec); QString amountScheduledOtherTransfer = scheduledOtherTransfer.formatMoney(file->baseCurrency().tradingSymbol(), prec); amountScheduledIncome.replace(QChar(' '), " "); amountScheduledExpense.replace(QChar(' '), " "); amountScheduledLiquidTransfer.replace(QChar(' '), " "); amountScheduledOtherTransfer.replace(QChar(' '), " "); //get liquid assets and liabilities QList accounts; QList::const_iterator account_it; MyMoneyMoney liquidAssets; MyMoneyMoney liquidLiabilities; // get list of all accounts file->accountList(accounts); for (account_it = accounts.constBegin(); account_it != accounts.constEnd();) { if (!(*account_it).isClosed()) { switch ((*account_it).accountType()) { //group all assets into one list case Account::Type::Checkings: case Account::Type::Savings: case Account::Type::Cash: { MyMoneyMoney value = MyMoneyFile::instance()->balance((*account_it).id(), QDate::currentDate()); //calculate balance for foreign currency accounts if ((*account_it).currencyId() != file->baseCurrency().id()) { ReportAccount repAcc = ReportAccount((*account_it).id()); MyMoneyMoney curPrice = repAcc.baseCurrencyPrice(QDate::currentDate()); MyMoneyMoney baseValue = value * curPrice; liquidAssets += baseValue; liquidAssets = liquidAssets.convert(10000); } else { liquidAssets += value; } break; } //group the liabilities into the other case Account::Type::CreditCard: { MyMoneyMoney value; value = MyMoneyFile::instance()->balance((*account_it).id(), QDate::currentDate()); //calculate balance if foreign currency if ((*account_it).currencyId() != file->baseCurrency().id()) { ReportAccount repAcc = ReportAccount((*account_it).id()); MyMoneyMoney curPrice = repAcc.baseCurrencyPrice(QDate::currentDate()); MyMoneyMoney baseValue = value * curPrice; liquidLiabilities += baseValue; liquidLiabilities = liquidLiabilities.convert(10000); } else { liquidLiabilities += value; } break; } default: break; } } ++account_it; } //calculate net worth MyMoneyMoney liquidWorth = liquidAssets + liquidLiabilities; //format assets, liabilities and net worth QString amountLiquidAssets = liquidAssets.formatMoney(file->baseCurrency().tradingSymbol(), prec); QString amountLiquidLiabilities = liquidLiabilities.formatMoney(file->baseCurrency().tradingSymbol(), prec); QString amountLiquidWorth = liquidWorth.formatMoney(file->baseCurrency().tradingSymbol(), prec); amountLiquidAssets.replace(QChar(' '), " "); amountLiquidLiabilities.replace(QChar(' '), " "); amountLiquidWorth.replace(QChar(' '), " "); //show the summary m_html += "
" + i18n("Cash Flow Summary") + "
\n
 
\n"; //print header m_html += ""; //income and expense title m_html += ""; m_html += ""; //column titles m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; //add row with banding m_html += QString(""); //print current income m_html += QString("").arg(showColoredAmount(amountIncome, incomeValue.isNegative())); //print the scheduled income m_html += QString("").arg(showColoredAmount(amountScheduledIncome, scheduledIncome.isNegative())); //print current expenses m_html += QString("").arg(showColoredAmount(amountExpense, expenseValue.isNegative())); //print the scheduled expenses m_html += QString("").arg(showColoredAmount(amountScheduledExpense, scheduledExpense.isNegative())); m_html += ""; m_html += "
"; m_html += i18n("Income and Expenses of Current Month"); m_html += "
"; m_html += i18n("Income"); m_html += ""; m_html += i18n("Scheduled Income"); m_html += ""; m_html += i18n("Expenses"); m_html += ""; m_html += i18n("Scheduled Expenses"); m_html += "
%2%2%2%2
"; //print header of assets and liabilities m_html += "
 
\n"; m_html += ""; //assets and liabilities title m_html += ""; m_html += ""; //column titles m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; //add row with banding m_html += QString(""); //print current liquid assets m_html += QString("").arg(showColoredAmount(amountLiquidAssets, liquidAssets.isNegative())); //print the scheduled transfers m_html += QString("").arg(showColoredAmount(amountScheduledLiquidTransfer, scheduledLiquidTransfer.isNegative())); //print current liabilities m_html += QString("").arg(showColoredAmount(amountLiquidLiabilities, liquidLiabilities.isNegative())); //print the scheduled transfers m_html += QString("").arg(showColoredAmount(amountScheduledOtherTransfer, scheduledOtherTransfer.isNegative())); m_html += ""; m_html += "
"; m_html += i18n("Liquid Assets and Liabilities"); m_html += "
"; m_html += i18n("Liquid Assets"); m_html += ""; m_html += i18n("Transfers to Liquid Liabilities"); m_html += ""; m_html += i18n("Liquid Liabilities"); m_html += ""; m_html += i18n("Other Transfers"); m_html += "
%2%2%2%2
"; //final conclusion MyMoneyMoney profitValue = incomeValue + expenseValue + scheduledIncome + scheduledExpense; MyMoneyMoney expectedAsset = liquidAssets + scheduledIncome + scheduledExpense + scheduledLiquidTransfer + scheduledOtherTransfer; MyMoneyMoney expectedLiabilities = liquidLiabilities + scheduledLiquidTransfer; QString amountExpectedAsset = expectedAsset.formatMoney(file->baseCurrency().tradingSymbol(), prec); QString amountExpectedLiabilities = expectedLiabilities.formatMoney(file->baseCurrency().tradingSymbol(), prec); QString amountProfit = profitValue.formatMoney(file->baseCurrency().tradingSymbol(), prec); amountProfit.replace(QChar(' '), " "); amountExpectedAsset.replace(QChar(' '), " "); amountExpectedLiabilities.replace(QChar(' '), " "); //print header of cash flow status m_html += "
 
\n"; m_html += ""; //income and expense title m_html += ""; m_html += ""; //column titles m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; //add row with banding m_html += QString(""); m_html += ""; //print expected assets m_html += QString("").arg(showColoredAmount(amountExpectedAsset, expectedAsset.isNegative())); //print expected liabilities m_html += QString("").arg(showColoredAmount(amountExpectedLiabilities, expectedLiabilities.isNegative())); //print expected profit m_html += QString("").arg(showColoredAmount(amountProfit, profitValue.isNegative())); m_html += ""; m_html += "
"; m_html += i18n("Cash Flow Status"); m_html += "
 "; m_html += i18n("Expected Liquid Assets"); m_html += ""; m_html += i18n("Expected Liquid Liabilities"); m_html += ""; m_html += i18n("Expected Profit/Loss"); m_html += "
 %2%2%2
"; m_html += "
"; } KHomeView *q_ptr; /** * daily balances of an account */ typedef QMap dailyBalances; #ifdef ENABLE_WEBENGINE QWebEngineView *m_view; #else KWebView *m_view; #endif QString m_html; bool m_showAllSchedules; bool m_needLoad; MyMoneyForecast m_forecast; MyMoneyMoney m_total; /** * Hold the last valid size of the net worth graph * for the times when the needed size can't be computed. */ QSize m_netWorthGraphLastValidSize; /** * daily forecast balance of accounts */ QMap m_accountList; QPrinter *m_currentPrinter; }; #endif diff --git a/kmymoney/views/kpayeesview.cpp b/kmymoney/views/kpayeesview.cpp index 121228722..eee3f7192 100644 --- a/kmymoney/views/kpayeesview.cpp +++ b/kmymoney/views/kpayeesview.cpp @@ -1,719 +1,719 @@ /*************************************************************************** kpayeesview.cpp --------------- begin : Thu Jan 24 2002 copyright : (C) 2000-2002 by Michael Edwardes Javier Campos Morales Felix Rodriguez John C Thomas Baumgart Kevin Tambascio Andreas Nicolai (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 "kpayeesview_p.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include #include "ui_kpayeesview.h" #include "kmymoneyviewbase_p.h" #include "kpayeeidentifierview.h" #include "mymoneypayee.h" #include "mymoneyexception.h" #include "mymoneyfile.h" #include "mymoneyaccount.h" #include "mymoneymoney.h" #include "mymoneytransactionfilter.h" #include "kmymoneysettings.h" #include "models.h" #include "accountsmodel.h" #include "mymoneysecurity.h" #include "mymoneycontact.h" #include "mymoneyprice.h" #include "mymoneysplit.h" #include "mymoneytransaction.h" #include "icons/icons.h" #include "transaction.h" #include "widgetenums.h" #include "mymoneyenums.h" #include "modelenums.h" #include "menuenums.h" using namespace Icons; // *** KPayeesView Implementation *** KPayeesView::KPayeesView(QWidget *parent) : KMyMoneyViewBase(*new KPayeesViewPrivate(this), parent) { connect(pActions[eMenu::Action::NewPayee], &QAction::triggered, this, &KPayeesView::slotNewPayee); connect(pActions[eMenu::Action::RenamePayee], &QAction::triggered, this, &KPayeesView::slotRenamePayee); connect(pActions[eMenu::Action::DeletePayee], &QAction::triggered, this, &KPayeesView::slotDeletePayee); connect(pActions[eMenu::Action::MergePayee], &QAction::triggered, this, &KPayeesView::slotMergePayee); } KPayeesView::~KPayeesView() { } void KPayeesView::slotChooseDefaultAccount() { Q_D(KPayeesView); MyMoneyFile* file = MyMoneyFile::instance(); QMap account_count; KMyMoneyRegister::RegisterItem* item = d->ui->m_register->firstItem(); while (item) { //only walk through selectable items. eg. transactions and not group markers if (item->isSelectable()) { auto t = dynamic_cast(item); if (!t) return; MyMoneySplit s = t->transaction().splitByPayee(d->m_payee.id()); const MyMoneyAccount& acc = file->account(s.accountId()); QString txt; if (s.action() != MyMoneySplit::actionName(eMyMoney::Split::Action::Amortization) && acc.accountType() != eMyMoney::Account::Type::AssetLoan && !file->isTransfer(t->transaction()) && t->transaction().splitCount() == 2) { MyMoneySplit s0 = t->transaction().splitByAccount(s.accountId(), false); if (account_count.contains(s0.accountId())) { account_count[s0.accountId()]++; } else { account_count[s0.accountId()] = 1; } } } item = item->nextItem(); } QMap::Iterator most_frequent, iter; most_frequent = account_count.begin(); for (iter = account_count.begin(); iter != account_count.end(); ++iter) { if (iter.value() > most_frequent.value()) { most_frequent = iter; } } if (most_frequent != account_count.end()) { d->ui->checkEnableDefaultCategory->setChecked(true); d->ui->comboDefaultCategory->setSelected(most_frequent.key()); d->setDirty(true); } } void KPayeesView::slotClosePayeeIdentifierSource() { Q_D(KPayeesView); if (!d->m_needLoad) d->ui->payeeIdentifiers->closeSource(); } void KPayeesView::slotSelectByVariant(const QVariantList& variant, eView::Intent intent) { switch (intent) { case eView::Intent::ShowPayee: if (variant.count() == 3) slotSelectPayeeAndTransaction(variant.at(0).toString(), variant.at(1).toString(), variant.at(2).toString()); break; default: break; } } void KPayeesView::slotStartRename(QListWidgetItem* item) { Q_D(KPayeesView); d->m_allowEditing = true; d->ui->m_payeesList->editItem(item); } // This variant is only called when a single payee is selected and renamed. void KPayeesView::slotRenameSinglePayee(QListWidgetItem* p) { Q_D(KPayeesView); //if there is no current item selected, exit if (d->m_allowEditing == false || !d->ui->m_payeesList->currentItem() || p != d->ui->m_payeesList->currentItem()) return; //qDebug() << "[KPayeesView::slotRenamePayee]"; // create a copy of the new name without appended whitespaces QString new_name = p->text(); if (d->m_payee.name() != new_name) { MyMoneyFileTransaction ft; try { // check if we already have a payee with the new name try { // this function call will throw an exception, if the payee // hasn't been found. MyMoneyFile::instance()->payeeByName(new_name); // the name already exists, ask the user whether he's sure to keep the name if (KMessageBox::questionYesNo(this, i18n("A payee with the name '%1' already exists. It is not advisable to have " "multiple payees with the same identification name. Are you sure you would like " "to rename the payee?", new_name)) != KMessageBox::Yes) { p->setText(d->m_payee.name()); return; } } catch (const MyMoneyException &) { // all ok, the name is unique } d->m_payee.setName(new_name); d->m_newName = new_name; MyMoneyFile::instance()->modifyPayee(d->m_payee); // the above call to modifyPayee will reload the view so // all references and pointers to the view have to be // re-established. // make sure, that the record is visible even if it moved // out of sight due to the rename operation d->ensurePayeeVisible(d->m_payee.id()); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(this, i18n("Unable to modify payee"), QString::fromLatin1(e.what())); } } else { p->setText(new_name); } } void KPayeesView::slotSelectPayee(QListWidgetItem* cur, QListWidgetItem* prev) { Q_D(KPayeesView); Q_UNUSED(cur); Q_UNUSED(prev); d->m_allowEditing = false; } void KPayeesView::slotSelectPayee() { Q_D(KPayeesView); // check if the content of a currently selected payee was modified // and ask to store the data if (d->isDirty()) { if (KMessageBox::questionYesNo(this, i18n("Do you want to save the changes for %1?", d->m_newName), i18n("Save changes")) == KMessageBox::Yes) { d->m_inSelection = true; slotUpdatePayee(); d->m_inSelection = false; } } // make sure we always clear the selected list when listing again d->m_selectedPayeesList.clear(); // loop over all payees and count the number of payees, also // obtain last selected payee d->selectedPayees(d->m_selectedPayeesList); updatePayeeActions(d->m_selectedPayeesList); emit selectObjects(d->m_selectedPayeesList); if (d->m_selectedPayeesList.isEmpty()) { d->ui->m_tabWidget->setEnabled(false); // disable tab widget d->ui->m_balanceLabel->hide(); d->ui->m_deleteButton->setEnabled(false); //disable delete, rename and merge buttons d->ui->m_renameButton->setEnabled(false); d->ui->m_mergeButton->setEnabled(false); d->clearItemData(); d->m_payee = MyMoneyPayee(); d->ui->m_syncAddressbook->setEnabled(false); return; // make sure we don't access an undefined payee } d->ui->m_deleteButton->setEnabled(true); //re-enable delete button d->ui->m_syncAddressbook->setEnabled(true); // if we have multiple payees selected, clear and disable the payee information if (d->m_selectedPayeesList.count() > 1) { d->ui->m_tabWidget->setEnabled(false); // disable tab widget d->ui->m_renameButton->setEnabled(false); // disable also the rename button d->ui->m_mergeButton->setEnabled(true); d->ui->m_balanceLabel->hide(); d->clearItemData(); } else { d->ui->m_mergeButton->setEnabled(false); d->ui->m_renameButton->setEnabled(true); } // otherwise we have just one selected, enable payee information widget d->ui->m_tabWidget->setEnabled(true); d->ui->m_balanceLabel->show(); // as of now we are updating only the last selected payee, and until // selection mode of the QListView has been changed to Extended, this // will also be the only selection and behave exactly as before - Andreas try { d->m_payee = d->m_selectedPayeesList[0]; d->m_newName = d->m_payee.name(); d->ui->addressEdit->setEnabled(true); d->ui->addressEdit->setText(d->m_payee.address()); d->ui->postcodeEdit->setEnabled(true); d->ui->postcodeEdit->setText(d->m_payee.postcode()); d->ui->telephoneEdit->setEnabled(true); d->ui->telephoneEdit->setText(d->m_payee.telephone()); d->ui->emailEdit->setEnabled(true); d->ui->emailEdit->setText(d->m_payee.email()); d->ui->notesEdit->setText(d->m_payee.notes()); QStringList keys; bool ignorecase = false; - MyMoneyPayee::payeeMatchType type = d->m_payee.matchData(ignorecase, keys); + auto type = d->m_payee.matchData(ignorecase, keys); - d->ui->matchTypeCombo->setCurrentIndex(d->ui->matchTypeCombo->findData(type)); + d->ui->matchTypeCombo->setCurrentIndex(d->ui->matchTypeCombo->findData(static_cast(type))); d->ui->matchKeyEditList->clear(); d->ui->matchKeyEditList->insertStringList(keys); d->ui->checkMatchIgnoreCase->setChecked(ignorecase); d->ui->checkEnableDefaultCategory->setChecked(d->m_payee.defaultAccountEnabled()); d->ui->comboDefaultCategory->setSelected(d->m_payee.defaultAccountId()); d->ui->payeeIdentifiers->setSource(d->m_payee); slotPayeeDataChanged(); d->showTransactions(); } catch (const MyMoneyException &e) { qDebug("exception during display of payee: %s", e.what()); d->ui->m_register->clear(); d->m_selectedPayeesList.clear(); d->m_payee = MyMoneyPayee(); } d->m_allowEditing = true; } void KPayeesView::slotKeyListChanged() { Q_D(KPayeesView); bool rc = false; bool ignorecase = false; QStringList keys; d->m_payee.matchData(ignorecase, keys); - if (d->ui->matchTypeCombo->currentData().toUInt() == MyMoneyPayee::matchKey) { + if (static_cast(d->ui->matchTypeCombo->currentData().toUInt()) == eMyMoney::Payee::MatchType::Key) { rc |= (keys != d->ui->matchKeyEditList->items()); } d->setDirty(rc); } void KPayeesView::slotPayeeDataChanged() { Q_D(KPayeesView); bool rc = false; if (d->ui->m_tabWidget->isEnabled()) { rc |= ((d->m_payee.email().isEmpty() != d->ui->emailEdit->text().isEmpty()) || (!d->ui->emailEdit->text().isEmpty() && d->m_payee.email() != d->ui->emailEdit->text())); rc |= ((d->m_payee.address().isEmpty() != d->ui->addressEdit->toPlainText().isEmpty()) || (!d->ui->addressEdit->toPlainText().isEmpty() && d->m_payee.address() != d->ui->addressEdit->toPlainText())); rc |= ((d->m_payee.postcode().isEmpty() != d->ui->postcodeEdit->text().isEmpty()) || (!d->ui->postcodeEdit->text().isEmpty() && d->m_payee.postcode() != d->ui->postcodeEdit->text())); rc |= ((d->m_payee.telephone().isEmpty() != d->ui->telephoneEdit->text().isEmpty()) || (!d->ui->telephoneEdit->text().isEmpty() && d->m_payee.telephone() != d->ui->telephoneEdit->text())); rc |= ((d->m_payee.name().isEmpty() != d->m_newName.isEmpty()) || (!d->m_newName.isEmpty() && d->m_payee.name() != d->m_newName)); rc |= ((d->m_payee.notes().isEmpty() != d->ui->notesEdit->toPlainText().isEmpty()) || (!d->ui->notesEdit->toPlainText().isEmpty() && d->m_payee.notes() != d->ui->notesEdit->toPlainText())); bool ignorecase = false; QStringList keys; - MyMoneyPayee::payeeMatchType type = d->m_payee.matchData(ignorecase, keys); + auto type = d->m_payee.matchData(ignorecase, keys); rc |= (static_cast(type) != d->ui->matchTypeCombo->currentData().toUInt()); d->ui->checkMatchIgnoreCase->setEnabled(false); d->ui->matchKeyEditList->setEnabled(false); - if (d->ui->matchTypeCombo->currentData().toUInt() != MyMoneyPayee::matchDisabled) { + if (static_cast(d->ui->matchTypeCombo->currentData().toUInt()) != eMyMoney::Payee::MatchType::Disabled) { d->ui->checkMatchIgnoreCase->setEnabled(true); // if we turn matching on, we default to 'ignore case' // TODO maybe make the default a user option - if (type == MyMoneyPayee::matchDisabled && d->ui->matchTypeCombo->currentData().toUInt() != MyMoneyPayee::matchDisabled) + if (type == eMyMoney::Payee::MatchType::Disabled && static_cast(d->ui->matchTypeCombo->currentData().toUInt()) != eMyMoney::Payee::MatchType::Disabled) d->ui->checkMatchIgnoreCase->setChecked(true); rc |= (ignorecase != d->ui->checkMatchIgnoreCase->isChecked()); - if (d->ui->matchTypeCombo->currentData().toUInt() == MyMoneyPayee::matchKey) { + if (static_cast(d->ui->matchTypeCombo->currentData().toUInt()) == eMyMoney::Payee::MatchType::Key) { d->ui->matchKeyEditList->setEnabled(true); rc |= (keys != d->ui->matchKeyEditList->items()); } } rc |= (d->ui->checkEnableDefaultCategory->isChecked() != d->m_payee.defaultAccountEnabled()); if (d->ui->checkEnableDefaultCategory->isChecked()) { d->ui->comboDefaultCategory->setEnabled(true); d->ui->labelDefaultCategory->setEnabled(true); // this is only going to understand the first in the list of selected accounts if (d->ui->comboDefaultCategory->getSelected().isEmpty()) { rc |= !d->m_payee.defaultAccountId().isEmpty(); } else { QString temp = d->ui->comboDefaultCategory->getSelected(); rc |= (temp.isEmpty() != d->m_payee.defaultAccountId().isEmpty()) || (!d->m_payee.defaultAccountId().isEmpty() && temp != d->m_payee.defaultAccountId()); } } else { d->ui->comboDefaultCategory->setEnabled(false); d->ui->labelDefaultCategory->setEnabled(false); } rc |= (d->m_payee.payeeIdentifiers() != d->ui->payeeIdentifiers->identifiers()); } d->setDirty(rc); } void KPayeesView::slotUpdatePayee() { Q_D(KPayeesView); if (d->isDirty()) { MyMoneyFileTransaction ft; d->setDirty(false); try { d->m_payee.setName(d->m_newName); d->m_payee.setAddress(d->ui->addressEdit->toPlainText()); d->m_payee.setPostcode(d->ui->postcodeEdit->text()); d->m_payee.setTelephone(d->ui->telephoneEdit->text()); d->m_payee.setEmail(d->ui->emailEdit->text()); d->m_payee.setNotes(d->ui->notesEdit->toPlainText()); - d->m_payee.setMatchData(static_cast(d->ui->matchTypeCombo->currentData().toUInt()), d->ui->checkMatchIgnoreCase->isChecked(), d->ui->matchKeyEditList->items()); + d->m_payee.setMatchData(static_cast(d->ui->matchTypeCombo->currentData().toUInt()), d->ui->checkMatchIgnoreCase->isChecked(), d->ui->matchKeyEditList->items()); d->m_payee.setDefaultAccountId(); d->m_payee.resetPayeeIdentifiers(d->ui->payeeIdentifiers->identifiers()); if (d->ui->checkEnableDefaultCategory->isChecked()) { QString temp; if (!d->ui->comboDefaultCategory->getSelected().isEmpty()) { temp = d->ui->comboDefaultCategory->getSelected(); d->m_payee.setDefaultAccountId(temp); } } MyMoneyFile::instance()->modifyPayee(d->m_payee); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(this, i18n("Unable to modify payee"), QString::fromLatin1(e.what())); } } } void KPayeesView::slotSyncAddressBook() { Q_D(KPayeesView); if (d->m_payeeRows.isEmpty()) { // empty list means no syncing is pending... foreach (auto item, d->ui->m_payeesList->selectedItems()) { d->m_payeeRows.append(d->ui->m_payeesList->row(item)); // ...so initialize one } d->ui->m_payeesList->clearSelection(); // otherwise slotSelectPayee will be run after every payee update // d->ui->m_syncAddressbook->setEnabled(false); // disallow concurrent syncs } if (d->m_payeeRows.count() <= d->m_payeeRow) { if (auto item = dynamic_cast(d->ui->m_payeesList->currentItem())) { // update ui if something is selected d->m_payee = item->payee(); d->ui->addressEdit->setText(d->m_payee.address()); d->ui->postcodeEdit->setText(d->m_payee.postcode()); d->ui->telephoneEdit->setText(d->m_payee.telephone()); } d->m_payeeRows.clear(); // that means end of sync d->m_payeeRow = 0; return; } if (auto item = dynamic_cast(d->ui->m_payeesList->item(d->m_payeeRows.at(d->m_payeeRow)))) d->m_payee = item->payee(); ++d->m_payeeRow; d->m_contact->fetchContact(d->m_payee.email()); // search for payee's data in addressbook and receive it in slotContactFetched } void KPayeesView::slotContactFetched(const ContactData &identity) { Q_D(KPayeesView); if (!identity.email.isEmpty()) { // empty e-mail means no identity fetched QString txt; if (!identity.street.isEmpty()) txt.append(identity.street + '\n'); if (!identity.locality.isEmpty()) { txt.append(identity.locality); if (!identity.postalCode.isEmpty()) txt.append(' ' + identity.postalCode + '\n'); else txt.append('\n'); } if (!identity.country.isEmpty()) txt.append(identity.country + '\n'); if (!txt.isEmpty() && d->m_payee.address().compare(txt) != 0) d->m_payee.setAddress(txt); if (!identity.postalCode.isEmpty() && d->m_payee.postcode().compare(identity.postalCode) != 0) d->m_payee.setPostcode(identity.postalCode); if (!identity.phoneNumber.isEmpty() && d->m_payee.telephone().compare(identity.phoneNumber) != 0) d->m_payee.setTelephone(identity.phoneNumber); MyMoneyFileTransaction ft; try { MyMoneyFile::instance()->modifyPayee(d->m_payee); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(this, i18n("Unable to modify payee"), QString::fromLatin1(e.what())); } } slotSyncAddressBook(); // process next payee } void KPayeesView::slotSendMail() { Q_D(KPayeesView); QRegularExpression re(".+@.+"); if (re.match(d->m_payee.email()).hasMatch()) QDesktopServices::openUrl(QUrl(QStringLiteral("mailto:?to=") + d->m_payee.email(), QUrl::TolerantMode)); } void KPayeesView::executeCustomAction(eView::Action action) { switch(action) { case eView::Action::Refresh: refresh(); break; case eView::Action::SetDefaultFocus: { Q_D(KPayeesView); QTimer::singleShot(0, d->m_searchWidget, SLOT(setFocus())); } break; case eView::Action::ClosePayeeIdentifierSource: slotClosePayeeIdentifierSource(); break; default: break; } } void KPayeesView::refresh() { Q_D(KPayeesView); if (isVisible()) { if (d->m_inSelection) { QTimer::singleShot(0, this, SLOT(refresh())); } else { d->loadPayees(); d->m_needsRefresh = false; } } else { d->m_needsRefresh = true; } } void KPayeesView::showEvent(QShowEvent* event) { Q_D(KPayeesView); if (d->m_needLoad) d->init(); emit customActionRequested(View::Payees, eView::Action::AboutToShow); if (d->m_needsRefresh) refresh(); // don't forget base class implementation QWidget::showEvent(event); QList list; d->selectedPayees(list); emit selectObjects(list); } void KPayeesView::updatePayeeActions(const QList &payees) { pActions[eMenu::Action::NewPayee]->setEnabled(true); const auto payeesCount = payees.count(); auto b = payeesCount == 1 ? true : false; pActions[eMenu::Action::RenamePayee]->setEnabled(b); b = payeesCount > 1 ? true : false; pActions[eMenu::Action::MergePayee]->setEnabled(b); b = payeesCount == 0 ? false : true; pActions[eMenu::Action::DeletePayee]->setEnabled(b); } void KPayeesView::slotSelectTransaction() { Q_D(KPayeesView); auto list = d->ui->m_register->selectedItems(); if (!list.isEmpty()) { const auto t = dynamic_cast(list[0]); if (t) emit selectByVariant(QVariantList {QVariant(t->split().accountId()), QVariant(t->transaction().id()) }, eView::Intent::ShowTransaction); } } void KPayeesView::slotSelectPayeeAndTransaction(const QString& payeeId, const QString& accountId, const QString& transactionId) { Q_D(KPayeesView); if (!isVisible()) return; try { // clear filter d->m_searchWidget->clear(); d->m_searchWidget->updateSearch(); // deselect all other selected items QList selectedItems = d->ui->m_payeesList->selectedItems(); QList::const_iterator payeesIt = selectedItems.constBegin(); while (payeesIt != selectedItems.constEnd()) { if (auto item = dynamic_cast(*payeesIt)) item->setSelected(false); ++payeesIt; } // find the payee in the list QListWidgetItem* it; for (int i = 0; i < d->ui->m_payeesList->count(); ++i) { it = d->ui->m_payeesList->item(i); auto item = dynamic_cast(it); if (item && item->payee().id() == payeeId) { d->ui->m_payeesList->scrollToItem(it, QAbstractItemView::PositionAtCenter); d->ui->m_payeesList->setCurrentItem(it); // active item and deselect all others d->ui->m_payeesList->setCurrentRow(i, QItemSelectionModel::ClearAndSelect); // and select it //make sure the payee selection is updated and transactions are updated accordingly slotSelectPayee(); KMyMoneyRegister::RegisterItem *registerItem = 0; for (i = 0; i < d->ui->m_register->rowCount(); ++i) { registerItem = d->ui->m_register->itemAtRow(i); if (auto t = dynamic_cast(registerItem)) { if (t->transaction().id() == transactionId && t->transaction().accountReferenced(accountId)) { d->ui->m_register->selectItem(registerItem); d->ui->m_register->ensureItemVisible(registerItem); break; } } } // quit out of outer for() loop break; } } } catch (const MyMoneyException &e) { qWarning("Unexpected exception in KPayeesView::slotSelectPayeeAndTransaction %s", e.what()); } } void KPayeesView::slotShowPayeesMenu(const QPoint& /*p*/) { Q_D(KPayeesView); if (dynamic_cast(d->ui->m_payeesList->currentItem())) { slotSelectPayee(); pMenus[eMenu::Menu::Payee]->exec(QCursor::pos()); } } void KPayeesView::slotHelp() { KHelpClient::invokeHelp("details.payees"); } void KPayeesView::slotChangeFilter(int index) { Q_D(KPayeesView); //update the filter type then reload the payees list d->m_payeeFilterType = index; d->loadPayees(); } void KPayeesView::slotNewPayee() { QString id; KMyMoneyUtils::newPayee(i18n("New Payee"), id); slotSelectPayeeAndTransaction(id); } void KPayeesView::slotRenamePayee() { Q_D(KPayeesView); if (d->ui->m_payeesList->currentItem() && d->ui->m_payeesList->selectedItems().count() == 1) { slotStartRename(d->ui->m_payeesList->currentItem()); } } void KPayeesView::slotDeletePayee() { Q_D(KPayeesView); if (d->m_selectedPayeesList.isEmpty()) return; // shouldn't happen // get confirmation from user QString prompt; if (d->m_selectedPayeesList.size() == 1) prompt = i18n("

Do you really want to remove the payee %1?

", d->m_selectedPayeesList.front().name()); else prompt = i18n("Do you really want to remove all selected payees?"); if (KMessageBox::questionYesNo(this, prompt, i18n("Remove Payee")) == KMessageBox::No) return; d->payeeReassign(KPayeeReassignDlg::TypeDelete); } void KPayeesView::slotMergePayee() { Q_D(KPayeesView); if (d->m_selectedPayeesList.size() < 1) return; // shouldn't happen if (KMessageBox::questionYesNo(this, i18n("

Do you really want to merge the selected payees?"), i18n("Merge Payees")) == KMessageBox::No) return; if (d->payeeReassign(KPayeeReassignDlg::TypeMerge)) // clean selection since we just deleted the selected payees d->m_selectedPayeesList.clear(); } diff --git a/kmymoney/views/kpayeesview_p.h b/kmymoney/views/kpayeesview_p.h index f1acd8381..a011fea6f 100644 --- a/kmymoney/views/kpayeesview_p.h +++ b/kmymoney/views/kpayeesview_p.h @@ -1,761 +1,761 @@ /*************************************************************************** kpayeesview_p.h --------------- begin : Thu Jan 24 2002 copyright : (C) 2000-2002 by Michael Edwardes Javier Campos Morales Felix Rodriguez John C Thomas Baumgart Kevin Tambascio Andreas Nicolai (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. * * * ***************************************************************************/ #ifndef KPAYEESVIEW_P_H #define KPAYEESVIEW_P_H #include "kpayeesview.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include #include "ui_kpayeesview.h" #include "kmymoneyviewbase_p.h" #include "kmymoneyutils.h" #include "kmymoneymvccombo.h" #include "mymoneypayee.h" #include "mymoneyexception.h" #include "mymoneyfile.h" #include "mymoneyaccount.h" #include "mymoneymoney.h" #include "mymoneytransactionfilter.h" #include "kmymoneysettings.h" #include "kpayeereassigndlg.h" #include "models.h" #include "accountsmodel.h" #include "mymoneysecurity.h" #include "mymoneyschedule.h" #include "mymoneycontact.h" #include "mymoneyaccountloan.h" #include "mymoneysplit.h" #include "mymoneyprice.h" #include "mymoneytransaction.h" #include "icons/icons.h" #include "transaction.h" #include "widgetenums.h" #include "mymoneyenums.h" #include "modelenums.h" #include "menuenums.h" using namespace Icons; // *** KPayeeListItem Implementation *** class KPayeeListItem : public QListWidgetItem { public: /** * Constructor to be used to construct a payee entry object. * * @param parent pointer to the QListWidget object this entry should be * added to. * @param payee const reference to MyMoneyPayee for which * the QListWidget entry is constructed */ explicit KPayeeListItem(QListWidget *parent, const MyMoneyPayee& payee) : QListWidgetItem(parent, QListWidgetItem::UserType), m_payee(payee) { setText(payee.name()); // allow in column rename setFlags(Qt::ItemIsEditable | Qt::ItemIsSelectable | Qt::ItemIsEnabled); } ~KPayeeListItem() { } const MyMoneyPayee& payee() const { return m_payee; } private: MyMoneyPayee m_payee; }; enum filterTypeE { eAllPayees = 0, eReferencedPayees = 1, eUnusedPayees = 2 }; class KPayeesViewPrivate : public KMyMoneyViewBasePrivate { Q_DECLARE_PUBLIC(KPayeesView) public: explicit KPayeesViewPrivate(KPayeesView *qq) : KMyMoneyViewBasePrivate(), q_ptr(qq), ui(new Ui::KPayeesView), m_contact(nullptr), m_payeeRow(0), m_needLoad(true), m_searchWidget(nullptr), m_inSelection(false), m_allowEditing(true), m_payeeFilterType(0), m_filterProxyModel(nullptr) { } ~KPayeesViewPrivate() { if(!m_needLoad) { // remember the splitter settings for startup auto grp = KSharedConfig::openConfig()->group("Last Use Settings"); grp.writeEntry("KPayeesViewSplitterSize", ui->m_splitter->saveState()); grp.sync(); } delete ui; } void init() { Q_Q(KPayeesView); m_needLoad = false; ui->setupUi(q); m_contact = new MyMoneyContact(q); m_filterProxyModel = new AccountNamesFilterProxyModel(q); m_filterProxyModel->setHideEquityAccounts(!KMyMoneySettings::expertMode()); m_filterProxyModel->addAccountGroup(QVector {eMyMoney::Account::Type::Asset, eMyMoney::Account::Type::Liability, eMyMoney::Account::Type::Income, eMyMoney::Account::Type::Expense, eMyMoney::Account::Type::Equity}); auto const model = Models::instance()->accountsModel(); m_filterProxyModel->setSourceModel(model); m_filterProxyModel->setSourceColumns(model->getColumns()); m_filterProxyModel->sort((int)eAccountsModel::Column::Account); ui->comboDefaultCategory->setModel(m_filterProxyModel); - ui->matchTypeCombo->addItem(i18nc("@item No matching", "No matching"), MyMoneyPayee::matchDisabled); - ui->matchTypeCombo->addItem(i18nc("@item Match Payees name partially", "Match Payees name (partial)"), MyMoneyPayee::matchName); - ui->matchTypeCombo->addItem(i18nc("@item Match Payees name exactly", "Match Payees name (exact)"), MyMoneyPayee::matchNameExact); - ui->matchTypeCombo->addItem(i18nc("@item Search match in list", "Match on a name listed below"), MyMoneyPayee::matchKey); + ui->matchTypeCombo->addItem(i18nc("@item No matching", "No matching"), static_cast(eMyMoney::Payee::MatchType::Disabled)); + ui->matchTypeCombo->addItem(i18nc("@item Match Payees name partially", "Match Payees name (partial)"), static_cast(eMyMoney::Payee::MatchType::Name)); + ui->matchTypeCombo->addItem(i18nc("@item Match Payees name exactly", "Match Payees name (exact)"), static_cast(eMyMoney::Payee::MatchType::NameExact)); + ui->matchTypeCombo->addItem(i18nc("@item Search match in list", "Match on a name listed below"), static_cast(eMyMoney::Payee::MatchType::Key)); // create the searchline widget // and insert it into the existing layout m_searchWidget = new KListWidgetSearchLine(q, ui->m_payeesList); m_searchWidget->setSizePolicy(QSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed)); ui->m_payeesList->setContextMenuPolicy(Qt::CustomContextMenu); ui->m_listTopHLayout->insertWidget(0, m_searchWidget); //load the filter type ui->m_filterBox->addItem(i18nc("@item Show all payees", "All")); ui->m_filterBox->addItem(i18nc("@item Show only used payees", "Used")); ui->m_filterBox->addItem(i18nc("@item Show only unused payees", "Unused")); ui->m_filterBox->setSizeAdjustPolicy(QComboBox::AdjustToContents); ui->m_newButton->setIcon(Icons::get(Icon::ListAddUser)); ui->m_renameButton->setIcon(Icons::get(Icon::UserProperties)); ui->m_deleteButton->setIcon(Icons::get(Icon::ListRemoveUser)); ui->m_mergeButton->setIcon(Icons::get(Icon::Merge)); ui->m_updateButton->setIcon(Icons::get(Icon::DialogOK)); ui->m_syncAddressbook->setIcon(Icons::get(Icon::Refresh)); ui->m_sendMail->setIcon(Icons::get(Icon::MailMessage)); ui->m_updateButton->setEnabled(false); ui->m_syncAddressbook->setEnabled(false); #ifndef KMM_ADDRESSBOOK_FOUND ui->m_syncAddressbook->hide(); #endif ui->matchTypeCombo->setCurrentIndex(0); ui->checkMatchIgnoreCase->setEnabled(false); ui->checkEnableDefaultCategory->setChecked(false); ui->labelDefaultCategory->setEnabled(false); ui->comboDefaultCategory->setEnabled(false); QList cols { eWidgets::eTransaction::Column::Date, eWidgets::eTransaction::Column::Account, eWidgets::eTransaction::Column::Detail, eWidgets::eTransaction::Column::ReconcileFlag, eWidgets::eTransaction::Column::Payment, eWidgets::eTransaction::Column::Deposit}; ui->m_register->setupRegister(MyMoneyAccount(), cols); ui->m_register->setSelectionMode(QTableWidget::SingleSelection); ui->m_register->setDetailsColumnType(eWidgets::eRegister::DetailColumn::AccountFirst); ui->m_balanceLabel->hide(); q->connect(m_contact, &MyMoneyContact::contactFetched, q, &KPayeesView::slotContactFetched); q->connect(ui->m_payeesList, static_cast(&QListWidget::currentItemChanged), q, static_cast(&KPayeesView::slotSelectPayee)); q->connect(ui->m_payeesList, &QListWidget::itemSelectionChanged, q, static_cast(&KPayeesView::slotSelectPayee)); q->connect(ui->m_payeesList, &QListWidget::itemDoubleClicked, q, &KPayeesView::slotStartRename); q->connect(ui->m_payeesList, &QListWidget::itemChanged, q, &KPayeesView::slotRenameSinglePayee); q->connect(ui->m_payeesList, &QWidget::customContextMenuRequested, q, &KPayeesView::slotShowPayeesMenu); q->connect(ui->m_newButton, &QAbstractButton::clicked, q, &KPayeesView::slotNewPayee); q->connect(ui->m_renameButton, &QAbstractButton::clicked, q, &KPayeesView::slotRenamePayee); q->connect(ui->m_deleteButton, &QAbstractButton::clicked, q, &KPayeesView::slotDeletePayee); q->connect(ui->m_mergeButton, &QAbstractButton::clicked, q, &KPayeesView::slotMergePayee); q->connect(ui->addressEdit, &QTextEdit::textChanged, q, &KPayeesView::slotPayeeDataChanged); q->connect(ui->postcodeEdit, &QLineEdit::textChanged, q, &KPayeesView::slotPayeeDataChanged); q->connect(ui->telephoneEdit, &QLineEdit::textChanged, q, &KPayeesView::slotPayeeDataChanged); q->connect(ui->emailEdit, &QLineEdit::textChanged, q, &KPayeesView::slotPayeeDataChanged); q->connect(ui->notesEdit, &QTextEdit::textChanged, q, &KPayeesView::slotPayeeDataChanged); q->connect(ui->matchKeyEditList, &KEditListWidget::changed, q, &KPayeesView::slotKeyListChanged); q->connect(ui->matchTypeCombo, static_cast(&QComboBox::currentIndexChanged), q, &KPayeesView::slotPayeeDataChanged); q->connect(ui->checkMatchIgnoreCase, &QAbstractButton::toggled, q, &KPayeesView::slotPayeeDataChanged); q->connect(ui->checkEnableDefaultCategory, &QAbstractButton::toggled, q, &KPayeesView::slotPayeeDataChanged); q->connect(ui->comboDefaultCategory, &KMyMoneyAccountCombo::accountSelected, q, &KPayeesView::slotPayeeDataChanged); q->connect(ui->buttonSuggestACategory, &QAbstractButton::clicked, q, &KPayeesView::slotChooseDefaultAccount); q->connect(ui->m_updateButton, &QAbstractButton::clicked, q, &KPayeesView::slotUpdatePayee); q->connect(ui->m_syncAddressbook, &QAbstractButton::clicked, q, &KPayeesView::slotSyncAddressBook); q->connect(ui->m_helpButton, &QAbstractButton::clicked, q, &KPayeesView::slotHelp); q->connect(ui->m_sendMail, &QAbstractButton::clicked, q, &KPayeesView::slotSendMail); q->connect(ui->m_register, &KMyMoneyRegister::Register::editTransaction, q, &KPayeesView::slotSelectTransaction); q->connect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, q, &KPayeesView::refresh); q->connect(ui->m_filterBox, static_cast(&QComboBox::currentIndexChanged), q, &KPayeesView::slotChangeFilter); q->connect(ui->payeeIdentifiers, &KPayeeIdentifierView::dataChanged, q, &KPayeesView::slotPayeeDataChanged); // use the size settings of the last run (if any) KConfigGroup grp = KSharedConfig::openConfig()->group("Last Use Settings"); ui->m_splitter->restoreState(grp.readEntry("KPayeesViewSplitterSize", QByteArray())); ui->m_splitter->setChildrenCollapsible(false); //At start we haven't any payee selected ui->m_tabWidget->setEnabled(false); // disable tab widget ui->m_deleteButton->setEnabled(false); //disable delete, rename and merge buttons ui->m_renameButton->setEnabled(false); ui->m_mergeButton->setEnabled(false); m_payee = MyMoneyPayee(); // make sure we don't access an undefined payee clearItemData(); } void loadPayees() { Q_Q(KPayeesView); if (m_inSelection) return; QMap isSelected; QString id; MyMoneyFile* file = MyMoneyFile::instance(); // remember which items are selected in the list QList selectedItems = ui->m_payeesList->selectedItems(); QList::const_iterator payeesIt = selectedItems.constBegin(); while (payeesIt != selectedItems.constEnd()) { KPayeeListItem* item = dynamic_cast(*payeesIt); if (item) isSelected[item->payee().id()] = true; ++payeesIt; } // keep current selected item KPayeeListItem *currentItem = static_cast(ui->m_payeesList->currentItem()); if (currentItem) id = currentItem->payee().id(); m_allowEditing = false; // clear the list m_searchWidget->clear(); m_searchWidget->updateSearch(); ui->m_payeesList->clear(); ui->m_register->clear(); currentItem = 0; QListlist = file->payeeList(); QList::ConstIterator it; for (it = list.constBegin(); it != list.constEnd(); ++it) { if (m_payeeFilterType == eAllPayees || (m_payeeFilterType == eReferencedPayees && file->isReferenced(*it)) || (m_payeeFilterType == eUnusedPayees && !file->isReferenced(*it))) { KPayeeListItem* item = new KPayeeListItem(ui->m_payeesList, *it); if (item->payee().id() == id) currentItem = item; if (isSelected[item->payee().id()]) item->setSelected(true); } } ui->m_payeesList->sortItems(); if (currentItem) { ui->m_payeesList->setCurrentItem(currentItem); ui->m_payeesList->scrollToItem(currentItem); } m_filterProxyModel->invalidate(); ui->comboDefaultCategory->expandAll(); q->slotSelectPayee(0, 0); m_allowEditing = true; } void selectedPayees(QList& payeesList) const { QList selectedItems = ui->m_payeesList->selectedItems(); QList::ConstIterator itemsIt = selectedItems.constBegin(); while (itemsIt != selectedItems.constEnd()) { KPayeeListItem* item = dynamic_cast(*itemsIt); if (item) payeesList << item->payee(); ++itemsIt; } } void ensurePayeeVisible(const QString& id) { for (int i = 0; i < ui->m_payeesList->count(); ++i) { KPayeeListItem* p = dynamic_cast(ui->m_payeesList->item(0)); if (p && p->payee().id() == id) { ui->m_payeesList->scrollToItem(p, QAbstractItemView::PositionAtCenter); ui->m_payeesList->setCurrentItem(p); // active item and deselect all others ui->m_payeesList->setCurrentRow(i, QItemSelectionModel::ClearAndSelect); // and select it break; } } } void clearItemData() { ui->addressEdit->setText(QString()); ui->postcodeEdit->setText(QString()); ui->telephoneEdit->setText(QString()); ui->emailEdit->setText(QString()); ui->notesEdit->setText(QString()); showTransactions(); } /** * This method loads the m_transactionList, clears * the m_TransactionPtrVector and rebuilds and sorts * it according to the current settings. Then it * loads the m_transactionView with the transaction data. */ void showTransactions() { MyMoneyMoney balance; const auto file = MyMoneyFile::instance(); MyMoneySecurity base = file->baseCurrency(); // setup sort order ui->m_register->setSortOrder(KMyMoneySettings::sortSearchView()); // clear the register ui->m_register->clear(); if (m_selectedPayeesList.isEmpty() || !ui->m_tabWidget->isEnabled()) { ui->m_balanceLabel->setText(i18n("Balance: %1", balance.formatMoney(file->baseCurrency().smallestAccountFraction()))); return; } // setup the list and the pointer vector MyMoneyTransactionFilter filter; for (QList::const_iterator it = m_selectedPayeesList.constBegin(); it != m_selectedPayeesList.constEnd(); ++it) filter.addPayee((*it).id()); filter.setDateFilter(KMyMoneySettings::startDate().date(), QDate()); // retrieve the list from the engine file->transactionList(m_transactionList, filter); // create the elements for the register QList >::const_iterator it; QMap uniqueMap; MyMoneyMoney deposit, payment; int splitCount = 0; bool balanceAccurate = true; for (it = m_transactionList.constBegin(); it != m_transactionList.constEnd(); ++it) { const MyMoneySplit& split = (*it).second; MyMoneyAccount acc = file->account(split.accountId()); ++splitCount; uniqueMap[(*it).first.id()]++; KMyMoneyRegister::Register::transactionFactory(ui->m_register, (*it).first, (*it).second, uniqueMap[(*it).first.id()]); // take care of foreign currencies MyMoneyMoney val = split.shares().abs(); if (acc.currencyId() != base.id()) { const MyMoneyPrice &price = file->price(acc.currencyId(), base.id()); // in case the price is valid, we use it. Otherwise, we keep // a flag that tells us that the balance is somewhat inaccurate if (price.isValid()) { val *= price.rate(base.id()); } else { balanceAccurate = false; } } if (split.shares().isNegative()) { payment += val; } else { deposit += val; } } balance = deposit - payment; // add the group markers ui->m_register->addGroupMarkers(); // sort the transactions according to the sort setting ui->m_register->sortItems(); // remove trailing and adjacent markers ui->m_register->removeUnwantedGroupMarkers(); ui->m_register->updateRegister(true); // we might end up here with updates disabled on the register so // make sure that we enable updates here ui->m_register->setUpdatesEnabled(true); ui->m_balanceLabel->setText(i18n("Balance: %1%2", balanceAccurate ? "" : "~", balance.formatMoney(file->baseCurrency().smallestAccountFraction()))); } /** * Implement common task when deleting or merging payees */ bool payeeReassign(int type) { Q_Q(KPayeesView); if (!(type >= 0 && type < KPayeeReassignDlg::TypeCount)) return false; const auto file = MyMoneyFile::instance(); MyMoneyFileTransaction ft; try { // create a transaction filter that contains all payees selected for removal MyMoneyTransactionFilter f = MyMoneyTransactionFilter(); for (QList::const_iterator it = m_selectedPayeesList.constBegin(); it != m_selectedPayeesList.constEnd(); ++it) { f.addPayee((*it).id()); } // request a list of all transactions that still use the payees in question QList translist = file->transactionList(f); // qDebug() << "[KPayeesView::slotDeletePayee] " << translist.count() << " transaction still assigned to payees"; // now get a list of all schedules that make use of one of the payees QList used_schedules; foreach (const auto schedule, file->scheduleList()) { // loop over all splits in the transaction of the schedule foreach (const auto split, schedule.transaction().splits()) { // is the payee in the split to be deleted? if (payeeInList(m_selectedPayeesList, split.payeeId())) { used_schedules.push_back(schedule); // remember this schedule break; } } } // qDebug() << "[KPayeesView::slotDeletePayee] " << used_schedules.count() << " schedules use one of the selected payees"; // and a list of all loan accounts that references one of the payees QList allAccounts; QList usedAccounts; file->accountList(allAccounts); foreach (const MyMoneyAccount &account, allAccounts) { if (account.isLoan()) { MyMoneyAccountLoan loanAccount(account); foreach (const MyMoneyPayee &payee, m_selectedPayeesList) { if (loanAccount.hasReferenceTo(payee.id())) { usedAccounts.append(account); } } } } MyMoneyPayee newPayee; bool addToMatchList = false; // if at least one payee is still referenced, we need to reassign its transactions first if (!translist.isEmpty() || !used_schedules.isEmpty() || !usedAccounts.isEmpty()) { // first create list with all non-selected payees QList remainingPayees; if (type == KPayeeReassignDlg::TypeMerge) { remainingPayees = m_selectedPayeesList; } else { remainingPayees = file->payeeList(); QList::iterator it_p; for (it_p = remainingPayees.begin(); it_p != remainingPayees.end();) { if (m_selectedPayeesList.contains(*it_p)) { it_p = remainingPayees.erase(it_p); } else { ++it_p; } } } // show error message if no payees remain if (remainingPayees.isEmpty()) { KMessageBox::sorry(q, i18n("At least one transaction/scheduled transaction or loan account is still referenced by a payee. " "Currently you have all payees selected. However, at least one payee must remain so " "that the transaction/scheduled transaction or loan account can be reassigned.")); return false; } // show transaction reassignment dialog KPayeeReassignDlg * dlg = new KPayeeReassignDlg(static_cast(type), q); KMyMoneyMVCCombo::setSubstringSearchForChildren(dlg, !KMyMoneySettings::stringMatchFromStart()); QString payee_id = dlg->show(remainingPayees); addToMatchList = dlg->addToMatchList(); delete dlg; // and kill the dialog if (payee_id.isEmpty()) return false; // the user aborted the dialog, so let's abort as well // try to get selected payee. If not possible and we are merging payees, // then we create a new one try { newPayee = file->payee(payee_id); } catch (const MyMoneyException &) { if (type == KPayeeReassignDlg::TypeMerge) { // it's ok to use payee_id for both arguments since the first is const, // so it's garantee not to change its content if (!KMyMoneyUtils::newPayee(payee_id, payee_id)) return false; // the user aborted the dialog, so let's abort as well newPayee = file->payee(payee_id); } else { return false; } } // TODO : check if we have a report that explicitively uses one of our payees // and issue an appropriate warning try { // now loop over all transactions and reassign payee for (auto& transaction : translist) { // create a copy of the splits list in the transaction // loop over all splits for (auto& split : transaction.splits()) { // if the split is assigned to one of the selected payees, we need to modify it if (payeeInList(m_selectedPayeesList, split.payeeId())) { split.setPayeeId(payee_id); // first modify payee in current split // then modify the split in our local copy of the transaction list transaction.modifySplit(split); // this does not modify the list object 'splits'! } } // for - Splits file->modifyTransaction(transaction); // modify the transaction in the MyMoney object } // for - Transactions // now loop over all schedules and reassign payees for (auto& schedule : used_schedules) { // create copy of transaction in current schedule auto trans = schedule.transaction(); // create copy of lists of splits for (auto& split : trans.splits()) { if (payeeInList(m_selectedPayeesList, split.payeeId())) { split.setPayeeId(payee_id); trans.modifySplit(split); // does not modify the list object 'splits'! } } // for - Splits // store transaction in current schedule schedule.setTransaction(trans); file->modifySchedule(schedule); // modify the schedule in the MyMoney engine } // for - Schedules // reassign the payees in the loans that reference the deleted payees foreach (const MyMoneyAccount &account, usedAccounts) { MyMoneyAccountLoan loanAccount(account); loanAccount.setPayee(payee_id); file->modifyAccount(loanAccount); } } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(q, i18n("Unable to reassign payee of transaction/split"), e.what()); } } else { // if !translist.isEmpty() if (type == KPayeeReassignDlg::TypeMerge) { KMessageBox::sorry(q, i18n("Nothing to merge."), i18n("Merge Payees")); return false; } } bool ignorecase; QStringList payeeNames; - MyMoneyPayee::payeeMatchType matchType = newPayee.matchData(ignorecase, payeeNames); + auto matchType = newPayee.matchData(ignorecase, payeeNames); QStringList deletedPayeeNames; // now loop over all selected payees and remove them for (QList::iterator it = m_selectedPayeesList.begin(); it != m_selectedPayeesList.end(); ++it) { if (newPayee.id() != (*it).id()) { if (addToMatchList) { deletedPayeeNames << (*it).name(); } file->removePayee(*it); } } // if we initially have no matching turned on, we just ignore the case (default) - if (matchType == MyMoneyPayee::matchDisabled) + if (matchType == eMyMoney::Payee::MatchType::Disabled) ignorecase = true; // update the destination payee if this was requested by the user if (addToMatchList && deletedPayeeNames.count() > 0) { // add new names to the list // TODO: it would be cool to somehow shrink the list to make better use // of regular expressions at this point. For now, we leave this task // to the user himeself. QStringList::const_iterator it_n; for (it_n = deletedPayeeNames.constBegin(); it_n != deletedPayeeNames.constEnd(); ++it_n) { - if (matchType == MyMoneyPayee::matchKey) { + if (matchType == eMyMoney::Payee::MatchType::Key) { // make sure we really need it and it is not caught by an existing regexp QStringList::const_iterator it_k; for (it_k = payeeNames.constBegin(); it_k != payeeNames.constEnd(); ++it_k) { QRegExp exp(*it_k, ignorecase ? Qt::CaseInsensitive : Qt::CaseSensitive); if (exp.indexIn(*it_n) != -1) break; } if (it_k == payeeNames.constEnd()) payeeNames << QRegExp::escape(*it_n); } else if (payeeNames.contains(*it_n) == 0) payeeNames << QRegExp::escape(*it_n); } // and update the payee in the engine context // make sure to turn on matching for this payee in the right mode - newPayee.setMatchData(MyMoneyPayee::matchKey, ignorecase, payeeNames); + newPayee.setMatchData(eMyMoney::Payee::MatchType::Key, ignorecase, payeeNames); file->modifyPayee(newPayee); } ft.commit(); // If we just deleted the payees, they sure don't exist anymore m_selectedPayeesList.clear(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(q, i18n("Unable to remove payee(s)"), e.what()); } return true; } /** * Check if a list contains a payee with a given id * * @param list const reference to value list * @param id const reference to id * * @retval true object has been found * @retval false object is not in list */ bool payeeInList(const QList& list, const QString& id) const { bool rc = false; QList::const_iterator it_p = list.begin(); while (it_p != list.end()) { if ((*it_p).id() == id) { rc = true; break; } ++it_p; } return rc; } /** Checks whether the currently selected payee is "dirty" * @return true, if payee is modified (is "dirty"); false otherwise */ bool isDirty() const { return ui->m_updateButton->isEnabled(); } /** Sets the payee's "dirty" (modified) status * @param dirty if true (default), payee will be set to dirty */ void setDirty(bool dirty) { ui->m_updateButton->setEnabled(dirty); } KPayeesView *q_ptr; Ui::KPayeesView *ui; MyMoneyPayee m_payee; QString m_newName; MyMoneyContact *m_contact; int m_payeeRow; QList m_payeeRows; /** * List of selected payees */ QList m_selectedPayeesList; /** * q member holds a list of all transactions */ QList > m_transactionList; /** * q member holds the load state of page */ bool m_needLoad; /** * Search widget for the list */ KListWidgetSearchLine* m_searchWidget; /** * Semaphore to suppress loading during selection */ bool m_inSelection; /** * q signals whether a payee can be edited **/ bool m_allowEditing; /** * q holds the filter type */ int m_payeeFilterType; AccountNamesFilterProxyModel *m_filterProxyModel; }; #endif diff --git a/kmymoney/widgets/budgetviewproxymodel.cpp b/kmymoney/widgets/budgetviewproxymodel.cpp index 1c8388612..8fd98d262 100644 --- a/kmymoney/widgets/budgetviewproxymodel.cpp +++ b/kmymoney/widgets/budgetviewproxymodel.cpp @@ -1,254 +1,254 @@ /* * Copyright 2006 Darren Gould * Copyright 2009-2014 Alvaro Soliverez * 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 "budgetviewproxymodel.h" #include "accountsviewproxymodel_p.h" // ---------------------------------------------------------------------------- // QT Includes // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes #include "mymoneyutils.h" #include "mymoneyfile.h" #include "mymoneyaccount.h" #include "mymoneysecurity.h" #include "mymoneymoney.h" #include "mymoneybudget.h" #include "models.h" #include "accountsmodel.h" #include "modelenums.h" using namespace eAccountsModel; class BudgetViewProxyModelPrivate : public AccountsViewProxyModelPrivate { Q_DISABLE_COPY(BudgetViewProxyModelPrivate) public: BudgetViewProxyModelPrivate() : AccountsViewProxyModelPrivate() { } ~BudgetViewProxyModelPrivate() override { } MyMoneyBudget m_budget; MyMoneyMoney m_lastBalance; }; BudgetViewProxyModel::BudgetViewProxyModel(QObject *parent) : AccountsViewProxyModel(*new BudgetViewProxyModelPrivate, parent) { } BudgetViewProxyModel::~BudgetViewProxyModel() { } /** * This function was reimplemented to add the data needed by the other columns that this model * is adding besides the columns of the @ref AccountsModel. */ QVariant BudgetViewProxyModel::data(const QModelIndex &index, int role) const { Q_D(const BudgetViewProxyModel); if (!MyMoneyFile::instance()->storageAttached()) return QVariant(); const auto sourceColumn = d->m_mdlColumns->at(mapToSource(index).column()); static QVector columnsToProcess {Column::TotalBalance, Column::TotalValue/*, AccountsModel::PostedValue, Column::Account*/}; if (columnsToProcess.contains(sourceColumn)) { const auto ixAccount = mapToSource(BudgetViewProxyModel::index(index.row(), static_cast(Column::Account), index.parent())); const auto account = ixAccount.data((int)Role::Account).value(); auto const file = MyMoneyFile::instance(); switch (role) { case Qt::DisplayRole: { switch (sourceColumn) { case Column::TotalBalance: if (file->security(account.currencyId()) != file->baseCurrency()) return QVariant(MyMoneyUtils::formatMoney(accountBalance(account.id()), file->security(account.currencyId()))); else return QVariant(); case Column::TotalValue: return QVariant(MyMoneyUtils::formatMoney(computeTotalValue(ixAccount), file->baseCurrency())); // FIXME: Posted value doesn't correspond with total value without below code. Investigate why and wheather it matters. // case AccountsModel::PostedValue: // return QVariant(MyMoneyUtils::formatMoney(accountValue(account, accountBalance(account.id())), file->baseCurrency())); default: break; } break; } case (int)Role::Balance: if (file->security(account.currencyId()) != file->baseCurrency()) return QVariant::fromValue(accountBalance(account.id())); else return QVariant(); case (int)Role::TotalValue: return QVariant::fromValue(computeTotalValue(ixAccount)); case (int)Role::Value: return QVariant::fromValue(accountValue(account, accountBalance(account.id()))); default: break; } } return AccountsViewProxyModel::data(index, role); } Qt::ItemFlags BudgetViewProxyModel::flags(const QModelIndex &index) const { Q_D(const BudgetViewProxyModel); Qt::ItemFlags flags = AccountsViewProxyModel::flags(index); if (!index.parent().isValid()) return flags & ~Qt::ItemIsSelectable; // check if any of the parent accounts has the 'include subaccounts' // flag set. If so, we don't allow selecting this account QModelIndex idx = index.parent(); while (idx.isValid()) { QModelIndex source_idx = mapToSource(idx); QVariant accountData = sourceModel()->data(source_idx, (int)Role::Account); if (accountData.canConvert()) { MyMoneyAccount account = accountData.value(); // find out if the account is budgeted MyMoneyBudget::AccountGroup budgetAccount = d->m_budget.account(account.id()); if (budgetAccount.id() == account.id()) { if (budgetAccount.budgetSubaccounts()) { return flags & ~Qt::ItemIsEnabled; } } } idx = idx.parent(); } return flags; } void BudgetViewProxyModel::setBudget(const MyMoneyBudget& budget) { Q_D(BudgetViewProxyModel); d->m_budget = budget; invalidate(); checkBalance(); } bool BudgetViewProxyModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const { Q_D(const BudgetViewProxyModel); if (hideUnusedIncomeExpenseAccounts()) { const auto index = sourceModel()->index(source_row, static_cast(Column::Account), source_parent); const auto accountData = sourceModel()->data(index, (int)Role::Account); if (accountData.canConvert()) { const auto account = accountData.value(); MyMoneyMoney balance; // find out if the account is budgeted const auto budgetAccount = d->m_budget.account(account.id()); if (budgetAccount.id() == account.id()) { balance = budgetAccount.balance(); switch (budgetAccount.budgetLevel()) { - case MyMoneyBudget::AccountGroup::eMonthly: + case eMyMoney::Budget::Level::Monthly: balance *= MyMoneyMoney(12); break; default: break; } } if (!balance.isZero()) return AccountsProxyModel::filterAcceptsRow(source_row, source_parent); } for (auto i = 0; i < sourceModel()->rowCount(index); ++i) { if (filterAcceptsRow(i, index)) return AccountsProxyModel::filterAcceptsRow(i, index); } return false; } return AccountsProxyModel::filterAcceptsRow(source_row, source_parent); } MyMoneyMoney BudgetViewProxyModel::accountBalance(const QString &accountId) const { Q_D(const BudgetViewProxyModel); MyMoneyMoney balance; // find out if the account is budgeted MyMoneyBudget::AccountGroup budgetAccount = d->m_budget.account(accountId); if (budgetAccount.id() == accountId) { balance = budgetAccount.balance(); switch (budgetAccount.budgetLevel()) { - case MyMoneyBudget::AccountGroup::eMonthly: + case eMyMoney::Budget::Level::Monthly: balance *= MyMoneyMoney(12); break; default: break; } } return balance; } MyMoneyMoney BudgetViewProxyModel::accountValue(const MyMoneyAccount &account, const MyMoneyMoney &balance) const { return Models::instance()->accountsModel()->accountValue(account, balance); } MyMoneyMoney BudgetViewProxyModel::computeTotalValue(const QModelIndex &source_index) const { auto model = sourceModel(); auto account = model->data(source_index, (int)Role::Account).value(); auto totalValue = accountValue(account, accountBalance(account.id())); for (auto i = 0; i < model->rowCount(source_index); ++i) totalValue += computeTotalValue(model->index(i, static_cast(Column::Account), source_index)); return totalValue; } void BudgetViewProxyModel::checkBalance() { Q_D(BudgetViewProxyModel); // compute the balance QModelIndexList incomeList = match(index(0, 0), (int)Role::ID, MyMoneyFile::instance()->income().id(), 1, Qt::MatchFlags(Qt::MatchExactly | Qt::MatchCaseSensitive)); QModelIndexList expenseList = match(index(0, 0), (int)Role::ID, MyMoneyFile::instance()->expense().id(), 1, Qt::MatchFlags(Qt::MatchExactly | Qt::MatchCaseSensitive)); MyMoneyMoney balance; if (!incomeList.isEmpty() && !expenseList.isEmpty()) { QVariant incomeValue = data(incomeList.front(), (int)Role::TotalValue); QVariant expenseValue = data(expenseList.front(), (int)Role::TotalValue); if (incomeValue.isValid() && expenseValue.isValid()) { balance = incomeValue.value() - expenseValue.value(); } } if (d->m_lastBalance != balance) { d->m_lastBalance = balance; emit balanceChanged(d->m_lastBalance); } }