File indexing completed on 2024-05-12 05:06:04
0001 /* 0002 SPDX-FileCopyrightText: 2000-2004 Michael Edwardes <mte@users.sourceforge.net> 0003 SPDX-FileCopyrightText: 2000-2004 Javier Campos Morales <javi_c@users.sourceforge.net> 0004 SPDX-FileCopyrightText: 2000-2004 Felix Rodriguez <frodriguez@users.sourceforge.net> 0005 SPDX-FileCopyrightText: 2000-2004 John C <thetacoturtle@users.sourceforge.net> 0006 SPDX-FileCopyrightText: 2000-2004 Thomas Baumgart <ipwizard@users.sourceforge.net> 0007 SPDX-FileCopyrightText: 2000-2004 Kevin Tambascio <ktambascio@users.sourceforge.net> 0008 SPDX-FileCopyrightText: 2000-2004 Ace Jones <acejones@users.sourceforge.net> 0009 SPDX-License-Identifier: GPL-2.0-or-later 0010 */ 0011 0012 #include "mymoneystatementreader.h" 0013 0014 // ---------------------------------------------------------------------------- 0015 // QT Headers 0016 0017 #include <QApplication> 0018 #include <QDialog> 0019 #include <QDialogButtonBox> 0020 #include <QLabel> 0021 #include <QList> 0022 #include <QStringList> 0023 #include <QVBoxLayout> 0024 0025 // ---------------------------------------------------------------------------- 0026 // KDE Headers 0027 0028 #include <KMessageBox> 0029 #include <KConfig> 0030 #include <KSharedConfig> 0031 #include <KConfigGroup> 0032 #include <KGuiItem> 0033 #include <KLocalizedString> 0034 0035 // ---------------------------------------------------------------------------- 0036 // Project Headers 0037 0038 #include "accountsmodel.h" 0039 #include "amountedit.h" 0040 #include "dialogenums.h" 0041 #include "existingtransactionmatchfinder.h" 0042 #include "kaccountselectdlg.h" 0043 #include "kmymoneyaccountcombo.h" 0044 #include "kmymoneymvccombo.h" 0045 #include "kmymoneysettings.h" 0046 #include "kmymoneyutils.h" 0047 #include "knewaccountwizard.h" 0048 #include "knewinvestmentwizard.h" 0049 #include "mymoneyaccount.h" 0050 #include "mymoneyenums.h" 0051 #include "mymoneyexception.h" 0052 #include "mymoneyfile.h" 0053 #include "mymoneypayee.h" 0054 #include "mymoneyprice.h" 0055 #include "mymoneysecurity.h" 0056 #include "mymoneystatement.h" 0057 #include "mymoneytransactionfilter.h" 0058 #include "mymoneyutils.h" 0059 #include "payeesmodel.h" 0060 #include "scheduledtransactionmatchfinder.h" 0061 #include "transactionmatcher.h" 0062 0063 #include "kmmyesno.h" 0064 0065 using namespace eMyMoney; 0066 0067 bool matchNotEmpty(const QString &l, const QString &r) 0068 { 0069 return !l.isEmpty() && QString::compare(l, r, Qt::CaseInsensitive) == 0; 0070 } 0071 0072 Q_GLOBAL_STATIC(QStringList, globalResultMessages); 0073 0074 class MyMoneyStatementReader::Private 0075 { 0076 public: 0077 Private() : 0078 transactionsCount(0), 0079 transactionsAdded(0), 0080 transactionsMatched(0), 0081 transactionsDuplicate(0), 0082 m_skipCategoryMatching(true), 0083 m_progressCallback(nullptr), 0084 scannedCategories(false) {} 0085 0086 const QString& feeId(const MyMoneyAccount& invAcc); 0087 const QString& interestId(const MyMoneyAccount& invAcc); 0088 QString interestId(const QString& name); 0089 QString expenseId(const QString& name); 0090 QString feeId(const QString& name); 0091 void assignUniqueBankID(MyMoneySplit& s, const MyMoneyStatement::Transaction& t_in); 0092 void setupPrice(MyMoneySplit &s, const MyMoneyAccount &splitAccount, const MyMoneyAccount &transactionAccount, const QDate &postDate); 0093 0094 MyMoneyAccount lastAccount; 0095 MyMoneyAccount m_account; 0096 MyMoneyAccount m_brokerageAccount; 0097 QList<MyMoneyPayee> payees; 0098 int transactionsCount; 0099 int transactionsAdded; 0100 int transactionsMatched; 0101 int transactionsDuplicate; 0102 QMap<QString, bool> uniqIds; 0103 QMap<QString, MyMoneySecurity> securitiesBySymbol; 0104 QMap<QString, MyMoneySecurity> securitiesByName; 0105 bool m_skipCategoryMatching; 0106 void (*m_progressCallback)(int, int, const QString&); 0107 QDate m_oldestPostDate; 0108 0109 private: 0110 void scanCategories(QString& id, const MyMoneyAccount& invAcc, const MyMoneyAccount& parentAccount, const QString& defaultName); 0111 /** 0112 * This method tries to figure out the category to be used for fees and interest 0113 * from previous transactions in the given @a investmentAccount and returns the 0114 * ids of those categories in @a feesId and @a interestId. The last used category 0115 * will be returned. 0116 */ 0117 void previouslyUsedCategories(const QString& investmentAccount, QString& feesId, QString& interestId); 0118 0119 QString nameToId(const QString& name, const MyMoneyAccount& parent); 0120 0121 private: 0122 QString m_feeId; 0123 QString m_interestId; 0124 bool scannedCategories; 0125 }; 0126 0127 0128 const QString& MyMoneyStatementReader::Private::feeId(const MyMoneyAccount& invAcc) 0129 { 0130 scanCategories(m_feeId, invAcc, MyMoneyFile::instance()->expense(), i18n("_Fees")); 0131 return m_feeId; 0132 } 0133 0134 const QString& MyMoneyStatementReader::Private::interestId(const MyMoneyAccount& invAcc) 0135 { 0136 scanCategories(m_interestId, invAcc, MyMoneyFile::instance()->income(), i18n("_Dividend")); 0137 return m_interestId; 0138 } 0139 0140 QString MyMoneyStatementReader::Private::nameToId(const QString& name, const MyMoneyAccount& parent) 0141 { 0142 // Adapted from KMyMoneyApp::createAccount(MyMoneyAccount& newAccount, MyMoneyAccount& parentAccount, MyMoneyAccount& brokerageAccount, MyMoneyMoney openingBal) 0143 // Needed to find/create category:sub-categories 0144 MyMoneyFile* file = MyMoneyFile::instance(); 0145 0146 QString id = file->categoryToAccount(name, Account::Type::Unknown); 0147 // if it does not exist, we have to create it 0148 if (id.isEmpty()) { 0149 MyMoneyAccount newAccount; 0150 MyMoneyAccount parentAccount = parent; 0151 newAccount.setName(name) ; 0152 int pos; 0153 // check for ':' in the name and use it as separator for a hierarchy 0154 while ((pos = newAccount.name().indexOf(MyMoneyFile::AccountSeparator)) != -1) { 0155 QString part = newAccount.name().left(pos); 0156 QString remainder = newAccount.name().mid(pos + 1); 0157 const MyMoneyAccount& existingAccount = file->subAccountByName(parentAccount, part); 0158 if (existingAccount.id().isEmpty()) { 0159 newAccount.setName(part); 0160 newAccount.setAccountType(parentAccount.accountType()); 0161 file->addAccount(newAccount, parentAccount); 0162 parentAccount = newAccount; 0163 } else { 0164 parentAccount = existingAccount; 0165 } 0166 newAccount.setParentAccountId(QString()); // make sure, there's no parent 0167 newAccount.clearId(); // and no id set for adding 0168 newAccount.removeAccountIds(); // and no sub-account ids 0169 newAccount.setName(remainder); 0170 }//end while 0171 newAccount.setAccountType(parentAccount.accountType()); 0172 0173 // make sure we have a currency. If none is assigned, we assume base currency 0174 if (newAccount.currencyId().isEmpty()) 0175 newAccount.setCurrencyId(file->baseCurrency().id()); 0176 0177 file->addAccount(newAccount, parentAccount); 0178 id = newAccount.id(); 0179 } 0180 return id; 0181 } 0182 0183 QString MyMoneyStatementReader::Private::expenseId(const QString& name) 0184 { 0185 MyMoneyAccount parent = MyMoneyFile::instance()->expense(); 0186 return nameToId(name, parent); 0187 } 0188 0189 QString MyMoneyStatementReader::Private::interestId(const QString& name) 0190 { 0191 MyMoneyAccount parent = MyMoneyFile::instance()->income(); 0192 return nameToId(name, parent); 0193 } 0194 0195 QString MyMoneyStatementReader::Private::feeId(const QString& name) 0196 { 0197 MyMoneyAccount parent = MyMoneyFile::instance()->expense(); 0198 return nameToId(name, parent); 0199 } 0200 0201 void MyMoneyStatementReader::Private::previouslyUsedCategories(const QString& investmentAccount, QString& feesId, QString& interestId) 0202 { 0203 feesId.clear(); 0204 interestId.clear(); 0205 MyMoneyFile* file = MyMoneyFile::instance(); 0206 try { 0207 MyMoneyAccount acc = file->account(investmentAccount); 0208 MyMoneyTransactionFilter filter(investmentAccount); 0209 filter.setReportAllSplits(false); 0210 // since we assume an investment account here, we need to collect the stock accounts as well 0211 filter.addAccount(acc.accountList()); 0212 QList< QPair<MyMoneyTransaction, MyMoneySplit> > list; 0213 file->transactionList(list, filter); 0214 QList< QPair<MyMoneyTransaction, MyMoneySplit> >::const_iterator it_t; 0215 for (it_t = list.constBegin(); it_t != list.constEnd(); ++it_t) { 0216 const MyMoneyTransaction& t = (*it_t).first; 0217 MyMoneySplit s = (*it_t).second; 0218 0219 acc = file->account(s.accountId()); 0220 // stock split shouldn't be fee or interest because it won't play nice with dissectTransaction 0221 // it was caused by processTransactionEntry adding splits in wrong order != with manual transaction entering 0222 if (acc.accountGroup() == Account::Type::Expense || acc.accountGroup() == Account::Type::Income) { 0223 const auto splits = t.splits(); 0224 for (const auto& sNew : splits) { 0225 acc = file->account(sNew.accountId()); 0226 if (acc.accountGroup() != Account::Type::Expense && // shouldn't be fee 0227 acc.accountGroup() != Account::Type::Income && // shouldn't be interest 0228 ((sNew.value() != sNew.shares()) || // shouldn't be checking account... 0229 (sNew.price() != MyMoneyMoney::ONE))) { // ...but sometimes it may look like checking account 0230 s = sNew; 0231 break; 0232 } 0233 } 0234 } 0235 0236 MyMoneySplit assetAccountSplit; 0237 QList<MyMoneySplit> feeSplits; 0238 QList<MyMoneySplit> interestSplits; 0239 MyMoneySecurity security; 0240 MyMoneySecurity currency; 0241 eMyMoney::Split::InvestmentTransactionType transactionType; 0242 MyMoneyUtils::dissectTransaction(t, s, assetAccountSplit, feeSplits, interestSplits, security, currency, transactionType); 0243 if (!feeSplits.isEmpty()) { 0244 feesId = feeSplits.first().accountId(); 0245 if (!interestId.isEmpty()) 0246 break; 0247 } 0248 if (!interestSplits.isEmpty()) { 0249 interestId = interestSplits.first().accountId(); 0250 if (!feesId.isEmpty()) 0251 break; 0252 } 0253 } 0254 } catch (const MyMoneyException &) { 0255 } 0256 0257 } 0258 0259 void MyMoneyStatementReader::Private::scanCategories(QString& id, const MyMoneyAccount& invAcc, const MyMoneyAccount& parentAccount, const QString& defaultName) 0260 { 0261 if (!scannedCategories) { 0262 previouslyUsedCategories(invAcc.id(), m_feeId, m_interestId); 0263 scannedCategories = true; 0264 } 0265 0266 if (id.isEmpty()) { 0267 MyMoneyFile* file = MyMoneyFile::instance(); 0268 MyMoneyAccount acc = file->accountByName(defaultName); 0269 // if it does not exist, we have to create it 0270 if (acc.id().isEmpty()) { 0271 MyMoneyAccount parent = parentAccount; 0272 acc.setName(defaultName); 0273 acc.setAccountType(parent.accountType()); 0274 acc.setCurrencyId(parent.currencyId()); 0275 file->addAccount(acc, parent); 0276 } 0277 id = acc.id(); 0278 } 0279 } 0280 0281 void MyMoneyStatementReader::Private::assignUniqueBankID(MyMoneySplit& s, const MyMoneyStatement::Transaction& t_in) 0282 { 0283 QString base(t_in.m_strBankID); 0284 0285 if (base.isEmpty()) { 0286 // in case the importer did not assign a bankID, we will do it here 0287 // we use the same algorithm as used in the KBanking plugin as this 0288 // has been served well for a long time already. 0289 auto h = MyMoneyTransaction::hash(t_in.m_strPayee.trimmed()); 0290 h = MyMoneyTransaction::hash(t_in.m_strMemo, h); 0291 h = MyMoneyTransaction::hash(t_in.m_amount.toString(), h); 0292 base = QString("%1-%2-%3").arg(s.accountId(), t_in.m_datePosted.toString(Qt::ISODate)).arg(h, 7, 16, QChar('0')); 0293 } 0294 0295 // make sure that id's are unique from this point on by appending a -# 0296 // postfix if needed 0297 QString hash(base); 0298 int idx = 1; 0299 for (;;) { 0300 QMap<QString, bool>::const_iterator it; 0301 it = uniqIds.constFind(hash); 0302 if (it == uniqIds.constEnd()) { 0303 uniqIds[hash] = true; 0304 break; 0305 } 0306 hash = QString("%1-%2").arg(base).arg(idx); 0307 ++idx; 0308 } 0309 0310 s.setBankID(hash); 0311 } 0312 0313 void MyMoneyStatementReader::Private::setupPrice(MyMoneySplit &s, const MyMoneyAccount &splitAccount, const MyMoneyAccount &transactionAccount, const QDate &postDate) 0314 { 0315 if (transactionAccount.currencyId() != splitAccount.currencyId()) { 0316 // a currency conversion is needed assume that split has already a proper value 0317 MyMoneyFile* file = MyMoneyFile::instance(); 0318 MyMoneySecurity toCurrency = file->security(splitAccount.currencyId()); 0319 MyMoneySecurity fromCurrency = file->security(transactionAccount.currencyId()); 0320 // get the price for the transaction's date 0321 const MyMoneyPrice &price = file->price(fromCurrency.id(), toCurrency.id(), postDate); 0322 // if the price is valid calculate the shares 0323 if (price.isValid()) { 0324 const int fract = splitAccount.fraction(toCurrency); 0325 const MyMoneyMoney &shares = s.value() * price.rate(toCurrency.id()); 0326 s.setShares(shares.convert(fract)); 0327 qDebug("Setting second split shares to %s", qPrintable(s.shares().formatMoney(toCurrency.id(), 2))); 0328 } else { 0329 qDebug("No price entry was found to convert from '%s' to '%s' on '%s'", 0330 qPrintable(fromCurrency.tradingSymbol()), qPrintable(toCurrency.tradingSymbol()), qPrintable(postDate.toString(Qt::ISODate))); 0331 } 0332 } 0333 } 0334 0335 MyMoneyStatementReader::MyMoneyStatementReader() : 0336 d(new Private), 0337 m_userAbort(false), 0338 m_autoCreatePayee(false), 0339 m_ft(0), 0340 m_progressCallback(0) 0341 { 0342 m_askPayeeCategory = KMyMoneySettings::askForPayeeCategory(); 0343 } 0344 0345 MyMoneyStatementReader::~MyMoneyStatementReader() 0346 { 0347 delete d; 0348 } 0349 0350 bool MyMoneyStatementReader::anyTransactionAdded() const 0351 { 0352 return (d->transactionsAdded != 0) ? true : false; 0353 } 0354 0355 void MyMoneyStatementReader::setAutoCreatePayee(bool create) 0356 { 0357 m_autoCreatePayee = create; 0358 } 0359 0360 void MyMoneyStatementReader::setAskPayeeCategory(bool ask) 0361 { 0362 m_askPayeeCategory = ask; 0363 } 0364 0365 QStringList MyMoneyStatementReader::importStatement(const QString& url, bool silent, void(*callback)(int, int, const QString&)) 0366 { 0367 QStringList summary; 0368 MyMoneyStatement s; 0369 if (MyMoneyStatement::readXMLFile(s, url)) 0370 summary = MyMoneyStatementReader::importStatement(s, silent, callback); 0371 else 0372 KMessageBox::error(nullptr, i18n("Error importing %1: This file is not a valid KMM statement file.", url), i18n("Invalid Statement")); 0373 0374 return summary; 0375 } 0376 0377 QStringList MyMoneyStatementReader::importStatement(const MyMoneyStatement& s, bool silent, void(*callback)(int, int, const QString&)) 0378 { 0379 auto result = false; 0380 0381 // keep a copy of the statement 0382 if (KMyMoneySettings::logImportedStatements()) { 0383 auto logFile = QString::fromLatin1("%1/kmm-statement-%2.txt") 0384 .arg(KMyMoneySettings::logPath(), QDateTime::currentDateTimeUtc().toString(QStringLiteral("yyyy-MM-ddThh-mm-ss.zzz"))); 0385 MyMoneyStatement::writeXMLFile(s, logFile); 0386 } 0387 0388 auto reader = new MyMoneyStatementReader; 0389 reader->setAutoCreatePayee(true); 0390 if (callback) 0391 reader->setProgressCallback(callback); 0392 0393 QStringList messages; 0394 result = reader->import(s, messages); 0395 0396 auto transactionAdded = reader->anyTransactionAdded(); 0397 0398 delete reader; 0399 0400 if (callback) 0401 callback(-1, -1, QString()); 0402 0403 if (!silent && transactionAdded) { 0404 globalResultMessages()->append(messages); 0405 } 0406 0407 if (!result) 0408 messages.clear(); 0409 return messages; 0410 } 0411 0412 bool MyMoneyStatementReader::import(const MyMoneyStatement& s, QStringList& messages) 0413 { 0414 // 0415 // Select the account 0416 // 0417 0418 d->m_account = MyMoneyAccount(); 0419 d->m_brokerageAccount = MyMoneyAccount(); 0420 0421 d->m_skipCategoryMatching = s.m_skipCategoryMatching; 0422 0423 // if the statement source left some information about 0424 // the account, we use it to get the current data of it 0425 if (!s.m_accountId.isEmpty()) { 0426 try { 0427 d->m_account = MyMoneyFile::instance()->account(s.m_accountId); 0428 } catch (const MyMoneyException &) { 0429 qDebug("Received reference '%s' to unknown account in statement", qPrintable(s.m_accountId)); 0430 } 0431 } 0432 0433 if (d->m_account.id().isEmpty()) { 0434 d->m_account.setName(s.m_strAccountName); 0435 d->m_account.setNumber(s.m_strAccountNumber); 0436 0437 switch (s.m_eType) { 0438 case eMyMoney::Statement::Type::Checkings: 0439 d->m_account.setAccountType(Account::Type::Checkings); 0440 break; 0441 case eMyMoney::Statement::Type::Savings: 0442 d->m_account.setAccountType(Account::Type::Savings); 0443 break; 0444 case eMyMoney::Statement::Type::Investment: 0445 //testing support for investment statements! 0446 //m_userAbort = true; 0447 //KMessageBox::error(kmymoney, i18n("This is an investment statement. These are not supported currently."), i18n("Critical Error")); 0448 d->m_account.setAccountType(Account::Type::Investment); 0449 break; 0450 case eMyMoney::Statement::Type::CreditCard: 0451 d->m_account.setAccountType(Account::Type::CreditCard); 0452 break; 0453 default: 0454 d->m_account.setAccountType(Account::Type::Unknown); 0455 break; 0456 } 0457 0458 0459 // we ask the user only if we have some transactions to process 0460 if (!m_userAbort && s.m_listTransactions.count() > 0) 0461 m_userAbort = ! selectOrCreateAccount(Select, d->m_account); 0462 } 0463 0464 // open an engine transaction 0465 m_ft = new MyMoneyFileTransaction(); 0466 0467 // see if we need to update some values stored with the account 0468 // take into account, that an older statement might get here again 0469 0470 const auto statementEndDate = s.statementEndDate(); 0471 const auto statementBalance = s.m_closingBalance; 0472 const auto previousStatementEndDate = QDate::fromString(d->m_account.value("lastImportedTransactionDate"), Qt::ISODate); 0473 const auto previousStatementBalance = MyMoneyMoney(d->m_account.value("lastStatementBalance")); 0474 0475 // in case balance or date differs 0476 if ((previousStatementBalance != statementBalance) || (previousStatementEndDate != statementEndDate)) { 0477 // we only update if we have a valid statement date and the previous statement date is empty or older 0478 if (statementEndDate.isValid() && (!previousStatementEndDate.isValid() || (statementEndDate >= previousStatementEndDate))) { 0479 d->m_account.setValue("lastImportedTransactionDate", statementEndDate.toString(Qt::ISODate)); 0480 if (!statementBalance.isAutoCalc()) { 0481 d->m_account.setValue("lastStatementBalance", statementBalance.toString()); 0482 } else { 0483 d->m_account.deletePair("lastStatementBalance"); 0484 } 0485 0486 try { 0487 MyMoneyFile::instance()->modifyAccount(d->m_account); 0488 } catch (const MyMoneyException&) { 0489 qDebug("Updating account in MyMoneyStatementReader::startImport failed"); 0490 } 0491 } 0492 } 0493 0494 if (!d->m_account.name().isEmpty()) 0495 messages += i18n("Importing statement for account %1", d->m_account.name()); 0496 else if (s.m_listTransactions.count() == 0) 0497 messages += i18n("Importing statement without transactions"); 0498 0499 qDebug("Importing statement for '%s'", qPrintable(d->m_account.name())); 0500 0501 // 0502 // Determine oldest transaction date 0503 // (we will use that as opening date for security accounts) 0504 // 0505 d->m_oldestPostDate = QDate::currentDate(); 0506 for (const auto& transaction : s.m_listTransactions) { 0507 if (transaction.m_datePosted < d->m_oldestPostDate) { 0508 d->m_oldestPostDate = transaction.m_datePosted; 0509 } 0510 } 0511 0512 // 0513 // Process the securities 0514 // 0515 signalProgress(0, s.m_listSecurities.count(), "Importing Statement ..."); 0516 int progress = 0; 0517 QList<MyMoneyStatement::Security>::const_iterator it_s = s.m_listSecurities.begin(); 0518 while (it_s != s.m_listSecurities.end()) { 0519 processSecurityEntry(*it_s); 0520 signalProgress(++progress, 0); 0521 ++it_s; 0522 } 0523 signalProgress(-1, -1); 0524 0525 // 0526 // Process the transactions 0527 // 0528 0529 if (!m_userAbort) { 0530 try { 0531 qDebug("Processing transactions (%s)", qPrintable(d->m_account.name())); 0532 signalProgress(0, s.m_listTransactions.count(), "Importing Statement ..."); 0533 progress = 0; 0534 QList<MyMoneyStatement::Transaction>::const_iterator it_t = s.m_listTransactions.begin(); 0535 while (it_t != s.m_listTransactions.end() && !m_userAbort) { 0536 processTransactionEntry(*it_t); 0537 signalProgress(++progress, 0); 0538 ++it_t; 0539 } 0540 qDebug("Processing transactions done (%s)", qPrintable(d->m_account.name())); 0541 0542 } catch (const MyMoneyException &e) { 0543 if (QString::fromLatin1(e.what()).contains("USERABORT")) 0544 m_userAbort = true; 0545 else 0546 qDebug("Caught exception from processTransactionEntry() not caused by USERABORT: %s", e.what()); 0547 } 0548 signalProgress(-1, -1); 0549 } 0550 0551 // 0552 // process price entries 0553 // 0554 if (!m_userAbort) { 0555 try { 0556 signalProgress(0, s.m_listPrices.count(), "Importing Statement ..."); 0557 KMyMoneyUtils::processPriceList(s); 0558 } catch (const MyMoneyException &e) { 0559 if (QString::fromLatin1(e.what()).contains("USERABORT")) 0560 m_userAbort = true; 0561 else 0562 qDebug("Caught exception from processPriceEntry() not caused by USERABORT: %s", e.what()); 0563 } 0564 signalProgress(-1, -1); 0565 } 0566 0567 bool rc = false; 0568 0569 // delete all payees created in vain 0570 int payeeCount = d->payees.count(); 0571 QList<MyMoneyPayee>::const_iterator it_p; 0572 for (it_p = d->payees.constBegin(); it_p != d->payees.constEnd(); ++it_p) { 0573 try { 0574 MyMoneyFile::instance()->removePayee(*it_p); 0575 --payeeCount; 0576 } catch (const MyMoneyException &) { 0577 // if we can't delete it, it must be in use which is ok for us 0578 } 0579 } 0580 0581 if (s.m_closingBalance.isAutoCalc()) { 0582 messages += i18n(" Statement balance is not contained in statement."); 0583 } else { 0584 messages += i18n(" Statement balance on %1 is reported to be %2", s.m_dateEnd.toString(Qt::ISODate), s.m_closingBalance.formatMoney("", 2)); 0585 } 0586 messages += i18n(" Transactions"); 0587 messages += i18np(" %1 processed", " %1 processed", d->transactionsCount); 0588 messages += i18ncp("x transactions have been added", " %1 added", " %1 added", d->transactionsAdded); 0589 messages += i18np(" %1 matched", " %1 matched", d->transactionsMatched); 0590 messages += i18np(" %1 duplicate", " %1 duplicates", d->transactionsDuplicate); 0591 messages += i18n(" Payees"); 0592 messages += i18ncp("x transactions have been created", " %1 created", " %1 created", payeeCount); 0593 messages += QString(); 0594 0595 // remove the Don't ask again entries 0596 KSharedConfigPtr config = KSharedConfig::openConfig(); 0597 KConfigGroup grp = config->group(QString::fromLatin1("Notification Messages")); 0598 QStringList::ConstIterator it; 0599 0600 for (it = m_dontAskAgain.constBegin(); it != m_dontAskAgain.constEnd(); ++it) { 0601 grp.deleteEntry(*it); 0602 } 0603 config->sync(); 0604 m_dontAskAgain.clear(); 0605 0606 rc = !m_userAbort; 0607 0608 // finish the transaction 0609 if (rc) 0610 m_ft->commit(); 0611 delete m_ft; 0612 m_ft = 0; 0613 0614 qDebug("Importing statement for '%s' done", qPrintable(d->m_account.name())); 0615 0616 return rc; 0617 } 0618 0619 void MyMoneyStatementReader::processSecurityEntry(const MyMoneyStatement::Security& sec_in) 0620 { 0621 // For a security entry, we will just make sure the security exists in the 0622 // file. It will not get added to the investment account until it's called 0623 // for in a transaction. 0624 MyMoneyFile* file = MyMoneyFile::instance(); 0625 0626 // check if we already have the security 0627 // In a statement, we do not know what type of security this is, so we will 0628 // not use type as a matching factor. 0629 MyMoneySecurity security; 0630 QList<MyMoneySecurity> list = file->securityList(); 0631 QList<MyMoneySecurity>::ConstIterator it = list.constBegin(); 0632 while (it != list.constEnd() && security.id().isEmpty()) { 0633 if (matchNotEmpty(sec_in.m_strSymbol, (*it).tradingSymbol()) || matchNotEmpty(sec_in.m_strId, (*it).value("kmm-security-id")) 0634 || matchNotEmpty(sec_in.m_strName, (*it).name())) { 0635 security = *it; 0636 } 0637 ++it; 0638 } 0639 0640 // if the security was not found, we have to create it while not forgetting 0641 // to setup the type 0642 if (security.id().isEmpty()) { 0643 security.setName(sec_in.m_strName); 0644 security.setTradingSymbol(sec_in.m_strSymbol); 0645 security.setTradingCurrency(file->baseCurrency().id()); 0646 security.setValue("kmm-security-id", sec_in.m_strId); 0647 security.setValue("kmm-online-source", "Yahoo Finance"); 0648 security.setSecurityType(Security::Type::Stock); 0649 security.setSmallestAccountFraction(static_cast<int>(sec_in.m_smallestFraction.toDouble())); 0650 MyMoneyFileTransaction ft; 0651 try { 0652 file->addSecurity(security); 0653 ft.commit(); 0654 qDebug() << "Created " << security.name() << " with id " << security.id(); 0655 } catch (const MyMoneyException &e) { 0656 KMessageBox::error(0, i18n("Error creating security record: %1", QString::fromLatin1(e.what())), i18n("Error")); 0657 } 0658 } else { 0659 qDebug() << "Found " << security.name() << " with id " << security.id(); 0660 } 0661 } 0662 0663 void MyMoneyStatementReader::processTransactionEntry(const MyMoneyStatement::Transaction& statementTransactionUnderImport) 0664 { 0665 MyMoneyFile* file = MyMoneyFile::instance(); 0666 0667 MyMoneyTransaction transactionUnderImport; 0668 0669 QString dbgMsg; 0670 dbgMsg = QString("Process on: '%1', id: '%2', symbol: '%3', amount: '%4', fees: '%5'") 0671 .arg(statementTransactionUnderImport.m_datePosted.toString(Qt::ISODate)) 0672 .arg(statementTransactionUnderImport.m_strBankID) 0673 .arg(statementTransactionUnderImport.m_strSymbol) 0674 .arg(statementTransactionUnderImport.m_amount.formatMoney("", 2)) 0675 .arg(statementTransactionUnderImport.m_fees.formatMoney("", 2)); 0676 qDebug("%s", qPrintable(dbgMsg)); 0677 0678 // mark it imported for the view 0679 transactionUnderImport.setImported(); 0680 0681 // TODO (Ace) We can get the commodity from the statement!! 0682 // Although then we would need UI to verify 0683 transactionUnderImport.setCommodity(d->m_account.currencyId()); 0684 0685 transactionUnderImport.setPostDate(statementTransactionUnderImport.m_datePosted); 0686 if (statementTransactionUnderImport.m_dateProcessed.isValid()) { 0687 transactionUnderImport.setEntryDate(statementTransactionUnderImport.m_dateProcessed); 0688 } 0689 transactionUnderImport.setMemo(statementTransactionUnderImport.m_strMemo); 0690 0691 MyMoneySplit s1; 0692 MyMoneySplit s2; 0693 MyMoneySplit sFees; 0694 MyMoneySplit sBrokerage; 0695 0696 s1.setMemo(statementTransactionUnderImport.m_strMemo); 0697 s1.setValue(statementTransactionUnderImport.m_amount + statementTransactionUnderImport.m_fees); 0698 s1.setShares(s1.value()); 0699 s1.setNumber(statementTransactionUnderImport.m_strNumber); 0700 0701 // set these values if a transfer split is needed at the very end. 0702 MyMoneyMoney transfervalue; 0703 0704 // If the user has chosen to import into an investment account, determine the correct account to use 0705 MyMoneyAccount thisaccount = d->m_account; 0706 QString brokerageactid; 0707 0708 if (thisaccount.accountType() == Account::Type::Investment) { 0709 // determine the brokerage account 0710 brokerageactid = d->m_account.value("kmm-brokerage-account"); 0711 if (brokerageactid.isEmpty()) { 0712 brokerageactid = file->accountByName(statementTransactionUnderImport.m_strBrokerageAccount).id(); 0713 } 0714 if (brokerageactid.isEmpty()) { 0715 brokerageactid = file->nameToAccount(statementTransactionUnderImport.m_strBrokerageAccount); 0716 } 0717 if (brokerageactid.isEmpty()) { 0718 brokerageactid = file->nameToAccount(thisaccount.brokerageName()); 0719 } 0720 if (brokerageactid.isEmpty()) { 0721 brokerageactid = file->accountByName(thisaccount.brokerageName()).id(); 0722 } 0723 if (brokerageactid.isEmpty()) { 0724 brokerageactid = SelectBrokerageAccount(); 0725 } 0726 0727 // find the security transacted, UNLESS this transaction didn't 0728 // involve any security. 0729 if ((statementTransactionUnderImport.m_eAction != eMyMoney::Transaction::Action::None) 0730 // eaInterest transactions MAY have a security. 0731 // && (t_in.m_eAction != MyMoneyStatement::Transaction::eaInterest) 0732 && (statementTransactionUnderImport.m_eAction != eMyMoney::Transaction::Action::Fees)) { 0733 // the correct account is the stock account which matches two criteria: 0734 // (1) it is a sub-account of the selected investment account, and 0735 // (2a) the symbol of the underlying security matches the security of the 0736 // transaction, or 0737 // (2b) the name of the security matches the name of the security of the transaction. 0738 0739 // search through each subordinate account 0740 auto found = false; 0741 QString currencyid; 0742 const auto accountList = thisaccount.accountList(); 0743 for (const auto& sAccount : accountList) { 0744 currencyid = file->account(sAccount).currencyId(); 0745 auto security = file->security(currencyid); 0746 if (matchNotEmpty(statementTransactionUnderImport.m_strSymbol, security.tradingSymbol()) || 0747 matchNotEmpty(statementTransactionUnderImport.m_strSecurity, security.name())) { 0748 thisaccount = file->account(sAccount); 0749 found = true; 0750 break; 0751 } 0752 } 0753 0754 // If there was no stock account under the m_account investment account, 0755 // add one using the security. 0756 if (!found) { 0757 // The security should always be available, because the statement file 0758 // should separately list all the securities referred to in the file, 0759 // and when we found a security, we added it to the file. 0760 0761 if (statementTransactionUnderImport.m_strSecurity.isEmpty()) { 0762 KMessageBox::information(0, i18n("This imported statement contains investment transactions with no security. These transactions will be ignored."), i18n("Security not found"), QString("BlankSecurity")); 0763 return; 0764 } else { 0765 MyMoneySecurity security; 0766 QList<MyMoneySecurity> list = MyMoneyFile::instance()->securityList(); 0767 QList<MyMoneySecurity>::ConstIterator it = list.constBegin(); 0768 while (it != list.constEnd() && security.id().isEmpty()) { 0769 if (matchNotEmpty(statementTransactionUnderImport.m_strSymbol, (*it).tradingSymbol()) || 0770 matchNotEmpty(statementTransactionUnderImport.m_strSecurity, (*it).name())) { 0771 security = *it; 0772 } 0773 ++it; 0774 } 0775 if (!security.id().isEmpty()) { 0776 thisaccount = MyMoneyAccount(); 0777 thisaccount.setName(security.name()); 0778 thisaccount.setAccountType(Account::Type::Stock); 0779 thisaccount.setCurrencyId(security.id()); 0780 thisaccount.setOpeningDate(d->m_oldestPostDate); 0781 currencyid = thisaccount.currencyId(); 0782 0783 file->addAccount(thisaccount, d->m_account); 0784 qDebug() << Q_FUNC_INFO << ": created account " << thisaccount.id() << " for security " << statementTransactionUnderImport.m_strSecurity << " under account " << d->m_account.id(); 0785 } 0786 // this security does not exist in the file. 0787 else { 0788 thisaccount = MyMoneyAccount(); 0789 thisaccount.setName(statementTransactionUnderImport.m_strSecurity); 0790 0791 qDebug() << Q_FUNC_INFO << ": opening new investment wizard for security " << statementTransactionUnderImport.m_strSecurity << " under account " << d->m_account.id(); 0792 KNewInvestmentWizard::newInvestment(thisaccount, d->m_account); 0793 } 0794 } 0795 } 0796 // Don't update price if there is no price information contained in the transaction 0797 if (statementTransactionUnderImport.m_eAction != eMyMoney::Transaction::Action::CashDividend 0798 && statementTransactionUnderImport.m_eAction != eMyMoney::Transaction::Action::Shrsin 0799 && statementTransactionUnderImport.m_eAction != eMyMoney::Transaction::Action::Shrsout) { 0800 // update the price, while we're here. in the future, this should be 0801 // an option 0802 const MyMoneyPrice &price = file->price(currencyid, transactionUnderImport.commodity(), statementTransactionUnderImport.m_datePosted, true); 0803 if (!price.isValid() && ((!statementTransactionUnderImport.m_amount.isZero() && !statementTransactionUnderImport.m_shares.isZero()) || !statementTransactionUnderImport.m_price.isZero())) { 0804 MyMoneyPrice newprice; 0805 if (!statementTransactionUnderImport.m_price.isZero()) { 0806 newprice = MyMoneyPrice(currencyid, transactionUnderImport.commodity(), statementTransactionUnderImport.m_datePosted, 0807 statementTransactionUnderImport.m_price.abs(), i18n("Statement Importer")); 0808 } else { 0809 newprice = MyMoneyPrice(currencyid, transactionUnderImport.commodity(), statementTransactionUnderImport.m_datePosted, 0810 (statementTransactionUnderImport.m_amount / statementTransactionUnderImport.m_shares).abs(), i18n("Statement Importer")); 0811 } 0812 file->addPrice(newprice); 0813 } 0814 } 0815 } 0816 s1.setAccountId(thisaccount.id()); 0817 d->assignUniqueBankID(s1, statementTransactionUnderImport); 0818 0819 if (statementTransactionUnderImport.m_eAction == eMyMoney::Transaction::Action::ReinvestDividend) { 0820 s1.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::ReinvestDividend)); 0821 s1.setShares(statementTransactionUnderImport.m_shares); 0822 0823 if (!statementTransactionUnderImport.m_price.isZero()) { 0824 s1.setPrice(statementTransactionUnderImport.m_price); 0825 } else { 0826 if (statementTransactionUnderImport.m_shares.isZero()) { 0827 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")); 0828 return; 0829 } 0830 MyMoneyMoney total = -statementTransactionUnderImport.m_amount - statementTransactionUnderImport.m_fees; 0831 s1.setPrice(MyMoneyMoney((total / statementTransactionUnderImport.m_shares).convertPrecision(file->security(thisaccount.currencyId()).pricePrecision()))); 0832 } 0833 0834 s2.setMemo(statementTransactionUnderImport.m_strMemo); 0835 if (statementTransactionUnderImport.m_strInterestCategory.isEmpty()) 0836 s2.setAccountId(d->interestId(thisaccount)); 0837 else 0838 s2.setAccountId(d->interestId(statementTransactionUnderImport.m_strInterestCategory)); 0839 0840 s2.setShares(-statementTransactionUnderImport.m_amount - statementTransactionUnderImport.m_fees); 0841 s2.setValue(s2.shares()); 0842 } else if (statementTransactionUnderImport.m_eAction == eMyMoney::Transaction::Action::CashDividend) { 0843 // Cash dividends require setting 2 splits to get all of the information 0844 // in. Split #2 will be the income split, and we'll set it to the first 0845 // income account. This is a hack, but it's needed in order to get the 0846 // amount into the transaction. 0847 0848 if (statementTransactionUnderImport.m_strInterestCategory.isEmpty()) 0849 s2.setAccountId(d->interestId(thisaccount)); 0850 else {// Ensure category sub-accounts are dealt with properly 0851 s2.setAccountId(d->interestId(statementTransactionUnderImport.m_strInterestCategory)); 0852 } 0853 s2.setShares(-statementTransactionUnderImport.m_amount - statementTransactionUnderImport.m_fees); 0854 s2.setValue(-statementTransactionUnderImport.m_amount - statementTransactionUnderImport.m_fees); 0855 0856 // Split 1 will be the zero-amount investment split that serves to 0857 // mark this transaction as a cash dividend and note which stock account 0858 // it belongs to and which already contains the correct id and bankId 0859 s1.setMemo(statementTransactionUnderImport.m_strMemo); 0860 s1.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::Dividend)); 0861 s1.setShares(MyMoneyMoney()); 0862 s1.setValue(MyMoneyMoney()); 0863 0864 /* at this point any fees have been taken into account already 0865 * so don't deduct them again. 0866 * BUG 322381 0867 */ 0868 transfervalue = statementTransactionUnderImport.m_amount; 0869 } else if (statementTransactionUnderImport.m_eAction == eMyMoney::Transaction::Action::Interest) { 0870 if (statementTransactionUnderImport.m_strInterestCategory.isEmpty()) 0871 s2.setAccountId(d->interestId(thisaccount)); 0872 else {// Ensure category sub-accounts are dealt with properly 0873 if (statementTransactionUnderImport.m_amount.isPositive()) 0874 s2.setAccountId(d->interestId(statementTransactionUnderImport.m_strInterestCategory)); 0875 else 0876 s2.setAccountId(d->expenseId(statementTransactionUnderImport.m_strInterestCategory)); 0877 } 0878 s2.setShares(-statementTransactionUnderImport.m_amount - statementTransactionUnderImport.m_fees); 0879 s2.setValue(-statementTransactionUnderImport.m_amount - statementTransactionUnderImport.m_fees); 0880 0881 /// *********** Add split as per Div ********** 0882 // Split 1 will be the zero-amount investment split that serves to 0883 // mark this transaction as a cash dividend and note which stock account 0884 // it belongs to. 0885 s1.setMemo(statementTransactionUnderImport.m_strMemo); 0886 s1.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::InterestIncome)); 0887 s1.setShares(MyMoneyMoney()); 0888 s1.setValue(MyMoneyMoney()); 0889 transfervalue = statementTransactionUnderImport.m_amount; 0890 0891 } else if (statementTransactionUnderImport.m_eAction == eMyMoney::Transaction::Action::Fees) { 0892 if (statementTransactionUnderImport.m_strInterestCategory.isEmpty()) 0893 s1.setAccountId(d->feeId(thisaccount)); 0894 else// Ensure category sub-accounts are dealt with properly 0895 s1.setAccountId(d->feeId(statementTransactionUnderImport.m_strInterestCategory)); 0896 s1.setShares(statementTransactionUnderImport.m_amount); 0897 s1.setValue(statementTransactionUnderImport.m_amount); 0898 0899 transfervalue = statementTransactionUnderImport.m_amount; 0900 0901 } else if ((statementTransactionUnderImport.m_eAction == eMyMoney::Transaction::Action::Buy) || 0902 (statementTransactionUnderImport.m_eAction == eMyMoney::Transaction::Action::Sell)) { 0903 s1.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::BuyShares)); 0904 if (!statementTransactionUnderImport.m_price.isZero()) { 0905 s1.setPrice(statementTransactionUnderImport.m_price.abs()); 0906 } else if (!statementTransactionUnderImport.m_shares.isZero()) { 0907 MyMoneyMoney total = statementTransactionUnderImport.m_amount + statementTransactionUnderImport.m_fees.abs(); 0908 s1.setPrice(MyMoneyMoney((total / statementTransactionUnderImport.m_shares).abs().convertPrecision(file->security(thisaccount.currencyId()).pricePrecision()))); 0909 } 0910 if (statementTransactionUnderImport.m_eAction == eMyMoney::Transaction::Action::Buy) 0911 s1.setShares(statementTransactionUnderImport.m_shares.abs()); 0912 else 0913 s1.setShares(-statementTransactionUnderImport.m_shares.abs()); 0914 s1.setValue(-(statementTransactionUnderImport.m_amount + statementTransactionUnderImport.m_fees.abs())); 0915 transfervalue = statementTransactionUnderImport.m_amount; 0916 0917 } else if ((statementTransactionUnderImport.m_eAction == eMyMoney::Transaction::Action::Shrsin) || 0918 (statementTransactionUnderImport.m_eAction == eMyMoney::Transaction::Action::Shrsout)) { 0919 s1.setValue(MyMoneyMoney()); 0920 s1.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::AddShares)); 0921 if (statementTransactionUnderImport.m_eAction == eMyMoney::Transaction::Action::Shrsin) { 0922 s1.setShares(statementTransactionUnderImport.m_shares.abs()); 0923 } else { 0924 s1.setShares(-(statementTransactionUnderImport.m_shares.abs())); 0925 } 0926 } else if (statementTransactionUnderImport.m_eAction == eMyMoney::Transaction::Action::None) { 0927 // User is attempting to import a non-investment transaction into this 0928 // investment account. This is not supportable the way KMyMoney is 0929 // written. However, if a user has an associated brokerage account, 0930 // we can stuff the transaction there. 0931 0932 brokerageactid = d->m_account.value("kmm-brokerage-account"); 0933 if (brokerageactid.isEmpty()) { 0934 brokerageactid = file->accountByName(d->m_account.brokerageName()).id(); 0935 } 0936 if (! brokerageactid.isEmpty()) { 0937 s1.setAccountId(brokerageactid); 0938 d->assignUniqueBankID(s1, statementTransactionUnderImport); 0939 0940 // Needed to satisfy the bankid check below. 0941 thisaccount = file->account(brokerageactid); 0942 } else { 0943 // Warning!! Your transaction is being thrown away. 0944 } 0945 } 0946 if (!statementTransactionUnderImport.m_fees.isZero()) { 0947 sFees.setMemo(i18n("(Fees) %1", statementTransactionUnderImport.m_strMemo)); 0948 sFees.setValue(statementTransactionUnderImport.m_fees); 0949 sFees.setShares(statementTransactionUnderImport.m_fees); 0950 sFees.setAccountId(d->feeId(thisaccount)); 0951 } 0952 } else { 0953 // For non-investment accounts, just use the selected account 0954 // Note that it is perfectly reasonable to import an investment statement into a non-investment account 0955 // if you really want. The investment-specific information, such as number of shares and action will 0956 // be discarded in that case. 0957 s1.setAccountId(d->m_account.id()); 0958 d->assignUniqueBankID(s1, statementTransactionUnderImport); 0959 } 0960 0961 const auto importedPayeeName = statementTransactionUnderImport.m_strPayee; 0962 if (!importedPayeeName.isEmpty()) { 0963 qDebug() << QLatin1String("Start matching payee") << importedPayeeName; 0964 QString payeeid; 0965 try { 0966 QList<MyMoneyPayee> pList = file->payeeList(); 0967 QList<MyMoneyPayee>::const_iterator it_p; 0968 QMap<int, QString> matchMap; 0969 for (it_p = pList.constBegin(); it_p != pList.constEnd(); ++it_p) { 0970 bool ignoreCase; 0971 QStringList keys; 0972 QStringList::const_iterator it_s; 0973 const auto matchType = (*it_p).matchData(ignoreCase, keys); 0974 switch (matchType) { 0975 case eMyMoney::Payee::MatchType::Disabled: 0976 break; 0977 0978 case eMyMoney::Payee::MatchType::Name: 0979 case eMyMoney::Payee::MatchType::NameExact: 0980 keys << QString("%1").arg(QRegularExpression::escape((*it_p).name())); 0981 if(matchType == eMyMoney::Payee::MatchType::NameExact) { 0982 keys.clear(); 0983 keys << QString("^%1$").arg(QRegularExpression::escape((*it_p).name())); 0984 } 0985 // intentional fall through 0986 0987 case eMyMoney::Payee::MatchType::Key: 0988 for (it_s = keys.constBegin(); it_s != keys.constEnd(); ++it_s) { 0989 QRegularExpression exp(*it_s, ignoreCase ? QRegularExpression::CaseInsensitiveOption : QRegularExpression::NoPatternOption); 0990 QRegularExpressionMatch match(exp.match(importedPayeeName)); 0991 if (match.hasMatch()) { 0992 qDebug() << "Found match with" << importedPayeeName << "on" << (*it_p).name() << "for" << match.capturedLength(); 0993 matchMap[match.capturedLength()] = (*it_p).id(); 0994 } 0995 } 0996 break; 0997 } 0998 } 0999 1000 // at this point we can have several scenarios: 1001 // a) multiple matches 1002 // b) a single match 1003 // c) no match at all 1004 // 1005 // for c) we just do nothing, for b) we take the one we found 1006 // in case of a) we take the one with the largest matchedLength() 1007 // which happens to be the last one in the map 1008 if (matchMap.count() > 1) { 1009 qDebug("Multiple matches"); 1010 QMap<int, QString>::const_iterator it_m = matchMap.constEnd(); 1011 --it_m; 1012 payeeid = *it_m; 1013 } else if (matchMap.count() == 1) { 1014 qDebug("Single matches"); 1015 payeeid = *(matchMap.constBegin()); 1016 } 1017 1018 // if we did not find a matching payee, we throw an exception and try to create it 1019 if (payeeid.isEmpty()) 1020 throw MYMONEYEXCEPTION_CSTRING("payee not matched"); 1021 1022 s1.setPayeeId(payeeid); 1023 1024 // in case the payee name differs from the match and the memo has 1025 // not been changed then keep the original payee name which may contain 1026 // some details which are otherwise lost 1027 const auto payee = file->payeesModel()->itemById(payeeid); 1028 if ((s1.memo() == transactionUnderImport.memo()) && (payee.name().toLower() != importedPayeeName.toLower())) { 1029 s1.setMemo(i18nc("Prepend name of payee (%1) to original memo (%2)", "Original payee: %1\n%2", importedPayeeName, s1.memo())); 1030 } 1031 } catch (const MyMoneyException &) { 1032 MyMoneyPayee payee; 1033 int rc = KMessageBox::PrimaryAction; 1034 1035 if (m_autoCreatePayee == false) { 1036 // Ask the user if that is what he intended to do? 1037 QString msg = i18n("Do you want to add \"%1\" as payee/receiver?\n\n", importedPayeeName); 1038 msg += i18n( 1039 "Selecting \"Yes\" will create the payee, \"No\" will skip " 1040 "creation of a payee record and remove the payee information " 1041 "from this transaction. Selecting \"Cancel\" aborts the import " 1042 "operation.\n\nIf you select \"No\" here and mark the \"Do not ask " 1043 "again\" checkbox, the payee information for all following transactions " 1044 "referencing \"%1\" will be removed.", 1045 importedPayeeName); 1046 1047 QString askKey = QString("Statement-Import-Payee-") + importedPayeeName; 1048 if (!m_dontAskAgain.contains(askKey)) { 1049 m_dontAskAgain += askKey; 1050 } 1051 rc = KMessageBox::questionTwoActionsCancel(0, 1052 msg, 1053 i18n("New payee/receiver"), 1054 KMMYesNo::yes(), 1055 KMMYesNo::no(), 1056 KStandardGuiItem::cancel(), 1057 askKey); 1058 } 1059 1060 if (rc == KMessageBox::PrimaryAction) { 1061 // for now, we just add the payee to the pool and turn 1062 // on simple name matching, so that future transactions 1063 // with the same name don't get here again. 1064 // 1065 // In the future, we could open a dialog and ask for 1066 // all the other attributes of the payee, but since this 1067 // is called in the context of an automatic procedure it 1068 // might distract the user. 1069 payee.setName(importedPayeeName); 1070 payee.setMatchData(eMyMoney::Payee::MatchType::Key, true, QStringList() << QString("^%1$").arg(QRegularExpression::escape(importedPayeeName))); 1071 if (m_askPayeeCategory) { 1072 // We use a QPointer because the dialog may get deleted 1073 // during exec() if the parent of the dialog gets deleted. 1074 // In that case the guarded ptr will reset to 0. 1075 QPointer<QDialog> dialog = new QDialog; 1076 dialog->setWindowTitle(i18n("Default Category for Payee")); 1077 dialog->setModal(true); 1078 1079 QWidget *mainWidget = new QWidget; 1080 QVBoxLayout *topcontents = new QVBoxLayout(mainWidget); 1081 1082 //add in caption? and account combo here 1083 QLabel* const label1 = new QLabel(i18n("Please select a default category for payee '%1'", importedPayeeName)); 1084 topcontents->addWidget(label1); 1085 1086 auto filterProxyModel = new AccountNamesFilterProxyModel(this); 1087 filterProxyModel->setHideEquityAccounts(!KMyMoneySettings::expertMode()); 1088 filterProxyModel->setHideZeroBalancedEquityAccounts(KMyMoneySettings::hideZeroBalanceEquities()); 1089 filterProxyModel->setHideZeroBalancedAccounts(KMyMoneySettings::hideZeroBalanceAccounts()); 1090 filterProxyModel->addAccountGroup(QVector<Account::Type> {Account::Type::Asset, Account::Type::Liability, Account::Type::Equity, Account::Type::Income, Account::Type::Expense}); 1091 1092 filterProxyModel->setSourceModel(MyMoneyFile::instance()->accountsModel()); 1093 filterProxyModel->sort(AccountsModel::Column::AccountName); 1094 1095 QPointer<KMyMoneyAccountCombo> accountCombo = new KMyMoneyAccountCombo(filterProxyModel); 1096 topcontents->addWidget(accountCombo); 1097 mainWidget->setLayout(topcontents); 1098 QVBoxLayout *mainLayout = new QVBoxLayout; 1099 1100 QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Cancel|QDialogButtonBox::No|QDialogButtonBox::Yes); 1101 dialog->setLayout(mainLayout); 1102 mainLayout->addWidget(mainWidget); 1103 connect(buttonBox, &QDialogButtonBox::accepted, dialog, &QDialog::accept); 1104 connect(buttonBox, &QDialogButtonBox::rejected, dialog, &QDialog::reject); 1105 mainLayout->addWidget(buttonBox); 1106 KGuiItem::assign(buttonBox->button(QDialogButtonBox::Yes), KGuiItem(i18n("Save Category"))); 1107 KGuiItem::assign(buttonBox->button(QDialogButtonBox::No), KGuiItem(i18n("No Category"))); 1108 KGuiItem::assign(buttonBox->button(QDialogButtonBox::Cancel), KGuiItem(i18n("Abort"))); 1109 1110 int result = dialog->exec(); 1111 1112 QString accountId; 1113 if (accountCombo && !accountCombo->getSelected().isEmpty()) { 1114 accountId = accountCombo->getSelected(); 1115 } 1116 delete dialog; 1117 //if they hit yes instead of no, then grab setting of account combo 1118 if (result == QDialog::Accepted) { 1119 payee.setDefaultAccountId(accountId); 1120 } else if (result != QDialog::Rejected) { 1121 //add cancel button? and throw exception like below 1122 throw MYMONEYEXCEPTION_CSTRING("USERABORT"); 1123 } 1124 } 1125 1126 try { 1127 file->addPayee(payee); 1128 qDebug("Payee '%s' created", qPrintable(payee.name())); 1129 d->payees << payee; 1130 payeeid = payee.id(); 1131 s1.setPayeeId(payeeid); 1132 1133 } catch (const MyMoneyException &e) { 1134 KMessageBox::detailedError(nullptr, i18n("Unable to add payee/receiver"), QString::fromLatin1(e.what())); 1135 1136 } 1137 1138 } else if (rc == KMessageBox::SecondaryAction) { 1139 s1.setPayeeId(QString()); 1140 1141 } else { 1142 throw MYMONEYEXCEPTION_CSTRING("USERABORT"); 1143 } 1144 } 1145 1146 if (thisaccount.accountType() != Account::Type::Stock) { 1147 // 1148 // Fill in other side of the transaction (category/etc) based on payee 1149 // 1150 // Note, this logic is lifted from KLedgerView::slotPayeeChanged(), 1151 // however this case is more complicated, because we have an amount and 1152 // a memo. We just don't have the other side of the transaction. 1153 // 1154 // We'll search for the most recent transaction in this account with 1155 // this payee. If this reference transaction is a simple 2-split 1156 // transaction, it's simple. If it's a complex split, and the amounts 1157 // are different, we have a problem. Somehow we have to balance the 1158 // transaction. For now, we'll leave it unbalanced, and let the user 1159 // handle it. 1160 // 1161 const MyMoneyPayee payeeObj = MyMoneyFile::instance()->payee(payeeid); 1162 if (statementTransactionUnderImport.m_listSplits.isEmpty() && !payeeObj.defaultAccountId().isEmpty()) { 1163 MyMoneyAccount splitAccount = file->account(payeeObj.defaultAccountId()); 1164 MyMoneySplit s; 1165 s.setReconcileFlag(eMyMoney::Split::State::Cleared); 1166 s.clearId(); 1167 s.setBankID(QString()); 1168 s.setShares(-s1.shares()); 1169 s.setValue(-s1.value()); 1170 s.setAccountId(payeeObj.defaultAccountId()); 1171 s.setMemo(transactionUnderImport.memo()); 1172 s.setPayeeId(payeeid); 1173 d->setupPrice(s, splitAccount, d->m_account, statementTransactionUnderImport.m_datePosted); 1174 transactionUnderImport.addSplit(s); 1175 1176 } else if (statementTransactionUnderImport.m_listSplits.isEmpty() && !d->m_skipCategoryMatching) { 1177 MyMoneyTransactionFilter filter(thisaccount.id()); 1178 filter.addPayee(payeeid); 1179 QList<MyMoneyTransaction> list; 1180 file->transactionList(list, filter); 1181 if (!list.empty()) { 1182 // Default to using the most recent transaction as the reference 1183 MyMoneyTransaction t_old = list.last(); 1184 1185 // if there is more than one matching transaction, try to be a little 1186 // smart about which one we use. we scan them all and check if 1187 // we find an exact match or use the one with the closest value 1188 1189 if (list.count() > 1) { 1190 QList<MyMoneyTransaction>::ConstIterator it_trans = list.constEnd(); 1191 MyMoneyMoney minDiff; 1192 do { 1193 --it_trans; 1194 MyMoneySplit s = (*it_trans).splitByAccount(thisaccount.id()); 1195 if (s.value() == s1.value()) { 1196 // in case of an exact match, we won't get better and we can stop. 1197 // keep searching if this transaction references a closed account 1198 if (!MyMoneyFile::instance()->referencesClosedAccount(*it_trans)) { 1199 t_old = *it_trans; 1200 break; 1201 } 1202 } else { 1203 MyMoneyMoney newDiff = (s.value() - s1.value()).abs(); 1204 if (newDiff < minDiff || minDiff.isZero()) { 1205 // keep it if it matches better than the current match 1206 // but only if it does not reference a closed account 1207 if (!MyMoneyFile::instance()->referencesClosedAccount(*it_trans)) { 1208 minDiff = newDiff; 1209 t_old = *it_trans; 1210 } 1211 } 1212 } 1213 } while (it_trans != list.constBegin()); 1214 } 1215 1216 // Only copy the splits if the transaction found does not reference a closed account 1217 if (!MyMoneyFile::instance()->referencesClosedAccount(t_old)) { 1218 // special care must be taken, if the old transaction references 1219 // a category and vat account combination that is not effective 1220 // anymore due to a change in the tax category. If that is the 1221 // case, we simply don't add the old tax category to the 1222 // transaction and let MyMoneyFile::updateVAT handle the correct 1223 // addition of a tax split. Since we don't know the order of 1224 // the splits we scan over all of them and update the 1225 // transactionUnderImport at the end of the loop. 1226 MyMoneySplit categorySplit; 1227 MyMoneySplit taxSplit; 1228 QString newVatAccountId; 1229 QString oldVatAccountId; 1230 1231 for (const auto& split : t_old.splits()) { 1232 // We don't need the split that covers this account, 1233 // we just need the other ones. 1234 if (split.accountId() != thisaccount.id()) { 1235 MyMoneySplit s(split); 1236 s.setReconcileFlag(eMyMoney::Split::State::NotReconciled); 1237 s.clearId(); 1238 s.setBankID(QString()); 1239 s.removeMatch(); 1240 1241 // in case the old transaction has two splits 1242 // we simply inverse the amount of the current 1243 // transaction found in s1. In other cases (more 1244 // than two splits we copy all splits and don't 1245 // modify the splits. This may lead to unbalanced 1246 // transactions which the user has to fix manually 1247 if (t_old.splits().count() == 2) { 1248 s.setShares(-s1.shares()); 1249 s.setValue(-s1.value()); 1250 s.setMemo(s1.memo()); 1251 } 1252 MyMoneyAccount splitAccount = file->account(s.accountId()); 1253 qDebug("Adding second split to %s(%s)", 1254 qPrintable(splitAccount.name()), 1255 qPrintable(s.accountId())); 1256 d->setupPrice(s, splitAccount, d->m_account, statementTransactionUnderImport.m_datePosted); 1257 transactionUnderImport.addSplit(s); 1258 1259 // check for vat categories 1260 if (t_old.splits().count() == 3) { 1261 if (!splitAccount.value(QLatin1String("VatAccount")).isEmpty()) { 1262 newVatAccountId = splitAccount.value(QLatin1String("VatAccount")); 1263 categorySplit = s; 1264 } else { 1265 taxSplit = s; 1266 oldVatAccountId = split.accountId(); 1267 } 1268 } 1269 } 1270 } 1271 1272 // now check if we have to remove the tax split. This is 1273 // the case when newVatAccountId is set and differs from 1274 // oldVatAccountId. 1275 if (!newVatAccountId.isEmpty()) { 1276 if (newVatAccountId.compare(oldVatAccountId)) { 1277 // remove the tax split 1278 transactionUnderImport.removeSplit(taxSplit); 1279 // and update the value of the remaining split 1280 categorySplit.setShares(-s1.shares()); 1281 categorySplit.setValue(-s1.value()); 1282 transactionUnderImport.modifySplit(categorySplit); 1283 } 1284 } 1285 } 1286 } 1287 } 1288 } 1289 } 1290 1291 s1.setReconcileFlag(statementTransactionUnderImport.m_reconcile); 1292 1293 // Add the 'account' split if it's needed 1294 if (! transfervalue.isZero()) { 1295 // in case the transaction has a reference to the brokerage account, we use it 1296 // but if brokerageactid has already been set, keep that. 1297 if (!statementTransactionUnderImport.m_strBrokerageAccount.isEmpty() && brokerageactid.isEmpty()) { 1298 brokerageactid = file->nameToAccount(statementTransactionUnderImport.m_strBrokerageAccount); 1299 } 1300 if (brokerageactid.isEmpty()) { 1301 brokerageactid = file->accountByName(statementTransactionUnderImport.m_strBrokerageAccount).id(); 1302 } 1303 // There is no BrokerageAccount so have to nowhere to put this split. 1304 if (!brokerageactid.isEmpty()) { 1305 sBrokerage.setMemo(statementTransactionUnderImport.m_strMemo); 1306 sBrokerage.setValue(transfervalue); 1307 sBrokerage.setShares(transfervalue); 1308 sBrokerage.setAccountId(brokerageactid); 1309 sBrokerage.setReconcileFlag(statementTransactionUnderImport.m_reconcile); 1310 MyMoneyAccount splitAccount = file->account(sBrokerage.accountId()); 1311 d->setupPrice(sBrokerage, splitAccount, d->m_account, statementTransactionUnderImport.m_datePosted); 1312 } 1313 } 1314 1315 if (!(sBrokerage == MyMoneySplit())) 1316 transactionUnderImport.addSplit(sBrokerage); 1317 1318 if (!(sFees == MyMoneySplit())) 1319 transactionUnderImport.addSplit(sFees); 1320 1321 if (!(s2 == MyMoneySplit())) 1322 transactionUnderImport.addSplit(s2); 1323 1324 transactionUnderImport.addSplit(s1); 1325 1326 // check if we need to add/update a VAT assignment 1327 file->updateVAT(transactionUnderImport); 1328 1329 if ((statementTransactionUnderImport.m_eAction != eMyMoney::Transaction::Action::ReinvestDividend) && (statementTransactionUnderImport.m_eAction != eMyMoney::Transaction::Action::CashDividend) && (statementTransactionUnderImport.m_eAction != eMyMoney::Transaction::Action::Interest) 1330 ) { 1331 //****************************************** 1332 // process splits 1333 //****************************************** 1334 1335 QList<MyMoneyStatement::Split>::const_iterator it_s; 1336 for (it_s = statementTransactionUnderImport.m_listSplits.begin(); it_s != statementTransactionUnderImport.m_listSplits.end(); ++it_s) { 1337 MyMoneySplit s3; 1338 s3.setAccountId((*it_s).m_accountId); 1339 MyMoneyAccount acc = file->account(s3.accountId()); 1340 s3.setPayeeId(s1.payeeId()); 1341 s3.setMemo((*it_s).m_strMemo); 1342 s3.setShares((*it_s).m_amount); 1343 s3.setValue((*it_s).m_amount); 1344 s3.setReconcileFlag((*it_s).m_reconcile); 1345 d->setupPrice(s3, acc, d->m_account, statementTransactionUnderImport.m_datePosted); 1346 transactionUnderImport.addSplit(s3); 1347 } 1348 } 1349 1350 // Add the transaction 1351 try { 1352 // check for matches already stored in the engine 1353 TransactionMatchFinder::MatchResult result; 1354 d->transactionsCount++; 1355 1356 ExistingTransactionMatchFinder existingTrMatchFinder(KMyMoneySettings::matchInterval()); 1357 result = existingTrMatchFinder.findMatch(transactionUnderImport, s1); 1358 if (result != TransactionMatchFinder::MatchNotFound) { 1359 MyMoneyTransaction matchedTransaction = existingTrMatchFinder.getMatchedTransaction(); 1360 if (result == TransactionMatchFinder::MatchDuplicate 1361 || !matchedTransaction.isImported() 1362 || result == TransactionMatchFinder::MatchPrecise) { // don't match with just imported transaction 1363 MyMoneySplit matchedSplit = existingTrMatchFinder.getMatchedSplit(); 1364 handleMatchingOfExistingTransaction(matchedTransaction, matchedSplit, transactionUnderImport, s1, result); 1365 return; 1366 } 1367 } 1368 1369 addTransaction(transactionUnderImport); 1370 ScheduledTransactionMatchFinder scheduledTrMatchFinder(thisaccount, KMyMoneySettings::matchInterval()); 1371 result = scheduledTrMatchFinder.findMatch(transactionUnderImport, s1); 1372 if (result != TransactionMatchFinder::MatchNotFound) { 1373 MyMoneySplit matchedSplit = scheduledTrMatchFinder.getMatchedSplit(); 1374 MyMoneySchedule matchedSchedule = scheduledTrMatchFinder.getMatchedSchedule(); 1375 1376 handleMatchingOfScheduledTransaction(matchedSchedule, matchedSplit, transactionUnderImport, s1); 1377 return; 1378 } 1379 1380 } catch (const MyMoneyException &e) { 1381 QString message(i18n("Problem adding or matching imported transaction with id '%1': %2", statementTransactionUnderImport.m_strBankID, e.what())); 1382 qDebug("%s", qPrintable(message)); 1383 1384 int result = KMessageBox::warningContinueCancel(0, message); 1385 if (result == KMessageBox::Cancel) 1386 throw MYMONEYEXCEPTION_CSTRING("USERABORT"); 1387 } 1388 } 1389 1390 QString MyMoneyStatementReader::SelectBrokerageAccount() 1391 { 1392 if (d->m_brokerageAccount.id().isEmpty()) { 1393 d->m_brokerageAccount.setAccountType(Account::Type::Checkings); 1394 if (!m_userAbort) 1395 m_userAbort = ! selectOrCreateAccount(Select, d->m_brokerageAccount); 1396 } 1397 return d->m_brokerageAccount.id(); 1398 } 1399 1400 bool MyMoneyStatementReader::selectOrCreateAccount(const SelectCreateMode /*mode*/, MyMoneyAccount& account) 1401 { 1402 bool result = false; 1403 1404 MyMoneyFile* file = MyMoneyFile::instance(); 1405 1406 QString accountId; 1407 1408 // Try to find an existing account in the engine which matches this one. 1409 // There are two ways to be a "matching account". The account number can 1410 // match the statement account OR the "StatementKey" property can match. 1411 // Either way, we'll update the "StatementKey" property for next time. 1412 1413 QString accountNumber = account.number(); 1414 if (! accountNumber.isEmpty()) { 1415 // Get a list of all accounts 1416 QList<MyMoneyAccount> accounts; 1417 file->accountList(accounts); 1418 1419 // Iterate through them 1420 QList<MyMoneyAccount>::const_iterator it_account = accounts.constBegin(); 1421 while (it_account != accounts.constEnd()) { 1422 if ( 1423 ((*it_account).value("StatementKey") == accountNumber) || 1424 ((*it_account).number() == accountNumber) 1425 ) { 1426 MyMoneyAccount newAccount((*it_account).id(), account); 1427 account = newAccount; 1428 accountId = (*it_account).id(); 1429 break; 1430 } 1431 1432 ++it_account; 1433 } 1434 } 1435 1436 // keep a copy for later use 1437 const QString originalAccountId(accountId); 1438 1439 QString msg = i18n("<b>You have downloaded a statement for the following account:</b><br/><br/>"); 1440 msg += i18n(" - Account Name: %1", account.name()) + "<br/>"; 1441 msg += i18n(" - Account Type: %1", MyMoneyAccount::accountTypeToString(account.accountType())) + "<br/>"; 1442 msg += i18n(" - Account Number: %1", account.number()) + "<br/>"; 1443 msg += "<br/>"; 1444 1445 if (!account.name().isEmpty()) { 1446 if (!accountId.isEmpty()) 1447 msg += i18n("Do you want to import transactions to this account?"); 1448 1449 else 1450 msg += i18n("KMyMoney cannot determine which of your accounts to use. You can " 1451 "create a new account by pressing the <b>Create</b> button " 1452 "or select another one manually from the selection box below."); 1453 } else { 1454 msg += i18n("No account information has been found in the selected statement file. " 1455 "Please select an account using the selection box in the dialog or " 1456 "create a new account by pressing the <b>Create</b> button."); 1457 } 1458 1459 eDialogs::Category type; 1460 #if 0 1461 if (account.accountType() == Account::Type::Checkings) { 1462 type = eDialogs::Category::checking; 1463 } else if (account.accountType() == Account::Type::Savings) { 1464 type = eDialogs::Category::savings; 1465 } else if (account.accountType() == Account::Type::Investment) { 1466 type = eDialogs::Category::investment; 1467 } else if (account.accountType() == Account::Type::CreditCard) { 1468 type = eDialogs::Category::creditCard; 1469 } else { 1470 type = static_cast<eDialogs::Category>(eDialogs::Category::asset | eDialogs::Category::liability); 1471 } 1472 #endif 1473 // FIXME: This is a quick fix to show all accounts in the account selection combo box 1474 // of the KAccountSelectDlg. This allows to select any asset or liability account during 1475 // statement import. 1476 // The real fix would be to detect the account type here 1477 // and add an option to show all accounts in the dialog. 1478 type = static_cast<eDialogs::Category>(eDialogs::Category::asset | eDialogs::Category::liability); 1479 1480 QPointer<KAccountSelectDlg> accountSelect = new KAccountSelectDlg(type, "StatementImport", 0); 1481 connect(accountSelect, &KAccountSelectDlg::createAccount, this, &MyMoneyStatementReader::slotNewAccount); 1482 1483 accountSelect->setHeader(i18n("Import transactions")); 1484 accountSelect->setDescription(msg); 1485 accountSelect->setAccount(account, accountId); 1486 accountSelect->setMode(false); 1487 accountSelect->showAbortButton(true); 1488 accountSelect->hideQifEntry(); 1489 bool done = false; 1490 while (!done) { 1491 if (accountSelect->exec() == QDialog::Accepted && !accountSelect->selectedAccount().isEmpty()) { 1492 result = true; 1493 done = true; 1494 // update account data (current and previous) 1495 accountId = accountSelect->selectedAccount(); 1496 account = file->account(accountId); 1497 MyMoneyAccount originalAccount; 1498 if (!originalAccountId.isEmpty()) { 1499 originalAccount = file->account(originalAccountId); 1500 } 1501 1502 // if we have an account number and it differs 1503 // from the one we have as reference on file 1504 if (! accountNumber.isEmpty() && account.value("StatementKey") != accountNumber) { 1505 // update it on the account and remove it from the previous one 1506 account.setValue("StatementKey", accountNumber); 1507 originalAccount.deletePair(QLatin1String("StatementKey")); 1508 1509 MyMoneyFileTransaction ft; 1510 try { 1511 MyMoneyFile::instance()->modifyAccount(account); 1512 if (!originalAccountId.isEmpty()) { 1513 MyMoneyFile::instance()->modifyAccount(originalAccount); 1514 } 1515 ft.commit(); 1516 } catch (const MyMoneyException &) { 1517 qDebug("Updating account in MyMoneyStatementReader::selectOrCreateAccount failed"); 1518 } 1519 } 1520 } else { 1521 if (accountSelect->aborted()) 1522 //throw MYMONEYEXCEPTION_CSTRING("USERABORT"); 1523 done = true; 1524 else 1525 KMessageBox::error(0, QLatin1String("<html>") + i18n("You must select an account, create a new one, or press the <b>Abort</b> button.") + QLatin1String("</html>")); 1526 } 1527 } 1528 delete accountSelect; 1529 1530 return result; 1531 } 1532 1533 const MyMoneyAccount& MyMoneyStatementReader::account() const { 1534 return d->m_account; 1535 } 1536 1537 void MyMoneyStatementReader::setProgressCallback(void(*callback)(int, int, const QString&)) 1538 { 1539 m_progressCallback = callback; 1540 } 1541 1542 void MyMoneyStatementReader::signalProgress(int current, int total, const QString& msg) 1543 { 1544 if (m_progressCallback != 0) 1545 (*m_progressCallback)(current, total, msg); 1546 } 1547 1548 void MyMoneyStatementReader::handleMatchingOfExistingTransaction(MyMoneyTransaction matchedTransaction, 1549 MyMoneySplit matchedSplit, 1550 MyMoneyTransaction& importedTransaction, 1551 const MyMoneySplit& importedSplit, 1552 const TransactionMatchFinder::MatchResult& matchResult) 1553 { 1554 TransactionMatcher matcher; 1555 1556 switch (matchResult) { 1557 case TransactionMatchFinder::MatchNotFound: 1558 break; 1559 case TransactionMatchFinder::MatchDuplicate: 1560 d->transactionsDuplicate++; 1561 qDebug() << "Detected transaction duplicate"; 1562 break; 1563 case TransactionMatchFinder::MatchImprecise: 1564 case TransactionMatchFinder::MatchPrecise: 1565 addTransaction(importedTransaction); 1566 qDebug() << "Detected as match to transaction" << matchedTransaction.id(); 1567 matcher.match(matchedTransaction, matchedSplit, importedTransaction, importedSplit, true); 1568 d->transactionsMatched++; 1569 break; 1570 } 1571 } 1572 1573 void MyMoneyStatementReader::handleMatchingOfScheduledTransaction(MyMoneySchedule schedule, 1574 MyMoneySplit matchedSplit, 1575 const MyMoneyTransaction& importedTransaction, 1576 const MyMoneySplit& importedSplit) 1577 { 1578 const auto file = MyMoneyFile::instance(); 1579 1580 if (askUserToEnterScheduleForMatching(schedule, importedSplit, importedTransaction)) { 1581 const auto origDueDate = schedule.nextDueDate(); 1582 1583 MyMoneyTransaction t = schedule.transaction(); 1584 // in case the amounts of the scheduled transaction and the 1585 // imported transaction differ, we need to update the amount 1586 // using the transaction editor. 1587 if (matchedSplit.shares() != importedSplit.shares() && !schedule.isFixed()) { 1588 matchedSplit.setShares(importedSplit.shares()); 1589 matchedSplit.setValue(importedSplit.value()); 1590 t.modifySplit(matchedSplit); 1591 // don't forget to update the counter split 1592 if (t.splitCount() == 2) { 1593 for (const auto& split : t.splits()) { 1594 if (split.id().compare(matchedSplit.id())) { 1595 auto newSplit(split); 1596 newSplit.setShares(-matchedSplit.shares()); 1597 newSplit.setValue(-matchedSplit.value()); 1598 t.modifySplit(newSplit); 1599 break; 1600 } 1601 } 1602 } else { 1603 file->updateVAT(t); 1604 } 1605 } 1606 1607 MyMoneyFileTransaction ft; 1608 try { 1609 file->addTransaction(t); 1610 1611 // we should not need this because addTransaction() does 1612 // update the data, but we want to stay on the safe side 1613 if (!t.id().isEmpty()) { 1614 t = MyMoneyFile::instance()->transaction(t.id()); 1615 schedule.setLastPayment(t.postDate()); 1616 } 1617 1618 // in case the next due date is invalid, the schedule is finished 1619 // we mark it as such by setting the next due date to one day past the end 1620 QDate nextDueDate = schedule.nextPayment(origDueDate); 1621 if (!nextDueDate.isValid()) { 1622 schedule.setNextDueDate(schedule.endDate().addDays(1)); 1623 } else { 1624 schedule.setNextDueDate(nextDueDate); 1625 } 1626 MyMoneyFile::instance()->modifySchedule(schedule); 1627 1628 // now match the two transactions 1629 TransactionMatcher matcher; 1630 matcher.match(t, matchedSplit, importedTransaction, importedSplit); 1631 d->transactionsMatched++; 1632 1633 ft.commit(); 1634 1635 } catch (const MyMoneyException& e) { 1636 QWidget* parent = QApplication::activeWindow(); 1637 KMessageBox::detailedError(parent, i18n("Unable to enter scheduled transaction '%1'", schedule.name()), e.what()); 1638 } 1639 } 1640 } 1641 1642 void MyMoneyStatementReader::addTransaction(MyMoneyTransaction& transaction) 1643 { 1644 MyMoneyFile* file = MyMoneyFile::instance(); 1645 1646 file->addTransaction(transaction); 1647 d->transactionsAdded++; 1648 } 1649 1650 bool MyMoneyStatementReader::askUserToEnterScheduleForMatching(const MyMoneySchedule& matchedSchedule, const MyMoneySplit& importedSplit, const MyMoneyTransaction & importedTransaction) const 1651 { 1652 QString scheduleName = matchedSchedule.name(); 1653 int currencyDenom = d->m_account.fraction(MyMoneyFile::instance()->currency(d->m_account.currencyId())); 1654 QString splitValue = importedSplit.value().formatMoney(currencyDenom); 1655 QString payeeName = MyMoneyFile::instance()->payee(importedSplit.payeeId()).name(); 1656 1657 QString questionMsg = i18n("KMyMoney has found a scheduled transaction which matches an imported transaction.<br/>" 1658 "Schedule name: <b>%1</b><br/>" 1659 "Transaction: <i>%2 %3</i><br/>" 1660 "Do you want KMyMoney to enter this schedule now so that the transaction can be matched?", 1661 scheduleName, splitValue, payeeName); 1662 1663 // check that dates are within user's setting 1664 const auto gap = static_cast<int>(qAbs(matchedSchedule.transaction().postDate().toJulianDay() - importedTransaction.postDate().toJulianDay())); 1665 if (gap > KMyMoneySettings::matchInterval()) 1666 questionMsg = i18np("KMyMoney has found a scheduled transaction which matches an imported transaction.<br/>" 1667 "Schedule name: <b>%2</b><br/>" 1668 "Transaction: <i>%3 %4</i><br/>" 1669 "The transaction dates are one day apart.<br/>" 1670 "Do you want KMyMoney to enter this schedule now so that the transaction can be matched?", 1671 "KMyMoney has found a scheduled transaction which matches an imported transaction.<br/>" 1672 "Schedule name: <b>%2</b><br/>" 1673 "Transaction: <i>%3 %4</i><br/>" 1674 "The transaction dates are %1 days apart.<br/>" 1675 "Do you want KMyMoney to enter this schedule now so that the transaction can be matched?", 1676 gap,scheduleName, splitValue, payeeName); 1677 1678 const int userAnswer = KMessageBox::questionTwoActions(0, 1679 QLatin1String("<html>") + questionMsg + QLatin1String("</html>"), 1680 i18n("Schedule found"), 1681 KMMYesNo::yes(), 1682 KMMYesNo::no()); 1683 1684 return (userAnswer == KMessageBox::PrimaryAction); 1685 } 1686 1687 void MyMoneyStatementReader::slotNewAccount(const MyMoneyAccount& acc) 1688 { 1689 auto newAcc = acc; 1690 NewAccountWizard::Wizard::newAccount(newAcc); 1691 } 1692 1693 void MyMoneyStatementReader::clearResultMessages() 1694 { 1695 globalResultMessages()->clear(); 1696 } 1697 1698 QStringList MyMoneyStatementReader::resultMessages() 1699 { 1700 return *globalResultMessages(); 1701 }