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 }