diff --git a/kmymoney/reports/listtable.cpp b/kmymoney/reports/listtable.cpp --- a/kmymoney/reports/listtable.cpp +++ b/kmymoney/reports/listtable.cpp @@ -4,6 +4,7 @@ begin : Sat 28 jun 2008 copyright : (C) 2004-2005 by Ace Jones 2008 by Alvaro Soliverez + (C) 2017 Łukasz Wojniłowicz ***************************************************************************/ @@ -48,68 +49,6 @@ // **************************************************************************** // -// 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 // // **************************************************************************** @@ -165,7 +104,6 @@ void ListTable::render(QString& result, QString& csv) const { - MyMoneyMoney grandtotal; MyMoneyFile* file = MyMoneyFile::instance(); result = ""; @@ -201,9 +139,11 @@ // 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 @@ -291,22 +231,10 @@ 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()); } // @@ -317,6 +245,7 @@ // ***DV*** MyMoneyMoney startingBalance; + MyMoneyMoney balanceChange = MyMoneyMoney(); for (QList::const_iterator it_row = m_rows.begin(); it_row != m_rows.end(); ++it_row) { @@ -328,109 +257,58 @@ 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 += - "" - "" + - i18nc("Total balance", "Total") + ' ' + oldName + "" - "" + subtotal_html + "\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 += "" - "" + - (*it_group).name() + "\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" @@ -448,11 +326,13 @@ } // ***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) { @@ -472,24 +352,35 @@ } } } - // 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 += ""; + if (!(*it_row)["depth"].isEmpty()) + result += i18nc("Total balance", "Total") + ' ' + prevGrpNames.at((*it_row)["depth"].toInt()) + ""; + 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)) { @@ -523,9 +414,14 @@ 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("%2%1%%3").arg(data, tlinkBegin, tlinkEnd); - csv += data + "%,"; + if (data.isEmpty()) { + result += QString(""); + csv += "\"\","; + } else { + data = (MyMoneyMoney(data) * MyMoneyMoney(100, 1)).formatMoney(fraction); + result += QString("%2%1%%3").arg(data, tlinkBegin, tlinkEnd); + csv += data + "%,"; + } } else if (dateColumns.contains(*it_column)) { // do this before we possibly change data csv += "\"" + data + "\","; @@ -535,9 +431,9 @@ QDate qd = QDate::fromString(data, Qt::ISODate); data = QLocale().toString(qd, QLocale::ShortFormat); } - result += QString("%2%1%3").arg(data, tlinkBegin, tlinkEnd); + result += QString("%2%1%3").arg(data, tlinkBegin, tlinkEnd); } else { - result += QString("%2%1%3").arg(data, tlinkBegin, tlinkEnd); + result += QString("%2%1%3").arg(data, tlinkBegin, tlinkEnd); csv += "\"" + data + "\","; } ++it_column; @@ -548,59 +444,6 @@ 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 += "" - "" + - i18nc("Total balance", "Total") + ' ' + (*it_group).oldName() + "" - "" + subtotal_html + "\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 += "" - "" + - i18n("Grand Total") + "" - "" + grandtotal_html + "\n"; - csv += "\"" + i18n("Grand Total") + "\",\"" + grandtotal_csv + "\"\n"; - } - } result += "\n"; } diff --git a/kmymoney/reports/querytable.h b/kmymoney/reports/querytable.h --- a/kmymoney/reports/querytable.h +++ b/kmymoney/reports/querytable.h @@ -44,6 +44,7 @@ { class ReportAccount; +class CashFlowList; /** * Calculates a query of information about the transaction database. @@ -66,9 +67,12 @@ 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(); }; diff --git a/kmymoney/reports/querytable.cpp b/kmymoney/reports/querytable.cpp --- a/kmymoney/reports/querytable.cpp +++ b/kmymoney/reports/querytable.cpp @@ -338,7 +338,7 @@ 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: @@ -375,12 +375,12 @@ 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"; @@ -390,8 +390,149 @@ 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() @@ -683,16 +824,16 @@ // 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"); @@ -746,13 +887,13 @@ //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(); @@ -812,7 +953,7 @@ //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(); @@ -949,7 +1090,7 @@ 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()) { @@ -971,12 +1112,40 @@ 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; @@ -1092,36 +1261,19 @@ } // 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() && @@ -1254,6 +1406,8 @@ //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) { @@ -1270,8 +1424,9 @@ 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 @@ -1281,7 +1436,7 @@ continue; // help for sort and render functions - qaccountrow["rank"] = '0'; + qaccountrow["rank"] = '1'; // // Handle currency conversion // @@ -1328,9 +1483,35 @@ 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() @@ -1522,7 +1703,7 @@ // 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) { @@ -1654,7 +1835,7 @@ 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()) { @@ -1667,6 +1848,7 @@ qA["id"] = 'A'; m_rows += qA; + qA["rank"] = "3"; //ending balance qA["price"] = endPrice.convert(MyMoneyMoney::precToDenom(KMyMoneyGlobalSettings::pricePrecision())).toString();