diff --git a/kmymoney/reports/listtable.cpp b/kmymoney/reports/listtable.cpp index 8b8038243..ba2b42bd8 100644 --- a/kmymoney/reports/listtable.cpp +++ b/kmymoney/reports/listtable.cpp @@ -1,719 +1,562 @@ /*************************************************************************** listtable.cpp ------------------- begin : Sat 28 jun 2008 copyright : (C) 2004-2005 by Ace Jones 2008 by Alvaro Soliverez + (C) 2017 Ł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. * * * ***************************************************************************/ #include "listtable.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes // This is just needed for i18n(). Once I figure out how to handle i18n // without using this macro directly, I'll be freed of KDE dependency. // ---------------------------------------------------------------------------- // Project Includes #include "mymoneyfile.h" #include "mymoneyreport.h" #include "mymoneyexception.h" #include "kmymoneyutils.h" #include "kmymoneyglobalsettings.h" #include "reportdebug.h" namespace reports { QStringList ListTable::TableRow::m_sortCriteria; -// **************************************************************************** -// -// Group Iterator -// -// **************************************************************************** - -class GroupIterator -{ -public: - GroupIterator(const QString& _group, const QString& _subtotal, unsigned _depth) : m_depth(_depth), m_groupField(_group), m_subtotalField(_subtotal) {} - GroupIterator() : m_depth(0) {} - void update(const ListTable::TableRow& _row) { - m_previousGroup = m_currentGroup; - m_currentGroup = _row[m_groupField]; - if (isSubtotal()) { - m_previousSubtotal = m_currentSubtotal; - m_currentSubtotal = MyMoneyMoney(); - } - m_currentSubtotal += MyMoneyMoney(_row[m_subtotalField]); - } - - bool isNewHeader() const { - return (m_currentGroup != m_previousGroup); - } - bool isSubtotal() const { - return (m_currentGroup != m_previousGroup) && (!m_previousGroup.isEmpty()); - } - const MyMoneyMoney& subtotal() const { - return m_previousSubtotal; - } - const MyMoneyMoney& currenttotal() const { - return m_currentSubtotal; - } - unsigned depth() const { - return m_depth; - } - const QString& name() const { - return m_currentGroup; - } - const QString& oldName() const { - return m_previousGroup; - } - const QString& groupField() const { - return m_groupField; - } - const QString& subtotalField() const { - return m_subtotalField; - } - // ***DV*** HACK make the currentGroup test different but look the same - void force() { - m_currentGroup += ' '; - } -private: - MyMoneyMoney m_currentSubtotal; - MyMoneyMoney m_previousSubtotal; - unsigned m_depth; - QString m_currentGroup; - QString m_previousGroup; - QString m_groupField; - QString m_subtotalField; -}; - // **************************************************************************** // // ListTable implementation // // **************************************************************************** bool ListTable::TableRow::operator< (const TableRow& _compare) const { bool result = false; QStringList::const_iterator it_criterion = m_sortCriteria.constBegin(); while (it_criterion != m_sortCriteria.constEnd()) { if (this->operator[](*it_criterion) < _compare[ *it_criterion ]) { result = true; break; } else if (this->operator[](*it_criterion) > _compare[ *it_criterion ]) break; ++it_criterion; } return result; } // needed for KDE < 3.2 implementation of qHeapSort bool ListTable::TableRow::operator<= (const TableRow& _compare) const { return (!(_compare < *this)); } bool ListTable::TableRow::operator== (const TableRow& _compare) const { return (!(*this < _compare) && !(_compare < *this)); } bool ListTable::TableRow::operator> (const TableRow& _compare) const { return (_compare < *this); } /** * TODO * * - Collapse 2- & 3- groups when they are identical * - Way more test cases (especially splits & transfers) * - Option to collapse splits * - Option to exclude transfers * */ ListTable::ListTable(const MyMoneyReport& _report): ReportTable(), m_config(_report) { } void ListTable::render(QString& result, QString& csv) const { - MyMoneyMoney grandtotal; MyMoneyFile* file = MyMoneyFile::instance(); result = ""; csv = ""; result += QString("

%1

\n").arg(m_config.name()); csv += "\"Report: " + m_config.name() + "\"\n"; //actual dates of the report result += QString("
"); if (!m_config.fromDate().isNull()) { result += i18nc("Report date range", "%1 through %2", QLocale().toString(m_config.fromDate(), QLocale::ShortFormat), QLocale().toString(m_config.toDate(), QLocale::ShortFormat)); result += QString("
\n"); result += QString("
 
\n"); csv += i18nc("Report date range", "%1 through %2", QLocale().toString(m_config.fromDate(), QLocale::ShortFormat), QLocale().toString(m_config.toDate(), QLocale::ShortFormat)); csv += QString("\n"); } result += QString("
"); if (m_config.isConvertCurrency()) { result += i18n("All currencies converted to %1" , file->baseCurrency().name()); csv += i18n("All currencies converted to %1\n" , file->baseCurrency().name()); } else { result += i18n("All values shown in %1 unless otherwise noted" , file->baseCurrency().name()); csv += i18n("All values shown in %1 unless otherwise noted\n" , file->baseCurrency().name()); } result += QString("
\n"); result += QString("
 
\n"); // retrieve the configuration parameters from the report definition. // the things that we care about for query reports are: // how to group the rows, what columns to display, and what field // to subtotal on QStringList groups = m_group.split(','); QStringList columns = m_columns.split(','); - columns += m_subtotal; + if (!m_subtotal.isEmpty() && m_subtotal.split(',').count() == 1) // constructPerformanceRow has subtotal columns already in columns + columns += m_subtotal; QStringList postcolumns = m_postcolumns.split(','); - columns += postcolumns; + if (!m_postcolumns.isEmpty()) // prevent creation of empty column + columns += postcolumns; // // Table header // QMap i18nHeaders; i18nHeaders["postdate"] = i18n("Date"); i18nHeaders["value"] = i18n("Amount"); i18nHeaders["number"] = i18n("Num"); i18nHeaders["payee"] = i18n("Payee"); i18nHeaders["tag"] = i18n("Tags"); i18nHeaders["category"] = i18n("Category"); i18nHeaders["account"] = i18n("Account"); i18nHeaders["memo"] = i18n("Memo"); i18nHeaders["topcategory"] = i18n("Top Category"); i18nHeaders["categorytype"] = i18n("Category Type"); i18nHeaders["month"] = i18n("Month"); i18nHeaders["week"] = i18n("Week"); i18nHeaders["reconcileflag"] = i18n("Reconciled"); i18nHeaders["action"] = i18n("Action"); i18nHeaders["shares"] = i18n("Shares"); i18nHeaders["price"] = i18n("Price"); i18nHeaders["latestprice"] = i18n("Price"); i18nHeaders["netinvvalue"] = i18n("Net Value"); i18nHeaders["buys"] = i18n("Buys"); i18nHeaders["sells"] = i18n("Sells"); i18nHeaders["reinvestincome"] = i18n("Dividends Reinvested"); i18nHeaders["cashincome"] = i18n("Dividends Paid Out"); i18nHeaders["startingbal"] = i18n("Starting Balance"); i18nHeaders["endingbal"] = i18n("Ending Balance"); i18nHeaders["return"] = i18n("Annualized Return"); i18nHeaders["returninvestment"] = i18n("Return On Investment"); i18nHeaders["fees"] = i18n("Fees"); i18nHeaders["interest"] = i18n("Interest"); i18nHeaders["payment"] = i18n("Payment"); i18nHeaders["balance"] = i18n("Balance"); i18nHeaders["type"] = i18n("Type"); i18nHeaders["name"] = i18nc("Account name", "Name"); i18nHeaders["nextduedate"] = i18n("Next Due Date"); i18nHeaders["occurence"] = i18n("Occurrence"); // krazy:exclude=spelling i18nHeaders["paymenttype"] = i18n("Payment Method"); i18nHeaders["institution"] = i18n("Institution"); i18nHeaders["description"] = i18n("Description"); i18nHeaders["openingdate"] = i18n("Opening Date"); i18nHeaders["currencyname"] = i18n("Currency"); i18nHeaders["balancewarning"] = i18n("Balance Early Warning"); i18nHeaders["maxbalancelimit"] = i18n("Balance Max Limit"); i18nHeaders["creditwarning"] = i18n("Credit Early Warning"); i18nHeaders["maxcreditlimit"] = i18n("Credit Max Limit"); i18nHeaders["tax"] = i18n("Tax"); i18nHeaders["favorite"] = i18n("Preferred"); i18nHeaders["loanamount"] = i18n("Loan Amount"); i18nHeaders["interestrate"] = i18n("Interest Rate"); i18nHeaders["nextinterestchange"] = i18n("Next Interest Change"); i18nHeaders["periodicpayment"] = i18n("Periodic Payment"); i18nHeaders["finalpayment"] = i18n("Final Payment"); i18nHeaders["currentbalance"] = i18n("Current Balance"); i18nHeaders["capitalgain"] = i18n("Capital Gain"); // the list of columns which represent money, so we can display them correctly QStringList moneyColumns = QString("value,shares,price,latestprice,netinvvalue,buys,sells,cashincome,reinvestincome,startingbal,fees,interest,payment,balance,balancewarning,maxbalancelimit,creditwarning,maxcreditlimit,loanamount,periodicpayment,finalpayment,currentbalance,startingbal,endingbal,capitalgain").split(','); // the list of columns which represent shares, which is like money except the // transaction currency will not be displayed QStringList sharesColumns = QString("shares").split(','); // the list of columns which represent a percentage, so we can display them correctly QStringList percentColumns = QString("return,returninvestment,interestrate").split(','); // the list of columns which represent dates, so we can display them correctly QStringList dateColumns = QString("postdate,entrydate,nextduedate,openingdate,nextinterestchange").split(','); result += "\n"; QStringList::const_iterator it_column = columns.constBegin(); while (it_column != columns.constEnd()) { QString i18nName = i18nHeaders[*it_column]; if (i18nName.isEmpty()) i18nName = *it_column; result += ""; csv += i18nName + ','; ++it_column; } result += "\n"; csv = csv.left(csv.length() - 1); csv += '\n'; - // - // Set up group iterators - // - // There is one active iterator for each level of grouping. - // As we step through the rows - // we update the group iterators each time based on the row data. If - // the group iterator changes and it had a previous value, we print a - // subtotal. Whether or not it had a previous value, we print a group - // header. The group iterator keeps track of a subtotal also. - - int depth = 1; - QList groupIteratorList; - QStringList::const_iterator it_grouplevel = groups.constBegin(); - while (it_grouplevel != groups.constEnd()) { - groupIteratorList += GroupIterator((*it_grouplevel), m_subtotal, depth++); - ++it_grouplevel; + // initialize group names to empty, so any group will have to display its header + QStringList prevGrpNames; + for (int i = 0; i < groups.count(); ++i) { + prevGrpNames.append(QString()); } // // Rows // bool row_odd = true; // ***DV*** MyMoneyMoney startingBalance; + MyMoneyMoney balanceChange = MyMoneyMoney(); for (QList::const_iterator it_row = m_rows.begin(); it_row != m_rows.end(); ++it_row) { // the standard fraction is the fraction of an non-cash account in the base currency // this could be overridden using the "fraction" element of a row for each row. // Currently (2008-02-21) this override is not used at all (ipwizard) int fraction = file->baseCurrency().smallestAccountFraction(); if ((*it_row).find("fraction") != (*it_row).end()) fraction = (*it_row)["fraction"].toInt(); - // - // Process Groups - // - - // ***DV*** HACK to force a subtotal and header, since this render doesn't - // always detect a group change for different accounts with the same name - // (as occurs with the same stock purchased from different investment accts) - if (it_row != m_rows.begin()) - if (((* it_row)["rank"] == "-2") && ((* it_row)["id"] == "A")) - (groupIteratorList.last()).force(); - - // There's a subtle bug here. If an earlier group gets a new group, - // then we need to force all the downstream groups to get one too. - - // Update the group iterators with the current row value - QList::iterator it_group = groupIteratorList.begin(); - while (it_group != groupIteratorList.end()) { - (*it_group).update(*it_row); - ++it_group; - } - - // Do subtotals backwards - if (m_config.isConvertCurrency()) { - it_group = groupIteratorList.end(); - if (it_group != groupIteratorList.begin()) - --it_group; - while (it_group != groupIteratorList.end()) { - if ((*it_group).isSubtotal()) { - if ((*it_group).depth() == 1) - grandtotal += (*it_group).subtotal(); - grandtotal = grandtotal.convert(fraction); - - QString subtotal_html = (*it_group).subtotal().formatMoney(fraction); - QString subtotal_csv = (*it_group).subtotal().formatMoney(fraction, false); - - // ***DV*** HACK fix the side-effiect from .force() method above - QString oldName = QString((*it_group).oldName()).trimmed(); - - result += - "" - "" - "\n"; - - csv += - "\"" + i18nc("Total balance", "Total") + " " + oldName + "\",\"" + subtotal_csv + "\"\n"; - } - - // going beyond begin() is not caught by the iterator - if (it_group == groupIteratorList.begin()) - break; - --it_group; - } - } - - // And headers forwards - it_group = groupIteratorList.begin(); - while (it_group != groupIteratorList.end()) { - if ((*it_group).isNewHeader()) { + // detect whether any of groups changed and display new group header in that case + for (int i = 0; i < groups.count(); ++i) { + if (prevGrpNames.at(i) != (*it_row)[groups.at(i)]) { row_odd = true; result += "" - "\n"; - csv += "\"" + (*it_group).name() + "\"\n"; + (*it_row)[groups.at(i)] + "\n"; + csv += "\"" + (*it_row)[groups.at(i)] + "\"\n"; + prevGrpNames.replace(i, (*it_row)[groups.at(i)]); } - ++it_group; } // // Columns // // skip the opening and closing balance row, // if the balance column is not shown - if ((columns.contains("balance") == 0) && ((*it_row)["rank"] == "-2")) + // rank = 0 for opening balance, rank = 3 for closing balance + if ((columns.contains("balance") == 0) && ((*it_row)["rank"] == "0" || (*it_row)["rank"] == "3")) continue; bool need_label = true; QString tlink; // link information to account and transaction // ***DV*** - if ((* it_row)["rank"] == "0") { + if ((* it_row)["rank"] == "1") { row_odd = ! row_odd; tlink = QString("id=%1&tid=%2") .arg((* it_row)["accountid"], (* it_row)["id"]); } - if ((* it_row)["rank"] == "-2") + if ((*it_row)["rank"] == "0" || (*it_row)["rank"] == "3") result += QString("").arg((* it_row)["id"]); - else if ((* it_row)["rank"] == "1") + else if ((* it_row)["rank"] == "2") result += QString("").arg(row_odd ? "item1" : "item0"); - else + else if ((* it_row)["rank"] == "4") { + if (m_config.rowType() == MyMoneyReport::eTag || //If we order by Tags don't show the Grand total as we can have multiple tags per transaction + !m_config.isConvertCurrency() && std::next(it_row) == m_rows.end())// grand total may be invalid if multiple currencies are used, so don't display it + continue; + else + result += QString(""); + } else result += QString("").arg(row_odd ? "row-odd " : "row-even"); QStringList::const_iterator it_column = columns.constBegin(); while (it_column != columns.constEnd()) { QString data = (*it_row)[*it_column]; // ***DV*** - if ((* it_row)["rank"] == "1") { + if ((* it_row)["rank"] == "2") { if (* it_column == "value") data = (* it_row)["split"]; else if (*it_column == "postdate" || *it_column == "number" || *it_column == "payee" || *it_column == "tag" || *it_column == "action" || *it_column == "shares" || *it_column == "price" || *it_column == "nextduedate" || *it_column == "balance" || *it_column == "account" || *it_column == "name") data = ""; } // ***DV*** - if ((* it_row)["rank"] == "-2") { + else if ((*it_row)["rank"] == "0" || (*it_row)["rank"] == "3") { if (*it_column == "balance") { data = (* it_row)["balance"]; - if ((* it_row)["id"] == "A") // opening balance? + if ((* it_row)["id"] == "A") { // opening balance? startingBalance = MyMoneyMoney(data); + balanceChange = MyMoneyMoney(); + } } if (need_label) { if ((*it_column == "payee") || (*it_column == "category") || (*it_column == "memo")) { if (!(*it_row)["shares"].isEmpty()) { data = ((* it_row)["id"] == "A") ? i18n("Initial Market Value") : i18n("Ending Market Value"); } else { data = ((* it_row)["id"] == "A") ? i18n("Opening Balance") : i18n("Closing Balance"); } need_label = false; } } } - // The 'balance' column is calculated at render-time // but not printed on split lines - else if (*it_column == "balance" && (* it_row)["rank"] == "0") { + else if (*it_column == "balance" && (* it_row)["rank"] == "1") { // Take the balance off the deepest group iterator - data = (groupIteratorList.back().currenttotal() + startingBalance).toString(); + balanceChange += MyMoneyMoney((*it_row).value("value", "0")); + data = (balanceChange + startingBalance).toString(); } + // display total title but only if first column doesn't contain any data + else if (it_column == columns.constBegin() && data.isEmpty() && (*it_row)["rank"] == "4") { + result += ""; + else + result += i18n("Grand Total") + ""; + ++it_column; + continue; + } + // Figure out how to render the value in this column, depending on // what its properties are. // // TODO: This and the i18n headings are handled // as a set of parallel vectors. Would be much better to make a single // vector of a properties class. QString tlinkBegin, tlinkEnd; if (!tlink.isEmpty()) { - tlinkBegin = QString("").arg(tlink); - tlinkEnd = QLatin1String(""); + tlinkBegin = QString("").arg(tlink); + tlinkEnd = QLatin1String(""); } if (sharesColumns.contains(*it_column)) { if (data.isEmpty()) { result += QString(""); csv += "\"\","; } else { result += QString("").arg(MyMoneyMoney(data).formatMoney("", 3), tlinkBegin, tlinkEnd); csv += "\"" + MyMoneyMoney(data).formatMoney("", 3, false) + "\","; } } else if (moneyColumns.contains(*it_column)) { if (data.isEmpty()) { result += QString("") .arg((*it_column == "value") ? " class=\"value\"" : ""); csv += "\"\","; } else if (MyMoneyMoney(data) == MyMoneyMoney::autoCalc) { result += QString("%3%2%4") .arg((*it_column == "value") ? " class=\"value\"" : "") .arg(i18n("Calculated"), tlinkBegin, tlinkEnd); csv += "\"" + i18n("Calculated") + "\","; } else if (*it_column == "price") { result += QString("") .arg(MyMoneyMoney(data).formatMoney(MyMoneyMoney::precToDenom(KMyMoneyGlobalSettings::pricePrecision())), tlinkBegin, tlinkEnd); csv += "\"" + (*it_row)["currency"] + " " + MyMoneyMoney(data).formatMoney(MyMoneyMoney::precToDenom(KMyMoneyGlobalSettings::pricePrecision()), false) + "\","; } else { result += QString("%4%2 %3%5") .arg((*it_column == "value") ? " class=\"value\"" : "") .arg((*it_row)["currency"]) .arg(MyMoneyMoney(data).formatMoney(fraction)) .arg(tlinkBegin, tlinkEnd); csv += "\"" + (*it_row)["currency"] + " " + MyMoneyMoney(data).formatMoney(fraction, false) + "\","; } } else if (percentColumns.contains(*it_column)) { - data = (MyMoneyMoney(data) * MyMoneyMoney(100, 1)).formatMoney(fraction); - result += QString("").arg(data, tlinkBegin, tlinkEnd); - csv += data + "%,"; + if (data.isEmpty()) { + result += QString(""); + csv += "\"\","; + } else { + data = (MyMoneyMoney(data) * MyMoneyMoney(100, 1)).formatMoney(fraction); + result += QString("").arg(data, tlinkBegin, tlinkEnd); + csv += data + "%,"; + } } else if (dateColumns.contains(*it_column)) { // do this before we possibly change data csv += "\"" + data + "\","; // if we have a locale() then use its date formatter if (!data.isEmpty()) { QDate qd = QDate::fromString(data, Qt::ISODate); data = QLocale().toString(qd, QLocale::ShortFormat); } - result += QString("").arg(data, tlinkBegin, tlinkEnd); + result += QString("").arg(data, tlinkBegin, tlinkEnd); } else { - result += QString("").arg(data, tlinkBegin, tlinkEnd); + result += QString("").arg(data, tlinkBegin, tlinkEnd); csv += "\"" + data + "\","; } ++it_column; tlink.clear(); } result += "\n"; csv = csv.left(csv.length() - 1); // remove final comma csv += '\n'; } - - // - // Final group totals - // - - // Do subtotals backwards - if (m_config.isConvertCurrency()) { - int fraction = file->baseCurrency().smallestAccountFraction(); - QList::iterator it_group = groupIteratorList.end(); - if (it_group != groupIteratorList.begin()) - --it_group; - while (it_group != groupIteratorList.end()) { - (*it_group).update(TableRow()); - - if ((*it_group).depth() == 1) { - grandtotal += (*it_group).subtotal(); - grandtotal = grandtotal.convert(fraction); - } - - - QString subtotal_html = (*it_group).subtotal().formatMoney(fraction); - QString subtotal_csv = (*it_group).subtotal().formatMoney(fraction, false); - - result += "" - "" - "\n"; - csv += "\"" + i18nc("Total balance", "Total") + " " + (*it_group).oldName() + "\",\"" + subtotal_csv + "\"\n"; - - // going beyond begin() is not caught by the iterator - if (it_group == groupIteratorList.begin()) - break; - --it_group; - } - - // - // Grand total - // - - QString grandtotal_html = grandtotal.formatMoney(fraction); - QString grandtotal_csv = grandtotal.formatMoney(fraction, false); - - //If we order by Tags don't show the Grand total as we can have multiple tags per transaction - if (m_config.rowType() != MyMoneyReport::eTag) { - result += "" - "" - "\n"; - csv += "\"" + i18n("Grand Total") + "\",\"" + grandtotal_csv + "\"\n"; - } - } result += "
" + i18nName + "
" + - i18nc("Total balance", "Total") + ' ' + oldName + "" + subtotal_html + "
" + - (*it_group).name() + "
"; + if (!(*it_row)["depth"].isEmpty()) + result += i18nc("Total balance", "Total") + ' ' + prevGrpNames.at((*it_row)["depth"].toInt()) + "%2%1%3%2%1%3%2%1%%3%2%1%%3%2%1%3%2%1%3%2%1%3%2%1%3
" + - i18nc("Total balance", "Total") + ' ' + (*it_group).oldName() + "" + subtotal_html + "
" + - i18n("Grand Total") + "" + grandtotal_html + "
\n"; } QString ListTable::renderBody() const { QString html, csv; render(html, csv); return html; } QString ListTable::renderCSV() const { QString html, csv; render(html, csv); return csv; } void ListTable::dump(const QString& file, const QString& context) const { QFile g(file); g.open(QIODevice::WriteOnly | QIODevice::Text); if (! context.isEmpty()) QTextStream(&g) << context.arg(renderBody()); else QTextStream(&g) << renderBody(); g.close(); } void ListTable::includeInvestmentSubAccounts() { // if we're not in expert mode, we need to make sure // that all stock accounts for the selected investment // account are also selected. // In case we get called for a non investment only report we quit if (KMyMoneyGlobalSettings::expertMode() || !m_config.isInvestmentsOnly()) { return; } // get all investment subAccountsList but do not include those with zero balance // or those which had no transactions during the timeframe of the report QStringList accountIdList; QStringList subAccountsList; MyMoneyFile* file = MyMoneyFile::instance(); // get the report account filter if (!m_config.accounts(accountIdList) && m_config.isInvestmentsOnly()) { // this will only execute if this is an investment-only report QList accountList; file->accountList(accountList); QList::const_iterator it_ma; for (it_ma = accountList.constBegin(); it_ma != accountList.constEnd(); ++it_ma) { if ((*it_ma).accountType() == MyMoneyAccount::Investment) { accountIdList.append((*it_ma).id()); } } } QStringList::const_iterator it_a; for (it_a = accountIdList.constBegin(); it_a != accountIdList.constEnd(); ++it_a) { MyMoneyAccount acc = file->account(*it_a); if (acc.accountType() == MyMoneyAccount::Investment) { QStringList::const_iterator it_b; for (it_b = acc.accountList().constBegin(); it_b != acc.accountList().constEnd(); ++it_b) { if (!accountIdList.contains(*it_b)) { subAccountsList.append(*it_b); } } } } if (m_config.isInvestmentsOnly() && !m_config.isIncludingUnusedAccounts()) { // if the balance is not zero at the end, include the subaccount QStringList::iterator it_balance; for (it_balance = subAccountsList.begin(); it_balance != subAccountsList.end();) { if (!file->balance((*it_balance), m_config.toDate()).isZero()) { m_config.addAccount((*it_balance)); it_balance = subAccountsList.erase((it_balance)); } else { ++it_balance; } } // if there are transactions for that subaccount, include them MyMoneyTransactionFilter filter; filter.setDateFilter(m_config.fromDate(), m_config.toDate()); filter.addAccount(subAccountsList); filter.setReportAllSplits(false); QList transactions = file->transactionList(filter); QList::const_iterator it_t = transactions.constBegin(); //Check each split for a matching account for (; it_t != transactions.constEnd(); ++it_t) { const QList& splits = (*it_t).splits(); QList::const_iterator it_s = splits.begin(); for (; it_s != splits.end(); ++it_s) { const QString& accountId = (*it_s).accountId(); if (!(*it_s).shares().isZero() && subAccountsList.contains(accountId)) { subAccountsList.removeOne(accountId); m_config.addAccount(accountId); } } } } else { // if not an investment-only report or explicitly including unused accounts // add all investment subaccounts m_config.addAccount(subAccountsList); } } } diff --git a/kmymoney/reports/querytable.cpp b/kmymoney/reports/querytable.cpp index 4d5a843dc..afbd7b4bb 100644 --- a/kmymoney/reports/querytable.cpp +++ b/kmymoney/reports/querytable.cpp @@ -1,1686 +1,1868 @@ /*************************************************************************** querytable.cpp ------------------- begin : Fri Jul 23 2004 copyright : (C) 2004-2005 by Ace Jones (C) 2007 Sascha Pfau (C) 2017 Łukasz Wojniłowicz ***************************************************************************/ /**************************************************************************** Contains code from the func_xirr and related methods of financial.cpp - KOffice 1.6 by Sascha Pfau. Sascha agreed to relicense those methods under GPLv2 or later. *****************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "querytable.h" #include // ---------------------------------------------------------------------------- // QT Includes #include #include // ---------------------------------------------------------------------------- // KDE Includes #include // ---------------------------------------------------------------------------- // Project Includes #include "mymoneyfile.h" #include "mymoneytransaction.h" #include "mymoneyreport.h" #include "mymoneyexception.h" #include "kmymoneyutils.h" #include "reportaccount.h" #include "reportdebug.h" #include "kmymoneyglobalsettings.h" namespace reports { // **************************************************************************** // // CashFlowListItem implementation // // Cash flow analysis tools for investment reports // // **************************************************************************** QDate CashFlowListItem::m_sToday = QDate::currentDate(); MyMoneyMoney CashFlowListItem::NPV(double _rate) const { double T = static_cast(m_sToday.daysTo(m_date)) / 365.0; MyMoneyMoney result(m_value.toDouble() / pow(1 + _rate, T), 100); //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 = begin(); while (it_cash != end()) { 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 { QDate date; double r = rate + 1.0; double res = 0.00000;//back().value().toDouble(); QList::const_iterator list_it = begin(); while (list_it != end()) { double e_i = ((* list_it).today().daysTo((* list_it).date())) / 365.0; MyMoneyMoney val = (* list_it).value(); if (e_i < 0) { res += val.toDouble() * pow(r, -e_i); } else { res += val.toDouble() / pow(r, e_i); } ++list_it; } return res; } double CashFlowList::xirrResultDerive(double& rate) const { QDate date; double r = rate + 1.0; double res = 0.00000; QList::const_iterator list_it = begin(); while (list_it != end()) { double e_i = ((* list_it).today().daysTo((* list_it).date())) / 365.0; MyMoneyMoney val = (* list_it).value(); res -= e_i * val.toDouble() / pow(r, e_i + 1.0); ++list_it; } return res; } double CashFlowList::IRR() const { double result = 0.0; // set 'today', which is the most recent of all dates in the list CashFlowListItem::setToday(mostRecent().date()); result = calculateXIRR(); return result; } MyMoneyMoney CashFlowList::total() const { MyMoneyMoney result; const_iterator it_cash = begin(); while (it_cash != end()) { result += (*it_cash).value(); ++it_cash; } return result; } void CashFlowList::dumpDebug() const { const_iterator it_item = begin(); while (it_item != end()) { 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() { switch (m_config.rowType()) { case MyMoneyReport::eAccountByTopAccount: case MyMoneyReport::eEquityType: case MyMoneyReport::eAccountType: case MyMoneyReport::eInstitution: constructAccountTable(); m_columns = "account"; break; case MyMoneyReport::eAccount: constructTransactionTable(); m_columns = "accountid,postdate"; break; case MyMoneyReport::ePayee: case MyMoneyReport::eTag: case MyMoneyReport::eMonth: case MyMoneyReport::eWeek: constructTransactionTable(); m_columns = "postdate,account"; break; case MyMoneyReport::eCashFlow: constructSplitsTable(); m_columns = "postdate"; break; default: constructTransactionTable(); m_columns = "postdate"; } // Sort the data to match the report definition m_subtotal = "value"; switch (m_config.rowType()) { case MyMoneyReport::eCashFlow: m_group = "categorytype,topcategory,category"; break; case MyMoneyReport::eCategory: m_group = "categorytype,topcategory,category"; break; case MyMoneyReport::eTopCategory: m_group = "categorytype,topcategory"; break; case MyMoneyReport::eTopAccount: m_group = "topaccount,account"; break; case MyMoneyReport::eAccount: m_group = "account"; break; case MyMoneyReport::eAccountReconcile: m_group = "account,reconcileflag"; break; case MyMoneyReport::ePayee: m_group = "payee"; break; case MyMoneyReport::eTag: m_group = "tag"; break; case MyMoneyReport::eMonth: m_group = "month"; break; case MyMoneyReport::eWeek: m_group = "week"; break; case MyMoneyReport::eAccountByTopAccount: m_group = "topaccount"; break; case MyMoneyReport::eEquityType: m_group = "equitytype"; break; case MyMoneyReport::eAccountType: m_group = "type"; break; case MyMoneyReport::eInstitution: m_group = "institution,topaccount"; break; default: throw MYMONEYEXCEPTION("QueryTable::QueryTable(): unhandled row type"); } - QString sort = m_group + ',' + m_columns + ",id,rank"; + QString sort = m_group + ",id,rank," + m_columns; switch (m_config.rowType()) { case MyMoneyReport::eAccountByTopAccount: case MyMoneyReport::eEquityType: case MyMoneyReport::eAccountType: case MyMoneyReport::eInstitution: m_columns = "account"; break; default: m_columns = "postdate"; } unsigned qc = m_config.queryColumns(); if (qc & MyMoneyReport::eQCnumber) m_columns += ",number"; if (qc & MyMoneyReport::eQCpayee) m_columns += ",payee"; if (qc & MyMoneyReport::eQCtag) m_columns += ",tag"; if (qc & MyMoneyReport::eQCcategory) m_columns += ",category"; if (qc & MyMoneyReport::eQCaccount) m_columns += ",account"; if (qc & MyMoneyReport::eQCreconciled) m_columns += ",reconcileflag"; if (qc & MyMoneyReport::eQCmemo) m_columns += ",memo"; if (qc & MyMoneyReport::eQCaction) m_columns += ",action"; if (qc & MyMoneyReport::eQCshares) m_columns += ",shares"; if (qc & MyMoneyReport::eQCprice) m_columns += ",price"; if (qc & MyMoneyReport::eQCperformance) { - m_columns += ",startingbal,buys,sells,reinvestincome,cashincome,return,returninvestment"; - m_subtotal = "endingbal"; + m_columns += ",startingbal,buys,sells,reinvestincome,cashincome,return,returninvestment,endingbal"; + m_subtotal = "startingbal,buys,sells,reinvestincome,cashincome,return,returninvestment,endingbal"; } if (qc & MyMoneyReport::eQCcapitalgain) { - m_columns += ",buys,sells"; - m_subtotal = "capitalgain"; + m_columns += ",buys,sells,capitalgain"; + m_subtotal = "buys,sells,capitalgain"; } if (qc & MyMoneyReport::eQCloan) { m_columns += ",payment,interest,fees"; m_postcolumns = "balance"; } if (qc & MyMoneyReport::eQCbalance) m_postcolumns = "balance"; TableRow::setSortCriteria(sort); - qSort(m_rows); + + 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 + if (m_rows.at(0)["rank"] == "4") // it should be unlikely that total row is at the top of rows, so... + m_rows.move(0, m_rows.count() - 1); // ...move it at the bottom + + QStringList subtotals = m_subtotal.split(','); + QStringList groups = m_group.split(','); + QStringList columns = m_columns.split(','); + if (!m_subtotal.isEmpty() && subtotals.count() == 1) + columns += m_subtotal; + QStringList postcolumns = m_postcolumns.split(','); + if (!m_postcolumns.isEmpty()) + columns += postcolumns; + + QList> totalGroups; + QMap totalsValues; + + // initialize all total values under summed columns to be zero + foreach (auto subtotal, subtotals) { + totalsValues.insert(subtotal, 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; + QList::iterator current_row, next_row; + for (current_row = m_rows.begin(); + current_row != m_rows.end();) { + + next_row = std::next(current_row); + + // total rows are useless at summing so remove whole block of them at once + while (next_row != m_rows.end() && (*next_row)["rank"] == "4") { + stashedTotalRows.append((*next_row)); // ...but stash them just in case + next_row = m_rows.erase(next_row); + } + + bool lastRow = (next_row == m_rows.end()); + + // sum all subtotal values for lowest group + foreach (auto subtotal, subtotals) { + totalGroups.last()[subtotal] += MyMoneyMoney((*current_row)[subtotal]); + } + + // 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 || (*current_row)[groups.at(i)] != (*next_row)[groups.at(i)]) { + TableRow totalsRow; + // custom total values calculations + foreach (auto subtotal, subtotals) { + if (subtotal == "returninvestment") + totalGroups[i + 1]["returninvestment"] = helperROI(totalGroups[i + 1]["buys"], totalGroups[i + 1]["sells"], + totalGroups[i + 1]["startingbal"], totalGroups[i + 1]["endingbal"], + totalGroups[i + 1]["cashincome"]); + } + + // total values that aren't calculated here, but are taken untouched from external source, e.g. constructPerformanceRow + if (!stashedTotalRows.isEmpty()) { + foreach (auto subtotal, subtotals) { + if (subtotal == "return") + totalsRow["return"] = stashedTotalRows.first()["return"]; + } + stashedTotalRows.removeFirst(); + } + + // sum all subtotal values for higher groups (excluding grand total) and reset lowest group values + QMap::iterator upperGrp = totalGroups[i].begin(); + QMap::iterator lowerGrp = totalGroups[i + 1].begin(); + + while(upperGrp != totalGroups[i].end()) { + totalsRow[lowerGrp.key()] = lowerGrp.value().toString(); // fill totals row with subtotal values... + (*upperGrp) += (*lowerGrp); + (*lowerGrp) = MyMoneyMoney(); + ++upperGrp; + ++lowerGrp; + } + + for (int j = 0; j < groups.count(); ++j) { + totalsRow[groups.at(j)] = (*current_row)[groups.at(j)]; // ...and identification + } + + totalsRow["rank"] = "4"; + totalsRow["depth"] = QString::number(i); + + if (lastRow) + m_rows.append(totalsRow); + else { + next_row = m_rows.insert(next_row, totalsRow); // current_row and next_row can diverge here by more than one + ++next_row; + } + } + } + + // code to put grand total row + if (lastRow) { + TableRow totalsRow; + + foreach (auto subtotal, subtotals) { + if (subtotal == "returninvestment") + totalGroups[0]["returninvestment"] = helperROI(totalGroups[0]["buys"], totalGroups[0]["sells"], + totalGroups[0]["startingbal"], totalGroups[0]["endingbal"], + totalGroups[0]["cashincome"]); + } + + if (!stashedTotalRows.isEmpty()) { + foreach (auto subtotal, subtotals) { + if (subtotal == "return") + totalsRow["return"] = stashedTotalRows.first()["return"]; + } + stashedTotalRows.removeFirst(); + } + + QMap::const_iterator grandTotalGrp = totalGroups[0].begin(); + while(grandTotalGrp != totalGroups[0].end()) { + totalsRow[grandTotalGrp.key()] = grandTotalGrp.value().toString(); + ++grandTotalGrp; + } + + for (int j = 0; j < groups.count(); ++j) { + totalsRow[groups.at(j)] = QString(); // no identification + } + + totalsRow["rank"] = "4"; + totalsRow["depth"] = ""; + m_rows.append(totalsRow); + break; // no use to loop further + } + current_row = next_row; // current_row 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 MyMoneyReport::eCategory: case MyMoneyReport::eTopCategory: use_summary = false; use_transfers = false; hide_details = false; break; case MyMoneyReport::ePayee: use_summary = false; use_transfers = false; hide_details = (m_config.detailLevel() == MyMoneyReport::eDetailNone); break; case MyMoneyReport::eTag: use_summary = false; use_transfers = false; hide_details = (m_config.detailLevel() == MyMoneyReport::eDetailNone); tag_special_case = true; break; default: use_summary = true; use_transfers = true; hide_details = (m_config.detailLevel() == MyMoneyReport::eDetailNone); break; } // support for opening and closing balances QMap accts; //get all transactions for this report QList transactions = file->transactionList(report); for (QList::const_iterator it_transaction = transactions.constBegin(); it_transaction != transactions.constEnd(); ++it_transaction) { TableRow qA, qS; QDate pd; QList tagIdListCache; qA["id"] = qS["id"] = (* it_transaction).id(); qA["entrydate"] = qS["entrydate"] = (* it_transaction).entryDate().toString(Qt::ISODate); qA["postdate"] = qS["postdate"] = (* it_transaction).postDate().toString(Qt::ISODate); qA["commodity"] = qS["commodity"] = (* it_transaction).commodity(); pd = (* it_transaction).postDate(); qA["month"] = qS["month"] = i18n("Month of %1", QDate(pd.year(), pd.month(), 1).toString(Qt::ISODate)); qA["week"] = qS["week"] = i18n("Week of %1", pd.addDays(1 - pd.dayOfWeek()).toString(Qt::ISODate)); qA["currency"] = qS["currency"] = ""; if ((* it_transaction).commodity() != file->baseCurrency().id()) { if (!report.isConvertCurrency()) { qA["currency"] = qS["currency"] = (*it_transaction).commodity(); } } // to handle splits, we decide on which account to base the split // (a reference point or point of view so to speak). here we take the // first account that is a stock account or loan account (or the first account // that is not an income or expense account if there is no stock or loan account) // to be the account (qA) that will have the sub-item "split" entries. we add // one transaction entry (qS) for each subsequent entry in the split. const QList& splits = (*it_transaction).splits(); QList::const_iterator myBegin, it_split; for (it_split = splits.begin(), myBegin = splits.end(); it_split != splits.end(); ++it_split) { ReportAccount splitAcc = (* it_split).accountId(); // always put split with a "stock" account if it exists if (splitAcc.isInvest()) break; // prefer to put splits with a "loan" account if it exists if (splitAcc.isLoan()) myBegin = it_split; if ((myBegin == splits.end()) && ! splitAcc.isIncomeExpense()) { myBegin = it_split; } } // select our "reference" split if (it_split == splits.end()) { it_split = myBegin; } else { myBegin = it_split; } // skip this transaction if we didn't find a valid base account - see the above description // for the base account's description - if we don't find it avoid a crash by skipping the transaction if (myBegin == splits.end()) continue; // if the split is still unknown, use the first one. I have seen this // happen with a transaction that has only a single split referencing an income or expense // account and has an amount and value of 0. Such a transaction will fall through // the above logic and leave 'it_split' pointing to splits.end() which causes the remainder // of this to end in an infinite loop. if (it_split == splits.end()) { it_split = splits.begin(); } // for "loan" reports, the loan transaction gets special treatment. // the splits of a loan transaction are placed on one line in the // reference (loan) account (qA). however, we process the matching // split entries (qS) normally. bool loan_special_case = false; if (m_config.queryColumns() & MyMoneyReport::eQCloan) { 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 (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... else qA["currency"] = qS["currency"] = myBeginCurrency; // ...or 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["account"] = splitAcc.name(); qA["accountid"] = splitAcc.id(); qA["topaccount"] = splitAcc.topParentName(); if (splitAcc.isInvest()) { // use the institution of the parent for stock accounts institution = splitAcc.parent().institutionId(); MyMoneyMoney shares = (*it_split).shares(); qA["action"] = (*it_split).action(); qA["shares"] = shares.isZero() ? "" : shares.toString(); qA["price"] = shares.isZero() ? "" : xr.convert(MyMoneyMoney::precToDenom(KMyMoneyGlobalSettings::pricePrecision())).toString(); if (((*it_split).action() == MyMoneySplit::ActionBuyShares) && shares.isNegative()) qA["action"] = "Sell"; qA["investaccount"] = splitAcc.parent().name(); MyMoneySplit stockSplit = (*it_split); MyMoneySplit assetAccountSplit; QList feeSplits; QList interestSplits; MyMoneySecurity currency; MyMoneySecurity security; MyMoneySplit::investTransactionTypeE 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 = 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_config.isConvertCurrency()) { if (myBeginCurrency != baseCurrency) { MyMoneyPrice price = file->price(myBeginCurrency, baseCurrency, (*it_transaction).postDate()); if (price.isValid()) { xr = price.rate(baseCurrency); qA["currency"] = qS["currency"] = ""; } else qA["currency"] = qS["currency"] = myBeginCurrency; } else xr = MyMoneyMoney::ONE; qA["price"] = shares.isZero() ? "" : (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["price"] = xr.toString(); a_fullname = splitAcc.fullName(); a_memo = (*it_split).memo(); transaction_text = m_config.match(&(*it_split)); qA["institution"] = institution.isEmpty() ? i18n("No Institution") : file->institution(institution).name(); qA["payee"] = payee.isEmpty() ? i18n("[Empty Payee]") : file->payee(payee).name().simplified(); if (tag_special_case) { tagIdListCache = tagIdList; } else { QString delimiter = ""; for (int i = 0; i < tagIdList.size(); i++) { qA["tag"] += delimiter + file->tag(tagIdList[i]).name().simplified(); delimiter = ", "; } } qA["reconciledate"] = (*it_split).reconcileDate().toString(Qt::ISODate); qA["reconcileflag"] = KMyMoneyUtils::reconcileStateToString((*it_split).reconcileFlag(), true); qA["number"] = (*it_split).number(); qA["memo"] = a_memo; qA["value"] = ((*it_split).shares() * xr).convert(fraction).toString(); qS["reconciledate"] = qA["reconciledate"]; qS["reconcileflag"] = qA["reconcileflag"]; qS["number"] = qA["number"]; qS["topcategory"] = splitAcc.topParentName(); qS["categorytype"] = i18n("Transfer"); // only include the configured accounts if (include_me) { if (loan_special_case) { // put the principal amount in the "value" column and convert to lowest fraction qA["value"] = (-(*it_split).shares() * xr).convert(fraction).toString(); - qA["rank"] = '0'; + qA["rank"] = '1'; qA["split"] = ""; } else { if ((splits.count() > 2) && use_summary) { // add the "summarized" split transaction // this is the sub-total of the split detail // convert to lowest fraction - qA["rank"] = '0'; + qA["rank"] = '1'; qA["category"] = i18n("[Split Transaction]"); qA["topcategory"] = i18nc("Split transaction", "Split"); qA["categorytype"] = 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::ActionAmortization) { // put the payment in the "payment" column and convert to lowest fraction qA["payment"] = value.toString(); } else if ((*it_split).action() == MyMoneySplit::ActionInterest) { // put the interest in the "interest" column and convert to lowest fraction qA["interest"] = value.toString(); } else if (splits.count() > 2) { // [dv: This comment carried from the original code. I am // not exactly clear on what it means or why we do this.] // Put the initial pay-in nowhere (that is, ignore it). This // is dangerous, though. The only way I can tell the initial // pay-in apart from fees is if there are only 2 splits in // the transaction. I wish there was a better way. } else { // accumulate everything else in the "fees" column MyMoneyMoney n0 = MyMoneyMoney(qA["fees"]); qA["fees"] = (n0 + value).toString(); } // we don't add qA here for a loan transaction. we'll add one // qA afer all of the split components have been processed. // (see below) } //--- special case to hide split transaction details else if (hide_details && (splits.count() > 2)) { // essentially, don't add any qA entries } //--- default case includes all transaction details else { //this is when the splits are going to be shown as children of the main split if ((splits.count() > 2) && use_summary) { qA["value"] = ""; //convert to lowest fraction qA["split"] = (-(*it_split).shares() * xr).convert(fraction).toString(); - qA["rank"] = '1'; + qA["rank"] = '2'; } else { //this applies when the transaction has only 2 splits, or each split is going to be //shown separately, eg. transactions by category qA["split"] = ""; - qA["rank"] = '0'; + qA["rank"] = '1'; } qA ["memo"] = (*it_split).memo(); // if different from base currency and not converting // show the currency of the split if (splitAcc.currencyId() != file->baseCurrency().id()) { if (!report.isConvertCurrency()) { qS["currency"] = splitAcc.currencyId(); } } else { qS["currency"] = ""; } if (! splitAcc.isIncomeExpense()) { qA["category"] = ((*it_split).shares().isNegative()) ? i18n("Transfer from %1", splitAcc.fullName()) : i18n("Transfer to %1", splitAcc.fullName()); qA["topcategory"] = splitAcc.topParentName(); qA["categorytype"] = i18n("Transfer"); } else { qA ["category"] = splitAcc.fullName(); qA ["topcategory"] = splitAcc.topParentName(); qA ["categorytype"] = KMyMoneyUtils::accountTypeToString(splitAcc.accountGroup()); } if (use_transfers || (splitAcc.isIncomeExpense() && m_config.includes(splitAcc))) { //if it matches the text of the main split of the transaction or //it matches this particular split, include it //otherwise, skip it //if the filter is "does not contain" exclude the split if it does not match //even it matches the whole split if ((m_config.isInvertingText() && m_config.match(&(*it_split))) || (!m_config.isInvertingText() && (transaction_text || m_config.match(&(*it_split))))) { if (tag_special_case) { if (!tagIdListCache.size()) qA["tag"] = i18n("[No Tag]"); else for (int i = 0; i < tagIdListCache.size(); i++) { qA["tag"] = file->tag(tagIdListCache[i]).name().simplified(); m_rows += qA; } } else { m_rows += qA; } } } } } if (m_config.includes(splitAcc) && use_transfers && !(splitAcc.isInvest() && include_me)) { // otherwise stock split is displayed twice in report if (! splitAcc.isIncomeExpense()) { //multiply by currency and convert to lowest fraction qS["value"] = ((*it_split).shares() * xr).convert(fraction).toString(); - qS["rank"] = '0'; + qS["rank"] = '1'; qS["account"] = splitAcc.name(); qS["accountid"] = splitAcc.id(); qS["topaccount"] = splitAcc.topParentName(); qS["category"] = ((*it_split).shares().isNegative()) ? i18n("Transfer to %1", a_fullname) : i18n("Transfer from %1", a_fullname); qS["institution"] = institution.isEmpty() ? i18n("No Institution") : file->institution(institution).name(); qS["memo"] = (*it_split).memo().isEmpty() ? a_memo : (*it_split).memo(); //FIXME-ALEX When is used this? I can't find in which condition we arrive here... maybe this code is useless? QString delimiter = ""; for (int i = 0; i < tagIdList.size(); i++) { qA["tag"] += delimiter + file->tag(tagIdList[i]).name().simplified(); delimiter = '+'; } qS["payee"] = payee.isEmpty() ? qA["payee"] : file->payee(payee).name().simplified(); //check the specific split against the filter for text and amount //TODO this should be done at the engine, but I have no clear idea how -- asoliverez //if the filter is "does not contain" exclude the split if it does not match //even it matches the whole split if ((m_config.isInvertingText() && m_config.match(&(*it_split))) || (!m_config.isInvertingText() && (transaction_text || m_config.match(&(*it_split))))) { m_rows += qS; // track accts that will need opening and closing balances accts.insert(splitAcc.id(), splitAcc); } } } } ++it_split; // look for wrap-around if (it_split == splits.end()) it_split = splits.begin(); // but terminate if this transaction has only a single split if (splits.count() < 2) break; //check if there have been more passes than there are splits //this is to prevent infinite loops in cases of data inconsistency -- asoliverez ++pass; if (pass > splits.count()) break; } while (it_split != myBegin); if (loan_special_case) { m_rows += qA; } } // now run through our accts list and add opening and closing balances switch (m_config.rowType()) { case MyMoneyReport::eAccount: case MyMoneyReport::eTopAccount: break; // case MyMoneyReport::eCategory: // case MyMoneyReport::eTopCategory: // case MyMoneyReport::ePayee: // case MyMoneyReport::eMonth: // case MyMoneyReport::eWeek: default: return; } QDate startDate, endDate; report.validDateRange(startDate, endDate); QString strStartDate = startDate.toString(Qt::ISODate); QString strEndDate = endDate.toString(Qt::ISODate); startDate = startDate.addDays(-1); QMap::const_iterator it_account, accts_end; for (it_account = accts.constBegin(); it_account != accts.constEnd(); ++it_account) { TableRow qA; ReportAccount account = (* it_account); //get fraction for account int fraction = account.currency().smallestAccountFraction(); //use base currency fraction if not initialized if (fraction == -1) fraction = file->baseCurrency().smallestAccountFraction(); QString institution = account.institutionId(); // use the institution of the parent for stock accounts if (account.isInvest()) institution = account.parent().institutionId(); MyMoneyMoney startBalance, endBalance, startPrice, endPrice; MyMoneyMoney startShares, endShares; //get price and convert currency if necessary if (m_config.isConvertCurrency()) { startPrice = (account.deepCurrencyPrice(startDate) * account.baseCurrencyPrice(startDate)).reduce(); endPrice = (account.deepCurrencyPrice(endDate) * account.baseCurrencyPrice(endDate)).reduce(); } else { startPrice = account.deepCurrencyPrice(startDate).reduce(); endPrice = account.deepCurrencyPrice(endDate).reduce(); } startShares = file->balance(account.id(), startDate); endShares = file->balance(account.id(), endDate); //get starting and ending balances startBalance = startShares * startPrice; endBalance = endShares * endPrice; //starting balance // don't show currency if we're converting or if it's not foreign qA["currency"] = (m_config.isConvertCurrency() || ! account.isForeignCurrency()) ? "" : account.currency().id(); qA["accountid"] = account.id(); qA["account"] = account.name(); qA["topaccount"] = account.topParentName(); qA["institution"] = institution.isEmpty() ? i18n("No Institution") : file->institution(institution).name(); - qA["rank"] = "-2"; + qA["rank"] = "0"; qA["price"] = startPrice.convert(MyMoneyMoney::precToDenom(KMyMoneyGlobalSettings::pricePrecision())).toString(); if (account.isInvest()) { qA["shares"] = startShares.toString(); } qA["postdate"] = strStartDate; qA["balance"] = startBalance.convert(fraction).toString(); qA["value"].clear(); qA["id"] = 'A'; m_rows += qA; //ending balance qA["price"] = endPrice.convert(MyMoneyMoney::precToDenom(KMyMoneyGlobalSettings::pricePrecision())).toString(); if (account.isInvest()) { qA["shares"] = endShares.toString(); } qA["postdate"] = strEndDate; qA["balance"] = endBalance.toString(); + qA["rank"] = "3"; qA["id"] = 'Z'; m_rows += qA; } } -void QueryTable::constructPerformanceRow(const ReportAccount& account, TableRow& result) const +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 +{ + 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 annualReturn; +} + +void QueryTable::constructPerformanceRow(const ReportAccount& account, TableRow& result, CashFlowList &all) const { MyMoneyFile* file = MyMoneyFile::instance(); MyMoneySecurity security; //get fraction depending on type of account int fraction = account.currency().smallestAccountFraction(); // // Calculate performance // // The following columns are created: // Account, Value on , Buys, Sells, Income, Value on , Return% MyMoneyReport report = m_config; QDate startingDate; QDate endingDate; MyMoneyMoney price; report.validDateRange(startingDate, endingDate); startingDate = startingDate.addDays(-1); //calculate starting balance if (m_config.isConvertCurrency()) { price = account.deepCurrencyPrice(startingDate) * account.baseCurrencyPrice(startingDate); } else { price = account.deepCurrencyPrice(startingDate); } //work around if there is no price for the starting balance if (!(file->balance(account.id(), startingDate)).isZero() && account.deepCurrencyPrice(startingDate) == MyMoneyMoney::ONE) { MyMoneyTransactionFilter filter; //get the transactions for the time before the report filter.setDateFilter(QDate(), startingDate); filter.addAccount(account.id()); filter.setReportAllSplits(true); QList startTransactions = file->transactionList(filter); if (startTransactions.size() > 0) { //get the last transaction MyMoneyTransaction startTrans = startTransactions.back(); MyMoneySplit s = startTrans.splitByAccount(account.id()); //get the price from the split of that account price = s.price(); if (m_config.isConvertCurrency()) price = price * account.baseCurrencyPrice(startingDate); } } if (m_config.isConvertCurrency()) { price = account.deepCurrencyPrice(startingDate) * account.baseCurrencyPrice(startingDate); } else { price = account.deepCurrencyPrice(startingDate); } MyMoneyMoney startingBal = file->balance(account.id(), startingDate) * price; //convert to lowest fraction startingBal = startingBal.convert(fraction); //calculate ending balance if (m_config.isConvertCurrency()) { price = account.deepCurrencyPrice(endingDate) * account.baseCurrencyPrice(endingDate); } else { price = account.deepCurrencyPrice(endingDate); } MyMoneyMoney endingBal = file->balance((account).id(), endingDate) * price; //convert to lowest fraction endingBal = endingBal.convert(fraction); CashFlowList buys; CashFlowList sells; CashFlowList reinvestincome; CashFlowList cashincome; report.setReportAllSplits(false); report.setConsiderCategory(true); report.clearAccountFilter(); report.addAccount(account.id()); QList transactions = file->transactionList(report); QList::const_iterator it_transaction = transactions.constBegin(); while (it_transaction != transactions.constEnd()) { // s is the split for the stock account MyMoneySplit s = (*it_transaction).splitByAccount(account.id()); MyMoneySplit assetAccountSplit; QList feeSplits; QList interestSplits; MyMoneySecurity currency; MyMoneySplit::investTransactionTypeE transactionType; KMyMoneyUtils::dissectTransaction((*it_transaction), s, assetAccountSplit, feeSplits, interestSplits, security, currency, transactionType); //get price for the day of the transaction if we have to calculate base currency //we are using the value of the split which is in deep currency if (m_config.isConvertCurrency()) { price = account.baseCurrencyPrice((*it_transaction).postDate()); //we only need base currency because the value is in deep currency } else { price = MyMoneyMoney::ONE; } MyMoneyMoney value = assetAccountSplit.value() * price; if (transactionType == MyMoneySplit::BuyShares) buys += CashFlowListItem((*it_transaction).postDate(), value); else if (transactionType == MyMoneySplit::SellShares) sells += CashFlowListItem((*it_transaction).postDate(), value); else if (transactionType == MyMoneySplit::ReinvestDividend) { value = interestSplits.first().value() * price; reinvestincome += CashFlowListItem((*it_transaction).postDate(), -value); } else if (transactionType == MyMoneySplit::Dividend || transactionType == MyMoneySplit::Yield) cashincome += CashFlowListItem((*it_transaction).postDate(), value); ++it_transaction; } // Note that reinvested dividends are not included , because these do not // represent a cash flow event. - CashFlowList all; all += buys; all += sells; all += cashincome; all += CashFlowListItem(startingDate, -startingBal); all += CashFlowListItem(endingDate, endingBal); - MyMoneyMoney returnInvestment; MyMoneyMoney buysTotal = buys.total(); MyMoneyMoney sellsTotal = sells.total(); MyMoneyMoney cashIncomeTotal = cashincome.total(); MyMoneyMoney reinvestIncomeTotal = reinvestincome.total(); - if (!buysTotal.isZero() || !startingBal.isZero()) { - returnInvestment = (sellsTotal + buysTotal + cashIncomeTotal + endingBal - startingBal) / (startingBal - buysTotal); - returnInvestment = returnInvestment.convert(10000); - } else - returnInvestment = MyMoneyMoney(); // if no investment then no return on investment - - 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; - } + MyMoneyMoney returnInvestment = helperROI(buysTotal, sellsTotal, startingBal, endingBal, cashIncomeTotal); + MyMoneyMoney annualReturn = helperIRR(all); // check if there are any meaningfull values before adding them to results if (!(buysTotal.isZero() && sellsTotal.isZero() && cashIncomeTotal.isZero() && reinvestIncomeTotal.isZero() && startingBal.isZero() && endingBal.isZero())) { result["return"] = annualReturn.toString(); result["returninvestment"] = returnInvestment.toString(); result["equitytype"] = KMyMoneyUtils::securityTypeToString(security.securityType()); result["buys"] = buysTotal.toString(); result["sells"] = sellsTotal.toString(); result["cashincome"] = cashIncomeTotal.toString(); result["reinvestincome"] = reinvestIncomeTotal.toString(); result["startingbal"] = startingBal.toString(); result["endingbal"] = endingBal.toString(); } } void QueryTable::constructCapitalGainRow(const ReportAccount& account, TableRow& result) const { MyMoneyFile* file = MyMoneyFile::instance(); MyMoneySecurity security; MyMoneyMoney price; MyMoneyMoney sellValue; MyMoneyMoney buyValue; MyMoneyMoney sellShares; MyMoneyMoney buyShares; // // Calculate capital gain // // The following columns are created: // Account, Buys, Sells, Capital Gain MyMoneyReport report = m_config; QDate startingDate; QDate endingDate; QDate newStartingDate; QDate newEndingDate; report.validDateRange(startingDate, endingDate); newStartingDate = startingDate; newEndingDate = endingDate; MyMoneyMoney endingShares = file->balance(account.id(), endingDate); // get how many shares there are over zero value bool reportedDateRange = true; // flag marking sell transactions between startingDate and endingDate report.setReportAllSplits(false); report.setConsiderCategory(true); report.clearAccountFilter(); report.addAccount(account.id()); 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 currency; MyMoneySplit::investTransactionTypeE transactionType; KMyMoneyUtils::dissectTransaction((*it_t), shareSplit, assetAccountSplit, feeSplits, interestSplits, security, currency, transactionType); //get price for the day of the transaction if we have to calculate base currency //we are using the value of the split which is in deep currency if (m_config.isConvertCurrency()) price = account.baseCurrencyPrice((*it_t).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 == MyMoneySplit::BuyShares) { if (endingShares.isZero()) { // add sold shares if (buyShares + shares > sellShares.abs()) { // add partially sold shares buyValue += (((sellShares.abs() - buyShares)) / shares) * value; buyShares = sellShares.abs(); } else { // add wholly sold shares buyValue += value; buyShares += shares; } } else if (endingShares >= shares) { // substract not-sold shares endingShares -= shares; } else { // substract partially not-sold shares buyValue += ((shares - endingShares) / shares) * value; buyShares += (shares - endingShares); endingShares = MyMoneyMoney(0); } } else if (transactionType == MyMoneySplit::SellShares && reportedDateRange) { sellValue += value; sellShares += shares; } else if (transactionType == MyMoneySplit::SplitShares) { // shares variable is denominator of split ratio here sellShares /= shares; buyShares /= shares; } else if (transactionType == MyMoneySplit::AddShares) { // added shares, when sold give 100% capital gain if (endingShares.isZero()) { // add added shares if (buyShares + shares > sellShares.abs()) { // add partially added shares buyShares = sellShares.abs(); } else { // add wholly added shares buyShares += shares; } } else if (endingShares >= shares) { // substract not-added shares endingShares -= shares; } else { // substract partially not-added shares buyShares += (shares - endingShares); endingShares = MyMoneyMoney(0); } } else if (transactionType == MyMoneySplit::RemoveShares && reportedDateRange) { // removed shares give no value in return so no capital gain on them sellShares += shares; } } reportedDateRange = false; newEndingDate = newStartingDate; newStartingDate = newStartingDate.addYears(-1); report.setDateFilter(newStartingDate, newEndingDate); // search for matching buy transactions year earlier } while (!sellShares.isZero() && account.openingDate() <= newEndingDate && sellShares.abs() > buyShares.abs()); // check if there are any meaningfull values before adding them to results if (!(buyValue.isZero() && sellValue.isZero())) { result["equitytype"] = KMyMoneyUtils::securityTypeToString(security.securityType()); result["buys"] = buyValue.toString(); result["sells"] = sellValue.toString(); result["capitalgain"] = (buyValue + sellValue).toString(); } report.setDateFilter(startingDate, endingDate); // reset data filter for next security } void QueryTable::constructAccountTable() { MyMoneyFile* file = MyMoneyFile::instance(); //make sure we have all subaccounts of investment accounts includeInvestmentSubAccounts(); + + QMap topAccounts; // 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() != MyMoneyAccount::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 if (m_config.queryColumns() == MyMoneyReport::eQCperformance) { - constructPerformanceRow(account, qaccountrow); + constructPerformanceRow(account, qaccountrow, accountCashflow); } else if (m_config.queryColumns() == MyMoneyReport::eQCcapitalgain) { constructCapitalGainRow(account, qaccountrow); } else qaccountrow["equitytype"].clear(); if (qaccountrow.isEmpty()) // don't add the account if there are no calculated values continue; // help for sort and render functions - qaccountrow["rank"] = '0'; + qaccountrow["rank"] = '1'; // // Handle currency conversion // MyMoneyMoney displayprice(1, 1); if (m_config.isConvertCurrency()) { // display currency is base currency, so set the price if (account.isForeignCurrency()) displayprice = account.baseCurrencyPrice(m_config.toDate()).reduce(); } else { // display currency is the account's deep currency. display this fact in the report qaccountrow["currency"] = account.currency().id(); } qaccountrow["account"] = account.name(); qaccountrow["accountid"] = account.id(); qaccountrow["topaccount"] = account.topParentName(); qaccountrow["shares"] = shares.toString(); //get fraction for account int fraction = account.currency().smallestAccountFraction(); //use base currency fraction if not initialized if (fraction == -1) fraction = file->baseCurrency().smallestAccountFraction(); MyMoneyMoney netprice = account.deepCurrencyPrice(m_config.toDate()).reduce() * displayprice; qaccountrow["price"] = (netprice.reduce()).convert(MyMoneyMoney::precToDenom(KMyMoneyGlobalSettings::pricePrecision())).toString(); qaccountrow["value"] = (netprice.reduce() * shares.reduce()).convert(fraction).toString(); QString iid = (*it_account).institutionId(); // If an account does not have an institution, get it from the top-parent. if (iid.isEmpty() && ! account.isTopLevel()) { ReportAccount topaccount = account.topParent(); iid = topaccount.institutionId(); } if (iid.isEmpty()) qaccountrow["institution"] = i18nc("No institution", "None"); else qaccountrow["institution"] = file->institution(iid).name(); qaccountrow["type"] = KMyMoneyUtils::accountTypeToString((*it_account).accountType()); + // assuming that that report is grouped by topaccount + if (m_config.queryColumns() == MyMoneyReport::eQCperformance) { + if (!topAccounts.contains(qaccountrow["topaccount"])) + topAccounts.insert(qaccountrow["topaccount"], accountCashflow); // create cashflow for unknown account... + else + topAccounts[qaccountrow["topaccount"]] += accountCashflow; // ...or add cashflow for known account + } + m_rows += qaccountrow; } } + + if (m_config.queryColumns() == MyMoneyReport::eQCperformance) { + TableRow qtotalsrow; + qtotalsrow["rank"] = "4"; // add identification of row as total + CashFlowList grandCashflow; + + // convert map of top accounts with cashflows to TableRow + for (QMap::iterator topAccount = topAccounts.begin(); topAccount != topAccounts.end(); ++topAccount) { + qtotalsrow["topaccount"] = topAccount.key(); + qtotalsrow["return"] = helperIRR(topAccount.value()).toString(); + grandCashflow += topAccount.value(); // cumulative sum of cashflows of each topaccount + m_rows += qtotalsrow; // rows aren't sorted yet, so no problem with adding them randomly at the end + } + qtotalsrow["topaccount"] = ""; // empty topaccount because it's grand cashflow + qtotalsrow["return"] = helperIRR(grandCashflow).toString(); + m_rows += qtotalsrow; + } + } void QueryTable::constructSplitsTable() { MyMoneyFile* file = MyMoneyFile::instance(); //make sure we have all subaccounts of investment accounts includeInvestmentSubAccounts(); MyMoneyReport report(m_config); report.setReportAllSplits(false); report.setConsiderCategory(true); // support for opening and closing balances QMap accts; //get all transactions for this report QList transactions = file->transactionList(report); for (QList::const_iterator it_transaction = transactions.constBegin(); it_transaction != transactions.constEnd(); ++it_transaction) { TableRow qA, qS; QDate pd; qA["id"] = qS["id"] = (* it_transaction).id(); qA["entrydate"] = qS["entrydate"] = (* it_transaction).entryDate().toString(Qt::ISODate); qA["postdate"] = qS["postdate"] = (* it_transaction).postDate().toString(Qt::ISODate); qA["commodity"] = qS["commodity"] = (* it_transaction).commodity(); pd = (* it_transaction).postDate(); qA["month"] = qS["month"] = i18n("Month of %1", QDate(pd.year(), pd.month(), 1).toString(Qt::ISODate)); qA["week"] = qS["week"] = i18n("Week of %1", pd.addDays(1 - pd.dayOfWeek()).toString(Qt::ISODate)); qA["currency"] = qS["currency"] = ""; if ((* it_transaction).commodity() != file->baseCurrency().id()) { if (!report.isConvertCurrency()) { qA["currency"] = qS["currency"] = (*it_transaction).commodity(); } } // to handle splits, we decide on which account to base the split // (a reference point or point of view so to speak). here we take the // first account that is a stock account or loan account (or the first account // that is not an income or expense account if there is no stock or loan account) // to be the account (qA) that will have the sub-item "split" entries. we add // one transaction entry (qS) for each subsequent entry in the split. const QList& splits = (*it_transaction).splits(); QList::const_iterator myBegin, it_split; //S_end = splits.end(); for (it_split = splits.begin(), myBegin = splits.end(); it_split != splits.end(); ++it_split) { ReportAccount splitAcc = (* it_split).accountId(); // always put split with a "stock" account if it exists if (splitAcc.isInvest()) break; // prefer to put splits with a "loan" account if it exists if (splitAcc.isLoan()) myBegin = it_split; if ((myBegin == splits.end()) && ! splitAcc.isIncomeExpense()) { myBegin = it_split; } } // select our "reference" split if (it_split == splits.end()) { it_split = myBegin; } else { myBegin = it_split; } // if the split is still unknown, use the first one. I have seen this // happen with a transaction that has only a single split referencing an income or expense // account and has an amount and value of 0. Such a transaction will fall through // the above logic and leave 'it_split' pointing to splits.end() which causes the remainder // of this to end in an infinite loop. if (it_split == splits.end()) { it_split = splits.begin(); } // for "loan" reports, the loan transaction gets special treatment. // the splits of a loan transaction are placed on one line in the // reference (loan) account (qA). however, we process the matching // split entries (qS) normally. bool loan_special_case = false; if (m_config.queryColumns() & MyMoneyReport::eQCloan) { ReportAccount splitAcc = (*it_split).accountId(); loan_special_case = splitAcc.isLoan(); } // There is a slight chance that at this point myBegin is still pointing to splits.end() if the // transaction only has income and expense splits (which should not happen). In that case, point // it to the first split if (myBegin == splits.end()) { myBegin = splits.begin(); } //the account of the beginning splits ReportAccount myBeginAcc = (*myBegin).accountId(); bool include_me = true; QString a_fullname = ""; QString a_memo = ""; int pass = 1; do { MyMoneyMoney xr; ReportAccount splitAcc = (* it_split).accountId(); //get fraction for account int fraction = splitAcc.currency().smallestAccountFraction(); //use base currency fraction if not initialized if (fraction == -1) fraction = file->baseCurrency().smallestAccountFraction(); QString institution = splitAcc.institutionId(); QString payee = (*it_split).payeeId(); const QList tagIdList = (*it_split).tagIdList(); if (m_config.isConvertCurrency()) { xr = (splitAcc.deepCurrencyPrice((*it_transaction).postDate()) * splitAcc.baseCurrencyPrice((*it_transaction).postDate())).reduce(); } else { xr = splitAcc.deepCurrencyPrice((*it_transaction).postDate()).reduce(); } // reverse the sign of incomes and expenses to keep consistency in the way it is displayed in other reports if (splitAcc.isIncomeExpense()) { xr = -xr; } if (splitAcc.isInvest()) { // use the institution of the parent for stock accounts institution = splitAcc.parent().institutionId(); MyMoneyMoney shares = (*it_split).shares(); qA["action"] = (*it_split).action(); qA["shares"] = shares.isZero() ? "" : (*it_split).shares().toString(); qA["price"] = shares.isZero() ? "" : xr.convert(MyMoneyMoney::precToDenom(KMyMoneyGlobalSettings::pricePrecision())).toString(); if (((*it_split).action() == MyMoneySplit::ActionBuyShares) && (*it_split).shares().isNegative()) qA["action"] = "Sell"; qA["investaccount"] = splitAcc.parent().name(); } include_me = m_config.includes(splitAcc); a_fullname = splitAcc.fullName(); a_memo = (*it_split).memo(); qA["price"] = xr.convert(MyMoneyMoney::precToDenom(KMyMoneyGlobalSettings::pricePrecision())).toString(); qA["account"] = splitAcc.name(); qA["accountid"] = splitAcc.id(); qA["topaccount"] = splitAcc.topParentName(); qA["institution"] = institution.isEmpty() ? i18n("No Institution") : file->institution(institution).name(); //FIXME-ALEX Is this useless? Isn't constructSplitsTable called only for cashflow type report? QString delimiter = ""; for (int i = 0; i < tagIdList.size(); i++) { qA["tag"] += delimiter + file->tag(tagIdList[i]).name().simplified(); delimiter = ','; } qA["payee"] = payee.isEmpty() ? i18n("[Empty Payee]") : file->payee(payee).name().simplified(); qA["reconciledate"] = (*it_split).reconcileDate().toString(Qt::ISODate); qA["reconcileflag"] = KMyMoneyUtils::reconcileStateToString((*it_split).reconcileFlag(), true); qA["number"] = (*it_split).number(); qA["memo"] = a_memo; qS["reconciledate"] = qA["reconciledate"]; qS["reconcileflag"] = qA["reconcileflag"]; qS["number"] = qA["number"]; qS["topcategory"] = splitAcc.topParentName(); // only include the configured accounts if (include_me) { // add the "summarized" split transaction // this is the sub-total of the split detail // convert to lowest fraction qA["value"] = ((*it_split).shares() * xr).convert(fraction).toString(); - qA["rank"] = '0'; + qA["rank"] = '1'; //fill in account information if (! splitAcc.isIncomeExpense() && it_split != myBegin) { qA["account"] = ((*it_split).shares().isNegative()) ? i18n("Transfer to %1", myBeginAcc.fullName()) : i18n("Transfer from %1", myBeginAcc.fullName()); } else if (it_split == myBegin) { //handle the main split if ((splits.count() > 2)) { //if it is the main split and has multiple splits, note that qA["account"] = i18n("[Split Transaction]"); } else { //fill the account name of the second split QList::const_iterator tempSplit = splits.begin(); //there are supposed to be only 2 splits if we ever get here if (tempSplit == myBegin && splits.count() > 1) ++tempSplit; //show the name of the category, or "transfer to/from" if it as an account ReportAccount tempSplitAcc = (*tempSplit).accountId(); if (! tempSplitAcc.isIncomeExpense()) { qA["account"] = ((*it_split).shares().isNegative()) ? i18n("Transfer to %1", tempSplitAcc.fullName()) : i18n("Transfer from %1", tempSplitAcc.fullName()); } else { qA["account"] = tempSplitAcc.fullName(); } } } else { //in any other case, fill in the account name of the main split qA["account"] = myBeginAcc.fullName(); } //category data is always the one of the split qA ["category"] = splitAcc.fullName(); qA ["topcategory"] = splitAcc.topParentName(); qA ["categorytype"] = KMyMoneyUtils::accountTypeToString(splitAcc.accountGroup()); m_rows += qA; // track accts that will need opening and closing balances accts.insert(splitAcc.id(), splitAcc); } ++it_split; // look for wrap-around if (it_split == splits.end()) it_split = splits.begin(); //check if there have been more passes than there are splits //this is to prevent infinite loops in cases of data inconsistency -- asoliverez ++pass; if (pass > splits.count()) break; } while (it_split != myBegin); if (loan_special_case) { m_rows += qA; } } // now run through our accts list and add opening and closing balances switch (m_config.rowType()) { case MyMoneyReport::eAccount: case MyMoneyReport::eTopAccount: break; // case MyMoneyReport::eCategory: // case MyMoneyReport::eTopCategory: // case MyMoneyReport::ePayee: // case MyMoneyReport::eMonth: // case MyMoneyReport::eWeek: default: return; } QDate startDate, endDate; report.validDateRange(startDate, endDate); QString strStartDate = startDate.toString(Qt::ISODate); QString strEndDate = endDate.toString(Qt::ISODate); startDate = startDate.addDays(-1); QMap::const_iterator it_account, accts_end; for (it_account = accts.constBegin(); it_account != accts.constEnd(); ++it_account) { TableRow qA; ReportAccount account = (* it_account); //get fraction for account int fraction = account.currency().smallestAccountFraction(); //use base currency fraction if not initialized if (fraction == -1) fraction = file->baseCurrency().smallestAccountFraction(); QString institution = account.institutionId(); // use the institution of the parent for stock accounts if (account.isInvest()) institution = account.parent().institutionId(); MyMoneyMoney startBalance, endBalance, startPrice, endPrice; MyMoneyMoney startShares, endShares; //get price and convert currency if necessary if (m_config.isConvertCurrency()) { startPrice = (account.deepCurrencyPrice(startDate) * account.baseCurrencyPrice(startDate)).reduce(); endPrice = (account.deepCurrencyPrice(endDate) * account.baseCurrencyPrice(endDate)).reduce(); } else { startPrice = account.deepCurrencyPrice(startDate).reduce(); endPrice = account.deepCurrencyPrice(endDate).reduce(); } startShares = file->balance(account.id(), startDate); endShares = file->balance(account.id(), endDate); //get starting and ending balances startBalance = startShares * startPrice; endBalance = endShares * endPrice; //starting balance // don't show currency if we're converting or if it's not foreign qA["currency"] = (m_config.isConvertCurrency() || ! account.isForeignCurrency()) ? "" : account.currency().id(); qA["accountid"] = account.id(); qA["account"] = account.name(); qA["topaccount"] = account.topParentName(); qA["institution"] = institution.isEmpty() ? i18n("No Institution") : file->institution(institution).name(); - qA["rank"] = "-2"; + qA["rank"] = "0"; qA["price"] = startPrice.convert(MyMoneyMoney::precToDenom(KMyMoneyGlobalSettings::pricePrecision())).toString(); if (account.isInvest()) { qA["shares"] = startShares.toString(); } qA["postdate"] = strStartDate; qA["balance"] = startBalance.convert(fraction).toString(); qA["value"].clear(); qA["id"] = 'A'; m_rows += qA; + qA["rank"] = "3"; //ending balance qA["price"] = endPrice.convert(MyMoneyMoney::precToDenom(KMyMoneyGlobalSettings::pricePrecision())).toString(); if (account.isInvest()) { qA["shares"] = endShares.toString(); } qA["postdate"] = strEndDate; qA["balance"] = endBalance.toString(); qA["id"] = 'Z'; m_rows += qA; } } } diff --git a/kmymoney/reports/querytable.h b/kmymoney/reports/querytable.h index 6dacfb18d..27af25150 100644 --- a/kmymoney/reports/querytable.h +++ b/kmymoney/reports/querytable.h @@ -1,158 +1,162 @@ /*************************************************************************** querytable.h ------------------- begin : Fri Jul 23 2004 copyright : (C) 2004-2005 by Ace Jones (C) 2007 Sascha Pfau ***************************************************************************/ /**************************************************************************** Contains code from the func_xirr and related methods of financial.cpp - KOffice 1.6 by Sascha Pfau. Sascha agreed to relicense those methods under GPLv2 or later. *****************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #ifndef QUERYTABLE_H #define QUERYTABLE_H // ---------------------------------------------------------------------------- // QT Includes #include #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes #include "mymoneyreport.h" #include "listtable.h" namespace reports { class ReportAccount; +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: QueryTable(const MyMoneyReport&); void init(); protected: void constructAccountTable(); + void constructTotalRows(); void constructTransactionTable(); void constructCapitalGainRow(const ReportAccount& account, TableRow& result) const; - void constructPerformanceRow(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; + void constructPerformanceRow(const ReportAccount& account, TableRow& result, CashFlowList &all) const; void constructSplitsTable(); }; // // Cash Flow analysis tools for investment reports // class CashFlowListItem { public: CashFlowListItem() {} CashFlowListItem(const QDate& _date, const MyMoneyMoney& _value): m_date(_date), m_value(_value) {} bool operator<(const CashFlowListItem& _second) const { return m_date < _second.m_date; } bool operator<=(const CashFlowListItem& _second) const { return m_date <= _second.m_date; } bool operator>(const CashFlowListItem& _second) const { return m_date > _second.m_date; } const QDate& date() const { return m_date; } const MyMoneyMoney& value() const { return m_value; } MyMoneyMoney NPV(double _rate) const; static void setToday(const QDate& _today) { m_sToday = _today; } const QDate& today() const { return m_sToday; } private: QDate m_date; MyMoneyMoney m_value; static QDate m_sToday; }; class CashFlowList: public QList { public: CashFlowList() {} MyMoneyMoney NPV(double rate) const; double IRR() const; MyMoneyMoney total() const; void dumpDebug() const; /** * Function: XIRR * * Compute the internal rate of return for a non-periodic series of cash flows. * * XIRR ( Values; Dates; [ Guess = 0.1 ] ) **/ double calculateXIRR() const; protected: CashFlowListItem mostRecent() const; private: /** * helper: xirrResult * * args[0] = values * args[1] = dates **/ double xirrResult(double& rate) const; /** * * helper: xirrResultDerive * * args[0] = values * args[1] = dates **/ double xirrResultDerive(double& rate) const; }; } #endif // QUERYREPORT_H