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 %2 - %3 %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, " ").replace("<", "<").replace(">", ">"), 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\"> %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, " ").replace("<", "<").replace(">", ">"), 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 %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> </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