File indexing completed on 2024-06-23 05:02:19

0001 /*
0002     SPDX-FileCopyrightText: 2005 Ace Jones <acejones@users.sourceforge.net>
0003     SPDX-FileCopyrightText: 2017-2018 Łukasz Wojniłowicz <lukasz.wojnilowicz@gmail.com>
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "querytable.h"
0008 
0009 #include <cmath>
0010 
0011 // ----------------------------------------------------------------------------
0012 // QT Includes
0013 
0014 #include <QList>
0015 #include <QDebug>
0016 
0017 // ----------------------------------------------------------------------------
0018 // KDE Includes
0019 
0020 #include <KLocalizedString>
0021 
0022 // ----------------------------------------------------------------------------
0023 // Project Includes
0024 
0025 #include "cashflowlist.h"
0026 #include "mymoneyfile.h"
0027 #include "mymoneyaccount.h"
0028 #include "mymoneysecurity.h"
0029 #include "mymoneyinstitution.h"
0030 #include "mymoneyprice.h"
0031 #include "mymoneypayee.h"
0032 #include "mymoneytag.h"
0033 #include "mymoneysplit.h"
0034 #include "mymoneytransaction.h"
0035 #include "mymoneyreport.h"
0036 #include "mymoneyexception.h"
0037 #include "mymoneyutils.h"
0038 #include "kmymoneyutils.h"
0039 #include "reportaccount.h"
0040 #include "mymoneyenums.h"
0041 
0042 constexpr QChar tagSeparator = QChar(QChar::ParagraphSeparator);
0043 
0044 namespace reports
0045 {
0046 
0047 // ****************************************************************************
0048 //
0049 // QueryTable implementation
0050 //
0051 // ****************************************************************************
0052 
0053 /**
0054   * TODO
0055   *
0056   * - Collapse 2- & 3- groups when they are identical
0057   * - Way more test cases (especially splits & transfers)
0058   * - Option to collapse splits
0059   * - Option to exclude transfers
0060   *
0061   */
0062 
0063 QueryTable::QueryTable(const MyMoneyReport& _report): ListTable(_report)
0064 {
0065     // separated into its own method to allow debugging (setting breakpoints
0066     // directly in ctors somehow does not work for me (ipwizard))
0067     // TODO: remove the init() method and move the code back to the ctor
0068     init();
0069 }
0070 
0071 void QueryTable::init()
0072 {
0073     m_columns.clear();
0074     m_group.clear();
0075     m_subtotal.clear();
0076     m_postcolumns.clear();
0077     switch (m_config.rowType()) {
0078     case eMyMoney::Report::RowType::AccountByTopAccount:
0079     case eMyMoney::Report::RowType::EquityType:
0080     case eMyMoney::Report::RowType::AccountType:
0081     case eMyMoney::Report::RowType::Institution:
0082         constructAccountTable();
0083         m_columns << ctAccount;
0084         break;
0085 
0086     case eMyMoney::Report::RowType::Account:
0087         constructTransactionTable();
0088         m_columns << ctAccountID << ctPostDate;
0089         break;
0090 
0091     case eMyMoney::Report::RowType::Payee:
0092     case eMyMoney::Report::RowType::Tag:
0093     case eMyMoney::Report::RowType::Month:
0094     case eMyMoney::Report::RowType::Week:
0095         constructTransactionTable();
0096         m_columns << ctPostDate << ctAccount;
0097         break;
0098     case eMyMoney::Report::RowType::CashFlow:
0099         constructSplitsTable();
0100         m_columns << ctPostDate;
0101         break;
0102     default:
0103         constructTransactionTable();
0104         m_columns << ctPostDate;
0105     }
0106 
0107     // Sort the data to match the report definition
0108     m_subtotal << ctValue;
0109 
0110     switch (m_config.rowType()) {
0111     case eMyMoney::Report::RowType::CashFlow:
0112         m_group << ctCategoryType << ctTopCategory << ctCategory;
0113         break;
0114     case eMyMoney::Report::RowType::Category:
0115         m_group << ctCategoryType << ctTopCategory << ctCategory;
0116         break;
0117     case eMyMoney::Report::RowType::TopCategory:
0118         m_group << ctCategoryType << ctTopCategory;
0119         break;
0120     case eMyMoney::Report::RowType::TopAccount:
0121         m_group << ctTopAccount << ctAccount;
0122         break;
0123     case eMyMoney::Report::RowType::Account:
0124         m_group << ctAccount;
0125         break;
0126     case eMyMoney::Report::RowType::AccountReconcile:
0127         m_group << ctAccount << ctReconcileFlag;
0128         break;
0129     case eMyMoney::Report::RowType::Payee:
0130         m_group << ctPayee;
0131         break;
0132     case eMyMoney::Report::RowType::Tag:
0133         m_group << ctTag;
0134         break;
0135     case eMyMoney::Report::RowType::Month:
0136         m_group << ctMonth;
0137         break;
0138     case eMyMoney::Report::RowType::Week:
0139         m_group << ctWeek;
0140         break;
0141     case eMyMoney::Report::RowType::AccountByTopAccount:
0142         m_group << ctTopAccount;
0143         break;
0144     case eMyMoney::Report::RowType::EquityType:
0145         m_group << ctEquityType;
0146         break;
0147     case eMyMoney::Report::RowType::AccountType:
0148         m_group << ctType;
0149         break;
0150     case eMyMoney::Report::RowType::Institution:
0151         m_group << ctInstitution << ctTopAccount;
0152         break;
0153     default:
0154         throw MYMONEYEXCEPTION_CSTRING("QueryTable::QueryTable(): unhandled row type");
0155     }
0156 
0157     QVector<cellTypeE> sort = QVector<cellTypeE>::fromList(m_group) << QVector<cellTypeE>::fromList(m_columns) << ctID << ctRank;
0158 
0159     m_columns.clear();
0160     switch (m_config.rowType()) {
0161     case eMyMoney::Report::RowType::AccountByTopAccount:
0162     case eMyMoney::Report::RowType::EquityType:
0163     case eMyMoney::Report::RowType::AccountType:
0164     case eMyMoney::Report::RowType::Institution:
0165         m_columns << ctAccount;
0166         break;
0167 
0168     default:
0169         m_columns << ctPostDate;
0170     }
0171 
0172     unsigned qc = m_config.queryColumns();
0173 
0174     if (qc & eMyMoney::Report::QueryColumn::Number)
0175         m_columns << ctNumber;
0176     if (qc & eMyMoney::Report::QueryColumn::Payee)
0177         m_columns << ctPayee;
0178     if (qc & eMyMoney::Report::QueryColumn::Tag)
0179         m_columns << ctTag;
0180     if (qc & eMyMoney::Report::QueryColumn::Category)
0181         m_columns << ctCategory;
0182     if (qc & eMyMoney::Report::QueryColumn::Account)
0183         m_columns << ctAccount;
0184     if (qc & eMyMoney::Report::QueryColumn::Reconciled)
0185         m_columns << ctReconcileFlag;
0186     if (qc & eMyMoney::Report::QueryColumn::Memo)
0187         m_columns << ctMemo;
0188     if (qc & eMyMoney::Report::QueryColumn::Action)
0189         m_columns << ctAction;
0190     if (qc & eMyMoney::Report::QueryColumn::Shares)
0191         m_columns << ctShares;
0192     if (qc & eMyMoney::Report::QueryColumn::Price)
0193         m_columns << ctPrice;
0194     if (qc & eMyMoney::Report::QueryColumn::Performance) {
0195         m_subtotal.clear();
0196         switch (m_config.investmentSum()) {
0197         case eMyMoney::Report::InvestmentSum::OwnedAndSold:
0198             m_columns << ctBuys << ctSells << ctReinvestIncome << ctCashIncome
0199                       << ctEndingBalance << ctReturn << ctReturnInvestment;
0200             m_subtotal << ctBuys << ctSells << ctReinvestIncome << ctCashIncome
0201                        << ctEndingBalance << ctReturn << ctReturnInvestment;
0202             break;
0203         case eMyMoney::Report::InvestmentSum::Owned:
0204             m_columns << ctBuys << ctReinvestIncome << ctMarketValue
0205                       << ctReturn << ctReturnInvestment;
0206             m_subtotal << ctBuys << ctReinvestIncome << ctMarketValue
0207                        << ctReturn << ctReturnInvestment;
0208             break;
0209         case eMyMoney::Report::InvestmentSum::Sold:
0210             m_columns << ctBuys << ctSells << ctCashIncome
0211                       << ctReturn << ctReturnInvestment;
0212             m_subtotal << ctBuys << ctSells << ctCashIncome
0213                        << ctReturn << ctReturnInvestment;
0214             break;
0215         case eMyMoney::Report::InvestmentSum::Period:
0216         default:
0217             m_columns << ctStartingBalance << ctBuys << ctSells
0218                       << ctReinvestIncome << ctCashIncome << ctEndingBalance
0219                       << ctReturn << ctReturnInvestment;
0220             m_subtotal << ctStartingBalance << ctBuys << ctSells
0221                        << ctReinvestIncome << ctCashIncome << ctEndingBalance
0222                        << ctReturn << ctReturnInvestment;
0223             break;
0224         }
0225     }
0226     if (qc & eMyMoney::Report::QueryColumn::CapitalGain) {
0227         m_subtotal.clear();
0228         switch (m_config.investmentSum()) {
0229         case eMyMoney::Report::InvestmentSum::Owned:
0230             m_columns << ctShares << ctBuyPrice << ctLastPrice
0231                       << ctBuys << ctMarketValue << ctPercentageGain
0232                       << ctCapitalGain;
0233             m_subtotal << ctShares << ctBuyPrice << ctLastPrice
0234                        << ctBuys << ctMarketValue << ctPercentageGain
0235                        << ctCapitalGain;
0236             break;
0237         case eMyMoney::Report::InvestmentSum::Sold:
0238         default:
0239             m_columns << ctBuys << ctSells << ctCapitalGain;
0240             m_subtotal << ctBuys << ctSells << ctCapitalGain;
0241             if (m_config.isShowingSTLTCapitalGains()) {
0242                 m_columns << ctBuysST << ctSellsST << ctCapitalGainST
0243                           << ctBuysLT << ctSellsLT << ctCapitalGainLT;
0244                 m_subtotal << ctBuysST << ctSellsST << ctCapitalGainST
0245                            << ctBuysLT << ctSellsLT << ctCapitalGainLT;
0246             }
0247             break;
0248         }
0249     }
0250     if (qc & eMyMoney::Report::QueryColumn::Loan) {
0251         m_columns << ctPayment << ctInterest << ctFees;
0252         m_postcolumns << ctBalance;
0253     }
0254     if (qc & eMyMoney::Report::QueryColumn::Balance)
0255         m_postcolumns << ctBalance;
0256 
0257     TableRow::setSortCriteria(sort);
0258     std::sort(m_rows.begin(), m_rows.end());
0259     if (m_config.isShowingColumnTotals())
0260         constructTotalRows(); // adds total rows to m_rows
0261 }
0262 
0263 void QueryTable::constructTotalRows()
0264 {
0265     if (m_rows.isEmpty())
0266         return;
0267 
0268     // qSort places grand total at first positions, because it doesn't belong to any group
0269     // subtotals are placed in front of the topAccount rows
0270     const auto rows = m_rows.count();
0271     for (int i = 0; i < rows-1; ++i) {
0272         // it should be unlikely that total row is at the top of rows, so...
0273         if ((m_rows.at(i)[ctRank] == QLatin1String("5")) || (m_rows.at(i)[ctTopAccount].isEmpty())) {
0274             // check if there are other entries than totals so moving makes sense
0275             for (int j = i+1; j <= rows-1; ++j) {
0276                 if ((m_rows.at(j)[ctRank] != QLatin1String("5")) && (!m_rows.at(j)[ctTopAccount].isEmpty())) {
0277                     m_rows.move(i, rows - 1);                   // ...move it at the end
0278                     --i;                                        // check the same slot again
0279                     break;
0280                 }
0281             }
0282         } else if (m_rows.at(i)[ctRank] == QLatin1String("4")) {
0283             // search last entry of same topAccount
0284             auto last = i+1;
0285             while ((m_rows.at(i)[ctTopAccount] == m_rows.at(last)[ctTopAccount]) && (last < (rows - 1))) {
0286                 ++last;
0287             }
0288             // move subtotal to last entry
0289             m_rows.move(i, last - 1);                       // ...move to end of entries
0290             i = last-1;
0291         }
0292     }
0293 
0294     MyMoneyFile* file = MyMoneyFile::instance();
0295     QList<cellTypeE> subtotals = m_subtotal;
0296     QList<cellTypeE> groups = m_group;
0297     QList<cellTypeE> columns = m_columns;
0298     if (!m_subtotal.isEmpty() && subtotals.count() == 1)
0299         columns.append(m_subtotal);
0300     QList<cellTypeE> postcolumns = m_postcolumns;
0301     if (!m_postcolumns.isEmpty())
0302         columns.append(postcolumns);
0303 
0304     QMap<QString, QList<QMap<cellTypeE, MyMoneyMoney>>> totalCurrency;
0305     QList<QMap<cellTypeE, MyMoneyMoney>> totalGroups;
0306     QMap<cellTypeE, MyMoneyMoney> totalsValues;
0307 
0308     // initialize all total values under summed columns to be zero
0309     for (const auto& subtotal : qAsConst(subtotals)) {
0310         totalsValues.insert(subtotal, MyMoneyMoney());
0311     }
0312     totalsValues.insert(ctRowsCount, MyMoneyMoney());
0313 
0314     // create total groups containing totals row for each group
0315     totalGroups.append(totalsValues);  // prepend with extra group for grand total
0316     for (int j = 0; j < groups.count(); ++j) {
0317         totalGroups.append(totalsValues);
0318     }
0319 
0320     QList<TableRow> stashedTotalRows;
0321     int iCurrentRow, iNextRow;
0322     for (iCurrentRow = 0; iCurrentRow < m_rows.count();) {
0323         iNextRow = iCurrentRow + 1;
0324 
0325         // total rows are useless at summing so remove whole block of them at once
0326         while (iNextRow != m_rows.count() && (m_rows.at(iNextRow).value(ctRank) == QLatin1String("4") || m_rows.at(iNextRow).value(ctRank) == QLatin1String("5"))) {
0327             stashedTotalRows.append(m_rows.takeAt(iNextRow)); // ...but stash them just in case
0328         }
0329 
0330         bool lastRow = (iNextRow == m_rows.count());
0331 
0332         // sum all subtotal values for lowest group
0333         QString currencyID = m_rows.at(iCurrentRow).value(ctCurrency);
0334         if (m_rows.at(iCurrentRow).value(ctRank) == QLatin1String("1")) { // don't sum up on balance (rank = 0 || rank = 3) and minor split (rank = 2)
0335             for (const auto& subtotal : qAsConst(subtotals)) {
0336                 if (!totalCurrency.contains(currencyID))
0337                     totalCurrency[currencyID].append(totalGroups);
0338                 totalCurrency[currencyID].last()[subtotal] += MyMoneyMoney(m_rows.at(iCurrentRow)[subtotal]);
0339             }
0340             totalCurrency[currencyID].last()[ctRowsCount] += MyMoneyMoney::ONE;
0341         }
0342 
0343         auto levelToClose = groups.count();
0344         if (!lastRow) {
0345             for (int i = 0; i < groups.count(); ++i) {
0346                 if (m_rows.at(iCurrentRow)[groups.at(i)] != m_rows.at(iNextRow)[groups.at(i)]) {
0347                     levelToClose = i;
0348                     break;
0349                 }
0350             }
0351         } else {
0352             levelToClose = 0;   // all, we're done
0353         }
0354         // iterate over groups from the lowest to the highest to close groups
0355         for (int i = groups.count() - 1; i >= levelToClose ; --i) {
0356             bool isMainCurrencyTotal = true;
0357             QMap<QString, QList<QMap<cellTypeE, MyMoneyMoney>>>::iterator currencyGrp = totalCurrency.begin();
0358             while (currencyGrp != totalCurrency.end()) {
0359                 if (!MyMoneyMoney((*currencyGrp).at(i + 1).value(ctRowsCount)).isZero()) {    // if no rows summed up, then no totals row
0360                     TableRow totalsRow;
0361                     // sum all subtotal values for higher groups (excluding grand total) and reset lowest group values
0362                     QMap<cellTypeE, MyMoneyMoney>::iterator upperGrp = (*currencyGrp)[i].begin();
0363                     QMap<cellTypeE, MyMoneyMoney>::iterator lowerGrp = (*currencyGrp)[i + 1].begin();
0364 
0365                     while(upperGrp != (*currencyGrp)[i].end()) {
0366                         totalsRow[lowerGrp.key()] = lowerGrp.value().toString();  // fill totals row with subtotal values...
0367                         (*upperGrp) += (*lowerGrp);
0368                         //          (*lowerGrp) = MyMoneyMoney();
0369                         ++upperGrp;
0370                         ++lowerGrp;
0371                     }
0372 
0373                     // custom total values calculations
0374                     for (const auto& subtotal : qAsConst(subtotals)) {
0375                         if (subtotal == ctReturnInvestment)
0376                             totalsRow[subtotal] = helperROI((*currencyGrp).at(i + 1).value(ctBuys) - (*currencyGrp).at(i + 1).value(ctReinvestIncome), (*currencyGrp).at(i + 1).value(ctSells),
0377                                                             (*currencyGrp).at(i + 1).value(ctStartingBalance), (*currencyGrp).at(i + 1).value(ctEndingBalance) + (*currencyGrp).at(i + 1).value(ctMarketValue),
0378                                                             (*currencyGrp).at(i + 1).value(ctCashIncome));
0379                         else if (subtotal == ctPercentageGain) {
0380                             const MyMoneyMoney denominator = (*currencyGrp).at(i + 1).value(ctBuys).abs();
0381                             totalsRow[subtotal] = denominator.isZero() ? QString():
0382                                                   (((*currencyGrp).at(i + 1).value(ctBuys) + (*currencyGrp).at(i + 1).value(ctMarketValue)) / denominator).toString();
0383                         } else if (subtotal == ctPrice)
0384                             totalsRow[subtotal] = MyMoneyMoney((*currencyGrp).at(i + 1).value(ctPrice) / (*currencyGrp).at(i + 1).value(ctRowsCount)).toString();
0385                     }
0386 
0387                     // total values that aren't calculated here, but are taken untouched from external source, e.g. constructPerformanceRow
0388                     if (!stashedTotalRows.isEmpty()) {
0389                         for (int j = 0; j < stashedTotalRows.count(); ++j) {
0390                             if (stashedTotalRows.at(j).value(ctCurrency) != currencyID)
0391                                 continue;
0392                             for (const auto& subtotal : qAsConst(subtotals)) {
0393                                 if (subtotal == ctReturn)
0394                                     totalsRow[ctReturn] = stashedTotalRows.takeAt(j).value(ctReturn);
0395                             }
0396                             break;
0397                         }
0398                     }
0399 
0400                     (*currencyGrp).replace(i + 1, totalsValues);
0401                     for (int j = 0; j < groups.count(); ++j) {
0402                         totalsRow[groups.at(j)] = m_rows.at(iCurrentRow)[groups.at(j)];   // ...and identification
0403                     }
0404 
0405                     currencyID = currencyGrp.key();
0406                     if (currencyID.isEmpty() && totalCurrency.count() > 1)
0407                         currencyID = file->baseCurrency().id();
0408                     totalsRow[ctCurrency] = currencyID;
0409                     if (isMainCurrencyTotal) {
0410                         totalsRow[ctRank] = QLatin1Char('4');
0411                         isMainCurrencyTotal = false;
0412                     } else
0413                         totalsRow[ctRank] = QLatin1Char('5');
0414                     totalsRow[ctDepth] = QString::number(i);
0415                     totalsRow.remove(ctRowsCount);
0416 
0417                     m_rows.insert(iNextRow++, totalsRow);  // iCurrentRow and iNextRow can diverge here by more than one
0418                 }
0419                 ++currencyGrp;
0420             }
0421         }
0422 
0423         // code to put grand total row
0424         if (lastRow) {
0425             bool isMainCurrencyTotal = true;
0426             QMap<QString, QList<QMap<cellTypeE, MyMoneyMoney>>>::iterator currencyGrp = totalCurrency.begin();
0427             while (currencyGrp != totalCurrency.end()) {
0428                 TableRow totalsRow;
0429                 QMap<cellTypeE, MyMoneyMoney>::const_iterator grandTotalGrp = (*currencyGrp)[0].constBegin();
0430                 while(grandTotalGrp != (*currencyGrp)[0].constEnd()) {
0431                     totalsRow[grandTotalGrp.key()] = grandTotalGrp.value().toString();
0432                     ++grandTotalGrp;
0433                 }
0434 
0435                 for (const auto& subtotal : qAsConst(subtotals)) {
0436                     if (subtotal == ctReturnInvestment) {
0437                         totalsRow[subtotal] = helperROI((*currencyGrp).at(0).value(ctBuys) - (*currencyGrp).at(0).value(ctReinvestIncome), (*currencyGrp).at(0).value(ctSells),
0438                                                         (*currencyGrp).at(0).value(ctStartingBalance), (*currencyGrp).at(0).value(ctEndingBalance) + (*currencyGrp).at(0).value(ctMarketValue),
0439                                                         (*currencyGrp).at(0).value(ctCashIncome));
0440                     } else if (subtotal == ctPercentageGain) {
0441                         if (!(*currencyGrp).at(0).value(ctBuys).abs().isZero()) {
0442                             totalsRow[subtotal] =
0443                                 (((*currencyGrp).at(0).value(ctBuys) + (*currencyGrp).at(0).value(ctMarketValue)) / (*currencyGrp).at(0).value(ctBuys).abs())
0444                                     .toString();
0445                         }
0446                     } else if (subtotal == ctPrice) {
0447                         totalsRow[subtotal] = MyMoneyMoney((*currencyGrp).at(0).value(ctPrice) / (*currencyGrp).at(0).value(ctRowsCount)).toString();
0448                     }
0449                 }
0450 
0451                 if (!stashedTotalRows.isEmpty()) {
0452                     for (int j = 0; j < stashedTotalRows.count(); ++j) {
0453                         for (const auto& subtotal : qAsConst(subtotals)) {
0454                             if (subtotal == ctReturn)
0455                                 totalsRow[ctReturn] = stashedTotalRows.takeAt(j).value(ctReturn);
0456                         }
0457                     }
0458                 }
0459 
0460                 for (int j = 0; j < groups.count(); ++j) {
0461                     totalsRow[groups.at(j)] = QString();      // no identification
0462                 }
0463 
0464                 currencyID = currencyGrp.key();
0465                 if (currencyID.isEmpty() && totalCurrency.count() > 1)
0466                     currencyID = file->baseCurrency().id();
0467                 totalsRow[ctCurrency] = currencyID;
0468                 if (isMainCurrencyTotal) {
0469                     totalsRow[ctRank] = QLatin1Char('4');
0470                     isMainCurrencyTotal = false;
0471                 } else
0472                     totalsRow[ctRank] = QLatin1Char('5');
0473                 totalsRow[ctDepth] = QString();
0474 
0475                 m_rows.append(totalsRow);
0476                 if (!m_containsNonBaseCurrency && totalsRow[ctCurrency] != file->baseCurrency().id()) {
0477                     m_containsNonBaseCurrency = true;
0478                 }
0479                 ++currencyGrp;
0480             }
0481             break;                                      // no use to loop further
0482         }
0483         iCurrentRow = iNextRow;                       // iCurrent makes here a leap forward by at least one
0484     }
0485 }
0486 
0487 void QueryTable::constructTransactionTable()
0488 {
0489     MyMoneyFile* file = MyMoneyFile::instance();
0490 
0491     //make sure we have all subaccounts of investment accounts
0492     includeInvestmentSubAccounts();
0493 
0494     MyMoneyReport report(m_config);
0495     report.setReportAllSplits(false);
0496     report.setConsiderCategory(true);
0497 
0498     bool use_transfers;
0499     bool use_summary;
0500     bool hide_details;
0501 
0502     switch (m_config.rowType()) {
0503     case eMyMoney::Report::RowType::Category:
0504     case eMyMoney::Report::RowType::TopCategory:
0505         use_summary = false;
0506         use_transfers = report.isIncludingTransfers();
0507         report.setTreatTransfersAsIncomeExpense(use_transfers);
0508         hide_details = false;
0509         break;
0510     case eMyMoney::Report::RowType::Payee:
0511         use_summary = false;
0512         use_transfers = report.isIncludingTransfers();
0513         report.setTreatTransfersAsIncomeExpense(use_transfers);
0514         hide_details = (m_config.detailLevel() == eMyMoney::Report::DetailLevel::None);
0515         break;
0516     case eMyMoney::Report::RowType::Tag:
0517         use_summary = false;
0518         use_transfers = report.isIncludingTransfers();
0519         report.setTreatTransfersAsIncomeExpense(use_transfers);
0520         hide_details = (m_config.detailLevel() == eMyMoney::Report::DetailLevel::None);
0521         break;
0522     default:
0523         use_summary = true;
0524         use_transfers = true;
0525         hide_details = (m_config.detailLevel() == eMyMoney::Report::DetailLevel::None);
0526         break;
0527     }
0528 
0529     // support for opening and closing balances
0530     QMap<QString, MyMoneyAccount> accts;
0531 
0532     //get all transactions for this report
0533     QList<MyMoneyTransaction> transactions;
0534     file->transactionList(transactions, report);
0535 
0536     for (QList<MyMoneyTransaction>::const_iterator it_transaction = transactions.constBegin(); it_transaction != transactions.constEnd(); ++it_transaction) {
0537 
0538         TableRow qA, qS;
0539         QList<TableRow> qStack;
0540         QDate pd;
0541 
0542         auto addRow = [&](const TableRow& row) {
0543             const auto tagIds = row[ctTag].split(tagSeparator, Qt::SkipEmptyParts);
0544             auto qT = row;
0545             if (m_config.rowType() == eMyMoney::Report::RowType::Tag) {
0546                 // if group by tags, we add the row for each tag we found
0547                 if (!tagIds.isEmpty()) {
0548                     for (const auto& tagId : qAsConst(tagIds)) {
0549                         qT[ctTag] = file->tag(tagId).name().simplified();
0550                         m_rows += qT;
0551                     }
0552                 } else {
0553                     qT[ctTag] = i18n("[No Tag]");
0554                     m_rows += qT;
0555                 }
0556             } else {
0557                 // otherwise, we combine the tags into one list
0558                 QString tags;
0559                 for (const auto& tagId : qAsConst(tagIds)) {
0560                     if (!tags.isEmpty()) {
0561                         tags.append(QLatin1Char(','));
0562                     }
0563                     tags.append(file->tag(tagId).name().simplified());
0564                 }
0565                 if (tags.isEmpty()) {
0566                     tags = i18n("[No Tag]");
0567                 }
0568                 qT[ctTag] = tags;
0569                 m_rows += qT;
0570             }
0571         };
0572 
0573         qA[ctID] = qS[ctID] = (* it_transaction).id();
0574         qA[ctEntryDate] = qS[ctEntryDate] = (* it_transaction).entryDate().toString(Qt::ISODate);
0575         qA[ctPostDate] = qS[ctPostDate] = (* it_transaction).postDate().toString(Qt::ISODate);
0576         qA[ctCommodity] = qS[ctCommodity] = (* it_transaction).commodity();
0577 
0578         pd = (* it_transaction).postDate();
0579         qA[ctMonth] = qS[ctMonth] = i18n("Month of %1", QDate(pd.year(), pd.month(), 1).toString(Qt::ISODate));
0580         qA[ctWeek] = qS[ctWeek] = i18n("Week of %1", pd.addDays(1 - pd.dayOfWeek()).toString(Qt::ISODate));
0581 
0582         if (report.isConvertCurrency())
0583             qA[ctCurrency] = qS[ctCurrency] = file->baseCurrency().id();
0584         else
0585             qA[ctCurrency] = qS[ctCurrency] = (*it_transaction).commodity();
0586 
0587         // to handle splits, we decide on which account to base the split
0588         // (a reference point or point of view so to speak). here we take the
0589         // first account that is a stock account or loan account (or the first account
0590         // that is not an income or expense account if there is no stock or loan account)
0591         // to be the account (qA) that will have the sub-item "split" entries. we add
0592         // one transaction entry (qS) for each subsequent entry in the split.
0593 
0594         const QList<MyMoneySplit>& splits = (*it_transaction).splits();
0595         QList<MyMoneySplit>::const_iterator myBegin, it_split;
0596 
0597         bool foundTaxAccount = false;
0598         for (it_split = splits.constBegin(), myBegin = splits.constEnd(); it_split != splits.constEnd(); ++it_split) {
0599             ReportAccount splitAcc((* it_split).accountId());
0600             // always put split with a "stock" account if it exists
0601             if (splitAcc.isInvest())
0602                 break;
0603 
0604             // remember if we have found a tax related account
0605             foundTaxAccount |= splitAcc.isInTaxReports();
0606 
0607             // prefer to put splits with a "loan" account if it exists
0608             if (splitAcc.isLoan())
0609                 myBegin = it_split;
0610 
0611             if ((myBegin == splits.end()) && ! splitAcc.isIncomeExpense()) {
0612                 // continue if split references an unselected account
0613                 if (report.includesAccount(splitAcc.id())) {
0614                     myBegin = it_split;
0615                 }
0616             }
0617         }
0618 
0619         // we can skip the transaction in case it is a tax report
0620         // and the transaction does not reference any tax related
0621         // account
0622         if (report.isTax() && !foundTaxAccount) {
0623             continue;
0624         }
0625 
0626         // select our "reference" split
0627         if (it_split == splits.end()) {
0628             it_split = myBegin;
0629         } else {
0630             myBegin = it_split;
0631         }
0632 
0633         // skip this transaction if we didn't find a valid base account - see the above description
0634         // for the base account's description - if we don't find it avoid a crash by skipping the transaction
0635         if (myBegin == splits.end())
0636             continue;
0637 
0638         // if the split is still unknown, use the first one. I have seen this
0639         // happen with a transaction that has only a single split referencing an income or expense
0640         // account and has an amount and value of 0. Such a transaction will fall through
0641         // the above logic and leave 'it_split' pointing to splits.end() which causes the remainder
0642         // of this to end in an infinite loop.
0643         if (it_split == splits.end()) {
0644             it_split = splits.begin();
0645         }
0646 
0647         // for "loan" reports, the loan transaction gets special treatment.
0648         // the splits of a loan transaction are placed on one line in the
0649         // reference (loan) account (qA). however, we process the matching
0650         // split entries (qS) normally.
0651 
0652         bool loan_special_case = false;
0653         if (m_config.queryColumns() & eMyMoney::Report::QueryColumn::Loan) {
0654             ReportAccount splitAcc((*it_split).accountId());
0655             loan_special_case = splitAcc.isLoan();
0656         }
0657 
0658         bool include_me = true;
0659         bool transaction_text = false; //indicates whether a text should be considered as a match for the transaction or for a split only
0660         QString a_fullname;
0661         QString a_memo;
0662         int pass = 1;
0663 
0664         QString myBeginCurrency;
0665         QString baseCurrency = file->baseCurrency().id();
0666 
0667         QMap<QString, MyMoneyMoney> xrMap; // container for conversion rates from given currency to myBeginCurrency
0668 
0669         do {
0670             MyMoneyMoney xr;
0671             ReportAccount splitAcc((* it_split).accountId());
0672             QString splitCurrency;
0673             if (splitAcc.isInvest())
0674                 splitCurrency = file->account(file->account((*it_split).accountId()).parentAccountId()).currencyId();
0675             else
0676                 splitCurrency = file->account((*it_split).accountId()).currencyId();
0677             if (it_split == myBegin)
0678                 myBeginCurrency = splitCurrency;
0679 
0680             //get fraction for account
0681             int fraction = splitAcc.currency().smallestAccountFraction();
0682 
0683             //use base currency fraction if not initialized
0684             if (fraction == -1)
0685                 fraction = file->baseCurrency().smallestAccountFraction();
0686 
0687             QString institution = splitAcc.institutionId();
0688             QString payee = (*it_split).payeeId();
0689 
0690             const QList<QString> tagIdList = (*it_split).tagIdList();
0691 
0692             //convert to base currency
0693             if (m_config.isConvertCurrency()) {
0694                 xr = xrMap.value(splitCurrency, xr);  // check if there is conversion rate to myBeginCurrency already stored...
0695                 if (xr == MyMoneyMoney())             // ...if not...
0696                     xr = (*it_split).price();         // ...take conversion rate to myBeginCurrency from split
0697                 else if (splitAcc.isInvest())         // if it's stock split...
0698                     xr *= (*it_split).price();          // ...multiply it by stock price stored in split
0699 
0700                 if (myBeginCurrency != baseCurrency) {                             // myBeginCurrency can differ from baseCurrency...
0701                     MyMoneyPrice price = file->price(myBeginCurrency, baseCurrency,
0702                                                      (*it_transaction).postDate());  // ...so check conversion rate...
0703                     if (price.isValid()) {
0704                         xr *= price.rate(baseCurrency);                                // ...and multiply it by current price...
0705                         qA[ctCurrency] = qS[ctCurrency] = baseCurrency;
0706                     } else
0707                         qA[ctCurrency] = qS[ctCurrency] = myBeginCurrency;             // ...and set information about non-baseCurrency
0708                 }
0709             } else if (splitAcc.isInvest())
0710                 xr = (*it_split).price();
0711             else {
0712                 // for the very first split we adjust the currency to the
0713                 // currency used for the split to make sure the right one
0714                 // is used in case it should differ from the transaction
0715                 // commodity. see bug #469195
0716                 if (myBegin == it_split) {
0717                     qA[ctCurrency] = qS[ctCurrency] = splitCurrency;
0718                 }
0719                 xr = MyMoneyMoney::ONE;
0720             }
0721 
0722             qA[ctTag] = (*it_split).tagIdList().join(tagSeparator);
0723 
0724             if (it_split == myBegin && splits.count() > 1) {
0725                 include_me = m_config.includes(splitAcc);
0726                 if (include_me)
0727                     // track accts that will need opening and closing balances
0728                     //FIXME in some cases it will show the opening and closing
0729                     //balances but no transactions if the splits are all filtered out -- asoliverez
0730                     accts.insert(splitAcc.id(), splitAcc);
0731 
0732                 qA[ctAccount] = splitAcc.name();
0733                 qA[ctAccountID] = splitAcc.id();
0734                 qA[ctTopAccount] = splitAcc.topParentName();
0735 
0736                 if (splitAcc.isInvest()) {
0737                     // use the institution of the parent for stock accounts
0738                     institution = splitAcc.parent().institutionId();
0739                     MyMoneyMoney shares = (*it_split).shares();
0740 
0741                     int pricePrecision = file->security(splitAcc.currencyId()).pricePrecision();
0742                     qA[ctAction] = (*it_split).action();
0743                     qA[ctShares] = shares.isZero() ? QString() : shares.toString();
0744                     qA[ctPrice] = shares.isZero() ? QString() : xr.convertPrecision(pricePrecision).toString();
0745 
0746                     if (((*it_split).action() == MyMoneySplit::actionName(eMyMoney::Split::Action::BuyShares)) && shares.isNegative())
0747                         qA[ctAction] = "Sell";
0748 
0749                     qA[ctInvestAccount] = splitAcc.parent().name();
0750 
0751                     MyMoneySplit stockSplit = (*it_split);
0752                     MyMoneySplit assetAccountSplit;
0753                     QList<MyMoneySplit> feeSplits;
0754                     QList<MyMoneySplit> interestSplits;
0755                     MyMoneySecurity currency;
0756                     MyMoneySecurity security;
0757                     eMyMoney::Split::InvestmentTransactionType transactionType;
0758                     MyMoneyUtils::dissectTransaction((*it_transaction), stockSplit, assetAccountSplit, feeSplits, interestSplits, security, currency, transactionType);
0759                     if (!(assetAccountSplit == MyMoneySplit())) {
0760                         for (it_split = splits.begin(); it_split != splits.end(); ++it_split) {
0761                             if ((*it_split) == assetAccountSplit) {
0762                                 splitAcc = ReportAccount(assetAccountSplit.accountId()); // switch over from stock split to asset split because amount in stock split doesn't take fees/interests into account
0763                                 include_me |= m_config.includes(splitAcc);
0764                                 myBegin = it_split;                       // set myBegin to asset split, so stock split can be listed in details under splits
0765                                 myBeginCurrency = (file->account((*myBegin).accountId())).currencyId();
0766                                 if (m_config.isConvertCurrency()) {
0767                                     if (myBeginCurrency != baseCurrency) {
0768                                         MyMoneyPrice price = file->price(myBeginCurrency, baseCurrency, (*it_transaction).postDate());
0769                                         if (price.isValid()) {
0770                                             xr = price.rate(baseCurrency);
0771                                             qA[ctCurrency] = qS[ctCurrency] = baseCurrency;
0772                                         } else
0773                                             qA[ctCurrency] = qS[ctCurrency] = myBeginCurrency;
0774                                     } else
0775                                         xr = MyMoneyMoney::ONE;
0776 
0777                                     qA[ctPrice] = shares.isZero() ? QString() : (stockSplit.price() * xr / (*it_split).price()).toString();
0778                                     // put conversion rate for all splits with this currency, so...
0779                                     // every split of transaction have the same conversion rate
0780                                     xrMap.insert(splitCurrency, MyMoneyMoney::ONE / (*it_split).price());
0781                                 } else
0782                                     xr = (*it_split).price();
0783                                 break;
0784                             }
0785                         }
0786                     }
0787                 } else
0788                     qA[ctPrice] = xr.toString();
0789 
0790                 a_fullname = splitAcc.fullName();
0791                 a_memo = (*it_split).memo();
0792 
0793                 transaction_text = m_config.match((*it_split));
0794 
0795                 qA[ctInstitution] = institution.isEmpty()
0796                                     ? i18n("No Institution")
0797                                     : file->institution(institution).name();
0798 
0799                 qA[ctPayee] = payee.isEmpty()
0800                               ? i18n("[Empty Payee]")
0801                               : file->payee(payee).name().simplified();
0802 
0803                 qA[ctReconcileDate] = (*it_split).reconcileDate().toString(Qt::ISODate);
0804                 qA[ctReconcileFlag] = KMyMoneyUtils::reconcileStateToString((*it_split).reconcileFlag(), true);
0805                 qA[ctNumber] = (*it_split).number();
0806 
0807                 qA[ctMemo] = a_memo;
0808 
0809                 qA[ctValue] = ((*it_split).shares() * xr).convert(fraction).toString();
0810 
0811                 qS[ctReconcileDate] = qA[ctReconcileDate];
0812                 qS[ctReconcileFlag] = qA[ctReconcileFlag];
0813                 qS[ctNumber] = qA[ctNumber];
0814 
0815                 qS[ctTopCategory] = splitAcc.topParentName();
0816                 qS[ctCategoryType] = i18n("Transfer");
0817 
0818                 // only include the configured accounts
0819                 if (include_me) {
0820 
0821                     if (loan_special_case) {
0822 
0823                         // put the principal amount in the "value" column and convert to lowest fraction
0824                         qA[ctValue] = (-(*it_split).shares() * xr).convert(fraction).toString();
0825 
0826                         qA[ctRank] = QLatin1Char('1');
0827                         qA[ctSplit].clear();
0828 
0829                     } else {
0830                         if ((splits.count() > 2) && use_summary) {
0831                             // add the "summarized" split transaction
0832                             // this is the sub-total of the split detail
0833                             // convert to lowest fraction
0834                             qA[ctRank] = QLatin1Char('1');
0835                             qA[ctCategory] = i18n("[Split Transaction]");
0836                             qA[ctTopCategory] = i18nc("Split transaction", "Split");
0837                             qA[ctCategoryType] = i18nc("Split transaction", "Split");
0838                             addRow(qA);
0839                             if (!m_containsNonBaseCurrency && qA[ctCurrency] != file->baseCurrency().id()) {
0840                                 m_containsNonBaseCurrency = true;
0841                             }
0842                         } else if (splits.count() > 2) {
0843                             // this applies when the transaction has more than 2 splits
0844                             // and each is shown separately
0845                             switch (m_config.rowType()) {
0846                             case eMyMoney::Report::RowType::Category:
0847                             case eMyMoney::Report::RowType::TopCategory:
0848                             case eMyMoney::Report::RowType::Tag:
0849                             case eMyMoney::Report::RowType::Payee:
0850                                 if (splitAcc.isAssetLiability()) {
0851                                     qA[ctValue] = ((*it_split).shares() * xr).convert(fraction).toString(); // needed for category reports, in case of multicurrency transaction it breaks it
0852                                     // make sure we use the right currency of the category
0853                                     // (will be ignored when converting to base currency)
0854                                     qA[ctCurrency] = splitAcc.currencyId();
0855                                 }
0856                                 break;
0857                             default:
0858                                 break;
0859                             }
0860                             qA[ctSplit].clear();
0861                             qA[ctRank] = QLatin1Char('1');
0862                             // keep it for now and don't add the data immediately
0863                             // as we may find a better match in one of the other splits
0864                             qStack += qA;
0865                         }
0866                     }
0867                 }
0868 
0869             } else {
0870 
0871                 if (include_me) {
0872 
0873                     if (loan_special_case) {
0874                         MyMoneyMoney value = (-(* it_split).shares() * xr).convert(fraction);
0875 
0876                         if ((*it_split).action() == MyMoneySplit::actionName(eMyMoney::Split::Action::Amortization)) {
0877                             // put the payment in the "payment" column and convert to lowest fraction
0878                             qA[ctPayee] = value.toString();
0879                         } else if ((*it_split).action() == MyMoneySplit::actionName(eMyMoney::Split::Action::Interest)) {
0880                             // put the interest in the "interest" column and convert to lowest fraction
0881                             qA[ctInterest] = value.toString();
0882                         } else if (splits.count() > 2) {
0883                             // [dv: This comment carried from the original code. I am
0884                             // not exactly clear on what it means or why we do this.]
0885                             // Put the initial pay-in nowhere (that is, ignore it). This
0886                             // is dangerous, though. The only way I can tell the initial
0887                             // pay-in apart from fees is if there are only 2 splits in
0888                             // the transaction.  I wish there was a better way.
0889                         } else {
0890                             // accumulate everything else in the "fees" column
0891                             MyMoneyMoney n0 = MyMoneyMoney(qA[ctFees]);
0892                             qA[ctFees] = (n0 + value).toString();
0893                         }
0894                         // we don't add qA here for a loan transaction. we'll add one
0895                         // qA after all of the split components have been processed.
0896                         // (see below)
0897 
0898                     }
0899 
0900                     //--- special case to hide split transaction details
0901                     else if (hide_details && (splits.count() > 2)) {
0902                         // essentially, don't add any qA entries
0903                     }
0904                     //--- default case includes all transaction details
0905                     else {
0906 
0907                         //this is when the splits are going to be shown as children of the main split
0908                         if ((splits.count() > 2) && use_summary) {
0909                             qA[ctValue].clear();
0910 
0911                             //convert to lowest fraction
0912                             qA[ctSplit] = (-(*it_split).shares() * xr).convert(fraction).toString();
0913                             qA[ctRank] = QLatin1Char('2');
0914                         } else {
0915                             //this applies when the transaction has only 2 splits, or each split is going to be
0916                             //shown separately, eg. transactions by category
0917                             switch (m_config.rowType()) {
0918                             case eMyMoney::Report::RowType::Category:
0919                             case eMyMoney::Report::RowType::TopCategory:
0920                             case eMyMoney::Report::RowType::Tag:
0921                             case eMyMoney::Report::RowType::Payee:
0922                                 if (splitAcc.isIncomeExpense()) {
0923                                     qA[ctValue] = (-(*it_split).shares() * xr).convert(fraction).toString(); // needed for category reports, in case of multicurrency transaction it breaks it
0924                                     // make sure we use the right currency of the category
0925                                     // (will be ignored when converting to base currency)
0926                                     qA[ctCurrency] = splitAcc.currencyId();
0927                                 }
0928                                 break;
0929                             default:
0930                                 break;
0931                             }
0932                             qA[ctSplit].clear();
0933                             qA[ctRank] = QLatin1Char('1');
0934                         }
0935 
0936                         qA [ctMemo] = (*it_split).memo();
0937 
0938                         if (report.isConvertCurrency())
0939                             qS[ctCurrency] = file->baseCurrency().id();
0940                         else
0941                             qS[ctCurrency] = splitAcc.currency().id();
0942 
0943                         if (! splitAcc.isIncomeExpense()) {
0944                             qA[ctCategory] = ((*it_split).shares().isNegative()) ?
0945                                              i18n("Transfer from %1", splitAcc.fullName())
0946                                              : i18n("Transfer to %1", splitAcc.fullName());
0947                             qA[ctTopCategory] = splitAcc.topParentName();
0948                             qA[ctCategoryType] = i18n("Transfer");
0949                         } else {
0950                             qA [ctCategory] = splitAcc.fullName();
0951                             qA [ctTopCategory] = splitAcc.topParentName();
0952                             qA [ctCategoryType] = MyMoneyAccount::accountTypeToString(splitAcc.accountGroup());
0953                         }
0954 
0955                         if (splits.count() > 1) {
0956                             if (use_transfers || (splitAcc.isIncomeExpense() && m_config.includes(splitAcc))) {
0957                                 //if it matches the text of the main split of the transaction or
0958                                 //it matches this particular split, include it
0959                                 //otherwise, skip it
0960                                 //if the filter is "does not contain" exclude the split if it does not match
0961                                 //even it matches the whole split
0962                                 if ((m_config.isInvertingText() &&
0963                                         m_config.match((*it_split)))
0964                                         || (!m_config.isInvertingText()
0965                                             && (transaction_text
0966                                                 || m_config.match((*it_split))))) {
0967                                     addRow(qA);
0968                                     if (!m_containsNonBaseCurrency && qA[ctCurrency] != file->baseCurrency().id()) {
0969                                         m_containsNonBaseCurrency = true;
0970                                     }
0971 
0972                                     // we don't need the stacked data
0973                                     qStack.clear();
0974                                 }
0975                             }
0976                         }
0977                     }
0978                 }
0979 
0980                 if ((m_config.includes(splitAcc) && use_transfers &&
0981                         !(splitAcc.isInvest() && include_me)) || splits.count() == 1) { // otherwise stock split is displayed twice in report
0982                     if (! splitAcc.isIncomeExpense()) {
0983                         //multiply by currency and convert to lowest fraction
0984                         qS[ctValue] = ((*it_split).shares() * xr).convert(fraction).toString();
0985 
0986                         qS[ctRank] = QLatin1Char('1');
0987 
0988                         qS[ctAccount] = splitAcc.name();
0989                         qS[ctAccountID] = splitAcc.id();
0990                         qS[ctTopAccount] = splitAcc.topParentName();
0991 
0992                         if (splits.count() > 1) {
0993                             qS[ctCategory] = ((*it_split).shares().isNegative())
0994                                              ? i18n("Transfer to %1", a_fullname)
0995                                              : i18n("Transfer from %1", a_fullname);
0996                         } else {
0997                             qS[ctCategory] = i18n("*** UNASSIGNED ***");
0998                         }
0999                         qS[ctInstitution] = institution.isEmpty()
1000                                             ? i18n("No Institution")
1001                                             : file->institution(institution).name();
1002 
1003                         qS[ctMemo] = (*it_split).memo().isEmpty()
1004                                      ? a_memo
1005                                      : (*it_split).memo();
1006 
1007                         qS[ctTag] = tagIdList.join(tagSeparator);
1008 
1009                         qS[ctPayee] = payee.isEmpty()
1010                                       ? qA[ctPayee]
1011                                       : file->payee(payee).name().simplified();
1012 
1013                         //check the specific split against the filter for text and amount
1014                         //TODO this should be done at the engine, but I have no clear idea how -- asoliverez
1015                         //if the filter is "does not contain" exclude the split if it does not match
1016                         //even it matches the whole split
1017                         if ((m_config.isInvertingText() &&
1018                                 m_config.match((*it_split)))
1019                                 || (!m_config.isInvertingText()
1020                                     && (transaction_text
1021                                         || m_config.match((*it_split))))) {
1022                             addRow(qS);
1023                             qStack.clear();
1024                             if (!m_containsNonBaseCurrency && qS[ctCurrency] != file->baseCurrency().id()) {
1025                                 m_containsNonBaseCurrency = true;
1026                             }
1027 
1028                             // track accts that will need opening and closing balances
1029                             accts.insert(splitAcc.id(), splitAcc);
1030                         }
1031                     }
1032                 }
1033             }
1034 
1035             ++it_split;
1036 
1037             // look for wrap-around
1038             if (it_split == splits.end())
1039                 it_split = splits.begin();
1040 
1041             // but terminate if this transaction has only a single split
1042             if (splits.count() < 2)
1043                 break;
1044 
1045             //check if there have been more passes than there are splits
1046             //this is to prevent infinite loops in cases of data inconsistency -- asoliverez
1047             ++pass;
1048             if (pass > splits.count())
1049                 break;
1050 
1051         } while (it_split != myBegin);
1052 
1053         if (loan_special_case) {
1054             addRow(qA);
1055             if (!m_containsNonBaseCurrency && qA[ctCurrency] != file->baseCurrency().id()) {
1056                 m_containsNonBaseCurrency = true;
1057             }
1058             qStack.clear();
1059         }
1060         // check if the stack contains a foreign currency
1061         for (const auto& row : qAsConst(qStack)) {
1062             if (!m_containsNonBaseCurrency && row[ctCurrency] != file->baseCurrency().id()) {
1063                 m_containsNonBaseCurrency = true;
1064                 break;
1065             }
1066         }
1067 
1068         // add any pending rows
1069         for (const auto& row : qAsConst(qStack)) {
1070             addRow(row);
1071         }
1072     }
1073 
1074     // now run through our accts list and add opening and closing balances
1075 
1076     switch (m_config.rowType()) {
1077     case eMyMoney::Report::RowType::Account:
1078     case eMyMoney::Report::RowType::TopAccount:
1079         break;
1080 
1081     // case eMyMoney::Report::RowType::Category:
1082     // case MyMoneyReport::eTopCategory:
1083     // case MyMoneyReport::ePayee:
1084     // case MyMoneyReport::eMonth:
1085     // case MyMoneyReport::eWeek:
1086     default:
1087         return;
1088     }
1089 
1090     QDate startDate, endDate;
1091 
1092     report.validDateRange(startDate, endDate);
1093     QString strStartDate = startDate.toString(Qt::ISODate);
1094     QString strEndDate = endDate.toString(Qt::ISODate);
1095     startDate = startDate.addDays(-1);
1096 
1097     for (auto it_account = accts.constBegin(); it_account != accts.constEnd(); ++it_account) {
1098         TableRow qA;
1099 
1100         ReportAccount account(*it_account);
1101 
1102         //get fraction for account
1103         int fraction = account.currency().smallestAccountFraction();
1104 
1105         //use base currency fraction if not initialized
1106         if (fraction == -1)
1107             fraction = file->baseCurrency().smallestAccountFraction();
1108 
1109         QString institution = account.institutionId();
1110 
1111         // use the institution of the parent for stock accounts
1112         if (account.isInvest())
1113             institution = account.parent().institutionId();
1114 
1115         MyMoneyMoney startBalance, endBalance, startPrice, endPrice;
1116         MyMoneyMoney startShares, endShares;
1117 
1118         //get price and convert currency if necessary
1119         if (m_config.isConvertCurrency()) {
1120             startPrice = (account.deepCurrencyPrice(startDate) * account.baseCurrencyPrice(startDate)).reduce();
1121             endPrice = (account.deepCurrencyPrice(endDate) * account.baseCurrencyPrice(endDate)).reduce();
1122         } else {
1123             startPrice = account.deepCurrencyPrice(startDate).reduce();
1124             endPrice = account.deepCurrencyPrice(endDate).reduce();
1125         }
1126         startShares = file->balance(account.id(), startDate);
1127         endShares = file->balance(account.id(), endDate);
1128 
1129         //get starting and ending balances
1130         startBalance = startShares * startPrice;
1131         endBalance = endShares * endPrice;
1132 
1133         //starting balance
1134         // don't show currency if we're converting or if it's not foreign
1135         if (m_config.isConvertCurrency())
1136             qA[ctCurrency] = file->baseCurrency().id();
1137         else
1138             qA[ctCurrency] = account.currency().id();
1139 
1140         qA[ctAccountID] = account.id();
1141         qA[ctAccount] = account.name();
1142         qA[ctTopAccount] = account.topParentName();
1143         qA[ctInstitution] = institution.isEmpty() ? i18n("No Institution") : file->institution(institution).name();
1144         qA[ctRank] = QLatin1Char('0');
1145 
1146         qA[ctPrice] = startPrice.convertPrecision(account.currency().pricePrecision()).toString();
1147         if (account.isInvest()) {
1148             qA[ctShares] = startShares.toString();
1149         }
1150 
1151         qA[ctPostDate] = strStartDate;
1152         qA[ctBalance] = startBalance.convert(fraction).toString();
1153         qA[ctValue].clear();
1154         qA[ctID] = QLatin1Char('A');
1155         m_rows += qA;
1156 
1157         //ending balance
1158         qA[ctPrice] = endPrice.convertPrecision(account.currency().pricePrecision()).toString();
1159 
1160         if (account.isInvest()) {
1161             qA[ctShares] = endShares.toString();
1162         }
1163 
1164         qA[ctPostDate] = strEndDate;
1165         qA[ctBalance] = endBalance.toString();
1166         qA[ctRank] = QLatin1Char('3');
1167         qA[ctID] = QLatin1Char('Z');
1168         m_rows += qA;
1169         if (!m_containsNonBaseCurrency && qA[ctCurrency] != file->baseCurrency().id()) {
1170             m_containsNonBaseCurrency = true;
1171         }
1172     }
1173 }
1174 
1175 QString QueryTable::helperROI(const MyMoneyMoney &buys, const MyMoneyMoney &sells, const MyMoneyMoney &startingBal, const MyMoneyMoney &endingBal, const MyMoneyMoney &cashIncome) const
1176 {
1177     MyMoneyMoney returnInvestment;
1178     if (!(startingBal - buys).isZero()) {
1179         returnInvestment = (sells + buys + cashIncome + endingBal - startingBal) / (startingBal - buys);
1180         return returnInvestment.convert(10000).toString();
1181     } else
1182         return QString();
1183 }
1184 
1185 QString QueryTable::helperIRR(const CashFlowList &all) const
1186 {
1187     try {
1188         return MyMoneyMoney(all.XIRR(), 10000).toString();
1189     } catch (MyMoneyException &e) {
1190         qDebug() << e.what();
1191         all.dumpDebug();
1192         return QString();
1193     }
1194 }
1195 
1196 void QueryTable::sumInvestmentValues(const ReportAccount& account, QList<CashFlowList>& cfList, QList<MyMoneyMoney>& shList) const
1197 {
1198     for (int i = InvestmentValue::Buys; i < InvestmentValue::End; ++i)
1199         cfList.append(CashFlowList());
1200     for (int i = InvestmentValue::Buys; i <= InvestmentValue::BuysOfOwned; ++i)
1201         shList.append(MyMoneyMoney());
1202 
1203     MyMoneyFile* file = MyMoneyFile::instance();
1204 
1205     MyMoneyReport report = m_config;
1206     QDate startingDate;
1207     QDate endingDate;
1208     QDate newStartingDate;
1209     QDate newEndingDate;
1210     const bool isSTLT = report.isShowingSTLTCapitalGains();
1211     const int settlementPeriod = report.settlementPeriod();
1212     QDate termSeparator = report.termSeparator().addDays(-settlementPeriod);
1213     report.validDateRange(startingDate, endingDate);
1214     newStartingDate = startingDate;
1215     newEndingDate = endingDate;
1216 
1217     if (report.queryColumns() & eMyMoney::Report::QueryColumn::CapitalGain) {
1218         // Saturday and Sunday aren't valid settlement dates
1219         if (endingDate.dayOfWeek() == Qt::Saturday)
1220             endingDate = endingDate.addDays(-1);
1221         else if (endingDate.dayOfWeek() == Qt::Sunday)
1222             endingDate = endingDate.addDays(-2);
1223 
1224         if (termSeparator.dayOfWeek() == Qt::Saturday)
1225             termSeparator = termSeparator.addDays(-1);
1226         else if (termSeparator.dayOfWeek() == Qt::Sunday)
1227             termSeparator = termSeparator.addDays(-2);
1228         if (startingDate.daysTo(endingDate) <= settlementPeriod)        // no days to check for
1229             return;
1230         termSeparator = termSeparator.addDays(-settlementPeriod);
1231         newEndingDate = endingDate.addDays(-settlementPeriod);
1232     }
1233 
1234     shList[BuysOfOwned] = file->balance(account.id(), newEndingDate); // get how many shares there are at the end of period
1235     MyMoneyMoney stashedBuysOfOwned = shList.at(BuysOfOwned);
1236 
1237     bool reportedDateRange = true;  // flag marking sell transactions between startingDate and endingDate
1238     report.setReportAllSplits(false);
1239     report.setConsiderCategory(true);
1240     report.clearAccountFilter();
1241     report.addAccount(account.id());
1242     report.setDateFilter(newStartingDate, newEndingDate);
1243 
1244     do {
1245         QList<MyMoneyTransaction> transactions;
1246         file->transactionList(transactions, report);
1247         for (QList<MyMoneyTransaction>::const_reverse_iterator  it_t = transactions.crbegin(); it_t != transactions.crend(); ++it_t) {
1248             MyMoneySplit shareSplit = (*it_t).splitByAccount(account.id());
1249             MyMoneySplit assetAccountSplit;
1250             QList<MyMoneySplit> feeSplits;
1251             QList<MyMoneySplit> interestSplits;
1252             MyMoneySecurity security;
1253             MyMoneySecurity currency;
1254             eMyMoney::Split::InvestmentTransactionType transactionType;
1255             MyMoneyUtils::dissectTransaction((*it_t), shareSplit, assetAccountSplit, feeSplits, interestSplits, security, currency, transactionType);
1256             QDate postDate = (*it_t).postDate();
1257             MyMoneyMoney price;
1258             //get price for the day of the transaction if we have to calculate base currency
1259             //we are using the value of the split which is in deep currency
1260             if (m_config.isConvertCurrency())
1261                 price = account.baseCurrencyPrice(postDate); //we only need base currency because the value is in deep currency
1262             else
1263                 price = MyMoneyMoney::ONE;
1264             MyMoneyMoney value = assetAccountSplit.value() * price;
1265             MyMoneyMoney shares = shareSplit.shares();
1266 
1267             if (transactionType == eMyMoney::Split::InvestmentTransactionType::BuyShares) {
1268                 if (reportedDateRange) {
1269                     cfList[Buys].append(CashFlowListItem(postDate, value));
1270                     shList[Buys] += shares;
1271                 }
1272 
1273                 if (shList.at(BuysOfOwned).isZero()) {                      // add sold shares
1274                     if (shList.at(BuysOfSells) + shares > shList.at(Sells).abs()) { // add partially sold shares
1275                         MyMoneyMoney tempVal = (((shList.at(Sells).abs() - shList.at(BuysOfSells))) / shares) * value;
1276                         cfList[BuysOfSells].append(CashFlowListItem(postDate, tempVal));
1277                         shList[BuysOfSells] = shList.at(Sells).abs();
1278                         if (isSTLT && postDate < termSeparator) {
1279                             cfList[LongTermBuysOfSells].append(CashFlowListItem(postDate, tempVal));
1280                             shList[LongTermBuysOfSells] = shList.at(BuysOfSells);
1281                         }
1282                     } else {                                                  // add wholly sold shares
1283                         cfList[BuysOfSells].append(CashFlowListItem(postDate, value));
1284                         shList[BuysOfSells] += shares;
1285                         if (isSTLT && postDate < termSeparator) {
1286                             cfList[LongTermBuysOfSells].append(CashFlowListItem(postDate, value));
1287                             shList[LongTermBuysOfSells] += shares;
1288                         }
1289                     }
1290                 } else if (shList.at(BuysOfOwned) >= shares) {              // subtract not-sold shares
1291                     shList[BuysOfOwned] -= shares;
1292                     cfList[BuysOfOwned].append(CashFlowListItem(postDate, value));
1293                 } else {                                                    // subtract partially not-sold shares
1294                     MyMoneyMoney tempVal = ((shares - shList.at(BuysOfOwned)) / shares) * value;
1295                     MyMoneyMoney tempVal2 = (shares - shList.at(BuysOfOwned));
1296                     cfList[BuysOfSells].append(CashFlowListItem(postDate, tempVal));
1297                     shList[BuysOfSells] += tempVal2;
1298                     if (isSTLT && postDate < termSeparator) {
1299                         cfList[LongTermBuysOfSells].append(CashFlowListItem(postDate, tempVal));
1300                         shList[LongTermBuysOfSells] += tempVal2;
1301                     }
1302                     cfList[BuysOfOwned].append(CashFlowListItem(postDate, (shList.at(BuysOfOwned) / shares) * value));
1303                     shList[BuysOfOwned] = MyMoneyMoney();
1304                 }
1305             } else if (transactionType == eMyMoney::Split::InvestmentTransactionType::SellShares && reportedDateRange) {
1306                 cfList[Sells].append(CashFlowListItem(postDate, value));
1307                 shList[Sells] += shares;
1308             } else if (transactionType == eMyMoney::Split::InvestmentTransactionType::SplitShares) {          // shares variable is denominator of split ratio here
1309                 for (int i = Buys; i <= InvestmentValue::BuysOfOwned; ++i)
1310                     shList[i] /= shares;
1311             } else if (transactionType == eMyMoney::Split::InvestmentTransactionType::AddShares ||            // added shares, when sold give 100% capital gain
1312                        transactionType == eMyMoney::Split::InvestmentTransactionType::ReinvestDividend) {
1313                 if (shList.at(BuysOfOwned).isZero()) {                            // add added/reinvested shares
1314                     if (shList.at(BuysOfSells) + shares > shList.at(Sells).abs()) { // add partially added/reinvested shares
1315                         shList[BuysOfSells] = shList.at(Sells).abs();
1316                         if (postDate < termSeparator)
1317                             shList[LongTermBuysOfSells] = shList[BuysOfSells];
1318                     } else {                                                        // add wholly added/reinvested shares
1319                         shList[BuysOfSells] += shares;
1320                         if (postDate < termSeparator)
1321                             shList[LongTermBuysOfSells] += shares;
1322                     }
1323                 } else if (shList.at(BuysOfOwned) >= shares) {                    // subtract not-added/not-reinvested shares
1324                     shList[BuysOfOwned] -= shares;
1325                     cfList[BuysOfOwned].append(CashFlowListItem(postDate, value));
1326                 } else {                                                          // subtract partially not-added/not-reinvested shares
1327                     MyMoneyMoney tempVal = (shares - shList.at(BuysOfOwned));
1328                     shList[BuysOfSells] += tempVal;
1329                     if (postDate < termSeparator)
1330                         shList[LongTermBuysOfSells] += tempVal;
1331 
1332                     cfList[BuysOfOwned].append(CashFlowListItem(postDate, (shList.at(BuysOfOwned) / shares) * value));
1333                     shList[BuysOfOwned] = MyMoneyMoney();
1334                 }
1335                 if (transactionType == eMyMoney::Split::InvestmentTransactionType::ReinvestDividend) {
1336                     value = MyMoneyMoney();
1337                     for (const auto& split : qAsConst(interestSplits))
1338                         value += split.value();
1339                     value *= price;
1340                     cfList[ReinvestIncome].append(CashFlowListItem(postDate, -value));
1341                 }
1342             } else if (transactionType == eMyMoney::Split::InvestmentTransactionType::RemoveShares && reportedDateRange) // removed shares give no value in return so no capital gain on them
1343                 shList[Sells] += shares;
1344             else if (transactionType == eMyMoney::Split::InvestmentTransactionType::Dividend || transactionType == eMyMoney::Split::InvestmentTransactionType::Yield)
1345                 cfList[CashIncome].append(CashFlowListItem(postDate, value));
1346 
1347         }
1348         reportedDateRange = false;
1349         newEndingDate = newStartingDate;
1350         newStartingDate = newStartingDate.addYears(-1);
1351         report.setDateFilter(newStartingDate, newEndingDate); // search for matching buy transactions year earlier
1352 
1353     } while (
1354         (
1355             (report.investmentSum() == eMyMoney::Report::InvestmentSum::Owned && !shList[BuysOfOwned].isZero()) ||
1356             (report.investmentSum() == eMyMoney::Report::InvestmentSum::Sold && !shList.at(Sells).isZero() && shList.at(Sells).abs() > shList.at(BuysOfSells).abs()) ||
1357             (report.investmentSum() == eMyMoney::Report::InvestmentSum::OwnedAndSold && (!shList[BuysOfOwned].isZero() || (!shList.at(Sells).isZero() && shList.at(Sells).abs() > shList.at(BuysOfSells).abs())))
1358         ) && account.openingDate() <= newEndingDate
1359     );
1360 
1361     // we've got buy value and no sell value of long-term shares, so get them
1362     if (isSTLT && !shList[LongTermBuysOfSells].isZero()) {
1363         newStartingDate = startingDate;
1364         newEndingDate = endingDate.addDays(-settlementPeriod);
1365         report.setDateFilter(newStartingDate, newEndingDate); // search for matching buy transactions year earlier
1366         QList<MyMoneyTransaction> transactions;
1367         file->transactionList(transactions, report);
1368         shList[BuysOfOwned] = shList[LongTermBuysOfSells];
1369 
1370         for (const auto& transaction : qAsConst(transactions)) {
1371             MyMoneySplit shareSplit = transaction.splitByAccount(account.id());
1372             MyMoneySplit assetAccountSplit;
1373             QList<MyMoneySplit> feeSplits;
1374             QList<MyMoneySplit> interestSplits;
1375             MyMoneySecurity security;
1376             MyMoneySecurity currency;
1377             eMyMoney::Split::InvestmentTransactionType transactionType;
1378             MyMoneyUtils::dissectTransaction(transaction, shareSplit, assetAccountSplit, feeSplits, interestSplits, security, currency, transactionType);
1379             QDate postDate = transaction.postDate();
1380             MyMoneyMoney price;
1381             if (m_config.isConvertCurrency())
1382                 price = account.baseCurrencyPrice(postDate); //we only need base currency because the value is in deep currency
1383             else
1384                 price = MyMoneyMoney::ONE;
1385             MyMoneyMoney value = assetAccountSplit.value() * price;
1386             MyMoneyMoney shares = shareSplit.shares();
1387 
1388             if (transactionType == eMyMoney::Split::InvestmentTransactionType::SellShares) {
1389                 if ((shList.at(LongTermSellsOfBuys) + shares).abs() >= shList.at(LongTermBuysOfSells)) { // add partially sold long-term shares
1390                     cfList[LongTermSellsOfBuys].append(CashFlowListItem(postDate, (shList.at(LongTermSellsOfBuys).abs() - shList.at(LongTermBuysOfSells)) / shares * value));
1391                     shList[LongTermSellsOfBuys] = shList.at(LongTermBuysOfSells);
1392                     break;
1393                 } else {                      // add wholly sold long-term shares
1394                     cfList[LongTermSellsOfBuys].append(CashFlowListItem(postDate, value));
1395                     shList[LongTermSellsOfBuys] += shares;
1396                 }
1397             } else if (transactionType == eMyMoney::Split::InvestmentTransactionType::RemoveShares) {
1398                 if ((shList.at(LongTermSellsOfBuys) + shares).abs() >= shList.at(LongTermBuysOfSells)) {
1399                     shList[LongTermSellsOfBuys] = shList.at(LongTermBuysOfSells);
1400                     break;
1401                 } else
1402                     shList[LongTermSellsOfBuys] += shares;
1403             }
1404         }
1405     }
1406 
1407     shList[BuysOfOwned] = stashedBuysOfOwned;
1408     report.setDateFilter(startingDate, endingDate); // reset data filter for next security
1409     return;
1410 }
1411 
1412 void QueryTable::constructPerformanceRow(const ReportAccount& account, TableRow& result, CashFlowList &all) const
1413 {
1414     MyMoneyReport report = m_config;
1415     QDate startingDate;
1416     QDate endingDate;
1417     report.validDateRange(startingDate, endingDate);
1418     startingDate = startingDate.addDays(-1);
1419 
1420     MyMoneyFile* file = MyMoneyFile::instance();
1421     //get fraction depending on type of account
1422     int fraction = account.currency().smallestAccountFraction();
1423     MyMoneyMoney price;
1424     if (m_config.isConvertCurrency())
1425         price = account.deepCurrencyPrice(startingDate) * account.baseCurrencyPrice(startingDate);
1426     else
1427         price = account.deepCurrencyPrice(startingDate);
1428 
1429     MyMoneyMoney startingBal = file->balance(account.id(), startingDate) * price;
1430 
1431     //convert to lowest fraction
1432     startingBal = startingBal.convert(fraction);
1433 
1434     //calculate ending balance
1435     if (m_config.isConvertCurrency())
1436         price = account.deepCurrencyPrice(endingDate) * account.baseCurrencyPrice(endingDate);
1437     else
1438         price = account.deepCurrencyPrice(endingDate);
1439 
1440     MyMoneyMoney endingBal = file->balance((account).id(), endingDate) * price;
1441 
1442     //convert to lowest fraction
1443     endingBal = endingBal.convert(fraction);
1444 
1445     QList<CashFlowList> cfList;
1446     QList<MyMoneyMoney> shList;
1447     sumInvestmentValues(account, cfList, shList);
1448 
1449     MyMoneyMoney buysTotal;
1450     MyMoneyMoney sellsTotal;
1451     MyMoneyMoney cashIncomeTotal;
1452     MyMoneyMoney reinvestIncomeTotal;
1453 
1454     switch (m_config.investmentSum()) {
1455     case eMyMoney::Report::InvestmentSum::OwnedAndSold:
1456         buysTotal = cfList.at(BuysOfSells).total() + cfList.at(BuysOfOwned).total();
1457         sellsTotal = cfList.at(Sells).total();
1458         cashIncomeTotal = cfList.at(CashIncome).total();
1459         reinvestIncomeTotal = cfList.at(ReinvestIncome).total();
1460         startingBal = MyMoneyMoney();
1461         if (buysTotal.isZero() && sellsTotal.isZero() &&
1462                 cashIncomeTotal.isZero() && reinvestIncomeTotal.isZero())
1463             return;
1464 
1465         all.append(cfList.at(BuysOfSells));
1466         all.append(cfList.at(BuysOfOwned));
1467         all.append(cfList.at(Sells));
1468         all.append(cfList.at(CashIncome));
1469 
1470         result[ctSells] = sellsTotal.toString();
1471         result[ctCashIncome] = cashIncomeTotal.toString();
1472         result[ctReinvestIncome] = reinvestIncomeTotal.toString();
1473         result[ctEndingBalance] = endingBal.toString();
1474         break;
1475     case eMyMoney::Report::InvestmentSum::Owned:
1476         buysTotal = cfList.at(BuysOfOwned).total();
1477         startingBal = MyMoneyMoney();
1478         if (buysTotal.isZero() && endingBal.isZero())
1479             return;
1480         all.append(cfList.at(BuysOfOwned));
1481         all.append(CashFlowListItem(endingDate, endingBal));
1482 
1483         result[ctReinvestIncome] = reinvestIncomeTotal.toString();
1484         result[ctMarketValue] = endingBal.toString();
1485         break;
1486     case eMyMoney::Report::InvestmentSum::Sold:
1487         buysTotal = cfList.at(BuysOfSells).total();
1488         sellsTotal = cfList.at(Sells).total();
1489         cashIncomeTotal = cfList.at(CashIncome).total();
1490         startingBal = endingBal = MyMoneyMoney();
1491         // check if there are any meaningful values before adding them to results
1492         if (buysTotal.isZero() && sellsTotal.isZero() && cashIncomeTotal.isZero())
1493             return;
1494         all.append(cfList.at(BuysOfSells));
1495         all.append(cfList.at(Sells));
1496         all.append(cfList.at(CashIncome));
1497 
1498         result[ctSells] = sellsTotal.toString();
1499         result[ctCashIncome] = cashIncomeTotal.toString();
1500         break;
1501     case eMyMoney::Report::InvestmentSum::Period:
1502     default:
1503         buysTotal = cfList.at(Buys).total();
1504         sellsTotal = cfList.at(Sells).total();
1505         cashIncomeTotal = cfList.at(CashIncome).total();
1506         reinvestIncomeTotal = cfList.at(ReinvestIncome).total();
1507         if (buysTotal.isZero() && sellsTotal.isZero() &&
1508                 cashIncomeTotal.isZero() && reinvestIncomeTotal.isZero() &&
1509                 startingBal.isZero() && endingBal.isZero())
1510             return;
1511 
1512         all.append(cfList.at(Buys));
1513         all.append(cfList.at(Sells));
1514         all.append(cfList.at(CashIncome));
1515         all.append(CashFlowListItem(startingDate, -startingBal));
1516         all.append(CashFlowListItem(endingDate, endingBal));
1517 
1518         result[ctSells] = sellsTotal.toString();
1519         result[ctCashIncome] = cashIncomeTotal.toString();
1520         result[ctReinvestIncome] = reinvestIncomeTotal.toString();
1521         result[ctStartingBalance] = startingBal.toString();
1522         result[ctEndingBalance] = endingBal.toString();
1523         break;
1524     }
1525 
1526     result[ctBuys] = buysTotal.toString();
1527     result[ctReturn] = helperIRR(all);
1528     result[ctReturnInvestment] = helperROI(buysTotal - reinvestIncomeTotal, sellsTotal, startingBal, endingBal, cashIncomeTotal);
1529     result[ctEquityType] = MyMoneySecurity::securityTypeToString(file->security(account.currencyId()).securityType());
1530 }
1531 
1532 void QueryTable::constructCapitalGainRow(const ReportAccount& account, TableRow& result) const
1533 {
1534     MyMoneyFile* file = MyMoneyFile::instance();
1535     QList<CashFlowList> cfList;
1536     QList<MyMoneyMoney> shList;
1537     sumInvestmentValues(account, cfList, shList);
1538 
1539     MyMoneyMoney buysTotal = cfList.at(BuysOfSells).total();
1540     MyMoneyMoney sellsTotal = cfList.at(Sells).total();
1541     MyMoneyMoney longTermBuysOfSellsTotal = cfList.at(LongTermBuysOfSells).total();
1542     MyMoneyMoney longTermSellsOfBuys = cfList.at(LongTermSellsOfBuys).total();
1543 
1544     switch (m_config.investmentSum()) {
1545     case eMyMoney::Report::InvestmentSum::Owned:
1546     {
1547         if (shList.at(BuysOfOwned).isZero())
1548             return;
1549 
1550         MyMoneyReport report = m_config;
1551         QDate startingDate;
1552         QDate endingDate;
1553         report.validDateRange(startingDate, endingDate);
1554 
1555         //get fraction depending on type of account
1556         int fraction = account.currency().smallestAccountFraction();
1557         MyMoneyMoney price;
1558 
1559         //calculate ending balance
1560         if (m_config.isConvertCurrency())
1561             price = account.deepCurrencyPrice(endingDate) * account.baseCurrencyPrice(endingDate);
1562         else
1563             price = account.deepCurrencyPrice(endingDate);
1564 
1565         MyMoneyMoney endingBal = shList.at(BuysOfOwned) * price;
1566 
1567         //convert to lowest fraction
1568         endingBal = endingBal.convert(fraction);
1569 
1570         buysTotal = cfList.at(BuysOfOwned).total() - cfList.at(ReinvestIncome).total();
1571 
1572         int pricePrecision = file->security(account.currencyId()).pricePrecision();
1573         result[ctBuys] = buysTotal.toString();
1574         result[ctShares] = shList.at(BuysOfOwned).toString();
1575         result[ctBuyPrice] = (buysTotal.abs() / shList.at(BuysOfOwned)).convertPrecision(pricePrecision).toString();
1576         result[ctLastPrice] = price.toString();
1577         result[ctMarketValue] = endingBal.toString();
1578         result[ctCapitalGain] = (buysTotal + endingBal).toString();
1579         result[ctPercentageGain] = buysTotal.isZero() ? QString() :
1580                                    ((buysTotal + endingBal)/buysTotal.abs()).toString();
1581         break;
1582     }
1583     case eMyMoney::Report::InvestmentSum::Sold:
1584     default:
1585         buysTotal = cfList.at(BuysOfSells).total() - cfList.at(ReinvestIncome).total();
1586         sellsTotal = cfList.at(Sells).total();
1587         longTermBuysOfSellsTotal = cfList.at(LongTermBuysOfSells).total();
1588         longTermSellsOfBuys = cfList.at(LongTermSellsOfBuys).total();
1589         // check if there are any meaningful values before adding them to results
1590         if (buysTotal.isZero() && sellsTotal.isZero() &&
1591                 longTermBuysOfSellsTotal.isZero() && longTermSellsOfBuys.isZero())
1592             return;
1593 
1594         result[ctBuys] = buysTotal.toString();
1595         result[ctSells] = sellsTotal.toString();
1596         result[ctCapitalGain] = (buysTotal + sellsTotal).toString();
1597         if (m_config.isShowingSTLTCapitalGains()) {
1598             result[ctBuysLT] = longTermBuysOfSellsTotal.toString();
1599             result[ctSellsLT] = longTermSellsOfBuys.toString();
1600             result[ctCapitalGainLT] = (longTermBuysOfSellsTotal + longTermSellsOfBuys).toString();
1601             result[ctBuysST] = (buysTotal - longTermBuysOfSellsTotal).toString();
1602             result[ctSellsST] = (sellsTotal - longTermSellsOfBuys).toString();
1603             result[ctCapitalGainST] = ((buysTotal - longTermBuysOfSellsTotal) + (sellsTotal - longTermSellsOfBuys)).toString();
1604         }
1605         break;
1606     }
1607 
1608     result[ctEquityType] = MyMoneySecurity::securityTypeToString(file->security(account.currencyId()).securityType());
1609 }
1610 
1611 void QueryTable::constructAccountTable()
1612 {
1613     MyMoneyFile* file = MyMoneyFile::instance();
1614 
1615     //make sure we have all subaccounts of investment accounts
1616     includeInvestmentSubAccounts();
1617 
1618     QMap<QString, QMap<QString, CashFlowList>> currencyCashFlow; // for total calculation
1619     QList<MyMoneyAccount> accounts;
1620     file->accountList(accounts);
1621     for (auto it_account = accounts.constBegin(); it_account != accounts.constEnd(); ++it_account) {
1622         // Note, "Investment" accounts are never included in account rows because
1623         // they don't contain anything by themselves.  In reports, they are only
1624         // useful as a "topaccount" aggregator of stock accounts
1625         if ((*it_account).isAssetLiability() && m_config.includes((*it_account)) && (*it_account).accountType() != eMyMoney::Account::Type::Investment) {
1626             // don't add the account if it is closed. In fact, the business logic
1627             // should prevent that an account can be closed with a balance not equal
1628             // to zero, but we never know.
1629             MyMoneyMoney shares = file->balance((*it_account).id(), m_config.toDate());
1630             if (shares.isZero() && (*it_account).isClosed())
1631                 continue;
1632 
1633             ReportAccount account(*it_account);
1634             TableRow qaccountrow;
1635             CashFlowList accountCashflow; // for total calculation
1636             switch(m_config.queryColumns()) {
1637             case eMyMoney::Report::QueryColumn::Performance:
1638             {
1639                 constructPerformanceRow(account, qaccountrow, accountCashflow);
1640                 if (!qaccountrow.isEmpty()) {
1641                     // assuming that that report is grouped by topaccount
1642                     qaccountrow[ctTopAccount] = account.topParentName();
1643                     if (m_config.isConvertCurrency())
1644                         qaccountrow[ctCurrency] = file->baseCurrency().id();
1645                     else
1646                         qaccountrow[ctCurrency] = account.currency().id();
1647 
1648                     if (!currencyCashFlow.value(qaccountrow.value(ctCurrency)).contains(qaccountrow.value(ctTopAccount)))
1649                         currencyCashFlow[qaccountrow.value(ctCurrency)].insert(qaccountrow.value(ctTopAccount), accountCashflow);   // create cashflow for unknown account...
1650                     else
1651                         currencyCashFlow[qaccountrow.value(ctCurrency)][qaccountrow.value(ctTopAccount)] += accountCashflow;        // ...or add cashflow for known account
1652                 }
1653                 break;
1654             }
1655             case eMyMoney::Report::QueryColumn::CapitalGain:
1656                 constructCapitalGainRow(account, qaccountrow);
1657                 break;
1658             default:
1659             {
1660                 //get fraction for account
1661                 int fraction = account.currency().smallestAccountFraction() != -1 ?
1662                                account.currency().smallestAccountFraction() : file->baseCurrency().smallestAccountFraction();
1663 
1664                 MyMoneyMoney netprice = account.deepCurrencyPrice(m_config.toDate());
1665                 if (m_config.isConvertCurrency() && account.isForeignCurrency())
1666                     netprice *= account.baseCurrencyPrice(m_config.toDate()); // display currency is base currency, so set the price
1667 
1668                 netprice = netprice.reduce();
1669                 shares = shares.reduce();
1670                 int pricePrecision = file->security(account.currencyId()).pricePrecision();
1671                 qaccountrow[ctPrice] = netprice.convertPrecision(pricePrecision).toString();
1672                 qaccountrow[ctValue] = (netprice * shares).convert(fraction).toString();
1673                 qaccountrow[ctShares] = shares.toString();
1674 
1675                 const auto iid = account.institutionId();
1676                 if (iid.isEmpty())
1677                     qaccountrow[ctInstitution] = i18nc("No institution", "None");
1678                 else
1679                     qaccountrow[ctInstitution] = file->institution(iid).name();
1680 
1681                 qaccountrow[ctType] = MyMoneyAccount::accountTypeToString(account.accountType());
1682             }
1683             }
1684 
1685             if (qaccountrow.isEmpty()) // don't add the account if there are no calculated values
1686                 continue;
1687 
1688             qaccountrow[ctRank] = QLatin1Char('1');
1689             qaccountrow[ctAccount] = account.name();
1690             qaccountrow[ctAccountID] = account.id();
1691             qaccountrow[ctTopAccount] = account.topParentName();
1692             if (m_config.isConvertCurrency())
1693                 qaccountrow[ctCurrency] = file->baseCurrency().id();
1694             else
1695                 qaccountrow[ctCurrency] = account.currency().id();
1696             m_rows.append(qaccountrow);
1697             if (!m_containsNonBaseCurrency && qaccountrow[ctCurrency] != file->baseCurrency().id()) {
1698                 m_containsNonBaseCurrency = true;
1699             }
1700         }
1701     }
1702 
1703     if (m_config.queryColumns() == eMyMoney::Report::QueryColumn::Performance && m_config.isShowingColumnTotals()) {
1704         TableRow qtotalsrow;
1705         qtotalsrow[ctRank] = QLatin1Char('4'); // add identification of row as total
1706         QMap<QString, CashFlowList> currencyGrandCashFlow;
1707 
1708         QMap<QString, QMap<QString, CashFlowList>>::iterator currencyAccGrp = currencyCashFlow.begin();
1709         while (currencyAccGrp != currencyCashFlow.end()) {
1710             // convert map of top accounts with cashflows to TableRow
1711             for (QMap<QString, CashFlowList>::iterator topAccount = (*currencyAccGrp).begin(); topAccount != (*currencyAccGrp).end(); ++topAccount) {
1712                 qtotalsrow[ctTopAccount] = topAccount.key();
1713                 qtotalsrow[ctReturn] = helperIRR(topAccount.value());
1714                 qtotalsrow[ctCurrency] = currencyAccGrp.key();
1715                 currencyGrandCashFlow[currencyAccGrp.key()] += topAccount.value();  // cumulative sum of cashflows of each topaccount
1716                 m_rows.append(qtotalsrow);            // rows aren't sorted yet, so no problem with adding them randomly at the end
1717                 if (!m_containsNonBaseCurrency && qtotalsrow[ctCurrency] != file->baseCurrency().id()) {
1718                     m_containsNonBaseCurrency = true;
1719                 }
1720             }
1721             ++currencyAccGrp;
1722         }
1723         QMap<QString, CashFlowList>::iterator currencyGrp = currencyGrandCashFlow.begin();
1724         qtotalsrow[ctTopAccount].clear();          // empty topaccount because it's grand cashflow
1725         while (currencyGrp != currencyGrandCashFlow.end()) {
1726             qtotalsrow[ctReturn] = helperIRR(currencyGrp.value());
1727             qtotalsrow[ctCurrency] = currencyGrp.key();
1728             m_rows.append(qtotalsrow);
1729             if (!m_containsNonBaseCurrency && qtotalsrow[ctCurrency] != file->baseCurrency().id()) {
1730                 m_containsNonBaseCurrency = true;
1731             }
1732             ++currencyGrp;
1733         }
1734     }
1735 }
1736 
1737 void QueryTable::constructSplitsTable()
1738 {
1739     MyMoneyFile* file = MyMoneyFile::instance();
1740 
1741     //make sure we have all subaccounts of investment accounts
1742     includeInvestmentSubAccounts();
1743 
1744     MyMoneyReport report(m_config);
1745     report.setReportAllSplits(false);
1746     report.setConsiderCategory(true);
1747 
1748     // support for opening and closing balances
1749     QMap<QString, MyMoneyAccount> accts;
1750 
1751     //get all transactions for this report
1752     QList<MyMoneyTransaction> transactions;
1753     file->transactionList(transactions, report);
1754     for (QList<MyMoneyTransaction>::const_iterator it_transaction = transactions.constBegin(); it_transaction != transactions.constEnd(); ++it_transaction) {
1755 
1756         TableRow qA, qS;
1757         QDate pd;
1758 
1759         qA[ctID] = qS[ctID] = (* it_transaction).id();
1760         qA[ctEntryDate] = qS[ctEntryDate] = (* it_transaction).entryDate().toString(Qt::ISODate);
1761         qA[ctPostDate] = qS[ctPostDate] = (* it_transaction).postDate().toString(Qt::ISODate);
1762         qA[ctCommodity] = qS[ctCommodity] = (* it_transaction).commodity();
1763 
1764         pd = (* it_transaction).postDate();
1765         qA[ctMonth] = qS[ctMonth] = i18n("Month of %1", QDate(pd.year(), pd.month(), 1).toString(Qt::ISODate));
1766         qA[ctWeek] = qS[ctWeek] = i18n("Week of %1", pd.addDays(1 - pd.dayOfWeek()).toString(Qt::ISODate));
1767 
1768         if (report.isConvertCurrency())
1769             qA[ctCurrency] = qS[ctCurrency] = file->baseCurrency().id();
1770         else
1771             qA[ctCurrency] = qS[ctCurrency] = (*it_transaction).commodity();
1772 
1773         // to handle splits, we decide on which account to base the split
1774         // (a reference point or point of view so to speak). here we take the
1775         // first account that is a stock account or loan account (or the first account
1776         // that is not an income or expense account if there is no stock or loan account)
1777         // to be the account (qA) that will have the sub-item "split" entries. we add
1778         // one transaction entry (qS) for each subsequent entry in the split.
1779         const QList<MyMoneySplit>& splits = (*it_transaction).splits();
1780         QList<MyMoneySplit>::const_iterator myBegin, it_split;
1781         //S_end = splits.end();
1782 
1783         for (it_split = splits.constBegin(), myBegin = splits.constEnd(); it_split != splits.constEnd(); ++it_split) {
1784             ReportAccount splitAcc((* it_split).accountId());
1785             // always put split with a "stock" account if it exists
1786             if (splitAcc.isInvest())
1787                 break;
1788 
1789             // prefer to put splits with a "loan" account if it exists
1790             if (splitAcc.isLoan())
1791                 myBegin = it_split;
1792 
1793             if ((myBegin == splits.end()) && ! splitAcc.isIncomeExpense()) {
1794                 myBegin = it_split;
1795             }
1796         }
1797 
1798         // select our "reference" split
1799         if (it_split == splits.end()) {
1800             it_split = myBegin;
1801         } else {
1802             myBegin = it_split;
1803         }
1804 
1805         // if the split is still unknown, use the first one. I have seen this
1806         // happen with a transaction that has only a single split referencing an income or expense
1807         // account and has an amount and value of 0. Such a transaction will fall through
1808         // the above logic and leave 'it_split' pointing to splits.end() which causes the remainder
1809         // of this to end in an infinite loop.
1810         if (it_split == splits.end()) {
1811             it_split = splits.begin();
1812         }
1813 
1814         // for "loan" reports, the loan transaction gets special treatment.
1815         // the splits of a loan transaction are placed on one line in the
1816         // reference (loan) account (qA). however, we process the matching
1817         // split entries (qS) normally.
1818         bool loan_special_case = false;
1819         if (m_config.queryColumns() & eMyMoney::Report::QueryColumn::Loan) {
1820             ReportAccount splitAcc((*it_split).accountId());
1821             loan_special_case = splitAcc.isLoan();
1822         }
1823 
1824         // There is a slight chance that at this point myBegin is still pointing to splits.end() if the
1825         // transaction only has income and expense splits (which should not happen). In that case, point
1826         // it to the first split
1827         if (myBegin == splits.end()) {
1828             myBegin = splits.begin();
1829         }
1830 
1831         //the account of the beginning splits
1832         ReportAccount myBeginAcc((*myBegin).accountId());
1833 
1834         QString a_fullname;
1835         QString a_memo;
1836         int pass = 1;
1837 
1838         do {
1839             MyMoneyMoney xr;
1840             ReportAccount splitAcc((* it_split).accountId());
1841 
1842             //get fraction for account
1843             int fraction = splitAcc.currency().smallestAccountFraction();
1844 
1845             //use base currency fraction if not initialized
1846             if (fraction == -1)
1847                 fraction = file->baseCurrency().smallestAccountFraction();
1848 
1849             QString institution = splitAcc.institutionId();
1850             QString payee = (*it_split).payeeId();
1851 
1852             const QList<QString> tagIdList = (*it_split).tagIdList();
1853 
1854             if (m_config.isConvertCurrency()) {
1855                 xr = (splitAcc.deepCurrencyPrice((*it_transaction).postDate()) * splitAcc.baseCurrencyPrice((*it_transaction).postDate())).reduce();
1856             } else {
1857                 xr = splitAcc.deepCurrencyPrice((*it_transaction).postDate()).reduce();
1858             }
1859 
1860             // reverse the sign of incomes and expenses to keep consistency in the way it is displayed in other reports
1861             if (splitAcc.isIncomeExpense()) {
1862                 xr = -xr;
1863             }
1864 
1865             if (splitAcc.isInvest()) {
1866 
1867                 // use the institution of the parent for stock accounts
1868                 institution = splitAcc.parent().institutionId();
1869                 MyMoneyMoney shares = (*it_split).shares();
1870                 int pricePrecision = file->security(splitAcc.currencyId()).pricePrecision();
1871                 qA[ctAction] = (*it_split).action();
1872                 qA[ctShares] = shares.isZero() ? QString() : (*it_split).shares().toString();
1873                 qA[ctPrice] = shares.isZero() ? QString() : xr.convertPrecision(pricePrecision).toString();
1874 
1875                 if (((*it_split).action() == MyMoneySplit::actionName(eMyMoney::Split::Action::BuyShares)) && (*it_split).shares().isNegative())
1876                     qA[ctAction] = "Sell";
1877 
1878                 qA[ctInvestAccount] = splitAcc.parent().name();
1879             }
1880 
1881             const auto include_me = m_config.includes(splitAcc);
1882             a_fullname = splitAcc.fullName();
1883             a_memo = (*it_split).memo();
1884 
1885             int pricePrecision = file->security(splitAcc.currencyId()).pricePrecision();
1886             qA[ctPrice] = xr.convertPrecision(pricePrecision).toString();
1887             qA[ctAccount] = splitAcc.name();
1888             qA[ctAccountID] = splitAcc.id();
1889             qA[ctTopAccount] = splitAcc.topParentName();
1890 
1891             qA[ctInstitution] = institution.isEmpty()
1892                                 ? i18n("No Institution")
1893                                 : file->institution(institution).name();
1894 
1895             //FIXME-ALEX Is this useless? Isn't constructSplitsTable called only for cashflow type report?
1896             QString delimiter;
1897             for (const auto& tagId : qAsConst(tagIdList)) {
1898                 qA[ctTag] += delimiter + file->tag(tagId).name().simplified();
1899                 delimiter = QLatin1Char(',');
1900             }
1901 
1902             qA[ctPayee] = payee.isEmpty()
1903                           ? i18n("[Empty Payee]")
1904                           : file->payee(payee).name().simplified();
1905 
1906             qA[ctReconcileDate] = (*it_split).reconcileDate().toString(Qt::ISODate);
1907             qA[ctReconcileFlag] = KMyMoneyUtils::reconcileStateToString((*it_split).reconcileFlag(), true);
1908             qA[ctNumber] = (*it_split).number();
1909 
1910             qA[ctMemo] = a_memo;
1911 
1912             qS[ctReconcileDate] = qA[ctReconcileDate];
1913             qS[ctReconcileFlag] = qA[ctReconcileFlag];
1914             qS[ctNumber] = qA[ctNumber];
1915 
1916             qS[ctTopCategory] = splitAcc.topParentName();
1917 
1918             // only include the configured accounts
1919             if (include_me) {
1920                 // add the "summarized" split transaction
1921                 // this is the sub-total of the split detail
1922                 // convert to lowest fraction
1923                 qA[ctValue] = ((*it_split).shares() * xr).convert(fraction).toString();
1924                 qA[ctRank] = QLatin1Char('1');
1925 
1926                 //fill in account information
1927                 if (! splitAcc.isIncomeExpense() && it_split != myBegin) {
1928                     qA[ctAccount] = ((*it_split).shares().isNegative()) ?
1929                                     i18n("Transfer to %1", myBeginAcc.fullName())
1930                                     : i18n("Transfer from %1", myBeginAcc.fullName());
1931                 } else if (it_split == myBegin) {
1932                     //handle the main split
1933                     if ((splits.count() > 2)) {
1934                         //if it is the main split and has multiple splits, note that
1935                         qA[ctAccount] = i18n("[Split Transaction]");
1936                     } else {
1937                         //fill the account name of the second split
1938                         QList<MyMoneySplit>::const_iterator tempSplit = splits.constBegin();
1939 
1940                         //there are supposed to be only 2 splits if we ever get here
1941                         if (tempSplit == myBegin && splits.count() > 1)
1942                             ++tempSplit;
1943 
1944                         //show the name of the category, or "transfer to/from" if it as an account
1945                         ReportAccount tempSplitAcc((*tempSplit).accountId());
1946                         if (! tempSplitAcc.isIncomeExpense()) {
1947                             qA[ctAccount] = ((*it_split).shares().isNegative()) ?
1948                                             i18n("Transfer to %1", tempSplitAcc.fullName())
1949                                             : i18n("Transfer from %1", tempSplitAcc.fullName());
1950                         } else {
1951                             qA[ctAccount] = tempSplitAcc.fullName();
1952                         }
1953                     }
1954                 } else {
1955                     //in any other case, fill in the account name of the main split
1956                     qA[ctAccount] = myBeginAcc.fullName();
1957                 }
1958 
1959                 //category data is always the one of the split
1960                 qA [ctCategory] = splitAcc.fullName();
1961                 qA [ctTopCategory] = splitAcc.topParentName();
1962                 qA [ctCategoryType] = MyMoneyAccount::accountTypeToString(splitAcc.accountGroup());
1963 
1964                 m_rows += qA;
1965                 if (!m_containsNonBaseCurrency && qA[ctCurrency] != file->baseCurrency().id()) {
1966                     m_containsNonBaseCurrency = true;
1967                 }
1968 
1969                 // track accts that will need opening and closing balances
1970                 accts.insert(splitAcc.id(), splitAcc);
1971             }
1972             ++it_split;
1973 
1974             // look for wrap-around
1975             if (it_split == splits.end())
1976                 it_split = splits.begin();
1977 
1978             //check if there have been more passes than there are splits
1979             //this is to prevent infinite loops in cases of data inconsistency -- asoliverez
1980             ++pass;
1981             if (pass > splits.count())
1982                 break;
1983 
1984         } while (it_split != myBegin);
1985 
1986         if (loan_special_case) {
1987             m_rows += qA;
1988             if (!m_containsNonBaseCurrency && qA[ctCurrency] != file->baseCurrency().id()) {
1989                 m_containsNonBaseCurrency = true;
1990             }
1991         }
1992     }
1993 
1994     // now run through our accts list and add opening and closing balances
1995 
1996     switch (m_config.rowType()) {
1997     case eMyMoney::Report::RowType::Account:
1998     case eMyMoney::Report::RowType::TopAccount:
1999         break;
2000 
2001     // case eMyMoney::Report::RowType::Category:
2002     // case MyMoneyReport::eTopCategory:
2003     // case MyMoneyReport::ePayee:
2004     // case MyMoneyReport::eMonth:
2005     // case MyMoneyReport::eWeek:
2006     default:
2007         return;
2008     }
2009 
2010     QDate startDate, endDate;
2011 
2012     report.validDateRange(startDate, endDate);
2013     QString strStartDate = startDate.toString(Qt::ISODate);
2014     QString strEndDate = endDate.toString(Qt::ISODate);
2015     startDate = startDate.addDays(-1);
2016 
2017     for (auto it_account = accts.constBegin(); it_account != accts.constEnd(); ++it_account) {
2018         TableRow qA;
2019 
2020         ReportAccount account((* it_account));
2021 
2022         //get fraction for account
2023         int fraction = account.currency().smallestAccountFraction();
2024 
2025         //use base currency fraction if not initialized
2026         if (fraction == -1)
2027             fraction = file->baseCurrency().smallestAccountFraction();
2028 
2029         QString institution = account.institutionId();
2030 
2031         // use the institution of the parent for stock accounts
2032         if (account.isInvest())
2033             institution = account.parent().institutionId();
2034 
2035         MyMoneyMoney startBalance, endBalance, startPrice, endPrice;
2036         MyMoneyMoney startShares, endShares;
2037 
2038         //get price and convert currency if necessary
2039         if (m_config.isConvertCurrency()) {
2040             startPrice = (account.deepCurrencyPrice(startDate) * account.baseCurrencyPrice(startDate)).reduce();
2041             endPrice = (account.deepCurrencyPrice(endDate) * account.baseCurrencyPrice(endDate)).reduce();
2042         } else {
2043             startPrice = account.deepCurrencyPrice(startDate).reduce();
2044             endPrice = account.deepCurrencyPrice(endDate).reduce();
2045         }
2046         startShares = file->balance(account.id(), startDate);
2047         endShares = file->balance(account.id(), endDate);
2048 
2049         //get starting and ending balances
2050         startBalance = startShares * startPrice;
2051         endBalance = endShares * endPrice;
2052 
2053         //starting balance
2054         // don't show currency if we're converting or if it's not foreign
2055         if (m_config.isConvertCurrency())
2056             qA[ctCurrency] = file->baseCurrency().id();
2057         else
2058             qA[ctCurrency] = account.currency().id();
2059 
2060         qA[ctAccountID] = account.id();
2061         qA[ctAccount] = account.name();
2062         qA[ctTopAccount] = account.topParentName();
2063         qA[ctInstitution] = institution.isEmpty() ? i18n("No Institution") : file->institution(institution).name();
2064         qA[ctRank] = QLatin1Char('0');
2065 
2066         int pricePrecision = file->security(account.currencyId()).pricePrecision();
2067         qA[ctPrice] = startPrice.convertPrecision(pricePrecision).toString();
2068         if (account.isInvest()) {
2069             qA[ctShares] = startShares.toString();
2070         }
2071 
2072         qA[ctPostDate] = strStartDate;
2073         qA[ctBalance] = startBalance.convert(fraction).toString();
2074         qA[ctValue].clear();
2075         qA[ctID] = QLatin1Char('A');
2076         m_rows += qA;
2077 
2078         qA[ctRank] = QLatin1Char('3');
2079         //ending balance
2080         qA[ctPrice] = endPrice.convertPrecision(pricePrecision).toString();
2081 
2082         if (account.isInvest()) {
2083             qA[ctShares] = endShares.toString();
2084         }
2085 
2086         qA[ctPostDate] = strEndDate;
2087         qA[ctBalance] = endBalance.toString();
2088         qA[ctID] = QLatin1Char('Z');
2089         m_rows += qA;
2090         if (!m_containsNonBaseCurrency && qA[ctCurrency] != file->baseCurrency().id()) {
2091             m_containsNonBaseCurrency = true;
2092         }
2093     }
2094 }
2095 
2096 
2097 
2098 }