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 }