diff --git a/kmymoney/reports/CMakeLists.txt b/kmymoney/reports/CMakeLists.txt index 44369798a..39a17fcc7 100644 --- a/kmymoney/reports/CMakeLists.txt +++ b/kmymoney/reports/CMakeLists.txt @@ -1,48 +1,49 @@ set (libreports_a_SOURCES + cashflowlist.cpp kreportchartview.cpp reportaccount.cpp listtable.cpp objectinfotable.cpp pivotgrid.cpp pivottable.cpp querytable.cpp reporttable.cpp kbalanceaxis.cpp ) kde4_add_library(reports STATIC ${libreports_a_SOURCES}) target_link_libraries(reports kmm_kdchart) add_dependencies(reports kmm_settings) ############## tests #################### if( KDE4_BUILD_TESTS ) # Pivot Grid set( pivotgridtest_SRCS pivotgridtest.cpp reportstestcommon.cpp ) kde4_add_unit_test( pivotgridtest TESTNAME kmymoney-pivotgridtest ${pivotgridtest_SRCS} ) add_dependencies( pivotgridtest reports kmymoney_base kmymoney_common kmm_storage ) target_link_libraries( pivotgridtest reports kmymoney_base kmymoney_common kmm_storage ${KDE4_KDEUI_LIBS} ${KDE4_KDECORE_LIBS} ${QT_QTGUI_LIBRARY} ${QT_QTXML_LIBRARY} ${QT_QTCORE_LIBRARY} ${QT_QTTEST_LIBRARY} ) # Pivot Table set( pivottabletest_SRCS pivottabletest.cpp reportstestcommon.cpp ../mymoney/mymoneytestutils.cpp ) kde4_add_unit_test( pivottabletest TESTNAME kmymoney-pivottabletest ${pivottabletest_SRCS} ) add_dependencies( pivottabletest dialogs widgets reports settings kmymoney_base kmymoney_common kmm_kdchart kmm_storage kmm_settings ) target_link_libraries( pivottabletest reports settings kmymoney_base kmymoney_common dialogs widgets kmm_kdchart kmm_storage kmm_settings ${KDE4_KDEUI_LIBS} ${KDE4_KDECORE_LIBS} ${QT_QTGUI_LIBRARY} ${QT_QTXML_LIBRARY} ${QT_QTCORE_LIBRARY} ${QT_QTTEST_LIBRARY} ) # Query Table set( querytabletest_SRCS querytabletest.cpp reportstestcommon.cpp ../mymoney/mymoneytestutils.cpp ) kde4_add_unit_test( querytabletest TESTNAME kmymoney-querytabletest ${querytabletest_SRCS} ) add_dependencies( querytabletest dialogs widgets reports settings kmymoney_base kmymoney_common kmm_storage kmm_settings ) target_link_libraries( querytabletest reports kmymoney_base kmymoney_common dialogs widgets kmm_storage kmm_settings ${KDE4_KDEUI_LIBS} ${KDE4_KDECORE_LIBS} ${QT_QTGUI_LIBRARY} ${QT_QTXML_LIBRARY} ${QT_QTCORE_LIBRARY} ${QT_QTTEST_LIBRARY} ) kde4_add_executable( reportcharttest reportcharttest.cpp ) target_link_libraries( reportcharttest reports kmm_kdchart ${QT_QTGUI_LIBRARY} ${QT_QTCORE_LIBRARY} ) endif( KDE4_BUILD_TESTS ) diff --git a/kmymoney/reports/cashflowlist.cpp b/kmymoney/reports/cashflowlist.cpp new file mode 100644 index 000000000..d31b55a64 --- /dev/null +++ b/kmymoney/reports/cashflowlist.cpp @@ -0,0 +1,165 @@ +/* + * Copyright 2007 Sascha Pfau + * Copyright 2018 Ralf Habacker + * + * 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. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/* + * This file contains code from the func_xirr and related methods of + * financial.cpp from KOffice by Sascha Pfau. Sascha agreed to relicense + * those methods under GPLv2 or later. + */ + +#include "cashflowlist.h" + +#include + +#include +#include + +/** + * Calculates the internal rate of return for a non-periodic + * series of cash flows. The calculation is based on a 365 days + * per year basis, ignoring leap years. + * + * In case the internal rate of return could not be calculated + * a MyMoneyException is raised. + * + * @param rate optional guess for rate + * @return internal rate of return or MyMoneyException + */ +double CashFlowList::XIRR(double rate) const +{ + if (size() < 2) + throw MYMONEYEXCEPTION("illegal argument exception"); + + double resultRate = rate; + + // define max epsilon + static const double maxEpsilon = 1e-10; + + // max number of iterations + static const int maxIter = 50; + + // Newton's method - try to find a res, with a accuracy of maxEpsilon + double newRate, rateEpsilon, resultValue; + int iter = 0; + bool contLoop = false; + int iterScan = 0; + bool resultRateScanEnd = false; + + // First the inner while-loop will be executed using the default Value resultRate + // or the user guessed resultRate if those do not deliver a solution for the + // Newton's method then the range from -0.99 to +0.99 will be scanned with a + // step size of 0.01 to find resultRate's value which can deliver a solution + // source hint: + // - outer loop from libreoffice + // - inner loop from KOffice + do { + if (iterScan >=1) + resultRate = -0.99 + (iterScan -1)* 0.01; + + do { + resultValue = xirrResult(resultRate); + newRate = resultRate - resultValue / xirrResultDerive(resultRate); + rateEpsilon = fabs(newRate - resultRate); + resultRate = newRate; + contLoop = (rateEpsilon > maxEpsilon) && (fabs(resultValue) > maxEpsilon); + } while (contLoop && (++iter < maxIter)); + iter = 0; +#ifdef Q_CC_MSVC + if (_isinf(resultRate) || _isnan(resultRate) || + _isinf(resultValue) || _isnan(resultValue)) +#else + if (std::isinf(resultRate) || std::isnan(resultRate) || + std::isinf(resultValue) || std::isnan(resultValue)) +#endif + contLoop = true; + iterScan++; + resultRateScanEnd = (iterScan >= 200); + } while(contLoop && !resultRateScanEnd); + + if (contLoop) + throw MYMONEYEXCEPTION("illegal argument exception"); + return resultRate; +} + +/** + * Calculates the resulting amount for the passed interest rate + * + * @param rate interest rate + * @return resulting amount + */ +double CashFlowList::xirrResult(double rate) const +{ + double r = rate + 1.0; + double result = at(0).value().toDouble(); + const QDate &date0 = at(0).date(); + + for(int i = 1; i < size(); i++) { + double e_i = date0.daysTo(at(i).date()) / 365.0; + result += at(i).value().toDouble() / pow(r, e_i); + } + return result; +} + +/** + * Calculates the first derivation of the resulting amount + * + * @param rate interest rate + * @return first derivation of resulting amount + */ +double CashFlowList::xirrResultDerive(double rate) const +{ + double r = rate + 1.0; + double result = 0; + const QDate &date0 = at(0).date(); + + for(int i = 1; i < size(); i++) { + double e_i = date0.daysTo(at(i).date()) / 365.0; + result -= e_i * at(i).value().toDouble() / pow(r, e_i + 1.0); + } + return result; +} + +/** + * Return the sum of all payments + * + * @return sum of all payments + */ +MyMoneyMoney CashFlowList::total() const +{ + MyMoneyMoney result; + + const_iterator it_cash = begin(); + while (it_cash != end()) { + result += (*it_cash).value(); + ++it_cash; + } + + return result; +} + +/** + * dump all payments + */ +void CashFlowList::dumpDebug() const +{ + const_iterator it_item = begin(); + while (it_item != end()) { + kDebug(2) << (*it_item).date().toString(Qt::ISODate) << " " << (*it_item).value().toString(); + ++it_item; + } +} diff --git a/kmymoney/reports/cashflowlist.h b/kmymoney/reports/cashflowlist.h new file mode 100644 index 000000000..15efad4fb --- /dev/null +++ b/kmymoney/reports/cashflowlist.h @@ -0,0 +1,75 @@ +/* + * Copyright 2007 Sascha Pfau + * Copyright 2018 Ralf Habacker + * + * 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. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/* + * This file contains code from the func_xirr and related methods of + * financial.cpp from KOffice by Sascha Pfau. Sascha agreed to relicense + * those methods under GPLv2 or later. + */ + +#ifndef CASHFLOWLIST_H +#define CASHFLOWLIST_H + +#include "mymoneymoney.h" + +#include +#include + +class CashFlowListItem +{ +public: + CashFlowListItem() {} + CashFlowListItem(const QDate& _date, const MyMoneyMoney& _value): m_date(_date), m_value(_value) {} + bool operator<(const CashFlowListItem& _second) const { + return m_date < _second.m_date; + } + bool operator<=(const CashFlowListItem& _second) const { + return m_date <= _second.m_date; + } + bool operator>(const CashFlowListItem& _second) const { + return m_date > _second.m_date; + } + const QDate& date() const { + return m_date; + } + const MyMoneyMoney& value() const { + return m_value; + } + +private: + QDate m_date; + MyMoneyMoney m_value; +}; + +/** + * Cash flow analysis tools for investment reports + */ +class CashFlowList: public QList +{ +public: + CashFlowList() {} + double XIRR(double rate = 0.1) const; + MyMoneyMoney total() const; + void dumpDebug() const; + +private: + double xirrResult(double rate) const; + double xirrResultDerive(double rate) const; +}; + +#endif // CASHFLOWLIST_H diff --git a/kmymoney/reports/querytable.cpp b/kmymoney/reports/querytable.cpp index 332c1c627..a82a505a8 100644 --- a/kmymoney/reports/querytable.cpp +++ b/kmymoney/reports/querytable.cpp @@ -1,1581 +1,1392 @@ /*************************************************************************** querytable.cpp ------------------- begin : Fri Jul 23 2004 copyright : (C) 2004-2005 by Ace Jones - (C) 2007 Sascha Pfau ***************************************************************************/ -/**************************************************************************** - Contains code from the func_xirr and related methods of financial.cpp - - KOffice 1.6 by Sascha Pfau. Sascha agreed to relicense those methods under - GPLv2 or later. -*****************************************************************************/ - /*************************************************************************** * * * 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. * * * ***************************************************************************/ #include "querytable.h" #include // ---------------------------------------------------------------------------- // QT Includes #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include // ---------------------------------------------------------------------------- // Project Includes +#include "cashflowlist.h" #include "mymoneyfile.h" #include "mymoneytransaction.h" #include "mymoneyreport.h" #include "mymoneyexception.h" #include "kmymoneyutils.h" #include "reportaccount.h" #include "reportdebug.h" #include "kmymoneyglobalsettings.h" namespace reports { -// **************************************************************************** -// -// CashFlowListItem implementation -// -// Cash flow analysis tools for investment reports -// -// **************************************************************************** - -QDate CashFlowListItem::m_sToday = QDate::currentDate(); - -MyMoneyMoney CashFlowListItem::NPV(double _rate) const -{ - double T = static_cast(m_sToday.daysTo(m_date)) / 365.0; - MyMoneyMoney result(m_value.toDouble() / pow(1 + _rate, T), 100); - - //kDebug(2) << "CashFlowListItem::NPV( " << _rate << " ) == " << result; - - return result; -} - -// **************************************************************************** -// -// CashFlowList implementation -// -// Cash flow analysis tools for investment reports -// -// **************************************************************************** - -CashFlowListItem CashFlowList::mostRecent() const -{ - CashFlowList dupe(*this); - - qSort(dupe); - - //kDebug(2) << " CashFlowList::mostRecent() == " << dupe.back().date().toString(Qt::ISODate); - - return dupe.back(); -} - -MyMoneyMoney CashFlowList::NPV(double _rate) const -{ - MyMoneyMoney result; - - const_iterator it_cash = begin(); - while (it_cash != end()) { - result += (*it_cash).NPV(_rate); - ++it_cash; - } - - //kDebug(2) << "CashFlowList::NPV( " << _rate << " ) == " << result << "------------------------" << endl; - - return result; -} - -double CashFlowList::calculateXIRR() const -{ - double resultRate = 0.00001; - - double resultZero = 0.00000; - //if ( args.count() > 2 ) - // resultRate = calc->conv()->asFloat ( args[2] ).asFloat(); - -// check pairs and count >= 2 and guess > -1.0 - //if ( args[0].count() != args[1].count() || args[1].count() < 2 || resultRate <= -1.0 ) - // return Value::errorVALUE(); - -// define max epsilon - static const double maxEpsilon = 1e-5; - -// max number of iterations - static const int maxIter = 50; - -// Newton's method - try to find a res, with a accuracy of maxEpsilon - double rateEpsilon, newRate, resultValue; - int i = 0; - bool contLoop; - - do { - resultValue = xirrResult(resultRate); - - double resultDerive = xirrResultDerive(resultRate); - - //check what happens if xirrResultDerive is zero - //Don't know if it is correct to dismiss the result - if (resultDerive != 0) { - newRate = resultRate - resultValue / resultDerive; - } else { - - newRate = resultRate - resultValue; - } - - rateEpsilon = fabs(newRate - resultRate); - - resultRate = newRate; - contLoop = (rateEpsilon > maxEpsilon) && (fabs(resultValue) > maxEpsilon); - } while (contLoop && (++i < maxIter)); - - if (contLoop) - return resultZero; - - return resultRate; -} - -double CashFlowList::xirrResult(double& rate) const -{ - QDate date; - - double r = rate + 1.0; - double res = 0.00000;//back().value().toDouble(); - - QList::const_iterator list_it = begin(); - while (list_it != end()) { - double e_i = ((* list_it).today().daysTo((* list_it).date())) / 365.0; - MyMoneyMoney val = (* list_it).value(); - - if (e_i < 0) { - res += val.toDouble() * pow(r, -e_i); - } else { - res += val.toDouble() / pow(r, e_i); - } - ++list_it; - } - - return res; -} - - -double CashFlowList::xirrResultDerive(double& rate) const -{ - QDate date; - - double r = rate + 1.0; - double res = 0.00000; - - QList::const_iterator list_it = begin(); - while (list_it != end()) { - double e_i = ((* list_it).today().daysTo((* list_it).date())) / 365.0; - MyMoneyMoney val = (* list_it).value(); - - res -= e_i * val.toDouble() / pow(r, e_i + 1.0); - ++list_it; - } - - return res; -} - -double CashFlowList::IRR() const -{ - double result = 0.0; - - // set 'today', which is the most recent of all dates in the list - CashFlowListItem::setToday(mostRecent().date()); - - result = calculateXIRR(); - return result; -} - -MyMoneyMoney CashFlowList::total() const -{ - MyMoneyMoney result; - - const_iterator it_cash = begin(); - while (it_cash != end()) { - result += (*it_cash).value(); - ++it_cash; - } - - return result; -} - -void CashFlowList::dumpDebug() const -{ - const_iterator it_item = begin(); - while (it_item != end()) { - kDebug(2) << (*it_item).date().toString(Qt::ISODate) << " " << (*it_item).value().toString(); - ++it_item; - } -} - // **************************************************************************** // // QueryTable implementation // // **************************************************************************** /** * TODO * * - Collapse 2- & 3- groups when they are identical * - Way more test cases (especially splits & transfers) * - Option to collapse splits * - Option to exclude transfers * */ QueryTable::QueryTable(const MyMoneyReport& _report): ListTable(_report) { // separated into its own method to allow debugging (setting breakpoints // directly in ctors somehow does not work for me (ipwizard)) // TODO: remove the init() method and move the code back to the ctor init(); } void QueryTable::init() { switch (m_config.rowType()) { case MyMoneyReport::Row::AccountByTopAccount: case MyMoneyReport::Row::EquityType: case MyMoneyReport::Row::AccountType: case MyMoneyReport::Row::Institution: constructAccountTable(); m_columns = "account"; break; case MyMoneyReport::Row::Account: constructTransactionTable(); m_columns = "accountid,postdate"; break; case MyMoneyReport::Row::Payee: case MyMoneyReport::Row::Tag: case MyMoneyReport::Row::Month: case MyMoneyReport::Row::Week: constructTransactionTable(); m_columns = "postdate,account"; break; case MyMoneyReport::Row::CashFlow: constructSplitsTable(); m_columns = "postdate"; break; default: constructTransactionTable(); m_columns = "postdate"; } // Sort the data to match the report definition m_subtotal = "value"; switch (m_config.rowType()) { case MyMoneyReport::Row::CashFlow: m_group = "categorytype,topcategory,category"; break; case MyMoneyReport::Row::Category: m_group = "categorytype,topcategory,category"; break; case MyMoneyReport::Row::TopCategory: m_group = "categorytype,topcategory"; break; case MyMoneyReport::Row::TopAccount: m_group = "topaccount,account"; break; case MyMoneyReport::Row::Account: m_group = "account"; break; case MyMoneyReport::Row::AccountReconcile: m_group = "account,reconcileflag"; break; case MyMoneyReport::Row::Payee: m_group = "payee"; break; case MyMoneyReport::Row::Tag: m_group = "tag"; break; case MyMoneyReport::Row::Month: m_group = "month"; break; case MyMoneyReport::Row::Week: m_group = "week"; break; case MyMoneyReport::Row::AccountByTopAccount: m_group = "topaccount"; break; case MyMoneyReport::Row::EquityType: m_group = "equitytype"; break; case MyMoneyReport::Row::AccountType: m_group = "type"; break; case MyMoneyReport::Row::Institution: m_group = "institution,topaccount"; break; default: throw MYMONEYEXCEPTION("QueryTable::QueryTable(): unhandled row type"); } QString sort; switch (m_config.rowType()) { case MyMoneyReport::Row::Month: case MyMoneyReport::Row::Week: sort = m_group + "sort," + m_columns + ",id,rank"; break; default: sort = m_group + ',' + m_columns + ",id,rank"; break; } switch (m_config.rowType()) { case MyMoneyReport::Row::AccountByTopAccount: case MyMoneyReport::Row::EquityType: case MyMoneyReport::Row::AccountType: case MyMoneyReport::Row::Institution: m_columns = "account"; break; default: m_columns = "postdate"; } unsigned qc = m_config.queryColumns(); if (qc & MyMoneyReport::QueryColumns::Number) m_columns += ",number"; if (qc & MyMoneyReport::QueryColumns::Payee) m_columns += ",payee"; if (qc & MyMoneyReport::QueryColumns::Tag) m_columns += ",tag"; if (qc & MyMoneyReport::QueryColumns::Category) m_columns += ",category"; if (qc & MyMoneyReport::QueryColumns::Account) m_columns += ",account"; if (qc & MyMoneyReport::QueryColumns::Reconciled) m_columns += ",reconcileflag"; if (qc & MyMoneyReport::QueryColumns::Memo) m_columns += ",memo"; if (qc & MyMoneyReport::QueryColumns::Action) m_columns += ",action"; if (qc & MyMoneyReport::QueryColumns::Shares) m_columns += ",shares"; if (qc & MyMoneyReport::QueryColumns::Price) m_columns += ",price"; if (qc & MyMoneyReport::QueryColumns::Performance) m_columns += ",startingbal,buys,sells,reinvestincome,cashincome,return,returninvestment"; if (qc & MyMoneyReport::QueryColumns::Loan) { m_columns += ",payment,interest,fees"; m_postcolumns = "balance"; } if (qc & MyMoneyReport::QueryColumns::Balance) m_postcolumns = "balance"; TableRow::setSortCriteria(sort); qSort(m_rows); } /** * helper function used by querytabletest * @param date date to create the string from * @return string with formatted date */ QString QueryTable::toDateString(const QDate &date) { return date.toString(Qt::DefaultLocaleLongDate); } void QueryTable::constructTransactionTable() { MyMoneyFile* file = MyMoneyFile::instance(); //make sure we have all subaccounts of investment accounts includeInvestmentSubAccounts(); MyMoneyReport report(m_config); report.setReportAllSplits(false); report.setConsiderCategory(true); bool use_transfers = false; bool use_summary = false; bool hide_details = false; bool tag_special_case = false; switch (m_config.rowType()) { case MyMoneyReport::Row::Category: case MyMoneyReport::Row::TopCategory: use_summary = false; use_transfers = false; hide_details = false; break; case MyMoneyReport::Row::Payee: use_summary = false; use_transfers = false; hide_details = (m_config.detailLevel() == MyMoneyReport::DetailLevel::None); break; case MyMoneyReport::Row::Tag: use_summary = false; use_transfers = false; hide_details = (m_config.detailLevel() == MyMoneyReport::DetailLevel::None); tag_special_case = true; break; default: use_summary = true; use_transfers = true; hide_details = (m_config.detailLevel() == MyMoneyReport::DetailLevel::None); break; } // support for opening and closing balances QMap accts; //get all transactions for this report QList transactions = file->transactionList(report); for (QList::const_iterator it_transaction = transactions.constBegin(); it_transaction != transactions.constEnd(); ++it_transaction) { TableRow qA, qS; QDate pd; QList tagIdListCache; qA["id"] = qS["id"] = (* it_transaction).id(); qA["entrydate"] = qS["entrydate"] = (* it_transaction).entryDate().toString(Qt::ISODate); qA["postdate"] = qS["postdate"] = (* it_transaction).postDate().toString(Qt::ISODate); qA["commodity"] = qS["commodity"] = (* it_transaction).commodity(); pd = (* it_transaction).postDate(); qA["monthsort"] = qS["monthsort"] = i18n("Month of %1", QDate(pd.year(), pd.month(), 1).toString(Qt::ISODate)); qA["weeksort"] = qS["weeksort"] = i18n("Week of %1", pd.addDays(1 - pd.dayOfWeek()).toString(Qt::ISODate)); qA["month"] = qS["month"] = i18n("Month of %1", toDateString(QDate(pd.year(), pd.month(), 1))); qA["week"] = qS["week"] = i18n("Week of %1", toDateString(pd.addDays(1 - pd.dayOfWeek()))); qA["currency"] = qS["currency"] = ""; if ((* it_transaction).commodity() != file->baseCurrency().id()) { if (!report.isConvertCurrency()) { qA["currency"] = qS["currency"] = (*it_transaction).commodity(); } } // to handle splits, we decide on which account to base the split // (a reference point or point of view so to speak). here we take the // first account that is a stock account or loan account (or the first account // that is not an income or expense account if there is no stock or loan account) // to be the account (qA) that will have the sub-item "split" entries. we add // one transaction entry (qS) for each subsequent entry in the split. const QList& splits = (*it_transaction).splits(); QList::const_iterator myBegin, it_split; for (it_split = splits.begin(), myBegin = splits.end(); it_split != splits.end(); ++it_split) { ReportAccount splitAcc = (* it_split).accountId(); // always put split with a "stock" account if it exists if (splitAcc.isInvest()) break; // prefer to put splits with a "loan" account if it exists if (splitAcc.isLoan()) myBegin = it_split; if ((myBegin == splits.end()) && ! splitAcc.isIncomeExpense()) { myBegin = it_split; } } // select our "reference" split if (it_split == splits.end()) { it_split = myBegin; } else { myBegin = it_split; } // skip this transaction if we didn't find a valid base account - see the above description // for the base account's description - if we don't find it avoid a crash by skipping the transaction if (myBegin == splits.end()) continue; // if the split is still unknown, use the first one. I have seen this // happen with a transaction that has only a single split referencing an income or expense // account and has an amount and value of 0. Such a transaction will fall through // the above logic and leave 'it_split' pointing to splits.end() which causes the remainder // of this to end in an infinite loop. if (it_split == splits.end()) { it_split = splits.begin(); } // for "loan" reports, the loan transaction gets special treatment. // the splits of a loan transaction are placed on one line in the // reference (loan) account (qA). however, we process the matching // split entries (qS) normally. bool loan_special_case = false; if (m_config.queryColumns() & MyMoneyReport::QueryColumns::Loan) { ReportAccount splitAcc = (*it_split).accountId(); loan_special_case = splitAcc.isLoan(); } bool include_me = true; bool transaction_text = false; //indicates whether a text should be considered as a match for the transaction or for a split only QString a_fullname = ""; QString a_memo = ""; int pass = 1; QString myBeginCurrency = (file->account((*myBegin).accountId())).currencyId(); //currency of the main split do { MyMoneyMoney xr; ReportAccount splitAcc = (* it_split).accountId(); //get fraction for account int fraction = splitAcc.currency().smallestAccountFraction(); //use base currency fraction if not initialized if (fraction == -1) fraction = file->baseCurrency().smallestAccountFraction(); QString institution = splitAcc.institutionId(); QString payee = (*it_split).payeeId(); const QList tagIdList = (*it_split).tagIdList(); //convert to base currency if (m_config.isConvertCurrency()) { xr = (splitAcc.deepCurrencyPrice((*it_transaction).postDate()) * splitAcc.baseCurrencyPrice((*it_transaction).postDate())).reduce(); } else { xr = (splitAcc.deepCurrencyPrice((*it_transaction).postDate())).reduce(); } if (splitAcc.isInvest()) { // use the institution of the parent for stock accounts institution = splitAcc.parent().institutionId(); MyMoneyMoney shares = (*it_split).shares(); qA["action"] = (*it_split).action(); qA["shares"] = shares.isZero() ? "" : (*it_split).shares().toString(); qA["price"] = shares.isZero() ? "" : xr.convert(MyMoneyMoney::precToDenom(KMyMoneyGlobalSettings::pricePrecision())).toString(); if (((*it_split).action() == MyMoneySplit::ActionBuyShares) && (*it_split).shares().isNegative()) qA["action"] = "Sell"; qA["investaccount"] = splitAcc.parent().name(); } if (it_split == myBegin) { include_me = m_config.includes(splitAcc); a_fullname = splitAcc.fullName(); a_memo = (*it_split).memo(); transaction_text = m_config.match(&(*it_split)); qA["price"] = xr.toString(); qA["account"] = splitAcc.name(); qA["accountid"] = splitAcc.id(); qA["topaccount"] = splitAcc.topParentName(); qA["institution"] = institution.isEmpty() ? i18n("No Institution") : file->institution(institution).name(); qA["payee"] = payee.isEmpty() ? i18n("[Empty Payee]") : file->payee(payee).name().simplified(); if (tag_special_case) { tagIdListCache = tagIdList; } else { QString delimiter = ""; for (int i = 0; i < tagIdList.size(); i++) { qA["tag"] += delimiter + file->tag(tagIdList[i]).name().simplified(); delimiter = ", "; } } qA["reconciledate"] = (*it_split).reconcileDate().toString(Qt::ISODate); qA["reconcileflag"] = KMyMoneyUtils::reconcileStateToString((*it_split).reconcileFlag(), true); qA["number"] = (*it_split).number(); qA["memo"] = a_memo; qA["value"] = (((*it_split).shares()) * xr).convert(fraction).toString(); qS["reconciledate"] = qA["reconciledate"]; qS["reconcileflag"] = qA["reconcileflag"]; qS["number"] = qA["number"]; qS["topcategory"] = splitAcc.topParentName(); qS["categorytype"] = i18n("Transfer"); // only include the configured accounts if (include_me) { if (loan_special_case) { // put the principal amount in the "value" column and convert to lowest fraction qA["value"] = ((-(*it_split).shares()) * xr).convert(fraction).toString(); qA["rank"] = '0'; qA["split"] = ""; } else { if ((splits.count() > 2) && use_summary) { // add the "summarized" split transaction // this is the sub-total of the split detail // convert to lowest fraction qA["value"] = ((*it_split).shares() * xr).convert(fraction).toString(); qA["rank"] = '0'; qA["category"] = i18n("[Split Transaction]"); qA["topcategory"] = i18nc("Split transaction", "Split"); qA["categorytype"] = i18nc("Split transaction", "Split"); m_rows += qA; } } // track accts that will need opening and closing balances //FIXME in some cases it will show the opening and closing //balances but no transactions if the splits are all filtered out -- asoliverez accts.insert(splitAcc.id(), splitAcc); } } else { if (include_me) { if (loan_special_case) { MyMoneyMoney value = ((-(* it_split).shares()) * xr).convert(fraction); if ((*it_split).action() == MyMoneySplit::ActionAmortization) { // put the payment in the "payment" column and convert to lowest fraction qA["payment"] = value.toString(); } else if ((*it_split).action() == MyMoneySplit::ActionInterest) { // put the interest in the "interest" column and convert to lowest fraction qA["interest"] = value.toString(); } else if (splits.count() > 2) { // [dv: This comment carried from the original code. I am // not exactly clear on what it means or why we do this.] // Put the initial pay-in nowhere (that is, ignore it). This // is dangerous, though. The only way I can tell the initial // pay-in apart from fees is if there are only 2 splits in // the transaction. I wish there was a better way. } else { // accumulate everything else in the "fees" column MyMoneyMoney n0 = MyMoneyMoney(qA["fees"]); qA["fees"] = (n0 + value).toString(); } // we don't add qA here for a loan transaction. we'll add one // qA afer all of the split components have been processed. // (see below) } //--- special case to hide split transaction details else if (hide_details && (splits.count() > 2)) { // essentially, don't add any qA entries } //--- default case includes all transaction details else { //this is when the splits are going to be shown as children of the main split if ((splits.count() > 2) && use_summary) { qA["value"] = ""; //convert to lowest fraction qA["split"] = ((-(*it_split).shares()) * xr).convert(fraction).toString(); qA["rank"] = '1'; } else { //this applies when the transaction has only 2 splits, or each split is going to be //shown separately, eg. transactions by category qA["split"] = ""; // multiply by currency and convert to lowest fraction // but only for income and expense // transfers are dealt with somewhere else below if (splitAcc.isIncomeExpense()) { // if the currency of the split is different from the currency of the main split, then convert to the currency of the main split MyMoneyMoney ieXr(xr); if (!m_config.isConvertCurrency() && splitAcc.currency().id() != myBeginCurrency) { ieXr = (xr * splitAcc.foreignCurrencyPrice(myBeginCurrency, (*it_transaction).postDate())).reduce(); } qA["value"] = ((-(*it_split).shares()) * ieXr).convert(fraction).toString(); } qA["rank"] = '0'; } qA ["memo"] = (*it_split).memo(); // if different from base currency and not converting // show the currency of the split if (splitAcc.currencyId() != file->baseCurrency().id()) { if (!report.isConvertCurrency()) { qS["currency"] = splitAcc.currencyId(); } } else { qS["currency"] = ""; } if (! splitAcc.isIncomeExpense()) { qA["category"] = ((*it_split).shares().isNegative()) ? i18n("Transfer from %1", splitAcc.fullName()) : i18n("Transfer to %1", splitAcc.fullName()); qA["topcategory"] = splitAcc.topParentName(); qA["categorytype"] = i18n("Transfer"); } else { qA ["category"] = splitAcc.fullName(); qA ["topcategory"] = splitAcc.topParentName(); qA ["categorytype"] = KMyMoneyUtils::accountTypeToString(splitAcc.accountGroup()); } if (use_transfers || (splitAcc.isIncomeExpense() && m_config.includes(splitAcc))) { //if it matches the text of the main split of the transaction or //it matches this particular split, include it //otherwise, skip it //if the filter is "does not contain" exclude the split if it does not match //even it matches the whole split if ((m_config.isInvertingText() && m_config.match(&(*it_split))) || (!m_config.isInvertingText() && (transaction_text || m_config.match(&(*it_split))))) { if (tag_special_case) { if (!tagIdListCache.size()) qA["tag"] = i18n("[No Tag]"); else for (int i = 0; i < tagIdListCache.size(); i++) { qA["tag"] = file->tag(tagIdListCache[i]).name().simplified(); m_rows += qA; } } else { m_rows += qA; } } } } } if (m_config.includes(splitAcc) && use_transfers) { if (! splitAcc.isIncomeExpense()) { //multiply by currency and convert to lowest fraction qS["value"] = ((*it_split).shares() * xr).convert(fraction).toString(); qS["rank"] = '0'; qS["account"] = splitAcc.name(); qS["accountid"] = splitAcc.id(); qS["topaccount"] = splitAcc.topParentName(); qS["category"] = ((*it_split).shares().isNegative()) ? i18n("Transfer to %1", a_fullname) : i18n("Transfer from %1", a_fullname); qS["institution"] = institution.isEmpty() ? i18n("No Institution") : file->institution(institution).name(); qS["memo"] = (*it_split).memo().isEmpty() ? a_memo : (*it_split).memo(); //FIXME-ALEX When is used this? I can't find in which condition we arrive here... maybe this code is useless? QString delimiter = ""; for (int i = 0; i < tagIdList.size(); i++) { qA["tag"] += delimiter + file->tag(tagIdList[i]).name().simplified(); delimiter = '+'; } qS["payee"] = payee.isEmpty() ? qA["payee"] : file->payee(payee).name().simplified(); //check the specific split against the filter for text and amount //TODO this should be done at the engine, but I have no clear idea how -- asoliverez //if the filter is "does not contain" exclude the split if it does not match //even it matches the whole split if ((m_config.isInvertingText() && m_config.match(&(*it_split))) || (!m_config.isInvertingText() && (transaction_text || m_config.match(&(*it_split))))) { m_rows += qS; // track accts that will need opening and closing balances accts.insert(splitAcc.id(), splitAcc); } } } } ++it_split; // look for wrap-around if (it_split == splits.end()) it_split = splits.begin(); // but terminate if this transaction has only a single split if (splits.count() < 2) break; //check if there have been more passes than there are splits //this is to prevent infinite loops in cases of data inconsistency -- asoliverez ++pass; if (pass > splits.count()) break; } while (it_split != myBegin); if (loan_special_case) { m_rows += qA; } } // now run through our accts list and add opening and closing balances switch (m_config.rowType()) { case MyMoneyReport::Row::Account: case MyMoneyReport::Row::TopAccount: break; // case MyMoneyReport::eCategory: // case MyMoneyReport::eTopCategory: // case MyMoneyReport::ePayee: // case MyMoneyReport::eMonth: // case MyMoneyReport::eWeek: default: return; } QDate startDate, endDate; report.validDateRange(startDate, endDate); QString strStartDate = startDate.toString(Qt::ISODate); QString strEndDate = endDate.toString(Qt::ISODate); startDate = startDate.addDays(-1); QMap::const_iterator it_account, accts_end; for (it_account = accts.constBegin(); it_account != accts.constEnd(); ++it_account) { TableRow qA; ReportAccount account = (* it_account); //get fraction for account int fraction = account.currency().smallestAccountFraction(); //use base currency fraction if not initialized if (fraction == -1) fraction = file->baseCurrency().smallestAccountFraction(); QString institution = account.institutionId(); // use the institution of the parent for stock accounts if (account.isInvest()) institution = account.parent().institutionId(); MyMoneyMoney startBalance, endBalance, startPrice, endPrice; MyMoneyMoney startShares, endShares; //get price and convert currency if necessary if (m_config.isConvertCurrency()) { startPrice = (account.deepCurrencyPrice(startDate) * account.baseCurrencyPrice(startDate)).reduce(); endPrice = (account.deepCurrencyPrice(endDate) * account.baseCurrencyPrice(endDate)).reduce(); } else { startPrice = account.deepCurrencyPrice(startDate).reduce(); endPrice = account.deepCurrencyPrice(endDate).reduce(); } startShares = file->balance(account.id(), startDate); endShares = file->balance(account.id(), endDate); //get starting and ending balances startBalance = startShares * startPrice; endBalance = endShares * endPrice; //starting balance // don't show currency if we're converting or if it's not foreign qA["currency"] = (m_config.isConvertCurrency() || ! account.isForeignCurrency()) ? "" : account.currency().id(); qA["accountid"] = account.id(); qA["account"] = account.name(); qA["topaccount"] = account.topParentName(); qA["institution"] = institution.isEmpty() ? i18n("No Institution") : file->institution(institution).name(); qA["rank"] = "-2"; qA["price"] = startPrice.convert(MyMoneyMoney::precToDenom(KMyMoneyGlobalSettings::pricePrecision())).toString(); if (account.isInvest()) { qA["shares"] = startShares.toString(); } qA["postdate"] = strStartDate; qA["balance"] = startBalance.convert(fraction).toString(); qA["value"].clear(); qA["id"] = 'A'; m_rows += qA; //ending balance qA["price"] = endPrice.convert(MyMoneyMoney::precToDenom(KMyMoneyGlobalSettings::pricePrecision())).toString(); if (account.isInvest()) { qA["shares"] = endShares.toString(); } qA["postdate"] = strEndDate; qA["balance"] = endBalance.toString(); qA["id"] = 'Z'; m_rows += qA; } } void QueryTable::constructPerformanceRow(const ReportAccount& account, TableRow& result) const { MyMoneyFile* file = MyMoneyFile::instance(); MyMoneySecurity security = file->security(account.currencyId()); result["equitytype"] = KMyMoneyUtils::securityTypeToString(security.securityType()); //get fraction depending on type of account int fraction = account.currency().smallestAccountFraction(); // // Calculate performance // // The following columns are created: // Account, Value on , Buys, Sells, Income, Value on , Return% MyMoneyReport report = m_config; QDate startingDate; QDate endingDate; MyMoneyMoney price; report.validDateRange(startingDate, endingDate); startingDate = startingDate.addDays(-1); //calculate starting balance if (m_config.isConvertCurrency()) { price = account.deepCurrencyPrice(startingDate) * account.baseCurrencyPrice(startingDate); } else { price = account.deepCurrencyPrice(startingDate); } //work around if there is no price for the starting balance if (!(file->balance(account.id(), startingDate)).isZero() && account.deepCurrencyPrice(startingDate) == MyMoneyMoney::ONE) { MyMoneyTransactionFilter filter; //get the transactions for the time before the report filter.setDateFilter(QDate(), startingDate); filter.addAccount(account.id()); filter.setReportAllSplits(true); QList startTransactions = file->transactionList(filter); if (startTransactions.size() > 0) { //get the last transaction MyMoneyTransaction startTrans = startTransactions.back(); MyMoneySplit s = startTrans.splitByAccount(account.id()); //get the price from the split of that account price = s.price(); if (m_config.isConvertCurrency()) price = price * account.baseCurrencyPrice(startingDate); } } if (m_config.isConvertCurrency()) { price = account.deepCurrencyPrice(startingDate) * account.baseCurrencyPrice(startingDate); } else { price = account.deepCurrencyPrice(startingDate); } MyMoneyMoney startingBal = file->balance(account.id(), startingDate) * price; //convert to lowest fraction startingBal = startingBal.convert(fraction); //calculate ending balance if (m_config.isConvertCurrency()) { price = account.deepCurrencyPrice(endingDate) * account.baseCurrencyPrice(endingDate); } else { price = account.deepCurrencyPrice(endingDate); } MyMoneyMoney endingBal = file->balance((account).id(), endingDate) * price; //convert to lowest fraction endingBal = endingBal.convert(fraction); //add start balance to calculate return on investment MyMoneyMoney returnInvestment = startingBal; MyMoneyMoney paidDividend; CashFlowList buys; CashFlowList sells; CashFlowList reinvestincome; CashFlowList cashincome; report.setReportAllSplits(false); report.setConsiderCategory(true); report.clearAccountFilter(); report.addAccount(account.id()); QList transactions = file->transactionList(report); QList::const_iterator it_transaction = transactions.constBegin(); while (it_transaction != transactions.constEnd()) { // s is the split for the stock account MyMoneySplit s = (*it_transaction).splitByAccount(account.id()); //get price for the day of the transaction if we have to calculate base currency //we are using the value of the split which is in deep currency if (m_config.isConvertCurrency()) { price = account.baseCurrencyPrice((*it_transaction).postDate()); //we only need base currency because the value is in deep currency } else { price = MyMoneyMoney::ONE; } MyMoneyMoney value = s.value() * price; const QString& action = s.action(); if (action == MyMoneySplit::ActionBuyShares) { if (s.value().isPositive()) { buys += CashFlowListItem((*it_transaction).postDate(), -value); } else { sells += CashFlowListItem((*it_transaction).postDate(), -value); } returnInvestment += value; //convert to lowest fraction returnInvestment = returnInvestment.convert(fraction); } else if (action == MyMoneySplit::ActionReinvestDividend) { reinvestincome += CashFlowListItem((*it_transaction).postDate(), value); } else if (action == MyMoneySplit::ActionDividend || action == MyMoneySplit::ActionYield) { // find the split with the category, which has the actual amount of the dividend QList splits = (*it_transaction).splits(); QList::const_iterator it_split = splits.constBegin(); while (it_split != splits.constEnd()) { ReportAccount acc = (*it_split).accountId(); if (acc.isIncomeExpense()) { cashincome += CashFlowListItem((*it_transaction).postDate(), -(*it_split).value() * price); paidDividend += ((-(*it_split).value()) * price).convert(fraction); } ++it_split; } } else if (action == MyMoneySplit::ActionAddShares) { // Add shares is not a buy operation, do nothing } else { //if the split does not match any action above, add it as buy or sell depending on sign //if value is zero, get the price for that date if (s.value().isZero()) { if (m_config.isConvertCurrency()) { price = account.deepCurrencyPrice((*it_transaction).postDate()) * account.baseCurrencyPrice((*it_transaction).postDate()); } else { price = account.deepCurrencyPrice((*it_transaction).postDate()); } value = s.shares() * price; if (s.shares().isPositive()) { buys += CashFlowListItem((*it_transaction).postDate(), -value); } else { sells += CashFlowListItem((*it_transaction).postDate(), -value); } returnInvestment += value; } else { value = s.value() * price; if (s.value().isPositive()) { buys += CashFlowListItem((*it_transaction).postDate(), -value); } else { sells += CashFlowListItem((*it_transaction).postDate(), -value); } returnInvestment += value; } } ++it_transaction; } // Note that reinvested dividends are not included , because these do not // represent a cash flow event. CashFlowList all; all += buys; all += sells; all += cashincome; all += CashFlowListItem(startingDate, -startingBal); all += CashFlowListItem(endingDate, endingBal); //check if no activity on that term if (!returnInvestment.isZero() && !endingBal.isZero()) { returnInvestment = ((endingBal + paidDividend) - returnInvestment) / returnInvestment; returnInvestment = returnInvestment.convert(10000); } else { returnInvestment = MyMoneyMoney(); } try { - double irr = all.IRR(); -#ifdef Q_CC_MSVC - MyMoneyMoney annualReturn = MyMoneyMoney(_isnan(irr) ? 0 : irr, 10000); -#else - MyMoneyMoney annualReturn = MyMoneyMoney(std::isnan(irr) ? 0 : irr, 10000); -#endif + MyMoneyMoney annualReturn(all.XIRR(), 10000); result["return"] = annualReturn.toString(); result["returninvestment"] = returnInvestment.toString(); - } catch (QString e) { - kDebug(2) << e; + } catch (MyMoneyException &e) { + result["return"] = ""; + kDebug(2) << e.what(); } result["buys"] = (-(buys.total())).toString(); result["sells"] = (-(sells.total())).toString(); result["cashincome"] = (cashincome.total()).toString(); result["reinvestincome"] = (reinvestincome.total()).toString(); result["startingbal"] = (startingBal).toString(); result["endingbal"] = (endingBal).toString(); } void QueryTable::constructAccountTable() { MyMoneyFile* file = MyMoneyFile::instance(); //make sure we have all subaccounts of investment accounts includeInvestmentSubAccounts(); QList accounts; file->accountList(accounts); QList::const_iterator it_account = accounts.constBegin(); while (it_account != accounts.constEnd()) { ReportAccount account = *it_account; //get fraction for account int fraction = account.currency().smallestAccountFraction(); //use base currency fraction if not initialized if (fraction == -1) fraction = MyMoneyFile::instance()->baseCurrency().smallestAccountFraction(); // Note, "Investment" accounts are never included in account rows because // they don't contain anything by themselves. In reports, they are only // useful as a "topaccount" aggregator of stock accounts if (account.isAssetLiability() && m_config.includes(account) && account.accountType() != MyMoneyAccount::Investment) { TableRow qaccountrow; // help for sort and render functions qaccountrow["rank"] = '0'; // // Handle currency conversion // MyMoneyMoney displayprice(1, 1); if (m_config.isConvertCurrency()) { // display currency is base currency, so set the price if (account.isForeignCurrency()) displayprice = account.baseCurrencyPrice(m_config.toDate()).reduce(); } else { // display currency is the account's deep currency. display this fact in the report qaccountrow["currency"] = account.currency().id(); } qaccountrow["account"] = account.name(); qaccountrow["accountid"] = account.id(); qaccountrow["topaccount"] = account.topParentName(); MyMoneyMoney shares = file->balance(account.id(), m_config.toDate()); qaccountrow["shares"] = shares.toString(); MyMoneyMoney netprice = account.deepCurrencyPrice(m_config.toDate()).reduce() * displayprice; qaccountrow["price"] = (netprice.reduce()).convert(MyMoneyMoney::precToDenom(KMyMoneyGlobalSettings::pricePrecision())).toString(); qaccountrow["value"] = (netprice.reduce() * shares.reduce()).convert(fraction).toString(); QString iid = (*it_account).institutionId(); // If an account does not have an institution, get it from the top-parent. if (iid.isEmpty() && ! account.isTopLevel()) { ReportAccount topaccount = account.topParent(); iid = topaccount.institutionId(); } if (iid.isEmpty()) qaccountrow["institution"] = i18nc("No institution", "None"); else qaccountrow["institution"] = file->institution(iid).name(); qaccountrow["type"] = KMyMoneyUtils::accountTypeToString((*it_account).accountType()); // TODO: Only do this if the report we're making really needs performance. Otherwise // it's an expensive calculation done for no reason if (account.isInvest()) { constructPerformanceRow(account, qaccountrow); } else qaccountrow["equitytype"].clear(); // don't add the account if it is closed. In fact, the business logic // should prevent that an account can be closed with a balance not equal // to zero, but we never know. if (!(shares.isZero() && account.isClosed())) m_rows += qaccountrow; } ++it_account; } } void QueryTable::constructSplitsTable() { MyMoneyFile* file = MyMoneyFile::instance(); //make sure we have all subaccounts of investment accounts includeInvestmentSubAccounts(); MyMoneyReport report(m_config); report.setReportAllSplits(false); report.setConsiderCategory(true); // support for opening and closing balances QMap accts; //get all transactions for this report QList transactions = file->transactionList(report); for (QList::const_iterator it_transaction = transactions.constBegin(); it_transaction != transactions.constEnd(); ++it_transaction) { TableRow qA, qS; QDate pd; qA["id"] = qS["id"] = (* it_transaction).id(); qA["entrydate"] = qS["entrydate"] = (* it_transaction).entryDate().toString(Qt::ISODate); qA["postdate"] = qS["postdate"] = (* it_transaction).postDate().toString(Qt::ISODate); qA["commodity"] = qS["commodity"] = (* it_transaction).commodity(); pd = (* it_transaction).postDate(); qA["month"] = qS["month"] = i18n("Month of %1", QDate(pd.year(), pd.month(), 1).toString(Qt::ISODate)); qA["week"] = qS["week"] = i18n("Week of %1", pd.addDays(1 - pd.dayOfWeek()).toString(Qt::ISODate)); qA["currency"] = qS["currency"] = ""; if ((* it_transaction).commodity() != file->baseCurrency().id()) { if (!report.isConvertCurrency()) { qA["currency"] = qS["currency"] = (*it_transaction).commodity(); } } // to handle splits, we decide on which account to base the split // (a reference point or point of view so to speak). here we take the // first account that is a stock account or loan account (or the first account // that is not an income or expense account if there is no stock or loan account) // to be the account (qA) that will have the sub-item "split" entries. we add // one transaction entry (qS) for each subsequent entry in the split. const QList& splits = (*it_transaction).splits(); QList::const_iterator myBegin, it_split; //S_end = splits.end(); for (it_split = splits.begin(), myBegin = splits.end(); it_split != splits.end(); ++it_split) { ReportAccount splitAcc = (* it_split).accountId(); // always put split with a "stock" account if it exists if (splitAcc.isInvest()) break; // prefer to put splits with a "loan" account if it exists if (splitAcc.isLoan()) myBegin = it_split; if ((myBegin == splits.end()) && ! splitAcc.isIncomeExpense()) { myBegin = it_split; } } // select our "reference" split if (it_split == splits.end()) { it_split = myBegin; } else { myBegin = it_split; } // if the split is still unknown, use the first one. I have seen this // happen with a transaction that has only a single split referencing an income or expense // account and has an amount and value of 0. Such a transaction will fall through // the above logic and leave 'it_split' pointing to splits.end() which causes the remainder // of this to end in an infinite loop. if (it_split == splits.end()) { it_split = splits.begin(); } // for "loan" reports, the loan transaction gets special treatment. // the splits of a loan transaction are placed on one line in the // reference (loan) account (qA). however, we process the matching // split entries (qS) normally. bool loan_special_case = false; if (m_config.queryColumns() & MyMoneyReport::QueryColumns::Loan) { ReportAccount splitAcc = (*it_split).accountId(); loan_special_case = splitAcc.isLoan(); } // There is a slight chance that at this point myBegin is still pointing to splits.end() if the // transaction only has income and expense splits (which should not happen). In that case, point // it to the first split if (myBegin == splits.end()) { myBegin = splits.begin(); } //the account of the beginning splits ReportAccount myBeginAcc = (*myBegin).accountId(); bool include_me = true; QString a_fullname = ""; QString a_memo = ""; int pass = 1; do { MyMoneyMoney xr; ReportAccount splitAcc = (* it_split).accountId(); //get fraction for account int fraction = splitAcc.currency().smallestAccountFraction(); //use base currency fraction if not initialized if (fraction == -1) fraction = file->baseCurrency().smallestAccountFraction(); QString institution = splitAcc.institutionId(); QString payee = (*it_split).payeeId(); const QList tagIdList = (*it_split).tagIdList(); if (m_config.isConvertCurrency()) { xr = (splitAcc.deepCurrencyPrice((*it_transaction).postDate()) * splitAcc.baseCurrencyPrice((*it_transaction).postDate())).reduce(); } else { xr = splitAcc.deepCurrencyPrice((*it_transaction).postDate()).reduce(); } // reverse the sign of incomes and expenses to keep consistency in the way it is displayed in other reports if (splitAcc.isIncomeExpense()) { xr = -xr; } if (splitAcc.isInvest()) { // use the institution of the parent for stock accounts institution = splitAcc.parent().institutionId(); MyMoneyMoney shares = (*it_split).shares(); qA["action"] = (*it_split).action(); qA["shares"] = shares.isZero() ? "" : (*it_split).shares().toString(); qA["price"] = shares.isZero() ? "" : xr.convert(MyMoneyMoney::precToDenom(KMyMoneyGlobalSettings::pricePrecision())).toString(); if (((*it_split).action() == MyMoneySplit::ActionBuyShares) && (*it_split).shares().isNegative()) qA["action"] = "Sell"; qA["investaccount"] = splitAcc.parent().name(); } include_me = m_config.includes(splitAcc); a_fullname = splitAcc.fullName(); a_memo = (*it_split).memo(); qA["price"] = xr.convert(MyMoneyMoney::precToDenom(KMyMoneyGlobalSettings::pricePrecision())).toString(); qA["account"] = splitAcc.name(); qA["accountid"] = splitAcc.id(); qA["topaccount"] = splitAcc.topParentName(); qA["institution"] = institution.isEmpty() ? i18n("No Institution") : file->institution(institution).name(); //FIXME-ALEX Is this useless? Isn't constructSplitsTable called only for cashflow type report? QString delimiter = ""; for (int i = 0; i < tagIdList.size(); i++) { qA["tag"] += delimiter + file->tag(tagIdList[i]).name().simplified(); delimiter = ','; } qA["payee"] = payee.isEmpty() ? i18n("[Empty Payee]") : file->payee(payee).name().simplified(); qA["reconciledate"] = (*it_split).reconcileDate().toString(Qt::ISODate); qA["reconcileflag"] = KMyMoneyUtils::reconcileStateToString((*it_split).reconcileFlag(), true); qA["number"] = (*it_split).number(); qA["memo"] = a_memo; qS["reconciledate"] = qA["reconciledate"]; qS["reconcileflag"] = qA["reconcileflag"]; qS["number"] = qA["number"]; qS["topcategory"] = splitAcc.topParentName(); // only include the configured accounts if (include_me) { // add the "summarized" split transaction // this is the sub-total of the split detail // convert to lowest fraction qA["value"] = ((*it_split).shares() * xr).convert(fraction).toString(); qA["rank"] = '0'; //fill in account information if (! splitAcc.isIncomeExpense() && it_split != myBegin) { qA["account"] = ((*it_split).shares().isNegative()) ? i18n("Transfer to %1", myBeginAcc.fullName()) : i18n("Transfer from %1", myBeginAcc.fullName()); } else if (it_split == myBegin) { //handle the main split if ((splits.count() > 2)) { //if it is the main split and has multiple splits, note that qA["account"] = i18n("[Split Transaction]"); } else { //fill the account name of the second split QList::const_iterator tempSplit = splits.begin(); //there are supposed to be only 2 splits if we ever get here if (tempSplit == myBegin && splits.count() > 1) ++tempSplit; //show the name of the category, or "transfer to/from" if it as an account ReportAccount tempSplitAcc = (*tempSplit).accountId(); if (! tempSplitAcc.isIncomeExpense()) { qA["account"] = ((*it_split).shares().isNegative()) ? i18n("Transfer to %1", tempSplitAcc.fullName()) : i18n("Transfer from %1", tempSplitAcc.fullName()); } else { qA["account"] = tempSplitAcc.fullName(); } } } else { //in any other case, fill in the account name of the main split qA["account"] = myBeginAcc.fullName(); } //category data is always the one of the split qA ["category"] = splitAcc.fullName(); qA ["topcategory"] = splitAcc.topParentName(); qA ["categorytype"] = KMyMoneyUtils::accountTypeToString(splitAcc.accountGroup()); m_rows += qA; // track accts that will need opening and closing balances accts.insert(splitAcc.id(), splitAcc); } ++it_split; // look for wrap-around if (it_split == splits.end()) it_split = splits.begin(); //check if there have been more passes than there are splits //this is to prevent infinite loops in cases of data inconsistency -- asoliverez ++pass; if (pass > splits.count()) break; } while (it_split != myBegin); if (loan_special_case) { m_rows += qA; } } // now run through our accts list and add opening and closing balances switch (m_config.rowType()) { case MyMoneyReport::Row::Account: case MyMoneyReport::Row::TopAccount: break; // case MyMoneyReport::eCategory: // case MyMoneyReport::eTopCategory: // case MyMoneyReport::ePayee: // case MyMoneyReport::eMonth: // case MyMoneyReport::eWeek: default: return; } QDate startDate, endDate; report.validDateRange(startDate, endDate); QString strStartDate = startDate.toString(Qt::ISODate); QString strEndDate = endDate.toString(Qt::ISODate); startDate = startDate.addDays(-1); QMap::const_iterator it_account, accts_end; for (it_account = accts.constBegin(); it_account != accts.constEnd(); ++it_account) { TableRow qA; ReportAccount account = (* it_account); //get fraction for account int fraction = account.currency().smallestAccountFraction(); //use base currency fraction if not initialized if (fraction == -1) fraction = file->baseCurrency().smallestAccountFraction(); QString institution = account.institutionId(); // use the institution of the parent for stock accounts if (account.isInvest()) institution = account.parent().institutionId(); MyMoneyMoney startBalance, endBalance, startPrice, endPrice; MyMoneyMoney startShares, endShares; //get price and convert currency if necessary if (m_config.isConvertCurrency()) { startPrice = (account.deepCurrencyPrice(startDate) * account.baseCurrencyPrice(startDate)).reduce(); endPrice = (account.deepCurrencyPrice(endDate) * account.baseCurrencyPrice(endDate)).reduce(); } else { startPrice = account.deepCurrencyPrice(startDate).reduce(); endPrice = account.deepCurrencyPrice(endDate).reduce(); } startShares = file->balance(account.id(), startDate); endShares = file->balance(account.id(), endDate); //get starting and ending balances startBalance = startShares * startPrice; endBalance = endShares * endPrice; //starting balance // don't show currency if we're converting or if it's not foreign qA["currency"] = (m_config.isConvertCurrency() || ! account.isForeignCurrency()) ? "" : account.currency().id(); qA["accountid"] = account.id(); qA["account"] = account.name(); qA["topaccount"] = account.topParentName(); qA["institution"] = institution.isEmpty() ? i18n("No Institution") : file->institution(institution).name(); qA["rank"] = "-2"; qA["price"] = startPrice.convert(MyMoneyMoney::precToDenom(KMyMoneyGlobalSettings::pricePrecision())).toString(); if (account.isInvest()) { qA["shares"] = startShares.toString(); } qA["postdate"] = strStartDate; qA["balance"] = startBalance.convert(fraction).toString(); qA["value"].clear(); qA["id"] = 'A'; m_rows += qA; //ending balance qA["price"] = endPrice.convert(MyMoneyMoney::precToDenom(KMyMoneyGlobalSettings::pricePrecision())).toString(); if (account.isInvest()) { qA["shares"] = endShares.toString(); } qA["postdate"] = strEndDate; qA["balance"] = endBalance.toString(); qA["id"] = 'Z'; m_rows += qA; } } } diff --git a/kmymoney/reports/querytable.h b/kmymoney/reports/querytable.h index fda571783..4df0c9bce 100644 --- a/kmymoney/reports/querytable.h +++ b/kmymoney/reports/querytable.h @@ -1,158 +1,71 @@ /*************************************************************************** querytable.h ------------------- begin : Fri Jul 23 2004 copyright : (C) 2004-2005 by Ace Jones - (C) 2007 Sascha Pfau ***************************************************************************/ -/**************************************************************************** - Contains code from the func_xirr and related methods of financial.cpp - - KOffice 1.6 by Sascha Pfau. Sascha agreed to relicense those methods under - GPLv2 or later. -*****************************************************************************/ - /*************************************************************************** * * * 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. * * * ***************************************************************************/ #ifndef QUERYTABLE_H #define QUERYTABLE_H // ---------------------------------------------------------------------------- // QT Includes #include #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes #include "mymoneyreport.h" #include "listtable.h" namespace reports { class ReportAccount; /** * Calculates a query of information about the transaction database. * * This is a middle-layer class, between the UI and the engine. The * MyMoneyReport class holds only the CONFIGURATION parameters. This * class actually does the work of retrieving the data from the engine * and formatting it for the user. * * @author Ace Jones * * @short **/ class QueryTable : public ListTable { public: QueryTable(const MyMoneyReport&); void init(); static QString toDateString(const QDate &date); protected: void constructAccountTable(); void constructTransactionTable(); void constructPerformanceRow(const ReportAccount& account, TableRow& result) const; void constructSplitsTable(); }; -// -// Cash Flow analysis tools for investment reports -// - -class CashFlowListItem -{ -public: - CashFlowListItem() {} - CashFlowListItem(const QDate& _date, const MyMoneyMoney& _value): m_date(_date), m_value(_value) {} - bool operator<(const CashFlowListItem& _second) const { - return m_date < _second.m_date; - } - bool operator<=(const CashFlowListItem& _second) const { - return m_date <= _second.m_date; - } - bool operator>(const CashFlowListItem& _second) const { - return m_date > _second.m_date; - } - const QDate& date() const { - return m_date; - } - const MyMoneyMoney& value() const { - return m_value; - } - MyMoneyMoney NPV(double _rate) const; - - static void setToday(const QDate& _today) { - m_sToday = _today; - } - const QDate& today() const { - return m_sToday; - } - -private: - QDate m_date; - MyMoneyMoney m_value; - - static QDate m_sToday; -}; - -class CashFlowList: public QList -{ -public: - CashFlowList() {} - MyMoneyMoney NPV(double rate) const; - double IRR() const; - MyMoneyMoney total() const; - void dumpDebug() const; - - /** - * Function: XIRR - * - * Compute the internal rate of return for a non-periodic series of cash flows. - * - * XIRR ( Values; Dates; [ Guess = 0.1 ] ) - **/ - double calculateXIRR() const; - -protected: - CashFlowListItem mostRecent() const; - -private: - /** - * helper: xirrResult - * - * args[0] = values - * args[1] = dates - **/ - double xirrResult(double& rate) const; - - /** - * - * helper: xirrResultDerive - * - * args[0] = values - * args[1] = dates - **/ - double xirrResultDerive(double& rate) const; -}; - } #endif // QUERYREPORT_H diff --git a/kmymoney/reports/querytabletest.cpp b/kmymoney/reports/querytabletest.cpp index 9a08728ed..66ca93109 100644 --- a/kmymoney/reports/querytabletest.cpp +++ b/kmymoney/reports/querytabletest.cpp @@ -1,807 +1,848 @@ /*************************************************************************** querytabletest.cpp ------------------- copyright : (C) 2002 by Thomas Baumgart email : ipwizard@users.sourceforge.net Ace Jones ***************************************************************************/ /*************************************************************************** * * * 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. * * * ***************************************************************************/ #include "querytabletest.h" #include #include #include #include #include #include #include #include #include #include #include +#include "cashflowlist.h" #include "reportstestcommon.h" #include "querytable.h" #include "mymoneyaccount.h" #include "mymoneysecurity.h" #include "mymoneyprice.h" #include "mymoneystoragedump.h" #include "mymoneyreport.h" #include "mymoneystatement.h" #include "mymoneystoragexml.h" using namespace reports; using namespace test; #define DEBUG_QUERYTABLETEST QTEST_KDEMAIN_CORE_WITH_COMPONENTNAME(QueryTableTest, "kmymoney") void QueryTableTest::writeTabletoHTML(reports::QueryTable &table, const QString &filename) { MyMoneyFileTransaction ft; MyMoneyFile::instance()->addReport(table.report()); ft.commit(); ::writeTabletoHTML(table, filename); } void QueryTableTest::saveKMyMoneyFile(const QString &filename) { QFile g(filename); g.open(QIODevice::WriteOnly); MyMoneyStorageXML xml; IMyMoneyStorageFormat& interface = xml; interface.writeFile(&g, dynamic_cast(MyMoneyFile::instance()->storage())); g.close(); } void QueryTableTest::init() { storage = new MyMoneySeqAccessMgr; storage->setFileFixVersion(storage->currentFixVersion()); file = MyMoneyFile::instance(); file->attachStorage(storage); MyMoneyFileTransaction ft; file->setValue("kmm-id", file->storageId()); file->addCurrency(MyMoneySecurity("CAD", "Canadian Dollar", "C$")); file->addCurrency(MyMoneySecurity("USD", "US Dollar", "$")); file->addCurrency(MyMoneySecurity("JPY", "Japanese Yen", QChar(0x00A5), 100, 1)); file->addCurrency(MyMoneySecurity("GBP", "British Pound", "#")); file->setBaseCurrency(file->currency("USD")); MyMoneyPayee payeeTest("Test Payee"); file->addPayee(payeeTest); MyMoneyPayee payeeTest2("Thomas Baumgart"); file->addPayee(payeeTest2); acAsset = (MyMoneyFile::instance()->asset().id()); acLiability = (MyMoneyFile::instance()->liability().id()); acExpense = (MyMoneyFile::instance()->expense().id()); acIncome = (MyMoneyFile::instance()->income().id()); acChecking = makeAccount(QString("Checking Account"), MyMoneyAccount::Checkings, moCheckingOpen, QDate(2004, 5, 15), acAsset); acCredit = makeAccount(QString("Credit Card"), MyMoneyAccount::CreditCard, moCreditOpen, QDate(2004, 7, 15), acLiability); acSolo = makeAccount(QString("Solo"), MyMoneyAccount::Expense, MyMoneyMoney(), QDate(2004, 1, 11), acExpense); acParent = makeAccount(QString("Parent"), MyMoneyAccount::Expense, MyMoneyMoney(), QDate(2004, 1, 11), acExpense); acChild = makeAccount(QString("Child"), MyMoneyAccount::Expense, MyMoneyMoney(), QDate(2004, 2, 11), acParent); acForeign = makeAccount(QString("Foreign"), MyMoneyAccount::Expense, MyMoneyMoney(), QDate(2004, 1, 11), acExpense); acTax = makeAccount(QString("Tax"), MyMoneyAccount::Expense, MyMoneyMoney(), QDate(2005, 1, 11), acExpense, "", true); MyMoneyInstitution i("Bank of the World", "", "", "", "", "", ""); file->addInstitution(i); inBank = i.id(); ft.commit(); } void QueryTableTest::cleanup() { file->detachStorage(storage); delete storage; } void QueryTableTest::testQueryBasics() { try { TransactionHelper t1q1(QDate(2004, 1, 1), MyMoneySplit::ActionWithdrawal, moSolo, acChecking, acSolo); TransactionHelper t2q1(QDate(2004, 2, 1), MyMoneySplit::ActionWithdrawal, moParent1, acCredit, acParent); TransactionHelper t3q1(QDate(2004, 3, 1), MyMoneySplit::ActionWithdrawal, moParent2, acCredit, acParent); TransactionHelper t4y1(QDate(2004, 11, 7), MyMoneySplit::ActionWithdrawal, moChild, acCredit, acChild); TransactionHelper t1q2(QDate(2004, 4, 1), MyMoneySplit::ActionWithdrawal, moSolo, acChecking, acSolo); TransactionHelper t2q2(QDate(2004, 5, 1), MyMoneySplit::ActionWithdrawal, moParent1, acCredit, acParent); TransactionHelper t3q2(QDate(2004, 6, 1), MyMoneySplit::ActionWithdrawal, moParent2, acCredit, acParent); TransactionHelper t4q2(QDate(2004, 11, 7), MyMoneySplit::ActionWithdrawal, moChild, acCredit, acChild); TransactionHelper t1y2(QDate(2005, 1, 1), MyMoneySplit::ActionWithdrawal, moSolo, acChecking, acSolo); TransactionHelper t2y2(QDate(2005, 5, 1), MyMoneySplit::ActionWithdrawal, moParent1, acCredit, acParent); TransactionHelper t3y2(QDate(2005, 9, 1), MyMoneySplit::ActionWithdrawal, moParent2, acCredit, acParent); TransactionHelper t4y2(QDate(2004, 11, 7), MyMoneySplit::ActionWithdrawal, moChild, acCredit, acChild); unsigned cols; MyMoneyReport filter; filter.setRowType(MyMoneyReport::Row::Category); cols = MyMoneyReport::QueryColumns::Number | MyMoneyReport::QueryColumns::Payee | MyMoneyReport::QueryColumns::Account; filter.setQueryColumns(static_cast(cols)); // filter.setName("Transactions by Category"); XMLandback(filter); QueryTable qtbl_1(filter); writeTabletoHTML(qtbl_1, "Transactions by Category.html"); QList rows = qtbl_1.rows(); QVERIFY(rows.count() == 12); QVERIFY(rows[0]["categorytype"] == "Expense"); QVERIFY(rows[0]["category"] == "Parent"); QVERIFY(rows[0]["postdate"] == "2004-02-01"); QVERIFY(rows[11]["categorytype"] == "Expense"); QVERIFY(rows[11]["category"] == "Solo"); QVERIFY(rows[11]["postdate"] == "2005-01-01"); QString html = qtbl_1.renderBody(); QVERIFY(searchHTML(html, i18nc("Total balance", "Total") + " Parent") == -(moParent1 + moParent2) * 3); QVERIFY(searchHTML(html, i18nc("Total balance", "Total") + " Parent: Child") == -(moChild) * 3); QVERIFY(searchHTML(html, i18nc("Total balance", "Total") + " Solo") == -(moSolo) * 3); QVERIFY(searchHTML(html, i18nc("Total balance", "Total") + " Expense") == -(moParent1 + moParent2 + moSolo + moChild) * 3); QVERIFY(searchHTML(html, i18nc("Grand total balance", "Grand Total")) == -(moParent1 + moParent2 + moSolo + moChild) * 3 + moCheckingOpen + moCreditOpen); filter.setRowType(MyMoneyReport::Row::TopCategory); cols = MyMoneyReport::QueryColumns::Number | MyMoneyReport::QueryColumns::Payee | MyMoneyReport::QueryColumns::Account; filter.setQueryColumns(static_cast(cols)); // filter.setName("Transactions by Top Category"); XMLandback(filter); QueryTable qtbl_2(filter); writeTabletoHTML(qtbl_2, "Transactions by Top Category.html"); rows = qtbl_2.rows(); QVERIFY(rows.count() == 12); QVERIFY(rows[0]["categorytype"] == "Expense"); QVERIFY(rows[0]["topcategory"] == "Parent"); QVERIFY(rows[0]["postdate"] == "2004-02-01"); QVERIFY(rows[8]["categorytype"] == "Expense"); QVERIFY(rows[8]["topcategory"] == "Parent"); QVERIFY(rows[8]["postdate"] == "2005-09-01"); QVERIFY(rows[11]["categorytype"] == "Expense"); QVERIFY(rows[11]["topcategory"] == "Solo"); QVERIFY(rows[11]["postdate"] == "2005-01-01"); html = qtbl_2.renderBody(); QVERIFY(searchHTML(html, i18nc("Total balance", "Total") + " Parent") == -(moParent1 + moParent2 + moChild) * 3); QVERIFY(searchHTML(html, i18nc("Total balance", "Total") + " Solo") == -(moSolo) * 3); QVERIFY(searchHTML(html, i18nc("Total balance", "Total") + " Expense") == -(moParent1 + moParent2 + moSolo + moChild) * 3); QVERIFY(searchHTML(html, i18nc("Grand total balance", "Grand Total")) == -(moParent1 + moParent2 + moSolo + moChild) * 3 + moCheckingOpen + moCreditOpen); filter.setRowType(MyMoneyReport::Row::Account); filter.setName("Transactions by Account"); cols = MyMoneyReport::QueryColumns::Number | MyMoneyReport::QueryColumns::Payee | MyMoneyReport::QueryColumns::Category; filter.setQueryColumns(static_cast(cols)); // XMLandback(filter); QueryTable qtbl_3(filter); writeTabletoHTML(qtbl_3, "Transactions by Account.html"); rows = qtbl_3.rows(); #if 1 QVERIFY(rows.count() == 16); QVERIFY(rows[1]["account"] == "Checking Account"); QVERIFY(rows[1]["category"] == "Solo"); QVERIFY(rows[1]["postdate"] == "2004-01-01"); QVERIFY(rows[14]["account"] == "Credit Card"); QVERIFY(rows[14]["category"] == "Parent"); QVERIFY(rows[14]["postdate"] == "2005-09-01"); #else QVERIFY(rows.count() == 12); QVERIFY(rows[0]["account"] == "Checking Account"); QVERIFY(rows[0]["category"] == "Solo"); QVERIFY(rows[0]["postdate"] == "2004-01-01"); QVERIFY(rows[11]["account"] == "Credit Card"); QVERIFY(rows[11]["category"] == "Parent"); QVERIFY(rows[11]["postdate"] == "2005-09-01"); #endif html = qtbl_3.renderBody(); QVERIFY(searchHTML(html, i18nc("Total balance for checking account", "Total") + " Checking Account") == -(moSolo) * 3 + moCheckingOpen); QVERIFY(searchHTML(html, i18nc("Total balance for credit card", "Total") + " Credit Card") == -(moParent1 + moParent2 + moChild) * 3 + moCreditOpen); QVERIFY(searchHTML(html, i18nc("Grand total balance", "Grand Total")) == -(moParent1 + moParent2 + moSolo + moChild) * 3 + moCheckingOpen + moCreditOpen); filter.setRowType(MyMoneyReport::Row::Payee); filter.setName("Transactions by Payee"); cols = MyMoneyReport::QueryColumns::Number | MyMoneyReport::QueryColumns::Memo | MyMoneyReport::QueryColumns::Category; filter.setQueryColumns(static_cast(cols)); // XMLandback(filter); QueryTable qtbl_4(filter); writeTabletoHTML(qtbl_4, "Transactions by Payee.html"); rows = qtbl_4.rows(); QVERIFY(rows.count() == 12); QVERIFY(rows[0]["payee"] == "Test Payee"); QVERIFY(rows[0]["category"] == "Solo"); QVERIFY(rows[0]["postdate"] == "2004-01-01"); QVERIFY(rows[8]["payee"] == "Test Payee"); QVERIFY(rows[8]["category"] == "Parent: Child"); QVERIFY(rows[8]["postdate"] == "2004-11-07"); QVERIFY(rows[11]["payee"] == "Test Payee"); QVERIFY(rows[11]["category"] == "Parent"); QVERIFY(rows[11]["postdate"] == "2005-09-01"); html = qtbl_4.renderBody(); QVERIFY(searchHTML(html, i18nc("Total balance", "Total") + " Test Payee") == -(moParent1 + moParent2 + moSolo + moChild) * 3); QVERIFY(searchHTML(html, i18nc("Grand total balance", "Grand Total")) == -(moParent1 + moParent2 + moSolo + moChild) * 3 + moCheckingOpen + moCreditOpen); filter.setRowType(MyMoneyReport::Row::Month); filter.setName("Transactions by Month"); cols = MyMoneyReport::QueryColumns::Number | MyMoneyReport::QueryColumns::Payee | MyMoneyReport::QueryColumns::Category; filter.setQueryColumns(static_cast(cols)); // XMLandback(filter); QueryTable qtbl_5(filter); writeTabletoHTML(qtbl_5, "Transactions by Month.html"); rows = qtbl_5.rows(); QVERIFY(rows.count() == 12); QVERIFY(rows[0]["payee"] == "Test Payee"); QVERIFY(rows[0]["category"] == "Solo"); QVERIFY(rows[0]["postdate"] == "2004-01-01"); QVERIFY(rows[8]["payee"] == "Test Payee"); QVERIFY(rows[8]["category"] == "Parent: Child"); QVERIFY(rows[8]["postdate"] == "2004-11-07"); QVERIFY(rows[11]["payee"] == "Test Payee"); QVERIFY(rows[11]["category"] == "Parent"); QVERIFY(rows[11]["postdate"] == "2005-09-01"); html = qtbl_5.renderBody(); QVERIFY(searchHTML(html, i18nc("Total balance", "Total") + " Month of " + QueryTable::toDateString(QDate(2004,1,1))) == -moSolo); QVERIFY(searchHTML(html, i18nc("Total balance", "Total") + " Month of " + QueryTable::toDateString(QDate(2004,11,1))) == -(moChild) * 3); QVERIFY(searchHTML(html, i18nc("Total balance", "Total") + " Month of " + QueryTable::toDateString(QDate(2004,5,1))) == -moParent1 + moCheckingOpen); QVERIFY(searchHTML(html, i18nc("Grand total balance", "Grand Total")) == -(moParent1 + moParent2 + moSolo + moChild) * 3 + moCheckingOpen + moCreditOpen); filter.setRowType(MyMoneyReport::Row::Week); filter.setName("Transactions by Week"); cols = MyMoneyReport::QueryColumns::Number | MyMoneyReport::QueryColumns::Payee | MyMoneyReport::QueryColumns::Category; filter.setQueryColumns(static_cast(cols)); // XMLandback(filter); QueryTable qtbl_6(filter); writeTabletoHTML(qtbl_6, "Transactions by Week.html"); rows = qtbl_6.rows(); QVERIFY(rows.count() == 12); QVERIFY(rows[0]["payee"] == "Test Payee"); QVERIFY(rows[0]["category"] == "Solo"); QVERIFY(rows[0]["postdate"] == "2004-01-01"); QVERIFY(rows[11]["payee"] == "Test Payee"); QVERIFY(rows[11]["category"] == "Parent"); QVERIFY(rows[11]["postdate"] == "2005-09-01"); html = qtbl_6.renderBody(); QVERIFY(searchHTML(html, i18nc("Total balance", "Total") + " Week of " + QueryTable::toDateString(QDate(2003, 12, 29))) == -moSolo); QVERIFY(searchHTML(html, i18nc("Total balance", "Total") + " Week of " + QueryTable::toDateString(QDate(2004, 11, 1))) == -(moChild) * 3); QVERIFY(searchHTML(html, i18nc("Total balance", "Total") + " Week of " + QueryTable::toDateString(QDate(2005, 8, 29))) == -moParent2); QVERIFY(searchHTML(html, i18nc("Grand total balance", "Grand Total")) == -(moParent1 + moParent2 + moSolo + moChild) * 3 + moCheckingOpen + moCreditOpen); } catch (const MyMoneyException &e) { QFAIL(qPrintable(e.what())); } // Test querytable::TableRow::operator> and operator== QueryTable::TableRow low; low["first"] = 'A'; low["second"] = 'B'; low["third"] = 'C'; QueryTable::TableRow high; high["first"] = 'A'; high["second"] = 'C'; high["third"] = 'B'; QueryTable::TableRow::setSortCriteria("first,second,third"); QVERIFY(low < high); QVERIFY(low <= high); QVERIFY(high > low); QVERIFY(high <= high); QVERIFY(high == high); } void QueryTableTest::testCashFlowAnalysis() { // - // Test IRR calculations + // Test XIRR calculations // CashFlowList list; list += CashFlowListItem(QDate(2004, 5, 3), MyMoneyMoney(1000.0)); list += CashFlowListItem(QDate(2004, 5, 20), MyMoneyMoney(59.0)); list += CashFlowListItem(QDate(2004, 6, 3), MyMoneyMoney(14.0)); list += CashFlowListItem(QDate(2004, 6, 24), MyMoneyMoney(92.0)); list += CashFlowListItem(QDate(2004, 7, 6), MyMoneyMoney(63.0)); list += CashFlowListItem(QDate(2004, 7, 25), MyMoneyMoney(15.0)); list += CashFlowListItem(QDate(2004, 8, 5), MyMoneyMoney(92.0)); list += CashFlowListItem(QDate(2004, 9, 2), MyMoneyMoney(18.0)); list += CashFlowListItem(QDate(2004, 9, 21), MyMoneyMoney(5.0)); list += CashFlowListItem(QDate(2004, 10, 16), MyMoneyMoney(-2037.0)); - MyMoneyMoney IRR(list.IRR(), 1000); - - QVERIFY(IRR == MyMoneyMoney(1676, 1000)); + MyMoneyMoney XIRR(list.XIRR(), 1000); + QVERIFY(XIRR == MyMoneyMoney(1676, 1000)); list.pop_back(); list += CashFlowListItem(QDate(2004, 10, 16), MyMoneyMoney(-1358.0)); - IRR = MyMoneyMoney(list.IRR(), 1000); + XIRR = MyMoneyMoney(list.XIRR(), 1000); + QVERIFY(XIRR.isZero()); + + // two entries + list.clear(); + list += CashFlowListItem(QDate(2005, 9, 22), MyMoneyMoney(-3472.57)); + list += CashFlowListItem(QDate(2009, 3, 18), MyMoneyMoney(6051.36)); + + XIRR = MyMoneyMoney(list.XIRR(), 1000); + QCOMPARE(XIRR.toDouble(), MyMoneyMoney(173, 1000).toDouble()); + + // check ignoring zero values + list += CashFlowListItem(QDate(1998, 11, 1), MyMoneyMoney(0.0)); + list += CashFlowListItem(QDate(2017, 8, 11), MyMoneyMoney(0.0)); + + XIRR = MyMoneyMoney(list.XIRR(), 1000); + QCOMPARE(XIRR.toDouble(), MyMoneyMoney(173, 1000).toDouble()); + + list.pop_back(); + // former implementation crashed + list += CashFlowListItem(QDate(2014, 8, 11), MyMoneyMoney(0.0)); + + XIRR = MyMoneyMoney(list.XIRR(), 1000); + QCOMPARE(XIRR.toDouble(), MyMoneyMoney(173, 1000).toDouble()); - QVERIFY(IRR.isZero()); + // different ordering + list.clear(); + list += CashFlowListItem(QDate(2004, 3, 18), MyMoneyMoney(6051.36)); + list += CashFlowListItem(QDate(2005, 9, 22), MyMoneyMoney(-3472.57)); + + XIRR = MyMoneyMoney(list.XIRR(), 1000); + QCOMPARE(XIRR.toDouble(), MyMoneyMoney(-307, 1000).toDouble()); + + + // not enough entries + list.pop_back(); + + bool result = false; + try { + XIRR = MyMoneyMoney(list.XIRR(), 1000); + } catch (MyMoneyException &e) { + result = true; + } + QVERIFY(result); } void QueryTableTest::testAccountQuery() { try { QString htmlcontext = QString("\n\n%1\n\n"); // // No transactions, opening balances only // MyMoneyReport filter; filter.setRowType(MyMoneyReport::Row::Institution); filter.setName("Accounts by Institution (No transactions)"); XMLandback(filter); QueryTable qtbl_1(filter); writeTabletoHTML(qtbl_1, "Accounts by Institution (No transactions).html"); QList rows = qtbl_1.rows(); QVERIFY(rows.count() == 2); QVERIFY(rows[0]["account"] == "Checking Account"); QVERIFY(MyMoneyMoney(rows[0]["value"]) == moCheckingOpen); QVERIFY(rows[0]["equitytype"].isEmpty()); QVERIFY(rows[1]["account"] == "Credit Card"); QVERIFY(MyMoneyMoney(rows[1]["value"]) == moCreditOpen); QVERIFY(rows[1]["equitytype"].isEmpty()); QString html = qtbl_1.renderBody(); QVERIFY(searchHTML(html, i18nc("Total balance", "Total") + " None") == moCheckingOpen + moCreditOpen); QVERIFY(searchHTML(html, i18nc("Grand total balance", "Grand Total")) == moCheckingOpen + moCreditOpen); // // Adding in transactions // TransactionHelper t1q1(QDate(2004, 1, 1), MyMoneySplit::ActionWithdrawal, moSolo, acChecking, acSolo); TransactionHelper t2q1(QDate(2004, 2, 1), MyMoneySplit::ActionWithdrawal, moParent1, acCredit, acParent); TransactionHelper t3q1(QDate(2004, 3, 1), MyMoneySplit::ActionWithdrawal, moParent2, acCredit, acParent); TransactionHelper t4y1(QDate(2004, 11, 7), MyMoneySplit::ActionWithdrawal, moChild, acCredit, acChild); TransactionHelper t1q2(QDate(2004, 4, 1), MyMoneySplit::ActionWithdrawal, moSolo, acChecking, acSolo); TransactionHelper t2q2(QDate(2004, 5, 1), MyMoneySplit::ActionWithdrawal, moParent1, acCredit, acParent); TransactionHelper t3q2(QDate(2004, 6, 1), MyMoneySplit::ActionWithdrawal, moParent2, acCredit, acParent); TransactionHelper t4q2(QDate(2004, 11, 7), MyMoneySplit::ActionWithdrawal, moChild, acCredit, acChild); TransactionHelper t1y2(QDate(2005, 1, 1), MyMoneySplit::ActionWithdrawal, moSolo, acChecking, acSolo); TransactionHelper t2y2(QDate(2005, 5, 1), MyMoneySplit::ActionWithdrawal, moParent1, acCredit, acParent); TransactionHelper t3y2(QDate(2005, 9, 1), MyMoneySplit::ActionWithdrawal, moParent2, acCredit, acParent); TransactionHelper t4y2(QDate(2004, 11, 7), MyMoneySplit::ActionWithdrawal, moChild, acCredit, acChild); filter.setRowType(MyMoneyReport::Row::Institution); filter.setName("Accounts by Institution (With Transactions)"); XMLandback(filter); QueryTable qtbl_2(filter); rows = qtbl_2.rows(); QVERIFY(rows.count() == 2); QVERIFY(rows[0]["account"] == "Checking Account"); QVERIFY(MyMoneyMoney(rows[0]["value"]) == (moCheckingOpen - moSolo*3)); QVERIFY(rows[1]["account"] == "Credit Card"); QVERIFY(MyMoneyMoney(rows[1]["value"]) == (moCreditOpen - (moParent1 + moParent2 + moChild) * 3)); html = qtbl_2.renderBody(); QVERIFY(searchHTML(html, i18n("Grand Total")) == moCheckingOpen + moCreditOpen - (moParent1 + moParent2 + moSolo + moChild) * 3); // // Account TYPES // filter.setRowType(MyMoneyReport::Row::AccountType); filter.setName("Accounts by Type"); XMLandback(filter); QueryTable qtbl_3(filter); rows = qtbl_3.rows(); QVERIFY(rows.count() == 2); QVERIFY(rows[0]["account"] == "Checking Account"); QVERIFY(MyMoneyMoney(rows[0]["value"]) == (moCheckingOpen - moSolo*3)); QVERIFY(rows[1]["account"] == "Credit Card"); QVERIFY(MyMoneyMoney(rows[1]["value"]) == (moCreditOpen - (moParent1 + moParent2 + moChild) * 3)); html = qtbl_3.renderBody(); QVERIFY(searchHTML(html, i18nc("Total balance", "Total") + ' ' + i18n("Checking")) == moCheckingOpen - moSolo*3); QVERIFY(searchHTML(html, i18nc("Total balance", "Total") + ' ' + i18n("Credit Card")) == moCreditOpen - (moParent1 + moParent2 + moChild) * 3); QVERIFY(searchHTML(html, i18nc("Grand total balance", "Grand Total")) == moCheckingOpen + moCreditOpen - (moParent1 + moParent2 + moSolo + moChild) * 3); } catch (const MyMoneyException &e) { QFAIL(qPrintable(e.what())); } } void QueryTableTest::testInvestment() { try { // Equities eqStock1 = makeEquity("Stock1", "STK1"); eqStock2 = makeEquity("Stock2", "STK2"); // Accounts acInvestment = makeAccount("Investment", MyMoneyAccount::Investment, moZero, QDate(2004, 1, 1), acAsset); acStock1 = makeAccount("Stock 1", MyMoneyAccount::Stock, moZero, QDate(2004, 1, 1), acInvestment, eqStock1); acStock2 = makeAccount("Stock 2", MyMoneyAccount::Stock, moZero, QDate(2004, 1, 1), acInvestment, eqStock2); acDividends = makeAccount("Dividends", MyMoneyAccount::Income, moZero, QDate(2004, 1, 1), acIncome); acInterest = makeAccount("Interest", MyMoneyAccount::Income, moZero, QDate(2004, 1, 1), acIncome); // Transactions // Date Action Shares Price Stock Asset Income InvTransactionHelper s1b1(QDate(2004, 2, 1), MyMoneySplit::ActionBuyShares, MyMoneyMoney(1000.00), MyMoneyMoney(100.00), acStock1, acChecking, QString()); InvTransactionHelper s1b2(QDate(2004, 3, 1), MyMoneySplit::ActionBuyShares, MyMoneyMoney(1000.00), MyMoneyMoney(110.00), acStock1, acChecking, QString()); InvTransactionHelper s1s1(QDate(2004, 4, 1), MyMoneySplit::ActionBuyShares, MyMoneyMoney(-200.00), MyMoneyMoney(120.00), acStock1, acChecking, QString()); InvTransactionHelper s1s2(QDate(2004, 5, 1), MyMoneySplit::ActionBuyShares, MyMoneyMoney(-200.00), MyMoneyMoney(100.00), acStock1, acChecking, QString()); InvTransactionHelper s1r1(QDate(2004, 6, 1), MyMoneySplit::ActionReinvestDividend, MyMoneyMoney(50.00), MyMoneyMoney(100.00), acStock1, QString(), acDividends); InvTransactionHelper s1r2(QDate(2004, 7, 1), MyMoneySplit::ActionReinvestDividend, MyMoneyMoney(50.00), MyMoneyMoney(80.00), acStock1, QString(), acDividends); InvTransactionHelper s1c1(QDate(2004, 8, 1), MyMoneySplit::ActionDividend, MyMoneyMoney(10.00), MyMoneyMoney(100.00), acStock1, acChecking, acDividends); InvTransactionHelper s1c2(QDate(2004, 9, 1), MyMoneySplit::ActionDividend, MyMoneyMoney(10.00), MyMoneyMoney(120.00), acStock1, acChecking, acDividends); InvTransactionHelper s1y1(QDate(2004, 9, 15), MyMoneySplit::ActionYield, MyMoneyMoney(10.00), MyMoneyMoney(110.00), acStock1, acChecking, acInterest); makeEquityPrice(eqStock1, QDate(2004, 10, 1), MyMoneyMoney(100.00)); // // Investment Transactions Report // MyMoneyReport invtran_r( MyMoneyReport::Row::TopAccount, MyMoneyReport::QueryColumns::Action | MyMoneyReport::QueryColumns::Shares | MyMoneyReport::QueryColumns::Price, MyMoneyTransactionFilter::userDefined, MyMoneyReport::DetailLevel::All, i18n("Investment Transactions"), i18n("Test Report") ); invtran_r.setDateFilter(QDate(2004, 1, 1), QDate(2004, 12, 31)); invtran_r.setInvestmentsOnly(true); XMLandback(invtran_r); QueryTable invtran(invtran_r); #if 1 writeTabletoHTML(invtran, "investment_transactions_test.html"); QList rows = invtran.rows(); QVERIFY(rows.count() == 17); QVERIFY(MyMoneyMoney(rows[1]["value"]) == MyMoneyMoney(100000.00)); QVERIFY(MyMoneyMoney(rows[2]["value"]) == MyMoneyMoney(110000.00)); QVERIFY(MyMoneyMoney(rows[3]["value"]) == MyMoneyMoney(-24000.00)); QVERIFY(MyMoneyMoney(rows[4]["value"]) == MyMoneyMoney(-20000.00)); QVERIFY(MyMoneyMoney(rows[5]["value"]) == MyMoneyMoney(5000.00)); QVERIFY(MyMoneyMoney(rows[6]["value"]) == MyMoneyMoney(4000.00)); // need to fix these... fundamentally different from the original test //QVERIFY(MyMoneyMoney(invtran.m_rows[8]["value"])==MyMoneyMoney( -1000.00)); //QVERIFY(MyMoneyMoney(invtran.m_rows[11]["value"])==MyMoneyMoney( -1200.00)); //QVERIFY(MyMoneyMoney(invtran.m_rows[14]["value"])==MyMoneyMoney( -1100.00)); QVERIFY(MyMoneyMoney(rows[1]["price"]) == MyMoneyMoney(100.00)); QVERIFY(MyMoneyMoney(rows[3]["price"]) == MyMoneyMoney(120.00)); QVERIFY(MyMoneyMoney(rows[5]["price"]) == MyMoneyMoney(100.00)); QVERIFY(MyMoneyMoney(rows[7]["price"]) == MyMoneyMoney(100.00)); QVERIFY(MyMoneyMoney(rows[10]["price"]) == MyMoneyMoney(120.00)); QVERIFY(MyMoneyMoney(rows[2]["shares"]) == MyMoneyMoney(1000.00)); QVERIFY(MyMoneyMoney(rows[4]["shares"]) == MyMoneyMoney(-200.00)); QVERIFY(MyMoneyMoney(rows[6]["shares"]) == MyMoneyMoney(50.00)); QVERIFY(MyMoneyMoney(rows[8]["shares"]) == MyMoneyMoney(0.00)); QVERIFY(MyMoneyMoney(rows[11]["shares"]) == MyMoneyMoney(0.00)); QVERIFY(rows[1]["action"] == "Buy"); QVERIFY(rows[3]["action"] == "Sell"); QVERIFY(rows[5]["action"] == "Reinvest"); QVERIFY(rows[7]["action"] == "Dividend"); QVERIFY(rows[13]["action"] == "Yield"); #else QVERIFY(rows.count() == 9); QVERIFY(MyMoneyMoney(rows[0]["value"]) == MyMoneyMoney(100000.00)); QVERIFY(MyMoneyMoney(rows[1]["value"]) == MyMoneyMoney(110000.00)); QVERIFY(MyMoneyMoney(rows[2]["value"]) == MyMoneyMoney(-24000.00)); QVERIFY(MyMoneyMoney(rows[3]["value"]) == MyMoneyMoney(-20000.00)); QVERIFY(MyMoneyMoney(rows[4]["value"]) == MyMoneyMoney(5000.00)); QVERIFY(MyMoneyMoney(rows[5]["value"]) == MyMoneyMoney(4000.00)); QVERIFY(MyMoneyMoney(rows[6]["value"]) == MyMoneyMoney(-1000.00)); QVERIFY(MyMoneyMoney(rows[7]["value"]) == MyMoneyMoney(-1200.00)); QVERIFY(MyMoneyMoney(rows[8]["value"]) == MyMoneyMoney(-1100.00)); QVERIFY(MyMoneyMoney(rows[0]["price"]) == MyMoneyMoney(100.00)); QVERIFY(MyMoneyMoney(rows[2]["price"]) == MyMoneyMoney(120.00)); QVERIFY(MyMoneyMoney(rows[4]["price"]) == MyMoneyMoney(100.00)); QVERIFY(MyMoneyMoney(rows[6]["price"]) == MyMoneyMoney(0.00)); QVERIFY(MyMoneyMoney(rows[8]["price"]) == MyMoneyMoney(0.00)); QVERIFY(MyMoneyMoney(rows[1]["shares"]) == MyMoneyMoney(1000.00)); QVERIFY(MyMoneyMoney(rows[3]["shares"]) == MyMoneyMoney(-200.00)); QVERIFY(MyMoneyMoney(rows[5]["shares"]) == MyMoneyMoney(50.00)); QVERIFY(MyMoneyMoney(rows[7]["shares"]) == MyMoneyMoney(0.00)); QVERIFY(MyMoneyMoney(rows[8]["shares"]) == MyMoneyMoney(0.00)); QVERIFY(rows[0]["action"] == "Buy"); QVERIFY(rows[2]["action"] == "Sell"); QVERIFY(rows[4]["action"] == "Reinvest"); QVERIFY(rows[6]["action"] == "Dividend"); QVERIFY(rows[8]["action"] == "Yield"); #endif QString html = invtran.renderBody(); #if 1 // i think this is the correct amount. different treatment of dividend and yield QVERIFY(searchHTML(html, i18n("Total Stock 1")) == MyMoneyMoney(175000.00)); QVERIFY(searchHTML(html, i18n("Grand Total")) == MyMoneyMoney(175000.00)); #else QVERIFY(searchHTML(html, i18n("Total Stock 1")) == MyMoneyMoney(171700.00)); QVERIFY(searchHTML(html, i18n("Grand Total")) == MyMoneyMoney(171700.00)); #endif // // Investment Performance Report // MyMoneyReport invhold_r( MyMoneyReport::Row::AccountByTopAccount, MyMoneyReport::QueryColumns::Performance, MyMoneyTransactionFilter::userDefined, MyMoneyReport::DetailLevel::All, i18n("Investment Performance by Account"), i18n("Test Report") ); invhold_r.setDateFilter(QDate(2004, 1, 1), QDate(2004, 10, 1)); invhold_r.setInvestmentsOnly(true); XMLandback(invhold_r); QueryTable invhold(invhold_r); writeTabletoHTML(invhold, "Investment Performance by Account.html"); rows = invhold.rows(); QVERIFY(rows.count() == 1); QVERIFY(MyMoneyMoney(rows[0]["return"]) == MyMoneyMoney("669/10000")); QVERIFY(MyMoneyMoney(rows[0]["buys"]) == MyMoneyMoney(210000.00)); QVERIFY(MyMoneyMoney(rows[0]["sells"]) == MyMoneyMoney(-44000.00)); QVERIFY(MyMoneyMoney(rows[0]["reinvestincome"]) == MyMoneyMoney(9000.00)); QVERIFY(MyMoneyMoney(rows[0]["cashincome"]) == MyMoneyMoney(3300.00)); QVERIFY(MyMoneyMoney(rows[0]["shares"]) == MyMoneyMoney(1700.00)); QVERIFY(MyMoneyMoney(rows[0]["price"]) == MyMoneyMoney(100.00)); html = invhold.renderBody(); QVERIFY(searchHTML(html, i18n("Grand Total")) == MyMoneyMoney(170000.00)); #ifdef DEBUG_QUERYTABLETEST saveKMyMoneyFile("investmentkmy.xml"); invtran.dump("invtran.html", "%1"); invhold.dump("invhold.html", "%1"); #endif } catch (const MyMoneyException &e) { QFAIL(qPrintable(e.what())); } } //this is to prevent me from making mistakes again when modifying balances - asoliverez //this case tests only the opening and ending balance of the accounts void QueryTableTest::testBalanceColumn() { try { TransactionHelper t1q1(QDate(2004, 1, 1), MyMoneySplit::ActionWithdrawal, moSolo, acChecking, acSolo); TransactionHelper t2q1(QDate(2004, 2, 1), MyMoneySplit::ActionWithdrawal, moParent1, acCredit, acParent); TransactionHelper t3q1(QDate(2004, 3, 1), MyMoneySplit::ActionWithdrawal, moParent2, acCredit, acParent); TransactionHelper t4y1(QDate(2004, 11, 7), MyMoneySplit::ActionWithdrawal, moChild, acCredit, acChild); TransactionHelper t1q2(QDate(2004, 4, 1), MyMoneySplit::ActionWithdrawal, moSolo, acChecking, acSolo); TransactionHelper t2q2(QDate(2004, 5, 1), MyMoneySplit::ActionWithdrawal, moParent1, acCredit, acParent); TransactionHelper t3q2(QDate(2004, 6, 1), MyMoneySplit::ActionWithdrawal, moParent2, acCredit, acParent); TransactionHelper t4q2(QDate(2004, 11, 7), MyMoneySplit::ActionWithdrawal, moChild, acCredit, acChild); TransactionHelper t1y2(QDate(2005, 1, 1), MyMoneySplit::ActionWithdrawal, moSolo, acChecking, acSolo); TransactionHelper t2y2(QDate(2005, 5, 1), MyMoneySplit::ActionWithdrawal, moParent1, acCredit, acParent); TransactionHelper t3y2(QDate(2005, 9, 1), MyMoneySplit::ActionWithdrawal, moParent2, acCredit, acParent); TransactionHelper t4y2(QDate(2004, 11, 7), MyMoneySplit::ActionWithdrawal, moChild, acCredit, acChild); unsigned cols; MyMoneyReport filter; filter.setRowType(MyMoneyReport::Row::Account); filter.setName("Transactions by Account"); cols = MyMoneyReport::QueryColumns::Number | MyMoneyReport::QueryColumns::Payee | MyMoneyReport::QueryColumns::Category | MyMoneyReport::QueryColumns::Balance; filter.setQueryColumns(static_cast(cols)); // XMLandback(filter); QueryTable qtbl_3(filter); writeTabletoHTML(qtbl_3, "Transactions by Account.html"); QString html = qtbl_3.renderBody(); QList rows = qtbl_3.rows(); QVERIFY(rows.count() == 16); //this is to make sure that the dates of closing and opening balances and the balance numbers are ok QString openingDate = KGlobal::locale()->formatDate(QDate(2004, 1, 1), KLocale::ShortDate); QString closingDate = KGlobal::locale()->formatDate(QDate(2005, 9, 1), KLocale::ShortDate); QVERIFY(html.indexOf(openingDate + "" + i18n("Opening Balance")) > 0); QVERIFY(html.indexOf(closingDate + "" + i18n("Closing Balance") + " -702.36") > 0); QVERIFY(html.indexOf(closingDate + "" + i18n("Closing Balance") + " -705.69") > 0); } catch (const MyMoneyException &e) { QFAIL(qPrintable(e.what())); } } void QueryTableTest::testBalanceColumnWithMultipleCurrencies() { try { MyMoneyMoney moJpyOpening(0.0, 1); MyMoneyMoney moJpyPrice(0.010, 100); MyMoneyMoney moJpyPrice2(0.011, 100); MyMoneyMoney moJpyPrice3(0.024, 100); MyMoneyMoney moTransaction(100, 1); MyMoneyMoney moJpyTransaction(100, 1); QString acJpyChecking = makeAccount(QString("Japanese Checking"), MyMoneyAccount::Checkings, moJpyOpening, QDate(2003, 11, 15), acAsset, "JPY"); makePrice("JPY", QDate(2004, 1, 1), MyMoneyMoney(moJpyPrice)); makePrice("JPY", QDate(2004, 5, 1), MyMoneyMoney(moJpyPrice2)); makePrice("JPY", QDate(2004, 6, 30), MyMoneyMoney(moJpyPrice3)); QDate openingDate(2004, 2, 20); QDate intermediateDate(2004, 5, 20); QDate closingDate(2004, 7, 20); TransactionHelper t1(openingDate, MyMoneySplit::ActionTransfer, MyMoneyMoney(moJpyTransaction), acJpyChecking, acChecking, "JPY"); TransactionHelper t4(openingDate, MyMoneySplit::ActionDeposit, MyMoneyMoney(moTransaction), acCredit, acChecking); TransactionHelper t2(intermediateDate, MyMoneySplit::ActionTransfer, MyMoneyMoney(moJpyTransaction), acJpyChecking, acChecking, "JPY"); TransactionHelper t5(intermediateDate, MyMoneySplit::ActionDeposit, MyMoneyMoney(moTransaction), acCredit, acChecking); TransactionHelper t3(closingDate, MyMoneySplit::ActionTransfer, MyMoneyMoney(moJpyTransaction), acJpyChecking, acChecking, "JPY"); TransactionHelper t6(closingDate, MyMoneySplit::ActionDeposit, MyMoneyMoney(moTransaction), acCredit, acChecking); // test that an income/expense transaction that involves a currency exchange is properly reported TransactionHelper t7(intermediateDate, MyMoneySplit::ActionWithdrawal, MyMoneyMoney(moJpyTransaction), acJpyChecking, acSolo, "JPY"); unsigned cols; MyMoneyReport filter; filter.setRowType(MyMoneyReport::Row::Account); filter.setName("Transactions by Account"); cols = MyMoneyReport::QueryColumns::Number | MyMoneyReport::QueryColumns::Payee | MyMoneyReport::QueryColumns::Category | MyMoneyReport::QueryColumns::Balance; filter.setQueryColumns(static_cast(cols)); // don't convert values to the default currency filter.setConvertCurrency(false); XMLandback(filter); QueryTable qtbl_3(filter); writeTabletoHTML(qtbl_3, "Transactions by Account (multiple currencies).html"); QString html = qtbl_3.renderBody(); QList rows = qtbl_3.rows(); QVERIFY(rows.count() == 19); //this is to make sure that the dates of closing and opening balances and the balance numbers are ok QString openingDateString = KGlobal::locale()->formatDate(openingDate, KLocale::ShortDate); QString intermediateDateString = KGlobal::locale()->formatDate(intermediateDate, KLocale::ShortDate); QString closingDateString = KGlobal::locale()->formatDate(closingDate, KLocale::ShortDate); // check the opening and closing balances QVERIFY(html.indexOf(openingDateString + "" + i18n("Opening Balance") + " 0.00") > 0); QVERIFY(html.indexOf(closingDateString + "" + i18n("Closing Balance") + " 304.00") > 0); QVERIFY(html.indexOf(closingDateString + "" + i18n("Closing Balance") + " -300.00") > 0); QVERIFY(html.indexOf(closingDateString + "" + i18n("Closing Balance") + "JPY -400.00") > 0); // after a transfer of 100 JPY the balance should be 1.00 - price is 0.010 (precision of 2) QVERIFY(html.indexOf("" + openingDateString + "Test PayeeTransfer from Japanese Checking 1.00 1.00") > 0); // after a transfer of 100 the balance should be 101.00 QVERIFY(html.indexOf("" + openingDateString + "Test PayeeTransfer from Credit Card 100.00 101.00") > 0); // after a transfer of 100 JPY the balance should be 102.00 - price is 0.011 (precision of 2) QVERIFY(html.indexOf("" + intermediateDateString + "Test PayeeTransfer from Japanese Checking 1.00 102.00") > 0); // after a transfer of 100 the balance should be 202.00 QVERIFY(html.indexOf("" + intermediateDateString + "Test PayeeTransfer from Credit Card 100.00 202.00") > 0); // after a transfer of 100 JPY the balance should be 204.00 - price is 0.024 (precision of 2) QVERIFY(html.indexOf("" + closingDateString + "Test PayeeTransfer from Japanese Checking 2.00 204.00") > 0); // after a transfer of 100 the balance should be 304.00 QVERIFY(html.indexOf("" + closingDateString + "Test PayeeTransfer from Credit Card 100.00 304.00") > 0); // a 100.00 JPY withdrawal should be displayed as such even if the expense account uses another currency QVERIFY(html.indexOf("" + intermediateDateString + "Test PayeeSoloJPY -100.00JPY -300.00") > 0); // now run the same report again but this time convert all values to the base currency and make sure the values are correct filter.setConvertCurrency(true); XMLandback(filter); QueryTable qtbl_4(filter); writeTabletoHTML(qtbl_4, "Transactions by Account (multiple currencies converted to base).html"); html = qtbl_4.renderBody(); rows = qtbl_4.rows(); QVERIFY(rows.count() == 19); // check the opening and closing balances QVERIFY(html.indexOf(openingDateString + "" + i18n("Opening Balance") + " 0.00") > 0); QVERIFY(html.indexOf(closingDateString + "" + i18n("Closing Balance") + " 304.00") > 0); QVERIFY(html.indexOf(closingDateString + "" + i18n("Closing Balance") + " -300.00") > 0); // although the balance should be -5.00 it's -8.00 because the foreign currency balance is converted using the closing date price (0.024) QVERIFY(html.indexOf(closingDateString + "" + i18n("Closing Balance") + " -8.00") > 0); // a 100.00 JPY transfer should be displayed as -1.00 when converted to the base currency using the opening date price QVERIFY(html.indexOf("" + openingDateString + "Test PayeeTransfer to Checking Account -1.00 -1.00") > 0); // a 100.00 JPY transfer should be displayed as -1.00 when converted to the base currency using the intermediate date price QVERIFY(html.indexOf("" + intermediateDateString + "Test PayeeTransfer to Checking Account -1.00 -2.00") > 0); // a 100.00 JPY withdrawal should be displayed as -1.00 when converted to the base currency using the intermediate date price QVERIFY(html.indexOf("" + intermediateDateString + "Test PayeeSolo -1.00 -3.00") > 0); // a 100.00 JPY transfer should be displayed as -2.00 when converted to the base currency using the closing date price (notice the balance is -5.00) QVERIFY(html.indexOf("" + closingDateString + "Test PayeeTransfer to Checking Account -2.00 -5.00") > 0); } catch (const MyMoneyException &e) { QFAIL(qPrintable(e.what())); } } void QueryTableTest::testTaxReport() { try { TransactionHelper t1q1(QDate(2004, 1, 1), MyMoneySplit::ActionWithdrawal, moSolo, acChecking, acSolo); TransactionHelper t2q1(QDate(2004, 2, 1), MyMoneySplit::ActionWithdrawal, moParent1, acChecking, acTax); unsigned cols; MyMoneyReport filter; filter.setRowType(MyMoneyReport::Row::Category); filter.setName("Tax Transactions"); cols = MyMoneyReport::QueryColumns::Number | MyMoneyReport::QueryColumns::Payee | MyMoneyReport::QueryColumns::Account; filter.setQueryColumns(static_cast(cols)); filter.setTax(true); XMLandback(filter); QueryTable qtbl_3(filter); writeTabletoHTML(qtbl_3, "Tax Transactions.html"); QList rows = qtbl_3.rows(); QString html = qtbl_3.renderBody(); QVERIFY(rows.count() == 1); } catch (const MyMoneyException &e) { QFAIL(qPrintable(e.what())); } }