You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
kmymoney/kmymoney2/reports/pivottable.cpp

2605 lines
88 KiB

/***************************************************************************
pivottable.cpp
-------------------
begin : Mon May 17 2004
copyright : (C) 2004-2005 by Ace Jones
email : <ace.j@hotpop.com>
Thomas Baumgart <ipwizard@users.sourceforge.net>
Alvaro Soliverez <asoliverez@gmail.com>
***************************************************************************/
/***************************************************************************
* *
* 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. *
* *
***************************************************************************/
// ----------------------------------------------------------------------------
// QT Includes
#include <qlayout.h>
#include <qdatetime.h>
#include <qregexp.h>
#include <qdragobject.h>
#include <qclipboard.h>
#include <qapplication.h>
#include <qprinter.h>
#include <qpainter.h>
#include <qfile.h>
#include <qdom.h>
// ----------------------------------------------------------------------------
// KDE Includes
// This is just needed for i18n() and weekStartDay().
// Once I figure out how to handle i18n
// without using this macro directly, I'll be freed of KDE dependency. This
// is a minor problem because we use these terms when rendering to HTML,
// and a more major problem because we need it to translate account types
// (e.g. MyMoneyAccount::Checkings) into their text representation. We also
// use that text representation in the core data structure of the report. (Ace)
#include <kglobal.h>
#include <klocale.h>
#include <kdebug.h>
#include <kcalendarsystem.h>
// ----------------------------------------------------------------------------
// Project Includes
#include "pivottable.h"
#include "pivotgrid.h"
#include "reportdebug.h"
#include "kreportchartview.h"
#include "../kmymoneyglobalsettings.h"
#include "../kmymoneyutils.h"
#include "../mymoney/mymoneyforecast.h"
#include <kmymoney/kmymoneyutils.h>
namespace reports {
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", m_sTabs.latin1(), m_methodName.latin1() );
m_sTabs.append("--");
}
}
Debug::~Debug()
{
if ( m_enabled )
{
m_sTabs.remove(0,2);
qDebug( "%s%s(): EXIT", m_sTabs.latin1(), m_methodName.latin1() );
if (m_methodName == m_sEnableKey)
m_enabled = false;
}
}
void Debug::output( const QString& _text )
{
if ( m_enabled )
qDebug( "%s%s(): %s", m_sTabs.latin1(), m_methodName.latin1(), _text.latin1() );
}
PivotTable::PivotTable( const MyMoneyReport& _config_f ):
m_runningSumsCalculated(false),
m_config_f( _config_f )
{
init();
}
void PivotTable::init(void)
{
DEBUG_ENTER(__PRETTY_FUNCTION__);
//
// 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-vs-actual reports only)
//
if ( m_config_f.hasBudget())
calculateBudgetMapping();
//
// Populate all transactions into the row/column pivot grid
//
QValueList<MyMoneyTransaction> transactions;
m_config_f.setReportAllSplits(false);
m_config_f.setConsiderCategory(true);
try {
transactions = file->transactionList(m_config_f);
} catch(MyMoneyException *e) {
qDebug("ERR: %s thrown in %s(%ld)", e->what().data(), e->file().data(), 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);
QValueList<MyMoneySchedule> schedules = file->scheduleList();
QValueList<MyMoneySchedule>::const_iterator it_schedule = schedules.begin();
while ( it_schedule != schedules.end() )
{
// 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
QValueList<QDate> paymentDates = (*it_schedule).paymentDates(nextpayment,configend);
QValueList<QDate>::const_iterator it_date = paymentDates.begin();
while ( it_date != paymentDates.end() )
{
//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<QString, MyMoneyMoney> loanBalances;
QValueList<MyMoneyTransaction>::const_iterator it_transaction = transactions.begin();
unsigned colofs = columnValue(m_beginDate) - 1;
while ( it_transaction != transactions.end() )
{
QDate postdate = (*it_transaction).postDate();
unsigned column = columnValue(postdate) - colofs;
MyMoneyTransaction tx = (*it_transaction);
// 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 )) {
QValueList<MyMoneySplit>::ConstIterator it_loanSplits;
for(it_loanSplits = tx.splits().begin(); it_loanSplits != tx.splits().end(); ++it_loanSplits) {
if((*it_loanSplits).isAmortizationSplit() && (*it_loanSplits).accountId() == splitAccount.id() )
loanBalances[splitAccount.id()] = loanBalances[splitAccount.id()] + (*it_loanSplits).shares();
}
}
}
}
QValueList<MyMoneySplit> splits = tx.splits();
QValueList<MyMoneySplit>::const_iterator it_split = splits.begin();
while ( it_split != splits.end() )
{
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();
}
void PivotTable::collapseColumns(void)
{
DEBUG_ENTER(__PRETTY_FUNCTION__);
unsigned columnpitch = m_config_f.columnPitch();
if ( columnpitch != 1 )
{
unsigned sourcemonth = (m_config_f.isColumnsAreDays())
// use the user's locale to determine the week's start
? (m_beginDate.dayOfWeek() + 8 - KGlobal::locale()->weekStartDay()) % 7
: m_beginDate.month();
unsigned sourcecolumn = 1;
unsigned 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(unsigned destcolumn, unsigned sourcecolumn)
{
DEBUG_ENTER(__PRETTY_FUNCTION__);
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 new 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 new 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(unsigned column)
{
DEBUG_ENTER(__PRETTY_FUNCTION__);
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 new 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(void)
{
DEBUG_ENTER(__PRETTY_FUNCTION__);
// one column for the opening balance
m_columnHeadings.append( "Opening" );
unsigned columnpitch = m_config_f.columnPitch();
// if this is a days-based report
if (m_config_f.isColumnsAreDays())
{
if ( columnpitch == 1 )
{
QDate columnDate = m_beginDate;
unsigned column = 1;
while ( column++ < m_numColumns )
{
QString heading = KGlobal::locale()->calendar()->monthName(columnDate.month(), columnDate.year(), true) + " " + 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
unsigned dow = (day.dayOfWeek() +8 -KGlobal::locale()->weekStartDay())%7;
while (day <= m_endDate)
{
if (((dow % columnpitch) == 0) || (day == m_endDate))
{
m_columnHeadings.append(QString("%1&nbsp;%2 - %3&nbsp;%4")
.arg(KGlobal::locale()->calendar()->monthName(prv.month(), prv.year(), true))
.arg(prv.day())
.arg(KGlobal::locale()->calendar()->monthName(day.month(), day.year(), true))
.arg(day.day()));
prv = day.addDays(1);
}
day = day.addDays(1);
dow++;
}
}
}
// else it's a months-based report
else
{
if ( columnpitch == 12 )
{
unsigned year = m_beginDate.year();
unsigned column = 1;
while ( column++ < m_numColumns )
m_columnHeadings.append(QString::number(year++));
}
else
{
unsigned year = m_beginDate.year();
bool includeyear = ( m_beginDate.year() != m_endDate.year() );
unsigned segment = ( m_beginDate.month() - 1 ) / columnpitch;
unsigned column = 1;
while ( column++ < m_numColumns )
{
QString heading = KGlobal::locale()->calendar()->monthName(1+segment*columnpitch, 2000, true);
if ( columnpitch != 1 )
heading += "-" + KGlobal::locale()->calendar()->monthName((1+segment)*columnpitch, 2000, true);
if ( includeyear )
heading += " " + QString::number(year);
m_columnHeadings.append( heading);
if ( ++segment >= 12/columnpitch )
{
segment -= 12/columnpitch;
++year;
}
}
}
}
}
void PivotTable::createAccountRows(void)
{
DEBUG_ENTER(__PRETTY_FUNCTION__);
MyMoneyFile* file = MyMoneyFile::instance();
QValueList<MyMoneyAccount> accounts;
file->accountList(accounts);
QValueList<MyMoneyAccount>::const_iterator it_account = accounts.begin();
while ( it_account != accounts.end() )
{
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( void )
{
DEBUG_ENTER(__PRETTY_FUNCTION__);
// 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();
QValueList<MyMoneyAccount> accounts;
file->accountList(accounts);
QValueList<MyMoneyAccount>::const_iterator it_account = accounts.begin();
while ( it_account != accounts.end() )
{
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);
QValueList<MyMoneyTransaction> 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.data()[eActual][0].calculateRunningSum(MyMoneyMoney(0,1));
unsigned column = 1;
while ( column < m_numColumns )
{
if ( it_row.data()[eActual].count() <= column )
throw new MYMONEYEXCEPTION(QString("Column %1 out of grid range (%2) in PivotTable::calculateRunningSums").arg(column).arg(it_row.data()[eActual].count()));
runningsum = it_row.data()[eActual][column].calculateRunningSum(runningsum);
++column;
}
}
void PivotTable::calculateRunningSums( void )
{
DEBUG_ENTER(__PRETTY_FUNCTION__);
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.data()[0];
unsigned column = 1;
while ( column < m_numColumns )
{
if ( it_row.data()[eActual].count() <= column )
throw new MYMONEYEXCEPTION(QString("Column %1 out of grid range (%2) in PivotTable::calculateRunningSums").arg(column).arg(it_row.data()[eActual].count()));
runningsum = ( it_row.data()[eActual][column] += runningsum );
++column;
}
#endif
calculateRunningSums( it_row );
++it_row;
}
++it_innergroup;
}
++it_outergroup;
}
}
MyMoneyMoney PivotTable::cellBalance(const QString& outergroup, const ReportAccount& _row, unsigned _column, bool budget)
{
if(m_runningSumsCalculated) {
qDebug("You must not call PivotTable::cellBalance() after calling PivotTable::calculateRunningSums()");
throw new 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 new 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 new 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());
unsigned column = 1;
while ( column < _column)
{
if ( m_grid[outergroup][innergroup][row][eActual].count() <= column )
throw new 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( void )
{
DEBUG_ENTER(__PRETTY_FUNCTION__);
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 select
MyMoneyBudget budget = MyMoneyBudget();
//if no budget has been selected
if (m_config_f.budget() == "Any" ) {
QValueList<MyMoneyBudget> budgets = file->budgetList();
QValueList<MyMoneyBudget>::const_iterator budgets_it = budgets.begin();
while( budgets_it != budgets.end() ) {
//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 we can't find a matching budget, take the first of the list
if( budget.id() == "" )
budget = budgets[0];
//assign the budget to the report
m_config_f.setBudget(budget.id(), m_config_f.isIncludingBudgetActuals());
} else {
//pick the budget selected by the user
budget = file->budget( m_config_f.budget());
}
// Dump the budget
//kdDebug(2) << "Budget " << budget.name() << ": " << endl;
// Go through all accounts in the system to build the mapping
QValueList<MyMoneyAccount> accounts;
file->accountList(accounts);
QValueList<MyMoneyAccount>::const_iterator it_account = accounts.begin();
while ( it_account != accounts.end() )
{
//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;
// kdDebug(2) << ReportAccount(acid).debugName() << " self-maps / type =" << budget.account(id).budgetLevel() << endl;
}
// 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;
// kdDebug(2) << ReportAccount(acid).debugName() << " maps to " << ReportAccount(id).debugName() << endl;
}
do
{
id = file->account ( id ).parentAccountId();
if ( budget.contains ( id ) )
{
if ( budget.account ( id ).budgetSubaccounts() )
{
m_budgetMap[acid] = id;
// kdDebug(2) << ReportAccount(acid).debugName() << " maps to " << ReportAccount(id).debugName() << endl;
break;
}
}
}
while ( ! id.isEmpty() );
}
}
++it_account;
} // end while looping through the accounts in the file
// Place the budget values into the budget grid
QValueList<MyMoneyBudget::AccountGroup> baccounts = budget.getaccounts();
QValueList<MyMoneyBudget::AccountGroup>::const_iterator it_bacc = baccounts.begin();
while ( it_bacc != baccounts.end() )
{
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<QDate, MyMoneyBudget::PeriodGroup>& periods = (*it_bacc).getPeriods();
MyMoneyMoney value = (*periods.begin()).amount() * reverse;
MyMoneyMoney price = MyMoneyMoney(1,1);
unsigned 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) {
//value = value * MyMoneyMoney(m_config_f.columnType(), 1);
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<QDate, MyMoneyBudget::PeriodGroup>::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( void )
{
DEBUG_ENTER(__PRETTY_FUNCTION__);
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() )
{
unsigned column = 1;
while ( column < m_numColumns )
{
if ( it_row.data()[eActual].count() <= column )
throw new MYMONEYEXCEPTION(QString("Column %1 out of grid range (%2) in PivotTable::convertToBaseCurrency").arg(column).arg(it_row.data()[eActual].count()));
QDate valuedate = columnDate(column);
//get base price for that date
MyMoneyMoney conversionfactor = it_row.key().baseCurrencyPrice(valuedate);
for(unsigned i = 0; i < m_rowTypeList.size(); ++i) {
if( m_rowTypeList[i] != eAverage ) {
//calculate base value
MyMoneyMoney oldval = it_row.data()[ m_rowTypeList[i] ][column];
MyMoneyMoney value = (oldval * conversionfactor).reduce();
//convert to lowest fraction
it_row.data()[ m_rowTypeList[i] ][column] = PivotCell(value.convert(fraction));
DEBUG_OUTPUT_IF(conversionfactor != MyMoneyMoney(1,1) ,QString("Factor of %1, value was %2, now %3").arg(conversionfactor).arg(DEBUG_SENSITIVE(oldval)).arg(DEBUG_SENSITIVE(it_row.data()[m_rowTypeList[i]][column].toDouble())));
}
}
++column;
}
++it_row;
}
++it_innergroup;
}
++it_outergroup;
}
}
void PivotTable::convertToDeepCurrency( void )
{
DEBUG_ENTER(__PRETTY_FUNCTION__);
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() )
{
unsigned column = 1;
while ( column < m_numColumns )
{
if ( it_row.data()[eActual].count() <= column )
throw new MYMONEYEXCEPTION(QString("Column %1 out of grid range (%2) in PivotTable::convertToDeepCurrency").arg(column).arg(it_row.data()[eActual].count()));
QDate valuedate = columnDate(column);
//get conversion factor for the account and date
MyMoneyMoney conversionfactor = it_row.key().deepCurrencyPrice(valuedate);
//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.data()[eActual][column];
MyMoneyMoney value = (oldval * conversionfactor).reduce();
//reduce to lowest fraction
it_row.data()[eActual][column] = PivotCell(value.convert(fraction));
//convert price data
if(m_config_f.isIncludingPrice()) {
MyMoneyMoney oldPriceVal = it_row.data()[ePrice][column];
MyMoneyMoney priceValue = (oldPriceVal * conversionfactor).reduce();
it_row.data()[ePrice][column] = PivotCell(priceValue.convert(10000));
}
DEBUG_OUTPUT_IF(conversionfactor != MyMoneyMoney(1,1) ,QString("Factor of %1, value was %2, now %3").arg(conversionfactor).arg(DEBUG_SENSITIVE(oldval)).arg(DEBUG_SENSITIVE(it_row.data()[eActual][column].toDouble())));
++column;
}
++it_row;
}
++it_innergroup;
}
++it_outergroup;
}
}
void PivotTable::calculateTotals( void )
{
//insert the row type that is going to be used
for(unsigned i = 0; i < m_rowTypeList.size(); ++i)
m_grid.m_total[ m_rowTypeList[i] ].insert( m_grid.m_total[ m_rowTypeList[i] ].end(), m_numColumns, PivotCell() );
//
// Outer groups
//
// iterate over outer groups
PivotGrid::iterator it_outergroup = m_grid.begin();
while ( it_outergroup != m_grid.end() )
{
for(unsigned i = 0; i < m_rowTypeList.size(); ++i)
(*it_outergroup).m_total[ m_rowTypeList[i] ].insert( (*it_outergroup).m_total[ m_rowTypeList[i] ].end(), m_numColumns, PivotCell() );
//
// Inner Groups
//
PivotOuterGroup::iterator it_innergroup = (*it_outergroup).begin();
while ( it_innergroup != (*it_outergroup).end() )
{
for(unsigned i = 0; i < m_rowTypeList.size(); ++i)
(*it_innergroup).m_total[ m_rowTypeList[i] ].insert( (*it_innergroup).m_total[ m_rowTypeList[i] ].end(), m_numColumns, PivotCell() );
//
// Rows
//
PivotInnerGroup::iterator it_row = (*it_innergroup).begin();
while ( it_row != (*it_innergroup).end() )
{
//
// Columns
//
unsigned column = 1;
while ( column < m_numColumns )
{
for(unsigned i = 0; i < m_rowTypeList.size(); ++i) {
if ( it_row.data()[ m_rowTypeList[i] ].count() <= column )
throw new MYMONEYEXCEPTION(QString("Column %1 out of grid range (%2) in PivotTable::calculateTotals, row columns").arg(column).arg(it_row.data()[ m_rowTypeList[i] ].count()));
if ( (*it_innergroup).m_total[ m_rowTypeList[i] ].count() <= column )
throw new 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.data()[ 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
//
unsigned column = 1;
while ( column < m_numColumns )
{
for(unsigned i = 0; i < m_rowTypeList.size(); ++i) {
if ( (*it_innergroup).m_total[ m_rowTypeList[i] ].count() <= column )
throw new 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 new 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
//
bool invert_total = (*it_outergroup).m_inverted;
unsigned column = 1;
while ( column < m_numColumns )
{
for(unsigned i = 0; i < m_rowTypeList.size(); ++i) {
if ( m_grid.m_total[ m_rowTypeList[i] ].count() <= column )
throw new 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;
m_grid.m_total[ m_rowTypeList[i] ][column] += value;
}
++column;
}
++it_outergroup;
}
//
// Report Totals
//
unsigned totalcolumn = 1;
while ( totalcolumn < m_numColumns )
{
for(unsigned i = 0; i < m_rowTypeList.size(); ++i) {
if ( m_grid.m_total[ m_rowTypeList[i] ].count() <= totalcolumn )
throw new 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, unsigned column, MyMoneyMoney value, bool budget, bool stockSplit )
{
DEBUG_ENTER(__PRETTY_FUNCTION__);
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 new 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 new MYMONEYEXCEPTION(QString("Column %1 out of grid range (%2) in PivotTable::assignCell").arg(column).arg(m_grid[outergroup][innergroup][row][eActual].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
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(__PRETTY_FUNCTION__);
// 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 );
}
}
unsigned PivotTable::columnValue(const QDate& _date) const
{
if (m_config_f.isColumnsAreDays())
return (QDate().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( void ) const
{
DEBUG_ENTER(__PRETTY_FUNCTION__);
//
// Report Title
//
QString result = QString("\"Report: %1\"\n").arg(m_config_f.name());
if ( m_config_f.isConvertCurrency() )
result += i18n("All currencies converted to %1\n").arg(MyMoneyFile::instance()->baseCurrency().name());
else
result += i18n("All values shown in %1 unless otherwise noted\n").arg(MyMoneyFile::instance()->baseCurrency().name());
//
// Table Header
//
result += i18n("Account");
unsigned column = 1;
while ( column < m_numColumns )
result += QString(",%1").arg(QString(m_columnHeadings[column++]));
if ( m_config_f.isShowingRowTotals() )
result += QString(",%1").arg(i18n("Total"));
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();
unsigned 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;
unsigned column = 1;
bool isUsed = false;
for(unsigned i = 0; i < m_rowTypeList.size(); ++i)
isUsed |= it_row.data()[ m_rowTypeList[i] ][0].isUsed();
while ( column < m_numColumns ) {
//show columns
for(unsigned i = 0; i < m_rowTypeList.size(); ++i) {
isUsed |= it_row.data()[ m_rowTypeList[i] ][column].isUsed();
rowdata += QString(",\"%1\"").arg(it_row.data()[ m_rowTypeList[i] ][column].formatMoney(fraction, false));
}
column++;
}
if ( m_config_f.isShowingRowTotals() ) {
for(unsigned 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 = i18n("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 )
{
unsigned column = 1;
for(unsigned i = 0; i < m_rowTypeList.size(); ++i)
isUsed |= (*it_innergroup).m_total[ m_rowTypeList[i] ][0].isUsed();
while ( column < m_numColumns )
{
for(unsigned 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(unsigned 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(i18n("Total")).arg(it_outergroup.key());
unsigned column = 1;
while ( column < m_numColumns ) {
for(unsigned 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(unsigned 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");
unsigned totalcolumn = 1;
while ( totalcolumn < m_numColumns ) {
for(unsigned 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(unsigned 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::renderHTML( void ) const
{
DEBUG_ENTER(__PRETTY_FUNCTION__);
QString colspan = QString(" colspan=\"%1\"").arg(m_numColumns + 1 + (m_config_f.isShowingRowTotals() ? 1 : 0) );
//
// Report Title
//
QString result = QString("<h2 class=\"report\">%1</h2>\n").arg(m_config_f.name());
//actual dates of the report
result += QString("<div class=\"subtitle\">");
result += i18n("Report date range", "%1 through %2").arg(KGlobal::locale()->formatDate(m_config_f.fromDate(), true)).arg(KGlobal::locale()->formatDate(m_config_f.toDate(), true));
result += QString("</div>\n");
result += QString("<div class=\"gap\">&nbsp;</div>\n");
//currency conversion message
result += QString("<div class=\"subtitle\">");
if ( m_config_f.isConvertCurrency() )
result += i18n("All currencies converted to %1").arg(MyMoneyFile::instance()->baseCurrency().name());
else
result += i18n("All values shown in %1 unless otherwise noted").arg(MyMoneyFile::instance()->baseCurrency().name());
result += QString("</div>\n");
result += QString("<div class=\"gap\">&nbsp;</div>\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<table class=\"report\" cellspacing=\"0\">\n"
"<thead><tr class=\"itemheader\">\n<th>%1</th>").arg(i18n("Account"));
QString headerspan;
int span = m_rowTypeList.size();
headerspan = QString(" colspan=\"%1\"").arg(span);
unsigned column = 1;
while ( column < m_numColumns )
result += QString("<th%1>%2</th>").arg(headerspan,QString(m_columnHeadings[column++]).replace(QRegExp(" "),"<br>"));
if ( m_config_f.isShowingRowTotals() )
result += QString("<th%1>%2</th>").arg(headerspan).arg(i18n("Total"));
result += "</tr></thead>\n";
//
// Header for multiple columns
//
if ( span > 1 )
{
result += "<tr><td></td>";
unsigned column = 1;
while ( column < m_numColumns )
{
QString lb;
if(column != 1)
lb = leftborder;
for(unsigned i = 0; i < m_rowTypeList.size(); ++i) {
result += QString("<td%2>%1</td>")
.arg(i18n( m_columnTypeHeaderList[i] ))
.arg(i == 0 ? lb : QString() );
}
column++;
}
if ( m_config_f.isShowingRowTotals() ) {
for(unsigned i = 0; i < m_rowTypeList.size(); ++i) {
result += QString("<td%2>%1</td>")
.arg(i18n( m_columnTypeHeaderList[i] ))
.arg(i == 0 ? leftborder : QString() );
}
}
result += "</tr>";
}
// 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.
QValueList<PivotOuterGroup> outergroups;
PivotGrid::const_iterator it_outergroup_map = m_grid.begin();
while ( it_outergroup_map != m_grid.end() )
{
outergroups.push_back(it_outergroup_map.data());
// 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;
}
qHeapSort(outergroups);
QValueList<PivotOuterGroup>::const_iterator it_outergroup = outergroups.begin();
while ( it_outergroup != outergroups.end() )
{
//
// Outer Group Header
//
result += QString("<tr class=\"sectionheader\"><td class=\"left\"%1>%2</td></tr>\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();
unsigned 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;
unsigned column = 1;
bool isUsed = it_row.data()[eActual][0].isUsed();
while ( column < m_numColumns )
{
QString lb;
if(column != 1)
lb = leftborder;
for(unsigned i = 0; i < m_rowTypeList.size(); ++i) {
rowdata += QString("<td%2>%1</td>")
.arg(coloredAmount(it_row.data()[ m_rowTypeList[i] ][column]))
.arg(i == 0 ? lb : QString());
isUsed |= it_row.data()[ m_rowTypeList[i] ][column].isUsed();
}
column++;
}
if ( m_config_f.isShowingRowTotals() )
{
for(unsigned i = 0; i < m_rowTypeList.size(); ++i) {
rowdata += QString("<td%2>%1</td>")
.arg(coloredAmount(it_row.data()[ 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("<tr class=\"row-%1\"%2><td%3 class=\"left\" style=\"text-indent: %4.0em\">%5%6</td>")
.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(" "), "&nbsp;"))
.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 += "</tr>\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("<tr class=\"row-%1\" id=\"subtotal\"><td class=\"left\">&nbsp;&nbsp;%2</td>")
.arg(rownum & 0x01 ? "even" : "odd")
.arg(i18n("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("<tr class=\"row-%1\"%2><td class=\"left\" style=\"text-indent: %3.0em;\">%5%6</td>")
.arg(rownum & 0x01 ? "even" : "odd")
.arg( m_config_f.detailLevel() == MyMoneyReport::eDetailAll ? "id=\"solo\"" : "" )
.arg(rowname.hierarchyDepth() - 1)
.arg(rowname.name().replace(QRegExp(" "), "&nbsp;"))
.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 )
{
unsigned column = 1;
isUsed |= (*it_innergroup).m_total[eActual][0].isUsed();
while ( column < m_numColumns )
{
QString lb;
if(column != 1)
lb = leftborder;
for(unsigned i = 0; i < m_rowTypeList.size(); ++i) {
finalRow += QString("<td%2>%1</td>")
.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(unsigned i = 0; i < m_rowTypeList.size(); ++i) {
finalRow += QString("<td%2>%1</td>")
.arg(coloredAmount((*it_innergroup).m_total[ m_rowTypeList[i] ].m_total))
.arg(i == 0 ? leftborder : QString());
}
}
finalRow += "</tr>\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("<tr class=\"sectionfooter\"><td class=\"left\">%1&nbsp;%2</td>").arg(i18n("Total")).arg((*it_outergroup).m_displayName);
unsigned column = 1;
while ( column < m_numColumns )
{
QString lb;
if(column != 1)
lb = leftborder;
for(unsigned i = 0; i < m_rowTypeList.size(); ++i) {
result += QString("<td%2>%1</td>")
.arg(coloredAmount((*it_outergroup).m_total[ m_rowTypeList[i] ][column]))
.arg(i == 0 ? lb : QString());
}
column++;
}
if ( m_config_f.isShowingRowTotals() )
{
for(unsigned i = 0; i < m_rowTypeList.size(); ++i) {
result += QString("<td%2>%1</td>")
.arg(coloredAmount((*it_outergroup).m_total[ m_rowTypeList[i] ].m_total))
.arg(i == 0 ? leftborder : QString());
}
}
result += "</tr>\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("<tr class=\"spacer\"><td>&nbsp;</td></tr>\n");
result += QString("<tr class=\"reportfooter\"><td class=\"left\">%1</td>").arg(i18n("Grand Total"));
unsigned totalcolumn = 1;
while ( totalcolumn < m_numColumns )
{
QString lb;
if(totalcolumn != 1)
lb = leftborder;
for(unsigned i = 0; i < m_rowTypeList.size(); ++i) {
result += QString("<td%2>%1</td>")
.arg(coloredAmount(m_grid.m_total[ m_rowTypeList[i] ][totalcolumn]))
.arg(i == 0 ? lb : QString());
}
totalcolumn++;
}
if ( m_config_f.isShowingRowTotals() )
{
for(unsigned i = 0; i < m_rowTypeList.size(); ++i) {
result += QString("<td%2>%1</td>")
.arg(coloredAmount(m_grid.m_total[ m_rowTypeList[i] ].m_total))
.arg(i == 0 ? leftborder : QString());
}
}
result += "</tr>\n";
}
result += QString("<tr class=\"spacer\"><td>&nbsp;</td></tr>\n");
result += QString("<tr class=\"spacer\"><td>&nbsp;</td></tr>\n");
result += "</table>\n";
return result;
}
void PivotTable::dump( const QString& file, const QString& /* context */) const
{
QFile g( file );
g.open( IO_WriteOnly );
QTextStream(&g) << renderHTML();
g.close();
}
#ifdef HAVE_KDCHART
void PivotTable::drawChart( KReportChartView& _view ) const
{
#if 1 // make this "#if 1" if you want to play with the axis settings
// not sure if 0 is X and 1 is Y.
KDChartAxisParams xAxisParams, yAxisParams;
KDChartAxisParams::deepCopy(xAxisParams, _view.params()->axisParams(0));
KDChartAxisParams::deepCopy(yAxisParams, _view.params()->axisParams(1));
// modify axis settings here
xAxisParams.setAxisLabelsFontMinSize(12);
xAxisParams.setAxisLabelsFontRelSize(20);
yAxisParams.setAxisLabelsFontMinSize(12);
yAxisParams.setAxisLabelsFontRelSize(20);
_view.params()->setAxisParams( 0, xAxisParams );
_view.params()->setAxisParams( 1, yAxisParams );
#endif
_view.params()->setLegendFontRelSize(20);
_view.params()->setLegendTitleFontRelSize(24);
_view.params()->setLegendTitleText(i18n("Legend"));
_view.params()->setAxisShowGrid(0,m_config_f.isChartGridLines());
_view.params()->setAxisShowGrid(1,m_config_f.isChartGridLines());
_view.params()->setPrintDataValues(m_config_f.isChartDataLabels());
// whether to limit the chart to use series totals only. Used for reports which only
// show one dimension (pie).
bool seriesTotals = 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.
bool accountSeries = true;
//what values should be shown
bool showBudget = m_config_f.hasBudget();
bool showForecast = m_config_f.isIncludingForecast();
bool showActual = false;
if( (m_config_f.isIncludingBudgetActuals()) || ( !showBudget && !showForecast) )
showActual = true;
_view.params()->setLineWidth( m_config_f.chartLineWidth() );
switch( m_config_f.chartType() )
{
case MyMoneyReport::eChartNone:
case MyMoneyReport::eChartEnd:
case MyMoneyReport::eChartLine:
_view.params()->setChartType( KDChartParams::Line );
_view.params()->setAxisDatasets( 0,0 );
break;
case MyMoneyReport::eChartBar:
_view.params()->setChartType( KDChartParams::Bar );
_view.params()->setBarChartSubType( KDChartParams::BarNormal );
break;
case MyMoneyReport::eChartStackedBar:
_view.params()->setChartType( KDChartParams::Bar );
_view.params()->setBarChartSubType( KDChartParams::BarStacked );
break;
case MyMoneyReport::eChartPie:
_view.params()->setChartType( KDChartParams::Pie );
// Charts should only be 3D if this adds any information
_view.params()->setThreeDPies( false );
accountSeries = false;
seriesTotals = true;
break;
case MyMoneyReport::eChartRing:
_view.params()->setChartType( KDChartParams::Ring );
_view.params()->setRelativeRingThickness( true );
accountSeries = false;
break;
}
// For onMouseOver events, we want to activate mouse tracking
_view.setMouseTracking( true );
//
// In KDChart parlance, a 'series' (or row) is an account (or accountgroup, etc)
// and an 'item' (or column) is a month
//
unsigned r;
unsigned c;
if ( accountSeries )
{
r = 1;
c = m_numColumns - 1;
}
else
{
c = 1;
r = m_numColumns - 1;
}
KDChartTableData data( r,c );
// The KReportChartView widget needs to know whether the legend
// corresponds to rows or columns
_view.setAccountSeries( accountSeries );
// Set up X axis labels (ie "abscissa" to use the technical term)
QStringList& abscissaNames = _view.abscissaNames();
abscissaNames.clear();
if ( accountSeries )
{
unsigned column = 1;
while ( column < m_numColumns ) {
abscissaNames += QString(m_columnHeadings[column++]).replace("&nbsp;", " ");
}
}
else
{
// we will set these up while putting in the chart values.
}
switch ( m_config_f.detailLevel() )
{
case MyMoneyReport::eDetailNone:
case MyMoneyReport::eDetailEnd:
case MyMoneyReport::eDetailAll:
{
unsigned rowNum = 0;
// iterate over outer groups
PivotGrid::const_iterator it_outergroup = m_grid.begin();
while ( it_outergroup != m_grid.end() )
{
// iterate over inner groups
PivotOuterGroup::const_iterator it_innergroup = (*it_outergroup).begin();
while ( it_innergroup != (*it_outergroup).end() )
{
//
// Rows
//
QString innergroupdata;
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(unsigned i = 0; i < m_rowTypeList.size(); ++i) {
//skip the budget difference rowset
if(m_rowTypeList[i] != eBudgetDiff ) {
rowNum = drawChartRowSet(rowNum, seriesTotals, accountSeries, data, it_row.data(), m_rowTypeList[i]);
//only show the column type in the header if there is more than one type
if(m_rowTypeList.size() > 1) {
_view.params()->setLegendText( rowNum-1, m_columnTypeHeaderList[i] + " - " + it_row.key().name() );
} else {
_view.params()->setLegendText( rowNum-1, it_row.key().name() );
}
}
}
}
++it_row;
}
++it_innergroup;
}
++it_outergroup;
}
}
break;
case MyMoneyReport::eDetailTop:
{
unsigned rowNum = 0;
// iterate over outer groups
PivotGrid::const_iterator it_outergroup = m_grid.begin();
while ( it_outergroup != m_grid.end() )
{
// iterate over inner groups
PivotOuterGroup::const_iterator it_innergroup = (*it_outergroup).begin();
while ( it_innergroup != (*it_outergroup).end() )
{
//iterate row types
for(unsigned i = 0; i < m_rowTypeList.size(); ++i) {
//skip the budget difference rowset
if(m_rowTypeList[i] != eBudgetDiff ) {
rowNum = drawChartRowSet(rowNum, seriesTotals, accountSeries, data, (*it_innergroup).m_total, m_rowTypeList[i]);
//only show the column type in the header if there is more than one type
if(m_rowTypeList.size() > 1) {
_view.params()->setLegendText( rowNum-1, m_columnTypeHeaderList[i] + " - " + it_innergroup.key() );
} else {
_view.params()->setLegendText( rowNum-1, it_innergroup.key() );
}
}
}
++it_innergroup;
}
++it_outergroup;
}
}
break;
case MyMoneyReport::eDetailGroup:
{
unsigned rowNum = 0;
// iterate over outer groups
PivotGrid::const_iterator it_outergroup = m_grid.begin();
while ( it_outergroup != m_grid.end() )
{
//iterate row types
for(unsigned i = 0; i < m_rowTypeList.size(); ++i) {
//skip the budget difference rowset
if(m_rowTypeList[i] != eBudgetDiff ) {
rowNum = drawChartRowSet(rowNum, seriesTotals, accountSeries, data, (*it_outergroup).m_total, m_rowTypeList[i]);
//only show the column type in the header if there is more than one type
if(m_rowTypeList.size() > 1) {
_view.params()->setLegendText( rowNum-1, m_columnTypeHeaderList[i] + " - " + it_outergroup.key() );
} else {
_view.params()->setLegendText( rowNum-1, it_outergroup.key() );
}
}
}
++it_outergroup;
}
//if selected, show totals too
if (m_config_f.isShowingRowTotals())
{
//iterate row types
for(unsigned i = 0; i < m_rowTypeList.size(); ++i) {
//skip the budget difference rowset
if(m_rowTypeList[i] != eBudgetDiff ) {
rowNum = drawChartRowSet(rowNum, seriesTotals, accountSeries, data, m_grid.m_total, m_rowTypeList[i]);
//only show the column type in the header if there is more than one type
if(m_rowTypeList.size() > 1) {
_view.params()->setLegendText( rowNum-1, m_columnTypeHeaderList[i] + " - " + i18n("Total") );
} else {
_view.params()->setLegendText( rowNum-1, i18n("Total") );
}
}
}
}
}
break;
case MyMoneyReport::eDetailTotal:
{
unsigned rowNum = 0;
//iterate row types
for(unsigned i = 0; i < m_rowTypeList.size(); ++i) {
//skip the budget difference rowset
if(m_rowTypeList[i] != eBudgetDiff ) {
rowNum = drawChartRowSet(rowNum, seriesTotals, accountSeries, data, m_grid.m_total, m_rowTypeList[i]);
//only show the column type in the header if there is more than one type
if(m_rowTypeList.size() > 1) {
_view.params()->setLegendText( rowNum-1, m_columnTypeHeaderList[i] + " - " + i18n("Total") );
} else {
_view.params()->setLegendText( rowNum-1, i18n("Total") );
}
}
}
}
break;
}
_view.setNewData(data);
// make sure to show only the required number of fractional digits on the labels of the graph
_view.params()->setDataValuesCalc(0, MyMoneyMoney::denomToPrec(MyMoneyFile::instance()->baseCurrency().smallestAccountFraction()));
_view.refreshLabels();
#if 0
// I have not been able to get this to work (ace)
//
// Set line to dashed for the future
//
if ( accountSeries )
{
// the first column of report which represents a date in the future, or one past the
// last column if all columns are in the present day. Only relevant when accountSeries==true
unsigned futurecolumn = columnValue(QDate::currentDate()) - columnValue(m_beginDate) + 1;
// kdDebug(2) << "futurecolumn: " << futurecolumn << endl;
// kdDebug(2) << "m_numColumns: " << m_numColumns << endl;
// Properties for line charts whose values are in the future.
KDChartPropertySet propSetFutureValue("future value", KDChartParams::KDCHART_PROPSET_NORMAL_DATA);
propSetFutureValue.setLineStyle(KDChartPropertySet::OwnID, Qt::DotLine);
const int idPropFutureValue = _view.params()->registerProperties(propSetFutureValue);
for(int col = futurecolumn; col < m_numColumns; ++col) {
_view.setProperty(0, col, idPropFutureValue);
}
}
#endif
}
#else
void PivotTable::drawChart( KReportChartView& ) const { }
#endif
unsigned PivotTable::drawChartRowSet(unsigned rowNum, const bool seriesTotals, const bool accountSeries, KDChartTableData& data, const PivotGridRowSet& rowSet, const ERowType rowType ) const
{
//only add a row if one has been added before
// TODO: This is inefficient. Really we should total up how many rows
// there will be and allocate it all at once.
if(rowNum > 0) {
if ( accountSeries )
data.expand( rowNum+1, m_numColumns-1 );
else
data.expand( m_numColumns-1, rowNum+1 );
}
// Columns
if ( seriesTotals )
{
if ( accountSeries )
data.setCell( rowNum, 0, rowSet[rowType].m_total.toDouble() );
else
data.setCell( 0, rowNum, rowSet[rowType].m_total.toDouble() );
}
else
{
unsigned column = 1;
while ( column < m_numColumns )
{
if ( accountSeries )
data.setCell( rowNum, column-1, rowSet[rowType][column].toDouble() );
else
data.setCell( column-1, rowNum, rowSet[rowType][column].toDouble() );
++column;
}
}
return ++rowNum;
}
QString PivotTable::coloredAmount(const MyMoneyMoney& amount, const QString& currencySymbol, int prec) const
{
QString result;
if( amount.isNegative() )
result += QString("<font color=\"rgb(%1,%2,%3)\">")
.arg(KMyMoneyGlobalSettings::listNegativeValueColor().red())
.arg(KMyMoneyGlobalSettings::listNegativeValueColor().green())
.arg(KMyMoneyGlobalSettings::listNegativeValueColor().blue());
result += amount.formatMoney(currencySymbol, prec);
if( amount.isNegative() )
result += QString("</font>");
return result;
}
void PivotTable::calculateBudgetDiff(void)
{
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() )
{
unsigned column = 1;
switch( it_row.key().accountGroup() )
{
case MyMoneyAccount::Income:
case MyMoneyAccount::Asset:
while ( column < m_numColumns ) {
it_row.data()[eBudgetDiff][column] = it_row.data()[eActual][column] - it_row.data()[eBudget][column];
++column;
}
break;
case MyMoneyAccount::Expense:
case MyMoneyAccount::Liability:
while ( column < m_numColumns ) {
it_row.data()[eBudgetDiff][column] = it_row.data()[eBudget][column] - it_row.data()[eActual][column];
++column;
}
break;
default:
break;
}
++it_row;
}
++it_innergroup;
}
++it_outergroup;
}
}
void PivotTable::calculateForecast(void)
{
//setup forecast
MyMoneyForecast forecast;
//setup forecast settings
//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
forecast.doForecast();
//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() )
{
unsigned column = 1;
QDate forecastDate = m_beginDate;
//check whether columns are days or months
if(m_config_f.isColumnsAreDays())
{
while(column < m_numColumns) {
it_row.data()[eForecast][column] = forecast.forecastBalance(it_row.key(), forecastDate);
forecastDate = forecastDate.addDays(1);
++column;
}
} else {
//if columns are months
while(column < m_numColumns) {
//set forecastDate to last day of each month
//TODO we really need a date manipulation util
forecastDate = QDate(forecastDate.year(), forecastDate.month(), forecastDate.daysInMonth());
//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.data()[eForecast][column] = forecast.forecastBalance(it_row.key(), forecastDate);
forecastDate = forecastDate.addDays(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 (void)
{
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() )
{
unsigned column = 1;
//check whether columns are days or months
if(m_config_f.columnType() == MyMoneyReport::eDays) {
while(column < m_numColumns) {
MyMoneyMoney totalPrice = MyMoneyMoney( 0, 1 );
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.data()[eActual][column] * averagePrice;
it_row.data()[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(0, 1);
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.data()[eActual][column] * averagePrice;
//fill in the average
it_row.data()[eAverage][column] = averageValue.convert(10000);
++column;
}
}
++it_row;
}
++it_innergroup;
}
++it_outergroup;
}
}
void PivotTable::fillBasePriceUnit(ERowType rowType)
{
//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() )
{
unsigned column = 1;
while ( column < m_numColumns ) {
//insert a unit of currency for each account
it_row.data() [rowType][column] = MyMoneyMoney ( 1, 1 );
++column;
}
++it_row;
}
++it_innergroup;
}
++it_outergroup;
}
}
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.begin(); it_a != accountList.end(); ++it_a) {
MyMoneyAccount acc = MyMoneyFile::instance()->account(*it_a);
if(acc.accountType() == MyMoneyAccount::Investment) {
for(it_b = acc.accountList().begin(); it_b != acc.accountList().end(); ++it_b) {
if(!accountList.contains(*it_b)) {
m_config_f.addAccount(*it_b);
}
}
}
}
}
}
}
} // namespace
// vim:cin:si:ai:et:ts=2:sw=2: