diff --git a/kmymoney/converter/mymoneystatementreader.cpp b/kmymoney/converter/mymoneystatementreader.cpp index 34a721a50..86af7f472 100644 --- a/kmymoney/converter/mymoneystatementreader.cpp +++ b/kmymoney/converter/mymoneystatementreader.cpp @@ -1,1578 +1,1577 @@ /*************************************************************************** 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 "kmymoneysettings.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), 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()); // 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; 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(); 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 int gap = std::abs(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/dialogs/konlinetransferform.cpp b/kmymoney/dialogs/konlinetransferform.cpp index 06fb0a22c..20fa06d1e 100644 --- a/kmymoney/dialogs/konlinetransferform.cpp +++ b/kmymoney/dialogs/konlinetransferform.cpp @@ -1,332 +1,332 @@ /* * 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 . */ #include "konlinetransferform.h" #include "ui_konlinetransferform.h" #include #include #include #include #include #include #include #include #include "kguiutils.h" #include "onlinetasks/interfaces/ui/ionlinejobedit.h" #include "mymoneyfile.h" #include "mymoney/onlinejobadministration.h" #include "onlinejob.h" #include "tasks/onlinetask.h" #include "accountsmodel.h" #include "models/models.h" #include "icons/icons.h" using namespace Icons; kOnlineTransferForm::kOnlineTransferForm(QWidget *parent) : QDialog(parent), ui(new Ui::kOnlineTransferForm), m_onlineJobEditWidgets(QList()), m_requiredFields(new KMandatoryFieldGroup(this)) { ui->setupUi(this); ui->unsupportedIcon->setPixmap(Icons::get(Icon::DialogInformation).pixmap(style()->pixelMetric(QStyle::PM_MessageBoxIconSize))); // The ui designer fills the QScrollArea with a QWidget. Remove it so we can simply check for .widget() == nullptr // if it contains a valid widget delete ui->creditTransferEdit->takeWidget(); OnlineBankingAccountNamesFilterProxyModel* accountsModel = new OnlineBankingAccountNamesFilterProxyModel(this); auto const model = Models::instance()->accountsModel(); accountsModel->setSourceModel(model); ui->originAccount->setModel(accountsModel); ui->convertMessage->hide(); ui->convertMessage->setWordWrap(true); auto edits = onlineJobAdministration::instance()->onlineJobEdits(); std::for_each(edits.constBegin(), edits.constEnd(), [this](onlineJobAdministration::onlineJobEditOffer in) {this->loadOnlineJobEditPlugin(in);}); // Message Widget for read only jobs m_duplicateJob = KStandardAction::copy(this); connect(m_duplicateJob, &QAction::triggered, this, &kOnlineTransferForm::duplicateCurrentJob); ui->headMessage->hide(); ui->headMessage->setWordWrap(true); ui->headMessage->setCloseButtonVisible(false); ui->headMessage->addAction(m_duplicateJob); connect(ui->transferTypeSelection, static_cast(&QComboBox::currentIndexChanged), this, &kOnlineTransferForm::convertCurrentJob); connect(ui->buttonAbort, &QAbstractButton::clicked, this, &kOnlineTransferForm::reject); connect(ui->buttonSend, &QAbstractButton::clicked, this, &kOnlineTransferForm::sendJob); connect(ui->buttonEnque, &QAbstractButton::clicked, this, &kOnlineTransferForm::accept); connect(m_requiredFields, static_cast(&KMandatoryFieldGroup::stateChanged), ui->buttonEnque, &QPushButton::setEnabled); connect(ui->originAccount, &KMyMoneyAccountCombo::accountSelected, this, &kOnlineTransferForm::accountChanged); accountChanged(); setJobReadOnly(false); m_requiredFields->add(ui->originAccount); m_requiredFields->setOkButton(ui->buttonSend); } void kOnlineTransferForm::loadOnlineJobEditPlugin(const onlineJobAdministration::onlineJobEditOffer& pluginDesc) { try { std::unique_ptr loader{new QPluginLoader(pluginDesc.fileName, this)}; QObject* plugin = loader->instance(); if (!plugin) { qWarning() << "Could not load plugin for online job editor from file \"" << pluginDesc.fileName << "\"."; return; } // Cast to KPluginFactory KPluginFactory* pluginFactory = qobject_cast< KPluginFactory* >(plugin); if (!pluginFactory) { qWarning() << "Could not create plugin factory for online job editor in file \"" << pluginDesc.fileName << "\"."; return; } IonlineJobEdit* widget = pluginFactory->create(pluginDesc.pluginKeyword, this); if (!widget) { qWarning() << "Could not create online job editor in file \"" << pluginDesc.fileName << "\"."; return; } // directly load the first widget into QScrollArea bool showWidget = true; if (!m_onlineJobEditWidgets.isEmpty()) { widget->setEnabled(false); showWidget = false; } m_onlineJobEditWidgets.append(widget); ui->transferTypeSelection->addItem(pluginDesc.name); m_requiredFields->add(widget); if (showWidget) showEditWidget(widget); - } catch (MyMoneyException& e) { + } catch (MyMoneyException&) { qWarning("Error while loading a plugin (IonlineJobEdit)."); } } void kOnlineTransferForm::convertCurrentJob(const int& index) { Q_ASSERT(index < m_onlineJobEditWidgets.count()); IonlineJobEdit* widget = m_onlineJobEditWidgets.at(index); // Vars set by onlineJobAdministration::convertBest onlineTaskConverter::convertType convertType; QString userMessage; widget->setOnlineJob(onlineJobAdministration::instance()->convertBest(activeOnlineJob(), widget->supportedOnlineTasks(), convertType, userMessage)); if (convertType == onlineTaskConverter::convertImpossible && userMessage.isEmpty()) userMessage = i18n("During the change of the order your previous entries could not be converted."); if (!userMessage.isEmpty()) { switch (convertType) { case onlineTaskConverter::convertionLossyMajor: ui->convertMessage->setMessageType(KMessageWidget::Warning); break; case onlineTaskConverter::convertImpossible: case onlineTaskConverter::convertionLossyMinor: ui->convertMessage->setMessageType(KMessageWidget::Information); break; case onlineTaskConverter::convertionLoseless: break; } ui->convertMessage->setText(userMessage); ui->convertMessage->animatedShow(); } showEditWidget(widget); } void kOnlineTransferForm::duplicateCurrentJob() { IonlineJobEdit* widget = qobject_cast< IonlineJobEdit* >(ui->creditTransferEdit->widget()); if (widget == 0) return; onlineJob duplicate(QString(), activeOnlineJob()); widget->setOnlineJob(duplicate); } void kOnlineTransferForm::accept() { emit acceptedForSave(activeOnlineJob()); QDialog::accept(); } void kOnlineTransferForm::sendJob() { emit acceptedForSend(activeOnlineJob()); QDialog::accept(); } void kOnlineTransferForm::reject() { QDialog::reject(); } bool kOnlineTransferForm::setOnlineJob(const onlineJob job) { QString name; try { name = job.task()->taskName(); } catch (const onlineJob::emptyTask&) { return false; } setCurrentAccount(job.responsibleAccount()); if (showEditWidget(name)) { IonlineJobEdit* widget = qobject_cast(ui->creditTransferEdit->widget()); if (widget != 0) { // This can happen if there are no widgets const bool ret = widget->setOnlineJob(job); setJobReadOnly(!job.isEditable()); return ret; } } return false; } void kOnlineTransferForm::accountChanged() { const QString accountId = ui->originAccount->getSelected(); try { ui->orderAccountBalance->setValue(MyMoneyFile::instance()->balance(accountId)); } catch (const MyMoneyException&) { // @todo this can happen until the selection allows to select correct accounts only ui->orderAccountBalance->setText(""); } foreach (IonlineJobEdit* widget, m_onlineJobEditWidgets) widget->setOriginAccount(accountId); checkNotSupportedWidget(); } bool kOnlineTransferForm::checkEditWidget() { return checkEditWidget(qobject_cast(ui->creditTransferEdit->widget())); } bool kOnlineTransferForm::checkEditWidget(IonlineJobEdit* widget) { if (widget != 0 && onlineJobAdministration::instance()->isJobSupported(ui->originAccount->getSelected(), widget->supportedOnlineTasks())) { return true; } return false; } /** @todo auto set another widget if a loseless convert is possible */ void kOnlineTransferForm::checkNotSupportedWidget() { if (!checkEditWidget()) { ui->displayStack->setCurrentIndex(0); } else { ui->displayStack->setCurrentIndex(1); } } void kOnlineTransferForm::setCurrentAccount(const QString& accountId) { ui->originAccount->setSelected(accountId); } onlineJob kOnlineTransferForm::activeOnlineJob() const { IonlineJobEdit* widget = qobject_cast(ui->creditTransferEdit->widget()); if (widget == 0) return onlineJob(); return widget->getOnlineJob(); } void kOnlineTransferForm::setJobReadOnly(const bool& readOnly) { ui->originAccount->setDisabled(readOnly); ui->transferTypeSelection->setDisabled(readOnly); if (readOnly) { ui->headMessage->setMessageType(KMessageWidget::Information); if (activeOnlineJob().sendDate().isValid()) ui->headMessage->setText(i18n("This credit-transfer was sent to your bank at %1 therefore cannot be edited anymore. You may create a copy for editing.", activeOnlineJob().sendDate().toString(Qt::DefaultLocaleShortDate))); else ui->headMessage->setText(i18n("This credit-transfer is not editable. You may create a copy for editing.")); if (this->isHidden()) ui->headMessage->show(); else ui->headMessage->animatedShow(); } else { ui->headMessage->animatedHide(); } } bool kOnlineTransferForm::showEditWidget(const QString& onlineTaskName) { int index = 0; foreach (IonlineJobEdit* widget, m_onlineJobEditWidgets) { if (widget->supportedOnlineTasks().contains(onlineTaskName)) { ui->transferTypeSelection->setCurrentIndex(index); showEditWidget(widget); return true; } ++index; } return false; } void kOnlineTransferForm::showEditWidget(IonlineJobEdit* widget) { Q_CHECK_PTR(widget); QWidget* oldWidget = ui->creditTransferEdit->takeWidget(); if (oldWidget != 0) { // This is true at the first call of showEditWidget() and if there are no widgets. oldWidget->setEnabled(false); disconnect(qobject_cast(oldWidget), &IonlineJobEdit::readOnlyChanged, this, &kOnlineTransferForm::setJobReadOnly); } widget->setEnabled(true); ui->creditTransferEdit->setWidget(widget); setJobReadOnly(widget->isReadOnly()); widget->show(); connect(widget, &IonlineJobEdit::readOnlyChanged, this, &kOnlineTransferForm::setJobReadOnly); checkNotSupportedWidget(); m_requiredFields->changed(); } kOnlineTransferForm::~kOnlineTransferForm() { ui->creditTransferEdit->takeWidget(); qDeleteAll(m_onlineJobEditWidgets); delete ui; delete m_duplicateJob; } diff --git a/kmymoney/dialogs/settings/ksettingsplugins.cpp b/kmymoney/dialogs/settings/ksettingsplugins.cpp index ce4c121db..f24192bfd 100644 --- a/kmymoney/dialogs/settings/ksettingsplugins.cpp +++ b/kmymoney/dialogs/settings/ksettingsplugins.cpp @@ -1,176 +1,176 @@ /*************************************************************************** ksettingsplugins.cpp -------------------- - (C) 2017 by Łukasz Wojniłowicz + Copyright (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 "ksettingsplugins.h" // ---------------------------------------------------------------------------- // QT Includes #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "pluginloader.h" struct pluginGroupInfo { QList plugins; KPluginSelector::PluginLoadMethod loadMethod; QString categoryName; }; class KSettingsPluginsPrivate { Q_DISABLE_COPY(KSettingsPluginsPrivate) public: KSettingsPluginsPrivate(KSettingsPlugins* qq) : m_pluginSelector(new KPluginSelector(qq)) { } ~KSettingsPluginsPrivate() { delete m_pluginSelector; } /** * @brief This should be called after save to kmymoneyrc in order to update cached on/off states */ void updateSavedPluginStates() { for (auto i = 0 ; i < pluginInfos.size(); ++i) savedPluginStates[i] = pluginInfos[i].isPluginEnabled(); } /** * @brief This compares plugin on/off states from KPluginSelector with cached one * @return true if user changes to plugin on/off state aren't different than initial */ bool isEqualToSavedStates() { for (auto i = 0 ; i < pluginInfos.size(); ++i) if (savedPluginStates[i] != pluginInfos[i].isPluginEnabled()) return false; return true; } KPluginSelector* const m_pluginSelector; QList pluginInfos; /** * @brief savedPluginStates This caches on/off states as in kmymoneyrc */ QBitArray savedPluginStates; }; KSettingsPlugins::KSettingsPlugins(QWidget* parent) : QWidget(parent), d_ptr(new KSettingsPluginsPrivate(this)) { Q_D(KSettingsPlugins); auto layout = new QVBoxLayout; setLayout(layout); // otherwise KPluginSelector occupies very little area layout->addWidget(d->m_pluginSelector); auto allPluginDatas = KMyMoneyPlugin::listPlugins(false); // fetch all available KMyMoney plugins QVector standardPlugins; QVector payeePlugins; QVector onlinePlugins; // divide plugins in some arbitrary categories for (const KPluginMetaData& pluginData : allPluginDatas) switch (KMyMoneyPlugin::pluginCategory(pluginData)) { case KMyMoneyPlugin::Category::StandardPlugin: standardPlugins.append(pluginData); break; case KMyMoneyPlugin::Category::PayeeIdentifier: payeePlugins.append(pluginData); break; case KMyMoneyPlugin::Category::OnlineBankOperations: onlinePlugins.append(pluginData); break; default: break; } const QVector pluginGroups { {KPluginInfo::fromMetaData(standardPlugins), KPluginSelector::PluginLoadMethod::ReadConfigFile, i18n("KMyMoney Plugins")}, {KPluginInfo::fromMetaData(payeePlugins), KPluginSelector::PluginLoadMethod::IgnoreConfigFile, i18n("Payee Identifier")}, {KPluginInfo::fromMetaData(onlinePlugins), KPluginSelector::PluginLoadMethod::IgnoreConfigFile, i18n("Online Banking Operations")} }; // add all plugins to selector for(const auto& pluginGroup : pluginGroups) { if (!pluginGroup.plugins.isEmpty()) { d->m_pluginSelector->addPlugins(pluginGroup.plugins, pluginGroup.loadMethod, pluginGroup.categoryName); // at that step plugin on/off state should be fetched automatically by KPluginSelector d->pluginInfos.append(pluginGroup.plugins); // store initial on/off state to be able to enable/disable Apply button } } d->savedPluginStates.resize(d->pluginInfos.size()); d->updateSavedPluginStates(); connect(d->m_pluginSelector, &KPluginSelector::changed, this, &KSettingsPlugins::slotPluginsSelectionChanged); } KSettingsPlugins::~KSettingsPlugins() { Q_D(KSettingsPlugins); delete d; } void KSettingsPlugins::slotPluginsSelectionChanged(bool b) { Q_D(KSettingsPlugins); if (b) { d->m_pluginSelector->updatePluginsState(); emit changed(!d->isEqualToSavedStates()); } } void KSettingsPlugins::slotResetToDefaults() { Q_D(KSettingsPlugins); d->m_pluginSelector->defaults(); } void KSettingsPlugins::slotSavePluginConfiguration() { Q_D(KSettingsPlugins); if (!d->isEqualToSavedStates()) { d->m_pluginSelector->save(); d->updateSavedPluginStates(); emit settingsChanged(QStringLiteral("Plugins")); } } diff --git a/kmymoney/kmymoney.cpp b/kmymoney/kmymoney.cpp index 862d5210a..60ba1aadd 100644 --- a/kmymoney/kmymoney.cpp +++ b/kmymoney/kmymoney.cpp @@ -1,4324 +1,4324 @@ /*************************************************************************** kmymoney.cpp ------------------- copyright : (C) 2000 by Michael Edwardes (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. * * * ***************************************************************************/ #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 // ---------------------------------------------------------------------------- // 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/kfindtransactiondlg.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/onlinejobmessagesview.h" #include "widgets/amountedit.h" #include "widgets/kmymoneyedit.h" #include "widgets/kmymoneymvccombo.h" #include "views/kmymoneyview.h" #include "views/konlinejoboutbox.h" #include "models/onlinejobmessagesmodel.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 "mymoney/onlinejobmessage.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 "konlinetransferform.h" #include #include #include "kmymoneyutils.h" #include "kcreditswindow.h" #include "ledgerdelegate.h" #include "storageenums.h" #include "mymoneyenums.h" #include "dialogenums.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 QHash pActions; QHash pMenus; enum backupStateE { BACKUP_IDLE = 0, BACKUP_MOUNTING, BACKUP_COPYING, BACKUP_UNMOUNTING }; class KMyMoneyApp::Private { public: Private(KMyMoneyApp *app) : q(app), m_ft(0), m_moveToAccountSelector(0), m_statementXMLindex(0), m_balanceWarning(0), m_backupResult(0), m_backupMount(0), m_ignoreBackupExitCode(false), m_fileOpen(false), m_fmode(QFileDevice::ReadUser | QFileDevice::WriteUser), m_myMoneyView(0), m_progressBar(0), m_searchDlg(0), m_autoSaveTimer(0), m_progressTimer(0), m_inAutoSaving(false), m_transactionEditor(0), m_endingBalanceDlg(0), m_saveEncrypted(0), m_additionalKeyLabel(0), m_additionalKeyButton(0), m_recentFiles(0), #ifdef KF5Holidays_FOUND m_holidayRegion(0), #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; MyMoneyFileTransaction* m_ft; KMyMoneyAccountSelector* m_moveToAccountSelector; 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; KFindTransactionDlg* m_searchDlg; // This is Auto Saving related bool m_autoSaveEnabled; QTimer* m_autoSaveTimer; QTimer* m_progressTimer; int m_autoSavePeriod; bool m_inAutoSaving; // pointer to the current transaction editor TransactionEditor* m_transactionEditor; // Reconciliation dialog KEndingBalanceDlg* m_endingBalanceDlg; // 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 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(); 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(); auto storage = new MyMoneyStorageMgr; 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); // things are finished, now we connect the storage to the engine // which forces a reload of the cache in the engine with those // objects that are cached MyMoneyFile::instance()->attachStorage(storage); // 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 bool rc = true; try { if (! url.isValid()) { throw MYMONEYEXCEPTION(i18n("Malformed URL '%1'", url.url())); } 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); { #ifdef Q_OS_WIN 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(this/*the global variable kmymoney is not yet assigned. So we pass it here*/); 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); /////////////////////////////////////////////////////////////////// // 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; int weekStart = locale.firstDayOfWeek(); int weekEnd = weekStart-1; if (weekEnd < Qt::Monday) { weekEnd = Qt::Sunday; } bool startFirst = (weekStart < weekEnd); for (int i = 0; i < 8; ++i) { if (startFirst) d->m_processingDays.setBit(i, (i >= weekStart && i <= weekEnd)); else d->m_processingDays.setBit(i, (i >= weekStart || i <= weekEnd)); } 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 the 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()); delete d->m_searchDlg; delete d->m_transactionEditor; delete d->m_endingBalanceDlg; delete d->m_moveToAccountSelector; #ifdef KF5Holidays_FOUND delete d->m_holidayRegion; #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::slotObjectDestroyed(QObject* o) { if (o == d->m_moveToAccountSelector) { d->m_moveToAccountSelector = 0; } } 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(); // ************* // 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); KStandardAction::print(this, &KMyMoneyApp::slotPrintView, aC); KStandardAction::preferences(this, &KMyMoneyApp::slotSettings, aC); /* Look-up table for all custom 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 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 Edit menu // ************* {Action::EditFindTransaction, &KMyMoneyApp::slotFindTransaction}, // ************* // 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; } #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)); - /* Dont't use url variable after KRecentFilesAction::addUrl + /* 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(); 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); 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(); 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::slotFindTransaction() { if (d->m_searchDlg == 0) { d->m_searchDlg = new KFindTransactionDlg(this); connect(d->m_searchDlg, SIGNAL(destroyed()), this, SLOT(slotCloseSearchDialog())); connect(d->m_searchDlg, SIGNAL(transactionSelected(QString,QString)), d->m_myMoneyView, SLOT(slotLedgerSelected(QString,QString))); } d->m_searchDlg->show(); d->m_searchDlg->raise(); d->m_searchDlg->activateWindow(); } void KMyMoneyApp::slotCloseSearchDialog() { if (d->m_searchDlg) d->m_searchDlg->deleteLater(); d->m_searchDlg = 0; } 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; kmymoney->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; } kmymoney->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++; } kmymoney->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) { kmymoney->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); } } } kmymoney->slotStatusProgressBar(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 kmymoney->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); QMenu *menu = dynamic_cast(w); if (menu) 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->slotTransactionsSelected(KMyMoneyRegister::SelectedTransactions()); 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(kmymoney, i18n("You must first select a KMyMoney file before you can import a statement.")) == KMessageBox::Continue) kmymoney->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 { #ifdef KF5Holidays_FOUND if (!d->m_processingDays.testBit(date.dayOfWeek())) return false; 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 Q_UNUSED(date); 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() { q->d->m_myMoneyView->slotObjectSelected(MyMoneyAccount()); q->d->m_myMoneyView->slotObjectSelected(MyMoneyInstitution()); q->d->m_myMoneyView->slotObjectSelected(MyMoneySchedule()); q->d->m_myMoneyView->slotObjectSelected(MyMoneyTag()); q->d->m_myMoneyView->slotTransactionsSelected(KMyMoneyRegister::SelectedTransactions()); 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/mymoney/mymoneyreport.cpp b/kmymoney/mymoney/mymoneyreport.cpp index 26325808a..480b12f8a 100644 --- a/kmymoney/mymoney/mymoneyreport.cpp +++ b/kmymoney/mymoney/mymoneyreport.cpp @@ -1,1633 +1,1633 @@ /*************************************************************************** mymoneyreport.cpp ------------------- begin : Sun July 4 2004 copyright : (C) 2004-2005 by Ace Jones email : acejones@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. * * * ***************************************************************************/ #include "mymoneyreport_p.h" // ---------------------------------------------------------------------------- // QT Includes #include #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes #include "mymoneymoney.h" #include "mymoneyfile.h" #include "mymoneyaccount.h" #include "mymoneystoragenames.h" #include "mymoneytransaction.h" #include "mymoneytransactionfilter.h" #include "mymoneyexception.h" using namespace MyMoneyStorageNodes; // define this to debug reports // #define DEBUG_REPORTS const QStringList MyMoneyReport::kRowTypeText = QString("none,assetliability,expenseincome,category,topcategory,account,tag,payee,month,week,topaccount,topaccount-account,equitytype,accounttype,institution,budget,budgetactual,schedule,accountinfo,accountloaninfo,accountreconcile,cashflow").split(','); const QStringList MyMoneyReport::kColumnTypeText = QString("none,months,bimonths,quarters,4,5,6,weeks,8,9,10,11,years").split(','); // if you add names here, don't forget to update the bitmap for EQueryColumns // and shift the bit for eQCend one position to the left const QStringList MyMoneyReport::kQueryColumnsText = QString("none,number,payee,category,tag,memo,account,reconcileflag,action,shares,price,performance,loan,balance,capitalgain").split(','); const MyMoneyReport::EReportType MyMoneyReport::kTypeArray[] = { eNoReport, ePivotTable, ePivotTable, eQueryTable, eQueryTable, eQueryTable, eQueryTable, eQueryTable, eQueryTable, eQueryTable, eQueryTable, eQueryTable, eQueryTable, eQueryTable, eQueryTable, ePivotTable, ePivotTable, eInfoTable, eInfoTable, eInfoTable, eQueryTable, eQueryTable, eNoReport }; const QStringList MyMoneyReport::kDetailLevelText = QString("none,all,top,group,total,invalid").split(','); const QStringList MyMoneyReport::kChartTypeText = QString("none,line,bar,pie,ring,stackedbar").split(','); // This should live in mymoney/mymoneytransactionfilter.h const QStringList kTypeText = QString("all,payments,deposits,transfers,none").split(','); const QStringList kStateText = QString("all,notreconciled,cleared,reconciled,frozen,none").split(','); const QStringList kDateLockText = QString("alldates,untiltoday,currentmonth,currentyear,monthtodate,yeartodate,yeartomonth,lastmonth,lastyear,last7days,last30days,last3months,last6months,last12months,next7days,next30days,next3months,next6months,next12months,userdefined,last3tonext3months,last11Months,currentQuarter,lastQuarter,nextQuarter,currentFiscalYear,lastFiscalYear,today,next18months").split(','); const QStringList kDataLockText = QString("automatic,userdefined").split(','); const QStringList kAccountTypeText = QString("unknown,checkings,savings,cash,creditcard,loan,certificatedep,investment,moneymarket,asset,liability,currency,income,expense,assetloan,stock,equity,invalid").split(','); MyMoneyReport::MyMoneyReport() : MyMoneyObject(*new MyMoneyReportPrivate) { Q_D(MyMoneyReport); d->m_name = "Unconfigured Pivot Table Report"; d->m_detailLevel = eDetailNone; d->m_investmentSum = eSumSold; d->m_hideTransactions = false; d->m_convertCurrency = true; d->m_favorite = false; d->m_tax = false; d->m_investments = false; d->m_loans = false; d->m_reportType = kTypeArray[eExpenseIncome]; d->m_rowType = eExpenseIncome; d->m_columnType = eMonths; d->m_columnsAreDays = false; d->m_queryColumns = eQCnone; d->m_dateLock = TransactionFilter::Date::UserDefined; d->m_accountGroupFilter = false; d->m_chartType = eChartLine; d->m_chartDataLabels = true; d->m_chartCHGridLines = true; d->m_chartSVGridLines = true; d->m_chartByDefault = false; d->m_logYaxis = false; d->m_dataRangeStart = '0'; d->m_dataRangeEnd = '0'; d->m_dataMajorTick = '0'; d->m_dataMinorTick = '0'; d->m_yLabelsPrecision = 2; d->m_dataLock = MyMoneyReport::automatic; d->m_includeSchedules = false; d->m_includeTransfers = false; d->m_includeBudgetActuals = false; d->m_includeUnusedAccounts = false; d->m_showRowTotals = false; d->m_showColumnTotals = true; d->m_includeForecast = false; d->m_includeMovingAverage = false; d->m_movingAverageDays = 0; d->m_includePrice = false; d->m_includeAveragePrice = false; d->m_mixedTime = false; d->m_currentDateColumn = 0; d->m_settlementPeriod = 3; d->m_showSTLTCapitalGains = false; d->m_tseparator = QDate::currentDate().addYears(-1); d->m_skipZero = false; d->m_chartLineWidth = m_lineWidth; } MyMoneyReport::MyMoneyReport(ERowType rt, unsigned ct, TransactionFilter::Date dl, EDetailLevel ss, const QString& name, const QString& comment) : MyMoneyObject(*new MyMoneyReportPrivate) { Q_D(MyMoneyReport); d->m_name = name; d->m_comment = comment; d->m_detailLevel = ss; d->m_investmentSum = ct & eQCcapitalgain ? eSumSold : eSumPeriod; d->m_hideTransactions = false; d->m_convertCurrency = true; d->m_favorite = false; d->m_tax = false; d->m_investments = false; d->m_loans = false; d->m_reportType = kTypeArray[rt]; d->m_rowType = rt; d->m_columnType = eMonths; d->m_columnsAreDays = false; d->m_queryColumns = eQCnone; d->m_dateLock = dl; d->m_accountGroupFilter = false; d->m_chartType = eChartLine; d->m_chartDataLabels = true; d->m_chartCHGridLines = true; d->m_chartSVGridLines = true; d->m_chartByDefault = false; d->m_logYaxis = false; d->m_dataRangeStart = '0'; d->m_dataRangeEnd = '0'; d->m_dataMajorTick = '0'; d->m_dataMinorTick = '0'; d->m_yLabelsPrecision = 2; d->m_dataLock = MyMoneyReport::automatic; d->m_includeSchedules = false; d->m_includeTransfers = false; d->m_includeBudgetActuals = false; d->m_includeUnusedAccounts = false; d->m_showRowTotals = false; d->m_showColumnTotals = true; d->m_includeForecast = false; d->m_includeMovingAverage = false; d->m_movingAverageDays = 0; d->m_includePrice = false; d->m_includeAveragePrice = false; d->m_mixedTime = false; d->m_currentDateColumn = 0; d->m_settlementPeriod = 3; d->m_showSTLTCapitalGains = false; d->m_tseparator = QDate::currentDate().addYears(-1); d->m_skipZero = false; //set initial values d->m_chartLineWidth = m_lineWidth; //set report type if (d->m_reportType == ePivotTable) d->m_columnType = static_cast(ct); if (d->m_reportType == eQueryTable) d->m_queryColumns = static_cast(ct); setDateFilter(dl); //throw exception if the type is inconsistent if ((rt > static_cast(sizeof(kTypeArray) / sizeof(kTypeArray[0]))) || (d->m_reportType == eNoReport)) throw MYMONEYEXCEPTION("Invalid report type"); //add the corresponding account groups if (rt == MyMoneyReport::eAssetLiability) { addAccountGroup(Account::Type::Asset); addAccountGroup(Account::Type::Liability); d->m_showRowTotals = true; } if (rt == MyMoneyReport::eAccount) { addAccountGroup(Account::Type::Asset); addAccountGroup(Account::Type::AssetLoan); addAccountGroup(Account::Type::Cash); addAccountGroup(Account::Type::Checkings); addAccountGroup(Account::Type::CreditCard); if (m_expertMode) addAccountGroup(Account::Type::Equity); addAccountGroup(Account::Type::Expense); addAccountGroup(Account::Type::Income); addAccountGroup(Account::Type::Liability); addAccountGroup(Account::Type::Loan); addAccountGroup(Account::Type::Savings); addAccountGroup(Account::Type::Stock); d->m_showRowTotals = true; } if (rt == MyMoneyReport::eExpenseIncome) { addAccountGroup(Account::Type::Expense); addAccountGroup(Account::Type::Income); d->m_showRowTotals = true; } //FIXME take this out once we have sorted out all issues regarding budget of assets and liabilities -- asoliverez@gmail.com if (rt == MyMoneyReport::eBudget || rt == MyMoneyReport::eBudgetActual) { addAccountGroup(Account::Type::Expense); addAccountGroup(Account::Type::Income); } if (rt == MyMoneyReport::eAccountInfo) { addAccountGroup(Account::Type::Asset); addAccountGroup(Account::Type::Liability); } //cash flow reports show splits for all account groups if (rt == MyMoneyReport::eCashFlow) { addAccountGroup(Account::Type::Expense); addAccountGroup(Account::Type::Income); addAccountGroup(Account::Type::Asset); addAccountGroup(Account::Type::Liability); } #ifdef DEBUG_REPORTS QDebug out = qDebug(); out << _name << toString(_rt) << toString(m_reportType); foreach(const Account::Type accountType, m_accountGroups) out << MyMoneyAccount::accountTypeToString(accountType); if (m_accounts.size() > 0) out << m_accounts; #endif } MyMoneyReport::MyMoneyReport(const QDomElement& node) : MyMoneyObject(*new MyMoneyReportPrivate, node) { Q_D(MyMoneyReport); d->m_currentDateColumn = 0; // properly initialize the object before reading it *this = MyMoneyReport(); if (!read(node)) clearId(); } MyMoneyReport::MyMoneyReport(const MyMoneyReport& other) : MyMoneyObject(*new MyMoneyReportPrivate(*other.d_func()), other.id()), MyMoneyTransactionFilter(other) { } MyMoneyReport::MyMoneyReport(const QString& id, const MyMoneyReport& other) : MyMoneyObject(*new MyMoneyReportPrivate(*other.d_func()), id), MyMoneyTransactionFilter(other) { Q_D(MyMoneyReport); d->m_movingAverageDays = 0; d->m_currentDateColumn = 0; } MyMoneyReport::~MyMoneyReport() { } MyMoneyReport::EReportType MyMoneyReport::reportType() const { Q_D(const MyMoneyReport); return d->m_reportType; } QString MyMoneyReport::name() const { Q_D(const MyMoneyReport); return d->m_name; } void MyMoneyReport::setName(const QString& s) { Q_D(MyMoneyReport); d->m_name = s; } bool MyMoneyReport::isShowingRowTotals() const { Q_D(const MyMoneyReport); return (d->m_showRowTotals); } void MyMoneyReport::setShowingRowTotals(bool f) { Q_D(MyMoneyReport); d->m_showRowTotals = f; } bool MyMoneyReport::isShowingColumnTotals() const { Q_D(const MyMoneyReport); return d->m_showColumnTotals; } void MyMoneyReport::setShowingColumnTotals(bool f) { Q_D(MyMoneyReport); d->m_showColumnTotals = f; } MyMoneyReport::ERowType MyMoneyReport::rowType() const { Q_D(const MyMoneyReport); return d->m_rowType; } void MyMoneyReport::setRowType(ERowType rt) { Q_D(MyMoneyReport); d->m_rowType = rt; d->m_reportType = kTypeArray[rt]; d->m_accountGroupFilter = false; d->m_accountGroups.clear(); if (rt == MyMoneyReport::eAssetLiability) { addAccountGroup(Account::Type::Asset); addAccountGroup(Account::Type::Liability); } if (rt == MyMoneyReport::eExpenseIncome) { addAccountGroup(Account::Type::Expense); addAccountGroup(Account::Type::Income); } } bool MyMoneyReport::isRunningSum() const { Q_D(const MyMoneyReport); return (d->m_rowType == eAssetLiability); } MyMoneyReport::EColumnType MyMoneyReport::columnType() const { Q_D(const MyMoneyReport); return d->m_columnType; } void MyMoneyReport::setColumnType(EColumnType ct) { Q_D(MyMoneyReport); d->m_columnType = ct; } bool MyMoneyReport::isConvertCurrency() const { Q_D(const MyMoneyReport); return d->m_convertCurrency; } void MyMoneyReport::setConvertCurrency(bool f) { Q_D(MyMoneyReport); d->m_convertCurrency = f; } uint MyMoneyReport::columnPitch() const { Q_D(const MyMoneyReport); return static_cast(d->m_columnType); } QString MyMoneyReport::comment() const { Q_D(const MyMoneyReport); return d->m_comment; } void MyMoneyReport::setComment(const QString& comment) { Q_D(MyMoneyReport); d->m_comment = comment; } MyMoneyReport::EQueryColumns MyMoneyReport::queryColumns() const { Q_D(const MyMoneyReport); return d->m_queryColumns; } void MyMoneyReport::setQueryColumns(EQueryColumns qc) { Q_D(MyMoneyReport); d->m_queryColumns = qc; } QString MyMoneyReport::group() const { Q_D(const MyMoneyReport); return d->m_group; } void MyMoneyReport::setGroup(const QString& group) { Q_D(MyMoneyReport); d->m_group = group; } bool MyMoneyReport::isFavorite() const { Q_D(const MyMoneyReport); return d->m_favorite; } void MyMoneyReport::setFavorite(bool f) { Q_D(MyMoneyReport); d->m_favorite = f; } bool MyMoneyReport::isTax() const { Q_D(const MyMoneyReport); return d->m_tax; } void MyMoneyReport::setTax(bool f) { Q_D(MyMoneyReport); d->m_tax = f; } bool MyMoneyReport::isInvestmentsOnly() const { Q_D(const MyMoneyReport); return d->m_investments; } void MyMoneyReport::setInvestmentsOnly(bool f) { Q_D(MyMoneyReport); d->m_investments = f; if (f) d->m_loans = false; } bool MyMoneyReport::isLoansOnly() const { Q_D(const MyMoneyReport); return d->m_loans; } void MyMoneyReport::setLoansOnly(bool f) { Q_D(MyMoneyReport); d->m_loans = f; if (f) d->m_investments = false; } MyMoneyReport::EDetailLevel MyMoneyReport::detailLevel() const { Q_D(const MyMoneyReport); return d->m_detailLevel; } void MyMoneyReport::setDetailLevel(EDetailLevel detail) { Q_D(MyMoneyReport); d->m_detailLevel = detail; } MyMoneyReport::EInvestmentSum MyMoneyReport::investmentSum() const { Q_D(const MyMoneyReport); return d->m_investmentSum; } void MyMoneyReport::setInvestmentSum(EInvestmentSum sum) { Q_D(MyMoneyReport); d->m_investmentSum = sum; } bool MyMoneyReport::isHideTransactions() const { Q_D(const MyMoneyReport); return d->m_hideTransactions; } void MyMoneyReport::setHideTransactions(bool f) { Q_D(MyMoneyReport); d->m_hideTransactions = f; } MyMoneyReport::EChartType MyMoneyReport::chartType() const { Q_D(const MyMoneyReport); return d->m_chartType; } void MyMoneyReport::setChartType(EChartType type) { Q_D(MyMoneyReport); d->m_chartType = type; } bool MyMoneyReport::isChartDataLabels() const { Q_D(const MyMoneyReport); return d->m_chartDataLabels; } void MyMoneyReport::setChartDataLabels(bool f) { Q_D(MyMoneyReport); d->m_chartDataLabels = f; } bool MyMoneyReport::isChartCHGridLines() const { Q_D(const MyMoneyReport); return d->m_chartCHGridLines; } void MyMoneyReport::setChartCHGridLines(bool f) { Q_D(MyMoneyReport); d->m_chartCHGridLines = f; } bool MyMoneyReport::isChartSVGridLines() const { Q_D(const MyMoneyReport); return d->m_chartSVGridLines; } void MyMoneyReport::setChartSVGridLines(bool f) { Q_D(MyMoneyReport); d->m_chartSVGridLines = f; } bool MyMoneyReport::isChartByDefault() const { Q_D(const MyMoneyReport); return d->m_chartByDefault; } void MyMoneyReport::setChartByDefault(bool f) { Q_D(MyMoneyReport); d->m_chartByDefault = f; } uint MyMoneyReport::chartLineWidth() const { Q_D(const MyMoneyReport); return d->m_chartLineWidth; } void MyMoneyReport::setChartLineWidth(uint f) { Q_D(MyMoneyReport); d->m_chartLineWidth = f; } bool MyMoneyReport::isLogYAxis() const { Q_D(const MyMoneyReport); return d->m_logYaxis; } void MyMoneyReport::setLogYAxis(bool f) { Q_D(MyMoneyReport); d->m_logYaxis = f; } QString MyMoneyReport::dataRangeStart() const { Q_D(const MyMoneyReport); return d->m_dataRangeStart; } void MyMoneyReport::setDataRangeStart(const QString& f) { Q_D(MyMoneyReport); d->m_dataRangeStart = f; } QString MyMoneyReport::dataRangeEnd() const { Q_D(const MyMoneyReport); return d->m_dataRangeEnd; } void MyMoneyReport::setDataRangeEnd(const QString& f) { Q_D(MyMoneyReport); d->m_dataRangeEnd = f; } QString MyMoneyReport::dataMajorTick() const { Q_D(const MyMoneyReport); return d->m_dataMajorTick; } void MyMoneyReport::setDataMajorTick(const QString& f) { Q_D(MyMoneyReport); d->m_dataMajorTick = f; } QString MyMoneyReport::dataMinorTick() const { Q_D(const MyMoneyReport); return d->m_dataMinorTick; } void MyMoneyReport::setDataMinorTick(const QString& f) { Q_D(MyMoneyReport); d->m_dataMinorTick = f; } uint MyMoneyReport::yLabelsPrecision() const { Q_D(const MyMoneyReport); return d->m_yLabelsPrecision; } void MyMoneyReport::setYLabelsPrecision(int f) { Q_D(MyMoneyReport); d->m_yLabelsPrecision = f; } bool MyMoneyReport::isIncludingSchedules() const { Q_D(const MyMoneyReport); return d->m_includeSchedules; } void MyMoneyReport::setIncludingSchedules(bool f) { Q_D(MyMoneyReport); d->m_includeSchedules = f; } bool MyMoneyReport::isColumnsAreDays() const { Q_D(const MyMoneyReport); return d->m_columnsAreDays; } void MyMoneyReport::setColumnsAreDays(bool f) { Q_D(MyMoneyReport); d->m_columnsAreDays = f; } bool MyMoneyReport::isIncludingTransfers() const { Q_D(const MyMoneyReport); return d->m_includeTransfers; } void MyMoneyReport::setIncludingTransfers(bool f) { Q_D(MyMoneyReport); d->m_includeTransfers = f; } bool MyMoneyReport::isIncludingUnusedAccounts() const { Q_D(const MyMoneyReport); return d->m_includeUnusedAccounts; } void MyMoneyReport::setIncludingUnusedAccounts(bool f) { Q_D(MyMoneyReport); d->m_includeUnusedAccounts = f; } bool MyMoneyReport::hasBudget() const { Q_D(const MyMoneyReport); return !d->m_budgetId.isEmpty(); } QString MyMoneyReport::budget() const { Q_D(const MyMoneyReport); return d->m_budgetId; } /** * Sets the budget used for this report * * @param budget The ID of the budget to use, or an empty string * to indicate a budget is NOT included * @param fa Whether to display actual data alongside the budget. * Setting to false means the report displays ONLY the budget itself. * @warning For now, the budget ID is ignored. The budget id is * simply checked for any non-empty string, and if so, hasBudget() * will return true. */ void MyMoneyReport::setBudget(const QString& budget, bool fa) { Q_D(MyMoneyReport); d->m_budgetId = budget; d->m_includeBudgetActuals = fa; } bool MyMoneyReport::isIncludingBudgetActuals() const { Q_D(const MyMoneyReport); return d->m_includeBudgetActuals; } void MyMoneyReport::setIncludingBudgetActuals(bool f) { Q_D(MyMoneyReport); d->m_includeBudgetActuals = f; } bool MyMoneyReport::isIncludingForecast() const { Q_D(const MyMoneyReport); return d->m_includeForecast; } void MyMoneyReport::setIncludingForecast(bool f) { Q_D(MyMoneyReport); d->m_includeForecast = f; } bool MyMoneyReport::isIncludingMovingAverage() const { Q_D(const MyMoneyReport); return d->m_includeMovingAverage; } void MyMoneyReport::setIncludingMovingAverage(bool f) { Q_D(MyMoneyReport); d->m_includeMovingAverage = f; } int MyMoneyReport::movingAverageDays() const { Q_D(const MyMoneyReport); return d->m_movingAverageDays; } void MyMoneyReport::setMovingAverageDays(int days) { Q_D(MyMoneyReport); d->m_movingAverageDays = days; } bool MyMoneyReport::isIncludingPrice() const { Q_D(const MyMoneyReport); return d->m_includePrice; } void MyMoneyReport::setIncludingPrice(bool f) { Q_D(MyMoneyReport); d->m_includePrice = f; } bool MyMoneyReport::isIncludingAveragePrice() const { Q_D(const MyMoneyReport); return d->m_includeAveragePrice; } void MyMoneyReport::setIncludingAveragePrice(bool f) { Q_D(MyMoneyReport); d->m_includeAveragePrice = f; } MyMoneyReport::dataOptionE MyMoneyReport::dataFilter() const { Q_D(const MyMoneyReport); return d->m_dataLock; } bool MyMoneyReport::isDataUserDefined() const { Q_D(const MyMoneyReport); return d->m_dataLock == MyMoneyReport::userDefined; } void MyMoneyReport::setDataFilter(dataOptionE u) { Q_D(MyMoneyReport); d->m_dataLock = u; } eMyMoney::TransactionFilter::Date MyMoneyReport::dateRange() const { Q_D(const MyMoneyReport); return d->m_dateLock; } bool MyMoneyReport::isDateUserDefined() const { Q_D(const MyMoneyReport); return d->m_dateLock == TransactionFilter::Date::UserDefined; } /** * Set the underlying date filter and LOCK that filter to the specified * range. For example, if @p _u is "CurrentMonth", this report should always * be updated to the current month no matter when the report is run. * * This updating is not entirely automatic, you should update it yourself by * calling updateDateFilter. * * @param _u The date range constant (MyMoneyTransactionFilter::dateRangeE) * which this report should be locked to. */ void MyMoneyReport::setDateFilter(TransactionFilter::Date u) { Q_D(MyMoneyReport); d->m_dateLock = u; if (u != TransactionFilter::Date::UserDefined) MyMoneyTransactionFilter::setDateFilter(u); } void MyMoneyReport::setDateFilter(const QDate& db, const QDate& de) { MyMoneyTransactionFilter::setDateFilter(db, de); } void MyMoneyReport::updateDateFilter() { Q_D(MyMoneyReport); if (d->m_dateLock != TransactionFilter::Date::UserDefined) MyMoneyTransactionFilter::setDateFilter(d->m_dateLock); } bool MyMoneyReport::isMixedTime() const { Q_D(const MyMoneyReport); return d->m_mixedTime; } void MyMoneyReport::setMixedTime(bool f) { Q_D(MyMoneyReport); d->m_mixedTime = f; } int MyMoneyReport::currentDateColumn() const { Q_D(const MyMoneyReport); return d->m_currentDateColumn; } void MyMoneyReport::setCurrentDateColumn(int f) { Q_D(MyMoneyReport); d->m_currentDateColumn = f; } uint MyMoneyReport::settlementPeriod() const { Q_D(const MyMoneyReport); return d->m_settlementPeriod; } void MyMoneyReport::setSettlementPeriod(uint days) { Q_D(MyMoneyReport); d->m_settlementPeriod = days; } bool MyMoneyReport::isShowingSTLTCapitalGains() const { Q_D(const MyMoneyReport); return d->m_showSTLTCapitalGains; } void MyMoneyReport::setShowSTLTCapitalGains(bool f) { Q_D(MyMoneyReport); d->m_showSTLTCapitalGains = f; } QDate MyMoneyReport::termSeparator() const { Q_D(const MyMoneyReport); return d->m_tseparator; } void MyMoneyReport::setTermSeparator(const QDate& date) { Q_D(MyMoneyReport); d->m_tseparator = date; } bool MyMoneyReport::isSkippingZero() const { Q_D(const MyMoneyReport); return d->m_skipZero; } void MyMoneyReport::setSkipZero(int f) { Q_D(MyMoneyReport); d->m_skipZero = f; } void MyMoneyReport::clearTransactionFilter() { Q_D(MyMoneyReport); d->m_accountGroupFilter = false; d->m_accountGroups.clear(); MyMoneyTransactionFilter::clear(); } void MyMoneyReport::assignFilter(const MyMoneyTransactionFilter& filter) { MyMoneyTransactionFilter::operator=(filter); } void MyMoneyReport::validDateRange(QDate& db, QDate& de) { db = fromDate(); de = toDate(); // if either begin or end date are invalid we have one of the following // possible date filters: // // a) begin date not set - first transaction until given end date // b) end date not set - from given date until last transaction // c) both not set - first transaction until last transaction // // If there is no transaction in the engine at all, we use the current // year as the filter criteria. if (!db.isValid() || !de.isValid()) { QList list = MyMoneyFile::instance()->transactionList(*this); QDate tmpBegin, tmpEnd; if (!list.isEmpty()) { qSort(list); // try to use the post dates tmpBegin = list.front().postDate(); tmpEnd = list.back().postDate(); // if the post dates are not valid try the entry dates if (!tmpBegin.isValid()) tmpBegin = list.front().entryDate(); if (!tmpEnd.isValid()) tmpEnd = list.back().entryDate(); } // make sure that we leave this function with valid dates no mather what if (!tmpBegin.isValid() || !tmpEnd.isValid() || tmpBegin > tmpEnd) { tmpBegin = QDate(QDate::currentDate().year(), 1, 1); // the first date in the file tmpEnd = QDate(QDate::currentDate().year(), 12, 31); // the last date in the file } if (!db.isValid()) db = tmpBegin; if (!de.isValid()) de = tmpEnd; } if (db > de) db = de; } bool MyMoneyReport::accountGroups(QList& list) const { Q_D(const MyMoneyReport); bool result = d->m_accountGroupFilter; if (result) { QList::const_iterator it_group = d->m_accountGroups.begin(); while (it_group != d->m_accountGroups.end()) { list += (*it_group); ++it_group; } } return result; } void MyMoneyReport::addAccountGroup(Account::Type type) { Q_D(MyMoneyReport); if (!d->m_accountGroups.isEmpty() && type != Account::Type::Unknown) { if (d->m_accountGroups.contains(type)) return; } d->m_accountGroupFilter = true; if (type != Account::Type::Unknown) d->m_accountGroups.push_back(type); } bool MyMoneyReport::includesAccountGroup(Account::Type type) const { Q_D(const MyMoneyReport); bool result = (! d->m_accountGroupFilter) || (isIncludingTransfers() && d->m_rowType == MyMoneyReport::eExpenseIncome) || d->m_accountGroups.contains(type); return result; } bool MyMoneyReport::includes(const MyMoneyAccount& acc) const { Q_D(const MyMoneyReport); auto result = false; if (includesAccountGroup(acc.accountGroup())) { switch (acc.accountGroup()) { case Account::Type::Income: case Account::Type::Expense: if (isTax()) result = (acc.value("Tax") == "Yes") && includesCategory(acc.id()); else result = includesCategory(acc.id()); break; case Account::Type::Asset: case Account::Type::Liability: if (isLoansOnly()) result = acc.isLoan() && includesAccount(acc.id()); else if (isInvestmentsOnly()) result = acc.isInvest() && includesAccount(acc.id()); else if (isIncludingTransfers() && d->m_rowType == MyMoneyReport::eExpenseIncome) // If transfers are included, ONLY include this account if it is NOT // included in the report itself!! result = ! includesAccount(acc.id()); else result = includesAccount(acc.id()); break; case Account::Type::Equity: if (isInvestmentsOnly()) result = (isIncludingPrice() || isIncludingAveragePrice()) && acc.isInvest() && includesAccount(acc.id()); break; default: result = includesAccount(acc.id()); } } return result; } void MyMoneyReport::write(QDomElement& e, QDomDocument *doc, bool anonymous) const { Q_D(const MyMoneyReport); // No matter what changes, be sure to have a 'type' attribute. Only change // the major type if it becomes impossible to maintain compatibility with // older versions of the program as new features are added to the reports. // Feel free to change the minor type every time a change is made here. // write report's internals if (d->m_reportType == ePivotTable) e.setAttribute(d->getAttrName(Report::Attribute::Type), "pivottable 1.15"); else if (d->m_reportType == eQueryTable) e.setAttribute(d->getAttrName(Report::Attribute::Type), "querytable 1.14"); else if (d->m_reportType == eInfoTable) e.setAttribute(d->getAttrName(Report::Attribute::Type), "infotable 1.0"); e.setAttribute(d->getAttrName(Report::Attribute::Group), d->m_group); e.setAttribute(d->getAttrName(Report::Attribute::ID), d->m_id); // write general tab if (anonymous) { e.setAttribute(d->getAttrName(Report::Attribute::Name), d->m_id); e.setAttribute(d->getAttrName(Report::Attribute::Comment), QString(d->m_comment).fill('x')); } else { e.setAttribute(d->getAttrName(Report::Attribute::Name), d->m_name); e.setAttribute(d->getAttrName(Report::Attribute::Comment), d->m_comment); } e.setAttribute(d->getAttrName(Report::Attribute::ConvertCurrency), d->m_convertCurrency); e.setAttribute(d->getAttrName(Report::Attribute::Favorite), d->m_favorite); e.setAttribute(d->getAttrName(Report::Attribute::SkipZero), d->m_skipZero); e.setAttribute(d->getAttrName(Report::Attribute::DateLock), kDateLockText[(int)d->m_dateLock]); if (d->m_reportType == ePivotTable) { // write report's internals e.setAttribute(d->getAttrName(Report::Attribute::IncludesActuals), d->m_includeBudgetActuals); e.setAttribute(d->getAttrName(Report::Attribute::IncludesForecast), d->m_includeForecast); e.setAttribute(d->getAttrName(Report::Attribute::IncludesPrice), d->m_includePrice); e.setAttribute(d->getAttrName(Report::Attribute::IncludesAveragePrice), d->m_includeAveragePrice); e.setAttribute(d->getAttrName(Report::Attribute::MixedTime), d->m_mixedTime); e.setAttribute(d->getAttrName(Report::Attribute::Investments), d->m_investments); // it's setable in rows/columns tab of querytable, but here it is internal setting // write rows/columns tab if (!d->m_budgetId.isEmpty()) e.setAttribute(d->getAttrName(Report::Attribute::Budget), d->m_budgetId); e.setAttribute(d->getAttrName(Report::Attribute::RowType), kRowTypeText[d->m_rowType]); e.setAttribute(d->getAttrName(Report::Attribute::ShowRowTotals), d->m_showRowTotals); e.setAttribute(d->getAttrName(Report::Attribute::ShowColumnTotals), d->m_showColumnTotals); e.setAttribute(d->getAttrName(Report::Attribute::Detail), kDetailLevelText[d->m_detailLevel]); e.setAttribute(d->getAttrName(Report::Attribute::IncludesMovingAverage), d->m_includeMovingAverage); if (d->m_includeMovingAverage) e.setAttribute(d->getAttrName(Report::Attribute::MovingAverageDays), d->m_movingAverageDays); e.setAttribute(d->getAttrName(Report::Attribute::IncludesSchedules), d->m_includeSchedules); e.setAttribute(d->getAttrName(Report::Attribute::IncludesTransfers), d->m_includeTransfers); e.setAttribute(d->getAttrName(Report::Attribute::IncludesUnused), d->m_includeUnusedAccounts); e.setAttribute(d->getAttrName(Report::Attribute::ColumnsAreDays), d->m_columnsAreDays); // write chart tab if (d->m_chartType < 0 || d->m_chartType >= kChartTypeText.size()) { qDebug("m_chartType out of bounds with %d on report of type %d. Default to none.", d->m_chartType, d->m_reportType); e.setAttribute(d->getAttrName(Report::Attribute::ChartType), kChartTypeText[eChartNone]); } else e.setAttribute(d->getAttrName(Report::Attribute::ChartType), kChartTypeText[d->m_chartType]); e.setAttribute(d->getAttrName(Report::Attribute::ChartCHGridLines), d->m_chartCHGridLines); e.setAttribute(d->getAttrName(Report::Attribute::ChartSVGridLines), d->m_chartSVGridLines); e.setAttribute(d->getAttrName(Report::Attribute::ChartDataLabels), d->m_chartDataLabels); e.setAttribute(d->getAttrName(Report::Attribute::ChartByDefault), d->m_chartByDefault); e.setAttribute(d->getAttrName(Report::Attribute::LogYAxis), d->m_logYaxis); e.setAttribute(d->getAttrName(Report::Attribute::ChartLineWidth), d->m_chartLineWidth); e.setAttribute(d->getAttrName(Report::Attribute::ColumnType), kColumnTypeText[d->m_columnType]); e.setAttribute(d->getAttrName(Report::Attribute::DataLock), kDataLockText[d->m_dataLock]); e.setAttribute(d->getAttrName(Report::Attribute::DataRangeStart), d->m_dataRangeStart); e.setAttribute(d->getAttrName(Report::Attribute::DataRangeEnd), d->m_dataRangeEnd); e.setAttribute(d->getAttrName(Report::Attribute::DataMajorTick), d->m_dataMajorTick); e.setAttribute(d->getAttrName(Report::Attribute::DataMinorTick), d->m_dataMinorTick); e.setAttribute(d->getAttrName(Report::Attribute::YLabelsPrecision), d->m_yLabelsPrecision); } else if (d->m_reportType == eQueryTable) { // write rows/columns tab e.setAttribute(d->getAttrName(Report::Attribute::RowType), kRowTypeText[d->m_rowType]); QStringList columns; unsigned qc = d->m_queryColumns; unsigned it_qc = eQCbegin; unsigned index = 1; while (it_qc != eQCend) { if (qc & it_qc) columns += kQueryColumnsText[index]; it_qc *= 2; index++; } e.setAttribute(d->getAttrName(Report::Attribute::QueryColumns), columns.join(",")); e.setAttribute(d->getAttrName(Report::Attribute::Tax), d->m_tax); e.setAttribute(d->getAttrName(Report::Attribute::Investments), d->m_investments); e.setAttribute(d->getAttrName(Report::Attribute::Loans), d->m_loans); e.setAttribute(d->getAttrName(Report::Attribute::HideTransactions), d->m_hideTransactions); e.setAttribute(d->getAttrName(Report::Attribute::ShowColumnTotals), d->m_showColumnTotals); e.setAttribute(d->getAttrName(Report::Attribute::Detail), kDetailLevelText[d->m_detailLevel]); // write performance tab if (d->m_queryColumns & eQCperformance || d->m_queryColumns & eQCcapitalgain) e.setAttribute(d->getAttrName(Report::Attribute::InvestmentSum), d->m_investmentSum); // write capital gains tab if (d->m_queryColumns & eQCcapitalgain) { if (d->m_investmentSum == MyMoneyReport::eSumSold) { e.setAttribute(d->getAttrName(Report::Attribute::SettlementPeriod), d->m_settlementPeriod); e.setAttribute(d->getAttrName(Report::Attribute::ShowSTLTCapitalGains), d->m_showSTLTCapitalGains); e.setAttribute(d->getAttrName(Report::Attribute::TermsSeparator), d->m_tseparator.toString(Qt::ISODate)); } } } else if (d->m_reportType == eInfoTable) e.setAttribute(d->getAttrName(Report::Attribute::ShowRowTotals), d->m_showRowTotals); // // Text Filter // QRegExp textfilter; if (textFilter(textfilter)) { QDomElement f = doc->createElement(d->getElName(Report::Element::Text)); f.setAttribute(d->getAttrName(Report::Attribute::Pattern), textfilter.pattern()); f.setAttribute(d->getAttrName(Report::Attribute::CaseSensitive), (textfilter.caseSensitivity() == Qt::CaseSensitive) ? 1 : 0); f.setAttribute(d->getAttrName(Report::Attribute::RegEx), (textfilter.patternSyntax() == QRegExp::Wildcard) ? 1 : 0); f.setAttribute(d->getAttrName(Report::Attribute::InvertText), MyMoneyTransactionFilter::isInvertingText()); e.appendChild(f); } // // Type & State Filters // QList typelist; if (types(typelist) && ! typelist.empty()) { // iterate over payees, and add each one QList::const_iterator it_type = typelist.constBegin(); while (it_type != typelist.constEnd()) { QDomElement p = doc->createElement(d->getElName(Report::Element::Type)); p.setAttribute(d->getAttrName(Report::Attribute::Type), kTypeText[*it_type]); e.appendChild(p); ++it_type; } } QList statelist; if (states(statelist) && ! statelist.empty()) { // iterate over payees, and add each one QList::const_iterator it_state = statelist.constBegin(); while (it_state != statelist.constEnd()) { QDomElement p = doc->createElement(d->getElName(Report::Element::State)); p.setAttribute(d->getAttrName(Report::Attribute::State), kStateText[*it_state]); e.appendChild(p); ++it_state; } } // // Number Filter // QString nrFrom, nrTo; if (numberFilter(nrFrom, nrTo)) { QDomElement f = doc->createElement(d->getElName(Report::Element::Number)); f.setAttribute(d->getAttrName(Report::Attribute::From), nrFrom); f.setAttribute(d->getAttrName(Report::Attribute::To), nrTo); e.appendChild(f); } // // Amount Filter // MyMoneyMoney from, to; if (amountFilter(from, to)) { // bool getAmountFilter(MyMoneyMoney&,MyMoneyMoney&); QDomElement f = doc->createElement(d->getElName(Report::Element::Amount)); f.setAttribute(d->getAttrName(Report::Attribute::From), from.toString()); f.setAttribute(d->getAttrName(Report::Attribute::To), to.toString()); e.appendChild(f); } // // Payees Filter // QStringList payeelist; if (payees(payeelist)) { if (payeelist.empty()) { QDomElement p = doc->createElement(d->getElName(Report::Element::Payee)); e.appendChild(p); } else { // iterate over payees, and add each one QStringList::const_iterator it_payee = payeelist.constBegin(); while (it_payee != payeelist.constEnd()) { QDomElement p = doc->createElement(d->getElName(Report::Element::Payee)); p.setAttribute(d->getAttrName(Report::Attribute::ID), *it_payee); e.appendChild(p); ++it_payee; } } } // // Tags Filter // QStringList taglist; if (tags(taglist)) { if (taglist.empty()) { QDomElement p = doc->createElement(d->getElName(Report::Element::Tag)); e.appendChild(p); } else { // iterate over tags, and add each one QStringList::const_iterator it_tag = taglist.constBegin(); while (it_tag != taglist.constEnd()) { QDomElement p = doc->createElement(d->getElName(Report::Element::Tag)); p.setAttribute(d->getAttrName(Report::Attribute::ID), *it_tag); e.appendChild(p); ++it_tag; } } } // // Account Groups Filter // QList accountgrouplist; if (accountGroups(accountgrouplist)) { // iterate over accounts, and add each one QList::const_iterator it_group = accountgrouplist.constBegin(); while (it_group != accountgrouplist.constEnd()) { QDomElement p = doc->createElement(d->getElName(Report::Element::AccountGroup)); p.setAttribute(d->getAttrName(Report::Attribute::Group), kAccountTypeText[(int)*it_group]); e.appendChild(p); ++it_group; } } // // Accounts Filter // QStringList accountlist; if (accounts(accountlist)) { // iterate over accounts, and add each one QStringList::const_iterator it_account = accountlist.constBegin(); while (it_account != accountlist.constEnd()) { QDomElement p = doc->createElement(d->getElName(Report::Element::Account)); p.setAttribute(d->getAttrName(Report::Attribute::ID), *it_account); e.appendChild(p); ++it_account; } } // // Categories Filter // accountlist.clear(); if (categories(accountlist)) { // iterate over accounts, and add each one QStringList::const_iterator it_account = accountlist.constBegin(); while (it_account != accountlist.constEnd()) { QDomElement p = doc->createElement(d->getElName(Report::Element::Category)); p.setAttribute(d->getAttrName(Report::Attribute::ID), *it_account); e.appendChild(p); ++it_account; } } // // Date Filter // if (d->m_dateLock == TransactionFilter::Date::UserDefined) { QDate dateFrom, dateTo; if (dateFilter(dateFrom, dateTo)) { QDomElement f = doc->createElement(d->getElName(Report::Element::Dates)); if (dateFrom.isValid()) f.setAttribute(d->getAttrName(Report::Attribute::From), dateFrom.toString(Qt::ISODate)); if (dateTo.isValid()) f.setAttribute(d->getAttrName(Report::Attribute::To), dateTo.toString(Qt::ISODate)); e.appendChild(f); } } } bool MyMoneyReport::read(const QDomElement& e) { Q_D(MyMoneyReport); // The goal of this reading method is 100% backward AND 100% forward // compatibility. Any report ever created with any version of KMyMoney // should be able to be loaded by this method (as long as it's one of the // report types supported in this version, of course) if (e.tagName().compare(nodeNames[nnReport]) != 0) return false; // read report's internals QString type = e.attribute(d->getAttrName(Report::Attribute::Type)); if (type.startsWith(QLatin1String("pivottable"))) d->m_reportType = ePivotTable; else if (type.startsWith(QLatin1String("querytable"))) d->m_reportType = eQueryTable; else if (type.startsWith(QLatin1String("infotable"))) d->m_reportType = eInfoTable; else return false; d->m_group = e.attribute(d->getAttrName(Report::Attribute::Group)); d->m_id = e.attribute(d->getAttrName(Report::Attribute::ID)); clearTransactionFilter(); // read date tab QString datelockstr = e.attribute(d->getAttrName(Report::Attribute::DateLock), "userdefined"); // Handle the pivot 1.2/query 1.1 case where the values were saved as // numbers bool ok = false; int i = datelockstr.toUInt(&ok); if (!ok) { i = kDateLockText.indexOf(datelockstr); if (i == -1) i = (int)TransactionFilter::Date::UserDefined; } setDateFilter(static_cast(i)); // read general tab d->m_name = e.attribute(d->getAttrName(Report::Attribute::Name)); d->m_comment = e.attribute(d->getAttrName(Report::Attribute::Comment), "Extremely old report"); d->m_convertCurrency = e.attribute(d->getAttrName(Report::Attribute::ConvertCurrency), "1").toUInt(); d->m_favorite = e.attribute(d->getAttrName(Report::Attribute::Favorite), "0").toUInt(); d->m_skipZero = e.attribute(d->getAttrName(Report::Attribute::SkipZero), "0").toUInt(); if (d->m_reportType == ePivotTable) { // read report's internals d->m_includeBudgetActuals = e.attribute(d->getAttrName(Report::Attribute::IncludesActuals), "0").toUInt(); d->m_includeForecast = e.attribute(d->getAttrName(Report::Attribute::IncludesForecast), "0").toUInt(); d->m_includePrice = e.attribute(d->getAttrName(Report::Attribute::IncludesPrice), "0").toUInt(); d->m_includeAveragePrice = e.attribute(d->getAttrName(Report::Attribute::IncludesAveragePrice), "0").toUInt(); d->m_mixedTime = e.attribute(d->getAttrName(Report::Attribute::MixedTime), "0").toUInt(); d->m_investments = e.attribute(d->getAttrName(Report::Attribute::Investments), "0").toUInt(); // read rows/columns tab if (e.hasAttribute(d->getAttrName(Report::Attribute::Budget))) d->m_budgetId = e.attribute(d->getAttrName(Report::Attribute::Budget)); i = kRowTypeText.indexOf(e.attribute(d->getAttrName(Report::Attribute::RowType))); if (i != -1) setRowType(static_cast(i)); else setRowType(eExpenseIncome); if (e.hasAttribute(d->getAttrName(Report::Attribute::ShowRowTotals))) d->m_showRowTotals = e.attribute(d->getAttrName(Report::Attribute::ShowRowTotals)).toUInt(); else if (rowType() == eExpenseIncome) // for backward compatibility d->m_showRowTotals = true; d->m_showColumnTotals = e.attribute(d->getAttrName(Report::Attribute::ShowColumnTotals), "1").toUInt(); //check for reports with older settings which didn't have the detail attribute i = kDetailLevelText.indexOf(e.attribute(d->getAttrName(Report::Attribute::Detail))); if (i != -1) d->m_detailLevel = static_cast(i); else d->m_detailLevel = eDetailAll; d->m_includeMovingAverage = e.attribute(d->getAttrName(Report::Attribute::IncludesMovingAverage), "0").toUInt(); if (d->m_includeMovingAverage) d->m_movingAverageDays = e.attribute(d->getAttrName(Report::Attribute::MovingAverageDays), "1").toUInt(); d->m_includeSchedules = e.attribute(d->getAttrName(Report::Attribute::IncludesSchedules), "0").toUInt(); d->m_includeTransfers = e.attribute(d->getAttrName(Report::Attribute::IncludesTransfers), "0").toUInt(); d->m_includeUnusedAccounts = e.attribute(d->getAttrName(Report::Attribute::IncludesUnused), "0").toUInt(); d->m_columnsAreDays = e.attribute(d->getAttrName(Report::Attribute::ColumnsAreDays), "0").toUInt(); // read chart tab i = kChartTypeText.indexOf(e.attribute(d->getAttrName(Report::Attribute::ChartType))); if (i != -1) d->m_chartType = static_cast(i); else d->m_chartType = eChartNone; d->m_chartCHGridLines = e.attribute(d->getAttrName(Report::Attribute::ChartCHGridLines), "1").toUInt(); d->m_chartSVGridLines = e.attribute(d->getAttrName(Report::Attribute::ChartSVGridLines), "1").toUInt(); d->m_chartDataLabels = e.attribute(d->getAttrName(Report::Attribute::ChartDataLabels), "1").toUInt(); d->m_chartByDefault = e.attribute(d->getAttrName(Report::Attribute::ChartByDefault), "0").toUInt(); d->m_logYaxis = e.attribute(d->getAttrName(Report::Attribute::LogYAxis), "0").toUInt(); d->m_chartLineWidth = e.attribute(d->getAttrName(Report::Attribute::ChartLineWidth), QString(m_lineWidth)).toUInt(); // read range tab i = kColumnTypeText.indexOf(e.attribute(d->getAttrName(Report::Attribute::ColumnType))); if (i != -1) setColumnType(static_cast(i)); else setColumnType(eMonths); i = kDataLockText.indexOf(e.attribute(d->getAttrName(Report::Attribute::DataLock))); if (i != -1) setDataFilter(static_cast(i)); else setDataFilter(MyMoneyReport::automatic); d->m_dataRangeStart = e.attribute(d->getAttrName(Report::Attribute::DataRangeStart), "0"); d->m_dataRangeEnd= e.attribute(d->getAttrName(Report::Attribute::DataRangeEnd), "0"); d->m_dataMajorTick = e.attribute(d->getAttrName(Report::Attribute::DataMajorTick), "0"); d->m_dataMinorTick = e.attribute(d->getAttrName(Report::Attribute::DataMinorTick), "0"); d->m_yLabelsPrecision = e.attribute(d->getAttrName(Report::Attribute::YLabelsPrecision), "2").toUInt(); } else if (d->m_reportType == eQueryTable) { // read rows/columns tab i = kRowTypeText.indexOf(e.attribute(d->getAttrName(Report::Attribute::RowType))); if (i != -1) setRowType(static_cast(i)); else setRowType(eAccount); unsigned qc = 0; QStringList columns = e.attribute(d->getAttrName(Report::Attribute::QueryColumns), "none").split(','); foreach (const auto column, columns) { i = kQueryColumnsText.indexOf(column); if (i > 0) qc |= (1 << (i - 1)); } setQueryColumns(static_cast(qc)); d->m_tax = e.attribute(d->getAttrName(Report::Attribute::Tax), "0").toUInt(); d->m_investments = e.attribute(d->getAttrName(Report::Attribute::Investments), "0").toUInt(); d->m_loans = e.attribute(d->getAttrName(Report::Attribute::Loans), "0").toUInt(); d->m_hideTransactions = e.attribute(d->getAttrName(Report::Attribute::HideTransactions), "0").toUInt(); d->m_showColumnTotals = e.attribute(d->getAttrName(Report::Attribute::ShowColumnTotals), "1").toUInt(); d->m_detailLevel = kDetailLevelText.indexOf(e.attribute(d->getAttrName(Report::Attribute::Detail), "none")) == eDetailAll ? eDetailAll : eDetailNone; // read performance or capital gains tab if (d->m_queryColumns & eQCperformance) d->m_investmentSum = static_cast(e.attribute(d->getAttrName(Report::Attribute::InvestmentSum), QString().setNum(MyMoneyReport::eSumPeriod)).toInt()); // read capital gains tab if (d->m_queryColumns & eQCcapitalgain) { d->m_investmentSum = static_cast(e.attribute(d->getAttrName(Report::Attribute::InvestmentSum), QString().setNum(MyMoneyReport::eSumSold)).toInt()); if (d->m_investmentSum == MyMoneyReport::eSumSold) { d->m_showSTLTCapitalGains = e.attribute(d->getAttrName(Report::Attribute::ShowSTLTCapitalGains), "0").toUInt(); d->m_settlementPeriod = e.attribute(d->getAttrName(Report::Attribute::SettlementPeriod), "3").toUInt(); d->m_tseparator = QDate::fromString(e.attribute(d->getAttrName(Report::Attribute::TermsSeparator), QDate::currentDate().addYears(-1).toString(Qt::ISODate)),Qt::ISODate); } } } else if (d->m_reportType == eInfoTable) { if (e.hasAttribute(d->getAttrName(Report::Attribute::ShowRowTotals))) d->m_showRowTotals = e.attribute(d->getAttrName(Report::Attribute::ShowRowTotals)).toUInt(); else d->m_showRowTotals = true; } QDomNode child = e.firstChild(); while (!child.isNull() && child.isElement()) { QDomElement c = child.toElement(); if (d->getElName(Report::Element::Text) == c.tagName() && c.hasAttribute(d->getAttrName(Report::Attribute::Pattern))) { setTextFilter(QRegExp(c.attribute(d->getAttrName(Report::Attribute::Pattern)), c.attribute(d->getAttrName(Report::Attribute::CaseSensitive), "1").toUInt() ? Qt::CaseSensitive : Qt::CaseInsensitive, c.attribute(d->getAttrName(Report::Attribute::RegEx), "1").toUInt() ? QRegExp::Wildcard : QRegExp::RegExp), c.attribute(d->getAttrName(Report::Attribute::InvertText), "0").toUInt()); } if (d->getElName(Report::Element::Type) == c.tagName() && c.hasAttribute(d->getAttrName(Report::Attribute::Type))) { i = kTypeText.indexOf(c.attribute(d->getAttrName(Report::Attribute::Type))); if (i != -1) addType(i); } if (d->getElName(Report::Element::State) == c.tagName() && c.hasAttribute(d->getAttrName(Report::Attribute::State))) { i = kStateText.indexOf(c.attribute(d->getAttrName(Report::Attribute::State))); if (i != -1) addState(i); } if (d->getElName(Report::Element::Number) == c.tagName()) setNumberFilter(c.attribute(d->getAttrName(Report::Attribute::From)), c.attribute(d->getAttrName(Report::Attribute::To))); if (d->getElName(Report::Element::Amount) == c.tagName()) setAmountFilter(MyMoneyMoney(c.attribute(d->getAttrName(Report::Attribute::From), "0/100")), MyMoneyMoney(c.attribute(d->getAttrName(Report::Attribute::To), "0/100"))); if (d->getElName(Report::Element::Dates) == c.tagName()) { QDate from, to; if (c.hasAttribute(d->getAttrName(Report::Attribute::From))) from = QDate::fromString(c.attribute(d->getAttrName(Report::Attribute::From)), Qt::ISODate); if (c.hasAttribute(d->getAttrName(Report::Attribute::To))) to = QDate::fromString(c.attribute(d->getAttrName(Report::Attribute::To)), Qt::ISODate); MyMoneyTransactionFilter::setDateFilter(from, to); } if (d->getElName(Report::Element::Payee) == c.tagName()) addPayee(c.attribute(d->getAttrName(Report::Attribute::ID))); if (d->getElName(Report::Element::Tag) == c.tagName()) addTag(c.attribute(d->getAttrName(Report::Attribute::ID))); if (d->getElName(Report::Element::Category) == c.tagName() && c.hasAttribute(d->getAttrName(Report::Attribute::ID))) addCategory(c.attribute(d->getAttrName(Report::Attribute::ID))); if (d->getElName(Report::Element::Account) == c.tagName() && c.hasAttribute(d->getAttrName(Report::Attribute::ID))) addAccount(c.attribute(d->getAttrName(Report::Attribute::ID))); if (d->getElName(Report::Element::AccountGroup) == c.tagName() && c.hasAttribute(d->getAttrName(Report::Attribute::Group))) { i = kAccountTypeText.indexOf(c.attribute(d->getAttrName(Report::Attribute::Group))); if (i != -1) addAccountGroup(static_cast(i)); } child = child.nextSibling(); } return true; } void MyMoneyReport::writeXML(QDomDocument& document, QDomElement& parent) const { QDomElement el = document.createElement(nodeNames[nnReport]); write(el, &document, false); parent.appendChild(el); } bool MyMoneyReport::hasReferenceTo(const QString& id) const { QStringList list; // collect all ids accounts(list); categories(list); payees(list); tags(list); - return (list.contains(id) > 0); + return list.contains(id); } int MyMoneyReport::m_lineWidth = 2; bool MyMoneyReport::m_expertMode = false; void MyMoneyReport::setLineWidth(int width) { m_lineWidth = width; } void MyMoneyReport::setExpertMode(bool expertMode) { m_expertMode = expertMode; } QString MyMoneyReport::toString(ERowType type) { switch(type) { case eNoRows : return "eNoRows"; case eAssetLiability : return "eAssetLiability"; case eExpenseIncome : return "eExpenseIncome"; case eCategory : return "eCategory"; case eTopCategory : return "eTopCategory"; case eAccount : return "eAccount"; case eTag : return "eTag"; case ePayee : return "ePayee"; case eMonth : return "eMonth"; case eWeek : return "eWeek"; case eTopAccount : return "eTopAccount"; case eAccountByTopAccount: return "eAccountByTopAccount"; case eEquityType : return "eEquityType"; case eAccountType : return "eAccountType"; case eInstitution : return "eInstitution"; case eBudget : return "eBudget"; case eBudgetActual : return "eBudgetActual"; case eSchedule : return "eSchedule"; case eAccountInfo : return "eAccountInfo"; case eAccountLoanInfo : return "eAccountLoanInfo"; case eAccountReconcile : return "eAccountReconcile"; case eCashFlow : return "eCashFlow"; default : return "undefined"; } } QString MyMoneyReport::toString(MyMoneyReport::EReportType type) { switch(type) { case eNoReport: return "eNoReport"; case ePivotTable: return "ePivotTable"; case eQueryTable: return "eQueryTable"; case eInfoTable: return "eInfoTable"; default: return "undefined"; } } diff --git a/kmymoney/pluginloader.cpp b/kmymoney/pluginloader.cpp index f7ed7aea9..81f108a8d 100644 --- a/kmymoney/pluginloader.cpp +++ b/kmymoney/pluginloader.cpp @@ -1,153 +1,153 @@ /*************************************************************************** pluginloader.cpp ------------------- - (C) 2017 by Łukasz Wojniłowicz + Copyright (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 "pluginloader.h" // ---------------------------------------------------------------------------- // QT Includes // ---------------------------------------------------------------------------- // KDE Includes #include #include #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "kmymoneyplugin.h" #include "onlinepluginextended.h" namespace KMyMoneyPlugin { Category pluginCategory(const KPluginMetaData& pluginInfo) { if (!pluginInfo.serviceTypes().contains(QStringLiteral("KMyMoney/Plugin"))) { auto jsonKMyMoneyData = pluginInfo.rawData()[QLatin1String("KMyMoney")].toObject(); if (!jsonKMyMoneyData[QLatin1String("OnlineTask")].isNull()) return OnlineBankOperations; else if (!jsonKMyMoneyData[QLatin1String("PayeeIdentifier")].isNull()) return PayeeIdentifier; } return StandardPlugin; } bool isPluginEnabled(const KPluginMetaData& pluginData, const KConfigGroup& pluginSection) { return pluginSection.readEntry(QString::fromLatin1("%1Enabled"). // we search here for e.g. "csvimporterEnabled = true" arg(pluginData.pluginId()), pluginData.isEnabledByDefault()); // if not found, then get default from plugin's json file } QMap listPlugins(bool onlyEnabled) { QMap plugins; const auto pluginDatas = KPluginLoader::findPlugins(QStringLiteral("kmymoney")); // that means search for plugins in "/lib64/plugins/kmymoney/" const auto pluginSection(KSharedConfig::openConfig()->group(QStringLiteral("Plugins"))); // section of config where plugin on/off were saved for (const KPluginMetaData& pluginData : pluginDatas) { if (pluginData.serviceTypes().contains(QStringLiteral("KMyMoney/Plugin"))) { if (!onlyEnabled || (onlyEnabled && isPluginEnabled(pluginData, pluginSection))) { plugins.insert(pluginData.pluginId(), pluginData); } } } return plugins; } void pluginHandling(Action action, Container& ctnPlugins, QObject* parent, KXMLGUIFactory* guiFactory) { QMap referencePluginDatas; if (action == Action::Load || action == Action::Reorganize) referencePluginDatas = listPlugins(true); if (action == Action::Unload || action == Action::Reorganize) { auto& plugins = ctnPlugins.standard; auto& refPlugins = referencePluginDatas; for (auto it = plugins.begin(); it != plugins.end();) { if (!refPlugins.contains(it.key())) { ctnPlugins.online.remove(it.key()); ctnPlugins.extended.remove(it.key()); ctnPlugins.importer.remove(it.key()); ctnPlugins.storage.remove(it.key()); guiFactory->removeClient(it.value()); it.value()->unplug(); delete it.value(); it = plugins.erase(it); continue; } ++it; } } if (action == Action::Load || action == Action::Reorganize) { auto& refPlugins = referencePluginDatas; for (auto it = refPlugins.cbegin(); it != refPlugins.cend(); ++it) { if (!ctnPlugins.standard.contains(it.key())) { KPluginLoader loader((*it).fileName()); auto factory = loader.factory(); if (!factory) { qWarning("Could not load plugin '%s', error: %s", qPrintable((*it).fileName()), qPrintable(loader.errorString())); loader.unload(); continue; } Plugin* plugin = factory->create(parent); if (!plugin) { qWarning("This is not KMyMoney plugin: '%s'", qPrintable((*it).fileName())); loader.unload(); continue; } ctnPlugins.standard.insert((*it).pluginId(), plugin); plugin->plug(); guiFactory->addClient(plugin); auto IOnline = qobject_cast(plugin); if (IOnline) ctnPlugins.online.insert((*it).pluginId(), IOnline); auto IExtended = qobject_cast(plugin); if (IExtended ) ctnPlugins.extended.insert((*it).pluginId(), IExtended ); auto IImporter = qobject_cast(plugin); if (IImporter) ctnPlugins.importer.insert((*it).pluginId(), IImporter); auto IStorage = qobject_cast(plugin); if (IStorage) ctnPlugins.storage.insert((*it).pluginId(), IStorage); } } } } } diff --git a/kmymoney/plugins/kmymoneyplugin.cpp b/kmymoney/plugins/kmymoneyplugin.cpp index 77e71ffee..15d16e203 100644 --- a/kmymoney/plugins/kmymoneyplugin.cpp +++ b/kmymoney/plugins/kmymoneyplugin.cpp @@ -1,110 +1,115 @@ /*************************************************************************** kmymoneyplugin.cpp ------------------- begin : Wed Jan 5 2005 copyright : (C) 2005 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 "kmymoneyplugin.h" // ---------------------------------------------------------------------------- // QT Includes // ---------------------------------------------------------------------------- // KDE Includes #include #include // ---------------------------------------------------------------------------- // Project Includes #include "interfaceloader.h" KMyMoneyPlugin::Plugin::Plugin(QObject* parent, const char* name) : QObject(), KXMLGUIClient() { Q_UNUSED(parent) setObjectName(name); } KMyMoneyPlugin::Plugin::~Plugin() { } void KMyMoneyPlugin::Plugin::plug() { } void KMyMoneyPlugin::Plugin::unplug() { } void KMyMoneyPlugin::Plugin::configurationChanged() { } KToggleAction* KMyMoneyPlugin::Plugin::toggleAction(const QString& actionName) const { static KToggleAction dummyAction(QString("Dummy"), 0); KToggleAction* p = dynamic_cast(actionCollection()->action(QString(actionName.toLatin1()))); if (!p) { qWarning("Action '%s' is not of type KToggleAction", qPrintable(actionName)); p = &dummyAction; } qWarning("Action with name '%s' not found!", qPrintable(actionName)); return p; } KMyMoneyPlugin::OnlinePlugin::OnlinePlugin() { } KMyMoneyPlugin::OnlinePlugin::~OnlinePlugin() { } KMyMoneyPlugin::AppInterface* KMyMoneyPlugin::Plugin::appInterface() const { Q_CHECK_PTR(KMyMoneyPlugin::pluginInterfaces().appInterface); return KMyMoneyPlugin::pluginInterfaces().appInterface; } KMyMoneyPlugin::ViewInterface* KMyMoneyPlugin::Plugin::viewInterface() const { Q_CHECK_PTR(KMyMoneyPlugin::pluginInterfaces().viewInterface); return KMyMoneyPlugin::pluginInterfaces().viewInterface; } KMyMoneyPlugin::StatementInterface* KMyMoneyPlugin::Plugin::statementInterface() const { Q_CHECK_PTR(KMyMoneyPlugin::pluginInterfaces().statementInterface); return KMyMoneyPlugin::pluginInterfaces().statementInterface; } KMyMoneyPlugin::ImportInterface* KMyMoneyPlugin::Plugin::importInterface() const { Q_CHECK_PTR(KMyMoneyPlugin::pluginInterfaces().importInterface); return KMyMoneyPlugin::pluginInterfaces().importInterface; } KMyMoneyPlugin::ImporterPlugin::ImporterPlugin() { } KMyMoneyPlugin::ImporterPlugin::~ImporterPlugin() { } + +IMyMoneyOperationsFormat* KMyMoneyPlugin::StoragePlugin::reader() +{ + return nullptr; +} diff --git a/kmymoney/plugins/kmymoneyplugin.h b/kmymoney/plugins/kmymoneyplugin.h index 2e4fda96d..09d88ce17 100644 --- a/kmymoney/plugins/kmymoneyplugin.h +++ b/kmymoney/plugins/kmymoneyplugin.h @@ -1,332 +1,332 @@ /* * This file is part of KMyMoney, A Personal Finance Manager by KDE * Copyright (C) 2005 Thomas Baumgart * 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 KMYMONEYPLUGIN_H #define KMYMONEYPLUGIN_H #include // ---------------------------------------------------------------------------- // QT Includes #include // ---------------------------------------------------------------------------- // KDE Includes #include class KToggleAction; // ---------------------------------------------------------------------------- // Project Includes #include "mymoneykeyvaluecontainer.h" class MyMoneyStorageMgr; class MyMoneyAccount; class KMyMoneySettings; class IMyMoneyOperationsFormat; namespace KMyMoneyPlugin { class AppInterface; } namespace KMyMoneyPlugin { class ImportInterface; } namespace KMyMoneyPlugin { class StatementInterface; } namespace KMyMoneyPlugin { class ViewInterface; } /** * @defgroup KMyMoneyPlugin * * KMyMoney knows several types of plugins. The most common and generic one is KMyMoneyPlugin::Plugin. * * Another group of plugins are just loaded on demand and offer special functions with a tight integration into KMyMoney. Whenever possible you should use this kind of plugins. * At the moment this are the onlineTask and payeeIdentifierData. * * @{ */ namespace KMyMoneyPlugin { /** * This class describes the interface between KMyMoney and it's plugins. * * The plugins are based on Qt 5's plugin system. So you must compile json information into the plugin. * KMyMoney looks into the folder "${PLUGIN_INSTALL_DIR}/kmymoney/" and loads all plugins found there (if the user did not deactivate the plugin). * * The json header of the plugin must comply with the requirements of KCoreAddon's KPluginMetaData class. * To load the plugin at start up the service type "KMyMoney/Plugin" must be set. * * @warning The plugin system for KMyMoney 5 is still in development. Especially the loading of the on-demand plugins (mainly undocumented :( ) will change. * * A basic json header is shown below. * @code{.json} { "KPlugin": { "Authors": [ { "Name": "Author's Names, Second Author", "Email": "E-Mail 1, E-Mail 2" } ], "Description": "Short description for plugin list (translateable)", "EnabledByDefault": true, "Icon": "icon to be shown in plugin list", "Id": "a unique identifier", "License": "see KPluginMetaData for list of predefined licenses (translateable)", "Name": "Name of the plugin (translateable)", "ServiceTypes": [ "KMyMoney/Plugin" ], "Version": "@PROJECT_VERSION@@PROJECT_VERSION_SUFFIX@", } } * @endcode * * This example assumes you are using * @code{.cmake} configure_file(${CMAKE_CURRENT_SOURCE_DIR}/... ${CMAKE_CURRENT_BINARY_DIR}/... @ONLY) @endcode * to replace the version variables using cmake. * * @see http://doc.qt.io/qt-5/plugins-howto.html * @see https://api.kde.org/frameworks/kcoreaddons/html/classKPluginMetaData.html * */ class KMM_PLUGIN_EXPORT Plugin : public QObject, public KXMLGUIClient { Q_OBJECT public: explicit Plugin(QObject* parent = nullptr, const char* name = ""); virtual ~Plugin(); public Q_SLOTS: /** * @brief Called during plug in process */ virtual void plug(); /** * @brief Called before unloading */ virtual void unplug(); /** * @brief Called if the configuration of the plugin was changed * @todo Implement */ virtual void configurationChanged() ; protected: /** See KMyMoneyApp::toggleAction() for a description */ KToggleAction* toggleAction(const QString& name) const; // define interface classes here. The interface classes provide a mechanism // for the plugin to interact with KMyMoney // they are defined in the following form for an interface // named Xxx: // // XxxInterface* xxxInterface(); AppInterface* appInterface() const; ViewInterface* viewInterface() const; StatementInterface* statementInterface() const; ImportInterface* importInterface() const; }; /** * This class describes the interface between the KMyMoney * application and it's ONLINE-BANKING plugins. All online banking plugins * must provide this interface. * * A good tutorial on how to design and develop a plugin * structure for a KDE application (e.g. KMyMoney) can be found at * http://developer.kde.org/documentation/tutorials/developing-a-plugin-structure/index.html * */ class KMM_PLUGIN_EXPORT OnlinePlugin { public: OnlinePlugin(); virtual ~OnlinePlugin(); virtual void protocols(QStringList& protocolList) const = 0; /** * This method returns a pointer to a widget representing an additional * tab that will be added to the KNewAccountDlg. The string referenced * with @a tabName will be filled with the text that should be placed * on the tab. It should return 0 if no additional tab is needed. * * Information about the account can be taken out of @a account. * * Once the pointer to the widget is returned to KMyMoney, it takes care * of destruction of all included widgets when the dialog is closed. The plugin * can access the widgets created after the call to storeConfigParameters() * happened. */ virtual QWidget* accountConfigTab(const MyMoneyAccount& account, QString& tabName) = 0; /** * This method is called by the framework whenever it is time to store * the configuration data maintained by the plugin. The plugin should use * the widgets created in accountConfigTab() to extract the current values. * * @param current The @a current container contains the current settings */ virtual MyMoneyKeyValueContainer onlineBankingSettings(const MyMoneyKeyValueContainer& current) = 0; /** * This method is called by the framework when the user wants to map * a KMyMoney account onto an online account. The KMyMoney account is identified * by @a acc and the online provider should store its data in @a onlineBankingSettings * upon success. * * @retval true if account is mapped * @retval false if account is not mapped */ virtual bool mapAccount(const MyMoneyAccount& acc, MyMoneyKeyValueContainer& onlineBankingSettings) = 0; /** * This method is called by the framework when the user wants to update * a KMyMoney account with data from an online account. The KMyMoney account is identified * by @a acc. The online provider should read its data from acc.onlineBankingSettings(). * @a true is returned upon success. The plugin might consider to stack the requests * in case @a moreAccounts is @p true. @a moreAccounts defaults to @p false. * * @retval true if account is updated * @retval false if account is not updated */ virtual bool updateAccount(const MyMoneyAccount& acc, bool moreAccounts = false) = 0; }; /** * This class describes the interface between the KMyMoney * application and it's IMPORTER plugins. All importer plugins * must provide this interface. * * A good tutorial on how to design and develop a plugin * structure for a KDE application (e.g. KMyMoney) can be found at * http://developer.kde.org/documentation/tutorials/developing-a-plugin-structure/index.html * */ class KMM_PLUGIN_EXPORT ImporterPlugin { public: ImporterPlugin(); virtual ~ImporterPlugin(); /** * This method returns the english-language name of the format * this plugin imports, e.g. "OFX" * * @return QString Name of the format */ virtual QString formatName() const = 0; /** * This method returns the filename filter suitable for passing to * KFileDialog::setFilter(), e.g. "*.ofx *.qfx" which describes how * files of this format are likely to be named in the file system * * @return QString Filename filter string */ virtual QString formatFilenameFilter() const = 0; /** * This method returns whether this plugin is able to import * a particular file. * * @param filename Fully-qualified pathname to a file * * @return bool Whether the indicated file is importable by this plugin */ virtual bool isMyFormat(const QString& filename) const = 0; /** * Import a file * * @param filename File to import * * @return bool Whether the import was successful. */ virtual bool import(const QString& filename) = 0; /** * Returns the error result of the last import * * @return QString English-language name of the error encountered in the * last import, or QString() if it was successful. * */ virtual QString lastError() const = 0; }; /** * This class describes the interface between the KMyMoney * application and it's STORAGE plugins. All storage plugins * must provide this interface. * */ class KMM_PLUGIN_EXPORT StoragePlugin { public: StoragePlugin() = default; virtual ~StoragePlugin() = default; /** * @brief Loads file into storage * @param storage Storage manager for the file * @param url URL of the file * @return true if successfully opened */ virtual bool open(MyMoneyStorageMgr *storage, const QUrl &url) = 0; /** * @brief Saves storage into file * @param url URL of the file * @return true if successfully saved */ virtual bool save(const QUrl &url) = 0; /** * @brief Returns storage reader * @return storage reader */ - virtual IMyMoneyOperationsFormat* reader(){ return nullptr; } + virtual IMyMoneyOperationsFormat* reader(); /** * @brief Storage identifier * @return Storage identifier */ virtual QString formatName() const = 0; virtual QString fileExtension() const = 0; }; } // end of namespace Q_DECLARE_INTERFACE(KMyMoneyPlugin::OnlinePlugin, "org.kmymoney.plugin.onlineplugin") Q_DECLARE_INTERFACE(KMyMoneyPlugin::ImporterPlugin, "org.kmymoney.plugin.importerplugin") Q_DECLARE_INTERFACE(KMyMoneyPlugin::StoragePlugin, "org.kmymoney.plugin.storageplugin") /** @} */ #endif diff --git a/kmymoney/plugins/qif/export/kexportdlg.cpp b/kmymoney/plugins/qif/export/kexportdlg.cpp index e6ce3e6f2..1a4ac0048 100644 --- a/kmymoney/plugins/qif/export/kexportdlg.cpp +++ b/kmymoney/plugins/qif/export/kexportdlg.cpp @@ -1,229 +1,229 @@ /*************************************************************************** kexportdlg.cpp - description ------------------- begin : Tue May 22 2001 copyright : (C) 2001 by Michael Edwardes email : mte@users.sourceforge.net Javier Campos Morales Felix Rodriguez ***************************************************************************/ /*************************************************************************** * * * 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 "kexportdlg.h" // ---------------------------------------------------------------------------- // QT Headers #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Headers #include #include #include #include #include #include // ---------------------------------------------------------------------------- // Project Headers #include "mymoneycategory.h" #include "mymoneyfile.h" #include "mymoneyaccount.h" #include "mymoneytransaction.h" #include "mymoneytransactionfilter.h" #include "kmymoneyaccountcombo.h" #include "kmymoneyutils.h" #include "models.h" #include "accountsmodel.h" #include #include "mymoneyenums.h" #include "modelenums.h" using namespace Icons; KExportDlg::KExportDlg(QWidget *parent) : KExportDlgDecl(parent) { // Set (almost) all the last used options readConfig(); loadProfiles(true); loadAccounts(); // load button icons KGuiItem::assign(m_qbuttonCancel, KStandardGuiItem::cancel()); KGuiItem okButtenItem(i18n("&Export"), Icons::get(Icon::DocumentExport), i18n("Start operation"), i18n("Use this to start the export operation")); KGuiItem::assign(m_qbuttonOk, okButtenItem); KGuiItem browseButtenItem(i18n("&Browse..."), Icons::get(Icon::DocumentOpen), i18n("Select filename"), i18n("Use this to select a filename to export to")); KGuiItem::assign(m_qbuttonBrowse, browseButtenItem); // connect the buttons to their functionality connect(m_qbuttonBrowse, &QAbstractButton::clicked, this, &KExportDlg::slotBrowse); connect(m_qbuttonOk, &QAbstractButton::clicked, this, &KExportDlg::slotOkClicked); connect(m_qbuttonCancel, &QAbstractButton::clicked, this, &QDialog::reject); // connect the change signals to the check slot and perform initial check connect(m_qlineeditFile, SIGNAL(textChanged(QString)), this, SLOT(checkData())); connect(m_qcheckboxAccount, SIGNAL(toggled(bool)), this, SLOT(checkData())); connect(m_qcheckboxCategories, SIGNAL(toggled(bool)), this, SLOT(checkData())); connect(m_accountComboBox, SIGNAL(accountSelected(QString)), this, SLOT(checkData(QString))); connect(m_profileComboBox, SIGNAL(activated(int)), this, SLOT(checkData())); connect(m_kmymoneydateStart, SIGNAL(dateChanged(QDate)), this, SLOT(checkData())); connect(m_kmymoneydateEnd, SIGNAL(dateChanged(QDate)), this, SLOT(checkData())); checkData(QString()); } KExportDlg::~KExportDlg() { } void KExportDlg::slotBrowse() { auto newName(QFileDialog::getSaveFileName(this, QString(), QString(), QLatin1String("*.QIF"))); if (!newName.endsWith(QLatin1String(".qif"), Qt::CaseInsensitive)) newName.append(QLatin1String(".qif")); if (!newName.isEmpty()) m_qlineeditFile->setText(newName); } void KExportDlg::loadProfiles(const bool selectLast) { QString current = m_profileComboBox->currentText(); m_profileComboBox->clear(); QStringList list; KSharedConfigPtr config = KSharedConfig::openConfig(); KConfigGroup grp = config->group("Profiles"); list = grp.readEntry("profiles", QStringList()); list.sort(); m_profileComboBox->insertItems(0, list); if (selectLast == true) { grp = config->group("Last Use Settings"); current = grp.readEntry("KExportDlg_LastProfile"); } m_profileComboBox->setCurrentItem(0); - if (list.contains(current) > 0) + if (list.contains(current)) m_profileComboBox->setCurrentIndex(m_profileComboBox->findText(current, Qt::MatchExactly)); } void KExportDlg::slotOkClicked() { // Make sure we save the last used settings for use next time, writeConfig(); accept(); } void KExportDlg::readConfig() { KSharedConfigPtr kconfig = KSharedConfig::openConfig(); KConfigGroup kgrp = kconfig->group("Last Use Settings"); m_qlineeditFile->setText(kgrp.readEntry("KExportDlg_LastFile")); m_qcheckboxAccount->setChecked(kgrp.readEntry("KExportDlg_AccountOpt", true)); m_qcheckboxCategories->setChecked(kgrp.readEntry("KExportDlg_CatOpt", true)); m_kmymoneydateStart->setDate(kgrp.readEntry("KExportDlg_StartDate", QDate())); m_kmymoneydateEnd->setDate(kgrp.readEntry("KExportDlg_EndDate", QDate())); // m_profileComboBox is loaded in loadProfiles(), so we don't worry here // m_accountComboBox is loaded in loadAccounts(), so we don't worry here } void KExportDlg::writeConfig() { KSharedConfigPtr kconfig = KSharedConfig::openConfig(); KConfigGroup grp = kconfig->group("Last Use Settings"); grp.writeEntry("KExportDlg_LastFile", m_qlineeditFile->text()); grp.writeEntry("KExportDlg_AccountOpt", m_qcheckboxAccount->isChecked()); grp.writeEntry("KExportDlg_CatOpt", m_qcheckboxCategories->isChecked()); grp.writeEntry("KExportDlg_StartDate", QDateTime(m_kmymoneydateStart->date())); grp.writeEntry("KExportDlg_EndDate", QDateTime(m_kmymoneydateEnd->date())); grp.writeEntry("KExportDlg_LastProfile", m_profileComboBox->currentText()); kconfig->sync(); } void KExportDlg::checkData(const QString& accountId) { bool okEnabled = false; if (!m_qlineeditFile->text().isEmpty()) { auto strFile(m_qlineeditFile->text()); if (!strFile.endsWith(QLatin1String(".qif"), Qt::CaseInsensitive)) strFile.append(QLatin1String(".qif")); m_qlineeditFile->setText(strFile); } MyMoneyAccount account; if (!accountId.isEmpty()) { MyMoneyFile* file = MyMoneyFile::instance(); account = file->account(accountId); if (m_lastAccount != accountId) { MyMoneyTransactionFilter filter(accountId); QList list = file->transactionList(filter); QList::Iterator it; if (!list.isEmpty()) { it = list.begin(); m_kmymoneydateStart->loadDate((*it).postDate()); it = list.end(); --it; m_kmymoneydateEnd->loadDate((*it).postDate()); } m_lastAccount = accountId; m_accountComboBox->setSelected(account.id()); } } if (!m_qlineeditFile->text().isEmpty() && !m_accountComboBox->getSelected().isEmpty() && !m_profileComboBox->currentText().isEmpty() && m_kmymoneydateStart->date() <= m_kmymoneydateEnd->date() && (m_qcheckboxAccount->isChecked() || m_qcheckboxCategories->isChecked())) okEnabled = true; m_qbuttonOk->setEnabled(okEnabled); } void KExportDlg::loadAccounts() { auto filterProxyModel = new AccountNamesFilterProxyModel(this); filterProxyModel->addAccountGroup(QVector {eMyMoney::Account::Type::Asset, eMyMoney::Account::Type::Liability}); auto const model = Models::instance()->accountsModel(); model->load(); filterProxyModel->setSourceColumns(model->getColumns()); filterProxyModel->setSourceModel(model); filterProxyModel->sort((int)eAccountsModel::Column::Account); m_accountComboBox->setModel(filterProxyModel); } QString KExportDlg::accountId() const { return m_lastAccount; } diff --git a/kmymoney/plugins/sql/payeeidentifier/nationalaccount/nationalaccountstorageplugin.cpp b/kmymoney/plugins/sql/payeeidentifier/nationalaccount/nationalaccountstorageplugin.cpp index 95fb650d6..34baf719a 100644 --- a/kmymoney/plugins/sql/payeeidentifier/nationalaccount/nationalaccountstorageplugin.cpp +++ b/kmymoney/plugins/sql/payeeidentifier/nationalaccount/nationalaccountstorageplugin.cpp @@ -1,103 +1,103 @@ /* * 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 . */ #include "nationalaccountstorageplugin.h" #include #include #include +#include #include "kmymoneystorageplugin.h" -/*K_PLUGIN_FACTORY(nationalAccountStoragePluginFactory, - registerPlugin(); - )*/ -K_EXPORT_PLUGIN(nationalAccountStoragePluginFactory("ibanBicStoragePlugin")) +K_PLUGIN_FACTORY_WITH_JSON(nationalAccountStoragePluginFactory, "kmymoney-nationalaccountnumberplugin.json", registerPlugin();) QString nationalAccountStoragePlugin::iid() { return QLatin1String("org.kmymoney.payeeIdentifier.nationalAccount.sqlStoragePlugin"); } nationalAccountStoragePlugin::nationalAccountStoragePlugin(QObject* parent, const QVariantList& options) : storagePlugin(parent, options) { } /** @todo to be implemented */ bool nationalAccountStoragePlugin::removePluginData(QSqlDatabase connection) { Q_UNUSED(connection); return false; } bool nationalAccountStoragePlugin::setupDatabase(QSqlDatabase connection) { // Get current version QSqlQuery query = QSqlQuery(connection); query.prepare("SELECT versionMajor FROM kmmPluginInfo WHERE iid = ?"); query.bindValue(0, iid()); if (!query.exec()) { qWarning("Could not execute query for nationalAccountStoragePlugin: %s", qPrintable(query.lastError().text())); return false; } int currentVersion = 0; if (query.next()) currentVersion = query.value(0).toInt(); // Create database in it's most recent version if version is 0 // (version 0 means the database was not installed) if (currentVersion == 0) { // If the database is recreated the table may be still there. So drop it if needed. No error handling needed // as this step is not necessary - only the creation is important. query.exec("DROP TABLE IF EXISTS kmmNationalAccountNumber;"); if (!query.exec( "CREATE TABLE kmmNationalAccountNumber (" " id varchar(32) NOT NULL PRIMARY KEY REFERENCES kmmPayeeIdentifier( id ) ON DELETE CASCADE ON UPDATE CASCADE," " countryCode varchar(3)," " accountNumber TEXT," " bankCode TEXT," " name TEXT" " );" )) { qWarning("Could not create table for nationalAccountStoragePlugin: %s", qPrintable(query.lastError().text())); return false; } query.prepare("INSERT INTO kmmPluginInfo (iid, versionMajor, versionMinor, uninstallQuery) VALUES(?, ?, ?, ?)"); query.bindValue(0, iid()); query.bindValue(1, 1); query.bindValue(2, 0); query.bindValue(3, "DROP TABLE kmmNationalAccountNumber;"); if (query.exec()) return true; qWarning("Could not save plugin info for nationalAccountStoragePlugin (%s): %s", qPrintable(iid()), qPrintable(query.lastError().text())); return false; } // Check if version is valid with this plugin switch (currentVersion) { case 1: return true; } return false; } + +#include "nationalaccountstorageplugin.moc" diff --git a/kmymoney/views/kaccountsview_p.h b/kmymoney/views/kaccountsview_p.h index 225628e5a..f12b74096 100644 --- a/kmymoney/views/kaccountsview_p.h +++ b/kmymoney/views/kaccountsview_p.h @@ -1,321 +1,321 @@ /*************************************************************************** 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 // ---------------------------------------------------------------------------- // 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::objectSelected, q, &KAccountsView::objectSelected); 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()); - } catch (const MyMoneyException &e) { + } 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 25c551c1e..955375454 100644 --- a/kmymoney/views/kgloballedgerview.cpp +++ b/kmymoney/views/kgloballedgerview.cpp @@ -1,1971 +1,1970 @@ /*************************************************************************** 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 "kmymoneysettings.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} }; 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::setDefaultFocus() { Q_D(KGlobalLedgerView); QTimer::singleShot(0, d->m_registerSearchLine->searchLine(), SLOT(setFocus())); } 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 aboutToShow(View::Ledgers); 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()); } // emit objectSelected(d->m_currentAccount); KMyMoneyRegister::SelectedTransactions list(d->m_register); emit transactionsSelected(list); } // 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(eView::Schedules::Requester req) { // check if request has been made because of reconciliation if (req != eView::Schedules::Requester::Reconciliation) return; 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()) { emit switchViewRequested(View::Ledgers); 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 switchViewRequested(View::Ledgers); 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::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 objectSelected(d->m_currentAccount); } 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 contextMenuRequested(MyMoneyFile::instance()->schedule(transactions.first().scheduleId())); else emit transactionsContextMenuRequested(d->m_selectedTransactions); } 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::statusProgress); connect(d->m_transactionEditor.data(), &TransactionEditor::statusMsg, this, &KGlobalLedgerView::statusMsg); 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 statusProgress(0, cnt); 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 statusProgress(i++, 0); } 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 statusProgress(-1, -1); } } 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 statusProgress(0, cnt); 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 statusProgress(i++, 0); } emit statusProgress(-1, -1); 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(); emit transactionsSelected(d->m_selectedTransactions); } } 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); emit transactionsSelected(d->m_selectedTransactions); 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(); emit transactionsSelected(d->m_selectedTransactions); } } 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 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); emit transactionsSelected(list); } 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 enterOverdueSchedulesRequested(d->m_currentAccount, eView::Schedules::Requester::Reconciliation); // 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) { 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) { 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()); emit accountReconciled(d->m_reconciliationAccount, d->m_endingBalanceDlg->statementDate(), d->m_endingBalanceDlg->previousBalance(), d->m_endingBalanceDlg->endingBalance(), transactionList); } 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()); } diff --git a/kmymoney/views/kpayeesview_p.h b/kmymoney/views/kpayeesview_p.h index df3f52472..37b980510 100644 --- a/kmymoney/views/kpayeesview_p.h +++ b/kmymoney/views/kpayeesview_p.h @@ -1,759 +1,759 @@ /*************************************************************************** kpayeesview_p.h --------------- begin : Thu Jan 24 2002 copyright : (C) 2000-2002 by Michael Edwardes Javier Campos Morales Felix Rodriguez John C Thomas Baumgart Kevin Tambascio Andreas Nicolai (C) 2017 by Łukasz Wojniłowicz ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #ifndef KPAYEESVIEW_P_H #define KPAYEESVIEW_P_H #include "kpayeesview.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include #include "ui_kpayeesview.h" #include "kmymoneyviewbase_p.h" #include "kmymoneyutils.h" #include "kmymoneymvccombo.h" #include "mymoneypayee.h" #include "mymoneyexception.h" #include "mymoneyfile.h" #include "mymoneyaccount.h" #include "mymoneymoney.h" #include "mymoneytransactionfilter.h" #include "kmymoneysettings.h" #include "kpayeereassigndlg.h" #include "models.h" #include "accountsmodel.h" #include "mymoneysecurity.h" #include "mymoneyschedule.h" #include "mymoneycontact.h" #include "mymoneyaccountloan.h" #include "mymoneysplit.h" #include "mymoneyprice.h" #include "mymoneytransaction.h" #include "icons/icons.h" #include "transaction.h" #include "widgetenums.h" #include "mymoneyenums.h" #include "modelenums.h" #include "menuenums.h" using namespace Icons; // *** KPayeeListItem Implementation *** class KPayeeListItem : public QListWidgetItem { public: /** * Constructor to be used to construct a payee entry object. * * @param parent pointer to the QListWidget object this entry should be * added to. * @param payee const reference to MyMoneyPayee for which * the QListWidget entry is constructed */ explicit KPayeeListItem(QListWidget *parent, const MyMoneyPayee& payee) : QListWidgetItem(parent, QListWidgetItem::UserType), m_payee(payee) { setText(payee.name()); // allow in column rename setFlags(Qt::ItemIsEditable | Qt::ItemIsSelectable | Qt::ItemIsEnabled); } ~KPayeeListItem() { } const MyMoneyPayee& payee() const { return m_payee; } private: MyMoneyPayee m_payee; }; enum filterTypeE { eAllPayees = 0, eReferencedPayees = 1, eUnusedPayees = 2 }; class KPayeesViewPrivate : public KMyMoneyViewBasePrivate { Q_DECLARE_PUBLIC(KPayeesView) public: explicit KPayeesViewPrivate(KPayeesView *qq) : KMyMoneyViewBasePrivate(), q_ptr(qq), ui(new Ui::KPayeesView), m_needLoad(true), m_inSelection(false), m_allowEditing(true), m_payeeFilterType(0) { } ~KPayeesViewPrivate() { if(!m_needLoad) { // remember the splitter settings for startup auto grp = KSharedConfig::openConfig()->group("Last Use Settings"); grp.writeEntry("KPayeesViewSplitterSize", ui->m_splitter->saveState()); grp.sync(); } delete ui; } void init() { Q_Q(KPayeesView); m_needLoad = false; ui->setupUi(q); m_contact = new MyMoneyContact(q); m_filterProxyModel = new AccountNamesFilterProxyModel(q); m_filterProxyModel->setHideEquityAccounts(!KMyMoneySettings::expertMode()); m_filterProxyModel->addAccountGroup(QVector {eMyMoney::Account::Type::Asset, eMyMoney::Account::Type::Liability, eMyMoney::Account::Type::Income, eMyMoney::Account::Type::Expense, eMyMoney::Account::Type::Equity}); auto const model = Models::instance()->accountsModel(); m_filterProxyModel->setSourceModel(model); m_filterProxyModel->setSourceColumns(model->getColumns()); m_filterProxyModel->sort((int)eAccountsModel::Column::Account); ui->comboDefaultCategory->setModel(m_filterProxyModel); ui->matchTypeCombo->addItem(i18nc("@item No matching", "No matching"), MyMoneyPayee::matchDisabled); ui->matchTypeCombo->addItem(i18nc("@item Match Payees name partially", "Match Payees name (partial)"), MyMoneyPayee::matchName); ui->matchTypeCombo->addItem(i18nc("@item Match Payees name exactly", "Match Payees name (exact)"), MyMoneyPayee::matchNameExact); ui->matchTypeCombo->addItem(i18nc("@item Search match in list", "Match on a name listed below"), MyMoneyPayee::matchKey); // create the searchline widget // and insert it into the existing layout m_searchWidget = new KListWidgetSearchLine(q, ui->m_payeesList); m_searchWidget->setSizePolicy(QSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed)); ui->m_payeesList->setContextMenuPolicy(Qt::CustomContextMenu); ui->m_listTopHLayout->insertWidget(0, m_searchWidget); //load the filter type ui->m_filterBox->addItem(i18nc("@item Show all payees", "All")); ui->m_filterBox->addItem(i18nc("@item Show only used payees", "Used")); ui->m_filterBox->addItem(i18nc("@item Show only unused payees", "Unused")); ui->m_filterBox->setSizeAdjustPolicy(QComboBox::AdjustToContents); ui->m_newButton->setIcon(Icons::get(Icon::ListAddUser)); ui->m_renameButton->setIcon(Icons::get(Icon::UserProperties)); ui->m_deleteButton->setIcon(Icons::get(Icon::ListRemoveUser)); ui->m_mergeButton->setIcon(Icons::get(Icon::Merge)); ui->m_updateButton->setIcon(Icons::get(Icon::DialogOK)); ui->m_syncAddressbook->setIcon(Icons::get(Icon::Refresh)); ui->m_sendMail->setIcon(Icons::get(Icon::MailMessage)); ui->m_updateButton->setEnabled(false); ui->m_syncAddressbook->setEnabled(false); #ifndef KMM_ADDRESSBOOK_FOUND ui->m_syncAddressbook->hide(); #endif ui->matchTypeCombo->setCurrentIndex(0); ui->checkMatchIgnoreCase->setEnabled(false); ui->checkEnableDefaultCategory->setChecked(false); ui->labelDefaultCategory->setEnabled(false); ui->comboDefaultCategory->setEnabled(false); QList cols { eWidgets::eTransaction::Column::Date, eWidgets::eTransaction::Column::Account, eWidgets::eTransaction::Column::Detail, eWidgets::eTransaction::Column::ReconcileFlag, eWidgets::eTransaction::Column::Payment, eWidgets::eTransaction::Column::Deposit}; ui->m_register->setupRegister(MyMoneyAccount(), cols); ui->m_register->setSelectionMode(QTableWidget::SingleSelection); ui->m_register->setDetailsColumnType(eWidgets::eRegister::DetailColumn::AccountFirst); ui->m_balanceLabel->hide(); q->connect(m_contact, &MyMoneyContact::contactFetched, q, &KPayeesView::slotContactFetched); q->connect(ui->m_payeesList, static_cast(&QListWidget::currentItemChanged), q, static_cast(&KPayeesView::slotSelectPayee)); q->connect(ui->m_payeesList, &QListWidget::itemSelectionChanged, q, static_cast(&KPayeesView::slotSelectPayee)); q->connect(ui->m_payeesList, &QListWidget::itemDoubleClicked, q, &KPayeesView::slotStartRename); q->connect(ui->m_payeesList, &QListWidget::itemChanged, q, &KPayeesView::slotRenameSinglePayee); q->connect(ui->m_payeesList, &QWidget::customContextMenuRequested, q, &KPayeesView::slotShowPayeesMenu); q->connect(ui->m_newButton, &QAbstractButton::clicked, q, &KPayeesView::slotNewPayee); q->connect(ui->m_renameButton, &QAbstractButton::clicked, q, &KPayeesView::slotRenamePayee); q->connect(ui->m_deleteButton, &QAbstractButton::clicked, q, &KPayeesView::slotDeletePayee); q->connect(ui->m_mergeButton, &QAbstractButton::clicked, q, &KPayeesView::slotMergePayee); q->connect(ui->addressEdit, &QTextEdit::textChanged, q, &KPayeesView::slotPayeeDataChanged); q->connect(ui->postcodeEdit, &QLineEdit::textChanged, q, &KPayeesView::slotPayeeDataChanged); q->connect(ui->telephoneEdit, &QLineEdit::textChanged, q, &KPayeesView::slotPayeeDataChanged); q->connect(ui->emailEdit, &QLineEdit::textChanged, q, &KPayeesView::slotPayeeDataChanged); q->connect(ui->notesEdit, &QTextEdit::textChanged, q, &KPayeesView::slotPayeeDataChanged); q->connect(ui->matchKeyEditList, &KEditListWidget::changed, q, &KPayeesView::slotKeyListChanged); q->connect(ui->matchTypeCombo, static_cast(&QComboBox::currentIndexChanged), q, &KPayeesView::slotPayeeDataChanged); q->connect(ui->checkMatchIgnoreCase, &QAbstractButton::toggled, q, &KPayeesView::slotPayeeDataChanged); q->connect(ui->checkEnableDefaultCategory, &QAbstractButton::toggled, q, &KPayeesView::slotPayeeDataChanged); q->connect(ui->comboDefaultCategory, &KMyMoneyAccountCombo::accountSelected, q, &KPayeesView::slotPayeeDataChanged); q->connect(ui->buttonSuggestACategory, &QAbstractButton::clicked, q, &KPayeesView::slotChooseDefaultAccount); q->connect(ui->m_updateButton, &QAbstractButton::clicked, q, &KPayeesView::slotUpdatePayee); q->connect(ui->m_syncAddressbook, &QAbstractButton::clicked, q, &KPayeesView::slotSyncAddressBook); q->connect(ui->m_helpButton, &QAbstractButton::clicked, q, &KPayeesView::slotHelp); q->connect(ui->m_sendMail, &QAbstractButton::clicked, q, &KPayeesView::slotSendMail); q->connect(ui->m_register, &KMyMoneyRegister::Register::editTransaction, q, &KPayeesView::slotSelectTransaction); q->connect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, q, &KPayeesView::refresh); q->connect(ui->m_filterBox, static_cast(&QComboBox::currentIndexChanged), q, &KPayeesView::slotChangeFilter); q->connect(ui->payeeIdentifiers, &KPayeeIdentifierView::dataChanged, q, &KPayeesView::slotPayeeDataChanged); // use the size settings of the last run (if any) KConfigGroup grp = KSharedConfig::openConfig()->group("Last Use Settings"); ui->m_splitter->restoreState(grp.readEntry("KPayeesViewSplitterSize", QByteArray())); ui->m_splitter->setChildrenCollapsible(false); //At start we haven't any payee selected ui->m_tabWidget->setEnabled(false); // disable tab widget ui->m_deleteButton->setEnabled(false); //disable delete, rename and merge buttons ui->m_renameButton->setEnabled(false); ui->m_mergeButton->setEnabled(false); m_payee = MyMoneyPayee(); // make sure we don't access an undefined payee clearItemData(); } void loadPayees() { Q_Q(KPayeesView); if (m_inSelection) return; QMap isSelected; QString id; MyMoneyFile* file = MyMoneyFile::instance(); // remember which items are selected in the list QList selectedItems = ui->m_payeesList->selectedItems(); QList::const_iterator payeesIt = selectedItems.constBegin(); while (payeesIt != selectedItems.constEnd()) { KPayeeListItem* item = dynamic_cast(*payeesIt); if (item) isSelected[item->payee().id()] = true; ++payeesIt; } // keep current selected item KPayeeListItem *currentItem = static_cast(ui->m_payeesList->currentItem()); if (currentItem) id = currentItem->payee().id(); m_allowEditing = false; // clear the list m_searchWidget->clear(); m_searchWidget->updateSearch(); ui->m_payeesList->clear(); ui->m_register->clear(); currentItem = 0; QListlist = file->payeeList(); QList::ConstIterator it; for (it = list.constBegin(); it != list.constEnd(); ++it) { if (m_payeeFilterType == eAllPayees || (m_payeeFilterType == eReferencedPayees && file->isReferenced(*it)) || (m_payeeFilterType == eUnusedPayees && !file->isReferenced(*it))) { KPayeeListItem* item = new KPayeeListItem(ui->m_payeesList, *it); if (item->payee().id() == id) currentItem = item; if (isSelected[item->payee().id()]) item->setSelected(true); } } ui->m_payeesList->sortItems(); if (currentItem) { ui->m_payeesList->setCurrentItem(currentItem); ui->m_payeesList->scrollToItem(currentItem); } m_filterProxyModel->invalidate(); ui->comboDefaultCategory->expandAll(); q->slotSelectPayee(0, 0); m_allowEditing = true; } void selectedPayees(QList& payeesList) const { QList selectedItems = ui->m_payeesList->selectedItems(); QList::ConstIterator itemsIt = selectedItems.constBegin(); while (itemsIt != selectedItems.constEnd()) { KPayeeListItem* item = dynamic_cast(*itemsIt); if (item) payeesList << item->payee(); ++itemsIt; } } void ensurePayeeVisible(const QString& id) { for (int i = 0; i < ui->m_payeesList->count(); ++i) { KPayeeListItem* p = dynamic_cast(ui->m_payeesList->item(0)); if (p && p->payee().id() == id) { ui->m_payeesList->scrollToItem(p, QAbstractItemView::PositionAtCenter); ui->m_payeesList->setCurrentItem(p); // active item and deselect all others ui->m_payeesList->setCurrentRow(i, QItemSelectionModel::ClearAndSelect); // and select it break; } } } void clearItemData() { ui->addressEdit->setText(QString()); ui->postcodeEdit->setText(QString()); ui->telephoneEdit->setText(QString()); ui->emailEdit->setText(QString()); ui->notesEdit->setText(QString()); showTransactions(); } /** * This method loads the m_transactionList, clears * the m_TransactionPtrVector and rebuilds and sorts * it according to the current settings. Then it * loads the m_transactionView with the transaction data. */ void showTransactions() { MyMoneyMoney balance; const auto file = MyMoneyFile::instance(); MyMoneySecurity base = file->baseCurrency(); // setup sort order ui->m_register->setSortOrder(KMyMoneySettings::sortSearchView()); // clear the register ui->m_register->clear(); if (m_selectedPayeesList.isEmpty() || !ui->m_tabWidget->isEnabled()) { ui->m_balanceLabel->setText(i18n("Balance: %1", balance.formatMoney(file->baseCurrency().smallestAccountFraction()))); return; } // setup the list and the pointer vector MyMoneyTransactionFilter filter; for (QList::const_iterator it = m_selectedPayeesList.constBegin(); it != m_selectedPayeesList.constEnd(); ++it) filter.addPayee((*it).id()); filter.setDateFilter(KMyMoneySettings::startDate().date(), QDate()); // retrieve the list from the engine file->transactionList(m_transactionList, filter); // create the elements for the register QList >::const_iterator it; QMap uniqueMap; MyMoneyMoney deposit, payment; int splitCount = 0; bool balanceAccurate = true; for (it = m_transactionList.constBegin(); it != m_transactionList.constEnd(); ++it) { const MyMoneySplit& split = (*it).second; MyMoneyAccount acc = file->account(split.accountId()); ++splitCount; uniqueMap[(*it).first.id()]++; KMyMoneyRegister::Register::transactionFactory(ui->m_register, (*it).first, (*it).second, uniqueMap[(*it).first.id()]); // take care of foreign currencies MyMoneyMoney val = split.shares().abs(); if (acc.currencyId() != base.id()) { const MyMoneyPrice &price = file->price(acc.currencyId(), base.id()); // in case the price is valid, we use it. Otherwise, we keep // a flag that tells us that the balance is somewhat inaccurate if (price.isValid()) { val *= price.rate(base.id()); } else { balanceAccurate = false; } } if (split.shares().isNegative()) { payment += val; } else { deposit += val; } } balance = deposit - payment; // add the group markers ui->m_register->addGroupMarkers(); // sort the transactions according to the sort setting ui->m_register->sortItems(); // remove trailing and adjacent markers ui->m_register->removeUnwantedGroupMarkers(); ui->m_register->updateRegister(true); // we might end up here with updates disabled on the register so // make sure that we enable updates here ui->m_register->setUpdatesEnabled(true); ui->m_balanceLabel->setText(i18n("Balance: %1%2", balanceAccurate ? "" : "~", balance.formatMoney(file->baseCurrency().smallestAccountFraction()))); } /** * Implement common task when deleting or merging payees */ bool payeeReassign(int type) { Q_Q(KPayeesView); if (!(type >= 0 && type < KPayeeReassignDlg::TypeCount)) return false; const auto file = MyMoneyFile::instance(); MyMoneyFileTransaction ft; try { // create a transaction filter that contains all payees selected for removal MyMoneyTransactionFilter f = MyMoneyTransactionFilter(); for (QList::const_iterator it = m_selectedPayeesList.constBegin(); it != m_selectedPayeesList.constEnd(); ++it) { f.addPayee((*it).id()); } // request a list of all transactions that still use the payees in question QList translist = file->transactionList(f); // qDebug() << "[KPayeesView::slotDeletePayee] " << translist.count() << " transaction still assigned to payees"; // now get a list of all schedules that make use of one of the payees QList used_schedules; foreach (const auto schedule, file->scheduleList()) { // loop over all splits in the transaction of the schedule foreach (const auto split, schedule.transaction().splits()) { // is the payee in the split to be deleted? if (payeeInList(m_selectedPayeesList, split.payeeId())) { used_schedules.push_back(schedule); // remember this schedule break; } } } // qDebug() << "[KPayeesView::slotDeletePayee] " << used_schedules.count() << " schedules use one of the selected payees"; // and a list of all loan accounts that references one of the payees QList allAccounts; QList usedAccounts; file->accountList(allAccounts); foreach (const MyMoneyAccount &account, allAccounts) { if (account.isLoan()) { MyMoneyAccountLoan loanAccount(account); foreach (const MyMoneyPayee &payee, m_selectedPayeesList) { if (loanAccount.hasReferenceTo(payee.id())) { usedAccounts.append(account); } } } } MyMoneyPayee newPayee; bool addToMatchList = false; // if at least one payee is still referenced, we need to reassign its transactions first if (!translist.isEmpty() || !used_schedules.isEmpty() || !usedAccounts.isEmpty()) { // first create list with all non-selected payees QList remainingPayees; if (type == KPayeeReassignDlg::TypeMerge) { remainingPayees = m_selectedPayeesList; } else { remainingPayees = file->payeeList(); QList::iterator it_p; for (it_p = remainingPayees.begin(); it_p != remainingPayees.end();) { if (m_selectedPayeesList.contains(*it_p)) { it_p = remainingPayees.erase(it_p); } else { ++it_p; } } } // show error message if no payees remain if (remainingPayees.isEmpty()) { KMessageBox::sorry(q, i18n("At least one transaction/scheduled transaction or loan account is still referenced by a payee. " "Currently you have all payees selected. However, at least one payee must remain so " "that the transaction/scheduled transaction or loan account can be reassigned.")); return false; } // show transaction reassignment dialog KPayeeReassignDlg * dlg = new KPayeeReassignDlg(static_cast(type), q); KMyMoneyMVCCombo::setSubstringSearchForChildren(dlg, !KMyMoneySettings::stringMatchFromStart()); QString payee_id = dlg->show(remainingPayees); addToMatchList = dlg->addToMatchList(); delete dlg; // and kill the dialog if (payee_id.isEmpty()) return false; // the user aborted the dialog, so let's abort as well // try to get selected payee. If not possible and we are merging payees, // then we create a new one try { newPayee = file->payee(payee_id); - } catch (const MyMoneyException &e) { + } catch (const MyMoneyException &) { if (type == KPayeeReassignDlg::TypeMerge) { // it's ok to use payee_id for both arguments since the first is const, // so it's garantee not to change its content if (!KMyMoneyUtils::newPayee(payee_id, payee_id)) return false; // the user aborted the dialog, so let's abort as well newPayee = file->payee(payee_id); } else { return false; } } // TODO : check if we have a report that explicitively uses one of our payees // and issue an appropriate warning try { // now loop over all transactions and reassign payee for (auto& transaction : translist) { // create a copy of the splits list in the transaction // loop over all splits for (auto& split : transaction.splits()) { // if the split is assigned to one of the selected payees, we need to modify it if (payeeInList(m_selectedPayeesList, split.payeeId())) { split.setPayeeId(payee_id); // first modify payee in current split // then modify the split in our local copy of the transaction list transaction.modifySplit(split); // this does not modify the list object 'splits'! } } // for - Splits file->modifyTransaction(transaction); // modify the transaction in the MyMoney object } // for - Transactions // now loop over all schedules and reassign payees for (auto& schedule : used_schedules) { // create copy of transaction in current schedule auto trans = schedule.transaction(); // create copy of lists of splits for (auto& split : trans.splits()) { if (payeeInList(m_selectedPayeesList, split.payeeId())) { split.setPayeeId(payee_id); trans.modifySplit(split); // does not modify the list object 'splits'! } } // for - Splits // store transaction in current schedule schedule.setTransaction(trans); file->modifySchedule(schedule); // modify the schedule in the MyMoney engine } // for - Schedules // reassign the payees in the loans that reference the deleted payees foreach (const MyMoneyAccount &account, usedAccounts) { MyMoneyAccountLoan loanAccount(account); loanAccount.setPayee(payee_id); file->modifyAccount(loanAccount); } } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(q, i18n("Unable to reassign payee of transaction/split"), i18n("%1 thrown in %2:%3", e.what(), e.file(), e.line())); } } else { // if !translist.isEmpty() if (type == KPayeeReassignDlg::TypeMerge) { KMessageBox::sorry(q, i18n("Nothing to merge."), i18n("Merge Payees")); return false; } } bool ignorecase; QStringList payeeNames; MyMoneyPayee::payeeMatchType matchType = newPayee.matchData(ignorecase, payeeNames); QStringList deletedPayeeNames; // now loop over all selected payees and remove them for (QList::iterator it = m_selectedPayeesList.begin(); it != m_selectedPayeesList.end(); ++it) { if (newPayee.id() != (*it).id()) { if (addToMatchList) { deletedPayeeNames << (*it).name(); } file->removePayee(*it); } } // if we initially have no matching turned on, we just ignore the case (default) if (matchType == MyMoneyPayee::matchDisabled) ignorecase = true; // update the destination payee if this was requested by the user if (addToMatchList && deletedPayeeNames.count() > 0) { // add new names to the list // TODO: it would be cool to somehow shrink the list to make better use // of regular expressions at this point. For now, we leave this task // to the user himeself. QStringList::const_iterator it_n; for (it_n = deletedPayeeNames.constBegin(); it_n != deletedPayeeNames.constEnd(); ++it_n) { if (matchType == MyMoneyPayee::matchKey) { // make sure we really need it and it is not caught by an existing regexp QStringList::const_iterator it_k; for (it_k = payeeNames.constBegin(); it_k != payeeNames.constEnd(); ++it_k) { QRegExp exp(*it_k, ignorecase ? Qt::CaseInsensitive : Qt::CaseSensitive); if (exp.indexIn(*it_n) != -1) break; } if (it_k == payeeNames.constEnd()) payeeNames << QRegExp::escape(*it_n); } else if (payeeNames.contains(*it_n) == 0) payeeNames << QRegExp::escape(*it_n); } // and update the payee in the engine context // make sure to turn on matching for this payee in the right mode newPayee.setMatchData(MyMoneyPayee::matchKey, ignorecase, payeeNames); file->modifyPayee(newPayee); } ft.commit(); // If we just deleted the payees, they sure don't exist anymore m_selectedPayeesList.clear(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(q, i18n("Unable to remove payee(s)"), i18n("%1 thrown in %2:%3", e.what(), e.file(), e.line())); } return true; } /** * Check if a list contains a payee with a given id * * @param list const reference to value list * @param id const reference to id * * @retval true object has been found * @retval false object is not in list */ bool payeeInList(const QList& list, const QString& id) const { bool rc = false; QList::const_iterator it_p = list.begin(); while (it_p != list.end()) { if ((*it_p).id() == id) { rc = true; break; } ++it_p; } return rc; } /** Checks whether the currently selected payee is "dirty" * @return true, if payee is modified (is "dirty"); false otherwise */ bool isDirty() const { return ui->m_updateButton->isEnabled(); } /** Sets the payee's "dirty" (modified) status * @param dirty if true (default), payee will be set to dirty */ void setDirty(bool dirty) { ui->m_updateButton->setEnabled(dirty); } KPayeesView *q_ptr; Ui::KPayeesView *ui; MyMoneyPayee m_payee; QString m_newName; MyMoneyContact *m_contact; int m_payeeRow; QList m_payeeRows; /** * List of selected payees */ QList m_selectedPayeesList; /** * q member holds a list of all transactions */ QList > m_transactionList; /** * q member holds the load state of page */ bool m_needLoad; /** * Search widget for the list */ KListWidgetSearchLine* m_searchWidget; /** * Semaphore to suppress loading during selection */ bool m_inSelection; /** * q signals whether a payee can be edited **/ bool m_allowEditing; /** * q holds the filter type */ int m_payeeFilterType; AccountNamesFilterProxyModel *m_filterProxyModel; }; #endif