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 }