From dadc34655c3ab961b0b0b94a10eaaba710f0b5e8 Mon Sep 17 00:00:00 2001 From: tpearson Date: Mon, 4 Jul 2011 22:38:03 +0000 Subject: Added kmymoney git-svn-id: svn://anonsvn.kde.org/home/kde/branches/trinity/applications/kmymoney@1239792 283d02a7-25f6-0310-bc7c-ecb5cbfe19da --- kmymoney2/dialogs/transactionmatcher.cpp | 361 +++++++++++++++++++++++++++++++ 1 file changed, 361 insertions(+) create mode 100644 kmymoney2/dialogs/transactionmatcher.cpp (limited to 'kmymoney2/dialogs/transactionmatcher.cpp') 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 + ***************************************************************************/ + +/*************************************************************************** + * * + * 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 + +// ---------------------------------------------------------------------------- +// Project Includes + +#include "transactionmatcher.h" +#include +#include +#include + +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 endSplits = endMatchTransaction.splits(); + QValueList::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& lastMatch, TransactionMatcher::autoMatchResultE& result, int variation) const +{ + Q_UNUSED(ti); + + + const QValueList& splits = tm.splits(); + QValueList::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(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(tm, *it_s); + result = matchedExact; + } else if(result != matchedExact) { + lastMatch = QPair(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 > list; + MyMoneyFile::instance()->transactionList(list, filter); + + // parse list + QValueList >::iterator it_l; + QPair 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 list; + QValueList::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; +} + -- cgit v1.2.1