void MyMoneySplitTest::testWriteXML() { MyMoneySplit s; s.setPayeeId("P000001"); QList<QString> tagIdList; tagIdList << "G000001"; s.setTagIdList(tagIdList); s.setShares(MyMoneyMoney(96379, 100)); s.setValue(MyMoneyMoney(96379, 1000)); s.setAccountId("A000076"); s.setNumber("124"); s.setBankID("SPID"); s.setAction(MyMoneySplit::ActionDeposit); s.setReconcileFlag(MyMoneySplit::Reconciled); QDomDocument doc("TEST"); QDomElement el = doc.createElement("SPLIT-CONTAINER"); doc.appendChild(el); s.writeXML(doc, el); QCOMPARE(doc.doctype().name(), QLatin1String("TEST")); QDomElement splitContainer = doc.documentElement(); QCOMPARE(splitContainer.tagName(), QLatin1String("SPLIT-CONTAINER")); QCOMPARE(splitContainer.childNodes().size(), 1); QVERIFY(splitContainer.childNodes().at(0).isElement()); QDomElement split = splitContainer.childNodes().at(0).toElement(); QCOMPARE(split.tagName(), QLatin1String("SPLIT")); QCOMPARE(split.attribute("payee"), QLatin1String("P000001")); QCOMPARE(split.attribute("reconcileflag"), QLatin1String("2")); QCOMPARE(split.attribute("shares"), QLatin1String("96379/100")); QCOMPARE(split.attribute("reconciledate"), QString()); QCOMPARE(split.attribute("action"), QLatin1String("Deposit")); QCOMPARE(split.attribute("bankid"), QLatin1String("SPID")); QCOMPARE(split.attribute("account"), QLatin1String("A000076")); QCOMPARE(split.attribute("number"), QLatin1String("124")); QCOMPARE(split.attribute("value"), QLatin1String("96379/1000")); QCOMPARE(split.attribute("memo"), QString()); QCOMPARE(split.attribute("id"), QString()); QCOMPARE(split.childNodes().size(), 1); QVERIFY(split.childNodes().at(0).isElement()); QDomElement tag = split.childNodes().at(0).toElement(); QCOMPARE(tag.tagName(), QLatin1String("TAG")); QCOMPARE(tag.attribute("id"), QLatin1String("G000001")); QCOMPARE(tag.childNodes().size(), 0); QString ref = QString( "<!DOCTYPE TEST>\n" "<SPLIT-CONTAINER>\n" " <SPLIT payee=\"P000001\" reconcileflag=\"2\" shares=\"96379/100\" reconciledate=\"\" action=\"Deposit\" bankid=\"SPID\" account=\"A000076\" number=\"124\" value=\"96379/1000\" memo=\"\" id=\"\">\n" " <TAG id=\"G000001\"/>\n" " </SPLIT>\n" "</SPLIT-CONTAINER>\n"); }
TransactionHelper::TransactionHelper( const QDate& _date, const QString& _action, MyMoneyMoney _value, const QString& _accountid, const QString& _categoryid, const QString& _currencyid, const QString& _payee ) { // _currencyid is the currency of the transaction, and of the _value // both the account and category can have their own currency (athough the category having // a foreign currency is not yet supported by the program, the reports will still allow it, // so it must be tested.) MyMoneyFile* file = MyMoneyFile::instance(); bool haspayee = ! _payee.isEmpty(); MyMoneyPayee payeeTest = file->payeeByName(_payee); MyMoneyFileTransaction ft; setPostDate(_date); QString currencyid = _currencyid; if ( currencyid.isEmpty() ) currencyid=MyMoneyFile::instance()->baseCurrency().id(); setCommodity(currencyid); MyMoneyMoney price; MyMoneySplit splitLeft; if ( haspayee ) splitLeft.setPayeeId(payeeTest.id()); splitLeft.setAction(_action); splitLeft.setValue(-_value); price = MyMoneyFile::instance()->price(currencyid, file->account(_accountid).currencyId(),_date).rate(file->account(_accountid).currencyId()); splitLeft.setShares(-_value * price); splitLeft.setAccountId(_accountid); addSplit(splitLeft); MyMoneySplit splitRight; if ( haspayee ) splitRight.setPayeeId(payeeTest.id()); splitRight.setAction(_action); splitRight.setValue(_value); price = MyMoneyFile::instance()->price(currencyid, file->account(_categoryid).currencyId(),_date).rate(file->account(_categoryid).currencyId()); splitRight.setShares(_value * price ); splitRight.setAccountId(_categoryid); addSplit(splitRight); MyMoneyFile::instance()->addTransaction(*this); ft.commit(); }
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); }
void MyMoneySplitTest::testReplaceId() { MyMoneySplit s; bool changed; s.setPayeeId("P000001"); s.setAccountId("A000076"); changed = s.replaceId("X0001", "Y00001"); QVERIFY(changed == false); QVERIFY(s.payeeId() == "P000001"); QVERIFY(s.accountId() == "A000076"); changed = s.replaceId("P000002", "P000001"); QVERIFY(changed == true); QVERIFY(s.payeeId() == "P000002"); QVERIFY(s.accountId() == "A000076"); changed = s.replaceId("A000079", "A000076"); QVERIFY(changed == true); QVERIFY(s.payeeId() == "P000002"); QVERIFY(s.accountId() == "A000079"); QString ref_ok = QString( "<!DOCTYPE TEST>\n" "<SPLIT-CONTAINER>\n" " <SPLIT payee=\"P000001\" reconciledate=\"\" shares=\"-125000/100\" action=\"Transfer\" bankid=\"A000076-2010-03-05-b6850c0-1\" number=\"\" reconcileflag=\"1\" memo=\"UMBUCHUNG\" value=\"-125000/100\" id=\"S0001\" account=\"A000076\" >\n" " <KEYVALUEPAIRS>\n" " <PAIR key=\"kmm-match-split\" value=\"S0002\" />\n" " <PAIR key=\"kmm-matched-tx\" value=\"&lt;!DOCTYPE MATCH>\n" " &lt;CONTAINER>\n" " &lt;TRANSACTION postdate="2010-03-05" memo="UMBUCHUNG" id="" commodity="EUR" entrydate="2010-03-08" >\n" " &lt;SPLITS>\n" " &lt;SPLIT payee="P000010" reconciledate="" shares="125000/100" action="Transfer" bankid="" number="" reconcileflag="0" memo="UMBUCHUNG" value="125000/100" id="S0001" account="A000087" />\n" " &lt;SPLIT payee="P000011" reconciledate="" shares="-125000/100" action="" bankid="A000076-2010-03-05-b6850c0-1" number="" reconcileflag="0" memo="UMBUCHUNG" value="-125000/100" id="S0002" account="A000076" />\n" " &lt;/SPLITS>\n" " &lt;KEYVALUEPAIRS>\n" " &lt;PAIR key="Imported" value="true" />\n" " &lt;/KEYVALUEPAIRS>\n" " &lt;/TRANSACTION>\n" " &lt;/CONTAINER>\n" "\" />\n" " <PAIR key=\"kmm-orig-memo\" value=\"\" />\n" " </KEYVALUEPAIRS>\n" " </SPLIT>\n" "</SPLIT-CONTAINER>\n" ); QDomDocument doc; QDomElement node; doc.setContent(ref_ok); node = doc.documentElement().firstChild().toElement(); try { s = MyMoneySplit(node); QVERIFY(s.payeeId() == "P000001"); QVERIFY(s.replaceId("P2", "P1") == false); QVERIFY(s.matchedTransaction().splits()[0].payeeId() == "P000010"); QVERIFY(s.matchedTransaction().splits()[1].payeeId() == "P000011"); QVERIFY(s.replaceId("P0010", "P000010") == true); QVERIFY(s.matchedTransaction().splits()[0].payeeId() == "P0010"); QVERIFY(s.matchedTransaction().splits()[1].payeeId() == "P000011"); QVERIFY(s.replaceId("P0011", "P000011") == true); QVERIFY(s.matchedTransaction().splits()[0].payeeId() == "P0010"); QVERIFY(s.matchedTransaction().splits()[1].payeeId() == "P0011"); } catch (const MyMoneyException &) { QFAIL("Unexpected exception"); } }
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); }