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