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

"); msg += i18n(" - Account Name: %1", account.name()) + "
"; msg += i18n(" - Account Type: %1", MyMoneyAccount::accountTypeToString(account.accountType())) + "
"; msg += i18n(" - Account Number: %1", account.number()) + "
"; msg += "
"; if (!account.name().isEmpty()) { if (!accountId.isEmpty()) msg += i18n("Do you want to import transactions to this account?"); else msg += i18n("KMyMoney cannot determine which of your accounts to use. You can " "create a new account by pressing the Create button " "or select another one manually from the selection box below."); } else { msg += i18n("No account information has been found in the selected statement file. " "Please select an account using the selection box in the dialog or " "create a new account by pressing the Create button."); } eDialogs::Category type; if (account.accountType() == Account::Type::Checkings) { type = eDialogs::Category::checking; } else if (account.accountType() == Account::Type::Savings) { type = eDialogs::Category::savings; } else if (account.accountType() == Account::Type::Investment) { type = eDialogs::Category::investment; } else if (account.accountType() == Account::Type::CreditCard) { type = eDialogs::Category::creditCard; } else { type = static_cast(eDialogs::Category::asset | eDialogs::Category::liability); } QPointer accountSelect = new KAccountSelectDlg(type, "StatementImport", 0); connect(accountSelect, &KAccountSelectDlg::createAccount, this, &MyMoneyStatementReader::slotNewAccount); accountSelect->setHeader(i18n("Import transactions")); accountSelect->setDescription(msg); accountSelect->setAccount(account, accountId); accountSelect->setMode(false); accountSelect->showAbortButton(true); accountSelect->hideQifEntry(); QString accname; bool done = false; while (!done) { if (accountSelect->exec() == QDialog::Accepted && !accountSelect->selectedAccount().isEmpty()) { result = true; done = true; accountId = accountSelect->selectedAccount(); account = file->account(accountId); if (! accountNumber.isEmpty() && account.value("StatementKey") != accountNumber) { account.setValue("StatementKey", accountNumber); MyMoneyFileTransaction ft; try { MyMoneyFile::instance()->modifyAccount(account); ft.commit(); accname = account.name(); } catch (const MyMoneyException &) { qDebug("Updating account in MyMoneyStatementReader::selectOrCreateAccount failed"); } } } else { if (accountSelect->aborted()) //throw MYMONEYEXCEPTION_CSTRING("USERABORT"); done = true; else KMessageBox::error(0, QLatin1String("") + i18n("You must select an account, create a new one, or press the Abort button.") + QLatin1String("")); } } delete accountSelect; return result; } const MyMoneyAccount& MyMoneyStatementReader::account() const { return d->m_account; } void MyMoneyStatementReader::setProgressCallback(void(*callback)(int, int, const QString&)) { m_progressCallback = callback; } void MyMoneyStatementReader::signalProgress(int current, int total, const QString& msg) { if (m_progressCallback != 0) (*m_progressCallback)(current, total, msg); } void MyMoneyStatementReader::handleMatchingOfExistingTransaction(TransactionMatcher & matcher, MyMoneyTransaction matchedTransaction, MyMoneySplit matchedSplit, MyMoneyTransaction & importedTransaction, const MyMoneySplit & importedSplit, const TransactionMatchFinder::MatchResult & matchResult) { switch (matchResult) { case TransactionMatchFinder::MatchNotFound: break; case TransactionMatchFinder::MatchDuplicate: d->transactionsDuplicate++; qDebug("Detected transaction duplicate"); break; case TransactionMatchFinder::MatchImprecise: case TransactionMatchFinder::MatchPrecise: addTransaction(importedTransaction); qDebug("Detected as match to transaction '%s'", qPrintable(matchedTransaction.id())); matcher.match(matchedTransaction, matchedSplit, importedTransaction, importedSplit, true); d->transactionsMatched++; break; } } void MyMoneyStatementReader::handleMatchingOfScheduledTransaction(TransactionMatcher & matcher, MyMoneySchedule matchedSchedule, MyMoneySplit matchedSplit, const MyMoneyTransaction & importedTransaction, const MyMoneySplit & importedSplit) { QPointer editor; if (askUserToEnterScheduleForMatching(matchedSchedule, importedSplit, importedTransaction)) { KEnterScheduleDlg dlg(0, matchedSchedule); editor = dlg.startEdit(); if (editor) { MyMoneyTransaction torig; try { // in case the amounts of the scheduled transaction and the // imported transaction differ, we need to update the amount // using the transaction editor. if (matchedSplit.shares() != importedSplit.shares() && !matchedSchedule.isFixed()) { // for now this only works with regular transactions and not // for investment transactions. As of this, we don't have // scheduled investment transactions anyway. auto se = dynamic_cast(editor.data()); if (se) { // the following call will update the amount field in the // editor and also adjust a possible VAT assignment. Make // sure to use only the absolute value of the amount, because // the editor keeps the sign in a different position (deposit, // withdrawal tab) KMyMoneyEdit* amount = dynamic_cast(se->haveWidget("amount")); if (amount) { amount->setValue(importedSplit.shares().abs()); se->slotUpdateAmount(importedSplit.shares().abs().toString()); // we also need to update the matchedSplit variable to // have the modified share/value. matchedSplit.setShares(importedSplit.shares()); matchedSplit.setValue(importedSplit.value()); } } } editor->createTransaction(torig, dlg.transaction(), dlg.transaction().splits().isEmpty() ? MyMoneySplit() : dlg.transaction().splits().front(), true); QString newId; if (editor->enterTransactions(newId, false, true)) { if (!newId.isEmpty()) { torig = MyMoneyFile::instance()->transaction(newId); matchedSchedule.setLastPayment(torig.postDate()); } matchedSchedule.setNextDueDate(matchedSchedule.nextPayment(matchedSchedule.nextDueDate())); MyMoneyFile::instance()->modifySchedule(matchedSchedule); } // now match the two transactions matcher.match(torig, matchedSplit, importedTransaction, importedSplit); d->transactionsMatched++; } catch (const MyMoneyException &) { // make sure we get rid of the editor before // the KEnterScheduleDlg is destroyed delete editor; throw; // rethrow } } // delete the editor delete editor; } } void MyMoneyStatementReader::addTransaction(MyMoneyTransaction& transaction) { MyMoneyFile* file = MyMoneyFile::instance(); file->addTransaction(transaction); d->transactionsAdded++; } bool MyMoneyStatementReader::askUserToEnterScheduleForMatching(const MyMoneySchedule& matchedSchedule, const MyMoneySplit& importedSplit, const MyMoneyTransaction & importedTransaction) const { QString scheduleName = matchedSchedule.name(); int currencyDenom = d->m_account.fraction(MyMoneyFile::instance()->currency(d->m_account.currencyId())); QString splitValue = importedSplit.value().formatMoney(currencyDenom); QString payeeName = MyMoneyFile::instance()->payee(importedSplit.payeeId()).name(); QString questionMsg = i18n("KMyMoney has found a scheduled transaction which matches an imported transaction.
" "Schedule name: %1
" "Transaction: %2 %3
" "Do you want KMyMoney to enter this schedule now so that the transaction can be matched?", scheduleName, splitValue, payeeName); // check that dates are within user's setting const auto gap = static_cast(qAbs(matchedSchedule.transaction().postDate().toJulianDay() - importedTransaction.postDate().toJulianDay())); if (gap > KMyMoneySettings::matchInterval()) questionMsg = i18np("KMyMoney has found a scheduled transaction which matches an imported transaction.
" "Schedule name: %2
" "Transaction: %3 %4
" "The transaction dates are one day apart.
" "Do you want KMyMoney to enter this schedule now so that the transaction can be matched?", "KMyMoney has found a scheduled transaction which matches an imported transaction.
" "Schedule name: %2
" "Transaction: %3 %4
" "The transaction dates are %1 days apart.
" "Do you want KMyMoney to enter this schedule now so that the transaction can be matched?", gap ,scheduleName, splitValue, payeeName); const int userAnswer = KMessageBox::questionYesNo(0, QLatin1String("") + questionMsg + QLatin1String(""), i18n("Schedule found")); return (userAnswer == KMessageBox::Yes); } void MyMoneyStatementReader::slotNewAccount(const MyMoneyAccount& acc) { auto newAcc = acc; NewAccountWizard::Wizard::newAccount(newAcc); } void MyMoneyStatementReader::clearResultMessages() { globalResultMessages()->clear(); } QStringList MyMoneyStatementReader::resultMessages() { return *globalResultMessages(); } diff --git a/kmymoney/mymoney/mymoneystatement.cpp b/kmymoney/mymoney/mymoneystatement.cpp index 54cdf5b77..c414bbb1c 100644 --- a/kmymoney/mymoney/mymoneystatement.cpp +++ b/kmymoney/mymoney/mymoneystatement.cpp @@ -1,379 +1,393 @@ /* * Copyright 2004-2006 Ace Jones - * Copyright 2005-2017 Thomas Baumgart + * Copyright 2005-2018 Thomas Baumgart * Copyright 2017-2018 Łukasz Wojniłowicz * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "mymoneystatement.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes namespace eMyMoney { namespace Statement { enum class Element { KMMStatement, Statement, Transaction, Split, Price, Security }; uint qHash(const Element key, uint seed) { return ::qHash(static_cast(key), seed); } enum class Attribute { Name, Symbol, ID, Version, AccountName, AccountNumber, RoutingNumber, Currency, BeginDate, EndDate, ClosingBalance, Type, AccountID, SkipCategoryMatching, DatePosted, Payee, Memo, Number, Amount, BankID, Reconcile, Action, Shares, Security, BrokerageAccount, Category, }; uint qHash(const Attribute key, uint seed) { return ::qHash(static_cast(key), seed); } } } using namespace eMyMoney; const QHash txAccountType { {Statement::Type::None, QStringLiteral("none")}, {Statement::Type::Checkings, QStringLiteral("checkings")}, {Statement::Type::Savings, QStringLiteral("savings")}, {Statement::Type::Investment, QStringLiteral("investment")}, {Statement::Type::CreditCard, QStringLiteral("creditcard")}, {Statement::Type::Invalid, QStringLiteral("invalid")} }; const QHash txAction { {Transaction::Action::None, QStringLiteral("none")}, {Transaction::Action::Buy, QStringLiteral("buy")}, {Transaction::Action::Sell, QStringLiteral("sell")}, {Transaction::Action::ReinvestDividend, QStringLiteral("reinvestdividend")}, {Transaction::Action::CashDividend, QStringLiteral("cashdividend")}, {Transaction::Action::Shrsin, QStringLiteral("add")}, {Transaction::Action::Shrsout, QStringLiteral("remove")}, {Transaction::Action::Stksplit, QStringLiteral("stocksplit")}, {Transaction::Action::Fees, QStringLiteral("fees")}, {Transaction::Action::Interest, QStringLiteral("interest")}, {Transaction::Action::Invalid, QStringLiteral("invalid")} }; QString getElName(const Statement::Element el) { static const QHash elNames { {Statement::Element::KMMStatement, QStringLiteral("KMYMONEY-STATEMENT")}, {Statement::Element::Statement, QStringLiteral("STATEMENT")}, {Statement::Element::Transaction, QStringLiteral("TRANSACTION")}, {Statement::Element::Split, QStringLiteral("SPLIT")}, {Statement::Element::Price, QStringLiteral("PRICE")}, {Statement::Element::Security, QStringLiteral("SECURITY")} }; return elNames[el]; } QString getAttrName(const Statement::Attribute attr) { static const QHash attrNames { {Statement::Attribute::Name, QStringLiteral("name")}, {Statement::Attribute::Symbol, QStringLiteral("symbol")}, {Statement::Attribute::ID, QStringLiteral("id")}, {Statement::Attribute::Version, QStringLiteral("version")}, {Statement::Attribute::AccountName, QStringLiteral("accountname")}, {Statement::Attribute::AccountNumber, QStringLiteral("accountnumber")}, {Statement::Attribute::RoutingNumber, QStringLiteral("routingnumber")}, {Statement::Attribute::Currency, QStringLiteral("currency")}, {Statement::Attribute::BeginDate, QStringLiteral("begindate")}, {Statement::Attribute::EndDate, QStringLiteral("enddate")}, {Statement::Attribute::ClosingBalance, QStringLiteral("closingbalance")}, {Statement::Attribute::Type, QStringLiteral("type")}, {Statement::Attribute::AccountID, QStringLiteral("accountid")}, {Statement::Attribute::SkipCategoryMatching, QStringLiteral("skipCategoryMatching")}, {Statement::Attribute::DatePosted, QStringLiteral("dateposted")}, {Statement::Attribute::Payee, QStringLiteral("payee")}, {Statement::Attribute::Memo, QStringLiteral("memo")}, {Statement::Attribute::Number, QStringLiteral("number")}, {Statement::Attribute::Amount, QStringLiteral("amount")}, {Statement::Attribute::BankID, QStringLiteral("bankid")}, {Statement::Attribute::Reconcile, QStringLiteral("reconcile")}, {Statement::Attribute::Action, QStringLiteral("action")}, {Statement::Attribute::Shares, QStringLiteral("shares")}, {Statement::Attribute::Security, QStringLiteral("security")}, {Statement::Attribute::BrokerageAccount, QStringLiteral("brokerageaccount")}, {Statement::Attribute::Category, QStringLiteral("version")}, }; return attrNames[attr]; } void MyMoneyStatement::write(QDomElement& _root, QDomDocument* _doc) const { QDomElement e = _doc->createElement(getElName(Statement::Element::Statement)); _root.appendChild(e); e.setAttribute(getAttrName(Statement::Attribute::Version), QStringLiteral("1.1")); e.setAttribute(getAttrName(Statement::Attribute::AccountName), m_strAccountName); e.setAttribute(getAttrName(Statement::Attribute::AccountNumber), m_strAccountNumber); e.setAttribute(getAttrName(Statement::Attribute::RoutingNumber), m_strRoutingNumber); e.setAttribute(getAttrName(Statement::Attribute::Currency), m_strCurrency); e.setAttribute(getAttrName(Statement::Attribute::BeginDate), m_dateBegin.toString(Qt::ISODate)); e.setAttribute(getAttrName(Statement::Attribute::EndDate), m_dateEnd.toString(Qt::ISODate)); e.setAttribute(getAttrName(Statement::Attribute::ClosingBalance), m_closingBalance.toString()); e.setAttribute(getAttrName(Statement::Attribute::Type), txAccountType[m_eType]); e.setAttribute(getAttrName(Statement::Attribute::AccountID), m_accountId); e.setAttribute(getAttrName(Statement::Attribute::SkipCategoryMatching), m_skipCategoryMatching); // iterate over transactions, and add each one foreach (const auto tansaction, m_listTransactions) { auto p = _doc->createElement(getElName(Statement::Element::Transaction)); p.setAttribute(getAttrName(Statement::Attribute::DatePosted), tansaction.m_datePosted.toString(Qt::ISODate)); p.setAttribute(getAttrName(Statement::Attribute::Payee), tansaction.m_strPayee); p.setAttribute(getAttrName(Statement::Attribute::Memo), tansaction.m_strMemo); p.setAttribute(getAttrName(Statement::Attribute::Number), tansaction.m_strNumber); p.setAttribute(getAttrName(Statement::Attribute::Amount), tansaction.m_amount.toString()); p.setAttribute(getAttrName(Statement::Attribute::BankID), tansaction.m_strBankID); p.setAttribute(getAttrName(Statement::Attribute::Reconcile), (int)tansaction.m_reconcile); p.setAttribute(getAttrName(Statement::Attribute::Action), txAction[tansaction.m_eAction]); if (m_eType == eMyMoney::Statement::Type::Investment) { p.setAttribute(getAttrName(Statement::Attribute::Shares), tansaction.m_shares.toString()); p.setAttribute(getAttrName(Statement::Attribute::Security), tansaction.m_strSecurity); p.setAttribute(getAttrName(Statement::Attribute::BrokerageAccount), tansaction.m_strBrokerageAccount); } // add all the splits we know of (might be empty) foreach (const auto split, tansaction.m_listSplits) { auto el = _doc->createElement(getElName(Statement::Element::Split)); el.setAttribute(getAttrName(Statement::Attribute::AccountID), split.m_accountId); el.setAttribute(getAttrName(Statement::Attribute::Amount), split.m_amount.toString()); el.setAttribute(getAttrName(Statement::Attribute::Reconcile), (int)split.m_reconcile); el.setAttribute(getAttrName(Statement::Attribute::Category), split.m_strCategoryName); el.setAttribute(getAttrName(Statement::Attribute::Memo), split.m_strMemo); el.setAttribute(getAttrName(Statement::Attribute::Reconcile), (int)split.m_reconcile); p.appendChild(el); } e.appendChild(p); } // iterate over prices, and add each one foreach (const auto price, m_listPrices) { auto p = _doc->createElement(getElName(Statement::Element::Price)); p.setAttribute(getAttrName(Statement::Attribute::DatePosted), price.m_date.toString(Qt::ISODate)); p.setAttribute(getAttrName(Statement::Attribute::Security), price.m_strSecurity); p.setAttribute(getAttrName(Statement::Attribute::Amount), price.m_amount.toString()); e.appendChild(p); } // iterate over securities, and add each one foreach (const auto security, m_listSecurities) { auto p = _doc->createElement(getElName(Statement::Element::Security)); p.setAttribute(getAttrName(Statement::Attribute::Name), security.m_strName); p.setAttribute(getAttrName(Statement::Attribute::Symbol), security.m_strSymbol); p.setAttribute(getAttrName(Statement::Attribute::ID), security.m_strId); e.appendChild(p); } } bool MyMoneyStatement::read(const QDomElement& _e) { bool result = false; if (_e.tagName() == getElName(Statement::Element::Statement)) { result = true; m_strAccountName = _e.attribute(getAttrName(Statement::Attribute::AccountName)); m_strAccountNumber = _e.attribute(getAttrName(Statement::Attribute::AccountNumber)); m_strRoutingNumber = _e.attribute(getAttrName(Statement::Attribute::RoutingNumber)); m_strCurrency = _e.attribute(getAttrName(Statement::Attribute::Currency)); m_dateBegin = QDate::fromString(_e.attribute(getAttrName(Statement::Attribute::BeginDate)), Qt::ISODate); m_dateEnd = QDate::fromString(_e.attribute(getAttrName(Statement::Attribute::EndDate)), Qt::ISODate); m_closingBalance = MyMoneyMoney(_e.attribute(getAttrName(Statement::Attribute::ClosingBalance))); m_accountId = _e.attribute(getAttrName(Statement::Attribute::AccountID)); m_skipCategoryMatching = _e.attribute(getAttrName(Statement::Attribute::SkipCategoryMatching)).isEmpty(); auto txt = _e.attribute(getAttrName(Statement::Attribute::Type), txAccountType[Statement::Type::Checkings]); m_eType = txAccountType.key(txt, m_eType); QDomNode child = _e.firstChild(); while (!child.isNull() && child.isElement()) { QDomElement c = child.toElement(); if (c.tagName() == getElName(Statement::Element::Transaction)) { MyMoneyStatement::Transaction t; t.m_datePosted = QDate::fromString(c.attribute(getAttrName(Statement::Attribute::DatePosted)), Qt::ISODate); t.m_amount = MyMoneyMoney(c.attribute(getAttrName(Statement::Attribute::Amount))); t.m_strMemo = c.attribute(getAttrName(Statement::Attribute::Memo)); t.m_strNumber = c.attribute(getAttrName(Statement::Attribute::Number)); t.m_strPayee = c.attribute(getAttrName(Statement::Attribute::Payee)); t.m_strBankID = c.attribute(getAttrName(Statement::Attribute::BankID)); t.m_reconcile = static_cast(c.attribute(getAttrName(Statement::Attribute::Reconcile)).toInt()); txt = c.attribute(getAttrName(Statement::Attribute::Action), txAction[eMyMoney::Transaction::Action::Buy]); t.m_eAction = txAction.key(txt, t.m_eAction); if (m_eType == eMyMoney::Statement::Type::Investment) { t.m_shares = MyMoneyMoney(c.attribute(getAttrName(Statement::Attribute::Shares))); t.m_strSecurity = c.attribute(getAttrName(Statement::Attribute::Security)); t.m_strBrokerageAccount = c.attribute(getAttrName(Statement::Attribute::BrokerageAccount)); } // process splits (if any) child = c.firstChild(); while (!child.isNull() && child.isElement()) { c = child.toElement(); if (c.tagName() == getElName(Statement::Element::Split)) { MyMoneyStatement::Split s; s.m_accountId = c.attribute(getAttrName(Statement::Attribute::AccountID)); s.m_amount = MyMoneyMoney(c.attribute(getAttrName(Statement::Attribute::Amount))); s.m_reconcile = static_cast(c.attribute(getAttrName(Statement::Attribute::Reconcile)).toInt()); s.m_strCategoryName = c.attribute(getAttrName(Statement::Attribute::Category)); s.m_strMemo = c.attribute(getAttrName(Statement::Attribute::Memo)); t.m_listSplits += s; } child = child.nextSibling(); } m_listTransactions += t; } else if (c.tagName() == getElName(Statement::Element::Price)) { MyMoneyStatement::Price p; p.m_date = QDate::fromString(c.attribute(getAttrName(Statement::Attribute::DatePosted)), Qt::ISODate); p.m_strSecurity = c.attribute(getAttrName(Statement::Attribute::Security)); p.m_amount = MyMoneyMoney(c.attribute(getAttrName(Statement::Attribute::Amount))); m_listPrices += p; } else if (c.tagName() == getElName(Statement::Element::Security)) { MyMoneyStatement::Security s; s.m_strName = c.attribute(getAttrName(Statement::Attribute::Name)); s.m_strSymbol = c.attribute(getAttrName(Statement::Attribute::Symbol)); s.m_strId = c.attribute(getAttrName(Statement::Attribute::ID)); m_listSecurities += s; } child = child.nextSibling(); } } return result; } bool MyMoneyStatement::isStatementFile(const QString& _filename) { // filename is considered a statement file if it contains // the tag "" in the first 20 lines. bool result = false; QFile f(_filename); if (f.open(QIODevice::ReadOnly)) { QTextStream ts(&f); auto lineCount = 20; while (!ts.atEnd() && !result && lineCount != 0) { if (ts.readLine().contains(QLatin1String(""), Qt::CaseInsensitive)) result = true; --lineCount; } f.close(); } return result; } void MyMoneyStatement::writeXMLFile(const MyMoneyStatement& _s, const QString& _filename) { static unsigned filenum = 1; auto filename = _filename; if (filename.isEmpty()) { filename = QString::fromLatin1("statement-%1%2.xml").arg((filenum < 10) ? QStringLiteral("0") : QString()).arg(filenum); filenum++; } auto doc = new QDomDocument(getElName(Statement::Element::KMMStatement)); Q_CHECK_PTR(doc); //writeStatementtoXMLDoc(_s,doc); QDomProcessingInstruction instruct = doc->createProcessingInstruction(QStringLiteral("xml"), QStringLiteral("version=\"1.0\" encoding=\"utf-8\"")); doc->appendChild(instruct); auto eroot = doc->createElement(getElName(Statement::Element::KMMStatement)); doc->appendChild(eroot); _s.write(eroot, doc); QFile g(filename); if (g.open(QIODevice::WriteOnly)) { QTextStream stream(&g); stream.setCodec("UTF-8"); stream << doc->toString(); g.close(); } delete doc; } bool MyMoneyStatement::readXMLFile(MyMoneyStatement& _s, const QString& _filename) { bool result = false; QFile f(_filename); f.open(QIODevice::ReadOnly); QDomDocument* doc = new QDomDocument; if (doc->setContent(&f, false)) { QDomElement rootElement = doc->documentElement(); if (!rootElement.isNull()) { QDomNode child = rootElement.firstChild(); while (!child.isNull() && child.isElement()) { result = true; QDomElement childElement = child.toElement(); _s.read(childElement); child = child.nextSibling(); } } } delete doc; return result; } + +QDate MyMoneyStatement::statementEndDate() const +{ + if (m_dateEnd.isValid()) + return m_dateEnd; + + QDate postDate; + for(auto t : m_listTransactions) { + if (t.m_datePosted > postDate) { + postDate = t.m_datePosted; + } + } + return postDate; +} diff --git a/kmymoney/mymoney/mymoneystatement.h b/kmymoney/mymoney/mymoneystatement.h index f6aab0986..d77d1a7fa 100644 --- a/kmymoney/mymoney/mymoneystatement.h +++ b/kmymoney/mymoney/mymoneystatement.h @@ -1,131 +1,141 @@ /* * Copyright 2004-2006 Ace Jones - * Copyright 2005-2017 Thomas Baumgart + * Copyright 2005-2018 Thomas Baumgart * Copyright 2017-2018 Łukasz Wojniłowicz * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef MYMONEYSTATEMENT_H #define MYMONEYSTATEMENT_H // ---------------------------------------------------------------------------- // QT Includes #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "kmm_mymoney_export.h" #include "mymoneymoney.h" #include "mymoneyenums.h" class QDomElement; class QDomDocument; /** Represents the electronic analog of the paper bank statement just like we used to get in the regular mail. This class is designed to be easy to extend and easy to create with minimal dependencies. So the header file should include as few project files as possible (preferably NONE). @author ace jones */ -class MyMoneyStatement +class KMM_MYMONEY_EXPORT MyMoneyStatement { public: struct Split { QString m_strCategoryName; QString m_strMemo; QString m_accountId; eMyMoney::Split::State m_reconcile = eMyMoney::Split::State::NotReconciled; MyMoneyMoney m_amount; }; struct Transaction { QDate m_datePosted; QString m_strPayee; QString m_strMemo; QString m_strNumber; QString m_strBankID; MyMoneyMoney m_amount; eMyMoney::Split::State m_reconcile = eMyMoney::Split::State::NotReconciled; eMyMoney::Transaction::Action m_eAction = eMyMoney::Transaction::Action::None; MyMoneyMoney m_shares; MyMoneyMoney m_fees; MyMoneyMoney m_price; QString m_strInterestCategory; QString m_strBrokerageAccount; QString m_strSymbol; QString m_strSecurity; QList m_listSplits; }; struct Price { QDate m_date; QString m_sourceName; QString m_strSecurity; QString m_strCurrency; MyMoneyMoney m_amount; }; struct Security { QString m_strName; QString m_strSymbol; QString m_strId; }; QString m_strAccountName; QString m_strAccountNumber; QString m_strRoutingNumber; /** * The statement provider's information for the statement reader how to find the * account. The provider usually leaves some value with a key unique to the provider in the KVP of the * MyMoneyAccount object when setting up the connection or at a later point in time. * Using the KMyMoneyPlugin::KMMStatementInterface::account() method it can retrieve the * MyMoneyAccount object for this key. The account id of that account should be returned * here. If no id is available, leave it empty. */ QString m_accountId; QString m_strCurrency; QDate m_dateBegin; QDate m_dateEnd; MyMoneyMoney m_closingBalance = MyMoneyMoney::autoCalc; eMyMoney::Statement::Type m_eType = eMyMoney::Statement::Type::None; QList m_listTransactions; QList m_listPrices; QList m_listSecurities; bool m_skipCategoryMatching = false; void write(QDomElement&, QDomDocument*) const; bool read(const QDomElement&); - KMM_MYMONEY_EXPORT static bool isStatementFile(const QString&); - KMM_MYMONEY_EXPORT static bool readXMLFile(MyMoneyStatement&, const QString&); - KMM_MYMONEY_EXPORT static void writeXMLFile(const MyMoneyStatement&, const QString&); + /** + * This method returns the date provided as the end date of the statement. + * In case this is not provided, we return the date of the youngest transaction + * instead. In case there are no transactions found, an invalid date is + * returned. + * + * @sa m_dateEnd + */ + QDate statementEndDate() const; + + static bool isStatementFile(const QString&); + static bool readXMLFile(MyMoneyStatement&, const QString&); + static void writeXMLFile(const MyMoneyStatement&, const QString&); }; /** * Make it possible to hold @ref MyMoneyStatement objects inside @ref QVariant objects. */ Q_DECLARE_METATYPE(MyMoneyStatement) #endif diff --git a/kmymoney/plugins/ofx/import/dialogs/mymoneyofxconnector.cpp b/kmymoney/plugins/ofx/import/dialogs/mymoneyofxconnector.cpp index f7977e694..75a8261db 100644 --- a/kmymoney/plugins/ofx/import/dialogs/mymoneyofxconnector.cpp +++ b/kmymoney/plugins/ofx/import/dialogs/mymoneyofxconnector.cpp @@ -1,773 +1,780 @@ /*************************************************************************** mymoneyofxconnector.cpp ------------------- begin : Sat Nov 13 2004 copyright : (C) 2002 by Ace Jones email : acejones@users.sourceforge.net ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "mymoneyofxconnector.h" // ---------------------------------------------------------------------------- // System Includes #include // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "mymoneyaccount.h" #include "mymoneykeyvaluecontainer.h" #include "mymoneyenums.h" using KWallet::Wallet; OfxHeaderVersion::OfxHeaderVersion(KComboBox* combo, const QString& headerVersion) : m_combo(combo) { combo->clear(); combo->addItem("102"); combo->addItem("103"); if (!headerVersion.isEmpty()) { combo->setCurrentItem(headerVersion); } else { combo->setCurrentItem("102"); } } QString OfxHeaderVersion::headerVersion() const { return m_combo->currentText(); } OfxAppVersion::OfxAppVersion(KComboBox* combo, KLineEdit* versionEdit, const QString& appId) : m_combo(combo), m_versionEdit(versionEdit) { // http://ofxblog.wordpress.com/2007/06/06/ofx-appid-and-appver-for-intuit-products/ // http://ofxblog.wordpress.com/2007/06/06/ofx-appid-and-appver-for-microsoft-money/ // Quicken m_appMap[i18n("Quicken Windows 2003")] = "QWIN:1200"; m_appMap[i18n("Quicken Windows 2004")] = "QWIN:1300"; m_appMap[i18n("Quicken Windows 2005")] = "QWIN:1400"; m_appMap[i18n("Quicken Windows 2006")] = "QWIN:1500"; m_appMap[i18n("Quicken Windows 2007")] = "QWIN:1600"; m_appMap[i18n("Quicken Windows 2008")] = "QWIN:1700"; // the following three added as found on // https://microsoftmoneyoffline.wordpress.com/appid-appver/ on 2013-02-28 m_appMap[i18n("Quicken Windows 2010")] = "QWIN:1800"; m_appMap[i18n("Quicken Windows 2011")] = "QWIN:1900"; m_appMap[i18n("Quicken Windows 2012")] = "QWIN:2100"; m_appMap[i18n("Quicken Windows 2013")] = "QWIN:2200"; m_appMap[i18n("Quicken Windows 2014")] = "QWIN:2300"; // following two added as found in previous URL on 2017-10-01 m_appMap[i18n("Quicken Windows 2015")] = "QWIN:2400"; m_appMap[i18n("Quicken Windows 2016")] = "QWIN:2500"; m_appMap[i18n("Quicken Windows (Expert)")] = "QWIN:"; // MS-Money m_appMap[i18n("MS-Money 2003")] = "Money:1100"; m_appMap[i18n("MS-Money 2004")] = "Money:1200"; m_appMap[i18n("MS-Money 2005")] = "Money:1400"; m_appMap[i18n("MS-Money 2006")] = "Money:1500"; m_appMap[i18n("MS-Money 2007")] = "Money:1600"; m_appMap[i18n("MS-Money Plus")] = "Money:1700"; m_appMap[i18n("MS-Money (Expert)")] = "Money:"; // KMyMoney m_appMap["KMyMoney"] = "KMyMoney:1000"; combo->clear(); combo->addItems(m_appMap.keys()); if (versionEdit) versionEdit->hide(); QMap::const_iterator it_a; // check for an exact match for (it_a = m_appMap.constBegin(); it_a != m_appMap.constEnd(); ++it_a) { if (*it_a == appId) break; } // not found, check if we have a manual version of this product QRegExp appExp("(\\w+:)(\\d+)"); if (it_a == m_appMap.constEnd()) { if (appExp.exactMatch(appId)) { for (it_a = m_appMap.constBegin(); it_a != m_appMap.constEnd(); ++it_a) { if (*it_a == appExp.cap(1)) break; } } } // if we still haven't found it, we use a default as last resort if (it_a != m_appMap.constEnd()) { combo->setCurrentItem(it_a.key()); if ((*it_a).endsWith(':')) { if (versionEdit) { versionEdit->show(); versionEdit->setText(appExp.cap(2)); } else { combo->setCurrentItem(i18n("Quicken Windows 2008")); } } } else { combo->setCurrentItem(i18n("Quicken Windows 2008")); } } const QString OfxAppVersion::appId() const { static QString defaultAppId("QWIN:1700"); QString app = m_combo->currentText(); if (m_appMap[app] != defaultAppId) { if (m_appMap[app].endsWith(':')) { if (m_versionEdit) { return m_appMap[app] + m_versionEdit->text(); } else { return QString(); } } return m_appMap[app]; } return QString(); } bool OfxAppVersion::isValid() const { QRegExp exp(".+:\\d+"); QString app = m_combo->currentText(); if (m_appMap[app].endsWith(':')) { if (m_versionEdit) { app = m_appMap[app] + m_versionEdit->text(); } else { app.clear(); } } else { app = m_appMap[app]; } return exp.exactMatch(app); } MyMoneyOfxConnector::MyMoneyOfxConnector(const MyMoneyAccount& _account): m_account(_account) { m_fiSettings = m_account.onlineBankingSettings(); } QString MyMoneyOfxConnector::iban() const { return m_fiSettings.value("bankid"); } QString MyMoneyOfxConnector::fiorg() const { return m_fiSettings.value("org"); } QString MyMoneyOfxConnector::fiid() const { return m_fiSettings.value("fid"); } QString MyMoneyOfxConnector::clientUid() const { return m_fiSettings.value("clientUid"); } QString MyMoneyOfxConnector::username() const { return m_fiSettings.value("username"); } QString MyMoneyOfxConnector::password() const { // if we don't find a password in the wallet, we use the old method // and retrieve it from the settings stored in the KMyMoney data storage. // in case we don't have a password on file, we ask the user QString key = OFX_PASSWORD_KEY(m_fiSettings.value("url"), m_fiSettings.value("uniqueId")); QString pwd = m_fiSettings.value("password"); // now check for the wallet Wallet *wallet = openSynchronousWallet(); if (wallet && !Wallet::keyDoesNotExist(Wallet::NetworkWallet(), Wallet::PasswordFolder(), key)) { wallet->setFolder(Wallet::PasswordFolder()); wallet->readPassword(key, pwd); } if (pwd.isEmpty()) { QPointer dlg = new KPasswordDialog(0); dlg->setPrompt(i18n("Enter your password for account %1", m_account.name())); if (dlg->exec()) pwd = dlg->password(); delete dlg; } return pwd; } QString MyMoneyOfxConnector::accountnum() const { return m_fiSettings.value("accountid"); } QString MyMoneyOfxConnector::url() const { return m_fiSettings.value("url"); } QDate MyMoneyOfxConnector::statementStartDate() const { if ((m_fiSettings.value("kmmofx-todayMinus").toInt() != 0) && !m_fiSettings.value("kmmofx-numRequestDays").isEmpty()) { return QDate::currentDate().addDays(-m_fiSettings.value("kmmofx-numRequestDays").toInt()); + } else if ((m_fiSettings.value("kmmofx-lastUpdate").toInt() != 0) && !m_account.value("lastImportedTransactionDate").isEmpty()) { - return QDate::fromString(m_account.value("lastImportedTransactionDate"), Qt::ISODate); + // get last statement request date from application account object + // and start from a few days before if the date is valid + QDate lastUpdate = QDate::fromString(m_account.value("lastImportedTransactionDate"), Qt::ISODate); + if (lastUpdate.isValid()) { + return lastUpdate.addDays(-3); + } + } else if ((m_fiSettings.value("kmmofx-pickDate").toInt() != 0) && !m_fiSettings.value("kmmofx-specificDate").isEmpty()) { return QDate::fromString(m_fiSettings.value("kmmofx-specificDate")); } return QDate::currentDate().addMonths(-2); } OfxAccountData::AccountType MyMoneyOfxConnector::accounttype() const { OfxAccountData::AccountType result = OfxAccountData::OFX_CHECKING; QString type = m_account.onlineBankingSettings()["type"]; if (type == "CHECKING") result = OfxAccountData::OFX_CHECKING; else if (type == "SAVINGS") result = OfxAccountData::OFX_SAVINGS; else if (type == "MONEY MARKET") result = OfxAccountData::OFX_MONEYMRKT; else if (type == "CREDIT LINE") result = OfxAccountData::OFX_CREDITLINE; else if (type == "CMA") result = OfxAccountData::OFX_CMA; else if (type == "CREDIT CARD") result = OfxAccountData::OFX_CREDITCARD; else if (type == "INVESTMENT") result = OfxAccountData::OFX_INVESTMENT; else { switch (m_account.accountType()) { case eMyMoney::Account::Type::Investment: result = OfxAccountData::OFX_INVESTMENT; break; case eMyMoney::Account::Type::CreditCard: result = OfxAccountData::OFX_CREDITCARD; break; case eMyMoney::Account::Type::Savings: result = OfxAccountData::OFX_SAVINGS; break; default: break; } } // This is a bit of a personalized hack. Sometimes we may want to override the // ofx type for an account. For now, I will stash it in the notes! QRegExp rexp("OFXTYPE:([A-Z]*)"); if (rexp.indexIn(m_account.description()) != -1) { QString override = rexp.cap(1); qDebug() << "MyMoneyOfxConnector::accounttype() overriding to " << result; if (override == "BANK") result = OfxAccountData::OFX_CHECKING; else if (override == "CC") result = OfxAccountData::OFX_CREDITCARD; else if (override == "INV") result = OfxAccountData::OFX_INVESTMENT; else if (override == "MONEYMARKET") result = OfxAccountData::OFX_MONEYMRKT; } return result; } void MyMoneyOfxConnector::initRequest(OfxFiLogin* fi) const { memset(fi, 0, sizeof(OfxFiLogin)); strncpy(fi->fid, fiid().toLatin1(), OFX_FID_LENGTH - 1); strncpy(fi->org, fiorg().toLatin1(), OFX_ORG_LENGTH - 1); strncpy(fi->userid, username().toLatin1(), OFX_USERID_LENGTH - 1); strncpy(fi->userpass, password().toLatin1(), OFX_USERPASS_LENGTH - 1); #ifdef LIBOFX_HAVE_CLIENTUID strncpy(fi->clientuid, clientUid().toLatin1(), OFX_CLIENTUID_LENGTH - 1); #endif // If we don't know better, we pretend to be Quicken 2008 // http://ofxblog.wordpress.com/2007/06/06/ofx-appid-and-appver-for-intuit-products/ // http://ofxblog.wordpress.com/2007/06/06/ofx-appid-and-appver-for-microsoft-money/ QString appId = m_account.onlineBankingSettings().value("appId"); QRegExp exp("(.*):(.*)"); if (exp.indexIn(appId) != -1) { strncpy(fi->appid, exp.cap(1).toLatin1(), OFX_APPID_LENGTH - 1); strncpy(fi->appver, exp.cap(2).toLatin1(), OFX_APPVER_LENGTH - 1); } else { strncpy(fi->appid, "QWIN", OFX_APPID_LENGTH - 1); strncpy(fi->appver, "1700", OFX_APPVER_LENGTH - 1); } QString headerVersion = m_account.onlineBankingSettings().value("kmmofx-headerVersion"); if (!headerVersion.isEmpty()) { strncpy(fi->header_version, headerVersion.toLatin1(), OFX_HEADERVERSION_LENGTH - 1); } } const QByteArray MyMoneyOfxConnector::statementRequest() const { OfxFiLogin fi; initRequest(&fi); OfxAccountData account; memset(&account, 0, sizeof(OfxAccountData)); if (!iban().toLatin1().isEmpty()) { strncpy(account.bank_id, iban().toLatin1(), OFX_BANKID_LENGTH - 1); strncpy(account.broker_id, iban().toLatin1(), OFX_BROKERID_LENGTH - 1); } strncpy(account.account_number, accountnum().toLatin1(), OFX_ACCTID_LENGTH - 1); account.account_type = accounttype(); QByteArray result; if (fi.userpass[0]) { char *szrequest = libofx_request_statement(&fi, &account, QDateTime(statementStartDate()).toTime_t()); QString request = szrequest; // remove the trailing zero result = request.toUtf8(); if(result.at(result.size()-1) == 0) result.truncate(result.size() - 1); free(szrequest); } return result; } #if 0 MyMoneyOfxConnector::Tag MyMoneyOfxConnector::message(const QString& _msgType, const QString& _trnType, const Tag& _request) { return Tag(_msgType + "MSGSRQV1") .subtag(Tag(_trnType + "TRNRQ") .element("TRNUID", uuid()) .element("CLTCOOKIE", "1") .subtag(_request)); } MyMoneyOfxConnector::Tag MyMoneyOfxConnector::investmentRequest() const { QString dtnow_string = QDateTime::currentDateTime().toString(Qt::ISODate).remove(QRegExp("[^0-9]")); QString dtstart_string = _dtstart.toString(Qt::ISODate).remove(QRegExp("[^0-9]")); return message("INVSTMT", "INVSTMT", Tag("INVSTMTRQ") .subtag(Tag("INVACCTFROM").element("BROKERID", fiorg()).element("ACCTID", accountnum())) .subtag(Tag("INCTRAN").element("DTSTART", dtstart_string).element("INCLUDE", "Y")) .element("INCOO", "Y") .subtag(Tag("INCPOS").element("DTASOF", dtnow_string).element("INCLUDE", "Y")) .element("INCBAL", "Y")); } MyMoneyOfxConnector::Tag MyMoneyOfxConnector::bankStatementRequest(const QDate& _dtstart) const { QString dtstart_string = _dtstart.toString(Qt::ISODate).remove(QRegExp("[^0-9]")); return message("BANK", "STMT", Tag("STMTRQ") .subtag(Tag("BANKACCTFROM").element("BANKID", iban()).element("ACCTID", accountnum()).element("ACCTTYPE", "CHECKING")) .subtag(Tag("INCTRAN").element("DTSTART", dtstart_string).element("INCLUDE", "Y"))); } MyMoneyOfxConnector::Tag MyMoneyOfxConnector::creditCardRequest(const QDate& _dtstart) const { QString dtstart_string = _dtstart.toString(Qt::ISODate).remove(QRegExp("[^0-9]")); return message("CREDITCARD", "CCSTMT", Tag("CCSTMTRQ") .subtag(Tag("CCACCTFROM").element("ACCTID", accountnum())) .subtag(Tag("INCTRAN").element("DTSTART", dtstart_string).element("INCLUDE", "Y"))); } MyMoneyOfxConnector::Tag MyMoneyOfxConnector::signOn() const { QString dtnow_string = QDateTime::currentDateTime().toString(Qt::ISODate).remove(QRegExp("[^0-9]")); Tag fi("FI"); fi.element("ORG", fiorg()); if (!fiid().isEmpty()) fi.element("FID", fiid()); return Tag("SIGNONMSGSRQV1") .subtag(Tag("SONRQ") .element("DTCLIENT", dtnow_string) .element("USERID", username()) .element("USERPASS", password()) .element("LANGUAGE", "ENG") .subtag(fi) .element("APPID", "QWIN") .element("APPVER", "1100")); } QString MyMoneyOfxConnector::header() { return QString("OFXHEADER:100\r\n" "DATA:OFXSGML\r\n" "VERSION:102\r\n" "SECURITY:NONE\r\n" "ENCODING:USASCII\r\n" "CHARSET:1252\r\n" "COMPRESSION:NONE\r\n" "OLDFILEUID:NONE\r\n" "NEWFILEUID:%1\r\n" "\r\n").arg(uuid()); } QString MyMoneyOfxConnector::uuid() { static int id = 1; return QDateTime::currentDateTime().toString("yyyyMMdd-hhmmsszzz-") + QString::number(id++); } // // Methods to provide RESPONSES to OFX requests. This has no real use in // KMyMoney, but it's included for the purposes of unit testing. This way, I // can create a MyMoneyAccount, write it to an OFX file, import that OFX file, // and check that everything made it through the importer. // // It's also a far-off dream to write an OFX server using KMyMoney as a // backend. It really should not be that hard, and it would fill a void in // the open source software community. // const QByteArray MyMoneyOfxConnector::statementResponse(const QDate& _dtstart) const { QString request; if (accounttype() == "CC") request = header() + Tag("OFX").subtag(signOnResponse()).subtag(creditCardStatementResponse(_dtstart)); else if (accounttype() == "INV") request = header() + Tag("OFX").subtag(signOnResponse()).data(investmentStatementResponse(_dtstart)); else request = header() + Tag("OFX").subtag(signOnResponse()).subtag(bankStatementResponse(_dtstart)); // remove the trailing zero QByteArray result = request.utf8(); result.truncate(result.size() - 1); return result; } MyMoneyOfxConnector::Tag MyMoneyOfxConnector::signOnResponse() const { QString dtnow_string = QDateTime::currentDateTime().toString(Qt::ISODate).remove(QRegExp("[^0-9]")); Tag sonrs("SONRS"); sonrs .subtag(Tag("STATUS") .element("CODE", "0") .element("SEVERITY", "INFO") .element("MESSAGE", "The operation succeeded.") ) .element("DTSERVER", dtnow_string) .element("LANGUAGE", "ENG"); Tag fi("FI"); if (!fiorg().isEmpty()) fi.element("ORG", fiorg()); if (!fiid().isEmpty()) fi.element("FID", fiid()); if (!fi.isEmpty()) sonrs.subtag(fi); return Tag("SIGNONMSGSRSV1").subtag(sonrs); } MyMoneyOfxConnector::Tag MyMoneyOfxConnector::messageResponse(const QString& _msgType, const QString& _trnType, const Tag& _response) { return Tag(_msgType + "MSGSRSV1") .subtag(Tag(_trnType + "TRNRS") .element("TRNUID", uuid()) .subtag(Tag("STATUS").element("CODE", "0").element("SEVERITY", "INFO")) .element("CLTCOOKIE", "1") .subtag(_response)); } MyMoneyOfxConnector::Tag MyMoneyOfxConnector::bankStatementResponse(const QDate& _dtstart) const { MyMoneyFile* file = MyMoneyFile::instance(); QString dtstart_string = _dtstart.toString(Qt::ISODate).remove(QRegExp("[^0-9]")); QString dtnow_string = QDateTime::currentDateTime().toString(Qt::ISODate).remove(QRegExp("[^0-9]")); QString transactionlist; MyMoneyTransactionFilter filter; filter.setDateFilter(_dtstart, QDate::currentDate()); filter.addAccount(m_account.id()); QList transactions = file->transactionList(filter); QList::const_iterator it_transaction = transactions.begin(); while (it_transaction != transactions.end()) { transactionlist += transaction(*it_transaction); ++it_transaction; } return messageResponse("BANK", "STMT", Tag("STMTRS") .element("CURDEF", "USD") .subtag(Tag("BANKACCTFROM").element("BANKID", iban()).element("ACCTID", accountnum()).element("ACCTTYPE", "CHECKING")) .subtag(Tag("BANKTRANLIST").element("DTSTART", dtstart_string).element("DTEND", dtnow_string).data(transactionlist)) .subtag(Tag("LEDGERBAL").element("BALAMT", file->balance(m_account.id()).formatMoney(QString(), 2, false)).element("DTASOF", dtnow_string))); } MyMoneyOfxConnector::Tag MyMoneyOfxConnector::creditCardStatementResponse(const QDate& _dtstart) const { MyMoneyFile* file = MyMoneyFile::instance(); QString dtstart_string = _dtstart.toString(Qt::ISODate).remove(QRegExp("[^0-9]")); QString dtnow_string = QDateTime::currentDateTime().toString(Qt::ISODate).remove(QRegExp("[^0-9]")); QString transactionlist; MyMoneyTransactionFilter filter; filter.setDateFilter(_dtstart, QDate::currentDate()); filter.addAccount(m_account.id()); QList transactions = file->transactionList(filter); QList::const_iterator it_transaction = transactions.begin(); while (it_transaction != transactions.end()) { transactionlist += transaction(*it_transaction); ++it_transaction; } return messageResponse("CREDITCARD", "CCSTMT", Tag("CCSTMTRS") .element("CURDEF", "USD") .subtag(Tag("CCACCTFROM").element("ACCTID", accountnum())) .subtag(Tag("BANKTRANLIST").element("DTSTART", dtstart_string).element("DTEND", dtnow_string).data(transactionlist)) .subtag(Tag("LEDGERBAL").element("BALAMT", file->balance(m_account.id()).formatMoney(QString(), 2, false)).element("DTASOF", dtnow_string))); } QString MyMoneyOfxConnector::investmentStatementResponse(const QDate& _dtstart) const { MyMoneyFile* file = MyMoneyFile::instance(); QString dtstart_string = _dtstart.toString(Qt::ISODate).remove(QRegExp("[^0-9]")); QString dtnow_string = QDateTime::currentDateTime().toString(Qt::ISODate).remove(QRegExp("[^0-9]")); QString transactionlist; MyMoneyTransactionFilter filter; filter.setDateFilter(_dtstart, QDate::currentDate()); filter.addAccount(m_account.id()); filter.addAccount(m_account.accountList()); QList transactions = file->transactionList(filter); QList::const_iterator it_transaction = transactions.begin(); while (it_transaction != transactions.end()) { transactionlist += investmentTransaction(*it_transaction); ++it_transaction; } Tag securitylist("SECLIST"); QCStringList accountids = m_account.accountList(); QCStringList::const_iterator it_accountid = accountids.begin(); while (it_accountid != accountids.end()) { MyMoneySecurity equity = file->security(file->account(*it_accountid).currencyId()); securitylist.subtag(Tag("STOCKINFO") .subtag(Tag("SECINFO") .subtag(Tag("SECID") .element("UNIQUEID", equity.id()) .element("UNIQUEIDTYPE", "KMYMONEY")) .element("SECNAME", equity.name()) .element("TICKER", equity.tradingSymbol()) .element("FIID", equity.id()))); ++it_accountid; } return messageResponse("INVSTMT", "INVSTMT", Tag("INVSTMTRS") .element("DTASOF", dtstart_string) .element("CURDEF", "USD") .subtag(Tag("INVACCTFROM").element("BROKERID", fiorg()).element("ACCTID", accountnum())) .subtag(Tag("INVTRANLIST").element("DTSTART", dtstart_string).element("DTEND", dtnow_string).data(transactionlist)) ) + Tag("SECLISTMSGSRSV1").subtag(securitylist); } MyMoneyOfxConnector::Tag MyMoneyOfxConnector::transaction(const MyMoneyTransaction& _t) const { // This method creates a transaction tag using ONLY the elements that importer uses MyMoneyFile* file = MyMoneyFile::instance(); //Use this version for bank/cc transactions MyMoneySplit s = _t.splitByAccount(m_account.id(), true); //TODO (Ace) Write "investmentTransaction()"... //Use this version for inv transactions //MyMoneySplit s = _t.splitByAccount( m_account.accountList(), true ); Tag result("STMTTRN"); result // This is a temporary hack. I don't use the trntype field in importing at all, // but libofx requires it to be there in order to import the file. .element("TRNTYPE", "DEBIT") .element("DTPOSTED", _t.postDate().toString(Qt::ISODate).remove(QRegExp("[^0-9]"))) .element("TRNAMT", s.value().formatMoney(QString(), 2, false)); if (! _t.bankID().isEmpty()) result.element("FITID", _t.bankID()); else result.element("FITID", _t.id()); if (! s.number().isEmpty()) result.element("CHECKNUM", s.number()); if (! s.payeeId().isEmpty()) result.element("NAME", file->payee(s.payeeId()).name()); if (! _t.memo().isEmpty()) result.element("MEMO", _t.memo()); return result; } MyMoneyOfxConnector::Tag MyMoneyOfxConnector::investmentTransaction(const MyMoneyTransaction& _t) const { MyMoneyFile* file = MyMoneyFile::instance(); //Use this version for inv transactions MyMoneySplit s = _t.splitByAccount(m_account.accountList(), true); QByteArray stockid = file->account(s.accountId()).currencyId(); Tag invtran("INVTRAN"); invtran.element("FITID", _t.id()).element("DTTRADE", _t.postDate().toString(Qt::ISODate).remove(QRegExp("[^0-9]"))); if (!_t.memo().isEmpty()) invtran.element("MEMO", _t.memo()); if (s.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::BuyShares)) { if (s.shares().isNegative()) { return Tag("SELLSTOCK") .subtag(Tag("INVSELL") .subtag(invtran) .subtag(Tag("SECID").element("UNIQUEID", stockid).element("UNIQUEIDTYPE", "KMYMONEY")) .element("UNITS", QString(((s.shares())).formatMoney(QString(), 2, false)).remove(QRegExp("[^0-9.\\-]"))) .element("UNITPRICE", QString((s.value() / s.shares()).formatMoney(QString(), 2, false)).remove(QRegExp("[^0-9.]"))) .element("TOTAL", QString((-s.value()).formatMoney(QString(), 2, false)).remove(QRegExp("[^0-9.\\-]"))) .element("SUBACCTSEC", "CASH") .element("SUBACCTFUND", "CASH")) .element("SELLTYPE", "SELL"); } else { return Tag("BUYSTOCK") .subtag(Tag("INVBUY") .subtag(invtran) .subtag(Tag("SECID").element("UNIQUEID", stockid).element("UNIQUEIDTYPE", "KMYMONEY")) .element("UNITS", QString((s.shares()).formatMoney(QString(), 2, false)).remove(QRegExp("[^0-9.\\-]"))) .element("UNITPRICE", QString((s.value() / s.shares()).formatMoney(QString(), 2, false)).remove(QRegExp("[^0-9.]"))) .element("TOTAL", QString((-(s.value())).formatMoney(QString(), 2, false)).remove(QRegExp("[^0-9.\\-]"))) .element("SUBACCTSEC", "CASH") .element("SUBACCTFUND", "CASH")) .element("BUYTYPE", "BUY"); } } else if (s.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::ReinvestDividend)) { // Should the TOTAL tag really be negative for a REINVEST? That's very strange, but // it's what they look like coming from my bank, and I can't find any information to refute it. return Tag("REINVEST") .subtag(invtran) .subtag(Tag("SECID").element("UNIQUEID", stockid).element("UNIQUEIDTYPE", "KMYMONEY")) .element("INCOMETYPE", "DIV") .element("TOTAL", QString((-s.value()).formatMoney(QString(), 2, false)).remove(QRegExp("[^0-9.\\-]"))) .element("SUBACCTSEC", "CASH") .element("UNITS", QString((s.shares()).formatMoney(QString(), 2, false)).remove(QRegExp("[^0-9.\\-]"))) .element("UNITPRICE", QString((s.value() / s.shares()).formatMoney(QString(), 2, false)).remove(QRegExp("[^0-9.]"))); } else if (s.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::Dividend)) { // find the split with the category, which has the actual amount of the dividend QList splits = _t.splits(); QList::const_iterator it_split = splits.begin(); bool found = false; while (it_split != splits.end()) { QByteArray accid = (*it_split).accountId(); MyMoneyAccount acc = file->account(accid); if (acc.accountType() == eMyMoney::Account::Type::Income || acc.accountType() == eMyMoney::Account::Type::Expense) { found = true; break; } ++it_split; } if (found) return Tag("INCOME") .subtag(invtran) .subtag(Tag("SECID").element("UNIQUEID", stockid).element("UNIQUEIDTYPE", "KMYMONEY")) .element("INCOMETYPE", "DIV") .element("TOTAL", QString((-(*it_split).value()).formatMoney(QString(), 2, false)).remove(QRegExp("[^0-9\\.\\-]"))) .element("SUBACCTSEC", "CASH") .element("SUBACCTFUND", "CASH"); else return Tag("ERROR").element("DETAILS", "Unable to determine the amount of this income transaction."); } //FIXME: Do something useful with these errors return Tag("ERROR").element("DETAILS", "This transaction contains an unsupported action type"); } #endif KWallet::Wallet *openSynchronousWallet() { using KWallet::Wallet; // first handle the simple case in which we already use the wallet but need the object again in // this case the wallet access permission dialog will no longer appear so we don't need to pass // a valid window id or do anything special since the function call should return immediately const bool alreadyUsingTheWallet = Wallet::users(Wallet::NetworkWallet()).contains("KMyMoney"); if (alreadyUsingTheWallet) { return Wallet::openWallet(Wallet::NetworkWallet(), 0, Wallet::Synchronous); } // search for a suitable parent for the wallet that needs to be deactivated while the // wallet access permission dialog is not dismissed with either accept or reject KWallet::Wallet *wallet = 0; QWidget *parentWidgetForWallet = 0; if (qApp->activeModalWidget()) { parentWidgetForWallet = qApp->activeModalWidget(); } else if (qApp->activeWindow()) { parentWidgetForWallet = qApp->activeWindow(); } else { QList mainWindowList = KMainWindow::memberList(); if (!mainWindowList.isEmpty()) parentWidgetForWallet = mainWindowList.front(); } // only open the wallet synchronously if we have a valid parent otherwise crashes could occur if (parentWidgetForWallet) { // while the wallet is being opened disable the widget to prevent input processing const bool enabled = parentWidgetForWallet->isEnabled(); parentWidgetForWallet->setEnabled(false); wallet = Wallet::openWallet(Wallet::NetworkWallet(), parentWidgetForWallet->winId(), Wallet::Synchronous); parentWidgetForWallet->setEnabled(enabled); } return wallet; } diff --git a/kmymoney/plugins/ofx/import/ofximporter.cpp b/kmymoney/plugins/ofx/import/ofximporter.cpp index ff0f5dcda..a71f4e7b7 100644 --- a/kmymoney/plugins/ofx/import/ofximporter.cpp +++ b/kmymoney/plugins/ofx/import/ofximporter.cpp @@ -1,883 +1,888 @@ /* * Copyright 2005 Ace Jones acejones@users.sourceforge.net * Copyright 2010-2018 Thomas Baumgart tbaumgart@kde.org * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * 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 "ofximporter.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include #include "konlinebankingstatus.h" #include "konlinebankingsetupwizard.h" #include "kofxdirectconnectdlg.h" #include "mymoneyaccount.h" #include "mymoneyexception.h" #include "mymoneystatement.h" #include "mymoneystatementreader.h" #include "statementinterface.h" #include "importinterface.h" #include "viewinterface.h" #include "ui_importoption.h" #include "kmymoneyutils.h" //#define DEBUG_LIBOFX using KWallet::Wallet; class OFXImporter::Private { public: Private() : m_valid(false), m_preferName(PreferId), m_walletIsOpen(false), m_statusDlg(0), m_wallet(0), m_updateStartDate(QDate(1900,1,1)), m_timestampOffset(0) {} bool m_valid; enum NamePreference { PreferId = 0, PreferName, PreferMemo } m_preferName; bool m_walletIsOpen; QList m_statementlist; QList m_securitylist; QString m_fatalerror; QStringList m_infos; QStringList m_warnings; QStringList m_errors; KOnlineBankingStatus* m_statusDlg; Wallet *m_wallet; QDate m_updateStartDate; int m_timestampOffset; }; OFXImporter::OFXImporter(QObject *parent, const QVariantList &args) : KMyMoneyPlugin::Plugin(parent, "ofximporter"), /* * the string in the line above must be the same as * X-KDE-PluginInfo-Name and the provider name assigned in * OfxImporterPlugin::onlineBankingSettings() */ KMyMoneyPlugin::ImporterPlugin(), d(new Private) { Q_UNUSED(args) setComponentName(QStringLiteral("ofximporter"), i18n("OFX Importer")); setXMLFile(QStringLiteral("ofximporter.rc")); createActions(); // For ease announce that we have been loaded. qDebug("Plugins: ofximporter loaded"); } OFXImporter::~OFXImporter() { delete d; qDebug("Plugins: ofximporter unloaded"); } void OFXImporter::createActions() { const auto &kpartgui = QStringLiteral("file_import_ofx"); auto a = actionCollection()->addAction(kpartgui); a->setText(i18n("OFX...")); connect(a, &QAction::triggered, this, static_cast(&OFXImporter::slotImportFile)); connect(viewInterface(), &KMyMoneyPlugin::ViewInterface::viewStateChanged, action(qPrintable(kpartgui)), &QAction::setEnabled); } void OFXImporter::slotImportFile() { QWidget * widget = new QWidget; Ui_ImportOption* option = new Ui_ImportOption; option->setupUi(widget); QUrl url = importInterface()->selectFile(i18n("OFX import file selection"), QString(), QStringLiteral("*.ofx *.qfx *.ofc|OFX files (*.ofx *.qfx *.ofc);;*|All files (*)"), QFileDialog::ExistingFile, widget); d->m_preferName = static_cast(option->m_preferName->currentIndex()); if (url.isValid()) { if (isMyFormat(url.path())) { statementInterface()->resetMessages(); slotImportFile(url.path()); statementInterface()->showMessages(d->m_statementlist.count()); } else { KMessageBox::error(0, i18n("Unable to import %1 using the OFX importer plugin. This file is not the correct format.", url.toDisplayString()), i18n("Incorrect format")); } } delete option; delete widget; } QString OFXImporter::formatName() const { return QStringLiteral("OFX"); } QString OFXImporter::formatFilenameFilter() const { return QStringLiteral("*.ofx *.qfx *.ofc"); } bool OFXImporter::isMyFormat(const QString& filename) const { // filename is considered an Ofx file if it contains // the tag "" or "" in the first 20 lines. // which contain some data bool result = false; QFile f(filename); if (f.open(QIODevice::ReadOnly | QIODevice::Text)) { QTextStream ts(&f); int lineCount = 20; while (!ts.atEnd() && !result && lineCount != 0) { // get a line of data and remove all unnecessary whitepace chars QString line = ts.readLine().simplified(); if (line.contains(QStringLiteral(""), Qt::CaseInsensitive) || line.contains(QStringLiteral(""), Qt::CaseInsensitive)) result = true; // count only lines that contain some non white space chars if (!line.isEmpty()) lineCount--; } f.close(); } return result; } bool OFXImporter::import(const QString& filename) { d->m_fatalerror = i18n("Unable to parse file"); d->m_valid = false; d->m_errors.clear(); d->m_warnings.clear(); d->m_infos.clear(); d->m_statementlist.clear(); d->m_securitylist.clear(); QByteArray filename_deep = QFile::encodeName(filename); ofx_STATUS_msg = true; ofx_INFO_msg = true; ofx_WARNING_msg = true; ofx_ERROR_msg = true; #ifdef DEBUG_LIBOFX ofx_PARSER_msg = true; ofx_DEBUG_msg = true; ofx_DEBUG1_msg = true; ofx_DEBUG2_msg = true; ofx_DEBUG3_msg = true; ofx_DEBUG4_msg = true; ofx_DEBUG5_msg = true; #endif LibofxContextPtr ctx = libofx_get_new_context(); Q_CHECK_PTR(ctx); + // Don't show the position that caused a message to be shown + // This has no setter (see libofx.h) + ofx_show_position = false; + qDebug("setup callback routines"); ofx_set_transaction_cb(ctx, ofxTransactionCallback, this); ofx_set_statement_cb(ctx, ofxStatementCallback, this); ofx_set_account_cb(ctx, ofxAccountCallback, this); ofx_set_security_cb(ctx, ofxSecurityCallback, this); ofx_set_status_cb(ctx, ofxStatusCallback, this); qDebug("process data"); libofx_proc_file(ctx, filename_deep, AUTODETECT); + qDebug("process data done"); libofx_free_context(ctx); if (d->m_valid) { d->m_fatalerror.clear(); d->m_valid = storeStatements(d->m_statementlist); } return d->m_valid; } QString OFXImporter::lastError() const { if (d->m_errors.count() == 0) return d->m_fatalerror; return d->m_errors.join(QStringLiteral("

")); } /* __________________________________________________________________________ * AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA * * Static callbacks for LibOFX * * YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY */ int OFXImporter::ofxTransactionCallback(struct OfxTransactionData data, void * pv) { // kDebug(2) << Q_FUNC_INFO; OFXImporter* pofx = reinterpret_cast(pv); MyMoneyStatement& s = pofx->back(); MyMoneyStatement::Transaction t; if (data.date_posted_valid) { QDateTime dt; dt.setTime_t(data.date_posted - pofx->d->m_timestampOffset * 60); t.m_datePosted = dt.date(); } else if (data.date_initiated_valid) { QDateTime dt; dt.setTime_t(data.date_initiated - pofx->d->m_timestampOffset * 60); t.m_datePosted = dt.date(); } if (t.m_datePosted.isValid()) { // verify the transaction date is one we want if (t.m_datePosted < pofx->d->m_updateStartDate) { //kDebug(0) << "discarding transaction dated" << qPrintable(t.m_datePosted.toString(Qt::ISODate)); return 0; } } if (data.amount_valid) { t.m_amount = MyMoneyMoney(data.amount, 1000); } if (data.check_number_valid) { t.m_strNumber = QString::fromUtf8(data.check_number); } if (data.fi_id_valid) { t.m_strBankID = QStringLiteral("ID ") + QString::fromUtf8(data.fi_id); } else if (data.reference_number_valid) { t.m_strBankID = QStringLiteral("REF ") + QString::fromUtf8(data.reference_number); } // Decide whether to use NAME, PAYEEID or MEMO to construct the payee bool validity[3] = {false, false, false}; QStringList values; switch (pofx->d->m_preferName) { case OFXImporter::Private::PreferId: // PAYEEID default: validity[0] = data.payee_id_valid; validity[1] = data.name_valid; validity[2] = data.memo_valid; values += QString::fromUtf8(data.payee_id); values += QString::fromUtf8(data.name); values += QString::fromUtf8(data.memo); break; case OFXImporter::Private::PreferName: // NAME validity[0] = data.name_valid; validity[1] = data.payee_id_valid; validity[2] = data.memo_valid; values += QString::fromUtf8(data.name); values += QString::fromUtf8(data.payee_id); values += QString::fromUtf8(data.memo); break; case OFXImporter::Private::PreferMemo: // MEMO validity[0] = data.memo_valid; validity[1] = data.payee_id_valid; validity[2] = data.name_valid; values += QString::fromUtf8(data.memo); values += QString::fromUtf8(data.payee_id); values += QString::fromUtf8(data.name); break; } // for investment transactions we don't use the meme as payee if (data.invtransactiontype_valid) { values.clear(); validity[0] = data.payee_id_valid; validity[1] = data.name_valid; validity[2] = false; values += QString::fromUtf8(data.payee_id); values += QString::fromUtf8(data.name); } for (int idx = 0; idx < 3; ++idx) { if (validity[idx]) { t.m_strPayee = values[idx]; break; } } // extract memo field if we haven't used it as payee if ((data.memo_valid) && (pofx->d->m_preferName != OFXImporter::Private::PreferMemo)) { t.m_strMemo = QString::fromUtf8(data.memo); } // If the payee or memo fields are blank, set them to // the other one which is NOT blank. (acejones) if (t.m_strPayee.isEmpty()) { // But we only create a payee for non-investment transactions (ipwizard) if (! t.m_strMemo.isEmpty() && data.invtransactiontype_valid == false) t.m_strPayee = t.m_strMemo; } else { if (t.m_strMemo.isEmpty()) t.m_strMemo = t.m_strPayee; } if (data.security_data_valid) { struct OfxSecurityData* secdata = data.security_data_ptr; if (secdata->ticker_valid) { t.m_strSymbol = QString::fromUtf8(secdata->ticker); } if (secdata->secname_valid) { t.m_strSecurity = QString::fromUtf8(secdata->secname); } } t.m_shares = MyMoneyMoney(); if (data.units_valid) { t.m_shares = MyMoneyMoney(data.units, 100000).reduce(); } t.m_price = MyMoneyMoney(); if (data.unitprice_valid) { t.m_price = MyMoneyMoney(data.unitprice, 100000).reduce(); } t.m_fees = MyMoneyMoney(); if (data.fees_valid) { t.m_fees += MyMoneyMoney(data.fees, 1000).reduce(); } if (data.commission_valid) { t.m_fees += MyMoneyMoney(data.commission, 1000).reduce(); } bool unhandledtype = false; QString type; if (data.invtransactiontype_valid) { switch (data.invtransactiontype) { case OFX_BUYDEBT: case OFX_BUYMF: case OFX_BUYOPT: case OFX_BUYOTHER: case OFX_BUYSTOCK: t.m_eAction = eMyMoney::Transaction::Action::Buy; break; case OFX_REINVEST: t.m_eAction = eMyMoney::Transaction::Action::ReinvestDividend; break; case OFX_SELLDEBT: case OFX_SELLMF: case OFX_SELLOPT: case OFX_SELLOTHER: case OFX_SELLSTOCK: t.m_eAction = eMyMoney::Transaction::Action::Sell; break; case OFX_INCOME: t.m_eAction = eMyMoney::Transaction::Action::CashDividend; // NOTE: With CashDividend, the amount of the dividend should // be in data.amount. Since I've never seen an OFX file with // cash dividends, this is an assumption on my part. (acejones) break; // // These types are all not handled. We will generate a warning for them. // case OFX_CLOSUREOPT: unhandledtype = true; type = QStringLiteral("CLOSUREOPT (Close a position for an option)"); break; case OFX_INVEXPENSE: unhandledtype = true; type = QStringLiteral("INVEXPENSE (Misc investment expense that is associated with a specific security)"); break; case OFX_JRNLFUND: unhandledtype = true; type = QStringLiteral("JRNLFUND (Journaling cash holdings between subaccounts within the same investment account)"); break; case OFX_MARGININTEREST: unhandledtype = true; type = QStringLiteral("MARGININTEREST (Margin interest expense)"); break; case OFX_RETOFCAP: unhandledtype = true; type = QStringLiteral("RETOFCAP (Return of capital)"); break; case OFX_SPLIT: unhandledtype = true; type = QStringLiteral("SPLIT (Stock or mutial fund split)"); break; case OFX_TRANSFER: unhandledtype = true; type = QStringLiteral("TRANSFER (Transfer holdings in and out of the investment account)"); break; default: unhandledtype = true; type = QString("UNKNOWN %1").arg(data.invtransactiontype); break; } } else t.m_eAction = eMyMoney::Transaction::Action::None; // In the case of investment transactions, the 'total' is supposed to the total amount // of the transaction. units * unitprice +/- commission. Easy, right? Sadly, it seems // some ofx creators do not follow this in all circumstances. Therefore, we have to double- // check the total here and adjust it if it's wrong. #if 0 // Even more sadly, this logic is BROKEN. It consistently results in bogus total // values, because of rounding errors in the price. A more through solution would // be to test if the comission alone is causing a discrepency, and adjust in that case. if (data.invtransactiontype_valid && data.unitprice_valid) { double proper_total = t.m_dShares * data.unitprice + t.m_moneyFees; if (proper_total != t.m_moneyAmount) { pofx->addWarning(QString("Transaction %1 has an incorrect total of %2. Using calculated total of %3 instead.").arg(t.m_strBankID).arg(t.m_moneyAmount).arg(proper_total)); t.m_moneyAmount = proper_total; } } #endif if (unhandledtype) pofx->addWarning(QString("Transaction %1 has an unsupported type (%2).").arg(t.m_strBankID, type)); else s.m_listTransactions += t; // kDebug(2) << Q_FUNC_INFO << "return 0 "; return 0; } int OFXImporter::ofxStatementCallback(struct OfxStatementData data, void* pv) { // kDebug(2) << Q_FUNC_INFO; OFXImporter* pofx = reinterpret_cast(pv); MyMoneyStatement& s = pofx->back(); pofx->setValid(); if (data.currency_valid) { s.m_strCurrency = QString::fromUtf8(data.currency); } if (data.account_id_valid) { s.m_strAccountNumber = QString::fromUtf8(data.account_id); } if (data.date_start_valid) { QDateTime dt; dt.setTime_t(data.date_start - pofx->d->m_timestampOffset * 60); s.m_dateBegin = dt.date(); } if (data.date_end_valid) { QDateTime dt; dt.setTime_t(data.date_end - pofx->d->m_timestampOffset * 60); s.m_dateEnd = dt.date(); } if (data.ledger_balance_valid && data.ledger_balance_date_valid) { s.m_closingBalance = MyMoneyMoney(data.ledger_balance); QDateTime dt; dt.setTime_t(data.ledger_balance_date); s.m_dateEnd = dt.date(); } // kDebug(2) << Q_FUNC_INFO << " return 0"; return 0; } int OFXImporter::ofxAccountCallback(struct OfxAccountData data, void * pv) { // kDebug(2) << Q_FUNC_INFO; OFXImporter* pofx = reinterpret_cast(pv); pofx->addnew(); MyMoneyStatement& s = pofx->back(); // Having any account at all makes an ofx statement valid pofx->d->m_valid = true; if (data.account_id_valid) { s.m_strAccountName = QString::fromUtf8(data.account_name); s.m_strAccountNumber = QString::fromUtf8(data.account_id); } if (data.bank_id_valid) { s.m_strRoutingNumber = QString::fromUtf8(data.bank_id); } if (data.broker_id_valid) { s.m_strRoutingNumber = QString::fromUtf8(data.broker_id); } if (data.currency_valid) { s.m_strCurrency = QString::fromUtf8(data.currency); } if (data.account_type_valid) { switch (data.account_type) { case OfxAccountData::OFX_CHECKING : s.m_eType = eMyMoney::Statement::Type::Checkings; break; case OfxAccountData::OFX_SAVINGS : s.m_eType = eMyMoney::Statement::Type::Savings; break; case OfxAccountData::OFX_MONEYMRKT : s.m_eType = eMyMoney::Statement::Type::Investment; break; case OfxAccountData::OFX_CREDITLINE : s.m_eType = eMyMoney::Statement::Type::CreditCard; break; case OfxAccountData::OFX_CMA : s.m_eType = eMyMoney::Statement::Type::CreditCard; break; case OfxAccountData::OFX_CREDITCARD : s.m_eType = eMyMoney::Statement::Type::CreditCard; break; case OfxAccountData::OFX_INVESTMENT : s.m_eType = eMyMoney::Statement::Type::Investment; break; } } // ask KMyMoney for an account id s.m_accountId = pofx->account(QStringLiteral("kmmofx-acc-ref"), QString("%1-%2").arg(s.m_strRoutingNumber, s.m_strAccountNumber)).id(); // copy over the securities s.m_listSecurities = pofx->d->m_securitylist; // kDebug(2) << Q_FUNC_INFO << " return 0"; return 0; } int OFXImporter::ofxSecurityCallback(struct OfxSecurityData data, void* pv) { // kDebug(2) << Q_FUNC_INFO; OFXImporter* pofx = reinterpret_cast(pv); MyMoneyStatement::Security sec; if (data.unique_id_valid) { sec.m_strId = QString::fromUtf8(data.unique_id); } if (data.secname_valid) { sec.m_strName = QString::fromUtf8(data.secname); } if (data.ticker_valid) { sec.m_strSymbol = QString::fromUtf8(data.ticker); } pofx->d->m_securitylist += sec; return 0; } int OFXImporter::ofxStatusCallback(struct OfxStatusData data, void * pv) { // kDebug(2) << Q_FUNC_INFO; OFXImporter* pofx = reinterpret_cast(pv); QString message; // if we got this far, we know we were able to parse the file. // so if it fails after here it can only because there were no actual // accounts in the file! pofx->d->m_fatalerror = i18n("No accounts found."); if (data.ofx_element_name_valid) message.prepend(QString("%1: ").arg(QString::fromUtf8(data.ofx_element_name))); if (data.code_valid) message += QString("%1 (Code %2): %3").arg(QString::fromUtf8(data.name)).arg(data.code).arg(QString::fromUtf8(data.description)); if (data.server_message_valid) message += QString(" (%1)").arg(QString::fromUtf8(data.server_message)); if (data.severity_valid) { switch (data.severity) { case OfxStatusData::INFO: pofx->addInfo(message); break; case OfxStatusData::ERROR: pofx->addError(message); break; case OfxStatusData::WARN: pofx->addWarning(message); break; default: pofx->addWarning(message); pofx->addWarning(QStringLiteral("Previous message was an unknown type. 'WARNING' was assumed.")); break; } } // kDebug(2) << Q_FUNC_INFO << " return 0 "; return 0; } QStringList OFXImporter::importStatement(const MyMoneyStatement &s) { qDebug("OfxImporterPlugin::importStatement start"); return statementInterface()->import(s, false); } MyMoneyAccount OFXImporter::account(const QString& key, const QString& value) const { return statementInterface()->account(key, value); } void OFXImporter::protocols(QStringList& protocolList) const { protocolList.clear(); protocolList << QStringLiteral("OFX"); } QWidget* OFXImporter::accountConfigTab(const MyMoneyAccount& acc, QString& name) { name = i18n("Online settings"); d->m_statusDlg = new KOnlineBankingStatus(acc, 0); return d->m_statusDlg; } MyMoneyKeyValueContainer OFXImporter::onlineBankingSettings(const MyMoneyKeyValueContainer& current) { MyMoneyKeyValueContainer kvp(current); // keep the provider name in sync with the one found in kmm_ofximport.desktop kvp[QStringLiteral("provider")] = objectName().toLower(); if (d->m_statusDlg) { kvp.deletePair(QStringLiteral("appId")); kvp.deletePair(QStringLiteral("kmmofx-headerVersion")); kvp.deletePair(QStringLiteral("password")); d->m_wallet = openSynchronousWallet(); if (d->m_wallet && (d->m_wallet->hasFolder(KWallet::Wallet::PasswordFolder()) || d->m_wallet->createFolder(KWallet::Wallet::PasswordFolder())) && d->m_wallet->setFolder(KWallet::Wallet::PasswordFolder())) { QString key = OFX_PASSWORD_KEY(kvp.value(QStringLiteral("url")), kvp.value(QStringLiteral("uniqueId"))); if (d->m_statusDlg->m_storePassword->isChecked()) { d->m_wallet->writePassword(key, d->m_statusDlg->m_password->text()); } else { if (d->m_wallet->hasEntry(key)) { d->m_wallet->removeEntry(key); } } } else { if (d->m_statusDlg->m_storePassword->isChecked()) { kvp.setValue(QStringLiteral("password"), d->m_statusDlg->m_password->text()); } } if (!d->m_statusDlg->appId().isEmpty()) kvp.setValue(QStringLiteral("appId"), d->m_statusDlg->appId()); kvp.setValue(QStringLiteral("kmmofx-headerVersion"), d->m_statusDlg->headerVersion()); kvp.setValue(QStringLiteral("kmmofx-numRequestDays"), QString::number(d->m_statusDlg->m_numdaysSpin->value())); kvp.setValue(QStringLiteral("kmmofx-todayMinus"), QString::number(d->m_statusDlg->m_todayRB->isChecked())); kvp.setValue(QStringLiteral("kmmofx-lastUpdate"), QString::number(d->m_statusDlg->m_lastUpdateRB->isChecked())); kvp.setValue(QStringLiteral("kmmofx-pickDate"), QString::number(d->m_statusDlg->m_pickDateRB->isChecked())); kvp.setValue(QStringLiteral("kmmofx-specificDate"), d->m_statusDlg->m_specificDate->date().toString()); kvp.setValue(QStringLiteral("kmmofx-preferName"), QString::number(d->m_statusDlg->m_preferredPayee->currentIndex())); if (!d->m_statusDlg->m_clientUidEdit->text().isEmpty()) kvp.setValue(QStringLiteral("clientUid"), d->m_statusDlg->m_clientUidEdit->text()); else kvp.deletePair(QStringLiteral("clientUid")); if (d->m_statusDlg->m_timestampOffset->time().msecsSinceStartOfDay() == 0) { kvp.deletePair(QStringLiteral("kmmofx-timestampOffset")); } else { // get offset in minutes int offset = d->m_statusDlg->m_timestampOffset->time().msecsSinceStartOfDay() / 1000 / 60; if (d->m_statusDlg->m_timestampOffsetSign->currentText() == QStringLiteral("-")) { offset = -offset; } kvp.setValue(QStringLiteral("kmmofx-timestampOffset"), QString::number(offset)); } // get rid of pre 4.6 values kvp.deletePair(QStringLiteral("kmmofx-preferPayeeid")); } return kvp; } bool OFXImporter::mapAccount(const MyMoneyAccount& acc, MyMoneyKeyValueContainer& settings) { Q_UNUSED(acc); bool rc = false; QPointer wiz = new KOnlineBankingSetupWizard(0); if (wiz->isInit()) { if (wiz->exec() == QDialog::Accepted) { rc = wiz->chosenSettings(settings); } } delete wiz; return rc; } bool OFXImporter::updateAccount(const MyMoneyAccount& acc, bool moreAccounts) { Q_UNUSED(moreAccounts); qDebug("OfxImporterPlugin::updateAccount"); try { if (!acc.id().isEmpty()) { // Save the value of preferName to be used by ofxTransactionCallback d->m_preferName = static_cast(acc.onlineBankingSettings().value(QStringLiteral("kmmofx-preferName")).toInt()); QPointer dlg = new KOfxDirectConnectDlg(acc); connect(dlg.data(), &KOfxDirectConnectDlg::statementReady, this, static_cast(&OFXImporter::slotImportFile)); // get the date of the earliest transaction that we are interested in // from the settings for this account MyMoneyKeyValueContainer settings = acc.onlineBankingSettings(); if (!settings.value(QStringLiteral("provider")).isEmpty()) { if ((settings.value(QStringLiteral("kmmofx-todayMinus")).toInt() != 0) && !settings.value(QStringLiteral("kmmofx-numRequestDays")).isEmpty()) { //kDebug(0) << "start date = today minus"; d->m_updateStartDate = QDate::currentDate().addDays(-settings.value(QStringLiteral("kmmofx-numRequestDays")).toInt()); } else if ((settings.value(QStringLiteral("kmmofx-lastUpdate")).toInt() != 0) && !acc.value(QStringLiteral("lastImportedTransactionDate")).isEmpty()) { //kDebug(0) << "start date = last update"; d->m_updateStartDate = QDate::fromString(acc.value(QStringLiteral("lastImportedTransactionDate")), Qt::ISODate); } else if ((settings.value(QStringLiteral("kmmofx-pickDate")).toInt() != 0) && !settings.value(QStringLiteral("kmmofx-specificDate")).isEmpty()) { //kDebug(0) << "start date = pick date"; d->m_updateStartDate = QDate::fromString(settings.value(QStringLiteral("kmmofx-specificDate"))); } else { //kDebug(0) << "start date = today - 2 months"; d->m_updateStartDate = QDate::currentDate().addMonths(-2); } } d->m_timestampOffset = settings.value("kmmofx-timestampOffset").toInt(); //kDebug(0) << "ofx plugin: account" << acc.name() << "earliest transaction date to process =" << qPrintable(d->m_updateStartDate.toString(Qt::ISODate)); if (dlg->init()) dlg->exec(); delete dlg; // reset the earliest-interesting-transaction date to the non-specific account setting d->m_updateStartDate = QDate(1900,1,1); d->m_timestampOffset = 0; } } catch (const MyMoneyException &e) { KMessageBox::information(0 , i18n("Error connecting to bank: %1", QString::fromLatin1(e.what()))); } return false; } void OFXImporter::slotImportFile(const QString& url) { qDebug("OfxImporterPlugin::slotImportFile"); if (!import(url)) { KMessageBox::error(0, QString("%1").arg(i18n("

Unable to import '%1' using the OFX importer plugin. The plugin returned the following error:

%2

", url, lastError())), i18n("Importing error")); } } bool OFXImporter::storeStatements(const QList &statements) { if (statements.isEmpty()) return true; auto ok = true; auto abort = false; // FIXME Deal with warnings/errors coming back from plugins /*if ( ofx.errors().count() ) { if ( KMessageBox::warningContinueCancelList(this,i18n("The following errors were returned from your bank"),ofx.errors(),i18n("OFX Errors")) == KMessageBox::Cancel ) abort = true; } if ( ofx.warnings().count() ) { if ( KMessageBox::warningContinueCancelList(this,i18n("The following warnings were returned from your bank"),ofx.warnings(),i18n("OFX Warnings"),KStandardGuiItem::cont(),"ofxwarnings") == KMessageBox::Cancel ) abort = true; }*/ qDebug("OfxImporterPlugin::storeStatements() with %d statements called", statements.count()); for (const auto& statement : statements) { if (abort) break; if (importStatement(statement).isEmpty()) ok = false; } if (!ok) KMessageBox::error(nullptr, i18n("Importing process terminated unexpectedly."), i18n("Failed to import all statements.")); return ok; } void OFXImporter::addnew() { d->m_statementlist.push_back(MyMoneyStatement()); } MyMoneyStatement& OFXImporter::back() { return d->m_statementlist.back(); } bool OFXImporter::isValid() const { return d->m_valid; } void OFXImporter::setValid() { d->m_valid = true; } void OFXImporter::addInfo(const QString& _msg) { d->m_infos += _msg; } void OFXImporter::addWarning(const QString& _msg) { d->m_warnings += _msg; } void OFXImporter::addError(const QString& _msg) { d->m_errors += _msg; } const QStringList& OFXImporter::infos() const // krazy:exclude=spelling { return d->m_infos; } const QStringList& OFXImporter::warnings() const { return d->m_warnings; } const QStringList& OFXImporter::errors() const { return d->m_errors; } K_PLUGIN_FACTORY_WITH_JSON(OFXImporterFactory, "ofximporter.json", registerPlugin();) #include "ofximporter.moc" diff --git a/kmymoney/widgets/kmymoneyselector.cpp b/kmymoney/widgets/kmymoneyselector.cpp index 238e7b45d..2ae41b887 100644 --- a/kmymoney/widgets/kmymoneyselector.cpp +++ b/kmymoney/widgets/kmymoneyselector.cpp @@ -1,570 +1,580 @@ /* * Copyright 2006-2018 Thomas Baumgart * Copyright 2017 Łukasz Wojniłowicz * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "kmymoneyselector_p.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes #include "kmymoneysettings.h" #include "widgetenums.h" using namespace eWidgets; KMyMoneySelector::KMyMoneySelector(QWidget *parent, Qt::WindowFlags flags) : QWidget(parent, flags), d_ptr(new KMyMoneySelectorPrivate(this)) { Q_D(KMyMoneySelector); d->init(); } KMyMoneySelector::KMyMoneySelector(KMyMoneySelectorPrivate &dd, QWidget* parent, Qt::WindowFlags flags) : QWidget(parent, flags), d_ptr(&dd) { Q_D(KMyMoneySelector); d->init(); } KMyMoneySelector::~KMyMoneySelector() { Q_D(KMyMoneySelector); delete d; } void KMyMoneySelector::clear() { Q_D(KMyMoneySelector); d->m_treeWidget->clear(); } void KMyMoneySelector::setSelectable(QTreeWidgetItem *item, bool selectable) { if (selectable) { item->setFlags(item->flags() | Qt::ItemIsSelectable); } else { item->setFlags(item->flags() & ~Qt::ItemIsSelectable); } } void KMyMoneySelector::slotSelectAllItems() { selectAllItems(true); } void KMyMoneySelector::slotDeselectAllItems() { selectAllItems(false); } void KMyMoneySelector::setSelectionMode(const QTreeWidget::SelectionMode mode) { Q_D(KMyMoneySelector); if (d->m_selMode != mode) { d->m_selMode = mode; clear(); // make sure, it's either Multi or Single if (mode != QTreeWidget::MultiSelection) { d->m_selMode = QTreeWidget::SingleSelection; connect(d->m_treeWidget, &QTreeWidget::itemSelectionChanged, this, &KMyMoneySelector::stateChanged); connect(d->m_treeWidget, &QTreeWidget::itemActivated, this, &KMyMoneySelector::slotItemSelected); connect(d->m_treeWidget, &QTreeWidget::itemClicked, this, &KMyMoneySelector::slotItemSelected); } else { disconnect(d->m_treeWidget, &QTreeWidget::itemSelectionChanged, this, &KMyMoneySelector::stateChanged); disconnect(d->m_treeWidget, &QTreeWidget::itemActivated, this, &KMyMoneySelector::slotItemSelected); disconnect(d->m_treeWidget, &QTreeWidget::itemClicked, this, &KMyMoneySelector::slotItemSelected); } } QWidget::update(); } QTreeWidget::SelectionMode KMyMoneySelector::selectionMode() const { Q_D(const KMyMoneySelector); return d->m_selMode; } void KMyMoneySelector::slotItemSelected(QTreeWidgetItem *item) { Q_D(KMyMoneySelector); if (d->m_selMode == QTreeWidget::SingleSelection) { if (item && item->flags().testFlag(Qt::ItemIsSelectable)) { emit itemSelected(item->data(0, (int)Selector::Role::Id).toString()); } } } QTreeWidgetItem* KMyMoneySelector::newItem(const QString& name, QTreeWidgetItem* after, const QString& key, const QString& id) { Q_D(KMyMoneySelector); QTreeWidgetItem* item = new QTreeWidgetItem(d->m_treeWidget, after); item->setText(0, name); item->setData(0, (int)Selector::Role::Key, key); item->setData(0, (int)Selector::Role::Id, id); item->setText(1, key); // hidden, but used for sorting item->setFlags(item->flags() & ~Qt::ItemIsUserCheckable); if (id.isEmpty()) { QFont font = item->font(0); font.setBold(true); item->setFont(0, font); setSelectable(item, false); } item->setExpanded(true); return item; } QTreeWidgetItem* KMyMoneySelector::newItem(const QString& name, QTreeWidgetItem* after, const QString& key) { return newItem(name, after, key, QString()); } QTreeWidgetItem* KMyMoneySelector::newItem(const QString& name, QTreeWidgetItem* after) { return newItem(name, after, QString(), QString()); } QTreeWidgetItem* KMyMoneySelector::newItem(const QString& name, const QString& key, const QString& id) { return newItem(name, 0, key, id); } QTreeWidgetItem* KMyMoneySelector::newItem(const QString& name, const QString& key) { return newItem(name, 0, key, QString()); } QTreeWidgetItem* KMyMoneySelector::newItem(const QString& name) { return newItem(name, 0, QString(), QString()); } QTreeWidgetItem* KMyMoneySelector::newTopItem(const QString& name, const QString& key, const QString& id) { Q_D(KMyMoneySelector); QTreeWidgetItem* item = new QTreeWidgetItem(d->m_treeWidget); item->setText(0, name); item->setData(0, (int)Selector::Role::Key, key); item->setData(0, (int)Selector::Role::Id, id); item->setText(1, key); // hidden, but used for sorting item->setFlags(item->flags() & ~Qt::ItemIsUserCheckable); if (d->m_selMode == QTreeWidget::MultiSelection) { item->setFlags(item->flags() | Qt::ItemIsUserCheckable); item->setCheckState(0, Qt::Checked); } return item; } QTreeWidgetItem* KMyMoneySelector::newItem(QTreeWidgetItem* parent, const QString& name, const QString& key, const QString& id) { Q_D(KMyMoneySelector); QTreeWidgetItem* item = new QTreeWidgetItem(parent); item->setText(0, name); item->setData(0, (int)Selector::Role::Key, key); item->setData(0, (int)Selector::Role::Id, id); item->setText(1, key); // hidden, but used for sorting item->setFlags(item->flags() & ~Qt::ItemIsUserCheckable); if (d->m_selMode == QTreeWidget::MultiSelection) { item->setFlags(item->flags() | Qt::ItemIsUserCheckable); item->setCheckState(0, Qt::Checked); } return item; } void KMyMoneySelector::protectItem(const QString& itemId, const bool protect) { Q_D(KMyMoneySelector); QTreeWidgetItemIterator it(d->m_treeWidget, QTreeWidgetItemIterator::Selectable); QTreeWidgetItem* it_v; // scan items while ((it_v = *it) != 0) { if (it_v->data(0, (int)Selector::Role::Id).toString() == itemId) { setSelectable(it_v, !protect); break; } ++it; } } QTreeWidgetItem* KMyMoneySelector::item(const QString& id) const { Q_D(const KMyMoneySelector); QTreeWidgetItemIterator it(d->m_treeWidget, QTreeWidgetItemIterator::Selectable); QTreeWidgetItem* it_v; while ((it_v = *it) != 0) { if (it_v->data(0, (int)Selector::Role::Id).toString() == id) break; ++it; } return it_v; } bool KMyMoneySelector::allItemsSelected() const { Q_D(const KMyMoneySelector); QTreeWidgetItem* rootItem = d->m_treeWidget->invisibleRootItem(); if (d->m_selMode == QTreeWidget::SingleSelection) return false; for (auto i = 0; i < rootItem->childCount(); ++i) { QTreeWidgetItem* item = rootItem->child(i); if (item->flags().testFlag(Qt::ItemIsUserCheckable)) { if (!(item->checkState(0) == Qt::Checked && allItemsSelected(item))) return false; } else { if (!allItemsSelected(item)) return false; } } return true; } bool KMyMoneySelector::allItemsSelected(const QTreeWidgetItem *item) const { for (auto i = 0; i < item->childCount(); ++i) { QTreeWidgetItem* child = item->child(i); if (child->flags().testFlag(Qt::ItemIsUserCheckable)) { if (!(child->checkState(0) == Qt::Checked && allItemsSelected(child))) return false; } } return true; } void KMyMoneySelector::removeItem(const QString& id) { Q_D(KMyMoneySelector); QTreeWidgetItem* it_v; QTreeWidgetItemIterator it(d->m_treeWidget); while ((it_v = *it) != 0) { if (id == it_v->data(0, (int)Selector::Role::Id).toString()) { if (it_v->childCount() > 0) { setSelectable(it_v, false); } else { delete it_v; } } it++; } // get rid of top items that just lost the last children (e.g. Favorites) it = QTreeWidgetItemIterator(d->m_treeWidget, QTreeWidgetItemIterator::NotSelectable); while ((it_v = *it) != 0) { if (it_v->childCount() == 0) delete it_v; it++; } } void KMyMoneySelector::selectAllItems(const bool state) { Q_D(KMyMoneySelector); selectAllSubItems(d->m_treeWidget->invisibleRootItem(), state); emit stateChanged(); } void KMyMoneySelector::selectItems(const QStringList& itemList, const bool state) { Q_D(KMyMoneySelector); selectSubItems(d->m_treeWidget->invisibleRootItem(), itemList, state); emit stateChanged(); } void KMyMoneySelector::selectSubItems(QTreeWidgetItem* item, const QStringList& itemList, const bool state) { for (auto i = 0; i < item->childCount(); ++i) { QTreeWidgetItem* child = item->child(i); if (child->flags().testFlag(Qt::ItemIsUserCheckable) && itemList.contains(child->data(0, (int)Selector::Role::Id).toString())) { child->setCheckState(0, state ? Qt::Checked : Qt::Unchecked); } selectSubItems(child, itemList, state); } emit stateChanged(); } void KMyMoneySelector::selectAllSubItems(QTreeWidgetItem* item, const bool state) { for (auto i = 0; i < item->childCount(); ++i) { QTreeWidgetItem* child = item->child(i); if (child->flags().testFlag(Qt::ItemIsUserCheckable)) { child->setCheckState(0, state ? Qt::Checked : Qt::Unchecked); } selectAllSubItems(child, state); } emit stateChanged(); } void KMyMoneySelector::selectedItems(QStringList& list) const { Q_D(const KMyMoneySelector); list.clear(); if (d->m_selMode == QTreeWidget::SingleSelection) { QTreeWidgetItem* it_c = d->m_treeWidget->currentItem(); if (it_c != 0) list << it_c->data(0, (int)Selector::Role::Id).toString(); } else { QTreeWidgetItem* rootItem = d->m_treeWidget->invisibleRootItem(); for (auto i = 0; i < rootItem->childCount(); ++i) { QTreeWidgetItem* child = rootItem->child(i); if (child->flags().testFlag(Qt::ItemIsUserCheckable)) { if (child->checkState(0) == Qt::Checked) list << child->data(0, (int)Selector::Role::Id).toString(); } selectedItems(list, child); } } } void KMyMoneySelector::selectedItems(QStringList& list, QTreeWidgetItem* item) const { for (auto i = 0; i < item->childCount(); ++i) { QTreeWidgetItem* child = item->child(i); if (child->flags().testFlag(Qt::ItemIsUserCheckable)) { if (child->checkState(0) == Qt::Checked) list << child->data(0, (int)Selector::Role::Id).toString(); } selectedItems(list, child); } } void KMyMoneySelector::itemList(QStringList& list) const { Q_D(const KMyMoneySelector); QTreeWidgetItemIterator it(d->m_treeWidget, QTreeWidgetItemIterator::Selectable); QTreeWidgetItem* it_v; while ((it_v = *it) != 0) { list << it_v->data(0, (int)Selector::Role::Id).toString(); it++; } } void KMyMoneySelector::setSelected(const QString& id, const bool state) { Q_D(const KMyMoneySelector); QTreeWidgetItemIterator it(d->m_treeWidget, QTreeWidgetItemIterator::Selectable); QTreeWidgetItem* item; QTreeWidgetItem* it_visible = 0; while ((item = *it) != 0) { if (item->data(0, (int)Selector::Role::Id).toString() == id) { if (item->flags().testFlag(Qt::ItemIsUserCheckable)) { item->setCheckState(0, state ? Qt::Checked : Qt::Unchecked); } d->m_treeWidget->setCurrentItem(item); if (!it_visible) it_visible = item; } it++; } // make sure the first one found is visible if (it_visible) d->m_treeWidget->scrollToItem(it_visible); } QTreeWidget* KMyMoneySelector::listView() const { Q_D(const KMyMoneySelector); return d->m_treeWidget; } int KMyMoneySelector::slotMakeCompletion(const QString& _txt) { QString txt(QRegExp::escape(_txt)); if (KMyMoneySettings::stringMatchFromStart() && QLatin1String(this->metaObject()->className()) == QLatin1String("KMyMoneySelector")) txt.prepend('^'); return slotMakeCompletion(QRegExp(txt, Qt::CaseInsensitive)); } bool KMyMoneySelector::match(const QRegExp& exp, QTreeWidgetItem* item) const { return exp.indexIn(item->text(0)) != -1; } -int KMyMoneySelector::slotMakeCompletion(const QRegExp& exp) +int KMyMoneySelector::slotMakeCompletion(const QRegExp& _exp) { Q_D(KMyMoneySelector); + auto exp(_exp); + QString pattern = exp.pattern(); + if (exp.patternSyntax() == QRegExp::RegExp) { + auto replacement = QStringLiteral(".*:"); + if (!KMyMoneySettings::stringMatchFromStart() || QLatin1String(this->metaObject()->className()) != QLatin1String("KMyMoneySelector")) { + replacement.append(QLatin1String(".*")); + } + pattern.replace(QLatin1String(":"), replacement); + exp.setPattern(pattern); + } QTreeWidgetItemIterator it(d->m_treeWidget, QTreeWidgetItemIterator::Selectable); QTreeWidgetItem* it_v; // The logic used here seems to be awkward. The problem is, that // QListViewItem::setVisible works recursively on all it's children // and grand-children. // // The way out of this is as follows: Make all items visible. // Then go through the list again and perform the checks. // If an item does not have any children (last leaf in the tree view) // perform the check. Then check recursively on the parent of this // leaf that it has no visible children. If that is the case, make the // parent invisible and continue this check with it's parent. while ((it_v = *it) != 0) { it_v->setHidden(false); ++it; } QTreeWidgetItem* firstMatch = 0; if (!exp.pattern().isEmpty()) { it = QTreeWidgetItemIterator(d->m_treeWidget, QTreeWidgetItemIterator::Selectable); while ((it_v = *it) != 0) { if (it_v->childCount() == 0) { if (!match(exp, it_v)) { // this is a node which does not contain the // text and does not have children. So we can // safely hide it. Then we check, if the parent // has more children which are still visible. If // none are found, the parent node is hidden also. We // continue until the top of the tree or until we // find a node that still has visible children. bool hide = true; while (hide) { it_v->setHidden(true); it_v = it_v->parent(); if (it_v && (it_v->flags() & Qt::ItemIsSelectable)) { hide = !match(exp, it_v); for (auto i = 0; hide && i < it_v->childCount(); ++i) { if (!it_v->child(i)->isHidden()) hide = false; } } else hide = false; } } else if (!firstMatch) { firstMatch = it_v; } ++it; } else if (match(exp, it_v)) { if (!firstMatch) { firstMatch = it_v; } // a node with children contains the text. We want // to display all child nodes in this case, so we need // to advance the iterator to the next sibling of the // current node. This could well be the sibling of a // parent or grandparent node. QTreeWidgetItem* curr = it_v; QTreeWidgetItem* item; while ((item = curr->treeWidget()->itemBelow(curr)) == 0) { curr = curr->parent(); if (curr == 0) break; if (match(exp, curr)) firstMatch = curr; } do { ++it; } while (*it && *it != item); } else { // It's a node with children that does not match. We don't // change it's status here. ++it; } } } // make the first match the one that is selected // if we have no match, make sure none is selected if (d->m_selMode == QTreeWidget::SingleSelection) { if (firstMatch) { d->m_treeWidget->setCurrentItem(firstMatch); d->m_treeWidget->scrollToItem(firstMatch); } else d->m_treeWidget->clearSelection(); } // Get the number of visible nodes for the return code auto cnt = 0; it = QTreeWidgetItemIterator(d->m_treeWidget, QTreeWidgetItemIterator::Selectable | QTreeWidgetItemIterator::NotHidden); while ((it_v = *it) != 0) { cnt++; it++; } return cnt; } bool KMyMoneySelector::contains(const QString& txt) const { Q_D(const KMyMoneySelector); QTreeWidgetItemIterator it(d->m_treeWidget, QTreeWidgetItemIterator::Selectable); QTreeWidgetItem* it_v; while ((it_v = *it) != 0) { if (it_v->text(0) == txt) { return true; } it++; } return false; } void KMyMoneySelector::slotItemPressed(QTreeWidgetItem* item, int /* col */) { Q_D(KMyMoneySelector); if (QApplication::mouseButtons() != Qt::RightButton) return; if (item->flags().testFlag(Qt::ItemIsUserCheckable)) { QStyleOptionButton opt; opt.rect = d->m_treeWidget->visualItemRect(item); QRect rect = d->m_treeWidget->style()->subElementRect(QStyle::SE_ViewItemCheckIndicator, &opt, d->m_treeWidget); if (rect.contains(d->m_treeWidget->mapFromGlobal(QCursor::pos()))) { // we get down here, if we have a right click onto the checkbox item->setCheckState(0, item->checkState(0) == Qt::Checked ? Qt::Unchecked : Qt::Checked); selectAllSubItems(item, item->checkState(0) == Qt::Checked); } } } QStringList KMyMoneySelector::selectedItems() const { QStringList list; selectedItems(list); return list; } QStringList KMyMoneySelector::itemList() const { QStringList list; itemList(list); return list; }