diff --git a/kmymoney/plugins/views/reports/core/CMakeLists.txt b/kmymoney/plugins/views/reports/core/CMakeLists.txt index 8652df58d..0e79c4a26 100644 --- a/kmymoney/plugins/views/reports/core/CMakeLists.txt +++ b/kmymoney/plugins/views/reports/core/CMakeLists.txt @@ -1,28 +1,29 @@ if(BUILD_TESTING) add_subdirectory(tests) endif() set (libreports_a_SOURCES + cashflowlist.cpp kreportchartview.cpp reportaccount.cpp listtable.cpp objectinfotable.cpp pivotgrid.cpp pivottable.cpp querytable.cpp reporttable.cpp ) add_library(reports STATIC ${libreports_a_SOURCES}) target_link_libraries(reports PUBLIC KChart Alkimia::alkimia Qt5::PrintSupport kmymoney_common kmm_settings PRIVATE KF5::I18n ) add_dependencies(reports kmm_settings) diff --git a/kmymoney/plugins/views/reports/core/cashflowlist.cpp b/kmymoney/plugins/views/reports/core/cashflowlist.cpp new file mode 100644 index 000000000..a035571c9 --- /dev/null +++ b/kmymoney/plugins/views/reports/core/cashflowlist.cpp @@ -0,0 +1,166 @@ +/* + * 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 "mymoneyexception.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 + */ +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()) { + qDebug() << (*it_item).date().toString(Qt::ISODate) << " " << (*it_item).value().toString(); + ++it_item; + } +} diff --git a/kmymoney/plugins/views/reports/core/cashflowlist.h b/kmymoney/plugins/views/reports/core/cashflowlist.h new file mode 100644 index 000000000..15efad4fb --- /dev/null +++ b/kmymoney/plugins/views/reports/core/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/plugins/views/reports/core/querytable.cpp b/kmymoney/plugins/views/reports/core/querytable.cpp index e4ae9e45a..a1e14f87a 100644 --- a/kmymoney/plugins/views/reports/core/querytable.cpp +++ b/kmymoney/plugins/views/reports/core/querytable.cpp @@ -1,2170 +1,1975 @@ /* * Copyright 2005 Ace Jones * Copyright 2017-2018 Łukasz Wojniłowicz * * 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 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. * - * * - ***************************************************************************/ - -/**************************************************************************** - 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. -*****************************************************************************/ - #include "querytable.h" #include // ---------------------------------------------------------------------------- // QT Includes #include #include // ---------------------------------------------------------------------------- // KDE Includes #include // ---------------------------------------------------------------------------- // Project Includes +#include "cashflowlist.h" #include "mymoneyfile.h" #include "mymoneyaccount.h" #include "mymoneysecurity.h" #include "mymoneyinstitution.h" #include "mymoneyprice.h" #include "mymoneypayee.h" #include "mymoneytag.h" #include "mymoneysplit.h" #include "mymoneytransaction.h" #include "mymoneyreport.h" #include "mymoneyexception.h" #include "kmymoneyutils.h" #include "reportaccount.h" #include "mymoneyenums.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); - - //qDebug() << "CashFlowListItem::NPV( " << _rate << " ) == " << result; - - return result; -} - -// **************************************************************************** -// -// CashFlowList implementation -// -// Cash flow analysis tools for investment reports -// -// **************************************************************************** - -CashFlowListItem CashFlowList::mostRecent() const -{ - CashFlowList dupe(*this); - - qSort(dupe); - - //qDebug() << " CashFlowList::mostRecent() == " << dupe.back().date().toString(Qt::ISODate); - - return dupe.back(); -} - -MyMoneyMoney CashFlowList::NPV(double _rate) const -{ - MyMoneyMoney result; - - const_iterator it_cash = constBegin(); - while (it_cash != constEnd()) { - result += (*it_cash).NPV(_rate); - ++it_cash; - } - - //qDebug() << "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 -{ - double r = rate + 1.0; - double res = 0.00000;//back().value().toDouble(); - - QList::const_iterator list_it = constBegin(); - while (list_it != constEnd()) { - 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 -{ - double r = rate + 1.0; - double res = 0.00000; - - QList::const_iterator list_it = constBegin(); - while (list_it != constEnd()) { - 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 = constBegin(); - while (it_cash != constEnd()) { - result += (*it_cash).value(); - ++it_cash; - } - - return result; -} - -void CashFlowList::dumpDebug() const -{ - const_iterator it_item = constBegin(); - while (it_item != constEnd()) { - qDebug() << (*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() { m_columns.clear(); m_group.clear(); m_subtotal.clear(); m_postcolumns.clear(); switch (m_config.rowType()) { case eMyMoney::Report::RowType::AccountByTopAccount: case eMyMoney::Report::RowType::EquityType: case eMyMoney::Report::RowType::AccountType: case eMyMoney::Report::RowType::Institution: constructAccountTable(); m_columns << ctAccount; break; case eMyMoney::Report::RowType::Account: constructTransactionTable(); m_columns << ctAccountID << ctPostDate; break; case eMyMoney::Report::RowType::Payee: case eMyMoney::Report::RowType::Tag: case eMyMoney::Report::RowType::Month: case eMyMoney::Report::RowType::Week: constructTransactionTable(); m_columns << ctPostDate << ctAccount; break; case eMyMoney::Report::RowType::CashFlow: constructSplitsTable(); m_columns << ctPostDate; break; default: constructTransactionTable(); m_columns << ctPostDate; } // Sort the data to match the report definition m_subtotal << ctValue; switch (m_config.rowType()) { case eMyMoney::Report::RowType::CashFlow: m_group << ctCategoryType << ctTopCategory << ctCategory; break; case eMyMoney::Report::RowType::Category: m_group << ctCategoryType << ctTopCategory << ctCategory; break; case eMyMoney::Report::RowType::TopCategory: m_group << ctCategoryType << ctTopCategory; break; case eMyMoney::Report::RowType::TopAccount: m_group << ctTopAccount << ctAccount; break; case eMyMoney::Report::RowType::Account: m_group << ctAccount; break; case eMyMoney::Report::RowType::AccountReconcile: m_group << ctAccount << ctReconcileFlag; break; case eMyMoney::Report::RowType::Payee: m_group << ctPayee; break; case eMyMoney::Report::RowType::Tag: m_group << ctTag; break; case eMyMoney::Report::RowType::Month: m_group << ctMonth; break; case eMyMoney::Report::RowType::Week: m_group << ctWeek; break; case eMyMoney::Report::RowType::AccountByTopAccount: m_group << ctTopAccount; break; case eMyMoney::Report::RowType::EquityType: m_group << ctEquityType; break; case eMyMoney::Report::RowType::AccountType: m_group << ctType; break; case eMyMoney::Report::RowType::Institution: m_group << ctInstitution << ctTopAccount; break; default: throw MYMONEYEXCEPTION_CSTRING("QueryTable::QueryTable(): unhandled row type"); } QVector sort = QVector::fromList(m_group) << QVector::fromList(m_columns) << ctID << ctRank; m_columns.clear(); switch (m_config.rowType()) { case eMyMoney::Report::RowType::AccountByTopAccount: case eMyMoney::Report::RowType::EquityType: case eMyMoney::Report::RowType::AccountType: case eMyMoney::Report::RowType::Institution: m_columns << ctAccount; break; default: m_columns << ctPostDate; } unsigned qc = m_config.queryColumns(); if (qc & eMyMoney::Report::QueryColumn::Number) m_columns << ctNumber; if (qc & eMyMoney::Report::QueryColumn::Payee) m_columns << ctPayee; if (qc & eMyMoney::Report::QueryColumn::Tag) m_columns << ctTag; if (qc & eMyMoney::Report::QueryColumn::Category) m_columns << ctCategory; if (qc & eMyMoney::Report::QueryColumn::Account) m_columns << ctAccount; if (qc & eMyMoney::Report::QueryColumn::Reconciled) m_columns << ctReconcileFlag; if (qc & eMyMoney::Report::QueryColumn::Memo) m_columns << ctMemo; if (qc & eMyMoney::Report::QueryColumn::Action) m_columns << ctAction; if (qc & eMyMoney::Report::QueryColumn::Shares) m_columns << ctShares; if (qc & eMyMoney::Report::QueryColumn::Price) m_columns << ctPrice; if (qc & eMyMoney::Report::QueryColumn::Performance) { m_subtotal.clear(); switch (m_config.investmentSum()) { case eMyMoney::Report::InvestmentSum::OwnedAndSold: m_columns << ctBuys << ctSells << ctReinvestIncome << ctCashIncome << ctEndingBalance << ctReturn << ctReturnInvestment; m_subtotal << ctBuys << ctSells << ctReinvestIncome << ctCashIncome << ctEndingBalance << ctReturn << ctReturnInvestment; break; case eMyMoney::Report::InvestmentSum::Owned: m_columns << ctBuys << ctReinvestIncome << ctMarketValue << ctReturn << ctReturnInvestment; m_subtotal << ctBuys << ctReinvestIncome << ctMarketValue << ctReturn << ctReturnInvestment; break; case eMyMoney::Report::InvestmentSum::Sold: m_columns << ctBuys << ctSells << ctCashIncome << ctReturn << ctReturnInvestment; m_subtotal << ctBuys << ctSells << ctCashIncome << ctReturn << ctReturnInvestment; break; case eMyMoney::Report::InvestmentSum::Period: default: m_columns << ctStartingBalance << ctBuys << ctSells << ctReinvestIncome << ctCashIncome << ctEndingBalance << ctReturn << ctReturnInvestment; m_subtotal << ctStartingBalance << ctBuys << ctSells << ctReinvestIncome << ctCashIncome << ctEndingBalance << ctReturn << ctReturnInvestment; break; } } if (qc & eMyMoney::Report::QueryColumn::CapitalGain) { m_subtotal.clear(); switch (m_config.investmentSum()) { case eMyMoney::Report::InvestmentSum::Owned: m_columns << ctShares << ctBuyPrice << ctLastPrice << ctBuys << ctMarketValue << ctPercentageGain << ctCapitalGain; m_subtotal << ctShares << ctBuyPrice << ctLastPrice << ctBuys << ctMarketValue << ctPercentageGain << ctCapitalGain; break; case eMyMoney::Report::InvestmentSum::Sold: default: m_columns << ctBuys << ctSells << ctCapitalGain; m_subtotal << ctBuys << ctSells << ctCapitalGain; if (m_config.isShowingSTLTCapitalGains()) { m_columns << ctBuysST << ctSellsST << ctCapitalGainST << ctBuysLT << ctSellsLT << ctCapitalGainLT; m_subtotal << ctBuysST << ctSellsST << ctCapitalGainST << ctBuysLT << ctSellsLT << ctCapitalGainLT; } break; } } if (qc & eMyMoney::Report::QueryColumn::Loan) { m_columns << ctPayment << ctInterest << ctFees; m_postcolumns << ctBalance; } if (qc & eMyMoney::Report::QueryColumn::Balance) m_postcolumns << ctBalance; TableRow::setSortCriteria(sort); qSort(m_rows); if (m_config.isShowingColumnTotals()) constructTotalRows(); // adds total rows to m_rows } void QueryTable::constructTotalRows() { if (m_rows.isEmpty()) return; // qSort places grand total at last position, because it doesn't belong to any group for (int i = 0; i < m_rows.count(); ++i) { if (m_rows.at(0)[ctRank] == QLatin1String("4") || m_rows.at(0)[ctRank] == QLatin1String("5")) // it should be unlikely that total row is at the top of rows, so... m_rows.move(0, m_rows.count() - 1 - i); // ...move it at the bottom else break; } MyMoneyFile* file = MyMoneyFile::instance(); QList subtotals = m_subtotal; QList groups = m_group; QList columns = m_columns; if (!m_subtotal.isEmpty() && subtotals.count() == 1) columns.append(m_subtotal); QList postcolumns = m_postcolumns; if (!m_postcolumns.isEmpty()) columns.append(postcolumns); QMap>> totalCurrency; QList> totalGroups; QMap totalsValues; // initialize all total values under summed columns to be zero foreach (auto subtotal, subtotals) { totalsValues.insert(subtotal, MyMoneyMoney()); } totalsValues.insert(ctRowsCount, MyMoneyMoney()); // create total groups containing totals row for each group totalGroups.append(totalsValues); // prepend with extra group for grand total for (int j = 0; j < groups.count(); ++j) { totalGroups.append(totalsValues); } QList stashedTotalRows; int iCurrentRow, iNextRow; for (iCurrentRow = 0; iCurrentRow < m_rows.count();) { iNextRow = iCurrentRow + 1; // total rows are useless at summing so remove whole block of them at once while (iNextRow != m_rows.count() && (m_rows.at(iNextRow).value(ctRank) == QLatin1String("4") || m_rows.at(iNextRow).value(ctRank) == QLatin1String("5"))) { stashedTotalRows.append(m_rows.takeAt(iNextRow)); // ...but stash them just in case } bool lastRow = (iNextRow == m_rows.count()); // sum all subtotal values for lowest group QString currencyID = m_rows.at(iCurrentRow).value(ctCurrency); if (m_rows.at(iCurrentRow).value(ctRank) == QLatin1String("1")) { // don't sum up on balance (rank = 0 || rank = 3) and minor split (rank = 2) foreach (auto subtotal, subtotals) { if (!totalCurrency.contains(currencyID)) totalCurrency[currencyID].append(totalGroups); totalCurrency[currencyID].last()[subtotal] += MyMoneyMoney(m_rows.at(iCurrentRow)[subtotal]); } totalCurrency[currencyID].last()[ctRowsCount] += MyMoneyMoney::ONE; } // iterate over groups from the lowest to the highest to find group change for (int i = groups.count() - 1; i >= 0 ; --i) { // if any of groups from next row changes (or next row is the last row), then it's time to put totals row if (lastRow || m_rows.at(iCurrentRow)[groups.at(i)] != m_rows.at(iNextRow)[groups.at(i)]) { bool isMainCurrencyTotal = true; QMap>>::iterator currencyGrp = totalCurrency.begin(); while (currencyGrp != totalCurrency.end()) { if (!MyMoneyMoney((*currencyGrp).at(i + 1).value(ctRowsCount)).isZero()) { // if no rows summed up, then no totals row TableRow totalsRow; // sum all subtotal values for higher groups (excluding grand total) and reset lowest group values QMap::iterator upperGrp = (*currencyGrp)[i].begin(); QMap::iterator lowerGrp = (*currencyGrp)[i + 1].begin(); while(upperGrp != (*currencyGrp)[i].end()) { totalsRow[lowerGrp.key()] = lowerGrp.value().toString(); // fill totals row with subtotal values... (*upperGrp) += (*lowerGrp); // (*lowerGrp) = MyMoneyMoney(); ++upperGrp; ++lowerGrp; } // custom total values calculations foreach (auto subtotal, subtotals) { if (subtotal == ctReturnInvestment) totalsRow[subtotal] = helperROI((*currencyGrp).at(i + 1).value(ctBuys) - (*currencyGrp).at(i + 1).value(ctReinvestIncome), (*currencyGrp).at(i + 1).value(ctSells), (*currencyGrp).at(i + 1).value(ctStartingBalance), (*currencyGrp).at(i + 1).value(ctEndingBalance) + (*currencyGrp).at(i + 1).value(ctMarketValue), (*currencyGrp).at(i + 1).value(ctCashIncome)).toString(); else if (subtotal == ctPercentageGain) totalsRow[subtotal] = (((*currencyGrp).at(i + 1).value(ctBuys) + (*currencyGrp).at(i + 1).value(ctMarketValue)) / (*currencyGrp).at(i + 1).value(ctBuys).abs()).toString(); else if (subtotal == ctPrice) totalsRow[subtotal] = MyMoneyMoney((*currencyGrp).at(i + 1).value(ctPrice) / (*currencyGrp).at(i + 1).value(ctRowsCount)).toString(); } // total values that aren't calculated here, but are taken untouched from external source, e.g. constructPerformanceRow if (!stashedTotalRows.isEmpty()) { for (int j = 0; j < stashedTotalRows.count(); ++j) { if (stashedTotalRows.at(j).value(ctCurrency) != currencyID) continue; foreach (auto subtotal, subtotals) { if (subtotal == ctReturn) totalsRow[ctReturn] = stashedTotalRows.takeAt(j)[ctReturn]; } break; } } (*currencyGrp).replace(i + 1, totalsValues); for (int j = 0; j < groups.count(); ++j) { totalsRow[groups.at(j)] = m_rows.at(iCurrentRow)[groups.at(j)]; // ...and identification } currencyID = currencyGrp.key(); if (currencyID.isEmpty() && totalCurrency.count() > 1) currencyID = file->baseCurrency().id(); totalsRow[ctCurrency] = currencyID; if (isMainCurrencyTotal) { totalsRow[ctRank] = QLatin1Char('4'); isMainCurrencyTotal = false; } else totalsRow[ctRank] = QLatin1Char('5'); totalsRow[ctDepth] = QString::number(i); totalsRow.remove(ctRowsCount); m_rows.insert(iNextRow++, totalsRow); // iCurrentRow and iNextRow can diverge here by more than one } ++currencyGrp; } } } // code to put grand total row if (lastRow) { bool isMainCurrencyTotal = true; QMap>>::iterator currencyGrp = totalCurrency.begin(); while (currencyGrp != totalCurrency.end()) { TableRow totalsRow; QMap::const_iterator grandTotalGrp = (*currencyGrp)[0].constBegin(); while(grandTotalGrp != (*currencyGrp)[0].constEnd()) { totalsRow[grandTotalGrp.key()] = grandTotalGrp.value().toString(); ++grandTotalGrp; } foreach (auto subtotal, subtotals) { if (subtotal == ctReturnInvestment) totalsRow[subtotal] = helperROI((*currencyGrp).at(0).value(ctBuys) - (*currencyGrp).at(0).value(ctReinvestIncome), (*currencyGrp).at(0).value(ctSells), (*currencyGrp).at(0).value(ctStartingBalance), (*currencyGrp).at(0).value(ctEndingBalance) + (*currencyGrp).at(0).value(ctMarketValue), (*currencyGrp).at(0).value(ctCashIncome)).toString(); else if (subtotal == ctPercentageGain) totalsRow[subtotal] = (((*currencyGrp).at(0).value(ctBuys) + (*currencyGrp).at(0).value(ctMarketValue)) / (*currencyGrp).at(0).value(ctBuys).abs()).toString(); else if (subtotal == ctPrice) totalsRow[subtotal] = MyMoneyMoney((*currencyGrp).at(0).value(ctPrice) / (*currencyGrp).at(0).value(ctRowsCount)).toString(); } if (!stashedTotalRows.isEmpty()) { for (int j = 0; j < stashedTotalRows.count(); ++j) { foreach (auto subtotal, subtotals) { if (subtotal == ctReturn) totalsRow[ctReturn] = stashedTotalRows.takeAt(j)[ctReturn]; } } } for (int j = 0; j < groups.count(); ++j) { totalsRow[groups.at(j)] = QString(); // no identification } currencyID = currencyGrp.key(); if (currencyID.isEmpty() && totalCurrency.count() > 1) currencyID = file->baseCurrency().id(); totalsRow[ctCurrency] = currencyID; if (isMainCurrencyTotal) { totalsRow[ctRank] = QLatin1Char('4'); isMainCurrencyTotal = false; } else totalsRow[ctRank] = QLatin1Char('5'); totalsRow[ctDepth] = QString(); m_rows.append(totalsRow); ++currencyGrp; } break; // no use to loop further } iCurrentRow = iNextRow; // iCurrent makes here a leap forward by at least one } } 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; bool use_summary; bool hide_details; bool tag_special_case = false; switch (m_config.rowType()) { case eMyMoney::Report::RowType::Category: case eMyMoney::Report::RowType::TopCategory: use_summary = false; use_transfers = false; hide_details = false; break; case eMyMoney::Report::RowType::Payee: use_summary = false; use_transfers = false; hide_details = (m_config.detailLevel() == eMyMoney::Report::DetailLevel::None); break; case eMyMoney::Report::RowType::Tag: use_summary = false; use_transfers = false; hide_details = (m_config.detailLevel() == eMyMoney::Report::DetailLevel::None); tag_special_case = true; break; default: use_summary = true; use_transfers = true; hide_details = (m_config.detailLevel() == eMyMoney::Report::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[ctID] = qS[ctID] = (* it_transaction).id(); qA[ctEntryDate] = qS[ctEntryDate] = (* it_transaction).entryDate().toString(Qt::ISODate); qA[ctPostDate] = qS[ctPostDate] = (* it_transaction).postDate().toString(Qt::ISODate); qA[ctCommodity] = qS[ctCommodity] = (* it_transaction).commodity(); pd = (* it_transaction).postDate(); qA[ctMonth] = qS[ctMonth] = i18n("Month of %1", QDate(pd.year(), pd.month(), 1).toString(Qt::ISODate)); qA[ctWeek] = qS[ctWeek] = i18n("Week of %1", pd.addDays(1 - pd.dayOfWeek()).toString(Qt::ISODate)); if (!m_containsNonBaseCurrency && (*it_transaction).commodity() != file->baseCurrency().id()) m_containsNonBaseCurrency = true; if (report.isConvertCurrency()) qA[ctCurrency] = qS[ctCurrency] = file->baseCurrency().id(); else qA[ctCurrency] = qS[ctCurrency] = (*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.constBegin(), myBegin = splits.constEnd(); it_split != splits.constEnd(); ++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() & eMyMoney::Report::QueryColumn::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; QString baseCurrency = file->baseCurrency().id(); QMap xrMap; // container for conversion rates from given currency to myBeginCurrency do { MyMoneyMoney xr; ReportAccount splitAcc((* it_split).accountId()); QString splitCurrency; if (splitAcc.isInvest()) splitCurrency = file->account(file->account((*it_split).accountId()).parentAccountId()).currencyId(); else splitCurrency = file->account((*it_split).accountId()).currencyId(); if (it_split == myBegin) myBeginCurrency = splitCurrency; //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 = xrMap.value(splitCurrency, xr); // check if there is conversion rate to myBeginCurrency already stored... if (xr == MyMoneyMoney()) // ...if not... xr = (*it_split).price(); // ...take conversion rate to myBeginCurrency from split else if (splitAcc.isInvest()) // if it's stock split... xr *= (*it_split).price(); // ...multiply it by stock price stored in split if (!m_containsNonBaseCurrency && myBeginCurrency != baseCurrency) m_containsNonBaseCurrency = true; if (myBeginCurrency != baseCurrency) { // myBeginCurrency can differ from baseCurrency... MyMoneyPrice price = file->price(myBeginCurrency, baseCurrency, (*it_transaction).postDate()); // ...so check conversion rate... if (price.isValid()) { xr *= price.rate(baseCurrency); // ...and multiply it by current price... qA[ctCurrency] = qS[ctCurrency] = baseCurrency; } else qA[ctCurrency] = qS[ctCurrency] = myBeginCurrency; // ...and set information about non-baseCurrency } } else if (splitAcc.isInvest()) xr = (*it_split).price(); else xr = MyMoneyMoney::ONE; if (it_split == myBegin) { include_me = m_config.includes(splitAcc); if (include_me) // 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); qA[ctAccount] = splitAcc.name(); qA[ctAccountID] = splitAcc.id(); qA[ctTopAccount] = splitAcc.topParentName(); if (splitAcc.isInvest()) { // use the institution of the parent for stock accounts institution = splitAcc.parent().institutionId(); MyMoneyMoney shares = (*it_split).shares(); int pricePrecision = file->security(splitAcc.currencyId()).pricePrecision(); qA[ctAction] = (*it_split).action(); qA[ctShares] = shares.isZero() ? QString() : shares.toString(); qA[ctPrice] = shares.isZero() ? QString() : xr.convertPrecision(pricePrecision).toString(); if (((*it_split).action() == MyMoneySplit::actionName(eMyMoney::Split::Action::BuyShares)) && shares.isNegative()) qA[ctAction] = "Sell"; qA[ctInvestAccount] = splitAcc.parent().name(); MyMoneySplit stockSplit = (*it_split); MyMoneySplit assetAccountSplit; QList feeSplits; QList interestSplits; MyMoneySecurity currency; MyMoneySecurity security; eMyMoney::Split::InvestmentTransactionType transactionType; KMyMoneyUtils::dissectTransaction((*it_transaction), stockSplit, assetAccountSplit, feeSplits, interestSplits, security, currency, transactionType); if (!(assetAccountSplit == MyMoneySplit())) { for (it_split = splits.begin(); it_split != splits.end(); ++it_split) { if ((*it_split) == assetAccountSplit) { splitAcc = ReportAccount(assetAccountSplit.accountId()); // switch over from stock split to asset split because amount in stock split doesn't take fees/interests into account myBegin = it_split; // set myBegin to asset split, so stock split can be listed in details under splits myBeginCurrency = (file->account((*myBegin).accountId())).currencyId(); if (!m_containsNonBaseCurrency && myBeginCurrency != baseCurrency) m_containsNonBaseCurrency = true; if (m_config.isConvertCurrency()) { if (myBeginCurrency != baseCurrency) { MyMoneyPrice price = file->price(myBeginCurrency, baseCurrency, (*it_transaction).postDate()); if (price.isValid()) { xr = price.rate(baseCurrency); qA[ctCurrency] = qS[ctCurrency] = baseCurrency; } else qA[ctCurrency] = qS[ctCurrency] = myBeginCurrency; } else xr = MyMoneyMoney::ONE; qA[ctPrice] = shares.isZero() ? QString() : (stockSplit.price() * xr / (*it_split).price()).toString(); // put conversion rate for all splits with this currency, so... // every split of transaction have the same conversion rate xrMap.insert(splitCurrency, MyMoneyMoney::ONE / (*it_split).price()); } else xr = (*it_split).price(); break; } } } } else qA[ctPrice] = xr.toString(); a_fullname = splitAcc.fullName(); a_memo = (*it_split).memo(); transaction_text = m_config.match((*it_split)); qA[ctInstitution] = institution.isEmpty() ? i18n("No Institution") : file->institution(institution).name(); qA[ctPayee] = payee.isEmpty() ? i18n("[Empty Payee]") : file->payee(payee).name().simplified(); if (tag_special_case) { tagIdListCache = tagIdList; } else { QString delimiter; foreach(const auto tagId, tagIdList) { qA[ctTag] += delimiter + file->tag(tagId).name().simplified(); delimiter = QLatin1Char(','); } } qA[ctReconcileDate] = (*it_split).reconcileDate().toString(Qt::ISODate); qA[ctReconcileFlag] = KMyMoneyUtils::reconcileStateToString((*it_split).reconcileFlag(), true); qA[ctNumber] = (*it_split).number(); qA[ctMemo] = a_memo; qA[ctValue] = ((*it_split).shares() * xr).convert(fraction).toString(); qS[ctReconcileDate] = qA[ctReconcileDate]; qS[ctReconcileFlag] = qA[ctReconcileFlag]; qS[ctNumber] = qA[ctNumber]; qS[ctTopCategory] = splitAcc.topParentName(); qS[ctCategoryType] = 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[ctValue] = (-(*it_split).shares() * xr).convert(fraction).toString(); qA[ctRank] = QLatin1Char('1'); qA[ctSplit].clear(); } 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[ctRank] = QLatin1Char('1'); qA[ctCategory] = i18n("[Split Transaction]"); qA[ctTopCategory] = i18nc("Split transaction", "Split"); qA[ctCategoryType] = i18nc("Split transaction", "Split"); m_rows += qA; } } } } else { if (include_me) { if (loan_special_case) { MyMoneyMoney value = (-(* it_split).shares() * xr).convert(fraction); if ((*it_split).action() == MyMoneySplit::actionName(eMyMoney::Split::Action::Amortization)) { // put the payment in the "payment" column and convert to lowest fraction qA[ctPayee] = value.toString(); } else if ((*it_split).action() == MyMoneySplit::actionName(eMyMoney::Split::Action::Interest)) { // put the interest in the "interest" column and convert to lowest fraction qA[ctInterest] = 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[ctFees]); qA[ctFees] = (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[ctValue].clear(); //convert to lowest fraction qA[ctSplit] = (-(*it_split).shares() * xr).convert(fraction).toString(); qA[ctRank] = QLatin1Char('2'); } else { //this applies when the transaction has only 2 splits, or each split is going to be //shown separately, eg. transactions by category switch (m_config.rowType()) { case eMyMoney::Report::RowType::Category: case eMyMoney::Report::RowType::TopCategory: if (splitAcc.isIncomeExpense()) qA[ctValue] = (-(*it_split).shares() * xr).convert(fraction).toString(); // needed for category reports, in case of multicurrency transaction it breaks it break; default: break; } qA[ctSplit].clear(); qA[ctRank] = QLatin1Char('1'); } qA [ctMemo] = (*it_split).memo(); if (!m_containsNonBaseCurrency && splitAcc.currencyId() != file->baseCurrency().id()) m_containsNonBaseCurrency = true; if (report.isConvertCurrency()) qS[ctCurrency] = file->baseCurrency().id(); else qS[ctCurrency] = splitAcc.currency().id(); if (! splitAcc.isIncomeExpense()) { qA[ctCategory] = ((*it_split).shares().isNegative()) ? i18n("Transfer from %1", splitAcc.fullName()) : i18n("Transfer to %1", splitAcc.fullName()); qA[ctTopCategory] = splitAcc.topParentName(); qA[ctCategoryType] = i18n("Transfer"); } else { qA [ctCategory] = splitAcc.fullName(); qA [ctTopCategory] = splitAcc.topParentName(); qA [ctCategoryType] = MyMoneyAccount::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[ctTag] = i18n("[No Tag]"); else for (int i = 0; i < tagIdListCache.size(); i++) { qA[ctTag] = file->tag(tagIdListCache[i]).name().simplified(); m_rows += qA; } } else { m_rows += qA; } } } } } if (m_config.includes(splitAcc) && use_transfers && !(splitAcc.isInvest() && include_me)) { // otherwise stock split is displayed twice in report if (! splitAcc.isIncomeExpense()) { //multiply by currency and convert to lowest fraction qS[ctValue] = ((*it_split).shares() * xr).convert(fraction).toString(); qS[ctRank] = QLatin1Char('1'); qS[ctAccount] = splitAcc.name(); qS[ctAccountID] = splitAcc.id(); qS[ctTopAccount] = splitAcc.topParentName(); qS[ctCategory] = ((*it_split).shares().isNegative()) ? i18n("Transfer to %1", a_fullname) : i18n("Transfer from %1", a_fullname); qS[ctInstitution] = institution.isEmpty() ? i18n("No Institution") : file->institution(institution).name(); qS[ctMemo] = (*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[ctTag] += delimiter + file->tag(tagIdList[i]).name().simplified(); delimiter = '+'; } qS[ctPayee] = payee.isEmpty() ? qA[ctPayee] : 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 eMyMoney::Report::RowType::Account: case eMyMoney::Report::RowType::TopAccount: break; // case eMyMoney::Report::RowType::Category: // 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); for (auto 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 if (!m_containsNonBaseCurrency && account.currency().id() != file->baseCurrency().id()) m_containsNonBaseCurrency = true; if (m_config.isConvertCurrency()) qA[ctCurrency] = file->baseCurrency().id(); else qA[ctCurrency] = account.currency().id(); qA[ctAccountID] = account.id(); qA[ctAccount] = account.name(); qA[ctTopAccount] = account.topParentName(); qA[ctInstitution] = institution.isEmpty() ? i18n("No Institution") : file->institution(institution).name(); qA[ctRank] = QLatin1Char('0'); qA[ctPrice] = startPrice.convertPrecision(account.currency().pricePrecision()).toString(); if (account.isInvest()) { qA[ctShares] = startShares.toString(); } qA[ctPostDate] = strStartDate; qA[ctBalance] = startBalance.convert(fraction).toString(); qA[ctValue].clear(); qA[ctID] = QLatin1Char('A'); m_rows += qA; //ending balance qA[ctPrice] = endPrice.convertPrecision(account.currency().pricePrecision()).toString(); if (account.isInvest()) { qA[ctShares] = endShares.toString(); } qA[ctPostDate] = strEndDate; qA[ctBalance] = endBalance.toString(); qA[ctRank] = QLatin1Char('3'); qA[ctID] = QLatin1Char('Z'); m_rows += qA; } } MyMoneyMoney QueryTable::helperROI(const MyMoneyMoney &buys, const MyMoneyMoney &sells, const MyMoneyMoney &startingBal, const MyMoneyMoney &endingBal, const MyMoneyMoney &cashIncome) const { MyMoneyMoney returnInvestment; if (!buys.isZero() || !startingBal.isZero()) { returnInvestment = (sells + buys + cashIncome + endingBal - startingBal) / (startingBal - buys); returnInvestment = returnInvestment.convert(10000); } else returnInvestment = MyMoneyMoney(); // if no investment then no return on investment return returnInvestment; } -MyMoneyMoney QueryTable::helperIRR(const CashFlowList &all) const +QString QueryTable::helperIRR(const CashFlowList &all) const { - MyMoneyMoney annualReturn; try { - double irr = all.IRR(); -#ifdef Q_CC_MSVC - annualReturn = MyMoneyMoney(_isnan(irr) ? 0 : irr, 10000); -#else - annualReturn = MyMoneyMoney(std::isnan(irr) ? 0 : irr, 10000); -#endif - } catch (QString e) { - qDebug() << e; + return MyMoneyMoney(all.XIRR(), 10000).toString(); + } catch (MyMoneyException &e) { + qDebug() << e.what(); + all.dumpDebug(); + return QString(); } - return annualReturn; } void QueryTable::sumInvestmentValues(const ReportAccount& account, QList& cfList, QList& shList) const { for (int i = InvestmentValue::Buys; i < InvestmentValue::End; ++i) cfList.append(CashFlowList()); for (int i = InvestmentValue::Buys; i <= InvestmentValue::BuysOfOwned; ++i) shList.append(MyMoneyMoney()); MyMoneyFile* file = MyMoneyFile::instance(); MyMoneyReport report = m_config; QDate startingDate; QDate endingDate; QDate newStartingDate; QDate newEndingDate; const bool isSTLT = report.isShowingSTLTCapitalGains(); const int settlementPeriod = report.settlementPeriod(); QDate termSeparator = report.termSeparator().addDays(-settlementPeriod); report.validDateRange(startingDate, endingDate); newStartingDate = startingDate; newEndingDate = endingDate; if (report.queryColumns() & eMyMoney::Report::QueryColumn::CapitalGain) { // Saturday and Sunday aren't valid settlement dates if (endingDate.dayOfWeek() == Qt::Saturday) endingDate = endingDate.addDays(-1); else if (endingDate.dayOfWeek() == Qt::Sunday) endingDate = endingDate.addDays(-2); if (termSeparator.dayOfWeek() == Qt::Saturday) termSeparator = termSeparator.addDays(-1); else if (termSeparator.dayOfWeek() == Qt::Sunday) termSeparator = termSeparator.addDays(-2); if (startingDate.daysTo(endingDate) <= settlementPeriod) // no days to check for return; termSeparator = termSeparator.addDays(-settlementPeriod); newEndingDate = endingDate.addDays(-settlementPeriod); } shList[BuysOfOwned] = file->balance(account.id(), newEndingDate); // get how many shares there are at the end of period MyMoneyMoney stashedBuysOfOwned = shList.at(BuysOfOwned); bool reportedDateRange = true; // flag marking sell transactions between startingDate and endingDate report.setReportAllSplits(false); report.setConsiderCategory(true); report.clearAccountFilter(); report.addAccount(account.id()); report.setDateFilter(newStartingDate, newEndingDate); do { QList transactions = file->transactionList(report); for (QList::const_reverse_iterator it_t = transactions.crbegin(); it_t != transactions.crend(); ++it_t) { MyMoneySplit shareSplit = (*it_t).splitByAccount(account.id()); MyMoneySplit assetAccountSplit; QList feeSplits; QList interestSplits; MyMoneySecurity security; MyMoneySecurity currency; eMyMoney::Split::InvestmentTransactionType transactionType; KMyMoneyUtils::dissectTransaction((*it_t), shareSplit, assetAccountSplit, feeSplits, interestSplits, security, currency, transactionType); QDate postDate = (*it_t).postDate(); MyMoneyMoney price; //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(postDate); //we only need base currency because the value is in deep currency else price = MyMoneyMoney::ONE; MyMoneyMoney value = assetAccountSplit.value() * price; MyMoneyMoney shares = shareSplit.shares(); if (transactionType == eMyMoney::Split::InvestmentTransactionType::BuyShares) { if (reportedDateRange) { cfList[Buys].append(CashFlowListItem(postDate, value)); shList[Buys] += shares; } if (shList.at(BuysOfOwned).isZero()) { // add sold shares if (shList.at(BuysOfSells) + shares > shList.at(Sells).abs()) { // add partially sold shares MyMoneyMoney tempVal = (((shList.at(Sells).abs() - shList.at(BuysOfSells))) / shares) * value; cfList[BuysOfSells].append(CashFlowListItem(postDate, tempVal)); shList[BuysOfSells] = shList.at(Sells).abs(); if (isSTLT && postDate < termSeparator) { cfList[LongTermBuysOfSells].append(CashFlowListItem(postDate, tempVal)); shList[LongTermBuysOfSells] = shList.at(BuysOfSells); } } else { // add wholly sold shares cfList[BuysOfSells].append(CashFlowListItem(postDate, value)); shList[BuysOfSells] += shares; if (isSTLT && postDate < termSeparator) { cfList[LongTermBuysOfSells].append(CashFlowListItem(postDate, value)); shList[LongTermBuysOfSells] += shares; } } } else if (shList.at(BuysOfOwned) >= shares) { // subtract not-sold shares shList[BuysOfOwned] -= shares; cfList[BuysOfOwned].append(CashFlowListItem(postDate, value)); } else { // subtract partially not-sold shares MyMoneyMoney tempVal = ((shares - shList.at(BuysOfOwned)) / shares) * value; MyMoneyMoney tempVal2 = (shares - shList.at(BuysOfOwned)); cfList[BuysOfSells].append(CashFlowListItem(postDate, tempVal)); shList[BuysOfSells] += tempVal2; if (isSTLT && postDate < termSeparator) { cfList[LongTermBuysOfSells].append(CashFlowListItem(postDate, tempVal)); shList[LongTermBuysOfSells] += tempVal2; } cfList[BuysOfOwned].append(CashFlowListItem(postDate, (shList.at(BuysOfOwned) / shares) * value)); shList[BuysOfOwned] = MyMoneyMoney(); } } else if (transactionType == eMyMoney::Split::InvestmentTransactionType::SellShares && reportedDateRange) { cfList[Sells].append(CashFlowListItem(postDate, value)); shList[Sells] += shares; } else if (transactionType == eMyMoney::Split::InvestmentTransactionType::SplitShares) { // shares variable is denominator of split ratio here for (int i = Buys; i <= InvestmentValue::BuysOfOwned; ++i) shList[i] /= shares; } else if (transactionType == eMyMoney::Split::InvestmentTransactionType::AddShares || // added shares, when sold give 100% capital gain transactionType == eMyMoney::Split::InvestmentTransactionType::ReinvestDividend) { if (shList.at(BuysOfOwned).isZero()) { // add added/reinvested shares if (shList.at(BuysOfSells) + shares > shList.at(Sells).abs()) { // add partially added/reinvested shares shList[BuysOfSells] = shList.at(Sells).abs(); if (postDate < termSeparator) shList[LongTermBuysOfSells] = shList[BuysOfSells]; } else { // add wholly added/reinvested shares shList[BuysOfSells] += shares; if (postDate < termSeparator) shList[LongTermBuysOfSells] += shares; } } else if (shList.at(BuysOfOwned) >= shares) { // subtract not-added/not-reinvested shares shList[BuysOfOwned] -= shares; cfList[BuysOfOwned].append(CashFlowListItem(postDate, value)); } else { // subtract partially not-added/not-reinvested shares MyMoneyMoney tempVal = (shares - shList.at(BuysOfOwned)); shList[BuysOfSells] += tempVal; if (postDate < termSeparator) shList[LongTermBuysOfSells] += tempVal; cfList[BuysOfOwned].append(CashFlowListItem(postDate, (shList.at(BuysOfOwned) / shares) * value)); shList[BuysOfOwned] = MyMoneyMoney(); } if (transactionType == eMyMoney::Split::InvestmentTransactionType::ReinvestDividend) { value = MyMoneyMoney(); foreach (const auto split, interestSplits) value += split.value(); value *= price; cfList[ReinvestIncome].append(CashFlowListItem(postDate, -value)); } } else if (transactionType == eMyMoney::Split::InvestmentTransactionType::RemoveShares && reportedDateRange) // removed shares give no value in return so no capital gain on them shList[Sells] += shares; else if (transactionType == eMyMoney::Split::InvestmentTransactionType::Dividend || transactionType == eMyMoney::Split::InvestmentTransactionType::Yield) cfList[CashIncome].append(CashFlowListItem(postDate, value)); } reportedDateRange = false; newEndingDate = newStartingDate; newStartingDate = newStartingDate.addYears(-1); report.setDateFilter(newStartingDate, newEndingDate); // search for matching buy transactions year earlier } while ( ( (report.investmentSum() == eMyMoney::Report::InvestmentSum::Owned && !shList[BuysOfOwned].isZero()) || (report.investmentSum() == eMyMoney::Report::InvestmentSum::Sold && !shList.at(Sells).isZero() && shList.at(Sells).abs() > shList.at(BuysOfSells).abs()) || (report.investmentSum() == eMyMoney::Report::InvestmentSum::OwnedAndSold && (!shList[BuysOfOwned].isZero() || (!shList.at(Sells).isZero() && shList.at(Sells).abs() > shList.at(BuysOfSells).abs()))) ) && account.openingDate() <= newEndingDate ); // we've got buy value and no sell value of long-term shares, so get them if (isSTLT && !shList[LongTermBuysOfSells].isZero()) { newStartingDate = startingDate; newEndingDate = endingDate.addDays(-settlementPeriod); report.setDateFilter(newStartingDate, newEndingDate); // search for matching buy transactions year earlier QList transactions = file->transactionList(report); shList[BuysOfOwned] = shList[LongTermBuysOfSells]; foreach (const auto transaction, transactions) { MyMoneySplit shareSplit = transaction.splitByAccount(account.id()); MyMoneySplit assetAccountSplit; QList feeSplits; QList interestSplits; MyMoneySecurity security; MyMoneySecurity currency; eMyMoney::Split::InvestmentTransactionType transactionType; KMyMoneyUtils::dissectTransaction(transaction, shareSplit, assetAccountSplit, feeSplits, interestSplits, security, currency, transactionType); QDate postDate = transaction.postDate(); MyMoneyMoney price; if (m_config.isConvertCurrency()) price = account.baseCurrencyPrice(postDate); //we only need base currency because the value is in deep currency else price = MyMoneyMoney::ONE; MyMoneyMoney value = assetAccountSplit.value() * price; MyMoneyMoney shares = shareSplit.shares(); if (transactionType == eMyMoney::Split::InvestmentTransactionType::SellShares) { if ((shList.at(LongTermSellsOfBuys) + shares).abs() >= shList.at(LongTermBuysOfSells)) { // add partially sold long-term shares cfList[LongTermSellsOfBuys].append(CashFlowListItem(postDate, (shList.at(LongTermSellsOfBuys).abs() - shList.at(LongTermBuysOfSells)) / shares * value)); shList[LongTermSellsOfBuys] = shList.at(LongTermBuysOfSells); break; } else { // add wholly sold long-term shares cfList[LongTermSellsOfBuys].append(CashFlowListItem(postDate, value)); shList[LongTermSellsOfBuys] += shares; } } else if (transactionType == eMyMoney::Split::InvestmentTransactionType::RemoveShares) { if ((shList.at(LongTermSellsOfBuys) + shares).abs() >= shList.at(LongTermBuysOfSells)) { shList[LongTermSellsOfBuys] = shList.at(LongTermBuysOfSells); break; } else shList[LongTermSellsOfBuys] += shares; } } } shList[BuysOfOwned] = stashedBuysOfOwned; report.setDateFilter(startingDate, endingDate); // reset data filter for next security return; } void QueryTable::constructPerformanceRow(const ReportAccount& account, TableRow& result, CashFlowList &all) const { MyMoneyReport report = m_config; QDate startingDate; QDate endingDate; report.validDateRange(startingDate, endingDate); startingDate = startingDate.addDays(-1); MyMoneyFile* file = MyMoneyFile::instance(); //get fraction depending on type of account int fraction = account.currency().smallestAccountFraction(); MyMoneyMoney price; 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); QList cfList; QList shList; sumInvestmentValues(account, cfList, shList); MyMoneyMoney buysTotal; MyMoneyMoney sellsTotal; MyMoneyMoney cashIncomeTotal; MyMoneyMoney reinvestIncomeTotal; switch (m_config.investmentSum()) { case eMyMoney::Report::InvestmentSum::OwnedAndSold: buysTotal = cfList.at(BuysOfSells).total() + cfList.at(BuysOfOwned).total(); sellsTotal = cfList.at(Sells).total(); cashIncomeTotal = cfList.at(CashIncome).total(); reinvestIncomeTotal = cfList.at(ReinvestIncome).total(); startingBal = MyMoneyMoney(); if (buysTotal.isZero() && sellsTotal.isZero() && cashIncomeTotal.isZero() && reinvestIncomeTotal.isZero()) return; all.append(cfList.at(BuysOfSells)); all.append(cfList.at(BuysOfOwned)); all.append(cfList.at(Sells)); all.append(cfList.at(CashIncome)); result[ctSells] = sellsTotal.toString(); result[ctCashIncome] = cashIncomeTotal.toString(); result[ctReinvestIncome] = reinvestIncomeTotal.toString(); result[ctEndingBalance] = endingBal.toString(); break; case eMyMoney::Report::InvestmentSum::Owned: buysTotal = cfList.at(BuysOfOwned).total(); startingBal = MyMoneyMoney(); if (buysTotal.isZero() && endingBal.isZero()) return; all.append(cfList.at(BuysOfOwned)); all.append(CashFlowListItem(endingDate, endingBal)); result[ctReinvestIncome] = reinvestIncomeTotal.toString(); result[ctMarketValue] = endingBal.toString(); break; case eMyMoney::Report::InvestmentSum::Sold: buysTotal = cfList.at(BuysOfSells).total(); sellsTotal = cfList.at(Sells).total(); cashIncomeTotal = cfList.at(CashIncome).total(); startingBal = endingBal = MyMoneyMoney(); // check if there are any meaningfull values before adding them to results if (buysTotal.isZero() && sellsTotal.isZero() && cashIncomeTotal.isZero()) return; all.append(cfList.at(BuysOfSells)); all.append(cfList.at(Sells)); all.append(cfList.at(CashIncome)); result[ctSells] = sellsTotal.toString(); result[ctCashIncome] = cashIncomeTotal.toString(); break; case eMyMoney::Report::InvestmentSum::Period: default: buysTotal = cfList.at(Buys).total(); sellsTotal = cfList.at(Sells).total(); cashIncomeTotal = cfList.at(CashIncome).total(); reinvestIncomeTotal = cfList.at(ReinvestIncome).total(); if (buysTotal.isZero() && sellsTotal.isZero() && cashIncomeTotal.isZero() && reinvestIncomeTotal.isZero() && startingBal.isZero() && endingBal.isZero()) return; all.append(cfList.at(Buys)); all.append(cfList.at(Sells)); all.append(cfList.at(CashIncome)); all.append(CashFlowListItem(startingDate, -startingBal)); all.append(CashFlowListItem(endingDate, endingBal)); result[ctSells] = sellsTotal.toString(); result[ctCashIncome] = cashIncomeTotal.toString(); result[ctReinvestIncome] = reinvestIncomeTotal.toString(); result[ctStartingBalance] = startingBal.toString(); result[ctEndingBalance] = endingBal.toString(); break; } MyMoneyMoney returnInvestment = helperROI(buysTotal - reinvestIncomeTotal, sellsTotal, startingBal, endingBal, cashIncomeTotal); - MyMoneyMoney annualReturn = helperIRR(all); result[ctBuys] = buysTotal.toString(); - result[ctReturn] = annualReturn.toString(); + result[ctReturn] = helperIRR(all); result[ctReturnInvestment] = returnInvestment.toString(); result[ctEquityType] = MyMoneySecurity::securityTypeToString(file->security(account.currencyId()).securityType()); } void QueryTable::constructCapitalGainRow(const ReportAccount& account, TableRow& result) const { MyMoneyFile* file = MyMoneyFile::instance(); QList cfList; QList shList; sumInvestmentValues(account, cfList, shList); MyMoneyMoney buysTotal = cfList.at(BuysOfSells).total(); MyMoneyMoney sellsTotal = cfList.at(Sells).total(); MyMoneyMoney longTermBuysOfSellsTotal = cfList.at(LongTermBuysOfSells).total(); MyMoneyMoney longTermSellsOfBuys = cfList.at(LongTermSellsOfBuys).total(); switch (m_config.investmentSum()) { case eMyMoney::Report::InvestmentSum::Owned: { if (shList.at(BuysOfOwned).isZero()) return; MyMoneyReport report = m_config; QDate startingDate; QDate endingDate; report.validDateRange(startingDate, endingDate); //get fraction depending on type of account int fraction = account.currency().smallestAccountFraction(); MyMoneyMoney price; //calculate ending balance if (m_config.isConvertCurrency()) price = account.deepCurrencyPrice(endingDate) * account.baseCurrencyPrice(endingDate); else price = account.deepCurrencyPrice(endingDate); MyMoneyMoney endingBal = shList.at(BuysOfOwned) * price; //convert to lowest fraction endingBal = endingBal.convert(fraction); buysTotal = cfList.at(BuysOfOwned).total() - cfList.at(ReinvestIncome).total(); int pricePrecision = file->security(account.currencyId()).pricePrecision(); result[ctBuys] = buysTotal.toString(); result[ctShares] = shList.at(BuysOfOwned).toString(); result[ctBuyPrice] = (buysTotal.abs() / shList.at(BuysOfOwned)).convertPrecision(pricePrecision).toString(); result[ctLastPrice] = price.toString(); result[ctMarketValue] = endingBal.toString(); result[ctCapitalGain] = (buysTotal + endingBal).toString(); result[ctPercentageGain] = ((buysTotal + endingBal)/buysTotal.abs()).toString(); break; } case eMyMoney::Report::InvestmentSum::Sold: default: buysTotal = cfList.at(BuysOfSells).total() - cfList.at(ReinvestIncome).total(); sellsTotal = cfList.at(Sells).total(); longTermBuysOfSellsTotal = cfList.at(LongTermBuysOfSells).total(); longTermSellsOfBuys = cfList.at(LongTermSellsOfBuys).total(); // check if there are any meaningfull values before adding them to results if (buysTotal.isZero() && sellsTotal.isZero() && longTermBuysOfSellsTotal.isZero() && longTermSellsOfBuys.isZero()) return; result[ctBuys] = buysTotal.toString(); result[ctSells] = sellsTotal.toString(); result[ctCapitalGain] = (buysTotal + sellsTotal).toString(); if (m_config.isShowingSTLTCapitalGains()) { result[ctBuysLT] = longTermBuysOfSellsTotal.toString(); result[ctSellsLT] = longTermSellsOfBuys.toString(); result[ctCapitalGainLT] = (longTermBuysOfSellsTotal + longTermSellsOfBuys).toString(); result[ctBuysST] = (buysTotal - longTermBuysOfSellsTotal).toString(); result[ctSellsST] = (sellsTotal - longTermSellsOfBuys).toString(); result[ctCapitalGainST] = ((buysTotal - longTermBuysOfSellsTotal) + (sellsTotal - longTermSellsOfBuys)).toString(); } break; } result[ctEquityType] = MyMoneySecurity::securityTypeToString(file->security(account.currencyId()).securityType()); } void QueryTable::constructAccountTable() { MyMoneyFile* file = MyMoneyFile::instance(); //make sure we have all subaccounts of investment accounts includeInvestmentSubAccounts(); QMap> currencyCashFlow; // for total calculation QList accounts; file->accountList(accounts); for (auto it_account = accounts.constBegin(); it_account != accounts.constEnd(); ++it_account) { // 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 ((*it_account).isAssetLiability() && m_config.includes((*it_account)) && (*it_account).accountType() != eMyMoney::Account::Type::Investment) { // 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. MyMoneyMoney shares = file->balance((*it_account).id(), m_config.toDate()); if (shares.isZero() && (*it_account).isClosed()) continue; ReportAccount account(*it_account); TableRow qaccountrow; CashFlowList accountCashflow; // for total calculation switch(m_config.queryColumns()) { case eMyMoney::Report::QueryColumn::Performance: { constructPerformanceRow(account, qaccountrow, accountCashflow); if (!qaccountrow.isEmpty()) { // assuming that that report is grouped by topaccount qaccountrow[ctTopAccount] = account.topParentName(); if (!m_containsNonBaseCurrency && account.currency().id() != file->baseCurrency().id()) m_containsNonBaseCurrency = true; if (m_config.isConvertCurrency()) qaccountrow[ctCurrency] = file->baseCurrency().id(); else qaccountrow[ctCurrency] = account.currency().id(); if (!currencyCashFlow.value(qaccountrow.value(ctCurrency)).contains(qaccountrow.value(ctTopAccount))) currencyCashFlow[qaccountrow.value(ctCurrency)].insert(qaccountrow.value(ctTopAccount), accountCashflow); // create cashflow for unknown account... else currencyCashFlow[qaccountrow.value(ctCurrency)][qaccountrow.value(ctTopAccount)] += accountCashflow; // ...or add cashflow for known account } break; } case eMyMoney::Report::QueryColumn::CapitalGain: constructCapitalGainRow(account, qaccountrow); break; default: { //get fraction for account int fraction = account.currency().smallestAccountFraction() != -1 ? account.currency().smallestAccountFraction() : file->baseCurrency().smallestAccountFraction(); MyMoneyMoney netprice = account.deepCurrencyPrice(m_config.toDate()); if (m_config.isConvertCurrency() && account.isForeignCurrency()) netprice *= account.baseCurrencyPrice(m_config.toDate()); // display currency is base currency, so set the price netprice = netprice.reduce(); shares = shares.reduce(); int pricePrecision = file->security(account.currencyId()).pricePrecision(); qaccountrow[ctPrice] = netprice.convertPrecision(pricePrecision).toString(); qaccountrow[ctValue] = (netprice * shares).convert(fraction).toString(); qaccountrow[ctShares] = shares.toString(); QString iid = account.institutionId(); // If an account does not have an institution, get it from the top-parent. if (iid.isEmpty() && !account.isTopLevel()) iid = account.topParent().institutionId(); if (iid.isEmpty()) qaccountrow[ctInstitution] = i18nc("No institution", "None"); else qaccountrow[ctInstitution] = file->institution(iid).name(); qaccountrow[ctType] = MyMoneyAccount::accountTypeToString(account.accountType()); } } if (qaccountrow.isEmpty()) // don't add the account if there are no calculated values continue; qaccountrow[ctRank] = QLatin1Char('1'); qaccountrow[ctAccount] = account.name(); qaccountrow[ctAccountID] = account.id(); qaccountrow[ctTopAccount] = account.topParentName(); if (!m_containsNonBaseCurrency && account.currency().id() != file->baseCurrency().id()) m_containsNonBaseCurrency = true; if (m_config.isConvertCurrency()) qaccountrow[ctCurrency] = file->baseCurrency().id(); else qaccountrow[ctCurrency] = account.currency().id(); m_rows.append(qaccountrow); } } if (m_config.queryColumns() == eMyMoney::Report::QueryColumn::Performance && m_config.isShowingColumnTotals()) { TableRow qtotalsrow; qtotalsrow[ctRank] = QLatin1Char('4'); // add identification of row as total QMap currencyGrandCashFlow; QMap>::iterator currencyAccGrp = currencyCashFlow.begin(); while (currencyAccGrp != currencyCashFlow.end()) { // convert map of top accounts with cashflows to TableRow for (QMap::iterator topAccount = (*currencyAccGrp).begin(); topAccount != (*currencyAccGrp).end(); ++topAccount) { qtotalsrow[ctTopAccount] = topAccount.key(); - qtotalsrow[ctReturn] = helperIRR(topAccount.value()).toString(); + qtotalsrow[ctReturn] = helperIRR(topAccount.value()); qtotalsrow[ctCurrency] = currencyAccGrp.key(); currencyGrandCashFlow[currencyAccGrp.key()] += topAccount.value(); // cumulative sum of cashflows of each topaccount m_rows.append(qtotalsrow); // rows aren't sorted yet, so no problem with adding them randomly at the end } ++currencyAccGrp; } QMap::iterator currencyGrp = currencyGrandCashFlow.begin(); qtotalsrow[ctTopAccount].clear(); // empty topaccount because it's grand cashflow while (currencyGrp != currencyGrandCashFlow.end()) { - qtotalsrow[ctReturn] = helperIRR(currencyGrp.value()).toString(); + qtotalsrow[ctReturn] = helperIRR(currencyGrp.value()); qtotalsrow[ctCurrency] = currencyGrp.key(); m_rows.append(qtotalsrow); ++currencyGrp; } } } 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[ctID] = qS[ctID] = (* it_transaction).id(); qA[ctEntryDate] = qS[ctEntryDate] = (* it_transaction).entryDate().toString(Qt::ISODate); qA[ctPostDate] = qS[ctPostDate] = (* it_transaction).postDate().toString(Qt::ISODate); qA[ctCommodity] = qS[ctCommodity] = (* it_transaction).commodity(); pd = (* it_transaction).postDate(); qA[ctMonth] = qS[ctMonth] = i18n("Month of %1", QDate(pd.year(), pd.month(), 1).toString(Qt::ISODate)); qA[ctWeek] = qS[ctWeek] = i18n("Week of %1", pd.addDays(1 - pd.dayOfWeek()).toString(Qt::ISODate)); if (!m_containsNonBaseCurrency && (*it_transaction).commodity() != file->baseCurrency().id()) m_containsNonBaseCurrency = true; if (report.isConvertCurrency()) qA[ctCurrency] = qS[ctCurrency] = file->baseCurrency().id(); else qA[ctCurrency] = qS[ctCurrency] = (*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.constBegin(), myBegin = splits.constEnd(); it_split != splits.constEnd(); ++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() & eMyMoney::Report::QueryColumn::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(); int pricePrecision = file->security(splitAcc.currencyId()).pricePrecision(); qA[ctAction] = (*it_split).action(); qA[ctShares] = shares.isZero() ? QString() : (*it_split).shares().toString(); qA[ctPrice] = shares.isZero() ? QString() : xr.convertPrecision(pricePrecision).toString(); if (((*it_split).action() == MyMoneySplit::actionName(eMyMoney::Split::Action::BuyShares)) && (*it_split).shares().isNegative()) qA[ctAction] = "Sell"; qA[ctInvestAccount] = splitAcc.parent().name(); } include_me = m_config.includes(splitAcc); a_fullname = splitAcc.fullName(); a_memo = (*it_split).memo(); int pricePrecision = file->security(splitAcc.currencyId()).pricePrecision(); qA[ctPrice] = xr.convertPrecision(pricePrecision).toString(); qA[ctAccount] = splitAcc.name(); qA[ctAccountID] = splitAcc.id(); qA[ctTopAccount] = splitAcc.topParentName(); qA[ctInstitution] = 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; foreach(const auto tagId, tagIdList) { qA[ctTag] += delimiter + file->tag(tagId).name().simplified(); delimiter = QLatin1Char(','); } qA[ctPayee] = payee.isEmpty() ? i18n("[Empty Payee]") : file->payee(payee).name().simplified(); qA[ctReconcileDate] = (*it_split).reconcileDate().toString(Qt::ISODate); qA[ctReconcileFlag] = KMyMoneyUtils::reconcileStateToString((*it_split).reconcileFlag(), true); qA[ctNumber] = (*it_split).number(); qA[ctMemo] = a_memo; qS[ctReconcileDate] = qA[ctReconcileDate]; qS[ctReconcileFlag] = qA[ctReconcileFlag]; qS[ctNumber] = qA[ctNumber]; qS[ctTopCategory] = 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[ctValue] = ((*it_split).shares() * xr).convert(fraction).toString(); qA[ctRank] = QLatin1Char('1'); //fill in account information if (! splitAcc.isIncomeExpense() && it_split != myBegin) { qA[ctAccount] = ((*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[ctAccount] = i18n("[Split Transaction]"); } else { //fill the account name of the second split QList::const_iterator tempSplit = splits.constBegin(); //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[ctAccount] = ((*it_split).shares().isNegative()) ? i18n("Transfer to %1", tempSplitAcc.fullName()) : i18n("Transfer from %1", tempSplitAcc.fullName()); } else { qA[ctAccount] = tempSplitAcc.fullName(); } } } else { //in any other case, fill in the account name of the main split qA[ctAccount] = myBeginAcc.fullName(); } //category data is always the one of the split qA [ctCategory] = splitAcc.fullName(); qA [ctTopCategory] = splitAcc.topParentName(); qA [ctCategoryType] = MyMoneyAccount::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 eMyMoney::Report::RowType::Account: case eMyMoney::Report::RowType::TopAccount: break; // case eMyMoney::Report::RowType::Category: // 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); for (auto 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 if (!m_containsNonBaseCurrency && account.currency().id() != file->baseCurrency().id()) m_containsNonBaseCurrency = true; if (m_config.isConvertCurrency()) qA[ctCurrency] = file->baseCurrency().id(); else qA[ctCurrency] = account.currency().id(); qA[ctAccountID] = account.id(); qA[ctAccount] = account.name(); qA[ctTopAccount] = account.topParentName(); qA[ctInstitution] = institution.isEmpty() ? i18n("No Institution") : file->institution(institution).name(); qA[ctRank] = QLatin1Char('0'); int pricePrecision = file->security(account.currencyId()).pricePrecision(); qA[ctPrice] = startPrice.convertPrecision(pricePrecision).toString(); if (account.isInvest()) { qA[ctShares] = startShares.toString(); } qA[ctPostDate] = strStartDate; qA[ctBalance] = startBalance.convert(fraction).toString(); qA[ctValue].clear(); qA[ctID] = QLatin1Char('A'); m_rows += qA; qA[ctRank] = QLatin1Char('3'); //ending balance qA[ctPrice] = endPrice.convertPrecision(pricePrecision).toString(); if (account.isInvest()) { qA[ctShares] = endShares.toString(); } qA[ctPostDate] = strEndDate; qA[ctBalance] = endBalance.toString(); qA[ctID] = QLatin1Char('Z'); m_rows += qA; } } } diff --git a/kmymoney/plugins/views/reports/core/querytable.h b/kmymoney/plugins/views/reports/core/querytable.h index b5420bc30..f1078b291 100644 --- a/kmymoney/plugins/views/reports/core/querytable.h +++ b/kmymoney/plugins/views/reports/core/querytable.h @@ -1,171 +1,82 @@ /* * Copyright 2005 Ace Jones * Copyright 2017-2018 Łukasz Wojniłowicz * * 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 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 "listtable.h" #include "mymoneymoney.h" class MyMoneyReport; +class CashFlowList; namespace reports { class ReportAccount; -class CashFlowList; /** * 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: explicit QueryTable(const MyMoneyReport&); void init(); protected: void constructAccountTable(); void constructTotalRows(); void constructTransactionTable(); void sumInvestmentValues(const ReportAccount &account, QList &cfList, QList &shList) const; void constructPerformanceRow(const ReportAccount& account, TableRow& result, CashFlowList &all) const; void constructCapitalGainRow(const ReportAccount& account, TableRow& result) const; MyMoneyMoney helperROI(const MyMoneyMoney& buys, const MyMoneyMoney& sells, const MyMoneyMoney& startingBal, const MyMoneyMoney& endingBal, const MyMoneyMoney& cashIncome) const; - MyMoneyMoney helperIRR(const CashFlowList& all) const; + QString helperIRR(const CashFlowList& all) const; void constructSplitsTable(); bool linkEntries() const final override { return true; } private: enum InvestmentValue {Buys = 0, Sells, BuysOfSells, SellsOfBuys, LongTermBuysOfSells, LongTermSellsOfBuys, BuysOfOwned, ReinvestIncome, CashIncome, End}; }; -// -// 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/plugins/views/reports/core/tests/querytable-test.cpp b/kmymoney/plugins/views/reports/core/tests/querytable-test.cpp index 7b102647f..8db4deff3 100644 --- a/kmymoney/plugins/views/reports/core/tests/querytable-test.cpp +++ b/kmymoney/plugins/views/reports/core/tests/querytable-test.cpp @@ -1,936 +1,977 @@ /* * Copyright 2005-2018 Thomas Baumgart * Copyright 2005-2006 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. * * 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 . */ #include "querytable-test.h" #include #include #include #include "tests/testutilities.h" +#include "cashflowlist.h" #include "querytable.h" #include "mymoneyinstitution.h" #include "mymoneyaccount.h" #include "mymoneysecurity.h" #include "mymoneyprice.h" #include "mymoneystoragedump.h" #include "mymoneyreport.h" #include "mymoneysplit.h" #include "mymoneypayee.h" #include "mymoneystatement.h" #include "mymoneyexception.h" #include "kmymoneysettings.h" using namespace reports; using namespace test; QTEST_GUILESS_MAIN(QueryTableTest) void writeTabletoHTML(const QueryTable& table, const QString& _filename = QString()) { static unsigned filenumber = 1; QString filename = _filename; if (filename.isEmpty()) { filename = QString::fromLatin1("report-%1.html").arg(filenumber, 2, 10,QLatin1Char('0')); ++filenumber; } QFile g(filename); g.open(QIODevice::WriteOnly); QTextStream(&g) << table.renderHTML(); g.close(); } void writeTabletoCSV(const QueryTable& table, const QString& _filename = QString()) { static unsigned filenumber = 1; QString filename = _filename; if (filename.isEmpty()) { filename = QString::fromLatin1("report-%1.csv").arg(filenumber, 2, 10,QLatin1Char('0')); ++filenumber; } QFile g(filename); g.open(QIODevice::WriteOnly); QTextStream(&g) << table.renderCSV(); g.close(); } void QueryTableTest::setup() { } void QueryTableTest::init() { storage = new MyMoneyStorageMgr; file = MyMoneyFile::instance(); file->attachStorage(storage); MyMoneyFileTransaction ft; file->addCurrency(MyMoneySecurity("CAD", "Canadian Dollar", "C$")); file->addCurrency(MyMoneySecurity("USD", "US Dollar", "$")); file->addCurrency(MyMoneySecurity("JPY", "Japanese Yen", QChar(0x00A5), 1)); file->addCurrency(MyMoneySecurity("GBP", "British Pound", "#")); file->setBaseCurrency(file->currency("USD")); MyMoneyPayee payeeTest; payeeTest.setName("Test Payee"); file->addPayee(payeeTest); MyMoneyPayee payeeTest2; payeeTest2.setName("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"), eMyMoney::Account::Type::Checkings, moCheckingOpen, QDate(2004, 5, 15), acAsset); acCredit = makeAccount(QString("Credit Card"), eMyMoney::Account::Type::CreditCard, moCreditOpen, QDate(2004, 7, 15), acLiability); acSolo = makeAccount(QString("Solo"), eMyMoney::Account::Type::Expense, MyMoneyMoney(), QDate(2004, 1, 11), acExpense); acParent = makeAccount(QString("Parent"), eMyMoney::Account::Type::Expense, MyMoneyMoney(), QDate(2004, 1, 11), acExpense); acChild = makeAccount(QString("Child"), eMyMoney::Account::Type::Expense, MyMoneyMoney(), QDate(2004, 2, 11), acParent); acForeign = makeAccount(QString("Foreign"), eMyMoney::Account::Type::Expense, MyMoneyMoney(), QDate(2004, 1, 11), acExpense); acTax = makeAccount(QString("Tax"), eMyMoney::Account::Type::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::actionName(eMyMoney::Split::Action::Withdrawal), moSolo, acChecking, acSolo); TransactionHelper t2q1(QDate(2004, 2, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent1, acCredit, acParent); TransactionHelper t3q1(QDate(2004, 3, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent2, acCredit, acParent); TransactionHelper t4y1(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moChild, acCredit, acChild); TransactionHelper t1q2(QDate(2004, 4, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moSolo, acChecking, acSolo); TransactionHelper t2q2(QDate(2004, 5, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent1, acCredit, acParent); TransactionHelper t3q2(QDate(2004, 6, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent2, acCredit, acParent); TransactionHelper t4q2(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moChild, acCredit, acChild); TransactionHelper t1y2(QDate(2005, 1, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moSolo, acChecking, acSolo); TransactionHelper t2y2(QDate(2005, 5, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent1, acCredit, acParent); TransactionHelper t3y2(QDate(2005, 9, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent2, acCredit, acParent); TransactionHelper t4y2(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moChild, acCredit, acChild); unsigned cols; MyMoneyReport filter; filter.setRowType(eMyMoney::Report::RowType::Category); cols = eMyMoney::Report::QueryColumn::Number | eMyMoney::Report::QueryColumn::Payee | eMyMoney::Report::QueryColumn::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() == 19); QVERIFY(rows[0][ListTable::ctCategoryType] == "Expense"); QVERIFY(rows[0][ListTable::ctCategory] == "Parent"); QVERIFY(rows[0][ListTable::ctPostDate] == "2004-02-01"); QVERIFY(rows[14][ListTable::ctCategoryType] == "Expense"); QVERIFY(rows[14][ListTable::ctCategory] == "Solo"); QVERIFY(rows[14][ListTable::ctPostDate] == "2005-01-01"); QVERIFY(MyMoneyMoney(rows[6][ListTable::ctValue]) == -(moParent1 + moParent2) * 3); QVERIFY(MyMoneyMoney(rows[10][ListTable::ctValue]) == -(moChild) * 3); QVERIFY(MyMoneyMoney(rows[16][ListTable::ctValue]) == -(moSolo) * 3); QVERIFY(MyMoneyMoney(rows[17][ListTable::ctValue]) == -(moParent1 + moParent2 + moSolo + moChild) * 3); QVERIFY(MyMoneyMoney(rows[18][ListTable::ctValue]) == -(moParent1 + moParent2 + moSolo + moChild) * 3 + moCheckingOpen + moCreditOpen); filter.setRowType(eMyMoney::Report::RowType::TopCategory); cols = eMyMoney::Report::QueryColumn::Number | eMyMoney::Report::QueryColumn::Payee | eMyMoney::Report::QueryColumn::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() == 16); QVERIFY(rows[0][ListTable::ctCategoryType] == "Expense"); QVERIFY(rows[0][ListTable::ctTopCategory] == "Parent"); QVERIFY(rows[0][ListTable::ctPostDate] == "2004-02-01"); QVERIFY(rows[8][ListTable::ctCategoryType] == "Expense"); QVERIFY(rows[8][ListTable::ctTopCategory] == "Parent"); QVERIFY(rows[8][ListTable::ctPostDate] == "2005-09-01"); QVERIFY(rows[12][ListTable::ctCategoryType] == "Expense"); QVERIFY(rows[12][ListTable::ctTopCategory] == "Solo"); QVERIFY(rows[12][ListTable::ctPostDate] == "2005-01-01"); QVERIFY(MyMoneyMoney(rows[9][ListTable::ctValue]) == -(moParent1 + moParent2 + moChild) * 3); QVERIFY(MyMoneyMoney(rows[13][ListTable::ctValue]) == -(moSolo) * 3); QVERIFY(MyMoneyMoney(rows[14][ListTable::ctValue]) == -(moParent1 + moParent2 + moSolo + moChild) * 3); QVERIFY(MyMoneyMoney(rows[15][ListTable::ctValue]) == -(moParent1 + moParent2 + moSolo + moChild) * 3 + moCheckingOpen + moCreditOpen); filter.setRowType(eMyMoney::Report::RowType::Account); filter.setName("Transactions by Account"); cols = eMyMoney::Report::QueryColumn::Number | eMyMoney::Report::QueryColumn::Payee | eMyMoney::Report::QueryColumn::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() == 19); QVERIFY(rows[1][ListTable::ctAccount] == "Checking Account"); QVERIFY(rows[1][ListTable::ctCategory] == "Solo"); QVERIFY(rows[1][ListTable::ctPostDate] == "2004-01-01"); QVERIFY(rows[15][ListTable::ctAccount] == "Credit Card"); QVERIFY(rows[15][ListTable::ctCategory] == "Parent"); QVERIFY(rows[15][ListTable::ctPostDate] == "2005-09-01"); #else QVERIFY(rows.count() == 12); QVERIFY(rows[0][ListTable::ctAccount] == "Checking Account"); QVERIFY(rows[0][ListTable::ctCategory] == "Solo"); QVERIFY(rows[0][ListTable::ctPostDate] == "2004-01-01"); QVERIFY(rows[11][ListTable::ctAccount] == "Credit Card"); QVERIFY(rows[11][ListTable::ctCategory] == "Parent"); QVERIFY(rows[11][ListTable::ctPostDate] == "2005-09-01"); #endif QVERIFY(MyMoneyMoney(rows[5][ListTable::ctValue]) == -(moSolo) * 3 + moCheckingOpen); QVERIFY(MyMoneyMoney(rows[17][ListTable::ctValue]) == -(moParent1 + moParent2 + moChild) * 3 + moCreditOpen); QVERIFY(MyMoneyMoney(rows[18][ListTable::ctValue]) == -(moParent1 + moParent2 + moSolo + moChild) * 3 + moCheckingOpen + moCreditOpen); filter.setRowType(eMyMoney::Report::RowType::Payee); filter.setName("Transactions by Payee"); cols = eMyMoney::Report::QueryColumn::Number | eMyMoney::Report::QueryColumn::Memo | eMyMoney::Report::QueryColumn::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() == 14); QVERIFY(rows[0][ListTable::ctPayee] == "Test Payee"); QVERIFY(rows[0][ListTable::ctCategory] == "Solo"); QVERIFY(rows[0][ListTable::ctPostDate] == "2004-01-01"); QVERIFY(rows[7][ListTable::ctPayee] == "Test Payee"); QVERIFY(rows[7][ListTable::ctCategory] == "Parent: Child"); QVERIFY(rows[7][ListTable::ctPostDate] == "2004-11-07"); QVERIFY(rows[11][ListTable::ctPayee] == "Test Payee"); QVERIFY(rows[11][ListTable::ctCategory] == "Parent"); QVERIFY(rows[11][ListTable::ctPostDate] == "2005-09-01"); QVERIFY(MyMoneyMoney(rows[12][ListTable::ctValue]) == -(moParent1 + moParent2 + moSolo + moChild) * 3); QVERIFY(MyMoneyMoney(rows[13][ListTable::ctValue]) == -(moParent1 + moParent2 + moSolo + moChild) * 3 + moCheckingOpen + moCreditOpen); filter.setRowType(eMyMoney::Report::RowType::Month); filter.setName("Transactions by Month"); cols = eMyMoney::Report::QueryColumn::Number | eMyMoney::Report::QueryColumn::Payee | eMyMoney::Report::QueryColumn::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() == 23); QVERIFY(rows[0][ListTable::ctPayee] == "Test Payee"); QVERIFY(rows[0][ListTable::ctCategory] == "Solo"); QVERIFY(rows[0][ListTable::ctPostDate] == "2004-01-01"); QVERIFY(rows[12][ListTable::ctPayee] == "Test Payee"); QVERIFY(rows[12][ListTable::ctCategory] == "Parent: Child"); QVERIFY(rows[12][ListTable::ctPostDate] == "2004-11-07"); QVERIFY(rows[20][ListTable::ctPayee] == "Test Payee"); QVERIFY(rows[20][ListTable::ctCategory] == "Parent"); QVERIFY(rows[20][ListTable::ctPostDate] == "2005-09-01"); QVERIFY(MyMoneyMoney(rows[1][ListTable::ctValue]) == -moSolo); QVERIFY(MyMoneyMoney(rows[15][ListTable::ctValue]) == -(moChild) * 3); QVERIFY(MyMoneyMoney(rows[9][ListTable::ctValue]) == -moParent1 + moCheckingOpen); QVERIFY(MyMoneyMoney(rows[22][ListTable::ctValue]) == -(moParent1 + moParent2 + moSolo + moChild) * 3 + moCheckingOpen + moCreditOpen); filter.setRowType(eMyMoney::Report::RowType::Week); filter.setName("Transactions by Week"); cols = eMyMoney::Report::QueryColumn::Number | eMyMoney::Report::QueryColumn::Payee | eMyMoney::Report::QueryColumn::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() == 23); QVERIFY(rows[0][ListTable::ctPayee] == "Test Payee"); QVERIFY(rows[0][ListTable::ctCategory] == "Solo"); QVERIFY(rows[0][ListTable::ctPostDate] == "2004-01-01"); QVERIFY(rows[20][ListTable::ctPayee] == "Test Payee"); QVERIFY(rows[20][ListTable::ctCategory] == "Parent"); QVERIFY(rows[20][ListTable::ctPostDate] == "2005-09-01"); QVERIFY(MyMoneyMoney(rows[1][ListTable::ctValue]) == -moSolo); QVERIFY(MyMoneyMoney(rows[15][ListTable::ctValue]) == -(moChild) * 3); QVERIFY(MyMoneyMoney(rows[21][ListTable::ctValue]) == -moParent2); QVERIFY(MyMoneyMoney(rows[22][ListTable::ctValue]) == -(moParent1 + moParent2 + moSolo + moChild) * 3 + moCheckingOpen + moCreditOpen); } catch (const MyMoneyException &e) { QFAIL(e.what()); } // Test querytable::TableRow::operator> and operator== QueryTable::TableRow low; low[ListTable::ctPrice] = 'A'; low[ListTable::ctLastPrice] = 'B'; low[ListTable::ctBuyPrice] = 'C'; QueryTable::TableRow high; high[ListTable::ctPrice] = 'A'; high[ListTable::ctLastPrice] = 'C'; high[ListTable::ctBuyPrice] = 'B'; QueryTable::TableRow::setSortCriteria({ListTable::ctPrice, ListTable::ctLastPrice, ListTable::ctBuyPrice}); 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(eMyMoney::Report::RowType::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() == 6); QVERIFY(rows[0][ListTable::ctAccount] == "Checking Account"); QVERIFY(MyMoneyMoney(rows[0][ListTable::ctValue]) == moCheckingOpen); QVERIFY(rows[0][ListTable::ctEquityType].isEmpty()); QVERIFY(rows[2][ListTable::ctAccount] == "Credit Card"); QVERIFY(MyMoneyMoney(rows[1][ListTable::ctValue]) == moCreditOpen); QVERIFY(rows[2][ListTable::ctEquityType].isEmpty()); QVERIFY(MyMoneyMoney(rows[4][ListTable::ctValue]) == moCheckingOpen + moCreditOpen); QVERIFY(MyMoneyMoney(rows[5][ListTable::ctValue]) == moCheckingOpen + moCreditOpen); // // Adding in transactions // TransactionHelper t1q1(QDate(2004, 1, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moSolo, acChecking, acSolo); TransactionHelper t2q1(QDate(2004, 2, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent1, acCredit, acParent); TransactionHelper t3q1(QDate(2004, 3, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent2, acCredit, acParent); TransactionHelper t4y1(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moChild, acCredit, acChild); TransactionHelper t1q2(QDate(2004, 4, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moSolo, acChecking, acSolo); TransactionHelper t2q2(QDate(2004, 5, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent1, acCredit, acParent); TransactionHelper t3q2(QDate(2004, 6, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent2, acCredit, acParent); TransactionHelper t4q2(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moChild, acCredit, acChild); TransactionHelper t1y2(QDate(2005, 1, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moSolo, acChecking, acSolo); TransactionHelper t2y2(QDate(2005, 5, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent1, acCredit, acParent); TransactionHelper t3y2(QDate(2005, 9, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent2, acCredit, acParent); TransactionHelper t4y2(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moChild, acCredit, acChild); filter.setRowType(eMyMoney::Report::RowType::Institution); filter.setName("Accounts by Institution (With Transactions)"); XMLandback(filter); QueryTable qtbl_2(filter); rows = qtbl_2.rows(); QVERIFY(rows.count() == 6); QVERIFY(rows[0][ListTable::ctAccount] == "Checking Account"); QVERIFY(MyMoneyMoney(rows[0][ListTable::ctValue]) == (moCheckingOpen - moSolo*3)); QVERIFY(rows[2][ListTable::ctAccount] == "Credit Card"); QVERIFY(MyMoneyMoney(rows[2][ListTable::ctValue]) == (moCreditOpen - (moParent1 + moParent2 + moChild) * 3)); QVERIFY(MyMoneyMoney(rows[5][ListTable::ctValue]) == moCheckingOpen + moCreditOpen - (moParent1 + moParent2 + moSolo + moChild) * 3); // // Account TYPES // filter.setRowType(eMyMoney::Report::RowType::AccountType); filter.setName("Accounts by Type"); XMLandback(filter); QueryTable qtbl_3(filter); rows = qtbl_3.rows(); QVERIFY(rows.count() == 5); QVERIFY(rows[0][ListTable::ctAccount] == "Checking Account"); QVERIFY(MyMoneyMoney(rows[0][ListTable::ctValue]) == (moCheckingOpen - moSolo * 3)); QVERIFY(rows[2][ListTable::ctAccount] == "Credit Card"); QVERIFY(MyMoneyMoney(rows[2][ListTable::ctValue]) == (moCreditOpen - (moParent1 + moParent2 + moChild) * 3)); QVERIFY(MyMoneyMoney(rows[1][ListTable::ctValue]) == moCheckingOpen - moSolo * 3); QVERIFY(MyMoneyMoney(rows[3][ListTable::ctValue]) == moCreditOpen - (moParent1 + moParent2 + moChild) * 3); QVERIFY(MyMoneyMoney(rows[4][ListTable::ctValue]) == moCheckingOpen + moCreditOpen - (moParent1 + moParent2 + moSolo + moChild) * 3); } catch (const MyMoneyException &e) { QFAIL(e.what()); } } void QueryTableTest::testInvestment() { try { // Equities eqStock1 = makeEquity("Stock1", "STK1"); eqStock2 = makeEquity("Stock2", "STK2"); eqStock3 = makeEquity("Stock3", "STK3"); eqStock4 = makeEquity("Stock4", "STK4"); // Accounts acInvestment = makeAccount("Investment", eMyMoney::Account::Type::Investment, moZero, QDate(2003, 11, 1), acAsset); acStock1 = makeAccount("Stock 1", eMyMoney::Account::Type::Stock, moZero, QDate(2004, 1, 1), acInvestment, eqStock1); acStock2 = makeAccount("Stock 2", eMyMoney::Account::Type::Stock, moZero, QDate(2004, 1, 1), acInvestment, eqStock2); acStock3 = makeAccount("Stock 3", eMyMoney::Account::Type::Stock, moZero, QDate(2003, 11, 1), acInvestment, eqStock3); acStock4 = makeAccount("Stock 4", eMyMoney::Account::Type::Stock, moZero, QDate(2004, 1, 1), acInvestment, eqStock4); acDividends = makeAccount("Dividends", eMyMoney::Account::Type::Income, moZero, QDate(2004, 1, 1), acIncome); acInterest = makeAccount("Interest", eMyMoney::Account::Type::Income, moZero, QDate(2004, 1, 1), acIncome); acFees = makeAccount("Fees", eMyMoney::Account::Type::Expense, moZero, QDate(2003, 11, 1), acExpense); // Transactions // Date Action Shares Price Stock Asset Income InvTransactionHelper s1b1(QDate(2003, 12, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::BuyShares), MyMoneyMoney(1000.00), MyMoneyMoney(100.00), acStock3, acChecking, QString()); InvTransactionHelper s1b2(QDate(2004, 1, 30), MyMoneySplit::actionName(eMyMoney::Split::Action::BuyShares), MyMoneyMoney(500.00), MyMoneyMoney(100.00), acStock4, acChecking, acFees, MyMoneyMoney(100.00)); InvTransactionHelper s1b3(QDate(2004, 1, 30), MyMoneySplit::actionName(eMyMoney::Split::Action::BuyShares), MyMoneyMoney(500.00), MyMoneyMoney(90.00), acStock4, acChecking, acFees, MyMoneyMoney(100.00)); InvTransactionHelper s1b4(QDate(2004, 2, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::BuyShares), MyMoneyMoney(1000.00), MyMoneyMoney(100.00), acStock1, acChecking, QString()); InvTransactionHelper s1b5(QDate(2004, 3, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::BuyShares), MyMoneyMoney(1000.00), MyMoneyMoney(110.00), acStock1, acChecking, QString()); InvTransactionHelper s1s1(QDate(2004, 4, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::BuyShares), MyMoneyMoney(-200.00), MyMoneyMoney(120.00), acStock1, acChecking, QString()); InvTransactionHelper s1s2(QDate(2004, 5, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::BuyShares), MyMoneyMoney(-200.00), MyMoneyMoney(100.00), acStock1, acChecking, QString()); InvTransactionHelper s1s3(QDate(2004, 5, 30), MyMoneySplit::actionName(eMyMoney::Split::Action::BuyShares), MyMoneyMoney(-1000.00), MyMoneyMoney(120.00), acStock4, acChecking, acFees, MyMoneyMoney(200.00)); InvTransactionHelper s1r1(QDate(2004, 6, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::ReinvestDividend), MyMoneyMoney(50.00), MyMoneyMoney(100.00), acStock1, QString(), acDividends); InvTransactionHelper s1r2(QDate(2004, 7, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::ReinvestDividend), MyMoneyMoney(50.00), MyMoneyMoney(80.00), acStock1, QString(), acDividends); InvTransactionHelper s1c1(QDate(2004, 8, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Dividend), MyMoneyMoney(10.00), MyMoneyMoney(100.00), acStock1, acChecking, acDividends); InvTransactionHelper s1c2(QDate(2004, 9, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Dividend), MyMoneyMoney(10.00), MyMoneyMoney(120.00), acStock1, acChecking, acDividends); InvTransactionHelper s1y1(QDate(2004, 9, 15), MyMoneySplit::actionName(eMyMoney::Split::Action::Yield), MyMoneyMoney(10.00), MyMoneyMoney(110.00), acStock1, acChecking, acInterest); makeEquityPrice(eqStock1, QDate(2004, 10, 1), MyMoneyMoney(100.00)); makeEquityPrice(eqStock3, QDate(2004, 10, 1), MyMoneyMoney(110.00)); makeEquityPrice(eqStock4, QDate(2004, 10, 1), MyMoneyMoney(110.00)); // // Investment Transactions Report // MyMoneyReport invtran_r( eMyMoney::Report::RowType::TopAccount, eMyMoney::Report::QueryColumn::Action | eMyMoney::Report::QueryColumn::Shares | eMyMoney::Report::QueryColumn::Price, eMyMoney::TransactionFilter::Date::UserDefined, eMyMoney::Report::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() == 32); QVERIFY(MyMoneyMoney(rows[1][ListTable::ctValue]) == MyMoneyMoney(-100000.00)); QVERIFY(MyMoneyMoney(rows[2][ListTable::ctValue]) == MyMoneyMoney(-110000.00)); QVERIFY(MyMoneyMoney(rows[3][ListTable::ctValue]) == MyMoneyMoney(24000.00)); QVERIFY(MyMoneyMoney(rows[4][ListTable::ctValue]) == MyMoneyMoney(20000.00)); QVERIFY(MyMoneyMoney(rows[5][ListTable::ctValue]) == MyMoneyMoney(5000.00)); QVERIFY(MyMoneyMoney(rows[6][ListTable::ctValue]) == MyMoneyMoney(4000.00)); QVERIFY(MyMoneyMoney(rows[19][ListTable::ctValue]) == MyMoneyMoney(-50100.00)); QVERIFY(MyMoneyMoney(rows[22][ListTable::ctValue]) == MyMoneyMoney(-45100.00)); // need to fix these... fundamentally different from the original test //QVERIFY(MyMoneyMoney(invtran.m_rows[8][ListTable::ctValue])==MyMoneyMoney( -1000.00)); //QVERIFY(MyMoneyMoney(invtran.m_rows[11][ListTable::ctValue])==MyMoneyMoney( -1200.00)); //QVERIFY(MyMoneyMoney(invtran.m_rows[14][ListTable::ctValue])==MyMoneyMoney( -1100.00)); QVERIFY(MyMoneyMoney(rows[1][ListTable::ctPrice]) == MyMoneyMoney(100.00)); QVERIFY(MyMoneyMoney(rows[3][ListTable::ctPrice]) == MyMoneyMoney(120.00)); QVERIFY(MyMoneyMoney(rows[5][ListTable::ctPrice]) == MyMoneyMoney(100.00)); QVERIFY(MyMoneyMoney(rows[7][ListTable::ctPrice]) == MyMoneyMoney()); QVERIFY(MyMoneyMoney(rows[10][ListTable::ctPrice]) == MyMoneyMoney()); QVERIFY(MyMoneyMoney(rows[19][ListTable::ctPrice]) == MyMoneyMoney(100.00)); QVERIFY(MyMoneyMoney(rows[22][ListTable::ctPrice]) == MyMoneyMoney(90.00)); QVERIFY(MyMoneyMoney(rows[2][ListTable::ctShares]) == MyMoneyMoney(1000.00)); QVERIFY(MyMoneyMoney(rows[4][ListTable::ctShares]) == MyMoneyMoney(-200.00)); QVERIFY(MyMoneyMoney(rows[6][ListTable::ctShares]) == MyMoneyMoney(50.00)); QVERIFY(MyMoneyMoney(rows[8][ListTable::ctShares]) == MyMoneyMoney(0.00)); QVERIFY(MyMoneyMoney(rows[11][ListTable::ctShares]) == MyMoneyMoney(0.00)); QVERIFY(MyMoneyMoney(rows[19][ListTable::ctShares]) == MyMoneyMoney(500.00)); QVERIFY(MyMoneyMoney(rows[22][ListTable::ctShares]) == MyMoneyMoney(500.00)); QVERIFY(rows[1][ListTable::ctAction] == "Buy"); QVERIFY(rows[3][ListTable::ctAction] == "Sell"); QVERIFY(rows[5][ListTable::ctAction] == "Reinvest"); QVERIFY(rows[7][ListTable::ctAction] == "Dividend"); QVERIFY(rows[13][ListTable::ctAction] == "Yield"); QVERIFY(rows[19][ListTable::ctAction] == "Buy"); QVERIFY(rows[22][ListTable::ctAction] == "Buy"); #else QVERIFY(rows.count() == 9); QVERIFY(MyMoneyMoney(rows[0][ListTable::ctValue]) == MyMoneyMoney(100000.00)); QVERIFY(MyMoneyMoney(rows[1][ListTable::ctValue]) == MyMoneyMoney(110000.00)); QVERIFY(MyMoneyMoney(rows[2][ListTable::ctValue]) == MyMoneyMoney(-24000.00)); QVERIFY(MyMoneyMoney(rows[3][ListTable::ctValue]) == MyMoneyMoney(-20000.00)); QVERIFY(MyMoneyMoney(rows[4][ListTable::ctValue]) == MyMoneyMoney(5000.00)); QVERIFY(MyMoneyMoney(rows[5][ListTable::ctValue]) == MyMoneyMoney(4000.00)); QVERIFY(MyMoneyMoney(rows[6][ListTable::ctValue]) == MyMoneyMoney(-1000.00)); QVERIFY(MyMoneyMoney(rows[7][ListTable::ctValue]) == MyMoneyMoney(-1200.00)); QVERIFY(MyMoneyMoney(rows[8][ListTable::ctValue]) == MyMoneyMoney(-1100.00)); QVERIFY(MyMoneyMoney(rows[0][ListTable::ctPrice]) == MyMoneyMoney(100.00)); QVERIFY(MyMoneyMoney(rows[2][ListTable::ctPrice]) == MyMoneyMoney(120.00)); QVERIFY(MyMoneyMoney(rows[4][ListTable::ctPrice]) == MyMoneyMoney(100.00)); QVERIFY(MyMoneyMoney(rows[6][ListTable::ctPrice]) == MyMoneyMoney(0.00)); QVERIFY(MyMoneyMoney(rows[8][ListTable::ctPrice]) == MyMoneyMoney(0.00)); QVERIFY(MyMoneyMoney(rows[1][ListTable::ctShares]) == MyMoneyMoney(1000.00)); QVERIFY(MyMoneyMoney(rows[3][ListTable::ctShares]) == MyMoneyMoney(-200.00)); QVERIFY(MyMoneyMoney(rows[5][ListTable::ctShares]) == MyMoneyMoney(50.00)); QVERIFY(MyMoneyMoney(rows[7][ListTable::ctShares]) == MyMoneyMoney(0.00)); QVERIFY(MyMoneyMoney(rows[8][ListTable::ctShares]) == MyMoneyMoney(0.00)); QVERIFY(rows[0][ListTable::ctAction] == "Buy"); QVERIFY(rows[2][ListTable::ctAction] == "Sell"); QVERIFY(rows[4][ListTable::ctAction] == "Reinvest"); QVERIFY(rows[6][ListTable::ctAction] == "Dividend"); QVERIFY(rows[8][ListTable::ctAction] == "Yield"); #endif #if 1 // i think this is the correct amount. different treatment of dividend and yield QVERIFY(MyMoneyMoney(rows[17][ListTable::ctValue]) == MyMoneyMoney(-153700.00)); QVERIFY(MyMoneyMoney(rows[29][ListTable::ctValue]) == MyMoneyMoney(24600.00)); QVERIFY(MyMoneyMoney(rows[31][ListTable::ctValue]) == MyMoneyMoney(-129100.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( eMyMoney::Report::RowType::AccountByTopAccount, eMyMoney::Report::QueryColumn::Performance, eMyMoney::TransactionFilter::Date::UserDefined, eMyMoney::Report::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() == 5); QVERIFY(MyMoneyMoney(rows[0][ListTable::ctReturn]) == MyMoneyMoney("669/10000")); QVERIFY(MyMoneyMoney(rows[0][ListTable::ctReturnInvestment]) == MyMoneyMoney("-39/5000")); QVERIFY(MyMoneyMoney(rows[0][ListTable::ctBuys]) == MyMoneyMoney(-210000.00)); QVERIFY(MyMoneyMoney(rows[0][ListTable::ctSells]) == MyMoneyMoney(44000.00)); QVERIFY(MyMoneyMoney(rows[0][ListTable::ctReinvestIncome]) == MyMoneyMoney(9000.00)); QVERIFY(MyMoneyMoney(rows[0][ListTable::ctCashIncome]) == MyMoneyMoney(3300.00)); QVERIFY(MyMoneyMoney(rows[1][ListTable::ctReturn]) == MyMoneyMoney("1349/10000")); QVERIFY(MyMoneyMoney(rows[1][ListTable::ctReturnInvestment]) == MyMoneyMoney("1/10")); QVERIFY(MyMoneyMoney(rows[1][ListTable::ctStartingBalance]) == MyMoneyMoney(100000.00)); // this should stay non-zero to check if investment performance is calculated at non-zero starting balance QVERIFY(MyMoneyMoney(rows[2][ListTable::ctReturn]) == MyMoneyMoney("2501/2500")); QVERIFY(MyMoneyMoney(rows[2][ListTable::ctReturnInvestment]) == MyMoneyMoney("323/1250")); QVERIFY(MyMoneyMoney(rows[2][ListTable::ctBuys]) == MyMoneyMoney(-95200.00)); QVERIFY(MyMoneyMoney(rows[2][ListTable::ctSells]) == MyMoneyMoney(119800.00)); QVERIFY(MyMoneyMoney(rows[2][ListTable::ctEndingBalance]) == MyMoneyMoney(0.00)); // this should stay zero to check if investment performance is calculated at zero ending balance QVERIFY(MyMoneyMoney(rows[4][ListTable::ctEndingBalance]) == MyMoneyMoney(280000.00)); #if 0 // Dump file & reports QFile g("investmentkmy.xml"); g.open(QIODevice::WriteOnly); MyMoneyStorageXML xml; IMyMoneyOperationsFormat& interface = xml; interface.writeFile(&g, dynamic_cast(MyMoneyFile::instance()->storage())); g.close(); invtran.dump("invtran.html", "%1"); invhold.dump("invhold.html", "%1"); #endif } catch (const MyMoneyException &e) { QFAIL(e.what()); } } // prevents bug #312135 void QueryTableTest::testSplitShares() { try { MyMoneyMoney firstSharesPurchase(16); MyMoneyMoney splitFactor(2); MyMoneyMoney secondSharesPurchase(1); MyMoneyMoney sharesAtTheEnd = firstSharesPurchase / splitFactor + secondSharesPurchase; MyMoneyMoney priceBeforeSplit(74.99, 100); MyMoneyMoney priceAfterSplit = splitFactor * priceBeforeSplit; // Equities eqStock1 = makeEquity("Stock1", "STK1"); // Accounts acInvestment = makeAccount("Investment", eMyMoney::Account::Type::Investment, moZero, QDate(2017, 8, 1), acAsset); acStock1 = makeAccount("Stock 1", eMyMoney::Account::Type::Stock, moZero, QDate(2017, 8, 1), acInvestment, eqStock1); // Transactions // Date Action Shares Price Stock Asset Income InvTransactionHelper s1b1(QDate(2017, 8, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::BuyShares), firstSharesPurchase, priceBeforeSplit, acStock1, acChecking, QString()); InvTransactionHelper s1s1(QDate(2017, 8, 2), MyMoneySplit::actionName(eMyMoney::Split::Action::SplitShares), splitFactor, MyMoneyMoney(), acStock1, QString(), QString()); InvTransactionHelper s1b2(QDate(2017, 8, 3), MyMoneySplit::actionName(eMyMoney::Split::Action::BuyShares), secondSharesPurchase, priceAfterSplit, acStock1, acChecking, QString()); // // Investment Performance Report // MyMoneyReport invhold_r( eMyMoney::Report::RowType::AccountByTopAccount, eMyMoney::Report::QueryColumn::Performance, eMyMoney::TransactionFilter::Date::UserDefined, eMyMoney::Report::DetailLevel::All, i18n("Investment Performance by Account"), i18n("Test Report") ); invhold_r.setDateFilter(QDate(2017, 8, 1), QDate(2017, 8, 3)); invhold_r.setInvestmentsOnly(true); XMLandback(invhold_r); QueryTable invhold(invhold_r); writeTabletoHTML(invhold, "Investment Performance by Account (with stock split).html"); const auto rows = invhold.rows(); QVERIFY(rows.count() == 3); QVERIFY(MyMoneyMoney(rows[0][ListTable::ctBuys]) == sharesAtTheEnd * priceAfterSplit * MyMoneyMoney(-1)); } catch (const MyMoneyException &e) { QFAIL(e.what()); } } // prevents bug #118159 void QueryTableTest::testConversionRate() { try { MyMoneyMoney firsConversionRate(1.1800, 10000); MyMoneyMoney secondConversionRate(1.1567, 10000); MyMoneyMoney amountWithdrawn(100); const auto acCadChecking = makeAccount(QString("Canadian Checking"), eMyMoney::Account::Type::Checkings, moZero, QDate(2017, 8, 1), acAsset, "CAD"); makePrice("CAD", QDate(2017, 8, 1), firsConversionRate); makePrice("CAD", QDate(2017, 8, 2), secondConversionRate); TransactionHelper t1(QDate(2017, 8, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), amountWithdrawn, acCadChecking, acSolo, "CAD"); MyMoneyReport filter; filter.setRowType(eMyMoney::Report::RowType::Account); filter.setDateFilter(QDate(2017, 8, 1), QDate(2017, 8, 2)); filter.setName("Transactions by Account"); auto cols = eMyMoney::Report::QueryColumn::Number | eMyMoney::Report::QueryColumn::Payee | eMyMoney::Report::QueryColumn::Category | eMyMoney::Report::QueryColumn::Balance; filter.setQueryColumns(static_cast(cols)); // XMLandback(filter); QueryTable qtbl(filter); writeTabletoHTML(qtbl, "Transactions by Account (conversion rate).html"); const auto rows = qtbl.rows(); QVERIFY(rows.count() == 5); QVERIFY(MyMoneyMoney(rows[1][ListTable::ctValue]) == amountWithdrawn * firsConversionRate * MyMoneyMoney(-1)); QVERIFY(MyMoneyMoney(rows[1][ListTable::ctPrice]) == firsConversionRate); QVERIFY(MyMoneyMoney(rows[2][ListTable::ctBalance]) == amountWithdrawn * secondConversionRate * MyMoneyMoney(-1)); QVERIFY(MyMoneyMoney(rows[2][ListTable::ctPrice]) == secondConversionRate); } catch (const MyMoneyException &e) { QFAIL(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::actionName(eMyMoney::Split::Action::Withdrawal), moSolo, acChecking, acSolo); TransactionHelper t2q1(QDate(2004, 2, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent1, acCredit, acParent); TransactionHelper t3q1(QDate(2004, 3, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent2, acCredit, acParent); TransactionHelper t4y1(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moChild, acCredit, acChild); TransactionHelper t1q2(QDate(2004, 4, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moSolo, acChecking, acSolo); TransactionHelper t2q2(QDate(2004, 5, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent1, acCredit, acParent); TransactionHelper t3q2(QDate(2004, 6, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent2, acCredit, acParent); TransactionHelper t4q2(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moChild, acCredit, acChild); TransactionHelper t1y2(QDate(2005, 1, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moSolo, acChecking, acSolo); TransactionHelper t2y2(QDate(2005, 5, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent1, acCredit, acParent); TransactionHelper t3y2(QDate(2005, 9, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent2, acCredit, acParent); TransactionHelper t4y2(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moChild, acCredit, acChild); unsigned cols; MyMoneyReport filter; filter.setRowType(eMyMoney::Report::RowType::Account); filter.setName("Transactions by Account"); cols = eMyMoney::Report::QueryColumn::Number | eMyMoney::Report::QueryColumn::Payee | eMyMoney::Report::QueryColumn::Category | eMyMoney::Report::QueryColumn::Balance; filter.setQueryColumns(static_cast(cols)); // XMLandback(filter); QueryTable qtbl_3(filter); writeTabletoHTML(qtbl_3, "Transactions by Account.html"); QString html = qtbl_3.renderHTML(); 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 openingDate = QLocale().toString(QDate(2004, 1, 1), QLocale::ShortFormat); QString closingDate = QLocale().toString(QDate(2005, 9, 1), QLocale::ShortFormat); 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(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"), eMyMoney::Account::Type::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::actionName(eMyMoney::Split::Action::Transfer), MyMoneyMoney(moJpyTransaction), acJpyChecking, acChecking, "JPY"); TransactionHelper t4(openingDate, MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit), MyMoneyMoney(moTransaction), acCredit, acChecking); TransactionHelper t2(intermediateDate, MyMoneySplit::actionName(eMyMoney::Split::Action::Transfer), MyMoneyMoney(moJpyTransaction), acJpyChecking, acChecking, "JPY"); TransactionHelper t5(intermediateDate, MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit), MyMoneyMoney(moTransaction), acCredit, acChecking); TransactionHelper t3(closingDate, MyMoneySplit::actionName(eMyMoney::Split::Action::Transfer), MyMoneyMoney(moJpyTransaction), acJpyChecking, acChecking, "JPY"); TransactionHelper t6(closingDate, MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit), MyMoneyMoney(moTransaction), acCredit, acChecking); // test that an income/expense transaction that involves a currency exchange is properly reported TransactionHelper t7(intermediateDate, MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), MyMoneyMoney(moJpyTransaction), acJpyChecking, acSolo, "JPY"); unsigned cols; MyMoneyReport filter; filter.setRowType(eMyMoney::Report::RowType::Account); filter.setName("Transactions by Account"); cols = eMyMoney::Report::QueryColumn::Number | eMyMoney::Report::QueryColumn::Payee | eMyMoney::Report::QueryColumn::Category | eMyMoney::Report::QueryColumn::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.renderHTML(); QList rows = qtbl_3.rows(); QVERIFY(rows.count() == 24); //this is to make sure that the dates of closing and opening balances and the balance numbers are ok QString openingDateString = QLocale().toString(openingDate, QLocale::ShortFormat); QString intermediateDateString = QLocale().toString(intermediateDate, QLocale::ShortFormat); QString closingDateString = QLocale().toString(closingDate, QLocale::ShortFormat); // check the opening and closing balances QVERIFY(html.indexOf(openingDateString + "" + i18n("Opening Balance") + "USD 0.00") > 0); QVERIFY(html.indexOf(closingDateString + "" + i18n("Closing Balance") + "USD 304.00") > 0); QVERIFY(html.indexOf(closingDateString + "" + i18n("Closing Balance") + "USD -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 CheckingUSD 1.00USD 1.00") > 0); // after a transfer of 100 the balance should be 101.00 QVERIFY(html.indexOf("" + openingDateString + "Test PayeeTransfer from Credit CardUSD 100.00USD 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 CheckingUSD 1.00USD 102.00") > 0); // after a transfer of 100 the balance should be 202.00 QVERIFY(html.indexOf("" + intermediateDateString + "Test PayeeTransfer from Credit CardUSD 100.00USD 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 CheckingUSD 2.00USD 204.00") > 0); // after a transfer of 100 the balance should be 304.00 QVERIFY(html.indexOf("" + closingDateString + "Test PayeeTransfer from Credit CardUSD 100.00USD 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.renderHTML(); rows = qtbl_4.rows(); QVERIFY(rows.count() == 23); // 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 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); // 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); } catch (const MyMoneyException &e) { QFAIL(e.what()); } } void QueryTableTest::testTaxReport() { try { TransactionHelper t1q1(QDate(2004, 1, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moSolo, acChecking, acSolo); TransactionHelper t2q1(QDate(2004, 2, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent1, acChecking, acTax); unsigned cols; MyMoneyReport filter; filter.setRowType(eMyMoney::Report::RowType::Category); filter.setName("Tax Transactions"); cols = eMyMoney::Report::QueryColumn::Number | eMyMoney::Report::QueryColumn::Payee | eMyMoney::Report::QueryColumn::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.renderHTML(); QVERIFY(rows.count() == 5); } catch (const MyMoneyException &e) { QFAIL(e.what()); } }