diff --git a/kmymoney/converter/existingtransactionmatchfinder.cpp b/kmymoney/converter/existingtransactionmatchfinder.cpp index c83747b8c..27d3706b9 100644 --- a/kmymoney/converter/existingtransactionmatchfinder.cpp +++ b/kmymoney/converter/existingtransactionmatchfinder.cpp @@ -1,58 +1,58 @@ /*************************************************************************** KMyMoney transaction importing module - searches for a matching transaction in the ledger copyright : (C) 2012 by Lukasz Maszczynski ***************************************************************************/ /*************************************************************************** * * * 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 "existingtransactionmatchfinder.h" #include #include #include "mymoneymoney.h" #include "mymoneyfile.h" #include "mymoneytransactionfilter.h" ExistingTransactionMatchFinder::ExistingTransactionMatchFinder(int matchWindow) : TransactionMatchFinder(matchWindow) { } void ExistingTransactionMatchFinder::createListOfMatchCandidates() { - MyMoneyTransactionFilter filter(importedSplit.accountId()); + MyMoneyTransactionFilter filter(m_importedSplit.accountId()); filter.setReportAllSplits(false); - filter.setDateFilter(importedTransaction.postDate().addDays(-matchWindow), importedTransaction.postDate().addDays(matchWindow)); - filter.setAmountFilter(importedSplit.shares(), importedSplit.shares()); + filter.setDateFilter(importedTransaction.postDate().addDays(-m_matchWindow), importedTransaction.postDate().addDays(m_matchWindow)); + filter.setAmountFilter(m_importedSplit.shares(), m_importedSplit.shares()); MyMoneyFile::instance()->transactionList(listOfMatchCandidates, filter); qDebug() << "Considering" << listOfMatchCandidates.size() << "existing transaction(s) for matching"; } void ExistingTransactionMatchFinder::findMatchInMatchCandidatesList() { foreach (const TransactionAndSplitPair & transactionAndSplit, listOfMatchCandidates) { const MyMoneyTransaction & theTransaction = transactionAndSplit.first; if (theTransaction.id() == importedTransaction.id()) { // just skip myself continue; } findMatchingSplit(theTransaction, 0); if (matchResult != MatchNotFound) { return; } } } diff --git a/kmymoney/converter/existingtransactionmatchfinder.h b/kmymoney/converter/existingtransactionmatchfinder.h index 72de96e15..05fd68c6f 100644 --- a/kmymoney/converter/existingtransactionmatchfinder.h +++ b/kmymoney/converter/existingtransactionmatchfinder.h @@ -1,52 +1,52 @@ /*************************************************************************** KMyMoney transaction importing module - searches for a matching transaction in the ledger copyright : (C) 2012 by Lukasz Maszczynski ***************************************************************************/ /*************************************************************************** * * * 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 EXISTINGTRANSACTIONMATCHFINDER_H #define EXISTINGTRANSACTIONMATCHFINDER_H #include #include #include "transactionmatchfinder.h" /** Implements searching for a matching transaction in the ledger */ class ExistingTransactionMatchFinder : public TransactionMatchFinder { public: /** Ctor, initializes the match finder * @param matchWindow max number of days the transactions may vary and still be considered to be matching */ - explicit ExistingTransactionMatchFinder(int matchWindow = 3); + explicit ExistingTransactionMatchFinder(int m_matchWindow = 3); protected: typedef QPair TransactionAndSplitPair; QList listOfMatchCandidates; /** Creates a list of transactions within matchWindow range and with the same amount as the imported transaction we're trying to match */ - virtual void createListOfMatchCandidates(); + void createListOfMatchCandidates() final override; /** Searches for a matching transaction in the ledger * * Match result can be set to any value described in @ref TransactionMatchFinder::findMatch(). * @ref MatchDuplicate is set if the imported transaction has the same bank id as the existing transaction; * @ref MatchImprecise is set when transaction dates are not equal, but within matchWindow range */ - virtual void findMatchInMatchCandidatesList(); + void findMatchInMatchCandidatesList() final override; }; #endif // EXISTINGTRANSACTIONMATCHFINDER_H diff --git a/kmymoney/converter/mymoneystatementreader.cpp b/kmymoney/converter/mymoneystatementreader.cpp index d944d5b2e..dabe008f4 100644 --- a/kmymoney/converter/mymoneystatementreader.cpp +++ b/kmymoney/converter/mymoneystatementreader.cpp @@ -1,1579 +1,1579 @@ /*************************************************************************** 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; - MyMoneyAccount acc = file->account(s.accountId()); + 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 ..."); - int progress = 0; + 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 (e.what() == "USERABORT") m_userAbort = true; else qDebug("Caught exception from processTransactionEntry() not caused by USERABORT: %s", qPrintable(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 (e.what() == "USERABORT") m_userAbort = true; else qDebug("Caught exception from processPriceEntry() not caused by USERABORT: %s", qPrintable(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", 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. - QString brokerageactid = d->m_account.value("kmm-brokerage-account").toUtf8(); + 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); switch (matchType) { case MyMoneyPayee::matchDisabled: break; case MyMoneyPayee::matchName: case MyMoneyPayee::matchNameExact: keys << QString("%1").arg(QRegExp::escape((*it_p).name())); if(matchType == MyMoneyPayee::matchNameExact) { keys.clear(); keys << QString("^%1$").arg(QRegExp::escape((*it_p).name())); } // intentional fall through case MyMoneyPayee::matchKey: 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("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))); 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("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(0, i18n("Unable to add payee/receiver"), i18n("%1 thrown in %2:%3", e.what(), e.file(), e.line())); } } else if (rc == KMessageBox::No) { s1.setPayeeId(QString()); } else { throw MYMONEYEXCEPTION("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("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("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 &e) { // make sure we get rid of the editor before // the KEnterScheduleDlg is destroyed delete editor; throw e; // 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/scheduledtransactionmatchfinder.cpp b/kmymoney/converter/scheduledtransactionmatchfinder.cpp index 765cb5181..7896a94c6 100644 --- a/kmymoney/converter/scheduledtransactionmatchfinder.cpp +++ b/kmymoney/converter/scheduledtransactionmatchfinder.cpp @@ -1,53 +1,53 @@ /*************************************************************************** KMyMoney transaction importing module - searches for a matching scheduled transaction copyright : (C) 2012 by Lukasz Maszczynski ***************************************************************************/ /*************************************************************************** * * * 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 "scheduledtransactionmatchfinder.h" #include #include #include "mymoneyfile.h" #include "mymoneyschedule.h" #include "kmymoneyutils.h" ScheduledTransactionMatchFinder::ScheduledTransactionMatchFinder(const MyMoneyAccount& account, int matchWindow) - : TransactionMatchFinder(matchWindow), account(account) + : TransactionMatchFinder(matchWindow), m_account(account) { } void ScheduledTransactionMatchFinder::createListOfMatchCandidates() { - listOfMatchCandidates = MyMoneyFile::instance()->scheduleList(account.id()); + listOfMatchCandidates = MyMoneyFile::instance()->scheduleList(m_account.id()); qDebug() << "Considering" << listOfMatchCandidates.size() << "schedule(s) for matching the transaction"; } void ScheduledTransactionMatchFinder::findMatchInMatchCandidatesList() { foreach (const MyMoneySchedule & schedule, listOfMatchCandidates) { QDate nextDueDate = schedule.nextDueDate(); - bool nextDueDateWithinMatchWindowRange = (nextDueDate >= importedTransaction.postDate().addDays(-matchWindow)) - && (nextDueDate <= importedTransaction.postDate().addDays(matchWindow)); + bool nextDueDateWithinMatchWindowRange = (nextDueDate >= importedTransaction.postDate().addDays(-m_matchWindow)) + && (nextDueDate <= importedTransaction.postDate().addDays(m_matchWindow)); if (schedule.isOverdue() || nextDueDateWithinMatchWindowRange) { MyMoneyTransaction scheduledTransaction = KMyMoneyUtils::scheduledTransaction(schedule); findMatchingSplit(scheduledTransaction, schedule.variation()); if (matchResult != MatchNotFound) { matchedSchedule.reset(new MyMoneySchedule(schedule)); return; } } } } diff --git a/kmymoney/converter/scheduledtransactionmatchfinder.h b/kmymoney/converter/scheduledtransactionmatchfinder.h index ba1c42209..471913bb4 100644 --- a/kmymoney/converter/scheduledtransactionmatchfinder.h +++ b/kmymoney/converter/scheduledtransactionmatchfinder.h @@ -1,53 +1,53 @@ /*************************************************************************** KMyMoney transaction importing module - searches for a matching scheduled transaction copyright : (C) 2012 by Lukasz Maszczynski ***************************************************************************/ /*************************************************************************** * * * 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 SCHEDULEDTRANSACTIONMATCHFINDER_H #define SCHEDULEDTRANSACTIONMATCHFINDER_H #include #include "transactionmatchfinder.h" #include "mymoneyaccount.h" /** Implements searching for a matching transaction in the scheduled transactions */ class ScheduledTransactionMatchFinder : public TransactionMatchFinder { public: /** Ctor, initializes the match finder * @param account the account for which the scheduled transactions shall be considered * @param matchWindow max number of days the transactions may vary and still be considered to be matching */ - ScheduledTransactionMatchFinder(const MyMoneyAccount& account, int matchWindow); + ScheduledTransactionMatchFinder(const MyMoneyAccount& m_account, int m_matchWindow); private: - MyMoneyAccount account; + MyMoneyAccount m_account; QList listOfMatchCandidates; /** Fills @ref listOfSchedules member with list of scheduled transactions for the @ref account */ - virtual void createListOfMatchCandidates(); + void createListOfMatchCandidates() final override; /** Searches for a matching transaction in the scheduled transactions * * The search result can be set to any value except @ref MatchDuplicate as described in * @ref TransactionMatchFinder::findMatch(). * @ref MatchImprecise is returned when transaction dates are not equal, but within matchWindow range */ - virtual void findMatchInMatchCandidatesList(); + void findMatchInMatchCandidatesList() final override; }; #endif // SCHEDULEDTRANSACTIONMATCHFINDER_H diff --git a/kmymoney/converter/tests/matchfinder-test.cpp b/kmymoney/converter/tests/matchfinder-test.cpp index 40b87524f..ef49b4ad3 100644 --- a/kmymoney/converter/tests/matchfinder-test.cpp +++ b/kmymoney/converter/tests/matchfinder-test.cpp @@ -1,490 +1,490 @@ /*************************************************************************** KMyMoney transaction importing module - tests for ExistingTransactionMatchFinder copyright : (C) 2012 by Lukasz Maszczynski ***************************************************************************/ /*************************************************************************** * * * 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 "matchfinder-test.h" #include "existingtransactionmatchfinder.h" #include #include "mymoneyfile.h" #include "mymoneysecurity.h" #include "mymoneymoney.h" #include "mymoneyenums.h" QTEST_GUILESS_MAIN(MatchFinderTest) MatchFinderTest::MatchFinderTest() : matchResult(TransactionMatchFinder::MatchResult::MatchNotFound) { } void MatchFinderTest::init() { file = MyMoneyFile::instance(); setupStorage(); setupCurrency(); setupAccounts(); setupPayees(); ledgerTransaction = buildDefaultTransaction(); importTransaction = buildDefaultTransaction(); existingTrFinder.reset(new ExistingTransactionMatchFinder(MATCH_WINDOW)); - schedule = buildNonOverdueSchedule(); + m_schedule = buildNonOverdueSchedule(); scheduledTrFinder.reset(new ScheduledTransactionMatchFinder(*account, MATCH_WINDOW)); } void MatchFinderTest::cleanup() { file->detachStorage(storage.data()); } void MatchFinderTest::setupStorage() { storage.reset(new MyMoneyStorageMgr); file->attachStorage(storage.data()); } void MatchFinderTest::setupCurrency() { MyMoneyFileTransaction ft; file->addCurrency(MyMoneySecurity("USD", "US Dollar", "$")); file->setBaseCurrency(file->currency("USD")); ft.commit(); } void MatchFinderTest::setupPayees() { payee.setName("John Doe"); otherPayee.setName("Jane Doe"); MyMoneyFileTransaction ft; file->addPayee(payee); file->addPayee(otherPayee); ft.commit(); } void MatchFinderTest::setupAccounts() { account.reset(new MyMoneyAccount); otherAccount.reset(new MyMoneyAccount); account->setName("Expenses account"); account->setAccountType(eMyMoney::Account::Type::Expense); account->setOpeningDate(QDate(2012, 12, 01)); account->setCurrencyId(MyMoneyFile::instance()->baseCurrency().id()); otherAccount->setName("Some other account"); otherAccount->setAccountType(eMyMoney::Account::Type::Expense); otherAccount->setOpeningDate(QDate(2012, 12, 01)); otherAccount->setCurrencyId(MyMoneyFile::instance()->baseCurrency().id()); MyMoneyFileTransaction ft; MyMoneyAccount parentAccount = file->expense(); file->addAccount(*account, parentAccount); file->addAccount(*otherAccount, parentAccount); ft.commit(); } MyMoneyTransaction MatchFinderTest::buildDefaultTransaction() const { MyMoneySplit split; split.setAccountId(account->id()); split.setBankID("#1"); split.setPayeeId(payee.id()); split.setShares(MyMoneyMoney(123.00)); MyMoneyTransaction transaction; transaction.setPostDate(QDate(2012, 12, 5)); transaction.setImported(true); transaction.addSplit(split); return transaction; } MyMoneyTransaction MatchFinderTest::buildMatchedTransaction(MyMoneyTransaction transaction) const { transaction.clearId(); MyMoneyTransaction matchingTransaction = transaction; transaction.splits().first().addMatch(matchingTransaction); return transaction; } QString MatchFinderTest::addTransactionToLedger(MyMoneyTransaction transaction) const { MyMoneyFileTransaction ft; file->addTransaction(transaction); ft.commit(); return transaction.id(); } MyMoneySchedule MatchFinderTest::buildNonOverdueSchedule() const { QDate tomorrow = QDate::currentDate().addDays(1); MyMoneyTransaction transaction = buildDefaultTransaction(); transaction.setPostDate(tomorrow); MyMoneySchedule nonOverdueSchedule("schedule name", eMyMoney::Schedule::Type::Transfer, eMyMoney::Schedule::Occurrence::Monthly, 1, eMyMoney::Schedule::PaymentType::BankTransfer, tomorrow, tomorrow.addMonths(2), false, false); nonOverdueSchedule.setTransaction(transaction); return nonOverdueSchedule; } void MatchFinderTest::addSchedule(MyMoneySchedule schedule) const { MyMoneyFileTransaction ft; file->addSchedule(schedule); ft.commit(); } void MatchFinderTest::expectMatchWithExistingTransaction(TransactionMatchFinder::MatchResult expectedResult) { matchResult = existingTrFinder->findMatch(importTransaction, importTransaction.splits().first()); QCOMPARE(matchResult, expectedResult); } void MatchFinderTest::expectMatchWithScheduledTransaction(TransactionMatchFinder::MatchResult expectedResult) { matchResult = scheduledTrFinder->findMatch(importTransaction, importTransaction.splits().first()); QCOMPARE(matchResult, expectedResult); } void MatchFinderTest::testDuplicate_allMatch() { addTransactionToLedger(ledgerTransaction); expectMatchWithExistingTransaction(TransactionMatchFinder::MatchDuplicate); } void MatchFinderTest::testDuplicate_payeeEmpty() { ledgerTransaction.splits().first().setPayeeId(payee.id()); importTransaction.splits().first().setPayeeId(""); addTransactionToLedger(ledgerTransaction); expectMatchWithExistingTransaction(TransactionMatchFinder::MatchDuplicate); } void MatchFinderTest::testDuplicate_payeeMismatch() { ledgerTransaction.splits().first().setPayeeId(payee.id()); importTransaction.splits().first().setPayeeId(otherPayee.id()); addTransactionToLedger(ledgerTransaction); expectMatchWithExistingTransaction(TransactionMatchFinder::MatchDuplicate); } void MatchFinderTest::testDuplicate_splitIsMarkedAsMatched() { ledgerTransaction = buildMatchedTransaction(ledgerTransaction); addTransactionToLedger(ledgerTransaction); expectMatchWithExistingTransaction(TransactionMatchFinder::MatchDuplicate); } void MatchFinderTest::testPreciseMatch_noBankId() { ledgerTransaction.splits().first().setBankID(""); importTransaction.splits().first().setBankID(""); addTransactionToLedger(ledgerTransaction); expectMatchWithExistingTransaction(TransactionMatchFinder::MatchPrecise); } void MatchFinderTest::testPreciseMatch_importBankId() { ledgerTransaction.splits().first().setBankID(""); importTransaction.splits().first().setBankID("#1"); addTransactionToLedger(ledgerTransaction); expectMatchWithExistingTransaction(TransactionMatchFinder::MatchPrecise); } void MatchFinderTest::testPreciseMatch_payeeEmpty() { ledgerTransaction.splits().first().setBankID(""); importTransaction.splits().first().setBankID(""); importTransaction.splits().first().setPayeeId(""); addTransactionToLedger(ledgerTransaction); expectMatchWithExistingTransaction(TransactionMatchFinder::MatchPrecise); } void MatchFinderTest::testImpreciseMatch_matchWindowLowerBound() { ledgerTransaction.splits().first().setBankID(""); importTransaction.splits().first().setBankID(""); importTransaction.setPostDate(importTransaction.postDate().addDays(-MATCH_WINDOW)); addTransactionToLedger(ledgerTransaction); expectMatchWithExistingTransaction(TransactionMatchFinder::MatchImprecise); } void MatchFinderTest::testImpreciseMatch_matchWindowUpperBound() { ledgerTransaction.splits().first().setBankID(""); importTransaction.splits().first().setBankID(""); importTransaction.setPostDate(importTransaction.postDate().addDays(MATCH_WINDOW)); addTransactionToLedger(ledgerTransaction); expectMatchWithExistingTransaction(TransactionMatchFinder::MatchImprecise); } void MatchFinderTest::testImpreciseMatch_payeeEmpty() { ledgerTransaction.splits().first().setBankID(""); importTransaction.splits().first().setBankID(""); importTransaction.splits().first().setPayeeId(""); importTransaction.setPostDate(importTransaction.postDate().addDays(1)); addTransactionToLedger(ledgerTransaction); expectMatchWithExistingTransaction(TransactionMatchFinder::MatchImprecise); } void MatchFinderTest::testNoMatch_bankIdMismatch() { ledgerTransaction.splits().first().setBankID("#1"); importTransaction.splits().first().setBankID("#2"); addTransactionToLedger(ledgerTransaction); expectMatchWithExistingTransaction(TransactionMatchFinder::MatchNotFound); } void MatchFinderTest::testNoMatch_ledgerBankIdNotEmpty() { ledgerTransaction.splits().first().setBankID("#1"); importTransaction.splits().first().setBankID(""); addTransactionToLedger(ledgerTransaction); expectMatchWithExistingTransaction(TransactionMatchFinder::MatchNotFound); } void MatchFinderTest::testNoMatch_accountMismatch_withBankId() { ledgerTransaction.splits().first().setAccountId(account->id()); importTransaction.splits().first().setAccountId(otherAccount->id()); addTransactionToLedger(ledgerTransaction); expectMatchWithExistingTransaction(TransactionMatchFinder::MatchNotFound); } void MatchFinderTest::testNoMatch_accountMismatch_noBankId() { ledgerTransaction.splits().first().setBankID(""); importTransaction.splits().first().setBankID(""); ledgerTransaction.splits().first().setAccountId(account->id()); importTransaction.splits().first().setAccountId(otherAccount->id()); addTransactionToLedger(ledgerTransaction); expectMatchWithExistingTransaction(TransactionMatchFinder::MatchNotFound); } void MatchFinderTest::testNoMatch_amountMismatch_withBankId() { ledgerTransaction.splits().first().setShares(MyMoneyMoney(1.11)); importTransaction.splits().first().setShares(MyMoneyMoney(9.99)); addTransactionToLedger(ledgerTransaction); expectMatchWithExistingTransaction(TransactionMatchFinder::MatchNotFound); } void MatchFinderTest::testNoMatch_amountMismatch_noBankId() { ledgerTransaction.splits().first().setBankID(""); importTransaction.splits().first().setBankID(""); ledgerTransaction.splits().first().setShares(MyMoneyMoney(1.11)); importTransaction.splits().first().setShares(MyMoneyMoney(9.99)); addTransactionToLedger(ledgerTransaction); expectMatchWithExistingTransaction(TransactionMatchFinder::MatchNotFound); } void MatchFinderTest::testNoMatch_payeeMismatch() { ledgerTransaction.splits().first().setBankID(""); importTransaction.splits().first().setBankID(""); ledgerTransaction.splits().first().setPayeeId(payee.id()); importTransaction.splits().first().setPayeeId(otherPayee.id()); addTransactionToLedger(ledgerTransaction); expectMatchWithExistingTransaction(TransactionMatchFinder::MatchNotFound); } void MatchFinderTest::testNoMatch_splitIsMarkedAsMatched() { ledgerTransaction.splits().first().setBankID(""); importTransaction.splits().first().setBankID(""); ledgerTransaction = buildMatchedTransaction(ledgerTransaction); addTransactionToLedger(ledgerTransaction); expectMatchWithExistingTransaction(TransactionMatchFinder::MatchNotFound); } void MatchFinderTest::testNoMatch_postDateMismatch_withBankId() { importTransaction.setPostDate(importTransaction.postDate().addDays(MATCH_WINDOW + 1)); addTransactionToLedger(ledgerTransaction); expectMatchWithExistingTransaction(TransactionMatchFinder::MatchNotFound); } void MatchFinderTest::testNoMatch_postDateMismatch_noBankId() { ledgerTransaction.splits().first().setBankID(""); importTransaction.splits().first().setBankID(""); importTransaction.setPostDate(importTransaction.postDate().addDays(MATCH_WINDOW + 1)); addTransactionToLedger(ledgerTransaction); expectMatchWithExistingTransaction(TransactionMatchFinder::MatchNotFound); } void MatchFinderTest::testExistingTransactionMatch_sameTransactionId_withBankId() { QString transactionId = addTransactionToLedger(ledgerTransaction); importTransaction = MyMoneyTransaction(transactionId, ledgerTransaction); expectMatchWithExistingTransaction(TransactionMatchFinder::MatchNotFound); } void MatchFinderTest::testExistingTransactionMatch_sameTransactionId_noBankId() { ledgerTransaction.splits().first().setBankID(""); QString transactionId = addTransactionToLedger(ledgerTransaction); importTransaction = MyMoneyTransaction(transactionId, ledgerTransaction); expectMatchWithExistingTransaction(TransactionMatchFinder::MatchNotFound); } void MatchFinderTest::testExistingTransactionMatch_multipleAccounts_withBankId() { ledgerTransaction.splits().first().setAccountId(account->id()); ledgerTransaction.splits().first().setBankID("#1"); importTransaction.splits().first().setAccountId(otherAccount->id()); importTransaction.splits().first().setBankID("#1"); MyMoneySplit secondSplit = importTransaction.splits().first(); secondSplit.clearId(); secondSplit.setAccountId(account->id()); secondSplit.setBankID("#1"); importTransaction.addSplit(secondSplit); addTransactionToLedger(ledgerTransaction); expectMatchWithExistingTransaction(TransactionMatchFinder::MatchNotFound); } void MatchFinderTest::testExistingTransactionMatch_multipleAccounts_noBankId() { ledgerTransaction.splits().first().setAccountId(account->id()); ledgerTransaction.splits().first().setBankID(""); importTransaction.splits().first().setAccountId(otherAccount->id()); importTransaction.splits().first().setBankID(""); MyMoneySplit secondSplit = importTransaction.splits().first(); secondSplit.clearId(); secondSplit.setAccountId(account->id()); secondSplit.setBankID(""); importTransaction.addSplit(secondSplit); addTransactionToLedger(ledgerTransaction); expectMatchWithExistingTransaction(TransactionMatchFinder::MatchNotFound); } void MatchFinderTest::testScheduleMatch_allMatch() { - importTransaction.setPostDate(schedule.adjustedNextDueDate()); - addSchedule(schedule); + importTransaction.setPostDate(m_schedule.adjustedNextDueDate()); + addSchedule(m_schedule); expectMatchWithScheduledTransaction(TransactionMatchFinder::MatchPrecise); - QCOMPARE(schedule.isOverdue(), false); + QCOMPARE(m_schedule.isOverdue(), false); } void MatchFinderTest::testScheduleMatch_dueDateWithinMatchWindow() { - QDate dateWithinMatchWindow = schedule.adjustedNextDueDate().addDays(MATCH_WINDOW); + QDate dateWithinMatchWindow = m_schedule.adjustedNextDueDate().addDays(MATCH_WINDOW); importTransaction.setPostDate(dateWithinMatchWindow); - addSchedule(schedule); + addSchedule(m_schedule); expectMatchWithScheduledTransaction(TransactionMatchFinder::MatchImprecise); - QCOMPARE(schedule.isOverdue(), false); + QCOMPARE(m_schedule.isOverdue(), false); } void MatchFinderTest::testScheduleMatch_amountWithinAllowedVariation() { - double exactAmount = schedule.transaction().splits()[0].shares().toDouble(); - double amountWithinAllowedVariation = exactAmount * (100 + schedule.variation()) / 100; + double exactAmount = m_schedule.transaction().splits()[0].shares().toDouble(); + double amountWithinAllowedVariation = exactAmount * (100 + m_schedule.variation()) / 100; importTransaction.splits()[0].setShares(MyMoneyMoney(amountWithinAllowedVariation)); - importTransaction.setPostDate(schedule.adjustedNextDueDate()); - addSchedule(schedule); + importTransaction.setPostDate(m_schedule.adjustedNextDueDate()); + addSchedule(m_schedule); expectMatchWithScheduledTransaction(TransactionMatchFinder::MatchPrecise); } void MatchFinderTest::testScheduleMatch_overdue() { - schedule.setNextDueDate(QDate::currentDate().addDays(-MATCH_WINDOW - 1)); + m_schedule.setNextDueDate(QDate::currentDate().addDays(-MATCH_WINDOW - 1)); importTransaction.setPostDate(QDate::currentDate()); - addSchedule(schedule); + addSchedule(m_schedule); expectMatchWithScheduledTransaction(TransactionMatchFinder::MatchImprecise); - QCOMPARE(schedule.isOverdue(), true); + QCOMPARE(m_schedule.isOverdue(), true); } void MatchFinderTest::testScheduleMismatch_dueDate() { - importTransaction.setPostDate(schedule.adjustedNextDueDate().addDays(MATCH_WINDOW + 1)); - addSchedule(schedule); + importTransaction.setPostDate(m_schedule.adjustedNextDueDate().addDays(MATCH_WINDOW + 1)); + addSchedule(m_schedule); expectMatchWithScheduledTransaction(TransactionMatchFinder::MatchNotFound); - QCOMPARE(schedule.isOverdue(), false); + QCOMPARE(m_schedule.isOverdue(), false); } void MatchFinderTest::testScheduleMismatch_amount() { - double exactAmount = schedule.transaction().splits()[0].shares().toDouble(); - double mismatchedAmount = exactAmount * (110 + schedule.variation()) / 100; + double exactAmount = m_schedule.transaction().splits()[0].shares().toDouble(); + double mismatchedAmount = exactAmount * (110 + m_schedule.variation()) / 100; importTransaction.splits()[0].setShares(MyMoneyMoney(mismatchedAmount)); - importTransaction.setPostDate(schedule.adjustedNextDueDate()); - addSchedule(schedule); + importTransaction.setPostDate(m_schedule.adjustedNextDueDate()); + addSchedule(m_schedule); expectMatchWithScheduledTransaction(TransactionMatchFinder::MatchNotFound); } diff --git a/kmymoney/converter/tests/matchfinder-test.h b/kmymoney/converter/tests/matchfinder-test.h index 99c7009be..044b26667 100644 --- a/kmymoney/converter/tests/matchfinder-test.h +++ b/kmymoney/converter/tests/matchfinder-test.h @@ -1,110 +1,110 @@ /*************************************************************************** KMyMoney transaction importing module - tests for ExistingTransactionMatchFinder copyright : (C) 2012 by Lukasz Maszczynski ***************************************************************************/ /*************************************************************************** * * * 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 MATCHFINDERTEST_H #define MATCHFINDERTEST_H #include #include #include "mymoneyaccount.h" #include "mymoneypayee.h" #include "mymoneystoragemgr.h" #include "existingtransactionmatchfinder.h" #include "scheduledtransactionmatchfinder.h" class MyMoneyFile; class MatchFinderTest : public QObject { Q_OBJECT private: MyMoneyFile * file; QScopedPointer account; QScopedPointer otherAccount; QScopedPointer storage; MyMoneyPayee payee; MyMoneyPayee otherPayee; static const int MATCH_WINDOW = 4; MyMoneyTransaction ledgerTransaction; MyMoneyTransaction importTransaction; TransactionMatchFinder::MatchResult matchResult; QScopedPointer existingTrFinder; - MyMoneySchedule schedule; + MyMoneySchedule m_schedule; QScopedPointer scheduledTrFinder; void setupStorage(); void setupCurrency(); void setupAccounts(); void setupPayees(); MyMoneyTransaction buildDefaultTransaction() const; MyMoneyTransaction buildMatchedTransaction(MyMoneyTransaction transaction) const; QString addTransactionToLedger(MyMoneyTransaction transaction) const; MyMoneySchedule buildNonOverdueSchedule() const; void addSchedule(MyMoneySchedule schedule) const; void expectMatchWithExistingTransaction(TransactionMatchFinder::MatchResult expectedResult); void expectMatchWithScheduledTransaction(TransactionMatchFinder::MatchResult expectedResult); private Q_SLOTS: void init(); void cleanup(); void testDuplicate_allMatch(); void testDuplicate_payeeEmpty(); void testDuplicate_payeeMismatch(); void testDuplicate_splitIsMarkedAsMatched(); void testPreciseMatch_noBankId(); void testPreciseMatch_importBankId(); void testPreciseMatch_payeeEmpty(); void testImpreciseMatch_matchWindowLowerBound(); void testImpreciseMatch_matchWindowUpperBound(); void testImpreciseMatch_payeeEmpty(); void testNoMatch_bankIdMismatch(); void testNoMatch_ledgerBankIdNotEmpty(); void testNoMatch_accountMismatch_withBankId(); void testNoMatch_accountMismatch_noBankId(); void testNoMatch_amountMismatch_withBankId(); void testNoMatch_amountMismatch_noBankId(); void testNoMatch_payeeMismatch(); void testNoMatch_splitIsMarkedAsMatched(); void testNoMatch_postDateMismatch_withBankId(); void testNoMatch_postDateMismatch_noBankId(); void testExistingTransactionMatch_sameTransactionId_withBankId(); void testExistingTransactionMatch_sameTransactionId_noBankId(); void testExistingTransactionMatch_multipleAccounts_withBankId(); void testExistingTransactionMatch_multipleAccounts_noBankId(); void testScheduleMatch_allMatch(); void testScheduleMatch_dueDateWithinMatchWindow(); void testScheduleMatch_amountWithinAllowedVariation(); void testScheduleMatch_overdue(); void testScheduleMismatch_dueDate(); void testScheduleMismatch_amount(); public: MatchFinderTest(); }; #endif // MATCHFINDERTEST_H diff --git a/kmymoney/converter/transactionmatchfinder.cpp b/kmymoney/converter/transactionmatchfinder.cpp index 7a3bdbb7b..7bb057352 100644 --- a/kmymoney/converter/transactionmatchfinder.cpp +++ b/kmymoney/converter/transactionmatchfinder.cpp @@ -1,159 +1,159 @@ /*************************************************************************** KMyMoney transaction importing module - base class for searching for a matching transaction copyright : (C) 2012 by Lukasz Maszczynski ***************************************************************************/ /*************************************************************************** * * * 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 "transactionmatchfinder.h" #include #include #include #include "mymoneymoney.h" #include "mymoneyaccount.h" #include "mymoneyfile.h" #include "mymoneypayee.h" #include "mymoneyexception.h" TransactionMatchFinder::TransactionMatchFinder(int _matchWindow) : - matchWindow(_matchWindow), + m_matchWindow(_matchWindow), matchResult(MatchNotFound) { } TransactionMatchFinder::~TransactionMatchFinder() { } TransactionMatchFinder::MatchResult TransactionMatchFinder::findMatch(const MyMoneyTransaction& transactionToMatch, const MyMoneySplit& splitToMatch) { importedTransaction = transactionToMatch; - importedSplit = splitToMatch; + m_importedSplit = splitToMatch; matchResult = MatchNotFound; matchedTransaction.reset(); matchedSplit.reset(); matchedSchedule.reset(); QString date = importedTransaction.postDate().toString(Qt::ISODate); - QString payeeName = MyMoneyFile::instance()->payee(importedSplit.payeeId()).name(); - QString amount = importedSplit.shares().formatMoney("", 2); - QString account = MyMoneyFile::instance()->account(importedSplit.accountId()).name(); + QString payeeName = MyMoneyFile::instance()->payee(m_importedSplit.payeeId()).name(); + QString amount = m_importedSplit.shares().formatMoney("", 2); + QString account = MyMoneyFile::instance()->account(m_importedSplit.accountId()).name(); qDebug() << "Looking for a match with transaction: " << date << "," << payeeName << "," << amount << "(referenced account: " << account << ")"; createListOfMatchCandidates(); findMatchInMatchCandidatesList(); return matchResult; } MyMoneySplit TransactionMatchFinder::getMatchedSplit() const { if (matchedSplit.isNull()) { throw MYMONEYEXCEPTION(i18n("Internal error - no matching splits")); } return *matchedSplit; } MyMoneyTransaction TransactionMatchFinder::getMatchedTransaction() const { if (matchedTransaction.isNull()) { throw MYMONEYEXCEPTION(i18n("Internal error - no matching transactions")); } return *matchedTransaction; } MyMoneySchedule TransactionMatchFinder::getMatchedSchedule() const { if (matchedSchedule.isNull()) { throw MYMONEYEXCEPTION(i18n("Internal error - no matching schedules")); } return *matchedSchedule; } bool TransactionMatchFinder::splitsAreDuplicates(const MyMoneySplit& split1, const MyMoneySplit& split2, int amountVariation) const { return (splitsAmountsMatch(split1, split2, amountVariation) && splitsBankIdsDuplicated(split1, split2)); } bool TransactionMatchFinder::splitsMatch(const MyMoneySplit& importedSplit, const MyMoneySplit& existingSplit, int amountVariation) const { return (splitsAccountsMatch(importedSplit, existingSplit) && splitsBankIdsMatch(importedSplit, existingSplit) && splitsAmountsMatch(importedSplit, existingSplit, amountVariation) && splitsPayeesMatchOrEmpty(importedSplit, existingSplit) && !existingSplit.isMatched()); } bool TransactionMatchFinder::splitsAccountsMatch(const MyMoneySplit & split1, const MyMoneySplit & split2) const { return split1.accountId() == split2.accountId(); } bool TransactionMatchFinder::splitsAmountsMatch(const MyMoneySplit& split1, const MyMoneySplit& split2, int amountVariation) const { MyMoneyMoney upper(split1.shares()); MyMoneyMoney lower(upper); if ((amountVariation > 0) && (amountVariation < 100)) { lower = lower - (lower.abs() * MyMoneyMoney(amountVariation, 100)); upper = upper + (upper.abs() * MyMoneyMoney(amountVariation, 100)); } return (split2.shares() >= lower) && (split2.shares() <= upper); } bool TransactionMatchFinder::splitsBankIdsDuplicated(const MyMoneySplit& split1, const MyMoneySplit& split2) const { return (!split1.bankID().isEmpty()) && (split1.bankID() == split2.bankID()); } bool TransactionMatchFinder::splitsBankIdsMatch(const MyMoneySplit& importedSplit, const MyMoneySplit& existingSplit) const { return (existingSplit.bankID().isEmpty() || existingSplit.bankID() == importedSplit.bankID()); } bool TransactionMatchFinder::splitsPayeesMatchOrEmpty(const MyMoneySplit& split1, const MyMoneySplit& split2) const { bool payeesMatch = (split1.payeeId() == split2.payeeId()); bool atLeastOnePayeeIsNotSet = (split1.payeeId().isEmpty() || split2.payeeId().isEmpty()); return payeesMatch || atLeastOnePayeeIsNotSet; } void TransactionMatchFinder::findMatchingSplit(const MyMoneyTransaction& transaction, int amountVariation) { foreach (const MyMoneySplit & split, transaction.splits()) { - if (splitsAreDuplicates(importedSplit, split, amountVariation)) { + if (splitsAreDuplicates(m_importedSplit, split, amountVariation)) { matchedTransaction.reset(new MyMoneyTransaction(transaction)); matchedSplit.reset(new MyMoneySplit(split)); matchResult = MatchDuplicate; break; } - if (splitsMatch(importedSplit, split, amountVariation)) { + if (splitsMatch(m_importedSplit, split, amountVariation)) { matchedTransaction.reset(new MyMoneyTransaction(transaction)); matchedSplit.reset(new MyMoneySplit(split)); bool datesMatchPrecisely = importedTransaction.postDate() == transaction.postDate(); if (datesMatchPrecisely) { matchResult = MatchPrecise; } else { matchResult = MatchImprecise; } break; } } } diff --git a/kmymoney/converter/transactionmatchfinder.h b/kmymoney/converter/transactionmatchfinder.h index 6c2443ef0..60526575c 100644 --- a/kmymoney/converter/transactionmatchfinder.h +++ b/kmymoney/converter/transactionmatchfinder.h @@ -1,167 +1,167 @@ /*************************************************************************** KMyMoney transaction importing module - base class for searching for a matching transaction copyright : (C) 2012 by Lukasz Maszczynski ***************************************************************************/ /*************************************************************************** * * * 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 TRANSACTIONMATCHFINDER_H #define TRANSACTIONMATCHFINDER_H #include #include "mymoneysplit.h" #include "mymoneytransaction.h" #include "mymoneyschedule.h" /** The class provides an interface for finding a MyMoneyTransaction match with the one being imported */ class TransactionMatchFinder { public: /// enumerates possible match results typedef enum { MatchNotFound, ///< no matching transaction found MatchImprecise, ///< matching transaction found MatchPrecise, ///< matching transaction found with exactly the same parameters MatchDuplicate ///< found transaction duplicate } MatchResult; /** Initializes the match finder. * @param matchWindow max number of days the transactions may vary and still be considered to be matching */ - explicit TransactionMatchFinder(int matchWindow); + explicit TransactionMatchFinder(int m_matchWindow); virtual ~TransactionMatchFinder(); /** Searches for a matching transaction. See derived classes to learn where the transaction is looked for. * * @param transactionToMatch the imported transaction we want to match * @param splitToMatch the split of that transaction referencing the account we import into * @return the search result. There are several possible results: * - @ref MatchNotFound means that the imported transaction does not match any other transaction * - @ref MatchDuplicate means that the imported transaction is a duplicate of another transaction, * - @ref MatchImprecise means that the imported transaction matches another transaction, but the match * is not precise (e.g. transaction dates are not equal, but within matchWindow range) * - @ref MatchPrecise means that the imported transaction matches another transaction precisely */ TransactionMatchFinder::MatchResult findMatch(const MyMoneyTransaction& transactionToMatch, const MyMoneySplit& splitToMatch); /** Returns the matched split. * * @throws MyMoneyException if no match is found */ MyMoneySplit getMatchedSplit() const; /** Returns the matched transaction * * @throws MyMoneyException if no transaction was matched */ MyMoneyTransaction getMatchedTransaction() const; /** Returns the matched schedule * * @throws MyMoneyException if no schedule was matched */ MyMoneySchedule getMatchedSchedule() const; protected: - int matchWindow; + int m_matchWindow; MyMoneyTransaction importedTransaction; //!< the imported transaction that is being matched - MyMoneySplit importedSplit; //!< the imported transaction's split that is being matched + MyMoneySplit m_importedSplit; //!< the imported transaction's split that is being matched MatchResult matchResult; //!< match result QScopedPointer matchedTransaction; //!< the transaction that matches the imported one QScopedPointer matchedSchedule; //!< the schedule that matches the imported transaction QScopedPointer matchedSplit; //!< the split that matches the imported one /** Prepares a list of match candidates for further processing, must be implemented in subclass */ virtual void createListOfMatchCandidates() = 0; /** Searches the list of match candidates for a real match, must be implemented in subclass */ virtual void findMatchInMatchCandidatesList() = 0; /** Checks whether one split is a duplicate of the other * @param split1 the first split * @param split2 the second split * @param amountVariation the max number of percent the amounts may differ and still be considered matching * @return true, if split2 is a duplicate of split1 (and vice-versa); false otherwise * * Splits are considered duplicates if both have the same (non-empty) bankId assigned and same amounts. */ bool splitsAreDuplicates(const MyMoneySplit & split1, const MyMoneySplit & split2, int amountVariation = 0) const; /** Checks whether one split matches the other * @param importedSplit the split being imported * @param existingSplit the existing split * @param amountVariation the max number of percent the amounts may differ and still be considered matching * @return true, if importedSplit matches existingSplit (not necessarily the other way around); false otherwise * * Splits are considered a match if both of them: * - reference the same account * - have matching bankID-s * - have matching ammounts * - have empty or matching payees * - are not marked as matched already */ - bool splitsMatch(const MyMoneySplit & importedSplit, const MyMoneySplit & existingSplit, int amountVariation = 0) const; + bool splitsMatch(const MyMoneySplit & m_importedSplit, const MyMoneySplit & existingSplit, int amountVariation = 0) const; /** Checks whether splits reference the same account * @param split1 the first split * @param split2 the second split * @return true, if the same account is referenced by the splits; false otherwise */ bool splitsAccountsMatch(const MyMoneySplit & split1, const MyMoneySplit & split2) const; /** Checks whether splits amounts match * @param split1 the first split * @param split2 the second split * @param amountVariation the max number of percent the amounts may differ and still be considered matching * @return true, if amounts match; false otherwise */ bool splitsAmountsMatch(const MyMoneySplit & split1, const MyMoneySplit & split2, int amountVariation = 0) const; /** Checks whether the splits' bankId-s match * @param importedSplit the imported split * @param existingSplit the existing split * @return true, if bank ids match; false otherwise * * BankID-s match if any of the two occurs: * - they are equal * - bankId of existing split is empty */ - bool splitsBankIdsMatch(const MyMoneySplit & importedSplit, const MyMoneySplit & existingSplit) const; + bool splitsBankIdsMatch(const MyMoneySplit & m_importedSplit, const MyMoneySplit & existingSplit) const; /** Checks whether the splits' bankId-s are duplicated * @param split1 the first split * @param split2 the second split * @return true, if bank ids are equal and non-empty; false otherwise */ bool splitsBankIdsDuplicated(const MyMoneySplit & split1, const MyMoneySplit & split2) const; /** Checks whether payees of both splits match each other or at least one of them is empty * @param split1 the first split * @param split2 the second split * @return true, if splits reference the same payee or at least one payee is empty; false otherwise */ bool splitsPayeesMatchOrEmpty(const MyMoneySplit & split1, const MyMoneySplit & split2) const; /** Searches for a split in the transaction which matches imported transaction's split * @param transaction the transaction to look for the split in * @param amountVariation the max number of percent the split amounts may differ and still be considered matching */ void findMatchingSplit(const MyMoneyTransaction & transaction, int amountVariation); }; #endif // TRANSACTIONMATCHFINDER_H diff --git a/kmymoney/dialogs/investtransactioneditor.cpp b/kmymoney/dialogs/investtransactioneditor.cpp index 1e012d1f7..5f29e900c 100644 --- a/kmymoney/dialogs/investtransactioneditor.cpp +++ b/kmymoney/dialogs/investtransactioneditor.cpp @@ -1,1254 +1,1251 @@ /*************************************************************************** investtransactioneditor.cpp ---------- begin : Fri Dec 15 2006 copyright : (C) 2006 by Thomas Baumgart email : Thomas Baumgart (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 "investtransactioneditor.h" #include "transactioneditor_p.h" #include // ---------------------------------------------------------------------------- // QT Includes #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include // ---------------------------------------------------------------------------- // Project Includes #include "kmymoneyreconcilecombo.h" #include "kmymoneyactivitycombo.h" #include "kmymoneytagcombo.h" #include "ktagcontainer.h" #include "investtransaction.h" #include "selectedtransactions.h" #include "transactioneditorcontainer.h" #include "kmymoneycategory.h" #include "kmymoneydateinput.h" #include "kmymoneyedit.h" #include "kmymoneyaccountselector.h" #include "kmymoneymvccombo.h" #include "mymoneyfile.h" #include "mymoneyexception.h" #include "mymoneysecurity.h" #include "mymoneyprice.h" #include "ksplittransactiondlg.h" #include "kcurrencycalculator.h" #include "kmymoneysettings.h" #include "investactivities.h" #include "kmymoneycompletion.h" #include "dialogenums.h" using namespace eMyMoney; using namespace KMyMoneyRegister; using namespace KMyMoneyTransactionForm; using namespace Invest; class InvestTransactionEditorPrivate : public TransactionEditorPrivate { Q_DISABLE_COPY(InvestTransactionEditorPrivate) Q_DECLARE_PUBLIC(InvestTransactionEditor) friend class Invest::Activity; public: InvestTransactionEditorPrivate(InvestTransactionEditor* qq) : TransactionEditorPrivate(qq), m_activity(0), m_transactionType(eMyMoney::Split::InvestmentTransactionType::BuyShares) { m_phonyAccount = MyMoneyAccount("Phony-ID", MyMoneyAccount()); } ~InvestTransactionEditorPrivate() { delete m_activity; } void hideCategory(const QString& name) { Q_Q(InvestTransactionEditor); if (auto cat = dynamic_cast(q->haveWidget(name))) { cat->hide(); cat->splitButton()->hide(); } } void activityFactory(eMyMoney::Split::InvestmentTransactionType type) { Q_Q(InvestTransactionEditor); if (!m_activity || type != m_activity->type()) { delete m_activity; switch (type) { default: case eMyMoney::Split::InvestmentTransactionType::BuyShares: m_activity = new Buy(q); break; case eMyMoney::Split::InvestmentTransactionType::SellShares: m_activity = new Sell(q); break; case eMyMoney::Split::InvestmentTransactionType::Dividend: case eMyMoney::Split::InvestmentTransactionType::Yield: m_activity = new Div(q); break; case eMyMoney::Split::InvestmentTransactionType::ReinvestDividend: m_activity = new Reinvest(q); break; case eMyMoney::Split::InvestmentTransactionType::AddShares: m_activity = new Add(q); break; case eMyMoney::Split::InvestmentTransactionType::RemoveShares: m_activity = new Remove(q); break; case eMyMoney::Split::InvestmentTransactionType::SplitShares: m_activity = new Invest::Split(q); break; case eMyMoney::Split::InvestmentTransactionType::InterestIncome: m_activity = new IntInc(q); break; } } } MyMoneyMoney subtotal(const QList& splits) const { MyMoneyMoney sum; foreach (const auto split, splits) sum += split.value(); return sum; } /** * This method creates a transaction to be used for the split fee/interest editor. * It has a reference to a phony account and the splits contained in @a splits . */ bool createPseudoTransaction(MyMoneyTransaction& t, const QList& splits) { t.removeSplits(); MyMoneySplit split; split.setAccountId(m_phonyAccount.id()); split.setValue(-subtotal(splits)); split.setShares(split.value()); t.addSplit(split); m_phonySplit = split; foreach (const auto it_s, splits) { split = it_s; split.clearId(); t.addSplit(split); } return true; } /** * Convenience method used by slotEditInterestSplits() and slotEditFeeSplits(). * * @param categoryWidgetName name of the category widget * @param amountWidgetName name of the amount widget * @param splits the splits that make up the transaction to be edited * @param isIncome @c false for fees, @c true for interest * @param slotEditSplits name of the slot to be connected to the focusIn signal of the * category widget named @p categoryWidgetName in case of multiple splits * in @p splits . */ int editSplits(const QString& categoryWidgetName, const QString& amountWidgetName, QList& splits, bool isIncome, const char* slotEditSplits) { Q_Q(InvestTransactionEditor); int rc = QDialog::Rejected; if (!m_openEditSplits) { // only get in here in a single instance m_openEditSplits = true; // force focus change to update all data auto category = dynamic_cast(m_editWidgets[categoryWidgetName]); if (!category) return rc; QWidget* w = category->splitButton(); if (w) w->setFocus(); auto amount = dynamic_cast(q->haveWidget(amountWidgetName)); if (!amount) return rc; MyMoneyTransaction transaction; transaction.setCommodity(m_currency.id()); if (splits.count() == 0 && !category->selectedItem().isEmpty()) { MyMoneySplit s; s.setAccountId(category->selectedItem()); s.setShares(amount->value()); s.setValue(s.shares()); splits << s; } // use the transactions commodity as the currency indicator for the splits // this is used to allow some useful setting for the fractions in the amount fields try { m_phonyAccount.setCurrencyId(m_transaction.commodity()); m_phonyAccount.fraction(MyMoneyFile::instance()->security(m_transaction.commodity())); } catch (const MyMoneyException &) { qDebug("Unable to setup precision"); } if (createPseudoTransaction(transaction, splits)) { MyMoneyMoney value; QPointer dlg = new KSplitTransactionDlg(transaction, m_phonySplit, m_phonyAccount, false, isIncome, MyMoneyMoney(), m_priceInfo, m_regForm); // q->connect(dlg, SIGNAL(newCategory(MyMoneyAccount&)), q, SIGNAL(newCategory(MyMoneyAccount&))); if ((rc = dlg->exec()) == QDialog::Accepted) { transaction = dlg->transaction(); // collect splits out of the transaction splits.clear(); MyMoneyMoney fees; foreach (const auto split, transaction.splits()) { if (split.accountId() == m_phonyAccount.id()) continue; splits << split; fees += split.shares(); } if (isIncome) fees = -fees; QString categoryId; q->setupCategoryWidget(category, splits, categoryId, slotEditSplits); amount->setValue(fees); q->slotUpdateTotalAmount(); } delete dlg; } // focus jumps into the memo field if ((w = q->haveWidget("memo")) != 0) { w->setFocus(); } m_openEditSplits = false; } return rc; } void updatePriceMode(const MyMoneySplit& split = MyMoneySplit()) { Q_Q(InvestTransactionEditor); if (auto label = dynamic_cast(q->haveWidget("price-label"))) { auto sharesEdit = dynamic_cast(q->haveWidget("shares")); auto priceEdit = dynamic_cast(q->haveWidget("price")); if (!sharesEdit || !priceEdit) return; MyMoneyMoney price; if (!split.id().isEmpty()) price = split.price().reduce(); else price = priceEdit->value().abs(); if (q->priceMode() == eDialogs::PriceMode::PricePerTransaction) { priceEdit->setPrecision(m_currency.pricePrecision()); label->setText(i18n("Transaction amount")); if (!sharesEdit->value().isZero()) priceEdit->setValue(sharesEdit->value().abs() * price); } else if (q->priceMode() == eDialogs::PriceMode::PricePerShare) { priceEdit->setPrecision(m_security.pricePrecision()); label->setText(i18n("Price/Share")); priceEdit->setValue(price); } else priceEdit->setValue(price); } } Activity* m_activity; MyMoneyAccount m_phonyAccount; MyMoneySplit m_phonySplit; MyMoneySplit m_assetAccountSplit; QList m_interestSplits; QList m_feeSplits; MyMoneySecurity m_security; MyMoneySecurity m_currency; eMyMoney::Split::InvestmentTransactionType m_transactionType; }; InvestTransactionEditor::InvestTransactionEditor() : TransactionEditor(*new InvestTransactionEditorPrivate(this)) { Q_D(InvestTransactionEditor); d->m_transactionType = eMyMoney::Split::InvestmentTransactionType::UnknownTransactionType; } InvestTransactionEditor::~InvestTransactionEditor() { } InvestTransactionEditor::InvestTransactionEditor(TransactionEditorContainer* regForm, KMyMoneyRegister::InvestTransaction* item, const KMyMoneyRegister::SelectedTransactions& list, const QDate& lastPostDate) : TransactionEditor(*new InvestTransactionEditorPrivate(this), regForm, item, list, lastPostDate) { Q_D(InvestTransactionEditor); // after the gometries of the container are updated hide the widgets which are not needed by the current activity connect(d->m_regForm, &TransactionEditorContainer::geometriesUpdated, this, &InvestTransactionEditor::slotTransactionContainerGeometriesUpdated); // dissect the transaction into its type, splits, currency, security etc. KMyMoneyUtils::dissectTransaction(d->m_transaction, d->m_split, d->m_assetAccountSplit, d->m_feeSplits, d->m_interestSplits, d->m_security, d->m_currency, d->m_transactionType); // determine initial activity object d->activityFactory(d->m_transactionType); } void InvestTransactionEditor::createEditWidgets() { Q_D(InvestTransactionEditor); auto activity = new KMyMoneyActivityCombo(); d->m_editWidgets["activity"] = activity; connect(activity, &KMyMoneyActivityCombo::activitySelected, this, &InvestTransactionEditor::slotUpdateActivity); connect(activity, &KMyMoneyActivityCombo::activitySelected, this, &InvestTransactionEditor::slotUpdateButtonState); d->m_editWidgets["postdate"] = new KMyMoneyDateInput; auto security = new KMyMoneySecurity; security->setPlaceholderText(i18n("Security")); d->m_editWidgets["security"] = security; connect(security, &KMyMoneyCombo::itemSelected, this, &InvestTransactionEditor::slotUpdateSecurity); connect(security, &QComboBox::editTextChanged, this, &InvestTransactionEditor::slotUpdateButtonState); connect(security, &KMyMoneyCombo::createItem, this, &InvestTransactionEditor::slotCreateSecurity); connect(security, &KMyMoneyCombo::objectCreation, this, &InvestTransactionEditor::objectCreation); auto asset = new KMyMoneyCategory(false, nullptr); asset->setPlaceholderText(i18n("Asset account")); d->m_editWidgets["asset-account"] = asset; connect(asset, &QComboBox::editTextChanged, this, &InvestTransactionEditor::slotUpdateButtonState); connect(asset, &KMyMoneyCombo::objectCreation, this, &InvestTransactionEditor::objectCreation); auto fees = new KMyMoneyCategory(true, nullptr); fees->setPlaceholderText(i18n("Fees")); d->m_editWidgets["fee-account"] = fees; connect(fees, &KMyMoneyCombo::itemSelected, this, &InvestTransactionEditor::slotUpdateFeeCategory); connect(fees, &QComboBox::editTextChanged, this, &InvestTransactionEditor::slotUpdateButtonState); connect(fees, &QComboBox::editTextChanged, this, &InvestTransactionEditor::slotUpdateFeeVisibility); connect(fees, &KMyMoneyCombo::createItem, this, &InvestTransactionEditor::slotCreateFeeCategory); connect(fees, &KMyMoneyCombo::objectCreation, this, &InvestTransactionEditor::objectCreation); connect(fees->splitButton(), &QAbstractButton::clicked, this, &InvestTransactionEditor::slotEditFeeSplits); auto interest = new KMyMoneyCategory(true, nullptr); interest->setPlaceholderText(i18n("Interest")); d->m_editWidgets["interest-account"] = interest; connect(interest, &KMyMoneyCombo::itemSelected, this, &InvestTransactionEditor::slotUpdateInterestCategory); connect(interest, &QComboBox::editTextChanged, this, &InvestTransactionEditor::slotUpdateButtonState); connect(interest, &QComboBox::editTextChanged, this, &InvestTransactionEditor::slotUpdateInterestVisibility); connect(interest, &KMyMoneyCombo::createItem, this, &InvestTransactionEditor::slotCreateInterestCategory); connect(interest, &KMyMoneyCombo::objectCreation, this, &InvestTransactionEditor::objectCreation); connect(interest->splitButton(), &QAbstractButton::clicked, this, &InvestTransactionEditor::slotEditInterestSplits); auto tag = new KTagContainer; tag->tagCombo()->setPlaceholderText(i18n("Tag")); tag->tagCombo()->setObjectName(QLatin1String("Tag")); d->m_editWidgets["tag"] = tag; connect(tag->tagCombo(), &QComboBox::editTextChanged, this, &InvestTransactionEditor::slotUpdateButtonState); connect(tag->tagCombo(), &KMyMoneyMVCCombo::createItem, this, &InvestTransactionEditor::slotNewTag); connect(tag->tagCombo(), &KMyMoneyMVCCombo::objectCreation, this, &InvestTransactionEditor::objectCreation); auto memo = new KTextEdit; memo->setTabChangesFocus(true); d->m_editWidgets["memo"] = memo; connect(memo, &QTextEdit::textChanged, this, &InvestTransactionEditor::slotUpdateInvestMemoState); connect(memo, &QTextEdit::textChanged, this, &InvestTransactionEditor::slotUpdateButtonState); d->m_activity->memoText().clear(); d->m_activity->memoChanged() = false; KMyMoneyEdit* value = new KMyMoneyEdit; value->setPlaceholderText(i18n("Shares")); value->setResetButtonVisible(false); d->m_editWidgets["shares"] = value; connect(value, &KMyMoneyEdit::textChanged, this, &InvestTransactionEditor::slotUpdateButtonState); connect(value, &KMyMoneyEdit::valueChanged, this, &InvestTransactionEditor::slotUpdateTotalAmount); value = new KMyMoneyEdit; value->setPlaceholderText(i18n("Price")); value->setResetButtonVisible(false); d->m_editWidgets["price"] = value; connect(value, &KMyMoneyEdit::textChanged, this, &InvestTransactionEditor::slotUpdateButtonState); connect(value, &KMyMoneyEdit::valueChanged, this, &InvestTransactionEditor::slotUpdateTotalAmount); value = new KMyMoneyEdit; // TODO once we have the selected transactions as array of Transaction // we can allow multiple splits for fee and interest value->setResetButtonVisible(false); d->m_editWidgets["fee-amount"] = value; connect(value, &KMyMoneyEdit::textChanged, this, &InvestTransactionEditor::slotUpdateButtonState); connect(value, &KMyMoneyEdit::valueChanged, this, &InvestTransactionEditor::slotUpdateTotalAmount); value = new KMyMoneyEdit; // TODO once we have the selected transactions as array of Transaction // we can allow multiple splits for fee and interest value->setResetButtonVisible(false); d->m_editWidgets["interest-amount"] = value; connect(value, &KMyMoneyEdit::textChanged, this, &InvestTransactionEditor::slotUpdateButtonState); connect(value, &KMyMoneyEdit::valueChanged, this, &InvestTransactionEditor::slotUpdateTotalAmount); auto reconcile = new KMyMoneyReconcileCombo; d->m_editWidgets["status"] = reconcile; connect(reconcile, &KMyMoneyMVCCombo::itemSelected, this, &InvestTransactionEditor::slotUpdateButtonState); KMyMoneyRegister::QWidgetContainer::iterator it_w; for (it_w = d->m_editWidgets.begin(); it_w != d->m_editWidgets.end(); ++it_w) { (*it_w)->installEventFilter(this); } QLabel* label; d->m_editWidgets["activity-label"] = label = new QLabel(i18n("Activity")); label->setAlignment(Qt::AlignVCenter); d->m_editWidgets["postdate-label"] = label = new QLabel(i18n("Date")); label->setAlignment(Qt::AlignVCenter); d->m_editWidgets["security-label"] = label = new QLabel(i18n("Security")); label->setAlignment(Qt::AlignVCenter); d->m_editWidgets["shares-label"] = label = new QLabel(i18n("Shares")); label->setAlignment(Qt::AlignVCenter); d->m_editWidgets["asset-label"] = label = new QLabel(i18n("Account")); label->setAlignment(Qt::AlignVCenter); d->m_editWidgets["price-label"] = label = new QLabel(i18n("Price/share")); label->setAlignment(Qt::AlignVCenter); d->m_editWidgets["fee-label"] = label = new QLabel(i18n("Fees")); label->setAlignment(Qt::AlignVCenter); d->m_editWidgets["fee-amount-label"] = label = new QLabel(""); label->setAlignment(Qt::AlignVCenter); d->m_editWidgets["interest-label"] = label = new QLabel(i18n("Interest")); label->setAlignment(Qt::AlignVCenter); d->m_editWidgets["interest-amount-label"] = label = new QLabel(i18n("Interest")); label->setAlignment(Qt::AlignVCenter); d->m_editWidgets["memo-label"] = label = new QLabel(i18n("Memo")); label->setAlignment(Qt::AlignVCenter); d->m_editWidgets["total"] = label = new QLabel(""); label->setAlignment(Qt::AlignVCenter | Qt::AlignRight); d->m_editWidgets["total-label"] = label = new QLabel(i18nc("Total value", "Total")); label->setAlignment(Qt::AlignVCenter); d->m_editWidgets["status-label"] = label = new QLabel(i18n("Status")); label->setAlignment(Qt::AlignVCenter); // if we don't have more than 1 selected transaction, we don't need // the "don't change" item in some of the combo widgets if (d->m_transactions.count() < 2) { reconcile->removeDontCare(); } } int InvestTransactionEditor::slotEditFeeSplits() { Q_D(InvestTransactionEditor); return d->editSplits("fee-account", "fee-amount", d->m_feeSplits, false, SLOT(slotEditFeeSplits())); } int InvestTransactionEditor::slotEditInterestSplits() { Q_D(InvestTransactionEditor); return d->editSplits("interest-account", "interest-amount", d->m_interestSplits, true, SLOT(slotEditInterestSplits())); } void InvestTransactionEditor::slotCreateSecurity(const QString& name, QString& id) { Q_D(InvestTransactionEditor); MyMoneyAccount acc; QRegExp exp("([^:]+)"); if (exp.indexIn(name) != -1) { acc.setName(exp.cap(1)); slotNewInvestment(acc, d->m_account); // return id id = acc.id(); if (!id.isEmpty()) { slotUpdateSecurity(id); } } } void InvestTransactionEditor::slotCreateFeeCategory(const QString& name, QString& id) { MyMoneyAccount acc; acc.setName(name); slotNewCategory(acc, MyMoneyFile::instance()->expense()); // return id id = acc.id(); } void InvestTransactionEditor::slotUpdateFeeCategory(const QString& id) { haveWidget("fee-amount")->setDisabled(id.isEmpty()); } void InvestTransactionEditor::slotUpdateFeeVisibility(const QString& txt) { Q_D(InvestTransactionEditor); static const QSet transactionTypesWithoutFee = QSet() << eMyMoney::Split::InvestmentTransactionType::AddShares << eMyMoney::Split::InvestmentTransactionType::RemoveShares << eMyMoney::Split::InvestmentTransactionType::SplitShares; auto feeAmount = dynamic_cast(haveWidget("fee-amount")); if (!feeAmount) return; feeAmount->setHidden(txt.isEmpty()); auto l = dynamic_cast(haveWidget("fee-amount-label")); auto fee = dynamic_cast(haveWidget("fee-account")); if (!fee) return; const auto hideFee = txt.isEmpty() || transactionTypesWithoutFee.contains(d->m_activity->type()); // no fee expected so hide if (hideFee) { if (l) { l->setText(""); } feeAmount->hide(); fee->splitButton()->hide(); } else { if (l) { l->setText(i18n("Fee Amount")); } feeAmount->show(); fee->splitButton()->show(); } } void InvestTransactionEditor::slotUpdateInterestCategory(const QString& id) { haveWidget("interest-amount")->setDisabled(id.isEmpty()); } void InvestTransactionEditor::slotUpdateInterestVisibility(const QString& txt) { Q_D(InvestTransactionEditor); static const QSet transactionTypesWithInterest = QSet() << eMyMoney::Split::InvestmentTransactionType::BuyShares << eMyMoney::Split::InvestmentTransactionType::SellShares << eMyMoney::Split::InvestmentTransactionType::Dividend << eMyMoney::Split::InvestmentTransactionType::InterestIncome << eMyMoney::Split::InvestmentTransactionType::Yield; QWidget* w = haveWidget("interest-amount"); w->setHidden(txt.isEmpty()); auto l = dynamic_cast(haveWidget("interest-amount-label")); auto interest = dynamic_cast(haveWidget("interest-account")); const auto showInterest = !txt.isEmpty() && transactionTypesWithInterest.contains(d->m_activity->type()); if (interest && showInterest) { interest->splitButton()->show(); w->show(); if (l) l->setText(i18n("Interest")); } else { if (interest) { interest->splitButton()->hide(); w->hide(); if (l) l->setText(QString()); } } } void InvestTransactionEditor::slotCreateInterestCategory(const QString& name, QString& id) { MyMoneyAccount acc; acc.setName(name); slotNewCategory(acc, MyMoneyFile::instance()->income()); id = acc.id(); } void InvestTransactionEditor::slotReloadEditWidgets() { Q_D(InvestTransactionEditor); auto interest = dynamic_cast(haveWidget("interest-account")); auto fees = dynamic_cast(haveWidget("fee-account")); auto security = dynamic_cast(haveWidget("security")); if (!interest || !fees || !security) return; AccountSet aSet; QString id; // interest-account aSet.clear(); aSet.addAccountGroup(Account::Type::Income); aSet.load(interest->selector()); setupCategoryWidget(interest, d->m_interestSplits, id, SLOT(slotEditInterestSplits())); // fee-account aSet.clear(); aSet.addAccountGroup(Account::Type::Expense); aSet.load(fees->selector()); setupCategoryWidget(fees, d->m_feeSplits, id, SLOT(slotEditFeeSplits())); // security aSet.clear(); aSet.load(security->selector(), i18n("Security"), d->m_account.accountList(), true); } void InvestTransactionEditor::loadEditWidgets(eWidgets::eRegister::Action) { loadEditWidgets(); } void InvestTransactionEditor::loadEditWidgets() { Q_D(InvestTransactionEditor); QString id; auto postDate = dynamic_cast(haveWidget("postdate")); auto reconcile = dynamic_cast(haveWidget("status")); auto security = dynamic_cast(haveWidget("security")); auto activity = dynamic_cast(haveWidget("activity")); auto asset = dynamic_cast(haveWidget("asset-account")); auto memo = dynamic_cast(d->m_editWidgets["memo"]); KMyMoneyEdit* value; auto interest = dynamic_cast(haveWidget("interest-account")); auto fees = dynamic_cast(haveWidget("fee-account")); if (!postDate || !reconcile || !security || !activity || !asset || !memo || !interest || !fees) return; // check if the current transaction has a reference to an equity account auto haveEquityAccount = false; foreach (const auto split, d->m_transaction.splits()) { auto acc = MyMoneyFile::instance()->account(split.accountId()); if (acc.accountType() == Account::Type::Equity) { haveEquityAccount = true; break; } } // asset-account AccountSet aSet; aSet.clear(); aSet.addAccountType(Account::Type::Checkings); aSet.addAccountType(Account::Type::Savings); aSet.addAccountType(Account::Type::Cash); aSet.addAccountType(Account::Type::Asset); aSet.addAccountType(Account::Type::Currency); aSet.addAccountType(Account::Type::CreditCard); if (KMyMoneySettings::expertMode() || haveEquityAccount) aSet.addAccountGroup(Account::Type::Equity); aSet.load(asset->selector()); // security security->setSuppressObjectCreation(false); // allow object creation on the fly aSet.clear(); aSet.load(security->selector(), i18n("Security"), d->m_account.accountList(), true); // memo memo->setText(d->m_split.memo()); d->m_activity->memoText() = d->m_split.memo(); d->m_activity->memoChanged() = false; if (!isMultiSelection()) { // date if (d->m_transaction.postDate().isValid()) postDate->setDate(d->m_transaction.postDate()); else if (d->m_lastPostDate.isValid()) postDate->setDate(d->m_lastPostDate); else postDate->setDate(QDate::currentDate()); // security (but only if it's not the investment account) if (d->m_split.accountId() != d->m_account.id()) { security->completion()->setSelected(d->m_split.accountId()); security->slotItemSelected(d->m_split.accountId()); } // activity activity->setActivity(d->m_activity->type()); slotUpdateActivity(activity->activity()); asset->completion()->setSelected(d->m_assetAccountSplit.accountId()); asset->slotItemSelected(d->m_assetAccountSplit.accountId()); // interest-account aSet.clear(); aSet.addAccountGroup(Account::Type::Income); aSet.load(interest->selector()); setupCategoryWidget(interest, d->m_interestSplits, id, SLOT(slotEditInterestSplits())); slotUpdateInterestVisibility(interest->currentText()); // fee-account aSet.clear(); aSet.addAccountGroup(Account::Type::Expense); aSet.load(fees->selector()); setupCategoryWidget(fees, d->m_feeSplits, id, SLOT(slotEditFeeSplits())); slotUpdateFeeVisibility(fees->currentText()); // shares // don't set the value if the number of shares is zero so that // we can see the hint value = dynamic_cast(haveWidget("shares")); if (!value) return; if (typeid(*(d->m_activity)) != typeid(Invest::Split(this))) value->setPrecision(MyMoneyMoney::denomToPrec(d->m_security.smallestAccountFraction())); else value->setPrecision(-1); if (!d->m_split.shares().isZero()) value->setValue(d->m_split.shares().abs()); // price d->updatePriceMode(d->m_split); // fee amount value = dynamic_cast(haveWidget("fee-amount")); if (!value) return; value->setValue(d->subtotal(d->m_feeSplits)); // interest amount value = dynamic_cast(haveWidget("interest-amount")); if (!value) return; value->setValue(-d->subtotal(d->m_interestSplits)); // total slotUpdateTotalAmount(); // status if (d->m_split.reconcileFlag() == eMyMoney::Split::State::Unknown) d->m_split.setReconcileFlag(eMyMoney::Split::State::NotReconciled); reconcile->setState(d->m_split.reconcileFlag()); } else { postDate->loadDate(QDate()); reconcile->setState(eMyMoney::Split::State::Unknown); // We don't allow to change the activity activity->setActivity(d->m_activity->type()); slotUpdateActivity(activity->activity()); activity->setDisabled(true); // scan the list of selected transactions and check that they have // the same activity. - KMyMoneyRegister::SelectedTransactions::iterator it_t = d->m_transactions.begin(); const QString& action = d->m_item->split().action(); bool isNegative = d->m_item->split().shares().isNegative(); bool allSameActivity = true; - for (it_t = d->m_transactions.begin(); allSameActivity && (it_t != d->m_transactions.end()); ++it_t) { + for (auto it_t = d->m_transactions.begin(); allSameActivity && (it_t != d->m_transactions.end()); ++it_t) { allSameActivity = (action == (*it_t).split().action() && (*it_t).split().shares().isNegative() == isNegative); } QStringList fields; fields << "shares" << "price" << "fee-amount" << "interest-amount"; - QStringList::const_iterator it_f; - for (it_f = fields.constBegin(); it_f != fields.constEnd(); ++it_f) { + for (auto it_f = fields.constBegin(); it_f != fields.constEnd(); ++it_f) { value = dynamic_cast(haveWidget((*it_f))); if (!value) return; value->setText(""); value->setAllowEmpty(); } // if we have transactions with different activities, disable some more widgets if (!allSameActivity) { fields << "asset-account" << "fee-account" << "interest-account"; - QStringList::const_iterator it_f; - for (it_f = fields.constBegin(); it_f != fields.constEnd(); ++it_f) { + for (auto it_f = fields.constBegin(); it_f != fields.constEnd(); ++it_f) { haveWidget(*it_f)->setDisabled(true); } } } } QWidget* InvestTransactionEditor::firstWidget() const { return nullptr; // let the creator use the first widget in the tab order } bool InvestTransactionEditor::isComplete(QString& reason) const { Q_D(const InvestTransactionEditor); reason.clear(); return d->m_activity->isComplete(reason); } void InvestTransactionEditor::slotUpdateSecurity(const QString& stockId) { Q_D(InvestTransactionEditor); auto file = MyMoneyFile::instance(); MyMoneyAccount stock = file->account(stockId); d->m_security = file->security(stock.currencyId()); d->m_currency = file->security(d->m_security.tradingCurrency()); bool currencyKnown = !d->m_currency.id().isEmpty(); if (!currencyKnown) { d->m_currency.setTradingSymbol("???"); } else { auto sharesWidget = dynamic_cast(haveWidget("shares")); if (sharesWidget) { if (typeid(*(d->m_activity)) != typeid(Invest::Split(this))) sharesWidget->setPrecision(MyMoneyMoney::denomToPrec(d->m_security.smallestAccountFraction())); else sharesWidget->setPrecision(-1); } } d->updatePriceMode(); d->m_activity->preloadAssetAccount(); haveWidget("shares")->setEnabled(currencyKnown); haveWidget("price")->setEnabled(currencyKnown); haveWidget("fee-amount")->setEnabled(currencyKnown); haveWidget("interest-amount")->setEnabled(currencyKnown); slotUpdateTotalAmount(); slotUpdateButtonState(); resizeForm(); } bool InvestTransactionEditor::fixTransactionCommodity(const MyMoneyAccount& /* account */) { return true; } void InvestTransactionEditor::totalAmount(MyMoneyMoney& amount) const { auto activityCombo = dynamic_cast(haveWidget("activity")); auto sharesEdit = dynamic_cast(haveWidget("shares")); auto priceEdit = dynamic_cast(haveWidget("price")); auto feesEdit = dynamic_cast(haveWidget("fee-amount")); auto interestEdit = dynamic_cast(haveWidget("interest-amount")); if (!activityCombo || !sharesEdit || !priceEdit || !feesEdit || !interestEdit) return; if (priceMode() == eDialogs::PriceMode::PricePerTransaction) amount = priceEdit->value().abs(); else amount = sharesEdit->value().abs() * priceEdit->value().abs(); if (feesEdit->isVisible()) { MyMoneyMoney fee = feesEdit->value(); MyMoneyMoney factor(-1, 1); switch (activityCombo->activity()) { case eMyMoney::Split::InvestmentTransactionType::BuyShares: case eMyMoney::Split::InvestmentTransactionType::ReinvestDividend: factor = MyMoneyMoney::ONE; break; default: break; } amount += (fee * factor); } if (interestEdit->isVisible()) { MyMoneyMoney interest = interestEdit->value(); MyMoneyMoney factor(1, 1); switch (activityCombo->activity()) { case eMyMoney::Split::InvestmentTransactionType::BuyShares: factor = MyMoneyMoney::MINUS_ONE; break; default: break; } amount += (interest * factor); } } void InvestTransactionEditor::slotUpdateTotalAmount() { Q_D(InvestTransactionEditor); auto total = dynamic_cast(haveWidget("total")); if (total && total->isVisible()) { MyMoneyMoney amount; totalAmount(amount); total->setText(amount.convert(d->m_currency.smallestAccountFraction(), d->m_security.roundingMethod()) .formatMoney(d->m_currency.tradingSymbol(), MyMoneyMoney::denomToPrec(d->m_currency.smallestAccountFraction()))); } } void InvestTransactionEditor::slotTransactionContainerGeometriesUpdated() { Q_D(InvestTransactionEditor); // when the geometries of the transaction container are updated some edit widgets that were // previously hidden are being shown (see QAbstractItemView::updateEditorGeometries) so we // need to update the activity with the current activity in order to show only the widgets // which are needed by the current activity if (d->m_editWidgets.isEmpty()) return; slotUpdateActivity(d->m_activity->type()); } void InvestTransactionEditor::slotUpdateActivity(eMyMoney::Split::InvestmentTransactionType activity) { Q_D(InvestTransactionEditor); // create new activity object if required d->activityFactory(activity); // hide all dynamic widgets d->hideCategory("interest-account"); d->hideCategory("fee-account"); QStringList dynwidgets; dynwidgets << "total-label" << "asset-label" << "fee-label" << "fee-amount-label" << "interest-label" << "interest-amount-label" << "price-label" << "shares-label"; // hiding labels works by clearing them. hide() does not do the job // as the underlying text in the QTable object will shine through QStringList::const_iterator it_s; for (it_s = dynwidgets.constBegin(); it_s != dynwidgets.constEnd(); ++it_s) { QLabel* w = dynamic_cast(haveWidget(*it_s)); if (w) w->setText(" "); } // real widgets can be hidden dynwidgets.clear(); dynwidgets << "asset-account" << "interest-amount" << "fee-amount" << "shares" << "price" << "total"; for (it_s = dynwidgets.constBegin(); it_s != dynwidgets.constEnd(); ++it_s) { QWidget* w = haveWidget(*it_s); if (w) w->hide(); } d->m_activity->showWidgets(); d->m_activity->preloadAssetAccount(); if (auto cat = dynamic_cast(haveWidget("interest-account"))) { if (cat->parentWidget()->isVisible()) slotUpdateInterestVisibility(cat->currentText()); else cat->splitButton()->hide(); } if (auto cat = dynamic_cast(haveWidget("fee-account"))) { if (cat->parentWidget()->isVisible()) slotUpdateFeeVisibility(cat->currentText()); else cat->splitButton()->hide(); } } eDialogs::PriceMode InvestTransactionEditor::priceMode() const { Q_D(const InvestTransactionEditor); eDialogs::PriceMode mode = static_cast(eDialogs::PriceMode::Price); auto sec = dynamic_cast(d->m_editWidgets["security"]); QString accId; if (sec && !sec->currentText().isEmpty()) { accId = sec->selectedItem(); if (accId.isEmpty()) accId = d->m_account.id(); } while (!accId.isEmpty() && mode == eDialogs::PriceMode::Price) { auto acc = MyMoneyFile::instance()->account(accId); if (acc.value("priceMode").isEmpty()) accId = acc.parentAccountId(); else mode = static_cast(acc.value("priceMode").toInt()); } // if mode is still then use that if (mode == eDialogs::PriceMode::Price) mode = eDialogs::PriceMode::PricePerShare; return mode; } MyMoneySecurity InvestTransactionEditor::security() const { Q_D(const InvestTransactionEditor); return d->m_security; } QList InvestTransactionEditor::feeSplits() const { Q_D(const InvestTransactionEditor); return d->m_feeSplits; } QList InvestTransactionEditor::interestSplits() const { Q_D(const InvestTransactionEditor); return d->m_interestSplits; } bool InvestTransactionEditor::setupPrice(const MyMoneyTransaction& t, MyMoneySplit& split) { Q_D(InvestTransactionEditor); auto file = MyMoneyFile::instance(); auto acc = file->account(split.accountId()); MyMoneySecurity toCurrency(file->security(acc.currencyId())); int fract = acc.fraction(); if (acc.currencyId() != t.commodity()) { if (acc.currencyId().isEmpty()) acc.setCurrencyId(t.commodity()); QMap::Iterator it_p; QString key = t.commodity() + '-' + acc.currencyId(); it_p = d->m_priceInfo.find(key); // if it's not found, then collect it from the user first MyMoneyMoney price; if (it_p == d->m_priceInfo.end()) { MyMoneySecurity fromCurrency = file->security(t.commodity()); MyMoneyMoney fromValue, toValue; fromValue = split.value(); const MyMoneyPrice &priceInfo = MyMoneyFile::instance()->price(fromCurrency.id(), toCurrency.id(), t.postDate()); toValue = split.value() * priceInfo.rate(toCurrency.id()); QPointer calc = new KCurrencyCalculator(fromCurrency, toCurrency, fromValue, toValue, t.postDate(), fract, d->m_regForm); if (calc->exec() == QDialog::Rejected) { delete calc; return false; } price = calc->price(); delete calc; d->m_priceInfo[key] = price; } else { price = (*it_p); } // update shares if the transaction commodity is the currency // of the current selected account split.setShares(split.value() * price); } else { split.setShares(split.value()); } return true; } bool InvestTransactionEditor::createTransaction(MyMoneyTransaction& t, const MyMoneyTransaction& torig, const MyMoneySplit& sorig, bool /* skipPriceDialog */) { Q_D(InvestTransactionEditor); auto file = MyMoneyFile::instance(); // we start with the previous values, make sure we can add them later on t = torig; MyMoneySplit s0 = sorig; s0.clearId(); auto sec = dynamic_cast(d->m_editWidgets["security"]); if (sec && (!isMultiSelection() || (isMultiSelection() && !sec->currentText().isEmpty()))) { QString securityId = sec->selectedItem(); if (!securityId.isEmpty()) { s0.setAccountId(securityId); MyMoneyAccount stockAccount = file->account(securityId); QString currencyId = stockAccount.currencyId(); MyMoneySecurity security = file->security(currencyId); t.setCommodity(security.tradingCurrency()); } else { s0.setAccountId(d->m_account.id()); t.setCommodity(d->m_account.currencyId()); } } // extract price info from original transaction d->m_priceInfo.clear(); if (!torig.id().isEmpty()) { foreach (const auto split, torig.splits()) { if (split.id() != sorig.id()) { auto cat = file->account(split.accountId()); if (cat.currencyId() != d->m_account.currencyId()) { if (cat.currencyId().isEmpty()) cat.setCurrencyId(d->m_account.currencyId()); if (!split.shares().isZero() && !split.value().isZero()) { d->m_priceInfo[cat.currencyId()] = (split.shares() / split.value()).reduce(); } } } } } t.removeSplits(); auto postDate = dynamic_cast(d->m_editWidgets["postdate"]); if (postDate && postDate->date().isValid()) { t.setPostDate(postDate->date()); } // memo and number field are special: if we have multiple transactions selected // and the edit field is empty, we treat it as "not modified". // FIXME a better approach would be to have a 'dirty' flag with the widgets // which identifies if the originally loaded value has been modified // by the user auto memo = dynamic_cast(d->m_editWidgets["memo"]); if (memo) { if (!isMultiSelection() || (isMultiSelection() && d->m_activity->memoChanged())) s0.setMemo(memo->toPlainText()); } MyMoneySplit assetAccountSplit; QList feeSplits; QList interestSplits; MyMoneySecurity security, currency; eMyMoney::Split::InvestmentTransactionType transactionType; // extract the splits from the original transaction KMyMoneyUtils::dissectTransaction(torig, sorig, assetAccountSplit, feeSplits, interestSplits, security, currency, transactionType); // check if the trading currency is the same if the security has changed // in case it differs, check that we have a price (request from user) // and convert all splits // TODO // do the conversions here // TODO // keep the current activity object and create a new one // that can be destroyed later on auto activity = d->m_activity; d->m_activity = 0; // make sure we create a new one d->activityFactory(activity->type()); // if the activity is not set in the combo widget, we keep // the one which is used in the original transaction auto activityCombo = dynamic_cast(haveWidget("activity")); if (activityCombo && activityCombo->activity() == eMyMoney::Split::InvestmentTransactionType::UnknownTransactionType) { d->activityFactory(transactionType); } // if we mark the split reconciled here, we'll use today's date if no reconciliation date is given auto status = dynamic_cast(d->m_editWidgets["status"]); if (status && status->state() != eMyMoney::Split::State::Unknown) s0.setReconcileFlag(status->state()); if (s0.reconcileFlag() == eMyMoney::Split::State::Reconciled && !s0.reconcileDate().isValid()) s0.setReconcileDate(QDate::currentDate()); // call the creation logic for the current selected activity bool rc = d->m_activity->createTransaction(t, s0, assetAccountSplit, feeSplits, d->m_feeSplits, interestSplits, d->m_interestSplits, security, currency); // now switch back to the original activity delete d->m_activity; d->m_activity = activity; // add the splits to the transaction if (rc) { if (security.name().isEmpty()) // new transaction has no security filled... security = file->security(file->account(s0.accountId()).currencyId()); // ...so fetch it from s0 split QList resultSplits; // concatenates splits for easy processing if (!assetAccountSplit.accountId().isEmpty()) resultSplits.append(assetAccountSplit); if (!feeSplits.isEmpty()) resultSplits.append(feeSplits); if (!interestSplits.isEmpty()) resultSplits.append(interestSplits); AlkValue::RoundingMethod roundingMethod = AlkValue::RoundRound; if (security.roundingMethod() != AlkValue::RoundNever) roundingMethod = security.roundingMethod(); int currencyFraction = currency.smallestAccountFraction(); int securityFraction = security.smallestAccountFraction(); // assuming that all non-stock splits are monetary foreach (auto split, resultSplits) { split.clearId(); split.setShares(MyMoneyMoney(split.shares().convertDenominator(currencyFraction, roundingMethod))); split.setValue(MyMoneyMoney(split.value().convertDenominator(currencyFraction, roundingMethod))); t.addSplit(split); } s0.setShares(MyMoneyMoney(s0.shares().convertDenominator(securityFraction, roundingMethod))); // only shares variable from stock split isn't evaluated in currency s0.setValue(MyMoneyMoney(s0.value().convertDenominator(currencyFraction, roundingMethod))); t.addSplit(s0); } return rc; } void InvestTransactionEditor::setupFinalWidgets() { addFinalWidget(haveWidget("memo")); } void InvestTransactionEditor::slotUpdateInvestMemoState() { Q_D(InvestTransactionEditor); auto memo = dynamic_cast(d->m_editWidgets["memo"]); if (memo) { d->m_activity->memoChanged() = (memo->toPlainText() != d->m_activity->memoText()); } } diff --git a/kmymoney/dialogs/kcategoryreassigndlg.h b/kmymoney/dialogs/kcategoryreassigndlg.h index fe116c440..2863655ad 100644 --- a/kmymoney/dialogs/kcategoryreassigndlg.h +++ b/kmymoney/dialogs/kcategoryreassigndlg.h @@ -1,68 +1,68 @@ /*************************************************************************** kcategoryreassigndlg.cpp ------------------- copyright : (C) 2007 by 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. * * * ***************************************************************************/ #ifndef KCATEGORYREASSIGNDLG_H #define KCATEGORYREASSIGNDLG_H // ---------------------------------------------------------------------------- // QT Includes #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes class MyMoneyAccount; namespace Ui { class KCategoryReassignDlg; } /** * Implementation of the dialog that lets the user select a payee in order * to re-assign transactions (for instance, if payees are deleted). */ class KCategoryReassignDlg : public QDialog { Q_OBJECT Q_DISABLE_COPY(KCategoryReassignDlg) public: explicit KCategoryReassignDlg(QWidget* parent = nullptr); ~KCategoryReassignDlg(); /** * This function sets up the dialog, lets the user select a category and returns * the id of the selected category in the list of all known income and expense accounts. * * @param category reference to MyMoneyAccount object of the category to be deleted * * @return Returns the id of the selected category in the list or QString() if * the dialog was aborted. QString() is also returned if the @a category * does not have an id. */ QString show(const MyMoneyAccount& category); protected: - void accept(); + void accept() final override; private: Ui::KCategoryReassignDlg *ui; }; #endif // KCATEGORYREASSIGNDLG_H diff --git a/kmymoney/dialogs/kcurrencyeditdlg.cpp b/kmymoney/dialogs/kcurrencyeditdlg.cpp index 62349294b..2d2d18bfc 100644 --- a/kmymoney/dialogs/kcurrencyeditdlg.cpp +++ b/kmymoney/dialogs/kcurrencyeditdlg.cpp @@ -1,621 +1,621 @@ /*************************************************************************** kcurrencyeditdlg.cpp - description ------------------- begin : Wed Mar 24 2004 copyright : (C) 2000-2004 by Michael Edwardes email : mte@users.sourceforge.net Javier Campos Morales Felix Rodriguez John C Thomas Baumgart Kevin Tambascio Alvaro Soliverez (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 "kcurrencyeditdlg.h" #include // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include // ---------------------------------------------------------------------------- // Project Includes #include "ui_kcurrencyeditdlg.h" #include "ui_kcurrencyeditordlg.h" #include "ui_kavailablecurrencydlg.h" #include "mymoneyexception.h" #include "mymoneysecurity.h" #include "mymoneyfile.h" #include "mymoneyprice.h" #include "kavailablecurrencydlg.h" #include "kcurrencyeditordlg.h" #include "kmymoneyutils.h" #include "icons/icons.h" #include "storageenums.h" using namespace Icons; // duplicated eMenu namespace from menuenums.h for consistency // there shouldn't be any clash, because we don't need menuenums.h here namespace eMenu { enum class Action { // ************* // The currency menu // ************* NewCurrency, RenameCurrency, DeleteCurrency, SetBaseCurrency, }; inline uint qHash(const Action key, uint seed) { return ::qHash(static_cast(key), seed); } } // this delegate is needed to disable editing the currency id (column 1) // since QTreeWidgetItem has only one set of flags for the whole row // the column editable property couldn't be set in an easier way class KCurrencyEditDelegate : public QStyledItemDelegate { public: explicit KCurrencyEditDelegate(QObject *parent = 0); protected: - QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const; + QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const final override; }; KCurrencyEditDelegate::KCurrencyEditDelegate(QObject* parent): QStyledItemDelegate(parent) { } QWidget *KCurrencyEditDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const { if (index.column() == 1) return 0; return QStyledItemDelegate::createEditor(parent, option, index); } class KCurrencyEditDlgPrivate { Q_DISABLE_COPY(KCurrencyEditDlgPrivate) Q_DECLARE_PUBLIC(KCurrencyEditDlg) public: explicit KCurrencyEditDlgPrivate(KCurrencyEditDlg *qq) : q_ptr(qq), ui(new Ui::KCurrencyEditDlg), m_availableCurrencyDlg(nullptr), m_currencyEditorDlg(nullptr), m_searchWidget(nullptr), m_inLoading(false) { } ~KCurrencyEditDlgPrivate() { delete ui; } enum removalModeE :int { RemoveSelected, RemoveUnused }; void removeCurrency(const removalModeE& mode) { Q_Q(KCurrencyEditDlg); auto file = MyMoneyFile::instance(); MyMoneyFileTransaction ft; QBitArray skip((int)eStorage::Reference::Count); skip.fill(false); // check reference to all... skip.setBit((int)eStorage::Reference::Price); // ...except price QTreeWidgetItemIterator it (ui->m_currencyList); // iterate over whole tree if (mode == RemoveUnused) { while (*it) { MyMoneySecurity currency = (*it)->data(0, Qt::UserRole).value(); if (file->baseCurrency() != currency && !file->isReferenced(currency, skip)) KMyMoneyUtils::deleteSecurity(currency, q); ++it; } } else if (mode == RemoveSelected) { QList currencyRows = ui->m_currencyList->selectedItems(); foreach(auto currencyRow, currencyRows) { MyMoneySecurity currency = currencyRow->data(0, Qt::UserRole).value(); if (file->baseCurrency() != currency && !file->isReferenced(currency, skip)) KMyMoneyUtils::deleteSecurity(currency, q); } } ft.commit(); ui->m_removeUnusedCurrencyButton->setDisabled(file->currencyList().count() <= 1); } void setBaseCurrency(const MyMoneySecurity& cur) { Q_Q(KCurrencyEditDlg); if (!cur.id().isEmpty()) { if (cur.id() != MyMoneyFile::instance()->baseCurrency().id()) { MyMoneyFileTransaction ft; try { MyMoneyFile::instance()->setBaseCurrency(cur); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::sorry(q, i18n("Cannot set %1 as base currency: %2", cur.name(), e.what()), i18n("Set base currency")); } } } } void updateCurrency(const QString ¤cyId, const QString& currencyName, const QString& currencyTradingSymbol) { Q_Q(KCurrencyEditDlg); const auto file = MyMoneyFile::instance(); try { if (currencyName != m_currentCurrency.name() || currencyTradingSymbol != m_currentCurrency.tradingSymbol()) { MyMoneySecurity currency = file->currency(currencyId); currency.setName(currencyName); currency.setTradingSymbol(currencyTradingSymbol); MyMoneyFileTransaction ft; try { file->modifyCurrency(currency); m_currentCurrency = currency; ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::sorry(q, i18n("Cannot update currency. %1", e.what()), i18n("Update currency")); } } } catch (const MyMoneyException &e) { KMessageBox::sorry(q, i18n("Cannot update currency. %1", e.what()), i18n("Update currency")); } } KCurrencyEditDlg *q_ptr; Ui::KCurrencyEditDlg *ui; KAvailableCurrencyDlg *m_availableCurrencyDlg; KCurrencyEditorDlg *m_currencyEditorDlg; MyMoneySecurity m_currentCurrency; /** * Search widget for the list */ KTreeWidgetSearchLineWidget* m_searchWidget; bool m_inLoading; }; KCurrencyEditDlg::KCurrencyEditDlg(QWidget *parent) : QDialog(parent), d_ptr(new KCurrencyEditDlgPrivate(this)) { Q_D(KCurrencyEditDlg); d->ui->setupUi(this); d->m_searchWidget = new KTreeWidgetSearchLineWidget(this, d->ui->m_currencyList); d->m_searchWidget->setSizePolicy(QSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed)); d->m_searchWidget->setFocus(); d->ui->verticalLayout->insertWidget(0, d->m_searchWidget); d->ui->m_currencyList->setItemDelegate(new KCurrencyEditDelegate(d->ui->m_currencyList)); d->ui->m_closeButton->setIcon(Icons::get(Icon::DialogClose)); d->ui->m_editCurrencyButton->setIcon(Icons::get(Icon::DocumentEdit)); d->ui->m_selectBaseCurrencyButton->setIcon(Icons::get(Icon::KMyMoney)); connect(d->ui->m_currencyList, &QWidget::customContextMenuRequested, this, &KCurrencyEditDlg::slotShowCurrencyMenu); connect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, this, &KCurrencyEditDlg::slotLoadCurrencies); connect(d->ui->m_currencyList, &QTreeWidget::itemChanged, this, static_cast(&KCurrencyEditDlg::slotUpdateCurrency)); connect(d->ui->m_currencyList, &QTreeWidget::itemSelectionChanged, this, &KCurrencyEditDlg::slotItemSelectionChanged); connect(d->ui->m_selectBaseCurrencyButton, &QAbstractButton::clicked, this, &KCurrencyEditDlg::slotSelectBaseCurrency); connect(d->ui->m_addCurrencyButton, &QAbstractButton::clicked, this, &KCurrencyEditDlg::slotAddCurrency); connect(d->ui->m_removeCurrencyButton, &QAbstractButton::clicked, this, &KCurrencyEditDlg::slotRemoveCurrency); connect(d->ui->m_editCurrencyButton, &QAbstractButton::clicked, this, &KCurrencyEditDlg::slotEditCurrency); connect(d->ui->m_removeUnusedCurrencyButton, &QAbstractButton::clicked, this, &KCurrencyEditDlg::slotRemoveUnusedCurrency); QTimer::singleShot(10, this, SLOT(timerDone())); } void KCurrencyEditDlg::timerDone() { Q_D(KCurrencyEditDlg); slotLoadCurrencies(); //resize the column widths for (auto i = 0; i < 3; ++i) d->ui->m_currencyList->resizeColumnToContents(i); if (!d->m_currentCurrency.id().isEmpty()) { QTreeWidgetItemIterator it(d->ui->m_currencyList); QTreeWidgetItem* q; while ((q = *it) != 0) { if (q->text(1) == d->m_currentCurrency.id()) { d->ui->m_currencyList->scrollToItem(q); break; } ++it; } } } KCurrencyEditDlg::~KCurrencyEditDlg() { Q_D(KCurrencyEditDlg); delete d; } void KCurrencyEditDlg::slotLoadCurrencies() { const QSet metalSymbols { "XAU", "XPD", "XPT", "XAG" }; Q_D(KCurrencyEditDlg); // catch recursive calls and avoid them if (d->m_inLoading) return; d->m_inLoading = true; disconnect(d->ui->m_currencyList, &QTreeWidget::currentItemChanged, this, static_cast(&KCurrencyEditDlg::slotSelectCurrency)); disconnect(d->ui->m_currencyList, &QTreeWidget::itemChanged, this, static_cast(&KCurrencyEditDlg::slotUpdateCurrency)); QList list = MyMoneyFile::instance()->currencyList(); QList::ConstIterator it; QTreeWidgetItem *first = 0; // sort the currencies ... // ... and make sure a few precious metals are at the ned qSort(list.begin(), list.end(), [=] (const MyMoneySecurity& c1, const MyMoneySecurity& c2) { const bool c1Metal = c1.tradingSymbol().startsWith(QLatin1Char('X')) && metalSymbols.contains(c1.tradingSymbol()); const bool c2Metal = c2.tradingSymbol().startsWith(QLatin1Char('X')) && metalSymbols.contains(c2.tradingSymbol()); if (c1Metal ^ c2Metal) return c2Metal; return c1.name().compare(c2.name()) < 0; }); QString localCurrency(localeconv()->int_curr_symbol); localCurrency.truncate(3); QString baseCurrency; try { baseCurrency = MyMoneyFile::instance()->baseCurrency().id(); } catch (const MyMoneyException &e) { qDebug("%s", qPrintable(e.what())); } // construct a transparent 16x16 pixmap QPixmap empty(16, 16); QBitmap mask(16, 16); mask.clear(); empty.setMask(mask); d->ui->m_currencyList->setSortingEnabled(false); d->ui->m_currencyList->clear(); for (it = list.constBegin(); it != list.constEnd(); ++it) { QTreeWidgetItem *p = new QTreeWidgetItem(d->ui->m_currencyList); p->setText(0, (*it).name()); p->setData(0, Qt::UserRole, QVariant::fromValue(*it)); p->setFlags(p->flags() | Qt::ItemIsEditable); p->setText(1, (*it).id()); // fix the ATS problem QString symbol = (*it).tradingSymbol(); if (((*it).id() == QLatin1String("ATS")) && (symbol != QString::fromUtf8("ÖS"))) { MyMoneySecurity tmp = d->m_currentCurrency; symbol = QString::fromUtf8("ÖS"); d->updateCurrency((*it).id(), (*it).name(), symbol); d->m_currentCurrency = tmp; } p->setText(2, symbol); if ((*it).id() == baseCurrency) { p->setData(0, Qt::DecorationRole, Icons::get(Icon::KMyMoney)); if (d->m_currentCurrency.id().isEmpty()) first = p; } else { p->setData(0, Qt::DecorationRole, empty); } // if we had a previously selected if (!d->m_currentCurrency.id().isEmpty()) { if (d->m_currentCurrency.id() == p->text(1)) first = p; } else if ((*it).id() == localCurrency && !first) first = p; } d->ui->m_removeUnusedCurrencyButton->setDisabled(list.count() <= 1); connect(d->ui->m_currencyList, &QTreeWidget::currentItemChanged, this, static_cast(&KCurrencyEditDlg::slotSelectCurrency)); connect(d->ui->m_currencyList, &QTreeWidget::itemChanged, this, static_cast(&KCurrencyEditDlg::slotUpdateCurrency)); if (first == 0) first = d->ui->m_currencyList->invisibleRootItem()->child(0); if (first != 0) { d->ui->m_currencyList->setCurrentItem(first); d->ui->m_currencyList->scrollToItem(first); } slotSelectCurrency(first); d->m_inLoading = false; } void KCurrencyEditDlg::slotUpdateCurrency(QTreeWidgetItem* citem, int) { slotUpdateCurrency(citem, nullptr); } void KCurrencyEditDlg::slotUpdateCurrency(QTreeWidgetItem* citem, QTreeWidgetItem *pitem) { Q_D(KCurrencyEditDlg); Q_UNUSED(pitem) //if there is no current item selected, exit if (!d->ui->m_currencyList->currentItem() || citem != d->ui->m_currencyList->currentItem()) return; //verify that the stored currency id is not empty and the edited fields are not empty either if (!d->m_currentCurrency.id().isEmpty() && !d->ui->m_currencyList->currentItem()->text(2).isEmpty() && !d->ui->m_currencyList->currentItem()->text(0).isEmpty()) { //check that either the name or the id have changed if (d->ui->m_currencyList->currentItem()->text(2) != d->m_currentCurrency.tradingSymbol() || d->ui->m_currencyList->currentItem()->text(0) != d->m_currentCurrency.name()) { //update the name and the id d->m_currentCurrency.setName(d->ui->m_currencyList->currentItem()->text(0)); d->m_currentCurrency.setTradingSymbol(d->ui->m_currencyList->currentItem()->text(2)); d->updateCurrency(d->m_currentCurrency.id(), d->m_currentCurrency.name(), d->m_currentCurrency.tradingSymbol()); } } } void KCurrencyEditDlg::slotSelectCurrency(const QString& id) { Q_D(KCurrencyEditDlg); QTreeWidgetItemIterator it(d->ui->m_currencyList); while (*it) { if ((*it)->text(1) == id) { d->ui->m_currencyList->blockSignals(true); slotSelectCurrency(*it); d->ui->m_currencyList->setCurrentItem(*it); d->ui->m_currencyList->scrollToItem(*it); d->ui->m_currencyList->blockSignals(false); break; } ++it; } } void KCurrencyEditDlg::slotSelectCurrency(QTreeWidgetItem *citem, QTreeWidgetItem *pitem) { Q_UNUSED(pitem) slotSelectCurrency(citem); } void KCurrencyEditDlg::slotSelectCurrency(QTreeWidgetItem *item) { Q_D(KCurrencyEditDlg); auto file = MyMoneyFile::instance(); QString baseId; try { baseId = MyMoneyFile::instance()->baseCurrency().id(); } catch (const MyMoneyException &) { } if (item) { try { d->m_currentCurrency = file->security(item->text(1)); } catch (const MyMoneyException &) { d->m_currentCurrency = MyMoneySecurity(); } QBitArray skip((int)eStorage::Reference::Count); skip.fill(false); skip.setBit((int)eStorage::Reference::Price); const bool rc1 = d->m_currentCurrency.id() == baseId; const bool rc2 = file->isReferenced(d->m_currentCurrency, skip); const int count = d->ui->m_currencyList->selectedItems().count(); d->ui->m_selectBaseCurrencyButton->setDisabled(rc1 || count != 1); d->ui->m_editCurrencyButton->setDisabled(count != 1); d->ui->m_removeCurrencyButton->setDisabled((rc1 || rc2) && count <= 1); // emit selectObject(d->m_currentCurrency); } } void KCurrencyEditDlg::slotItemSelectionChanged() { Q_D(KCurrencyEditDlg); int count = d->ui->m_currencyList->selectedItems().count(); if (!d->ui->m_selectBaseCurrencyButton->isEnabled() && count == 1) slotSelectCurrency(d->ui->m_currencyList->currentItem()); if (count > 1) d->ui->m_removeCurrencyButton->setEnabled(true); } void KCurrencyEditDlg::slotShowCurrencyMenu(const QPoint& p) { Q_D(KCurrencyEditDlg); auto item = d->ui->m_currencyList->itemAt(p); if (item) { const auto sec = item->data(0, Qt::UserRole).value(); const auto file = MyMoneyFile::instance(); QBitArray skip((int)eStorage::Reference::Count); skip.fill(false); skip.setBit((int)eStorage::Reference::Transaction); const auto cond1 = !sec.id().isEmpty(); const auto cond2 = cond1 && !file->isReferenced(sec, skip); const auto cond3 = cond1 && sec.id() != file->baseCurrency().id(); auto menu = new QMenu; typedef void(KCurrencyEditDlg::*KCurrencyEditDlgFunc)(); struct actionInfo { eMenu::Action action; KCurrencyEditDlgFunc callback; QString text; Icon icon; bool enabled; }; const QVector actionInfos { {eMenu::Action::NewCurrency, &KCurrencyEditDlg::slotNewCurrency, i18n("New currency"), Icon::ListAdd, true}, {eMenu::Action::RenameCurrency, &KCurrencyEditDlg::slotRenameCurrency, i18n("Rename currency"), Icon::EditRename, cond1}, {eMenu::Action::DeleteCurrency, &KCurrencyEditDlg::slotDeleteCurrency, i18n("Delete currency"), Icon::EditDelete, cond2}, {eMenu::Action::SetBaseCurrency, &KCurrencyEditDlg::slotSetBaseCurrency, i18n("Select as base currency"), Icon::KMyMoney, cond3} }; QList LUTActions; for (const auto& info : actionInfos) { auto a = new QAction(Icons::get(info.icon), info.text, nullptr); // WARNING: no empty Icon::Empty here a->setEnabled(info.enabled); connect(a, &QAction::triggered, this, info.callback); LUTActions.append(a); } menu->addSection(i18nc("Menu header","Currency options")); menu->addActions(LUTActions); menu->exec(QCursor::pos()); } } void KCurrencyEditDlg::slotSelectBaseCurrency() { Q_D(KCurrencyEditDlg); if (!d->m_currentCurrency.id().isEmpty()) { QTreeWidgetItem* p = d->ui->m_currencyList->currentItem(); d->setBaseCurrency(d->m_currentCurrency); // in case the dataChanged() signal was not sent out (nested FileTransaction) // we update the list manually if (p == d->ui->m_currencyList->currentItem()) slotLoadCurrencies(); } } void KCurrencyEditDlg::slotAddCurrency() { Q_D(KCurrencyEditDlg); d->m_availableCurrencyDlg = new KAvailableCurrencyDlg; // create new dialog for selecting currencies to add if (d->m_availableCurrencyDlg->exec() != QDialog::Rejected) { auto file = MyMoneyFile::instance(); QMap ancientCurrencies = file->ancientCurrencies(); MyMoneyFileTransaction ft; QList currencyRows = d->m_availableCurrencyDlg->ui->m_currencyList->selectedItems(); // get selected currencies from new dialog foreach (auto currencyRow, currencyRows) { MyMoneySecurity currency = currencyRow->data(0, Qt::UserRole).value(); file->addCurrency(currency); if (ancientCurrencies.value(currency, MyMoneyPrice()) != MyMoneyPrice()) // if ancient currency is added... file->addPrice(ancientCurrencies[currency]); // ...we want to add last known exchange rate as well } ft.commit(); d->ui->m_removeUnusedCurrencyButton->setDisabled(file->currencyList().count() <= 1); } delete d->m_availableCurrencyDlg; } void KCurrencyEditDlg::slotRemoveCurrency() { Q_D(KCurrencyEditDlg); d->removeCurrency(KCurrencyEditDlgPrivate::RemoveSelected); } void KCurrencyEditDlg::slotRemoveUnusedCurrency() { Q_D(KCurrencyEditDlg); d->removeCurrency(KCurrencyEditDlgPrivate::RemoveUnused); } void KCurrencyEditDlg::slotEditCurrency() { Q_D(KCurrencyEditDlg); MyMoneySecurity currency = d->ui->m_currencyList->currentItem()->data(0, Qt::UserRole).value(); d->m_currencyEditorDlg = new KCurrencyEditorDlg(currency); // create new dialog for editing currency if (d->m_currencyEditorDlg->exec() != QDialog::Rejected) { auto file = MyMoneyFile::instance(); MyMoneyFileTransaction ft; currency.setPricePrecision(d->m_currencyEditorDlg->ui->m_pricePrecision->value()); try { file->modifyCurrency(currency); ft.commit(); } catch (const MyMoneyException &e) { qDebug("%s", qPrintable(e.what())); } } delete d->m_currencyEditorDlg; } void KCurrencyEditDlg::slotNewCurrency() { QString sid = QInputDialog::getText(0, i18n("New currency"), i18n("Enter ISO 4217 code for the new currency")); if (!sid.isEmpty()) { QString id(sid); MyMoneySecurity currency(id, i18n("New currency")); MyMoneyFileTransaction ft; try { MyMoneyFile::instance()->addCurrency(currency); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::sorry(this, i18n("Cannot create new currency. %1", e.what()), i18n("New currency")); } slotSelectCurrency(id); } } void KCurrencyEditDlg::slotRenameCurrency() { Q_D(KCurrencyEditDlg); QTreeWidgetItemIterator it_l(d->ui->m_currencyList, QTreeWidgetItemIterator::Selected); QTreeWidgetItem* it_v; if ((it_v = *it_l) != 0) { d->ui->m_currencyList->editItem(it_v, 0); } } void KCurrencyEditDlg::slotDeleteCurrency() { Q_D(KCurrencyEditDlg); if (!d->m_currentCurrency.id().isEmpty()) { MyMoneyFileTransaction ft; try { MyMoneyFile::instance()->removeCurrency(d->m_currentCurrency); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::sorry(this, i18n("Cannot delete currency %1. %2", d->m_currentCurrency.name(), e.what()), i18n("Delete currency")); } } } void KCurrencyEditDlg::slotSetBaseCurrency() { Q_D(KCurrencyEditDlg); if (!d->m_currentCurrency.id().isEmpty()) { if (d->m_currentCurrency.id() != MyMoneyFile::instance()->baseCurrency().id()) { MyMoneyFileTransaction ft; try { MyMoneyFile::instance()->setBaseCurrency(d->m_currentCurrency); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::sorry(this, i18n("Cannot set %1 as base currency: %2", d->m_currentCurrency.name(), e.what()), i18n("Set base currency")); } } } } diff --git a/kmymoney/dialogs/keditscheduledlg.cpp b/kmymoney/dialogs/keditscheduledlg.cpp index 25ac9c8ae..6bcf643a6 100644 --- a/kmymoney/dialogs/keditscheduledlg.cpp +++ b/kmymoney/dialogs/keditscheduledlg.cpp @@ -1,776 +1,776 @@ /*************************************************************************** keditscheduledlg.cpp - description ------------------- begin : Mon Sep 3 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 "keditscheduledlg.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "ui_keditscheduledlg.h" #include "tabbar.h" #include "mymoneyexception.h" #include "mymoneyfile.h" #include "mymoneyaccount.h" #include "mymoneymoney.h" #include "mymoneyschedule.h" #include "mymoneysplit.h" #include "mymoneytransaction.h" #include "register.h" #include "transactionform.h" #include "transaction.h" #include "selectedtransactions.h" #include "transactioneditor.h" #include "kmymoneylineedit.h" #include "kmymoneydateinput.h" #include "kmymoneymvccombo.h" #include "kguiutils.h" #include "kmymoneyutils.h" #include "knewaccountdlg.h" #include "knewinvestmentwizard.h" #include "keditloanwizard.h" #include "kmymoneysettings.h" #include "mymoneyenums.h" #include "widgetenums.h" using namespace eMyMoney; class KEditScheduleDlgPrivate { Q_DISABLE_COPY(KEditScheduleDlgPrivate) Q_DECLARE_PUBLIC(KEditScheduleDlg) public: explicit KEditScheduleDlgPrivate(KEditScheduleDlg *qq) : q_ptr(qq), ui(new Ui::KEditScheduleDlg), m_item(nullptr), m_editor(nullptr), m_requiredFields(nullptr) { } ~KEditScheduleDlgPrivate() { delete ui; } void init() { Q_Q(KEditScheduleDlg); ui->setupUi(q); m_requiredFields = new KMandatoryFieldGroup(q); m_requiredFields->setOkButton(ui->buttonBox->button(QDialogButtonBox::Ok)); // button to be enabled when all fields present // make sure, we have a tabbar with the form // insert it after the horizontal line ui->m_paymentInformationLayout->insertWidget(2, ui->m_form->getTabBar(ui->m_form->parentWidget())); // we never need to see the register ui->m_register->hide(); // ... setup the form ... ui->m_form->setupForm(m_schedule.account()); // ... and the register ... ui->m_register->clear(); // ... now add the transaction to register and form ... auto t = transaction(); if (m_schedule.transaction().splits().isEmpty()) m_item = KMyMoneyRegister::Register::transactionFactory(ui->m_register, t, MyMoneySplit(), 0); else m_item = KMyMoneyRegister::Register::transactionFactory(ui->m_register, t, m_schedule.transaction().splits().isEmpty() ? MyMoneySplit() : m_schedule.transaction().splits().front(), 0); ui->m_register->selectItem(m_item); // show the account row m_item->setShowRowInForm(0, true); ui->m_form->slotSetTransaction(m_item); // setup widget contents ui->m_nameEdit->setText(m_schedule.name()); ui->m_frequencyEdit->setCurrentItem((int)m_schedule.occurrencePeriod()); if (ui->m_frequencyEdit->currentItem() == Schedule::Occurrence::Any) ui->m_frequencyEdit->setCurrentItem((int)Schedule::Occurrence::Monthly); q->slotFrequencyChanged((int)ui->m_frequencyEdit->currentItem()); ui->m_frequencyNoEdit->setValue(m_schedule.occurrenceMultiplier()); // load option widgets ui->m_paymentMethodEdit->insertItem(i18n("Direct deposit"), (int)Schedule::PaymentType::DirectDeposit); ui->m_paymentMethodEdit->insertItem(i18n("Manual deposit"), (int)Schedule::PaymentType::ManualDeposit); ui->m_paymentMethodEdit->insertItem(i18n("Direct debit"), (int)Schedule::PaymentType::DirectDebit); ui->m_paymentMethodEdit->insertItem(i18n("Standing order"), (int)Schedule::PaymentType::StandingOrder); ui->m_paymentMethodEdit->insertItem(i18n("Bank transfer"), (int)Schedule::PaymentType::BankTransfer); ui->m_paymentMethodEdit->insertItem(i18n("Write check"), (int)Schedule::PaymentType::WriteChecque); ui->m_paymentMethodEdit->insertItem(i18nc("Other payment method", "Other"), (int)Schedule::PaymentType::Other); auto method = m_schedule.paymentType(); if (method == Schedule::PaymentType::Any) method = Schedule::PaymentType::Other; ui->m_paymentMethodEdit->setCurrentItem((int)method); switch (m_schedule.weekendOption()) { case Schedule::WeekendOption::MoveNothing: ui->m_weekendOptionEdit->setCurrentIndex(0); break; case Schedule::WeekendOption::MoveBefore: ui->m_weekendOptionEdit->setCurrentIndex(1); break; case Schedule::WeekendOption::MoveAfter: ui->m_weekendOptionEdit->setCurrentIndex(2); break; } ui->m_estimateEdit->setChecked(!m_schedule.isFixed()); ui->m_lastDayInMonthEdit->setChecked(m_schedule.lastDayInMonth()); ui->m_autoEnterEdit->setChecked(m_schedule.autoEnter()); ui->m_endSeriesEdit->setChecked(m_schedule.willEnd()); ui->m_endOptionsFrame->setEnabled(m_schedule.willEnd()); if (m_schedule.willEnd()) { ui->m_RemainingEdit->setValue(m_schedule.transactionsRemaining()); ui->m_FinalPaymentEdit->setDate(m_schedule.endDate()); } q->connect(ui->m_RemainingEdit, static_cast(&QSpinBox::valueChanged), q, &KEditScheduleDlg::slotRemainingChanged); q->connect(ui->m_FinalPaymentEdit, &KMyMoneyDateInput::dateChanged, q, &KEditScheduleDlg::slotEndDateChanged); q->connect(ui->m_frequencyEdit, &KMyMoneyGeneralCombo::itemSelected, q, &KEditScheduleDlg::slotFrequencyChanged); q->connect(ui->m_frequencyNoEdit, static_cast(&QSpinBox::valueChanged), q, &KEditScheduleDlg::slotOccurrenceMultiplierChanged); q->connect(ui->buttonBox, &QDialogButtonBox::helpRequested, q, &KEditScheduleDlg::slotShowHelp); q->setModal(true); // force the initial height to be as small as possible QTimer::singleShot(0, q, SLOT(slotSetupSize())); // we just hide the variation field for now and enable the logic // once we have a respective member in the MyMoneySchedule object ui->m_variation->hide(); } /** * Helper method to recalculate and update Transactions Remaining * when other values are changed */ void updateTransactionsRemaining() { auto remain = m_schedule.transactionsRemaining(); if (remain != ui->m_RemainingEdit->value()) { ui->m_RemainingEdit->blockSignals(true); ui->m_RemainingEdit->setValue(remain); ui->m_RemainingEdit->blockSignals(false); } } MyMoneyTransaction transaction() const { auto t = m_schedule.transaction(); if (m_editor) { m_editor->createTransaction(t, m_schedule.transaction(), m_schedule.transaction().splits().isEmpty() ? MyMoneySplit() : m_schedule.transaction().splits().front(), false); } t.clearId(); t.setEntryDate(QDate()); return t; } KEditScheduleDlg *q_ptr; Ui::KEditScheduleDlg *ui; MyMoneySchedule m_schedule; KMyMoneyRegister::Transaction* m_item; QWidgetList m_tabOrderWidgets; TransactionEditor* m_editor; KMandatoryFieldGroup* m_requiredFields; }; KEditScheduleDlg::KEditScheduleDlg(const MyMoneySchedule& schedule, QWidget *parent) : QDialog(parent), d_ptr(new KEditScheduleDlgPrivate(this)) { Q_D(KEditScheduleDlg); d->m_schedule = schedule; d->m_editor = 0; d->init(); } KEditScheduleDlg::~KEditScheduleDlg() { Q_D(KEditScheduleDlg); delete d; } void KEditScheduleDlg::slotSetupSize() { resize(width(), minimumSizeHint().height()); } TransactionEditor* KEditScheduleDlg::startEdit() { Q_D(KEditScheduleDlg); KMyMoneyRegister::SelectedTransactions list(d->ui->m_register); TransactionEditor* editor = d->m_item->createEditor(d->ui->m_form, list, QDate()); // check that we use the same transaction commodity in all selected transactions // if not, we need to update this in the editor's list. The user can also bail out // of this operation which means that we have to stop editing here. if (editor && !d->m_schedule.account().id().isEmpty()) { if (!editor->fixTransactionCommodity(d->m_schedule.account())) { // if the user wants to quit, we need to destroy the editor // and bail out delete editor; editor = 0; } } if (editor) { editor->setScheduleInfo(d->ui->m_nameEdit->text()); connect(editor, &TransactionEditor::transactionDataSufficient, d->ui->buttonBox->button(QDialogButtonBox::Ok), &QWidget::setEnabled); connect(editor, &TransactionEditor::escapePressed, d->ui->buttonBox->button(QDialogButtonBox::Cancel), &QAbstractButton::animateClick); connect(editor, &TransactionEditor::returnPressed, d->ui->buttonBox->button(QDialogButtonBox::Ok), &QAbstractButton::animateClick); connect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, editor, &TransactionEditor::slotReloadEditWidgets); // connect(editor, SIGNAL(finishEdit(KMyMoneyRegister::SelectedTransactions)), this, SLOT(slotLeaveEditMode(KMyMoneyRegister::SelectedTransactions))); connect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, editor, &TransactionEditor::slotReloadEditWidgets); // create the widgets, place them in the parent and load them with data // setup tab order d->m_tabOrderWidgets.clear(); eWidgets::eRegister::Action action = eWidgets::eRegister::Action::Withdrawal; switch (d->m_schedule.type()) { case Schedule::Type::Deposit: action = eWidgets::eRegister::Action::Deposit; break; case Schedule::Type::Bill: action = eWidgets::eRegister::Action::Withdrawal; editor->setPaymentMethod(d->m_schedule.paymentType()); break; case Schedule::Type::Transfer: action = eWidgets::eRegister::Action::Transfer; break; default: // if we end up here, we don't have a known schedule type (yet). in this case, we just glimpse // into the transaction and determine the type. in case we don't have a transaction with splits // we stick with the default action already set up if (d->m_schedule.transaction().splits().count() > 0) { auto isDeposit = false; auto isTransfer = false; auto splits = d->m_schedule.transaction().splits(); foreach (const auto split, splits) { if (split.accountId() == d->m_schedule.account().id()) { isDeposit = !(split.shares().isNegative()); } else { auto acc = MyMoneyFile::instance()->account(split.accountId()); if (acc.isAssetLiability() && d->m_schedule.transaction().splits().count() == 2) { isTransfer = true; } } } if (isTransfer) action = eWidgets::eRegister::Action::Transfer; else if (isDeposit) action = eWidgets::eRegister::Action::Deposit; } break; } editor->setup(d->m_tabOrderWidgets, d->m_schedule.account(), action); // if it's not a check, then we need to clear // a possibly assigned check number if (d->m_schedule.paymentType() != Schedule::PaymentType::WriteChecque) { QWidget* w = editor->haveWidget("number"); if (w) { if (auto numberWidget = dynamic_cast(w)) numberWidget->loadText(QString()); } } Q_ASSERT(!d->m_tabOrderWidgets.isEmpty()); d->m_tabOrderWidgets.push_front(d->ui->m_paymentMethodEdit); // editor->setup() leaves the tabbar as the last widget in the stack, but we // need it as first here. So we move it around. QWidget* w = editor->haveWidget("tabbar"); if (w) { int idx = d->m_tabOrderWidgets.indexOf(w); if (idx != -1) { d->m_tabOrderWidgets.removeAt(idx); d->m_tabOrderWidgets.push_front(w); } } // don't forget our three buttons and additional widgets // make sure to use the correct order d->m_tabOrderWidgets.push_front(d->ui->m_frequencyEdit); d->m_tabOrderWidgets.push_front(d->ui->m_frequencyNoEdit); d->m_tabOrderWidgets.push_front(d->ui->m_nameEdit); d->m_tabOrderWidgets.append(d->ui->m_weekendOptionEdit); d->m_tabOrderWidgets.append(d->ui->m_estimateEdit); d->m_tabOrderWidgets.append(d->ui->m_variation); d->m_tabOrderWidgets.append(d->ui->m_lastDayInMonthEdit); d->m_tabOrderWidgets.append(d->ui->m_autoEnterEdit); d->m_tabOrderWidgets.append(d->ui->m_endSeriesEdit); d->m_tabOrderWidgets.append(d->ui->m_RemainingEdit); d->m_tabOrderWidgets.append(d->ui->m_FinalPaymentEdit); d->m_tabOrderWidgets.append(d->ui->buttonBox->button(QDialogButtonBox::Ok)); d->m_tabOrderWidgets.append(d->ui->buttonBox->button(QDialogButtonBox::Cancel)); d->m_tabOrderWidgets.append(d->ui->buttonBox->button(QDialogButtonBox::Help)); for (auto i = 0; i < d->m_tabOrderWidgets.size(); ++i) { - QWidget* w = d->m_tabOrderWidgets.at(i); + w = d->m_tabOrderWidgets.at(i); if (w) { w->installEventFilter(this); w->installEventFilter(editor); } } // connect the postdate modification signal to our update routine if (auto dateEdit = dynamic_cast(editor->haveWidget("postdate"))) connect(dateEdit, &KMyMoneyDateInput::dateChanged, this, &KEditScheduleDlg::slotPostDateChanged); d->ui->m_nameEdit->setFocus(); // add the required fields to the mandatory group d->m_requiredFields->add(d->ui->m_nameEdit); d->m_requiredFields->add(editor->haveWidget("account")); d->m_requiredFields->add(editor->haveWidget("category")); // fix labels if (auto label = dynamic_cast(editor->haveWidget("date-label"))) label->setText(i18n("Next due date")); d->m_editor = editor; slotSetPaymentMethod((int)d->m_schedule.paymentType()); connect(d->ui->m_paymentMethodEdit, &KMyMoneyGeneralCombo::itemSelected, this, &KEditScheduleDlg::slotSetPaymentMethod); connect(editor, &TransactionEditor::operationTypeChanged, this, &KEditScheduleDlg::slotFilterPaymentType); } return editor; } void KEditScheduleDlg::accept() { Q_D(KEditScheduleDlg); // Force the focus to be on the OK button. This will trigger creation // of any unknown objects (payees, categories etc.) d->ui->buttonBox->button(QDialogButtonBox::Ok)->setFocus(); // only accept if the button is really still enabled. We could end // up here, if the user filled all fields, the focus is on the category // field, but the category is not yet existent. When the user presses the // OK button in this context, he will be asked if he wants to create // the category or not. In case he decides no, we end up here with no // category filled in, so we don't run through the final acceptance. if (d->ui->buttonBox->button(QDialogButtonBox::Ok)->isEnabled()) QDialog::accept(); } const MyMoneySchedule& KEditScheduleDlg::schedule() { Q_D(KEditScheduleDlg); if (d->m_editor) { auto t = d->transaction(); if (d->m_schedule.nextDueDate() != t.postDate()) { d->m_schedule.setNextDueDate(t.postDate()); d->m_schedule.setStartDate(t.postDate()); } d->m_schedule.setTransaction(t); d->m_schedule.setName(d->ui->m_nameEdit->text()); d->m_schedule.setFixed(!d->ui->m_estimateEdit->isChecked()); d->m_schedule.setOccurrencePeriod(static_cast(d->ui->m_frequencyEdit->currentItem())); d->m_schedule.setOccurrenceMultiplier(d->ui->m_frequencyNoEdit->value()); switch (d->ui->m_weekendOptionEdit->currentIndex()) { case 0: d->m_schedule.setWeekendOption(Schedule::WeekendOption::MoveNothing); break; case 1: d->m_schedule.setWeekendOption(Schedule::WeekendOption::MoveBefore); break; case 2: d->m_schedule.setWeekendOption(Schedule::WeekendOption::MoveAfter); break; } d->m_schedule.setType(Schedule::Type::Bill); if (auto tabbar = dynamic_cast(d->m_editor->haveWidget("tabbar"))) { switch (static_cast(tabbar->currentIndex())) { case eWidgets::eRegister::Action::Deposit: d->m_schedule.setType(Schedule::Type::Deposit); break; default: case eWidgets::eRegister::Action::Withdrawal: d->m_schedule.setType(Schedule::Type::Bill); break; case eWidgets::eRegister::Action::Transfer: d->m_schedule.setType(Schedule::Type::Transfer); break; } } else { qDebug("No tabbar found in KEditScheduleDlg::schedule(). Defaulting type to BILL"); } if(d->ui->m_lastDayInMonthEdit->isEnabled()) d->m_schedule.setLastDayInMonth(d->ui->m_lastDayInMonthEdit->isChecked()); else d->m_schedule.setLastDayInMonth(false); d->m_schedule.setAutoEnter(d->ui->m_autoEnterEdit->isChecked()); d->m_schedule.setPaymentType(static_cast(d->ui->m_paymentMethodEdit->currentItem())); if (d->ui->m_endSeriesEdit->isEnabled() && d->ui->m_endSeriesEdit->isChecked()) { d->m_schedule.setEndDate(d->ui->m_FinalPaymentEdit->date()); } else { d->m_schedule.setEndDate(QDate()); } } return d->m_schedule; } void KEditScheduleDlg::newSchedule(const MyMoneyTransaction& _t, eMyMoney::Schedule::Occurrence occurrence) { MyMoneySchedule schedule; schedule.setOccurrence(occurrence); // if the schedule is based on an existing transaction, // we take the post date and project it to the next // schedule in a month. if (_t != MyMoneyTransaction()) { MyMoneyTransaction t(_t); schedule.setTransaction(t); if (occurrence != eMyMoney::Schedule::Occurrence::Once) schedule.setNextDueDate(schedule.nextPayment(t.postDate())); } QPointer dlg = new KEditScheduleDlg(schedule, nullptr); QPointer transactionEditor = dlg->startEdit(); if (transactionEditor) { KMyMoneyMVCCombo::setSubstringSearchForChildren(dlg, !KMyMoneySettings::stringMatchFromStart()); if (dlg->exec() == QDialog::Accepted && dlg != 0) { MyMoneyFileTransaction ft; try { schedule = dlg->schedule(); MyMoneyFile::instance()->addSchedule(schedule); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::error(nullptr, i18n("Unable to add scheduled transaction: %1", e.what()), i18n("Add scheduled transaction")); } } } delete transactionEditor; delete dlg; } void KEditScheduleDlg::editSchedule(const MyMoneySchedule& inputSchedule) { try { auto schedule = MyMoneyFile::instance()->schedule(inputSchedule.id()); KEditScheduleDlg* sched_dlg = nullptr; KEditLoanWizard* loan_wiz = nullptr; switch (schedule.type()) { case eMyMoney::Schedule::Type::Bill: case eMyMoney::Schedule::Type::Deposit: case eMyMoney::Schedule::Type::Transfer: { sched_dlg = new KEditScheduleDlg(schedule, nullptr); QPointer transactionEditor = sched_dlg->startEdit(); if (transactionEditor) { KMyMoneyMVCCombo::setSubstringSearchForChildren(sched_dlg, !KMyMoneySettings::stringMatchFromStart()); if (sched_dlg->exec() == QDialog::Accepted) { MyMoneyFileTransaction ft; try { MyMoneySchedule sched = sched_dlg->schedule(); // Check whether the new Schedule Date // is at or before the lastPaymentDate // If it is, ask the user whether to clear the // lastPaymentDate const auto& next = sched.nextDueDate(); const auto& last = sched.lastPayment(); if (next.isValid() && last.isValid() && next <= last) { // Entered a date effectively no later // than previous payment. Date would be // updated automatically so we probably // want to clear it. Let's ask the user. if (KMessageBox::questionYesNo(nullptr, i18n("You have entered a scheduled transaction date of %1. Because the scheduled transaction was last paid on %2, KMyMoney will automatically adjust the scheduled transaction date to the next date unless the last payment date is reset. Do you want to reset the last payment date?", QLocale().toString(next, QLocale::ShortFormat), QLocale().toString(last, QLocale::ShortFormat)), i18n("Reset Last Payment Date"), KStandardGuiItem::yes(), KStandardGuiItem::no()) == KMessageBox::Yes) { sched.setLastPayment(QDate()); } } MyMoneyFile::instance()->modifySchedule(sched); // delete the editor before we emit the dataChanged() signal from the // engine. Calling this twice in a row does not hurt. delete transactionEditor; ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(nullptr, i18n("Unable to modify scheduled transaction '%1'", inputSchedule.name()), e.what()); } } delete transactionEditor; } delete sched_dlg; break; } case eMyMoney::Schedule::Type::LoanPayment: { loan_wiz = new KEditLoanWizard(schedule.account(2)); if (loan_wiz->exec() == QDialog::Accepted) { MyMoneyFileTransaction ft; try { MyMoneyFile::instance()->modifySchedule(loan_wiz->schedule()); MyMoneyFile::instance()->modifyAccount(loan_wiz->account()); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(nullptr, i18n("Unable to modify scheduled transaction '%1'", inputSchedule.name()), e.what()); } } delete loan_wiz; break; } case eMyMoney::Schedule::Type::Any: break; } } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(nullptr, i18n("Unable to modify scheduled transaction '%1'", inputSchedule.name()), e.what()); } } bool KEditScheduleDlg::focusNextPrevChild(bool next) { Q_D(KEditScheduleDlg); auto rc = false; auto w = qApp->focusWidget(); auto currentWidgetIndex = d->m_tabOrderWidgets.indexOf(w); while (w && currentWidgetIndex == -1) { // qDebug("'%s' not in list, use parent", qPrintable(w->objectName())); w = w->parentWidget(); currentWidgetIndex = d->m_tabOrderWidgets.indexOf(w); } if (currentWidgetIndex != -1) { do { // if(w) qDebug("tab order is at '%s (%d/%d)'", qPrintable(w->objectName()), currentWidgetIndex, d->m_tabOrderWidgets.size()); currentWidgetIndex += next ? 1 : -1; if (currentWidgetIndex < 0) currentWidgetIndex = d->m_tabOrderWidgets.size() - 1; else if (currentWidgetIndex >= d->m_tabOrderWidgets.size()) currentWidgetIndex = 0; w = d->m_tabOrderWidgets[currentWidgetIndex]; // qDebug("currentWidgetIndex = %d, w = %p", currentWidgetIndex, w); if (((w->focusPolicy() & Qt::TabFocus) == Qt::TabFocus) && w->isVisible() && w->isEnabled()) { // qDebug("Selecting '%s' as focus", qPrintable(w->objectName())); w->setFocus(); rc = true; } } while (rc == false); } return rc; } void KEditScheduleDlg::resizeEvent(QResizeEvent* ev) { Q_D(KEditScheduleDlg); d->ui->m_register->resize((int)eWidgets::eTransaction::Column::Detail); d->ui->m_form->resize((int)eWidgets::eTransactionForm::Column::Value1); QDialog::resizeEvent(ev); } void KEditScheduleDlg::slotRemainingChanged(int value) { Q_D(KEditScheduleDlg); // Make sure the required fields are set if (auto dateEdit = dynamic_cast(d->m_editor->haveWidget("postdate"))) d->m_schedule.setNextDueDate(dateEdit->date()); d->m_schedule.setOccurrencePeriod(static_cast(d->ui->m_frequencyEdit->currentItem())); d->m_schedule.setOccurrenceMultiplier(d->ui->m_frequencyNoEdit->value()); if (d->m_schedule.transactionsRemaining() != value) { d->ui->m_FinalPaymentEdit->blockSignals(true); d->ui->m_FinalPaymentEdit->setDate(d->m_schedule.dateAfter(value)); d->ui->m_FinalPaymentEdit->blockSignals(false); } } void KEditScheduleDlg::slotEndDateChanged(const QDate& date) { Q_D(KEditScheduleDlg); // Make sure the required fields are set if (auto dateEdit = dynamic_cast(d->m_editor->haveWidget("postdate"))) d->m_schedule.setNextDueDate(dateEdit->date()); d->m_schedule.setOccurrencePeriod(static_cast(d->ui->m_frequencyEdit->currentItem())); d->m_schedule.setOccurrenceMultiplier(d->ui->m_frequencyNoEdit->value()); if (d->m_schedule.endDate() != date) { d->m_schedule.setEndDate(date); d->updateTransactionsRemaining(); } } void KEditScheduleDlg::slotPostDateChanged(const QDate& date) { Q_D(KEditScheduleDlg); if (d->m_schedule.nextDueDate() != date) { if (d->ui->m_endOptionsFrame->isEnabled()) { d->m_schedule.setNextDueDate(date); d->m_schedule.setOccurrenceMultiplier(d->ui->m_frequencyNoEdit->value()); d->m_schedule.setOccurrencePeriod(static_cast(d->ui->m_frequencyEdit->currentItem())); d->m_schedule.setEndDate(d->ui->m_FinalPaymentEdit->date()); d->updateTransactionsRemaining(); } } } void KEditScheduleDlg::slotSetPaymentMethod(int item) { Q_D(KEditScheduleDlg); if (auto dateEdit = dynamic_cast(d->m_editor->haveWidget("number"))) { dateEdit->setVisible(item == (int)Schedule::PaymentType::WriteChecque); // hiding the label does not work, because the label underneath will shine // through. So we either write the label or a blank if (auto label = dynamic_cast(d->m_editor->haveWidget("number-label"))) label->setText((item == (int)Schedule::PaymentType::WriteChecque) ? i18n("Number") : " "); } } void KEditScheduleDlg::slotFrequencyChanged(int item) { Q_D(KEditScheduleDlg); d->ui->m_endSeriesEdit->setEnabled(item != (int)Schedule::Occurrence::Once); bool isEndSeries = d->ui->m_endSeriesEdit->isChecked(); if (isEndSeries) d->ui->m_endOptionsFrame->setEnabled(item != (int)Schedule::Occurrence::Once); switch (item) { case (int)Schedule::Occurrence::Daily: case (int)Schedule::Occurrence::Weekly: d->ui->m_frequencyNoEdit->setEnabled(true); d->ui->m_lastDayInMonthEdit->setEnabled(false); break; case (int)Schedule::Occurrence::EveryHalfMonth: case (int)Schedule::Occurrence::Monthly: case (int)Schedule::Occurrence::Yearly: // Supports Frequency Number d->ui->m_frequencyNoEdit->setEnabled(true); d->ui->m_lastDayInMonthEdit->setEnabled(true); break; default: // Multiplier is always 1 d->ui->m_frequencyNoEdit->setEnabled(false); d->ui->m_frequencyNoEdit->setValue(1); d->ui->m_lastDayInMonthEdit->setEnabled(true); break; } if (isEndSeries && (item != (int)Schedule::Occurrence::Once)) { // Changing the frequency changes the number // of remaining transactions if (auto dateEdit = dynamic_cast(d->m_editor->haveWidget("postdate"))) d->m_schedule.setNextDueDate(dateEdit->date()); d->m_schedule.setOccurrenceMultiplier(d->ui->m_frequencyNoEdit->value()); d->m_schedule.setOccurrencePeriod(static_cast(item)); d->m_schedule.setEndDate(d->ui->m_FinalPaymentEdit->date()); d->updateTransactionsRemaining(); } } void KEditScheduleDlg::slotOccurrenceMultiplierChanged(int multiplier) { Q_D(KEditScheduleDlg); // Make sure the required fields are set auto oldOccurrenceMultiplier = d->m_schedule.occurrenceMultiplier(); if (multiplier != oldOccurrenceMultiplier) { if (d->ui->m_endOptionsFrame->isEnabled()) { if (auto dateEdit = dynamic_cast(d->m_editor->haveWidget("postdate"))) d->m_schedule.setNextDueDate(dateEdit->date()); d->m_schedule.setOccurrenceMultiplier(multiplier); d->m_schedule.setOccurrencePeriod(static_cast(d->ui->m_frequencyEdit->currentItem())); d->m_schedule.setEndDate(d->ui->m_FinalPaymentEdit->date()); d->updateTransactionsRemaining(); } } } void KEditScheduleDlg::slotShowHelp() { KHelpClient::invokeHelp("details.schedules.intro"); } void KEditScheduleDlg::slotFilterPaymentType(int index) { Q_D(KEditScheduleDlg); //save selected item to reload if possible auto selectedId = d->ui->m_paymentMethodEdit->itemData(d->ui->m_paymentMethodEdit->currentIndex(), Qt::UserRole).toInt(); //clear and reload the widget with the correct items d->ui->m_paymentMethodEdit->clear(); // load option widgets eWidgets::eRegister::Action action = static_cast(index); if (action != eWidgets::eRegister::Action::Withdrawal) { d->ui->m_paymentMethodEdit->insertItem(i18n("Direct deposit"), (int)Schedule::PaymentType::DirectDeposit); d->ui->m_paymentMethodEdit->insertItem(i18n("Manual deposit"), (int)Schedule::PaymentType::ManualDeposit); } if (action != eWidgets::eRegister::Action::Deposit) { d->ui->m_paymentMethodEdit->insertItem(i18n("Direct debit"), (int)Schedule::PaymentType::DirectDebit); d->ui->m_paymentMethodEdit->insertItem(i18n("Write check"), (int)Schedule::PaymentType::WriteChecque); } d->ui->m_paymentMethodEdit->insertItem(i18n("Standing order"), (int)Schedule::PaymentType::StandingOrder); d->ui->m_paymentMethodEdit->insertItem(i18n("Bank transfer"), (int)Schedule::PaymentType::BankTransfer); d->ui->m_paymentMethodEdit->insertItem(i18nc("Other payment method", "Other"), (int)Schedule::PaymentType::Other); auto newIndex = d->ui->m_paymentMethodEdit->findData(QVariant(selectedId), Qt::UserRole, Qt::MatchExactly); if (newIndex > -1) { d->ui->m_paymentMethodEdit->setCurrentIndex(newIndex); } else { d->ui->m_paymentMethodEdit->setCurrentIndex(0); } } diff --git a/kmymoney/dialogs/kenterscheduledlg.cpp b/kmymoney/dialogs/kenterscheduledlg.cpp index 6631472d5..b80cf54ac 100644 --- a/kmymoney/dialogs/kenterscheduledlg.cpp +++ b/kmymoney/dialogs/kenterscheduledlg.cpp @@ -1,387 +1,387 @@ /*************************************************************************** kenterscheduledlg.cpp ------------------- begin : Sat Apr 7 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 "kenterscheduledlg.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "ui_kenterscheduledlg.h" #include "tabbar.h" #include "mymoneysplit.h" #include "mymoneytransaction.h" #include "mymoneyfile.h" #include "mymoneyaccount.h" #include "mymoneymoney.h" #include "mymoneyschedule.h" #include "register.h" #include "transactionform.h" #include "transaction.h" #include "selectedtransactions.h" #include "transactioneditor.h" #include "kmymoneyutils.h" #include "kmymoneylineedit.h" #include "kmymoneydateinput.h" #include "knewaccountdlg.h" #include "knewinvestmentwizard.h" #include "mymoneyexception.h" #include "icons/icons.h" #include "mymoneyenums.h" #include "dialogenums.h" #include "widgetenums.h" using namespace Icons; class KEnterScheduleDlgPrivate { Q_DISABLE_COPY(KEnterScheduleDlgPrivate) public: KEnterScheduleDlgPrivate() : ui(new Ui::KEnterScheduleDlg), m_item(nullptr), m_showWarningOnce(true), m_extendedReturnCode(eDialogs::ScheduleResultCode::Cancel) { } ~KEnterScheduleDlgPrivate() { delete ui; } Ui::KEnterScheduleDlg *ui; MyMoneySchedule m_schedule; KMyMoneyRegister::Transaction* m_item; QWidgetList m_tabOrderWidgets; bool m_showWarningOnce; eDialogs::ScheduleResultCode m_extendedReturnCode; }; KEnterScheduleDlg::KEnterScheduleDlg(QWidget *parent, const MyMoneySchedule& schedule) : QDialog(parent), d_ptr(new KEnterScheduleDlgPrivate) { Q_D(KEnterScheduleDlg); d->ui->setupUi(this); d->m_schedule = schedule; d->m_extendedReturnCode = eDialogs::ScheduleResultCode::Enter; d->ui->buttonOk->setIcon(Icons::get(Icon::KeyEnter)); d->ui->buttonSkip->setIcon(Icons::get(Icon::MediaSeekForward)); KGuiItem::assign(d->ui->buttonCancel, KStandardGuiItem::cancel()); KGuiItem::assign(d->ui->buttonHelp, KStandardGuiItem::help()); d->ui->buttonIgnore->setHidden(true); d->ui->buttonSkip->setHidden(true); // make sure, we have a tabbar with the form KMyMoneyTransactionForm::TabBar* tabbar = d->ui->m_form->getTabBar(d->ui->m_form->parentWidget()); // we never need to see the register d->ui->m_register->hide(); // ... setup the form ... d->ui->m_form->setupForm(d->m_schedule.account()); // ... and the register ... d->ui->m_register->clear(); // ... now add the transaction to register and form ... MyMoneyTransaction t = transaction(); d->m_item = KMyMoneyRegister::Register::transactionFactory(d->ui->m_register, t, d->m_schedule.transaction().splits().isEmpty() ? MyMoneySplit() : d->m_schedule.transaction().splits().front(), 0); d->ui->m_register->selectItem(d->m_item); // show the account row d->m_item->setShowRowInForm(0, true); d->ui->m_form->slotSetTransaction(d->m_item); // no need to see the tabbar tabbar->hide(); // setup name and type d->ui->m_scheduleName->setText(d->m_schedule.name()); d->ui->m_type->setText(KMyMoneyUtils::scheduleTypeToString(d->m_schedule.type())); connect(d->ui->buttonHelp, &QAbstractButton::clicked, this, &KEnterScheduleDlg::slotShowHelp); connect(d->ui->buttonIgnore, &QAbstractButton::clicked, this, &KEnterScheduleDlg::slotIgnore); connect(d->ui->buttonSkip, &QAbstractButton::clicked, this, &KEnterScheduleDlg::slotSkip); } KEnterScheduleDlg::~KEnterScheduleDlg() { Q_D(KEnterScheduleDlg); delete d; } eDialogs::ScheduleResultCode KEnterScheduleDlg::resultCode() const { Q_D(const KEnterScheduleDlg); if (result() == QDialog::Accepted) return d->m_extendedReturnCode; return eDialogs::ScheduleResultCode::Cancel; } void KEnterScheduleDlg::showExtendedKeys(bool visible) { Q_D(KEnterScheduleDlg); d->ui->buttonIgnore->setVisible(visible); d->ui->buttonSkip->setVisible(visible); } void KEnterScheduleDlg::slotIgnore() { Q_D(KEnterScheduleDlg); d->m_extendedReturnCode = eDialogs::ScheduleResultCode::Ignore; accept(); } void KEnterScheduleDlg::slotSkip() { Q_D(KEnterScheduleDlg); d->m_extendedReturnCode = eDialogs::ScheduleResultCode::Skip; accept(); } MyMoneyTransaction KEnterScheduleDlg::transaction() { Q_D(KEnterScheduleDlg); auto t = d->m_schedule.transaction(); try { if (d->m_schedule.type() == eMyMoney::Schedule::Type::LoanPayment) { KMyMoneyUtils::calculateAutoLoan(d->m_schedule, t, QMap()); } } catch (const MyMoneyException &e) { KMessageBox::detailedError(this, i18n("Unable to load schedule details"), e.what()); } t.clearId(); t.setEntryDate(QDate()); return t; } QDate KEnterScheduleDlg::date(const QDate& _date) const { Q_D(const KEnterScheduleDlg); auto date(_date); return d->m_schedule.adjustedDate(date, d->m_schedule.weekendOption()); } void KEnterScheduleDlg::resizeEvent(QResizeEvent* ev) { Q_UNUSED(ev) Q_D(KEnterScheduleDlg); d->ui->m_register->resize((int)eWidgets::eTransaction::Column::Detail); d->ui->m_form->resize((int)eWidgets::eTransactionForm::Column::Value1); QDialog::resizeEvent(ev); } void KEnterScheduleDlg::slotSetupSize() { resize(width(), minimumSizeHint().height()); } int KEnterScheduleDlg::exec() { Q_D(KEnterScheduleDlg); if (d->m_showWarningOnce) { d->m_showWarningOnce = false; KMessageBox::information(this, QString("") + i18n("

Please check that all the details in the following dialog are correct and press OK.

Editable data can be changed and can either be applied to just this occurrence or for all subsequent occurrences for this schedule. (You will be asked what you intend after pressing OK in the following dialog)

") + QString("
"), i18n("Enter scheduled transaction"), "EnterScheduleDlgInfo"); } // force the initial height to be as small as possible QTimer::singleShot(0, this, SLOT(slotSetupSize())); return QDialog::exec(); } TransactionEditor* KEnterScheduleDlg::startEdit() { Q_D(KEnterScheduleDlg); KMyMoneyRegister::SelectedTransactions list(d->ui->m_register); auto editor = d->m_item->createEditor(d->ui->m_form, list, QDate()); if (editor) { editor->setScheduleInfo(d->m_schedule.name()); editor->setPaymentMethod(d->m_schedule.paymentType()); } // check that we use the same transaction commodity in all selected transactions // if not, we need to update this in the editor's list. The user can also bail out // of this operation which means that we have to stop editing here. if (editor) { if (!editor->fixTransactionCommodity(d->m_schedule.account())) { // if the user wants to quit, we need to destroy the editor // and bail out delete editor; editor = 0; } } if (editor) { connect(editor, &TransactionEditor::transactionDataSufficient, d->ui->buttonOk, &QWidget::setEnabled); connect(editor, &TransactionEditor::escapePressed, d->ui->buttonCancel, &QAbstractButton::animateClick); connect(editor, &TransactionEditor::returnPressed, d->ui->buttonOk, &QAbstractButton::animateClick); connect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, editor, &TransactionEditor::slotReloadEditWidgets); // connect(editor, SIGNAL(finishEdit(KMyMoneyRegister::SelectedTransactions)), this, SLOT(slotLeaveEditMode(KMyMoneyRegister::SelectedTransactions))); connect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, editor, &TransactionEditor::slotReloadEditWidgets); // create the widgets, place them in the parent and load them with data // setup tab order d->m_tabOrderWidgets.clear(); eWidgets::eRegister::Action action = eWidgets::eRegister::Action::Withdrawal; switch (d->m_schedule.type()) { case eMyMoney::Schedule::Type::Transfer: action = eWidgets::eRegister::Action::Transfer; break; case eMyMoney::Schedule::Type::Deposit: action = eWidgets::eRegister::Action::Deposit; break; case eMyMoney::Schedule::Type::LoanPayment: switch (d->m_schedule.paymentType()) { case eMyMoney::Schedule::PaymentType::DirectDeposit: case eMyMoney::Schedule::PaymentType::ManualDeposit: action = eWidgets::eRegister::Action::Deposit; break; default: break; } break; default: break; } editor->setup(d->m_tabOrderWidgets, d->m_schedule.account(), action); MyMoneyTransaction t = d->m_schedule.transaction(); QString num = t.splits().first().number(); QWidget* w = editor->haveWidget("number"); if (d->m_schedule.paymentType() == eMyMoney::Schedule::PaymentType::WriteChecque) { auto file = MyMoneyFile::instance(); if (file->checkNoUsed(d->m_schedule.account().id(), num)) { // increment and try again num = KMyMoneyUtils::getAdjacentNumber(num); } num = KMyMoneyUtils::nextCheckNumber(d->m_schedule.account()); KMyMoneyUtils::updateLastNumberUsed(d->m_schedule.account(), num); d->m_schedule.account().setValue("lastNumberUsed", num); if (w) if (auto numberWidget = dynamic_cast(w)) numberWidget->loadText(num); } else { // if it's not a check, then we need to clear // a possibly assigned check number if (w) if (auto numberWidget = dynamic_cast(w)) numberWidget->loadText(QString()); } Q_ASSERT(!d->m_tabOrderWidgets.isEmpty()); // editor->setup() leaves the tabbar as the last widget in the stack, but we // need it as first here. So we move it around. w = editor->haveWidget("tabbar"); if (w) { int idx = d->m_tabOrderWidgets.indexOf(w); if (idx != -1) { d->m_tabOrderWidgets.removeAt(idx); d->m_tabOrderWidgets.push_front(w); } } // don't forget our three buttons d->m_tabOrderWidgets.append(d->ui->buttonOk); d->m_tabOrderWidgets.append(d->ui->buttonCancel); d->m_tabOrderWidgets.append(d->ui->buttonHelp); for (auto i = 0; i < d->m_tabOrderWidgets.size(); ++i) { - QWidget* w = d->m_tabOrderWidgets.at(i); + w = d->m_tabOrderWidgets.at(i); if (w) { w->installEventFilter(this); w->installEventFilter(editor); } } // Check if the editor has some preference on where to set the focus // If not, set the focus to the first widget in the tab order QWidget* focusWidget = editor->firstWidget(); if (!focusWidget) focusWidget = d->m_tabOrderWidgets.first(); focusWidget->setFocus(); // Make sure, we use the adjusted date if (auto dateEdit = dynamic_cast(editor->haveWidget("postdate"))) dateEdit->setDate(d->m_schedule.adjustedNextDueDate()); } return editor; } bool KEnterScheduleDlg::focusNextPrevChild(bool next) { Q_D(KEnterScheduleDlg); auto rc = false; auto w = qApp->focusWidget(); int currentWidgetIndex = d->m_tabOrderWidgets.indexOf(w); while (w && currentWidgetIndex == -1) { // qDebug("'%s' not in list, use parent", w->className()); w = w->parentWidget(); currentWidgetIndex = d->m_tabOrderWidgets.indexOf(w); } if (currentWidgetIndex != -1) { // if(w) qDebug("tab order is at '%s'", w->className()); currentWidgetIndex += next ? 1 : -1; if (currentWidgetIndex < 0) currentWidgetIndex = d->m_tabOrderWidgets.size() - 1; else if (currentWidgetIndex >= d->m_tabOrderWidgets.size()) currentWidgetIndex = 0; w = d->m_tabOrderWidgets[currentWidgetIndex]; // qDebug("currentWidgetIndex = %d, w = %p", currentWidgetIndex, w); if (((w->focusPolicy() & Qt::TabFocus) == Qt::TabFocus) && w->isVisible() && w->isEnabled()) { // qDebug("Selecting '%s' as focus", w->className()); w->setFocus(); rc = true; } } return rc; } void KEnterScheduleDlg::slotShowHelp() { KHelpClient::invokeHelp("details.schedules.entering"); } diff --git a/kmymoney/dialogs/kequitypriceupdatedlg.cpp b/kmymoney/dialogs/kequitypriceupdatedlg.cpp index c71e5a697..1a5a6abf7 100644 --- a/kmymoney/dialogs/kequitypriceupdatedlg.cpp +++ b/kmymoney/dialogs/kequitypriceupdatedlg.cpp @@ -1,818 +1,818 @@ /*************************************************************************** kequitypriceupdatedlg.cpp - description ------------------- begin : Mon Sep 1 2003 copyright : (C) 2000-2003 by Michael Edwardes email : mte@users.sourceforge.net Javier Campos Morales Felix Rodriguez John C Thomas Baumgart Kevin Tambascio Ace Jones (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 "kequitypriceupdatedlg.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "ui_kequitypriceupdatedlg.h" #include "mymoneyfile.h" #include "mymoneyaccount.h" #include "mymoneysecurity.h" #include "mymoneyprice.h" #include "webpricequote.h" #include "kequitypriceupdateconfdlg.h" #include "kmymoneyutils.h" #include "mymoneyexception.h" #include "dialogenums.h" #define WEBID_COL 0 #define NAME_COL 1 #define PRICE_COL 2 #define DATE_COL 3 #define KMMID_COL 4 #define SOURCE_COL 5 class KEquityPriceUpdateDlgPrivate { Q_DISABLE_COPY(KEquityPriceUpdateDlgPrivate) Q_DECLARE_PUBLIC(KEquityPriceUpdateDlg) public: explicit KEquityPriceUpdateDlgPrivate(KEquityPriceUpdateDlg *qq) : q_ptr(qq), ui(new Ui::KEquityPriceUpdateDlg), m_fUpdateAll(false), m_updatingPricePolicy(eDialogs::UpdatePrice::All) { } ~KEquityPriceUpdateDlgPrivate() { delete ui; } void init(const QString& securityId) { Q_Q(KEquityPriceUpdateDlg); ui->setupUi(q); m_fUpdateAll = false; QStringList headerList; headerList << i18n("ID") << i18nc("Equity name", "Name") << i18n("Price") << i18n("Date"); ui->lvEquityList->header()->setSortIndicator(0, Qt::AscendingOrder); ui->lvEquityList->setColumnWidth(NAME_COL, 125); // This is a "get it up and running" hack. Will replace this in the future. headerList << i18nc("Internal identifier", "Internal ID") << i18nc("Online quote source", "Source"); ui->lvEquityList->setColumnWidth(KMMID_COL, 0); ui->lvEquityList->setHeaderLabels(headerList); ui->lvEquityList->setSelectionMode(QAbstractItemView::MultiSelection); ui->lvEquityList->setAllColumnsShowFocus(true); ui->btnUpdateAll->setEnabled(false); auto file = MyMoneyFile::instance(); // // Add each price pair that we know about // // send in securityId == "XXX YYY" to get a single-shot update for XXX to YYY. // for consistency reasons, this accepts the same delimiters as WebPriceQuote::launch() QRegExp splitrx("([0-9a-z\\.]+)[^a-z0-9]+([0-9a-z\\.]+)", Qt::CaseInsensitive); MyMoneySecurityPair currencyIds; if (splitrx.indexIn(securityId) != -1) { currencyIds = MyMoneySecurityPair(splitrx.cap(1), splitrx.cap(2)); } MyMoneyPriceList prices = file->priceList(); for (MyMoneyPriceList::ConstIterator it_price = prices.constBegin(); it_price != prices.constEnd(); ++it_price) { const MyMoneySecurityPair& pair = it_price.key(); if (file->security(pair.first).isCurrency() && (securityId.isEmpty() || (pair == currencyIds))) { const MyMoneyPriceEntries& entries = (*it_price); if (entries.count() > 0 && entries.begin().key() <= QDate::currentDate()) { addPricePair(pair, false); ui->btnUpdateAll->setEnabled(true); } } } // // Add each investment // QList securities = file->securityList(); for (QList::const_iterator it = securities.constBegin(); it != securities.constEnd(); ++it) { if (!(*it).isCurrency() && (securityId.isEmpty() || ((*it).id() == securityId)) && !(*it).value("kmm-online-source").isEmpty() ) { addInvestment(*it); ui->btnUpdateAll->setEnabled(true); } } // if list is empty, add the request price pair if (ui->lvEquityList->invisibleRootItem()->childCount() == 0) { addPricePair(currencyIds, true); } q->connect(ui->btnUpdateSelected, &QAbstractButton::clicked, q, &KEquityPriceUpdateDlg::slotUpdateSelectedClicked); q->connect(ui->btnUpdateAll, &QAbstractButton::clicked, q, &KEquityPriceUpdateDlg::slotUpdateAllClicked); q->connect(ui->m_fromDate, &KMyMoneyDateInput::dateChanged, q, &KEquityPriceUpdateDlg::slotDateChanged); q->connect(ui->m_toDate, &KMyMoneyDateInput::dateChanged, q, &KEquityPriceUpdateDlg::slotDateChanged); q->connect(&m_webQuote, &WebPriceQuote::csvquote, q, &KEquityPriceUpdateDlg::slotReceivedCSVQuote); q->connect(&m_webQuote, &WebPriceQuote::quote, q, &KEquityPriceUpdateDlg::slotReceivedQuote); q->connect(&m_webQuote, &WebPriceQuote::failed, q, &KEquityPriceUpdateDlg::slotQuoteFailed); q->connect(&m_webQuote, &WebPriceQuote::status, q, &KEquityPriceUpdateDlg::logStatusMessage); q->connect(&m_webQuote, &WebPriceQuote::error, q, &KEquityPriceUpdateDlg::logErrorMessage); q->connect(ui->lvEquityList, &QTreeWidget::itemSelectionChanged, q, &KEquityPriceUpdateDlg::slotUpdateSelection); q->connect(ui->btnConfigure, &QAbstractButton::clicked, q, &KEquityPriceUpdateDlg::slotConfigureClicked); if (!securityId.isEmpty()) { ui->btnUpdateSelected->hide(); ui->btnUpdateAll->hide(); // delete layout1; QTimer::singleShot(100, q, SLOT(slotUpdateAllClicked())); } // Hide OK button until we have received the first update ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); q->slotUpdateSelection(); // previous versions of this dialog allowed to store a "Don't ask again" switch. // Since we don't support it anymore, we just get rid of it KSharedConfigPtr config = KSharedConfig::openConfig(); KConfigGroup grp = config->group("Notification Messages"); grp.deleteEntry("KEquityPriceUpdateDlg::slotQuoteFailed::Price Update Failed"); grp.sync(); grp = config->group("Equity Price Update"); int policyValue = grp.readEntry("PriceUpdatingPolicy", (int)eDialogs::UpdatePrice::Missing); if (policyValue > (int)eDialogs::UpdatePrice::Ask || policyValue < (int)eDialogs::UpdatePrice::All) m_updatingPricePolicy = eDialogs::UpdatePrice::Missing; else m_updatingPricePolicy = static_cast(policyValue); } void addPricePair(const MyMoneySecurityPair& pair, bool dontCheckExistance) { auto file = MyMoneyFile::instance(); const auto symbol = QString::fromLatin1("%1 > %2").arg(pair.first, pair.second); const auto id = QString::fromLatin1("%1 %2").arg(pair.first, pair.second); // Check that the pair does not already exist if (ui->lvEquityList->findItems(id, Qt::MatchExactly, KMMID_COL).empty()) { const MyMoneyPrice &pr = file->price(pair.first, pair.second); if (pr.source() != QLatin1String("KMyMoney")) { bool keep = true; if ((pair.first == file->baseCurrency().id()) || (pair.second == file->baseCurrency().id())) { const QString& foreignCurrency = file->foreignCurrency(pair.first, pair.second); // check that the foreign currency is still in use QList::const_iterator it_a; QList list; file->accountList(list); for (it_a = list.constBegin(); !dontCheckExistance && it_a != list.constEnd(); ++it_a) { // if it's an account denominated in the foreign currency // keep it if (((*it_a).currencyId() == foreignCurrency) && !(*it_a).isClosed()) break; // if it's an investment traded in the foreign currency // keep it if ((*it_a).isInvest() && !(*it_a).isClosed()) { MyMoneySecurity sec = file->security((*it_a).currencyId()); if (sec.tradingCurrency() == foreignCurrency) break; } } // if it is in use, it_a is not equal to list.end() if (it_a == list.constEnd() && !dontCheckExistance) keep = false; } if (keep) { auto item = new QTreeWidgetItem(); item->setText(WEBID_COL, symbol); item->setText(NAME_COL, i18n("%1 units in %2", pair.first, pair.second)); if (pr.isValid()) { MyMoneySecurity fromCurrency = file->currency(pair.second); MyMoneySecurity toCurrency = file->currency(pair.first); item->setText(PRICE_COL, pr.rate(pair.second).formatMoney(fromCurrency.tradingSymbol(), toCurrency.pricePrecision())); item->setText(DATE_COL, pr.date().toString(Qt::ISODate)); } item->setText(KMMID_COL, id); item->setText(SOURCE_COL, "KMyMoney Currency"); // This string value should not be localized ui->lvEquityList->invisibleRootItem()->addChild(item); } } } } void addInvestment(const MyMoneySecurity& inv) { const auto id = inv.id(); // Check that the pair does not already exist if (ui->lvEquityList->findItems(id, Qt::MatchExactly, KMMID_COL).empty()) { auto file = MyMoneyFile::instance(); // check that the security is still in use QList::const_iterator it_a; QList list; file->accountList(list); for (it_a = list.constBegin(); it_a != list.constEnd(); ++it_a) { if ((*it_a).isInvest() && ((*it_a).currencyId() == inv.id()) && !(*it_a).isClosed()) break; } // if it is in use, it_a is not equal to list.end() if (it_a != list.constEnd()) { QString webID; WebPriceQuoteSource onlineSource(inv.value("kmm-online-source")); if (onlineSource.m_webIDBy == WebPriceQuoteSource::identifyBy::IdentificationNumber) webID = inv.value("kmm-security-id"); // insert ISIN number... else if (onlineSource.m_webIDBy == WebPriceQuoteSource::identifyBy::Name) webID = inv.name(); // ...or name... else webID = inv.tradingSymbol(); // ...or symbol QTreeWidgetItem* item = new QTreeWidgetItem(); item->setForeground(WEBID_COL, KColorScheme(QPalette::Normal).foreground(KColorScheme::NormalText)); if (webID.isEmpty()) { webID = i18n("[No identifier]"); item->setForeground(WEBID_COL, KColorScheme(QPalette::Normal).foreground(KColorScheme::NegativeText)); } item->setText(WEBID_COL, webID); item->setText(NAME_COL, inv.name()); MyMoneySecurity currency = file->currency(inv.tradingCurrency()); const MyMoneyPrice &pr = file->price(id.toUtf8(), inv.tradingCurrency()); if (pr.isValid()) { item->setText(PRICE_COL, pr.rate(currency.id()).formatMoney(currency.tradingSymbol(), inv.pricePrecision())); item->setText(DATE_COL, pr.date().toString(Qt::ISODate)); } item->setText(KMMID_COL, id); if (inv.value("kmm-online-quote-system") == "Finance::Quote") item->setText(SOURCE_COL, QString("Finance::Quote %1").arg(inv.value("kmm-online-source"))); else item->setText(SOURCE_COL, inv.value("kmm-online-source")); ui->lvEquityList->invisibleRootItem()->addChild(item); // If this investment is denominated in a foreign currency, ensure that // the appropriate price pair is also on the list if (currency.id() != file->baseCurrency().id()) { addPricePair(MyMoneySecurityPair(currency.id(), file->baseCurrency().id()), false); } } } } KEquityPriceUpdateDlg *q_ptr; Ui::KEquityPriceUpdateDlg *ui; bool m_fUpdateAll; eDialogs::UpdatePrice m_updatingPricePolicy; WebPriceQuote m_webQuote; }; KEquityPriceUpdateDlg::KEquityPriceUpdateDlg(QWidget *parent, const QString& securityId) : QDialog(parent), d_ptr(new KEquityPriceUpdateDlgPrivate(this)) { Q_D(KEquityPriceUpdateDlg); d->init(securityId); } KEquityPriceUpdateDlg::KEquityPriceUpdateDlg(QWidget *parent) : KEquityPriceUpdateDlg(parent, QString()) { } KEquityPriceUpdateDlg::~KEquityPriceUpdateDlg() { Q_D(KEquityPriceUpdateDlg); auto config = KSharedConfig::openConfig(); auto grp = config->group("Equity Price Update"); grp.writeEntry("PriceUpdatingPolicy", static_cast(d->m_updatingPricePolicy)); grp.sync(); delete d; } void KEquityPriceUpdateDlg::logErrorMessage(const QString& message) { logStatusMessage(QString("") + message + QString("")); } void KEquityPriceUpdateDlg::logStatusMessage(const QString& message) { Q_D(KEquityPriceUpdateDlg); d->ui->lbStatus->append(message); } MyMoneyPrice KEquityPriceUpdateDlg::price(const QString& id) const { Q_D(const KEquityPriceUpdateDlg); MyMoneyPrice price; QTreeWidgetItem* item = nullptr; QList foundItems = d->ui->lvEquityList->findItems(id, Qt::MatchExactly, KMMID_COL); if (! foundItems.empty()) item = foundItems.at(0); if (item) { MyMoneyMoney rate(item->text(PRICE_COL)); if (!rate.isZero()) { - QString id = item->text(KMMID_COL).toUtf8(); + QString kmm_id = item->text(KMMID_COL).toUtf8(); // if the ID has a space, then this is TWO ID's, so it's a currency quote - if (id.contains(" ")) { - QStringList ids = id.split(' ', QString::SkipEmptyParts); + if (kmm_id.contains(" ")) { + QStringList ids = kmm_id.split(' ', QString::SkipEmptyParts); QString fromid = ids[0].toUtf8(); QString toid = ids[1].toUtf8(); price = MyMoneyPrice(fromid, toid, QDate().fromString(item->text(DATE_COL), Qt::ISODate), rate, item->text(SOURCE_COL)); } else // otherwise, it's a security quote { - MyMoneySecurity security = MyMoneyFile::instance()->security(id); - price = MyMoneyPrice(id, security.tradingCurrency(), QDate().fromString(item->text(DATE_COL), Qt::ISODate), rate, item->text(SOURCE_COL)); + MyMoneySecurity security = MyMoneyFile::instance()->security(kmm_id); + price = MyMoneyPrice(kmm_id, security.tradingCurrency(), QDate().fromString(item->text(DATE_COL), Qt::ISODate), rate, item->text(SOURCE_COL)); } } } return price; } void KEquityPriceUpdateDlg::storePrices() { Q_D(KEquityPriceUpdateDlg); // update the new prices into the equities auto file = MyMoneyFile::instance(); QString name; MyMoneyFileTransaction ft; try { for (auto i = 0; i < d->ui->lvEquityList->invisibleRootItem()->childCount(); ++i) { QTreeWidgetItem* item = d->ui->lvEquityList->invisibleRootItem()->child(i); // turn on signals before we modify the last entry in the list file->blockSignals(i < d->ui->lvEquityList->invisibleRootItem()->childCount() - 1); MyMoneyMoney rate(item->text(PRICE_COL)); if (!rate.isZero()) { QString id = item->text(KMMID_COL); QString fromid; QString toid; // if the ID has a space, then this is TWO ID's, so it's a currency quote if (id.contains(QLatin1Char(' '))) { QStringList ids = id.split(QLatin1Char(' '), QString::SkipEmptyParts); fromid = ids.at(0); toid = ids.at(1); name = QString::fromLatin1("%1 --> %2").arg(fromid, toid); } else { // otherwise, it's a security quote MyMoneySecurity security = file->security(id); name = security.name(); fromid = id; toid = security.tradingCurrency(); } // TODO (Ace) Better handling of the case where there is already a price // for this date. Currently, it just overrides the old value. Really it // should check to see if the price is the same and prompt the user. file->addPrice(MyMoneyPrice(fromid, toid, QDate::fromString(item->text(DATE_COL), Qt::ISODate), rate, item->text(SOURCE_COL))); } } ft.commit(); } catch (const MyMoneyException &) { qDebug("Unable to add price information for %s", qPrintable(name)); } } void KEquityPriceUpdateDlg::slotConfigureClicked() { Q_D(KEquityPriceUpdateDlg); QPointer dlg = new EquityPriceUpdateConfDlg(d->m_updatingPricePolicy); if (dlg->exec() == QDialog::Accepted) d->m_updatingPricePolicy = dlg->policy(); delete dlg; } void KEquityPriceUpdateDlg::slotUpdateSelection() { Q_D(KEquityPriceUpdateDlg); // Only enable the update button if there is a selection d->ui->btnUpdateSelected->setEnabled(false); if (! d->ui->lvEquityList->selectedItems().empty()) d->ui->btnUpdateSelected->setEnabled(true); } void KEquityPriceUpdateDlg::slotUpdateSelectedClicked() { Q_D(KEquityPriceUpdateDlg); // disable sorting while the update is running to maintain the current order of items on which // the update process depends and which could be changed with sorting enabled due to the updated values d->ui->lvEquityList->setSortingEnabled(false); auto item = d->ui->lvEquityList->invisibleRootItem()->child(0); auto skipCnt = 1; while (item && !item->isSelected()) { item = d->ui->lvEquityList->invisibleRootItem()->child(skipCnt); ++skipCnt; } d->m_webQuote.setDate(d->ui->m_fromDate->date(), d->ui->m_toDate->date()); if (item) { d->ui->prgOnlineProgress->setMaximum(1 + d->ui->lvEquityList->invisibleRootItem()->childCount()); d->ui->prgOnlineProgress->setValue(skipCnt); d->m_webQuote.launch(item->text(WEBID_COL), item->text(KMMID_COL), item->text(SOURCE_COL)); } else { logErrorMessage("No security selected."); } } void KEquityPriceUpdateDlg::slotUpdateAllClicked() { Q_D(KEquityPriceUpdateDlg); // disable sorting while the update is running to maintain the current order of items on which // the update process depends and which could be changed with sorting enabled due to the updated values d->ui->lvEquityList->setSortingEnabled(false); QTreeWidgetItem* item = d->ui->lvEquityList->invisibleRootItem()->child(0); if (item) { d->ui->prgOnlineProgress->setMaximum(1 + d->ui->lvEquityList->invisibleRootItem()->childCount()); d->ui->prgOnlineProgress->setValue(1); d->m_fUpdateAll = true; d->m_webQuote.launch(item->text(WEBID_COL), item->text(KMMID_COL), item->text(SOURCE_COL)); } else { logErrorMessage("Security list is empty."); } } void KEquityPriceUpdateDlg::slotDateChanged() { Q_D(KEquityPriceUpdateDlg); d->ui->m_fromDate->blockSignals(true); d->ui->m_toDate->blockSignals(true); if (d->ui->m_toDate->date() > QDate::currentDate()) d->ui->m_toDate->setDate(QDate::currentDate()); if (d->ui->m_fromDate->date() > d->ui->m_toDate->date()) d->ui->m_fromDate->setDate(d->ui->m_toDate->date()); d->ui->m_fromDate->blockSignals(false); d->ui->m_toDate->blockSignals(false); } void KEquityPriceUpdateDlg::slotQuoteFailed(const QString& _kmmID, const QString& _webID) { Q_D(KEquityPriceUpdateDlg); auto foundItems = d->ui->lvEquityList->findItems(_kmmID, Qt::MatchExactly, KMMID_COL); QTreeWidgetItem* item = nullptr; if (! foundItems.empty()) item = foundItems.at(0); // Give the user some options int result; if (_kmmID.contains(" ")) { if (item) result = KMessageBox::warningContinueCancel(this, i18n("Failed to retrieve an exchange rate for %1 from %2. It will be skipped this time.", _webID, item->text(SOURCE_COL)), i18n("Price Update Failed")); else return; } else if (!item) { return; } else { result = KMessageBox::questionYesNoCancel(this, QString::fromLatin1("%1").arg(i18n("Failed to retrieve a quote for %1 from %2. Press No to remove the online price source from this security permanently, Yes to continue updating this security during future price updates or Cancel to stop the current update operation.", _webID, item->text(SOURCE_COL))), i18n("Price Update Failed"), KStandardGuiItem::yes(), KStandardGuiItem::no()); } if (result == KMessageBox::No) { // Disable price updates for this security MyMoneyFileTransaction ft; try { // Get this security (by ID) MyMoneySecurity security = MyMoneyFile::instance()->security(_kmmID.toUtf8()); // Set the quote source to blank security.setValue("kmm-online-source", QString()); security.setValue("kmm-online-quote-system", QString()); // Re-commit the security MyMoneyFile::instance()->modifySecurity(security); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::error(this, QString("") + i18n("Cannot update security %1: %2", _webID, e.what()) + QString(""), i18n("Price Update Failed")); } } // As long as the user doesn't want to cancel, move on! if (result != KMessageBox::Cancel) { QTreeWidgetItem* next = nullptr; d->ui->prgOnlineProgress->setValue(d->ui->prgOnlineProgress->value() + 1); item->setSelected(false); // launch the NEXT one ... in case of m_fUpdateAll == false, we // need to parse the list to find the next selected one next = d->ui->lvEquityList->invisibleRootItem()->child(d->ui->lvEquityList->invisibleRootItem()->indexOfChild(item) + 1); if (!d->m_fUpdateAll) { while (next && !next->isSelected()) { d->ui->prgOnlineProgress->setValue(d->ui->prgOnlineProgress->value() + 1); next = d->ui->lvEquityList->invisibleRootItem()->child(d->ui->lvEquityList->invisibleRootItem()->indexOfChild(next) + 1); } } if (next) { d->m_webQuote.launch(next->text(WEBID_COL), next->text(KMMID_COL), next->text(SOURCE_COL)); } else { finishUpdate(); } } else { finishUpdate(); } } void KEquityPriceUpdateDlg::slotReceivedCSVQuote(const QString& _kmmID, const QString& _webID, MyMoneyStatement& st) { Q_D(KEquityPriceUpdateDlg); auto foundItems = d->ui->lvEquityList->findItems(_kmmID, Qt::MatchExactly, KMMID_COL); QTreeWidgetItem* item = nullptr; if (! foundItems.empty()) item = foundItems.at(0); QTreeWidgetItem* next = nullptr; if (item) { auto file = MyMoneyFile::instance(); MyMoneySecurity fromCurrency, toCurrency; if (!_kmmID.contains(QLatin1Char(' '))) { try { toCurrency = MyMoneyFile::instance()->security(_kmmID); fromCurrency = MyMoneyFile::instance()->security(toCurrency.tradingCurrency()); } catch (const MyMoneyException &) { fromCurrency = toCurrency = MyMoneySecurity(); } } else { QRegExp splitrx("([0-9a-z\\.]+)[^a-z0-9]+([0-9a-z\\.]+)", Qt::CaseInsensitive); if (splitrx.indexIn(_kmmID) != -1) { try { fromCurrency = MyMoneyFile::instance()->security(splitrx.cap(2).toUtf8()); toCurrency = MyMoneyFile::instance()->security(splitrx.cap(1).toUtf8()); } catch (const MyMoneyException &) { fromCurrency = toCurrency = MyMoneySecurity(); } } } if (d->m_updatingPricePolicy != eDialogs::UpdatePrice::All) { QStringList qSources = WebPriceQuote::quoteSources(); for (auto it = st.m_listPrices.begin(); it != st.m_listPrices.end();) { MyMoneyPrice storedPrice = file->price(toCurrency.id(), fromCurrency.id(), (*it).m_date, true); bool priceValid = storedPrice.isValid(); if (!priceValid) ++it; else { switch(d->m_updatingPricePolicy) { case eDialogs::UpdatePrice::Missing: it = st.m_listPrices.erase(it); break; case eDialogs::UpdatePrice::Downloaded: if (!qSources.contains(storedPrice.source())) it = st.m_listPrices.erase(it); else ++it; break; case eDialogs::UpdatePrice::SameSource: if (storedPrice.source().compare((*it).m_sourceName) != 0) it = st.m_listPrices.erase(it); else ++it; break; case eDialogs::UpdatePrice::Ask: { auto result = KMessageBox::questionYesNoCancel(this, i18n("For %1 on %2 price %3 already exists.
" "Do you want to replace it with %4?", storedPrice.from(), storedPrice.date().toString(Qt::ISODate), QString().setNum(storedPrice.rate(storedPrice.to()).toDouble(), 'g', 10), QString().setNum((*it).m_amount.toDouble(), 'g', 10)), i18n("Price Already Exists")); switch(result) { case KMessageBox::ButtonCode::Yes: ++it; break; case KMessageBox::ButtonCode::No: it = st.m_listPrices.erase(it); break; default: case KMessageBox::ButtonCode::Cancel: finishUpdate(); return; break; } break; } default: ++it; break; } } } } if (!st.m_listPrices.isEmpty()) { MyMoneyFileTransaction ft; KMyMoneyUtils::processPriceList(st); ft.commit(); // latest price could be in the last or in the first row MyMoneyStatement::Price priceClass; if (st.m_listPrices.first().m_date > st.m_listPrices.last().m_date) priceClass = st.m_listPrices.first(); else priceClass = st.m_listPrices.last(); // update latest price in dialog if applicable auto latestDate = QDate::fromString(item->text(DATE_COL),Qt::ISODate); if (latestDate <= priceClass.m_date && priceClass.m_amount.isPositive()) { item->setText(PRICE_COL, priceClass.m_amount.formatMoney(fromCurrency.tradingSymbol(), toCurrency.pricePrecision())); item->setText(DATE_COL, priceClass.m_date.toString(Qt::ISODate)); item->setText(SOURCE_COL, priceClass.m_sourceName); } logStatusMessage(i18n("Price for %1 updated (id %2)", _webID, _kmmID)); // make sure to make OK button available } d->ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true); d->ui->prgOnlineProgress->setValue(d->ui->prgOnlineProgress->value() + 1); item->setSelected(false); // launch the NEXT one ... in case of m_fUpdateAll == false, we // need to parse the list to find the next selected one next = d->ui->lvEquityList->invisibleRootItem()->child(d->ui->lvEquityList->invisibleRootItem()->indexOfChild(item) + 1); if (!d->m_fUpdateAll) { while (next && !next->isSelected()) { d->ui->prgOnlineProgress->setValue(d->ui->prgOnlineProgress->value() + 1); next = d->ui->lvEquityList->invisibleRootItem()->child(d->ui->lvEquityList->invisibleRootItem()->indexOfChild(next) + 1); } } } else { logErrorMessage(i18n("Received a price for %1 (id %2), but this symbol is not on the list. Aborting entire update.", _webID, _kmmID)); } if (next) { d->m_webQuote.launch(next->text(WEBID_COL), next->text(KMMID_COL), next->text(SOURCE_COL)); } else { finishUpdate(); } } void KEquityPriceUpdateDlg::slotReceivedQuote(const QString& _kmmID, const QString& _webID, const QDate& _date, const double& _price) { Q_D(KEquityPriceUpdateDlg); auto foundItems = d->ui->lvEquityList->findItems(_kmmID, Qt::MatchExactly, KMMID_COL); QTreeWidgetItem* item = nullptr; if (! foundItems.empty()) item = foundItems.at(0); QTreeWidgetItem* next = 0; if (item) { if (_price > 0.0f && _date.isValid()) { QDate date = _date; if (date > QDate::currentDate()) date = QDate::currentDate(); MyMoneyMoney price = MyMoneyMoney::ONE; QString id = _kmmID.toUtf8(); MyMoneySecurity fromCurrency, toCurrency; if (_kmmID.contains(" ") == 0) { MyMoneySecurity security = MyMoneyFile::instance()->security(id); QString factor = security.value("kmm-online-factor"); if (!factor.isEmpty()) { price = price * MyMoneyMoney(factor); } try { toCurrency = MyMoneyFile::instance()->security(id); fromCurrency = MyMoneyFile::instance()->security(toCurrency.tradingCurrency()); } catch (const MyMoneyException &) { fromCurrency = toCurrency = MyMoneySecurity(); } } else { QRegExp splitrx("([0-9a-z\\.]+)[^a-z0-9]+([0-9a-z\\.]+)", Qt::CaseInsensitive); if (splitrx.indexIn(_kmmID) != -1) { try { fromCurrency = MyMoneyFile::instance()->security(splitrx.cap(2).toUtf8()); toCurrency = MyMoneyFile::instance()->security(splitrx.cap(1).toUtf8()); } catch (const MyMoneyException &) { fromCurrency = toCurrency = MyMoneySecurity(); } } } price *= MyMoneyMoney(_price, MyMoneyMoney::precToDenom(toCurrency.pricePrecision())); item->setText(PRICE_COL, price.formatMoney(fromCurrency.tradingSymbol(), toCurrency.pricePrecision())); item->setText(DATE_COL, date.toString(Qt::ISODate)); logStatusMessage(i18n("Price for %1 updated (id %2)", _webID, _kmmID)); // make sure to make OK button available d->ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true); } else { logErrorMessage(i18n("Received an invalid price for %1, unable to update.", _webID)); } d->ui->prgOnlineProgress->setValue(d->ui->prgOnlineProgress->value() + 1); item->setSelected(false); // launch the NEXT one ... in case of m_fUpdateAll == false, we // need to parse the list to find the next selected one next = d->ui->lvEquityList->invisibleRootItem()->child(d->ui->lvEquityList->invisibleRootItem()->indexOfChild(item) + 1); if (!d->m_fUpdateAll) { while (next && !next->isSelected()) { d->ui->prgOnlineProgress->setValue(d->ui->prgOnlineProgress->value() + 1); next = d->ui->lvEquityList->invisibleRootItem()->child(d->ui->lvEquityList->invisibleRootItem()->indexOfChild(next) + 1); } } } else { logErrorMessage(i18n("Received a price for %1 (id %2), but this symbol is not on the list. Aborting entire update.", _webID, _kmmID)); } if (next) { d->m_webQuote.launch(next->text(WEBID_COL), next->text(KMMID_COL), next->text(SOURCE_COL)); } else { finishUpdate(); } } void KEquityPriceUpdateDlg::finishUpdate() { Q_D(KEquityPriceUpdateDlg); // we've run past the end, reset to the default value. d->m_fUpdateAll = false; // force progress bar to show 100% d->ui->prgOnlineProgress->setValue(d->ui->prgOnlineProgress->maximum()); // re-enable the sorting that was disabled during the update process d->ui->lvEquityList->setSortingEnabled(true); } // 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 WEBID_COL #undef NAME_COL #undef PRICE_COL #undef DATE_COL #undef KMMID_COL #undef SOURCE_COL diff --git a/kmymoney/dialogs/knewaccountdlg.cpp b/kmymoney/dialogs/knewaccountdlg.cpp index 8fac2639f..ac8ff1291 100644 --- a/kmymoney/dialogs/knewaccountdlg.cpp +++ b/kmymoney/dialogs/knewaccountdlg.cpp @@ -1,964 +1,962 @@ /*************************************************************************** knewaccountdlg.cpp ------------------- copyright : (C) 2000 by Michael Edwardes 2004 by 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 "knewaccountdlg.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Headers #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "ui_knewaccountdlg.h" #include "kmymoneyedit.h" #include "kmymoneydateinput.h" #include #include "mymoneyfile.h" #include "mymoneyinstitution.h" #include "mymoneyaccount.h" #include "kmymoneysettings.h" #include "kmymoneycurrencyselector.h" #include "knewbankdlg.h" #include "models.h" #include "accountsmodel.h" #include "hierarchyfilterproxymodel.h" #include "mymoneyenums.h" #include "modelenums.h" using namespace eMyMoney; class KNewAccountDlgPrivate { Q_DISABLE_COPY(KNewAccountDlgPrivate) Q_DECLARE_PUBLIC(KNewAccountDlg) public: explicit KNewAccountDlgPrivate(KNewAccountDlg *qq) : q_ptr(qq), ui(new Ui::KNewAccountDlg), m_filterProxyModel(nullptr), m_categoryEditor(false), m_isEditing(false) { } ~KNewAccountDlgPrivate() { delete ui; } void init() { Q_Q(KNewAccountDlg); ui->setupUi(q); auto file = MyMoneyFile::instance(); // initialize the m_parentAccount member QVector filterAccountGroup {m_account.accountGroup()}; switch (m_account.accountGroup()) { case Account::Type::Asset: m_parentAccount = file->asset(); break; case Account::Type::Liability: m_parentAccount = file->liability(); break; case Account::Type::Income: m_parentAccount = file->income(); break; case Account::Type::Expense: m_parentAccount = file->expense(); break; case Account::Type::Equity: m_parentAccount = file->equity(); break; default: qDebug("Seems we have an account that hasn't been mapped to the top five"); if (m_categoryEditor) { m_parentAccount = file->income(); filterAccountGroup[0] = Account::Type::Income; } else { m_parentAccount = file->asset(); filterAccountGroup[0] = Account::Type::Asset; } } ui->m_amountGroup->setId(ui->m_grossAmount, 0); ui->m_amountGroup->setId(ui->m_netAmount, 1); // the proxy filter model m_filterProxyModel = new HierarchyFilterProxyModel(q); m_filterProxyModel->setHideClosedAccounts(true); m_filterProxyModel->setHideEquityAccounts(!KMyMoneySettings::expertMode()); m_filterProxyModel->addAccountGroup(filterAccountGroup); m_filterProxyModel->setCurrentAccountId(m_account.id()); auto const model = Models::instance()->accountsModel(); m_filterProxyModel->setSourceModel(model); m_filterProxyModel->setSourceColumns(model->getColumns()); m_filterProxyModel->setDynamicSortFilter(true); ui->m_parentAccounts->setModel(m_filterProxyModel); ui->m_parentAccounts->sortByColumn((int)eAccountsModel::Column::Account, Qt::AscendingOrder); ui->m_subAccountLabel->setText(i18n("Is a sub account")); ui->accountNameEdit->setText(m_account.name()); ui->descriptionEdit->setText(m_account.description()); ui->typeCombo->setEnabled(true); // load the price mode combo ui->m_priceMode->insertItem(i18nc("default price mode", "(default)"), 0); ui->m_priceMode->insertItem(i18n("Price per share"), 1); ui->m_priceMode->insertItem(i18n("Total for all shares"), 2); int priceMode = 0; if (m_account.accountType() == Account::Type::Investment) { ui->m_priceMode->setEnabled(true); if (!m_account.value("priceMode").isEmpty()) priceMode = m_account.value("priceMode").toInt(); } ui->m_priceMode->setCurrentItem(priceMode); bool haveMinBalance = false; bool haveMaxCredit = false; if (!m_account.openingDate().isValid()) { m_account.setOpeningDate(KMyMoneySettings::firstFiscalDate()); } ui->m_openingDateEdit->setDate(m_account.openingDate()); handleOpeningBalanceCheckbox(m_account.currencyId()); if (m_categoryEditor) { // get rid of the tabs that are not used for categories int tab = ui->m_tab->indexOf(ui->m_institutionTab); if (tab != -1) ui->m_tab->removeTab(tab); tab = ui->m_tab->indexOf(ui->m_limitsTab); if (tab != -1) ui->m_tab->removeTab(tab); //m_qlistviewParentAccounts->setEnabled(true); ui->accountNoEdit->setEnabled(false); ui->m_institutionBox->hide(); ui->m_qcheckboxNoVat->hide(); ui->typeCombo->addItem(MyMoneyAccount::accountTypeToString(Account::Type::Income), (int)Account::Type::Income); ui->typeCombo->addItem(MyMoneyAccount::accountTypeToString(Account::Type::Expense), (int)Account::Type::Expense); // Hardcoded but acceptable - if above we set the default to income do the same here switch (m_account.accountType()) { case Account::Type::Expense: ui->typeCombo->setCurrentItem(MyMoneyAccount::accountTypeToString(Account::Type::Expense), false); break; case Account::Type::Income: default: ui->typeCombo->setCurrentItem(MyMoneyAccount::accountTypeToString(Account::Type::Income), false); break; } ui->m_currency->setEnabled(true); if (m_isEditing) { ui->typeCombo->setEnabled(false); ui->m_currency->setDisabled(MyMoneyFile::instance()->isReferenced(m_account)); } ui->m_qcheckboxPreferred->hide(); ui->m_qcheckboxTax->setChecked(m_account.value("Tax").toLower() == "yes"); ui->m_costCenterRequiredCheckBox->setChecked(m_account.isCostCenterRequired()); loadVatAccounts(); } else { // get rid of the tabs that are not used for accounts int taxtab = ui->m_tab->indexOf(ui->m_taxTab); if (taxtab != -1) { ui->m_vatCategory->setText(i18n("VAT account")); ui->m_qcheckboxTax->setChecked(m_account.value("Tax") == "Yes"); loadVatAccounts(); } else { ui->m_tab->removeTab(taxtab); } ui->m_costCenterRequiredCheckBox->hide(); switch (m_account.accountType()) { case Account::Type::Savings: case Account::Type::Cash: haveMinBalance = true; break; case Account::Type::Checkings: haveMinBalance = true; haveMaxCredit = true; break; case Account::Type::CreditCard: haveMaxCredit = true; break; default: // no limit available, so we might get rid of the tab int tab = ui->m_tab->indexOf(ui->m_limitsTab); if (tab != -1) ui->m_tab->removeTab(tab); // don't try to hide the widgets we just wiped // in the next step haveMaxCredit = haveMinBalance = true; break; } if (!haveMaxCredit) { ui->m_maxCreditLabel->setEnabled(false); ui->m_maxCreditLabel->hide(); ui->m_maxCreditEarlyEdit->hide(); ui->m_maxCreditAbsoluteEdit->hide(); } if (!haveMinBalance) { ui->m_minBalanceLabel->setEnabled(false); ui->m_minBalanceLabel->hide(); ui->m_minBalanceEarlyEdit->hide(); ui->m_minBalanceAbsoluteEdit->hide(); } QString typeString = MyMoneyAccount::accountTypeToString(m_account.accountType()); if (m_isEditing) { if (m_account.isLiquidAsset()) { ui->typeCombo->addItem(MyMoneyAccount::accountTypeToString(Account::Type::Checkings), (int)Account::Type::Checkings); ui->typeCombo->addItem(MyMoneyAccount::accountTypeToString(Account::Type::Savings), (int)Account::Type::Savings); ui->typeCombo->addItem(MyMoneyAccount::accountTypeToString(Account::Type::Cash), (int)Account::Type::Cash); } else { ui->typeCombo->addItem(typeString, (int)m_account.accountType()); // Once created, accounts of other account types are not // allowed to be changed. ui->typeCombo->setEnabled(false); } // Once created, a currency cannot be changed if it is referenced. ui->m_currency->setDisabled(MyMoneyFile::instance()->isReferenced(m_account)); } else { ui->typeCombo->addItem(MyMoneyAccount::accountTypeToString(Account::Type::Checkings), (int)Account::Type::Checkings); ui->typeCombo->addItem(MyMoneyAccount::accountTypeToString(Account::Type::Savings), (int)Account::Type::Savings); ui->typeCombo->addItem(MyMoneyAccount::accountTypeToString(Account::Type::Cash), (int)Account::Type::Cash); ui->typeCombo->addItem(MyMoneyAccount::accountTypeToString(Account::Type::CreditCard), (int)Account::Type::CreditCard); ui->typeCombo->addItem(MyMoneyAccount::accountTypeToString(Account::Type::Loan), (int)Account::Type::Loan); ui->typeCombo->addItem(MyMoneyAccount::accountTypeToString(Account::Type::Investment), (int)Account::Type::Investment); ui->typeCombo->addItem(MyMoneyAccount::accountTypeToString(Account::Type::Asset), (int)Account::Type::Asset); ui->typeCombo->addItem(MyMoneyAccount::accountTypeToString(Account::Type::Liability), (int)Account::Type::Liability); ui->typeCombo->addItem(MyMoneyAccount::accountTypeToString(Account::Type::Stock), (int)Account::Type::Stock); /* ui->typeCombo->addItem(MyMoneyAccount::accountTypeToString(Account::Type::CertificateDep), (int)Account::Type::CertificateDep); ui->typeCombo->addItem(MyMoneyAccount::accountTypeToString(Account::Type::MoneyMarket), (int)Account::Type::MoneyMarket); ui->typeCombo->addItem(MyMoneyAccount::accountTypeToString(Account::Type::Currency), (int)Account::Type::Currency); */ // Do not create account types that are not supported // by the current engine. if (m_account.accountType() == Account::Type::Unknown || m_account.accountType() == Account::Type::CertificateDep || m_account.accountType() == Account::Type::MoneyMarket || m_account.accountType() == Account::Type::Currency) typeString = MyMoneyAccount::accountTypeToString(Account::Type::Checkings); } ui->typeCombo->setCurrentItem(typeString, false); if (m_account.isInvest()) ui->m_institutionBox->hide(); ui->accountNoEdit->setText(m_account.number()); ui->m_qcheckboxPreferred->setChecked(m_account.value("PreferredAccount") == "Yes"); ui->m_qcheckboxNoVat->setChecked(m_account.value("NoVat") == "Yes"); loadKVP("iban", ui->ibanEdit); loadKVP("minBalanceAbsolute", ui->m_minBalanceAbsoluteEdit); loadKVP("minBalanceEarly", ui->m_minBalanceEarlyEdit); loadKVP("maxCreditAbsolute", ui->m_maxCreditAbsoluteEdit); loadKVP("maxCreditEarly", ui->m_maxCreditEarlyEdit); // reverse the sign for display purposes if (!ui->m_maxCreditAbsoluteEdit->lineedit()->text().isEmpty()) ui->m_maxCreditAbsoluteEdit->setValue(ui->m_maxCreditAbsoluteEdit->value()*MyMoneyMoney::MINUS_ONE); if (!ui->m_maxCreditEarlyEdit->lineedit()->text().isEmpty()) ui->m_maxCreditEarlyEdit->setValue(ui->m_maxCreditEarlyEdit->value()*MyMoneyMoney::MINUS_ONE); loadKVP("lastNumberUsed", ui->m_lastCheckNumberUsed); if (m_account.isInvest()) { ui->typeCombo->setEnabled(false); ui->m_qcheckboxPreferred->hide(); ui->m_currencyText->hide(); ui->m_currency->hide(); } else { // use the old field and override a possible new value if (!MyMoneyMoney(m_account.value("minimumBalance")).isZero()) { ui->m_minBalanceAbsoluteEdit->setValue(MyMoneyMoney(m_account.value("minimumBalance"))); } } // ui->m_qcheckboxTax->hide(); TODO should only be visible for VAT category/account } ui->m_currency->setSecurity(file->currency(m_account.currencyId())); // Load the institutions // then the accounts QString institutionName; try { if (m_isEditing && !m_account.institutionId().isEmpty()) institutionName = file->institution(m_account.institutionId()).name(); else institutionName.clear(); } catch (const MyMoneyException &e) { qDebug("exception in init for account dialog: %s", qPrintable(e.what())); } if (m_account.isInvest()) ui->m_parentAccounts->setEnabled(false); if (!m_categoryEditor) q->slotLoadInstitutions(institutionName); ui->accountNameEdit->setFocus(); q->connect(ui->buttonBox, &QDialogButtonBox::rejected, q, &QDialog::reject); q->connect(ui->buttonBox, &QDialogButtonBox::accepted, q, &KNewAccountDlg::okClicked); q->connect(ui->m_parentAccounts->selectionModel(), &QItemSelectionModel::selectionChanged, q, &KNewAccountDlg::slotSelectionChanged); q->connect(ui->m_qbuttonNew, &QAbstractButton::clicked, q, &KNewAccountDlg::slotNewClicked); q->connect(ui->typeCombo, static_cast(&QComboBox::currentIndexChanged), q, &KNewAccountDlg::slotAccountTypeChanged); q->connect(ui->accountNameEdit, &QLineEdit::textChanged, q, &KNewAccountDlg::slotCheckFinished); q->connect(ui->m_vatCategory, &QAbstractButton::toggled, q, &KNewAccountDlg::slotVatChanged); q->connect(ui->m_vatAssignment, &QAbstractButton::toggled, q, &KNewAccountDlg::slotVatAssignmentChanged); q->connect(ui->m_vatCategory, &QAbstractButton::toggled, q, &KNewAccountDlg::slotCheckFinished); q->connect(ui->m_vatAssignment, &QAbstractButton::toggled, q, &KNewAccountDlg::slotCheckFinished); q->connect(ui->m_vatRate, &KMyMoneyEdit::textChanged, q, &KNewAccountDlg::slotCheckFinished); q->connect(ui->m_vatAccount, &KMyMoneySelector::stateChanged, q, &KNewAccountDlg::slotCheckFinished); q->connect(ui->m_currency, static_cast(&QComboBox::activated), q, &KNewAccountDlg::slotCheckCurrency); q->connect(ui->m_minBalanceEarlyEdit, &KMyMoneyEdit::valueChanged, q, &KNewAccountDlg::slotAdjustMinBalanceAbsoluteEdit); q->connect(ui->m_minBalanceAbsoluteEdit, &KMyMoneyEdit::valueChanged, q, &KNewAccountDlg::slotAdjustMinBalanceEarlyEdit); q->connect(ui->m_maxCreditEarlyEdit, &KMyMoneyEdit::valueChanged, q, &KNewAccountDlg::slotAdjustMaxCreditAbsoluteEdit); q->connect(ui->m_maxCreditAbsoluteEdit, &KMyMoneyEdit::valueChanged, q, &KNewAccountDlg::slotAdjustMaxCreditEarlyEdit); q->connect(ui->m_qcomboboxInstitutions, static_cast(&QComboBox::activated), q, &KNewAccountDlg::slotLoadInstitutions); auto parentIndex = m_filterProxyModel->getSelectedParentAccountIndex(); ui->m_parentAccounts->expand(parentIndex); ui->m_parentAccounts->selectionModel()->select(parentIndex, QItemSelectionModel::SelectCurrent); ui->m_parentAccounts->scrollTo(parentIndex, QAbstractItemView::PositionAtTop); ui->m_vatCategory->setChecked(false); ui->m_vatAssignment->setChecked(false); // make sure our account does not have an id and no parent assigned // and certainly no children in case we create a new account if (!m_isEditing) { m_account.clearId(); m_account.setParentAccountId(QString()); m_account.removeAccountIds(); } else { if (!m_account.value("VatRate").isEmpty()) { ui->m_vatCategory->setChecked(true); ui->m_vatRate->setValue(MyMoneyMoney(m_account.value("VatRate"))*MyMoneyMoney(100, 1)); } else { if (!m_account.value("VatAccount").isEmpty()) { QString accId = m_account.value("VatAccount").toLatin1(); try { // make sure account exists MyMoneyFile::instance()->account(accId); ui->m_vatAssignment->setChecked(true); ui->m_vatAccount->setSelected(accId); ui->m_grossAmount->setChecked(true); if (m_account.value("VatAmount") == "Net") ui->m_netAmount->setChecked(true); } catch (const MyMoneyException &) { } } } } q->slotVatChanged(ui->m_vatCategory->isChecked()); q->slotVatAssignmentChanged(ui->m_vatAssignment->isChecked()); q->slotCheckFinished(); auto requiredFields = new KMandatoryFieldGroup(q); requiredFields->setOkButton(ui->buttonBox->button(QDialogButtonBox::Ok)); // button to be enabled when all fields present requiredFields->add(ui->accountNameEdit); } void loadKVP(const QString& key, KMyMoneyEdit* widget) { if (!widget) return; if (m_account.value(key).isEmpty()) { widget->clearText(); } else { widget->setValue(MyMoneyMoney(m_account.value(key))); } } void loadKVP(const QString& key, KLineEdit* widget) { if (!widget) return; widget->setText(m_account.value(key)); } void storeKVP(const QString& key, const QString& text, const QString& value) { if (text.isEmpty()) m_account.deletePair(key); else m_account.setValue(key, value); } void storeKVP(const QString& key, QCheckBox* widget) { if (widget) { if(widget->isChecked()) { m_account.setValue(key, "Yes");; } else { m_account.deletePair(key); } } } void storeKVP(const QString& key, KMyMoneyEdit* widget) { storeKVP(key, widget->lineedit()->text(), widget->text()); } void storeKVP(const QString& key, KLineEdit* widget) { storeKVP(key, widget->text(), widget->text()); } void loadVatAccounts() { QList list; MyMoneyFile::instance()->accountList(list); QList::Iterator it; QStringList loadListExpense; QStringList loadListIncome; QStringList loadListAsset; QStringList loadListLiability; for (it = list.begin(); it != list.end(); ++it) { if (!(*it).value("VatRate").isEmpty()) { if ((*it).accountType() == Account::Type::Expense) loadListExpense += (*it).id(); else if ((*it).accountType() == Account::Type::Income) loadListIncome += (*it).id(); else if ((*it).accountType() == Account::Type::Asset) loadListAsset += (*it).id(); else if ((*it).accountType() == Account::Type::Liability) loadListLiability += (*it).id(); } } AccountSet vatSet; if (!loadListAsset.isEmpty()) vatSet.load(ui->m_vatAccount, i18n("Asset"), loadListAsset, true); if (!loadListLiability.isEmpty()) vatSet.load(ui->m_vatAccount, i18n("Liability"), loadListLiability, false); if (!loadListIncome.isEmpty()) vatSet.load(ui->m_vatAccount, i18n("Income"), loadListIncome, false); if (!loadListExpense.isEmpty()) vatSet.load(ui->m_vatAccount, i18n("Expense"), loadListExpense, false); } void adjustEditWidgets(KMyMoneyEdit* dst, KMyMoneyEdit* src, char mode, int corr) { MyMoneyMoney factor(corr, 1); if (m_account.accountGroup() == Account::Type::Asset) factor = -factor; switch (mode) { case '<': if (src->value()*factor < dst->value()*factor) dst->setValue(src->value()); break; case '>': if (src->value()*factor > dst->value()*factor) dst->setValue(src->value()); break; } } void handleOpeningBalanceCheckbox(const QString ¤cyId) { if (m_account.accountType() == Account::Type::Equity) { // check if there is another opening balance account with the same currency bool isOtherOpenBalancingAccount = false; QList list; MyMoneyFile::instance()->accountList(list); QList::Iterator it; for (it = list.begin(); it != list.end(); ++it) { if (it->id() == m_account.id() || currencyId != it->currencyId() || it->accountType() != Account::Type::Equity) continue; if (it->value("OpeningBalanceAccount") == "Yes") { isOtherOpenBalancingAccount = true; break; } } if (!isOtherOpenBalancingAccount) { bool isOpenBalancingAccount = m_account.value("OpeningBalanceAccount") == "Yes"; ui->m_qcheckboxOpeningBalance->setChecked(isOpenBalancingAccount); if (isOpenBalancingAccount) { // let only allow state change if no transactions are assigned to this account bool hasTransactions = MyMoneyFile::instance()->transactionCount(m_account.id()) != 0; ui->m_qcheckboxOpeningBalance->setEnabled(!hasTransactions); if (hasTransactions) ui->m_qcheckboxOpeningBalance->setToolTip(i18n("Option has been disabled because there are transactions assigned to this account")); } } else { ui->m_qcheckboxOpeningBalance->setChecked(false); ui->m_qcheckboxOpeningBalance->setEnabled(false); ui->m_qcheckboxOpeningBalance->setToolTip(i18n("Option has been disabled because there is another account flagged to be an opening balance account for this currency")); } } else { ui->m_qcheckboxOpeningBalance->setVisible(false); } } KNewAccountDlg *q_ptr; Ui::KNewAccountDlg *ui; MyMoneyAccount m_account; MyMoneyAccount m_parentAccount; HierarchyFilterProxyModel *m_filterProxyModel; bool m_categoryEditor; bool m_isEditing; }; KNewAccountDlg::KNewAccountDlg(const MyMoneyAccount& account, bool isEditing, bool categoryEditor, QWidget *parent, const QString& title) : QDialog(parent), d_ptr(new KNewAccountDlgPrivate(this)) { Q_D(KNewAccountDlg); d->m_account = account; d->m_categoryEditor = categoryEditor; d->m_isEditing = isEditing; if (!title.isEmpty()) setWindowTitle(title); d->init(); } MyMoneyMoney KNewAccountDlg::openingBalance() const { Q_D(const KNewAccountDlg); return d->ui->m_openingBalanceEdit->value(); } void KNewAccountDlg::setOpeningBalance(const MyMoneyMoney& balance) { Q_D(KNewAccountDlg); d->ui->m_openingBalanceEdit->setValue(balance); } void KNewAccountDlg::setOpeningBalanceShown(bool shown) { Q_D(KNewAccountDlg); d->ui->m_openingBalanceLabel->setVisible(shown); d->ui->m_openingBalanceEdit->setVisible(shown); } void KNewAccountDlg::setOpeningDateShown(bool shown) { Q_D(KNewAccountDlg); d->ui->m_openingDateLabel->setVisible(shown); d->ui->m_openingDateEdit->setVisible(shown); } void KNewAccountDlg::okClicked() { Q_D(KNewAccountDlg); auto file = MyMoneyFile::instance(); QString accountNameText = d->ui->accountNameEdit->text(); if (accountNameText.isEmpty()) { KMessageBox::error(this, i18n("You have not specified a name.\nPlease fill in this field.")); d->ui->accountNameEdit->setFocus(); return; } MyMoneyAccount parent = parentAccount(); if (parent.name().length() == 0) { KMessageBox::error(this, i18n("Please select a parent account.")); return; } if (!d->m_categoryEditor) { QString institutionNameText = d->ui->m_qcomboboxInstitutions->currentText(); if (institutionNameText != i18n("(No Institution)")) { try { - auto file = MyMoneyFile::instance(); - QList list = file->institutionList(); QList::ConstIterator institutionIterator; for (institutionIterator = list.constBegin(); institutionIterator != list.constEnd(); ++institutionIterator) { if ((*institutionIterator).name() == institutionNameText) d->m_account.setInstitutionId((*institutionIterator).id()); } } catch (const MyMoneyException &e) { qDebug("Exception in account institution set: %s", qPrintable(e.what())); } } else { d->m_account.setInstitutionId(QString()); } } d->m_account.setName(accountNameText); d->m_account.setNumber(d->ui->accountNoEdit->text()); d->storeKVP("iban", d->ui->ibanEdit); d->storeKVP("minBalanceAbsolute", d->ui->m_minBalanceAbsoluteEdit); d->storeKVP("minBalanceEarly", d->ui->m_minBalanceEarlyEdit); // the figures for credit line with reversed sign if (!d->ui->m_maxCreditAbsoluteEdit->lineedit()->text().isEmpty()) d->ui->m_maxCreditAbsoluteEdit->setValue(d->ui->m_maxCreditAbsoluteEdit->value()*MyMoneyMoney::MINUS_ONE); if (!d->ui->m_maxCreditEarlyEdit->lineedit()->text().isEmpty()) d->ui->m_maxCreditEarlyEdit->setValue(d->ui->m_maxCreditEarlyEdit->value()*MyMoneyMoney::MINUS_ONE); d->storeKVP("maxCreditAbsolute", d->ui->m_maxCreditAbsoluteEdit); d->storeKVP("maxCreditEarly", d->ui->m_maxCreditEarlyEdit); if (!d->ui->m_maxCreditAbsoluteEdit->lineedit()->text().isEmpty()) d->ui->m_maxCreditAbsoluteEdit->setValue(d->ui->m_maxCreditAbsoluteEdit->value()*MyMoneyMoney::MINUS_ONE); if (!d->ui->m_maxCreditEarlyEdit->lineedit()->text().isEmpty()) d->ui->m_maxCreditEarlyEdit->setValue(d->ui->m_maxCreditEarlyEdit->value()*MyMoneyMoney::MINUS_ONE); d->storeKVP("lastNumberUsed", d->ui->m_lastCheckNumberUsed); // delete a previous version of the minimumbalance information d->storeKVP("minimumBalance", QString(), QString()); Account::Type acctype; if (!d->m_categoryEditor) { acctype = static_cast(d->ui->typeCombo->currentData().toInt()); // If it's a loan, check if the parent is asset or liability. In // case of asset, we change the account type to be AssetLoan if (acctype == Account::Type::Loan && parent.accountGroup() == Account::Type::Asset) acctype = Account::Type::AssetLoan; } else { acctype = parent.accountGroup(); QString newName; if (!MyMoneyFile::instance()->isStandardAccount(parent.id())) { newName = MyMoneyFile::instance()->accountToCategory(parent.id()) + MyMoneyFile::AccountSeparator; } newName += accountNameText; if (!file->categoryToAccount(newName, acctype).isEmpty() && (file->categoryToAccount(newName, acctype) != d->m_account.id())) { KMessageBox::error(this, QString("") + i18n("A category named %1 already exists. You cannot create a second category with the same name.", newName) + QString("")); return; } } d->m_account.setAccountType(acctype); d->m_account.setDescription(d->ui->descriptionEdit->toPlainText()); d->m_account.setOpeningDate(d->ui->m_openingDateEdit->date()); if (!d->m_categoryEditor) { d->m_account.setCurrencyId(d->ui->m_currency->security().id()); d->storeKVP("PreferredAccount", d->ui->m_qcheckboxPreferred); d->storeKVP("NoVat", d->ui->m_qcheckboxNoVat); if (d->ui->m_minBalanceAbsoluteEdit->isVisible()) { d->m_account.setValue("minimumBalance", d->ui->m_minBalanceAbsoluteEdit->value().toString()); } } else { if (KMyMoneySettings::hideUnusedCategory() && !d->m_isEditing) { KMessageBox::information(this, i18n("You have selected to suppress the display of unused categories in the KMyMoney configuration dialog. The category you just created will therefore only be shown if it is used. Otherwise, it will be hidden in the accounts/categories view."), i18n("Hidden categories"), "NewHiddenCategory"); } d->m_account.setCostCenterRequired(d->ui->m_costCenterRequiredCheckBox->isChecked()); } d->storeKVP("Tax", d->ui->m_qcheckboxTax); if (d->ui->m_qcheckboxOpeningBalance->isChecked()) d->m_account.setValue("OpeningBalanceAccount", "Yes"); else d->m_account.deletePair("OpeningBalanceAccount"); d->m_account.deletePair("VatAccount"); d->m_account.deletePair("VatAmount"); d->m_account.deletePair("VatRate"); if (d->ui->m_vatCategory->isChecked()) { d->m_account.setValue("VatRate", (d->ui->m_vatRate->value().abs() / MyMoneyMoney(100, 1)).toString()); } else { if (d->ui->m_vatAssignment->isChecked() && !d->ui->m_vatAccount->selectedItems().isEmpty()) { d->m_account.setValue("VatAccount", d->ui->m_vatAccount->selectedItems().first()); if (d->ui->m_netAmount->isChecked()) d->m_account.setValue("VatAmount", "Net"); } } accept(); } MyMoneyAccount KNewAccountDlg::account() { Q_D(KNewAccountDlg); // assign the right currency to the account d->m_account.setCurrencyId(d->ui->m_currency->security().id()); // and the price mode switch (d->ui->m_priceMode->currentItem()) { case 0: d->m_account.deletePair("priceMode"); break; case 1: case 2: d->m_account.setValue("priceMode", QString("%1").arg(d->ui->m_priceMode->currentItem())); break; } return d->m_account; } MyMoneyAccount KNewAccountDlg::parentAccount() const { Q_D(const KNewAccountDlg); return d->m_parentAccount; } void KNewAccountDlg::slotSelectionChanged(const QItemSelection ¤t, const QItemSelection &previous) { Q_UNUSED(previous) Q_D(KNewAccountDlg); if (!current.indexes().empty()) { QVariant account = d->ui->m_parentAccounts->model()->data(current.indexes().front(), (int)eAccountsModel::Role::Account); if (account.isValid()) { d->m_parentAccount = account.value(); d->ui->m_subAccountLabel->setText(i18n("Is a sub account of %1", d->m_parentAccount.name())); } } } void KNewAccountDlg::slotLoadInstitutions(const QString& name) { Q_D(KNewAccountDlg); d->ui->m_qcomboboxInstitutions->clear(); QString bic; // Are we forcing the user to use institutions? d->ui->m_qcomboboxInstitutions->addItem(i18n("(No Institution)")); d->ui->m_bicValue->setText(" "); d->ui->ibanEdit->setEnabled(false); d->ui->accountNoEdit->setEnabled(false); try { auto file = MyMoneyFile::instance(); QList list = file->institutionList(); QList::ConstIterator institutionIterator; for (institutionIterator = list.constBegin(); institutionIterator != list.constEnd(); ++institutionIterator) { if ((*institutionIterator).name() == name) { d->ui->ibanEdit->setEnabled(true); d->ui->accountNoEdit->setEnabled(true); d->ui->m_bicValue->setText((*institutionIterator).value("bic")); } d->ui->m_qcomboboxInstitutions->addItem((*institutionIterator).name()); } d->ui->m_qcomboboxInstitutions->setCurrentItem(name, false); } catch (const MyMoneyException &e) { qDebug("Exception in institution load: %s", qPrintable(e.what())); } } void KNewAccountDlg::slotNewClicked() { MyMoneyInstitution institution; QPointer dlg = new KNewBankDlg(institution, this); if (dlg->exec()) { MyMoneyFileTransaction ft; try { auto file = MyMoneyFile::instance(); institution = dlg->institution(); file->addInstitution(institution); ft.commit(); slotLoadInstitutions(institution.name()); } catch (const MyMoneyException &) { KMessageBox::information(this, i18n("Cannot add institution")); } } delete dlg; } void KNewAccountDlg::slotAccountTypeChanged(int index) { Q_D(KNewAccountDlg); Account::Type oldType; auto type = d->ui->typeCombo->itemData(index).value(); try { oldType = d->m_account.accountType(); if (oldType != type) { d->m_account.setAccountType(type); // update the account group displayed in the accounts hierarchy d->m_filterProxyModel->clear(); d->m_filterProxyModel->addAccountGroup(QVector {d->m_account.accountGroup()}); } } catch (const MyMoneyException &) { qWarning("Unexpected exception in KNewAccountDlg::slotAccountTypeChanged()"); } } void KNewAccountDlg::slotCheckFinished() { Q_D(KNewAccountDlg); auto showButton = true; if (d->ui->accountNameEdit->text().length() == 0) { showButton = false; } if (d->ui->m_vatCategory->isChecked() && d->ui->m_vatRate->value() <= MyMoneyMoney()) { showButton = false; } else { if (d->ui->m_vatAssignment->isChecked() && d->ui->m_vatAccount->selectedItems().isEmpty()) showButton = false; } d->ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(showButton); } void KNewAccountDlg::slotVatChanged(bool state) { Q_D(KNewAccountDlg); if (state) { d->ui->m_vatCategoryFrame->show(); d->ui->m_vatAssignmentFrame->hide(); } else { d->ui->m_vatCategoryFrame->hide(); if (!d->m_account.isAssetLiability()) { d->ui->m_vatAssignmentFrame->show(); } } } void KNewAccountDlg::slotVatAssignmentChanged(bool state) { Q_D(KNewAccountDlg); d->ui->m_vatAccount->setEnabled(state); d->ui->m_amountGroupBox->setEnabled(state); } void KNewAccountDlg::slotAdjustMinBalanceAbsoluteEdit(const QString&) { Q_D(KNewAccountDlg); d->adjustEditWidgets(d->ui->m_minBalanceAbsoluteEdit, d->ui->m_minBalanceEarlyEdit, '<', -1); } void KNewAccountDlg::slotAdjustMinBalanceEarlyEdit(const QString&) { Q_D(KNewAccountDlg); d->adjustEditWidgets(d->ui->m_minBalanceEarlyEdit, d->ui->m_minBalanceAbsoluteEdit, '>', -1); } void KNewAccountDlg::slotAdjustMaxCreditAbsoluteEdit(const QString&) { Q_D(KNewAccountDlg); d->adjustEditWidgets(d->ui->m_maxCreditAbsoluteEdit, d->ui->m_maxCreditEarlyEdit, '>', 1); } void KNewAccountDlg::slotAdjustMaxCreditEarlyEdit(const QString&) { Q_D(KNewAccountDlg); d->adjustEditWidgets(d->ui->m_maxCreditEarlyEdit, d->ui->m_maxCreditAbsoluteEdit, '<', 1); } void KNewAccountDlg::slotCheckCurrency(int index) { Q_D(KNewAccountDlg); Q_UNUSED(index) d->handleOpeningBalanceCheckbox(d->ui->m_currency->security().id()); } void KNewAccountDlg::addTab(QWidget* w, const QString& name) { Q_D(KNewAccountDlg); if (w) { w->setParent(d->ui->m_tab); d->ui->m_tab->addTab(w, name); } } void KNewAccountDlg::newCategory(MyMoneyAccount& account, const MyMoneyAccount& parent) { if (KMessageBox::questionYesNo(nullptr, QString::fromLatin1("%1").arg(i18n("

The category %1 currently does not exist. Do you want to create it?

The parent account will default to %2 but can be changed in the following dialog.

", account.name(), parent.name())), i18n("Create category"), KStandardGuiItem::yes(), KStandardGuiItem::no(), "CreateNewCategories") == KMessageBox::Yes) { KNewAccountDlg::createCategory(account, parent); } else { // we should not keep the 'no' setting because that can confuse people like // I have seen in some usability tests. So we just delete it right away. KSharedConfigPtr kconfig = KSharedConfig::openConfig(); if (kconfig) { kconfig->group(QLatin1String("Notification Messages")).deleteEntry(QLatin1String("CreateNewCategories")); } } } void KNewAccountDlg::createCategory(MyMoneyAccount& account, const MyMoneyAccount& parent) { if (!parent.id().isEmpty()) { try { // make sure parent account exists MyMoneyFile::instance()->account(parent.id()); account.setParentAccountId(parent.id()); account.setAccountType(parent.accountType()); } catch (const MyMoneyException &) { } } QPointer dialog = new KNewAccountDlg(account, false, true, 0, i18n("Create a new Category")); dialog->setOpeningBalanceShown(false); dialog->setOpeningDateShown(false); if (dialog->exec() == QDialog::Accepted && dialog != 0) { MyMoneyAccount parentAccount, brokerageAccount; account = dialog->account(); parentAccount = dialog->parentAccount(); MyMoneyFile::instance()->createAccount(account, parentAccount, brokerageAccount, MyMoneyMoney()); } delete dialog; } diff --git a/kmymoney/kmymoney.cpp b/kmymoney/kmymoney.cpp index 29fb75c96..6a8761992 100644 --- a/kmymoney/kmymoney.cpp +++ b/kmymoney/kmymoney.cpp @@ -1,4316 +1,4316 @@ /*************************************************************************** kmymoney.cpp ------------------- copyright : (C) 2000 by Michael Edwardes (C) 2007 by Thomas Baumgart (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 #include "kmymoney.h" // ---------------------------------------------------------------------------- // Std C++ / STL Includes #include #include #include // ---------------------------------------------------------------------------- // QT Includes #include #include // only for performance tests #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef KF5Holidays_FOUND #include #include #endif #ifdef KF5Activities_FOUND #include #endif // ---------------------------------------------------------------------------- // Project Includes #include "kmymoneysettings.h" #include "kmymoneyadaptor.h" #include "dialogs/settings/ksettingskmymoney.h" #include "dialogs/kbackupdlg.h" #include "dialogs/kenterscheduledlg.h" #include "dialogs/kconfirmmanualenterdlg.h" #include "dialogs/kmymoneypricedlg.h" #include "dialogs/kcurrencyeditdlg.h" #include "dialogs/kequitypriceupdatedlg.h" #include "dialogs/kmymoneyfileinfodlg.h" #include "dialogs/knewbankdlg.h" #include "wizards/newinvestmentwizard/knewinvestmentwizard.h" #include "dialogs/knewaccountdlg.h" #include "dialogs/editpersonaldatadlg.h" #include "dialogs/kcurrencycalculator.h" #include "dialogs/keditscheduledlg.h" #include "wizards/newloanwizard/keditloanwizard.h" #include "dialogs/kpayeereassigndlg.h" #include "dialogs/kcategoryreassigndlg.h" #include "wizards/endingbalancedlg/kendingbalancedlg.h" #include "dialogs/kbalancechartdlg.h" #include "dialogs/kloadtemplatedlg.h" #include "dialogs/kgpgkeyselectiondlg.h" #include "dialogs/ktemplateexportdlg.h" #include "dialogs/transactionmatcher.h" #include "wizards/newuserwizard/knewuserwizard.h" #include "wizards/newaccountwizard/knewaccountwizard.h" #include "dialogs/kbalancewarning.h" #include "widgets/kmymoneyaccountselector.h" #include "widgets/kmymoneypayeecombo.h" #include "widgets/amountedit.h" #include "widgets/kmymoneyedit.h" #include "widgets/kmymoneymvccombo.h" #include "views/kmymoneyview.h" #include "models/models.h" #include "models/accountsmodel.h" #include "models/equitiesmodel.h" #include "models/securitiesmodel.h" #include "mymoney/mymoneyobject.h" #include "mymoney/mymoneyfile.h" #include "mymoney/mymoneyinstitution.h" #include "mymoney/mymoneyaccount.h" #include "mymoney/mymoneyaccountloan.h" #include "mymoney/mymoneysecurity.h" #include "mymoney/mymoneypayee.h" #include "mymoney/mymoneyprice.h" #include "mymoney/mymoneytag.h" #include "mymoney/mymoneybudget.h" #include "mymoney/mymoneyreport.h" #include "mymoney/mymoneysplit.h" #include "mymoney/mymoneyutils.h" #include "mymoney/mymoneystatement.h" #include "mymoney/mymoneyforecast.h" #include "mymoney/mymoneytransactionfilter.h" #include "converter/mymoneystatementreader.h" #include "converter/mymoneytemplate.h" #include "plugins/interfaces/kmmappinterface.h" #include "plugins/interfaces/kmmviewinterface.h" #include "plugins/interfaces/kmmstatementinterface.h" #include "plugins/interfaces/kmmimportinterface.h" #include "plugins/interfaceloader.h" #include "plugins/onlinepluginextended.h" #include "pluginloader.h" #include "tasks/credittransfer.h" #include "icons/icons.h" #include "misc/webconnect.h" #include "storage/mymoneystoragemgr.h" #include "storage/mymoneystoragexml.h" #include "storage/mymoneystoragebin.h" #include "storage/mymoneystorageanon.h" #include #include "transactioneditor.h" #include #include #include "kmymoneyutils.h" #include "kcreditswindow.h" #include "ledgerdelegate.h" #include "storageenums.h" #include "mymoneyenums.h" #include "dialogenums.h" #include "viewenums.h" #include "menuenums.h" #include "misc/platformtools.h" #ifdef KMM_DEBUG #include "mymoney/storage/mymoneystoragedump.h" #include "mymoneytracer.h" #endif using namespace Icons; using namespace eMenu; static constexpr KCompressionDevice::CompressionType const& COMPRESSION_TYPE = KCompressionDevice::GZip; static constexpr char recoveryKeyId[] = "0xD2B08440"; static constexpr char recoveryKeyId2[] = "59B0F826D2B08440"; // define the default period to warn about an expiring recoverkey to 30 days // but allows to override this setting during build time #ifndef RECOVER_KEY_EXPIRATION_WARNING #define RECOVER_KEY_EXPIRATION_WARNING 30 #endif enum backupStateE { BACKUP_IDLE = 0, BACKUP_MOUNTING, BACKUP_COPYING, BACKUP_UNMOUNTING }; class KMyMoneyApp::Private { public: Private(KMyMoneyApp *app) : q(app), m_statementXMLindex(0), m_balanceWarning(0), m_backupState(backupStateE::BACKUP_IDLE), m_backupResult(0), m_backupMount(0), m_ignoreBackupExitCode(false), m_fileOpen(false), m_fmode(QFileDevice::ReadUser | QFileDevice::WriteUser), m_fileType(KMyMoneyApp::KmmXML), m_myMoneyView(nullptr), m_startDialog(false), m_progressBar(nullptr), m_statusLabel(nullptr), m_autoSaveEnabled(true), m_autoSaveTimer(nullptr), m_progressTimer(nullptr), m_autoSavePeriod(0), m_inAutoSaving(false), m_saveEncrypted(nullptr), m_additionalKeyLabel(nullptr), m_additionalKeyButton(nullptr), m_recentFiles(nullptr), #ifdef KF5Holidays_FOUND m_holidayRegion(nullptr), #endif #ifdef KF5Activities_FOUND m_activityResourceInstance(nullptr), #endif m_applicationIsReady(true), m_webConnect(new WebConnect(app)) { // since the days of the week are from 1 to 7, // and a day of the week is used to index this bit array, // resize the array to 8 elements (element 0 is left unused) m_processingDays.resize(8); } void closeFile(); void unlinkStatementXML(); void moveInvestmentTransaction(const QString& fromId, const QString& toId, const MyMoneyTransaction& t); QList > automaticReconciliation(const MyMoneyAccount &account, const QList > &transactions, const MyMoneyMoney &amount); /** * The public interface. */ KMyMoneyApp * const q; int m_statementXMLindex; KBalanceWarning* m_balanceWarning; /** the configuration object of the application */ KSharedConfigPtr m_config; /** * @brief Structure of plugins objects by their interfaces */ KMyMoneyPlugin::Container m_plugins; /** * The following variable represents the state while crafting a backup. * It can have the following values * * - IDLE: the default value if not performing a backup * - MOUNTING: when a mount command has been issued * - COPYING: when a copy command has been issued * - UNMOUNTING: when an unmount command has been issued */ backupStateE m_backupState; /** * This variable keeps the result of the backup operation. */ int m_backupResult; /** * This variable is set, when the user selected to mount/unmount * the backup volume. */ bool m_backupMount; /** * Flag for internal run control */ bool m_ignoreBackupExitCode; bool m_fileOpen; QFileDevice::Permissions m_fmode; KMyMoneyApp::fileTypeE m_fileType; KProcess m_proc; /// A pointer to the view holding the tabs. KMyMoneyView *m_myMoneyView; /// The URL of the file currently being edited when open. QUrl m_fileName; bool m_startDialog; QString m_mountpoint; QProgressBar* m_progressBar; QTime m_lastUpdate; QLabel* m_statusLabel; // allows multiple imports to be launched trough web connect and to be executed sequentially QQueue m_importUrlsQueue; // This is Auto Saving related bool m_autoSaveEnabled; QTimer* m_autoSaveTimer; QTimer* m_progressTimer; int m_autoSavePeriod; bool m_inAutoSaving; // Pointer to the combo box used for key selection during // File/Save as KComboBox* m_saveEncrypted; // id's that need to be remembered QString m_accountGoto, m_payeeGoto; QStringList m_additionalGpgKeys; QLabel* m_additionalKeyLabel; QPushButton* m_additionalKeyButton; KRecentFilesAction* m_recentFiles; #ifdef KF5Holidays_FOUND // used by the calendar interface for schedules KHolidays::HolidayRegion* m_holidayRegion; #endif #ifdef KF5Activities_FOUND KActivities::ResourceInstance * m_activityResourceInstance; #endif QBitArray m_processingDays; QMap m_holidayMap; QStringList m_consistencyCheckResult; bool m_applicationIsReady; WebConnect* m_webConnect; // methods void consistencyCheck(bool alwaysDisplayResults); static void setThemedCSS(); void copyConsistencyCheckResults(); void saveConsistencyCheckResults(); void checkAccountName(const MyMoneyAccount& _acc, const QString& name) const { auto file = MyMoneyFile::instance(); if (_acc.name() != name) { MyMoneyAccount acc(_acc); acc.setName(name); file->modifyAccount(acc); } } /** * This method updates names of currencies from file to localized names */ void updateCurrencyNames() { auto file = MyMoneyFile::instance(); MyMoneyFileTransaction ft; QList storedCurrencies = MyMoneyFile::instance()->currencyList(); QList availableCurrencies = MyMoneyFile::instance()->availableCurrencyList(); QStringList currencyIDs; foreach (auto currency, availableCurrencies) currencyIDs.append(currency.id()); try { foreach (auto currency, storedCurrencies) { int i = currencyIDs.indexOf(currency.id()); if (i != -1 && availableCurrencies.at(i).name() != currency.name()) { currency.setName(availableCurrencies.at(i).name()); file->modifyCurrency(currency); } } ft.commit(); } catch (const MyMoneyException &e) { qDebug("Error %s updating currency names", qPrintable(e.what())); } } void updateAccountNames() { // make sure we setup the name of the base accounts in translated form try { MyMoneyFileTransaction ft; const auto file = MyMoneyFile::instance(); checkAccountName(file->asset(), i18n("Asset")); checkAccountName(file->liability(), i18n("Liability")); checkAccountName(file->income(), i18n("Income")); checkAccountName(file->expense(), i18n("Expense")); checkAccountName(file->equity(), i18n("Equity")); ft.commit(); } catch (const MyMoneyException &) { } } void ungetString(QIODevice *qfile, char *buf, int len) { buf = &buf[len-1]; while (len--) { qfile->ungetChar(*buf--); } } bool applyFileFixes() { const auto blocked = MyMoneyFile::instance()->blockSignals(true); KSharedConfigPtr config = KSharedConfig::openConfig(); KConfigGroup grp = config->group("General Options"); // For debugging purposes, we can turn off the automatic fix manually // by setting the entry in kmymoneyrc to true grp = config->group("General Options"); if (grp.readEntry("SkipFix", false) != true) { MyMoneyFileTransaction ft; try { // Check if we have to modify the file before we allow to work with it auto s = MyMoneyFile::instance()->storage(); while (s->fileFixVersion() < s->currentFixVersion()) { qDebug("%s", qPrintable((QString("testing fileFixVersion %1 < %2").arg(s->fileFixVersion()).arg(s->currentFixVersion())))); switch (s->fileFixVersion()) { case 0: fixFile_0(); s->setFileFixVersion(1); break; case 1: fixFile_1(); s->setFileFixVersion(2); break; case 2: fixFile_2(); s->setFileFixVersion(3); break; case 3: fixFile_3(); s->setFileFixVersion(4); break; // add new levels above. Don't forget to increase currentFixVersion() for all // the storage backends this fix applies to default: throw MYMONEYEXCEPTION(i18n("Unknown fix level in input file")); } } ft.commit(); } catch (const MyMoneyException &) { MyMoneyFile::instance()->blockSignals(blocked); return false; } } else { qDebug("Skipping automatic transaction fix!"); } MyMoneyFile::instance()->blockSignals(blocked); return true; } void connectStorageToModels() { q->connect(MyMoneyFile::instance(), &MyMoneyFile::objectAdded, Models::instance()->accountsModel(), &AccountsModel::slotObjectAdded); q->connect(MyMoneyFile::instance(), &MyMoneyFile::objectModified, Models::instance()->accountsModel(), &AccountsModel::slotObjectModified); q->connect(MyMoneyFile::instance(), &MyMoneyFile::objectRemoved, Models::instance()->accountsModel(), &AccountsModel::slotObjectRemoved); q->connect(MyMoneyFile::instance(), &MyMoneyFile::balanceChanged, Models::instance()->accountsModel(), &AccountsModel::slotBalanceOrValueChanged); q->connect(MyMoneyFile::instance(), &MyMoneyFile::valueChanged, Models::instance()->accountsModel(), &AccountsModel::slotBalanceOrValueChanged); q->connect(MyMoneyFile::instance(), &MyMoneyFile::objectAdded, Models::instance()->institutionsModel(), &InstitutionsModel::slotObjectAdded); q->connect(MyMoneyFile::instance(), &MyMoneyFile::objectModified, Models::instance()->institutionsModel(), &InstitutionsModel::slotObjectModified); q->connect(MyMoneyFile::instance(), &MyMoneyFile::objectRemoved, Models::instance()->institutionsModel(), &InstitutionsModel::slotObjectRemoved); q->connect(MyMoneyFile::instance(), &MyMoneyFile::balanceChanged, Models::instance()->institutionsModel(), &AccountsModel::slotBalanceOrValueChanged); q->connect(MyMoneyFile::instance(), &MyMoneyFile::valueChanged, Models::instance()->institutionsModel(), &AccountsModel::slotBalanceOrValueChanged); q->connect(MyMoneyFile::instance(), &MyMoneyFile::objectAdded, Models::instance()->equitiesModel(), &EquitiesModel::slotObjectAdded); q->connect(MyMoneyFile::instance(), &MyMoneyFile::objectModified, Models::instance()->equitiesModel(), &EquitiesModel::slotObjectModified); q->connect(MyMoneyFile::instance(), &MyMoneyFile::objectRemoved, Models::instance()->equitiesModel(), &EquitiesModel::slotObjectRemoved); q->connect(MyMoneyFile::instance(), &MyMoneyFile::balanceChanged, Models::instance()->equitiesModel(), &EquitiesModel::slotBalanceOrValueChanged); q->connect(MyMoneyFile::instance(), &MyMoneyFile::valueChanged, Models::instance()->equitiesModel(), &EquitiesModel::slotBalanceOrValueChanged); q->connect(MyMoneyFile::instance(), &MyMoneyFile::objectAdded, Models::instance()->securitiesModel(), &SecuritiesModel::slotObjectAdded); q->connect(MyMoneyFile::instance(), &MyMoneyFile::objectModified, Models::instance()->securitiesModel(), &SecuritiesModel::slotObjectModified); q->connect(MyMoneyFile::instance(), &MyMoneyFile::objectRemoved, Models::instance()->securitiesModel(), &SecuritiesModel::slotObjectRemoved); } void disconnectStorageFromModels() { q->disconnect(MyMoneyFile::instance(), &MyMoneyFile::objectAdded, Models::instance()->accountsModel(), &AccountsModel::slotObjectAdded); q->disconnect(MyMoneyFile::instance(), &MyMoneyFile::objectModified, Models::instance()->accountsModel(), &AccountsModel::slotObjectModified); q->disconnect(MyMoneyFile::instance(), &MyMoneyFile::objectRemoved, Models::instance()->accountsModel(), &AccountsModel::slotObjectRemoved); q->disconnect(MyMoneyFile::instance(), &MyMoneyFile::balanceChanged, Models::instance()->accountsModel(), &AccountsModel::slotBalanceOrValueChanged); q->disconnect(MyMoneyFile::instance(), &MyMoneyFile::valueChanged, Models::instance()->accountsModel(), &AccountsModel::slotBalanceOrValueChanged); q->disconnect(MyMoneyFile::instance(), &MyMoneyFile::objectAdded, Models::instance()->institutionsModel(), &InstitutionsModel::slotObjectAdded); q->disconnect(MyMoneyFile::instance(), &MyMoneyFile::objectModified, Models::instance()->institutionsModel(), &InstitutionsModel::slotObjectModified); q->disconnect(MyMoneyFile::instance(), &MyMoneyFile::objectRemoved, Models::instance()->institutionsModel(), &InstitutionsModel::slotObjectRemoved); q->disconnect(MyMoneyFile::instance(), &MyMoneyFile::balanceChanged, Models::instance()->institutionsModel(), &AccountsModel::slotBalanceOrValueChanged); q->disconnect(MyMoneyFile::instance(), &MyMoneyFile::valueChanged, Models::instance()->institutionsModel(), &AccountsModel::slotBalanceOrValueChanged); q->disconnect(MyMoneyFile::instance(), &MyMoneyFile::objectAdded, Models::instance()->equitiesModel(), &EquitiesModel::slotObjectAdded); q->disconnect(MyMoneyFile::instance(), &MyMoneyFile::objectModified, Models::instance()->equitiesModel(), &EquitiesModel::slotObjectModified); q->disconnect(MyMoneyFile::instance(), &MyMoneyFile::objectRemoved, Models::instance()->equitiesModel(), &EquitiesModel::slotObjectRemoved); q->disconnect(MyMoneyFile::instance(), &MyMoneyFile::balanceChanged, Models::instance()->equitiesModel(), &EquitiesModel::slotBalanceOrValueChanged); q->disconnect(MyMoneyFile::instance(), &MyMoneyFile::valueChanged, Models::instance()->equitiesModel(), &EquitiesModel::slotBalanceOrValueChanged); q->disconnect(MyMoneyFile::instance(), &MyMoneyFile::objectAdded, Models::instance()->securitiesModel(), &SecuritiesModel::slotObjectAdded); q->disconnect(MyMoneyFile::instance(), &MyMoneyFile::objectModified, Models::instance()->securitiesModel(), &SecuritiesModel::slotObjectModified); q->disconnect(MyMoneyFile::instance(), &MyMoneyFile::objectRemoved, Models::instance()->securitiesModel(), &SecuritiesModel::slotObjectRemoved); } /** * This method is used after a file or database has been * read into storage, and performs various initialization tasks * * @retval true all went okay * @retval false an exception occurred during this process */ bool initializeStorage() { const auto blocked = MyMoneyFile::instance()->blockSignals(true); updateAccountNames(); updateCurrencyNames(); selectBaseCurrency(); // setup the standard precision AmountEdit::setStandardPrecision(MyMoneyMoney::denomToPrec(MyMoneyFile::instance()->baseCurrency().smallestAccountFraction())); KMyMoneyEdit::setStandardPrecision(MyMoneyMoney::denomToPrec(MyMoneyFile::instance()->baseCurrency().smallestAccountFraction())); if (!applyFileFixes()) return false; MyMoneyFile::instance()->blockSignals(blocked); emit q->kmmFilePlugin(KMyMoneyApp::postOpen); Models::instance()->fileOpened(); connectStorageToModels(); // inform everyone about new data MyMoneyFile::instance()->forceDataChanged(); q->slotCheckSchedules(); m_myMoneyView->slotFileOpened(); onlineJobAdministration::instance()->updateActions(); return true; } /** * This method attaches an empty storage object to the MyMoneyFile * object. It calls removeStorage() to remove a possibly attached * storage object. */ void newStorage() { removeStorage(); auto file = MyMoneyFile::instance(); file->attachStorage(new MyMoneyStorageMgr); } /** * This method removes an attached storage from the MyMoneyFile * object. */ void removeStorage() { auto file = MyMoneyFile::instance(); auto p = file->storage(); if (p) { file->detachStorage(p); delete p; } } /** * if no base currency is defined, start the dialog and force it to be set */ void selectBaseCurrency() { auto file = MyMoneyFile::instance(); // check if we have a base currency. If not, we need to select one QString baseId; try { baseId = MyMoneyFile::instance()->baseCurrency().id(); } catch (const MyMoneyException &e) { qDebug("%s", qPrintable(e.what())); } if (baseId.isEmpty()) { QPointer dlg = new KCurrencyEditDlg(q); // connect(dlg, SIGNAL(selectBaseCurrency(MyMoneySecurity)), this, SLOT(slotSetBaseCurrency(MyMoneySecurity))); dlg->exec(); delete dlg; } try { baseId = MyMoneyFile::instance()->baseCurrency().id(); } catch (const MyMoneyException &e) { qDebug("%s", qPrintable(e.what())); } if (!baseId.isEmpty()) { // check that all accounts have a currency QList list; file->accountList(list); QList::Iterator it; // don't forget those standard accounts list << file->asset(); list << file->liability(); list << file->income(); list << file->expense(); list << file->equity(); for (it = list.begin(); it != list.end(); ++it) { QString cid; try { if (!(*it).currencyId().isEmpty() || (*it).currencyId().length() != 0) cid = MyMoneyFile::instance()->currency((*it).currencyId()).id(); } catch (const MyMoneyException& e) { qDebug() << QLatin1String("Account") << (*it).id() << (*it).name() << e.what(); } if (cid.isEmpty()) { (*it).setCurrencyId(baseId); MyMoneyFileTransaction ft; try { file->modifyAccount(*it); ft.commit(); } catch (const MyMoneyException &e) { qDebug("Unable to setup base currency in account %s (%s): %s", qPrintable((*it).name()), qPrintable((*it).id()), qPrintable(e.what())); } } } } } /** * Calls MyMoneyFile::readAllData which reads a MyMoneyFile into appropriate * data structures in memory. The return result is examined to make sure no * errors occurred whilst parsing. * * @param url The URL to read from. * If no protocol is specified, file:// is assumed. * * @return Whether the read was successful. */ bool openNondatabase(const QUrl &url) { if (!url.isValid()) throw MYMONEYEXCEPTION(QString::fromLatin1("Invalid URL %1").arg(qPrintable(url.url()))); QString fileName; auto downloadedFile = false; if (url.isLocalFile()) { fileName = url.toLocalFile(); } else { fileName = KMyMoneyUtils::downloadFile(url); downloadedFile = true; } if (!KMyMoneyUtils::fileExists(QUrl::fromLocalFile(fileName))) throw MYMONEYEXCEPTION(QString::fromLatin1("Error opening the file.\n" "Requested file: '%1'.\n" "Downloaded file: '%2'").arg(qPrintable(url.url()), fileName)); QFile file(fileName); if (!file.open(QIODevice::ReadOnly)) throw MYMONEYEXCEPTION(QString::fromLatin1("Cannot read the file: %1").arg(fileName)); QByteArray qbaFileHeader(2, '\0'); const auto sFileToShort = QString::fromLatin1("File %1 is too short.").arg(fileName); if (file.read(qbaFileHeader.data(), 2) != 2) throw MYMONEYEXCEPTION(sFileToShort); file.close(); // There's a problem with the KFilterDev and KGPGFile classes: // One supports the at(n) member but not ungetch() together with // read() and the other does not provide an at(n) method but // supports read() that considers the ungetch() buffer. QFile // supports everything so this is not a problem. We solve the problem // for now by keeping track of which method can be used. auto haveAt = true; auto isEncrypted = false; emit q->kmmFilePlugin(preOpen); QIODevice* qfile = nullptr; QString sFileHeader(qbaFileHeader); if (sFileHeader == QString("\037\213")) { // gzipped? qfile = new KCompressionDevice(fileName, COMPRESSION_TYPE); } else if (sFileHeader == QString("--") || // PGP ASCII armored? sFileHeader == QString("\205\001") || // PGP binary? sFileHeader == QString("\205\002")) { // PGP binary? if (KGPGFile::GPGAvailable()) { qfile = new KGPGFile(fileName); haveAt = false; isEncrypted = true; } else { throw MYMONEYEXCEPTION(QString::fromLatin1("%1").arg(i18n("GPG is not available for decryption of file %1", fileName))); } } else { // we can't use file directly, as we delete qfile later on qfile = new QFile(file.fileName()); } if (!qfile->open(QIODevice::ReadOnly)) { delete qfile; throw MYMONEYEXCEPTION(QString::fromLatin1("Cannot read the file: %1").arg(fileName)); } qbaFileHeader.resize(8); if (qfile->read(qbaFileHeader.data(), 8) != 8) throw MYMONEYEXCEPTION(sFileToShort); if (haveAt) qfile->seek(0); else ungetString(qfile, qbaFileHeader.data(), 8); // Ok, we got the first block of 8 bytes. Read in the two // unsigned long int's by preserving endianess. This is // achieved by reading them through a QDataStream object qint32 magic0, magic1; QDataStream s(&qbaFileHeader, QIODevice::ReadOnly); s >> magic0; s >> magic1; // If both magic numbers match (we actually read in the // text 'KMyMoney' then we assume a binary file and // construct a reader for it. Otherwise, we construct // an XML reader object. // // The expression magic0 < 30 is only used to create // a binary reader if we assume an old binary file. This // should be removed at some point. An alternative is to // check the beginning of the file against an pattern // of the XML file (e.g. '?%1").arg(i18n("File %1 contains the old binary format used by KMyMoney. Please use an older version of KMyMoney (0.8.x) that still supports this format to convert it to the new XML based format.", fileName))); } // Scan the first 70 bytes to see if we find something // we know. For now, we support our own XML format and // GNUCash XML format. If the file is smaller, then it // contains no valid data and we reject it anyway. qbaFileHeader.resize(70); if (qfile->read(qbaFileHeader.data(), 70) != 70) throw MYMONEYEXCEPTION(sFileToShort); if (haveAt) qfile->seek(0); else ungetString(qfile, qbaFileHeader.data(), 70); IMyMoneyOperationsFormat* pReader = nullptr; QRegExp kmyexp(""); QRegExp gncexp("formatName().compare(QLatin1String("GNC")) == 0) { pReader = plugin->reader(); break; } } if (!pReader) { KMessageBox::error(q, i18n("Couldn't find suitable plugin to read your storage.")); return false; } m_fileType = KMyMoneyApp::GncXML; } else { throw MYMONEYEXCEPTION(QString::fromLatin1("%1").arg(i18n("File %1 contains an unknown file format.", fileName))); } // disconnect the current storga manager from the engine MyMoneyFile::instance()->detachStorage(); // create a new empty storage object auto storage = new MyMoneyStorageMgr; // attach the storage before reading the file, since the online // onlineJobAdministration object queries the engine during // loading. MyMoneyFile::instance()->attachStorage(storage); pReader->setProgressCallback(&KMyMoneyApp::progressCallback); pReader->readFile(qfile, storage); pReader->setProgressCallback(0); delete pReader; qfile->close(); delete qfile; // if a temporary file was downloaded, then it will be removed // with the next call. Otherwise, it stays untouched on the local // filesystem. if (downloadedFile) QFile::remove(fileName); // encapsulate transactions to the engine to be able to commit/rollback MyMoneyFileTransaction ft; // make sure we setup the encryption key correctly if (isEncrypted && MyMoneyFile::instance()->value("kmm-encryption-key").isEmpty()) MyMoneyFile::instance()->setValue("kmm-encryption-key", KMyMoneySettings::gpgRecipientList().join(",")); ft.commit(); return true; } /** * This method is called from readFile to open a database file which * is to be processed in 'proper' database mode, i.e. in-place updates * * @param dbaseURL pseudo-QUrl representation of database * * @retval true Database opened successfully * @retval false Could not open or read database */ bool openDatabase(const QUrl &url) { // open the database auto pStorage = MyMoneyFile::instance()->storage(); if (!pStorage) pStorage = new MyMoneyStorageMgr; auto rc = false; auto pluginFound = false; for (const auto& plugin : m_plugins.storage) { if (plugin->formatName().compare(QLatin1String("SQL")) == 0) { rc = plugin->open(pStorage, url); pluginFound = true; break; } } if(!pluginFound) KMessageBox::error(q, i18n("Couldn't find suitable plugin to read your storage.")); if(!rc) { removeStorage(); delete pStorage; return false; } if (pStorage) { MyMoneyFile::instance()->detachStorage(); MyMoneyFile::instance()->attachStorage(pStorage); } return true; } /** * Close the currently opened file and create an empty new file. * * @see MyMoneyFile */ void newFile() { closeFile(); m_fileType = KMyMoneyApp::KmmXML; // assume native type until saved m_fileOpen = true; } /** * Saves the data into permanent storage using the XML format. * * @param url The URL to save into. * If no protocol is specified, file:// is assumed. * @param keyList QString containing a comma separated list of keys * to be used for encryption. If @p keyList is empty, * the file will be saved unencrypted (the default) * * @retval false save operation failed * @retval true save operation was successful */ bool saveFile(const QUrl &url, const QString& keyList = QString()) { QString filename = url.path(); if (!m_fileOpen) { KMessageBox::error(q, i18n("Tried to access a file when it has not been opened")); return false; } emit q->kmmFilePlugin(KMyMoneyApp::preSave); std::unique_ptr storageWriter; // If this file ends in ".ANON.XML" then this should be written using the // anonymous writer. bool plaintext = filename.right(4).toLower() == ".xml"; if (filename.right(9).toLower() == ".anon.xml") storageWriter = std::make_unique(); else storageWriter = std::make_unique(); // actually, url should be the parameter to this function // but for now, this would involve too many changes auto rc = true; try { if (! url.isValid()) { throw MYMONEYEXCEPTION(i18n("Malformed URL '%1'", url.url())); } if (url.scheme() == QLatin1String("sql")) { rc = false; auto pluginFound = false; for (const auto& plugin : m_plugins.storage) { if (plugin->formatName().compare(QLatin1String("SQL")) == 0) { rc = plugin->save(url); pluginFound = true; break; } } if(!pluginFound) throw MYMONEYEXCEPTION(i18n("Couldn't find suitable plugin to save your storage.")); } else if (url.isLocalFile()) { filename = url.toLocalFile(); try { const unsigned int nbak = KMyMoneySettings::autoBackupCopies(); if (nbak) { KBackup::numberedBackupFile(filename, QString(), QStringLiteral("~"), nbak); } saveToLocalFile(filename, storageWriter.get(), plaintext, keyList); } catch (const MyMoneyException &) { throw MYMONEYEXCEPTION(i18n("Unable to write changes to '%1'", filename)); } } else { QTemporaryFile tmpfile; tmpfile.open(); // to obtain the name tmpfile.close(); saveToLocalFile(tmpfile.fileName(), storageWriter.get(), plaintext, keyList); Q_CONSTEXPR int permission = -1; QFile file(tmpfile.fileName()); file.open(QIODevice::ReadOnly); KIO::StoredTransferJob *putjob = KIO::storedPut(file.readAll(), url, permission, KIO::JobFlag::Overwrite); if (!putjob->exec()) { throw MYMONEYEXCEPTION(i18n("Unable to upload to '%1'.
%2", url.toDisplayString(), putjob->errorString())); } file.close(); } m_fileType = KMyMoneyApp::KmmXML; } catch (const MyMoneyException &e) { KMessageBox::error(q, e.what()); MyMoneyFile::instance()->setDirty(); rc = false; } emit q->kmmFilePlugin(postSave); return rc; } /** * This method is used by saveFile() to store the data * either directly in the destination file if it is on * the local file system or in a temporary file when * the final destination is reached over a network * protocol (e.g. FTP) * * @param localFile the name of the local file * @param writer pointer to the formatter * @param plaintext whether to override any compression & encryption settings * @param keyList QString containing a comma separated list of keys to be used for encryption * If @p keyList is empty, the file will be saved unencrypted * * @note This method will close the file when it is written. */ void saveToLocalFile(const QString& localFile, IMyMoneyOperationsFormat* pWriter, bool plaintext, const QString& keyList) { // Check GPG encryption bool encryptFile = true; bool encryptRecover = false; if (!keyList.isEmpty()) { if (!KGPGFile::GPGAvailable()) { KMessageBox::sorry(q, i18n("GPG does not seem to be installed on your system. Please make sure that GPG can be found using the standard search path. This time, encryption is disabled."), i18n("GPG not found")); encryptFile = false; } else { if (KMyMoneySettings::encryptRecover()) { encryptRecover = true; if (!KGPGFile::keyAvailable(QString(recoveryKeyId))) { KMessageBox::sorry(q, i18n("

You have selected to encrypt your data also with the KMyMoney recover key, but the key with id

%1

has not been found in your keyring at this time. Please make sure to import this key into your keyring. You can find it on the KMyMoney web-site. This time your data will not be encrypted with the KMyMoney recover key.

", QString(recoveryKeyId)), i18n("GPG Key not found")); encryptRecover = false; } } for(const QString& key: keyList.split(',', QString::SkipEmptyParts)) { if (!KGPGFile::keyAvailable(key)) { KMessageBox::sorry(q, i18n("

You have specified to encrypt your data for the user-id

%1.

Unfortunately, a valid key for this user-id was not found in your keyring. Please make sure to import a valid key for this user-id. This time, encryption is disabled.

", key), i18n("GPG Key not found")); encryptFile = false; break; } } if (encryptFile == true) { QString msg = i18n("

You have configured to save your data in encrypted form using GPG. Make sure you understand that you might lose all your data if you encrypt it, but cannot decrypt it later on. If unsure, answer No.

"); if (KMessageBox::questionYesNo(q, msg, i18n("Store GPG encrypted"), KStandardGuiItem::yes(), KStandardGuiItem::no(), "StoreEncrypted") == KMessageBox::No) { encryptFile = false; } } } } // Create a temporary file if needed QString writeFile = localFile; QTemporaryFile tmpFile; if (QFile::exists(localFile)) { tmpFile.open(); writeFile = tmpFile.fileName(); tmpFile.close(); } /** * @brief Automatically restore settings when scope is left */ struct restorePreviousSettingsHelper { restorePreviousSettingsHelper() : m_signalsWereBlocked{MyMoneyFile::instance()->signalsBlocked()} { MyMoneyFile::instance()->blockSignals(true); } ~restorePreviousSettingsHelper() { MyMoneyFile::instance()->blockSignals(m_signalsWereBlocked); } const bool m_signalsWereBlocked; } restoreHelper; MyMoneyFileTransaction ft; MyMoneyFile::instance()->deletePair("kmm-encryption-key"); std::unique_ptr device; if (!keyList.isEmpty() && encryptFile && !plaintext) { std::unique_ptr kgpg = std::unique_ptr(new KGPGFile{writeFile}); if (kgpg) { for(const QString& key: keyList.split(',', QString::SkipEmptyParts)) { kgpg->addRecipient(key.toLatin1()); } if (encryptRecover) { kgpg->addRecipient(recoveryKeyId); } MyMoneyFile::instance()->setValue("kmm-encryption-key", keyList); device = std::unique_ptr(kgpg.release()); } } else { QFile *file = new QFile(writeFile); // The second parameter of KCompressionDevice means that KCompressionDevice will delete the QFile object device = std::unique_ptr(new KCompressionDevice{file, true, (plaintext) ? KCompressionDevice::None : COMPRESSION_TYPE}); } ft.commit(); if (!device || !device->open(QIODevice::WriteOnly)) { throw MYMONEYEXCEPTION(i18n("Unable to open file '%1' for writing.", localFile)); } pWriter->setProgressCallback(&KMyMoneyApp::progressCallback); pWriter->writeFile(device.get(), MyMoneyFile::instance()->storage()); device->close(); // Check for errors if possible, only possible for KGPGFile QFileDevice *fileDevice = qobject_cast(device.get()); if (fileDevice && fileDevice->error() != QFileDevice::NoError) { throw MYMONEYEXCEPTION(i18n("Failure while writing to '%1'", localFile)); } if (writeFile != localFile) { // This simple comparison is possible because the strings are equal if no temporary file was created. // If a temporary file was created, it is made in a way that the name is definitely different. So no // symlinks etc. have to be evaluated. if (!QFile::remove(localFile) || !QFile::rename(writeFile, localFile)) throw MYMONEYEXCEPTION(i18n("Failure while writing to '%1'", localFile)); } QFile::setPermissions(localFile, m_fmode); pWriter->setProgressCallback(0); } /** * Call this to see if the MyMoneyFile contains any unsaved data. * * @retval true if any data has been modified but not saved * @retval false otherwise */ bool dirty() { if (!m_fileOpen) return false; return MyMoneyFile::instance()->dirty(); } /* DO NOT ADD code to this function or any of it's called ones. Instead, create a new function, fixFile_n, and modify the initializeStorage() logic above to call it */ void fixFile_3() { // make sure each storage object contains a (unique) id MyMoneyFile::instance()->storageId(); } void fixFile_2() { auto file = MyMoneyFile::instance(); MyMoneyTransactionFilter filter; filter.setReportAllSplits(false); QList transactionList; file->transactionList(transactionList, filter); // scan the transactions and modify transactions with two splits // which reference an account and a category to have the memo text // of the account. auto count = 0; foreach (const auto transaction, transactionList) { if (transaction.splitCount() == 2) { QString accountId; QString categoryId; QString accountMemo; QString categoryMemo; foreach (const auto split, transaction.splits()) { auto acc = file->account(split.accountId()); if (acc.isIncomeExpense()) { categoryId = split.id(); categoryMemo = split.memo(); } else { accountId = split.id(); accountMemo = split.memo(); } } if (!accountId.isEmpty() && !categoryId.isEmpty() && accountMemo != categoryMemo) { MyMoneyTransaction t(transaction); MyMoneySplit s(t.splitById(categoryId)); s.setMemo(accountMemo); t.modifySplit(s); file->modifyTransaction(t); ++count; } } } qDebug("%d transactions fixed in fixFile_2", count); } void fixFile_1() { // we need to fix reports. If the account filter list contains // investment accounts, we need to add the stock accounts to the list // as well if we don't have the expert mode enabled if (!KMyMoneySettings::expertMode()) { try { QList reports = MyMoneyFile::instance()->reportList(); QList::iterator it_r; for (it_r = reports.begin(); it_r != reports.end(); ++it_r) { QStringList list; (*it_r).accounts(list); QStringList missing; QStringList::const_iterator it_a, it_b; for (it_a = list.constBegin(); it_a != list.constEnd(); ++it_a) { auto acc = MyMoneyFile::instance()->account(*it_a); if (acc.accountType() == eMyMoney::Account::Type::Investment) { foreach (const auto accountID, acc.accountList()) { if (!list.contains(accountID)) { missing.append(accountID); } } } } if (!missing.isEmpty()) { (*it_r).addAccount(missing); MyMoneyFile::instance()->modifyReport(*it_r); } } } catch (const MyMoneyException &) { } } } #if 0 if (!m_accountsView->allItemsSelected()) { // retrieve a list of selected accounts QStringList list; m_accountsView->selectedItems(list); // if we're not in expert mode, we need to make sure // that all stock accounts for the selected investment // account are also selected if (!KMyMoneySettings::expertMode()) { QStringList missing; QStringList::const_iterator it_a, it_b; for (it_a = list.begin(); it_a != list.end(); ++it_a) { auto acc = MyMoneyFile::instance()->account(*it_a); if (acc.accountType() == Account::Type::Investment) { foreach (const auto accountID, acc.accountList()) { if (!list.contains(accountID)) { missing.append(accountID); } } } } list += missing; } m_filter.addAccount(list); } #endif void fixFile_0() { /* (Ace) I am on a crusade against file fixups. Whenever we have to fix the * file, it is really a warning. So I'm going to print a debug warning, and * then go track them down when I see them to figure out how they got saved * out needing fixing anyway. */ auto file = MyMoneyFile::instance(); QList accountList; file->accountList(accountList); QList::Iterator it_a; QList scheduleList = file->scheduleList(); QList::Iterator it_s; MyMoneyAccount equity = file->equity(); MyMoneyAccount asset = file->asset(); bool equityListEmpty = equity.accountList().count() == 0; for (it_a = accountList.begin(); it_a != accountList.end(); ++it_a) { if ((*it_a).accountType() == eMyMoney::Account::Type::Loan || (*it_a).accountType() == eMyMoney::Account::Type::AssetLoan) { fixLoanAccount_0(*it_a); } // until early before 0.8 release, the equity account was not saved to // the file. If we have an equity account with no sub-accounts but // find and equity account that has equity() as it's parent, we reparent // this account. Need to move it to asset() first, because otherwise // MyMoneyFile::reparent would act as NOP. if (equityListEmpty && (*it_a).accountType() == eMyMoney::Account::Type::Equity) { if ((*it_a).parentAccountId() == equity.id()) { auto acc = *it_a; // tricky, force parent account to be empty so that we really // can re-parent it acc.setParentAccountId(QString()); file->reparentAccount(acc, equity); qDebug() << Q_FUNC_INFO << " fixed account " << acc.id() << " reparented to " << equity.id(); } } } for (it_s = scheduleList.begin(); it_s != scheduleList.end(); ++it_s) { fixSchedule_0(*it_s); } fixTransactions_0(); } void fixSchedule_0(MyMoneySchedule sched) { MyMoneyTransaction t = sched.transaction(); QList splitList = t.splits(); QList::ConstIterator it_s; bool updated = false; try { // Check if the splits contain valid data and set it to // be valid. for (it_s = splitList.constBegin(); it_s != splitList.constEnd(); ++it_s) { // the first split is always the account on which this transaction operates // and if the transaction commodity is not set, we take this if (it_s == splitList.constBegin() && t.commodity().isEmpty()) { qDebug() << Q_FUNC_INFO << " " << t.id() << " has no commodity"; try { auto acc = MyMoneyFile::instance()->account((*it_s).accountId()); t.setCommodity(acc.currencyId()); updated = true; } catch (const MyMoneyException &) { } } // make sure the account exists. If not, remove the split try { MyMoneyFile::instance()->account((*it_s).accountId()); } catch (const MyMoneyException &) { qDebug() << Q_FUNC_INFO << " " << sched.id() << " " << (*it_s).id() << " removed, because account '" << (*it_s).accountId() << "' does not exist."; t.removeSplit(*it_s); updated = true; } if ((*it_s).reconcileFlag() != eMyMoney::Split::State::NotReconciled) { qDebug() << Q_FUNC_INFO << " " << sched.id() << " " << (*it_s).id() << " should be 'not reconciled'"; MyMoneySplit split = *it_s; split.setReconcileDate(QDate()); split.setReconcileFlag(eMyMoney::Split::State::NotReconciled); t.modifySplit(split); updated = true; } // the schedule logic used to operate only on the value field. // This is now obsolete. if ((*it_s).shares().isZero() && !(*it_s).value().isZero()) { MyMoneySplit split = *it_s; split.setShares(split.value()); t.modifySplit(split); updated = true; } } // If there have been changes, update the schedule and // the engine data. if (updated) { sched.setTransaction(t); MyMoneyFile::instance()->modifySchedule(sched); } } catch (const MyMoneyException &e) { qWarning("Unable to update broken schedule: %s", qPrintable(e.what())); } } void fixLoanAccount_0(MyMoneyAccount acc) { if (acc.value("final-payment").isEmpty() || acc.value("term").isEmpty() || acc.value("periodic-payment").isEmpty() || acc.value("loan-amount").isEmpty() || acc.value("interest-calculation").isEmpty() || acc.value("schedule").isEmpty() || acc.value("fixed-interest").isEmpty()) { KMessageBox::information(q, i18n("

The account \"%1\" was previously created as loan account but some information is missing.

The new loan wizard will be started to collect all relevant information.

Please use KMyMoney version 0.8.7 or later and earlier than version 0.9 to correct the problem.

" , acc.name()), i18n("Account problem")); throw MYMONEYEXCEPTION("Fix LoanAccount0 not supported anymore"); } } void fixTransactions_0() { auto file = MyMoneyFile::instance(); QList scheduleList = file->scheduleList(); MyMoneyTransactionFilter filter; filter.setReportAllSplits(false); QList transactionList; file->transactionList(transactionList, filter); QList::Iterator it_x; QStringList interestAccounts; KMSTATUS(i18n("Fix transactions")); q->slotStatusProgressBar(0, scheduleList.count() + transactionList.count()); int cnt = 0; // scan the schedules to find interest accounts for (it_x = scheduleList.begin(); it_x != scheduleList.end(); ++it_x) { MyMoneyTransaction t = (*it_x).transaction(); QList::ConstIterator it_s; QStringList accounts; bool hasDuplicateAccounts = false; foreach (const auto split, t.splits()) { if (accounts.contains(split.accountId())) { hasDuplicateAccounts = true; qDebug() << Q_FUNC_INFO << " " << t.id() << " has multiple splits with account " << split.accountId(); } else { accounts << split.accountId(); } if (split.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::Interest)) { if (interestAccounts.contains(split.accountId()) == 0) { interestAccounts << split.accountId(); } } } if (hasDuplicateAccounts) { fixDuplicateAccounts_0(t); } ++cnt; if (!(cnt % 10)) q->slotStatusProgressBar(cnt); } // scan the transactions and modify loan transactions for (auto& transaction : transactionList) { QString defaultAction; QList splits = transaction.splits(); QStringList accounts; // check if base commodity is set. if not, set baseCurrency if (transaction.commodity().isEmpty()) { qDebug() << Q_FUNC_INFO << " " << transaction.id() << " has no base currency"; transaction.setCommodity(file->baseCurrency().id()); file->modifyTransaction(transaction); } bool isLoan = false; // Determine default action if (transaction.splitCount() == 2) { // check for transfer int accountCount = 0; MyMoneyMoney val; foreach (const auto split, splits) { auto acc = file->account(split.accountId()); if (acc.accountGroup() == eMyMoney::Account::Type::Asset || acc.accountGroup() == eMyMoney::Account::Type::Liability) { val = split.value(); accountCount++; if (acc.accountType() == eMyMoney::Account::Type::Loan || acc.accountType() == eMyMoney::Account::Type::AssetLoan) isLoan = true; } else break; } if (accountCount == 2) { if (isLoan) defaultAction = MyMoneySplit::actionName(eMyMoney::Split::Action::Amortization); else defaultAction = MyMoneySplit::actionName(eMyMoney::Split::Action::Transfer); } else { if (val.isNegative()) defaultAction = MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal); else defaultAction = MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit); } } isLoan = false; foreach (const auto split, splits) { auto acc = file->account(split.accountId()); MyMoneyMoney val = split.value(); if (acc.accountGroup() == eMyMoney::Account::Type::Asset || acc.accountGroup() == eMyMoney::Account::Type::Liability) { if (!val.isPositive()) { defaultAction = MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal); break; } else { defaultAction = MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit); break; } } } #if 0 // Check for correct actions in transactions referencing credit cards bool needModify = false; // The action fields are actually not used anymore in the ledger view logic // so we might as well skip this whole thing here! for (it_s = splits.begin(); needModify == false && it_s != splits.end(); ++it_s) { auto acc = file->account((*it_s).accountId()); MyMoneyMoney val = (*it_s).value(); if (acc.accountType() == Account::Type::CreditCard) { if (val < 0 && (*it_s).action() != MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal) && (*it_s).action() != MyMoneySplit::actionName(eMyMoney::Split::Action::Transfer)) needModify = true; if (val >= 0 && (*it_s).action() != MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit) && (*it_s).action() != MyMoneySplit::actionName(eMyMoney::Split::Action::Transfer)) needModify = true; } } // (Ace) Extended the #endif down to cover this conditional, because as-written // it will ALWAYS be skipped. if (needModify == true) { for (it_s = splits.begin(); it_s != splits.end(); ++it_s) { (*it_s).setAction(defaultAction); transaction.modifySplit(*it_s); file->modifyTransaction(transaction); } splits = transaction.splits(); // update local copy qDebug("Fixed credit card assignment in %s", transaction.id().data()); } #endif // Check for correct assignment of ActionInterest in all splits // and check if there are any duplicates in this transactions for (auto& split : splits) { MyMoneyAccount splitAccount = file->account(split.accountId()); if (!accounts.contains(split.accountId())) { accounts << split.accountId(); } // if this split references an interest account, the action // must be of type ActionInterest if (interestAccounts.contains(split.accountId())) { if (split.action() != MyMoneySplit::actionName(eMyMoney::Split::Action::Interest)) { qDebug() << Q_FUNC_INFO << " " << transaction.id() << " contains an interest account (" << split.accountId() << ") but does not have ActionInterest"; split.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::Interest)); transaction.modifySplit(split); file->modifyTransaction(transaction); qDebug("Fixed interest action in %s", qPrintable(transaction.id())); } // if it does not reference an interest account, it must not be // of type ActionInterest } else { if (split.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::Interest)) { qDebug() << Q_FUNC_INFO << " " << transaction.id() << " does not contain an interest account so it should not have ActionInterest"; split.setAction(defaultAction); transaction.modifySplit(split); file->modifyTransaction(transaction); qDebug("Fixed interest action in %s", qPrintable(transaction.id())); } } // check that for splits referencing an account that has // the same currency as the transactions commodity the value // and shares field are the same. if (transaction.commodity() == splitAccount.currencyId() && split.value() != split.shares()) { qDebug() << Q_FUNC_INFO << " " << transaction.id() << " " << split.id() << " uses the transaction currency, but shares != value"; split.setShares(split.value()); transaction.modifySplit(split); file->modifyTransaction(transaction); } // fix the shares and values to have the correct fraction if (!splitAccount.isInvest()) { try { int fract = splitAccount.fraction(); if (split.shares() != split.shares().convert(fract)) { qDebug("adjusting fraction in %s,%s", qPrintable(transaction.id()), qPrintable(split.id())); split.setShares(split.shares().convert(fract)); split.setValue(split.value().convert(fract)); transaction.modifySplit(split); file->modifyTransaction(transaction); } } catch (const MyMoneyException &) { qDebug("Missing security '%s', split not altered", qPrintable(splitAccount.currencyId())); } } } ++cnt; if (!(cnt % 10)) q->slotStatusProgressBar(cnt); } q->slotStatusProgressBar(-1, -1); } void fixDuplicateAccounts_0(MyMoneyTransaction& t) { qDebug("Duplicate account in transaction %s", qPrintable(t.id())); } }; KMyMoneyApp::KMyMoneyApp(QWidget* parent) : KXmlGuiWindow(parent), d(new Private(this)) { #ifdef KMM_DBUS new KmymoneyAdaptor(this); QDBusConnection::sessionBus().registerObject("/KMymoney", this); QDBusConnection::sessionBus().interface()->registerService( "org.kde.kmymoney-" + QString::number(platformTools::processId()), QDBusConnectionInterface::DontQueueService); #endif // Register the main engine types used as meta-objects qRegisterMetaType("MyMoneyMoney"); qRegisterMetaType("MyMoneySecurity"); // preset the pointer because we need it during the course of this constructor kmymoney = this; d->m_config = KSharedConfig::openConfig(); d->setThemedCSS(); MyMoneyTransactionFilter::setFiscalYearStart(KMyMoneySettings::firstFiscalMonth(), KMyMoneySettings::firstFiscalDay()); updateCaption(true); QFrame* frame = new QFrame; frame->setFrameStyle(QFrame::NoFrame); // values for margin (11) and spacing(6) taken from KDialog implementation QBoxLayout* layout = new QBoxLayout(QBoxLayout::TopToBottom, frame); layout->setContentsMargins(2, 2, 2, 2); layout->setSpacing(6); { #if defined(Q_OS_WIN) || defined(Q_OS_MACOS) QString themeName = QLatin1Literal("system"); // using QIcon::setThemeName on Craft build system causes icons to disappear #else QString themeName = KMyMoneySettings::iconsTheme(); // get theme user wants #endif if (!themeName.isEmpty() && themeName != QLatin1Literal("system")) // if it isn't default theme then set it QIcon::setThemeName(themeName); Icons::setIconThemeNames(QIcon::themeName()); // get whatever theme user ends up with and hope our icon names will fit that theme } initStatusBar(); pActions = initActions(); pMenus = initMenus(); d->newStorage(); d->m_myMoneyView = new KMyMoneyView; layout->addWidget(d->m_myMoneyView, 10); connect(d->m_myMoneyView, &KMyMoneyView::aboutToChangeView, this, &KMyMoneyApp::slotResetSelections); connect(d->m_myMoneyView, SIGNAL(currentPageChanged(KPageWidgetItem*,KPageWidgetItem*)), this, SLOT(slotUpdateActions())); connect(d->m_myMoneyView, &KMyMoneyView::statusMsg, this, &KMyMoneyApp::slotStatusMsg); connect(d->m_myMoneyView, &KMyMoneyView::statusProgress, this, &KMyMoneyApp::slotStatusProgressBar); connect(this, &KMyMoneyApp::fileLoaded, d->m_myMoneyView, &KMyMoneyView::slotRefreshViews); // Initialize kactivities resource instance #ifdef KF5Activities_FOUND d->m_activityResourceInstance = new KActivities::ResourceInstance(window()->winId(), this); connect(this, &KMyMoneyApp::fileLoaded, d->m_activityResourceInstance, &KActivities::ResourceInstance::setUri); #endif const auto viewActions = d->m_myMoneyView->actionsToBeConnected(); actionCollection()->addActions(viewActions.values()); for (auto it = viewActions.cbegin(); it != viewActions.cend(); ++it) pActions.insert(it.key(), it.value()); /////////////////////////////////////////////////////////////////// // call inits to invoke all other construction parts readOptions(); // now initialize the plugin structure createInterfaces(); KMyMoneyPlugin::pluginHandling(KMyMoneyPlugin::Action::Load, d->m_plugins, this, guiFactory()); onlineJobAdministration::instance()->setOnlinePlugins(d->m_plugins.extended); d->m_myMoneyView->setOnlinePlugins(d->m_plugins.online); d->m_myMoneyView->setStoragePlugins(d->m_plugins.storage); setCentralWidget(frame); connect(&d->m_proc, SIGNAL(finished(int,QProcess::ExitStatus)), this, SLOT(slotBackupHandleEvents())); // force to show the home page if the file is closed connect(pActions[Action::ViewTransactionDetail], &QAction::toggled, d->m_myMoneyView, &KMyMoneyView::slotShowTransactionDetail); d->m_backupState = BACKUP_IDLE; QLocale locale; for (auto const& weekDay: locale.weekdays()) { d->m_processingDays.setBit(static_cast(weekDay)); } d->m_autoSaveTimer = new QTimer(this); d->m_progressTimer = new QTimer(this); connect(d->m_autoSaveTimer, SIGNAL(timeout()), this, SLOT(slotAutoSave())); connect(d->m_progressTimer, SIGNAL(timeout()), this, SLOT(slotStatusProgressDone())); // make sure, we get a note when the engine changes state connect(MyMoneyFile::instance(), SIGNAL(dataChanged()), this, SLOT(slotDataChanged())); // connect the WebConnect server connect(d->m_webConnect, SIGNAL(gotUrl(QUrl)), this, SLOT(webConnect(QUrl))); // make sure we have a balance warning object d->m_balanceWarning = new KBalanceWarning(this); // setup the initial configuration slotUpdateConfiguration(QString()); // kickstart date change timer slotDateChanged(); connect(this, SIGNAL(fileLoaded(QUrl)), onlineJobAdministration::instance(), SLOT(updateOnlineTaskProperties())); } KMyMoneyApp::~KMyMoneyApp() { d->removeStorage(); // delete cached objects since they are in the way // when unloading the plugins onlineJobAdministration::instance()->clearCaches(); // we need to unload all plugins before we destroy anything else KMyMoneyPlugin::pluginHandling(KMyMoneyPlugin::Action::Unload, d->m_plugins, this, guiFactory()); #ifdef KF5Holidays_FOUND delete d->m_holidayRegion; #endif #ifdef KF5Activities_FOUND delete d->m_activityResourceInstance; #endif delete d; } QUrl KMyMoneyApp::lastOpenedURL() { QUrl url = d->m_startDialog ? QUrl() : d->m_fileName; if (!url.isValid()) { url = QUrl::fromUserInput(readLastUsedFile()); } ready(); return url; } void KMyMoneyApp::slotInstallConsistencyCheckContextMenu() { // this code relies on the implementation of KMessageBox::informationList to add a context menu to that list, // please adjust it if it's necessary or rewrite the way the consistency check results are displayed if (QWidget* dialog = QApplication::activeModalWidget()) { if (QListWidget* widget = dialog->findChild()) { // give the user a hint that the data can be saved widget->setToolTip(i18n("This is the consistency check log, use the context menu to copy or save it.")); widget->setWhatsThis(widget->toolTip()); widget->setContextMenuPolicy(Qt::CustomContextMenu); connect(widget, SIGNAL(customContextMenuRequested(QPoint)), SLOT(slotShowContextMenuForConsistencyCheck(QPoint))); } } } void KMyMoneyApp::slotShowContextMenuForConsistencyCheck(const QPoint &pos) { // allow the user to save the consistency check results if (QWidget* widget = qobject_cast< QWidget* >(sender())) { QMenu contextMenu(widget); QAction* copy = new QAction(i18n("Copy to clipboard"), widget); QAction* save = new QAction(i18n("Save to file"), widget); contextMenu.addAction(copy); contextMenu.addAction(save); QAction *result = contextMenu.exec(widget->mapToGlobal(pos)); if (result == copy) { // copy the consistency check results to the clipboard d->copyConsistencyCheckResults(); } else if (result == save) { // save the consistency check results to a file d->saveConsistencyCheckResults(); } } } QHash KMyMoneyApp::initMenus() { QHash lutMenus; const QHash menuNames { {Menu::Institution, QStringLiteral("institution_context_menu")}, {Menu::Account, QStringLiteral("account_context_menu")}, {Menu::Schedule, QStringLiteral("schedule_context_menu")}, {Menu::Category, QStringLiteral("category_context_menu")}, {Menu::Tag, QStringLiteral("tag_context_menu")}, {Menu::Payee, QStringLiteral("payee_context_menu")}, {Menu::Investment, QStringLiteral("investment_context_menu")}, {Menu::Transaction, QStringLiteral("transaction_context_menu")}, {Menu::MoveTransaction, QStringLiteral("transaction_move_menu")}, {Menu::MarkTransaction, QStringLiteral("transaction_mark_menu")}, {Menu::MarkTransactionContext, QStringLiteral("transaction_context_mark_menu")}, {Menu::OnlineJob, QStringLiteral("onlinejob_context_menu")} }; for (auto it = menuNames.cbegin(); it != menuNames.cend(); ++it) lutMenus.insert(it.key(), qobject_cast(factory()->container(it.value(), this))); return lutMenus; } QHash KMyMoneyApp::initActions() { auto aC = actionCollection(); /* Look-up table for all custom and standard actions. It's required for: 1) building QList with QActions to be added to ActionCollection 2) adding custom features to QActions like e.g. keyboard shortcut */ QHash lutActions; // ************* // Adding standard actions // ************* KStandardAction::openNew(this, &KMyMoneyApp::slotFileNew, aC); KStandardAction::open(this, &KMyMoneyApp::slotFileOpen, aC); d->m_recentFiles = KStandardAction::openRecent(this, &KMyMoneyApp::slotFileOpenRecent, aC); KStandardAction::save(this, &KMyMoneyApp::slotFileSave, aC); KStandardAction::saveAs(this, &KMyMoneyApp::slotFileSaveAs, aC); KStandardAction::close(this, &KMyMoneyApp::slotFileClose, aC); KStandardAction::quit(this, &KMyMoneyApp::slotFileQuit, aC); lutActions.insert(Action::Print, KStandardAction::print(this, &KMyMoneyApp::slotPrintView, aC)); KStandardAction::preferences(this, &KMyMoneyApp::slotSettings, aC); // ************* // Adding all actions // ************* { // struct for creating useless (unconnected) QAction struct actionInfo { Action action; QString name; QString text; Icon icon; }; const QVector actionInfos { // ************* // The File menu // ************* {Action::FileBackup, QStringLiteral("file_backup"), i18n("Backup..."), Icon::Empty}, {Action::FileImportStatement, QStringLiteral("file_import_statement"), i18n("Statement file..."), Icon::Empty}, {Action::FileImportTemplate, QStringLiteral("file_import_template"), i18n("Account Template..."), Icon::Empty}, {Action::FileExportTemplate, QStringLiteral("file_export_template"), i18n("Account Template..."), Icon::Empty}, {Action::FilePersonalData, QStringLiteral("view_personal_data"), i18n("Personal Data..."), Icon::UserProperties}, #ifdef KMM_DEBUG {Action::FileDump, QStringLiteral("file_dump"), i18n("Dump Memory"), Icon::Empty}, #endif {Action::FileInformation, QStringLiteral("view_file_info"), i18n("File-Information..."), Icon::DocumentProperties}, // ************* // The Edit menu // ************* {Action::EditFindTransaction, QStringLiteral("edit_find_transaction"), i18n("Find transaction..."), Icon::EditFindTransaction}, // ************* // The View menu // ************* {Action::ViewTransactionDetail, QStringLiteral("view_show_transaction_detail"), i18n("Show Transaction Detail"), Icon::ViewTransactionDetail}, {Action::ViewHideReconciled, QStringLiteral("view_hide_reconciled_transactions"), i18n("Hide reconciled transactions"), Icon::HideReconciled}, {Action::ViewHideCategories, QStringLiteral("view_hide_unused_categories"), i18n("Hide unused categories"), Icon::HideCategories}, {Action::ViewShowAll, QStringLiteral("view_show_all_accounts"), i18n("Show all accounts"), Icon::Empty}, // ********************* // The institutions menu // ********************* {Action::NewInstitution, QStringLiteral("institution_new"), i18n("New institution..."), Icon::InstitutionNew}, {Action::EditInstitution, QStringLiteral("institution_edit"), i18n("Edit institution..."), Icon::InstitutionEdit}, {Action::DeleteInstitution, QStringLiteral("institution_delete"), i18n("Delete institution..."), Icon::InstitutionDelete}, // ***************** // The accounts menu // ***************** {Action::NewAccount, QStringLiteral("account_new"), i18n("New account..."), Icon::AccountNew}, {Action::OpenAccount, QStringLiteral("account_open"), i18n("Open ledger"), Icon::ViewFinancialList}, {Action::StartReconciliation, QStringLiteral("account_reconcile"), i18n("Reconcile..."), Icon::Reconcile}, {Action::FinishReconciliation, QStringLiteral("account_reconcile_finish"), i18nc("Finish reconciliation", "Finish"), Icon::AccountFinishReconciliation}, {Action::PostponeReconciliation, QStringLiteral("account_reconcile_postpone"), i18n("Postpone reconciliation"), Icon::MediaPlaybackPause}, {Action::EditAccount, QStringLiteral("account_edit"), i18n("Edit account..."), Icon::AccountEdit}, {Action::DeleteAccount, QStringLiteral("account_delete"), i18n("Delete account..."), Icon::AccountDelete}, {Action::CloseAccount, QStringLiteral("account_close"), i18n("Close account"), Icon::AccountClose}, {Action::ReopenAccount, QStringLiteral("account_reopen"), i18n("Reopen account"), Icon::AccountReopen}, {Action::ReportAccountTransactions, QStringLiteral("account_transaction_report"), i18n("Transaction report"), Icon::ViewFinancialList}, {Action::ChartAccountBalance, QStringLiteral("account_chart"), i18n("Show balance chart..."), Icon::OfficeChartLine}, {Action::MapOnlineAccount, QStringLiteral("account_online_map"), i18n("Map account..."), Icon::NewsSubscribe}, {Action::UnmapOnlineAccount, QStringLiteral("account_online_unmap"), i18n("Unmap account..."), Icon::NewsUnsubscribe}, {Action::UpdateAccount, QStringLiteral("account_online_update"), i18n("Update account..."), Icon::AccountUpdate}, {Action::UpdateAllAccounts, QStringLiteral("account_online_update_all"), i18n("Update all accounts..."), Icon::AccountUpdateAll}, {Action::AccountCreditTransfer, QStringLiteral("account_online_new_credit_transfer"), i18n("New credit transfer"), Icon::AccountCreditTransfer}, // ******************* // The categories menu // ******************* {Action::NewCategory, QStringLiteral("category_new"), i18n("New category..."), Icon::CategoryNew}, {Action::EditCategory, QStringLiteral("category_edit"), i18n("Edit category..."), Icon::CategoryEdit}, {Action::DeleteCategory, QStringLiteral("category_delete"), i18n("Delete category..."), Icon::CategoryDelete}, // ************** // The tools menu // ************** {Action::ToolCurrencies, QStringLiteral("tools_currency_editor"), i18n("Currencies..."), Icon::ViewCurrencyList}, {Action::ToolPrices, QStringLiteral("tools_price_editor"), i18n("Prices..."), Icon::Empty}, {Action::ToolUpdatePrices, QStringLiteral("tools_update_prices"), i18n("Update Stock and Currency Prices..."), Icon::ToolUpdatePrices}, {Action::ToolConsistency, QStringLiteral("tools_consistency_check"), i18n("Consistency Check"), Icon::Empty}, {Action::ToolPerformance, QStringLiteral("tools_performancetest"), i18n("Performance-Test"), Icon::Fork}, {Action::ToolCalculator, QStringLiteral("tools_kcalc"), i18n("Calculator..."), Icon::AccessoriesCalculator}, // ***************** // The settings menu // ***************** {Action::SettingsAllMessages, QStringLiteral("settings_enable_messages"), i18n("Enable all messages"), Icon::Empty}, // ************* // The help menu // ************* {Action::HelpShow, QStringLiteral("help_show_tip"), i18n("&Show tip of the day"), Icon::Tip}, // *************************** // Actions w/o main menu entry // *************************** {Action::NewTransaction, QStringLiteral("transaction_new"), i18nc("New transaction button", "New"), Icon::TransactionNew}, {Action::EditTransaction, QStringLiteral("transaction_edit"), i18nc("Edit transaction button", "Edit"), Icon::TransactionEdit}, {Action::EnterTransaction, QStringLiteral("transaction_enter"), i18nc("Enter transaction", "Enter"), Icon::DialogOK}, {Action::EditSplits, QStringLiteral("transaction_editsplits"), i18nc("Edit split button", "Edit splits"), Icon::Split}, {Action::CancelTransaction, QStringLiteral("transaction_cancel"), i18nc("Cancel transaction edit", "Cancel"), Icon::DialogCancel}, {Action::DeleteTransaction, QStringLiteral("transaction_delete"), i18nc("Delete transaction", "Delete"), Icon::EditDelete}, {Action::DuplicateTransaction, QStringLiteral("transaction_duplicate"), i18nc("Duplicate transaction", "Duplicate"), Icon::EditCopy}, {Action::MatchTransaction, QStringLiteral("transaction_match"), i18nc("Button text for match transaction", "Match"),Icon::TransactionMatch}, {Action::AcceptTransaction, QStringLiteral("transaction_accept"), i18nc("Accept 'imported' and 'matched' transaction", "Accept"), Icon::TransactionAccept}, {Action::ToggleReconciliationFlag, QStringLiteral("transaction_mark_toggle"), i18nc("Toggle reconciliation flag", "Toggle"), Icon::Empty}, {Action::MarkCleared, QStringLiteral("transaction_mark_cleared"), i18nc("Mark transaction cleared", "Cleared"), Icon::Empty}, {Action::MarkReconciled, QStringLiteral("transaction_mark_reconciled"), i18nc("Mark transaction reconciled", "Reconciled"), Icon::Empty}, {Action::MarkNotReconciled, QStringLiteral("transaction_mark_notreconciled"), i18nc("Mark transaction not reconciled", "Not reconciled"), Icon::Empty}, {Action::SelectAllTransactions, QStringLiteral("transaction_select_all"), i18nc("Select all transactions", "Select all"), Icon::Empty}, {Action::GoToAccount, QStringLiteral("transaction_goto_account"), i18n("Go to account"), Icon::GoJump}, {Action::GoToPayee, QStringLiteral("transaction_goto_payee"), i18n("Go to payee"), Icon::GoJump}, {Action::NewScheduledTransaction, QStringLiteral("transaction_create_schedule"), i18n("Create scheduled transaction..."), Icon::AppointmentNew}, {Action::AssignTransactionsNumber, QStringLiteral("transaction_assign_number"), i18n("Assign next number"), Icon::Empty}, {Action::CombineTransactions, QStringLiteral("transaction_combine"), i18nc("Combine transactions", "Combine"), Icon::Empty}, {Action::CopySplits, QStringLiteral("transaction_copy_splits"), i18n("Copy splits"), Icon::Empty}, //Investment {Action::NewInvestment, QStringLiteral("investment_new"), i18n("New investment..."), Icon::InvestmentNew}, {Action::EditInvestment, QStringLiteral("investment_edit"), i18n("Edit investment..."), Icon::InvestmentEdit}, {Action::DeleteInvestment, QStringLiteral("investment_delete"), i18n("Delete investment..."), Icon::InvestmentDelete}, {Action::UpdatePriceOnline, QStringLiteral("investment_online_price_update"), i18n("Online price update..."), Icon::InvestmentOnlinePrice}, {Action::UpdatePriceManually, QStringLiteral("investment_manual_price_update"), i18n("Manual price update..."), Icon::Empty}, //Schedule {Action::NewSchedule, QStringLiteral("schedule_new"), i18n("New scheduled transaction"), Icon::AppointmentNew}, {Action::EditSchedule, QStringLiteral("schedule_edit"), i18n("Edit scheduled transaction"), Icon::DocumentEdit}, {Action::DeleteSchedule, QStringLiteral("schedule_delete"), i18n("Delete scheduled transaction"), Icon::EditDelete}, {Action::DuplicateSchedule, QStringLiteral("schedule_duplicate"), i18n("Duplicate scheduled transaction"), Icon::EditCopy}, {Action::EnterSchedule, QStringLiteral("schedule_enter"), i18n("Enter next transaction..."), Icon::KeyEnter}, {Action::SkipSchedule, QStringLiteral("schedule_skip"), i18n("Skip next transaction..."), Icon::MediaSeekForward}, //Payees {Action::NewPayee, QStringLiteral("payee_new"), i18n("New payee"), Icon::ListAddUser}, {Action::RenamePayee, QStringLiteral("payee_rename"), i18n("Rename payee"), Icon::PayeeRename}, {Action::DeletePayee, QStringLiteral("payee_delete"), i18n("Delete payee"), Icon::ListRemoveUser}, {Action::MergePayee, QStringLiteral("payee_merge"), i18n("Merge payees"), Icon::PayeeMerge}, //Tags {Action::NewTag, QStringLiteral("tag_new"), i18n("New tag"), Icon::ListAddTag}, {Action::RenameTag, QStringLiteral("tag_rename"), i18n("Rename tag"), Icon::TagRename}, {Action::DeleteTag, QStringLiteral("tag_delete"), i18n("Delete tag"), Icon::ListRemoveTag}, //debug actions #ifdef KMM_DEBUG {Action::WizardNewUser, QStringLiteral("new_user_wizard"), i18n("Test new feature"), Icon::Empty}, {Action::DebugTraces, QStringLiteral("debug_traces"), i18n("Debug Traces"), Icon::Empty}, #endif {Action::DebugTimers, QStringLiteral("debug_timers"), i18n("Debug Timers"), Icon::Empty}, // onlineJob actions {Action::DeleteOnlineJob, QStringLiteral("onlinejob_delete"), i18n("Remove credit transfer"), Icon::EditDelete}, {Action::EditOnlineJob, QStringLiteral("onlinejob_edit"), i18n("Edit credit transfer"), Icon::DocumentEdit}, {Action::LogOnlineJob, QStringLiteral("onlinejob_log"), i18n("Show log"), Icon::Empty}, }; for (const auto& info : actionInfos) { auto a = new QAction(this); // KActionCollection::addAction by name sets object name anyways, // so, as better alternative, set it here right from the start a->setObjectName(info.name); a->setText(info.text); if (info.icon != Icon::Empty) // no need to set empty icon a->setIcon(Icons::get(info.icon)); a->setEnabled(false); lutActions.insert(info.action, a); // store QAction's pointer for later processing } } { // List with slots that get connected here. Other slots get connected in e.g. appropriate views typedef void(KMyMoneyApp::*KMyMoneyAppFunc)(); const QHash actionConnections { // ************* // The File menu // ************* // {Action::FileOpenDatabase, &KMyMoneyApp::slotOpenDatabase}, // {Action::FileSaveAsDatabase, &KMyMoneyApp::slotSaveAsDatabase}, {Action::FileBackup, &KMyMoneyApp::slotBackupFile}, {Action::FileImportTemplate, &KMyMoneyApp::slotLoadAccountTemplates}, {Action::FileExportTemplate, &KMyMoneyApp::slotSaveAccountTemplates}, {Action::FilePersonalData, &KMyMoneyApp::slotFileViewPersonal}, #ifdef KMM_DEBUG {Action::FileDump, &KMyMoneyApp::slotFileFileInfo}, #endif {Action::FileInformation, &KMyMoneyApp::slotFileInfoDialog}, // ************* // The View menu // ************* {Action::ViewTransactionDetail, &KMyMoneyApp::slotShowTransactionDetail}, {Action::ViewHideReconciled, &KMyMoneyApp::slotHideReconciledTransactions}, {Action::ViewHideCategories, &KMyMoneyApp::slotHideUnusedCategories}, {Action::ViewShowAll, &KMyMoneyApp::slotShowAllAccounts}, // ************** // The tools menu // ************** {Action::ToolCurrencies, &KMyMoneyApp::slotCurrencyDialog}, {Action::ToolPrices, &KMyMoneyApp::slotPriceDialog}, {Action::ToolUpdatePrices, &KMyMoneyApp::slotEquityPriceUpdate}, {Action::ToolConsistency, &KMyMoneyApp::slotFileConsistencyCheck}, {Action::ToolPerformance, &KMyMoneyApp::slotPerformanceTest}, // {Action::ToolSQL, &KMyMoneyApp::slotGenerateSql}, {Action::ToolCalculator, &KMyMoneyApp::slotToolsStartKCalc}, // ***************** // The settings menu // ***************** {Action::SettingsAllMessages, &KMyMoneyApp::slotEnableMessages}, // ************* // The help menu // ************* {Action::HelpShow, &KMyMoneyApp::slotShowTipOfTheDay}, // *************************** // Actions w/o main menu entry // *************************** //debug actions #ifdef KMM_DEBUG {Action::WizardNewUser, &KMyMoneyApp::slotNewFeature}, {Action::DebugTraces, &KMyMoneyApp::slotToggleTraces}, #endif {Action::DebugTimers, &KMyMoneyApp::slotToggleTimers}, }; for (auto connection = actionConnections.cbegin(); connection != actionConnections.cend(); ++connection) connect(lutActions[connection.key()], &QAction::triggered, this, connection.value()); } // ************* // Setting some of added actions checkable // ************* { // Some actions are checkable, // so set them here const QVector checkableActions { Action::ViewTransactionDetail, Action::ViewHideReconciled, Action::ViewHideCategories, #ifdef KMM_DEBUG Action::DebugTraces, Action::DebugTimers, #endif Action::ViewShowAll }; for (const auto& it : checkableActions) { lutActions[it]->setCheckable(true); lutActions[it]->setEnabled(true); } } // ************* // Setting actions that are always enabled // ************* { const QVector alwaysEnabled { Action::HelpShow, Action::SettingsAllMessages, Action::ToolPerformance, Action::ToolCalculator }; for (const auto& action : alwaysEnabled) { lutActions[action]->setEnabled(true); } } // ************* // Setting keyboard shortcuts for some of added actions // ************* { const QVector> actionShortcuts { {qMakePair(Action::EditFindTransaction, Qt::CTRL + Qt::Key_F)}, {qMakePair(Action::ViewTransactionDetail, Qt::CTRL + Qt::Key_T)}, {qMakePair(Action::ViewHideReconciled, Qt::CTRL + Qt::Key_R)}, {qMakePair(Action::ViewHideCategories, Qt::CTRL + Qt::Key_U)}, {qMakePair(Action::ViewShowAll, Qt::CTRL + Qt::SHIFT + Qt::Key_A)}, {qMakePair(Action::StartReconciliation, Qt::CTRL + Qt::SHIFT + Qt::Key_R)}, {qMakePair(Action::NewTransaction, Qt::CTRL + Qt::Key_Insert)}, {qMakePair(Action::ToggleReconciliationFlag, Qt::CTRL + Qt::Key_Space)}, {qMakePair(Action::MarkCleared, Qt::CTRL + Qt::ALT+ Qt::Key_Space)}, {qMakePair(Action::MarkReconciled, Qt::CTRL + Qt::SHIFT + Qt::Key_Space)}, {qMakePair(Action::SelectAllTransactions, Qt::CTRL + Qt::Key_A)}, #ifdef KMM_DEBUG {qMakePair(Action::WizardNewUser, Qt::CTRL + Qt::Key_G)}, #endif {qMakePair(Action::AssignTransactionsNumber, Qt::CTRL + Qt::SHIFT + Qt::Key_N)} }; for(const auto& it : actionShortcuts) aC->setDefaultShortcut(lutActions[it.first], it.second); } // ************* // Misc settings // ************* connect(onlineJobAdministration::instance(), &onlineJobAdministration::canSendCreditTransferChanged, lutActions.value(Action::AccountCreditTransfer), &QAction::setEnabled); // Setup transaction detail switch lutActions[Action::ViewTransactionDetail]->setChecked(KMyMoneySettings::showRegisterDetailed()); lutActions[Action::ViewHideReconciled]->setChecked(KMyMoneySettings::hideReconciledTransactions()); lutActions[Action::ViewHideCategories]->setChecked(KMyMoneySettings::hideUnusedCategory()); lutActions[Action::ViewShowAll]->setChecked(false); // ************* // Adding actions to ActionCollection // ************* actionCollection()->addActions(lutActions.values()); // ************************ // Currently unused actions // ************************ #if 0 new KToolBarPopupAction(i18n("View back"), "go-previous", 0, this, SLOT(slotShowPreviousView()), actionCollection(), "go_back"); new KToolBarPopupAction(i18n("View forward"), "go-next", 0, this, SLOT(slotShowNextView()), actionCollection(), "go_forward"); action("go_back")->setEnabled(false); action("go_forward")->setEnabled(false); #endif // use the absolute path to your kmymoneyui.rc file for testing purpose in createGUI(); setupGUI(); // reconnect about app entry to dialog with full credits information auto aboutApp = aC->action(QString::fromLatin1(KStandardAction::name(KStandardAction::AboutApp))); aboutApp->disconnect(); connect(aboutApp, &QAction::triggered, this, &KMyMoneyApp::slotShowCredits); QMenu *menuContainer; menuContainer = static_cast(factory()->container(QStringLiteral("import"), this)); menuContainer->setIcon(Icons::get(Icon::DocumentImport)); menuContainer = static_cast(factory()->container(QStringLiteral("export"), this)); menuContainer->setIcon(Icons::get(Icon::DocumentExport)); return lutActions; } #ifdef KMM_DEBUG void KMyMoneyApp::dumpActions() const { const QList list = actionCollection()->actions(); foreach (const auto it, list) std::cout << qPrintable(it->objectName()) << ": " << qPrintable(it->text()) << std::endl; } #endif bool KMyMoneyApp::isActionToggled(const Action _a) { return pActions[_a]->isChecked(); } void KMyMoneyApp::initStatusBar() { /////////////////////////////////////////////////////////////////// // STATUSBAR d->m_statusLabel = new QLabel; statusBar()->addWidget(d->m_statusLabel); ready(); // Initialization of progress bar taken from KDevelop ;-) d->m_progressBar = new QProgressBar; statusBar()->addWidget(d->m_progressBar); d->m_progressBar->setFixedHeight(d->m_progressBar->sizeHint().height() - 8); // hide the progress bar for now slotStatusProgressBar(-1, -1); } void KMyMoneyApp::saveOptions() { KConfigGroup grp = d->m_config->group("General Options"); grp.writeEntry("Geometry", size()); grp.writeEntry("Show Statusbar", actionCollection()->action(KStandardAction::name(KStandardAction::ShowStatusbar))->isChecked()); KConfigGroup toolbarGrp = d->m_config->group("mainToolBar"); toolBar("mainToolBar")->saveSettings(toolbarGrp); d->m_recentFiles->saveEntries(d->m_config->group("Recent Files")); } void KMyMoneyApp::readOptions() { KConfigGroup grp = d->m_config->group("General Options"); pActions[Action::ViewHideReconciled]->setChecked(KMyMoneySettings::hideReconciledTransactions()); pActions[Action::ViewHideCategories]->setChecked(KMyMoneySettings::hideUnusedCategory()); d->m_recentFiles->loadEntries(d->m_config->group("Recent Files")); // Startdialog is written in the settings dialog d->m_startDialog = grp.readEntry("StartDialog", true); } void KMyMoneyApp::resizeEvent(QResizeEvent* ev) { KMainWindow::resizeEvent(ev); updateCaption(true); } int KMyMoneyApp::askSaveOnClose() { int ans; if (KMyMoneySettings::autoSaveOnClose()) { ans = KMessageBox::Yes; } else { ans = KMessageBox::warningYesNoCancel(this, i18n("The file has been changed, save it?")); } return ans; } bool KMyMoneyApp::queryClose() { if (!isReady()) return false; if (d->dirty()) { int ans = askSaveOnClose(); if (ans == KMessageBox::Cancel) return false; else if (ans == KMessageBox::Yes) { bool saved = slotFileSave(); saveOptions(); return saved; } } // if (d->m_myMoneyView->isDatabase()) // slotFileClose(); // close off the database saveOptions(); return true; } ///////////////////////////////////////////////////////////////////// // SLOT IMPLEMENTATION ///////////////////////////////////////////////////////////////////// void KMyMoneyApp::slotFileInfoDialog() { QPointer dlg = new KMyMoneyFileInfoDlg(0); dlg->exec(); delete dlg; } void KMyMoneyApp::slotPerformanceTest() { // dump performance report to stderr int measurement[2]; QTime timer; MyMoneyAccount acc; qDebug("--- Starting performance tests ---"); // AccountList // MyMoneyFile::instance()->preloadCache(); measurement[0] = measurement[1] = 0; timer.start(); for (int i = 0; i < 1000; ++i) { QList list; MyMoneyFile::instance()->accountList(list); measurement[i != 0] = timer.elapsed(); } std::cerr << "accountList()" << std::endl; std::cerr << "First time: " << measurement[0] << " msec" << std::endl; std::cerr << "Total time: " << (measurement[0] + measurement[1]) << " msec" << std::endl; std::cerr << "Average : " << (measurement[0] + measurement[1]) / 1000 << " msec" << std::endl; // Balance of asset account(s) // MyMoneyFile::instance()->preloadCache(); measurement[0] = measurement[1] = 0; acc = MyMoneyFile::instance()->asset(); for (int i = 0; i < 1000; ++i) { timer.start(); MyMoneyMoney result = MyMoneyFile::instance()->balance(acc.id()); measurement[i != 0] += timer.elapsed(); } std::cerr << "balance(Asset)" << std::endl; std::cerr << "First time: " << measurement[0] << " msec" << std::endl; std::cerr << "Average : " << (measurement[0] + measurement[1]) / 1000 << " msec" << std::endl; // total balance of asset account // MyMoneyFile::instance()->preloadCache(); measurement[0] = measurement[1] = 0; acc = MyMoneyFile::instance()->asset(); for (int i = 0; i < 1000; ++i) { timer.start(); MyMoneyMoney result = MyMoneyFile::instance()->totalBalance(acc.id()); measurement[i != 0] += timer.elapsed(); } std::cerr << "totalBalance(Asset)" << std::endl; std::cerr << "First time: " << measurement[0] << " msec" << std::endl; std::cerr << "Average : " << (measurement[0] + measurement[1]) / 1000 << " msec" << std::endl; // Balance of expense account(s) // MyMoneyFile::instance()->preloadCache(); measurement[0] = measurement[1] = 0; acc = MyMoneyFile::instance()->expense(); for (int i = 0; i < 1000; ++i) { timer.start(); MyMoneyMoney result = MyMoneyFile::instance()->balance(acc.id()); measurement[i != 0] += timer.elapsed(); } std::cerr << "balance(Expense)" << std::endl; std::cerr << "First time: " << measurement[0] << " msec" << std::endl; std::cerr << "Average : " << (measurement[0] + measurement[1]) / 1000 << " msec" << std::endl; // total balance of expense account // MyMoneyFile::instance()->preloadCache(); measurement[0] = measurement[1] = 0; acc = MyMoneyFile::instance()->expense(); timer.start(); for (int i = 0; i < 1000; ++i) { MyMoneyMoney result = MyMoneyFile::instance()->totalBalance(acc.id()); measurement[i != 0] = timer.elapsed(); } std::cerr << "totalBalance(Expense)" << std::endl; std::cerr << "First time: " << measurement[0] << " msec" << std::endl; std::cerr << "Total time: " << (measurement[0] + measurement[1]) << " msec" << std::endl; std::cerr << "Average : " << (measurement[0] + measurement[1]) / 1000 << " msec" << std::endl; // transaction list // MyMoneyFile::instance()->preloadCache(); measurement[0] = measurement[1] = 0; if (MyMoneyFile::instance()->asset().accountCount()) { MyMoneyTransactionFilter filter(MyMoneyFile::instance()->asset().accountList()[0]); filter.setDateFilter(QDate(), QDate::currentDate()); QList list; timer.start(); for (int i = 0; i < 100; ++i) { list = MyMoneyFile::instance()->transactionList(filter); measurement[i != 0] = timer.elapsed(); } std::cerr << "transactionList()" << std::endl; std::cerr << "First time: " << measurement[0] << " msec" << std::endl; std::cerr << "Total time: " << (measurement[0] + measurement[1]) << " msec" << std::endl; std::cerr << "Average : " << (measurement[0] + measurement[1]) / 100 << " msec" << std::endl; } // transaction list // MyMoneyFile::instance()->preloadCache(); measurement[0] = measurement[1] = 0; if (MyMoneyFile::instance()->asset().accountCount()) { MyMoneyTransactionFilter filter(MyMoneyFile::instance()->asset().accountList()[0]); filter.setDateFilter(QDate(), QDate::currentDate()); QList list; timer.start(); for (int i = 0; i < 100; ++i) { MyMoneyFile::instance()->transactionList(list, filter); measurement[i != 0] = timer.elapsed(); } std::cerr << "transactionList(list)" << std::endl; std::cerr << "First time: " << measurement[0] << " msec" << std::endl; std::cerr << "Total time: " << (measurement[0] + measurement[1]) << " msec" << std::endl; std::cerr << "Average : " << (measurement[0] + measurement[1]) / 100 << " msec" << std::endl; } // MyMoneyFile::instance()->preloadCache(); } void KMyMoneyApp::slotFileNew() { KMSTATUS(i18n("Creating new document...")); slotFileClose(); if (!d->m_fileOpen) { // next line required until we move all file handling out of KMyMoneyView d->newFile(); d->m_fileName = QUrl(); updateCaption(); NewUserWizard::Wizard *wizard = new NewUserWizard::Wizard(); if (wizard->exec() == QDialog::Accepted) { MyMoneyFileTransaction ft; MyMoneyFile* file = MyMoneyFile::instance(); try { // store the user info file->setUser(wizard->user()); // create and setup base currency file->addCurrency(wizard->baseCurrency()); file->setBaseCurrency(wizard->baseCurrency()); // create a possible institution MyMoneyInstitution inst = wizard->institution(); if (inst.name().length()) { file->addInstitution(inst); } // create a possible checking account auto acc = wizard->account(); if (acc.name().length()) { acc.setInstitutionId(inst.id()); MyMoneyAccount asset = file->asset(); file->addAccount(acc, asset); // create possible opening balance transaction if (!wizard->openingBalance().isZero()) { file->createOpeningBalanceTransaction(acc, wizard->openingBalance()); } } // import the account templates QList templates = wizard->templates(); QList::iterator it_t; for (it_t = templates.begin(); it_t != templates.end(); ++it_t) { (*it_t).importTemplate(&progressCallback); } d->m_fileName = wizard->url(); ft.commit(); KMyMoneySettings::setFirstTimeRun(false); // FIXME This is a bit clumsy. We re-read the freshly // created file to be able to run through all the // fixup logic and then save it to keep the modified // flag off. slotFileSave(); if (d->openNondatabase(d->m_fileName)) { d->m_fileOpen = true; d->initializeStorage(); } slotFileSave(); // now keep the filename in the recent files used list //KRecentFilesAction *p = dynamic_cast(action(KStandardAction::name(KStandardAction::OpenRecent))); //if(p) d->m_recentFiles->addUrl(d->m_fileName); writeLastUsedFile(d->m_fileName.url()); } catch (const MyMoneyException &) { // next line required until we move all file handling out of KMyMoneyView d->closeFile(); } if (wizard->startSettingsAfterFinished()) slotSettings(); } else { // next line required until we move all file handling out of KMyMoneyView d->closeFile(); } delete wizard; updateCaption(); emit fileLoaded(d->m_fileName); } } bool KMyMoneyApp::isDatabase() { return (d->m_fileOpen && ((d->m_fileType == KmmDb))); } bool KMyMoneyApp::isNativeFile() { return (d->m_fileOpen && (d->m_fileType < MaxNativeFileType)); } bool KMyMoneyApp::fileOpen() const { return d->m_fileOpen; } // General open void KMyMoneyApp::slotFileOpen() { KMSTATUS(i18n("Open a file.")); QString prevDir = readLastUsedDir(); QString fileExtensions; fileExtensions.append(i18n("KMyMoney files (*.kmy *.xml)")); fileExtensions.append(QLatin1String(";;")); for (const auto& plugin : d->m_plugins.storage) { const auto fileExtension = plugin->fileExtension(); if (!fileExtension.isEmpty()) { fileExtensions.append(fileExtension); fileExtensions.append(QLatin1String(";;")); } } fileExtensions.append(i18n("All files (*)")); QPointer dialog = new QFileDialog(this, QString(), prevDir, fileExtensions); dialog->setFileMode(QFileDialog::ExistingFile); dialog->setAcceptMode(QFileDialog::AcceptOpen); if (dialog->exec() == QDialog::Accepted && dialog != nullptr) { slotFileOpenRecent(dialog->selectedUrls().first()); } delete dialog; } bool KMyMoneyApp::isImportableFile(const QUrl &url) { bool result = false; // Iterate through the plugins and see if there's a loaded plugin who can handle it QMap::const_iterator it_plugin = d->m_plugins.importer.constBegin(); while (it_plugin != d->m_plugins.importer.constEnd()) { if ((*it_plugin)->isMyFormat(url.path())) { result = true; break; } ++it_plugin; } // If we did not find a match, try importing it as a KMM statement file, // which is really just for testing. the statement file is not exposed // to users. if (it_plugin == d->m_plugins.importer.constEnd()) if (MyMoneyStatement::isStatementFile(url.path())) result = true; // Place code here to test for QIF and other locally-supported formats // (i.e. not a plugin). If you add them here, be sure to add it to // the webConnect function. return result; } bool KMyMoneyApp::isFileOpenedInAnotherInstance(const QUrl &url) { const auto instances = instanceList(); #ifdef KMM_DBUS // check if there are other instances which might have this file open for (const auto& instance : instances) { QDBusInterface remoteApp(instance, "/KMymoney", "org.kde.kmymoney"); QDBusReply reply = remoteApp.call("filename"); if (!reply.isValid()) qDebug("D-Bus error while calling app->filename()"); else if (reply.value() == url.url()) return true; } #else Q_UNUSED(url) #endif return false; } void KMyMoneyApp::slotFileOpenRecent(const QUrl &url) { KMSTATUS(i18n("Loading file...")); if (isFileOpenedInAnotherInstance(url)) { KMessageBox::sorry(this, i18n("

File %1 is already opened in another instance of KMyMoney

", url.toDisplayString(QUrl::PreferLocalFile)), i18n("Duplicate open")); return; } if (url.scheme() != QLatin1String("sql") && !KMyMoneyUtils::fileExists(url)) { KMessageBox::sorry(this, i18n("

%1 is either an invalid filename or the file does not exist. You can open another file or create a new one.

", url.toDisplayString(QUrl::PreferLocalFile)), i18n("File not found")); return; } if (d->m_fileOpen) slotFileClose(); if (d->m_fileOpen) return; try { auto isOpened = false; if (url.scheme() == QLatin1String("sql")) isOpened = d->openDatabase(url); else isOpened = d->openNondatabase(url); if (!isOpened) return; d->m_fileOpen = true; if (!d->initializeStorage()) { d->m_fileOpen = false; return; } if (isNativeFile()) { d->m_fileName = url; updateCaption(); writeLastUsedFile(url.toDisplayString(QUrl::PreferLocalFile)); /* Don't use url variable after KRecentFilesAction::addUrl * as it might delete it. * More in API reference to this method */ d->m_recentFiles->addUrl(url); } else { d->m_fileName = QUrl(); // imported files have no filename } } catch (const MyMoneyException &e) { KMessageBox::sorry(this, i18n("Cannot open file as requested. Error was: %1", e.what())); } updateCaption(); emit fileLoaded(d->m_fileName); } bool KMyMoneyApp::slotFileSave() { // if there's nothing changed, there's no need to save anything if (!d->dirty()) return true; bool rc = false; KMSTATUS(i18n("Saving file...")); if (d->m_fileName.isEmpty()) return slotFileSaveAs(); d->consistencyCheck(false); setEnabled(false); if (isDatabase()) { auto pluginFound = false; for (const auto& plugin : d->m_plugins.storage) { if (plugin->formatName().compare(QLatin1String("SQL")) == 0) { rc = plugin->save(d->m_fileName); pluginFound = true; break; } } if(!pluginFound) KMessageBox::error(this, i18n("Couldn't find suitable plugin to save your storage.")); } else { rc = d->saveFile(d->m_fileName, MyMoneyFile::instance()->value("kmm-encryption-key")); } setEnabled(true); d->m_autoSaveTimer->stop(); updateCaption(); return rc; } bool KMyMoneyApp::slotFileSaveAs() { bool rc = false; KMSTATUS(i18n("Saving file with a new filename...")); QString selectedKeyName; if (KGPGFile::GPGAvailable() && KMyMoneySettings::writeDataEncrypted()) { // fill the secret key list and combo box QStringList keyList; KGPGFile::secretKeyList(keyList); QPointer dlg = new KGpgKeySelectionDlg(this); dlg->setSecretKeys(keyList, KMyMoneySettings::gpgRecipient()); dlg->setAdditionalKeys(KMyMoneySettings::gpgRecipientList()); - const int rc = dlg->exec(); + rc = dlg->exec(); if ((rc == QDialog::Accepted) && (dlg != 0)) { d->m_additionalGpgKeys = dlg->additionalKeys(); selectedKeyName = dlg->secretKey(); } delete dlg; if (rc != QDialog::Accepted) { return false; } } QString prevDir; // don't prompt file name if not a native file if (isNativeFile()) prevDir = readLastUsedDir(); QPointer dlg = new QFileDialog(this, i18n("Save As"), prevDir, QString(QLatin1String("%2 (%1);;")).arg(QStringLiteral("*.kmy")).arg(i18nc("KMyMoney (Filefilter)", "KMyMoney files")) + QString(QLatin1String("%2 (%1);;")).arg(QStringLiteral("*.xml")).arg(i18nc("XML (Filefilter)", "XML files")) + QString(QLatin1String("%2 (%1);;")).arg(QStringLiteral("*.anon.xml")).arg(i18nc("Anonymous (Filefilter)", "Anonymous files")) + QString(QLatin1String("%2 (%1);;")).arg(QStringLiteral("*")).arg(i18nc("All files (Filefilter)", "All files"))); dlg->setAcceptMode(QFileDialog::AcceptSave); if (dlg->exec() == QDialog::Accepted && dlg != 0) { QUrl newURL = dlg->selectedUrls().first(); if (!newURL.fileName().isEmpty()) { d->consistencyCheck(false); QString newName = newURL.toDisplayString(QUrl::PreferLocalFile); // append extension if not present if (!newName.endsWith(QLatin1String(".kmy"), Qt::CaseInsensitive) && !newName.endsWith(QLatin1String(".xml"), Qt::CaseInsensitive)) newName.append(QLatin1String(".kmy")); newURL = QUrl::fromUserInput(newName); d->m_recentFiles->addUrl(newURL); setEnabled(false); // If this is the anonymous file export, just save it, don't actually take the // name, or remember it! Don't even try to encrypt it if (newName.endsWith(QLatin1String(".anon.xml"), Qt::CaseInsensitive)) rc = d->saveFile(newURL); else { d->m_fileName = newURL; QString encryptionKeys; QRegExp keyExp(".* \\((.*)\\)"); if (keyExp.indexIn(selectedKeyName) != -1) { encryptionKeys = keyExp.cap(1); if (!d->m_additionalGpgKeys.isEmpty()) { if (!encryptionKeys.isEmpty()) encryptionKeys.append(QLatin1Char(',')); encryptionKeys.append(d->m_additionalGpgKeys.join(QLatin1Char(','))); } } rc = d->saveFile(d->m_fileName, encryptionKeys); //write the directory used for this file as the default one for next time. writeLastUsedDir(newURL.toDisplayString(QUrl::RemoveFilename | QUrl::PreferLocalFile | QUrl::StripTrailingSlash)); writeLastUsedFile(newName); } d->m_autoSaveTimer->stop(); setEnabled(true); } } delete dlg; updateCaption(); return rc; } void KMyMoneyApp::slotFileCloseWindow() { KMSTATUS(i18n("Closing window...")); if (d->dirty()) { int answer = askSaveOnClose(); if (answer == KMessageBox::Cancel) return; else if (answer == KMessageBox::Yes) slotFileSave(); } close(); } void KMyMoneyApp::slotFileClose() { // bool okToSelect = true; // check if transaction editor is open and ask user what he wants to do // slotTransactionsCancelOrEnter(okToSelect); // if (!okToSelect) // return; // no update status here, as we might delete the status too early. if (d->dirty()) { int answer = askSaveOnClose(); if (answer == KMessageBox::Cancel) return; else if (answer == KMessageBox::Yes) slotFileSave(); } d->closeFile(); } void KMyMoneyApp::slotFileQuit() { // don't modify the status message here as this will prevent quit from working!! // See the beginning of queryClose() and isReady() why. Thomas Baumgart 2005-10-17 bool quitApplication = true; QList memberList = KMainWindow::memberList(); if (!memberList.isEmpty()) { QList::const_iterator w_it = memberList.constBegin(); for (; w_it != memberList.constEnd(); ++w_it) { // only close the window if the closeEvent is accepted. If the user presses Cancel on the saveModified() dialog, // the window and the application stay open. if (!(*w_it)->close()) { quitApplication = false; break; } } } // We will only quit if all windows were processed and not cancelled if (quitApplication) { QCoreApplication::quit(); } } void KMyMoneyApp::slotShowTransactionDetail() { } void KMyMoneyApp::slotHideReconciledTransactions() { KMyMoneySettings::setHideReconciledTransactions(pActions[Action::ViewHideReconciled]->isChecked()); d->m_myMoneyView->slotRefreshViews(); } void KMyMoneyApp::slotHideUnusedCategories() { KMyMoneySettings::setHideUnusedCategory(pActions[Action::ViewHideCategories]->isChecked()); d->m_myMoneyView->slotRefreshViews(); } void KMyMoneyApp::slotShowAllAccounts() { KMyMoneySettings::setShowAllAccounts(pActions[Action::ViewShowAll]->isChecked()); d->m_myMoneyView->slotRefreshViews(); } #ifdef KMM_DEBUG void KMyMoneyApp::slotFileFileInfo() { if (!d->m_fileOpen) { KMessageBox::information(this, i18n("No KMyMoneyFile open")); return; } QFile g("kmymoney.dump"); g.open(QIODevice::WriteOnly); QDataStream st(&g); MyMoneyStorageDump dumper; dumper.writeStream(st, MyMoneyFile::instance()->storage()); g.close(); } void KMyMoneyApp::slotToggleTraces() { MyMoneyTracer::onOff(pActions[Action::DebugTraces]->isChecked() ? 1 : 0); } #endif void KMyMoneyApp::slotToggleTimers() { extern bool timersOn; // main.cpp timersOn = pActions[Action::DebugTimers]->isChecked(); } QString KMyMoneyApp::slotStatusMsg(const QString &text) { /////////////////////////////////////////////////////////////////// // change status message permanently QString previousMessage = d->m_statusLabel->text(); d->m_applicationIsReady = false; QString currentMessage = text; if (currentMessage.isEmpty() || currentMessage == i18nc("Application is ready to use", "Ready.")) { d->m_applicationIsReady = true; currentMessage = i18nc("Application is ready to use", "Ready."); } statusBar()->clearMessage(); d->m_statusLabel->setText(currentMessage); return previousMessage; } void KMyMoneyApp::ready() { slotStatusMsg(QString()); } bool KMyMoneyApp::isReady() { return d->m_applicationIsReady; } void KMyMoneyApp::slotStatusProgressBar(int current, int total) { if (total == -1 && current == -1) { // reset if (d->m_progressTimer) { d->m_progressTimer->start(500); // remove from screen in 500 msec d->m_progressBar->setValue(d->m_progressBar->maximum()); } } else if (total != 0) { // init d->m_progressTimer->stop(); d->m_progressBar->setMaximum(total); d->m_progressBar->setValue(0); d->m_progressBar->show(); } else { // update QTime currentTime = QTime::currentTime(); // only process painting if last update is at least 250 ms ago if (abs(d->m_lastUpdate.msecsTo(currentTime)) > 250) { d->m_progressBar->setValue(current); d->m_lastUpdate = currentTime; } } } void KMyMoneyApp::slotStatusProgressDone() { d->m_progressTimer->stop(); d->m_progressBar->reset(); d->m_progressBar->hide(); d->m_progressBar->setValue(0); } void KMyMoneyApp::progressCallback(int current, int total, const QString& msg) { if (!msg.isEmpty()) kmymoney->slotStatusMsg(msg); kmymoney->slotStatusProgressBar(current, total); } void KMyMoneyApp::slotFileViewPersonal() { if (!d->m_fileOpen) { KMessageBox::information(this, i18n("No KMyMoneyFile open")); return; } KMSTATUS(i18n("Viewing personal data...")); MyMoneyFile* file = MyMoneyFile::instance(); MyMoneyPayee user = file->user(); QPointer editPersonalDataDlg = new EditPersonalDataDlg(user.name(), user.address(), user.city(), user.state(), user.postcode(), user.telephone(), user.email(), this, i18n("Edit Personal Data")); if (editPersonalDataDlg->exec() == QDialog::Accepted && editPersonalDataDlg != 0) { user.setName(editPersonalDataDlg->userName()); user.setAddress(editPersonalDataDlg->userStreet()); user.setCity(editPersonalDataDlg->userTown()); user.setState(editPersonalDataDlg->userCountry()); user.setPostcode(editPersonalDataDlg->userPostcode()); user.setTelephone(editPersonalDataDlg->userTelephone()); user.setEmail(editPersonalDataDlg->userEmail()); MyMoneyFileTransaction ft; try { file->setUser(user); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::information(this, i18n("Unable to store user information: %1", e.what())); } } delete editPersonalDataDlg; } void KMyMoneyApp::slotLoadAccountTemplates() { KMSTATUS(i18n("Importing account templates.")); int rc; QPointer dlg = new KLoadTemplateDlg(); if ((rc = dlg->exec()) == QDialog::Accepted && dlg != 0) { MyMoneyFileTransaction ft; try { // import the account templates QList templates = dlg->templates(); QList::iterator it_t; for (it_t = templates.begin(); it_t != templates.end(); ++it_t) { (*it_t).importTemplate(&progressCallback); } ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(0, i18n("Error"), i18n("Unable to import template(s): %1, thrown in %2:%3", e.what(), e.file(), e.line())); } } delete dlg; } void KMyMoneyApp::slotSaveAccountTemplates() { KMSTATUS(i18n("Exporting account templates.")); QString savePath = QStandardPaths::writableLocation(QStandardPaths::DataLocation) + "/templates/" + QLocale().name(); - QDir d(savePath); - if (!d.exists()) - d.mkpath(savePath); + QDir templatesDir(savePath); + if (!templatesDir.exists()) + templatesDir.mkpath(savePath); QString newName = QFileDialog::getSaveFileName(this, i18n("Save as..."), savePath, i18n("KMyMoney template files (*.kmt);;All files (*)")); // // If there is no file extension, then append a .kmt at the end of the file name. // If there is a file extension, make sure it is .kmt, delete any others. // if (!newName.isEmpty()) { // find last . delimiter int nLoc = newName.lastIndexOf('.'); if (nLoc != -1) { QString strExt, strTemp; strTemp = newName.left(nLoc + 1); strExt = newName.right(newName.length() - (nLoc + 1)); if ((strExt.indexOf("kmt", 0, Qt::CaseInsensitive) == -1)) { strTemp.append("kmt"); //append to make complete file name newName = strTemp; } } else { newName.append(".kmt"); } if (okToWriteFile(QUrl::fromLocalFile(newName))) { QPointer dlg = new KTemplateExportDlg(this); if (dlg->exec() == QDialog::Accepted && dlg) { MyMoneyTemplate templ; templ.setTitle(dlg->title()); templ.setShortDescription(dlg->shortDescription()); templ.setLongDescription(dlg->longDescription()); templ.exportTemplate(&progressCallback); templ.saveTemplate(QUrl::fromLocalFile(newName)); } delete dlg; } } } bool KMyMoneyApp::okToWriteFile(const QUrl &url) { Q_UNUSED(url) // check if the file exists and warn the user bool reallySaveFile = true; if (KMyMoneyUtils::fileExists(url)) { if (KMessageBox::warningYesNo(this, QLatin1String("") + i18n("The file %1 already exists. Do you really want to overwrite it?", url.toDisplayString(QUrl::PreferLocalFile)) + QLatin1String(""), i18n("File already exists")) != KMessageBox::Yes) reallySaveFile = false; } return reallySaveFile; } void KMyMoneyApp::slotSettings() { // if we already have an instance of the settings dialog, then use it if (KConfigDialog::showDialog("KMyMoney-Settings")) return; // otherwise, we have to create it auto dlg = new KSettingsKMyMoney(this, "KMyMoney-Settings", KMyMoneySettings::self()); connect(dlg, &KSettingsKMyMoney::settingsChanged, this, &KMyMoneyApp::slotUpdateConfiguration); dlg->show(); } void KMyMoneyApp::slotShowCredits() { KAboutData aboutData = initializeCreditsData(); KAboutApplicationDialog dlg(aboutData, this); dlg.exec(); } void KMyMoneyApp::slotUpdateConfiguration(const QString &dialogName) { if(dialogName.compare(QLatin1String("Plugins")) == 0) { KMyMoneyPlugin::pluginHandling(KMyMoneyPlugin::Action::Reorganize, d->m_plugins, this, guiFactory()); onlineJobAdministration::instance()->updateActions(); onlineJobAdministration::instance()->setOnlinePlugins(d->m_plugins.extended); d->m_myMoneyView->setOnlinePlugins(d->m_plugins.online); d->m_myMoneyView->setStoragePlugins(d->m_plugins.storage); return; } MyMoneyTransactionFilter::setFiscalYearStart(KMyMoneySettings::firstFiscalMonth(), KMyMoneySettings::firstFiscalDay()); #ifdef ENABLE_UNFINISHEDFEATURES LedgerSeparator::setFirstFiscalDate(KMyMoneySettings::firstFiscalMonth(), KMyMoneySettings::firstFiscalDay()); #endif d->m_myMoneyView->updateViewType(); // update the sql storage module settings // MyMoneyStorageSql::setStartDate(KMyMoneySettings::startDate().date()); // update the report module settings MyMoneyReport::setLineWidth(KMyMoneySettings::lineWidth()); // update the holiday region configuration setHolidayRegion(KMyMoneySettings::holidayRegion()); d->m_myMoneyView->slotRefreshViews(); // re-read autosave configuration d->m_autoSaveEnabled = KMyMoneySettings::autoSaveFile(); d->m_autoSavePeriod = KMyMoneySettings::autoSavePeriod(); // stop timer if turned off but running if (d->m_autoSaveTimer->isActive() && !d->m_autoSaveEnabled) { d->m_autoSaveTimer->stop(); } // start timer if turned on and needed but not running if (!d->m_autoSaveTimer->isActive() && d->m_autoSaveEnabled && d->dirty()) { d->m_autoSaveTimer->setSingleShot(true); d->m_autoSaveTimer->start(d->m_autoSavePeriod * 60 * 1000); } d->setThemedCSS(); // check if the recovery key is still valid or expires soon if (KMyMoneySettings::writeDataEncrypted() && KMyMoneySettings::encryptRecover()) { if (KGPGFile::GPGAvailable()) { KGPGFile file; QDateTime expirationDate = file.keyExpires(QLatin1String(recoveryKeyId2)); if (expirationDate.isValid() && QDateTime::currentDateTime().daysTo(expirationDate) <= RECOVER_KEY_EXPIRATION_WARNING) { bool skipMessage = false; //get global config object for our app. KSharedConfigPtr kconfig = KSharedConfig::openConfig(); KConfigGroup grp; QDate lastWarned; if (kconfig) { grp = d->m_config->group("General Options"); lastWarned = grp.readEntry("LastRecoverKeyExpirationWarning", QDate()); if (QDate::currentDate() == lastWarned) { skipMessage = true; } } if (!skipMessage) { if (kconfig) { grp.writeEntry("LastRecoverKeyExpirationWarning", QDate::currentDate()); } KMessageBox::information(this, i18np("You have configured KMyMoney to use GPG to protect your data and to encrypt your data also with the KMyMoney recover key. This key is about to expire in %1 day. Please update the key from a keyserver using your GPG frontend (e.g. KGPG).", "You have configured KMyMoney to use GPG to protect your data and to encrypt your data also with the KMyMoney recover key. This key is about to expire in %1 days. Please update the key from a keyserver using your GPG frontend (e.g. KGPG).", QDateTime::currentDateTime().daysTo(expirationDate)), i18n("Recover key expires soon")); } } } } } void KMyMoneyApp::slotBackupFile() { // Save the file first so isLocalFile() works if (d->m_myMoneyView && d->dirty()) { if (KMessageBox::questionYesNo(this, i18n("The file must be saved first " "before it can be backed up. Do you want to continue?")) == KMessageBox::No) { return; } slotFileSave(); } if (d->m_fileName.isEmpty()) return; if (!d->m_fileName.isLocalFile()) { KMessageBox::sorry(this, i18n("The current implementation of the backup functionality only supports local files as source files. Your current source file is '%1'.", d->m_fileName.url()), i18n("Local files only")); return; } QPointer backupDlg = new KBackupDlg(this); int returncode = backupDlg->exec(); if (returncode == QDialog::Accepted && backupDlg != 0) { d->m_backupMount = backupDlg->mountCheckBoxChecked(); d->m_proc.clearProgram(); d->m_backupState = BACKUP_MOUNTING; d->m_mountpoint = backupDlg->mountPoint(); if (d->m_backupMount) { slotBackupMount(); } else { progressCallback(0, 300, ""); #ifdef Q_OS_WIN d->m_ignoreBackupExitCode = true; QTimer::singleShot(0, this, SLOT(slotBackupHandleEvents())); #else // If we don't have to mount a device, we just issue // a dummy command to start the copy operation d->m_proc.setProgram("true"); d->m_proc.start(); #endif } } delete backupDlg; } void KMyMoneyApp::slotBackupMount() { progressCallback(0, 300, i18n("Mounting %1", d->m_mountpoint)); d->m_proc.setProgram("mount"); d->m_proc << d->m_mountpoint; d->m_proc.start(); } bool KMyMoneyApp::slotBackupWriteFile() { QFileInfo fi(d->m_fileName.fileName()); QString today = QDate::currentDate().toString("-yyyy-MM-dd.") + fi.suffix(); QString backupfile = d->m_mountpoint + '/' + d->m_fileName.fileName(); KMyMoneyUtils::appendCorrectFileExt(backupfile, today); // check if file already exists and ask what to do QFile f(backupfile); if (f.exists()) { int answer = KMessageBox::warningContinueCancel(this, i18n("Backup file for today exists on that device. Replace?"), i18n("Backup"), KGuiItem(i18n("&Replace"))); if (answer == KMessageBox::Cancel) { return false; } } progressCallback(50, 0, i18n("Writing %1", backupfile)); d->m_proc.clearProgram(); #ifdef Q_OS_WIN d->m_proc << "cmd.exe" << "/c" << "copy" << "/b" << "/y"; d->m_proc << (QDir::toNativeSeparators(d->m_fileName.toLocalFile()) + "+ nul") << QDir::toNativeSeparators(backupfile); #else d->m_proc << "cp" << "-f"; d->m_proc << d->m_fileName.toLocalFile() << backupfile; #endif d->m_backupState = BACKUP_COPYING; d->m_proc.start(); return true; } void KMyMoneyApp::slotBackupUnmount() { progressCallback(250, 0, i18n("Unmounting %1", d->m_mountpoint)); d->m_proc.clearProgram(); d->m_proc.setProgram("umount"); d->m_proc << d->m_mountpoint; d->m_backupState = BACKUP_UNMOUNTING; d->m_proc.start(); } void KMyMoneyApp::slotBackupFinish() { d->m_backupState = BACKUP_IDLE; progressCallback(-1, -1, QString()); ready(); } void KMyMoneyApp::slotBackupHandleEvents() { switch (d->m_backupState) { case BACKUP_MOUNTING: if (d->m_ignoreBackupExitCode || (d->m_proc.exitStatus() == QProcess::NormalExit && d->m_proc.exitCode() == 0)) { d->m_ignoreBackupExitCode = false; d->m_backupResult = 0; if (!slotBackupWriteFile()) { d->m_backupResult = 1; if (d->m_backupMount) slotBackupUnmount(); else slotBackupFinish(); } } else { KMessageBox::information(this, i18n("Error mounting device"), i18n("Backup")); d->m_backupResult = 1; if (d->m_backupMount) slotBackupUnmount(); else slotBackupFinish(); } break; case BACKUP_COPYING: if (d->m_proc.exitStatus() == QProcess::NormalExit && d->m_proc.exitCode() == 0) { if (d->m_backupMount) { slotBackupUnmount(); } else { progressCallback(300, 0, i18nc("Backup done", "Done")); KMessageBox::information(this, i18n("File successfully backed up"), i18n("Backup")); slotBackupFinish(); } } else { qDebug("copy exit code is %d", d->m_proc.exitCode()); d->m_backupResult = 1; KMessageBox::information(this, i18n("Error copying file to device"), i18n("Backup")); if (d->m_backupMount) slotBackupUnmount(); else slotBackupFinish(); } break; case BACKUP_UNMOUNTING: if (d->m_proc.exitStatus() == QProcess::NormalExit && d->m_proc.exitCode() == 0) { progressCallback(300, 0, i18nc("Backup done", "Done")); if (d->m_backupResult == 0) KMessageBox::information(this, i18n("File successfully backed up"), i18n("Backup")); } else { KMessageBox::information(this, i18n("Error unmounting device"), i18n("Backup")); } slotBackupFinish(); break; default: qWarning("Unknown state for backup operation!"); progressCallback(-1, -1, QString()); ready(); break; } } void KMyMoneyApp::slotShowTipOfTheDay() { KTipDialog::showTip(d->m_myMoneyView, "", true); } void KMyMoneyApp::slotShowPreviousView() { } void KMyMoneyApp::slotShowNextView() { } void KMyMoneyApp::slotGenerateSql() { // QPointer editor = new KGenerateSqlDlg(this); // editor->setObjectName("Generate Database SQL"); // editor->exec(); // delete editor; } void KMyMoneyApp::slotToolsStartKCalc() { QString cmd = KMyMoneySettings::externalCalculator(); // if none is present, we fall back to the default if (cmd.isEmpty()) { #if defined(Q_OS_WIN32) cmd = QLatin1String("calc"); #elif defined(Q_OS_MAC) cmd = QLatin1String("open -a Calculator"); #else cmd = QLatin1String("kcalc"); #endif } KRun::runCommand(cmd, this); } void KMyMoneyApp::createAccount(MyMoneyAccount& newAccount, MyMoneyAccount& parentAccount, MyMoneyAccount& brokerageAccount, MyMoneyMoney openingBal) { MyMoneyFile *file = MyMoneyFile::instance(); try { const MyMoneySecurity& sec = file->security(newAccount.currencyId()); // Check the opening balance if (openingBal.isPositive() && newAccount.accountGroup() == eMyMoney::Account::Type::Liability) { QString message = i18n("This account is a liability and if the " "opening balance represents money owed, then it should be negative. " "Negate the amount?\n\n" "Please click Yes to change the opening balance to %1,\n" "Please click No to leave the amount as %2,\n" "Please click Cancel to abort the account creation." , MyMoneyUtils::formatMoney(-openingBal, newAccount, sec) , MyMoneyUtils::formatMoney(openingBal, newAccount, sec)); int ans = KMessageBox::questionYesNoCancel(this, message); if (ans == KMessageBox::Yes) { openingBal = -openingBal; } else if (ans == KMessageBox::Cancel) return; } file->createAccount(newAccount, parentAccount, brokerageAccount, openingBal); } catch (const MyMoneyException &e) { KMessageBox::information(this, i18n("Unable to add account: %1", e.what())); } } void KMyMoneyApp::slotInvestmentNew(MyMoneyAccount& account, const MyMoneyAccount& parent) { KNewInvestmentWizard::newInvestment(account, parent); } void KMyMoneyApp::slotCategoryNew(MyMoneyAccount& account, const MyMoneyAccount& parent) { KNewAccountDlg::newCategory(account, parent); } void KMyMoneyApp::slotCategoryNew(MyMoneyAccount& account) { KNewAccountDlg::newCategory(account, MyMoneyAccount()); } void KMyMoneyApp::slotAccountNew(MyMoneyAccount& account) { NewAccountWizard::Wizard::newAccount(account); } void KMyMoneyApp::createSchedule(MyMoneySchedule newSchedule, MyMoneyAccount& newAccount) { MyMoneyFile* file = MyMoneyFile::instance(); // Add the schedule only if one exists // // Remember to modify the first split to reference the newly created account if (!newSchedule.name().isEmpty()) { try { // We assume at least 2 splits in the transaction MyMoneyTransaction t = newSchedule.transaction(); if (t.splitCount() < 2) { throw MYMONEYEXCEPTION("Transaction for schedule has less than 2 splits!"); } MyMoneyFileTransaction ft; try { file->addSchedule(newSchedule); // in case of a loan account, we keep a reference to this // schedule in the account if (newAccount.accountType() == eMyMoney::Account::Type::Loan || newAccount.accountType() == eMyMoney::Account::Type::AssetLoan) { newAccount.setValue("schedule", newSchedule.id()); file->modifyAccount(newAccount); } ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::information(this, i18n("Unable to add scheduled transaction: %1", e.what())); } } catch (const MyMoneyException &e) { KMessageBox::information(this, i18n("Unable to add scheduled transaction: %1", e.what())); } } } QList > KMyMoneyApp::Private::automaticReconciliation(const MyMoneyAccount &account, const QList > &transactions, const MyMoneyMoney &amount) { static const int NR_OF_STEPS_LIMIT = 300000; static const int PROGRESSBAR_STEPS = 1000; QList > result = transactions; KMSTATUS(i18n("Running automatic reconciliation")); int progressBarIndex = 0; q->slotStatusProgressBar(progressBarIndex, NR_OF_STEPS_LIMIT / PROGRESSBAR_STEPS); // optimize the most common case - all transactions should be cleared QListIterator > itTransactionSplitResult(result); MyMoneyMoney transactionsBalance; while (itTransactionSplitResult.hasNext()) { const QPair &transactionSplit = itTransactionSplitResult.next(); transactionsBalance += transactionSplit.second.shares(); } if (amount == transactionsBalance) { result = transactions; return result; } q->slotStatusProgressBar(progressBarIndex++, 0); // only one transaction is uncleared itTransactionSplitResult.toFront(); int index = 0; while (itTransactionSplitResult.hasNext()) { const QPair &transactionSplit = itTransactionSplitResult.next(); if (transactionsBalance - transactionSplit.second.shares() == amount) { result.removeAt(index); return result; } index++; } q->slotStatusProgressBar(progressBarIndex++, 0); // more than one transaction is uncleared - apply the algorithm result.clear(); const MyMoneySecurity &security = MyMoneyFile::instance()->security(account.currencyId()); double precision = 0.1 / account.fraction(security); QList sumList; sumList << MyMoneyMoney(); QMap > > sumToComponentsMap; // compute the possible matches QListIterator > itTransactionSplit(transactions); while (itTransactionSplit.hasNext()) { const QPair &transactionSplit = itTransactionSplit.next(); QListIterator itSum(sumList); QList tempList; while (itSum.hasNext()) { const MyMoneyMoney &sum = itSum.next(); QList > splitIds; splitIds << qMakePair(transactionSplit.first.id(), transactionSplit.second.id()); if (sumToComponentsMap.contains(sum)) { if (sumToComponentsMap.value(sum).contains(qMakePair(transactionSplit.first.id(), transactionSplit.second.id()))) { continue; } splitIds.append(sumToComponentsMap.value(sum)); } tempList << transactionSplit.second.shares() + sum; sumToComponentsMap[transactionSplit.second.shares() + sum] = splitIds; int size = sumToComponentsMap.size(); if (size % PROGRESSBAR_STEPS == 0) { q->slotStatusProgressBar(progressBarIndex++, 0); } if (size > NR_OF_STEPS_LIMIT) { return result; // it's taking too much resources abort the algorithm } } QList unionList; unionList.append(tempList); unionList.append(sumList); qSort(unionList); sumList.clear(); MyMoneyMoney smallestSumFromUnion = unionList.first(); sumList.append(smallestSumFromUnion); QListIterator itUnion(unionList); while (itUnion.hasNext()) { MyMoneyMoney sumFromUnion = itUnion.next(); if (smallestSumFromUnion < MyMoneyMoney(1 - precision / transactions.size())*sumFromUnion) { smallestSumFromUnion = sumFromUnion; sumList.append(sumFromUnion); } } } q->slotStatusProgressBar(NR_OF_STEPS_LIMIT / PROGRESSBAR_STEPS, 0); if (sumToComponentsMap.contains(amount)) { - QListIterator > itTransactionSplit(transactions); - while (itTransactionSplit.hasNext()) { - const QPair &transactionSplit = itTransactionSplit.next(); + QListIterator > itTransactionSplit2(transactions); + while (itTransactionSplit2.hasNext()) { + const QPair &transactionSplit = itTransactionSplit2.next(); const QList > &splitIds = sumToComponentsMap.value(amount); if (splitIds.contains(qMakePair(transactionSplit.first.id(), transactionSplit.second.id()))) { result.append(transactionSplit); } } } #ifdef KMM_DEBUG qDebug("For the amount %s a number of %d possible sums where computed from the set of %d transactions: ", qPrintable(MyMoneyUtils::formatMoney(amount, security)), sumToComponentsMap.size(), transactions.size()); #endif q->slotStatusProgressBar(-1, -1); return result; } void KMyMoneyApp::slotReparentAccount(const MyMoneyAccount& _src, const MyMoneyInstitution& _dst) { MyMoneyAccount src(_src); src.setInstitutionId(_dst.id()); MyMoneyFileTransaction ft; try { MyMoneyFile::instance()->modifyAccount(src); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::sorry(this, i18n("

%1 cannot be moved to institution %2. Reason: %3

", src.name(), _dst.name(), e.what())); } } void KMyMoneyApp::slotReparentAccount(const MyMoneyAccount& _src, const MyMoneyAccount& _dst) { MyMoneyAccount src(_src); MyMoneyAccount dst(_dst); MyMoneyFileTransaction ft; try { MyMoneyFile::instance()->reparentAccount(src, dst); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::sorry(this, i18n("

%1 cannot be moved to %2. Reason: %3

", src.name(), dst.name(), e.what())); } } void KMyMoneyApp::slotScheduleNew(const MyMoneyTransaction& _t, eMyMoney::Schedule::Occurrence occurrence) { KEditScheduleDlg::newSchedule(_t, occurrence); } void KMyMoneyApp::slotPayeeNew(const QString& newnameBase, QString& id) { KMyMoneyUtils::newPayee(newnameBase, id); } void KMyMoneyApp::slotNewFeature() { } // move a stock transaction from one investment account to another void KMyMoneyApp::Private::moveInvestmentTransaction(const QString& /*fromId*/, const QString& toId, const MyMoneyTransaction& tx) { MyMoneyAccount toInvAcc = MyMoneyFile::instance()->account(toId); MyMoneyTransaction t(tx); // first determine which stock we are dealing with. // fortunately, investment transactions have only one stock involved QString stockAccountId; QString stockSecurityId; MyMoneySplit s; foreach (const auto split, t.splits()) { stockAccountId = split.accountId(); stockSecurityId = MyMoneyFile::instance()->account(stockAccountId).currencyId(); if (!MyMoneyFile::instance()->security(stockSecurityId).isCurrency()) { s = split; break; } } // Now check the target investment account to see if it // contains a stock with this id QString newStockAccountId; foreach (const auto sAccount, toInvAcc.accountList()) { if (MyMoneyFile::instance()->account(sAccount).currencyId() == stockSecurityId) { newStockAccountId = sAccount; break; } } // if it doesn't exist, we need to add it as a copy of the old one // no 'copyAccount()' function?? if (newStockAccountId.isEmpty()) { MyMoneyAccount stockAccount = MyMoneyFile::instance()->account(stockAccountId); MyMoneyAccount newStock; newStock.setName(stockAccount.name()); newStock.setNumber(stockAccount.number()); newStock.setDescription(stockAccount.description()); newStock.setInstitutionId(stockAccount.institutionId()); newStock.setOpeningDate(stockAccount.openingDate()); newStock.setAccountType(stockAccount.accountType()); newStock.setCurrencyId(stockAccount.currencyId()); newStock.setClosed(stockAccount.isClosed()); MyMoneyFile::instance()->addAccount(newStock, toInvAcc); newStockAccountId = newStock.id(); } // now update the split and the transaction s.setAccountId(newStockAccountId); t.modifySplit(s); MyMoneyFile::instance()->modifyTransaction(t); } void KMyMoneyApp::showContextMenu(const QString& containerName) { QWidget* w = factory()->container(containerName, this); if (auto menu = dynamic_cast(w)) menu->exec(QCursor::pos()); else qDebug("menu '%s' not found: w = %p, menu = %p", qPrintable(containerName), w, menu); } void KMyMoneyApp::slotPrintView() { d->m_myMoneyView->slotPrintView(); } void KMyMoneyApp::updateCaption(bool skipActions) { QString caption; caption = d->m_fileName.fileName(); if (caption.isEmpty() && d->m_myMoneyView && d->m_fileOpen) caption = i18n("Untitled"); // MyMoneyFile::instance()->dirty() throws an exception, if // there's no storage object available. In this case, we // assume that the storage object is not changed. Actually, // this can only happen if we are newly created early on. bool modified; try { modified = MyMoneyFile::instance()->dirty(); } catch (const MyMoneyException &) { modified = false; skipActions = true; } #ifdef KMM_DEBUG caption += QString(" (%1 x %2)").arg(width()).arg(height()); #endif setCaption(caption, modified); if (!skipActions) { d->m_myMoneyView->enableViewsIfFileOpen(d->m_fileOpen); slotUpdateActions(); } } void KMyMoneyApp::slotUpdateActions() { const auto file = MyMoneyFile::instance(); const bool fileOpen = d->m_fileOpen; const bool modified = file->dirty(); // const bool importRunning = (d->m_smtReader != 0); auto aC = actionCollection(); // ************* // Disabling actions based on conditions // ************* { QString tooltip = i18n("Create a new transaction"); const QVector> actionStates { // {qMakePair(Action::FileOpenDatabase, true)}, // {qMakePair(Action::FileSaveAsDatabase, fileOpen)}, {qMakePair(Action::FilePersonalData, fileOpen)}, {qMakePair(Action::FileBackup, (fileOpen && !isDatabase()))}, {qMakePair(Action::FileInformation, fileOpen)}, {qMakePair(Action::FileImportTemplate, fileOpen/* && !importRunning*/)}, {qMakePair(Action::FileExportTemplate, fileOpen/* && !importRunning*/)}, #ifdef KMM_DEBUG {qMakePair(Action::FileDump, fileOpen)}, #endif {qMakePair(Action::EditFindTransaction, fileOpen)}, {qMakePair(Action::ToolCurrencies, fileOpen)}, {qMakePair(Action::ToolPrices, fileOpen)}, {qMakePair(Action::ToolUpdatePrices, fileOpen)}, {qMakePair(Action::ToolConsistency, fileOpen)}, {qMakePair(Action::NewAccount, fileOpen)}, {qMakePair(Action::AccountCreditTransfer, onlineJobAdministration::instance()->canSendCreditTransfer())}, {qMakePair(Action::NewInstitution, fileOpen)}, // {qMakePair(Action::TransactionNew, (fileOpen && d->m_myMoneyView->canCreateTransactions(KMyMoneyRegister::SelectedTransactions(), tooltip)))}, {qMakePair(Action::NewSchedule, fileOpen)}, // {qMakePair(Action::CurrencyNew, fileOpen)}, // {qMakePair(Action::PriceNew, fileOpen)}, }; for (const auto& a : actionStates) pActions[a.first]->setEnabled(a.second); } // ************* // Disabling standard actions based on conditions // ************* aC->action(QString::fromLatin1(KStandardAction::name(KStandardAction::Save)))->setEnabled(modified /*&& !d->m_myMoneyView->isDatabase()*/); aC->action(QString::fromLatin1(KStandardAction::name(KStandardAction::SaveAs)))->setEnabled(fileOpen); aC->action(QString::fromLatin1(KStandardAction::name(KStandardAction::Close)))->setEnabled(fileOpen); aC->action(QString::fromLatin1(KStandardAction::name(KStandardAction::Print)))->setEnabled(fileOpen && d->m_myMoneyView->canPrint()); } void KMyMoneyApp::slotResetSelections() { d->m_myMoneyView->slotObjectSelected(MyMoneyAccount()); d->m_myMoneyView->slotObjectSelected(MyMoneyInstitution()); d->m_myMoneyView->slotObjectSelected(MyMoneySchedule()); d->m_myMoneyView->slotObjectSelected(MyMoneyTag()); d->m_myMoneyView->slotSelectByVariant(QVariantList {QVariant::fromValue(KMyMoneyRegister::SelectedTransactions())}, eView::Intent::SelectRegisterTransactions); slotUpdateActions(); } void KMyMoneyApp::slotDataChanged() { // As this method is called every time the MyMoneyFile instance // notifies a modification, it's the perfect place to start the timer if needed if (d->m_autoSaveEnabled && !d->m_autoSaveTimer->isActive()) { d->m_autoSaveTimer->setSingleShot(true); d->m_autoSaveTimer->start(d->m_autoSavePeriod * 60 * 1000); //miliseconds } updateCaption(); } void KMyMoneyApp::slotCurrencyDialog() { QPointer dlg = new KCurrencyEditDlg(this); dlg->exec(); delete dlg; } void KMyMoneyApp::slotPriceDialog() { QPointer dlg = new KMyMoneyPriceDlg(this); dlg->exec(); delete dlg; } void KMyMoneyApp::slotFileConsistencyCheck() { d->consistencyCheck(true); updateCaption(); } void KMyMoneyApp::Private::consistencyCheck(bool alwaysDisplayResult) { KMSTATUS(i18n("Running consistency check...")); MyMoneyFileTransaction ft; try { m_consistencyCheckResult = MyMoneyFile::instance()->consistencyCheck(); ft.commit(); } catch (const MyMoneyException &e) { m_consistencyCheckResult.append(i18n("Consistency check failed: %1", e.what())); // always display the result if the check failed alwaysDisplayResult = true; } // in case the consistency check was OK, we get a single line as result // in all errneous cases, we get more than one line and force the // display of them. if (alwaysDisplayResult || m_consistencyCheckResult.size() > 1) { QString msg = i18n("The consistency check has found no issues in your data. Details are presented below."); if (m_consistencyCheckResult.size() > 1) msg = i18n("The consistency check has found some issues in your data. Details are presented below. Those issues that could not be corrected automatically need to be solved by the user."); // install a context menu for the list after the dialog is displayed QTimer::singleShot(500, q, SLOT(slotInstallConsistencyCheckContextMenu())); KMessageBox::informationList(0, msg, m_consistencyCheckResult, i18n("Consistency check result")); } // this data is no longer needed m_consistencyCheckResult.clear(); } void KMyMoneyApp::Private::copyConsistencyCheckResults() { QClipboard *clipboard = QApplication::clipboard(); clipboard->setText(m_consistencyCheckResult.join(QLatin1String("\n"))); } void KMyMoneyApp::Private::saveConsistencyCheckResults() { QUrl fileUrl = QFileDialog::getSaveFileUrl(q); if (!fileUrl.isEmpty()) { QFile file(fileUrl.toLocalFile()); if (file.open(QFile::WriteOnly | QFile::Append | QFile::Text)) { QTextStream out(&file); out << m_consistencyCheckResult.join(QLatin1String("\n")); file.close(); } } } void KMyMoneyApp::Private::setThemedCSS() { const QStringList CSSnames {QStringLiteral("kmymoney.css"), QStringLiteral("welcome.css")}; const QString rcDir("/html/"); const QStringList defaultCSSDirs = QStandardPaths::locateAll(QStandardPaths::AppDataLocation, rcDir, QStandardPaths::LocateDirectory); // scan the list of directories to find the ones that really // contains all files we look for QString defaultCSSDir; foreach (const auto dir, defaultCSSDirs) { defaultCSSDir = dir; foreach (const auto CSSname, CSSnames) { QFileInfo fileInfo(defaultCSSDir + CSSname); if (!fileInfo.exists()) { defaultCSSDir.clear(); break; } } if (!defaultCSSDir.isEmpty()) { break; } } // make sure we have the local directory where the themed version is stored const QString themedCSSDir = QStandardPaths::standardLocations(QStandardPaths::AppConfigLocation).first() + rcDir; QDir().mkpath(themedCSSDir); foreach (const auto CSSname, CSSnames) { const QString defaultCSSFilename = defaultCSSDir + CSSname; QFileInfo fileInfo(defaultCSSFilename); if (fileInfo.exists()) { const QString themedCSSFilename = themedCSSDir + CSSname; QFile::remove(themedCSSFilename); if (QFile::copy(defaultCSSFilename, themedCSSFilename)) { QFile cssFile (themedCSSFilename); if (cssFile.open(QIODevice::ReadWrite)) { QTextStream cssStream(&cssFile); auto cssText = cssStream.readAll(); cssText.replace(QLatin1String("./"), defaultCSSDir, Qt::CaseSensitive); cssText.replace(QLatin1String("WindowText"), KMyMoneySettings::schemeColor(SchemeColor::WindowText).name(), Qt::CaseSensitive); cssText.replace(QLatin1String("Window"), KMyMoneySettings::schemeColor(SchemeColor::WindowBackground).name(), Qt::CaseSensitive); cssText.replace(QLatin1String("HighlightText"), KMyMoneySettings::schemeColor(SchemeColor::ListHighlightText).name(), Qt::CaseSensitive); cssText.replace(QLatin1String("Highlight"), KMyMoneySettings::schemeColor(SchemeColor::ListHighlight).name(), Qt::CaseSensitive); cssText.replace(QLatin1String("black"), KMyMoneySettings::schemeColor(SchemeColor::ListGrid).name(), Qt::CaseSensitive); cssStream.seek(0); cssStream << cssText; cssFile.close(); } } } } } void KMyMoneyApp::slotCheckSchedules() { if (KMyMoneySettings::checkSchedule() == true) { KMSTATUS(i18n("Checking for overdue scheduled transactions...")); MyMoneyFile *file = MyMoneyFile::instance(); QDate checkDate = QDate::currentDate().addDays(KMyMoneySettings::checkSchedulePreview()); QList scheduleList = file->scheduleList(); QList::Iterator it; eDialogs::ScheduleResultCode rc = eDialogs::ScheduleResultCode::Enter; for (it = scheduleList.begin(); (it != scheduleList.end()) && (rc != eDialogs::ScheduleResultCode::Cancel); ++it) { // Get the copy in the file because it might be modified by commitTransaction MyMoneySchedule schedule = file->schedule((*it).id()); if (schedule.autoEnter()) { try { while (!schedule.isFinished() && (schedule.adjustedNextDueDate() <= checkDate) && rc != eDialogs::ScheduleResultCode::Ignore && rc != eDialogs::ScheduleResultCode::Cancel) { rc = d->m_myMoneyView->enterSchedule(schedule, true, true); schedule = file->schedule((*it).id()); // get a copy of the modified schedule } } catch (const MyMoneyException &) { } } if (rc == eDialogs::ScheduleResultCode::Ignore) { // if the current schedule was ignored then we must make sure that the user can still enter the next scheduled transaction rc = eDialogs::ScheduleResultCode::Enter; } } updateCaption(); } } void KMyMoneyApp::writeLastUsedDir(const QString& directory) { //get global config object for our app. KSharedConfigPtr kconfig = KSharedConfig::openConfig(); if (kconfig) { KConfigGroup grp = kconfig->group("General Options"); //write path entry, no error handling since its void. grp.writeEntry("LastUsedDirectory", directory); } } void KMyMoneyApp::writeLastUsedFile(const QString& fileName) { //get global config object for our app. KSharedConfigPtr kconfig = KSharedConfig::openConfig(); if (kconfig) { KConfigGroup grp = d->m_config->group("General Options"); // write path entry, no error handling since its void. // use a standard string, as fileName could contain a protocol // e.g. file:/home/thb/.... grp.writeEntry("LastUsedFile", fileName); } } QString KMyMoneyApp::readLastUsedDir() const { QString str; //get global config object for our app. KSharedConfigPtr kconfig = KSharedConfig::openConfig(); if (kconfig) { KConfigGroup grp = d->m_config->group("General Options"); //read path entry. Second parameter is the default if the setting is not found, which will be the default document path. str = grp.readEntry("LastUsedDirectory", QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation)); // if the path stored is empty, we use the default nevertheless if (str.isEmpty()) str = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation); } return str; } QString KMyMoneyApp::readLastUsedFile() const { QString str; // get global config object for our app. KSharedConfigPtr kconfig = KSharedConfig::openConfig(); if (kconfig) { KConfigGroup grp = d->m_config->group("General Options"); // read filename entry. str = grp.readEntry("LastUsedFile", ""); } return str; } QString KMyMoneyApp::filename() const { return d->m_fileName.url(); } QUrl KMyMoneyApp::filenameURL() const { return d->m_fileName; } void KMyMoneyApp::addToRecentFiles(const QUrl& url) { d->m_recentFiles->addUrl(url); } QTimer* KMyMoneyApp::autosaveTimer() { return d->m_autoSaveTimer; } WebConnect* KMyMoneyApp::webConnect() const { return d->m_webConnect; } QList KMyMoneyApp::instanceList() const { QList list; #ifdef KMM_DBUS QDBusReply reply = QDBusConnection::sessionBus().interface()->registeredServiceNames(); if (reply.isValid()) { QStringList apps = reply.value(); QStringList::ConstIterator it; // build a list of service names of all running kmymoney applications without this one for (it = apps.constBegin(); it != apps.constEnd(); ++it) { // please change this method of creating a list of 'all the other kmymoney instances that are running on the system' // since assuming that D-Bus creates service names with org.kde.kmymoney-PID is an observation I don't think that it's documented somwhere if ((*it).indexOf("org.kde.kmymoney-") == 0) { uint thisProcPid = platformTools::processId(); if ((*it).indexOf(QString("org.kde.kmymoney-%1").arg(thisProcPid)) != 0) list += (*it); } } } else { qDebug("D-Bus returned the following error while obtaining instances: %s", qPrintable(reply.error().message())); } #endif return list; } void KMyMoneyApp::slotEquityPriceUpdate() { QPointer dlg = new KEquityPriceUpdateDlg(this); if (dlg->exec() == QDialog::Accepted && dlg != 0) dlg->storePrices(); delete dlg; } void KMyMoneyApp::webConnect(const QString& sourceUrl, const QByteArray& asn_id) { // // Web connect attempts to go through the known importers and see if the file // can be importing using that method. If so, it will import it using that // plugin // Q_UNUSED(asn_id) d->m_importUrlsQueue.enqueue(sourceUrl); // only start processing if this is the only import so far if (d->m_importUrlsQueue.count() == 1) { while (!d->m_importUrlsQueue.isEmpty()) { // get the value of the next item from the queue // but leave it on the queue for now QString url = d->m_importUrlsQueue.head(); // Bring this window to the forefront. This method was suggested by // Lubos Lunak of the KDE core development team. // TODO: port KF5 (WebConnect) //KStartupInfo::setNewStartupId(this, asn_id); // Make sure we have an open file if (! d->m_fileOpen && KMessageBox::warningContinueCancel(this, i18n("You must first select a KMyMoney file before you can import a statement.")) == KMessageBox::Continue) slotFileOpen(); // only continue if the user really did open a file. if (d->m_fileOpen) { KMSTATUS(i18n("Importing a statement via Web Connect")); // remove the statement files d->unlinkStatementXML(); QMap::const_iterator it_plugin = d->m_plugins.importer.constBegin(); while (it_plugin != d->m_plugins.importer.constEnd()) { if ((*it_plugin)->isMyFormat(url)) { QList statements; if (!(*it_plugin)->import(url)) { KMessageBox::error(this, i18n("Unable to import %1 using %2 plugin. The plugin returned the following error: %3", url, (*it_plugin)->formatName(), (*it_plugin)->lastError()), i18n("Importing error")); } break; } ++it_plugin; } // If we did not find a match, try importing it as a KMM statement file, // which is really just for testing. the statement file is not exposed // to users. if (it_plugin == d->m_plugins.importer.constEnd()) if (MyMoneyStatement::isStatementFile(url)) MyMoneyStatementReader::importStatement(url, false, &progressCallback); } // remove the current processed item from the queue d->m_importUrlsQueue.dequeue(); } } } void KMyMoneyApp::slotEnableMessages() { KMessageBox::enableAllMessages(); KMessageBox::information(this, i18n("All messages have been enabled."), i18n("All messages")); } void KMyMoneyApp::createInterfaces() { // Sets up the plugin interface KMyMoneyPlugin::pluginInterfaces().appInterface = new KMyMoneyPlugin::KMMAppInterface(this, this); KMyMoneyPlugin::pluginInterfaces().importInterface = new KMyMoneyPlugin::KMMImportInterface(this); KMyMoneyPlugin::pluginInterfaces().statementInterface = new KMyMoneyPlugin::KMMStatementInterface(this); KMyMoneyPlugin::pluginInterfaces().viewInterface = new KMyMoneyPlugin::KMMViewInterface(d->m_myMoneyView, this); // setup the calendar interface for schedules MyMoneySchedule::setProcessingCalendar(this); } void KMyMoneyApp::slotAutoSave() { if (!d->m_inAutoSaving) { // store the focus widget so we can restore it after save QPointer focusWidget = qApp->focusWidget(); d->m_inAutoSaving = true; KMSTATUS(i18n("Auto saving...")); //calls slotFileSave if needed, and restart the timer //it the file is not saved, reinitializes the countdown. if (d->dirty() && d->m_autoSaveEnabled) { if (!slotFileSave() && d->m_autoSavePeriod > 0) { d->m_autoSaveTimer->setSingleShot(true); d->m_autoSaveTimer->start(d->m_autoSavePeriod * 60 * 1000); } } d->m_inAutoSaving = false; if (focusWidget && focusWidget != qApp->focusWidget()) { // we have a valid focus widget so restore it focusWidget->setFocus(); } } } void KMyMoneyApp::slotDateChanged() { QDateTime dt = QDateTime::currentDateTime(); QDateTime nextDay(QDate(dt.date().addDays(1)), QTime(0, 0, 0)); // +1 is to make sure that we're already in the next day when the // signal is sent (this way we also avoid setting the timer to 0) QTimer::singleShot((dt.secsTo(nextDay) + 1)*1000, this, SLOT(slotDateChanged())); d->m_myMoneyView->slotRefreshViews(); } void KMyMoneyApp::setHolidayRegion(const QString& holidayRegion) { #ifdef KF5Holidays_FOUND //since the cost of updating the cache is now not negligible //check whether the region has been modified if (!d->m_holidayRegion || d->m_holidayRegion->regionCode() != holidayRegion) { // Delete the previous holidayRegion before creating a new one. delete d->m_holidayRegion; // Create a new holidayRegion. d->m_holidayRegion = new KHolidays::HolidayRegion(holidayRegion); //clear and update the holiday cache preloadHolidays(); } #else Q_UNUSED(holidayRegion); #endif } bool KMyMoneyApp::isProcessingDate(const QDate& date) const { if (!d->m_processingDays.testBit(date.dayOfWeek())) return false; #ifdef KF5Holidays_FOUND if (!d->m_holidayRegion || !d->m_holidayRegion->isValid()) return true; //check first whether it's already in cache if (d->m_holidayMap.contains(date)) { return d->m_holidayMap.value(date, true); } else { bool processingDay = !d->m_holidayRegion->isHoliday(date); d->m_holidayMap.insert(date, processingDay); return processingDay; } #else return true; #endif } void KMyMoneyApp::preloadHolidays() { #ifdef KF5Holidays_FOUND //clear the cache before loading d->m_holidayMap.clear(); //only do this if it is a valid region if (d->m_holidayRegion && d->m_holidayRegion->isValid()) { //load holidays for the forecast days plus 1 cycle, to be on the safe side int forecastDays = KMyMoneySettings::forecastDays() + KMyMoneySettings::forecastAccountCycle(); QDate endDate = QDate::currentDate().addDays(forecastDays); //look for holidays for the next 2 years as a minimum. That should give a good margin for the cache if (endDate < QDate::currentDate().addYears(2)) endDate = QDate::currentDate().addYears(2); KHolidays::Holiday::List holidayList = d->m_holidayRegion->holidays(QDate::currentDate(), endDate); KHolidays::Holiday::List::const_iterator holiday_it; for (holiday_it = holidayList.constBegin(); holiday_it != holidayList.constEnd(); ++holiday_it) { for (QDate holidayDate = (*holiday_it).observedStartDate(); holidayDate <= (*holiday_it).observedEndDate(); holidayDate = holidayDate.addDays(1)) d->m_holidayMap.insert(holidayDate, false); } for (QDate date = QDate::currentDate(); date <= endDate; date = date.addDays(1)) { //if it is not a processing day, set it to false if (!d->m_processingDays.testBit(date.dayOfWeek())) { d->m_holidayMap.insert(date, false); } else if (!d->m_holidayMap.contains(date)) { //if it is not a holiday nor a weekend, it is a processing day d->m_holidayMap.insert(date, true); } } } #endif } KMStatus::KMStatus(const QString &text) { m_prevText = kmymoney->slotStatusMsg(text); } KMStatus::~KMStatus() { kmymoney->slotStatusMsg(m_prevText); } void KMyMoneyApp::Private::unlinkStatementXML() { QDir d(KMyMoneySettings::logPath(), "kmm-statement*"); for (uint i = 0; i < d.count(); ++i) { qDebug("Remove %s", qPrintable(d[i])); d.remove(KMyMoneySettings::logPath() + QString("/%1").arg(d[i])); } m_statementXMLindex = 0; } void KMyMoneyApp::Private::closeFile() { m_myMoneyView->slotObjectSelected(MyMoneyAccount()); m_myMoneyView->slotObjectSelected(MyMoneyInstitution()); m_myMoneyView->slotObjectSelected(MyMoneySchedule()); m_myMoneyView->slotObjectSelected(MyMoneyTag()); m_myMoneyView->slotSelectByVariant(QVariantList {QVariant::fromValue(KMyMoneyRegister::SelectedTransactions())}, eView::Intent::SelectRegisterTransactions); m_myMoneyView->finishReconciliation(MyMoneyAccount()); m_myMoneyView->slotFileClosed(); disconnectStorageFromModels(); // notify the models that the file is going to be closed (we should have something like dataChanged that reaches the models first) Models::instance()->fileClosed(); emit q->kmmFilePlugin(KMyMoneyApp::preClose); if (q->isDatabase()) MyMoneyFile::instance()->storage()->close(); // to log off a database user newStorage(); emit q->kmmFilePlugin(postClose); m_fileOpen = false; m_fileName = QUrl(); q->updateCaption(); // just create a new balance warning object delete m_balanceWarning; m_balanceWarning = new KBalanceWarning(q); emit q->fileLoaded(m_fileName); } diff --git a/kmymoney/kmymoney.h b/kmymoney/kmymoney.h index da24c5922..223276a52 100644 --- a/kmymoney/kmymoney.h +++ b/kmymoney/kmymoney.h @@ -1,704 +1,704 @@ /*************************************************************************** kmymoney.h ------------------- copyright : (C) 2000-2001 by Michael Edwardes (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. * * * ***************************************************************************/ #ifndef KMYMONEY_H #define KMYMONEY_H // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include // ---------------------------------------------------------------------------- // Project Includes #include #include "kmymoneyutils.h" #include "mymoneyaccount.h" #include "mymoney/onlinejob.h" #include "onlinejobtyped.h" #include "mymoneykeyvaluecontainer.h" #include "mymoneymoney.h" #include "selectedtransactions.h" #include "mymoneysplit.h" #include "mymoneytransaction.h" #include "mymoneyenums.h" class QResizeEvent; class MyMoneyObject; class MyMoneyInstitution; class MyMoneyAccount; class MyMoneySecurity; class MyMoneyPayee; class MyMoneyPrice; class MyMoneyTag; class MyMoneySplit; class MyMoneyTransaction; class WebConnect; class creditTransfer; class IMyMoneyOperationsFormat; template class onlineJobTyped; namespace eDialogs { enum class ScheduleResultCode; } namespace eMenu { enum class Action; enum class Menu; } /*! \mainpage KMyMoney Main Page for API documentation. * * \section intro Introduction * * This is the API documentation for KMyMoney. It should be used as a reference * for KMyMoney developers and users who wish to see how KMyMoney works. This * documentation will be kept up-to-date as development progresses and should be * read for new features that have been developed in KMyMoney. */ /** * The base class for KMyMoney application windows. It sets up the main * window and reads the config file as well as providing a menubar, toolbar * and statusbar. * * @see KMyMoneyView * * @author Michael Edwardes 2000-2001 * @author Thomas Baumgart 2006-2008 * * @short Main application class. */ class KMyMoneyApp : public KXmlGuiWindow, public IMyMoneyProcessingCalendar { Q_OBJECT private Q_SLOTS: /** * Add a context menu to the list used by KMessageBox::informationList to display the consistency check results. */ void slotInstallConsistencyCheckContextMenu(); /** * Handle the context menu of the list used by KMessageBox::informationList to display the consistency check results. */ void slotShowContextMenuForConsistencyCheck(const QPoint &); protected Q_SLOTS: /** * This slot is intended to be used as part of auto saving. This is used when the * QTimer emits the timeout signal and simply checks that the file is dirty (has * received modifications to its contents), and call the appropriate method to * save the file. Furthermore, re-starts the timer (possibly not needed). * @author mvillarino 2005 * @see KMyMoneyApp::slotDataChanged() */ void slotAutoSave(); /** * This slot re-enables all message for which the "Don't show again" * option had been selected. */ void slotEnableMessages(); /** * Called to run performance test. */ void slotPerformanceTest(); /** * Called to generate the sql to create kmymoney database tables etc. */ void slotGenerateSql(); #ifdef KMM_DEBUG /** * Called when the user asks for file information. */ void slotFileFileInfo(); /** * Debugging only: turn on/off traces */ void slotToggleTraces(); #endif /** * Debugging only: turn on/off timers */ void slotToggleTimers(); /** * Called when the user asks for the personal information. */ void slotFileViewPersonal(); void slotLoadAccountTemplates(); void slotSaveAccountTemplates(); /** * Open up the application wide settings dialog. * * @see KSettingsDlg */ void slotSettings(); /** * Called to show credits window. */ void slotShowCredits(); /** * Called when the user wishes to backup the current file */ void slotBackupFile(); /** * Perform mount operation before making a backup of the current file */ void slotBackupMount(); /** * Perform the backup write operation */ bool slotBackupWriteFile(); /** * Perform unmount operation after making a backup of the current file */ void slotBackupUnmount(); /** * Finish backup of the current file */ void slotBackupFinish(); /** * Handle events on making a backup of the current file */ void slotBackupHandleEvents(); void slotShowTipOfTheDay(); void slotShowPreviousView(); void slotShowNextView(); /** * Calls the print logic for the current view */ void slotPrintView(); /** * Call this slot, if any configuration parameter has changed */ void slotUpdateConfiguration(const QString &dialogName); /** * This slot is used to start new features during the development cycle */ void slotNewFeature(); /** * This slot triggers an update of all views and restarts * a single shot timer to call itself again at beginning of * the next day. */ void slotDateChanged(); /** * This slot will be called when the engine data changed * and the application object needs to update its state. */ void slotDataChanged(); /** * This slot collects information for a new scheduled transaction * based on transaction @a t and @a occurrence and saves it in the engine. */ void slotScheduleNew(const MyMoneyTransaction& t, eMyMoney::Schedule::Occurrence occurrence = eMyMoney::Schedule::Occurrence::Monthly); void slotStatusProgressDone(); public: enum fileActions { preOpen, postOpen, preSave, postSave, preClose, postClose }; // Keep a note of the file type typedef enum _fileTypeE { KmmBinary = 0, // native, binary KmmXML, // native, XML KmmDb, // SQL database /* insert new native file types above this line */ MaxNativeFileType, /* and non-native types below */ GncXML // Gnucash XML } fileTypeE; /** * This method checks if there is at least one asset or liability account * in the current storage object. If not, it starts the new account wizard. */ void createInitialAccount(); /** * This method returns the last URL used or an empty URL * depending on the option setting if the last file should * be opened during startup or the open file dialog should * be displayed. * * @return URL of last opened file or empty if the program * should start with the open file dialog */ QUrl lastOpenedURL(); /** * construtor of KMyMoneyApp, calls all init functions to create the application. */ explicit KMyMoneyApp(QWidget* parent = 0); /** * Destructor */ ~KMyMoneyApp(); static void progressCallback(int current, int total, const QString&); void writeLastUsedDir(const QString& directory); QString readLastUsedDir() const; void writeLastUsedFile(const QString& fileName); QString readLastUsedFile() const; /** * Returns whether there is an importer available that can handle this file */ bool isImportableFile(const QUrl &url); /** * This method is used to update the caption of the application window. * It sets the caption to "filename [modified] - KMyMoney". * * @param skipActions if true, the actions will not be updated. This * is usually onyl required by some early calls when * these widgets are not yet created (the default is false). */ void updateCaption(bool skipActions = false); /** * This method returns a list of all 'other' dcop registered kmymoney processes. * It's a subset of the return of DCOPclient()->registeredApplications(). * * @retval QStringList of process ids */ QList instanceList() const; #ifdef KMM_DEBUG /** * Dump a list of the names of all defined KActions to stdout. */ void dumpActions() const; #endif /** * Popup the context menu with the respective @p containerName. * Valid container names are defined in kmymoneyui.rc */ void showContextMenu(const QString& containerName); void createAccount(MyMoneyAccount& newAccount, MyMoneyAccount& parentAccount, MyMoneyAccount& brokerageAccount, MyMoneyMoney openingBal); QString filename() const; QUrl filenameURL() const; void addToRecentFiles(const QUrl& url); QTimer* autosaveTimer(); /** * Checks if the file with the @a url already exists. If so, * the user is asked if he/she wants to override the file. * If the user's answer is negative, @p false will be returned. * @p true will be returned in all other cases. */ bool okToWriteFile(const QUrl &url); /** * Return pointer to the WebConnect object */ WebConnect* webConnect() const; /** * Call this to find out if the currently open file is a sql database * * @retval true file is database * @retval false file is serial */ bool isDatabase(); /** * Call this to find out if the currently open file is native KMM * * @retval true file is native * @retval false file is foreign */ bool isNativeFile(); bool fileOpen() const; protected: /** save general Options like all bar positions and status as well as the geometry and the recent file list to the configuration * file */ void saveOptions(); /** * Creates the interfaces necessary for the plugins to work. Therefore, * this method must be called prior to loadPlugins(). */ void createInterfaces(); /** * read general options again and initialize all variables like the recent file list */ void readOptions(); /** * Gets pointers for menus preset by KXMLGUIFactory * @return pointers for menus */ QHash initMenus(); /** * Initializes QActions (names, object names, icons, some connections, shortcuts) * @return pointers for actions */ QHash initActions(); /** initializes the dynamic menus (account selectors) */ void initDynamicMenus(); /** * sets up the statusbar for the main window by initialzing a statuslabel. */ void initStatusBar(); /** queryClose is called by KMainWindow on each closeEvent of a window. Against the * default implementation (only returns true), this calls saveModified() on the document object to ask if the document shall * be saved if Modified; on cancel the closeEvent is rejected. * The settings are saved using saveOptions() if we are about to close. * @see KMainWindow#queryClose * @see QWidget#closeEvent */ - virtual bool queryClose(); + bool queryClose() final override; void slotCheckSchedules(); - virtual void resizeEvent(QResizeEvent*); + void resizeEvent(QResizeEvent*) final override; void createSchedule(MyMoneySchedule newSchedule, MyMoneyAccount& newAccount); /** * This method preloads the holidays for the duration of the default forecast period */ void preloadHolidays(); public Q_SLOTS: void slotFileInfoDialog(); /** */ void slotFileNew(); /** open a file and load it into the document*/ void slotFileOpen(); bool isFileOpenedInAnotherInstance(const QUrl &url); /** opens a file from the recent files menu */ void slotFileOpenRecent(const QUrl &url); /** * saves the current document. If it has no name yet, the user * will be queried for it. * * @retval false save operation failed * @retval true save operation was successful */ bool slotFileSave(); /** * ask the user for the filename and save the current document * * @retval false save operation failed * @retval true save operation was successful */ bool slotFileSaveAs(); /** asks for saving if the file is modified, then closes the actual file and window */ void slotFileCloseWindow(); /** asks for saving if the file is modified, then closes the actual file */ void slotFileClose(); /** * closes all open windows by calling close() on each memberList item * until the list is empty, then quits the application. * If queryClose() returns false because the user canceled the * saveModified() dialog, the closing breaks. */ void slotFileQuit(); void slotFileConsistencyCheck(); /** * fires up the price table editor */ void slotPriceDialog(); /** * fires up the currency table editor */ void slotCurrencyDialog(); /** * dummy method needed just for initialization */ void slotShowTransactionDetail(); /** * Toggles the hide reconciled transactions setting */ void slotHideReconciledTransactions(); /** * Toggles the hide unused categories setting */ void slotHideUnusedCategories(); /** * Toggles the show all accounts setting */ void slotShowAllAccounts(); /** * changes the statusbar contents for the standard label permanently, * used to indicate current actions. Returns the previous value for * 'stacked' usage. * * @param text the text that is displayed in the statusbar */ QString slotStatusMsg(const QString &text); /** * This method changes the progress bar in the status line according * to the parameters @p current and @p total. The following special * cases exist: * * - current = -1 and total = -1 will reset the progress bar * - current = ?? and total != 0 will setup the 100% mark to @p total * - current = xx and total == 0 will set the percentage * * @param current the current value with respect to the initialised * 100% mark * @param total the total value (100%) */ void slotStatusProgressBar(int current, int total = 0); /** * Called to update stock and currency prices from the user menu */ void slotEquityPriceUpdate(); /** * This slot reparents account @p src to be a child of account @p dest * * @param src account to be reparented * @param dest new parent */ void slotReparentAccount(const MyMoneyAccount& src, const MyMoneyAccount& dest); /** * This slot reparents account @p src to be a held at institution @p dest * * @param src account to be reparented * @param dest new parent institution */ void slotReparentAccount(const MyMoneyAccount& src, const MyMoneyInstitution& dest); /** * Create a new investment in a given @p parent investment account */ void slotInvestmentNew(MyMoneyAccount& account, const MyMoneyAccount& parent); /** * Brings up the new category editor and saves the information. * The dialog will be preset with the name and parent account. * * @param account reference of category to be created. The @p name member * should be filled by the caller. The object will be filled * with additional information during the creation process * esp. the @p id member. * @param parent reference to parent account (defaults to none) */ void slotCategoryNew(MyMoneyAccount& account, const MyMoneyAccount& parent); void slotCategoryNew(MyMoneyAccount& account); /** */ void slotPayeeNew(const QString& newnameBase, QString& id); /** * This slot fires up the KCalc application */ void slotToolsStartKCalc(); void slotResetSelections(); /** * Brings up the new account wizard and saves the information. */ void slotAccountNew(MyMoneyAccount&); /** * This method updates all KAction items to the current state. */ void slotUpdateActions(); void webConnect(const QString& sourceUrl, const QByteArray &asn_id); void webConnect(const QUrl url) { webConnect(url.path(), QByteArray()); } private: /** * This method sets the holidayRegion for use by the processing calendar. */ void setHolidayRegion(const QString& holidayRegion); /** * Load the status bar with the 'ready' message. This is hold in a single * place, so that is consistent with isReady(). */ void ready(); /** * Check if the status bar contains the 'ready' message. The return * value is used e.g. to detect if a quit operation is allowed or not. * * @retval true application is idle * @retval false application is active working on a longer operation */ bool isReady(); /** * Re-implemented from IMyMoneyProcessingCalendar */ - bool isProcessingDate(const QDate& date) const; + bool isProcessingDate(const QDate& date) const final override; /** * Depending on the setting of AutoSaveOnQuit, this method * asks the user to save the file or not. * * @returns see return values of KMessageBox::warningYesNoCancel() */ int askSaveOnClose(); Q_SIGNALS: /** * This signal is emitted when a new file is loaded. In the case file * is closed, this signal is also emitted with an empty url. */ void fileLoaded(const QUrl &url); /** * This signal is emitted when a transaction/list of transactions has been selected by * the GUI. If no transaction is selected or the selection is removed, * @p transactions is identical to an empty QList. This signal is used * by plugins to get information about changes. */ void transactionsSelected(const KMyMoneyRegister::SelectedTransactions& transactions); /** * This signal is emitted if a specific transaction with @a transactionId in account * @a accountId is selected */ void transactionSelected(const QString accountId, const QString& transactionId); /** * This signal is sent out, when the user presses Ctrl+A or activates * the Select all transactions action. */ void selectAllTransactions(); /** * This signal is emitted when a new institution has been selected by * the GUI. If no institution is selected or the selection is removed, * @a institution is identical to MyMoneyInstitution(). This signal is used * by plugins to get information about changes. */ void institutionSelected(const MyMoneyInstitution& institution); /** * This signal is emitted when a new schedule has been selected by * the GUI. If no schedule is selected or the selection is removed, * @a schedule is identical to MyMoneySchedule(). This signal is used * by plugins to get information about changes. */ void scheduleSelected(const MyMoneySchedule& schedule); void startMatchTransaction(const MyMoneyTransaction& t); void cancelMatchTransaction(); void kmmFilePlugin(unsigned int); public: bool isActionToggled(const eMenu::Action _a); static const QHash s_Actions; private: /// \internal d-pointer class. class Private; /* * Actually, one should write "Private * const d" but that confuses the KIDL * compiler in this context. It complains about the const keyword. So we leave * it out here */ /// \internal d-pointer instance. Private* d; }; extern KMyMoneyApp *kmymoney; class KMStatus { public: explicit KMStatus(const QString &text); ~KMStatus(); private: QString m_prevText; }; #define KMSTATUS(msg) KMStatus _thisStatus(msg) #endif // KMYMONEY_H diff --git a/kmymoney/misc/charvalidator.h b/kmymoney/misc/charvalidator.h index 542f33428..fca3f9102 100644 --- a/kmymoney/misc/charvalidator.h +++ b/kmymoney/misc/charvalidator.h @@ -1,38 +1,38 @@ /* * This file is part of KMyMoney, A Personal Finance Manager by KDE * Copyright (C) 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 CHARVALIDATOR_H #define CHARVALIDATOR_H #include class charValidator : public QValidator { Q_OBJECT public: explicit charValidator(QObject* parent = 0, const QString& characters = QString()); - virtual QValidator::State validate(QString& , int&) const; + QValidator::State validate(QString& , int&) const final override; void setAllowedCharacters(const QString&); private: QString m_allowedCharacters; }; #endif // CHARVALIDATOR_H diff --git a/kmymoney/models/accountsmodel.cpp b/kmymoney/models/accountsmodel.cpp index f6b3ea9c7..52f2bae20 100644 --- a/kmymoney/models/accountsmodel.cpp +++ b/kmymoney/models/accountsmodel.cpp @@ -1,1256 +1,1256 @@ /*************************************************************************** * Copyright 2010 Cristian Onet onet.cristian@gmail.com * * Copyright 2017, 2018 Łukasz Wojniłowicz lukasz.wojnilowicz@gmail.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) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of version 3 of the license. * * * * 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 "accountsmodel.h" // ---------------------------------------------------------------------------- // QT Includes #include // ---------------------------------------------------------------------------- // KDE Includes #include // ---------------------------------------------------------------------------- // Project Includes #include "mymoneyutils.h" #include "mymoneymoney.h" #include "mymoneyexception.h" #include "mymoneyfile.h" #include "mymoneyinstitution.h" #include "mymoneyaccount.h" #include "mymoneysecurity.h" #include "mymoneyprice.h" #include "kmymoneysettings.h" #include "icons.h" #include "modelenums.h" #include "mymoneyenums.h" #include "viewenums.h" using namespace Icons; using namespace eAccountsModel; using namespace eMyMoney; class AccountsModelPrivate { Q_DECLARE_PUBLIC(AccountsModel) public: /** * The pimpl. */ AccountsModelPrivate(AccountsModel *qq) : q_ptr(qq), m_file(MyMoneyFile::instance()) { m_columns.append(Column::Account); } virtual ~AccountsModelPrivate() { } void init() { Q_Q(AccountsModel); QStringList headerLabels; for (const auto& column : m_columns) headerLabels.append(q->getHeaderName(column)); q->setHorizontalHeaderLabels(headerLabels); } void loadPreferredAccount(const MyMoneyAccount &acc, QStandardItem *fromNode /*accounts' regular node*/, const int row, QStandardItem *toNode /*accounts' favourite node*/) { if (acc.value(QStringLiteral("PreferredAccount")) != QLatin1String("Yes")) return; auto favRow = toNode->rowCount(); if (auto favItem = itemFromAccountId(toNode, acc.id())) favRow = favItem->row(); for (auto i = 0; i < fromNode->columnCount(); ++i) { auto itemToClone = fromNode->child(row, i); if (itemToClone) toNode->setChild(favRow, i, itemToClone->clone()); } } /** * Load all the sub-accounts recursively. * * @param model The model in which to load the data. * @param accountsItem The item from the model of the parent account of the sub-accounts which are being loaded. * @param favoriteAccountsItem The item of the favorites accounts groups so favorite accounts can be added here also. * @param list The list of the account id's of the sub-accounts which are being loaded. * */ void loadSubaccounts(QStandardItem *node, QStandardItem *favoriteAccountsItem, const QStringList& subaccounts) { for (const auto& subaccStr : subaccounts) { const auto subacc = m_file->account(subaccStr); auto item = new QStandardItem(subacc.name()); // initialize first column of subaccount node->appendRow(item); // add subaccount row to node item->setEditable(false); item->setData(node->data((int)Role::DisplayOrder), (int)Role::DisplayOrder); // inherit display order role from node loadSubaccounts(item, favoriteAccountsItem, subacc.accountList()); // subaccount may have subaccounts as well // set the account data after the children have been loaded const auto row = item->row(); setAccountData(node, row, subacc, m_columns); // initialize rest of columns of subaccount loadPreferredAccount(subacc, node, row, favoriteAccountsItem); // add to favourites node if preferred } } /** * Note: this functions should only be called after the child account data has been set. */ void setAccountData(QStandardItem *node, const int row, const MyMoneyAccount &account, const QList &columns) { QStandardItem *cell; auto getCell = [&, row](const auto column) { cell = node->child(row, column); // try to get QStandardItem if (!cell) { // it may be uninitialized cell = new QStandardItem; // so create one node->setChild(row, column, cell); // and add it under the node } }; auto colNum = m_columns.indexOf(Column::Account); if (colNum == -1) return; getCell(colNum); auto font = cell->data(Qt::FontRole).value(); // display the names of closed accounts with strikeout font if (account.isClosed() != font.strikeOut()) font.setStrikeOut(account.isClosed()); if (columns.contains(Column::Account)) { // setting account column cell->setData(account.name(), Qt::DisplayRole); // cell->setData(QVariant::fromValue(account), (int)Role::Account); // is set in setAccountBalanceAndValue cell->setData(QVariant(account.id()), (int)Role::ID); cell->setData(QVariant(account.value("PreferredAccount") == QLatin1String("Yes")), (int)Role::Favorite); cell->setData(QVariant(QIcon(account.accountPixmap(m_reconciledAccount.id().isEmpty() ? false : account.id() == m_reconciledAccount.id()))), Qt::DecorationRole); cell->setData(MyMoneyFile::instance()->accountToCategory(account.id(), true), (int)Role::FullName); cell->setData(font, Qt::FontRole); } // Type if (columns.contains(Column::Type)) { colNum = m_columns.indexOf(Column::Type); if (colNum != -1) { getCell(colNum); cell->setData(account.accountTypeToString(account.accountType()), Qt::DisplayRole); cell->setData(font, Qt::FontRole); } } // Account's number if (columns.contains(Column::AccountNumber)) { colNum = m_columns.indexOf(Column::AccountNumber); if (colNum != -1) { getCell(colNum); cell->setData(account.number(), Qt::DisplayRole); cell->setData(font, Qt::FontRole); } } // Account's sort code if (columns.contains(Column::AccountSortCode)) { colNum = m_columns.indexOf(Column::AccountSortCode); if (colNum != -1) { getCell(colNum); cell->setData(account.value("iban"), Qt::DisplayRole); cell->setData(font, Qt::FontRole); } } const auto checkMark = Icons::get(Icon::DialogOK); switch (account.accountType()) { case Account::Type::Income: case Account::Type::Expense: case Account::Type::Asset: case Account::Type::Liability: // Tax if (columns.contains(Column::Tax)) { colNum = m_columns.indexOf(Column::Tax); if (colNum != -1) { getCell(colNum); if (account.value("Tax").toLower() == "yes") cell->setData(checkMark, Qt::DecorationRole); else cell->setData(QIcon(), Qt::DecorationRole); } } // VAT Account if (columns.contains(Column::VAT)) { colNum = m_columns.indexOf(Column::VAT); if (colNum != -1) { getCell(colNum); if (!account.value("VatAccount").isEmpty()) { const auto vatAccount = MyMoneyFile::instance()->account(account.value("VatAccount")); cell->setData(vatAccount.name(), Qt::DisplayRole); cell->setData(QVariant(Qt::AlignLeft | Qt::AlignVCenter), Qt::TextAlignmentRole); // VAT Rate } else if (!account.value("VatRate").isEmpty()) { const auto vatRate = MyMoneyMoney(account.value("VatRate")) * MyMoneyMoney(100, 1); cell->setData(QString::fromLatin1("%1 %").arg(vatRate.formatMoney(QString(), 1)), Qt::DisplayRole); cell->setData(QVariant(Qt::AlignRight | Qt::AlignVCenter), Qt::TextAlignmentRole); } else { cell->setData(QString(), Qt::DisplayRole); } } } // CostCenter if (columns.contains(Column::CostCenter)) { colNum = m_columns.indexOf(Column::CostCenter); if (colNum != -1) { getCell(colNum); if (account.isCostCenterRequired()) cell->setData(checkMark, Qt::DecorationRole); else cell->setData(QIcon(), Qt::DecorationRole); } } break; default: break; } // balance and value setAccountBalanceAndValue(node, row, account, columns); } void setInstitutionTotalValue(QStandardItem *node, const int row) { const auto colInstitution = m_columns.indexOf(Column::Account); auto itInstitution = node->child(row, colInstitution); const auto valInstitution = childrenTotalValue(itInstitution, true); itInstitution->setData(QVariant::fromValue(valInstitution ), (int)Role::TotalValue); const auto colTotalValue = m_columns.indexOf(Column::TotalValue); if (colTotalValue == -1) return; auto cell = node->child(row, colTotalValue); if (!cell) { cell = new QStandardItem; node->setChild(row, colTotalValue, cell); } const auto fontColor = KMyMoneySettings::schemeColor(valInstitution.isNegative() ? SchemeColor::Negative : SchemeColor::Positive); cell->setData(QVariant(fontColor), Qt::ForegroundRole); cell->setData(QVariant(itInstitution->data(Qt::FontRole).value()), Qt::FontRole); cell->setData(QVariant(Qt::AlignRight | Qt::AlignVCenter), Qt::TextAlignmentRole); cell->setData(MyMoneyUtils::formatMoney(valInstitution, m_file->baseCurrency()), Qt::DisplayRole); } void setAccountBalanceAndValue(QStandardItem *node, const int row, const MyMoneyAccount &account, const QList &columns) { QStandardItem *cell; auto getCell = [&, row](auto column) { cell = node->child(row, column); if (!cell) { cell = new QStandardItem; node->setChild(row, column, cell); } }; // setting account column auto colNum = m_columns.indexOf(Column::Account); if (colNum == -1) return; getCell(colNum); MyMoneyMoney accountBalance, accountValue, accountTotalValue; if (columns.contains(Column::Account)) { // update values only when requested accountBalance = balance(account); accountValue = value(account, accountBalance); accountTotalValue = childrenTotalValue(cell) + accountValue; cell->setData(QVariant::fromValue(account), (int)Role::Account); cell->setData(QVariant::fromValue(accountBalance), (int)Role::Balance); cell->setData(QVariant::fromValue(accountValue), (int)Role::Value); cell->setData(QVariant::fromValue(accountTotalValue), (int)Role::TotalValue); } else { // otherwise save up on tedious calculations accountBalance = cell->data((int)Role::Balance).value(); accountValue = cell->data((int)Role::Value).value(); accountTotalValue = cell->data((int)Role::TotalValue).value(); } const auto font = QVariant(cell->data(Qt::FontRole).value()); const auto alignment = QVariant(Qt::AlignRight | Qt::AlignVCenter); // setting total balance column if (columns.contains(Column::TotalBalance)) { colNum = m_columns.indexOf(Column::TotalBalance); if (colNum != -1) { const auto accountBalanceStr = QVariant::fromValue(MyMoneyUtils::formatMoney(accountBalance, m_file->security(account.currencyId()))); getCell(colNum); // only show the balance, if its a different security/currency if (m_file->security(account.currencyId()) != m_file->baseCurrency()) { cell->setData(accountBalanceStr, Qt::DisplayRole); } cell->setData(font, Qt::FontRole); cell->setData(alignment, Qt::TextAlignmentRole); } } // setting posted value column if (columns.contains(Column::PostedValue)) { colNum = m_columns.indexOf(Column::PostedValue); if (colNum != -1) { const auto accountValueStr = QVariant::fromValue(MyMoneyUtils::formatMoney(accountValue, m_file->baseCurrency())); getCell(colNum); const auto fontColor = KMyMoneySettings::schemeColor(accountValue.isNegative() ? SchemeColor::Negative : SchemeColor::Positive); cell->setData(QVariant(fontColor), Qt::ForegroundRole); cell->setData(accountValueStr, Qt::DisplayRole); cell->setData(font, Qt::FontRole); cell->setData(alignment, Qt::TextAlignmentRole); } } // setting total value column if (columns.contains(Column::TotalValue)) { colNum = m_columns.indexOf(Column::TotalValue); if (colNum != -1) { const auto accountTotalValueStr = QVariant::fromValue(MyMoneyUtils::formatMoney(accountTotalValue, m_file->baseCurrency())); getCell(colNum); const auto fontColor = KMyMoneySettings::schemeColor(accountTotalValue.isNegative() ? SchemeColor::Negative : SchemeColor::Positive); cell->setData(accountTotalValueStr, Qt::DisplayRole); cell->setData(font, Qt::FontRole); cell->setData(QVariant(fontColor), Qt::ForegroundRole); cell->setData(alignment, Qt::TextAlignmentRole); } } } /** * Compute the balance of the given account. * * @param account The account for which the balance is being computed. */ MyMoneyMoney balance(const MyMoneyAccount &account) { MyMoneyMoney balance; // a closed account has a zero balance by definition if (!account.isClosed()) { // account.balance() is not compatable with stock accounts if (account.isInvest()) balance = m_file->balance(account.id()); else balance = account.balance(); } // for income and liability accounts, we reverse the sign switch (account.accountGroup()) { case Account::Type::Income: case Account::Type::Liability: case Account::Type::Equity: balance = -balance; break; default: break; } return balance; } /** * Compute the value of the given account using the provided balance. * The value is defined as the balance of the account converted to the base currency. * * @param account The account for which the value is being computed. * @param balance The balance which should be used. * * @see balance */ MyMoneyMoney value(const MyMoneyAccount &account, const MyMoneyMoney &balance) { if (account.isClosed()) return MyMoneyMoney(); QList prices; MyMoneySecurity security = m_file->baseCurrency(); try { if (account.isInvest()) { security = m_file->security(account.currencyId()); prices += m_file->price(account.currencyId(), security.tradingCurrency()); if (security.tradingCurrency() != m_file->baseCurrency().id()) { MyMoneySecurity sec = m_file->security(security.tradingCurrency()); prices += m_file->price(sec.id(), m_file->baseCurrency().id()); } } else if (account.currencyId() != m_file->baseCurrency().id()) { security = m_file->security(account.currencyId()); prices += m_file->price(account.currencyId(), m_file->baseCurrency().id()); } } catch (const MyMoneyException &e) { qDebug() << Q_FUNC_INFO << " caught exception while adding " << account.name() << "[" << account.id() << "]: " << e.what(); } MyMoneyMoney value = balance; { QList::const_iterator it_p; - QString security = account.currencyId(); + QString securityID = account.currencyId(); for (it_p = prices.constBegin(); it_p != prices.constEnd(); ++it_p) { - value = (value * (MyMoneyMoney::ONE / (*it_p).rate(security))).convertPrecision(m_file->security(security).pricePrecision()); - if ((*it_p).from() == security) - security = (*it_p).to(); + value = (value * (MyMoneyMoney::ONE / (*it_p).rate(securityID))).convertPrecision(m_file->security(securityID).pricePrecision()); + if ((*it_p).from() == securityID) + securityID = (*it_p).to(); else - security = (*it_p).from(); + securityID = (*it_p).from(); } value = value.convert(m_file->baseCurrency().smallestAccountFraction()); } return value; } /** * Compute the total value of the child accounts of the given account. * Note that the value of the current account is not in this sum. Also, * before calling this function, the caller must make sure that the values * of all sub-account must be already in the model in the @ref Role::Value. * * @param index The index of the account in the model. * @see value */ MyMoneyMoney childrenTotalValue(const QStandardItem *node, const bool isInstitutionsModel = false) { MyMoneyMoney totalValue; if (!node) return totalValue; for (auto i = 0; i < node->rowCount(); ++i) { const auto childNode = node->child(i, (int)Column::Account); if (childNode->hasChildren()) totalValue += childrenTotalValue(childNode, isInstitutionsModel); const auto data = childNode->data((int)Role::Value); if (data.isValid()) { auto value = data.value(); if (isInstitutionsModel) { const auto account = childNode->data((int)Role::Account).value(); if (account.accountGroup() == Account::Type::Liability) value = -value; } totalValue += value; } } return totalValue; } /** * Function to get the item from an account id. * * @param parent The parent to localize the search in the child items of this parameter. * @param accountId Search based on this parameter. * * @return The item corresponding to the given account id, NULL if the account was not found. */ QStandardItem *itemFromAccountId(QStandardItem *parent, const QString &accountId) { auto const model = parent->model(); const auto list = model->match(model->index(0, 0, parent->index()), (int)Role::ID, QVariant(accountId), 1, Qt::MatchFlags(Qt::MatchExactly | Qt::MatchCaseSensitive)); if (!list.isEmpty()) return model->itemFromIndex(list.front()); // TODO: if not found at this item search for it in the model and if found reparent it. return nullptr; } /** * Function to get the item from an account id without knowing it's parent item. * Note that for the accounts which have two items in the model (favorite accounts) * the account item which is not the child of the favorite accounts item is always returned. * * @param model The model in which to search. * @param accountId Search based on this parameter. * * @return The item corresponding to the given account id, NULL if the account was not found. */ QStandardItem *itemFromAccountId(QStandardItemModel *model, const QString &accountId) { const auto list = model->match(model->index(0, 0), (int)Role::ID, QVariant(accountId), -1, Qt::MatchFlags(Qt::MatchExactly | Qt::MatchCaseSensitive | Qt::MatchRecursive)); for (const auto& index : list) { // always return the account which is not the child of the favorite accounts item if (index.parent().data((int)Role::ID).toString() != AccountsModel::favoritesAccountId) return model->itemFromIndex(index); } return nullptr; } AccountsModel *q_ptr; /** * Used to load the accounts data. */ MyMoneyFile *m_file; /** * Used to emit the @ref netWorthChanged signal. */ MyMoneyMoney m_lastNetWorth; /** * Used to emit the @ref profitChanged signal. */ MyMoneyMoney m_lastProfit; /** * Used to set the reconciliation flag. */ MyMoneyAccount m_reconciledAccount; QList m_columns; static const QString m_accountsModelConfGroup; static const QString m_accountsModelColumnSelection; }; const QString AccountsModelPrivate::m_accountsModelConfGroup = QStringLiteral("AccountsModel"); const QString AccountsModelPrivate::m_accountsModelColumnSelection = QStringLiteral("ColumnSelection"); const QString AccountsModel::favoritesAccountId(QStringLiteral("Favorites")); /** * The constructor is private so that only the @ref Models object can create such an object. */ AccountsModel::AccountsModel(QObject *parent) : QStandardItemModel(parent), d_ptr(new AccountsModelPrivate(this)) { Q_D(AccountsModel); d->init(); } AccountsModel::AccountsModel(AccountsModelPrivate &dd, QObject *parent) : QStandardItemModel(parent), d_ptr(&dd) { Q_D(AccountsModel); d->init(); } AccountsModel::~AccountsModel() { Q_D(AccountsModel); delete d; } /** * Perform the initial load of the model data * from the @ref MyMoneyFile. * */ void AccountsModel::load() { Q_D(AccountsModel); this->blockSignals(true); QStandardItem *rootItem = invisibleRootItem(); QFont font; font.setBold(true); // adding favourite accounts node auto favoriteAccountsItem = new QStandardItem(); favoriteAccountsItem->setEditable(false); rootItem->appendRow(favoriteAccountsItem); { QMap itemData; itemData[Qt::DisplayRole] = itemData[Qt::EditRole] = itemData[(int)Role::FullName] = i18n("Favorites"); itemData[Qt::FontRole] = font; itemData[Qt::DecorationRole] = Icons::get(Icon::ViewBankAccount); itemData[(int)Role::ID] = favoritesAccountId; itemData[(int)Role::DisplayOrder] = 0; this->setItemData(favoriteAccountsItem->index(), itemData); } // adding account categories (asset, liability, etc.) node const QVector categories { Account::Type::Asset, Account::Type::Liability, Account::Type::Income, Account::Type::Expense, Account::Type::Equity }; for (const auto category : categories) { MyMoneyAccount account; QString accountName; int displayOrder; switch (category) { case Account::Type::Asset: // Asset accounts account = d->m_file->asset(); accountName = i18n("Asset accounts"); displayOrder = 1; break; case Account::Type::Liability: // Liability accounts account = d->m_file->liability(); accountName = i18n("Liability accounts"); displayOrder = 2; break; case Account::Type::Income: // Income categories account = d->m_file->income(); accountName = i18n("Income categories"); displayOrder = 3; break; case Account::Type::Expense: // Expense categories account = d->m_file->expense(); accountName = i18n("Expense categories"); displayOrder = 4; break; case Account::Type::Equity: // Equity accounts account = d->m_file->equity(); accountName = i18n("Equity accounts"); displayOrder = 5; break; default: continue; } auto accountsItem = new QStandardItem(accountName); accountsItem->setEditable(false); rootItem->appendRow(accountsItem); { QMap itemData; itemData[Qt::DisplayRole] = accountName; itemData[(int)Role::FullName] = itemData[Qt::EditRole] = QVariant::fromValue(MyMoneyFile::instance()->accountToCategory(account.id(), true)); itemData[Qt::FontRole] = font; itemData[(int)Role::DisplayOrder] = displayOrder; this->setItemData(accountsItem->index(), itemData); } // adding accounts (specific bank/investment accounts) belonging to given accounts category for (const auto& accStr : account.accountList()) { const auto acc = d->m_file->account(accStr); auto item = new QStandardItem(acc.name()); accountsItem->appendRow(item); item->setEditable(false); auto subaccountsStr = acc.accountList(); // filter out stocks with zero balance if requested by user for (auto subaccStr = subaccountsStr.begin(); subaccStr != subaccountsStr.end();) { const auto subacc = d->m_file->account(*subaccStr); if (subacc.isInvest() && KMyMoneySettings::hideZeroBalanceEquities() && subacc.balance().isZero()) subaccStr = subaccountsStr.erase(subaccStr); else ++subaccStr; } // adding subaccounts (e.g. stocks under given investment account) belonging to given account d->loadSubaccounts(item, favoriteAccountsItem, subaccountsStr); const auto row = item->row(); d->setAccountData(accountsItem, row, acc, d->m_columns); d->loadPreferredAccount(acc, accountsItem, row, favoriteAccountsItem); } d->setAccountData(rootItem, accountsItem->row(), account, d->m_columns); } checkNetWorth(); checkProfit(); this->blockSignals(false); } QModelIndex AccountsModel::accountById(const QString& id) const { QModelIndexList accountList = match(index(0, 0), (int)Role::ID, id, 1, Qt::MatchFlags(Qt::MatchExactly | Qt::MatchRecursive)); if(accountList.count() == 1) { return accountList.first(); } return QModelIndex(); } QList *AccountsModel::getColumns() { Q_D(AccountsModel); return &d->m_columns; } void AccountsModel::setColumnVisibility(const Column column, const bool show) { Q_D(AccountsModel); const auto ixCol = d->m_columns.indexOf(column); // get column index in our column's map if (!show && ixCol != -1) { // start removing column row by row from bottom to up d->m_columns.removeOne(column); // remove it from our column's map blockSignals(true); // block signals to not emit resources consuming dataChanged for (auto i = 0; i < rowCount(); ++i) { // recursive lambda function to remove cell belonging to unwanted column from all rows auto removeCellFromRow = [=](auto &&self, QStandardItem *item) -> bool { for(auto j = 0; j < item->rowCount(); ++j) { auto childItem = item->child(j); if (childItem->hasChildren()) self(self, childItem); childItem->removeColumn(ixCol); } return true; }; auto topItem = item(i); if (topItem->hasChildren()) removeCellFromRow(removeCellFromRow, topItem); topItem->removeColumn(ixCol); } blockSignals(false); // unblock signals, so model can update itself with new column removeColumn(ixCol); // remove column from invisible root item which triggers model's view update } else if (show && ixCol == -1) { // start inserting columns row by row from up to bottom (otherwise columns will be inserted automatically) auto model = qobject_cast(this); const auto isInstitutionsModel = model ? true : false; // if it's institution's model, then don't set any data on institution nodes auto newColPos = 0; for(; newColPos < d->m_columns.count(); ++newColPos) { if (d->m_columns.at(newColPos) > column) break; } d->m_columns.insert(newColPos, column); // insert columns according to enum order for cleanliness insertColumn(newColPos); setHorizontalHeaderItem(newColPos, new QStandardItem(getHeaderName(column))); blockSignals(true); for (auto i = 0; i < rowCount(); ++i) { // recursive lambda function to remove cell belonging to unwanted column from all rows auto addCellToRow = [&, newColPos](auto &&self, QStandardItem *item) -> bool { for(auto j = 0; j < item->rowCount(); ++j) { auto childItem = item->child(j); childItem->insertColumns(newColPos, 1); if (childItem->hasChildren()) self(self, childItem); d->setAccountData(item, j, childItem->data((int)Role::Account).value(), QList {column}); } return true; }; auto topItem = item(i); topItem->insertColumns(newColPos, 1); if (topItem->hasChildren()) addCellToRow(addCellToRow, topItem); if (isInstitutionsModel) d->setInstitutionTotalValue(invisibleRootItem(), i); else if (i !=0) // favourites node doesn't play well here, so exclude it from update d->setAccountData(invisibleRootItem(), i, topItem->data((int)Role::Account).value(), QList {column}); } blockSignals(false); } } QString AccountsModel::getHeaderName(const Column column) { switch(column) { case Column::Account: return i18n("Account"); case Column::Type: return i18n("Type"); case Column::Tax: return i18nc("Column heading for category in tax report", "Tax"); case Column::VAT: return i18nc("Column heading for VAT category", "VAT"); case Column::CostCenter: return i18nc("Column heading for Cost Center", "CC"); case Column::TotalBalance: return i18n("Total Balance"); case Column::PostedValue: return i18n("Posted Value"); case Column::TotalValue: return i18n("Total Value"); case Column::AccountNumber: return i18n("Number"); case Column::AccountSortCode: return i18nc("IBAN, SWIFT, etc.", "Sort Code"); default: return QString(); } } /** * Check if netWorthChanged should be emitted. */ void AccountsModel::checkNetWorth() { Q_D(AccountsModel); // compute the net woth QModelIndexList assetList = match(index(0, 0), (int)Role::ID, MyMoneyFile::instance()->asset().id(), 1, Qt::MatchFlags(Qt::MatchExactly | Qt::MatchCaseSensitive)); QModelIndexList liabilityList = match(index(0, 0), (int)Role::ID, MyMoneyFile::instance()->liability().id(), 1, Qt::MatchFlags(Qt::MatchExactly | Qt::MatchCaseSensitive)); MyMoneyMoney netWorth; if (!assetList.isEmpty() && !liabilityList.isEmpty()) { const auto assetValue = data(assetList.front(), (int)Role::TotalValue); const auto liabilityValue = data(liabilityList.front(), (int)Role::TotalValue); if (assetValue.isValid() && liabilityValue.isValid()) netWorth = assetValue.value() - liabilityValue.value(); } if (d->m_lastNetWorth != netWorth) { d->m_lastNetWorth = netWorth; emit netWorthChanged(QVariantList {QVariant::fromValue(d->m_lastNetWorth)}, eView::Intent::UpdateNetWorth); } } /** * Check if profitChanged should be emitted. */ void AccountsModel::checkProfit() { Q_D(AccountsModel); // compute the profit const auto incomeList = match(index(0, 0), (int)Role::ID, MyMoneyFile::instance()->income().id(), 1, Qt::MatchFlags(Qt::MatchExactly | Qt::MatchCaseSensitive)); const auto expenseList = match(index(0, 0), (int)Role::ID, MyMoneyFile::instance()->expense().id(), 1, Qt::MatchFlags(Qt::MatchExactly | Qt::MatchCaseSensitive)); MyMoneyMoney profit; if (!incomeList.isEmpty() && !expenseList.isEmpty()) { const auto incomeValue = data(incomeList.front(), (int)Role::TotalValue); const auto expenseValue = data(expenseList.front(), (int)Role::TotalValue); if (incomeValue.isValid() && expenseValue.isValid()) profit = incomeValue.value() - expenseValue.value(); } if (d->m_lastProfit != profit) { d->m_lastProfit = profit; emit profitChanged(QVariantList {QVariant::fromValue(d->m_lastProfit)}, eView::Intent::UpdateProfit); } } MyMoneyMoney AccountsModel::accountValue(const MyMoneyAccount &account, const MyMoneyMoney &balance) { Q_D(AccountsModel); return d->value(account, balance); } /** * This slot should be connected so that the model will be notified which account is being reconciled. */ void AccountsModel::slotReconcileAccount(const MyMoneyAccount &account, const QDate &reconciliationDate, const MyMoneyMoney &endingBalance) { Q_D(AccountsModel); Q_UNUSED(reconciliationDate) Q_UNUSED(endingBalance) if (d->m_reconciledAccount.id() != account.id()) { // first clear the flag of the old reconciliation account if (!d->m_reconciledAccount.id().isEmpty()) { const auto list = match(index(0, 0), (int)Role::ID, QVariant(d->m_reconciledAccount.id()), -1, Qt::MatchFlags(Qt::MatchExactly | Qt::MatchCaseSensitive | Qt::MatchRecursive)); for (const auto& index : list) setData(index, QVariant(QIcon(account.accountPixmap(false))), Qt::DecorationRole); } // then set the reconciliation flag of the new reconciliation account const auto list = match(index(0, 0), (int)Role::ID, QVariant(account.id()), -1, Qt::MatchFlags(Qt::MatchExactly | Qt::MatchCaseSensitive | Qt::MatchRecursive)); for (const auto& index : list) setData(index, QVariant(QIcon(account.accountPixmap(true))), Qt::DecorationRole); d->m_reconciledAccount = account; } } /** * Notify the model that an object has been added. An action is performed only if the object is an account. * */ void AccountsModel::slotObjectAdded(File::Object objType, const QString& id) { Q_D(AccountsModel); if (objType != File::Object::Account) return; const auto account = MyMoneyFile::instance()->account(id); auto favoriteAccountsItem = d->itemFromAccountId(this, favoritesAccountId); auto parentAccountItem = d->itemFromAccountId(this, account.parentAccountId()); auto item = d->itemFromAccountId(parentAccountItem, account.id()); if (!item) { item = new QStandardItem(account.name()); parentAccountItem->appendRow(item); item->setEditable(false); } // load the sub-accounts if there are any - there could be sub accounts if this is an add operation // that was triggered in slotObjectModified on an already existing account which went trough a hierarchy change d->loadSubaccounts(item, favoriteAccountsItem, account.accountList()); const auto row = item->row(); d->setAccountData(parentAccountItem, row, account, d->m_columns); d->loadPreferredAccount(account, parentAccountItem, row, favoriteAccountsItem); checkNetWorth(); checkProfit(); } /** * Notify the model that an object has been modified. An action is performed only if the object is an account. * */ void AccountsModel::slotObjectModified(File::Object objType, const QString& id) { Q_D(AccountsModel); if (objType != File::Object::Account) return; const auto account = MyMoneyFile::instance()->account(id); auto accountItem = d->itemFromAccountId(this, id); if (!accountItem) { qDebug() << "Unexpected null accountItem in AccountsModel::slotObjectModified"; return; } const auto oldAccount = accountItem->data((int)Role::Account).value(); if (oldAccount.parentAccountId() == account.parentAccountId()) { // the hierarchy did not change so update the account data auto parentAccountItem = accountItem->parent(); if (!parentAccountItem) parentAccountItem = this->invisibleRootItem(); const auto row = accountItem->row(); d->setAccountData(parentAccountItem, row, account, d->m_columns); // and the child of the favorite item if the account is a favorite account or it's favorite status has just changed if (auto favoriteAccountsItem = d->itemFromAccountId(this, favoritesAccountId)) { if (account.value("PreferredAccount") == QLatin1String("Yes")) d->loadPreferredAccount(account, parentAccountItem, row, favoriteAccountsItem); else if (auto favItem = d->itemFromAccountId(favoriteAccountsItem, account.id())) favoriteAccountsItem->removeRow(favItem->row()); // it's not favorite anymore } } else { // this means that the hierarchy was changed - simulate this with a remove followed by and add operation slotObjectRemoved(File::Object::Account, oldAccount.id()); slotObjectAdded(File::Object::Account, id); } checkNetWorth(); checkProfit(); } /** * Notify the model that an object has been removed. An action is performed only if the object is an account. * */ void AccountsModel::slotObjectRemoved(File::Object objType, const QString& id) { if (objType != File::Object::Account) return; const auto list = match(index(0, 0), (int)Role::ID, id, -1, Qt::MatchFlags(Qt::MatchExactly | Qt::MatchRecursive)); for (const auto& index : list) removeRow(index.row(), index.parent()); checkNetWorth(); checkProfit(); } /** * Notify the model that the account balance has been changed. */ void AccountsModel::slotBalanceOrValueChanged(const MyMoneyAccount &account) { Q_D(AccountsModel); auto itParent = d->itemFromAccountId(this, account.id()); // get node of account in model auto isTopLevel = false; // it could be top-level but we don't know it yet while (itParent && !isTopLevel) { // loop in which we set total values and balances from the bottom to the top auto itCurrent = itParent; const auto accCurrent = d->m_file->account(itCurrent->data((int)Role::Account).value().id()); if (accCurrent.id().isEmpty()) { // this is institution d->setInstitutionTotalValue(invisibleRootItem(), itCurrent->row()); break; // it's top-level node so nothing above that; } itParent = itCurrent->parent(); if (!itParent) { itParent = this->invisibleRootItem(); isTopLevel = true; } d->setAccountBalanceAndValue(itParent, itCurrent->row(), accCurrent, d->m_columns); } checkNetWorth(); checkProfit(); } /** * The pimpl of the @ref InstitutionsModel derived from the pimpl of the @ref AccountsModel. */ class InstitutionsModelPrivate : public AccountsModelPrivate { public: InstitutionsModelPrivate(InstitutionsModel *qq) : AccountsModelPrivate(qq) { } ~InstitutionsModelPrivate() override { } /** * Function to get the institution item from an institution id. * * @param model The model in which to look for the item. * @param institutionId Search based on this parameter. * * @return The item corresponding to the given institution id, NULL if the institution was not found. */ QStandardItem *institutionItemFromId(QStandardItemModel *model, const QString &institutionId) { const auto list = model->match(model->index(0, 0), (int)Role::ID, QVariant(institutionId), 1, Qt::MatchFlags(Qt::MatchExactly | Qt::MatchCaseSensitive)); if (!list.isEmpty()) return model->itemFromIndex(list.front()); return nullptr; // this should rarely fail as we add all institutions early on } /** * Function to add the account item to it's corresponding institution item. * * @param model The model where to add the item. * @param account The account for which to create the item. * */ void loadInstitution(QStandardItemModel *model, const MyMoneyAccount &account) { if (!account.isAssetLiability() && !account.isInvest()) return; // we've got account but don't know under which institution it should be added, so we find it out auto idInstitution = account.institutionId(); if (account.isInvest()) { // if it's stock account then... const auto investmentAccount = m_file->account(account.parentAccountId()); // ...get investment account it's under and... idInstitution = investmentAccount.institutionId(); // ...get institution from investment account } auto itInstitution = institutionItemFromId(model, idInstitution); auto itAccount = itemFromAccountId(itInstitution, account.id()); // check if account already exists under institution // only stock accounts are added to their parent in the institutions view // this makes hierarchy maintenance a lot easier since the stock accounts // are the only ones that always have the same institution as their parent auto itInvestmentAccount = account.isInvest() ? itemFromAccountId(itInstitution, account.parentAccountId()) : nullptr; if (!itAccount) { itAccount = new QStandardItem(account.name()); if (itInvestmentAccount) // stock account nodes go under investment account nodes and... itInvestmentAccount->appendRow(itAccount); else if (itInstitution) // ...the rest goes under institution's node itInstitution->appendRow(itAccount); else return; itAccount->setEditable(false); } if (itInvestmentAccount) { setAccountData(itInvestmentAccount, itAccount->row(), account, m_columns); // set data for stock account node setAccountData(itInstitution, itInvestmentAccount->row(), m_file->account(account.parentAccountId()), m_columns); // set data for investment account node } else if (itInstitution) { setAccountData(itInstitution, itAccount->row(), account, m_columns); } } /** * Function to add an institution item to the model. * * @param model The model in which to add the item. * @param institution The institution object which should be represented by the item. * */ void addInstitutionItem(QStandardItemModel *model, const MyMoneyInstitution &institution) { QFont font; font.setBold(true); auto itInstitution = new QStandardItem(Icons::get(Icon::ViewInstitutions), institution.name()); itInstitution->setFont(font); itInstitution->setData(QVariant::fromValue(MyMoneyMoney()), (int)Role::TotalValue); itInstitution->setData(institution.id(), (int)Role::ID); itInstitution->setData(QVariant::fromValue(institution), (int)Role::Account); itInstitution->setData(6, (int)Role::DisplayOrder); itInstitution->setEditable(false); model->invisibleRootItem()->appendRow(itInstitution); setInstitutionTotalValue(model->invisibleRootItem(), itInstitution->row()); } }; /** * The institution model contains the accounts grouped by institution. * */ InstitutionsModel::InstitutionsModel(QObject *parent) : AccountsModel(*new InstitutionsModelPrivate(this), parent) { } InstitutionsModel::~InstitutionsModel() { } /** * Perform the initial load of the model data * from the @ref MyMoneyFile. * */ void InstitutionsModel::load() { Q_D(InstitutionsModel); // create items for all the institutions auto institutionList = d->m_file->institutionList(); MyMoneyInstitution none; none.setName(i18n("Accounts with no institution assigned")); institutionList.append(none); for (const auto& institution : institutionList) // add all known institutions as top-level nodes d->addInstitutionItem(this, institution); QList accountsList; QList stocksList; d->m_file->accountList(accountsList); for (const auto& account : accountsList) { // add account nodes under institution nodes... if (account.isInvest()) // ...but wait with stocks until investment accounts appear stocksList.append(account); else d->loadInstitution(this, account); } for (const auto& stock : stocksList) { if (!(KMyMoneySettings::hideZeroBalanceEquities() && stock.balance().isZero())) d->loadInstitution(this, stock); } for (auto i = 0 ; i < rowCount(); ++i) d->setInstitutionTotalValue(invisibleRootItem(), i); } /** * Notify the model that an object has been added. An action is performed only if the object is an account or an institution. * */ void InstitutionsModel::slotObjectAdded(File::Object objType, const QString& id) { Q_D(InstitutionsModel); if (objType == File::Object::Institution) { // if an institution was added then add the item which will represent it const auto institution = MyMoneyFile::instance()->institution(id); d->addInstitutionItem(this, institution); } if (objType != File::Object::Account) return; // if an account was added then add the item which will represent it only for real accounts const auto account = MyMoneyFile::instance()->account(id); // nothing to do for root accounts and categories if (account.parentAccountId().isEmpty() || account.isIncomeExpense()) return; // load the account into the institution d->loadInstitution(this, account); // load the investment sub-accounts if there are any - there could be sub-accounts if this is an add operation // that was triggered in slotObjectModified on an already existing account which went trough a hierarchy change const auto sAccounts = account.accountList(); if (!sAccounts.isEmpty()) { QList subAccounts; d->m_file->accountList(subAccounts, sAccounts); for (const auto& subAccount : subAccounts) { if (subAccount.isInvest()) { d->loadInstitution(this, subAccount); } } } } /** * Notify the model that an object has been modified. An action is performed only if the object is an account or an institution. * */ void InstitutionsModel::slotObjectModified(File::Object objType, const QString& id) { Q_D(InstitutionsModel); if (objType == File::Object::Institution) { // if an institution was modified then modify the item which represents it const auto institution = MyMoneyFile::instance()->institution(id); if (auto institutionItem = d->institutionItemFromId(this, id)) { institutionItem->setData(institution.name(), Qt::DisplayRole); institutionItem->setData(QVariant::fromValue(institution), (int)Role::Account); institutionItem->setIcon(MyMoneyInstitution::pixmap()); } } if (objType != File::Object::Account) return; // if an account was modified then modify the item which represents it const auto account = MyMoneyFile::instance()->account(id); // nothing to do for root accounts, categories and equity accounts since they don't have a representation in this model if (account.parentAccountId().isEmpty() || account.isIncomeExpense() || account.accountType() == Account::Type::Equity) return; auto accountItem = d->itemFromAccountId(this, account.id()); const auto oldAccount = accountItem->data((int)Role::Account).value(); if (oldAccount.institutionId() == account.institutionId()) { // the hierarchy did not change so update the account data d->setAccountData(accountItem->parent(), accountItem->row(), account, d->m_columns); } else { // this means that the hierarchy was changed - simulate this with a remove followed by and add operation slotObjectRemoved(File::Object::Account, oldAccount.id()); slotObjectAdded(File::Object::Account, id); } } /** * Notify the model that an object has been removed. An action is performed only if the object is an account or an institution. * */ void InstitutionsModel::slotObjectRemoved(File::Object objType, const QString& id) { Q_D(InstitutionsModel); if (objType == File::Object::Institution) { // if an institution was removed then remove the item which represents it if (auto itInstitution = d->institutionItemFromId(this, id)) removeRow(itInstitution->row(), itInstitution->index().parent()); } if (objType != File::Object::Account) return; // if an account was removed then remove the item which represents it and recompute the institution's value auto itAccount = d->itemFromAccountId(this, id); if (!itAccount) return; // this could happen if the account isIncomeExpense const auto account = itAccount->data((int)Role::Account).value(); if (auto itInstitution = d->itemFromAccountId(this, account.institutionId())) { AccountsModel::slotObjectRemoved(objType, id); d->setInstitutionTotalValue(invisibleRootItem(), itInstitution->row()); } } diff --git a/kmymoney/models/costcentermodel.h b/kmymoney/models/costcentermodel.h index adb45ed0a..b5b6d3cbe 100644 --- a/kmymoney/models/costcentermodel.h +++ b/kmymoney/models/costcentermodel.h @@ -1,74 +1,74 @@ /*************************************************************************** costcentermodel.h ------------------- begin : Sun Dec 27 2015 copyright : (C) 2015 by Thomas Baumgart email : 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. * * * ***************************************************************************/ #ifndef COSTCENTERMODEL_H #define COSTCENTERMODEL_H // ---------------------------------------------------------------------------- // QT Includes #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes /** */ class CostCenterModel : public QAbstractListModel { Q_OBJECT public: explicit CostCenterModel(QObject* parent = 0); virtual ~CostCenterModel(); enum Roles { CostCenterIdRole = Qt::UserRole, // must remain Qt::UserRole due to KMyMoneyMVCCombo::selectedItem ShortNameRole, }; - virtual int rowCount(const QModelIndex& parent = QModelIndex()) const; - virtual int columnCount(const QModelIndex& parent = QModelIndex()) const; - virtual QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const; - virtual QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const; + int rowCount(const QModelIndex& parent = QModelIndex()) const final override; + int columnCount(const QModelIndex& parent = QModelIndex()) const final override; + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const final override; + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const final override; - virtual Qt::ItemFlags flags(const QModelIndex& index) const; - virtual bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole); + Qt::ItemFlags flags(const QModelIndex& index) const final override; + bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole) final override; /** * clears all objects currently in the model */ void unload(); /** * Loads the model with data from the engine */ void load(); public Q_SLOTS: private: struct Private; QScopedPointer d; }; #endif // COSTCENTERMODEL_H diff --git a/kmymoney/models/onlinebankingaccountsfilterproxymodel.h b/kmymoney/models/onlinebankingaccountsfilterproxymodel.h index 89975072d..5901e24c4 100644 --- a/kmymoney/models/onlinebankingaccountsfilterproxymodel.h +++ b/kmymoney/models/onlinebankingaccountsfilterproxymodel.h @@ -1,47 +1,47 @@ /* * This file is part of KMyMoney, A Personal Finance Manager by KDE * Copyright (C) 2014 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 ONLINEBANKINGACCOUNTSFILTERPROXYMODEL_H #define ONLINEBANKINGACCOUNTSFILTERPROXYMODEL_H #include "kmm_models_export.h" #include class KMM_MODELS_EXPORT OnlineBankingAccountsFilterProxyModel : public QSortFilterProxyModel { Q_OBJECT public: explicit OnlineBankingAccountsFilterProxyModel(QObject* parent = 0); /** * @brief Makes accounts which do not support any onlineJob non-selectable */ - virtual Qt::ItemFlags flags(const QModelIndex& index) const; + Qt::ItemFlags flags(const QModelIndex& index) const override; protected: - virtual bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const; + bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const final override; private: /** * @brief Has parent at least one visible child? */ bool filterAcceptsParent(const QModelIndex& index) const; }; #endif // ONLINEBANKINGACCOUNTSFILTERPROXYMODEL_H diff --git a/kmymoney/models/payeeidentifiercontainermodel.h b/kmymoney/models/payeeidentifiercontainermodel.h index 2a84fffff..a34fd0145 100644 --- a/kmymoney/models/payeeidentifiercontainermodel.h +++ b/kmymoney/models/payeeidentifiercontainermodel.h @@ -1,98 +1,98 @@ /* * This file is part of KMyMoney, A Personal Finance Manager by KDE * Copyright (C) 2014 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 PAYEEIDENTIFIERCONTAINERMODEL_H #define PAYEEIDENTIFIERCONTAINERMODEL_H #include "kmm_models_export.h" #include #include #include "mymoney/payeeidentifiermodel.h" #include "mymoney/mymoneypayeeidentifiercontainer.h" #include "payeeidentifier/payeeidentifier.h" /** * @brief Model for MyMoneyPayeeIdentifierContainer * * Changes the user does have initernal effect only. * * @see payeeIdentifierModel */ class MyMoneyPayeeIdentifierContainer; class payeeIdentifier; class KMM_MODELS_EXPORT payeeIdentifierContainerModel : public QAbstractListModel { Q_OBJECT public: /** * @brief Roles for this model * * They are equal to payeeIdentifierModel::roles */ enum roles { payeeIdentifierType = payeeIdentifierModel::payeeIdentifierType, /**< type of payeeIdentifier */ payeeIdentifier = payeeIdentifierModel::payeeIdentifier /**< actual payeeIdentifier */ }; explicit payeeIdentifierContainerModel(QObject* parent = 0); - virtual QVariant data(const QModelIndex& index, int role) const; + QVariant data(const QModelIndex& index, int role) const final override; /** * This model only supports to edit payeeIdentifier role with a QVariant of type * payeeIdentifier. */ - virtual bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole); + bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole) final override; - virtual Qt::ItemFlags flags(const QModelIndex& index) const; + Qt::ItemFlags flags(const QModelIndex& index) const final override; - virtual int rowCount(const QModelIndex& parent) const; + int rowCount(const QModelIndex& parent) const final override; - virtual bool insertRows(int row, int count, const QModelIndex& parent); - virtual bool removeRows(int row, int count, const QModelIndex& parent); + bool insertRows(int row, int count, const QModelIndex& parent) final override; + bool removeRows(int row, int count, const QModelIndex& parent) final override; /** * @brief Set source of data * * This makes the model editable. */ void setSource(MyMoneyPayeeIdentifierContainer data); /** @brief Get stored data */ QList< ::payeeIdentifier > identifiers() const; public Q_SLOTS: /** * @brief Removes all data from the model * * The model is not editable afterwards. */ void closeSource(); private: /** @internal * The use of a shared pointer makes this future prof. Because using identifier() causes * some unnecessary work. */ QSharedPointer m_data; }; #endif // PAYEEIDENTIFIERCONTAINERMODEL_H diff --git a/kmymoney/models/payeesmodel.h b/kmymoney/models/payeesmodel.h index fc59e3b4f..c73644763 100644 --- a/kmymoney/models/payeesmodel.h +++ b/kmymoney/models/payeesmodel.h @@ -1,73 +1,73 @@ /*************************************************************************** payeesmodel.h ------------------- begin : Mon Oct 03 2016 copyright : (C) 2016 by Thomas Baumgart email : 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. * * * ***************************************************************************/ #ifndef PAYEESMODEL_H #define PAYEESMODEL_H // ---------------------------------------------------------------------------- // QT Includes #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes /** */ class PayeesModel : public QAbstractListModel { Q_OBJECT public: explicit PayeesModel(QObject* parent = 0); - virtual ~PayeesModel(); + ~PayeesModel(); enum Roles { PayeeIdRole = Qt::UserRole, // must remain Qt::UserRole due to KMyMoneyMVCCombo::selectedItem, }; - virtual int rowCount(const QModelIndex& parent = QModelIndex()) const; - virtual int columnCount(const QModelIndex& parent = QModelIndex()) const; - virtual QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const; - virtual QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const; + int rowCount(const QModelIndex& parent = QModelIndex()) const final override; + int columnCount(const QModelIndex& parent = QModelIndex()) const final override; + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const final override; + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const final override; - virtual Qt::ItemFlags flags(const QModelIndex& index) const; - virtual bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole); + Qt::ItemFlags flags(const QModelIndex& index) const final override; + bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole) final override; /** * clears all objects currently in the model */ void unload(); /** * Loads the model with data from the engine */ void load(); public Q_SLOTS: private: struct Private; QScopedPointer d; }; #endif // PAYEESMODEL_H diff --git a/kmymoney/mymoney/mymoneyaccountloan.h b/kmymoney/mymoney/mymoneyaccountloan.h index 5ed144044..a434118f0 100644 --- a/kmymoney/mymoney/mymoneyaccountloan.h +++ b/kmymoney/mymoney/mymoneyaccountloan.h @@ -1,97 +1,97 @@ /*************************************************************************** mymoneyaccountloan.h ------------------- copyright : (C) 2002 by 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. * * * ***************************************************************************/ #ifndef MYMONEYACCOUNTLOAN_H #define MYMONEYACCOUNTLOAN_H // ---------------------------------------------------------------------------- // QT Includes // ---------------------------------------------------------------------------- // Project Includes #include "mymoneyaccount.h" /** * This class is a convenience class to access data for loan accounts. * It does contain the same member variables as a MyMoneyAccount object, * but serves a set of getter/setter methods to ease the access to * laon relevant data stored in the key value container of the MyMoneyAccount * object. */ class MyMoneyMoney; class KMM_MYMONEY_EXPORT MyMoneyAccountLoan : public MyMoneyAccount { public: enum interestDueE { paymentDue = 0, paymentReceived }; enum interestChangeUnitE { changeDaily = 0, changeWeekly, changeMonthly, changeYearly }; MyMoneyAccountLoan() {} MyMoneyAccountLoan(const MyMoneyAccount&); // krazy:exclude=explicit ~MyMoneyAccountLoan() {} const MyMoneyMoney loanAmount() const; void setLoanAmount(const MyMoneyMoney& amount); const MyMoneyMoney interestRate(const QDate& date) const; void setInterestRate(const QDate& date, const MyMoneyMoney& rate); interestDueE interestCalculation() const; void setInterestCalculation(const interestDueE onReception); const QDate nextInterestChange() const; void setNextInterestChange(const QDate& date); const QString schedule() const; void setSchedule(const QString& sched); bool fixedInterestRate() const; void setFixedInterestRate(const bool fixed); const MyMoneyMoney finalPayment() const; void setFinalPayment(const MyMoneyMoney& finalPayment); unsigned int term() const; void setTerm(const unsigned int payments); int interestChangeFrequency(int* unit = 0) const; void setInterestChangeFrequency(const int amount, const int unit); const MyMoneyMoney periodicPayment() const; void setPeriodicPayment(const MyMoneyMoney& payment); int interestCompounding() const; void setInterestCompounding(int frequency); const QString payee() const; void setPayee(const QString& payee); const QString interestAccountId() const; void setInterestAccountId(const QString& id); /** * 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; + bool hasReferenceTo(const QString& id) const final override; }; #endif diff --git a/kmymoney/mymoney/mymoneyfile.cpp b/kmymoney/mymoney/mymoneyfile.cpp index 09da4bf14..de70bfe8d 100644 --- a/kmymoney/mymoney/mymoneyfile.cpp +++ b/kmymoney/mymoney/mymoneyfile.cpp @@ -1,3470 +1,3469 @@ /*************************************************************************** mymoneyfile.cpp ------------------- copyright : (C) 2000 by Michael Edwardes (C) 2002, 2007-2011 by 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 "mymoneyfile.h" #include // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include // ---------------------------------------------------------------------------- // Project Includes #include "mymoneystoragemgr.h" #include "mymoneyinstitution.h" #include "mymoneyaccount.h" #include "mymoneyaccountloan.h" #include "mymoneysecurity.h" #include "mymoneyreport.h" #include "mymoneybalancecache.h" #include "mymoneybudget.h" #include "mymoneyprice.h" #include "mymoneypayee.h" #include "mymoneytag.h" #include "mymoneyschedule.h" #include "mymoneysplit.h" #include "mymoneytransaction.h" #include "mymoneycostcenter.h" #include "mymoneyexception.h" #include "onlinejob.h" #include "storageenums.h" #include "mymoneystoragenames.h" #include "mymoneyenums.h" // include the following line to get a 'cout' for debug purposes // #include using namespace eMyMoney; using namespace MyMoneyStandardAccounts; const QString MyMoneyFile::AccountSeparator = QChar(':'); MyMoneyFile MyMoneyFile::file; typedef QList > BalanceNotifyList; typedef QMap CacheNotifyList; /// @todo make this template based class MyMoneyNotification { public: MyMoneyNotification(File::Mode mode, const MyMoneyTransaction& t) : m_objType(File::Object::Transaction), m_notificationMode(mode), m_id(t.id()) { } MyMoneyNotification(File::Mode mode, const MyMoneyAccount& acc) : m_objType(File::Object::Account), m_notificationMode(mode), m_id(acc.id()) { } MyMoneyNotification(File::Mode mode, const MyMoneyInstitution& institution) : m_objType(File::Object::Institution), m_notificationMode(mode), m_id(institution.id()) { } MyMoneyNotification(File::Mode mode, const MyMoneyPayee& payee) : m_objType(File::Object::Payee), m_notificationMode(mode), m_id(payee.id()) { } MyMoneyNotification(File::Mode mode, const MyMoneyTag& tag) : m_objType(File::Object::Tag), m_notificationMode(mode), m_id(tag.id()) { } MyMoneyNotification(File::Mode mode, const MyMoneySchedule& schedule) : m_objType(File::Object::Schedule), m_notificationMode(mode), m_id(schedule.id()) { } MyMoneyNotification(File::Mode mode, const MyMoneySecurity& security) : m_objType(File::Object::Security), m_notificationMode(mode), m_id(security.id()) { } MyMoneyNotification(File::Mode mode, const onlineJob& job) : m_objType(File::Object::OnlineJob), m_notificationMode(mode), m_id(job.id()) { } File::Object objectType() const { return m_objType; } File::Mode notificationMode() const { return m_notificationMode; } const QString& id() const { return m_id; } protected: MyMoneyNotification(File::Object obj, File::Mode mode, const QString& id) : m_objType(obj), m_notificationMode(mode), m_id(id) {} private: File::Object m_objType; File::Mode m_notificationMode; QString m_id; }; class MyMoneyFile::Private { public: Private() : m_storage(0), m_inTransaction(false) {} ~Private() { delete m_storage; } /** * This method is used to add an id to the list of objects * to be removed from the cache. If id is empty, then nothing is added to the list. * * @param id id of object to be notified * @param reload reload the object (@c true) or not (@c false). The default is @c true * @see attach, detach */ void addCacheNotification(const QString& id, const QDate& date) { if (!id.isEmpty()) m_balanceNotifyList.append(std::make_pair(id, date)); } /** * This method is used to clear the notification list */ void clearCacheNotification() { // reset list to be empty m_balanceNotifyList.clear(); } /** * This method is used to clear all * objects mentioned in m_notificationList from the cache. */ void notify() { foreach (const BalanceNotifyList::value_type & i, m_balanceNotifyList) { m_balanceChangedSet += i.first; if (i.second.isValid()) { m_balanceCache.clear(i.first, i.second); } else { m_balanceCache.clear(i.first); } } clearCacheNotification(); } /** * This method checks if a storage object is attached and * throws and exception if not. */ inline void checkStorage() const { if (m_storage == 0) throw MYMONEYEXCEPTION("No storage object attached to MyMoneyFile"); } /** * This method checks that a transaction has been started with * startTransaction() and throws an exception otherwise. Calls * checkStorage() to make sure a storage object is present and attached. */ void checkTransaction(const char* txt) const { checkStorage(); if (!m_inTransaction) { throw MYMONEYEXCEPTION(QString("No transaction started for %1").arg(txt)); } } void priceChanged(const MyMoneyFile& file, const MyMoneyPrice price) { // get all affected accounts and add them to the m_valueChangedSet QList accList; file.accountList(accList); QList::const_iterator account_it; for (account_it = accList.constBegin(); account_it != accList.constEnd(); ++account_it) { QString currencyId = account_it->currencyId(); if (currencyId != file.baseCurrency().id() && (currencyId == price.from() || currencyId == price.to())) { // this account is not in the base currency and the price affects it's value m_valueChangedSet.insert(account_it->id()); } } } /** * This member points to the storage strategy */ MyMoneyStorageMgr *m_storage; bool m_inTransaction; MyMoneySecurity m_baseCurrency; /** * @brief Cache for MyMoneyObjects * * It is also used to emit the objectAdded() and objectModified() signals. * => If one of these signals is used, you must use this cache. */ MyMoneyPriceList m_priceCache; MyMoneyBalanceCache m_balanceCache; /** * This member keeps a list of account ids to notify * after a single operation is completed. The balance cache * is cleared for that account and all dates on or after * the one supplied. If the date is invalid, the entire * balance cache is cleared for that account. */ BalanceNotifyList m_balanceNotifyList; /** * This member keeps a list of account ids for which * a balanceChanged() signal needs to be emitted when * a set of operations has been committed. * * @sa MyMoneyFile::commitTransaction() */ QSet m_balanceChangedSet; /** * This member keeps a list of account ids for which * a valueChanged() signal needs to be emitted when * a set of operations has been committed. * * @sa MyMoneyFile::commitTransaction() */ QSet m_valueChangedSet; /** * This member keeps the list of changes in the engine * in historical order. The type can be 'added', 'modified' * or removed. */ QList m_changeSet; }; class MyMoneyNotifier { public: MyMoneyNotifier(MyMoneyFile::Private* file) { m_file = file; m_file->clearCacheNotification(); } ~MyMoneyNotifier() { m_file->notify(); } private: MyMoneyFile::Private* m_file; }; MyMoneyFile::MyMoneyFile() : d(new Private) { } MyMoneyFile::~MyMoneyFile() { delete d; } MyMoneyFile::MyMoneyFile(MyMoneyStorageMgr *storage) : d(new Private) { attachStorage(storage); } MyMoneyFile* MyMoneyFile::instance() { return &file; } void MyMoneyFile::attachStorage(MyMoneyStorageMgr* const storage) { if (d->m_storage != 0) throw MYMONEYEXCEPTION("Storage already attached"); if (storage == 0) throw MYMONEYEXCEPTION("Storage must not be 0"); d->m_storage = storage; // force reload of base currency d->m_baseCurrency = MyMoneySecurity(); // and the whole cache d->m_balanceCache.clear(); d->m_priceCache.clear(); // notify application about new data availability emit beginChangeNotification(); emit dataChanged(); emit endChangeNotification(); } void MyMoneyFile::detachStorage(MyMoneyStorageMgr* const /* storage */) { d->m_balanceCache.clear(); d->m_priceCache.clear(); d->m_storage = nullptr; } MyMoneyStorageMgr* MyMoneyFile::storage() const { return d->m_storage; } bool MyMoneyFile::storageAttached() const { return d->m_storage != 0; } void MyMoneyFile::startTransaction() { d->checkStorage(); if (d->m_inTransaction) { throw MYMONEYEXCEPTION("Already started a transaction!"); } d->m_storage->startTransaction(); d->m_inTransaction = true; d->m_changeSet.clear(); } bool MyMoneyFile::hasTransaction() const { return d->m_inTransaction; } void MyMoneyFile::commitTransaction() { d->checkTransaction(Q_FUNC_INFO); // commit the transaction in the storage const auto changed = d->m_storage->commitTransaction(); d->m_inTransaction = false; // inform the outside world about the beginning of notifications emit beginChangeNotification(); // Now it's time to send out some signals to the outside world // First we go through the d->m_changeSet and emit respective // signals about addition, modification and removal of engine objects const auto& changes = d->m_changeSet; for (const auto& change : changes) { switch (change.notificationMode()) { case File::Mode::Remove: emit objectRemoved(change.objectType(), change.id()); // if there is a balance change recorded for this account remove it since the account itself will be removed // this can happen when deleting categories that have transactions and the reassign category feature was used d->m_balanceChangedSet.remove(change.id()); break; case File::Mode::Add: emit objectAdded(change.objectType(), change.id()); break; case File::Mode::Modify: emit objectModified(change.objectType(), change.id()); break; } } // we're done with the change set, so we clear it d->m_changeSet.clear(); // now send out the balanceChanged signal for all those // accounts for which we have an indication about a possible // change. const auto& balanceChanges = d->m_balanceChangedSet; for (const auto& id : balanceChanges) { // if we notify about balance change we don't need to notify about value change // for the same account since a balance change implies a value change d->m_valueChangedSet.remove(id); emit balanceChanged(account(id)); } d->m_balanceChangedSet.clear(); // now notify about the remaining value changes const auto& m_valueChanges = d->m_valueChangedSet; for (const auto& id : m_valueChanges) emit valueChanged(account(id)); d->m_valueChangedSet.clear(); // as a last action, send out the global dataChanged signal if (changed) emit dataChanged(); // inform the outside world about the end of notifications emit endChangeNotification(); } void MyMoneyFile::rollbackTransaction() { d->checkTransaction(Q_FUNC_INFO); d->m_storage->rollbackTransaction(); d->m_inTransaction = false; d->m_balanceChangedSet.clear(); d->m_valueChangedSet.clear(); d->m_changeSet.clear(); } void MyMoneyFile::addInstitution(MyMoneyInstitution& institution) { // perform some checks to see that the institution stuff is OK. For // now we assume that the institution must have a name, the ID is not set // and it does not have a parent (MyMoneyFile). if (institution.name().length() == 0 || institution.id().length() != 0) throw MYMONEYEXCEPTION("Not a new institution"); d->checkTransaction(Q_FUNC_INFO); d->m_storage->addInstitution(institution); d->m_changeSet += MyMoneyNotification(File::Mode::Add, institution); } void MyMoneyFile::modifyInstitution(const MyMoneyInstitution& institution) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->modifyInstitution(institution); d->m_changeSet += MyMoneyNotification(File::Mode::Modify, institution); } void MyMoneyFile::modifyTransaction(const MyMoneyTransaction& transaction) { d->checkTransaction(Q_FUNC_INFO); MyMoneyTransaction tCopy(transaction); // now check the splits bool loanAccountAffected = false; const auto splits1 = transaction.splits(); for (const auto& split : splits1) { // the following line will throw an exception if the // account does not exist auto acc = MyMoneyFile::account(split.accountId()); if (acc.id().isEmpty()) throw MYMONEYEXCEPTION("Cannot store split with no account assigned"); if (isStandardAccount(split.accountId())) throw MYMONEYEXCEPTION("Cannot store split referencing standard account"); if (acc.isLoan() && (split.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::Transfer))) loanAccountAffected = true; } // change transfer splits between asset/liability and loan accounts // into amortization splits if (loanAccountAffected) { const auto splits = transaction.splits(); for (const auto& split : splits) { if (split.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::Transfer)) { auto acc = MyMoneyFile::account(split.accountId()); if (acc.isAssetLiability()) { MyMoneySplit s = split; s.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::Amortization)); tCopy.modifySplit(s); } } } } // clear all changed objects from cache MyMoneyNotifier notifier(d); // get the current setting of this transaction MyMoneyTransaction tr = MyMoneyFile::transaction(transaction.id()); // scan the splits again to update notification list // and mark all accounts that are referenced const auto splits2 = tr.splits(); foreach (const auto& split, splits2) d->addCacheNotification(split.accountId(), tr.postDate()); // make sure the value is rounded to the accounts precision fixSplitPrecision(tCopy); // perform modification d->m_storage->modifyTransaction(tCopy); // and mark all accounts that are referenced const auto splits3 = tCopy.splits(); for (const auto& split : splits3) d->addCacheNotification(split.accountId(), tCopy.postDate()); d->m_changeSet += MyMoneyNotification(File::Mode::Modify, transaction); } void MyMoneyFile::modifyAccount(const MyMoneyAccount& _account) { d->checkTransaction(Q_FUNC_INFO); MyMoneyAccount account(_account); auto acc = MyMoneyFile::account(account.id()); // check that for standard accounts only specific parameters are changed if (isStandardAccount(account.id())) { // make sure to use the stuff we found on file account = acc; // and only use the changes that are allowed account.setName(_account.name()); account.setCurrencyId(_account.currencyId()); // now check that it is the same if (!(account == _account)) throw MYMONEYEXCEPTION("Unable to modify the standard account groups"); } if (account.accountType() != acc.accountType() && !account.isLiquidAsset() && !acc.isLiquidAsset()) throw MYMONEYEXCEPTION("Unable to change account type"); // if the account was moved to another institution, we notify // the old one as well as the new one and the structure change if (acc.institutionId() != account.institutionId()) { MyMoneyInstitution inst; if (!acc.institutionId().isEmpty()) { inst = institution(acc.institutionId()); inst.removeAccountId(acc.id()); modifyInstitution(inst); // modifyInstitution updates d->m_changeSet already } if (!account.institutionId().isEmpty()) { inst = institution(account.institutionId()); inst.addAccountId(acc.id()); modifyInstitution(inst); // modifyInstitution updates d->m_changeSet already } } d->m_storage->modifyAccount(account); d->m_changeSet += MyMoneyNotification(File::Mode::Modify, account); } void MyMoneyFile::reparentAccount(MyMoneyAccount &acc, MyMoneyAccount& parent) { d->checkTransaction(Q_FUNC_INFO); // check that it's not one of the standard account groups if (isStandardAccount(acc.id())) throw MYMONEYEXCEPTION("Unable to reparent the standard account groups"); if (acc.accountGroup() == parent.accountGroup() || (acc.accountType() == Account::Type::Income && parent.accountType() == Account::Type::Expense) || (acc.accountType() == Account::Type::Expense && parent.accountType() == Account::Type::Income)) { if (acc.isInvest() && parent.accountType() != Account::Type::Investment) throw MYMONEYEXCEPTION("Unable to reparent Stock to non-investment account"); if (parent.accountType() == Account::Type::Investment && !acc.isInvest()) throw MYMONEYEXCEPTION("Unable to reparent non-stock to investment account"); // keep a notification of the current parent MyMoneyAccount curParent = account(acc.parentAccountId()); d->m_storage->reparentAccount(acc, parent); d->m_changeSet += MyMoneyNotification(File::Mode::Modify, curParent); d->m_changeSet += MyMoneyNotification(File::Mode::Modify, parent); d->m_changeSet += MyMoneyNotification(File::Mode::Modify, acc); } else throw MYMONEYEXCEPTION("Unable to reparent to different account type"); } MyMoneyInstitution MyMoneyFile::institution(const QString& id) const { return d->m_storage->institution(id); } MyMoneyAccount MyMoneyFile::account(const QString& id) const { if (Q_UNLIKELY(id.isEmpty())) // FIXME: Stop requesting accounts with empty id return MyMoneyAccount(); return d->m_storage->account(id); } -MyMoneyAccount MyMoneyFile::subAccountByName(const MyMoneyAccount& acc, const QString& name) const +MyMoneyAccount MyMoneyFile::subAccountByName(const MyMoneyAccount& account, const QString& name) const { static MyMoneyAccount nullAccount; - const auto accounts = acc.accountList(); + const auto accounts = account.accountList(); for (const auto& acc : accounts) { - const auto sacc = account(acc); + const auto sacc = MyMoneyFile::account(acc); if (sacc.name().compare(name) == 0) return sacc; } return nullAccount; } MyMoneyAccount MyMoneyFile::accountByName(const QString& name) const { try { return d->m_storage->accountByName(name); } catch(const MyMoneyException&) { } return MyMoneyAccount(); } void MyMoneyFile::removeTransaction(const MyMoneyTransaction& transaction) { d->checkTransaction(Q_FUNC_INFO); // clear all changed objects from cache MyMoneyNotifier notifier(d); // get the engine's idea about this transaction MyMoneyTransaction tr = MyMoneyFile::transaction(transaction.id()); // scan the splits again to update notification list const auto splits = tr.splits(); for (const auto& split : splits) { auto acc = account(split.accountId()); if (acc.isClosed()) throw MYMONEYEXCEPTION(i18n("Cannot remove transaction that references a closed account.")); d->addCacheNotification(split.accountId(), tr.postDate()); //FIXME-ALEX Do I need to add d->addCacheNotification(split.tagList()); ?? } d->m_storage->removeTransaction(transaction); // remove a possible notification of that same object from the changeSet QList::iterator it; for(it = d->m_changeSet.begin(); it != d->m_changeSet.end();) { if((*it).id() == transaction.id()) { it = d->m_changeSet.erase(it); } else { ++it; } } d->m_changeSet += MyMoneyNotification(File::Mode::Remove, transaction); } bool MyMoneyFile::hasActiveSplits(const QString& id) const { d->checkStorage(); return d->m_storage->hasActiveSplits(id); } bool MyMoneyFile::isStandardAccount(const QString& id) const { d->checkStorage(); return d->m_storage->isStandardAccount(id); } void MyMoneyFile::setAccountName(const QString& id, const QString& name) const { d->checkTransaction(Q_FUNC_INFO); auto acc = account(id); d->m_storage->setAccountName(id, name); d->m_changeSet += MyMoneyNotification(File::Mode::Modify, acc); } void MyMoneyFile::removeAccount(const MyMoneyAccount& account) { d->checkTransaction(Q_FUNC_INFO); MyMoneyAccount parent; MyMoneyAccount acc; MyMoneyInstitution institution; // check that the account and its parent exist // this will throw an exception if the id is unknown acc = MyMoneyFile::account(account.id()); parent = MyMoneyFile::account(account.parentAccountId()); if (!acc.institutionId().isEmpty()) institution = MyMoneyFile::institution(acc.institutionId()); // check that it's not one of the standard account groups if (isStandardAccount(account.id())) throw MYMONEYEXCEPTION("Unable to remove the standard account groups"); if (hasActiveSplits(account.id())) { throw MYMONEYEXCEPTION("Unable to remove account with active splits"); } // collect all sub-ordinate accounts for notification const auto accounts = acc.accountList(); for (const auto& id : accounts) d->m_changeSet += MyMoneyNotification(File::Mode::Modify, MyMoneyFile::account(id)); // don't forget the parent and a possible institution if (!institution.id().isEmpty()) { institution.removeAccountId(account.id()); d->m_storage->modifyInstitution(institution); d->m_changeSet += MyMoneyNotification(File::Mode::Modify, institution); } acc.setInstitutionId(QString()); d->m_storage->removeAccount(acc); d->m_balanceCache.clear(acc.id()); d->m_changeSet += MyMoneyNotification(File::Mode::Modify, parent); d->m_changeSet += MyMoneyNotification(File::Mode::Remove, acc); } void MyMoneyFile::removeAccountList(const QStringList& account_list, unsigned int level) { if (level > 100) throw MYMONEYEXCEPTION("Too deep recursion in [MyMoneyFile::removeAccountList]!"); d->checkTransaction(Q_FUNC_INFO); // upon entry, we check that we could proceed with the operation if (!level) { if (!hasOnlyUnusedAccounts(account_list, 0)) { throw MYMONEYEXCEPTION("One or more accounts cannot be removed"); } } // process all accounts in the list and test if they have transactions assigned foreach (const auto sAccount, account_list) { auto a = d->m_storage->account(sAccount); //qDebug() << "Deleting account '"<< a.name() << "'"; // first remove all sub-accounts if (!a.accountList().isEmpty()) { removeAccountList(a.accountList(), level + 1); // then remove account itself, but we first have to get // rid of the account list that is still stored in // the MyMoneyAccount object. Easiest way is to get a fresh copy. a = d->m_storage->account(sAccount); } // make sure to remove the item from the cache removeAccount(a); } } bool MyMoneyFile::hasOnlyUnusedAccounts(const QStringList& account_list, unsigned int level) { if (level > 100) throw MYMONEYEXCEPTION("Too deep recursion in [MyMoneyFile::hasOnlyUnusedAccounts]!"); // process all accounts in the list and test if they have transactions assigned for (const auto& sAccount : account_list) { if (transactionCount(sAccount) != 0) return false; // the current account has a transaction assigned if (!hasOnlyUnusedAccounts(account(sAccount).accountList(), level + 1)) return false; // some sub-account has a transaction assigned } return true; // all subaccounts unused } void MyMoneyFile::removeInstitution(const MyMoneyInstitution& institution) { d->checkTransaction(Q_FUNC_INFO); MyMoneyInstitution inst = MyMoneyFile::institution(institution.id()); bool blocked = signalsBlocked(); blockSignals(true); const auto accounts = inst.accountList(); for (const auto& acc : accounts) { auto a = account(acc); a.setInstitutionId(QString()); modifyAccount(a); d->m_changeSet += MyMoneyNotification(File::Mode::Modify, a); } blockSignals(blocked); d->m_storage->removeInstitution(institution); d->m_changeSet += MyMoneyNotification(File::Mode::Remove, institution); } void MyMoneyFile::createAccount(MyMoneyAccount& newAccount, MyMoneyAccount& parentAccount, MyMoneyAccount& brokerageAccount, MyMoneyMoney openingBal) { // make sure we have a currency. If none is assigned, we assume base currency if (newAccount.currencyId().isEmpty()) newAccount.setCurrencyId(baseCurrency().id()); MyMoneyFileTransaction ft; try { int pos; // check for ':' in the name and use it as separator for a hierarchy while ((pos = newAccount.name().indexOf(MyMoneyFile::AccountSeparator)) != -1) { QString part = newAccount.name().left(pos); QString remainder = newAccount.name().mid(pos + 1); const MyMoneyAccount& existingAccount = subAccountByName(parentAccount, part); if (existingAccount.id().isEmpty()) { newAccount.setName(part); addAccount(newAccount, parentAccount); parentAccount = newAccount; } else { parentAccount = existingAccount; } newAccount.setParentAccountId(QString()); // make sure, there's no parent newAccount.clearId(); // and no id set for adding newAccount.removeAccountIds(); // and no sub-account ids newAccount.setName(remainder); } addAccount(newAccount, parentAccount); // in case of a loan account, we add the initial payment if ((newAccount.accountType() == Account::Type::Loan || newAccount.accountType() == Account::Type::AssetLoan) && !newAccount.value("kmm-loan-payment-acc").isEmpty() && !newAccount.value("kmm-loan-payment-date").isEmpty()) { MyMoneyAccountLoan acc(newAccount); MyMoneyTransaction t; MyMoneySplit a, b; a.setAccountId(acc.id()); b.setAccountId(acc.value("kmm-loan-payment-acc").toLatin1()); a.setValue(acc.loanAmount()); if (acc.accountType() == Account::Type::Loan) a.setValue(-a.value()); a.setShares(a.value()); b.setValue(-a.value()); b.setShares(b.value()); a.setMemo(i18n("Loan payout")); b.setMemo(i18n("Loan payout")); t.setPostDate(QDate::fromString(acc.value("kmm-loan-payment-date"), Qt::ISODate)); newAccount.deletePair("kmm-loan-payment-acc"); newAccount.deletePair("kmm-loan-payment-date"); MyMoneyFile::instance()->modifyAccount(newAccount); t.addSplit(a); t.addSplit(b); addTransaction(t); createOpeningBalanceTransaction(newAccount, openingBal); // in case of an investment account we check if we should create // a brokerage account } else if (newAccount.accountType() == Account::Type::Investment && !brokerageAccount.name().isEmpty()) { addAccount(brokerageAccount, parentAccount); // set a link from the investment account to the brokerage account modifyAccount(newAccount); createOpeningBalanceTransaction(brokerageAccount, openingBal); } else createOpeningBalanceTransaction(newAccount, openingBal); ft.commit(); } catch (const MyMoneyException &e) { qWarning("Unable to create account: %s", qPrintable(e.what())); throw MYMONEYEXCEPTION(e.what()); } } void MyMoneyFile::addAccount(MyMoneyAccount& account, MyMoneyAccount& parent) { d->checkTransaction(Q_FUNC_INFO); MyMoneyInstitution institution; // perform some checks to see that the account stuff is OK. For // now we assume that the account must have a name, has no // transaction and sub-accounts and parent account // it's own ID is not set and it does not have a pointer to (MyMoneyFile) if (account.name().length() == 0) throw MYMONEYEXCEPTION("Account has no name"); if (account.id().length() != 0) throw MYMONEYEXCEPTION("New account must have no id"); if (account.accountList().count() != 0) throw MYMONEYEXCEPTION("New account must have no sub-accounts"); if (!account.parentAccountId().isEmpty()) throw MYMONEYEXCEPTION("New account must have no parent-id"); if (account.accountType() == Account::Type::Unknown) throw MYMONEYEXCEPTION("Account has invalid type"); // make sure, that the parent account exists // if not, an exception is thrown. If it exists, // get a copy of the current data auto acc = MyMoneyFile::account(parent.id()); #if 0 // TODO: remove the following code as we now can have multiple accounts // with the same name even in the same hierarchy position of the account tree // // check if the selected name is currently not among the child accounts // if we find one, then return it as the new account QStringList::const_iterator it_a; foreach (const auto accountID, acc.accountList()) { MyMoneyAccount a = MyMoneyFile::account(accountID); if (account.name() == a.name()) { account = a; return; } } #endif // FIXME: make sure, that the parent has the same type // I left it out here because I don't know, if there is // a tight coupling between e.g. checking accounts and the // class asset. It certainly does not make sense to create an // expense account under an income account. Maybe it does, I don't know. // We enforce, that a stock account can never be a parent and // that the parent for a stock account must be an investment. Also, // an investment cannot have another investment account as it's parent if (parent.isInvest()) throw MYMONEYEXCEPTION("Stock account cannot be parent account"); if (account.isInvest() && parent.accountType() != Account::Type::Investment) throw MYMONEYEXCEPTION("Stock account must have investment account as parent "); if (!account.isInvest() && parent.accountType() == Account::Type::Investment) throw MYMONEYEXCEPTION("Investment account can only have stock accounts as children"); // if an institution is set, verify that it exists if (account.institutionId().length() != 0) { // check the presence of the institution. if it // does not exist, an exception is thrown institution = MyMoneyFile::institution(account.institutionId()); } // if we don't have a valid opening date use today if (!account.openingDate().isValid()) { account.setOpeningDate(QDate::currentDate()); } // make sure to set the opening date for categories to a // fixed date (1900-1-1). See #313793 on b.k.o for details if (account.isIncomeExpense()) { account.setOpeningDate(QDate(1900, 1, 1)); } // if we don't have a currency assigned use the base currency if (account.currencyId().isEmpty()) { account.setCurrencyId(baseCurrency().id()); } // make sure the parent id is setup account.setParentAccountId(parent.id()); d->m_storage->addAccount(account); d->m_changeSet += MyMoneyNotification(File::Mode::Add, account); d->m_storage->addAccount(parent, account); d->m_changeSet += MyMoneyNotification(File::Mode::Modify, parent); if (account.institutionId().length() != 0) { institution.addAccountId(account.id()); d->m_storage->modifyInstitution(institution); d->m_changeSet += MyMoneyNotification(File::Mode::Modify, institution); } } MyMoneyTransaction MyMoneyFile::createOpeningBalanceTransaction(const MyMoneyAccount& acc, const MyMoneyMoney& balance) { MyMoneyTransaction t; // if the opening balance is not zero, we need // to create the respective transaction if (!balance.isZero()) { d->checkTransaction(Q_FUNC_INFO); MyMoneySecurity currency = security(acc.currencyId()); MyMoneyAccount openAcc = openingBalanceAccount(currency); if (openAcc.openingDate() > acc.openingDate()) { openAcc.setOpeningDate(acc.openingDate()); modifyAccount(openAcc); } MyMoneySplit s; t.setPostDate(acc.openingDate()); t.setCommodity(acc.currencyId()); s.setAccountId(acc.id()); s.setShares(balance); s.setValue(balance); t.addSplit(s); s.clearId(); s.setAccountId(openAcc.id()); s.setShares(-balance); s.setValue(-balance); t.addSplit(s); addTransaction(t); } return t; } QString MyMoneyFile::openingBalanceTransaction(const MyMoneyAccount& acc) const { QString result; MyMoneySecurity currency = security(acc.currencyId()); MyMoneyAccount openAcc; try { openAcc = openingBalanceAccount(currency); } catch (const MyMoneyException &) { return result; } // Iterate over all the opening balance transactions for this currency MyMoneyTransactionFilter filter; filter.addAccount(openAcc.id()); QList transactions = transactionList(filter); QList::const_iterator it_t = transactions.constBegin(); while (it_t != transactions.constEnd()) { try { // Test whether the transaction also includes a split into // this account (*it_t).splitByAccount(acc.id(), true /*match*/); // If so, we have a winner! result = (*it_t).id(); break; } catch (const MyMoneyException &) { // If not, keep searching ++it_t; } } return result; } MyMoneyAccount MyMoneyFile::openingBalanceAccount(const MyMoneySecurity& security) { if (!security.isCurrency()) throw MYMONEYEXCEPTION("Opening balance for non currencies not supported"); try { return openingBalanceAccount_internal(security); } catch (const MyMoneyException &) { MyMoneyFileTransaction ft; MyMoneyAccount acc; try { acc = createOpeningBalanceAccount(security); ft.commit(); } catch (const MyMoneyException &) { qDebug("Unable to create opening balance account for security %s", qPrintable(security.id())); } return acc; } } MyMoneyAccount MyMoneyFile::openingBalanceAccount(const MyMoneySecurity& security) const { return openingBalanceAccount_internal(security); } MyMoneyAccount MyMoneyFile::openingBalanceAccount_internal(const MyMoneySecurity& security) const { if (!security.isCurrency()) throw MYMONEYEXCEPTION("Opening balance for non currencies not supported"); MyMoneyAccount acc; QList accounts; QList::ConstIterator it; accountList(accounts, equity().accountList(), true); for (it = accounts.constBegin(); it != accounts.constEnd(); ++it) { if (it->value("OpeningBalanceAccount") == QLatin1String("Yes") && it->currencyId() == security.id()) { acc = *it; break; } } if (acc.id().isEmpty()) { for (it = accounts.constBegin(); it != accounts.constEnd(); ++it) { if (it->name().startsWith(MyMoneyFile::openingBalancesPrefix()) && it->currencyId() == security.id()) { acc = *it; break; } } } if (acc.id().isEmpty()) { throw MYMONEYEXCEPTION(QString("No opening balance account for %1").arg(security.tradingSymbol())); } return acc; } MyMoneyAccount MyMoneyFile::createOpeningBalanceAccount(const MyMoneySecurity& security) { d->checkTransaction(Q_FUNC_INFO); MyMoneyAccount acc; QList accounts; QList::ConstIterator it; accountList(accounts, equity().accountList(), true); // find present opening balance accounts without containing '(' QString name; QString parentAccountId; QRegExp exp(QString("\\([A-Z]{3}\\)")); for (it = accounts.constBegin(); it != accounts.constEnd(); ++it) { if (it->value("OpeningBalanceAccount") == QLatin1String("Yes") && exp.indexIn(it->name()) == -1) { name = it->name(); parentAccountId = it->parentAccountId(); break; } } if (name.isEmpty()) name = MyMoneyFile::openingBalancesPrefix(); if (security.id() != baseCurrency().id()) { name += QString(" (%1)").arg(security.id()); } acc.setName(name); acc.setAccountType(Account::Type::Equity); acc.setCurrencyId(security.id()); acc.setValue("OpeningBalanceAccount", "Yes"); MyMoneyAccount parent = !parentAccountId.isEmpty() ? account(parentAccountId) : equity(); this->addAccount(acc, parent); return acc; } void MyMoneyFile::addTransaction(MyMoneyTransaction& transaction) { d->checkTransaction(Q_FUNC_INFO); // clear all changed objects from cache MyMoneyNotifier notifier(d); // perform some checks to see that the transaction stuff is OK. For // now we assume that // * no ids are assigned // * the date valid (must not be empty) // * the referenced accounts in the splits exist // first perform all the checks if (!transaction.id().isEmpty()) throw MYMONEYEXCEPTION("Unable to add transaction with id set"); if (!transaction.postDate().isValid()) throw MYMONEYEXCEPTION("Unable to add transaction with invalid postdate"); // now check the splits auto loanAccountAffected = false; const auto splits1 = transaction.splits(); for (const auto& split : splits1) { // the following line will throw an exception if the // account does not exist or is one of the standard accounts auto acc = MyMoneyFile::account(split.accountId()); if (acc.id().isEmpty()) throw MYMONEYEXCEPTION("Cannot add split with no account assigned"); if (acc.isLoan()) loanAccountAffected = true; if (isStandardAccount(split.accountId())) throw MYMONEYEXCEPTION("Cannot add split referencing standard account"); } // change transfer splits between asset/liability and loan accounts // into amortization splits if (loanAccountAffected) { foreach (const auto split, transaction.splits()) { if (split.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::Transfer)) { auto acc = MyMoneyFile::account(split.accountId()); if (acc.isAssetLiability()) { MyMoneySplit s = split; s.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::Amortization)); transaction.modifySplit(s); } } } } // check that we have a commodity if (transaction.commodity().isEmpty()) { transaction.setCommodity(baseCurrency().id()); } // make sure the value is rounded to the accounts precision fixSplitPrecision(transaction); // then add the transaction to the file global pool d->m_storage->addTransaction(transaction); // scan the splits again to update notification list const auto splits2 = transaction.splits(); for (const auto& split : splits2) d->addCacheNotification(split.accountId(), transaction.postDate()); d->m_changeSet += MyMoneyNotification(File::Mode::Add, transaction); } MyMoneyTransaction MyMoneyFile::transaction(const QString& id) const { d->checkStorage(); return d->m_storage->transaction(id); } MyMoneyTransaction MyMoneyFile::transaction(const QString& account, const int idx) const { d->checkStorage(); return d->m_storage->transaction(account, idx); } void MyMoneyFile::addPayee(MyMoneyPayee& payee) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->addPayee(payee); d->m_changeSet += MyMoneyNotification(File::Mode::Add, payee); } MyMoneyPayee MyMoneyFile::payee(const QString& id) const { if (Q_UNLIKELY(id.isEmpty())) return MyMoneyPayee(); return d->m_storage->payee(id); } MyMoneyPayee MyMoneyFile::payeeByName(const QString& name) const { d->checkStorage(); return d->m_storage->payeeByName(name); } void MyMoneyFile::modifyPayee(const MyMoneyPayee& payee) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->modifyPayee(payee); d->m_changeSet += MyMoneyNotification(File::Mode::Modify, payee); } void MyMoneyFile::removePayee(const MyMoneyPayee& payee) { d->checkTransaction(Q_FUNC_INFO); // FIXME we need to make sure, that the payee is not referenced anymore d->m_storage->removePayee(payee); d->m_changeSet += MyMoneyNotification(File::Mode::Remove, payee); } void MyMoneyFile::addTag(MyMoneyTag& tag) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->addTag(tag); d->m_changeSet += MyMoneyNotification(File::Mode::Add, tag); } MyMoneyTag MyMoneyFile::tag(const QString& id) const { return d->m_storage->tag(id); } MyMoneyTag MyMoneyFile::tagByName(const QString& name) const { d->checkStorage(); return d->m_storage->tagByName(name); } void MyMoneyFile::modifyTag(const MyMoneyTag& tag) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->modifyTag(tag); d->m_changeSet += MyMoneyNotification(File::Mode::Modify, tag); } void MyMoneyFile::removeTag(const MyMoneyTag& tag) { d->checkTransaction(Q_FUNC_INFO); // FIXME we need to make sure, that the tag is not referenced anymore d->m_storage->removeTag(tag); d->m_changeSet += MyMoneyNotification(File::Mode::Remove, tag); } void MyMoneyFile::accountList(QList& list, const QStringList& idlist, const bool recursive) const { if (idlist.isEmpty()) { d->m_storage->accountList(list); #if 0 // TODO: I have no idea what this was good for, but it caused the networth report // to show double the numbers so I commented it out (ipwizard, 2008-05-24) if (d->m_storage && (list.isEmpty() || list.size() != d->m_storage->accountCount())) { d->m_storage->accountList(list); d->m_cache.preloadAccount(list); } #endif QList::Iterator it; for (it = list.begin(); it != list.end();) { if (isStandardAccount((*it).id())) { it = list.erase(it); } else { ++it; } } } else { QList::ConstIterator it; QList list_a; d->m_storage->accountList(list_a); for (it = list_a.constBegin(); it != list_a.constEnd(); ++it) { if (!isStandardAccount((*it).id())) { if (idlist.indexOf((*it).id()) != -1) { list.append(*it); if (recursive == true && !(*it).accountList().isEmpty()) { accountList(list, (*it).accountList(), true); } } } } } } QList MyMoneyFile::institutionList() const { return d->m_storage->institutionList(); } // general get functions MyMoneyPayee MyMoneyFile::user() const { d->checkStorage(); return d->m_storage->user(); } // general set functions void MyMoneyFile::setUser(const MyMoneyPayee& user) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->setUser(user); } bool MyMoneyFile::dirty() const { if (!d->m_storage) return false; return d->m_storage->dirty(); } void MyMoneyFile::setDirty() const { d->checkStorage(); d->m_storage->setDirty(); } unsigned int MyMoneyFile::accountCount() const { d->checkStorage(); return d->m_storage->accountCount(); } void MyMoneyFile::ensureDefaultCurrency(MyMoneyAccount& acc) const { if (acc.currencyId().isEmpty()) { if (!baseCurrency().id().isEmpty()) acc.setCurrencyId(baseCurrency().id()); } } MyMoneyAccount MyMoneyFile::liability() const { d->checkStorage(); return account(stdAccNames[stdAccLiability]); } MyMoneyAccount MyMoneyFile::asset() const { d->checkStorage(); return account(stdAccNames[stdAccAsset]); } MyMoneyAccount MyMoneyFile::expense() const { d->checkStorage(); return account(stdAccNames[stdAccExpense]); } MyMoneyAccount MyMoneyFile::income() const { d->checkStorage(); return account(stdAccNames[stdAccIncome]); } MyMoneyAccount MyMoneyFile::equity() const { d->checkStorage(); return account(stdAccNames[stdAccEquity]); } unsigned int MyMoneyFile::transactionCount(const QString& account) const { d->checkStorage(); return d->m_storage->transactionCount(account); } unsigned int MyMoneyFile::transactionCount() const { return transactionCount(QString()); } QMap MyMoneyFile::transactionCountMap() const { d->checkStorage(); return d->m_storage->transactionCountMap(); } unsigned int MyMoneyFile::institutionCount() const { d->checkStorage(); return d->m_storage->institutionCount(); } MyMoneyMoney MyMoneyFile::balance(const QString& id, const QDate& date) const { if (date.isValid()) { MyMoneyBalanceCacheItem bal = d->m_balanceCache.balance(id, date); if (bal.isValid()) return bal.balance(); } d->checkStorage(); MyMoneyMoney returnValue = d->m_storage->balance(id, date); if (date.isValid()) { d->m_balanceCache.insert(id, date, returnValue); } return returnValue; } MyMoneyMoney MyMoneyFile::balance(const QString& id) const { return balance(id, QDate()); } MyMoneyMoney MyMoneyFile::clearedBalance(const QString &id, const QDate& date) const { MyMoneyMoney cleared; QList list; cleared = balance(id, date); MyMoneyAccount account = this->account(id); MyMoneyMoney factor(1, 1); if (account.accountGroup() == Account::Type::Liability || account.accountGroup() == Account::Type::Equity) factor = -factor; MyMoneyTransactionFilter filter; filter.addAccount(id); filter.setDateFilter(QDate(), date); filter.setReportAllSplits(false); filter.addState((int)TransactionFilter::State::NotReconciled); transactionList(list, filter); for (QList::const_iterator it_t = list.constBegin(); it_t != list.constEnd(); ++it_t) { const QList& splits = (*it_t).splits(); for (QList::const_iterator it_s = splits.constBegin(); it_s != splits.constEnd(); ++it_s) { const MyMoneySplit &split = (*it_s); if (split.accountId() != id) continue; cleared -= split.shares(); } } return cleared * factor; } MyMoneyMoney MyMoneyFile::totalBalance(const QString& id, const QDate& date) const { d->checkStorage(); return d->m_storage->totalBalance(id, date); } MyMoneyMoney MyMoneyFile::totalBalance(const QString& id) const { return totalBalance(id, QDate()); } void MyMoneyFile::warningMissingRate(const QString& fromId, const QString& toId) const { MyMoneySecurity from, to; try { from = security(fromId); to = security(toId); qWarning("Missing price info for conversion from %s to %s", qPrintable(from.name()), qPrintable(to.name())); } catch (const MyMoneyException &e) { qWarning("Missing security caught in MyMoneyFile::warningMissingRate(): %s(%ld) %s", qPrintable(e.file()), e.line(), qPrintable(e.what())); } } void MyMoneyFile::transactionList(QList >& list, MyMoneyTransactionFilter& filter) const { d->checkStorage(); d->m_storage->transactionList(list, filter); } void MyMoneyFile::transactionList(QList& list, MyMoneyTransactionFilter& filter) const { d->checkStorage(); d->m_storage->transactionList(list, filter); } QList MyMoneyFile::transactionList(MyMoneyTransactionFilter& filter) const { d->checkStorage(); return d->m_storage->transactionList(filter); } QList MyMoneyFile::payeeList() const { return d->m_storage->payeeList(); } QList MyMoneyFile::tagList() const { return d->m_storage->tagList(); } QString MyMoneyFile::accountToCategory(const QString& accountId, bool includeStandardAccounts) const { MyMoneyAccount acc; QString rc; if (!accountId.isEmpty()) { acc = account(accountId); do { if (!rc.isEmpty()) rc = AccountSeparator + rc; rc = acc.name() + rc; acc = account(acc.parentAccountId()); } while (!acc.id().isEmpty() && (includeStandardAccounts || !isStandardAccount(acc.id()))); } return rc; } QString MyMoneyFile::categoryToAccount(const QString& category, Account::Type type) const { QString id; // search the category in the expense accounts and if it is not found, try // to locate it in the income accounts if (type == Account::Type::Unknown || type == Account::Type::Expense) { id = locateSubAccount(MyMoneyFile::instance()->expense(), category); } if ((id.isEmpty() && type == Account::Type::Unknown) || type == Account::Type::Income) { id = locateSubAccount(MyMoneyFile::instance()->income(), category); } return id; } QString MyMoneyFile::categoryToAccount(const QString& category) const { return categoryToAccount(category, Account::Type::Unknown); } QString MyMoneyFile::nameToAccount(const QString& name) const { QString id; // search the category in the asset accounts and if it is not found, try // to locate it in the liability accounts id = locateSubAccount(MyMoneyFile::instance()->asset(), name); if (id.isEmpty()) id = locateSubAccount(MyMoneyFile::instance()->liability(), name); return id; } QString MyMoneyFile::parentName(const QString& name) const { return name.section(AccountSeparator, 0, -2); } QString MyMoneyFile::locateSubAccount(const MyMoneyAccount& base, const QString& category) const { MyMoneyAccount nextBase; QString level, remainder; level = category.section(AccountSeparator, 0, 0); remainder = category.section(AccountSeparator, 1); foreach (const auto sAccount, base.accountList()) { nextBase = account(sAccount); if (nextBase.name() == level) { if (remainder.isEmpty()) { return nextBase.id(); } return locateSubAccount(nextBase, remainder); } } return QString(); } QString MyMoneyFile::value(const QString& key) const { d->checkStorage(); return d->m_storage->value(key); } void MyMoneyFile::setValue(const QString& key, const QString& val) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->setValue(key, val); } void MyMoneyFile::deletePair(const QString& key) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->deletePair(key); } void MyMoneyFile::addSchedule(MyMoneySchedule& sched) { d->checkTransaction(Q_FUNC_INFO); const auto splits = sched.transaction().splits(); for (const auto& split : splits) { // the following line will throw an exception if the // account does not exist or is one of the standard accounts const auto acc = account(split.accountId()); if (acc.id().isEmpty()) throw MYMONEYEXCEPTION("Cannot add split with no account assigned"); if (isStandardAccount(split.accountId())) throw MYMONEYEXCEPTION("Cannot add split referencing standard account"); } d->m_storage->addSchedule(sched); d->m_changeSet += MyMoneyNotification(File::Mode::Add, sched); } void MyMoneyFile::modifySchedule(const MyMoneySchedule& sched) { d->checkTransaction(Q_FUNC_INFO); foreach (const auto split, sched.transaction().splits()) { // the following line will throw an exception if the // account does not exist or is one of the standard accounts auto acc = MyMoneyFile::account(split.accountId()); if (acc.id().isEmpty()) throw MYMONEYEXCEPTION("Cannot store split with no account assigned"); if (isStandardAccount(split.accountId())) throw MYMONEYEXCEPTION("Cannot store split referencing standard account"); } d->m_storage->modifySchedule(sched); d->m_changeSet += MyMoneyNotification(File::Mode::Modify, sched); } void MyMoneyFile::removeSchedule(const MyMoneySchedule& sched) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->removeSchedule(sched); d->m_changeSet += MyMoneyNotification(File::Mode::Remove, sched); } MyMoneySchedule MyMoneyFile::schedule(const QString& id) const { return d->m_storage->schedule(id); } QList MyMoneyFile::scheduleList( const QString& accountId, const Schedule::Type type, const Schedule::Occurrence occurrence, const Schedule::PaymentType paymentType, const QDate& startDate, const QDate& endDate, const bool overdue) const { d->checkStorage(); return d->m_storage->scheduleList(accountId, type, occurrence, paymentType, startDate, endDate, overdue); } QList MyMoneyFile::scheduleList( const QString& accountId) const { return scheduleList(accountId, Schedule::Type::Any, Schedule::Occurrence::Any, Schedule::PaymentType::Any, QDate(), QDate(), false); } QList MyMoneyFile::scheduleList() const { return scheduleList(QString(), Schedule::Type::Any, Schedule::Occurrence::Any, Schedule::PaymentType::Any, QDate(), QDate(), false); } QStringList MyMoneyFile::consistencyCheck() { QList list; QList::Iterator it_a; QList::Iterator it_sch; QList::Iterator it_p; QList::Iterator it_t; QList::Iterator it_r; QStringList accountRebuild; QMap interestAccounts; MyMoneyAccount parent; MyMoneyAccount child; MyMoneyAccount toplevel; QString parentId; QStringList rc; int problemCount = 0; int unfixedCount = 0; QString problemAccount; // check that we have a storage object d->checkTransaction(Q_FUNC_INFO); // get the current list of accounts accountList(list); // add the standard accounts list << MyMoneyFile::instance()->asset(); list << MyMoneyFile::instance()->liability(); list << MyMoneyFile::instance()->income(); list << MyMoneyFile::instance()->expense(); for (it_a = list.begin(); it_a != list.end(); ++it_a) { // no more checks for standard accounts if (isStandardAccount((*it_a).id())) { continue; } switch ((*it_a).accountGroup()) { case Account::Type::Asset: toplevel = asset(); break; case Account::Type::Liability: toplevel = liability(); break; case Account::Type::Expense: toplevel = expense(); break; case Account::Type::Income: toplevel = income(); break; case Account::Type::Equity: toplevel = equity(); break; default: qWarning("%s:%d This should never happen!", __FILE__ , __LINE__); break; } // check for loops in the hierarchy parentId = (*it_a).parentAccountId(); try { bool dropOut = false; while (!isStandardAccount(parentId) && !dropOut) { parent = account(parentId); if (parent.id() == (*it_a).id()) { // parent loops, so we need to re-parent to toplevel account // find parent account in our list problemCount++; QList::Iterator it_b; for (it_b = list.begin(); it_b != list.end(); ++it_b) { if ((*it_b).id() == parent.id()) { if (problemAccount != (*it_a).name()) { problemAccount = (*it_a).name(); rc << i18n("* Problem with account '%1'", problemAccount); rc << i18n(" * Loop detected between this account and account '%1'.", (*it_b).name()); rc << i18n(" Reparenting account '%2' to top level account '%1'.", toplevel.name(), (*it_a).name()); (*it_a).setParentAccountId(toplevel.id()); if (accountRebuild.contains(toplevel.id()) == 0) accountRebuild << toplevel.id(); if (accountRebuild.contains((*it_a).id()) == 0) accountRebuild << (*it_a).id(); dropOut = true; break; } } } } parentId = parent.parentAccountId(); } } catch (const MyMoneyException &) { // if we don't know about a parent, we catch it later } // check that the parent exists parentId = (*it_a).parentAccountId(); try { parent = account(parentId); if ((*it_a).accountGroup() != parent.accountGroup()) { problemCount++; if (problemAccount != (*it_a).name()) { problemAccount = (*it_a).name(); rc << i18n("* Problem with account '%1'", problemAccount); } // the parent belongs to a different group, so we reconnect to the // master group account (asset, liability, etc) to which this account // should belong and update it in the engine. rc << i18n(" * Parent account '%1' belongs to a different group.", parent.name()); rc << i18n(" New parent account is the top level account '%1'.", toplevel.name()); (*it_a).setParentAccountId(toplevel.id()); // make sure to rebuild the sub-accounts of the top account // and the one we removed this account from if (accountRebuild.contains(toplevel.id()) == 0) accountRebuild << toplevel.id(); if (accountRebuild.contains(parent.id()) == 0) accountRebuild << parent.id(); } else if (!parent.accountList().contains((*it_a).id())) { problemCount++; if (problemAccount != (*it_a).name()) { problemAccount = (*it_a).name(); rc << i18n("* Problem with account '%1'", problemAccount); } // parent exists, but does not have a reference to the account rc << i18n(" * Parent account '%1' does not contain '%2' as sub-account.", parent.name(), problemAccount); if (accountRebuild.contains(parent.id()) == 0) accountRebuild << parent.id(); } } catch (const MyMoneyException &) { // apparently, the parent does not exist anymore. we reconnect to the // master group account (asset, liability, etc) to which this account // should belong and update it in the engine. problemCount++; if (problemAccount != (*it_a).name()) { problemAccount = (*it_a).name(); rc << i18n("* Problem with account '%1'", problemAccount); } rc << i18n(" * The parent with id %1 does not exist anymore.", parentId); rc << i18n(" New parent account is the top level account '%1'.", toplevel.name()); (*it_a).setParentAccountId(toplevel.id()); // make sure to rebuild the sub-accounts of the top account if (accountRebuild.contains(toplevel.id()) == 0) accountRebuild << toplevel.id(); } // now check that all the children exist and have the correct type foreach (const auto accountID, (*it_a).accountList()) { // check that the child exists try { child = account(accountID); if (child.parentAccountId() != (*it_a).id()) { throw MYMONEYEXCEPTION("Child account has a different parent"); } } catch (const MyMoneyException &) { problemCount++; if (problemAccount != (*it_a).name()) { problemAccount = (*it_a).name(); rc << i18n("* Problem with account '%1'", problemAccount); } rc << i18n(" * Child account with id %1 does not exist anymore.", accountID); rc << i18n(" The child account list will be reconstructed."); if (accountRebuild.contains((*it_a).id()) == 0) accountRebuild << (*it_a).id(); } } // see if it is a loan account. if so, remember the assigned interest account if ((*it_a).isLoan()) { MyMoneyAccountLoan loan(*it_a); if (!loan.interestAccountId().isEmpty()) { interestAccounts[loan.interestAccountId()] = true; } try { payee(loan.payee()); } catch (const MyMoneyException &) { problemCount++; if (problemAccount != (*it_a).name()) { problemAccount = (*it_a).name(); rc << i18n("* Problem with account '%1'", problemAccount); } rc << i18n(" * The payee with id %1 referenced by the loan does not exist anymore.", loan.payee()); rc << i18n(" The payee will be removed."); // remove the payee - the account will be modified in the engine later (*it_a).deletePair("payee"); } } // check if it is a category and set the date to 1900-01-01 if different if ((*it_a).isIncomeExpense()) { if (((*it_a).openingDate().isValid() == false) || ((*it_a).openingDate() != QDate(1900, 1, 1))) { (*it_a).setOpeningDate(QDate(1900, 1, 1)); } } // check for clear text online password in the online settings if (!(*it_a).onlineBankingSettings().value("password").isEmpty()) { if (problemAccount != (*it_a).name()) { problemAccount = (*it_a).name(); rc << i18n("* Problem with account '%1'", problemAccount); } rc << i18n(" * Older versions of KMyMoney stored an OFX password for this account in cleartext."); rc << i18n(" Please open it in the account editor (Account/Edit account) once and press OK."); rc << i18n(" This will store the password in the KDE wallet and remove the cleartext version."); ++unfixedCount; } // if the account was modified, we need to update it in the engine if (!(d->m_storage->account((*it_a).id()) == (*it_a))) { try { d->m_storage->modifyAccount(*it_a, true); } catch (const MyMoneyException &) { rc << i18n(" * Unable to update account data in engine."); return rc; } } } if (accountRebuild.count() != 0) { rc << i18n("* Reconstructing the child lists for"); } // clear the affected lists for (it_a = list.begin(); it_a != list.end(); ++it_a) { if (accountRebuild.contains((*it_a).id())) { rc << QString(" %1").arg((*it_a).name()); // clear the account list (*it_a).removeAccountIds(); } } // reconstruct the lists for (it_a = list.begin(); it_a != list.end(); ++it_a) { QList::Iterator it; parentId = (*it_a).parentAccountId(); if (accountRebuild.contains(parentId)) { for (it = list.begin(); it != list.end(); ++it) { if ((*it).id() == parentId) { (*it).addAccountId((*it_a).id()); break; } } } } // update the engine objects for (it_a = list.begin(); it_a != list.end(); ++it_a) { if (accountRebuild.contains((*it_a).id())) { try { d->m_storage->modifyAccount(*it_a, true); } catch (const MyMoneyException &) { rc << i18n(" * Unable to update account data for account %1 in engine", (*it_a).name()); } } } // For some reason, files exist with invalid ids. This has been found in the payee id // so we fix them here QList pList = payeeList(); QMappayeeConversionMap; for (it_p = pList.begin(); it_p != pList.end(); ++it_p) { if ((*it_p).id().length() > 7) { // found one of those with an invalid ids // create a new one and store it in the map. MyMoneyPayee payee = (*it_p); payee.clearId(); d->m_storage->addPayee(payee); payeeConversionMap[(*it_p).id()] = payee.id(); rc << i18n(" * Payee %1 recreated with fixed id", payee.name()); ++problemCount; } } // Fix the transactions MyMoneyTransactionFilter filter; filter.setReportAllSplits(false); const auto tList = d->m_storage->transactionList(filter); // Generate the list of interest accounts for (const auto& transaction : tList) { const auto splits = transaction.splits(); for (const auto& split : splits) { if (split.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::Interest)) interestAccounts[split.accountId()] = true; } } QSet supportedAccountTypes; supportedAccountTypes << Account::Type::Checkings << Account::Type::Savings << Account::Type::Cash << Account::Type::CreditCard << Account::Type::Asset << Account::Type::Liability; QSet reportedUnsupportedAccounts; for (const auto& transaction : tList) { MyMoneyTransaction t = transaction; bool tChanged = false; QDate accountOpeningDate; QStringList accountList; const auto splits = t.splits(); foreach (const auto split, splits) { bool sChanged = false; MyMoneySplit s = split; if (payeeConversionMap.find(split.payeeId()) != payeeConversionMap.end()) { s.setPayeeId(payeeConversionMap[s.payeeId()]); sChanged = true; rc << i18n(" * Payee id updated in split of transaction '%1'.", t.id()); ++problemCount; } try { const auto acc = this->account(s.accountId()); // compute the newest opening date of all accounts involved in the transaction // in case the newest opening date is newer than the transaction post date, do one // of the following: // // a) for category and stock accounts: update the opening date of the account // b) for account types where the user cannot modify the opening date through // the UI issue a warning (for each account only once) // c) others will be caught later if (!acc.isIncomeExpense() && !acc.isInvest()) { if (acc.openingDate() > t.postDate()) { if (!accountOpeningDate.isValid() || acc.openingDate() > accountOpeningDate) { accountOpeningDate = acc.openingDate(); } accountList << this->accountToCategory(acc.id()); if (!supportedAccountTypes.contains(acc.accountType()) && !reportedUnsupportedAccounts.contains(acc.id())) { rc << i18n(" * Opening date of Account '%1' cannot be changed to support transaction '%2' post date.", this->accountToCategory(acc.id()), t.id()); reportedUnsupportedAccounts << acc.id(); ++unfixedCount; } } } else { if (acc.openingDate() > t.postDate()) { rc << i18n(" * Transaction '%1' post date '%2' is older than opening date '%4' of account '%3'.", t.id(), t.postDate().toString(Qt::ISODate), this->accountToCategory(acc.id()), acc.openingDate().toString(Qt::ISODate)); rc << i18n(" Account opening date updated."); MyMoneyAccount newAcc = acc; newAcc.setOpeningDate(t.postDate()); this->modifyAccount(newAcc); ++problemCount; } } // make sure, that shares and value have the same number if they // represent the same currency. if (t.commodity() == acc.currencyId() && s.shares().reduce() != s.value().reduce()) { // use the value as master if the transaction is balanced if (t.splitSum().isZero()) { s.setShares(s.value()); rc << i18n(" * shares set to value in split of transaction '%1'.", t.id()); } else { s.setValue(s.shares()); rc << i18n(" * value set to shares in split of transaction '%1'.", t.id()); } sChanged = true; ++problemCount; } } catch (const MyMoneyException &) { rc << i18n(" * Split %2 in transaction '%1' contains a reference to invalid account %3. Please fix manually.", t.id(), split.id(), split.accountId()); ++unfixedCount; } // make sure the interest splits are marked correct as such if (interestAccounts.find(s.accountId()) != interestAccounts.end() && s.action() != MyMoneySplit::actionName(eMyMoney::Split::Action::Interest)) { s.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::Interest)); sChanged = true; rc << i18n(" * action marked as interest in split of transaction '%1'.", t.id()); ++problemCount; } if (sChanged) { tChanged = true; t.modifySplit(s); } } // make sure that the transaction's post date is valid if (!t.postDate().isValid()) { tChanged = true; t.setPostDate(t.entryDate().isValid() ? t.entryDate() : QDate::currentDate()); rc << i18n(" * Transaction '%1' has an invalid post date.", t.id()); rc << i18n(" The post date was updated to '%1'.", QLocale().toString(t.postDate(), QLocale::ShortFormat)); ++problemCount; } // check if the transaction's post date is after the opening date // of all accounts involved in the transaction. In case it is not, // issue a warning with the details about the transaction incl. // the account names and dates involved if (accountOpeningDate.isValid() && t.postDate() < accountOpeningDate) { QDate originalPostDate = t.postDate(); #if 0 // for now we do not activate the logic to move the post date to a later // point in time. This could cause some severe trouble if you have lots // of ancient data collected with older versions of KMyMoney that did not // enforce certain conditions like we do now. t.setPostDate(accountOpeningDate); tChanged = true; // copy the price information for investments to the new date QList::const_iterator it_t; foreach (const auto split, t.splits()) { if ((split.action() != "Buy") && (split.action() != "Reinvest")) { continue; } QString id = split.accountId(); auto acc = this->account(id); MyMoneySecurity sec = this->security(acc.currencyId()); MyMoneyPrice price(acc.currencyId(), sec.tradingCurrency(), t.postDate(), split.price(), "Transaction"); this->addPrice(price); break; } #endif rc << i18n(" * Transaction '%1' has a post date '%2' before one of the referenced account's opening date.", t.id(), QLocale().toString(originalPostDate, QLocale::ShortFormat)); rc << i18n(" Referenced accounts: %1", accountList.join(",")); rc << i18n(" The post date was not updated to '%1'.", QLocale().toString(accountOpeningDate, QLocale::ShortFormat)); ++unfixedCount; } if (tChanged) { d->m_storage->modifyTransaction(t); } } // Fix the schedules QList schList = scheduleList(); for (it_sch = schList.begin(); it_sch != schList.end(); ++it_sch) { MyMoneySchedule sch = (*it_sch); MyMoneyTransaction t = sch.transaction(); auto tChanged = false; foreach (const auto split, t.splits()) { MyMoneySplit s = split; bool sChanged = false; if (payeeConversionMap.find(split.payeeId()) != payeeConversionMap.end()) { s.setPayeeId(payeeConversionMap[s.payeeId()]); sChanged = true; rc << i18n(" * Payee id updated in split of schedule '%1'.", (*it_sch).name()); ++problemCount; } if (!split.value().isZero() && split.shares().isZero()) { s.setShares(s.value()); sChanged = true; rc << i18n(" * Split in scheduled transaction '%1' contained value != 0 and shares == 0.", (*it_sch).name()); rc << i18n(" Shares set to value."); ++problemCount; } // make sure, we don't have a bankid stored with a split in a schedule if (!split.bankID().isEmpty()) { s.setBankID(QString()); sChanged = true; rc << i18n(" * Removed bankid from split in scheduled transaction '%1'.", (*it_sch).name()); ++problemCount; } // make sure, that shares and value have the same number if they // represent the same currency. try { const auto acc = this->account(s.accountId()); if (t.commodity() == acc.currencyId() && s.shares().reduce() != s.value().reduce()) { // use the value as master if the transaction is balanced if (t.splitSum().isZero()) { s.setShares(s.value()); rc << i18n(" * shares set to value in split in schedule '%1'.", (*it_sch).name()); } else { s.setValue(s.shares()); rc << i18n(" * value set to shares in split in schedule '%1'.", (*it_sch).name()); } sChanged = true; ++problemCount; } } catch (const MyMoneyException &) { rc << i18n(" * Split %2 in schedule '%1' contains a reference to invalid account %3. Please fix manually.", (*it_sch).name(), split.id(), split.accountId()); ++unfixedCount; } if (sChanged) { t.modifySplit(s); tChanged = true; } } if (tChanged) { sch.setTransaction(t); d->m_storage->modifySchedule(sch); } } // Fix the reports QList rList = reportList(); for (it_r = rList.begin(); it_r != rList.end(); ++it_r) { MyMoneyReport r = *it_r; - QStringList pList; - QStringList::Iterator it_p; - (*it_r).payees(pList); + QStringList payeeList; + (*it_r).payees(payeeList); bool rChanged = false; - for (it_p = pList.begin(); it_p != pList.end(); ++it_p) { - if (payeeConversionMap.find(*it_p) != payeeConversionMap.end()) { + for (auto it_payee = payeeList.begin(); it_payee != payeeList.end(); ++it_payee) { + if (payeeConversionMap.find(*it_payee) != payeeConversionMap.end()) { rc << i18n(" * Payee id updated in report '%1'.", (*it_r).name()); ++problemCount; - r.removeReference(*it_p); - r.addPayee(payeeConversionMap[*it_p]); + r.removeReference(*it_payee); + r.addPayee(payeeConversionMap[*it_payee]); rChanged = true; } } if (rChanged) { d->m_storage->modifyReport(r); } } // erase old payee ids QMap::Iterator it_m; for (it_m = payeeConversionMap.begin(); it_m != payeeConversionMap.end(); ++it_m) { MyMoneyPayee payee = this->payee(it_m.key()); removePayee(payee); rc << i18n(" * Payee '%1' removed.", payee.id()); ++problemCount; } //look for accounts which have currencies other than the base currency but no price on the opening date //all accounts using base currency are excluded, since that's the base used for foreing currency calculation //thus it is considered as always present //accounts that represent Income/Expense categories are also excluded as price is irrelevant for their //fake opening date since a forex rate is required for all multi-currency transactions //get all currencies in use QStringList currencyList; QList accountForeignCurrency; QList accList; accountList(accList); QList::const_iterator account_it; for (account_it = accList.constBegin(); account_it != accList.constEnd(); ++account_it) { MyMoneyAccount account = *account_it; if (!account.isIncomeExpense() && !currencyList.contains(account.currencyId()) && account.currencyId() != baseCurrency().id() && !account.currencyId().isEmpty()) { //add the currency and the account-currency pair currencyList.append(account.currencyId()); accountForeignCurrency.append(account); } } MyMoneyPriceList pricesList = priceList(); QMap securityPriceDate; //get the first date of the price for each security MyMoneyPriceList::const_iterator prices_it; for (prices_it = pricesList.constBegin(); prices_it != pricesList.constEnd(); ++prices_it) { MyMoneyPrice firstPrice = (*((*prices_it).constBegin())); //only check the price if the currency is in use if (currencyList.contains(firstPrice.from()) || currencyList.contains(firstPrice.to())) { //check the security in the from field //if it is there, check if it is older QPair pricePair = qMakePair(firstPrice.from(), firstPrice.to()); securityPriceDate[pricePair] = firstPrice.date(); } } //compare the dates with the opening dates of the accounts using each currency QList::const_iterator accForeignList_it; bool firstInvProblem = true; for (accForeignList_it = accountForeignCurrency.constBegin(); accForeignList_it != accountForeignCurrency.constEnd(); ++accForeignList_it) { //setup the price pair correctly QPair pricePair; //setup the reverse, which can also be used for rate conversion QPair reversePricePair; if ((*accForeignList_it).isInvest()) { //if it is a stock, we have to search for a price from its stock to the currency of the account QString securityId = (*accForeignList_it).currencyId(); QString tradingCurrencyId = security(securityId).tradingCurrency(); pricePair = qMakePair(securityId, tradingCurrencyId); reversePricePair = qMakePair(tradingCurrencyId, securityId); } else { //if it is a regular account we search for a price from the currency of the account to the base currency QString currency = (*accForeignList_it).currencyId(); QString baseCurrencyId = baseCurrency().id(); pricePair = qMakePair(currency, baseCurrencyId); reversePricePair = qMakePair(baseCurrencyId, currency); } //compare the first price with the opening date of the account if ((!securityPriceDate.contains(pricePair) || securityPriceDate.value(pricePair) > (*accForeignList_it).openingDate()) && (!securityPriceDate.contains(reversePricePair) || securityPriceDate.value(reversePricePair) > (*accForeignList_it).openingDate())) { if (firstInvProblem) { firstInvProblem = false; rc << i18n("* Potential problem with investments/currencies"); } QDate openingDate = (*accForeignList_it).openingDate(); MyMoneySecurity secError = security((*accForeignList_it).currencyId()); if (!(*accForeignList_it).isInvest()) { rc << i18n(" * The account '%1' in currency '%2' has no price set for the opening date '%3'.", (*accForeignList_it).name(), secError.name(), openingDate.toString(Qt::ISODate)); rc << i18n(" Please enter a price for the currency on or before the opening date."); } else { rc << i18n(" * The investment '%1' has no price set for the opening date '%2'.", (*accForeignList_it).name(), openingDate.toString(Qt::ISODate)); rc << i18n(" Please enter a price for the investment on or before the opening date."); } ++unfixedCount; } } // Fix the budgets that somehow still reference invalid accounts QString problemBudget; QList bList = budgetList(); for (QList::const_iterator it_b = bList.constBegin(); it_b != bList.constEnd(); ++it_b) { MyMoneyBudget b = *it_b; QList baccounts = b.getaccounts(); bool bChanged = false; for (QList::const_iterator it_bacc = baccounts.constBegin(); it_bacc != baccounts.constEnd(); ++it_bacc) { try { account((*it_bacc).id()); } catch (const MyMoneyException &) { problemCount++; if (problemBudget != b.name()) { problemBudget = b.name(); rc << i18n("* Problem with budget '%1'", problemBudget); } rc << i18n(" * The account with id %1 referenced by the budget does not exist anymore.", (*it_bacc).id()); rc << i18n(" The account reference will be removed."); // remove the reference to the account b.removeReference((*it_bacc).id()); bChanged = true; } } if (bChanged) { d->m_storage->modifyBudget(b); } } // add more checks here if (problemCount == 0 && unfixedCount == 0) { rc << i18n("Finished: data is consistent."); } else { const QString problemsCorrected = i18np("%1 problem corrected.", "%1 problems corrected.", problemCount); const QString problemsRemaining = i18np("%1 problem still present.", "%1 problems still present.", unfixedCount); rc << QString(); rc << i18nc("%1 is a string, e.g. 7 problems corrected; %2 is a string, e.g. 3 problems still present", "Finished: %1 %2", problemsCorrected, problemsRemaining); } return rc; } QString MyMoneyFile::createCategory(const MyMoneyAccount& base, const QString& name) { d->checkTransaction(Q_FUNC_INFO); MyMoneyAccount parent = base; QString categoryText; if (base.id() != expense().id() && base.id() != income().id()) throw MYMONEYEXCEPTION("Invalid base category"); QStringList subAccounts = name.split(AccountSeparator); QStringList::Iterator it; for (it = subAccounts.begin(); it != subAccounts.end(); ++it) { MyMoneyAccount categoryAccount; categoryAccount.setName(*it); categoryAccount.setAccountType(base.accountType()); if (it == subAccounts.begin()) categoryText += *it; else categoryText += (AccountSeparator + *it); // Only create the account if it doesn't exist try { QString categoryId = categoryToAccount(categoryText); if (categoryId.isEmpty()) addAccount(categoryAccount, parent); else { categoryAccount = account(categoryId); } } catch (const MyMoneyException &e) { qDebug("Unable to add account %s, %s, %s: %s", qPrintable(categoryAccount.name()), qPrintable(parent.name()), qPrintable(categoryText), qPrintable(e.what())); } parent = categoryAccount; } return categoryToAccount(name); } QString MyMoneyFile::checkCategory(const QString& name, const MyMoneyMoney& value, const MyMoneyMoney& value2) { QString accountId; MyMoneyAccount newAccount; bool found = true; if (!name.isEmpty()) { // The category might be constructed with an arbitraty depth (number of // colon delimited fields). We try to find a parent account within this // hierarchy by searching the following sequence: // // aaaa:bbbb:cccc:ddddd // // 1. search aaaa:bbbb:cccc:dddd, create nothing // 2. search aaaa:bbbb:cccc , create dddd // 3. search aaaa:bbbb , create cccc:dddd // 4. search aaaa , create bbbb:cccc:dddd // 5. don't search , create aaaa:bbbb:cccc:dddd newAccount.setName(name); QString accName; // part to be created (right side in above list) QString parent(name); // a possible parent part (left side in above list) do { accountId = categoryToAccount(parent); if (accountId.isEmpty()) { found = false; // prepare next step if (!accName.isEmpty()) accName.prepend(':'); accName.prepend(parent.section(':', -1)); newAccount.setName(accName); parent = parent.section(':', 0, -2); } else if (!accName.isEmpty()) { newAccount.setParentAccountId(accountId); } } while (!parent.isEmpty() && accountId.isEmpty()); // if we did not find the category, we create it if (!found) { - MyMoneyAccount parent; + MyMoneyAccount parentAccount; if (newAccount.parentAccountId().isEmpty()) { if (!value.isNegative() && value2.isNegative()) - parent = income(); + parentAccount = income(); else - parent = expense(); + parentAccount = expense(); } else { - parent = account(newAccount.parentAccountId()); + parentAccount = account(newAccount.parentAccountId()); } newAccount.setAccountType((!value.isNegative() && value2.isNegative()) ? Account::Type::Income : Account::Type::Expense); MyMoneyAccount brokerage; // clear out the parent id, because createAccount() does not like that newAccount.setParentAccountId(QString()); - createAccount(newAccount, parent, brokerage, MyMoneyMoney()); + createAccount(newAccount, parentAccount, brokerage, MyMoneyMoney()); accountId = newAccount.id(); } } return accountId; } void MyMoneyFile::addSecurity(MyMoneySecurity& security) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->addSecurity(security); d->m_changeSet += MyMoneyNotification(File::Mode::Add, security); } void MyMoneyFile::modifySecurity(const MyMoneySecurity& security) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->modifySecurity(security); d->m_changeSet += MyMoneyNotification(File::Mode::Modify, security); } void MyMoneyFile::removeSecurity(const MyMoneySecurity& security) { d->checkTransaction(Q_FUNC_INFO); // FIXME check that security is not referenced by other object d->m_storage->removeSecurity(security); d->m_changeSet += MyMoneyNotification(File::Mode::Remove, security); } MyMoneySecurity MyMoneyFile::security(const QString& id) const { if (Q_UNLIKELY(id.isEmpty())) return baseCurrency(); return d->m_storage->security(id); } QList MyMoneyFile::securityList() const { d->checkStorage(); return d->m_storage->securityList(); } void MyMoneyFile::addCurrency(const MyMoneySecurity& currency) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->addCurrency(currency); d->m_changeSet += MyMoneyNotification(File::Mode::Add, currency); } void MyMoneyFile::modifyCurrency(const MyMoneySecurity& currency) { d->checkTransaction(Q_FUNC_INFO); // force reload of base currency object if (currency.id() == d->m_baseCurrency.id()) d->m_baseCurrency.clearId(); d->m_storage->modifyCurrency(currency); d->m_changeSet += MyMoneyNotification(File::Mode::Modify, currency); } void MyMoneyFile::removeCurrency(const MyMoneySecurity& currency) { d->checkTransaction(Q_FUNC_INFO); if (currency.id() == d->m_baseCurrency.id()) throw MYMONEYEXCEPTION("Cannot delete base currency."); // FIXME check that security is not referenced by other object d->m_storage->removeCurrency(currency); d->m_changeSet += MyMoneyNotification(File::Mode::Remove, currency); } MyMoneySecurity MyMoneyFile::currency(const QString& id) const { if (id.isEmpty()) return baseCurrency(); try { const auto currency = d->m_storage->currency(id); if (currency.id().isEmpty()) throw MYMONEYEXCEPTION(QString::fromLatin1("Currency '%1' not found.").arg(id)); return currency; } catch(const MyMoneyException&) { const auto security = d->m_storage->security(id); if (security.id().isEmpty()) { throw MYMONEYEXCEPTION(QString::fromLatin1("Security '%1' not found.").arg(id)); } return security; } } QMap MyMoneyFile::ancientCurrencies() const { QMap ancientCurrencies; ancientCurrencies.insert(MyMoneySecurity("ATS", i18n("Austrian Schilling"), QString::fromUtf8("ÖS")), MyMoneyPrice("ATS", "EUR", QDate(1998, 12, 31), MyMoneyMoney(10000, 137603), QLatin1Literal("KMyMoney"))); ancientCurrencies.insert(MyMoneySecurity("DEM", i18n("German Mark"), "DM"), MyMoneyPrice("ATS", "EUR", QDate(1998, 12, 31), MyMoneyMoney(100000, 195583), QLatin1Literal("KMyMoney"))); ancientCurrencies.insert(MyMoneySecurity("FRF", i18n("French Franc"), "FF"), MyMoneyPrice("FRF", "EUR", QDate(1998, 12, 31), MyMoneyMoney(100000, 655957), QLatin1Literal("KMyMoney"))); ancientCurrencies.insert(MyMoneySecurity("ITL", i18n("Italian Lira"), QChar(0x20A4)), MyMoneyPrice("ITL", "EUR", QDate(1998, 12, 31), MyMoneyMoney(100, 193627), QLatin1Literal("KMyMoney"))); ancientCurrencies.insert(MyMoneySecurity("ESP", i18n("Spanish Peseta"), QString()), MyMoneyPrice("ESP", "EUR", QDate(1998, 12, 31), MyMoneyMoney(1000, 166386), QLatin1Literal("KMyMoney"))); ancientCurrencies.insert(MyMoneySecurity("NLG", i18n("Dutch Guilder"), QString()), MyMoneyPrice("NLG", "EUR", QDate(1998, 12, 31), MyMoneyMoney(100000, 220371), QLatin1Literal("KMyMoney"))); ancientCurrencies.insert(MyMoneySecurity("BEF", i18n("Belgian Franc"), "Fr"), MyMoneyPrice("BEF", "EUR", QDate(1998, 12, 31), MyMoneyMoney(10000, 403399), QLatin1Literal("KMyMoney"))); ancientCurrencies.insert(MyMoneySecurity("LUF", i18n("Luxembourg Franc"), "Fr"), MyMoneyPrice("LUF", "EUR", QDate(1998, 12, 31), MyMoneyMoney(10000, 403399), QLatin1Literal("KMyMoney"))); ancientCurrencies.insert(MyMoneySecurity("PTE", i18n("Portuguese Escudo"), QString()), MyMoneyPrice("PTE", "EUR", QDate(1998, 12, 31), MyMoneyMoney(1000, 200482), QLatin1Literal("KMyMoney"))); ancientCurrencies.insert(MyMoneySecurity("IEP", i18n("Irish Pound"), QChar(0x00A3)), MyMoneyPrice("IEP", "EUR", QDate(1998, 12, 31), MyMoneyMoney(1000000, 787564), QLatin1Literal("KMyMoney"))); ancientCurrencies.insert(MyMoneySecurity("FIM", i18n("Finnish Markka"), QString()), MyMoneyPrice("FIM", "EUR", QDate(1998, 12, 31), MyMoneyMoney(100000, 594573), QLatin1Literal("KMyMoney"))); ancientCurrencies.insert(MyMoneySecurity("GRD", i18n("Greek Drachma"), QChar(0x20AF)), MyMoneyPrice("GRD", "EUR", QDate(1998, 12, 31), MyMoneyMoney(100, 34075), QLatin1Literal("KMyMoney"))); // http://en.wikipedia.org/wiki/Bulgarian_lev ancientCurrencies.insert(MyMoneySecurity("BGL", i18n("Bulgarian Lev"), "BGL"), MyMoneyPrice("BGL", "BGN", QDate(1999, 7, 5), MyMoneyMoney(1, 1000), QLatin1Literal("KMyMoney"))); ancientCurrencies.insert(MyMoneySecurity("ROL", i18n("Romanian Leu"), "ROL"), MyMoneyPrice("ROL", "RON", QDate(2005, 6, 30), MyMoneyMoney(1, 10000), QLatin1Literal("KMyMoney"))); ancientCurrencies.insert(MyMoneySecurity("RUR", i18n("Russian Ruble (old)"), "RUR"), MyMoneyPrice("RUR", "RUB", QDate(1998, 1, 1), MyMoneyMoney(1, 1000), QLatin1Literal("KMyMoney"))); ancientCurrencies.insert(MyMoneySecurity("SIT", i18n("Slovenian Tolar"), "SIT"), MyMoneyPrice("SIT", "EUR", QDate(2006, 12, 31), MyMoneyMoney(1, 23964), QLatin1Literal("KMyMoney"))); // Source: http://www.tf-portfoliosolutions.net/products/turkishlira.aspx ancientCurrencies.insert(MyMoneySecurity("TRL", i18n("Turkish Lira (old)"), "TL"), MyMoneyPrice("TRL", "TRY", QDate(2004, 12, 31), MyMoneyMoney(1, 1000000), QLatin1Literal("KMyMoney"))); // Source: http://www.focus.de/finanzen/news/malta-und-zypern_aid_66058.html ancientCurrencies.insert(MyMoneySecurity("MTL", i18n("Maltese Lira"), "MTL"), MyMoneyPrice("MTL", "EUR", QDate(2008, 1, 1), MyMoneyMoney(429300, 1000000), QLatin1Literal("KMyMoney"))); ancientCurrencies.insert(MyMoneySecurity("CYP", i18n("Cyprus Pound"), QString("C%1").arg(QChar(0x00A3))), MyMoneyPrice("CYP", "EUR", QDate(2008, 1, 1), MyMoneyMoney(585274, 1000000), QLatin1Literal("KMyMoney"))); // Source: http://www.focus.de/finanzen/news/waehrungszone-slowakei-ist-neuer-euro-staat_aid_359025.html ancientCurrencies.insert(MyMoneySecurity("SKK", i18n("Slovak Koruna"), "SKK"), MyMoneyPrice("SKK", "EUR", QDate(2008, 12, 31), MyMoneyMoney(1000, 30126), QLatin1Literal("KMyMoney"))); // Source: http://en.wikipedia.org/wiki/Mozambican_metical ancientCurrencies.insert(MyMoneySecurity("MZM", i18n("Mozambique Metical"), "MT"), MyMoneyPrice("MZM", "MZN", QDate(2006, 7, 1), MyMoneyMoney(1, 1000), QLatin1Literal("KMyMoney"))); // Source https://en.wikipedia.org/wiki/Azerbaijani_manat ancientCurrencies.insert(MyMoneySecurity("AZM", i18n("Azerbaijani Manat"), "m."), MyMoneyPrice("AZM", "AZN", QDate(2006, 1, 1), MyMoneyMoney(1, 5000), QLatin1Literal("KMyMoney"))); // Source: https://en.wikipedia.org/wiki/Litas ancientCurrencies.insert(MyMoneySecurity("LTL", i18n("Lithuanian Litas"), "Lt"), MyMoneyPrice("LTL", "EUR", QDate(2015, 1, 1), MyMoneyMoney(100000, 345280), QLatin1Literal("KMyMoney"))); // Source: https://en.wikipedia.org/wiki/Belarusian_ruble ancientCurrencies.insert(MyMoneySecurity("BYR", i18n("Belarusian Ruble (old)"), "BYR"), MyMoneyPrice("BYR", "BYN", QDate(2016, 7, 1), MyMoneyMoney(1, 10000), QLatin1Literal("KMyMoney"))); return ancientCurrencies; } QList MyMoneyFile::availableCurrencyList() const { QList currencyList; currencyList.append(MyMoneySecurity("AFA", i18n("Afghanistan Afghani"))); currencyList.append(MyMoneySecurity("ALL", i18n("Albanian Lek"))); currencyList.append(MyMoneySecurity("ANG", i18n("Netherland Antillian Guilder"))); currencyList.append(MyMoneySecurity("DZD", i18n("Algerian Dinar"))); currencyList.append(MyMoneySecurity("ADF", i18n("Andorran Franc"))); currencyList.append(MyMoneySecurity("ADP", i18n("Andorran Peseta"))); currencyList.append(MyMoneySecurity("AON", i18n("Angolan New Kwanza"))); currencyList.append(MyMoneySecurity("ARS", i18n("Argentine Peso"), "$")); currencyList.append(MyMoneySecurity("AWG", i18n("Aruban Florin"))); currencyList.append(MyMoneySecurity("AUD", i18n("Australian Dollar"), "$")); currencyList.append(MyMoneySecurity("AZN", i18n("Azerbaijani Manat"), "m.")); currencyList.append(MyMoneySecurity("BSD", i18n("Bahamian Dollar"), "$")); currencyList.append(MyMoneySecurity("BHD", i18n("Bahraini Dinar"), "BHD", 1000)); currencyList.append(MyMoneySecurity("BDT", i18n("Bangladeshi Taka"))); currencyList.append(MyMoneySecurity("BBD", i18n("Barbados Dollar"), "$")); currencyList.append(MyMoneySecurity("BTC", i18n("Bitcoin"), "BTC")); currencyList.append(MyMoneySecurity("BYN", i18n("Belarusian Ruble"), "Br")); currencyList.append(MyMoneySecurity("BZD", i18n("Belize Dollar"), "$")); currencyList.append(MyMoneySecurity("BMD", i18n("Bermudian Dollar"), "$")); currencyList.append(MyMoneySecurity("BTN", i18n("Bhutan Ngultrum"))); currencyList.append(MyMoneySecurity("BOB", i18n("Bolivian Boliviano"))); currencyList.append(MyMoneySecurity("BAM", i18n("Bosnian Convertible Mark"))); currencyList.append(MyMoneySecurity("BWP", i18n("Botswana Pula"))); currencyList.append(MyMoneySecurity("BRL", i18n("Brazilian Real"), "R$")); currencyList.append(MyMoneySecurity("GBP", i18n("British Pound"), QChar(0x00A3))); currencyList.append(MyMoneySecurity("BND", i18n("Brunei Dollar"), "$")); currencyList.append(MyMoneySecurity("BGN", i18n("Bulgarian Lev (new)"))); currencyList.append(MyMoneySecurity("BIF", i18n("Burundi Franc"))); currencyList.append(MyMoneySecurity("XAF", i18n("CFA Franc BEAC"))); currencyList.append(MyMoneySecurity("XOF", i18n("CFA Franc BCEAO"))); currencyList.append(MyMoneySecurity("XPF", i18n("CFP Franc Pacifique"), "F", 1, 100)); currencyList.append(MyMoneySecurity("KHR", i18n("Cambodia Riel"))); currencyList.append(MyMoneySecurity("CAD", i18n("Canadian Dollar"), "$")); currencyList.append(MyMoneySecurity("CVE", i18n("Cape Verde Escudo"))); currencyList.append(MyMoneySecurity("KYD", i18n("Cayman Islands Dollar"), "$")); currencyList.append(MyMoneySecurity("CLP", i18n("Chilean Peso"))); currencyList.append(MyMoneySecurity("CNY", i18n("Chinese Yuan Renminbi"))); currencyList.append(MyMoneySecurity("COP", i18n("Colombian Peso"))); currencyList.append(MyMoneySecurity("KMF", i18n("Comoros Franc"))); currencyList.append(MyMoneySecurity("CRC", i18n("Costa Rican Colon"), QChar(0x20A1))); currencyList.append(MyMoneySecurity("HRK", i18n("Croatian Kuna"))); currencyList.append(MyMoneySecurity("CUP", i18n("Cuban Peso"))); currencyList.append(MyMoneySecurity("CZK", i18n("Czech Koruna"))); currencyList.append(MyMoneySecurity("DKK", i18n("Danish Krone"), "kr")); currencyList.append(MyMoneySecurity("DJF", i18n("Djibouti Franc"))); currencyList.append(MyMoneySecurity("DOP", i18n("Dominican Peso"))); currencyList.append(MyMoneySecurity("XCD", i18n("East Caribbean Dollar"), "$")); currencyList.append(MyMoneySecurity("EGP", i18n("Egyptian Pound"), QChar(0x00A3))); currencyList.append(MyMoneySecurity("SVC", i18n("El Salvador Colon"))); currencyList.append(MyMoneySecurity("ERN", i18n("Eritrean Nakfa"))); currencyList.append(MyMoneySecurity("EEK", i18n("Estonian Kroon"))); currencyList.append(MyMoneySecurity("ETB", i18n("Ethiopian Birr"))); currencyList.append(MyMoneySecurity("EUR", i18n("Euro"), QChar(0x20ac))); currencyList.append(MyMoneySecurity("FKP", i18n("Falkland Islands Pound"), QChar(0x00A3))); currencyList.append(MyMoneySecurity("FJD", i18n("Fiji Dollar"), "$")); currencyList.append(MyMoneySecurity("GMD", i18n("Gambian Dalasi"))); currencyList.append(MyMoneySecurity("GEL", i18n("Georgian Lari"))); currencyList.append(MyMoneySecurity("GHC", i18n("Ghanaian Cedi"))); currencyList.append(MyMoneySecurity("GIP", i18n("Gibraltar Pound"), QChar(0x00A3))); currencyList.append(MyMoneySecurity("GTQ", i18n("Guatemalan Quetzal"))); currencyList.append(MyMoneySecurity("GWP", i18n("Guinea-Bissau Peso"))); currencyList.append(MyMoneySecurity("GYD", i18n("Guyanan Dollar"), "$")); currencyList.append(MyMoneySecurity("HTG", i18n("Haitian Gourde"))); currencyList.append(MyMoneySecurity("HNL", i18n("Honduran Lempira"))); currencyList.append(MyMoneySecurity("HKD", i18n("Hong Kong Dollar"), "$")); currencyList.append(MyMoneySecurity("HUF", i18n("Hungarian Forint"), "HUF", 1, 100)); currencyList.append(MyMoneySecurity("ISK", i18n("Iceland Krona"))); currencyList.append(MyMoneySecurity("INR", i18n("Indian Rupee"), QChar(0x20A8))); currencyList.append(MyMoneySecurity("IDR", i18n("Indonesian Rupiah"), "IDR", 1)); currencyList.append(MyMoneySecurity("IRR", i18n("Iranian Rial"), "IRR", 1)); currencyList.append(MyMoneySecurity("IQD", i18n("Iraqi Dinar"), "IQD", 1000)); currencyList.append(MyMoneySecurity("ILS", i18n("Israeli New Shekel"), QChar(0x20AA))); currencyList.append(MyMoneySecurity("JMD", i18n("Jamaican Dollar"), "$")); currencyList.append(MyMoneySecurity("JPY", i18n("Japanese Yen"), QChar(0x00A5), 1)); currencyList.append(MyMoneySecurity("JOD", i18n("Jordanian Dinar"), "JOD", 1000)); currencyList.append(MyMoneySecurity("KZT", i18n("Kazakhstan Tenge"))); currencyList.append(MyMoneySecurity("KES", i18n("Kenyan Shilling"))); currencyList.append(MyMoneySecurity("KWD", i18n("Kuwaiti Dinar"), "KWD", 1000)); currencyList.append(MyMoneySecurity("KGS", i18n("Kyrgyzstan Som"))); currencyList.append(MyMoneySecurity("LAK", i18n("Laos Kip"), QChar(0x20AD))); currencyList.append(MyMoneySecurity("LVL", i18n("Latvian Lats"))); currencyList.append(MyMoneySecurity("LBP", i18n("Lebanese Pound"), QChar(0x00A3))); currencyList.append(MyMoneySecurity("LSL", i18n("Lesotho Loti"))); currencyList.append(MyMoneySecurity("LRD", i18n("Liberian Dollar"), "$")); currencyList.append(MyMoneySecurity("LYD", i18n("Libyan Dinar"), "LYD", 1000)); currencyList.append(MyMoneySecurity("MOP", i18n("Macau Pataca"))); currencyList.append(MyMoneySecurity("MKD", i18n("Macedonian Denar"))); currencyList.append(MyMoneySecurity("MGF", i18n("Malagasy Franc"), "MGF", 500)); currencyList.append(MyMoneySecurity("MWK", i18n("Malawi Kwacha"))); currencyList.append(MyMoneySecurity("MYR", i18n("Malaysian Ringgit"))); currencyList.append(MyMoneySecurity("MVR", i18n("Maldive Rufiyaa"))); currencyList.append(MyMoneySecurity("MLF", i18n("Mali Republic Franc"))); currencyList.append(MyMoneySecurity("MRO", i18n("Mauritanian Ouguiya"), "MRO", 5)); currencyList.append(MyMoneySecurity("MUR", i18n("Mauritius Rupee"))); currencyList.append(MyMoneySecurity("MXN", i18n("Mexican Peso"), "$")); currencyList.append(MyMoneySecurity("MDL", i18n("Moldavian Leu"))); currencyList.append(MyMoneySecurity("MNT", i18n("Mongolian Tugrik"), QChar(0x20AE))); currencyList.append(MyMoneySecurity("MAD", i18n("Moroccan Dirham"))); currencyList.append(MyMoneySecurity("MZN", i18n("Mozambique Metical"), "MT")); currencyList.append(MyMoneySecurity("MMK", i18n("Myanmar Kyat"))); currencyList.append(MyMoneySecurity("NAD", i18n("Namibian Dollar"), "$")); currencyList.append(MyMoneySecurity("NPR", i18n("Nepalese Rupee"))); currencyList.append(MyMoneySecurity("NZD", i18n("New Zealand Dollar"), "$")); currencyList.append(MyMoneySecurity("NIC", i18n("Nicaraguan Cordoba Oro"))); currencyList.append(MyMoneySecurity("NGN", i18n("Nigerian Naira"), QChar(0x20A6))); currencyList.append(MyMoneySecurity("KPW", i18n("North Korean Won"), QChar(0x20A9))); currencyList.append(MyMoneySecurity("NOK", i18n("Norwegian Kroner"), "kr")); currencyList.append(MyMoneySecurity("OMR", i18n("Omani Rial"), "OMR", 1000)); currencyList.append(MyMoneySecurity("PKR", i18n("Pakistan Rupee"))); currencyList.append(MyMoneySecurity("PAB", i18n("Panamanian Balboa"))); currencyList.append(MyMoneySecurity("PGK", i18n("Papua New Guinea Kina"))); currencyList.append(MyMoneySecurity("PYG", i18n("Paraguay Guarani"))); currencyList.append(MyMoneySecurity("PEN", i18n("Peruvian Nuevo Sol"))); currencyList.append(MyMoneySecurity("PHP", i18n("Philippine Peso"), QChar(0x20B1))); currencyList.append(MyMoneySecurity("PLN", i18n("Polish Zloty"))); currencyList.append(MyMoneySecurity("QAR", i18n("Qatari Rial"))); currencyList.append(MyMoneySecurity("RON", i18n("Romanian Leu (new)"))); currencyList.append(MyMoneySecurity("RUB", i18n("Russian Ruble"))); currencyList.append(MyMoneySecurity("RWF", i18n("Rwanda Franc"))); currencyList.append(MyMoneySecurity("WST", i18n("Samoan Tala"))); currencyList.append(MyMoneySecurity("STD", i18n("Sao Tome and Principe Dobra"))); currencyList.append(MyMoneySecurity("SAR", i18n("Saudi Riyal"))); currencyList.append(MyMoneySecurity("RSD", i18n("Serbian Dinar"))); currencyList.append(MyMoneySecurity("SCR", i18n("Seychelles Rupee"))); currencyList.append(MyMoneySecurity("SLL", i18n("Sierra Leone Leone"))); currencyList.append(MyMoneySecurity("SGD", i18n("Singapore Dollar"), "$")); currencyList.append(MyMoneySecurity("SBD", i18n("Solomon Islands Dollar"), "$")); currencyList.append(MyMoneySecurity("SOS", i18n("Somali Shilling"))); currencyList.append(MyMoneySecurity("ZAR", i18n("South African Rand"))); currencyList.append(MyMoneySecurity("KRW", i18n("South Korean Won"), QChar(0x20A9))); currencyList.append(MyMoneySecurity("LKR", i18n("Sri Lanka Rupee"))); currencyList.append(MyMoneySecurity("SHP", i18n("St. Helena Pound"), QChar(0x00A3))); currencyList.append(MyMoneySecurity("SDD", i18n("Sudanese Dinar"))); currencyList.append(MyMoneySecurity("SRG", i18n("Suriname Guilder"))); currencyList.append(MyMoneySecurity("SZL", i18n("Swaziland Lilangeni"))); currencyList.append(MyMoneySecurity("SEK", i18n("Swedish Krona"))); currencyList.append(MyMoneySecurity("CHF", i18n("Swiss Franc"), "SFr")); currencyList.append(MyMoneySecurity("SYP", i18n("Syrian Pound"), QChar(0x00A3))); currencyList.append(MyMoneySecurity("TWD", i18n("Taiwan Dollar"), "$")); currencyList.append(MyMoneySecurity("TJS", i18n("Tajikistan Somoni"))); currencyList.append(MyMoneySecurity("TZS", i18n("Tanzanian Shilling"))); currencyList.append(MyMoneySecurity("THB", i18n("Thai Baht"), QChar(0x0E3F))); currencyList.append(MyMoneySecurity("TOP", i18n("Tongan Pa'anga"))); currencyList.append(MyMoneySecurity("TTD", i18n("Trinidad and Tobago Dollar"), "$")); currencyList.append(MyMoneySecurity("TND", i18n("Tunisian Dinar"), "TND", 1000)); currencyList.append(MyMoneySecurity("TRY", i18n("Turkish Lira"), QChar(0x20BA))); currencyList.append(MyMoneySecurity("TMM", i18n("Turkmenistan Manat"))); currencyList.append(MyMoneySecurity("USD", i18n("US Dollar"), "$")); currencyList.append(MyMoneySecurity("UGX", i18n("Uganda Shilling"))); currencyList.append(MyMoneySecurity("UAH", i18n("Ukraine Hryvnia"))); currencyList.append(MyMoneySecurity("CLF", i18n("Unidad de Fometo"))); currencyList.append(MyMoneySecurity("AED", i18n("United Arab Emirates Dirham"))); currencyList.append(MyMoneySecurity("UYU", i18n("Uruguayan Peso"))); currencyList.append(MyMoneySecurity("UZS", i18n("Uzbekistani Sum"))); currencyList.append(MyMoneySecurity("VUV", i18n("Vanuatu Vatu"))); currencyList.append(MyMoneySecurity("VEB", i18n("Venezuelan Bolivar"))); currencyList.append(MyMoneySecurity("VND", i18n("Vietnamese Dong"), QChar(0x20AB))); currencyList.append(MyMoneySecurity("ZMK", i18n("Zambian Kwacha"))); currencyList.append(MyMoneySecurity("ZWD", i18n("Zimbabwe Dollar"), "$")); currencyList.append(ancientCurrencies().keys()); // sort the currencies ... qSort(currencyList.begin(), currencyList.end(), [] (const MyMoneySecurity& c1, const MyMoneySecurity& c2) { return c1.name().compare(c2.name()) < 0; }); // ... and add a few precious metals at the ned currencyList.append(MyMoneySecurity("XAU", i18n("Gold"), "XAU", 1000000)); currencyList.append(MyMoneySecurity("XPD", i18n("Palladium"), "XPD", 1000000)); currencyList.append(MyMoneySecurity("XPT", i18n("Platinum"), "XPT", 1000000)); currencyList.append(MyMoneySecurity("XAG", i18n("Silver"), "XAG", 1000000)); return currencyList; } QList MyMoneyFile::currencyList() const { d->checkStorage(); return d->m_storage->currencyList(); } QString MyMoneyFile::foreignCurrency(const QString& first, const QString& second) const { if (baseCurrency().id() == second) return first; return second; } MyMoneySecurity MyMoneyFile::baseCurrency() const { if (d->m_baseCurrency.id().isEmpty()) { QString id = QString(value("kmm-baseCurrency")); if (!id.isEmpty()) d->m_baseCurrency = currency(id); } return d->m_baseCurrency; } void MyMoneyFile::setBaseCurrency(const MyMoneySecurity& curr) { // make sure the currency exists MyMoneySecurity c = currency(curr.id()); if (c.id() != d->m_baseCurrency.id()) { setValue("kmm-baseCurrency", curr.id()); // force reload of base currency cache d->m_baseCurrency = MyMoneySecurity(); } } void MyMoneyFile::addPrice(const MyMoneyPrice& price) { if (price.rate(QString()).isZero()) return; d->checkTransaction(Q_FUNC_INFO); // store the account's which are affected by this price regarding their value d->priceChanged(*this, price); d->m_storage->addPrice(price); } void MyMoneyFile::removePrice(const MyMoneyPrice& price) { d->checkTransaction(Q_FUNC_INFO); // store the account's which are affected by this price regarding their value d->priceChanged(*this, price); d->m_storage->removePrice(price); } MyMoneyPrice MyMoneyFile::price(const QString& fromId, const QString& toId, const QDate& date, const bool exactDate) const { d->checkStorage(); QString to(toId); if (to.isEmpty()) to = value("kmm-baseCurrency"); // if some id is missing, we can return an empty price object if (fromId.isEmpty() || to.isEmpty()) return MyMoneyPrice(); // we don't search our tables if someone asks stupid stuff if (fromId == toId) { return MyMoneyPrice(fromId, toId, date, MyMoneyMoney::ONE, "KMyMoney"); } // if not asking for exact date, try to find the exact date match first, // either the requested price or its reciprocal value. If unsuccessful, it will move // on and look for prices of previous dates MyMoneyPrice rc = d->m_storage->price(fromId, to, date, true); if (!rc.isValid()) { // not found, search 'to-from' rate and use reciprocal value rc = d->m_storage->price(to, fromId, date, true); // not found, search previous dates, if exact date is not needed if (!exactDate && !rc.isValid()) { // search 'from-to' and 'to-from', select the most recent one MyMoneyPrice fromPrice = d->m_storage->price(fromId, to, date, exactDate); MyMoneyPrice toPrice = d->m_storage->price(to, fromId, date, exactDate); // check first whether both prices are valid if (fromPrice.isValid() && toPrice.isValid()) { if (fromPrice.date() >= toPrice.date()) { // if 'from-to' is newer or the same date, prefer that one rc = fromPrice; } else { // otherwise, use the reciprocal price rc = toPrice; } } else if (fromPrice.isValid()) { // check if any of the prices is valid, return that one rc = fromPrice; } else if (toPrice.isValid()) { rc = toPrice; } } } return rc; } MyMoneyPrice MyMoneyFile::price(const QString& fromId, const QString& toId) const { return price(fromId, toId, QDate::currentDate(), false); } MyMoneyPrice MyMoneyFile::price(const QString& fromId) const { return price(fromId, QString(), QDate::currentDate(), false); } MyMoneyPriceList MyMoneyFile::priceList() const { d->checkStorage(); return d->m_storage->priceList(); } bool MyMoneyFile::hasAccount(const QString& id, const QString& name) const { const auto accounts = account(id).accountList(); for (const auto& acc : accounts) { if (account(acc).name().compare(name) == 0) return true; } return false; } QList MyMoneyFile::reportList() const { d->checkStorage(); return d->m_storage->reportList(); } void MyMoneyFile::addReport(MyMoneyReport& report) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->addReport(report); } void MyMoneyFile::modifyReport(const MyMoneyReport& report) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->modifyReport(report); } unsigned MyMoneyFile::countReports() const { d->checkStorage(); return d->m_storage->countReports(); } MyMoneyReport MyMoneyFile::report(const QString& id) const { d->checkStorage(); return d->m_storage->report(id); } void MyMoneyFile::removeReport(const MyMoneyReport& report) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->removeReport(report); } QList MyMoneyFile::budgetList() const { d->checkStorage(); return d->m_storage->budgetList(); } void MyMoneyFile::addBudget(MyMoneyBudget &budget) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->addBudget(budget); } MyMoneyBudget MyMoneyFile::budgetByName(const QString& name) const { d->checkStorage(); return d->m_storage->budgetByName(name); } void MyMoneyFile::modifyBudget(const MyMoneyBudget& budget) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->modifyBudget(budget); } unsigned MyMoneyFile::countBudgets() const { d->checkStorage(); return d->m_storage->countBudgets(); } MyMoneyBudget MyMoneyFile::budget(const QString& id) const { d->checkStorage(); return d->m_storage->budget(id); } void MyMoneyFile::removeBudget(const MyMoneyBudget& budget) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->removeBudget(budget); } void MyMoneyFile::addOnlineJob(onlineJob& job) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->addOnlineJob(job); d->m_changeSet += MyMoneyNotification(File::Mode::Add, job); } void MyMoneyFile::modifyOnlineJob(const onlineJob job) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->modifyOnlineJob(job); d->m_changeSet += MyMoneyNotification(File::Mode::Modify, job); } onlineJob MyMoneyFile::getOnlineJob(const QString &jobId) const { d->checkStorage(); return d->m_storage->getOnlineJob(jobId); } QList MyMoneyFile::onlineJobList() const { d->checkStorage(); return d->m_storage->onlineJobList(); } /** @todo improve speed by passing count job to m_storage */ int MyMoneyFile::countOnlineJobs() const { return onlineJobList().count(); } /** * @brief Remove onlineJob * @param job onlineJob to remove */ void MyMoneyFile::removeOnlineJob(const onlineJob& job) { d->checkTransaction(Q_FUNC_INFO); // clear all changed objects from cache if (job.isLocked()) { return; } d->m_changeSet += MyMoneyNotification(File::Mode::Remove, job); d->m_storage->removeOnlineJob(job); } void MyMoneyFile::removeOnlineJob(const QStringList onlineJobIds) { foreach (QString jobId, onlineJobIds) { removeOnlineJob(getOnlineJob(jobId)); } } void MyMoneyFile::costCenterList(QList< MyMoneyCostCenter >& list) const { d->checkStorage(); list = d->m_storage->costCenterList(); } bool MyMoneyFile::addVATSplit(MyMoneyTransaction& transaction, const MyMoneyAccount& account, const MyMoneyAccount& category, const MyMoneyMoney& amount) { bool rc = false; try { MyMoneySplit cat; // category MyMoneySplit tax; // tax if (category.value("VatAccount").isEmpty()) return false; MyMoneyAccount vatAcc = this->account(category.value("VatAccount").toLatin1()); const MyMoneySecurity& asec = security(account.currencyId()); const MyMoneySecurity& csec = security(category.currencyId()); const MyMoneySecurity& vsec = security(vatAcc.currencyId()); if (asec.id() != csec.id() || asec.id() != vsec.id()) { qDebug("Auto VAT assignment only works if all three accounts use the same currency."); return false; } MyMoneyMoney vatRate(vatAcc.value("VatRate")); MyMoneyMoney gv, nv; // gross value, net value int fract = account.fraction(); if (!vatRate.isZero()) { tax.setAccountId(vatAcc.id()); // qDebug("vat amount is '%s'", category.value("VatAmount").toLatin1()); if (category.value("VatAmount").toLower() != QString("net")) { // split value is the gross value gv = amount; nv = (gv / (MyMoneyMoney::ONE + vatRate)).convert(fract); MyMoneySplit catSplit = transaction.splitByAccount(account.id(), false); catSplit.setShares(-nv); catSplit.setValue(catSplit.shares()); transaction.modifySplit(catSplit); } else { // split value is the net value nv = amount; gv = (nv * (MyMoneyMoney::ONE + vatRate)).convert(fract); MyMoneySplit accSplit = transaction.splitByAccount(account.id()); accSplit.setValue(gv.convert(fract)); accSplit.setShares(accSplit.value()); transaction.modifySplit(accSplit); } tax.setValue(-(gv - nv).convert(fract)); tax.setShares(tax.value()); transaction.addSplit(tax); rc = true; } } catch (const MyMoneyException &) { } return rc; } bool MyMoneyFile::isReferenced(const MyMoneyObject& obj, const QBitArray& skipChecks) const { d->checkStorage(); return d->m_storage->isReferenced(obj, skipChecks); } bool MyMoneyFile::isReferenced(const MyMoneyObject& obj) const { return isReferenced(obj, QBitArray((int)eStorage::Reference::Count)); } bool MyMoneyFile::checkNoUsed(const QString& accId, const QString& no) const { // by definition, an empty string or a non-numeric string is not used QRegExp exp(QString("(.*\\D)?(\\d+)(\\D.*)?")); if (no.isEmpty() || exp.indexIn(no) == -1) return false; MyMoneyTransactionFilter filter; filter.addAccount(accId); QList transactions = transactionList(filter); QList::ConstIterator it_t = transactions.constBegin(); while (it_t != transactions.constEnd()) { try { MyMoneySplit split; // Test whether the transaction also includes a split into // this account split = (*it_t).splitByAccount(accId, true /*match*/); if (!split.number().isEmpty() && split.number() == no) return true; } catch (const MyMoneyException &) { } ++it_t; } return false; } QString MyMoneyFile::highestCheckNo(const QString& accId) const { unsigned64 lno = 0; unsigned64 cno; QString no; MyMoneyTransactionFilter filter; filter.addAccount(accId); QList transactions = transactionList(filter); QList::ConstIterator it_t = transactions.constBegin(); while (it_t != transactions.constEnd()) { try { // Test whether the transaction also includes a split into // this account MyMoneySplit split = (*it_t).splitByAccount(accId, true /*match*/); if (!split.number().isEmpty()) { // non-numerical values stored in number will return 0 in the next line cno = split.number().toULongLong(); if (cno > lno) { lno = cno; no = split.number(); } } } catch (const MyMoneyException &) { } ++it_t; } return no; } bool MyMoneyFile::hasNewerTransaction(const QString& accId, const QDate& date) const { MyMoneyTransactionFilter filter; filter.addAccount(accId); filter.setDateFilter(date.addDays(+1), QDate()); return !transactionList(filter).isEmpty(); } void MyMoneyFile::clearCache() { d->checkStorage(); d->m_balanceCache.clear(); } void MyMoneyFile::forceDataChanged() { emit dataChanged(); } bool MyMoneyFile::isTransfer(const MyMoneyTransaction& t) const { auto rc = false; if (t.splitCount() == 2) { foreach (const auto split, t.splits()) { auto acc = account(split.accountId()); if (acc.isIncomeExpense()) { rc = true; break; } } } return rc; } bool MyMoneyFile::referencesClosedAccount(const MyMoneyTransaction& t) const { auto ret = false; foreach (const auto split, t.splits()) { if (referencesClosedAccount(split)) { ret = true; break; } } return ret; } bool MyMoneyFile::referencesClosedAccount(const MyMoneySplit& s) const { if (s.accountId().isEmpty()) return false; try { return account(s.accountId()).isClosed(); } catch (const MyMoneyException &) { } return false; } QString MyMoneyFile::storageId() { QString id = value("kmm-id"); if (id.isEmpty()) { MyMoneyFileTransaction ft; try { QUuid uid = QUuid::createUuid(); setValue("kmm-id", uid.toString()); ft.commit(); id = uid.toString(); } catch (const MyMoneyException &) { qDebug("Unable to setup UID for new storage object"); } } return id; } QString MyMoneyFile::openingBalancesPrefix() { return i18n("Opening Balances"); } bool MyMoneyFile::hasMatchingOnlineBalance(const MyMoneyAccount& _acc) const { // get current values auto acc = account(_acc.id()); // if there's no last transaction import data we are done if (acc.value("lastImportedTransactionDate").isEmpty() || acc.value("lastStatementBalance").isEmpty()) return false; // otherwise, we compare the balances MyMoneyMoney balance(acc.value("lastStatementBalance")); MyMoneyMoney accBalance = this->balance(acc.id(), QDate::fromString(acc.value("lastImportedTransactionDate"), Qt::ISODate)); return balance == accBalance; } int MyMoneyFile::countTransactionsWithSpecificReconciliationState(const QString& accId, TransactionFilter::State state) const { MyMoneyTransactionFilter filter; filter.addAccount(accId); filter.addState((int)state); return transactionList(filter).count(); } /** * Make sure that the splits value has the precision of the corresponding account */ void MyMoneyFile::fixSplitPrecision(MyMoneyTransaction& t) const { auto transactionSecurity = security(t.commodity()); auto transactionFraction = transactionSecurity.smallestAccountFraction(); for (auto& split : t.splits()) { auto acc = account(split.accountId()); auto fraction = acc.fraction(); if(fraction == -1) { auto sec = security(acc.currencyId()); fraction = acc.fraction(sec); } split.setShares(static_cast(split.shares().convertDenominator(fraction).canonicalize())); split.setValue(static_cast(split.value().convertDenominator(transactionFraction).canonicalize())); } } class MyMoneyFileTransactionPrivate { Q_DISABLE_COPY(MyMoneyFileTransactionPrivate) public: MyMoneyFileTransactionPrivate() : m_isNested(MyMoneyFile::instance()->hasTransaction()), m_needRollback(!m_isNested) { } public: bool m_isNested; bool m_needRollback; }; MyMoneyFileTransaction::MyMoneyFileTransaction() : d_ptr(new MyMoneyFileTransactionPrivate) { Q_D(MyMoneyFileTransaction); if (!d->m_isNested) MyMoneyFile::instance()->startTransaction(); } MyMoneyFileTransaction::~MyMoneyFileTransaction() { try { rollback(); } catch (const MyMoneyException & e) { qDebug() << e.what(); } Q_D(MyMoneyFileTransaction); delete d; } void MyMoneyFileTransaction::restart() { rollback(); Q_D(MyMoneyFileTransaction); d->m_needRollback = !d->m_isNested; if (!d->m_isNested) MyMoneyFile::instance()->startTransaction(); } void MyMoneyFileTransaction::commit() { Q_D(MyMoneyFileTransaction); if (!d->m_isNested) MyMoneyFile::instance()->commitTransaction(); d->m_needRollback = false; } void MyMoneyFileTransaction::rollback() { Q_D(MyMoneyFileTransaction); if (d->m_needRollback) MyMoneyFile::instance()->rollbackTransaction(); d->m_needRollback = false; } diff --git a/kmymoney/mymoney/mymoneyfile.h b/kmymoney/mymoney/mymoneyfile.h index be9cbcafd..66cd2da70 100644 --- a/kmymoney/mymoney/mymoneyfile.h +++ b/kmymoney/mymoney/mymoneyfile.h @@ -1,1702 +1,1702 @@ /*************************************************************************** mymoneyfile.h ------------------- copyright : (C) 2002, 2007 by 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. * * * ***************************************************************************/ #ifndef MYMONEYFILE_H #define MYMONEYFILE_H // ---------------------------------------------------------------------------- // QT Includes #include // ---------------------------------------------------------------------------- // Project Includes #include "kmm_mymoney_export.h" #include "mymoneyunittestable.h" /** * @author Thomas Baumgart, Michael Edwardes, Kevin Tambascio, Christian Dávid */ /** * This class represents the interface to the MyMoney engine. * For historical reasons it is still called MyMoneyFile. * It is implemented using the singleton pattern and thus only * exists once for each running instance of an application. * * The instance of the MyMoneyFile object is accessed as follows: * * @code * MyMoneyFile *file = MyMoneyFile::instance(); * file->anyMemberFunction(); * @endcode * * The first line of the above code creates a unique MyMoneyFile * object if it is called for the first time ever. All subsequent * calls to this functions return a pointer to the object created * during the first call. * * As the MyMoneyFile object represents the business logic, a storage * manager must be attached to it. This mechanism allows to use different * access methods to store the objects. The interface to access such an * storage manager is defined in the class MyMoneyStorageMgr. The methods * attachStorage() and detachStorage() are used to attach/detach a * storage manager object. The following code can be used to create a * functional MyMoneyFile instance: * * @code * MyMoneyStorageMgr *storage = .... * MyMoneyFile *file = MyMoneyFile::instance(); * file->attachStorage(storage); * @endcode * * The methods addAccount(), modifyAccount() and removeAccount() implement the * general account maintenance functions. The method reparentAccount() is * available to move an account from one superordinate account to another. * account() and accountList() are used to retrieve a single instance or a * QList of MyMoneyAccount objects. * * The methods addInstitution(), modifyInstitution() and removeInstitution() * implement the general institution maintenance functions. institution() and * institutionList() are used to retrieve a single instance or a * QList of MyMoneyInstitution objects. * * The methods addPayee(), modifyPayee() and removePayee() * implement the general payee maintenance functions. * payee() and payeeList() are used to retrieve a single instance or a * QList of MyMoneyPayee objects. * * The methods addTag(), modifyTag() and removeTag() * implement the general tag maintenance functions. * tag() and tagList() are used to retrieve a single instance or a * QList of MyMoneyTag objects. * * The methods addTransaction(), modifyTransaction() and removeTransaction() * implement the general transaction maintenance functions. * transaction() and transactionList() are used to retrieve * a single instance or a QList of MyMoneyTransaction objects. * * The methods addSecurity(), modifySecurity() and removeSecurity() * implement the general access to equities held in the engine. * * The methods addCurrency(), modifyCurrency() and removeCurrency() * implement the general access to multiple currencies held in the engine. * The methods baseCurrency() and setBaseCurrency() allow to retrieve/set * the currency selected by the user as base currency. If a currency * reference is emtpy, it will usually be interpreted as baseCurrency(). * * The methods liability(), asset(), expense(), income() and equity() are * used to retrieve the five standard accounts. isStandardAccount() * checks if a given accountId references one of the or not. * setAccountName() is used to specify a name for the standard accounts * from the GUI. * * The MyMoneyFile object emits the dataChanged() signal when data * has been changed. * * For abritrary values that have to be stored with the storage object * but are of importance to the application only, the object is derived * for MyMoneyKeyValueContainer which provides a container to store * these values indexed by an alphanumeric key. * * @exception MyMoneyException is thrown whenever an error occurs * while the engine code is running. The MyMoneyException:: object * describes the problem. */ template class QMap; class QString; class QStringList; class QBitArray; class MyMoneyStorageMgr; class MyMoneyCostCenter; class MyMoneyAccount; class MyMoneyInstitution; class MyMoneySecurity; class MyMoneyPrice; typedef QPair MyMoneySecurityPair; typedef QMap MyMoneyPriceEntries; typedef QMap MyMoneyPriceList; class MyMoneySchedule; class MyMoneyTag; class MyMoneyPayee; class MyMoneyBudget; class MyMoneyReport; class MyMoneyMoney; class MyMoneySplit; class MyMoneyObject; class MyMoneyTransaction; class MyMoneyTransactionFilter; class onlineJob; namespace eMyMoney { namespace Account { enum class Type; } namespace File { enum class Object; } namespace Schedule { enum class Type; enum class Occurrence; enum class PaymentType; } namespace TransactionFilter { enum class State; } } class KMM_MYMONEY_EXPORT MyMoneyFile : public QObject { Q_OBJECT KMM_MYMONEY_UNIT_TESTABLE public: friend class MyMoneyNotifier; /** * This is the function to access the MyMoneyFile object. * It returns a pointer to the single instance of the object. */ static MyMoneyFile* instance(); /** * This is the destructor for any MyMoneyFile object */ ~MyMoneyFile(); /** * @deprecated This is a convenience constructor. Do not use it anymore. * It will be deprecated in a future version of the engine. * * @param storage pointer to object that implements the MyMoneyStorageMgr * interface. */ explicit MyMoneyFile(MyMoneyStorageMgr *storage); // general get functions MyMoneyPayee user() const; // general set functions void setUser(const MyMoneyPayee& user); /** * This method is used to attach a storage object to the MyMoneyFile object * Without an attached storage object, the MyMoneyFile object is * of no use. * * After successful completion, the dataChanged() signal is emitted. * * In case of an error condition, an exception is thrown. * The following error conditions are checked: * * - @a storage is not equal to 0 * - there is no other @a storage object attached (use detachStorage() * to revert the attachStorage() operation. * * @param storage pointer to object that implements the MyMoneyStorageMgr * interface. * * @sa detachStorage() */ void attachStorage(MyMoneyStorageMgr* const storage); /** * This method is used to detach a previously attached storage * object from the MyMoneyFile object. If no storage object * is attached to the engine, this is a NOP. * * @param storage pointer to object that implements the MyMoneyStorageMgr * interface. * * @sa attachStorage() */ void detachStorage(MyMoneyStorageMgr* const storage = 0); /** * This method returns whether a storage is currently attached to * the engine or not. * * @return true if storage object is attached, false otherwise */ bool storageAttached() const; /** * This method returns a pointer to the storage object * * @return const pointer to the current attached storage object. * If no object is attached, returns 0. */ MyMoneyStorageMgr* storage() const; /** * This method must be called before any single change or a series of changes * in the underlying storage area is performed. * Once all changes are complete (i.e. the transaction is completed), * commitTransaction() must be called to finalize all changes. If an error occurs * during the processing of the changes call rollbackTransaction() to undo the * changes done so far. */ void startTransaction(); /** * This method returns whether a transaction has been started (@a true) * or not (@a false). */ bool hasTransaction() const; /** * @sa startTransaction() */ void commitTransaction(); /** * @sa startTransaction() */ void rollbackTransaction(); /** * This method is used to return the standard liability account * @return MyMoneyAccount liability account(group) */ MyMoneyAccount liability() const; /** * This method is used to return the standard asset account * @return MyMoneyAccount asset account(group) */ MyMoneyAccount asset() const; /** * This method is used to return the standard expense account * @return MyMoneyAccount expense account(group) */ MyMoneyAccount expense() const; /** * This method is used to return the standard income account * @return MyMoneyAccount income account(group) */ MyMoneyAccount income() const; /** * This method is used to return the standard equity account * @return MyMoneyAccount equity account(group) */ MyMoneyAccount equity() const; /** * This method returns the account information for the opening * balances account for the given @p security. If the respective * account does not exist, it will be created. The name is constructed * using MyMoneyFile::openingBalancesPrefix() and appending " (xxx)" in * case the @p security is not the baseCurrency(). The account created * will be a sub-account of the standard equity account provided by equity(). * * @param security Security for which the account is searched * * @return The opening balance account * * @note No notifications will be sent! */ MyMoneyAccount openingBalanceAccount(const MyMoneySecurity& security); /** * This method is essentially the same as the above, except it works on * const objects. If there is no opening balance account, this method * WILL NOT create one. Instead it will thrown an exception. * * @param security Security for which the account is searched * * @return The opening balance account * * @note No notifications will be sent! */ MyMoneyAccount openingBalanceAccount(const MyMoneySecurity& security) const; /** * Create an opening balance transaction for the account @p acc * with a value of @p balance. If the corresponding opening balance account * for the account's currency does not exist it will be created. If it exists * and it's opening date is later than the opening date of @p acc, * the opening date of the opening balances account will be adjusted to the * one of @p acc. * * @param acc reference to account for which the opening balance transaction * should be created * @param balance reference to the value of the opening balance transaction * * @returns The created MyMoneyTransaction object. In case no transaction has been * created, the id of the object is empty. */ MyMoneyTransaction createOpeningBalanceTransaction(const MyMoneyAccount& acc, const MyMoneyMoney& balance); /** * Retrieve the opening balance transaction for the account @p acc. * If there is no opening balance transaction, QString() will be returned. * * @param acc reference to account for which the opening balance transaction * should be retrieved * @return QString id for the transaction, or QString() if no transaction exists */ QString openingBalanceTransaction(const MyMoneyAccount& acc) const; /** * This method returns an indicator if the MyMoneyFile object has been * changed after it has last been saved to permanent storage. * * @return true if changed, false if not */ bool dirty() const; /** * This method is used to force the attached storage object to * be dirty. This is used by the application to re-set the dirty * flag after a failed upload to a server when the save operation * to a local temp file was OK. */ void setDirty() const; /** * Adds an institution to the file-global institution pool. A * respective institution-ID will be generated for this object. * The ID is stored as QString in the object passed as argument. * * An exception will be thrown upon error conditions. * @param institution The complete institution information in a * MyMoneyInstitution object */ void addInstitution(MyMoneyInstitution& institution); /** * Modifies an already existing institution in the file global * institution pool. * * An exception will be thrown upon error conditions. * * @param institution The complete new institution information */ void modifyInstitution(const MyMoneyInstitution& institution); /** * Deletes an existing institution from the file global institution pool * Also modifies the accounts that reference this institution as * their institution. * * An exception will be thrown upon error conditions. * * @param institution institution to be deleted. */ void removeInstitution(const MyMoneyInstitution& institution); void createAccount(MyMoneyAccount& newAccount, MyMoneyAccount& parentAccount, MyMoneyAccount& brokerageAccount, MyMoneyMoney openingBal); /** * Adds an account to the file-global account pool. A respective * account-ID will be generated within this record. The modified * members of @a account will be updated. * * A few parameters of the account to be added are checked against * the following conditions. If they do not match, an exception is * thrown. * * An account must match the following conditions: * * a) the account must have a name with length > 0 * b) the account must not have an id assigned * c) the transaction list must be empty * d) the account must not have any sub-ordinate accounts * e) the account must have no parent account * f) the account must not have any reference to a MyMoneyFile object * * An exception will be thrown upon error conditions. * * @param account The complete account information in a MyMoneyAccount object * @param parent The complete account information of the parent account */ void addAccount(MyMoneyAccount& account, MyMoneyAccount& parent); /** * Modifies an already existing account in the file global account pool. * * An exception will be thrown upon error conditions. * * @param account reference to the new account information */ void modifyAccount(const MyMoneyAccount& account); /** * This method re-parents an existing account * * An exception will be thrown upon error conditions. * * @param account MyMoneyAccount reference to account to be re-parented * @param parent MyMoneyAccount reference to new parent account */ void reparentAccount(MyMoneyAccount &account, MyMoneyAccount& parent); /** * moves splits from one account to another * * @param oldAccount id of the current account * @param newAccount if of the new account * * @return the number of modified splits */ unsigned int moveSplits(const QString& oldAccount, const QString& newAccount); /** * This method is used to determince, if the account with the * given ID is referenced by any split in m_transactionList. * * @param id id of the account to be checked for * @return true if account is referenced, false otherwise */ bool hasActiveSplits(const QString& id) const; /** * This method is used to check whether a given * account id references one of the standard accounts or not. * * An exception will be thrown upon error conditions. * * @param id account id * @return true if account-id is one of the standards, false otherwise */ bool isStandardAccount(const QString& id) const; /** * Returns @a true, if transaction @p t is a transfer transaction. * A transfer transaction has two splits, both referencing either * an asset, a liability or an equity account. */ bool isTransfer(const MyMoneyTransaction& t) const; /** * This method is used to set the name for the specified standard account * within the storage area. An exception will be thrown, if an error * occurs * * @param id QString reference to one of the standard accounts. * @param name QString reference to the name to be set * */ void setAccountName(const QString& id, const QString& name) const; /** * Deletes an existing account from the file global account pool * This method only allows to remove accounts that are not * referenced by any split. Use moveSplits() to move splits * to another account. An exception is thrown in case of a * problem. * * @param account reference to the account to be deleted. */ void removeAccount(const MyMoneyAccount& account); /** * Deletes existing accounts and their subaccounts recursivly * from the global account pool. * This method expects that all accounts and their subaccounts * are no longer assigned to any transactions or splits. * An exception is thrown in case of a problem deleting an account. * * The optional parameter level is used to keep track of the recursion level. * If the recursion level exceeds 100 (some arbitrary number which seems a good * maximum), an exception is thrown. * * @param account_list Reference to a list of account IDs to be deleted. * @param level Parameter to keep track of recursion level (do not pass a value here). */ void removeAccountList(const QStringList& account_list, unsigned int level = 0); /** * This member function checks all accounts identified by account_list * and their subaccounts whether they are assigned to transactions/splits or not. * The function calls itself recursively with the list of sub-accounts of * the currently processed account. * * The optional parameter level is used to keep track of the recursion level. * If the recursion level exceeds 100 (some arbitrary number which seems a good * maximum), an exception is thrown. * * @param account_list A QStringList with account IDs that need to be checked. * @param level (optional) Optional parameter to indicate recursion level. * @return Returns 'false' if at least one account has been found that * is still referenced by a transaction. */ bool hasOnlyUnusedAccounts(const QStringList& account_list, unsigned int level = 0); /** * Adds a transaction to the file-global transaction pool. A respective * transaction-ID will be generated for this object. The ID is stored * as QString in the object passed as argument. * Splits must reference valid accounts and valid payees. The payee * id can be empty. * * An exception will be thrown upon error conditions. * * @param transaction reference to the transaction */ void addTransaction(MyMoneyTransaction& transaction); /** * This method is used to update a specific transaction in the * transaction pool of the MyMoneyFile object. * Splits must reference valid accounts and valid payees. The payee * id can be empty. * * An exception will be thrown upon error conditions. * * @param transaction reference to transaction to be changed */ void modifyTransaction(const MyMoneyTransaction& transaction); /** * This method is used to extract a transaction from the file global * transaction pool through an id. In case of an invalid id, an * exception will be thrown. * * @param id id of transaction as QString. * @return reference to the requested transaction */ MyMoneyTransaction transaction(const QString& id) const; /** * This method is used to extract a transaction from the file global * transaction pool through an index into an account. * * @param account id of the account as QString * @param idx number of transaction in this account * @return reference to MyMoneyTransaction object */ MyMoneyTransaction transaction(const QString& account, const int idx) const; /** * This method is used to pull a list of transactions from the file * global transaction pool. It returns all those transactions * that match the filter passed as argument. If the filter is empty, * the whole journal will be returned. * The list returned is sorted according to the transactions posting date. * If more than one transaction exists for the same date, the order among * them is undefined. * * @param filter MyMoneyTransactionFilter object with the match criteria * * @return set of transactions in form of a QList */ QList transactionList(MyMoneyTransactionFilter& filter) const; void transactionList(QList& list, MyMoneyTransactionFilter& filter) const; void transactionList(QList >& list, MyMoneyTransactionFilter& filter) const; /** * This method is used to remove a transaction from the transaction * pool (journal). * * @param transaction const reference to transaction to be deleted */ void removeTransaction(const MyMoneyTransaction& transaction); /** * This method is used to return the actual balance of an account * without it's sub-ordinate accounts. If a @p date is presented, * the balance at the beginning of this date (not including any * transaction on this date) is returned. Otherwise all recorded * transactions are included in the balance. * * @param id id of the account in question * @param date return balance for specific date (default = QDate()) * @return balance of the account as MyMoneyMoney object */ MyMoneyMoney balance(const QString& id, const QDate& date) const; MyMoneyMoney balance(const QString& id) const; /** * This method is used to return the cleared balance of an account * without it's sub-ordinate accounts for a specific date. All * recorded transactions are included in the balance. * This method is used by the reconcialition functionality * * @param id id of the account in question * @param date return cleared balance for specific date * @return balance of the account as MyMoneyMoney object */ MyMoneyMoney clearedBalance(const QString& id, const QDate& date) const; /** * This method is used to return the actual balance of an account * including it's sub-ordinate accounts. If a @p date is presented, * the balance at the beginning of this date (not including any * transaction on this date) is returned. Otherwise all recorded * transactions are included in the balance. * * @param id id of the account in question * @param date return balance for specific date (default = QDate()) * @return balance of the account as MyMoneyMoney object */ MyMoneyMoney totalBalance(const QString& id, const QDate& date) const; MyMoneyMoney totalBalance(const QString& id) const; /** * This method returns the number of transactions currently known to file * in the range 0..MAXUINT * * @param account QString reference to account id. If account is empty + all transactions (the journal) will be counted. If account * is not empty it returns the number of transactions * that have splits in this account. * * @return number of transactions in journal/account */ unsigned int transactionCount(const QString& account) const; unsigned int transactionCount() const; /** * This method returns a QMap filled with the number of transactions * per account. The account id serves as index into the map. If one * needs to have all transactionCounts() for many accounts, this method * is faster than calling transactionCount(const QString& account) many * times. * * @return QMap with numbers of transactions per account */ QMap transactionCountMap() const; /** * This method returns the number of institutions currently known to file * in the range 0..MAXUINT * * @return number of institutions known to file */ unsigned int institutionCount() const; /** * This method returns the number of accounts currently known to file * in the range 0..MAXUINT * * @return number of accounts currently known inside a MyMoneyFile object */ unsigned int accountCount() const; /** * Returns the institution of a given ID * * @param id id of the institution to locate * @return MyMoneyInstitution object filled with data. If the institution * could not be found, an exception will be thrown */ MyMoneyInstitution institution(const QString& id) const; /** * This method returns a list of the institutions * inside a MyMoneyFile object. This is a convenience method * to the one above * * @return QList containing the institution objects */ QList institutionList() const; /** * Returns the account addressed by its id. * * @param id id of the account to locate. * @return MyMoneyAccount object carrying the @p id. An exception is thrown * if the id is unknown */ MyMoneyAccount account(const QString& id) const; /** * Returns the account addressed by its name. * * @param name name of the account to locate. * @return First MyMoneyAccount object found carrying the @p name. * An empty MyMoneyAccount object will be returned if the name is not found. */ MyMoneyAccount accountByName(const QString& name) const; /** * Returns the sub-account addressed by its name. * * @param acc account to search in * @param name name of the account to locate. * @return First MyMoneyAccount object found carrying the @p name. * An empty MyMoneyAccount object will be returned if the name is not found. */ - MyMoneyAccount subAccountByName(const MyMoneyAccount& acc, const QString& name) const; + MyMoneyAccount subAccountByName(const MyMoneyAccount& account, const QString& name) const; /** * This method returns a list of accounts inside a MyMoneyFile object. * An optional parameter is a list of id's. If this list is emtpy (the default) * the returned list contains all accounts, otherwise only those referenced * in the id-list. * * @param list reference to QList receiving the account objects * @param idlist QStringList of account ids of those accounts that * should be returned. If this list is empty, all accounts * currently known will be returned. * * @param recursive if @p true, then recurse in all found accounts. The default is @p false */ void accountList(QList& list, const QStringList& idlist = QStringList(), const bool recursive = false) const; /** * This method is used to convert an account id to a string representation * of the names which can be used as a category description. If the account * is part of a hierarchy, the category name will be the concatenation of * the single account names separated by MyMoneyFile::AccountSeparator. * * @param accountId QString reference of the account's id * @param includeStandardAccounts if true, the standard top account will be part * of the name, otherwise it will not be included (default is @c false) * * @return QString of the constructed name. */ QString accountToCategory(const QString& accountId, bool includeStandardAccounts = false) const; /** * This method is used to convert a string representing a category to * an account id. A category can be the concatenation of multiple accounts * representing a hierarchy of accounts. They have to be separated by * MyMoneyFile::AccountSeparator. * * @param category const reference to QString containing the category * @param type account type if a specific type is required (defaults to Unknown) * * @return QString of the corresponding account. If account was not found * the return value will be an empty string. */ QString categoryToAccount(const QString& category, eMyMoney::Account::Type type) const; QString categoryToAccount(const QString& category) const; /** * This method is used to convert a string representing an asset or * liability account to an account id. An account name can be the * concatenation of multiple accounts representing a hierarchy of * accounts. They have to be separated by MyMoneyFile::AccountSeparator. * * @param name const reference to QString containing the account name * * @return QString of the corresponding account. If account was not found * the return value will be an empty string. */ QString nameToAccount(const QString& name) const; /** * This method is used to extract the parent part of an account hierarchy * name who's parts are separated by MyMoneyFile::AccountSeparator. * * @param name full account name * @return parent name (full account name excluding the last part) */ QString parentName(const QString& name) const; /** * This method is used to create a new payee * * An exception will be thrown upon error conditions * * @param payee MyMoneyPayee reference to payee information */ void addPayee(MyMoneyPayee& payee); /** * This method is used to retrieve information about a payee * An exception will be thrown upon error conditions. * * @param id QString reference to id of payee * * @return MyMoneyPayee object of payee */ MyMoneyPayee payee(const QString& id) const; /** * This method is used to retrieve the id to a corresponding * name of a payee/receiver. * An exception will be thrown upon error conditions. * * @param payee QString reference to name of payee * * @return MyMoneyPayee object of payee */ MyMoneyPayee payeeByName(const QString& payee) const; /** * This method is used to modify an existing payee * * An exception will be thrown upon error conditions * * @param payee MyMoneyPayee reference to payee information */ void modifyPayee(const MyMoneyPayee& payee); /** * This method is used to remove an existing payee. * An error condition occurs, if the payee is still referenced * by a split. * * An exception will be thrown upon error conditions * * @param payee MyMoneyPayee reference to payee information */ void removePayee(const MyMoneyPayee& payee); /** * This method returns a list of the payees * inside a MyMoneyStorage object * * @return QList containing the payee information */ QList payeeList() const; /** * This method is used to create a new tag * * An exception will be thrown upon error conditions * * @param tag MyMoneyTag reference to tag information */ void addTag(MyMoneyTag& tag); /** * This method is used to retrieve information about a tag * An exception will be thrown upon error conditions. * * @param id QString reference to id of tag * * @return MyMoneyTag object of tag */ MyMoneyTag tag(const QString& id) const; /** * This method is used to retrieve the id to a corresponding * name of a tag. * An exception will be thrown upon error conditions. * * @param tag QString reference to name of tag * * @return MyMoneyTag object of tag */ MyMoneyTag tagByName(const QString& tag) const; /** * This method is used to modify an existing tag * * An exception will be thrown upon error conditions * * @param tag MyMoneyTag reference to tag information */ void modifyTag(const MyMoneyTag& tag); /** * This method is used to remove an existing tag. * An error condition occurs, if the tag is still referenced * by a split. * * An exception will be thrown upon error conditions * * @param tag MyMoneyTag reference to tag information */ void removeTag(const MyMoneyTag& tag); /** * This method returns a list of the tags * inside a MyMoneyStorage object * * @return QList containing the tag information */ QList tagList() const; /** * This method returns a list of the cost centers * inside a MyMoneyStorage object * * @return QList containing the cost center information */ void costCenterList(QList< MyMoneyCostCenter >& list) const; /** * This method is used to extract a value from the storage's * KeyValueContainer. For details see MyMoneyKeyValueContainer::value(). * @note Do not use this method to return the value of the key @p kmm-id. Use * storageId() instead. * * @param key const reference to QString containing the key * @return QString containing the value */ QString value(const QString& key) const; /** * This method is used to set a value in the storage's * KeyValueContainer. For details see MyMoneyKeyValueContainer::setValue(). * * @param key const reference to QString containing the key * @param val const reference to QString containing the value * * @note Keys starting with the leadin @p kmm- are reserved for internal use * by the MyMoneyFile object. */ void setValue(const QString& key, const QString& val); /** * This method returns the unique id of the attached storage object. * In case the storage object does not have an id yet, a new one will be * assigned. * * @return QString containing the value * * An exception is thrown if no storage object is attached. */ QString storageId(); /** * This method is used to delete a key-value-pair from the * storage's KeyValueContainer identified by the parameter * @p key. For details see MyMoneyKeyValueContainer::deletePair(). * * @param key const reference to QString containing the key */ void deletePair(const QString& key); /** * This method is used to add a scheduled transaction to the engine. * It must be sure, that the id of the object is not filled. When the * method returns to the caller, the id will be filled with the * newly created object id value. * * An exception will be thrown upon erroneous situations. * * @param sched reference to the MyMoneySchedule object */ void addSchedule(MyMoneySchedule& sched); /** * This method is used to modify an existing MyMoneySchedule * object. Therefor, the id attribute of the object must be set. * * An exception will be thrown upon erroneous situations. * * @param sched const reference to the MyMoneySchedule object to be updated */ void modifySchedule(const MyMoneySchedule& sched); /** * This method is used to remove an existing MyMoneySchedule object * from the engine. The id attribute of the object must be set. * * An exception will be thrown upon erroneous situations. * * @param sched const reference to the MyMoneySchedule object to be updated */ void removeSchedule(const MyMoneySchedule& sched); /** * This method is used to retrieve a single MyMoneySchedule object. * The id of the object must be supplied in the parameter @p id. * * An exception will be thrown upon erroneous situations. * * @param id QString containing the id of the MyMoneySchedule object * @return MyMoneySchedule object */ MyMoneySchedule schedule(const QString& id) const; /** * This method is used to extract a list of scheduled transactions * according to the filter criteria passed as arguments. * * @param accountId only search for scheduled transactions that reference * account @p accountId. If accountId is the empty string, * this filter is off. Default is @p QString(). * @param type only schedules of type @p type are searched for. * See eMyMoney::Schedule::Type for details. * Default is eMyMoney::Schedule::Type::Any * @param occurrence only schedules of occurrence type @p occurrence are searched for. * See eMyMoney::Schedule::Occurrence for details. * Default is eMyMoney::Schedule::Occurrence::Any * @param paymentType only schedules of payment method @p paymentType * are searched for. * See eMyMoney::Schedule::PaymentType for details. * Default is eMyMoney::Schedule::PaymentType::Any * @param startDate only schedules with payment dates after @p startDate * are searched for. Default is all dates (QDate()). * @param endDate only schedules with payment dates ending prior to @p endDate * are searched for. Default is all dates (QDate()). * @param overdue if true, only those schedules that are overdue are * searched for. Default is false (all schedules will be returned). * * @return const QList list of schedule objects. */ QList scheduleList(const QString& accountId, const eMyMoney::Schedule::Type type, const eMyMoney::Schedule::Occurrence occurrence, const eMyMoney::Schedule::PaymentType paymentType, const QDate& startDate, const QDate& endDate, const bool overdue) const; QList scheduleList(const QString& accountId) const; QList scheduleList() const; QStringList consistencyCheck(); /** * MyMoneyFile::openingBalancesPrefix() is a special string used * to generate the name for opening balances accounts. See openingBalanceAccount() * for details. */ static QString openingBalancesPrefix(); /** * MyMoneyFile::AccountSeparator is used as the separator * between account names to form a hierarchy. */ static const QString AccountSeparator; /** * createCategory creates a category from a text name. * * The whole account hierarchy is created if it does not * already exist. e.g if name = Bills:Credit Card and * base = expense(), Bills will first be checked to see if * it exists and created if not. Credit Card will then * be created with Bills as it's parent. The Credit Card account * will have it's id returned. * * @param base The base account (expense or income) * @param name The category to create * * @return The category account id or empty on error. * * @exception An exception will be thrown, if @p base is not equal * expense() or income(). **/ QString createCategory(const MyMoneyAccount& base, const QString& name); /** * This method is used to get the account id of the split for * a transaction from the text found in the QIF $ or L record. * If an account with the name is not found, the user is asked * if it should be created. * * @param name name of account as found in the QIF file * @param value value found in the T record * @param value2 value found in the $ record for split transactions * * @return id of the account for the split. If no name is specified * or the account was not found and not created the * return value will be "". */ QString checkCategory(const QString& name, const MyMoneyMoney& value, const MyMoneyMoney& value2); /** * This method is used to add a new security object to the engine. * The ID of the object is the trading symbol, so there is no need for an additional * ID since the symbol is guaranteed to be unique. * * An exception will be thrown upon erroneous situations. * * @param security reference to the MyMoneySecurity object */ void addSecurity(MyMoneySecurity& security); /** * This method is used to modify an existing MyMoneySchedule * object. * * An exception will be thrown upon erroneous situations. * * @param security reference to the MyMoneySecurity object to be updated */ void modifySecurity(const MyMoneySecurity& security); /** * This method is used to remove an existing MyMoneySecurity object * from the engine. * * An exception will be thrown upon erroneous situations. * * @param security reference to the MyMoneySecurity object to be removed */ void removeSecurity(const MyMoneySecurity& security); /** * This method is used to retrieve a single MyMoneySecurity object. * The id of the object must be supplied in the parameter @p id. * If no security with the given id is found, then a corresponding * currency is searched. If @p id is empty, the baseCurrency() is returned. * * An exception will be thrown upon erroneous situations. * * @param id QString containing the id of the MyMoneySecurity object * @return MyMoneySecurity object */ MyMoneySecurity security(const QString& id) const; /** * This method is used to retrieve a list of all MyMoneySecurity objects. */ QList securityList() const; /** * This method is used to add a new currency object to the engine. * The ID of the object is the trading symbol, so there is no need for an additional * ID since the symbol is guaranteed to be unique. * * An exception will be thrown upon erroneous situations. * * @param currency reference to the MyMoneySecurity object */ void addCurrency(const MyMoneySecurity& currency); /** * This method is used to modify an existing MyMoneySecurity * object. * * An exception will be thrown upon erroneous situations. * * @param currency reference to the MyMoneySecurity object */ void modifyCurrency(const MyMoneySecurity& currency); /** * This method is used to remove an existing MyMoneySecurity object * from the engine. * * An exception will be thrown upon erroneous situations. * * @param currency reference to the MyMoneySecurity object */ void removeCurrency(const MyMoneySecurity& currency); /** * This method is used to retrieve a single MyMoneySecurity object. * The id of the object must be supplied in the parameter @p id. * If @p id is empty, this method returns baseCurrency(). In case * no currency is found, @p id is searched in the loaded set of securities. * * An exception will be thrown upon erroneous situations. * * @param id QString containing the id of the MyMoneySchedule object * @return MyMoneySchedule object */ MyMoneySecurity currency(const QString& id) const; /** * This method is used to retrieve the map of ancient currencies (together with their last prices) * known to the engine. * * An exception will be thrown upon erroneous situations. * * @return QMap of all MyMoneySecurity and MyMoneyPrice objects. */ QMap ancientCurrencies() const; /** * This method is used to retrieve the list of available currencies * known to the engine. * * An exception will be thrown upon erroneous situations. * * @return QList of all MyMoneySecurity objects. */ QList availableCurrencyList() const; /** * This method is used to retrieve the list of stored currencies * known to the engine. * * An exception will be thrown upon erroneous situations. * * @return QList of all MyMoneySecurity objects. */ QList currencyList() const; /** * This method retrieves a MyMoneySecurity object representing * the selected base currency. If the base currency is not * selected (e.g. due to a previous call to setBaseCurrency()) * a standard MyMoneySecurity object will be returned. See * MyMoneySecurity() for details. * * An exception will be thrown upon erroneous situations. * * @return MyMoneySecurity describing base currency */ MyMoneySecurity baseCurrency() const; /** * This method returns the foreign currency of the given two * currency ids. If second is the base currency id then @a first * is returned otherwise @a second is returned. */ QString foreignCurrency(const QString& first, const QString& second) const; /** * This method allows to select the base currency. It does * not perform any changes to the data in the engine. It merely * stores a reference to the base currency. The currency * passed as argument must exist in the engine. * * An exception will be thrown upon erroneous situations. * * @param currency */ void setBaseCurrency(const MyMoneySecurity& currency); /** * This method adds/replaces a price to/from the price list */ void addPrice(const MyMoneyPrice& price); /** * This method removes a price from the price list */ void removePrice(const MyMoneyPrice& price); /** * This method is used to retrieve a price for a specific security * on a specific date. If there is no price for this date, the last * known price for this currency is used. If no price information * is available, 1.0 will be returned as price. * * @param fromId the id of the currency in question * @param toId the id of the currency to convert to (if emtpy, baseCurrency) * @param date the date for which the price should be returned (default = today) * @param exactDate if true, entry for date must exist, if false any price information * with a date less or equal to @p date will be returned * * @return price found as MyMoneyPrice object * @note This throws an exception when the base currency is not set and toId is empty */ MyMoneyPrice price(const QString& fromId, const QString& toId, const QDate& date, const bool exactDate = false) const; MyMoneyPrice price(const QString& fromId, const QString& toId) const; MyMoneyPrice price(const QString& fromId) const; /** * This method returns a list of all prices. * * @return MyMoneyPriceList of all MyMoneyPrice objects. */ MyMoneyPriceList priceList() const; /** * This method allows to interrogate the engine, if a known account * with id @p id has a subaccount with the name @p name. * * @param id id of the account to look at * @param name account name that needs to be searched force * @retval true account with name @p name found as subaccounts * @retval false no subaccount present with that name */ bool hasAccount(const QString& id, const QString& name) const; /** * This method is used to retrieve the list of all reports * known to the engine. * * An exception will be thrown upon erroneous situations. * * @return QList of all MyMoneyReport objects. */ QList reportList() const; /** * Adds a report to the file-global institution pool. A * respective report-ID will be generated for this object. * The ID is stored as QString in the object passed as argument. * * An exception will be thrown upon error conditions. * * @param report The complete report information in a * MyMoneyReport object */ void addReport(MyMoneyReport& report); /** * Modifies an already existing report in the file global * report pool. * * An exception will be thrown upon error conditions. * * @param report The complete new report information */ void modifyReport(const MyMoneyReport& report); /** * This method returns the number of reports currently known to file * in the range 0..MAXUINT * * @return number of reports known to file */ unsigned countReports() const; /** * This method is used to retrieve a single MyMoneyReport object. * The id of the object must be supplied in the parameter @p id. * * An exception will be thrown upon erroneous situations. * * @param id QString containing the id of the MyMoneyReport object * @return MyMoneyReport object */ MyMoneyReport report(const QString& id) const; /** * This method is used to remove an existing MyMoneyReport object * from the engine. The id attribute of the object must be set. * * An exception will be thrown upon erroneous situations. * * @param report const reference to the MyMoneyReport object to be updated */ void removeReport(const MyMoneyReport& report); /** * This method is used to retrieve the list of all budgets * known to the engine. * * An exception will be thrown upon erroneous situations. * * @return QList of all MyMoneyBudget objects. */ QList budgetList() const; /** * Adds a budget to the file-global institution pool. A * respective budget-ID will be generated for this object. * The ID is stored as QString in the object passed as argument. * * An exception will be thrown upon error conditions. * * @param budget The complete budget information in a * MyMoneyBudget object */ void addBudget(MyMoneyBudget &budget); /** * This method is used to retrieve the id to a corresponding * name of a budget. * An exception will be thrown upon error conditions. * * @param budget QString reference to name of budget * * @return MyMoneyBudget refernce to object of budget */ MyMoneyBudget budgetByName(const QString& budget) const; /** * Modifies an already existing budget in the file global * budget pool. * * An exception will be thrown upon error conditions. * * @param budget The complete new budget information */ void modifyBudget(const MyMoneyBudget& budget); /** * This method returns the number of budgets currently known to file * in the range 0..MAXUINT * * @return number of budgets known to file */ unsigned countBudgets() const; /** * This method is used to retrieve a single MyMoneyBudget object. * The id of the object must be supplied in the parameter @p id. * * An exception will be thrown upon erroneous situations. * * @param id QString containing the id of the MyMoneyBudget object * @return MyMoneyBudget object */ MyMoneyBudget budget(const QString& id) const; /** * This method is used to remove an existing MyMoneyBudget object * from the engine. The id attribute of the object must be set. * * An exception will be thrown upon erroneous situations. * * @param budget const reference to the MyMoneyBudget object to be updated */ void removeBudget(const MyMoneyBudget& budget); /** * This method is used to add a VAT split to a transaction. * * @param transaction reference to the transaction * @param account reference to the account * @param category reference to the category * @param amount reference to the amount of the VAT split * * @return true if a VAT split has been added */ bool addVATSplit(MyMoneyTransaction& transaction, const MyMoneyAccount& account, const MyMoneyAccount& category, const MyMoneyMoney& amount); /** * This method checks, if the given @p object is referenced * by another engine object. * * @param obj const reference to object to be checked * @param skipCheck QBitArray with eStorage::Reference bits set for which * the check should be skipped * * @retval false @p object is not referenced * @retval true @p institution is referenced */ bool isReferenced(const MyMoneyObject& obj, const QBitArray& skipCheck) const; bool isReferenced(const MyMoneyObject& obj) const; /** * Returns true if any of the accounts referenced by the splits * of transaction @a t is closed. */ bool referencesClosedAccount(const MyMoneyTransaction& t) const; /** * Returns true if the accounts referenced by the split @a s is closed. */ bool referencesClosedAccount(const MyMoneySplit& s) const; /** * This method checks if the given check no &p no is used in * a transaction referencing account &p accId. If @p accId is empty, * @p false is returned. * * @param accId id of account to checked * @param no check number to be verified if used or not * @retval false @p no is not in use * @retval true @p no is already assigned */ bool checkNoUsed(const QString& accId, const QString& no) const; /** * This method returns the highest assigned check no for * account @p accId. * * @param accId id of account to be scanned * @return highest check no. used */ QString highestCheckNo(const QString& accId) const; /** * This method checks if there is a transaction * after the date @p date for account @p accId. * * @param accId id of account to be scanned * @param date date to compare with * @retval false if there is no transaction after @p date * @retval true if there is a transaction after @p date */ bool hasNewerTransaction(const QString& accId, const QDate& date) const; /** * Clear all internal caches (used internally for performance measurements) */ void clearCache(); void forceDataChanged(); /** * This returns @p true if file and online balance of a specific * @p account are matching. Returns false if there is no online balance. * * @param account @p account to be checked * @retval false if @p account has balance mismatch or if there is no online balance. * @retval true if @p account has matching balances */ bool hasMatchingOnlineBalance(const MyMoneyAccount& account) const; /** * This returns the number of transactions of a specific reconciliation state @p state of account with id @p accId. * * @param accId @p accId is the account id of account to be checked * @param state @p state reconciliation state * @return number of transactions with state @p state */ int countTransactionsWithSpecificReconciliationState(const QString& accId, eMyMoney::TransactionFilter::State state) const; /** * @brief Saves a new onlineJob * @param job you stay owner of the object (a copy will be created) */ void addOnlineJob(onlineJob& job); /** * @brief Saves a onlineJob * @param job you stay owner of the object (a copy will be created) */ void modifyOnlineJob(const onlineJob job); /** * @brief Returns onlineJob identified by jobId * @param jobId * @return */ onlineJob getOnlineJob(const QString &jobId) const; /** * @brief Returns all onlineJobs * @return all online jobs, caller gains ownership */ QList onlineJobList() const; /** * @brief Returns the number of onlineJobs */ int countOnlineJobs() const; /** * @brief Remove onlineJob * * @note Removing an onlineJob fails if it is locked */ void removeOnlineJob(const onlineJob& job); /** * @brief Removes multiple onlineJobs by id * * @note Removing an onlineJob fails if it is locked */ void removeOnlineJob(const QStringList onlineJobIds); protected: /** * This is the constructor for a new empty file description */ MyMoneyFile(); Q_SIGNALS: /** * This signal is emitted when a transaction has been committed and * the notifications are about to be sent out. */ void beginChangeNotification(); /** * This signal is emitted when a transaction has been committed and * all notifications have been sent out. */ void endChangeNotification(); /** * This signal is emitted whenever any data has been changed in the engine * via any of the methods of this object */ void dataChanged(); /** * This signal is emitted by the engine whenever a new object * had been added. The data for the new object is contained in * @a obj. */ void objectAdded(eMyMoney::File::Object objType, const QString& id); /** * This signal is emitted by the engine whenever an object * had been removed. * * @note: The data contained in @a obj is only for reference * purposes and should not be used to call any MyMoneyFile * method anymore as the object is already deleted in the storage * when the signal is emitted. */ void objectRemoved(eMyMoney::File::Object objType, const QString& id); /** * This signal is emitted by the engine whenever an object * had been changed. The new state of the object is contained * in @a obj. */ void objectModified(eMyMoney::File::Object objType, const QString& id); /** * This signal is emitted by the engine whenever the balance * of an account had been changed by adding, modifying or * removing transactions from the MyMoneyFile object. */ void balanceChanged(const MyMoneyAccount& acc); /** * This signal is emitted by the engine whenever the value * of an account had been changed by adding or removing * prices from the MyMoneyFile object. */ void valueChanged(const MyMoneyAccount& acc); private: static MyMoneyFile file; MyMoneyFile& operator=(MyMoneyFile&); // not allowed for singleton MyMoneyFile(const MyMoneyFile&); // not allowed for singleton QString locateSubAccount(const MyMoneyAccount& base, const QString& category) const; void ensureDefaultCurrency(MyMoneyAccount& acc) const; void warningMissingRate(const QString& fromId, const QString& toId) const; /** * This method creates an opening balances account. The name is constructed * using MyMoneyFile::openingBalancesPrefix() and appending " (xxx)" in * case the @p security is not the baseCurrency(). The account created * will be a sub-account of the standard equity account provided by equity(). * * @param security Security for which the account is searched */ MyMoneyAccount createOpeningBalanceAccount(const MyMoneySecurity& security); MyMoneyAccount openingBalanceAccount_internal(const MyMoneySecurity& security) const; /** * Make sure that the splits value has the precision of the corresponding account */ void fixSplitPrecision(MyMoneyTransaction& t) const; private: /// \internal d-pointer class. class Private; /// \internal d-pointer instance. Private* const d; }; class MyMoneyFileTransactionPrivate; class KMM_MYMONEY_EXPORT MyMoneyFileTransaction { Q_DISABLE_COPY(MyMoneyFileTransaction) public: MyMoneyFileTransaction(); ~MyMoneyFileTransaction(); /** * Commit the current transaction. * * @warning Make sure not to use any variable that might have been altered by * the transaction. Please keep in mind, that changing transactions * can also affect account objects. If you still need those variables * just reload them from the engine. */ void commit(); void rollback(); void restart(); private: MyMoneyFileTransactionPrivate * const d_ptr; Q_DECLARE_PRIVATE(MyMoneyFileTransaction) }; #endif diff --git a/kmymoney/mymoney/mymoneyforecast.cpp b/kmymoney/mymoney/mymoneyforecast.cpp index a8e8bc1b8..c580212b8 100644 --- a/kmymoney/mymoney/mymoneyforecast.cpp +++ b/kmymoney/mymoney/mymoneyforecast.cpp @@ -1,1708 +1,1708 @@ /*************************************************************************** mymoneyforecast.cpp ------------------- begin : Wed May 30 2007 copyright : (C) 2007 by Alvaro Soliverez email : asoliverez@gmail.com (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 "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 (int 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 (int t_day = 1; t_day <= q->accountsCycle(); ++t_day) { int 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 (int 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 (int 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 acc = file->account(split.accountId()); - if (q->isForecastAccount(acc)) { + 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[acc.id()]; + balance = m_accountList[accountFromSplit.id()]; for (QDate f_day = QDate::currentDate(); f_day < forecastDate;) { - balanceMap[acc.id()] += m_accountList[acc.id()][f_day]; + 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 acc = file->account(split.accountId()); - if (q->isForecastAccount(acc)) { + auto accountFromSplit = file->account(split.accountId()); + if (q->isForecastAccount(accountFromSplit)) { dailyBalances balance; - balance = m_accountList[acc.id()]; + balance = m_accountList[accountFromSplit.id()]; //int 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 (acc.accountType() == eMyMoney::Account::Type::Income) { + if (accountFromSplit.accountType() == eMyMoney::Account::Type::Income) { balance[forecastDate] += (split.shares() * MyMoneyMoney::MINUS_ONE); } else { balance[forecastDate] += split.shares(); } - m_accountList[acc.id()] = balance; + 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 (int 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 int 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 (int 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 int trendDay, const int totalWeight) { Q_Q(MyMoneyForecast); MyMoneyMoney balanceVariation; for (int 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 int trendDay, const int actualTerms, const MyMoneyMoney& meanTerms) { Q_Q(MyMoneyForecast); MyMoneyMoney meanBalance, totalBalance, totalTerms; totalTerms = MyMoneyMoney(actualTerms, 1); //calculate mean balance for (int 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; for (int it_terms = q->forecastCycles() - actualTerms, term = 1; (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(); int auxForecastTerms; int 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 (int 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 for (int i = 1, w = q->forecastCycles(); i <= auxForecastTerms; ++i, --w) totalWeight += w; } for (int 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 (int 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. */ int calculateBeginForecastDay() { Q_Q(MyMoneyForecast); int fDays = q->forecastDays(); int beginDay = q->beginForecastDay(); int 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 */ int m_accountsCycle; /** * number of cycles to use in forecast */ int m_forecastCycles; /** * number of days to forecast */ int m_forecastDays; /** * date to start forecast */ QDate m_beginForecastDate; /** * day to start forecast */ int 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("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, int trendDays) { auto file = MyMoneyFile::instance(); MyMoneyTransactionFilter filter; MyMoneyMoney netIncome; QDate startDate; QDate openingDate = acc.openingDate(); //validate arguments if (trendDays < 1) { throw MYMONEYEXCEPTION("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, int offset) { QDate forecastDate = QDate::currentDate().addDays(offset); return forecastBalance(acc, forecastDate); } int 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; } int 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 (int 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; int daysToBeginDay; daysToBeginDay = QDate::currentDate().daysTo(beginForecastDate()); for (int 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 (int 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; int daysToBeginDay; daysToBeginDay = QDate::currentDate().daysTo(beginForecastDate()); for (int 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 (int 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 (int 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("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); 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()); } } } int MyMoneyForecast::historyDays() const { Q_D(const MyMoneyForecast); return (d->m_historyStartDate.daysTo(d->m_historyEndDate) + 1); } void MyMoneyForecast::setAccountsCycle(int accountsCycle) { Q_D(MyMoneyForecast); d->m_accountsCycle = accountsCycle; } void MyMoneyForecast::setForecastCycles(int forecastCycles) { Q_D(MyMoneyForecast); d->m_forecastCycles = forecastCycles; } void MyMoneyForecast::setForecastDays(int forecastDays) { Q_D(MyMoneyForecast); d->m_forecastDays = forecastDays; } void MyMoneyForecast::setBeginForecastDate(const QDate &beginForecastDate) { Q_D(MyMoneyForecast); d->m_beginForecastDate = beginForecastDate; } void MyMoneyForecast::setBeginForecastDay(int beginDay) { Q_D(MyMoneyForecast); d->m_beginForecastDay = beginDay; } void MyMoneyForecast::setForecastMethod(int 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(int daysToStartDate) { setHistoryStartDate(QDate::currentDate().addDays(-daysToStartDate)); } void MyMoneyForecast::setHistoryEndDate(int 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; } int MyMoneyForecast::accountsCycle() const { Q_D(const MyMoneyForecast); return d->m_accountsCycle; } int MyMoneyForecast::forecastCycles() const { Q_D(const MyMoneyForecast); return d->m_forecastCycles; } int MyMoneyForecast::forecastDays() const { Q_D(const MyMoneyForecast); return d->m_forecastDays; } QDate MyMoneyForecast::beginForecastDate() const { Q_D(const MyMoneyForecast); return d->m_beginForecastDate; } int 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/mymoneymoney.cpp b/kmymoney/mymoney/mymoneymoney.cpp index 9a1946380..90dc7c8ef 100644 --- a/kmymoney/mymoney/mymoneymoney.cpp +++ b/kmymoney/mymoney/mymoneymoney.cpp @@ -1,421 +1,421 @@ /*************************************************************************** mymoneymymoney.cpp - description ------------------- begin : Thu Feb 21 2002 copyright : (C) 2000-2002 by Michael Edwardes (C) 2011 by Carlos Eduardo da Silva (C) 2001-2017 by 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. * * * ***************************************************************************/ // make sure, that this is defined before we even include any other header file #ifndef __STDC_LIMIT_MACROS #define __STDC_LIMIT_MACROS // force definition of min and max values #endif #include "mymoneymoney.h" #include #include // ---------------------------------------------------------------------------- // QT Includes #include // ---------------------------------------------------------------------------- // Project Includes #include "mymoneyexception.h" #include "mymoneyenums.h" const MyMoneyMoney MyMoneyMoney::ONE = MyMoneyMoney(1, 1); const MyMoneyMoney MyMoneyMoney::MINUS_ONE = MyMoneyMoney(-1, 1); namespace eMyMoney { namespace Money { enum fileVersionE : int { FILE_4_BYTE_VALUE = 0, FILE_8_BYTE_VALUE }; QChar _thousandSeparator = QLatin1Char(','); QChar _decimalSeparator = QLatin1Char('.'); eMyMoney::Money::signPosition _negativeMonetarySignPosition = BeforeQuantityMoney; eMyMoney::Money::signPosition _positiveMonetarySignPosition = BeforeQuantityMoney; bool _negativePrefixCurrencySymbol = false; bool _positivePrefixCurrencySymbol = false; eMyMoney::Money::fileVersionE _fileVersion = fileVersionE::FILE_4_BYTE_VALUE; } } //eMyMoney::Money::_thousandSeparator = QLatin1Char(','); //eMyMoney::Money::_decimalSeparator = QLatin1Char('.'); //eMyMoney::Money::signPosition eMyMoney::Money::_negativeMonetarySignPosition = BeforeQuantityMoney; //eMyMoney::Money::signPosition eMyMoney::Money::_positiveMonetarySignPosition = BeforeQuantityMoney; //bool eMyMoney::Money::_negativePrefixCurrencySymbol = false; //bool eMyMoney::Money::_positivePrefixCurrencySymbol = false; //MyMoneyMoney::fileVersionE eMyMoney::Money::_fileVersion = MyMoneyMoney::FILE_4_BYTE_VALUE; MyMoneyMoney MyMoneyMoney::maxValue = MyMoneyMoney(INT64_MAX, 100); MyMoneyMoney MyMoneyMoney::minValue = MyMoneyMoney(INT64_MIN, 100); MyMoneyMoney MyMoneyMoney::autoCalc = MyMoneyMoney(INT64_MIN + 1, 100); void MyMoneyMoney::setNegativePrefixCurrencySymbol(const bool flag) { eMyMoney::Money::_negativePrefixCurrencySymbol = flag; } void MyMoneyMoney::setPositivePrefixCurrencySymbol(const bool flag) { eMyMoney::Money::_positivePrefixCurrencySymbol = flag; } void MyMoneyMoney::setNegativeMonetarySignPosition(const eMyMoney::Money::signPosition pos) { eMyMoney::Money::_negativeMonetarySignPosition = pos; } eMyMoney::Money::signPosition MyMoneyMoney::negativeMonetarySignPosition() { return eMyMoney::Money::_negativeMonetarySignPosition; } void MyMoneyMoney::setPositiveMonetarySignPosition(const eMyMoney::Money::signPosition pos) { eMyMoney::Money::_positiveMonetarySignPosition = pos; } eMyMoney::Money::signPosition MyMoneyMoney::positiveMonetarySignPosition() { return eMyMoney::Money::_positiveMonetarySignPosition; } void MyMoneyMoney::setThousandSeparator(const QChar &separator) { if (separator != QLatin1Char(' ')) eMyMoney::Money::_thousandSeparator = separator; else eMyMoney::Money::_thousandSeparator = 0; } const QChar MyMoneyMoney::thousandSeparator() { return eMyMoney::Money::_thousandSeparator; } void MyMoneyMoney::setDecimalSeparator(const QChar &separator) { if (separator != QLatin1Char(' ')) eMyMoney::Money::_decimalSeparator = separator; else eMyMoney::Money::_decimalSeparator = 0; } const QChar MyMoneyMoney::decimalSeparator() { return eMyMoney::Money::_decimalSeparator; } MyMoneyMoney::MyMoneyMoney(const QString& pszAmount) : AlkValue(pszAmount, eMyMoney::Money::_decimalSeparator) { } //////////////////////////////////////////////////////////////////////////////// // Name: MyMoneyMoney // Purpose: Constructor - constructs object from an amount in a signed64 value // Returns: None // Throws: Nothing. // Arguments: Amount - signed 64 object containing amount // denom - denominator of the object // //////////////////////////////////////////////////////////////////////////////// MyMoneyMoney::MyMoneyMoney(signed64 Amount, const signed64 denom) { if (!denom) throw MYMONEYEXCEPTION("Denominator 0 not allowed!"); *this = AlkValue(QString::fromLatin1("%1/%2").arg(Amount).arg(denom), eMyMoney::Money::_decimalSeparator); } //////////////////////////////////////////////////////////////////////////////// // Name: MyMoneyMoney // Purpose: Constructor - constructs object from an amount in a integer value // Returns: None // Throws: Nothing. // Arguments: iAmount - integer object containing amount // denom - denominator of the object // //////////////////////////////////////////////////////////////////////////////// MyMoneyMoney::MyMoneyMoney(const int iAmount, const signed64 denom) { if (!denom) throw MYMONEYEXCEPTION("Denominator 0 not allowed!"); *this = AlkValue(iAmount, denom); } //////////////////////////////////////////////////////////////////////////////// // Name: MyMoneyMoney // Purpose: Constructor - constructs object from an amount in a long integer value // Returns: None // Throws: Nothing. // Arguments: iAmount - integer object containing amount // denom - denominator of the object // //////////////////////////////////////////////////////////////////////////////// MyMoneyMoney::MyMoneyMoney(const long int iAmount, const signed64 denom) { if (!denom) throw MYMONEYEXCEPTION("Denominator 0 not allowed!"); *this = AlkValue(QString::fromLatin1("%1/%2").arg(iAmount).arg(denom), eMyMoney::Money::_decimalSeparator); } MyMoneyMoney::~MyMoneyMoney() { } MyMoneyMoney MyMoneyMoney::abs() const { return static_cast(AlkValue::abs()); } QString MyMoneyMoney::formatMoney(int denom, bool showThousandSeparator) const { return formatMoney(QString(), denomToPrec(denom), showThousandSeparator); } QString MyMoneyMoney::formatMoney(const QString& currency, const int prec, bool showThousandSeparator) const { QString res; QString tmpCurrency = currency; int tmpPrec = prec; mpz_class denom = 1; mpz_class value; // if prec == -1 we want the maximum possible but w/o trailing zeroes if (tmpPrec > -1) { while (tmpPrec--) { denom *= 10; } } else { // fix it to a max of 9 digits on the right side for now denom = 1000000000; } // as long as AlkValue::convertDenominator() does not take an // mpz_class as the denominator, we need to use a signed int // and limit the precision to 9 digits (the max we can // present with 31 bits #if 1 - signed int d; + signed int denominator; if (mpz_fits_sint_p(denom.get_mpz_t())) { - d = mpz_get_si(denom.get_mpz_t()); + denominator = mpz_get_si(denom.get_mpz_t()); } else { - d = 1000000000; + denominator = 1000000000; } - value = static_cast(convertDenominator(d)).valueRef().get_num(); + value = static_cast(convertDenominator(denominator)).valueRef().get_num(); #else value = static_cast(convertDenominator(denom)).valueRef().get_num(); #endif // Once we really support multiple currencies then this method will // be much better than using KLocale::global()->formatMoney. bool bNegative = false; - mpz_class left = value / static_cast(convertDenominator(d)).valueRef().get_den(); + mpz_class left = value / static_cast(convertDenominator(denominator)).valueRef().get_den(); mpz_class right = mpz_class((valueRef() - mpq_class(left)) * denom); if (right < 0) { right = -right; bNegative = true; } if (left < 0) { left = -left; bNegative = true; } // convert the integer (left) part to a string res.append(left.get_str().c_str()); // if requested, insert thousand separators every three digits if (showThousandSeparator) { int pos = res.length(); while ((0 < (pos -= 3)) && thousandSeparator() != 0) res.insert(pos, thousandSeparator()); } // take care of the fractional part if (prec > 0 || (prec == -1 && right != 0)) { if (decimalSeparator() != 0) res += decimalSeparator(); auto rs = QString::fromLatin1("%1").arg(right.get_str().c_str()); if (prec != -1) rs = rs.rightJustified(prec, QLatin1Char('0'), true); else { rs = rs.rightJustified(9, QLatin1Char('0'), true); // no trailing zeroes or decimal separators while (rs.endsWith(QLatin1Char('0'))) rs.truncate(rs.length() - 1); while (rs.endsWith(decimalSeparator())) rs.truncate(rs.length() - 1); } res += rs; } eMyMoney::Money::signPosition signpos = bNegative ? eMyMoney::Money::_negativeMonetarySignPosition : eMyMoney::Money::_positiveMonetarySignPosition; auto sign = bNegative ? QString::fromLatin1("-") : QString(); switch (signpos) { case eMyMoney::Money::ParensAround: res.prepend(QLatin1Char('(')); res.append(QLatin1Char(')')); break; case eMyMoney::Money::BeforeQuantityMoney: res.prepend(sign); break; case eMyMoney::Money::AfterQuantityMoney: res.append(sign); break; case eMyMoney::Money::BeforeMoney: tmpCurrency.prepend(sign); break; case eMyMoney::Money::AfterMoney: tmpCurrency.append(sign); break; } if (!tmpCurrency.isEmpty()) { if (bNegative ? eMyMoney::Money::_negativePrefixCurrencySymbol : eMyMoney::Money::_positivePrefixCurrencySymbol) { res.prepend(QLatin1Char(' ')); res.prepend(tmpCurrency); } else { res.append(QLatin1Char(' ')); res.append(tmpCurrency); } } return res; } //////////////////////////////////////////////////////////////////////////////// // Name: operator+ // Purpose: Addition operator - adds the input amount to the object // Returns: The current object // Throws: Nothing. // Arguments: b - MyMoneyMoney object to be added // //////////////////////////////////////////////////////////////////////////////// const MyMoneyMoney MyMoneyMoney::operator+(const MyMoneyMoney& _b) const { return static_cast(AlkValue::operator+(_b)); } //////////////////////////////////////////////////////////////////////////////// // Name: operator- // Purpose: Addition operator - subtracts the input amount from the object // Returns: The current object // Throws: Nothing. // Arguments: AmountInPence - MyMoneyMoney object to be subtracted // //////////////////////////////////////////////////////////////////////////////// const MyMoneyMoney MyMoneyMoney::operator-(const MyMoneyMoney& _b) const { return static_cast(AlkValue::operator-(_b)); } //////////////////////////////////////////////////////////////////////////////// // Name: operator* // Purpose: Multiplication operator - multiplies the input amount to the object // Returns: The current object // Throws: Nothing. // Arguments: b - MyMoneyMoney object to be multiplied // //////////////////////////////////////////////////////////////////////////////// const MyMoneyMoney MyMoneyMoney::operator*(const MyMoneyMoney& _b) const { return static_cast(AlkValue::operator*(_b)); } //////////////////////////////////////////////////////////////////////////////// // Name: operator/ // Purpose: Division operator - divides the object by the input amount // Returns: The current object // Throws: Nothing. // Arguments: b - MyMoneyMoney object to be used as dividend // //////////////////////////////////////////////////////////////////////////////// const MyMoneyMoney MyMoneyMoney::operator/(const MyMoneyMoney& _b) const { return static_cast(AlkValue::operator/(_b)); } bool MyMoneyMoney::isNegative() const { return (valueRef() < 0) ? true : false; } bool MyMoneyMoney::isPositive() const { return (valueRef() > 0) ? true : false; } bool MyMoneyMoney::isZero() const { return valueRef() == 0; } bool MyMoneyMoney::isAutoCalc() const { return (*this == autoCalc); } MyMoneyMoney MyMoneyMoney::convert(const signed64 _denom, const AlkValue::RoundingMethod how) const { return static_cast(convertDenominator(_denom, how)); } MyMoneyMoney MyMoneyMoney::reduce() const { MyMoneyMoney out(*this); out.canonicalize(); return out; } signed64 MyMoneyMoney::precToDenom(int prec) { signed64 denom = 1; while (prec--) denom *= 10; return denom; } double MyMoneyMoney::toDouble() const { return valueRef().get_d(); } int MyMoneyMoney::denomToPrec(signed64 fract) { int rc = 0; while (fract > 1) { rc++; fract /= 10; } return rc; } diff --git a/kmymoney/mymoney/mymoneyschedule.h b/kmymoney/mymoney/mymoneyschedule.h index f468a486d..b4c17cf92 100644 --- a/kmymoney/mymoney/mymoneyschedule.h +++ b/kmymoney/mymoney/mymoneyschedule.h @@ -1,731 +1,731 @@ /*************************************************************************** mymoneyschedule.h ------------------- copyright : (C) 2000-2002 by Michael Edwardes (C) 2007 by 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. * * * ***************************************************************************/ #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(); /** * 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; + 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; + 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/mymoneystatement.cpp b/kmymoney/mymoney/mymoneystatement.cpp index 7738808aa..01810bd0f 100644 --- a/kmymoney/mymoney/mymoneystatement.cpp +++ b/kmymoney/mymoney/mymoneystatement.cpp @@ -1,384 +1,384 @@ /*************************************************************************** mymoneystatement.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 (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 "mymoneystatement.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes namespace eMyMoney { namespace Statement { enum class Element { KMMStatement, Statement, Transaction, Split, Price, Security }; uint qHash(const Element key, uint seed) { return ::qHash(static_cast(key), seed); } enum class Attribute { Name, Symbol, ID, Version, AccountName, AccountNumber, RoutingNumber, Currency, BeginDate, EndDate, ClosingBalance, Type, AccountID, SkipCategoryMatching, DatePosted, Payee, Memo, Number, Amount, BankID, Reconcile, Action, Shares, Security, BrokerageAccount, Category, }; uint qHash(const Attribute key, uint seed) { return ::qHash(static_cast(key), seed); } } } using namespace eMyMoney; const QHash txAccountType { {Statement::Type::None, QStringLiteral("none")}, {Statement::Type::Checkings, QStringLiteral("checkings")}, {Statement::Type::Savings, QStringLiteral("savings")}, {Statement::Type::Investment, QStringLiteral("investment")}, {Statement::Type::CreditCard, QStringLiteral("creditcard")}, {Statement::Type::Invalid, QStringLiteral("invalid")} }; const QHash txAction { {Transaction::Action::None, QStringLiteral("none")}, {Transaction::Action::Buy, QStringLiteral("buy")}, {Transaction::Action::Sell, QStringLiteral("sell")}, {Transaction::Action::ReinvestDividend, QStringLiteral("reinvestdividend")}, {Transaction::Action::CashDividend, QStringLiteral("cashdividend")}, {Transaction::Action::Shrsin, QStringLiteral("add")}, {Transaction::Action::Shrsout, QStringLiteral("remove")}, {Transaction::Action::Stksplit, QStringLiteral("stocksplit")}, {Transaction::Action::Fees, QStringLiteral("fees")}, {Transaction::Action::Interest, QStringLiteral("interest")}, {Transaction::Action::Invalid, QStringLiteral("invalid")} }; QString getElName(const Statement::Element el) { static const QHash elNames { {Statement::Element::KMMStatement, QStringLiteral("KMYMONEY-STATEMENT")}, {Statement::Element::Statement, QStringLiteral("STATEMENT")}, {Statement::Element::Transaction, QStringLiteral("TRANSACTION")}, {Statement::Element::Split, QStringLiteral("SPLIT")}, {Statement::Element::Price, QStringLiteral("PRICE")}, {Statement::Element::Security, QStringLiteral("SECURITY")} }; return elNames[el]; } QString getAttrName(const Statement::Attribute attr) { static const QHash attrNames { {Statement::Attribute::Name, QStringLiteral("name")}, {Statement::Attribute::Symbol, QStringLiteral("symbol")}, {Statement::Attribute::ID, QStringLiteral("id")}, {Statement::Attribute::Version, QStringLiteral("version")}, {Statement::Attribute::AccountName, QStringLiteral("accountname")}, {Statement::Attribute::AccountNumber, QStringLiteral("accountnumber")}, {Statement::Attribute::RoutingNumber, QStringLiteral("routingnumber")}, {Statement::Attribute::Currency, QStringLiteral("currency")}, {Statement::Attribute::BeginDate, QStringLiteral("begindate")}, {Statement::Attribute::EndDate, QStringLiteral("enddate")}, {Statement::Attribute::ClosingBalance, QStringLiteral("closingbalance")}, {Statement::Attribute::Type, QStringLiteral("type")}, {Statement::Attribute::AccountID, QStringLiteral("accountid")}, {Statement::Attribute::SkipCategoryMatching, QStringLiteral("skipCategoryMatching")}, {Statement::Attribute::DatePosted, QStringLiteral("dateposted")}, {Statement::Attribute::Payee, QStringLiteral("payee")}, {Statement::Attribute::Memo, QStringLiteral("memo")}, {Statement::Attribute::Number, QStringLiteral("number")}, {Statement::Attribute::Amount, QStringLiteral("amount")}, {Statement::Attribute::BankID, QStringLiteral("bankid")}, {Statement::Attribute::Reconcile, QStringLiteral("reconcile")}, {Statement::Attribute::Action, QStringLiteral("action")}, {Statement::Attribute::Shares, QStringLiteral("shares")}, {Statement::Attribute::Security, QStringLiteral("security")}, {Statement::Attribute::BrokerageAccount, QStringLiteral("brokerageaccount")}, {Statement::Attribute::Category, QStringLiteral("version")}, }; return attrNames[attr]; } void MyMoneyStatement::write(QDomElement& _root, QDomDocument* _doc) const { QDomElement e = _doc->createElement(getElName(Statement::Element::Statement)); _root.appendChild(e); e.setAttribute(getAttrName(Statement::Attribute::Version), QStringLiteral("1.1")); e.setAttribute(getAttrName(Statement::Attribute::AccountName), m_strAccountName); e.setAttribute(getAttrName(Statement::Attribute::AccountNumber), m_strAccountNumber); e.setAttribute(getAttrName(Statement::Attribute::RoutingNumber), m_strRoutingNumber); e.setAttribute(getAttrName(Statement::Attribute::Currency), m_strCurrency); e.setAttribute(getAttrName(Statement::Attribute::BeginDate), m_dateBegin.toString(Qt::ISODate)); e.setAttribute(getAttrName(Statement::Attribute::EndDate), m_dateEnd.toString(Qt::ISODate)); e.setAttribute(getAttrName(Statement::Attribute::ClosingBalance), m_closingBalance.toString()); e.setAttribute(getAttrName(Statement::Attribute::Type), txAccountType[m_eType]); e.setAttribute(getAttrName(Statement::Attribute::AccountID), m_accountId); e.setAttribute(getAttrName(Statement::Attribute::SkipCategoryMatching), m_skipCategoryMatching); // iterate over transactions, and add each one foreach (const auto tansaction, m_listTransactions) { auto p = _doc->createElement(getElName(Statement::Element::Transaction)); p.setAttribute(getAttrName(Statement::Attribute::DatePosted), tansaction.m_datePosted.toString(Qt::ISODate)); p.setAttribute(getAttrName(Statement::Attribute::Payee), tansaction.m_strPayee); p.setAttribute(getAttrName(Statement::Attribute::Memo), tansaction.m_strMemo); p.setAttribute(getAttrName(Statement::Attribute::Number), tansaction.m_strNumber); p.setAttribute(getAttrName(Statement::Attribute::Amount), tansaction.m_amount.toString()); p.setAttribute(getAttrName(Statement::Attribute::BankID), tansaction.m_strBankID); p.setAttribute(getAttrName(Statement::Attribute::Reconcile), (int)tansaction.m_reconcile); p.setAttribute(getAttrName(Statement::Attribute::Action), txAction[tansaction.m_eAction]); if (m_eType == eMyMoney::Statement::Type::Investment) { p.setAttribute(getAttrName(Statement::Attribute::Shares), tansaction.m_shares.toString()); p.setAttribute(getAttrName(Statement::Attribute::Security), tansaction.m_strSecurity); p.setAttribute(getAttrName(Statement::Attribute::BrokerageAccount), tansaction.m_strBrokerageAccount); } // add all the splits we know of (might be empty) foreach (const auto split, tansaction.m_listSplits) { auto el = _doc->createElement(getElName(Statement::Element::Split)); el.setAttribute(getAttrName(Statement::Attribute::AccountID), split.m_accountId); el.setAttribute(getAttrName(Statement::Attribute::Amount), split.m_amount.toString()); el.setAttribute(getAttrName(Statement::Attribute::Reconcile), (int)split.m_reconcile); el.setAttribute(getAttrName(Statement::Attribute::Category), split.m_strCategoryName); el.setAttribute(getAttrName(Statement::Attribute::Memo), split.m_strMemo); el.setAttribute(getAttrName(Statement::Attribute::Reconcile), (int)split.m_reconcile); p.appendChild(el); } e.appendChild(p); } // iterate over prices, and add each one foreach (const auto price, m_listPrices) { auto p = _doc->createElement(getElName(Statement::Element::Price)); p.setAttribute(getAttrName(Statement::Attribute::DatePosted), price.m_date.toString(Qt::ISODate)); p.setAttribute(getAttrName(Statement::Attribute::Security), price.m_strSecurity); p.setAttribute(getAttrName(Statement::Attribute::Amount), price.m_amount.toString()); e.appendChild(p); } // iterate over securities, and add each one foreach (const auto security, m_listSecurities) { auto p = _doc->createElement(getElName(Statement::Element::Security)); p.setAttribute(getAttrName(Statement::Attribute::Name), security.m_strName); p.setAttribute(getAttrName(Statement::Attribute::Symbol), security.m_strSymbol); p.setAttribute(getAttrName(Statement::Attribute::ID), security.m_strId); e.appendChild(p); } } bool MyMoneyStatement::read(const QDomElement& _e) { bool result = false; if (_e.tagName() == getElName(Statement::Element::Statement)) { result = true; m_strAccountName = _e.attribute(getAttrName(Statement::Attribute::AccountName)); m_strAccountNumber = _e.attribute(getAttrName(Statement::Attribute::AccountNumber)); m_strRoutingNumber = _e.attribute(getAttrName(Statement::Attribute::RoutingNumber)); m_strCurrency = _e.attribute(getAttrName(Statement::Attribute::Currency)); m_dateBegin = QDate::fromString(_e.attribute(getAttrName(Statement::Attribute::BeginDate)), Qt::ISODate); m_dateEnd = QDate::fromString(_e.attribute(getAttrName(Statement::Attribute::EndDate)), Qt::ISODate); m_closingBalance = MyMoneyMoney(_e.attribute(getAttrName(Statement::Attribute::ClosingBalance))); m_accountId = _e.attribute(getAttrName(Statement::Attribute::AccountID)); m_skipCategoryMatching = _e.attribute(getAttrName(Statement::Attribute::SkipCategoryMatching)).isEmpty(); auto txt = _e.attribute(getAttrName(Statement::Attribute::Type), txAccountType[Statement::Type::Checkings]); m_eType = txAccountType.key(txt, m_eType); QDomNode child = _e.firstChild(); while (!child.isNull() && child.isElement()) { QDomElement c = child.toElement(); if (c.tagName() == getElName(Statement::Element::Transaction)) { MyMoneyStatement::Transaction t; t.m_datePosted = QDate::fromString(c.attribute(getAttrName(Statement::Attribute::DatePosted)), Qt::ISODate); t.m_amount = MyMoneyMoney(c.attribute(getAttrName(Statement::Attribute::Amount))); t.m_strMemo = c.attribute(getAttrName(Statement::Attribute::Memo)); t.m_strNumber = c.attribute(getAttrName(Statement::Attribute::Number)); t.m_strPayee = c.attribute(getAttrName(Statement::Attribute::Payee)); t.m_strBankID = c.attribute(getAttrName(Statement::Attribute::BankID)); t.m_reconcile = static_cast(c.attribute(getAttrName(Statement::Attribute::Reconcile)).toInt()); - auto txt = c.attribute(getAttrName(Statement::Attribute::Action), txAction[eMyMoney::Transaction::Action::Buy]); + txt = c.attribute(getAttrName(Statement::Attribute::Action), txAction[eMyMoney::Transaction::Action::Buy]); t.m_eAction = txAction.key(txt, t.m_eAction); if (m_eType == eMyMoney::Statement::Type::Investment) { t.m_shares = MyMoneyMoney(c.attribute(getAttrName(Statement::Attribute::Shares))); t.m_strSecurity = c.attribute(getAttrName(Statement::Attribute::Security)); t.m_strBrokerageAccount = c.attribute(getAttrName(Statement::Attribute::BrokerageAccount)); } // process splits (if any) - QDomNode child = c.firstChild(); + child = c.firstChild(); while (!child.isNull() && child.isElement()) { - QDomElement c = child.toElement(); + c = child.toElement(); if (c.tagName() == getElName(Statement::Element::Split)) { MyMoneyStatement::Split s; s.m_accountId = c.attribute(getAttrName(Statement::Attribute::AccountID)); s.m_amount = MyMoneyMoney(c.attribute(getAttrName(Statement::Attribute::Amount))); s.m_reconcile = static_cast(c.attribute(getAttrName(Statement::Attribute::Reconcile)).toInt()); s.m_strCategoryName = c.attribute(getAttrName(Statement::Attribute::Category)); s.m_strMemo = c.attribute(getAttrName(Statement::Attribute::Memo)); t.m_listSplits += s; } child = child.nextSibling(); } m_listTransactions += t; } else if (c.tagName() == getElName(Statement::Element::Price)) { MyMoneyStatement::Price p; p.m_date = QDate::fromString(c.attribute(getAttrName(Statement::Attribute::DatePosted)), Qt::ISODate); p.m_strSecurity = c.attribute(getAttrName(Statement::Attribute::Security)); p.m_amount = MyMoneyMoney(c.attribute(getAttrName(Statement::Attribute::Amount))); m_listPrices += p; } else if (c.tagName() == getElName(Statement::Element::Security)) { MyMoneyStatement::Security s; s.m_strName = c.attribute(getAttrName(Statement::Attribute::Name)); s.m_strSymbol = c.attribute(getAttrName(Statement::Attribute::Symbol)); s.m_strId = c.attribute(getAttrName(Statement::Attribute::ID)); m_listSecurities += s; } child = child.nextSibling(); } } return result; } bool MyMoneyStatement::isStatementFile(const QString& _filename) { // filename is considered a statement file if it contains // the tag "" in the first 20 lines. bool result = false; QFile f(_filename); if (f.open(QIODevice::ReadOnly)) { QTextStream ts(&f); auto lineCount = 20; while (!ts.atEnd() && !result && lineCount != 0) { if (ts.readLine().contains(QLatin1String(""), Qt::CaseInsensitive)) result = true; --lineCount; } f.close(); } return result; } void MyMoneyStatement::writeXMLFile(const MyMoneyStatement& _s, const QString& _filename) { static unsigned filenum = 1; auto filename = _filename; if (filename.isEmpty()) { filename = QString::fromLatin1("statement-%1%2.xml").arg((filenum < 10) ? QStringLiteral("0") : QString()).arg(filenum); filenum++; } auto doc = new QDomDocument(getElName(Statement::Element::KMMStatement)); Q_CHECK_PTR(doc); //writeStatementtoXMLDoc(_s,doc); QDomProcessingInstruction instruct = doc->createProcessingInstruction(QStringLiteral("xml"), QStringLiteral("version=\"1.0\" encoding=\"utf-8\"")); doc->appendChild(instruct); auto eroot = doc->createElement(getElName(Statement::Element::KMMStatement)); doc->appendChild(eroot); _s.write(eroot, doc); QFile g(filename); if (g.open(QIODevice::WriteOnly)) { QTextStream stream(&g); stream.setCodec("UTF-8"); stream << doc->toString(); g.close(); } delete doc; } bool MyMoneyStatement::readXMLFile(MyMoneyStatement& _s, const QString& _filename) { bool result = false; QFile f(_filename); f.open(QIODevice::ReadOnly); QDomDocument* doc = new QDomDocument; if (doc->setContent(&f, false)) { QDomElement rootElement = doc->documentElement(); if (!rootElement.isNull()) { QDomNode child = rootElement.firstChild(); while (!child.isNull() && child.isElement()) { result = true; QDomElement childElement = child.toElement(); _s.read(childElement); child = child.nextSibling(); } } } delete doc; return result; } diff --git a/kmymoney/mymoney/onlinejob.cpp b/kmymoney/mymoney/onlinejob.cpp index 1255c08f8..6e1e97652 100644 --- a/kmymoney/mymoney/onlinejob.cpp +++ b/kmymoney/mymoney/onlinejob.cpp @@ -1,323 +1,323 @@ /* This file is part of KMyMoney, A Personal Finance Manager by KDE Copyright (C) 2013 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(onlineTask* task, const QString &id) : MyMoneyObject(*new onlineJobPrivate, id), m_task(task) { 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* task) : MyMoneyObject(*new onlineJobPrivate, QString()), m_task(task) { 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; } onlineTask* onlineJob::task() { if (m_task == 0) throw emptyTask(__FILE__, __LINE__); return m_task; } const onlineTask* onlineJob::task() const { if (m_task == 0) throw emptyTask(__FILE__, __LINE__); 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 sendingState, const QDateTime &dateTime) +void onlineJob::setBankAnswer(const onlineJob::sendingState state, const QDateTime &dateTime) { Q_D(onlineJob); - d->m_jobBankAnswerState = sendingState; + d->m_jobBankAnswerState = state; d->m_jobBankAnswerDate = dateTime; } -void onlineJob::setBankAnswer(const onlineJob::sendingState sendingState) +void onlineJob::setBankAnswer(const onlineJob::sendingState state) { - setBankAnswer(sendingState, QDateTime::currentDateTime()); + 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; } /** @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 59dc67843..a7dfb026d 100644 --- a/kmymoney/mymoney/onlinejob.h +++ b/kmymoney/mymoney/onlinejob.h @@ -1,355 +1,355 @@ /* This file is part of KMyMoney, A Personal Finance Manager by KDE Copyright (C) 2013 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 #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(); /** * @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(); /** * @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 sendingState, const QDateTime &dateTime); - void setBankAnswer(const sendingState sendingState); + 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; /** * @brief Thrown if a cast of a task fails * * This is inspired by std::bad_cast */ class badTaskCast : public MyMoneyException { public: explicit badTaskCast(const QString& file = QString(), const long unsigned int& line = 0) : MyMoneyException("Casted onlineTask with wrong type", file, line) {} }; /** * @brief Thrown if a task of an invalid onlineJob is requested */ class emptyTask : public MyMoneyException { public: explicit emptyTask(const QString& file = QString(), const long unsigned int& line = 0) : MyMoneyException("Requested onlineTask of onlineJob without any task", file, line) {} }; /** @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 badTaskCast(__FILE__, __LINE__); return ret; } template const T* onlineJob::task() const { const T* ret = dynamic_cast(m_task); if (ret == 0) throw badTaskCast(__FILE__, __LINE__); return ret; } Q_DECLARE_METATYPE(onlineJob) #endif // ONLINEJOB_H diff --git a/kmymoney/mymoney/payeeidentifier/payeeidentifierdata.h b/kmymoney/mymoney/payeeidentifier/payeeidentifierdata.h index 4cbbe16de..1b5a505cd 100644 --- a/kmymoney/mymoney/payeeidentifier/payeeidentifierdata.h +++ b/kmymoney/mymoney/payeeidentifier/payeeidentifierdata.h @@ -1,138 +1,138 @@ /* * This file is part of KMyMoney, A Personal Finance Manager by KDE * Copyright (C) 2014 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 PAYEEIDENTIFIERDATA_H #define PAYEEIDENTIFIERDATA_H #include "payeeidentifier/kmm_payeeidentifier_export.h" #include #include #include #include #include #include "storage/databasestoreableobject.h" class payeeIdentifier; class payeeIdentifierLoader; /** * @brief Define a unique identifier for an payeeIdentifier subclass * * Use this macro in your class's public section. * * This also defines the helper ::ptr, ::constPtr and className::ptr cloneSharedPtr() * * @param PIDID the payeeIdentifier id, e.g. "org.kmymoney.payeeIdentifier.swift". Must be * unique among all payeeIdentifiers as it is used internaly to store data, to compare * types and for type casting (there must not be more than one class which uses that pidid). */ #define PAYEEIDENTIFIER_IID(className, iid) \ /** @brief Returns the payeeIdentifier Iid */ \ static const QString& staticPayeeIdentifierIid() { \ static const QString _pidid = QLatin1String( iid ); \ return _pidid; \ } \ /** @brief Returns the payeeIdentifier Id */ \ - virtual QString payeeIdentifierId() const { \ + QString payeeIdentifierId() const final override { \ return className::staticPayeeIdentifierIid(); \ } /** * @brief "Something" that identifies a payee (or an account of a payee) * * The simplest form of this class is an identifier for an bank account (consisting of an account number * and a bank code). But also an e-mail address which is used by an online money-transfer service could be * such an identifier (that is the reason for the abstract name "payeeIdentifier"). * * But also the creditor identifier for debit-notes in sepa-countries can be a subclass. It does not * address an account but a company. * * Any payee (@ref MyMoneyPayee) can have several payeeIdentifiers. * * The online banking system uses payeeIdentifiers to dertermine if it is able so create a credit-transfer * to a given payee. During import the payeeIdentifiers are used to find a payee. * * You should use the shared pointer payeeIdentifier::ptr to handle payeeIdentifiers. To copy them used * cloneSharedPtr(). * * @intenal First this is more complex than creating a superset of all possible identifiers. But there * are many of them. And using this method it is a lot easier to create the comparison operators and * things like isValid(). * * @section Inheriting * * To identify the type of an payeeIdentifier you must use the macro @ref PAYEEIDENTIFIER_IID() * in the public section of your subclass. */ class KMM_PAYEEIDENTIFIER_EXPORT payeeIdentifierData : public databaseStoreableObject { public: virtual ~payeeIdentifierData() {} /** * Use PAYEEIDENTIFIER_ID(className, PIDID) to reimplement this method. */ virtual QString payeeIdentifierId() const = 0; /** * @brief Comparison operator */ virtual bool operator==(const payeeIdentifierData& other) const = 0; virtual bool operator!=(const payeeIdentifierData& other) const { return (!operator==(other)); } /** * @brief Check if this payeeIdentifier contains correct data * * You should be able to handle invalid data. It is the task of the ui to prevent * invalid data. But during several procedures invalid data could be used (e.g. * during import). */ virtual bool isValid() const = 0; /** * @brief Create a new payeeIdentifier form XML data * * @param element Note: there could be more data in that elemenet than you created in writeXML() */ virtual payeeIdentifierData* createFromXml(const QDomElement &element) const = 0; virtual payeeIdentifierData* createFromSqlDatabase(QSqlDatabase db, const QString& identId) const = 0; /** * @see MyMoneyObject::writeXML() * * @warning Do not set an attribute "type" or "id" to parent, it is used to store internal data and is * set automatically. */ virtual void writeXML(QDomDocument &document, QDomElement &parent) const = 0; protected: /** * @brief Create deep copy */ virtual payeeIdentifierData* clone() const = 0; friend class payeeIdentifierLoader; friend class payeeIdentifier; }; Q_DECLARE_INTERFACE(payeeIdentifierData, "org.kmymoney.payeeIdentifier") #endif // PAYEEIDENTIFIERDATA_H diff --git a/kmymoney/mymoney/payeeidentifiermodel.cpp b/kmymoney/mymoney/payeeidentifiermodel.cpp index 907b0418b..5d0ce5814 100644 --- a/kmymoney/mymoney/payeeidentifiermodel.cpp +++ b/kmymoney/mymoney/payeeidentifiermodel.cpp @@ -1,161 +1,161 @@ /* * This file is part of KMyMoney, A Personal Finance Manager by KDE * Copyright (C) 2015, 2016 Christian David * * 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 "payeeidentifiermodel.h" #include #include #include "mymoneyfile.h" #include "mymoneyexception.h" /** * @brief create unique value for QModelIndex::internalId() to indicate "not set" */ static constexpr decltype(reinterpret_cast(0)->internalId()) invalidParent = std::numeric_limits(0)->internalId())>::max(); payeeIdentifierModel::payeeIdentifierModel(QObject* parent) : QAbstractItemModel(parent), m_payeeIdentifierIds(), m_typeFilter() { } void payeeIdentifierModel::setTypeFilter(QStringList filter) { m_typeFilter = filter; loadData(); } void payeeIdentifierModel::setTypeFilter(QString type) { setTypeFilter(QStringList(type)); } void payeeIdentifierModel::loadData() { beginResetModel(); const QList payees = MyMoneyFile::instance()->payeeList(); m_payeeIdentifierIds.clear(); m_payeeIdentifierIds.reserve(payees.count()); Q_FOREACH(const MyMoneyPayee& payee, payees) { m_payeeIdentifierIds.append(payee.id()); } endResetModel(); } MyMoneyPayee payeeIdentifierModel::payeeByIndex(const QModelIndex& index) const { if (index.isValid() && index.row() >= 0 && index.row() < m_payeeIdentifierIds.count()) { try { return MyMoneyFile::instance()->payee(m_payeeIdentifierIds.at(index.row())); } catch (MyMoneyException&) { } } return MyMoneyPayee(); } QVariant payeeIdentifierModel::data(const QModelIndex& index, int role) const { if (!index.isValid()) return QVariant(); - const bool isPayeeIdentifier = index.parent().isValid(); + const auto isPayeeIdentifierValid = index.parent().isValid(); if (role == payeeIdentifierModel::isPayeeIdentifier) - return isPayeeIdentifier; + return isPayeeIdentifierValid; - const MyMoneyPayee payee = (isPayeeIdentifier) ? payeeByIndex(index.parent()) : payeeByIndex(index); + const MyMoneyPayee payee = (isPayeeIdentifierValid) ? payeeByIndex(index.parent()) : payeeByIndex(index); - if (role == payeeName || (!isPayeeIdentifier && role == Qt::DisplayRole)) { + if (role == payeeName || (!isPayeeIdentifierValid && role == Qt::DisplayRole)) { // Return data for MyMoneyPayee return payee.name(); - } else if (isPayeeIdentifier) { + } else if (isPayeeIdentifierValid) { // Return data for payeeIdentifier if (index.row() >= 0 && static_cast(index.row()) < payee.payeeIdentifierCount()) { ::payeeIdentifier ident = payee.payeeIdentifier(index.row()); if (role == payeeIdentifier) { return QVariant::fromValue< ::payeeIdentifier >(ident); } else if (ident.isNull()) { return QVariant(); } else if (role == payeeIdentifierType) { return ident.iid(); } else if (role == Qt::DisplayRole) { // The custom delegates won't ask for this role return QVariant::fromValue(i18n("The plugin to show this information could not be found.")); } } } return QVariant(); } Qt::ItemFlags payeeIdentifierModel::flags(const QModelIndex &index) const { Q_UNUSED(index) #if 0 if (!index.parent().isValid()) { if (payeeByIndex(index).payeeIdentifierCount() > 0) return Qt::ItemIsEnabled; } #endif return (Qt::ItemIsEnabled | Qt::ItemIsSelectable); } /** * @intenal The internalId of QModelIndex is set to the row of the parent or invalidParent if there is no * parent. * * @todo Qt5: the type of the internal id changed! */ QModelIndex payeeIdentifierModel::index(int row, int column, const QModelIndex &parent) const { if (parent.isValid()) return createIndex(row, column, parent.row()); return createIndex(row, column, invalidParent); } int payeeIdentifierModel::columnCount(const QModelIndex& parent) const { Q_UNUSED(parent); return 1; } int payeeIdentifierModel::rowCount(const QModelIndex& parent) const { if (parent.isValid()) { if (parent.internalId() != invalidParent) return 0; return payeeByIndex(parent).payeeIdentifierCount(); } return m_payeeIdentifierIds.count(); } QModelIndex payeeIdentifierModel::parent(const QModelIndex& child) const { if (child.internalId() != invalidParent) return createIndex(child.internalId(), 0, invalidParent); return QModelIndex(); } diff --git a/kmymoney/mymoney/payeeidentifiermodel.h b/kmymoney/mymoney/payeeidentifiermodel.h index 70104a780..15b76beb6 100644 --- a/kmymoney/mymoney/payeeidentifiermodel.h +++ b/kmymoney/mymoney/payeeidentifiermodel.h @@ -1,76 +1,76 @@ /* * This file is part of KMyMoney, A Personal Finance Manager by KDE * Copyright (C) 2015 Christian David * * 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 PAYEEIDENTIFIERMODEL_H #define PAYEEIDENTIFIERMODEL_H #include "kmm_mymoney_export.h" #include #include #include "mymoney/mymoneypayee.h" /** * @brief Read-only model for stored payeeIdentifiers * * @note You must set an filter * * @internal if needed this can be extended to an read/write model */ class KMM_MYMONEY_EXPORT payeeIdentifierModel : public QAbstractItemModel { Q_OBJECT public: enum roles { payeeName = Qt::UserRole, /**< MyMoneyPayee::name() */ isPayeeIdentifier = Qt::UserRole + 1, /**< refers index to payeeIdentifier (true) or MyMoneyPayee (false) */ payeeIdentifierType = Qt::UserRole + 2, /**< type of payeeIdentifier */ payeeIdentifier = Qt::UserRole + 3, /**< actual payeeIdentifier */ payeeIdentifierUserRole = Qt::UserRole + 4 /**< role to start with for inheriting models */ }; explicit payeeIdentifierModel(QObject* parent = 0); - virtual QVariant data(const QModelIndex& index, int role) const; - virtual int columnCount(const QModelIndex& parent) const; - virtual int rowCount(const QModelIndex& parent) const; - virtual QModelIndex parent(const QModelIndex& child) const; - virtual QModelIndex index(int row, int column, const QModelIndex &parent) const; - virtual Qt::ItemFlags flags(const QModelIndex &index) const; + QVariant data(const QModelIndex& index, int role) const final override; + int columnCount(const QModelIndex& parent) const final override; + int rowCount(const QModelIndex& parent) const final override; + QModelIndex parent(const QModelIndex& child) const final override; + QModelIndex index(int row, int column, const QModelIndex &parent) const final override; + Qt::ItemFlags flags(const QModelIndex &index) const final override; /** * @brief Set which payeeIdentifier types to show * * @param filter list of payeeIdentifier types. An empty list leads to an empty model. */ void setTypeFilter(QStringList filter); /** convenience overload for setTypeFilter(QStringList) */ void setTypeFilter(QString type); void loadData(); private: typedef QPair identId_t; inline MyMoneyPayee payeeByIndex(const QModelIndex& index) const; QStringList m_payeeIdentifierIds; QStringList m_typeFilter; }; #endif // PAYEEIDENTIFIERMODEL_H diff --git a/kmymoney/mymoney/storage/mymoneymap.h b/kmymoney/mymoney/storage/mymoneymap.h index b4f2b331a..b80f27283 100644 --- a/kmymoney/mymoney/storage/mymoneymap.h +++ b/kmymoney/mymoney/storage/mymoneymap.h @@ -1,366 +1,366 @@ /*************************************************************************** mymoneymap.h ------------------- copyright : (C) 2007-2008 by Thomas Baumgart email : (C) 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 #include #include #include #ifndef MYMONEYMAP_H #define MYMONEYMAP_H #define MY_OWN_DEBUG 0 /** * @author Thomas Baumgart * * This template class adds transaction security to the QMap<> class. * The interface is very simple. Before you perform any changes, * you have to call the startTransaction() method. Then you can use * the insert(), modify() and remove() methods to modify the map. * Changes are recorded and if you are finished, use the * commitTransaction() to finish the transaction. If you want to go * back before you have committed the transaction, use * rollbackTransaction() to set the container to the state it was * in before you called startTransaction(). * * The implementation is based on the command pattern, in case * someone is interested. */ template class MyMoneyMap : protected QMap { private: // check if a key required (not already contained in the stack) or not bool required(const Key& key) const { if (m_stack.count() > 1) { for (auto i = 0; i < m_stack.count(); ++i) { if (m_stack[i]->key() == key) { return false; } } } return true; } public: MyMoneyMap() : QMap() {} ~MyMoneyMap() {} void startTransaction(unsigned long* id = 0) { m_stack.push(new MyMoneyMapStart(this, id)); } void rollbackTransaction(void) { if (m_stack.isEmpty()) throw MYMONEYEXCEPTION("No transaction started to rollback changes"); // undo all actions MyMoneyMapAction* action; while (!m_stack.isEmpty()) { action = m_stack.pop(); action->undo(); delete action; } } bool commitTransaction(void) { if (m_stack.isEmpty()) throw MYMONEYEXCEPTION("No transaction started to commit changes"); bool rc = m_stack.count() > 1; // remove all actions from the stack MyMoneyMapAction* action; while (!m_stack.isEmpty()) { action = m_stack.pop(); delete action; } return rc; } void insert(const Key& key, const T& obj) { if (m_stack.isEmpty()) throw MYMONEYEXCEPTION("No transaction started to insert new element into container"); // check if information about the object identified by 'key' // is already present in the stack if (!required(key)) { this->QMap::insert(key, obj); return; } // store object in m_stack.push(new MyMoneyMapInsert(this, key, obj)); } void modify(const Key& key, const T& obj) { if (m_stack.isEmpty()) throw MYMONEYEXCEPTION("No transaction started to modify element in container"); #if 0 // had to take this out, because we use QPair in one instance as key if (key.isEmpty()) throw MYMONEYEXCEPTION("No key to update object"); #endif // check if information about the object identified by 'key' // is already present in the stack if (!required(key)) { this->QMap::insert(key, obj); return; } m_stack.push(new MyMoneyMapModify(this, key, obj)); } void remove(const Key& key) { if (m_stack.isEmpty()) throw MYMONEYEXCEPTION("No transaction started to remove element from container"); #if 0 // had to take this out, because we use QPair in one instance as key if (key.isEmpty()) throw MYMONEYEXCEPTION("No key to remove object"); #endif // check if information about the object identified by 'key' // is already present in the stack if (!required(key)) { this->QMap::remove(key); return; } m_stack.push(new MyMoneyMapRemove(this, key)); } MyMoneyMap& operator= (const QMap& m) { if (!m_stack.isEmpty()) { throw MYMONEYEXCEPTION("Cannot assign whole container during transaction"); } QMap::operator=(m); return *this; } inline QList values(void) const { return QMap::values(); } inline QList keys(void) const { return QMap::keys(); } const T& operator[](const Key& k) const { return find(k).value(); #if 0 /*QT_CHECK_INVALID_MAP_ELEMENT;*/ /*PORT ME KDE4*/ return QMap::operator[](k); #endif } inline typename QMap::const_iterator find(const Key& k) const { return QMap::find(k); } inline typename QMap::const_iterator begin(void) const { return QMap::constBegin(); } inline typename QMap::const_iterator end(void) const { return QMap::constEnd(); } typedef typename QMap::const_iterator const_iterator; inline bool contains(const Key& k) const { return find(k) != end(); } inline void map(QMap& that) const { //QMap* ptr = dynamic_cast* >(this); //that = *ptr; that = *(static_cast* >(const_cast* >(this))); } inline int count(void) const { return QMap::count(); } #if MY_OWN_DEBUG void dump(void) const { printf("Container dump\n"); printf(" items in container = %d\n", count()); printf(" items on stack = %d\n", m_stack.count()); const_iterator it; for (it = begin(); it != end(); ++it) { printf(" %s \n", it.key().data()); } } #endif private: class MyMoneyMapAction { public: MyMoneyMapAction(MyMoneyMap* container) : m_container(container) {} MyMoneyMapAction(MyMoneyMap* container, const Key& key, const T& obj) : m_container(container), m_obj(obj), m_key(key) {} virtual ~MyMoneyMapAction() {} virtual void undo(void) = 0; const Key& key(void) const { return m_key; } protected: MyMoneyMap* m_container; T m_obj; Key m_key; }; class MyMoneyMapStart : public MyMoneyMapAction { public: MyMoneyMapStart(MyMoneyMap* container, unsigned long* id) : MyMoneyMapAction(container), m_idPtr(id), m_id(0) { if (id != 0) m_id = *id; } ~MyMoneyMapStart() final {} - void undo(void) final { + void undo(void) final override { if (m_idPtr != 0) *m_idPtr = m_id; } private: unsigned long* m_idPtr; unsigned long m_id; }; class MyMoneyMapInsert : public MyMoneyMapAction { public: MyMoneyMapInsert(MyMoneyMap* container, const Key& key, const T& obj) : MyMoneyMapAction(container, key, obj) { container->QMap::insert(key, obj); } ~MyMoneyMapInsert() final {} - void undo(void) final { + void undo(void) final override { // m_container->remove(m_key) does not work on GCC 4.0.2 // using this-> to access those member does the trick this->m_container->QMap::remove(this->m_key); } }; class MyMoneyMapRemove : public MyMoneyMapAction { public: MyMoneyMapRemove(MyMoneyMap* container, const Key& key) : MyMoneyMapAction(container, key, (*container)[key]) { container->QMap::remove(key); } ~MyMoneyMapRemove() final {} - void undo(void) final { + void undo(void) final override { this->m_container->insert(this->m_key, this->m_obj); } }; class MyMoneyMapModify : public MyMoneyMapAction { public: MyMoneyMapModify(MyMoneyMap* container, const Key& key, const T& obj) : MyMoneyMapAction(container, key, (*container)[key]) { container->QMap::insert(key, obj); } ~MyMoneyMapModify() final {} - void undo(void) final { + void undo(void) final override { this->m_container->QMap::insert(this->m_key, this->m_obj); } }; protected: QStack m_stack; }; #if MY_OWN_DEBUG #include #include main() { MyMoneyMap container; MyMoneyMap ct; MyMoneyAccount acc; acc.setName("Test"); // this should not be possible // container["a"] = acc; QList list; list = container.values(); MyMoneyAccount b; b.setName("Thomas"); try { container.startTransaction(); container.insert("001", acc); container.dump(); container.commitTransaction(); acc.setName("123"); container.startTransaction(); container.modify("001", acc); container.dump(); container.rollbackTransaction(); container.dump(); container.startTransaction(); container.remove(QString("001")); container.dump(); container.rollbackTransaction(); container.dump(); b = container["001"]; printf("b.name() = %s\n", b.name().data()); QMap::ConstIterator it; it = container.find("001"); it = container.begin(); } catch (const MyMoneyException &e) { printf("Caught exception: %s\n", e.what().data()); } QMap map; map["005"] = b; container = map; printf("b.name() = %s\n", container["001"].name().data()); printf("b.name() = %s\n", container["005"].name().data()); } #endif #endif diff --git a/kmymoney/mymoney/storage/mymoneystorageanon.h b/kmymoney/mymoney/storage/mymoneystorageanon.h index cfab8ca7c..64c6e164d 100644 --- a/kmymoney/mymoney/storage/mymoneystorageanon.h +++ b/kmymoney/mymoney/storage/mymoneystorageanon.h @@ -1,120 +1,120 @@ /*************************************************************************** mymoneystorageanon.h ------------------- begin : Thu Oct 24 2002 copyright : (C) 2000-2002 by Michael Edwardes email : mte@users.sourceforge.net Javier Campos Morales Felix Rodriguez John C Thomas Baumgart Kevin Tambascio Ace Jone ***************************************************************************/ /*************************************************************************** * * * 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 MYMONEYSTORAGEANON_H #define MYMONEYSTORAGEANON_H #include "kmm_mymoney_export.h" // ---------------------------------------------------------------------------- // QT Includes // ---------------------------------------------------------------------------- // Project Includes #include "mymoneystoragexml.h" #include "mymoneymoney.h" class MyMoneyKeyValueContainer; /** * @author Kevin Tambascio (ktambascio@users.sourceforge.net) */ #define VERSION_0_60_XML 0x10000010 // Version 0.5 file version info #define VERSION_0_61_XML 0x10000011 // use 8 bytes for MyMoneyMoney objects /** * This class provides storage of an anonymized version of the current * file. Any object with an ID (account, transaction, etc) is renamed * with that ID. Any other string value the user typed in is replaced with * x's equal in length to the original string. Any numeric value is * replaced with an arbitrary number which matches the sign of the original. * * The purpose of this class is to give users a way to send a developer * their file without comprimising their financial data. If a user * encounters an error, they should try saving the anonymous version of the * file and see if the error is still there. If so, they should notify the * list of the problem, and then when requested, send the anonymous file * privately to the developer who takes the problem. I still don't think * it's wise to post the file to the public list...maybe I'm just paranoid. * * @author Ace Jones */ class KMM_MYMONEY_EXPORT MyMoneyStorageANON : public MyMoneyStorageXML { public: MyMoneyStorageANON(); virtual ~MyMoneyStorageANON(); protected: - void writeUserInformation(QDomElement& userInfo); + void writeUserInformation(QDomElement& userInfo) final override; - void writeInstitution(QDomElement& institutions, const MyMoneyInstitution& i); + void writeInstitution(QDomElement& institutions, const MyMoneyInstitution& i) final override; - void writePayee(QDomElement& payees, const MyMoneyPayee& p); + void writePayee(QDomElement& payees, const MyMoneyPayee& p) final override; - void writeTag(QDomElement& tags, const MyMoneyTag& ta); + void writeTag(QDomElement& tags, const MyMoneyTag& ta) final override; - void writeAccount(QDomElement& accounts, const MyMoneyAccount& p); + void writeAccount(QDomElement& accounts, const MyMoneyAccount& p) final override; - void writeTransaction(QDomElement& transactions, const MyMoneyTransaction& tx); + void writeTransaction(QDomElement& transactions, const MyMoneyTransaction& tx) final override; - void writeSchedule(QDomElement& scheduledTx, const MyMoneySchedule& tx); + void writeSchedule(QDomElement& scheduledTx, const MyMoneySchedule& tx) final override; - void writeBudget(QDomElement& budgets, const MyMoneyBudget& b); + void writeBudget(QDomElement& budgets, const MyMoneyBudget& b) final override; - void writeReport(QDomElement& reports, const MyMoneyReport& r); + void writeReport(QDomElement& reports, const MyMoneyReport& r) final override; - void readFile(QIODevice* s, MyMoneyStorageMgr* storage); + void readFile(QIODevice* s, MyMoneyStorageMgr* storage) final override; - void writeSecurity(QDomElement& securityElement, const MyMoneySecurity& security); + void writeSecurity(QDomElement& securityElement, const MyMoneySecurity& security) final override; /** Cannot remove prive data from plugins, yet. It is simply doing nothing. */ - void writeOnlineJob(QDomElement& onlineJobs, const onlineJob& job); + void writeOnlineJob(QDomElement& onlineJobs, const onlineJob& job) final override; QDomElement findChildElement(const QString& name, const QDomElement& root); private: /** * The list of key-value pairs to not modify */ static QStringList zKvpNoModify; /** * The list of key-value pairs which are numbers to be hidden */ static QStringList zKvpXNumber; QString hideString(const QString&) const; MyMoneyMoney hideNumber(const MyMoneyMoney&) const; void fakeTransaction(MyMoneyTransaction& tn); void fakeBudget(MyMoneyBudget& bn); void fakeKeyValuePair(MyMoneyKeyValueContainer& _kvp); MyMoneyMoney m_factor; }; #endif diff --git a/kmymoney/mymoney/storage/mymoneystoragexml.cpp b/kmymoney/mymoney/storage/mymoneystoragexml.cpp index c05a134ab..55099e515 100644 --- a/kmymoney/mymoney/storage/mymoneystoragexml.cpp +++ b/kmymoney/mymoney/storage/mymoneystoragexml.cpp @@ -1,1066 +1,1066 @@ /*************************************************************************** mymoneystoragexml.cpp - description ------------------- begin : Thu Oct 24 2002 copyright : (C) 2002 by Kevin Tambascio (C) 2004 by 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 "mymoneystoragexml.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include // ---------------------------------------------------------------------------- // Project Includes #include "mymoneystoragemgr.h" #include "mymoneyexception.h" #include "mymoneykeyvaluecontainer.h" #include "mymoneyaccount.h" #include "mymoneysecurity.h" #include "mymoneymoney.h" #include "mymoneyschedule.h" #include "mymoneypayee.h" #include "mymoneytag.h" #include "mymoneyreport.h" #include "mymoneybudget.h" #include "mymoneyinstitution.h" #include "mymoneystoragenames.h" #include "mymoneyutils.h" #include "mymoneyprice.h" #include "mymoneycostcenter.h" #include "mymoneytransaction.h" #include "onlinejob.h" #include "mymoneyenums.h" using namespace MyMoneyStorageTags; using namespace MyMoneyStorageNodes; using namespace MyMoneyStorageAttributes; unsigned int MyMoneyStorageXML::fileVersionRead = 0; unsigned int MyMoneyStorageXML::fileVersionWrite = 0; class MyMoneyStorageXML::Private { friend class MyMoneyStorageXML; public: Private() : m_nextTransactionID(0) {} QMap iList; QMap aList; QMap tList; QMap pList; QMap taList; QMap sList; QMap secList; QMap rList; QMap bList; QMap onlineJobList; QMap prList; QMap ccList; QString m_fromSecurity; QString m_toSecurity; unsigned long m_nextTransactionID; static const int TRANSACTION_ID_SIZE = 18; QString nextTransactionID() { QString id; id.setNum(++m_nextTransactionID); id = 'T' + id.rightJustified(TRANSACTION_ID_SIZE, '0'); return id; } }; class MyMoneyXmlContentHandler : public QXmlContentHandler { public: MyMoneyXmlContentHandler(MyMoneyStorageXML* reader); virtual ~MyMoneyXmlContentHandler() {} - virtual void setDocumentLocator(QXmlLocator * locator) { + virtual void setDocumentLocator(QXmlLocator * locator) final override { m_loc = locator; } - virtual bool startDocument(); - virtual bool endDocument(); - virtual bool startPrefixMapping(const QString & prefix, const QString & uri); - virtual bool endPrefixMapping(const QString & prefix); - virtual bool startElement(const QString & namespaceURI, const QString & localName, const QString & qName, const QXmlAttributes & atts); - virtual bool endElement(const QString & namespaceURI, const QString & localName, const QString & qName); - virtual bool characters(const QString & ch); - virtual bool ignorableWhitespace(const QString & ch); - virtual bool processingInstruction(const QString & target, const QString & data); - virtual bool skippedEntity(const QString & name); - virtual QString errorString() const ; + virtual bool startDocument() final override; + virtual bool endDocument() final override; + virtual bool startPrefixMapping(const QString & prefix, const QString & uri) final override; + virtual bool endPrefixMapping(const QString & prefix) final override; + virtual bool startElement(const QString & namespaceURI, const QString & localName, const QString & qName, const QXmlAttributes & atts) final override; + virtual bool endElement(const QString & namespaceURI, const QString & localName, const QString & qName) final override; + virtual bool characters(const QString & ch) final override; + virtual bool ignorableWhitespace(const QString & ch) final override; + virtual bool processingInstruction(const QString & target, const QString & data) final override; + virtual bool skippedEntity(const QString & name) final override; + virtual QString errorString() const final override; private: MyMoneyStorageXML* m_reader; QXmlLocator* m_loc; int m_level; int m_elementCount; QDomDocument m_doc; /** * @node Text in the xml file is not added to this QDomElement. Only tags and their attributes are added. */ QDomElement m_baseNode; /** * @node Text in the xml file is not added to this QDomElement. Only tags and their attributes are added. */ QDomElement m_currNode; QString m_errMsg; }; MyMoneyXmlContentHandler::MyMoneyXmlContentHandler(MyMoneyStorageXML* reader) : m_reader(reader), m_loc(0), m_level(0), m_elementCount(0) { } bool MyMoneyXmlContentHandler::startDocument() { qDebug("startDocument"); return true; } bool MyMoneyXmlContentHandler::endDocument() { qDebug("endDocument"); return true; } bool MyMoneyXmlContentHandler::skippedEntity(const QString & /* name */) { // qDebug(QString("Skipped entity '%1'").arg(name)); return true; } bool MyMoneyXmlContentHandler::startPrefixMapping(const QString& /*prefix */, const QString & /* uri */) { // qDebug(QString("start prefix '%1', '%2'").arg(prefix).arg(uri)); return true; } bool MyMoneyXmlContentHandler::endPrefixMapping(const QString& /* prefix */) { // qDebug(QString("end prefix '%1'").arg(prefix)); return true; } bool MyMoneyXmlContentHandler::startElement(const QString& /* namespaceURI */, const QString& /* localName */, const QString& qName, const QXmlAttributes & atts) { if (m_level == 0) { QString s = qName.toUpper(); if (s == nodeNames[nnTransaction] || s == nodeNames[nnAccount] || s == nodeNames[nnPrice] || s == nodeNames[nnPayee] || s == nodeNames[nnTag] || s == nodeNames[nnCostCenter] || s == nodeNames[nnCurrency] || s == nodeNames[nnSecurity] || s == nodeNames[nnKeyValuePairs] || s == nodeNames[nnInstitution] || s == nodeNames[nnReport] || s == nodeNames[nnBudget] || s == tagNames[tnFileInfo] || s == tagNames[tnUser] || s == nodeNames[nnScheduleTX] || s == nodeNames[nnOnlineJob]) { m_baseNode = m_doc.createElement(qName); for (int i = 0; i < atts.count(); ++i) { m_baseNode.setAttribute(atts.qName(i), atts.value(i)); } m_currNode = m_baseNode; m_level = 1; } else if (s == tagNames[tnTransactions]) { if (atts.count()) { int count = atts.value(attrNames[anCount]).toInt(); m_reader->signalProgress(0, count, i18n("Loading transactions...")); m_elementCount = 0; } } else if (s == tagNames[tnAccounts]) { if (atts.count()) { int count = atts.value(attrNames[anCount]).toInt(); m_reader->signalProgress(0, count, i18n("Loading accounts...")); m_elementCount = 0; } } else if (s == tagNames[tnSecurities]) { qDebug("reading securities"); if (atts.count()) { int count = atts.value(attrNames[anCount]).toInt(); m_reader->signalProgress(0, count, i18n("Loading securities...")); m_elementCount = 0; } } else if (s == tagNames[tnCurrencies]) { if (atts.count()) { int count = atts.value(attrNames[anCount]).toInt(); m_reader->signalProgress(0, count, i18n("Loading currencies...")); m_elementCount = 0; } } else if (s == tagNames[tnReports]) { if (atts.count()) { int count = atts.value(attrNames[anCount]).toInt(); m_reader->signalProgress(0, count, i18n("Loading reports...")); m_elementCount = 0; } } else if (s == tagNames[tnPrices]) { if (atts.count()) { int count = atts.value(attrNames[anCount]).toInt(); m_reader->signalProgress(0, count, i18n("Loading prices...")); m_elementCount = 0; } } else if (s == nodeNames[nnPricePair]) { if (atts.count()) { m_reader->d->m_fromSecurity = atts.value(attrNames[anFrom]); m_reader->d->m_toSecurity = atts.value(attrNames[anTo]); } } else if (s == tagNames[tnCostCenters]) { if(atts.count()) { int count = atts.value(attrNames[anCount]).toInt(); m_reader->signalProgress(0, count, i18n("Loading cost center...")); m_elementCount = 0; } } } else { m_level++; QDomElement node = m_doc.createElement(qName); for (int i = 0; i < atts.count(); ++i) { node.setAttribute(atts.qName(i), atts.value(i)); } m_currNode.appendChild(node); m_currNode = node; } return true; } bool MyMoneyXmlContentHandler::endElement(const QString& /* namespaceURI */, const QString& /* localName */, const QString& qName) { bool rc = true; QString s = qName.toUpper(); if (m_level) { m_currNode = m_currNode.parentNode().toElement(); m_level--; if (!m_level) { try { if (s == nodeNames[nnTransaction]) { MyMoneyTransaction t0(m_baseNode); if (!t0.id().isEmpty()) { MyMoneyTransaction t1(m_reader->d->nextTransactionID(), t0); m_reader->d->tList[t1.uniqueSortKey()] = t1; } m_reader->signalProgress(++m_elementCount, 0); } else if (s == nodeNames[nnAccount]) { MyMoneyAccount a(m_baseNode); if (!a.id().isEmpty()) m_reader->d->aList[a.id()] = a; m_reader->signalProgress(++m_elementCount, 0); } else if (s == nodeNames[nnPayee]) { MyMoneyPayee p(m_baseNode); if (!p.id().isEmpty()) m_reader->d->pList[p.id()] = p; m_reader->signalProgress(++m_elementCount, 0); } else if (s == nodeNames[nnTag]) { MyMoneyTag ta(m_baseNode); if (!ta.id().isEmpty()) m_reader->d->taList[ta.id()] = ta; m_reader->signalProgress(++m_elementCount, 0); } else if (s == nodeNames[nnCurrency]) { - MyMoneySecurity s(m_baseNode); - if (!s.id().isEmpty()) - m_reader->d->secList[s.id()] = s; + MyMoneySecurity sec(m_baseNode); + if (!sec.id().isEmpty()) + m_reader->d->secList[sec.id()] = sec; m_reader->signalProgress(++m_elementCount, 0); } else if (s == nodeNames[nnSecurity]) { - MyMoneySecurity s(m_baseNode); - if (!s.id().isEmpty()) - m_reader->d->secList[s.id()] = s; + MyMoneySecurity sec(m_baseNode); + if (!sec.id().isEmpty()) + m_reader->d->secList[sec.id()] = sec; m_reader->signalProgress(++m_elementCount, 0); } else if (s == nodeNames[nnKeyValuePairs]) { MyMoneyKeyValueContainer kvp(m_baseNode); m_reader->m_storage->setPairs(kvp.pairs()); } else if (s == nodeNames[nnInstitution]) { MyMoneyInstitution i(m_baseNode); if (!i.id().isEmpty()) m_reader->d->iList[i.id()] = i; } else if (s == nodeNames[nnReport]) { MyMoneyReport r(m_baseNode); if (!r.id().isEmpty()) m_reader->d->rList[r.id()] = r; m_reader->signalProgress(++m_elementCount, 0); } else if (s == nodeNames[nnBudget]) { MyMoneyBudget b(m_baseNode); if (!b.id().isEmpty()) m_reader->d->bList[b.id()] = b; } else if (s == tagNames[tnFileInfo]) { rc = m_reader->readFileInformation(m_baseNode); m_reader->signalProgress(-1, -1); } else if (s == tagNames[tnUser]) { rc = m_reader->readUserInformation(m_baseNode); m_reader->signalProgress(-1, -1); } else if (s == nodeNames[nnScheduleTX]) { - MyMoneySchedule s(m_baseNode); - if (!s.id().isEmpty()) - m_reader->d->sList[s.id()] = s; + MyMoneySchedule sch(m_baseNode); + if (!sch.id().isEmpty()) + m_reader->d->sList[sch.id()] = sch; } else if (s == nodeNames[nnPrice]) { MyMoneyPrice p(m_reader->d->m_fromSecurity, m_reader->d->m_toSecurity, m_baseNode); m_reader->d->prList[MyMoneySecurityPair(m_reader->d->m_fromSecurity, m_reader->d->m_toSecurity)][p.date()] = p; m_reader->signalProgress(++m_elementCount, 0); } else if (s == nodeNames[nnOnlineJob]) { onlineJob job(m_baseNode); if (!job.id().isEmpty()) m_reader->d->onlineJobList[job.id()] = job; } else if (s == nodeNames[nnCostCenter]) { MyMoneyCostCenter c(m_baseNode); if(!c.id().isEmpty()) { m_reader->d->ccList[c.id()] = c; } m_reader->signalProgress(++m_elementCount, 0); } else { m_errMsg = i18n("Unknown XML tag %1 found in line %2", qName, m_loc->lineNumber()); qWarning() << m_errMsg; rc = false; } } catch (const MyMoneyException &e) { m_errMsg = i18n("Exception while creating a %1 element: %2", s, e.what()); qWarning() << m_errMsg; rc = false; } m_doc = QDomDocument(); } } else { if (s == tagNames[tnInstitutions]) { // last institution read, now dump them into the engine m_reader->m_storage->loadInstitutions(m_reader->d->iList); m_reader->d->iList.clear(); } else if (s == tagNames[tnAccounts]) { // last account read, now dump them into the engine m_reader->m_storage->loadAccounts(m_reader->d->aList); m_reader->d->aList.clear(); m_reader->signalProgress(-1, -1); } else if (s == tagNames[tnPayees]) { // last payee read, now dump them into the engine m_reader->m_storage->loadPayees(m_reader->d->pList); m_reader->d->pList.clear(); } else if (s == tagNames[tnTags]) { // last tag read, now dump them into the engine m_reader->m_storage->loadTags(m_reader->d->taList); m_reader->d->taList.clear(); } else if (s == tagNames[tnTransactions]) { // last transaction read, now dump them into the engine m_reader->m_storage->loadTransactions(m_reader->d->tList); m_reader->d->tList.clear(); m_reader->signalProgress(-1, -1); } else if (s == tagNames[tnSchedules]) { // last schedule read, now dump them into the engine m_reader->m_storage->loadSchedules(m_reader->d->sList); m_reader->d->sList.clear(); } else if (s == tagNames[tnSecurities]) { // last security read, now dump them into the engine m_reader->m_storage->loadSecurities(m_reader->d->secList); m_reader->d->secList.clear(); m_reader->signalProgress(-1, -1); } else if (s == tagNames[tnCurrencies]) { // last currency read, now dump them into the engine m_reader->m_storage->loadCurrencies(m_reader->d->secList); m_reader->d->secList.clear(); m_reader->signalProgress(-1, -1); } else if (s == tagNames[tnReports]) { // last report read, now dump them into the engine m_reader->m_storage->loadReports(m_reader->d->rList); m_reader->d->rList.clear(); m_reader->signalProgress(-1, -1); } else if (s == tagNames[tnBudgets]) { // last budget read, now dump them into the engine m_reader->m_storage->loadBudgets(m_reader->d->bList); m_reader->d->bList.clear(); } else if (s == tagNames[tnPrices]) { // last price read, now dump them into the engine m_reader->m_storage->loadPrices(m_reader->d->prList); m_reader->d->bList.clear(); m_reader->signalProgress(-1, -1); } else if (s == tagNames[tnOnlineJobs]) { m_reader->m_storage->loadOnlineJobs(m_reader->d->onlineJobList); m_reader->d->onlineJobList.clear(); } else if (s == tagNames[tnCostCenters]) { m_reader->m_storage->loadCostCenters(m_reader->d->ccList); m_reader->d->ccList.clear(); m_reader->signalProgress(-1, -1); } } return rc; } bool MyMoneyXmlContentHandler::characters(const QString& /* ch */) { return true; } bool MyMoneyXmlContentHandler::ignorableWhitespace(const QString& /* ch */) { return true; } bool MyMoneyXmlContentHandler::processingInstruction(const QString& /* target */, const QString& /* data */) { return true; } QString MyMoneyXmlContentHandler::errorString() const { return m_errMsg; } MyMoneyStorageXML::MyMoneyStorageXML() : m_progressCallback(0), m_storage(0), m_doc(0), d(new Private()) { } MyMoneyStorageXML::~MyMoneyStorageXML() { delete d; } // Function to read in the file, send to XML parser. void MyMoneyStorageXML::readFile(QIODevice* pDevice, MyMoneyStorageMgr* storage) { Q_CHECK_PTR(storage); Q_CHECK_PTR(pDevice); if (!storage) return; m_storage = storage; m_doc = new QDomDocument; Q_CHECK_PTR(m_doc); qDebug("reading file"); // creating the QXmlInputSource object based on a QIODevice object // reads all data of the underlying object into memory. I have not // found an object that reads on the fly. I tried to derive one myself, // but there could be a severe problem with decoding when reading // blocks of data and not a stream. So I left it the way it is. (ipwizard) QXmlInputSource xml(pDevice); qDebug("start parsing file"); MyMoneyXmlContentHandler mmxml(this); QXmlSimpleReader reader; reader.setContentHandler(&mmxml); if (!reader.parse(&xml, false)) { delete m_doc; m_doc = 0; signalProgress(-1, -1); throw MYMONEYEXCEPTION("File was not parsable!"); } // check if we need to build up the account balances if (fileVersionRead < 2) m_storage->rebuildAccountBalances(); delete m_doc; m_doc = 0; // this seems to be nonsense, but it clears the dirty flag // as a side-effect. m_storage->setLastModificationDate(m_storage->lastModificationDate()); m_storage = 0; //hides the progress bar. signalProgress(-1, -1); } void MyMoneyStorageXML::writeFile(QIODevice* qf, MyMoneyStorageMgr* storage) { Q_CHECK_PTR(qf); Q_CHECK_PTR(storage); if (!storage) { return; } m_storage = storage; // qDebug("XMLWRITER: Starting file write"); m_doc = new QDomDocument(tagNames[tnKMMFile]); Q_CHECK_PTR(m_doc); QDomProcessingInstruction instruct = m_doc->createProcessingInstruction("xml", "version=\"1.0\" encoding=\"utf-8\""); m_doc->appendChild(instruct); QDomElement mainElement = m_doc->createElement(tagNames[tnKMMFile]); m_doc->appendChild(mainElement); QDomElement fileInfo = m_doc->createElement(tagNames[tnFileInfo]); writeFileInformation(fileInfo); mainElement.appendChild(fileInfo); QDomElement userInfo = m_doc->createElement(tagNames[tnUser]); writeUserInformation(userInfo); mainElement.appendChild(userInfo); QDomElement institutions = m_doc->createElement(tagNames[tnInstitutions]); writeInstitutions(institutions); mainElement.appendChild(institutions); QDomElement payees = m_doc->createElement(tagNames[tnPayees]); writePayees(payees); mainElement.appendChild(payees); QDomElement costCenters = m_doc->createElement(tagNames[tnCostCenters]); writeCostCenters(costCenters); mainElement.appendChild(costCenters); QDomElement tags = m_doc->createElement(tagNames[tnTags]); writeTags(tags); mainElement.appendChild(tags); QDomElement accounts = m_doc->createElement(tagNames[tnAccounts]); writeAccounts(accounts); mainElement.appendChild(accounts); QDomElement transactions = m_doc->createElement(tagNames[tnTransactions]); writeTransactions(transactions); mainElement.appendChild(transactions); QDomElement keyvalpairs = writeKeyValuePairs(m_storage->pairs()); mainElement.appendChild(keyvalpairs); QDomElement schedules = m_doc->createElement(tagNames[tnSchedules]); writeSchedules(schedules); mainElement.appendChild(schedules); QDomElement equities = m_doc->createElement(tagNames[tnSecurities]); writeSecurities(equities); mainElement.appendChild(equities); QDomElement currencies = m_doc->createElement(tagNames[tnCurrencies]); writeCurrencies(currencies); mainElement.appendChild(currencies); QDomElement prices = m_doc->createElement(tagNames[tnPrices]); writePrices(prices); mainElement.appendChild(prices); QDomElement reports = m_doc->createElement(tagNames[tnReports]); writeReports(reports); mainElement.appendChild(reports); QDomElement budgets = m_doc->createElement(tagNames[tnBudgets]); writeBudgets(budgets); mainElement.appendChild(budgets); QDomElement onlineJobs = m_doc->createElement(tagNames[tnOnlineJobs]); writeOnlineJobs(onlineJobs); mainElement.appendChild(onlineJobs); QTextStream stream(qf); stream.setCodec("UTF-8"); stream << m_doc->toString(); delete m_doc; m_doc = 0; //hides the progress bar. signalProgress(-1, -1); // this seems to be nonsense, but it clears the dirty flag // as a side-effect. m_storage->setLastModificationDate(m_storage->lastModificationDate()); m_storage = 0; } bool MyMoneyStorageXML::readFileInformation(const QDomElement& fileInfo) { signalProgress(0, 3, i18n("Loading file information...")); bool rc = true; QDomElement temp = findChildElement(getElName(enCreationDate), fileInfo); if (temp == QDomElement()) { rc = false; } QString strDate = MyMoneyUtils::QStringEmpty(temp.attribute(attrNames[anDate])); m_storage->setCreationDate(MyMoneyUtils::stringToDate(strDate)); signalProgress(1, 0); temp = findChildElement(getElName(enLastModifiedDate), fileInfo); if (temp == QDomElement()) { rc = false; } strDate = MyMoneyUtils::QStringEmpty(temp.attribute(attrNames[anDate])); m_storage->setLastModificationDate(MyMoneyUtils::stringToDate(strDate)); signalProgress(2, 0); temp = findChildElement(getElName(enVersion), fileInfo); if (temp == QDomElement()) { rc = false; } QString strVersion = MyMoneyUtils::QStringEmpty(temp.attribute(attrNames[anID])); fileVersionRead = strVersion.toUInt(0, 16); temp = findChildElement(getElName(enFixVersion), fileInfo); if (temp != QDomElement()) { QString strFixVersion = MyMoneyUtils::QStringEmpty(temp.attribute(attrNames[anID])); m_storage->setFileFixVersion(strFixVersion.toUInt()); // skip KMyMoneyView::fixFile_2() if (m_storage->fileFixVersion() == 2) { m_storage->setFileFixVersion(3); } } // FIXME The old version stuff used this rather odd number // We now use increments if (fileVersionRead == VERSION_0_60_XML) fileVersionRead = 1; signalProgress(3, 0); return rc; } void MyMoneyStorageXML::writeFileInformation(QDomElement& fileInfo) { QDomElement creationDate = m_doc->createElement(getElName(enCreationDate)); creationDate.setAttribute(attrNames[anDate], MyMoneyUtils::dateToString(m_storage->creationDate())); fileInfo.appendChild(creationDate); QDomElement lastModifiedDate = m_doc->createElement(getElName(enLastModifiedDate)); lastModifiedDate.setAttribute(attrNames[anDate], MyMoneyUtils::dateToString(m_storage->lastModificationDate())); fileInfo.appendChild(lastModifiedDate); QDomElement version = m_doc->createElement(getElName(enVersion)); version.setAttribute(attrNames[anID], "1"); fileInfo.appendChild(version); QDomElement fixVersion = m_doc->createElement(getElName(enFixVersion)); fixVersion.setAttribute(attrNames[anID], m_storage->fileFixVersion()); fileInfo.appendChild(fixVersion); } void MyMoneyStorageXML::writeUserInformation(QDomElement& userInfo) { MyMoneyPayee user = m_storage->user(); userInfo.setAttribute(attrNames[anName], user.name()); userInfo.setAttribute(attrNames[anEmail], user.email()); QDomElement address = m_doc->createElement(getElName(enAddress)); address.setAttribute(attrNames[anStreet], user.address()); address.setAttribute(attrNames[anCity], user.city()); address.setAttribute(attrNames[anCountry], user.state()); address.setAttribute(attrNames[anZipCode], user.postcode()); address.setAttribute(attrNames[anTelephone], user.telephone()); userInfo.appendChild(address); } bool MyMoneyStorageXML::readUserInformation(const QDomElement& userElement) { bool rc = true; signalProgress(0, 1, i18n("Loading user information...")); MyMoneyPayee user; user.setName(MyMoneyUtils::QStringEmpty(userElement.attribute(attrNames[anName]))); user.setEmail(MyMoneyUtils::QStringEmpty(userElement.attribute(attrNames[anEmail]))); QDomElement addressNode = findChildElement(getElName(enAddress), userElement); if (!addressNode.isNull()) { user.setAddress(MyMoneyUtils::QStringEmpty(addressNode.attribute(attrNames[anStreet]))); user.setCity(MyMoneyUtils::QStringEmpty(addressNode.attribute(attrNames[anCity]))); user.setState(MyMoneyUtils::QStringEmpty(addressNode.attribute(attrNames[anCountry]))); user.setPostcode(MyMoneyUtils::QStringEmpty(addressNode.attribute(attrNames[anZipCode]))); user.setTelephone(MyMoneyUtils::QStringEmpty(addressNode.attribute(attrNames[anTelephone]))); } m_storage->setUser(user); signalProgress(1, 0); return rc; } void MyMoneyStorageXML::writeInstitutions(QDomElement& institutions) { const QList list = m_storage->institutionList(); QList::ConstIterator it; institutions.setAttribute(attrNames[anCount], list.count()); for (it = list.begin(); it != list.end(); ++it) writeInstitution(institutions, *it); } void MyMoneyStorageXML::writeInstitution(QDomElement& institution, const MyMoneyInstitution& i) { i.writeXML(*m_doc, institution); } void MyMoneyStorageXML::writePayees(QDomElement& payees) { const QList list = m_storage->payeeList(); QList::ConstIterator it; payees.setAttribute(attrNames[anCount], list.count()); for (it = list.begin(); it != list.end(); ++it) writePayee(payees, *it); } void MyMoneyStorageXML::writePayee(QDomElement& payee, const MyMoneyPayee& p) { p.writeXML(*m_doc, payee); } void MyMoneyStorageXML::writeTags(QDomElement& tags) { const QList list = m_storage->tagList(); QList::ConstIterator it; tags.setAttribute(attrNames[anCount], list.count()); for (it = list.begin(); it != list.end(); ++it) writeTag(tags, *it); } void MyMoneyStorageXML::writeTag(QDomElement& tag, const MyMoneyTag& ta) { ta.writeXML(*m_doc, tag); } void MyMoneyStorageXML::writeAccounts(QDomElement& accounts) { QList list; m_storage->accountList(list); QList::ConstIterator it; accounts.setAttribute(attrNames[anCount], list.count() + 5); writeAccount(accounts, m_storage->asset()); writeAccount(accounts, m_storage->liability()); writeAccount(accounts, m_storage->expense()); writeAccount(accounts, m_storage->income()); writeAccount(accounts, m_storage->equity()); signalProgress(0, list.count(), i18n("Saving accounts...")); int i = 0; for (it = list.constBegin(); it != list.constEnd(); ++it) { writeAccount(accounts, *it); signalProgress(++i, 0); } } void MyMoneyStorageXML::writeAccount(QDomElement& account, const MyMoneyAccount& p) { p.writeXML(*m_doc, account); } void MyMoneyStorageXML::writeTransactions(QDomElement& transactions) { MyMoneyTransactionFilter filter; filter.setReportAllSplits(false); const auto list = m_storage->transactionList(filter); transactions.setAttribute(attrNames[anCount], list.count()); QList::ConstIterator it; signalProgress(0, list.count(), i18n("Saving transactions...")); int i = 0; for (it = list.constBegin(); it != list.constEnd(); ++it) { writeTransaction(transactions, *it); signalProgress(++i, 0); } } void MyMoneyStorageXML::writeTransaction(QDomElement& transaction, const MyMoneyTransaction& tx) { tx.writeXML(*m_doc, transaction); } void MyMoneyStorageXML::writeSchedules(QDomElement& scheduled) { const QList list = m_storage->scheduleList(QString(), eMyMoney::Schedule::Type::Any, eMyMoney::Schedule::Occurrence::Any, eMyMoney::Schedule::PaymentType::Any, QDate(), QDate(), false); QList::ConstIterator it; scheduled.setAttribute(attrNames[anCount], list.count()); for (it = list.constBegin(); it != list.constEnd(); ++it) { this->writeSchedule(scheduled, *it); } } void MyMoneyStorageXML::writeSchedule(QDomElement& scheduledTx, const MyMoneySchedule& tx) { tx.writeXML(*m_doc, scheduledTx); } void MyMoneyStorageXML::writeSecurities(QDomElement& equities) { const QList securityList = m_storage->securityList(); equities.setAttribute(attrNames[anCount], securityList.count()); if (securityList.size()) { for (QList::ConstIterator it = securityList.constBegin(); it != securityList.constEnd(); ++it) { writeSecurity(equities, (*it)); } } } void MyMoneyStorageXML::writeSecurity(QDomElement& securityElement, const MyMoneySecurity& security) { security.writeXML(*m_doc, securityElement); } void MyMoneyStorageXML::writeCurrencies(QDomElement& currencies) { const QList currencyList = m_storage->currencyList(); currencies.setAttribute(attrNames[anCount], currencyList.count()); if (currencyList.size()) { for (QList::ConstIterator it = currencyList.constBegin(); it != currencyList.constEnd(); ++it) { writeSecurity(currencies, (*it)); } } } void MyMoneyStorageXML::writeReports(QDomElement& parent) { const QList list = m_storage->reportList(); QList::ConstIterator it; parent.setAttribute(attrNames[anCount], list.count()); signalProgress(0, list.count(), i18n("Saving reports...")); unsigned i = 0; for (it = list.constBegin(); it != list.constEnd(); ++it) { writeReport(parent, (*it)); signalProgress(++i, 0); } } void MyMoneyStorageXML::writeReport(QDomElement& report, const MyMoneyReport& r) { r.writeXML(*m_doc, report); } void MyMoneyStorageXML::writeBudgets(QDomElement& parent) { const QList list = m_storage->budgetList(); QList::ConstIterator it; parent.setAttribute(attrNames[anCount], list.count()); signalProgress(0, list.count(), i18n("Saving budgets...")); unsigned i = 0; for (it = list.constBegin(); it != list.constEnd(); ++it) { writeBudget(parent, (*it)); signalProgress(++i, 0); } } void MyMoneyStorageXML::writeBudget(QDomElement& budget, const MyMoneyBudget& b) { b.writeXML(*m_doc, budget); } void MyMoneyStorageXML::writeOnlineJobs(QDomElement& parent) { const QList list = m_storage->onlineJobList(); parent.setAttribute(attrNames[anCount], list.count()); signalProgress(0, list.count(), i18n("Saving online banking orders...")); unsigned i = 0; QList::ConstIterator end = list.constEnd(); for (QList::ConstIterator it = list.constBegin(); it != end; ++it) { writeOnlineJob(parent, *it); signalProgress(++i, 0); } } void MyMoneyStorageXML::writeOnlineJob(QDomElement& onlineJobs, const onlineJob& job) { job.writeXML(*m_doc, onlineJobs); } void MyMoneyStorageXML::writeCostCenters(QDomElement& parent) { const QList list = m_storage->costCenterList(); parent.setAttribute(attrNames[anCount], list.count()); signalProgress(0, list.count(), i18n("Saving costcenters...")); unsigned i = 0; Q_FOREACH(MyMoneyCostCenter costCenter, list) { writeCostCenter(parent, costCenter); signalProgress(++i, 0); } } void MyMoneyStorageXML::writeCostCenter(QDomElement& costCenters, const MyMoneyCostCenter& costCenter) { costCenter.writeXML(*m_doc, costCenters); } QDomElement MyMoneyStorageXML::findChildElement(const QString& name, const QDomElement& root) { QDomNode child = root.firstChild(); while (!child.isNull()) { if (child.isElement()) { QDomElement childElement = child.toElement(); if (name == childElement.tagName()) { return childElement; } } child = child.nextSibling(); } return QDomElement(); } QDomElement MyMoneyStorageXML::writeKeyValuePairs(const QMap pairs) { if (m_doc) { QDomElement keyValPairs = m_doc->createElement(nodeNames[nnKeyValuePairs]); QMap::const_iterator it; for (it = pairs.constBegin(); it != pairs.constEnd(); ++it) { QDomElement pair = m_doc->createElement(getElName(enPair)); pair.setAttribute(attrNames[anKey], it.key()); pair.setAttribute(attrNames[anValue], it.value()); keyValPairs.appendChild(pair); } return keyValPairs; } return QDomElement(); } void MyMoneyStorageXML::writePrices(QDomElement& prices) { const MyMoneyPriceList list = m_storage->priceList(); MyMoneyPriceList::ConstIterator it; prices.setAttribute(attrNames[anCount], list.count()); for (it = list.constBegin(); it != list.constEnd(); ++it) { QDomElement price = m_doc->createElement(nodeNames[nnPricePair]); price.setAttribute(attrNames[anFrom], it.key().first); price.setAttribute(attrNames[anTo], it.key().second); writePricePair(price, *it); prices.appendChild(price); } } void MyMoneyStorageXML::writePricePair(QDomElement& price, const MyMoneyPriceEntries& p) { MyMoneyPriceEntries::ConstIterator it; for (it = p.constBegin(); it != p.constEnd(); ++it) { QDomElement entry = m_doc->createElement(nodeNames[nnPrice]); writePrice(entry, *it); price.appendChild(entry); } } void MyMoneyStorageXML::writePrice(QDomElement& price, const MyMoneyPrice& p) { price.setAttribute(attrNames[anDate], p.date().toString(Qt::ISODate)); price.setAttribute(attrNames[anPrice], p.rate(QString()).toString()); price.setAttribute(attrNames[anSource], p.source()); } void MyMoneyStorageXML::setProgressCallback(void(*callback)(int, int, const QString&)) { m_progressCallback = callback; } void MyMoneyStorageXML::signalProgress(int current, int total, const QString& msg) { if (m_progressCallback != 0) (*m_progressCallback)(current, total, msg); } #if 0 /*! This convenience function returns all of the remaining data in the device. @note It's copied from the original Qt sources and modified to fix a problem with KFilterDev that does not correctly return atEnd() status in certain circumstances which caused our application to lock at startup. */ QByteArray QIODevice::readAll() { if (isDirectAccess()) { // we know the size int n = size() - at(); // ### fix for 64-bit or large files? int totalRead = 0; QByteArray ba(n); char* c = ba.data(); while (n) { int r = read(c, n); if (r < 0) return QByteArray(); n -= r; c += r; totalRead += r; // If we have a translated file, then it is possible that // we read less bytes than size() reports if (atEnd()) { ba.resize(totalRead); break; } } return ba; } else { // read until we reach the end const int blocksize = 512; int nread = 0; QByteArray ba; int r = 1; while (!atEnd() && r != 0) { ba.resize(nread + blocksize); r = read(ba.data() + nread, blocksize); if (r < 0) return QByteArray(); nread += r; } ba.resize(nread); return ba; } } #endif const QString MyMoneyStorageXML::getElName(const elNameE _el) { static const QHash elNames = { {enAddress, QStringLiteral("ADDRESS")}, {enCreationDate, QStringLiteral("CREATION_DATE")}, {enLastModifiedDate, QStringLiteral("LAST_MODIFIED_DATE")}, {enVersion, QStringLiteral("VERSION")}, {enFixVersion, QStringLiteral("FIXVERSION")}, {enPair, QStringLiteral("PAIR")} }; return elNames[_el]; } diff --git a/kmymoney/mymoney/storage/mymoneystoragexml.h b/kmymoney/mymoney/storage/mymoneystoragexml.h index d198f9787..179b1de52 100644 --- a/kmymoney/mymoney/storage/mymoneystoragexml.h +++ b/kmymoney/mymoney/storage/mymoneystoragexml.h @@ -1,197 +1,197 @@ /*************************************************************************** mymoneystoragexml.h ------------------- begin : Thu Oct 24 2002 copyright : (C) 2002 by Kevin Tambascio (C) 2004 by 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. * * * ***************************************************************************/ #ifndef MYMONEYSTORAGEXML_H #define MYMONEYSTORAGEXML_H #include "kmm_mymoney_export.h" // ---------------------------------------------------------------------------- // QT Includes #include #include // ---------------------------------------------------------------------------- // Project Includes #include "imymoneystorageformat.h" /** *@author Kevin Tambascio (ktambascio@users.sourceforge.net) */ #define VERSION_0_60_XML 0x10000010 // Version 0.5 file version info #define VERSION_0_61_XML 0x10000011 // use 8 bytes for MyMoneyMoney objects class QString; class QIODevice; class QDomElement; class QDomDocument; class QDate; class MyMoneyStorageMgr; class MyMoneyInstitution; class MyMoneyAccount; class MyMoneySecurity; class MyMoneySchedule; class MyMoneyPayee; class MyMoneyTag; class MyMoneyBudget; class MyMoneyReport; class MyMoneyPrice; class MyMoneyTransaction; class MyMoneyCostCenter; class onlineJob; template class QList; typedef QPair MyMoneySecurityPair; typedef QMap MyMoneyPriceEntries; typedef QMap MyMoneyPriceList; class KMM_MYMONEY_EXPORT MyMoneyStorageXML : public IMyMoneyOperationsFormat { friend class MyMoneyXmlContentHandler; public: MyMoneyStorageXML(); virtual ~MyMoneyStorageXML(); enum fileVersionDirectionType { Reading = 0, /**< version of file to be read */ Writing = 1 /**< version to be used when writing a file */ }; protected: - void setProgressCallback(void(*callback)(int, int, const QString&)); + void setProgressCallback(void(*callback)(int, int, const QString&)) override; void signalProgress(int current, int total, const QString& = ""); /** * This method returns the version of the underlying file. It * is used by the MyMoney objects contained in a MyMoneyStorageBin object (e.g. * MyMoneyAccount, MyMoneyInstitution, MyMoneyTransaction, etc.) to * determine the layout used when reading/writing a persistant file. * A parameter is used to determine the direction. * * @param dir information about the direction (reading/writing). The * default is reading. * * @return version QString of file's version * * @see m_fileVersionRead, m_fileVersionWrite */ static unsigned int fileVersion(fileVersionDirectionType dir = Reading); QList readElements(QString groupTag, QString itemTag = QString()); bool readFileInformation(const QDomElement& fileInfo); virtual void writeFileInformation(QDomElement& fileInfo); virtual void writeUserInformation(QDomElement& userInfo); virtual void writeInstitution(QDomElement& institutions, const MyMoneyInstitution& i); virtual void writeInstitutions(QDomElement& institutions); virtual void writePrices(QDomElement& prices); virtual void writePricePair(QDomElement& price, const MyMoneyPriceEntries& p); virtual void writePrice(QDomElement& prices, const MyMoneyPrice& p); virtual void writePayees(QDomElement& payees); virtual void writePayee(QDomElement& payees, const MyMoneyPayee& p); virtual void writeTags(QDomElement& tags); virtual void writeTag(QDomElement& tags, const MyMoneyTag& ta); virtual void writeAccounts(QDomElement& accounts); virtual void writeAccount(QDomElement& accounts, const MyMoneyAccount& p); virtual void writeTransactions(QDomElement& transactions); virtual void writeTransaction(QDomElement& transactions, const MyMoneyTransaction& tx); virtual void writeSchedules(QDomElement& scheduled); virtual void writeSchedule(QDomElement& scheduledTx, const MyMoneySchedule& tx); virtual void writeReports(QDomElement& e); virtual void writeReport(QDomElement& report, const MyMoneyReport& r); virtual void writeBudgets(QDomElement& e); virtual void writeBudget(QDomElement& budget, const MyMoneyBudget& b); virtual void writeOnlineJobs(QDomElement& onlineJobs); virtual void writeOnlineJob(QDomElement& onlineJobs, const onlineJob& job); virtual void writeSecurities(QDomElement& securities); virtual void writeSecurity(QDomElement& securityElement, const MyMoneySecurity& security); virtual void writeCostCenters(QDomElement& parent); virtual void writeCostCenter(QDomElement& costCenters, const MyMoneyCostCenter& costCenter); virtual void writeCurrencies(QDomElement& currencies); virtual QDomElement writeKeyValuePairs(const QMap pairs); - virtual void readFile(QIODevice* s, MyMoneyStorageMgr* storage); - virtual void writeFile(QIODevice* s, MyMoneyStorageMgr* storage); + virtual void readFile(QIODevice* s, MyMoneyStorageMgr* storage) override; + virtual void writeFile(QIODevice* s, MyMoneyStorageMgr* storage) override; bool readUserInformation(const QDomElement& userElement); void readPricePair(const QDomElement& pricePair); const MyMoneyPrice readPrice(const QString& from, const QString& to, const QDomElement& price); QDomElement findChildElement(const QString& name, const QDomElement& root); private: void (*m_progressCallback)(int, int, const QString&); enum elNameE { enAddress, enCreationDate, enLastModifiedDate, enVersion, enFixVersion, enPair }; static const QString getElName(const elNameE _el); protected: MyMoneyStorageMgr *m_storage; QDomDocument *m_doc; private: /// \internal d-pointer class. class Private; /// \internal d-pointer instance. Private* const d; /** * This member is used to store the file version information * obtained while reading a file. */ static unsigned int fileVersionRead; /** * This member is used to store the file version information * to be used when writing a file. */ static unsigned int fileVersionWrite; /** * This member keeps the id of the base currency. We need this * temporarily to convert the price history from the old to the * new format. This should go at some time beyond 0.8 (ipwizard) */ QString m_baseCurrencyId; }; #endif diff --git a/kmymoney/mymoney/tests/mymoneyobject-test.cpp b/kmymoney/mymoney/tests/mymoneyobject-test.cpp index e1e93aca1..43be27a75 100644 --- a/kmymoney/mymoney/tests/mymoneyobject-test.cpp +++ b/kmymoney/mymoney/tests/mymoneyobject-test.cpp @@ -1,185 +1,185 @@ /*************************************************************************** mymoneyobjecttest.cpp ------------------- copyright : (C) 2005 by Thomas Baumgart email : ipwizard@users.sourceforge.net ***************************************************************************/ /*************************************************************************** * * * 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 "mymoneyobject-test.h" #include #include #include #include "mymoneyobject_p.h" #include "mymoneyexception.h" #include "mymoneyaccount.h" class TestMyMoneyObjectPrivate : public MyMoneyObjectPrivate { public: TestMyMoneyObjectPrivate() { } }; class TestMyMoneyObject : public MyMoneyObject { Q_DECLARE_PRIVATE(TestMyMoneyObject) public: TestMyMoneyObject() : MyMoneyObject(*new MyMoneyObjectPrivate) {} TestMyMoneyObject & operator=(TestMyMoneyObject other); friend void swap(TestMyMoneyObject& first, TestMyMoneyObject& second); ~TestMyMoneyObject(){} TestMyMoneyObject(const QDomElement& node, const bool forceId = true) : MyMoneyObject(*new MyMoneyObjectPrivate, node, forceId) {} - virtual bool hasReferenceTo(const QString&) const { + bool hasReferenceTo(const QString&) const final override { return false; } - virtual void writeXML(QDomDocument&, QDomElement&) const {} + void writeXML(QDomDocument&, QDomElement&) const final override {} }; void swap(TestMyMoneyObject& first, TestMyMoneyObject& second) { using std::swap; swap(first.d_ptr, second.d_ptr); } TestMyMoneyObject & TestMyMoneyObject::operator=(TestMyMoneyObject other) { swap(*this, other); return *this; } QTEST_GUILESS_MAIN(MyMoneyObjectTest) void MyMoneyObjectTest::testEmptyConstructor() { MyMoneyAccount a; QVERIFY(a.id().isEmpty()); } void MyMoneyObjectTest::testConstructor() { MyMoneyAccount a(QString("thb"), MyMoneyAccount()); QVERIFY(!a.id().isEmpty()); QVERIFY(a.id() == QString("thb")); } void MyMoneyObjectTest::testClearId() { MyMoneyAccount a(QString("thb"), MyMoneyAccount()); QVERIFY(!a.id().isEmpty()); a.clearId(); QVERIFY(a.id().isEmpty()); } void MyMoneyObjectTest::testCopyConstructor() { MyMoneyAccount a(QString("thb"), MyMoneyAccount()); MyMoneyAccount b(a); QVERIFY(a.MyMoneyObject::operator==(b)); } void MyMoneyObjectTest::testAssignmentConstructor() { MyMoneyAccount a(QString("thb"), MyMoneyAccount()); MyMoneyAccount b = a; QVERIFY(a.MyMoneyObject::operator==(b)); } void MyMoneyObjectTest::testEquality() { MyMoneyAccount a(QString("thb"), MyMoneyAccount()); MyMoneyAccount b(QString("thb"), MyMoneyAccount()); MyMoneyAccount c(QString("ace"), MyMoneyAccount()); QVERIFY(a.MyMoneyObject::operator==(b)); QVERIFY(!(a.MyMoneyObject::operator==(c))); } void MyMoneyObjectTest::testReadXML() { TestMyMoneyObject t; QString ref_ok = QString( "\n" "\n" " \n" " \n" "\n" ); QString ref_false1 = QString( "\n" "\n" " \n" " \n" "\n" ); QString ref_false2 = QString( "\n" "\n" " \n" " \n" "\n" ); QDomDocument doc; QDomElement node; // id="" but required doc.setContent(ref_false1); node = doc.documentElement().firstChild().toElement(); try { t = TestMyMoneyObject(node); QFAIL("Missing expected exception"); } catch (const MyMoneyException &) { } // id attribute missing but required doc.setContent(ref_false2); node = doc.documentElement().firstChild().toElement(); try { t = TestMyMoneyObject(node); QFAIL("Missing expected exception"); } catch (const MyMoneyException &) { } // id present doc.setContent(ref_ok); node = doc.documentElement().firstChild().toElement(); try { t = TestMyMoneyObject(node); QVERIFY(t.id() == "T000000000000000001"); } catch (const MyMoneyException &) { QFAIL("Unexpected exception"); } // id="" but not required doc.setContent(ref_false1); node = doc.documentElement().firstChild().toElement(); try { t = TestMyMoneyObject(node, false); QVERIFY(t.id().isEmpty()); } catch (const MyMoneyException &) { QFAIL("Unexpected exception"); } } diff --git a/kmymoney/mymoney/tests/onlinejobtyped-test.cpp b/kmymoney/mymoney/tests/onlinejobtyped-test.cpp index dba39cb32..b75966566 100644 --- a/kmymoney/mymoney/tests/onlinejobtyped-test.cpp +++ b/kmymoney/mymoney/tests/onlinejobtyped-test.cpp @@ -1,136 +1,136 @@ /* * This file is part of KMyMoney, A Personal Finance Manager by KDE * Copyright (C) 2014 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 "onlinejobtyped-test.h" #include #include "onlinejob.h" #include "onlinejobtyped.h" #include "onlinetasks/dummy/tasks/dummytask.h" QTEST_GUILESS_MAIN(onlineJobTypedTest) class dummyTask2 : public dummyTask {}; class onlineTaskInterface {}; class onlineTaskDummy3 : public onlineTask, public onlineTaskInterface { public: ONLINETASK_META(dummyTask, "org.kmymoney.onlinetasks.dummytask"); - virtual bool isValid() const { + bool isValid() const final override { return true; } - virtual QString jobTypeName() const { + QString jobTypeName() const final override { return QLatin1String("Dummy credit transfer"); } - virtual QString storagePluginIid() const { + QString storagePluginIid() const final override { return QString(); } - virtual bool sqlModify(QSqlDatabase, const QString&) const { + bool sqlModify(QSqlDatabase, const QString&) const final override { return false; } - virtual bool sqlSave(QSqlDatabase, const QString&) const { + bool sqlSave(QSqlDatabase, const QString&) const final override { return false; } - virtual bool sqlRemove(QSqlDatabase, const QString&) const { + bool sqlRemove(QSqlDatabase, const QString&) const final override { return false; } protected: - virtual onlineTaskDummy3* clone() const { + onlineTaskDummy3* clone() const final override { return (new onlineTaskDummy3); } - virtual bool hasReferenceTo(const QString&) const { + bool hasReferenceTo(const QString&) const final override { return false; } - virtual void writeXML(QDomDocument&, QDomElement&) const {} - virtual onlineTaskDummy3* createFromXml(const QDomElement &) const { + void writeXML(QDomDocument&, QDomElement&) const final override {} + onlineTaskDummy3* createFromXml(const QDomElement &) const final override { return (new onlineTaskDummy3); } - virtual onlineTask* createFromSqlDatabase(QSqlDatabase, const QString&) const { + onlineTask* createFromSqlDatabase(QSqlDatabase, const QString&) const final override { return (new onlineTaskDummy3); } - virtual QString responsibleAccount() const { + QString responsibleAccount() const final override { return QString(); - }; + } }; void onlineJobTypedTest::initTestCase() { } void onlineJobTypedTest::cleanupTestCase() { } void onlineJobTypedTest::copyContructor() { dummyTask* task = new dummyTask; onlineJobTyped job(task); QVERIFY(!job.isNull()); QVERIFY(job.m_task == task); } void onlineJobTypedTest::constructWithIncompatibleType() { try { onlineJobTyped job(new dummyTask); QFAIL("Missing expected exception"); } catch (const onlineJob::badTaskCast&) { } catch (...) { QFAIL("Wrong exception thrown"); } } void onlineJobTypedTest::constructWithNull() { try { onlineJobTyped job(0); QFAIL("Missing expected exception"); } catch (const onlineJob::emptyTask&) { } catch (...) { QFAIL("Wrong exception thrown"); } } void onlineJobTypedTest::copyByAssignment() { dummyTask* task = new dummyTask; task->setTestNumber(8888); onlineJobTyped job(new dummyTask); job = onlineJobTyped(task); QVERIFY(!job.isNull()); QVERIFY(dynamic_cast(job.task())); QCOMPARE(job.task()->testNumber(), 8888); } void onlineJobTypedTest::constructWithManadtoryDynamicCast() { onlineJob job(new onlineTaskDummy3); try { onlineJobTyped jobTyped(job); } catch (...) { QFAIL("Unexpected exception"); } } diff --git a/kmymoney/payeeidentifier/ibanandbic/bicmodel.h b/kmymoney/payeeidentifier/ibanandbic/bicmodel.h index b3acad876..4ed3c9303 100644 --- a/kmymoney/payeeidentifier/ibanandbic/bicmodel.h +++ b/kmymoney/payeeidentifier/ibanandbic/bicmodel.h @@ -1,40 +1,40 @@ /* * This file is part of KMyMoney, A Personal Finance Manager by KDE * Copyright (C) 2014 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 BICMODEL_H #define BICMODEL_H #include #include "iban_bic_identifier_export.h" class IBAN_BIC_IDENTIFIER_EXPORT bicModel : public QSqlQueryModel { Q_OBJECT public: enum DisplayRole { InstitutionNameRole = Qt::UserRole }; explicit bicModel(QObject* parent = 0); - virtual QVariant data(const QModelIndex& item, int role = Qt::DisplayRole) const; + QVariant data(const QModelIndex& item, int role = Qt::DisplayRole) const final override; }; #endif // BICMODEL_H diff --git a/kmymoney/payeeidentifier/ibanandbic/ibanbic.h b/kmymoney/payeeidentifier/ibanandbic/ibanbic.h index 0a4449630..7941e538a 100644 --- a/kmymoney/payeeidentifier/ibanandbic/ibanbic.h +++ b/kmymoney/payeeidentifier/ibanandbic/ibanbic.h @@ -1,271 +1,271 @@ /* * This file is part of KMyMoney, A Personal Finance Manager by KDE * Copyright (C) 2014 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 PAYEEIDENTIFIER_IBANBIC_H #define PAYEEIDENTIFIER_IBANBIC_H #include #include #include "payeeidentifier/payeeidentifierdata.h" #include "mymoneyunittestable.h" #include "iban_bic_identifier_export.h" class ibanBicData; namespace payeeIdentifiers { /** * @brief Plugin to handle IBANs and BICs * * Can store a pair of an International Bank Account Number (ISO 13616) and Business Identifier Code (ISO 9362). * */ class IBAN_BIC_IDENTIFIER_EXPORT ibanBic : public payeeIdentifierData { KMM_MYMONEY_UNIT_TESTABLE public: PAYEEIDENTIFIER_IID(ibanBic, "org.kmymoney.payeeIdentifier.ibanbic"); enum bicAllocationStatus { bicAllocated = 0, bicNotAllocated, bicAllocationUncertain }; ibanBic(); ibanBic(const ibanBic& other); - ibanBic* clone() const; - ibanBic* createFromXml(const QDomElement& element) const; - void writeXML(QDomDocument& document, QDomElement& parent) const; + ibanBic* clone() const final override; + ibanBic* createFromXml(const QDomElement& element) const final override; + void writeXML(QDomDocument& document, QDomElement& parent) const final override; /** * @brief Set an owner name for this account */ void setOwnerName(const QString& ownerName) { m_ownerName = ownerName; } QString ownerName() const { return m_ownerName; } /** * @brief Set a IBAN * * The IBAN can contain spaces and other special chars. */ void setIban(const QString& iban); /** @copydoc m_iban * Use this method if you know that iban is in electronic format already. No futher checks are done. */ void setElectronicIban(const QString& iban) { Q_ASSERT(iban == ibanToElectronic(iban)); m_iban = iban; } /** @copydoc m_iban */ QString electronicIban() const { return m_iban; } /** * @brief Returns iban in human readable format * @see toPaperformatIban() */ QString paperformatIban(const QString& separator = QLatin1String(" ")) const; /** * @brief Set Business Identifier Code * * Call without parameter or QString() to remove bic * * @param bic will be normalized */ void setBic(const QString& bic = QString()); /** * @brief Business Identifier Code * According to ISO 9362 * * The returned bic is normalized: * A tailing XXX is omitted, all characters are uppercase. */ QString storedBic() const { return m_bic; } /** * @copydoc storedBic() * * Return a stored BIC (if there is any) or try to use the iban to get a BIC. */ QString bic() const; /** * @brief Business Identifier Code with tailing XXX * * Like @a bic() but always 11 characters long (if bic is invalid, it can have another length). */ QString fullBic() const; /** * @copydoc fullBic() * * This method will not try to use the iban to get a bic. */ QString fullStoredBic() const; /** * @brief Lookup institutions name * * Uses any available information to return an institutionName */ QString institutionName() const { return institutionNameByBic(bic()); } - virtual bool operator==(const payeeIdentifierData& other) const; + bool operator==(const payeeIdentifierData& other) const final override; bool operator==(const ibanBic& other) const; - virtual bool isValid() const; + bool isValid() const final override; /** * @brief Extends a bic to 11 characters * * Also all characters are made upper case. */ static QString bicToFullFormat(QString bic); /** * @brief Converts an iban to canonical format for machines * * Will remove all white spaces. */ static QString ibanToElectronic(const QString& iban); /** * @brief Converts an iban to human readable format * * Grouped in four letters strings separated by a white space. * * @param iban an iban, not needed to be canonical, valid or completed * @param separator Overwrite the default separator (e.g. a smaller space) */ static QString ibanToPaperformat(const QString& iban, const QString& separator = QLatin1String(" ")); /** * @brief Extract Basic Bank Account Number * * Returns the Basic Bank Account Number (BBAN) from the IBAN. * The BBAN is the IBAN without country code and the two digit checksum. */ static QString bban(const QString& iban); static int ibanLengthByCountry(const QString& countryCode); static QString institutionNameByBic(const QString& bic); static QString bicByIban(const QString& iban); static QString localBankCodeByIban(const QString& iban); /** * @brief Chech if IBAN is valid */ bool isIbanValid() const; /** * @brief Check if IBAN can be valid * * This method also checks if the given country code is valid. * * If also local aware checks are done (e.g. character set and length of BBAN). * * @todo Implement local aware checks */ static bool isIbanValid(const QString& iban); /** * @brief Check if this BIC is assigned to an bank * * This method does not check the given BIC but looks up in the database directly. * So it might be useful if time consumption is important but isBicAllocated() should * be your first option. * * @param bic BIC to test in canonical format (always 11 characters long, all characters uppercase) * @see isBicAllocated() */ static bicAllocationStatus isCanonicalBicAllocated(const QString& bic); /** @brief Check if this BIC is assigned to an bank * * @param bic BIC to test. */ static bicAllocationStatus isBicAllocated(const QString& bic); /** * @brief Check the checksum * * Test if the ISO 7064 mod 97-10 checksum of the iban is correct. * * @param iban An IBAN in electronic format (important!) */ static bool validateIbanChecksum(const QString& iban); static const int ibanMaxLength; - QString storagePluginIid() const { + QString storagePluginIid() const final override { return QLatin1String("org.kmymoney.payeeIdentifier.ibanbic.sqlStoragePlugin"); } - bool sqlSave(QSqlDatabase databaseConnection, const QString& objectId) const; - bool sqlModify(QSqlDatabase databaseConnection, const QString& objectId) const; - bool sqlRemove(QSqlDatabase databaseConnection, const QString& objectId) const; + bool sqlSave(QSqlDatabase databaseConnection, const QString& objectId) const final override; + bool sqlModify(QSqlDatabase databaseConnection, const QString& objectId) const final override; + bool sqlRemove(QSqlDatabase databaseConnection, const QString& objectId) const final override; private: /** * @brief Business Identifier Code * According to ISO 9362 * * A trailing XXX must be ommitted. All characters must be upper case. */ QString m_bic; /** * @brief International Bank Account Number * According to ISO 13616-1:2007 Part 1 * in normalized (electronic) format (no spaces etc.) */ QString m_iban; QString m_ownerName; static ::ibanBicData* getIbanBicData(); static ::ibanBicData* m_ibanBicData; bool writeQuery(QSqlQuery& query, const QString& id) const; - payeeIdentifierData* createFromSqlDatabase(QSqlDatabase db, const QString& identId) const; + payeeIdentifierData* createFromSqlDatabase(QSqlDatabase db, const QString& identId) const final override; static QString canonizeBic(const QString& bic); }; } // namespace payeeIdentifiers #endif // PAYEEIDENTIFIER_IBANBIC_H diff --git a/kmymoney/payeeidentifier/ibanandbic/widgets/bicvalidator.h b/kmymoney/payeeidentifier/ibanandbic/widgets/bicvalidator.h index d8a1c9f76..cdeebc297 100644 --- a/kmymoney/payeeidentifier/ibanandbic/widgets/bicvalidator.h +++ b/kmymoney/payeeidentifier/ibanandbic/widgets/bicvalidator.h @@ -1,39 +1,39 @@ /* * This file is part of KMyMoney, A Personal Finance Manager by KDE * Copyright (C) 2014 Christian David * * 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 BICVALIDATOR_H #define BICVALIDATOR_H #include #include "payeeidentifier_iban_bic_widgets_export.h" #include "kmymoneyvalidationfeedback.h" namespace eWidgets { namespace ValidationFeedback { enum class MessageType; } } class PAYEEIDENTIFIER_IBAN_BIC_WIDGETS_EXPORT bicValidator : public QValidator { Q_OBJECT public: explicit bicValidator(QObject* parent = 0); - virtual QValidator::State validate(QString& , int&) const; + QValidator::State validate(QString& , int&) const final override; static QPair validateWithMessage(const QString&); }; #endif // BICVALIDATOR_H diff --git a/kmymoney/payeeidentifier/ibanandbic/widgets/ibanbicitemdelegate.h b/kmymoney/payeeidentifier/ibanandbic/widgets/ibanbicitemdelegate.h index f7f1a7937..9dabaad3a 100644 --- a/kmymoney/payeeidentifier/ibanandbic/widgets/ibanbicitemdelegate.h +++ b/kmymoney/payeeidentifier/ibanandbic/widgets/ibanbicitemdelegate.h @@ -1,50 +1,50 @@ /* * This file is part of KMyMoney, A Personal Finance Manager by KDE * Copyright (C) 2014 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 IBANBICITEMDELEGATE_H #define IBANBICITEMDELEGATE_H #include "payeeidentifier_iban_bic_widgets_export.h" #include #include "payeeidentifier/payeeidentifiertyped.h" #include "payeeidentifier/ibanandbic/ibanbic.h" class PAYEEIDENTIFIER_IBAN_BIC_WIDGETS_EXPORT ibanBicItemDelegate : public QStyledItemDelegate { Q_OBJECT Q_PLUGIN_METADATA(IID "org.kmymoney.payeeIdentifier.ibanbic.delegate" FILE "kmymoney-ibanbic-delegate.json") public: explicit ibanBicItemDelegate(QObject* parent = nullptr, const QVariantList& args = QVariantList()); - virtual void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const; - virtual QSize sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const; - virtual QWidget* createEditor(QWidget* parent, const QStyleOptionViewItem& option, const QModelIndex& index) const; - virtual void setEditorData(QWidget* editor, const QModelIndex& index) const; - virtual void setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const; - virtual void updateEditorGeometry(QWidget* editor, const QStyleOptionViewItem& option, const QModelIndex& index) const; + void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const override; + QSize sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const override; + QWidget* createEditor(QWidget* parent, const QStyleOptionViewItem& option, const QModelIndex& index) const override; + void setEditorData(QWidget* editor, const QModelIndex& index) const override; + void setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const override; + void updateEditorGeometry(QWidget* editor, const QStyleOptionViewItem& option, const QModelIndex& index) const override; Q_SIGNALS: void sizeHintChanged(const QModelIndex&) const; private: inline payeeIdentifierTyped ibanBicByIndex(const QModelIndex& index) const; }; #endif // IBANBICITEMDELEGATE_H diff --git a/kmymoney/payeeidentifier/ibanandbic/widgets/ibanvalidator.h b/kmymoney/payeeidentifier/ibanandbic/widgets/ibanvalidator.h index a70bb566a..48efcbed7 100644 --- a/kmymoney/payeeidentifier/ibanandbic/widgets/ibanvalidator.h +++ b/kmymoney/payeeidentifier/ibanandbic/widgets/ibanvalidator.h @@ -1,43 +1,43 @@ /* * This file is part of KMyMoney, A Personal Finance Manager by KDE * Copyright (C) 2014 Christian David * * 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 IBANVALIDATOR_H #define IBANVALIDATOR_H #include "payeeidentifier_iban_bic_widgets_export.h" #include #include "kmymoneyvalidationfeedback.h" namespace eWidgets { namespace ValidationFeedback { enum class MessageType; } } class PAYEEIDENTIFIER_IBAN_BIC_WIDGETS_EXPORT ibanValidator : public QValidator { Q_OBJECT public: explicit ibanValidator(QObject* parent = 0); - virtual State validate(QString& , int&) const; + State validate(QString& , int&) const final override; State validate(const QString&) const; - virtual void fixup(QString&) const; + void fixup(QString&) const final override; static QPair validateWithMessage(const QString&); }; #endif // IBANVALIDATOR_H diff --git a/kmymoney/payeeidentifier/ibanandbic/widgets/kbicedit.cpp b/kmymoney/payeeidentifier/ibanandbic/widgets/kbicedit.cpp index 8fe23681c..a76839161 100644 --- a/kmymoney/payeeidentifier/ibanandbic/widgets/kbicedit.cpp +++ b/kmymoney/payeeidentifier/ibanandbic/widgets/kbicedit.cpp @@ -1,120 +1,120 @@ /* * This file is part of KMyMoney, A Personal Finance Manager by KDE * Copyright (C) 2014 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 "kbicedit.h" #include #include #include #include #include #include #include "../bicmodel.h" #include "bicvalidator.h" #include "kmymoneyvalidationfeedback.h" class bicItemDelegate : public QStyledItemDelegate { public: explicit bicItemDelegate(QObject* parent = 0) : QStyledItemDelegate(parent) {} - void paint(QPainter * painter, const QStyleOptionViewItem & option, const QModelIndex & index) const; - virtual QSize sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const; + void paint(QPainter * painter, const QStyleOptionViewItem & option, const QModelIndex & index) const final override; + QSize sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const final override; private: inline QFont getSmallFont(const QStyleOptionViewItem& option) const; }; KBicEdit::KBicEdit(QWidget* parent) : KLineEdit(parent) { QCompleter* completer = new QCompleter(this); bicModel* model = new bicModel(this); completer->setModel(model); m_popupDelegate = new bicItemDelegate(this); completer->popup()->setItemDelegate(m_popupDelegate); setCompleter(completer); bicValidator *const validator = new bicValidator(this); setValidator(validator); } KBicEdit::~KBicEdit() { delete m_popupDelegate; } QFont bicItemDelegate::getSmallFont(const QStyleOptionViewItem& option) const { QFont smallFont = option.font; smallFont.setPointSize(0.9*smallFont.pointSize()); return smallFont; } QSize bicItemDelegate::sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const { QStyleOptionViewItem opt = option; initStyleOption(&opt, index); QFontMetrics metrics(option.font); QFontMetrics smallMetrics(getSmallFont(option)); const QStyle *style = opt.widget ? opt.widget->style() : QApplication::style(); const int margin = style->pixelMetric(QStyle::PM_FocusFrameHMargin) + 1; // A bic has maximal 11 characters. So we guess, we want to display 11 characters. The name of the institution has to adapt to what is given return QSize(metrics.width(QLatin1Char('X')) + 2*margin, metrics.lineSpacing() + smallMetrics.lineSpacing() + smallMetrics.leading() + 2*margin); } /** * @todo enable eliding (use QFontMetrics::elidedText() ) */ void bicItemDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const { QStyleOptionViewItem opt = option; initStyleOption(&opt, index); // Background QStyle *style = opt.widget ? opt.widget->style() : QApplication::style(); style->drawPrimitive(QStyle::PE_PanelItemViewItem, &opt, painter, opt.widget); const int margin = style->pixelMetric(QStyle::PM_FocusFrameHMargin) + 1; const QRect textArea = QRect(opt.rect.x() + margin, opt.rect.y() + margin, opt.rect.width() - 2 * margin, opt.rect.height() - 2 * margin); // Paint name painter->save(); QFont smallFont = getSmallFont(opt); QFontMetrics metrics(opt.font); QFontMetrics smallMetrics(smallFont); QRect nameRect = style->alignedRect(opt.direction, Qt::AlignBottom, QSize(textArea.width(), smallMetrics.lineSpacing()), textArea); painter->setFont(smallFont); style->drawItemText(painter, nameRect, Qt::AlignBottom, QApplication::palette(), true, index.model()->data(index, bicModel::InstitutionNameRole).toString(), option.state & QStyle::State_Selected ? QPalette::HighlightedText : QPalette::Mid); painter->restore(); // Paint BIC painter->save(); QFont normal = painter->font(); normal.setBold(true); painter->setFont(normal); QRect bicRect = style->alignedRect(opt.direction, Qt::AlignTop, QSize(textArea.width(), metrics.lineSpacing()), textArea); const QString bic = index.model()->data(index, Qt::DisplayRole).toString(); style->drawItemText(painter, bicRect, Qt::AlignTop, QApplication::palette(), true, bic, option.state & QStyle::State_Selected ? QPalette::HighlightedText : QPalette::Text); painter->restore(); } diff --git a/kmymoney/payeeidentifier/nationalaccount/nationalaccount.h b/kmymoney/payeeidentifier/nationalaccount/nationalaccount.h index 2615c23a4..02f920cfb 100644 --- a/kmymoney/payeeidentifier/nationalaccount/nationalaccount.h +++ b/kmymoney/payeeidentifier/nationalaccount/nationalaccount.h @@ -1,96 +1,96 @@ /* * This file is part of KMyMoney, A Personal Finance Manager by KDE * Copyright (C) 2014 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 NATIONALACCOUNTID_H #define NATIONALACCOUNTID_H #include "mymoney/payeeidentifier/payeeidentifierdata.h" #include "nationalaccount_identifier_export.h" namespace payeeIdentifiers { class NATIONALACCOUNT_IDENTIFIER_EXPORT nationalAccount : public payeeIdentifierData { public: PAYEEIDENTIFIER_IID(nationalAccount, "org.kmymoney.payeeIdentifier.national"); nationalAccount(); nationalAccount(const nationalAccount& other); - virtual bool isValid() const; - virtual bool operator==(const payeeIdentifierData& other) const; + bool isValid() const final override; + bool operator==(const payeeIdentifierData& other) const final override; bool operator==(const nationalAccount& other) const; - nationalAccount* clone() const { + nationalAccount* clone() const final override { return new nationalAccount(*this); } - nationalAccount* createFromXml(const QDomElement& element) const; - void writeXML(QDomDocument& document, QDomElement& parent) const; - - QString storagePluginIid() const; - bool sqlSave(QSqlDatabase databaseConnection, const QString& objectId) const; - bool sqlModify(QSqlDatabase databaseConnection, const QString& objectId) const; - bool sqlRemove(QSqlDatabase databaseConnection, const QString& objectId) const; - nationalAccount* createFromSqlDatabase(QSqlDatabase db, const QString& identId) const; + nationalAccount* createFromXml(const QDomElement& element) const final override; + void writeXML(QDomDocument& document, QDomElement& parent) const final override; + + QString storagePluginIid() const final override; + bool sqlSave(QSqlDatabase databaseConnection, const QString& objectId) const final override; + bool sqlModify(QSqlDatabase databaseConnection, const QString& objectId) const final override; + bool sqlRemove(QSqlDatabase databaseConnection, const QString& objectId) const final override; + nationalAccount* createFromSqlDatabase(QSqlDatabase db, const QString& identId) const final override; void setBankCode(const QString& bankCode) { m_bankCode = bankCode; } QString bankCode() const { return m_bankCode; } /** @todo implement */ QString bankName() const { return QString(); } void setAccountNumber(const QString& accountNumber) { m_accountNumber = accountNumber; } QString accountNumber() const { return m_accountNumber; } QString country() const { return m_country; } void setCountry(const QString& countryCode) { m_country = countryCode.toUpper(); } QString ownerName() const { return m_ownerName; } void setOwnerName(const QString& ownerName) { m_ownerName = ownerName; } private: bool writeQuery(QSqlQuery& query, const QString& id) const; QString m_ownerName; QString m_country; QString m_bankCode; QString m_accountNumber; }; } // namespace payeeIdentifiers #endif // NATIONALACCOUNTID_H diff --git a/kmymoney/payeeidentifier/nationalaccount/ui/nationalaccountdelegate.h b/kmymoney/payeeidentifier/nationalaccount/ui/nationalaccountdelegate.h index a59d5ea59..ae99e8fd4 100644 --- a/kmymoney/payeeidentifier/nationalaccount/ui/nationalaccountdelegate.h +++ b/kmymoney/payeeidentifier/nationalaccount/ui/nationalaccountdelegate.h @@ -1,48 +1,48 @@ /* * This file is part of KMyMoney, A Personal Finance Manager by KDE * Copyright (C) 2014 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 NATIONALACCOUNTDELEGATE_H #define NATIONALACCOUNTDELEGATE_H #include #include "../nationalaccount.h" #include "payeeidentifier/payeeidentifiertyped.h" class nationalAccountDelegate : public QStyledItemDelegate { Q_OBJECT public: explicit nationalAccountDelegate(QObject* parent, const QVariantList& options = QVariantList()); - virtual void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const; - virtual QSize sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const; - virtual QWidget* createEditor(QWidget* parent, const QStyleOptionViewItem& option, const QModelIndex& index) const; - virtual void setEditorData(QWidget* editor, const QModelIndex& index) const; - virtual void setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const; - virtual void updateEditorGeometry(QWidget* editor, const QStyleOptionViewItem& option, const QModelIndex& index) const; + void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const final override; + QSize sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const final override; + QWidget* createEditor(QWidget* parent, const QStyleOptionViewItem& option, const QModelIndex& index) const final override; + void setEditorData(QWidget* editor, const QModelIndex& index) const final override; + void setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const final override; + void updateEditorGeometry(QWidget* editor, const QStyleOptionViewItem& option, const QModelIndex& index) const final override; Q_SIGNALS: void sizeHintChanged(const QModelIndex&) const; private: inline payeeIdentifierTyped identByIndex(const QModelIndex& index) const; }; #endif // NATIONALACCOUNTDELEGATE_H diff --git a/kmymoney/payeeidentifier/unavailableplugin/unavailableplugin.h b/kmymoney/payeeidentifier/unavailableplugin/unavailableplugin.h index a5a468cbf..f22f69c47 100644 --- a/kmymoney/payeeidentifier/unavailableplugin/unavailableplugin.h +++ b/kmymoney/payeeidentifier/unavailableplugin/unavailableplugin.h @@ -1,74 +1,74 @@ /* * This file is part of KMyMoney, A Personal Finance Manager by KDE * Copyright (C) 2014 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 UNAVAILABLEPLUGIN_H #define UNAVAILABLEPLUGIN_H #include "payeeidentifier/payeeidentifierdata.h" #include class payeeIdentifierLoader; namespace payeeIdentifiers { /** * @brief A payeeIdentifier which is used to store the plain xml data * * To avoid data loss if a plugin could not be loaded this payeeIdentifier is used in the xml backend. * It stores the data in plain xml so it can be written back to the file. */ class payeeIdentifierUnavailable : public payeeIdentifierData { public: PAYEEIDENTIFIER_IID(payeeIdentifierUnavailable, "org.kmymoney.payeeIdentifier.payeeIdentifierUnavailable"); payeeIdentifierUnavailable(); - virtual void writeXML(QDomDocument& document, QDomElement& parent) const; - virtual payeeIdentifierUnavailable* createFromXml(const QDomElement& element) const; - virtual bool isValid() const; - virtual bool operator==(const payeeIdentifierData& other) const; + void writeXML(QDomDocument& document, QDomElement& parent) const final override; + payeeIdentifierUnavailable* createFromXml(const QDomElement& element) const final override; + bool isValid() const final override; + bool operator==(const payeeIdentifierData& other) const final override; bool operator==(const payeeIdentifierUnavailable& other) const; friend class payeeIdentifierLoader; /** @todo make private */ explicit payeeIdentifierUnavailable(QDomElement data); /** * @name SqlMethods * @{ * For SQL databases this plugin is not needed nor used. So these functions * do not have a real implementation. */ - virtual QString storagePluginIid() const; - virtual bool sqlSave(QSqlDatabase databaseConnection, const QString& onlineJobId) const; - virtual bool sqlModify(QSqlDatabase databaseConnection, const QString& onlineJobId) const; - virtual bool sqlRemove(QSqlDatabase databaseConnection, const QString& onlineJobId) const; - virtual payeeIdentifierData* createFromSqlDatabase(QSqlDatabase db, const QString& identId) const; + QString storagePluginIid() const final override; + bool sqlSave(QSqlDatabase databaseConnection, const QString& onlineJobId) const final override; + bool sqlModify(QSqlDatabase databaseConnection, const QString& onlineJobId) const final override; + bool sqlRemove(QSqlDatabase databaseConnection, const QString& onlineJobId) const final override; + payeeIdentifierData* createFromSqlDatabase(QSqlDatabase db, const QString& identId) const final override; /** @} */ protected: - virtual payeeIdentifierUnavailable* clone() const; + payeeIdentifierUnavailable* clone() const final override; private: QDomElement m_data; }; } // namespace payeeidentifiers #endif // UNAVAILABLEPLUGIN_H diff --git a/kmymoney/plugins/csv/import/bankingwizardpage.h b/kmymoney/plugins/csv/import/bankingwizardpage.h index f68eff54c..10f110fa9 100644 --- a/kmymoney/plugins/csv/import/bankingwizardpage.h +++ b/kmymoney/plugins/csv/import/bankingwizardpage.h @@ -1,85 +1,85 @@ /******************************************************************************* * bankingwizardpage.h * ------------------ * begin : Thur Jan 01 2015 * copyright : (C) 2015 by Allan Anderson * email : agander93@gmail.com * copyright : (C) 2016 by Łukasz Wojniłowicz * email : lukasz.wojnilowicz@gmail.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. * * * ********************************************************************************/ #ifndef BANKINGWIZARDPAGE_H #define BANKINGWIZARDPAGE_H // ---------------------------------------------------------------------------- // QT Includes // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes #include "csvwizardpage.h" // ---------------------------------------------------------------------------- class BankingProfile; class MyMoneyStatement; namespace Ui { class BankingPage; } class BankingPage : public CSVWizardPage { Q_OBJECT public: explicit BankingPage(CSVWizard *dlg, CSVImporterCore *imp); ~BankingPage(); bool validateCreditDebit(); /** * This method fills QIF file with bank/credit card data */ void makeQIF(const MyMoneyStatement &st, const QString &outFileName); private: - void initializePage(); - bool isComplete() const; - int nextId() const; + void initializePage() final override; + bool isComplete() const final override; + int nextId() const final override; bool validateMemoComboBox(); void resetComboBox(const Column comboBox); bool validateSelectedColumn(const int col, const Column type); BankingProfile *m_profile; Ui::BankingPage *ui; private Q_SLOTS: void memoColSelected(int col); void categoryColSelected(int col); void numberColSelected(int col); void payeeColSelected(int col); void dateColSelected(int col); void debitColSelected(int col); void creditColSelected(int col); void amountColSelected(int col); void amountToggled(bool checked); void debitCreditToggled(bool checked); void oppositeSignsClicked(bool checked); void clearColumns(); }; #endif // BANKINGWIZARDPAGE_H diff --git a/kmymoney/plugins/csv/import/core/csvimportercore.cpp b/kmymoney/plugins/csv/import/core/csvimportercore.cpp index 671bc644d..adf42c94f 100644 --- a/kmymoney/plugins/csv/import/core/csvimportercore.cpp +++ b/kmymoney/plugins/csv/import/core/csvimportercore.cpp @@ -1,1767 +1,1766 @@ /*************************************************************************** csvimportercore.cpp ------------------- begin : Sun May 21 2017 copyright : (C) 2010 by Allan Anderson email : agander93@gmail.com copyright : (C) 2016-2017 by Łukasz Wojniłowicz email : lukasz.wojnilowicz@gmail.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 "csvimportercore.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "mymoneyfile.h" #include "mymoneyaccount.h" #include "mymoneysecurity.h" #include "mymoneytransaction.h" #include "csvutil.h" #include "convdate.h" #include "mymoneyenums.h" const QHash CSVImporterCore::m_profileConfPrefix { {Profile::Banking, QStringLiteral("Bank")}, {Profile::Investment, QStringLiteral("Invest")}, {Profile::CurrencyPrices, QStringLiteral("CPrices")}, {Profile::StockPrices, QStringLiteral("SPrices")} }; const QHash CSVImporterCore::m_colTypeConfName { {Column::Date, QStringLiteral("DateCol")}, {Column::Memo, QStringLiteral("MemoCol")}, {Column::Number, QStringLiteral("NumberCol")}, {Column::Payee, QStringLiteral("PayeeCol")}, {Column::Amount, QStringLiteral("AmountCol")}, {Column::Credit, QStringLiteral("CreditCol")}, {Column::Debit, QStringLiteral("DebitCol")}, {Column::Category, QStringLiteral("CategoryCol")}, {Column::Type, QStringLiteral("TypeCol")}, {Column::Price, QStringLiteral("PriceCol")}, {Column::Quantity, QStringLiteral("QuantityCol")}, {Column::Fee, QStringLiteral("FeeCol")}, {Column::Symbol, QStringLiteral("SymbolCol")}, {Column::Name, QStringLiteral("NameCol")}, }; const QHash CSVImporterCore::m_miscSettingsConfName { {ConfDirectory, QStringLiteral("Directory")}, {ConfEncoding, QStringLiteral("Encoding")}, {ConfDateFormat, QStringLiteral("DateFormat")}, {ConfFieldDelimiter, QStringLiteral("FieldDelimiter")}, {ConfTextDelimiter, QStringLiteral("TextDelimiter")}, {ConfDecimalSymbol, QStringLiteral("DecimalSymbol")}, {ConfStartLine, QStringLiteral("StartLine")}, {ConfTrailerLines, QStringLiteral("TrailerLines")}, {ConfOppositeSigns, QStringLiteral("OppositeSigns")}, {ConfFeeIsPercentage, QStringLiteral("FeeIsPercentage")}, {ConfFeeRate, QStringLiteral("FeeRate")}, {ConfMinFee, QStringLiteral("MinFee")}, {ConfSecurityName, QStringLiteral("SecurityName")}, {ConfSecuritySymbol, QStringLiteral("SecuritySymbol")}, {ConfCurrencySymbol, QStringLiteral("CurrencySymbol")}, {ConfPriceFraction, QStringLiteral("PriceFraction")}, {ConfDontAsk, QStringLiteral("DontAsk")}, {ConfHeight, QStringLiteral("Height")}, {ConfWidth, QStringLiteral("Width")} }; const QHash CSVImporterCore::m_transactionConfName { {eMyMoney::Transaction::Action::Buy, QStringLiteral("BuyParam")}, {eMyMoney::Transaction::Action::Sell, QStringLiteral("SellParam")}, {eMyMoney::Transaction::Action::ReinvestDividend, QStringLiteral("ReinvdivParam")}, {eMyMoney::Transaction::Action::CashDividend, QStringLiteral("DivXParam")}, {eMyMoney::Transaction::Action::Interest, QStringLiteral("IntIncParam")}, {eMyMoney::Transaction::Action::Shrsin, QStringLiteral("ShrsinParam")}, {eMyMoney::Transaction::Action::Shrsout, QStringLiteral("ShrsoutParam")} }; const QString CSVImporterCore::m_confProfileNames = QStringLiteral("ProfileNames"); const QString CSVImporterCore::m_confPriorName = QStringLiteral("Prior"); const QString CSVImporterCore::m_confMiscName = QStringLiteral("Misc"); CSVImporterCore::CSVImporterCore() : m_profile(0), m_isActionTypeValidated(false) { m_convertDate = new ConvertDate; m_file = new CSVFile; m_priceFractions << MyMoneyMoney(0.01) << MyMoneyMoney(0.1) << MyMoneyMoney::ONE << MyMoneyMoney(10) << MyMoneyMoney(100); validateConfigFile(); readMiscSettings(); } CSVImporterCore::~CSVImporterCore() { delete m_convertDate; delete m_file; } MyMoneyStatement CSVImporterCore::unattendedImport(const QString &filename, CSVProfile *profile) { MyMoneyStatement st; m_profile = profile; m_convertDate->setDateFormatIndex(m_profile->m_dateFormat); if (m_file->getInFileName(filename)) { m_file->readFile(m_profile); m_file->setupParser(m_profile); if (profile->m_decimalSymbol == DecimalSymbol::Auto) { auto columns = getNumericalColumns(); if (detectDecimalSymbols(columns) != -2) return st; } if (!createStatement(st)) st = MyMoneyStatement(); } return st; } KSharedConfigPtr CSVImporterCore::configFile() { return KSharedConfig::openConfig(QStringLiteral("kmymoney/csvimporterrc")); } void CSVImporterCore::profileFactory(const Profile type, const QString &name) { // delete current profile if (m_profile) { delete m_profile; m_profile = nullptr; } switch (type) { default: case Profile::Investment: m_profile = new InvestmentProfile; break; case Profile::Banking: m_profile = new BankingProfile; break; case Profile::CurrencyPrices: case Profile::StockPrices: m_profile = new PricesProfile(type); break; } m_profile->m_profileName = name; } void CSVImporterCore::readMiscSettings() { KConfigGroup miscGroup(configFile(), m_confMiscName); m_autodetect.clear(); m_autodetect.insert(AutoFieldDelimiter, miscGroup.readEntry(QStringLiteral("AutoFieldDelimiter"), true)); m_autodetect.insert(AutoDecimalSymbol, miscGroup.readEntry(QStringLiteral("AutoDecimalSymbol"), true)); m_autodetect.insert(AutoDateFormat, miscGroup.readEntry(QStringLiteral("AutoDateFormat"), true)); m_autodetect.insert(AutoAccountInvest, miscGroup.readEntry(QStringLiteral("AutoAccountInvest"), true)); m_autodetect.insert(AutoAccountBank, miscGroup.readEntry(QStringLiteral("AutoAccountBank"), true)); } void CSVImporterCore::validateConfigFile() { const KSharedConfigPtr config = configFile(); KConfigGroup profileNamesGroup(config, m_confProfileNames); if (!profileNamesGroup.exists()) { profileNamesGroup.writeEntry(m_profileConfPrefix.value(Profile::Banking), QStringList()); profileNamesGroup.writeEntry(m_profileConfPrefix.value(Profile::Investment), QStringList()); profileNamesGroup.writeEntry(m_profileConfPrefix.value(Profile::CurrencyPrices), QStringList()); profileNamesGroup.writeEntry(m_profileConfPrefix.value(Profile::StockPrices), QStringList()); profileNamesGroup.writeEntry(m_confPriorName + m_profileConfPrefix.value(Profile::Banking), int()); profileNamesGroup.writeEntry(m_confPriorName + m_profileConfPrefix.value(Profile::Investment), int()); profileNamesGroup.writeEntry(m_confPriorName + m_profileConfPrefix.value(Profile::CurrencyPrices), int()); profileNamesGroup.writeEntry(m_confPriorName + m_profileConfPrefix.value(Profile::StockPrices), int()); profileNamesGroup.sync(); } KConfigGroup miscGroup(config, m_confMiscName); if (!miscGroup.exists()) { miscGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfHeight), "400"); miscGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfWidth), "800"); miscGroup.sync(); } QList confVer = miscGroup.readEntry("KMMVer", QList {0, 0, 0}); if (updateConfigFile(confVer)) // write kmmVer only if there were no errors miscGroup.writeEntry("KMMVer", confVer); } bool CSVImporterCore::updateConfigFile(QList &confVer) { bool ret = true; QList kmmVer = QList {5, 0, 0}; int kmmVersion = kmmVer.at(0) * 100 + kmmVer.at(1) * 10 + kmmVer.at(2); int confVersion = confVer.at(0) * 100 + confVer.at(1) * 10 + confVer.at(2); if (confVersion > kmmVersion) { KMessageBox::information(0, i18n("Version of your CSV config file is %1.%2.%3 and is newer than supported version %4.%5.%6. Expect troubles.", confVer.at(0), confVer.at(1), confVer.at(2), kmmVer.at(0), kmmVer.at(1), kmmVer.at(2))); ret = false; return ret; } else if (confVersion == kmmVersion) return true; confVer = kmmVer; const KSharedConfigPtr config = configFile(); QString configFilePath = config.constData()->name(); QFile::copy(configFilePath, configFilePath + QLatin1String(".bak")); KConfigGroup profileNamesGroup(config, m_confProfileNames); QStringList bankProfiles = profileNamesGroup.readEntry(m_profileConfPrefix.value(Profile::Banking), QStringList()); QStringList investProfiles = profileNamesGroup.readEntry(m_profileConfPrefix.value(Profile::Investment), QStringList()); QStringList invalidBankProfiles = profileNamesGroup.readEntry(QLatin1String("Invalid") + m_profileConfPrefix.value(Profile::Banking), QStringList()); // get profiles that was marked invalid during last update QStringList invalidInvestProfiles = profileNamesGroup.readEntry(QLatin1String("Invalid") + m_profileConfPrefix.value(Profile::Investment), QStringList()); QString bankPrefix = m_profileConfPrefix.value(Profile::Banking) + QLatin1Char('-'); QString investPrefix = m_profileConfPrefix.value(Profile::Investment) + QLatin1Char('-'); // for kmm < 5.0.0 change 'BankNames' to 'ProfileNames' and remove 'MainWindow' group if (confVersion < 500 && bankProfiles.isEmpty()) { KConfigGroup oldProfileNamesGroup(config, "BankProfiles"); bankProfiles = oldProfileNamesGroup.readEntry("BankNames", QStringList()); // profile names are under 'BankNames' entry for kmm < 5.0.0 bankPrefix = QLatin1String("Profiles-"); // needed to remove non-existent profiles in first run oldProfileNamesGroup.deleteGroup(); KConfigGroup oldMainWindowGroup(config, "MainWindow"); oldMainWindowGroup.deleteGroup(); KConfigGroup oldSecuritiesGroup(config, "Securities"); oldSecuritiesGroup.deleteGroup(); } bool firstTry = false; if (invalidBankProfiles.isEmpty() && invalidInvestProfiles.isEmpty()) // if there is no invalid profiles then this might be first update try firstTry = true; int invalidProfileResponse = QDialogButtonBox::No; for (auto profileName = bankProfiles.begin(); profileName != bankProfiles.end();) { KConfigGroup bankProfile(config, bankPrefix + *profileName); if (!bankProfile.exists() && !invalidBankProfiles.contains(*profileName)) { // if there is reference to profile but no profile then remove this reference profileName = bankProfiles.erase(profileName); continue; } // for kmm < 5.0.0 remove 'FileType' and 'ProfileName' and assign them to either "Bank=" or "Invest=" if (confVersion < 500) { QString lastUsedDirectory; KConfigGroup oldBankProfile(config, QLatin1String("Profiles-") + *profileName); // if half of configuration is updated and the other one untouched this is needed QString oldProfileType = oldBankProfile.readEntry("FileType", QString()); KConfigGroup newProfile; if (oldProfileType == QLatin1String("Invest")) { oldBankProfile.deleteEntry("BrokerageParam"); oldBankProfile.writeEntry(m_colTypeConfName.value(Column::Type), oldBankProfile.readEntry("PayeeCol")); oldBankProfile.deleteEntry("PayeeCol"); oldBankProfile.deleteEntry("Filter"); oldBankProfile.deleteEntry("SecurityName"); lastUsedDirectory = oldBankProfile.readEntry("InvDirectory"); newProfile = KConfigGroup(config, m_profileConfPrefix.value(Profile::Investment) + QLatin1Char('-') + *profileName); investProfiles.append(*profileName); profileName = bankProfiles.erase(profileName); } else if (oldProfileType == QLatin1String("Banking")) { lastUsedDirectory = oldBankProfile.readEntry("CsvDirectory"); newProfile = KConfigGroup(config, m_profileConfPrefix.value(Profile::Banking) + QLatin1Char('-') + *profileName); ++profileName; } else { if (invalidProfileResponse != QDialogButtonBox::YesToAll && invalidProfileResponse != QDialogButtonBox::NoToAll) { if (!firstTry && !invalidBankProfiles.contains(*profileName)) { // if it isn't first update run and profile isn't on the list of invalid ones then don't bother ++profileName; continue; } invalidProfileResponse = KMessageBox::createKMessageBox(nullptr, new QDialogButtonBox(QDialogButtonBox::Yes | QDialogButtonBox::YesToAll | QDialogButtonBox::No | QDialogButtonBox::NoToAll), QMessageBox::Warning, i18n("
During update of %1
" "the profile type for %2 could not be recognized.
" "The profile cannot be used because of that.
" "Do you want to delete it?
", configFilePath, *profileName), QStringList(), QString(), nullptr, KMessageBox::Dangerous); } switch (invalidProfileResponse) { case QDialogButtonBox::YesToAll: case QDialogButtonBox::Yes: oldBankProfile.deleteGroup(); invalidBankProfiles.removeOne(*profileName); profileName = bankProfiles.erase(profileName); break; case QDialogButtonBox::NoToAll: case QDialogButtonBox::No: if (!invalidBankProfiles.contains(*profileName)) // on user request: don't delete profile but keep eye on it invalidBankProfiles.append(*profileName); ret = false; ++profileName; break; } continue; } oldBankProfile.deleteEntry("FileType"); oldBankProfile.deleteEntry("ProfileName"); oldBankProfile.deleteEntry("DebitFlag"); oldBankProfile.deleteEntry("InvDirectory"); oldBankProfile.deleteEntry("CsvDirectory"); oldBankProfile.sync(); oldBankProfile.copyTo(&newProfile); oldBankProfile.deleteGroup(); newProfile.writeEntry(m_miscSettingsConfName.value(ConfDirectory), lastUsedDirectory); newProfile.writeEntry(m_miscSettingsConfName.value(ConfEncoding), "106" /*UTF-8*/ ); // in 4.8 encoding wasn't supported well so set it to utf8 by default newProfile.sync(); } } for (auto profileName = investProfiles.begin(); profileName != investProfiles.end();) { KConfigGroup investProfile(config, investPrefix + *profileName); if (!investProfile.exists() && !invalidInvestProfiles.contains(*profileName)) { // if there is reference to profile but no profile then remove this reference profileName = investProfiles.erase(profileName); continue; } ++profileName; } profileNamesGroup.writeEntry(m_profileConfPrefix.value(Profile::Banking), bankProfiles); // update profile names as some of them might have been changed profileNamesGroup.writeEntry(m_profileConfPrefix.value(Profile::Investment), investProfiles); if (invalidBankProfiles.isEmpty()) // if no invalid profiles then we don't need this variable anymore profileNamesGroup.deleteEntry("InvalidBank"); else profileNamesGroup.writeEntry("InvalidBank", invalidBankProfiles); if (invalidInvestProfiles.isEmpty()) profileNamesGroup.deleteEntry("InvalidInvest"); else profileNamesGroup.writeEntry("InvalidInvest", invalidInvestProfiles); if (ret) QFile::remove(configFilePath + ".bak"); // remove backup if all is ok return ret; } bool CSVImporterCore::profilesAction(const Profile type, const ProfileAction action, const QString &name, const QString &newname) { bool ret = false; const KSharedConfigPtr config = configFile(); KConfigGroup profileNamesGroup(config, m_confProfileNames); QString profileTypeStr = m_profileConfPrefix.value(type); QStringList profiles = profileNamesGroup.readEntry(profileTypeStr, QStringList()); KConfigGroup profileName(config, profileTypeStr + QLatin1Char('-') + name); switch (action) { case ProfileAction::UpdateLastUsed: profileNamesGroup.writeEntry(m_confPriorName + profileTypeStr, profiles.indexOf(name)); break; case ProfileAction::Add: if (!profiles.contains(newname)) { profiles.append(newname); ret = true; } break; case ProfileAction::Remove: { profiles.removeOne(name); profileName.deleteGroup(); profileName.sync(); ret = true; break; } case ProfileAction::Rename: { if (!newname.isEmpty() && name != newname) { int idx = profiles.indexOf(name); if (idx != -1) { profiles[idx] = newname; KConfigGroup newProfileName(config, profileTypeStr + QLatin1Char('-') + newname); if (profileName.exists() && !newProfileName.exists()) { profileName.copyTo(&newProfileName); profileName.deleteGroup(); profileName.sync(); newProfileName.sync(); ret = true; } } } break; } } profileNamesGroup.writeEntry(profileTypeStr, profiles); profileNamesGroup.sync(); return ret; } bool CSVImporterCore::validateDateFormat(const int col) { bool isOK = true; for (int row = m_profile->m_startLine; row <= m_profile->m_endLine; ++row) { QStandardItem* item = m_file->m_model->item(row, col); QDate dat = m_convertDate->convertDate(item->text()); if (dat == QDate()) { isOK = false; break; } } return isOK; } bool CSVImporterCore::validateDecimalSymbols(const QList &columns) { bool isOK = true; foreach (const auto column, columns) { m_file->m_parse->setDecimalSymbol(m_decimalSymbolIndexMap.value(column)); for (int row = m_profile->m_startLine; row <= m_profile->m_endLine; ++row) { QStandardItem *item = m_file->m_model->item(row, column); QString rawNumber = item->text(); m_file->m_parse->possiblyReplaceSymbol(rawNumber); if (m_file->m_parse->invalidConversion() && !rawNumber.isEmpty()) { // empty strings are welcome isOK = false; break; } } } return isOK; } bool CSVImporterCore::validateCurrencies(const PricesProfile *profile) { if (profile->m_securitySymbol.isEmpty() || profile->m_currencySymbol.isEmpty()) return false; return true; } bool CSVImporterCore::validateSecurity(const PricesProfile *profile) { if (profile->m_securitySymbol.isEmpty() || profile->m_securityName.isEmpty()) return false; return true; } bool CSVImporterCore::validateSecurity(const InvestmentProfile *profile) { if (profile->m_securitySymbol.isEmpty() || profile->m_securityName.isEmpty()) return false; return true; } bool CSVImporterCore::validateSecurities() { QSet onlySymbols; QSet onlyNames; sortSecurities(onlySymbols, onlyNames, m_mapSymbolName); if (!onlySymbols.isEmpty() || !onlyNames.isEmpty()) return false; return true; } eMyMoney::Transaction::Action CSVImporterCore::processActionTypeField(const InvestmentProfile *profile, const int row, const int col) { if (col == -1) return eMyMoney::Transaction::Action::None; QString type = m_file->m_model->item(row, col)->text(); QList actions; actions << eMyMoney::Transaction::Action::Buy << eMyMoney::Transaction::Action::Sell << // first and second most frequent action eMyMoney::Transaction::Action::ReinvestDividend << eMyMoney::Transaction::Action::CashDividend << // we don't want "reinv-dividend" to be accidentaly caught by "dividend" eMyMoney::Transaction::Action::Interest << eMyMoney::Transaction::Action::Shrsin << eMyMoney::Transaction::Action::Shrsout; foreach (const auto action, actions) { if (profile->m_transactionNames.value(action).contains(type, Qt::CaseInsensitive)) return action; } return eMyMoney::Transaction::Action::None; } validationResultE CSVImporterCore::validateActionType(MyMoneyStatement::Transaction &tr) { validationResultE ret = ValidActionType; QList validActionTypes = createValidActionTypes(tr); if (validActionTypes.isEmpty()) ret = InvalidActionValues; else if (!validActionTypes.contains(tr.m_eAction)) ret = NoActionType; return ret; } bool CSVImporterCore::calculateFee() { auto profile = dynamic_cast(m_profile); if (!profile) return false; if ((profile->m_feeRate.isEmpty() || // check whether feeRate... profile->m_colTypeNum.value(Column::Amount) == -1)) // ...and amount is in place return false; QString decimalSymbol; if (profile->m_decimalSymbol == DecimalSymbol::Auto) { DecimalSymbol detectedSymbol = detectDecimalSymbol(profile->m_colTypeNum.value(Column::Amount), QString()); if (detectedSymbol == DecimalSymbol::Auto) return false; m_file->m_parse->setDecimalSymbol(detectedSymbol); decimalSymbol = m_file->m_parse->decimalSymbol(detectedSymbol); } else decimalSymbol = m_file->m_parse->decimalSymbol(profile->m_decimalSymbol); MyMoneyMoney feePercent(m_file->m_parse->possiblyReplaceSymbol(profile->m_feeRate)); // convert 0.67% ... feePercent /= MyMoneyMoney(100); // ... to 0.0067 if (profile->m_minFee.isEmpty()) profile->m_minFee = QString::number(0.00, 'f', 2); MyMoneyMoney minFee(m_file->m_parse->possiblyReplaceSymbol(profile->m_minFee)); QList items; for (int row = 0; row < profile->m_startLine; ++row) // fill rows above with whitespace for nice effect with markUnwantedRows items.append(new QStandardItem(QString())); for (int row = profile->m_startLine; row <= profile->m_endLine; ++row) { QString txt, numbers; bool ok = false; numbers = txt = m_file->m_model->item(row, profile->m_colTypeNum.value(Column::Amount))->text(); numbers.remove(QRegularExpression(QStringLiteral("[,. ]"))).toInt(&ok); if (!ok) { // check if it's numerical string... items.append(new QStandardItem(QString())); continue; // ...and skip if not (TODO: allow currency symbols and IDs) } if (txt.startsWith(QLatin1Char('('))) { txt.remove(QRegularExpression(QStringLiteral("[()]"))); txt.prepend(QLatin1Char('-')); } txt = m_file->m_parse->possiblyReplaceSymbol(txt); MyMoneyMoney fee(txt); fee *= feePercent; if (fee < minFee) fee = minFee; txt.setNum(fee.toDouble(), 'f', 4); txt.replace(QLatin1Char('.'), decimalSymbol); //make sure decimal symbol is uniform in whole line items.append(new QStandardItem(txt)); } for (int row = profile->m_endLine + 1; row < m_file->m_rowCount; ++row) // fill rows below with whitespace for nice effect with markUnwantedRows items.append(new QStandardItem(QString())); int col = profile->m_colTypeNum.value(Column::Fee, -1); if (col == -1) { // fee column isn't present m_file->m_model->appendColumn(items); ++m_file->m_columnCount; } else if (col >= m_file->m_columnCount) { // column number must have been stored in profile m_file->m_model->appendColumn(items); ++m_file->m_columnCount; } else { // fee column is present and has been recalculated m_file->m_model->removeColumn(m_file->m_columnCount - 1); m_file->m_model->appendColumn(items); } profile->m_colTypeNum[Column::Fee] = m_file->m_columnCount - 1; return true; } DecimalSymbol CSVImporterCore::detectDecimalSymbol(const int col, const QString &exclude) { DecimalSymbol detectedSymbol = DecimalSymbol::Auto; QString pattern; QRegularExpression re("^[\\(+-]?\\d+[\\)]?$"); // matches '0' ; '+12' ; '-345' ; '(6789)' bool dotIsDecimalSeparator = false; bool commaIsDecimalSeparator = false; for (int row = m_profile->m_startLine; row <= m_profile->m_endLine; ++row) { QString txt = m_file->m_model->item(row, col)->text(); if (txt.isEmpty()) // nothing to process, so go to next row continue; int dotPos = txt.lastIndexOf(QLatin1Char('.')); // get last positions of decimal/thousand separator... int commaPos = txt.lastIndexOf(QLatin1Char(',')); // ...to be able to determine which one is the last if (dotPos != -1 && commaPos != -1) { if (dotPos > commaPos && commaIsDecimalSeparator == false) // following case 1,234.56 dotIsDecimalSeparator = true; else if (dotPos < commaPos && dotIsDecimalSeparator == false) // following case 1.234,56 commaIsDecimalSeparator = true; else // following case 1.234,56 and somwhere earlier there was 1,234.56 so unresolvable conflict return detectedSymbol; } else if (dotPos != -1) { // following case 1.23 if (dotIsDecimalSeparator) // it's already know that dotIsDecimalSeparator continue; if (!commaIsDecimalSeparator) // if there is no conflict with comma as decimal separator dotIsDecimalSeparator = true; else { if (txt.count(QLatin1Char('.')) > 1) // following case 1.234.567 so OK continue; else if (txt.length() - 4 == dotPos) // following case 1.234 and somwhere earlier there was 1.234,56 so OK continue; else // following case 1.23 and somwhere earlier there was 1,23 so unresolvable conflict return detectedSymbol; } } else if (commaPos != -1) { // following case 1,23 if (commaIsDecimalSeparator) // it's already know that commaIsDecimalSeparator continue; else if (!dotIsDecimalSeparator) // if there is no conflict with dot as decimal separator commaIsDecimalSeparator = true; else { if (txt.count(QLatin1Char(',')) > 1) // following case 1,234,567 so OK continue; else if (txt.length() - 4 == commaPos) // following case 1,234 and somwhere earlier there was 1,234.56 so OK continue; else // following case 1,23 and somwhere earlier there was 1.23 so unresolvable conflict return detectedSymbol; } } else { // following case 123 if (pattern.isEmpty()) { } txt.remove(QRegularExpression(QLatin1String("[ ") + QRegularExpression::escape(exclude) + QLatin1String("]"))); QRegularExpressionMatch match = re.match(txt); if (match.hasMatch()) // if string is pure numerical then go forward... continue; else // ...if not then it's non-numerical garbage return detectedSymbol; } } if (dotIsDecimalSeparator) detectedSymbol = DecimalSymbol::Dot; else if (commaIsDecimalSeparator) detectedSymbol = DecimalSymbol::Comma; else { // whole column was empty, but we don't want to fail so take OS's decimal symbol if (QLocale().decimalPoint() == QLatin1Char('.')) detectedSymbol = DecimalSymbol::Dot; else detectedSymbol = DecimalSymbol::Comma; } return detectedSymbol; } int CSVImporterCore::detectDecimalSymbols(const QList &columns) { int ret = -2; // get list of used currencies to remove them from col QList accounts; MyMoneyFile *file = MyMoneyFile::instance(); file->accountList(accounts); QList accountTypes; accountTypes << eMyMoney::Account::Type::Checkings << eMyMoney::Account::Type::Savings << eMyMoney::Account::Type::Liability << eMyMoney::Account::Type::Checkings << eMyMoney::Account::Type::Savings << eMyMoney::Account::Type::Cash << eMyMoney::Account::Type::CreditCard << eMyMoney::Account::Type::Loan << eMyMoney::Account::Type::Asset << eMyMoney::Account::Type::Liability; QSet currencySymbols; foreach (const auto account, accounts) { if (accountTypes.contains(account.accountType())) { // account must actually have currency property currencySymbols.insert(account.currencyId()); // add currency id currencySymbols.insert(file->currency(account.currencyId()).tradingSymbol()); // add currency symbol } } QString filteredCurrencies = QStringList(currencySymbols.values()).join(""); QString pattern = QString::fromLatin1("%1%2").arg(QLocale().currencySymbol()).arg(filteredCurrencies); foreach (const auto column, columns) { DecimalSymbol detectedSymbol = detectDecimalSymbol(column, pattern); if (detectedSymbol == DecimalSymbol::Auto) { ret = column; return ret; } m_decimalSymbolIndexMap.insert(column, detectedSymbol); } return ret; } QList CSVImporterCore::findAccounts(const QList &accountTypes, const QString &statementHeader) { MyMoneyFile* file = MyMoneyFile::instance(); QList accountList; file->accountList(accountList); QList filteredTypes; QList filteredAccounts; - QList::iterator account; QRegularExpression filterOutChars(QStringLiteral("[-., ]")); foreach (const auto account, accountList) { if (accountTypes.contains(account.accountType()) && !(account).isClosed()) filteredTypes.append(account); } // filter out accounts whose names aren't in statements header foreach (const auto account, filteredTypes) { QString txt = account.name(); txt.remove(filterOutChars); if (txt.isEmpty() || txt.length() < 3) continue; if (statementHeader.contains(txt, Qt::CaseInsensitive)) filteredAccounts.append(account); } // if filtering returned more results, filter out accounts whose numbers aren't in statements header if (filteredAccounts.count() > 1) { - for (account = filteredAccounts.begin(); account != filteredAccounts.end();) { + for (auto account = filteredAccounts.begin(); account != filteredAccounts.end();) { QString txt = (*account).number(); txt.remove(filterOutChars); if (txt.isEmpty() || txt.length() < 3) { ++account; continue; } if (statementHeader.contains(txt, Qt::CaseInsensitive)) ++account; else account = filteredAccounts.erase(account); } } // if filtering returned more results, filter out accounts whose numbers are the shortest if (filteredAccounts.count() > 1) { for (auto i = 1; i < filteredAccounts.count();) { auto firstAccNumber = filteredAccounts.at(0).number(); auto secondAccNumber = filteredAccounts.at(i).number(); if (firstAccNumber.length() > secondAccNumber.length()) { filteredAccounts.removeAt(i); } else if (firstAccNumber.length() < secondAccNumber.length()) { filteredAccounts.removeAt(0); --i; } else { ++i; } } } // if filtering returned more results, filter out accounts whose names are the shortest if (filteredAccounts.count() > 1) { for (auto i = 1; i < filteredAccounts.count();) { auto firstAccName = filteredAccounts.at(0).name(); auto secondAccName = filteredAccounts.at(i).name(); if (firstAccName.length() > secondAccName.length()) { filteredAccounts.removeAt(i); } else if (firstAccName.length() < secondAccName.length()) { filteredAccounts.removeAt(0); --i; } else { ++i; } } } // if filtering by name and number didn't return nothing, then try filtering by number only if (filteredAccounts.isEmpty()) { foreach (const auto account, filteredTypes) { QString txt = account.number(); txt.remove(filterOutChars); if (txt.isEmpty() || txt.length() < 3) continue; if (statementHeader.contains(txt, Qt::CaseInsensitive)) filteredAccounts.append(account); } } return filteredAccounts; } bool CSVImporterCore::detectAccount(MyMoneyStatement &st) { QString statementHeader; for (int row = 0; row < m_profile->m_startLine; ++row) // concatenate header for better search for (int col = 0; col < m_file->m_columnCount; ++col) statementHeader.append(m_file->m_model->item(row, col)->text()); statementHeader.remove(QRegularExpression(QStringLiteral("[-., ]"))); QList accounts; QList accountTypes; switch(m_profile->type()) { default: case Profile::Banking: accountTypes << eMyMoney::Account::Type::Checkings << eMyMoney::Account::Type::Savings << eMyMoney::Account::Type::Liability << eMyMoney::Account::Type::Checkings << eMyMoney::Account::Type::Savings << eMyMoney::Account::Type::Cash << eMyMoney::Account::Type::CreditCard << eMyMoney::Account::Type::Loan << eMyMoney::Account::Type::Asset << eMyMoney::Account::Type::Liability; accounts = findAccounts(accountTypes, statementHeader); break; case Profile::Investment: accountTypes << eMyMoney::Account::Type::Investment; // take investment accounts... accounts = findAccounts(accountTypes, statementHeader); //...and search them in statement header break; } if (accounts.count() == 1) { // set account in statement, if it was the only one match st.m_strAccountName = accounts.first().name(); st.m_strAccountNumber = accounts.first().number(); st.m_accountId = accounts.first().id(); switch (accounts.first().accountType()) { case eMyMoney::Account::Type::Checkings: st.m_eType = eMyMoney::Statement::Type::Checkings; break; case eMyMoney::Account::Type::Savings: st.m_eType = eMyMoney::Statement::Type::Savings; break; case eMyMoney::Account::Type::Investment: st.m_eType = eMyMoney::Statement::Type::Investment; break; case eMyMoney::Account::Type::CreditCard: st.m_eType = eMyMoney::Statement::Type::CreditCard; break; default: st.m_eType = eMyMoney::Statement::Type::None; } return true; } return false; } bool CSVImporterCore::processBankRow(MyMoneyStatement &st, const BankingProfile *profile, const int row) { MyMoneyStatement::Transaction tr; QString memo; QString txt; // process number field if (profile->m_colTypeNum.value(Column::Number, -1) != -1) tr.m_strNumber = txt; // process date field int col = profile->m_colTypeNum.value(Column::Date, -1); tr.m_datePosted = processDateField(row, col); if (tr.m_datePosted == QDate()) return false; // process payee field col = profile->m_colTypeNum.value(Column::Payee, -1); if (col != -1) tr.m_strPayee = m_file->m_model->item(row, col)->text(); // process memo field col = profile->m_colTypeNum.value(Column::Memo, -1); if (col != -1) memo.append(m_file->m_model->item(row, col)->text()); for (int i = 0; i < profile->m_memoColList.count(); ++i) { if (profile->m_memoColList.at(i) != col) { if (!memo.isEmpty()) memo.append(QLatin1Char('\n')); if (profile->m_memoColList.at(i) < m_file->m_columnCount) memo.append(m_file->m_model->item(row, profile->m_memoColList.at(i))->text()); } } tr.m_strMemo = memo; // process amount field col = profile->m_colTypeNum.value(Column::Amount, -1); tr.m_amount = processAmountField(profile, row, col); if (col != -1 && profile->m_oppositeSigns) // change signs to opposite if requested by user tr.m_amount *= MyMoneyMoney(-1); // process credit/debit field if (profile->m_colTypeNum.value(Column::Credit, -1) != -1 && profile->m_colTypeNum.value(Column::Debit, -1) != -1) { QString credit = m_file->m_model->item(row, profile->m_colTypeNum.value(Column::Credit))->text(); QString debit = m_file->m_model->item(row, profile->m_colTypeNum.value(Column::Debit))->text(); tr.m_amount = processCreditDebit(credit, debit); if (!credit.isEmpty() && !debit.isEmpty()) return false; } MyMoneyStatement::Split s1; s1.m_amount = tr.m_amount; s1.m_strMemo = tr.m_strMemo; MyMoneyStatement::Split s2 = s1; s2.m_reconcile = tr.m_reconcile; s2.m_amount = -s1.m_amount; // process category field col = profile->m_colTypeNum.value(Column::Category, -1); if (col != -1) { txt = m_file->m_model->item(row, col)->text(); QString accountId = MyMoneyFile::instance()->checkCategory(txt, s1.m_amount, s2.m_amount); if (!accountId.isEmpty()) { s2.m_accountId = accountId; s2.m_strCategoryName = txt; tr.m_listSplits.append(s2); } } // calculate hash txt.clear(); for (int i = 0; i < m_file->m_columnCount; ++i) txt.append(m_file->m_model->item(row, i)->text()); QString hashBase = QString::fromLatin1("%1-%2") .arg(tr.m_datePosted.toString(Qt::ISODate)) .arg(MyMoneyTransaction::hash(txt)); QString hash; for (uchar idx = 0; idx < 0xFF; ++idx) { // assuming threre will be no more than 256 transactions with the same hashBase hash = QString::fromLatin1("%1-%2").arg(hashBase).arg(idx); QSet::const_iterator it = m_hashSet.constFind(hash); if (it == m_hashSet.constEnd()) break; } m_hashSet.insert(hash); tr.m_strBankID = hash; st.m_listTransactions.append(tr); // Add the MyMoneyStatement::Transaction to the statement return true; } bool CSVImporterCore::processInvestRow(MyMoneyStatement &st, const InvestmentProfile *profile, const int row) { MyMoneyStatement::Transaction tr; QString memo; QString txt; // process date field int col = profile->m_colTypeNum.value(Column::Date, -1); tr.m_datePosted = processDateField(row, col); if (tr.m_datePosted == QDate()) return false; // process quantity field col = profile->m_colTypeNum.value(Column::Quantity, -1); tr.m_shares = processQuantityField(profile, row, col); // process price field col = profile->m_colTypeNum.value(Column::Price, -1); tr.m_price = processPriceField(profile, row, col); // process amount field col = profile->m_colTypeNum.value(Column::Amount, -1); tr.m_amount = processAmountField(profile, row, col); // process type field col = profile->m_colTypeNum.value(Column::Type, -1); tr.m_eAction = processActionTypeField(profile, row, col); if (!m_isActionTypeValidated && col != -1 && // if action type wasn't validated in wizard then... validateActionType(tr) != ValidActionType) // ...check if price, amount, quantity is appropriate return false; // process fee field col = profile->m_colTypeNum.value(Column::Fee, -1); if (col != -1) { if (profile->m_decimalSymbol == DecimalSymbol::Auto) { DecimalSymbol decimalSymbol = m_decimalSymbolIndexMap.value(col); m_file->m_parse->setDecimalSymbol(decimalSymbol); } txt = m_file->m_model->item(row, col)->text(); if (txt.startsWith(QLatin1Char('('))) // check if brackets notation is used for negative numbers txt.remove(QRegularExpression(QStringLiteral("[()]"))); if (txt.isEmpty()) tr.m_fees = MyMoneyMoney(); else { MyMoneyMoney fee(m_file->m_parse->possiblyReplaceSymbol(txt)); if (profile->m_feeIsPercentage && profile->m_feeRate.isEmpty()) // fee is percent fee *= tr.m_amount / MyMoneyMoney(100); // as percentage fee.abs(); tr.m_fees = fee; } } // process symbol and name field col = profile->m_colTypeNum.value(Column::Symbol, -1); if (col != -1) tr.m_strSymbol = m_file->m_model->item(row, col)->text(); col = profile->m_colTypeNum.value(Column::Name, -1); if (col != -1 && tr.m_strSymbol.isEmpty()) { // case in which symbol field is empty txt = m_file->m_model->item(row, col)->text(); tr.m_strSymbol = m_mapSymbolName.key(txt); // it's all about getting the right symbol } else if (!profile->m_securitySymbol.isEmpty()) tr.m_strSymbol = profile->m_securitySymbol; else if (tr.m_strSymbol.isEmpty()) return false; tr.m_strSecurity = m_mapSymbolName.value(tr.m_strSymbol); // take name from prepared names to avoid potential name mismatch // process memo field col = profile->m_colTypeNum.value(Column::Memo, -1); if (col != -1) memo.append(m_file->m_model->item(row, col)->text()); for (int i = 0; i < profile->m_memoColList.count(); ++i) { if (profile->m_memoColList.at(i) != col) { if (!memo.isEmpty()) memo.append(QLatin1Char('\n')); if (profile->m_memoColList.at(i) < m_file->m_columnCount) memo.append(m_file->m_model->item(row, profile->m_memoColList.at(i))->text()); } } tr.m_strMemo = memo; tr.m_strInterestCategory.clear(); // no special category tr.m_strBrokerageAccount.clear(); // no brokerage account auto-detection MyMoneyStatement::Split s1; s1.m_amount = tr.m_amount; s1.m_strMemo = tr.m_strMemo; MyMoneyStatement::Split s2 = s1; s2.m_amount = -s1.m_amount; s2.m_accountId = MyMoneyFile::instance()->checkCategory(tr.m_strInterestCategory, s1.m_amount, s2.m_amount); // deduct fees from amount if (tr.m_eAction == eMyMoney::Transaction::Action::CashDividend || tr.m_eAction == eMyMoney::Transaction::Action::Sell || tr.m_eAction == eMyMoney::Transaction::Action::Interest) tr.m_amount -= tr.m_fees; else if (tr.m_eAction == eMyMoney::Transaction::Action::Buy) { if (tr.m_amount.isPositive()) tr.m_amount = -tr.m_amount; //if broker doesn't use minus sings for buy transactions, set it manually here tr.m_amount -= tr.m_fees; } else if (tr.m_eAction == eMyMoney::Transaction::Action::None) tr.m_listSplits.append(s2); st.m_listTransactions.append(tr); // Add the MyMoneyStatement::Transaction to the statement return true; } bool CSVImporterCore::processPriceRow(MyMoneyStatement &st, const PricesProfile *profile, const int row) { MyMoneyStatement::Price pr; // process date field int col = profile->m_colTypeNum.value(Column::Date, -1); pr.m_date = processDateField(row, col); if (pr.m_date == QDate()) return false; // process price field col = profile->m_colTypeNum.value(Column::Price, -1); pr.m_amount = processPriceField(profile, row, col); switch (profile->type()) { case Profile::CurrencyPrices: if (profile->m_securitySymbol.isEmpty() || profile->m_currencySymbol.isEmpty()) return false; pr.m_strSecurity = profile->m_securitySymbol; pr.m_strCurrency = profile->m_currencySymbol; break; case Profile::StockPrices: if (profile->m_securityName.isEmpty()) return false; pr.m_strSecurity = profile->m_securityName; break; default: return false; } pr.m_sourceName = profile->m_profileName; st.m_listPrices.append(pr); // Add price to the statement return true; } QDate CSVImporterCore::processDateField(const int row, const int col) { QDate date; if (col != -1) { QString txt = m_file->m_model->item(row, col)->text(); date = m_convertDate->convertDate(txt); // Date column } return date; } MyMoneyMoney CSVImporterCore::processCreditDebit(QString &credit, QString &debit) { MyMoneyMoney amount; if (m_profile->m_decimalSymbol == DecimalSymbol::Auto) setupFieldDecimalSymbol(m_profile->m_colTypeNum.value(Column::Credit)); if (credit.startsWith(QLatin1Char('('))) { // check if brackets notation is used for negative numbers credit.remove(QRegularExpression(QStringLiteral("[()]"))); credit.prepend(QLatin1Char('-')); } if (debit.startsWith(QLatin1Char('('))) { // check if brackets notation is used for negative numbers debit.remove(QRegularExpression(QStringLiteral("[()]"))); debit.prepend(QLatin1Char('-')); } if (!credit.isEmpty() && !debit.isEmpty()) { // we do not expect both fields to be non-zero if (MyMoneyMoney(credit).isZero()) credit = QString(); if (MyMoneyMoney(debit).isZero()) debit = QString(); } if (!debit.startsWith(QLatin1Char('-')) && !debit.isEmpty()) // ensure debit field is negative debit.prepend(QLatin1Char('-')); if (!credit.isEmpty() && debit.isEmpty()) amount = MyMoneyMoney(m_file->m_parse->possiblyReplaceSymbol(credit)); else if (credit.isEmpty() && !debit.isEmpty()) amount = MyMoneyMoney(m_file->m_parse->possiblyReplaceSymbol(debit)); else if (!credit.isEmpty() && !debit.isEmpty()) { // both fields are non-empty and non-zero so let user decide return amount; } else amount = MyMoneyMoney(); // both fields are empty and zero so set amount to zero return amount; } MyMoneyMoney CSVImporterCore::processQuantityField(const CSVProfile *profile, const int row, const int col) { MyMoneyMoney shares; if (col != -1) { if (profile->m_decimalSymbol == DecimalSymbol::Auto) setupFieldDecimalSymbol(col); QString txt = m_file->m_model->item(row, col)->text(); txt.remove(QRegularExpression(QStringLiteral("-+"))); // remove unwanted sings in quantity if (!txt.isEmpty()) shares = MyMoneyMoney(m_file->m_parse->possiblyReplaceSymbol(txt)); } return shares; } MyMoneyMoney CSVImporterCore::processAmountField(const CSVProfile *profile, const int row, const int col) { MyMoneyMoney amount; if (col != -1) { if (profile->m_decimalSymbol == DecimalSymbol::Auto) setupFieldDecimalSymbol(col); QString txt = m_file->m_model->item(row, col)->text(); if (txt.startsWith(QLatin1Char('('))) { // check if brackets notation is used for negative numbers txt.remove(QRegularExpression(QStringLiteral("[()]"))); txt.prepend(QLatin1Char('-')); } if (!txt.isEmpty()) amount = MyMoneyMoney(m_file->m_parse->possiblyReplaceSymbol(txt)); } return amount; } MyMoneyMoney CSVImporterCore::processPriceField(const InvestmentProfile *profile, const int row, const int col) { MyMoneyMoney price; if (col != -1) { if (profile->m_decimalSymbol == DecimalSymbol::Auto) setupFieldDecimalSymbol(col); QString txt = m_file->m_model->item(row, col)->text(); if (!txt.isEmpty()) { price = MyMoneyMoney(m_file->m_parse->possiblyReplaceSymbol(txt)); price *= m_priceFractions.at(profile->m_priceFraction); } } return price; } MyMoneyMoney CSVImporterCore::processPriceField(const PricesProfile *profile, const int row, const int col) { MyMoneyMoney price; if (col != -1) { if (profile->m_decimalSymbol == DecimalSymbol::Auto) setupFieldDecimalSymbol(col); QString txt = m_file->m_model->item(row, col)->text(); if (!txt.isEmpty()) { price = MyMoneyMoney(m_file->m_parse->possiblyReplaceSymbol(txt)); price *= m_priceFractions.at(profile->m_priceFraction); } } return price; } QList CSVImporterCore::createValidActionTypes(MyMoneyStatement::Transaction &tr) { QList validActionTypes; if (tr.m_shares.isPositive() && tr.m_price.isPositive() && !tr.m_amount.isZero()) validActionTypes << eMyMoney::Transaction::Action::ReinvestDividend << eMyMoney::Transaction::Action::Buy << eMyMoney::Transaction::Action::Sell; else if (tr.m_shares.isZero() && tr.m_price.isZero() && !tr.m_amount.isZero()) validActionTypes << eMyMoney::Transaction::Action::CashDividend << eMyMoney::Transaction::Action::Interest; else if (tr.m_shares.isPositive() && tr.m_price.isZero() && tr.m_amount.isZero()) validActionTypes << eMyMoney::Transaction::Action::Shrsin << eMyMoney::Transaction::Action::Shrsout; return validActionTypes; } bool CSVImporterCore::sortSecurities(QSet& onlySymbols, QSet& onlyNames, QMap& mapSymbolName) { QList securityList = MyMoneyFile::instance()->securityList(); int symbolCol = m_profile->m_colTypeNum.value(Column::Symbol, -1); int nameCol = m_profile->m_colTypeNum.value(Column::Name, -1); // sort by availability of symbol and name for (int row = m_profile->m_startLine; row <= m_profile->m_endLine; ++row) { QString symbol; QString name; if (symbolCol != -1) symbol = m_file->m_model->item(row, symbolCol)->text().trimmed(); if (nameCol != -1) name = m_file->m_model->item(row, nameCol)->text().trimmed(); if (!symbol.isEmpty() && !name.isEmpty()) mapSymbolName.insert(symbol, name); else if (!symbol.isEmpty()) onlySymbols.insert(symbol); else if (!name.isEmpty()) onlyNames.insert(name); else return false; } // try to find names for symbols for (QSet::iterator symbol = onlySymbols.begin(); symbol != onlySymbols.end();) { QList filteredSecurities; foreach (const auto sec, securityList) { if ((*symbol).compare(sec.tradingSymbol(), Qt::CaseInsensitive) == 0) filteredSecurities.append(sec); // gather all securities that by matched by symbol } if (filteredSecurities.count() == 1) { // single security matched by the symbol so... mapSymbolName.insert(*symbol, filteredSecurities.first().name()); symbol = onlySymbols.erase(symbol); // ...it's no longer unknown } else if (!filteredSecurities.isEmpty()) { // multiple securities matched by the symbol // TODO: Ask user which security should we match to mapSymbolName.insert(*symbol, filteredSecurities.first().name()); symbol = onlySymbols.erase(symbol); } else // no security matched, so leave it as unknown ++symbol; } // try to find symbols for names for (QSet::iterator name = onlyNames.begin(); name != onlyNames.end();) { QList filteredSecurities; foreach (const auto sec, securityList) { if ((*name).compare(sec.name(), Qt::CaseInsensitive) == 0) filteredSecurities.append(sec); // gather all securities that by matched by name } if (filteredSecurities.count() == 1) { // single security matched by the name so... mapSymbolName.insert(filteredSecurities.first().tradingSymbol(), *name); name = onlyNames.erase(name); // ...it's no longer unknown } else if (!filteredSecurities.isEmpty()) { // multiple securities matched by the name // TODO: Ask user which security should we match to mapSymbolName.insert(filteredSecurities.first().tradingSymbol(), *name); name = onlySymbols.erase(name); } else // no security matched, so leave it as unknown ++name; } return true; } void CSVImporterCore::setupFieldDecimalSymbol(int col) { m_file->m_parse->setDecimalSymbol(m_decimalSymbolIndexMap.value(col)); } QList CSVImporterCore::getNumericalColumns() { QList columns; switch(m_profile->type()) { case Profile::Banking: if (m_profile->m_colTypeNum.value(Column::Amount, -1) != -1) { columns << m_profile->m_colTypeNum.value(Column::Amount); } else { columns << m_profile->m_colTypeNum.value(Column::Debit); columns << m_profile->m_colTypeNum.value(Column::Credit); } break; case Profile::Investment: columns << m_profile->m_colTypeNum.value(Column::Amount); columns << m_profile->m_colTypeNum.value(Column::Price); columns << m_profile->m_colTypeNum.value(Column::Quantity); if (m_profile->m_colTypeNum.value(Column::Fee, -1) != -1) columns << m_profile->m_colTypeNum.value(Column::Fee); break; case Profile::CurrencyPrices: case Profile::StockPrices: columns << m_profile->m_colTypeNum.value(Column::Price); break; default: break; } return columns; } bool CSVImporterCore::createStatement(MyMoneyStatement &st) { switch (m_profile->type()) { case Profile::Banking: { if (!st.m_listTransactions.isEmpty()) // don't create statement if there is one return true; st.m_eType = eMyMoney::Statement::Type::None; if (m_autodetect.value(AutoAccountBank)) detectAccount(st); m_hashSet.clear(); BankingProfile *profile = dynamic_cast(m_profile); for (int row = m_profile->m_startLine; row <= m_profile->m_endLine; ++row) if (!processBankRow(st, profile, row)) { // parse fields st = MyMoneyStatement(); return false; } return true; break; } case Profile::Investment: { if (!st.m_listTransactions.isEmpty()) // don't create statement if there is one return true; st.m_eType = eMyMoney::Statement::Type::Investment; if (m_autodetect.value(AutoAccountInvest)) detectAccount(st); auto profile = dynamic_cast(m_profile); if ((m_profile->m_colTypeNum.value(Column::Fee, -1) == -1 || m_profile->m_colTypeNum.value(Column::Fee, -1) >= m_file->m_columnCount) && profile && !profile->m_feeRate.isEmpty()) // fee column has not been calculated so do it now calculateFee(); if (profile) { for (int row = m_profile->m_startLine; row <= m_profile->m_endLine; ++row) if (!processInvestRow(st, profile, row)) { // parse fields st = MyMoneyStatement(); return false; } } for (QMap::const_iterator it = m_mapSymbolName.cbegin(); it != m_mapSymbolName.cend(); ++it) { MyMoneyStatement::Security security; security.m_strSymbol = it.key(); security.m_strName = it.value(); st.m_listSecurities.append(security); } return true; break; } default: case Profile::CurrencyPrices: case Profile::StockPrices: { if (!st.m_listPrices.isEmpty()) // don't create statement if there is one return true; st.m_eType = eMyMoney::Statement::Type::None; if (auto profile = dynamic_cast(m_profile)) { for (int row = m_profile->m_startLine; row <= m_profile->m_endLine; ++row) if (!processPriceRow(st, profile, row)) { // parse fields st = MyMoneyStatement(); return false; } } for (QMap::const_iterator it = m_mapSymbolName.cbegin(); it != m_mapSymbolName.cend(); ++it) { MyMoneyStatement::Security security; security.m_strSymbol = it.key(); security.m_strName = it.value(); st.m_listSecurities.append(security); } return true; } } return true; } void CSVProfile::readSettings(const KConfigGroup &profilesGroup) { m_lastUsedDirectory = profilesGroup.readEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfDirectory), QString()); m_startLine = profilesGroup.readEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfStartLine), 0); m_trailerLines = profilesGroup.readEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfTrailerLines), 0); m_encodingMIBEnum = profilesGroup.readEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfEncoding), 106 /* UTF-8 */); m_dateFormat = static_cast(profilesGroup.readEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfDateFormat), (int)DateFormat::YearMonthDay)); m_textDelimiter = static_cast(profilesGroup.readEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfTextDelimiter), (int)TextDelimiter::DoubleQuote)); m_fieldDelimiter = static_cast(profilesGroup.readEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfFieldDelimiter), (int)FieldDelimiter::Auto)); m_decimalSymbol = static_cast(profilesGroup.readEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfDecimalSymbol), (int)DecimalSymbol::Auto)); initColNumType(); } void CSVProfile::writeSettings(KConfigGroup &profilesGroup) { QFileInfo fileInfo (m_lastUsedDirectory); if (fileInfo.isFile()) m_lastUsedDirectory = fileInfo.absolutePath(); if (m_lastUsedDirectory.startsWith(QDir::homePath())) // replace /home/user with ~/ for brevity m_lastUsedDirectory.replace(0, QDir::homePath().length(), QLatin1Char('~')); profilesGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfDirectory), m_lastUsedDirectory); profilesGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfEncoding), m_encodingMIBEnum); profilesGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfDateFormat), (int)m_dateFormat); profilesGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfFieldDelimiter), (int)m_fieldDelimiter); profilesGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfTextDelimiter), (int)m_textDelimiter); profilesGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfDecimalSymbol), (int)m_decimalSymbol); profilesGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfStartLine), m_startLine); profilesGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfTrailerLines), m_trailerLines); } bool BankingProfile::readSettings(const KSharedConfigPtr &config) { bool exists = true; KConfigGroup profilesGroup(config, CSVImporterCore::m_profileConfPrefix.value(type()) + QLatin1Char('-') + m_profileName); if (!profilesGroup.exists()) exists = false; m_colTypeNum[Column::Payee] = profilesGroup.readEntry(CSVImporterCore::m_colTypeConfName.value(Column::Payee), -1); m_colTypeNum[Column::Number] = profilesGroup.readEntry(CSVImporterCore::m_colTypeConfName.value(Column::Number), -1); m_colTypeNum[Column::Amount] = profilesGroup.readEntry(CSVImporterCore::m_colTypeConfName.value(Column::Amount), -1); m_colTypeNum[Column::Debit] = profilesGroup.readEntry(CSVImporterCore::m_colTypeConfName.value(Column::Debit), -1); m_colTypeNum[Column::Credit] = profilesGroup.readEntry(CSVImporterCore::m_colTypeConfName.value(Column::Credit), -1); m_colTypeNum[Column::Date] = profilesGroup.readEntry(CSVImporterCore::m_colTypeConfName.value(Column::Date), -1); m_colTypeNum[Column::Category] = profilesGroup.readEntry(CSVImporterCore::m_colTypeConfName.value(Column::Category), -1); m_colTypeNum[Column::Memo] = -1; // initialize, otherwise random data may go here m_oppositeSigns = profilesGroup.readEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfOppositeSigns), 0); m_memoColList = profilesGroup.readEntry(CSVImporterCore::m_colTypeConfName.value(Column::Memo), QList()); CSVProfile::readSettings(profilesGroup); return exists; } void BankingProfile::writeSettings(const KSharedConfigPtr &config) { KConfigGroup profilesGroup(config, CSVImporterCore::m_profileConfPrefix.value(type()) + QLatin1Char('-') + m_profileName); CSVProfile::writeSettings(profilesGroup); profilesGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfOppositeSigns), m_oppositeSigns); profilesGroup.writeEntry(CSVImporterCore::m_colTypeConfName.value(Column::Payee), m_colTypeNum.value(Column::Payee)); profilesGroup.writeEntry(CSVImporterCore::m_colTypeConfName.value(Column::Number), m_colTypeNum.value(Column::Number)); profilesGroup.writeEntry(CSVImporterCore::m_colTypeConfName.value(Column::Amount), m_colTypeNum.value(Column::Amount)); profilesGroup.writeEntry(CSVImporterCore::m_colTypeConfName.value(Column::Debit), m_colTypeNum.value(Column::Debit)); profilesGroup.writeEntry(CSVImporterCore::m_colTypeConfName.value(Column::Credit), m_colTypeNum.value(Column::Credit)); profilesGroup.writeEntry(CSVImporterCore::m_colTypeConfName.value(Column::Date), m_colTypeNum.value(Column::Date)); profilesGroup.writeEntry(CSVImporterCore::m_colTypeConfName.value(Column::Category), m_colTypeNum.value(Column::Category)); profilesGroup.writeEntry(CSVImporterCore::m_colTypeConfName.value(Column::Memo), m_memoColList); profilesGroup.config()->sync(); } bool InvestmentProfile::readSettings(const KSharedConfigPtr &config) { bool exists = true; KConfigGroup profilesGroup(config, CSVImporterCore::m_profileConfPrefix.value(type()) + QLatin1Char('-') + m_profileName); if (!profilesGroup.exists()) exists = false; m_transactionNames[eMyMoney::Transaction::Action::Buy] = profilesGroup.readEntry(CSVImporterCore::m_transactionConfName.value(eMyMoney::Transaction::Action::Buy), QString(i18nc("Type of operation as in financial statement", "buy")).split(',', QString::SkipEmptyParts)); m_transactionNames[eMyMoney::Transaction::Action::Sell] = profilesGroup.readEntry(CSVImporterCore::m_transactionConfName.value(eMyMoney::Transaction::Action::Sell), QString(i18nc("Type of operation as in financial statement", "sell,repurchase")).split(',', QString::SkipEmptyParts)); m_transactionNames[eMyMoney::Transaction::Action::ReinvestDividend] = profilesGroup.readEntry(CSVImporterCore::m_transactionConfName.value(eMyMoney::Transaction::Action::ReinvestDividend), QString(i18nc("Type of operation as in financial statement", "reinvest,reinv,re-inv")).split(',', QString::SkipEmptyParts)); m_transactionNames[eMyMoney::Transaction::Action::CashDividend] = profilesGroup.readEntry(CSVImporterCore::m_transactionConfName.value(eMyMoney::Transaction::Action::CashDividend), QString(i18nc("Type of operation as in financial statement", "dividend")).split(',', QString::SkipEmptyParts)); m_transactionNames[eMyMoney::Transaction::Action::Interest] = profilesGroup.readEntry(CSVImporterCore::m_transactionConfName.value(eMyMoney::Transaction::Action::Interest), QString(i18nc("Type of operation as in financial statement", "interest,income")).split(',', QString::SkipEmptyParts)); m_transactionNames[eMyMoney::Transaction::Action::Shrsin] = profilesGroup.readEntry(CSVImporterCore::m_transactionConfName.value(eMyMoney::Transaction::Action::Shrsin), QString(i18nc("Type of operation as in financial statement", "add,stock dividend,divd reinv,transfer in,re-registration in,journal entry")).split(',', QString::SkipEmptyParts)); m_transactionNames[eMyMoney::Transaction::Action::Shrsout] = profilesGroup.readEntry(CSVImporterCore::m_transactionConfName.value(eMyMoney::Transaction::Action::Shrsout), QString(i18nc("Type of operation as in financial statement", "remove")).split(',', QString::SkipEmptyParts)); m_colTypeNum[Column::Date] = profilesGroup.readEntry(CSVImporterCore::m_colTypeConfName.value(Column::Date), -1); m_colTypeNum[Column::Type] = profilesGroup.readEntry(CSVImporterCore::m_colTypeConfName.value(Column::Type), -1); //use for type col. m_colTypeNum[Column::Price] = profilesGroup.readEntry(CSVImporterCore::m_colTypeConfName.value(Column::Price), -1); m_colTypeNum[Column::Quantity] = profilesGroup.readEntry(CSVImporterCore::m_colTypeConfName.value(Column::Quantity), -1); m_colTypeNum[Column::Amount] = profilesGroup.readEntry(CSVImporterCore::m_colTypeConfName.value(Column::Amount), -1); m_colTypeNum[Column::Name] = profilesGroup.readEntry(CSVImporterCore::m_colTypeConfName.value(Column::Name), -1); m_colTypeNum[Column::Fee] = profilesGroup.readEntry(CSVImporterCore::m_colTypeConfName.value(Column::Fee), -1); m_colTypeNum[Column::Symbol] = profilesGroup.readEntry(CSVImporterCore::m_colTypeConfName.value(Column::Symbol), -1); m_colTypeNum[Column::Memo] = -1; // initialize, otherwise random data may go here m_feeIsPercentage = profilesGroup.readEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfFeeIsPercentage), false); m_feeRate = profilesGroup.readEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfFeeRate), QString()); m_minFee = profilesGroup.readEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfMinFee), QString()); m_memoColList = profilesGroup.readEntry(CSVImporterCore::m_colTypeConfName.value(Column::Memo), QList()); m_securityName = profilesGroup.readEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfSecurityName), QString()); m_securitySymbol = profilesGroup.readEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfSecuritySymbol), QString()); m_dontAsk = profilesGroup.readEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfDontAsk), 0); m_priceFraction = profilesGroup.readEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfPriceFraction), 2); CSVProfile::readSettings(profilesGroup); return exists; } void InvestmentProfile::writeSettings(const KSharedConfigPtr &config) { KConfigGroup profilesGroup(config, CSVImporterCore::m_profileConfPrefix.value(type()) + QLatin1Char('-') + m_profileName); CSVProfile::writeSettings(profilesGroup); profilesGroup.writeEntry(CSVImporterCore::m_transactionConfName.value(eMyMoney::Transaction::Action::Buy), m_transactionNames.value(eMyMoney::Transaction::Action::Buy)); profilesGroup.writeEntry(CSVImporterCore::m_transactionConfName.value(eMyMoney::Transaction::Action::Sell), m_transactionNames.value(eMyMoney::Transaction::Action::Sell)); profilesGroup.writeEntry(CSVImporterCore::m_transactionConfName.value(eMyMoney::Transaction::Action::ReinvestDividend), m_transactionNames.value(eMyMoney::Transaction::Action::ReinvestDividend)); profilesGroup.writeEntry(CSVImporterCore::m_transactionConfName.value(eMyMoney::Transaction::Action::CashDividend), m_transactionNames.value(eMyMoney::Transaction::Action::CashDividend)); profilesGroup.writeEntry(CSVImporterCore::m_transactionConfName.value(eMyMoney::Transaction::Action::Interest), m_transactionNames.value(eMyMoney::Transaction::Action::Interest)); profilesGroup.writeEntry(CSVImporterCore::m_transactionConfName.value(eMyMoney::Transaction::Action::Shrsin), m_transactionNames.value(eMyMoney::Transaction::Action::Shrsin)); profilesGroup.writeEntry(CSVImporterCore::m_transactionConfName.value(eMyMoney::Transaction::Action::Shrsout), m_transactionNames.value(eMyMoney::Transaction::Action::Shrsout)); profilesGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfPriceFraction), m_priceFraction); profilesGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfFeeIsPercentage), m_feeIsPercentage); profilesGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfFeeRate), m_feeRate); profilesGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfMinFee), m_minFee); profilesGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfSecurityName), m_securityName); profilesGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfSecuritySymbol), m_securitySymbol); profilesGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfDontAsk), m_dontAsk); profilesGroup.writeEntry(CSVImporterCore::m_colTypeConfName.value(Column::Date), m_colTypeNum.value(Column::Date)); profilesGroup.writeEntry(CSVImporterCore::m_colTypeConfName.value(Column::Type), m_colTypeNum.value(Column::Type)); profilesGroup.writeEntry(CSVImporterCore::m_colTypeConfName.value(Column::Quantity), m_colTypeNum.value(Column::Quantity)); profilesGroup.writeEntry(CSVImporterCore::m_colTypeConfName.value(Column::Amount), m_colTypeNum.value(Column::Amount)); profilesGroup.writeEntry(CSVImporterCore::m_colTypeConfName.value(Column::Price), m_colTypeNum.value(Column::Price)); profilesGroup.writeEntry(CSVImporterCore::m_colTypeConfName.value(Column::Symbol), m_colTypeNum.value(Column::Symbol)); profilesGroup.writeEntry(CSVImporterCore::m_colTypeConfName.value(Column::Name), m_colTypeNum.value(Column::Name)); profilesGroup.writeEntry(CSVImporterCore::m_colTypeConfName.value(Column::Fee), m_colTypeNum.value(Column::Fee)); profilesGroup.writeEntry(CSVImporterCore::m_colTypeConfName.value(Column::Memo), m_memoColList); profilesGroup.config()->sync(); } bool PricesProfile::readSettings(const KSharedConfigPtr &config) { bool exists = true; KConfigGroup profilesGroup(config, CSVImporterCore::m_profileConfPrefix.value(type()) + QLatin1Char('-') + m_profileName); if (!profilesGroup.exists()) exists = false; m_colTypeNum[Column::Date] = profilesGroup.readEntry(CSVImporterCore::m_colTypeConfName.value(Column::Date), -1); m_colTypeNum[Column::Price] = profilesGroup.readEntry(CSVImporterCore::m_colTypeConfName.value(Column::Price), -1); m_priceFraction = profilesGroup.readEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfPriceFraction), 2); m_securityName = profilesGroup.readEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfSecurityName), QString()); m_securitySymbol = profilesGroup.readEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfSecuritySymbol), QString()); m_currencySymbol = profilesGroup.readEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfCurrencySymbol), QString()); m_dontAsk = profilesGroup.readEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfDontAsk), 0); CSVProfile::readSettings(profilesGroup); return exists; } void PricesProfile::writeSettings(const KSharedConfigPtr &config) { KConfigGroup profilesGroup(config, CSVImporterCore::m_profileConfPrefix.value(type()) + QLatin1Char('-') + m_profileName); CSVProfile::writeSettings(profilesGroup); profilesGroup.writeEntry(CSVImporterCore::m_colTypeConfName.value(Column::Date), m_colTypeNum.value(Column::Date)); profilesGroup.writeEntry(CSVImporterCore::m_colTypeConfName.value(Column::Price), m_colTypeNum.value(Column::Price)); profilesGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfPriceFraction), m_priceFraction); profilesGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfSecurityName), m_securityName); profilesGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfSecuritySymbol), m_securitySymbol); profilesGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfCurrencySymbol), m_currencySymbol); profilesGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfDontAsk), m_dontAsk); profilesGroup.config()->sync(); } CSVFile::CSVFile() : m_columnCount(0), m_rowCount(0) { m_parse = new Parse; m_model = new QStandardItemModel; } CSVFile::~CSVFile() { delete m_parse; delete m_model; } void CSVFile::getStartEndRow(CSVProfile *profile) { profile->m_endLine = m_rowCount - 1; if (profile->m_endLine > profile->m_trailerLines) profile->m_endLine -= profile->m_trailerLines; if (profile->m_startLine > profile->m_endLine) // Don't allow m_startLine > m_endLine profile->m_startLine = profile->m_endLine; } void CSVFile::getColumnCount(CSVProfile *profile, const QStringList &rows) { if (rows.isEmpty()) return; QVector delimiterIndexes; if (profile->m_fieldDelimiter == FieldDelimiter::Auto) delimiterIndexes = QVector{FieldDelimiter::Comma, FieldDelimiter::Semicolon, FieldDelimiter::Colon, FieldDelimiter::Tab}; // include all delimiters to test or ... else delimiterIndexes = QVector{profile->m_fieldDelimiter}; // ... only the one specified QList totalDelimiterCount({0, 0, 0, 0}); // Total in file for each delimiter QList thisDelimiterCount({0, 0, 0, 0}); // Total in this line for each delimiter int colCount = 0; // Total delimiters in this line FieldDelimiter possibleDelimiter = FieldDelimiter::Comma; m_columnCount = 0; foreach (const auto row, rows) { foreach(const auto delimiterIndex, delimiterIndexes) { m_parse->setFieldDelimiter(delimiterIndex); colCount = m_parse->parseLine(row).count(); // parse each line using each delimiter if (colCount > thisDelimiterCount.at((int)delimiterIndex)) thisDelimiterCount[(int)delimiterIndex] = colCount; if (thisDelimiterCount[(int)delimiterIndex] > m_columnCount) m_columnCount = thisDelimiterCount.at((int)delimiterIndex); totalDelimiterCount[(int)delimiterIndex] += colCount; if (totalDelimiterCount.at((int)delimiterIndex) > totalDelimiterCount.at((int)possibleDelimiter)) possibleDelimiter = delimiterIndex; } } if (delimiterIndexes.count() != 1) // if purpose was to autodetect... profile->m_fieldDelimiter = possibleDelimiter; // ... then change field delimiter m_parse->setFieldDelimiter(profile->m_fieldDelimiter); // restore original field delimiter } bool CSVFile::getInFileName(QString inFileName) { QFileInfo fileInfo; if (!inFileName.isEmpty()) { if (inFileName.startsWith(QLatin1Char('~'))) inFileName.replace(0, 1, QDir::homePath()); fileInfo = QFileInfo(inFileName); if (fileInfo.isFile()) { // if it is file... if (fileInfo.exists()) { // ...and exists... m_inFileName = inFileName; // ...then set as valid filename return true; // ...and return success... } else { // ...but if not... fileInfo.setFile(fileInfo.absolutePath()); //...then set start directory to directory of that file... if (!fileInfo.exists()) //...and if it doesn't exist too... fileInfo.setFile(QDir::homePath()); //...then set start directory to home path } } else if (fileInfo.isDir()) { if (fileInfo.exists()) fileInfo = QFileInfo(inFileName); else fileInfo.setFile(QDir::homePath()); } } else fileInfo = QFileInfo(QDir::homePath()); QPointer dialog = new QFileDialog(nullptr, QString(), fileInfo.absoluteFilePath(), i18n("CSV Files (*.csv)")); dialog->setFileMode(QFileDialog::ExistingFile); QUrl url; if (dialog->exec() == QDialog::Accepted) url = dialog->selectedUrls().first(); delete dialog; if (url.isEmpty()) { m_inFileName.clear(); return false; } else m_inFileName = url.toDisplayString(QUrl::PreferLocalFile); return true; } void CSVFile::setupParser(CSVProfile *profile) { if (profile->m_decimalSymbol != DecimalSymbol::Auto) m_parse->setDecimalSymbol(profile->m_decimalSymbol); m_parse->setFieldDelimiter(profile->m_fieldDelimiter); m_parse->setTextDelimiter(profile->m_textDelimiter); } void CSVFile::readFile(CSVProfile *profile) { QFile inFile(m_inFileName); if (!inFile.exists()) return; inFile.open(QIODevice::ReadOnly); QTextStream inStream(&inFile); QTextCodec* codec = QTextCodec::codecForMib(profile->m_encodingMIBEnum); inStream.setCodec(codec); QString buf = inStream.readAll(); inFile.close(); m_parse->setTextDelimiter(profile->m_textDelimiter); QStringList rows = m_parse->parseFile(buf); // parse the buffer m_rowCount = m_parse->lastLine(); // won't work without above line getColumnCount(profile, rows); getStartEndRow(profile); // prepare model from rows having rowCount and columnCount m_model->clear(); for (int i = 0; i < m_rowCount; ++i) { QList itemList; QStringList columns = m_parse->parseLine(rows.takeFirst()); // take instead of read from rows to preserve memory for (int j = 0; j < m_columnCount; ++j) itemList.append(new QStandardItem(columns.value(j, QString()))); m_model->appendRow(itemList); } } diff --git a/kmymoney/plugins/csv/import/core/csvimportercore.h b/kmymoney/plugins/csv/import/core/csvimportercore.h index 8a746ac86..76281cd2e 100644 --- a/kmymoney/plugins/csv/import/core/csvimportercore.h +++ b/kmymoney/plugins/csv/import/core/csvimportercore.h @@ -1,403 +1,403 @@ /*************************************************************************** csvimportercore.h ------------------- begin : Sun May 21 2017 copyright : (C) 2015 by Allan Anderson email : agander93@gmail.com copyright : (C) 2017 by Łukasz Wojniłowicz email : lukasz.wojnilowicz@gmail.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. * * * ***************************************************************************/ #ifndef CSVIMPORTERCORE_H #define CSVIMPORTERCORE_H // ---------------------------------------------------------------------------- // KDE Includes #include // ---------------------------------------------------------------------------- // QT Includes #include // Project Includes #include "mymoneystatement.h" #include "csvenums.h" #include "csv/import/core/kmm_csvimportercore_export.h" class MyMoneyAccount; class KConfigGroup; class QStandardItemModel; class Parse; class ConvertDate; namespace eMyMoney { namespace Account { enum class Type; } } enum autodetectTypeE { AutoFieldDelimiter, AutoDecimalSymbol, AutoDateFormat, AutoAccountInvest, AutoAccountBank }; enum miscSettingsE { ConfDirectory, ConfEncoding, ConfDateFormat, ConfFieldDelimiter, ConfTextDelimiter, ConfDecimalSymbol, ConfStartLine, ConfTrailerLines, ConfOppositeSigns, ConfFeeIsPercentage, ConfFeeRate, ConfMinFee, ConfSecurityName, ConfSecuritySymbol, ConfCurrencySymbol, ConfPriceFraction, ConfDontAsk, ConfHeight, ConfWidth }; enum validationResultE { ValidActionType, InvalidActionValues, NoActionType }; class KMM_CSVIMPORTERCORE_NO_EXPORT CSVProfile { protected: CSVProfile() : m_encodingMIBEnum(0), m_startLine(0), m_endLine(0), m_trailerLines(0), m_dateFormat(DateFormat::DayMonthYear), m_fieldDelimiter(FieldDelimiter::Auto), m_textDelimiter(TextDelimiter::DoubleQuote), m_decimalSymbol(DecimalSymbol::Auto) { } CSVProfile(const QString &profileName, int encodingMIBEnum, int startLine, int trailerLines, DateFormat dateFormat, FieldDelimiter fieldDelimiter, TextDelimiter textDelimiter, DecimalSymbol decimalSymbol, QMap &colTypeNum) : m_profileName(profileName), m_encodingMIBEnum(encodingMIBEnum), m_startLine(startLine), m_endLine(startLine), m_trailerLines(trailerLines), m_dateFormat(dateFormat), m_fieldDelimiter(fieldDelimiter), m_textDelimiter(textDelimiter), m_decimalSymbol(decimalSymbol), m_colTypeNum(colTypeNum) { initColNumType(); } void readSettings(const KConfigGroup &profilesGroup); void writeSettings(KConfigGroup &profilesGroup); void initColNumType() { for (auto it = m_colTypeNum.constBegin(); it != m_colTypeNum.constEnd(); ++it) m_colNumType.insert(it.value(), it.key()); } public: virtual ~CSVProfile() {} virtual Profile type() const = 0; virtual bool readSettings(const KSharedConfigPtr &config) = 0; virtual void writeSettings(const KSharedConfigPtr &config) = 0; QString m_profileName; QString m_lastUsedDirectory; int m_encodingMIBEnum; int m_startLine; int m_endLine; int m_trailerLines; DateFormat m_dateFormat; FieldDelimiter m_fieldDelimiter; TextDelimiter m_textDelimiter; DecimalSymbol m_decimalSymbol; QMap m_colTypeNum; QMap m_colNumType; }; class KMM_CSVIMPORTERCORE_EXPORT BankingProfile : public CSVProfile { public: explicit BankingProfile() : CSVProfile(), m_oppositeSigns(false) {} BankingProfile(QString profileName, int encodingMIBEnum, int startLine, int trailerLines, DateFormat dateFormat, FieldDelimiter fieldDelimiter, TextDelimiter textDelimiter, DecimalSymbol decimalSymbol, QMap colTypeNum, bool oppositeSigns) : CSVProfile(profileName, encodingMIBEnum, startLine, trailerLines, dateFormat, fieldDelimiter, textDelimiter, decimalSymbol, colTypeNum), m_oppositeSigns(oppositeSigns) {} - Profile type() const { return Profile::Banking; } - bool readSettings(const KSharedConfigPtr &config); - void writeSettings(const KSharedConfigPtr &config); + Profile type() const final override { return Profile::Banking; } + bool readSettings(const KSharedConfigPtr &config) final override; + void writeSettings(const KSharedConfigPtr &config) final override; QList m_memoColList; bool m_oppositeSigns; }; class KMM_CSVIMPORTERCORE_EXPORT InvestmentProfile : public CSVProfile { public: explicit InvestmentProfile() : CSVProfile(), m_priceFraction(2), m_dontAsk(0), m_feeIsPercentage(false) { } InvestmentProfile(QString profileName, int encodingMIBEnum, int startLine, int trailerLines, DateFormat dateFormat, FieldDelimiter fieldDelimiter, TextDelimiter textDelimiter, DecimalSymbol decimalSymbol, QMap colTypeNum, int priceFraction, QMap transactionNames) : CSVProfile(profileName, encodingMIBEnum, startLine, trailerLines, dateFormat, fieldDelimiter, textDelimiter, decimalSymbol, colTypeNum), m_transactionNames(transactionNames), m_priceFraction(priceFraction), m_dontAsk(0), m_feeIsPercentage(false) { } - Profile type() const { return Profile::Investment; } - bool readSettings(const KSharedConfigPtr &config); - void writeSettings(const KSharedConfigPtr &config); + Profile type() const final override { return Profile::Investment; } + bool readSettings(const KSharedConfigPtr &config) final override; + void writeSettings(const KSharedConfigPtr &config) final override; QMap m_transactionNames; QString m_feeRate; QString m_minFee; QString m_securityName; QString m_securitySymbol; QList m_memoColList; int m_priceFraction; int m_dontAsk; bool m_feeIsPercentage; }; class KMM_CSVIMPORTERCORE_EXPORT PricesProfile : public CSVProfile { public: explicit PricesProfile() : CSVProfile(), m_dontAsk(0), m_priceFraction(2), m_profileType(Profile::CurrencyPrices) { } explicit PricesProfile(const Profile profileType) : CSVProfile(), m_dontAsk(0), m_priceFraction(2), m_profileType(profileType) { } PricesProfile(QString profileName, int encodingMIBEnum, int startLine, int trailerLines, DateFormat dateFormat, FieldDelimiter fieldDelimiter, TextDelimiter textDelimiter, DecimalSymbol decimalSymbol, QMap colTypeNum, int priceFraction, Profile profileType) : CSVProfile(profileName, encodingMIBEnum, startLine, trailerLines, dateFormat, fieldDelimiter, textDelimiter, decimalSymbol, colTypeNum), m_dontAsk(0), m_priceFraction(priceFraction), m_profileType(profileType) { } - Profile type() const { return m_profileType; } - bool readSettings(const KSharedConfigPtr &config); - void writeSettings(const KSharedConfigPtr &config); + Profile type() const final override { return m_profileType; } + bool readSettings(const KSharedConfigPtr &config) final override; + void writeSettings(const KSharedConfigPtr &config) final override; QString m_securityName; QString m_securitySymbol; QString m_currencySymbol; int m_dontAsk; int m_priceFraction; Profile m_profileType; }; class KMM_CSVIMPORTERCORE_EXPORT CSVFile { public: explicit CSVFile(); ~CSVFile(); void getStartEndRow(CSVProfile *profile); /** * If delimiter = -1 this method tries different field * delimiters to get the one with which file has the most columns. * Otherwise it gets only column count for specified delimiter. */ void getColumnCount(CSVProfile *profile, const QStringList &rows); /** * This method gets the filename of * the financial statement. */ bool getInFileName(QString startDir = QString()); void setupParser(CSVProfile *profile); /** * This method gets file into buffer * It will laso store file's end column and row. */ void readFile(CSVProfile *profile); Parse *m_parse; QStandardItemModel *m_model; QString m_inFileName; int m_columnCount; int m_rowCount; }; class KMM_CSVIMPORTERCORE_EXPORT CSVImporterCore { public: explicit CSVImporterCore(); ~CSVImporterCore(); /** * This method will silently import csv file. Main purpose of this method are online quotes. */ MyMoneyStatement unattendedImport(const QString &filename, CSVProfile *profile); static KSharedConfigPtr configFile(); void profileFactory(const Profile type, const QString &name); void readMiscSettings(); /** * This method ensures that configuration file contains all necessary fields * and that it is up to date. */ void validateConfigFile(); /** * This method contains routines to update configuration file * from kmmVer to latest. */ bool updateConfigFile(QList &confVer); /** * This method will update [ProfileNames] in csvimporterrrc */ static bool profilesAction(const Profile type, const ProfileAction action, const QString &name, const QString &newname); /** * This methods will ensure that fields of input rows are correct. */ bool validateDateFormat(const int col); bool validateDecimalSymbols(const QList &columns); bool validateCurrencies(const PricesProfile *profile); bool validateSecurity(const PricesProfile *profile); bool validateSecurity(const InvestmentProfile *profile); bool validateSecurities(); validationResultE validateActionType(MyMoneyStatement::Transaction &tr); /** * This method will try to detect decimal symbol in input column. */ int detectDecimalSymbols(const QList &columns); DecimalSymbol detectDecimalSymbol(const int col, const QString &exclude); /** * This method will try to detect account from csv header. */ QList findAccounts(const QList &accountTypes, const QString &statementHeader); bool detectAccount(MyMoneyStatement &st); /** * This methods will evaluate input row and append it to a statement. */ bool processBankRow(MyMoneyStatement &st, const BankingProfile *profile, const int row); bool processInvestRow(MyMoneyStatement &st, const InvestmentProfile *profile, const int row); bool processPriceRow(MyMoneyStatement &st, const PricesProfile *profile, const int row); /** * This methods will evaluate fields of input row and return statement's useful value. */ QDate processDateField(const int row, const int col); MyMoneyMoney processCreditDebit(QString &credit, QString &debit ); MyMoneyMoney processPriceField(const InvestmentProfile *profile, const int row, const int col); MyMoneyMoney processPriceField(const PricesProfile *profile, const int row, const int col); MyMoneyMoney processAmountField(const CSVProfile *profile, const int row, const int col); MyMoneyMoney processQuantityField(const CSVProfile *profile, const int row, const int col); eMyMoney::Transaction::Action processActionTypeField(const InvestmentProfile *profile, const int row, const int col); /** * This method creates valid set of possible transactions * according to quantity, amount and price */ QList createValidActionTypes(MyMoneyStatement::Transaction &tr); /** * This method will add fee column to model based on amount and fee rate. */ bool calculateFee(); /** * This method gets securities from investment statement and * tries to get pairs of symbol and name either * from KMM or from statement data. * In case it's not successful onlySymbols and onlyNames won't be empty. */ bool sortSecurities(QSet& onlySymbols, QSet& onlyNames, QMap& mapSymbolName); /** * Helper method to set decimal symbol in case it was set to autodetect. */ void setupFieldDecimalSymbol(int col); /** * Helper method to get all column numbers that were pointed as nummeric */ QList getNumericalColumns(); bool createStatement(MyMoneyStatement &st); ConvertDate *m_convertDate; CSVFile *m_file; CSVProfile *m_profile; KSharedConfigPtr m_config; bool m_isActionTypeValidated; QList m_priceFractions; QSet m_hashSet; QMap m_decimalSymbolIndexMap; QMap m_mapSymbolName; QMap m_autodetect; static const QHash m_colTypeConfName; static const QHash m_profileConfPrefix; static const QHash m_transactionConfName; static const QHash m_miscSettingsConfName; static const QString m_confProfileNames; static const QString m_confPriorName; static const QString m_confMiscName; }; #endif diff --git a/kmymoney/plugins/csv/import/investmentwizardpage.cpp b/kmymoney/plugins/csv/import/investmentwizardpage.cpp index 25b49b2ae..434c3e0bf 100644 --- a/kmymoney/plugins/csv/import/investmentwizardpage.cpp +++ b/kmymoney/plugins/csv/import/investmentwizardpage.cpp @@ -1,666 +1,666 @@ /******************************************************************************* * investmentwizardpage.cpp * -------------------- * begin : Thur Jan 01 2015 * copyright : (C) 2015 by Allan Anderson * email : agander93@gmail.com * copyright : (C) 2016 by Łukasz Wojniłowicz * email : lukasz.wojnilowicz@gmail.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 "investmentwizardpage.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include // ---------------------------------------------------------------------------- // Project Includes #include "mymoneyfile.h" #include "mymoneysecurity.h" #include "csvwizard.h" #include "core/csvimportercore.h" #include "transactiondlg.h" #include "securitydlg.h" #include "securitiesdlg.h" #include "ui_investmentwizardpage.h" #include "ui_securitiesdlg.h" #include "ui_securitydlg.h" // ---------------------------------------------------------------------------- InvestmentPage::InvestmentPage(CSVWizard *dlg, CSVImporterCore *imp) : CSVWizardPage(dlg, imp), ui(new Ui::InvestmentPage) { ui->setupUi(this); connect(ui->m_clear, &QAbstractButton::clicked, this, &InvestmentPage::clearColumns); connect(ui->m_clearFee, &QAbstractButton::clicked, this, &InvestmentPage::clearFee); connect(ui->m_calculateFee, &QAbstractButton::clicked, this, &InvestmentPage::calculateFee); // initialize column names m_dlg->m_colTypeName.insert(Column::Type, i18n("Type")); m_dlg->m_colTypeName.insert(Column::Price, i18n("Price")); m_dlg->m_colTypeName.insert(Column::Quantity, i18n("Quantity")); m_dlg->m_colTypeName.insert(Column::Fee, i18n("Fee")); m_dlg->m_colTypeName.insert(Column::Date, i18n("Date")); m_dlg->m_colTypeName.insert(Column::Amount, i18n("Amount")); m_dlg->m_colTypeName.insert(Column::Symbol, i18n("Symbol")); m_dlg->m_colTypeName.insert(Column::Name, i18n("Name")); m_dlg->m_colTypeName.insert(Column::Memo, i18n("Memo")); m_profile = dynamic_cast(m_imp->m_profile); connect(ui->m_memoCol, SIGNAL(currentIndexChanged(int)), this, SLOT(memoColSelected(int))); connect(ui->m_typeCol, SIGNAL(currentIndexChanged(int)), this, SLOT(typeColSelected(int))); connect(ui->m_dateCol, SIGNAL(currentIndexChanged(int)), this, SLOT(dateColSelected(int))); connect(ui->m_quantityCol, SIGNAL(currentIndexChanged(int)), this, SLOT(quantityColSelected(int))); connect(ui->m_priceCol, SIGNAL(currentIndexChanged(int)), this, SLOT(priceColSelected(int))); connect(ui->m_priceFraction, SIGNAL(currentIndexChanged(int)), this, SLOT(fractionChanged(int))); connect(ui->m_amountCol, SIGNAL(currentIndexChanged(int)), this, SLOT(amountColSelected(int))); connect(ui->m_feeCol, SIGNAL(currentIndexChanged(int)), this, SLOT(feeColSelected(int))); connect(ui->m_symbolCol, SIGNAL(currentIndexChanged(int)), this, SLOT(symbolColSelected(int))); connect(ui->m_nameCol, SIGNAL(currentIndexChanged(int)), this, SLOT(nameColSelected(int))); connect(ui->m_feeIsPercentage, &QAbstractButton::clicked, this, &InvestmentPage::feeIsPercentageClicked); connect(ui->m_feeRate, &QLineEdit::editingFinished, this, &InvestmentPage::feeInputsChanged); connect(ui->m_feeRate, &QLineEdit::textChanged, this, &InvestmentPage::feeRateChanged); connect(ui->m_minFee, &QLineEdit::textChanged, this, &InvestmentPage::minFeeChanged); } InvestmentPage::~InvestmentPage() { delete m_securitiesDlg; delete ui; } void InvestmentPage::calculateFee() { m_imp->calculateFee(); m_dlg->updateWindowSize(); m_dlg->markUnwantedRows(); } void InvestmentPage::initializePage() { QHash columns {{Column::Amount, ui->m_amountCol}, {Column::Type, ui->m_typeCol}, {Column::Quantity, ui->m_quantityCol}, {Column::Memo, ui->m_memoCol}, {Column::Price, ui->m_priceCol}, {Column::Date, ui->m_dateCol}, {Column::Fee, ui->m_feeCol}, {Column::Symbol, ui->m_symbolCol}, {Column::Name, ui->m_nameCol}}; if (ui->m_dateCol->count() != m_imp->m_file->m_columnCount) m_dlg->initializeComboBoxes(columns); ui->m_feeIsPercentage->setChecked(m_profile->m_feeIsPercentage); columns.remove(Column::Memo); for (auto it = columns.cbegin(); it != columns.cend(); ++it) it.value()->setCurrentIndex(m_profile->m_colTypeNum.value(it.key())); ui->m_priceFraction->blockSignals(true); foreach (const auto priceFraction, m_imp->m_priceFractions) ui->m_priceFraction->addItem(QString::number(priceFraction.toDouble(), 'g', 3)); ui->m_priceFraction->blockSignals(false); ui->m_priceFraction->setCurrentIndex(m_profile->m_priceFraction); ui->m_feeRate->setText(m_profile->m_feeRate); ui->m_minFee->setText(m_profile->m_minFee); ui->m_feeRate->setValidator(new QRegularExpressionValidator(QRegularExpression(QStringLiteral("[0-9]{1,2}[") + QLocale().decimalPoint() + QStringLiteral("]{1,1}[0-9]{0,2}")), this) ); ui->m_minFee->setValidator(new QRegularExpressionValidator(QRegularExpression(QStringLiteral("[0-9]{1,}[") + QLocale().decimalPoint() + QStringLiteral("]{0,1}[0-9]{0,}")), this) ); if (!m_profile->m_feeRate.isEmpty()) { // fee rate indicates that fee column needs to be calculated if (m_imp->calculateFee()) { ui->m_feeCol->blockSignals(true); int feeCol = ui->m_feeCol->count(); ui->m_feeCol->addItem(QString::number(feeCol + 1)); ui->m_feeCol->setEnabled(false); ui->m_feeCol->setCurrentIndex(feeCol); ui->m_feeCol->blockSignals(false); m_dlg->updateWindowSize(); m_dlg->markUnwantedRows(); } } else if (m_profile->m_colTypeNum.value(Column::Fee, -1) >= ui->m_feeCol->count()) { // no fee rate, calculated fee column index exist, but the column doesn't exist and that's not ok m_profile->m_colTypeNum[Column::Fee] = -1; m_profile->m_colNumType.remove(m_profile->m_colNumType.key(Column::Fee)); } for (int i = 0; i < m_profile->m_memoColList.count(); ++i) { // Set up all memo fields... int tmp = m_profile->m_memoColList.at(i); if (tmp < m_profile->m_colTypeNum.count()) ui->m_memoCol->setCurrentIndex(tmp); } feeInputsChanged(); } bool InvestmentPage::isComplete() const { return ui->m_dateCol->currentIndex() > -1 && ui->m_typeCol->currentIndex() > -1 && ui->m_quantityCol->currentIndex() > -1 && ui->m_priceCol->currentIndex() > -1 && ui->m_amountCol->currentIndex() > -1 && ui->m_priceFraction->currentIndex() > -1; } bool InvestmentPage::validatePage() { if (ui->m_symbolCol->currentIndex() == -1 && ui->m_nameCol->currentIndex() == -1) return validateSecurity(); else return validateSecurities(); } void InvestmentPage::cleanupPage() { clearFeeCol(); } void InvestmentPage::memoColSelected(int col) { if (m_profile->m_colNumType.value(col) == Column::Type || m_profile->m_colNumType.value(col) == Column::Name) { int rc = KMessageBox::Yes; if (isVisible()) rc = KMessageBox::questionYesNo(m_dlg, i18n("
The '%1' field already has this column selected.
" "
If you wish to copy that data to the memo field, click 'Yes'.
", m_dlg->m_colTypeName.value(m_profile->m_colNumType.value(col)))); if (rc == KMessageBox::Yes) { ui->m_memoCol->setItemText(col, QString::number(col + 1) + QChar(QLatin1Char('*'))); if (!m_profile->m_memoColList.contains(col)) m_profile->m_memoColList.append(col); } else { ui->m_memoCol->setItemText(col, QString::number(col + 1)); m_profile->m_memoColList.removeOne(col); } //allow only separate memo field occupy combobox ui->m_memoCol->blockSignals(true); if (m_profile->m_colTypeNum.value(Column::Memo) != -1) ui->m_memoCol->setCurrentIndex(m_profile->m_colTypeNum.value(Column::Memo)); else ui->m_memoCol->setCurrentIndex(-1); ui->m_memoCol->blockSignals(false); return; } if (m_profile->m_colTypeNum.value(Column::Memo) != -1) // check if this memo has any column 'number' assigned... m_profile->m_memoColList.removeOne(col); // ...if true remove it from memo list if(validateSelectedColumn(col, Column::Memo)) if (col != - 1 && !m_profile->m_memoColList.contains(col)) m_profile->m_memoColList.append(col); } void InvestmentPage::dateColSelected(int col) { validateSelectedColumn(col, Column::Date); } void InvestmentPage::feeColSelected(int col) { validateSelectedColumn(col, Column::Fee); feeInputsChanged(); } void InvestmentPage::typeColSelected(int col) { if (validateSelectedColumn(col, Column::Type)) if (!validateMemoComboBox()) // user could have it already in memo so... memoColSelected(col); // ...if true set memo field again } void InvestmentPage::quantityColSelected(int col) { validateSelectedColumn(col, Column::Quantity); } void InvestmentPage::priceColSelected(int col) { validateSelectedColumn(col, Column::Price); } void InvestmentPage::amountColSelected(int col) { validateSelectedColumn(col, Column::Amount); clearFeeCol(); feeInputsChanged(); } void InvestmentPage::symbolColSelected(int col) { validateSelectedColumn(col, Column::Symbol); m_imp->m_mapSymbolName.clear(); // new symbol column so this map is no longer valid } void InvestmentPage::nameColSelected(int col) { if (validateSelectedColumn(col, Column::Name)) if (!validateMemoComboBox()) // user could have it already in memo so... memoColSelected(col); // ...if true set memo field again m_imp->m_mapSymbolName.clear(); // new name column so this map is no longer valid } void InvestmentPage::feeIsPercentageClicked(bool checked) { m_profile->m_feeIsPercentage = checked; } void InvestmentPage::fractionChanged(int col) { m_profile->m_priceFraction = col; emit completeChanged(); } void InvestmentPage::clearColumns() { ui->m_amountCol->setCurrentIndex(-1); ui->m_dateCol->setCurrentIndex(-1); ui->m_priceCol->setCurrentIndex(-1); ui->m_quantityCol->setCurrentIndex(-1); ui->m_memoCol->setCurrentIndex(-1); ui->m_typeCol->setCurrentIndex(-1); ui->m_nameCol->setCurrentIndex(-1); ui->m_symbolCol->setCurrentIndex(-1); ui->m_priceFraction->setCurrentIndex(-1); clearFee(); } void InvestmentPage::clearFee() { clearFeeCol(); ui->m_feeCol->setCurrentIndex(-1); ui->m_feeIsPercentage->setChecked(false); ui->m_calculateFee->setEnabled(false); ui->m_feeRate->setEnabled(true); ui->m_minFee->setEnabled(false); ui->m_feeRate->clear(); ui->m_minFee->clear(); } void InvestmentPage::feeInputsChanged() { // if (ui->comboBoxInv_feeCol->currentIndex() < m_importer->m_file->m_columnCount && // ui->comboBoxInv_feeCol->currentIndex() > -1) { // ui->lineEdit_minFee->setEnabled(false); // ui->lineEdit_feeRate->setEnabled(false); // ui->lineEdit_minFee->clear(); // ui->lineEdit_feeRate->clear(); // } if (m_profile->m_feeRate.isEmpty()) { ui->m_feeCol->setEnabled(true); ui->m_feeIsPercentage->setEnabled(true); ui->m_minFee->setEnabled(false); ui->m_calculateFee->setEnabled(false); } else { ui->m_feeCol->setEnabled(false); ui->m_feeIsPercentage->setEnabled(false); ui->m_feeIsPercentage->setChecked(true); ui->m_minFee->setEnabled(true); ui->m_feeRate->setEnabled(true); if (m_profile->m_colTypeNum.value(Column::Amount) != -1) ui->m_calculateFee->setEnabled(true); } } void InvestmentPage::feeRateChanged(const QString &text) { m_profile->m_feeRate = text; } void InvestmentPage::minFeeChanged(const QString &text) { m_profile->m_minFee = text; } void InvestmentPage::clearFeeCol() { if (!m_profile->m_feeRate.isEmpty() && // if fee rate isn't empty... m_profile->m_colTypeNum.value(Column::Fee) >= m_imp->m_file->m_columnCount - 1 && !ui->m_feeCol->isEnabled()) { // ...and fee column is last... --m_imp->m_file->m_columnCount; m_imp->m_file->m_model->removeColumn(m_imp->m_file->m_columnCount); int feeCol = ui->m_feeCol->currentIndex(); ui->m_feeCol->setCurrentIndex(-1); ui->m_feeCol->removeItem(feeCol); m_dlg->updateWindowSize(); } ui->m_feeCol->setEnabled(true); ui->m_feeIsPercentage->setEnabled(true); ui->m_feeIsPercentage->setChecked(false); } void InvestmentPage::resetComboBox(const Column comboBox) { switch (comboBox) { case Column::Amount: ui->m_amountCol->setCurrentIndex(-1); break; case Column::Date: ui->m_dateCol->setCurrentIndex(-1); break; case Column::Fee: ui->m_feeCol->setCurrentIndex(-1); break; case Column::Memo: ui->m_memoCol->setCurrentIndex(-1); break; case Column::Price: ui->m_priceCol->setCurrentIndex(-1); break; case Column::Quantity: ui->m_quantityCol->setCurrentIndex(-1); break; case Column::Type: ui->m_typeCol->setCurrentIndex(-1); break; case Column::Symbol: ui->m_symbolCol->setCurrentIndex(-1); break; case Column::Name: ui->m_nameCol->setCurrentIndex(-1); break; default: KMessageBox::sorry(m_dlg, i18n("
Field name not recognised.
'%1'
Please re-enter your column selections.", (int)comboBox), i18n("CSV import")); } } bool InvestmentPage::validateActionType() { for (int row = m_profile->m_startLine; row <= m_profile->m_endLine; ++row) { MyMoneyStatement::Transaction tr; // process quantity field int col = m_profile->m_colTypeNum.value(Column::Quantity); tr.m_shares = m_imp->processQuantityField(m_profile, row, col); // process price field col = m_profile->m_colTypeNum.value(Column::Price); tr.m_price = m_imp->processPriceField(m_profile, row, col); // process amount field col = m_profile->m_colTypeNum.value(Column::Amount); tr.m_amount = m_imp->processAmountField(m_profile, row, col); // process type field col = m_profile->m_colTypeNum.value(Column::Type); tr.m_eAction = m_imp->processActionTypeField(m_profile, row, col); switch(m_imp->validateActionType(tr)) { case InvalidActionValues: KMessageBox::sorry(m_dlg, i18n("The values in the columns you have selected\ndo not match any expected investment type.\n" "Please check the fields in the current transaction,\nand also your selections."), i18n("CSV import")); return false; break; case NoActionType: { bool unknownType = false; if (tr.m_eAction == eMyMoney::Transaction::Action::None) unknownType = true; QStringList colList; QStringList colHeaders; - for (int col = 0; col < m_imp->m_file->m_columnCount; ++col) { + for (col = 0; col < m_imp->m_file->m_columnCount; ++col) { colHeaders.append(m_dlg->m_colTypeName.value(m_profile->m_colNumType.value(col, Column::Invalid), QString(i18nc("Unused column", "Unused")))); colList.append(m_imp->m_file->m_model->item(row, col)->text()); } QList validActionTypes = m_imp->createValidActionTypes(tr); QPointer transactionDlg = new TransactionDlg(colList, colHeaders, m_profile->m_colTypeNum.value(Column::Type), validActionTypes); if (transactionDlg->exec() == QDialog::Rejected) { KMessageBox::information(m_dlg, i18n("
No valid action type found for this transaction.
" "
Please check the parameters supplied.
"), i18n("CSV import")); return false; } else tr.m_eAction = transactionDlg->getActionType(); delete transactionDlg; if (unknownType) { // type was unknown so store it col = m_profile->m_colTypeNum.value(Column::Type); m_profile->m_transactionNames[tr.m_eAction].append(m_imp->m_file->m_model->item(row, col)->text()); // store action type } } default: break; } } m_imp->m_isActionTypeValidated = true; return true; } bool InvestmentPage::validateSelectedColumn(const int col, const Column type) { if (m_profile->m_colTypeNum.value(type) != -1) // check if this 'type' has any column 'number' assigned... m_profile->m_colNumType.remove(m_profile->m_colTypeNum.value(type)); // ...if true remove 'type' assigned to this column 'number' bool ret = true; if (col == -1) { // user only wanted to reset his column so allow him m_profile->m_colTypeNum[type] = col; // assign new column 'number' to this 'type' } else if (m_profile->m_colNumType.contains(col)) { // if this column 'number' has already 'type' assigned KMessageBox::information(m_dlg, i18n("The '%1' field already has this column selected.
Please reselect both entries as necessary.
", m_dlg->m_colTypeName.value(m_profile->m_colNumType.value(col)))); resetComboBox(m_profile->m_colNumType[col]); resetComboBox(type); ret = false; } else { m_profile->m_colTypeNum[type] = col; // assign new column 'number' to this 'type' m_profile->m_colNumType[col] = type; // assign new 'type' to this column 'number' } emit completeChanged(); return ret; } bool InvestmentPage::validateMemoComboBox() { if (m_profile->m_memoColList.count() == 0) return true; for (int i = 0; i < ui->m_memoCol->count(); ++i) { QString txt = ui->m_memoCol->itemText(i); if (txt.contains(QLatin1Char('*'))) // check if text containing '*' belongs to valid column types if (m_profile->m_colNumType.value(i) != Column::Name && m_profile->m_colNumType.value(i) != Column::Type) { ui->m_memoCol->setItemText(i, QString::number(i + 1)); m_profile->m_memoColList.removeOne(i); return false; } } return true; } bool InvestmentPage::validateSecurities() { if (m_securitiesDlg.isNull() && m_imp->m_mapSymbolName.isEmpty()) { QSet onlySymbols; QSet onlyNames; m_imp->sortSecurities(onlySymbols, onlyNames, m_imp->m_mapSymbolName); if (!onlySymbols.isEmpty() || !onlyNames.isEmpty()) { m_securitiesDlg = new SecuritiesDlg; for (QSet::const_iterator symbol = onlySymbols.cbegin(); symbol != onlySymbols.cend(); ++symbol) m_securitiesDlg->displayLine(*symbol, QString()); for (QSet::const_iterator name = onlyNames.cbegin(); name != onlyNames.cend(); ++name) m_securitiesDlg->displayLine(QString(), *name); } } if (!m_securitiesDlg.isNull()) { QTableWidget* symbolTable = m_securitiesDlg->ui->tableWidget; if (m_securitiesDlg->exec() == QDialog::Rejected) { return false; } else { for (int row = 0; row < symbolTable->rowCount(); ++row) { QString symbol = symbolTable->item(row, 1)->text(); QString name = symbolTable->item(row, 2)->text(); m_imp->m_mapSymbolName.insert(symbol, name); } delete m_securitiesDlg; } } return true; } bool InvestmentPage::validateSecurity() { if (!m_profile->m_securitySymbol.isEmpty() && !m_profile->m_securityName.isEmpty()) m_imp->m_mapSymbolName.insert(m_profile->m_securitySymbol, m_profile->m_securityName); MyMoneyFile* file = MyMoneyFile::instance(); if (m_securityDlg.isNull() && (m_imp->m_mapSymbolName.isEmpty() || !(m_profile->m_dontAsk && m_dlg->m_skipSetup))) { m_securityDlg = new SecurityDlg; m_securityDlg->initializeSecurities(m_profile->m_securitySymbol, m_profile->m_securityName); m_securityDlg->ui->cbDontAsk->setChecked(m_profile->m_dontAsk); } if (!m_securityDlg.isNull()) { if (m_securityDlg->exec() == QDialog::Rejected) { return false; } else { QString securityID = m_securityDlg->security(); if (!securityID.isEmpty()) { m_profile->m_securityName = file->security(securityID).name(); m_profile->m_securitySymbol = file->security(securityID).tradingSymbol(); } else { m_profile->m_securityName = m_securityDlg->name(); m_profile->m_securitySymbol = m_securityDlg->symbol(); } m_profile->m_dontAsk = m_securityDlg->dontAsk(); m_imp->m_mapSymbolName.clear(); m_imp->m_mapSymbolName.insert(m_profile->m_securitySymbol, m_profile->m_securityName); // probably we should check if security with given symbol and name exists... delete m_securityDlg; // ...but KMM allows creating duplicates, so don't bother } } if (m_imp->m_mapSymbolName.isEmpty()) return false; return true; } void InvestmentPage::makeQIF(const MyMoneyStatement& st, const QString& outFileName) { QFile oFile(outFileName); oFile.open(QIODevice::WriteOnly); QTextStream out(&oFile); QString buffer; QString strEType; switch (st.m_eType) { case eMyMoney::Statement::Type::Investment: default: strEType = QStringLiteral("Invst"); } if (!st.m_strAccountName.isEmpty()) { buffer.append(QStringLiteral("!Account\n")); buffer.append(QChar(QLatin1Char('N')) + st.m_strAccountName + QChar(QLatin1Char('\n'))); buffer.append(QChar(QLatin1Char('T')) + strEType + QChar(QLatin1Char('\n'))); buffer.append(QStringLiteral("^\n")); } for (QList::const_iterator it = st.m_listSecurities.constBegin(); it != st.m_listSecurities.constEnd(); ++it) { buffer.append(QStringLiteral("!Type:Security\n")); buffer.append(QChar(QLatin1Char('N')) + (*it).m_strName + QChar(QLatin1Char('\n'))); buffer.append(QChar(QLatin1Char('S')) + (*it).m_strSymbol + QChar(QLatin1Char('\n'))); buffer.append(QStringLiteral("TStock\n^\n")); } if (!st.m_strAccountName.isEmpty()) { buffer.append(QStringLiteral("!Account\n")); buffer.append(QChar(QLatin1Char('N')) + st.m_strAccountName + QChar(QLatin1Char('\n'))); buffer.append(QChar(QLatin1Char('T')) + strEType + QChar(QLatin1Char('\n'))); buffer.append(QStringLiteral("^\n")); } buffer.append(QStringLiteral("!Type:") + strEType + QChar(QLatin1Char('\n'))); for (QList::const_iterator it = st.m_listTransactions.constBegin(); it != st.m_listTransactions.constEnd(); ++it) { buffer.append(QChar(QLatin1Char('D')) + it->m_datePosted.toString(QStringLiteral("MM/dd/yyyy")) + QChar(QLatin1Char('\n'))); buffer.append(QChar(QLatin1Char('Y')) + it->m_strSecurity + QChar(QLatin1Char('\n'))); QString txt; switch (it->m_eAction) { case eMyMoney::Transaction::Action::Buy: txt = QStringLiteral("Buy"); break; case eMyMoney::Transaction::Action::Sell: txt = QStringLiteral("Sell"); break; case eMyMoney::Transaction::Action::ReinvestDividend: txt = QStringLiteral("ReinvDiv"); break; case eMyMoney::Transaction::Action::CashDividend: txt = QStringLiteral("Div"); break; case eMyMoney::Transaction::Action::Interest: txt = QStringLiteral("IntInc"); break; case eMyMoney::Transaction::Action::Shrsin: txt = QStringLiteral("ShrsIn"); break; case eMyMoney::Transaction::Action::Shrsout: txt = QStringLiteral("ShrsOut"); break; case eMyMoney::Transaction::Action::Stksplit: txt = QStringLiteral("stksplit"); break; default: txt = QStringLiteral("unknown"); // shouldn't happen } buffer.append(QChar(QLatin1Char('N')) + txt + QChar(QLatin1Char('\n'))); if (it->m_eAction == eMyMoney::Transaction::Action::Buy) // we added 'N' field so buy transaction should have any sign txt.setNum(it->m_amount.abs().toDouble(), 'f', 4); else txt.setNum(it->m_amount.toDouble(), 'f', 4); buffer.append(QChar(QLatin1Char('T')) + txt + QChar(QLatin1Char('\n'))); txt.setNum(it->m_shares.toDouble(), 'f', 4); buffer.append(QChar(QLatin1Char('Q')) + txt + QChar(QLatin1Char('\n'))); txt.setNum(it->m_price.toDouble(), 'f', 4); buffer.append(QChar(QLatin1Char('I')) + txt + QChar(QLatin1Char('\n'))); if (!it->m_fees.isZero()) { txt.setNum(it->m_fees.toDouble(), 'f', 4); buffer.append(QChar(QLatin1Char('O')) + txt + QChar(QLatin1Char('\n'))); } if (!it->m_strBrokerageAccount.isEmpty()) buffer.append(QChar(QLatin1Char('L')) + it->m_strBrokerageAccount + QChar(QLatin1Char('\n'))); if (!it->m_strMemo.isEmpty()) buffer.append(QChar(QLatin1Char('M')) + it->m_strMemo + QChar(QLatin1Char('\n'))); buffer.append(QStringLiteral("^\n")); out << buffer;// output qif file buffer.clear(); } oFile.close(); } diff --git a/kmymoney/plugins/csv/import/investmentwizardpage.h b/kmymoney/plugins/csv/import/investmentwizardpage.h index 8560a29a4..46588ed3c 100644 --- a/kmymoney/plugins/csv/import/investmentwizardpage.h +++ b/kmymoney/plugins/csv/import/investmentwizardpage.h @@ -1,120 +1,120 @@ /******************************************************************************* * investmentwizardpage.h * ------------------ * begin : Thur Jan 01 2015 * copyright : (C) 2015 by Allan Anderson * email : agander93@gmail.com * copyright : (C) 2016 by Łukasz Wojniłowicz * email : lukasz.wojnilowicz@gmail.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. * * * ********************************************************************************/ #ifndef INVESTMENTWIZARDPAGE_H #define INVESTMENTWIZARDPAGE_H // ---------------------------------------------------------------------------- // QT Includes #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes #include "csvwizardpage.h" // ---------------------------------------------------------------------------- class InvestmentProfile; class SecurityDlg; class SecuritiesDlg; class MyMoneyStatement; namespace Ui { class InvestmentPage; } class InvestmentPage : public CSVWizardPage { Q_OBJECT public: explicit InvestmentPage(CSVWizard *dlg, CSVImporterCore *imp); ~InvestmentPage(); /** * This method fills QIF file with investment data */ void makeQIF(const MyMoneyStatement &st, const QString &outFileName); /** * This method validates the column numbers entered by the user. It then * checks the values in those columns for compatibility with the input * investment activity type. */ bool validateActionType(); private: - void initializePage(); - bool isComplete() const; - bool validatePage(); - void cleanupPage(); + void initializePage() final override; + bool isComplete() const final override; + bool validatePage() final override; + void cleanupPage() final override; void clearFeeCol(); /** * This method will check whether memo combobox is still valid * after changing name or type column. */ bool validateMemoComboBox(); void resetComboBox(const Column comboBox); /** * This method is called column on investment page is selected. * It sets m_colTypeNum, m_colNumType and runs column validation. */ bool validateSelectedColumn(const int col, const Column type); /** * This method ensures that every security has symbol and name. */ bool validateSecurity(); bool validateSecurities(); QPointer m_securityDlg; QPointer m_securitiesDlg; InvestmentProfile *m_profile; Ui::InvestmentPage *ui; private Q_SLOTS: void clearFee(); void memoColSelected(int col); void dateColSelected(int col); void feeColSelected(int col); void typeColSelected(int col); void quantityColSelected(int col); void priceColSelected(int col); void amountColSelected(int col); void symbolColSelected(int col); void nameColSelected(int col); void feeIsPercentageClicked(bool checked); void fractionChanged(int col); void clearColumns(); void feeInputsChanged(); void feeRateChanged(const QString &text); void minFeeChanged(const QString &text); void calculateFee(); }; #endif // INVESTMENTWIZARDPAGE_H diff --git a/kmymoney/plugins/csv/import/priceswizardpage.h b/kmymoney/plugins/csv/import/priceswizardpage.h index 60b1afb0a..ce1f7a344 100644 --- a/kmymoney/plugins/csv/import/priceswizardpage.h +++ b/kmymoney/plugins/csv/import/priceswizardpage.h @@ -1,88 +1,88 @@ /******************************************************************************* * priceswizardpage.h * ------------------ * begin : Sat Jan 21 2017 * copyright : (C) 2016 by Łukasz Wojniłowicz * email : lukasz.wojnilowicz@gmail.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. * * * ********************************************************************************/ #ifndef PRICESWIZARDPAGE_H #define PRICESWIZARDPAGE_H // ---------------------------------------------------------------------------- // QT Includes #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes #include "csvwizardpage.h" // ---------------------------------------------------------------------------- class PricesProfile; class SecurityDlg; class CurrenciesDlg; namespace Ui { class PricesPage; } class PricesPage : public CSVWizardPage { Q_OBJECT public: explicit PricesPage(CSVWizard *dlg, CSVImporterCore *imp); ~PricesPage(); private: - void initializePage(); - bool isComplete() const; - bool validatePage(); + void initializePage() final override; + bool isComplete() const final override; + bool validatePage() final override; void resetComboBox(const Column comboBox); /** * This method is called column on prices page is selected. * It sets m_colTypeNum, m_colNumType and runs column validation. */ bool validateSelectedColumn(const int col, const Column type); /** * This method ensures that there is security for price import. */ bool validateSecurity(); /** * This method ensures that there are currencies for price import. */ bool validateCurrencies(); PricesProfile *m_profile; Ui::PricesPage *ui; QPointer m_securityDlg; QPointer m_currenciesDlg; private Q_SLOTS: void dateColSelected(int col); void priceColSelected(int col); void fractionChanged(int col); void clearColumns(); }; #endif // PRICESWIZARDPAGE_H diff --git a/kmymoney/plugins/gnc/import/mymoneygncreader.h b/kmymoney/plugins/gnc/import/mymoneygncreader.h index 4bc0e118d..e16a4ba54 100644 --- a/kmymoney/plugins/gnc/import/mymoneygncreader.h +++ b/kmymoney/plugins/gnc/import/mymoneygncreader.h @@ -1,1092 +1,1092 @@ /*************************************************************************** mymoneygncreader - description ------------------- begin : Wed Mar 3 2004 copyright : (C) 2000-2004 by Michael Edwardes email : mte@users.sourceforge.net Javier Campos Morales Felix Rodriguez John C Thomas Baumgart Kevin Tambascio (C) 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. * * * ***************************************************************************/ /* The main class of this module, MyMoneyGncReader, contains only a readFile() function, which controls the import of data from an XML file created by the current GnuCash version (1.8.8). The XML is processed in class XmlReader, which is an implementation of the Qt SAX2 reader class. Data in the input file is processed as a set of objects which fortunately, though perhaps not surprisingly, have almost a one-for-one correspondence with KMyMoney objects. These objects are bounded by start and end XML elements, and may contain both nested objects (described as sub objects in the code), and data items, also delimited by start and end elements. For example: * start of sub object within file Account Name * data string with start and end elements ... * end of sub objects A GnuCash file may consist of more than one 'book', or set of data. It is not clear how we could currently implement this, so only the first book in a file is processed. This should satisfy most user situations. GnuCash is somewhat inconsistent in its division of the major sections of the file. For example, multiple price history entries are delimited by elements, while each account starts with its own top-level element. In general, the 'container' elements are ignored. XmlReader This is an implementation of the Qt QXmlDefaultHandler class, which provides three main function calls in addition to start and end of document. The startElement() and endElement() calls are self-explanatory, the characters() function provides data strings. Thus in the above example, the sequence of calls would be startElement() for gnc:account startElement() for act:name characters() for 'Account Name' endElement() for act:name ... endElement() for gnc:account Objects Since the processing requirements of XML for most elements are very similar, the common code is implemented in a GncObject class, from which the others are derived, with virtual function calls to cater for any differences. The 'grandfather' object, GncFile representing the file (or more correctly, 'book') as a whole, is created in the startDocument() function call. The constructor function of each object is responsible for providing two lists for the XmlReader to scan, a list of element names which represent sub objects (called sub elements in the code), and a similar list of names representing data elements. In addition, an array of variables (m_v) is provided and initialized, to contain the actual data strings. Implementation Since objects may be nested, a stack is used, with the top element pointing to the 'current object'. The startDocument() call creates the first, GncFile, object at the top of the stack. As each startElement() call occurs, the two#include "mymoneygncreader.h" element lists created by the current object are scanned. If this element represents the start of a sub object, the current object's subEl() function is called to create an instance of the appropriate type. This is then pushed to the top of the stack, and the new object's initiate() function is called. This is used to process any XML attributes attached to the element; GnuCash makes little use of these. If this represents the start of a data element, a pointer (m_dataPointer) is set to point to an entry in the array (m_v) in which a subsequent characters() call can store the actual data. When an endElement() call occurs, a check is made to see if it matches the element name which started the current object. If so, the object's terminate() function is called. If the object represents a similar KMM object, this will normally result in a call to a conversion routine in the main (MyMoneyGncReader) class to convert the data to native format and place it in storage. The stack is then popped, and the parent (now current) object notified by a call to its endSubEl() function. Again depending on the type of object, this will either delete the instance, or save it in its own storage for later processing. For example, a GncSplit object makes little sense outside the context of its transaction, so will be saved by the transaction. A GncTransaction object on the other hand will be converted, along with its attendant splits, and then deleted by its parent. Since at any one time an object will only be processing either a subobject or a data element, a single object variable, m_state, is used to determine the actual type. In effect, it acts as the current index into either the subElement or dataElement list. As an object variable, it will be saved on the stack across subobject processing. Exceptions and Problems Fatal exceptions are processed via the standard MyMoneyException method. Due to differences in implementation between GnuCash and KMM, it is not always possible to provide an absolutely correct conversion. When such a problem situation is recognized, a message, along with any relevant variable data, is passed to the main class, and used to produce a report when processing terminates. Anonymizer When debugging problems, it is often useful to have a trace of what is happening within the module. However, in view of the sensitive nature of personal finance data, most users will be reluctant to provide this. Accordingly, an anonymize (hide()) function is provided to handle data strings. These may either be passed through asis (non-personal data), blanked out (non-critical but possibly personal data), replaced with a generated version (required, but possibly personal), or randomized (monetary amounts). The action for each data item is determined in the object's constructor function along with the creation of the data element list. This module will later be used as the basis of a file anonymizer, which will enable users to safely provide us with a copy of their GnuCash files, and will allow us to test the structure, if not the data content, of the file. */ #ifndef MYMONEYGNCREADER_H #define MYMONEYGNCREADER_H // system includes #include // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #ifndef _GNCFILEANON #include "storage/imymoneystorageformat.h" #endif // _GNCFILEANON // not sure what these are for, but leave them in #define VERSION_0_60_XML 0x10000010 // Version 0.5 file version info #define VERSION_0_61_XML 0x10000011 // use 8 bytes for MyMoneyMoney objects #define GNUCASH_ID_KEY "GNUCASH_ID" class MyMoneyAccount; class MyMoneySecurity; class MyMoneyTransaction; class MyMoneySplit; typedef QMap map_accountIds; typedef map_accountIds::iterator map_accountIds_iter; typedef map_accountIds::const_iterator map_accountIds_citer; typedef QMap map_elementVersions; class MyMoneyGncReader; class QIODevice; class QDate; class QTextCodec; class MyMoneyStorageMgr; class QXmlAttributes; class QXmlInputSource; class QXmlSimpleReader; /** GncObject is the base class for the various objects in the gnucash file Beyond the first level XML objects, elements will be of one of three types: 1. Sub object elements, which require creation of another object to process 2. Data object elements, which are only followed by data to be stored in a variable (m_v array) 3. Ignored objects, data not needed and not included herein */ class GncKvp; class GncObject { public: GncObject(); virtual ~GncObject() {} // make sure to have impl of all virtual rtns to avoid vtable errors? protected: friend class XmlReader; friend class MyMoneyGncReader; // check for sub object element; if it is, create the object GncObject *isSubElement(const QString &elName, const QXmlAttributes& elAttrs); // check for data element; if so, set data pointer bool isDataElement(const QString &elName, const QXmlAttributes& elAttrs); // process start element for 'this'; normally for attribute checking; other initialization done in constructor virtual void initiate(const QString&, const QXmlAttributes&) { return ; - }; + } // a sub object has completed; process the data it gathered virtual void endSubEl(GncObject *) { m_dataPtr = 0; return ; - }; + } // store data for data element void storeData(const QString& pData) { // NB - data MAY come in chunks, and may need to be anonymized if (m_dataPtr != 0) m_dataPtr->append(hide(pData, m_anonClass)); } // following is provided only for a future file anonymizer QString getData() const { return ((m_dataPtr != 0) ? *m_dataPtr : ""); - }; + } void resetDataPtr() { m_dataPtr = 0; - }; + } // process end element for 'this'; usually to convert to KMM format virtual void terminate() { return ; - }; + } void setVersion(const QString& v) { m_version = v; return; - }; + } QString version() const { return (m_version); - }; + } // some gnucash elements have version attribute; check it void checkVersion(const QString&, const QXmlAttributes&, const map_elementVersions&); // get name of element processed by 'this' QString getElName() const { return (m_elementName); - }; + } // pass 'main' pointer to object void setPm(MyMoneyGncReader *pM) { pMain = pM; - }; + } const QString getKvpValue(const QString& key, const QString& type = QString()) const; // debug only void debugDump(); // called by isSubElement to create appropriate sub object virtual GncObject *startSubEl() { return (0); - }; + } // called by isDataElement to set variable pointer virtual void dataEl(const QXmlAttributes&) { m_dataPtr = &(m_v[m_state]); m_anonClass = m_anonClassList[m_state]; - }; + } // return gnucash data string variable pointer virtual QString var(int i) const; // anonymize data virtual QString hide(QString, unsigned int); unsigned int kvpCount() const { return (m_kvpList.count()); - }; //! + } //! MyMoneyGncReader *pMain; // pointer to 'main' class // used at start of each transaction so same money hide factor is applied to all splits void adjustHideFactor(); QString m_elementName; // save 'this' element's name QString m_version; // and it's gnucash version const QString *m_subElementList; // list of sub object element names for 'this' unsigned int m_subElementListCount; // count of above const QString *m_dataElementList; // ditto for data elements unsigned int m_dataElementListCount; QString *m_dataPtr; // pointer to m_v variable for current data item mutable QList m_v; // storage for variable pointers unsigned int m_state; // effectively, the index to subElementList or dataElementList, whichever is currently in use const unsigned int *m_anonClassList; enum anonActions {ASIS, SUPPRESS, NXTACC, NXTEQU, NXTPAY, NXTSCHD, MAYBEQ, MONEY1, MONEY2}; // anonymize actions - see hide() unsigned int m_anonClass; // class of current data item for anonymizer static double m_moneyHideFactor; // a per-transaction factor QList m_kvpList; //! }; // ***************************************************************************** // This is the 'grandfather' object representing the gnucash file as a whole class GncFile : public GncObject { public: GncFile(); ~GncFile(); private: enum iSubEls {BOOK, COUNT, CMDTY, PRICE, ACCT, TX, TEMPLATES, SCHEDULES, END_FILE_SELS }; - virtual GncObject *startSubEl(); - virtual void endSubEl(GncObject *); + GncObject *startSubEl() final override; + void endSubEl(GncObject *) final override; bool m_processingTemplates; // gnc uses same transaction element for ordinary and template tx's; this will distinguish bool m_bookFound; // to detect multi-book files }; // The following are 'utility' objects, which occur within several other object types // ************* GncKvp******************************************** // Key/value pairs, which are introduced by the 'slot' element // Consist of slot:key (the 'name' of the kvp), and slot:value (the data value) // the slot value also contains a slot type (string, integer, etc) implemented as an XML attribute // kvp's may be nested class GncKvp : public GncObject { public: GncKvp(); ~GncKvp(); //protected: friend class MyMoneyGncReader; QString key() const { return (var(KEY)); - }; + } QString value() const { return (var(VALUE)); - }; + } QString type() const { return (m_kvpType); - }; + } const GncKvp getKvp(unsigned int i) const { return (m_kvpList[i]); - }; + } private: // subsidiary objects/elements enum KvpSubEls {KVP, END_Kvp_SELS }; - virtual GncObject *startSubEl(); - virtual void endSubEl(GncObject *); + GncObject *startSubEl() final override; + void endSubEl(GncObject *) final override; // data elements enum KvpDataEls {KEY, VALUE, END_Kvp_DELS }; - virtual void dataEl(const QXmlAttributes&); + void dataEl(const QXmlAttributes&) final override; QString m_kvpType; // type is an XML attribute }; // ************* GncLot******************************************** // KMM doesn't have support for lots as yet class GncLot : public GncObject { public: GncLot(); ~GncLot(); protected: friend class MyMoneyGncReader; private: }; // **************************************************************************** // commodity specification. consists of // cmdty:space - either ISO4217 if this cmdty is a currency, or, usually, the name of a stock exchange // cmdty:id - ISO4217 currency symbol, or 'ticker symbol' class GncCmdtySpec : public GncObject { public: GncCmdtySpec(); ~GncCmdtySpec(); protected: friend class MyMoneyGncReader; friend class GncTransaction; bool isCurrency() const { return (m_v[CMDTYSPC] == QString("ISO4217")); }; QString id() const { return (m_v[CMDTYID]); }; QString space() const { return (m_v[CMDTYSPC]); }; private: // data elements enum CmdtySpecDataEls {CMDTYSPC, CMDTYID, END_CmdtySpec_DELS}; - virtual QString hide(QString, unsigned int); + QString hide(QString, unsigned int) final override; }; // ********************************************************************* // date; maybe one of two types, ts:date which is date/time, gdate which is date only // we do not preserve time data (at present) class GncDate : public GncObject { public: GncDate(); ~GncDate(); protected: friend class MyMoneyGncReader; friend class GncPrice; friend class GncTransaction; friend class GncSplit; friend class GncSchedule; friend class GncRecurrence; const QDate date() const { return (QDate::fromString(m_v[TSDATE].section(' ', 0, 0), Qt::ISODate)); }; private: // data elements enum DateDataEls {TSDATE, GDATE, END_Date_DELS}; - virtual void dataEl(const QXmlAttributes&) { + void dataEl(const QXmlAttributes&) final override { m_dataPtr = &(m_v[TSDATE]); m_anonClass = GncObject::ASIS; } - ; // treat both date types the same + // treat both date types the same }; /** Following are the main objects within the gnucash file, which correspond largely one-for-one with similar objects in the kmymoney structure, apart from schedules which gnc splits between template (transaction data) and schedule (date data) */ //******************************************************************** class GncCountData : public GncObject { public: GncCountData(); ~GncCountData(); private: - virtual void initiate(const QString&, const QXmlAttributes&); - virtual void terminate(); + void initiate(const QString&, const QXmlAttributes&) final override; + void terminate() final override; QString m_countType; // type of element being counted }; //******************************************************************** class GncCommodity : public GncObject { public: GncCommodity(); ~GncCommodity(); protected: friend class MyMoneyGncReader; // access data values bool isCurrency() const { return (var(SPACE) == QString("ISO4217")); - }; + } QString space() const { return (var(SPACE)); - }; + } QString id() const { return (var(ID)); - }; + } QString name() const { return (var(NAME)); - }; + } QString fraction() const { return (var(FRACTION)); - }; + } private: - virtual void terminate(); + void terminate() final override; // data elements enum {SPACE, ID, NAME, FRACTION, END_Commodity_DELS}; }; // ************* GncPrice******************************************** class GncPrice : public GncObject { public: GncPrice(); ~GncPrice(); protected: friend class MyMoneyGncReader; // access data values const GncCmdtySpec *commodity() const { return (m_vpCommodity); - }; + } const GncCmdtySpec *currency() const { return (m_vpCurrency); - }; + } QString value() const { return (var(VALUE)); - }; + } QDate priceDate() const { return (m_vpPriceDate->date()); - }; + } private: - virtual void terminate(); + void terminate() final override; // sub object elements enum PriceSubEls {CMDTY, CURR, PRICEDATE, END_Price_SELS }; - virtual GncObject *startSubEl(); - virtual void endSubEl(GncObject *); + GncObject *startSubEl() final override; + void endSubEl(GncObject *) final override; // data elements enum PriceDataEls {VALUE, END_Price_DELS }; GncCmdtySpec *m_vpCommodity, *m_vpCurrency; GncDate *m_vpPriceDate; }; // ************* GncAccount******************************************** class GncAccount : public GncObject { public: GncAccount(); ~GncAccount(); protected: friend class MyMoneyGncReader; // access data values GncCmdtySpec *commodity() const { return (m_vpCommodity); - }; + } QString id() const { return (var(ID)); - }; + } QString name() const { return (var(NAME)); - }; + } QString desc() const { return (var(DESC)); - }; + } QString type() const { return (var(TYPE)); - }; + } QString parent() const { return (var(PARENT)); - }; + } private: // subsidiary objects/elements enum AccountSubEls {CMDTY, KVP, LOTS, END_Account_SELS }; - virtual GncObject *startSubEl(); - virtual void endSubEl(GncObject *); - virtual void terminate(); + GncObject *startSubEl() final override; + void endSubEl(GncObject *) final override; + void terminate() final override; // data elements enum AccountDataEls {ID, NAME, DESC, TYPE, PARENT, END_Account_DELS }; GncCmdtySpec *m_vpCommodity; }; // ************* GncSplit******************************************** class GncSplit : public GncObject { public: GncSplit(); ~GncSplit(); protected: friend class MyMoneyGncReader; // access data values QString id() const { return (var(ID)); - }; + } QString memo() const { return (var(MEMO)); - }; + } QString recon() const { return (var(RECON)); - }; + } QString value() const { return (var(VALUE)); - }; + } QString qty() const { return (var(QTY)); - }; + } QString acct() const { return (var(ACCT)); - }; + } const QDate reconDate() const { QDate x = QDate(); return (m_vpDateReconciled == NULL ? x : m_vpDateReconciled->date()); - }; + } private: // subsidiary objects/elements enum TransactionSubEls {RECDATE, END_Split_SELS }; - virtual GncObject *startSubEl(); - virtual void endSubEl(GncObject *); + GncObject *startSubEl() final override; + void endSubEl(GncObject *) final override; // data elements enum SplitDataEls {ID, MEMO, RECON, VALUE, QTY, ACCT, END_Split_DELS }; GncDate *m_vpDateReconciled; }; // ************* GncTransaction******************************************** class GncTransaction : public GncObject { public: GncTransaction(bool processingTemplates); ~GncTransaction(); protected: friend class MyMoneyGncReader; // access data values QString id() const { return (var(ID)); - }; + } QString no() const { return (var(NO)); - }; + } QString desc() const { return (var(DESC)); - }; + } QString currency() const { return (m_vpCurrency == NULL ? QString() : m_vpCurrency->id()); - }; + } QDate dateEntered() const { return (m_vpDateEntered->date()); - }; + } QDate datePosted() const { return (m_vpDatePosted->date()); - }; + } bool isTemplate() const { return (m_template); - }; + } unsigned int splitCount() const { return (m_splitList.count()); - }; + } const GncObject *getSplit(unsigned int i) const { return (m_splitList.at(i)); - }; + } private: // subsidiary objects/elements enum TransactionSubEls {CURRCY, POSTED, ENTERED, SPLIT, KVP, END_Transaction_SELS }; - virtual GncObject *startSubEl(); - virtual void endSubEl(GncObject *); - virtual void terminate(); + GncObject *startSubEl() final override; + void endSubEl(GncObject *) final override; + void terminate() final override; const GncKvp getKvp(unsigned int i) const { return (m_kvpList.at(i)); - }; + } // data elements enum TransactionDataEls {ID, NO, DESC, END_Transaction_DELS }; GncCmdtySpec *m_vpCurrency; GncDate *m_vpDateEntered, *m_vpDatePosted; mutable QList m_splitList; bool m_template; // true if this is a template for scheduled transaction }; // ************* GncTemplateSplit******************************************** class GncTemplateSplit : public GncObject { public: GncTemplateSplit(); ~GncTemplateSplit(); protected: friend class MyMoneyGncReader; // access data values QString id() const { return (var(ID)); - }; + } QString memo() const { return (var(MEMO)); - }; + } QString recon() const { return (var(RECON)); - }; + } QString value() const { return (var(VALUE)); - }; + } QString qty() const { return (var(QTY)); - }; + } QString acct() const { return (var(ACCT)); - }; + } private: const GncKvp getKvp(unsigned int i) const { return (m_kvpList[i]); }; // subsidiary objects/elements enum TemplateSplitSubEls {KVP, END_TemplateSplit_SELS }; - virtual GncObject *startSubEl(); - virtual void endSubEl(GncObject *); + GncObject *startSubEl() final override; + void endSubEl(GncObject *) final override; // data elements enum TemplateSplitDataEls {ID, MEMO, RECON, VALUE, QTY, ACCT, END_TemplateSplit_DELS }; }; // ************* GncSchedule******************************************** class GncFreqSpec; class GncRecurrence; class GncSchedDef; class GncSchedule : public GncObject { public: GncSchedule(); ~GncSchedule(); protected: friend class MyMoneyGncReader; // access data values QString name() const { return (var(NAME)); - }; + } QString enabled() const { return var(ENABLED); - }; + } QString autoCreate() const { return (var(AUTOC)); - }; + } QString autoCrNotify() const { return (var(AUTOCN)); - }; + } QString autoCrDays() const { return (var(AUTOCD)); - }; + } QString advCrDays() const { return (var(ADVCD)); - }; + } QString advCrRemindDays() const { return (var(ADVRD)); - }; + } QString instanceCount() const { return (var(INSTC)); - }; + } QString numOccurs() const { return (var(NUMOCC)); - }; + } QString remOccurs() const { return (var(REMOCC)); - }; + } QString templId() const { return (var(TEMPLID)); - }; + } QDate startDate() const { QDate x = QDate(); return (m_vpStartDate == NULL ? x : m_vpStartDate->date()); - }; + } QDate lastDate() const { QDate x = QDate(); return (m_vpLastDate == NULL ? x : m_vpLastDate->date()); - }; + } QDate endDate() const { QDate x = QDate(); return (m_vpEndDate == NULL ? x : m_vpEndDate->date()); - }; + } const GncFreqSpec *getFreqSpec() const { return (m_vpFreqSpec); - }; + } const GncSchedDef *getSchedDef() const { return (m_vpSchedDef); - }; + } private: // subsidiary objects/elements enum ScheduleSubEls {STARTDATE, LASTDATE, ENDDATE, FREQ, RECURRENCE, DEFINST, END_Schedule_SELS }; - virtual GncObject *startSubEl(); - virtual void endSubEl(GncObject *); - virtual void terminate(); + GncObject *startSubEl() final override; + void endSubEl(GncObject *) final override; + void terminate() final override; // data elements enum ScheduleDataEls {NAME, ENABLED, AUTOC, AUTOCN, AUTOCD, ADVCD, ADVRD, INSTC, NUMOCC, REMOCC, TEMPLID, END_Schedule_DELS }; GncDate *m_vpStartDate, *m_vpLastDate, *m_vpEndDate; GncFreqSpec *m_vpFreqSpec; mutable QList m_vpRecurrence; // gnc handles multiple occurrences GncSchedDef *m_vpSchedDef; }; // ************* GncFreqSpec******************************************** class GncFreqSpec : public GncObject { public: GncFreqSpec(); ~GncFreqSpec(); protected: friend class MyMoneyGncReader; // access data values (only interval type used at present) QString intervalType() const { return (var(INTVT)); - }; + } private: // subsidiary objects/elements enum FreqSpecSubEls {COMPO, END_FreqSpec_SELS }; - virtual GncObject *startSubEl(); - virtual void endSubEl(GncObject *); + GncObject *startSubEl() final override; + void endSubEl(GncObject *) final override; // data elements enum FreqSpecDataEls {INTVT, MONTHLY, DAILY, WEEKLY, INTVI, INTVO, INTVD, END_FreqSpec_DELS}; - virtual void terminate(); + void terminate() final override; mutable QList m_fsList; }; // ************* GncRecurrence******************************************** // this object replaces GncFreqSpec from Gnucash 2.2 onwards class GncRecurrence : public GncObject { public: GncRecurrence(); ~GncRecurrence(); protected: friend class MyMoneyGncReader; // access data values QDate startDate() const { QDate x = QDate(); return (m_vpStartDate == NULL ? x : m_vpStartDate->date()); - }; + } QString mult() const { return (var(MULT)); - }; + } QString periodType() const { return (var(PERIODTYPE)); - }; + } QString getFrequency() const; private: // subsidiary objects/elements enum RecurrenceSubEls {STARTDATE, END_Recurrence_SELS }; - virtual GncObject *startSubEl(); - virtual void endSubEl(GncObject *); + GncObject *startSubEl() final override; + void endSubEl(GncObject *) final override; // data elements enum RecurrenceDataEls {MULT, PERIODTYPE, END_Recurrence_DELS}; - virtual void terminate(); + void terminate() final override; GncDate *m_vpStartDate; }; // ************* GncSchedDef******************************************** // This is a sub-object of GncSchedule, (sx:deferredInstance) function currently unknown class GncSchedDef : public GncObject { public: GncSchedDef(); ~GncSchedDef(); protected: friend class MyMoneyGncReader; private: // subsidiary objects/elements }; // **************************************************************************************** /** XML Reader The XML reader is an implementation of the Qt SAX2 XML parser. It determines the type of object represented by the XMl, and calls the appropriate object functions */ // ***************************************************************************************** class XmlReader : public QXmlDefaultHandler { protected: friend class MyMoneyGncReader; XmlReader(MyMoneyGncReader *pM); // keep pointer to 'main' void processFile(QIODevice*); // main entry point of reader // define xml content handler functions - bool startDocument(); - bool startElement(const QString&, const QString&, const QString&, const QXmlAttributes&); - bool endElement(const QString&, const QString&, const QString&); - bool characters(const QString &); - bool endDocument(); + bool startDocument() final override; + bool startElement(const QString&, const QString&, const QString&, const QXmlAttributes&) final override; + bool endElement(const QString&, const QString&, const QString&) final override; + bool characters(const QString &) final override; + bool endDocument() final override; private: QXmlInputSource *m_source; QXmlSimpleReader *m_reader; QStack m_os; // stack of sub objects GncObject *m_co; // current object, for ease of coding (=== m_os.top) MyMoneyGncReader *pMain; // the 'main' pointer, to pass on to objects bool m_headerFound; // check for gnc-v2 header #ifdef _GNCFILEANON int lastType; // 0 = start element, 1 = data, 2 = end element int indentCount; #endif // _GNCFILEANON }; /** MyMoneyGncReader - Main class for this module Controls overall operation of the importer */ #ifndef _GNCFILEANON class MyMoneyGncReader : public IMyMoneyOperationsFormat { #else class MyMoneyGncReader { #endif // _GNCFILEANON public: MyMoneyGncReader(); virtual ~MyMoneyGncReader(); /** * Import a GnuCash XML file * * @param pDevice : pointer to GnuCash file * @param storage : pointer to MyMoneySerialize storage * * @return void * */ #ifndef _GNCFILEANON - void readFile(QIODevice* pDevice, MyMoneyStorageMgr *storage); // main entry point, IODevice is gnucash file - void writeFile(QIODevice*, MyMoneyStorageMgr*) { + void readFile(QIODevice* pDevice, MyMoneyStorageMgr *storage) final override; // main entry point, IODevice is gnucash file + void writeFile(QIODevice*, MyMoneyStorageMgr*) final override { return ; - }; // dummy entry needed by kmymoneywiew. we will not be writing + } // dummy entry needed by kmymoneywiew. we will not be writing #else void readFile(QString, QString); #endif // _GNCFILEANON QTextCodec *m_decoder; protected: friend class GncObject; // pity we can't just say GncObject. And compiler doesn't like multiple friends on one line... friend class GncFile; // there must be a better way... friend class GncDate; friend class GncCmdtySpec; friend class GncKvp; friend class GncLot; friend class GncCountData; friend class GncCommodity; friend class GncPrice; friend class GncAccount; friend class GncTransaction; friend class GncSplit; friend class GncTemplateTransaction; friend class GncTemplateSplit; friend class GncSchedule; friend class GncFreqSpec; friend class GncRecurrence; friend class XmlReader; #ifndef _GNCFILEANON /** functions to convert gnc objects to our equivalent */ void convertCommodity(const GncCommodity *); void convertPrice(const GncPrice *); void convertAccount(const GncAccount *); void convertTransaction(const GncTransaction *); void convertSplit(const GncSplit *); void saveTemplateTransaction(GncTransaction *t) { m_templateList.append(t); }; void convertSchedule(const GncSchedule *); void convertFreqSpec(const GncFreqSpec *); void convertRecurrence(const GncRecurrence *); #else /** functions to convert gnc objects to our equivalent */ void convertCommodity(const GncCommodity *) { return; }; void convertPrice(const GncPrice *) { return; }; void convertAccount(const GncAccount *) { return; }; void convertTransaction(const GncTransaction *) { return; }; void convertSplit(const GncSplit *) { return; }; void saveTemplateTransaction(GncTransaction *t) { return; }; void convertSchedule(const GncSchedule *) { return; }; void convertFreqSpec(const GncFreqSpec *) { return; }; #endif // _GNCFILEANON /** to post messages for final report */ void postMessage(const QString&, const unsigned int, const char *); void postMessage(const QString&, const unsigned int, const char *, const char *); void postMessage(const QString&, const unsigned int, const char *, const char *, const char *); void postMessage(const QString&, const unsigned int, const QStringList&); - void setProgressCallback(void(*callback)(int, int, const QString&)); + void setProgressCallback(void(*callback)(int, int, const QString&)) final override; void signalProgress(int current, int total, const QString& = ""); /** user options */ /** Scheduled Transactions Due to differences in implementation, it is not always possible to import scheduled transactions correctly. Though best efforts are made, it may be that some imported transactions cause problems within kmymoney. An attempt is made within the importer to identify potential problem transactions, and setting this option will cause them to be dropped from the file. A report of which were dropped, and why, will be produced. m_dropSuspectSchedules - drop suspect scheduled transactions */ bool m_dropSuspectSchedules; /** Investments In kmymoney, all accounts representing investments (stocks, shares, bonds, etc.) must have an associated investment account (e.g. a broker account). The stock account holds the share balance, the investment account a money balance. Gnucash does not do this, so we cannot automate this function. If you have investments, you must select one of the following options. 0 - create a separate investment account for each stock with the same name as the stock 1 - create a single investment account to hold all stocks - you will be asked for a name 2 - create multiple investment accounts - you will be asked for a name for each stock N.B. :- option 2 doesn't really work quite as desired at present */ unsigned int m_investmentOption; /** Online quotes The user has the option to use the Finance::Quote system, as used by GnuCash, to retrieve online share price quotes */ bool m_useFinanceQuote; /** Tx Notes handling Under some usage conditions, non-split GnuCash transactions may contain residual, usually incorrect, memo data which is not normally visible to the user. When imported into KMyMoney however, due to display differences, this data can become visible. Often, these transactions will have a Notes field describing the real purpose of the transaction. If this option is selected, these notes, if present, will be used to override the extraneous memo data." */ bool m_useTxNotes; // set gnucash counts (not always accurate!) void setGncCommodityCount(int i) { m_gncCommodityCount = i; - }; + } void setGncAccountCount(int i) { m_gncAccountCount = i; - }; + } void setGncTransactionCount(int i) { m_gncTransactionCount = i; - }; + } void setGncScheduleCount(int i) { m_gncScheduleCount = i; - }; + } void setSmallBusinessFound(bool b) { m_smallBusinessFound = b; - }; + } void setBudgetsFound(bool b) { m_budgetsFound = b; - }; + } void setLotsFound(bool b) { m_lotsFound = b; - }; + } /* Debug Options If you don't know what these are, best leave them alone. gncdebug - produce general debug messages xmldebug - produce a trace of the gnucash file XML bAnonymize - hide personal data (account names, payees, etc., randomize money amounts) */ bool gncdebug; // general debug messages bool xmldebug; // xml trace bool bAnonymize; // anonymize input static double m_fileHideFactor; // an overall anonymization factor to be applied to all items bool developerDebug; private: void setOptions(); // to set user options from dialog void setFileHideFactor(); // the following handles the gnucash indicator for a bad value (-1/0) which causes us probs QString convBadValue(QString gncValue) const { return (gncValue == "-1/0" ? "0/1" : gncValue); - }; + } #ifndef _GNCFILEANON MyMoneyTransaction convertTemplateTransaction(const QString&, const GncTransaction *); void convertTemplateSplit(const QString&, const GncTemplateSplit *); #endif // _GNCFILEANON // wind up when all done void terminate(); QString buildReportSection(const QString&); bool writeReportToFile(const QList&); // main storage #ifndef _GNCFILEANON MyMoneyStorageMgr *m_storage; #else QTextStream oStream; #endif // _GNCFILEANON XmlReader *m_xr; /** to hold the callback pointer for the progress bar */ void (*m_progressCallback)(int, int, const QString&); // a map of which versions of the various elements (objects) we can import map_elementVersions m_versionList; // counters holding count data from the Gnc 'count-data' section int m_gncCommodityCount; int m_gncAccountCount; int m_gncTransactionCount; int m_gncScheduleCount; // flags indicating detection of features not (yet?) supported bool m_smallBusinessFound; bool m_budgetsFound; bool m_lotsFound; /** counters for reporting */ int m_commodityCount; int m_priceCount; int m_accountCount; int m_transactionCount; int m_templateCount; int m_scheduleCount; #ifndef _GNCFILEANON // counters for error reporting int m_ccCount, m_orCount, m_scCount; // currency counter QMap m_currencyCount; /** * Map gnucash vs. Kmm ids for accounts, equities, schedules, price sources */ QMap m_mapIds; QString m_rootId; // save the root id for terminate() QMap m_mapEquities; QMap m_mapSchedules; QMap m_mapSources; /** * A list of stock accounts (gnc ids) which will be held till the end so we can implement the user's investment option */ QList m_stockList; /** * Temporary storage areas for transaction processing */ QString m_txCommodity; // save commodity for current transaction QString m_txPayeeId; // gnc has payee at tx level, we need it at split level QDate m_txDatePosted; // ditto for post date QString m_txChequeNo; // ditto for cheque number /** In kmm, the order of splits is critical to some operations. These * areas will hold the splits until we've read them all */ QList m_splitList, m_liabilitySplitList, m_otherSplitList; bool m_potentialTransfer; // to determine whether this might be a transfer /** Schedules are processed through 3 different functions, any of which may set this flag */ bool m_suspectSchedule; /** * A holding area for template txs while we're waiting for the schedules */ QList m_templateList; /** Hold a list of suspect schedule ids for later processing? */ QList m_suspectList; /** * To hold message data till final report */ QMap m_messageList; /** * Internal utility functions */ QString createPayee(const QString&); // create a payee and return it's id QString createOrphanAccount(const QString&); // create unknown account and return the id QDate incrDate(QDate lastDate, unsigned char interval, unsigned int intervalCount); // for date calculations MyMoneyAccount checkConsistency(MyMoneyAccount& parent, MyMoneyAccount& child); // gnucash is sometimes TOO flexible void checkInvestmentOption(QString stockId); // implement user investment option void getPriceSource(MyMoneySecurity stock, QString gncSource); /** * This method loads all known currencies and saves them to the storage */ void loadAllCurrencies(); #endif // _GNCFILEANON }; #endif // MYMONEYGNCREADER_H diff --git a/kmymoney/plugins/interfaces/kmmimportinterface.h b/kmymoney/plugins/interfaces/kmmimportinterface.h index 619ac2205..4e5cb23b8 100644 --- a/kmymoney/plugins/interfaces/kmmimportinterface.h +++ b/kmymoney/plugins/interfaces/kmmimportinterface.h @@ -1,54 +1,54 @@ /*************************************************************************** kmmimportinterface.h ------------------- begin : Mon Apr 14 2008 copyright : (C) 2008 Thomas Baumgart email : ipwizard@users.sourceforge.net (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 KMMIMPORTINTERFACE_H #define KMMIMPORTINTERFACE_H // ---------------------------------------------------------------------------- // QT Includes #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes #include "importinterface.h" namespace KMyMoneyPlugin { /** * This class represents the implementation of the * ImportInterface. */ class KMMImportInterface : public ImportInterface { Q_OBJECT public: explicit KMMImportInterface(QObject* parent, const char* name = 0); - ~KMMImportInterface() {} + ~KMMImportInterface() override {} - QUrl selectFile(const QString& title, const QString& path, const QString& mask, QFileDialog::FileMode mode, QWidget *widget) const; + QUrl selectFile(const QString& title, const QString& path, const QString& mask, QFileDialog::FileMode mode, QWidget *widget) const final override; }; } // namespace #endif diff --git a/kmymoney/plugins/interfaces/kmmstatementinterface.h b/kmymoney/plugins/interfaces/kmmstatementinterface.h index a4fef5a2d..11768c5ee 100644 --- a/kmymoney/plugins/interfaces/kmmstatementinterface.h +++ b/kmymoney/plugins/interfaces/kmmstatementinterface.h @@ -1,72 +1,72 @@ /*************************************************************************** kmmstatementinterface.h ------------------- begin : Wed Jan 5 2005 copyright : (C) 2005 Thomas Baumgart email : ipwizard@users.sourceforge.net (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 KMMSTATEMENTINTERFACE_H #define KMMSTATEMENTINTERFACE_H // ---------------------------------------------------------------------------- // QT Includes // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes class MyMoneyAccount; class MyMoneyKeyValueContainer; #include "statementinterface.h" namespace KMyMoneyPlugin { /** * This class represents the implementation of the * StatementInterface. */ class KMMStatementInterface : public StatementInterface { Q_OBJECT public: explicit KMMStatementInterface(QObject* parent, const char* name = 0); ~KMMStatementInterface() {} /** * This method imports a MyMoneyStatement into the engine */ - bool import(const MyMoneyStatement& s, bool silent = false); + bool import(const MyMoneyStatement& s, bool silent = false) final override; /** * This method returns the account for a given @a key - @a value pair. * If the account is not found in the list of accounts, MyMoneyAccount() * is returned. The @a key - @a value pair can be in the account's kvp * container or the account's online settings kvp container. */ - MyMoneyAccount account(const QString& key, const QString& value) const; + MyMoneyAccount account(const QString& key, const QString& value) const final override; /** * This method stores the online parameters in @a kvps used by the plugin * with the account @a acc. */ - void setAccountOnlineParameters(const MyMoneyAccount&acc, const MyMoneyKeyValueContainer& kvps) const; + void setAccountOnlineParameters(const MyMoneyAccount&acc, const MyMoneyKeyValueContainer& kvps) const final override; }; } // namespace #endif diff --git a/kmymoney/plugins/kbanking/dialogs/kbmapaccount.h b/kmymoney/plugins/kbanking/dialogs/kbmapaccount.h index cc8fcac62..70b794545 100644 --- a/kmymoney/plugins/kbanking/dialogs/kbmapaccount.h +++ b/kmymoney/plugins/kbanking/dialogs/kbmapaccount.h @@ -1,60 +1,60 @@ /*************************************************************************** begin : Mon Mar 01 2004 copyright : (C) 2004 by Martin Preuss email : martin@libchipcard.de *************************************************************************** * * * 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 KBMAPACCOUNT_H #define KBMAPACCOUNT_H #include #include class KBankingExt; class KBMapAccount: public QDialog { Q_OBJECT public: KBMapAccount(KBankingExt *kb, const char *bankCode, const char *accountId, QWidget* parent = 0, Qt::WindowFlags fl = 0); ~KBMapAccount(); AB_ACCOUNT *getAccount(); - void accept(); + void accept() final override; protected Q_SLOTS: void slotSelectionChanged(); void slotHelpClicked(); private: /// \internal d-pointer class. struct Private; /// \internal d-pointer instance. Private* const d; /* KMyMoneyBanking *_banking; AB_ACCOUNT *_account; KBAccountListView *_accountList; */ }; #endif /* QBANKING_MAPACCOUNT_H */ diff --git a/kmymoney/plugins/kbanking/gwenkdegui.h b/kmymoney/plugins/kbanking/gwenkdegui.h index 0961f8d44..186a26428 100644 --- a/kmymoney/plugins/kbanking/gwenkdegui.h +++ b/kmymoney/plugins/kbanking/gwenkdegui.h @@ -1,91 +1,91 @@ /* * A gwenhywfar gui for aqbanking using KDE widgets * Copyright 2014 - 2016 Christian David * * 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) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * 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 GWENKDEGUI_H #define GWENKDEGUI_H #include #include "gwen-gui-qt5/qt5_gui.hpp" /** * @brief Gwenhywfar Gui by KDE * * * @author Christian David */ class gwenKdeGui : public QT5_Gui { public: gwenKdeGui(); ~gwenKdeGui(); - virtual int getPassword(uint32_t flags, + int getPassword(uint32_t flags, const char *token, const char *title, const char *text, char *buffer, int minLen, int maxLen, - uint32_t guiid); + uint32_t guiid) final override; }; /** * @brief Helper class which is receiver for several signals */ class gwenKdeGuiTanResult : public QObject { Q_OBJECT public: explicit gwenKdeGuiTanResult(QObject* parent = nullptr) : QObject(parent), m_tan(QString()), m_aborted(false) {} - virtual ~gwenKdeGuiTanResult() {} + ~gwenKdeGuiTanResult() {} QString tan() { return m_tan; } bool aborted() { return m_aborted; } public Q_SLOTS: void abort() { m_aborted = true; } void acceptTan(QString tan) { m_tan = tan; m_aborted = false; } private: QString m_tan; bool m_aborted; }; #endif // GWENKDEGUI_H diff --git a/kmymoney/plugins/kbanking/kbanking.cpp b/kmymoney/plugins/kbanking/kbanking.cpp index a5f4a36e4..9fa64177e 100644 --- a/kmymoney/plugins/kbanking/kbanking.cpp +++ b/kmymoney/plugins/kbanking/kbanking.cpp @@ -1,1520 +1,1520 @@ /*************************************************************************** * Copyright 2004 Martin Preuss aquamaniac@users.sourceforge.net * * Copyright 2009 Cristian Onet onet.cristian@gmail.com * * Copyright 2010 Thomas Baumgart ipwizard@users.sourceforge.net * * Copyright 2015 Christian David christian-david@web.de * * * * 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) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of version 3 of the license. * * * * 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 #include "kbanking.h" #include // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include #include //! @todo remove @c #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include #include #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // Library Includes #include #include #include #include #include #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "mymoney/onlinejob.h" #include "kbaccountsettings.h" #include "kbmapaccount.h" #include "mymoneyfile.h" #include "onlinejobadministration.h" #include "kmymoneyview.h" #include "kbpickstartdate.h" #include "mymoneyinstitution.h" #include "gwenkdegui.h" #include "gwenhywfarqtoperators.h" #include "aqbankingkmmoperators.h" #include "mymoneystatement.h" #include "statementinterface.h" #include "viewinterface.h" #ifdef KMM_DEBUG // Added an option to open the chipTanDialog from the menu for debugging purposes #include "chiptandialog.h" #endif class KBanking::Private { public: Private() : passwordCacheTimer(nullptr), jobList(), fileId() { QString gwenProxy = QString::fromLocal8Bit(qgetenv("GWEN_PROXY")); if (gwenProxy.isEmpty()) { std::unique_ptr cfg = std::unique_ptr(new KConfig("kioslaverc")); QRegExp exp("(\\w+://)?([^/]{2}.+:\\d+)"); QString proxy; KConfigGroup grp = cfg->group("Proxy Settings"); int type = grp.readEntry("ProxyType", 0); switch (type) { case 0: // no proxy break; case 1: // manual specified proxy = grp.readEntry("httpsProxy"); qDebug("KDE https proxy setting is '%s'", qPrintable(proxy)); if (exp.exactMatch(proxy)) { proxy = exp.cap(2); qDebug("Setting GWEN_PROXY to '%s'", qPrintable(proxy)); if (!qputenv("GWEN_PROXY", qPrintable(proxy))) { qDebug("Unable to setup GWEN_PROXY"); } } break; default: // other currently not supported qDebug("KDE proxy setting of type %d not supported", type); break; } } } /** * KMyMoney asks for accounts over and over again which causes a lot of "Job not supported with this account" error messages. * This function filters messages with that string. */ static int gwenLogHook(GWEN_GUI* gui, const char* domain, GWEN_LOGGER_LEVEL level, const char* message) { Q_UNUSED(gui); Q_UNUSED(domain); Q_UNUSED(level); const char* messageToFilter = "Job not supported with this account"; if (strstr(message, messageToFilter) != 0) return 1; return 0; } QTimer *passwordCacheTimer; QMap jobList; QString fileId; }; KBanking::KBanking(QObject *parent, const QVariantList &args) : OnlinePluginExtended(parent, "kbanking") , d(new Private) , m_configAction(nullptr) , m_importAction(nullptr) , m_kbanking(nullptr) , m_accountSettings(nullptr) { Q_UNUSED(args) qDebug("Plugins: kbanking loaded"); } KBanking::~KBanking() { delete d; qDebug("Plugins: kbanking unloaded"); } void KBanking::plug() { m_kbanking = new KBankingExt(this, "KMyMoney"); d->passwordCacheTimer = new QTimer(this); d->passwordCacheTimer->setSingleShot(true); d->passwordCacheTimer->setInterval(60000); connect(d->passwordCacheTimer, &QTimer::timeout, this, &KBanking::slotClearPasswordCache); if (m_kbanking) { if (AB_Banking_HasConf4(m_kbanking->getCInterface())) { qDebug("KBankingPlugin: No AqB4 config found."); if (AB_Banking_HasConf3(m_kbanking->getCInterface())) { qDebug("KBankingPlugin: No AqB3 config found."); if (!AB_Banking_HasConf2(m_kbanking->getCInterface())) { qDebug("KBankingPlugin: AqB2 config found - converting."); AB_Banking_ImportConf2(m_kbanking->getCInterface()); } } else { qDebug("KBankingPlugin: AqB3 config found - converting."); AB_Banking_ImportConf3(m_kbanking->getCInterface()); } } //! @todo when is gwenKdeGui deleted? gwenKdeGui *gui = new gwenKdeGui(); GWEN_Gui_SetGui(gui->getCInterface()); GWEN_Logger_SetLevel(0, GWEN_LoggerLevel_Warning); if (m_kbanking->init() == 0) { // Tell the host application to load my GUI component setComponentName("kbanking", "KBanking"); setXMLFile("kbanking.rc"); qDebug("Plugins: kbanking pluged"); // get certificate handling and dialog settings management AB_Gui_Extend(gui->getCInterface(), m_kbanking->getCInterface()); // create actions createActions(); // load protocol conversion list loadProtocolConversion(); GWEN_Logger_SetLevel(AQBANKING_LOGDOMAIN, GWEN_LoggerLevel_Warning); GWEN_Gui_SetLogHookFn(GWEN_Gui_GetGui(), &KBanking::Private::gwenLogHook); } else { qWarning("Could not initialize KBanking online banking interface"); delete m_kbanking; m_kbanking = 0; } } } void KBanking::unplug() { d->passwordCacheTimer->deleteLater(); if (m_kbanking) { m_kbanking->fini(); delete m_kbanking; qDebug("Plugins: kbanking unpluged"); } } void KBanking::loadProtocolConversion() { if (m_kbanking) { m_protocolConversionMap = { {"aqhbci", "HBCI"}, {"aqofxconnect", "OFX"}, {"aqyellownet", "YellowNet"}, {"aqgeldkarte", "Geldkarte"}, {"aqdtaus", "DTAUS"} }; } } void KBanking::protocols(QStringList& protocolList) const { if (m_kbanking) { std::list list = m_kbanking->getActiveProviders(); std::list::iterator it; for (it = list.begin(); it != list.end(); ++it) { // skip the dummy if (*it == "aqnone") continue; QMap::const_iterator it_m; it_m = m_protocolConversionMap.find((*it).c_str()); if (it_m != m_protocolConversionMap.end()) protocolList << (*it_m); else protocolList << (*it).c_str(); } } } QWidget* KBanking::accountConfigTab(const MyMoneyAccount& acc, QString& name) { const MyMoneyKeyValueContainer& kvp = acc.onlineBankingSettings(); name = i18n("Online settings"); if (m_kbanking) { m_accountSettings = new KBAccountSettings(acc, 0); m_accountSettings->loadUi(kvp); return m_accountSettings; } QLabel* label = new QLabel(i18n("KBanking module not correctly initialized"), 0); label->setAlignment(Qt::AlignVCenter | Qt::AlignHCenter); return label; } MyMoneyKeyValueContainer KBanking::onlineBankingSettings(const MyMoneyKeyValueContainer& current) { MyMoneyKeyValueContainer kvp(current); kvp["provider"] = objectName().toLower(); if (m_accountSettings) { m_accountSettings->loadKvp(kvp); } return kvp; } void KBanking::createActions() { QAction *settings_aqbanking = actionCollection()->addAction("settings_aqbanking"); settings_aqbanking->setText(i18n("Configure Aq&Banking...")); connect(settings_aqbanking, &QAction::triggered, this, &KBanking::slotSettings); QAction *file_import_aqbanking = actionCollection()->addAction("file_import_aqbanking"); file_import_aqbanking->setText(i18n("AqBanking importer...")); connect(file_import_aqbanking, &QAction::triggered, this, &KBanking::slotImport); Q_CHECK_PTR(viewInterface()); connect(viewInterface(), &KMyMoneyPlugin::ViewInterface::viewStateChanged, action("file_import_aqbanking"), &QAction::setEnabled); #ifdef KMM_DEBUG QAction *openChipTanDialog = actionCollection()->addAction("open_chiptan_dialog"); openChipTanDialog->setText("Open ChipTan Dialog"); connect(openChipTanDialog, &QAction::triggered, [&](){ auto dlg = new chipTanDialog(); dlg->setHhdCode("0F04871100030333555414312C32331D"); dlg->setInfoText("

Test Graphic for debugging

The encoded data is

Account Number: 335554
Amount: 1,23

"); connect(dlg, &QDialog::accepted, dlg, &chipTanDialog::deleteLater); connect(dlg, &QDialog::rejected, dlg, &chipTanDialog::deleteLater); dlg->show(); }); #endif } void KBanking::slotSettings() { if (m_kbanking) { GWEN_DIALOG* dlg = AB_SetupDialog_new(m_kbanking->getCInterface()); if (dlg == NULL) { DBG_ERROR(0, "Could not create setup dialog."); return; } if (GWEN_Gui_ExecDialog(dlg, 0) == 0) { DBG_ERROR(0, "Aborted by user"); GWEN_Dialog_free(dlg); return; } GWEN_Dialog_free(dlg); } } bool KBanking::mapAccount(const MyMoneyAccount& acc, MyMoneyKeyValueContainer& settings) { bool rc = false; if (m_kbanking && !acc.id().isEmpty()) { m_kbanking->askMapAccount(acc); // at this point, the account should be mapped // so we search it and setup the account reference in the KMyMoney object AB_ACCOUNT* ab_acc; ab_acc = aqbAccount(acc); if (ab_acc) { MyMoneyAccount a(acc); setupAccountReference(a, ab_acc); settings = a.onlineBankingSettings(); rc = true; } } return rc; } AB_ACCOUNT* KBanking::aqbAccount(const MyMoneyAccount& acc) const { if (m_kbanking == 0) { return 0; } // certainly looking for an expense or income account does not make sense at this point // so we better get out right away if (acc.isIncomeExpense()) { return 0; } AB_ACCOUNT *ab_acc = AB_Banking_GetAccountByAlias(m_kbanking->getCInterface(), m_kbanking->mappingId(acc).toUtf8().data()); // if the account is not found, we temporarily scan for the 'old' mapping (the one w/o the file id) // in case we find it, we setup the new mapping in addition on the fly. if (!ab_acc && acc.isAssetLiability()) { ab_acc = AB_Banking_GetAccountByAlias(m_kbanking->getCInterface(), acc.id().toUtf8().data()); if (ab_acc) { qDebug("Found old mapping for '%s' but not new. Setup new mapping", qPrintable(acc.name())); m_kbanking->setAccountAlias(ab_acc, m_kbanking->mappingId(acc).toUtf8().constData()); // TODO at some point in time, we should remove the old mapping } } return ab_acc; } AB_ACCOUNT* KBanking::aqbAccount(const QString& accountId) const { MyMoneyAccount account = MyMoneyFile::instance()->account(accountId); return aqbAccount(account); } QString KBanking::stripLeadingZeroes(const QString& s) const { QString rc(s); QRegExp exp("^(0*)([^0].*)"); if (exp.exactMatch(s)) { rc = exp.cap(2); } return rc; } void KBanking::setupAccountReference(const MyMoneyAccount& acc, AB_ACCOUNT* ab_acc) { MyMoneyKeyValueContainer kvp; if (ab_acc) { QString accountNumber = stripLeadingZeroes(AB_Account_GetAccountNumber(ab_acc)); QString routingNumber = stripLeadingZeroes(AB_Account_GetBankCode(ab_acc)); QString val = QString("%1-%2").arg(routingNumber, accountNumber); if (val != acc.onlineBankingSettings().value("kbanking-acc-ref")) { - MyMoneyKeyValueContainer kvp; + kvp.clear(); // make sure to keep our own previous settings const QMap& vals = acc.onlineBankingSettings().pairs(); QMap::const_iterator it_p; for (it_p = vals.begin(); it_p != vals.end(); ++it_p) { if (QString(it_p.key()).startsWith("kbanking-")) { kvp.setValue(it_p.key(), *it_p); } } kvp.setValue("kbanking-acc-ref", val); kvp.setValue("provider", objectName().toLower()); setAccountOnlineParameters(acc, kvp); } } else { // clear the connection setAccountOnlineParameters(acc, kvp); } } bool KBanking::accountIsMapped(const MyMoneyAccount& acc) { return aqbAccount(acc) != 0; } bool KBanking::updateAccount(const MyMoneyAccount& acc) { return updateAccount(acc, false); } bool KBanking::updateAccount(const MyMoneyAccount& acc, bool moreAccounts) { if (!m_kbanking) return false; bool rc = false; if (!acc.id().isEmpty()) { AB_JOB *job = 0; int rv; /* get AqBanking account */ AB_ACCOUNT *ba = aqbAccount(acc); // Update the connection between the KMyMoney account and the AqBanking equivalent. // If the account is not found anymore ba == 0 and the connection is removed. setupAccountReference(acc, ba); if (!ba) { KMessageBox::error(0, i18n("" "The given application account %1 " "has not been mapped to an online " "account." "", acc.name()), i18n("Account Not Mapped")); } else { bool enqueJob = true; if (acc.onlineBankingSettings().value("kbanking-txn-download") != "no") { /* create getTransactions job */ job = AB_JobGetTransactions_new(ba); rv = AB_Job_CheckAvailability(job); if (rv) { DBG_ERROR(0, "Job \"GetTransactions\" is not available (%d)", rv); KMessageBox::error(0, i18n("" "The update job is not supported by the " "bank/account/backend.\n" ""), i18n("Job not Available")); AB_Job_free(job); job = 0; } if (job) { int days = AB_JobGetTransactions_GetMaxStoreDays(job); QDate qd; if (days > 0) { GWEN_TIME *ti1; GWEN_TIME *ti2; ti1 = GWEN_CurrentTime(); ti2 = GWEN_Time_fromSeconds(GWEN_Time_Seconds(ti1) - (60 * 60 * 24 * days)); GWEN_Time_free(ti1); ti1 = ti2; int year, month, day; if (GWEN_Time_GetBrokenDownDate(ti1, &day, &month, &year)) { DBG_ERROR(0, "Bad date"); qd = QDate(); } else qd = QDate(year, month + 1, day); GWEN_Time_free(ti1); } // get last statement request date from application account object // and start from a few days before if the date is valid QDate lastUpdate = QDate::fromString(acc.value("lastImportedTransactionDate"), Qt::ISODate); if (lastUpdate.isValid()) lastUpdate = lastUpdate.addDays(-3); int dateOption = acc.onlineBankingSettings().value("kbanking-statementDate").toInt(); switch (dateOption) { case 0: // Ask user break; case 1: // No date qd = QDate(); break; case 2: // Last download qd = lastUpdate; break; case 3: // First possible // qd is already setup break; } // the pick start date option dialog is needed in // case the dateOption is 0 or the date option is > 1 // and the qd is invalid if (dateOption == 0 || (dateOption > 1 && !qd.isValid())) { QPointer psd = new KBPickStartDate(m_kbanking, qd, lastUpdate, acc.name(), lastUpdate.isValid() ? 2 : 3, 0, true); if (psd->exec() == QDialog::Accepted) { qd = psd->date(); } else { enqueJob = false; } delete psd; } if (enqueJob) { if (qd.isValid()) { GWEN_TIME *ti1; ti1 = GWEN_Time_new(qd.year(), qd.month() - 1, qd.day(), 0, 0, 0, 0); AB_JobGetTransactions_SetFromTime(job, ti1); GWEN_Time_free(ti1); } rv = m_kbanking->enqueueJob(job); if (rv) { DBG_ERROR(0, "Error %d", rv); KMessageBox::error(0, i18n("" "Could not enqueue the job.\n" ""), i18n("Error")); } } AB_Job_free(job); } } if (enqueJob) { /* create getBalance job */ job = AB_JobGetBalance_new(ba); rv = AB_Job_CheckAvailability(job); if (!rv) rv = m_kbanking->enqueueJob(job); else rv = 0; AB_Job_free(job); if (rv) { DBG_ERROR(0, "Error %d", rv); KMessageBox::error(0, i18n("" "Could not enqueue the job.\n" ""), i18n("Error")); } else { rc = true; emit queueChanged(); } } } } // make sure we have at least one job in the queue before sending it if (!moreAccounts && m_kbanking->getEnqueuedJobs().size() > 0) executeQueue(); return rc; } void KBanking::executeQueue() { if (m_kbanking && m_kbanking->getEnqueuedJobs().size() > 0) { AB_IMEXPORTER_CONTEXT *ctx; ctx = AB_ImExporterContext_new(); int rv = m_kbanking->executeQueue(ctx); if (!rv) { m_kbanking->importContext(ctx, 0); } else { DBG_ERROR(0, "Error: %d", rv); } AB_ImExporterContext_free(ctx); } } /** @todo improve error handling, e.g. by adding a .isValid to nationalTransfer * @todo use new onlineJob system */ void KBanking::sendOnlineJob(QList& jobs) { Q_CHECK_PTR(m_kbanking); m_onlineJobQueue.clear(); QList unhandledJobs; if (!jobs.isEmpty()) { foreach (onlineJob job, jobs) { if (sepaOnlineTransfer::name() == job.task()->taskName()) { onlineJobTyped typedJob(job); enqueTransaction(typedJob); job = typedJob; } else { job.addJobMessage(onlineJobMessage(eMyMoney::OnlineJob::MessageType::Error, "KBanking", "Cannot handle this request")); unhandledJobs.append(job); } m_onlineJobQueue.insert(m_kbanking->mappingId(job), job); } executeQueue(); } jobs = m_onlineJobQueue.values() + unhandledJobs; m_onlineJobQueue.clear(); } QStringList KBanking::availableJobs(QString accountId) { try { MyMoneyAccount acc = MyMoneyFile::instance()->account(accountId); QString id = MyMoneyFile::instance()->value("kmm-id"); if(id != d->fileId) { d->jobList.clear(); d->fileId = id; } } catch (const MyMoneyException&) { // Exception usually means account was not found return QStringList(); } if(d->jobList.contains(accountId)) { return d->jobList[accountId]; } QStringList list; AB_ACCOUNT* abAccount = aqbAccount(accountId); if (!abAccount) { return list; } // Check availableJobs // sepa transfer AB_JOB* abJob = AB_JobSepaTransfer_new(abAccount); if (AB_Job_CheckAvailability(abJob) == 0) list.append(sepaOnlineTransfer::name()); AB_Job_free(abJob); d->jobList[accountId] = list; return list; } /** @brief experimenting with QScopedPointer and aqBanking pointers */ class QScopedPointerAbJobDeleter { public: static void cleanup(AB_JOB* job) { AB_Job_free(job); } }; /** @brief experimenting with QScopedPointer and aqBanking pointers */ class QScopedPointerAbAccountDeleter { public: static void cleanup(AB_ACCOUNT* account) { AB_Account_free(account); } }; IonlineTaskSettings::ptr KBanking::settings(QString accountId, QString taskName) { AB_ACCOUNT* abAcc = aqbAccount(accountId); if (abAcc == 0) return IonlineTaskSettings::ptr(); if (sepaOnlineTransfer::name() == taskName) { // Get limits for sepaonlinetransfer QScopedPointer abJob(AB_JobSepaTransfer_new(abAcc)); if (AB_Job_CheckAvailability(abJob.data()) != 0) return IonlineTaskSettings::ptr(); const AB_TRANSACTION_LIMITS* limits = AB_Job_GetFieldLimits(abJob.data()); return AB_TransactionLimits_toSepaOnlineTaskSettings(limits).dynamicCast(); } return IonlineTaskSettings::ptr(); } bool KBanking::enqueTransaction(onlineJobTyped& job) { /* get AqBanking account */ const QString accId = job.constTask()->responsibleAccount(); AB_ACCOUNT *abAccount = aqbAccount(accId); if (!abAccount) { job.addJobMessage(onlineJobMessage(eMyMoney::OnlineJob::MessageType::Warning, "KBanking", i18n("" "The given application account %1 " "has not been mapped to an online " "account." "", MyMoneyFile::instance()->account(accId).name()))); return false; } //setupAccountReference(acc, ba); // needed? AB_JOB *abJob = AB_JobSepaTransfer_new(abAccount); int rv = AB_Job_CheckAvailability(abJob); if (rv) { qDebug("AB_ERROR_OFFSET is %i", AB_ERROR_OFFSET); job.addJobMessage(onlineJobMessage(eMyMoney::OnlineJob::MessageType::Error, "AqBanking", QString("Sepa credit transfers for account \"%1\" are not available, error code %2.").arg(MyMoneyFile::instance()->account(accId).name(), rv) ) ); return false; } AB_TRANSACTION *AbTransaction = AB_Transaction_new(); // Recipient payeeIdentifiers::ibanBic beneficiaryAcc = job.constTask()->beneficiaryTyped(); AB_Transaction_SetRemoteName(AbTransaction, GWEN_StringList_fromQString(beneficiaryAcc.ownerName())); AB_Transaction_SetRemoteIban(AbTransaction, beneficiaryAcc.electronicIban().toUtf8().constData()); AB_Transaction_SetRemoteBic(AbTransaction, beneficiaryAcc.fullStoredBic().toUtf8().constData()); // Origin Account AB_Transaction_SetLocalAccount(AbTransaction, abAccount); // Purpose QStringList qPurpose = job.constTask()->purpose().split('\n'); GWEN_STRINGLIST *purpose = GWEN_StringList_fromQStringList(qPurpose); AB_Transaction_SetPurpose(AbTransaction, purpose); GWEN_StringList_free(purpose); // Reference // AqBanking duplicates the string. This should be safe. AB_Transaction_SetEndToEndReference(AbTransaction, job.constTask()->endToEndReference().toUtf8().constData()); // Other Fields AB_Transaction_SetTextKey(AbTransaction, job.constTask()->textKey()); AB_Transaction_SetValue(AbTransaction, AB_Value_fromMyMoneyMoney(job.constTask()->value())); /** @todo LOW remove Debug info */ qDebug() << "SetTransaction: " << AB_Job_SetTransaction(abJob, AbTransaction); GWEN_DB_NODE *gwenNode = AB_Job_GetAppData(abJob); GWEN_DB_SetCharValue(gwenNode, GWEN_DB_FLAGS_DEFAULT, "kmmOnlineJobId", m_kbanking->mappingId(job).toLatin1().constData()); qDebug() << "Enqueue: " << m_kbanking->enqueueJob(abJob); //delete localAcc; return true; } void KBanking::startPasswordTimer() { if (d->passwordCacheTimer->isActive()) d->passwordCacheTimer->stop(); d->passwordCacheTimer->start(); } void KBanking::slotClearPasswordCache() { m_kbanking->clearPasswordCache(); } void KBanking::slotImport() { if (!m_kbanking->interactiveImport()) qWarning("Error on import dialog"); } bool KBanking::importStatement(const MyMoneyStatement& s) { return statementInterface()->import(s); } MyMoneyAccount KBanking::account(const QString& key, const QString& value) const { return statementInterface()->account(key, value); } void KBanking::setAccountOnlineParameters(const MyMoneyAccount& acc, const MyMoneyKeyValueContainer& kvps) const { return statementInterface()->setAccountOnlineParameters(acc, kvps); } KBankingExt::KBankingExt(KBanking* parent, const char* appname, const char* fname) : AB_Banking(appname, fname) , m_parent(parent) , _jobQueue(0) { m_sepaKeywords = {QString::fromUtf8("SEPA-BASISLASTSCHRIFT"), QString::fromUtf8("SEPA-ÜBERWEISUNG")}; } int KBankingExt::init() { int rv = AB_Banking::init(); if (rv < 0) return rv; rv = onlineInit(); if (rv) { fprintf(stderr, "Error on online init (%d).\n", rv); AB_Banking::fini(); return rv; } _jobQueue = AB_Job_List2_new(); return 0; } int KBankingExt::fini() { if (_jobQueue) { AB_Job_List2_FreeAll(_jobQueue); _jobQueue = 0; } const int rv = onlineFini(); if (rv) { AB_Banking::fini(); return rv; } return AB_Banking::fini(); } int KBankingExt::executeQueue(AB_IMEXPORTER_CONTEXT *ctx) { m_parent->startPasswordTimer(); int rv = AB_Banking::executeJobs(_jobQueue, ctx); if (rv != 0) { qDebug() << "Sending queue by aqbanking got error no " << rv; } /** check result of each job */ AB_JOB_LIST2_ITERATOR* jobIter = AB_Job_List2_First(_jobQueue); if (jobIter) { AB_JOB* abJob = AB_Job_List2Iterator_Data(jobIter); while (abJob) { GWEN_DB_NODE* gwenNode = AB_Job_GetAppData(abJob); if (gwenNode == 0) { qWarning("Executed AB_Job without KMyMoney id"); abJob = AB_Job_List2Iterator_Next(jobIter); break; } QString jobIdent = QString::fromUtf8(GWEN_DB_GetCharValue(gwenNode, "kmmOnlineJobId", 0, "")); onlineJob job = m_parent->m_onlineJobQueue.value(jobIdent); if (job.isNull()) { // It should not be possiblie that this will happen (only if AqBanking fails heavily). //! @todo correct exception text qWarning("Executed a job which was not in queue. Please inform the KMyMoney developers."); abJob = AB_Job_List2Iterator_Next(jobIter); continue; } AB_JOB_STATUS abStatus = AB_Job_GetStatus(abJob); if (abStatus == AB_Job_StatusSent || abStatus == AB_Job_StatusPending || abStatus == AB_Job_StatusFinished || abStatus == AB_Job_StatusError || abStatus == AB_Job_StatusUnknown) job.setJobSend(); if (abStatus == AB_Job_StatusFinished) job.setBankAnswer(onlineJob::acceptedByBank); else if (abStatus == AB_Job_StatusError || abStatus == AB_Job_StatusUnknown) job.setBankAnswer(onlineJob::sendingError); job.addJobMessage(onlineJobMessage(eMyMoney::OnlineJob::MessageType::Debug, "KBanking", "Job was processed")); m_parent->m_onlineJobQueue.insert(jobIdent, job); abJob = AB_Job_List2Iterator_Next(jobIter); } AB_Job_List2Iterator_free(jobIter); } AB_JOB_LIST2 *oldQ = _jobQueue; _jobQueue = AB_Job_List2_new(); AB_Job_List2_FreeAll(oldQ); emit m_parent->queueChanged(); m_parent->startPasswordTimer(); return rv; } void KBankingExt::clearPasswordCache() { /* clear password DB */ GWEN_Gui_SetPasswordStatus(NULL, NULL, GWEN_Gui_PasswordStatus_Remove, 0); } std::list KBankingExt::getEnqueuedJobs() { AB_JOB_LIST2 *ll; std::list rl; ll = _jobQueue; if (ll && AB_Job_List2_GetSize(ll)) { AB_JOB *j; AB_JOB_LIST2_ITERATOR *it; it = AB_Job_List2_First(ll); assert(it); j = AB_Job_List2Iterator_Data(it); assert(j); while (j) { rl.push_back(j); j = AB_Job_List2Iterator_Next(it); } AB_Job_List2Iterator_free(it); } return rl; } int KBankingExt::enqueueJob(AB_JOB *j) { assert(_jobQueue); assert(j); AB_Job_Attach(j); AB_Job_List2_PushBack(_jobQueue, j); return 0; } int KBankingExt::dequeueJob(AB_JOB *j) { assert(_jobQueue); AB_Job_List2_Remove(_jobQueue, j); AB_Job_free(j); emit m_parent->queueChanged(); return 0; } void KBankingExt::transfer() { //m_parent->transfer(); } bool KBankingExt::askMapAccount(const MyMoneyAccount& acc) { MyMoneyFile* file = MyMoneyFile::instance(); QString bankId; QString accountId; // extract some information about the bank. if we have a sortcode // (BLZ) we display it, otherwise the name is enough. try { const MyMoneyInstitution &bank = file->institution(acc.institutionId()); bankId = bank.name(); if (!bank.sortcode().isEmpty()) bankId = bank.sortcode(); } catch (const MyMoneyException &e) { // no bank assigned, we just leave the field emtpy } // extract account information. if we have an account number // we show it, otherwise the name will be displayed accountId = acc.number(); if (accountId.isEmpty()) accountId = acc.name(); // do the mapping. the return value of this method is either // true, when the user mapped the account or false, if he // decided to quit the dialog. So not really a great thing // to present some more information. KBMapAccount *w; w = new KBMapAccount(this, bankId.toUtf8().constData(), accountId.toUtf8().constData()); if (w->exec() == QDialog::Accepted) { AB_ACCOUNT *a; a = w->getAccount(); assert(a); DBG_NOTICE(0, "Mapping application account \"%s\" to " "online account \"%s/%s\"", qPrintable(acc.name()), AB_Account_GetBankCode(a), AB_Account_GetAccountNumber(a)); // TODO remove the following line once we don't need backward compatibility setAccountAlias(a, acc.id().toUtf8().constData()); qDebug("Setup mapping to '%s'", acc.id().toUtf8().constData()); setAccountAlias(a, mappingId(acc).toUtf8().constData()); qDebug("Setup mapping to '%s'", mappingId(acc).toUtf8().constData()); delete w; return true; } delete w; return false; } QString KBankingExt::mappingId(const MyMoneyObject& object) const { QString id = MyMoneyFile::instance()->storageId() + QLatin1Char('-') + object.id(); // AqBanking does not handle the enclosing parens, so we remove it id.remove('{'); id.remove('}'); return id; } bool KBankingExt::interactiveImport() { AB_IMEXPORTER_CONTEXT *ctx; GWEN_DIALOG *dlg; int rv; ctx = AB_ImExporterContext_new(); dlg = AB_ImporterDialog_new(getCInterface(), ctx, NULL); if (dlg == NULL) { DBG_ERROR(0, "Could not create importer dialog."); AB_ImExporterContext_free(ctx); return false; } rv = GWEN_Gui_ExecDialog(dlg, 0); if (rv == 0) { DBG_ERROR(0, "Aborted by user"); GWEN_Dialog_free(dlg); AB_ImExporterContext_free(ctx); return false; } if (!importContext(ctx, 0)) { DBG_ERROR(0, "Error on importContext"); GWEN_Dialog_free(dlg); AB_ImExporterContext_free(ctx); return false; } GWEN_Dialog_free(dlg); AB_ImExporterContext_free(ctx); return true; } const AB_ACCOUNT_STATUS* KBankingExt::_getAccountStatus(AB_IMEXPORTER_ACCOUNTINFO *ai) { const AB_ACCOUNT_STATUS *ast; const AB_ACCOUNT_STATUS *best; best = 0; ast = AB_ImExporterAccountInfo_GetFirstAccountStatus(ai); while (ast) { if (!best) best = ast; else { const GWEN_TIME *tiBest; const GWEN_TIME *ti; tiBest = AB_AccountStatus_GetTime(best); ti = AB_AccountStatus_GetTime(ast); if (!tiBest) { best = ast; } else { if (ti) { double d; /* we have two times, compare them */ d = GWEN_Time_Diff(ti, tiBest); if (d > 0) /* newer */ best = ast; } } } ast = AB_ImExporterAccountInfo_GetNextAccountStatus(ai); } /* while */ return best; } void KBankingExt::_xaToStatement(MyMoneyStatement &ks, const MyMoneyAccount& acc, const AB_TRANSACTION *t) { const GWEN_STRINGLIST *sl; QString s; QString memo; const char *p; const AB_VALUE *val; const GWEN_TIME *ti; const GWEN_TIME *startTime = 0; MyMoneyStatement::Transaction kt; unsigned long h; kt.m_fees = MyMoneyMoney(); // bank's transaction id p = AB_Transaction_GetFiId(t); if (p) kt.m_strBankID = QString("ID ") + QString::fromUtf8(p); // payee s.truncate(0); sl = AB_Transaction_GetRemoteName(t); if (sl) { GWEN_STRINGLISTENTRY *se; se = GWEN_StringList_FirstEntry(sl); while (se) { p = GWEN_StringListEntry_Data(se); assert(p); s += QString::fromUtf8(p); se = GWEN_StringListEntry_Next(se); } // while } kt.m_strPayee = s; // memo // The variable 's' contains the old method of extracting // the memo which added a linefeed after each part received // from AqBanking. The new variable 'memo' does not have // this inserted linefeed. We keep the variable 's' to // construct the hash-value to retrieve the reference s.truncate(0); sl = AB_Transaction_GetPurpose(t); if (sl) { GWEN_STRINGLISTENTRY *se; bool insertLineSep = false; se = GWEN_StringList_FirstEntry(sl); while (se) { p = GWEN_StringListEntry_Data(se); assert(p); if (insertLineSep) s += '\n'; insertLineSep = true; s += QString::fromUtf8(p).trimmed(); memo += QString::fromUtf8(p).trimmed(); se = GWEN_StringListEntry_Next(se); } // while // Sparda / Netbank hack: the software these banks use stores // parts of the payee name in the beginning of the purpose field // in case the payee name exceeds the 27 character limit. This is // the case, when one of the strings listed in m_sepaKeywords is part // of the purpose fields but does not start at the beginning. In this // case, the part leading up to the keyword is to be treated as the // tail of the payee. Also, a blank is inserted after the keyword. QSet::const_iterator itk; for (itk = m_sepaKeywords.constBegin(); itk != m_sepaKeywords.constEnd(); ++itk) { int idx = s.indexOf(*itk); if (idx >= 0) { if (idx > 0) { // re-add a possibly removed blank to name if (kt.m_strPayee.length() < 27) kt.m_strPayee += ' '; kt.m_strPayee += s.left(idx); s = s.mid(idx); } s = QString("%1 %2").arg(*itk).arg(s.mid((*itk).length())); // now do the same for 'memo' except for updating the payee idx = memo.indexOf(*itk); if (idx >= 0) { if (idx > 0) { memo = memo.mid(idx); } } memo = QString("%1 %2").arg(*itk).arg(memo.mid((*itk).length())); break; } } // in case we have some SEPA fields filled with information // we add them to the memo field p = AB_Transaction_GetEndToEndReference(t); if (p) { s += QString(", EREF: %1").arg(p); if(memo.length()) memo.append('\n'); memo.append(QString("EREF: %1").arg(p)); } p = AB_Transaction_GetCustomerReference(t); if (p) { s += QString(", CREF: %1").arg(p); if(memo.length()) memo.append('\n'); memo.append(QString("CREF: %1").arg(p)); } p = AB_Transaction_GetMandateId(t); if (p) { s += QString(", MREF: %1").arg(p); if(memo.length()) memo.append('\n'); memo.append(QString("MREF: %1").arg(p)); } p = AB_Transaction_GetCreditorSchemeId(t); if (p) { s += QString(", CRED: %1").arg(p); if(memo.length()) memo.append('\n'); memo.append(QString("CRED: %1").arg(p)); } p = AB_Transaction_GetOriginatorIdentifier(t); if (p) { s += QString(", DEBT: %1").arg(p); if(memo.length()) memo.append('\n'); memo.append(QString("DEBT: %1").arg(p)); } } kt.m_strMemo = memo; // calculate the hash code and start with the payee info // and append the memo field h = MyMoneyTransaction::hash(kt.m_strPayee.trimmed()); h = MyMoneyTransaction::hash(s, h); // see, if we need to extract the payee from the memo field const MyMoneyKeyValueContainer& kvp = acc.onlineBankingSettings(); QString rePayee = kvp.value("kbanking-payee-regexp"); if (!rePayee.isEmpty() && kt.m_strPayee.isEmpty()) { QString reMemo = kvp.value("kbanking-memo-regexp"); QStringList exceptions = kvp.value("kbanking-payee-exceptions").split(';', QString::SkipEmptyParts); bool needExtract = true; QStringList::const_iterator it_s; for (it_s = exceptions.constBegin(); needExtract && it_s != exceptions.constEnd(); ++it_s) { QRegExp exp(*it_s, Qt::CaseInsensitive); if (exp.indexIn(kt.m_strMemo) != -1) { needExtract = false; } } if (needExtract) { QRegExp expPayee(rePayee, Qt::CaseInsensitive); QRegExp expMemo(reMemo, Qt::CaseInsensitive); if (expPayee.indexIn(kt.m_strMemo) != -1) { kt.m_strPayee = expPayee.cap(1); if (expMemo.indexIn(kt.m_strMemo) != -1) { kt.m_strMemo = expMemo.cap(1); } } } } kt.m_strPayee = kt.m_strPayee.trimmed(); // date ti = AB_Transaction_GetDate(t); if (!ti) ti = AB_Transaction_GetValutaDate(t); if (ti) { int year, month, day; if (!startTime) startTime = ti; /*else { dead code if (GWEN_Time_Diff(ti, startTime) < 0) startTime = ti; }*/ if (!GWEN_Time_GetBrokenDownDate(ti, &day, &month, &year)) { kt.m_datePosted = QDate(year, month + 1, day); } } else { DBG_WARN(0, "No date for transaction"); } // value val = AB_Transaction_GetValue(t); if (val) { if (ks.m_strCurrency.isEmpty()) { p = AB_Value_GetCurrency(val); if (p) ks.m_strCurrency = p; } else { p = AB_Value_GetCurrency(val); if (p) s = p; if (ks.m_strCurrency.toLower() != s.toLower()) { // TODO: handle currency difference DBG_ERROR(0, "Mixed currencies currently not allowed"); } } kt.m_amount = MyMoneyMoney(AB_Value_GetValueAsDouble(val)); // The initial implementation of this feature was based on // a denominator of 100. Since the denominator might be // different nowadays, we make sure to use 100 for the // duplicate detection QString tmpVal = kt.m_amount.formatMoney(100, false); tmpVal.remove(QRegExp("[,\\.]")); tmpVal += QLatin1String("/100"); h = MyMoneyTransaction::hash(tmpVal, h); } else { DBG_WARN(0, "No value for transaction"); } if (startTime) { int year, month, day; if (!GWEN_Time_GetBrokenDownDate(startTime, &day, &month, &year)) { QDate d(year, month + 1, day); if (!ks.m_dateBegin.isValid()) ks.m_dateBegin = d; else if (d < ks.m_dateBegin) ks.m_dateBegin = d; if (!ks.m_dateEnd.isValid()) ks.m_dateEnd = d; else if (d > ks.m_dateEnd) ks.m_dateEnd = d; } } else { DBG_WARN(0, "No date in current transaction"); } // add information about remote account to memo in case we have something const char *remoteAcc = AB_Transaction_GetRemoteAccountNumber(t); const char *remoteBankCode = AB_Transaction_GetRemoteBankCode(t); if (remoteAcc && remoteBankCode) { kt.m_strMemo += QString("\n%1/%2").arg(remoteBankCode, remoteAcc); } // make hash value unique in case we don't have one already if (kt.m_strBankID.isEmpty()) { QString hashBase; hashBase.sprintf("%s-%07lx", qPrintable(kt.m_datePosted.toString(Qt::ISODate)), h); int idx = 1; QString hash; for (;;) { hash = QString("%1-%2").arg(hashBase).arg(idx); QMap::const_iterator it; it = m_hashMap.constFind(hash); if (it == m_hashMap.constEnd()) { m_hashMap[hash] = true; break; } ++idx; } kt.m_strBankID = QString("%1-%2").arg(acc.id()).arg(hash); } // store transaction ks.m_listTransactions += kt; } bool KBankingExt::importAccountInfo(AB_IMEXPORTER_ACCOUNTINFO *ai, uint32_t /*flags*/) { const char *p; DBG_INFO(0, "Importing account..."); // account number MyMoneyStatement ks; p = AB_ImExporterAccountInfo_GetAccountNumber(ai); if (p) { ks.m_strAccountNumber = m_parent->stripLeadingZeroes(p); } p = AB_ImExporterAccountInfo_GetBankCode(ai); if (p) { ks.m_strRoutingNumber = m_parent->stripLeadingZeroes(p); } MyMoneyAccount kacc = m_parent->account("kbanking-acc-ref", QString("%1-%2").arg(ks.m_strRoutingNumber, ks.m_strAccountNumber)); ks.m_accountId = kacc.id(); // account name p = AB_ImExporterAccountInfo_GetAccountName(ai); if (p) ks.m_strAccountName = p; // account type switch (AB_ImExporterAccountInfo_GetType(ai)) { case AB_AccountType_Bank: ks.m_eType = eMyMoney::Statement::Type::Savings; break; case AB_AccountType_CreditCard: ks.m_eType = eMyMoney::Statement::Type::CreditCard; break; case AB_AccountType_Checking: ks.m_eType = eMyMoney::Statement::Type::Checkings; break; case AB_AccountType_Savings: ks.m_eType = eMyMoney::Statement::Type::Savings; break; case AB_AccountType_Investment: ks.m_eType = eMyMoney::Statement::Type::Investment; break; case AB_AccountType_Cash: default: ks.m_eType = eMyMoney::Statement::Type::None; } // account status const AB_ACCOUNT_STATUS* ast = _getAccountStatus(ai); if (ast) { const AB_BALANCE *bal; bal = AB_AccountStatus_GetBookedBalance(ast); if (!bal) bal = AB_AccountStatus_GetNotedBalance(ast); if (bal) { const AB_VALUE* val = AB_Balance_GetValue(bal); if (val) { DBG_INFO(0, "Importing balance"); ks.m_closingBalance = AB_Value_toMyMoneyMoney(val); p = AB_Value_GetCurrency(val); if (p) ks.m_strCurrency = p; } const GWEN_TIME* ti = AB_Balance_GetTime(bal); if (ti) { int year, month, day; if (!GWEN_Time_GetBrokenDownDate(ti, &day, &month, &year)) ks.m_dateEnd = QDate(year, month + 1, day); } else { DBG_WARN(0, "No time for balance"); } } else { DBG_WARN(0, "No account balance"); } } else { DBG_WARN(0, "No account status"); } // clear hash map m_hashMap.clear(); // get all transactions const AB_TRANSACTION* t = AB_ImExporterAccountInfo_GetFirstTransaction(ai); while (t) { _xaToStatement(ks, kacc, t); t = AB_ImExporterAccountInfo_GetNextTransaction(ai); } // import them if (!m_parent->importStatement(ks)) { if (KMessageBox::warningYesNo(0, i18n("Error importing statement. Do you want to continue?"), i18n("Critical Error")) == KMessageBox::No) { DBG_ERROR(0, "User aborted"); return false; } } return true; } K_PLUGIN_FACTORY_WITH_JSON(KBankingFactory, "kbanking.json", registerPlugin();) #include "kbanking.moc" diff --git a/kmymoney/plugins/kbanking/kbanking.h b/kmymoney/plugins/kbanking/kbanking.h index 8af6638d1..d7072f98a 100644 --- a/kmymoney/plugins/kbanking/kbanking.h +++ b/kmymoney/plugins/kbanking/kbanking.h @@ -1,253 +1,253 @@ /*************************************************************************** * Copyright 2004 Martin Preuss aquamaniac@users.sourceforge.net * * Copyright 2009 Cristian Onet onet.cristian@gmail.com * * Copyright 2010 Thomas Baumgart ipwizard@users.sourceforge.net * * Copyright 2016 Christian David christian-david@web.de * * * * 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) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of version 3 of the license. * * * * 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 KBANKING_H #define KBANKING_H #ifdef HAVE_CONFIG_H #include #endif // ---------------------------------------------------------------------------- // QT Includes #include // ---------------------------------------------------------------------------- // KDE & Library Includes class KAction; class QBanking; class KBankingExt; class KBAccountSettings; #include // ---------------------------------------------------------------------------- // Project Includes #include "kmymoneyplugin.h" #include "onlinepluginextended.h" #include "mymoneyaccount.h" #include "mymoneykeyvaluecontainer.h" #include "mymoney/onlinejobtyped.h" #include "onlinetasks/sepa/tasks/sepaonlinetransfer.h" #include "banking.hpp" /** * This class represents the KBanking plugin towards KMymoney. * All GUI related issues are handled in this object. */ class MyMoneyStatement; class KBanking : public KMyMoneyPlugin::OnlinePluginExtended { friend class KBankingExt; Q_OBJECT Q_INTERFACES(KMyMoneyPlugin::OnlinePluginExtended KMyMoneyPlugin::OnlinePlugin) public: explicit KBanking(QObject *parent, const QVariantList &args); ~KBanking() override; bool importStatement(const MyMoneyStatement& s); MyMoneyAccount account(const QString& key, const QString& value) const; void setAccountOnlineParameters(const MyMoneyAccount& acc, const MyMoneyKeyValueContainer& kvps) const; void protocols(QStringList& protocolList) const override; QStringList availableJobs(QString accountId) override; IonlineTaskSettings::ptr settings(QString accountId, QString taskName) override; void sendOnlineJob(QList& jobs) override; void plug() override; void unplug() override; private: /** * creates the action objects available through the application menus */ void createActions(); /** * creates the context menu */ void createContextMenu(); /** * checks whether a given KMyMoney account with id @p id is * already mapped or not. * * @param acc reference to KMyMoney account object * @retval false account is not mapped to an AqBanking account * @retval true account is mapped to an AqBanking account */ bool accountIsMapped(const MyMoneyAccount& acc); /** * sets up the reference string consisting out of BLZ and account number * in the KMyMoney object so that we can find it later on when importing data. */ void setupAccountReference(const MyMoneyAccount& acc, AB_ACCOUNT* ab_acc); /** * Returns the value of the parameter @a s with all leading 0's stripped. */ QString stripLeadingZeroes(const QString& s) const; /** * Prefills the protocol conversion list to allow mapping * of AqBanking internal names to external names */ void loadProtocolConversion(); /** * Creates an additional tab widget for the account edit dialog * to represent the necessary parameters for online banking * through AqBanking. */ QWidget* accountConfigTab(const MyMoneyAccount& acc, QString& name) override; /** * Stores the configuration data kept in the widgets created * in accountConfigTab() and returns them in a key value container * The current settings are accessible through the reference to * @a current. */ MyMoneyKeyValueContainer onlineBankingSettings(const MyMoneyKeyValueContainer& current) override; /** * Called by the application to map the KMyMoney account @a acc * to an AqBanking account. Calls KBanking to set up AqBanking mappings. * Returns the necessary settings for the plugin in @a settings and * @a true if the mapping was successful. */ bool mapAccount(const MyMoneyAccount& acc, MyMoneyKeyValueContainer& settings) override; /** * This method translates a MyMoneyAccount to the corresponding AB_ACCOUNT object pointer. * If no mapped account can be detected, it returns 0. */ AB_ACCOUNT* aqbAccount(const MyMoneyAccount& acc) const; /** * This is a convenient method for aqbAccount if you have KMyMoney's account id only. */ AB_ACCOUNT* aqbAccount(const QString& accountId) const; /** * Called by the application framework to update the * KMyMoney account @a acc with data from the online source. * Store the jobs in the outbox in case @a moreAccounts is true */ bool updateAccount(const MyMoneyAccount& acc, bool moreAccounts) override; /** * Kept for backward compatibility. Use * updateAccount(const MyMoneyAccount& acc, bool moreAccounts) instead. * * @deprecated */ bool updateAccount(const MyMoneyAccount& acc) DEPRECATED; /** * Trigger the password cache timer */ void startPasswordTimer(); bool enqueTransaction(onlineJobTyped& job); protected Q_SLOTS: void slotSettings(); void slotImport(); void slotClearPasswordCache(); void executeQueue(); Q_SIGNALS: void queueChanged(); private: class Private; Private* const d; KAction* m_configAction; KAction* m_importAction; KBankingExt* m_kbanking; QMap m_protocolConversionMap; KBAccountSettings* m_accountSettings; /** * @brief @ref onlineJob "onlineJobs" which are executed at the moment * Key is onlineJob->id(). This container is used during execution of jobs. */ QMap m_onlineJobQueue; }; /** * This class is the special implementation to glue the AB_Banking class * with the KMyMoneyPlugin structure. */ class KBankingExt : public AB_Banking { friend class KBanking; public: explicit KBankingExt(KBanking* parent, const char* appname, const char* fname = 0); virtual ~KBankingExt() {} int executeQueue(AB_IMEXPORTER_CONTEXT *ctx); int enqueueJob(AB_JOB *j); int dequeueJob(AB_JOB *j); std::list getEnqueuedJobs(); void transfer(); virtual bool interactiveImport(); protected: - int init(); - int fini(); + int init() final override; + int fini() final override; bool askMapAccount(const MyMoneyAccount& acc); QString mappingId(const MyMoneyObject& object) const; - bool importAccountInfo(AB_IMEXPORTER_ACCOUNTINFO *ai, uint32_t flags); + bool importAccountInfo(AB_IMEXPORTER_ACCOUNTINFO *ai, uint32_t flags) final override; const AB_ACCOUNT_STATUS* _getAccountStatus(AB_IMEXPORTER_ACCOUNTINFO *ai); void _xaToStatement(MyMoneyStatement &ks, const MyMoneyAccount&, const AB_TRANSACTION *t); void clearPasswordCache(); private: KBanking* m_parent; QMap m_hashMap; AB_JOB_LIST2 *_jobQueue; QSet m_sepaKeywords; }; #endif // KBANKING diff --git a/kmymoney/plugins/kbanking/tasksettings/credittransfersettingsbase.h b/kmymoney/plugins/kbanking/tasksettings/credittransfersettingsbase.h index bd8e111ff..ca9c001ba 100644 --- a/kmymoney/plugins/kbanking/tasksettings/credittransfersettingsbase.h +++ b/kmymoney/plugins/kbanking/tasksettings/credittransfersettingsbase.h @@ -1,165 +1,165 @@ /* * This file is part of KMyMoney, A Personal Finance Manager by KDE * Copyright (C) 2013 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 CREDITTRANSFERSETTINGSBASE_H #define CREDITTRANSFERSETTINGSBASE_H #include "onlinetasks/sepa/tasks/sepaonlinetransfer.h" /** * @brief Base class for sepaCreditTransfer and germanCreditTransfer settings * * @internal Both credit transfers have similar fields */ class creditTransferSettingsBase : public sepaOnlineTransfer::settings { public: creditTransferSettingsBase() : _purposeMaxLines(0) , _purposeLineLength(0) , _purposeMinLength(0) , _recipientNameMaxLines(0) , _recipientNameLength(0) , _recipientNameMinLength(0) , _payeeNameMaxLines(0) , _payeeNameLength(0) , _payeeNameMinLength(0) , m_endToEndReferenceLength(0) {} // Limits getter - int purposeMaxLines() const { + int purposeMaxLines() const final override { return _purposeMaxLines; } - int purposeLineLength() const { + int purposeLineLength() const final override { return _purposeLineLength; } - int purposeMinLength() const { + int purposeMinLength() const final override { return _purposeMinLength; } - int recipientNameLineLength() const { + int recipientNameLineLength() const final override { return _recipientNameLength; } - int recipientNameMinLength() const { + int recipientNameMinLength() const final override { return _recipientNameMinLength; } - int payeeNameLineLength() const { + int payeeNameLineLength() const final override { return _payeeNameLength; } - int payeeNameMinLength() const { + int payeeNameMinLength() const final override { return _payeeNameMinLength; } - QString allowedChars() const { + QString allowedChars() const final override { return _allowedChars; } - virtual int endToEndReferenceLength() const { + virtual int endToEndReferenceLength() const final override { return m_endToEndReferenceLength; } // Checker - bool checkPurposeCharset(const QString& string) const; - bool checkPurposeLineLength(const QString& purpose) const; - validators::lengthStatus checkPurposeLength(const QString& purpose) const; - bool checkPurposeMaxLines(const QString& purpose) const; + bool checkPurposeCharset(const QString& string) const final override; + bool checkPurposeLineLength(const QString& purpose) const final override; + validators::lengthStatus checkPurposeLength(const QString& purpose) const final override; + bool checkPurposeMaxLines(const QString& purpose) const final override; - validators::lengthStatus checkNameLength(const QString& name) const; - bool checkNameCharset(const QString& name) const; + validators::lengthStatus checkNameLength(const QString& name) const final override; + bool checkNameCharset(const QString& name) const final override; - validators::lengthStatus checkRecipientLength(const QString& name) const; - bool checkRecipientCharset(const QString& name) const; + validators::lengthStatus checkRecipientLength(const QString& name) const final override; + bool checkRecipientCharset(const QString& name) const final override; - virtual validators::lengthStatus checkEndToEndReferenceLength(const QString& reference) const; + virtual validators::lengthStatus checkEndToEndReferenceLength(const QString& reference) const final override; - virtual bool checkRecipientBic(const QString& bic) const; + virtual bool checkRecipientBic(const QString& bic) const final override; /** * @brief Checks if the bic is mandatory for the given iban * * For the check usually only the first two chars are needed. So you do not * need to validate the IBAN. * * There is no need to format fromIban or toIban in any way (it is trimmed automatically). */ - virtual bool isBicMandatory(const QString& fromIban, const QString& toIban) const; + virtual bool isBicMandatory(const QString& fromIban, const QString& toIban) const final override; validators::lengthStatus checkRecipientAccountNumber(const QString& accountNumber) const; validators::lengthStatus checkRecipientBankCode(const QString& bankCode) const; // Limits setter void setEndToEndReferenceLength(const int& length) { m_endToEndReferenceLength = length; } void setPurposeLimits(const int& lines, const int& lineLength, const int& minLength) { _purposeMaxLines = lines; _purposeLineLength = lineLength; _purposeMinLength = minLength; } void setRecipientNameLimits(const int& lines, const int& lineLength, const int& minLength) { _recipientNameMaxLines = lines; _recipientNameLength = lineLength; _recipientNameMinLength = minLength; } void setPayeeNameLimits(const int& lines, const int& lineLength, const int& minLength) { _payeeNameMaxLines = lines; _payeeNameLength = lineLength; _payeeNameMinLength = minLength; } void setAllowedChars(QString characters) { _allowedChars = characters; } private: /** @brief number of lines allowed in purpose */ int _purposeMaxLines; /** @brief number of chars allowed in each purpose line */ int _purposeLineLength; /** @brief Minimal number of chars needed for purpose */ int _purposeMinLength; /** @brief number of lines allowed for recipient name */ int _recipientNameMaxLines; /** @brief number of chars allowed in each recipient line */ int _recipientNameLength; /** @brief Minimal number of chars needed as recipient name */ int _recipientNameMinLength; /** @brief number of lines allowed for payee name */ int _payeeNameMaxLines; /** @brief number of chars allowed in each payee line */ int _payeeNameLength; /** @brief Minibal number of chars for payee name */ int _payeeNameMinLength; /** @brief characters allowd in purpose and recipient name */ QString _allowedChars; /** @brief Number of chars allowed for sepa reference */ int m_endToEndReferenceLength; }; #endif // CREDITTRANSFERSETTINGSBASE_H diff --git a/kmymoney/plugins/kbanking/widgets/chiptandialog.h b/kmymoney/plugins/kbanking/widgets/chiptandialog.h index 259a80c9e..ceb2403dd 100644 --- a/kmymoney/plugins/kbanking/widgets/chiptandialog.h +++ b/kmymoney/plugins/kbanking/widgets/chiptandialog.h @@ -1,77 +1,77 @@ /* * A tan input dialog for optical chipTan used in online banking * Copyright 2014 Christian David * * 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) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * 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 CHIPTANDIALOG_H #define CHIPTANDIALOG_H #include #include namespace Ui { class chipTanDialog; } class chipTanDialog : public QDialog { Q_OBJECT Q_PROPERTY(QString infoText READ infoText() WRITE setInfoText) Q_PROPERTY(QString hhdCode READ hhdCode() WRITE setHhdCode) Q_PROPERTY(int flickerFieldWidth READ flickerFieldWidth WRITE setFlickerFieldWidth) public: explicit chipTanDialog(QWidget* parent = 0); ~chipTanDialog(); enum Result { Accepted = 0, Rejected, InternalError }; QString infoText(); QString hhdCode(); QString tan(); int flickerFieldWidth(); public Q_SLOTS: - void accept(); - void reject(); + void accept() final override; + void reject() final override; void setInfoText(const QString&); void setHhdCode(const QString&); void setTanLimits(const int& minLength, const int& maxLength); void setFlickerFieldWidth(const int& width); void setFlickerFieldClockSetting(const int& width); private Q_SLOTS: void tanInputChanged(const QString&); void flickerFieldWidthChanged(const int& width); void flickerFieldClockSettingChanged(const int& takt); private: std::unique_ptr ui; QString m_tan; bool m_accepted; void setRootObjectProperty(const char* property, const QVariant& value); }; #endif // CHIPTANDIALOG_H diff --git a/kmymoney/plugins/kbanking/widgets/kbaccountlist.h b/kmymoney/plugins/kbanking/widgets/kbaccountlist.h index bb13f124a..f4d648873 100644 --- a/kmymoney/plugins/kbanking/widgets/kbaccountlist.h +++ b/kmymoney/plugins/kbanking/widgets/kbaccountlist.h @@ -1,74 +1,74 @@ /*************************************************************************** begin : Mon Mar 01 2004 copyright : (C) 2004 by Martin Preuss email : martin@libchipcard.de *************************************************************************** * * * 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 KBACCOUNTLIST_H #define KBACCOUNTLIST_H #include #include #include class KBAccountListView; class KBAccountListViewItem; class KBAccountListViewItem: public QTreeWidgetItem { private: AB_ACCOUNT *_account; void _populate(); - bool operator< (const QTreeWidgetItem & other) const; //!< correctly sort text columns, which contain numbers + bool operator< (const QTreeWidgetItem & other) const final override; //!< correctly sort text columns, which contain numbers public: KBAccountListViewItem(KBAccountListView *parent, AB_ACCOUNT *acc); KBAccountListViewItem(KBAccountListView *parent, QTreeWidgetItem *after, AB_ACCOUNT *acc); KBAccountListViewItem(const KBAccountListViewItem &item); virtual ~KBAccountListViewItem(); AB_ACCOUNT *getAccount(); }; class KBAccountListView: public QTreeWidget { private: public: explicit KBAccountListView(QWidget *parent = 0); virtual ~KBAccountListView(); void addAccount(AB_ACCOUNT *acc); void addAccounts(const std::list &accs); AB_ACCOUNT *getCurrentAccount(); std::list getSelectedAccounts(); std::list getSortedAccounts(); }; #endif /* QBANKING_ACCOUNTLIST_H */ diff --git a/kmymoney/plugins/ofx/import/dialogs/kofxdirectconnectdlg.cpp b/kmymoney/plugins/ofx/import/dialogs/kofxdirectconnectdlg.cpp index 47d37fcd9..afc459994 100644 --- a/kmymoney/plugins/ofx/import/dialogs/kofxdirectconnectdlg.cpp +++ b/kmymoney/plugins/ofx/import/dialogs/kofxdirectconnectdlg.cpp @@ -1,225 +1,225 @@ /*************************************************************************** kofxdirectconnectdlg.cpp ------------------- begin : Sat Nov 13 2004 copyright : (C) 2002 by Ace Jones email : acejones@users.sourceforge.net ***************************************************************************/ /*************************************************************************** * * * 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 "kofxdirectconnectdlg.h" #include "kmymoneysettings.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "mymoneyofxconnector.h" class KOfxDirectConnectDlg::Private { public: Private() : m_firstData(true) {} QFile m_fpTrace; bool m_firstData; }; KOfxDirectConnectDlg::KOfxDirectConnectDlg(const MyMoneyAccount& account, QWidget *parent) : KOfxDirectConnectDlgDecl(parent), d(new Private), m_tmpfile(0), m_connector(account), m_job(0) { } KOfxDirectConnectDlg::~KOfxDirectConnectDlg() { if (d->m_fpTrace.isOpen()) { d->m_fpTrace.close(); } delete m_tmpfile; delete d; } bool KOfxDirectConnectDlg::init() { show(); QByteArray request = m_connector.statementRequest(); if (request.isEmpty()) { hide(); return false; } // For debugging, dump out the request #if 0 QFile g("request.ofx"); g.open(QIODevice::WriteOnly); QTextStream(&g) << m_connector.url() << "\n" << QString(request); g.close(); #endif if (KMyMoneySettings::logOfxTransactions()) { QString logPath = KMyMoneySettings::logPath(); d->m_fpTrace.setFileName(QString("%1/ofxlog.txt").arg(logPath)); d->m_fpTrace.open(QIODevice::WriteOnly | QIODevice::Append); } if (d->m_fpTrace.isOpen()) { - QByteArray data = m_connector.url().toUtf8(); + QByteArray connectorData = m_connector.url().toUtf8(); d->m_fpTrace.write("url: ", 5); - d->m_fpTrace.write(data, strlen(data)); + d->m_fpTrace.write(connectorData, strlen(connectorData)); d->m_fpTrace.write("\n", 1); d->m_fpTrace.write("request:\n", 9); QByteArray trcData(request); // make local copy trcData.replace('\r', ""); // krazy:exclude=doublequote_chars d->m_fpTrace.write(trcData, trcData.size()); d->m_fpTrace.write("\n", 1); d->m_fpTrace.write("response:\n", 10); } qDebug("creating job"); m_job = KIO::http_post(QUrl(m_connector.url()), request, KIO::HideProgressInfo); // open the temp file. We come around here twice if init() is called twice if (m_tmpfile) { qDebug() << "Already connected, using " << m_tmpfile->fileName(); delete m_tmpfile; //delete otherwise we mem leak } m_tmpfile = new QTemporaryFile(); // for debugging purposes one might want to leave the temp file around // in order to achieve this, please uncomment the next line // m_tmpfile->setAutoRemove(false); if (!m_tmpfile->open()) { qWarning("Unable to open tempfile '%s' for download.", qPrintable(m_tmpfile->fileName())); return false; } m_job->addMetaData("content-type", "Content-type: application/x-ofx"); connect(m_job, SIGNAL(result(KJob*)), this, SLOT(slotOfxFinished(KJob*))); connect(m_job, SIGNAL(data(KIO::Job*,QByteArray)), this, SLOT(slotOfxData(KIO::Job*,QByteArray))); setStatus(QString("Contacting %1...").arg(m_connector.url())); kProgress1->setMaximum(3); kProgress1->setValue(1); return true; } void KOfxDirectConnectDlg::setStatus(const QString& _status) { textLabel1->setText(_status); qDebug() << "STATUS:" << _status; } void KOfxDirectConnectDlg::setDetails(const QString& _details) { qDebug() << "DETAILS: " << _details; } void KOfxDirectConnectDlg::slotOfxData(KIO::Job*, const QByteArray& _ba) { qDebug("Got %d bytes of data", _ba.size()); if (d->m_firstData) { setStatus("Connection established, retrieving data..."); setDetails(QString("Downloading data to %1...").arg(m_tmpfile->fileName())); kProgress1->setValue(kProgress1->value() + 1); d->m_firstData = false; } m_tmpfile->write(_ba); setDetails(QString("Got %1 bytes").arg(_ba.size())); if (d->m_fpTrace.isOpen()) { QByteArray trcData(_ba); trcData.replace('\r', ""); // krazy:exclude=doublequote_chars d->m_fpTrace.write(trcData, trcData.size()); } } void KOfxDirectConnectDlg::slotOfxFinished(KJob* /* e */) { qDebug("Job finished"); kProgress1->setValue(kProgress1->value() + 1); setStatus("Completed."); if (d->m_fpTrace.isOpen()) { d->m_fpTrace.write("\nCompleted\n\n\n\n", 14); } int error = m_job->error(); if (m_tmpfile) { qDebug("Closing tempfile"); m_tmpfile->close(); } qDebug("Tempfile closed"); if (error) { qDebug("Show error message"); m_job->uiDelegate()->showErrorMessage(); } else if (m_job->isErrorPage()) { qDebug("Process error page"); QString details; if (m_tmpfile) { QFile f(m_tmpfile->fileName()); if (f.open(QIODevice::ReadOnly)) { QTextStream stream(&f); QString line; while (!stream.atEnd()) { details += stream.readLine(); // line of text excluding '\n' } f.close(); qDebug() << "The HTTP request failed: " << details; } } KMessageBox::detailedSorry(this, i18n("The HTTP request failed."), details, i18nc("The HTTP request failed", "Failed")); } else if (m_tmpfile) { qDebug("Emit statementReady signal with '%s'", qPrintable(m_tmpfile->fileName())); emit statementReady(m_tmpfile->fileName()); qDebug("Return from signal statementReady() processing"); } delete m_tmpfile; m_tmpfile = 0; hide(); qDebug("Finishing slotOfxFinished"); } void KOfxDirectConnectDlg::reject() { if (m_job) m_job->kill(); if (m_tmpfile) { m_tmpfile->close(); delete m_tmpfile; m_tmpfile = 0; } QDialog::reject(); } diff --git a/kmymoney/plugins/ofx/import/dialogs/kofxdirectconnectdlg.h b/kmymoney/plugins/ofx/import/dialogs/kofxdirectconnectdlg.h index 9856a40b3..4656ba45e 100644 --- a/kmymoney/plugins/ofx/import/dialogs/kofxdirectconnectdlg.h +++ b/kmymoney/plugins/ofx/import/dialogs/kofxdirectconnectdlg.h @@ -1,96 +1,96 @@ /*************************************************************************** kofxdirectconnectdlg.h ------------------- begin : Sat Nov 13 2004 copyright : (C) 2002 by Ace Jones email : acejones@users.sourceforge.net ***************************************************************************/ /*************************************************************************** * * * 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 KOFXDIRECTCONNECTDLG_H #define KOFXDIRECTCONNECTDLG_H // ---------------------------------------------------------------------------- // QT Includes // ---------------------------------------------------------------------------- // KDE Includes class QTemporaryFile; class KJob; namespace KIO { class Job; class TransferJob; } // ---------------------------------------------------------------------------- // Project Includes #include "mymoneyofxconnector.h" #include "ui_kofxdirectconnectdlgdecl.h" /** @author ace jones */ class KOfxDirectConnectDlgDecl : public QDialog, public Ui::KOfxDirectConnectDlgDecl { public: explicit KOfxDirectConnectDlgDecl(QWidget *parent) : QDialog(parent) { setupUi(this); } }; class KOfxDirectConnectDlg : public KOfxDirectConnectDlgDecl { Q_OBJECT public: explicit KOfxDirectConnectDlg(const MyMoneyAccount&, QWidget *parent = 0); ~KOfxDirectConnectDlg(); /** * Initializes the download of OFX statement data. * * @returns true if download was initialized * @returns false if download was not started */ bool init(); Q_SIGNALS: /** * This signal is emitted when the statement is downloaded * and stored in file @a fname. */ void statementReady(const QString& fname); protected Q_SLOTS: void slotOfxFinished(KJob*); void slotOfxData(KIO::Job*, const QByteArray&); - virtual void reject(); + void reject() final override; protected: void setStatus(const QString& _status); void setDetails(const QString& _details); private: /// \internal d-pointer class. class Private; /// \internal d-pointer instance. Private* const d; QTemporaryFile* m_tmpfile; MyMoneyOfxConnector m_connector; KIO::TransferJob* m_job; }; #endif // KOFXDIRECTCONNECTDLG_H diff --git a/kmymoney/plugins/onlinepluginextended.h b/kmymoney/plugins/onlinepluginextended.h index 967ccc29e..e79491e37 100644 --- a/kmymoney/plugins/onlinepluginextended.h +++ b/kmymoney/plugins/onlinepluginextended.h @@ -1,112 +1,112 @@ /* * This file is part of KMyMoney, A Personal Finance Manager f*or KDE * Copyright (C) 2014 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 ONLINEPLUGINEXTENDED_H #define ONLINEPLUGINEXTENDED_H #include "kmymoneyplugin.h" #include "mymoney/onlinejob.h" #include "onlinetasks/interfaces/tasks/ionlinetasksettings.h" class onlineJob; class onlineTask; class payeeIdentifierData; namespace KMyMoneyPlugin { /** * @brief Interface between KMyMoney and Online Banking plugins for executing transactions * * This interface is under active development and will change often! Do not use it at the moment! * * @author Christian David (christian-david@web.de) */ class KMM_PLUGIN_EXPORT OnlinePluginExtended : public Plugin, public OnlinePlugin { Q_OBJECT public: OnlinePluginExtended(QObject* parent, const char* name); virtual ~OnlinePluginExtended() {} /** * @brief List onlineJobs supported by an account * * KMyMoney will use this function to ask the online plugin which online jobs it supports. * Later changes can be made public using the jobAvailable signals. * * @return A QStringList with supported onlineTask::name()s as values. */ virtual QStringList availableJobs(QString accountId) = 0; /** * @brief Get settings for onlineTask * * @see onlineTask::settings */ virtual IonlineTaskSettings::ptr settings(QString accountId, QString taskName) = 0; /** * @brief Send onlineJobs to bank * * @param jobs Do not delete the onlineJob objects. You can edit them but expect them to be deleted after * you returned from this function. */ virtual void sendOnlineJob(QList& jobs) = 0; - virtual void plug() = 0; - virtual void unplug() = 0; + virtual void plug() override = 0; + virtual void unplug() override = 0; Q_SIGNALS: /** * @brief Emit to make onlineJob available * * In case a onlineJob got available during runtime, emit one of these signals. */ void jobAvailable(QString accountId, QString); void jobAvailable(QString accountId, QStringList); void jobUnavailable(QString accountId, QString); //void jobUnavailable( QString accountId ); }; class KMM_PLUGIN_EXPORT onlineTaskFactory { public: virtual onlineTask* createOnlineTask(const QString& taskId) const = 0; // Make g++ happy virtual ~onlineTaskFactory() {} }; class KMM_PLUGIN_EXPORT payeeIdentifierDataFactory { public: virtual payeeIdentifierData* createPayeeIdentifier(const QString& payeeIdentifierIid) const = 0; // Make g+ happy virtual ~payeeIdentifierDataFactory() {} }; } // namespace KMyMoneyPlugin Q_DECLARE_INTERFACE(KMyMoneyPlugin::OnlinePluginExtended, "org.kmymoney.plugin.onlinepluginextended"); Q_DECLARE_INTERFACE(KMyMoneyPlugin::onlineTaskFactory, "org.kmymoney.plugin.onlinetaskfactory"); Q_DECLARE_INTERFACE(KMyMoneyPlugin::payeeIdentifierDataFactory, "org.kmymoney.plugin.payeeidentifierfactory"); #endif // ONLINEPLUGINEXTENDED_H diff --git a/kmymoney/plugins/onlinetasks/dummy/tasks/dummytask.h b/kmymoney/plugins/onlinetasks/dummy/tasks/dummytask.h index a2afc241f..049218666 100644 --- a/kmymoney/plugins/onlinetasks/dummy/tasks/dummytask.h +++ b/kmymoney/plugins/onlinetasks/dummy/tasks/dummytask.h @@ -1,94 +1,94 @@ /* * This file is part of KMyMoney, A Personal Finance Manager by KDE * Copyright (C) 2013 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 DUMMYTASK_H #define DUMMYTASK_H #include "onlinetasks/interfaces/tasks/onlinetask.h" class dummyTask : public onlineTask { public: ONLINETASK_META(dummyTask, "org.kmymoney.onlinetasks.dummy"); dummyTask() : m_testNumber(0) { } dummyTask(const dummyTask& other) : onlineTask(other), m_testNumber(other.m_testNumber) { } /** * @brief Checks if the task is ready for sending */ - virtual bool isValid() const { + bool isValid() const final override { return true; - }; + } /** * @brief Human readable type-name */ - virtual QString jobTypeName() const { + QString jobTypeName() const final override { return QLatin1String("Dummy task"); - }; + } void setTestNumber(const int& number) { m_testNumber = number; } int testNumber() { return m_testNumber; } - virtual QString storagePluginIid() const { + QString storagePluginIid() const final override { return QString(); } - virtual bool sqlSave(QSqlDatabase databaseConnection, const QString& onlineJobId) const { + bool sqlSave(QSqlDatabase databaseConnection, const QString& onlineJobId) const final override { Q_UNUSED(databaseConnection); Q_UNUSED(onlineJobId); return false; } - virtual bool sqlModify(QSqlDatabase databaseConnection, const QString& onlineJobId) const { + bool sqlModify(QSqlDatabase databaseConnection, const QString& onlineJobId) const final override { Q_UNUSED(databaseConnection); Q_UNUSED(onlineJobId); return false; } - virtual bool sqlRemove(QSqlDatabase databaseConnection, const QString& onlineJobId) const { + bool sqlRemove(QSqlDatabase databaseConnection, const QString& onlineJobId) const final override { Q_UNUSED(databaseConnection); Q_UNUSED(onlineJobId); return false; } protected: - virtual dummyTask* clone() const { + dummyTask* clone() const final override { return (new dummyTask(*this)); } - virtual bool hasReferenceTo(const QString &id) const { + bool hasReferenceTo(const QString &id) const final override { Q_UNUSED(id); return false; } - virtual void writeXML(QDomDocument&, QDomElement&) const {} - virtual dummyTask* createFromXml(const QDomElement&) const { + void writeXML(QDomDocument&, QDomElement&) const final override {} + dummyTask* createFromXml(const QDomElement&) const final override { return (new dummyTask); } - virtual onlineTask* createFromSqlDatabase(QSqlDatabase connection, const QString& onlineJobId) const { + onlineTask* createFromSqlDatabase(QSqlDatabase connection, const QString& onlineJobId) const final override { Q_UNUSED(connection); Q_UNUSED(onlineJobId); return (new dummyTask); } - virtual QString responsibleAccount() const { + QString responsibleAccount() const final override { return QString(); - }; + } int m_testNumber; }; #endif // DUMMYTASK_H diff --git a/kmymoney/plugins/onlinetasks/sepa/sepaonlinetasksloader.h b/kmymoney/plugins/onlinetasks/sepa/sepaonlinetasksloader.h index 74ac36f1d..d5a0cd69b 100644 --- a/kmymoney/plugins/onlinetasks/sepa/sepaonlinetasksloader.h +++ b/kmymoney/plugins/onlinetasks/sepa/sepaonlinetasksloader.h @@ -1,35 +1,35 @@ /* * This file is part of KMyMoney, A Personal Finance Manager by KDE * Copyright (C) 2013 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 SEPAONLINETASKSLOADER_H #define SEPAONLINETASKSLOADER_H #include class sepaOnlineTasksLoader : public QObject, public KMyMoneyPlugin::onlineTaskFactory { Q_OBJECT Q_INTERFACES(KMyMoneyPlugin::onlineTaskFactory) public: explicit sepaOnlineTasksLoader(QObject* parent = nullptr, const QVariantList& options = QVariantList{}); - virtual onlineTask* createOnlineTask(const QString& taskId) const; + onlineTask* createOnlineTask(const QString& taskId) const final override; }; #endif // SEPAONLINETASKSLOADER_H diff --git a/kmymoney/plugins/onlinetasks/sepa/sepastorageplugin.h b/kmymoney/plugins/onlinetasks/sepa/sepastorageplugin.h index ca4dc0394..1e823207b 100644 --- a/kmymoney/plugins/onlinetasks/sepa/sepastorageplugin.h +++ b/kmymoney/plugins/onlinetasks/sepa/sepastorageplugin.h @@ -1,39 +1,39 @@ /* * This file is part of KMyMoney, A Personal Finance Manager by KDE * Copyright (C) 2014 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 SEPASTORAGEPLUGIN_H #define SEPASTORAGEPLUGIN_H #include "kmymoneystorageplugin.h" #include class sepaStoragePlugin : public KMyMoneyPlugin::storagePlugin { Q_OBJECT Q_INTERFACES(KMyMoneyPlugin::storagePlugin) public: explicit sepaStoragePlugin(QObject* parent = 0, const QVariantList& options = QVariantList()); - virtual bool removePluginData(QSqlDatabase connection); - virtual bool setupDatabase(QSqlDatabase connection); + bool removePluginData(QSqlDatabase connection) final override; + bool setupDatabase(QSqlDatabase connection) final override; static const QString iid; }; #endif // SEPASTORAGEPLUGIN_H diff --git a/kmymoney/plugins/onlinetasks/sepa/tasks/sepaonlinetransfer.h b/kmymoney/plugins/onlinetasks/sepa/tasks/sepaonlinetransfer.h index 722812242..8e8ea5caa 100644 --- a/kmymoney/plugins/onlinetasks/sepa/tasks/sepaonlinetransfer.h +++ b/kmymoney/plugins/onlinetasks/sepa/tasks/sepaonlinetransfer.h @@ -1,132 +1,132 @@ /* This file is part of KMyMoney, A Personal Finance Manager by KDE Copyright (C) 2013 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 SEPAONLINETRANSFER_H #define SEPAONLINETRANSFER_H #include "misc/validators.h" #include "onlinetasks/interfaces/tasks/onlinetask.h" #include "onlinetasks/interfaces/tasks/credittransfer.h" #include "onlinetasks/interfaces/tasks/ionlinetasksettings.h" #include "payeeidentifier/ibanandbic/ibanbic.h" /** * @brief SEPA Credit Transfer */ class sepaOnlineTransfer : public onlineTask, public creditTransfer { Q_INTERFACES(creditTransfer) public: ONLINETASK_META(sepaOnlineTransfer, "org.kmymoney.creditTransfer.sepa"); sepaOnlineTransfer(); sepaOnlineTransfer(const sepaOnlineTransfer &other); - virtual QString responsibleAccount() const = 0; + virtual QString responsibleAccount() const override = 0; virtual void setOriginAccount(const QString& accountId) = 0; - virtual MyMoneyMoney value() const = 0; + virtual MyMoneyMoney value() const override = 0; virtual void setValue(MyMoneyMoney value) = 0; virtual void setBeneficiary(const payeeIdentifiers::ibanBic& accountIdentifier) = 0; virtual payeeIdentifiers::ibanBic beneficiaryTyped() const = 0; virtual void setPurpose(const QString purpose) = 0; - virtual QString purpose() const = 0; + virtual QString purpose() const override = 0; virtual void setEndToEndReference(const QString& reference) = 0; virtual QString endToEndReference() const = 0; /** * @brief Returns the origin account identifier * @return you are owner of the object */ virtual payeeIdentifier originAccountIdentifier() const = 0; /** * National account can handle the currency of the related account only. */ - virtual MyMoneySecurity currency() const = 0; + virtual MyMoneySecurity currency() const override = 0; - virtual bool isValid() const = 0; + virtual bool isValid() const override = 0; - virtual QString jobTypeName() const = 0; + virtual QString jobTypeName() const override = 0; virtual unsigned short int textKey() const = 0; virtual unsigned short int subTextKey() const = 0; - virtual bool hasReferenceTo(const QString& id) const = 0; + virtual bool hasReferenceTo(const QString& id) const override = 0; class settings : public IonlineTaskSettings { public: // Limits getter virtual int purposeMaxLines() const = 0; virtual int purposeLineLength() const = 0; virtual int purposeMinLength() const = 0; virtual int recipientNameLineLength() const = 0; virtual int recipientNameMinLength() const = 0; virtual int payeeNameLineLength() const = 0; virtual int payeeNameMinLength() const = 0; virtual QString allowedChars() const = 0; // Checker virtual bool checkPurposeCharset(const QString& string) const = 0; virtual bool checkPurposeLineLength(const QString& purpose) const = 0; virtual validators::lengthStatus checkPurposeLength(const QString& purpose) const = 0; virtual bool checkPurposeMaxLines(const QString& purpose) const = 0; virtual validators::lengthStatus checkNameLength(const QString& name) const = 0; virtual bool checkNameCharset(const QString& name) const = 0; virtual validators::lengthStatus checkRecipientLength(const QString& name) const = 0; virtual bool checkRecipientCharset(const QString& name) const = 0; virtual int endToEndReferenceLength() const = 0; virtual validators::lengthStatus checkEndToEndReferenceLength(const QString& reference) const = 0; virtual bool checkRecipientBic(const QString& bic) const = 0; /** * @brief Checks if the bic is mandatory for the given iban * * For the check usually only the first two chars are needed. So you do not * need to validate the IBAN. * * @todo LOW: Implement, should be simple to test: if the country code in iban is the same as in origin iban and * the iban belongs to a sepa country a bic is not necessary. Will change 1. Feb 2016. */ virtual bool isBicMandatory(const QString& payeeiban, const QString& beneficiaryIban) const = 0; }; virtual QSharedPointer getSettings() const = 0; protected: - virtual sepaOnlineTransfer* clone() const = 0; + virtual sepaOnlineTransfer* clone() const override = 0; - virtual sepaOnlineTransfer* createFromXml(const QDomElement &element) const = 0; - virtual void writeXML(QDomDocument& document, QDomElement& parent) const = 0; + virtual sepaOnlineTransfer* createFromXml(const QDomElement &element) const override = 0; + virtual void writeXML(QDomDocument& document, QDomElement& parent) const override = 0; }; Q_DECLARE_INTERFACE(sepaOnlineTransfer, "org.kmymoney.creditTransfer.sepa") #endif // SEPAONLINETRANSFER_H diff --git a/kmymoney/plugins/onlinetasks/sepa/tasks/sepaonlinetransferimpl.cpp b/kmymoney/plugins/onlinetasks/sepa/tasks/sepaonlinetransferimpl.cpp index 8ef17609c..222ffd6a7 100644 --- a/kmymoney/plugins/onlinetasks/sepa/tasks/sepaonlinetransferimpl.cpp +++ b/kmymoney/plugins/onlinetasks/sepa/tasks/sepaonlinetransferimpl.cpp @@ -1,380 +1,380 @@ /* This file is part of KMyMoney, A Personal Finance Manager by KDE Copyright (C) 2013 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 "sepaonlinetransferimpl.h" #include #include #include "mymoneyutils.h" #include "mymoneyaccount.h" #include "mymoney/mymoneyfile.h" #include "mymoneypayee.h" #include "mymoney/onlinejobadministration.h" #include "misc/validators.h" #include "payeeidentifiertyped.h" #include "tasks/sepaonlinetransfer.h" static const unsigned short defaultTextKey = 51; static const unsigned short defaultSubTextKey = 0; /** * @brief Fallback if plugin fails to create settings correctly */ class sepaOnlineTransferSettingsFallback : public sepaOnlineTransfer::settings { public: // Limits getter - virtual int purposeMaxLines() const { + int purposeMaxLines() const final override { return 1; } - virtual int purposeLineLength() const { + int purposeLineLength() const final override { return 27; } - virtual int purposeMinLength() const { + int purposeMinLength() const final override { return 0; } - virtual int recipientNameLineLength() const { + int recipientNameLineLength() const final override { return 1; } - virtual int recipientNameMinLength() const { + int recipientNameMinLength() const final override { return 0; } - virtual int payeeNameLineLength() const { + int payeeNameLineLength() const final override { return 0; } - virtual int payeeNameMinLength() const { + int payeeNameMinLength() const final override { return 0; } - virtual QString allowedChars() const { + QString allowedChars() const final override { return QString(); } // Checker - virtual bool checkPurposeCharset(const QString&) const { + bool checkPurposeCharset(const QString&) const final override { return false; } - virtual bool checkPurposeLineLength(const QString&) const { + bool checkPurposeLineLength(const QString&) const final override { return false; } - virtual validators::lengthStatus checkPurposeLength(const QString&) const { + validators::lengthStatus checkPurposeLength(const QString&) const final override { return validators::tooLong; } - virtual bool checkPurposeMaxLines(const QString&) const { + bool checkPurposeMaxLines(const QString&) const final override { return false; } - virtual validators::lengthStatus checkNameLength(const QString&) const { + validators::lengthStatus checkNameLength(const QString&) const final override { return validators::tooLong; } - virtual bool checkNameCharset(const QString&) const { + bool checkNameCharset(const QString&) const final override{ return false; } - virtual validators::lengthStatus checkRecipientLength(const QString&) const { + validators::lengthStatus checkRecipientLength(const QString&) const final override { return validators::tooLong; } - virtual bool checkRecipientCharset(const QString&) const { + bool checkRecipientCharset(const QString&) const final override { return false; } - virtual int endToEndReferenceLength() const { + int endToEndReferenceLength() const final override { return 0; } - virtual validators::lengthStatus checkEndToEndReferenceLength(const QString&) const { + validators::lengthStatus checkEndToEndReferenceLength(const QString&) const final override { return validators::tooLong; } virtual bool isIbanValid(const QString&) const { return false; } - virtual bool checkRecipientBic(const QString&) const { + bool checkRecipientBic(const QString&) const final override { return false; } - virtual bool isBicMandatory(const QString&, const QString&) const { + bool isBicMandatory(const QString&, const QString&) const final override { return true; } }; sepaOnlineTransferImpl::sepaOnlineTransferImpl() : sepaOnlineTransfer(), _settings(QSharedPointer()), _originAccount(QString()), _value(0), _purpose(QString("")), _endToEndReference(QString("")), _beneficiaryAccount(payeeIdentifiers::ibanBic()), _textKey(defaultTextKey), _subTextKey(defaultSubTextKey) { } sepaOnlineTransferImpl::sepaOnlineTransferImpl(const sepaOnlineTransferImpl& other) : sepaOnlineTransfer(other), _settings(other._settings), _originAccount(other._originAccount), _value(other._value), _purpose(other._purpose), _endToEndReference(other._endToEndReference), _beneficiaryAccount(other._beneficiaryAccount), _textKey(other._textKey), _subTextKey(other._subTextKey) { } sepaOnlineTransfer *sepaOnlineTransferImpl::clone() const { sepaOnlineTransfer *transfer = new sepaOnlineTransferImpl(*this); return transfer; } //! @todo add validation of local name bool sepaOnlineTransferImpl::isValid() const { QString iban; try { payeeIdentifier ident = originAccountIdentifier(); iban = ident.data()->electronicIban(); } catch (payeeIdentifier::exception&) { } - QSharedPointer settings = getSettings(); - if (settings->checkPurposeLength(_purpose) == validators::ok - && settings->checkPurposeMaxLines(_purpose) - && settings->checkPurposeLineLength(_purpose) - && settings->checkPurposeCharset(_purpose) - && settings->checkEndToEndReferenceLength(_endToEndReference) == validators::ok + QSharedPointer localSettings = getSettings(); + if (localSettings->checkPurposeLength(_purpose) == validators::ok + && localSettings->checkPurposeMaxLines(_purpose) + && localSettings->checkPurposeLineLength(_purpose) + && localSettings->checkPurposeCharset(_purpose) + && localSettings->checkEndToEndReferenceLength(_endToEndReference) == validators::ok //&& settings->checkRecipientCharset( _beneficiaryAccount.ownerName() ) //&& settings->checkRecipientLength( _beneficiaryAccount.ownerName()) == validators::ok && _beneficiaryAccount.isIbanValid() // do not check the BIC, maybe it is not needed - && (!settings->isBicMandatory(iban, _beneficiaryAccount.electronicIban()) || (settings->checkRecipientBic(_beneficiaryAccount.bic()) && _beneficiaryAccount.isValid() /** @todo double check of BIC here, fix that */)) + && (!localSettings->isBicMandatory(iban, _beneficiaryAccount.electronicIban()) || (localSettings->checkRecipientBic(_beneficiaryAccount.bic()) && _beneficiaryAccount.isValid() /** @todo double check of BIC here, fix that */)) && value().isPositive() ) return true; return false; } payeeIdentifier sepaOnlineTransferImpl::originAccountIdentifier() const { if (!_originAccount.isEmpty()) { QList< payeeIdentifierTyped > idents; //#ifndef Q_OS_WIN /// @todo Fix loading originAccountIdentifier on MS-Windows // the next statment causes problems on MS Windows () according to // https://phabricator.kde.org/D10125 which was pushed in commit // 44f846f13d0f7c7cca1178d56492471cb9f5092b idents = MyMoneyFile::instance()->account(_originAccount).payeeIdentifiersByType(); //#endif if (!idents.isEmpty()) { payeeIdentifierTyped ident = idents[0]; ident->setOwnerName(MyMoneyFile::instance()->user().name()); return ident; } } return payeeIdentifier(new payeeIdentifiers::ibanBic); } MyMoneySecurity sepaOnlineTransferImpl::currency() const { if (!_originAccount.isEmpty()) { const QString currencyId = MyMoneyFile::instance()->account(_originAccount).currencyId(); return MyMoneyFile::instance()->security(currencyId); } return MyMoneyFile::instance()->baseCurrency(); } /** * @internal To ensure that we never return a nullptr, @a sepaOnlineTransferSettingsFallback is used if the online plugin fails * to give us an correct value */ QSharedPointer sepaOnlineTransferImpl::getSettings() const { if (_settings.isNull()) { _settings = onlineJobAdministration::instance()->taskSettings(name(), _originAccount); if (_settings.isNull()) _settings = QSharedPointer< const sepaOnlineTransfer::settings >(new sepaOnlineTransferSettingsFallback); } Q_CHECK_PTR(_settings); return _settings; } void sepaOnlineTransferImpl::setOriginAccount(const QString &accountId) { if (_originAccount != accountId) { _originAccount = accountId; _settings = QSharedPointer(); } } void sepaOnlineTransferImpl::writeXML(QDomDocument& document, QDomElement& parent) const { Q_UNUSED(document); parent.setAttribute("originAccount", _originAccount); parent.setAttribute("value", _value.toString()); parent.setAttribute("textKey", _textKey); parent.setAttribute("subTextKey", _subTextKey); if (!_purpose.isEmpty()) { parent.setAttribute("purpose", _purpose); } if (!_endToEndReference.isEmpty()) { parent.setAttribute("endToEndReference", _endToEndReference); } QDomElement beneficiaryEl = document.createElement("beneficiary"); _beneficiaryAccount.writeXML(document, beneficiaryEl); parent.appendChild(beneficiaryEl); } sepaOnlineTransfer* sepaOnlineTransferImpl::createFromXml(const QDomElement& element) const { sepaOnlineTransferImpl* task = new sepaOnlineTransferImpl(); task->setOriginAccount(element.attribute("originAccount", QString())); task->setValue(MyMoneyMoney(MyMoneyUtils::QStringEmpty(element.attribute("value", QString())))); task->_textKey = element.attribute("textKey", QString().setNum(defaultTextKey)).toUShort(); task->_subTextKey = element.attribute("subTextKey", QString().setNum(defaultSubTextKey)).toUShort(); task->setPurpose(element.attribute("purpose", QString())); task->setEndToEndReference(element.attribute("endToEndReference", QString())); payeeIdentifiers::ibanBic beneficiary; payeeIdentifiers::ibanBic* beneficiaryPtr = 0; QDomElement beneficiaryEl = element.firstChildElement("beneficiary"); if (!beneficiaryEl.isNull()) { beneficiaryPtr = beneficiary.createFromXml(beneficiaryEl); } if (beneficiaryPtr == 0) { task->_beneficiaryAccount = beneficiary; } else { task->_beneficiaryAccount = *beneficiaryPtr; } delete beneficiaryPtr; return task; } onlineTask* sepaOnlineTransferImpl::createFromSqlDatabase(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->_textKey = query.value(7).toUInt(); task->_subTextKey = 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->_beneficiaryAccount = beneficiary; return task; } return 0; } void sepaOnlineTransferImpl::bindValuesToQuery(QSqlQuery& query, const QString& id) const { query.bindValue(":id", id); query.bindValue(":originAccount", _originAccount); query.bindValue(":value", _value.toString()); query.bindValue(":purpose", _purpose); query.bindValue(":endToEndReference", (_endToEndReference.isEmpty()) ? QVariant() : QVariant::fromValue(_endToEndReference)); query.bindValue(":beneficiaryName", _beneficiaryAccount.ownerName()); query.bindValue(":beneficiaryIban", _beneficiaryAccount.electronicIban()); query.bindValue(":beneficiaryBic", (_beneficiaryAccount.storedBic().isEmpty()) ? QVariant() : QVariant::fromValue(_beneficiaryAccount.storedBic())); query.bindValue(":textKey", _textKey); query.bindValue(":subTextKey", _subTextKey); } bool sepaOnlineTransferImpl::sqlSave(QSqlDatabase databaseConnection, const QString& onlineJobId) const { QSqlQuery query = QSqlQuery(databaseConnection); 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(query, onlineJobId); if (!query.exec()) { qWarning("Error while saving sepa order '%s': %s", qPrintable(onlineJobId), qPrintable(query.lastError().text())); return false; } return true; } bool sepaOnlineTransferImpl::sqlModify(QSqlDatabase databaseConnection, const QString& onlineJobId) const { QSqlQuery query = QSqlQuery(databaseConnection); 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(query, onlineJobId); if (!query.exec()) { qWarning("Could not modify sepaOnlineTransfer '%s': %s", qPrintable(onlineJobId), qPrintable(query.lastError().text())); return false; } return true; } bool sepaOnlineTransferImpl::sqlRemove(QSqlDatabase databaseConnection, const QString& onlineJobId) const { QSqlQuery query = QSqlQuery(databaseConnection); query.prepare("DELETE FROM kmmSepaOrders WHERE id = ?"); query.bindValue(0, onlineJobId); return query.exec(); } bool sepaOnlineTransferImpl::hasReferenceTo(const QString& id) const { return (id == _originAccount); } QString sepaOnlineTransferImpl::jobTypeName() const { return QLatin1String("SEPA Credit Transfer"); } diff --git a/kmymoney/plugins/onlinetasks/sepa/tasks/sepaonlinetransferimpl.h b/kmymoney/plugins/onlinetasks/sepa/tasks/sepaonlinetransferimpl.h index f35214f78..3130897e1 100644 --- a/kmymoney/plugins/onlinetasks/sepa/tasks/sepaonlinetransferimpl.h +++ b/kmymoney/plugins/onlinetasks/sepa/tasks/sepaonlinetransferimpl.h @@ -1,121 +1,121 @@ /* This file is part of KMyMoney, A Personal Finance Manager by KDE Copyright (C) 2013 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 SEPAONLINETRANSFERIMPL_H #define SEPAONLINETRANSFERIMPL_H #include "sepaonlinetransfer.h" #include "../sepastorageplugin.h" /** * @brief SEPA Credit Transfer */ class sepaOnlineTransferImpl : public sepaOnlineTransfer { Q_INTERFACES(sepaOnlineTransfer) public: ONLINETASK_META(sepaOnlineTransfer, "org.kmymoney.creditTransfer.sepa"); sepaOnlineTransferImpl(); sepaOnlineTransferImpl(const sepaOnlineTransferImpl &other); - QString responsibleAccount() const { + QString responsibleAccount() const final override { return _originAccount; } - void setOriginAccount(const QString& accountId); + void setOriginAccount(const QString& accountId) final override; - MyMoneyMoney value() const { + MyMoneyMoney value() const final override{ return _value; } - virtual void setValue(MyMoneyMoney value) { + void setValue(MyMoneyMoney value) final override { _value = value; } - virtual void setBeneficiary(const payeeIdentifiers::ibanBic& accountIdentifier) { + void setBeneficiary(const payeeIdentifiers::ibanBic& accountIdentifier) final override { _beneficiaryAccount = accountIdentifier; - }; - virtual payeeIdentifier beneficiary() const { + } + payeeIdentifier beneficiary() const final override { return payeeIdentifier(_beneficiaryAccount.clone()); } - virtual payeeIdentifiers::ibanBic beneficiaryTyped() const { + payeeIdentifiers::ibanBic beneficiaryTyped() const final override { return _beneficiaryAccount; } - virtual void setPurpose(const QString purpose) { + void setPurpose(const QString purpose) final override { _purpose = purpose; } - QString purpose() const { + QString purpose() const final override { return _purpose; } - virtual void setEndToEndReference(const QString& reference) { + void setEndToEndReference(const QString& reference) final override { _endToEndReference = reference; } - QString endToEndReference() const { + QString endToEndReference() const final override { return _endToEndReference; } - payeeIdentifier originAccountIdentifier() const; + payeeIdentifier originAccountIdentifier() const final override; - MyMoneySecurity currency() const; + MyMoneySecurity currency() const final override; - bool isValid() const; + bool isValid() const final override; - QString jobTypeName() const; - virtual QString storagePluginIid() const { + QString jobTypeName() const final override; + QString storagePluginIid() const final override { return sepaStoragePlugin::iid; } - virtual bool sqlSave(QSqlDatabase databaseConnection, const QString& onlineJobId) const; - virtual bool sqlModify(QSqlDatabase databaseConnection, const QString& onlineJobId) const; - virtual bool sqlRemove(QSqlDatabase databaseConnection, const QString& onlineJobId) const; + bool sqlSave(QSqlDatabase databaseConnection, const QString& onlineJobId) const final override; + bool sqlModify(QSqlDatabase databaseConnection, const QString& onlineJobId) const final override; + bool sqlRemove(QSqlDatabase databaseConnection, const QString& onlineJobId) const final override; - unsigned short int textKey() const { + unsigned short int textKey() const final override { return _textKey; } - unsigned short int subTextKey() const { + unsigned short int subTextKey() const final override { return _subTextKey; } - virtual bool hasReferenceTo(const QString& id) const; + bool hasReferenceTo(const QString& id) const final override; - QSharedPointer getSettings() const; + QSharedPointer getSettings() const final override; protected: - sepaOnlineTransfer* clone() const; + sepaOnlineTransfer* clone() const final override; - virtual sepaOnlineTransfer* createFromXml(const QDomElement &element) const; - virtual onlineTask* createFromSqlDatabase(QSqlDatabase connection, const QString& onlineJobId) const; - virtual void writeXML(QDomDocument& document, QDomElement& parent) const; + sepaOnlineTransfer* createFromXml(const QDomElement &element) const final override; + onlineTask* createFromSqlDatabase(QSqlDatabase connection, const QString& onlineJobId) const final override; + void writeXML(QDomDocument& document, QDomElement& parent) const final override; private: void bindValuesToQuery(QSqlQuery& query, const QString& id) const; mutable QSharedPointer _settings; QString _originAccount; MyMoneyMoney _value; QString _purpose; QString _endToEndReference; payeeIdentifiers::ibanBic _beneficiaryAccount; unsigned short int _textKey; unsigned short int _subTextKey; }; #endif // SEPAONLINETRANSFERIMPL_H diff --git a/kmymoney/plugins/onlinetasks/sepa/ui/sepacredittransferedit.cpp b/kmymoney/plugins/onlinetasks/sepa/ui/sepacredittransferedit.cpp index 5be658a5b..044274195 100644 --- a/kmymoney/plugins/onlinetasks/sepa/ui/sepacredittransferedit.cpp +++ b/kmymoney/plugins/onlinetasks/sepa/ui/sepacredittransferedit.cpp @@ -1,535 +1,535 @@ /* This file is part of KMyMoney, A Personal Finance Manager by KDE Copyright (C) 2013 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 "sepacredittransferedit.h" #include "ui_sepacredittransferedit.h" #include #include #include #include #include "kguiutils.h" #include "mymoney/payeeidentifiermodel.h" #include "onlinetasks/sepa/tasks/sepaonlinetransfer.h" #include "payeeidentifier/ibanandbic/widgets/ibanvalidator.h" #include "payeeidentifier/ibanandbic/widgets/bicvalidator.h" #include "payeeidentifier/payeeidentifiertyped.h" #include "misc/charvalidator.h" #include "payeeidentifier/ibanandbic/ibanbic.h" #include "styleditemdelegateforwarder.h" #include "payeeidentifier/ibanandbic/widgets/ibanbicitemdelegate.h" #include "onlinejobtyped.h" #include "mymoneyaccount.h" #include "widgetenums.h" class ibanBicCompleterDelegate : public StyledItemDelegateForwarder { Q_OBJECT public: ibanBicCompleterDelegate(QObject *parent) : StyledItemDelegateForwarder(parent) {} protected: - virtual QAbstractItemDelegate* getItemDelegate(const QModelIndex &index) const { + QAbstractItemDelegate* getItemDelegate(const QModelIndex &index) const final override { static QPointer defaultDelegate; static QPointer ibanBicDelegate; const bool ibanBicRequested = index.model()->data(index, payeeIdentifierModel::isPayeeIdentifier).toBool(); QAbstractItemDelegate* delegate = (ibanBicRequested) ? ibanBicDelegate : defaultDelegate; if (delegate == 0) { if (ibanBicRequested) { // Use this->parent() as parent because "this" is const ibanBicDelegate = new ibanBicItemDelegate(this->parent()); delegate = ibanBicDelegate; } else { // Use this->parent() as parent because "this" is const defaultDelegate = new QStyledItemDelegate(this->parent()); delegate = defaultDelegate; } connectSignals(delegate, Qt::UniqueConnection); } Q_CHECK_PTR(delegate); return delegate; } }; class payeeIdentifierCompleterPopup : public QTreeView { Q_OBJECT public: payeeIdentifierCompleterPopup(QWidget* parent = 0) : QTreeView(parent) { setRootIsDecorated(false); setAlternatingRowColors(true); setAnimated(true); setHeaderHidden(true); setUniformRowHeights(false); expandAll(); } }; class ibanBicFilterProxyModel : public QSortFilterProxyModel { Q_OBJECT public: enum roles { payeeIban = payeeIdentifierModel::payeeIdentifierUserRole, /**< electornic IBAN of payee */ }; ibanBicFilterProxyModel(QObject* parent = 0) : QSortFilterProxyModel(parent) {} - virtual QVariant data(const QModelIndex &index, int role) const { + QVariant data(const QModelIndex &index, int role) const final override { if (role == payeeIban) { if (!index.isValid()) return QVariant(); try { payeeIdentifierTyped iban = payeeIdentifierTyped( index.model()->data(index, payeeIdentifierModel::payeeIdentifier).value() ); return iban->electronicIban(); } catch (payeeIdentifier::exception&) { return QVariant(); } } return QSortFilterProxyModel::data(index, role); } - virtual bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const { + bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const final override { if (!source_parent.isValid()) return true; QModelIndex index = source_parent.model()->index(source_row, 0, source_parent); return (source_parent.model()->data(index, payeeIdentifierModel::payeeIdentifierType).toString() == payeeIdentifiers::ibanBic::staticPayeeIdentifierIid()); } }; class ibanBicCompleter : public QCompleter { Q_OBJECT public: ibanBicCompleter(QObject* parent = 0); Q_SIGNALS: void activatedName(const QString& name) const; void highlightedName(const QString& name) const; void activatedBic(const QString& bic) const; void highlightedBic(const QString& bic) const; void activatedIban(const QString& iban) const; void highlightedIban(const QString& iban) const; private Q_SLOTS: void slotActivated(const QModelIndex& index) const; void slotHighlighted(const QModelIndex& index) const; }; ibanBicCompleter::ibanBicCompleter(QObject *parent) : QCompleter(parent) { connect(this, SIGNAL(activated(QModelIndex)), SLOT(slotActivated(QModelIndex))); connect(this, SIGNAL(highlighted(QModelIndex)), SLOT(slotHighlighted(QModelIndex))); } void ibanBicCompleter::slotActivated(const QModelIndex &index) const { if (!index.isValid()) return; emit activatedName(index.model()->data(index, payeeIdentifierModel::payeeName).toString()); try { payeeIdentifierTyped iban = payeeIdentifierTyped( index.model()->data(index, payeeIdentifierModel::payeeIdentifier).value() ); emit activatedIban(iban->electronicIban()); emit activatedBic(iban->storedBic()); } catch (payeeIdentifier::exception&) { } } void ibanBicCompleter::slotHighlighted(const QModelIndex &index) const { if (!index.isValid()) return; emit highlightedName(index.model()->data(index, payeeIdentifierModel::payeeName).toString()); try { payeeIdentifierTyped iban = payeeIdentifierTyped( index.model()->data(index, payeeIdentifierModel::payeeIdentifier).value() ); emit highlightedIban(iban->electronicIban()); emit highlightedBic(iban->storedBic()); } catch (payeeIdentifier::exception&) { } } sepaCreditTransferEdit::sepaCreditTransferEdit(QWidget *parent, QVariantList args) : IonlineJobEdit(parent, args), ui(new Ui::sepaCreditTransferEdit), m_onlineJob(onlineJobTyped()), m_requiredFields(new KMandatoryFieldGroup(this)), m_readOnly(false), m_showAllErrors(false) { ui->setupUi(this); m_requiredFields->add(ui->beneficiaryIban); m_requiredFields->add(ui->value); // Other required fields are set in updateSettings() connect(m_requiredFields, SIGNAL(stateChanged(bool)), this, SLOT(requiredFieldsCompleted(bool))); connect(ui->beneficiaryName, SIGNAL(textChanged(QString)), this, SLOT(beneficiaryNameChanged(QString))); connect(ui->beneficiaryIban, SIGNAL(textChanged(QString)), this, SLOT(beneficiaryIbanChanged(QString))); connect(ui->beneficiaryBankCode, SIGNAL(textChanged(QString)), this, SLOT(beneficiaryBicChanged(QString))); connect(ui->value, SIGNAL(valueChanged(QString)), this, SLOT(valueChanged())); connect(ui->sepaReference, SIGNAL(textChanged(QString)), this, SLOT(endToEndReferenceChanged(QString))); connect(ui->purpose, SIGNAL(textChanged()), this, SLOT(purposeChanged())); connect(qApp, SIGNAL(focusChanged(QWidget*,QWidget*)), this, SLOT(updateEveryStatus())); connect(ui->beneficiaryName, SIGNAL(textChanged(QString)), this, SIGNAL(onlineJobChanged())); connect(ui->beneficiaryIban, SIGNAL(textChanged(QString)), this, SIGNAL(onlineJobChanged())); connect(ui->beneficiaryBankCode, SIGNAL(textChanged(QString)), this, SIGNAL(onlineJobChanged())); connect(ui->value, SIGNAL(valueChanged(QString)), this, SIGNAL(onlineJobChanged())); connect(ui->sepaReference, SIGNAL(textChanged(QString)), this, SIGNAL(onlineJobChanged())); connect(ui->purpose, SIGNAL(textChanged()), this, SIGNAL(onlineJobChanged())); // Connect signals for read only connect(this, SIGNAL(readOnlyChanged(bool)), ui->beneficiaryName, SLOT(setReadOnly(bool))); connect(this, SIGNAL(readOnlyChanged(bool)), ui->beneficiaryIban, SLOT(setReadOnly(bool))); connect(this, SIGNAL(readOnlyChanged(bool)), ui->beneficiaryBankCode, SLOT(setReadOnly(bool))); connect(this, SIGNAL(readOnlyChanged(bool)), ui->value, SLOT(setReadOnly(bool))); connect(this, SIGNAL(readOnlyChanged(bool)), ui->sepaReference, SLOT(setReadOnly(bool))); connect(this, SIGNAL(readOnlyChanged(bool)), ui->purpose, SLOT(setReadOnly(bool))); // Create models for completers payeeIdentifierModel* identModel = new payeeIdentifierModel(this); identModel->setTypeFilter(payeeIdentifiers::ibanBic::staticPayeeIdentifierIid()); ibanBicFilterProxyModel* filterModel = new ibanBicFilterProxyModel(this); filterModel->setSourceModel(identModel); KDescendantsProxyModel* descendantsModel = new KDescendantsProxyModel(this); descendantsModel->setSourceModel(filterModel); // Set completers popup and bind them to the corresponding fields { // Beneficiary name field ibanBicCompleter* completer = new ibanBicCompleter(this); completer->setModel(descendantsModel); completer->setCompletionRole(payeeIdentifierModel::payeeName); completer->setCaseSensitivity(Qt::CaseInsensitive); connect(completer, SIGNAL(activatedIban(QString)), ui->beneficiaryIban, SLOT(setText(QString))); connect(completer, SIGNAL(activatedBic(QString)), ui->beneficiaryBankCode, SLOT(setText(QString))); ui->beneficiaryName->setCompleter(completer); QAbstractItemView *itemView = new payeeIdentifierCompleterPopup(); completer->setPopup(itemView); // setPopup() resets the delegate itemView->setItemDelegate(new ibanBicCompleterDelegate(this)); } { // IBAN field ibanBicCompleter* ibanCompleter = new ibanBicCompleter(this); ibanCompleter->setModel(descendantsModel); ibanCompleter->setCompletionRole(ibanBicFilterProxyModel::payeeIban); ibanCompleter->setCaseSensitivity(Qt::CaseInsensitive); connect(ibanCompleter, SIGNAL(activatedName(QString)), ui->beneficiaryName, SLOT(setText(QString))); connect(ibanCompleter, SIGNAL(activatedBic(QString)), ui->beneficiaryBankCode, SLOT(setText(QString))); ui->beneficiaryIban->setCompleter(ibanCompleter); QAbstractItemView *itemView = new payeeIdentifierCompleterPopup(); ibanCompleter->setPopup(itemView); // setPopup() resets the delegate itemView->setItemDelegate(new ibanBicCompleterDelegate(this)); } } sepaCreditTransferEdit::~sepaCreditTransferEdit() { delete ui; } void sepaCreditTransferEdit::showEvent(QShowEvent* event) { updateEveryStatus(); QWidget::showEvent(event); } void sepaCreditTransferEdit::showAllErrorMessages(const bool state) { if (m_showAllErrors != state) { m_showAllErrors = state; updateEveryStatus(); } } onlineJobTyped sepaCreditTransferEdit::getOnlineJobTyped() const { onlineJobTyped sepaJob(m_onlineJob); sepaJob.task()->setValue(ui->value->value()); sepaJob.task()->setPurpose(ui->purpose->toPlainText()); sepaJob.task()->setEndToEndReference(ui->sepaReference->text()); payeeIdentifiers::ibanBic accIdent; accIdent.setOwnerName(ui->beneficiaryName->text()); accIdent.setIban(ui->beneficiaryIban->text()); accIdent.setBic(ui->beneficiaryBankCode->text()); sepaJob.task()->setBeneficiary(accIdent); return sepaJob; } void sepaCreditTransferEdit::setOnlineJob(const onlineJobTyped& job) { m_onlineJob = job; updateSettings(); setReadOnly(!job.isEditable()); ui->purpose->setText(job.task()->purpose()); ui->sepaReference->setText(job.task()->endToEndReference()); ui->value->setValue(job.task()->value()); ui->beneficiaryName->setText(job.task()->beneficiaryTyped().ownerName()); ui->beneficiaryIban->setText(job.task()->beneficiaryTyped().paperformatIban()); ui->beneficiaryBankCode->setText(job.task()->beneficiaryTyped().storedBic()); } bool sepaCreditTransferEdit::setOnlineJob(const onlineJob& job) { if (!job.isNull() && job.task()->taskName() == sepaOnlineTransfer::name()) { setOnlineJob(onlineJobTyped(job)); return true; } return false; } void sepaCreditTransferEdit::setOriginAccount(const QString& accountId) { m_onlineJob.task()->setOriginAccount(accountId); updateSettings(); } void sepaCreditTransferEdit::updateEveryStatus() { beneficiaryNameChanged(ui->beneficiaryName->text()); beneficiaryIbanChanged(ui->beneficiaryIban->text()); beneficiaryBicChanged(ui->beneficiaryBankCode->text()); purposeChanged(); valueChanged(); endToEndReferenceChanged(ui->sepaReference->text()); } void sepaCreditTransferEdit::setReadOnly(const bool& readOnly) { // Only set writeable if it changes something and if it is possible if (readOnly != m_readOnly && (readOnly == true || getOnlineJobTyped().isEditable())) { m_readOnly = readOnly; emit readOnlyChanged(m_readOnly); } } void sepaCreditTransferEdit::updateSettings() { QSharedPointer settings = taskSettings(); // Reference ui->sepaReference->setMaxLength(settings->endToEndReferenceLength()); if (settings->endToEndReferenceLength() == 0) ui->sepaReference->setEnabled(false); else ui->sepaReference->setEnabled(true); // Purpose ui->purpose->setAllowedChars(settings->allowedChars()); ui->purpose->setMaxLineLength(settings->purposeLineLength()); ui->purpose->setMaxLines(settings->purposeMaxLines()); if (settings->purposeMinLength()) m_requiredFields->add(ui->purpose); else m_requiredFields->remove(ui->purpose); // Beneficiary Name ui->beneficiaryName->setValidator(new charValidator(ui->beneficiaryName, settings->allowedChars())); ui->beneficiaryName->setMaxLength(settings->recipientNameLineLength()); if (settings->recipientNameMinLength() != 0) m_requiredFields->add(ui->beneficiaryName); else m_requiredFields->remove(ui->beneficiaryName); updateEveryStatus(); } void sepaCreditTransferEdit::beneficiaryIbanChanged(const QString& iban) { // Check IBAN QPair answer = ibanValidator::validateWithMessage(iban); if (m_showAllErrors || iban.length() > 5 || (!ui->beneficiaryIban->hasFocus() && !iban.isEmpty())) ui->feedbackIban->setFeedback(answer.first, answer.second); else ui->feedbackIban->removeFeedback(); // Check if BIC is mandatory QSharedPointer settings = taskSettings(); QString payeeIban; try { payeeIdentifier ident = getOnlineJobTyped().task()->originAccountIdentifier(); payeeIban = ident.data()->electronicIban(); } catch (payeeIdentifier::exception&) { } if (settings->isBicMandatory(payeeIban, iban)) { m_requiredFields->add(ui->beneficiaryBankCode); beneficiaryBicChanged(ui->beneficiaryBankCode->text()); } else { m_requiredFields->remove(ui->beneficiaryBankCode); beneficiaryBicChanged(ui->beneficiaryBankCode->text()); } } void sepaCreditTransferEdit::beneficiaryBicChanged(const QString& bic) { if (bic.isEmpty() && !ui->beneficiaryIban->text().isEmpty()) { QSharedPointer settings = taskSettings(); const payeeIdentifier payee = getOnlineJobTyped().task()->originAccountIdentifier(); QString iban; try { iban = payee.data()->electronicIban(); } catch (payeeIdentifier::badCast&) { } if (settings->isBicMandatory(iban , ui->beneficiaryIban->text())) { ui->feedbackBic->setFeedback(eWidgets::ValidationFeedback::MessageType::Error, i18n("For this beneficiary's country the BIC is mandatory.")); return; } } QPair answer = bicValidator::validateWithMessage(bic); if (m_showAllErrors || bic.length() >= 8 || (!ui->beneficiaryBankCode->hasFocus() && !bic.isEmpty())) ui->feedbackBic->setFeedback(answer.first, answer.second); else ui->feedbackBic->removeFeedback(); } void sepaCreditTransferEdit::beneficiaryNameChanged(const QString& name) { QSharedPointer settings = taskSettings(); if (name.length() < settings->recipientNameMinLength() && (m_showAllErrors || (!ui->beneficiaryName->hasFocus() && !name.isEmpty()))) { ui->feedbackName->setFeedback(eWidgets::ValidationFeedback::MessageType::Error, i18np("A beneficiary name is needed.", "The beneficiary name must be at least %1 characters long", settings->recipientNameMinLength() )); } else { ui->feedbackName->removeFeedback(); } } void sepaCreditTransferEdit::valueChanged() { if ((!ui->value->isValid() && (m_showAllErrors || (!ui->value->hasFocus() && ui->value->value().toDouble() != 0))) || (!ui->value->value().isPositive() && ui->value->value().toDouble() != 0)) { ui->feedbackAmount->setFeedback(eWidgets::ValidationFeedback::MessageType::Error, i18n("A positive amount to transfer is needed.")); return; } if (!ui->value->isValid()) return; const MyMoneyAccount account = getOnlineJob().responsibleMyMoneyAccount(); const MyMoneyMoney expectedBalance = account.balance() - ui->value->value(); if (expectedBalance < MyMoneyMoney(account.value("maxCreditAbsolute"))) { ui->feedbackAmount->setFeedback(eWidgets::ValidationFeedback::MessageType::Warning, i18n("After this credit transfer the account's balance will be below your credit limit.")); } else if (expectedBalance < MyMoneyMoney(account.value("minBalanceAbsolute"))) { ui->feedbackAmount->setFeedback(eWidgets::ValidationFeedback::MessageType::Information, i18n("After this credit transfer the account's balance will be below the minimal balance.")); } else { ui->feedbackAmount->removeFeedback(); } } void sepaCreditTransferEdit::endToEndReferenceChanged(const QString& reference) { QSharedPointer settings = taskSettings(); if (settings->checkEndToEndReferenceLength(reference) == validators::tooLong) { ui->feedbackReference->setFeedback(eWidgets::ValidationFeedback::MessageType::Error, i18np("The end-to-end reference cannot contain more than one character.", "The end-to-end reference cannot contain more than %1 characters.", settings->endToEndReferenceLength() )); } else { ui->feedbackReference->removeFeedback(); } } void sepaCreditTransferEdit::purposeChanged() { const QString purpose = ui->purpose->toPlainText(); QSharedPointer settings = taskSettings(); QString message; if (!settings->checkPurposeLineLength(purpose)) message = i18np("The maximal line length of %1 character per line is exceeded.", "The maximal line length of %1 characters per line is exceeded.", settings->purposeLineLength()) .append('\n'); if (!settings->checkPurposeCharset(purpose)) message.append(i18n("The purpose can only contain the letters A-Z, spaces and ':?.,-()+ and /")).append('\n'); if (!settings->checkPurposeMaxLines(purpose)) { message.append(i18np("In the purpose only a single line is allowed.", "The purpose cannot contain more than %1 lines.", settings->purposeMaxLines())) .append('\n'); } else if (settings->checkPurposeLength(purpose) == validators::tooShort) { message.append(i18np("A purpose is needed.", "The purpose must be at least %1 characters long.", settings->purposeMinLength())) .append('\n'); } // Remove the last '\n' message.chop(1); if (!message.isEmpty()) { ui->feedbackPurpose->setFeedback(eWidgets::ValidationFeedback::MessageType::Error, message); } else { ui->feedbackPurpose->removeFeedback(); } } QSharedPointer< const sepaOnlineTransfer::settings > sepaCreditTransferEdit::taskSettings() { return getOnlineJobTyped().constTask()->getSettings(); } #include "sepacredittransferedit.moc" diff --git a/kmymoney/plugins/onlinetasks/sepa/ui/sepacredittransferedit.h b/kmymoney/plugins/onlinetasks/sepa/ui/sepacredittransferedit.h index 610ee72d3..49c2de171 100644 --- a/kmymoney/plugins/onlinetasks/sepa/ui/sepacredittransferedit.h +++ b/kmymoney/plugins/onlinetasks/sepa/ui/sepacredittransferedit.h @@ -1,126 +1,126 @@ /* * This file is part of KMyMoney, A Personal Finance Manager by KDE * Copyright (C) 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 SEPACREDITTRANSFEREDIT_H #define SEPACREDITTRANSFEREDIT_H #include #include "mymoney/onlinejobtyped.h" #include "onlinetasks/sepa/tasks/sepaonlinetransfer.h" #include "onlinetasks/interfaces/ui/ionlinejobedit.h" class KMandatoryFieldGroup; namespace Ui { class sepaCreditTransferEdit; } /** * @brief Widget to edit sepaOnlineTransfer */ class sepaCreditTransferEdit : public IonlineJobEdit { Q_OBJECT Q_PROPERTY(bool readOnly READ isReadOnly WRITE setReadOnly NOTIFY readOnlyChanged); Q_PROPERTY(onlineJob job READ getOnlineJob WRITE setOnlineJob); Q_INTERFACES(IonlineJobEdit); public: explicit sepaCreditTransferEdit(QWidget *parent = 0, QVariantList args = QVariantList()); ~sepaCreditTransferEdit(); onlineJobTyped getOnlineJobTyped() const; - onlineJob getOnlineJob() const { + onlineJob getOnlineJob() const final override { return getOnlineJobTyped(); } - QStringList supportedOnlineTasks() const { + QStringList supportedOnlineTasks() const final override { return QStringList(sepaOnlineTransfer::name()); } QString label() const { return i18n("SEPA Credit Transfer"); - }; + } - bool isValid() const { + bool isValid() const final override { return getOnlineJobTyped().isValid(); - }; + } - bool isReadOnly() const { + bool isReadOnly() const final override { return m_readOnly; } - virtual void showAllErrorMessages(const bool); + void showAllErrorMessages(const bool) final override; - virtual void showEvent(QShowEvent*); + void showEvent(QShowEvent*) final override; Q_SIGNALS: void onlineJobChanged(); void readOnlyChanged(bool); public Q_SLOTS: void setOnlineJob(const onlineJobTyped &job); - bool setOnlineJob(const onlineJob& job); - void setOriginAccount(const QString& accountId); + bool setOnlineJob(const onlineJob& job) final override; + void setOriginAccount(const QString& accountId) final override; void setReadOnly(const bool&); private Q_SLOTS: void updateSettings(); void updateEveryStatus(); /** @{ * These slots are called when the corosponding field is changed * to start the validation. */ void purposeChanged(); void beneficiaryIbanChanged(const QString& iban); void beneficiaryBicChanged(const QString& bic); void beneficiaryNameChanged(const QString& name); void valueChanged(); void endToEndReferenceChanged(const QString& reference); /** @} */ /** * @brief Convenient slot to emit validityChanged() * * A default implementation to emit validityChanged() based on getOnlineJob().isValid(). * This is useful if you use @a kMandatoryFieldsGroup in your widget. Just connect kMandatoryFieldsGroup::stateChanged(bool) * to this slot. * * @param status if false, validityChanged(false) is emitted without further checks. */ void requiredFieldsCompleted(const bool& status = true) { if (status) { emit validityChanged(getOnlineJobTyped().isValid()); } else { emit validityChanged(false); } } private: Ui::sepaCreditTransferEdit *ui; onlineJobTyped m_onlineJob; KMandatoryFieldGroup* m_requiredFields; bool m_readOnly; bool m_showAllErrors; QSharedPointer taskSettings(); }; #endif // SEPACREDITTRANSFEREDIT_H diff --git a/kmymoney/plugins/onlinetasks/unavailabletask/tasks/unavailabletask.h b/kmymoney/plugins/onlinetasks/unavailabletask/tasks/unavailabletask.h index f94db1293..977283761 100644 --- a/kmymoney/plugins/onlinetasks/unavailabletask/tasks/unavailabletask.h +++ b/kmymoney/plugins/onlinetasks/unavailabletask/tasks/unavailabletask.h @@ -1,70 +1,70 @@ /* * This file is part of KMyMoney, A Personal Finance Manager by KDE * Copyright (C) 2014 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 UNAVAILABLETASK_H #define UNAVAILABLETASK_H #include #include "onlinetasks/interfaces/tasks/onlinetask.h" /** * @brief Task which can be used if original task is unavailable * * This task simply stores the XML data given to it and can write it back. * * The XML storage backend needs to load all tasks into memory. To prevent * data corruption if the original task cannot be loaded, this task can be used. */ class unavailableTask : public onlineTask { public: ONLINETASK_META(unavailableTask, "org.kmymoney.onlineTask.unavailableTask"); - virtual bool isValid() const; - virtual QString jobTypeName() const; + bool isValid() const override; + QString jobTypeName() const override; /** * @name SqlMethods * @{ * For sql databases this plugin is not needed nor used. So these functions * do not have a real implementation. */ - virtual QString storagePluginIid() const; - virtual bool sqlSave(QSqlDatabase databaseConnection, const QString& onlineJobId) const; - virtual bool sqlModify(QSqlDatabase databaseConnection, const QString& onlineJobId) const; - virtual bool sqlRemove(QSqlDatabase databaseConnection, const QString& onlineJobId) const; - virtual onlineTask* createFromSqlDatabase(QSqlDatabase connection, const QString& onlineJobId) const; + QString storagePluginIid() const override; + bool sqlSave(QSqlDatabase databaseConnection, const QString& onlineJobId) const override; + bool sqlModify(QSqlDatabase databaseConnection, const QString& onlineJobId) const override; + bool sqlRemove(QSqlDatabase databaseConnection, const QString& onlineJobId) const override; + onlineTask* createFromSqlDatabase(QSqlDatabase connection, const QString& onlineJobId) const override; /** @} */ protected: - virtual QString responsibleAccount() const; - virtual unavailableTask* createFromXml(const QDomElement& element) const; - virtual void writeXML(QDomDocument& document, QDomElement& parent) const; - virtual bool hasReferenceTo(const QString& id) const; - virtual unavailableTask* clone() const; + QString responsibleAccount() const override; + unavailableTask* createFromXml(const QDomElement& element) const override; + void writeXML(QDomDocument& document, QDomElement& parent) const override; + bool hasReferenceTo(const QString& id) const override; + unavailableTask* clone() const override; private: explicit unavailableTask(const QDomElement& element); /** * The data received by createFromXml(). Written back by writeXML(). */ QDomElement m_data; }; #endif // UNAVAILABLETASK_H diff --git a/kmymoney/plugins/qif/config/mymoneyqifprofileeditor.h b/kmymoney/plugins/qif/config/mymoneyqifprofileeditor.h index a758d214c..24b35e6aa 100644 --- a/kmymoney/plugins/qif/config/mymoneyqifprofileeditor.h +++ b/kmymoney/plugins/qif/config/mymoneyqifprofileeditor.h @@ -1,93 +1,93 @@ /*************************************************************************** kqifprofileeditor.h - description ------------------- begin : Tue Dec 24 2002 copyright : (C) 2002 by Thomas Baumgart email : thb@net-bembel.de ***************************************************************************/ /*************************************************************************** * * * 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 MYMONEYQIFPROFILEEDITOR_H #define MYMONEYQIFPROFILEEDITOR_H // ---------------------------------------------------------------------------- // QT Includes #include #include class QTreeWidgetItem; // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes #include "ui_mymoneyqifprofileeditor.h" #include "mymoneyqifprofile.h" /** * @author Thomas Baumgart */ class MyMoneyQifProfileNameValidator : public QValidator { Q_OBJECT public: explicit MyMoneyQifProfileNameValidator(QObject* o); virtual ~MyMoneyQifProfileNameValidator(); - QValidator::State validate(QString&, int&) const; + QValidator::State validate(QString&, int&) const final override; }; class MyMoneyQifProfileEditor : public QWidget, public Ui::MyMoneyQifProfileEditor { Q_OBJECT public: explicit MyMoneyQifProfileEditor(const bool edit = false, QWidget *parent = 0); virtual ~MyMoneyQifProfileEditor(); /** * This method returns the currently selected profile in the list box. */ const QString selectedProfile() const; protected Q_SLOTS: void slotLoadProfileFromConfig(const QString& name); void slotReset(); void slotRename(); void slotDelete(); void slotNew(); void slotAmountTypeSelected(); void slotDecimalChanged(const QString& val); void slotThousandsChanged(const QString& val); void slotHelp(); private: void loadProfileListFromConfig(); void loadWidgets(); void showProfile(); void addProfile(const QString& name); void deleteProfile(const QString& name); const QString enterName(bool& ok); private: bool m_inEdit; MyMoneyQifProfile m_profile; bool m_isDirty; bool m_isAccepted; QTreeWidgetItem* m_selectedAmountType; }; #endif diff --git a/kmymoney/plugins/qif/import/mymoneyqifreader.cpp b/kmymoney/plugins/qif/import/mymoneyqifreader.cpp index a47f005a2..e9f852678 100644 --- a/kmymoney/plugins/qif/import/mymoneyqifreader.cpp +++ b/kmymoney/plugins/qif/import/mymoneyqifreader.cpp @@ -1,2083 +1,2083 @@ /*************************************************************************** mymoneyqifreader.cpp ------------------- begin : Mon Jan 27 2003 copyright : (C) 2000-2003 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 "mymoneyqifreader.h" // ---------------------------------------------------------------------------- // QT Headers #include #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Headers #include #include #include #include #include #include "kjobwidgets.h" #include "kio/job.h" // ---------------------------------------------------------------------------- // Project Headers #include "mymoneyfile.h" #include "mymoneysecurity.h" #include "mymoneysplit.h" #include "mymoneyexception.h" #include "kmymoneysettings.h" #include "mymoneystatement.h" // define this to debug the code. Using external filters // while debugging did not work too good for me, so I added // this code. // #define DEBUG_IMPORT #ifdef DEBUG_IMPORT #ifdef __GNUC__ #warning "DEBUG_IMPORT defined --> external filter not available!!!!!!!" #endif #endif class MyMoneyQifReader::Private { public: Private() : accountType(eMyMoney::Account::Type::Checkings), firstTransaction(true), mapCategories(true), transactionType(MyMoneyQifReader::QifEntryTypeE::EntryUnknown) {} const QString accountTypeToQif(eMyMoney::Account::Type type) const; /** * finalize the current statement and add it to the statement list */ void finishStatement(); bool isTransfer(QString& name, const QString& leftDelim, const QString& rightDelim); /** * Converts the QIF specific N-record of investment transactions into * a category name */ const QString typeToAccountName(const QString& type) const; /** * Converts the QIF reconcile state to the KMyMoney reconcile state */ eMyMoney::Split::State reconcileState(const QString& state) const; /** */ void fixMultiLineMemo(QString& memo) const; public: /** * the statement that is currently collected/processed */ MyMoneyStatement st; /** * the list of all statements to be sent to MyMoneyStatementReader */ QList statements; /** * a list of already used hashes in this file */ QMap m_hashMap; QString st_AccountName; QString st_AccountId; eMyMoney::Account::Type accountType; bool firstTransaction; bool mapCategories; MyMoneyQifReader::QifEntryTypeE transactionType; }; void MyMoneyQifReader::Private::fixMultiLineMemo(QString& memo) const { memo.replace("\\n", "\n"); } void MyMoneyQifReader::Private::finishStatement() { // in case we have collected any data in the statement, we keep it if ((st.m_listTransactions.count() + st.m_listPrices.count() + st.m_listSecurities.count()) > 0) { statements += st; qDebug("Statement with %d transactions, %d prices and %d securities added to the statement list", st.m_listTransactions.count(), st.m_listPrices.count(), st.m_listSecurities.count()); } eMyMoney::Statement::Type type = st.m_eType; //stash type and... // start with a fresh statement st = MyMoneyStatement(); st.m_skipCategoryMatching = !mapCategories; st.m_eType = type; } const QString MyMoneyQifReader::Private::accountTypeToQif(eMyMoney::Account::Type type) const { QString rc = "Bank"; switch (type) { default: break; case eMyMoney::Account::Type::Cash: rc = "Cash"; break; case eMyMoney::Account::Type::CreditCard: rc = "CCard"; break; case eMyMoney::Account::Type::Asset: rc = "Oth A"; break; case eMyMoney::Account::Type::Liability: rc = "Oth L"; break; case eMyMoney::Account::Type::Investment: rc = "Port"; break; } return rc; } const QString MyMoneyQifReader::Private::typeToAccountName(const QString& type) const { if (type == "reinvint") return i18nc("Category name", "Reinvested interest"); if (type == "reinvdiv") return i18nc("Category name", "Reinvested dividend"); if (type == "reinvlg") return i18nc("Category name", "Reinvested dividend (long term)"); if (type == "reinvsh") return i18nc("Category name", "Reinvested dividend (short term)"); if (type == "div") return i18nc("Category name", "Dividend"); if (type == "intinc") return i18nc("Category name", "Interest"); if (type == "cgshort") return i18nc("Category name", "Capital Gain (short term)"); if (type == "cgmid") return i18nc("Category name", "Capital Gain (mid term)"); if (type == "cglong") return i18nc("Category name", "Capital Gain (long term)"); if (type == "rtrncap") return i18nc("Category name", "Returned capital"); if (type == "miscinc") return i18nc("Category name", "Miscellaneous income"); if (type == "miscexp") return i18nc("Category name", "Miscellaneous expense"); if (type == "sell" || type == "buy") return i18nc("Category name", "Investment fees"); return i18n("Unknown QIF type %1", type); } bool MyMoneyQifReader::Private::isTransfer(QString& tmp, const QString& leftDelim, const QString& rightDelim) { // it's a transfer, extract the account name // I've seen entries like this // // S[Mehrwertsteuer]/_VATCode_N_I (The '/' is the Quicken class symbol) // // so extracting is a bit more complex and we use a regexp for it QRegExp exp(QString("\\%1(.*)\\%2(.*)").arg(leftDelim, rightDelim)); bool rc; if ((rc = (exp.indexIn(tmp) != -1)) == true) { tmp = exp.cap(1) + exp.cap(2); tmp = tmp.trimmed(); } return rc; } eMyMoney::Split::State MyMoneyQifReader::Private::reconcileState(const QString& state) const { if (state == "X" || state == "R") // Reconciled return eMyMoney::Split::State::Reconciled; if (state == "*") // Cleared return eMyMoney::Split::State::Cleared; return eMyMoney::Split::State::NotReconciled; } MyMoneyQifReader::MyMoneyQifReader() : d(new Private), m_file(nullptr), m_extractedLine(0), m_autoCreatePayee(true), m_pos(0), m_linenumber(0), m_ft(nullptr) { m_skipAccount = false; m_transactionsProcessed = m_transactionsSkipped = 0; m_progressCallback = 0; m_file = 0; m_entryType = EntryUnknown; m_processingData = false; m_userAbort = false; m_warnedInvestment = false; m_warnedSecurity = false; m_warnedPrice = false; connect(&m_filter, SIGNAL(bytesWritten(qint64)), this, SLOT(slotSendDataToFilter())); connect(&m_filter, SIGNAL(readyReadStandardOutput()), this, SLOT(slotReceivedDataFromFilter())); connect(&m_filter, SIGNAL(finished(int,QProcess::ExitStatus)), this, SLOT(slotImportFinished())); connect(&m_filter, SIGNAL(readyReadStandardError()), this, SLOT(slotReceivedErrorFromFilter())); } MyMoneyQifReader::~MyMoneyQifReader() { delete m_file; delete d; } void MyMoneyQifReader::setCategoryMapping(bool map) { d->mapCategories = map; } void MyMoneyQifReader::setURL(const QUrl &url) { m_url = url; } void MyMoneyQifReader::setProfile(const QString& profile) { m_qifProfile.loadProfile("Profile-" + profile); } void MyMoneyQifReader::slotSendDataToFilter() { long len; if (m_file->atEnd()) { m_filter.closeWriteChannel(); } else { len = m_file->read(m_buffer, sizeof(m_buffer)); if (len == -1) { qWarning("Failed to read block from QIF import file"); m_filter.closeWriteChannel(); m_filter.kill(); } else { m_filter.write(m_buffer, len); } } } void MyMoneyQifReader::slotReceivedErrorFromFilter() { qWarning("%s", qPrintable(QString(m_filter.readAllStandardError()))); } void MyMoneyQifReader::slotReceivedDataFromFilter() { parseReceivedData(m_filter.readAllStandardOutput()); } void MyMoneyQifReader::parseReceivedData(const QByteArray& data) { const char* buff = data.data(); int len = data.length(); m_pos += len; // signalProgress(m_pos, 0); while (len) { // process char if (*buff == '\n' || *buff == '\r') { // found EOL if (!m_lineBuffer.isEmpty()) { m_qifLines << QString::fromUtf8(m_lineBuffer.trimmed()); } m_lineBuffer = QByteArray(); } else { // collect all others m_lineBuffer += (*buff); } ++buff; --len; } } void MyMoneyQifReader::slotImportFinished() { // check if the last EOL char was missing and add the trailing line if (!m_lineBuffer.isEmpty()) { m_qifLines << QString::fromUtf8(m_lineBuffer.trimmed()); } qDebug("Read %ld bytes", m_pos); QTimer::singleShot(0, this, SLOT(slotProcessData())); } void MyMoneyQifReader::slotProcessData() { signalProgress(-1, -1); // scan the file and try to determine numeric and date formats m_qifProfile.autoDetect(m_qifLines); // the detection is accurate for numeric values, but it could be // that the dates were too ambiguous so that we have to let the user // decide which one to pick. QStringList dateFormats; m_qifProfile.possibleDateFormats(dateFormats); QString format; if (dateFormats.count() > 1) { bool ok; format = QInputDialog::getItem(0, i18n("Date format selection"), i18n("Pick the date format that suits your input file"), dateFormats, 05, false, &ok); if (!ok) { m_userAbort = true; } } else format = dateFormats.first(); if (!format.isEmpty()) { m_qifProfile.setInputDateFormat(format); qDebug("Selected date format: '%s'", qPrintable(format)); } else { // cancel the process because there is probably nothing to work with m_userAbort = true; } signalProgress(0, m_qifLines.count(), i18n("Importing QIF...")); QStringList::iterator it; for (it = m_qifLines.begin(); m_userAbort == false && it != m_qifLines.end(); ++it) { ++m_linenumber; // qDebug("Proc: '%s'", (*it).data()); if ((*it).startsWith('!')) { processQifSpecial(*it); m_qifEntry.clear(); } else if (*it == "^") { if (m_qifEntry.count() > 0) { signalProgress(m_linenumber, 0); processQifEntry(); m_qifEntry.clear(); } } else { m_qifEntry += *it; } } d->finishStatement(); qDebug("%d lines processed", m_linenumber); signalProgress(-1, -1); emit statementsReady(d->statements); } bool MyMoneyQifReader::startImport() { bool rc = false; d->st = MyMoneyStatement(); d->st.m_skipCategoryMatching = !d->mapCategories; m_dontAskAgain.clear(); m_accountTranslation.clear(); m_userAbort = false; m_pos = 0; m_linenumber = 0; m_filename.clear(); m_data.clear(); if (m_url.isEmpty()) { return rc; } else if (m_url.isLocalFile()) { m_filename = m_url.toLocalFile(); } else { m_filename = QDir::tempPath(); if(!m_filename.endsWith(QDir::separator())) m_filename += QDir::separator(); m_filename += m_url.fileName(); qDebug() << "Source:" << m_url.toDisplayString() << "Destination:" << m_filename; KIO::FileCopyJob *job = KIO::file_copy(m_url, QUrl::fromUserInput(m_filename), -1, KIO::Overwrite); // KJobWidgets::setWindow(job, kmymoney); if (job->exec() && job->error()) { KMessageBox::detailedError(0, i18n("Error while loading file '%1'.", m_url.toDisplayString()), job->errorString(), i18n("File access error")); return rc; } } m_file = new QFile(m_filename); if (m_file->open(QIODevice::ReadOnly)) { #ifdef DEBUG_IMPORT qint64 len; while (!m_file->atEnd()) { len = m_file->read(m_buffer, sizeof(m_buffer)); if (len == -1) { qWarning("Failed to read block from QIF import file"); } else { parseReceivedData(QByteArray(m_buffer, len)); } } QTimer::singleShot(0, this, SLOT(slotImportFinished())); rc = true; #else QString program; QStringList arguments; program.clear(); arguments.clear(); // start filter process, use 'cat -' as the default filter if (m_qifProfile.filterScriptImport().isEmpty()) { #ifdef Q_OS_WIN32 //krazy:exclude=cpp // this is the Windows equivalent of 'cat -' but since 'type' does not work with stdin // we pass the filename converted to native separators as a parameter program = "cmd.exe"; arguments << "/c"; arguments << "type"; arguments << QDir::toNativeSeparators(m_filename); #else program = "cat"; arguments << "-"; #endif } else { arguments << m_qifProfile.filterScriptImport().split(' ', QString::KeepEmptyParts); program = arguments.takeFirst(); } m_entryType = EntryUnknown; m_filter.setProcessChannelMode(QProcess::MergedChannels); m_filter.start(program, arguments); if (m_filter.waitForStarted()) { signalProgress(0, m_file->size(), i18n("Reading QIF...")); slotSendDataToFilter(); rc = true; // emit statementsReady(d->statements); } else { KMessageBox::detailedError(0, i18n("Error while running the filter '%1'.", m_filter.program()), m_filter.errorString(), i18n("Filter error")); } #endif } return rc; } void MyMoneyQifReader::processQifSpecial(const QString& _line) { QString line = _line.mid(1); // get rid of exclamation mark if (line.left(5).toLower() == QString("type:")) { line = line.mid(5); // exportable accounts if (line.toLower() == "ccard" || KMyMoneySettings::qifCreditCard().toLower().contains(line.toLower())) { d->accountType = eMyMoney::Account::Type::CreditCard; d->firstTransaction = true; d->transactionType = m_entryType = EntryTransaction; } else if (line.toLower() == "bank" || KMyMoneySettings::qifBank().toLower().contains(line.toLower())) { d->accountType = eMyMoney::Account::Type::Checkings; d->firstTransaction = true; d->transactionType = m_entryType = EntryTransaction; } else if (line.toLower() == "cash" || KMyMoneySettings::qifCash().toLower().contains(line.toLower())) { d->accountType = eMyMoney::Account::Type::Cash; d->firstTransaction = true; d->transactionType = m_entryType = EntryTransaction; } else if (line.toLower() == "oth a" || KMyMoneySettings::qifAsset().toLower().contains(line.toLower())) { d->accountType = eMyMoney::Account::Type::Asset; d->firstTransaction = true; d->transactionType = m_entryType = EntryTransaction; } else if (line.toLower() == "oth l" || line.toLower() == i18nc("QIF tag for liability account", "Oth L").toLower()) { d->accountType = eMyMoney::Account::Type::Liability; d->firstTransaction = true; d->transactionType = m_entryType = EntryTransaction; } else if (line.toLower() == "invst" || line.toLower() == i18nc("QIF tag for investment account", "Invst").toLower()) { d->accountType = eMyMoney::Account::Type::Investment; d->transactionType = m_entryType = EntryInvestmentTransaction; } else if (line.toLower() == "invoice" || KMyMoneySettings::qifInvoice().toLower().contains(line.toLower())) { m_entryType = EntrySkip; } else if (line.toLower() == "tax") { m_entryType = EntrySkip; } else if (line.toLower() == "bill") { m_entryType = EntrySkip; // exportable lists } else if (line.toLower() == "cat" || line.toLower() == i18nc("QIF tag for category", "Cat").toLower()) { m_entryType = EntryCategory; } else if (line.toLower() == "security" || line.toLower() == i18nc("QIF tag for security", "Security").toLower()) { m_entryType = EntrySecurity; } else if (line.toLower() == "prices" || line.toLower() == i18nc("QIF tag for prices", "Prices").toLower()) { m_entryType = EntryPrice; } else if (line.toLower() == "payee") { m_entryType = EntryPayee; } else if (line.toLower() == "memorized") { m_entryType = EntryMemorizedTransaction; } else if (line.toLower() == "class" || line.toLower() == i18nc("QIF tag for a class", "Class").toLower()) { m_entryType = EntryClass; } else if (line.toLower() == "budget") { m_entryType = EntrySkip; } else if (line.toLower() == "invitem") { m_entryType = EntrySkip; } else if (line.toLower() == "template") { m_entryType = EntrySkip; } else { qWarning("Unknown type code '%s' in QIF file on line %d", qPrintable(line), m_linenumber); m_entryType = EntrySkip; } // option headers } else if (line.toLower() == "account") { m_entryType = EntryAccount; } else if (line.toLower() == "option:autoswitch") { m_entryType = EntryAccount; } else if (line.toLower() == "clear:autoswitch") { m_entryType = d->transactionType; } } void MyMoneyQifReader::processQifEntry() { // This method processes a 'QIF Entry' which is everything between two caret // signs // try { switch (m_entryType) { case EntryCategory: processCategoryEntry(); break; case EntryUnknown: qDebug() << "Line " << m_linenumber << ": Warning: Found an entry without a type being specified. Checking assumed."; processTransactionEntry(); break; case EntryTransaction: processTransactionEntry(); break; case EntryInvestmentTransaction: processInvestmentTransactionEntry(); break; case EntryAccount: processAccountEntry(); break; case EntrySecurity: processSecurityEntry(); break; case EntryPrice: processPriceEntry(); break; case EntryPayee: processPayeeEntry(); break; case EntryClass: qDebug() << "Line " << m_linenumber << ": Classes are not yet supported!"; break; case EntryMemorizedTransaction: qDebug() << "Line " << m_linenumber << ": Memorized transactions are not yet implemented!"; break; case EntrySkip: break; default: qDebug() << "Line " << m_linenumber << ": EntryType " << m_entryType << " not yet implemented!"; break; } } catch (const MyMoneyException &e) { if (e.what() != "USERABORT") { qDebug() << "Line " << m_linenumber << ": Unhandled error: " << e.what(); } else { m_userAbort = true; } } } const QString MyMoneyQifReader::extractLine(const QChar& id, int cnt) { QStringList::ConstIterator it; m_extractedLine = -1; for (it = m_qifEntry.constBegin(); it != m_qifEntry.constEnd(); ++it) { ++m_extractedLine; if ((*it)[0] == id) { if (cnt-- == 1) { return (*it).mid(1); } } } m_extractedLine = -1; return QString(); } bool MyMoneyQifReader::extractSplits(QList& listqSplits) const { // *** With apologies to QString MyMoneyQifReader::extractLine *** QStringList::ConstIterator it; bool ret = false; bool memoPresent = false; int neededCount = 0; qSplit q; for (it = m_qifEntry.constBegin(); it != m_qifEntry.constEnd(); ++it) { if (((*it)[0] == 'S') || ((*it)[0] == '$') || ((*it)[0] == 'E')) { memoPresent = false; // in case no memo line in this split if ((*it)[0] == 'E') { q.m_strMemo = (*it).mid(1); // 'E' = Memo d->fixMultiLineMemo(q.m_strMemo); memoPresent = true; // This transaction contains memo } else if ((*it)[0] == 'S') { q.m_strCategoryName = (*it).mid(1); // 'S' = CategoryName neededCount ++; } else if ((*it)[0] == '$') { q.m_amount = (*it).mid(1); // '$' = Amount neededCount ++; } if (neededCount > 1) { // CategoryName & Amount essential listqSplits += q; // Add valid split if (!memoPresent) { // If no memo, clear previous q.m_strMemo.clear(); } - qSplit q; // Start new split + q = qSplit(); // Start new split neededCount = 0; ret = true; } } } return ret; } #if 0 void MyMoneyQifReader::processMSAccountEntry(const eMyMoney::Account::Type accountType) { if (extractLine('P').toLower() == m_qifProfile.openingBalanceText().toLower()) { m_account = MyMoneyAccount(); m_account.setAccountType(accountType); QString txt = extractLine('T'); MyMoneyMoney balance = m_qifProfile.value('T', txt); QDate date = m_qifProfile.date(extractLine('D')); m_account.setOpeningDate(date); QString name = extractLine('L'); if (name.left(1) == m_qifProfile.accountDelimiter().left(1)) { name = name.mid(1, name.length() - 2); } d->st_AccountName = name; m_account.setName(name); selectOrCreateAccount(Select, m_account, balance); d->st.m_accountId = m_account.id(); if (! balance.isZero()) { MyMoneyFile* file = MyMoneyFile::instance(); QString openingtxid = file->openingBalanceTransaction(m_account); MyMoneyFileTransaction ft; if (! openingtxid.isEmpty()) { MyMoneyTransaction openingtx = file->transaction(openingtxid); MyMoneySplit split = openingtx.splitByAccount(m_account.id()); if (split.shares() != balance) { const MyMoneySecurity& sec = file->security(m_account.currencyId()); if (KMessageBox::questionYesNo( KMyMoneyUtils::mainWindow(), i18n("The %1 account currently has an opening balance of %2. This QIF file reports an opening balance of %3. Would you like to overwrite the current balance with the one from the QIF file?", m_account.name(), split.shares().formatMoney(m_account, sec), balance.formatMoney(m_account, sec)), i18n("Overwrite opening balance"), KStandardGuiItem::yes(), KStandardGuiItem::no(), "OverwriteOpeningBalance") == KMessageBox::Yes) { file->removeTransaction(openingtx); m_account.setOpeningDate(date); file->createOpeningBalanceTransaction(m_account, balance); } } } else { // Add an opening balance m_account.setOpeningDate(date); file->createOpeningBalanceTransaction(m_account, balance); } ft.commit(); } } else { // for some unknown reason, Quicken 2001 generates the following (somewhat // misleading) sequence of lines: // // 1: !Account // 2: NAT&T Universal // 3: DAT&T Univers(...xxxx) [CLOSED] // 4: TCCard // 5: ^ // 6: !Type:CCard // 7: !Account // 8: NCFCU Visa // 9: DRick's CFCU Visa card (...xxxx) // 10: TCCard // 11: ^ // 12: !Type:CCard // 13: D1/ 4' 1 // // Lines 1-5 are processed via processQifEntry() and processAccountEntry() // Then Quicken issues line 6 but since the account does not carry any // transaction does not write an end delimiter. Arrrgh! So we end up with // a QIF entry comprising of lines 6-11 and end up in this routine. Actually, // lines 7-11 are the leadin for the next account. So we check here if // the !Type:xxx record also contains an !Account line and process the // entry as required. // // (Ace) I think a better solution here is to handle exclamation point // lines separately from entries. In the above case: // Line 1 would set the mode to "account entries". // Lines 2-5 would be interpreted as an account entry. This would set m_account. // Line 6 would set the mode to "cc transaction entries". // Line 7 would immediately set the mode to "account entries" again // Lines 8-11 would be interpreted as an account entry. This would set m_account. // Line 12 would set the mode to "cc transaction entries" // Lines 13+ would be interpreted as cc transaction entries, and life is good int exclamationCnt = 1; QString category; do { category = extractLine('!', exclamationCnt++); } while (!category.isEmpty() && category != "Account"); // we have such a weird empty account if (category == "Account") { processAccountEntry(); } else { selectOrCreateAccount(Select, m_account); d->st_AccountName = m_account.name(); d->st.m_strAccountName = m_account.name(); d->st.m_accountId = m_account.id(); d->st.m_strAccountNumber = m_account.id(); m_account.setNumber(m_account.id()); if (m_entryType == EntryInvestmentTransaction) processInvestmentTransactionEntry(); else processTransactionEntry(); } } } #endif void MyMoneyQifReader::processPayeeEntry() { // TODO } void MyMoneyQifReader::processCategoryEntry() { MyMoneyFile* file = MyMoneyFile::instance(); MyMoneyAccount account = MyMoneyAccount(); account.setName(extractLine('N')); account.setDescription(extractLine('D')); MyMoneyAccount parentAccount; //The extractline routine will more than likely return 'empty', // so also have to test that either the 'I' or 'E' was detected //and set up accounts accordingly. if ((!extractLine('I').isEmpty()) || (m_extractedLine != -1)) { account.setAccountType(eMyMoney::Account::Type::Income); parentAccount = file->income(); } else if ((!extractLine('E').isEmpty()) || (m_extractedLine != -1)) { account.setAccountType(eMyMoney::Account::Type::Expense); parentAccount = file->expense(); } // check if we can find the account already in the file auto acc = findAccount(account, MyMoneyAccount()); // if not, we just create it if (acc.id().isEmpty()) { MyMoneyAccount brokerage; file->createAccount(account, parentAccount, brokerage, MyMoneyMoney()); } } MyMoneyAccount MyMoneyQifReader::findAccount(const MyMoneyAccount& acc, const MyMoneyAccount& parent) const { static MyMoneyAccount nullAccount; MyMoneyFile* file = MyMoneyFile::instance(); QList parents; try { // search by id if (!acc.id().isEmpty()) { return file->account(acc.id()); } // collect the parents. in case parent does not have an id, we scan the all top-level accounts if (parent.id().isEmpty()) { parents << file->asset(); parents << file->liability(); parents << file->income(); parents << file->expense(); parents << file->equity(); } else { parents << parent; } QList::const_iterator it_p; for (it_p = parents.constBegin(); it_p != parents.constEnd(); ++it_p) { MyMoneyAccount parentAccount = *it_p; // search by name (allow hierarchy) int pos; // check for ':' in the name and use it as separator for a hierarchy QString name = acc.name(); bool notFound = false; while ((pos = name.indexOf(MyMoneyFile::AccountSeparator)) != -1) { QString part = name.left(pos); QString remainder = name.mid(pos + 1); const auto existingAccount = file->subAccountByName(parentAccount, part); // if account has not been found, continue with next top level parent if (existingAccount.id().isEmpty()) { notFound = true; break; } parentAccount = existingAccount; name = remainder; } if (notFound) continue; const auto existingAccount = file->subAccountByName(parentAccount, name); if (!existingAccount.id().isEmpty()) { if (acc.accountType() != eMyMoney::Account::Type::Unknown) { if (acc.accountType() != existingAccount.accountType()) continue; } return existingAccount; } } } catch (const MyMoneyException &e) { KMessageBox::error(0, i18n("Unable to find account: %1", e.what())); } return nullAccount; } const QString MyMoneyQifReader::transferAccount(const QString& name, bool useBrokerage) { QString accountId; QStringList tmpEntry = m_qifEntry; // keep temp copies MyMoneyAccount tmpAccount = m_account; m_qifEntry.clear(); // and construct a temp entry to create/search the account m_qifEntry << QString("N%1").arg(name); m_qifEntry << QString("Tunknown"); m_qifEntry << QString("D%1").arg(i18n("Autogenerated by QIF importer")); accountId = processAccountEntry(false); // in case we found a reference to an investment account, we need // to switch to the brokerage account instead. MyMoneyAccount acc = MyMoneyFile::instance()->account(accountId); if (useBrokerage && (acc.accountType() == eMyMoney::Account::Type::Investment)) { m_qifEntry.clear(); // and construct a temp entry to create/search the account m_qifEntry << QString("N%1").arg(acc.brokerageName()); m_qifEntry << QString("Tunknown"); m_qifEntry << QString("D%1").arg(i18n("Autogenerated by QIF importer")); accountId = processAccountEntry(false); } m_qifEntry = tmpEntry; // restore local copies m_account = tmpAccount; return accountId; } void MyMoneyQifReader::createOpeningBalance(eMyMoney::Account::Type accType) { MyMoneyFile* file = MyMoneyFile::instance(); // if we don't have a name for the current account we need to extract the name from the L-record if (m_account.name().isEmpty()) { QString name = extractLine('L'); if (name.isEmpty()) { name = i18n("QIF imported, no account name supplied"); } auto b = d->isTransfer(name, m_qifProfile.accountDelimiter().left(1), m_qifProfile.accountDelimiter().mid(1, 1)); Q_UNUSED(b) QStringList entry = m_qifEntry; // keep a temp copy m_qifEntry.clear(); // and construct a temp entry to create/search the account m_qifEntry << QString("N%1").arg(name); m_qifEntry << QString("T%1").arg(d->accountTypeToQif(accType)); m_qifEntry << QString("D%1").arg(i18n("Autogenerated by QIF importer")); processAccountEntry(); m_qifEntry = entry; // restore local copy } MyMoneyFileTransaction ft; try { bool needCreate = true; MyMoneyAccount acc = m_account; // in case we're dealing with an investment account, we better use // the accompanying brokerage account for the opening balance acc = file->accountByName(m_account.brokerageName()); // check if we already have an opening balance transaction QString tid = file->openingBalanceTransaction(acc); MyMoneyTransaction ot; if (!tid.isEmpty()) { ot = file->transaction(tid); MyMoneySplit s0 = ot.splitByAccount(acc.id()); // if the value is the same, we can silently skip this transaction if (s0.shares() == m_qifProfile.value('T', extractLine('T'))) { needCreate = false; } if (needCreate) { // in case we create it anyway, we issue a warning to the user to check it manually KMessageBox::sorry(0, QString("%1").arg(i18n("KMyMoney has imported a second opening balance transaction into account %1 which differs from the one found already on file. Please correct this manually once the import is done.", acc.name())), i18n("Opening balance problem")); } } if (needCreate) { acc.setOpeningDate(m_qifProfile.date(extractLine('D'))); file->modifyAccount(acc); MyMoneyTransaction t = file->createOpeningBalanceTransaction(acc, m_qifProfile.value('T', extractLine('T'))); if (!t.id().isEmpty()) { t.setImported(); file->modifyTransaction(t); } ft.commit(); } // make sure to use the updated version of the account if (m_account.id() == acc.id()) m_account = acc; // remember which account we created d->st.m_accountId = m_account.id(); } catch (const MyMoneyException &e) { KMessageBox::detailedError(0, i18n("Error while creating opening balance transaction"), QString("%1(%2):%3").arg(e.file()).arg(e.line()).arg(e.what()), i18n("File access error")); } } void MyMoneyQifReader::processTransactionEntry() { ++m_transactionsProcessed; // in case the user selected to skip the account or the account // was not found we skip this transaction /* if(m_account.id().isEmpty()) { m_transactionsSkipped++; return; } */ MyMoneyFile* file = MyMoneyFile::instance(); MyMoneyStatement::Split s1; MyMoneyStatement::Transaction tr; QString tmp; QString accountId; int pos; QString payee = extractLine('P'); unsigned long h; h = MyMoneyTransaction::hash(m_qifEntry.join(";")); QString hashBase; hashBase.sprintf("%s-%07lx", qPrintable(m_qifProfile.date(extractLine('D')).toString(Qt::ISODate)), h); int idx = 1; QString hash; for (;;) { hash = QString("%1-%2").arg(hashBase).arg(idx); QMap::const_iterator it; it = d->m_hashMap.constFind(hash); if (it == d->m_hashMap.constEnd()) { d->m_hashMap[hash] = true; break; } ++idx; } tr.m_strBankID = hash; if (d->firstTransaction) { // check if this is an opening balance transaction and process it out of the statement if (!payee.isEmpty() && ((payee.toLower() == "opening balance") || KMyMoneySettings::qifOpeningBalance().toLower().contains(payee.toLower()))) { createOpeningBalance(d->accountType); d->firstTransaction = false; return; } } // Process general transaction data if (d->st.m_accountId.isEmpty()) d->st.m_accountId = m_account.id(); s1.m_accountId = d->st.m_accountId; switch (d->accountType) { case eMyMoney::Account::Type::Checkings: d->st.m_eType=eMyMoney::Statement::Type::Checkings; break; case eMyMoney::Account::Type::Savings: d->st.m_eType=eMyMoney::Statement::Type::Savings; break; case eMyMoney::Account::Type::Investment: d->st.m_eType=eMyMoney::Statement::Type::Investment; break; case eMyMoney::Account::Type::CreditCard: d->st.m_eType=eMyMoney::Statement::Type::CreditCard; break; default: d->st.m_eType=eMyMoney::Statement::Type::None; break; } tr.m_datePosted = (m_qifProfile.date(extractLine('D'))); if (!tr.m_datePosted.isValid()) { int rc = KMessageBox::warningContinueCancel(0, i18n("The date entry \"%1\" read from the file cannot be interpreted through the current " "date profile setting of \"%2\".\n\nPressing \"Continue\" will " "assign todays date to the transaction. Pressing \"Cancel\" will abort " "the import operation. You can then restart the import and select a different " "QIF profile or create a new one.", extractLine('D'), m_qifProfile.inputDateFormat()), i18n("Invalid date format")); switch (rc) { case KMessageBox::Continue: tr.m_datePosted = (QDate::currentDate()); break; case KMessageBox::Cancel: throw MYMONEYEXCEPTION("USERABORT"); break; } } tmp = extractLine('L'); pos = tmp.lastIndexOf("--"); if (tmp.left(1) == m_qifProfile.accountDelimiter().left(1)) { // it's a transfer, so we wipe the memo // tmp = ""; why?? // st.m_strAccountName = tmp; } else if (pos != -1) { // what's this? // t.setValue("Dialog", tmp.mid(pos+2)); tmp = tmp.left(pos); } // t.setMemo(tmp); // Assign the "#" field to the transaction's bank id // This is the custom KMM extension to QIF for a unique ID tmp = extractLine('#'); if (!tmp.isEmpty()) { tr.m_strBankID = QString("ID %1").arg(tmp); } #if 0 // Collect data for the account's split s1.m_accountId = m_account.id(); tmp = extractLine('S'); pos = tmp.findRev("--"); if (pos != -1) { tmp = tmp.left(pos); } if (tmp.left(1) == m_qifProfile.accountDelimiter().left(1)) // it's a transfer, extract the account name tmp = tmp.mid(1, tmp.length() - 2); s1.m_strCategoryName = tmp; #endif // TODO (Ace) Deal with currencies more gracefully. QIF cannot deal with multiple // currencies, so we should assume that transactions imported into a given // account are in THAT ACCOUNT's currency. If one of those involves a transfer // to an account with a different currency, value and shares should be // different. (Shares is in the target account's currency, value is in the // transaction's) s1.m_amount = m_qifProfile.value('T', extractLine('T')); tr.m_amount = m_qifProfile.value('T', extractLine('T')); tr.m_shares = m_qifProfile.value('T', extractLine('T')); tmp = extractLine('N'); if (!tmp.isEmpty()) tr.m_strNumber = tmp; if (!payee.isEmpty()) { tr.m_strPayee = payee; } tr.m_reconcile = d->reconcileState(extractLine('C')); tr.m_strMemo = extractLine('M'); d->fixMultiLineMemo(tr.m_strMemo); s1.m_strMemo = tr.m_strMemo; // tr.m_listSplits.append(s1); // split transaction // ****** ensure each field is ****** // * attached to correct split * QList listqSplits; if (! extractSplits(listqSplits)) { MyMoneyAccount account; // use the same values for the second split, but clear the ID and reverse the value MyMoneyStatement::Split s2 = s1; s2.m_reconcile = tr.m_reconcile; s2.m_amount = (-s1.m_amount); // s2.clearId(); // standard transaction tmp = extractLine('L'); if (d->isTransfer(tmp, m_qifProfile.accountDelimiter().left(1), m_qifProfile.accountDelimiter().mid(1, 1))) { accountId = transferAccount(tmp, false); } else { /* pos = tmp.findRev("--"); if(pos != -1) { t.setValue("Dialog", tmp.mid(pos+2)); tmp = tmp.left(pos); }*/ // it's an expense / income tmp = tmp.trimmed(); accountId = file->checkCategory(tmp, s1.m_amount, s2.m_amount); } if (!accountId.isEmpty()) { try { - MyMoneyAccount account = file->account(accountId); + account = file->account(accountId); // FIXME: check that the type matches and ask if not if (account.accountType() == eMyMoney::Account::Type::Investment) { qDebug() << "Line " << m_linenumber << ": Cannot transfer to/from an investment account. Transaction ignored."; return; } if (account.id() == m_account.id()) { qDebug() << "Line " << m_linenumber << ": Cannot transfer to the same account. Transfer ignored."; accountId.clear(); } } catch (const MyMoneyException &) { qDebug() << "Line " << m_linenumber << ": Account with id " << accountId.data() << " not found"; accountId.clear(); } } if (!accountId.isEmpty()) { s2.m_accountId = accountId; s2.m_strCategoryName = tmp; tr.m_listSplits.append(s2); } } else { int count; for (count = 1; count <= listqSplits.count(); ++count) { // Use true splits count MyMoneyStatement::Split s2 = s1; s2.m_amount = (-m_qifProfile.value('$', listqSplits[count-1].m_amount)); // Amount of split s2.m_strMemo = listqSplits[count-1].m_strMemo; // Memo in split tmp = listqSplits[count-1].m_strCategoryName; // Category in split if (d->isTransfer(tmp, m_qifProfile.accountDelimiter().left(1), m_qifProfile.accountDelimiter().mid(1, 1))) { accountId = transferAccount(tmp, false); } else { pos = tmp.lastIndexOf("--"); if (pos != -1) { tmp = tmp.left(pos); } tmp = tmp.trimmed(); accountId = file->checkCategory(tmp, s1.m_amount, s2.m_amount); } if (!accountId.isEmpty()) { try { MyMoneyAccount account = file->account(accountId); // FIXME: check that the type matches and ask if not if (account.accountType() == eMyMoney::Account::Type::Investment) { qDebug() << "Line " << m_linenumber << ": Cannot convert a split transfer to/from an investment account. Split removed. Total amount adjusted from " << tr.m_amount.formatMoney("", 2) << " to " << (tr.m_amount + s2.m_amount).formatMoney("", 2) << "\n"; tr.m_amount += s2.m_amount; continue; } if (account.id() == m_account.id()) { qDebug() << "Line " << m_linenumber << ": Cannot transfer to the same account. Transfer ignored."; accountId.clear(); } } catch (const MyMoneyException &) { qDebug() << "Line " << m_linenumber << ": Account with id " << accountId.data() << " not found"; accountId.clear(); } } if (!accountId.isEmpty()) { s2.m_accountId = accountId; s2.m_strCategoryName = tmp; tr.m_listSplits += s2; // in case the transaction does not have a memo and we // process the first split just copy the memo over if (tr.m_listSplits.count() == 1 && tr.m_strMemo.isEmpty()) tr.m_strMemo = s2.m_strMemo; } else { // TODO add an option to create a "Unassigned" category // for now, we just drop the split which will show up as unbalanced // transaction in the KMyMoney ledger view } } } // Add the transaction to the statement d->st.m_listTransactions += tr; } void MyMoneyQifReader::processInvestmentTransactionEntry() { // qDebug() << "Investment Transaction:" << m_qifEntry.count() << " lines"; /* Items for Investment Accounts Field Indicator Explanation D Date N Action Y Security (NAME, not symbol) I Price Q Quantity (number of shares or split ratio) T Transaction amount C Cleared status P Text in the first line for transfers and reminders (Payee) M Memo O Commission L Account for the transfer $ Amount transferred ^ End of the entry It will be presumed all transactions are to the associated cash account, if one exists, unless otherwise noted by the 'L' field. Expense/Income categories will be automatically generated, "_Dividend", "_InterestIncome", etc. */ MyMoneyStatement::Transaction tr; d->st.m_eType = eMyMoney::Statement::Type::Investment; // t.setCommodity(m_account.currencyId()); // 'D' field: Date QDate date = m_qifProfile.date(extractLine('D')); if (date.isValid()) tr.m_datePosted = date; else { int rc = KMessageBox::warningContinueCancel(0, i18n("The date entry \"%1\" read from the file cannot be interpreted through the current " "date profile setting of \"%2\".\n\nPressing \"Continue\" will " "assign todays date to the transaction. Pressing \"Cancel\" will abort " "the import operation. You can then restart the import and select a different " "QIF profile or create a new one.", extractLine('D'), m_qifProfile.inputDateFormat()), i18n("Invalid date format")); switch (rc) { case KMessageBox::Continue: tr.m_datePosted = QDate::currentDate(); break; case KMessageBox::Cancel: throw MYMONEYEXCEPTION("USERABORT"); break; } } // 'M' field: Memo QString memo = extractLine('M'); d->fixMultiLineMemo(memo); tr.m_strMemo = memo; unsigned long h; h = MyMoneyTransaction::hash(m_qifEntry.join(";")); QString hashBase; hashBase.sprintf("%s-%07lx", qPrintable(m_qifProfile.date(extractLine('D')).toString(Qt::ISODate)), h); int idx = 1; QString hash; for (;;) { hash = QString("%1-%2").arg(hashBase).arg(idx); QMap::const_iterator it; it = d->m_hashMap.constFind(hash); if (it == d->m_hashMap.constEnd()) { d->m_hashMap[hash] = true; break; } ++idx; } tr.m_strBankID = hash; // '#' field: BankID QString tmp = extractLine('#'); if (! tmp.isEmpty()) tr.m_strBankID = QString("ID %1").arg(tmp); // Reconciliation flag tr.m_reconcile = d->reconcileState(extractLine('C')); // 'O' field: Fees tr.m_fees = m_qifProfile.value('T', extractLine('O')); // 'T' field: Amount MyMoneyMoney amount = m_qifProfile.value('T', extractLine('T')); tr.m_amount = amount; MyMoneyStatement::Price price; price.m_date = date; price.m_strSecurity = extractLine('Y'); price.m_amount = m_qifProfile.value('T', extractLine('I')); #if 0 // we must check for that later, because certain activities don't need a security // 'Y' field: Security name QString securityname = extractLine('Y').toLower(); if (securityname.isEmpty()) { qDebug() << "Line " << m_linenumber << ": Investment transaction without a security is not supported."; return; } tr.m_strSecurity = securityname; #endif #if 0 // For now, we let the statement reader take care of that. // The big problem here is that the Y field is not the SYMBOL, it's the NAME. // The name is not very unique, because people could have used slightly different // abbreviations or ordered words differently, etc. // // If there is a perfect name match with a subordinate stock account, great. // More likely, we have to rely on the QIF file containing !Type:Security // records, which tell us the mapping from name to symbol. // // Therefore, generally it is not recommended to import a QIF file containing // investment transactions but NOT containing security records. QString securitysymbol = m_investmentMap[securityname]; // the correct account is the stock account which matches two criteria: // (1) it is a sub-account of the selected investment account, and either // (2a) the security name of the transaction matches the name of the security, OR // (2b) the security name of the transaction maps to a symbol which matches the symbol of the security // search through each subordinate account bool found = false; MyMoneyAccount thisaccount = m_account; QStringList accounts = thisaccount.accountList(); QStringList::const_iterator it_account = accounts.begin(); while (!found && it_account != accounts.end()) { QString currencyid = file->account(*it_account).currencyId(); MyMoneySecurity security = file->security(currencyid); QString symbol = security.tradingSymbol().toLower(); QString name = security.name().toLower(); if (securityname == name || securitysymbol == symbol) { d->st_AccountId = *it_account; s1.m_accountId = *it_account; thisaccount = file->account(*it_account); found = true; #if 0 // update the price, while we're here. in the future, this should be // an option QString basecurrencyid = file->baseCurrency().id(); MyMoneyPrice price = file->price(currencyid, basecurrencyid, t_in.m_datePosted, true); if (!price.isValid()) { MyMoneyPrice newprice(currencyid, basecurrencyid, t_in.m_datePosted, t_in.m_moneyAmount / t_in.m_dShares, i18n("Statement Importer")); file->addPrice(newprice); } #endif } ++it_account; } if (!found) { qDebug() << "Line " << m_linenumber << ": Security " << securityname << " not found in this account. Transaction ignored."; // If the security is not known, notify the user // TODO (Ace) A "SelectOrCreateAccount" interface for investments KMessageBox::information(0, i18n("This investment account does not contain the \"%1\" security. " "Transactions involving this security will be ignored.", securityname), i18n("Security not found"), QString("MissingSecurity%1").arg(securityname.trimmed())); return; } #endif // 'Y' field: Security tr.m_strSecurity = extractLine('Y'); // 'Q' field: Quantity MyMoneyMoney quantity = m_qifProfile.value('T', extractLine('Q')); // 'N' field: Action QString action = extractLine('N').toLower(); // remove trailing X, which seems to have no purpose (?!) bool xAction = false; if (action.endsWith('x')) { action = action.left(action.length() - 1); xAction = true; } tmp = extractLine('L'); // if the action ends in an X, the L-Record contains the asset account // to which the dividend should be transferred. In the other cases, it // may contain a category that identifies the income category for the // dividend payment if ((xAction == true) || (d->isTransfer(tmp, m_qifProfile.accountDelimiter().left(1), m_qifProfile.accountDelimiter().mid(1, 1)) == true)) { tmp = tmp.remove(QRegExp("[\\[\\]]")); // xAction != true so ignore any'[ and ]' if (!tmp.isEmpty()) { // use 'L' record name tr.m_strBrokerageAccount = tmp; transferAccount(tmp); // make sure the account exists } else { tr.m_strBrokerageAccount = m_account.brokerageName();// use brokerage account transferAccount(m_account.brokerageName()); // make sure the account exists } } else { tmp = tmp.remove(QRegExp("[\\[\\]]")); // xAction != true so ignore any'[ and ]' tr.m_strInterestCategory = tmp; tr.m_strBrokerageAccount = m_account.brokerageName(); } // Whether to create a cash split for the other side of the value QString accountname; //= extractLine('L'); if (action == "reinvint" || action == "reinvdiv" || action == "reinvlg" || action == "reinvsh") { d->st.m_listPrices += price; tr.m_shares = quantity; tr.m_eAction = (eMyMoney::Transaction::Action::ReinvestDividend); tr.m_price = m_qifProfile.value('I', extractLine('I')); tr.m_strInterestCategory = extractLine('L'); if (tr.m_strInterestCategory.isEmpty()) { tr.m_strInterestCategory = d->typeToAccountName(action); } } else if (action == "div" || action == "cgshort" || action == "cgmid" || action == "cglong" || action == "rtrncap") { tr.m_eAction = (eMyMoney::Transaction::Action::CashDividend); // make sure, we have valid category. Either taken from the L-Record above, // or derived from the action code if (tr.m_strInterestCategory.isEmpty()) { tr.m_strInterestCategory = d->typeToAccountName(action); } // For historic reasons (coming from the OFX importer) the statement // reader expects the dividend with a reverse sign. So we just do that. tr.m_amount -= tr.m_fees; // We need an extra split which 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. MyMoneyStatement::Split s2; s2.m_amount = MyMoneyMoney(); s2.m_strCategoryName = extractLine('Y'); tr.m_listSplits.append(s2); } else if (action == "intinc" || action == "miscinc" || action == "miscexp") { tr.m_eAction = (eMyMoney::Transaction::Action::Interest); if (action == "miscexp") tr.m_eAction = (eMyMoney::Transaction::Action::Fees); // make sure, we have a valid category. Either taken from the L-Record above, // or derived from the action code if (tr.m_strInterestCategory.isEmpty()) { tr.m_strInterestCategory = d->typeToAccountName(action); } if (action == "intinc") { - MyMoneyMoney price = m_qifProfile.value('I', extractLine('I')); + MyMoneyMoney priceValue = m_qifProfile.value('I', extractLine('I')); tr.m_amount -= tr.m_fees; - if ((!quantity.isZero()) && (!price.isZero())) - tr.m_amount = -(quantity * price); + if ((!quantity.isZero()) && (!priceValue.isZero())) + tr.m_amount = -(quantity * priceValue); } else // For historic reasons (coming from the OFX importer) the statement // reader expects the dividend with a reverse sign. So we just do that. if (action != "miscexp") tr.m_amount = -(amount - tr.m_fees); if (tr.m_strMemo.isEmpty()) tr.m_strMemo = (QString("%1 %2").arg(extractLine('Y')).arg(d->typeToAccountName(action))).trimmed(); } else if (action == "xin" || action == "xout") { QString payee = extractLine('P'); if (!payee.isEmpty() && ((payee.toLower() == "opening balance") || KMyMoneySettings::qifOpeningBalance().toLower().contains(payee.toLower()))) { createOpeningBalance(eMyMoney::Account::Type::Investment); return; } tr.m_eAction = (eMyMoney::Transaction::Action::None); MyMoneyStatement::Split s2; - QString tmp = extractLine('L'); + tmp = extractLine('L'); if (d->isTransfer(tmp, m_qifProfile.accountDelimiter().left(1), m_qifProfile.accountDelimiter().mid(1, 1))) { s2.m_accountId = transferAccount(tmp); s2.m_strCategoryName = tmp; } else { s2.m_strCategoryName = extractLine('L'); if (tr.m_strInterestCategory.isEmpty()) { s2.m_strCategoryName = d->typeToAccountName(action); } } if (action == "xout") tr.m_amount = -tr.m_amount; s2.m_amount = -tr.m_amount; tr.m_listSplits.append(s2); } else if (action == "buy") { d->st.m_listPrices += price; tr.m_price = m_qifProfile.value('I', extractLine('I')); tr.m_shares = quantity; tr.m_amount = -amount; tr.m_eAction = (eMyMoney::Transaction::Action::Buy); } else if (action == "sell") { d->st.m_listPrices += price; tr.m_price = m_qifProfile.value('I', extractLine('I')); tr.m_shares = -quantity; tr.m_amount = amount; tr.m_eAction = (eMyMoney::Transaction::Action::Sell); } else if (action == "shrsin") { tr.m_shares = quantity; tr.m_eAction = (eMyMoney::Transaction::Action::Shrsin); } else if (action == "shrsout") { tr.m_shares = -quantity; tr.m_eAction = (eMyMoney::Transaction::Action::Shrsout); } else if (action == "stksplit") { MyMoneyMoney splitfactor = (quantity / MyMoneyMoney(10, 1)).reduce(); // Stock splits not supported // qDebug() << "Line " << m_linenumber << ": Stock split not supported (date=" << date << " security=" << securityname << " factor=" << splitfactor.toString() << ")"; // s1.setShares(splitfactor); // s1.setValue(0); // s1.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::SplitShares)); // return; } else { // Unsupported action type qDebug() << "Line " << m_linenumber << ": Unsupported transaction action (" << action << ")"; return; } d->st.m_strAccountName = accountname; // accountname appears not to get set d->st.m_listTransactions += tr; /************************************************************************* * * These transactions are natively supported by KMyMoney * *************************************************************************/ /* D1/ 3' 5 NShrsIn YGENERAL MOTORS CORP 52BR1 I20 Q200 U4,000.00 T4,000.00 M200 shares added to account @ $20/share ^ */ /* ^ D1/14' 5 NShrsOut YTEMPLETON GROWTH 97GJ0 Q50 90 ^ */ /* D1/28' 5 NBuy YGENERAL MOTORS CORP 52BR1 I24.35 Q100 U2,435.00 T2,435.00 ^ */ /* D1/ 5' 5 NSell YUnited Vanguard I8.41 Q50 U420.50 T420.50 ^ */ /* D1/ 7' 5 NReinvDiv YFRANKLIN INCOME 97GM2 I38 Q1 U38.00 T38.00 ^ */ /************************************************************************* * * These transactions are all different kinds of income. (Anything that * follows the DNYUT pattern). They are all handled the same, the only * difference is which income account the income is placed into. By * default, it's placed into _xxx where xxx is the right side of the * N field. e.g. NDiv transaction goes into the _Div account * *************************************************************************/ /* D1/10' 5 NDiv YTEMPLETON GROWTH 97GJ0 U10.00 T10.00 ^ */ /* D1/10' 5 NIntInc YTEMPLETON GROWTH 97GJ0 U20.00 T20.00 ^ */ /* D1/10' 5 NCGShort YTEMPLETON GROWTH 97GJ0 U111.00 T111.00 ^ */ /* D1/10' 5 NCGLong YTEMPLETON GROWTH 97GJ0 U333.00 T333.00 ^ */ /* D1/10' 5 NCGMid YTEMPLETON GROWTH 97GJ0 U222.00 T222.00 ^ */ /* D2/ 2' 5 NRtrnCap YFRANKLIN INCOME 97GM2 U1,234.00 T1,234.00 ^ */ /************************************************************************* * * These transactions deal with miscellaneous activity that KMyMoney * does not support, but may support in the future. * *************************************************************************/ /* Note the Q field is the split ratio per 10 shares, so Q12.5 is a 12.5:10 split, otherwise known as 5:4. D1/14' 5 NStkSplit YIBM Q12.5 ^ */ /************************************************************************* * * These transactions deal with short positions and options, which are * not supported at all by KMyMoney. They will be ignored for now. * There may be a way to hack around this, by creating a new security * "IBM_Short". * *************************************************************************/ /* D1/21' 5 NShtSell YIBM I92.38 Q100 U9,238.00 T9,238.00 ^ */ /* D1/28' 5 NCvrShrt YIBM I92.89 Q100 U9,339.00 T9,339.00 O50.00 ^ */ /* D6/ 1' 5 NVest YIBM Option Q20 ^ */ /* D6/ 8' 5 NExercise YIBM Option I60.952381 Q20 MFrom IBM Option Grant 6/1/2004 ^ */ /* D6/ 1'14 NExpire YIBM Option Q5 ^ */ /************************************************************************* * * These transactions do not have an associated investment ("Y" field) * so presumably they are only valid for the cash account. Once I * understand how these are really implemented, they can probably be * handled without much trouble. * *************************************************************************/ /* D1/14' 5 NCash U-100.00 T-100.00 LBank Chrg ^ */ /* D1/15' 5 NXOut U500.00 T500.00 L[CU Savings] $500.00 ^ */ /* D1/28' 5 NXIn U1,000.00 T1,000.00 L[CU Checking] $1,000.00 ^ */ /* D1/25' 5 NMargInt U25.00 T25.00 ^ */ } const QString MyMoneyQifReader::findOrCreateIncomeAccount(const QString& searchname) { QString result; MyMoneyFile *file = MyMoneyFile::instance(); // First, try to find this account as an income account MyMoneyAccount acc = file->income(); QStringList list = acc.accountList(); QStringList::ConstIterator it_accid = list.constBegin(); while (it_accid != list.constEnd()) { acc = file->account(*it_accid); if (acc.name() == searchname) { result = *it_accid; break; } ++it_accid; } // If we did not find the account, now we must create one. if (result.isEmpty()) { - MyMoneyAccount acc; - acc.setName(searchname); - acc.setAccountType(eMyMoney::Account::Type::Income); + MyMoneyAccount newAccount; + newAccount.setName(searchname); + newAccount.setAccountType(eMyMoney::Account::Type::Income); MyMoneyAccount income = file->income(); MyMoneyFileTransaction ft; - file->addAccount(acc, income); + file->addAccount(newAccount, income); ft.commit(); - result = acc.id(); + result = newAccount.id(); } return result; } // TODO (Ace) Combine this and the previous function const QString MyMoneyQifReader::findOrCreateExpenseAccount(const QString& searchname) { QString result; MyMoneyFile *file = MyMoneyFile::instance(); // First, try to find this account as an income account MyMoneyAccount acc = file->expense(); QStringList list = acc.accountList(); QStringList::ConstIterator it_accid = list.constBegin(); while (it_accid != list.constEnd()) { acc = file->account(*it_accid); if (acc.name() == searchname) { result = *it_accid; break; } ++it_accid; } // If we did not find the account, now we must create one. if (result.isEmpty()) { - MyMoneyAccount acc; - acc.setName(searchname); - acc.setAccountType(eMyMoney::Account::Type::Expense); + MyMoneyAccount newAccount; + newAccount.setName(searchname); + newAccount.setAccountType(eMyMoney::Account::Type::Expense); MyMoneyFileTransaction ft; MyMoneyAccount expense = file->expense(); - file->addAccount(acc, expense); + file->addAccount(newAccount, expense); ft.commit(); - result = acc.id(); + result = newAccount.id(); } return result; } const QString MyMoneyQifReader::processAccountEntry(bool resetAccountId) { MyMoneyFile* file = MyMoneyFile::instance(); MyMoneyAccount account; QString tmp; account.setName(extractLine('N')); // qDebug("Process account '%s'", account.name().data()); account.setDescription(extractLine('D')); tmp = extractLine('$'); if (tmp.length() > 0) account.setValue("lastStatementBalance", tmp); tmp = extractLine('/'); if (tmp.length() > 0) account.setValue("lastStatementDate", m_qifProfile.date(tmp).toString("yyyy-MM-dd")); QifEntryTypeE transactionType = EntryTransaction; QString type = extractLine('T').toLower().remove(QRegExp("\\s+")); if (type == m_qifProfile.profileType().toLower().remove(QRegExp("\\s+"))) { account.setAccountType(eMyMoney::Account::Type::Checkings); } else if (type == "ccard" || type == "creditcard") { account.setAccountType(eMyMoney::Account::Type::CreditCard); } else if (type == "cash") { account.setAccountType(eMyMoney::Account::Type::Cash); } else if (type == "otha") { account.setAccountType(eMyMoney::Account::Type::Asset); } else if (type == "othl") { account.setAccountType(eMyMoney::Account::Type::Liability); } else if (type == "invst" || type == "port") { account.setAccountType(eMyMoney::Account::Type::Investment); transactionType = EntryInvestmentTransaction; } else if (type == "mutual") { // stock account w/o umbrella investment account account.setAccountType(eMyMoney::Account::Type::Stock); transactionType = EntryInvestmentTransaction; } else if (type == "unknown") { // don't do anything with the type, leave it unknown } else { account.setAccountType(eMyMoney::Account::Type::Checkings); qDebug() << "Line " << m_linenumber << ": Unknown account type '" << type << "', checkings assumed"; } // check if we can find the account already in the file auto acc = findAccount(account, MyMoneyAccount()); if (acc.id().isEmpty()) { // in case the account is not found by name and the type is // unknown, we have to assume something and create a checking account. // this might be wrong, but we have no choice at this point. if (account.accountType() == eMyMoney::Account::Type::Unknown) account.setAccountType(eMyMoney::Account::Type::Checkings); MyMoneyAccount parentAccount; MyMoneyAccount brokerage; // in case it's a stock account, we need to setup a fix investment account if (account.isInvest()) { acc.setName(i18n("%1 (Investment)", account.name())); // use the same name for the investment account acc.setDescription(i18n("Autogenerated by QIF importer from type Mutual account entry")); acc.setAccountType(eMyMoney::Account::Type::Investment); parentAccount = file->asset(); file->createAccount(acc, parentAccount, brokerage, MyMoneyMoney()); parentAccount = acc; qDebug("We still need to create the stock account in MyMoneyQifReader::processAccountEntry()"); } else { // setup parent according the type of the account switch (account.accountGroup()) { case eMyMoney::Account::Type::Asset: default: parentAccount = file->asset(); break; case eMyMoney::Account::Type::Liability: parentAccount = file->liability(); break; case eMyMoney::Account::Type::Equity: parentAccount = file->equity(); break; } } // investment accounts will receive a brokerage account, as KMyMoney // currently does not allow to store funds in the investment account directly // but only create it (not here, but later) if it is needed if (account.accountType() == eMyMoney::Account::Type::Investment) { brokerage.setName(QString()); // brokerage name empty so account not created yet brokerage.setAccountType(eMyMoney::Account::Type::Checkings); brokerage.setCurrencyId(MyMoneyFile::instance()->baseCurrency().id()); } file->createAccount(account, parentAccount, brokerage, MyMoneyMoney()); acc = account; // qDebug("Account created"); } else { // qDebug("Existing account found"); } if (resetAccountId) { // possibly start a new statement d->finishStatement(); m_account = acc; d->st.m_accountId = m_account.id(); // needed here for account selection d->transactionType = transactionType; } return acc.id(); } void MyMoneyQifReader::setProgressCallback(void(*callback)(int, int, const QString&)) { m_progressCallback = callback; } void MyMoneyQifReader::signalProgress(int current, int total, const QString& msg) { if (m_progressCallback != 0) (*m_progressCallback)(current, total, msg); } void MyMoneyQifReader::processPriceEntry() { /* !Type:Prices "IBM",141 9/16,"10/23/98" ^ !Type:Prices "GMW",21.28," 3/17' 5" ^ !Type:Prices "GMW",71652181.001,"67/128/ 0" ^ Note that Quicken will often put in a price with a bogus date and number. We will ignore prices with bogus dates. Hopefully that will catch all of these. Also note that prices can be in fractional units, e.g. 141 9/16. */ QStringList::const_iterator it_line = m_qifEntry.constBegin(); // Make a price for each line QRegExp priceExp("\"(.*)\",(.*),\"(.*)\""); while (it_line != m_qifEntry.constEnd()) { if (priceExp.indexIn(*it_line) != -1) { MyMoneyStatement::Price price; price.m_strSecurity = priceExp.cap(1); QString pricestr = priceExp.cap(2); QString datestr = priceExp.cap(3); qDebug() << "Price:" << price.m_strSecurity << " / " << pricestr << " / " << datestr; // Only add the price if the date is valid. If invalid, fail silently. See note above. // Also require the price value to not have any slashes. Old prices will be something like // "25 9/16", which we do not support. So we'll skip the price for now. QDate date = m_qifProfile.date(datestr); MyMoneyMoney rate(m_qifProfile.value('P', pricestr)); if (date.isValid() && !rate.isZero()) { price.m_amount = rate; price.m_date = date; d->st.m_listPrices += price; } } ++it_line; } } void MyMoneyQifReader::processSecurityEntry() { /* !Type:Security NVANGUARD 500 INDEX SVFINX TMutual Fund ^ */ MyMoneyStatement::Security security; security.m_strName = extractLine('N'); security.m_strSymbol = extractLine('S'); d->st.m_listSecurities += security; } diff --git a/kmymoney/plugins/sql/mymoneydbdef.h b/kmymoney/plugins/sql/mymoneydbdef.h index 8b1625f62..1cd579b15 100644 --- a/kmymoney/plugins/sql/mymoneydbdef.h +++ b/kmymoney/plugins/sql/mymoneydbdef.h @@ -1,462 +1,462 @@ /*************************************************************************** mymoneydbdef.h ------------------- begin : 20 February 2010 copyright : (C) 2010 by Fernando Vilas email : tonybloom@users.sourceforge.net : Fernando Vilas ***************************************************************************/ /*************************************************************************** * * * 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 MYMONEYDBDEF_H #define MYMONEYDBDEF_H // ---------------------------------------------------------------------------- // System Includes #include // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes class MyMoneyDbDriver; template class QExplicitlySharedDataPointer; /** * The MyMoneyDbColumn class is a base type for generic db columns. * Derived types exist for several common column types. */ class MyMoneyDbColumn : public QSharedData { public: explicit MyMoneyDbColumn(const QString& iname, const QString& itype = QString(), const bool iprimary = false, const bool inotnull = false, const int initVersion = 0, const int lastVersion = std::numeric_limits::max(), QString defaultValue = QString() ): m_name(iname), m_type(itype), m_defaultValue(defaultValue), m_isPrimary(iprimary), m_isNotNull(inotnull), m_initVersion(initVersion), m_lastVersion(lastVersion) {} MyMoneyDbColumn() {} virtual ~MyMoneyDbColumn() {} /** * This method is used to copy column objects. Because there are several derived types, * clone() is more appropriate than a copy ctor in most cases. */ virtual MyMoneyDbColumn* clone() const; /** * This method generates the DDL (Database Design Language) string for the column. * * @param dbType Database driver type * * @return QString of the DDL for the column, tailored for what the driver supports. */ virtual const QString generateDDL(const QExplicitlySharedDataPointer& driver) const; const QString& name() const { return m_name; } const QString& type() const { return m_type; } bool isPrimaryKey() const { return m_isPrimary; } bool isNotNull() const { return m_isNotNull; } int initVersion() const { return m_initVersion; } int lastVersion() const { return m_lastVersion; } QString defaultValue() const { return m_defaultValue; } private: QString m_name; QString m_type; QString m_defaultValue; bool m_isPrimary; bool m_isNotNull; int m_initVersion; int m_lastVersion; }; /** * The MyMoneyDbDatetimeColumn class is a representation of datetime columns. */ class MyMoneyDbDatetimeColumn : public MyMoneyDbColumn { public: explicit MyMoneyDbDatetimeColumn(const QString& iname, const bool iprimary = false, const bool inotnull = false, const int initVersion = 0): MyMoneyDbColumn(iname, "", iprimary, inotnull, initVersion) {} virtual ~MyMoneyDbDatetimeColumn() {} - virtual const QString generateDDL(const QExplicitlySharedDataPointer& driver) const; - virtual MyMoneyDbDatetimeColumn* clone() const; + const QString generateDDL(const QExplicitlySharedDataPointer& driver) const final override; + MyMoneyDbDatetimeColumn* clone() const final override; private: static const QString calcType(); }; /** * The MyMoneyDbColumn class is a representation of integer db columns. */ class MyMoneyDbIntColumn : public MyMoneyDbColumn { public: enum size {TINY, SMALL, MEDIUM, BIG}; explicit MyMoneyDbIntColumn(const QString& iname, const size type = MEDIUM, const bool isigned = true, const bool iprimary = false, const bool inotnull = false, const int initVersion = 0, const int lastVersion = std::numeric_limits::max(), const QString& defaultValue = QString() ): MyMoneyDbColumn(iname, "", iprimary, inotnull, initVersion, lastVersion, defaultValue), m_type(type), m_isSigned(isigned) {} virtual ~MyMoneyDbIntColumn() {} - virtual const QString generateDDL(const QExplicitlySharedDataPointer& driver) const; - virtual MyMoneyDbIntColumn* clone() const; + const QString generateDDL(const QExplicitlySharedDataPointer& driver) const final override; + MyMoneyDbIntColumn* clone() const final override; size type() const { return m_type; } bool isSigned() const { return m_isSigned; } private: size m_type; bool m_isSigned; }; /** * The MyMoneyDbTextColumn class is a representation of text db columns, * for drivers that support it. If the driver does not support it, it is * usually some sort of really large varchar or varchar2. */ class MyMoneyDbTextColumn : public MyMoneyDbColumn { public: enum size {TINY, NORMAL, MEDIUM, LONG}; explicit MyMoneyDbTextColumn(const QString& iname, const size type = MEDIUM, const bool iprimary = false, const bool inotnull = false, const int initVersion = 0): MyMoneyDbColumn(iname, "", iprimary, inotnull, initVersion), m_type(type) {} virtual ~MyMoneyDbTextColumn() {} - virtual const QString generateDDL(const QExplicitlySharedDataPointer& driver) const; - virtual MyMoneyDbTextColumn* clone() const; + const QString generateDDL(const QExplicitlySharedDataPointer& driver) const final override; + MyMoneyDbTextColumn* clone() const final override; size type() const { return m_type; } private: size m_type; }; /** * The MyMoneyDbIndex class is a representation of a db index. * To provide generic support for most databases, the table name, * name of the index, and list of columns for the index are required. * Additionally, the user can specify whether the index is unique or not. * * At this time, different types of index are not supported, since the * portability is fairly limited. */ class MyMoneyDbIndex { public: MyMoneyDbIndex(const QString& table, const QString& name, const QStringList& columns, bool unique = false): m_table(table), m_unique(unique), m_name(name), m_columns(columns) {} MyMoneyDbIndex() {} inline const QString table() const { return m_table; } inline bool isUnique() const { return m_unique; } inline const QString name() const { return m_name; } inline const QStringList columns() const { return m_columns; } const QString generateDDL(const QExplicitlySharedDataPointer& driver) const; private: QString m_table; bool m_unique; QString m_name; QStringList m_columns; }; /** * The MyMoneyDbTable class is a representation of a db table. * It has a list of the columns (pointers to MyMoneyDbColumn types) and a * list of any indices that may be on the table. * Additionally, a string for a parameterized query for each of some common * tasks on a table is created by the ctor. * * Const iterators over the list of columns are provided as a convenience. */ class MyMoneyDbTable { public: MyMoneyDbTable(const QString& iname, const QList >& ifields, const QString& initVersion = "1.0"): m_name(iname), m_fields(ifields), m_initVersion(initVersion) {} MyMoneyDbTable() {} inline const QString& name() const { return (m_name); } inline const QString& insertString() const { return (m_insertString); }; inline const QString selectAllString(bool terminate = true) const { return (terminate ? QString(m_selectAllString + ";") : m_selectAllString); }; inline const QString& updateString() const { return (m_updateString); }; inline const QString& deleteString() const { return (m_deleteString); }; /** * This method determines whether the table has a primary key field * * @param int database version which has to be checked * * @return bool table has a priimary key */ bool hasPrimaryKey(int version = std::numeric_limits::max()) const; /** * This method determines the string required to drop the primary key for the table * based on the db specific syntax. * * @param dbType The driver type of the database. * * @return QString for the syntax to drop the primary key. */ const QString dropPrimaryKeyString(const QExplicitlySharedDataPointer& driver) const; /** * This method returns a comma-separated list of all column names in the table * which were present in a given version * * @param version version of database definition required * * @return QString column list */ const QString columnList(const int version = std::numeric_limits::max()) const; /** * This method returns the string for changing a column's definition. It covers statements * like ALTER TABLE..CHANGE COLUMN, MODIFY COLUMN, etc. * * @param dbType The driver type of the database. * @param columnName The name of the column to be modified. * @param newDef The MyMoneyColumn object of the new column definition. * * @return QString containing DDL to change the column. */ const QString modifyColumnString(const QExplicitlySharedDataPointer& driver, const QString& columnName, const MyMoneyDbColumn& newDef) const; /** * This method builds all of the SQL strings for common operations. */ void buildSQLStrings(); /** * This method generates the DDL required to create the table. * * @param dbType The driver type of the database. * * @return QString of the DDL. */ const QString generateCreateSQL(const QExplicitlySharedDataPointer& driver, int version = std::numeric_limits::max()) const; /** * This method creates a MyMoneyDbIndex object and adds it to the list of indices for the table. * * @param name The name of the index. * @param columns The list of the columns affected. * @param unique Whether or not this should be a unique index. */ void addIndex(const QString& name, const QStringList& columns, bool unique = false); typedef QList >::const_iterator field_iterator; inline field_iterator begin() const { return m_fields.constBegin(); } inline field_iterator end() const { return m_fields.constEnd(); } int fieldNumber(const QString& name) const; typedef QList::const_iterator index_iterator; inline index_iterator indexBegin() const { return m_indices.constBegin(); } inline index_iterator indexEnd() const { return m_indices.constEnd(); } private: QString m_name; QList > m_fields; QHash m_fieldOrder; QList m_indices; QString m_initVersion; QString m_insertString; // string to insert a record QString m_selectAllString; // to select all fields QString m_updateString; // normal string for record update QString m_deleteString; // string to delete 1 record }; /** * The MyMoneyDbView class is a representation of a db view. * * Views will be dropped and recreated on upgrade, so there is no need * to do anything more complex than storing the name of the view and * the CREATE VIEW string. */ class MyMoneyDbView { public: MyMoneyDbView(const QString& name, const QString& createString, const QString& initVersion = "0.1") : m_name(name), m_createString(createString), m_initVersion(initVersion) {} MyMoneyDbView() {} inline const QString& name() const { return (m_name); } inline const QString createString() const { return (m_createString); }; private: QString m_name; QString m_createString; QString m_initVersion; }; /** * The MyMoneyDbDef class is */ class MyMoneyDbDef { friend class MyMoneyStorageSql; friend class MyMoneyStorageSqlPrivate; public: MyMoneyDbDef(); ~MyMoneyDbDef() {} const QString generateSQL(const QExplicitlySharedDataPointer& driver) const; typedef QMap::const_iterator table_iterator; inline table_iterator tableBegin() const { return m_tables.constBegin(); } inline table_iterator tableEnd() const { return m_tables.constEnd(); } typedef QMap::const_iterator view_iterator; inline view_iterator viewBegin() const { return m_views.constBegin(); } inline view_iterator viewEnd() const { return m_views.constEnd(); } inline unsigned int currentVersion() const { return (m_currentVersion); }; private: const QString enclose(const QString& text) const { return (QString("'" + text + "'")); } static unsigned int m_currentVersion; // The current version of the database layout #define TABLE(name) void name(); #define VIEW(name) void name(); TABLE(FileInfo) TABLE(Institutions) TABLE(Payees) TABLE(PayeesPayeeIdentifier) TABLE(Tags) TABLE(TagSplits) TABLE(Accounts) TABLE(AccountsPayeeIdentifier) TABLE(Transactions) TABLE(Splits) TABLE(KeyValuePairs) TABLE(Schedules) TABLE(SchedulePaymentHistory) TABLE(Securities) TABLE(Prices) TABLE(Currencies) TABLE(Reports) TABLE(Budgets) TABLE(OnlineJobs) TABLE(PayeeIdentifier) TABLE(PluginInfo) TABLE(CostCenter) VIEW(Balances) protected: QMap m_tables; QMap m_views; }; #endif // MYMONEYDBDEF_H diff --git a/kmymoney/plugins/sql/mymoneydbdriver.cpp b/kmymoney/plugins/sql/mymoneydbdriver.cpp index 7c9dea316..666122f03 100644 --- a/kmymoney/plugins/sql/mymoneydbdriver.cpp +++ b/kmymoney/plugins/sql/mymoneydbdriver.cpp @@ -1,706 +1,706 @@ /*************************************************************************** mymoneydbdriver.cpp --------------------- begin : 19 February 2010 copyright : (C) 2010 by Fernando Vilas email : Fernando Vilas (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 "mymoneydbdriver.h" // ---------------------------------------------------------------------------- // QT Includes #include #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes #include "mymoneydbdef.h" #include "mymoneyexception.h" //********************* The driver subclass definitions ******************** // It is only necessary to implement the functions that deviate from the // default of the base type class MyMoneyDb2Driver : public MyMoneyDbDriver { public: MyMoneyDb2Driver() { } - virtual QString textString(const MyMoneyDbTextColumn& c) const; + QString textString(const MyMoneyDbTextColumn& c) const final override; }; class MyMoneyInterbaseDriver : public MyMoneyDbDriver { public: MyMoneyInterbaseDriver() { } }; class MyMoneyMysqlDriver : public MyMoneyDbDriver { public: MyMoneyMysqlDriver() { } - virtual bool isTested() const; - virtual bool canAutocreate() const; - virtual QString defaultDbName() const; - virtual QString createDbString(const QString& name) const; - virtual QString dropPrimaryKeyString(const QString& name) const; - virtual QString dropIndexString(const QString& tableName, const QString& indexName) const; - virtual QString modifyColumnString(const QString& tableName, const QString& columnName, const MyMoneyDbColumn& newDef) const; - virtual QString intString(const MyMoneyDbIntColumn& c) const; - virtual QString timestampString(const MyMoneyDbDatetimeColumn& c) const; - virtual QString tableOptionString() const; - virtual QString highestNumberFromIdString(const QString& tableName, const QString& tableField, const int prefixLength) const; - virtual QStringList tables(QSql::TableType tt, const QSqlDatabase& db) const; + bool isTested() const final override; + bool canAutocreate() const final override; + QString defaultDbName() const final override; + QString createDbString(const QString& name) const final override; + QString dropPrimaryKeyString(const QString& name) const final override; + QString dropIndexString(const QString& tableName, const QString& indexName) const final override; + QString modifyColumnString(const QString& tableName, const QString& columnName, const MyMoneyDbColumn& newDef) const final override; + QString intString(const MyMoneyDbIntColumn& c) const final override; + QString timestampString(const MyMoneyDbDatetimeColumn& c) const final override; + QString tableOptionString() const final override; + QString highestNumberFromIdString(const QString& tableName, const QString& tableField, const int prefixLength) const final override; + QStringList tables(QSql::TableType tt, const QSqlDatabase& db) const final override; }; class MyMoneyOracleDriver : public MyMoneyDbDriver { public: MyMoneyOracleDriver() { } - virtual QString dropPrimaryKeyString(const QString& name) const; - virtual QString modifyColumnString(const QString& tableName, const QString& columnName, const MyMoneyDbColumn& newDef) const; - virtual QString intString(const MyMoneyDbIntColumn& c) const; - virtual QString textString(const MyMoneyDbTextColumn& c) const; - virtual QString highestNumberFromIdString(const QString& tableName, const QString& tableField, const int prefixLength) const; + QString dropPrimaryKeyString(const QString& name) const final override; + QString modifyColumnString(const QString& tableName, const QString& columnName, const MyMoneyDbColumn& newDef) const final override; + QString intString(const MyMoneyDbIntColumn& c) const final override; + QString textString(const MyMoneyDbTextColumn& c) const final override; + QString highestNumberFromIdString(const QString& tableName, const QString& tableField, const int prefixLength) const final override; }; class MyMoneyODBCDriver : public MyMoneyDbDriver { public: MyMoneyODBCDriver() { } - virtual QString timestampString(const MyMoneyDbDatetimeColumn& c) const; + QString timestampString(const MyMoneyDbDatetimeColumn& c) const final override; }; class MyMoneyPostgresqlDriver : public MyMoneyDbDriver { public: MyMoneyPostgresqlDriver() { } - virtual bool isTested() const; - virtual bool canAutocreate() const; - virtual QString defaultDbName() const; - virtual QString createDbString(const QString& name) const; - virtual QString dropPrimaryKeyString(const QString& name) const; - virtual QString modifyColumnString(const QString& tableName, const QString& columnName, const MyMoneyDbColumn& newDef) const; - virtual QString intString(const MyMoneyDbIntColumn& c) const; - virtual QString textString(const MyMoneyDbTextColumn& c) const; - virtual QString highestNumberFromIdString(const QString& tableName, const QString& tableField, const int prefixLength) const; + bool isTested() const final override; + bool canAutocreate() const final override; + QString defaultDbName() const final override; + QString createDbString(const QString& name) const final override; + QString dropPrimaryKeyString(const QString& name) const final override; + QString modifyColumnString(const QString& tableName, const QString& columnName, const MyMoneyDbColumn& newDef) const final override; + QString intString(const MyMoneyDbIntColumn& c) const final override; + QString textString(const MyMoneyDbTextColumn& c) const final override; + QString highestNumberFromIdString(const QString& tableName, const QString& tableField, const int prefixLength) const final override; }; class MyMoneySybaseDriver : public MyMoneyDbDriver { public: MyMoneySybaseDriver() { } }; class MyMoneySqlite3Driver : public MyMoneyDbDriver { public: MyMoneySqlite3Driver() { } - virtual bool isTested() const; - virtual QString forUpdateString() const; - virtual QString intString(const MyMoneyDbIntColumn& c) const; - virtual bool requiresExternalFile() const; - virtual bool requiresCreation() const; - virtual bool isPasswordSupported() const; + bool isTested() const final override; + QString forUpdateString() const final override; + QString intString(const MyMoneyDbIntColumn& c) const final override; + bool requiresExternalFile() const final override; + bool requiresCreation() const final override; + bool isPasswordSupported() const override; }; class MyMoneySqlCipher3Driver : public MyMoneySqlite3Driver { public: - virtual bool isPasswordSupported() const; + bool isPasswordSupported() const final override; }; //********************* The driver map ********************* // This function is only used by the GUI types at the moment const QMap MyMoneyDbDriver::driverMap() { QMap map; map["QDB2"] = QString("IBM DB2"); map["QIBASE"] = QString("Borland Interbase"); map["QMYSQL"] = QString("MySQL"); map["QOCI"] = QString("Oracle Call Interface"); map["QODBC"] = QString("Open Database Connectivity"); map["QPSQL"] = QString("PostgreSQL v7.3 and up"); map["QTDS"] = QString("Sybase Adaptive Server and Microsoft SQL Server"); map["QSQLITE"] = QString("SQLite Version 3"); map["SQLCIPHER"] = QString("SQLCipher Version 3 (encrypted SQLite)"); return map; } //********************* The factory ********************* QExplicitlySharedDataPointer MyMoneyDbDriver::create(const QString& type) { if (type == "QDB2") return QExplicitlySharedDataPointer (new MyMoneyDb2Driver()); else if (type == "QIBASE") return QExplicitlySharedDataPointer (new MyMoneyInterbaseDriver()); else if (type == "QMYSQL") return QExplicitlySharedDataPointer (new MyMoneyMysqlDriver()); else if (type == "QOCI") return QExplicitlySharedDataPointer (new MyMoneyOracleDriver()); else if (type == "QODBC") return QExplicitlySharedDataPointer (new MyMoneyODBCDriver()); else if (type == "QPSQL") return QExplicitlySharedDataPointer (new MyMoneyPostgresqlDriver()); else if (type == "QTDS") return QExplicitlySharedDataPointer (new MyMoneySybaseDriver()); else if (type == "QSQLITE") return QExplicitlySharedDataPointer (new MyMoneySqlite3Driver()); else if (type == "SQLCIPHER") return QExplicitlySharedDataPointer (new MyMoneySqlCipher3Driver()); else throw MYMONEYEXCEPTION(QString("Unknown database driver type").arg(type)); } MyMoneyDbDriver::MyMoneyDbDriver() { } MyMoneyDbDriver::~MyMoneyDbDriver() { } //******************************************************* // By default, claim that the driver is not tested // For Mysql, Pgsql, and SQLite, return true. bool MyMoneyDbDriver::isTested() const { return false; } bool MyMoneyMysqlDriver::isTested() const { return true; } bool MyMoneyPostgresqlDriver::isTested() const { return true; } bool MyMoneySqlite3Driver::isTested() const { return true; } //******************************************************* // By default, claim that the database cannot be created // For Mysql and Pgsql, return true. bool MyMoneyDbDriver::canAutocreate() const { return false; } bool MyMoneyMysqlDriver::canAutocreate() const { return true; } bool MyMoneyPostgresqlDriver::canAutocreate() const { return true; } //******************************************************* // By default, there is no default name QString MyMoneyDbDriver::defaultDbName() const { return ""; } // The default db for Mysql is "mysql" QString MyMoneyMysqlDriver::defaultDbName() const { return "mysql"; } // The default db for Postgres is "template1" QString MyMoneyPostgresqlDriver::defaultDbName() const { return "template1"; } //******************************************************* // By default, just attempt to create the database // Mysql and Postgres need the character set specified. QString MyMoneyDbDriver::createDbString(const QString& name) const { return QString("CREATE DATABASE %1").arg(name); } QString MyMoneyMysqlDriver::createDbString(const QString& name) const { return MyMoneyDbDriver::createDbString(name) + " CHARACTER SET 'utf8' COLLATE 'utf8_unicode_ci'"; } QString MyMoneyPostgresqlDriver:: createDbString(const QString& name) const { return MyMoneyDbDriver::createDbString(name) + " WITH ENCODING='UTF8' LC_CTYPE='C' TEMPLATE=template0"; } //******************************************************* // By default, the DBMS does not require an external file // At present, only sqlite does bool MyMoneyDbDriver::requiresExternalFile() const { return false; } bool MyMoneySqlite3Driver::requiresExternalFile() const { return true; } //******************************************************* // By default, the DBMS requires creating before use // At present, only sqlite doesn't AFAIK bool MyMoneyDbDriver::requiresCreation() const { return true; } bool MyMoneySqlite3Driver::requiresCreation() const { return false; } //******************************************************* // There is no standard for dropping a primary key. // If it is supported, it will have to be implemented for each DBMS QString MyMoneyDbDriver::dropPrimaryKeyString(const QString& name) const { Q_UNUSED(name); return ""; } QString MyMoneyMysqlDriver::dropPrimaryKeyString(const QString& name) const { return QString("ALTER TABLE %1 DROP PRIMARY KEY;").arg(name); } QString MyMoneyOracleDriver::dropPrimaryKeyString(const QString& name) const { return QString("ALTER TABLE %1 DROP PRIMARY KEY;").arg(name); } QString MyMoneyPostgresqlDriver::dropPrimaryKeyString(const QString& name) const { return QString("ALTER TABLE %1 DROP CONSTRAINT %2_pkey;").arg(name).arg(name); } //******************************************************* // There is apparently no standard for dropping an index. // If it is supported, it will have to be implemented for each DBMS QString MyMoneyDbDriver::dropIndexString(const QString& tableName, const QString& indexName) const { Q_UNUSED(tableName); return QString("DROP INDEX %1;").arg(indexName); } QString MyMoneyMysqlDriver::dropIndexString(const QString& tableName, const QString& indexName) const { return QString("DROP INDEX %1 ON %2;").arg(indexName).arg(tableName); } //******************************************************* // There is no standard for modifying a column // If it is supported, it will have to be implemented for each DBMS QString MyMoneyDbDriver::modifyColumnString(const QString& tableName, const QString& columnName, const MyMoneyDbColumn& newDef) const { Q_UNUSED(tableName); Q_UNUSED(columnName); Q_UNUSED(newDef); return ""; } QString MyMoneyMysqlDriver::modifyColumnString(const QString& tableName, const QString& columnName, const MyMoneyDbColumn& newDef) const { return QString("ALTER TABLE %1 CHANGE %2 %3") .arg(tableName) .arg(columnName) .arg(newDef.generateDDL(QExplicitlySharedDataPointer(const_cast(this)))); } QString MyMoneyPostgresqlDriver::modifyColumnString(const QString& tableName, const QString& columnName, const MyMoneyDbColumn& newDef) const { return QString("ALTER TABLE %1 ALTER COLUMN %2 TYPE %3") .arg(tableName) .arg(columnName) .arg(newDef.generateDDL(QExplicitlySharedDataPointer(const_cast(this)))); } QString MyMoneyOracleDriver::modifyColumnString(const QString& tableName, const QString& columnName, const MyMoneyDbColumn& newDef) const { return QString("ALTER TABLE %1 MODIFY %2 %3") .arg(tableName) .arg(columnName) .arg(newDef.generateDDL(QExplicitlySharedDataPointer(const_cast(this)))); } //******************************************************* // Define the integer column types in terms of the standard // Each DBMS typically has its own variation of this QString MyMoneyDbDriver::intString(const MyMoneyDbIntColumn& c) const { QString qs = c.name(); switch (c.type()) { case MyMoneyDbIntColumn::TINY: case MyMoneyDbIntColumn::SMALL: qs += " smallint"; break; case MyMoneyDbIntColumn::BIG: qs += " bigint"; break; case MyMoneyDbIntColumn::MEDIUM: default: qs += " int"; break; } if (c.isNotNull()) qs += " NOT NULL"; return qs; } QString MyMoneyMysqlDriver::intString(const MyMoneyDbIntColumn& c) const { QString qs = c.name(); switch (c.type()) { case MyMoneyDbIntColumn::TINY: qs += " tinyint"; break; case MyMoneyDbIntColumn::SMALL: qs += " smallint"; break; case MyMoneyDbIntColumn::BIG: qs += " bigint"; break; case MyMoneyDbIntColumn::MEDIUM: default: qs += " int"; break; } if (! c.isSigned()) qs += " unsigned"; if (c.isNotNull()) qs += " NOT NULL"; return qs; } QString MyMoneySqlite3Driver::intString(const MyMoneyDbIntColumn& c) const { QString qs = c.name(); switch (c.type()) { case MyMoneyDbIntColumn::TINY: qs += " tinyint"; break; case MyMoneyDbIntColumn::SMALL: qs += " smallint"; break; case MyMoneyDbIntColumn::BIG: qs += " bigint"; break; case MyMoneyDbIntColumn::MEDIUM: default: qs += " int"; break; } if (! c.isSigned()) qs += " unsigned"; if (c.isNotNull()) qs += " NOT NULL"; return qs; } QString MyMoneyPostgresqlDriver::intString(const MyMoneyDbIntColumn& c) const { QString qs = c.name(); switch (c.type()) { case MyMoneyDbIntColumn::TINY: case MyMoneyDbIntColumn::SMALL: qs += " int2"; break; case MyMoneyDbIntColumn::BIG: qs += " int8"; break; case MyMoneyDbIntColumn::MEDIUM: default: qs += " int4"; break; } if (c.isNotNull()) qs += " NOT NULL"; if (! c.isSigned()) qs += QString(" check(%1 >= 0)").arg(c.name()); return qs; } QString MyMoneyOracleDriver::intString(const MyMoneyDbIntColumn& c) const { QString qs = c.name(); switch (c.type()) { case MyMoneyDbIntColumn::TINY: qs += " number(3)"; break; case MyMoneyDbIntColumn::SMALL: qs += " number(5)"; break; case MyMoneyDbIntColumn::BIG: qs += " number(20)"; break; case MyMoneyDbIntColumn::MEDIUM: default: qs += " number(10)"; break; } if (c.isNotNull()) qs += " NOT NULL"; if (! c.isSigned()) qs += QString(" check(%1 >= 0)").arg(c.name()); return qs; } //******************************************************* // Define the text column types in terms of the standard // Each DBMS typically has its own variation of this QString MyMoneyDbDriver::textString(const MyMoneyDbTextColumn& c) const { QString qs = c.name(); switch (c.type()) { case MyMoneyDbTextColumn::TINY: qs += " tinytext"; break; case MyMoneyDbTextColumn::MEDIUM: qs += " mediumtext"; break; case MyMoneyDbTextColumn::LONG: qs += " longtext"; break; case MyMoneyDbTextColumn::NORMAL: default: qs += " text"; break; } if (c.isNotNull()) qs += " NOT NULL"; return qs; } QString MyMoneyDb2Driver::textString(const MyMoneyDbTextColumn& c) const { QString qs = c.name(); switch (c.type()) { case MyMoneyDbTextColumn::TINY: qs += " varchar(255)"; break; case MyMoneyDbTextColumn::MEDIUM: qs += " clob(16M)"; break; case MyMoneyDbTextColumn::LONG: qs += " clob(2G)"; break; case MyMoneyDbTextColumn::NORMAL: default: qs += " clob(64K)"; break; } if (c.isNotNull()) qs += " NOT NULL"; return qs; } QString MyMoneyOracleDriver::textString(const MyMoneyDbTextColumn& c) const { QString qs = c.name(); switch (c.type()) { case MyMoneyDbTextColumn::TINY: qs += " varchar2(255)"; break; case MyMoneyDbTextColumn::MEDIUM: case MyMoneyDbTextColumn::LONG: case MyMoneyDbTextColumn::NORMAL: default: qs += " clob"; break; } if (c.isNotNull()) qs += " NOT NULL"; return qs; } QString MyMoneyPostgresqlDriver::textString(const MyMoneyDbTextColumn& c) const { QString qs = QString("%1 text").arg(c.name()); if (c.isNotNull()) qs += " NOT NULL"; return qs; } //******************************************************* // Define the timestamp column types in terms of the standard // Each DBMS typically has its own variation of this QString MyMoneyDbDriver::timestampString(const MyMoneyDbDatetimeColumn& c) const { QString qs = QString("%1 timestamp").arg(c.name()); if (c.isNotNull()) qs += " NOT NULL"; return qs; } // Mysql has a timestamp type, but datetime is closer to the standard QString MyMoneyMysqlDriver::timestampString(const MyMoneyDbDatetimeColumn& c) const { QString qs = QString("%1 datetime").arg(c.name()); if (c.isNotNull()) qs += " NOT NULL"; return qs; } QString MyMoneyODBCDriver::timestampString(const MyMoneyDbDatetimeColumn& c) const { QString qs = QString("%1 datetime").arg(c.name()); if (c.isNotNull()) qs += " NOT NULL"; return qs; } //*********************************************** // Define the FOR UPDATE string // So far, only SQLite requires special handling. QString MyMoneyDbDriver::forUpdateString() const { return " FOR UPDATE"; } QString MyMoneySqlite3Driver::forUpdateString() const { return ""; } //*********************************************** // Define the table option string // So far, only mysql requires special handling. QString MyMoneyMysqlDriver::tableOptionString() const { return " ENGINE = InnoDB"; } QString MyMoneyDbDriver::tableOptionString() const { return ""; } //*********************************************** // Define the highestIdNum string // PostgreSQL and Oracle return errors when a non-numerical string is cast to an integer, so a regex is used to skip strings that aren't entirely numerical after the prefix is removed QString MyMoneyDbDriver::highestNumberFromIdString(const QString& tableName, const QString& tableField, const int prefixLength) const { return QString("SELECT MAX(CAST(SUBSTR(%1, %2) AS INTEGER)) FROM %3;").arg(tableField).arg(prefixLength + 1).arg(tableName); } QString MyMoneyMysqlDriver::highestNumberFromIdString(const QString& tableName, const QString& tableField, const int prefixLength) const { return QString("SELECT MAX(CAST(SUBSTR(%1, %2) AS UNSIGNED INTEGER)) FROM %3;").arg(tableField).arg(prefixLength + 1).arg(tableName); } QString MyMoneyPostgresqlDriver::highestNumberFromIdString(const QString& tableName, const QString& tableField, const int prefixLength) const { return QString("SELECT MAX(CAST(SUBSTR(%1, %2) AS INTEGER)) FROM %3 WHERE SUBSTR(%1, %2) ~ '^[0-9]+$';").arg(tableField).arg(prefixLength + 1).arg(tableName); } QString MyMoneyOracleDriver::highestNumberFromIdString(const QString& tableName, const QString& tableField, const int prefixLength) const { return QString("SELECT MAX(TO_NUMBER(SUBSTR(%1, %2))) FROM %3 WHERE REGEXP_LIKE(SUBSTR(%1, %2), '^[0-9]+$');").arg(tableField).arg(prefixLength + 1).arg(tableName); } //************************************************* // Define bool MyMoneyDbDriver::isPasswordSupported() const { return true; } bool MyMoneySqlite3Driver::isPasswordSupported() const { return false; } bool MyMoneySqlCipher3Driver::isPasswordSupported() const { return true; } //************************************************* // replace the QSqlDatabase::tables() call for Mysql only // see bug 252841 QStringList MyMoneyDbDriver::tables(QSql::TableType tt, const QSqlDatabase& db) const { return (db.tables(tt)); } QStringList MyMoneyMysqlDriver::tables(QSql::TableType tt, const QSqlDatabase& db) const { QStringList tableList; QSqlQuery q(db); QString selectString; switch (tt) { case QSql::AllTables: selectString = QString("SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA = '%1'").arg(db.databaseName()); if (!q.exec(selectString)) { throw MYMONEYEXCEPTION("select names failed in mymoneydbdriver.cpp"); } while (q.next()) tableList.append(q.value(0).toString()); break; case QSql::Tables: case QSql::SystemTables: case QSql::Views: qWarning("Programming error in mymoneydbdriver.cpp"); // KMM only uses AllTables; implement other options if required } return (tableList); } diff --git a/kmymoney/plugins/sql/mymoneystoragesql.cpp b/kmymoney/plugins/sql/mymoneystoragesql.cpp index 0c8004911..a1746519d 100644 --- a/kmymoney/plugins/sql/mymoneystoragesql.cpp +++ b/kmymoney/plugins/sql/mymoneystoragesql.cpp @@ -1,2836 +1,2836 @@ /*************************************************************************** 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 QString dbName = url.path(); 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(); //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 { 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") + ' ' + callingFunction); } 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("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 q(*this); q.prepare(d->m_db.m_tables["kmmInstitutions"].deleteString()); q.bindValue(":id", inst.id()); if (!q.exec()) throw MYMONEYEXCEPTION(d->buildError(q, Q_FUNC_INFO, QString("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 q(*this); q.prepare(d->m_db.m_tables["kmmPayees"].insertString()); d->writePayee(payee, q); ++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 (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(); } q.prepare("INSERT INTO kmmPayeesPayeeIdentifier (payeeId, identifierId, userOrder) VALUES(?, ?, ?)"); q.bindValue(0, payeeIdList); q.bindValue(1, identIds); q.bindValue(2, order); if (!q.execBatch()) throw MYMONEYEXCEPTION(d->buildError(q, Q_FUNC_INFO, QString("writing payee's identifiers"))); // krazy:exclude=crashy } d->writeFileInfo(); } void MyMoneyStorageSql::modifyPayee(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); // Get a list of old identifiers first q.prepare("SELECT identifierId FROM kmmPayeesPayeeIdentifier WHERE payeeId = ?"); q.bindValue(0, payee.id()); if (!q.exec()) throw MYMONEYEXCEPTION(d->buildError(q, Q_FUNC_INFO, QString("modifying payee's identifiers (getting old values failed)"))); // krazy:exclude=crashy QStringList oldIdentIds; oldIdentIds.reserve(q.numRowsAffected()); while (q.next()) oldIdentIds << q.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 q.prepare("DELETE FROM kmmPayeesPayeeIdentifier WHERE payeeId = ?"); q.bindValue(0, payee.id()); if (!q.exec()) throw MYMONEYEXCEPTION(d->buildError(q, Q_FUNC_INFO, QString("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(); } } q.prepare("INSERT INTO kmmPayeesPayeeIdentifier (payeeId, userOrder, identifierId) VALUES(?, ?, ?)"); q.bindValue(0, payeeIdList); q.bindValue(1, order); q.bindValue(2, identIdList); if (!q.execBatch()) throw MYMONEYEXCEPTION(d->buildError(q, Q_FUNC_INFO, QString("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 q(*this); // Get identifiers first so we know which to delete q.prepare("SELECT identifierId FROM kmmPayeesPayeeIdentifier WHERE payeeId = ?"); q.bindValue(0, payee.id()); if (!q.exec()) throw MYMONEYEXCEPTION(d->buildError(q, Q_FUNC_INFO, QString("removing payee's identifiers (getting old values failed)"))); // krazy:exclude=crashy QStringList identIds; while (q.next()) identIds << q.value(0).toString(); QMap idents = fetchPayeeIdentifiers(identIds); foreach (payeeIdentifier ident, idents) { removePayeeIdentifier(ident); } // Delete entries from mapping table q.prepare("DELETE FROM kmmPayeesPayeeIdentifier WHERE payeeId = ?"); q.bindValue(0, payee.id()); if (!q.exec()) throw MYMONEYEXCEPTION(d->buildError(q, Q_FUNC_INFO, QString("removing payee's identifiers (delete from mapping table)"))); // krazy:exclude=crashy // Delete payee q.prepare(d->m_db.m_tables["kmmPayees"].deleteString()); q.bindValue(":id", payee.id()); if (!q.exec()) throw MYMONEYEXCEPTION(d->buildError(q, Q_FUNC_INFO, QString("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 q(*this); q.prepare(d->m_db.m_tables["kmmTags"].deleteString()); q.bindValue(":id", tag.id()); if (!q.exec()) throw MYMONEYEXCEPTION(d->buildError(q, Q_FUNC_INFO, QString("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 q(*this); q.prepare(d->m_db.m_tables["kmmAccounts"].deleteString()); q.bindValue(":id", acc.id()); if (!q.exec()) throw MYMONEYEXCEPTION(d->buildError(q, Q_FUNC_INFO, QString("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 q(*this); q.prepare("SELECT accountId FROM kmmSplits WHERE transactionId = :txId;"); q.bindValue(":txId", tx.id()); if (!q.exec()) throw MYMONEYEXCEPTION(d->buildError(q, Q_FUNC_INFO, "retrieving old splits")); while (q.next()) { QString id = q.value(0).toString(); --d->m_transactionCountMap[id]; } // add the transaction and splits q.prepare(d->m_db.m_tables["kmmTransactions"].updateString()); d->writeTransaction(tx.id(), tx, q, "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 q(*this); q.prepare(d->m_db.m_tables["kmmSecurities"].deleteString()); q.bindValue(":id", kvpList); if (!q.execBatch()) throw MYMONEYEXCEPTION(d->buildError(q, Q_FUNC_INFO, QString("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 q(*this); QString s = d->m_db.m_tables["kmmPrices"].selectAllString(false); s += " WHERE fromId = :fromId AND toId = :toId AND priceDate = :priceDate;"; q.prepare(s); q.bindValue(":fromId", p.from()); q.bindValue(":toId", p.to()); q.bindValue(":priceDate", p.date().toString(Qt::ISODate)); if (!q.exec()) throw MYMONEYEXCEPTION(d->buildError(q, Q_FUNC_INFO, QString("finding Price"))); // krazy:exclude=crashy if (q.next()) { q.prepare(d->m_db.m_tables["kmmPrices"].updateString()); } else { q.prepare(d->m_db.m_tables["kmmPrices"].insertString()); ++d->m_prices; newRecord = true; } q.bindValue(":fromId", p.from()); q.bindValue(":toId", p.to()); q.bindValue(":priceDate", p.date().toString(Qt::ISODate)); q.bindValue(":price", p.rate(QString()).toString()); const MyMoneySecurity sec = d->m_storage->security(p.to()); q.bindValue(":priceFormatted", p.rate(QString()).formatMoney("", sec.pricePrecision())); q.bindValue(":priceSource", p.source()); if (!q.exec()) throw MYMONEYEXCEPTION(d->buildError(q, Q_FUNC_INFO, QString("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 q(*this); q.prepare(d->m_db.m_tables["kmmPrices"].deleteString()); q.bindValue(":fromId", p.from()); q.bindValue(":toId", p.to()); q.bindValue(":priceDate", p.date().toString(Qt::ISODate)); if (!q.exec()) throw MYMONEYEXCEPTION(d->buildError(q, Q_FUNC_INFO, QString("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 q(*this); q.prepare(d->m_db.m_tables["kmmCurrencies"].deleteString()); q.bindValue(":ISOcode", sec.id()); if (!q.exec()) throw MYMONEYEXCEPTION(d->buildError(q, Q_FUNC_INFO, QString("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 q(*this); q.prepare("DELETE FROM kmmReportConfig WHERE id = :id"); q.bindValue(":id", rep.id()); if (!q.exec()) throw MYMONEYEXCEPTION(d->buildError(q, Q_FUNC_INFO, QString("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 q(*this); q.prepare(d->m_db.m_tables["kmmBudgetConfig"].deleteString()); q.bindValue(":id", bud.id()); if (!q.exec()) throw MYMONEYEXCEPTION(d->buildError(q, Q_FUNC_INFO, QString("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 q(*this); q.prepare("INSERT INTO kmmOnlineJobs (id, type, jobSend, bankAnswerDate, state, locked) VALUES(:id, :type, :jobSend, :bankAnswerDate, :state, :locked);"); d->writeOnlineJob(job, q); if (!q.exec()) throw MYMONEYEXCEPTION(d->buildError(q, Q_FUNC_INFO, QString("writing onlineJob"))); // krazy:exclude=crashy ++d->m_onlineJobs; try { // Save online task d->insertStorableObject(*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 MYMONEYEXCEPTION(d->buildError(query, Q_FUNC_INFO, QString("writing onlineJob"))); // krazy:exclude=crashy try { // Modify online task d->updateStorableObject(*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->deleteStorableObject(*job.constTask(), job.id()); } catch (onlineJob::emptyTask&) { } QSqlQuery q(*this); q.prepare(d->m_db.m_tables["kmmOnlineJobs"].deleteString()); q.bindValue(":id", job.id()); if (!q.exec()) throw MYMONEYEXCEPTION(d->buildError(q, Q_FUNC_INFO, QString("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->insertStorableObject(*ident.data(), ident.idString()); } catch (payeeIdentifier::empty&) { } } void MyMoneyStorageSql::modifyPayeeIdentifier(const payeeIdentifier& ident) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QSqlQuery q(*this); q.prepare("SELECT type FROM kmmPayeeIdentifier WHERE id = ?"); q.bindValue(0, ident.idString()); if (!q.exec() || !q.next()) throw MYMONEYEXCEPTION(d->buildError(q, Q_FUNC_INFO, QString("modifying payeeIdentifier"))); // krazy:exclude=crashy bool typeChanged = (q.value(0).toString() != ident.iid()); if (typeChanged) { // Delete old identifier if type changed const payeeIdentifier oldIdent(fetchPayeeIdentifier(ident.idString())); try { d->deleteStorableObject(*oldIdent.data(), ident.idString()); } catch (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(QLatin1String("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 } } q.prepare("UPDATE kmmPayeeIdentifier SET type = :type WHERE id = :id"); d->writePayeeIdentifier(ident, q); try { if (typeChanged) d->insertStorableObject(*ident.data(), ident.idString()); else d->updateStorableObject(*ident.data(), ident.idString()); } catch (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->deleteStorableObject(*ident.data(), ident.idString()); } catch (payeeIdentifier::empty&) { } QSqlQuery q(*this); q.prepare(d->m_db.m_tables["kmmPayeeIdentifier"].deleteString()); q.bindValue(":id", ident.idString()); if (!q.exec()) throw MYMONEYEXCEPTION(d->buildError(q, Q_FUNC_INFO, QString("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("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 MYMONEYEXCEPTION(d->buildError(query, Q_FUNC_INFO, QString("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("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); // 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 MYMONEYEXCEPTION(d->buildError(query, Q_FUNC_INFO, QString("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 = onlineJobAdministration::instance()->createOnlineTaskFromSqlDatabase(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(QLatin1String("payeeIdentifier with id '") + id + QLatin1String("' not found.")); // 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 MYMONEYEXCEPTION(d->buildError(query, Q_FUNC_INFO, QString("reading payee identifiers"))); // krazy:exclude=crashy QMap identList; while (query.next()) { const QString id = query.value(0).toString(); identList.insert(id, payeeIdentifierLoader::instance()->createPayeeIdentifierFromSqlDatabase(*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 MYMONEYEXCEPTION(d->buildError(query, Q_FUNC_INFO, QString("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 MYMONEYEXCEPTION(d->buildError(query, Q_FUNC_INFO, QString("reading Account"))); // krazy:exclude=crashy if (!sq.exec()) throw MYMONEYEXCEPTION(d->buildError(query, Q_FUNC_INFO, QString("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 MYMONEYEXCEPTION(d->buildError(query, Q_FUNC_INFO, QString("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 MYMONEYEXCEPTION(d->buildError(query, Q_FUNC_INFO, QString("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 MYMONEYEXCEPTION(d->buildError(query, Q_FUNC_INFO, QString("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& t = d->m_db.m_tables["kmmTransactions"]; + const MyMoneyDbTable& transactionTable = d->m_db.m_tables["kmmTransactions"]; QSqlQuery q2(*const_cast (this)); - q2.prepare(t.selectAllString(false) + " WHERE id = :id;"); + 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(t.fieldNumber("postDate")).toString())); - tx.setMemo(q2.value(t.fieldNumber("memo")).toString()); - tx.setEntryDate(d->GETDATE(q2.value(t.fieldNumber("entryDate")).toString())); - tx.setCommodity(q2.value(t.fieldNumber("currencyId")).toString()); - tx.setBankID(q2.value(t.fieldNumber("bankId")).toString()); + 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 MYMONEYEXCEPTION(d->buildError(query, Q_FUNC_INFO, QString("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 MYMONEYEXCEPTION(d->buildError(query, Q_FUNC_INFO, QString("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 MYMONEYEXCEPTION(d->buildError(query, Q_FUNC_INFO, QString("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 MYMONEYEXCEPTION(d->buildError(query, Q_FUNC_INFO, QString("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 MYMONEYEXCEPTION(d->buildError(query, Q_FUNC_INFO, QString("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 MYMONEYEXCEPTION(d->buildError(query, Q_FUNC_INFO, QString("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 MYMONEYEXCEPTION(d->buildError(query, Q_FUNC_INFO, QString("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 900e81e9a..95423bb5d 100644 --- a/kmymoney/plugins/sql/mymoneystoragesql_p.h +++ b/kmymoney/plugins/sql/mymoneystoragesql_p.h @@ -1,2835 +1,2834 @@ /*************************************************************************** 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 "payeeidentifier/payeeidentifierdata.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(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() 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() { } /** * 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 MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, QString("retrieving highest ID number"))); // krazy:exclude=crashy 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 MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, "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 MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, "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 MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, "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 MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, "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 MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, "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 MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, "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 MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, "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 MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, "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..."); - QList::ConstIterator it; 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 MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, "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 MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, "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 MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, "deleting Security")); query2.bindValue(":fromId", idList); query2.bindValue(":toId", idList); if (!query2.execBatch()) throw MYMONEYEXCEPTION(buildError(query2, Q_FUNC_INFO, "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 MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, QString("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 MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, "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 MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, "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 MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, "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 MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, "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 MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, QString("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 MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, "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 MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, "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 MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, "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 MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, "deleting Budget")); } } void writeOnlineJobs() { Q_Q(MyMoneyStorageSql); QSqlQuery query(*q); if (!query.exec("DELETE FROM kmmOnlineJobs;")) throw MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, QLatin1String("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 (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(i18np("Could not save one onlineJob.", "Could not save %1 onlineJobs.", failedJobs.count())); } } /** @} */ /** * @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 MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, QString("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); 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 MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, QString("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 MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, QString("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 MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, QString("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 MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, QString("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 MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, "building Split list")); // krazy:exclude=crashy while (query.next()) dbList.append(query.value(0).toUInt()); - QList::ConstIterator it; - uint i = 0; QSqlQuery query2(*q); query.prepare(m_db.m_tables["kmmSplits"].updateString()); query2.prepare(m_db.m_tables["kmmSplits"].insertString()); - for (it = splitList.constBegin(), i = 0; it != splitList.constEnd(); ++it, ++i) { + 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 MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, "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 MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, QString("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 MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, QString("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 MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, QString("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 MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, QString("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 MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, QString("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 MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, QString("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 MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, QString("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 MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, QString("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 MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, QString("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 MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, QString("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 MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, QString("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 MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, QString("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 MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, QString("reading FileInfo"))); // krazy:exclude=crashy if (!query.next()) throw MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, QString("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 MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, QString("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 MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, QString("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 MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, QString("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 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 MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, "deleting Splits")); query.prepare("DELETE FROM kmmKeyValuePairs WHERE kvpType = 'SPLIT' " "AND kvpId LIKE '?%'"); query.bindValue(1, idList); if (!query.execBatch()) throw MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, "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 MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, "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 MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, QString("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 MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, "deleting Schedule Payment History")); // krazy:exclude=crashy query.prepare(m_db.m_tables["kmmSchedules"].deleteString()); query.bindValue(":id", id); if (!query.exec()) throw MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, "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 MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, QString("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("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 MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, QString("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 MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, QString("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 MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, QString("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 MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, QString("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 MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, QString("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 MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, QString("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 MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, "getting record count")); // krazy:exclude=crashy if (!query.next()) throw MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, "retrieving record count")); recordCount += query.value(0).toInt(); ++tt; } if (recordCount != 0) { 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_UNUSED(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 errorMsg; KMyMoneyPlugin::storagePlugin* plugin = KServiceTypeTrader::createInstanceFromQuery( QLatin1String("KMyMoney/sqlStoragePlugin"), QString("'%1' ~in [X-KMyMoney-PluginIid]").arg(iid.replace(QLatin1Char('\''), QLatin1String("\\'"))), 0, QVariantList(), &errorMsg ); if (plugin == 0) throw MYMONEYEXCEPTION(QString("Could not load sqlStoragePlugin '%1', (error: %2)").arg(iid, errorMsg)); MyMoneyDbTransaction t(*q, Q_FUNC_INFO); if (plugin->setupDatabase(*q)) { m_loadedStoragePlugins.insert(iid); return true; } throw MYMONEYEXCEPTION(QString("Could not install sqlStoragePlugin '%1' in database.").arg(iid)); } void insertStorableObject(const databaseStoreableObject& obj, const QString& id) { Q_Q(MyMoneyStorageSql); setupStoragePlugin(obj.storagePluginIid()); if (!obj.sqlSave(*q, id)) throw MYMONEYEXCEPTION(QString("Could not save object with id '%1' in database (plugin failed).").arg(id)); } void updateStorableObject(const databaseStoreableObject& obj, const QString& id) { Q_Q(MyMoneyStorageSql); setupStoragePlugin(obj.storagePluginIid()); if (!obj.sqlModify(*q, id)) throw MYMONEYEXCEPTION(QString("Could not modify object with id '%1' in database (plugin failed).").arg(id)); } void deleteStorableObject(const databaseStoreableObject& obj, const QString& id) { Q_Q(MyMoneyStorageSql); setupStoragePlugin(obj.storagePluginIid()); if (!obj.sqlRemove(*q, id)) throw MYMONEYEXCEPTION(QString("Could not remove object with id '%1' from database (plugin failed).").arg(id)); } void alert(QString s) const // FIXME: remove... { qDebug() << s; } void signalProgress(int current, int total, const QString& msg) const { if (m_progressCallback != 0) (*m_progressCallback)(current, total, msg); } void signalProgress(int current, int 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/payeeidentifier/ibanandbic/ibanbicstorageplugin.h b/kmymoney/plugins/sql/payeeidentifier/ibanandbic/ibanbicstorageplugin.h index 18a195f96..970b5ddba 100644 --- a/kmymoney/plugins/sql/payeeidentifier/ibanandbic/ibanbicstorageplugin.h +++ b/kmymoney/plugins/sql/payeeidentifier/ibanandbic/ibanbicstorageplugin.h @@ -1,37 +1,37 @@ /* * This file is part of KMyMoney, A Personal Finance Manager by KDE * Copyright (C) 2014 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 IBANBICSTORAGEPLUGIN_H #define IBANBICSTORAGEPLUGIN_H #include "mymoney/storage/kmymoneystorageplugin.h" class ibanBicStoragePlugin : public KMyMoneyPlugin::storagePlugin { Q_OBJECT Q_INTERFACES(KMyMoneyPlugin::storagePlugin) // Q_PLUGIN_METADATA(IID "org.kmymoney.payeeIdentifier.ibanbic.sqlStoragePlugin") public: explicit ibanBicStoragePlugin(QObject* parent = 0, const QVariantList& options = QVariantList()); - virtual bool removePluginData(QSqlDatabase connection); - virtual bool setupDatabase(QSqlDatabase connection); + bool removePluginData(QSqlDatabase connection) final override; + bool setupDatabase(QSqlDatabase connection) final override; static QString iid(); }; #endif // IBANBICSTORAGEPLUGIN_H diff --git a/kmymoney/plugins/sql/payeeidentifier/nationalaccount/nationalaccountstorageplugin.h b/kmymoney/plugins/sql/payeeidentifier/nationalaccount/nationalaccountstorageplugin.h index 8ef5bae6e..c19710c27 100644 --- a/kmymoney/plugins/sql/payeeidentifier/nationalaccount/nationalaccountstorageplugin.h +++ b/kmymoney/plugins/sql/payeeidentifier/nationalaccount/nationalaccountstorageplugin.h @@ -1,36 +1,36 @@ /* * This file is part of KMyMoney, A Personal Finance Manager by KDE * Copyright (C) 2014 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 NATIONALACCOUNTSTORAGEPLUGIN_H #define NATIONALACCOUNTSTORAGEPLUGIN_H #include "mymoney/storage/kmymoneystorageplugin.h" class nationalAccountStoragePlugin : public KMyMoneyPlugin::storagePlugin { Q_OBJECT Q_INTERFACES(KMyMoneyPlugin::storagePlugin) public: explicit nationalAccountStoragePlugin(QObject* parent = 0, const QVariantList& options = QVariantList()); - virtual bool removePluginData(QSqlDatabase connection); - virtual bool setupDatabase(QSqlDatabase connection); + bool removePluginData(QSqlDatabase connection) final override; + bool setupDatabase(QSqlDatabase connection) final override; static QString iid(); }; #endif // NATIONALACCOUNTSTORAGEPLUGIN_H diff --git a/kmymoney/plugins/sql/tests/mymoneydatabasemgr-test.cpp b/kmymoney/plugins/sql/tests/mymoneydatabasemgr-test.cpp index 952197ad0..34bba1843 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); - MyMoneyFileTransaction ft; + 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); - MyMoneyFileTransaction ft; + 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 m, m1; + MyMoneyPayee::payeeMatchType m1, m2; bool ignore, ignore1; QStringList keys, keys1; - m = p.matchData(ignore, keys); - m1 = p1.matchData(ignore1, keys1); - QVERIFY(m == m1); + 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/forecast/fixedcolumntreeview.cpp b/kmymoney/plugins/views/forecast/fixedcolumntreeview.cpp index c985a0f74..ceea93fd5 100644 --- a/kmymoney/plugins/views/forecast/fixedcolumntreeview.cpp +++ b/kmymoney/plugins/views/forecast/fixedcolumntreeview.cpp @@ -1,285 +1,285 @@ /* * Copyright 2014 Cristian Oneț * * 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) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * 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 "fixedcolumntreeview.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes class FixedColumnDelegate : public QStyledItemDelegate { QTreeView *m_sourceView; public: explicit FixedColumnDelegate(FixedColumnTreeView *fixedColumView, QTreeView *sourceView) : QStyledItemDelegate(fixedColumView), m_sourceView(sourceView) { } - virtual void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { + virtual void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const final override { QStyleOptionViewItem optV4 = option; initStyleOption(&optV4, index); // the fixed column's position has always this value optV4.viewItemPosition = QStyleOptionViewItem::Beginning; if (m_sourceView->hasFocus()) { // draw the current row as active if the source list has focus QModelIndex currentIndex = m_sourceView->currentIndex(); if (currentIndex.isValid() && currentIndex.row() == index.row() && currentIndex.parent() == index.parent()) { optV4.state |= QStyle::State_Active; } } QStyledItemDelegate::paint(painter, optV4, index); } }; struct FixedColumnTreeView::Private { Private(FixedColumnTreeView *pub, QTreeView *parent) : - pub(pub), - parent(parent) { + m_pub(pub), + m_parent(parent) { } void syncExpanded(const QModelIndex& parentIndex = QModelIndex()) { - const int rows = parent->model()->rowCount(parentIndex); + const int rows = m_parent->model()->rowCount(parentIndex); for (auto i = 0; i < rows; ++i) { - const QModelIndex &index = parent->model()->index(i, 0, parentIndex); - if (parent->isExpanded(index)) { - pub->expand(index); + const QModelIndex &index = m_parent->model()->index(i, 0, parentIndex); + if (m_parent->isExpanded(index)) { + m_pub->expand(index); syncExpanded(index); } } } void syncModels() { - if (pub->model() != parent->model()) { + if (m_pub->model() != m_parent->model()) { // set the model - pub->setModel(parent->model()); + m_pub->setModel(m_parent->model()); // hide all but the first column - for (int col = 1; col < pub->model()->columnCount(); ++col) - pub->setColumnHidden(col, true); + for (int col = 1; col < m_pub->model()->columnCount(); ++col) + m_pub->setColumnHidden(col, true); // set the selection model - pub->setSelectionModel(parent->selectionModel()); + m_pub->setSelectionModel(m_parent->selectionModel()); // when the model has changed we need to sync the expanded state of the views syncExpanded(); } } void syncProperties() { //pub->setAllColumnsShowFocus(parent->allColumnsShowFocus()); - pub->setAlternatingRowColors(parent->alternatingRowColors()); - pub->setIconSize(parent->iconSize()); - pub->setSortingEnabled(parent->isSortingEnabled()); - pub->setUniformRowHeights(pub->uniformRowHeights()); + m_pub->setAlternatingRowColors(m_parent->alternatingRowColors()); + m_pub->setIconSize(m_parent->iconSize()); + m_pub->setSortingEnabled(m_parent->isSortingEnabled()); + m_pub->setUniformRowHeights(m_pub->uniformRowHeights()); } void syncGeometry() { // update the geometry of the fixed column view to match that of the source model's geometry - pub->setGeometry(parent->frameWidth(), parent->frameWidth(), parent->columnWidth(0), - parent->viewport()->height() + (parent->header()->isVisible() ? parent->header()->height() : 0)); + m_pub->setGeometry(m_parent->frameWidth(), m_parent->frameWidth(), m_parent->columnWidth(0), + m_parent->viewport()->height() + (m_parent->header()->isVisible() ? m_parent->header()->height() : 0)); } - FixedColumnTreeView *pub; - QTreeView *parent; + FixedColumnTreeView *m_pub; + QTreeView *m_parent; }; FixedColumnTreeView::FixedColumnTreeView(QTreeView *parent) : QTreeView(parent) , d(new Private(this, parent)) { // no borders and scrollbars for the fixed column view setStyleSheet("QTreeView { border: none; }"); setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); // the focus proxy is forwarded to the source list setFocusProxy(parent); // perform the special selection and hover drawing even when the fixed column view has no focus - setItemDelegate(new FixedColumnDelegate(this, d->parent)); + setItemDelegate(new FixedColumnDelegate(this, d->m_parent)); // stack the source view under the fixed column view - d->parent->viewport()->stackUnder(this); + d->m_parent->viewport()->stackUnder(this); // the resize mode of the fixed view needs to be fixed to allow a user resize only from the parent tree header()->sectionResizeMode(QHeaderView::Fixed); // connect the scroll bars to keep the two views in sync - connect(verticalScrollBar(), &QAbstractSlider::valueChanged, d->parent->verticalScrollBar(), &QAbstractSlider::setValue); - connect(d->parent->verticalScrollBar(), &QAbstractSlider::valueChanged, verticalScrollBar(), &QAbstractSlider::setValue); + connect(verticalScrollBar(), &QAbstractSlider::valueChanged, d->m_parent->verticalScrollBar(), &QAbstractSlider::setValue); + connect(d->m_parent->verticalScrollBar(), &QAbstractSlider::valueChanged, verticalScrollBar(), &QAbstractSlider::setValue); // keep the expanded/collapsed states synchronized between the two views - connect(d->parent, &QTreeView::expanded, this, &FixedColumnTreeView::onExpanded); + connect(d->m_parent, &QTreeView::expanded, this, &FixedColumnTreeView::onExpanded); connect(this, &QTreeView::expanded, this, &FixedColumnTreeView::onExpanded); - connect(d->parent, &QTreeView::collapsed, this, &FixedColumnTreeView::onCollapsed); + connect(d->m_parent, &QTreeView::collapsed, this, &FixedColumnTreeView::onCollapsed); connect(this, &QTreeView::collapsed, this, &FixedColumnTreeView::onCollapsed); // keep the sort indicators synchronized between the two views - connect(d->parent->header(), &QHeaderView::sortIndicatorChanged, this, &FixedColumnTreeView::updateSortIndicator); + connect(d->m_parent->header(), &QHeaderView::sortIndicatorChanged, this, &FixedColumnTreeView::updateSortIndicator); connect(header(), &QHeaderView::sortIndicatorChanged, this, &FixedColumnTreeView::updateSortIndicator); // forward all signals - connect(this, &QAbstractItemView::activated, d->parent, &QAbstractItemView::activated); - connect(this, &QAbstractItemView::clicked, d->parent, &QAbstractItemView::clicked); - connect(this, &QAbstractItemView::doubleClicked, d->parent, &QAbstractItemView::doubleClicked); - connect(this, &QAbstractItemView::entered, d->parent, &QAbstractItemView::entered); - connect(this, &QAbstractItemView::pressed, d->parent, &QAbstractItemView::pressed); - connect(this, &QAbstractItemView::viewportEntered, d->parent, &QAbstractItemView::viewportEntered); + connect(this, &QAbstractItemView::activated, d->m_parent, &QAbstractItemView::activated); + connect(this, &QAbstractItemView::clicked, d->m_parent, &QAbstractItemView::clicked); + connect(this, &QAbstractItemView::doubleClicked, d->m_parent, &QAbstractItemView::doubleClicked); + connect(this, &QAbstractItemView::entered, d->m_parent, &QAbstractItemView::entered); + connect(this, &QAbstractItemView::pressed, d->m_parent, &QAbstractItemView::pressed); + connect(this, &QAbstractItemView::viewportEntered, d->m_parent, &QAbstractItemView::viewportEntered); // handle the resize of the first column in the source view - connect(d->parent->header(), &QHeaderView::sectionResized, this, &FixedColumnTreeView::updateSectionWidth); + connect(d->m_parent->header(), &QHeaderView::sectionResized, this, &FixedColumnTreeView::updateSectionWidth); // forward right click to the source list - setContextMenuPolicy(d->parent->contextMenuPolicy()); + setContextMenuPolicy(d->m_parent->contextMenuPolicy()); if (contextMenuPolicy() == Qt::CustomContextMenu) { - connect(this, &QWidget::customContextMenuRequested, d->parent, &QWidget::customContextMenuRequested); + connect(this, &QWidget::customContextMenuRequested, d->m_parent, &QWidget::customContextMenuRequested); } // enable hover indicator synchronization between the two views - d->parent->viewport()->installEventFilter(this); - d->parent->viewport()->setMouseTracking(true); + d->m_parent->viewport()->installEventFilter(this); + d->m_parent->viewport()->setMouseTracking(true); viewport()->setMouseTracking(true); d->syncProperties(); - if (d->parent->isVisible()) { + if (d->m_parent->isVisible()) { // the source view is already visible so show the frozen column also d->syncModels(); show(); d->syncGeometry(); } } FixedColumnTreeView::~FixedColumnTreeView() { delete d; } void FixedColumnTreeView::sourceModelUpdated() { d->syncModels(); d->syncGeometry(); } void FixedColumnTreeView::onExpanded(const QModelIndex& index) { - if (sender() == this && !d->parent->isExpanded(index)) { - d->parent->expand(index); + if (sender() == this && !d->m_parent->isExpanded(index)) { + d->m_parent->expand(index); } - if (sender() == d->parent && !isExpanded(index)) { + if (sender() == d->m_parent && !isExpanded(index)) { expand(index); } } void FixedColumnTreeView::onCollapsed(const QModelIndex& index) { - if (sender() == this && d->parent->isExpanded(index)) { - d->parent->collapse(index); + if (sender() == this && d->m_parent->isExpanded(index)) { + d->m_parent->collapse(index); } - if (sender() == d->parent && isExpanded(index)) { + if (sender() == d->m_parent && isExpanded(index)) { collapse(index); } } bool FixedColumnTreeView::viewportEvent(QEvent *event) { if (underMouse()) { // forward mouse move and hover leave events to the source list if (event->type() == QEvent::MouseMove || event->type() == QEvent::HoverLeave) { - QApplication::sendEvent(d->parent->viewport(), event); + QApplication::sendEvent(d->m_parent->viewport(), event); } } return QTreeView::viewportEvent(event); } bool FixedColumnTreeView::eventFilter(QObject *object, QEvent *event) { - if (object == d->parent->viewport()) { + if (object == d->m_parent->viewport()) { switch (event->type()) { case QEvent::MouseMove: - if (!underMouse() && d->parent->underMouse()) { + if (!underMouse() && d->m_parent->underMouse()) { QMouseEvent *me = static_cast(event); // translate the position of the event but don't send buttons or modifiers because we only need the movement for the hover QMouseEvent translatedMouseEvent(me->type(), QPoint(width() - 2, me->pos().y()), Qt::NoButton, Qt::MouseButtons(), Qt::KeyboardModifiers()); QApplication::sendEvent(viewport(), &translatedMouseEvent); } break; case QEvent::HoverLeave: - if (!underMouse() && d->parent->underMouse()) { + if (!underMouse() && d->m_parent->underMouse()) { QApplication::sendEvent(viewport(), event); } break; case QEvent::Show: d->syncModels(); show(); // intentional fall through case QEvent::Resize: d->syncGeometry(); break; default: break; } } return QTreeView::eventFilter(object, event); } void FixedColumnTreeView::updateSectionWidth(int logicalIndex, int, int newSize) { if (logicalIndex == 0) { - const int maxFirstColumnWidth = d->parent->width() * 0.8; + const int maxFirstColumnWidth = d->m_parent->width() * 0.8; if (newSize > maxFirstColumnWidth) { // limit the size of the first column so that it will not become larger than the view's width - d->parent->setColumnWidth(logicalIndex, maxFirstColumnWidth); + d->m_parent->setColumnWidth(logicalIndex, maxFirstColumnWidth); } else { // update the size of the fixed column setColumnWidth(0, newSize); // update the geometry d->syncGeometry(); } } } void FixedColumnTreeView::updateSortIndicator(int logicalIndex, Qt::SortOrder order) { - if (sender() == header() && d->parent->header()->sortIndicatorSection() != logicalIndex) { - d->parent->header()->setSortIndicator(logicalIndex, order); + if (sender() == header() && d->m_parent->header()->sortIndicatorSection() != logicalIndex) { + d->m_parent->header()->setSortIndicator(logicalIndex, order); } - if (sender() == d->parent->header() && header()->sortIndicatorSection() != logicalIndex) { + if (sender() == d->m_parent->header() && header()->sortIndicatorSection() != logicalIndex) { header()->setSortIndicator(logicalIndex, order); } } diff --git a/kmymoney/plugins/views/forecast/kforecastview_p.h b/kmymoney/plugins/views/forecast/kforecastview_p.h index a8ed4f81f..b69155e98 100644 --- a/kmymoney/plugins/views/forecast/kforecastview_p.h +++ b/kmymoney/plugins/views/forecast/kforecastview_p.h @@ -1,1048 +1,1048 @@ /*************************************************************************** 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; int dropMinimum; int dropZero; 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 int daysToBeginDay; if (QDate::currentDate() < forecast.beginForecastDate()) { daysToBeginDay = QDate::currentDate().daysTo(forecast.beginForecastDate()); } else { daysToBeginDay = forecast.accountsCycle(); } for (int i = 0; ((i*forecast.accountsCycle()) + daysToBeginDay) <= forecast.forecastDays(); ++i) { int 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 dropMinimum = forecast.daysToMinimumBalance(acc); //Check if the account is going to be below zero in the future 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(); int 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 (int i = 1; ((i * forecast.accountsCycle()) + daysToBeginDay) <= forecast.forecastDays(); ++i) { headerLabels << i18n("Min Bal %1", i); headerLabels << i18n("Min Date %1", i); } for (int 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()); int 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"); { - QDate forecastStartDate = forecast.forecastStartDate(); - QDate forecastEndDate = forecast.forecastEndDate(); + 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 }; MyMoneyReport reportCfg = MyMoneyReport( MyMoneyReport::eAssetLiability, MyMoneyReport::eMonths, 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.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; int it_c = 1; // iterator for the columns of the listview const auto file = MyMoneyFile::instance(); int 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 (QDate summaryDate = QDate::currentDate().addDays(daysToBeginDay); summaryDate <= forecast.forecastEndDate(); summaryDate = summaryDate.addDays(forecast.accountsCycle()), ++it_c) { + 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/onlinejoboutbox/konlinetransferform.h b/kmymoney/plugins/views/onlinejoboutbox/konlinetransferform.h index bec1b5a20..588aceec6 100644 --- a/kmymoney/plugins/views/onlinejoboutbox/konlinetransferform.h +++ b/kmymoney/plugins/views/onlinejoboutbox/konlinetransferform.h @@ -1,137 +1,137 @@ /* * This file is part of KMyMoney, A Personal Finance Manager by KDE * Copyright (C) 2014 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. * * 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 KONLINETRANSFERFORM_H #define KONLINETRANSFERFORM_H // ---------------------------------------------------------------------------- // QT Includes #include // ---------------------------------------------------------------------------- // Project Includes #include "mymoney/onlinejob.h" #include "mymoney/onlinejobadministration.h" class IonlineJobEdit; class KMandatoryFieldGroup; namespace Ui { class kOnlineTransferForm; } /** * @brief The kOnlineTransferForm class * * @todo Disable Send/Enque button if no task is shown. * @todo If this dialog is shown a second time without setting a onlineJob it, it shows the previous content. * Fix this by creating the IonlineJobEdit widgets on demand and destroying them afterwards. */ class kOnlineTransferForm : public QDialog { Q_OBJECT public: explicit kOnlineTransferForm(QWidget *parent = nullptr); virtual ~kOnlineTransferForm(); Q_SIGNALS: /** @brief The user wants this job to be saved */ void acceptedForSave(onlineJob); /** @brief User wants to send the onlineJob directly */ void acceptedForSend(onlineJob); public Q_SLOTS: - virtual void accept(); - virtual void reject(); + void accept() final override; + void reject()final override; /** @brief sets the current origin account */ virtual void setCurrentAccount(const QString& accountId); /** * @brief Sets an onlineTransfer to edit * * @return true if there is widget which supports editing this onlineJob */ virtual bool setOnlineJob(const onlineJob); void duplicateCurrentJob(); private Q_SLOTS: /** @brief Slot for account selection box */ void accountChanged(); /** * @brief Slot to change job type * @param index of KComboBox (== index of selected widget in m_onlineJobEditWidgets) */ void convertCurrentJob(const int& index); /** @brief Slot for send button */ void sendJob(); /** * @brief Load a plugin */ void loadOnlineJobEditPlugin(const onlineJobAdministration::onlineJobEditOffer& plugin); /** @{ */ /** * @brief Activates the onlineJobEdit widget */ bool showEditWidget(const QString& onlineTaskName); void showEditWidget(IonlineJobEdit* widget); /** @} */ /** * @brief Shows warning if checkEditWidget() == false */ void checkNotSupportedWidget(); void setJobReadOnly(const bool&); private: /** * @brief returns the currently edited onlineJob * Can be a null job */ onlineJob activeOnlineJob() const; Ui::kOnlineTransferForm* ui; QList m_onlineJobEditWidgets; KMandatoryFieldGroup* m_requiredFields; QAction* m_duplicateJob; /** * @brief Checks if widget can edit any task the selected account supports */ bool checkEditWidget(IonlineJobEdit* widget); /** * @brief Checks current widget * @see checkEditWidget( IonlineJobEdit* widget ) */ bool checkEditWidget(); void editWidgetChanged(); }; #endif // KONLINETRANSFERFORM_H diff --git a/kmymoney/plugins/views/onlinejoboutbox/onlinejobmessagesmodel.h b/kmymoney/plugins/views/onlinejoboutbox/onlinejobmessagesmodel.h index 811a762a9..bb93a1265 100644 --- a/kmymoney/plugins/views/onlinejoboutbox/onlinejobmessagesmodel.h +++ b/kmymoney/plugins/views/onlinejoboutbox/onlinejobmessagesmodel.h @@ -1,45 +1,45 @@ /* * This file is part of KMyMoney, A Personal Finance Manager by KDE * Copyright (C) 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 ONLINEJOBMESSAGESMODEL_H #define ONLINEJOBMESSAGESMODEL_H #include #include "mymoney/onlinejob.h" class onlineJobMessagesModel : public QAbstractTableModel { Q_OBJECT public: explicit onlineJobMessagesModel(QObject* parent = 0); - virtual QVariant data(const QModelIndex& index, int role) const; - virtual int columnCount(const QModelIndex& parent) const; - virtual int rowCount(const QModelIndex& parent) const; - virtual QModelIndex parent(const QModelIndex& child) const; - virtual QVariant headerData(int section, Qt::Orientation orientation, int role) const; + QVariant data(const QModelIndex& index, int role) const final override; + int columnCount(const QModelIndex& parent) const final override; + int rowCount(const QModelIndex& parent) const final override; + QModelIndex parent(const QModelIndex& child) const final override; + QVariant headerData(int section, Qt::Orientation orientation, int role) const final override; public Q_SLOTS: void setOnlineJob(const onlineJob& job); protected: onlineJob m_job; }; #endif // ONLINEJOBMESSAGESMODEL_H diff --git a/kmymoney/plugins/views/onlinejoboutbox/onlinejobmodel.h b/kmymoney/plugins/views/onlinejoboutbox/onlinejobmodel.h index e484ca9ed..ad450e0ed 100644 --- a/kmymoney/plugins/views/onlinejoboutbox/onlinejobmodel.h +++ b/kmymoney/plugins/views/onlinejoboutbox/onlinejobmodel.h @@ -1,80 +1,80 @@ /* * This file is part of KMyMoney, A Personal Finance Manager by KDE * Copyright (C) 2014-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 ONLINEJOBMODEL_H #define ONLINEJOBMODEL_H #include #include class MyMoneyObject; namespace eMyMoney { namespace File { enum class Object; } } class onlineJobModel : public QAbstractTableModel { Q_OBJECT public: /** * @brief Item Data roles for onlineJobs * In addition to Qt::ItemDataRole */ enum roles { OnlineJobId = Qt::UserRole, /**< QString of onlineJob.id() */ OnlineJobRole /**< the real onlineJob */ }; enum columns { ColAccount, ColAction, ColDestination, ColValue }; /** Only @ref Models should be able to construct this class */ explicit onlineJobModel(QObject *parent = nullptr); friend class Models; - int rowCount(const QModelIndex & parent = QModelIndex()) const; - int columnCount(const QModelIndex & parent = QModelIndex()) const; - QVariant data(const QModelIndex & index, int role = Qt::DisplayRole) const; - QVariant headerData(int section, Qt::Orientation orientation , int role = Qt::DisplayRole) const; + int rowCount(const QModelIndex & parent = QModelIndex()) const final override; + int columnCount(const QModelIndex & parent = QModelIndex()) const final override; + QVariant data(const QModelIndex & index, int role = Qt::DisplayRole) const final override; + QVariant headerData(int section, Qt::Orientation orientation , int role = Qt::DisplayRole) const final override; /** @brief Remove onlineJob identified by row */ bool removeRow(int row, const QModelIndex & parent = QModelIndex()); /** @brief Remove onlineJobs identified by row and count */ - bool removeRows(int row, int count, const QModelIndex & parent = QModelIndex()); + bool removeRows(int row, int count, const QModelIndex & parent = QModelIndex()) final override; Q_SIGNALS: public Q_SLOTS: void reloadAll(); void slotObjectAdded(eMyMoney::File::Object objType, const QString &id); void slotObjectModified(eMyMoney::File::Object objType, const QString &id); void slotObjectRemoved(eMyMoney::File::Object objType, const QString& id); /** @brief Load data from MyMoneyFile */ void load(); void unload(); private: QStringList m_jobIdList; }; #endif // ONLINEJOBMODEL_H diff --git a/kmymoney/plugins/views/onlinejoboutbox/onlinejoboutboxview.h b/kmymoney/plugins/views/onlinejoboutbox/onlinejoboutboxview.h index fc5330935..f84063e05 100644 --- a/kmymoney/plugins/views/onlinejoboutbox/onlinejoboutboxview.h +++ b/kmymoney/plugins/views/onlinejoboutbox/onlinejoboutboxview.h @@ -1,47 +1,47 @@ /*************************************************************************** onlinejoboutboxview.h ------------------- copyright : (C) 2018 by Łukasz Wojniłowicz email : lukasz.wojnilowicz@gmail.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. * * * ***************************************************************************/ #ifndef ONLINEJOBOUTBOXVIEW_H #define ONLINEJOBOUTBOXVIEW_H // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // QT Includes // Project Includes #include "kmymoneyplugin.h" class KOnlineJobOutboxView; class OnlineJobOutboxView : public KMyMoneyPlugin::Plugin { Q_OBJECT public: explicit OnlineJobOutboxView(QObject *parent, const QVariantList &args); ~OnlineJobOutboxView() final; - void plug() final; - void unplug() final; + void plug() final override; + void unplug() final override; private: KOnlineJobOutboxView* m_view; }; #endif diff --git a/kmymoney/plugins/views/reports/kreportsview_p.h b/kmymoney/plugins/views/reports/kreportsview_p.h index 0a0058896..f78c7beb7 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); + 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) { m_table = new PivotTable(m_report); m_chartEnabled = true; } else if (m_report.reportType() == MyMoneyReport::eQueryTable) { m_table = new QueryTable(m_report); m_chartEnabled = false; } else if (m_report.reportType() == MyMoneyReport::eInfoTable) { 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) { - QTreeWidgetItem* item = m_tocTreeWidget->topLevelItem(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()) { - QString groupName = (*it_group).name(); + 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; - QString groupName = (*it_report).group(); + 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; - QString groupName = I18N_NOOP("Old Customized Reports"); + 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, TransactionFilter::Date::CurrentMonth, MyMoneyReport::eDetailAll, i18n("Income and Expenses This Month"), i18n("Default Report") )); list.push_back(MyMoneyReport( MyMoneyReport::eExpenseIncome, MyMoneyReport::eMonths, TransactionFilter::Date::YearToDate, MyMoneyReport::eDetailAll, i18n("Income and Expenses This Year"), i18n("Default Report") )); list.push_back(MyMoneyReport( MyMoneyReport::eExpenseIncome, MyMoneyReport::eYears, TransactionFilter::Date::All, MyMoneyReport::eDetailAll, i18n("Income and Expenses By Year"), i18n("Default Report") )); list.push_back(MyMoneyReport( MyMoneyReport::eExpenseIncome, MyMoneyReport::eMonths, TransactionFilter::Date::Last12Months, MyMoneyReport::eDetailTop, i18n("Income and Expenses Graph"), i18n("Default Report") )); list.back().setChartByDefault(true); list.back().setChartType(MyMoneyReport::eChartLine); list.back().setChartDataLabels(false); list.push_back(MyMoneyReport( MyMoneyReport::eExpenseIncome, MyMoneyReport::eMonths, TransactionFilter::Date::YearToDate, MyMoneyReport::eDetailGroup, i18n("Income and Expenses Pie Chart"), i18n("Default Report") )); list.back().setChartByDefault(true); list.back().setChartType(MyMoneyReport::eChartPie); list.back().setShowingRowTotals(false); groups.push_back(list); } { ReportGroup list("Net Worth", i18n("Net Worth")); list.push_back(MyMoneyReport( MyMoneyReport::eAssetLiability, MyMoneyReport::eMonths, TransactionFilter::Date::YearToDate, MyMoneyReport::eDetailTop, i18n("Net Worth By Month"), i18n("Default Report") )); list.push_back(MyMoneyReport( MyMoneyReport::eAssetLiability, MyMoneyReport::eMonths, TransactionFilter::Date::Today, MyMoneyReport::eDetailTop, i18n("Net Worth Today"), i18n("Default Report") )); list.push_back(MyMoneyReport( MyMoneyReport::eAssetLiability, MyMoneyReport::eYears, TransactionFilter::Date::All, MyMoneyReport::eDetailTop, i18n("Net Worth By Year"), i18n("Default Report") )); list.push_back(MyMoneyReport( MyMoneyReport::eAssetLiability, MyMoneyReport::eMonths, TransactionFilter::Date::Next7Days, MyMoneyReport::eDetailTop, 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, TransactionFilter::Date::Last12Months, MyMoneyReport::eDetailTotal, 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.push_back(MyMoneyReport( MyMoneyReport::eInstitution, MyMoneyReport::eQCnone, TransactionFilter::Date::YearToDate, MyMoneyReport::eDetailTop, i18n("Account Balances by Institution"), i18n("Default Report") )); list.push_back(MyMoneyReport( MyMoneyReport::eAccountType, MyMoneyReport::eQCnone, TransactionFilter::Date::YearToDate, MyMoneyReport::eDetailTop, 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, TransactionFilter::Date::YearToDate, MyMoneyReport::eDetailAll, 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, TransactionFilter::Date::YearToDate, MyMoneyReport::eDetailAll, i18n("Transactions by Category"), i18n("Default Report") )); list.push_back(MyMoneyReport( MyMoneyReport::ePayee, MyMoneyReport::eQCnumber | MyMoneyReport::eQCcategory | MyMoneyReport::eQCtag, TransactionFilter::Date::YearToDate, MyMoneyReport::eDetailAll, i18n("Transactions by Payee"), i18n("Default Report") )); list.push_back(MyMoneyReport( MyMoneyReport::eTag, MyMoneyReport::eQCnumber | MyMoneyReport::eQCcategory, TransactionFilter::Date::YearToDate, MyMoneyReport::eDetailAll, i18n("Transactions by Tag"), i18n("Default Report") )); list.push_back(MyMoneyReport( MyMoneyReport::eMonth, MyMoneyReport::eQCnumber | MyMoneyReport::eQCpayee | MyMoneyReport::eQCcategory | MyMoneyReport::eQCtag, TransactionFilter::Date::YearToDate, MyMoneyReport::eDetailAll, i18n("Transactions by Month"), i18n("Default Report") )); list.push_back(MyMoneyReport( MyMoneyReport::eWeek, MyMoneyReport::eQCnumber | MyMoneyReport::eQCpayee | MyMoneyReport::eQCcategory | MyMoneyReport::eQCtag, TransactionFilter::Date::YearToDate, MyMoneyReport::eDetailAll, i18n("Transactions by Week"), i18n("Default Report") )); list.push_back(MyMoneyReport( MyMoneyReport::eAccount, MyMoneyReport::eQCloan, TransactionFilter::Date::All, MyMoneyReport::eDetailAll, i18n("Loan Transactions"), i18n("Default Report") )); list.back().setLoansOnly(true); list.push_back(MyMoneyReport( MyMoneyReport::eAccountReconcile, MyMoneyReport::eQCnumber | MyMoneyReport::eQCpayee | MyMoneyReport::eQCcategory | MyMoneyReport::eQCbalance, TransactionFilter::Date::Last3Months, MyMoneyReport::eDetailAll, 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, TransactionFilter::Date::YearToDate, MyMoneyReport::eDetailAll, 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, TransactionFilter::Date::YearToDate, MyMoneyReport::eDetailAll, i18n("Investment Transactions"), i18n("Default Report") )); list.back().setInvestmentsOnly(true); list.push_back(MyMoneyReport( MyMoneyReport::eAccountByTopAccount, MyMoneyReport::eQCshares | MyMoneyReport::eQCprice, TransactionFilter::Date::YearToDate, MyMoneyReport::eDetailAll, i18n("Investment Holdings by Account"), i18n("Default Report") )); list.back().setInvestmentsOnly(true); list.push_back(MyMoneyReport( MyMoneyReport::eEquityType, MyMoneyReport::eQCshares | MyMoneyReport::eQCprice, TransactionFilter::Date::YearToDate, MyMoneyReport::eDetailAll, i18n("Investment Holdings by Type"), i18n("Default Report") )); list.back().setInvestmentsOnly(true); list.push_back(MyMoneyReport( MyMoneyReport::eAccountByTopAccount, MyMoneyReport::eQCperformance, TransactionFilter::Date::YearToDate, MyMoneyReport::eDetailAll, i18n("Investment Performance by Account"), i18n("Default Report") )); list.back().setInvestmentsOnly(true); list.push_back(MyMoneyReport( MyMoneyReport::eEquityType, MyMoneyReport::eQCperformance, TransactionFilter::Date::YearToDate, MyMoneyReport::eDetailAll, i18n("Investment Performance by Type"), i18n("Default Report") )); list.back().setInvestmentsOnly(true); list.push_back(MyMoneyReport( MyMoneyReport::eAccountByTopAccount, MyMoneyReport::eQCcapitalgain, TransactionFilter::Date::YearToDate, MyMoneyReport::eDetailAll, i18n("Investment Capital Gains by Account"), i18n("Default Report") )); list.back().setInvestmentsOnly(true); list.push_back(MyMoneyReport( MyMoneyReport::eEquityType, MyMoneyReport::eQCcapitalgain, TransactionFilter::Date::YearToDate, MyMoneyReport::eDetailAll, i18n("Investment Capital Gains by Type"), i18n("Default Report") )); list.back().setInvestmentsOnly(true); list.push_back(MyMoneyReport( MyMoneyReport::eAssetLiability, MyMoneyReport::eMonths, TransactionFilter::Date::Today, MyMoneyReport::eDetailAll, 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().setInvestmentsOnly(true); list.push_back(MyMoneyReport( MyMoneyReport::eAssetLiability, MyMoneyReport::eMonths, TransactionFilter::Date::Last12Months, MyMoneyReport::eDetailAll, 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().setColumnsAreDays(true); list.back().setInvestmentsOnly(true); list.push_back(MyMoneyReport( MyMoneyReport::eAssetLiability, MyMoneyReport::eMonths, TransactionFilter::Date::Last12Months, MyMoneyReport::eDetailAll, 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().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, TransactionFilter::Date::Last12Months, MyMoneyReport::eDetailAll, 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().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, TransactionFilter::Date::Last30Days, MyMoneyReport::eDetailAll, i18n("Investment Moving Average"), i18n("Default Report") )); list.back().setChartCHGridLines(false); list.back().setChartSVGridLines(false); list.back().setChartType(MyMoneyReport::eChartLine); 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, TransactionFilter::Date::Last30Days, MyMoneyReport::eDetailAll, 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().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, TransactionFilter::Date::YearToDate, MyMoneyReport::eDetailAll, i18n("Tax Transactions by Category"), i18n("Default Report") )); list.back().setTax(true); list.push_back(MyMoneyReport( MyMoneyReport::ePayee, MyMoneyReport::eQCnumber | MyMoneyReport::eQCcategory | MyMoneyReport::eQCaccount, TransactionFilter::Date::YearToDate, MyMoneyReport::eDetailAll, i18n("Tax Transactions by Payee"), i18n("Default Report") )); list.back().setTax(true); list.push_back(MyMoneyReport( MyMoneyReport::eCategory, MyMoneyReport::eQCnumber | MyMoneyReport::eQCpayee | MyMoneyReport::eQCaccount, TransactionFilter::Date::LastFiscalYear, MyMoneyReport::eDetailAll, 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, TransactionFilter::Date::LastFiscalYear, MyMoneyReport::eDetailAll, 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, TransactionFilter::Date::YearToDate, MyMoneyReport::eDetailAll, 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, TransactionFilter::Date::YearToMonth, MyMoneyReport::eDetailAll, 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, TransactionFilter::Date::CurrentMonth, MyMoneyReport::eDetailAll, i18n("Monthly Budgeted vs. Actual"), i18n("Default Report") )); list.back().setBudget("Any", true); list.push_back(MyMoneyReport( MyMoneyReport::eBudgetActual, MyMoneyReport::eMonths, TransactionFilter::Date::CurrentYear, MyMoneyReport::eDetailAll, 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, TransactionFilter::Date::CurrentMonth, MyMoneyReport::eDetailAll, i18n("Monthly Budget"), i18n("Default Report") )); list.back().setBudget("Any", false); list.push_back(MyMoneyReport( MyMoneyReport::eBudget, MyMoneyReport::eMonths, TransactionFilter::Date::CurrentYear, MyMoneyReport::eDetailAll, i18n("Yearly Budget"), i18n("Default Report") )); list.back().setBudget("Any", false); list.back().setShowingRowTotals(true); list.push_back(MyMoneyReport( MyMoneyReport::eBudgetActual, MyMoneyReport::eMonths, TransactionFilter::Date::CurrentYear, MyMoneyReport::eDetailGroup, 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); groups.push_back(list); } { ReportGroup list("Forecast", i18n("Forecast")); list.push_back(MyMoneyReport( MyMoneyReport::eAssetLiability, MyMoneyReport::eMonths, TransactionFilter::Date::Next12Months, MyMoneyReport::eDetailTop, i18n("Forecast By Month"), i18n("Default Report") )); list.back().setIncludingForecast(true); list.push_back(MyMoneyReport( MyMoneyReport::eAssetLiability, MyMoneyReport::eMonths, TransactionFilter::Date::NextQuarter, MyMoneyReport::eDetailTop, i18n("Forecast Next Quarter"), i18n("Default Report") )); list.back().setColumnsAreDays(true); list.back().setIncludingForecast(true); list.push_back(MyMoneyReport( MyMoneyReport::eExpenseIncome, MyMoneyReport::eMonths, TransactionFilter::Date::CurrentYear, MyMoneyReport::eDetailTop, i18n("Income and Expenses Forecast This Year"), i18n("Default Report") )); list.back().setIncludingForecast(true); list.push_back(MyMoneyReport( MyMoneyReport::eAssetLiability, MyMoneyReport::eMonths, TransactionFilter::Date::Next3Months, MyMoneyReport::eDetailTotal, 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); groups.push_back(list); } { ReportGroup list("Information", i18n("General Information")); list.push_back(MyMoneyReport( MyMoneyReport::eSchedule, MyMoneyReport::eMonths, TransactionFilter::Date::Next12Months, MyMoneyReport::eDetailAll, i18n("Schedule Information"), i18n("Default Report") )); list.back().setDetailLevel(MyMoneyReport::eDetailAll); list.push_back(MyMoneyReport( MyMoneyReport::eSchedule, MyMoneyReport::eMonths, TransactionFilter::Date::Next12Months, MyMoneyReport::eDetailAll, i18n("Schedule Summary Information"), i18n("Default Report") )); list.back().setDetailLevel(MyMoneyReport::eDetailTop); list.push_back(MyMoneyReport( MyMoneyReport::eAccountInfo, MyMoneyReport::eMonths, TransactionFilter::Date::Today, MyMoneyReport::eDetailAll, i18n("Account Information"), i18n("Default Report") )); list.back().setConvertCurrency(false); list.push_back(MyMoneyReport( MyMoneyReport::eAccountLoanInfo, MyMoneyReport::eMonths, TransactionFilter::Date::Today, MyMoneyReport::eDetailAll, 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/reportsview.h b/kmymoney/plugins/views/reports/reportsview.h index 50070416d..134abe57d 100644 --- a/kmymoney/plugins/views/reports/reportsview.h +++ b/kmymoney/plugins/views/reports/reportsview.h @@ -1,47 +1,47 @@ /*************************************************************************** reportsview.h ------------------- copyright : (C) 2018 by Łukasz Wojniłowicz email : lukasz.wojnilowicz@gmail.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. * * * ***************************************************************************/ #ifndef REPORTSVIEW_H #define REPORTSVIEW_H // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // QT Includes // Project Includes #include "kmymoneyplugin.h" class KReportsView; class ReportsView : public KMyMoneyPlugin::Plugin { Q_OBJECT public: explicit ReportsView(QObject *parent, const QVariantList &args); ~ReportsView() final; - void plug() final; - void unplug() final; + void plug() final override; + void unplug() final override; private: KReportsView* m_view; }; #endif diff --git a/kmymoney/plugins/views/reports/reporttabimpl.h b/kmymoney/plugins/views/reports/reporttabimpl.h index 5200a9a1f..67e5bcd11 100644 --- a/kmymoney/plugins/views/reports/reporttabimpl.h +++ b/kmymoney/plugins/views/reports/reporttabimpl.h @@ -1,147 +1,147 @@ /* 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. */ #ifndef REPORTTABIMPL_H #define REPORTTABIMPL_H #include #include class DateRangeDlg; namespace Ui { class ReportTabGeneral; class ReportTabRowColPivot; class ReportTabRowColQuery; class ReportTabChart; class ReportTabRange; class ReportTabCapitalGain; class ReportTabPerformance; } class ReportTabGeneral : public QWidget { Q_DISABLE_COPY(ReportTabGeneral) public: explicit ReportTabGeneral(QWidget *parent); ~ReportTabGeneral(); Ui::ReportTabGeneral* ui; }; class ReportTabRowColPivot : public QWidget { Q_DISABLE_COPY(ReportTabRowColPivot) public: explicit ReportTabRowColPivot(QWidget *parent); ~ReportTabRowColPivot(); Ui::ReportTabRowColPivot* ui; }; class ReportTabRowColQuery : public QWidget { Q_OBJECT Q_DISABLE_COPY(ReportTabRowColQuery) public: explicit ReportTabRowColQuery(QWidget *parent); ~ReportTabRowColQuery(); Ui::ReportTabRowColQuery* ui; private Q_SLOTS: void slotHideTransactionsChanged(bool checked); }; class ReportTabChart : public QWidget { Q_OBJECT Q_DISABLE_COPY(ReportTabChart) public: explicit ReportTabChart(QWidget *parent); ~ReportTabChart(); Ui::ReportTabChart* ui; private Q_SLOTS: void slotChartTypeChanged(int index); }; class ReportTabRange : public QWidget { Q_OBJECT Q_DISABLE_COPY(ReportTabRange) public: explicit ReportTabRange(QWidget *parent); ~ReportTabRange(); Ui::ReportTabRange* ui; DateRangeDlg *m_dateRange; void setRangeLogarythmic(bool set); private: enum EDimension { eRangeStart = 0, eRangeEnd, eMajorTick, eMinorTick}; private Q_SLOTS: void slotEditingFinished(EDimension dim); void slotEditingFinishedStart(); void slotEditingFinishedEnd(); void slotEditingFinishedMajor(); void slotEditingFinishedMinor(); void slotYLabelsPrecisionChanged(const int &value); void slotDataLockChanged(int index); }; class ReportTabCapitalGain : public QWidget { Q_OBJECT Q_DISABLE_COPY(ReportTabCapitalGain) public: explicit ReportTabCapitalGain(QWidget *parent); ~ReportTabCapitalGain(); Ui::ReportTabCapitalGain* ui; private Q_SLOTS: void slotInvestmentSumChanged(int index); }; class ReportTabPerformance : public QWidget { public: explicit ReportTabPerformance(QWidget *parent); ~ReportTabPerformance(); Ui::ReportTabPerformance* ui; }; class MyDoubleValidator : public QDoubleValidator { public: explicit MyDoubleValidator(int decimals, QObject * parent = 0); - QValidator::State validate(QString &s, int &i) const; + QValidator::State validate(QString &s, int &i) const final override; }; #endif /* REPORTTABIMPL_H */ diff --git a/kmymoney/plugins/views/reports/tocitem.h b/kmymoney/plugins/views/reports/tocitem.h index e05446395..c03b2c64b 100644 --- a/kmymoney/plugins/views/reports/tocitem.h +++ b/kmymoney/plugins/views/reports/tocitem.h @@ -1,103 +1,103 @@ /*************************************************************************** tocitem.h - description ------------------- begin : Sat Jul 03 2010 copyright : (C) Bernd Gonsior email : bernd.gonsior@googlemail.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. * * * ***************************************************************************/ #ifndef TOCITEM_H #define TOCITEM_H // ---------------------------------------------------------------------------- // QT Includes #include #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes /** * Base class for items in reports table of contents (TOC). * It provides the type of the item (reportgroup or report) * and an operator for sorting. */ class TocItem : public QTreeWidgetItem { public: /** Type of TOC-item */ enum ItemType { /** item represents a reportgroup */ GROUP = QTreeWidgetItem::UserType + 10, /** item represents a report */ REPORT = QTreeWidgetItem::UserType + 20 } type; /** Constructor. * * @param parent pointer to the parent QWidget * @param columns List of texts in columns */ TocItem(QTreeWidget* parent, QStringList columns); /** Constructor. * * @param parent pointer to the parent QWidget * @param columns List of texts in columns */ TocItem(QTreeWidgetItem* parent, QStringList columns); /** Indicates, whether the item represents a report or a reportgroup. * * @retval true the item represents a report * @retval false the item represents a reportgroup */ bool isReport(); private: /** Operator used to sort TocItems. * TOC has to be sorted in a quite special way: * @li @c reportgroups numerically by group-number * @li @c reports alphabetically by text of column 0 * * Because the operator is defined @c const it is not possible, * to use a property of a class derived from @c QTreeWidgetItem. * So we use the @c QVariant data of @c QTreeWidgetItem in following way: * * QVariant contains a QStringList at position 0 with * role @c Qt::UserRole. * The first entry of this list is the item-type (report or * reportgroup). The second entry is the item-type-specific sort-key, for * reports simply the text of column 0, for reportgroups the groupnumber as * string with leading zeros. * * Examples: *
    * reportgroup:
    *  list.at(0) = QString::number(TocItem::GROUP);
    *  list.at(1) = "001"
    *
    * report:
    *  list.at(0) = QString::number(TocItem::REPORT);
    *  list.at(1) = ""
    * 
*/ - bool operator<(const QTreeWidgetItem &other)const; + bool operator<(const QTreeWidgetItem &other) const final override; }; #endif diff --git a/kmymoney/reports/kbalanceaxis.h b/kmymoney/reports/kbalanceaxis.h index e022e1bf4..8f48a0bcf 100644 --- a/kmymoney/reports/kbalanceaxis.h +++ b/kmymoney/reports/kbalanceaxis.h @@ -1,38 +1,38 @@ /*************************************************************************** kbalanceaxis.h - description ------------------- begin : Sun Jul 18 2010 copyright : (C) 2010 by Alvaro Soliverez email : asoliverez@kde.org ***************************************************************************/ /*************************************************************************** * * * 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 KBALANCEAXIS_H #define KBALANCEAXIS_H #include #include namespace KChart { class AbstractCartesianDiagram; } class KBalanceAxis : public KChart::CartesianAxis { Q_OBJECT public: KBalanceAxis(); explicit KBalanceAxis(KChart::AbstractCartesianDiagram* parent); - const QString customizedLabel(const QString& label) const; + const QString customizedLabel(const QString& label) const final override; }; #endif diff --git a/kmymoney/reports/kreportchartview.cpp b/kmymoney/reports/kreportchartview.cpp index 0f2ef2e37..c7d162679 100644 --- a/kmymoney/reports/kreportchartview.cpp +++ b/kmymoney/reports/kreportchartview.cpp @@ -1,745 +1,745 @@ /*************************************************************************** kreportchartview.cpp ------------------- begin : Sun Aug 14 2005 copyright : (C) 2004-2005 by Ace Jones email : (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 "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: { 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: { 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: { KChart::BarDiagram* diagram = new KChart::BarDiagram(this, cartesianPlane); cartesianPlane->replaceDiagram(diagram); diagram->addAxis(xAxis); diagram->addAxis(yAxis); break; } case MyMoneyReport::eChartStackedBar: { 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:{ 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: { KChart::PieDiagram* diagram = new KChart::PieDiagram(this, polarPlane); polarPlane->replaceDiagram(diagram); setSeriesTotals(true); break; } case MyMoneyReport::eChartRing: { 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: { // 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: { // 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: { // 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: { // 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: 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) { 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 data; - data.insert(Qt::DisplayRole, QVariant(value)); + QMap cellMap; + cellMap.insert(Qt::DisplayRole, QVariant(value)); if (!tip.isEmpty()) - data.insert(Qt::ToolTipRole, QVariant(tip)); + cellMap.insert(Qt::ToolTipRole, QVariant(tip)); const QModelIndex index = m_model.index(row, column); - m_model.setItemData(index, data); + 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.h b/kmymoney/reports/listtable.h index e2707ef91..b5300ae1a 100644 --- a/kmymoney/reports/listtable.h +++ b/kmymoney/reports/listtable.h @@ -1,155 +1,155 @@ /*************************************************************************** listtable.h ------------------- begin : Sat 28 jun 2008 copyright : (C) 2004-2005 by Ace Jones 2008 by 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. * * * ***************************************************************************/ #ifndef LISTTABLE_H #define LISTTABLE_H // ---------------------------------------------------------------------------- // QT Includes #include #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes #include "reporttable.h" class MyMoneyReport; namespace reports { class ReportAccount; /** * Calculates a query of information about the transaction database. * * This is a middle-layer class, between the implementing classes and the engine. The * MyMoneyReport class holds only the CONFIGURATION parameters. This * class has some common methods used by querytable and objectinfo classes * * @author Alvaro Soliverez * * @short **/ class ListTable : public ReportTable { public: explicit ListTable(const MyMoneyReport&); - QString renderHTML() const; - QString renderCSV() const; - void drawChart(KReportChartView&) const {} - void dump(const QString& file, const QString& context = QString()) const; + QString renderHTML() const final override; + QString renderCSV() const final override; + void drawChart(KReportChartView&) const final override {} + void dump(const QString& file, const QString& context = QString()) const final override; void init(); public: enum cellTypeE /*{*/ /*Money*/ {ctValue, ctNetInvValue, ctMarketValue, ctPrice, ctLastPrice, ctBuyPrice, ctBuys, ctSells, ctBuysST, ctSellsST, ctBuysLT, ctSellsLT, ctCapitalGain, ctCapitalGainST,ctCapitalGainLT, ctCashIncome, ctReinvestIncome, ctFees, ctInterest, ctStartingBalance, ctEndingBalance, ctBalance, ctCurrentBalance, ctBalanceWarning, ctMaxBalanceLimit, ctOpeningBalance, ctCreditWarning, ctMaxCreditLimit, ctLoanAmount, ctPeriodicPayment, ctFinalPayment, ctPayment, /*Shares*/ ctShares, /*Percent*/ ctReturn, ctReturnInvestment, ctInterestRate, ctPercentageGain, /*Date*/ ctPostDate, ctEntryDate, ctNextDueDate, ctOpeningDate, ctNextInterestChange, ctMonth, ctWeek, ctReconcileDate, /*Misc*/ ctCurrency, ctCurrencyName, ctCommodity, ctID, ctRank, ctSplit, ctMemo, ctAccount, ctAccountID, ctTopAccount, ctInvestAccount, ctInstitution, ctCategory, ctTopCategory, ctCategoryType, ctNumber, ctReconcileFlag, ctAction, ctTag, ctPayee, ctEquityType, ctType, ctName, ctDepth, ctRowsCount, ctTax, ctFavorite, ctDescription, ctOccurrence, ctPaymentType }; /** * Contains a single row in the table. * * Each column is a key/value pair, both strings. This class is just * a QMap with the added ability to specify which columns you'd like to * use as a sort key when you qHeapSort a list of these TableRows */ class TableRow: public QMap { public: bool operator< (const TableRow&) const; bool operator<= (const TableRow&) const; bool operator> (const TableRow&) const; bool operator== (const TableRow&) const; static void setSortCriteria(const QVector& _criteria) { m_sortCriteria = _criteria; } private: static QVector m_sortCriteria; }; const QList& rows() { return m_rows; } protected: void render(QString&, QString&) const; /** * If not in expert mode, include all subaccounts for each selected * investment account. * For investment-only reports, it will also exclude the subaccounts * that have a zero balance */ void includeInvestmentSubAccounts(); QList m_rows; QList m_group; /** * Comma-separated list of columns to place BEFORE the subtotal column */ QList m_columns; /** * Name of the subtotal column */ QList m_subtotal; /** * Comma-separated list of columns to place AFTER the subtotal column */ QList m_postcolumns; private: enum cellGroupE { cgMoney, cgShares, cgPercent, cgDate, cgPrice, cgMisc }; static cellGroupE cellGroup(const cellTypeE cellType); static QString tableHeader(const cellTypeE cellType); }; } #endif diff --git a/kmymoney/reports/pivottable.cpp b/kmymoney/reports/pivottable.cpp index 421b9beba..ec6af81c1 100644 --- a/kmymoney/reports/pivottable.cpp +++ b/kmymoney/reports/pivottable.cpp @@ -1,2355 +1,2355 @@ /*************************************************************************** pivottable.cpp ------------------- begin : Mon May 17 2004 copyright : (C) 2004-2005 by Ace Jones email : Thomas Baumgart 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. * * * ***************************************************************************/ #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) { 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 thrown in %s(%ld)", qPrintable(e.what()), qPrintable(e.file()), e.line()); throw e; } 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()); //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 split, tx.splits()) { - if (split.isAmortizationSplit() && split.accountId() == splitAccount.id()) - loanBalances[splitAccount.id()] = loanBalances[splitAccount.id()] + split.shares(); + 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("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("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("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("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("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("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("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("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("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: // 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: // 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) { 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()))) { assignCell(outergroup, splitAccount, column, value, true /*budget*/); } budgetDate = budgetDate.addMonths(1); } } } break; case MyMoneyBudget::AccountGroup::eMonthByMonth: // 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: { 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()))) { //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("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("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("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("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("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("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 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("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("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("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("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("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 (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) { - auto column = 0; + 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; - auto column = 0; + 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)) { // 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) { - auto column = 0; + 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()); - auto column = 0; + 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 += ""; - auto column = 0; + 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) { // // 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) { // // 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; - auto column = 0; + 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)) { // 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(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) { - auto column = 0; + 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); - auto column = 0; + 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 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) { 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: { averageStart = QDate(columnDate(column).year(), 1, 1); break; } case MyMoneyReport::eBiMonths: { averageStart = QDate(columnDate(column).year(), columnDate(column).month(), 1).addMonths(-1); break; } case MyMoneyReport::eQuarters: { averageStart = QDate(columnDate(column).year(), columnDate(column).month(), 1).addMonths(-1); break; } case MyMoneyReport::eMonths: { averageStart = QDate(columnDate(column).year(), columnDate(column).month(), 1); break; } case MyMoneyReport::eWeeks: { 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/pivottable.h b/kmymoney/reports/pivottable.h index 595da3738..15f5887a6 100644 --- a/kmymoney/reports/pivottable.h +++ b/kmymoney/reports/pivottable.h @@ -1,372 +1,372 @@ /*************************************************************************** pivottable.h ------------------- begin : Sat May 22 2004 copyright : (C) 2004-2005 by Ace Jones Thomas Baumgart 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. * * * ***************************************************************************/ #ifndef PIVOTTABLE_H #define PIVOTTABLE_H // ---------------------------------------------------------------------------- // QT Includes #include #include #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes #include "reporttable.h" #include "pivotgrid.h" #include "reportaccount.h" class MyMoneyReport; namespace reports { class KReportChartView; } namespace reports { /** * Calculates a 'pivot table' of information about the transaction database. * Based on pivot tables in MS Excel, and implemented as 'Data Pilot' in * OpenOffice.Org Calc. * * | Month,etc * -------------+------------ * Expense Type | Sum(Value) * Category | * * This is a middle-layer class, between the UI and the engine. The * MyMoneyReport class holds only the CONFIGURATION parameters. This * class actually does the work of retrieving the data from the engine * and formatting it for the user. * * @author Ace Jones * * @short **/ class PivotTable : public ReportTable { KMM_MYMONEY_UNIT_TESTABLE public: /** * Create a Pivot table style report * * @param _report The configuration parameters for this report */ explicit PivotTable(const MyMoneyReport& _report); /** * virtual Destructur */ virtual ~PivotTable() {} /** * Render the report body to an HTML stream. * * @return QString HTML string representing the report body */ - QString renderHTML() const; + QString renderHTML() const final override; /** * Render the report to a comma-separated-values stream. * * @return QString CSV string representing the report */ - QString renderCSV() const; + QString renderCSV() const final override; /** * Render the report to a graphical chart * * @param view The KReportChartView into which to draw the chart. */ - void drawChart(KReportChartView& view) const; + void drawChart(KReportChartView& view) const final override; /** * Dump the report's HTML to a file * * @param file The filename to dump into * @param context unused, but provided for interface compatibility */ - void dump(const QString& file, const QString& context = QString()) const; + void dump(const QString& file, const QString& context = QString()) const final override; /** * Returns the grid generated by the report * */ PivotGrid grid() { return m_grid; } protected: void init(); // used for debugging the constructor private: PivotGrid m_grid; QStringList m_columnHeadings; int m_numColumns; QDate m_beginDate; QDate m_endDate; bool m_runningSumsCalculated; int m_startColumn; /** * For budget-vs-actual reports only, maps each account to the account which holds * the budget for it. If an account is not contained in this map, it is not included * in the budget. */ QMap m_budgetMap; /** * This list contains the types of PivotGridRows that are going to be shown in the report */ QList m_rowTypeList; /** * This list contains the i18n headers for the column types */ QStringList m_columnTypeHeaderList; /** * This method returns the formatted value of @a amount with * a possible @a currencySymbol added and @a prec fractional digits. * @a currencySymbol defaults to be empty and @a prec defaults to 2. * * If @a amount is negative the formatted value is enclosed in an * HTML font tag to modify the color to reflect the user settings for * negtive numbers. * * Example: 1.23 is returned as '1.23' whereas -1.23 is returned as * @verbatim -1.23@endverbatim * with $red, $green and $blue being the actual value for the * chosen color. */ QString coloredAmount(const MyMoneyMoney& amount, const QString& currencySymbol, int prec) const; protected: /** * Creates a row in the grid if it doesn't already exist * * Downsteam assignment functions will assume that this row already * exists, so this function creates a row of the needed length populated * with zeros. * * @param outergroup The outer row group * @param row The row itself * @param recursive Whether to also recursively create rows for our parent accounts */ void createRow(const QString& outergroup, const ReportAccount& row, bool recursive); /** * Assigns a value into the grid * * Adds the given value to the value which already exists at the specified grid position * * @param outergroup The outer row group * @param row The row itself * @param column The column * @param value The value to be added in * @param budget Whether this is a budget value (@p true) or an actual * value (@p false). Defaults to @p false. * @param stockSplit Whether this is a stock split (@p true) or an actual * value (@p false). Defaults to @p false. */ inline void assignCell(const QString& outergroup, const ReportAccount& row, int column, MyMoneyMoney value, bool budget = false, bool stockSplit = false); /** * Create a row for each included account. This is used when * the config parameter isIncludingUnusedAccount() is true */ void createAccountRows(); /** * Record the opening balances of all qualifying accounts into the grid. * * For accounts opened before the report period, places the balance into the '0' column. * For those opened during the report period, places the balance into the appropriate column * for the month when it was opened. */ void calculateOpeningBalances(); /** * Calculate budget mapping * * For budget-vs-actual reports, this creates a mapping between each account * in the user's hierarchy and the account where the budget is held for it. * This is needed because the user can budget on a given account for that * account and all its descendants. Also if NO budget is placed on the * account or any of its parents, the account is not included in the map. */ void calculateBudgetMapping(); /** * Calculate the running sums. * * After calling this method, each cell of the report will contain the running sum of all * the cells in its row in this and earlier columns. * * For example, consider a row with these values: * 01 02 03 04 05 06 07 08 09 10 * * After calling this function, the row will look like this: * 01 03 06 10 15 21 28 36 45 55 */ void calculateRunningSums(); void calculateRunningSums(PivotInnerGroup::iterator& it_row); /** * This method calculates the difference between a @a budgeted and an @a * actual amount. The calculation is based on the type of the * @a repAccount. The difference value is calculated as follows: * * If @a repAccount is of type eMyMoney::Account::Type::Income * * @code * diff = actual - budgeted * @endcode * * If @a repAccount is of type eMyMoney::Account::Type::Expense * * @code * diff = budgeted - actual * @endcode * * In all other cases, 0 is returned. */ void calculateBudgetDiff(); /** * This method calculates forecast for a report */ void calculateForecast(); /** * This method inserts units to be used to display prices */ void fillBasePriceUnit(ERowType rowType); /** * This method collects the first date for which there is a price for every security */ QMap securityFirstPrice(); /** * This method calculates moving average for a report */ void calculateMovingAverage(); /** * Calculate the row and column totals * * This function will set the m_total members of all the TGrid objects. Be sure the values are * all converted to the base currency first!! * */ void calculateTotals(); /** * Convert each value in the grid to the base currency * */ void convertToBaseCurrency(); /** * Convert each value in the grid to the account/category's deep currency * * See AccountDescriptor::deepCurrencyPrice() for a description of 'deep' currency * */ void convertToDeepCurrency(); /** * Turn month-long columns into larger time periods if needed * * For example, consider a row with these values: * 01 02 03 04 05 06 07 08 09 10 * * If the column pitch is 3 (i.e. quarterly), after calling this function, * the row will look like this: * 06 15 26 10 */ void collapseColumns(); /** * Determine the proper column headings based on the time periods covered by each column * */ void calculateColumnHeadings(); /** * Helper methods for collapseColumns * */ void accumulateColumn(int destcolumn, int sourcecolumn); void clearColumn(int column); /** * Calculate the column of a given date. This is the absolute column in a * hypothetical report that covers all of known time. In reality an actual * report will be a subset of that. * * @param _date The date */ int columnValue(const QDate& _date) const; /** * Calculate the date of the last day covered by a given column. * * @param column The column */ QDate columnDate(int column) const; /** * Returns the balance of a given cell. Throws an exception once calculateRunningSums() has been run. */ MyMoneyMoney cellBalance(const QString& outergroup, const ReportAccount& _row, int column, bool budget); /** * Draws a PivotGridRowSet in a chart for the given ERowType */ unsigned drawChartRowSet(int rowNum, const bool seriesTotals, const bool accountSeries, KReportChartView& chartView, const PivotGridRowSet& rowSet, const ERowType rowType) const; /** * Loads m_rowTypeList with the list of PivotGridRow types that the reporttable * should show */ void loadRowTypeList(); /** * If not in expert mode, include all subaccounts for each selected * investment account */ void includeInvestmentSubAccounts(); /** * Returns the column which holds the current date * Returns -1 if the current date is not within range */ int currentDateColumn(); }; } #endif // PIVOTTABLE_H diff --git a/kmymoney/reports/querytable.cpp b/kmymoney/reports/querytable.cpp index e7cc2e08f..3f67313b6 100644 --- a/kmymoney/reports/querytable.cpp +++ b/kmymoney/reports/querytable.cpp @@ -1,2168 +1,2168 @@ /*************************************************************************** querytable.cpp ------------------- begin : Fri Jul 23 2004 copyright : (C) 2004-2005 by Ace Jones (C) 2007 Sascha Pfau (C) 2017 Łukasz Wojniłowicz ***************************************************************************/ /**************************************************************************** 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. *****************************************************************************/ /*************************************************************************** * * * 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 "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: constructAccountTable(); m_columns << ctAccount; break; case MyMoneyReport::eAccount: constructTransactionTable(); m_columns << ctAccountID << ctPostDate; break; case MyMoneyReport::ePayee: case MyMoneyReport::eTag: case MyMoneyReport::eMonth: case MyMoneyReport::eWeek: constructTransactionTable(); m_columns << ctPostDate << ctAccount; break; case MyMoneyReport::eCashFlow: 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: m_group << ctCategoryType << ctTopCategory << ctCategory; break; case MyMoneyReport::eCategory: m_group << ctCategoryType << ctTopCategory << ctCategory; break; case MyMoneyReport::eTopCategory: m_group << ctCategoryType << ctTopCategory; break; case MyMoneyReport::eTopAccount: m_group << ctTopAccount << ctAccount; break; case MyMoneyReport::eAccount: m_group << ctAccount; break; case MyMoneyReport::eAccountReconcile: m_group << ctAccount << ctReconcileFlag; break; case MyMoneyReport::ePayee: m_group << ctPayee; break; case MyMoneyReport::eTag: m_group << ctTag; break; case MyMoneyReport::eMonth: m_group << ctMonth; break; case MyMoneyReport::eWeek: m_group << ctWeek; break; case MyMoneyReport::eAccountByTopAccount: m_group << ctTopAccount; break; case MyMoneyReport::eEquityType: m_group << ctEquityType; break; case MyMoneyReport::eAccountType: m_group << ctType; break; case MyMoneyReport::eInstitution: m_group << ctInstitution << ctTopAccount; break; default: throw MYMONEYEXCEPTION("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: m_columns << ctAccount; break; default: m_columns << ctPostDate; } unsigned qc = m_config.queryColumns(); if (qc & MyMoneyReport::eQCnumber) m_columns << ctNumber; if (qc & MyMoneyReport::eQCpayee) m_columns << ctPayee; if (qc & MyMoneyReport::eQCtag) m_columns << ctTag; if (qc & MyMoneyReport::eQCcategory) m_columns << ctCategory; if (qc & MyMoneyReport::eQCaccount) m_columns << ctAccount; if (qc & MyMoneyReport::eQCreconciled) m_columns << ctReconcileFlag; if (qc & MyMoneyReport::eQCmemo) m_columns << ctMemo; if (qc & MyMoneyReport::eQCaction) m_columns << ctAction; if (qc & MyMoneyReport::eQCshares) m_columns << ctShares; if (qc & MyMoneyReport::eQCprice) m_columns << ctPrice; if (qc & MyMoneyReport::eQCperformance) { m_subtotal.clear(); switch (m_config.investmentSum()) { case MyMoneyReport::eSumOwnedAndSold: m_columns << ctBuys << ctSells << ctReinvestIncome << ctCashIncome << ctEndingBalance << ctReturn << ctReturnInvestment; m_subtotal << ctBuys << ctSells << ctReinvestIncome << ctCashIncome << ctEndingBalance << ctReturn << ctReturnInvestment; break; case MyMoneyReport::eSumOwned: m_columns << ctBuys << ctReinvestIncome << ctMarketValue << ctReturn << ctReturnInvestment; m_subtotal << ctBuys << ctReinvestIncome << ctMarketValue << ctReturn << ctReturnInvestment; break; case MyMoneyReport::eSumSold: m_columns << ctBuys << ctSells << ctCashIncome << ctReturn << ctReturnInvestment; m_subtotal << ctBuys << ctSells << ctCashIncome << ctReturn << ctReturnInvestment; break; case MyMoneyReport::eSumPeriod: 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) { m_subtotal.clear(); switch (m_config.investmentSum()) { case MyMoneyReport::eSumOwned: m_columns << ctShares << ctBuyPrice << ctLastPrice << ctBuys << ctMarketValue << ctPercentageGain << ctCapitalGain; m_subtotal << ctShares << ctBuyPrice << ctLastPrice << ctBuys << ctMarketValue << ctPercentageGain << ctCapitalGain; break; case MyMoneyReport::eSumSold: 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) { m_columns << ctPayment << ctInterest << ctFees; m_postcolumns << ctBalance; } if (qc & MyMoneyReport::eQCbalance) 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 } - QString currencyID = currencyGrp.key(); + 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 } - QString currencyID = currencyGrp.key(); + 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: use_summary = false; use_transfers = false; hide_details = false; break; case MyMoneyReport::ePayee: use_summary = false; use_transfers = false; hide_details = (m_config.detailLevel() == MyMoneyReport::eDetailNone); break; case MyMoneyReport::eTag: use_summary = false; use_transfers = false; hide_details = (m_config.detailLevel() == MyMoneyReport::eDetailNone); tag_special_case = true; break; default: use_summary = true; use_transfers = true; hide_details = (m_config.detailLevel() == MyMoneyReport::eDetailNone); 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) { 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: 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: break; // case MyMoneyReport::eCategory: // 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) { // 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()))) ) && 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: 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: 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: 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: 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: { 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: 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: { 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: 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()) { 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) { 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: break; // case MyMoneyReport::eCategory: // 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/views/kaccountsview_p.h b/kmymoney/views/kaccountsview_p.h index 730009f83..988e96953 100644 --- a/kmymoney/views/kaccountsview_p.h +++ b/kmymoney/views/kaccountsview_p.h @@ -1,323 +1,323 @@ /*************************************************************************** kaccountsview.cpp ------------------- copyright : (C) 2007 by 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. * * * ***************************************************************************/ #ifndef KACCOUNTSVIEW_P_H #define KACCOUNTSVIEW_P_H #include "kaccountsview.h" // ---------------------------------------------------------------------------- // QT Includes #include #include // ---------------------------------------------------------------------------- // KDE Includes #include // ---------------------------------------------------------------------------- // Project Includes #include "ui_kaccountsview.h" #include "kmymoneyaccountsviewbase_p.h" #include "mymoneyexception.h" #include "mymoneysplit.h" #include "mymoneyschedule.h" #include "mymoneytransaction.h" #include "knewaccountdlg.h" #include "keditloanwizard.h" #include "mymoneyaccount.h" #include "mymoneymoney.h" #include "mymoneyfile.h" #include "accountsviewproxymodel.h" #include "kmymoneyplugin.h" #include "icons.h" #include "mymoneyenums.h" using namespace Icons; class KAccountsViewPrivate : public KMyMoneyAccountsViewBasePrivate { Q_DECLARE_PUBLIC(KAccountsView) public: explicit KAccountsViewPrivate(KAccountsView *qq) : q_ptr(qq), ui(new Ui::KAccountsView), m_haveUnusedCategories(false), m_onlinePlugins(nullptr) { } ~KAccountsViewPrivate() { delete ui; } void init() { Q_Q(KAccountsView); ui->setupUi(q); m_accountTree = &ui->m_accountTree; // setup icons for collapse and expand button ui->m_collapseButton->setIcon(Icons::get(Icon::ListCollapse)); ui->m_expandButton->setIcon(Icons::get(Icon::ListExpand)); m_proxyModel = ui->m_accountTree->init(View::Accounts); q->connect(m_proxyModel, &AccountsProxyModel::unusedIncomeExpenseAccountHidden, q, &KAccountsView::slotUnusedIncomeExpenseAccountHidden); q->connect(ui->m_searchWidget, &QLineEdit::textChanged, m_proxyModel, &QSortFilterProxyModel::setFilterFixedString); q->connect(ui->m_accountTree, &KMyMoneyAccountTreeView::selectByObject, q, &KAccountsView::selectByObject); q->connect(ui->m_accountTree, &KMyMoneyAccountTreeView::selectByVariant, q, &KAccountsView::selectByVariant); q->connect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, q, &KAccountsView::refresh); } void editLoan() { Q_Q(KAccountsView); if (m_currentAccount.id().isEmpty()) return; const auto file = MyMoneyFile::instance(); if (file->isStandardAccount(m_currentAccount.id())) return; QPointer wizard = new KEditLoanWizard(m_currentAccount); q->connect(wizard, &KEditLoanWizard::newCategory, q, &KAccountsView::slotNewCategory); q->connect(wizard, &KEditLoanWizard::createPayee, q, &KAccountsView::slotNewPayee); if (wizard->exec() == QDialog::Accepted && wizard != 0) { MyMoneySchedule sch; try { - MyMoneySchedule sch = file->schedule(m_currentAccount.value("schedule").toLatin1()); + sch = file->schedule(m_currentAccount.value("schedule").toLatin1()); } catch (const MyMoneyException &) { qDebug() << "schedule" << m_currentAccount.value("schedule").toLatin1() << "not found"; } if (!(m_currentAccount == wizard->account()) || !(sch == wizard->schedule())) { MyMoneyFileTransaction ft; try { file->modifyAccount(wizard->account()); if (!sch.id().isEmpty()) { sch = wizard->schedule(); } try { file->schedule(sch.id()); file->modifySchedule(sch); ft.commit(); } catch (const MyMoneyException &) { try { if(sch.transaction().splitCount() >= 2) { file->addSchedule(sch); } ft.commit(); } catch (const MyMoneyException &e) { qDebug("Cannot add schedule: '%s'", qPrintable(e.what())); } } } catch (const MyMoneyException &e) { qDebug("Unable to modify account %s: '%s'", qPrintable(m_currentAccount.name()), qPrintable(e.what())); } } } delete wizard; } void editAccount() { if (m_currentAccount.id().isEmpty()) return; const auto file = MyMoneyFile::instance(); if (file->isStandardAccount(m_currentAccount.id())) return; // set a status message so that the application can't be closed until the editing is done // slotStatusMsg(caption); auto tid = file->openingBalanceTransaction(m_currentAccount); MyMoneyTransaction t; MyMoneySplit s0, s1; QPointer dlg = new KNewAccountDlg(m_currentAccount, true, false, 0, i18n("Edit account '%1'", m_currentAccount.name())); if (!tid.isEmpty()) { try { t = file->transaction(tid); s0 = t.splitByAccount(m_currentAccount.id()); s1 = t.splitByAccount(m_currentAccount.id(), false); dlg->setOpeningBalance(s0.shares()); if (m_currentAccount.accountGroup() == eMyMoney::Account::Type::Liability) { dlg->setOpeningBalance(-s0.shares()); } } catch (const MyMoneyException &e) { qDebug() << "Error retrieving opening balance transaction " << tid << ": " << e.what() << "\n"; tid.clear(); } } // check for online modules QMap::const_iterator it_plugin; if (m_onlinePlugins) { it_plugin = m_onlinePlugins->constEnd(); const auto& kvp = m_currentAccount.onlineBankingSettings(); if (!kvp["provider"].isEmpty()) { // if we have an online provider for this account, we need to check // that we have the corresponding plugin. If that exists, we ask it // to provide an additional tab for the account editor. it_plugin = m_onlinePlugins->constFind(kvp["provider"].toLower()); if (it_plugin != m_onlinePlugins->constEnd()) { QString name; auto w = (*it_plugin)->accountConfigTab(m_currentAccount, name); dlg->addTab(w, name); } } } if (dlg != 0 && dlg->exec() == QDialog::Accepted) { try { MyMoneyFileTransaction ft; auto account = dlg->account(); auto parent = dlg->parentAccount(); if (m_onlinePlugins && it_plugin != m_onlinePlugins->constEnd()) { account.setOnlineBankingSettings((*it_plugin)->onlineBankingSettings(account.onlineBankingSettings())); } auto bal = dlg->openingBalance(); if (m_currentAccount.accountGroup() == eMyMoney::Account::Type::Liability) { bal = -bal; } // we need to modify first, as reparent would override all other changes file->modifyAccount(account); if (account.parentAccountId() != parent.id()) { file->reparentAccount(account, parent); } if (!tid.isEmpty() && dlg->openingBalance().isZero()) { file->removeTransaction(t); } else if (!tid.isEmpty() && !dlg->openingBalance().isZero()) { s0.setShares(bal); s0.setValue(bal); t.modifySplit(s0); s1.setShares(-bal); s1.setValue(-bal); t.modifySplit(s1); t.setPostDate(account.openingDate()); file->modifyTransaction(t); } else if (tid.isEmpty() && !dlg->openingBalance().isZero()) { file->createOpeningBalanceTransaction(m_currentAccount, bal); } ft.commit(); // reload the account object as it might have changed in the meantime // slotSelectAccount(file->account(account.id())); } catch (const MyMoneyException &e) { Q_Q(KAccountsView); KMessageBox::error(q, i18n("Unable to modify account '%1'. Cause: %2", m_currentAccount.name(), e.what())); } } delete dlg; // ready(); } enum CanCloseAccountCodeE { AccountCanClose = 0, // can close the account AccountBalanceNonZero, // balance is non zero AccountChildrenOpen, // account has open children account AccountScheduleReference // account is referenced in a schedule }; /** * This method checks, if an account can be closed or not. An account * can be closed if: * * - the balance is zero and * - all children are already closed and * - there is no unfinished schedule referencing the account * * @param acc reference to MyMoneyAccount object in question * @retval true account can be closed * @retval false account cannot be closed */ CanCloseAccountCodeE canCloseAccount(const MyMoneyAccount& acc) { // balance must be zero if (!acc.balance().isZero()) return AccountBalanceNonZero; // all children must be already closed foreach (const auto sAccount, acc.accountList()) { if (!MyMoneyFile::instance()->account(sAccount).isClosed()) { return AccountChildrenOpen; } } // there must be no unfinished schedule referencing the account QList list = MyMoneyFile::instance()->scheduleList(); QList::const_iterator it_l; for (it_l = list.constBegin(); it_l != list.constEnd(); ++it_l) { if ((*it_l).isFinished()) continue; if ((*it_l).hasReferenceTo(acc.id())) return AccountScheduleReference; } return AccountCanClose; } /** * This method checks if an account can be closed and enables/disables * the close account action * If disabled, it sets a tooltip explaning why it cannot be closed * @brief enableCloseAccountAction * @param acc reference to MyMoneyAccount object in question */ void hintCloseAccountAction(const MyMoneyAccount& acc, QAction* a) { switch (canCloseAccount(acc)) { case AccountCanClose: a->setToolTip(QString()); return; case AccountBalanceNonZero: a->setToolTip(i18n("The balance of the account must be zero before the account can be closed")); return; case AccountChildrenOpen: a->setToolTip(i18n("All subaccounts must be closed before the account can be closed")); return; case AccountScheduleReference: a->setToolTip(i18n("This account is still included in an active schedule")); return; } } KAccountsView *q_ptr; Ui::KAccountsView *ui; bool m_haveUnusedCategories; MyMoneyAccount m_currentAccount; QMap* m_onlinePlugins; }; #endif diff --git a/kmymoney/views/kgloballedgerview.cpp b/kmymoney/views/kgloballedgerview.cpp index ececfa424..6c8b95b09 100644 --- a/kmymoney/views/kgloballedgerview.cpp +++ b/kmymoney/views/kgloballedgerview.cpp @@ -1,2073 +1,2071 @@ /*************************************************************************** kgloballedgerview.cpp - description ------------------- begin : Wed Jul 26 2006 copyright : (C) 2006 by Thomas Baumgart email : Thomas Baumgart (C) 2017 by Łukasz Wojniłowicz ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "kgloballedgerview_p.h" #include // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "mymoneyaccount.h" #include "mymoneyfile.h" #include "kmymoneyaccountcombo.h" #include "kmymoneypayeecombo.h" #include "keditscheduledlg.h" #include "kendingbalancedlg.h" #include "register.h" #include "transactioneditor.h" #include "selectedtransactions.h" #include "kmymoneysettings.h" #include "registersearchline.h" #include "kfindtransactiondlg.h" #include "accountsmodel.h" #include "models.h" #include "mymoneyschedule.h" #include "mymoneysecurity.h" #include "mymoneytransaction.h" #include "mymoneytransactionfilter.h" #include "mymoneysplit.h" #include "transaction.h" #include "transactionform.h" #include "widgetenums.h" #include "mymoneyenums.h" #include "menuenums.h" using namespace eMenu; QDate KGlobalLedgerViewPrivate::m_lastPostDate; KGlobalLedgerView::KGlobalLedgerView(QWidget *parent) : KMyMoneyViewBase(*new KGlobalLedgerViewPrivate(this), parent) { typedef void(KGlobalLedgerView::*KGlobalLedgerViewFunc)(); const QHash actionConnections { {Action::NewTransaction, &KGlobalLedgerView::slotNewTransaction}, {Action::EditTransaction, &KGlobalLedgerView::slotEditTransaction}, {Action::DeleteTransaction, &KGlobalLedgerView::slotDeleteTransaction}, {Action::DuplicateTransaction, &KGlobalLedgerView::slotDuplicateTransaction}, {Action::EnterTransaction, &KGlobalLedgerView::slotEnterTransaction}, {Action::AcceptTransaction, &KGlobalLedgerView::slotAcceptTransaction}, {Action::CancelTransaction, &KGlobalLedgerView::slotCancelTransaction}, {Action::EditSplits, &KGlobalLedgerView::slotEditSplits}, {Action::CopySplits, &KGlobalLedgerView::slotCopySplits}, {Action::GoToPayee, &KGlobalLedgerView::slotGoToPayee}, {Action::GoToAccount, &KGlobalLedgerView::slotGoToAccount}, {Action::MatchTransaction, &KGlobalLedgerView::slotMatchTransactions}, {Action::CombineTransactions, &KGlobalLedgerView::slotCombineTransactions}, {Action::ToggleReconciliationFlag, &KGlobalLedgerView::slotToggleReconciliationFlag}, {Action::MarkCleared, &KGlobalLedgerView::slotMarkCleared}, {Action::MarkReconciled, &KGlobalLedgerView::slotMarkReconciled}, {Action::MarkNotReconciled, &KGlobalLedgerView::slotMarkNotReconciled}, {Action::SelectAllTransactions, &KGlobalLedgerView::slotSelectAllTransactions}, {Action::NewScheduledTransaction, &KGlobalLedgerView::slotCreateScheduledTransaction}, {Action::AssignTransactionsNumber, &KGlobalLedgerView::slotAssignNumber}, {Action::StartReconciliation, &KGlobalLedgerView::slotStartReconciliation}, {Action::FinishReconciliation, &KGlobalLedgerView::slotFinishReconciliation}, {Action::PostponeReconciliation, &KGlobalLedgerView::slotPostponeReconciliation}, {Action::OpenAccount, &KGlobalLedgerView::slotOpenAccount}, {Action::EditFindTransaction, &KGlobalLedgerView::slotFindTransaction}, }; for (auto a = actionConnections.cbegin(); a != actionConnections.cend(); ++a) connect(pActions[a.key()], &QAction::triggered, this, a.value()); Q_D(KGlobalLedgerView); d->m_balanceWarning.reset(new KBalanceWarning(this)); } KGlobalLedgerView::~KGlobalLedgerView() { } void KGlobalLedgerView::executeCustomAction(eView::Action action) { switch(action) { case eView::Action::Refresh: refresh(); break; case eView::Action::SetDefaultFocus: { Q_D(KGlobalLedgerView); QTimer::singleShot(0, d->m_registerSearchLine->searchLine(), SLOT(setFocus())); } break; default: break; } } void KGlobalLedgerView::refresh() { Q_D(KGlobalLedgerView); if (isVisible()) { if (!d->m_inEditMode) { setUpdatesEnabled(false); d->loadView(); setUpdatesEnabled(true); d->m_needsRefresh = false; // force a new account if the current one is empty d->m_newAccountLoaded = d->m_currentAccount.id().isEmpty(); } } else { d->m_needsRefresh = true; } } void KGlobalLedgerView::showEvent(QShowEvent* event) { Q_D(KGlobalLedgerView); if (d->m_needLoad) d->init(); emit customActionRequested(View::Ledgers, eView::Action::AboutToShow); if (d->m_needsRefresh) { if (!d->m_inEditMode) { setUpdatesEnabled(false); d->loadView(); setUpdatesEnabled(true); d->m_needsRefresh = false; d->m_newAccountLoaded = false; } } else { if (!d->m_lastSelectedAccountID.isEmpty()) { try { const auto acc = MyMoneyFile::instance()->account(d->m_lastSelectedAccountID); slotSelectAccount(acc.id()); } catch (const MyMoneyException &) { d->m_lastSelectedAccountID.clear(); // account is invalid } } else { slotSelectAccount(d->m_accountComboBox->getSelected()); } KMyMoneyRegister::SelectedTransactions list(d->m_register); updateLedgerActions(list); emit selectByVariant(QVariantList {QVariant::fromValue(list)}, eView::Intent::SelectRegisterTransactions); } // don't forget base class implementation QWidget::showEvent(event); } void KGlobalLedgerView::updateActions(const MyMoneyObject& obj) { Q_D(KGlobalLedgerView); // if (typeid(obj) != typeid(MyMoneyAccount) && // (obj.id().isEmpty() && d->m_currentAccount.id().isEmpty())) // do not disable actions that were already disabled)) // return; const auto& acc = static_cast(obj); const QVector actionsToBeDisabled { Action::StartReconciliation, Action::FinishReconciliation, Action::PostponeReconciliation, Action::OpenAccount, Action::NewTransaction }; for (const auto& a : actionsToBeDisabled) pActions[a]->setEnabled(false); auto b = acc.isClosed() ? false : true; pMenus[Menu::MoveTransaction]->setEnabled(b); QString tooltip; pActions[Action::NewTransaction]->setEnabled(canCreateTransactions(tooltip)); pActions[Action::NewTransaction]->setToolTip(tooltip); const auto file = MyMoneyFile::instance(); if (!acc.id().isEmpty() && !file->isStandardAccount(acc.id())) { switch (acc.accountGroup()) { case eMyMoney::Account::Type::Asset: case eMyMoney::Account::Type::Liability: case eMyMoney::Account::Type::Equity: pActions[Action::OpenAccount]->setEnabled(true); if (acc.accountGroup() != eMyMoney::Account::Type::Equity) { if (d->m_reconciliationAccount.id().isEmpty()) { pActions[Action::StartReconciliation]->setEnabled(true); pActions[Action::StartReconciliation]->setToolTip(i18n("Reconcile")); } else { auto tip = i18n("Reconcile - disabled because you are currently reconciling %1", d->m_reconciliationAccount.name()); pActions[Action::StartReconciliation]->setToolTip(tip); if (!d->m_transactionEditor) { pActions[Action::FinishReconciliation]->setEnabled(acc.id() == d->m_reconciliationAccount.id()); pActions[Action::PostponeReconciliation]->setEnabled(acc.id() == d->m_reconciliationAccount.id()); } } } break; case eMyMoney::Account::Type::Income : case eMyMoney::Account::Type::Expense : pActions[Action::OpenAccount]->setEnabled(true); break; default: break; } } d->m_currentAccount = acc; // slotSelectAccount(acc); } void KGlobalLedgerView::updateLedgerActions(const KMyMoneyRegister::SelectedTransactions& list) { Q_D(KGlobalLedgerView); d->selectTransactions(list); updateLedgerActionsInternal(); } void KGlobalLedgerView::updateLedgerActionsInternal() { Q_D(KGlobalLedgerView); const QVector actionsToBeDisabled { Action::EditTransaction, Action::EditSplits, Action::EnterTransaction, Action::CancelTransaction, Action::DeleteTransaction, Action::MatchTransaction, Action::AcceptTransaction, Action::DuplicateTransaction, Action::ToggleReconciliationFlag, Action::MarkCleared, Action::GoToAccount, Action::GoToPayee, Action::AssignTransactionsNumber, Action::NewScheduledTransaction, Action::CombineTransactions, Action::SelectAllTransactions, Action::CopySplits, }; for (const auto& a : actionsToBeDisabled) pActions[a]->setEnabled(false); const auto file = MyMoneyFile::instance(); pActions[Action::MatchTransaction]->setText(i18nc("Button text for match transaction", "Match")); // pActions[Action::TransactionNew]->setToolTip(i18n("Create a new transaction")); pMenus[Menu::MoveTransaction]->setEnabled(false); pMenus[Menu::MarkTransaction]->setEnabled(false); pMenus[Menu::MarkTransactionContext]->setEnabled(false); pActions[Action::SelectAllTransactions]->setEnabled(true); if (!d->m_selectedTransactions.isEmpty() && !d->m_selectedTransactions.first().isScheduled()) { // enable 'delete transaction' only if at least one of the // selected transactions does not reference a closed account bool enable = false; KMyMoneyRegister::SelectedTransactions::const_iterator it_t; for (it_t = d->m_selectedTransactions.constBegin(); (enable == false) && (it_t != d->m_selectedTransactions.constEnd()); ++it_t) { enable = !(*it_t).transaction().id().isEmpty() && !file->referencesClosedAccount((*it_t).transaction()); } pActions[Action::DeleteTransaction]->setEnabled(enable); if (!d->m_transactionEditor) { QString tooltip = i18n("Duplicate the current selected transactions"); pActions[Action::DuplicateTransaction]->setEnabled(canDuplicateTransactions(d->m_selectedTransactions, tooltip) && !d->m_selectedTransactions[0].transaction().id().isEmpty()); pActions[Action::DuplicateTransaction]->setToolTip(tooltip); if (canEditTransactions(d->m_selectedTransactions, tooltip)) { pActions[Action::EditTransaction]->setEnabled(true); // editing splits is allowed only if we have one transaction selected if (d->m_selectedTransactions.count() == 1) { pActions[Action::EditSplits]->setEnabled(true); } if (d->m_currentAccount.isAssetLiability() && d->m_currentAccount.accountType() != eMyMoney::Account::Type::Investment) { pActions[Action::NewScheduledTransaction]->setEnabled(d->m_selectedTransactions.count() == 1); } } pActions[Action::EditTransaction]->setToolTip(tooltip); if (!d->m_currentAccount.isClosed()) pMenus[Menu::MoveTransaction]->setEnabled(true); pMenus[Menu::MarkTransaction]->setEnabled(true); pMenus[Menu::MarkTransactionContext]->setEnabled(true); // Allow marking the transaction if at least one is selected pActions[Action::MarkCleared]->setEnabled(true); pActions[Action::MarkReconciled]->setEnabled(true); pActions[Action::MarkNotReconciled]->setEnabled(true); pActions[Action::ToggleReconciliationFlag]->setEnabled(true); if (!d->m_accountGoto.isEmpty()) pActions[Action::GoToAccount]->setEnabled(true); if (!d->m_payeeGoto.isEmpty()) pActions[Action::GoToPayee]->setEnabled(true); // Matching is enabled as soon as one regular and one imported transaction is selected int matchedCount = 0; int importedCount = 0; KMyMoneyRegister::SelectedTransactions::const_iterator it; for (it = d->m_selectedTransactions.constBegin(); it != d->m_selectedTransactions.constEnd(); ++it) { if ((*it).transaction().isImported()) ++importedCount; if ((*it).split().isMatched()) ++matchedCount; } if (d->m_selectedTransactions.count() == 2 /* && pActions[Action::TransactionEdit]->isEnabled() */) { pActions[Action::MatchTransaction]->setEnabled(true); } if (importedCount != 0 || matchedCount != 0) pActions[Action::AcceptTransaction]->setEnabled(true); if (matchedCount != 0) { pActions[Action::MatchTransaction]->setEnabled(true); pActions[Action::MatchTransaction]->setText(i18nc("Button text for unmatch transaction", "Unmatch")); pActions[Action::MatchTransaction]->setIcon(QIcon("process-stop")); } if (d->m_selectedTransactions.count() > 1) { pActions[Action::CombineTransactions]->setEnabled(true); } if (d->m_selectedTransactions.count() >= 2) { int singleSplitTransactions = 0; int multipleSplitTransactions = 0; foreach (const KMyMoneyRegister::SelectedTransaction& st, d->m_selectedTransactions) { switch (st.transaction().splitCount()) { case 0: break; case 1: singleSplitTransactions++; break; default: multipleSplitTransactions++; break; } } if (singleSplitTransactions > 0 && multipleSplitTransactions == 1) { pActions[Action::CopySplits]->setEnabled(true); } } if (d->m_selectedTransactions.count() >= 2) { int singleSplitTransactions = 0; int multipleSplitTransactions = 0; foreach(const KMyMoneyRegister::SelectedTransaction& st, d->m_selectedTransactions) { switch(st.transaction().splitCount()) { case 0: break; case 1: singleSplitTransactions++; break; default: multipleSplitTransactions++; break; } } if(singleSplitTransactions > 0 && multipleSplitTransactions == 1) { pActions[Action::CopySplits]->setEnabled(true); } } } else { pActions[Action::AssignTransactionsNumber]->setEnabled(d->m_transactionEditor->canAssignNumber()); pActions[Action::NewTransaction]->setEnabled(false); pActions[Action::DeleteTransaction]->setEnabled(false); QString reason; pActions[Action::EnterTransaction]->setEnabled(d->m_transactionEditor->isComplete(reason)); //FIXME: Port to KDE4 // the next line somehow worked in KDE3 but does not have // any influence under KDE4 /// Works for me when 'reason' is set. Allan pActions[Action::EnterTransaction]->setToolTip(reason); pActions[Action::CancelTransaction]->setEnabled(true); } } } void KGlobalLedgerView::slotAboutToSelectItem(KMyMoneyRegister::RegisterItem* item, bool& okToSelect) { Q_UNUSED(item); slotCancelOrEnterTransactions(okToSelect); } void KGlobalLedgerView::slotUpdateSummaryLine(const KMyMoneyRegister::SelectedTransactions& selection) { Q_D(KGlobalLedgerView); if (selection.count() > 1) { MyMoneyMoney balance; foreach (const KMyMoneyRegister::SelectedTransaction& t, selection) { if (!t.isScheduled()) { balance += t.split().shares(); } } d->m_rightSummaryLabel->setText(QString("%1: %2").arg(QChar(0x2211), balance.formatMoney("", d->m_precision))); } else { if (d->isReconciliationAccount()) { d->m_rightSummaryLabel->setText(i18n("Difference: %1", d->m_totalBalance.formatMoney("", d->m_precision))); } else { if (d->m_currentAccount.accountType() != eMyMoney::Account::Type::Investment) { d->m_rightSummaryLabel->setText(i18n("Balance: %1", d->m_totalBalance.formatMoney("", d->m_precision))); bool showNegative = d->m_totalBalance.isNegative(); if (d->m_currentAccount.accountGroup() == eMyMoney::Account::Type::Liability && !d->m_totalBalance.isZero()) showNegative = !showNegative; if (showNegative) { QPalette palette = d->m_rightSummaryLabel->palette(); palette.setColor(d->m_rightSummaryLabel->foregroundRole(), KMyMoneySettings::schemeColor(SchemeColor::Negative)); d->m_rightSummaryLabel->setPalette(palette); } } else { d->m_rightSummaryLabel->setText(i18n("Investment value: %1%2", d->m_balanceIsApproximated ? "~" : "", d->m_totalBalance.formatMoney(MyMoneyFile::instance()->baseCurrency().tradingSymbol(), d->m_precision))); } } } } void KGlobalLedgerView::resizeEvent(QResizeEvent* ev) { Q_D(KGlobalLedgerView); if (d->m_needLoad) d->init(); d->m_register->resize((int)eWidgets::eTransaction::Column::Detail); d->m_form->resize((int)eWidgets::eTransactionForm::Column::Value1); KMyMoneyViewBase::resizeEvent(ev); } void KGlobalLedgerView::slotSetReconcileAccount(const MyMoneyAccount& acc, const QDate& reconciliationDate, const MyMoneyMoney& endingBalance) { Q_D(KGlobalLedgerView); if(d->m_needLoad) d->init(); if (d->m_reconciliationAccount.id() != acc.id()) { // make sure the account is selected if (!acc.id().isEmpty()) slotSelectAccount(acc.id()); d->m_reconciliationAccount = acc; d->m_reconciliationDate = reconciliationDate; d->m_endingBalance = endingBalance; if (acc.accountGroup() == eMyMoney::Account::Type::Liability) d->m_endingBalance = -endingBalance; d->m_newAccountLoaded = true; if (acc.id().isEmpty()) { d->m_buttonbar->removeAction(pActions[Action::PostponeReconciliation]); d->m_buttonbar->removeAction(pActions[Action::FinishReconciliation]); } else { d->m_buttonbar->addAction(pActions[Action::PostponeReconciliation]); d->m_buttonbar->addAction(pActions[Action::FinishReconciliation]); // when we start reconciliation, we need to reload the view // because no data has been changed. When postponing or finishing // reconciliation, the data change in the engine takes care of updateing // the view. refresh(); } } } void KGlobalLedgerView::slotSetReconcileAccount(const MyMoneyAccount& acc, const QDate& reconciliationDate) { slotSetReconcileAccount(acc, reconciliationDate, MyMoneyMoney()); } void KGlobalLedgerView::slotSetReconcileAccount(const MyMoneyAccount& acc) { slotSetReconcileAccount(acc, QDate(), MyMoneyMoney()); } void KGlobalLedgerView::slotSetReconcileAccount() { slotSetReconcileAccount(MyMoneyAccount(), QDate(), MyMoneyMoney()); } void KGlobalLedgerView::slotShowTransactionMenu(const MyMoneySplit& sp) { Q_UNUSED(sp) pMenus[Menu::Transaction]->exec(QCursor::pos()); } void KGlobalLedgerView::slotContinueReconciliation() { Q_D(KGlobalLedgerView); const auto file = MyMoneyFile::instance(); MyMoneyAccount account; try { account = file->account(d->m_currentAccount.id()); // get rid of previous run. delete d->m_endingBalanceDlg; d->m_endingBalanceDlg = new KEndingBalanceDlg(account, this); if (account.isAssetLiability()) { if (d->m_endingBalanceDlg->exec() == QDialog::Accepted) { if (KMyMoneySettings::autoReconciliation()) { MyMoneyMoney startBalance = d->m_endingBalanceDlg->previousBalance(); MyMoneyMoney endBalance = d->m_endingBalanceDlg->endingBalance(); QDate endDate = d->m_endingBalanceDlg->statementDate(); QList > transactionList; MyMoneyTransactionFilter filter(account.id()); filter.addState((int)eMyMoney::TransactionFilter::State::Cleared); filter.addState((int)eMyMoney::TransactionFilter::State::NotReconciled); filter.setDateFilter(QDate(), endDate); filter.setConsiderCategory(false); filter.setReportAllSplits(true); file->transactionList(transactionList, filter); QList > result = d->automaticReconciliation(account, transactionList, endBalance - startBalance); if (!result.empty()) { QString message = i18n("KMyMoney has detected transactions matching your reconciliation data.\nWould you like KMyMoney to clear these transactions for you?"); if (KMessageBox::questionYesNo(this, message, i18n("Automatic reconciliation"), KStandardGuiItem::yes(), KStandardGuiItem::no(), "AcceptAutomaticReconciliation") == KMessageBox::Yes) { // mark the transactions cleared KMyMoneyRegister::SelectedTransactions oldSelection = d->m_selectedTransactions; d->m_selectedTransactions.clear(); QListIterator > itTransactionSplitResult(result); while (itTransactionSplitResult.hasNext()) { const QPair &transactionSplit = itTransactionSplitResult.next(); d->m_selectedTransactions.append(KMyMoneyRegister::SelectedTransaction(transactionSplit.first, transactionSplit.second, QString())); } // mark all transactions in d->m_selectedTransactions as 'Cleared' d->markTransaction(eMyMoney::Split::State::Cleared); d->m_selectedTransactions = oldSelection; } } } if (!file->isStandardAccount(account.id()) && account.isAssetLiability()) { if (!isVisible()) emit customActionRequested(View::Ledgers, eView::Action::SwitchView); Models::instance()->accountsModel()->slotReconcileAccount(account, d->m_endingBalanceDlg->statementDate(), d->m_endingBalanceDlg->endingBalance()); slotSetReconcileAccount(account, d->m_endingBalanceDlg->statementDate(), d->m_endingBalanceDlg->endingBalance()); // check if the user requests us to create interest // or charge transactions. auto ti = d->m_endingBalanceDlg->interestTransaction(); auto tc = d->m_endingBalanceDlg->chargeTransaction(); MyMoneyFileTransaction ft; try { if (ti != MyMoneyTransaction()) { MyMoneyFile::instance()->addTransaction(ti); } if (tc != MyMoneyTransaction()) { MyMoneyFile::instance()->addTransaction(tc); } ft.commit(); } catch (const MyMoneyException &e) { qWarning("interest transaction not stored: '%s'", qPrintable(e.what())); } // reload the account object as it might have changed in the meantime d->m_reconciliationAccount = file->account(account.id()); updateActions(d->m_currentAccount); updateLedgerActionsInternal(); // slotUpdateActions(); } } } } catch (const MyMoneyException &) { } } void KGlobalLedgerView::slotLedgerSelected(const QString& _accId, const QString& transaction) { auto acc = MyMoneyFile::instance()->account(_accId); QString accId(_accId); switch (acc.accountType()) { case Account::Type::Stock: // if a stock account is selected, we show the // the corresponding parent (investment) account acc = MyMoneyFile::instance()->account(acc.parentAccountId()); accId = acc.id(); // intentional fall through case Account::Type::Checkings: case Account::Type::Savings: case Account::Type::Cash: case Account::Type::CreditCard: case Account::Type::Loan: case Account::Type::Asset: case Account::Type::Liability: case Account::Type::AssetLoan: case Account::Type::Income: case Account::Type::Expense: case Account::Type::Investment: case Account::Type::Equity: if (!isVisible()) emit customActionRequested(View::Ledgers, eView::Action::SwitchView); slotSelectAccount(accId, transaction); break; case Account::Type::CertificateDep: case Account::Type::MoneyMarket: case Account::Type::Currency: qDebug("No ledger view available for account type %d", (int)acc.accountType()); break; default: qDebug("Unknown account type %d in KMyMoneyView::slotLedgerSelected", (int)acc.accountType()); break; } } void KGlobalLedgerView::slotSelectByObject(const MyMoneyObject& obj, eView::Intent intent) { switch(intent) { case eView::Intent::UpdateActions: updateActions(obj); break; case eView::Intent::FinishEnteringOverdueScheduledTransactions: slotContinueReconciliation(); break; case eView::Intent::SynchronizeAccountInLedgersView: slotSelectAccount(obj); break; default: break; } } void KGlobalLedgerView::slotSelectByVariant(const QVariantList& variant, eView::Intent intent) { switch(intent) { case eView::Intent::ShowTransaction: if (variant.count() == 2) slotLedgerSelected(variant.at(0).toString(), variant.at(1).toString()); break; default: break; } } void KGlobalLedgerView::slotSelectAccount(const MyMoneyObject& obj) { Q_D(KGlobalLedgerView); if (typeid(obj) != typeid(MyMoneyAccount)) return/* false */; d->m_lastSelectedAccountID = obj.id(); } void KGlobalLedgerView::slotSelectAccount(const QString& id) { slotSelectAccount(id, QString()); } bool KGlobalLedgerView::slotSelectAccount(const QString& id, const QString& transactionId) { Q_D(KGlobalLedgerView); auto rc = true; if (!id.isEmpty()) { if (d->m_currentAccount.id() != id) { try { d->m_currentAccount = MyMoneyFile::instance()->account(id); // if a stock account is selected, we show the // the corresponding parent (investment) account if (d->m_currentAccount.isInvest()) { d->m_currentAccount = MyMoneyFile::instance()->account(d->m_currentAccount.parentAccountId()); } d->m_lastSelectedAccountID = d->m_currentAccount.id(); d->m_newAccountLoaded = true; refresh(); } catch (const MyMoneyException &) { qDebug("Unable to retrieve account %s", qPrintable(id)); rc = false; } } else { // we need to refresh m_account.m_accountList, a child could have been deleted d->m_currentAccount = MyMoneyFile::instance()->account(id); emit selectByObject(d->m_currentAccount, eView::Intent::None); emit selectByObject(d->m_currentAccount, eView::Intent::SynchronizeAccountInInvestmentView); } d->selectTransaction(transactionId); } return rc; } bool KGlobalLedgerView::selectEmptyTransaction() { Q_D(KGlobalLedgerView); bool rc = false; if (!d->m_inEditMode) { // in case we don't know the type of transaction to be created, // have at least one selected transaction and the id of // this transaction is not empty, we take it as template for the // transaction to be created KMyMoneyRegister::SelectedTransactions list(d->m_register); if ((d->m_action == eWidgets::eRegister::Action::None) && (!list.isEmpty()) && (!list[0].transaction().id().isEmpty())) { // the new transaction to be created will have the same type // as the one that currently has the focus KMyMoneyRegister::Transaction* t = dynamic_cast(d->m_register->focusItem()); if (t) d->m_action = t->actionType(); d->m_register->clearSelection(); } // if we still don't have an idea which type of transaction // to create, we use the default. if (d->m_action == eWidgets::eRegister::Action::None) { d->setupDefaultAction(); } d->m_register->selectItem(d->m_register->lastItem()); d->m_register->updateRegister(); rc = true; } return rc; } TransactionEditor* KGlobalLedgerView::startEdit(const KMyMoneyRegister::SelectedTransactions& list) { Q_D(KGlobalLedgerView); // we use the warnlevel to keep track, if we have to warn the // user that some or all splits have been reconciled or if the // user cannot modify the transaction if at least one split // has the status frozen. The following value are used: // // 0 - no sweat, user can modify // 1 - user should be warned that at least one split has been reconciled // already // 2 - user will be informed, that this transaction cannot be changed anymore int warnLevel = list.warnLevel(); Q_ASSERT(warnLevel < 2); // otherwise the edit action should not be enabled switch (warnLevel) { case 0: break; case 1: if (KMessageBox::warningContinueCancel(this, i18n( "At least one split of the selected transactions has been reconciled. " "Do you wish to continue to edit the transactions anyway?" ), i18n("Transaction already reconciled"), KStandardGuiItem::cont(), KStandardGuiItem::cancel(), "EditReconciledTransaction") == KMessageBox::Cancel) { warnLevel = 2; } break; case 2: KMessageBox::sorry(this, i18n("At least one split of the selected transactions has been frozen. " "Editing the transactions is therefore prohibited."), i18n("Transaction already frozen")); break; case 3: KMessageBox::sorry(this, i18n("At least one split of the selected transaction references an account that has been closed. " "Editing the transactions is therefore prohibited."), i18n("Account closed")); break; } if (warnLevel > 1) return 0; TransactionEditor* editor = 0; KMyMoneyRegister::Transaction* item = dynamic_cast(d->m_register->focusItem()); if (item) { // in case the current focus item is not selected, we move the focus to the first selected transaction if (!item->isSelected()) { KMyMoneyRegister::RegisterItem* p; for (p = d->m_register->firstItem(); p; p = p->nextItem()) { KMyMoneyRegister::Transaction* t = dynamic_cast(p); if (t && t->isSelected()) { d->m_register->setFocusItem(t); item = t; break; } } } // decide, if we edit in the register or in the form TransactionEditorContainer* parent; if (d->m_formFrame->isVisible()) parent = d->m_form; else { parent = d->m_register; } editor = item->createEditor(parent, list, KGlobalLedgerViewPrivate::m_lastPostDate); // check that we use the same transaction commodity in all selected transactions // if not, we need to update this in the editor's list. The user can also bail out // of this operation which means that we have to stop editing here. if (editor) { if (!editor->fixTransactionCommodity(d->m_currentAccount)) { // if the user wants to quit, we need to destroy the editor // and bail out delete editor; editor = 0; } } if (editor) { if (parent == d->m_register) { // make sure, the height of the table is correct d->m_register->updateRegister(KMyMoneySettings::ledgerLens() | !KMyMoneySettings::transactionForm()); } d->m_inEditMode = true; connect(editor, &TransactionEditor::transactionDataSufficient, pActions[Action::EnterTransaction], &QAction::setEnabled); connect(editor, &TransactionEditor::returnPressed, pActions[Action::EnterTransaction], &QAction::trigger); connect(editor, &TransactionEditor::escapePressed, pActions[Action::CancelTransaction], &QAction::trigger); connect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, editor, &TransactionEditor::slotReloadEditWidgets); connect(editor, &TransactionEditor::finishEdit, this, &KGlobalLedgerView::slotLeaveEditMode); connect(editor, &TransactionEditor::objectCreation, d->m_mousePressFilter, &MousePressFilter::setFilterDeactive); connect(editor, &TransactionEditor::assignNumber, this, &KGlobalLedgerView::slotAssignNumber); connect(editor, &TransactionEditor::lastPostDateUsed, this, &KGlobalLedgerView::slotKeepPostDate); // create the widgets, place them in the parent and load them with data // setup tab order d->m_tabOrderWidgets.clear(); editor->setup(d->m_tabOrderWidgets, d->m_currentAccount, d->m_action); Q_ASSERT(!d->m_tabOrderWidgets.isEmpty()); // install event filter in all taborder widgets QWidgetList::const_iterator it_w = d->m_tabOrderWidgets.constBegin(); for (; it_w != d->m_tabOrderWidgets.constEnd(); ++it_w) { (*it_w)->installEventFilter(this); } // Install a filter that checks if a mouse press happened outside // of one of our own widgets. qApp->installEventFilter(d->m_mousePressFilter); // Check if the editor has some preference on where to set the focus // If not, set the focus to the first widget in the tab order QWidget* focusWidget = editor->firstWidget(); if (!focusWidget) focusWidget = d->m_tabOrderWidgets.first(); // for some reason, this only works reliably if delayed a bit QTimer::singleShot(10, focusWidget, SLOT(setFocus())); // preset to 'I have no idea which type to create' for the next round. d->m_action = eWidgets::eRegister::Action::None; } } return editor; } void KGlobalLedgerView::slotTransactionsContextMenuRequested() { Q_D(KGlobalLedgerView); auto transactions = d->m_selectedTransactions; updateLedgerActionsInternal(); // emit transactionsSelected(d->m_selectedTransactions); // that should select MyMoneySchedule in KScheduledView if (!transactions.isEmpty() && transactions.first().isScheduled()) emit selectByObject(MyMoneyFile::instance()->schedule(transactions.first().scheduleId()), eView::Intent::OpenContextMenu); else slotShowTransactionMenu(MyMoneySplit()); } void KGlobalLedgerView::slotLeaveEditMode(const KMyMoneyRegister::SelectedTransactions& list) { Q_D(KGlobalLedgerView); d->m_inEditMode = false; qApp->removeEventFilter(d->m_mousePressFilter); // a possible focusOut event may have removed the focus, so we // install it back again. d->m_register->focusItem()->setFocus(true); // if we come back from editing a new item, we make sure that // we always select the very last known transaction entry no // matter if the transaction has been created or not. if (list.count() && list[0].transaction().id().isEmpty()) { // block signals to prevent some infinite loops that might occur here. d->m_register->blockSignals(true); d->m_register->clearSelection(); KMyMoneyRegister::RegisterItem* p = d->m_register->lastItem(); if (p && p->prevItem()) p = p->prevItem(); d->m_register->selectItem(p); d->m_register->updateRegister(true); d->m_register->blockSignals(false); // we need to update the form manually as sending signals was blocked KMyMoneyRegister::Transaction* t = dynamic_cast(p); if (t) d->m_form->slotSetTransaction(t); } else { if (!KMyMoneySettings::transactionForm()) { // update the row height of the transactions because it might differ between viewing/editing mode when not using the transaction form d->m_register->blockSignals(true); d->m_register->updateRegister(true); d->m_register->blockSignals(false); } } d->m_needsRefresh = true; // TODO: Why transaction in view doesn't update without this? if (d->m_needsRefresh) refresh(); d->m_register->setFocus(); } bool KGlobalLedgerView::focusNextPrevChild(bool next) { Q_D(KGlobalLedgerView); bool rc = false; // qDebug("KGlobalLedgerView::focusNextPrevChild(editmode=%s)", m_inEditMode ? "true" : "false"); if (d->m_inEditMode) { QWidget *w = 0; w = qApp->focusWidget(); // qDebug("w = %p", w); int currentWidgetIndex = d->m_tabOrderWidgets.indexOf(w); while (w && currentWidgetIndex == -1) { // qDebug("'%s' not in list, use parent", qPrintable(w->objectName())); w = w->parentWidget(); currentWidgetIndex = d->m_tabOrderWidgets.indexOf(w); } if (currentWidgetIndex != -1) { // if(w) qDebug("tab order is at '%s'", qPrintable(w->objectName())); currentWidgetIndex += next ? 1 : -1; if (currentWidgetIndex < 0) currentWidgetIndex = d->m_tabOrderWidgets.size() - 1; else if (currentWidgetIndex >= d->m_tabOrderWidgets.size()) currentWidgetIndex = 0; w = d->m_tabOrderWidgets[currentWidgetIndex]; // qDebug("currentWidgetIndex = %d, w = %p", currentWidgetIndex, w); if (((w->focusPolicy() & Qt::TabFocus) == Qt::TabFocus) && w->isVisible() && w->isEnabled()) { // qDebug("Selecting '%s' (%p) as focus", qPrintable(w->objectName()), w); w->setFocus(); rc = true; } } } else rc = KMyMoneyViewBase::focusNextPrevChild(next); return rc; } bool KGlobalLedgerView::eventFilter(QObject* o, QEvent* e) { Q_D(KGlobalLedgerView); bool rc = false; // Need to capture mouse position here as QEvent::ToolTip is too slow d->m_tooltipPosn = QCursor::pos(); if (e->type() == QEvent::KeyPress) { if (d->m_inEditMode) { // qDebug("object = %s, key = %d", o->className(), k->key()); if (o == d->m_register) { // we hide all key press events from the register // while editing a transaction rc = true; } } } if (!rc) rc = KMyMoneyViewBase::eventFilter(o, e); return rc; } void KGlobalLedgerView::slotSortOptions() { Q_D(KGlobalLedgerView); QPointer dlg = new KSortOptionDlg(this); QString key; QString sortOrder, def; if (d->isReconciliationAccount()) { key = "kmm-sort-reconcile"; def = KMyMoneySettings::sortReconcileView(); } else { key = "kmm-sort-std"; def = KMyMoneySettings::sortNormalView(); } // check if we have an account override of the sort order if (!d->m_currentAccount.value(key).isEmpty()) sortOrder = d->m_currentAccount.value(key); QString oldOrder = sortOrder; dlg->setSortOption(sortOrder, def); if (dlg->exec() == QDialog::Accepted) { if (dlg != 0) { sortOrder = dlg->sortOption(); if (sortOrder != oldOrder) { if (sortOrder.isEmpty()) { d->m_currentAccount.deletePair(key); } else { d->m_currentAccount.setValue(key, sortOrder); } MyMoneyFileTransaction ft; try { MyMoneyFile::instance()->modifyAccount(d->m_currentAccount); ft.commit(); } catch (const MyMoneyException &e) { qDebug("Unable to update sort order for account '%s': %s", qPrintable(d->m_currentAccount.name()), qPrintable(e.what())); } } } } delete dlg; } void KGlobalLedgerView::slotToggleTransactionMark(KMyMoneyRegister::Transaction* /* t */) { Q_D(KGlobalLedgerView); if (!d->m_inEditMode) { slotToggleReconciliationFlag(); } } void KGlobalLedgerView::slotKeepPostDate(const QDate& date) { KGlobalLedgerViewPrivate::m_lastPostDate = date; } QString KGlobalLedgerView::accountId() const { Q_D(const KGlobalLedgerView); return d->m_currentAccount.id(); } bool KGlobalLedgerView::canCreateTransactions(QString& tooltip) const { Q_D(const KGlobalLedgerView); bool rc = true; // we can only create transactions in the ledger view so // we check that this is the active page if(!isVisible()) { tooltip = i18n("Creating transactions can only be performed in the ledger view"); rc = false; } if (d->m_currentAccount.id().isEmpty()) { tooltip = i18n("Cannot create transactions when no account is selected."); rc = false; } if (d->m_currentAccount.accountGroup() == eMyMoney::Account::Type::Income || d->m_currentAccount.accountGroup() == eMyMoney::Account::Type::Expense) { tooltip = i18n("Cannot create transactions in the context of a category."); d->showTooltip(tooltip); rc = false; } if (d->m_currentAccount.isClosed()) { tooltip = i18n("Cannot create transactions in a closed account."); d->showTooltip(tooltip); rc = false; } return rc; } bool KGlobalLedgerView::canModifyTransactions(const KMyMoneyRegister::SelectedTransactions& list, QString& tooltip) const { Q_D(const KGlobalLedgerView); return d->canProcessTransactions(list, tooltip) && list.canModify(); } bool KGlobalLedgerView::canDuplicateTransactions(const KMyMoneyRegister::SelectedTransactions& list, QString& tooltip) const { Q_D(const KGlobalLedgerView); return d->canProcessTransactions(list, tooltip) && list.canDuplicate(); } bool KGlobalLedgerView::canEditTransactions(const KMyMoneyRegister::SelectedTransactions& list, QString& tooltip) const { Q_D(const KGlobalLedgerView); // check if we can edit the list of transactions. We can edit, if // // a) no mix of standard and investment transactions exist // b) if a split transaction is selected, this is the only selection // c) none of the splits is frozen // d) the transaction having the current focus is selected // check for d) if (!d->canProcessTransactions(list, tooltip)) return false; // check for c) if (list.warnLevel() == 2) { tooltip = i18n("Cannot edit transactions with frozen splits."); d->showTooltip(tooltip); return false; } bool rc = true; int investmentTransactions = 0; int normalTransactions = 0; if (d->m_currentAccount.accountGroup() == eMyMoney::Account::Type::Income || d->m_currentAccount.accountGroup() == eMyMoney::Account::Type::Expense) { tooltip = i18n("Cannot edit transactions in the context of a category."); d->showTooltip(tooltip); rc = false; } if (d->m_currentAccount.isClosed()) { tooltip = i18n("Cannot create or edit any transactions in Account %1 as it is closed", d->m_currentAccount.name()); d->showTooltip(tooltip); rc = false; } KMyMoneyRegister::SelectedTransactions::const_iterator it_t; QString action; for (it_t = list.begin(); rc && it_t != list.end(); ++it_t) { if ((*it_t).transaction().id().isEmpty()) { tooltip.clear(); rc = false; continue; } if (KMyMoneyUtils::transactionType((*it_t).transaction()) == KMyMoneyUtils::InvestmentTransaction) { if (action.isEmpty()) { action = (*it_t).split().action(); continue; } if (action == (*it_t).split().action()) { continue; } else { tooltip = (i18n("Cannot edit mixed investment action/type transactions together.")); d->showTooltip(tooltip); rc = false; break; } } if (KMyMoneyUtils::transactionType((*it_t).transaction()) == KMyMoneyUtils::InvestmentTransaction) ++investmentTransactions; else ++normalTransactions; // check for a) if (investmentTransactions != 0 && normalTransactions != 0) { tooltip = i18n("Cannot edit investment transactions and non-investment transactions together."); d->showTooltip(tooltip); rc = false; break; } // check for b) but only for normalTransactions if ((*it_t).transaction().splitCount() > 2 && normalTransactions != 0) { if (list.count() > 1) { tooltip = i18n("Cannot edit multiple split transactions at once."); d->showTooltip(tooltip); rc = false; break; } } } // check for multiple transactions being selected in an investment account // we do not allow editing in this case: https://bugs.kde.org/show_bug.cgi?id=240816 // later on, we might allow to edit investment transactions of the same type /// Can now disable the following check. /* if (rc == true && investmentTransactions > 1) { tooltip = i18n("Cannot edit multiple investment transactions at once"); rc = false; }*/ // now check that we have the correct account type for investment transactions if (rc == true && investmentTransactions != 0) { if (d->m_currentAccount.accountType() != eMyMoney::Account::Type::Investment) { tooltip = i18n("Cannot edit investment transactions in the context of this account."); rc = false; } } return rc; } void KGlobalLedgerView::slotMoveToAccount(const QString& id) { Q_D(KGlobalLedgerView); // close the menu, if it is still open if (pMenus[Menu::Transaction]->isVisible()) pMenus[Menu::Transaction]->close(); if (!d->m_selectedTransactions.isEmpty()) { MyMoneyFileTransaction ft; try { foreach (const auto selection, d->m_selectedTransactions) { if (d->m_currentAccount.accountType() == eMyMoney::Account::Type::Investment) { d->moveInvestmentTransaction(d->m_currentAccount.id(), id, selection.transaction()); } else { auto changed = false; auto t = selection.transaction(); foreach (const auto split, selection.transaction().splits()) { if (split.accountId() == d->m_currentAccount.id()) { MyMoneySplit s = split; s.setAccountId(id); t.modifySplit(s); changed = true; } } if (changed) { MyMoneyFile::instance()->modifyTransaction(t); } } } ft.commit(); } catch (const MyMoneyException &) { } } } void KGlobalLedgerView::slotUpdateMoveToAccountMenu() { Q_D(KGlobalLedgerView); d->createTransactionMoveMenu(); // in case we were not able to create the selector, we // better get out of here. Anything else would cause // a crash later on (accountSet.load) if (!d->m_moveToAccountSelector) return; if (!d->m_currentAccount.id().isEmpty()) { AccountSet accountSet; if (d->m_currentAccount.accountType() == eMyMoney::Account::Type::Investment) { accountSet.addAccountType(eMyMoney::Account::Type::Investment); } else if (d->m_currentAccount.isAssetLiability()) { accountSet.addAccountType(eMyMoney::Account::Type::Checkings); accountSet.addAccountType(eMyMoney::Account::Type::Savings); accountSet.addAccountType(eMyMoney::Account::Type::Cash); accountSet.addAccountType(eMyMoney::Account::Type::AssetLoan); accountSet.addAccountType(eMyMoney::Account::Type::CertificateDep); accountSet.addAccountType(eMyMoney::Account::Type::MoneyMarket); accountSet.addAccountType(eMyMoney::Account::Type::Asset); accountSet.addAccountType(eMyMoney::Account::Type::Currency); accountSet.addAccountType(eMyMoney::Account::Type::CreditCard); accountSet.addAccountType(eMyMoney::Account::Type::Loan); accountSet.addAccountType(eMyMoney::Account::Type::Liability); } else if (d->m_currentAccount.isIncomeExpense()) { accountSet.addAccountType(eMyMoney::Account::Type::Income); accountSet.addAccountType(eMyMoney::Account::Type::Expense); } accountSet.load(d->m_moveToAccountSelector); // remove those accounts that we currently reference foreach (const auto selection, d->m_selectedTransactions) { foreach (const auto split, selection.transaction().splits()) { d->m_moveToAccountSelector->removeItem(split.accountId()); } } // remove those accounts from the list that are denominated // in a different currency auto list = d->m_moveToAccountSelector->accountList(); QList::const_iterator it_a; for (it_a = list.constBegin(); it_a != list.constEnd(); ++it_a) { auto acc = MyMoneyFile::instance()->account(*it_a); if (acc.currencyId() != d->m_currentAccount.currencyId()) d->m_moveToAccountSelector->removeItem((*it_a)); } } } void KGlobalLedgerView::slotObjectDestroyed(QObject* o) { Q_D(KGlobalLedgerView); if (o == d->m_moveToAccountSelector) { d->m_moveToAccountSelector = nullptr; } } void KGlobalLedgerView::slotCancelOrEnterTransactions(bool& okToSelect) { Q_D(KGlobalLedgerView); static bool oneTime = false; if (!oneTime) { oneTime = true; auto dontShowAgain = "CancelOrEditTransaction"; // qDebug("KMyMoneyApp::slotCancelOrEndEdit"); if (d->m_transactionEditor) { if (KMyMoneySettings::focusChangeIsEnter() && pActions[Action::EnterTransaction]->isEnabled()) { slotEnterTransaction(); if (d->m_transactionEditor) { // if at this stage the editor is still there that means that entering the transaction was cancelled // for example by pressing cancel on the exchange rate editor so we must stay in edit mode okToSelect = false; } } else { // okToSelect is preset to true if a cancel of the dialog is useful and false if it is not int rc; KGuiItem noGuiItem = KStandardGuiItem::save(); KGuiItem yesGuiItem = KStandardGuiItem::discard(); KGuiItem cancelGuiItem = KStandardGuiItem::cont(); // if the transaction can't be entered make sure that it can't be entered by pressing no either if (!pActions[Action::EnterTransaction]->isEnabled()) { noGuiItem.setEnabled(false); noGuiItem.setToolTip(pActions[Action::EnterTransaction]->toolTip()); } if (okToSelect == true) { rc = KMessageBox::warningYesNoCancel(this, i18n("

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

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

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

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

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

"), i18n("End transaction edit"), yesGuiItem, noGuiItem, dontShowAgain); } switch (rc) { case KMessageBox::Yes: slotCancelTransaction(); break; case KMessageBox::No: slotEnterTransaction(); // make sure that we'll see this message the next time no matter // if the user has chosen the 'Don't show again' checkbox KMessageBox::enableMessage(dontShowAgain); if (d->m_transactionEditor) { // if at this stage the editor is still there that means that entering the transaction was cancelled // for example by pressing cancel on the exchange rate editor so we must stay in edit mode okToSelect = false; } break; case KMessageBox::Cancel: // make sure that we'll see this message the next time no matter // if the user has chosen the 'Don't show again' checkbox KMessageBox::enableMessage(dontShowAgain); okToSelect = false; break; } } } oneTime = false; } } void KGlobalLedgerView::slotNewSchedule(const MyMoneyTransaction& _t, eMyMoney::Schedule::Occurrence occurrence) { KEditScheduleDlg::newSchedule(_t, occurrence); } void KGlobalLedgerView::slotNewTransactionForm(eWidgets::eRegister::Action id) { Q_D(KGlobalLedgerView); if (!d->m_inEditMode) { d->m_action = id; // since we jump here via code, we have to make sure to react only // if the action is enabled if (pActions[Action::NewTransaction]->isEnabled()) { if (d->createNewTransaction()) { d->m_transactionEditor = d->startEdit(d->m_selectedTransactions); if (d->m_transactionEditor) { KMyMoneyMVCCombo::setSubstringSearchForChildren(this/*d->m_myMoneyView*/, !KMyMoneySettings::stringMatchFromStart()); KMyMoneyPayeeCombo* payeeEdit = dynamic_cast(d->m_transactionEditor->haveWidget("payee")); if (payeeEdit && !d->m_lastPayeeEnteredId.isEmpty()) { // in case we entered a new transaction before and used a payee, // we reuse it here. Save the text to the edit widget, select it // so that hitting any character will start entering another payee. payeeEdit->setSelectedItem(d->m_lastPayeeEnteredId); payeeEdit->lineEdit()->selectAll(); } if (d->m_transactionEditor) { connect(d->m_transactionEditor.data(), &TransactionEditor::statusProgress, this, &KGlobalLedgerView::slotStatusProgress); connect(d->m_transactionEditor.data(), &TransactionEditor::statusMsg, this, &KGlobalLedgerView::slotStatusMsg); connect(d->m_transactionEditor.data(), &TransactionEditor::scheduleTransaction, this, &KGlobalLedgerView::slotNewSchedule); } updateLedgerActionsInternal(); // emit transactionsSelected(d->m_selectedTransactions); } } } } } void KGlobalLedgerView::slotNewTransaction() { slotNewTransactionForm(eWidgets::eRegister::Action::None); } void KGlobalLedgerView::slotEditTransaction() { Q_D(KGlobalLedgerView); // qDebug("KMyMoneyApp::slotTransactionsEdit()"); // since we jump here via code, we have to make sure to react only // if the action is enabled if (pActions[Action::EditTransaction]->isEnabled()) { // as soon as we edit a transaction, we don't remember the last payee entered d->m_lastPayeeEnteredId.clear(); d->m_transactionEditor = d->startEdit(d->m_selectedTransactions); KMyMoneyMVCCombo::setSubstringSearchForChildren(this/*d->m_myMoneyView*/, !KMyMoneySettings::stringMatchFromStart()); updateLedgerActionsInternal(); } } void KGlobalLedgerView::slotDeleteTransaction() { Q_D(KGlobalLedgerView); // since we may jump here via code, we have to make sure to react only // if the action is enabled if (!pActions[Action::DeleteTransaction]->isEnabled()) return; if (d->m_selectedTransactions.isEmpty()) return; if (d->m_selectedTransactions.warnLevel() == 1) { if (KMessageBox::warningContinueCancel(this, i18n("At least one split of the selected transactions has been reconciled. " "Do you wish to delete the transactions anyway?"), i18n("Transaction already reconciled")) == KMessageBox::Cancel) return; } auto msg = i18np("Do you really want to delete the selected transaction?", "Do you really want to delete all %1 selected transactions?", d->m_selectedTransactions.count()); if (KMessageBox::questionYesNo(this, msg, i18n("Delete transaction")) == KMessageBox::Yes) { //KMSTATUS(i18n("Deleting transactions")); d->doDeleteTransactions(); } } void KGlobalLedgerView::slotDuplicateTransaction() { Q_D(KGlobalLedgerView); // since we may jump here via code, we have to make sure to react only // if the action is enabled if (pActions[Action::DuplicateTransaction]->isEnabled()) { KMyMoneyRegister::SelectedTransactions selectionList = d->m_selectedTransactions; KMyMoneyRegister::SelectedTransactions::iterator it_t; int i = 0; int cnt = d->m_selectedTransactions.count(); // KMSTATUS(i18n("Duplicating transactions")); emit selectByVariant(QVariantList {QVariant(0), QVariant(cnt)}, eView::Intent::ReportProgress); MyMoneyFileTransaction ft; MyMoneyTransaction lt; try { foreach (const auto selection, selectionList) { auto t = selection.transaction(); // wipe out any reconciliation information for (auto& split : t.splits()) { split.setReconcileFlag(eMyMoney::Split::State::NotReconciled); split.setReconcileDate(QDate()); split.setBankID(QString()); } // clear invalid data t.setEntryDate(QDate()); t.clearId(); // and set the post date to today t.setPostDate(QDate::currentDate()); MyMoneyFile::instance()->addTransaction(t); lt = t; emit selectByVariant(QVariantList {QVariant(i++), QVariant(0)}, eView::Intent::ReportProgress); } ft.commit(); // select the new transaction in the ledger if (!d->m_currentAccount.id().isEmpty()) slotLedgerSelected(d->m_currentAccount.id(), lt.id()); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(this, i18n("Error"), i18n("Unable to duplicate transaction(s): %1, thrown in %2:%3", e.what(), e.file(), e.line())); } // switch off the progress bar emit selectByVariant(QVariantList {QVariant(-1), QVariant(-1)}, eView::Intent::ReportProgress); } } void KGlobalLedgerView::slotEnterTransaction() { Q_D(KGlobalLedgerView); // since we jump here via code, we have to make sure to react only // if the action is enabled if (pActions[Action::EnterTransaction]->isEnabled()) { // disable the action while we process it to make sure it's processed only once since // d->m_transactionEditor->enterTransactions(newId) will run QCoreApplication::processEvents // we could end up here twice which will cause a crash slotUpdateActions() will enable the action again pActions[Action::EnterTransaction]->setEnabled(false); if (d->m_transactionEditor) { QString accountId = d->m_currentAccount.id(); QString newId; connect(d->m_transactionEditor.data(), &TransactionEditor::balanceWarning, d->m_balanceWarning.data(), &KBalanceWarning::slotShowMessage); if (d->m_transactionEditor->enterTransactions(newId)) { KMyMoneyPayeeCombo* payeeEdit = dynamic_cast(d->m_transactionEditor->haveWidget("payee")); if (payeeEdit && !newId.isEmpty()) { d->m_lastPayeeEnteredId = payeeEdit->selectedItem(); } d->deleteTransactionEditor(); } if (!newId.isEmpty()) { slotLedgerSelected(accountId, newId); } } updateLedgerActionsInternal(); } } void KGlobalLedgerView::slotAcceptTransaction() { Q_D(KGlobalLedgerView); KMyMoneyRegister::SelectedTransactions list = d->m_selectedTransactions; KMyMoneyRegister::SelectedTransactions::const_iterator it_t; int cnt = list.count(); int i = 0; emit selectByVariant(QVariantList {QVariant(0), QVariant(cnt)}, eView::Intent::ReportProgress); MyMoneyFileTransaction ft; try { for (it_t = list.constBegin(); it_t != list.constEnd(); ++it_t) { // reload transaction in case it got changed during the course of this loop MyMoneyTransaction t = MyMoneyFile::instance()->transaction((*it_t).transaction().id()); if (t.isImported()) { t.setImported(false); if (!d->m_currentAccount.id().isEmpty()) { foreach (const auto split, t.splits()) { if (split.accountId() == d->m_currentAccount.id()) { if (split.reconcileFlag() == eMyMoney::Split::State::NotReconciled) { MyMoneySplit s = split; s.setReconcileFlag(eMyMoney::Split::State::Cleared); t.modifySplit(s); } } } } MyMoneyFile::instance()->modifyTransaction(t); } if ((*it_t).split().isMatched()) { // reload split in case it got changed during the course of this loop MyMoneySplit s = t.splitById((*it_t).split().id()); TransactionMatcher matcher(d->m_currentAccount); matcher.accept(t, s); } emit selectByVariant(QVariantList {QVariant(i++), QVariant(0)}, eView::Intent::ReportProgress); } emit selectByVariant(QVariantList {QVariant(-1), QVariant(-1)}, eView::Intent::ReportProgress); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(this, i18n("Error"), i18n("Unable to accept transaction: %1, thrown in %2:%3", e.what(), e.file(), e.line())); } } void KGlobalLedgerView::slotCancelTransaction() { Q_D(KGlobalLedgerView); // since we jump here via code, we have to make sure to react only // if the action is enabled if (pActions[Action::CancelTransaction]->isEnabled()) { // make sure, we block the enter function pActions[Action::EnterTransaction]->setEnabled(false); // qDebug("KMyMoneyApp::slotTransactionsCancel"); d->deleteTransactionEditor(); updateLedgerActions(d->m_selectedTransactions); emit selectByVariant(QVariantList {QVariant::fromValue(d->m_selectedTransactions)}, eView::Intent::SelectRegisterTransactions); } } void KGlobalLedgerView::slotEditSplits() { Q_D(KGlobalLedgerView); // since we jump here via code, we have to make sure to react only // if the action is enabled if (pActions[Action::EditSplits]->isEnabled()) { // as soon as we edit a transaction, we don't remember the last payee entered d->m_lastPayeeEnteredId.clear(); d->m_transactionEditor = d->startEdit(d->m_selectedTransactions); updateLedgerActions(d->m_selectedTransactions); emit selectByVariant(QVariantList {QVariant::fromValue(d->m_selectedTransactions)}, eView::Intent::SelectRegisterTransactions); if (d->m_transactionEditor) { KMyMoneyMVCCombo::setSubstringSearchForChildren(this/*d->m_myMoneyView*/, !KMyMoneySettings::stringMatchFromStart()); if (d->m_transactionEditor->slotEditSplits() == QDialog::Accepted) { MyMoneyFileTransaction ft; try { QString id; connect(d->m_transactionEditor.data(), &TransactionEditor::balanceWarning, d->m_balanceWarning.data(), &KBalanceWarning::slotShowMessage); d->m_transactionEditor->enterTransactions(id); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(this, i18n("Error"), i18n("Unable to modify transaction: %1, thrown in %2:%3", e.what(), e.file(), e.line())); } } } d->deleteTransactionEditor(); updateLedgerActions(d->m_selectedTransactions); emit selectByVariant(QVariantList {QVariant::fromValue(d->m_selectedTransactions)}, eView::Intent::SelectRegisterTransactions); } } void KGlobalLedgerView::slotCopySplits() { Q_D(KGlobalLedgerView); const auto file = MyMoneyFile::instance(); if (d->m_selectedTransactions.count() >= 2) { int singleSplitTransactions = 0; int multipleSplitTransactions = 0; KMyMoneyRegister::SelectedTransaction selectedSourceTransaction; foreach (const auto& st, d->m_selectedTransactions) { switch (st.transaction().splitCount()) { case 0: break; case 1: singleSplitTransactions++; break; default: selectedSourceTransaction = st; multipleSplitTransactions++; break; } } if (singleSplitTransactions > 0 && multipleSplitTransactions == 1) { MyMoneyFileTransaction ft; try { const auto& sourceTransaction = selectedSourceTransaction.transaction(); const auto& sourceSplit = selectedSourceTransaction.split(); foreach (const KMyMoneyRegister::SelectedTransaction& st, d->m_selectedTransactions) { auto t = st.transaction(); // don't process the source transaction if (sourceTransaction.id() == t.id()) { continue; } const auto& baseSplit = st.split(); if (t.splitCount() == 1) { foreach (const auto& split, sourceTransaction.splits()) { // Don't copy the source split, as we already have that // as part of the destination transaction if (split.id() == sourceSplit.id()) { continue; } MyMoneySplit sp(split); // clear the ID and reconciliation state sp.clearId(); sp.setReconcileFlag(eMyMoney::Split::State::NotReconciled); sp.setReconcileDate(QDate()); // in case it is a simple transaction consisting of two splits, // we can adjust the share and value part of the second split we // just created. We need to keep a possible price in mind in case // of different currencies if (sourceTransaction.splitCount() == 2) { sp.setValue(-baseSplit.value()); sp.setShares(-(baseSplit.shares() * baseSplit.price())); } t.addSplit(sp); } file->modifyTransaction(t); } } ft.commit(); } catch (const MyMoneyException &) { qDebug() << "transactionCopySplits() failed"; } } } } void KGlobalLedgerView::slotGoToPayee() { Q_D(KGlobalLedgerView); if (!d->m_payeeGoto.isEmpty()) { try { QString transactionId; if (d->m_selectedTransactions.count() == 1) { transactionId = d->m_selectedTransactions[0].transaction().id(); } // make sure to pass copies, as d->myMoneyView->slotPayeeSelected() overrides // d->m_payeeGoto and d->m_currentAccount while calling slotUpdateActions() QString payeeId = d->m_payeeGoto; QString accountId = d->m_currentAccount.id(); emit selectByVariant(QVariantList {QVariant(payeeId), QVariant(accountId), QVariant(transactionId)}, eView::Intent::ShowPayee); // emit openPayeeRequested(payeeId, accountId, transactionId); } catch (const MyMoneyException &) { } } } void KGlobalLedgerView::slotGoToAccount() { Q_D(KGlobalLedgerView); if (!d->m_accountGoto.isEmpty()) { try { QString transactionId; if (d->m_selectedTransactions.count() == 1) { transactionId = d->m_selectedTransactions[0].transaction().id(); } // make sure to pass a copy, as d->myMoneyView->slotLedgerSelected() overrides // d->m_accountGoto while calling slotUpdateActions() slotLedgerSelected(d->m_accountGoto, transactionId); } catch (const MyMoneyException &) { } } } void KGlobalLedgerView::slotMatchTransactions() { Q_D(KGlobalLedgerView); // if the menu action is retrieved it can contain an '&' character for the accelerator causing the comparison to fail if not removed QString transactionActionText = pActions[Action::MatchTransaction]->text(); transactionActionText.remove('&'); if (transactionActionText == i18nc("Button text for match transaction", "Match")) d->transactionMatch(); else d->transactionUnmatch(); } void KGlobalLedgerView::slotCombineTransactions() { qDebug("slotTransactionCombine() not implemented yet"); } void KGlobalLedgerView::slotToggleReconciliationFlag() { Q_D(KGlobalLedgerView); d->markTransaction(eMyMoney::Split::State::Unknown); } void KGlobalLedgerView::slotMarkCleared() { Q_D(KGlobalLedgerView); d->markTransaction(eMyMoney::Split::State::Cleared); } void KGlobalLedgerView::slotMarkReconciled() { Q_D(KGlobalLedgerView); d->markTransaction(eMyMoney::Split::State::Reconciled); } void KGlobalLedgerView::slotMarkNotReconciled() { Q_D(KGlobalLedgerView); d->markTransaction(eMyMoney::Split::State::NotReconciled); } void KGlobalLedgerView::slotSelectAllTransactions() { Q_D(KGlobalLedgerView); if(d->m_needLoad) d->init(); d->m_register->clearSelection(); KMyMoneyRegister::RegisterItem* p = d->m_register->firstItem(); while (p) { KMyMoneyRegister::Transaction* t = dynamic_cast(p); if (t) { if (t->isVisible() && t->isSelectable() && !t->isScheduled() && !t->id().isEmpty()) { t->setSelected(true); } } p = p->nextItem(); } // this is here only to re-paint the items without selecting anything because the data (including the selection) is not really held in the model right now d->m_register->selectAll(); // inform everyone else about the selected items KMyMoneyRegister::SelectedTransactions list(d->m_register); updateLedgerActions(list); emit selectByVariant(QVariantList {QVariant::fromValue(list)}, eView::Intent::SelectRegisterTransactions); } void KGlobalLedgerView::slotCreateScheduledTransaction() { Q_D(KGlobalLedgerView); if (d->m_selectedTransactions.count() == 1) { // make sure to have the current selected split as first split in the schedule MyMoneyTransaction t = d->m_selectedTransactions[0].transaction(); MyMoneySplit s = d->m_selectedTransactions[0].split(); QString splitId = s.id(); s.clearId(); s.setReconcileFlag(eMyMoney::Split::State::NotReconciled); s.setReconcileDate(QDate()); t.removeSplits(); t.addSplit(s); foreach (const auto split, d->m_selectedTransactions[0].transaction().splits()) { if (split.id() != splitId) { MyMoneySplit s0 = split; s0.clearId(); s0.setReconcileFlag(eMyMoney::Split::State::NotReconciled); s0.setReconcileDate(QDate()); t.addSplit(s0); } } KEditScheduleDlg::newSchedule(t, eMyMoney::Schedule::Occurrence::Monthly); } } void KGlobalLedgerView::slotAssignNumber() { Q_D(KGlobalLedgerView); if (d->m_transactionEditor) d->m_transactionEditor->assignNextNumber(); } void KGlobalLedgerView::slotStartReconciliation() { Q_D(KGlobalLedgerView); // we cannot reconcile standard accounts if (!MyMoneyFile::instance()->isStandardAccount(d->m_currentAccount.id())) emit selectByObject(d->m_currentAccount, eView::Intent::StartEnteringOverdueScheduledTransactions); // asynchronous call to KScheduledView::slotEnterOverdueSchedules is made here // after that all activity should be continued in KGlobalLedgerView::slotContinueReconciliation() } void KGlobalLedgerView::slotFinishReconciliation() { Q_D(KGlobalLedgerView); const auto file = MyMoneyFile::instance(); if (!d->m_reconciliationAccount.id().isEmpty()) { // retrieve list of all transactions that are not reconciled or cleared QList > transactionList; MyMoneyTransactionFilter filter(d->m_reconciliationAccount.id()); filter.addState((int)eMyMoney::TransactionFilter::State::Cleared); filter.addState((int)eMyMoney::TransactionFilter::State::NotReconciled); filter.setDateFilter(QDate(), d->m_endingBalanceDlg->statementDate()); filter.setConsiderCategory(false); filter.setReportAllSplits(true); file->transactionList(transactionList, filter); auto balance = MyMoneyFile::instance()->balance(d->m_reconciliationAccount.id(), d->m_endingBalanceDlg->statementDate()); MyMoneyMoney actBalance, clearedBalance; actBalance = clearedBalance = balance; // walk the list of transactions to figure out the balance(s) - QList >::const_iterator it; - for (it = transactionList.constBegin(); it != transactionList.constEnd(); ++it) { + for (auto it = transactionList.constBegin(); it != transactionList.constEnd(); ++it) { if ((*it).second.reconcileFlag() == eMyMoney::Split::State::NotReconciled) { clearedBalance -= (*it).second.shares(); } } if (d->m_endingBalanceDlg->endingBalance() != clearedBalance) { auto message = i18n("You are about to finish the reconciliation of this account with a difference between your bank statement and the transactions marked as cleared.\n" "Are you sure you want to finish the reconciliation?"); if (KMessageBox::questionYesNo(this, message, i18n("Confirm end of reconciliation"), KStandardGuiItem::yes(), KStandardGuiItem::no()) == KMessageBox::No) return; } MyMoneyFileTransaction ft; // refresh object d->m_reconciliationAccount = file->account(d->m_reconciliationAccount.id()); // Turn off reconciliation mode // Models::instance()->accountsModel()->slotReconcileAccount(MyMoneyAccount(), QDate(), MyMoneyMoney()); // slotSetReconcileAccount(MyMoneyAccount(), QDate(), MyMoneyMoney()); // d->m_myMoneyView->finishReconciliation(d->m_reconciliationAccount); // only update the last statement balance here, if we haven't a newer one due // to download of online statements. if (d->m_reconciliationAccount.value("lastImportedTransactionDate").isEmpty() || QDate::fromString(d->m_reconciliationAccount.value("lastImportedTransactionDate"), Qt::ISODate) < d->m_endingBalanceDlg->statementDate()) { d->m_reconciliationAccount.setValue("lastStatementBalance", d->m_endingBalanceDlg->endingBalance().toString()); // in case we override the last statement balance here, we have to make sure // that we don't show the online balance anymore, as it might be different d->m_reconciliationAccount.deletePair("lastImportedTransactionDate"); } d->m_reconciliationAccount.setLastReconciliationDate(d->m_endingBalanceDlg->statementDate()); // keep a record of this reconciliation d->m_reconciliationAccount.addReconciliation(d->m_endingBalanceDlg->statementDate(), d->m_endingBalanceDlg->endingBalance()); d->m_reconciliationAccount.deletePair("lastReconciledBalance"); d->m_reconciliationAccount.deletePair("statementBalance"); d->m_reconciliationAccount.deletePair("statementDate"); try { // update the account data file->modifyAccount(d->m_reconciliationAccount); /* // collect the list of cleared splits for this account filter.clear(); filter.addAccount(d->m_reconciliationAccount.id()); filter.addState(eMyMoney::TransactionFilter::Cleared); filter.setConsiderCategory(false); filter.setReportAllSplits(true); file->transactionList(transactionList, filter); */ // walk the list of transactions/splits and mark the cleared ones as reconciled - QList >::iterator it; - for (it = transactionList.begin(); it != transactionList.end(); ++it) { + for (auto it = transactionList.begin(); it != transactionList.end(); ++it) { MyMoneySplit sp = (*it).second; // skip the ones that are not marked cleared if (sp.reconcileFlag() != eMyMoney::Split::State::Cleared) continue; // always retrieve a fresh copy of the transaction because we // might have changed it already with another split MyMoneyTransaction t = file->transaction((*it).first.id()); sp.setReconcileFlag(eMyMoney::Split::State::Reconciled); sp.setReconcileDate(d->m_endingBalanceDlg->statementDate()); t.modifySplit(sp); // update the engine ... file->modifyTransaction(t); // ... and the list (*it) = qMakePair(t, sp); } ft.commit(); // reload account data from engine as the data might have changed in the meantime d->m_reconciliationAccount = file->account(d->m_reconciliationAccount.id()); /** * This signal is emitted when an account has been successfully reconciled * and all transactions are updated in the engine. It can be used by plugins * to create reconciliation reports. * * @param account the account data * @param date the reconciliation date as provided through the dialog * @param startingBalance the starting balance as provided through the dialog * @param endingBalance the ending balance as provided through the dialog * @param transactionList reference to QList of QPair containing all * transaction/split pairs processed by the reconciliation. */ emit selectByVariant(QVariantList { QVariant::fromValue(d->m_reconciliationAccount), QVariant::fromValue(d->m_endingBalanceDlg->statementDate()), QVariant::fromValue(d->m_endingBalanceDlg->previousBalance()), QVariant::fromValue(d->m_endingBalanceDlg->endingBalance()), QVariant::fromValue(transactionList) }, eView::Intent::AccountReconciled); } catch (const MyMoneyException &) { qDebug("Unexpected exception when setting cleared to reconcile"); } // Turn off reconciliation mode Models::instance()->accountsModel()->slotReconcileAccount(MyMoneyAccount(), QDate(), MyMoneyMoney()); slotSetReconcileAccount(MyMoneyAccount(), QDate(), MyMoneyMoney()); } // Turn off reconciliation mode d->m_reconciliationAccount = MyMoneyAccount(); updateActions(d->m_currentAccount); updateLedgerActionsInternal(); d->loadView(); // slotUpdateActions(); } void KGlobalLedgerView::slotPostponeReconciliation() { Q_D(KGlobalLedgerView); MyMoneyFileTransaction ft; const auto file = MyMoneyFile::instance(); if (!d->m_reconciliationAccount.id().isEmpty()) { // refresh object d->m_reconciliationAccount = file->account(d->m_reconciliationAccount.id()); // Turn off reconciliation mode // Models::instance()->accountsModel()->slotReconcileAccount(MyMoneyAccount(), QDate(), MyMoneyMoney()); // slotSetReconcileAccount(MyMoneyAccount(), QDate(), MyMoneyMoney()); // d->m_myMoneyView->finishReconciliation(d->m_reconciliationAccount); d->m_reconciliationAccount.setValue("lastReconciledBalance", d->m_endingBalanceDlg->previousBalance().toString()); d->m_reconciliationAccount.setValue("statementBalance", d->m_endingBalanceDlg->endingBalance().toString()); d->m_reconciliationAccount.setValue("statementDate", d->m_endingBalanceDlg->statementDate().toString(Qt::ISODate)); try { file->modifyAccount(d->m_reconciliationAccount); ft.commit(); d->m_reconciliationAccount = MyMoneyAccount(); updateActions(d->m_currentAccount); updateLedgerActionsInternal(); // slotUpdateActions(); } catch (const MyMoneyException &) { qDebug("Unexpected exception when setting last reconcile info into account"); ft.rollback(); d->m_reconciliationAccount = file->account(d->m_reconciliationAccount.id()); } // Turn off reconciliation mode Models::instance()->accountsModel()->slotReconcileAccount(MyMoneyAccount(), QDate(), MyMoneyMoney()); slotSetReconcileAccount(MyMoneyAccount(), QDate(), MyMoneyMoney()); d->loadView(); } } void KGlobalLedgerView::slotOpenAccount() { Q_D(KGlobalLedgerView); if (!MyMoneyFile::instance()->isStandardAccount(d->m_currentAccount.id())) slotLedgerSelected(d->m_currentAccount.id(), QString()); } void KGlobalLedgerView::slotFindTransaction() { Q_D(KGlobalLedgerView); if (!d->m_searchDlg) { d->m_searchDlg = new KFindTransactionDlg(this); connect(d->m_searchDlg, &QObject::destroyed, this, &KGlobalLedgerView::slotCloseSearchDialog); connect(d->m_searchDlg, &KFindTransactionDlg::transactionSelected, this, &KGlobalLedgerView::slotLedgerSelected); } d->m_searchDlg->show(); d->m_searchDlg->raise(); d->m_searchDlg->activateWindow(); } void KGlobalLedgerView::slotCloseSearchDialog() { Q_D(KGlobalLedgerView); if (d->m_searchDlg) d->m_searchDlg->deleteLater(); d->m_searchDlg = nullptr; } void KGlobalLedgerView::slotStatusMsg(const QString& txt) { emit selectByVariant(QVariantList {QVariant(txt)}, eView::Intent::ReportProgressMessage); } void KGlobalLedgerView::slotStatusProgress(int cnt, int base) { emit selectByVariant(QVariantList {QVariant(cnt), QVariant(base)}, eView::Intent::ReportProgress); } void KGlobalLedgerView::slotTransactionsSelected(const KMyMoneyRegister::SelectedTransactions& list) { updateLedgerActions(list); emit selectByVariant(QVariantList {QVariant::fromValue(list)}, eView::Intent::SelectRegisterTransactions); } diff --git a/kmymoney/views/kgloballedgerview_p.h b/kmymoney/views/kgloballedgerview_p.h index 760249f6a..eb492057d 100644 --- a/kmymoney/views/kgloballedgerview_p.h +++ b/kmymoney/views/kgloballedgerview_p.h @@ -1,1649 +1,1649 @@ /*************************************************************************** kgloballedgerview_p.h - description ------------------- begin : Wed Jul 26 2006 copyright : (C) 2006 by Thomas Baumgart email : Thomas Baumgart (C) 2017 by Łukasz Wojniłowicz ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #ifndef KGLOBALLEDGERVIEW_P_H #define KGLOBALLEDGERVIEW_P_H #include "kgloballedgerview.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "kmymoneyviewbase_p.h" #include "kendingbalancedlg.h" #include "kfindtransactiondlg.h" #include "kmymoneyaccountselector.h" #include "kmymoneyutils.h" #include "mymoneyexception.h" #include "mymoneymoney.h" #include "mymoneyaccount.h" #include "mymoneyfile.h" #include "kmymoneyaccountcombo.h" #include "kbalancewarning.h" #include "transactionmatcher.h" #include "tabbar.h" #include "register.h" #include "transactioneditor.h" #include "selectedtransactions.h" #include "kmymoneysettings.h" #include "registersearchline.h" #include "scheduledtransaction.h" #include "accountsmodel.h" #include "models.h" #include "mymoneyprice.h" #include "mymoneyschedule.h" #include "mymoneysecurity.h" #include "mymoneytransaction.h" #include "mymoneytransactionfilter.h" #include "mymoneysplit.h" #include "mymoneypayee.h" #include "mymoneytracer.h" #include "transaction.h" #include "transactionform.h" #include "fancydategroupmarkers.h" #include "widgetenums.h" #include "mymoneyenums.h" #include "modelenums.h" #include "menuenums.h" #include #ifdef KMM_DEBUG #include "mymoneyutils.h" #endif using namespace eMenu; using namespace eMyMoney; /** * helper class implementing an event filter to detect mouse button press * events on widgets outside a given set of widgets. This is used internally * to detect when to leave the edit mode. */ class MousePressFilter : public QObject { Q_OBJECT public: explicit MousePressFilter(QWidget* parent = nullptr) : QObject(parent), m_lastMousePressEvent(0), m_filterActive(true) { } /** * Add widget @p w to the list of possible parent objects. See eventFilter() how * they will be used. */ void addWidget(QWidget* w) { m_parents.append(w); } public Q_SLOTS: /** * This slot allows to activate/deactivate the filter. By default the * filter is active. * * @param state Allows to activate (@a true) or deactivate (@a false) the filter */ void setFilterActive(bool state = true) { m_filterActive = state; } /** * This slot allows to activate/deactivate the filter. By default the * filter is active. * * @param state Allows to deactivate (@a true) or activate (@a false) the filter */ void setFilterDeactive(bool state = false) { setFilterActive(!state); } protected: /** * This method checks if the widget @p child is a child of * the widget @p parent and returns either @a true or @a false. * * @param child pointer to child widget * @param parent pointer to parent widget * @retval true @p child points to widget which has @p parent as parent or grand-parent * @retval false @p child points to a widget which is not related to @p parent */ bool isChildOf(QWidget* child, QWidget* parent) { // QDialogs cannot be detected directly, but it can be assumed, // that events on a widget that do not have a parent widget within // our application are dialogs. if (!child->parentWidget()) return true; while (child) { // if we are a child of the given parent, we have a match if (child == parent) return true; // if we are at the application level, we don't have a match if (child->inherits("KMyMoneyApp")) return false; // If one of the ancestors is a KPassivePopup or a KDialog or a popup widget then // it's as if it is a child of our own because these widgets could // appear during transaction entry (message boxes, completer widgets) if (dynamic_cast(child) || ((child->windowFlags() & Qt::Popup) && /*child != kmymoney*/ !child->parentWidget())) // has no parent, then it must be top-level window return true; child = child->parentWidget(); } return false; } /** * Reimplemented from base class. Sends out the mousePressedOnExternalWidget() signal * if object @p o points to an object which is not a child widget of any added previously * using the addWidget() method. The signal is sent out only once for each event @p e. * * @param o pointer to QObject * @param e pointer to QEvent * @return always returns @a false */ - bool eventFilter(QObject* o, QEvent* e) + bool eventFilter(QObject* o, QEvent* e) final override { if (m_filterActive) { if (e->type() == QEvent::MouseButtonPress && !m_lastMousePressEvent) { QWidget* w = qobject_cast(o); if (!w) { return QObject::eventFilter(o, e); } QList::const_iterator it_w; for (it_w = m_parents.constBegin(); it_w != m_parents.constEnd(); ++it_w) { if (isChildOf(w, (*it_w))) { m_lastMousePressEvent = e; break; } } if (it_w == m_parents.constEnd()) { m_lastMousePressEvent = e; bool rc = false; emit mousePressedOnExternalWidget(rc); } } if (e->type() != QEvent::MouseButtonPress) { m_lastMousePressEvent = 0; } } return false; } Q_SIGNALS: void mousePressedOnExternalWidget(bool&); private: QList m_parents; QEvent* m_lastMousePressEvent; bool m_filterActive; }; class KGlobalLedgerViewPrivate : public KMyMoneyViewBasePrivate { Q_DECLARE_PUBLIC(KGlobalLedgerView) public: explicit KGlobalLedgerViewPrivate(KGlobalLedgerView *qq) : q_ptr(qq), m_mousePressFilter(0), m_registerSearchLine(0), m_precision(2), m_recursion(false), m_showDetails(false), m_action(eWidgets::eRegister::Action::None), m_filterProxyModel(0), m_accountComboBox(0), m_balanceIsApproximated(false), m_toolbarFrame(nullptr), m_registerFrame(nullptr), m_buttonFrame(nullptr), m_formFrame(nullptr), m_summaryFrame(nullptr), m_register(nullptr), m_buttonbar(nullptr), m_leftSummaryLabel(nullptr), m_centerSummaryLabel(nullptr), m_rightSummaryLabel(nullptr), m_form(nullptr), m_needLoad(true), m_newAccountLoaded(true), m_inEditMode(false), m_transactionEditor(nullptr), m_balanceWarning(nullptr), m_moveToAccountSelector(nullptr), m_endingBalanceDlg(nullptr), m_searchDlg(nullptr) { } ~KGlobalLedgerViewPrivate() { delete m_moveToAccountSelector; delete m_endingBalanceDlg; delete m_searchDlg; } void init() { Q_Q(KGlobalLedgerView); m_needLoad = false; auto vbox = new QVBoxLayout(q); q->setLayout(vbox); vbox->setSpacing(6); vbox->setMargin(0); m_mousePressFilter = new MousePressFilter((QWidget*)q); m_action = eWidgets::eRegister::Action::None; // the proxy filter model m_filterProxyModel = new AccountNamesFilterProxyModel(q); m_filterProxyModel->addAccountGroup(QVector {eMyMoney::Account::Type::Asset, eMyMoney::Account::Type::Liability, 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); // create the toolbar frame at the top of the view m_toolbarFrame = new QFrame(); QHBoxLayout* toolbarLayout = new QHBoxLayout(m_toolbarFrame); toolbarLayout->setContentsMargins(0, 0, 0, 0); toolbarLayout->setSpacing(6); // the account selector widget m_accountComboBox = new KMyMoneyAccountCombo(); m_accountComboBox->setModel(m_filterProxyModel); toolbarLayout->addWidget(m_accountComboBox); vbox->addWidget(m_toolbarFrame); toolbarLayout->setStretchFactor(m_accountComboBox, 60); // create the register frame m_registerFrame = new QFrame(); QVBoxLayout* registerFrameLayout = new QVBoxLayout(m_registerFrame); registerFrameLayout->setContentsMargins(0, 0, 0, 0); registerFrameLayout->setSpacing(0); vbox->addWidget(m_registerFrame); vbox->setStretchFactor(m_registerFrame, 2); m_register = new KMyMoneyRegister::Register(m_registerFrame); m_register->setUsedWithEditor(true); registerFrameLayout->addWidget(m_register); m_register->installEventFilter(q); q->connect(m_register, &KMyMoneyRegister::Register::openContextMenu, q, &KGlobalLedgerView::slotTransactionsContextMenuRequested); q->connect(m_register, &KMyMoneyRegister::Register::transactionsSelected, q, &KGlobalLedgerView::slotUpdateSummaryLine); q->connect(m_register->horizontalHeader(), &QWidget::customContextMenuRequested, q, &KGlobalLedgerView::slotSortOptions); q->connect(m_register, &KMyMoneyRegister::Register::reconcileStateColumnClicked, q, &KGlobalLedgerView::slotToggleTransactionMark); // insert search line widget m_registerSearchLine = new KMyMoneyRegister::RegisterSearchLineWidget(m_register, m_toolbarFrame); toolbarLayout->addWidget(m_registerSearchLine); toolbarLayout->setStretchFactor(m_registerSearchLine, 100); // create the summary frame m_summaryFrame = new QFrame(); QHBoxLayout* summaryFrameLayout = new QHBoxLayout(m_summaryFrame); summaryFrameLayout->setContentsMargins(0, 0, 0, 0); summaryFrameLayout->setSpacing(0); m_leftSummaryLabel = new QLabel(m_summaryFrame); m_centerSummaryLabel = new QLabel(m_summaryFrame); m_rightSummaryLabel = new QLabel(m_summaryFrame); summaryFrameLayout->addWidget(m_leftSummaryLabel); QSpacerItem* spacer = new QSpacerItem(20, 1, QSizePolicy::Expanding, QSizePolicy::Minimum); summaryFrameLayout->addItem(spacer); summaryFrameLayout->addWidget(m_centerSummaryLabel); spacer = new QSpacerItem(20, 1, QSizePolicy::Expanding, QSizePolicy::Minimum); summaryFrameLayout->addItem(spacer); summaryFrameLayout->addWidget(m_rightSummaryLabel); vbox->addWidget(m_summaryFrame); // create the button frame m_buttonFrame = new QFrame(q); QVBoxLayout* buttonLayout = new QVBoxLayout(m_buttonFrame); buttonLayout->setContentsMargins(0, 0, 0, 0); buttonLayout->setSpacing(0); vbox->addWidget(m_buttonFrame); m_buttonbar = new KToolBar(m_buttonFrame, 0, true); m_buttonbar->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); buttonLayout->addWidget(m_buttonbar); m_buttonbar->addAction(pActions[eMenu::Action::NewTransaction]); m_buttonbar->addAction(pActions[eMenu::Action::DeleteTransaction]); m_buttonbar->addAction(pActions[eMenu::Action::EditTransaction]); m_buttonbar->addAction(pActions[eMenu::Action::EnterTransaction]); m_buttonbar->addAction(pActions[eMenu::Action::CancelTransaction]); m_buttonbar->addAction(pActions[eMenu::Action::AcceptTransaction]); m_buttonbar->addAction(pActions[eMenu::Action::MatchTransaction]); // create the transaction form frame m_formFrame = new QFrame(q); QVBoxLayout* frameLayout = new QVBoxLayout(m_formFrame); frameLayout->setContentsMargins(5, 5, 5, 5); frameLayout->setSpacing(0); m_form = new KMyMoneyTransactionForm::TransactionForm(m_formFrame); frameLayout->addWidget(m_form->getTabBar(m_formFrame)); frameLayout->addWidget(m_form); m_formFrame->setFrameShape(QFrame::Panel); m_formFrame->setFrameShadow(QFrame::Raised); vbox->addWidget(m_formFrame); q->connect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, q, &KGlobalLedgerView::refresh); q->connect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, q, &KGlobalLedgerView::slotUpdateMoveToAccountMenu); q->connect(m_register, static_cast(&KMyMoneyRegister::Register::focusChanged), m_form, &KMyMoneyTransactionForm::TransactionForm::slotSetTransaction); q->connect(m_register, static_cast(&KMyMoneyRegister::Register::focusChanged), q, &KGlobalLedgerView::updateLedgerActionsInternal); // q->connect(m_accountComboBox, &KMyMoneyAccountCombo::accountSelected, q, &KGlobalLedgerView::slotAccountSelected); q->connect(m_accountComboBox, &KMyMoneyAccountCombo::accountSelected, q, static_cast(&KGlobalLedgerView::slotSelectAccount)); q->connect(m_accountComboBox, &KMyMoneyAccountCombo::accountSelected, q, &KGlobalLedgerView::slotUpdateMoveToAccountMenu); q->connect(m_register, &KMyMoneyRegister::Register::transactionsSelected, q, &KGlobalLedgerView::slotTransactionsSelected); q->connect(m_register, &KMyMoneyRegister::Register::transactionsSelected, q, &KGlobalLedgerView::slotUpdateMoveToAccountMenu); q->connect(m_register, &KMyMoneyRegister::Register::editTransaction, q, &KGlobalLedgerView::slotEditTransaction); q->connect(m_register, &KMyMoneyRegister::Register::emptyItemSelected, q, &KGlobalLedgerView::slotNewTransaction); q->connect(m_register, &KMyMoneyRegister::Register::aboutToSelectItem, q, &KGlobalLedgerView::slotAboutToSelectItem); q->connect(m_mousePressFilter, &MousePressFilter::mousePressedOnExternalWidget, q, &KGlobalLedgerView::slotCancelOrEnterTransactions); q->connect(m_form, &KMyMoneyTransactionForm::TransactionForm::newTransaction, q, static_cast(&KGlobalLedgerView::slotNewTransactionForm)); // setup mouse press filter m_mousePressFilter->addWidget(m_formFrame); m_mousePressFilter->addWidget(m_buttonFrame); m_mousePressFilter->addWidget(m_summaryFrame); m_mousePressFilter->addWidget(m_registerFrame); m_tooltipPosn = QPoint(); } /** * This method reloads the account selection combo box of the * view with all asset and liability accounts from the engine. * If the account id of the current account held in @p m_accountId is * empty or if the referenced account does not exist in the engine, * the first account found in the list will be made the current account. */ void loadAccounts() { const auto file = MyMoneyFile::instance(); // check if the current account still exists and make it the // current account if (!m_lastSelectedAccountID.isEmpty()) { try { m_currentAccount = file->account(m_lastSelectedAccountID); } catch (const MyMoneyException &) { m_lastSelectedAccountID.clear(); m_currentAccount = MyMoneyAccount(); return; } } // TODO: check why the invalidate is needed here m_filterProxyModel->invalidate(); m_filterProxyModel->sort((int)eAccountsModel::Column::Account); m_filterProxyModel->setHideClosedAccounts(KMyMoneySettings::hideClosedAccounts() && !KMyMoneySettings::showAllAccounts()); m_filterProxyModel->setHideEquityAccounts(!KMyMoneySettings::expertMode()); m_accountComboBox->expandAll(); if (m_currentAccount.id().isEmpty()) { // find the first favorite account QModelIndexList list = m_filterProxyModel->match(m_filterProxyModel->index(0, 0), (int)eAccountsModel::Role::Favorite, QVariant(true), 1, Qt::MatchFlags(Qt::MatchExactly | Qt::MatchCaseSensitive | Qt::MatchRecursive)); if (list.count() > 0) { QVariant accountId = list.front().data((int)eAccountsModel::Role::ID); if (accountId.isValid()) { m_currentAccount = file->account(accountId.toString()); } } if (m_currentAccount.id().isEmpty()) { // there are no favorite accounts find any account - QModelIndexList list = m_filterProxyModel->match(m_filterProxyModel->index(0, 0), + list = m_filterProxyModel->match(m_filterProxyModel->index(0, 0), Qt::DisplayRole, QVariant(QString("*")), -1, Qt::MatchFlags(Qt::MatchWildcard | Qt::MatchRecursive)); for (QModelIndexList::ConstIterator it = list.constBegin(); it != list.constEnd(); ++it) { if (!it->parent().isValid()) continue; // skip the top level accounts QVariant accountId = (*it).data((int)eAccountsModel::Role::ID); if (accountId.isValid()) { MyMoneyAccount a = file->account(accountId.toString()); if (!a.isInvest() && !a.isClosed()) { m_currentAccount = a; break; } } } } } if (!m_currentAccount.id().isEmpty()) { m_accountComboBox->setSelected(m_currentAccount.id()); try { m_precision = MyMoneyMoney::denomToPrec(m_currentAccount.fraction()); } catch (const MyMoneyException &) { qDebug("Security %s for account %s not found", qPrintable(m_currentAccount.currencyId()), qPrintable(m_currentAccount.name())); m_precision = 2; } } } /** * This method clears the register, form, transaction list. See @sa m_register, * @sa m_transactionList */ void clear() { // clear current register contents m_register->clear(); // setup header font QFont font = KMyMoneySettings::listHeaderFontEx(); QFontMetrics fm(font); int height = fm.lineSpacing() + 6; m_register->horizontalHeader()->setMinimumHeight(height); m_register->horizontalHeader()->setMaximumHeight(height); m_register->horizontalHeader()->setFont(font); // setup cell font font = KMyMoneySettings::listCellFontEx(); m_register->setFont(font); // clear the form m_form->clear(); // the selected transactions list m_transactionList.clear(); // and the selected account in the combo box m_accountComboBox->setSelected(QString()); // fraction defaults to two digits m_precision = 2; } void loadView() { MYMONEYTRACER(tracer); Q_Q(KGlobalLedgerView); // setup form visibility m_formFrame->setVisible(KMyMoneySettings::transactionForm()); // no account selected // emit q->objectSelected(MyMoneyAccount()); // no transaction selected KMyMoneyRegister::SelectedTransactions list; emit q->selectByVariant(QVariantList {QVariant::fromValue(list)}, eView::Intent::SelectRegisterTransactions); QMap isSelected; QString focusItemId; QString backUpFocusItemId; // in case the focus item is removed QString anchorItemId; QString backUpAnchorItemId; // in case the anchor item is removed if (!m_newAccountLoaded) { // remember the current selected transactions KMyMoneyRegister::RegisterItem* item = m_register->firstItem(); for (; item; item = item->nextItem()) { if (item->isSelected()) { isSelected[item->id()] = true; } } // remember the item that has the focus storeId(m_register->focusItem(), focusItemId, backUpFocusItemId); // and the one that has the selection anchor storeId(m_register->anchorItem(), anchorItemId, backUpAnchorItemId); } else { m_registerSearchLine->searchLine()->clear(); } // clear the current contents ... clear(); // ... load the combobox widget and select current account ... loadAccounts(); // ... setup the register columns ... m_register->setupRegister(m_currentAccount); // ... setup the form ... m_form->setupForm(m_currentAccount); if (m_currentAccount.id().isEmpty()) { // if we don't have an account we bail out q->setEnabled(false); return; } q->setEnabled(true); m_register->setUpdatesEnabled(false); // ... and recreate it KMyMoneyRegister::RegisterItem* focusItem = 0; KMyMoneyRegister::RegisterItem* anchorItem = 0; QMap actBalance, clearedBalance, futureBalance; QMap::iterator it_b; try { // setup the filter to select the transactions we want to display // and update the sort order QString sortOrder; QString key; QDate reconciliationDate = m_reconciliationDate; MyMoneyTransactionFilter filter(m_currentAccount.id()); // if it's an investment account, we also take care of // the sub-accounts (stock accounts) if (m_currentAccount.accountType() == eMyMoney::Account::Type::Investment) filter.addAccount(m_currentAccount.accountList()); if (isReconciliationAccount()) { key = "kmm-sort-reconcile"; sortOrder = KMyMoneySettings::sortReconcileView(); filter.addState((int)eMyMoney::TransactionFilter::State::NotReconciled); filter.addState((int)eMyMoney::TransactionFilter::State::Cleared); } else { filter.setDateFilter(KMyMoneySettings::startDate().date(), QDate()); key = "kmm-sort-std"; sortOrder = KMyMoneySettings::sortNormalView(); if (KMyMoneySettings::hideReconciledTransactions() && !m_currentAccount.isIncomeExpense()) { filter.addState((int)eMyMoney::TransactionFilter::State::NotReconciled); filter.addState((int)eMyMoney::TransactionFilter::State::Cleared); } } filter.setReportAllSplits(true); // check if we have an account override of the sort order if (!m_currentAccount.value(key).isEmpty()) sortOrder = m_currentAccount.value(key); // setup sort order m_register->setSortOrder(sortOrder); // retrieve the list from the engine MyMoneyFile::instance()->transactionList(m_transactionList, filter); emit q->slotStatusProgress(0, m_transactionList.count()); // create the elements for the register QList >::const_iterator it; QMapuniqueMap; int i = 0; for (it = m_transactionList.constBegin(); it != m_transactionList.constEnd(); ++it) { uniqueMap[(*it).first.id()]++; KMyMoneyRegister::Transaction* t = KMyMoneyRegister::Register::transactionFactory(m_register, (*it).first, (*it).second, uniqueMap[(*it).first.id()]); actBalance[t->split().accountId()] = MyMoneyMoney(); emit q->slotStatusProgress(++i, 0); // if we're in reconciliation and the state is cleared, we // force the item to show in dimmed intensity to get a visual focus // on those items, that we need to work on if (isReconciliationAccount() && (*it).second.reconcileFlag() == eMyMoney::Split::State::Cleared) { t->setReducedIntensity(true); } } // create dummy entries for the scheduled transactions if sorted by postdate int period = KMyMoneySettings::schedulePreview(); if (m_register->primarySortKey() == eWidgets::SortField::PostDate) { // show scheduled transactions which have a scheduled postdate // within the next 'period' days. In reconciliation mode, the // period starts on the statement date. QDate endDate = QDate::currentDate().addDays(period); if (isReconciliationAccount()) endDate = reconciliationDate.addDays(period); QList scheduleList = MyMoneyFile::instance()->scheduleList(m_currentAccount.id()); while (!scheduleList.isEmpty()) { MyMoneySchedule& s = scheduleList.first(); for (;;) { if (s.isFinished() || s.adjustedNextDueDate() > endDate) { break; } MyMoneyTransaction t(s.id(), KMyMoneyUtils::scheduledTransaction(s)); // if the transaction is scheduled and overdue, it can't // certainly be posted in the past. So we take today's date // as the alternative if (s.isOverdue()) { t.setPostDate(s.adjustedDate(QDate::currentDate(), s.weekendOption())); } else { t.setPostDate(s.adjustedNextDueDate()); } foreach (const auto split, t.splits()) { if (split.accountId() == m_currentAccount.id()) { new KMyMoneyRegister::StdTransactionScheduled(m_register, t, split, uniqueMap[t.id()]); } } // keep track of this payment locally (not in the engine) if (s.isOverdue()) { s.setLastPayment(QDate::currentDate()); } else { s.setLastPayment(s.nextDueDate()); } // if this is a one time schedule, we can bail out here as we're done if (s.occurrence() == eMyMoney::Schedule::Occurrence::Once) break; // for all others, we check if the next payment date is still 'in range' QDate nextDueDate = s.nextPayment(s.nextDueDate()); if (nextDueDate.isValid()) { s.setNextDueDate(nextDueDate); } else { break; } } scheduleList.pop_front(); } } // add the group markers m_register->addGroupMarkers(); // sort the transactions according to the sort setting m_register->sortItems(); // remove trailing and adjacent markers m_register->removeUnwantedGroupMarkers(); // add special markers for reconciliation now so that they do not get // removed by m_register->removeUnwantedGroupMarkers(). Needs resorting // of items but that's ok. KMyMoneyRegister::StatementGroupMarker* statement = 0; KMyMoneyRegister::StatementGroupMarker* dStatement = 0; KMyMoneyRegister::StatementGroupMarker* pStatement = 0; if (isReconciliationAccount()) { switch (m_register->primarySortKey()) { case eWidgets::SortField::PostDate: statement = new KMyMoneyRegister::StatementGroupMarker(m_register, eWidgets::eRegister::CashFlowDirection::Deposit, reconciliationDate, i18n("Statement Details")); m_register->sortItems(); break; case eWidgets::SortField::Type: dStatement = new KMyMoneyRegister::StatementGroupMarker(m_register, eWidgets::eRegister::CashFlowDirection::Deposit, reconciliationDate, i18n("Statement Deposit Details")); pStatement = new KMyMoneyRegister::StatementGroupMarker(m_register, eWidgets::eRegister::CashFlowDirection::Payment, reconciliationDate, i18n("Statement Payment Details")); m_register->sortItems(); break; default: break; } } // we need at least the balance for the account we currently show actBalance[m_currentAccount.id()] = MyMoneyMoney(); if (m_currentAccount.accountType() == eMyMoney::Account::Type::Investment) foreach (const auto accountID, m_currentAccount.accountList()) actBalance[accountID] = MyMoneyMoney(); // determine balances (actual, cleared). We do this by getting the actual // balance of all entered transactions from the engine and walk the list // of transactions backward. Also re-select a transaction if it was // selected before and setup the focus item. MyMoneyMoney factor(1, 1); if (m_currentAccount.accountGroup() == eMyMoney::Account::Type::Liability || m_currentAccount.accountGroup() == eMyMoney::Account::Type::Equity) factor = -factor; QMap deposits; QMap payments; QMap depositAmount; QMap paymentAmount; for (it_b = actBalance.begin(); it_b != actBalance.end(); ++it_b) { MyMoneyMoney balance = MyMoneyFile::instance()->balance(it_b.key()); balance = balance * factor; clearedBalance[it_b.key()] = futureBalance[it_b.key()] = (*it_b) = balance; deposits[it_b.key()] = payments[it_b.key()] = 0; depositAmount[it_b.key()] = MyMoneyMoney(); paymentAmount[it_b.key()] = MyMoneyMoney(); } tracer.printf("total balance of %s = %s", qPrintable(m_currentAccount.name()), qPrintable(actBalance[m_currentAccount.id()].formatMoney("", 2))); tracer.printf("future balance of %s = %s", qPrintable(m_currentAccount.name()), qPrintable(futureBalance[m_currentAccount.id()].formatMoney("", 2))); tracer.printf("cleared balance of %s = %s", qPrintable(m_currentAccount.name()), qPrintable(clearedBalance[m_currentAccount.id()].formatMoney("", 2))); KMyMoneyRegister::RegisterItem* p = m_register->lastItem(); focusItem = 0; // take care of possibly trailing scheduled transactions (bump up the future balance) while (p) { if (p->isSelectable()) { KMyMoneyRegister::Transaction* t = dynamic_cast(p); if (t && t->isScheduled()) { MyMoneyMoney balance = futureBalance[t->split().accountId()]; const MyMoneySplit& split = t->split(); // if this split is a stock split, we can't just add the amount of shares if (t->transaction().isStockSplit()) { balance = balance * split.shares(); } else { balance += split.shares() * factor; } futureBalance[split.accountId()] = balance; } else if (t && !focusItem) focusItem = p; } p = p->prevItem(); } p = m_register->lastItem(); while (p) { KMyMoneyRegister::Transaction* t = dynamic_cast(p); if (t) { if (isSelected.contains(t->id())) t->setSelected(true); matchItemById(&focusItem, t, focusItemId, backUpFocusItemId); matchItemById(&anchorItem, t, anchorItemId, backUpAnchorItemId); const MyMoneySplit& split = t->split(); MyMoneyMoney balance = futureBalance[split.accountId()]; t->setBalance(balance); // if this split is a stock split, we can't just add the amount of shares if (t->transaction().isStockSplit()) { balance /= split.shares(); } else { balance -= split.shares() * factor; } if (!t->isScheduled()) { if (isReconciliationAccount() && t->transaction().postDate() <= reconciliationDate && split.reconcileFlag() == eMyMoney::Split::State::Cleared) { if (split.shares().isNegative()) { payments[split.accountId()]++; paymentAmount[split.accountId()] += split.shares(); } else { deposits[split.accountId()]++; depositAmount[split.accountId()] += split.shares(); } } if (t->transaction().postDate() > QDate::currentDate()) { tracer.printf("Reducing actual balance by %s because %s/%s(%s) is in the future", qPrintable((split.shares() * factor).formatMoney("", 2)), qPrintable(t->transaction().id()), qPrintable(split.id()), qPrintable(t->transaction().postDate().toString(Qt::ISODate))); actBalance[split.accountId()] -= split.shares() * factor; } } futureBalance[split.accountId()] = balance; } p = p->prevItem(); } clearedBalance[m_currentAccount.id()] = MyMoneyFile::instance()->clearedBalance(m_currentAccount.id(), reconciliationDate); tracer.printf("total balance of %s = %s", qPrintable(m_currentAccount.name()), qPrintable(actBalance[m_currentAccount.id()].formatMoney("", 2))); tracer.printf("future balance of %s = %s", qPrintable(m_currentAccount.name()), qPrintable(futureBalance[m_currentAccount.id()].formatMoney("", 2))); tracer.printf("cleared balance of %s = %s", qPrintable(m_currentAccount.name()), qPrintable(clearedBalance[m_currentAccount.id()].formatMoney("", 2))); // update statement information if (statement) { const QString aboutDeposits = i18np("%1 deposit (%2)", "%1 deposits (%2)", deposits[m_currentAccount.id()], depositAmount[m_currentAccount.id()].abs().formatMoney(m_currentAccount.fraction())); const QString aboutPayments = i18np("%1 payment (%2)", "%1 payments (%2)", payments[m_currentAccount.id()], paymentAmount[m_currentAccount.id()].abs().formatMoney(m_currentAccount.fraction())); statement->setText(i18nc("%1 is a string, e.g. 7 deposits; %2 is a string, e.g. 4 payments", "%1, %2", aboutDeposits, aboutPayments)); } if (pStatement) { pStatement->setText(i18np("%1 payment (%2)", "%1 payments (%2)", payments[m_currentAccount.id()] , paymentAmount[m_currentAccount.id()].abs().formatMoney(m_currentAccount.fraction()))); } if (dStatement) { dStatement->setText(i18np("%1 deposit (%2)", "%1 deposits (%2)", deposits[m_currentAccount.id()] , depositAmount[m_currentAccount.id()].abs().formatMoney(m_currentAccount.fraction()))); } // add a last empty entry for new transactions // leave some information about the current account MyMoneySplit split; split.setReconcileFlag(eMyMoney::Split::State::NotReconciled); // make sure to use the value specified in the option during reconciliation if (isReconciliationAccount()) split.setReconcileFlag(static_cast(KMyMoneySettings::defaultReconciliationState())); KMyMoneyRegister::Register::transactionFactory(m_register, MyMoneyTransaction(), split, 0); m_register->updateRegister(true); if (focusItem) { // in case we have some selected items we just set the focus item // in other cases, we make the focusitem also the selected item if (anchorItem && (anchorItem != focusItem)) { m_register->setFocusItem(focusItem); m_register->setAnchorItem(anchorItem); } else m_register->selectItem(focusItem, true); } else { // just use the empty line at the end if nothing else exists in the ledger p = m_register->lastItem(); m_register->setFocusItem(p); m_register->selectItem(p); focusItem = p; } updateSummaryLine(actBalance, clearedBalance); emit q->slotStatusProgress(-1, -1); } catch (const MyMoneyException &) { m_currentAccount = MyMoneyAccount(); clear(); } m_showDetails = KMyMoneySettings::showRegisterDetailed(); // and tell everyone what's selected emit q->selectByObject(m_currentAccount, eView::Intent::None); KMyMoneyRegister::SelectedTransactions actualSelection(m_register); emit q->selectByVariant(QVariantList {QVariant::fromValue(actualSelection)}, eView::Intent::SelectRegisterTransactions); } void selectTransaction(const QString& id) { if (!id.isEmpty()) { KMyMoneyRegister::RegisterItem* p = m_register->lastItem(); while (p) { KMyMoneyRegister::Transaction* t = dynamic_cast(p); if (t) { if (t->transaction().id() == id) { m_register->selectItem(t); m_register->ensureItemVisible(t); break; } } p = p->prevItem(); } } } /** * @brief selects transactions for processing with slots * @param list of transactions * @return false if only schedule is to be selected */ bool selectTransactions(const KMyMoneyRegister::SelectedTransactions& list) { Q_Q(KGlobalLedgerView); // list can either contain a list of transactions or a single selected scheduled transaction // in the latter case, the transaction id is actually the one of the schedule. In order // to differentiate between the two, we just ask for the schedule. If we don't find one - because // we passed the id of a real transaction - then we know that fact. We use the schedule here, // because the list of schedules is kept in a cache by MyMoneyFile. This way, we save some trips // to the backend which we would have to do if we check for the transaction. m_selectedTransactions.clear(); auto sch = MyMoneySchedule(); auto ret = true; m_accountGoto.clear(); m_payeeGoto.clear(); if (!list.isEmpty() && !list.first().isScheduled()) { m_selectedTransactions = list; if (list.count() == 1) { const MyMoneySplit& sp = m_selectedTransactions[0].split(); if (!sp.payeeId().isEmpty()) { try { auto payee = MyMoneyFile::instance()->payee(sp.payeeId()); if (!payee.name().isEmpty()) { m_payeeGoto = payee.id(); auto name = payee.name(); name.replace(QRegExp("&(?!&)"), "&&"); pActions[Action::GoToPayee]->setText(i18n("Go to '%1'", name)); } } catch (const MyMoneyException &) { } } try { const auto& t = m_selectedTransactions[0].transaction(); // search the first non-income/non-expense accunt and use it for the 'goto account' - const auto& sp = m_selectedTransactions[0].split(); + const auto& selectedTransactionSplit = m_selectedTransactions[0].split(); foreach (const auto split, t.splits()) { - if (split.id() != sp.id()) { + if (split.id() != selectedTransactionSplit.id()) { auto acc = MyMoneyFile::instance()->account(split.accountId()); if (!acc.isIncomeExpense()) { // for stock accounts we show the portfolio account if (acc.isInvest()) { acc = MyMoneyFile::instance()->account(acc.parentAccountId()); } m_accountGoto = acc.id(); auto name = acc.name(); name.replace(QRegExp("&(?!&)"), "&&"); pActions[Action::GoToAccount]->setText(i18n("Go to '%1'", name)); break; } } } } catch (const MyMoneyException &) { } } } else if (!list.isEmpty()) { sch = MyMoneyFile::instance()->schedule(list.first().scheduleId()); m_selectedTransactions.append(list.first()); ret = false; } emit q->selectByObject(sch, eView::Intent::None); // make sure, we show some neutral menu entry if we don't have an object if (m_payeeGoto.isEmpty()) pActions[Action::GoToPayee]->setText(i18n("Go to payee")); if (m_accountGoto.isEmpty()) pActions[Action::GoToAccount]->setText(i18n("Go to account")); return ret; } /** * Returns @a true if setReconciliationAccount() has been called for * the current loaded account. * * @retval true current account is in reconciliation mode * @retval false current account is not in reconciliation mode */ bool isReconciliationAccount() const { return m_currentAccount.id() == m_reconciliationAccount.id(); } /** * Updates the values on the summary line beneath the register with * the given values. The contents shown differs between reconciliation * mode and normal mode. * * @param actBalance map of account indexed values to be used as actual balance * @param clearedBalance map of account indexed values to be used as cleared balance */ void updateSummaryLine(const QMap& actBalance, const QMap& clearedBalance) { Q_Q(KGlobalLedgerView); const auto file = MyMoneyFile::instance(); m_leftSummaryLabel->show(); m_centerSummaryLabel->show(); m_rightSummaryLabel->show(); if (isReconciliationAccount()) { if (m_currentAccount.accountType() != eMyMoney::Account::Type::Investment) { m_leftSummaryLabel->setText(i18n("Statement: %1", m_endingBalance.formatMoney("", m_precision))); m_centerSummaryLabel->setText(i18nc("Cleared balance", "Cleared: %1", clearedBalance[m_currentAccount.id()].formatMoney("", m_precision))); m_totalBalance = clearedBalance[m_currentAccount.id()] - m_endingBalance; } } else { // update summary line in normal mode QDate reconcileDate = m_currentAccount.lastReconciliationDate(); if (reconcileDate.isValid()) { m_leftSummaryLabel->setText(i18n("Last reconciled: %1", QLocale().toString(reconcileDate, QLocale::ShortFormat))); } else { m_leftSummaryLabel->setText(i18n("Never reconciled")); } QPalette palette = m_rightSummaryLabel->palette(); palette.setColor(m_rightSummaryLabel->foregroundRole(), m_leftSummaryLabel->palette().color(q->foregroundRole())); if (m_currentAccount.accountType() != eMyMoney::Account::Type::Investment) { m_centerSummaryLabel->setText(i18nc("Cleared balance", "Cleared: %1", clearedBalance[m_currentAccount.id()].formatMoney("", m_precision))); m_totalBalance = actBalance[m_currentAccount.id()]; } else { m_centerSummaryLabel->hide(); MyMoneyMoney balance; MyMoneySecurity base = file->baseCurrency(); QMap::const_iterator it_b; // reset the approximated flag m_balanceIsApproximated = false; for (it_b = actBalance.begin(); it_b != actBalance.end(); ++it_b) { MyMoneyAccount stock = file->account(it_b.key()); QString currencyId = stock.currencyId(); MyMoneySecurity sec = file->security(currencyId); MyMoneyMoney rate(1, 1); if (stock.isInvest()) { currencyId = sec.tradingCurrency(); const MyMoneyPrice &priceInfo = file->price(sec.id(), currencyId); m_balanceIsApproximated |= !priceInfo.isValid(); rate = priceInfo.rate(sec.tradingCurrency()); } if (currencyId != base.id()) { const MyMoneyPrice &priceInfo = file->price(sec.tradingCurrency(), base.id()); m_balanceIsApproximated |= !priceInfo.isValid(); rate = (rate * priceInfo.rate(base.id())).convertPrecision(sec.pricePrecision()); } balance += ((*it_b) * rate).convert(base.smallestAccountFraction()); } m_totalBalance = balance; } m_rightSummaryLabel->setPalette(palette); } // determine the number of selected transactions KMyMoneyRegister::SelectedTransactions selection; m_register->selectedTransactions(selection); q->slotUpdateSummaryLine(selection); } /** * setup the default action according to the current account type */ void setupDefaultAction() { switch (m_currentAccount.accountType()) { case eMyMoney::Account::Type::Asset: case eMyMoney::Account::Type::AssetLoan: case eMyMoney::Account::Type::Savings: m_action = eWidgets::eRegister::Action::Deposit; break; default: m_action = eWidgets::eRegister::Action::Withdrawal; break; } } // used to store the id of an item and the id of an immeadiate unselected sibling void storeId(KMyMoneyRegister::RegisterItem *item, QString &id, QString &backupId) { if (item) { // the id of the item id = item->id(); // the id of the item's previous/next unselected item for (KMyMoneyRegister::RegisterItem *it = item->prevItem(); it != 0 && backupId.isEmpty(); it = it->prevItem()) { if (!it->isSelected()) { backupId = it->id(); } } // if we didn't found previous unselected items search trough the next items for (KMyMoneyRegister::RegisterItem *it = item->nextItem(); it != 0 && backupId.isEmpty(); it = it->nextItem()) { if (!it->isSelected()) { backupId = it->id(); } } } } // use to match an item by it's id or a backup id which has a lower precedence void matchItemById(KMyMoneyRegister::RegisterItem **item, KMyMoneyRegister::Transaction* t, QString &id, QString &backupId) { if (!backupId.isEmpty() && t->id() == backupId) *item = t; if (!id.isEmpty() && t->id() == id) { // we found the real thing there's no need for the backup anymore backupId.clear(); *item = t; } } bool canProcessTransactions(const KMyMoneyRegister::SelectedTransactions& list, QString& tooltip) const { MyMoneyAccount acc; QString closedAccount; if (m_register->focusItem() == 0) return false; // bool rc = true; if (list.warnLevel() == 3) { //Closed account somewhere auto it_t = list.first(); // for (auto it_t = list.cbegin(); rc && it_t != list.cend(); ++it_t) { auto splitList = it_t.transaction().splits(); auto id = splitList.first().accountId(); acc = MyMoneyFile::instance()->account(id); if (!acc.isClosed()) { //wrong split, try other id = splitList.last().accountId(); acc = MyMoneyFile::instance()->account(id); } closedAccount = acc.name(); // break; // } tooltip = i18n("Cannot process transactions in account %1, which is closed.", closedAccount); showTooltip(tooltip); return false; } if (!m_register->focusItem()->isSelected()) { tooltip = i18n("Cannot process transaction with focus if it is not selected."); showTooltip(tooltip); return false; } tooltip.clear(); return !list.isEmpty(); } void showTooltip(const QString msg) const { QToolTip::showText(m_tooltipPosn, msg); } bool createNewTransaction() { Q_Q(KGlobalLedgerView); auto rc = false; QString txt; if (q->canCreateTransactions(txt)) { rc = q->selectEmptyTransaction(); } return rc; } TransactionEditor* startEdit(const KMyMoneyRegister::SelectedTransactions& list) { Q_Q(KGlobalLedgerView); TransactionEditor* editor = 0; QString txt; if (q->canEditTransactions(list, txt) || q->canCreateTransactions(txt)) { editor = q->startEdit(list); } return editor; } void doDeleteTransactions() { Q_Q(KGlobalLedgerView); KMyMoneyRegister::SelectedTransactions list = m_selectedTransactions; KMyMoneyRegister::SelectedTransactions::iterator it_t; int cnt = list.count(); int i = 0; emit q->slotStatusProgress(0, cnt); MyMoneyFileTransaction ft; const auto file = MyMoneyFile::instance(); try { it_t = list.begin(); while (it_t != list.end()) { // only remove those transactions that do not reference a closed account if (!file->referencesClosedAccount((*it_t).transaction())) { file->removeTransaction((*it_t).transaction()); // remove all those references in the list of selected transactions // that refer to the same transaction we just removed so that we // will not be caught by an exception later on (see bko #285310) KMyMoneyRegister::SelectedTransactions::iterator it_td = it_t; ++it_td; while (it_td != list.end()) { if ((*it_t).transaction().id() == (*it_td).transaction().id()) { it_td = list.erase(it_td); i++; // bump count of deleted transactions } else { ++it_td; } } } // need to ensure "nextCheckNumber" is still correct auto acc = file->account((*it_t).split().accountId()); // the "lastNumberUsed" might have been the txn number deleted // so adjust it QString deletedNum = (*it_t).split().number(); // decrement deletedNum and set new "lastNumberUsed" QString num = KMyMoneyUtils::getAdjacentNumber(deletedNum, -1); acc.setValue("lastNumberUsed", num); file->modifyAccount(acc); list.erase(it_t); it_t = list.begin(); emit q->slotStatusProgress(i++, 0); } ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(q, i18n("Error"), i18n("Unable to delete transaction(s): %1, thrown in %2:%3", e.what(), e.file(), e.line())); } emit q->slotStatusProgress(-1, -1); } void deleteTransactionEditor() { // make sure, we don't use the transaction editor pointer // anymore from now on auto p = m_transactionEditor; m_transactionEditor = nullptr; delete p; } void transactionUnmatch() { Q_Q(KGlobalLedgerView); KMyMoneyRegister::SelectedTransactions::const_iterator it; MyMoneyFileTransaction ft; try { for (it = m_selectedTransactions.constBegin(); it != m_selectedTransactions.constEnd(); ++it) { if ((*it).split().isMatched()) { TransactionMatcher matcher(m_currentAccount); matcher.unmatch((*it).transaction(), (*it).split()); } } ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(q, i18n("Unable to unmatch the selected transactions"), e.what()); } } void transactionMatch() { Q_Q(KGlobalLedgerView); if (m_selectedTransactions.count() != 2) return; MyMoneyTransaction startMatchTransaction; MyMoneyTransaction endMatchTransaction; MyMoneySplit startSplit; MyMoneySplit endSplit; KMyMoneyRegister::SelectedTransactions::const_iterator it; KMyMoneyRegister::SelectedTransactions toBeDeleted; for (it = m_selectedTransactions.constBegin(); it != m_selectedTransactions.constEnd(); ++it) { if ((*it).transaction().isImported()) { if (endMatchTransaction.id().isEmpty()) { endMatchTransaction = (*it).transaction(); endSplit = (*it).split(); toBeDeleted << *it; } else { //This is a second imported transaction, we still want to merge startMatchTransaction = (*it).transaction(); startSplit = (*it).split(); } } else if (!(*it).split().isMatched()) { if (startMatchTransaction.id().isEmpty()) { startMatchTransaction = (*it).transaction(); startSplit = (*it).split(); } else { endMatchTransaction = (*it).transaction(); endSplit = (*it).split(); toBeDeleted << *it; } } } #if 0 KMergeTransactionsDlg dlg(m_selectedAccount); dlg.addTransaction(startMatchTransaction); dlg.addTransaction(endMatchTransaction); if (dlg.exec() == QDialog::Accepted) #endif { MyMoneyFileTransaction ft; try { if (startMatchTransaction.id().isEmpty()) throw MYMONEYEXCEPTION(i18n("No manually entered transaction selected for matching")); if (endMatchTransaction.id().isEmpty()) throw MYMONEYEXCEPTION(i18n("No imported transaction selected for matching")); TransactionMatcher matcher(m_currentAccount); matcher.match(startMatchTransaction, startSplit, endMatchTransaction, endSplit, true); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(q, i18n("Unable to match the selected transactions"), e.what()); } } } /** * Mark the selected transactions as provided by @a flag. If * flag is @a MyMoneySplit::Unknown, the future state depends * on the current stat of the split's flag accoring to the * following table: * * - NotReconciled --> Cleared * - Cleared --> Reconciled * - Reconciled --> NotReconciled */ void markTransaction(eMyMoney::Split::State flag) { Q_Q(KGlobalLedgerView); auto list = m_selectedTransactions; KMyMoneyRegister::SelectedTransactions::const_iterator it_t; auto cnt = list.count(); auto i = 0; emit q->slotStatusProgress(0, cnt); MyMoneyFileTransaction ft; try { for (it_t = list.constBegin(); it_t != list.constEnd(); ++it_t) { // turn on signals before we modify the last entry in the list cnt--; MyMoneyFile::instance()->blockSignals(cnt != 0); // get a fresh copy auto t = MyMoneyFile::instance()->transaction((*it_t).transaction().id()); auto sp = t.splitById((*it_t).split().id()); if (sp.reconcileFlag() != flag) { if (flag == eMyMoney::Split::State::Unknown) { if (m_reconciliationAccount.id().isEmpty()) { // in normal mode we cycle through all states switch (sp.reconcileFlag()) { case eMyMoney::Split::State::NotReconciled: sp.setReconcileFlag(eMyMoney::Split::State::Cleared); break; case eMyMoney::Split::State::Cleared: sp.setReconcileFlag(eMyMoney::Split::State::Reconciled); break; case eMyMoney::Split::State::Reconciled: sp.setReconcileFlag(eMyMoney::Split::State::NotReconciled); break; default: break; } } else { // in reconciliation mode we skip the reconciled state switch (sp.reconcileFlag()) { case eMyMoney::Split::State::NotReconciled: sp.setReconcileFlag(eMyMoney::Split::State::Cleared); break; case eMyMoney::Split::State::Cleared: sp.setReconcileFlag(eMyMoney::Split::State::NotReconciled); break; default: break; } } } else { sp.setReconcileFlag(flag); } t.modifySplit(sp); MyMoneyFile::instance()->modifyTransaction(t); } emit q->slotStatusProgress(i++, 0); } emit q->slotStatusProgress(-1, -1); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(q, i18n("Error"), i18n("Unable to modify transaction: %1, thrown in %2:%3", e.what(), e.file(), e.line())); } } // move a stock transaction from one investment account to another void moveInvestmentTransaction(const QString& /*fromId*/, const QString& toId, const MyMoneyTransaction& tx) { MyMoneyAccount toInvAcc = MyMoneyFile::instance()->account(toId); MyMoneyTransaction t(tx); // first determine which stock we are dealing with. // fortunately, investment transactions have only one stock involved QString stockAccountId; QString stockSecurityId; MyMoneySplit s; foreach (const auto split, t.splits()) { stockAccountId = split.accountId(); stockSecurityId = MyMoneyFile::instance()->account(stockAccountId).currencyId(); if (!MyMoneyFile::instance()->security(stockSecurityId).isCurrency()) { s = split; break; } } // Now check the target investment account to see if it // contains a stock with this id QString newStockAccountId; foreach (const auto sAccount, toInvAcc.accountList()) { if (MyMoneyFile::instance()->account(sAccount).currencyId() == stockSecurityId) { newStockAccountId = sAccount; break; } } // if it doesn't exist, we need to add it as a copy of the old one // no 'copyAccount()' function?? if (newStockAccountId.isEmpty()) { MyMoneyAccount stockAccount = MyMoneyFile::instance()->account(stockAccountId); MyMoneyAccount newStock; newStock.setName(stockAccount.name()); newStock.setNumber(stockAccount.number()); newStock.setDescription(stockAccount.description()); newStock.setInstitutionId(stockAccount.institutionId()); newStock.setOpeningDate(stockAccount.openingDate()); newStock.setAccountType(stockAccount.accountType()); newStock.setCurrencyId(stockAccount.currencyId()); newStock.setClosed(stockAccount.isClosed()); MyMoneyFile::instance()->addAccount(newStock, toInvAcc); newStockAccountId = newStock.id(); } // now update the split and the transaction s.setAccountId(newStockAccountId); t.modifySplit(s); MyMoneyFile::instance()->modifyTransaction(t); } void createTransactionMoveMenu() { Q_Q(KGlobalLedgerView); if (!m_moveToAccountSelector) { auto menu = pMenus[eMenu::Menu::MoveTransaction]; if (menu ) { auto accountSelectorAction = new QWidgetAction(menu); m_moveToAccountSelector = new KMyMoneyAccountSelector(menu, 0, false); m_moveToAccountSelector->setObjectName("transaction_move_menu_selector"); accountSelectorAction->setDefaultWidget(m_moveToAccountSelector); menu->addAction(accountSelectorAction); q->connect(m_moveToAccountSelector, &QObject::destroyed, q, &KGlobalLedgerView::slotObjectDestroyed); q->connect(m_moveToAccountSelector, &KMyMoneySelector::itemSelected, q, &KGlobalLedgerView::slotMoveToAccount); } } } QList > automaticReconciliation(const MyMoneyAccount &account, const QList > &transactions, const MyMoneyMoney &amount) { Q_Q(KGlobalLedgerView); static const int NR_OF_STEPS_LIMIT = 300000; static const int PROGRESSBAR_STEPS = 1000; QList > result = transactions; // KMSTATUS(i18n("Running automatic reconciliation")); auto progressBarIndex = 0; q->slotStatusProgress(progressBarIndex, NR_OF_STEPS_LIMIT / PROGRESSBAR_STEPS); // optimize the most common case - all transactions should be cleared QListIterator > itTransactionSplitResult(result); MyMoneyMoney transactionsBalance; while (itTransactionSplitResult.hasNext()) { const QPair &transactionSplit = itTransactionSplitResult.next(); transactionsBalance += transactionSplit.second.shares(); } if (amount == transactionsBalance) { result = transactions; return result; } q->slotStatusProgress(progressBarIndex++, 0); // only one transaction is uncleared itTransactionSplitResult.toFront(); int index = 0; while (itTransactionSplitResult.hasNext()) { const QPair &transactionSplit = itTransactionSplitResult.next(); if (transactionsBalance - transactionSplit.second.shares() == amount) { result.removeAt(index); return result; } index++; } q->slotStatusProgress(progressBarIndex++, 0); // more than one transaction is uncleared - apply the algorithm result.clear(); const auto& security = MyMoneyFile::instance()->security(account.currencyId()); double precision = 0.1 / account.fraction(security); QList sumList; sumList << MyMoneyMoney(); QMap > > sumToComponentsMap; // compute the possible matches - QListIterator > itTransactionSplit(transactions); - while (itTransactionSplit.hasNext()) { - const QPair &transactionSplit = itTransactionSplit.next(); + QListIterator > it_ts(transactions); + while (it_ts.hasNext()) { + const QPair &transactionSplit = it_ts.next(); QListIterator itSum(sumList); QList tempList; while (itSum.hasNext()) { const MyMoneyMoney &sum = itSum.next(); QList > splitIds; splitIds << qMakePair(transactionSplit.first.id(), transactionSplit.second.id()); if (sumToComponentsMap.contains(sum)) { if (sumToComponentsMap.value(sum).contains(qMakePair(transactionSplit.first.id(), transactionSplit.second.id()))) { continue; } splitIds.append(sumToComponentsMap.value(sum)); } tempList << transactionSplit.second.shares() + sum; sumToComponentsMap[transactionSplit.second.shares() + sum] = splitIds; int size = sumToComponentsMap.size(); if (size % PROGRESSBAR_STEPS == 0) { q->slotStatusProgress(progressBarIndex++, 0); } if (size > NR_OF_STEPS_LIMIT) { return result; // it's taking too much resources abort the algorithm } } QList unionList; unionList.append(tempList); unionList.append(sumList); qSort(unionList); sumList.clear(); MyMoneyMoney smallestSumFromUnion = unionList.first(); sumList.append(smallestSumFromUnion); QListIterator itUnion(unionList); while (itUnion.hasNext()) { MyMoneyMoney sumFromUnion = itUnion.next(); if (smallestSumFromUnion < MyMoneyMoney(1 - precision / transactions.size())*sumFromUnion) { smallestSumFromUnion = sumFromUnion; sumList.append(sumFromUnion); } } } q->slotStatusProgress(NR_OF_STEPS_LIMIT / PROGRESSBAR_STEPS, 0); if (sumToComponentsMap.contains(amount)) { QListIterator > itTransactionSplit(transactions); while (itTransactionSplit.hasNext()) { const QPair &transactionSplit = itTransactionSplit.next(); const QList > &splitIds = sumToComponentsMap.value(amount); if (splitIds.contains(qMakePair(transactionSplit.first.id(), transactionSplit.second.id()))) { result.append(transactionSplit); } } } #ifdef KMM_DEBUG qDebug("For the amount %s a number of %d possible sums where computed from the set of %d transactions: ", qPrintable(MyMoneyUtils::formatMoney(amount, security)), sumToComponentsMap.size(), transactions.size()); #endif q->slotStatusProgress(-1, -1); return result; } KGlobalLedgerView *q_ptr; MousePressFilter *m_mousePressFilter; KMyMoneyRegister::RegisterSearchLineWidget* m_registerSearchLine; // QString m_reconciliationAccount; QDate m_reconciliationDate; MyMoneyMoney m_endingBalance; int m_precision; bool m_recursion; bool m_showDetails; eWidgets::eRegister::Action m_action; // models AccountNamesFilterProxyModel *m_filterProxyModel; // widgets KMyMoneyAccountCombo* m_accountComboBox; MyMoneyMoney m_totalBalance; bool m_balanceIsApproximated; // frames QFrame* m_toolbarFrame; QFrame* m_registerFrame; QFrame* m_buttonFrame; QFrame* m_formFrame; QFrame* m_summaryFrame; // widgets KMyMoneyRegister::Register* m_register; KToolBar* m_buttonbar; /** * This member holds the currently selected account */ MyMoneyAccount m_currentAccount; QString m_lastSelectedAccountID; MyMoneyAccount m_reconciliationAccount; /** * This member holds the transaction list */ QList > m_transactionList; QLabel* m_leftSummaryLabel; QLabel* m_centerSummaryLabel; QLabel* m_rightSummaryLabel; KMyMoneyTransactionForm::TransactionForm* m_form; /** * This member holds the load state of page */ bool m_needLoad; bool m_newAccountLoaded; bool m_inEditMode; QWidgetList m_tabOrderWidgets; QPoint m_tooltipPosn; KMyMoneyRegister::SelectedTransactions m_selectedTransactions; /** * This member keeps the date that was used as the last posting date. * It will be updated whenever the user modifies the post date * and is used to preset the posting date when new transactions are created. * This member is initialised to the current date when the program is started. */ static QDate m_lastPostDate; // pointer to the current transaction editor QPointer m_transactionEditor; // id's that need to be remembered QString m_accountGoto, m_payeeGoto; QString m_lastPayeeEnteredId; QScopedPointer m_balanceWarning; KMyMoneyAccountSelector* m_moveToAccountSelector; // Reconciliation dialog KEndingBalanceDlg* m_endingBalanceDlg; KFindTransactionDlg* m_searchDlg; }; #endif diff --git a/kmymoney/views/khomeview_p.h b/kmymoney/views/khomeview_p.h index 495d4a603..b2799c194 100644 --- a/kmymoney/views/khomeview_p.h +++ b/kmymoney/views/khomeview_p.h @@ -1,1983 +1,1983 @@ /*************************************************************************** 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 #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; MyMoneyFile::instance()->accountList(list); if (list.count() == 0) { m_view->setHtml(KWelcomePage::welcomePage(), QUrl("file://")); } else { //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://")); } } void showNetWorthGraph() { Q_Q(KHomeView); m_html += QString("
%1
\n
 
\n").arg(i18n("Net Worth Forecast")); MyMoneyReport reportCfg = MyMoneyReport( MyMoneyReport::eAssetLiability, MyMoneyReport::eMonths, TransactionFilter::Date::UserDefined, // overridden by the setDateFilter() call below MyMoneyReport::eDetailTotal, i18n("Net Worth Forecast"), i18n("Generated Report")); reportCfg.setChartByDefault(true); reportCfg.setChartCHGridLines(false); reportCfg.setChartSVGridLines(false); reportCfg.setChartDataLabels(false); reportCfg.setChartType(MyMoneyReport::eChartLine); 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", qPrintable(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); int i = 0; int colspan = 1; //get begin day int 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 += ""; int 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(""; int dropZero = -1; //account dropped below zero int 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 (int 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, TransactionFilter::Date::CurrentMonth, MyMoneyReport::eDetailAll, 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) { - QDate nextDate = (*sched_it).nextPayment((*sched_it).lastPayment()); + 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()) { - auto acc = file->account(split.accountId()); + 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/kmymoneyview.h b/kmymoney/views/kmymoneyview.h index 137d1cfeb..bc4e318aa 100644 --- a/kmymoney/views/kmymoneyview.h +++ b/kmymoney/views/kmymoneyview.h @@ -1,369 +1,369 @@ /*************************************************************************** kmymoneyview.h ------------------- copyright : (C) 2000-2001 by Michael Edwardes 2004 by Thomas Baumgart (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. * * * ***************************************************************************/ #ifndef KMYMONEYVIEW_H #define KMYMONEYVIEW_H #include // ---------------------------------------------------------------------------- // QT Includes #include // ---------------------------------------------------------------------------- // KDE Includes #include // ---------------------------------------------------------------------------- // Project Includes #include "selectedtransactions.h" #ifdef KF5Activities_FOUND namespace KActivities { class ResourceInstance; } #endif namespace eAccountsModel { enum class Column; } namespace eMenu { enum class Action; } namespace KMyMoneyPlugin { class OnlinePlugin; } namespace KMyMoneyPlugin { class StoragePlugin; } namespace eDialogs { enum class ScheduleResultCode; } namespace eView { enum class Intent; } namespace eView { enum class Action; } namespace Icons { enum class Icon; } class KMyMoneyApp; class KHomeView; class KAccountsView; class KCategoriesView; class KInstitutionsView; class KPayeesView; class KTagsView; class KBudgetView; class KScheduledView; class KGlobalLedgerView; class IMyMoneyOperationsFormat; class MyMoneyTransaction; class KInvestmentView; class KReportsView; class SimpleLedgerView; class MyMoneySchedule; class MyMoneySecurity; class MyMoneyReport; class TransactionEditor; class KOnlineJobOutbox; class KMyMoneyTitleLabel; class MyMoneyAccount; class MyMoneyMoney; class MyMoneyObject; class QLabel; class KMyMoneyViewBase; /** * This class represents the view of the MyMoneyFile which contains * Banks/Accounts/Transactions, Recurring transactions (or Bills & Deposits) * and scripts (yet to be implemented). Each different aspect of the file * is represented by a tab within the view. * * @author Michael Edwardes 2001 Copyright 2000-2001 * * @short Handles the view of the MyMoneyFile. */ enum class View; class KMyMoneyView : public KPageWidget { Q_OBJECT public: // file actions for plugin enum fileActions { preOpen, postOpen, preSave, postSave, preClose, postClose }; private: enum menuID { AccountNew = 1, AccountOpen, AccountReconcile, AccountEdit, AccountDelete, AccountOnlineMap, AccountOnlineUpdate, AccountOfxConnect, CategoryNew }; enum storageTypeE { Memory = 0, Database } _storageType; KPageWidgetModel* m_model; QHash viewFrames; QHash viewBases; KMyMoneyTitleLabel* m_header; int m_lastViewSelected; QMap* m_storagePlugins; private: void viewAccountList(const QString& selectAccount); // Show the accounts view void createSchedule(MyMoneySchedule s, MyMoneyAccount& a); public: /** * The constructor for KMyMoneyView. Just creates all the tabs for the * different aspects of the MyMoneyFile. */ KMyMoneyView(); /** * Destructor */ ~KMyMoneyView(); /** * This method enables the state of all views (except home view) according * to an open file. */ void enableViewsIfFileOpen(bool fileOpen); void switchToDefaultView(); void switchToHomeView(); void addWidget(QWidget* w); void showPageAndFocus(View idView); void showPage(View idView); /** * check if the current view allows to print something * * @retval true Yes, view allows to print * @retval false No, view cannot print */ bool canPrint(); void finishReconciliation(const MyMoneyAccount& account); void showTitleBar(bool show); /** * This method changes the view type according to the settings. */ void updateViewType(); void slotAccountTreeViewChanged(const eAccountsModel::Column column, const bool show); void setOnlinePlugins(QMap& plugins); void setStoragePlugins(QMap& plugins); // TODO: remove that function /** * ugly proxy function */ eDialogs::ScheduleResultCode enterSchedule(MyMoneySchedule& schedule, bool autoEnter, bool extendedKeys); void addView(KMyMoneyViewBase* view, const QString& name, View idView); void removeView(View idView); /** * @brief actionsToBeConnected are actions that need ActionCollection * which is available in KMyMoneyApp * @return QHash of action id and QAction itself */ QHash actionsToBeConnected(); protected: /** * Overwritten because KMyMoney has it's custom header. */ - virtual bool showPageHeader() const; + bool showPageHeader() const final override; public Q_SLOTS: /** * This slot writes information about the page passed as argument @a current * in the kmymoney.rc file so that it can be selected automatically when * the application is started again. * * @param current QModelIndex of the current page item * @param previous QModelIndex of the previous page item */ void slotCurrentPageChanged(const QModelIndex current, const QModelIndex previous); /** * Brings up a dialog to change the list(s) settings and saves them into the * class KMyMoneySettings (a singleton). * * @see KListSettingsDlg * Refreshes all views. Used e.g. after settings have been changed or * data has been loaded from external sources (QIF import). **/ void slotRefreshViews(); /** * Called, whenever the payees view should pop up and a specific * transaction in an account should be shown. * * @param payeeId The ID of the payee to be shown * @param accountId The ID of the account to be shown * @param transactionId The ID of the transaction to be selected */ void slotPayeeSelected(const QString& payeeId, const QString& accountId, const QString& transactionId); /** * Called, whenever the tags view should pop up and a specific * transaction in an account should be shown. * * @param tagId The ID of the tag to be shown * @param accountId The ID of the account to be shown * @param transactionId The ID of the transaction to be selected */ void slotTagSelected(const QString& tagId, const QString& accountId, const QString& transactionId); /** * This slot prints the current view. */ void slotPrintView(); /** * Called when the user changes the detail * setting of the transaction register * * @param detailed if true, the register is shown with all details */ void slotShowTransactionDetail(bool detailed); /** * Informs respective views about selected object, so they can * update action states and current object. * @param obj Account, Category, Investment, Stock, Institution */ void slotObjectSelected(const MyMoneyObject& obj); void slotSelectByObject(const MyMoneyObject& obj, eView::Intent intent); void slotSelectByVariant(const QVariantList& variant, eView::Intent intent); void slotCustomActionRequested(View view, eView::Action action); void slotFileOpened(); void slotFileClosed(); private Q_SLOTS: /** * This slots switches the view to the specific page */ void slotShowHomePage(); void slotShowInstitutionsPage(); void slotShowAccountsPage(); void slotShowSchedulesPage(); void slotShowCategoriesPage(); void slotShowTagsPage(); void slotShowPayeesPage(); void slotShowLedgersPage(); void slotShowInvestmentsPage(); void slotShowReportsPage(); void slotShowBudgetPage(); void slotShowForecastPage(); void slotShowOutboxPage(); /** * Opens object in ledgers or edits in case of institution * @param obj Account, Category, Investment, Stock, Institution */ void slotOpenObjectRequested(const MyMoneyObject& obj); /** * Opens context menu based on objects's type * @param obj Account, Category, Investment, Stock, Institution */ void slotContextMenuRequested(const MyMoneyObject& obj); protected Q_SLOTS: /** * eventually replace this with KMyMoneyApp::slotCurrencySetBase(). * it contains the same code * * @deprecated */ void slotSetBaseCurrency(const MyMoneySecurity& baseCurrency); private: /** * Internal method used by slotAccountNew() and slotAccountCategory(). */ void accountNew(const bool createCategory); void resetViewSelection(const View); Q_SIGNALS: /** * This signal is emitted whenever a view is selected. * The parameter @p view is identified as one of KMyMoneyView::viewID. */ void viewActivated(int view); /** * This signal is emitted whenever a new view is about to be selected. */ void aboutToChangeView(); void accountSelectedForContextMenu(const MyMoneyAccount& acc); void viewStateChanged(bool enabled); /** * This signal is emitted to inform the kmmFile plugin when various file actions * occur. The Action parameter distinguishes between them. */ void kmmFilePlugin(unsigned int action); /** * @brief proxy signal */ void statusMsg(const QString& txt); /** * @brief proxy signal */ void statusProgress(int cnt, int base); void accountReconciled(const MyMoneyAccount& account, const QDate& date, const MyMoneyMoney& startingBalance, const MyMoneyMoney& endingBalance, const QList >& transactionList); /** * This signal is emitted when a transaction/list of transactions has been selected by * the GUI. If no transaction is selected or the selection is removed, * @p transactions is identical to an empty QList. This signal is used * by plugins to get information about changes. */ void transactionsSelected(const KMyMoneyRegister::SelectedTransactions& transactions); /** * This signal is emitted when a new account has been selected by * the GUI. If no account is selected or the selection is removed, * @a account is identical to MyMoneyAccount(). This signal is used * by plugins to get information about changes. */ void accountSelected(const MyMoneyAccount& account); }; #endif diff --git a/kmymoney/views/kmymoneywebpage.h b/kmymoney/views/kmymoneywebpage.h index 1eb94fdf1..663919043 100644 --- a/kmymoney/views/kmymoneywebpage.h +++ b/kmymoney/views/kmymoneywebpage.h @@ -1,61 +1,61 @@ /*************************************************************************** kmymoneywebpage.h ------------------- copyright : (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 KMYMONEYWEBPAGE_H #define KMYMONEYWEBPAGE_H #include // ---------------------------------------------------------------------------- // QT Includes // ---------------------------------------------------------------------------- // KDE Includes #ifdef ENABLE_WEBENGINE #include #else #include #endif // ---------------------------------------------------------------------------- // Project Includes #ifdef ENABLE_WEBENGINE class MyQWebEnginePage : public QWebEnginePage #else class MyQWebEnginePage : public KWebPage #endif { Q_OBJECT public: #ifdef ENABLE_WEBENGINE explicit MyQWebEnginePage(QObject* parent = nullptr) : QWebEnginePage(parent){} #else explicit MyQWebEnginePage(QObject* parent = nullptr) : KWebPage(parent){} #endif protected: #ifdef ENABLE_WEBENGINE - bool acceptNavigationRequest(const QUrl &url, NavigationType type, bool); + bool acceptNavigationRequest(const QUrl &url, NavigationType type, bool) final override; #else - bool acceptNavigationRequest(QWebFrame *frame, const QNetworkRequest &request, NavigationType type); + bool acceptNavigationRequest(QWebFrame *frame, const QNetworkRequest &request, NavigationType type) final override; #endif }; #endif diff --git a/kmymoney/views/kscheduledview_p.h b/kmymoney/views/kscheduledview_p.h index 52c67d55b..7289c0f54 100644 --- a/kmymoney/views/kscheduledview_p.h +++ b/kmymoney/views/kscheduledview_p.h @@ -1,684 +1,684 @@ /*************************************************************************** kscheduledview_p.h - description ------------------- begin : Sun Jan 27 2002 copyright : (C) 2000-2002 by Michael Edwardes email : mte@users.sourceforge.net 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 KSCHEDULEDVIEW_P_H #define KSCHEDULEDVIEW_P_H #include "kscheduledview.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "ui_kscheduledview.h" #include "kmymoneyviewbase_p.h" #include "kenterscheduledlg.h" #include "kbalancewarning.h" #include "transactioneditor.h" #include "kconfirmmanualenterdlg.h" #include "kmymoneymvccombo.h" #include "kmymoneyutils.h" #include "kmymoneysettings.h" #include "mymoneyexception.h" #include "kscheduletreeitem.h" #include "ktreewidgetfilterlinewidget.h" #include "icons/icons.h" #include "mymoneyutils.h" #include "mymoneyaccount.h" #include "mymoneymoney.h" #include "mymoneysecurity.h" #include "mymoneyschedule.h" #include "mymoneyfile.h" #include "mymoneypayee.h" #include "mymoneysplit.h" #include "mymoneytransaction.h" #include "mymoneyenums.h" #include "menuenums.h" #include "dialogenums.h" using namespace Icons; class KScheduledViewPrivate : public KMyMoneyViewBasePrivate { Q_DECLARE_PUBLIC(KScheduledView) public: explicit KScheduledViewPrivate(KScheduledView *qq) : KMyMoneyViewBasePrivate(), q_ptr(qq), ui(new Ui::KScheduledView), m_kaccPopup(nullptr), m_openBills(true), m_openDeposits(true), m_openTransfers(true), m_openLoans(true), m_needLoad(true), m_searchWidget(nullptr), m_balanceWarning(nullptr) { } ~KScheduledViewPrivate() { if(!m_needLoad) writeConfig(); delete ui; } void init() { Q_Q(KScheduledView); m_needLoad = false; ui->setupUi(q); // create the searchline widget // and insert it into the existing layout m_searchWidget = new KTreeWidgetFilterLineWidget(q, ui->m_scheduleTree); ui->vboxLayout->insertWidget(1, m_searchWidget); //enable custom context menu ui->m_scheduleTree->setContextMenuPolicy(Qt::CustomContextMenu); ui->m_scheduleTree->setSelectionMode(QAbstractItemView::SingleSelection); readConfig(); q->connect(ui->m_qbuttonNew, &QAbstractButton::clicked, pActions[eMenu::Action::NewSchedule], &QAction::trigger); // attach popup to 'Filter...' button m_kaccPopup = new QMenu(q); ui->m_accountsCombo->setMenu(m_kaccPopup); q->connect(m_kaccPopup, &QMenu::triggered, q, &KScheduledView::slotAccountActivated); KGuiItem::assign(ui->m_qbuttonNew, KMyMoneyUtils::scheduleNewGuiItem()); KGuiItem::assign(ui->m_accountsCombo, KMyMoneyUtils::accountsFilterGuiItem()); q->connect(ui->m_scheduleTree, &QWidget::customContextMenuRequested, q, &KScheduledView::customContextMenuRequested); q->connect(ui->m_scheduleTree, &QTreeWidget::itemSelectionChanged, q, &KScheduledView::slotSetSelectedItem); q->connect(ui->m_scheduleTree, &QTreeWidget::itemDoubleClicked, q, &KScheduledView::slotListItemExecuted); q->connect(ui->m_scheduleTree, &QTreeWidget::itemExpanded, q, &KScheduledView::slotListViewExpanded); q->connect(ui->m_scheduleTree, &QTreeWidget::itemCollapsed, q, &KScheduledView::slotListViewCollapsed); q->connect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, q, &KScheduledView::refresh); } static bool accountNameLessThan(const MyMoneyAccount& acc1, const MyMoneyAccount& acc2) { return acc1.name().toLower() < acc2.name().toLower(); } void refreshSchedule(bool full, const QString& schedId) { Q_Q(KScheduledView); ui->m_scheduleTree->header()->setFont(KMyMoneySettings::listHeaderFontEx()); ui->m_scheduleTree->clear(); try { if (full) { try { m_kaccPopup->clear(); MyMoneyFile* file = MyMoneyFile::instance(); // extract a list of all accounts under the asset group // and sort them by name QList list; QStringList accountList = file->asset().accountList(); accountList.append(file->liability().accountList()); file->accountList(list, accountList, true); qStableSort(list.begin(), list.end(), accountNameLessThan); QList::ConstIterator it_a; for (it_a = list.constBegin(); it_a != list.constEnd(); ++it_a) { if (!(*it_a).isClosed()) { QAction* act; act = m_kaccPopup->addAction((*it_a).name()); act->setCheckable(true); act->setChecked(true); } } } catch (const MyMoneyException &e) { KMessageBox::detailedError(q, i18n("Unable to load accounts: "), e.what()); } } MyMoneyFile *file = MyMoneyFile::instance(); QList scheduledItems = file->scheduleList(); if (scheduledItems.count() == 0) return; //disable sorting for performance ui->m_scheduleTree->setSortingEnabled(false); KScheduleTreeItem *itemBills = new KScheduleTreeItem(ui->m_scheduleTree); itemBills->setIcon(0, Icons::get(Icon::ViewExpense)); itemBills->setText(0, i18n("Bills")); itemBills->setData(0, KScheduleTreeItem::OrderRole, QVariant("0")); itemBills->setFirstColumnSpanned(true); itemBills->setFlags(Qt::ItemIsEnabled); QFont bold = itemBills->font(0); bold.setBold(true); itemBills->setFont(0, bold); KScheduleTreeItem *itemDeposits = new KScheduleTreeItem(ui->m_scheduleTree); itemDeposits->setIcon(0, Icons::get(Icon::ViewIncome)); itemDeposits->setText(0, i18n("Deposits")); itemDeposits->setData(0, KScheduleTreeItem::OrderRole, QVariant("1")); itemDeposits->setFirstColumnSpanned(true); itemDeposits->setFlags(Qt::ItemIsEnabled); itemDeposits->setFont(0, bold); KScheduleTreeItem *itemLoans = new KScheduleTreeItem(ui->m_scheduleTree); itemLoans->setIcon(0, Icons::get(Icon::ViewLoan)); itemLoans->setText(0, i18n("Loans")); itemLoans->setData(0, KScheduleTreeItem::OrderRole, QVariant("2")); itemLoans->setFirstColumnSpanned(true); itemLoans->setFlags(Qt::ItemIsEnabled); itemLoans->setFont(0, bold); KScheduleTreeItem *itemTransfers = new KScheduleTreeItem(ui->m_scheduleTree); itemTransfers->setIcon(0, Icons::get(Icon::ViewFinancialTransfer)); itemTransfers->setText(0, i18n("Transfers")); itemTransfers->setData(0, KScheduleTreeItem::OrderRole, QVariant("3")); itemTransfers->setFirstColumnSpanned(true); itemTransfers->setFlags(Qt::ItemIsEnabled); itemTransfers->setFont(0, bold); QList::Iterator it; QTreeWidgetItem *openItem = 0; for (it = scheduledItems.begin(); it != scheduledItems.end(); ++it) { MyMoneySchedule schedData = (*it); QTreeWidgetItem* item = 0; bool bContinue = true; QStringList::iterator accIt; for (accIt = m_filterAccounts.begin(); accIt != m_filterAccounts.end(); ++accIt) { if (*accIt == schedData.account().id()) { bContinue = false; // Filter it out break; } } if (!bContinue) continue; QTreeWidgetItem* parent = 0; switch (schedData.type()) { case eMyMoney::Schedule::Type::Any: // Should we display an error ? // We just sort it as bill and fall through here case eMyMoney::Schedule::Type::Bill: parent = itemBills; break; case eMyMoney::Schedule::Type::Deposit: parent = itemDeposits; break; case eMyMoney::Schedule::Type::Transfer: parent = itemTransfers; break; case eMyMoney::Schedule::Type::LoanPayment: parent = itemLoans; break; } if (parent) { if (!KMyMoneySettings::hideFinishedSchedules() || !schedData.isFinished()) { item = addScheduleItem(parent, schedData); if (schedData.id() == schedId) openItem = item; } } } if (openItem) { ui->m_scheduleTree->setCurrentItem(openItem); } // using a timeout is the only way, I got the 'ensureTransactionVisible' // working when coming from hidden form to visible form. I assume, this // has something to do with the delayed update of the display somehow. q->resize(q->width(), q->height() - 1); QTimer::singleShot(10, q, SLOT(slotTimerDone())); ui->m_scheduleTree->update(); // force repaint in case the filter is set m_searchWidget->searchLine()->updateSearch(QString()); if (m_openBills) itemBills->setExpanded(true); if (m_openDeposits) itemDeposits->setExpanded(true); if (m_openTransfers) itemTransfers->setExpanded(true); if (m_openLoans) itemLoans->setExpanded(true); } catch (const MyMoneyException &e) { KMessageBox::error(q, e.what()); } for (int i = 0; i < ui->m_scheduleTree->columnCount(); ++i) { ui->m_scheduleTree->resizeColumnToContents(i); } //reenable sorting after loading items ui->m_scheduleTree->setSortingEnabled(true); } void readConfig() { KSharedConfigPtr config = KSharedConfig::openConfig(); KConfigGroup grp = config->group("Last Use Settings"); m_openBills = grp.readEntry("KScheduleView_openBills", true); m_openDeposits = grp.readEntry("KScheduleView_openDeposits", true); m_openTransfers = grp.readEntry("KScheduleView_openTransfers", true); m_openLoans = grp.readEntry("KScheduleView_openLoans", true); QByteArray columns; columns = grp.readEntry("KScheduleView_treeState", columns); ui->m_scheduleTree->header()->restoreState(columns); ui->m_scheduleTree->header()->setFont(KMyMoneySettings::listHeaderFontEx()); } void writeConfig() { KSharedConfigPtr config = KSharedConfig::openConfig(); KConfigGroup grp = config->group("Last Use Settings"); grp.writeEntry("KScheduleView_openBills", m_openBills); grp.writeEntry("KScheduleView_openDeposits", m_openDeposits); grp.writeEntry("KScheduleView_openTransfers", m_openTransfers); grp.writeEntry("KScheduleView_openLoans", m_openLoans); QByteArray columns = ui->m_scheduleTree->header()->saveState(); grp.writeEntry("KScheduleView_treeState", columns); config->sync(); } QTreeWidgetItem* addScheduleItem(QTreeWidgetItem* parent, MyMoneySchedule& schedule) { KScheduleTreeItem* item = new KScheduleTreeItem(parent); item->setData(0, Qt::UserRole, QVariant::fromValue(schedule)); item->setData(0, KScheduleTreeItem::OrderRole, schedule.name()); if (!schedule.isFinished()) { if (schedule.isOverdue()) { item->setIcon(0, Icons::get(Icon::ViewUpcominEvents)); QBrush brush = item->foreground(0); brush.setColor(Qt::red); for (int i = 0; i < ui->m_scheduleTree->columnCount(); ++i) { item->setForeground(i, brush); } } else { item->setIcon(0, Icons::get(Icon::ViewCalendarDay)); } } else { item->setIcon(0, Icons::get(Icon::DialogClose)); QBrush brush = item->foreground(0); brush.setColor(Qt::darkGreen); for (int i = 0; i < ui->m_scheduleTree->columnCount(); ++i) { item->setForeground(i, brush); } } try { MyMoneyTransaction transaction = schedule.transaction(); MyMoneySplit s1 = (transaction.splits().size() < 1) ? MyMoneySplit() : transaction.splits()[0]; MyMoneySplit s2 = (transaction.splits().size() < 2) ? MyMoneySplit() : transaction.splits()[1]; MyMoneySplit split; MyMoneyAccount acc; switch (schedule.type()) { case eMyMoney::Schedule::Type::Deposit: if (s1.value().isNegative()) split = s2; else split = s1; break; case eMyMoney::Schedule::Type::LoanPayment: { auto found = false; foreach (const auto it_split, transaction.splits()) { acc = MyMoneyFile::instance()->account(it_split.accountId()); if (acc.accountGroup() == eMyMoney::Account::Type::Asset || acc.accountGroup() == eMyMoney::Account::Type::Liability) { if (acc.accountType() != eMyMoney::Account::Type::Loan && acc.accountType() != eMyMoney::Account::Type::AssetLoan) { split = it_split; found = true; break; } } } if (!found) { qWarning("Split for payment account not found in %s:%d.", __FILE__, __LINE__); } break; } default: if (!s1.value().isPositive()) split = s1; else split = s2; break; } acc = MyMoneyFile::instance()->account(split.accountId()); item->setText(0, schedule.name()); MyMoneySecurity currency = MyMoneyFile::instance()->currency(acc.currencyId()); QString accName = acc.name(); if (!accName.isEmpty()) { item->setText(1, accName); } else { item->setText(1, "---"); } item->setData(1, KScheduleTreeItem::OrderRole, QVariant(accName)); QString payeeName; if (!s1.payeeId().isEmpty()) { payeeName = MyMoneyFile::instance()->payee(s1.payeeId()).name(); item->setText(2, payeeName); } else { item->setText(2, "---"); } item->setData(2, KScheduleTreeItem::OrderRole, QVariant(payeeName)); MyMoneyMoney amount = split.shares().abs(); item->setData(3, Qt::UserRole, QVariant::fromValue(amount)); if (!accName.isEmpty()) { item->setText(3, QString("%1 ").arg(MyMoneyUtils::formatMoney(amount, acc, currency))); } else { //there are some cases where the schedule does not have an account //in those cases the account will not have a fraction //use base currency instead item->setText(3, QString("%1 ").arg(MyMoneyUtils::formatMoney(amount, MyMoneyFile::instance()->baseCurrency()))); } item->setTextAlignment(3, Qt::AlignRight | Qt::AlignVCenter); item->setData(3, KScheduleTreeItem::OrderRole, QVariant::fromValue(amount)); // Do the real next payment like ms-money etc QDate nextDueDate; if (schedule.isFinished()) { item->setText(4, i18nc("Finished schedule", "Finished")); } else { nextDueDate = schedule.adjustedNextDueDate(); item->setText(4, QLocale().toString(schedule.adjustedNextDueDate(), QLocale::ShortFormat)); } item->setData(4, KScheduleTreeItem::OrderRole, QVariant(nextDueDate)); item->setText(5, i18nc("Frequency of schedule", schedule.occurrenceToString().toLatin1())); item->setText(6, KMyMoneyUtils::paymentMethodToString(schedule.paymentType())); } catch (const MyMoneyException &e) { item->setText(0, "Error:"); item->setText(1, e.what()); } return item; } /** * This method allows to enter the next scheduled transaction of * the given schedule @a s. In case @a extendedKeys is @a true, * the given schedule can also be skipped or ignored. * If @a autoEnter is @a true and the schedule does not contain * an estimated value, the schedule is entered as is without further * interaction with the user. In all other cases, the user will * be presented a dialog and allowed to adjust the values for this * instance of the schedule. * * The transaction will be created and entered into the ledger * and the schedule updated. */ eDialogs::ScheduleResultCode enterSchedule(MyMoneySchedule& schedule, bool autoEnter = false, bool extendedKeys = false) { Q_Q(KScheduledView); auto rc = eDialogs::ScheduleResultCode::Cancel; if (!schedule.id().isEmpty()) { try { schedule = MyMoneyFile::instance()->schedule(schedule.id()); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(q, i18n("Unable to enter scheduled transaction '%1'", schedule.name()), e.what()); return rc; } QPointer dlg = new KEnterScheduleDlg(q, schedule); try { QDate origDueDate = schedule.nextDueDate(); dlg->showExtendedKeys(extendedKeys); QPointer transactionEditor = dlg->startEdit(); if (transactionEditor) { KMyMoneyMVCCombo::setSubstringSearchForChildren(dlg, !KMyMoneySettings::stringMatchFromStart()); MyMoneyTransaction torig, taccepted; transactionEditor->createTransaction(torig, dlg->transaction(), schedule.transaction().splits().isEmpty() ? MyMoneySplit() : schedule.transaction().splits().front(), true); // force actions to be available no matter what (will be updated according to the state during // slotTransactionsEnter or slotTransactionsCancel) pActions[eMenu::Action::CancelTransaction]->setEnabled(true); pActions[eMenu::Action::EnterTransaction]->setEnabled(true); KConfirmManualEnterDlg::Action action = KConfirmManualEnterDlg::ModifyOnce; if (!autoEnter || !schedule.isFixed()) { for (; dlg != 0;) { rc = eDialogs::ScheduleResultCode::Cancel; if (dlg->exec() == QDialog::Accepted && dlg != 0) { rc = dlg->resultCode(); if (rc == eDialogs::ScheduleResultCode::Enter) { transactionEditor->createTransaction(taccepted, torig, torig.splits().isEmpty() ? MyMoneySplit() : torig.splits().front(), true); // make sure to suppress comparison of some data: postDate torig.setPostDate(taccepted.postDate()); if (torig != taccepted) { QPointer cdlg = new KConfirmManualEnterDlg(schedule, q); cdlg->loadTransactions(torig, taccepted); if (cdlg->exec() == QDialog::Accepted) { action = cdlg->action(); delete cdlg; break; } delete cdlg; // the user has chosen 'cancel' during confirmation, // we go back to the editor continue; } } else if (rc == eDialogs::ScheduleResultCode::Skip) { slotTransactionsCancel(transactionEditor, schedule); skipSchedule(schedule); } else { slotTransactionsCancel(transactionEditor, schedule); } } else { if (autoEnter) { if (KMessageBox::warningYesNo(q, i18n("Are you sure you wish to stop this scheduled transaction from being entered into the register?\n\nKMyMoney will prompt you again next time it starts unless you manually enter it later.")) == KMessageBox::No) { // the user has chosen 'No' for the above question, // we go back to the editor continue; } } slotTransactionsCancel(transactionEditor, schedule); } break; } } // if we still have the editor around here, the user did not cancel if ((transactionEditor != 0) && (dlg != 0)) { MyMoneyFileTransaction ft; try { MyMoneyTransaction t; // add the new transaction switch (action) { case KConfirmManualEnterDlg::UseOriginal: // setup widgets with original transaction data transactionEditor->setTransaction(dlg->transaction(), dlg->transaction().splits().isEmpty() ? MyMoneySplit() : dlg->transaction().splits().front()); // and create a transaction based on that data taccepted = MyMoneyTransaction(); transactionEditor->createTransaction(taccepted, dlg->transaction(), dlg->transaction().splits().isEmpty() ? MyMoneySplit() : dlg->transaction().splits().front(), true); break; case KConfirmManualEnterDlg::ModifyAlways: torig = taccepted; torig.setPostDate(origDueDate); schedule.setTransaction(torig); break; case KConfirmManualEnterDlg::ModifyOnce: break; } QString newId; q->connect(transactionEditor, SIGNAL(balanceWarning(QWidget*,MyMoneyAccount,QString)), m_balanceWarning.data(), SLOT(slotShowMessage(QWidget*,MyMoneyAccount,QString))); if (transactionEditor->enterTransactions(newId, false)) { if (!newId.isEmpty()) { - MyMoneyTransaction t = MyMoneyFile::instance()->transaction(newId); + t = MyMoneyFile::instance()->transaction(newId); schedule.setLastPayment(t.postDate()); } // in case the next due date is invalid, the schedule is finished // we mark it as such by setting the next due date to one day past the end QDate nextDueDate = schedule.nextPayment(origDueDate); if (!nextDueDate.isValid()) { schedule.setNextDueDate(schedule.endDate().addDays(1)); } else { schedule.setNextDueDate(nextDueDate); } MyMoneyFile::instance()->modifySchedule(schedule); rc = eDialogs::ScheduleResultCode::Enter; // delete the editor before we emit the dataChanged() signal from the // engine. Calling this twice in a row does not hurt. delete transactionEditor; ft.commit(); } } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(q, i18n("Unable to enter scheduled transaction '%1'", schedule.name()), e.what()); } delete transactionEditor; } } } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(q, i18n("Unable to enter scheduled transaction '%1'", schedule.name()), e.what()); } delete dlg; } return rc; } void slotTransactionsCancel(TransactionEditor* editor, const MyMoneySchedule& schedule) { Q_Q(KScheduledView); // since we jump here via code, we have to make sure to react only // if the action is enabled if (pActions[eMenu::Action::CancelTransaction]->isEnabled()) { // make sure, we block the enter function pActions[eMenu::Action::EnterTransaction]->setEnabled(false); // qDebug("KMyMoneyApp::slotTransactionsCancel"); delete editor; emit q->selectByObject(schedule, eView::Intent::None); } } /** * This method allows to skip the next scheduled transaction of * the given schedule @a s. * */ void skipSchedule(MyMoneySchedule& schedule) { Q_Q(KScheduledView); if (!schedule.id().isEmpty()) { try { schedule = MyMoneyFile::instance()->schedule(schedule.id()); if (!schedule.isFinished()) { if (schedule.occurrence() != eMyMoney::Schedule::Occurrence::Once) { QDate next = schedule.nextDueDate(); if (!schedule.isFinished() && (KMessageBox::questionYesNo(q, i18n("Do you really want to skip the %1 transaction scheduled for %2?", schedule.name(), QLocale().toString(next, QLocale::ShortFormat)))) == KMessageBox::Yes) { MyMoneyFileTransaction ft; schedule.setLastPayment(next); schedule.setNextDueDate(schedule.nextPayment(next)); MyMoneyFile::instance()->modifySchedule(schedule); ft.commit(); } } } } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(q, i18n("Unable to skip scheduled transaction %1.", schedule.name()), e.what()); } } } KScheduledView *q_ptr; Ui::KScheduledView *ui; /// The selected schedule id in the list view. QMenu *m_kaccPopup; QStringList m_filterAccounts; bool m_openBills; bool m_openDeposits; bool m_openTransfers; bool m_openLoans; /** * This member holds the load state of page */ bool m_needLoad; /** * Search widget for the list */ KTreeWidgetSearchLineWidget* m_searchWidget; MyMoneySchedule m_currentSchedule; QScopedPointer m_balanceWarning; }; #endif diff --git a/kmymoney/views/kscheduletreeitem.h b/kmymoney/views/kscheduletreeitem.h index 712188d03..84c6dd80a 100644 --- a/kmymoney/views/kscheduletreeitem.h +++ b/kmymoney/views/kscheduletreeitem.h @@ -1,53 +1,53 @@ /*************************************************************************** kscheduletreeitem.h - description ------------------- begin : Fri Jul 16 2010 copyright : (C) 2010 by Alvaro Soliverez email : asoliverez@kde.org ***************************************************************************/ /*************************************************************************** * * * 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 KSCHEDULETREEITEM_H #define KSCHEDULETREEITEM_H // ---------------------------------------------------------------------------- // QT Includes #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes class KScheduleTreeItem : public QTreeWidgetItem { public: explicit KScheduleTreeItem(QTreeWidget* parent); explicit KScheduleTreeItem(QTreeWidgetItem* &parent); - bool operator<(const QTreeWidgetItem &otherItem) const; + bool operator<(const QTreeWidgetItem &otherItem) const final override; enum ScheduleItemDataRole { ScheduleIdRole = Qt::UserRole, OrderRole = Qt::UserRole + 1 }; }; #endif // KSCHEDULETREEITEM_H diff --git a/kmymoney/views/ledgerdelegate.h b/kmymoney/views/ledgerdelegate.h index 469810d40..61dbbfc6a 100644 --- a/kmymoney/views/ledgerdelegate.h +++ b/kmymoney/views/ledgerdelegate.h @@ -1,115 +1,115 @@ /*************************************************************************** ledgerdelegate.h ------------------- begin : Sat Aug 8 2015 copyright : (C) 2015 by Thomas Baumgart email : 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. * * * ***************************************************************************/ #ifndef LEDGERDELEGATE_H #define LEDGERDELEGATE_H // ---------------------------------------------------------------------------- // QT Includes #include class QColor; // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes #include "ledgermodel.h" #include "modelenums.h" class LedgerView; class MyMoneyMoney; class LedgerSeparator { public: explicit LedgerSeparator(eLedgerModel::Role role) : m_role(role) {} virtual ~LedgerSeparator() {} virtual bool rowHasSeparator(const QModelIndex& index) const = 0; virtual QString separatorText(const QModelIndex& index) const = 0; virtual void adjustBackgroundScheme(QPalette& palette, const QModelIndex& index) const = 0; static void setFirstFiscalDate(int firstMonth, int firstDay); static void setShowFiscalDate(bool show) { showFiscalDate = show; } static void setShowFancyDate(bool show) { showFancyDate = show; } protected: inline QModelIndex nextIndex(const QModelIndex& index) const; eLedgerModel::Role m_role; static QDate firstFiscalDate; static bool showFiscalDate; static bool showFancyDate; }; class LedgerDelegate : public QStyledItemDelegate { Q_OBJECT public: explicit LedgerDelegate(LedgerView* parent = 0); virtual ~LedgerDelegate(); - virtual void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const; - virtual QSize sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const; - virtual QWidget* createEditor(QWidget* parent, const QStyleOptionViewItem& option, const QModelIndex& index) const; - virtual void setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const; - virtual void setEditorData(QWidget* editWidget, const QModelIndex& index) const; + void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const final override; + QSize sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const final override; + QWidget* createEditor(QWidget* parent, const QStyleOptionViewItem& option, const QModelIndex& index) const final override; + void setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const final override; + void setEditorData(QWidget* editWidget, const QModelIndex& index) const final override; virtual void setSortRole(eLedgerModel::Role role); - virtual void updateEditorGeometry(QWidget* editor, const QStyleOptionViewItem& option, const QModelIndex& index) const; + void updateEditorGeometry(QWidget* editor, const QStyleOptionViewItem& option, const QModelIndex& index) const final override; /** * This method returns the row that currently has an editor * or -1 if no editor is open */ virtual int editorRow() const; void setOnlineBalance(const QDate& date, const MyMoneyMoney& amount, int fraction = 0); static void setErroneousColor(const QColor& color); static void setImportedColor(const QColor& color); static QColor erroneousColor(); protected: - bool eventFilter(QObject* o, QEvent* event); + bool eventFilter(QObject* o, QEvent* event) final override; protected Q_SLOTS: void endEdit(); Q_SIGNALS: void sizeHintChanged(const QModelIndex&) const; private: class Private; Private * const d; static QColor m_erroneousColor; static QColor m_importedColor; static QColor m_separatorColor; }; #endif // LEDGERDELEGATE_H diff --git a/kmymoney/views/ledgerview.h b/kmymoney/views/ledgerview.h index 3e1c9fbae..e6248d0ae 100644 --- a/kmymoney/views/ledgerview.h +++ b/kmymoney/views/ledgerview.h @@ -1,113 +1,113 @@ /*************************************************************************** ledgerview.h ------------------- begin : Sat Aug 8 2015 copyright : (C) 2015 by Thomas Baumgart email : 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. * * * ***************************************************************************/ #ifndef LEDGERVIEW_H #define LEDGERVIEW_H // ---------------------------------------------------------------------------- // QT Includes #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes class MyMoneyAccount; class LedgerView : public QTableView { Q_OBJECT public: explicit LedgerView(QWidget* parent = 0); virtual ~LedgerView(); virtual void setAccount(const MyMoneyAccount& acc); virtual QString accountId() const; /** * This method is used to modify the visibilty of the * empty entry at the end of the ledger. The default * for the parameter @a show is @c true. */ void setShowEntryForNewTransaction(bool show = true); /** * Returns true if the sign of the values displayed has * been inverted depending on the account type. */ bool showValuesInverted() const; public Q_SLOTS: /** * This method scrolls the ledger so that the current item is visible */ void ensureCurrentItemIsVisible(); /** * Overridden for internal reasons. No change in base functionality */ void edit(const QModelIndex& index) { QTableView::edit(index); } protected: - bool edit(const QModelIndex& index, EditTrigger trigger, QEvent* event); - virtual void mousePressEvent(QMouseEvent* event); - virtual void mouseMoveEvent(QMouseEvent* event); - virtual void mouseDoubleClickEvent(QMouseEvent* event); - virtual void wheelEvent(QWheelEvent *event); - virtual void moveEvent(QMoveEvent *event); - virtual void resizeEvent(QResizeEvent* event); - virtual void paintEvent(QPaintEvent* event); - virtual int sizeHintForRow(int row) const; + bool edit(const QModelIndex& index, EditTrigger trigger, QEvent* event) final override; + void mousePressEvent(QMouseEvent* event) final override; + void mouseMoveEvent(QMouseEvent* event) final override; + void mouseDoubleClickEvent(QMouseEvent* event) final override; + void wheelEvent(QWheelEvent *event) final override; + void moveEvent(QMoveEvent *event) final override; + void resizeEvent(QResizeEvent* event) final override; + void paintEvent(QPaintEvent* event) final override; + int sizeHintForRow(int row) const final override; protected Q_SLOTS: - virtual void closeEditor(QWidget* editor, QAbstractItemDelegate::EndEditHint hint); - virtual void rowsInserted(const QModelIndex& index, int start, int end); - virtual void rowsAboutToBeRemoved(const QModelIndex& index, int start, int end); - virtual void currentChanged(const QModelIndex ¤t, const QModelIndex &previous); + void closeEditor(QWidget* editor, QAbstractItemDelegate::EndEditHint hint) final override; + void rowsInserted(const QModelIndex& index, int start, int end) final override; + void rowsAboutToBeRemoved(const QModelIndex& index, int start, int end) final override; + void currentChanged(const QModelIndex ¤t, const QModelIndex &previous) final override; virtual void adjustDetailColumn(int newViewportWidth); virtual void adjustDetailColumn(); virtual void recalculateBalances(); virtual void accountChanged(); Q_SIGNALS: void transactionSelected(const QString& transactionSplitId); void aboutToStartEdit(); void aboutToFinishEdit(); protected: class Private; Private * const d; }; class SplitView : public LedgerView { Q_OBJECT public: explicit SplitView(QWidget* parent = 0); virtual ~SplitView(); protected Q_SLOTS: - virtual void recalculateBalances() {} + void recalculateBalances() final override {} }; #endif // LEDGERVIEW_H diff --git a/kmymoney/views/newspliteditor.h b/kmymoney/views/newspliteditor.h index 0439c942f..33ec170c1 100644 --- a/kmymoney/views/newspliteditor.h +++ b/kmymoney/views/newspliteditor.h @@ -1,107 +1,107 @@ /*************************************************************************** newspliteditor.h ------------------- begin : Sat Apr 9 2016 copyright : (C) 2016 by Thomas Baumgart email : 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. * * * ***************************************************************************/ #ifndef NEWSPLITEDITOR_H #define NEWSPLITEDITOR_H // ---------------------------------------------------------------------------- // QT Includes #include #include class QWidget; // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes #include "mymoneymoney.h" class NewSplitEditor : public QFrame { Q_OBJECT public: /** * @a accountId is the current account displayed for the transaction */ explicit NewSplitEditor(QWidget* parent, const QString& accountId = QString()); virtual ~NewSplitEditor(); /** * This method returns true if the user pressed the enter button. * It remains false, in case the user pressed the cancel button. */ virtual bool accepted() const; void setShowValuesInverted(bool inverse); bool showValuesInverted(); protected: - virtual void keyPressEvent(QKeyEvent* e); + void keyPressEvent(QKeyEvent* e) final override; public Q_SLOTS: /** * This method returns the transaction split id passed * to setSplitId(). */ QString splitId() const; /** * Returns the id of the selected account in the category widget */ QString accountId() const; void setAccountId(const QString& id); /** * Returns the contents of the memo widget */ QString memo() const; void setMemo(const QString& memo); MyMoneyMoney amount() const; void setAmount(MyMoneyMoney value); QString costCenterId() const; void setCostCenterId(const QString& id); QString number() const; void setNumber(const QString& id); protected Q_SLOTS: virtual void reject(); virtual void acceptEdit(); virtual void numberChanged(const QString& newNumber); virtual void categoryChanged(const QString& accountId); virtual void costCenterChanged(int costCenterIndex); virtual void amountChanged(); Q_SIGNALS: void done(); void transactionChanged(const QString&); private: struct Private; QScopedPointer d; }; #endif // NEWSPLITEDITOR_H diff --git a/kmymoney/views/newtransactioneditor.cpp b/kmymoney/views/newtransactioneditor.cpp index b148d93dc..3d0626ddf 100644 --- a/kmymoney/views/newtransactioneditor.cpp +++ b/kmymoney/views/newtransactioneditor.cpp @@ -1,728 +1,728 @@ /*************************************************************************** newtransactioneditor.cpp ------------------- begin : Sat Aug 8 2015 copyright : (C) 2015 by Thomas Baumgart email : 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. * * * ***************************************************************************/ #include "newtransactioneditor.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include // ---------------------------------------------------------------------------- // Project Includes #include "creditdebithelper.h" #include "mymoneyfile.h" #include "mymoneyaccount.h" #include "mymoneyexception.h" #include "kmymoneyutils.h" #include "kmymoneyaccountcombo.h" #include "models.h" #include "accountsmodel.h" #include "costcentermodel.h" #include "ledgermodel.h" #include "splitmodel.h" #include "payeesmodel.h" #include "mymoneysplit.h" #include "mymoneytransaction.h" #include "ui_newtransactioneditor.h" #include "splitdialog.h" #include "widgethintframe.h" #include "icons/icons.h" #include "modelenums.h" #include "mymoneyenums.h" using namespace Icons; Q_GLOBAL_STATIC(QDate, lastUsedPostDate) class NewTransactionEditor::Private { public: Private(NewTransactionEditor* parent) : ui(new Ui_NewTransactionEditor) , accountsModel(new AccountNamesFilterProxyModel(parent)) , costCenterModel(new QSortFilterProxyModel(parent)) , payeesModel(new QSortFilterProxyModel(parent)) , accepted(false) , costCenterRequired(false) , amountHelper(nullptr) { accountsModel->setObjectName("NewTransactionEditor::accountsModel"); costCenterModel->setObjectName("SortedCostCenterModel"); payeesModel->setObjectName("SortedPayeesModel"); statusModel.setObjectName("StatusModel"); splitModel.setObjectName("SplitModel"); costCenterModel->setSortLocaleAware(true); costCenterModel->setSortCaseSensitivity(Qt::CaseInsensitive); payeesModel->setSortLocaleAware(true); payeesModel->setSortCaseSensitivity(Qt::CaseInsensitive); createStatusEntry(eMyMoney::Split::State::NotReconciled); createStatusEntry(eMyMoney::Split::State::Cleared); createStatusEntry(eMyMoney::Split::State::Reconciled); // createStatusEntry(eMyMoney::Split::State::Frozen); } ~Private() { delete ui; } void createStatusEntry(eMyMoney::Split::State status); void updateWidgetState(); bool checkForValidTransaction(bool doUserInteraction = true); bool isDatePostOpeningDate(const QDate& date, const QString& accountId); bool postdateChanged(const QDate& date); bool costCenterChanged(int costCenterIndex); bool categoryChanged(const QString& accountId); bool numberChanged(const QString& newNumber); bool valueChanged(CreditDebitHelper* valueHelper); Ui_NewTransactionEditor* ui; AccountNamesFilterProxyModel* accountsModel; QSortFilterProxyModel* costCenterModel; QSortFilterProxyModel* payeesModel; bool accepted; bool costCenterRequired; bool costCenterOk; SplitModel splitModel; QStandardItemModel statusModel; QString transactionSplitId; - MyMoneyAccount account; + MyMoneyAccount m_account; MyMoneyTransaction transaction; MyMoneySplit split; CreditDebitHelper* amountHelper; }; void NewTransactionEditor::Private::createStatusEntry(eMyMoney::Split::State status) { QStandardItem* p = new QStandardItem(KMyMoneyUtils::reconcileStateToString(status, true)); p->setData((int)status); statusModel.appendRow(p); } void NewTransactionEditor::Private::updateWidgetState() { // just in case it is disabled we turn it on ui->costCenterCombo->setEnabled(true); // setup the category/account combo box. If we have a split transaction, we disable the // combo box altogether. Changes can only be made via the split dialog editor bool blocked = false; QModelIndex index; // update the category combo box ui->accountCombo->setEnabled(true); switch(splitModel.rowCount()) { case 0: ui->accountCombo->setSelected(QString()); break; case 1: index = splitModel.index(0, 0); ui->accountCombo->setSelected(splitModel.data(index, (int)eLedgerModel::Role::AccountId).toString()); break; default: index = splitModel.index(0, 0); blocked = ui->accountCombo->lineEdit()->blockSignals(true); ui->accountCombo->lineEdit()->setText(i18n("Split transaction")); ui->accountCombo->setDisabled(true); ui->accountCombo->lineEdit()->blockSignals(blocked); ui->costCenterCombo->setDisabled(true); ui->costCenterLabel->setDisabled(true); break; } ui->accountCombo->hidePopup(); // update the costcenter combo box if(ui->costCenterCombo->isEnabled()) { // extract the cost center index = splitModel.index(0, 0); QModelIndexList ccList = costCenterModel->match(costCenterModel->index(0, 0), CostCenterModel::CostCenterIdRole, splitModel.data(index, (int)eLedgerModel::Role::CostCenterId), 1, Qt::MatchFlags(Qt::MatchExactly | Qt::MatchCaseSensitive | Qt::MatchRecursive)); if (ccList.count() > 0) { index = ccList.front(); ui->costCenterCombo->setCurrentIndex(index.row()); } } } bool NewTransactionEditor::Private::checkForValidTransaction(bool doUserInteraction) { QStringList infos; bool rc = true; if(!postdateChanged(ui->dateEdit->date())) { infos << ui->dateEdit->toolTip(); rc = false; } if(!costCenterChanged(ui->costCenterCombo->currentIndex())) { infos << ui->costCenterCombo->toolTip(); rc = false; } if(doUserInteraction) { /// @todo add dialog here that shows the @a infos about the problem } return rc; } bool NewTransactionEditor::Private::isDatePostOpeningDate(const QDate& date, const QString& accountId) { bool rc = true; try { MyMoneyAccount account = MyMoneyFile::instance()->account(accountId); const bool isIncomeExpense = account.isIncomeExpense(); // we don't check for categories if(!isIncomeExpense) { if(date < account.openingDate()) rc = false; } } catch (MyMoneyException &e) { qDebug() << "Ooops: invalid account id" << accountId << "in" << Q_FUNC_INFO; } return rc; } bool NewTransactionEditor::Private::postdateChanged(const QDate& date) { bool rc = true; WidgetHintFrame::hide(ui->dateEdit, i18n("The posting date of the transaction.")); // collect all account ids QStringList accountIds; - accountIds << account.id(); + accountIds << m_account.id(); for(int row = 0; row < splitModel.rowCount(); ++row) { QModelIndex index = splitModel.index(row, 0); accountIds << splitModel.data(index, (int)eLedgerModel::Role::AccountId).toString();; } Q_FOREACH(QString accountId, accountIds) { if(!isDatePostOpeningDate(date, accountId)) { MyMoneyAccount account = MyMoneyFile::instance()->account(accountId); WidgetHintFrame::show(ui->dateEdit, i18n("The posting date is prior to the opening date of account %1.", account.name())); rc = false; break; } } return rc; } bool NewTransactionEditor::Private::costCenterChanged(int costCenterIndex) { bool rc = true; WidgetHintFrame::hide(ui->costCenterCombo, i18n("The cost center this transaction should be assigned to.")); if(costCenterIndex != -1) { if(costCenterRequired && ui->costCenterCombo->currentText().isEmpty()) { WidgetHintFrame::show(ui->costCenterCombo, i18n("A cost center assignment is required for a transaction in the selected category.")); rc = false; } if(rc == true && splitModel.rowCount() == 1) { QModelIndex index = costCenterModel->index(costCenterIndex, 0); QString costCenterId = costCenterModel->data(index, CostCenterModel::CostCenterIdRole).toString(); index = splitModel.index(0, 0); splitModel.setData(index, costCenterId, (int)eLedgerModel::Role::CostCenterId); } } return rc; } bool NewTransactionEditor::Private::categoryChanged(const QString& accountId) { bool rc = true; if(!accountId.isEmpty() && splitModel.rowCount() <= 1) { try { MyMoneyAccount category = MyMoneyFile::instance()->account(accountId); const bool isIncomeExpense = category.isIncomeExpense(); ui->costCenterCombo->setEnabled(isIncomeExpense); ui->costCenterLabel->setEnabled(isIncomeExpense); costCenterRequired = category.isCostCenterRequired(); rc &= costCenterChanged(ui->costCenterCombo->currentIndex()); rc &= postdateChanged(ui->dateEdit->date()); // make sure we have a split in the model bool newSplit = false; if(splitModel.rowCount() == 0) { splitModel.addEmptySplitEntry(); newSplit = true; } const QModelIndex index = splitModel.index(0, 0); splitModel.setData(index, accountId, (int)eLedgerModel::Role::AccountId); if(newSplit) { costCenterChanged(ui->costCenterCombo->currentIndex()); if(amountHelper->haveValue()) { splitModel.setData(index, QVariant::fromValue(-amountHelper->value()), (int)eLedgerModel::Role::SplitValue); /// @todo make sure to convert initial value to shares according to price information splitModel.setData(index, QVariant::fromValue(-amountHelper->value()), (int)eLedgerModel::Role::SplitShares); } } /// @todo we need to make sure to support multiple currencies here } catch (MyMoneyException &e) { qDebug() << "Ooops: invalid account id" << accountId << "in" << Q_FUNC_INFO; } } return rc; } bool NewTransactionEditor::Private::numberChanged(const QString& newNumber) { bool rc = true; WidgetHintFrame::hide(ui->numberEdit, i18n("The check number used for this transaction.")); if(!newNumber.isEmpty()) { const LedgerModel* model = Models::instance()->ledgerModel(); QModelIndexList list = model->match(model->index(0, 0), (int)eLedgerModel::Role::Number, QVariant(newNumber), -1, // all splits Qt::MatchFlags(Qt::MatchExactly | Qt::MatchCaseSensitive | Qt::MatchRecursive)); foreach(QModelIndex index, list) { - if(model->data(index, (int)eLedgerModel::Role::AccountId) == account.id() + if(model->data(index, (int)eLedgerModel::Role::AccountId) == m_account.id() && model->data(index, (int)eLedgerModel::Role::TransactionSplitId) != transactionSplitId) { WidgetHintFrame::show(ui->numberEdit, i18n("The check number %1 has already been used in this account.", newNumber)); rc = false; break; } } } return rc; } bool NewTransactionEditor::Private::valueChanged(CreditDebitHelper* valueHelper) { bool rc = true; if(valueHelper->haveValue() && splitModel.rowCount() <= 1) { rc = false; try { MyMoneyMoney shares; if(splitModel.rowCount() == 1) { const QModelIndex index = splitModel.index(0, 0); splitModel.setData(index, QVariant::fromValue(-amountHelper->value()), (int)eLedgerModel::Role::SplitValue); /// @todo make sure to support multiple currencies splitModel.setData(index, QVariant::fromValue(-amountHelper->value()), (int)eLedgerModel::Role::SplitShares); } else { /// @todo ask what to do: if the rest of the splits is the same amount we could simply reverse the sign /// of all splits, otherwise we could ask if the user wants to start the split editor or anything else. } rc = true; } catch (MyMoneyException &e) { qDebug() << "Ooops: somwthing went wrong in" << Q_FUNC_INFO; } } return rc; } NewTransactionEditor::NewTransactionEditor(QWidget* parent, const QString& accountId) : QFrame(parent, Qt::FramelessWindowHint /* | Qt::X11BypassWindowManagerHint */) , d(new Private(this)) { auto const model = Models::instance()->accountsModel(); // extract account information from model const auto index = model->accountById(accountId); - d->account = model->data(index, (int)eAccountsModel::Role::Account).value(); + d->m_account = model->data(index, (int)eAccountsModel::Role::Account).value(); d->ui->setupUi(this); d->accountsModel->addAccountGroup(QVector {eMyMoney::Account::Type::Asset, eMyMoney::Account::Type::Liability, eMyMoney::Account::Type::Income, eMyMoney::Account::Type::Expense, eMyMoney::Account::Type::Equity}); d->accountsModel->setHideEquityAccounts(false); d->accountsModel->setSourceModel(model); d->accountsModel->setSourceColumns(model->getColumns()); d->accountsModel->sort((int)eAccountsModel::Column::Account); d->ui->accountCombo->setModel(d->accountsModel); d->costCenterModel->setSortRole(Qt::DisplayRole); d->costCenterModel->setSourceModel(Models::instance()->costCenterModel()); d->costCenterModel->sort(0); d->ui->costCenterCombo->setEditable(true); d->ui->costCenterCombo->setModel(d->costCenterModel); d->ui->costCenterCombo->setModelColumn(0); d->ui->costCenterCombo->completer()->setFilterMode(Qt::MatchContains); d->payeesModel->setSortRole(Qt::DisplayRole); d->payeesModel->setSourceModel(Models::instance()->payeesModel()); d->payeesModel->sort(0); d->ui->payeeEdit->setEditable(true); d->ui->payeeEdit->setModel(d->payeesModel); d->ui->payeeEdit->setModelColumn(0); d->ui->payeeEdit->completer()->setFilterMode(Qt::MatchContains); d->ui->enterButton->setIcon(Icons::get(Icon::DialogOK)); d->ui->cancelButton->setIcon(Icons::get(Icon::DialogCancel)); d->ui->statusCombo->setModel(&d->statusModel); d->ui->dateEdit->setDisplayFormat(QLocale().dateFormat(QLocale::ShortFormat)); d->ui->amountEditCredit->setAllowEmpty(true); d->ui->amountEditDebit->setAllowEmpty(true); d->amountHelper = new CreditDebitHelper(this, d->ui->amountEditCredit, d->ui->amountEditDebit); WidgetHintFrameCollection* frameCollection = new WidgetHintFrameCollection(this); frameCollection->addFrame(new WidgetHintFrame(d->ui->dateEdit)); frameCollection->addFrame(new WidgetHintFrame(d->ui->costCenterCombo)); frameCollection->addFrame(new WidgetHintFrame(d->ui->numberEdit, WidgetHintFrame::Warning)); frameCollection->addWidget(d->ui->enterButton); connect(d->ui->numberEdit, SIGNAL(textChanged(QString)), this, SLOT(numberChanged(QString))); connect(d->ui->costCenterCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(costCenterChanged(int))); connect(d->ui->accountCombo, SIGNAL(accountSelected(QString)), this, SLOT(categoryChanged(QString))); connect(d->ui->dateEdit, SIGNAL(dateChanged(QDate)), this, SLOT(postdateChanged(QDate))); connect(d->amountHelper, SIGNAL(valueChanged()), this, SLOT(valueChanged())); connect(d->ui->cancelButton, SIGNAL(clicked(bool)), this, SLOT(reject())); connect(d->ui->enterButton, SIGNAL(clicked(bool)), this, SLOT(acceptEdit())); connect(d->ui->splitEditorButton, SIGNAL(clicked(bool)), this, SLOT(editSplits())); // handle some events in certain conditions different from default d->ui->payeeEdit->installEventFilter(this); d->ui->costCenterCombo->installEventFilter(this); d->ui->tagComboBox->installEventFilter(this); d->ui->statusCombo->installEventFilter(this); // setup tooltip // setWindowFlags(Qt::FramelessWindowHint | Qt::X11BypassWindowManagerHint); } NewTransactionEditor::~NewTransactionEditor() { } bool NewTransactionEditor::accepted() const { return d->accepted; } void NewTransactionEditor::acceptEdit() { if(d->checkForValidTransaction()) { d->accepted = true; emit done(); } } void NewTransactionEditor::reject() { emit done(); } void NewTransactionEditor::keyPressEvent(QKeyEvent* e) { if (!e->modifiers() || (e->modifiers() & Qt::KeypadModifier && e->key() == Qt::Key_Enter)) { switch (e->key()) { case Qt::Key_Enter: case Qt::Key_Return: { if(focusWidget() == d->ui->cancelButton) { reject(); } else { if(d->ui->enterButton->isEnabled()) { d->ui->enterButton->click(); } return; } } break; case Qt::Key_Escape: reject(); break; default: e->ignore(); return; } } else { e->ignore(); } } void NewTransactionEditor::loadTransaction(const QString& id) { const LedgerModel* model = Models::instance()->ledgerModel(); const QString transactionId = model->transactionIdFromTransactionSplitId(id); if(id.isEmpty()) { d->transactionSplitId.clear(); d->transaction = MyMoneyTransaction(); if(lastUsedPostDate()->isValid()) { d->ui->dateEdit->setDate(*lastUsedPostDate()); } else { d->ui->dateEdit->setDate(QDate::currentDate()); } bool blocked = d->ui->accountCombo->lineEdit()->blockSignals(true); d->ui->accountCombo->lineEdit()->clear(); d->ui->accountCombo->lineEdit()->blockSignals(blocked); } else { // find which item has this id and set is as the current item QModelIndexList list = model->match(model->index(0, 0), (int)eLedgerModel::Role::TransactionId, QVariant(transactionId), -1, // all splits Qt::MatchFlags(Qt::MatchExactly | Qt::MatchCaseSensitive | Qt::MatchRecursive)); Q_FOREACH(QModelIndex index, list) { // the selected split? const QString transactionSplitId = model->data(index, (int)eLedgerModel::Role::TransactionSplitId).toString(); if(transactionSplitId == id) { d->transactionSplitId = id; d->transaction = model->data(index, (int)eLedgerModel::Role::Transaction).value(); d->split = model->data(index, (int)eLedgerModel::Role::Split).value(); d->ui->dateEdit->setDate(model->data(index, (int)eLedgerModel::Role::PostDate).toDate()); d->ui->payeeEdit->lineEdit()->setText(model->data(index, (int)eLedgerModel::Role::PayeeName).toString()); d->ui->memoEdit->clear(); d->ui->memoEdit->insertPlainText(model->data(index, (int)eLedgerModel::Role::Memo).toString()); d->ui->memoEdit->moveCursor(QTextCursor::Start); d->ui->memoEdit->ensureCursorVisible(); // The calculator for the amount field can simply be added as an icon to the line edit widget. // See http://stackoverflow.com/questions/11381865/how-to-make-an-extra-icon-in-qlineedit-like-this howto do it d->ui->amountEditCredit->setText(model->data(model->index(index.row(), (int)eLedgerModel::Column::Payment)).toString()); d->ui->amountEditDebit->setText(model->data(model->index(index.row(), (int)eLedgerModel::Column::Deposit)).toString()); d->ui->numberEdit->setText(model->data(index, (int)eLedgerModel::Role::Number).toString()); d->ui->statusCombo->setCurrentIndex(model->data(index, (int)eLedgerModel::Role::Number).toInt()); QModelIndexList stList = d->statusModel.match(d->statusModel.index(0, 0), Qt::UserRole+1, model->data(index, (int)eLedgerModel::Role::Reconciliation).toInt()); if(stList.count()) { QModelIndex stIndex = stList.front(); d->ui->statusCombo->setCurrentIndex(stIndex.row()); } } else { d->splitModel.addSplit(transactionSplitId); } } d->updateWidgetState(); } // set focus to date edit once we return to event loop QMetaObject::invokeMethod(d->ui->dateEdit, "setFocus", Qt::QueuedConnection); } void NewTransactionEditor::numberChanged(const QString& newNumber) { d->numberChanged(newNumber); } void NewTransactionEditor::categoryChanged(const QString& accountId) { d->categoryChanged(accountId); } void NewTransactionEditor::costCenterChanged(int costCenterIndex) { d->costCenterChanged(costCenterIndex); } void NewTransactionEditor::postdateChanged(const QDate& date) { d->postdateChanged(date); } void NewTransactionEditor::valueChanged() { d->valueChanged(d->amountHelper); } void NewTransactionEditor::editSplits() { SplitModel splitModel; splitModel.deepCopy(d->splitModel, true); // create an empty split at the end splitModel.addEmptySplitEntry(); - QPointer splitDialog = new SplitDialog(d->account, transactionAmount(), this); + QPointer splitDialog = new SplitDialog(d->m_account, transactionAmount(), this); splitDialog->setModel(&splitModel); int rc = splitDialog->exec(); if(splitDialog && (rc == QDialog::Accepted)) { // remove that empty split again before we update the splits splitModel.removeEmptySplitEntry(); // copy the splits model contents d->splitModel.deepCopy(splitModel, true); // update the transaction amount d->amountHelper->setValue(splitDialog->transactionAmount()); d->updateWidgetState(); QWidget *next = d->ui->tagComboBox; if(d->ui->costCenterCombo->isEnabled()) { next = d->ui->costCenterCombo; } next->setFocus(); } if(splitDialog) { splitDialog->deleteLater(); } } MyMoneyMoney NewTransactionEditor::transactionAmount() const { return d->amountHelper->value(); } void NewTransactionEditor::saveTransaction() { MyMoneyTransaction t; if(!d->transactionSplitId.isEmpty()) { t = d->transaction; } else { // we keep the date when adding a new transaction // for the next new one *lastUsedPostDate() = d->ui->dateEdit->date(); } QList splits = t.splits(); // first remove the splits that are gone foreach (const auto split, t.splits()) { if(split.id() == d->split.id()) { continue; } int row; for(row = 0; row < d->splitModel.rowCount(); ++row) { QModelIndex index = d->splitModel.index(row, 0); if(d->splitModel.data(index, (int)eLedgerModel::Role::SplitId).toString() == split.id()) { break; } } // if the split is not in the model, we get rid of it if(d->splitModel.rowCount() == row) { t.removeSplit(split); } } MyMoneyFileTransaction ft; try { // new we update the split we are opened for MyMoneySplit sp(d->split); sp.setNumber(d->ui->numberEdit->text()); sp.setMemo(d->ui->memoEdit->toPlainText()); sp.setShares(d->amountHelper->value()); if(t.commodity().isEmpty()) { - t.setCommodity(d->account.currencyId()); + t.setCommodity(d->m_account.currencyId()); sp.setValue(d->amountHelper->value()); } else { /// @todo check that the transactions commodity is the same /// as the one of the account this split references. If /// that is not the case, the next statement would create /// a problem sp.setValue(d->amountHelper->value()); } if(sp.reconcileFlag() != eMyMoney::Split::State::Reconciled && !sp.reconcileDate().isValid() && d->ui->statusCombo->currentIndex() == (int)eMyMoney::Split::State::Reconciled) { sp.setReconcileDate(QDate::currentDate()); } sp.setReconcileFlag(static_cast(d->ui->statusCombo->currentIndex())); // sp.setPayeeId(d->ui->payeeEdit->cu) if(sp.id().isEmpty()) { t.addSplit(sp); } else { t.modifySplit(sp); } t.setPostDate(d->ui->dateEdit->date()); // now update and add what we have in the model const SplitModel * model = &d->splitModel; for(int row = 0; row < model->rowCount(); ++row) { QModelIndex index = model->index(row, 0); MyMoneySplit s; const QString splitId = model->data(index, (int)eLedgerModel::Role::SplitId).toString(); if(!SplitModel::isNewSplitId(splitId)) { s = t.splitById(splitId); } s.setNumber(model->data(index, (int)eLedgerModel::Role::Number).toString()); s.setMemo(model->data(index, (int)eLedgerModel::Role::Memo).toString()); s.setAccountId(model->data(index, (int)eLedgerModel::Role::AccountId).toString()); s.setShares(model->data(index, (int)eLedgerModel::Role::SplitShares).value()); s.setValue(model->data(index, (int)eLedgerModel::Role::SplitValue).value()); s.setCostCenterId(model->data(index, (int)eLedgerModel::Role::CostCenterId).toString()); s.setPayeeId(model->data(index, (int)eLedgerModel::Role::PayeeId).toString()); // reconcile flag and date if(s.id().isEmpty()) { t.addSplit(s); } else { t.modifySplit(s); } } if(t.id().isEmpty()) { MyMoneyFile::instance()->addTransaction(t); } else { MyMoneyFile::instance()->modifyTransaction(t); } ft.commit(); } catch (const MyMoneyException &e) { qDebug() << Q_FUNC_INFO << "something went wrong" << e.what(); } } bool NewTransactionEditor::eventFilter(QObject* o, QEvent* e) { auto cb = qobject_cast(o); if (o) { // filter out wheel events for combo boxes if the popup view is not visible if ((e->type() == QEvent::Wheel) && !cb->view()->isVisible()) { return true; } } return QFrame::eventFilter(o, e); } // kate: space-indent on; indent-width 2; remove-trailing-space on; remove-trailing-space-save on; diff --git a/kmymoney/views/payeeidentifierselectiondelegate.h b/kmymoney/views/payeeidentifierselectiondelegate.h index 3bdcaf7ea..126ea529a 100644 --- a/kmymoney/views/payeeidentifierselectiondelegate.h +++ b/kmymoney/views/payeeidentifierselectiondelegate.h @@ -1,50 +1,50 @@ /* * This file is part of KMyMoney, A Personal Finance Manager by KDE * Copyright (C) 2014 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 PAYEEIDENTIFIERSELECTIONDELEGATE_H #define PAYEEIDENTIFIERSELECTIONDELEGATE_H #include #include class payeeIdentifierSelectionDelegate : public QStyledItemDelegate { Q_OBJECT public: explicit payeeIdentifierSelectionDelegate(QObject* parent = 0); - virtual QWidget* createEditor(QWidget* parent, const QStyleOptionViewItem& option, const QModelIndex& index) const; - virtual void setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const; - virtual void updateEditorGeometry(QWidget* editor, const QStyleOptionViewItem& option, const QModelIndex& index) const; - virtual QSize sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const; + QWidget* createEditor(QWidget* parent, const QStyleOptionViewItem& option, const QModelIndex& index) const final override; + void setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const final override; + void updateEditorGeometry(QWidget* editor, const QStyleOptionViewItem& option, const QModelIndex& index) const final override; + QSize sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const final override; }; class payeeIdentifierTypeSelectionWidget : public QComboBox { Q_OBJECT public: explicit payeeIdentifierTypeSelectionWidget(QWidget* parent = 0); Q_SIGNALS: void commitData(QWidget* editor); private Q_SLOTS: void itemSelected(int index); }; #endif // PAYEEIDENTIFIERSELECTIONDELEGATE_H diff --git a/kmymoney/views/splitdelegate.h b/kmymoney/views/splitdelegate.h index e242c545f..0e6fb2c73 100644 --- a/kmymoney/views/splitdelegate.h +++ b/kmymoney/views/splitdelegate.h @@ -1,79 +1,79 @@ /*************************************************************************** splitdelegate.h ------------------- begin : Wed Apr 6 2016 copyright : (C) 2016 by Thomas Baumgart email : 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. * * * ***************************************************************************/ #ifndef SPLITDELEGATE_H #define SPLITDELEGATE_H // ---------------------------------------------------------------------------- // QT Includes #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes class SplitDelegate : public QStyledItemDelegate { Q_OBJECT public: explicit SplitDelegate(QObject* parent = 0); virtual ~SplitDelegate(); - virtual void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const; - virtual QSize sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const; - virtual QWidget* createEditor(QWidget* parent, const QStyleOptionViewItem& option, const QModelIndex& index) const; - virtual void setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const; - virtual void setEditorData(QWidget* editWidget, const QModelIndex& index) const; + void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const final override; + QSize sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const final override; + QWidget* createEditor(QWidget* parent, const QStyleOptionViewItem& option, const QModelIndex& index) const final override; + void setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const final override; + void setEditorData(QWidget* editWidget, const QModelIndex& index) const final override; - virtual void updateEditorGeometry(QWidget* editor, const QStyleOptionViewItem& option, const QModelIndex& index) const; + void updateEditorGeometry(QWidget* editor, const QStyleOptionViewItem& option, const QModelIndex& index) const final override; /** * This method returns the row that currently has an editor * or -1 if no editor is open */ virtual int editorRow() const; void setShowValuesInverted(bool inverse); bool showValuesInverted(); static void setErroneousColor(const QColor& color); static void setImportedColor(const QColor& color); static QColor erroneousColor(); protected: - bool eventFilter(QObject* o, QEvent* event); + bool eventFilter(QObject* o, QEvent* event) final override; protected Q_SLOTS: void endEdit(); Q_SIGNALS: void sizeHintChanged(const QModelIndex&) const; private: class Private; Private * const d; static QColor m_erroneousColor; static QColor m_importedColor; }; #endif // SPLITDELEGATE_H diff --git a/kmymoney/views/splitdialog.h b/kmymoney/views/splitdialog.h index 14b1de89c..c62e35ab8 100644 --- a/kmymoney/views/splitdialog.h +++ b/kmymoney/views/splitdialog.h @@ -1,83 +1,83 @@ /*************************************************************************** splitdialog.h ------------------- begin : Sat Aug 8 2015 copyright : (C) 2015 by Thomas Baumgart email : 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. * * * ***************************************************************************/ #ifndef SPLITDIALOG_H #define SPLITDIALOG_H // ---------------------------------------------------------------------------- // QT Includes #include #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes #include "mymoneymoney.h" class QAbstractItemModel; class MyMoneyAccount; class NewTransactionEditor; class SplitDialog : public QDialog { Q_OBJECT public: explicit SplitDialog(const MyMoneyAccount& account, const MyMoneyMoney& mainAmount, NewTransactionEditor* parent, Qt::WindowFlags f = 0); virtual ~SplitDialog(); void setModel(QAbstractItemModel* model); void setAccountId(const QString& id); /** * Returns the amount for the transaction. */ MyMoneyMoney transactionAmount() const; public Q_SLOTS: - virtual void accept(); - virtual int exec(); + void accept() final override; + int exec() final override; private Q_SLOTS: void adjustSummary(); void disableButtons(); void enableButtons(); void newSplit(); protected Q_SLOTS: void deleteSelectedSplits(); void deleteAllSplits(); void deleteZeroSplits(); void mergeSplits(); void selectionChanged(); void updateButtonState(); protected: - virtual void resizeEvent(QResizeEvent* ev); + void resizeEvent(QResizeEvent* ev) final override; void adjustSummaryWidth(); private: class Private; QScopedPointer d; }; #endif // SPLITDIALOG_H diff --git a/kmymoney/views/widgethintframe.h b/kmymoney/views/widgethintframe.h index d69d1ad00..3c5e8e435 100644 --- a/kmymoney/views/widgethintframe.h +++ b/kmymoney/views/widgethintframe.h @@ -1,108 +1,108 @@ /*************************************************************************** widgethintframe.h ------------------- begin : Sat Aug 8 2015 copyright : (C) 2015 by Thomas Baumgart email : 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. * * * ***************************************************************************/ #ifndef WIDGETHINTFRAME_H #define WIDGETHINTFRAME_H // ---------------------------------------------------------------------------- // QT Includes #include class QWidget; // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes class WidgetHintFrame : public QFrame { Q_OBJECT Q_ENUMS(FrameStyle) public: enum FrameStyle { Error = 0, Warning, Info }; explicit WidgetHintFrame(QWidget* editWidget, FrameStyle style = Error, Qt::WindowFlags f = 0); ~WidgetHintFrame(); void attachToWidget(QWidget* w); void detachFromWidget(); bool isErroneous() const; QWidget* editWidget() const; /** * Shows the info frame around @a editWidget and in case @a tooltip * is not null (@sa QString::isNull()) the respective message will * be loaded into the @a editWidget's tooltip. In case @a tooltip is null * (the default) the @a editWidget's tooltip will not be changed. */ static void show(QWidget* editWidget, const QString& tooltip = QString()); /** * Hides the info frame around @a editWidget and in case @a tooltip * is not null (@sa QString::isNull()) the respective message will * be loaded into the @a editWidget's tooltip. In case @a tooltip is null * (the default) the @a editWidget's tooltip will not be changed. */ static void hide(QWidget* editWidget, const QString& tooltip = QString()); protected: - virtual bool eventFilter(QObject* o, QEvent* e); + bool eventFilter(QObject* o, QEvent* e) final override; Q_SIGNALS: void changed(); private: class Private; Private * const d; }; class WidgetHintFrameCollection : public QObject { Q_OBJECT public: explicit WidgetHintFrameCollection(QObject* parent = 0); void addFrame(WidgetHintFrame* frame); void addWidget(QWidget* w); void removeWidget(QWidget* w); public Q_SLOTS: void inputChange(); protected Q_SLOTS: virtual void frameDestroyed(QObject* o); virtual void updateWidgets(); Q_SIGNALS: void inputIsValid(bool valid); private: class Private; Private * const d; }; #endif // WIDGETHINTFRAME_H diff --git a/kmymoney/widgets/amountvalidator.cpp b/kmymoney/widgets/amountvalidator.cpp index 668c40aae..631c19052 100644 --- a/kmymoney/widgets/amountvalidator.cpp +++ b/kmymoney/widgets/amountvalidator.cpp @@ -1,151 +1,151 @@ /*************************************************************************** amountvalidator.cpp ------------------- copyright : (C) 2016 by 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 "amountvalidator.h" // ---------------------------------------------------------------------------- // QT Includes #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes AmountValidator::AmountValidator(QObject * parent) : QDoubleValidator(parent) { } AmountValidator::AmountValidator(double bottom, double top, int decimals, QObject * parent) : QDoubleValidator(bottom, top, decimals, parent) { } AmountValidator::~AmountValidator() { } /* * The code of the following function is taken from kdeui/knumvalidator.cpp * and adjusted to always use the monetary symbols defined in the KDE System Settings */ QValidator::State AmountValidator::validate(QString & input, int & _p) const { QString s = input; QLocale locale; // ok, we have to re-format the number to have: // 1. decimalSymbol == '.' // 2. negativeSign == '-' // 3. positiveSign == // 4. thousandsSeparator() == (we don't check that there // are exactly three decimals between each separator): - QString dec = locale.decimalPoint(), + QString decimalPoint = locale.decimalPoint(), n = locale.negativeSign(), p = locale.positiveSign(), - t = locale.groupSeparator(); + separatorCharacter = locale.groupSeparator(); // first, delete p's and t's: if (!p.isEmpty()) for (int idx = s.indexOf(p) ; idx >= 0 ; idx = s.indexOf(p, idx)) s.remove(idx, p.length()); - if (!t.isEmpty()) - for (int idx = s.indexOf(t) ; idx >= 0 ; idx = s.indexOf(t, idx)) - s.remove(idx, t.length()); + if (!separatorCharacter.isEmpty()) + for (int idx = s.indexOf(separatorCharacter) ; idx >= 0 ; idx = s.indexOf(separatorCharacter, idx)) + s.remove(idx, separatorCharacter.length()); // then, replace the d's and n's if ((!n.isEmpty() && n.indexOf('.') != -1) || - (!dec.isEmpty() && dec.indexOf('-') != -1)) { + (!decimalPoint.isEmpty() && decimalPoint.indexOf('-') != -1)) { // make sure we don't replace something twice: qWarning() << "KDoubleValidator: decimal symbol contains '-' or " "negative sign contains '.' -> improve algorithm" << endl; return Invalid; } - if (!dec.isEmpty() && dec != ".") - for (int idx = s.indexOf(dec) ; idx >= 0 ; idx = s.indexOf(dec, idx + 1)) - s.replace(idx, dec.length(), "."); + if (!decimalPoint.isEmpty() && decimalPoint != ".") + for (int idx = s.indexOf(decimalPoint) ; idx >= 0 ; idx = s.indexOf(decimalPoint, idx + 1)) + s.replace(idx, decimalPoint.length(), "."); if (!n.isEmpty() && n != "-") for (int idx = s.indexOf(n) ; idx >= 0 ; idx = s.indexOf(n, idx + 1)) s.replace(idx, n.length(), "-"); // TODO: port KF5 (support for paren around negative numbers) #if 0 // Take care of monetary parens around the value if selected via // the locale settings. // If the lead-in or lead-out paren is present, remove it // before passing the string to the QDoubleValidator if (l->negativeMonetarySignPosition() == KLocale::ParensAround || l->positiveMonetarySignPosition() == KLocale::ParensAround) { QRegExp regExp("^(\\()?([\\d-\\.]*)(\\))?$"); if (s.indexOf(regExp) != -1) { s = regExp.cap(2); } } #endif // check for non numeric values (QDoubleValidator allows an 'e', we don't) QRegExp nonNumeric("[^\\d-\\.]+"); if (s.indexOf(nonNumeric) != -1) return Invalid; // check for minus sign trailing the number QRegExp trailingMinus("^([^-]*)\\w*-$"); if (s.indexOf(trailingMinus) != -1) { s = QString("-%1").arg(trailingMinus.cap(1)); } // check for the maximum allowed number of decimal places int decPos = s.indexOf('.'); if (decPos != -1) { if (decimals() == 0) return Invalid; if (((int)(s.length()) - decPos) > decimals()) return Invalid; } // If we have just a single minus sign, we are done if (s == QString("-")) return Acceptable; QValidator::State rc = QDoubleValidator::validate(s, _p); // TODO: port KF5 (support for paren around negative numbers) #if 0 if (rc == Acceptable) { // If the numeric value is acceptable, we check if the parens // are ok. If only the lead-in is present, the return value // is intermediate, if only the lead-out is present then it // definitely is invalid. Nevertheless, we check for parens // only, if the locale settings have it enabled. if (l->negativeMonetarySignPosition() == KLocale::ParensAround || l->positiveMonetarySignPosition() == KLocale::ParensAround) { int tmp = input.count('(') - input.count(')'); if (tmp > 0) rc = Intermediate; else if (tmp < 0) rc = Invalid; } } #endif return rc; } diff --git a/kmymoney/widgets/kmymoneyaccountcombo.cpp b/kmymoney/widgets/kmymoneyaccountcombo.cpp index 3c651ed0f..462dd49d5 100644 --- a/kmymoney/widgets/kmymoneyaccountcombo.cpp +++ b/kmymoney/widgets/kmymoneyaccountcombo.cpp @@ -1,370 +1,370 @@ /*************************************************************************** kmymoneyaccountbutton - description ------------------- begin : Mon May 31 2004 copyright : (C) 2000-2004 by Michael Edwardes email : mte@users.sourceforge.net 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. * * * ***************************************************************************/ #include "kmymoneyaccountcombo.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes #include "mymoneyfile.h" #include "modelenums.h" class KMyMoneyAccountCombo::Private { public: Private(KMyMoneyAccountCombo* q) : m_q(q) , m_popupView(0) , m_inMakeCompletion(false) { m_q->setInsertPolicy(QComboBox::NoInsert); m_q->setMinimumWidth(m_q->fontMetrics().width(QLatin1Char('W')) * 15); m_q->setMaxVisibleItems(15); } KMyMoneyAccountCombo* m_q; QTreeView* m_popupView; QString m_lastSelectedAccount; bool m_inMakeCompletion; QString fullAccountName(const QAbstractItemModel* model, const QModelIndex& index, bool includeMainCategory = false) const; void selectFirstMatchingItem(); }; QString KMyMoneyAccountCombo::Private::fullAccountName(const QAbstractItemModel* model, const QModelIndex& _index, bool includeMainCategory) const { QString rc; if(_index.isValid()) { QModelIndex index = _index; QString sep; do { rc = QString("%1%2%3").arg(model->data(index).toString()).arg(sep).arg(rc); sep = QLatin1String(":"); index = index.parent(); } while(index.isValid()); if(!includeMainCategory) { QRegExp mainCategory(QString("[^%1]+%2(.*)").arg(sep).arg(sep)); if(mainCategory.exactMatch(rc)) { rc = mainCategory.cap(1); } } } return rc; } void KMyMoneyAccountCombo::Private::selectFirstMatchingItem() { if(m_popupView) { bool isBlocked = m_popupView->blockSignals(true); m_popupView->setCurrentIndex(QModelIndex()); for (auto i = 0; i < m_q->model()->rowCount(QModelIndex()); ++i) { QModelIndex childIndex = m_q->model()->index(i, 0); if (m_q->model()->hasChildren(childIndex)) { // search the first leaf do { childIndex = m_q->model()->index(0, 0, childIndex); } while(m_q->model()->hasChildren(childIndex)); // make it the current selection if it's selectable if(m_q->model()->flags(childIndex) & Qt::ItemIsSelectable) { m_popupView->setCurrentIndex(childIndex); } break; } } m_popupView->blockSignals(isBlocked); } } KMyMoneyAccountCombo::KMyMoneyAccountCombo(QSortFilterProxyModel *model, QWidget *parent) : KComboBox(parent) , d(new Private(this)) { setModel(model); } KMyMoneyAccountCombo::KMyMoneyAccountCombo(QWidget *parent) : KComboBox(parent) , d(new Private(this)) { } KMyMoneyAccountCombo::~KMyMoneyAccountCombo() { } void KMyMoneyAccountCombo::setEditable(bool isEditable) { KComboBox::setEditable(isEditable); // don't do the standard behavior if(lineEdit()) { lineEdit()->setObjectName("AccountComboLineEdit"); connect(lineEdit(), &QLineEdit::textEdited, this, &KMyMoneyAccountCombo::makeCompletion); } } void KMyMoneyAccountCombo::wheelEvent(QWheelEvent *ev) { Q_UNUSED(ev) // don't change anything with the help of the wheel, yet (due to the tree model) } void KMyMoneyAccountCombo::expandAll() { if (d->m_popupView) d->m_popupView->expandAll(); } void KMyMoneyAccountCombo::collapseAll() { if (d->m_popupView) d->m_popupView->collapseAll(); } void KMyMoneyAccountCombo::activated() { - QVariant data = view()->currentIndex().data((int)eAccountsModel::Role::ID); - if (data.isValid()) { - setSelected(data.toString()); + auto variant = view()->currentIndex().data((int)eAccountsModel::Role::ID); + if (variant.isValid()) { + setSelected(variant.toString()); } } bool KMyMoneyAccountCombo::eventFilter(QObject* o, QEvent* e) { if(isEditable() && o == d->m_popupView) { // propagate all relevant key press events to the lineEdit widget if(e->type() == QEvent::KeyPress) { QKeyEvent* kev = static_cast(e); bool forLineEdit = (kev->text().length() > 0); switch(kev->key()) { case Qt::Key_Escape: case Qt::Key_Up: case Qt::Key_Down: forLineEdit = false; break; default: break; } if(forLineEdit) { return lineEdit()->event(e); } } else if(e->type() == QEvent::KeyRelease) { QKeyEvent* kev = static_cast(e); switch(kev->key()) { case Qt::Key_Enter: case Qt::Key_Return: activated(); hidePopup(); break; } } else if(e->type() == QEvent::FocusOut) { // if we tab out and have a selection in the popup view // than we use that entry completely activated(); hidePopup(); } } return KComboBox::eventFilter(o, e); } void KMyMoneyAccountCombo::setSelected(const QString& id) { // make sure, we have all items available for search if(isEditable()) { lineEdit()->clear(); } // find which item has this id and set it as the current item QModelIndexList list = model()->match(model()->index(0, 0), (int)eAccountsModel::Role::ID, QVariant(id), 1, Qt::MatchFlags(Qt::MatchExactly | Qt::MatchWrap | Qt::MatchRecursive)); // CAUTION: Without Qt::MatchWrap no results for credit card, so nothing happens in ledger view if (!list.isEmpty()) { // make sure the popup is closed from here on hidePopup(); d->m_lastSelectedAccount = id; QModelIndex index = list.front(); if(isEditable()) { lineEdit()->setText(d->fullAccountName(model(), index)); } else { // ensure that combobox is properly set when KMyMoneyAccountCombo::setSelected is called programmatically blockSignals(true); setRootModelIndex(index.parent()); setCurrentIndex(index.row()); setRootModelIndex(QModelIndex()); blockSignals(false); } emit accountSelected(id); } } const QString& KMyMoneyAccountCombo::getSelected() const { return d->m_lastSelectedAccount; } void KMyMoneyAccountCombo::setModel(QSortFilterProxyModel *model) { // CAUTION! Assumption is being made that Account column number is always 0 if ((int)eAccountsModel::Column::Account != 0) { qFatal("eAccountsModel::Column::Account must be 0 in modelenums.h"); } // since we create a new popup view, we get rid of an existing one delete d->m_popupView; // call base class implementation KComboBox::setModel(model); // setup filtering criteria model->setFilterKeyColumn((int)eAccountsModel::Column::Account); model->setFilterRole((int)eAccountsModel::Role::FullName); // create popup view, attach model and allow to select a single item d->m_popupView = new QTreeView(this); d->m_popupView->setModel(model); d->m_popupView->setSelectionMode(QAbstractItemView::SingleSelection); setView(d->m_popupView); // setup view parameters d->m_popupView->setHeaderHidden(true); d->m_popupView->setRootIsDecorated(true); d->m_popupView->setAlternatingRowColors(true); d->m_popupView->setAnimated(true); d->m_popupView->expandAll(); // setup signal connections connect(d->m_popupView, &QAbstractItemView::activated, this, &KMyMoneyAccountCombo::selectItem); if(isEditable()) { connect(lineEdit(), &QLineEdit::textEdited, this, &KMyMoneyAccountCombo::makeCompletion); } else { connect(this, static_cast(&KMyMoneyAccountCombo::KComboBox::activated), this, &KMyMoneyAccountCombo::activated); } } void KMyMoneyAccountCombo::selectItem(const QModelIndex& index) { if(index.isValid() && (model()->flags(index) & Qt::ItemIsSelectable)) { setSelected(model()->data(index, (int)eAccountsModel::Role::ID).toString()); } } void KMyMoneyAccountCombo::makeCompletion(const QString& txt) { if(!d->m_inMakeCompletion) { d->m_inMakeCompletion = true; AccountNamesFilterProxyModel* filterModel = qobject_cast(model()); if(filterModel) { const auto completionStr = QStringLiteral(".*"); if (txt.contains(MyMoneyFile::AccountSeparator) == 0) { // for some reason it helps to avoid internal errors if we // clear the filter before setting it to a new value filterModel->setFilterFixedString(QString()); const auto filterString = QString::fromLatin1("%1%2%3").arg(completionStr).arg(QRegExp::escape(txt)).arg(completionStr); filterModel->setFilterRegExp(QRegExp(filterString, Qt::CaseInsensitive)); } else { QStringList parts = txt.split(MyMoneyFile::AccountSeparator /*, QString::SkipEmptyParts */); QString pattern; QStringList::iterator it; for (it = parts.begin(); it != parts.end(); ++it) { if (pattern.length() > 1) pattern += MyMoneyFile::AccountSeparator; pattern += QRegExp::escape(QString(*it).trimmed()) + completionStr; } // for some reason it helps to avoid internal errors if we // clear the filter before setting it to a new value filterModel->setFilterFixedString(QString()); filterModel->setFilterRegExp(QRegExp(pattern, Qt::CaseInsensitive)); // if we don't have a match, we try it again, but this time // we add a wildcard for the top level if (filterModel->visibleItems() == 0) { // for some reason it helps to avoid internal errors if we // clear the filter before setting it to a new value pattern = pattern.prepend(completionStr + MyMoneyFile::AccountSeparator); filterModel->setFilterFixedString(QString()); filterModel->setFilterRegExp(QRegExp(pattern, Qt::CaseInsensitive)); } } // if nothing is shown, we might as well close the popup switch(filterModel->visibleItems()) { case 0: hidePopup(); break; default: setMaxVisibleItems(15); expandAll(); showPopup(); break; } d->selectFirstMatchingItem(); // keep current text in edit widget no matter what bool blocked = lineEdit()->signalsBlocked(); lineEdit()->blockSignals(true); lineEdit()->setText(txt); lineEdit()->blockSignals(blocked); } d->m_inMakeCompletion = false; } } void KMyMoneyAccountCombo::showPopup() { if(d->m_popupView) { d->m_popupView->show(); d->m_popupView->installEventFilter(this); } KComboBox::showPopup(); } void KMyMoneyAccountCombo::hidePopup() { if(d->m_popupView) { d->m_popupView->hide(); d->m_popupView->removeEventFilter(this); } KComboBox::hidePopup(); } // kate: space-indent on; indent-width 2; remove-trailing-space on; remove-trailing-space-save on; diff --git a/kmymoney/widgets/kmymoneyaccountcombo.h b/kmymoney/widgets/kmymoneyaccountcombo.h index 5a38acb1d..daf3c717b 100644 --- a/kmymoney/widgets/kmymoneyaccountcombo.h +++ b/kmymoney/widgets/kmymoneyaccountcombo.h @@ -1,180 +1,180 @@ /*************************************************************************** kmymoneyaccountcombo - description ------------------- begin : Mon May 31 2004 copyright : (C) 2000-2004 by Michael Edwardes email : mte@users.sourceforge.net 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 KMYMONEYACCOUNTCOMBO_H #define KMYMONEYACCOUNTCOMBO_H // ---------------------------------------------------------------------------- // QT Includes // ---------------------------------------------------------------------------- // KDE Includes #include // ---------------------------------------------------------------------------- // Project Includes #include "accountsproxymodel.h" #include "onlinebankingaccountsfilterproxymodel.h" /** * A proxy model used to filter all the data from the core accounts model leaving * only the name of the accounts so this model can be used in the account * completion combo. * * It shows only the first column (account name) and makes top level items non-selectable. * * @see AccountsModel * @see AccountsFilterProxyModel * * @author Cristian Onet 2010 * @author Christian David */ template class AccountNamesFilterProxyModelTpl : public baseProxyModel { public: explicit AccountNamesFilterProxyModelTpl(QObject *parent = 0); - virtual Qt::ItemFlags flags(const QModelIndex &index) const; + virtual Qt::ItemFlags flags(const QModelIndex &index) const override; protected: - bool filterAcceptsColumn(int source_column, const QModelIndex &source_parent) const; + bool filterAcceptsColumn(int source_column, const QModelIndex &source_parent) const override; }; /** * @brief "typedef" for AccountNamesFilterProxyModelTpl * * To create valid Qt moc data this class inherits the template and uses Q_OBJECT. * * @code * typedef AccountNamesFilterProxyModelTpl AccountNamesFilterProxyModel; * @endcode * * should work as well. */ class AccountNamesFilterProxyModel : public AccountNamesFilterProxyModelTpl { Q_OBJECT public: explicit AccountNamesFilterProxyModel(QObject* parent = 0) : AccountNamesFilterProxyModelTpl< AccountsProxyModel >(parent) {} }; /** * @brief OnlineBankingAccountFilterProxyModel showing only the name column * * Is equivalent to AccountNamesFilterProxyModel using OnlineBankingAccountFilterProxyModel as base. */ typedef AccountNamesFilterProxyModelTpl OnlineBankingAccountNamesFilterProxyModel; /** * @brief A general account selection widget based on a KComboBox * * This widget allows to select an account from the provided set of accounts. This * set is passed as model in the constructor or via setModel(). In case the widget * is configured to be editable via setEditable() the combo box contains a lineedit * widget. This lineedit provides auto completion. * * In addition to the KComboBox which supports a list view popup, this widget * provides a tree view popup to show the account hierarchy. * * @author Cristian Onet */ class KMyMoneyAccountCombo : public KComboBox { Q_OBJECT Q_DISABLE_COPY(KMyMoneyAccountCombo) public: explicit KMyMoneyAccountCombo(QSortFilterProxyModel *model, QWidget* parent = nullptr); explicit KMyMoneyAccountCombo(QWidget* parent = nullptr); ~KMyMoneyAccountCombo(); void setSelected(const QString& id); const QString& getSelected() const; void setModel(QSortFilterProxyModel *model); /** * Overridden to get specific behavior */ void setEditable(bool isEditable); bool eventFilter(QObject* o, QEvent* e) override; public Q_SLOTS: void expandAll(); void collapseAll(); void showPopup() override; void hidePopup() override; protected: void wheelEvent(QWheelEvent *ev) override; protected Q_SLOTS: void activated(); void makeCompletion(const QString& txt) override; void selectItem(const QModelIndex& index); Q_SIGNALS: void accountSelected(const QString&); private: class Private; QScopedPointer const d; }; template AccountNamesFilterProxyModelTpl::AccountNamesFilterProxyModelTpl(QObject *parent) : baseProxyModel(parent) { } /** * Top items are not selectable because they are not real accounts but are only used for grouping. */ template Qt::ItemFlags AccountNamesFilterProxyModelTpl::flags(const QModelIndex &index) const { if (!index.parent().isValid()) return baseProxyModel::flags(index) & ~Qt::ItemIsSelectable; return baseProxyModel::flags(index); } /** * Filter all but the first column. */ template bool AccountNamesFilterProxyModelTpl::filterAcceptsColumn(int source_column, const QModelIndex &source_parent) const { Q_UNUSED(source_parent) if (source_column == 0) return true; return false; } #endif // kate: space-indent on; indent-width 2; remove-trailing-space on; remove-trailing-space-save on; diff --git a/kmymoney/widgets/kmymoneyaccountselector.cpp b/kmymoney/widgets/kmymoneyaccountselector.cpp index a786bfbfe..faf276f9c 100644 --- a/kmymoney/widgets/kmymoneyaccountselector.cpp +++ b/kmymoney/widgets/kmymoneyaccountselector.cpp @@ -1,579 +1,578 @@ /*************************************************************************** kmymoneyaccountselector.cpp - description ------------------- begin : Thu Sep 18 2003 copyright : (C) 2003 by Thomas Baumgart email : mte@users.sourceforge.net 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. * * * ***************************************************************************/ #include "kmymoneyaccountselector.h" #include "kmymoneyselector_p.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include // ---------------------------------------------------------------------------- // Project Includes #include "mymoneyfile.h" #include "mymoneyaccount.h" #include "kmymoneysettings.h" #include "icons/icons.h" #include "mymoneyenums.h" #include "dialogenums.h" #include "widgetenums.h" using namespace Icons; using namespace eMyMoney; class KMyMoneyAccountSelectorPrivate : public KMyMoneySelectorPrivate { Q_DISABLE_COPY(KMyMoneyAccountSelectorPrivate) public: KMyMoneyAccountSelectorPrivate(KMyMoneyAccountSelector *qq) : KMyMoneySelectorPrivate(qq), m_allAccountsButton(0), m_noAccountButton(0), m_incomeCategoriesButton(0), m_expenseCategoriesButton(0) { } QPushButton* m_allAccountsButton; QPushButton* m_noAccountButton; QPushButton* m_incomeCategoriesButton; QPushButton* m_expenseCategoriesButton; QList m_typeList; QStringList m_accountList; }; KMyMoneyAccountSelector::KMyMoneyAccountSelector(QWidget *parent, Qt::WindowFlags flags, const bool createButtons) : KMyMoneySelector(*new KMyMoneyAccountSelectorPrivate(this), parent, flags) { Q_D(KMyMoneyAccountSelector); if (createButtons) { QVBoxLayout* buttonLayout = new QVBoxLayout(); buttonLayout->setSpacing(6); d->m_allAccountsButton = new QPushButton(this); d->m_allAccountsButton->setObjectName("m_allAccountsButton"); d->m_allAccountsButton->setText(i18nc("Select all accounts", "All")); buttonLayout->addWidget(d->m_allAccountsButton); d->m_incomeCategoriesButton = new QPushButton(this); d->m_incomeCategoriesButton->setObjectName("m_incomeCategoriesButton"); d->m_incomeCategoriesButton->setText(i18n("Income")); buttonLayout->addWidget(d->m_incomeCategoriesButton); d->m_expenseCategoriesButton = new QPushButton(this); d->m_expenseCategoriesButton->setObjectName("m_expenseCategoriesButton"); d->m_expenseCategoriesButton->setText(i18n("Expense")); buttonLayout->addWidget(d->m_expenseCategoriesButton); d->m_noAccountButton = new QPushButton(this); d->m_noAccountButton->setObjectName("m_noAccountButton"); d->m_noAccountButton->setText(i18nc("No account", "None")); buttonLayout->addWidget(d->m_noAccountButton); QSpacerItem* spacer = new QSpacerItem(0, 67, QSizePolicy::Minimum, QSizePolicy::Expanding); buttonLayout->addItem(spacer); d->m_layout->addLayout(buttonLayout); connect(d->m_allAccountsButton, &QAbstractButton::clicked, this, &KMyMoneyAccountSelector::slotSelectAllAccounts); connect(d->m_noAccountButton, &QAbstractButton::clicked, this, &KMyMoneyAccountSelector::slotDeselectAllAccounts); connect(d->m_incomeCategoriesButton, &QAbstractButton::clicked, this, &KMyMoneyAccountSelector::slotSelectIncomeCategories); connect(d->m_expenseCategoriesButton, &QAbstractButton::clicked, this, &KMyMoneyAccountSelector::slotSelectExpenseCategories); } } KMyMoneyAccountSelector::~KMyMoneyAccountSelector() { } void KMyMoneyAccountSelector::removeButtons() { Q_D(KMyMoneyAccountSelector); delete d->m_allAccountsButton; delete d->m_incomeCategoriesButton; delete d->m_expenseCategoriesButton; delete d->m_noAccountButton; } void KMyMoneyAccountSelector::slotSelectAllAccounts() { selectAllItems(true); } void KMyMoneyAccountSelector::slotDeselectAllAccounts() { selectAllItems(false); } void KMyMoneyAccountSelector::selectCategories(const bool income, const bool expense) { Q_D(KMyMoneyAccountSelector); QTreeWidgetItemIterator it_v(d->m_treeWidget); for (; *it_v != 0; ++it_v) { if ((*it_v)->text(0) == i18n("Income categories")) selectAllSubItems(*it_v, income); else if ((*it_v)->text(0) == i18n("Expense categories")) selectAllSubItems(*it_v, expense); } emit stateChanged(); } void KMyMoneyAccountSelector::slotSelectIncomeCategories() { selectCategories(true, false); } void KMyMoneyAccountSelector::slotSelectExpenseCategories() { selectCategories(false, true); } void KMyMoneyAccountSelector::setSelectionMode(QTreeWidget::SelectionMode mode) { Q_D(KMyMoneyAccountSelector); d->m_incomeCategoriesButton->setHidden(mode == QTreeWidget::MultiSelection); d->m_expenseCategoriesButton->setHidden(mode == QTreeWidget::MultiSelection); KMyMoneySelector::setSelectionMode(mode); } QStringList KMyMoneyAccountSelector::accountList(const QList& filterList) const { Q_D(const KMyMoneyAccountSelector); QStringList list; QTreeWidgetItemIterator it(d->m_treeWidget, QTreeWidgetItemIterator::Selectable); while (*it) { QVariant id = (*it)->data(0, (int)eWidgets::Selector::Role::Id); MyMoneyAccount acc = MyMoneyFile::instance()->account(id.toString()); if (filterList.count() == 0 || filterList.contains(acc.accountType())) list << id.toString(); it++; } return list; } QStringList KMyMoneyAccountSelector::accountList() const { return accountList(QList()); } bool KMyMoneyAccountSelector::match(const QRegExp& exp, QTreeWidgetItem* item) const { if (!item->flags().testFlag(Qt::ItemIsSelectable)) return false; return exp.indexIn(item->data(0, (int)eWidgets::Selector::Role::Key).toString().mid(1)) != -1; } bool KMyMoneyAccountSelector::contains(const QString& txt) const { Q_D(const KMyMoneyAccountSelector); QTreeWidgetItemIterator it(d->m_treeWidget, QTreeWidgetItemIterator::Selectable); QTreeWidgetItem* it_v; QString baseName = i18n("Asset") + '|' + i18n("Liability") + '|' + i18n("Income") + '|' + i18n("Expense") + '|' + i18n("Equity") + '|' + i18n("Security"); while ((it_v = *it) != 0) { QRegExp exp(QString("^(?:%1):%2$").arg(baseName).arg(QRegExp::escape(txt))); if (exp.indexIn(it_v->data(0, (int)eWidgets::Selector::Role::Key).toString().mid(1)) != -1) { return true; } it++; } return false; } class AccountSetPrivate { Q_DISABLE_COPY(AccountSetPrivate) public: AccountSetPrivate() : m_count(0), m_file(MyMoneyFile::instance()), m_favorites(0), m_hideClosedAccounts(true) { } int m_count; MyMoneyFile* m_file; QList m_typeList; QTreeWidgetItem* m_favorites; bool m_hideClosedAccounts; }; AccountSet::AccountSet() : d_ptr(new AccountSetPrivate) { } AccountSet::~AccountSet() { Q_D(AccountSet); delete d; } void AccountSet::addAccountGroup(Account::Type group) { Q_D(AccountSet); if (group == Account::Type::Asset) { d->m_typeList << Account::Type::Checkings; d->m_typeList << Account::Type::Savings; d->m_typeList << Account::Type::Cash; d->m_typeList << Account::Type::AssetLoan; d->m_typeList << Account::Type::CertificateDep; d->m_typeList << Account::Type::Investment; d->m_typeList << Account::Type::Stock; d->m_typeList << Account::Type::MoneyMarket; d->m_typeList << Account::Type::Asset; d->m_typeList << Account::Type::Currency; } else if (group == Account::Type::Liability) { d->m_typeList << Account::Type::CreditCard; d->m_typeList << Account::Type::Loan; d->m_typeList << Account::Type::Liability; } else if (group == Account::Type::Income) { d->m_typeList << Account::Type::Income; } else if (group == Account::Type::Expense) { d->m_typeList << Account::Type::Expense; } else if (group == Account::Type::Equity) { d->m_typeList << Account::Type::Equity; } } void AccountSet::addAccountType(Account::Type type) { Q_D(AccountSet); d->m_typeList << type; } void AccountSet::removeAccountType(Account::Type type) { Q_D(AccountSet); int index = d->m_typeList.indexOf(type); if (index != -1) { d->m_typeList.removeAt(index); } } void AccountSet::clear() { Q_D(AccountSet); d->m_typeList.clear(); } int AccountSet::load(KMyMoneyAccountSelector* selector) { Q_D(AccountSet); QStringList list; QStringList::ConstIterator it_l; int count = 0; int typeMask = 0; QString currentId; if (selector->selectionMode() == QTreeWidget::SingleSelection) { - QStringList list; - selector->selectedItems(list); - if (!list.isEmpty()) - currentId = list.first(); - } + selector->selectedItems(list); + if (!list.isEmpty()) + currentId = list.first(); + } if (d->m_typeList.contains(Account::Type::Checkings) || d->m_typeList.contains(Account::Type::Savings) || d->m_typeList.contains(Account::Type::Cash) || d->m_typeList.contains(Account::Type::AssetLoan) || d->m_typeList.contains(Account::Type::CertificateDep) || d->m_typeList.contains(Account::Type::Investment) || d->m_typeList.contains(Account::Type::Stock) || d->m_typeList.contains(Account::Type::MoneyMarket) || d->m_typeList.contains(Account::Type::Asset) || d->m_typeList.contains(Account::Type::Currency)) typeMask |= eDialogs::Category::asset; if (d->m_typeList.contains(Account::Type::CreditCard) || d->m_typeList.contains(Account::Type::Loan) || d->m_typeList.contains(Account::Type::Liability)) typeMask |= eDialogs::Category::liability; if (d->m_typeList.contains(Account::Type::Income)) typeMask |= eDialogs::Category::income; if (d->m_typeList.contains(Account::Type::Expense)) typeMask |= eDialogs::Category::expense; if (d->m_typeList.contains(Account::Type::Equity)) typeMask |= eDialogs::Category::equity; selector->clear(); QTreeWidget* lv = selector->listView(); d->m_count = 0; QString key; QTreeWidgetItem* after = 0; // create the favorite section first and sort it to the beginning key = QString("A%1").arg(i18n("Favorites")); d->m_favorites = selector->newItem(i18n("Favorites"), key); //get the account icon from cache or insert it if it is not there QPixmap accountPixmap; if (!QPixmapCache::find("account", accountPixmap)) { QIcon icon = Icons::get(Icon::ViewBankAccount); if (!icon.availableSizes().isEmpty()) accountPixmap = icon.pixmap(icon.availableSizes().first()); QPixmapCache::insert("account", accountPixmap); } d->m_favorites->setIcon(0, QIcon(accountPixmap)); for (auto mask = 0x01; mask != eDialogs::Category::last; mask <<= 1) { QTreeWidgetItem* item = 0; if ((typeMask & mask & eDialogs::Category::asset) != 0) { ++d->m_count; key = QString("B%1").arg(i18n("Asset")); item = selector->newItem(i18n("Asset accounts"), key); item->setIcon(0, d->m_file->asset().accountPixmap()); list = d->m_file->asset().accountList(); } if ((typeMask & mask & eDialogs::Category::liability) != 0) { ++d->m_count; key = QString("C%1").arg(i18n("Liability")); item = selector->newItem(i18n("Liability accounts"), key); item->setIcon(0, d->m_file->liability().accountPixmap()); list = d->m_file->liability().accountList(); } if ((typeMask & mask & eDialogs::Category::income) != 0) { ++d->m_count; key = QString("D%1").arg(i18n("Income")); item = selector->newItem(i18n("Income categories"), key); item->setIcon(0, d->m_file->income().accountPixmap()); list = d->m_file->income().accountList(); if (selector->selectionMode() == QTreeWidget::MultiSelection) { selector->d_func()->m_incomeCategoriesButton->show(); } } if ((typeMask & mask & eDialogs::Category::expense) != 0) { ++d->m_count; key = QString("E%1").arg(i18n("Expense")); item = selector->newItem(i18n("Expense categories"), key); item->setIcon(0, d->m_file->expense().accountPixmap()); list = d->m_file->expense().accountList(); if (selector->selectionMode() == QTreeWidget::MultiSelection) { selector->d_func()->m_expenseCategoriesButton->show(); } } if ((typeMask & mask & eDialogs::Category::equity) != 0) { ++d->m_count; key = QString("F%1").arg(i18n("Equity")); item = selector->newItem(i18n("Equity accounts"), key); item->setIcon(0, d->m_file->equity().accountPixmap()); list = d->m_file->equity().accountList(); } if (!after) after = item; if (item != 0) { // scan all matching accounts found in the engine for (it_l = list.constBegin(); it_l != list.constEnd(); ++it_l) { const MyMoneyAccount& acc = d->m_file->account(*it_l); ++d->m_count; ++count; //this will include an account if it matches the account type and //if it is still open or it has been set to show closed accounts if (includeAccount(acc) && (!isHidingClosedAccounts() || !acc.isClosed())) { QString tmpKey; tmpKey = key + MyMoneyFile::AccountSeparator + acc.name(); QTreeWidgetItem* subItem = selector->newItem(item, acc.name(), tmpKey, acc.id()); subItem->setIcon(0, acc.accountPixmap()); if (acc.value("PreferredAccount") == "Yes" && d->m_typeList.contains(acc.accountType())) { selector->newItem(d->m_favorites, acc.name(), tmpKey, acc.id())->setIcon(0, acc.accountPixmap());; } if (acc.accountList().count() > 0) { subItem->setExpanded(true); count += loadSubAccounts(selector, subItem, tmpKey, acc.accountList()); } // the item is not selectable if it has been added only because a subaccount matches the type if (!d->m_typeList.contains(acc.accountType())) { selector->setSelectable(subItem, false); } subItem->sortChildren(1, Qt::AscendingOrder); } } item->sortChildren(1, Qt::AscendingOrder); } } d->m_favorites->sortChildren(1, Qt::AscendingOrder); lv->invisibleRootItem()->sortChildren(1, Qt::AscendingOrder); // if we don't have a favorite account or the selector is for multi-mode // we get rid of the favorite entry and subentries. if (d->m_favorites->childCount() == 0 || selector->selectionMode() == QTreeWidget::MultiSelection) { delete d->m_favorites; d->m_favorites = 0; } if (lv->itemAt(0, 0)) { if (currentId.isEmpty()) { lv->setCurrentItem(lv->itemAt(0, 0)); lv->clearSelection(); } else { selector->setSelected(currentId); } } selector->update(); return count; } int AccountSet::load(KMyMoneyAccountSelector* selector, const QString& baseName, const QList& accountIdList, const bool clear) { Q_D(AccountSet); int count = 0; QTreeWidgetItem* item = 0; d->m_typeList.clear(); if (clear) { d->m_count = 0; selector->clear(); } item = selector->newItem(baseName); ++d->m_count; QList::ConstIterator it; for (it = accountIdList.constBegin(); it != accountIdList.constEnd(); ++it) { const MyMoneyAccount& acc = d->m_file->account(*it); if (acc.isClosed()) continue; QString tmpKey; // the first character must be preset. Since we don't know any sort order here, we just use A tmpKey = QString("A%1%2%3").arg(baseName, MyMoneyFile::AccountSeparator, acc.name()); selector->newItem(item, acc.name(), tmpKey, acc.id())->setIcon(0, acc.accountPixmap()); ++d->m_count; ++count; } QTreeWidget* lv = selector->listView(); if (lv->itemAt(0, 0)) { lv->setCurrentItem(lv->itemAt(0, 0)); lv->clearSelection(); } selector->update(); return count; } int AccountSet::count() const { Q_D(const AccountSet); return d->m_count; } void AccountSet::setHideClosedAccounts(bool _bool) { Q_D(AccountSet); d->m_hideClosedAccounts = _bool; } bool AccountSet::isHidingClosedAccounts() const { Q_D(const AccountSet); return d->m_hideClosedAccounts; } int AccountSet::loadSubAccounts(KMyMoneyAccountSelector* selector, QTreeWidgetItem* parent, const QString& key, const QStringList& list) { Q_D(AccountSet); QStringList::ConstIterator it_l; int count = 0; for (it_l = list.constBegin(); it_l != list.constEnd(); ++it_l) { const MyMoneyAccount& acc = d->m_file->account(*it_l); // don't include stock accounts if not in expert mode if (acc.isInvest() && !KMyMoneySettings::expertMode()) continue; //this will include an account if it matches the account type and //if it is still open or it has been set to show closed accounts if (includeAccount(acc) && (!isHidingClosedAccounts() || !acc.isClosed())) { QString tmpKey; tmpKey = key + MyMoneyFile::AccountSeparator + acc.name(); ++count; ++d->m_count; QTreeWidgetItem* item = selector->newItem(parent, acc.name(), tmpKey, acc.id()); item->setIcon(0, acc.accountPixmap()); if (acc.value("PreferredAccount") == "Yes" && d->m_typeList.contains(acc.accountType())) { selector->newItem(d->m_favorites, acc.name(), tmpKey, acc.id())->setIcon(0, acc.accountPixmap()); } if (acc.accountList().count() > 0) { item->setExpanded(true); count += loadSubAccounts(selector, item, tmpKey, acc.accountList()); } // the item is not selectable if it has been added only because a subaccount matches the type if (!d->m_typeList.contains(acc.accountType())) { selector->setSelectable(item, false); } item->sortChildren(1, Qt::AscendingOrder); } } return count; } bool AccountSet::includeAccount(const MyMoneyAccount& acc) { Q_D(AccountSet); if (d->m_typeList.contains(acc.accountType())) return true; foreach (const auto sAccount, acc.accountList()) if (includeAccount(d->m_file->account(sAccount))) return true; return false; } diff --git a/kmymoney/widgets/kmymoneyaccounttreeview.cpp b/kmymoney/widgets/kmymoneyaccounttreeview.cpp index 26336031f..201013d2c 100644 --- a/kmymoney/widgets/kmymoneyaccounttreeview.cpp +++ b/kmymoney/widgets/kmymoneyaccounttreeview.cpp @@ -1,263 +1,263 @@ /*************************************************************************** * Copyright 2010 Cristian Onet onet.cristian@gmail.com * * Copyright 2017 Łukasz Wojniłowicz lukasz.wojnilowicz@gmail.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) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of version 3 of the license. * * * * 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 "kmymoneyaccounttreeview.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include // ---------------------------------------------------------------------------- // Project Includes #include "mymoneyinstitution.h" #include "mymoneyaccount.h" #include "models.h" #include "accountsmodel.h" #include "accountsviewproxymodel.h" #include "budgetviewproxymodel.h" #include "modelenums.h" #include "mymoneyenums.h" #include "viewenums.h" class KMyMoneyAccountTreeViewPrivate { Q_DISABLE_COPY(KMyMoneyAccountTreeViewPrivate) Q_DECLARE_PUBLIC(KMyMoneyAccountTreeView) public: explicit KMyMoneyAccountTreeViewPrivate(KMyMoneyAccountTreeView *qq) : q_ptr(qq), m_model(nullptr), m_view(View::None) { } ~KMyMoneyAccountTreeViewPrivate() { } QVector getVisibleGroups(const View view) { switch (view) { case View::Institutions: case View::Accounts: return QVector {eMyMoney::Account::Type::Asset, eMyMoney::Account::Type::Liability, eMyMoney::Account::Type::Equity}; case View::Categories: case View::Budget: return QVector {eMyMoney::Account::Type::Income, eMyMoney::Account::Type::Expense}; default: return QVector (); } } QSet readVisibleColumns(const View view) { QSet columns; const auto grp = KSharedConfig::openConfig()->group(getConfGrpName(view)); const auto cfgColumns = grp.readEntry("ColumnsSelection", QList()); columns.insert(eAccountsModel::Column::Account); foreach (const auto column, cfgColumns) columns.insert(static_cast(column)); return columns; } void openIndex(const QModelIndex &index) { Q_Q(KMyMoneyAccountTreeView); if (index.isValid()) { QVariant data = q->model()->data(index, (int)eAccountsModel::Role::Account); if (data.isValid()) { if (data.canConvert()) { emit q->selectByObject(data.value(), eView::Intent::OpenObject); } if (data.canConvert()) { emit q->selectByObject(data.value(), eView::Intent::OpenObject); } } } } static QString getConfGrpName(const View view) { switch (view) { case View::Institutions: return QStringLiteral("KInstitutionsView"); case View::Accounts: return QStringLiteral("KAccountsView"); case View::Categories: return QStringLiteral("KCategoriesView"); case View::Budget: return QStringLiteral("KBudgetsView"); default: return QString(); } } KMyMoneyAccountTreeView *q_ptr; AccountsViewProxyModel *m_model; View m_view; }; KMyMoneyAccountTreeView::KMyMoneyAccountTreeView(QWidget *parent) : QTreeView(parent), d_ptr(new KMyMoneyAccountTreeViewPrivate(this)) { setContextMenuPolicy(Qt::CustomContextMenu); // allow context menu to be opened on tree items header()->setContextMenuPolicy(Qt::CustomContextMenu); // allow context menu to be opened on tree header for columns selection connect(this, &QWidget::customContextMenuRequested, this, &KMyMoneyAccountTreeView::customContextMenuRequested); setAllColumnsShowFocus(true); setAlternatingRowColors(true); setIconSize(QSize(22, 22)); setSortingEnabled(true); } KMyMoneyAccountTreeView::~KMyMoneyAccountTreeView() { Q_D(KMyMoneyAccountTreeView); if (d->m_view != View::None) { auto grp = KSharedConfig::openConfig()->group(d->getConfGrpName(d->m_view)); const auto columns = header()->saveState(); grp.writeEntry("HeaderState", columns); QList visColumns; foreach (const auto column, d->m_model->getVisibleColumns()) visColumns.append(static_cast(column)); grp.writeEntry("ColumnsSelection", visColumns); grp.sync(); } delete d; } AccountsViewProxyModel *KMyMoneyAccountTreeView::init(View view) { Q_D(KMyMoneyAccountTreeView); d->m_view = view; if (view != View::Budget) d->m_model = new AccountsViewProxyModel(this); else d->m_model = new BudgetViewProxyModel(this); d->m_model->addAccountGroup(d->getVisibleGroups(view)); const auto accountsModel = Models::instance()->accountsModel(); const auto institutionsModel = Models::instance()->institutionsModel(); AccountsModel *sourceModel; if (view != View::Institutions) sourceModel = accountsModel; else sourceModel = institutionsModel; foreach (const auto column, d->readVisibleColumns(view)) { d->m_model->setColumnVisibility(column, true); accountsModel->setColumnVisibility(column, true); institutionsModel->setColumnVisibility(column, true); } d->m_model->setSourceModel(sourceModel); d->m_model->setSourceColumns(sourceModel->getColumns()); setModel(d->m_model); connect(this->header(), &QWidget::customContextMenuRequested, d->m_model, &AccountsViewProxyModel::slotColumnsMenu); connect(d->m_model, &AccountsViewProxyModel::columnToggled, this, &KMyMoneyAccountTreeView::slotColumnToggled); // restore the headers const auto grp = KSharedConfig::openConfig()->group(d->getConfGrpName(view)); const auto columnNames = grp.readEntry("HeaderState", QByteArray()); header()->restoreState(columnNames); return d->m_model; } void KMyMoneyAccountTreeView::mouseDoubleClickEvent(QMouseEvent *event) { Q_D(KMyMoneyAccountTreeView); d->openIndex(currentIndex()); event->accept(); } void KMyMoneyAccountTreeView::keyPressEvent(QKeyEvent *event) { Q_D(KMyMoneyAccountTreeView); if (event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter) { d->openIndex(currentIndex()); event->accept(); } else { QTreeView::keyPressEvent(event); } } void KMyMoneyAccountTreeView::customContextMenuRequested(const QPoint) { const auto index = model()->index(currentIndex().row(), (int)eAccountsModel::Column::Account, currentIndex().parent()); if (index.isValid() && (model()->flags(index) & Qt::ItemIsSelectable)) { - const auto data = model()->data(index, (int)eAccountsModel::Role::Account); - if (data.isValid()) { - if (data.canConvert()) { - emit selectByObject(data.value(), eView::Intent::None); - emit selectByObject(data.value(), eView::Intent::OpenContextMenu); + const auto dataVariant = model()->data(index, (int)eAccountsModel::Role::Account); + if (dataVariant.isValid()) { + if (dataVariant.canConvert()) { + emit selectByObject(dataVariant.value(), eView::Intent::None); + emit selectByObject(dataVariant.value(), eView::Intent::OpenContextMenu); } - if (data.canConvert()) { - emit selectByObject(data.value(), eView::Intent::None); - emit selectByObject(data.value(), eView::Intent::OpenContextMenu); + if (dataVariant.canConvert()) { + emit selectByObject(dataVariant.value(), eView::Intent::None); + emit selectByObject(dataVariant.value(), eView::Intent::OpenContextMenu); } } } } void KMyMoneyAccountTreeView::selectionChanged(const QItemSelection &selected, const QItemSelection &deselected) { QTreeView::selectionChanged(selected, deselected); if (!selected.empty()) { auto indexes = selected.front().indexes(); if (!indexes.empty()) { - const auto data = model()->data(model()->index(indexes.front().row(), (int)eAccountsModel::Column::Account, indexes.front().parent()), (int)eAccountsModel::Role::Account); - if (data.isValid()) { - if (data.canConvert()) - emit selectByObject(data.value(), eView::Intent::None); + const auto dataVariant = model()->data(model()->index(indexes.front().row(), (int)eAccountsModel::Column::Account, indexes.front().parent()), (int)eAccountsModel::Role::Account); + if (dataVariant.isValid()) { + if (dataVariant.canConvert()) + emit selectByObject(dataVariant.value(), eView::Intent::None); - if (data.canConvert()) - emit selectByObject(data.value(), eView::Intent::None); + if (dataVariant.canConvert()) + emit selectByObject(dataVariant.value(), eView::Intent::None); // an object was successfully selected return; } } } // since no object was selected reset the object selection emit selectByObject(MyMoneyAccount(), eView::Intent::None); emit selectByObject(MyMoneyInstitution(), eView::Intent::None); } void KMyMoneyAccountTreeView::slotColumnToggled(const eAccountsModel::Column column, const bool show) { emit selectByVariant(QVariantList {QVariant::fromValue(column), QVariant(show)}, eView::Intent::ToggleColumn); } diff --git a/kmymoney/widgets/kmymoneydateinput.h b/kmymoney/widgets/kmymoneydateinput.h index b8f1e6683..a5d599682 100644 --- a/kmymoney/widgets/kmymoneydateinput.h +++ b/kmymoney/widgets/kmymoneydateinput.h @@ -1,150 +1,150 @@ /*************************************************************************** kmymoneydateinput.h ------------------- copyright : (C) 2000 by Michael Edwardes email : mte@users.sourceforge.net ***************************************************************************/ /*************************************************************************** * * * 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 KMYMONEYDATEINPUT_H #define KMYMONEYDATEINPUT_H // ---------------------------------------------------------------------------- // QT Includes #include #include #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes #include "kmm_widgets_export.h" // Ideas neatly taken from korganizer // Respective authors are credited. // Some ideas/code have been borrowed from Calendar-0.13 (phoenix.bmedesign.com/~qt) namespace KMyMoney { /** * Provided to be able to catch the focusOut events before the contents gets changed */ class OldDateEdit : public QDateEdit { Q_OBJECT public: explicit OldDateEdit(const QDate& date, QWidget* parent = nullptr); protected: /** if the date was cleared (a state which is not supported by QDateEdit) * make sure that a date can be entered again */ - virtual void keyPressEvent(QKeyEvent* k); + void keyPressEvent(QKeyEvent* k) final override; /** reimplemented for internal reasons */ - virtual bool event(QEvent* e); + bool event(QEvent* e) final override; /** reimplemented for internal reasons */ - virtual bool focusNextPrevChild(bool next); + bool focusNextPrevChild(bool next) final override; /** reimplemented for internal reasons */ - virtual void focusInEvent(QFocusEvent *event); + void focusInEvent(QFocusEvent *event) final override; }; }; // namespace /** * This class provides the general widget used for date selection * throughout the KMyMoney project. It provides an QDateEdit widget * which is based on an edit field with spin boxes and adds a QPushButton * to open a KDatePicker. */ class KMM_WIDGETS_EXPORT KMyMoneyDateInput : public QWidget { Q_OBJECT Q_PROPERTY(QDate date READ date WRITE setDate STORED false) public: explicit KMyMoneyDateInput(QWidget* parent = nullptr, Qt::AlignmentFlag flags = Qt::AlignLeft); ~KMyMoneyDateInput(); /** * Returns the selected date in the widget. If the widget is not * showing a date, a QDate() object is returned which has an invalid date. */ QDate date() const; /** * Set the date shown in the widget to @a date. If @a date is invalid, * no text will be shown. The internal widget will use 1.1.1800 for this * special case, as the standard QDateEdit widget does not support an * invalid date as of Qt4 anymore, but we need it anyway for multi transaction * edit. */ void setDate(QDate date); void setMaximumDate(const QDate& max); /** * Setup the widget with @a date. This date is stored internally and * can be reloaded using resetDate(). * * @sa setDate, resetDate */ void loadDate(const QDate& date); /** * Setup the widget with the date loaded using loadDate(). * * @sa loadDate */ void resetDate(); QWidget* focusWidget() const; void setRange(const QDate & min, const QDate & max); void markAsBadDate(bool bad = false, const QColor& = QColor()); Q_SIGNALS: void dateChanged(const QDate& date); protected: /** * - increments/decrements the date upon +/- key input * - increments/decrements the date upon Up/Down key input * - sets the date to current date when the 'T' key is pressed. * The actual key for this to happen might be overridden through * an i18n package. The 'T'-key is always possible. */ void keyPressEvent(QKeyEvent* k) override; void showEvent(QShowEvent* event) override; /** To intercept events sent to focusWidget() */ bool eventFilter(QObject *o, QEvent *e) override; protected Q_SLOTS: void slotDateChosen(QDate date); void toggleDatePicker(); private Q_SLOTS: void slotDateChosenRef(const QDate& date); void fixSize(); private: struct Private; Private * const d; }; #endif diff --git a/kmymoney/widgets/kmymoneyfrequencycombo.cpp b/kmymoney/widgets/kmymoneyfrequencycombo.cpp index 230b678f8..36f03149c 100644 --- a/kmymoney/widgets/kmymoneyfrequencycombo.cpp +++ b/kmymoney/widgets/kmymoneyfrequencycombo.cpp @@ -1,90 +1,90 @@ /*************************************************************************** kmymoneyfrequencycombo.cpp - description ------------------- begin : Sat Jan 09 2010 copyright : (C) 2010 by Thomas Baumgart Cristian Onet 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 "kmymoneyfrequencycombo.h" // ---------------------------------------------------------------------------- // QT Includes #include // ---------------------------------------------------------------------------- // KDE Includes #include // ---------------------------------------------------------------------------- // Project Includes #include "mymoneyschedule.h" #include "mymoneyenums.h" using namespace eMyMoney; KMyMoneyFrequencyCombo::KMyMoneyFrequencyCombo(QWidget* parent) : KMyMoneyOccurrenceCombo(parent) { addItem(i18nc("Frequency of schedule", MyMoneySchedule::occurrenceToString(Schedule::Occurrence::Once).toLatin1()), (int)Schedule::Occurrence::Once); addItem(i18nc("Frequency of schedule", MyMoneySchedule::occurrenceToString(Schedule::Occurrence::Daily).toLatin1()), (int)Schedule::Occurrence::Daily); addItem(i18nc("Frequency of schedule", MyMoneySchedule::occurrenceToString(Schedule::Occurrence::Weekly).toLatin1()), (int)Schedule::Occurrence::Weekly); addItem(i18nc("Frequency of schedule", MyMoneySchedule::occurrenceToString(Schedule::Occurrence::EveryOtherWeek).toLatin1()), (int)Schedule::Occurrence::EveryOtherWeek); addItem(i18nc("Frequency of schedule", MyMoneySchedule::occurrenceToString(Schedule::Occurrence::EveryHalfMonth).toLatin1()), (int)Schedule::Occurrence::EveryHalfMonth); addItem(i18nc("Frequency of schedule", MyMoneySchedule::occurrenceToString(Schedule::Occurrence::EveryThreeWeeks).toLatin1()), (int)Schedule::Occurrence::EveryThreeWeeks); addItem(i18nc("Frequency of schedule", MyMoneySchedule::occurrenceToString(Schedule::Occurrence::EveryThirtyDays).toLatin1()), (int)Schedule::Occurrence::EveryThirtyDays); addItem(i18nc("Frequency of schedule", MyMoneySchedule::occurrenceToString(Schedule::Occurrence::EveryFourWeeks).toLatin1()), (int)Schedule::Occurrence::EveryFourWeeks); addItem(i18nc("Frequency of schedule", MyMoneySchedule::occurrenceToString(Schedule::Occurrence::Monthly).toLatin1()), (int)Schedule::Occurrence::Monthly); addItem(i18nc("Frequency of schedule", MyMoneySchedule::occurrenceToString(Schedule::Occurrence::EveryEightWeeks).toLatin1()), (int)Schedule::Occurrence::EveryEightWeeks); addItem(i18nc("Frequency of schedule", MyMoneySchedule::occurrenceToString(Schedule::Occurrence::EveryOtherMonth).toLatin1()), (int)Schedule::Occurrence::EveryOtherMonth); addItem(i18nc("Frequency of schedule", MyMoneySchedule::occurrenceToString(Schedule::Occurrence::EveryThreeMonths).toLatin1()), (int)Schedule::Occurrence::EveryThreeMonths); addItem(i18nc("Frequency of schedule", MyMoneySchedule::occurrenceToString(Schedule::Occurrence::EveryFourMonths).toLatin1()), (int)Schedule::Occurrence::EveryFourMonths); addItem(i18nc("Frequency of schedule", MyMoneySchedule::occurrenceToString(Schedule::Occurrence::TwiceYearly).toLatin1()), (int)Schedule::Occurrence::TwiceYearly); addItem(i18nc("Frequency of schedule", MyMoneySchedule::occurrenceToString(Schedule::Occurrence::Yearly).toLatin1()), (int)Schedule::Occurrence::Yearly); addItem(i18nc("Frequency of schedule", MyMoneySchedule::occurrenceToString(Schedule::Occurrence::EveryOtherYear).toLatin1()), (int)Schedule::Occurrence::EveryOtherYear); connect(this, static_cast(&QComboBox::currentIndexChanged), this, &KMyMoneyFrequencyCombo::slotCurrentDataChanged); } KMyMoneyFrequencyCombo::~KMyMoneyFrequencyCombo() { } int KMyMoneyFrequencyCombo::daysBetweenEvents() const { return MyMoneySchedule::daysBetweenEvents(currentItem()); } int KMyMoneyFrequencyCombo::eventsPerYear() const { return MyMoneySchedule::eventsPerYear(currentItem()); } QVariant KMyMoneyFrequencyCombo::currentData() const { return itemData(currentIndex(), Qt::UserRole); } -void KMyMoneyFrequencyCombo::setCurrentData(QVariant data) +void KMyMoneyFrequencyCombo::setCurrentData(QVariant datavar) { - setItemData(currentIndex(), data, Qt::UserRole); + setItemData(currentIndex(), datavar, Qt::UserRole); } void KMyMoneyFrequencyCombo::slotCurrentDataChanged() { emit currentDataChanged(currentData()); } diff --git a/kmymoney/widgets/kmymoneyfrequencycombo.h b/kmymoney/widgets/kmymoneyfrequencycombo.h index dfd4519c0..4b1b690f6 100644 --- a/kmymoney/widgets/kmymoneyfrequencycombo.h +++ b/kmymoney/widgets/kmymoneyfrequencycombo.h @@ -1,78 +1,78 @@ /*************************************************************************** kmymoneyfrequencycombo.h - description ------------------- begin : Mon Jan 09 2010 copyright : (C) 2010 by Thomas Baumgart Cristian Onet 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 KMYMONEYFREQUENCYCOMBO_H #define KMYMONEYFREQUENCYCOMBO_H // ---------------------------------------------------------------------------- // QT Includes // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes #include "kmymoneyoccurrencecombo.h" /** * This class implements a payment frequency selector * @author Thomas Baumgart */ class KMM_WIDGETS_EXPORT KMyMoneyFrequencyCombo : public KMyMoneyOccurrenceCombo { Q_OBJECT Q_DISABLE_COPY(KMyMoneyFrequencyCombo) Q_PROPERTY(QVariant data READ currentData WRITE setCurrentData STORED false) public: explicit KMyMoneyFrequencyCombo(QWidget* parent = nullptr); ~KMyMoneyFrequencyCombo() override; /** * This method returns the number of events for the selected payment * frequency (eg for yearly the return value is 1 and for monthly it * is 12). In case, the frequency cannot be converted (once, every other year, etc.) * the method returns 0. */ int eventsPerYear() const; /** * This method returns the number of days between two events of * the selected frequency. The return value for months is based * on 30 days and the year is 360 days long. */ int daysBetweenEvents() const; QVariant currentData() const; - void setCurrentData(QVariant data); + void setCurrentData(QVariant datavar); Q_SIGNALS: void currentDataChanged(QVariant data); protected Q_SLOTS: void slotCurrentDataChanged(); private: QVariant data; }; #endif diff --git a/kmymoney/widgets/kmymoneymvccombo.cpp b/kmymoney/widgets/kmymoneymvccombo.cpp index 7c3968557..61ad56ae8 100644 --- a/kmymoney/widgets/kmymoneymvccombo.cpp +++ b/kmymoney/widgets/kmymoneymvccombo.cpp @@ -1,321 +1,321 @@ /*************************************************************************** kmymoneymvccombo.cpp - description ------------------- begin : Sat Jan 09 2010 copyright : (C) 2010 by Thomas Baumgart Cristian Onet 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 "kmymoneymvccombo.h" #include "kmymoneymvccombo_p.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include // ---------------------------------------------------------------------------- // Project Includes KMyMoneyMVCCombo::KMyMoneyMVCCombo(QWidget* parent) : KComboBox(parent), d_ptr(new KMyMoneyMVCComboPrivate) { view()->setAlternatingRowColors(true); connect(this, static_cast(&KMyMoneyMVCCombo::KComboBox::activated), this, &KMyMoneyMVCCombo::activated); } KMyMoneyMVCCombo::KMyMoneyMVCCombo(bool editable, QWidget* parent) : KComboBox(editable, parent), d_ptr(new KMyMoneyMVCComboPrivate) { Q_D(KMyMoneyMVCCombo); d->m_completer = new QCompleter(this); d->m_completer->setCaseSensitivity(Qt::CaseInsensitive); d->m_completer->setModel(model()); setCompleter(d->m_completer); // setSubstringSearch(!KMyMoneySettings::stringMatchFromStart()); view()->setAlternatingRowColors(true); setInsertPolicy(QComboBox::NoInsert); // don't insert new objects due to object creation connect(this, static_cast(&KMyMoneyMVCCombo::KComboBox::activated), this, &KMyMoneyMVCCombo::activated); } KMyMoneyMVCCombo::KMyMoneyMVCCombo(KMyMoneyMVCComboPrivate &dd, QWidget* parent) : KComboBox(parent), d_ptr(&dd) { view()->setAlternatingRowColors(true); connect(this, static_cast(&KMyMoneyMVCCombo::KComboBox::activated), this, &KMyMoneyMVCCombo::activated); } KMyMoneyMVCCombo::KMyMoneyMVCCombo(KMyMoneyMVCComboPrivate &dd, bool editable, QWidget* parent) : KComboBox(editable, parent), d_ptr(&dd) { Q_D(KMyMoneyMVCCombo); d->m_completer = new QCompleter(this); d->m_completer->setCaseSensitivity(Qt::CaseInsensitive); d->m_completer->setModel(model()); setCompleter(d->m_completer); // setSubstringSearch(!KMyMoneySettings::stringMatchFromStart()); view()->setAlternatingRowColors(true); setInsertPolicy(QComboBox::NoInsert); // don't insert new objects due to object creation connect(this, static_cast(&KMyMoneyMVCCombo::KComboBox::activated), this, &KMyMoneyMVCCombo::activated); } KMyMoneyMVCCombo::~KMyMoneyMVCCombo() { Q_D(KMyMoneyMVCCombo); delete d; } void KMyMoneyMVCCombo::setEditable(bool editable) { Q_D(KMyMoneyMVCCombo); KComboBox::setEditable(editable); if(editable) { if(!d->m_completer) { d->m_completer = new QCompleter(this); d->m_completer->setCaseSensitivity(Qt::CaseInsensitive); d->m_completer->setModel(model()); } setCompleter(d->m_completer); } } void KMyMoneyMVCCombo::setSubstringSearch(bool enabled) { Q_D(KMyMoneyMVCCombo); d->m_completer->setCompletionMode(QCompleter::PopupCompletion); d->m_completer->setModel(model()); d->m_completer->setFilterMode(enabled ? Qt::MatchContains : Qt::MatchStartsWith); } void KMyMoneyMVCCombo::setSubstringSearchForChildren(QWidget*const widget, bool enabled) { Q_CHECK_PTR(widget); QList comboList; comboList = widget->findChildren(); foreach (KMyMoneyMVCCombo *combo, comboList) { combo->setSubstringSearch(enabled); } } void KMyMoneyMVCCombo::setPlaceholderText(const QString& hint) const { KLineEdit* le = qobject_cast(lineEdit()); if (le) { le->setPlaceholderText(hint); } } QString KMyMoneyMVCCombo::selectedItem() const { Q_D(const KMyMoneyMVCCombo); - QVariant data = itemData(currentIndex()); - if (data.isValid()) - d->m_id = data.toString(); + auto dataVariant = itemData(currentIndex()); + if (dataVariant.isValid()) + d->m_id = dataVariant.toString(); else d->m_id.clear(); return d->m_id; } void KMyMoneyMVCCombo::setSelectedItem(const QString& id) { Q_D(KMyMoneyMVCCombo); d->m_id = id; setCurrentIndex(findData(QVariant(d->m_id))); } void KMyMoneyMVCCombo::activated(int index) { Q_D(KMyMoneyMVCCombo); - QVariant data = itemData(index); - if (data.isValid()) { - d->m_id = data.toString(); + auto dataVariant = itemData(index); + if (dataVariant.isValid()) { + d->m_id = dataVariant.toString(); emit itemSelected(d->m_id); } } void KMyMoneyMVCCombo::connectNotify(const QMetaMethod & signal) { Q_D(KMyMoneyMVCCombo); if (signal != QMetaMethod::fromSignal(&KMyMoneyMVCCombo::createItem)) { d->m_canCreateObjects = true; } } void KMyMoneyMVCCombo::disconnectNotify(const QMetaMethod & signal) { Q_D(KMyMoneyMVCCombo); if (signal != QMetaMethod::fromSignal(&KMyMoneyMVCCombo::createItem)) { d->m_canCreateObjects = false; } } void KMyMoneyMVCCombo::setCurrentText(const QString& txt) { KComboBox::setItemText(KComboBox::currentIndex(), txt); } void KMyMoneyMVCCombo::setCurrentText() { KComboBox::setItemText(KComboBox::currentIndex(), QString()); } void KMyMoneyMVCCombo::focusOutEvent(QFocusEvent* e) { Q_D(KMyMoneyMVCCombo); // when showing m_completion we'll receive a focus out event even if the focus // will still remain at this widget since this widget is the completion's focus proxy // so ignore the focus out event caused by showin a widget of type Qt::Popup if (e->reason() == Qt::PopupFocusReason) return; if (d->m_inFocusOutEvent) { KComboBox::focusOutEvent(e); return; } //check if the focus went to a widget in TransactionFrom or in the Register if (e->reason() == Qt::MouseFocusReason) { QObject *w = this->parent(); QObject *q = qApp->focusWidget()->parent(); // KMyMoneyTagCombo is inside KTagContainer, KMyMoneyPayeeCombo isn't it if (w->inherits("KTagContainer")) w = w->parent(); while (q && q->objectName() != "qt_scrollarea_viewport") q = q->parent(); if (q != w && qApp->focusWidget()->objectName() != "register") { KComboBox::focusOutEvent(e); return; } } d->m_inFocusOutEvent = true; if (isEditable() && !currentText().isEmpty() && e->reason() != Qt::ActiveWindowFocusReason) { if (d->m_canCreateObjects) { // in case we tab out, we make sure that if the current completion // contains the current text that we set the current text to // the full completion text but only if the completion box is visible. // BUG 254984 is resolved with the visbility check if (e->reason() != Qt::MouseFocusReason) { if (d->m_completer->popup() && d->m_completer->popup()->isVisible() && d->m_completer->currentCompletion().contains(currentText(), Qt::CaseInsensitive)) { lineEdit()->setText(d->m_completer->currentCompletion()); } } //check if the current text is contained in the internal list, if not ask the user if want to create a new item. checkCurrentText(); // else if we cannot create objects, and the current text is not // in the list, then we clear the text and the selection. } else if (!contains(currentText())) { clearEditText(); } //this is to cover the case when you highlight an item but don't activate it with Enter if (currentText() != itemText(currentIndex())) { setCurrentIndex(findText(currentText(), Qt::MatchExactly)); emit activated(currentIndex()); } } KComboBox::focusOutEvent(e); // force update of hint and id if there is no text in the widget if (isEditable() && currentText().isEmpty()) { QString id = d->m_id; d->m_id.clear(); if (!id.isEmpty()) emit itemSelected(d->m_id); update(); } d->m_inFocusOutEvent = false; // This is used only be KMyMoneyTagCombo at this time emit lostFocus(); } void KMyMoneyMVCCombo::checkCurrentText() { Q_D(KMyMoneyMVCCombo); if (!contains(currentText())) { QString id; // annouce that we go into a possible dialog to create an object // This can be used by upstream widgets to disable filters etc. emit objectCreation(true); emit createItem(currentText(), id); // Announce that we return from object creation emit objectCreation(false); // update the field to a possibly created object d->m_id = id; addEntry(currentText(), id); setCurrentTextById(id); } } void KMyMoneyMVCCombo::addEntry(const QString& newTxt, const QString& id) { // find the correct position in the list int idx; for(idx = 0; idx < model()->rowCount(); ++idx) { const QString txt = itemText(idx); if (txt.compare(newTxt) > 0) { break; } } // and insert the new item insertItem(idx - 1, QIcon(), currentText(), id); } void KMyMoneyMVCCombo::setCurrentTextById(const QString& id) { clearEditText(); if (!id.isEmpty()) { int index = findData(QVariant(id), Qt::UserRole, Qt::MatchExactly); if (index > -1) { setCompletedText(itemText(index)); setEditText(itemText(index)); setCurrentIndex(index); } } } void KMyMoneyMVCCombo::protectItem(int id, bool protect) { QStandardItemModel* standardModel = qobject_cast (model()); QStandardItem* standardItem = standardModel->item(id); standardItem->setSelectable(!protect); } diff --git a/kmymoney/widgets/kmymoneyreconcilecombo.cpp b/kmymoney/widgets/kmymoneyreconcilecombo.cpp index 3f74982cf..67b0b165b 100644 --- a/kmymoney/widgets/kmymoneyreconcilecombo.cpp +++ b/kmymoney/widgets/kmymoneyreconcilecombo.cpp @@ -1,115 +1,115 @@ /*************************************************************************** kmymoneyreconcilecombo.cpp - description ------------------- begin : Sat Jan 09 2010 copyright : (C) 2010 by Thomas Baumgart Cristian Onet 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 "kmymoneyreconcilecombo.h" // ---------------------------------------------------------------------------- // QT Includes #include // ---------------------------------------------------------------------------- // KDE Includes #include // ---------------------------------------------------------------------------- // Project Includes #include "mymoneyenums.h" using namespace eMyMoney; KMyMoneyReconcileCombo::KMyMoneyReconcileCombo(QWidget* w) : KMyMoneyMVCCombo(false, w) { // add the items in reverse order of appearance (see KMyMoneySelector::newItem() for details) addItem(i18n("Reconciled"), QVariant("R")); addItem(i18nc("Reconciliation state 'Cleared'", "Cleared"), QVariant("C")); addItem(i18n("Not reconciled"), QVariant(" ")); addItem(" ", QVariant("U")); connect(this, &KMyMoneyMVCCombo::itemSelected, this, &KMyMoneyReconcileCombo::slotSetState); } KMyMoneyReconcileCombo::~KMyMoneyReconcileCombo() { } void KMyMoneyReconcileCombo::slotSetState(const QString& state) { setSelectedItem(state); } void KMyMoneyReconcileCombo::removeDontCare() { //Remove unknown state removeItem(3); } void KMyMoneyReconcileCombo::setState(Split::State state) { QString id; switch (state) { case Split::State::NotReconciled: id = ' '; break; case Split::State::Cleared: id = 'C'; break; case Split::State::Reconciled: id = 'R'; break; case Split::State::Frozen: id = 'F'; break; case Split::State::Unknown: id = 'U'; break; default: qDebug() << "Unknown reconcile state '" << (int)state << "' in KMyMoneyReconcileCombo::setState()\n"; break; } setSelectedItem(id); } Split::State KMyMoneyReconcileCombo::state() const { Split::State state = Split::State::NotReconciled; - QVariant data = itemData(currentIndex()); + auto dataVariant = itemData(currentIndex()); QString dataVal; - if (data.isValid()) - dataVal = data.toString(); + if (dataVariant.isValid()) + dataVal = dataVariant.toString(); else return state; if (!dataVal.isEmpty()) { if (dataVal == "C") state = Split::State::Cleared; if (dataVal == "R") state = Split::State::Reconciled; if (dataVal == "F") state = Split::State::Frozen; if (dataVal == "U") state = Split::State::Unknown; } return state; } diff --git a/kmymoney/widgets/kmymoneytextedithighlighter.h b/kmymoney/widgets/kmymoneytextedithighlighter.h index b2c996c35..07359815c 100644 --- a/kmymoney/widgets/kmymoneytextedithighlighter.h +++ b/kmymoney/widgets/kmymoneytextedithighlighter.h @@ -1,47 +1,47 @@ /* This file is part of KMyMoney, A Personal Finance Manager by KDE Copyright (C) 2013 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. 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 KMYMONEYTEXTEDIT_H #define KMYMONEYTEXTEDIT_H #include class KMyMoneyTextEditHighlighterPrivate; class KMyMoneyTextEditHighlighter : public Sonnet::Highlighter { Q_DISABLE_COPY(KMyMoneyTextEditHighlighter) public: explicit KMyMoneyTextEditHighlighter(QTextEdit* parent = nullptr); // krazy:exclude=qclasses ~KMyMoneyTextEditHighlighter(); void setAllowedChars(const QString& chars); void setMaxLength(const int& length); void setMaxLines(const int& lines); void setMaxLineLength(const int& length); protected: - virtual void highlightBlock(const QString& text); + void highlightBlock(const QString& text) final override; private: KMyMoneyTextEditHighlighterPrivate * const d_ptr; Q_DECLARE_PRIVATE(KMyMoneyTextEditHighlighter) }; #endif // KMYMONEYTEXTEDIT_H diff --git a/kmymoney/widgets/kpricetreeitem.h b/kmymoney/widgets/kpricetreeitem.h index f6e07dcf3..4d5470e83 100644 --- a/kmymoney/widgets/kpricetreeitem.h +++ b/kmymoney/widgets/kpricetreeitem.h @@ -1,53 +1,53 @@ /*************************************************************************** kpricetreeitem.h - description ------------------- begin : Sun Jul 18 2010 copyright : (C) 2010 by Alvaro Soliverez email : asoliverez@kde.org (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 KPRICETREEITEM_H #define KPRICETREEITEM_H // ---------------------------------------------------------------------------- // QT Includes #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes class KPriceTreeItem : public QTreeWidgetItem { public: explicit KPriceTreeItem(QTreeWidget* parent); - bool operator<(const QTreeWidgetItem &otherItem) const; + bool operator<(const QTreeWidgetItem &otherItem) const final override; enum PriceItemDataRole { ScheduleIdRole = Qt::UserRole, OrderRole = Qt::UserRole + 1 }; enum ePriceColumns { ePriceCommodity = 0, ePriceStockName, ePriceCurrency, ePriceDate, ePricePrice, ePriceSource }; }; #endif // KPRICETREEITEM_H diff --git a/kmymoney/widgets/ktreewidgetfilterlinewidget.h b/kmymoney/widgets/ktreewidgetfilterlinewidget.h index 321a7ed9a..734d10567 100644 --- a/kmymoney/widgets/ktreewidgetfilterlinewidget.h +++ b/kmymoney/widgets/ktreewidgetfilterlinewidget.h @@ -1,42 +1,42 @@ /* * This file is part of KMyMoney, A Personal Finance Manager by KDE * Copyright (C) 2015 Christian David * (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. * * 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 KTREEWIDGETFILTERLINEWIDGET_H #define KTREEWIDGETFILTERLINEWIDGET_H #include #include "kmm_widgets_export.h" class KMM_WIDGETS_EXPORT KTreeWidgetFilterLineWidget : public KTreeWidgetSearchLineWidget { Q_OBJECT public: explicit KTreeWidgetFilterLineWidget(QWidget* parent = nullptr, QTreeWidget *treeWidget = 0); protected Q_SLOTS: /** * @copydoc KTreeWidgetSearchLineWidget::createWidgets() * * After widgets are created, this version finds the label and renames it to "Filter" */ - virtual void createWidgets(); + void createWidgets() final override; }; #endif // KTREEWIDGETFILTERLINEWIDGET_H diff --git a/kmymoney/widgets/register.cpp b/kmymoney/widgets/register.cpp index a40bf4075..1d316e7fd 100644 --- a/kmymoney/widgets/register.cpp +++ b/kmymoney/widgets/register.cpp @@ -1,1892 +1,1892 @@ /*************************************************************************** register.cpp - description ------------------- begin : Fri Mar 10 2006 copyright : (C) 2006 by Thomas Baumgart email : Thomas Baumgart (C) 2017 by Łukasz Wojniłowicz ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "register.h" #include // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include // ---------------------------------------------------------------------------- // Project Includes #include "mymoneysplit.h" #include "mymoneytransaction.h" #include "mymoneyexception.h" #include "mymoneyaccount.h" #include "stdtransactiondownloaded.h" #include "stdtransactionmatched.h" #include "selectedtransactions.h" #include "scheduledtransaction.h" #include "kmymoneysettings.h" #include "mymoneymoney.h" #include "mymoneyfile.h" #include "groupmarkers.h" #include "fancydategroupmarkers.h" #include "registeritemdelegate.h" #include "itemptrvector.h" #include "mymoneyenums.h" #include "widgetenums.h" using namespace KMyMoneyRegister; using namespace eWidgets; using namespace eMyMoney; namespace KMyMoneyRegister { class RegisterPrivate { public: RegisterPrivate() : m_selectAnchor(nullptr), m_focusItem(nullptr), m_ensureVisibleItem(nullptr), m_firstItem(nullptr), m_lastItem(nullptr), m_firstErroneous(nullptr), m_lastErroneous(nullptr), m_rowHeightHint(0), m_ledgerLensForced(false), m_selectionMode(QTableWidget::MultiSelection), m_needResize(true), m_listsDirty(false), m_ignoreNextButtonRelease(false), m_needInitialColumnResize(false), m_usedWithEditor(false), m_mouseButton(Qt::MouseButtons(Qt::NoButton)), m_modifiers(Qt::KeyboardModifiers(Qt::NoModifier)), m_lastCol(eTransaction::Column::Account), m_detailsColumnType(eRegister::DetailColumn::PayeeFirst) { } ~RegisterPrivate() { } ItemPtrVector m_items; QVector m_itemIndex; RegisterItem* m_selectAnchor; RegisterItem* m_focusItem; RegisterItem* m_ensureVisibleItem; RegisterItem* m_firstItem; RegisterItem* m_lastItem; RegisterItem* m_firstErroneous; RegisterItem* m_lastErroneous; int m_markErroneousTransactions; int m_rowHeightHint; MyMoneyAccount m_account; bool m_ledgerLensForced; QAbstractItemView::SelectionMode m_selectionMode; bool m_needResize; bool m_listsDirty; bool m_ignoreNextButtonRelease; bool m_needInitialColumnResize; bool m_usedWithEditor; Qt::MouseButtons m_mouseButton; Qt::KeyboardModifiers m_modifiers; eTransaction::Column m_lastCol; QList m_sortOrder; QRect m_lastRepaintRect; eRegister::DetailColumn m_detailsColumnType; }; Register::Register(QWidget *parent) : TransactionEditorContainer(parent), d_ptr(new RegisterPrivate) { // used for custom coloring with the help of the application's stylesheet setObjectName(QLatin1String("register")); setItemDelegate(new RegisterItemDelegate(this)); setEditTriggers(QAbstractItemView::NoEditTriggers); setColumnCount((int)eTransaction::Column::LastColumn); setSelectionBehavior(QAbstractItemView::SelectRows); setAcceptDrops(true); setShowGrid(false); setContextMenuPolicy(Qt::DefaultContextMenu); setHorizontalHeaderItem((int)eTransaction::Column::Number, new QTableWidgetItem()); setHorizontalHeaderItem((int)eTransaction::Column::Date, new QTableWidgetItem()); setHorizontalHeaderItem((int)eTransaction::Column::Account, new QTableWidgetItem()); setHorizontalHeaderItem((int)eTransaction::Column::Security, new QTableWidgetItem()); setHorizontalHeaderItem((int)eTransaction::Column::Detail, new QTableWidgetItem()); setHorizontalHeaderItem((int)eTransaction::Column::ReconcileFlag, new QTableWidgetItem()); setHorizontalHeaderItem((int)eTransaction::Column::Payment, new QTableWidgetItem()); setHorizontalHeaderItem((int)eTransaction::Column::Deposit, new QTableWidgetItem()); setHorizontalHeaderItem((int)eTransaction::Column::Quantity, new QTableWidgetItem()); setHorizontalHeaderItem((int)eTransaction::Column::Price, new QTableWidgetItem()); setHorizontalHeaderItem((int)eTransaction::Column::Value, new QTableWidgetItem()); setHorizontalHeaderItem((int)eTransaction::Column::Balance, new QTableWidgetItem()); // keep the following list in sync with KMyMoneyRegister::Column in transaction.h horizontalHeaderItem((int)eTransaction::Column::Number)->setText(i18nc("Cheque Number", "No.")); horizontalHeaderItem((int)eTransaction::Column::Date)->setText(i18n("Date")); horizontalHeaderItem((int)eTransaction::Column::Account)->setText(i18n("Account")); horizontalHeaderItem((int)eTransaction::Column::Security)->setText(i18n("Security")); horizontalHeaderItem((int)eTransaction::Column::Detail)->setText(i18n("Details")); horizontalHeaderItem((int)eTransaction::Column::ReconcileFlag)->setText(i18n("C")); horizontalHeaderItem((int)eTransaction::Column::Payment)->setText(i18n("Payment")); horizontalHeaderItem((int)eTransaction::Column::Deposit)->setText(i18n("Deposit")); horizontalHeaderItem((int)eTransaction::Column::Quantity)->setText(i18n("Quantity")); horizontalHeaderItem((int)eTransaction::Column::Price)->setText(i18n("Price")); horizontalHeaderItem((int)eTransaction::Column::Value)->setText(i18n("Value")); horizontalHeaderItem((int)eTransaction::Column::Balance)->setText(i18n("Balance")); verticalHeader()->hide(); horizontalHeader()->setSectionResizeMode(QHeaderView::Fixed); horizontalHeader()->setSortIndicatorShown(false); horizontalHeader()->setSectionsMovable(false); horizontalHeader()->setSectionsClickable(false); horizontalHeader()->setContextMenuPolicy(Qt::CustomContextMenu); connect(this, &QTableWidget::cellClicked, this, static_cast(&Register::selectItem)); connect(this, &QTableWidget::cellDoubleClicked, this, &Register::slotDoubleClicked); } Register::~Register() { Q_D(Register); clear(); delete d; } bool Register::eventFilter(QObject* o, QEvent* e) { if (o == this && e->type() == QEvent::KeyPress) { auto ke = dynamic_cast(e); if (ke && ke->key() == Qt::Key_Menu) { emit openContextMenu(); return true; } } return QTableWidget::eventFilter(o, e); } void Register::setupRegister(const MyMoneyAccount& account, const QList& cols) { Q_D(Register); d->m_account = account; setUpdatesEnabled(false); for (auto i = 0; i < (int)eTransaction::Column::LastColumn; ++i) hideColumn(i); d->m_needInitialColumnResize = true; d->m_lastCol = static_cast(0); QList::const_iterator it_c; for (it_c = cols.begin(); it_c != cols.end(); ++it_c) { if ((*it_c) > eTransaction::Column::LastColumn) continue; showColumn((int)*it_c); if (*it_c > d->m_lastCol) d->m_lastCol = *it_c; } setUpdatesEnabled(true); } void Register::setupRegister(const MyMoneyAccount& account, bool showAccountColumn) { Q_D(Register); d->m_account = account; setUpdatesEnabled(false); for (auto i = 0; i < (int)eTransaction::Column::LastColumn; ++i) hideColumn(i); horizontalHeaderItem((int)eTransaction::Column::Payment)->setText(i18nc("Payment made from account", "Payment")); horizontalHeaderItem((int)eTransaction::Column::Deposit)->setText(i18nc("Deposit into account", "Deposit")); if (account.id().isEmpty()) { setUpdatesEnabled(true); return; } d->m_needInitialColumnResize = true; // turn on standard columns showColumn((int)eTransaction::Column::Date); showColumn((int)eTransaction::Column::Detail); showColumn((int)eTransaction::Column::ReconcileFlag); // balance switch (account.accountType()) { case Account::Type::Stock: break; default: showColumn((int)eTransaction::Column::Balance); break; } // Number column switch (account.accountType()) { case Account::Type::Savings: case Account::Type::Cash: case Account::Type::Loan: case Account::Type::AssetLoan: case Account::Type::Asset: case Account::Type::Liability: case Account::Type::Equity: if (KMyMoneySettings::alwaysShowNrField()) showColumn((int)eTransaction::Column::Number); break; case Account::Type::Checkings: case Account::Type::CreditCard: showColumn((int)eTransaction::Column::Number); break; default: hideColumn((int)eTransaction::Column::Number); break; } switch (account.accountType()) { case Account::Type::Income: case Account::Type::Expense: showAccountColumn = true; break; default: break; } if (showAccountColumn) showColumn((int)eTransaction::Column::Account); // Security, activity, payment, deposit, amount, price and value column switch (account.accountType()) { default: showColumn((int)eTransaction::Column::Payment); showColumn((int)eTransaction::Column::Deposit); break; case Account::Type::Investment: showColumn((int)eTransaction::Column::Security); showColumn((int)eTransaction::Column::Quantity); showColumn((int)eTransaction::Column::Price); showColumn((int)eTransaction::Column::Value); break; } // headings switch (account.accountType()) { case Account::Type::CreditCard: horizontalHeaderItem((int)eTransaction::Column::Payment)->setText(i18nc("Payment made with credit card", "Charge")); horizontalHeaderItem((int)eTransaction::Column::Deposit)->setText(i18nc("Payment towards credit card", "Payment")); break; case Account::Type::Asset: case Account::Type::AssetLoan: horizontalHeaderItem((int)eTransaction::Column::Payment)->setText(i18nc("Decrease of asset/liability value", "Decrease")); horizontalHeaderItem((int)eTransaction::Column::Deposit)->setText(i18nc("Increase of asset/liability value", "Increase")); break; case Account::Type::Liability: case Account::Type::Loan: horizontalHeaderItem((int)eTransaction::Column::Payment)->setText(i18nc("Increase of asset/liability value", "Increase")); horizontalHeaderItem((int)eTransaction::Column::Deposit)->setText(i18nc("Decrease of asset/liability value", "Decrease")); break; case Account::Type::Income: case Account::Type::Expense: horizontalHeaderItem((int)eTransaction::Column::Payment)->setText(i18n("Income")); horizontalHeaderItem((int)eTransaction::Column::Deposit)->setText(i18n("Expense")); break; default: break; } d->m_lastCol = eTransaction::Column::Balance; setUpdatesEnabled(true); } bool Register::focusNextPrevChild(bool next) { return QFrame::focusNextPrevChild(next); } void Register::setSortOrder(const QString& order) { Q_D(Register); const QStringList orderList = order.split(',', QString::SkipEmptyParts); QStringList::const_iterator it; d->m_sortOrder.clear(); for (it = orderList.constBegin(); it != orderList.constEnd(); ++it) { d->m_sortOrder << static_cast((*it).toInt()); } } const QList& Register::sortOrder() const { Q_D(const Register); return d->m_sortOrder; } void Register::sortItems() { Q_D(Register); if (d->m_items.count() == 0) return; // sort the array of pointers to the transactions d->m_items.sort(); // update the next/prev item chains RegisterItem* prev = 0; RegisterItem* item; d->m_firstItem = d->m_lastItem = 0; for (QVector::size_type i = 0; i < d->m_items.size(); ++i) { item = d->m_items[i]; if (!item) continue; if (!d->m_firstItem) d->m_firstItem = item; d->m_lastItem = item; if (prev) prev->setNextItem(item); item->setPrevItem(prev); item->setNextItem(0); prev = item; } // update the balance visibility settings item = d->m_lastItem; bool showBalance = true; while (item) { auto t = dynamic_cast(item); if (t) { t->setShowBalance(showBalance); if (!t->isVisible()) { showBalance = false; } } item = item->prevItem(); } // force update of the item index (row to item array) d->m_listsDirty = true; } eTransaction::Column Register::lastCol() const { Q_D(const Register); return d->m_lastCol; } SortField Register::primarySortKey() const { Q_D(const Register); if (!d->m_sortOrder.isEmpty()) return static_cast(d->m_sortOrder.first()); return SortField::Unknown; } void Register::clear() { Q_D(Register); d->m_firstErroneous = d->m_lastErroneous = 0; d->m_ensureVisibleItem = 0; d->m_items.clear(); RegisterItem* p; while ((p = firstItem()) != 0) { delete p; } d->m_firstItem = d->m_lastItem = 0; d->m_listsDirty = true; d->m_selectAnchor = 0; d->m_focusItem = 0; #ifndef KMM_DESIGNER // recalculate row height hint QFontMetrics fm(KMyMoneySettings::listCellFontEx()); d->m_rowHeightHint = fm.lineSpacing() + 6; #endif d->m_needInitialColumnResize = true; d->m_needResize = true; updateRegister(true); } void Register::insertItemAfter(RegisterItem*p, RegisterItem* prev) { Q_D(Register); RegisterItem* next = 0; if (!prev) prev = lastItem(); if (prev) { next = prev->nextItem(); prev->setNextItem(p); } if (next) next->setPrevItem(p); p->setPrevItem(prev); p->setNextItem(next); if (!d->m_firstItem) d->m_firstItem = p; if (!d->m_lastItem) d->m_lastItem = p; if (prev == d->m_lastItem) d->m_lastItem = p; d->m_listsDirty = true; d->m_needResize = true; } void Register::addItem(RegisterItem* p) { Q_D(Register); RegisterItem* q = lastItem(); if (q) q->setNextItem(p); p->setPrevItem(q); p->setNextItem(0); d->m_items.append(p); if (!d->m_firstItem) d->m_firstItem = p; d->m_lastItem = p; d->m_listsDirty = true; d->m_needResize = true; } void Register::removeItem(RegisterItem* p) { Q_D(Register); // remove item from list if (p->prevItem()) p->prevItem()->setNextItem(p->nextItem()); if (p->nextItem()) p->nextItem()->setPrevItem(p->prevItem()); // update first and last pointer if required if (p == d->m_firstItem) d->m_firstItem = p->nextItem(); if (p == d->m_lastItem) d->m_lastItem = p->prevItem(); // make sure we don't do it twice p->setNextItem(0); p->setPrevItem(0); // remove it from the m_items array int i = d->m_items.indexOf(p); if (-1 != i) { d->m_items[i] = 0; } d->m_listsDirty = true; d->m_needResize = true; } RegisterItem* Register::firstItem() const { Q_D(const Register); return d->m_firstItem; } RegisterItem* Register::nextItem(RegisterItem* item) const { return item->nextItem(); } RegisterItem* Register::lastItem() const { Q_D(const Register); return d->m_lastItem; } void Register::setupItemIndex(int rowCount) { Q_D(Register); // setup index array d->m_itemIndex.clear(); d->m_itemIndex.reserve(rowCount); // fill index array rowCount = 0; RegisterItem* prev = 0; d->m_firstItem = d->m_lastItem = 0; for (QVector::size_type i = 0; i < d->m_items.size(); ++i) { RegisterItem* item = d->m_items[i]; if (!item) continue; if (!d->m_firstItem) d->m_firstItem = item; d->m_lastItem = item; if (prev) prev->setNextItem(item); item->setPrevItem(prev); item->setNextItem(0); prev = item; for (int j = item->numRowsRegister(); j; --j) { d->m_itemIndex.push_back(item); } } } void Register::updateAlternate() const { Q_D(const Register); bool alternate = false; for (QVector::size_type i = 0; i < d->m_items.size(); ++i) { RegisterItem* item = d->m_items[i]; if (!item) continue; if (item->isVisible()) { item->setAlternate(alternate); alternate ^= true; } } } void Register::suppressAdjacentMarkers() { bool lastWasGroupMarker = false; KMyMoneyRegister::RegisterItem* p = lastItem(); auto t = dynamic_cast(p); if (t && t->transaction().id().isEmpty()) { lastWasGroupMarker = true; p = p->prevItem(); } while (p) { auto m = dynamic_cast(p); if (m) { // make adjacent group marker invisible except those that show statement information if (lastWasGroupMarker && (dynamic_cast(m) == 0)) { m->setVisible(false); } lastWasGroupMarker = true; } else if (p->isVisible()) lastWasGroupMarker = false; p = p->prevItem(); } } void Register::updateRegister(bool forceUpdateRowHeight) { Q_D(Register); if (d->m_listsDirty || forceUpdateRowHeight) { // don't get in here recursively d->m_listsDirty = false; int rowCount = 0; // determine the number of rows we need to display all items // while going through the list, check for erroneous transactions for (QVector::size_type i = 0; i < d->m_items.size(); ++i) { RegisterItem* item = d->m_items[i]; if (!item) continue; item->setStartRow(rowCount); item->setNeedResize(); rowCount += item->numRowsRegister(); if (item->isErroneous()) { if (!d->m_firstErroneous) d->m_firstErroneous = item; d->m_lastErroneous = item; } } updateAlternate(); // create item index setupItemIndex(rowCount); bool needUpdateHeaders = (QTableWidget::rowCount() != rowCount) | forceUpdateRowHeight; // setup QTable. Make sure to suppress screen updates for now setRowCount(rowCount); // if we need to update the headers, we do it now for all rows // again we make sure to suppress screen updates if (needUpdateHeaders) { for (auto i = 0; i < rowCount; ++i) { RegisterItem* item = itemAtRow(i); if (item->isVisible()) { showRow(i); } else { hideRow(i); } verticalHeader()->resizeSection(i, item->rowHeightHint()); } verticalHeader()->setUpdatesEnabled(true); } // force resizeing of the columns if necessary if (d->m_needInitialColumnResize) { QTimer::singleShot(0, this, SLOT(resize())); d->m_needInitialColumnResize = false; } else { update(); // if the number of rows changed, we might need to resize the register // to make sure we reflect the current visibility of the scrollbars. if (needUpdateHeaders) QTimer::singleShot(0, this, SLOT(resize())); } } } int Register::rowHeightHint() const { Q_D(const Register); if (!d->m_rowHeightHint) { qDebug("Register::rowHeightHint(): m_rowHeightHint is zero!!"); } return d->m_rowHeightHint; } void Register::focusInEvent(QFocusEvent* ev) { Q_D(const Register); QTableWidget::focusInEvent(ev); if (d->m_focusItem) { d->m_focusItem->setFocus(true, false); } } bool Register::event(QEvent* event) { if (event->type() == QEvent::ToolTip) { QHelpEvent *helpEvent = static_cast(event); // get the row, if it's the header, then we're done // otherwise, adjust the row to be 0 based. int row = rowAt(helpEvent->y()); if (!row) return true; --row; int col = columnAt(helpEvent->x()); RegisterItem* item = itemAtRow(row); if (!item) return true; row = row - item->startRow(); QString msg; QRect rect; if (!item->maybeTip(helpEvent->pos(), row, col, rect, msg)) return true; if (!msg.isEmpty()) { QToolTip::showText(helpEvent->globalPos(), msg); } else { QToolTip::hideText(); event->ignore(); } return true; } return TransactionEditorContainer::event(event); } void Register::focusOutEvent(QFocusEvent* ev) { Q_D(Register); if (d->m_focusItem) { d->m_focusItem->setFocus(false, false); } QTableWidget::focusOutEvent(ev); } void Register::resizeEvent(QResizeEvent* ev) { TransactionEditorContainer::resizeEvent(ev); resize((int)eTransaction::Column::Detail, true); } void Register::resize() { resize((int)eTransaction::Column::Detail); } void Register::resize(int col, bool force) { Q_D(Register); if (!d->m_needResize && !force) return; d->m_needResize = false; // resize the register int w = viewport()->width(); // TODO I was playing a bit with manual ledger resizing but could not get // a good solution. I just leave the code around, so that maybe others // pick it up again. So far, it's not clear to me where to store the // size of the sections: // // a) with the account (as it is done now) // b) with the application for the specific account type // c) ???? // // Ideas are welcome (ipwizard: 2007-07-19) // Note: currently there's no way to switch back to automatic // column sizing once the manual sizing option has been saved #if 0 if (m_account.value("kmm-ledger-column-width").isEmpty()) { #endif // check which space we need if (columnWidth((int)eTransaction::Column::Number)) adjustColumn((int)eTransaction::Column::Number); if (columnWidth((int)eTransaction::Column::Account)) adjustColumn((int)eTransaction::Column::Account); if (columnWidth((int)eTransaction::Column::Payment)) adjustColumn((int)eTransaction::Column::Payment); if (columnWidth((int)eTransaction::Column::Deposit)) adjustColumn((int)eTransaction::Column::Deposit); if (columnWidth((int)eTransaction::Column::Quantity)) adjustColumn((int)eTransaction::Column::Quantity); if (columnWidth((int)eTransaction::Column::Balance)) adjustColumn((int)eTransaction::Column::Balance); if (columnWidth((int)eTransaction::Column::Price)) adjustColumn((int)eTransaction::Column::Price); if (columnWidth((int)eTransaction::Column::Value)) adjustColumn((int)eTransaction::Column::Value); // make amount columns all the same size // only extend the entry columns to make sure they fit // the widget int dwidth = 0; int ewidth = 0; if (ewidth < columnWidth((int)eTransaction::Column::Payment)) ewidth = columnWidth((int)eTransaction::Column::Payment); if (ewidth < columnWidth((int)eTransaction::Column::Deposit)) ewidth = columnWidth((int)eTransaction::Column::Deposit); if (ewidth < columnWidth((int)eTransaction::Column::Quantity)) ewidth = columnWidth((int)eTransaction::Column::Quantity); if (dwidth < columnWidth((int)eTransaction::Column::Balance)) dwidth = columnWidth((int)eTransaction::Column::Balance); if (ewidth < columnWidth((int)eTransaction::Column::Price)) ewidth = columnWidth((int)eTransaction::Column::Price); if (dwidth < columnWidth((int)eTransaction::Column::Value)) dwidth = columnWidth((int)eTransaction::Column::Value); int swidth = columnWidth((int)eTransaction::Column::Security); if (swidth > 0) { adjustColumn((int)eTransaction::Column::Security); swidth = columnWidth((int)eTransaction::Column::Security); } adjustColumn((int)eTransaction::Column::Date); #ifndef KMM_DESIGNER // Resize the date and money fields to either // a) the size required by the input widget if no transaction form is shown and the register is used with an editor // b) the adjusted value for the input widget if the transaction form is visible or an editor is not used if (d->m_usedWithEditor && !KMyMoneySettings::transactionForm()) { QPushButton *pushButton = new QPushButton; const int pushButtonSpacing = pushButton->sizeHint().width() + 5; setColumnWidth((int)eTransaction::Column::Date, columnWidth((int)eTransaction::Column::Date) + pushButtonSpacing + 4/* space for the spinbox arrows */); ewidth += pushButtonSpacing; if (swidth > 0) { // extend the security width to make space for the selector arrow swidth = columnWidth((int)eTransaction::Column::Security) + 40; } delete pushButton; } #endif if (columnWidth((int)eTransaction::Column::Payment)) setColumnWidth((int)eTransaction::Column::Payment, ewidth); if (columnWidth((int)eTransaction::Column::Deposit)) setColumnWidth((int)eTransaction::Column::Deposit, ewidth); if (columnWidth((int)eTransaction::Column::Quantity)) setColumnWidth((int)eTransaction::Column::Quantity, ewidth); if (columnWidth((int)eTransaction::Column::Balance)) setColumnWidth((int)eTransaction::Column::Balance, dwidth); if (columnWidth((int)eTransaction::Column::Price)) setColumnWidth((int)eTransaction::Column::Price, ewidth); if (columnWidth((int)eTransaction::Column::Value)) setColumnWidth((int)eTransaction::Column::Value, dwidth); if (columnWidth((int)eTransaction::Column::ReconcileFlag)) setColumnWidth((int)eTransaction::Column::ReconcileFlag, 20); if (swidth > 0) setColumnWidth((int)eTransaction::Column::Security, swidth); #if 0 // see comment above } else { QStringList colSizes = QStringList::split(",", m_account.value("kmm-ledger-column-width"), true); for (int i; i < colSizes.count(); ++i) { int colWidth = colSizes[i].toInt(); if (colWidth == 0) continue; setColumnWidth(i, w * colWidth / 100); } } #endif for (auto i = 0; i < columnCount(); ++i) { if (i == col) continue; w -= columnWidth(i); } setColumnWidth(col, w); } void Register::forceUpdateLists() { Q_D(Register); d->m_listsDirty = true; } int Register::minimumColumnWidth(int col) { Q_D(Register); QHeaderView *topHeader = horizontalHeader(); int w = topHeader->fontMetrics().width(horizontalHeaderItem(col) ? horizontalHeaderItem(col)->text() : QString()) + 10; w = qMax(w, 20); #ifdef KMM_DESIGNER return w; #else int maxWidth = 0; int minWidth = 0; QFontMetrics cellFontMetrics(KMyMoneySettings::listCellFontEx()); switch (col) { case (int)eTransaction::Column::Date: minWidth = cellFontMetrics.width(QLocale().toString(QDate(6999, 12, 29), QLocale::ShortFormat) + " "); break; default: break; } // scan through the transactions for (auto i = 0; i < d->m_items.size(); ++i) { RegisterItem* const item = d->m_items[i]; if (!item) continue; auto t = dynamic_cast(item); if (t) { int nw = 0; try { nw = t->registerColWidth(col, cellFontMetrics); } catch (const MyMoneyException &) { // This should only be reached if the data in the file disappeared // from under us, such as when the account was deleted from a // different view, then this view is restored. In this case, new // data is about to be loaded into the view anyway, so just remove // the item from the register and swallow the exception. //qDebug("%s", qPrintable(e.what())); removeItem(t); } w = qMax(w, nw); if (maxWidth) { if (w > maxWidth) { w = maxWidth; break; } } if (w < minWidth) { w = minWidth; break; } } } return w; #endif } void Register::adjustColumn(int col) { setColumnWidth(col, minimumColumnWidth(col)); } void Register::clearSelection() { unselectItems(); TransactionEditorContainer::clearSelection(); } void Register::doSelectItems(int from, int to, bool selected) { Q_D(Register); int start, end; // make sure start is smaller than end if (from <= to) { start = from; end = to; } else { start = to; end = from; } // make sure we stay in bounds if (start < 0) start = 0; if ((end <= -1) || (end > (d->m_items.size() - 1))) end = d->m_items.size() - 1; RegisterItem* firstItem; RegisterItem* lastItem; firstItem = lastItem = 0; for (int i = start; i <= end; ++i) { RegisterItem* const item = d->m_items[i]; if (item) { if (selected != item->isSelected()) { if (!firstItem) firstItem = item; item->setSelected(selected); lastItem = item; } } } } RegisterItem* Register::itemAtRow(int row) const { Q_D(const Register); if (row >= 0 && row < d->m_itemIndex.size()) { return d->m_itemIndex[row]; } return 0; } int Register::rowToIndex(int row) const { Q_D(const Register); for (auto i = 0; i < d->m_items.size(); ++i) { RegisterItem* const item = d->m_items[i]; if (!item) continue; if (row >= item->startRow() && row < (item->startRow() + item->numRowsRegister())) return i; } return -1; } void Register::selectedTransactions(SelectedTransactions& list) const { Q_D(const Register); if (d->m_focusItem && d->m_focusItem->isSelected() && d->m_focusItem->isVisible()) { auto t = dynamic_cast(d->m_focusItem); if (t) { QString id; if (t->isScheduled()) id = t->transaction().id(); SelectedTransaction s(t->transaction(), t->split(), id); list << s; } } for (auto i = 0; i < d->m_items.size(); ++i) { RegisterItem* const item = d->m_items[i]; // make sure, we don't include the focus item twice if (item == d->m_focusItem) continue; if (item && item->isSelected() && item->isVisible()) { auto t = dynamic_cast(item); if (t) { QString id; if (t->isScheduled()) id = t->transaction().id(); SelectedTransaction s(t->transaction(), t->split(), id); list << s; } } } } QList Register::selectedItems() const { Q_D(const Register); QList list; RegisterItem* item = d->m_firstItem; while (item) { if (item && item->isSelected() && item->isVisible()) { list << item; } item = item->nextItem(); } return list; } int Register::selectedItemsCount() const { Q_D(const Register); auto cnt = 0; RegisterItem* item = d->m_firstItem; while (item) { if (item->isSelected() && item->isVisible()) ++cnt; item = item->nextItem(); } return cnt; } void Register::mouseReleaseEvent(QMouseEvent *e) { Q_D(Register); if (e->button() == Qt::RightButton) { // see the comment in Register::contextMenuEvent // on Linux we never get here but on Windows this // event is fired before the contextMenuEvent which // causes the loss of the multiple selection; to avoid // this just ignore the event and act like on Linux return; } if (d->m_ignoreNextButtonRelease) { d->m_ignoreNextButtonRelease = false; return; } d->m_mouseButton = e->button(); d->m_modifiers = QApplication::keyboardModifiers(); QTableWidget::mouseReleaseEvent(e); } void Register::contextMenuEvent(QContextMenuEvent *e) { Q_D(Register); if (e->reason() == QContextMenuEvent::Mouse) { // since mouse release event is not called, we need // to reset the mouse button and the modifiers here d->m_mouseButton = Qt::NoButton; d->m_modifiers = Qt::NoModifier; // if a selected item is clicked don't change the selection RegisterItem* item = itemAtRow(rowAt(e->y())); if (item && !item->isSelected()) selectItem(rowAt(e->y()), columnAt(e->x())); } openContextMenu(); } void Register::unselectItems(int from, int to) { doSelectItems(from, to, false); } void Register::selectItems(int from, int to) { doSelectItems(from, to, true); } void Register::selectItem(int row, int col) { Q_D(Register); if (row >= 0 && row < d->m_itemIndex.size()) { RegisterItem* item = d->m_itemIndex[row]; // don't support selecting when the item has an editor // or the item itself is not selectable if (item->hasEditorOpen() || !item->isSelectable()) { d->m_mouseButton = Qt::NoButton; return; } QString id = item->id(); selectItem(item); // selectItem() might have changed the pointers, so we // need to reconstruct it here item = itemById(id); auto t = dynamic_cast(item); if (t) { if (!id.isEmpty()) { if (t && col == (int)eTransaction::Column::ReconcileFlag && selectedItemsCount() == 1 && !t->isScheduled()) emit reconcileStateColumnClicked(t); } else { emit emptyItemSelected(); } } } } void Register::setAnchorItem(RegisterItem* anchorItem) { Q_D(Register); d->m_selectAnchor = anchorItem; } bool Register::setFocusItem(RegisterItem* focusItem) { Q_D(Register); if (focusItem && focusItem->canHaveFocus()) { if (d->m_focusItem) { d->m_focusItem->setFocus(false); } auto item = dynamic_cast(focusItem); if (d->m_focusItem != focusItem && item) { emit focusChanged(item); } d->m_focusItem = focusItem; d->m_focusItem->setFocus(true); if (d->m_listsDirty) updateRegister(KMyMoneySettings::ledgerLens() | !KMyMoneySettings::transactionForm()); ensureItemVisible(d->m_focusItem); return true; } else return false; } bool Register::setFocusToTop() { Q_D(Register); RegisterItem* rgItem = d->m_firstItem; while (rgItem) { if (setFocusItem(rgItem)) return true; rgItem = rgItem->nextItem(); } return false; } void Register::selectItem(RegisterItem* item, bool dontChangeSelections) { Q_D(Register); if (!item) return; Qt::MouseButtons buttonState = d->m_mouseButton; Qt::KeyboardModifiers modifiers = d->m_modifiers; d->m_mouseButton = Qt::NoButton; d->m_modifiers = Qt::NoModifier; if (d->m_selectionMode == NoSelection) return; if (item->isSelectable()) { QString id = item->id(); QList itemList = selectedItems(); bool okToSelect = true; auto cnt = itemList.count(); auto scheduledTransactionSelected = false; if (cnt > 0) { auto& r = *(itemList.front()); scheduledTransactionSelected = (typeid(r) == typeid(StdTransactionScheduled)); } if (buttonState & Qt::LeftButton) { if (!(modifiers & (Qt::ShiftModifier | Qt::ControlModifier)) || (d->m_selectAnchor == 0)) { if ((cnt != 1) || ((cnt == 1) && !item->isSelected())) { emit aboutToSelectItem(item, okToSelect); if (okToSelect) { // pointer 'item' might have changed. reconstruct it. item = itemById(id); unselectItems(); item->setSelected(true); setFocusItem(item); } } if (okToSelect) d->m_selectAnchor = item; } if (d->m_selectionMode == MultiSelection) { switch (modifiers & (Qt::ShiftModifier | Qt::ControlModifier)) { case Qt::ControlModifier: if (scheduledTransactionSelected || typeid(*item) == typeid(StdTransactionScheduled)) okToSelect = false; // toggle selection state of current item emit aboutToSelectItem(item, okToSelect); if (okToSelect) { // pointer 'item' might have changed. reconstruct it. item = itemById(id); item->setSelected(!item->isSelected()); setFocusItem(item); } break; case Qt::ShiftModifier: if (scheduledTransactionSelected || typeid(*item) == typeid(StdTransactionScheduled)) okToSelect = false; emit aboutToSelectItem(item, okToSelect); if (okToSelect) { // pointer 'item' might have changed. reconstruct it. item = itemById(id); unselectItems(); if (d->m_selectAnchor) selectItems(rowToIndex(d->m_selectAnchor->startRow()), rowToIndex(item->startRow())); setFocusItem(item); } break; } } } else { // we get here when called by application logic emit aboutToSelectItem(item, okToSelect); if (okToSelect) { // pointer 'item' might have changed. reconstruct it. item = itemById(id); if (!dontChangeSelections) unselectItems(); item->setSelected(true); setFocusItem(item); d->m_selectAnchor = item; } } if (okToSelect) { SelectedTransactions list(this); emit transactionsSelected(list); } } } void Register::ensureFocusItemVisible() { Q_D(Register); ensureItemVisible(d->m_focusItem); } void Register::ensureItemVisible(RegisterItem* item) { Q_D(Register); if (!item) return; d->m_ensureVisibleItem = item; QTimer::singleShot(0, this, SLOT(slotEnsureItemVisible())); } void Register::slotDoubleClicked(int row, int) { Q_D(Register); if (row >= 0 && row < d->m_itemIndex.size()) { RegisterItem* p = d->m_itemIndex[row]; if (p->isSelectable()) { d->m_ignoreNextButtonRelease = true; // double click to start editing only works if the focus // item is among the selected ones if (!focusItem()) { setFocusItem(p); if (d->m_selectionMode != NoSelection) p->setSelected(true); } if (d->m_focusItem->isSelected()) { // don't emit the signal right away but wait until // we come back to the Qt main loop QTimer::singleShot(0, this, SIGNAL(editTransaction())); } } } } void Register::slotEnsureItemVisible() { Q_D(Register); // if clear() has been called since the timer was // started, we just ignore the call if (!d->m_ensureVisibleItem) return; // make sure to catch latest changes setUpdatesEnabled(false); updateRegister(); setUpdatesEnabled(true); // since the item will be made visible at the top of the viewport make the bottom index visible first to make the whole item visible scrollTo(model()->index(d->m_ensureVisibleItem->startRow() + d->m_ensureVisibleItem->numRowsRegister() - 1, (int)eTransaction::Column::Detail)); scrollTo(model()->index(d->m_ensureVisibleItem->startRow(), (int)eTransaction::Column::Detail)); } QString Register::text(int /*row*/, int /*col*/) const { return QString("a"); } QWidget* Register::createEditor(int /*row*/, int /*col*/, bool /*initFromCell*/) const { return 0; } void Register::setCellContentFromEditor(int /*row*/, int /*col*/) { } void Register::endEdit(int /*row*/, int /*col*/, bool /*accept*/, bool /*replace*/) { } RegisterItem* Register::focusItem() const { Q_D(const Register); return d->m_focusItem; } RegisterItem* Register::anchorItem() const { Q_D(const Register); return d->m_selectAnchor; } void Register::arrangeEditWidgets(QMap& editWidgets, KMyMoneyRegister::Transaction* t) { t->arrangeWidgetsInRegister(editWidgets); ensureItemVisible(t); // updateContents(); } void Register::tabOrder(QWidgetList& tabOrderWidgets, KMyMoneyRegister::Transaction* t) const { t->tabOrderInRegister(tabOrderWidgets); } void Register::removeEditWidgets(QMap& editWidgets) { // remove pointers from map QMap::iterator it; for (it = editWidgets.begin(); it != editWidgets.end();) { if ((*it)->parentWidget() == this) { editWidgets.erase(it); it = editWidgets.begin(); } else ++it; } // now delete the widgets if (auto t = dynamic_cast(focusItem())) { for (int row = t->startRow(); row < t->startRow() + t->numRowsRegister(true); ++row) { for (int col = 0; col < columnCount(); ++col) { if (cellWidget(row, col)) { cellWidget(row, col)->hide(); setCellWidget(row, col, 0); } } // make sure to reduce the possibly size to what it was before editing started setRowHeight(row, t->rowHeightHint()); } } } RegisterItem* Register::itemById(const QString& id) const { Q_D(const Register); if (id.isEmpty()) return d->m_lastItem; for (QVector::size_type i = 0; i < d->m_items.size(); ++i) { RegisterItem* item = d->m_items[i]; if (!item) continue; if (item->id() == id) return item; } return 0; } void Register::handleItemChange(RegisterItem* old, bool shift, bool control) { Q_D(Register); if (d->m_selectionMode == MultiSelection) { if (shift) { selectRange(d->m_selectAnchor ? d->m_selectAnchor : old, d->m_focusItem, false, true, (d->m_selectAnchor && !control) ? true : false); } else if (!control) { selectItem(d->m_focusItem, false); } } } void Register::selectRange(RegisterItem* from, RegisterItem* to, bool invert, bool includeFirst, bool clearSel) { if (!from || !to) return; if (from == to && !includeFirst) return; bool swap = false; if (to == from->prevItem()) swap = true; RegisterItem* item; if (!swap && from != to && from != to->prevItem()) { bool found = false; for (item = from; item; item = item->nextItem()) { if (item == to) { found = true; break; } } if (!found) swap = true; } if (swap) { item = from; from = to; to = item; if (!includeFirst) to = to->prevItem(); } else if (!includeFirst) { from = from->nextItem(); } if (clearSel) { for (item = firstItem(); item; item = item->nextItem()) { if (item->isSelected() && item->isVisible()) { item->setSelected(false); } } } for (item = from; item; item = item->nextItem()) { if (item->isSelectable()) { if (!invert) { if (!item->isSelected() && item->isVisible()) { item->setSelected(true); } } else { bool sel = !item->isSelected(); if ((item->isSelected() != sel) && item->isVisible()) { item->setSelected(sel); } } } if (item == to) break; } } void Register::scrollPage(int key, Qt::KeyboardModifiers modifiers) { Q_D(Register); RegisterItem* oldFocusItem = d->m_focusItem; // make sure we have a focus item if (!d->m_focusItem) setFocusItem(d->m_firstItem); if (!d->m_focusItem && d->m_firstItem) setFocusItem(d->m_firstItem->nextItem()); if (!d->m_focusItem) return; RegisterItem* item = d->m_focusItem; int height = 0; switch (key) { case Qt::Key_PageUp: while (height < viewport()->height() && item->prevItem()) { do { item = item->prevItem(); if (item->isVisible()) height += item->rowHeightHint(); } while ((!item->isSelectable() || !item->isVisible()) && item->prevItem()); while ((!item->isSelectable() || !item->isVisible()) && item->nextItem()) item = item->nextItem(); } break; case Qt::Key_PageDown: while (height < viewport()->height() && item->nextItem()) { do { if (item->isVisible()) height += item->rowHeightHint(); item = item->nextItem(); } while ((!item->isSelectable() || !item->isVisible()) && item->nextItem()); while ((!item->isSelectable() || !item->isVisible()) && item->prevItem()) item = item->prevItem(); } break; case Qt::Key_Up: if (item->prevItem()) { do { item = item->prevItem(); } while ((!item->isSelectable() || !item->isVisible()) && item->prevItem()); } break; case Qt::Key_Down: if (item->nextItem()) { do { item = item->nextItem(); } while ((!item->isSelectable() || !item->isVisible()) && item->nextItem()); } break; case Qt::Key_Home: item = d->m_firstItem; while ((!item->isSelectable() || !item->isVisible()) && item->nextItem()) item = item->nextItem(); break; case Qt::Key_End: item = d->m_lastItem; while ((!item->isSelectable() || !item->isVisible()) && item->prevItem()) item = item->prevItem(); break; } // make sure to avoid selecting a possible empty transaction at the end auto t = dynamic_cast(item); if (t && t->transaction().id().isEmpty()) { if (t->prevItem()) { item = t->prevItem(); } } if (!(modifiers & Qt::ShiftModifier) || !d->m_selectAnchor) d->m_selectAnchor = item; setFocusItem(item); if (item->isSelectable()) { handleItemChange(oldFocusItem, modifiers & Qt::ShiftModifier, modifiers & Qt::ControlModifier); // tell the world about the changes in selection SelectedTransactions list(this); emit transactionsSelected(list); } if (d->m_focusItem && !d->m_focusItem->isSelected() && d->m_selectionMode == SingleSelection) selectItem(item); } void Register::keyPressEvent(QKeyEvent* ev) { Q_D(Register); switch (ev->key()) { case Qt::Key_Space: if (d->m_selectionMode != NoSelection) { // get the state out of the event ... d->m_modifiers = ev->modifiers(); // ... and pretend that we have pressed the left mouse button ;) d->m_mouseButton = Qt::LeftButton; selectItem(d->m_focusItem); } break; case Qt::Key_PageUp: case Qt::Key_PageDown: case Qt::Key_Home: case Qt::Key_End: case Qt::Key_Down: case Qt::Key_Up: scrollPage(ev->key(), ev->modifiers()); break; case Qt::Key_Enter: case Qt::Key_Return: // don't emit the signal right away but wait until // we come back to the Qt main loop QTimer::singleShot(0, this, SIGNAL(editTransaction())); break; default: QTableWidget::keyPressEvent(ev); break; } } Transaction* Register::transactionFactory(Register *parent, const MyMoneyTransaction& transaction, const MyMoneySplit& split, int uniqueId) { Transaction* t = 0; MyMoneySplit s = split; if (parent->account() == MyMoneyAccount()) { t = new KMyMoneyRegister::StdTransaction(parent, transaction, s, uniqueId); return t; } switch (parent->account().accountType()) { case Account::Type::Checkings: case Account::Type::Savings: case Account::Type::Cash: case Account::Type::CreditCard: case Account::Type::Loan: case Account::Type::Asset: case Account::Type::Liability: case Account::Type::Currency: case Account::Type::Income: case Account::Type::Expense: case Account::Type::AssetLoan: case Account::Type::Equity: if (s.accountId().isEmpty()) s.setAccountId(parent->account().id()); if (s.isMatched()) t = new KMyMoneyRegister::StdTransactionMatched(parent, transaction, s, uniqueId); else if (transaction.isImported()) t = new KMyMoneyRegister::StdTransactionDownloaded(parent, transaction, s, uniqueId); else t = new KMyMoneyRegister::StdTransaction(parent, transaction, s, uniqueId); break; case Account::Type::Investment: if (s.isMatched()) t = new KMyMoneyRegister::InvestTransaction/* Matched */(parent, transaction, s, uniqueId); else if (transaction.isImported()) t = new KMyMoneyRegister::InvestTransactionDownloaded(parent, transaction, s, uniqueId); else t = new KMyMoneyRegister::InvestTransaction(parent, transaction, s, uniqueId); break; case Account::Type::CertificateDep: case Account::Type::MoneyMarket: case Account::Type::Stock: default: qDebug("Register::transactionFactory: invalid accountTypeE %d", (int)parent->account().accountType()); break; } return t; } const MyMoneyAccount& Register::account() const { Q_D(const Register); return d->m_account; } void Register::addGroupMarkers() { Q_D(Register); QMap list; QMap::const_iterator it; KMyMoneyRegister::RegisterItem* p = firstItem(); KMyMoneyRegister::Transaction* t; QString name; QDate today; QDate yesterday, thisWeek, lastWeek; QDate thisMonth, lastMonth; QDate thisYear; int weekStartOfs; switch (primarySortKey()) { case SortField::PostDate: case SortField::EntryDate: today = QDate::currentDate(); thisMonth.setDate(today.year(), today.month(), 1); lastMonth = thisMonth.addMonths(-1); yesterday = today.addDays(-1); // a = QDate::dayOfWeek() todays weekday (1 = Monday, 7 = Sunday) // b = QLocale().firstDayOfWeek() first day of week (1 = Monday, 7 = Sunday) weekStartOfs = today.dayOfWeek() - QLocale().firstDayOfWeek(); if (weekStartOfs < 0) { weekStartOfs = 7 + weekStartOfs; } thisWeek = today.addDays(-weekStartOfs); lastWeek = thisWeek.addDays(-7); thisYear.setDate(today.year(), 1, 1); if (KMyMoneySettings::startDate().date() != QDate(1900, 1, 1)) new KMyMoneyRegister::FancyDateGroupMarker(this, KMyMoneySettings::startDate().date(), i18n("Prior transactions possibly filtered")); if (KMyMoneySettings::showFancyMarker()) { if (d->m_account.lastReconciliationDate().isValid()) new KMyMoneyRegister::StatementGroupMarker(this, eRegister::CashFlowDirection::Deposit, d->m_account.lastReconciliationDate(), i18n("Last reconciliation")); if (!d->m_account.value("lastImportedTransactionDate").isEmpty() && !d->m_account.value("lastStatementBalance").isEmpty()) { MyMoneyMoney balance(d->m_account.value("lastStatementBalance")); if (d->m_account.accountGroup() == Account::Type::Liability) balance = -balance; auto txt = i18n("Online Statement Balance: %1", balance.formatMoney(d->m_account.fraction())); - KMyMoneyRegister::StatementGroupMarker *p = new KMyMoneyRegister::StatementGroupMarker(this, eRegister::CashFlowDirection::Deposit, QDate::fromString(d->m_account.value("lastImportedTransactionDate"), Qt::ISODate), txt); + KMyMoneyRegister::StatementGroupMarker *pGroupMarker = new KMyMoneyRegister::StatementGroupMarker(this, eRegister::CashFlowDirection::Deposit, QDate::fromString(d->m_account.value("lastImportedTransactionDate"), Qt::ISODate), txt); - p->setErroneous(!MyMoneyFile::instance()->hasMatchingOnlineBalance(d->m_account)); + pGroupMarker->setErroneous(!MyMoneyFile::instance()->hasMatchingOnlineBalance(d->m_account)); } new KMyMoneyRegister::FancyDateGroupMarker(this, thisYear, i18n("This year")); new KMyMoneyRegister::FancyDateGroupMarker(this, lastMonth, i18n("Last month")); new KMyMoneyRegister::FancyDateGroupMarker(this, thisMonth, i18n("This month")); new KMyMoneyRegister::FancyDateGroupMarker(this, lastWeek, i18n("Last week")); new KMyMoneyRegister::FancyDateGroupMarker(this, thisWeek, i18n("This week")); new KMyMoneyRegister::FancyDateGroupMarker(this, yesterday, i18n("Yesterday")); new KMyMoneyRegister::FancyDateGroupMarker(this, today, i18n("Today")); new KMyMoneyRegister::FancyDateGroupMarker(this, today.addDays(1), i18n("Future transactions")); new KMyMoneyRegister::FancyDateGroupMarker(this, thisWeek.addDays(7), i18n("Next week")); new KMyMoneyRegister::FancyDateGroupMarker(this, thisMonth.addMonths(1), i18n("Next month")); } else { new KMyMoneyRegister::SimpleDateGroupMarker(this, today.addDays(1), i18n("Future transactions")); } if (KMyMoneySettings::showFiscalMarker()) { QDate currentFiscalYear = KMyMoneySettings::firstFiscalDate(); new KMyMoneyRegister::FiscalYearGroupMarker(this, currentFiscalYear, i18n("Current fiscal year")); new KMyMoneyRegister::FiscalYearGroupMarker(this, currentFiscalYear.addYears(-1), i18n("Previous fiscal year")); new KMyMoneyRegister::FiscalYearGroupMarker(this, currentFiscalYear.addYears(1), i18n("Next fiscal year")); } break; case SortField::Type: if (KMyMoneySettings::showFancyMarker()) { new KMyMoneyRegister::TypeGroupMarker(this, eRegister::CashFlowDirection::Deposit, d->m_account.accountType()); new KMyMoneyRegister::TypeGroupMarker(this, eRegister::CashFlowDirection::Payment, d->m_account.accountType()); } break; case SortField::ReconcileState: if (KMyMoneySettings::showFancyMarker()) { new KMyMoneyRegister::ReconcileGroupMarker(this, eMyMoney::Split::State::NotReconciled); new KMyMoneyRegister::ReconcileGroupMarker(this, eMyMoney::Split::State::Cleared); new KMyMoneyRegister::ReconcileGroupMarker(this, eMyMoney::Split::State::Reconciled); new KMyMoneyRegister::ReconcileGroupMarker(this, eMyMoney::Split::State::Frozen); } break; case SortField::Payee: if (KMyMoneySettings::showFancyMarker()) { while (p) { if ((t = dynamic_cast(p))) list[t->sortPayee()] = 1; p = p->nextItem(); } for (it = list.constBegin(); it != list.constEnd(); ++it) { name = it.key(); if (name.isEmpty()) { name = i18nc("Unknown payee", "Unknown"); } new KMyMoneyRegister::PayeeGroupMarker(this, name); } } break; case SortField::Category: if (KMyMoneySettings::showFancyMarker()) { while (p) { if ((t = dynamic_cast(p))) list[t->sortCategory()] = 1; p = p->nextItem(); } for (it = list.constBegin(); it != list.constEnd(); ++it) { name = it.key(); if (name.isEmpty()) { name = i18nc("Unknown category", "Unknown"); } new KMyMoneyRegister::CategoryGroupMarker(this, name); } } break; case SortField::Security: if (KMyMoneySettings::showFancyMarker()) { while (p) { if ((t = dynamic_cast(p))) list[t->sortSecurity()] = 1; p = p->nextItem(); } for (it = list.constBegin(); it != list.constEnd(); ++it) { name = it.key(); if (name.isEmpty()) { name = i18nc("Unknown security", "Unknown"); } new KMyMoneyRegister::CategoryGroupMarker(this, name); } } break; default: // no markers supported break; } } void Register::removeUnwantedGroupMarkers() { // remove all trailing group markers except statement markers KMyMoneyRegister::RegisterItem* q; KMyMoneyRegister::RegisterItem* p = lastItem(); while (p) { q = p; if (dynamic_cast(p) || dynamic_cast(p)) break; p = p->prevItem(); delete q; } // remove all adjacent group markers bool lastWasGroupMarker = false; p = lastItem(); while (p) { q = p; auto m = dynamic_cast(p); p = p->prevItem(); if (m) { m->markVisible(true); // make adjacent group marker invisible except those that show statement information if (lastWasGroupMarker && (dynamic_cast(m) == 0)) { m->markVisible(false); } lastWasGroupMarker = true; } else if (q->isVisible()) lastWasGroupMarker = false; } } void Register::setLedgerLensForced(bool forced) { Q_D(Register); d->m_ledgerLensForced = forced; } bool Register::ledgerLens() const { Q_D(const Register); return d->m_ledgerLensForced; } void Register::setSelectionMode(SelectionMode mode) { Q_D(Register); d->m_selectionMode = mode; } void Register::setUsedWithEditor(bool value) { Q_D(Register); d->m_usedWithEditor = value; } eRegister::DetailColumn Register::getDetailsColumnType() const { Q_D(const Register); return d->m_detailsColumnType; } void Register::setDetailsColumnType(eRegister::DetailColumn detailsColumnType) { Q_D(Register); d->m_detailsColumnType = detailsColumnType; } } diff --git a/kmymoney/widgets/registeritemdelegate.h b/kmymoney/widgets/registeritemdelegate.h index 10c9c4084..ef3711cf8 100644 --- a/kmymoney/widgets/registeritemdelegate.h +++ b/kmymoney/widgets/registeritemdelegate.h @@ -1,57 +1,57 @@ /*************************************************************************** registeritemdelegate.h ---------- begin : Fri Mar 10 2006 copyright : (C) 2006 by Thomas Baumgart email : Thomas Baumgart (C) 2017 by Łukasz Wojniłowicz ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #ifndef REGISTERITEMDELEGATE_H #define REGISTERITEMDELEGATE_H // ---------------------------------------------------------------------------- // QT Includes #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes class QPainter; class QModelIndex; class QStyleOptionViewItem; namespace KMyMoneyRegister { class Register; class RegisterItemDelegate : public QStyledItemDelegate { Q_OBJECT Q_DISABLE_COPY(RegisterItemDelegate) public: explicit RegisterItemDelegate(Register *parent); ~RegisterItemDelegate(); - void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const; + void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const final override; private: Register *m_register; }; } // namespace #endif diff --git a/kmymoney/widgets/stdtransaction.cpp b/kmymoney/widgets/stdtransaction.cpp index 660036edd..0bda40e9f 100644 --- a/kmymoney/widgets/stdtransaction.cpp +++ b/kmymoney/widgets/stdtransaction.cpp @@ -1,702 +1,702 @@ /*************************************************************************** stdtransaction.cpp - description ------------------- begin : Tue Jun 13 2006 copyright : (C) 2000-2006 by 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 "stdtransaction.h" #include "stdtransaction_p.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include // ---------------------------------------------------------------------------- // Project Includes #include "kmymoneypayeecombo.h" #include "kmymoneycombo.h" #include "kmymoneytagcombo.h" #include "tabbar.h" #include "ktagcontainer.h" #include "mymoneytransaction.h" #include "mymoneysplit.h" #include "mymoneyexception.h" #include "mymoneyfile.h" #include "register.h" #include "transactionform.h" #include "kmymoneylineedit.h" #include "kmymoneyutils.h" #ifndef KMM_DESIGNER #include "stdtransactioneditor.h" #endif #include "kmymoneysettings.h" #include "widgetenums.h" #include "mymoneyenums.h" using namespace eWidgets; using namespace KMyMoneyRegister; using namespace KMyMoneyTransactionForm; StdTransaction::StdTransaction(Register *parent, const MyMoneyTransaction& transaction, const MyMoneySplit& split, int uniqueId) : Transaction(*new StdTransactionPrivate, parent, transaction, split, uniqueId) { Q_D(StdTransaction); d->m_showAccountRow = false; try { d->m_categoryHeader = i18n("Category"); switch (transaction.splitCount()) { default: d->m_category = i18nc("Split transaction (category replacement)", "Split transaction"); break; case 0: // the empty transaction case 1: break; case 2: setupFormHeader(d->m_transaction.splitByAccount(d->m_split.accountId(), false).accountId()); break; } } catch (const MyMoneyException &e) { qDebug() << "Problem determining the category for transaction '" << d->m_transaction.id() << "'. Reason: " << e.what() << "\n"; } d->m_rowsForm = 6; if (KMyMoneyUtils::transactionType(d->m_transaction) == KMyMoneyUtils::InvestmentTransaction) { - MyMoneySplit split = KMyMoneyUtils::stockSplit(d->m_transaction); - d->m_payee = MyMoneyFile::instance()->account(split.accountId()).name(); + MyMoneySplit stockSplit = KMyMoneyUtils::stockSplit(d->m_transaction); + d->m_payee = MyMoneyFile::instance()->account(stockSplit.accountId()).name(); QString addon; - if (split.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::BuyShares)) { - if (split.value().isNegative()) { + if (stockSplit.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::BuyShares)) { + if (stockSplit.value().isNegative()) { addon = i18n("Sell"); } else { addon = i18n("Buy"); } - } else if (split.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::Dividend)) { + } else if (stockSplit.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::Dividend)) { addon = i18n("Dividend"); - } else if (split.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::Yield)) { + } else if (stockSplit.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::Yield)) { addon = i18n("Yield"); - } else if (split.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::InterestIncome)) { + } else if (stockSplit.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::InterestIncome)) { addon = i18n("Interest Income"); } if (!addon.isEmpty()) { d->m_payee += QString(" (%1)").arg(addon); } d->m_payeeHeader = i18n("Activity"); d->m_category = i18n("Investment transaction"); } // setup initial size setNumRowsRegister(numRowsRegister(KMyMoneySettings::showRegisterDetailed())); emit parent->itemAdded(this); } StdTransaction::~StdTransaction() { } const char* StdTransaction::className() { return "StdTransaction"; } void StdTransaction::setupFormHeader(const QString& id) { Q_D(StdTransaction); d->m_category = MyMoneyFile::instance()->accountToCategory(id); switch (MyMoneyFile::instance()->account(id).accountGroup()) { case eMyMoney::Account::Type::Asset: case eMyMoney::Account::Type::Liability: d->m_categoryHeader = d->m_split.shares().isNegative() ? i18n("Transfer to") : i18n("Transfer from"); break; default: d->m_categoryHeader = i18n("Category"); break; } } eRegister::Action StdTransaction::actionType() const { Q_D(const StdTransaction); eRegister::Action action = eRegister::Action::None; // if at least one split is referencing an income or // expense account, we will not call it a transfer auto found = false; foreach (const auto split, d->m_transaction.splits()) { if (split.accountId() == d->m_split.accountId()) continue; auto acc = MyMoneyFile::instance()->account(split.accountId()); if (acc.accountGroup() == eMyMoney::Account::Type::Income || acc.accountGroup() == eMyMoney::Account::Type::Expense) { // otherwise, we have to determine between deposit and withdrawal action = d->m_split.shares().isNegative() ? eRegister::Action::Withdrawal : eRegister::Action::Deposit; found = true; break; } } // otherwise, it's a transfer if (!found) action = eRegister::Action::Transfer; return action; } void StdTransaction::loadTab(TransactionForm* form) { Q_D(StdTransaction); KMyMoneyTransactionForm::TabBar* bar = form->getTabBar(); bar->setSignalEmission(eTabBar::SignalEmission::Never); for (auto i = 0; i < bar->count(); ++i) { bar->setTabEnabled(i, true); } if (d->m_transaction.splitCount() > 0) { bar->setCurrentIndex((int)actionType()); } bar->setSignalEmission(eTabBar::SignalEmission::Always); } int StdTransaction::numColsForm() const { return 4; } void StdTransaction::setupForm(TransactionForm* form) { Transaction::setupForm(form); form->setSpan(4, (int)eTransactionForm::Column::Value1, 3, 1); } bool StdTransaction::showRowInForm(int row) const { Q_D(const StdTransaction); return row == 0 ? d->m_showAccountRow : true; } void StdTransaction::setShowRowInForm(int row, bool show) { Q_D(StdTransaction); if (row == 0) d->m_showAccountRow = show; } bool StdTransaction::formCellText(QString& txt, Qt::Alignment& align, int row, int col, QPainter* /* painter */) { Q_D(const StdTransaction); // if(m_transaction != MyMoneyTransaction()) { switch (row) { case 0: switch (col) { case (int)eTransactionForm::Column::Label1: align |= Qt::AlignLeft; txt = i18n("Account"); break; } break; case 1: switch (col) { case (int)eTransactionForm::Column::Label1: align |= Qt::AlignLeft; txt = d->m_payeeHeader; break; case (int)eTransactionForm::Column::Value1: align |= Qt::AlignLeft; txt = d->m_payee; break; case (int)eTransactionForm::Column::Label2: align |= Qt::AlignLeft; if (haveNumberField()) txt = i18n("Number"); break; case (int)eTransactionForm::Column::Value2: align |= Qt::AlignRight; if (haveNumberField()) txt = d->m_split.number(); break; } break; case 2: switch (col) { case (int)eTransactionForm::Column::Label1: align |= Qt::AlignLeft; txt = d->m_categoryHeader; break; case (int)eTransactionForm::Column::Value1: align |= Qt::AlignLeft; txt = d->m_category; if (d->m_transaction != MyMoneyTransaction()) { if (txt.isEmpty() && !d->m_split.value().isZero()) txt = i18n("*** UNASSIGNED ***"); } break; case (int)eTransactionForm::Column::Label2: align |= Qt::AlignLeft; txt = i18n("Date"); break; case (int)eTransactionForm::Column::Value2: align |= Qt::AlignRight; if (d->m_transaction != MyMoneyTransaction()) txt = QLocale().toString(d->m_transaction.postDate(), QLocale::ShortFormat); break; } break; case 3: switch (col) { case (int)eTransactionForm::Column::Label1: align |= Qt::AlignLeft; txt = i18n("Tags"); break; case (int)eTransactionForm::Column::Value1: align |= Qt::AlignLeft; if (!d->m_tagList.isEmpty()) { for (auto i = 0; i < d->m_tagList.size() - 1; ++i) txt += d->m_tagList[i] + ", "; txt += d->m_tagList.last(); } //if (m_transaction != MyMoneyTransaction()) // txt = m_split.tagId(); break; case (int)eTransactionForm::Column::Label2: align |= Qt::AlignLeft; txt = i18n("Amount"); break; case (int)eTransactionForm::Column::Value2: align |= Qt::AlignRight; if (d->m_transaction != MyMoneyTransaction()) { txt = (d->m_split.value(d->m_transaction.commodity(), d->m_splitCurrencyId).abs()).formatMoney(d->m_account.fraction()); } break; } break; case 4: switch (col) { case (int)eTransactionForm::Column::Label1: align |= Qt::AlignLeft; txt = i18n("Memo"); break; case (int)eTransactionForm::Column::Value1: align &= ~Qt::AlignVCenter; align |= Qt::AlignTop; align |= Qt::AlignLeft; if (d->m_transaction != MyMoneyTransaction()) txt = d->m_split.memo().section('\n', 0, 2); break; } break; case 5: switch (col) { case (int)eTransactionForm::Column::Label2: align |= Qt::AlignLeft; txt = i18n("Status"); break; case (int)eTransactionForm::Column::Value2: align |= Qt::AlignRight; txt = reconcileState(); break; } } // } if (col == (int)eTransactionForm::Column::Value2 && row == 1) { return haveNumberField(); } return (col == (int)eTransactionForm::Column::Value1 && row < 5) || (col == (int)eTransactionForm::Column::Value2 && row > 0 && row != 4); } void StdTransaction::registerCellText(QString& txt, Qt::Alignment& align, int row, int col, QPainter* painter) { Q_D(const StdTransaction); switch (row) { case 0: switch (col) { case (int)eTransaction::Column::Number: align |= Qt::AlignLeft; if (haveNumberField()) txt = d->m_split.number(); break; case (int)eTransaction::Column::Date: align |= Qt::AlignLeft; txt = QLocale().toString(d->m_transaction.postDate(), QLocale::ShortFormat); break; case (int)eTransaction::Column::Detail: switch (d->m_parent->getDetailsColumnType()) { case eRegister::DetailColumn::PayeeFirst: txt = d->m_payee; break; case eRegister::DetailColumn::AccountFirst: txt = d->m_category; if (!d->m_tagList.isEmpty()) { txt += " ( "; for (auto i = 0; i < d->m_tagList.size() - 1; ++i) { txt += " " + d->m_tagList[i] + ", "; } txt += " " + d->m_tagList.last() + " )"; } break; } align |= Qt::AlignLeft; if (txt.isEmpty() && d->m_rowsRegister < 3) { singleLineMemo(txt, d->m_split); } if (txt.isEmpty() && d->m_rowsRegister < 2) { if (d->m_account.accountType() != eMyMoney::Account::Type::Income && d->m_account.accountType() != eMyMoney::Account::Type::Expense) { txt = d->m_category; if (txt.isEmpty() && !d->m_split.value().isZero()) { txt = i18n("*** UNASSIGNED ***"); if (painter) painter->setPen(KMyMoneySettings::schemeColor(SchemeColor::TransactionErroneous)); } } } break; case (int)eTransaction::Column::ReconcileFlag: align |= Qt::AlignHCenter; txt = reconcileState(false); break; case (int)eTransaction::Column::Payment: align |= Qt::AlignRight; if (d->m_split.value().isNegative()) { txt = (-d->m_split.value(d->m_transaction.commodity(), d->m_splitCurrencyId)).formatMoney(d->m_account.fraction()); } break; case (int)eTransaction::Column::Deposit: align |= Qt::AlignRight; if (!d->m_split.value().isNegative()) { txt = d->m_split.value(d->m_transaction.commodity(), d->m_splitCurrencyId).formatMoney(d->m_account.fraction()); } break; case (int)eTransaction::Column::Balance: align |= Qt::AlignRight; if (d->m_showBalance) txt = d->m_balance.formatMoney(d->m_account.fraction()); else txt = "----"; break; case (int)eTransaction::Column::Account: // txt = m_objects->account(m_transaction.splits()[0].accountId()).name(); txt = MyMoneyFile::instance()->account(d->m_split.accountId()).name(); break; default: break; } break; case 1: switch (col) { case (int)eTransaction::Column::Detail: switch (d->m_parent->getDetailsColumnType()) { case eRegister::DetailColumn::PayeeFirst: txt = d->m_category; if (!d->m_tagList.isEmpty()) { txt += " ( "; for (auto i = 0; i < d->m_tagList.size() - 1; ++i) { txt += " " + d->m_tagList[i] + ", "; } txt += " " + d->m_tagList.last() + " )"; } break; case eRegister::DetailColumn::AccountFirst: txt = d->m_payee; break; } align |= Qt::AlignLeft; if (txt.isEmpty() && !d->m_split.value().isZero()) { txt = i18n("*** UNASSIGNED ***"); if (painter) painter->setPen(KMyMoneySettings::schemeColor(SchemeColor::TransactionErroneous)); } break; default: break; } break; case 2: switch (col) { case (int)eTransaction::Column::Detail: align |= Qt::AlignLeft; singleLineMemo(txt, d->m_split); break; default: break; } break; } } int StdTransaction::registerColWidth(int col, const QFontMetrics& cellFontMetrics) { QString txt; int firstRow = 0, lastRow = numRowsRegister(); int nw = 0; for (int i = firstRow; i <= lastRow; ++i) { Qt::Alignment align; registerCellText(txt, align, i, col, 0); int w = cellFontMetrics.width(txt + " "); if (w > nw) nw = w; } return nw; } void StdTransaction::arrangeWidgetsInForm(QMap& editWidgets) { Q_D(StdTransaction); if (!d->m_form || !d->m_parent) return; setupFormPalette(editWidgets); arrangeWidget(d->m_form, 0, (int)eTransactionForm::Column::Label1, editWidgets["account-label"]); arrangeWidget(d->m_form, 0, (int)eTransactionForm::Column::Value1, editWidgets["account"]); arrangeWidget(d->m_form, 1, (int)eTransactionForm::Column::Label1, editWidgets["cashflow"]); arrangeWidget(d->m_form, 1, (int)eTransactionForm::Column::Value1, editWidgets["payee"]); arrangeWidget(d->m_form, 2, (int)eTransactionForm::Column::Label1, editWidgets["category-label"]); arrangeWidget(d->m_form, 2, (int)eTransactionForm::Column::Value1, editWidgets["category"]->parentWidget()); arrangeWidget(d->m_form, 3, (int)eTransactionForm::Column::Label1, editWidgets["tag-label"]); arrangeWidget(d->m_form, 3, (int)eTransactionForm::Column::Value1, editWidgets["tag"]); arrangeWidget(d->m_form, 4, (int)eTransactionForm::Column::Label1, editWidgets["memo-label"]); arrangeWidget(d->m_form, 4, (int)eTransactionForm::Column::Value1, editWidgets["memo"]); if (haveNumberField()) { arrangeWidget(d->m_form, 1, (int)eTransactionForm::Column::Label2, editWidgets["number-label"]); arrangeWidget(d->m_form, 1, (int)eTransactionForm::Column::Value2, editWidgets["number"]); } arrangeWidget(d->m_form, 2, (int)eTransactionForm::Column::Label2, editWidgets["date-label"]); arrangeWidget(d->m_form, 2, (int)eTransactionForm::Column::Value2, editWidgets["postdate"]); arrangeWidget(d->m_form, 3, (int)eTransactionForm::Column::Label2, editWidgets["amount-label"]); arrangeWidget(d->m_form, 3, (int)eTransactionForm::Column::Value2, editWidgets["amount"]); arrangeWidget(d->m_form, 5, (int)eTransactionForm::Column::Label2, editWidgets["status-label"]); arrangeWidget(d->m_form, 5, (int)eTransactionForm::Column::Value2, editWidgets["status"]); // get rid of the hints. we don't need them for the form QMap::iterator it; for (it = editWidgets.begin(); it != editWidgets.end(); ++it) { KMyMoneyCombo* combo = dynamic_cast(*it); KMyMoneyLineEdit* edit = dynamic_cast(*it); KMyMoneyPayeeCombo* payee = dynamic_cast(*it); KTagContainer* tag = dynamic_cast(*it); if (combo) combo->setPlaceholderText(QString()); if (edit) edit->setPlaceholderText(QString()); if (payee) payee->setPlaceholderText(QString()); if (tag) tag->tagCombo()->setPlaceholderText(QString()); } auto form = dynamic_cast(d->m_form); auto w = dynamic_cast(editWidgets["tabbar"]); if (w && form) { // insert the tabbar in the boxlayout so it will take the place of the original tabbar which was hidden if (auto boxLayout = dynamic_cast(form->getTabBar()->parentWidget()->layout())) boxLayout->insertWidget(0, w); } } void StdTransaction::tabOrderInForm(QWidgetList& tabOrderWidgets) const { Q_D(const StdTransaction); QStringList taborder = KMyMoneySettings::stdTransactionFormTabOrder().split(',', QString::SkipEmptyParts); QStringList::const_iterator it_s = taborder.constBegin(); QWidget* w; while (it_s != taborder.constEnd()) { if (*it_s == "account") { tabOrderWidgets.append(focusWidget(d->m_form->cellWidget(0, (int)eTransactionForm::Column::Value1))); } else if (*it_s == "cashflow") { tabOrderWidgets.append(focusWidget(d->m_form->cellWidget(1, (int)eTransactionForm::Column::Label1))); } else if (*it_s == "payee") { tabOrderWidgets.append(focusWidget(d->m_form->cellWidget(1, (int)eTransactionForm::Column::Value1))); } else if (*it_s == "category") { // make sure to have the category field and the split button as separate tab order widgets // ok, we have to have some internal knowledge about the KMyMoneyCategory object, but // it's one of our own widgets, so we actually don't care. Just make sure, that we don't // go haywire when someone changes the KMyMoneyCategory object ... - QWidget* w = d->m_form->cellWidget(2, (int)eTransactionForm::Column::Value1); + w = d->m_form->cellWidget(2, (int)eTransactionForm::Column::Value1); tabOrderWidgets.append(focusWidget(w)); w = w->findChild("splitButton"); if (w) tabOrderWidgets.append(w); } else if (*it_s == "tag") { tabOrderWidgets.append(focusWidget(d->m_form->cellWidget(3, (int)eTransactionForm::Column::Value1))); } else if (*it_s == "memo") { tabOrderWidgets.append(focusWidget(d->m_form->cellWidget(4, (int)eTransactionForm::Column::Value1))); } else if (*it_s == "number") { if (haveNumberField()) { if ((w = focusWidget(d->m_form->cellWidget(1, (int)eTransactionForm::Column::Value2)))) tabOrderWidgets.append(w); } } else if (*it_s == "date") { tabOrderWidgets.append(focusWidget(d->m_form->cellWidget(2, (int)eTransactionForm::Column::Value2))); } else if (*it_s == "amount") { tabOrderWidgets.append(focusWidget(d->m_form->cellWidget(3, (int)eTransactionForm::Column::Value2))); } else if (*it_s == "state") { tabOrderWidgets.append(focusWidget(d->m_form->cellWidget(5, (int)eTransactionForm::Column::Value2))); } ++it_s; } } void StdTransaction::arrangeWidgetsInRegister(QMap& editWidgets) { Q_D(StdTransaction); if (!d->m_parent) return; setupRegisterPalette(editWidgets); if (haveNumberField()) arrangeWidget(d->m_parent, d->m_startRow + 0, (int)eTransaction::Column::Number, editWidgets["number"]); arrangeWidget(d->m_parent, d->m_startRow + 0, (int)eTransaction::Column::Date, editWidgets["postdate"]); arrangeWidget(d->m_parent, d->m_startRow + 1, (int)eTransaction::Column::Date, editWidgets["status"]); arrangeWidget(d->m_parent, d->m_startRow + 0, (int)eTransaction::Column::Detail, editWidgets["payee"]); arrangeWidget(d->m_parent, d->m_startRow + 1, (int)eTransaction::Column::Detail, editWidgets["category"]->parentWidget()); arrangeWidget(d->m_parent, d->m_startRow + 2, (int)eTransaction::Column::Detail, editWidgets["tag"]); arrangeWidget(d->m_parent, d->m_startRow + 3, (int)eTransaction::Column::Detail, editWidgets["memo"]); arrangeWidget(d->m_parent, d->m_startRow + 0, (int)eTransaction::Column::Payment, editWidgets["payment"]); arrangeWidget(d->m_parent, d->m_startRow + 0, (int)eTransaction::Column::Deposit, editWidgets["deposit"]); // increase the height of the row containing the memo widget d->m_parent->setRowHeight(d->m_startRow + 3, d->m_parent->rowHeightHint() * 3); } void StdTransaction::tabOrderInRegister(QWidgetList& tabOrderWidgets) const { Q_D(const StdTransaction); QStringList taborder = KMyMoneySettings::stdTransactionRegisterTabOrder().split(',', QString::SkipEmptyParts); QStringList::const_iterator it_s = taborder.constBegin(); QWidget* w; while (it_s != taborder.constEnd()) { if (*it_s == "number") { if (haveNumberField()) { if ((w = focusWidget(d->m_parent->cellWidget(d->m_startRow + 0, (int)eTransaction::Column::Number)))) tabOrderWidgets.append(w); } } else if (*it_s == "date") { tabOrderWidgets.append(focusWidget(d->m_parent->cellWidget(d->m_startRow + 0, (int)eTransaction::Column::Date))); } else if (*it_s == "payee") { tabOrderWidgets.append(focusWidget(d->m_parent->cellWidget(d->m_startRow + 0, (int)eTransaction::Column::Detail))); } else if (*it_s == "category") { // make sure to have the category field and the split button as separate tab order widgets // ok, we have to have some internal knowledge about the KMyMoneyCategory object, but // it's one of our own widgets, so we actually don't care. Just make sure, that we don't // go haywire when someone changes the KMyMoneyCategory object ... w = d->m_parent->cellWidget(d->m_startRow + 1, (int)eTransaction::Column::Detail); tabOrderWidgets.append(focusWidget(w)); w = w->findChild("splitButton"); if (w) tabOrderWidgets.append(w); } else if (*it_s == "tag") { tabOrderWidgets.append(focusWidget(d->m_parent->cellWidget(d->m_startRow + 2, (int)eTransaction::Column::Detail))); } else if (*it_s == "memo") { tabOrderWidgets.append(focusWidget(d->m_parent->cellWidget(d->m_startRow + 3, (int)eTransaction::Column::Detail))); } else if (*it_s == "payment") { tabOrderWidgets.append(focusWidget(d->m_parent->cellWidget(d->m_startRow + 0, (int)eTransaction::Column::Payment))); } else if (*it_s == "deposit") { tabOrderWidgets.append(focusWidget(d->m_parent->cellWidget(d->m_startRow + 0, (int)eTransaction::Column::Deposit))); } else if (*it_s == "state") { tabOrderWidgets.append(focusWidget(d->m_parent->cellWidget(d->m_startRow + 1, (int)eTransaction::Column::Date))); } ++it_s; } } int StdTransaction::numRowsRegister(bool expanded) const { Q_D(const StdTransaction); int numRows = 1; if (expanded) { numRows = 4; if (!d->m_inEdit) { //When not in edit Tags haven't a separate row; numRows--; if (d->m_payee.isEmpty()) { numRows--; } if (d->m_split.memo().isEmpty()) { numRows--; } // For income and expense accounts that only have // two splits we only show one line, because the // account name is already contained in the account column. if (d->m_account.accountType() == eMyMoney::Account::Type::Income || d->m_account.accountType() == eMyMoney::Account::Type::Expense) { if (numRows > 2 && d->m_transaction.splitCount() == 2) numRows = 1; } } } return numRows; } int StdTransaction::numRowsRegister() const { return RegisterItem::numRowsRegister(); } TransactionEditor* StdTransaction::createEditor(TransactionEditorContainer* regForm, const KMyMoneyRegister::SelectedTransactions& list, const QDate& lastPostDate) { #ifndef KMM_DESIGNER Q_D(StdTransaction); d->m_inRegisterEdit = regForm == d->m_parent; return new StdTransactionEditor(regForm, this, list, lastPostDate); #else return NULL; #endif } diff --git a/kmymoney/widgets/transaction.cpp b/kmymoney/widgets/transaction.cpp index 888a29bba..f1074e760 100644 --- a/kmymoney/widgets/transaction.cpp +++ b/kmymoney/widgets/transaction.cpp @@ -1,941 +1,941 @@ /*************************************************************************** transaction.cpp - description ------------------- begin : Tue Jun 13 2006 copyright : (C) 2000-2006 by 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 "transaction.h" #include "transaction_p.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include // ---------------------------------------------------------------------------- // Project Includes #include "mymoneyutils.h" #include "mymoneytransaction.h" #include "mymoneysplit.h" #include "mymoneyfile.h" #include "mymoneysecurity.h" #include "mymoneypayee.h" #include "mymoneytag.h" #include "register.h" #include "kmymoneycategory.h" #include "kmymoneydateinput.h" #include "transactionform.h" #include "kmymoneyedit.h" #include "kmymoneyutils.h" #include "registerfilter.h" #include "tabbar.h" #include "kmymoneysettings.h" #include "widgetenums.h" #include "mymoneyenums.h" using namespace eWidgets; using namespace KMyMoneyRegister; using namespace KMyMoneyTransactionForm; static unsigned char attentionSign[] = { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x14, 0x08, 0x06, 0x00, 0x00, 0x00, 0x8D, 0x89, 0x1D, 0x0D, 0x00, 0x00, 0x00, 0x04, 0x73, 0x42, 0x49, 0x54, 0x08, 0x08, 0x08, 0x08, 0x7C, 0x08, 0x64, 0x88, 0x00, 0x00, 0x00, 0x19, 0x74, 0x45, 0x58, 0x74, 0x53, 0x6F, 0x66, 0x74, 0x77, 0x61, 0x72, 0x65, 0x00, 0x77, 0x77, 0x77, 0x2E, 0x69, 0x6E, 0x6B, 0x73, 0x63, 0x61, 0x70, 0x65, 0x2E, 0x6F, 0x72, 0x67, 0x9B, 0xEE, 0x3C, 0x1A, 0x00, 0x00, 0x02, 0x05, 0x49, 0x44, 0x41, 0x54, 0x38, 0x8D, 0xAD, 0x95, 0xBF, 0x4B, 0x5B, 0x51, 0x14, 0xC7, 0x3F, 0x2F, 0xBC, 0x97, 0x97, 0x97, 0x97, 0x77, 0xF3, 0xF2, 0x1C, 0xA4, 0x54, 0x6B, 0x70, 0x10, 0x44, 0x70, 0x2A, 0x91, 0x2E, 0x52, 0x02, 0x55, 0x8A, 0xB5, 0xA3, 0xAB, 0x38, 0x08, 0x66, 0xCC, 0xEE, 0xE0, 0xE2, 0x20, 0xB8, 0x38, 0xB8, 0xB8, 0xF8, 0x1F, 0x38, 0x29, 0xA5, 0x29, 0x74, 0x90, 0x0E, 0x0D, 0x0E, 0x22, 0x1D, 0x44, 0xA8, 0xD0, 0xD4, 0xB4, 0x58, 0x4B, 0x09, 0xF9, 0xF1, 0x4A, 0x3B, 0xD4, 0xD3, 0xE1, 0x55, 0xD3, 0x34, 0xAF, 0x49, 0x6C, 0x3D, 0xF0, 0x85, 0x7B, 0xCF, 0xFD, 0x9E, 0xEF, 0x3D, 0xE7, 0xFE, 0xD4, 0x44, 0x84, 0xDB, 0xB4, 0x48, 0x2F, 0xA4, 0x94, 0xAB, 0xE5, 0x52, 0xAE, 0x96, 0xEB, 0x49, 0x51, 0x44, 0x3A, 0x02, 0x18, 0x88, 0xC7, 0xF1, 0xE3, 0x71, 0x7C, 0x60, 0xA0, 0x1B, 0xBF, 0x6B, 0x86, 0x49, 0xC5, 0x46, 0x3E, 0x47, 0x34, 0x9F, 0x23, 0x9A, 0x54, 0x6C, 0xFC, 0x57, 0x86, 0x40, 0xC6, 0x4B, 0xE1, 0x37, 0xCA, 0x48, 0xA3, 0x8C, 0x78, 0x29, 0x7C, 0x20, 0xD3, 0x31, 0xA6, 0xD3, 0xA0, 0x52, 0x1C, 0x6D, 0x6F, 0x72, 0xD9, 0x28, 0x23, 0xFE, 0x07, 0x64, 0x7B, 0x93, 0x4B, 0xA5, 0x38, 0xFA, 0x27, 0x41, 0x60, 0x6E, 0x74, 0x84, 0x7A, 0xE5, 0x1D, 0x92, 0x54, 0x88, 0xE7, 0x22, 0xD5, 0x12, 0x32, 0x3A, 0x42, 0x1D, 0x98, 0xBB, 0x91, 0x20, 0x60, 0xDA, 0x36, 0x17, 0xFB, 0x7B, 0xC8, 0xC1, 0x4B, 0x04, 0x02, 0xBC, 0x7E, 0x81, 0xEC, 0xEF, 0x21, 0xB6, 0xCD, 0x05, 0x60, 0xF6, 0x2C, 0x68, 0x9A, 0x2C, 0xCF, 0x4C, 0xE1, 0x4B, 0x05, 0x39, 0x3F, 0x69, 0x0A, 0xBE, 0x7F, 0x83, 0x48, 0x05, 0x99, 0x99, 0xC2, 0x37, 0x4D, 0x96, 0x7B, 0x12, 0x04, 0xFA, 0x2D, 0x8B, 0xC6, 0xE9, 0x61, 0x10, 0x2C, 0x15, 0xC4, 0x8A, 0x21, 0x86, 0x8E, 0xFC, 0xF8, 0x12, 0xF4, 0x4F, 0x0F, 0x11, 0xCB, 0xA2, 0x01, 0xF4, 0x77, 0x3D, 0x36, 0x4E, 0x82, 0xF5, 0xA5, 0x05, 0x8C, 0xE1, 0x74, 0xD3, 0x37, 0x34, 0x18, 0x20, 0xF2, 0x8B, 0x3D, 0x9C, 0x86, 0xA5, 0x05, 0x0C, 0x27, 0xC1, 0x7A, 0xC7, 0x63, 0x03, 0x8C, 0x2B, 0x07, 0xBF, 0x5A, 0x6A, 0x66, 0x27, 0x15, 0x64, 0x3A, 0x8B, 0x3C, 0x7A, 0xD8, 0xEA, 0xAB, 0x96, 0x10, 0xE5, 0xE0, 0x03, 0xE3, 0x7F, 0xCD, 0x50, 0x39, 0x6C, 0xAD, 0xAD, 0x10, 0x53, 0xAA, 0x75, 0xD2, 0xF4, 0xBD, 0x00, 0x2D, 0x5C, 0x05, 0x6B, 0x2B, 0xC4, 0x94, 0xC3, 0xD6, 0xEF, 0xFE, 0x6B, 0x41, 0x4D, 0xD3, 0x66, 0xFB, 0x3C, 0xC6, 0x16, 0xE7, 0xDB, 0x97, 0x61, 0xE2, 0x3E, 0x3C, 0xC8, 0xB4, 0x15, 0xC7, 0xE2, 0x3C, 0x91, 0x3E, 0x8F, 0x31, 0x4D, 0xD3, 0x66, 0x5B, 0x4A, 0x06, 0x8C, 0x84, 0xCD, 0x59, 0x61, 0xA7, 0xB5, 0xAC, 0x2B, 0x9C, 0x1C, 0x04, 0x08, 0x1B, 0x2B, 0xEC, 0x20, 0x09, 0x9B, 0x33, 0xC0, 0xB8, 0xDE, 0x65, 0x43, 0x27, 0x9F, 0x9D, 0xA4, 0x1E, 0x16, 0xF0, 0xF9, 0x6D, 0xB0, 0xC3, 0x86, 0x1E, 0xB4, 0xC3, 0x38, 0xD9, 0x49, 0xEA, 0x86, 0x4E, 0xFE, 0xEA, 0x29, 0xF4, 0x2C, 0x8B, 0xDA, 0x71, 0x31, 0x9C, 0xFC, 0xF5, 0x23, 0x32, 0x34, 0x88, 0xDC, 0xBD, 0x13, 0x5C, 0xBF, 0x30, 0xCE, 0x71, 0x11, 0xB1, 0x2C, 0x6A, 0x80, 0xA7, 0xDB, 0x36, 0xAB, 0x4F, 0xA6, 0x89, 0xBA, 0x49, 0x38, 0xFF, 0xD4, 0xBE, 0x4E, 0x00, 0xAF, 0x9E, 0x81, 0x08, 0xD4, 0xEA, 0x01, 0xFE, 0x34, 0x37, 0x09, 0x4F, 0x1F, 0x13, 0xDD, 0x7D, 0xCE, 0xAA, 0x96, 0x72, 0x29, 0x7C, 0xFB, 0xCE, 0x44, 0xB8, 0xD4, 0xCD, 0x2C, 0x66, 0x52, 0xD4, 0x6E, 0xFB, 0x0B, 0xF8, 0x09, 0x63, 0x63, 0x31, 0xE4, 0x85, 0x76, 0x2E, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82 }; Transaction::Transaction(Register *parent, const MyMoneyTransaction& transaction, const MyMoneySplit& split, int uniqueId) : RegisterItem(*new TransactionPrivate, parent) { Q_D(Transaction); d->m_transaction = transaction; d->m_split = split; d->m_form = nullptr; d->m_uniqueId = d->m_transaction.id(); d->init(uniqueId); } Transaction::Transaction(TransactionPrivate &dd, Register* parent, const MyMoneyTransaction& transaction, const MyMoneySplit& split, int uniqueId) : RegisterItem(dd, parent) { Q_D(Transaction); d->m_form = nullptr; d->m_transaction = transaction; d->m_split = split; d->m_uniqueId = d->m_transaction.id(); d->init(uniqueId); } Transaction::~Transaction() { } const char* Transaction::className() { return "Transaction"; } bool Transaction::isSelectable() const { return true; } bool Transaction::isSelected() const { Q_D(const Transaction); return d->m_selected; } void Transaction::setSelected(bool selected) { Q_D(Transaction); if (!selected || (selected && isVisible())) d->m_selected = selected; } bool Transaction::canHaveFocus() const { return true; } bool Transaction::hasFocus() const { Q_D(const Transaction); return d->m_focus; } bool Transaction::hasEditorOpen() const { Q_D(const Transaction); return d->m_inEdit; } bool Transaction::isScheduled() const { return false; } void Transaction::setFocus(bool focus, bool updateLens) { Q_D(Transaction); if (focus != d->m_focus) { d->m_focus = focus; } if (updateLens) { if (KMyMoneySettings::ledgerLens() || !KMyMoneySettings::transactionForm() || KMyMoneySettings::showRegisterDetailed() || d->m_parent->ledgerLens()) { if (focus) setNumRowsRegister(numRowsRegister(true)); else setNumRowsRegister(numRowsRegister(KMyMoneySettings::showRegisterDetailed())); } } } bool Transaction::isErroneous() const { Q_D(const Transaction); return d->m_erroneous; } QDate Transaction::sortPostDate() const { Q_D(const Transaction); return d->m_transaction.postDate(); } int Transaction::sortSamePostDate() const { return 2; } QDate Transaction::sortEntryDate() const { Q_D(const Transaction); return d->m_transaction.entryDate(); } const QString& Transaction::sortPayee() const { Q_D(const Transaction); return d->m_payee; } const QList& Transaction::sortTagList() const { Q_D(const Transaction); return d->m_tagList; } MyMoneyMoney Transaction::sortValue() const { Q_D(const Transaction); return d->m_split.shares(); } QString Transaction::sortNumber() const { Q_D(const Transaction); return d->m_split.number(); } const QString& Transaction::sortEntryOrder() const { Q_D(const Transaction); return d->m_uniqueId; } eRegister::CashFlowDirection Transaction::sortType() const { Q_D(const Transaction); return d->m_split.shares().isNegative() ? eRegister::CashFlowDirection::Payment : eRegister::CashFlowDirection::Deposit; } const QString& Transaction::sortCategory() const { Q_D(const Transaction); return d->m_category; } eMyMoney::Split::State Transaction::sortReconcileState() const { Q_D(const Transaction); return d->m_split.reconcileFlag(); } QString Transaction::id() const { Q_D(const Transaction); return d->m_uniqueId; } const MyMoneyTransaction& Transaction::transaction() const { Q_D(const Transaction); return d->m_transaction; } const MyMoneySplit& Transaction::split() const { Q_D(const Transaction); return d->m_split; } void Transaction::setBalance(const MyMoneyMoney& balance) { Q_D(Transaction); d->m_balance = balance; } const MyMoneyMoney& Transaction::balance() const { Q_D(const Transaction); return d->m_balance; } bool Transaction::paintRegisterCellSetup(QPainter *painter, QStyleOptionViewItem &option, const QModelIndex &index) { Q_D(Transaction); Q_UNUSED(painter) if (d->m_reducedIntensity) { option.palette.setColor(QPalette::Text, option.palette.color(QPalette::Disabled, QPalette::Text)); } if (d->m_selected) { option.state |= QStyle::State_Selected; } else { option.state &= ~QStyle::State_Selected; } if (d->m_focus) { option.state |= QStyle::State_HasFocus; } else { option.state &= ~QStyle::State_HasFocus; } if (option.widget && option.widget->hasFocus()) { option.palette.setCurrentColorGroup(QPalette::Active); } else { option.palette.setCurrentColorGroup(QPalette::Inactive); } if (index.column() == 0) { option.viewItemPosition = QStyleOptionViewItem::Beginning; } else if (index.column() == (int)eTransaction::Column::LastColumn - 1) { option.viewItemPosition = QStyleOptionViewItem::End; } else { option.viewItemPosition = QStyleOptionViewItem::Middle; } // do we need to switch to the error color? if (d->m_erroneous) { option.palette.setColor(QPalette::Text, KMyMoneySettings::schemeColor(SchemeColor::TransactionErroneous)); } // do we need to switch to the negative balance color? if (index.column() == (int)eTransaction::Column::Balance) { bool showNegative = d->m_balance.isNegative(); if (d->m_account.accountGroup() == eMyMoney::Account::Type::Liability && !d->m_balance.isZero()) showNegative = !showNegative; if (showNegative) option.palette.setColor(QPalette::Text, KMyMoneySettings::schemeColor(SchemeColor::TransactionErroneous)); } return true; } void Transaction::registerCellText(QString& txt, int row, int col) { Qt::Alignment align; registerCellText(txt, align, row, col, 0); } void Transaction::paintRegisterCell(QPainter *painter, QStyleOptionViewItem &option, const QModelIndex &index) { Q_D(Transaction); painter->save(); if (paintRegisterCellSetup(painter, option, index)) { const QStyle *style = option.widget ? option.widget->style() : QApplication::style(); const QWidget* widget = option.widget; // clear the mouse over state before painting the background option.state &= ~QStyle::State_MouseOver; // the background if (option.state & QStyle::State_Selected || option.state & QStyle::State_HasFocus) { // if this is not the first row of the transaction paint the previous rows // since the selection background is painted from the first row of the transaction if (index.row() > startRow()) { QStyleOptionViewItem optionSibling = option; QModelIndex previousRowItem = index.sibling(index.row() - 1, index.column()); optionSibling.rect = d->m_parent->visualRect(previousRowItem); paintRegisterCell(painter, optionSibling, previousRowItem); } // paint the selection background only from the first row on to the last row at once if (index.row() == startRow()) { QRect old = option.rect; int extraHeight = 0; if (d->m_inRegisterEdit) { // since, when editing a transaction inside the register (without the transaction form), // row heights can have various sizes (the memo row is larger than the rest) we have // to iterate over all the items of the transaction to compute the size of the selection rectangle // of course we start with the item after this one because it's size is already in the rectangle for (int i = startRow() + 1; i < startRow() + numRowsRegister(); ++i) { extraHeight += d->m_parent->visualRect(index.sibling(i, index.column())).height(); } } else { // we are not editing in the register so all rows have the same sizes just compute the extra height extraHeight = (numRowsRegister() - 1) * option.rect.height(); } option.rect.setBottom(option.rect.bottom() + extraHeight); style->drawPrimitive(QStyle::PE_PanelItemViewItem, &option, painter, widget); if (d->m_focus && index.column() == (int)eTransaction::Column::Detail) { option.state |= QStyle::State_HasFocus; style->drawPrimitive(QStyle::PE_FrameFocusRect, &option, painter, widget); } option.rect = old; } } else { if (d->m_alternate) { painter->fillRect(option.rect, option.palette.alternateBase()); } else { painter->fillRect(option.rect, option.palette.base()); } } // the text // construct the text for the cell QString txt; option.displayAlignment = Qt::AlignVCenter; if (d->m_transaction != MyMoneyTransaction() && !d->m_inRegisterEdit) { registerCellText(txt, option.displayAlignment, index.row() - startRow(), index.column(), painter); } if (Qt::mightBeRichText(txt)) { QTextDocument document; // this should set the alignment of the html but it does not work so htmls will be left aligned document.setDefaultTextOption(QTextOption(option.displayAlignment)); document.setDocumentMargin(2); document.setHtml(txt); painter->translate(option.rect.topLeft()); QAbstractTextDocumentLayout::PaintContext ctx; ctx.palette = option.palette; // Highlighting text if item is selected if (d->m_selected) ctx.palette.setColor(QPalette::Text, option.palette.color(QPalette::HighlightedText)); document.documentLayout()->draw(painter, ctx); painter->translate(-option.rect.topLeft()); } else { // draw plain text properly aligned style->drawItemText(painter, option.rect.adjusted(2, 0, -2, 0), option.displayAlignment, option.palette, true, txt, d->m_selected ? QPalette::HighlightedText : QPalette::Text); } // draw the grid if it's needed if (KMyMoneySettings::showGrid()) { const int gridHint = style->styleHint(QStyle::SH_Table_GridLineColor, &option, widget); const QPen gridPen = QPen(QColor(static_cast(gridHint)), 0); QPen old = painter->pen(); painter->setPen(gridPen); if (index.row() == startRow()) painter->drawLine(option.rect.topLeft(), option.rect.topRight()); painter->drawLine(option.rect.topLeft(), option.rect.bottomLeft()); painter->setPen(old); } // possible icons if (index.row() == startRow() && index.column() == (int)eTransaction::Column::Detail) { if (d->m_erroneous) { QPixmap attention; attention.loadFromData(attentionSign, sizeof(attentionSign), 0, 0); style->drawItemPixmap(painter, option.rect, Qt::AlignRight | Qt::AlignVCenter, attention); } } } painter->restore(); } bool Transaction::formCellText(QString& /* txt */, Qt::Alignment& /* align */, int /* row */, int /* col */, QPainter* /* painter */) { return false; } void Transaction::registerCellText(QString& /* txt */, Qt::Alignment& /* align */, int /* row */, int /* col */, QPainter* /* painter */) { } int Transaction::registerColWidth(int /* col */, const QFontMetrics& /* cellFontMetrics */) { return 0; } int Transaction::formRowHeight(int /*row*/) { Q_D(Transaction); if (d->m_formRowHeight < 0) { d->m_formRowHeight = formRowHeight(); } return d->m_formRowHeight; } int Transaction::formRowHeight() const { Q_D(const Transaction); if (d->m_formRowHeight < 0) { // determine the height of the objects in the table KMyMoneyDateInput dateInput; KMyMoneyCategory category(true, nullptr); return qMax(dateInput.sizeHint().height(), category.sizeHint().height()); } return d->m_formRowHeight; } void Transaction::setupForm(TransactionForm* form) { Q_D(Transaction); d->m_form = form; form->verticalHeader()->setUpdatesEnabled(false); form->horizontalHeader()->setUpdatesEnabled(false); form->setRowCount(numRowsForm()); form->setColumnCount(numColsForm()); // Force all cells to have some text (so that paintCell is called for each cell) for (int r = 0; r < numRowsForm(); ++r) { for (int c = 0; c < numColsForm(); ++c) { if (r == 0 && form->columnWidth(c) == 0) { form->setColumnWidth(c, 10); } } } form->horizontalHeader()->setUpdatesEnabled(true); form->verticalHeader()->setUpdatesEnabled(true); loadTab(form); } void Transaction::paintFormCell(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) { Q_D(Transaction); if (!d->m_form) return; QRect cellRect = option.rect; QRect textRect(cellRect); textRect.setWidth(textRect.width() - 2); textRect.setHeight(textRect.height() - 2); painter->setPen(option.palette.text().color()); QString txt; Qt::Alignment align = Qt::AlignVCenter; bool editField = formCellText(txt, align, index.row(), index.column(), painter); // if we have an editable field and don't currently edit the transaction // show the background in a different color if (editField && !d->m_inEdit) { painter->fillRect(textRect, option.palette.alternateBase()); } if (!d->m_inEdit) painter->drawText(textRect, align, txt); } void Transaction::setupPalette(const QPalette& palette, QMap& editWidgets) { QMap::iterator it_w; for (it_w = editWidgets.begin(); it_w != editWidgets.end(); ++it_w) { if (*it_w) { (*it_w)->setPalette(palette); } } } void Transaction::setupFormPalette(QMap& editWidgets) { Q_D(Transaction); QPalette palette = d->m_parent->palette(); palette.setColor(QPalette::Active, QPalette::Base, palette.color(QPalette::Active, QPalette::Base)); setupPalette(palette, editWidgets); } void Transaction::setupRegisterPalette(QMap& editWidgets) { Q_D(Transaction); // make sure, we're using the right palette QPalette palette = d->m_parent->palette(); // use the highlight color as background palette.setColor(QPalette::Active, QPalette::Background, palette.color(QPalette::Active, QPalette::Highlight)); setupPalette(palette, editWidgets); } QWidget* Transaction::focusWidget(QWidget* w) const { if (w) { while (w->focusProxy()) w = w->focusProxy(); } return w; } void Transaction::arrangeWidget(QTableWidget* tbl, int row, int col, QWidget* w) const { if (w) { tbl->setCellWidget(row, col, w); // remove the widget from the QTable's eventFilter so that all // events will be directed to the edit widget w->removeEventFilter(tbl); } } bool Transaction::haveNumberField() const { Q_D(const Transaction); auto rc = true; switch (d->m_account.accountType()) { case eMyMoney::Account::Type::Savings: case eMyMoney::Account::Type::Cash: case eMyMoney::Account::Type::Loan: case eMyMoney::Account::Type::AssetLoan: case eMyMoney::Account::Type::Asset: case eMyMoney::Account::Type::Liability: case eMyMoney::Account::Type::Equity: rc = KMyMoneySettings::alwaysShowNrField(); break; case eMyMoney::Account::Type::Checkings: case eMyMoney::Account::Type::CreditCard: // the next case is used for the editor when the account // is unknown (eg. when creating new schedules) case eMyMoney::Account::Type::Unknown: break; default: rc = false; break; } return rc; } bool Transaction::maybeTip(const QPoint& cpos, int row, int col, QRect& r, QString& msg) { Q_D(Transaction); if (col != (int)eTransaction::Column::Detail) return false; if (!d->m_erroneous && d->m_transaction.splitCount() < 3) return false; // check for detail column in row 0 of the transaction for a possible // exclamation mark. m_startRow is based 0, whereas the row to obtain // the modelindex is based 1, so we need to add one here r = d->m_parent->visualRect(d->m_parent->model()->index(d->m_startRow + 1, col)); r.setBottom(r.bottom() + (numRowsRegister() - 1)*r.height()); if (r.contains(cpos) && d->m_erroneous) { if (d->m_transaction.splits().count() < 2) { msg = QString("%1").arg(i18n("Transaction is missing a category assignment.")); } else { const auto sec = MyMoneyFile::instance()->security(d->m_account.currencyId()); msg = QString("%1").arg(i18n("The transaction has a missing assignment of %1.", MyMoneyUtils::formatMoney(d->m_transaction.splitSum().abs(), d->m_account, sec))); } return true; } // check if the mouse cursor is located on row 1 of the transaction // and display the details of a split transaction if it is one if (row == 1 && r.contains(cpos) && d->m_transaction.splitCount() > 2) { auto file = MyMoneyFile::instance(); QString txt; const auto sec = file->security(d->m_transaction.commodity()); MyMoneyMoney factor(1, 1); if (!d->m_split.value().isNegative()) factor = -factor; foreach (const auto split, d->m_transaction.splits()) { if (split == d->m_split) continue; const MyMoneyAccount& acc = file->account(split.accountId()); auto category = file->accountToCategory(acc.id()); auto amount = MyMoneyUtils::formatMoney((split.value() * factor), acc, sec); txt += QString("%1%2").arg(category, amount); } msg = QString("%1
").arg(txt); return true; } return false; } QString Transaction::reconcileState(bool text) const { Q_D(const Transaction); auto txt = KMyMoneyUtils::reconcileStateToString(d->m_split.reconcileFlag(), text); if ((text == true) && (txt == i18nc("Unknown reconciliation state", "Unknown")) && (d->m_transaction == MyMoneyTransaction())) txt.clear(); return txt; } void Transaction::startEditMode() { Q_D(Transaction); d->m_inEdit = true; // hide the original tabbar since the edit tabbar will be added if (auto form = dynamic_cast(d->m_form)) form->getTabBar()->setVisible(false); // only update the number of lines displayed if we edit inside the register if (d->m_inRegisterEdit) setNumRowsRegister(numRowsRegister(true)); } int Transaction::numRowsRegister() const { return RegisterItem::numRowsRegister(); } void Transaction::leaveEditMode() { Q_D(Transaction); // show the original tabbar since the edit tabbar was removed if (auto form = dynamic_cast(d->m_form)) form->getTabBar()->setVisible(true); // make sure we reset the row height of all the transaction's rows because it could have been changed during edit if (d->m_parent) { for (auto i = 0; i < numRowsRegister(); ++i) d->m_parent->setRowHeight(d->m_startRow + i, d->m_parent->rowHeightHint()); } d->m_inEdit = false; d->m_inRegisterEdit = false; setFocus(hasFocus(), true); } void Transaction::singleLineMemo(QString& txt, const MyMoneySplit& split) const { txt = split.memo(); // remove empty lines txt.replace("\n\n", "\n"); // replace '\n' with ", " txt.replace('\n', ", "); } int Transaction::rowHeightHint() const { Q_D(const Transaction); return d->m_inEdit ? formRowHeight() : RegisterItem::rowHeightHint(); } bool Transaction::matches(const RegisterFilter& filter) const { Q_D(const Transaction); // check if the state matches if (!transaction().id().isEmpty()) { switch (filter.state) { default: break; case eRegister::ItemState::Imported: if (!transaction().isImported()) return false; break; case eRegister::ItemState::Matched: if (!split().isMatched()) return false; break; case eRegister::ItemState::Erroneous: if (transaction().splitSum().isZero()) return false; break; case eRegister::ItemState::NotMarked: if (split().reconcileFlag() != eMyMoney::Split::State::NotReconciled) return false; break; case eRegister::ItemState::NotReconciled: if (split().reconcileFlag() != eMyMoney::Split::State::NotReconciled && split().reconcileFlag() != eMyMoney::Split::State::Cleared) return false; break; case eRegister::ItemState::Cleared: if (split().reconcileFlag() != eMyMoney::Split::State::Cleared) return false; break; } } // check if the text matches if (filter.text.isEmpty() || d->m_transaction.splitCount() == 0) return true; auto file = MyMoneyFile::instance(); foreach (const auto split, d->m_transaction.splits()) { // check if the text is contained in one of the fields // memo, number, payee, tag, account if (split.memo().contains(filter.text, Qt::CaseInsensitive) || split.number().contains(filter.text, Qt::CaseInsensitive)) return true; if (!split.payeeId().isEmpty()) { const MyMoneyPayee& payee = file->payee(split.payeeId()); if (payee.name().contains(filter.text, Qt::CaseInsensitive)) return true; } if (!split.tagIdList().isEmpty()) { const QList& t = split.tagIdList(); for (auto i = 0; i < t.count(); ++i) { if ((file->tag(t[i])).name().contains(filter.text, Qt::CaseInsensitive)) return true; } } const MyMoneyAccount& acc = file->account(split.accountId()); // search for account hierachy if (filter.text.contains(MyMoneyFile::AccountSeparator)) { QStringList names; MyMoneyAccount current = acc; QString accountId; do { names.prepend(current.name()); accountId = current.parentAccountId(); current = file->account(accountId); } while (current.accountType() != eMyMoney::Account::Type::Unknown && !MyMoneyFile::instance()->isStandardAccount(accountId)); if (names.size() > 1 && names.join(MyMoneyFile::AccountSeparator).contains(filter.text, Qt::CaseInsensitive)) return true; } if (acc.name().contains(filter.text, Qt::CaseInsensitive)) return true; QString s(filter.text); s.replace(MyMoneyMoney::thousandSeparator(), QChar()); if (!s.isEmpty()) { // check if any of the value field matches if a value has been entered QString r = split.value().formatMoney(d->m_account.fraction(), false); if (r.contains(s, Qt::CaseInsensitive)) return true; - const MyMoneyAccount& acc = file->account(split.accountId()); - r = split.shares().formatMoney(acc.fraction(), false); + const MyMoneyAccount& splitAcc = file->account(split.accountId()); + r = split.shares().formatMoney(splitAcc.fraction(), false); if (r.contains(s, Qt::CaseInsensitive)) return true; } } return false; } void Transaction::setShowBalance(bool showBalance) { Q_D(Transaction); d->m_showBalance = showBalance; } bool Transaction::showRowInForm(int row) const { Q_UNUSED(row) return true; } void Transaction::setShowRowInForm(int row, bool show) { Q_UNUSED(row); Q_UNUSED(show) } void Transaction::setReducedIntensity(bool reduced) { Q_D(Transaction); d->m_reducedIntensity = reduced; } void Transaction::setVisible(bool visible) { Q_D(Transaction); if (visible != isVisible()) { RegisterItem::setVisible(visible); RegisterItem* p; Transaction* t; if (!visible) { // if we are hidden, we need to inform all previous transactions // about it so that they don't show the balance p = prevItem(); while (p) { if ((t = dynamic_cast(p))) { if (!t->d_func()->m_showBalance) break; t->d_func()->m_showBalance = false; } p = p->prevItem(); } } else { // if we are shown, we need to check if the next transaction // is visible and change the display of the balance p = this; do { p = p->nextItem(); t = dynamic_cast(p); } while (!t && p); // if the next transaction is visible or I am the last one if ((t && t->d_func()->m_showBalance) || !t) { d->m_showBalance = true; p = prevItem(); while (p && p->isVisible()) { if ((t = dynamic_cast(p))) { if (t->d_func()->m_showBalance) break; t->d_func()->m_showBalance = true; } p = p->prevItem(); } } } } } diff --git a/kmymoney/widgets/transactioneditorcontainer.h b/kmymoney/widgets/transactioneditorcontainer.h index 879496c14..0388bc488 100644 --- a/kmymoney/widgets/transactioneditorcontainer.h +++ b/kmymoney/widgets/transactioneditorcontainer.h @@ -1,58 +1,58 @@ /*************************************************************************** transactioneditorcontainer.h ---------- begin : Wed Jun 07 2006 copyright : (C) 2006 by Thomas Baumgart email : Thomas Baumgart (C) 2017 by Łukasz Wojniłowicz ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #ifndef TRANSACTIONEDITORCONTAINER_H #define TRANSACTIONEDITORCONTAINER_H // ---------------------------------------------------------------------------- // QT Includes #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes namespace KMyMoneyRegister { class Transaction; } class TransactionEditorContainer : public QTableWidget { Q_OBJECT Q_DISABLE_COPY(TransactionEditorContainer) public: explicit TransactionEditorContainer(QWidget* parent); virtual ~TransactionEditorContainer(); virtual void arrangeEditWidgets(QMap& editWidgets, KMyMoneyRegister::Transaction* t) = 0; virtual void removeEditWidgets(QMap& editWidgets) = 0; virtual void tabOrder(QWidgetList& tabOrderWidgets, KMyMoneyRegister::Transaction* t) const = 0; // FIXME remove tabbar // virtual int action(QMap& editWidgets) const = 0; // virtual void setProtectedAction(QMap& editWidgets, ProtectedAction action) = 0; Q_SIGNALS: void geometriesUpdated(); protected Q_SLOTS: - void updateGeometries(); + void updateGeometries() final override; }; #endif diff --git a/kmymoney/wizards/endingbalancedlg/interestchargecheckingswizardpage.h b/kmymoney/wizards/endingbalancedlg/interestchargecheckingswizardpage.h index f693658e3..33ff12d8a 100644 --- a/kmymoney/wizards/endingbalancedlg/interestchargecheckingswizardpage.h +++ b/kmymoney/wizards/endingbalancedlg/interestchargecheckingswizardpage.h @@ -1,56 +1,56 @@ /*************************************************************************** interestchargecheckingswizardpage.h - description ------------------- begin : Sun Jul 18 2010 copyright : (C) 2010 by Fernando Vilas email : kmymoney-devel@kde.org ***************************************************************************/ /*************************************************************************** * * * 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 INTERESTCHARGECHECKINGSWIZARDPAGE_H #define INTERESTCHARGECHECKINGSWIZARDPAGE_H // ---------------------------------------------------------------------------- // QT Includes #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes namespace Ui { class InterestChargeCheckingsWizardPage; } /** * This class implements the InterestChargeCheckings page of the * @ref KEndingBalanceDlg wizard. */ class InterestChargeCheckingsWizardPage : public QWizardPage { Q_OBJECT Q_DISABLE_COPY(InterestChargeCheckingsWizardPage) public: explicit InterestChargeCheckingsWizardPage(QWidget *parent = nullptr); - ~InterestChargeCheckingsWizardPage(); + ~InterestChargeCheckingsWizardPage() override; /** * Overload the isComplete function to control the Next button */ - bool isComplete() const; + bool isComplete() const final override; Ui::InterestChargeCheckingsWizardPage *ui; }; #endif diff --git a/kmymoney/wizards/endingbalancedlg/kendingbalancedlg.h b/kmymoney/wizards/endingbalancedlg/kendingbalancedlg.h index 9edfc0fa3..461f9b3db 100644 --- a/kmymoney/wizards/endingbalancedlg/kendingbalancedlg.h +++ b/kmymoney/wizards/endingbalancedlg/kendingbalancedlg.h @@ -1,107 +1,107 @@ /*************************************************************************** kendingbalancedlg.h ------------------- copyright : (C) 2000 by Michael Edwardes email : mte@users.sourceforge.net ***************************************************************************/ /*************************************************************************** * * * 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 KENDINGBALANCEDLG_H #define KENDINGBALANCEDLG_H // ---------------------------------------------------------------------------- // QT Includes #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes class QDate; class MyMoneyMoney; class MyMoneyAccount; class MyMoneyTransaction; /** * This dialog is wizard based and used to enter additional * information required to start the reconciliation process. * This version implements the behaviour for checkings, * savings and credit card accounts. * * @author Thomas Baumgart */ class KEndingBalanceDlgPrivate; class KEndingBalanceDlg : public QWizard { Q_OBJECT Q_DISABLE_COPY(KEndingBalanceDlg) public: enum { Page_CheckingStart, Page_PreviousPostpone, Page_CheckingStatementInfo, Page_InterestChargeCheckings }; explicit KEndingBalanceDlg(const MyMoneyAccount& account, QWidget *parent = nullptr); ~KEndingBalanceDlg(); MyMoneyMoney endingBalance() const; MyMoneyMoney previousBalance() const; QDate statementDate() const; MyMoneyTransaction interestTransaction(); MyMoneyTransaction chargeTransaction(); /** * This method returns the id of the next page in the wizard. * It is overloaded here to support the dynamic nature of this wizard. * * @return id of the next page or -1 if there is no next page */ - int nextId() const; + int nextId() const final override; protected: bool createTransaction(MyMoneyTransaction& t, const int sign, const MyMoneyMoney& amount, const QString& category, const QDate& date); MyMoneyMoney adjustedReturnValue(const MyMoneyMoney& v) const; void createCategory(const QString& txt, QString& id, const MyMoneyAccount& parent); protected Q_SLOTS: void slotReloadEditWidgets(); void help(); void slotCreateInterestCategory(const QString& txt, QString& id); void slotCreateChargesCategory(const QString& txt, QString& id); - void accept(); + void accept() final override; void slotUpdateBalances(); Q_SIGNALS: /** * proxy signal for KMyMoneyPayeeCombo::createItem(const QString&, QString&) */ void createPayee(const QString&, QString&); /** * emit when a category is about to be created */ void createCategory(MyMoneyAccount& acc, const MyMoneyAccount& parent); private: KEndingBalanceDlgPrivate * const d_ptr; Q_DECLARE_PRIVATE(KEndingBalanceDlg) private Q_SLOTS: void slotNewPayee(const QString& newnameBase, QString& id); }; #endif diff --git a/kmymoney/wizards/newaccountwizard/khierarchypage.cpp b/kmymoney/wizards/newaccountwizard/khierarchypage.cpp index 3d3202e75..d9c83ae73 100644 --- a/kmymoney/wizards/newaccountwizard/khierarchypage.cpp +++ b/kmymoney/wizards/newaccountwizard/khierarchypage.cpp @@ -1,126 +1,126 @@ /*************************************************************************** khierarchypage.cpp ------------------- begin : Tue Sep 25 2006 copyright : (C) 2007 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 "khierarchypage.h" #include "khierarchypage_p.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes #include "ui_khierarchypage.h" #include "accountsmodel.h" #include "hierarchyfilterproxymodel.h" #include "kmymoneyaccounttreeview.h" #include "kmymoneysettings.h" #include "knewaccountwizard.h" #include "knewaccountwizard_p.h" #include "kaccountsummarypage.h" #include "kaccounttypepage.h" #include "modelenums.h" #include "models.h" #include "mymoneyaccount.h" #include "mymoneyenums.h" #include "wizardpage.h" using namespace eMyMoney; namespace NewAccountWizard { HierarchyPage::HierarchyPage(Wizard* wizard) : QWidget(wizard), WizardPage(*new HierarchyPagePrivate(wizard), StepParentAccount, this, wizard) { Q_D(HierarchyPage); d->ui->setupUi(this); d->m_filterProxyModel = nullptr; // the proxy filter model d->m_filterProxyModel = new HierarchyFilterProxyModel(this); d->m_filterProxyModel->setHideClosedAccounts(true); d->m_filterProxyModel->setHideEquityAccounts(!KMyMoneySettings::expertMode()); d->m_filterProxyModel->addAccountGroup(QVector {Account::Type::Asset, Account::Type::Liability}); auto const model = Models::instance()->accountsModel(); d->m_filterProxyModel->setSourceModel(model); d->m_filterProxyModel->setSourceColumns(model->getColumns()); d->m_filterProxyModel->setDynamicSortFilter(true); d->ui->m_parentAccounts->setModel(d->m_filterProxyModel); d->ui->m_parentAccounts->sortByColumn((int)eAccountsModel::Column::Account, Qt::AscendingOrder); connect(d->ui->m_parentAccounts->selectionModel(), &QItemSelectionModel::currentChanged, this, &HierarchyPage::parentAccountChanged); } HierarchyPage::~HierarchyPage() { } void HierarchyPage::enterPage() { Q_D(HierarchyPage); // Ensure that the list reflects the Account Type MyMoneyAccount topAccount = d->m_wizard->d_func()->m_accountTypePage->parentAccount(); d->m_filterProxyModel->clear(); d->m_filterProxyModel->addAccountGroup(QVector {topAccount.accountGroup()}); d->ui->m_parentAccounts->expandAll(); } KMyMoneyWizardPage* HierarchyPage::nextPage() const { Q_D(const HierarchyPage); return d->m_wizard->d_func()->m_accountSummaryPage; } QWidget* HierarchyPage::initialFocusWidget() const { Q_D(const HierarchyPage); return d->ui->m_parentAccounts; } const MyMoneyAccount& HierarchyPage::parentAccount() { Q_D(HierarchyPage); - QVariant data = d->ui->m_parentAccounts->model()->data(d->ui->m_parentAccounts->currentIndex(), (int)eAccountsModel::Role::Account); - if (data.isValid()) { - d->m_parentAccount = data.value(); + auto dataVariant = d->ui->m_parentAccounts->model()->data(d->ui->m_parentAccounts->currentIndex(), (int)eAccountsModel::Role::Account); + if (dataVariant.isValid()) { + d->m_parentAccount = dataVariant.value(); } else { d->m_parentAccount = MyMoneyAccount(); } return d->m_parentAccount; } bool HierarchyPage::isComplete() const { Q_D(const HierarchyPage); return d->ui->m_parentAccounts->currentIndex().isValid(); } void HierarchyPage::parentAccountChanged() { completeStateChanged(); } } diff --git a/kmymoney/wizards/newinvestmentwizard/kinvestmentdetailswizardpage.h b/kmymoney/wizards/newinvestmentwizard/kinvestmentdetailswizardpage.h index 031503074..253a46659 100644 --- a/kmymoney/wizards/newinvestmentwizard/kinvestmentdetailswizardpage.h +++ b/kmymoney/wizards/newinvestmentwizard/kinvestmentdetailswizardpage.h @@ -1,78 +1,78 @@ /*************************************************************************** kinvestmentdetailswizardpage - description ------------------- begin : Sun Jun 27 2010 copyright : (C) 2010 by Fernando Vilas email : kmymoney-devel@kde.org (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 KINVESTMENTDETAILSWIZARDPAGE_H #define KINVESTMENTDETAILSWIZARDPAGE_H // ---------------------------------------------------------------------------- // QT Includes #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes class MyMoneySecurity; namespace Ui { class KInvestmentDetailsWizardPage; } /** * This class implements the investment details page of the * @ref KNewInvestmentWizard. */ class KInvestmentDetailsWizardPage : public QWizardPage { Q_OBJECT public: explicit KInvestmentDetailsWizardPage(QWidget *parent = nullptr); ~KInvestmentDetailsWizardPage(); void init2(const MyMoneySecurity& security); /** * Overload isComplete to handle the required fields */ - bool isComplete() const; + bool isComplete() const final override; /** * Functions to control or read the m_priceMode widget */ int priceMode() const; void setCurrentPriceMode(int mode); void setPriceModeEnabled(bool enabled); /** * load or set the name of the m_investmentName item widget. The difference * can be seen in the @ref KMyMoneyLineEdit type. */ void loadName(const QString& name); void setName(const QString& name); void setupInvestmentSymbol(); Q_SIGNALS: void checkForExistingSymbol(const QString& symbol); private: Ui::KInvestmentDetailsWizardPage *ui; }; #endif diff --git a/kmymoney/wizards/newinvestmentwizard/konlineupdatewizardpage.h b/kmymoney/wizards/newinvestmentwizard/konlineupdatewizardpage.h index e59a10405..97835e03c 100644 --- a/kmymoney/wizards/newinvestmentwizard/konlineupdatewizardpage.h +++ b/kmymoney/wizards/newinvestmentwizard/konlineupdatewizardpage.h @@ -1,67 +1,67 @@ /*************************************************************************** konlineupdatewizardpage - description ------------------- begin : Sun Jun 27 2010 copyright : (C) 2010 by Fernando Vilas email : kmymoney-devel@kde.org (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 KONLINEUPDATEWIZARDPAGE_H #define KONLINEUPDATEWIZARDPAGE_H // ---------------------------------------------------------------------------- // QT Includes #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes class MyMoneySecurity; namespace Ui { class KOnlineUpdateWizardPage; } /** * This class implements the Online Update page of the * @ref KNewInvestmentWizard. */ class KOnlineUpdateWizardPage : public QWizardPage { Q_OBJECT public: explicit KOnlineUpdateWizardPage(QWidget *parent = nullptr); ~KOnlineUpdateWizardPage(); /** * Overload the isComplete function to control the Next button */ - bool isComplete() const; + bool isComplete() const final override; void init2(const MyMoneySecurity& security); /** * Return whether the m_onlineFactor control is enabled */ bool isOnlineFactorEnabled() const; public Q_SLOTS: void slotCheckPage(const QString&); void slotSourceChanged(bool); private: Ui::KOnlineUpdateWizardPage *ui; }; #endif diff --git a/kmymoney/wizards/newloanwizard/assetaccountwizardpage.h b/kmymoney/wizards/newloanwizard/assetaccountwizardpage.h index d702c37d3..ff83d7593 100644 --- a/kmymoney/wizards/newloanwizard/assetaccountwizardpage.h +++ b/kmymoney/wizards/newloanwizard/assetaccountwizardpage.h @@ -1,57 +1,57 @@ /*************************************************************************** assetaccountwizardpage - description ------------------- begin : Sun Jul 4 2010 copyright : (C) 2010 by Fernando Vilas email : kmymoney-devel@kde.org ***************************************************************************/ /*************************************************************************** * * * 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 ASSETACCOUNTWIZARDPAGE_H #define ASSETACCOUNTWIZARDPAGE_H // ---------------------------------------------------------------------------- // QT Includes #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes namespace Ui { class AssetAccountWizardPage; } /** * This class implements the Online Update page of the * @ref KNewInvestmentWizard. */ class AssetAccountWizardPage : public QWizardPage { Q_OBJECT public: explicit AssetAccountWizardPage(QWidget *parent = nullptr); ~AssetAccountWizardPage(); /** * Overload the isComplete function to control the Next button */ - bool isComplete() const; + bool isComplete() const final override; Ui::AssetAccountWizardPage *ui; public Q_SLOTS: void slotAccountNew(); }; #endif diff --git a/kmymoney/wizards/newloanwizard/effectivedatewizardpage.h b/kmymoney/wizards/newloanwizard/effectivedatewizardpage.h index 899ac9711..609bf4fa4 100644 --- a/kmymoney/wizards/newloanwizard/effectivedatewizardpage.h +++ b/kmymoney/wizards/newloanwizard/effectivedatewizardpage.h @@ -1,60 +1,60 @@ /*************************************************************************** effectivedatewizardpage - description ------------------- begin : Sun Jul 4 2010 copyright : (C) 2010 by Fernando Vilas email : kmymoney-devel@kde.org ***************************************************************************/ /*************************************************************************** * * * 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 EFFECTIVEDATEWIZARDPAGE_H #define EFFECTIVEDATEWIZARDPAGE_H // ---------------------------------------------------------------------------- // QT Includes #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes namespace Ui { class EffectiveDateWizardPage; } /** * This class implements the Effective Date page of the * @ref KNewLoanWizard. */ class EffectiveDateWizardPage : public QWizardPage { Q_OBJECT public: explicit EffectiveDateWizardPage(QWidget *parent = nullptr); ~EffectiveDateWizardPage(); /** * Overload the isComplete function to control the Next button */ - bool isComplete() const; + bool isComplete() const final override; /** * Overload the initializePage function to set widgets based on * the inputs from previous pages. */ - void initializePage(); + void initializePage() final override; Ui::EffectiveDateWizardPage *ui; }; #endif diff --git a/kmymoney/wizards/newloanwizard/firstpaymentwizardpage.h b/kmymoney/wizards/newloanwizard/firstpaymentwizardpage.h index 7a670fdec..68dec9a36 100644 --- a/kmymoney/wizards/newloanwizard/firstpaymentwizardpage.h +++ b/kmymoney/wizards/newloanwizard/firstpaymentwizardpage.h @@ -1,59 +1,59 @@ /*************************************************************************** firstpaymentwizardpage - description ------------------- begin : Sun Jul 4 2010 copyright : (C) 2010 by Fernando Vilas email : kmymoney-devel@kde.org ***************************************************************************/ /*************************************************************************** * * * 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 FIRSTPAYMENTWIZARDPAGE_H #define FIRSTPAYMENTWIZARDPAGE_H // ---------------------------------------------------------------------------- // QT Includes #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes namespace Ui { class FirstPaymentWizardPage; } /** * This class implements the Online Update page of the * @ref KNewInvestmentWizard. */ class FirstPaymentWizardPage : public QWizardPage { Q_OBJECT public: explicit FirstPaymentWizardPage(QWidget *parent = nullptr); ~FirstPaymentWizardPage(); /** * Overload the isComplete function to control the Next button */ - bool isComplete() const; + bool isComplete() const final override; /** * Overload the initializePage function to set widgets based on * the inputs from previous pages. */ - void initializePage(); + void initializePage() final override; Ui::FirstPaymentWizardPage *ui; }; #endif diff --git a/kmymoney/wizards/newloanwizard/interestcategorywizardpage.h b/kmymoney/wizards/newloanwizard/interestcategorywizardpage.h index 8ffeb8e06..ca9f82db6 100644 --- a/kmymoney/wizards/newloanwizard/interestcategorywizardpage.h +++ b/kmymoney/wizards/newloanwizard/interestcategorywizardpage.h @@ -1,57 +1,57 @@ /*************************************************************************** interestcategorywizardpage - description ------------------- begin : Sun Jul 4 2010 copyright : (C) 2010 by Fernando Vilas email : kmymoney-devel@kde.org ***************************************************************************/ /*************************************************************************** * * * 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 INTERESTCATEGORYWIZARDPAGE_H #define INTERESTCATEGORYWIZARDPAGE_H // ---------------------------------------------------------------------------- // QT Includes #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes namespace Ui { class InterestCategoryWizardPage; } /** * This class implements the Interest Category page of the * @ref KNewLoanWizard. */ class InterestCategoryWizardPage : public QWizardPage { Q_OBJECT public: explicit InterestCategoryWizardPage(QWidget *parent = nullptr); ~InterestCategoryWizardPage(); /** * Overload the isComplete function to control the Next button */ - bool isComplete() const; + bool isComplete() const final override; Ui::InterestCategoryWizardPage *ui; protected Q_SLOTS: void slotCreateCategory(); }; #endif diff --git a/kmymoney/wizards/newloanwizard/interesteditwizardpage.h b/kmymoney/wizards/newloanwizard/interesteditwizardpage.h index 3c4f51327..ac5e61afd 100644 --- a/kmymoney/wizards/newloanwizard/interesteditwizardpage.h +++ b/kmymoney/wizards/newloanwizard/interesteditwizardpage.h @@ -1,54 +1,54 @@ /*************************************************************************** interesteditwizardpage - description ------------------- begin : Sun Jul 4 2010 copyright : (C) 2010 by Fernando Vilas email : kmymoney-devel@kde.org ***************************************************************************/ /*************************************************************************** * * * 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 INTERESTEDITWIZARDPAGE_H #define INTERESTEDITWIZARDPAGE_H // ---------------------------------------------------------------------------- // QT Includes #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes namespace Ui { class InterestEditWizardPage; } /** * This class implements the Interest Edit page of the * @ref KNewLoanWizard. */ class InterestEditWizardPage : public QWizardPage { Q_OBJECT public: explicit InterestEditWizardPage(QWidget *parent = nullptr); - ~InterestEditWizardPage(); + ~InterestEditWizardPage() override; /** * Overload the isComplete function to control the Next button */ - bool isComplete() const; + bool isComplete() const final override; Ui::InterestEditWizardPage *ui; }; #endif diff --git a/kmymoney/wizards/newloanwizard/interestwizardpage.h b/kmymoney/wizards/newloanwizard/interestwizardpage.h index 65cc5d2b0..4fbc855f6 100644 --- a/kmymoney/wizards/newloanwizard/interestwizardpage.h +++ b/kmymoney/wizards/newloanwizard/interestwizardpage.h @@ -1,57 +1,57 @@ /*************************************************************************** interestwizardpage - description ------------------- begin : Sun Jul 4 2010 copyright : (C) 2010 by Fernando Vilas email : kmymoney-devel@kde.org ***************************************************************************/ /*************************************************************************** * * * 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 INTERESTWIZARDPAGE_H #define INTERESTWIZARDPAGE_H // ---------------------------------------------------------------------------- // QT Includes #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes namespace Ui { class InterestWizardPage; } /** * This class implements the Interest page of the * @ref KNewLoanWizard. */ class InterestWizardPage : public QWizardPage { Q_OBJECT public: explicit InterestWizardPage(QWidget *parent = nullptr); - ~InterestWizardPage(); + ~InterestWizardPage() override; /** * Overload the initializePage function to set widgets based on * the inputs from previous pages. */ - void initializePage(); + void initializePage() final override; Ui::InterestWizardPage *ui; public Q_SLOTS: void resetCalculator(); }; #endif diff --git a/kmymoney/wizards/newloanwizard/keditloanwizard.h b/kmymoney/wizards/newloanwizard/keditloanwizard.h index ad8fc8bbb..173840559 100644 --- a/kmymoney/wizards/newloanwizard/keditloanwizard.h +++ b/kmymoney/wizards/newloanwizard/keditloanwizard.h @@ -1,81 +1,81 @@ /*************************************************************************** keditloanwizard.h - description ------------------- begin : Wed Nov 12 2003 copyright : (C) 2000-2003 by Michael Edwardes email : mte@users.sourceforge.net 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 KEDITLOANWIZARD_H #define KEDITLOANWIZARD_H // ---------------------------------------------------------------------------- // QT Includes // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes #include "knewloanwizard.h" /** * @author Thomas Baumgart */ class KEditLoanWizardPrivate; class KEditLoanWizard : public KNewLoanWizard { Q_OBJECT public: explicit KEditLoanWizard(const MyMoneyAccount& account, QWidget *parent = nullptr); - ~KEditLoanWizard(); + ~KEditLoanWizard() override; /** * This method returns the schedule for the payments. The account * where the amortization should be transferred to is the one * we currently edited with this wizard. * * @return MyMoneySchedule object for payments */ const MyMoneySchedule schedule() const; /** * This method returns a MyMoneyAccount object with all data * filled out as provided by the wizard. * * @return updated MyMoneyAccount object */ const MyMoneyAccount account() const; void loadWidgets(const MyMoneyAccount& acc); const MyMoneyTransaction transaction() const; - bool validateCurrentPage(); + bool validateCurrentPage() final override; protected: void updateEditSummary(); private: Q_DISABLE_COPY(KEditLoanWizard) Q_DECLARE_PRIVATE(KEditLoanWizard) }; #endif diff --git a/kmymoney/wizards/newloanwizard/knewloanwizard.h b/kmymoney/wizards/newloanwizard/knewloanwizard.h index 4c362ef32..89d2df577 100644 --- a/kmymoney/wizards/newloanwizard/knewloanwizard.h +++ b/kmymoney/wizards/newloanwizard/knewloanwizard.h @@ -1,169 +1,169 @@ /*************************************************************************** knewloanwizard.h - description ------------------- begin : Wed Oct 8 2003 copyright : (C) 2000-2003 by Thomas Baumgart email : mte@users.sourceforge.net Javier Campos Morales Felix Rodriguez John C Thomas Baumgart Kevin Tambascio ***************************************************************************/ /*************************************************************************** * * * 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 KNEWLOANWIZARD_H #define KNEWLOANWIZARD_H // ---------------------------------------------------------------------------- // QT Includes #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes class QString; class MyMoneyAccount; class MyMoneySchedule; class MyMoneyAccountLoan; class MyMoneyTransaction; /** * @author Thomas Baumgart */ /** * This class implementes a wizard for the creation of loan accounts. * The user is asked a set of questions and according to the answers * the respective MyMoneyAccount object can be requested from the * wizard when accept() has been called. A MyMoneySchedule is also * available to create a schedule entry for the payments to the newly * created loan. * */ class KNewLoanWizardPrivate; class KNewLoanWizard : public QWizard { Q_OBJECT //TODO: find a way to make this not a friend class friend class AdditionalFeesWizardPage; public: enum { Page_Intro, Page_EditIntro, Page_NewGeneralInfo, Page_EditSelection, Page_LoanAttributes, Page_EffectiveDate, Page_LendBorrow, Page_Name, Page_InterestType, Page_PreviousPayments, Page_RecordPayment, Page_VariableInterestDate, Page_PaymentEdit, Page_InterestEdit, Page_FirstPayment, Page_NewCalculateLoan, Page_PaymentFrequency, Page_InterestCalculation, Page_LoanAmount, Page_Interest, Page_Duration, Page_Payment, Page_FinalPayment, Page_CalculationOverview, Page_NewPayments, Page_InterestCategory, Page_AdditionalFees, Page_Schedule, Page_SummaryEdit, Page_AssetAccount, Page_Summary }; explicit KNewLoanWizard(QWidget *parent = nullptr); ~KNewLoanWizard(); /** * This method returns the schedule for the payments. The account * where the amortization should be transferred to is the one * we currently try to create with this wizard. The appropriate split * will be returned as the first split of the transaction inside * * as parameter @p accountId as this is the account that was created * after this wizard was left via the accept() method. * * @return MyMoneySchedule object for payments */ MyMoneySchedule schedule() const; /** * This method returns the id of the account to/from which * the payout should be created. If the checkbox that allows * to skip the creation of this transaction is checked, this * method returns QString() * * @return id of account or empty QString */ QString initialPaymentAccount() const; /** * This method returns the date of the payout transaction. * If the checkbox that allows to skip the creation of * this transaction is checked, this method returns QDate() * * @return selected date or invalid QDate if checkbox is selected. */ QDate initialPaymentDate() const; - bool validateCurrentPage(); + bool validateCurrentPage() override; const MyMoneyAccountLoan account() const; /** * This method returns the id of the next page in the wizard. * It is overloaded here to support the dynamic nature of this wizard. * * @return id of the next page or -1 if there is no next page */ - int nextId() const; + int nextId() const final override; protected Q_SLOTS: // void slotNewPayee(const QString&); void slotReloadEditWidgets(); Q_SIGNALS: /** * This signal is emitted, when a new category name has been * entered by the user and this name is not known as account * by the MyMoneyFile object. * Before the signal is emitted, a MyMoneyAccount is constructed * by this object and filled with the desired name. All other members * of MyMoneyAccount will remain in their default state. Upon return, * the connected slot should have created the object in the MyMoneyFile * engine and filled the member @p id. * * @param acc reference to MyMoneyAccount object that caries the name * and will return information about the created category. */ void newCategory(MyMoneyAccount& acc); /** * This signal is sent out, when a new payee needs to be created * @sa KMyMoneyCombo::createItem() * * @param txt The name of the payee to be created * @param id A connected slot should store the id of the created object * in this variable */ void createPayee(const QString& txt, QString& id); protected: const QScopedPointer d_ptr; KNewLoanWizard(KNewLoanWizardPrivate &dd, QWidget *parent); private: Q_DISABLE_COPY(KNewLoanWizard) Q_DECLARE_PRIVATE(KNewLoanWizard) private Q_SLOTS: void slotNewCategory(MyMoneyAccount &acc); void slotNewPayee(const QString& newnameBase, QString& id); }; #endif diff --git a/kmymoney/wizards/newloanwizard/loanamountwizardpage.h b/kmymoney/wizards/newloanwizard/loanamountwizardpage.h index 64ac7b278..d04cd73ec 100644 --- a/kmymoney/wizards/newloanwizard/loanamountwizardpage.h +++ b/kmymoney/wizards/newloanwizard/loanamountwizardpage.h @@ -1,63 +1,63 @@ /*************************************************************************** loanamountwizardpage - description ------------------- begin : Sun Jul 4 2010 copyright : (C) 2010 by Fernando Vilas email : kmymoney-devel@kde.org ***************************************************************************/ /*************************************************************************** * * * 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 LOANAMOUNTWIZARDPAGE_H #define LOANAMOUNTWIZARDPAGE_H // ---------------------------------------------------------------------------- // QT Includes #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes namespace Ui { class LoanAmountWizardPage; } /** * This class implements the Loan Amount page of the * @ref KNewLoanWizard. */ class LoanAmountWizardPage : public QWizardPage { Q_OBJECT public: explicit LoanAmountWizardPage(QWidget *parent = nullptr); ~LoanAmountWizardPage(); /** * Overload the isComplete function to control the Next button */ - bool isComplete() const; + bool isComplete() const final override; /** * Overload the initializePage function to set widgets based on * the inputs from previous pages. */ - void initializePage(); + void initializePage() final override; Ui::LoanAmountWizardPage *ui; public Q_SLOTS: void resetCalculator(); }; #endif diff --git a/kmymoney/wizards/newloanwizard/loanattributeswizardpage.h b/kmymoney/wizards/newloanwizard/loanattributeswizardpage.h index f7218e460..1cebdcfdb 100644 --- a/kmymoney/wizards/newloanwizard/loanattributeswizardpage.h +++ b/kmymoney/wizards/newloanwizard/loanattributeswizardpage.h @@ -1,68 +1,68 @@ /*************************************************************************** loanattributeswizardpage - description ------------------- begin : Mon Dec 30 2013 copyright : (C) 2013 by Jeremy Whiting email : jpwhiting@kde.org ***************************************************************************/ /*************************************************************************** * * * 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 LOANATTRIBUTESWIZARDPAGE_H #define LOANATTRIBUTESWIZARDPAGE_H // ---------------------------------------------------------------------------- // QT Includes #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes namespace Ui { class LoanAttributesWizardPage; } /** * This class implements the Loan Attributes page of the * @ref KNewLoanWizard. */ class LoanAttributesWizardPage : public QWizardPage { Q_OBJECT public: explicit LoanAttributesWizardPage(QWidget *parent = nullptr); ~LoanAttributesWizardPage(); /** * Overload the isComplete function to control the Next button */ - bool isComplete() const; + bool isComplete() const final override; /** * Overload the initializePage function to set widgets based on * the inputs from previous pages. */ - void initializePage(); + void initializePage() final override; /** * Set the institution combobox to the name given */ void setInstitution(const QString &institutionName); Ui::LoanAttributesWizardPage *ui; protected Q_SLOTS: void slotNewClicked(); }; #endif diff --git a/kmymoney/wizards/newloanwizard/namewizardpage.h b/kmymoney/wizards/newloanwizard/namewizardpage.h index 0f9b38b72..d5c93678b 100644 --- a/kmymoney/wizards/newloanwizard/namewizardpage.h +++ b/kmymoney/wizards/newloanwizard/namewizardpage.h @@ -1,60 +1,60 @@ /*************************************************************************** namewizardpage - description ------------------- begin : Sun Jul 4 2010 copyright : (C) 2010 by Fernando Vilas email : kmymoney-devel@kde.org ***************************************************************************/ /*************************************************************************** * * * 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 NAMEWIZARDPAGE_H #define NAMEWIZARDPAGE_H // ---------------------------------------------------------------------------- // QT Includes #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes namespace Ui { class NameWizardPage; } /** * This class implements the Name page of the * @ref KNewLoanWizard. */ class NameWizardPage : public QWizardPage { Q_OBJECT public: explicit NameWizardPage(QWidget *parent = nullptr); ~NameWizardPage(); /** * Some things on this page depend on previous pages for correct * initialization, so overload initializePage() */ - void initializePage(); + void initializePage() final override; /** * Overload the isComplete function to control the Next button */ - bool isComplete() const; + bool isComplete() const final override; Ui::NameWizardPage *ui; }; #endif diff --git a/kmymoney/wizards/newloanwizard/recordpaymentwizardpage.h b/kmymoney/wizards/newloanwizard/recordpaymentwizardpage.h index da917b283..e5db4a1fb 100644 --- a/kmymoney/wizards/newloanwizard/recordpaymentwizardpage.h +++ b/kmymoney/wizards/newloanwizard/recordpaymentwizardpage.h @@ -1,56 +1,56 @@ /*************************************************************************** recordpaymentwizardpage - description ------------------- begin : Sun Jul 4 2010 copyright : (C) 2010 by Fernando Vilas email : kmymoney-devel@kde.org ***************************************************************************/ /*************************************************************************** * * * 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 RECORDPAYMENTWIZARDPAGE_H #define RECORDPAYMENTWIZARDPAGE_H // ---------------------------------------------------------------------------- // QT Includes #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes namespace Ui { class RecordPaymentWizardPage; } /** * This class implements the Record Payment page of the * @ref KNewLoanWizard. */ class RecordPaymentWizardPage : public QWizardPage { Q_OBJECT public: explicit RecordPaymentWizardPage(QWidget *parent = nullptr); ~RecordPaymentWizardPage(); /** * Overload the initializePage function to set widgets based on * the inputs from previous pages. */ - void initializePage(); + void initializePage() final override; private: Ui::RecordPaymentWizardPage *ui; }; #endif diff --git a/kmymoney/wizards/newloanwizard/schedulewizardpage.h b/kmymoney/wizards/newloanwizard/schedulewizardpage.h index 757d8220d..de5f9f469 100644 --- a/kmymoney/wizards/newloanwizard/schedulewizardpage.h +++ b/kmymoney/wizards/newloanwizard/schedulewizardpage.h @@ -1,60 +1,60 @@ /*************************************************************************** schedulewizardpage - description ------------------- begin : Sun Jul 4 2010 copyright : (C) 2010 by Fernando Vilas email : kmymoney-devel@kde.org ***************************************************************************/ /*************************************************************************** * * * 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 SCHEDULEWIZARDPAGE_H #define SCHEDULEWIZARDPAGE_H // ---------------------------------------------------------------------------- // QT Includes #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes namespace Ui { class ScheduleWizardPage; } /** * This class implements the Schedule page of the * @ref KNewLoanWizard. */ class ScheduleWizardPage : public QWizardPage { Q_OBJECT public: explicit ScheduleWizardPage(QWidget *parent = nullptr); ~ScheduleWizardPage(); /** * Overload the isComplete function to control the Next button */ - bool isComplete() const; + bool isComplete() const final override; /** * Overload the initializePage function to set widgets based on * the inputs from previous pages. */ - void initializePage(); + void initializePage() final override; Ui::ScheduleWizardPage *ui; }; #endif diff --git a/kmymoney/wizards/newloanwizard/summaryeditwizardpage.h b/kmymoney/wizards/newloanwizard/summaryeditwizardpage.h index 1ab7ab303..ba42433ed 100644 --- a/kmymoney/wizards/newloanwizard/summaryeditwizardpage.h +++ b/kmymoney/wizards/newloanwizard/summaryeditwizardpage.h @@ -1,56 +1,56 @@ /*************************************************************************** summaryeditwizardpage - description ------------------- begin : Sun Jul 4 2010 copyright : (C) 2010 by Fernando Vilas email : kmymoney-devel@kde.org ***************************************************************************/ /*************************************************************************** * * * 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 SUMMARYEDITWIZARDPAGE_H #define SUMMARYEDITWIZARDPAGE_H // ---------------------------------------------------------------------------- // QT Includes #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes namespace Ui { class SummaryEditWizardPage; } /** * This class implements the Summary Edit page of the * @ref KNewLoanWizard. */ class SummaryEditWizardPage : public QWizardPage { Q_OBJECT public: explicit SummaryEditWizardPage(QWidget *parent = nullptr); - ~SummaryEditWizardPage(); + ~SummaryEditWizardPage() override; /** * Overload the initializePage function to set widgets based on * the inputs from previous pages. */ - void initializePage(); + void initializePage() final override; private: Ui::SummaryEditWizardPage *ui; }; #endif diff --git a/kmymoney/wizards/newloanwizard/summarywizardpage.h b/kmymoney/wizards/newloanwizard/summarywizardpage.h index 5b547da16..cb7ab4159 100644 --- a/kmymoney/wizards/newloanwizard/summarywizardpage.h +++ b/kmymoney/wizards/newloanwizard/summarywizardpage.h @@ -1,56 +1,56 @@ /*************************************************************************** summarywizardpage - description ------------------- begin : Sun Jul 4 2010 copyright : (C) 2010 by Fernando Vilas email : kmymoney-devel@kde.org ***************************************************************************/ /*************************************************************************** * * * 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 SUMMARYWIZARDPAGE_H #define SUMMARYWIZARDPAGE_H // ---------------------------------------------------------------------------- // QT Includes #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes namespace Ui { class SummaryWizardPage; } /** * This class implements the Summary page of the * @ref KNewLoanWizard. */ class SummaryWizardPage : public QWizardPage { Q_OBJECT public: explicit SummaryWizardPage(QWidget *parent = nullptr); ~SummaryWizardPage(); /** * Overload the initializePage function to set widgets based on * the inputs from previous pages. */ - void initializePage(); + void initializePage() final override; private: Ui::SummaryWizardPage *ui; }; #endif diff --git a/libkgpgfile/kgpgfile.h b/libkgpgfile/kgpgfile.h index c6a75ffa3..9781f5bd1 100644 --- a/libkgpgfile/kgpgfile.h +++ b/libkgpgfile/kgpgfile.h @@ -1,168 +1,168 @@ /*************************************************************************** kgpgfile.h ------------------- copyright : (C) 2004,2005,2009 by 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. * * * ***************************************************************************/ #ifndef KGPGFILE_H #define KGPGFILE_H // ---------------------------------------------------------------------------- // QT Includes #include #include class QDateTime; // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes /** * @author Thomas Baumgart */ /** * A class for reading and writing data to/from an * encrypted e.g. file. * * This class presents a QFile based object to the application * but reads/writes data from/to the file through an instance of GPG. * * @code * * +------------------+ write +-----------+ +---------+ * | |--------->|\ |----->| | * | Application code | read | QFile | | gpgme++ | * | |<---------|/ |<-----| | * +------------------+ | KGPGFile | +---------+ * | | | * | control| | +-------+ * +-------------->| |----->| | * | | | File | * | |----->| | * | | +-------+ * +-----------+ * @endcode * * The @p write interface contains methods as write() and putch(), the @p read * interface the methods read(), getch() and ungetch(). The @p control interface * special methods only available with KGPGFile e.g. addRecipient(), keyAvailable() and * GPGAvailable(). Other, more general methods such as open(), close() and flush() are * not shown in the above picture. */ class KGPGFile : public QFile { Q_OBJECT public: explicit KGPGFile(const QString& fname = "", const QString& homedir = "~/.gnupg", const QString& options = ""); ~KGPGFile(); - virtual bool open(OpenMode mode); - virtual void close(); + bool open(OpenMode mode) final override; + void close() final override; virtual void flush(); - virtual qint64 readData(char *data, qint64 maxlen); - virtual qint64 writeData(const char *data, qint64 maxlen); + qint64 readData(char *data, qint64 maxlen) final override; + qint64 writeData(const char *data, qint64 maxlen) final override; /** * Adds a recipient for whom the file should be encrypted. * At least one recipient must be specified using this * method before the file can be written to. @p recipient * must contain a valid name as defined by GPG. See the * GPG documentation for more information. * * @param recipient recipients identification (e.g. e-mail address) */ void addRecipient(const QString& recipient); /** * sets the name of the file to @p fn. This method must be * called prior to open(). */ void setFileName(const QString& fn); /** This function returns the error from the GPG system as a user * readable string. The strinf is empty if there were no errors. */ QString errorToString() const; /** * This method returns the information about the expiration date of a key. * An invalid QDateTime object is returned if @a name matches more than one * key or the key does not have an expiration date. */ QDateTime keyExpires(const QString& name); /** * Checks whether GPG is available or not * * @retval true GPG can be started and returns a version number * @retval false GPG is not available */ static bool GPGAvailable(); /** * Checks whether a key for a given user-id @p name exists. * * @param name the user-id to be checked. @p name can be * any reference understood by GPG (e.g. an e-mail * address or a key-id) * @retval true key for user-id @p name was found * @retval false key for user-id @p not available */ static bool keyAvailable(const QString& name); /** * This function returns a list of the secret keys contained * in the keyring. Each list item is divided into two fields * separated by a colon (':'). The first field contains the * key id, the second field the name. The list may contain * multiple entries with the same key-id and different names. * * Example of an entry in the list: * * "9C59DB40B75DD3BA:Thomas Baumgart " */ static void secretKeyList(QStringList& list); /** * This function returns a list of the public keys contained * in the keyring. Each list item is divided into two fields * separated by a colon (':'). The first field contains the * key id, the second field the name. The list may contain * multiple entries with the same key-id and different names. * * Example of an entry in the list: * * "9C59DB40B75DD3BA:Thomas Baumgart " */ static void publicKeyList(QStringList& list); private: void keyList(QStringList& list, bool secretKeys = false, const QString& pattern = QString()); private: /// \internal d-pointer class. class Private; /// \internal d-pointer instance. Private* const d; }; #endif diff --git a/tools/xea2kmt.cpp b/tools/xea2kmt.cpp index c04fb45ec..b3b8a2762 100644 --- a/tools/xea2kmt.cpp +++ b/tools/xea2kmt.cpp @@ -1,656 +1,656 @@ /*************************************************************************** xea2kmt.cpp ------------------- copyright : (C) 2014 by Ralf Habacker ****************************************************************************/ /*************************************************************************** * * * 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 "../kmymoney/mymoney/mymoneyaccount.h" #include #include #include #include #include #include #include #include "mymoneyenums.h" using namespace eMyMoney; QDebug operator <<(QDebug out, const QXmlStreamNamespaceDeclaration &a) { out << "QXmlStreamNamespaceDeclaration(" << "prefix:" << a.prefix().toString() << "namespaceuri:" << a.namespaceUri().toString() << ")"; return out; } QDebug operator <<(QDebug out, const QXmlStreamAttribute &a) { out << "QXmlStreamAttribute(" << "prefix:" << a.prefix().toString() << "namespaceuri:" << a.namespaceUri().toString() << "name:" << a.name().toString() << " value:" << a.value().toString() << ")"; return out; } bool debug = false; bool withID = false; bool noLevel1Names = false; bool withTax = false; bool prefixNameWithCode = false; typedef QMap DirNameMapType; /** * map to hold differences from gnucash to kmymoney template directory * @return directory name map */ DirNameMapType &getDirNameMap() { static DirNameMapType dirNameMap; dirNameMap["cs"] = "cs_CZ"; dirNameMap["da"] = "dk"; dirNameMap["ja"] = "ja_JP"; dirNameMap["ko"] = "ko_KR"; dirNameMap["nb"] = "nb_NO"; dirNameMap["nl"] = "nl_NL"; dirNameMap["ru"] = "ru_RU"; return dirNameMap; } int toKMyMoneyAccountType(const QString &type) { if(type == "ROOT") return (int)Account::Type::Unknown; else if (type == "BANK") return (int)Account::Type::Checkings; else if (type == "CASH") return (int)Account::Type::Cash; else if (type == "CREDIT") return (int)Account::Type::CreditCard; else if (type == "INVEST") return (int)Account::Type::Investment; else if (type == "RECEIVABLE") return (int)Account::Type::Asset; else if (type == "ASSET") return (int)Account::Type::Asset; else if (type == "PAYABLE") return (int)Account::Type::Liability; else if (type == "LIABILITY") return (int)Account::Type::Liability; else if (type == "CURRENCY") return (int)Account::Type::Currency; else if (type == "INCOME") return (int)Account::Type::Income; else if (type == "EXPENSE") return (int)Account::Type::Expense; else if (type == "STOCK") return (int)Account::Type::Stock; else if (type == "MUTUAL") return (int)Account::Type::Stock; else if (type == "EQUITY") return (int)Account::Type::Equity; else return 99; // unknown } class TemplateAccount { public: typedef QList List; typedef QList PointerList; typedef QMap SlotList; QString id; - QString type; - QString name; + QString m_type; + QString m_name; QString code; QString parent; SlotList slotList; TemplateAccount() { } TemplateAccount(const TemplateAccount &b) : id(b.id), - type(b.type), - name(b.name), + m_type(b.m_type), + m_name(b.m_name), code(b.code), parent(b.parent), slotList(b.slotList) { } void clear() { id = ""; - type = ""; - name = ""; + m_type = ""; + m_name = ""; code = ""; parent = ""; slotList.clear(); } bool readSlots(QXmlStreamReader &xml) { while (!xml.atEnd()) { QXmlStreamReader::TokenType type = xml.readNext(); if (type == QXmlStreamReader::StartElement) { QStringRef _name = xml.name(); if (_name == "slot") { type = xml.readNext(); if (type == QXmlStreamReader::Characters) type = xml.readNext(); if (type == QXmlStreamReader::StartElement) { QStringRef name = xml.name(); QString key, value; if (name == "key") key = xml.readElementText(QXmlStreamReader::SkipChildElements).trimmed(); type = xml.readNext(); if (type == QXmlStreamReader::Characters) type = xml.readNext(); if (type == QXmlStreamReader::StartElement) { name = xml.name(); if (name == "value") value = xml.readElementText(QXmlStreamReader::SkipChildElements).trimmed(); } if (!key.isEmpty() && !value.isEmpty()) slotList[key] = value; } } } else if (type == QXmlStreamReader::EndElement) { QStringRef _name = xml.name(); if (_name == "slots") return true; } } return true; } bool read(QXmlStreamReader &xml) { while (!xml.atEnd()) { xml.readNext(); QStringRef _name = xml.name(); if (xml.isEndElement() && _name == "account") { - if (prefixNameWithCode && !code.isEmpty() && !name.startsWith(code)) - name = code + ' ' + name; + if (prefixNameWithCode && !code.isEmpty() && !m_name.startsWith(code)) + m_name = code + ' ' + m_name; return true; } if (xml.isStartElement()) { if (_name == "name") - name = xml.readElementText(QXmlStreamReader::SkipChildElements).trimmed(); + m_name = xml.readElementText(QXmlStreamReader::SkipChildElements).trimmed(); else if (_name == "id") id = xml.readElementText(QXmlStreamReader::SkipChildElements).trimmed(); else if (_name == "type") - type = xml.readElementText(QXmlStreamReader::SkipChildElements).trimmed(); + m_type = xml.readElementText(QXmlStreamReader::SkipChildElements).trimmed(); else if (_name == "code") code = xml.readElementText(QXmlStreamReader::SkipChildElements).trimmed(); else if (_name == "parent") parent = xml.readElementText(QXmlStreamReader::SkipChildElements).trimmed(); else if (_name == "slots") readSlots(xml); else { if (debug) qDebug() << "skipping" << _name.toString(); } } } return false; } }; QDebug operator <<(QDebug out, const TemplateAccount &a) { out << "TemplateAccount(" - << "name:" << a.name + << "name:" << a.m_name << "id:" << a.id - << "type:" << a.type + << "type:" << a.m_type << "code:" << a.code << "parent:" << a.parent << "slotList:" << a.slotList << ")\n"; return out; } QDebug operator <<(QDebug out, const TemplateAccount::PointerList &a) { out << "TemplateAccount::List("; foreach(const TemplateAccount *account, a) out << *account; out << ")"; return out; } class TemplateFile { public: QString title; QString longDescription; QString shortDescription; TemplateAccount::List accounts; bool read(QXmlStreamReader &xml) { Q_ASSERT(xml.isStartElement() && xml.name() == "gnc-account-example"); while (xml.readNextStartElement()) { QStringRef name = xml.name(); if (xml.name() == "title") title = xml.readElementText().trimmed(); else if (xml.name() == "short-description") shortDescription = xml.readElementText().trimmed().replace(" ", " "); else if (xml.name() == "long-description") longDescription = xml.readElementText().trimmed().replace(" ", " "); else if (xml.name() == "account") { TemplateAccount account; if (account.read(xml)) accounts.append(account); } else { if (debug) qDebug() << "skipping" << name.toString(); xml.skipCurrentElement(); } } return true; } bool writeAsXml(QXmlStreamWriter &xml) { xml.writeStartElement("","title"); xml.writeCharacters(title); xml.writeEndElement(); xml.writeStartElement("","shortdesc"); xml.writeCharacters(shortDescription); xml.writeEndElement(); xml.writeStartElement("","longdesc"); xml.writeCharacters(longDescription); xml.writeEndElement(); xml.writeStartElement("","accounts"); bool result = writeAccountsAsXml(xml); xml.writeEndElement(); return result; } bool writeAccountsAsXml(QXmlStreamWriter &xml, const QString &id="", int index=0) { TemplateAccount::PointerList list; if (index == 0) list = accountsByType("ROOT"); else list = accountsByParentID(id); foreach(TemplateAccount *account, list) { - if (account->type != "ROOT") + if (account->m_type != "ROOT") { xml.writeStartElement("","account"); - xml.writeAttribute("type", QString::number(toKMyMoneyAccountType(account->type))); - xml.writeAttribute("name", noLevel1Names && index < 2 ? "" : account->name); + xml.writeAttribute("type", QString::number(toKMyMoneyAccountType(account->m_type))); + xml.writeAttribute("name", noLevel1Names && index < 2 ? "" : account->m_name); if (withID) xml.writeAttribute("id", account->id); if (withTax) { if (account->slotList.contains("tax-related")) { xml.writeStartElement("flag"); xml.writeAttribute("name","Tax"); xml.writeAttribute("value",account->slotList["tax-related"]); xml.writeEndElement(); } } } index++; writeAccountsAsXml(xml, account->id, index); index--; xml.writeEndElement(); } return true; } TemplateAccount *account(const QString &id) { for(int i=0; i < accounts.size(); i++) { TemplateAccount &account = accounts[i]; if (account.id == id) return &account; } return 0; } TemplateAccount::PointerList accountsByType(const QString &type) { TemplateAccount::PointerList list; for(int i=0; i < accounts.size(); i++) { TemplateAccount &account = accounts[i]; - if (account.type == type) + if (account.m_type == type) list.append(&account); } return list; } static bool nameLessThan(TemplateAccount *a1, TemplateAccount *a2) { - return a1->name < a2->name; + return a1->m_name < a2->m_name; } TemplateAccount::PointerList accountsByParentID(const QString &parentID) { TemplateAccount::PointerList list; for(int i=0; i < accounts.size(); i++) { TemplateAccount &account = accounts[i]; if (account.parent == parentID) list.append(&account); } qSort(list.begin(), list.end(), nameLessThan); return list; } bool dumpTemplates(const QString &id="", int index=0) { TemplateAccount::PointerList list; if (index == 0) list = accountsByType("ROOT"); else list = accountsByParentID(id); foreach(TemplateAccount *account, list) { QString a; a.fill(' ', index); - qDebug() << a << account->name << toKMyMoneyAccountType(account->type); + qDebug() << a << account->m_name << toKMyMoneyAccountType(account->m_type); index++; dumpTemplates(account->id, index); index--; } return true; } }; QDebug operator <<(QDebug out, const TemplateFile &a) { out << "TemplateFile(" << "title:" << a.title << "short description:" << a.shortDescription << "long description:" << a.longDescription << "accounts:"; foreach(const TemplateAccount &account, a.accounts) out << account; out << ")"; return out; } class GnuCashAccountTemplateReader { public: GnuCashAccountTemplateReader() { } bool read(const QString &filename) { QFile file(filename); QTextStream in(&file); in.setCodec("utf-8"); if(!file.open(QIODevice::ReadOnly)) return false; inFileName = filename; return read(in.device()); } TemplateFile &result() { return _template; } bool dumpTemplates() { return _template.dumpTemplates(); } bool writeAsXml(const QString &filename=QString()) { if (filename.isEmpty()) { QTextStream stream(stdout); return writeAsXml(stream.device()); } else { QFile file(filename); if(!file.open(QIODevice::WriteOnly)) return false; return writeAsXml(&file); } } protected: bool checkAndUpdateAvailableNamespaces(QXmlStreamReader &xml) { if (xml.namespaceDeclarations().size() < 5) { qWarning() << "gnucash template file is missing required name space declarations; adding by self"; } xml.addExtraNamespaceDeclaration(QXmlStreamNamespaceDeclaration("act", "http://www.gnucash.org/XML/act")); xml.addExtraNamespaceDeclaration(QXmlStreamNamespaceDeclaration("gnc", "http://www.gnucash.org/XML/gnc")); xml.addExtraNamespaceDeclaration(QXmlStreamNamespaceDeclaration("gnc-act", "http://www.gnucash.org/XML/gnc-act")); xml.addExtraNamespaceDeclaration(QXmlStreamNamespaceDeclaration("cmdty","http://www.gnucash.org/XML/cmdty")); xml.addExtraNamespaceDeclaration(QXmlStreamNamespaceDeclaration("slot","http://www.gnucash.org/XML/slot")); return true; } bool read(QIODevice *device) { - xml.setDevice(device); - while(!xml.atEnd()) + m_xml.setDevice(device); + while(!m_xml.atEnd()) { - xml.readNext(); - if (xml.isStartElement()) + m_xml.readNext(); + if (m_xml.isStartElement()) { - if (xml.name() == "gnc-account-example") + if (m_xml.name() == "gnc-account-example") { - checkAndUpdateAvailableNamespaces(xml); - _template.read(xml); + checkAndUpdateAvailableNamespaces(m_xml); + _template.read(m_xml); } else - xml.raiseError(QObject::tr("The file is not an gnucash account template file.")); + m_xml.raiseError(QObject::tr("The file is not an gnucash account template file.")); } } - if (xml.error() != QXmlStreamReader::NoError) - qWarning() << xml.errorString(); - return !xml.error(); + if (m_xml.error() != QXmlStreamReader::NoError) + qWarning() << m_xml.errorString(); + return !m_xml.error(); } bool writeAsXml(QIODevice *device) { QXmlStreamWriter xml(device); xml.setAutoFormatting(true); xml.setAutoFormattingIndent(1); xml.setCodec("utf-8"); xml.writeStartDocument(); QString fileName = inFileName; fileName.replace(QRegExp(".*/accounts"),"accounts"); xml.writeComment(QString("\n" " Converted using xea2kmt from GnuCash sources\n" "\n" " %1\n" "\n" " Please check the source file for possible copyright\n" " and license information.\n" ).arg(fileName)); xml.writeDTD(""); xml.writeStartElement("","kmymoney-account-template"); bool result = _template.writeAsXml(xml); xml.writeEndElement(); xml.writeEndDocument(); return result; } - QXmlStreamReader xml; + QXmlStreamReader m_xml; TemplateFile _template; QString inFileName; }; void scanDir(QDir dir, QStringList &files) { dir.setNameFilters(QStringList("*.gnucash-xea")); dir.setFilter(QDir::Files | QDir::NoDotAndDotDot | QDir::NoSymLinks); if (debug) qDebug() << "Scanning: " << dir.path(); QStringList fileList = dir.entryList(); for (int i=0; i []"; qWarning() << argv[0] << " --in-dir --out-dir "; qWarning() << "options:"; qWarning() << " --debug - output debug information"; qWarning() << " --help - this page"; qWarning() << " --no-level1-names - do not export account names for top level accounts"; qWarning() << " --prefix-name-with-code - prefix account name with account code if present"; qWarning() << " --with-id - write account id attribute"; qWarning() << " --with-tax-related - parse and export gnucash 'tax-related' flag"; qWarning() << " --in-dir - search for gnucash templates files in "; qWarning() << " --out-dir - generate kmymoney templates below