summaryrefslogtreecommitdiffstats
path: root/kmymoney2/dialogs/transactionmatcher.cpp
diff options
context:
space:
mode:
authortpearson <tpearson@283d02a7-25f6-0310-bc7c-ecb5cbfe19da>2011-07-04 22:38:03 +0000
committertpearson <tpearson@283d02a7-25f6-0310-bc7c-ecb5cbfe19da>2011-07-04 22:38:03 +0000
commitdadc34655c3ab961b0b0b94a10eaaba710f0b5e8 (patch)
tree99e72842fe687baea16376a147619b6048d7e441 /kmymoney2/dialogs/transactionmatcher.cpp
downloadkmymoney-dadc34655c3ab961b0b0b94a10eaaba710f0b5e8.tar.gz
kmymoney-dadc34655c3ab961b0b0b94a10eaaba710f0b5e8.zip
Added kmymoney
git-svn-id: svn://anonsvn.kde.org/home/kde/branches/trinity/applications/kmymoney@1239792 283d02a7-25f6-0310-bc7c-ecb5cbfe19da
Diffstat (limited to 'kmymoney2/dialogs/transactionmatcher.cpp')
-rw-r--r--kmymoney2/dialogs/transactionmatcher.cpp361
1 files changed, 361 insertions, 0 deletions
diff --git a/kmymoney2/dialogs/transactionmatcher.cpp b/kmymoney2/dialogs/transactionmatcher.cpp
new file mode 100644
index 0000000..5b8d4b5
--- /dev/null
+++ b/kmymoney2/dialogs/transactionmatcher.cpp
@@ -0,0 +1,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;
+}
+