File indexing completed on 2024-05-12 16:42:13

0001 /*
0002     SPDX-FileCopyrightText: 2008-2015 Thomas Baumgart <tbaumgart@kde.org>
0003     SPDX-FileCopyrightText: 2015 Christian Dávid <christian-david@web.de>
0004     SPDX-FileCopyrightText: 2017-2018 Łukasz Wojniłowicz <lukasz.wojnilowicz@gmail.com>
0005     SPDX-License-Identifier: GPL-2.0-or-later
0006 */
0007 
0008 #include "transactionmatcher.h"
0009 
0010 #include <QDate>
0011 
0012 #include <KLocalizedString>
0013 
0014 #include "mymoneyaccount.h"
0015 #include "mymoneymoney.h"
0016 #include "mymoneysecurity.h"
0017 #include "mymoneysplit.h"
0018 #include "mymoneytransaction.h"
0019 #include "mymoneyutils.h"
0020 #include "mymoneyfile.h"
0021 #include "mymoneyexception.h"
0022 #include "mymoneyenums.h"
0023 
0024 class TransactionMatcherPrivate
0025 {
0026     Q_DISABLE_COPY(TransactionMatcherPrivate)
0027 
0028 public:
0029     TransactionMatcherPrivate()
0030     {
0031     }
0032 
0033     MyMoneyAccount m_account;
0034 };
0035 
0036 TransactionMatcher::TransactionMatcher(const MyMoneyAccount& acc) :
0037     d_ptr(new TransactionMatcherPrivate)
0038 {
0039     Q_D(TransactionMatcher);
0040     d->m_account = acc;
0041 }
0042 
0043 TransactionMatcher::~TransactionMatcher()
0044 {
0045     Q_D(TransactionMatcher);
0046     delete d;
0047 }
0048 
0049 void TransactionMatcher::match(MyMoneyTransaction tm, MyMoneySplit sm, MyMoneyTransaction ti, MyMoneySplit si, bool allowImportedTransactions)
0050 {
0051     Q_D(TransactionMatcher);
0052     auto sec = MyMoneyFile::instance()->security(d->m_account.currencyId());
0053 
0054     // Now match the transactions.
0055     //
0056     // 'Matching' the transactions entails DELETING the end transaction,
0057     // and MODIFYING the start transaction as needed.
0058     //
0059     // There are a variety of ways that a transaction can conflict.
0060     // Post date, splits, amount are the ones that seem to matter.
0061     // TODO: Handle these conflicts intelligently, at least warning
0062     // the user, or better yet letting the user choose which to use.
0063     //
0064     // For now, we will just use the transaction details from the start
0065     // transaction.  The only thing we'll take from the end transaction
0066     // are the bank ID's.
0067     //
0068     // What we have to do here is iterate over the splits in the end
0069     // transaction, and find the corresponding split in the start
0070     // transaction.  If there is a bankID in the end split but not the
0071     // start split, add it to the start split.  If there is a bankID
0072     // in BOTH, then this transaction cannot be merged (both transactions
0073     // were imported!!)  If the corresponding start split cannot  be
0074     // found and the end split has a bankID, we should probably just fail.
0075     // Although we could ADD it to the transaction.
0076 
0077     // ipwizard: Don't know if iterating over the transactions is a good idea.
0078     // In case of a split transaction recorded with KMyMoney and the transaction
0079     // data being imported consisting only of a single category assignment, this
0080     // does not make much sense. The same applies for investment transactions
0081     // stored in KMyMoney against imported transactions. I think a better solution
0082     // is to just base the match on the splits referencing the same (currently
0083     // selected) account.
0084 
0085     // verify, that tm is a manual (non-matched) transaction
0086     // allow matching two manual transactions
0087 
0088     if ((!allowImportedTransactions && tm.isImported()) || sm.isMatched())
0089         throw MYMONEYEXCEPTION_CSTRING("First transaction does not match requirement for matching");
0090 
0091     // verify that the amounts are the same, otherwise we should not be matching!
0092     if (sm.shares() != si.shares()) {
0093         throw MYMONEYEXCEPTION(QString::fromLatin1("Splits for %1 have conflicting values (%2,%3)").arg(d->m_account.name(), MyMoneyUtils::formatMoney(sm.shares(), d->m_account, sec), MyMoneyUtils::formatMoney(si.shares(), d->m_account, sec)));
0094     }
0095 
0096     // ipwizard: I took over the code to keep the bank id found in the endMatchTransaction
0097     // This might not work for QIF imports as they don't setup this information. It sure
0098     // makes sense for OFX and HBCI.
0099     const QString& bankID = si.bankID();
0100     if (!bankID.isEmpty()) {
0101         try {
0102             if (sm.bankID().isEmpty()) {
0103                 sm.setBankID(bankID);
0104                 tm.modifySplit(sm);
0105             }
0106         } catch (const MyMoneyException &e) {
0107             throw MYMONEYEXCEPTION(QString::fromLatin1("Unable to match all splits (%1)").arg(e.what()));
0108         }
0109     }
0110     //
0111     //  we now allow matching of two non-imported transactions
0112     //
0113 
0114     // mark the split as cleared if it does not have a reconciliation information yet
0115     if (sm.reconcileFlag() == eMyMoney::Split::State::NotReconciled) {
0116         sm.setReconcileFlag(eMyMoney::Split::State::Cleared);
0117     }
0118 
0119     // if we don't have a payee assigned to the manually entered transaction
0120     // we use the one we found in the imported transaction
0121     if (sm.payeeId().isEmpty() && !si.payeeId().isEmpty()) {
0122         sm.setValue("kmm-orig-payee", sm.payeeId());
0123         sm.setPayeeId(si.payeeId());
0124     }
0125 
0126     // We use the imported postdate and keep the previous one for unmatch
0127     if (tm.postDate() != ti.postDate()) {
0128         sm.setValue("kmm-orig-postdate", tm.postDate().toString(Qt::ISODate));
0129         tm.setPostDate(ti.postDate());
0130     }
0131 
0132     // combine the two memos into one
0133     QString memo = sm.memo();
0134     if (!si.memo().isEmpty() && si.memo() != memo) {
0135         sm.setValue("kmm-orig-memo", memo);
0136         if (!memo.isEmpty())
0137             memo += '\n';
0138         memo += si.memo();
0139     }
0140     sm.setMemo(memo);
0141 
0142     // remember the split we matched
0143     sm.setValue("kmm-match-split", si.id());
0144 
0145     sm.addMatch(ti);
0146     tm.modifySplit(sm);
0147 
0148     ti.modifySplit(si);///
0149     MyMoneyFile::instance()->modifyTransaction(tm);
0150     // Delete the end transaction if it was stored in the engine
0151     if (!ti.id().isEmpty())
0152         MyMoneyFile::instance()->removeTransaction(ti);
0153 }
0154 
0155 void TransactionMatcher::unmatch(const MyMoneyTransaction& _t, const MyMoneySplit& _s)
0156 {
0157     if (_s.isMatched()) {
0158         MyMoneyTransaction tm(_t);
0159         MyMoneySplit sm(_s);
0160         MyMoneyTransaction ti(sm.matchedTransaction());
0161         MyMoneySplit si;
0162         // if we don't have a split, then we don't have a memo
0163         try {
0164             si = ti.splitById(sm.value("kmm-match-split"));
0165         } catch (const MyMoneyException &) {
0166         }
0167         sm.removeMatch();
0168 
0169         // restore the postdate if modified
0170         if (!sm.value("kmm-orig-postdate").isEmpty()) {
0171             tm.setPostDate(QDate::fromString(sm.value("kmm-orig-postdate"), Qt::ISODate));
0172         }
0173 
0174         // restore payee if modified
0175         if (!sm.value("kmm-orig-payee").isEmpty()) {
0176             sm.setPayeeId(sm.value("kmm-orig-payee"));
0177         }
0178 
0179         // restore memo if modified
0180         if (!sm.value("kmm-orig-memo").isEmpty()) {
0181             sm.setMemo(sm.value("kmm-orig-memo"));
0182         }
0183 
0184         sm.deletePair("kmm-orig-postdate");
0185         sm.deletePair("kmm-orig-payee");
0186         sm.deletePair("kmm-orig-memo");
0187         sm.deletePair("kmm-match-split");
0188         tm.modifySplit(sm);
0189 
0190         MyMoneyFile::instance()->modifyTransaction(tm);
0191         MyMoneyFile::instance()->addTransaction(ti);
0192     }
0193 }
0194 
0195 void TransactionMatcher::accept(const MyMoneyTransaction& _t, const MyMoneySplit& _s)
0196 {
0197     if (_s.isMatched()) {
0198         MyMoneyTransaction tm(_t);
0199         MyMoneySplit sm(_s);
0200         sm.removeMatch();
0201         sm.deletePair("kmm-orig-postdate");
0202         sm.deletePair("kmm-orig-payee");
0203         sm.deletePair("kmm-orig-memo");
0204         sm.deletePair("kmm-match-split");
0205         tm.modifySplit(sm);
0206 
0207         MyMoneyFile::instance()->modifyTransaction(tm);
0208     }
0209 }