1 /*
2 * Copyright 2008-2015 Thomas Baumgart <tbaumgart@kde.org>
3 * Copyright 2015 Christian Dávid <christian-david@web.de>
4 * Copyright 2017-2018 Łukasz Wojniłowicz <lukasz.wojnilowicz@gmail.com>
5 *
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU General Public License as
8 * published by the Free Software Foundation; either version 2 of
9 * the License, or (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program. If not, see <http://www.gnu.org/licenses/>.
18 */
19
20 #include "transactionmatcher.h"
21
22 #include <QDate>
23
24 #include <KLocalizedString>
25
26 #include "mymoneyaccount.h"
27 #include "mymoneymoney.h"
28 #include "mymoneysecurity.h"
29 #include "mymoneysplit.h"
30 #include "mymoneytransaction.h"
31 #include "mymoneyutils.h"
32 #include "mymoneyfile.h"
33 #include "mymoneyexception.h"
34 #include "mymoneyenums.h"
35
36 class TransactionMatcherPrivate
37 {
38 Q_DISABLE_COPY(TransactionMatcherPrivate)
39
40 public:
TransactionMatcherPrivate()41 TransactionMatcherPrivate()
42 {
43 }
44
45 MyMoneyAccount m_account;
46 };
47
TransactionMatcher(const MyMoneyAccount & acc)48 TransactionMatcher::TransactionMatcher(const MyMoneyAccount& acc) :
49 d_ptr(new TransactionMatcherPrivate)
50 {
51 Q_D(TransactionMatcher);
52 d->m_account = acc;
53 }
54
~TransactionMatcher()55 TransactionMatcher::~TransactionMatcher()
56 {
57 Q_D(TransactionMatcher);
58 delete d;
59 }
60
match(MyMoneyTransaction tm,MyMoneySplit sm,MyMoneyTransaction ti,MyMoneySplit si,bool allowImportedTransactions)61 void TransactionMatcher::match(MyMoneyTransaction tm, MyMoneySplit sm, MyMoneyTransaction ti, MyMoneySplit si, bool allowImportedTransactions)
62 {
63 Q_D(TransactionMatcher);
64 auto sec = MyMoneyFile::instance()->security(d->m_account.currencyId());
65
66 // Now match the transactions.
67 //
68 // 'Matching' the transactions entails DELETING the end transaction,
69 // and MODIFYING the start transaction as needed.
70 //
71 // There are a variety of ways that a transaction can conflict.
72 // Post date, splits, amount are the ones that seem to matter.
73 // TODO: Handle these conflicts intelligently, at least warning
74 // the user, or better yet letting the user choose which to use.
75 //
76 // For now, we will just use the transaction details from the start
77 // transaction. The only thing we'll take from the end transaction
78 // are the bank ID's.
79 //
80 // What we have to do here is iterate over the splits in the end
81 // transaction, and find the corresponding split in the start
82 // transaction. If there is a bankID in the end split but not the
83 // start split, add it to the start split. If there is a bankID
84 // in BOTH, then this transaction cannot be merged (both transactions
85 // were imported!!) If the corresponding start split cannot be
86 // found and the end split has a bankID, we should probably just fail.
87 // Although we could ADD it to the transaction.
88
89 // ipwizard: Don't know if iterating over the transactions is a good idea.
90 // In case of a split transaction recorded with KMyMoney and the transaction
91 // data being imported consisting only of a single category assignment, this
92 // does not make much sense. The same applies for investment transactions
93 // stored in KMyMoney against imported transactions. I think a better solution
94 // is to just base the match on the splits referencing the same (currently
95 // selected) account.
96
97 // verify, that tm is a manual (non-matched) transaction
98 // allow matching two manual transactions
99
100 if ((!allowImportedTransactions && tm.isImported()) || sm.isMatched())
101 throw MYMONEYEXCEPTION_CSTRING("First transaction does not match requirement for matching");
102
103 // verify that the amounts are the same, otherwise we should not be matching!
104 if (sm.shares() != si.shares()) {
105 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)));
106 }
107
108 // ipwizard: I took over the code to keep the bank id found in the endMatchTransaction
109 // This might not work for QIF imports as they don't setup this information. It sure
110 // makes sense for OFX and HBCI.
111 const QString& bankID = si.bankID();
112 if (!bankID.isEmpty()) {
113 try {
114 if (sm.bankID().isEmpty()) {
115 sm.setBankID(bankID);
116 tm.modifySplit(sm);
117 }
118 } catch (const MyMoneyException &e) {
119 throw MYMONEYEXCEPTION(QString::fromLatin1("Unable to match all splits (%1)").arg(e.what()));
120 }
121 }
122 //
123 // we now allow matching of two non-imported transactions
124 //
125
126 // mark the split as cleared if it does not have a reconciliation information yet
127 if (sm.reconcileFlag() == eMyMoney::Split::State::NotReconciled) {
128 sm.setReconcileFlag(eMyMoney::Split::State::Cleared);
129 }
130
131 // if we don't have a payee assigned to the manually entered transaction
132 // we use the one we found in the imported transaction
133 if (sm.payeeId().isEmpty() && !si.payeeId().isEmpty()) {
134 sm.setValue("kmm-orig-payee", sm.payeeId());
135 sm.setPayeeId(si.payeeId());
136 }
137
138 // We use the imported postdate and keep the previous one for unmatch
139 if (tm.postDate() != ti.postDate()) {
140 sm.setValue("kmm-orig-postdate", tm.postDate().toString(Qt::ISODate));
141 tm.setPostDate(ti.postDate());
142 }
143
144 // combine the two memos into one
145 QString memo = sm.memo();
146 if (!si.memo().isEmpty() && si.memo() != memo) {
147 sm.setValue("kmm-orig-memo", memo);
148 if (!memo.isEmpty())
149 memo += '\n';
150 memo += si.memo();
151 }
152 sm.setMemo(memo);
153
154 // remember the split we matched
155 sm.setValue("kmm-match-split", si.id());
156
157 sm.addMatch(ti);
158 tm.modifySplit(sm);
159
160 ti.modifySplit(si);///
161 MyMoneyFile::instance()->modifyTransaction(tm);
162 // Delete the end transaction if it was stored in the engine
163 if (!ti.id().isEmpty())
164 MyMoneyFile::instance()->removeTransaction(ti);
165 }
166
unmatch(const MyMoneyTransaction & _t,const MyMoneySplit & _s)167 void TransactionMatcher::unmatch(const MyMoneyTransaction& _t, const MyMoneySplit& _s)
168 {
169 if (_s.isMatched()) {
170 MyMoneyTransaction tm(_t);
171 MyMoneySplit sm(_s);
172 MyMoneyTransaction ti(sm.matchedTransaction());
173 MyMoneySplit si;
174 // if we don't have a split, then we don't have a memo
175 try {
176 si = ti.splitById(sm.value("kmm-match-split"));
177 } catch (const MyMoneyException &) {
178 }
179 sm.removeMatch();
180
181 // restore the postdate if modified
182 if (!sm.value("kmm-orig-postdate").isEmpty()) {
183 tm.setPostDate(QDate::fromString(sm.value("kmm-orig-postdate"), Qt::ISODate));
184 }
185
186 // restore payee if modified
187 if (!sm.value("kmm-orig-payee").isEmpty()) {
188 sm.setPayeeId(sm.value("kmm-orig-payee"));
189 }
190
191 // restore memo if modified
192 if (!sm.value("kmm-orig-memo").isEmpty()) {
193 sm.setMemo(sm.value("kmm-orig-memo"));
194 }
195
196 sm.deletePair("kmm-orig-postdate");
197 sm.deletePair("kmm-orig-payee");
198 sm.deletePair("kmm-orig-memo");
199 sm.deletePair("kmm-match-split");
200 tm.modifySplit(sm);
201
202 MyMoneyFile::instance()->modifyTransaction(tm);
203 MyMoneyFile::instance()->addTransaction(ti);
204 }
205 }
206
accept(const MyMoneyTransaction & _t,const MyMoneySplit & _s)207 void TransactionMatcher::accept(const MyMoneyTransaction& _t, const MyMoneySplit& _s)
208 {
209 if (_s.isMatched()) {
210 MyMoneyTransaction tm(_t);
211 MyMoneySplit sm(_s);
212 sm.removeMatch();
213 sm.deletePair("kmm-orig-postdate");
214 sm.deletePair("kmm-orig-payee");
215 sm.deletePair("kmm-orig-memo");
216 sm.deletePair("kmm-match-split");
217 tm.modifySplit(sm);
218
219 MyMoneyFile::instance()->modifyTransaction(tm);
220 }
221 }
222