File indexing completed on 2024-05-19 05:06:53

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 // ----------------------------------------------------------------------------
0011 // QT Includes
0012 
0013 #include <QDate>
0014 
0015 // ----------------------------------------------------------------------------
0016 // KDE Includes
0017 
0018 #include <KLocalizedString>
0019 
0020 // ----------------------------------------------------------------------------
0021 // Project Includes
0022 
0023 #include "mymoneyaccount.h"
0024 #include "mymoneyenums.h"
0025 #include "mymoneyexception.h"
0026 #include "mymoneyfile.h"
0027 #include "mymoneymoney.h"
0028 #include "mymoneysecurity.h"
0029 #include "mymoneysplit.h"
0030 #include "mymoneytransaction.h"
0031 #include "mymoneyutils.h"
0032 
0033 class TransactionMatcherPrivate
0034 {
0035     Q_DISABLE_COPY(TransactionMatcherPrivate)
0036 
0037 public:
0038     TransactionMatcherPrivate()
0039     {
0040     }
0041 
0042     void clearMatchFlags(MyMoneySplit& s)
0043     {
0044         s.deletePair("kmm-orig-postdate");
0045         s.deletePair("kmm-orig-payee");
0046         s.deletePair("kmm-orig-memo");
0047         s.deletePair("kmm-orig-onesplit");
0048         s.deletePair("kmm-orig-not-reconciled");
0049         s.deletePair("kmm-match-split");
0050         s.deletePair("kmm-matched-tx");
0051     }
0052 };
0053 
0054 TransactionMatcher::TransactionMatcher()
0055     : d_ptr(new TransactionMatcherPrivate)
0056 {
0057 }
0058 
0059 TransactionMatcher::~TransactionMatcher()
0060 {
0061     Q_D(TransactionMatcher);
0062     delete d;
0063 }
0064 
0065 void TransactionMatcher::match(MyMoneyTransaction tm, MyMoneySplit sm, MyMoneyTransaction ti, MyMoneySplit si, bool allowImportedTransactions)
0066 {
0067     if (sm.accountId() != si.accountId()) {
0068         throw MYMONEYEXCEPTION_CSTRING("Both splits must reference the same account for matching");
0069     }
0070 
0071     const auto file = MyMoneyFile::instance();
0072     auto account = file->account(sm.accountId());
0073     auto sec = file->security(account.currencyId());
0074 
0075     // Now match the transactions.
0076     //
0077     // 'Matching' the transactions entails DELETING the imported transaction (ti),
0078     // and MODIFYING the manual entered transaction (tm) as needed.
0079     //
0080     // There are a variety of ways that a transaction can conflict.
0081     // Post date, splits, amount are the ones that seem to matter.
0082     // TODO: Handle these conflicts intelligently, at least warning
0083     // the user, or better yet letting the user choose which to use.
0084     //
0085     // If the imported split (si) contains a bankID but none is in sm
0086     // use the one found in si and add it to sm.
0087     // If there is a bankID in BOTH, then this transaction
0088     // cannot be merged (both transactions were imported!!)
0089     //
0090     // If the postdate of si differs from the one in sm, we use the one in si
0091     // if ti has the imported flag set.
0092 
0093     // verify, that tm is a manual (non-matched) transaction
0094     // allow matching two manual transactions
0095 
0096     if ((!allowImportedTransactions && tm.isImported()) || sm.isMatched() || si.isMatched())
0097         throw MYMONEYEXCEPTION_CSTRING("Transactions does not fullfil requirements for matching");
0098 
0099     // verify that the amounts are the same, otherwise we should not be matching!
0100     if (sm.shares() != si.shares()) {
0101         throw MYMONEYEXCEPTION(
0102             QString::fromLatin1("Splits for %1 have conflicting values (%2,%3)")
0103                 .arg(account.name(), MyMoneyUtils::formatMoney(sm.shares(), account, sec), MyMoneyUtils::formatMoney(si.shares(), account, sec)));
0104     }
0105 
0106     // ipwizard: I took over the code to keep the bank id found in the endMatchTransaction
0107     // This might not work for QIF imports as they don't setup this information. It sure
0108     // makes sense for OFX and HBCI.
0109     const QString& bankID = si.bankID();
0110     if (!bankID.isEmpty()) {
0111         try {
0112             if (sm.bankID().isEmpty()) {
0113                 sm.setBankID(bankID);
0114                 tm.modifySplit(sm);
0115             }
0116         } catch (const MyMoneyException &e) {
0117             throw MYMONEYEXCEPTION(QString::fromLatin1("Unable to match all splits (%1)").arg(e.what()));
0118         }
0119     }
0120     //
0121     //  we now allow matching of two non-imported transactions
0122     //
0123 
0124     // mark the split as cleared if it does not have a reconciliation information yet
0125     if (sm.reconcileFlag() == eMyMoney::Split::State::NotReconciled) {
0126         sm.MyMoneyKeyValueContainer::setValue("kmm-orig-not-reconciled", true, false);
0127         sm.setReconcileFlag(eMyMoney::Split::State::Cleared);
0128     }
0129 
0130     // if we don't have a payee assigned to the manually entered transaction
0131     // we use the one we found in the imported transaction
0132     if (sm.payeeId().isEmpty() && !si.payeeId().isEmpty()) {
0133         // we use "\xff\xff\xff\xff" as the default so that
0134         // the entry will be written, since payeeId is empty
0135         sm.setValue("kmm-orig-payee", sm.payeeId(), QStringLiteral("\xff\xff\xff\xff"));
0136         sm.setPayeeId(si.payeeId());
0137     }
0138 
0139     // We use the imported postdate and keep the previous one for unmatch
0140     if ((tm.postDate() != ti.postDate()) && ti.isImported()) {
0141         sm.setValue("kmm-orig-postdate", tm.postDate().toString(Qt::ISODate));
0142         tm.setPostDate(ti.postDate());
0143     }
0144 
0145     // combine the two memos into one if they differ
0146     QString memo = sm.memo();
0147     if (si.memo() != memo) {
0148         // we use "\xff\xff\xff\xff" as the default so that
0149         // the entry will be written, even if memo is empty
0150         sm.setValue("kmm-orig-memo", memo, QStringLiteral("\xff\xff\xff\xff"));
0151         if (!memo.isEmpty() && !si.memo().isEmpty())
0152             memo += '\n';
0153         memo += si.memo();
0154     }
0155     sm.setMemo(memo);
0156 
0157     // if tm has only one split and ti has more than one, then simply
0158     // copy all splits from ti to tm (skipping si) and remember that
0159     // tm had no splits.
0160     if (tm.splitCount() == 1 && ti.splitCount() > 1) {
0161         sm.MyMoneyKeyValueContainer::setValue("kmm-orig-onesplit", true, false);
0162         const auto splits = ti.splits();
0163         for (auto split : splits) {
0164             if (split.id() == si.id())
0165                 continue;
0166             split.clearId();
0167             tm.addSplit(split);
0168         }
0169     }
0170 
0171     // remember the split we matched
0172     sm.setValue("kmm-match-split", si.id());
0173 
0174     sm.addMatch(ti);
0175     tm.modifySplit(sm);
0176 
0177     if (file->isInvestmentTransaction(tm)) {
0178         // find the security split and set the memo to the same as sm.memo
0179         const auto splits = tm.splits();
0180         for (auto split : splits) {
0181             const auto acc = file->account(split.accountId());
0182             const auto security = file->security(acc.currencyId());
0183             if (!security.isCurrency()) {
0184                 split.setMemo(memo);
0185                 tm.modifySplit(split);
0186                 break;
0187             }
0188         }
0189     }
0190 
0191     file->modifyTransaction(tm);
0192 
0193     // Delete the imported transaction if it was previously stored in the engine
0194     if (!ti.id().isEmpty())
0195         file->removeTransaction(ti);
0196 }
0197 
0198 void TransactionMatcher::unmatch(const MyMoneyTransaction& _t, const MyMoneySplit& _s)
0199 {
0200     Q_D(TransactionMatcher);
0201     if (_s.isMatched()) {
0202         MyMoneyTransaction tm(_t);
0203         MyMoneySplit sm(_s);
0204         MyMoneyTransaction ti(sm.matchedTransaction());
0205         MyMoneySplit si;
0206         // if we don't have a split, then we don't have a memo
0207         try {
0208             si = ti.splitById(sm.value("kmm-match-split"));
0209         } catch (const MyMoneyException &) {
0210         }
0211         sm.removeMatch();
0212 
0213         // restore the postdate if modified
0214         if (!sm.value("kmm-orig-postdate").isEmpty()) {
0215             tm.setPostDate(QDate::fromString(sm.value("kmm-orig-postdate"), Qt::ISODate));
0216         }
0217 
0218         // restore payee if modified
0219         if (sm.pairs().contains("kmm-orig-payee")) {
0220             sm.setPayeeId(sm.value("kmm-orig-payee"));
0221         }
0222 
0223         // restore memo if modified
0224         if (sm.pairs().contains("kmm-orig-memo")) {
0225             sm.setMemo(sm.value("kmm-orig-memo"));
0226         }
0227 
0228         // restore reconcileFlag if modified
0229         if (sm.MyMoneyKeyValueContainer::value("kmm-orig-not-reconciled", false)) {
0230             sm.setReconcileFlag(eMyMoney::Split::State::NotReconciled);
0231             sm.setReconcileDate(QDate());
0232         }
0233 
0234         // remove splits if they were added during matching
0235         if (sm.MyMoneyKeyValueContainer::value("kmm-orig-onesplit", false)) {
0236             const auto splits = tm.splits();
0237             for (const auto& split : splits) {
0238                 if (split.id() == sm.id())
0239                     continue;
0240                 tm.removeSplit(split);
0241             }
0242         }
0243 
0244         d->clearMatchFlags(sm);
0245         sm.setBankID(QString());
0246         tm.modifySplit(sm);
0247 
0248         MyMoneyFile::instance()->modifyTransaction(tm);
0249         MyMoneyFile::instance()->addTransaction(ti);
0250     }
0251 }
0252 
0253 void TransactionMatcher::accept(const MyMoneyTransaction& _t, const MyMoneySplit& _s)
0254 {
0255     Q_D(TransactionMatcher);
0256     if (_s.isMatched()) {
0257         MyMoneyTransaction tm(_t);
0258         MyMoneySplit sm(_s);
0259         sm.removeMatch();
0260         d->clearMatchFlags(sm);
0261         tm.modifySplit(sm);
0262 
0263         MyMoneyFile::instance()->modifyTransaction(tm);
0264     }
0265 }