summaryrefslogtreecommitdiffstats
path: root/kmymoney2/dialogs/transactionmatcher.cpp
blob: 5b8d4b58338de431b4f6fe7be9c68730e3953c45 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
/***************************************************************************
                             transactionmatcher.cpp
                             ----------
    begin                : Tue Jul 08 2008
    copyright            : (C) 2008 by Thomas Baumgart
    email                : Thomas Baumgart <ipwizard@users.sourceforge.net>
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 ***************************************************************************/

// ----------------------------------------------------------------------------
// QT Includes

// ----------------------------------------------------------------------------
// KDE Includes

#include <klocale.h>

// ----------------------------------------------------------------------------
// Project Includes

#include "transactionmatcher.h"
#include <kmymoney/mymoneyfile.h>
#include <kmymoney/mymoneyscheduled.h>
#include <kmymoney/kmymoneyutils.h>

TransactionMatcher::TransactionMatcher(const MyMoneyAccount& acc) :
  m_account(acc),
  m_days(3)
{
}

void TransactionMatcher::match(MyMoneyTransaction tm, MyMoneySplit sm, MyMoneyTransaction ti, MyMoneySplit si, bool allowImportedTransactions)
{
  const MyMoneySecurity& sec = MyMoneyFile::instance()->security(m_account.currencyId());

  // Now match the transactions.
  //
  // 'Matching' the transactions entails DELETING the end transaction,
  // and MODIFYING the start transaction as needed.
  //
  // There are a variety of ways that a transaction can conflict.
  // Post date, splits, amount are the ones that seem to matter.
  // TODO: Handle these conflicts intelligently, at least warning
  // the user, or better yet letting the user choose which to use.
  //
  // For now, we will just use the transaction details from the start
  // transaction.  The only thing we'll take from the end transaction
  // are the bank ID's.
  //
  // What we have to do here is iterate over the splits in the end
  // transaction, and find the corresponding split in the start
  // transaction.  If there is a bankID in the end split but not the
  // start split, add it to the start split.  If there is a bankID
  // in BOTH, then this transaction cannot be merged (both transactions
  // were imported!!)  If the corresponding start split cannot  be
  // found and the end split has a bankID, we should probably just fail.
  // Although we could ADD it to the transaction.

  // ipwizard: Don't know if iterating over the transactions is a good idea.
  // In case of a split transaction recorded with KMyMoney and the transaction
  // data being imported consisting only of a single category assignment, this
  // does not make much sense. The same applies for investment transactions
  // stored in KMyMoney against imported transactions. I think a better solution
  // is to just base the match on the splits referencing the same (currently
  // selected) account.

  // verify, that tm is a manually (non-matched) transaction and ti an imported one
  if(sm.isMatched() || (!allowImportedTransactions && tm.isImported()))
    throw new MYMONEYEXCEPTION(i18n("First transaction does not match requirement for matching"));
  if(!ti.isImported())
    throw new MYMONEYEXCEPTION(i18n("Second transaction does not match requirement for matching"));

  // verify that the amounts are the same, otherwise we should not be matching!
  if(sm.shares() != si.shares()) {
    throw new MYMONEYEXCEPTION(i18n("Splits for %1 have conflicting values (%2,%3)").arg(m_account.name()).arg(sm.shares().formatMoney(m_account, sec), si.shares().formatMoney(m_account, sec)));
  }

  // ipwizard: I took over the code to keep the bank id found in the endMatchTransaction
  // This might not work for QIF imports as they don't setup this information. It sure
  // makes sense for OFX and HBCI.
  const QString& bankID = si.bankID();
  if (!bankID.isEmpty()) {
    try {
      if (sm.bankID().isEmpty() ) {
        sm.setBankID( bankID );
        tm.modifySplit(sm);
      } else if(sm.bankID() != bankID) {
        throw new MYMONEYEXCEPTION(i18n("Both of these transactions have been imported into %1.  Therefore they cannot be matched.  Matching works with one imported transaction and one non-imported transaction.").arg(m_account.name()));
      }
    } catch(MyMoneyException *e) {
      QString estr = e->what();
      delete e;
      throw new MYMONEYEXCEPTION(i18n("Unable to match all splits (%1)").arg(estr));
    }
  }

#if 0 // Ace's original code
  // TODO (Ace) Add in another error to catch the case where a user
  // tries to match two hand-entered transactions.
  QValueList<MyMoneySplit> endSplits = endMatchTransaction.splits();
  QValueList<MyMoneySplit>::const_iterator it_split = endSplits.begin();
  while (it_split != endSplits.end())
  {
    // find the corresponding split in the start transaction
    MyMoneySplit startSplit;
    QString accountid = (*it_split).accountId();
    try
    {
      startSplit = startMatchTransaction.splitByAccount( accountid );
    }
      // only exception is thrown if we cannot find a split like this
    catch(MyMoneyException *e)
    {
      delete e;
      startSplit = (*it_split);
      startSplit.clearId();
      startMatchTransaction.addSplit(startSplit);
    }

    // verify that the amounts are the same, otherwise we should not be
    // matching!
    if ( (*it_split).value() != startSplit.value() )
    {
      QString accountname = MyMoneyFile::instance()->account(accountid).name();
      throw new MYMONEYEXCEPTION(i18n("Splits for %1 have conflicting values (%2,%3)").arg(accountname).arg((*it_split).value().formatMoney(),startSplit.value().formatMoney()));
    }

    QString bankID = (*it_split).bankID();
    if ( ! bankID.isEmpty() )
    {
      try
      {
        if ( startSplit.bankID().isEmpty() )
        {
          startSplit.setBankID( bankID );
          startMatchTransaction.modifySplit(startSplit);
        }
        else
        {
          QString accountname = MyMoneyFile::instance()->account(accountid).name();
          throw new MYMONEYEXCEPTION(i18n("Both of these transactions have been imported into %1.  Therefore they cannot be matched.  Matching works with one imported transaction and one non-imported transaction.").arg(accountname));
        }
      }
      catch(MyMoneyException *e)
      {
        QString estr = e->what();
        delete e;
        throw new MYMONEYEXCEPTION(i18n("Unable to match all splits (%1)").arg(estr));
      }
    }
    ++it_split;
  }
#endif

  // mark the split as cleared if it does not have a reconciliation information yet
  if(sm.reconcileFlag() == MyMoneySplit::NotReconciled) {
    sm.setReconcileFlag(MyMoneySplit::Cleared);
  }

  // if we don't have a payee assigned to the manually entered transaction
  // we use the one we found in the imported transaction
  if(sm.payeeId().isEmpty() && !si.payeeId().isEmpty()) {
    sm.setValue("kmm-orig-payee", sm.payeeId());
    sm.setPayeeId(si.payeeId());
  }

  // We use the imported postdate and keep the previous one for unmatch
  if(tm.postDate() != ti.postDate()) {
    sm.setValue("kmm-orig-postdate", tm.postDate().toString(Qt::ISODate));
    tm.setPostDate(ti.postDate());
  }

  // combine the two memos into one
  QString memo = sm.memo();
  if(!si.memo().isEmpty() && si.memo() != memo) {
    sm.setValue("kmm-orig-memo", memo);
    if(!memo.isEmpty())
      memo += "\n";
    memo += si.memo();
  }
  sm.setMemo(memo);

  // remember the split we matched
  sm.setValue("kmm-match-split", si.id());

  sm.addMatch(ti);
  tm.modifySplit(sm);

  MyMoneyFile::instance()->modifyTransaction(tm);
  // Delete the end transaction if it was stored in the engine
  if(!ti.id().isEmpty())
    MyMoneyFile::instance()->removeTransaction(ti);
}

void TransactionMatcher::unmatch(const MyMoneyTransaction& _t, const MyMoneySplit& _s)
{
  if(_s.isMatched()) {
    MyMoneyTransaction tm(_t);
    MyMoneySplit sm(_s);
    MyMoneyTransaction ti(sm.matchedTransaction());
    MyMoneySplit si;
    // if we don't have a split, then we don't have a memo
    try {
      si = ti.splitById(sm.value("kmm-match-split"));
    } catch(MyMoneyException* e) {
      delete e;
    }
    sm.removeMatch();

    // restore the postdate if modified
    if(!sm.value("kmm-orig-postdate").isEmpty()) {
      tm.setPostDate(QDate::fromString(sm.value("kmm-orig-postdate"), Qt::ISODate));
    }

    // restore payee if modified
    if(!sm.value("kmm-orig-payee").isEmpty()) {
      sm.setPayeeId(sm.value("kmm-orig-payee"));
    }

    // restore memo if modified
    if(!sm.value("kmm-orig-memo").isEmpty()) {
      sm.setMemo(sm.value("kmm-orig-memo"));
    }

    sm.deletePair("kmm-orig-postdate");
    sm.deletePair("kmm-orig-payee");
    sm.deletePair("kmm-orig-memo");
    sm.deletePair("kmm-match-split");
    tm.modifySplit(sm);

    MyMoneyFile::instance()->modifyTransaction(tm);
    MyMoneyFile::instance()->addTransaction(ti);
  }
}

void TransactionMatcher::accept(const MyMoneyTransaction& _t, const MyMoneySplit& _s)
{
  if(_s.isMatched()) {
    MyMoneyTransaction tm(_t);
    MyMoneySplit sm(_s);
    sm.removeMatch();
    sm.deletePair("kmm-orig-postdate");
    sm.deletePair("kmm-orig-payee");
    sm.deletePair("kmm-orig-memo");
    sm.deletePair("kmm-match-split");
    tm.modifySplit(sm);

    MyMoneyFile::instance()->modifyTransaction(tm);
  }
}

void TransactionMatcher::checkTransaction(const MyMoneyTransaction& tm, const MyMoneyTransaction& ti, const MyMoneySplit& si, QPair<MyMoneyTransaction, MyMoneySplit>& lastMatch, TransactionMatcher::autoMatchResultE& result, int variation) const
{
  Q_UNUSED(ti);


  const QValueList<MyMoneySplit>& splits = tm.splits();
  QValueList<MyMoneySplit>::const_iterator it_s;
  for(it_s = splits.begin(); it_s != splits.end(); ++it_s) {
    MyMoneyMoney upper((*it_s).shares());
    MyMoneyMoney lower(upper);
    if((variation > 0) && (variation < 100)) {
      lower = lower - (lower.abs() * MyMoneyMoney(variation, 100));
      upper = upper + (upper.abs() * MyMoneyMoney(variation, 100));
    }
    // we only check for duplicates / matches if the sign
    // of the amount for this split is identical
    if((si.shares() >= lower) && (si.shares() <= upper)) {
      // check for duplicate (we can only do that, if we have a bankID)
      if(!si.bankID().isEmpty()) {
        if((*it_s).bankID() == si.bankID()) {
          lastMatch = QPair<MyMoneyTransaction, MyMoneySplit>(tm, *it_s);
          result = matchedDuplicate;
          break;
        }
        // in case the stored split already has a bankid
        // assigned, it must be a different one and therefore
        // will certainly not match
        if(!(*it_s).bankID().isEmpty())
          continue;
      }
      // check if this is the one that matches
      if((*it_s).accountId() == si.accountId()
      && (si.shares() >= lower) && (si.shares() <= upper)
      && !(*it_s).isMatched()) {
        if(tm.postDate() == ti.postDate()) {
          lastMatch = QPair<MyMoneyTransaction, MyMoneySplit>(tm, *it_s);
          result = matchedExact;
        } else if(result != matchedExact) {
          lastMatch = QPair<MyMoneyTransaction, MyMoneySplit>(tm, *it_s);
          result = matched;
        }
      }
    }
  }
}

MyMoneyObject const * TransactionMatcher::findMatch(const MyMoneyTransaction& ti, const MyMoneySplit& si, MyMoneySplit& sm, autoMatchResultE& result)
{
  result = notMatched;
  sm = MyMoneySplit();

  MyMoneyTransactionFilter filter(si.accountId());
  filter.setReportAllSplits(false);
  filter.setDateFilter(ti.postDate().addDays(-m_days), ti.postDate().addDays(m_days));
  filter.setAmountFilter(si.shares(), si.shares());

  QValueList<QPair<MyMoneyTransaction, MyMoneySplit> > list;
  MyMoneyFile::instance()->transactionList(list, filter);

  // parse list
  QValueList<QPair<MyMoneyTransaction, MyMoneySplit> >::iterator it_l;
  QPair<MyMoneyTransaction, MyMoneySplit> lastMatch;

  for(it_l = list.begin(); (result != matchedDuplicate) && (it_l != list.end()); ++it_l) {
    // just skip myself
    if((*it_l).first.id() == ti.id()) {
      continue;
    }

    checkTransaction((*it_l).first, ti, si, lastMatch, result);
  }

  MyMoneyObject* rc = 0;
  if(result != notMatched) {
    sm = lastMatch.second;
    rc = new MyMoneyTransaction(lastMatch.first);

  } else {
    // if we did not find anything, we need to scan for scheduled transactions
    QValueList<MyMoneySchedule> list;
    QValueList<MyMoneySchedule>::iterator it_sch;
    // find all schedules that have a reference to the current account
    list = MyMoneyFile::instance()->scheduleList(m_account.id());
    for(it_sch = list.begin(); (result != matched && result != matchedExact) && (it_sch != list.end()); ++it_sch) {
      // get the next due date adjusted by the weekend switch
      QDate nextDueDate = (*it_sch).nextDueDate();
      if((*it_sch).isOverdue() ||
         (nextDueDate >= ti.postDate().addDays(-m_days)
         && nextDueDate <= ti.postDate().addDays(m_days))) {
        MyMoneyTransaction st = KMyMoneyUtils::scheduledTransaction(*it_sch);
        checkTransaction(st, ti, si, lastMatch, result, (*it_sch).variation());
        if(result == matched || result == matchedExact) {
          sm = lastMatch.second;
          rc = new MyMoneySchedule(*it_sch);
        }
      }
    }
  }

  return rc;
}