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; } } } } }
void MyMoneySplitTest::testReadXML() { MyMoneySplit s; QString ref_ok = QString( "<!DOCTYPE TEST>\n" "<SPLIT-CONTAINER>\n" " <SPLIT payee=\"P000001\" reconciledate=\"\" shares=\"96379/100\" action=\"Deposit\" bankid=\"SPID\" number=\"124\" reconcileflag=\"2\" memo=\"MyMemo\" value=\"96379/1000\" account=\"A000076\">\n" " <TAG id=\"G000001\"/>\n" " </SPLIT>\n" "</SPLIT-CONTAINER>\n"); QString ref_false = QString( "<!DOCTYPE TEST>\n" "<SPLIT-CONTAINER>\n" " <SPLITS payee=\"P000001\" reconciledate=\"\" shares=\"96379/100\" action=\"Deposit\" bankid=\"SPID\" number=\"124\" reconcileflag=\"2\" memo=\"\" value=\"96379/1000\" account=\"A000076\" />\n" " <TAG id=\"G000001\"/>\n" "</SPLIT-CONTAINER>\n"); QDomDocument doc; QDomElement node; doc.setContent(ref_false); node = doc.documentElement().firstChild().toElement(); try { s = MyMoneySplit(node); QFAIL("Missing expected exception"); } catch (const MyMoneyException &) { } doc.setContent(ref_ok); node = doc.documentElement().firstChild().toElement(); try { s = MyMoneySplit(node); QVERIFY(s.id().isEmpty()); QVERIFY(s.payeeId() == "P000001"); QList<QString> tagIdList; tagIdList << "G000001"; QVERIFY(s.tagIdList() == tagIdList); QVERIFY(s.reconcileDate() == QDate()); QVERIFY(s.shares() == MyMoneyMoney(96379, 100)); QVERIFY(s.value() == MyMoneyMoney(96379, 1000)); QVERIFY(s.number() == "124"); QVERIFY(s.bankID() == "SPID"); QVERIFY(s.reconcileFlag() == MyMoneySplit::Reconciled); QVERIFY(s.action() == MyMoneySplit::ActionDeposit); QVERIFY(s.accountId() == "A000076"); QVERIFY(s.memo() == "MyMemo"); } catch (const MyMoneyException &) { } }
void CsvUtil::dissectTransaction(const MyMoneyTransaction& transaction, const MyMoneySplit& split, MyMoneySplit& assetAccountSplit, QList<MyMoneySplit>& feeSplits, QList<MyMoneySplit>& interestSplits, MyMoneySecurity& security, MyMoneySecurity& currency, MyMoneySplit::investTransactionTypeE& transactionType) { // collect the splits. split references the stock account and should already // be set up. assetAccountSplit references the corresponding asset account (maybe // empty), feeSplits is the list of all expenses and interestSplits // the list of all incomes MyMoneyFile* file = MyMoneyFile::instance(); QList<MyMoneySplit>::ConstIterator it_s; for (it_s = transaction.splits().constBegin(); it_s != transaction.splits().constEnd(); ++it_s) { MyMoneyAccount acc = file->account((*it_s).accountId()); if ((*it_s).id() == split.id()) { security = file->security(acc.currencyId()); } else if (acc.accountGroup() == MyMoneyAccount::Expense) { feeSplits.append(*it_s); } else if (acc.accountGroup() == MyMoneyAccount::Income) { interestSplits.append(*it_s); } else { assetAccountSplit = *it_s; } } // determine transaction type if (split.action() == MyMoneySplit::ActionAddShares) { transactionType = (!split.shares().isNegative()) ? MyMoneySplit::AddShares : MyMoneySplit::RemoveShares; } else if (split.action() == MyMoneySplit::ActionBuyShares) { transactionType = (!split.value().isNegative()) ? MyMoneySplit::BuyShares : MyMoneySplit::SellShares; } else if (split.action() == MyMoneySplit::ActionDividend) { transactionType = MyMoneySplit::Dividend; } else if (split.action() == MyMoneySplit::ActionReinvestDividend) { transactionType = MyMoneySplit::ReinvestDividend; } else if (split.action() == MyMoneySplit::ActionYield) { transactionType = MyMoneySplit::Yield; } else if (split.action() == MyMoneySplit::ActionSplitShares) { transactionType = MyMoneySplit::SplitShares; } else if (split.action() == MyMoneySplit::ActionInterestIncome) { transactionType = MyMoneySplit::InterestIncome; } else transactionType = MyMoneySplit::BuyShares; currency.setTradingSymbol("???"); try { currency = file->security(transaction.commodity()); } catch (const MyMoneyException &) { } }
void MyMoneySplitTest::testAssignmentConstructor() { testSetFunctions(); MyMoneySplit n; n = *m; QVERIFY(n.accountId() == "Account"); QVERIFY(n.memo() == "Memo"); QVERIFY(n.reconcileDate() == QDate(1, 2, 3)); QVERIFY(n.reconcileFlag() == MyMoneySplit::Cleared); QVERIFY(n.shares() == MyMoneyMoney(1234, 100)); QVERIFY(n.value() == MyMoneyMoney(3456, 100)); QVERIFY(n.id() == "MyID"); QVERIFY(n.payeeId() == "Payee"); QList<QString> tagIdList; tagIdList << "Tag"; QVERIFY(m->tagIdList() == tagIdList); QVERIFY(n.action() == "Action"); QVERIFY(n.transactionId() == "TestTransaction"); QVERIFY(n.value("Key") == "Value"); }
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 manual (non-matched) transaction // allow matching two manual transactions if ((!allowImportedTransactions && tm.isImported()) || sm.isMatched()) throw MYMONEYEXCEPTION(i18n("First 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 MYMONEYEXCEPTION(i18n("Splits for %1 have conflicting values (%2,%3)", m_account.name(), MyMoneyUtils::formatMoney(sm.shares(), m_account, sec), MyMoneyUtils::formatMoney(si.shares(), m_account, sec))); } // check that dates are within user's setting const int gap = abs(tm.postDate().toJulianDay() - ti.postDate().toJulianDay()); if (gap > KMyMoneyGlobalSettings::matchInterval()) { int rc = KMessageBox::questionYesNo(0, i18np("The transaction dates are one day apart. Do you want to match them anyway?", "The transaction dates are %1 days apart. Do you want to match them anyway?", gap)); if (rc == KMessageBox::No) { return; } } // 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); } } catch (const MyMoneyException &e) { QString estr = e.what(); throw MYMONEYEXCEPTION(i18n("Unable to match all splits (%1)", estr)); } } // // we now allow matching of two non-imported transactions // // 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); ti.modifySplit(si);/// MyMoneyFile::instance()->modifyTransaction(tm); // Delete the end transaction if it was stored in the engine if (!ti.id().isEmpty()) MyMoneyFile::instance()->removeTransaction(ti); }
int KSplitTransactionDlg::exec() { // for deposits, we invert the sign of all splits. // don't forget to revert when we're done ;-) if (m_isDeposit) { for (int i = 0; i < m_transaction.splits().count(); ++i) { MyMoneySplit split = m_transaction.splits()[i]; split.setValue(-split.value()); split.setShares(-split.shares()); m_transaction.modifySplit(split); } } int rc; do { transactionsTable->setFocus(); // initialize the display transactionsTable->setTransaction(m_transaction, m_split, m_account); updateSums(); rc = KSplitTransactionDlgDecl::exec(); if (rc == Accepted) { if (!diffAmount().isZero()) { KSplitCorrectionDlgDecl* corrDlg = new KSplitCorrectionDlgDecl(this); QVBoxLayout *mainLayout = new QVBoxLayout; corrDlg->setLayout(mainLayout); mainLayout->addWidget(corrDlg->findChild<QWidget*>("verticalLayout")); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok|QDialogButtonBox::Cancel); connect(buttonBox, SIGNAL(accepted()), corrDlg, SLOT(accept())); connect(buttonBox, SIGNAL(rejected()), corrDlg, SLOT(reject())); mainLayout->addWidget(buttonBox); corrDlg->buttonGroup->setId(corrDlg->continueBtn, 0); corrDlg->buttonGroup->setId(corrDlg->changeBtn, 1); corrDlg->buttonGroup->setId(corrDlg->distributeBtn, 2); corrDlg->buttonGroup->setId(corrDlg->leaveBtn, 3); corrDlg->setModal(true); MyMoneySplit split = m_transaction.splits()[0]; QString total = (-split.value()).formatMoney("", m_precision); QString sums = splitsValue().formatMoney("", m_precision); QString diff = diffAmount().formatMoney("", m_precision); // now modify the text items of the dialog to contain the correct values QString q = i18n("The total amount of this transaction is %1 while " "the sum of the splits is %2. The remaining %3 are " "unassigned.", total, sums, diff); corrDlg->explanation->setText(q); q = i18n("Change &total amount of transaction to %1.", sums); corrDlg->changeBtn->setText(q); q = i18n("&Distribute difference of %1 among all splits.", diff); corrDlg->distributeBtn->setText(q); // FIXME remove the following line once distribution among // all splits is implemented corrDlg->distributeBtn->hide(); // if we have only two splits left, we don't allow leaving sth. unassigned. if (m_transaction.splitCount() < 3) { q = i18n("&Leave total amount of transaction at %1.", total); } else { q = i18n("&Leave %1 unassigned.", diff); } corrDlg->leaveBtn->setText(q); if ((rc = corrDlg->exec()) == Accepted) { switch (corrDlg->buttonGroup->checkedId()) { case 0: // continue to edit rc = Rejected; break; case 1: // modify total split.setValue(-splitsValue()); split.setShares(-splitsValue()); m_transaction.modifySplit(split); break; case 2: // distribute difference qDebug("distribution of difference not yet supported in KSplitTransactionDlg::slotFinishClicked()"); break; case 3: // leave unassigned break; } } delete corrDlg; } } else break; } while (rc != Accepted); // for deposits, we inverted the sign of all splits. // now we revert it back, so that things are left correct if (m_isDeposit) { for (int i = 0; i < m_transaction.splits().count(); ++i) { MyMoneySplit split = m_transaction.splits()[i]; split.setValue(-split.value()); split.setShares(-split.shares()); m_transaction.modifySplit(split); } } return rc; }
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); }