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