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

0001 /*
0002     SPDX-FileCopyrightText: 2005-2006 Ace Jones <acejones@users.sourceforge.net>
0003     SPDX-FileCopyrightText: 2005-2018 Thomas Baumgart <tbaumgart@kde.org>
0004     SPDX-FileCopyrightText: 2007-2014 Alvaro Soliverez <asoliverez@gmail.com>
0005     SPDX-License-Identifier: GPL-2.0-or-later
0006 */
0007 
0008 #include "pivottable.h"
0009 
0010 // ----------------------------------------------------------------------------
0011 // QT Includes
0012 #include <QFile>
0013 #include <QList>
0014 #include <QRegularExpression>
0015 #include <QTextStream>
0016 
0017 // ----------------------------------------------------------------------------
0018 // KDE Includes
0019 
0020 #include <KLocalizedString>
0021 
0022 // ----------------------------------------------------------------------------
0023 // Project Includes
0024 #include "journalmodel.h"
0025 #include "kmymoneysettings.h"
0026 #include "kmymoneyutils.h"
0027 #include "kreportchartview.h"
0028 #include "mymoneybudget.h"
0029 #include "mymoneyenums.h"
0030 #include "mymoneyexception.h"
0031 #include "mymoneyfile.h"
0032 #include "mymoneyforecast.h"
0033 #include "mymoneyprice.h"
0034 #include "mymoneyreport.h"
0035 #include "mymoneyschedule.h"
0036 #include "mymoneysecurity.h"
0037 #include "mymoneysplit.h"
0038 #include "mymoneytransaction.h"
0039 #include "pivotgrid.h"
0040 #include "reportdebug.h"
0041 
0042 namespace KChart {
0043 class Widget;
0044 }
0045 
0046 namespace reports
0047 {
0048 
0049 using KChart::Widget;
0050 
0051 QString Debug::m_sTabs;
0052 bool Debug::m_sEnabled = DEBUG_ENABLED_BY_DEFAULT;
0053 QString Debug::m_sEnableKey;
0054 
0055 Debug::Debug(const QString& _name): m_methodName(_name), m_enabled(m_sEnabled)
0056 {
0057     if (!m_enabled && _name == m_sEnableKey)
0058         m_enabled = true;
0059 
0060     if (m_enabled) {
0061         qDebug("%s%s(): ENTER", qPrintable(m_sTabs), qPrintable(m_methodName));
0062         m_sTabs.append("--");
0063     }
0064 }
0065 
0066 Debug::~Debug()
0067 {
0068     if (m_enabled) {
0069         m_sTabs.remove(0, 2);
0070         qDebug("%s%s(): EXIT", qPrintable(m_sTabs), qPrintable(m_methodName));
0071 
0072         if (m_methodName == m_sEnableKey)
0073             m_enabled = false;
0074     }
0075 }
0076 
0077 void Debug::output(const QString& _text)
0078 {
0079     if (m_enabled)
0080         qDebug("%s%s(): %s", qPrintable(m_sTabs), qPrintable(m_methodName), qPrintable(_text));
0081 }
0082 
0083 PivotTable::PivotTable(const MyMoneyReport& _report):
0084     ReportTable(_report),
0085     m_runningSumsCalculated(false)
0086 {
0087     init();
0088 }
0089 
0090 void PivotTable::init()
0091 {
0092     DEBUG_ENTER(Q_FUNC_INFO);
0093 
0094     //
0095     // Initialize locals
0096     //
0097 
0098     MyMoneyFile* file = MyMoneyFile::instance();
0099 
0100     //
0101     // Initialize member variables
0102     //
0103 
0104     //make sure we have all subaccounts of investment accounts
0105     includeInvestmentSubAccounts();
0106 
0107     m_config.validDateRange(m_beginDate, m_endDate);
0108 
0109     // If we need to calculate running sums, it does not make sense
0110     // to show a row total column
0111     if (m_config.isRunningSum())
0112         m_config.setShowingRowTotals(false);
0113 
0114     if (m_config.isRunningSum() &&
0115             !m_config.isIncludingPrice() &&
0116             !m_config.isIncludingAveragePrice() &&
0117             !m_config.isIncludingMovingAverage())
0118         m_startColumn = 1;
0119     else
0120         m_startColumn = 0;
0121 
0122     m_numColumns = columnValue(m_endDate) - columnValue(m_beginDate) + 1 + m_startColumn; // 1 for m_beginDate values and m_startColumn for opening balance values
0123 
0124     //Load what types of row the report is going to show
0125     loadRowTypeList();
0126 
0127     //
0128     // Initialize outer groups of the grid
0129     //
0130     if (m_config.rowType() == eMyMoney::Report::RowType::AssetLiability) {
0131         m_grid.insert(MyMoneyAccount::accountTypeToString(eMyMoney::Account::Type::Asset), PivotOuterGroup(m_numColumns));
0132         m_grid.insert(MyMoneyAccount::accountTypeToString(eMyMoney::Account::Type::Liability), PivotOuterGroup(m_numColumns, PivotOuterGroup::m_kDefaultSortOrder, true /* inverted */));
0133     } else {
0134         m_grid.insert(MyMoneyAccount::accountTypeToString(eMyMoney::Account::Type::Income), PivotOuterGroup(m_numColumns, PivotOuterGroup::m_kDefaultSortOrder - 2));
0135         m_grid.insert(MyMoneyAccount::accountTypeToString(eMyMoney::Account::Type::Expense), PivotOuterGroup(m_numColumns, PivotOuterGroup::m_kDefaultSortOrder - 1, true /* inverted */));
0136         //
0137         // Create rows for income/expense reports with all accounts included
0138         //
0139         if (m_config.isIncludingUnusedAccounts())
0140             createAccountRows();
0141     }
0142 
0143     //
0144     // Initialize grid totals
0145     //
0146 
0147     m_grid.m_total = PivotGridRowSet(m_numColumns);
0148 
0149     //
0150     // Get opening balances
0151     // Only net worth report qualifies
0152     if (m_startColumn == 1)
0153         calculateOpeningBalances();
0154 
0155     //
0156     // Calculate budget mapping
0157     // (for budget reports only)
0158     //
0159     if (m_config.hasBudget())
0160         calculateBudgetMapping();
0161 
0162     // prices report doesn't need transactions, but it needs account stub
0163     // otherwise fillBasePriceUnit won't do nothing
0164     if (m_config.isIncludingPrice() ||
0165             m_config.isIncludingAveragePrice()) {
0166         QList<MyMoneyAccount> accounts;
0167         file->accountList(accounts);
0168         for (const auto& acc : qAsConst(accounts)) {
0169             if (acc.isInvest()) {
0170                 const ReportAccount repAcc(acc);
0171                 if (m_config.includes(repAcc)) {
0172                     const auto outergroup = acc.accountTypeToString(acc.accountType());
0173                     assignCell(outergroup, repAcc, 0, MyMoneyMoney(), false, false);    // add account stub
0174                 }
0175             }
0176         }
0177     } else {
0178         //
0179         // Populate all transactions into the row/column pivot grid
0180         //
0181 
0182         QList<MyMoneyTransaction> transactions;
0183         m_config.setReportAllSplits(false);
0184         m_config.setConsiderCategory(true);
0185         try {
0186             file->transactionList(transactions, m_config);
0187         } catch (const MyMoneyException &e) {
0188             qDebug("ERR: %s", e.what());
0189             throw;
0190         }
0191         DEBUG_OUTPUT(QString("Found %1 matching transactions").arg(transactions.count()));
0192 
0193 
0194         // Include scheduled transactions if required
0195         if (m_config.isIncludingSchedules()) {
0196             // Create a custom version of the report filter, excluding date
0197             // We'll use this to compare the transaction against
0198             MyMoneyTransactionFilter schedulefilter(m_config);
0199             schedulefilter.setDateFilter(QDate(), QDate());
0200 
0201             // Get the real dates from the config filter
0202             QDate configbegin, configend;
0203             m_config.validDateRange(configbegin, configend);
0204 
0205             QList<MyMoneySchedule> schedules = file->scheduleList();
0206             QList<MyMoneySchedule>::const_iterator it_schedule = schedules.constBegin();
0207             while (it_schedule != schedules.constEnd()) {
0208                 // If the transaction meets the filter
0209                 MyMoneyTransaction tx = (*it_schedule).transaction();
0210                 if (!(*it_schedule).isFinished() && schedulefilter.match(tx)) {
0211                     // Keep the id of the schedule with the transaction so that
0212                     // we can do the autocalc later on in case of a loan payment
0213                     tx.setValue("kmm-schedule-id", (*it_schedule).id());
0214 
0215                     // Get the dates when a payment will be made within the report window
0216                     QDate nextpayment = (*it_schedule).adjustedNextPayment(configbegin);
0217                     if (nextpayment.isValid()) {
0218                         // Add one transaction for each date
0219                         QList<QDate> paymentDates = (*it_schedule).paymentDates(nextpayment, configend);
0220                         QList<QDate>::const_iterator it_date = paymentDates.constBegin();
0221                         while (it_date != paymentDates.constEnd()) {
0222                             //if the payment occurs in the past, enter it tomorrow
0223                             if (QDate::currentDate() >= *it_date) {
0224                                 tx.setPostDate(QDate::currentDate().addDays(1));
0225                             } else {
0226                                 tx.setPostDate(*it_date);
0227                             }
0228                             if (tx.postDate() <= configend
0229                                     && tx.postDate() >= configbegin) {
0230                                 transactions += tx;
0231                             }
0232 
0233                             DEBUG_OUTPUT(QString("Added transaction for schedule %1 on %2").arg((*it_schedule).id()).arg((*it_date).toString()));
0234 
0235                             ++it_date;
0236                         }
0237                     }
0238                 }
0239 
0240                 ++it_schedule;
0241             }
0242         }
0243 
0244         // whether asset & liability transactions are actually to be considered
0245         // transfers
0246         bool al_transfers = (m_config.rowType() == eMyMoney::Report::RowType::ExpenseIncome) && (m_config.isIncludingTransfers());
0247 
0248         //this is to store balance for loan accounts when not included in the report
0249         QMap<QString, MyMoneyMoney> loanBalances;
0250 
0251         QList<MyMoneyTransaction>::const_iterator it_transaction = transactions.constBegin();
0252         int colofs = columnValue(m_beginDate) - m_startColumn;
0253         while (it_transaction != transactions.constEnd()) {
0254             MyMoneyTransaction tx(*it_transaction);
0255             if (m_openingBalanceTransactions.contains(tx.id())) {
0256                 ++it_transaction;
0257                 continue;
0258             }
0259             QDate postdate = tx.postDate();
0260             if (postdate < m_beginDate) {
0261                 qDebug("MyMoneyFile::transactionList returned a transaction that is outside the date filter, skipping it");
0262                 ++it_transaction;
0263                 continue;
0264             }
0265             int column = columnValue(postdate) - colofs;
0266 
0267             // check if we need to call the autocalculation routine
0268             if (tx.isLoanPayment() && tx.hasAutoCalcSplit() && (tx.value("kmm-schedule-id").length() > 0)) {
0269                 // make sure to consider any autocalculation for loan payments
0270                 MyMoneySchedule sched = file->schedule(tx.value("kmm-schedule-id"));
0271                 const MyMoneySplit& split = tx.amortizationSplit();
0272                 if (!split.id().isEmpty()) {
0273                     ReportAccount splitAccount(file->account(split.accountId()));
0274                     eMyMoney::Account::Type type = splitAccount.accountGroup();
0275                     QString outergroup = MyMoneyAccount::accountTypeToString(type);
0276 
0277                     //if the account is included in the report, calculate the balance from the cells
0278                     if (m_config.includes(splitAccount)) {
0279                         loanBalances[splitAccount.id()] = cellBalance(outergroup, splitAccount, column, false);
0280                     } else {
0281                         //if it is not in the report and also not in loanBalances, get the balance from the file
0282                         if (!loanBalances.contains(splitAccount.id())) {
0283                             QDate dueDate = sched.nextDueDate();
0284 
0285                             //if the payment is overdue, use current date
0286                             if (dueDate < QDate::currentDate())
0287                                 dueDate = QDate::currentDate();
0288 
0289                             //get the balance from the file for the date
0290                             loanBalances[splitAccount.id()] = file->balance(splitAccount.id(), dueDate.addDays(-1));
0291                         }
0292                     }
0293 
0294                     KMyMoneyUtils::calculateAutoLoan(sched, tx, loanBalances);
0295 
0296                     //if the loan split is not included in the report, update the balance for the next occurrence
0297                     if (!m_config.includes(splitAccount)) {
0298                         for (const auto& txsplit : qAsConst(tx.splits())) {
0299                             if (txsplit.isAmortizationSplit() && txsplit.accountId() == splitAccount.id())
0300                                 loanBalances[splitAccount.id()] = loanBalances[splitAccount.id()] + txsplit.shares();
0301                         }
0302                     }
0303                 }
0304             }
0305 
0306             QList<MyMoneySplit> splits = tx.splits();
0307             QList<MyMoneySplit>::const_iterator it_split = splits.constBegin();
0308             while (it_split != splits.constEnd()) {
0309                 ReportAccount splitAccount((*it_split).accountId());
0310 
0311                 // Each split must be further filtered, because if even one split matches,
0312                 // the ENTIRE transaction is returned with all splits (even non-matching ones)
0313                 if (m_config.includes(splitAccount) && m_config.match((*it_split))) {
0314                     // reverse sign to match common notation for cash flow direction, only for expense/income splits
0315                     MyMoneyMoney reverse(splitAccount.isIncomeExpense() ? -1 : 1, 1);
0316 
0317                     MyMoneyMoney value;
0318                     // the outer group is the account class (major account type)
0319                     eMyMoney::Account::Type type = splitAccount.accountGroup();
0320                     QString outergroup = MyMoneyAccount::accountTypeToString(type);
0321 
0322                     value = (*it_split).shares();
0323                     bool stockSplit = tx.isStockSplit();
0324                     if (!stockSplit) {
0325                         // retrieve the value in the account's underlying currency
0326                         if (value != MyMoneyMoney::autoCalc) {
0327                             value = value * reverse;
0328                         } else {
0329                             qDebug("PivotTable::PivotTable(): This must not happen");
0330                             value = MyMoneyMoney();  // keep it 0 so far
0331                         }
0332 
0333                         // Except in the case of transfers on an income/expense report
0334                         if (al_transfers && (type == eMyMoney::Account::Type::Asset || type == eMyMoney::Account::Type::Liability)) {
0335                             outergroup = i18n("Transfers");
0336                             value = -value;
0337                         }
0338                     }
0339                     // add the value to its correct position in the pivot table
0340                     assignCell(outergroup, splitAccount, column, value, false, stockSplit);
0341                 }
0342                 ++it_split;
0343             }
0344 
0345             ++it_transaction;
0346         }
0347     }
0348 
0349     //
0350     // Get forecast data
0351     //
0352     if (m_config.isIncludingForecast())
0353         calculateForecast();
0354 
0355     //
0356     //Insert Price data
0357     //
0358     if (m_config.isIncludingPrice())
0359         fillBasePriceUnit(ePrice);
0360 
0361     //
0362     //Insert Average Price data
0363     //
0364     if (m_config.isIncludingAveragePrice()) {
0365         fillBasePriceUnit(eActual);
0366         calculateMovingAverage();
0367     }
0368 
0369     //
0370     // Collapse columns to match column type
0371     //
0372 
0373 
0374     if (m_config.columnPitch() > 1)
0375         collapseColumns();
0376 
0377     //
0378     // Calculate the running sums
0379     // (for running sum reports only)
0380     //
0381     if (m_config.isRunningSum() && (m_config.chartType() != eMyMoney::Report::ChartType::Pie))
0382         calculateRunningSums();
0383 
0384     //
0385     // Calculate Moving Average
0386     //
0387     if (m_config.isIncludingMovingAverage())
0388         calculateMovingAverage();
0389 
0390     //
0391     // Calculate Budget Difference
0392     //
0393 
0394     if (m_config.isIncludingBudgetActuals())
0395         calculateBudgetDiff();
0396 
0397     //
0398     // Convert all values to the deep currency
0399     //
0400 
0401     convertToDeepCurrency();
0402 
0403     //
0404     // Convert all values to the base currency
0405     //
0406 
0407     if (m_config.isConvertCurrency())
0408         convertToBaseCurrency();
0409 
0410     //
0411     // Determine column headings
0412     //
0413 
0414     calculateColumnHeadings();
0415 
0416     //
0417     // Calculate row and column totals
0418     //
0419 
0420     calculateTotals();
0421 
0422     //
0423     // If using mixed time, calculate column for current date
0424     //
0425     m_config.setCurrentDateColumn(currentDateColumn());
0426 }
0427 
0428 void PivotTable::collapseColumns()
0429 {
0430     DEBUG_ENTER(Q_FUNC_INFO);
0431 
0432     // check if the report is bound to the fiscal year
0433     // and calculate the appropriate offset for the column break
0434     int fiscalYearOffset(0);
0435     if (startDateIsFiscalYearStart()) {
0436         fiscalYearOffset = KMyMoneySettings::firstFiscalMonth() - 1;
0437     }
0438 
0439     int columnpitch = m_config.columnPitch();
0440     if (columnpitch != 1) {
0441         int sourcemonth = (m_config.isColumnsAreDays())
0442                           // use the user's locale to determine the week's start
0443                           ? (m_beginDate.dayOfWeek() + 8 - QLocale().firstDayOfWeek()) % 7
0444                           : m_beginDate.month();
0445         int sourcecolumn = m_startColumn;
0446         int destcolumn = m_startColumn;
0447         while (sourcecolumn < m_numColumns) {
0448             if (sourcecolumn != destcolumn) {
0449 #if 0
0450                 // TODO: Clean up this rather inefficient kludge. We really should jump by an entire
0451                 // destcolumn at a time on RS reports, and calculate the proper sourcecolumn to use,
0452                 // allowing us to clear and accumulate only ONCE per destcolumn
0453                 if (m_config_f.isRunningSum())
0454                     clearColumn(destcolumn);
0455 #endif
0456                 accumulateColumn(destcolumn, sourcecolumn);
0457             }
0458 
0459             if (++sourcecolumn < m_numColumns) {
0460                 if (((sourcemonth++ - fiscalYearOffset) % columnpitch) == 0) {
0461                     if (sourcecolumn != ++destcolumn)
0462                         clearColumn(destcolumn);
0463                 }
0464             }
0465         }
0466         m_numColumns = destcolumn + 1;
0467     }
0468 }
0469 
0470 void PivotTable::accumulateColumn(int destcolumn, int sourcecolumn)
0471 {
0472     DEBUG_ENTER(Q_FUNC_INFO);
0473     DEBUG_OUTPUT(QString("From Column %1 to %2").arg(sourcecolumn).arg(destcolumn));
0474 
0475     // iterate over outer groups
0476     PivotGrid::iterator it_outergroup = m_grid.begin();
0477     while (it_outergroup != m_grid.end()) {
0478         // iterate over inner groups
0479         PivotOuterGroup::iterator it_innergroup = (*it_outergroup).begin();
0480         while (it_innergroup != (*it_outergroup).end()) {
0481             // iterator over rows
0482             PivotInnerGroup::iterator it_row = (*it_innergroup).begin();
0483             while (it_row != (*it_innergroup).end()) {
0484                 if ((*it_row)[eActual].count() <= sourcecolumn)
0485                     throw MYMONEYEXCEPTION(QString::fromLatin1("Sourcecolumn %1 out of grid range (%2) in PivotTable::accumulateColumn").arg(sourcecolumn).arg((*it_row)[eActual].count()));
0486                 if ((*it_row)[eActual].count() <= destcolumn)
0487                     throw MYMONEYEXCEPTION(QString::fromLatin1("Destcolumn %1 out of grid range (%2) in PivotTable::accumulateColumn").arg(sourcecolumn).arg((*it_row)[eActual].count()));
0488 
0489                 (*it_row)[eActual][destcolumn] += (*it_row)[eActual][sourcecolumn];
0490                 ++it_row;
0491             }
0492 
0493             ++it_innergroup;
0494         }
0495         ++it_outergroup;
0496     }
0497 }
0498 
0499 void PivotTable::clearColumn(int column)
0500 {
0501     DEBUG_ENTER(Q_FUNC_INFO);
0502     DEBUG_OUTPUT(QString("Column %1").arg(column));
0503 
0504     // iterate over outer groups
0505     PivotGrid::iterator it_outergroup = m_grid.begin();
0506     while (it_outergroup != m_grid.end()) {
0507         // iterate over inner groups
0508         PivotOuterGroup::iterator it_innergroup = (*it_outergroup).begin();
0509         while (it_innergroup != (*it_outergroup).end()) {
0510             // iterator over rows
0511             PivotInnerGroup::iterator it_row = (*it_innergroup).begin();
0512             while (it_row != (*it_innergroup).end()) {
0513                 if ((*it_row)[eActual].count() <= column)
0514                     throw MYMONEYEXCEPTION(QString::fromLatin1("Column %1 out of grid range (%2) in PivotTable::accumulateColumn").arg(column).arg((*it_row)[eActual].count()));
0515 
0516                 (*it_row++)[eActual][column] = PivotCell();
0517             }
0518 
0519             ++it_innergroup;
0520         }
0521         ++it_outergroup;
0522     }
0523 }
0524 
0525 void PivotTable::calculateColumnHeadings()
0526 {
0527     DEBUG_ENTER(Q_FUNC_INFO);
0528 
0529     // one column for the opening balance
0530     if (m_startColumn == 1)
0531         m_columnHeadings.append(i18nc("@title:column Report heading", "Opening"));
0532 
0533     int columnpitch = m_config.columnPitch();
0534 
0535     if (columnpitch == 0) {
0536         // output the warning but don't crash by dividing with 0
0537         qWarning("PivotTable::calculateColumnHeadings() Invalid column pitch");
0538         return;
0539     }
0540 
0541     // if this is a days-based report
0542     if (m_config.isColumnsAreDays()) {
0543         if (columnpitch == 1) {
0544             QDate columnDate = m_beginDate;
0545             int column = m_startColumn;
0546             while (column++ < m_numColumns) {
0547                 QString heading = QLocale().monthName(columnDate.month(), QLocale::ShortFormat) + ' ' + QString::number(columnDate.day());
0548                 columnDate = columnDate.addDays(1);
0549                 m_columnHeadings.append(heading);
0550             }
0551         } else {
0552             QDate day = m_beginDate;
0553             QDate prv = m_beginDate;
0554 
0555             // use the user's locale to determine the week's start
0556             int dow = (day.dayOfWeek() + 8 - QLocale().firstDayOfWeek()) % 7;
0557 
0558             while (day <= m_endDate) {
0559                 if (((dow % columnpitch) == 0) || (day == m_endDate)) {
0560                     m_columnHeadings.append(QString("%1&nbsp;%2 - %3&nbsp;%4")
0561                                             .arg(QLocale().monthName(prv.month(), QLocale::ShortFormat))
0562                                             .arg(prv.day())
0563                                             .arg(QLocale().monthName(day.month(), QLocale::ShortFormat))
0564                                             .arg(day.day()));
0565                     prv = day.addDays(1);
0566                 }
0567                 day = day.addDays(1);
0568                 dow++;
0569             }
0570         }
0571     }
0572 
0573     // else it's a months-based report
0574     else {
0575         if (columnpitch == 12) {
0576             int year = m_beginDate.year();
0577             int column = m_startColumn;
0578             const auto columnCoversTwoYears = !fiscalYearIsCalendarYear() || !startDateIsFiscalYearStart();
0579             // in case the report is for a fiscal year and the fiscal year does not
0580             // start on Jan 1st, we show both years, the first with 4 digits, the second
0581             // with 2 digits (e.g. "2021/22"). If the first year is the last of a
0582             // century, we print both with 4 digits (e.g. "2099/2100"). In all
0583             // other cases we simply print the current year with 4 digits.
0584             while (column++ < m_numColumns) {
0585                 QString yearHeading;
0586                 if (columnCoversTwoYears) {
0587                     if ((year % 100) == 99) {
0588                         yearHeading = QStringLiteral("%1/%2").arg(year).arg(year + 1);
0589                     } else {
0590                         yearHeading = QStringLiteral("%1/%2").arg(year).arg((year + 1) % 100, 2, 10, QLatin1Char('0'));
0591                     }
0592                 } else {
0593                     yearHeading = QString::number(year);
0594                 }
0595                 m_columnHeadings.append(yearHeading);
0596                 year++;
0597             }
0598         } else {
0599             int year = m_beginDate.year();
0600             bool includeyear = (m_beginDate.year() != m_endDate.year());
0601             int segment = (m_beginDate.month() - 1) / columnpitch;
0602             int column = m_startColumn;
0603             while (column++ < m_numColumns) {
0604                 QString heading = QLocale().monthName(1 + segment * columnpitch, QLocale::ShortFormat);
0605                 if (columnpitch != 1)
0606                     heading += '-' + QLocale().monthName((1 + segment) * columnpitch, QLocale::ShortFormat);
0607                 if (includeyear)
0608                     heading += ' ' + QString::number(year);
0609                 m_columnHeadings.append(heading);
0610                 if (++segment >= 12 / columnpitch) {
0611                     segment -= 12 / columnpitch;
0612                     ++year;
0613                 }
0614             }
0615         }
0616     }
0617 }
0618 
0619 void PivotTable::createAccountRows()
0620 {
0621     DEBUG_ENTER(Q_FUNC_INFO);
0622     MyMoneyFile* file = MyMoneyFile::instance();
0623 
0624     QList<MyMoneyAccount> accounts;
0625     file->accountList(accounts);
0626 
0627     QList<MyMoneyAccount>::const_iterator it_account = accounts.constBegin();
0628 
0629     while (it_account != accounts.constEnd()) {
0630         ReportAccount account(*it_account);
0631 
0632         // only include this item if its account group is included in this report
0633         // and if the report includes this account
0634         if (m_config.includes(*it_account)) {
0635             DEBUG_OUTPUT(QString("Includes account %1").arg(account.name()));
0636 
0637             // the row group is the account class (major account type)
0638             QString outergroup = MyMoneyAccount::accountTypeToString(account.accountGroup());
0639             // place into the 'opening' column...
0640             assignCell(outergroup, account, 0, MyMoneyMoney());
0641         }
0642         ++it_account;
0643     }
0644 }
0645 
0646 void PivotTable::calculateOpeningBalances()
0647 {
0648     DEBUG_ENTER(Q_FUNC_INFO);
0649 
0650     // First, determine the inclusive dates of the report.  Normally, that's just
0651     // the begin & end dates of m_config_f.  However, if either of those dates are
0652     // blank, we need to use m_beginDate and/or m_endDate instead.
0653     QDate from = m_config.fromDate();
0654     QDate to = m_config.toDate();
0655     if (! from.isValid())
0656         from = m_beginDate;
0657     if (! to.isValid())
0658         to = m_endDate;
0659 
0660     MyMoneyFile* file = MyMoneyFile::instance();
0661 
0662     QList<MyMoneyAccount> accounts;
0663     file->accountList(accounts);
0664 
0665     QList<MyMoneyAccount>::const_iterator it_account = accounts.constBegin();
0666 
0667     JournalModel* journalModel = file->journalModel();
0668     const auto start = journalModel->MyMoneyModelBase::lowerBound(journalModel->keyForDate(m_beginDate)).row();
0669     const auto end = journalModel->MyMoneyModelBase::upperBound(journalModel->keyForDate(m_endDate)).row();
0670 
0671     while (it_account != accounts.constEnd()) {
0672         ReportAccount account(*it_account);
0673 
0674         // only include this item if its account group is included in this report
0675         // and if the report includes this account
0676         if (m_config.includes(*it_account)) {
0677 
0678             //do not include account if it is closed and it has no transactions in the report period
0679             if (account.isClosed()) {
0680                 // check if the account has transactions for the report timeframe
0681                 if ((start == -1) || (end == -1)) {
0682                     ++it_account;
0683                     continue;
0684                 }
0685                 QModelIndex idx;
0686                 bool canSkip = true;
0687                 for (int row = start; canSkip && (row < end); ++row) {
0688                     idx = journalModel->index(row, 0);
0689                     if (idx.data(eMyMoney::Model::SplitAccountIdRole).toString() == account.id()) {
0690                         if (!idx.data(eMyMoney::Model::SplitSharesRole).value<MyMoneyMoney>().isZero()) {
0691                             canSkip = false;
0692                             break;
0693                         }
0694                     }
0695                 }
0696                 if (canSkip) {
0697                     DEBUG_OUTPUT(QString("DOES NOT INCLUDE account %1").arg(account.name()));
0698                     ++it_account;
0699                     continue;
0700                 }
0701             }
0702 
0703             DEBUG_OUTPUT(QString("Includes account %1").arg(account.name()));
0704             // the row group is the account class (major account type)
0705             QString outergroup = MyMoneyAccount::accountTypeToString(account.accountGroup());
0706 
0707             // extract the balance of the account for the given begin date, which is
0708             // the opening balance plus the sum of all transactions prior to the begin
0709             // date
0710 
0711             // this is in the underlying currency
0712             MyMoneyMoney value = file->balance(account.id(), from.addDays(-1));
0713 
0714             if ((columnValue(from) == columnValue(account.openingDate())) && value.isZero()) {
0715                 auto tid = file->openingBalanceTransaction(account);
0716                 if (!tid.isEmpty()) {
0717                     try {
0718                         const auto t = file->transaction(tid);
0719                         const auto s0 = t.splitByAccount(account.id());
0720                         value = s0.shares();
0721                         m_openingBalanceTransactions << tid;
0722                     } catch (const MyMoneyException &e) {
0723                         qDebug() << "Error retrieving opening balance transaction " << tid << ": " << e.what();
0724                     }
0725                 }
0726             }
0727 
0728             // place into the 'opening' column...
0729             assignCell(outergroup, account, 0, value);
0730         } else {
0731             DEBUG_OUTPUT(QString("DOES NOT INCLUDE account %1").arg(account.name()));
0732         }
0733 
0734         ++it_account;
0735     }
0736 }
0737 
0738 void PivotTable::calculateRunningSums(PivotInnerGroup::iterator& it_row)
0739 {
0740     MyMoneyMoney runningsum = it_row.value()[eActual][0].calculateRunningSum(MyMoneyMoney());
0741     int column = m_startColumn;
0742     while (column < m_numColumns) {
0743         if (it_row.value()[eActual].count() <= column)
0744             throw MYMONEYEXCEPTION(QString::fromLatin1("Column %1 out of grid range (%2) in PivotTable::calculateRunningSums").arg(column).arg(it_row.value()[eActual].count()));
0745 
0746         runningsum = it_row.value()[eActual][column].calculateRunningSum(runningsum);
0747 
0748         ++column;
0749     }
0750 }
0751 
0752 void PivotTable::calculateRunningSums()
0753 {
0754     DEBUG_ENTER(Q_FUNC_INFO);
0755 
0756     m_runningSumsCalculated = true;
0757 
0758     PivotGrid::iterator it_outergroup = m_grid.begin();
0759     while (it_outergroup != m_grid.end()) {
0760         PivotOuterGroup::iterator it_innergroup = (*it_outergroup).begin();
0761         while (it_innergroup != (*it_outergroup).end()) {
0762             PivotInnerGroup::iterator it_row = (*it_innergroup).begin();
0763             while (it_row != (*it_innergroup).end()) {
0764 #if 0
0765                 MyMoneyMoney runningsum = it_row.value()[0];
0766                 int column = m_startColumn;
0767                 while (column < m_numColumns) {
0768                     if (it_row.value()[eActual].count() <= column)
0769                         throw MYMONEYEXCEPTION(QString::fromLatin1("Column %1 out of grid range (%2) in PivotTable::calculateRunningSums").arg(column).arg(it_row.value()[eActual].count()));
0770 
0771                     runningsum = (it_row.value()[eActual][column] += runningsum);
0772 
0773                     ++column;
0774                 }
0775 #endif
0776                 calculateRunningSums(it_row);
0777                 ++it_row;
0778             }
0779             ++it_innergroup;
0780         }
0781         ++it_outergroup;
0782     }
0783 }
0784 
0785 MyMoneyMoney PivotTable::cellBalance(const QString& outergroup, const ReportAccount& _row, int _column, bool budget)
0786 {
0787     if (m_runningSumsCalculated) {
0788         qDebug("You must not call PivotTable::cellBalance() after calling PivotTable::calculateRunningSums()");
0789         throw MYMONEYEXCEPTION(QString::fromLatin1("You must not call PivotTable::cellBalance() after calling PivotTable::calculateRunningSums()"));
0790     }
0791 
0792     // for budget reports, if this is the actual value, map it to the account which
0793     // holds its budget
0794     ReportAccount row = _row;
0795     if (!budget && m_config.hasBudget()) {
0796         QString newrow = m_budgetMap[row.id()];
0797 
0798         // if there was no mapping found, then the budget report is not interested
0799         // in this account.
0800         if (newrow.isEmpty())
0801             return MyMoneyMoney();
0802 
0803         row = ReportAccount(newrow);
0804     }
0805 
0806     // ensure the row already exists (and its parental hierarchy)
0807     createRow(outergroup, row, true);
0808 
0809     // Determine the inner group from the top-most parent account
0810     QString innergroup(row.topParentName());
0811 
0812     if (m_numColumns <= _column)
0813         throw MYMONEYEXCEPTION(QString::fromLatin1("Column %1 out of m_numColumns range (%2) in PivotTable::cellBalance").arg(_column).arg(m_numColumns));
0814     if (m_grid[outergroup][innergroup][row][eActual].count() <= _column)
0815         throw MYMONEYEXCEPTION(QString::fromLatin1("Column %1 out of grid range (%2) in PivotTable::cellBalance").arg(_column).arg(m_grid[outergroup][innergroup][row][eActual].count()));
0816 
0817     MyMoneyMoney balance;
0818     if (budget)
0819         balance = m_grid[outergroup][innergroup][row][eBudget][0].cellBalance(MyMoneyMoney());
0820     else
0821         balance = m_grid[outergroup][innergroup][row][eActual][0].cellBalance(MyMoneyMoney());
0822 
0823     int column = m_startColumn;
0824     while (column < _column) {
0825         if (m_grid[outergroup][innergroup][row][eActual].count() <= column)
0826             throw MYMONEYEXCEPTION(QString::fromLatin1("Column %1 out of grid range (%2) in PivotTable::cellBalance").arg(column).arg(m_grid[outergroup][innergroup][row][eActual].count()));
0827 
0828         balance = m_grid[outergroup][innergroup][row][eActual][column].cellBalance(balance);
0829 
0830         ++column;
0831     }
0832 
0833     return balance;
0834 }
0835 
0836 
0837 void PivotTable::calculateBudgetMapping()
0838 {
0839     DEBUG_ENTER(Q_FUNC_INFO);
0840 
0841     MyMoneyFile* file = MyMoneyFile::instance();
0842 
0843     // Only do this if there is at least one budget in the file
0844     if (file->countBudgets()) {
0845         // Select a budget
0846         //
0847         // It will choose the first budget in the list for the start year of the report if no budget is selected
0848         MyMoneyBudget budget = MyMoneyBudget();
0849         QList<MyMoneyBudget> budgets = file->budgetList();
0850         bool validBudget = false;
0851 
0852         //check that the selected budget is valid
0853         if (m_config.budget() != "Any") {
0854             QList<MyMoneyBudget>::const_iterator budgets_it = budgets.constBegin();
0855             while (budgets_it != budgets.constEnd()) {
0856                 //pick the budget by id
0857                 if ((*budgets_it).id() == m_config.budget()) {
0858                     budget = file->budget((*budgets_it).id());
0859                     validBudget = true;
0860                     break;
0861                 }
0862                 ++budgets_it;
0863             }
0864         }
0865 
0866         //if no valid budget has been selected
0867         if (!validBudget) {
0868             //if the budget list is empty, just return
0869             if (budgets.count() == 0) {
0870                 return;
0871             }
0872 
0873             QList<MyMoneyBudget>::const_iterator budgets_it = budgets.constBegin();
0874             while (budgets_it != budgets.constEnd()) {
0875                 //pick the first budget that matches the report start year
0876                 if ((*budgets_it).budgetStart().year() == QDate::currentDate().year()) {
0877                     budget = file->budget((*budgets_it).id());
0878                     break;
0879                 }
0880                 ++budgets_it;
0881             }
0882             //if it can't find a matching budget, take the first one on the list
0883             if (budget.id().isEmpty()) {
0884                 budget = budgets[0];
0885             }
0886 
0887             //assign the budget to the report
0888             m_config.setBudget(budget.id(), m_config.isIncludingBudgetActuals());
0889         }
0890 
0891         // Dump the budget
0892         //qDebug() << "Budget " << budget.name() << ": ";
0893 
0894         // Go through all accounts in the system to build the mapping
0895         QList<MyMoneyAccount> accounts;
0896         file->accountList(accounts);
0897         QList<MyMoneyAccount>::const_iterator it_account = accounts.constBegin();
0898         while (it_account != accounts.constEnd()) {
0899             //include only the accounts selected for the report
0900             if (m_config.includes(*it_account)) {
0901                 QString id = (*it_account).id();
0902                 QString acid = id;
0903 
0904                 // If the budget contains this account outright
0905                 if (budget.contains(id)) {
0906                     // Add it to the mapping
0907                     m_budgetMap[acid] = id;
0908                     // qDebug() << ReportAccount(acid).debugName() << " self-maps / type =" << budget.account(id).budgetLevel();
0909                 }
0910                 // Otherwise, search for a parent account which includes sub-accounts
0911                 else {
0912                     //if includeBudgetActuals, include all accounts regardless of whether in budget or not
0913                     if (m_config.isIncludingBudgetActuals()) {
0914                         m_budgetMap[acid] = id;
0915                         // qDebug() << ReportAccount(acid).debugName() << " maps to " << ReportAccount(id).debugName();
0916                     }
0917                     do {
0918                         id = file->account(id).parentAccountId();
0919                         if (budget.contains(id)) {
0920                             if (budget.account(id).budgetSubaccounts()) {
0921                                 m_budgetMap[acid] = id;
0922                                 // qDebug() << ReportAccount(acid).debugName() << " maps to " << ReportAccount(id).debugName();
0923                                 break;
0924                             }
0925                         }
0926                     } while (! id.isEmpty());
0927                 }
0928             }
0929             ++it_account;
0930         } // end while looping through the accounts in the file
0931 
0932         // Place the budget values into the budget grid
0933         QList<MyMoneyBudget::AccountGroup> baccounts = budget.getaccounts();
0934         QList<MyMoneyBudget::AccountGroup>::const_iterator it_bacc = baccounts.constBegin();
0935         while (it_bacc != baccounts.constEnd()) {
0936             ReportAccount splitAccount((*it_bacc).id());
0937 
0938             //include the budget account only if it is included in the report
0939             if (m_config.includes(splitAccount)) {
0940                 eMyMoney::Account::Type type = splitAccount.accountGroup();
0941                 QString outergroup = MyMoneyAccount::accountTypeToString(type);
0942 
0943                 // reverse sign to match common notation for cash flow direction, only for expense/income splits
0944                 MyMoneyMoney reverse((splitAccount.accountType() == eMyMoney::Account::Type::Expense) ? -1 : 1, 1);
0945 
0946                 const QMap<QDate, MyMoneyBudget::PeriodGroup>& periods = (*it_bacc).getPeriods();
0947 
0948                 // skip the account if it has no periods
0949                 if (periods.count() < 1) {
0950                     ++it_bacc;
0951                     continue;
0952                 }
0953 
0954                 MyMoneyMoney value = (*periods.begin()).amount() * reverse;
0955                 int column = m_startColumn;
0956 
0957                 // based on the kind of budget it is, deal accordingly
0958                 switch ((*it_bacc).budgetLevel()) {
0959                 case eMyMoney::Budget::Level::Yearly:
0960                     // divide the single yearly value by 12 and place it in each column
0961                     value /= MyMoneyMoney(12, 1);
0962                 // intentional fall through
0963 
0964                 case eMyMoney::Budget::Level::None:
0965                 case eMyMoney::Budget::Level::Max:
0966                 case eMyMoney::Budget::Level::Monthly:
0967                     // place the single monthly value in each column of the report
0968                     // only add the value if columns are monthly or longer
0969                     if (m_config.columnType() == eMyMoney::Report::ColumnType::BiMonths
0970                             || m_config.columnType() == eMyMoney::Report::ColumnType::Months
0971                             || m_config.columnType() == eMyMoney::Report::ColumnType::Years
0972                             || m_config.columnType() == eMyMoney::Report::ColumnType::Quarters) {
0973                         QDate budgetDate = budget.budgetStart();
0974                         while (column < m_numColumns && budget.budgetStart().addYears(1) > budgetDate) {
0975                             //only show budget values if the budget year and the column date match
0976                             //no currency conversion is done here because that is done for all columns later
0977                             if (budgetDate >= columnDate(column+1)) {
0978                                 ++column;
0979                             } else {
0980                                 if (budgetDate >= m_beginDate.addDays(-m_beginDate.day() + 1)
0981                                         && budgetDate <= m_endDate.addDays(m_endDate.daysInMonth() - m_endDate.day())
0982                                         && budgetDate > (columnDate(column).addMonths(-static_cast<int>(m_config.columnType())))) {
0983                                     assignCell(outergroup, splitAccount, column, value, true /*budget*/);
0984                                 }
0985                                 budgetDate = budgetDate.addMonths(1);
0986                             }
0987                         }
0988                     }
0989                     break;
0990                 case eMyMoney::Budget::Level::MonthByMonth:
0991                     // place each value in the appropriate column
0992                     // budget periods are supposed to come in order just like columns
0993                 {
0994                     QMap<QDate, MyMoneyBudget::PeriodGroup>::const_iterator it_period = periods.begin();
0995                     while (it_period != periods.end() && column < m_numColumns) {
0996                         if ((*it_period).startDate() >= columnDate(column + 1 - m_startColumn)) {
0997                             ++column;
0998                         } else {
0999                             switch (m_config.columnType()) {
1000                             case eMyMoney::Report::ColumnType::Years:
1001                             case eMyMoney::Report::ColumnType::BiMonths:
1002                             case eMyMoney::Report::ColumnType::Quarters:
1003                             case eMyMoney::Report::ColumnType::Months: {
1004                                 if ((*it_period).startDate() >= m_beginDate.addDays(-m_beginDate.day() + 1)
1005                                         && (*it_period).startDate() <= m_endDate.addDays(m_endDate.daysInMonth() - m_endDate.day())
1006                                         && (*it_period).startDate() > (columnDate(column).addMonths(-static_cast<int>(m_config.columnType())))) {
1007                                     //no currency conversion is done here because that is done for all columns later
1008                                     value = (*it_period).amount() * reverse;
1009                                     assignCell(outergroup, splitAccount, column, value, true /*budget*/);
1010                                 }
1011                                 ++it_period;
1012                                 break;
1013                             }
1014                             default:
1015                                 break;
1016                             }
1017                         }
1018                     }
1019                     break;
1020                 }
1021                 }
1022             }
1023             ++it_bacc;
1024         }
1025     } // end if there was a budget
1026 }
1027 
1028 void PivotTable::convertToBaseCurrency()
1029 {
1030     DEBUG_ENTER(Q_FUNC_INFO);
1031 
1032     MyMoneyFile* file = MyMoneyFile::instance();
1033     int fraction = file->baseCurrency().smallestAccountFraction();
1034     QList<ERowType> rowTypeList = m_rowTypeList;
1035     QMap<QString, MyMoneySecurity> securityCache;
1036 
1037     rowTypeList.removeOne(eAverage);
1038 
1039     PivotGrid::iterator it_outergroup = m_grid.begin();
1040     while (it_outergroup != m_grid.end()) {
1041         PivotOuterGroup::iterator it_innergroup = (*it_outergroup).begin();
1042         while (it_innergroup != (*it_outergroup).end()) {
1043             PivotInnerGroup::iterator it_row = (*it_innergroup).begin();
1044             while (it_row != (*it_innergroup).end()) {
1045                 auto column = 0;
1046                 while (column < m_numColumns) {
1047                     if (it_row.value()[eActual].count() <= column)
1048                         throw MYMONEYEXCEPTION(QString::fromLatin1("Column %1 out of grid range (%2) in PivotTable::convertToBaseCurrency").arg(column).arg(it_row.value()[eActual].count()));
1049 
1050                     QDate valuedate = columnDate(column);
1051 
1052                     //get base price for that date
1053                     MyMoneyMoney conversionfactor = it_row.key().baseCurrencyPrice(valuedate, m_config.isSkippingZero());
1054                     int pricePrecision;
1055                     if (it_row.key().isInvest()) {
1056                         const auto currencyId = it_row.key().currencyId();
1057                         if (!securityCache.contains(currencyId)) {
1058                             securityCache[currencyId] = file->security(it_row.key().currencyId());
1059                         }
1060                         pricePrecision = securityCache[currencyId].pricePrecision();
1061                     } else
1062                         pricePrecision = MyMoneyMoney::denomToPrec(fraction);
1063 
1064                     for (const auto& rowType : qAsConst(rowTypeList)) {
1065                         //calculate base value
1066                         MyMoneyMoney oldval = it_row.value()[rowType][column];
1067                         MyMoneyMoney value = (oldval * conversionfactor).reduce();
1068 
1069                         //convert to lowest fraction
1070                         if (rowType == ePrice)
1071                             it_row.value()[rowType][column] = PivotCell(MyMoneyMoney(value.convertPrecision(pricePrecision)));
1072                         else
1073                             it_row.value()[rowType][column] = PivotCell(value.convert(fraction));
1074 
1075                         DEBUG_OUTPUT_IF(conversionfactor != MyMoneyMoney::ONE, QString("Factor of %1, value was %2, now %3").arg(conversionfactor.toDouble()).arg(DEBUG_SENSITIVE(oldval.toDouble())).arg(DEBUG_SENSITIVE(it_row.value()[rowType][column].toDouble())));
1076                     }
1077                     ++column;
1078                 }
1079                 ++it_row;
1080             }
1081             ++it_innergroup;
1082         }
1083         ++it_outergroup;
1084     }
1085 }
1086 
1087 void PivotTable::convertToDeepCurrency()
1088 {
1089     DEBUG_ENTER(Q_FUNC_INFO);
1090     MyMoneyFile* file = MyMoneyFile::instance();
1091 
1092     PivotGrid::iterator it_outergroup = m_grid.begin();
1093     while (it_outergroup != m_grid.end()) {
1094         PivotOuterGroup::iterator it_innergroup = (*it_outergroup).begin();
1095         while (it_innergroup != (*it_outergroup).end()) {
1096             PivotInnerGroup::iterator it_row = (*it_innergroup).begin();
1097             while (it_row != (*it_innergroup).end()) {
1098                 auto column = 0;
1099                 while (column < m_numColumns) {
1100                     if (it_row.value()[eActual].count() <= column)
1101                         throw MYMONEYEXCEPTION(QString::fromLatin1("Column %1 out of grid range (%2) in PivotTable::convertToDeepCurrency").arg(column).arg(it_row.value()[eActual].count()));
1102 
1103                     QDate valuedate = columnDate(column);
1104 
1105                     //get conversion factor for the account and date
1106                     MyMoneyMoney conversionfactor = it_row.key().deepCurrencyPrice(valuedate, m_config.isSkippingZero());
1107 
1108                     //use the fraction relevant to the account at hand
1109                     int fraction = it_row.key().currency().smallestAccountFraction();
1110 
1111                     //use base currency fraction if not initialized
1112                     if (fraction == -1)
1113                         fraction = file->baseCurrency().smallestAccountFraction();
1114 
1115                     //convert to deep currency
1116                     MyMoneyMoney oldval = it_row.value()[eActual][column];
1117                     MyMoneyMoney value = (oldval * conversionfactor).reduce();
1118                     //reduce to lowest fraction
1119                     it_row.value()[eActual][column] = PivotCell(value.convert(fraction));
1120 
1121                     //convert price data
1122                     if (m_config.isIncludingPrice()) {
1123                         MyMoneyMoney oldPriceVal = it_row.value()[ePrice][column];
1124                         MyMoneyMoney priceValue = (oldPriceVal * conversionfactor).reduce();
1125                         it_row.value()[ePrice][column] = PivotCell(priceValue.convert(10000));
1126                     }
1127 
1128                     DEBUG_OUTPUT_IF(conversionfactor != MyMoneyMoney::ONE, QString("Factor of %1, value was %2, now %3").arg(conversionfactor.toDouble()).arg(DEBUG_SENSITIVE(oldval.toDouble())).arg(DEBUG_SENSITIVE(it_row.value()[eActual][column].toDouble())));
1129 
1130                     ++column;
1131                 }
1132                 ++it_row;
1133             }
1134             ++it_innergroup;
1135         }
1136         ++it_outergroup;
1137     }
1138 }
1139 
1140 void PivotTable::calculateTotals()
1141 {
1142     //insert the row type that is going to be used
1143     for (int i = 0; i < m_rowTypeList.size(); ++i) {
1144         for (int k = 0; k < m_numColumns; ++k) {
1145             m_grid.m_total[ m_rowTypeList[i] ].append(PivotCell());
1146         }
1147     }
1148     //
1149     // Outer groups
1150     //
1151 
1152     // iterate over outer groups
1153     PivotGrid::iterator it_outergroup = m_grid.begin();
1154     while (it_outergroup != m_grid.end()) {
1155         for (int i = 0; i < m_rowTypeList.size(); ++i) {
1156             for (int k = 0; k < m_numColumns; ++k) {
1157                 (*it_outergroup).m_total[ m_rowTypeList[i] ].append(PivotCell());
1158             }
1159         }
1160 
1161         //
1162         // Inner Groups
1163         //
1164 
1165         PivotOuterGroup::iterator it_innergroup = (*it_outergroup).begin();
1166         while (it_innergroup != (*it_outergroup).end()) {
1167             for (int i = 0; i < m_rowTypeList.size(); ++i) {
1168                 for (int k = 0; k < m_numColumns; ++k) {
1169                     (*it_innergroup).m_total[ m_rowTypeList[i] ].append(PivotCell());
1170                 }
1171             }
1172             //
1173             // Rows
1174             //
1175 
1176             PivotInnerGroup::iterator it_row = (*it_innergroup).begin();
1177             while (it_row != (*it_innergroup).end()) {
1178                 //
1179                 // Columns
1180                 //
1181 
1182                 auto column = 0;
1183                 while (column < m_numColumns) {
1184                     for (int i = 0; i < m_rowTypeList.size(); ++i) {
1185                         if (it_row.value()[ m_rowTypeList[i] ].count() <= column)
1186                             throw MYMONEYEXCEPTION(QString::fromLatin1("Column %1 out of grid range (%2) in PivotTable::calculateTotals, row columns").arg(column).arg(it_row.value()[ m_rowTypeList[i] ].count()));
1187                         if ((*it_innergroup).m_total[ m_rowTypeList[i] ].count() <= column)
1188                             throw MYMONEYEXCEPTION(QString::fromLatin1("Column %1 out of grid range (%2) in PivotTable::calculateTotals, inner group totals").arg(column).arg((*it_innergroup).m_total[ m_rowTypeList[i] ].count()));
1189 
1190                         //calculate total
1191                         MyMoneyMoney value = it_row.value()[ m_rowTypeList[i] ][column];
1192                         (*it_innergroup).m_total[ m_rowTypeList[i] ][column] += value;
1193                         (*it_row)[ m_rowTypeList[i] ].m_total += value;
1194                     }
1195                     ++column;
1196                 }
1197                 ++it_row;
1198             }
1199 
1200             //
1201             // Inner Row Group Totals
1202             //
1203 
1204             auto column = 0;
1205             while (column < m_numColumns) {
1206                 for (int i = 0; i < m_rowTypeList.size(); ++i) {
1207                     if ((*it_innergroup).m_total[ m_rowTypeList[i] ].count() <= column)
1208                         throw MYMONEYEXCEPTION(QString::fromLatin1("Column %1 out of grid range (%2) in PivotTable::calculateTotals, inner group totals").arg(column).arg((*it_innergroup).m_total[ m_rowTypeList[i] ].count()));
1209                     if ((*it_outergroup).m_total[ m_rowTypeList[i] ].count() <= column)
1210                         throw MYMONEYEXCEPTION(QString::fromLatin1("Column %1 out of grid range (%2) in PivotTable::calculateTotals, outer group totals").arg(column).arg((*it_innergroup).m_total[ m_rowTypeList[i] ].count()));
1211 
1212                     //calculate totals
1213                     MyMoneyMoney value = (*it_innergroup).m_total[ m_rowTypeList[i] ][column];
1214                     (*it_outergroup).m_total[ m_rowTypeList[i] ][column] += value;
1215                     (*it_innergroup).m_total[ m_rowTypeList[i] ].m_total += value;
1216                 }
1217                 ++column;
1218             }
1219 
1220             ++it_innergroup;
1221         }
1222 
1223         //
1224         // Outer Row Group Totals
1225         //
1226 
1227         const bool isIncomeExpense = (m_config.rowType() == eMyMoney::Report::RowType::ExpenseIncome);
1228         const bool invert_total = (*it_outergroup).m_inverted;
1229         auto column = 0;
1230         while (column < m_numColumns) {
1231             for (int i = 0; i < m_rowTypeList.size(); ++i) {
1232                 if (m_grid.m_total[ m_rowTypeList[i] ].count() <= column)
1233                     throw MYMONEYEXCEPTION(QString::fromLatin1("Column %1 out of grid range (%2) in PivotTable::calculateTotals, grid totals").arg(column).arg((*it_innergroup).m_total[ m_rowTypeList[i] ].count()));
1234 
1235                 //calculate actual totals
1236                 MyMoneyMoney value = (*it_outergroup).m_total[ m_rowTypeList[i] ][column];
1237                 (*it_outergroup).m_total[ m_rowTypeList[i] ].m_total += value;
1238 
1239                 //so far the invert only applies to actual and budget
1240                 if (invert_total && m_rowTypeList[i] != eBudgetDiff && m_rowTypeList[i] != eForecast)
1241                     value = -value;
1242                 // forecast income expense reports should be inverted as opposed to asset/liability reports
1243                 if (invert_total && isIncomeExpense && m_rowTypeList[i] == eForecast)
1244                     value = -value;
1245 
1246                 m_grid.m_total[ m_rowTypeList[i] ][column] += value;
1247             }
1248             ++column;
1249         }
1250         ++it_outergroup;
1251     }
1252 
1253     //
1254     // Report Totals
1255     //
1256 
1257     auto totalcolumn = 0;
1258     while (totalcolumn < m_numColumns) {
1259         for (int i = 0; i < m_rowTypeList.size(); ++i) {
1260             if (m_grid.m_total[ m_rowTypeList[i] ].count() <= totalcolumn)
1261                 throw MYMONEYEXCEPTION(QString::fromLatin1("Total column %1 out of grid range (%2) in PivotTable::calculateTotals, grid totals").arg(totalcolumn).arg(m_grid.m_total[ m_rowTypeList[i] ].count()));
1262 
1263             //calculate actual totals
1264             MyMoneyMoney value = m_grid.m_total[ m_rowTypeList[i] ][totalcolumn];
1265             m_grid.m_total[ m_rowTypeList[i] ].m_total += value;
1266         }
1267         ++totalcolumn;
1268     }
1269 }
1270 
1271 void PivotTable::assignCell(const QString& outergroup, const ReportAccount& _row, int column, MyMoneyMoney value, bool budget, bool stockSplit)
1272 {
1273     DEBUG_ENTER(Q_FUNC_INFO);
1274     DEBUG_OUTPUT(QString("Parameters: %1,%2,%3,%4,%5").arg(outergroup).arg(_row.debugName()).arg(column).arg(DEBUG_SENSITIVE(value.toDouble())).arg(budget));
1275 
1276     // for budget reports, if this is the actual value, map it to the account which
1277     // holds its budget
1278     ReportAccount row = _row;
1279     if (!budget && m_config.hasBudget()) {
1280         QString newrow = m_budgetMap[row.id()];
1281 
1282         // if there was no mapping found, then the budget report is not interested
1283         // in this account.
1284         if (newrow.isEmpty())
1285             return;
1286 
1287         row = ReportAccount(newrow);
1288     }
1289 
1290     // ensure the row already exists (and its parental hierarchy)
1291     createRow(outergroup, row, true);
1292 
1293     // Determine the inner group from the top-most parent account
1294     QString innergroup(row.topParentName());
1295 
1296     if (m_numColumns <= column)
1297         throw MYMONEYEXCEPTION(QString::fromLatin1("Column %1 out of m_numColumns range (%2) in PivotTable::assignCell").arg(column).arg(m_numColumns));
1298     if (m_grid[outergroup][innergroup][row][eActual].count() <= column)
1299         throw MYMONEYEXCEPTION(QString::fromLatin1("Column %1 out of grid range (%2) in PivotTable::assignCell").arg(column).arg(m_grid[outergroup][innergroup][row][eActual].count()));
1300     if (m_grid[outergroup][innergroup][row][eBudget].count() <= column)
1301         throw MYMONEYEXCEPTION(QString::fromLatin1("Column %1 out of grid range (%2) in PivotTable::assignCell").arg(column).arg(m_grid[outergroup][innergroup][row][eBudget].count()));
1302 
1303     if (!stockSplit) {
1304         // Determine whether the value should be inverted before being placed in the row
1305         if (m_grid[outergroup].m_inverted)
1306             value = -value;
1307 
1308         // Add the value to the grid cell
1309         if (budget) {
1310             m_grid[outergroup][innergroup][row][eBudget][column] += value;
1311         } else {
1312             // If it is loading an actual value for a budget report
1313             // check whether it is a subaccount of a budget account (include subaccounts)
1314             // If so, check if is the same currency and convert otherwise
1315             if (m_config.hasBudget() &&
1316                     row.id() != _row.id() &&
1317                     row.currencyId() != _row.currencyId()) {
1318                 ReportAccount origAcc = _row;
1319                 MyMoneyMoney rate = origAcc.foreignCurrencyPrice(row.currencyId(), columnDate(column), false);
1320                 m_grid[outergroup][innergroup][row][eActual][column] += (value * rate).reduce();
1321             } else {
1322                 m_grid[outergroup][innergroup][row][eActual][column] += value;
1323             }
1324         }
1325     } else {
1326         m_grid[outergroup][innergroup][row][eActual][column] += PivotCell::stockSplit(value);
1327     }
1328 
1329 }
1330 
1331 void PivotTable::createRow(const QString& outergroup, const ReportAccount& row, bool recursive)
1332 {
1333     DEBUG_ENTER(Q_FUNC_INFO);
1334 
1335     // Determine the inner group from the top-most parent account
1336     QString innergroup(row.topParentName());
1337 
1338     if (! m_grid.contains(outergroup)) {
1339         DEBUG_OUTPUT(QString("Adding group [%1]").arg(outergroup));
1340         m_grid[outergroup] = PivotOuterGroup(m_numColumns);
1341     }
1342 
1343     if (! m_grid[outergroup].contains(innergroup)) {
1344         DEBUG_OUTPUT(QString("Adding group [%1][%2]").arg(outergroup).arg(innergroup));
1345         m_grid[outergroup][innergroup] = PivotInnerGroup(m_numColumns);
1346     }
1347 
1348     if (! m_grid[outergroup][innergroup].contains(row)) {
1349         DEBUG_OUTPUT(QString("Adding row [%1][%2][%3]").arg(outergroup).arg(innergroup).arg(row.debugName()));
1350         m_grid[outergroup][innergroup][row] = PivotGridRowSet(m_numColumns);
1351 
1352         if (recursive && !row.isTopLevel())
1353             createRow(outergroup, row.parent(), recursive);
1354     }
1355 }
1356 
1357 int PivotTable::columnValue(const QDate& _date) const
1358 {
1359     if (m_config.isColumnsAreDays())
1360         return (static_cast<int>(m_beginDate.daysTo(_date)));
1361     else
1362         return (_date.year() * 12 + _date.month());
1363 }
1364 
1365 QDate PivotTable::columnDate(int column) const
1366 {
1367     if (m_config.isColumnsAreDays()) {
1368         // The m_config.columnPitch() return value is defined as 'unsigned int'.
1369         // We need to cast this to an 'int' here, because if column is zero (e.g.
1370         // for the opening balance) and m_startColumn is one, the result of the
1371         // subtraction should be -1 (the day before m_beginDate), but since it
1372         // is treated as 'unsigned int' a very large value is added resulting in a
1373         // date way in the future.
1374         return m_beginDate.addDays(static_cast<int>(m_config.columnPitch() * column) - m_startColumn);
1375     } else
1376         return m_beginDate.addMonths(m_config.columnPitch() * column).addDays(-m_startColumn);
1377 }
1378 
1379 QString PivotTable::renderCSV() const
1380 {
1381     DEBUG_ENTER(Q_FUNC_INFO);
1382 
1383     MyMoneyFile* file = MyMoneyFile::instance();
1384     int pricePrecision = 0;
1385     int currencyPrecision = 0;
1386     int precision = MyMoneyMoney::denomToPrec(file->baseCurrency().smallestAccountFraction());
1387 
1388     bool isMultipleCurrencies = false;
1389 
1390     //
1391     // Table Header
1392     //
1393 
1394     QString result = i18nc("@title:column report header", "Account");
1395 
1396     auto column = 0;
1397     while (column < m_numColumns) {
1398         result += QString(",%1").arg(QString(m_columnHeadings[column++]));
1399         if (m_rowTypeList.size() > 1) {
1400             QString separator;
1401             separator = separator.fill(',', m_rowTypeList.size() - 1);
1402             result += separator;
1403         }
1404     }
1405 
1406     //show total columns
1407     if (m_config.isShowingRowTotals())
1408         result += QString(",%1").arg(i18nc("Total balance", "Total"));
1409 
1410     result += '\n';
1411 
1412     // Row Type Header
1413     if (m_rowTypeList.size() > 1) {
1414         column = 0;
1415         while (column < m_numColumns) {
1416             for (int i = 0; i < m_rowTypeList.size(); ++i) {
1417                 result += QString(",%1").arg(m_columnTypeHeaderList[i]);
1418             }
1419             column++;
1420         }
1421         if (m_config.isShowingRowTotals()) {
1422             for (int i = 0; i < m_rowTypeList.size(); ++i) {
1423                 result += QString(",%1").arg(m_columnTypeHeaderList[i]);
1424             }
1425         }
1426         result += '\n';
1427     }
1428 
1429     //
1430     // Outer groups
1431     //
1432 
1433     // iterate over outer groups
1434     PivotGrid::const_iterator it_outergroup = m_grid.begin();
1435     while (it_outergroup != m_grid.end()) {
1436         //
1437         // Outer Group Header
1438         //
1439 
1440         if (!(m_config.isIncludingPrice() || m_config.isIncludingAveragePrice()))
1441             result += it_outergroup.key() + '\n';
1442 
1443         //
1444         // Inner Groups
1445         //
1446 
1447         PivotOuterGroup::const_iterator it_innergroup = (*it_outergroup).begin();
1448         while (it_innergroup != (*it_outergroup).end()) {
1449             //
1450             // Rows
1451             //
1452 
1453             QString innergroupdata;
1454             PivotInnerGroup::const_iterator it_row = (*it_innergroup).begin();
1455             while (it_row != (*it_innergroup).end()) {
1456                 ReportAccount rowname = it_row.key();
1457 
1458                 //
1459                 // Columns
1460                 //
1461 
1462                 QString rowdata;
1463                 column = 0;
1464 
1465                 bool isUsed = false;
1466                 for (int i = 0; i < m_rowTypeList.size(); ++i)
1467                     isUsed |= it_row.value()[ m_rowTypeList[i] ][0].isUsed();
1468 
1469                 if (it_row.key().accountType() != eMyMoney::Account::Type::Investment) {
1470                     while (column < m_numColumns) {
1471 
1472                         //show columns
1473                         for (const auto& rowType : qAsConst(m_rowTypeList)) {
1474                             if (rowType == ePrice) {
1475                                 if (pricePrecision == 0) {
1476                                     if (it_row.key().isInvest()) {
1477                                         pricePrecision = file->currency(it_row.key().currencyId()).pricePrecision();
1478                                         precision = pricePrecision;
1479                                     } else
1480                                         precision = MyMoneyMoney::denomToPrec(file->baseCurrency().smallestAccountFraction());
1481                                 } else
1482                                     precision = pricePrecision;
1483                             } else {
1484                                 if (currencyPrecision == 0) {
1485                                     if (it_row.key().isInvest()) // stock account isn't evaluated in currency, so take investment account instead
1486                                         currencyPrecision = MyMoneyMoney::denomToPrec(it_row.key().parent().fraction());
1487                                     else
1488                                         currencyPrecision = MyMoneyMoney::denomToPrec(it_row.key().fraction());
1489                                     precision = currencyPrecision;
1490                                 } else
1491                                     precision = currencyPrecision;
1492                             }
1493                             rowdata += QString(",\"%1\"").arg(it_row.value()[rowType][column].formatMoney(QString(), precision, false));
1494                             isUsed |= it_row.value()[rowType][column].isUsed();
1495                         }
1496                         column++;
1497                     }
1498 
1499                     if (m_config.isShowingRowTotals()) {
1500                         for (int i = 0; i < m_rowTypeList.size(); ++i)
1501                             rowdata += QString(",\"%1\"").arg((*it_row)[ m_rowTypeList[i] ].m_total.formatMoney(QString(), precision, false));
1502                     }
1503                 } else {
1504                     for (auto i = 0; i < m_numColumns + m_rowTypeList.size(); ++i)
1505                         rowdata.append(',');
1506                 }
1507 
1508                 //
1509                 // Row Header
1510                 //
1511 
1512                 if (!rowname.isClosed() || isUsed) {
1513                     innergroupdata += "\"" + QString().fill(' ', rowname.hierarchyDepth() - 1) + rowname.name();
1514 
1515                     // if we don't convert the currencies to the base currency and the
1516                     // current row contains a foreign currency, then we append the currency
1517                     // to the name of the account
1518                     if (!m_config.isConvertCurrency() && rowname.isForeignCurrency())
1519                         innergroupdata += QString(" (%1)").arg(rowname.currencyId());
1520 
1521                     innergroupdata += '\"';
1522 
1523                     if (isUsed)
1524                         innergroupdata += rowdata;
1525 
1526                     innergroupdata += '\n';
1527                     if (!isMultipleCurrencies && rowname.isForeignCurrency())
1528                         isMultipleCurrencies = true;
1529                     if (!m_containsNonBaseCurrency && rowname.isForeignCurrency())
1530                         m_containsNonBaseCurrency = true;
1531                 }
1532                 ++it_row;
1533             }
1534 
1535             //
1536             // Inner Row Group Totals
1537             //
1538 
1539             bool finishrow = true;
1540             QString finalRow;
1541             bool isUsed = false;
1542             if (m_config.detailLevel() == eMyMoney::Report::DetailLevel::All && ((*it_innergroup).size() > 1)) {
1543                 // Print the individual rows
1544                 result += innergroupdata;
1545 
1546                 if (m_config.isConvertCurrency() && m_config.isShowingColumnTotals()) {
1547                     // Start the TOTALS row
1548                     finalRow = i18nc("Total balance", "Total");
1549                     isUsed = true;
1550                 } else {
1551                     finishrow = false;
1552                 }
1553             } else {
1554                 // Start the single INDIVIDUAL ACCOUNT row
1555                 ReportAccount rowname = (*it_innergroup).begin().key();
1556                 isUsed |= !rowname.isClosed();
1557 
1558                 finalRow = "\"" + QString().fill(' ', rowname.hierarchyDepth() - 1) + rowname.name();
1559                 if (!m_config.isConvertCurrency() && rowname.isForeignCurrency())
1560                     finalRow += QString(" (%1)").arg(rowname.currencyId());
1561                 finalRow += "\"";
1562             }
1563 
1564             // Finish the row started above, unless told not to
1565             if (finishrow) {
1566                 column = 0;
1567 
1568                 for (int i = 0; i < m_rowTypeList.size(); ++i)
1569                     isUsed |= (*it_innergroup).m_total[ m_rowTypeList[i] ][0].isUsed();
1570 
1571                 while (column < m_numColumns) {
1572                     for (int i = 0; i < m_rowTypeList.size(); ++i) {
1573                         isUsed |= (*it_innergroup).m_total[ m_rowTypeList[i] ][column].isUsed();
1574                         finalRow += QString(",\"%1\"").arg((*it_innergroup).m_total[ m_rowTypeList[i] ][column].formatMoney(QString(), precision, false));
1575                     }
1576                     column++;
1577                 }
1578 
1579                 if (m_config.isShowingRowTotals()) {
1580                     for (int i = 0; i < m_rowTypeList.size(); ++i)
1581                         finalRow += QString(",\"%1\"").arg((*it_innergroup).m_total[ m_rowTypeList[i] ].m_total.formatMoney(QString(), precision, false));
1582                 }
1583 
1584                 finalRow += '\n';
1585             }
1586 
1587             if (isUsed) {
1588                 result += finalRow;
1589             }
1590             ++it_innergroup;
1591         }
1592 
1593         //
1594         // Outer Row Group Totals
1595         //
1596 
1597         if (m_config.isConvertCurrency() && m_config.isShowingColumnTotals()) {
1598             result += QString("%1 %2").arg(i18nc("Total balance", "Total"), it_outergroup.key());
1599             column = 0;
1600             while (column < m_numColumns) {
1601                 for (int i = 0; i < m_rowTypeList.size(); ++i)
1602                     result += QString(",\"%1\"").arg((*it_outergroup).m_total[ m_rowTypeList[i] ][column].formatMoney(QString(), precision, false));
1603 
1604                 column++;
1605             }
1606 
1607             if (m_config.isShowingRowTotals()) {
1608                 for (int i = 0; i < m_rowTypeList.size(); ++i)
1609                     result += QString(",\"%1\"").arg((*it_outergroup).m_total[ m_rowTypeList[i] ].m_total.formatMoney(QString(), precision, false));
1610             }
1611 
1612             result += '\n';
1613         }
1614         ++it_outergroup;
1615     }
1616 
1617     //
1618     // Report Totals
1619     //
1620 
1621     if (m_config.isConvertCurrency() && m_config.isShowingColumnTotals()) {
1622         result += i18n("Grand Total");
1623         auto totalcolumn = 0;
1624         while (totalcolumn < m_numColumns) {
1625             for (int i = 0; i < m_rowTypeList.size(); ++i)
1626                 result += QString(",\"%1\"").arg(m_grid.m_total[ m_rowTypeList[i] ][totalcolumn].formatMoney(QString(), precision, false));
1627 
1628             totalcolumn++;
1629         }
1630 
1631         if (m_config.isShowingRowTotals()) {
1632             for (int i = 0; i < m_rowTypeList.size(); ++i)
1633                 result += QString(",\"%1\"").arg(m_grid.m_total[ m_rowTypeList[i] ].m_total.formatMoney(QString(), precision, false));
1634         }
1635 
1636         result += '\n';
1637     }
1638     return result;
1639 }
1640 
1641 QString PivotTable::renderHTML() const
1642 {
1643     DEBUG_ENTER(Q_FUNC_INFO);
1644 
1645     MyMoneyFile* file = MyMoneyFile::instance();
1646     int pricePrecision = 0;
1647     int currencyPrecision = 0;
1648     int precision = MyMoneyMoney::denomToPrec(file->baseCurrency().smallestAccountFraction());
1649     QString colspan = QString(" colspan=\"%1\"").arg(m_numColumns + 1 + (m_config.isShowingRowTotals() ? 1 : 0));
1650 
1651     // setup a leftborder for better readability of budget vs actual reports
1652     QString leftborder;
1653     if (m_rowTypeList.size() > 1)
1654         leftborder = " class=\"leftborder\"";
1655 
1656     //
1657     // Table Header
1658     //
1659     QString result = QString(
1660                          "\n\n<table align=\"center\" class=\"report\" cellspacing=\"0\">\n"
1661                          "<thead><tr class=\"itemheader\">\n<th>%1</th>")
1662                          .arg(i18nc("@title:column report header", "Account"));
1663 
1664     QString headerspan;
1665     int span = m_rowTypeList.size();
1666 
1667     headerspan = QString(" colspan=\"%1\"").arg(span);
1668 
1669     auto column = 0;
1670     static const QRegularExpression whiteSpaceRegex(QLatin1String(" "));
1671     while (column < m_numColumns)
1672         result += QString("<th%1>%2</th>").arg(headerspan, QString(m_columnHeadings[column++]).replace(whiteSpaceRegex, "<br>"));
1673 
1674     if (m_config.isShowingRowTotals())
1675         result += QString("<th%1>%2</th>").arg(headerspan, i18nc("Total balance", "Total"));
1676 
1677     result += "</tr></thead>\n";
1678 
1679     //
1680     // Header for multiple columns
1681     //
1682     if (span > 1) {
1683         result += "<tr><td></td>";
1684 
1685         column = 0;
1686         while (column < m_numColumns) {
1687             QString lb;
1688             if (column != 0)
1689                 lb = leftborder;
1690 
1691             for (int i = 0; i < m_rowTypeList.size(); ++i) {
1692                 result += QString("<td%2>%1</td>").arg(m_columnTypeHeaderList[i], i == 0 ? lb : QString());
1693             }
1694             column++;
1695         }
1696         if (m_config.isShowingRowTotals()) {
1697             for (int i = 0; i < m_rowTypeList.size(); ++i) {
1698                 result += QString("<td%2>%1</td>").arg(m_columnTypeHeaderList[i], i == 0 ? leftborder : QString());
1699             }
1700         }
1701         result += "</tr>";
1702     }
1703 
1704 
1705     // Skip the body of the report if the report only calls for totals to be shown
1706     if (m_config.detailLevel() != eMyMoney::Report::DetailLevel::Total) {
1707         //
1708         // Outer groups
1709         //
1710 
1711         // Need to sort the outergroups.  They can't always be sorted by name.  So we create a list of
1712         // map iterators, and sort that.  Then we'll iterate through the map iterators and use those as
1713         // before.
1714         //
1715         // I hope this doesn't bog the performance of reports, given that we're copying the entire report
1716         // data.  If this is a perf hit, we could change to storing outergroup pointers, I think.
1717 
1718         QList<PivotOuterGroup> outergroups;
1719         PivotGrid::const_iterator it_outergroup_map = m_grid.begin();
1720         while (it_outergroup_map != m_grid.end()) {
1721             outergroups.push_back(it_outergroup_map.value());
1722 
1723             // copy the name into the outergroup, because we will now lose any association with
1724             // the map iterator
1725             outergroups.back().m_displayName = it_outergroup_map.key();
1726 
1727             ++it_outergroup_map;
1728         }
1729         std::sort(outergroups.begin(), outergroups.end());
1730 
1731         QList<PivotOuterGroup>::const_iterator it_outergroup = outergroups.constBegin();
1732         while (it_outergroup != outergroups.constEnd()) {
1733             //
1734             // Outer Group Header
1735             //
1736 
1737             if (!(m_config.isIncludingPrice() || m_config.isIncludingAveragePrice()))
1738                 result += QString("<tr class=\"sectionheader\"><td class=\"left\"%1>%2</td></tr>\n").arg(colspan, (*it_outergroup).m_displayName);
1739 
1740             // Skip the inner groups if the report only calls for outer group totals to be shown
1741             if (m_config.detailLevel() != eMyMoney::Report::DetailLevel::Group) {
1742 
1743                 //
1744                 // Inner Groups
1745                 //
1746 
1747                 PivotOuterGroup::const_iterator it_innergroup = (*it_outergroup).begin();
1748                 int rownum = 0;
1749                 while (it_innergroup != (*it_outergroup).end()) {
1750                     //
1751                     // Rows
1752                     //
1753 
1754                     QString innergroupdata;
1755                     PivotInnerGroup::const_iterator it_row = (*it_innergroup).begin();
1756 
1757                     while (it_row != (*it_innergroup).end()) {
1758                         //
1759                         // Columns
1760                         //
1761 
1762                         QString rowdata;
1763                         column = 0;
1764                         pricePrecision = 0; // new row => new account => new precision
1765                         currencyPrecision = 0;
1766                         bool isUsed = it_row.value()[eActual][0].isUsed();
1767                         if (it_row.key().accountType() != eMyMoney::Account::Type::Investment) {
1768                             while (column < m_numColumns) {
1769                                 QString lb;
1770                                 if (column > 0)
1771                                     lb = leftborder;
1772 
1773                                 for (const auto& rowType : qAsConst(m_rowTypeList)) {
1774                                     if (rowType == ePrice) {
1775                                         if (pricePrecision == 0) {
1776                                             if (it_row.key().isInvest()) {
1777                                                 pricePrecision = file->currency(it_row.key().currencyId()).pricePrecision();
1778                                                 precision = pricePrecision;
1779                                             } else
1780                                                 precision = MyMoneyMoney::denomToPrec(file->baseCurrency().smallestAccountFraction());
1781                                         } else
1782                                             precision = pricePrecision;
1783                                     } else {
1784                                         if (currencyPrecision == 0) {
1785                                             if (it_row.key().isInvest()) // stock account isn't evaluated in currency, so take investment account instead
1786                                                 currencyPrecision = MyMoneyMoney::denomToPrec(it_row.key().parent().fraction());
1787                                             else
1788                                                 currencyPrecision = MyMoneyMoney::denomToPrec(
1789                                                     !m_config.isConvertCurrency() ? it_row.key().fraction() : file->baseCurrency().smallestAccountFraction());
1790                                             precision = currencyPrecision;
1791                                         } else
1792                                             precision = currencyPrecision;
1793                                     }
1794                                     rowdata += QString("<td%2>%1</td>").arg(coloredAmount(it_row.value()[rowType][column], QString(), precision), lb);
1795                                     lb.clear();
1796                                     isUsed |= it_row.value()[rowType][column].isUsed();
1797                                 }
1798                                 ++column;
1799                             }
1800 
1801                             if (m_config.isShowingRowTotals()) {
1802                                 for (int i = 0; i < m_rowTypeList.size(); ++i) {
1803                                     rowdata += QString("<td%2>%1</td>")
1804                                                    .arg(coloredAmount(it_row.value()[m_rowTypeList[i]].m_total, QString(), precision),
1805                                                         i == 0 ? leftborder : QString());
1806                                 }
1807                             }
1808                         } else
1809                             rowdata += QStringLiteral("<td colspan=%1></td>").arg(m_numColumns + m_rowTypeList.size());
1810 
1811                         //
1812                         // Row Header
1813                         //
1814 
1815                         ReportAccount rowname = it_row.key();
1816 
1817                         // don't show closed accounts if they have not been used
1818                         if (!rowname.isClosed() || isUsed) {
1819                             innergroupdata +=
1820                                 QString("<tr class=\"row-%1\"%2><td%3 class=\"left\" style=\"text-indent: %4.0em\">%5%6</td>")
1821                                     .arg((rownum & 0x01 ? "even" : "odd"), (rowname.isTopLevel() ? " id=\"topparent\"" : ""), "")
1822                                     .arg((rowname.hierarchyDepth() - 1) * 16)
1823                                     .arg(rowname.name().replace(whiteSpaceRegex, "&nbsp;").replace("<", "&lt;").replace(">", "&gt;"),
1824                                          (m_config.isConvertCurrency() || !rowname.isForeignCurrency()) ? QString()
1825                                                                                                         : QString(" (%1)").arg(rowname.currency().id()));
1826 
1827                             // Don't print this row if it's going to be all zeros
1828                             // TODO: Uncomment this, and deal with the case where the data
1829                             // is zero, but the budget is non-zero
1830                             //if ( !(*it_row).m_total.isZero() )
1831                             innergroupdata += rowdata;
1832 
1833                             innergroupdata += "</tr>\n";
1834                             if (!m_containsNonBaseCurrency && rowname.isForeignCurrency())
1835                                 m_containsNonBaseCurrency = true;
1836                         }
1837 
1838                         ++it_row;
1839                     }
1840 
1841                     //
1842                     // Inner Row Group Totals
1843                     //
1844 
1845                     bool finishrow = true;
1846                     QString finalRow;
1847                     bool isUsed = false;
1848                     if (m_config.detailLevel() == eMyMoney::Report::DetailLevel::All && ((*it_innergroup).size() > 1)) {
1849                         // Print the individual rows
1850                         result += innergroupdata;
1851 
1852                         if (m_config.isConvertCurrency() && m_config.isShowingColumnTotals()) {
1853                             // Start the TOTALS row
1854                             finalRow = QString("<tr class=\"row-%1\" id=\"subtotal\"><td class=\"left\">&nbsp;&nbsp;%2</td>")
1855                                            .arg(rownum & 0x01 ? "even" : "odd", i18nc("Total balance", "Total"));
1856                             // don't suppress display of totals
1857                             isUsed = true;
1858                         } else {
1859                             finishrow = false;
1860                             ++rownum;
1861                         }
1862                     } else {
1863                         // Start the single INDIVIDUAL ACCOUNT row
1864                         // FIXME: There is a bit of a bug here with class=leftX.  There's only a finite number
1865                         // of classes I can define in the .CSS file, and the user can theoretically nest deeper.
1866                         // The right solution is to use style=Xem, and calculate X.  Let's see if anyone complains
1867                         // first :)  Also applies to the row header case above.
1868                         // FIXED: I found it in one of my reports and changed it to the proposed method.
1869                         // This works for me (ipwizard)
1870                         ReportAccount rowname = (*it_innergroup).begin().key();
1871                         isUsed |= !rowname.isClosed();
1872                         finalRow = QString("<tr class=\"row-%1\"%2><td class=\"left\" style=\"text-indent: %3px;\">%5%6</td>")
1873                                        .arg(rownum & 0x01 ? "even" : "odd", m_config.detailLevel() == eMyMoney::Report::DetailLevel::All ? "id=\"solo\"" : "")
1874                                        .arg((rowname.hierarchyDepth() - 1) * 16)
1875                                        .arg(rowname.name().replace(whiteSpaceRegex, "&nbsp;").replace("<", "&lt;").replace(">", "&gt;"),
1876                                             (m_config.isConvertCurrency() || !rowname.isForeignCurrency()) ? QString()
1877                                                                                                            : QString(" (%1)").arg(rowname.currency().id()));
1878                     }
1879 
1880                     // Finish the row started above, unless told not to
1881                     if (finishrow) {
1882                         column = 0;
1883                         isUsed |= (*it_innergroup).m_total[eActual][0].isUsed();
1884                         while (column < m_numColumns) {
1885                             QString lb;
1886                             if (column != 0)
1887                                 lb = leftborder;
1888 
1889                             for (int i = 0; i < m_rowTypeList.size(); ++i) {
1890                                 finalRow +=
1891                                     QString("<td%2>%1</td>")
1892                                         .arg(coloredAmount((*it_innergroup).m_total[m_rowTypeList[i]][column], QString(), precision), i == 0 ? lb : QString());
1893                                 isUsed |= (*it_innergroup).m_total[ m_rowTypeList[i] ][column].isUsed();
1894                             }
1895 
1896                             column++;
1897                         }
1898 
1899                         if (m_config.isShowingRowTotals()) {
1900                             for (int i = 0; i < m_rowTypeList.size(); ++i) {
1901                                 finalRow += QString("<td%2>%1</td>")
1902                                                 .arg(coloredAmount((*it_innergroup).m_total[m_rowTypeList[i]].m_total, QString(), precision),
1903                                                      i == 0 ? leftborder : QString());
1904                             }
1905                         }
1906 
1907                         finalRow += "</tr>\n";
1908                         if (isUsed) {
1909                             result += finalRow;
1910                             ++rownum;
1911                         }
1912                     }
1913 
1914                     ++it_innergroup;
1915 
1916                 } // end while iterating on the inner groups
1917 
1918             } // end if detail level is not "group"
1919 
1920             //
1921             // Outer Row Group Totals
1922             //
1923 
1924             if (m_config.isConvertCurrency() && m_config.isShowingColumnTotals()) {
1925                 result += QString("<tr class=\"sectionfooter\"><td class=\"left\">%1&nbsp;%2</td>")
1926                               .arg(i18nc("Total balance", "Total"), (*it_outergroup).m_displayName);
1927                 column = 0;
1928                 while (column < m_numColumns) {
1929                     QString lb;
1930                     if (column != 0)
1931                         lb = leftborder;
1932 
1933                     for (int i = 0; i < m_rowTypeList.size(); ++i) {
1934                         result += QString("<td%2>%1</td>")
1935                                       .arg(coloredAmount((*it_outergroup).m_total[m_rowTypeList[i]][column], QString(), precision), i == 0 ? lb : QString());
1936                     }
1937 
1938                     column++;
1939                 }
1940 
1941                 if (m_config.isShowingRowTotals()) {
1942                     for (int i = 0; i < m_rowTypeList.size(); ++i) {
1943                         result +=
1944                             QString("<td%2>%1</td>")
1945                                 .arg(coloredAmount((*it_outergroup).m_total[m_rowTypeList[i]].m_total, QString(), precision), i == 0 ? leftborder : QString());
1946                     }
1947                 }
1948                 result += "</tr>\n";
1949             }
1950 
1951             ++it_outergroup;
1952 
1953         } // end while iterating on the outergroups
1954 
1955     } // end if detail level is not "total"
1956 
1957     //
1958     // Report Totals
1959     //
1960 
1961     if (m_config.isConvertCurrency() && m_config.isShowingColumnTotals()) {
1962         result += QString("<tr class=\"spacer\"><td>&nbsp;</td></tr>\n");
1963         result += QString("<tr class=\"reportfooter\"><td class=\"left\">%1</td>").arg(i18n("Grand Total"));
1964         auto totalcolumn = 0;
1965         while (totalcolumn < m_numColumns) {
1966             QString lb;
1967             if (totalcolumn != 0)
1968                 lb = leftborder;
1969 
1970             for (int i = 0; i < m_rowTypeList.size(); ++i) {
1971                 result +=
1972                     QString("<td%2>%1</td>").arg(coloredAmount(m_grid.m_total[m_rowTypeList[i]][totalcolumn], QString(), precision), i == 0 ? lb : QString());
1973             }
1974 
1975             totalcolumn++;
1976         }
1977 
1978         if (m_config.isShowingRowTotals()) {
1979             for (int i = 0; i < m_rowTypeList.size(); ++i) {
1980                 result += QString("<td%2>%1</td>")
1981                               .arg(coloredAmount(m_grid.m_total[m_rowTypeList[i]].m_total, QString(), precision), i == 0 ? leftborder : QString());
1982             }
1983         }
1984 
1985         result += "</tr>\n";
1986     }
1987     result += "</table>\n";
1988     return result;
1989 }
1990 
1991 void PivotTable::dump(const QString& file, const QString& /* context */) const
1992 {
1993     QFile g(file);
1994     g.open(QIODevice::WriteOnly);
1995     QTextStream(&g) << renderHTML();
1996     g.close();
1997 }
1998 
1999 void PivotTable::drawChart(KReportChartView& chartView) const
2000 {
2001     chartView.drawPivotChart(m_grid, m_config, m_numColumns, m_columnHeadings, m_rowTypeList, m_columnTypeHeaderList);
2002 }
2003 
2004 QString PivotTable::coloredAmount(const MyMoneyMoney& amount, const QString& currencySymbol, int prec) const
2005 {
2006     const auto value = amount.formatMoney(currencySymbol, prec);
2007     if (amount.isNegative())
2008         return QString::fromLatin1("<font color=%1>%2</font>")
2009                .arg(KMyMoneySettings::schemeColor(SchemeColor::Negative).name(), value);
2010     else
2011         return value;
2012 }
2013 
2014 void PivotTable::calculateBudgetDiff()
2015 {
2016     PivotGrid::iterator it_outergroup = m_grid.begin();
2017     const auto propagateBudgetDifference = m_config.isPropagateBudgetDifference();
2018     while (it_outergroup != m_grid.end()) {
2019         PivotOuterGroup::iterator it_innergroup = (*it_outergroup).begin();
2020         while (it_innergroup != (*it_outergroup).end()) {
2021             PivotInnerGroup::iterator it_row = (*it_innergroup).begin();
2022             while (it_row != (*it_innergroup).end()) {
2023                 int column = m_startColumn;
2024                 switch (it_row.key().accountGroup()) {
2025                 case eMyMoney::Account::Type::Income:
2026                 case eMyMoney::Account::Type::Asset:
2027                     while (column < m_numColumns) {
2028                         it_row.value()[eBudgetDiff][column] = PivotCell(it_row.value()[eActual][column] - it_row.value()[eBudget][column]);
2029                         ++column;
2030                         if (propagateBudgetDifference && (column < m_numColumns)) {
2031                             it_row.value()[eBudget][column] -= it_row.value()[eBudgetDiff][column - 1];
2032                         }
2033                     }
2034                     break;
2035                 case eMyMoney::Account::Type::Expense:
2036                 case eMyMoney::Account::Type::Liability:
2037                     while (column < m_numColumns) {
2038                         it_row.value()[eBudgetDiff][column] = PivotCell(it_row.value()[eBudget][column] - it_row.value()[eActual][column]);
2039                         ++column;
2040                         if (propagateBudgetDifference && (column < m_numColumns)) {
2041                             it_row.value()[eBudget][column] += it_row.value()[eBudgetDiff][column - 1];
2042                         }
2043                     }
2044                     break;
2045                 default:
2046                     break;
2047                 }
2048                 ++it_row;
2049             }
2050             ++it_innergroup;
2051         }
2052         ++it_outergroup;
2053     }
2054 
2055 }
2056 
2057 void PivotTable::calculateForecast()
2058 {
2059     //setup forecast
2060     MyMoneyForecast forecast = KMyMoneyUtils::forecast();
2061 
2062     //since this is a net worth forecast we want to include all account even those that are not in use
2063     forecast.setIncludeUnusedAccounts(true);
2064 
2065     //setup forecast dates
2066     if (m_endDate > QDate::currentDate()) {
2067         forecast.setForecastEndDate(m_endDate);
2068         forecast.setForecastStartDate(QDate::currentDate());
2069         forecast.setForecastDays(QDate::currentDate().daysTo(m_endDate));
2070     } else {
2071         forecast.setForecastStartDate(m_beginDate);
2072         forecast.setForecastEndDate(m_endDate);
2073         forecast.setForecastDays(m_beginDate.daysTo(m_endDate) + 1);
2074     }
2075 
2076     //adjust history dates if beginning date is before today
2077     if (m_beginDate < QDate::currentDate()) {
2078         forecast.setHistoryEndDate(m_beginDate.addDays(-1));
2079         forecast.setHistoryStartDate(forecast.historyEndDate().addDays(-forecast.accountsCycle()*forecast.forecastCycles()));
2080     }
2081 
2082     //run forecast
2083     if (m_config.rowType() == eMyMoney::Report::RowType::AssetLiability) { //asset and liability
2084         forecast.doForecast();
2085     } else { //income and expenses
2086         MyMoneyBudget budget;
2087         forecast.createBudget(budget, m_beginDate.addYears(-1), m_beginDate.addDays(-1), m_beginDate, m_endDate, false);
2088     }
2089 
2090     // check if we need to copy the opening balances
2091     // the conditions might be too tight but those fix a reported
2092     // problem and should avoid side effects in other situations
2093     // see https://bugs.kde.org/show_bug.cgi?id=391961
2094     const bool copyOpeningBalances = (m_startColumn == 1) &&
2095                                      !m_config.isIncludingSchedules() &&
2096                                      (m_config.isRunningSum());
2097 
2098     //go through the data and add forecast
2099     PivotGrid::iterator it_outergroup = m_grid.begin();
2100     while (it_outergroup != m_grid.end()) {
2101         PivotOuterGroup::iterator it_innergroup = (*it_outergroup).begin();
2102         while (it_innergroup != (*it_outergroup).end()) {
2103             PivotInnerGroup::iterator it_row = (*it_innergroup).begin();
2104             while (it_row != (*it_innergroup).end()) {
2105                 int column = m_startColumn;
2106                 QDate forecastDate = m_beginDate;
2107                 // check whether the opening balance needs to be setup
2108                 if (copyOpeningBalances) {
2109                     if (it_row.key().accountGroup() == eMyMoney::Account::Type::Liability) {
2110                         it_row.value()[eForecast][0] -= it_row.value()[eActual][0];
2111                     } else {
2112                         it_row.value()[eForecast][0] += it_row.value()[eActual][0];
2113                     }
2114                     // multiply the shares with the price in case of
2115                     // an investment to obtain the value of it
2116                     if (it_row.key().isInvest()) {
2117                         it_row.value()[eForecast][0] *= it_row.key().deepCurrencyPrice(m_beginDate.addDays(-1));
2118                     }
2119                 }
2120                 //check whether columns are days or months
2121                 if (m_config.isColumnsAreDays()) {
2122                     while (column < m_numColumns) {
2123                         it_row.value()[eForecast][column] = PivotCell(forecast.forecastBalance(it_row.key(), forecastDate));
2124 
2125                         forecastDate = forecastDate.addDays(1);
2126                         ++column;
2127                     }
2128                 } else {
2129                     //if columns are months
2130                     while (column < m_numColumns) {
2131                         // the forecast balance is on the first day of the month see MyMoneyForecast::calculateScheduledMonthlyBalances()
2132                         forecastDate = QDate(forecastDate.year(), forecastDate.month(), 1);
2133                         //check that forecastDate is not over ending date
2134                         if (forecastDate > m_endDate)
2135                             forecastDate = m_endDate;
2136 
2137                         //get forecast balance and set the corresponding column
2138                         it_row.value()[eForecast][column] = PivotCell(forecast.forecastBalance(it_row.key(), forecastDate));
2139 
2140                         forecastDate = forecastDate.addMonths(1);
2141                         ++column;
2142                     }
2143                 }
2144                 ++it_row;
2145             }
2146             ++it_innergroup;
2147         }
2148         ++it_outergroup;
2149     }
2150 }
2151 
2152 void PivotTable::loadRowTypeList()
2153 {
2154     if ((m_config.isIncludingBudgetActuals()) ||
2155             (!m_config.hasBudget()
2156              && !m_config.isIncludingForecast()
2157              && !m_config.isIncludingMovingAverage()
2158              && !m_config.isIncludingPrice()
2159              && !m_config.isIncludingAveragePrice())
2160        ) {
2161         m_rowTypeList.append(eActual);
2162         m_columnTypeHeaderList.append(i18nc("@title:column report header", "Actual"));
2163     }
2164 
2165     if (m_config.hasBudget()) {
2166         m_rowTypeList.append(eBudget);
2167         m_columnTypeHeaderList.append(i18nc("@title:column report header", "Budget"));
2168     }
2169 
2170     if (m_config.isIncludingBudgetActuals()) {
2171         m_rowTypeList.append(eBudgetDiff);
2172         m_columnTypeHeaderList.append(i18nc("@title:column report header", "Difference"));
2173     }
2174 
2175     if (m_config.isIncludingForecast()) {
2176         m_rowTypeList.append(eForecast);
2177         m_columnTypeHeaderList.append(i18nc("@title:column report header", "Forecast"));
2178     }
2179 
2180     if (m_config.isIncludingMovingAverage()) {
2181         m_rowTypeList.append(eAverage);
2182         m_columnTypeHeaderList.append(i18nc("@title:column report header", "Moving Average"));
2183     }
2184 
2185     if (m_config.isIncludingAveragePrice()) {
2186         m_rowTypeList.append(eAverage);
2187         m_columnTypeHeaderList.append(i18nc("@title:column report header", "Moving Average Price"));
2188     }
2189 
2190     if (m_config.isIncludingPrice()) {
2191         m_rowTypeList.append(ePrice);
2192         m_columnTypeHeaderList.append(i18nc("@title:column report header", "Price"));
2193     }
2194 }
2195 
2196 
2197 void PivotTable::calculateMovingAverage()
2198 {
2199     int delta = m_config.movingAverageDays() / 2;
2200 
2201     //go through the data and add the moving average
2202     PivotGrid::iterator it_outergroup = m_grid.begin();
2203     while (it_outergroup != m_grid.end()) {
2204         PivotOuterGroup::iterator it_innergroup = (*it_outergroup).begin();
2205         while (it_innergroup != (*it_outergroup).end()) {
2206             PivotInnerGroup::iterator it_row = (*it_innergroup).begin();
2207             while (it_row != (*it_innergroup).end()) {
2208                 int column = m_startColumn;
2209 
2210                 //check whether columns are days or months
2211                 if (m_config.columnType() == eMyMoney::Report::ColumnType::Days) {
2212                     while (column < m_numColumns) {
2213                         MyMoneyMoney totalPrice = MyMoneyMoney();
2214 
2215                         QDate averageStart = columnDate(column).addDays(-delta);
2216                         QDate averageEnd = columnDate(column).addDays(delta);
2217                         for (QDate averageDate = averageStart; averageDate <= averageEnd; averageDate = averageDate.addDays(1)) {
2218                             if (m_config.isConvertCurrency()) {
2219                                 totalPrice += it_row.key().deepCurrencyPrice(averageDate) * it_row.key().baseCurrencyPrice(averageDate);
2220                             } else {
2221                                 totalPrice += it_row.key().deepCurrencyPrice(averageDate);
2222                             }
2223                             totalPrice = totalPrice.convert(10000);
2224                         }
2225 
2226                         //calculate the average price
2227                         MyMoneyMoney averagePrice = totalPrice / MyMoneyMoney((averageStart.daysTo(averageEnd) + 1), 1);
2228 
2229                         //get the actual value, multiply by the average price and save that value
2230                         MyMoneyMoney averageValue = it_row.value()[eActual][column] * averagePrice;
2231                         it_row.value()[eAverage][column] = PivotCell(averageValue.convert(10000));
2232 
2233                         ++column;
2234                     }
2235                 } else {
2236                     //if columns are months
2237                     while (column < m_numColumns) {
2238                         QDate averageStart = columnDate(column);
2239 
2240                         //set the right start date depending on the column type
2241                         switch (m_config.columnType()) {
2242                         case eMyMoney::Report::ColumnType::Years: {
2243                             averageStart = QDate(columnDate(column).year(), 1, 1);
2244                             break;
2245                         }
2246                         case eMyMoney::Report::ColumnType::BiMonths: {
2247                             averageStart = QDate(columnDate(column).year(), columnDate(column).month(), 1).addMonths(-1);
2248                             break;
2249                         }
2250                         case eMyMoney::Report::ColumnType::Quarters: {
2251                             averageStart = QDate(columnDate(column).year(), columnDate(column).month(), 1).addMonths(-1);
2252                             break;
2253                         }
2254                         case eMyMoney::Report::ColumnType::Months: {
2255                             averageStart = QDate(columnDate(column).year(), columnDate(column).month(), 1);
2256                             break;
2257                         }
2258                         case eMyMoney::Report::ColumnType::Weeks: {
2259                             averageStart = columnDate(column).addDays(-columnDate(column).dayOfWeek() + 1);
2260                             break;
2261                         }
2262                         default:
2263                             break;
2264                         }
2265 
2266                         //gather the actual data and calculate the average
2267                         MyMoneyMoney totalPrice = MyMoneyMoney();
2268                         QDate averageEnd = columnDate(column);
2269                         for (QDate averageDate = averageStart; averageDate <= averageEnd; averageDate = averageDate.addDays(1)) {
2270                             if (m_config.isConvertCurrency()) {
2271                                 totalPrice += it_row.key().deepCurrencyPrice(averageDate) * it_row.key().baseCurrencyPrice(averageDate);
2272                             } else {
2273                                 totalPrice += it_row.key().deepCurrencyPrice(averageDate);
2274                             }
2275                             totalPrice = totalPrice.convert(10000);
2276                         }
2277 
2278                         MyMoneyMoney averagePrice = totalPrice / MyMoneyMoney((averageStart.daysTo(averageEnd) + 1), 1);
2279                         MyMoneyMoney averageValue = it_row.value()[eActual][column] * averagePrice;
2280 
2281                         //fill in the average
2282                         it_row.value()[eAverage][column] = PivotCell(averageValue.convert(10000));
2283 
2284                         ++column;
2285                     }
2286                 }
2287                 ++it_row;
2288             }
2289             ++it_innergroup;
2290         }
2291         ++it_outergroup;
2292     }
2293 }
2294 
2295 void PivotTable::fillBasePriceUnit(ERowType rowType)
2296 {
2297     MyMoneyFile* file = MyMoneyFile::instance();
2298     QString baseCurrencyId = file->baseCurrency().id();
2299 
2300     //get the first price date for securities
2301     QMap<QString, QDate> securityDates = securityFirstPrice();
2302 
2303     //go through the data
2304     PivotGrid::iterator it_outergroup = m_grid.begin();
2305     while (it_outergroup != m_grid.end()) {
2306         PivotOuterGroup::iterator it_innergroup = (*it_outergroup).begin();
2307         while (it_innergroup != (*it_outergroup).end()) {
2308             PivotInnerGroup::iterator it_row = (*it_innergroup).begin();
2309             while (it_row != (*it_innergroup).end()) {
2310                 int column = m_startColumn;
2311 
2312                 //if it is a base currency fill all the values
2313                 bool firstPriceExists = false;
2314                 if (it_row.key().currencyId() == baseCurrencyId) {
2315                     firstPriceExists = true;
2316                 }
2317 
2318                 while (column < m_numColumns) {
2319                     //check whether the date for that column is on or after the first price
2320                     if (!firstPriceExists
2321                             && securityDates.contains(it_row.key().currencyId())
2322                             && columnDate(column) >= securityDates.value(it_row.key().currencyId())) {
2323                         firstPriceExists = true;
2324                     }
2325 
2326                     //only add the dummy value if there is a price for that date
2327                     if (firstPriceExists) {
2328                         //insert a unit of currency for each account
2329                         it_row.value()[rowType][column] = PivotCell(MyMoneyMoney::ONE);
2330                     }
2331                     ++column;
2332                 }
2333                 ++it_row;
2334             }
2335             ++it_innergroup;
2336         }
2337         ++it_outergroup;
2338     }
2339 }
2340 
2341 QMap<QString, QDate> PivotTable::securityFirstPrice()
2342 {
2343     MyMoneyFile* file = MyMoneyFile::instance();
2344     MyMoneyPriceList priceList = file->priceList();
2345     QMap<QString, QDate> securityPriceDate;
2346 
2347     MyMoneyPriceList::const_iterator prices_it;
2348     for (prices_it = priceList.constBegin(); prices_it != priceList.constEnd(); ++prices_it) {
2349         MyMoneyPrice firstPrice = (*((*prices_it).constBegin()));
2350 
2351         //check the security in the from field
2352         //if it is there, check if it is older
2353         if (securityPriceDate.contains(firstPrice.from())) {
2354             if (securityPriceDate.value(firstPrice.from()) > firstPrice.date()) {
2355                 securityPriceDate[firstPrice.from()] = firstPrice.date();
2356             }
2357         } else {
2358             securityPriceDate.insert(firstPrice.from(), firstPrice.date());
2359         }
2360 
2361         //check the security in the to field
2362         //if it is there, check if it is older
2363         if (securityPriceDate.contains(firstPrice.to())) {
2364             if (securityPriceDate.value(firstPrice.to()) > firstPrice.date()) {
2365                 securityPriceDate[firstPrice.to()] = firstPrice.date();
2366             }
2367         } else {
2368             securityPriceDate.insert(firstPrice.to(), firstPrice.date());
2369         }
2370     }
2371     return securityPriceDate;
2372 }
2373 
2374 void PivotTable::includeInvestmentSubAccounts()
2375 {
2376     // if we're not in expert mode, we need to make sure
2377     // that all stock accounts for the selected investment
2378     // account are also selected
2379     QStringList accountList;
2380     if (m_config.accounts(accountList)) {
2381         if (!KMyMoneySettings::expertMode()) {
2382             for (const auto& sAccount : qAsConst(accountList)) {
2383                 auto acc = MyMoneyFile::instance()->account(sAccount);
2384                 if (acc.accountType() == eMyMoney::Account::Type::Investment) {
2385                     const auto subAccounts = acc.accountList();
2386                     for (const auto& sSubAccount : qAsConst(subAccounts)) {
2387                         if (!accountList.contains(sSubAccount)) {
2388                             m_config.addAccount(sSubAccount);
2389                         }
2390                     }
2391                 }
2392             }
2393         }
2394     }
2395 }
2396 
2397 int PivotTable::currentDateColumn()
2398 {
2399 
2400     //return -1 if the columns do not include the current date
2401     if (m_beginDate > QDate::currentDate() || m_endDate < QDate::currentDate()) {
2402         return -1;
2403     }
2404 
2405     //check the date of each column and return if it is the one for the current date
2406     //if columns are not days, return the one for the current month or year
2407     int column = m_startColumn;
2408 
2409     while (column < m_numColumns) {
2410         if (columnDate(column) >= QDate::currentDate()) {
2411             break;
2412         }
2413         column++;
2414     }
2415 
2416     //if there is no column matching the current date, return -1
2417     if (column == m_numColumns) {
2418         column = -1;
2419     }
2420     return column;
2421 }
2422 
2423 bool reports::PivotTable::startDateIsFiscalYearStart() const
2424 {
2425     return (QDate(2021, m_beginDate.month(), m_beginDate.day()) == QDate(2021, KMyMoneySettings::firstFiscalMonth(), KMyMoneySettings::firstFiscalDay()));
2426 }
2427 
2428 bool reports::PivotTable::fiscalYearIsCalendarYear() const
2429 {
2430     return (QDate(2021, 1, 1) == QDate(2021, KMyMoneySettings::firstFiscalMonth(), KMyMoneySettings::firstFiscalDay()));
2431 }
2432 
2433 } // namespace