File indexing completed on 2024-05-12 05:06:04

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