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.
2605 lines
88 KiB
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 <tqlayout.h>
|
|
#include <tqdatetime.h>
|
|
#include <tqregexp.h>
|
|
#include <tqdragobject.h>
|
|
#include <tqclipboard.h>
|
|
#include <tqapplication.h>
|
|
#include <tqprinter.h>
|
|
#include <tqpainter.h>
|
|
#include <tqfile.h>
|
|
#include <tqdom.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 {
|
|
|
|
TQString Debug::m_sTabs;
|
|
bool Debug::m_sEnabled = DEBUG_ENABLED_BY_DEFAULT;
|
|
TQString Debug::m_sEnableKey;
|
|
|
|
Debug::Debug( const TQString& _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 TQString& _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 = TQDate( m_beginDate.year(), m_beginDate.month(), 1 );
|
|
m_endDate = TQDate( 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
|
|
//
|
|
|
|
TQValueList<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(TQString("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(TQDate(),TQDate());
|
|
|
|
// Get the real dates from the config filter
|
|
TQDate configbegin, configend;
|
|
m_config_f.validDateRange(configbegin, configend);
|
|
|
|
TQValueList<MyMoneySchedule> schedules = file->scheduleList();
|
|
TQValueList<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
|
|
TQDate nextpayment = (*it_schedule).adjustedNextPayment(configbegin);
|
|
if ( nextpayment.isValid() )
|
|
{
|
|
// Add one transaction for each date
|
|
TQValueList<TQDate> paymentDates = (*it_schedule).paymentDates(nextpayment,configend);
|
|
TQValueList<TQDate>::const_iterator it_date = paymentDates.begin();
|
|
while ( it_date != paymentDates.end() )
|
|
{
|
|
//if the payment occurs in the past, enter it tomorrow
|
|
if(TQDate::currentDate() >= *it_date) {
|
|
tx.setPostDate(TQDate::currentDate().addDays(1));
|
|
} else {
|
|
tx.setPostDate(*it_date);
|
|
}
|
|
if ( tx.postDate() <= configend
|
|
&& tx.postDate() >= configbegin ) {
|
|
transactions += tx;
|
|
}
|
|
|
|
DEBUG_OUTPUT(TQString("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
|
|
TQMap<TQString, MyMoneyMoney> loanBalances;
|
|
|
|
TQValueList<MyMoneyTransaction>::const_iterator it_transaction = transactions.begin();
|
|
unsigned colofs = columnValue(m_beginDate) - 1;
|
|
while ( it_transaction != transactions.end() )
|
|
{
|
|
TQDate 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();
|
|
TQString 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())) {
|
|
TQDate dueDate = sched.nextDueDate();
|
|
|
|
//if the payment is overdue, use current date
|
|
if(dueDate < TQDate::currentDate())
|
|
dueDate = TQDate::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 )) {
|
|
TQValueList<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();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
TQValueList<MyMoneySplit> splits = tx.splits();
|
|
TQValueList<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();
|
|
TQString 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(TQString("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(TQString("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(TQString("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(TQString("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(TQString("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 )
|
|
{
|
|
TQDate columnDate = m_beginDate;
|
|
unsigned column = 1;
|
|
while ( column++ < m_numColumns )
|
|
{
|
|
TQString heading = KGlobal::locale()->calendar()->monthName(columnDate.month(), columnDate.year(), true) + " " + TQString::number(columnDate.day());
|
|
columnDate = columnDate.addDays(1);
|
|
m_columnHeadings.append( heading);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
TQDate day = m_beginDate;
|
|
TQDate 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(TQString("%1 %2 - %3 %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(TQString::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 )
|
|
{
|
|
TQString 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 += " " + TQString::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();
|
|
|
|
TQValueList<MyMoneyAccount> accounts;
|
|
file->accountList(accounts);
|
|
|
|
TQValueList<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(TQString("Includes account %1").arg(account.name()));
|
|
|
|
// the row group is the account class (major account type)
|
|
TQString 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.
|
|
TQDate from = m_config_f.fromDate();
|
|
TQDate to = m_config_f.toDate();
|
|
if ( ! from.isValid() )
|
|
from = m_beginDate;
|
|
if ( ! to.isValid() )
|
|
to = m_endDate;
|
|
|
|
MyMoneyFile* file = MyMoneyFile::instance();
|
|
|
|
TQValueList<MyMoneyAccount> accounts;
|
|
file->accountList(accounts);
|
|
|
|
TQValueList<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);
|
|
TQValueList<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(TQString("DOES NOT INCLUDE account %1").arg(account.name()));
|
|
++it_account;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
DEBUG_OUTPUT(TQString("Includes account %1").arg(account.name()));
|
|
// the row group is the account class (major account type)
|
|
TQString 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(TQString("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(TQString("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(TQString("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 TQString& 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(TQString("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() )
|
|
{
|
|
TQString 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
|
|
TQString innergroup( row.topParentName() );
|
|
|
|
if ( m_numColumns <= _column )
|
|
throw new MYMONEYEXCEPTION(TQString("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(TQString("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(TQString("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" ) {
|
|
TQValueList<MyMoneyBudget> budgets = file->budgetList();
|
|
TQValueList<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() == TQDate::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
|
|
TQValueList<MyMoneyAccount> accounts;
|
|
file->accountList(accounts);
|
|
TQValueList<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 ) ) {
|
|
TQString id = ( *it_account ).id();
|
|
TQString 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
|
|
TQValueList<MyMoneyBudget::AccountGroup> baccounts = budget.getaccounts();
|
|
TQValueList<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();
|
|
TQString 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 TQMap<TQDate, 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);
|
|
|
|
TQDate 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
|
|
{
|
|
TQMap<TQDate, 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(TQString("Column %1 out of grid range (%2) in PivotTable::convertToBaseCurrency").arg(column).arg(it_row.data()[eActual].count()));
|
|
|
|
TQDate 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) ,TQString("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(TQString("Column %1 out of grid range (%2) in PivotTable::convertToDeepCurrency").arg(column).arg(it_row.data()[eActual].count()));
|
|
|
|
TQDate 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) ,TQString("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(TQString("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(TQString("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(TQString("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(TQString("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(TQString("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(TQString("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 TQString& outergroup, const ReportAccount& _row, unsigned column, MyMoneyMoney value, bool budget, bool stockSplit )
|
|
{
|
|
DEBUG_ENTER(__PRETTY_FUNCTION__);
|
|
DEBUG_OUTPUT(TQString("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() )
|
|
{
|
|
TQString 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
|
|
TQString innergroup( row.topParentName() );
|
|
|
|
if ( m_numColumns <= column )
|
|
throw new MYMONEYEXCEPTION(TQString("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(TQString("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 TQString& outergroup, const ReportAccount& row, bool recursive )
|
|
{
|
|
DEBUG_ENTER(__PRETTY_FUNCTION__);
|
|
|
|
// Determine the inner group from the top-most parent account
|
|
TQString innergroup( row.topParentName() );
|
|
|
|
if ( ! m_grid.contains(outergroup) )
|
|
{
|
|
DEBUG_OUTPUT(TQString("Adding group [%1]").arg(outergroup));
|
|
m_grid[outergroup] = PivotOuterGroup(m_numColumns);
|
|
}
|
|
|
|
if ( ! m_grid[outergroup].contains(innergroup) )
|
|
{
|
|
DEBUG_OUTPUT(TQString("Adding group [%1][%2]").arg(outergroup).arg(innergroup));
|
|
m_grid[outergroup][innergroup] = PivotInnerGroup(m_numColumns);
|
|
}
|
|
|
|
if ( ! m_grid[outergroup][innergroup].contains(row) )
|
|
{
|
|
DEBUG_OUTPUT(TQString("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 TQDate& _date) const
|
|
{
|
|
if (m_config_f.isColumnsAreDays())
|
|
return (TQDate().daysTo(_date));
|
|
else
|
|
return (_date.year() * 12 + _date.month());
|
|
}
|
|
|
|
TQDate 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);
|
|
}
|
|
|
|
TQString PivotTable::renderCSV( void ) const
|
|
{
|
|
DEBUG_ENTER(__PRETTY_FUNCTION__);
|
|
|
|
//
|
|
// Report Title
|
|
//
|
|
|
|
TQString result = TQString("\"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 += TQString(",%1").arg(TQString(m_columnHeadings[column++]));
|
|
|
|
if ( m_config_f.isShowingRowTotals() )
|
|
result += TQString(",%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
|
|
//
|
|
|
|
TQString 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
|
|
//
|
|
|
|
TQString 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 += TQString(",\"%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 += TQString(",\"%1\"").arg((*it_row)[ m_rowTypeList[i] ].m_total.formatMoney(fraction, false));
|
|
}
|
|
|
|
//
|
|
// Row Header
|
|
//
|
|
|
|
if(!rowname.isClosed() || isUsed) {
|
|
innergroupdata += "\"" + TQString().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 += TQString(" (%1)").arg(rowname.currencyId());
|
|
|
|
innergroupdata += "\"";
|
|
|
|
if ( isUsed )
|
|
innergroupdata += rowdata;
|
|
|
|
innergroupdata += "\n";
|
|
}
|
|
++it_row;
|
|
}
|
|
|
|
//
|
|
// Inner Row Group Totals
|
|
//
|
|
|
|
bool finishrow = true;
|
|
TQString 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 = "\"" + TQString().fill(' ',rowname.hierarchyDepth() - 1) + rowname.name();
|
|
if (!m_config_f.isConvertCurrency() && rowname.isForeignCurrency() )
|
|
finalRow += TQString(" (%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 += TQString(",\"%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 += TQString(",\"%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 += TQString("%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 += TQString(",\"%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 += TQString(",\"%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 += TQString(",\"%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 += TQString(",\"%1\"").arg(m_grid.m_total[ m_rowTypeList[i] ].m_total.formatMoney(fraction, false));
|
|
}
|
|
|
|
result += "\n";
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
TQString PivotTable::renderHTML( void ) const
|
|
{
|
|
DEBUG_ENTER(__PRETTY_FUNCTION__);
|
|
|
|
TQString colspan = TQString(" colspan=\"%1\"").arg(m_numColumns + 1 + (m_config_f.isShowingRowTotals() ? 1 : 0) );
|
|
|
|
//
|
|
// Report Title
|
|
//
|
|
|
|
TQString result = TQString("<h2 class=\"report\">%1</h2>\n").arg(m_config_f.name());
|
|
|
|
//actual dates of the report
|
|
result += TQString("<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 += TQString("</div>\n");
|
|
result += TQString("<div class=\"gap\"> </div>\n");
|
|
|
|
//currency conversion message
|
|
result += TQString("<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 += TQString("</div>\n");
|
|
result += TQString("<div class=\"gap\"> </div>\n");
|
|
|
|
// setup a leftborder for better readability of budget vs actual reports
|
|
TQString leftborder;
|
|
if (m_rowTypeList.size() > 1)
|
|
leftborder = " class=\"leftborder\"";
|
|
|
|
//
|
|
// Table Header
|
|
//
|
|
result += TQString("\n\n<table class=\"report\" cellspacing=\"0\">\n"
|
|
"<thead><tr class=\"itemheader\">\n<th>%1</th>").arg(i18n("Account"));
|
|
|
|
TQString headerspan;
|
|
int span = m_rowTypeList.size();
|
|
|
|
headerspan = TQString(" colspan=\"%1\"").arg(span);
|
|
|
|
unsigned column = 1;
|
|
while ( column < m_numColumns )
|
|
result += TQString("<th%1>%2</th>").arg(headerspan,TQString(m_columnHeadings[column++]).replace(TQRegExp(" "),"<br>"));
|
|
|
|
if ( m_config_f.isShowingRowTotals() )
|
|
result += TQString("<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 )
|
|
{
|
|
TQString lb;
|
|
if(column != 1)
|
|
lb = leftborder;
|
|
|
|
for(unsigned i = 0; i < m_rowTypeList.size(); ++i) {
|
|
result += TQString("<td%2>%1</td>")
|
|
.arg(i18n( m_columnTypeHeaderList[i] ))
|
|
.arg(i == 0 ? lb : TQString() );
|
|
}
|
|
column++;
|
|
}
|
|
if ( m_config_f.isShowingRowTotals() ) {
|
|
for(unsigned i = 0; i < m_rowTypeList.size(); ++i) {
|
|
result += TQString("<td%2>%1</td>")
|
|
.arg(i18n( m_columnTypeHeaderList[i] ))
|
|
.arg(i == 0 ? leftborder : TQString() );
|
|
}
|
|
}
|
|
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.
|
|
TQValueList<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);
|
|
|
|
TQValueList<PivotOuterGroup>::const_iterator it_outergroup = outergroups.begin();
|
|
while ( it_outergroup != outergroups.end() )
|
|
{
|
|
//
|
|
// Outer Group Header
|
|
//
|
|
|
|
result += TQString("<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
|
|
//
|
|
|
|
TQString innergroupdata;
|
|
PivotInnerGroup::const_iterator it_row = (*it_innergroup).begin();
|
|
while ( it_row != (*it_innergroup).end() )
|
|
{
|
|
//
|
|
// Columns
|
|
//
|
|
|
|
TQString rowdata;
|
|
unsigned column = 1;
|
|
bool isUsed = it_row.data()[eActual][0].isUsed();
|
|
while ( column < m_numColumns )
|
|
{
|
|
TQString lb;
|
|
if(column != 1)
|
|
lb = leftborder;
|
|
|
|
for(unsigned i = 0; i < m_rowTypeList.size(); ++i) {
|
|
rowdata += TQString("<td%2>%1</td>")
|
|
.arg(coloredAmount(it_row.data()[ m_rowTypeList[i] ][column]))
|
|
.arg(i == 0 ? lb : TQString());
|
|
|
|
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 += TQString("<td%2>%1</td>")
|
|
.arg(coloredAmount(it_row.data()[ m_rowTypeList[i] ].m_total))
|
|
.arg(i == 0 ? leftborder : TQString());
|
|
}
|
|
}
|
|
|
|
//
|
|
// Row Header
|
|
//
|
|
|
|
ReportAccount rowname = it_row.key();
|
|
|
|
// don't show closed accounts if they have not been used
|
|
if(!rowname.isClosed() || isUsed) {
|
|
innergroupdata += TQString("<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(TQRegExp(" "), " "))
|
|
.arg((m_config_f.isConvertCurrency() || !rowname.isForeignCurrency() )?TQString():TQString(" (%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;
|
|
TQString 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 = TQString("<tr class=\"row-%1\" id=\"subtotal\"><td class=\"left\"> %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 = TQString("<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(TQRegExp(" "), " "))
|
|
.arg((m_config_f.isConvertCurrency() || !rowname.isForeignCurrency() )?TQString():TQString(" (%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 )
|
|
{
|
|
TQString lb;
|
|
if(column != 1)
|
|
lb = leftborder;
|
|
|
|
for(unsigned i = 0; i < m_rowTypeList.size(); ++i) {
|
|
finalRow += TQString("<td%2>%1</td>")
|
|
.arg(coloredAmount((*it_innergroup).m_total[ m_rowTypeList[i] ][column]))
|
|
.arg(i == 0 ? lb : TQString());
|
|
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 += TQString("<td%2>%1</td>")
|
|
.arg(coloredAmount((*it_innergroup).m_total[ m_rowTypeList[i] ].m_total))
|
|
.arg(i == 0 ? leftborder : TQString());
|
|
}
|
|
}
|
|
|
|
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 += TQString("<tr class=\"sectionfooter\"><td class=\"left\">%1 %2</td>").arg(i18n("Total")).arg((*it_outergroup).m_displayName);
|
|
unsigned column = 1;
|
|
while ( column < m_numColumns )
|
|
{
|
|
TQString lb;
|
|
if(column != 1)
|
|
lb = leftborder;
|
|
|
|
for(unsigned i = 0; i < m_rowTypeList.size(); ++i) {
|
|
result += TQString("<td%2>%1</td>")
|
|
.arg(coloredAmount((*it_outergroup).m_total[ m_rowTypeList[i] ][column]))
|
|
.arg(i == 0 ? lb : TQString());
|
|
}
|
|
|
|
column++;
|
|
}
|
|
|
|
if ( m_config_f.isShowingRowTotals() )
|
|
{
|
|
for(unsigned i = 0; i < m_rowTypeList.size(); ++i) {
|
|
result += TQString("<td%2>%1</td>")
|
|
.arg(coloredAmount((*it_outergroup).m_total[ m_rowTypeList[i] ].m_total))
|
|
.arg(i == 0 ? leftborder : TQString());
|
|
}
|
|
}
|
|
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 += TQString("<tr class=\"spacer\"><td> </td></tr>\n");
|
|
result += TQString("<tr class=\"reportfooter\"><td class=\"left\">%1</td>").arg(i18n("Grand Total"));
|
|
unsigned totalcolumn = 1;
|
|
while ( totalcolumn < m_numColumns )
|
|
{
|
|
TQString lb;
|
|
if(totalcolumn != 1)
|
|
lb = leftborder;
|
|
|
|
for(unsigned i = 0; i < m_rowTypeList.size(); ++i) {
|
|
result += TQString("<td%2>%1</td>")
|
|
.arg(coloredAmount(m_grid.m_total[ m_rowTypeList[i] ][totalcolumn]))
|
|
.arg(i == 0 ? lb : TQString());
|
|
}
|
|
|
|
totalcolumn++;
|
|
}
|
|
|
|
if ( m_config_f.isShowingRowTotals() )
|
|
{
|
|
for(unsigned i = 0; i < m_rowTypeList.size(); ++i) {
|
|
result += TQString("<td%2>%1</td>")
|
|
.arg(coloredAmount(m_grid.m_total[ m_rowTypeList[i] ].m_total))
|
|
.arg(i == 0 ? leftborder : TQString());
|
|
}
|
|
}
|
|
|
|
result += "</tr>\n";
|
|
}
|
|
|
|
result += TQString("<tr class=\"spacer\"><td> </td></tr>\n");
|
|
result += TQString("<tr class=\"spacer\"><td> </td></tr>\n");
|
|
result += "</table>\n";
|
|
|
|
return result;
|
|
}
|
|
|
|
void PivotTable::dump( const TQString& file, const TQString& /* context */) const
|
|
{
|
|
TQFile g( file );
|
|
g.open( IO_WriteOnly );
|
|
TQTextStream(&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)
|
|
TQStringList& abscissaNames = _view.abscissaNames();
|
|
abscissaNames.clear();
|
|
if ( accountSeries )
|
|
{
|
|
unsigned column = 1;
|
|
while ( column < m_numColumns ) {
|
|
abscissaNames += TQString(m_columnHeadings[column++]).replace(" ", " ");
|
|
}
|
|
}
|
|
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
|
|
//
|
|
TQString 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(TQDate::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, TQt::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;
|
|
}
|
|
|
|
TQString PivotTable::coloredAmount(const MyMoneyMoney& amount, const TQString& currencySymbol, int prec) const
|
|
{
|
|
TQString result;
|
|
if( amount.isNegative() )
|
|
result += TQString("<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 += TQString("</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 > TQDate::currentDate()) {
|
|
forecast.setForecastEndDate(m_endDate);
|
|
forecast.setForecastStartDate(TQDate::currentDate());
|
|
forecast.setForecastDays(TQDate::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 < TQDate::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;
|
|
TQDate 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 = TQDate(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 );
|
|
|
|
TQDate averageStart = columnDate(column).addDays(-delta);
|
|
TQDate averageEnd = columnDate(column).addDays(delta);
|
|
for(TQDate 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) {
|
|
TQDate averageStart = columnDate(column);
|
|
|
|
//set the right start date depending on the column type
|
|
switch(m_config_f.columnType()) {
|
|
case MyMoneyReport::eYears:
|
|
{
|
|
averageStart = TQDate(columnDate(column).year(), 1, 1);
|
|
break;
|
|
}
|
|
case MyMoneyReport::eBiMonths:
|
|
{
|
|
averageStart = TQDate(columnDate(column).year(), columnDate(column).month(), 1).addMonths(-1);
|
|
break;
|
|
}
|
|
case MyMoneyReport::eQuarters:
|
|
{
|
|
averageStart = TQDate(columnDate(column).year(), columnDate(column).month(), 1).addMonths(-1);
|
|
break;
|
|
}
|
|
case MyMoneyReport::eMonths:
|
|
{
|
|
averageStart = TQDate(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);
|
|
TQDate averageEnd = columnDate(column);
|
|
for(TQDate 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
|
|
TQStringList accountList;
|
|
if(m_config_f.accounts(accountList)) {
|
|
if(!KMyMoneyGlobalSettings::expertMode()) {
|
|
TQStringList::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:
|