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