diff --git a/kmymoney/reports/kreportchartview.cpp b/kmymoney/reports/kreportchartview.cpp index adda62fbe..efceeb288 100644 --- a/kmymoney/reports/kreportchartview.cpp +++ b/kmymoney/reports/kreportchartview.cpp @@ -1,655 +1,655 @@ /*************************************************************************** kreportchartview.cpp ------------------- begin : Sun Aug 14 2005 copyright : (C) 2004-2005 by Ace Jones email : (C) 2017 Łukasz Wojniłowicz ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "kreportchartview.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include // ---------------------------------------------------------------------------- // Project Includes #include #include #include #include #include #include #include #include #include #include #include #include #include "kmymoneyglobalsettings.h" #include #include using namespace reports; KReportChartView::KReportChartView(QWidget* parent) : KChart::Chart(parent), m_accountSeries(0), m_seriesTotals(0), m_numColumns(0), m_skipZero(0), m_backgroundBrush(KColorScheme(QPalette::Current).background()), m_foregroundBrush(KColorScheme(QPalette::Current).foreground()) { // ******************************************************************** // Set KMyMoney's Chart Parameter Defaults // ******************************************************************** //Set the background obtained from the color scheme BackgroundAttributes backAttr(backgroundAttributes()); backAttr.setBrush(m_backgroundBrush); backAttr.setVisible(true); setBackgroundAttributes(backAttr); //Line diagram KChart::LineDiagram* diagram = new KChart::LineDiagram; diagram->setModel(&m_model); this->coordinatePlane()->replaceDiagram(diagram); } void KReportChartView::drawPivotChart(const PivotGrid &grid, const MyMoneyReport &config, int numberColumns, const QStringList& columnHeadings, const QList& rowTypeList, const QStringList& columnTypeHeaderList) { //set the number of columns setNumColumns(numberColumns); //set skipZero m_skipZero = config.isSkippingZero(); //remove existing headers while (headerFooters().count() > 0) { HeaderFooter* delHeader = headerFooters().at(0); takeHeaderFooter(delHeader); delete delHeader; } //make sure the model is clear m_model.removeColumns(0, m_model.columnCount()); m_model.removeRows(0, m_model.rowCount()); //set the new header HeaderFooter* header = new HeaderFooter(this); header->setText(config.name()); header->setType(HeaderFooter::Header); header->setPosition(Position::North); TextAttributes headerTextAttr(header->textAttributes()); headerTextAttr.setPen(m_foregroundBrush.color()); header->setTextAttributes(headerTextAttr); addHeaderFooter(header); // whether to limit the chart to use series totals only. Used for reports which only // show one dimension (pie). setSeriesTotals(false); // whether series (rows) are accounts (true) or months (false). This causes a lot // of complexity in the charts. The problem is that circular reports work best with // an account in a COLUMN, while line/bar prefer it in a ROW. setAccountSeries(true); switch (config.chartType()) { case MyMoneyReport::eChartNone: case MyMoneyReport::eChartEnd: case MyMoneyReport::eChartLine: { KChart::LineDiagram* diagram = new KChart::LineDiagram; if (config.isSkippingZero()) { LineAttributes attributes = diagram->lineAttributes(); attributes.setMissingValuesPolicy(LineAttributes::MissingValuesAreBridged); diagram->setLineAttributes(attributes); } CartesianCoordinatePlane* cartesianPlane = new CartesianCoordinatePlane; replaceCoordinatePlane(cartesianPlane); coordinatePlane()->replaceDiagram(diagram); break; } case MyMoneyReport::eChartBar: { KChart::BarDiagram* diagram = new KChart::BarDiagram; CartesianCoordinatePlane* cartesianPlane = new CartesianCoordinatePlane; replaceCoordinatePlane(cartesianPlane); coordinatePlane()->replaceDiagram(diagram); break; } case MyMoneyReport::eChartStackedBar: { KChart::BarDiagram* diagram = new KChart::BarDiagram; CartesianCoordinatePlane* cartesianPlane = new CartesianCoordinatePlane; replaceCoordinatePlane(cartesianPlane); diagram->setType(BarDiagram::Stacked); coordinatePlane()->replaceDiagram(diagram); break; } case MyMoneyReport::eChartPie: { KChart::PieDiagram* diagram = new KChart::PieDiagram; PolarCoordinatePlane* polarPlane = new PolarCoordinatePlane; replaceCoordinatePlane(polarPlane); coordinatePlane()->replaceDiagram(diagram); setAccountSeries(false); setSeriesTotals(true); break; } case MyMoneyReport::eChartRing: { KChart::RingDiagram* diagram = new KChart::RingDiagram; PolarCoordinatePlane* polarPlane = new PolarCoordinatePlane; replaceCoordinatePlane(polarPlane); polarPlane->replaceDiagram(diagram); //chartView.params()->setRelativeRingThickness( true ); setAccountSeries(false); break; } } //get the diagram for later use AbstractDiagram* planeDiagram = coordinatePlane()->diagram(); //set grid attributes GridAttributes gridAttr(coordinatePlane()->globalGridAttributes()); gridAttr.setGridVisible(config.isChartGridLines()); coordinatePlane()->setGlobalGridAttributes(gridAttr); //the palette - we set it here because it is a property of the diagram switch (KMyMoneySettings::chartsPalette()) { case 0: planeDiagram->useDefaultColors(); break; case 1: planeDiagram->useRainbowColors(); break; case 2: default: planeDiagram->useSubduedColors(); break; } //the legend will be used later Legend* legend = new Legend(planeDiagram, this); legend->setTitleText(i18nc("Chart legend title", "Legend")); //set up the axes for cartesian diagrams if (config.chartType() == MyMoneyReport::eChartLine || config.chartType() == MyMoneyReport::eChartBar || config.chartType() == MyMoneyReport::eChartStackedBar) { //set x axis CartesianAxis *xAxis = new CartesianAxis(); xAxis->setPosition(CartesianAxis::Bottom); xAxis->setTitleText(i18n("Time")); TextAttributes xAxisTitleTextAttr(xAxis->titleTextAttributes()); xAxisTitleTextAttr.setMinimalFontSize(QFontDatabase::systemFont(QFontDatabase::GeneralFont).pointSize()); xAxisTitleTextAttr.setPen(m_foregroundBrush.color()); xAxis->setTitleTextAttributes(xAxisTitleTextAttr); TextAttributes xAxisTextAttr(xAxis->textAttributes()); xAxisTextAttr.setPen(m_foregroundBrush.color()); xAxis->setTextAttributes(xAxisTextAttr); RulerAttributes xAxisRulerAttr(xAxis->rulerAttributes()); xAxisRulerAttr.setTickMarkPen(m_foregroundBrush.color()); xAxisRulerAttr.setShowRulerLine(true); xAxis->setRulerAttributes(xAxisRulerAttr); //set y axis KBalanceAxis *yAxis = new KBalanceAxis(); yAxis->setPosition(CartesianAxis::Left); // TODO // if the chart shows prices and no balance // the axis title should be 'Price' if (config.isIncludingPrice()) { yAxis->setTitleText(i18n("Price")); } else { yAxis->setTitleText(i18n("Balance")); } TextAttributes yAxisTitleTextAttr(yAxis->titleTextAttributes()); yAxisTitleTextAttr.setMinimalFontSize(QFontDatabase::systemFont(QFontDatabase::GeneralFont).pointSize()); yAxisTitleTextAttr.setPen(m_foregroundBrush.color()); yAxis->setTitleTextAttributes(yAxisTitleTextAttr); TextAttributes yAxisTextAttr(yAxis->textAttributes()); yAxisTextAttr.setPen(m_foregroundBrush.color()); yAxis->setTextAttributes(yAxisTextAttr); RulerAttributes yAxisRulerAttr(yAxis->rulerAttributes()); yAxisRulerAttr.setTickMarkPen(m_foregroundBrush.color()); yAxisRulerAttr.setShowRulerLine(true); yAxis->setRulerAttributes(yAxisRulerAttr); //add the axes to the corresponding diagram if (config.chartType() == MyMoneyReport::eChartLine) { KChart::LineDiagram* lineDiagram = qobject_cast(planeDiagram); lineDiagram->addAxis(xAxis); lineDiagram->addAxis(yAxis); } else if (config.chartType() == MyMoneyReport::eChartBar || config.chartType() == MyMoneyReport::eChartStackedBar) { KChart::BarDiagram* barDiagram = qobject_cast(planeDiagram); barDiagram->addAxis(xAxis); barDiagram->addAxis(yAxis); } } const bool blocked = m_model.blockSignals(true); // don't emit dataChanged() signal during each drawPivotRowSet switch (config.detailLevel()) { case MyMoneyReport::eDetailNone: case MyMoneyReport::eDetailEnd: case MyMoneyReport::eDetailAll: { int rowNum = 0; // iterate over outer groups PivotGrid::const_iterator it_outergroup = grid.begin(); while (it_outergroup != grid.end()) { // iterate over inner groups PivotOuterGroup::const_iterator it_innergroup = (*it_outergroup).begin(); while (it_innergroup != (*it_outergroup).end()) { // // Rows // PivotInnerGroup::const_iterator it_row = (*it_innergroup).begin(); while (it_row != (*it_innergroup).end()) { //Do not include investments accounts in the chart because they are merely container of stock and other accounts if (it_row.key().accountType() != MyMoneyAccount::Investment) { //iterate row types for (int i = 0; i < rowTypeList.size(); ++i) { //skip the budget difference rowset if (rowTypeList[i] != eBudgetDiff) { QString legendText; //only show the column type in the header if there is more than one type if (rowTypeList.size() > 1) { legendText = QString(columnTypeHeaderList[i] + " - " + it_row.key().name()); } else { legendText = QString(it_row.key().name()); } //set the legend text legend->setText(rowNum, legendText); //clear to speed up drawPivotRowSet if (!config.isChartDataLabels()) legendText.clear(); //set the cell value and tooltip rowNum = drawPivotRowSet(rowNum, it_row.value(), rowTypeList[i], legendText, 1, numColumns() - 1); } } } ++it_row; } ++it_innergroup; } ++it_outergroup; } } break; case MyMoneyReport::eDetailTop: { int rowNum = 0; // iterate over outer groups PivotGrid::const_iterator it_outergroup = grid.begin(); while (it_outergroup != grid.end()) { // iterate over inner groups PivotOuterGroup::const_iterator it_innergroup = (*it_outergroup).begin(); while (it_innergroup != (*it_outergroup).end()) { //iterate row types for (int i = 0; i < rowTypeList.size(); ++i) { //skip the budget difference rowset if (rowTypeList[i] != eBudgetDiff) { QString legendText; //only show the column type in the header if there is more than one type if (rowTypeList.size() > 1) { legendText = QString(columnTypeHeaderList[i] + " - " + it_innergroup.key()); } else { legendText = QString(it_innergroup.key()); } //set the legend text legend->setText(rowNum, legendText); //clear to speed up drawPivotRowSet if (!config.isChartDataLabels()) legendText.clear(); //set the cell value and tooltip rowNum = drawPivotRowSet(rowNum, (*it_innergroup).m_total, rowTypeList[i], legendText, 1, numColumns() - 1); } } ++it_innergroup; } ++it_outergroup; } } break; case MyMoneyReport::eDetailGroup: { int rowNum = 0; // iterate over outer groups PivotGrid::const_iterator it_outergroup = grid.begin(); while (it_outergroup != grid.end()) { //iterate row types for (int i = 0; i < rowTypeList.size(); ++i) { //skip the budget difference rowset if (rowTypeList[i] != eBudgetDiff) { QString legendText; //only show the column type in the header if there is more than one type if (rowTypeList.size() > 1) { legendText = QString(columnTypeHeaderList[i] + " - " + it_outergroup.key()); } else { legendText = QString(it_outergroup.key()); } //set the legend text legend->setText(rowNum, legendText); //clear to speed up drawPivotRowSet if (!config.isChartDataLabels()) legendText.clear(); //set the cell value and tooltip rowNum = drawPivotRowSet(rowNum, (*it_outergroup).m_total, rowTypeList[i], legendText, 1, numColumns() - 1); } } ++it_outergroup; } //if selected, show totals too if (config.isShowingRowTotals()) { //iterate row types for (int i = 0; i < rowTypeList.size(); ++i) { //skip the budget difference rowset if (rowTypeList[i] != eBudgetDiff) { QString legendText; //only show the column type in the header if there is more than one type if (rowTypeList.size() > 1) { legendText = QString(columnTypeHeaderList[i] + " - " + i18nc("Total balance", "Total")); } else { legendText = QString(i18nc("Total balance", "Total")); } //set the legend text legend->setText(rowNum, legendText); //clear to speed up drawPivotRowSet if (!config.isChartDataLabels()) legendText.clear(); //set the cell value rowNum = drawPivotRowSet(rowNum, grid.m_total, rowTypeList[i], legendText, 1, numColumns() - 1); } } } } break; case MyMoneyReport::eDetailTotal: { int rowNum = 0; //iterate row types for (int i = 0; i < rowTypeList.size(); ++i) { //skip the budget difference rowset if (rowTypeList[i] != eBudgetDiff) { QString legendText; //only show the column type in the header if there is more than one type if (rowTypeList.size() > 1) { legendText = QString(columnTypeHeaderList[i] + " - " + i18nc("Total balance", "Total")); } else { legendText = QString(i18nc("Total balance", "Total")); } //set the legend legend->setText(rowNum, legendText); //clear to speed up drawPivotRowSet if (!config.isChartDataLabels()) legendText.clear(); if (config.isMixedTime() && (rowTypeList[i] == eActual || rowTypeList[i] == eForecast)) { if (rowTypeList[i] == eActual) { rowNum = drawPivotRowSet(rowNum, grid.m_total, rowTypeList[i], legendText, 1, config.currentDateColumn()); } else if (rowTypeList[i] == eForecast) { rowNum = drawPivotRowSet(rowNum, grid.m_total, rowTypeList[i], legendText, config.currentDateColumn(), numColumns() - 1); } else { rowNum = drawPivotRowSet(rowNum, grid.m_total, rowTypeList[i], legendText, 1, numColumns() - 1); } } else { //set cell value rowNum = drawPivotRowSet(rowNum, grid.m_total, rowTypeList[i], legendText, 1, numColumns() - 1); } } } } break; } m_model.blockSignals(blocked); // reenable dataChanged() signal if (config.chartType() == MyMoneyReport::eChartLine || config.chartType() == MyMoneyReport::eChartBar || config.chartType() == MyMoneyReport::eChartStackedBar) { // Set up X axis labels (ie "abscissa" to use the technical term) QStringList abscissaNames; if (accountSeries()) { // if not, we will set these up while putting in the chart values. int column = 1; while (column < numColumns()) { abscissaNames += QString(columnHeadings[column++]).replace(" ", " "); } qDebug() << "rows: " << m_model.rowCount() << "columns: " << m_model.columnCount(); m_model.setVerticalHeaderLabels(abscissaNames); } } //assign model to the diagram planeDiagram->setModel(&m_model); //set the legend basic attributes //this is done after adding the legend because the values are overridden when adding the legend to the chart for (uint i = static_cast(KMyMoneyGlobalSettings::maximumLegendItems()); i < legend->datasetCount(); ++i) { legend->setDatasetHidden(i, true); } legend->setTitleText(i18nc("Chart lines legend", "Legend")); legend->setUseAutomaticMarkerSize(false); FrameAttributes legendFrameAttr(legend->frameAttributes()); legendFrameAttr.setPen(m_foregroundBrush.color()); // leave some space between the content and the frame legendFrameAttr.setPadding(2); legend->setFrameAttributes(legendFrameAttr); legend->setPosition(Position::East); legend->setTextAlignment(Qt::AlignLeft); legend->setLegendStyle(KChart::Legend::MarkersAndLines); replaceLegend(legend); // set the text attributes after calling replaceLegend() otherwise fon sizes will get overwritten qreal generalFontSize = QFontDatabase::systemFont(QFontDatabase::GeneralFont).pointSizeF(); if (generalFontSize == -1) generalFontSize = 8; // this is a fallback if the fontsize was specified in pixels TextAttributes legendTextAttr(legend->textAttributes()); legendTextAttr.setPen(m_foregroundBrush.color()); legendTextAttr.setFontSize(KChart::Measure(generalFontSize, KChartEnums::MeasureCalculationModeAbsolute)); legend->setTextAttributes(legendTextAttr); TextAttributes legendTitleTextAttr(legend->titleTextAttributes()); legendTitleTextAttr.setPen(m_foregroundBrush.color()); legendTitleTextAttr.setFontSize(KChart::Measure(generalFontSize + 4, KChartEnums::MeasureCalculationModeAbsolute)); legend->setTitleTextAttributes(legendTitleTextAttr); //this sets the line width only for line diagrams setLineWidth(config.chartLineWidth()); //set data value attributes //make sure to show only the required number of fractional digits on the labels of the graph DataValueAttributes dataValueAttr(planeDiagram->dataValueAttributes()); MarkerAttributes markerAttr(dataValueAttr.markerAttributes()); markerAttr.setVisible(true); markerAttr.setMarkerStyle(MarkerAttributes::MarkerCircle); markerAttr.setMarkerSize(QSize(8, 8)); dataValueAttr.setMarkerAttributes(markerAttr); TextAttributes dataValueTextAttr(dataValueAttr.textAttributes()); dataValueTextAttr.setPen(m_foregroundBrush.color()); dataValueAttr.setTextAttributes(dataValueTextAttr); dataValueAttr.setVisible(config.isChartDataLabels()); - dataValueAttr.setDecimalDigits(MyMoneyMoney::denomToPrec(MyMoneyFile::instance()->baseCurrency().smallestAccountFraction())); + dataValueAttr.setDecimalDigits(KMyMoneyGlobalSettings::pricePrecision()); planeDiagram->setDataValueAttributes(dataValueAttr); planeDiagram->setAllowOverlappingDataValueTexts(true); if (qMin(static_cast(KMyMoneyGlobalSettings::maximumLegendItems()), legend->datasetCount()) < 2) { // the legend is needed only if at least two data sets are rendered removeLegend(); } } unsigned KReportChartView::drawPivotRowSet(int rowNum, const PivotGridRowSet& rowSet, const ERowType rowType, const QString& legendText, int startColumn, int endColumn) { if (coordinatePlane()->diagram()->datasetDimension() != 1) return ++rowNum; //if endColumn is invalid, make it the same as numColumns if (endColumn == 0) endColumn = numColumns(); double value; QString toolTip; if (( accountSeries() && !seriesTotals()) || (!accountSeries() && seriesTotals())) { justifyModelSize(endColumn > numColumns() - 1 ? endColumn + 1 : numColumns(), rowNum + 1); } else { justifyModelSize(rowNum + 1, endColumn > numColumns() - 1 ? endColumn + 1 : numColumns()); } // Columns if (seriesTotals()) { value = rowSet[rowType].m_total.toDouble(); //set the tooltip if (!legendText.isEmpty()) toolTip = QString("

%1

%2
") .arg(legendText) - .arg(value, 0, 'f', 2); + .arg(value, 0, 'g', KMyMoneyGlobalSettings::pricePrecision()); //set the cell value if (accountSeries()) this->setDataCell(rowNum, 0, value, toolTip); else this->setDataCell(0, rowNum, value, toolTip); } else { int column = startColumn; while (column <= endColumn && column < numColumns()) { //if zero and set to skip, increase column and continue with next value if (m_skipZero && rowSet[rowType][column].isZero()) { ++column; continue; } else { value = rowSet[rowType][column].toDouble(); if (!legendText.isEmpty()) toolTip = QString("

%1

%2
") .arg(legendText) - .arg(value, 0, 'f', 2); + .arg(value, 0, 'g', KMyMoneyGlobalSettings::pricePrecision()); if (accountSeries()) this->setDataCell(column, rowNum, value, toolTip); else this->setDataCell(rowNum, column, value, toolTip); } ++column; } } return ++rowNum; } void KReportChartView::setDataCell(int row, int column, const double value, QString tip) { QMap data; data.insert(Qt::DisplayRole, QVariant(value)); if (!tip.isEmpty()) data.insert(Qt::ToolTipRole, QVariant(tip)); const QModelIndex index = m_model.index(row, column); m_model.setItemData(index, data); } /** * Justifies the model, so that the given rows and columns fit into it. */ void KReportChartView::justifyModelSize(int rows, int columns) { const int currentRows = m_model.rowCount(); const int currentCols = m_model.columnCount(); if (currentCols < columns) if (! m_model.insertColumns(currentCols, columns - currentCols)) qDebug() << "justifyModelSize: could not increase model size."; if (currentRows < rows) if (! m_model.insertRows(currentRows, rows - currentRows)) qDebug() << "justifyModelSize: could not increase model size."; Q_ASSERT(m_model.rowCount() >= rows); Q_ASSERT(m_model.columnCount() >= columns); } void KReportChartView::setLineWidth(const int lineWidth) { if (qobject_cast(coordinatePlane()->diagram())) { LineDiagram* lineDiagram = qobject_cast(coordinatePlane()->diagram()); QList pens; pens = lineDiagram->datasetPens(); for (int i = 0; i < pens.count(); ++i) { pens[i].setWidth(lineWidth); lineDiagram->setPen(i, pens.at(i)); } } } void KReportChartView::drawLimitLine(const double limit) { if (coordinatePlane()->diagram()->datasetDimension() != 1) return; // temporarily disconnect the view from the model to aovid update of view on // emission of the dataChanged() signal for each call of setDataCell(). // This speeds up the runtime of drawLimitLine() by a factor of // approx. 60 on my box (1831ms vs. 31ms). AbstractDiagram* planeDiagram = coordinatePlane()->diagram(); planeDiagram->setModel(0); //we get the current number of rows and we add one after that int row = m_model.rowCount(); justifyModelSize(m_numColumns, row + 1); for (int col = 0; col < m_numColumns; ++col) { setDataCell(col, row, limit); } planeDiagram->setModel(&m_model); //TODO: add format to the line } void KReportChartView::removeLegend() { Legend* chartLegend = Chart::legend(); delete chartLegend; } diff --git a/kmymoney/reports/pivottable.cpp b/kmymoney/reports/pivottable.cpp index 86a166e32..833dc4503 100644 --- a/kmymoney/reports/pivottable.cpp +++ b/kmymoney/reports/pivottable.cpp @@ -1,2291 +1,2296 @@ /*************************************************************************** pivottable.cpp ------------------- begin : Mon May 17 2004 copyright : (C) 2004-2005 by Ace Jones email : Thomas Baumgart Alvaro Soliverez ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "pivottable.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include // ---------------------------------------------------------------------------- // Project Includes #include "pivotgrid.h" #include "reportdebug.h" #include "kreportchartview.h" #include "kmymoneyglobalsettings.h" #include "kmymoneyutils.h" #include "mymoneyforecast.h" #include namespace reports { using KChart::Widget; QString Debug::m_sTabs; bool Debug::m_sEnabled = DEBUG_ENABLED_BY_DEFAULT; QString Debug::m_sEnableKey; Debug::Debug(const QString& _name): m_methodName(_name), m_enabled(m_sEnabled) { if (!m_enabled && _name == m_sEnableKey) m_enabled = true; if (m_enabled) { qDebug("%s%s(): ENTER", qPrintable(m_sTabs), qPrintable(m_methodName)); m_sTabs.append("--"); } } Debug::~Debug() { if (m_enabled) { m_sTabs.remove(0, 2); qDebug("%s%s(): EXIT", qPrintable(m_sTabs), qPrintable(m_methodName)); if (m_methodName == m_sEnableKey) m_enabled = false; } } void Debug::output(const QString& _text) { if (m_enabled) qDebug("%s%s(): %s", qPrintable(m_sTabs), qPrintable(m_methodName), qPrintable(_text)); } PivotTable::PivotTable(const MyMoneyReport& _config_f): ReportTable(), m_runningSumsCalculated(false), m_config_f(_config_f) { init(); } void PivotTable::init() { DEBUG_ENTER(Q_FUNC_INFO); // // Initialize locals // MyMoneyFile* file = MyMoneyFile::instance(); // // Initialize member variables // //make sure we have all subaccounts of investment accounts includeInvestmentSubAccounts(); m_config_f.validDateRange(m_beginDate, m_endDate); // If we need to calculate running sums, it does not make sense // to show a row total column if (m_config_f.isRunningSum()) m_config_f.setShowingRowTotals(false); // if this is a months-based report if (! m_config_f.isColumnsAreDays()) { // strip out the 'days' component of the begin and end dates. // we're only using these variables to contain year and month. m_beginDate = QDate(m_beginDate.year(), m_beginDate.month(), 1); m_endDate = QDate(m_endDate.year(), m_endDate.month(), 1); } m_numColumns = columnValue(m_endDate) - columnValue(m_beginDate) + 2; //Load what types of row the report is going to show loadRowTypeList(); // // Initialize outer groups of the grid // if (m_config_f.rowType() == MyMoneyReport::eAssetLiability) { m_grid.insert(KMyMoneyUtils::accountTypeToString(MyMoneyAccount::Asset), PivotOuterGroup(m_numColumns)); m_grid.insert(KMyMoneyUtils::accountTypeToString(MyMoneyAccount::Liability), PivotOuterGroup(m_numColumns, PivotOuterGroup::m_kDefaultSortOrder, true /* inverted */)); } else { m_grid.insert(KMyMoneyUtils::accountTypeToString(MyMoneyAccount::Income), PivotOuterGroup(m_numColumns, PivotOuterGroup::m_kDefaultSortOrder - 2)); m_grid.insert(KMyMoneyUtils::accountTypeToString(MyMoneyAccount::Expense), PivotOuterGroup(m_numColumns, PivotOuterGroup::m_kDefaultSortOrder - 1, true /* inverted */)); // // Create rows for income/expense reports with all accounts included // if (m_config_f.isIncludingUnusedAccounts()) createAccountRows(); } // // Initialize grid totals // m_grid.m_total = PivotGridRowSet(m_numColumns); // // Get opening balances // (for running sum reports only) // if (m_config_f.isRunningSum()) calculateOpeningBalances(); // // Calculate budget mapping // (for budget reports only) // if (m_config_f.hasBudget()) calculateBudgetMapping(); // // Populate all transactions into the row/column pivot grid // QList transactions; m_config_f.setReportAllSplits(false); m_config_f.setConsiderCategory(true); try { transactions = file->transactionList(m_config_f); } catch (const MyMoneyException &e) { qDebug("ERR: %s thrown in %s(%ld)", qPrintable(e.what()), qPrintable(e.file()), e.line()); throw e; } DEBUG_OUTPUT(QString("Found %1 matching transactions").arg(transactions.count())); // Include scheduled transactions if required if (m_config_f.isIncludingSchedules()) { // Create a custom version of the report filter, excluding date // We'll use this to compare the transaction against MyMoneyTransactionFilter schedulefilter(m_config_f); schedulefilter.setDateFilter(QDate(), QDate()); // Get the real dates from the config filter QDate configbegin, configend; m_config_f.validDateRange(configbegin, configend); QList schedules = file->scheduleList(); QList::const_iterator it_schedule = schedules.constBegin(); while (it_schedule != schedules.constEnd()) { // If the transaction meets the filter MyMoneyTransaction tx = (*it_schedule).transaction(); if (!(*it_schedule).isFinished() && schedulefilter.match(tx)) { // Keep the id of the schedule with the transaction so that // we can do the autocalc later on in case of a loan payment tx.setValue("kmm-schedule-id", (*it_schedule).id()); // Get the dates when a payment will be made within the report window QDate nextpayment = (*it_schedule).adjustedNextPayment(configbegin); if (nextpayment.isValid()) { // Add one transaction for each date QList paymentDates = (*it_schedule).paymentDates(nextpayment, configend); QList::const_iterator it_date = paymentDates.constBegin(); while (it_date != paymentDates.constEnd()) { //if the payment occurs in the past, enter it tomorrow if (QDate::currentDate() >= *it_date) { tx.setPostDate(QDate::currentDate().addDays(1)); } else { tx.setPostDate(*it_date); } if (tx.postDate() <= configend && tx.postDate() >= configbegin) { transactions += tx; } DEBUG_OUTPUT(QString("Added transaction for schedule %1 on %2").arg((*it_schedule).id()).arg((*it_date).toString())); ++it_date; } } } ++it_schedule; } } // whether asset & liability transactions are actually to be considered // transfers bool al_transfers = (m_config_f.rowType() == MyMoneyReport::eExpenseIncome) && (m_config_f.isIncludingTransfers()); //this is to store balance for loan accounts when not included in the report QMap loanBalances; QList::const_iterator it_transaction = transactions.constBegin(); int colofs = columnValue(m_beginDate) - 1; while (it_transaction != transactions.constEnd()) { MyMoneyTransaction tx = (*it_transaction); QDate postdate = tx.postDate(); if (postdate < m_beginDate) { qDebug("MyMoneyFile::transactionList returned a transaction that is outside the date filter, skipping it"); ++it_transaction; continue; } int column = columnValue(postdate) - colofs; // check if we need to call the autocalculation routine if (tx.isLoanPayment() && tx.hasAutoCalcSplit() && (tx.value("kmm-schedule-id").length() > 0)) { // make sure to consider any autocalculation for loan payments MyMoneySchedule sched = file->schedule(tx.value("kmm-schedule-id")); const MyMoneySplit& split = tx.amortizationSplit(); if (!split.id().isEmpty()) { ReportAccount splitAccount = file->account(split.accountId()); MyMoneyAccount::accountTypeE type = splitAccount.accountGroup(); QString outergroup = KMyMoneyUtils::accountTypeToString(type); //if the account is included in the report, calculate the balance from the cells if (m_config_f.includes(splitAccount)) { loanBalances[splitAccount.id()] = cellBalance(outergroup, splitAccount, column, false); } else { //if it is not in the report and also not in loanBalances, get the balance from the file if (!loanBalances.contains(splitAccount.id())) { QDate dueDate = sched.nextDueDate(); //if the payment is overdue, use current date if (dueDate < QDate::currentDate()) dueDate = QDate::currentDate(); //get the balance from the file for the date loanBalances[splitAccount.id()] = file->balance(splitAccount.id(), dueDate.addDays(-1)); } } KMyMoneyUtils::calculateAutoLoan(sched, tx, loanBalances); //if the loan split is not included in the report, update the balance for the next occurrence if (!m_config_f.includes(splitAccount)) { QList::ConstIterator it_loanSplits; for (it_loanSplits = tx.splits().constBegin(); it_loanSplits != tx.splits().constEnd(); ++it_loanSplits) { if ((*it_loanSplits).isAmortizationSplit() && (*it_loanSplits).accountId() == splitAccount.id()) loanBalances[splitAccount.id()] = loanBalances[splitAccount.id()] + (*it_loanSplits).shares(); } } } } QList splits = tx.splits(); QList::const_iterator it_split = splits.constBegin(); while (it_split != splits.constEnd()) { ReportAccount splitAccount = (*it_split).accountId(); // Each split must be further filtered, because if even one split matches, // the ENTIRE transaction is returned with all splits (even non-matching ones) if (m_config_f.includes(splitAccount) && m_config_f.match(&(*it_split))) { // reverse sign to match common notation for cash flow direction, only for expense/income splits MyMoneyMoney reverse(splitAccount.isIncomeExpense() ? -1 : 1, 1); MyMoneyMoney value; // the outer group is the account class (major account type) MyMoneyAccount::accountTypeE type = splitAccount.accountGroup(); QString outergroup = KMyMoneyUtils::accountTypeToString(type); value = (*it_split).shares(); bool stockSplit = tx.isStockSplit(); if (!stockSplit) { // retrieve the value in the account's underlying currency if (value != MyMoneyMoney::autoCalc) { value = value * reverse; } else { qDebug("PivotTable::PivotTable(): This must not happen"); value = MyMoneyMoney(); // keep it 0 so far } // Except in the case of transfers on an income/expense report if (al_transfers && (type == MyMoneyAccount::Asset || type == MyMoneyAccount::Liability)) { outergroup = i18n("Transfers"); value = -value; } } // add the value to its correct position in the pivot table assignCell(outergroup, splitAccount, column, value, false, stockSplit); } ++it_split; } ++it_transaction; } // // Get forecast data // if (m_config_f.isIncludingForecast()) calculateForecast(); // //Insert Price data // if (m_config_f.isIncludingPrice()) fillBasePriceUnit(ePrice); // //Insert Average Price data // if (m_config_f.isIncludingAveragePrice()) { fillBasePriceUnit(eActual); calculateMovingAverage(); } // // Collapse columns to match column type // if (m_config_f.columnPitch() > 1) collapseColumns(); // // Calculate the running sums // (for running sum reports only) // if (m_config_f.isRunningSum()) calculateRunningSums(); // // Calculate Moving Average // if (m_config_f.isIncludingMovingAverage()) calculateMovingAverage(); // // Calculate Budget Difference // if (m_config_f.isIncludingBudgetActuals()) calculateBudgetDiff(); // // Convert all values to the deep currency // convertToDeepCurrency(); // // Convert all values to the base currency // if (m_config_f.isConvertCurrency()) convertToBaseCurrency(); // // Determine column headings // calculateColumnHeadings(); // // Calculate row and column totals // calculateTotals(); // // If using mixed time, calculate column for current date // m_config_f.setCurrentDateColumn(currentDateColumn()); } void PivotTable::collapseColumns() { DEBUG_ENTER(Q_FUNC_INFO); int columnpitch = m_config_f.columnPitch(); if (columnpitch != 1) { int sourcemonth = (m_config_f.isColumnsAreDays()) // use the user's locale to determine the week's start ? (m_beginDate.dayOfWeek() + 8 - QLocale().firstDayOfWeek()) % 7 : m_beginDate.month(); int sourcecolumn = 1; int destcolumn = 1; while (sourcecolumn < m_numColumns) { if (sourcecolumn != destcolumn) { #if 0 // TODO: Clean up this rather inefficient kludge. We really should jump by an entire // destcolumn at a time on RS reports, and calculate the proper sourcecolumn to use, // allowing us to clear and accumulate only ONCE per destcolumn if (m_config_f.isRunningSum()) clearColumn(destcolumn); #endif accumulateColumn(destcolumn, sourcecolumn); } if (++sourcecolumn < m_numColumns) { if ((sourcemonth++ % columnpitch) == 0) { if (sourcecolumn != ++destcolumn) clearColumn(destcolumn); } } } m_numColumns = destcolumn + 1; } } void PivotTable::accumulateColumn(int destcolumn, int sourcecolumn) { DEBUG_ENTER(Q_FUNC_INFO); DEBUG_OUTPUT(QString("From Column %1 to %2").arg(sourcecolumn).arg(destcolumn)); // iterate over outer groups PivotGrid::iterator it_outergroup = m_grid.begin(); while (it_outergroup != m_grid.end()) { // iterate over inner groups PivotOuterGroup::iterator it_innergroup = (*it_outergroup).begin(); while (it_innergroup != (*it_outergroup).end()) { // iterator over rows PivotInnerGroup::iterator it_row = (*it_innergroup).begin(); while (it_row != (*it_innergroup).end()) { if ((*it_row)[eActual].count() <= sourcecolumn) throw MYMONEYEXCEPTION(QString("Sourcecolumn %1 out of grid range (%2) in PivotTable::accumulateColumn").arg(sourcecolumn).arg((*it_row)[eActual].count())); if ((*it_row)[eActual].count() <= destcolumn) throw MYMONEYEXCEPTION(QString("Destcolumn %1 out of grid range (%2) in PivotTable::accumulateColumn").arg(sourcecolumn).arg((*it_row)[eActual].count())); (*it_row)[eActual][destcolumn] += (*it_row)[eActual][sourcecolumn]; ++it_row; } ++it_innergroup; } ++it_outergroup; } } void PivotTable::clearColumn(int column) { DEBUG_ENTER(Q_FUNC_INFO); DEBUG_OUTPUT(QString("Column %1").arg(column)); // iterate over outer groups PivotGrid::iterator it_outergroup = m_grid.begin(); while (it_outergroup != m_grid.end()) { // iterate over inner groups PivotOuterGroup::iterator it_innergroup = (*it_outergroup).begin(); while (it_innergroup != (*it_outergroup).end()) { // iterator over rows PivotInnerGroup::iterator it_row = (*it_innergroup).begin(); while (it_row != (*it_innergroup).end()) { if ((*it_row)[eActual].count() <= column) throw MYMONEYEXCEPTION(QString("Column %1 out of grid range (%2) in PivotTable::accumulateColumn").arg(column).arg((*it_row)[eActual].count())); (*it_row++)[eActual][column] = PivotCell(); } ++it_innergroup; } ++it_outergroup; } } void PivotTable::calculateColumnHeadings() { DEBUG_ENTER(Q_FUNC_INFO); // one column for the opening balance m_columnHeadings.append("Opening"); int columnpitch = m_config_f.columnPitch(); if (columnpitch == 0) { // output the warning but don't crash by dividing with 0 qWarning("PivotTable::calculateColumnHeadings() Invalid column pitch"); return; } // if this is a days-based report if (m_config_f.isColumnsAreDays()) { if (columnpitch == 1) { QDate columnDate = m_beginDate; int column = 1; while (column++ < m_numColumns) { QString heading = QLocale().monthName(columnDate.month(), QLocale::ShortFormat) + ' ' + QString::number(columnDate.day()); columnDate = columnDate.addDays(1); m_columnHeadings.append(heading); } } else { QDate day = m_beginDate; QDate prv = m_beginDate; // use the user's locale to determine the week's start int dow = (day.dayOfWeek() + 8 - QLocale().firstDayOfWeek()) % 7; while (day <= m_endDate) { if (((dow % columnpitch) == 0) || (day == m_endDate)) { m_columnHeadings.append(QString("%1 %2 - %3 %4") .arg(QLocale().monthName(prv.month(), QLocale::ShortFormat)) .arg(prv.day()) .arg(QLocale().monthName(day.month(), QLocale::ShortFormat)) .arg(day.day())); prv = day.addDays(1); } day = day.addDays(1); dow++; } } } // else it's a months-based report else { if (columnpitch == 12) { int year = m_beginDate.year(); int column = 1; while (column++ < m_numColumns) m_columnHeadings.append(QString::number(year++)); } else { int year = m_beginDate.year(); bool includeyear = (m_beginDate.year() != m_endDate.year()); int segment = (m_beginDate.month() - 1) / columnpitch; int column = 1; while (column++ < m_numColumns) { QString heading = QLocale().monthName(1 + segment * columnpitch, QLocale::ShortFormat); if (columnpitch != 1) heading += '-' + QLocale().monthName((1 + segment) * columnpitch, QLocale::ShortFormat); if (includeyear) heading += ' ' + QString::number(year); m_columnHeadings.append(heading); if (++segment >= 12 / columnpitch) { segment -= 12 / columnpitch; ++year; } } } } } void PivotTable::createAccountRows() { DEBUG_ENTER(Q_FUNC_INFO); MyMoneyFile* file = MyMoneyFile::instance(); QList accounts; file->accountList(accounts); QList::const_iterator it_account = accounts.constBegin(); while (it_account != accounts.constEnd()) { ReportAccount account = *it_account; // only include this item if its account group is included in this report // and if the report includes this account if (m_config_f.includes(*it_account)) { DEBUG_OUTPUT(QString("Includes account %1").arg(account.name())); // the row group is the account class (major account type) QString outergroup = KMyMoneyUtils::accountTypeToString(account.accountGroup()); // place into the 'opening' column... assignCell(outergroup, account, 0, MyMoneyMoney()); } ++it_account; } } void PivotTable::calculateOpeningBalances() { DEBUG_ENTER(Q_FUNC_INFO); // First, determine the inclusive dates of the report. Normally, that's just // the begin & end dates of m_config_f. However, if either of those dates are // blank, we need to use m_beginDate and/or m_endDate instead. QDate from = m_config_f.fromDate(); QDate to = m_config_f.toDate(); if (! from.isValid()) from = m_beginDate; if (! to.isValid()) to = m_endDate; MyMoneyFile* file = MyMoneyFile::instance(); QList accounts; file->accountList(accounts); QList::const_iterator it_account = accounts.constBegin(); while (it_account != accounts.constEnd()) { ReportAccount account = *it_account; // only include this item if its account group is included in this report // and if the report includes this account if (m_config_f.includes(*it_account)) { //do not include account if it is closed and it has no transactions in the report period if (account.isClosed()) { //check if the account has transactions for the report timeframe MyMoneyTransactionFilter filter; filter.addAccount(account.id()); filter.setDateFilter(m_beginDate, m_endDate); filter.setReportAllSplits(false); QList transactions = file->transactionList(filter); //if a closed account has no transactions in that timeframe, do not include it if (transactions.size() == 0) { DEBUG_OUTPUT(QString("DOES NOT INCLUDE account %1").arg(account.name())); ++it_account; continue; } } DEBUG_OUTPUT(QString("Includes account %1").arg(account.name())); // the row group is the account class (major account type) QString outergroup = KMyMoneyUtils::accountTypeToString(account.accountGroup()); // extract the balance of the account for the given begin date, which is // the opening balance plus the sum of all transactions prior to the begin // date // this is in the underlying currency MyMoneyMoney value = file->balance(account.id(), from.addDays(-1)); // place into the 'opening' column... assignCell(outergroup, account, 0, value); } else { DEBUG_OUTPUT(QString("DOES NOT INCLUDE account %1").arg(account.name())); } ++it_account; } } void PivotTable::calculateRunningSums(PivotInnerGroup::iterator& it_row) { MyMoneyMoney runningsum = it_row.value()[eActual][0].calculateRunningSum(MyMoneyMoney()); int column = 1; while (column < m_numColumns) { if (it_row.value()[eActual].count() <= column) throw MYMONEYEXCEPTION(QString("Column %1 out of grid range (%2) in PivotTable::calculateRunningSums").arg(column).arg(it_row.value()[eActual].count())); runningsum = it_row.value()[eActual][column].calculateRunningSum(runningsum); ++column; } } void PivotTable::calculateRunningSums() { DEBUG_ENTER(Q_FUNC_INFO); m_runningSumsCalculated = true; PivotGrid::iterator it_outergroup = m_grid.begin(); while (it_outergroup != m_grid.end()) { PivotOuterGroup::iterator it_innergroup = (*it_outergroup).begin(); while (it_innergroup != (*it_outergroup).end()) { PivotInnerGroup::iterator it_row = (*it_innergroup).begin(); while (it_row != (*it_innergroup).end()) { #if 0 MyMoneyMoney runningsum = it_row.value()[0]; int column = 1; while (column < m_numColumns) { if (it_row.value()[eActual].count() <= column) throw MYMONEYEXCEPTION(QString("Column %1 out of grid range (%2) in PivotTable::calculateRunningSums").arg(column).arg(it_row.value()[eActual].count())); runningsum = (it_row.value()[eActual][column] += runningsum); ++column; } #endif calculateRunningSums(it_row); ++it_row; } ++it_innergroup; } ++it_outergroup; } } MyMoneyMoney PivotTable::cellBalance(const QString& outergroup, const ReportAccount& _row, int _column, bool budget) { if (m_runningSumsCalculated) { qDebug("You must not call PivotTable::cellBalance() after calling PivotTable::calculateRunningSums()"); throw MYMONEYEXCEPTION(QString("You must not call PivotTable::cellBalance() after calling PivotTable::calculateRunningSums()")); } // for budget reports, if this is the actual value, map it to the account which // holds its budget ReportAccount row = _row; if (!budget && m_config_f.hasBudget()) { QString newrow = m_budgetMap[row.id()]; // if there was no mapping found, then the budget report is not interested // in this account. if (newrow.isEmpty()) return MyMoneyMoney(); row = newrow; } // ensure the row already exists (and its parental hierarchy) createRow(outergroup, row, true); // Determine the inner group from the top-most parent account QString innergroup(row.topParentName()); if (m_numColumns <= _column) throw MYMONEYEXCEPTION(QString("Column %1 out of m_numColumns range (%2) in PivotTable::cellBalance").arg(_column).arg(m_numColumns)); if (m_grid[outergroup][innergroup][row][eActual].count() <= _column) throw MYMONEYEXCEPTION(QString("Column %1 out of grid range (%2) in PivotTable::cellBalance").arg(_column).arg(m_grid[outergroup][innergroup][row][eActual].count())); MyMoneyMoney balance; if (budget) balance = m_grid[outergroup][innergroup][row][eBudget][0].cellBalance(MyMoneyMoney()); else balance = m_grid[outergroup][innergroup][row][eActual][0].cellBalance(MyMoneyMoney()); int column = 1; while (column < _column) { if (m_grid[outergroup][innergroup][row][eActual].count() <= column) throw MYMONEYEXCEPTION(QString("Column %1 out of grid range (%2) in PivotTable::cellBalance").arg(column).arg(m_grid[outergroup][innergroup][row][eActual].count())); balance = m_grid[outergroup][innergroup][row][eActual][column].cellBalance(balance); ++column; } return balance; } void PivotTable::calculateBudgetMapping() { DEBUG_ENTER(Q_FUNC_INFO); MyMoneyFile* file = MyMoneyFile::instance(); // Only do this if there is at least one budget in the file if (file->countBudgets()) { // Select a budget // // It will choose the first budget in the list for the start year of the report if no budget is selected MyMoneyBudget budget = MyMoneyBudget(); QList budgets = file->budgetList(); bool validBudget = false; //check that the selected budget is valid if (m_config_f.budget() != "Any") { QList::const_iterator budgets_it = budgets.constBegin(); while (budgets_it != budgets.constEnd()) { //pick the budget by id if ((*budgets_it).id() == m_config_f.budget()) { budget = file->budget((*budgets_it).id()); validBudget = true; break; } ++budgets_it; } } //if no valid budget has been selected if (!validBudget) { //if the budget list is empty, just return if (budgets.count() == 0) { return; } QList::const_iterator budgets_it = budgets.constBegin(); while (budgets_it != budgets.constEnd()) { //pick the first budget that matches the report start year if ((*budgets_it).budgetStart().year() == QDate::currentDate().year()) { budget = file->budget((*budgets_it).id()); break; } ++budgets_it; } //if it can't find a matching budget, take the first one on the list if (budget.id().isEmpty()) { budget = budgets[0]; } //assign the budget to the report m_config_f.setBudget(budget.id(), m_config_f.isIncludingBudgetActuals()); } // Dump the budget //qDebug() << "Budget " << budget.name() << ": "; // Go through all accounts in the system to build the mapping QList accounts; file->accountList(accounts); QList::const_iterator it_account = accounts.constBegin(); while (it_account != accounts.constEnd()) { //include only the accounts selected for the report if (m_config_f.includes(*it_account)) { QString id = (*it_account).id(); QString acid = id; // If the budget contains this account outright if (budget.contains(id)) { // Add it to the mapping m_budgetMap[acid] = id; // qDebug() << ReportAccount(acid).debugName() << " self-maps / type =" << budget.account(id).budgetLevel(); } // Otherwise, search for a parent account which includes sub-accounts else { //if includeBudgetActuals, include all accounts regardless of whether in budget or not if (m_config_f.isIncludingBudgetActuals()) { m_budgetMap[acid] = id; // qDebug() << ReportAccount(acid).debugName() << " maps to " << ReportAccount(id).debugName(); } do { id = file->account(id).parentAccountId(); if (budget.contains(id)) { if (budget.account(id).budgetSubaccounts()) { m_budgetMap[acid] = id; // qDebug() << ReportAccount(acid).debugName() << " maps to " << ReportAccount(id).debugName(); break; } } } while (! id.isEmpty()); } } ++it_account; } // end while looping through the accounts in the file // Place the budget values into the budget grid QList baccounts = budget.getaccounts(); QList::const_iterator it_bacc = baccounts.constBegin(); while (it_bacc != baccounts.constEnd()) { ReportAccount splitAccount = (*it_bacc).id(); //include the budget account only if it is included in the report if (m_config_f.includes(splitAccount)) { MyMoneyAccount::accountTypeE type = splitAccount.accountGroup(); QString outergroup = KMyMoneyUtils::accountTypeToString(type); // reverse sign to match common notation for cash flow direction, only for expense/income splits MyMoneyMoney reverse((splitAccount.accountType() == MyMoneyAccount::Expense) ? -1 : 1, 1); const QMap& periods = (*it_bacc).getPeriods(); // skip the account if it has no periods if (periods.count() < 1) { ++it_bacc; continue; } MyMoneyMoney value = (*periods.begin()).amount() * reverse; int column = 1; // based on the kind of budget it is, deal accordingly switch ((*it_bacc).budgetLevel()) { case MyMoneyBudget::AccountGroup::eYearly: // divide the single yearly value by 12 and place it in each column value /= MyMoneyMoney(12, 1); case MyMoneyBudget::AccountGroup::eNone: case MyMoneyBudget::AccountGroup::eMax: case MyMoneyBudget::AccountGroup::eMonthly: // place the single monthly value in each column of the report // only add the value if columns are monthly or longer if (m_config_f.columnType() == MyMoneyReport::eBiMonths || m_config_f.columnType() == MyMoneyReport::eMonths || m_config_f.columnType() == MyMoneyReport::eYears || m_config_f.columnType() == MyMoneyReport::eQuarters) { QDate budgetDate = budget.budgetStart(); while (column < m_numColumns && budget.budgetStart().addYears(1) > budgetDate) { //only show budget values if the budget year and the column date match //no currency conversion is done here because that is done for all columns later if (budgetDate > columnDate(column)) { ++column; } else { if (budgetDate >= m_beginDate.addDays(-m_beginDate.day() + 1) && budgetDate <= m_endDate.addDays(m_endDate.daysInMonth() - m_endDate.day()) && budgetDate > (columnDate(column).addMonths(-m_config_f.columnType()))) { assignCell(outergroup, splitAccount, column, value, true /*budget*/); } budgetDate = budgetDate.addMonths(1); } } } break; case MyMoneyBudget::AccountGroup::eMonthByMonth: // place each value in the appropriate column // budget periods are supposed to come in order just like columns { QMap::const_iterator it_period = periods.begin(); while (it_period != periods.end() && column < m_numColumns) { if ((*it_period).startDate() > columnDate(column)) { ++column; } else { switch (m_config_f.columnType()) { case MyMoneyReport::eYears: case MyMoneyReport::eBiMonths: case MyMoneyReport::eQuarters: case MyMoneyReport::eMonths: { if ((*it_period).startDate() >= m_beginDate.addDays(-m_beginDate.day() + 1) && (*it_period).startDate() <= m_endDate.addDays(m_endDate.daysInMonth() - m_endDate.day()) && (*it_period).startDate() > (columnDate(column).addMonths(-m_config_f.columnType()))) { //no currency conversion is done here because that is done for all columns later value = (*it_period).amount() * reverse; assignCell(outergroup, splitAccount, column, value, true /*budget*/); } ++it_period; break; } default: break; } } } break; } } } ++it_bacc; } } // end if there was a budget } void PivotTable::convertToBaseCurrency() { DEBUG_ENTER(Q_FUNC_INFO); int fraction = MyMoneyFile::instance()->baseCurrency().smallestAccountFraction(); PivotGrid::iterator it_outergroup = m_grid.begin(); while (it_outergroup != m_grid.end()) { PivotOuterGroup::iterator it_innergroup = (*it_outergroup).begin(); while (it_innergroup != (*it_outergroup).end()) { PivotInnerGroup::iterator it_row = (*it_innergroup).begin(); while (it_row != (*it_innergroup).end()) { int column = 1; while (column < m_numColumns) { if (it_row.value()[eActual].count() <= column) throw MYMONEYEXCEPTION(QString("Column %1 out of grid range (%2) in PivotTable::convertToBaseCurrency").arg(column).arg(it_row.value()[eActual].count())); QDate valuedate = columnDate(column); //get base price for that date MyMoneyMoney conversionfactor = it_row.key().baseCurrencyPrice(valuedate, m_config_f.isSkippingZero()); for (int i = 0; i < m_rowTypeList.size(); ++i) { if (m_rowTypeList[i] != eAverage) { //calculate base value MyMoneyMoney oldval = it_row.value()[ m_rowTypeList[i] ][column]; MyMoneyMoney value = (oldval * conversionfactor).reduce(); //convert to lowest fraction it_row.value()[ m_rowTypeList[i] ][column] = PivotCell(value.convert(fraction)); DEBUG_OUTPUT_IF(conversionfactor != MyMoneyMoney::ONE , QString("Factor of %1, value was %2, now %3").arg(conversionfactor).arg(DEBUG_SENSITIVE(oldval)).arg(DEBUG_SENSITIVE(it_row.value()[m_rowTypeList[i]][column].toDouble()))); } } ++column; } ++it_row; } ++it_innergroup; } ++it_outergroup; } } void PivotTable::convertToDeepCurrency() { DEBUG_ENTER(Q_FUNC_INFO); MyMoneyFile* file = MyMoneyFile::instance(); PivotGrid::iterator it_outergroup = m_grid.begin(); while (it_outergroup != m_grid.end()) { PivotOuterGroup::iterator it_innergroup = (*it_outergroup).begin(); while (it_innergroup != (*it_outergroup).end()) { PivotInnerGroup::iterator it_row = (*it_innergroup).begin(); while (it_row != (*it_innergroup).end()) { int column = 1; while (column < m_numColumns) { if (it_row.value()[eActual].count() <= column) throw MYMONEYEXCEPTION(QString("Column %1 out of grid range (%2) in PivotTable::convertToDeepCurrency").arg(column).arg(it_row.value()[eActual].count())); QDate valuedate = columnDate(column); //get conversion factor for the account and date MyMoneyMoney conversionfactor = it_row.key().deepCurrencyPrice(valuedate, m_config_f.isSkippingZero()); //use the fraction relevant to the account at hand int fraction = it_row.key().currency().smallestAccountFraction(); //use base currency fraction if not initialized if (fraction == -1) fraction = file->baseCurrency().smallestAccountFraction(); //convert to deep currency MyMoneyMoney oldval = it_row.value()[eActual][column]; MyMoneyMoney value = (oldval * conversionfactor).reduce(); //reduce to lowest fraction it_row.value()[eActual][column] = PivotCell(value.convert(fraction)); //convert price data if (m_config_f.isIncludingPrice()) { MyMoneyMoney oldPriceVal = it_row.value()[ePrice][column]; MyMoneyMoney priceValue = (oldPriceVal * conversionfactor).reduce(); it_row.value()[ePrice][column] = PivotCell(priceValue.convert(10000)); } DEBUG_OUTPUT_IF(conversionfactor != MyMoneyMoney::ONE , QString("Factor of %1, value was %2, now %3").arg(conversionfactor).arg(DEBUG_SENSITIVE(oldval)).arg(DEBUG_SENSITIVE(it_row.value()[eActual][column].toDouble()))); ++column; } ++it_row; } ++it_innergroup; } ++it_outergroup; } } void PivotTable::calculateTotals() { //insert the row type that is going to be used for (int i = 0; i < m_rowTypeList.size(); ++i) { for (int k = 0; k < m_numColumns; ++k) { m_grid.m_total[ m_rowTypeList[i] ].append(PivotCell()); } } // // Outer groups // // iterate over outer groups PivotGrid::iterator it_outergroup = m_grid.begin(); while (it_outergroup != m_grid.end()) { for (int i = 0; i < m_rowTypeList.size(); ++i) { for (int k = 0; k < m_numColumns; ++k) { (*it_outergroup).m_total[ m_rowTypeList[i] ].append(PivotCell()); } } // // Inner Groups // PivotOuterGroup::iterator it_innergroup = (*it_outergroup).begin(); while (it_innergroup != (*it_outergroup).end()) { for (int i = 0; i < m_rowTypeList.size(); ++i) { for (int k = 0; k < m_numColumns; ++k) { (*it_innergroup).m_total[ m_rowTypeList[i] ].append(PivotCell()); } } // // Rows // PivotInnerGroup::iterator it_row = (*it_innergroup).begin(); while (it_row != (*it_innergroup).end()) { // // Columns // int column = 1; while (column < m_numColumns) { for (int i = 0; i < m_rowTypeList.size(); ++i) { if (it_row.value()[ m_rowTypeList[i] ].count() <= column) throw MYMONEYEXCEPTION(QString("Column %1 out of grid range (%2) in PivotTable::calculateTotals, row columns").arg(column).arg(it_row.value()[ m_rowTypeList[i] ].count())); if ((*it_innergroup).m_total[ m_rowTypeList[i] ].count() <= column) throw MYMONEYEXCEPTION(QString("Column %1 out of grid range (%2) in PivotTable::calculateTotals, inner group totals").arg(column).arg((*it_innergroup).m_total[ m_rowTypeList[i] ].count())); //calculate total MyMoneyMoney value = it_row.value()[ m_rowTypeList[i] ][column]; (*it_innergroup).m_total[ m_rowTypeList[i] ][column] += value; (*it_row)[ m_rowTypeList[i] ].m_total += value; } ++column; } ++it_row; } // // Inner Row Group Totals // int column = 1; while (column < m_numColumns) { for (int i = 0; i < m_rowTypeList.size(); ++i) { if ((*it_innergroup).m_total[ m_rowTypeList[i] ].count() <= column) throw MYMONEYEXCEPTION(QString("Column %1 out of grid range (%2) in PivotTable::calculateTotals, inner group totals").arg(column).arg((*it_innergroup).m_total[ m_rowTypeList[i] ].count())); if ((*it_outergroup).m_total[ m_rowTypeList[i] ].count() <= column) throw MYMONEYEXCEPTION(QString("Column %1 out of grid range (%2) in PivotTable::calculateTotals, outer group totals").arg(column).arg((*it_innergroup).m_total[ m_rowTypeList[i] ].count())); //calculate totals MyMoneyMoney value = (*it_innergroup).m_total[ m_rowTypeList[i] ][column]; (*it_outergroup).m_total[ m_rowTypeList[i] ][column] += value; (*it_innergroup).m_total[ m_rowTypeList[i] ].m_total += value; } ++column; } ++it_innergroup; } // // Outer Row Group Totals // const bool isIncomeExpense = (m_config_f.rowType() == MyMoneyReport::eExpenseIncome); const bool invert_total = (*it_outergroup).m_inverted; int column = 1; while (column < m_numColumns) { for (int i = 0; i < m_rowTypeList.size(); ++i) { if (m_grid.m_total[ m_rowTypeList[i] ].count() <= column) throw MYMONEYEXCEPTION(QString("Column %1 out of grid range (%2) in PivotTable::calculateTotals, grid totals").arg(column).arg((*it_innergroup).m_total[ m_rowTypeList[i] ].count())); //calculate actual totals MyMoneyMoney value = (*it_outergroup).m_total[ m_rowTypeList[i] ][column]; (*it_outergroup).m_total[ m_rowTypeList[i] ].m_total += value; //so far the invert only applies to actual and budget if (invert_total && m_rowTypeList[i] != eBudgetDiff && m_rowTypeList[i] != eForecast) value = -value; // forecast income expense reports should be inverted as oposed to asset/liability reports if (invert_total && isIncomeExpense && m_rowTypeList[i] == eForecast) value = -value; m_grid.m_total[ m_rowTypeList[i] ][column] += value; } ++column; } ++it_outergroup; } // // Report Totals // int totalcolumn = 1; while (totalcolumn < m_numColumns) { for (int i = 0; i < m_rowTypeList.size(); ++i) { if (m_grid.m_total[ m_rowTypeList[i] ].count() <= totalcolumn) throw MYMONEYEXCEPTION(QString("Total column %1 out of grid range (%2) in PivotTable::calculateTotals, grid totals").arg(totalcolumn).arg(m_grid.m_total[ m_rowTypeList[i] ].count())); //calculate actual totals MyMoneyMoney value = m_grid.m_total[ m_rowTypeList[i] ][totalcolumn]; m_grid.m_total[ m_rowTypeList[i] ].m_total += value; } ++totalcolumn; } } void PivotTable::assignCell(const QString& outergroup, const ReportAccount& _row, int column, MyMoneyMoney value, bool budget, bool stockSplit) { DEBUG_ENTER(Q_FUNC_INFO); DEBUG_OUTPUT(QString("Parameters: %1,%2,%3,%4,%5").arg(outergroup).arg(_row.debugName()).arg(column).arg(DEBUG_SENSITIVE(value.toDouble())).arg(budget)); // for budget reports, if this is the actual value, map it to the account which // holds its budget ReportAccount row = _row; if (!budget && m_config_f.hasBudget()) { QString newrow = m_budgetMap[row.id()]; // if there was no mapping found, then the budget report is not interested // in this account. if (newrow.isEmpty()) return; row = newrow; } // ensure the row already exists (and its parental hierarchy) createRow(outergroup, row, true); // Determine the inner group from the top-most parent account QString innergroup(row.topParentName()); if (m_numColumns <= column) throw MYMONEYEXCEPTION(QString("Column %1 out of m_numColumns range (%2) in PivotTable::assignCell").arg(column).arg(m_numColumns)); if (m_grid[outergroup][innergroup][row][eActual].count() <= column) throw MYMONEYEXCEPTION(QString("Column %1 out of grid range (%2) in PivotTable::assignCell").arg(column).arg(m_grid[outergroup][innergroup][row][eActual].count())); if (m_grid[outergroup][innergroup][row][eBudget].count() <= column) throw MYMONEYEXCEPTION(QString("Column %1 out of grid range (%2) in PivotTable::assignCell").arg(column).arg(m_grid[outergroup][innergroup][row][eBudget].count())); if (!stockSplit) { // Determine whether the value should be inverted before being placed in the row if (m_grid[outergroup].m_inverted) value = -value; // Add the value to the grid cell if (budget) { m_grid[outergroup][innergroup][row][eBudget][column] += value; } else { // If it is loading an actual value for a budget report // check whether it is a subaccount of a budget account (include subaccounts) // If so, check if is the same currency and convert otherwise if (m_config_f.hasBudget() && row.id() != _row.id() && row.currencyId() != _row.currencyId()) { ReportAccount origAcc = _row; MyMoneyMoney rate = origAcc.foreignCurrencyPrice(row.currencyId(), columnDate(column), false); m_grid[outergroup][innergroup][row][eActual][column] += (value * rate).reduce(); } else { m_grid[outergroup][innergroup][row][eActual][column] += value; } } } else { m_grid[outergroup][innergroup][row][eActual][column] += PivotCell::stockSplit(value); } } void PivotTable::createRow(const QString& outergroup, const ReportAccount& row, bool recursive) { DEBUG_ENTER(Q_FUNC_INFO); // Determine the inner group from the top-most parent account QString innergroup(row.topParentName()); if (! m_grid.contains(outergroup)) { DEBUG_OUTPUT(QString("Adding group [%1]").arg(outergroup)); m_grid[outergroup] = PivotOuterGroup(m_numColumns); } if (! m_grid[outergroup].contains(innergroup)) { DEBUG_OUTPUT(QString("Adding group [%1][%2]").arg(outergroup).arg(innergroup)); m_grid[outergroup][innergroup] = PivotInnerGroup(m_numColumns); } if (! m_grid[outergroup][innergroup].contains(row)) { DEBUG_OUTPUT(QString("Adding row [%1][%2][%3]").arg(outergroup).arg(innergroup).arg(row.debugName())); m_grid[outergroup][innergroup][row] = PivotGridRowSet(m_numColumns); if (recursive && !row.isTopLevel()) createRow(outergroup, row.parent(), recursive); } } int PivotTable::columnValue(const QDate& _date) const { if (m_config_f.isColumnsAreDays()) return (m_beginDate.daysTo(_date)); else return (_date.year() * 12 + _date.month()); } QDate PivotTable::columnDate(int column) const { if (m_config_f.isColumnsAreDays()) return m_beginDate.addDays(m_config_f.columnPitch() * column - 1); else return m_beginDate.addMonths(m_config_f.columnPitch() * column).addDays(-1); } QString PivotTable::renderCSV() const { DEBUG_ENTER(Q_FUNC_INFO); // // Report Title // QString result = QString("\"Report: %1\"\n").arg(m_config_f.name()); result += i18nc("Report date range", "%1 through %2\n", QLocale().toString(m_config_f.fromDate(), QLocale::ShortFormat), QLocale().toString(m_config_f.toDate(), QLocale::ShortFormat)); if (m_config_f.isConvertCurrency()) result += i18n("All currencies converted to %1\n", MyMoneyFile::instance()->baseCurrency().name()); else result += i18n("All values shown in %1 unless otherwise noted\n", MyMoneyFile::instance()->baseCurrency().name()); // // Table Header // result += i18n("Account"); int column = 1; while (column < m_numColumns) { result += QString(",%1").arg(QString(m_columnHeadings[column++])); if (m_rowTypeList.size() > 1) { QString separator; separator = separator.fill(',', m_rowTypeList.size() - 1); result += separator; } } //show total columns if (m_config_f.isShowingRowTotals()) result += QString(",%1").arg(i18nc("Total balance", "Total")); result += '\n'; // Row Type Header if (m_rowTypeList.size() > 1) { int column = 1; while (column < m_numColumns) { for (int i = 0; i < m_rowTypeList.size(); ++i) { result += QString(",%1").arg(m_columnTypeHeaderList[i]); } column++; } if (m_config_f.isShowingRowTotals()) { for (int i = 0; i < m_rowTypeList.size(); ++i) { result += QString(",%1").arg(m_columnTypeHeaderList[i]); } } result += '\n'; } int fraction = MyMoneyFile::instance()->baseCurrency().smallestAccountFraction(); // // Outer groups // // iterate over outer groups PivotGrid::const_iterator it_outergroup = m_grid.begin(); while (it_outergroup != m_grid.end()) { // // Outer Group Header // result += it_outergroup.key() + '\n'; // // Inner Groups // PivotOuterGroup::const_iterator it_innergroup = (*it_outergroup).begin(); int rownum = 0; while (it_innergroup != (*it_outergroup).end()) { // // Rows // QString innergroupdata; PivotInnerGroup::const_iterator it_row = (*it_innergroup).begin(); while (it_row != (*it_innergroup).end()) { ReportAccount rowname = it_row.key(); int fraction = rowname.currency().smallestAccountFraction(); // // Columns // QString rowdata; int column = 1; bool isUsed = false; for (int i = 0; i < m_rowTypeList.size(); ++i) isUsed |= it_row.value()[ m_rowTypeList[i] ][0].isUsed(); while (column < m_numColumns) { //show columns for (int i = 0; i < m_rowTypeList.size(); ++i) { isUsed |= it_row.value()[ m_rowTypeList[i] ][column].isUsed(); rowdata += QString(",\"%1\"").arg(it_row.value()[ m_rowTypeList[i] ][column].formatMoney(fraction, false)); } column++; } if (m_config_f.isShowingRowTotals()) { for (int i = 0; i < m_rowTypeList.size(); ++i) rowdata += QString(",\"%1\"").arg((*it_row)[ m_rowTypeList[i] ].m_total.formatMoney(fraction, false)); } // // Row Header // if (!rowname.isClosed() || isUsed) { innergroupdata += "\"" + QString().fill(' ', rowname.hierarchyDepth() - 1) + rowname.name(); // if we don't convert the currencies to the base currency and the // current row contains a foreign currency, then we append the currency // to the name of the account if (!m_config_f.isConvertCurrency() && rowname.isForeignCurrency()) innergroupdata += QString(" (%1)").arg(rowname.currencyId()); innergroupdata += '\"'; if (isUsed) innergroupdata += rowdata; innergroupdata += '\n'; } ++it_row; } // // Inner Row Group Totals // bool finishrow = true; QString finalRow; bool isUsed = false; if (m_config_f.detailLevel() == MyMoneyReport::eDetailAll && ((*it_innergroup).size() > 1)) { // Print the individual rows result += innergroupdata; if (m_config_f.isShowingColumnTotals()) { // Start the TOTALS row finalRow = i18nc("Total balance", "Total"); isUsed = true; } else { ++rownum; finishrow = false; } } else { // Start the single INDIVIDUAL ACCOUNT row ReportAccount rowname = (*it_innergroup).begin().key(); isUsed |= !rowname.isClosed(); finalRow = "\"" + QString().fill(' ', rowname.hierarchyDepth() - 1) + rowname.name(); if (!m_config_f.isConvertCurrency() && rowname.isForeignCurrency()) finalRow += QString(" (%1)").arg(rowname.currencyId()); finalRow += "\""; } // Finish the row started above, unless told not to if (finishrow) { int column = 1; for (int i = 0; i < m_rowTypeList.size(); ++i) isUsed |= (*it_innergroup).m_total[ m_rowTypeList[i] ][0].isUsed(); while (column < m_numColumns) { for (int i = 0; i < m_rowTypeList.size(); ++i) { isUsed |= (*it_innergroup).m_total[ m_rowTypeList[i] ][column].isUsed(); finalRow += QString(",\"%1\"").arg((*it_innergroup).m_total[ m_rowTypeList[i] ][column].formatMoney(fraction, false)); } column++; } if (m_config_f.isShowingRowTotals()) { for (int i = 0; i < m_rowTypeList.size(); ++i) finalRow += QString(",\"%1\"").arg((*it_innergroup).m_total[ m_rowTypeList[i] ].m_total.formatMoney(fraction, false)); } finalRow += '\n'; } if (isUsed) { result += finalRow; ++rownum; } ++it_innergroup; } // // Outer Row Group Totals // if (m_config_f.isShowingColumnTotals()) { result += QString("%1 %2").arg(i18nc("Total balance", "Total")).arg(it_outergroup.key()); int column = 1; while (column < m_numColumns) { for (int i = 0; i < m_rowTypeList.size(); ++i) result += QString(",\"%1\"").arg((*it_outergroup).m_total[ m_rowTypeList[i] ][column].formatMoney(fraction, false)); column++; } if (m_config_f.isShowingRowTotals()) { for (int i = 0; i < m_rowTypeList.size(); ++i) result += QString(",\"%1\"").arg((*it_outergroup).m_total[ m_rowTypeList[i] ].m_total.formatMoney(fraction, false)); } result += '\n'; } ++it_outergroup; } // // Report Totals // if (m_config_f.isShowingColumnTotals()) { result += i18n("Grand Total"); int totalcolumn = 1; while (totalcolumn < m_numColumns) { for (int i = 0; i < m_rowTypeList.size(); ++i) result += QString(",\"%1\"").arg(m_grid.m_total[ m_rowTypeList[i] ][totalcolumn].formatMoney(fraction, false)); totalcolumn++; } if (m_config_f.isShowingRowTotals()) { for (int i = 0; i < m_rowTypeList.size(); ++i) result += QString(",\"%1\"").arg(m_grid.m_total[ m_rowTypeList[i] ].m_total.formatMoney(fraction, false)); } result += '\n'; } return result; } QString PivotTable::renderBody() const { DEBUG_ENTER(Q_FUNC_INFO); QString colspan = QString(" colspan=\"%1\"").arg(m_numColumns + 1 + (m_config_f.isShowingRowTotals() ? 1 : 0)); // // Report Title // QString result = QString("

%1

\n").arg(m_config_f.name()); //actual dates of the report result += QString("
"); result += i18nc("Report date range", "%1 through %2", QLocale().toString(m_config_f.fromDate(), QLocale::ShortFormat), QLocale().toString(m_config_f.toDate(), QLocale::ShortFormat)); result += QString("
\n"); result += QString("
 
\n"); //currency conversion message result += QString("
"); if (m_config_f.isConvertCurrency()) result += i18n("All currencies converted to %1", MyMoneyFile::instance()->baseCurrency().name()); else result += i18n("All values shown in %1 unless otherwise noted", MyMoneyFile::instance()->baseCurrency().name()); result += QString("
\n"); result += QString("
 
\n"); // setup a leftborder for better readability of budget vs actual reports QString leftborder; if (m_rowTypeList.size() > 1) leftborder = " class=\"leftborder\""; // // Table Header // result += QString("\n\n\n" "\n").arg(i18n("Account")); QString headerspan; int span = m_rowTypeList.size(); headerspan = QString(" colspan=\"%1\"").arg(span); int column = 1; while (column < m_numColumns) result += QString("%2").arg(headerspan, QString(m_columnHeadings[column++]).replace(QRegExp(" "), "
")); if (m_config_f.isShowingRowTotals()) result += QString("%2").arg(headerspan).arg(i18nc("Total balance", "Total")); result += "
\n"; // // Header for multiple columns // if (span > 1) { result += ""; int column = 1; while (column < m_numColumns) { QString lb; if (column != 1) lb = leftborder; for (int i = 0; i < m_rowTypeList.size(); ++i) { result += QString("%1") .arg(m_columnTypeHeaderList[i]) .arg(i == 0 ? lb : QString()); } column++; } if (m_config_f.isShowingRowTotals()) { for (int i = 0; i < m_rowTypeList.size(); ++i) { result += QString("%1") .arg(m_columnTypeHeaderList[i]) .arg(i == 0 ? leftborder : QString()); } } result += ""; } // Skip the body of the report if the report only calls for totals to be shown if (m_config_f.detailLevel() != MyMoneyReport::eDetailTotal) { // // Outer groups // // Need to sort the outergroups. They can't always be sorted by name. So we create a list of // map iterators, and sort that. Then we'll iterate through the map iterators and use those as // before. // // I hope this doesn't bog the performance of reports, given that we're copying the entire report // data. If this is a perf hit, we could change to storing outergroup pointers, I think. QList outergroups; PivotGrid::const_iterator it_outergroup_map = m_grid.begin(); while (it_outergroup_map != m_grid.end()) { outergroups.push_back(it_outergroup_map.value()); // copy the name into the outergroup, because we will now lose any association with // the map iterator outergroups.back().m_displayName = it_outergroup_map.key(); ++it_outergroup_map; } qSort(outergroups.begin(), outergroups.end()); QList::const_iterator it_outergroup = outergroups.constBegin(); while (it_outergroup != outergroups.constEnd()) { // // Outer Group Header // result += QString("\n").arg(colspan).arg((*it_outergroup).m_displayName); // Skip the inner groups if the report only calls for outer group totals to be shown if (m_config_f.detailLevel() != MyMoneyReport::eDetailGroup) { // // Inner Groups // PivotOuterGroup::const_iterator it_innergroup = (*it_outergroup).begin(); int rownum = 0; while (it_innergroup != (*it_outergroup).end()) { // // Rows // QString innergroupdata; PivotInnerGroup::const_iterator it_row = (*it_innergroup).begin(); while (it_row != (*it_innergroup).end()) { // // Columns // QString rowdata; int column = 1; bool isUsed = it_row.value()[eActual][0].isUsed(); while (column < m_numColumns) { QString lb; if (column != 1) lb = leftborder; for (int i = 0; i < m_rowTypeList.size(); ++i) { rowdata += QString("%1") .arg(coloredAmount(it_row.value()[ m_rowTypeList[i] ][column])) .arg(i == 0 ? lb : QString()); isUsed |= it_row.value()[ m_rowTypeList[i] ][column].isUsed(); } column++; } if (m_config_f.isShowingRowTotals()) { for (int i = 0; i < m_rowTypeList.size(); ++i) { rowdata += QString("%1") .arg(coloredAmount(it_row.value()[ m_rowTypeList[i] ].m_total)) .arg(i == 0 ? leftborder : QString()); } } // // Row Header // ReportAccount rowname = it_row.key(); // don't show closed accounts if they have not been used if (!rowname.isClosed() || isUsed) { innergroupdata += QString("%5%6") .arg(rownum & 0x01 ? "even" : "odd") .arg(rowname.isTopLevel() ? " id=\"topparent\"" : "") .arg("") //.arg((*it_row).m_total.isZero() ? colspan : "") // colspan the distance if this row will be blank .arg(rowname.hierarchyDepth() - 1) .arg(rowname.name().replace(QRegExp(" "), " ")) .arg((m_config_f.isConvertCurrency() || !rowname.isForeignCurrency()) ? QString() : QString(" (%1)").arg(rowname.currency().id())); // Don't print this row if it's going to be all zeros // TODO: Uncomment this, and deal with the case where the data // is zero, but the budget is non-zero //if ( !(*it_row).m_total.isZero() ) innergroupdata += rowdata; innergroupdata += "\n"; } ++it_row; } // // Inner Row Group Totals // bool finishrow = true; QString finalRow; bool isUsed = false; if (m_config_f.detailLevel() == MyMoneyReport::eDetailAll && ((*it_innergroup).size() > 1)) { // Print the individual rows result += innergroupdata; if (m_config_f.isShowingColumnTotals()) { // Start the TOTALS row finalRow = QString("") .arg(rownum & 0x01 ? "even" : "odd") .arg(i18nc("Total balance", "Total")); // don't suppress display of totals isUsed = true; } else { finishrow = false; ++rownum; } } else { // Start the single INDIVIDUAL ACCOUNT row // FIXME: There is a bit of a bug here with class=leftX. There's only a finite number // of classes I can define in the .CSS file, and the user can theoretically nest deeper. // The right solution is to use style=Xem, and calculate X. Let's see if anyone complains // first :) Also applies to the row header case above. // FIXED: I found it in one of my reports and changed it to the proposed method. // This works for me (ipwizard) ReportAccount rowname = (*it_innergroup).begin().key(); isUsed |= !rowname.isClosed(); finalRow = QString("") .arg(rownum & 0x01 ? "even" : "odd") .arg(m_config_f.detailLevel() == MyMoneyReport::eDetailAll ? "id=\"solo\"" : "") .arg(rowname.hierarchyDepth() - 1) .arg(rowname.name().replace(QRegExp(" "), " ")) .arg((m_config_f.isConvertCurrency() || !rowname.isForeignCurrency()) ? QString() : QString(" (%1)").arg(rowname.currency().id())); } // Finish the row started above, unless told not to if (finishrow) { int column = 1; isUsed |= (*it_innergroup).m_total[eActual][0].isUsed(); while (column < m_numColumns) { QString lb; if (column != 1) lb = leftborder; for (int i = 0; i < m_rowTypeList.size(); ++i) { finalRow += QString("%1") .arg(coloredAmount((*it_innergroup).m_total[ m_rowTypeList[i] ][column])) .arg(i == 0 ? lb : QString()); isUsed |= (*it_innergroup).m_total[ m_rowTypeList[i] ][column].isUsed(); } column++; } if (m_config_f.isShowingRowTotals()) { for (int i = 0; i < m_rowTypeList.size(); ++i) { finalRow += QString("%1") .arg(coloredAmount((*it_innergroup).m_total[ m_rowTypeList[i] ].m_total)) .arg(i == 0 ? leftborder : QString()); } } finalRow += "\n"; if (isUsed) { result += finalRow; ++rownum; } } ++it_innergroup; } // end while iterating on the inner groups } // end if detail level is not "group" // // Outer Row Group Totals // if (m_config_f.isShowingColumnTotals()) { result += QString("").arg(i18nc("Total balance", "Total")).arg((*it_outergroup).m_displayName); int column = 1; while (column < m_numColumns) { QString lb; if (column != 1) lb = leftborder; for (int i = 0; i < m_rowTypeList.size(); ++i) { result += QString("%1") .arg(coloredAmount((*it_outergroup).m_total[ m_rowTypeList[i] ][column])) .arg(i == 0 ? lb : QString()); } column++; } if (m_config_f.isShowingRowTotals()) { for (int i = 0; i < m_rowTypeList.size(); ++i) { result += QString("%1") .arg(coloredAmount((*it_outergroup).m_total[ m_rowTypeList[i] ].m_total)) .arg(i == 0 ? leftborder : QString()); } } result += "\n"; } ++it_outergroup; } // end while iterating on the outergroups } // end if detail level is not "total" // // Report Totals // if (m_config_f.isShowingColumnTotals()) { result += QString("\n"); result += QString("").arg(i18n("Grand Total")); int totalcolumn = 1; while (totalcolumn < m_numColumns) { QString lb; if (totalcolumn != 1) lb = leftborder; for (int i = 0; i < m_rowTypeList.size(); ++i) { result += QString("%1") .arg(coloredAmount(m_grid.m_total[ m_rowTypeList[i] ][totalcolumn])) .arg(i == 0 ? lb : QString()); } totalcolumn++; } if (m_config_f.isShowingRowTotals()) { for (int i = 0; i < m_rowTypeList.size(); ++i) { result += QString("%1") .arg(coloredAmount(m_grid.m_total[ m_rowTypeList[i] ].m_total)) .arg(i == 0 ? leftborder : QString()); } } result += "\n"; } result += QString("\n"); result += QString("\n"); result += "
%1
%2
  %2
%5%6
%1 %2
 
%1
 
 
\n"; return result; } void PivotTable::dump(const QString& file, const QString& /* context */) const { QFile g(file); g.open(QIODevice::WriteOnly); QTextStream(&g) << renderBody(); g.close(); } void PivotTable::drawChart(KReportChartView& chartView) const { chartView.drawPivotChart(m_grid, m_config_f, m_numColumns, m_columnHeadings, m_rowTypeList, m_columnTypeHeaderList); } +QString PivotTable::coloredAmount(const MyMoneyMoney& amount, const QString& currencySymbol) const +{ + return coloredAmount(amount, currencySymbol, KMyMoneyGlobalSettings::pricePrecision()); +} + QString PivotTable::coloredAmount(const MyMoneyMoney& amount, const QString& currencySymbol, int prec) const { QString result; if (amount.isNegative()) result += QString("") .arg(KMyMoneyGlobalSettings::listNegativeValueColor().red()) .arg(KMyMoneyGlobalSettings::listNegativeValueColor().green()) .arg(KMyMoneyGlobalSettings::listNegativeValueColor().blue()); result += amount.formatMoney(currencySymbol, prec); if (amount.isNegative()) result += QString(""); return result; } void PivotTable::calculateBudgetDiff() { PivotGrid::iterator it_outergroup = m_grid.begin(); while (it_outergroup != m_grid.end()) { PivotOuterGroup::iterator it_innergroup = (*it_outergroup).begin(); while (it_innergroup != (*it_outergroup).end()) { PivotInnerGroup::iterator it_row = (*it_innergroup).begin(); while (it_row != (*it_innergroup).end()) { int column = 1; switch (it_row.key().accountGroup()) { case MyMoneyAccount::Income: case MyMoneyAccount::Asset: while (column < m_numColumns) { it_row.value()[eBudgetDiff][column] = it_row.value()[eActual][column] - it_row.value()[eBudget][column]; ++column; } break; case MyMoneyAccount::Expense: case MyMoneyAccount::Liability: while (column < m_numColumns) { it_row.value()[eBudgetDiff][column] = it_row.value()[eBudget][column] - it_row.value()[eActual][column]; ++column; } break; default: break; } ++it_row; } ++it_innergroup; } ++it_outergroup; } } void PivotTable::calculateForecast() { //setup forecast MyMoneyForecast forecast = KMyMoneyGlobalSettings::forecast(); //since this is a net worth forecast we want to include all account even those that are not in use forecast.setIncludeUnusedAccounts(true); //setup forecast dates if (m_endDate > QDate::currentDate()) { forecast.setForecastEndDate(m_endDate); forecast.setForecastStartDate(QDate::currentDate()); forecast.setForecastDays(QDate::currentDate().daysTo(m_endDate)); } else { forecast.setForecastStartDate(m_beginDate); forecast.setForecastEndDate(m_endDate); forecast.setForecastDays(m_beginDate.daysTo(m_endDate) + 1); } //adjust history dates if beginning date is before today if (m_beginDate < QDate::currentDate()) { forecast.setHistoryEndDate(m_beginDate.addDays(-1)); forecast.setHistoryStartDate(forecast.historyEndDate().addDays(-forecast.accountsCycle()*forecast.forecastCycles())); } //run forecast if (m_config_f.rowType() == MyMoneyReport::eAssetLiability) { //asset and liability forecast.doForecast(); } else { //income and expenses MyMoneyBudget budget; forecast.createBudget(budget, m_beginDate.addYears(-1), m_beginDate.addDays(-1), m_beginDate, m_endDate, false); } //go through the data and add forecast PivotGrid::iterator it_outergroup = m_grid.begin(); while (it_outergroup != m_grid.end()) { PivotOuterGroup::iterator it_innergroup = (*it_outergroup).begin(); while (it_innergroup != (*it_outergroup).end()) { PivotInnerGroup::iterator it_row = (*it_innergroup).begin(); while (it_row != (*it_innergroup).end()) { int column = 1; QDate forecastDate = m_beginDate; //check whether columns are days or months if (m_config_f.isColumnsAreDays()) { while (column < m_numColumns) { it_row.value()[eForecast][column] = forecast.forecastBalance(it_row.key(), forecastDate); forecastDate = forecastDate.addDays(1); ++column; } } else { //if columns are months while (column < m_numColumns) { // the forecast balance is on the first day of the month see MyMoneyForecast::calculateScheduledMonthlyBalances() forecastDate = QDate(forecastDate.year(), forecastDate.month(), 1); //check that forecastDate is not over ending date if (forecastDate > m_endDate) forecastDate = m_endDate; //get forecast balance and set the corresponding column it_row.value()[eForecast][column] = forecast.forecastBalance(it_row.key(), forecastDate); forecastDate = forecastDate.addMonths(1); ++column; } } ++it_row; } ++it_innergroup; } ++it_outergroup; } } void PivotTable::loadRowTypeList() { if ((m_config_f.isIncludingBudgetActuals()) || (!m_config_f.hasBudget() && !m_config_f.isIncludingForecast() && !m_config_f.isIncludingMovingAverage() && !m_config_f.isIncludingPrice() && !m_config_f.isIncludingAveragePrice()) ) { m_rowTypeList.append(eActual); m_columnTypeHeaderList.append(i18n("Actual")); } if (m_config_f.hasBudget()) { m_rowTypeList.append(eBudget); m_columnTypeHeaderList.append(i18n("Budget")); } if (m_config_f.isIncludingBudgetActuals()) { m_rowTypeList.append(eBudgetDiff); m_columnTypeHeaderList.append(i18n("Difference")); } if (m_config_f.isIncludingForecast()) { m_rowTypeList.append(eForecast); m_columnTypeHeaderList.append(i18n("Forecast")); } if (m_config_f.isIncludingMovingAverage()) { m_rowTypeList.append(eAverage); m_columnTypeHeaderList.append(i18n("Moving Average")); } if (m_config_f.isIncludingAveragePrice()) { m_rowTypeList.append(eAverage); m_columnTypeHeaderList.append(i18n("Moving Average Price")); } if (m_config_f.isIncludingPrice()) { m_rowTypeList.append(ePrice); m_columnTypeHeaderList.append(i18n("Price")); } } void PivotTable::calculateMovingAverage() { int delta = m_config_f.movingAverageDays() / 2; //go through the data and add the moving average PivotGrid::iterator it_outergroup = m_grid.begin(); while (it_outergroup != m_grid.end()) { PivotOuterGroup::iterator it_innergroup = (*it_outergroup).begin(); while (it_innergroup != (*it_outergroup).end()) { PivotInnerGroup::iterator it_row = (*it_innergroup).begin(); while (it_row != (*it_innergroup).end()) { int column = 1; //check whether columns are days or months if (m_config_f.columnType() == MyMoneyReport::eDays) { while (column < m_numColumns) { MyMoneyMoney totalPrice = MyMoneyMoney(); QDate averageStart = columnDate(column).addDays(-delta); QDate averageEnd = columnDate(column).addDays(delta); for (QDate averageDate = averageStart; averageDate <= averageEnd; averageDate = averageDate.addDays(1)) { if (m_config_f.isConvertCurrency()) { totalPrice += it_row.key().deepCurrencyPrice(averageDate) * it_row.key().baseCurrencyPrice(averageDate); } else { totalPrice += it_row.key().deepCurrencyPrice(averageDate); } totalPrice = totalPrice.convert(10000); } //calculate the average price MyMoneyMoney averagePrice = totalPrice / MyMoneyMoney((averageStart.daysTo(averageEnd) + 1), 1); //get the actual value, multiply by the average price and save that value MyMoneyMoney averageValue = it_row.value()[eActual][column] * averagePrice; it_row.value()[eAverage][column] = averageValue.convert(10000); ++column; } } else { //if columns are months while (column < m_numColumns) { QDate averageStart = columnDate(column); //set the right start date depending on the column type switch (m_config_f.columnType()) { case MyMoneyReport::eYears: { averageStart = QDate(columnDate(column).year(), 1, 1); break; } case MyMoneyReport::eBiMonths: { averageStart = QDate(columnDate(column).year(), columnDate(column).month(), 1).addMonths(-1); break; } case MyMoneyReport::eQuarters: { averageStart = QDate(columnDate(column).year(), columnDate(column).month(), 1).addMonths(-1); break; } case MyMoneyReport::eMonths: { averageStart = QDate(columnDate(column).year(), columnDate(column).month(), 1); break; } case MyMoneyReport::eWeeks: { averageStart = columnDate(column).addDays(-columnDate(column).dayOfWeek() + 1); break; } default: break; } //gather the actual data and calculate the average MyMoneyMoney totalPrice = MyMoneyMoney(); QDate averageEnd = columnDate(column); for (QDate averageDate = averageStart; averageDate <= averageEnd; averageDate = averageDate.addDays(1)) { if (m_config_f.isConvertCurrency()) { totalPrice += it_row.key().deepCurrencyPrice(averageDate) * it_row.key().baseCurrencyPrice(averageDate); } else { totalPrice += it_row.key().deepCurrencyPrice(averageDate); } totalPrice = totalPrice.convert(10000); } MyMoneyMoney averagePrice = totalPrice / MyMoneyMoney((averageStart.daysTo(averageEnd) + 1), 1); MyMoneyMoney averageValue = it_row.value()[eActual][column] * averagePrice; //fill in the average it_row.value()[eAverage][column] = averageValue.convert(10000); ++column; } } ++it_row; } ++it_innergroup; } ++it_outergroup; } } void PivotTable::fillBasePriceUnit(ERowType rowType) { MyMoneyFile* file = MyMoneyFile::instance(); QString baseCurrencyId = file->baseCurrency().id(); //get the first price date for securities QMap securityDates = securityFirstPrice(); //go through the data PivotGrid::iterator it_outergroup = m_grid.begin(); while (it_outergroup != m_grid.end()) { PivotOuterGroup::iterator it_innergroup = (*it_outergroup).begin(); while (it_innergroup != (*it_outergroup).end()) { PivotInnerGroup::iterator it_row = (*it_innergroup).begin(); while (it_row != (*it_innergroup).end()) { int column = 1; //if it is a base currency fill all the values bool firstPriceExists = false; if (it_row.key().currencyId() == baseCurrencyId) { firstPriceExists = true; } while (column < m_numColumns) { //check whether the date for that column is on or after the first price if (!firstPriceExists && securityDates.contains(it_row.key().currencyId()) && columnDate(column) >= securityDates.value(it_row.key().currencyId())) { firstPriceExists = true; } //only add the dummy value if there is a price for that date if (firstPriceExists) { //insert a unit of currency for each account it_row.value()[rowType][column] = MyMoneyMoney::ONE; } ++column; } ++it_row; } ++it_innergroup; } ++it_outergroup; } } QMap PivotTable::securityFirstPrice() { MyMoneyFile* file = MyMoneyFile::instance(); MyMoneyPriceList priceList = file->priceList(); QMap securityPriceDate; MyMoneyPriceList::const_iterator prices_it; for (prices_it = priceList.constBegin(); prices_it != priceList.constEnd(); ++prices_it) { MyMoneyPrice firstPrice = (*((*prices_it).constBegin())); //check the security in the from field //if it is there, check if it is older if (securityPriceDate.contains(firstPrice.from())) { if (securityPriceDate.value(firstPrice.from()) > firstPrice.date()) { securityPriceDate[firstPrice.from()] = firstPrice.date(); } } else { securityPriceDate.insert(firstPrice.from(), firstPrice.date()); } //check the security in the to field //if it is there, check if it is older if (securityPriceDate.contains(firstPrice.to())) { if (securityPriceDate.value(firstPrice.to()) > firstPrice.date()) { securityPriceDate[firstPrice.to()] = firstPrice.date(); } } else { securityPriceDate.insert(firstPrice.to(), firstPrice.date()); } } return securityPriceDate; } void PivotTable::includeInvestmentSubAccounts() { // if we're not in expert mode, we need to make sure // that all stock accounts for the selected investment // account are also selected QStringList accountList; if (m_config_f.accounts(accountList)) { if (!KMyMoneyGlobalSettings::expertMode()) { QStringList::const_iterator it_a, it_b; for (it_a = accountList.constBegin(); it_a != accountList.constEnd(); ++it_a) { MyMoneyAccount acc = MyMoneyFile::instance()->account(*it_a); if (acc.accountType() == MyMoneyAccount::Investment) { for (it_b = acc.accountList().constBegin(); it_b != acc.accountList().constEnd(); ++it_b) { if (!accountList.contains(*it_b)) { m_config_f.addAccount(*it_b); } } } } } } } int PivotTable::currentDateColumn() { //return -1 if the columns do not include the current date if (m_beginDate > QDate::currentDate() || m_endDate < QDate::currentDate()) { return -1; } //check the date of each column and return if it is the one for the current date //if columns are not days, return the one for the current month or year int column = 1; while (column < m_numColumns) { if (columnDate(column) >= QDate::currentDate()) { break; } column++; } //if there is no column matching the current date, return -1 if (column == m_numColumns) { column = -1; } return column; } } // namespace diff --git a/kmymoney/reports/pivottable.h b/kmymoney/reports/pivottable.h index 304119f28..5edf6409c 100644 --- a/kmymoney/reports/pivottable.h +++ b/kmymoney/reports/pivottable.h @@ -1,374 +1,375 @@ /*************************************************************************** pivottable.h ------------------- begin : Sat May 22 2004 copyright : (C) 2004-2005 by Ace Jones Thomas Baumgart Alvaro Soliverez ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #ifndef PIVOTTABLE_H #define PIVOTTABLE_H // ---------------------------------------------------------------------------- // QT Includes #include #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes #include "kreportchartview.h" #include "mymoneyfile.h" #include "mymoneyreport.h" #include "reporttable.h" #include "pivotgrid.h" #include "reportaccount.h" using namespace KChart; namespace reports { /** * Calculates a 'pivot table' of information about the transaction database. * Based on pivot tables in MS Excel, and implemented as 'Data Pilot' in * OpenOffice.Org Calc. * * | Month,etc * -------------+------------ * Expense Type | Sum(Value) * Category | * * This is a middle-layer class, between the UI and the engine. The * MyMoneyReport class holds only the CONFIGURATION parameters. This * class actually does the work of retrieving the data from the engine * and formatting it for the user. * * @author Ace Jones * * @short **/ class PivotTable : public ReportTable { KMM_MYMONEY_UNIT_TESTABLE public: /** * Create a Pivot table style report * * @param _config_f The configuration parameters for this report */ PivotTable(const MyMoneyReport& _config_f); /** * virtual Destructur */ virtual ~PivotTable() {} /** * Render the report body to an HTML stream. * * @return QString HTML string representing the report body */ QString renderBody() const; /** * Render the report to a comma-separated-values stream. * * @return QString CSV string representing the report */ QString renderCSV() const; /** * Render the report to a graphical chart * * @param view The KReportChartView into which to draw the chart. */ void drawChart(KReportChartView& view) const; /** * Dump the report's HTML to a file * * @param file The filename to dump into * @param context unused, but provided for interface compatibility */ void dump(const QString& file, const QString& context = QString()) const; /** * Returns the grid generated by the report * */ PivotGrid grid() { return m_grid; } protected: void init(); // used for debugging the constructor private: PivotGrid m_grid; QStringList m_columnHeadings; int m_numColumns; QDate m_beginDate; QDate m_endDate; bool m_runningSumsCalculated; /** * For budget-vs-actual reports only, maps each account to the account which holds * the budget for it. If an account is not contained in this map, it is not included * in the budget. */ QMap m_budgetMap; /** * This list contains the types of PivotGridRows that are going to be shown in the report */ QList m_rowTypeList; /** * This list contains the i18n headers for the column types */ QStringList m_columnTypeHeaderList; MyMoneyReport m_config_f; /** * This method returns the formatted value of @a amount with * a possible @a currencySymbol added and @a prec fractional digits. * @a currencySymbol defaults to be empty and @a prec defaults to 2. * * If @a amount is negative the formatted value is enclosed in an * HTML font tag to modify the color to reflect the user settings for * negtive numbers. * * Example: 1.23 is returned as '1.23' whereas -1.23 is returned as * @verbatim -1.23@endverbatim * with $red, $green and $blue being the actual value for the * chosen color. */ - QString coloredAmount(const MyMoneyMoney& amount, const QString& currencySymbol = QString(), int prec = 2) const; + QString coloredAmount(const MyMoneyMoney& amount, const QString& currencySymbol = QString()) const; + QString coloredAmount(const MyMoneyMoney& amount, const QString& currencySymbol, int prec) const; protected: /** * Creates a row in the grid if it doesn't already exist * * Downsteam assignment functions will assume that this row already * exists, so this function creates a row of the needed length populated * with zeros. * * @param outergroup The outer row group * @param row The row itself * @param recursive Whether to also recursively create rows for our parent accounts */ void createRow(const QString& outergroup, const ReportAccount& row, bool recursive); /** * Assigns a value into the grid * * Adds the given value to the value which already exists at the specified grid position * * @param outergroup The outer row group * @param row The row itself * @param column The column * @param value The value to be added in * @param budget Whether this is a budget value (@p true) or an actual * value (@p false). Defaults to @p false. * @param stockSplit Whether this is a stock split (@p true) or an actual * value (@p false). Defaults to @p false. */ inline void assignCell(const QString& outergroup, const ReportAccount& row, int column, MyMoneyMoney value, bool budget = false, bool stockSplit = false); /** * Create a row for each included account. This is used when * the config parameter isIncludingUnusedAccount() is true */ void createAccountRows(); /** * Record the opening balances of all qualifying accounts into the grid. * * For accounts opened before the report period, places the balance into the '0' column. * For those opened during the report period, places the balance into the appropriate column * for the month when it was opened. */ void calculateOpeningBalances(); /** * Calculate budget mapping * * For budget-vs-actual reports, this creates a mapping between each account * in the user's hierarchy and the account where the budget is held for it. * This is needed because the user can budget on a given account for that * account and all its descendants. Also if NO budget is placed on the * account or any of its parents, the account is not included in the map. */ void calculateBudgetMapping(); /** * Calculate the running sums. * * After calling this method, each cell of the report will contain the running sum of all * the cells in its row in this and earlier columns. * * For example, consider a row with these values: * 01 02 03 04 05 06 07 08 09 10 * * After calling this function, the row will look like this: * 01 03 06 10 15 21 28 36 45 55 */ void calculateRunningSums(); void calculateRunningSums(PivotInnerGroup::iterator& it_row); /** * This method calculates the difference between a @a budgeted and an @a * actual amount. The calculation is based on the type of the * @a repAccount. The difference value is calculated as follows: * * If @a repAccount is of type MyMoneyAccount::Income * * @code * diff = actual - budgeted * @endcode * * If @a repAccount is of type MyMoneyAccount::Expense * * @code * diff = budgeted - actual * @endcode * * In all other cases, 0 is returned. */ void calculateBudgetDiff(); /** * This method calculates forecast for a report */ void calculateForecast(); /** * This method inserts units to be used to display prices */ void fillBasePriceUnit(ERowType rowType); /** * This method collects the first date for which there is a price for every security */ QMap securityFirstPrice(); /** * This method calculates moving average for a report */ void calculateMovingAverage(); /** * Calculate the row and column totals * * This function will set the m_total members of all the TGrid objects. Be sure the values are * all converted to the base currency first!! * */ void calculateTotals(); /** * Convert each value in the grid to the base currency * */ void convertToBaseCurrency(); /** * Convert each value in the grid to the account/category's deep currency * * See AccountDescriptor::deepCurrencyPrice() for a description of 'deep' currency * */ void convertToDeepCurrency(); /** * Turn month-long columns into larger time periods if needed * * For example, consider a row with these values: * 01 02 03 04 05 06 07 08 09 10 * * If the column pitch is 3 (i.e. quarterly), after calling this function, * the row will look like this: * 06 15 26 10 */ void collapseColumns(); /** * Determine the proper column headings based on the time periods covered by each column * */ void calculateColumnHeadings(); /** * Helper methods for collapseColumns * */ void accumulateColumn(int destcolumn, int sourcecolumn); void clearColumn(int column); /** * Calculate the column of a given date. This is the absolute column in a * hypothetical report that covers all of known time. In reality an actual * report will be a subset of that. * * @param _date The date */ int columnValue(const QDate& _date) const; /** * Calculate the date of the last day covered by a given column. * * @param column The column */ QDate columnDate(int column) const; /** * Returns the balance of a given cell. Throws an exception once calculateRunningSums() has been run. */ MyMoneyMoney cellBalance(const QString& outergroup, const ReportAccount& _row, int column, bool budget); /** * Draws a PivotGridRowSet in a chart for the given ERowType */ unsigned drawChartRowSet(int rowNum, const bool seriesTotals, const bool accountSeries, KReportChartView& chartView, const PivotGridRowSet& rowSet, const ERowType rowType) const; /** * Loads m_rowTypeList with the list of PivotGridRow types that the reporttable * should show */ void loadRowTypeList(); /** * If not in expert mode, include all subaccounts for each selected * investment account */ void includeInvestmentSubAccounts(); /** * Returns the column which holds the current date * Returns -1 if the current date is not within range */ int currentDateColumn(); }; } #endif // PIVOTTABLE_H