File indexing completed on 2024-05-12 16:41:58

0001 /*
0002     KMyMoney transaction importing module - base class for searching for a matching transaction
0003 
0004     SPDX-FileCopyrightText: 2012 Lukasz Maszczynski <lukasz@maszczynski.net>
0005     SPDX-License-Identifier: GPL-2.0-or-later
0006 */
0007 
0008 #include "transactionmatchfinder.h"
0009 
0010 #include <QDebug>
0011 #include <QDate>
0012 
0013 #include <KLocalizedString>
0014 
0015 #include "mymoneymoney.h"
0016 #include "mymoneyaccount.h"
0017 #include "mymoneyfile.h"
0018 #include "mymoneypayee.h"
0019 #include "mymoneyexception.h"
0020 
0021 TransactionMatchFinder::TransactionMatchFinder(int _matchWindow) :
0022     m_matchWindow(_matchWindow),
0023     matchResult(MatchNotFound)
0024 {
0025 }
0026 
0027 TransactionMatchFinder::~TransactionMatchFinder()
0028 {
0029 }
0030 
0031 TransactionMatchFinder::MatchResult TransactionMatchFinder::findMatch(const MyMoneyTransaction& transactionToMatch, const MyMoneySplit& splitToMatch)
0032 {
0033     importedTransaction = transactionToMatch;
0034     m_importedSplit = splitToMatch;
0035     matchResult = MatchNotFound;
0036     matchedTransaction.reset();
0037     matchedSplit.reset();
0038     matchedSchedule.reset();
0039 
0040     QString date = importedTransaction.postDate().toString(Qt::ISODate);
0041     QString payeeName = MyMoneyFile::instance()->payee(m_importedSplit.payeeId()).name();
0042     QString amount = m_importedSplit.shares().formatMoney("", 2);
0043     QString account = MyMoneyFile::instance()->account(m_importedSplit.accountId()).name();
0044     qDebug() << "Looking for a match with transaction: " << date << "," << payeeName << "," << amount
0045              << "(referenced account: " << account << ")";
0046 
0047     createListOfMatchCandidates();
0048     findMatchInMatchCandidatesList();
0049     return matchResult;
0050 }
0051 
0052 MyMoneySplit TransactionMatchFinder::getMatchedSplit() const
0053 {
0054     if (matchedSplit.isNull()) {
0055         throw MYMONEYEXCEPTION(QString::fromLatin1("Internal error - no matching splits"));
0056     }
0057 
0058     return *matchedSplit;
0059 }
0060 
0061 MyMoneyTransaction TransactionMatchFinder::getMatchedTransaction() const
0062 {
0063     if (matchedTransaction.isNull()) {
0064         throw MYMONEYEXCEPTION(QString::fromLatin1("Internal error - no matching transactions"));
0065     }
0066 
0067     return *matchedTransaction;
0068 }
0069 
0070 MyMoneySchedule TransactionMatchFinder::getMatchedSchedule() const
0071 {
0072     if (matchedSchedule.isNull()) {
0073         throw MYMONEYEXCEPTION(QString::fromLatin1("Internal error - no matching schedules"));
0074     }
0075 
0076     return *matchedSchedule;
0077 }
0078 
0079 bool TransactionMatchFinder::splitsAreDuplicates(const MyMoneySplit& split1, const MyMoneySplit& split2, int amountVariation) const
0080 {
0081     return (splitsAmountsMatch(split1, split2, amountVariation) && splitsBankIdsDuplicated(split1, split2));
0082 }
0083 
0084 bool TransactionMatchFinder::splitsMatch(const MyMoneySplit& importedSplit, const MyMoneySplit& existingSplit, int amountVariation) const
0085 {
0086     return (splitsAccountsMatch(importedSplit, existingSplit) //
0087             && splitsBankIdsMatch(importedSplit, existingSplit) //
0088             && splitsAmountsMatch(importedSplit, existingSplit, amountVariation)  //
0089             && splitsPayeesMatchOrEmpty(importedSplit, existingSplit) //
0090             && !existingSplit.isMatched());
0091 }
0092 
0093 bool TransactionMatchFinder::splitsAccountsMatch(const MyMoneySplit & split1, const MyMoneySplit & split2) const
0094 {
0095     return split1.accountId() == split2.accountId();
0096 }
0097 
0098 bool TransactionMatchFinder::splitsAmountsMatch(const MyMoneySplit& split1, const MyMoneySplit& split2, int amountVariation) const
0099 {
0100     MyMoneyMoney upper(split1.shares());
0101     MyMoneyMoney lower(upper);
0102     if ((amountVariation > 0) && (amountVariation < 100)) {
0103         lower = lower - (lower.abs() * MyMoneyMoney(amountVariation, 100));
0104         upper = upper + (upper.abs() * MyMoneyMoney(amountVariation, 100));
0105     }
0106 
0107     return (split2.shares() >= lower) && (split2.shares() <= upper);
0108 }
0109 
0110 bool TransactionMatchFinder::splitsBankIdsDuplicated(const MyMoneySplit& split1, const MyMoneySplit& split2) const
0111 {
0112     return (!split1.bankID().isEmpty()) && (split1.bankID() == split2.bankID());
0113 }
0114 
0115 bool TransactionMatchFinder::splitsBankIdsMatch(const MyMoneySplit& importedSplit, const MyMoneySplit& existingSplit) const
0116 {
0117     return (existingSplit.bankID().isEmpty() || existingSplit.bankID() == importedSplit.bankID());
0118 }
0119 
0120 bool TransactionMatchFinder::splitsPayeesMatchOrEmpty(const MyMoneySplit& split1, const MyMoneySplit& split2) const
0121 {
0122     bool payeesMatch = (split1.payeeId() == split2.payeeId());
0123     bool atLeastOnePayeeIsNotSet = (split1.payeeId().isEmpty() || split2.payeeId().isEmpty());
0124     return payeesMatch || atLeastOnePayeeIsNotSet;
0125 }
0126 
0127 void TransactionMatchFinder::findMatchingSplit(const MyMoneyTransaction& transaction, int amountVariation)
0128 {
0129     foreach (const MyMoneySplit & split, transaction.splits()) {
0130         if (splitsAreDuplicates(m_importedSplit, split, amountVariation)) {
0131             matchedTransaction.reset(new MyMoneyTransaction(transaction));
0132             matchedSplit.reset(new MyMoneySplit(split));
0133             matchResult = MatchDuplicate;
0134             break;
0135         }
0136 
0137         if (splitsMatch(m_importedSplit, split, amountVariation)) {
0138             matchedTransaction.reset(new MyMoneyTransaction(transaction));
0139             matchedSplit.reset(new MyMoneySplit(split));
0140 
0141             bool datesMatchPrecisely = importedTransaction.postDate() == transaction.postDate();
0142             if (datesMatchPrecisely) {
0143                 matchResult = MatchPrecise;
0144             } else {
0145                 matchResult = MatchImprecise;
0146             }
0147             break;
0148         }
0149     }
0150 }