summaryrefslogtreecommitdiffstats
path: root/kmymoney2/reports
diff options
context:
space:
mode:
authortpearson <tpearson@283d02a7-25f6-0310-bc7c-ecb5cbfe19da>2011-07-04 22:38:03 +0000
committertpearson <tpearson@283d02a7-25f6-0310-bc7c-ecb5cbfe19da>2011-07-04 22:38:03 +0000
commitdadc34655c3ab961b0b0b94a10eaaba710f0b5e8 (patch)
tree99e72842fe687baea16376a147619b6048d7e441 /kmymoney2/reports
downloadkmymoney-dadc34655c3ab961b0b0b94a10eaaba710f0b5e8.tar.gz
kmymoney-dadc34655c3ab961b0b0b94a10eaaba710f0b5e8.zip
Added kmymoney
git-svn-id: svn://anonsvn.kde.org/home/kde/branches/trinity/applications/kmymoney@1239792 283d02a7-25f6-0310-bc7c-ecb5cbfe19da
Diffstat (limited to 'kmymoney2/reports')
-rw-r--r--kmymoney2/reports/Makefile.am16
-rw-r--r--kmymoney2/reports/kreportchartview.cpp210
-rw-r--r--kmymoney2/reports/kreportchartview.h95
-rw-r--r--kmymoney2/reports/kreportsviewtest.h91
-rw-r--r--kmymoney2/reports/listtable.cpp633
-rw-r--r--kmymoney2/reports/listtable.h121
-rw-r--r--kmymoney2/reports/objectinfotable.cpp368
-rw-r--r--kmymoney2/reports/objectinfotable.h75
-rw-r--r--kmymoney2/reports/pivotgrid.cpp161
-rw-r--r--kmymoney2/reports/pivotgrid.h151
-rw-r--r--kmymoney2/reports/pivotgridtest.cpp198
-rw-r--r--kmymoney2/reports/pivotgridtest.h47
-rw-r--r--kmymoney2/reports/pivottable.cpp2604
-rw-r--r--kmymoney2/reports/pivottable.h356
-rw-r--r--kmymoney2/reports/pivottabletest.cpp1021
-rw-r--r--kmymoney2/reports/pivottabletest.h75
-rw-r--r--kmymoney2/reports/querytable.cpp1522
-rw-r--r--kmymoney2/reports/querytable.h142
-rw-r--r--kmymoney2/reports/querytabletest.cpp694
-rw-r--r--kmymoney2/reports/querytabletest.h53
-rw-r--r--kmymoney2/reports/reportaccount.cpp355
-rw-r--r--kmymoney2/reports/reportaccount.h238
-rw-r--r--kmymoney2/reports/reportdebug.h83
-rw-r--r--kmymoney2/reports/reportstestcommon.cpp494
-rw-r--r--kmymoney2/reports/reportstestcommon.h133
-rw-r--r--kmymoney2/reports/reporttable.h54
26 files changed, 9990 insertions, 0 deletions
diff --git a/kmymoney2/reports/Makefile.am b/kmymoney2/reports/Makefile.am
new file mode 100644
index 0000000..d6d050f
--- /dev/null
+++ b/kmymoney2/reports/Makefile.am
@@ -0,0 +1,16 @@
+KDE_OPTIONS = noautodist
+
+INCLUDES = $(all_includes) -I.. -I$(top_srcdir) -I. -I$(top_srcdir)/libkdchart
+
+noinst_LIBRARIES = libreports.a
+libreports_a_METASOURCES = AUTO
+
+libreports_a_SOURCES = pivotgrid.cpp pivottable.cpp listtable.cpp querytable.cpp objectinfotable.cpp reportaccount.cpp kreportchartview.cpp
+
+noinst_HEADERS = kreportchartview.h kreportsviewtest.h pivotgrid.h pivottable.h pivottabletest.h pivotgridtest.h listtable.h querytable.h querytabletest.h objectinfotable.h reportaccount.h reportdebug.h reportstestcommon.h kreportchartview.h reporttable.h
+
+if CPPUNIT
+check_LIBRARIES = libreportstest.a
+
+libreportstest_a_SOURCES = reportstestcommon.cpp pivottabletest.cpp pivotgridtest.cpp querytabletest.cpp
+endif
diff --git a/kmymoney2/reports/kreportchartview.cpp b/kmymoney2/reports/kreportchartview.cpp
new file mode 100644
index 0000000..21b08fa
--- /dev/null
+++ b/kmymoney2/reports/kreportchartview.cpp
@@ -0,0 +1,210 @@
+/***************************************************************************
+ kreportchartview.cpp
+ -------------------
+ begin : Sun Aug 14 2005
+ copyright : (C) 2004-2005 by Ace Jones
+ email : <ace.j@hotpop.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. *
+ * *
+ ***************************************************************************/
+
+#ifdef HAVE_CONFIG_H
+#include "../../config.h"
+#endif
+#ifdef HAVE_KDCHART
+
+// ----------------------------------------------------------------------------
+// QT Includes
+
+// ----------------------------------------------------------------------------
+// KDE Includes
+
+// ----------------------------------------------------------------------------
+// Project Includes
+
+#include "kreportchartview.h"
+#include <KDChartDataRegion.h>
+
+using namespace reports;
+
+KReportChartView::KReportChartView( QWidget* parent, const char* name ): KDChartWidget(parent,name)
+{
+ // ********************************************************************
+ // Set KMyMoney's Chart Parameter Defaults
+ // ********************************************************************
+ this->setPaletteBackgroundColor( Qt::white );
+
+ KDChartParams* _params = new KDChartParams();
+ _params->setChartType( KDChartParams::Line );
+ _params->setAxisLabelStringParams( KDChartAxisParams::AxisPosBottom,&m_abscissaNames,0);
+ _params->setDataSubduedColors();
+
+ /**
+ // use line marker, but only circles.
+ _params->setLineMarker( true );
+ _params->setLineMarkerSize( QSize(8,8) );
+ _params->setLineMarkerStyle( 0, KDChartParams::LineMarkerCircle );
+ _params->setLineMarkerStyle( 1, KDChartParams::LineMarkerCircle );
+ _params->setLineMarkerStyle( 2, KDChartParams::LineMarkerCircle );
+ **/
+
+ // initialize parameters
+ this->setParams(_params);
+
+ // initialize data
+ KDChartTableData* _data = new KDChartTableData();
+ this->setData(_data);
+
+ // ********************************************************************
+ // Some Examplatory Chart Table Data
+ // ********************************************************************
+
+ /**
+ // 1st series
+ this->data()->setCell( 0, 0, 17.5 );
+ this->data()->setCell( 0, 1, 125 ); // highest value
+ this->data()->setCell( 0, 2, 6.67 ); // lowest value
+ this->data()->setCell( 0, 3, 33.333 );
+ this->data()->setCell( 0, 4, 30 );
+ // 2nd series
+ this->data()->setCell( 1, 0, 40 );
+ this->data()->setCell( 1, 1, 40 );
+ this->data()->setCell( 1, 2, 45.5 );
+ this->data()->setCell( 1, 3, 45 );
+ this->data()->setCell( 1, 4, 35 );
+ // 3rd series
+ this->data()->setCell( 2, 0, 25 );
+ // missing value: setCell( 2, 1, 25 );
+ this->data()->setCell( 2, 2, 30 );
+ this->data()->setCell( 2, 3, 45 );
+ this->data()->setCell( 2, 4, 40 );
+ **/
+
+ // ********************************************************************
+ // Tooltip Setup
+ // ********************************************************************
+ label = new QLabel( this );
+ label->hide();
+ // mouse tracking on will force the mouseMoveEvent() method to be called from Qt
+ label->setMouseTracking( true );
+ label->setFrameStyle( QFrame::PopupPanel | QFrame::Raised );
+ label->setAlignment( AlignRight );
+ label->setAutoResize( true );
+}
+
+/**
+ * This function implements mouseMoveEvents
+ */
+void KReportChartView::mouseMoveEvent( QMouseEvent* event )
+{
+ QPoint translate, pos; // some movement helpers
+ uint dataset; // the current dataset (eg. category)
+ uint datasets; // the total number of datasets
+ double value; // the value of the region
+ double pivot_sum; // the sum over all categories in the current pivot point
+
+ // the data region in which the cursor was last time
+ static uint previous;
+
+ // if mouse tracking is disabled, don't show any tooltip
+ if ( !this->hasMouseTracking() )
+ return ;
+
+ // find the data region below the current mouse location
+ // ..by going through every data region and checking whether it
+ // contains the mouse pointer
+ KDChartDataRegion* current = 0;
+ QPtrListIterator < KDChartDataRegion > it( *(this->dataRegions()) );
+ while ( ( current = it.current() ) ) {
+ ++it;
+ if ( current->contains( event->pos() ) )
+ {
+ // we found the data region that contains the mouse
+ value = this->data()->cellVal(current->row, current->col).toDouble();
+
+ // get the dataset that the region corresponds to
+ if ( this->getAccountSeries() )
+ {
+ dataset = current->row;
+ datasets= this->data()->rows();
+ pivot_sum = value * 100.0 / this->data()->colSum(current->col);
+ }
+ else
+ {
+ dataset = current->col;
+ datasets= this->data()->cols();
+ pivot_sum = value * 100.0 / this->data()->rowSum(current->row);
+ }
+
+ // if we entered a new data region or the label was invisible
+ if ( !label->isVisible() || previous != dataset )
+ {
+ // if there is more than one dataset, show percentage
+ if(datasets > 1)
+ {
+ // set the tooltip text
+ label->setText(QString("<h2>%1</h2><strong>%2</strong><br>(%3\%)")
+ .arg(this->params()->legendText( dataset ))
+ .arg(value, 0, 'f', 2)
+ .arg(pivot_sum, 0, 'f', 2)
+ );
+ }
+ else // if there is only one dataset, don't show percentage
+ {
+ // set the tooltip text
+ label->setText(QString("<h2>%1</h2><strong>%2</strong>")
+ .arg(this->params()->legendText( dataset ))
+ .arg(value, 0, 'f', 2)
+ );
+ }
+
+ previous = dataset;
+ }
+
+ translate.setX( -10 - label->width());
+ translate.setY( 20);
+
+ // display the label near the cursor
+ pos = event->pos() + translate;
+
+ // but don't let the label leave the visible area
+ if( pos.x() < 0 )
+ pos.setX(0);
+ if( pos.y() < 0 )
+ pos.setY(0);
+ if( pos.x() + label->width() > this->width() )
+ pos.setX( this->width() - label->width() );
+ if( pos.y() + label->height() > this->height() )
+ pos.setY( this->height() - label->height() );
+
+ // now set the label position and show the label
+ label->move( pos );
+ label->show();
+
+ // In a more abstract class, we would emit a dateMouseMove event:
+ //emit this->dataMouseMove( event->pos(), current->row, current->col );
+
+ return ;
+ }
+ }
+ // if the cursor was not found in any data region, hide the label
+ label->hide();
+}
+
+void KReportChartView::setProperty(int row, int col, int id)
+{
+#ifdef HAVE_KDCHART_SETPROP
+ this->data()->setProp(row, col, id);
+#else
+ this->data()->cell(row, col).setPropertySet(id);
+#endif
+}
+
+#endif
diff --git a/kmymoney2/reports/kreportchartview.h b/kmymoney2/reports/kreportchartview.h
new file mode 100644
index 0000000..a1bf786
--- /dev/null
+++ b/kmymoney2/reports/kreportchartview.h
@@ -0,0 +1,95 @@
+/***************************************************************************
+ kreportchartview.h
+ -------------------
+ begin : Sat May 22 2004
+ copyright : (C) 2004-2005 by Ace Jones
+ email : <ace.j@hotpop.com>
+ Thomas Baumgart <ipwizard@users.sourceforge.net>
+ ***************************************************************************/
+
+/***************************************************************************
+ * *
+ * This program is free software; you can redistribute it and/or modify *
+ * it under the terms of the GNU General Public License as published by *
+ * the Free Software Foundation; either version 2 of the License, or *
+ * (at your option) any later version. *
+ * *
+ ***************************************************************************/
+
+#ifndef KREPORTCHARTVIEW_H
+#define KREPORTCHARTVIEW_H
+
+#ifdef HAVE_CONFIG_H
+#include "../../config.h"
+#endif
+#ifdef HAVE_KDCHART
+
+// ----------------------------------------------------------------------------
+// QT Includes
+
+// ----------------------------------------------------------------------------
+// KDE Includes
+// Some STL headers in GCC4.3 contain operator new. Memory checker mangles these
+#ifdef _CHECK_MEMORY
+ #undef new
+#endif
+
+#include <qlabel.h>
+#include <KDChartWidget.h>
+#include <KDChartTable.h>
+#include <KDChartParams.h>
+#include <KDChartAxisParams.h>
+
+// ----------------------------------------------------------------------------
+// Project Includes
+#ifdef _CHECK_MEMORY
+ #include <kmymoney/mymoneyutils.h>
+#endif
+
+namespace reports {
+
+class KReportChartView: public KDChartWidget
+{
+public:
+ KReportChartView( QWidget* parent, const char* name );
+ ~KReportChartView() {}
+ static bool implemented(void) { return true; }
+ void setNewData( const KDChartTableData& newdata ) { this->setData(new KDChartTableData(newdata)); }
+ QStringList& abscissaNames(void) { return m_abscissaNames; }
+ void refreshLabels(void) { this->params()->setAxisLabelStringParams( KDChartAxisParams::AxisPosBottom,&m_abscissaNames,0); }
+ void setProperty(int row, int col, int id);
+// void setCircularLabels(void) { this->params()->setAxisLabelStringParams( KDChartAxisParams::AxisPosCircular,&m_abscissaNames,0); }
+
+ void setAccountSeries(bool accountSeries) {_accountSeries = accountSeries; }
+ bool getAccountSeries(void) {return _accountSeries; }
+
+protected:
+ virtual void mouseMoveEvent( QMouseEvent* event );
+
+private:
+ QStringList m_abscissaNames;
+ bool _accountSeries;
+
+ // label to display when hovering on a data region
+ QLabel *label;
+};
+
+} // end namespace reports
+
+#else
+
+namespace reports {
+
+class KReportChartView : public QWidget
+{
+public:
+ KReportChartView( QWidget* parent, const char* name ): QWidget(parent,name) {}
+ ~KReportChartView() {}
+ static bool implemented(void) { return false; }
+};
+
+} // end namespace reports
+
+#endif
+
+#endif // KREPORTCHARTVIEW_H
diff --git a/kmymoney2/reports/kreportsviewtest.h b/kmymoney2/reports/kreportsviewtest.h
new file mode 100644
index 0000000..70660c4
--- /dev/null
+++ b/kmymoney2/reports/kreportsviewtest.h
@@ -0,0 +1,91 @@
+/***************************************************************************
+ mymoneyaccounttest.h
+ -------------------
+ copyright : (C) 2002 by Thomas Baumgart
+ email : ipwizard@users.sourceforge.net
+ Ace Jones <ace.jones@hotpop.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. *
+ * *
+ ***************************************************************************/
+
+#ifndef __KREPORTSVIEWTEST_H__
+#define __KREPORTSVIEWTEST_H__
+
+#include <cppunit/extensions/HelperMacros.h>
+#include "../mymoney/mymoneyfile.h"
+#include "../mymoney/storage/mymoneyseqaccessmgr.h"
+
+class KReportsViewTest : public CppUnit::TestFixture {
+ CPPUNIT_TEST_SUITE(KReportsViewTest);
+ CPPUNIT_TEST(testNetWorthSingle);
+ CPPUNIT_TEST(testNetWorthOfsetting);
+ CPPUNIT_TEST(testNetWorthOpeningPrior);
+ CPPUNIT_TEST(testNetWorthDateFilter);
+ CPPUNIT_TEST(testSpendingEmpty);
+ CPPUNIT_TEST(testSingleTransaction);
+ CPPUNIT_TEST(testSubAccount);
+ CPPUNIT_TEST(testFilterIEvsIE);
+ CPPUNIT_TEST(testFilterALvsAL);
+ CPPUNIT_TEST(testFilterALvsIE);
+ CPPUNIT_TEST(testFilterAllvsIE);
+ CPPUNIT_TEST(testFilterBasics);
+ CPPUNIT_TEST(testMultipleCurrencies);
+ CPPUNIT_TEST(testAdvancedFilter);
+ CPPUNIT_TEST(testColumnType);
+ CPPUNIT_TEST(testXMLWrite);
+ CPPUNIT_TEST(testQueryBasics);
+ CPPUNIT_TEST(testCashFlowAnalysis);
+ CPPUNIT_TEST(testAccountQuery);
+ CPPUNIT_TEST(testInvestment);
+#ifdef USE_OFX_DIRECTCONNECT
+ CPPUNIT_TEST(testOfxImport);
+#endif
+ CPPUNIT_TEST(testWebQuotes);
+ CPPUNIT_TEST(testDateFormat);
+ CPPUNIT_TEST(testHasReferenceTo);
+ CPPUNIT_TEST_SUITE_END();
+
+private:
+ MyMoneyAccount *m;
+
+ MyMoneySeqAccessMgr* storage;
+ MyMoneyFile* file;
+
+public:
+ KReportsViewTest();
+ void setUp ();
+ void tearDown ();
+ void testNetWorthSingle();
+ void testNetWorthOfsetting();
+ void testNetWorthOpeningPrior();
+ void testNetWorthDateFilter();
+ void testSpendingEmpty();
+ void testSingleTransaction();
+ void testSubAccount();
+ void testFilterIEvsIE();
+ void testFilterALvsAL();
+ void testFilterALvsIE();
+ void testFilterAllvsIE();
+ void testFilterBasics();
+ void testMultipleCurrencies();
+ void testAdvancedFilter();
+ void testColumnType();
+ void testXMLWrite();
+ void testQueryBasics();
+ void testCashFlowAnalysis();
+ void testAccountQuery();
+ void testOfxImport();
+ void testInvestment();
+ void testWebQuotes();
+ void testDateFormat();
+ void testHasReferenceTo();
+};
+
+#endif
diff --git a/kmymoney2/reports/listtable.cpp b/kmymoney2/reports/listtable.cpp
new file mode 100644
index 0000000..797b392
--- /dev/null
+++ b/kmymoney2/reports/listtable.cpp
@@ -0,0 +1,633 @@
+/***************************************************************************
+ listtable.cpp
+ -------------------
+ begin : Sat 28 jun 2008
+ copyright : (C) 2004-2005 by Ace Jones
+ 2008 by Alvaro Soliverez
+ email : acejones@users.sourceforge.net
+ 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 <qvaluelist.h>
+#include <qfile.h>
+#include <qtextstream.h>
+
+// ----------------------------------------------------------------------------
+// KDE Includes
+// This is just needed for i18n(). Once I figure out how to handle i18n
+// without using this macro directly, I'll be freed of KDE dependency.
+
+#include <klocale.h>
+#include <kdebug.h>
+
+// ----------------------------------------------------------------------------
+// Project Includes
+#include "../mymoney/mymoneyfile.h"
+#include "../mymoney/mymoneyreport.h"
+#include "../mymoney/mymoneyexception.h"
+#include "../kmymoneyutils.h"
+#include "../kmymoneyglobalsettings.h"
+#include "reportdebug.h"
+#include "listtable.h"
+
+namespace reports {
+
+ QStringList ListTable::TableRow::m_sortCriteria;
+
+ // ****************************************************************************
+ //
+ // Group Iterator
+ //
+ // ****************************************************************************
+
+ class GroupIterator
+ {
+ public:
+ GroupIterator ( const QString& _group, const QString& _subtotal, unsigned _depth ) : m_depth ( _depth ), m_groupField ( _group ), m_subtotalField ( _subtotal ) {}
+ GroupIterator ( void ) {}
+ void update ( const ListTable::TableRow& _row )
+ {
+ m_previousGroup = m_currentGroup;
+ m_currentGroup = _row[m_groupField];
+ if ( isSubtotal() )
+ {
+ m_previousSubtotal = m_currentSubtotal;
+ m_currentSubtotal = MyMoneyMoney();
+ }
+ m_currentSubtotal += _row[m_subtotalField];
+ }
+
+ bool isNewHeader ( void ) const { return ( m_currentGroup != m_previousGroup ); }
+ bool isSubtotal ( void ) const { return ( m_currentGroup != m_previousGroup ) && ( !m_previousGroup.isEmpty() ); }
+ const MyMoneyMoney& subtotal ( void ) const { return m_previousSubtotal; }
+ const MyMoneyMoney& currenttotal ( void ) const { return m_currentSubtotal; }
+ unsigned depth ( void ) const { return m_depth; }
+ const QString& name ( void ) const { return m_currentGroup; }
+ const QString& oldName ( void ) const { return m_previousGroup; }
+ const QString& groupField ( void ) const { return m_groupField; }
+ const QString& subtotalField ( void ) const { return m_subtotalField; }
+ // ***DV*** HACK make the currentGroup test different but look the same
+ void force ( void ) { m_currentGroup += " "; }
+ private:
+ MyMoneyMoney m_currentSubtotal;
+ MyMoneyMoney m_previousSubtotal;
+ unsigned m_depth;
+ QString m_currentGroup;
+ QString m_previousGroup;
+ QString m_groupField;
+ QString m_subtotalField;
+ };
+
+// ****************************************************************************
+//
+// ListTable implementation
+//
+// ****************************************************************************
+
+ bool ListTable::TableRow::operator< ( const TableRow& _compare ) const
+ {
+ bool result = false;
+
+ QStringList::const_iterator it_criterion = m_sortCriteria.begin();
+ while ( it_criterion != m_sortCriteria.end() )
+ {
+ if ( this->operator[] ( *it_criterion ) < _compare[ *it_criterion ] )
+ {
+ result = true;
+ break;
+ }
+ else if ( this->operator[] ( *it_criterion ) > _compare[ *it_criterion ] )
+ break;
+
+ ++it_criterion;
+ }
+ return result;
+ }
+
+// needed for KDE < 3.2 implementation of qHeapSort
+ bool ListTable::TableRow::operator<= ( const TableRow& _compare ) const
+ {
+ return ( ! ( _compare < *this ) );
+ }
+
+ bool ListTable::TableRow::operator== ( const TableRow& _compare ) const
+ {
+ return ( ! ( *this < _compare ) && ! ( _compare < *this ) );
+ }
+
+ bool ListTable::TableRow::operator> ( const TableRow& _compare ) const
+ {
+ return ( _compare < *this );
+ }
+
+ /**
+ * TODO
+ *
+ * - Collapse 2- & 3- groups when they are identical
+ * - Way more test cases (especially splits & transfers)
+ * - Option to collapse splits
+ * - Option to exclude transfers
+ *
+ */
+
+ ListTable::ListTable ( const MyMoneyReport& _report ) : m_config ( _report )
+ {
+ }
+
+ void ListTable::render ( QString& result, QString& csv ) const
+ {
+ MyMoneyMoney grandtotal;
+ MyMoneyFile* file = MyMoneyFile::instance();
+
+ result = "";
+ csv = "";
+ result += QString ( "<h2 class=\"report\">%1</h2>\n" ).arg ( m_config.name() );
+ csv += "\"Report: " + m_config.name() + "\"\n";
+ //actual dates of the report
+ result += QString("<div class=\"subtitle\">");
+ if(!m_config.fromDate().isNull()) {
+ result += i18n("Report date range", "%1 through %2").arg(KGlobal::locale()->formatDate(m_config.fromDate(), true)).arg(KGlobal::locale()->formatDate(m_config.toDate(), true));
+ result += QString("</div>\n");
+ result += QString("<div class=\"gap\">&nbsp;</div>\n");
+
+ csv += i18n("Report date range", "%1 through %2").arg(KGlobal::locale()->formatDate(m_config.fromDate(), true)).arg(KGlobal::locale()->formatDate(m_config.toDate(), true));
+ csv += QString("\n");
+ }
+
+
+ result += QString ( "<div class=\"subtitle\">" );
+ if ( m_config.isConvertCurrency() )
+ {
+ result += i18n ( "All currencies converted to %1" ).arg ( file->baseCurrency().name() );
+ csv += i18n ( "All currencies converted to %1\n" ).arg ( file->baseCurrency().name() );
+ }
+ else
+ {
+ result += i18n ( "All values shown in %1 unless otherwise noted" ).arg ( file->baseCurrency().name() );
+ csv += i18n ( "All values shown in %1 unless otherwise noted\n" ).arg ( file->baseCurrency().name() );
+ }
+ result += QString ( "</div>\n" );
+ result += QString ( "<div class=\"gap\">&nbsp;</div>\n" );
+
+ // retrieve the configuration parameters from the report definition.
+ // the things that we care about for query reports are:
+ // how to group the rows, what columns to display, and what field
+ // to subtotal on
+ QStringList groups = QStringList::split ( ",", m_group );
+ QStringList columns = QStringList::split ( ",", m_columns );
+ columns += m_subtotal;
+ QStringList postcolumns = QStringList::split ( ",", m_postcolumns );
+ columns += postcolumns;
+
+ //
+ // Table header
+ //
+ QMap<QString, QString> i18nHeaders;
+ i18nHeaders["postdate"] = i18n ( "Date" );
+ i18nHeaders["value"] = i18n ( "Amount" );
+ i18nHeaders["number"] = i18n ( "Num" );
+ i18nHeaders["payee"] = i18n ( "Payee" );
+ i18nHeaders["category"] = i18n ( "Category" );
+ i18nHeaders["account"] = i18n ( "Account" );
+ i18nHeaders["memo"] = i18n ( "Memo" );
+ i18nHeaders["topcategory"] = i18n ( "Top Category" );
+ i18nHeaders["categorytype"] = i18n ( "Category Type" );
+ i18nHeaders["month"] = i18n ( "Month" );
+ i18nHeaders["week"] = i18n ( "Week" );
+ i18nHeaders["reconcileflag"] = i18n ( "Reconciled" );
+ i18nHeaders["action"] = i18n ( "Action" );
+ i18nHeaders["shares"] = i18n ( "Shares" );
+ i18nHeaders["price"] = i18n ( "Price" );
+ i18nHeaders["latestprice"] = i18n ( "Price" );
+ i18nHeaders["netinvvalue"] = i18n ( "Net Value" );
+ i18nHeaders["buys"] = i18n ( "Buys" );
+ i18nHeaders["sells"] = i18n ( "Sells" );
+ i18nHeaders["reinvestincome"] = i18n ( "Dividends Reinvested" );
+ i18nHeaders["cashincome"] = i18n ( "Dividends Paid Out" );
+ i18nHeaders["startingbal"] = i18n ( "Starting Balance" );
+ i18nHeaders["endingbal"] = i18n ( "Ending Balance" );
+ i18nHeaders["return"] = i18n ( "Annualized Return" );
+ i18nHeaders["returninvestment"] = i18n ( "Return On Investment" );
+ i18nHeaders["fees"] = i18n ( "Fees" );
+ i18nHeaders["interest"] = i18n ( "Interest" );
+ i18nHeaders["payment"] = i18n ( "Payment" );
+ i18nHeaders["balance"] = i18n ( "Balance" );
+ i18nHeaders["type"] = i18n ( "Type" );
+ i18nHeaders["name"] = i18n ( "Name" );
+ i18nHeaders["nextduedate"] = i18n ( "Next Due Date" );
+ i18nHeaders["occurence"] = i18n ( "Occurence" );
+ i18nHeaders["paymenttype"] = i18n ( "Payment Method" );
+ i18nHeaders["institution"] = i18n ( "Institution" );
+ i18nHeaders["description"] = i18n ( "Description" );
+ i18nHeaders["openingdate"] = i18n ( "Opening Date" );
+ i18nHeaders["currencyname"] = i18n ( "Currency" );
+ i18nHeaders["balancewarning"] = i18n ( "Balance Early Warning" );
+ i18nHeaders["maxbalancelimit"] = i18n ( "Balance Max Limit" );
+ i18nHeaders["creditwarning"] = i18n ( "Credit Early Warning" );
+ i18nHeaders["maxcreditlimit"] = i18n ( "Credit Max Limit" );
+ i18nHeaders["tax"] = i18n ( "Tax" );
+ i18nHeaders["favorite"] = i18n ( "Preferred" );
+ i18nHeaders["loanamount"] = i18n ( "Loan Amount" );
+ i18nHeaders["interestrate"] = i18n ( "Interest Rate" );
+ i18nHeaders["nextinterestchange"] = i18n ( "Next Interest Change" );
+ i18nHeaders["periodicpayment"] = i18n ( "Periodic Payment" );
+ i18nHeaders["finalpayment"] = i18n ( "Final Payment" );
+ i18nHeaders["currentbalance"] = i18n ( "Current Balance" );
+
+ // the list of columns which represent money, so we can display them correctly
+ QStringList moneyColumns = QStringList::split ( ",", "value,shares,price,latestprice,netinvvalue,buys,sells,cashincome,reinvestincome,startingbal,fees,interest,payment,balance,balancewarning,maxbalancelimit,creditwarning,maxcreditlimit,loanamount,periodicpayment,finalpayment,currentbalance" );
+
+ // the list of columns which represent shares, which is like money except the
+ // transaction currency will not be displayed
+ QStringList sharesColumns = QStringList::split ( ",", "shares" );
+
+ // the list of columns which represent a percentage, so we can display them correctly
+ QStringList percentColumns = QStringList::split ( ",", "return,returninvestment,interestrate" );
+
+ // the list of columns which represent dates, so we can display them correctly
+ QStringList dateColumns = QStringList::split ( ",", "postdate,entrydate,nextduedate,openingdate,nextinterestchange" );
+
+ result += "<table class=\"report\">\n<thead><tr class=\"itemheader\">";
+
+ QStringList::const_iterator it_column = columns.begin();
+ while ( it_column != columns.end() )
+ {
+ QString i18nName = i18nHeaders[*it_column];
+ if ( i18nName.isEmpty() )
+ i18nName = *it_column;
+ result += "<th>" + i18nName + "</th>";
+ csv += i18nName + ",";
+ ++it_column;
+ }
+
+ result += "</tr></thead>\n";
+ csv = csv.left ( csv.length() - 1 );
+ csv += "\n";
+
+ //
+ // Set up group iterators
+ //
+ // There is one active iterator for each level of grouping.
+ // As we step through the rows
+ // we update the group iterators each time based on the row data. If
+ // the group iterator changes and it had a previous value, we print a
+ // subtotal. Whether or not it had a previous value, we print a group
+ // header. The group iterator keeps track of a subtotal also.
+
+ int depth = 1;
+ QValueList<GroupIterator> groupIteratorList;
+ QStringList::const_iterator it_grouplevel = groups.begin();
+ while ( it_grouplevel != groups.end() )
+ {
+ groupIteratorList += GroupIterator ( ( *it_grouplevel ), m_subtotal, depth++ );
+ ++it_grouplevel;
+ }
+
+ //
+ // Rows
+ //
+
+ bool row_odd = true;
+
+ // ***DV***
+ MyMoneyMoney startingBalance;
+ for ( QValueList<TableRow>::const_iterator it_row = m_rows.begin();
+ it_row != m_rows.end();
+ ++it_row ) {
+
+ // the standard fraction is the fraction of an non-cash account in the base currency
+ // this could be overridden using the "fraction" element of a row for each row.
+ // Currently (2008-02-21) this override is not used at all (ipwizard)
+ int fraction = file->baseCurrency().smallestAccountFraction();
+ if ( ( *it_row ).find ( "fraction" ) != ( *it_row ).end() )
+ fraction = ( *it_row ) ["fraction"].toInt();
+
+ //
+ // Process Groups
+ //
+
+ // ***DV*** HACK to force a subtotal and header, since this render doesn't
+ // always detect a group change for different accounts with the same name
+ // (as occurs with the same stock purchased from different investment accts)
+ if ( it_row != m_rows.begin() )
+ if ( ( ( * it_row ) ["rank"] == "-2" ) && ( ( * it_row ) ["id"] == "A" ) )
+ ( groupIteratorList.last() ).force();
+
+ // There's a subtle bug here. If an earlier group gets a new group,
+ // then we need to force all the downstream groups to get one too.
+
+ // Update the group iterators with the current row value
+ QValueList<GroupIterator>::iterator it_group = groupIteratorList.begin();
+ while ( it_group != groupIteratorList.end() )
+ {
+ ( *it_group ).update ( *it_row );
+ ++it_group;
+ }
+
+ // Do subtotals backwards
+ if ( m_config.isConvertCurrency() )
+ {
+ it_group = groupIteratorList.fromLast();
+ while ( it_group != groupIteratorList.end() )
+ {
+ if ( ( *it_group ).isSubtotal() )
+ {
+ if ( ( *it_group ).depth() == 1 )
+ grandtotal += ( *it_group ).subtotal();
+ grandtotal = grandtotal.convert(fraction);
+
+ QString subtotal_html = ( *it_group ).subtotal().formatMoney ( fraction );
+ QString subtotal_csv = ( *it_group ).subtotal().formatMoney ( fraction, false );
+
+ // ***DV*** HACK fix the side-effiect from .force() method above
+ QString oldName = QString ( ( *it_group ).oldName() ).stripWhiteSpace();
+
+ result +=
+ "<tr class=\"sectionfooter\">"
+ "<td class=\"left" + QString::number ( ( ( *it_group ).depth() - 1 ) ) + "\" "
+ "colspan=\"" +
+ QString::number ( columns.count() - 1 - postcolumns.count() ) + "\">" +
+ i18n ( "Total" ) + " " + oldName + "</td>"
+ "<td>" + subtotal_html + "</td></tr>\n";
+
+ csv +=
+ "\"" + i18n ( "Total" ) + " " + oldName + "\",\"" + subtotal_csv + "\"\n";
+ }
+ --it_group;
+ }
+ }
+
+ // And headers forwards
+ it_group = groupIteratorList.begin();
+ while ( it_group != groupIteratorList.end() )
+ {
+ if ( ( *it_group ).isNewHeader() )
+ {
+ row_odd = true;
+ result += "<tr class=\"sectionheader\">"
+ "<td class=\"left" + QString::number ( ( ( *it_group ).depth() - 1 ) ) + "\" "
+ "colspan=\"" + QString::number ( columns.count() ) + "\">" +
+ ( *it_group ).name() + "</td></tr>\n";
+ csv += "\"" + ( *it_group ).name() + "\"\n";
+ }
+ ++it_group;
+ }
+
+ //
+ // Columns
+ //
+
+ // skip the opening and closing balance row,
+ // if the balance column is not shown
+ if ( ( columns.contains ( "balance" ) == 0 ) && ( ( *it_row ) ["rank"] == "-2" ) )
+ continue;
+
+ bool need_label = true;
+
+ // ***DV***
+ if ( ( * it_row ) ["rank"] == "0" ) row_odd = ! row_odd;
+
+ if ( ( * it_row ) ["rank"] == "-2" )
+ result += QString ( "<tr class=\"item%1\">" ).arg ( ( * it_row ) ["id"] );
+ else
+ if ( ( * it_row ) ["rank"] == "1" )
+ result += QString ( "<tr class=\"%1\">" ).arg ( row_odd ? "item1" : "item0" );
+ else
+ result += QString ( "<tr class=\"%1\">" ).arg ( row_odd ? "row-odd " : "row-even" );
+
+ QStringList::const_iterator it_column = columns.begin();
+ while ( it_column != columns.end() )
+ {
+ QString data = ( *it_row ) [*it_column];
+
+ // ***DV***
+ if ( ( * it_row ) ["rank"] == "1" ) {
+ if ( * it_column == "value" )
+ data = ( * it_row ) ["split"];
+ else if ( *it_column == "postdate"
+ || *it_column == "number"
+ || *it_column == "payee"
+ || *it_column == "action"
+ || *it_column == "shares"
+ || *it_column == "price"
+ || *it_column == "nextduedate"
+ || *it_column == "balance"
+ || *it_column == "account"
+ || *it_column == "name" )
+ data = "";
+ }
+
+ // ***DV***
+ if ( ( * it_row ) ["rank"] == "-2" ) {
+ if ( *it_column == "balance" ) {
+ data = ( * it_row ) ["balance"];
+ if ( ( * it_row ) ["id"] == "A" ) // opening balance?
+ startingBalance = MyMoneyMoney ( data );
+ }
+
+ if ( need_label ) {
+ if ( ( * it_column == "payee" ) ||
+ ( * it_column == "category" ) ||
+ ( * it_column == "memo" ) ) {
+ if ( ( * it_row ) ["shares"] != "" ) {
+ data = ( ( * it_row ) ["id"] == "A" )
+ ? i18n ( "Initial Market Value" )
+ : i18n ( "Ending Market Value" );
+ } else {
+ data = ( ( * it_row ) ["id"] == "A" )
+ ? i18n ( "Opening Balance" )
+ : i18n ( "Closing Balance" );
+ }
+ need_label = false;
+ }
+ }
+ }
+
+ // The 'balance' column is calculated at render-time
+ // but not printed on split lines
+ else if ( *it_column == "balance" && ( * it_row ) ["rank"] == "0" )
+ {
+ // Take the balance off the deepest group iterator
+ data = ( groupIteratorList.back().currenttotal() + startingBalance ).toString();
+ }
+
+ // Figure out how to render the value in this column, depending on
+ // what its properties are.
+ //
+ // TODO: This and the i18n headings are handled
+ // as a set of parallel vectors. Would be much better to make a single
+ // vector of a properties class.
+ if ( sharesColumns.contains ( *it_column ) )
+ {
+ if ( data.isEmpty() ) {
+ result += QString ( "<td></td>" );
+ csv += "\"\",";
+ }
+ else {
+ result += QString ( "<td>%1</td>" ).arg ( MyMoneyMoney ( data ).formatMoney ( "", 3 ) );
+ csv += "\"" + MyMoneyMoney ( data ).formatMoney ( "", 3, false ) + "\",";
+ }
+ }
+ else if ( moneyColumns.contains ( *it_column ) )
+ {
+ if ( data.isEmpty() ) {
+ result += QString ( "<td%1></td>" )
+ .arg ( ( *it_column == "value" ) ? " class=\"value\"" : "" );
+ csv += "\"\",";
+ } else if ( MyMoneyMoney( data ) == MyMoneyMoney::autoCalc ) {
+ result += QString ( "<td%1>%2</td>" )
+ .arg ( ( *it_column == "value" ) ? " class=\"value\"" : "" )
+ .arg (i18n("Calculated"));
+ csv += "\""+ i18n("Calculated") +"\",";
+ } else if ( *it_column == "price" ) {
+ result += QString ( "<td>%2</td>" )
+ .arg ( MyMoneyMoney ( data ).formatMoney ( MyMoneyMoney::precToDenom(KMyMoneyGlobalSettings::pricePrecision()) ) );
+ csv += "\"" + ( *it_row ) ["currency"] + " " + MyMoneyMoney ( data ).formatMoney ( MyMoneyMoney::precToDenom(KMyMoneyGlobalSettings::pricePrecision()), false ) + "\",";
+ } else {
+ result += QString ( "<td%1>%2&nbsp;%3</td>" )
+ .arg ( ( *it_column == "value" ) ? " class=\"value\"" : "" )
+ .arg ( ( *it_row ) ["currency"] )
+ .arg ( MyMoneyMoney ( data ).formatMoney ( fraction ) );
+ csv += "\"" + ( *it_row ) ["currency"] + " " + MyMoneyMoney ( data ).formatMoney ( fraction, false ) + "\",";
+ }
+ }
+ else if ( percentColumns.contains ( *it_column ) )
+ {
+ data = ( MyMoneyMoney ( data ) * MyMoneyMoney ( 100, 1 ) ).formatMoney ( fraction );
+ result += QString ( "<td>%1%</td>" ).arg ( data );
+ csv += data + "%,";
+ }
+ else if ( dateColumns.contains ( *it_column ) )
+ {
+ // do this before we possibly change data
+ csv += "\"" + data + "\",";
+
+ // if we have a locale() then use its date formatter
+ if ( KGlobal::locale() && ! data.isEmpty() ) {
+ QDate qd = QDate::fromString ( data, Qt::ISODate );
+ data = KGlobal::locale()->formatDate ( qd, true );
+ }
+ result += QString ( "<td class=\"left\">%1</td>" ).arg ( data );
+ }
+ else
+ {
+ result += QString ( "<td class=\"left\">%1</td>" ).arg ( data );
+ csv += "\"" + data + "\",";
+ }
+ ++it_column;
+ }
+
+ result += "</tr>\n";
+ csv = csv.left ( csv.length() - 1 ); // remove final comma
+ csv += "\n";
+ }
+
+ //
+ // Final group totals
+ //
+
+ // Do subtotals backwards
+ if ( m_config.isConvertCurrency() )
+ {
+ int fraction = file->baseCurrency().smallestAccountFraction();
+ QValueList<GroupIterator>::iterator it_group = groupIteratorList.fromLast();
+ while ( it_group != groupIteratorList.end() )
+ {
+ ( *it_group ).update ( TableRow() );
+
+ if ( ( *it_group ).depth() == 1 ) {
+ grandtotal += ( *it_group ).subtotal();
+ grandtotal = grandtotal.convert(fraction);
+ }
+
+
+ QString subtotal_html = ( *it_group ).subtotal().formatMoney ( fraction );
+ QString subtotal_csv = ( *it_group ).subtotal().formatMoney ( fraction, false );
+
+ result += "<tr class=\"sectionfooter\">"
+ "<td class=\"left" + QString::number ( ( *it_group ).depth() - 1 ) + "\" "
+ "colspan=\"" + QString::number ( columns.count() - 1 - postcolumns.count() ) + "\">" +
+ i18n ( "Total" ) + " " + ( *it_group ).oldName() + "</td>"
+ "<td>" + subtotal_html + "</td></tr>\n";
+ csv += "\"" + i18n ( "Total" ) + " " + ( *it_group ).oldName() + "\",\"" + subtotal_csv + "\"\n";
+ --it_group;
+ }
+
+ //
+ // Grand total
+ //
+
+ QString grandtotal_html = grandtotal.formatMoney ( fraction );
+ QString grandtotal_csv = grandtotal.formatMoney ( fraction, false );
+
+ result += "<tr class=\"sectionfooter\">"
+ "<td class=\"left0\" "
+ "colspan=\"" + QString::number ( columns.count() - 1 - postcolumns.count() ) + "\">" +
+ i18n ( "Grand Total" ) + "</td>"
+ "<td>" + grandtotal_html + "</td></tr>\n";
+ csv += "\"" + i18n ( "Grand Total" ) + "\",\"" + grandtotal_csv + "\"\n";
+ }
+ result += "</table>\n";
+ }
+
+ QString ListTable::renderHTML ( void ) const
+ {
+ QString html, csv;
+ render ( html, csv );
+ return html;
+ }
+
+ QString ListTable::renderCSV ( void ) const
+ {
+ QString html, csv;
+ render ( html, csv );
+ return csv;
+ }
+
+ void ListTable::dump ( const QString& file, const QString& context ) const
+ {
+ QFile g ( file );
+ g.open ( IO_WriteOnly );
+
+ if ( ! context.isEmpty() )
+ QTextStream ( &g ) << context.arg ( renderHTML() );
+ else
+ QTextStream ( &g ) << renderHTML();
+ g.close();
+ }
+
+ void ListTable::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.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.addAccount(*it_b);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+}
diff --git a/kmymoney2/reports/listtable.h b/kmymoney2/reports/listtable.h
new file mode 100644
index 0000000..5ffa64d
--- /dev/null
+++ b/kmymoney2/reports/listtable.h
@@ -0,0 +1,121 @@
+/***************************************************************************
+ listtable.h
+ -------------------
+ begin : Sat 28 jun 2008
+ copyright : (C) 2004-2005 by Ace Jones
+ 2008 by Alvaro Soliverez
+ email : acejones@users.sourceforge.net
+ 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. *
+ * *
+ ***************************************************************************/
+
+#ifndef LISTTABLE_H
+#define LISTTABLE_H
+
+// ----------------------------------------------------------------------------
+// QT Includes
+
+#include <qstringlist.h>
+
+// ----------------------------------------------------------------------------
+// KDE Includes
+
+// ----------------------------------------------------------------------------
+// Project Includes
+
+#include "../mymoney/mymoneyreport.h"
+#include "reporttable.h"
+
+namespace reports {
+
+ class ReportAccount;
+
+ /**
+ * Calculates a query of information about the transaction database.
+ *
+ * This is a middle-layer class, between the implementing classes and the engine. The
+ * MyMoneyReport class holds only the CONFIGURATION parameters. This
+ * class has some common methods used by querytable and objectinfo classes
+ *
+ * @author Alvaro Soliverez
+ *
+ * @short
+ **/
+
+ class ListTable : public ReportTable
+ {
+ public:
+ ListTable ( const MyMoneyReport& );
+ QString renderHTML ( void ) const;
+ QString renderCSV ( void ) const;
+ void drawChart ( KReportChartView& ) const {}
+ void dump ( const QString& file, const QString& context = QString() ) const;
+ void init ( void );
+
+ public:
+ /**
+ * Contains a single row in the table.
+ *
+ * Each column is a key/value pair, both strings. This class is just
+ * a QMap with the added ability to specify which columns you'd like to
+ * use as a sort key when you qHeapSort a list of these TableRows
+ */
+ class TableRow: public QMap<QString, QString>
+ {
+ public:
+ bool operator< ( const TableRow& ) const;
+ bool operator<= ( const TableRow& ) const;
+ bool operator> ( const TableRow& ) const;
+ bool operator== ( const TableRow& ) const;
+
+ static void setSortCriteria ( const QString& _criteria ) { m_sortCriteria = QStringList::split ( ",", _criteria ); }
+ private:
+ static QStringList m_sortCriteria;
+ };
+
+ QValueList<TableRow> rows() {return m_rows;};
+
+ protected:
+ void render ( QString&, QString& ) const;
+
+ /**
+ * If not in expert mode, include all subaccounts for each selected
+ * investment account
+ */
+ void includeInvestmentSubAccounts(void);
+
+ QValueList<TableRow> m_rows;
+
+ QString m_group;
+ /**
+ * Comma-separated list of columns to place BEFORE the subtotal column
+ */
+ QString m_columns;
+ /**
+ * Name of the subtotal column
+ */
+ QString m_subtotal;
+ /**
+ * Comma-separated list of columns to place AFTER the subtotal column
+ */
+ QString m_postcolumns;
+ QString m_summarize;
+ QString m_propagate;
+
+ MyMoneyReport m_config;
+
+
+ };
+
+}
+
+#endif
+
diff --git a/kmymoney2/reports/objectinfotable.cpp b/kmymoney2/reports/objectinfotable.cpp
new file mode 100644
index 0000000..649f6c2
--- /dev/null
+++ b/kmymoney2/reports/objectinfotable.cpp
@@ -0,0 +1,368 @@
+/***************************************************************************
+ objectinfotable.cpp
+ -------------------
+ begin : Sat 28 jun 2008
+ copyright : (C) 2004-2005 by Ace Jones
+ 2008 by Alvaro Soliverez
+ email : acejones@users.sourceforge.net
+ 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 <qvaluelist.h>
+#include <qfile.h>
+#include <qtextstream.h>
+
+// ----------------------------------------------------------------------------
+// KDE Includes
+// This is just needed for i18n(). Once I figure out how to handle i18n
+// without using this macro directly, I'll be freed of KDE dependency.
+
+#include <klocale.h>
+#include <kdebug.h>
+
+// ----------------------------------------------------------------------------
+// Project Includes
+#include "../mymoney/mymoneyfile.h"
+#include "../mymoney/mymoneyreport.h"
+#include "../mymoney/mymoneyexception.h"
+#include "../kmymoneyutils.h"
+#include "reportaccount.h"
+#include "reportdebug.h"
+#include "objectinfotable.h"
+
+namespace reports {
+
+// ****************************************************************************
+//
+// ObjectInfoTable implementation
+//
+// ****************************************************************************
+
+/**
+ * TODO
+ *
+ * - Collapse 2- & 3- groups when they are identical
+ * - Way more test cases (especially splits & transfers)
+ * - Option to collapse splits
+ * - Option to exclude transfers
+ *
+ */
+
+ObjectInfoTable::ObjectInfoTable(const MyMoneyReport& _report): ListTable(_report)
+{
+ // seperated into its own method to allow debugging (setting breakpoints
+ // directly in ctors somehow does not work for me (ipwizard))
+ // TODO: remove the init() method and move the code back to the ctor
+ init();
+}
+
+void ObjectInfoTable::init ( void )
+{
+ switch ( m_config.rowType() )
+ {
+ case MyMoneyReport::eSchedule:
+ constructScheduleTable();
+ m_columns = "nextduedate,name";
+ break;
+ case MyMoneyReport::eAccountInfo:
+ constructAccountTable();
+ m_columns = "institution,type,name";
+ break;
+ case MyMoneyReport::eAccountLoanInfo:
+ constructAccountLoanTable();
+ m_columns = "institution,type,name";
+ break;
+ default:
+ break;
+ }
+
+ // Sort the data to match the report definition
+ m_subtotal="value";
+
+ switch ( m_config.rowType() )
+ {
+ case MyMoneyReport::eSchedule:
+ m_group = "type";
+ m_subtotal="value";
+ break;
+ case MyMoneyReport::eAccountInfo:
+ case MyMoneyReport::eAccountLoanInfo:
+ m_group = "topcategory,institution";
+ m_subtotal="currentbalance";
+ break;
+ default:
+ throw new MYMONEYEXCEPTION ( "ObjectInfoTable::ObjectInfoTable(): unhandled row type" );
+ }
+
+ QString sort = m_group + "," + m_columns + ",id,rank";
+
+ switch ( m_config.rowType() ) {
+ case MyMoneyReport::eSchedule:
+ if ( m_config.detailLevel() == MyMoneyReport::eDetailAll ) {
+ m_columns="name,payee,paymenttype,occurence,nextduedate,category";
+ } else {
+ m_columns="name,payee,paymenttype,occurence,nextduedate";
+ }
+ break;
+ case MyMoneyReport::eAccountInfo:
+ m_columns="type,name,number,description,openingdate,currencyname,balancewarning,maxbalancelimit,creditwarning,maxcreditlimit,tax,favorite";
+ break;
+ case MyMoneyReport::eAccountLoanInfo:
+ m_columns="type,name,number,description,openingdate,currencyname,payee,loanamount,interestrate,nextinterestchange,periodicpayment,finalpayment,favorite";
+ break;
+ default:
+ m_columns = "";
+ }
+
+ TableRow::setSortCriteria ( sort );
+ qHeapSort ( m_rows );
+}
+
+void ObjectInfoTable::constructScheduleTable ( void )
+{
+ MyMoneyFile* file = MyMoneyFile::instance();
+
+ QValueList<MyMoneySchedule> schedules;
+
+ schedules = file->scheduleList ( "", MyMoneySchedule::TYPE_ANY, MyMoneySchedule::OCCUR_ANY, MyMoneySchedule::STYPE_ANY, m_config.fromDate(), m_config.toDate() );
+
+ QValueList<MyMoneySchedule>::const_iterator it_schedule = schedules.begin();
+ while ( it_schedule != schedules.end() )
+ {
+ MyMoneySchedule schedule = *it_schedule;
+
+ ReportAccount account = schedule.account();
+
+ if ( m_config.includes ( account ) ) {
+ //get fraction for account
+ int fraction = account.fraction();
+
+ //use base currency fraction if not initialized
+ if ( fraction == -1 )
+ fraction = MyMoneyFile::instance()->baseCurrency().smallestAccountFraction();
+
+ TableRow scheduleRow;
+
+ //convert to base currency if needed
+ MyMoneyMoney xr = MyMoneyMoney(1,1);
+ if (m_config.isConvertCurrency() && account.isForeignCurrency()) {
+ xr = account.baseCurrencyPrice(QDate::currentDate()).reduce();
+ }
+
+ // help for sort and render functions
+ scheduleRow["rank"] = "0";
+
+ //schedule data
+ scheduleRow["id"] = schedule.id();
+ scheduleRow["name"] = schedule.name();
+ scheduleRow["nextduedate"] = schedule.nextDueDate().toString ( Qt::ISODate );
+ scheduleRow["type"] = KMyMoneyUtils::scheduleTypeToString ( schedule.type() );
+ scheduleRow["occurence"] = i18n( schedule.occurenceToString() );
+ scheduleRow["paymenttype"] = KMyMoneyUtils::paymentMethodToString ( schedule.paymentType() );
+
+ //scheduleRow["category"] = account.name();
+
+ //to get the payee we must look into the splits of the transaction
+ MyMoneyTransaction transaction = schedule.transaction();
+ MyMoneySplit split = transaction.splitByAccount ( account.id(), true );
+ scheduleRow["value"] = (split.value() * xr).toString();
+ MyMoneyPayee payee = file->payee ( split.payeeId() );
+ scheduleRow["payee"] = payee.name();
+ m_rows += scheduleRow;
+
+ //the text matches the main split
+ bool transaction_text = m_config.match(&split);
+
+ if ( m_config.detailLevel() == MyMoneyReport::eDetailAll )
+ {
+ //get the information for all splits
+ QValueList<MyMoneySplit> splits = transaction.splits();
+ QValueList<MyMoneySplit>::const_iterator split_it = splits.begin();
+ for ( ;split_it != splits.end(); split_it++ )
+ {
+ TableRow splitRow;
+ ReportAccount splitAcc = ( *split_it ).accountId();
+
+ splitRow["rank"] = "1";
+ splitRow["id"] = schedule.id();
+ splitRow["name"] = schedule.name();
+ splitRow["type"] = KMyMoneyUtils::scheduleTypeToString ( schedule.type() );
+ splitRow["nextduedate"] = schedule.nextDueDate().toString ( Qt::ISODate );
+
+ if ( ( *split_it ).value() == MyMoneyMoney::autoCalc ) {
+ splitRow["split"] = MyMoneyMoney::autoCalc.toString();
+ } else if ( ! splitAcc.isIncomeExpense() ) {
+ splitRow["split"] = ( *split_it ).value().toString();
+ } else {
+ splitRow["split"] = ( - ( *split_it ).value() ).toString();
+ }
+
+ //if it is an assett account, mark it as a transfer
+ if ( ! splitAcc.isIncomeExpense() ) {
+ splitRow["category"] = ( ( * split_it ).value().isNegative() )
+ ? i18n ( "Transfer from %1" ).arg ( splitAcc.fullName() )
+ : i18n ( "Transfer to %1" ).arg ( splitAcc.fullName() );
+ } else {
+ splitRow ["category"] = splitAcc.fullName();
+ }
+
+ //add the split only if it matches the text or it matches the main split
+ if(m_config.match( &(*split_it) )
+ || transaction_text )
+ m_rows += splitRow;
+ }
+ }
+ }
+ ++it_schedule;
+ }
+}
+
+void ObjectInfoTable::constructAccountTable ( void )
+{
+ MyMoneyFile* file = MyMoneyFile::instance();
+
+ //make sure we have all subaccounts of investment accounts
+ includeInvestmentSubAccounts();
+
+ QValueList<MyMoneyAccount> accounts;
+ file->accountList(accounts);
+ QValueList<MyMoneyAccount>::const_iterator it_account = accounts.begin();
+ while ( it_account != accounts.end() )
+ {
+ TableRow accountRow;
+ ReportAccount account = *it_account;
+
+ if(m_config.includes(account)
+ && account.accountType() != MyMoneyAccount::Stock
+ && !account.isClosed())
+ {
+ MyMoneyMoney value;
+ accountRow["rank"] = "0";
+ accountRow["topcategory"] = KMyMoneyUtils::accountTypeToString(account.accountGroup());
+ accountRow["institution"] = (file->institution(account.institutionId())).name();
+ accountRow["type"] = KMyMoneyUtils::accountTypeToString(account.accountType());
+ accountRow["name"] = account.name();
+ accountRow["number"] = account.number();
+ accountRow["description"] = account.description();
+ accountRow["openingdate"] = account.openingDate().toString( Qt::ISODate );
+ //accountRow["currency"] = (file->currency(account.currencyId())).tradingSymbol();
+ accountRow["currencyname"] = (file->currency(account.currencyId())).name();
+ accountRow["balancewarning"] = account.value("minBalanceEarly");
+ accountRow["maxbalancelimit"] = account.value("minBalanceAbsolute");
+ accountRow["creditwarning"] = account.value("maxCreditEarly");
+ accountRow["maxcreditlimit"] = account.value("maxCreditAbsolute");
+ accountRow["tax"] = account.value("Tax");
+ accountRow["favorite"] = account.value("PreferredAccount");
+
+ //investment accounts show the balances of all its subaccounts
+ if(account.accountType() == MyMoneyAccount::Investment) {
+ value = investmentBalance(account);
+ } else {
+ value = file->balance(account.id());
+ }
+
+ //convert to base currency if needed
+ if (m_config.isConvertCurrency() && account.isForeignCurrency()) {
+ MyMoneyMoney xr = account.baseCurrencyPrice(QDate::currentDate()).reduce();
+ value = value * xr;
+ }
+ accountRow["currentbalance"] = value.toString();
+
+ m_rows += accountRow;
+ }
+ ++it_account;
+ }
+}
+
+void ObjectInfoTable::constructAccountLoanTable ( void )
+{
+ MyMoneyFile* file = MyMoneyFile::instance();
+
+ QValueList<MyMoneyAccount> accounts;
+ file->accountList(accounts);
+ QValueList<MyMoneyAccount>::const_iterator it_account = accounts.begin();
+ while ( it_account != accounts.end() )
+ {
+ TableRow accountRow;
+ ReportAccount account = *it_account;
+ MyMoneyAccountLoan loan = *it_account;
+
+ if(m_config.includes(account) &&
+ ( account.accountType() == MyMoneyAccount::Loan
+ || account.accountType() == MyMoneyAccount::AssetLoan )
+ && !account.isClosed())
+ {
+ //convert to base currency if needed
+ MyMoneyMoney xr = MyMoneyMoney(1,1);
+ if (m_config.isConvertCurrency() && account.isForeignCurrency()) {
+ xr = account.baseCurrencyPrice(QDate::currentDate()).reduce();
+ }
+
+ accountRow["rank"] = "0";
+ accountRow["topcategory"] = KMyMoneyUtils::accountTypeToString(account.accountGroup());
+ accountRow["institution"] = (file->institution(account.institutionId())).name();
+ accountRow["type"] = KMyMoneyUtils::accountTypeToString(account.accountType());
+ accountRow["name"] = account.name();
+ accountRow["number"] = account.number();
+ accountRow["description"] = account.description();
+ accountRow["openingdate"] = account.openingDate().toString( Qt::ISODate );
+ //accountRow["currency"] = (file->currency(account.currencyId())).tradingSymbol();
+ accountRow["currencyname"] = (file->currency(account.currencyId())).name();
+ accountRow["payee"] = file->payee(loan.payee()).name();
+ accountRow["loanamount"] = (loan.loanAmount() * xr).toString();
+ accountRow["interestrate"] = (loan.interestRate(QDate::currentDate())/MyMoneyMoney(100,1)*xr).toString();
+ accountRow["nextinterestchange"] = loan.nextInterestChange().toString( Qt::ISODate );
+ accountRow["periodicpayment"] = (loan.periodicPayment() * xr).toString();
+ accountRow["finalpayment"] = (loan.finalPayment() * xr).toString();
+ accountRow["favorite"] = account.value("PreferredAccount");
+
+ MyMoneyMoney value = file->balance(account.id());
+ value = value * xr;
+ accountRow["currentbalance"] = value.toString();
+ m_rows += accountRow;
+ }
+ ++it_account;
+ }
+}
+
+MyMoneyMoney ObjectInfoTable::investmentBalance(const MyMoneyAccount& acc)
+{
+ MyMoneyFile* file = MyMoneyFile::instance();
+ MyMoneyMoney value;
+
+ value = file->balance(acc.id());
+ QValueList<QString>::const_iterator it_a;
+ for(it_a = acc.accountList().begin(); it_a != acc.accountList().end(); ++it_a) {
+ MyMoneyAccount stock = file->account(*it_a);
+ try {
+ MyMoneyMoney val;
+ MyMoneyMoney balance = file->balance(stock.id());
+ MyMoneySecurity security = file->security(stock.currencyId());
+ MyMoneyPrice price = file->price(stock.currencyId(), security.tradingCurrency());
+ val = balance * price.rate(security.tradingCurrency());
+ // adjust value of security to the currency of the account
+ MyMoneySecurity accountCurrency = file->currency(acc.currencyId());
+ val = val * file->price(security.tradingCurrency(), accountCurrency.id()).rate(accountCurrency.id());
+ val = val.convert(acc.fraction());
+ value += val;
+ } catch(MyMoneyException* e) {
+ qWarning("%s", (QString("cannot convert stock balance of %1 to base currency: %2").arg(stock.name(), e->what())).data());
+ delete e;
+ }
+ }
+ return value;
+}
+
+}
+// vim:cin:si:ai:et:ts=2:sw=2:
diff --git a/kmymoney2/reports/objectinfotable.h b/kmymoney2/reports/objectinfotable.h
new file mode 100644
index 0000000..0b4ab71
--- /dev/null
+++ b/kmymoney2/reports/objectinfotable.h
@@ -0,0 +1,75 @@
+/***************************************************************************
+ objectinfotable.h
+ -------------------
+ begin : Sat 28 jun 2008
+ copyright : (C) 2004-2005 by Ace Jones
+ 2008 by Alvaro Soliverez
+ email : acejones@users.sourceforge.net
+ 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. *
+ * *
+ ***************************************************************************/
+
+#ifndef OBJECTINFOTABLE_H
+#define OBJECTINFOTABLE_H
+
+// ----------------------------------------------------------------------------
+// QT Includes
+
+#include <qstringlist.h>
+
+// ----------------------------------------------------------------------------
+// KDE Includes
+
+// ----------------------------------------------------------------------------
+// Project Includes
+
+#include "../mymoney/mymoneyreport.h"
+#include "listtable.h"
+
+namespace reports {
+
+class ReportAccount;
+
+/**
+ * Calculates a query of information about the transaction database.
+ *
+ * This is a middle-layer class, between the UI and the engine. The
+ * MyMoneyReport class holds only the CONFIGURATION parameters. This
+ * class actually does the work of retrieving the data from the engine
+ * and formatting it for the user.
+ *
+ * @author Ace Jones
+ *
+ * @short
+**/
+
+class ObjectInfoTable : public ListTable
+{
+public:
+ ObjectInfoTable ( const MyMoneyReport& );
+ void init ( void );
+
+protected:
+ void constructScheduleTable ( void );
+ void constructAccountTable ( void );
+ void constructAccountLoanTable ( void );
+
+private:
+ /**
+ * @param acc the investment account
+ * @return the balance in the currency of the investment account
+ */
+ MyMoneyMoney investmentBalance(const MyMoneyAccount& acc);
+};
+
+}
+
+#endif // QUERYREPORT_H
diff --git a/kmymoney2/reports/pivotgrid.cpp b/kmymoney2/reports/pivotgrid.cpp
new file mode 100644
index 0000000..9cdf9b3
--- /dev/null
+++ b/kmymoney2/reports/pivotgrid.cpp
@@ -0,0 +1,161 @@
+/***************************************************************************
+ pivotgrid.cpp
+ -------------------
+ begin : Mon May 17 2004
+ copyright : (C) 2004-2005 by Ace Jones
+ email : <ace.j@hotpop.com>
+ Thomas Baumgart <ipwizard@users.sourceforge.net>
+ ***************************************************************************/
+
+/***************************************************************************
+ * *
+ * 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 "reportdebug.h"
+#include "kreportchartview.h"
+#include "../kmymoneyglobalsettings.h"
+
+#include <kmymoney/kmymoneyutils.h>
+
+namespace reports {
+
+ const unsigned PivotOuterGroup::m_kDefaultSortOrder = 100;
+
+ PivotCell::PivotCell(const MyMoneyMoney& value) :
+ MyMoneyMoney(value),
+ m_stockSplit(MyMoneyMoney(1,1))
+ {
+ m_cellUsed |= !value.isZero();
+ }
+
+PivotCell PivotCell::operator += (const PivotCell& right)
+{
+ const MyMoneyMoney& r = static_cast<const MyMoneyMoney&>(right);
+ *this += r;
+ m_postSplit = m_postSplit * right.m_stockSplit;
+ m_stockSplit = m_stockSplit * right.m_stockSplit;
+ m_postSplit += right.m_postSplit;
+ m_cellUsed |= right.m_cellUsed;
+ return *this;
+}
+
+PivotCell PivotCell::operator += (const MyMoneyMoney& value)
+{
+ m_cellUsed |= !value.isZero();
+ if(m_stockSplit != MyMoneyMoney(1,1))
+ m_postSplit += value;
+ else
+ MyMoneyMoney::operator += (value);
+ return *this;
+}
+
+PivotCell PivotCell::stockSplit(const MyMoneyMoney& factor)
+{
+ PivotCell s;
+ s.m_stockSplit = factor;
+ return s;
+}
+
+const QString PivotCell::formatMoney(int fraction, bool showThousandSeparator) const
+{
+ return formatMoney("", MyMoneyMoney::denomToPrec(fraction), showThousandSeparator);
+}
+
+const QString PivotCell::formatMoney(const QString& currency, const int prec, bool showThousandSeparator) const
+{
+ // construct the result
+ MyMoneyMoney res = (*this * m_stockSplit) + m_postSplit;
+ return res.formatMoney(currency, prec, showThousandSeparator);
+}
+
+MyMoneyMoney PivotCell::calculateRunningSum(const MyMoneyMoney& runningSum)
+{
+ MyMoneyMoney::operator += (runningSum);
+ MyMoneyMoney::operator = ((*this * m_stockSplit) + m_postSplit);
+ m_postSplit = MyMoneyMoney(0,1);
+ m_stockSplit = MyMoneyMoney(1,1);
+ return *this;
+}
+
+MyMoneyMoney PivotCell::cellBalance(const MyMoneyMoney& _balance)
+{
+ MyMoneyMoney balance(_balance);
+ balance += *this;
+ balance = (balance * m_stockSplit) + m_postSplit;
+ return balance;
+}
+
+PivotGridRowSet::PivotGridRowSet( unsigned _numcolumns )
+{
+ insert(eActual, PivotGridRow(_numcolumns));
+ insert(eBudget, PivotGridRow(_numcolumns));
+ insert(eBudgetDiff, PivotGridRow(_numcolumns));
+ insert(eForecast, PivotGridRow(_numcolumns));
+ insert(eAverage, PivotGridRow(_numcolumns));
+ insert(ePrice, PivotGridRow(_numcolumns));
+}
+
+PivotGridRowSet PivotGrid::rowSet(QString id)
+{
+
+ //go through the data and get the row that matches the id
+ PivotGrid::iterator it_outergroup = begin();
+ while ( it_outergroup != 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(it_row.key().id() == id)
+ return it_row.data();
+
+ ++it_row;
+ }
+ ++it_innergroup;
+ }
+ ++it_outergroup;
+ }
+ return PivotGridRowSet();
+}
+
+} // namespace
+// vim:cin:si:ai:et:ts=2:sw=2:
diff --git a/kmymoney2/reports/pivotgrid.h b/kmymoney2/reports/pivotgrid.h
new file mode 100644
index 0000000..ca7f5ab
--- /dev/null
+++ b/kmymoney2/reports/pivotgrid.h
@@ -0,0 +1,151 @@
+/***************************************************************************
+ pivotgrid.h
+ -------------------
+ begin : Sat May 22 2004
+ copyright : (C) 2004-2005 by Ace Jones
+ email : <ace.j@hotpop.com>
+ Thomas Baumgart <ipwizard@users.sourceforge.net>
+ ***************************************************************************/
+
+/***************************************************************************
+ * *
+ * This program is free software; you can redistribute it and/or modify *
+ * it under the terms of the GNU General Public License as published by *
+ * the Free Software Foundation; either version 2 of the License, or *
+ * (at your option) any later version. *
+ * *
+ ***************************************************************************/
+
+#ifndef PIVOTGRID_H
+#define PIVOTGRID_H
+
+// ----------------------------------------------------------------------------
+// QT Includes
+
+#include <qmap.h>
+#include <qvaluelist.h>
+
+// ----------------------------------------------------------------------------
+// KDE Includes
+
+// ----------------------------------------------------------------------------
+// Project Includes
+
+#include "reportaccount.h"
+
+namespace reports {
+
+ enum ERowType {eActual, eBudget, eBudgetDiff, eForecast, eAverage, ePrice };
+
+ /**
+ * The fundamental data construct of this class is a 'grid'. It is organized as follows:
+ *
+ * A 'Row' is a row of money values, each column is a month. The first month corresponds to
+ * m_beginDate.
+ *
+ * A 'Row Pair' is two rows of money values. Each column is the SAME month. One row is the
+ * 'actual' values for the period, the other row is the 'budgetted' values for the same
+ * period. For ease of implementation, a Row Pair is implemented as a Row which contains
+ * another Row. The inherited Row is the 'actual', the contained row is the 'Budget'.
+ *
+ * An 'Inner Group' contains a rows for each subordinate account within a single top-level
+ * account. It also contains a mapping from the account descriptor for the subordinate account
+ * to its row data. So if we have an Expense account called "Computers", with sub-accounts called
+ * "Hardware", "Software", and "Peripherals", there will be one Inner Group for "Computers"
+ * which contains three Rows.
+ *
+ * An 'Outer Group' contains Inner Row Groups for all the top-level accounts in a given
+ * account class. Account classes are Expense, Income, Asset, Liability. In the case above,
+ * the "Computers" Inner Group is contained within the "Expense" Outer Group.
+ *
+ * A 'Grid' is the set of all Outer Groups contained in this report.
+ *
+ */
+ class PivotCell: public MyMoneyMoney
+ {
+ public:
+ PivotCell() : m_stockSplit(MyMoneyMoney(1,1)), m_cellUsed(false) {}
+ PivotCell(const MyMoneyMoney& value);
+ static PivotCell stockSplit(const MyMoneyMoney& factor);
+ PivotCell operator += (const PivotCell& right);
+ PivotCell operator += (const MyMoneyMoney& value);
+ const QString formatMoney(int fraction, bool showThousandSeparator = true) const;
+ const QString formatMoney(const QString& currency, const int prec, bool showThousandSeparator = true) const;
+ MyMoneyMoney calculateRunningSum(const MyMoneyMoney& runningSum);
+ MyMoneyMoney cellBalance(const MyMoneyMoney& _balance);
+ bool isUsed(void) const { return m_cellUsed; }
+ private:
+ MyMoneyMoney m_stockSplit;
+ MyMoneyMoney m_postSplit;
+ bool m_cellUsed;
+ };
+ class PivotGridRow: public QValueList<PivotCell>
+ {
+ public:
+
+ PivotGridRow( unsigned _numcolumns = 0 )
+ {
+ if ( _numcolumns )
+ insert( end(), _numcolumns, PivotCell() );
+ }
+ MyMoneyMoney m_total;
+ };
+
+ class PivotGridRowSet: public QMap<ERowType, PivotGridRow>
+ {
+ public:
+ PivotGridRowSet( unsigned _numcolumns = 0 );
+ };
+
+ class PivotInnerGroup: public QMap<ReportAccount,PivotGridRowSet>
+ {
+ public:
+ PivotInnerGroup( unsigned _numcolumns = 0 ): m_total(_numcolumns) {}
+
+ PivotGridRowSet m_total;
+ };
+
+ class PivotOuterGroup: public QMap<QString,PivotInnerGroup>
+ {
+ public:
+ PivotOuterGroup( unsigned _numcolumns = 0, unsigned _sort=m_kDefaultSortOrder, bool _inverted=false): m_total(_numcolumns), m_inverted(_inverted), m_sortOrder(_sort) {}
+ int operator<( const PivotOuterGroup& _right )
+ {
+ if ( m_sortOrder != _right.m_sortOrder )
+ return m_sortOrder < _right.m_sortOrder;
+ else
+ return m_displayName < _right.m_displayName;
+ }
+ PivotGridRowSet m_total;
+
+ // An inverted outergroup means that all values placed in subordinate rows
+ // should have their sign inverted from typical cash-flow notation. Also it
+ // means that when the report is summed, the values should be inverted again
+ // so that the grand total is really "non-inverted outergroup MINUS inverted outergroup".
+ bool m_inverted;
+
+ // The localized name of the group for display in the report. Outergoups need this
+ // independently, because they will lose their association with the TGrid when the
+ // report is rendered.
+ QString m_displayName;
+
+ // lower numbers sort toward the top of the report. defaults to 100, which is a nice
+ // middle-of-the-road value
+ unsigned m_sortOrder;
+
+ // default sort order
+ static const unsigned m_kDefaultSortOrder;
+ };
+ class PivotGrid: public QMap<QString,PivotOuterGroup>
+ {
+ public:
+ PivotGridRowSet rowSet (QString id);
+
+ PivotGridRowSet m_total;
+ };
+
+}
+
+#endif
+// PIVOTGRID_H
+// vim:cin:si:ai:et:ts=2:sw=2:
diff --git a/kmymoney2/reports/pivotgridtest.cpp b/kmymoney2/reports/pivotgridtest.cpp
new file mode 100644
index 0000000..397491d
--- /dev/null
+++ b/kmymoney2/reports/pivotgridtest.cpp
@@ -0,0 +1,198 @@
+/***************************************************************************
+ pivotgridtest.cpp
+ -------------------
+ copyright : (C) 2002-2005 by Thomas Baumgart
+ email : ipwizard@users.sourceforge.net
+ Ace Jones <ace.j@hotpop.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. *
+ * *
+ ***************************************************************************/
+/*
+#include <qvaluelist.h>
+#include <qvaluevector.h>
+#include <qdom.h>
+#include <qfile.h>
+
+#include <kdebug.h>
+#include <kdeversion.h>
+#include <kglobal.h>
+#include <kglobalsettings.h>
+#include <klocale.h>
+#include <kstandarddirs.h>
+
+// DOH, mmreport.h uses this without including it!!
+#include "../mymoney/mymoneyaccount.h"
+
+#include "../mymoney/mymoneysecurity.h"
+#include "../mymoney/mymoneyprice.h"
+#include "../mymoney/mymoneyreport.h"
+#include "../mymoney/mymoneystatement.h"
+#include "../mymoney/storage/mymoneystoragedump.h"
+#include "../mymoney/storage/mymoneystoragexml.h"
+*/
+
+#include "pivotgridtest.h"
+
+#include "reportstestcommon.h"
+
+#define private public
+#include "../reports/pivotgrid.h"
+#undef private
+
+using namespace reports;
+using namespace test;
+
+PivotGridTest::PivotGridTest()
+{
+}
+
+void PivotGridTest::setUp ()
+{
+ storage = new MyMoneySeqAccessMgr;
+ file = MyMoneyFile::instance();
+ file->attachStorage(storage);
+
+ MyMoneyFileTransaction ft;
+ file->addCurrency(MyMoneySecurity("CAD", "Canadian Dollar", "C$"));
+ file->addCurrency(MyMoneySecurity("USD", "US Dollar", "$"));
+ file->addCurrency(MyMoneySecurity("JPY", "Japanese Yen", QChar(0x00A5), 100, 1));
+ file->addCurrency(MyMoneySecurity("GBP", "British Pound", "#"));
+ file->setBaseCurrency(file->currency("USD"));
+
+ MyMoneyPayee payeeTest("Test Payee");
+ file->addPayee(payeeTest);
+ MyMoneyPayee payeeTest2("Thomas Baumgart");
+ file->addPayee(payeeTest2);
+
+ acAsset = (MyMoneyFile::instance()->asset().id());
+ acLiability = (MyMoneyFile::instance()->liability().id());
+ acExpense = (MyMoneyFile::instance()->expense().id());
+ acIncome = (MyMoneyFile::instance()->income().id());
+ acChecking = makeAccount(QString("Checking Account"),MyMoneyAccount::Checkings,moCheckingOpen,QDate(2004,5,15),acAsset);
+ acCredit = makeAccount(QString("Credit Card"),MyMoneyAccount::CreditCard,moCreditOpen,QDate(2004,7,15),acLiability);
+ acSolo = makeAccount(QString("Solo"),MyMoneyAccount::Expense,0,QDate(2004,1,11),acExpense);
+ acParent = makeAccount(QString("Parent"),MyMoneyAccount::Expense,0,QDate(2004,1,11),acExpense);
+ acChild = makeAccount(QString("Child"),MyMoneyAccount::Expense,0,QDate(2004,2,11),acParent);
+ acForeign = makeAccount(QString("Foreign"),MyMoneyAccount::Expense,0,QDate(2004,1,11),acExpense);
+
+ acSecondChild = makeAccount(QString("Second Child"),MyMoneyAccount::Expense,0,QDate(2004,2,11),acParent);
+ acGrandChild1 = makeAccount(QString("Grand Child 1"),MyMoneyAccount::Expense,0,QDate(2004,2,11),acChild);
+ acGrandChild2 = makeAccount(QString("Grand Child 2"),MyMoneyAccount::Expense,0,QDate(2004,2,11),acChild);
+
+ MyMoneyInstitution i("Bank of the World","","","","","","");
+ file->addInstitution(i);
+ inBank = i.id();
+ ft.commit();
+}
+
+void PivotGridTest::tearDown ()
+{
+ file->detachStorage(storage);
+ delete storage;
+}
+
+void PivotGridTest::testCellAddValue(void)
+{
+ PivotCell a;
+ CPPUNIT_ASSERT(a == MyMoneyMoney(0,1));
+ CPPUNIT_ASSERT(a.m_stockSplit == MyMoneyMoney(1,1));
+ CPPUNIT_ASSERT(a.m_postSplit == MyMoneyMoney(0,1));
+ CPPUNIT_ASSERT(a.formatMoney("", 2) == MyMoneyMoney(0,1).formatMoney("", 2));
+
+ PivotCell b(MyMoneyMoney(13,10));
+ CPPUNIT_ASSERT(b == MyMoneyMoney(13,10));
+ CPPUNIT_ASSERT(b.m_stockSplit == MyMoneyMoney(1,1));
+ CPPUNIT_ASSERT(b.m_postSplit == MyMoneyMoney(0,1));
+ CPPUNIT_ASSERT(b.formatMoney("", 2) == MyMoneyMoney(13,10).formatMoney("", 2));
+
+ PivotCell s(b);
+ CPPUNIT_ASSERT(s == MyMoneyMoney(13,10));
+ CPPUNIT_ASSERT(s.m_stockSplit == MyMoneyMoney(1,1));
+ CPPUNIT_ASSERT(s.m_postSplit == MyMoneyMoney(0,1));
+ CPPUNIT_ASSERT(s.formatMoney("", 2) == MyMoneyMoney(13,10).formatMoney("", 2));
+
+ s = PivotCell::stockSplit(MyMoneyMoney(1,2));
+ CPPUNIT_ASSERT(s == MyMoneyMoney(0,1));
+ CPPUNIT_ASSERT(s.m_stockSplit == MyMoneyMoney(1,2));
+ CPPUNIT_ASSERT(s.m_postSplit == MyMoneyMoney(0,1));
+ CPPUNIT_ASSERT(s.formatMoney("", 2) == MyMoneyMoney(0,1).formatMoney("", 2));
+
+ a += MyMoneyMoney(1,1);
+ a += MyMoneyMoney(2,1);
+ CPPUNIT_ASSERT(a == MyMoneyMoney(3,1));
+ CPPUNIT_ASSERT(a.m_stockSplit == MyMoneyMoney(1,1));
+ CPPUNIT_ASSERT(a.m_postSplit == MyMoneyMoney(0,1));
+ CPPUNIT_ASSERT(a.formatMoney("", 2) == MyMoneyMoney(3,1).formatMoney("", 2));
+
+ a += s;
+ CPPUNIT_ASSERT(a == MyMoneyMoney(3,1));
+ CPPUNIT_ASSERT(a.m_stockSplit == MyMoneyMoney(1,2));
+ CPPUNIT_ASSERT(a.m_postSplit == MyMoneyMoney(0,1));
+ CPPUNIT_ASSERT(a.formatMoney("", 2) == MyMoneyMoney(15,10).formatMoney("", 2));
+
+ a += MyMoneyMoney(3,1);
+ a += MyMoneyMoney(3,1);
+ CPPUNIT_ASSERT(a == MyMoneyMoney(3,1));
+ CPPUNIT_ASSERT(a.m_stockSplit == MyMoneyMoney(1,2));
+ CPPUNIT_ASSERT(a.m_postSplit == MyMoneyMoney(6,1));
+ CPPUNIT_ASSERT(a.formatMoney("", 2) == MyMoneyMoney(75,10).formatMoney("", 2));
+}
+
+void PivotGridTest::testCellAddCell(void)
+{
+ PivotCell a,b;
+
+ a += MyMoneyMoney(3,1);
+ a += PivotCell::stockSplit(MyMoneyMoney(2,1));
+ a += MyMoneyMoney(4,1);
+
+ CPPUNIT_ASSERT(a == MyMoneyMoney(3,1));
+ CPPUNIT_ASSERT(a.m_stockSplit == MyMoneyMoney(2,1));
+ CPPUNIT_ASSERT(a.m_postSplit == MyMoneyMoney(4,1));
+ CPPUNIT_ASSERT(a.formatMoney("", 2) == MyMoneyMoney(10,1).formatMoney("", 2));
+
+ b += MyMoneyMoney(4,1);
+ b += PivotCell::stockSplit(MyMoneyMoney(4,1));
+ b += MyMoneyMoney(16,1);
+
+ CPPUNIT_ASSERT(b == MyMoneyMoney(4,1));
+ CPPUNIT_ASSERT(b.m_stockSplit == MyMoneyMoney(4,1));
+ CPPUNIT_ASSERT(b.m_postSplit == MyMoneyMoney(16,1));
+ CPPUNIT_ASSERT(b.formatMoney("", 2) == MyMoneyMoney(32,1).formatMoney("", 2));
+
+ a += b;
+
+ CPPUNIT_ASSERT(a == MyMoneyMoney(3,1));
+ CPPUNIT_ASSERT(a.m_stockSplit == MyMoneyMoney(8,1));
+ CPPUNIT_ASSERT(a.m_postSplit == MyMoneyMoney(48,1));
+ CPPUNIT_ASSERT(a.formatMoney("", 2) == MyMoneyMoney(72,1).formatMoney("", 2));
+}
+
+void PivotGridTest::testCellRunningSum(void)
+{
+ PivotCell a;
+ MyMoneyMoney runningSum(12,10);
+
+ a += MyMoneyMoney(3,1);
+ a += PivotCell::stockSplit(MyMoneyMoney(125,100));
+ a += MyMoneyMoney(134,10);
+
+ CPPUNIT_ASSERT(a.m_stockSplit != MyMoneyMoney(1,1));
+ CPPUNIT_ASSERT(a.m_postSplit != MyMoneyMoney(0,1));
+
+ runningSum = a.calculateRunningSum(runningSum);
+
+ CPPUNIT_ASSERT(runningSum == MyMoneyMoney(1865,100));
+ CPPUNIT_ASSERT(a.formatMoney("", 2) == MyMoneyMoney(1865,100).formatMoney("", 2));
+ CPPUNIT_ASSERT(a.m_stockSplit == MyMoneyMoney(1,1));
+ CPPUNIT_ASSERT(a.m_postSplit == MyMoneyMoney(0,1));
+}
+
+// vim:cin:si:ai:et:ts=2:sw=2:
diff --git a/kmymoney2/reports/pivotgridtest.h b/kmymoney2/reports/pivotgridtest.h
new file mode 100644
index 0000000..50b6f57
--- /dev/null
+++ b/kmymoney2/reports/pivotgridtest.h
@@ -0,0 +1,47 @@
+/***************************************************************************
+ pivotgridtest.h
+ -------------------
+ copyright : (C) 2002 by Thomas Baumgart
+ email : ipwizard@users.sourceforge.net
+ Ace Jones <ace.jones@hotpop.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. *
+ * *
+ ***************************************************************************/
+
+#ifndef PIVOTGRIDTEST_H
+#define PIVOTGRIDTEST_H
+
+#include <cppunit/extensions/HelperMacros.h>
+#include "../mymoney/mymoneyfile.h"
+#include "../mymoney/storage/mymoneyseqaccessmgr.h"
+
+class PivotGridTest : public CppUnit::TestFixture {
+ CPPUNIT_TEST_SUITE(PivotGridTest);
+ CPPUNIT_TEST(testCellAddValue);
+ CPPUNIT_TEST(testCellAddCell);
+ CPPUNIT_TEST(testCellRunningSum);
+ CPPUNIT_TEST_SUITE_END();
+
+private:
+ MyMoneyAccount *m;
+
+ MyMoneySeqAccessMgr* storage;
+ MyMoneyFile* file;
+
+public:
+ PivotGridTest();
+ void setUp ();
+ void tearDown ();
+ void testCellAddValue();
+ void testCellAddCell();
+ void testCellRunningSum();
+};
+
+#endif // PIVOTGRIDTEST_H
diff --git a/kmymoney2/reports/pivottable.cpp b/kmymoney2/reports/pivottable.cpp
new file mode 100644
index 0000000..c12ca57
--- /dev/null
+++ b/kmymoney2/reports/pivottable.cpp
@@ -0,0 +1,2604 @@
+/***************************************************************************
+ 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:
diff --git a/kmymoney2/reports/pivottable.h b/kmymoney2/reports/pivottable.h
new file mode 100644
index 0000000..226c9a5
--- /dev/null
+++ b/kmymoney2/reports/pivottable.h
@@ -0,0 +1,356 @@
+/***************************************************************************
+ pivottable.h
+ -------------------
+ begin : Sat May 22 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. *
+ * *
+ ***************************************************************************/
+
+#ifndef PIVOTTABLE_H
+#define PIVOTTABLE_H
+
+// ----------------------------------------------------------------------------
+// QT Includes
+#include <qmap.h>
+#include <qvaluelist.h>
+
+// ----------------------------------------------------------------------------
+// KDE Includes
+
+// ----------------------------------------------------------------------------
+// Project Includes
+#include "kreportchartview.h"
+#include "../mymoney/mymoneyfile.h"
+#include "../mymoney/mymoneyreport.h"
+#include "reporttable.h"
+#include "pivotgrid.h"
+#include "reportaccount.h"
+
+namespace reports {
+
+/**
+ * Calculates a 'pivot table' of information about the transaction database.
+ * Based on pivot tables in MS Excel, and implemented as 'Data Pilot' in
+ * OpenOffice.Org Calc.
+ *
+ * | Month,etc
+ * -------------+------------
+ * Expense Type | Sum(Value)
+ * Category |
+ *
+ * This is a middle-layer class, between the UI and the engine. The
+ * MyMoneyReport class holds only the CONFIGURATION parameters. This
+ * class actually does the work of retrieving the data from the engine
+ * and formatting it for the user.
+ *
+ * @author Ace Jones
+ *
+ * @short
+**/
+class PivotTable : public ReportTable
+{
+public:
+ /**
+ * Create a Pivot table style report
+ *
+ * @param _config_f The configuration parameters for this report
+ */
+ PivotTable( const MyMoneyReport& _config_f );
+
+ /**
+ * virtual Destructur
+ */
+ virtual ~PivotTable() {}
+
+ /**
+ * Render the report to an HTML stream.
+ *
+ * @return QString HTML string representing the report
+ */
+ QString renderHTML( void ) const;
+ /**
+ * Render the report to a comma-separated-values stream.
+ *
+ * @return QString CSV string representing the report
+ */
+ QString renderCSV( void ) const;
+
+ /**
+ * Render the report to a graphical chart
+ *
+ * @param view The KReportChartView into which to draw the chart.
+ */
+ void drawChart( KReportChartView& view ) const;
+
+ /**
+ * Dump the report's HTML to a file
+ *
+ * @param file The filename to dump into
+ * @param context unused, but provided for interface compatibility
+ */
+ void dump( const QString& file, const QString& context=QString()) const;
+
+ /**
+ * Returns the grid generated by the report
+ *
+ */
+ PivotGrid grid(void) {return m_grid;}
+
+protected:
+ void init(void); // used for debugging the constructor
+
+private:
+
+ PivotGrid m_grid;
+
+ QStringList m_columnHeadings;
+ unsigned m_numColumns;
+ QDate m_beginDate;
+ QDate m_endDate;
+ bool m_runningSumsCalculated;
+
+ /**
+ * For budget-vs-actual reports only, maps each account to the account which holds
+ * the budget for it. If an account is not contained in this map, it is not included
+ * in the budget.
+ */
+ QMap<QString, QString> m_budgetMap;
+
+ /**
+ * This list contains the types of PivotGridRows that are going to be shown in the report
+ */
+ QValueList<ERowType> m_rowTypeList;
+
+ /**
+ * This list contains the i18n headers for the column types
+ */
+ QValueList<QString> m_columnTypeHeaderList;
+
+ MyMoneyReport m_config_f;
+
+ /**
+ * This method returns the formatted value of @a amount with
+ * a possible @a currencySymbol added and @a prec fractional digits.
+ * @a currencySymbol defaults to be empty and @a prec defaults to 2.
+ *
+ * If @a amount is negative the formatted value is enclosed in an
+ * HTML font tag to modify the color to reflect the user settings for
+ * negtive numbers.
+ *
+ * Example: 1.23 is returned as '1.23' whereas -1.23 is returned as
+ * @verbatim <font color="rgb($red,$green,$blue)">-1.23</font>@endverbatim
+ * with $red, $green and $blue being the actual value for the
+ * chosen color.
+ */
+ QString coloredAmount(const MyMoneyMoney& amount, const QString& currencySymbol = QString(), int prec = 2 ) const;
+
+protected:
+ /**
+ * Creates a row in the grid if it doesn't already exist
+ *
+ * Downsteam assignment functions will assume that this row already
+ * exists, so this function creates a row of the needed length populated
+ * with zeros.
+ *
+ * @param outergroup The outer row group
+ * @param row The row itself
+ * @param recursive Whether to also recursively create rows for our parent accounts
+ */
+ void createRow( const QString& outergroup, const ReportAccount& row, bool recursive );
+
+ /**
+ * Assigns a value into the grid
+ *
+ * Adds the given value to the value which already exists at the specified grid position
+ *
+ * @param outergroup The outer row group
+ * @param row The row itself
+ * @param column The column
+ * @param value The value to be added in
+ * @param budget Whether this is a budget value (@p true) or an actual
+ * value (@p false). Defaults to @p false.
+ * @param stockSplit Wheter this is a stock split (@p true) or an actual
+ * value (@p false). Defaults to @p false.
+ */
+ inline void assignCell( const QString& outergroup, const ReportAccount& row, unsigned column, MyMoneyMoney value, bool budget = false, bool stockSplit = false );
+
+ /**
+ * Create a row for each included account. This is used when
+ * the config parameter isIncludingUnusedAccount() is true
+ */
+ void createAccountRows(void);
+
+ /**
+ * Record the opening balances of all qualifying accounts into the grid.
+ *
+ * For accounts opened before the report period, places the balance into the '0' column.
+ * For those opened during the report period, places the balance into the appropriate column
+ * for the month when it was opened.
+ */
+ void calculateOpeningBalances( void );
+
+ /**
+ * Calculate budget mapping
+ *
+ * For budget-vs-actual reports, this creates a mapping between each account
+ * in the user's hierarchy and the account where the budget is held for it.
+ * This is needed because the user can budget on a given account for that
+ * account and all its descendants. Also if NO budget is placed on the
+ * account or any of its parents, the account is not included in the map.
+ */
+ void calculateBudgetMapping( void );
+
+ /**
+ * Calculate the running sums.
+ *
+ * After calling this method, each cell of the report will contain the running sum of all
+ * the cells in its row in this and earlier columns.
+ *
+ * For example, consider a row with these values:
+ * 01 02 03 04 05 06 07 08 09 10
+ *
+ * After calling this function, the row will look like this:
+ * 01 03 06 10 15 21 28 36 45 55
+ */
+ void calculateRunningSums( void );
+ void calculateRunningSums( PivotInnerGroup::iterator& it_row);
+
+ /**
+ * This method calculates the difference between a @a budgeted and an @a
+ * actual amount. The calculation is based on the type of the
+ * @a repAccount. The difference value is calculated as follows:
+ *
+ * If @a repAccount is of type MyMoneyAccount::Income
+ *
+ * @code
+ * diff = actual - budgeted
+ * @endcode
+ *
+ * If @a repAccount is of type MyMoneyAccount::Expense
+ *
+ * @code
+ * diff = budgeted - actual
+ * @endcode
+ *
+ * In all other cases, 0 is returned.
+ */
+ void calculateBudgetDiff(void);
+
+ /**
+ * This method calculates forecast for a report
+ */
+ void calculateForecast(void);
+
+ /**
+ * This method inserts units to be used to display prices
+ */
+ void fillBasePriceUnit(ERowType rowType);
+
+ /**
+ * This method calculates moving average for a report
+ */
+ void calculateMovingAverage(void);
+
+ /**
+ * Calculate the row and column totals
+ *
+ * This function will set the m_total members of all the TGrid objects. Be sure the values are
+ * all converted to the base currency first!!
+ *
+ */
+ void calculateTotals( void );
+
+ /**
+ * Convert each value in the grid to the base currency
+ *
+ */
+ void convertToBaseCurrency( void );
+
+ /**
+ * Convert each value in the grid to the account/category's deep currency
+ *
+ * See AccountDescriptor::deepCurrencyPrice() for a description of 'deep' currency
+ *
+ */
+ void convertToDeepCurrency( void );
+
+ /**
+ * Turn month-long columns into larger time periods if needed
+ *
+ * For example, consider a row with these values:
+ * 01 02 03 04 05 06 07 08 09 10
+ *
+ * If the column pitch is 3 (i.e. quarterly), after calling this function,
+ * the row will look like this:
+ * 06 15 26 10
+ */
+ void collapseColumns(void);
+
+ /**
+ * Determine the proper column headings based on the time periods covered by each column
+ *
+ */
+ void calculateColumnHeadings(void);
+
+ /**
+ * Helper methods for collapseColumns
+ *
+ */
+ void accumulateColumn(unsigned destcolumn, unsigned sourcecolumn);
+ void clearColumn(unsigned column);
+
+ /**
+ * Calculate the column of a given date. This is the absolute column in a
+ * hypothetical report that covers all of known time. In reality an actual
+ * report will be a subset of that.
+ *
+ * @param _date The date
+ */
+ unsigned columnValue(const QDate& _date) const;
+
+ /**
+ * Calculate the date of the last day covered by a given column.
+ *
+ * @param column The column
+ */
+ QDate columnDate(int column) const;
+
+ /**
+ * Returns the balance of a given cell. Throws an exception once calculateRunningSums() has been run.
+ */
+ MyMoneyMoney cellBalance(const QString& outergroup, const ReportAccount& _row, unsigned column, bool budget);
+
+ /**
+ * Draws a PivotGridRowSet in a chart for the given ERowType
+ */
+ unsigned drawChartRowSet(unsigned rowNum, const bool seriesTotals, const bool accountSeries, KDChartTableData& data, const PivotGridRowSet& rowSet, const ERowType rowType ) const;
+
+ /**
+ * Loads m_rowTypeList with the list of PivotGridRow types that the reporttable
+ * should show
+ */
+ void loadRowTypeList(void);
+
+ /**
+ * If not in expert mode, include all subaccounts for each selected
+ * investment account
+ */
+ void includeInvestmentSubAccounts(void);
+};
+
+
+}
+#endif
+// PIVOTTABLE_H
+// vim:cin:si:ai:et:ts=2:sw=2:
diff --git a/kmymoney2/reports/pivottabletest.cpp b/kmymoney2/reports/pivottabletest.cpp
new file mode 100644
index 0000000..a235c0b
--- /dev/null
+++ b/kmymoney2/reports/pivottabletest.cpp
@@ -0,0 +1,1021 @@
+/***************************************************************************
+ pivottabletest.cpp
+ -------------------
+ copyright : (C) 2002-2005 by Thomas Baumgart
+ email : ipwizard@users.sourceforge.net
+ Ace Jones <ace.j@hotpop.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. *
+ * *
+ ***************************************************************************/
+
+#include <qvaluelist.h>
+#include <qvaluevector.h>
+#include <qdom.h>
+#include <qfile.h>
+
+#include <kdebug.h>
+#include <kdeversion.h>
+#include <kglobal.h>
+#include <kglobalsettings.h>
+#include <klocale.h>
+#include <kstandarddirs.h>
+
+// DOH, mmreport.h uses this without including it!!
+#include "../mymoney/mymoneyaccount.h"
+
+#include "../mymoney/mymoneysecurity.h"
+#include "../mymoney/mymoneyprice.h"
+#include "../mymoney/mymoneyreport.h"
+#include "../mymoney/mymoneystatement.h"
+#include "../mymoney/storage/mymoneystoragedump.h"
+#include "../mymoney/storage/mymoneystoragexml.h"
+
+#define private public
+#include "../reports/pivottable.h"
+#undef private
+
+#include "reportstestcommon.h"
+#include "pivottabletest.h"
+
+using namespace reports;
+using namespace test;
+
+PivotTableTest::PivotTableTest()
+{
+}
+
+void PivotTableTest::setUp ()
+{
+ storage = new MyMoneySeqAccessMgr;
+ file = MyMoneyFile::instance();
+ file->attachStorage(storage);
+
+ MyMoneyFileTransaction ft;
+ file->addCurrency(MyMoneySecurity("CAD", "Canadian Dollar", "C$"));
+ file->addCurrency(MyMoneySecurity("USD", "US Dollar", "$"));
+ file->addCurrency(MyMoneySecurity("JPY", "Japanese Yen", QChar(0x00A5), 100, 1));
+ file->addCurrency(MyMoneySecurity("GBP", "British Pound", "#"));
+ file->setBaseCurrency(file->currency("USD"));
+
+ MyMoneyPayee payeeTest("Test Payee");
+ file->addPayee(payeeTest);
+ MyMoneyPayee payeeTest2("Thomas Baumgart");
+ file->addPayee(payeeTest2);
+
+ acAsset = (MyMoneyFile::instance()->asset().id());
+ acLiability = (MyMoneyFile::instance()->liability().id());
+ acExpense = (MyMoneyFile::instance()->expense().id());
+ acIncome = (MyMoneyFile::instance()->income().id());
+ acChecking = makeAccount(QString("Checking Account"),MyMoneyAccount::Checkings,moCheckingOpen,QDate(2004,5,15),acAsset);
+ acCredit = makeAccount(QString("Credit Card"),MyMoneyAccount::CreditCard,moCreditOpen,QDate(2004,7,15),acLiability);
+ acSolo = makeAccount(QString("Solo"),MyMoneyAccount::Expense,0,QDate(2004,1,11),acExpense);
+ acParent = makeAccount(QString("Parent"),MyMoneyAccount::Expense,0,QDate(2004,1,11),acExpense);
+ acChild = makeAccount(QString("Child"),MyMoneyAccount::Expense,0,QDate(2004,2,11),acParent);
+ acForeign = makeAccount(QString("Foreign"),MyMoneyAccount::Expense,0,QDate(2004,1,11),acExpense);
+
+ acSecondChild = makeAccount(QString("Second Child"),MyMoneyAccount::Expense,0,QDate(2004,2,11),acParent);
+ acGrandChild1 = makeAccount(QString("Grand Child 1"),MyMoneyAccount::Expense,0,QDate(2004,2,11),acChild);
+ acGrandChild2 = makeAccount(QString("Grand Child 2"),MyMoneyAccount::Expense,0,QDate(2004,2,11),acChild);
+
+ MyMoneyInstitution i("Bank of the World","","","","","","");
+ file->addInstitution(i);
+ inBank = i.id();
+ ft.commit();
+}
+
+void PivotTableTest::tearDown ()
+{
+ file->detachStorage(storage);
+ delete storage;
+}
+
+void PivotTableTest::testNetWorthSingle()
+{
+ try
+ {
+ MyMoneyReport filter;
+ filter.setRowType( MyMoneyReport::eAssetLiability );
+ filter.setDateFilter(QDate(2004,1,1),QDate(2004,7,1).addDays(-1));
+ XMLandback(filter);
+ PivotTable networth_f(filter);
+ writeTabletoCSV(networth_f);
+
+ CPPUNIT_ASSERT(networth_f.m_grid["Asset"]["Checking Account"][acChecking][eActual][5]==moCheckingOpen);
+ CPPUNIT_ASSERT(networth_f.m_grid["Asset"]["Checking Account"][acChecking][eActual][6]==moCheckingOpen);
+ CPPUNIT_ASSERT(networth_f.m_grid["Asset"]["Checking Account"].m_total[eActual][5]==moCheckingOpen);
+ CPPUNIT_ASSERT(networth_f.m_grid.m_total[eActual][0]==moZero);
+ CPPUNIT_ASSERT(networth_f.m_grid.m_total[eActual][4]==moZero);
+ CPPUNIT_ASSERT(networth_f.m_grid.m_total[eActual][5]==moCheckingOpen);
+ CPPUNIT_ASSERT(networth_f.m_grid.m_total[eActual][6]==moCheckingOpen);
+ }
+ catch(MyMoneyException *e)
+ {
+ CPPUNIT_FAIL(e->what());
+ delete e;
+ }
+}
+
+void PivotTableTest::testNetWorthOfsetting()
+{
+ // Test the net worth report to make sure it picks up the opening balance for two
+ // accounts opened during the period of the report, one asset & one liability. Test
+ // that it calculates the totals correctly.
+
+ MyMoneyReport filter;
+ filter.setRowType( MyMoneyReport::eAssetLiability );
+ filter.setDateFilter(QDate(2004,1,1),QDate(2005,1,1).addDays(-1));
+ XMLandback(filter);
+ PivotTable networth_f( filter );
+ CPPUNIT_ASSERT(networth_f.m_grid["Liability"]["Credit Card"][acCredit][eActual][7]==-moCreditOpen);
+ CPPUNIT_ASSERT(networth_f.m_grid.m_total[eActual][0]==moZero);
+ CPPUNIT_ASSERT(networth_f.m_grid.m_total[eActual][12]==moCheckingOpen+moCreditOpen);
+
+}
+
+void PivotTableTest::testNetWorthOpeningPrior()
+{
+ // Test the net worth report to make sure it's picking up opening balances PRIOR to
+ // the period of the report.
+
+ MyMoneyReport filter;
+ filter.setRowType( MyMoneyReport::eAssetLiability );
+ filter.setDateFilter(QDate(2005,8,1),QDate(2005,12,31));
+ filter.setName("Net Worth Opening Prior 1");
+ XMLandback(filter);
+ PivotTable networth_f( filter );
+ writeTabletoCSV(networth_f);
+
+ CPPUNIT_ASSERT(networth_f.m_grid["Liability"]["Credit Card"].m_total[eActual][0]==-moCreditOpen);
+ CPPUNIT_ASSERT(networth_f.m_grid["Asset"]["Checking Account"].m_total[eActual][0]==moCheckingOpen);
+ CPPUNIT_ASSERT(networth_f.m_grid.m_total[eActual][0]==moCheckingOpen+moCreditOpen);
+ CPPUNIT_ASSERT(networth_f.m_grid.m_total[eActual][1]==moCheckingOpen+moCreditOpen);
+
+ // Test the net worth report to make sure that transactions prior to the report
+ // period are included in the opening balance
+
+ TransactionHelper t1( QDate(2004,11,7), MyMoneySplit::ActionWithdrawal, moParent1, acCredit, acParent );
+ TransactionHelper t2( QDate(2004,11,7), MyMoneySplit::ActionWithdrawal, moParent2, acCredit, acParent );
+ TransactionHelper t3( QDate(2004,11,7), MyMoneySplit::ActionWithdrawal, moChild, acChecking, acChild );
+
+ filter.setName("Net Worth Opening Prior 2");
+ PivotTable networth_f2( filter );
+ writeTabletoCSV(networth_f2);
+ CPPUNIT_ASSERT(networth_f2.m_grid["Liability"]["Credit Card"].m_total[eActual][1]==-moCreditOpen+moParent);
+ CPPUNIT_ASSERT(networth_f2.m_grid["Asset"]["Checking Account"].m_total[eActual][1]==moCheckingOpen-moChild);
+ CPPUNIT_ASSERT(networth_f2.m_grid.m_total[eActual][1]==moCheckingOpen+moCreditOpen-moChild-moParent);
+}
+
+void PivotTableTest::testNetWorthDateFilter()
+{
+ // Test a net worth report whose period is prior to the time any accounts are open,
+ // so the report should be zero.
+
+ MyMoneyReport filter;
+ filter.setRowType( MyMoneyReport::eAssetLiability );
+ filter.setDateFilter(QDate(2004,1,1),QDate(2004,2,1).addDays(-1));
+ XMLandback(filter);
+ PivotTable networth_f( filter );
+ CPPUNIT_ASSERT(networth_f.m_grid.m_total[eActual][1]==moZero);
+
+}
+
+void PivotTableTest::testSpendingEmpty()
+{
+ // test a spending report with no entries
+
+ MyMoneyReport filter;
+ filter.setRowType( MyMoneyReport::eExpenseIncome );
+ XMLandback(filter);
+ PivotTable spending_f1( filter );
+ CPPUNIT_ASSERT(spending_f1.m_grid.m_total[eActual].m_total==moZero);
+
+ filter.setDateFilter(QDate(2004,9,1),QDate(2005,1,1).addDays(-1));
+ PivotTable spending_f2( filter );
+ CPPUNIT_ASSERT(spending_f2.m_grid.m_total[eActual].m_total==moZero);
+}
+
+void PivotTableTest::testSingleTransaction()
+{
+ // Test a single transaction
+ TransactionHelper t( QDate(2004,10,31), MyMoneySplit::ActionWithdrawal,moSolo, acChecking, acSolo );
+
+ MyMoneyReport filter;
+ filter.setRowType( MyMoneyReport::eExpenseIncome );
+ filter.setDateFilter(QDate(2004,9,1),QDate(2005,1,1).addDays(-1));
+ filter.setName("Spending with Single Transaction.html");
+ XMLandback(filter);
+ PivotTable spending_f( filter );
+ writeTabletoHTML(spending_f,"Spending with Single Transaction.html");
+
+ CPPUNIT_ASSERT(spending_f.m_grid["Expense"]["Solo"][acSolo][eActual][2]==moSolo);
+ CPPUNIT_ASSERT(spending_f.m_grid["Expense"]["Solo"].m_total[eActual][2]==moSolo);
+ CPPUNIT_ASSERT(spending_f.m_grid["Expense"]["Solo"].m_total[eActual][1]==moZero);
+ CPPUNIT_ASSERT(spending_f.m_grid.m_total[eActual][2]==(-moSolo));
+ CPPUNIT_ASSERT(spending_f.m_grid.m_total[eActual].m_total==(-moSolo));
+
+ filter.clear();
+ filter.setRowType(MyMoneyReport::eAssetLiability);
+ filter.setDateFilter(QDate(2004,9,1),QDate(2005,1,1).addDays(-1));
+ XMLandback(filter);
+ PivotTable networth_f( filter );
+ CPPUNIT_ASSERT(networth_f.m_grid["Asset"]["Checking Account"].m_total[eActual][2]==(moCheckingOpen-moSolo) );
+}
+
+void PivotTableTest::testSubAccount()
+{
+ // Test a sub-account with a value, under an account with a value
+
+ TransactionHelper t1( QDate(2004,11,7), MyMoneySplit::ActionWithdrawal, moParent1, acCredit, acParent );
+ TransactionHelper t2( QDate(2004,11,7), MyMoneySplit::ActionWithdrawal, moParent2, acCredit, acParent );
+ TransactionHelper t3( QDate(2004,11,7), MyMoneySplit::ActionWithdrawal, moChild, acCredit, acChild );
+
+ MyMoneyReport filter;
+ filter.setRowType( MyMoneyReport::eExpenseIncome );
+ filter.setDateFilter(QDate(2004,9,1),QDate(2005,1,1).addDays(-1));
+ filter.setDetailLevel(MyMoneyReport::eDetailAll);
+ filter.setName("Spending with Sub-Account");
+ XMLandback(filter);
+ PivotTable spending_f( filter );
+ writeTabletoHTML(spending_f,"Spending with Sub-Account.html");
+
+ CPPUNIT_ASSERT(spending_f.m_grid["Expense"]["Parent"][acParent][eActual][3]==moParent);
+ CPPUNIT_ASSERT(spending_f.m_grid["Expense"]["Parent"][acChild][eActual][3]==moChild);
+ CPPUNIT_ASSERT(spending_f.m_grid["Expense"]["Parent"].m_total[eActual][3]==moParent+moChild);
+ CPPUNIT_ASSERT(spending_f.m_grid["Expense"]["Parent"].m_total[eActual][2]==moZero);
+ CPPUNIT_ASSERT(spending_f.m_grid["Expense"]["Parent"].m_total[eActual].m_total==moParent+moChild);
+ CPPUNIT_ASSERT(spending_f.m_grid.m_total[eActual][3]==(-moParent-moChild));
+ CPPUNIT_ASSERT(spending_f.m_grid.m_total[eActual].m_total==(-moParent-moChild));
+
+ filter.clear();
+ filter.setRowType(MyMoneyReport::eAssetLiability);
+ filter.setDateFilter(QDate(2004,9,1),QDate(2005,1,1).addDays(-1));
+ filter.setName("Net Worth with Sub-Account");
+ XMLandback(filter);
+ PivotTable networth_f( filter );
+ writeTabletoHTML(networth_f,"Net Worth with Sub-Account.html");
+ CPPUNIT_ASSERT(networth_f.m_grid["Liability"]["Credit Card"].m_total[eActual][3]==moParent+moChild-moCreditOpen );
+ CPPUNIT_ASSERT(networth_f.m_grid.m_total[eActual][4] == -moParent-moChild+moCreditOpen+moCheckingOpen );
+
+}
+
+void PivotTableTest::testFilterIEvsIE()
+{
+ // Test that removing an income/spending account will remove the entry from an income/spending report
+ TransactionHelper t1( QDate(2004,10,31), MyMoneySplit::ActionWithdrawal, moSolo, acChecking, acSolo );
+ TransactionHelper t2( QDate(2004,11,7), MyMoneySplit::ActionWithdrawal, moParent1, acCredit, acParent );
+ TransactionHelper t3( QDate(2004,11,7), MyMoneySplit::ActionWithdrawal, moChild, acCredit, acChild );
+
+ MyMoneyReport filter;
+ filter.setRowType( MyMoneyReport::eExpenseIncome );
+ filter.setDateFilter(QDate(2004,9,1),QDate(2005,1,1).addDays(-1));
+ filter.addCategory(acChild);
+ filter.addCategory(acSolo);
+ XMLandback(filter);
+ PivotTable spending_f( filter );
+
+ CPPUNIT_ASSERT(spending_f.m_grid["Expense"]["Parent"].m_total[eActual][3]==moChild);
+ CPPUNIT_ASSERT(spending_f.m_grid["Expense"].m_total[eActual][2]==moSolo);
+ CPPUNIT_ASSERT(spending_f.m_grid.m_total[eActual].m_total==-moSolo-moChild);
+
+}
+
+void PivotTableTest::testFilterALvsAL()
+{
+ // Test that removing an asset/liability account will remove the entry from an asset/liability report
+ TransactionHelper t1( QDate(2004,10,31), MyMoneySplit::ActionWithdrawal, moSolo, acChecking, acSolo );
+ TransactionHelper t2( QDate(2004,11,7), MyMoneySplit::ActionWithdrawal, moParent1, acCredit, acParent );
+ TransactionHelper t3( QDate(2004,11,7), MyMoneySplit::ActionWithdrawal, moChild, acCredit, acChild );
+
+ MyMoneyReport filter;
+ filter.setRowType( MyMoneyReport::eAssetLiability );
+ filter.setDateFilter(QDate(2004,9,1),QDate(2005,1,1).addDays(-1));
+ filter.addAccount(acChecking);
+ filter.addCategory(acChild);
+ filter.addCategory(acSolo);
+ XMLandback(filter);
+ PivotTable networth_f( filter );
+ CPPUNIT_ASSERT(networth_f.m_grid.m_total[eActual][3] == -moSolo+moCheckingOpen );
+}
+
+void PivotTableTest::testFilterALvsIE()
+{
+ // Test that removing an asset/liability account will remove the entry from an income/spending report
+ TransactionHelper t1( QDate(2004,10,31), MyMoneySplit::ActionWithdrawal, moSolo, acChecking, acSolo );
+ TransactionHelper t2( QDate(2004,11,7), MyMoneySplit::ActionWithdrawal, moParent1, acCredit, acParent );
+ TransactionHelper t3( QDate(2004,11,7), MyMoneySplit::ActionWithdrawal, moChild, acCredit, acChild );
+
+ MyMoneyReport filter;
+ filter.setRowType( MyMoneyReport::eExpenseIncome );
+ filter.setDateFilter(QDate(2004,9,1),QDate(2005,1,1).addDays(-1));
+ filter.addAccount(acChecking);
+ CPPUNIT_ASSERT(file->transactionList(filter).count() == 1);
+
+ XMLandback(filter);
+ PivotTable spending_f( filter );
+
+ CPPUNIT_ASSERT(spending_f.m_grid["Expense"].m_total[eActual][3]==moZero);
+ CPPUNIT_ASSERT(spending_f.m_grid["Expense"].m_total[eActual][2]==moSolo);
+ CPPUNIT_ASSERT(spending_f.m_grid.m_total[eActual].m_total==-moSolo);
+}
+
+void PivotTableTest::testFilterAllvsIE()
+{
+ // Test that removing an asset/liability account AND an income/expense
+ // category will remove the entry from an income/spending report
+ TransactionHelper t1( QDate(2004,10,31), MyMoneySplit::ActionWithdrawal, moSolo, acChecking, acSolo );
+ TransactionHelper t2( QDate(2004,11,7), MyMoneySplit::ActionWithdrawal, moParent1, acCredit, acParent );
+ TransactionHelper t3( QDate(2004,11,7), MyMoneySplit::ActionWithdrawal, moChild, acCredit, acChild );
+
+ MyMoneyReport filter;
+ filter.setRowType( MyMoneyReport::eExpenseIncome );
+ filter.setDateFilter(QDate(2004,9,1),QDate(2005,1,1).addDays(-1));
+ filter.addAccount(acCredit);
+ filter.addCategory(acChild);
+ PivotTable spending_f( filter );
+
+ CPPUNIT_ASSERT(spending_f.m_grid["Expense"].m_total[eActual][2]==moZero);
+ CPPUNIT_ASSERT(spending_f.m_grid["Expense"].m_total[eActual][3]==moChild);
+ CPPUNIT_ASSERT(spending_f.m_grid.m_total[eActual].m_total==-moChild);
+}
+
+void PivotTableTest::testFilterBasics()
+{
+ // Test that the filters are operating the way that the reports expect them to
+ TransactionHelper t1( QDate(2004,10,31), MyMoneySplit::ActionWithdrawal, moSolo, acChecking, acSolo );
+ TransactionHelper t2( QDate(2004,11,7), MyMoneySplit::ActionWithdrawal, moParent1, acCredit, acParent );
+ TransactionHelper t3( QDate(2004,11,7), MyMoneySplit::ActionWithdrawal, moParent2, acCredit, acParent );
+ TransactionHelper t4( QDate(2004,11,7), MyMoneySplit::ActionWithdrawal, moChild, acCredit, acChild );
+
+ MyMoneyTransactionFilter filter;
+ filter.clear();
+ filter.setDateFilter(QDate(2004,9,1),QDate(2005,1,1).addDays(-1));
+ filter.addCategory(acSolo);
+ filter.setReportAllSplits(false);
+ filter.setConsiderCategory(true);
+
+ CPPUNIT_ASSERT(file->transactionList(filter).count() == 1);
+
+ filter.addCategory(acParent);
+
+ CPPUNIT_ASSERT(file->transactionList(filter).count() == 3);
+
+ filter.addAccount(acChecking);
+
+ CPPUNIT_ASSERT(file->transactionList(filter).count() == 1);
+
+ filter.clear();
+ filter.setDateFilter(QDate(2004,9,1),QDate(2005,1,1).addDays(-1));
+ filter.addCategory(acParent);
+ filter.addAccount(acCredit);
+ filter.setReportAllSplits(false);
+ filter.setConsiderCategory(true);
+
+ CPPUNIT_ASSERT(file->transactionList(filter).count() == 2);
+}
+
+void PivotTableTest::testMultipleCurrencies()
+{
+ MyMoneyMoney moCanOpening( 0.0 );
+ MyMoneyMoney moJpyOpening( 0.0 );
+ MyMoneyMoney moCanPrice( 0.75 );
+ MyMoneyMoney moJpyPrice( 0.010 );
+ MyMoneyMoney moJpyPrice2( 0.011 );
+ MyMoneyMoney moJpyPrice3( 0.014 );
+ MyMoneyMoney moJpyPrice4( 0.0395 );
+ MyMoneyMoney moCanTransaction( 100.0 );
+ MyMoneyMoney moJpyTransaction( 100.0 );
+
+ QString acCanChecking = makeAccount(QString("Canadian Checking"),MyMoneyAccount::Checkings,moCanOpening,QDate(2003,11,15),acAsset,"CAD");
+ QString acJpyChecking = makeAccount(QString("Japanese Checking"),MyMoneyAccount::Checkings,moJpyOpening,QDate(2003,11,15),acAsset,"JPY");
+ QString acCanCash = makeAccount(QString("Canadian"),MyMoneyAccount::Expense,0,QDate(2004,2,11),acForeign,"CAD");
+ QString acJpyCash = makeAccount(QString("Japanese"),MyMoneyAccount::Expense,0,QDate(2004,2,11),acForeign,"JPY");
+
+ makePrice("CAD",QDate(2004,1,1),MyMoneyMoney(moCanPrice));
+ makePrice("JPY",QDate(2004,1,1),MyMoneyMoney(moJpyPrice));
+ makePrice("JPY",QDate(2004,5,1),MyMoneyMoney(moJpyPrice2));
+ makePrice("JPY",QDate(2004,6,30),MyMoneyMoney(moJpyPrice3));
+ makePrice("JPY",QDate(2004,7,15),MyMoneyMoney(moJpyPrice4));
+
+ TransactionHelper t1( QDate(2004,2,20), MyMoneySplit::ActionWithdrawal,MyMoneyMoney(moJpyTransaction), acJpyChecking, acJpyCash, "JPY" );
+ TransactionHelper t2( QDate(2004,3,20), MyMoneySplit::ActionWithdrawal,MyMoneyMoney(moJpyTransaction), acJpyChecking, acJpyCash, "JPY" );
+ TransactionHelper t3( QDate(2004,4,20), MyMoneySplit::ActionWithdrawal,MyMoneyMoney(moJpyTransaction), acJpyChecking, acJpyCash, "JPY" );
+ TransactionHelper t4( QDate(2004,2,20), MyMoneySplit::ActionWithdrawal,MyMoneyMoney(moCanTransaction), acCanChecking, acCanCash, "CAD" );
+ TransactionHelper t5( QDate(2004,3,20), MyMoneySplit::ActionWithdrawal,MyMoneyMoney(moCanTransaction), acCanChecking, acCanCash, "CAD" );
+ TransactionHelper t6( QDate(2004,4,20), MyMoneySplit::ActionWithdrawal,MyMoneyMoney(moCanTransaction), acCanChecking, acCanCash, "CAD" );
+
+#if 0
+ QFile g( "multicurrencykmy.xml" );
+ g.open( IO_WriteOnly );
+ MyMoneyStorageXML xml;
+ IMyMoneyStorageFormat& interface = xml;
+ interface.writeFile(&g, dynamic_cast<IMyMoneySerialize*> (MyMoneyFile::instance()->storage()));
+ g.close();
+#endif
+
+ MyMoneyReport filter;
+ filter.setRowType( MyMoneyReport::eExpenseIncome );
+ filter.setDateFilter(QDate(2004,1,1),QDate(2005,1,1).addDays(-1));
+ filter.setDetailLevel(MyMoneyReport::eDetailAll);
+ filter.setConvertCurrency(true);
+ filter.setName("Multiple Currency Spending Rerport (with currency conversion)");
+ XMLandback(filter);
+
+ PivotTable spending_f( filter );
+
+ writeTabletoCSV(spending_f);
+
+ // test single foreign currency
+ CPPUNIT_ASSERT(spending_f.m_grid["Expense"]["Foreign"][acCanCash][eActual][2]==(moCanTransaction*moCanPrice));
+ CPPUNIT_ASSERT(spending_f.m_grid["Expense"]["Foreign"][acCanCash][eActual][3]==(moCanTransaction*moCanPrice));
+ CPPUNIT_ASSERT(spending_f.m_grid["Expense"]["Foreign"][acCanCash][eActual][4]==(moCanTransaction*moCanPrice));
+
+ // test multiple foreign currencies under a common parent
+ CPPUNIT_ASSERT(spending_f.m_grid["Expense"]["Foreign"][acJpyCash][eActual][2]==(moJpyTransaction*moJpyPrice));
+ CPPUNIT_ASSERT(spending_f.m_grid["Expense"]["Foreign"][acJpyCash][eActual][3]==(moJpyTransaction*moJpyPrice));
+ CPPUNIT_ASSERT(spending_f.m_grid["Expense"]["Foreign"][acJpyCash][eActual][4]==(moJpyTransaction*moJpyPrice));
+ CPPUNIT_ASSERT(spending_f.m_grid["Expense"]["Foreign"].m_total[eActual][2]==(moJpyTransaction*moJpyPrice + moCanTransaction*moCanPrice));
+ CPPUNIT_ASSERT(spending_f.m_grid["Expense"]["Foreign"].m_total[eActual].m_total==(moJpyTransaction*moJpyPrice + moCanTransaction*moCanPrice + moJpyTransaction*moJpyPrice + moCanTransaction*moCanPrice + moJpyTransaction*moJpyPrice + moCanTransaction*moCanPrice));
+
+ // Test the report type where we DO NOT convert the currency
+ filter.setConvertCurrency(false);
+ filter.setDetailLevel(MyMoneyReport::eDetailAll);
+ filter.setName("Multiple Currency Spending Report (WITHOUT currency conversion)");
+ XMLandback(filter);
+ PivotTable spending_fnc( filter );
+ writeTabletoCSV(spending_fnc);
+
+ CPPUNIT_ASSERT(spending_fnc.m_grid["Expense"]["Foreign"][acCanCash][eActual][2]==(moCanTransaction));
+ CPPUNIT_ASSERT(spending_fnc.m_grid["Expense"]["Foreign"][acCanCash][eActual][3]==(moCanTransaction));
+ CPPUNIT_ASSERT(spending_fnc.m_grid["Expense"]["Foreign"][acCanCash][eActual][4]==(moCanTransaction));
+ CPPUNIT_ASSERT(spending_fnc.m_grid["Expense"]["Foreign"][acJpyCash][eActual][2]==(moJpyTransaction));
+ CPPUNIT_ASSERT(spending_fnc.m_grid["Expense"]["Foreign"][acJpyCash][eActual][3]==(moJpyTransaction));
+ CPPUNIT_ASSERT(spending_fnc.m_grid["Expense"]["Foreign"][acJpyCash][eActual][4]==(moJpyTransaction));
+
+ filter.setConvertCurrency(true);
+ filter.clear();
+ filter.setName("Multiple currency net worth");
+ filter.setRowType(MyMoneyReport::eAssetLiability);
+ filter.setDateFilter(QDate(2004,1,1),QDate(2005,1,1).addDays(-1));
+ XMLandback(filter);
+ PivotTable networth_f( filter );
+ writeTabletoCSV(networth_f);
+
+ // test single foreign currency
+ CPPUNIT_ASSERT(networth_f.m_grid["Asset"]["Canadian Checking"][acCanChecking][eActual][1]==(moCanOpening*moCanPrice));
+ CPPUNIT_ASSERT(networth_f.m_grid["Asset"]["Canadian Checking"][acCanChecking][eActual][2]==((moCanOpening-moCanTransaction)*moCanPrice));
+ CPPUNIT_ASSERT(networth_f.m_grid["Asset"]["Canadian Checking"][acCanChecking][eActual][3]==((moCanOpening-moCanTransaction-moCanTransaction)*moCanPrice));
+ CPPUNIT_ASSERT(networth_f.m_grid["Asset"]["Canadian Checking"][acCanChecking][eActual][4]==((moCanOpening-moCanTransaction-moCanTransaction-moCanTransaction)*moCanPrice));
+ CPPUNIT_ASSERT(networth_f.m_grid["Asset"]["Canadian Checking"][acCanChecking][eActual][12]==((moCanOpening-moCanTransaction-moCanTransaction-moCanTransaction)*moCanPrice));
+
+ // test Stable currency price, fluctuating account balance
+ CPPUNIT_ASSERT(networth_f.m_grid["Asset"]["Japanese Checking"][acJpyChecking][eActual][1]==(moJpyOpening*moJpyPrice));
+ CPPUNIT_ASSERT(networth_f.m_grid["Asset"]["Japanese Checking"][acJpyChecking][eActual][2]==((moJpyOpening-moJpyTransaction)*moJpyPrice));
+ CPPUNIT_ASSERT(networth_f.m_grid["Asset"]["Japanese Checking"][acJpyChecking][eActual][3]==((moJpyOpening-moJpyTransaction-moJpyTransaction)*moJpyPrice));
+ CPPUNIT_ASSERT(networth_f.m_grid["Asset"]["Japanese Checking"][acJpyChecking][eActual][4]==((moJpyOpening-moJpyTransaction-moJpyTransaction-moJpyTransaction)*moJpyPrice));
+
+ // test Fluctuating currency price, stable account balance
+ CPPUNIT_ASSERT(networth_f.m_grid["Asset"]["Japanese Checking"][acJpyChecking][eActual][5]==((moJpyOpening-moJpyTransaction-moJpyTransaction-moJpyTransaction)*moJpyPrice2));
+ CPPUNIT_ASSERT(networth_f.m_grid["Asset"]["Japanese Checking"][acJpyChecking][eActual][6]==((moJpyOpening-moJpyTransaction-moJpyTransaction-moJpyTransaction)*moJpyPrice3));
+ CPPUNIT_ASSERT(networth_f.m_grid["Asset"]["Japanese Checking"][acJpyChecking][eActual][7]==((moJpyOpening-moJpyTransaction-moJpyTransaction-moJpyTransaction)*moJpyPrice4));
+
+ // test multiple currencies totalled up
+ CPPUNIT_ASSERT(networth_f.m_grid["Asset"].m_total[eActual][4]==((moCanOpening-moCanTransaction-moCanTransaction-moCanTransaction)*moCanPrice)+((moJpyOpening-moJpyTransaction-moJpyTransaction-moJpyTransaction)*moJpyPrice));
+ CPPUNIT_ASSERT(networth_f.m_grid["Asset"].m_total[eActual][5]==((moCanOpening-moCanTransaction-moCanTransaction-moCanTransaction)*moCanPrice)+((moJpyOpening-moJpyTransaction-moJpyTransaction-moJpyTransaction)*moJpyPrice2)+moCheckingOpen);
+
+}
+
+void PivotTableTest::testAdvancedFilter()
+{
+ // test more advanced filtering capabilities
+
+ // amount
+ {
+ TransactionHelper t1( QDate(2004,11,7), MyMoneySplit::ActionWithdrawal, moParent1, acCredit, acParent );
+ TransactionHelper t2( QDate(2004,11,7), MyMoneySplit::ActionWithdrawal, moChild, acCredit, acChild );
+
+ MyMoneyReport filter;
+ filter.setRowType( MyMoneyReport::eExpenseIncome );
+ filter.setDateFilter(QDate(2004,1,1),QDate(2005,1,1).addDays(-1));
+ filter.setAmountFilter(moChild,moChild);
+ XMLandback(filter);
+ PivotTable spending_f( filter );
+ CPPUNIT_ASSERT(spending_f.m_grid.m_total[eActual].m_total==-moChild);
+ }
+
+ // payee (specific)
+ {
+ TransactionHelper t1( QDate(2004,10,31), MyMoneySplit::ActionWithdrawal, moSolo, acChecking, acSolo );
+ TransactionHelper t2( QDate(2004,11,7), MyMoneySplit::ActionWithdrawal, moParent1, acCredit, acParent );
+ TransactionHelper t3( QDate(2004,11,7), MyMoneySplit::ActionWithdrawal, moChild, acCredit, acChild );
+ TransactionHelper t4( QDate(2004,11,7), MyMoneySplit::ActionWithdrawal, moThomas, acCredit, acParent, QString(), "Thomas Baumgart" );
+
+ MyMoneyReport filter;
+ filter.setRowType( MyMoneyReport::eExpenseIncome );
+ filter.setDateFilter(QDate(2004,1,1),QDate(2005,1,1).addDays(-1));
+ filter.addPayee(MyMoneyFile::instance()->payeeByName("Thomas Baumgart").id());
+ filter.setName("Spending with Payee Filter");
+ XMLandback(filter);
+ PivotTable spending_f( filter );
+ writeTabletoHTML(spending_f,"Spending with Payee Filter.html");
+
+ CPPUNIT_ASSERT(spending_f.m_grid["Expense"]["Parent"][acParent][eActual][11]==moThomas);
+ CPPUNIT_ASSERT(spending_f.m_grid.m_total[eActual].m_total==-moThomas);
+ }
+ // payee (no payee)
+ {
+ TransactionHelper t1( QDate(2004,10,31), MyMoneySplit::ActionWithdrawal, moSolo, acChecking, acSolo );
+ TransactionHelper t2( QDate(2004,11,7), MyMoneySplit::ActionWithdrawal, moParent1, acCredit, acParent );
+ TransactionHelper t3( QDate(2004,11,7), MyMoneySplit::ActionWithdrawal, moChild, acCredit, acChild );
+ TransactionHelper t4( QDate(2004,11,7), MyMoneySplit::ActionWithdrawal, moNoPayee, acCredit, acParent, QString(), QString() );
+
+ MyMoneyReport filter;
+ filter.setRowType( MyMoneyReport::eExpenseIncome );
+ filter.setDateFilter(QDate(2004,1,1),QDate(2005,1,1).addDays(-1));
+ filter.addPayee(QString());
+ XMLandback(filter);
+ PivotTable spending_f( filter );
+ CPPUNIT_ASSERT(spending_f.m_grid["Expense"]["Parent"][acParent][eActual][11]==moNoPayee);
+ CPPUNIT_ASSERT(spending_f.m_grid.m_total[eActual].m_total==-moNoPayee);
+ }
+
+ // text
+ {
+ TransactionHelper t1( QDate(2004,10,31), MyMoneySplit::ActionWithdrawal, moSolo, acChecking, acSolo );
+ TransactionHelper t2( QDate(2004,11,7), MyMoneySplit::ActionWithdrawal, moParent1, acCredit, acParent );
+ TransactionHelper t3( QDate(2004,11,7), MyMoneySplit::ActionWithdrawal, moChild, acCredit, acChild );
+ TransactionHelper t4( QDate(2004,11,7), MyMoneySplit::ActionWithdrawal, moThomas, acCredit, acParent, QString(), "Thomas Baumgart" );
+
+ MyMoneyReport filter;
+ filter.setRowType( MyMoneyReport::eExpenseIncome );
+ filter.setDateFilter(QDate(2004,1,1),QDate(2005,1,1).addDays(-1));
+ filter.setTextFilter(QRegExp("Thomas"));
+ XMLandback(filter);
+ PivotTable spending_f( filter );
+ }
+
+ // type (payment, deposit, transfer)
+ {
+ TransactionHelper t1( QDate(2004,1,1), MyMoneySplit::ActionWithdrawal, moSolo, acChecking, acSolo );
+ TransactionHelper t2( QDate(2004,2,1), MyMoneySplit::ActionDeposit, -moParent1, acCredit, acParent );
+ TransactionHelper t3( QDate(2004,11,1), MyMoneySplit::ActionTransfer, moChild, acCredit, acChecking );
+
+ MyMoneyReport filter;
+ filter.setRowType( MyMoneyReport::eExpenseIncome );
+ filter.addType(MyMoneyTransactionFilter::payments);
+ XMLandback(filter);
+ PivotTable spending_f( filter );
+
+ CPPUNIT_ASSERT(spending_f.m_grid.m_total[eActual].m_total == -moSolo);
+
+ filter.clear();
+ filter.addType(MyMoneyTransactionFilter::deposits);
+ XMLandback(filter);
+ PivotTable spending_f2( filter );
+
+ CPPUNIT_ASSERT(spending_f2.m_grid.m_total[eActual].m_total == moParent1);
+
+ filter.clear();
+ filter.addType(MyMoneyTransactionFilter::transfers);
+ XMLandback(filter);
+ PivotTable spending_f3( filter );
+
+ CPPUNIT_ASSERT(spending_f3.m_grid.m_total[eActual].m_total == moZero);
+
+ filter.setRowType(MyMoneyReport::eAssetLiability);
+ filter.setDateFilter( QDate(2004,1,1), QDate(2004,12,31) );
+ XMLandback(filter);
+ PivotTable networth_f4( filter );
+
+ CPPUNIT_ASSERT(networth_f4.m_grid["Asset"].m_total[eActual][11] == moCheckingOpen + moChild);
+ CPPUNIT_ASSERT(networth_f4.m_grid["Liability"].m_total[eActual][11] == - moCreditOpen + moChild);
+ CPPUNIT_ASSERT(networth_f4.m_grid.m_total[eActual][10] == moCheckingOpen + moCreditOpen);
+ CPPUNIT_ASSERT(networth_f4.m_grid.m_total[eActual][11] == moCheckingOpen + moCreditOpen);
+ }
+
+ // state (reconciled, cleared, not)
+ {
+ TransactionHelper t1( QDate(2004,1,1), MyMoneySplit::ActionWithdrawal, moSolo, acChecking, acSolo );
+ TransactionHelper t2( QDate(2004,2,1), MyMoneySplit::ActionWithdrawal, moParent1, acCredit, acParent );
+ TransactionHelper t3( QDate(2004,3,1), MyMoneySplit::ActionWithdrawal, moParent2, acCredit, acParent );
+ TransactionHelper t4( QDate(2004,4,1), MyMoneySplit::ActionWithdrawal, moChild, acCredit, acChild );
+
+ QValueList<MyMoneySplit> splits = t1.splits();
+ splits[0].setReconcileFlag(MyMoneySplit::Cleared);
+ splits[1].setReconcileFlag(MyMoneySplit::Cleared);
+ t1.modifySplit(splits[0]);
+ t1.modifySplit(splits[1]);
+ t1.update();
+
+ splits.clear();
+ splits = t2.splits();
+ splits[0].setReconcileFlag(MyMoneySplit::Reconciled);
+ splits[1].setReconcileFlag(MyMoneySplit::Reconciled);
+ t2.modifySplit(splits[0]);
+ t2.modifySplit(splits[1]);
+ t2.update();
+
+ MyMoneyReport filter;
+ filter.setRowType( MyMoneyReport::eExpenseIncome );
+ filter.setDateFilter(QDate(2004,1,1),QDate(2005,1,1).addDays(-1));
+ filter.addState(MyMoneyTransactionFilter::cleared);
+ XMLandback(filter);
+ PivotTable spending_f( filter );
+
+ CPPUNIT_ASSERT(spending_f.m_grid.m_total[eActual].m_total==-moSolo);
+
+ filter.addState(MyMoneyTransactionFilter::reconciled);
+ XMLandback(filter);
+ PivotTable spending_f2( filter );
+
+ CPPUNIT_ASSERT(spending_f2.m_grid.m_total[eActual].m_total==-moSolo-moParent1);
+
+ filter.clear();
+ filter.addState(MyMoneyTransactionFilter::notReconciled);
+ XMLandback(filter);
+ PivotTable spending_f3( filter );
+
+ CPPUNIT_ASSERT(spending_f3.m_grid.m_total[eActual].m_total==-moChild-moParent2);
+ }
+
+ // number
+ {
+ TransactionHelper t1( QDate(2004,10,31), MyMoneySplit::ActionWithdrawal, moSolo, acChecking, acSolo );
+ TransactionHelper t2( QDate(2004,11,7), MyMoneySplit::ActionWithdrawal, moParent1, acCredit, acParent );
+ TransactionHelper t3( QDate(2004,11,7), MyMoneySplit::ActionWithdrawal, moParent2, acCredit, acParent );
+ TransactionHelper t4( QDate(2004,11,7), MyMoneySplit::ActionWithdrawal, moChild, acCredit, acChild );
+
+ QValueList<MyMoneySplit> splits = t1.splits();
+ splits[0].setNumber("1");
+ splits[1].setNumber("1");
+ t1.modifySplit(splits[0]);
+ t1.modifySplit(splits[1]);
+ t1.update();
+
+ splits.clear();
+ splits = t2.splits();
+ splits[0].setNumber("2");
+ splits[1].setNumber("2");
+ t2.modifySplit(splits[0]);
+ t2.modifySplit(splits[1]);
+ t2.update();
+
+ splits.clear();
+ splits = t3.splits();
+ splits[0].setNumber("3");
+ splits[1].setNumber("3");
+ t3.modifySplit(splits[0]);
+ t3.modifySplit(splits[1]);
+ t3.update();
+
+ splits.clear();
+ splits = t2.splits();
+ splits[0].setNumber("4");
+ splits[1].setNumber("4");
+ t4.modifySplit(splits[0]);
+ t4.modifySplit(splits[1]);
+ t4.update();
+
+ MyMoneyReport filter;
+ filter.setRowType( MyMoneyReport::eExpenseIncome );
+ filter.setDateFilter(QDate(2004,1,1),QDate(2005,1,1).addDays(-1));
+ filter.setNumberFilter("1","3");
+ XMLandback(filter);
+ PivotTable spending_f( filter );
+ CPPUNIT_ASSERT(spending_f.m_grid.m_total[eActual].m_total==-moSolo-moParent1-moParent2);
+ }
+
+ // blank dates
+ {
+ TransactionHelper t1y1( QDate(2003,10,1), MyMoneySplit::ActionWithdrawal, moSolo, acChecking, acSolo );
+ TransactionHelper t2y1( QDate(2003,11,1), MyMoneySplit::ActionWithdrawal, moParent1, acCredit, acParent );
+ TransactionHelper t3y1( QDate(2003,12,1), MyMoneySplit::ActionWithdrawal, moParent2, acCredit, acParent );
+
+ TransactionHelper t1y2( QDate(2004,4,1), MyMoneySplit::ActionWithdrawal, moSolo, acChecking, acSolo );
+ TransactionHelper t2y2( QDate(2004,5,1), MyMoneySplit::ActionWithdrawal, moParent1, acCredit, acParent );
+ TransactionHelper t3y2( QDate(2004,6,1), MyMoneySplit::ActionWithdrawal, moParent2, acCredit, acParent );
+
+ TransactionHelper t1y3( QDate(2005,1,1), MyMoneySplit::ActionWithdrawal, moSolo, acChecking, acSolo );
+ TransactionHelper t2y3( QDate(2005,5,1), MyMoneySplit::ActionWithdrawal, moParent1, acCredit, acParent );
+ TransactionHelper t3y3( QDate(2005,9,1), MyMoneySplit::ActionWithdrawal, moParent2, acCredit, acParent );
+
+ MyMoneyReport filter;
+ filter.setRowType( MyMoneyReport::eExpenseIncome );
+ filter.setDateFilter(QDate(),QDate(2004,7,1));
+ XMLandback(filter);
+ PivotTable spending_f( filter );
+ CPPUNIT_ASSERT(spending_f.m_grid.m_total[eActual].m_total==-moSolo-moParent1-moParent2-moSolo-moParent1-moParent2);
+
+ filter.clear();
+ XMLandback(filter);
+ PivotTable spending_f2( filter );
+ CPPUNIT_ASSERT(spending_f2.m_grid.m_total[eActual].m_total==-moSolo-moParent1-moParent2-moSolo-moParent1-moParent2-moSolo-moParent1-moParent2);
+
+ }
+
+}
+
+void PivotTableTest::testColumnType()
+{
+ // test column type values of other than 'month'
+
+ TransactionHelper t1q1( QDate(2004,1,1), MyMoneySplit::ActionWithdrawal, moSolo, acChecking, acSolo );
+ TransactionHelper t2q1( QDate(2004,2,1), MyMoneySplit::ActionWithdrawal, moParent1, acCredit, acParent );
+ TransactionHelper t3q1( QDate(2004,3,1), MyMoneySplit::ActionWithdrawal, moParent2, acCredit, acParent );
+
+ TransactionHelper t1q2( QDate(2004,4,1), MyMoneySplit::ActionWithdrawal, moSolo, acChecking, acSolo );
+ TransactionHelper t2q2( QDate(2004,5,1), MyMoneySplit::ActionWithdrawal, moParent1, acCredit, acParent );
+ TransactionHelper t3q2( QDate(2004,6,1), MyMoneySplit::ActionWithdrawal, moParent2, acCredit, acParent );
+
+ TransactionHelper t1y2( QDate(2005,1,1), MyMoneySplit::ActionWithdrawal, moSolo, acChecking, acSolo );
+ TransactionHelper t2y2( QDate(2005,5,1), MyMoneySplit::ActionWithdrawal, moParent1, acCredit, acParent );
+ TransactionHelper t3y2( QDate(2005,9,1), MyMoneySplit::ActionWithdrawal, moParent2, acCredit, acParent );
+
+ MyMoneyReport filter;
+ filter.setRowType( MyMoneyReport::eExpenseIncome );
+ filter.setDateFilter(QDate(2003,12,31),QDate(2005,12,31));
+ filter.setRowType( MyMoneyReport::eExpenseIncome );
+ filter.setColumnType(MyMoneyReport::eBiMonths);
+ XMLandback(filter);
+ PivotTable spending_b( filter );
+
+ CPPUNIT_ASSERT(spending_b.m_grid.m_total[eActual][1] == moZero);
+ CPPUNIT_ASSERT(spending_b.m_grid.m_total[eActual][2] == -moParent1-moSolo);
+ CPPUNIT_ASSERT(spending_b.m_grid.m_total[eActual][3] == -moParent2-moSolo);
+ CPPUNIT_ASSERT(spending_b.m_grid.m_total[eActual][4] == -moParent);
+ CPPUNIT_ASSERT(spending_b.m_grid.m_total[eActual][5] == moZero);
+ CPPUNIT_ASSERT(spending_b.m_grid.m_total[eActual][6] == moZero);
+ CPPUNIT_ASSERT(spending_b.m_grid.m_total[eActual][7] == moZero);
+ CPPUNIT_ASSERT(spending_b.m_grid.m_total[eActual][8] == -moSolo);
+ CPPUNIT_ASSERT(spending_b.m_grid.m_total[eActual][9] == moZero);
+ CPPUNIT_ASSERT(spending_b.m_grid.m_total[eActual][10] == -moParent1);
+ CPPUNIT_ASSERT(spending_b.m_grid.m_total[eActual][11] == moZero);
+ CPPUNIT_ASSERT(spending_b.m_grid.m_total[eActual][12] == -moParent2);
+ CPPUNIT_ASSERT(spending_b.m_grid.m_total[eActual][13] == moZero);
+
+ filter.setColumnType(MyMoneyReport::eQuarters);
+ XMLandback(filter);
+ PivotTable spending_q( filter );
+
+ CPPUNIT_ASSERT(spending_q.m_grid.m_total[eActual][1] == moZero);
+ CPPUNIT_ASSERT(spending_q.m_grid.m_total[eActual][2] == -moSolo-moParent);
+ CPPUNIT_ASSERT(spending_q.m_grid.m_total[eActual][3] == -moSolo-moParent);
+ CPPUNIT_ASSERT(spending_q.m_grid.m_total[eActual][4] == moZero);
+ CPPUNIT_ASSERT(spending_q.m_grid.m_total[eActual][5] == moZero);
+ CPPUNIT_ASSERT(spending_q.m_grid.m_total[eActual][6] == -moSolo);
+ CPPUNIT_ASSERT(spending_q.m_grid.m_total[eActual][7] == -moParent1);
+ CPPUNIT_ASSERT(spending_q.m_grid.m_total[eActual][8] == -moParent2);
+ CPPUNIT_ASSERT(spending_q.m_grid.m_total[eActual][9] == moZero);
+
+ filter.setRowType( MyMoneyReport::eAssetLiability );
+ filter.setName( "Net Worth by Quarter" );
+ XMLandback(filter);
+ PivotTable networth_q( filter );
+ writeTabletoHTML( networth_q, "Net Worth by Quarter.html" );
+
+ CPPUNIT_ASSERT(networth_q.m_grid.m_total[eActual][1] == moZero);
+ CPPUNIT_ASSERT(networth_q.m_grid.m_total[eActual][2] == -moSolo-moParent);
+ CPPUNIT_ASSERT(networth_q.m_grid.m_total[eActual][3] == -moSolo-moParent-moSolo-moParent+moCheckingOpen);
+ CPPUNIT_ASSERT(networth_q.m_grid.m_total[eActual][4] == -moSolo-moParent-moSolo-moParent+moCheckingOpen+moCreditOpen);
+ CPPUNIT_ASSERT(networth_q.m_grid.m_total[eActual][5] == -moSolo-moParent-moSolo-moParent+moCheckingOpen+moCreditOpen);
+ CPPUNIT_ASSERT(networth_q.m_grid.m_total[eActual][6] == -moSolo-moSolo-moParent-moSolo-moParent+moCheckingOpen+moCreditOpen);
+ CPPUNIT_ASSERT(networth_q.m_grid.m_total[eActual][7] == -moParent1-moSolo-moSolo-moParent-moSolo-moParent+moCheckingOpen+moCreditOpen);
+ CPPUNIT_ASSERT(networth_q.m_grid.m_total[eActual][8] == -moParent2-moParent1-moSolo-moSolo-moParent-moSolo-moParent+moCheckingOpen+moCreditOpen);
+ CPPUNIT_ASSERT(networth_q.m_grid.m_total[eActual][9] == -moParent2-moParent1-moSolo-moSolo-moParent-moSolo-moParent+moCheckingOpen+moCreditOpen);
+
+ filter.setRowType( MyMoneyReport::eExpenseIncome );
+ filter.setColumnType(MyMoneyReport::eYears);
+ XMLandback(filter);
+ PivotTable spending_y( filter );
+
+ CPPUNIT_ASSERT(spending_y.m_grid.m_total[eActual][1] == moZero);
+ CPPUNIT_ASSERT(spending_y.m_grid.m_total[eActual][2] == -moSolo-moParent-moSolo-moParent);
+ CPPUNIT_ASSERT(spending_y.m_grid.m_total[eActual][3] == -moSolo-moParent);
+ CPPUNIT_ASSERT(spending_y.m_grid.m_total[eActual].m_total == -moSolo-moParent-moSolo-moParent-moSolo-moParent);
+
+ filter.setRowType( MyMoneyReport::eAssetLiability );
+ XMLandback(filter);
+ PivotTable networth_y( filter );
+
+ CPPUNIT_ASSERT(networth_y.m_grid.m_total[eActual][1] == moZero);
+ CPPUNIT_ASSERT(networth_y.m_grid.m_total[eActual][2] == -moSolo-moParent-moSolo-moParent+moCheckingOpen+moCreditOpen);
+ CPPUNIT_ASSERT(networth_y.m_grid.m_total[eActual][3] == -moSolo-moParent-moSolo-moParent-moSolo-moParent+moCheckingOpen+moCreditOpen);
+
+ // Test days-based reports
+
+ TransactionHelper t1d1( QDate(2004,7,1), MyMoneySplit::ActionWithdrawal, moSolo, acChecking, acSolo );
+ TransactionHelper t2d1( QDate(2004,7,1), MyMoneySplit::ActionWithdrawal, moParent1, acCredit, acParent );
+ TransactionHelper t3d1( QDate(2004,7,5), MyMoneySplit::ActionWithdrawal, moParent2, acCredit, acParent );
+
+ TransactionHelper t1d2( QDate(2004,7,14), MyMoneySplit::ActionWithdrawal, moSolo, acChecking, acSolo );
+ TransactionHelper t2d2( QDate(2004,7,15), MyMoneySplit::ActionWithdrawal, moParent1, acCredit, acParent );
+ TransactionHelper t3d2( QDate(2004,7,20), MyMoneySplit::ActionWithdrawal, moParent2, acCredit, acParent );
+
+ TransactionHelper t1d3( QDate(2004,8,2), MyMoneySplit::ActionWithdrawal, moSolo, acChecking, acSolo );
+ TransactionHelper t2d3( QDate(2004,8,3), MyMoneySplit::ActionWithdrawal, moParent1, acCredit, acParent );
+ TransactionHelper t3d3( QDate(2004,8,4), MyMoneySplit::ActionWithdrawal, moParent2, acCredit, acParent );
+
+ filter.setDateFilter(QDate(2004,7,2),QDate(2004,7,14));
+ filter.setRowType( MyMoneyReport::eExpenseIncome );
+ filter.setColumnType(MyMoneyReport::eMonths);
+ filter.setColumnsAreDays(true);
+
+ XMLandback(filter);
+ PivotTable spending_days( filter );
+ writeTabletoHTML(spending_days,"Spending by Days.html");
+
+ CPPUNIT_ASSERT(spending_days.m_grid.m_total[eActual][4] == -moParent2);
+ CPPUNIT_ASSERT(spending_days.m_grid.m_total[eActual][13] == -moSolo);
+ CPPUNIT_ASSERT(spending_days.m_grid.m_total[eActual].m_total == -moSolo-moParent2);
+
+ unsigned save_dayweekstart = KGlobal::locale()->weekStartDay();
+ KGlobal::locale()->setWeekStartDay(2);
+
+ filter.setDateFilter(QDate(2004,7,2),QDate(2004,8,1));
+ filter.setRowType( MyMoneyReport::eExpenseIncome );
+ filter.setColumnType(static_cast<MyMoneyReport::EColumnType>(7));
+ filter.setColumnsAreDays(true);
+
+ XMLandback(filter);
+ PivotTable spending_weeks( filter );
+ writeTabletoHTML(spending_weeks,"Spending by Weeks.html");
+
+ KGlobal::locale()->setWeekStartDay(save_dayweekstart);
+
+ CPPUNIT_ASSERT(spending_weeks.m_grid.m_total[eActual][0] == moZero);
+ CPPUNIT_ASSERT(spending_weeks.m_grid.m_total[eActual][1] == -moParent2);
+ CPPUNIT_ASSERT(spending_weeks.m_grid.m_total[eActual][2] == moZero);
+ CPPUNIT_ASSERT(spending_weeks.m_grid.m_total[eActual][3] == -moSolo-moParent1);
+ CPPUNIT_ASSERT(spending_weeks.m_grid.m_total[eActual][4] == -moParent2);
+ CPPUNIT_ASSERT(spending_weeks.m_grid.m_total[eActual][5] == moZero);
+ CPPUNIT_ASSERT(spending_weeks.m_grid.m_total[eActual].m_total == -moSolo-moParent-moParent2);
+
+
+}
+
+void PivotTableTest::testInvestment(void)
+{
+ try
+ {
+ // Equities
+ eqStock1 = makeEquity("Stock1","STK1");
+ eqStock2 = makeEquity("Stock2","STK2");
+
+ // Accounts
+ acInvestment = makeAccount("Investment",MyMoneyAccount::Investment,moZero,QDate(2004,1,1),acAsset);
+ acStock1 = makeAccount("Stock 1",MyMoneyAccount::Stock,moZero,QDate(2004,1,1),acInvestment,eqStock1);
+ acStock2 = makeAccount("Stock 2",MyMoneyAccount::Stock,moZero,QDate(2004,1,1),acInvestment,eqStock2);
+ acDividends = makeAccount("Dividends",MyMoneyAccount::Income,moZero,QDate(2004,1,1),acIncome);
+
+ // Transactions
+ // Date Action Shares Price Stock Asset Income
+ InvTransactionHelper s1b1( QDate(2004,2,1), MyMoneySplit::ActionBuyShares, 1000.00, 100.00, acStock1, acChecking, QString() );
+ InvTransactionHelper s1b2( QDate(2004,3,1), MyMoneySplit::ActionBuyShares, 1000.00, 110.00, acStock1, acChecking, QString() );
+ InvTransactionHelper s1s1( QDate(2004,4,1), MyMoneySplit::ActionBuyShares, -200.00, 120.00, acStock1, acChecking, QString() );
+ InvTransactionHelper s1s2( QDate(2004,5,1), MyMoneySplit::ActionBuyShares, -200.00, 100.00, acStock1, acChecking, QString() );
+ InvTransactionHelper s1r1( QDate(2004,6,1), MyMoneySplit::ActionReinvestDividend, 50.00, 100.00, acStock1, QString(), acDividends );
+ InvTransactionHelper s1r2( QDate(2004,7,1), MyMoneySplit::ActionReinvestDividend, 50.00, 80.00, acStock1, QString(), acDividends );
+ InvTransactionHelper s1c1( QDate(2004,8,1), MyMoneySplit::ActionDividend, 10.00, 100.00, acStock1, acChecking, acDividends );
+ InvTransactionHelper s1c2( QDate(2004,9,1), MyMoneySplit::ActionDividend, 10.00, 120.00, acStock1, acChecking, acDividends );
+
+ makeEquityPrice( eqStock1, QDate(2004,10,1), 100.00 );
+
+ //
+ // Net Worth Report (with investments)
+ //
+
+ MyMoneyReport networth_r;
+ networth_r.setRowType( MyMoneyReport::eAssetLiability );
+ networth_r.setDateFilter(QDate(2004,1,1),QDate(2004,12,31).addDays(-1));
+ XMLandback(networth_r);
+ PivotTable networth(networth_r);
+
+ networth.dump("networth_i.html");
+
+ CPPUNIT_ASSERT(networth.m_grid["Asset"]["Investment"].m_total[eActual][1]==moZero);
+ // 1000 shares @ $100.00
+ CPPUNIT_ASSERT(networth.m_grid["Asset"]["Investment"].m_total[eActual][2]==MyMoneyMoney(100000.0));
+ // 2000 shares @ $110.00
+ CPPUNIT_ASSERT(networth.m_grid["Asset"]["Investment"].m_total[eActual][3]==MyMoneyMoney(220000.0));
+ // 1800 shares @ $120.00
+ CPPUNIT_ASSERT(networth.m_grid["Asset"]["Investment"].m_total[eActual][4]==MyMoneyMoney(216000.0));
+ // 1600 shares @ $100.00
+ CPPUNIT_ASSERT(networth.m_grid["Asset"]["Investment"].m_total[eActual][5]==MyMoneyMoney(160000.0));
+ // 1650 shares @ $100.00
+ CPPUNIT_ASSERT(networth.m_grid["Asset"]["Investment"].m_total[eActual][6]==MyMoneyMoney(165000.0));
+ // 1700 shares @ $ 80.00
+ CPPUNIT_ASSERT(networth.m_grid["Asset"]["Investment"].m_total[eActual][7]==MyMoneyMoney(136000.0));
+ // 1700 shares @ $100.00
+ CPPUNIT_ASSERT(networth.m_grid["Asset"]["Investment"].m_total[eActual][8]==MyMoneyMoney(170000.0));
+ // 1700 shares @ $120.00
+ CPPUNIT_ASSERT(networth.m_grid["Asset"]["Investment"].m_total[eActual][9]==MyMoneyMoney(204000.0));
+ // 1700 shares @ $100.00
+ CPPUNIT_ASSERT(networth.m_grid["Asset"]["Investment"].m_total[eActual][10]==MyMoneyMoney(170000.0));
+
+#if 0
+ // Dump file & reports
+ QFile g( "investmentkmy.xml" );
+ g.open( IO_WriteOnly );
+ MyMoneyStorageXML xml;
+ IMyMoneyStorageFormat& interface = xml;
+ interface.writeFile(&g, dynamic_cast<IMyMoneySerialize*> (MyMoneyFile::instance()->storage()));
+ g.close();
+
+ invtran.dump("invtran.html","<html><head></head><body>%1</body></html>");
+ invhold.dump("invhold.html","<html><head></head><body>%1</body></html>");
+#endif
+
+ }
+ catch(MyMoneyException *e)
+ {
+ CPPUNIT_FAIL(e->what());
+ delete e;
+ }
+}
+
+void PivotTableTest::testBudget(void)
+{
+
+ // 1. Budget on A, transations on A
+ {
+ BudgetHelper budget;
+ budget += BudgetEntryHelper( QDate(2006,1,1), acSolo, false, 100.0 );
+
+ MyMoneyReport report(MyMoneyReport::eBudgetActual,
+ MyMoneyReport::eMonths,
+ MyMoneyTransactionFilter::yearToDate,
+ MyMoneyReport::eDetailTop,
+ "Yearly Budgeted vs. Actual","Default Report");
+ PivotTable table(report);
+ }
+
+ // 2. Budget on B, not applying to sub accounts, transactions on B and B:1
+ {
+ BudgetHelper budget;
+ budget += BudgetEntryHelper( QDate(2006,1,1), acParent, false, 100.0 );
+ MyMoneyReport report(MyMoneyReport::eBudgetActual,
+ MyMoneyReport::eMonths,
+ MyMoneyTransactionFilter::yearToDate,
+ MyMoneyReport::eDetailTop,
+ "Yearly Budgeted vs. Actual","Default Report");
+ PivotTable table(report);
+ }
+
+ // - Both B and B:1 totals should show up
+ // - B actuals compare against B budget
+ // - B:1 actuals compare against 0
+
+ // 3. Budget on C, applying to sub accounts, transactions on C and C:1 and C:1:a
+ {
+ BudgetHelper budget;
+ budget += BudgetEntryHelper( QDate(2006,1,1), acParent, true, 100.0 );
+ MyMoneyReport report(MyMoneyReport::eBudgetActual,
+ MyMoneyReport::eMonths,
+ MyMoneyTransactionFilter::yearToDate,
+ MyMoneyReport::eDetailTop ,
+ "Yearly Budgeted vs. Actual","Default Report");
+ PivotTable table(report);
+ }
+
+ // - Only C totals show up, not C:1 or C:1:a totals
+ // - C + C:1 totals compare against C budget
+
+ // 4. Budget on D, not applying to sub accounts, budget on D:1 not applying, budget on D:2 applying. Transactions on D, D:1, D:2, D:2:a, D:2:b
+ {
+ BudgetHelper budget;
+ budget += BudgetEntryHelper( QDate(2006,1,1), acParent, false, 100.0 );
+ budget += BudgetEntryHelper( QDate(2006,1,1), acChild, false, 100.0 );
+ budget += BudgetEntryHelper( QDate(2006,1,1), acSecondChild, true, 100.0 );
+ MyMoneyReport report(MyMoneyReport::eBudgetActual,
+ MyMoneyReport::eMonths,
+ MyMoneyTransactionFilter::yearToDate,
+ MyMoneyReport::eDetailTop,
+ "Yearly Budgeted vs. Actual","Default Report");
+ PivotTable table(report);
+ }
+
+ // - Totals for D, D:1, D:2 show up. D:2:a and D:2:b do not
+ // - D actuals (only) compare against D budget
+ // - Ditto for D:1
+ // - D:2 acutals and children compare against D:2 budget
+
+ // 5. Budget on E, no transactions on E
+ {
+ BudgetHelper budget;
+ budget += BudgetEntryHelper( QDate(2006,1,1), acSolo, false, 100.0 );
+ MyMoneyReport report(MyMoneyReport::eBudgetActual,
+ MyMoneyReport::eMonths,
+ MyMoneyTransactionFilter::yearToDate,
+ MyMoneyReport::eDetailTop,
+ "Yearly Budgeted vs. Actual","Default Report");
+ PivotTable table(report);
+ }
+}
+
+// vim:cin:si:ai:et:ts=2:sw=2:
diff --git a/kmymoney2/reports/pivottabletest.h b/kmymoney2/reports/pivottabletest.h
new file mode 100644
index 0000000..299355c
--- /dev/null
+++ b/kmymoney2/reports/pivottabletest.h
@@ -0,0 +1,75 @@
+/***************************************************************************
+ pivottabletest.h
+ -------------------
+ copyright : (C) 2002 by Thomas Baumgart
+ email : ipwizard@users.sourceforge.net
+ Ace Jones <ace.jones@hotpop.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. *
+ * *
+ ***************************************************************************/
+
+#ifndef PIVOTTABLETEST_H
+#define PIVOTTABLETEST_H
+
+#include <cppunit/extensions/HelperMacros.h>
+#include "../mymoney/mymoneyfile.h"
+#include "../mymoney/storage/mymoneyseqaccessmgr.h"
+
+class PivotTableTest : public CppUnit::TestFixture {
+ CPPUNIT_TEST_SUITE(PivotTableTest);
+ CPPUNIT_TEST(testNetWorthSingle);
+ CPPUNIT_TEST(testNetWorthOfsetting);
+ CPPUNIT_TEST(testNetWorthOpeningPrior);
+ CPPUNIT_TEST(testNetWorthDateFilter);
+ CPPUNIT_TEST(testSpendingEmpty);
+ CPPUNIT_TEST(testSingleTransaction);
+ CPPUNIT_TEST(testSubAccount);
+ CPPUNIT_TEST(testFilterIEvsIE);
+ CPPUNIT_TEST(testFilterALvsAL);
+ CPPUNIT_TEST(testFilterALvsIE);
+ CPPUNIT_TEST(testFilterAllvsIE);
+ CPPUNIT_TEST(testFilterBasics);
+ CPPUNIT_TEST(testMultipleCurrencies);
+ CPPUNIT_TEST(testAdvancedFilter);
+ CPPUNIT_TEST(testColumnType);
+ CPPUNIT_TEST(testInvestment);
+ CPPUNIT_TEST(testBudget);
+ CPPUNIT_TEST_SUITE_END();
+
+private:
+ MyMoneyAccount *m;
+
+ MyMoneySeqAccessMgr* storage;
+ MyMoneyFile* file;
+
+public:
+ PivotTableTest();
+ void setUp ();
+ void tearDown ();
+ void testNetWorthSingle();
+ void testNetWorthOfsetting();
+ void testNetWorthOpeningPrior();
+ void testNetWorthDateFilter();
+ void testSpendingEmpty();
+ void testSingleTransaction();
+ void testSubAccount();
+ void testFilterIEvsIE();
+ void testFilterALvsAL();
+ void testFilterALvsIE();
+ void testFilterAllvsIE();
+ void testFilterBasics();
+ void testMultipleCurrencies();
+ void testAdvancedFilter();
+ void testColumnType();
+ void testInvestment();
+ void testBudget();
+};
+
+#endif // PIVOTTABLETEST_H
diff --git a/kmymoney2/reports/querytable.cpp b/kmymoney2/reports/querytable.cpp
new file mode 100644
index 0000000..29702c6
--- /dev/null
+++ b/kmymoney2/reports/querytable.cpp
@@ -0,0 +1,1522 @@
+/***************************************************************************
+ querytable.cpp
+ -------------------
+ begin : Fri Jul 23 2004
+ copyright : (C) 2004-2005 by Ace Jones
+ (C) 2007 Sascha Pfau
+ email : acejones@users.sourceforge.net
+ MrPeacock@gmail.com
+ ***************************************************************************/
+
+/****************************************************************************
+ Contains code from the func_xirr and related methods of financial.cpp
+ - KOffice 1.6 by Sascha Pfau. Sascha agreed to relicense those methods under
+ GPLv2 or later.
+*****************************************************************************/
+
+/***************************************************************************
+ * *
+ * 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 <qvaluelist.h>
+#include <qfile.h>
+#include <qtextstream.h>
+
+// ----------------------------------------------------------------------------
+// KDE Includes
+// This is just needed for i18n(). Once I figure out how to handle i18n
+// without using this macro directly, I'll be freed of KDE dependency.
+
+#include <klocale.h>
+#include <kdebug.h>
+
+// ----------------------------------------------------------------------------
+// Project Includes
+#include "../mymoney/mymoneyfile.h"
+#include "../mymoney/mymoneytransaction.h"
+#include "../mymoney/mymoneyreport.h"
+#include "../mymoney/mymoneyexception.h"
+#include "../kmymoneyutils.h"
+#include "../kmymoneyglobalsettings.h"
+#include "reportaccount.h"
+#include "reportdebug.h"
+#include "querytable.h"
+
+namespace reports {
+
+// ****************************************************************************
+//
+// CashFlowListItem implementation
+//
+// Cash flow analysis tools for investment reports
+//
+// ****************************************************************************
+
+QDate CashFlowListItem::m_sToday = QDate::currentDate();
+
+MyMoneyMoney CashFlowListItem::NPV( double _rate ) const
+{
+ double T = static_cast<double>(m_sToday.daysTo(m_date)) / 365.0;
+ MyMoneyMoney result = m_value.toDouble() / pow(1+_rate,T);
+
+ //kdDebug(2) << "CashFlowListItem::NPV( " << _rate << " ) == " << result << endl;
+
+ return result;
+}
+
+// ****************************************************************************
+//
+// CashFlowList implementation
+//
+// Cash flow analysis tools for investment reports
+//
+// ****************************************************************************
+
+CashFlowListItem CashFlowList::mostRecent(void) const
+{
+ CashFlowList dupe( *this );
+ qHeapSort( dupe );
+
+ //kdDebug(2) << " CashFlowList::mostRecent() == " << dupe.back().date().toString(Qt::ISODate) << endl;
+
+ return dupe.back();
+}
+
+MyMoneyMoney CashFlowList::NPV( double _rate ) const
+{
+ MyMoneyMoney result = 0.0;
+
+ const_iterator it_cash = begin();
+ while ( it_cash != end() )
+ {
+ result += (*it_cash).NPV( _rate );
+ ++it_cash;
+ }
+
+ //kdDebug(2) << "CashFlowList::NPV( " << _rate << " ) == " << result << endl << "------------------------" << endl;
+
+ return result;
+}
+
+double CashFlowList::calculateXIRR ( void ) const
+{
+ double resultRate = 0.00001;
+
+ double resultZero = 0.00000;
+ //if ( args.count() > 2 )
+ // resultRate = calc->conv()->asFloat ( args[2] ).asFloat();
+
+// check pairs and count >= 2 and guess > -1.0
+ //if ( args[0].count() != args[1].count() || args[1].count() < 2 || resultRate <= -1.0 )
+ // return Value::errorVALUE();
+
+// define max epsilon
+ static const double maxEpsilon = 1e-5;
+
+// max number of iterations
+ static const int maxIter = 50;
+
+// Newton's method - try to find a res, with a accuracy of maxEpsilon
+ double rateEpsilon, newRate, resultValue;
+ int i = 0;
+ bool contLoop;
+
+ do
+ {
+ resultValue = xirrResult ( resultRate );
+
+ double resultDerive = xirrResultDerive ( resultRate );
+
+ //check what happens if xirrResultDerive is zero
+ //Don't know if it is correct to dismiss the result
+ if( resultDerive != 0 ) {
+ newRate = resultRate - resultValue / resultDerive;
+ } else {
+
+ newRate = resultRate - resultValue;
+ }
+
+ rateEpsilon = fabs ( newRate - resultRate );
+
+ resultRate = newRate;
+ contLoop = ( rateEpsilon > maxEpsilon ) && ( fabs ( resultValue ) > maxEpsilon );
+ }
+ while ( contLoop && ( ++i < maxIter ) );
+
+ if ( contLoop )
+ return resultZero;
+
+ return resultRate;
+}
+
+double CashFlowList::xirrResult ( double& rate ) const
+{
+ QDate date;
+
+ double r = rate + 1.0;
+ double res = 0.00000;//back().value().toDouble();
+
+ QValueList<CashFlowListItem>::const_iterator list_it = begin();
+ while( list_it != end() ) {
+ double e_i = ( (* list_it).today().daysTo ( (* list_it).date() ) ) / 365.0;
+ MyMoneyMoney val = (* list_it).value();
+
+ res += val.toDouble() / pow ( r, e_i );
+ ++list_it;
+ }
+
+ return res;
+}
+
+
+double CashFlowList::xirrResultDerive ( double& rate ) const
+{
+ QDate date;
+
+ double r = rate + 1.0;
+ double res = 0.00000;
+
+ QValueList<CashFlowListItem>::const_iterator list_it = begin();
+ while( list_it != end() ) {
+ double e_i = ( (* list_it).today().daysTo ( (* list_it).date() ) ) / 365.0;
+ MyMoneyMoney val = (* list_it).value();
+
+ res -= e_i * val.toDouble() / pow ( r, e_i + 1.0 );
+ ++list_it;
+ }
+
+ return res;
+}
+
+double CashFlowList::IRR( void ) const
+{
+ double result = 0.0;
+
+ // set 'today', which is the most recent of all dates in the list
+ CashFlowListItem::setToday( mostRecent().date() );
+
+ result = calculateXIRR();
+ return result;
+}
+
+MyMoneyMoney CashFlowList::total(void) const
+{
+ MyMoneyMoney result;
+
+ const_iterator it_cash = begin();
+ while ( it_cash != end() )
+ {
+ result += (*it_cash).value();
+ ++it_cash;
+ }
+
+ return result;
+}
+
+void CashFlowList::dumpDebug(void) const
+{
+ const_iterator it_item = begin();
+ while ( it_item != end() )
+ {
+ kdDebug(2) << (*it_item).date().toString(Qt::ISODate) << " " << (*it_item).value().toString() << endl;
+ ++it_item;
+ }
+}
+
+// ****************************************************************************
+//
+// QueryTable implementation
+//
+// ****************************************************************************
+
+/**
+ * TODO
+ *
+ * - Collapse 2- & 3- groups when they are identical
+ * - Way more test cases (especially splits & transfers)
+ * - Option to collapse splits
+ * - Option to exclude transfers
+ *
+ */
+
+QueryTable::QueryTable(const MyMoneyReport& _report): ListTable(_report)
+{
+ // seperated into its own method to allow debugging (setting breakpoints
+ // directly in ctors somehow does not work for me (ipwizard))
+ // TODO: remove the init() method and move the code back to the ctor
+ init();
+}
+
+void QueryTable::init(void)
+{
+ switch ( m_config.rowType() )
+ {
+ case MyMoneyReport::eAccountByTopAccount:
+ case MyMoneyReport::eEquityType:
+ case MyMoneyReport::eAccountType:
+ case MyMoneyReport::eInstitution:
+ constructAccountTable();
+ m_columns="account";
+ break;
+
+ case MyMoneyReport::eAccount:
+ constructTransactionTable();
+ m_columns="accountid,postdate";
+ break;
+
+ case MyMoneyReport::ePayee:
+ case MyMoneyReport::eMonth:
+ case MyMoneyReport::eWeek:
+ constructTransactionTable();
+ m_columns="postdate,account";
+ break;
+ case MyMoneyReport::eCashFlow:
+ constructSplitsTable();
+ m_columns="postdate";
+ break;
+ default:
+ constructTransactionTable();
+ m_columns="postdate";
+ }
+
+ // Sort the data to match the report definition
+ m_subtotal="value";
+
+ switch ( m_config.rowType() )
+ {
+ case MyMoneyReport::eCashFlow:
+ m_group = "categorytype,topcategory,category";
+ break;
+ case MyMoneyReport::eCategory:
+ m_group = "categorytype,topcategory,category";
+ break;
+ case MyMoneyReport::eTopCategory:
+ m_group = "categorytype,topcategory";
+ break;
+ case MyMoneyReport::eTopAccount:
+ m_group = "topaccount,account";
+ break;
+ case MyMoneyReport::eAccount:
+ m_group = "account";
+ break;
+ case MyMoneyReport::eAccountReconcile:
+ m_group = "account,reconcileflag";
+ break;
+ case MyMoneyReport::ePayee:
+ m_group = "payee";
+ break;
+ case MyMoneyReport::eMonth:
+ m_group = "month";
+ break;
+ case MyMoneyReport::eWeek:
+ m_group = "week";
+ break;
+ case MyMoneyReport::eAccountByTopAccount:
+ m_group = "topaccount";
+ break;
+ case MyMoneyReport::eEquityType:
+ m_group = "equitytype";
+ break;
+ case MyMoneyReport::eAccountType:
+ m_group = "type";
+ break;
+ case MyMoneyReport::eInstitution:
+ m_group = "institution,topaccount";
+ break;
+ default:
+ throw new MYMONEYEXCEPTION("QueryTable::QueryTable(): unhandled row type");
+ }
+
+ QString sort = m_group + "," + m_columns + ",id,rank";
+
+ switch (m_config.rowType()) {
+ case MyMoneyReport::eAccountByTopAccount:
+ case MyMoneyReport::eEquityType:
+ case MyMoneyReport::eAccountType:
+ case MyMoneyReport::eInstitution:
+ m_columns="account";
+ break;
+
+ default:
+ m_columns="postdate";
+ }
+
+ unsigned qc = m_config.queryColumns();
+
+ if ( qc & MyMoneyReport::eQCnumber )
+ m_columns += ",number";
+ if ( qc & MyMoneyReport::eQCpayee )
+ m_columns += ",payee";
+ if ( qc & MyMoneyReport::eQCcategory )
+ m_columns += ",category";
+ if ( qc & MyMoneyReport::eQCaccount )
+ m_columns += ",account";
+ if ( qc & MyMoneyReport::eQCreconciled )
+ m_columns += ",reconcileflag";
+ if ( qc & MyMoneyReport::eQCmemo )
+ m_columns += ",memo";
+ if ( qc & MyMoneyReport::eQCaction )
+ m_columns += ",action";
+ if ( qc & MyMoneyReport::eQCshares )
+ m_columns += ",shares";
+ if ( qc & MyMoneyReport::eQCprice )
+ m_columns += ",price";
+ if ( qc & MyMoneyReport::eQCperformance )
+ m_columns += ",startingbal,buys,sells,reinvestincome,cashincome,return,returninvestment";
+ if ( qc & MyMoneyReport::eQCloan )
+ {
+ m_columns += ",payment,interest,fees";
+ m_postcolumns = "balance";
+ }
+ if ( qc & MyMoneyReport::eQCbalance)
+ m_postcolumns = "balance";
+
+ TableRow::setSortCriteria(sort);
+ qHeapSort(m_rows);
+}
+
+void QueryTable::constructTransactionTable(void)
+{
+ MyMoneyFile* file = MyMoneyFile::instance();
+
+ //make sure we have all subaccounts of investment accounts
+ includeInvestmentSubAccounts();
+
+ MyMoneyReport report(m_config);
+ report.setReportAllSplits(false);
+ report.setConsiderCategory(true);
+
+ bool use_transfers;
+ bool use_summary;
+ bool hide_details;
+
+ switch (m_config.rowType()) {
+ case MyMoneyReport::eCategory:
+ case MyMoneyReport::eTopCategory:
+ use_summary = false;
+ use_transfers = false;
+ hide_details = false;
+ break;
+ case MyMoneyReport::ePayee:
+ use_summary = false;
+ use_transfers = false;
+ hide_details = (m_config.detailLevel() == MyMoneyReport::eDetailNone);
+ break;
+ default:
+ use_summary = true;
+ use_transfers = true;
+ hide_details = (m_config.detailLevel() == MyMoneyReport::eDetailNone);
+ break;
+ }
+
+ // support for opening and closing balances
+ QMap<QString, MyMoneyAccount> accts;
+
+ //get all transactions for this report
+ QValueList<MyMoneyTransaction> transactions = file->transactionList(report);
+ for (QValueList<MyMoneyTransaction>::const_iterator it_transaction = transactions.begin(); it_transaction != transactions.end(); ++it_transaction) {
+
+ TableRow qA, qS;
+ QDate pd;
+
+ qA["id"] = qS["id"] = (* it_transaction).id();
+ qA["entrydate"] = qS["entrydate"] = (* it_transaction).entryDate().toString(Qt::ISODate);
+ qA["postdate"] = qS["postdate"] = (* it_transaction).postDate().toString(Qt::ISODate);
+ qA["commodity"] = qS["commodity"] = (* it_transaction).commodity();
+
+ pd = (* it_transaction).postDate();
+ qA["month"] = qS["month"] = i18n("Month of %1").arg(QDate(pd.year(),pd.month(),1).toString(Qt::ISODate));
+ qA["week"] = qS["week"] = i18n("Week of %1").arg(pd.addDays(1-pd.dayOfWeek()).toString(Qt::ISODate));
+
+ qA["currency"] = qS["currency"] = "";
+
+ if((* it_transaction).commodity() != file->baseCurrency().id()) {
+ if (!report.isConvertCurrency()) {
+ qA["currency"] = qS["currency"] = (*it_transaction).commodity();
+ }
+ }
+
+ // to handle splits, we decide on which account to base the split
+ // (a reference point or point of view so to speak). here we take the
+ // first account that is a stock account or loan account (or the first account
+ // that is not an income or expense account if there is no stock or loan account)
+ // to be the account (qA) that will have the sub-item "split" entries. we add
+ // one transaction entry (qS) for each subsequent entry in the split.
+
+ const QValueList<MyMoneySplit>& splits = (*it_transaction).splits();
+ QValueList<MyMoneySplit>::const_iterator myBegin, it_split;
+ //S_end = splits.end();
+
+ for (it_split = splits.begin(), myBegin = splits.end(); it_split != splits.end(); ++it_split) {
+ ReportAccount splitAcc = (* it_split).accountId();
+ // always put split with a "stock" account if it exists
+ if (splitAcc.isInvest())
+ break;
+
+ // prefer to put splits with a "loan" account if it exists
+ if(splitAcc.isLoan())
+ myBegin = it_split;
+
+ if((myBegin == splits.end()) && ! splitAcc.isIncomeExpense()) {
+ myBegin = it_split;
+ }
+ }
+
+ // select our "reference" split
+ if (it_split == splits.end()) {
+ it_split = myBegin;
+ } else {
+ myBegin = it_split;
+ }
+
+ // if the split is still unknown, use the first one. I have seen this
+ // happen with a transaction that has only a single split referencing an income or expense
+ // account and has an amount and value of 0. Such a transaction will fall through
+ // the above logic and leave 'it_split' pointing to splits.end() which causes the remainder
+ // of this to end in an infinite loop.
+ if(it_split == splits.end()) {
+ it_split = splits.begin();
+ }
+
+ // for "loan" reports, the loan transaction gets special treatment.
+ // the splits of a loan transaction are placed on one line in the
+ // reference (loan) account (qA). however, we process the matching
+ // split entries (qS) normally.
+
+ bool loan_special_case = false;
+ if(m_config.queryColumns() & MyMoneyReport::eQCloan) {
+ ReportAccount splitAcc = (*it_split).accountId();
+ loan_special_case = splitAcc.isLoan();
+ }
+
+#if 0
+ // a stock dividend or yield transaction is also a special case.
+ // [dv: the original comment follows]
+ // handle cash dividends. these little fellas require very special handling.
+ // the stock account will produce a row with zero value & zero shares. Then
+ // there will be 2 split rows, a category and a transfer account. We are
+ // only concerned with the transfer account, and we will NOT show the income
+ // account. (This may have to be changed later if we feel we need it.)
+
+ // [dv: this special case just doesn't make sense to me -- it seems to
+ // violate the "zero sum" transaction concept. for now, then, the stock
+ // dividend / yield special case goes unimplemented.]
+
+ bool stock_special_case =
+ (a.isInvest() &&
+ ((* is).action() == MyMoneySplit::ActionDividend ||
+ (* is).action() == MyMoneySplit::ActionYield));
+#endif
+
+ bool include_me = true;
+ bool transaction_text = false; //indicates whether a text should be considered as a match for the transaction or for a split only
+ QString a_fullname = "";
+ QString a_memo = "";
+ unsigned int pass = 1;
+ QString myBeginCurrency = (file->account((*myBegin).accountId())).currencyId(); //currency of the main split
+ do {
+ MyMoneyMoney xr;
+ ReportAccount splitAcc = (* it_split).accountId();
+
+ //use the fraction relevant to the account at hand
+ int fraction = splitAcc.currency().smallestAccountFraction();
+
+ //use base currency fraction if not initialized
+ if(fraction == -1)
+ fraction = file->baseCurrency().smallestAccountFraction();
+
+ QString institution = splitAcc.institutionId();
+ QString payee = (*it_split).payeeId();
+
+ //convert to base currency
+ if ( m_config.isConvertCurrency() ) {
+ xr = (splitAcc.deepCurrencyPrice((*it_transaction).postDate()) * splitAcc.baseCurrencyPrice((*it_transaction).postDate())).reduce();
+ } else {
+ xr = (splitAcc.deepCurrencyPrice((*it_transaction).postDate())).reduce();
+ //if the currency of the split is different from the currency of the main split, then convert to the currency of the main split
+ if(splitAcc.currency().id() != myBeginCurrency) {
+ xr = (xr * splitAcc.foreignCurrencyPrice(myBeginCurrency, (*it_transaction).postDate())).reduce();
+ }
+ }
+
+ if (splitAcc.isInvest()) {
+
+ // use the institution of the parent for stock accounts
+ institution = splitAcc.parent().institutionId();
+ MyMoneyMoney shares = (*it_split).shares();
+
+ qA["action"] = (*it_split).action();
+ qA["shares"] = shares.isZero() ? "" : (*it_split).shares().toString();
+ qA["price"] = shares.isZero() ? "" : xr.convert(MyMoneyMoney::precToDenom(KMyMoneyGlobalSettings::pricePrecision())).toString();
+
+ if (((*it_split).action() == MyMoneySplit::ActionBuyShares) && (*it_split).shares().isNegative())
+ qA["action"] = "Sell";
+
+ qA["investaccount"] = splitAcc.parent().name();
+ }
+
+ if (it_split == myBegin) {
+
+ include_me = m_config.includes(splitAcc);
+ a_fullname = splitAcc.fullName();
+ a_memo = (*it_split).memo();
+
+ transaction_text = m_config.match(&(*it_split));
+
+ qA["price"] = xr.convert(MyMoneyMoney::precToDenom(KMyMoneyGlobalSettings::pricePrecision())).toString();
+ qA["account"] = splitAcc.name();
+ qA["accountid"] = splitAcc.id();
+ qA["topaccount"] = splitAcc.topParentName();
+
+ qA["institution"] = institution.isEmpty()
+ ? i18n("No Institution")
+ : file->institution(institution).name();
+
+ qA["payee"] = payee.isEmpty()
+ ? i18n("[Empty Payee]")
+ : file->payee(payee).name().simplifyWhiteSpace();
+
+ qA["reconciledate"] = (*it_split).reconcileDate().toString(Qt::ISODate);
+ qA["reconcileflag"] = KMyMoneyUtils::reconcileStateToString((*it_split).reconcileFlag(), true );
+ qA["number"] = (*it_split).number();
+
+ qA["memo"] = a_memo;
+
+ qS["reconciledate"] = qA["reconciledate"];
+ qS["reconcileflag"] = qA["reconcileflag"];
+ qS["number"] = qA["number"];
+
+ qS["topcategory"] = splitAcc.topParentName();
+ qS["categorytype"] = i18n("Transfer");
+
+ // only include the configured accounts
+ if (include_me) {
+
+ if (loan_special_case) {
+
+ // put the principal amount in the "value" column and convert to lowest fraction
+ qA["value"] = ((-(*it_split).shares()) * xr).convert(fraction).toString();
+
+ qA["rank"] = "0";
+ qA["split"] = "";
+
+ } else {
+ if ((splits.count() > 2) && use_summary) {
+
+ // add the "summarized" split transaction
+ // this is the sub-total of the split detail
+ // convert to lowest fraction
+ qA["value"] = ((*it_split).shares() * xr).convert(fraction).toString();
+ qA["rank"] = "0";
+ qA["category"] = i18n("[Split Transaction]");
+ qA["topcategory"] = i18n("Split");
+ qA["categorytype"] = i18n("Split");
+
+ m_rows += qA;
+ }
+ }
+
+ // track accts that will need opening and closing balances
+ //FIXME in some cases it will show the opening and closing
+ //balances but no transactions if the splits are all filtered out -- asoliverez
+ accts.insert (splitAcc.id(), splitAcc);
+ }
+
+ } else {
+
+ if (include_me) {
+
+ if (loan_special_case) {
+ MyMoneyMoney value = ((-(* it_split).shares()) * xr).convert(fraction);
+
+ if ((*it_split).action() == MyMoneySplit::ActionAmortization) {
+ // put the payment in the "payment" column and convert to lowest fraction
+ qA["payment"] = value.toString();
+ }
+ else if ((*it_split).action() == MyMoneySplit::ActionInterest) {
+ // put the interest in the "interest" column and convert to lowest fraction
+ qA["interest"] = value.toString();
+ }
+ else if (splits.count() > 2) {
+ // [dv: This comment carried from the original code. I am
+ // not exactly clear on what it means or why we do this.]
+ // Put the initial pay-in nowhere (that is, ignore it). This
+ // is dangerous, though. The only way I can tell the initial
+ // pay-in apart from fees is if there are only 2 splits in
+ // the transaction. I wish there was a better way.
+ }
+ else {
+ // accumulate everything else in the "fees" column
+ MyMoneyMoney n0 = MyMoneyMoney(qA["fees"]);
+ qA["fees"] = (n0 + value).toString();
+ }
+ // we don't add qA here for a loan transaction. we'll add one
+ // qA afer all of the split components have been processed.
+ // (see below)
+
+ }
+
+ //--- special case to hide split transaction details
+ else if (hide_details && (splits.count() > 2)) {
+ // essentially, don't add any qA entries
+ }
+
+ //--- default case includes all transaction details
+ else {
+
+ //this is when the splits are going to be shown as children of the main split
+ if ((splits.count() > 2) && use_summary) {
+ qA["value"] = "";
+
+ //convert to lowest fraction
+ qA["split"] = ((-(*it_split).shares()) * xr).convert(fraction).toString();
+ qA["rank"] = "1";
+ } else {
+ //this applies when the transaction has only 2 splits, or each split is going to be
+ //shown separately, eg. transactions by category
+
+ qA["split"] = "";
+
+ //multiply by currency and convert to lowest fraction
+ qA["value"] = ((-(*it_split).shares()) * xr).convert(fraction).toString();
+ qA["rank"] = "0";
+ }
+
+ qA ["memo"] = (*it_split).memo();
+
+ if (! splitAcc.isIncomeExpense()) {
+ qA["category"] = ((*it_split).shares().isNegative()) ?
+ i18n("Transfer from %1").arg(splitAcc.fullName())
+ : i18n("Transfer to %1").arg(splitAcc.fullName());
+ qA["topcategory"] = splitAcc.topParentName();
+ qA["categorytype"] = i18n("Transfer");
+ }
+ else {
+ qA ["category"] = splitAcc.fullName();
+ qA ["topcategory"] = splitAcc.topParentName();
+ qA ["categorytype"] = KMyMoneyUtils::accountTypeToString(splitAcc.accountGroup());
+ }
+
+ if (use_transfers || (splitAcc.isIncomeExpense() && m_config.includes(splitAcc)))
+ {
+ //if it matches the text of the main split of the transaction or
+ //it matches this particular split, include it
+ //otherwise, skip it
+ //if the filter is "does not contain" exclude the split if it does not match
+ //even it matches the whole split
+ if((m_config.isInvertingText() &&
+ m_config.match( &(*it_split) ))
+ || ( !m_config.isInvertingText()
+ && (transaction_text
+ || m_config.match( &(*it_split) )))) {
+ m_rows += qA;
+ }
+ }
+ }
+ }
+
+ if (m_config.includes(splitAcc) && use_transfers) {
+ if (! splitAcc.isIncomeExpense()) {
+
+ //multiply by currency and convert to lowest fraction
+ qS["value"] = ((*it_split).shares() * xr).convert(fraction).toString();
+
+ qS["rank"] = "0";
+
+ qS["account"] = splitAcc.name();
+ qS["accountid"] = splitAcc.id();
+ qS["topaccount"] = splitAcc.topParentName();
+
+ qS["category"] = ((*it_split).shares().isNegative())
+ ? i18n("Transfer to %1").arg(a_fullname)
+ : i18n("Transfer from %1").arg(a_fullname);
+
+ qS["institution"] = institution.isEmpty()
+ ? i18n("No Institution")
+ : file->institution(institution).name();
+
+ qS["memo"] = (*it_split).memo().isEmpty()
+ ? a_memo
+ : (*it_split).memo();
+
+ qS["payee"] = payee.isEmpty()
+ ? qA["payee"]
+ : file->payee(payee).name().simplifyWhiteSpace();
+
+ //check the specific split against the filter for text and amount
+ //TODO this should be done at the engine, but I have no clear idea how -- asoliverez
+ //if the filter is "does not contain" exclude the split if it does not match
+ //even it matches the whole split
+ if((m_config.isInvertingText() &&
+ m_config.match( &(*it_split) ))
+ || ( !m_config.isInvertingText()
+ && (transaction_text
+ || m_config.match( &(*it_split) )))) {
+ m_rows += qS;
+
+ // track accts that will need opening and closing balances
+ accts.insert (splitAcc.id(), splitAcc);
+ }
+ }
+ }
+ }
+
+ ++it_split;
+
+ // look for wrap-around
+ if (it_split == splits.end())
+ it_split = splits.begin();
+
+ // but terminate if this transaction has only a single split
+ if(splits.count() < 2)
+ break;
+
+ //check if there have been more passes than there are splits
+ //this is to prevent infinite loops in cases of data inconsistency -- asoliverez
+ ++pass;
+ if( pass > splits.count() )
+ break;
+
+ } while (it_split != myBegin);
+
+ if (loan_special_case) {
+ m_rows += qA;
+ }
+ }
+
+ // now run through our accts list and add opening and closing balances
+
+ switch (m_config.rowType()) {
+ case MyMoneyReport::eAccount:
+ case MyMoneyReport::eTopAccount:
+ break;
+
+ // case MyMoneyReport::eCategory:
+ // case MyMoneyReport::eTopCategory:
+ // case MyMoneyReport::ePayee:
+ // case MyMoneyReport::eMonth:
+ // case MyMoneyReport::eWeek:
+ default:
+ return;
+ }
+
+ QDate startDate, endDate;
+
+ report.validDateRange(startDate, endDate);
+ QString strStartDate = startDate.toString(Qt::ISODate);
+ QString strEndDate = endDate.toString(Qt::ISODate);
+ startDate = startDate.addDays(-1);
+
+ QMap<QString, MyMoneyAccount>::const_iterator it_account, accts_end;
+ for (it_account = accts.begin(); it_account != accts.end(); ++it_account) {
+ TableRow qA;
+
+ ReportAccount account = (* it_account);
+
+ //get fraction for account
+ int fraction = account.currency().smallestAccountFraction();
+
+ //use base currency fraction if not initialized
+ if(fraction == -1)
+ fraction = file->baseCurrency().smallestAccountFraction();
+
+ QString institution = account.institutionId();
+
+ // use the institution of the parent for stock accounts
+ if (account.isInvest())
+ institution = account.parent().institutionId();
+
+ MyMoneyMoney startBalance, endBalance, startPrice, endPrice;
+ MyMoneyMoney startShares, endShares;
+
+ //get price and convert currency if necessary
+ if ( m_config.isConvertCurrency() ) {
+ startPrice = (account.deepCurrencyPrice(startDate) * account.baseCurrencyPrice(startDate)).reduce();
+ endPrice = (account.deepCurrencyPrice(endDate) * account.baseCurrencyPrice(endDate)).reduce();
+ } else {
+ startPrice = account.deepCurrencyPrice(startDate).reduce();
+ endPrice = account.deepCurrencyPrice(endDate).reduce();
+ }
+ startShares = file->balance(account.id(),startDate);
+ endShares = file->balance(account.id(),endDate);
+
+ //get starting and ending balances
+ startBalance = startShares * startPrice;
+ endBalance = endShares * endPrice;
+
+ //starting balance
+ // don't show currency if we're converting or if it's not foreign
+ qA["currency"] = (m_config.isConvertCurrency() || ! account.isForeignCurrency()) ? "" : account.currency().id();
+
+ qA["accountid"] = account.id();
+ qA["account"] = account.name();
+ qA["topaccount"] = account.topParentName();
+ qA["institution"] = institution.isEmpty() ? i18n("No Institution") : file->institution(institution).name();
+ qA["rank"] = "-2";
+
+ qA["price"] = startPrice.convert(MyMoneyMoney::precToDenom(KMyMoneyGlobalSettings::pricePrecision())).toString();
+ if (account.isInvest()) {
+ qA["shares"] = startShares.toString();
+ }
+
+ qA["postdate"] = strStartDate;
+ qA["balance"] = startBalance.convert(fraction).toString();
+ qA["value"] = QString();
+ qA["id"] = "A";
+ m_rows += qA;
+
+ //ending balance
+ qA["price"] = endPrice.convert(MyMoneyMoney::precToDenom(KMyMoneyGlobalSettings::pricePrecision())).toString();
+
+ if (account.isInvest()) {
+ qA["shares"] = endShares.toString();
+ }
+
+ qA["postdate"] = strEndDate;
+ qA["balance"] = endBalance.toString();
+ qA["id"] = "Z";
+ m_rows += qA;
+ }
+}
+
+void QueryTable::constructPerformanceRow( const ReportAccount& account, TableRow& result ) const
+{
+ MyMoneyFile* file = MyMoneyFile::instance();
+ MyMoneySecurity security = file->security(account.currencyId());
+
+ result["equitytype"] = KMyMoneyUtils::securityTypeToString(security.securityType());
+
+ //set fraction
+ int fraction = account.currency().smallestAccountFraction();
+
+ //
+ // Calculate performance
+ //
+
+ // The following columns are created:
+ // Account, Value on <Opening>, Buys, Sells, Income, Value on <Closing>, Return%
+
+ MyMoneyReport report = m_config;
+ QDate startingDate;
+ QDate endingDate;
+ MyMoneyMoney price;
+ report.validDateRange( startingDate, endingDate );
+ startingDate = startingDate.addDays(-1);
+
+ //calculate starting balance
+ if ( m_config.isConvertCurrency() ) {
+ price = account.deepCurrencyPrice(startingDate) * account.baseCurrencyPrice(startingDate);
+ } else {
+ price = account.deepCurrencyPrice(startingDate);
+ }
+
+ //work around if there is no price for the starting balance
+ if(!(file->balance(account.id(),startingDate)).isZero()
+ && account.deepCurrencyPrice(startingDate) == MyMoneyMoney(1, 1))
+ {
+ MyMoneyTransactionFilter filter;
+ //get the transactions for the time before the report
+ filter.setDateFilter(QDate(), startingDate);
+ filter.addAccount(account.id());
+ filter.setReportAllSplits(true);
+
+ QValueList<MyMoneyTransaction> startTransactions = file->transactionList(filter);
+ if(startTransactions.size() > 0)
+ {
+ //get the last transaction
+ MyMoneyTransaction startTrans = startTransactions.back();
+ MyMoneySplit s = startTrans.splitByAccount(account.id());
+ //get the price from the split of that account
+ price = s.price();
+ if ( m_config.isConvertCurrency() )
+ price = price * account.baseCurrencyPrice(startingDate);
+ }
+ }if ( m_config.isConvertCurrency() ) {
+ price = account.deepCurrencyPrice(startingDate) * account.baseCurrencyPrice(startingDate);
+ } else {
+ price = account.deepCurrencyPrice(startingDate);
+ }
+
+
+ MyMoneyMoney startingBal = file->balance(account.id(),startingDate) * price;
+
+ //convert to lowest fraction
+ startingBal = startingBal.convert(fraction);
+
+ //calculate ending balance
+ if ( m_config.isConvertCurrency() ) {
+ price = account.deepCurrencyPrice(endingDate) * account.baseCurrencyPrice(endingDate);
+ } else {
+ price = account.deepCurrencyPrice(endingDate);
+ }
+ MyMoneyMoney endingBal = file->balance((account).id(),endingDate) * price;
+
+ //convert to lowest fraction
+ endingBal = endingBal.convert(fraction);
+
+ //add start balance to calculate return on investment
+ MyMoneyMoney returnInvestment = startingBal;
+ MyMoneyMoney paidDividend;
+ CashFlowList buys;
+ CashFlowList sells;
+ CashFlowList reinvestincome;
+ CashFlowList cashincome;
+
+ report.setReportAllSplits(false);
+ report.setConsiderCategory(true);
+ report.clearAccountFilter();
+ report.addAccount(account.id());
+ QValueList<MyMoneyTransaction> transactions = file->transactionList( report );
+ QValueList<MyMoneyTransaction>::const_iterator it_transaction = transactions.begin();
+ while ( it_transaction != transactions.end() )
+ {
+ // s is the split for the stock account
+ MyMoneySplit s = (*it_transaction).splitByAccount(account.id());
+
+ //get price for the day of the transaction if we have to calculate base currency
+ //we are using the value of the split which is in deep currency
+ if ( m_config.isConvertCurrency() ) {
+ price = account.baseCurrencyPrice((*it_transaction).postDate()); //we only need base currency because the value is in deep currency
+ } else {
+ price = MyMoneyMoney(1,1);
+ }
+
+ MyMoneyMoney value = s.value() * price;
+
+ const QString& action = s.action();
+ if ( action == MyMoneySplit::ActionBuyShares )
+ {
+ if ( s.value().isPositive() ) {
+ buys += CashFlowListItem( (*it_transaction).postDate(), -value );
+ } else {
+ sells += CashFlowListItem( (*it_transaction).postDate(), -value );
+ }
+ returnInvestment += value;
+ //convert to lowest fraction
+ returnInvestment = returnInvestment.convert(fraction);
+ } else if ( action == MyMoneySplit::ActionReinvestDividend ) {
+ reinvestincome += CashFlowListItem( (*it_transaction).postDate(), value );
+ } else if ( action == MyMoneySplit::ActionDividend || action == MyMoneySplit::ActionYield ) {
+ // find the split with the category, which has the actual amount of the dividend
+ QValueList<MyMoneySplit> splits = (*it_transaction).splits();
+ QValueList<MyMoneySplit>::const_iterator it_split = splits.begin();
+ bool found = false;
+ while( it_split != splits.end() ) {
+ ReportAccount acc = (*it_split).accountId();
+ if ( acc.isIncomeExpense() ) {
+ found = true;
+ break;
+ }
+ ++it_split;
+ }
+
+ if ( found ) {
+ cashincome += CashFlowListItem( (*it_transaction).postDate(), -(*it_split).value() * price);
+ paidDividend += ((-(*it_split).value()) * price).convert(fraction);
+ }
+ } else {
+ //if the split does not match any action above, add it as buy or sell depending on sign
+
+ //if value is zero, get the price for that date
+ if( s.value().isZero() ) {
+ if ( m_config.isConvertCurrency() ) {
+ price = account.deepCurrencyPrice((*it_transaction).postDate()) * account.baseCurrencyPrice((*it_transaction).postDate());
+ } else {
+ price = account.deepCurrencyPrice((*it_transaction).postDate());
+ }
+ value = s.shares() * price;
+ if ( s.shares().isPositive() ) {
+ buys += CashFlowListItem( (*it_transaction).postDate(), -value );
+ } else {
+ sells += CashFlowListItem( (*it_transaction).postDate(), -value );
+ }
+ returnInvestment += value;
+ } else {
+ value = s.value() * price;
+ if ( s.value().isPositive() ) {
+ buys += CashFlowListItem( (*it_transaction).postDate(), -value );
+ } else {
+ sells += CashFlowListItem( (*it_transaction).postDate(), -value );
+ }
+ returnInvestment += value;
+ }
+ }
+ ++it_transaction;
+ }
+
+ // Note that reinvested dividends are not included , because these do not
+ // represent a cash flow event.
+ CashFlowList all;
+ all += buys;
+ all += sells;
+ all += cashincome;
+ all += CashFlowListItem(startingDate, -startingBal);
+ all += CashFlowListItem(endingDate, endingBal);
+
+ //check if no activity on that term
+ if(!returnInvestment.isZero() && !endingBal.isZero()) {
+ returnInvestment = ((endingBal + paidDividend) - returnInvestment)/returnInvestment;
+ returnInvestment = returnInvestment.convert(10000);
+ } else {
+ returnInvestment = MyMoneyMoney(0,1);
+ }
+
+ try
+ {
+ MyMoneyMoney annualReturn = MyMoneyMoney(all.IRR(),10000);
+ result["return"] = annualReturn.toString();
+ result["returninvestment"] = returnInvestment.toString();
+ }
+ catch (QString e)
+ {
+ kdDebug(2) << e << endl;
+ }
+
+ result["buys"] = (-(buys.total())).toString();
+ result["sells"] = (-(sells.total())).toString();
+ result["cashincome"] = (cashincome.total()).toString();
+ result["reinvestincome"] = (reinvestincome.total()).toString();
+ result["startingbal"] = (startingBal).toString();
+ result["endingbal"] = (endingBal).toString();
+}
+
+void QueryTable::constructAccountTable(void)
+{
+ MyMoneyFile* file = MyMoneyFile::instance();
+
+ //make sure we have all subaccounts of investment accounts
+ includeInvestmentSubAccounts();
+
+ QValueList<MyMoneyAccount> accounts;
+ file->accountList(accounts);
+ QValueList<MyMoneyAccount>::const_iterator it_account = accounts.begin();
+ while ( it_account != accounts.end() )
+ {
+ ReportAccount account = *it_account;
+
+ //get fraction for account
+ int fraction = account.currency().smallestAccountFraction();
+
+ //use base currency fraction if not initialized
+ if(fraction == -1)
+ fraction = MyMoneyFile::instance()->baseCurrency().smallestAccountFraction();
+
+ // Note, "Investment" accounts are never included in account rows because
+ // they don't contain anything by themselves. In reports, they are only
+ // useful as a "topaccount" aggregator of stock accounts
+ if ( account.isAssetLiability() && m_config.includes(account) && account.accountType() != MyMoneyAccount::Investment )
+ {
+ TableRow qaccountrow;
+
+ // help for sort and render functions
+ qaccountrow["rank"] = "0";
+
+ //
+ // Handle currency conversion
+ //
+
+ MyMoneyMoney displayprice(1.0);
+ if ( m_config.isConvertCurrency() )
+ {
+ // display currency is base currency, so set the price
+ if ( account.isForeignCurrency() )
+ displayprice = account.baseCurrencyPrice(m_config.toDate()).reduce();
+ }
+ else
+ {
+ // display currency is the account's deep currency. display this fact in the report
+ qaccountrow["currency"] = account.currency().id();
+ }
+
+ qaccountrow["account"] = account.name();
+ qaccountrow["accountid"] = account.id();
+ qaccountrow["topaccount"] = account.topParentName();
+
+ MyMoneyMoney shares = file->balance(account.id(),m_config.toDate());
+ qaccountrow["shares"] = shares.toString();
+
+ MyMoneyMoney netprice = account.deepCurrencyPrice(m_config.toDate()).reduce() * displayprice;
+ qaccountrow["price"] = ( netprice.reduce() ).convert(MyMoneyMoney::precToDenom(KMyMoneyGlobalSettings::pricePrecision())).toString();
+ qaccountrow["value"] = ( netprice.reduce() * shares.reduce() ).convert(fraction).toString();
+
+ QString iid = (*it_account).institutionId();
+
+ // If an account does not have an institution, get it from the top-parent.
+ if ( iid.isEmpty() && ! account.isTopLevel() )
+ {
+ ReportAccount topaccount = account.topParent();
+ iid = topaccount.institutionId();
+ }
+
+ if ( iid.isEmpty() )
+ qaccountrow["institution"] = i18n("None");
+ else
+ qaccountrow["institution"] = file->institution(iid).name();
+
+ qaccountrow["type"] = KMyMoneyUtils::accountTypeToString((*it_account).accountType());
+
+ // TODO: Only do this if the report we're making really needs performance. Otherwise
+ // it's an expensive calculation done for no reason
+ if ( account.isInvest() )
+ {
+ constructPerformanceRow( account, qaccountrow );
+ }
+ else
+ qaccountrow["equitytype"] = QString();
+
+ // don't add the account if it is closed. In fact, the business logic
+ // should prevent that an account can be closed with a balance not equal
+ // to zero, but we never know.
+ if(!(shares.isZero() && account.isClosed()))
+ m_rows += qaccountrow;
+ }
+
+ ++it_account;
+ }
+}
+
+void QueryTable::constructSplitsTable(void)
+{
+ MyMoneyFile* file = MyMoneyFile::instance();
+
+ //make sure we have all subaccounts of investment accounts
+ includeInvestmentSubAccounts();
+
+ MyMoneyReport report(m_config);
+ report.setReportAllSplits(false);
+ report.setConsiderCategory(true);
+
+ // support for opening and closing balances
+ QMap<QString, MyMoneyAccount> accts;
+
+ //get all transactions for this report
+ QValueList<MyMoneyTransaction> transactions = file->transactionList(report);
+ for (QValueList<MyMoneyTransaction>::const_iterator it_transaction = transactions.begin(); it_transaction != transactions.end(); ++it_transaction) {
+
+ TableRow qA, qS;
+ QDate pd;
+
+ qA["id"] = qS["id"] = (* it_transaction).id();
+ qA["entrydate"] = qS["entrydate"] = (* it_transaction).entryDate().toString(Qt::ISODate);
+ qA["postdate"] = qS["postdate"] = (* it_transaction).postDate().toString(Qt::ISODate);
+ qA["commodity"] = qS["commodity"] = (* it_transaction).commodity();
+
+ pd = (* it_transaction).postDate();
+ qA["month"] = qS["month"] = i18n("Month of %1").arg(QDate(pd.year(),pd.month(),1).toString(Qt::ISODate));
+ qA["week"] = qS["week"] = i18n("Week of %1").arg(pd.addDays(1-pd.dayOfWeek()).toString(Qt::ISODate));
+
+ qA["currency"] = qS["currency"] = "";
+
+ if((* it_transaction).commodity() != file->baseCurrency().id()) {
+ if (!report.isConvertCurrency()) {
+ qA["currency"] = qS["currency"] = (*it_transaction).commodity();
+ }
+ }
+
+ // to handle splits, we decide on which account to base the split
+ // (a reference point or point of view so to speak). here we take the
+ // first account that is a stock account or loan account (or the first account
+ // that is not an income or expense account if there is no stock or loan account)
+ // to be the account (qA) that will have the sub-item "split" entries. we add
+ // one transaction entry (qS) for each subsequent entry in the split.
+ const QValueList<MyMoneySplit>& splits = (*it_transaction).splits();
+ QValueList<MyMoneySplit>::const_iterator myBegin, it_split;
+ //S_end = splits.end();
+
+ for (it_split = splits.begin(), myBegin = splits.end(); it_split != splits.end(); ++it_split) {
+ ReportAccount splitAcc = (* it_split).accountId();
+ // always put split with a "stock" account if it exists
+ if (splitAcc.isInvest())
+ break;
+
+ // prefer to put splits with a "loan" account if it exists
+ if(splitAcc.isLoan())
+ myBegin = it_split;
+
+ if((myBegin == splits.end()) && ! splitAcc.isIncomeExpense()) {
+ myBegin = it_split;
+ }
+ }
+
+ // select our "reference" split
+ if (it_split == splits.end()) {
+ it_split = myBegin;
+ } else {
+ myBegin = it_split;
+ }
+
+ // if the split is still unknown, use the first one. I have seen this
+ // happen with a transaction that has only a single split referencing an income or expense
+ // account and has an amount and value of 0. Such a transaction will fall through
+ // the above logic and leave 'it_split' pointing to splits.end() which causes the remainder
+ // of this to end in an infinite loop.
+ if(it_split == splits.end()) {
+ it_split = splits.begin();
+ }
+
+ // for "loan" reports, the loan transaction gets special treatment.
+ // the splits of a loan transaction are placed on one line in the
+ // reference (loan) account (qA). however, we process the matching
+ // split entries (qS) normally.
+ bool loan_special_case = false;
+ if(m_config.queryColumns() & MyMoneyReport::eQCloan) {
+ ReportAccount splitAcc = (*it_split).accountId();
+ loan_special_case = splitAcc.isLoan();
+ }
+
+ //the account of the beginning splits
+ ReportAccount myBeginAcc = (*myBegin).accountId();
+
+ bool include_me = true;
+ bool transaction_text = false; //indicates whether a text should be considered as a match for the transaction or for a split only
+ QString a_fullname = "";
+ QString a_memo = "";
+ unsigned int pass = 1;
+
+ do {
+ MyMoneyMoney xr;
+ ReportAccount splitAcc = (* it_split).accountId();
+
+ //get fraction for account
+ int fraction = splitAcc.currency().smallestAccountFraction();
+
+ //use base currency fraction if not initialized
+ if(fraction == -1)
+ fraction = file->baseCurrency().smallestAccountFraction();
+
+ QString institution = splitAcc.institutionId();
+ QString payee = (*it_split).payeeId();
+
+ if ( m_config.isConvertCurrency() ) {
+ xr = (splitAcc.deepCurrencyPrice((*it_transaction).postDate()) * splitAcc.baseCurrencyPrice((*it_transaction).postDate())).reduce();
+ } else {
+ xr = splitAcc.deepCurrencyPrice((*it_transaction).postDate()).reduce();
+ }
+
+ //there is a bug where the price sometimes returns 1
+ //get the price from the split in that case
+ /*if(m_config.isConvertCurrency() && xr == MyMoneyMoney(1,1)) {
+ xr = (*it_split).price();
+ }*/
+
+ if (splitAcc.isInvest()) {
+
+ // use the institution of the parent for stock accounts
+ institution = splitAcc.parent().institutionId();
+ MyMoneyMoney shares = (*it_split).shares();
+
+ qA["action"] = (*it_split).action();
+ qA["shares"] = shares.isZero() ? "" : (*it_split).shares().toString();
+ qA["price"] = shares.isZero() ? "" : xr.convert(MyMoneyMoney::precToDenom(KMyMoneyGlobalSettings::pricePrecision())).toString();
+
+ if (((*it_split).action() == MyMoneySplit::ActionBuyShares) && (*it_split).shares().isNegative())
+ qA["action"] = "Sell";
+
+ qA["investaccount"] = splitAcc.parent().name();
+ }
+
+ include_me = m_config.includes(splitAcc);
+ a_fullname = splitAcc.fullName();
+ a_memo = (*it_split).memo();
+
+ transaction_text = m_config.match(&(*it_split));
+
+ qA["price"] = xr.convert(MyMoneyMoney::precToDenom(KMyMoneyGlobalSettings::pricePrecision())).toString();
+ qA["account"] = splitAcc.name();
+ qA["accountid"] = splitAcc.id();
+ qA["topaccount"] = splitAcc.topParentName();
+
+ qA["institution"] = institution.isEmpty()
+ ? i18n("No Institution")
+ : file->institution(institution).name();
+
+ qA["payee"] = payee.isEmpty()
+ ? i18n("[Empty Payee]")
+ : file->payee(payee).name().simplifyWhiteSpace();
+
+ qA["reconciledate"] = (*it_split).reconcileDate().toString(Qt::ISODate);
+ qA["reconcileflag"] = KMyMoneyUtils::reconcileStateToString((*it_split).reconcileFlag(), true );
+ qA["number"] = (*it_split).number();
+
+ qA["memo"] = a_memo;
+
+ qS["reconciledate"] = qA["reconciledate"];
+ qS["reconcileflag"] = qA["reconcileflag"];
+ qS["number"] = qA["number"];
+
+ qS["topcategory"] = splitAcc.topParentName();
+
+ // only include the configured accounts
+ if (include_me) {
+ // add the "summarized" split transaction
+ // this is the sub-total of the split detail
+ // convert to lowest fraction
+ qA["value"] = ((*it_split).shares() * xr).convert(fraction).toString();
+ qA["rank"] = "0";
+
+ //fill in account information
+ if (! splitAcc.isIncomeExpense() && it_split != myBegin) {
+ qA["account"] = ((*it_split).shares().isNegative()) ?
+ i18n("Transfer to %1").arg(myBeginAcc.fullName())
+ : i18n("Transfer from %1").arg(myBeginAcc.fullName());
+ } else if (it_split == myBegin ) {
+ //handle the main split
+ if((splits.count() > 2)) {
+ //if it is the main split and has multiple splits, note that
+ qA["account"] = i18n("[Split Transaction]");
+ } else {
+ //fill the account name of the second split
+ QValueList<MyMoneySplit>::const_iterator tempSplit = splits.begin();
+
+ //there are supposed to be only 2 splits if we ever get here
+ if(tempSplit == myBegin && splits.count() > 1)
+ ++tempSplit;
+
+ //show the name of the category, or "transfer to/from" if it as an account
+ ReportAccount tempSplitAcc = (*tempSplit).accountId();
+ if (! tempSplitAcc.isIncomeExpense()) {
+ qA["account"] = ((*it_split).shares().isNegative()) ?
+ i18n("Transfer to %1").arg(tempSplitAcc.fullName())
+ : i18n("Transfer from %1").arg(tempSplitAcc.fullName());
+ } else {
+ qA["account"] = tempSplitAcc.fullName();
+ }
+ }
+ } else {
+ //in any other case, fill in the account name of the main split
+ qA["account"] = myBeginAcc.fullName();
+ }
+
+ //category data is always the one of the split
+ qA ["category"] = splitAcc.fullName();
+ qA ["topcategory"] = splitAcc.topParentName();
+ qA ["categorytype"] = KMyMoneyUtils::accountTypeToString(splitAcc.accountGroup());
+
+ m_rows += qA;
+
+ // track accts that will need opening and closing balances
+ accts.insert (splitAcc.id(), splitAcc);
+ }
+ ++it_split;
+
+ // look for wrap-around
+ if (it_split == splits.end())
+ it_split = splits.begin();
+
+ //check if there have been more passes than there are splits
+ //this is to prevent infinite loops in cases of data inconsistency -- asoliverez
+ ++pass;
+ if( pass > splits.count() )
+ break;
+
+ } while (it_split != myBegin);
+
+ if (loan_special_case) {
+ m_rows += qA;
+ }
+ }
+
+ // now run through our accts list and add opening and closing balances
+
+ switch (m_config.rowType()) {
+ case MyMoneyReport::eAccount:
+ case MyMoneyReport::eTopAccount:
+ break;
+
+ // case MyMoneyReport::eCategory:
+ // case MyMoneyReport::eTopCategory:
+ // case MyMoneyReport::ePayee:
+ // case MyMoneyReport::eMonth:
+ // case MyMoneyReport::eWeek:
+ default:
+ return;
+ }
+
+ QDate startDate, endDate;
+
+ report.validDateRange(startDate, endDate);
+ QString strStartDate = startDate.toString(Qt::ISODate);
+ QString strEndDate = endDate.toString(Qt::ISODate);
+ startDate = startDate.addDays(-1);
+
+ QMap<QString, MyMoneyAccount>::const_iterator it_account, accts_end;
+ for (it_account = accts.begin(); it_account != accts.end(); ++it_account) {
+ TableRow qA;
+
+ ReportAccount account = (* it_account);
+
+ //get fraction for account
+ int fraction = account.currency().smallestAccountFraction();
+
+ //use base currency fraction if not initialized
+ if(fraction == -1)
+ fraction = file->baseCurrency().smallestAccountFraction();
+
+ QString institution = account.institutionId();
+
+ // use the institution of the parent for stock accounts
+ if (account.isInvest())
+ institution = account.parent().institutionId();
+
+ MyMoneyMoney startBalance, endBalance, startPrice, endPrice;
+ MyMoneyMoney startShares, endShares;
+
+ //get price and convert currency if necessary
+ if ( m_config.isConvertCurrency() ) {
+ startPrice = (account.deepCurrencyPrice(startDate) * account.baseCurrencyPrice(startDate)).reduce();
+ endPrice = (account.deepCurrencyPrice(endDate) * account.baseCurrencyPrice(endDate)).reduce();
+ } else {
+ startPrice = account.deepCurrencyPrice(startDate).reduce();
+ endPrice = account.deepCurrencyPrice(endDate).reduce();
+ }
+ startShares = file->balance(account.id(),startDate);
+ endShares = file->balance(account.id(),endDate);
+
+ //get starting and ending balances
+ startBalance = startShares * startPrice;
+ endBalance = endShares * endPrice;
+
+ //starting balance
+ // don't show currency if we're converting or if it's not foreign
+ qA["currency"] = (m_config.isConvertCurrency() || ! account.isForeignCurrency()) ? "" : account.currency().id();
+
+ qA["accountid"] = account.id();
+ qA["account"] = account.name();
+ qA["topaccount"] = account.topParentName();
+ qA["institution"] = institution.isEmpty() ? i18n("No Institution") : file->institution(institution).name();
+ qA["rank"] = "-2";
+
+ qA["price"] = startPrice.convert(MyMoneyMoney::precToDenom(KMyMoneyGlobalSettings::pricePrecision())).toString();
+ if (account.isInvest()) {
+ qA["shares"] = startShares.toString();
+ }
+
+ qA["postdate"] = strStartDate;
+ qA["balance"] = startBalance.convert(fraction).toString();
+ qA["value"] = QString();
+ qA["id"] = "A";
+ m_rows += qA;
+
+ //ending balance
+ qA["price"] = endPrice.convert(MyMoneyMoney::precToDenom(KMyMoneyGlobalSettings::pricePrecision())).toString();
+
+ if (account.isInvest()) {
+ qA["shares"] = endShares.toString();
+ }
+
+ qA["postdate"] = strEndDate;
+ qA["balance"] = endBalance.toString();
+ qA["id"] = "Z";
+ m_rows += qA;
+ }
+}
+
+}
+// vim:cin:si:ai:et:ts=2:sw=2:
diff --git a/kmymoney2/reports/querytable.h b/kmymoney2/reports/querytable.h
new file mode 100644
index 0000000..7beb3d4
--- /dev/null
+++ b/kmymoney2/reports/querytable.h
@@ -0,0 +1,142 @@
+/***************************************************************************
+ querytable.h
+ -------------------
+ begin : Fri Jul 23 2004
+ copyright : (C) 2004-2005 by Ace Jones
+ (C) 2007 Sascha Pfau
+ email : acejones@users.sourceforge.net
+ MrPeacock@gmail.com
+ ***************************************************************************/
+
+/****************************************************************************
+ Contains code from the func_xirr and related methods of financial.cpp
+ - KOffice 1.6 by Sascha Pfau. Sascha agreed to relicense those methods under
+ GPLv2 or later.
+*****************************************************************************/
+
+/***************************************************************************
+ * *
+ * This program is free software; you can redistribute it and/or modify *
+ * it under the terms of the GNU General Public License as published by *
+ * the Free Software Foundation; either version 2 of the License, or *
+ * (at your option) any later version. *
+ * *
+ ***************************************************************************/
+
+#ifndef QUERYTABLE_H
+#define QUERYTABLE_H
+
+// ----------------------------------------------------------------------------
+// QT Includes
+
+#include <qstringlist.h>
+
+// ----------------------------------------------------------------------------
+// KDE Includes
+
+// ----------------------------------------------------------------------------
+// Project Includes
+
+#include "../mymoney/mymoneyreport.h"
+#include "listtable.h"
+
+namespace reports {
+
+class ReportAccount;
+
+/**
+ * Calculates a query of information about the transaction database.
+ *
+ * This is a middle-layer class, between the UI and the engine. The
+ * MyMoneyReport class holds only the CONFIGURATION parameters. This
+ * class actually does the work of retrieving the data from the engine
+ * and formatting it for the user.
+ *
+ * @author Ace Jones
+ *
+ * @short
+**/
+
+class QueryTable : public ListTable
+{
+ public:
+ QueryTable(const MyMoneyReport&);
+ void init(void);
+
+ protected:
+ void constructAccountTable(void);
+ void constructTransactionTable(void);
+ void constructPerformanceRow( const ReportAccount& account, TableRow& result ) const;
+ void constructSplitsTable(void);
+
+};
+
+//
+// Cash Flow analysis tools for investment reports
+//
+
+class CashFlowListItem
+{
+public:
+ CashFlowListItem(void) {}
+ CashFlowListItem( const QDate& _date, const MyMoneyMoney& _value ): m_date(_date), m_value(_value) {}
+ bool operator<( const CashFlowListItem _second ) const { return m_date < _second.m_date; }
+ bool operator<=( const CashFlowListItem _second ) const { return m_date <= _second.m_date; }
+ bool operator>( const CashFlowListItem _second ) const { return m_date > _second.m_date; }
+ const QDate& date( void ) const { return m_date; }
+ const MyMoneyMoney& value( void ) const { return m_value; }
+ MyMoneyMoney NPV( double _rate ) const;
+
+ static void setToday( const QDate& _today ) { m_sToday = _today; }
+ const QDate& today( void ) const { return m_sToday; }
+
+private:
+ QDate m_date;
+ MyMoneyMoney m_value;
+
+ static QDate m_sToday;
+};
+
+class CashFlowList: public QValueList<CashFlowListItem>
+{
+ public:
+ CashFlowList(void) {}
+ MyMoneyMoney NPV(double rate) const;
+ double IRR(void) const;
+ MyMoneyMoney total(void) const;
+ void dumpDebug(void) const;
+
+ /**
+ * Function: XIRR
+ *
+ * Compute the internal rate of return for a non-periodic series of cash flows.
+ *
+ * XIRR ( Values; Dates; [ Guess = 0.1 ] )
+ **/
+ double calculateXIRR ( void ) const;
+
+ protected:
+ CashFlowListItem mostRecent(void) const;
+
+ private:
+ /**
+ * helper: xirrResult
+ *
+ * args[0] = values
+ * args[1] = dates
+ **/
+ double xirrResult ( double& rate ) const;
+
+ /**
+ *
+ * helper: xirrResultDerive
+ *
+ * args[0] = values
+ * args[1] = dates
+ **/
+ double xirrResultDerive ( double& rate ) const;
+};
+
+}
+
+#endif // QUERYREPORT_H
diff --git a/kmymoney2/reports/querytabletest.cpp b/kmymoney2/reports/querytabletest.cpp
new file mode 100644
index 0000000..8b3c579
--- /dev/null
+++ b/kmymoney2/reports/querytabletest.cpp
@@ -0,0 +1,694 @@
+/***************************************************************************
+ querytabletest.cpp
+ -------------------
+ copyright : (C) 2002 by Thomas Baumgart
+ email : ipwizard@users.sourceforge.net
+ Ace Jones <ace.j@hotpop.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. *
+ * *
+ ***************************************************************************/
+
+#include <qvaluelist.h>
+#include <qvaluevector.h>
+#include <qfile.h>
+
+#include <kdebug.h>
+#include <kdeversion.h>
+#include <kglobal.h>
+#include <kglobalsettings.h>
+#include <klocale.h>
+#include <kstandarddirs.h>
+
+#include "querytabletest.h"
+#include "reportstestcommon.h"
+
+#define private public
+#include "querytable.h"
+#undef private
+
+#include "../mymoney/mymoneyaccount.h"
+#include "../mymoney/mymoneysecurity.h"
+#include "../mymoney/mymoneyprice.h"
+#include "../mymoney/storage/mymoneystoragedump.h"
+#include "../mymoney/mymoneyreport.h"
+#include "../mymoney/mymoneystatement.h"
+#include "../mymoney/storage/mymoneystoragexml.h"
+
+using namespace reports;
+using namespace test;
+
+QueryTableTest::QueryTableTest()
+{
+}
+
+void QueryTableTest::setUp () {
+
+ storage = new MyMoneySeqAccessMgr;
+ file = MyMoneyFile::instance();
+ file->attachStorage(storage);
+ MyMoneyFileTransaction ft;
+ file->addCurrency(MyMoneySecurity("CAD", "Canadian Dollar", "C$"));
+ file->addCurrency(MyMoneySecurity("USD", "US Dollar", "$"));
+ file->addCurrency(MyMoneySecurity("JPY", "Japanese Yen", QChar(0x00A5), 100, 1));
+ file->addCurrency(MyMoneySecurity("GBP", "British Pound", "#"));
+ file->setBaseCurrency(file->currency("USD"));
+
+ MyMoneyPayee payeeTest("Test Payee");
+ file->addPayee(payeeTest);
+ MyMoneyPayee payeeTest2("Thomas Baumgart");
+ file->addPayee(payeeTest2);
+
+ acAsset = (MyMoneyFile::instance()->asset().id());
+ acLiability = (MyMoneyFile::instance()->liability().id());
+ acExpense = (MyMoneyFile::instance()->expense().id());
+ acIncome = (MyMoneyFile::instance()->income().id());
+ acChecking = makeAccount(QString("Checking Account"),MyMoneyAccount::Checkings,moCheckingOpen,QDate(2004,5,15),acAsset);
+ acCredit = makeAccount(QString("Credit Card"),MyMoneyAccount::CreditCard,moCreditOpen,QDate(2004,7,15),acLiability);
+ acSolo = makeAccount(QString("Solo"),MyMoneyAccount::Expense,0,QDate(2004,1,11),acExpense);
+ acParent = makeAccount(QString("Parent"),MyMoneyAccount::Expense,0,QDate(2004,1,11),acExpense);
+ acChild = makeAccount(QString("Child"),MyMoneyAccount::Expense,0,QDate(2004,2,11),acParent);
+ acForeign = makeAccount(QString("Foreign"),MyMoneyAccount::Expense,0,QDate(2004,1,11),acExpense);
+ acTax = makeAccount(QString("Tax"), MyMoneyAccount::Expense,0,QDate(2005,1,11),acExpense, "", true);
+
+ MyMoneyInstitution i("Bank of the World","","","","","","");
+ file->addInstitution(i);
+ inBank = i.id();
+ ft.commit();
+}
+
+void QueryTableTest::tearDown ()
+{
+ file->detachStorage(storage);
+ delete storage;
+}
+
+void QueryTableTest::testQueryBasics()
+{
+ try
+ {
+ TransactionHelper t1q1( QDate(2004,1,1), MyMoneySplit::ActionWithdrawal, moSolo, acChecking, acSolo );
+ TransactionHelper t2q1( QDate(2004,2,1), MyMoneySplit::ActionWithdrawal, moParent1, acCredit, acParent );
+ TransactionHelper t3q1( QDate(2004,3,1), MyMoneySplit::ActionWithdrawal, moParent2, acCredit, acParent );
+ TransactionHelper t4y1( QDate(2004,11,7), MyMoneySplit::ActionWithdrawal, moChild, acCredit, acChild );
+
+ TransactionHelper t1q2( QDate(2004,4,1), MyMoneySplit::ActionWithdrawal, moSolo, acChecking, acSolo );
+ TransactionHelper t2q2( QDate(2004,5,1), MyMoneySplit::ActionWithdrawal, moParent1, acCredit, acParent );
+ TransactionHelper t3q2( QDate(2004,6,1), MyMoneySplit::ActionWithdrawal, moParent2, acCredit, acParent );
+ TransactionHelper t4q2( QDate(2004,11,7), MyMoneySplit::ActionWithdrawal, moChild, acCredit, acChild );
+
+ TransactionHelper t1y2( QDate(2005,1,1), MyMoneySplit::ActionWithdrawal, moSolo, acChecking, acSolo );
+ TransactionHelper t2y2( QDate(2005,5,1), MyMoneySplit::ActionWithdrawal, moParent1, acCredit, acParent );
+ TransactionHelper t3y2( QDate(2005,9,1), MyMoneySplit::ActionWithdrawal, moParent2, acCredit, acParent );
+ TransactionHelper t4y2( QDate(2004,11,7), MyMoneySplit::ActionWithdrawal, moChild, acCredit, acChild );
+
+ unsigned cols;
+
+ MyMoneyReport filter;
+ filter.setRowType( MyMoneyReport::eCategory );
+ cols = MyMoneyReport::eQCnumber | MyMoneyReport::eQCpayee | MyMoneyReport::eQCaccount;
+ filter.setQueryColumns( static_cast<MyMoneyReport::EQueryColumns>(cols) ); //
+ filter.setName("Transactions by Category");
+ XMLandback(filter);
+ QueryTable qtbl_1(filter);
+
+ writeTabletoHTML(qtbl_1,"Transactions by Category.html");
+
+ QValueList<ListTable::TableRow> rows = qtbl_1.rows();
+
+ CPPUNIT_ASSERT(rows.count() == 12);
+ CPPUNIT_ASSERT(rows[0]["categorytype"]=="Expense");
+ CPPUNIT_ASSERT(rows[0]["category"]=="Parent");
+ CPPUNIT_ASSERT(rows[0]["postdate"]=="2004-02-01");
+ CPPUNIT_ASSERT(rows[11]["categorytype"]=="Expense");
+ CPPUNIT_ASSERT(rows[11]["category"]=="Solo");
+ CPPUNIT_ASSERT(rows[11]["postdate"]=="2005-01-01");
+
+ QString html = qtbl_1.renderHTML();
+ CPPUNIT_ASSERT( searchHTML(html,i18n("Total")+" Parent") == -(moParent1 + moParent2) * 3 );
+ CPPUNIT_ASSERT( searchHTML(html,i18n("Total")+" Parent: Child") == -(moChild) * 3 );
+ CPPUNIT_ASSERT( searchHTML(html,i18n("Total")+" Solo") == -(moSolo) * 3 );
+ CPPUNIT_ASSERT( searchHTML(html,i18n("Total")+" Expense") == -(moParent1 + moParent2 + moSolo + moChild) * 3 );
+ CPPUNIT_ASSERT( searchHTML(html,i18n("Grand Total")) == -(moParent1 + moParent2 + moSolo + moChild) * 3 + moCheckingOpen + moCreditOpen );
+ filter.setRowType( MyMoneyReport::eTopCategory );
+ cols = MyMoneyReport::eQCnumber | MyMoneyReport::eQCpayee | MyMoneyReport::eQCaccount;
+ filter.setQueryColumns( static_cast<MyMoneyReport::EQueryColumns>(cols) ); //
+ filter.setName("Transactions by Top Category");
+ XMLandback(filter);
+ QueryTable qtbl_2(filter);
+
+ writeTabletoHTML(qtbl_2,"Transactions by Top Category.html");
+
+ rows = qtbl_2.rows();
+
+ CPPUNIT_ASSERT(rows.count() == 12);
+ CPPUNIT_ASSERT(rows[0]["categorytype"]=="Expense");
+ CPPUNIT_ASSERT(rows[0]["topcategory"]=="Parent");
+ CPPUNIT_ASSERT(rows[0]["postdate"]=="2004-02-01");
+ CPPUNIT_ASSERT(rows[8]["categorytype"]=="Expense");
+ CPPUNIT_ASSERT(rows[8]["topcategory"]=="Parent");
+ CPPUNIT_ASSERT(rows[8]["postdate"]=="2005-09-01");
+ CPPUNIT_ASSERT(rows[11]["categorytype"]=="Expense");
+ CPPUNIT_ASSERT(rows[11]["topcategory"]=="Solo");
+ CPPUNIT_ASSERT(rows[11]["postdate"]=="2005-01-01");
+
+ html = qtbl_2.renderHTML();
+ CPPUNIT_ASSERT( searchHTML(html,i18n("Total")+" Parent") == -(moParent1 + moParent2 + moChild) * 3 );
+ CPPUNIT_ASSERT( searchHTML(html,i18n("Total")+" Solo") == -(moSolo) * 3 );
+ CPPUNIT_ASSERT( searchHTML(html,i18n("Total")+" Expense") == -(moParent1 + moParent2 + moSolo + moChild) * 3 );
+ CPPUNIT_ASSERT( searchHTML(html,i18n("Grand Total")) == -(moParent1 + moParent2 + moSolo + moChild) * 3 + moCheckingOpen + moCreditOpen);
+
+ filter.setRowType( MyMoneyReport::eAccount );
+ filter.setName("Transactions by Account");
+ cols = MyMoneyReport::eQCnumber | MyMoneyReport::eQCpayee | MyMoneyReport::eQCcategory;
+ filter.setQueryColumns( static_cast<MyMoneyReport::EQueryColumns>(cols) ); //
+ XMLandback(filter);
+ QueryTable qtbl_3(filter);
+
+ writeTabletoHTML(qtbl_3,"Transactions by Account.html");
+
+ rows = qtbl_3.rows();
+
+#if 1
+ CPPUNIT_ASSERT(rows.count() == 16);
+ CPPUNIT_ASSERT(rows[1]["account"]=="Checking Account");
+ CPPUNIT_ASSERT(rows[1]["category"]=="Solo");
+ CPPUNIT_ASSERT(rows[1]["postdate"]=="2004-01-01");
+ CPPUNIT_ASSERT(rows[14]["account"]=="Credit Card");
+ CPPUNIT_ASSERT(rows[14]["category"]=="Parent");
+ CPPUNIT_ASSERT(rows[14]["postdate"]=="2005-09-01");
+#else
+ CPPUNIT_ASSERT(rows.count() == 12);
+ CPPUNIT_ASSERT(rows[0]["account"]=="Checking Account");
+ CPPUNIT_ASSERT(rows[0]["category"]=="Solo");
+ CPPUNIT_ASSERT(rows[0]["postdate"]=="2004-01-01");
+ CPPUNIT_ASSERT(rows[11]["account"]=="Credit Card");
+ CPPUNIT_ASSERT(rows[11]["category"]=="Parent");
+ CPPUNIT_ASSERT(rows[11]["postdate"]=="2005-09-01");
+#endif
+
+ html = qtbl_3.renderHTML();
+ CPPUNIT_ASSERT( searchHTML(html,i18n("Total")+" Checking Account") == -(moSolo) * 3 + moCheckingOpen);
+ CPPUNIT_ASSERT( searchHTML(html,i18n("Total")+" Credit Card") == -(moParent1 + moParent2 + moChild) * 3 + moCreditOpen );
+ CPPUNIT_ASSERT( searchHTML(html,i18n("Grand Total")) == -(moParent1 + moParent2 + moSolo + moChild) * 3 + moCheckingOpen + moCreditOpen);
+
+ filter.setRowType( MyMoneyReport::ePayee );
+ filter.setName("Transactions by Payee");
+ cols = MyMoneyReport::eQCnumber | MyMoneyReport::eQCmemo | MyMoneyReport::eQCcategory;
+ filter.setQueryColumns( static_cast<MyMoneyReport::EQueryColumns>(cols) ); //
+ XMLandback(filter);
+ QueryTable qtbl_4(filter);
+
+ writeTabletoHTML(qtbl_4,"Transactions by Payee.html");
+
+ rows = qtbl_4.rows();
+
+ CPPUNIT_ASSERT(rows.count() == 12);
+ CPPUNIT_ASSERT(rows[0]["payee"]=="Test Payee");
+ CPPUNIT_ASSERT(rows[0]["category"]=="Solo");
+ CPPUNIT_ASSERT(rows[0]["postdate"]=="2004-01-01");
+ CPPUNIT_ASSERT(rows[8]["payee"]=="Test Payee");
+ CPPUNIT_ASSERT(rows[8]["category"]=="Parent: Child");
+ CPPUNIT_ASSERT(rows[8]["postdate"]=="2004-11-07");
+ CPPUNIT_ASSERT(rows[11]["payee"]=="Test Payee");
+ CPPUNIT_ASSERT(rows[11]["category"]=="Parent");
+ CPPUNIT_ASSERT(rows[11]["postdate"]=="2005-09-01");
+
+ html = qtbl_4.renderHTML();
+ CPPUNIT_ASSERT( searchHTML(html,i18n("Total")+" Test Payee") == -(moParent1 + moParent2 + moSolo + moChild) * 3 );
+ CPPUNIT_ASSERT( searchHTML(html,i18n("Grand Total")) == -(moParent1 + moParent2 + moSolo + moChild) * 3 + moCheckingOpen + moCreditOpen);
+
+ filter.setRowType( MyMoneyReport::eMonth );
+ filter.setName("Transactions by Month");
+ cols = MyMoneyReport::eQCnumber | MyMoneyReport::eQCpayee | MyMoneyReport::eQCcategory;
+ filter.setQueryColumns( static_cast<MyMoneyReport::EQueryColumns>(cols) ); //
+ XMLandback(filter);
+ QueryTable qtbl_5(filter);
+
+ writeTabletoHTML(qtbl_5,"Transactions by Month.html");
+
+ rows = qtbl_5.rows();
+
+ CPPUNIT_ASSERT(rows.count() == 12);
+ CPPUNIT_ASSERT(rows[0]["payee"]=="Test Payee");
+ CPPUNIT_ASSERT(rows[0]["category"]=="Solo");
+ CPPUNIT_ASSERT(rows[0]["postdate"]=="2004-01-01");
+ CPPUNIT_ASSERT(rows[8]["payee"]=="Test Payee");
+ CPPUNIT_ASSERT(rows[8]["category"]=="Parent: Child");
+ CPPUNIT_ASSERT(rows[8]["postdate"]=="2004-11-07");
+ CPPUNIT_ASSERT(rows[11]["payee"]=="Test Payee");
+ CPPUNIT_ASSERT(rows[11]["category"]=="Parent");
+ CPPUNIT_ASSERT(rows[11]["postdate"]=="2005-09-01");
+
+ html = qtbl_5.renderHTML();
+ CPPUNIT_ASSERT( searchHTML(html,i18n("Total")+" Month of 2004-01-01") == -moSolo );
+ CPPUNIT_ASSERT( searchHTML(html,i18n("Total")+" Month of 2004-11-01") == -(moChild) * 3 );
+ CPPUNIT_ASSERT( searchHTML(html,i18n("Total")+" Month of 2004-05-01") == -moParent1 + moCheckingOpen );
+ CPPUNIT_ASSERT( searchHTML(html,i18n("Grand Total")) == -(moParent1 + moParent2 + moSolo + moChild) * 3 + moCheckingOpen + moCreditOpen);
+
+ filter.setRowType( MyMoneyReport::eWeek );
+ filter.setName("Transactions by Week");
+ cols = MyMoneyReport::eQCnumber | MyMoneyReport::eQCpayee | MyMoneyReport::eQCcategory;
+ filter.setQueryColumns( static_cast<MyMoneyReport::EQueryColumns>(cols) ); //
+ XMLandback(filter);
+ QueryTable qtbl_6(filter);
+
+ writeTabletoHTML(qtbl_6,"Transactions by Week.html");
+
+ rows = qtbl_6.rows();
+
+ CPPUNIT_ASSERT(rows.count() == 12);
+ CPPUNIT_ASSERT(rows[0]["payee"]=="Test Payee");
+ CPPUNIT_ASSERT(rows[0]["category"]=="Solo");
+ CPPUNIT_ASSERT(rows[0]["postdate"]=="2004-01-01");
+ CPPUNIT_ASSERT(rows[11]["payee"]=="Test Payee");
+ CPPUNIT_ASSERT(rows[11]["category"]=="Parent");
+ CPPUNIT_ASSERT(rows[11]["postdate"]=="2005-09-01");
+
+ html = qtbl_6.renderHTML();
+ CPPUNIT_ASSERT( searchHTML(html,i18n("Total")+" Week of 2003-12-29") == -moSolo );
+ CPPUNIT_ASSERT( searchHTML(html,i18n("Total")+" Week of 2004-11-01") == -(moChild) * 3 );
+ CPPUNIT_ASSERT( searchHTML(html,i18n("Total")+" Week of 2005-08-29") == -moParent2 );
+ CPPUNIT_ASSERT( searchHTML(html,i18n("Grand Total")) == -(moParent1 + moParent2 + moSolo + moChild) * 3 + moCheckingOpen + moCreditOpen);
+ }
+ catch(MyMoneyException *e)
+ {
+ CPPUNIT_FAIL(e->what());
+ delete e;
+ }
+
+ // Test querytable::TableRow::operator> and operator==
+
+ QueryTable::TableRow low;
+ low["first"] = "A";
+ low["second"] = "B";
+ low["third"] = "C";
+
+ QueryTable::TableRow high;
+ high["first"] = "A";
+ high["second"] = "C";
+ high["third"] = "B";
+
+ QueryTable::TableRow::setSortCriteria("first,second,third");
+ CPPUNIT_ASSERT( low < high );
+ CPPUNIT_ASSERT( low <= high );
+ CPPUNIT_ASSERT( high > low );
+ CPPUNIT_ASSERT( high <= high );
+ CPPUNIT_ASSERT( high == high );
+}
+
+void QueryTableTest::testCashFlowAnalysis()
+{
+ //
+ // Test IRR calculations
+ //
+
+ CashFlowList list;
+
+ list += CashFlowListItem( QDate(2004,5,3),1000.0 );
+ list += CashFlowListItem( QDate(2004,5,20),59.0 );
+ list += CashFlowListItem( QDate(2004,6,3),14.0 );
+ list += CashFlowListItem( QDate(2004,6,24),92.0 );
+ list += CashFlowListItem( QDate(2004,7,6),63.0 );
+ list += CashFlowListItem( QDate(2004,7,25),15.0 );
+ list += CashFlowListItem( QDate(2004,8,5),92.0 );
+ list += CashFlowListItem( QDate(2004,9,2),18.0 );
+ list += CashFlowListItem( QDate(2004,9,21),5.0 );
+ list += CashFlowListItem( QDate(2004,10,16),-2037.0 );
+
+ MyMoneyMoney IRR(list.IRR(),1000);
+
+ CPPUNIT_ASSERT( IRR == MyMoneyMoney(1676,1000) );
+
+ list.pop_back();
+ list += CashFlowListItem( QDate(2004,10,16),-1358.0 );
+
+ IRR = MyMoneyMoney( list.IRR(), 1000 );
+
+ CPPUNIT_ASSERT( IRR.isZero() );
+}
+
+void QueryTableTest::testAccountQuery()
+{
+ try
+ {
+ QString htmlcontext = QString("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0//EN\">\n<html><head><link rel=\"stylesheet\" type=\"text/css\" href=\"html/kmymoney2.css\"></head><body>\n%1\n</body></html>\n");
+
+ //
+ // No transactions, opening balances only
+ //
+
+ MyMoneyReport filter;
+ filter.setRowType( MyMoneyReport::eInstitution );
+ filter.setName("Accounts by Institution (No transactions)");
+ XMLandback(filter);
+ QueryTable qtbl_1(filter);
+
+ writeTabletoHTML(qtbl_1,"Accounts by Institution (No transactions).html");
+
+ QValueList<ListTable::TableRow> rows = qtbl_1.rows();
+
+ CPPUNIT_ASSERT(rows.count() == 2);
+ CPPUNIT_ASSERT(rows[0]["account"]=="Checking Account");
+ CPPUNIT_ASSERT(MyMoneyMoney(rows[0]["value"])==moCheckingOpen);
+ CPPUNIT_ASSERT(rows[0]["equitytype"].isEmpty());
+ CPPUNIT_ASSERT(rows[1]["account"]=="Credit Card");
+ CPPUNIT_ASSERT(MyMoneyMoney(rows[1]["value"])==moCreditOpen);
+ CPPUNIT_ASSERT(rows[1]["equitytype"].isEmpty());
+
+ QString html = qtbl_1.renderHTML();
+ CPPUNIT_ASSERT( searchHTML(html,i18n("Total")+" None") == moCheckingOpen+moCreditOpen );
+ CPPUNIT_ASSERT( searchHTML(html,i18n("Grand Total")) == moCheckingOpen+moCreditOpen );
+
+ //
+ // Adding in transactions
+ //
+
+ TransactionHelper t1q1( QDate(2004,1,1), MyMoneySplit::ActionWithdrawal, moSolo, acChecking, acSolo );
+ TransactionHelper t2q1( QDate(2004,2,1), MyMoneySplit::ActionWithdrawal, moParent1, acCredit, acParent );
+ TransactionHelper t3q1( QDate(2004,3,1), MyMoneySplit::ActionWithdrawal, moParent2, acCredit, acParent );
+ TransactionHelper t4y1( QDate(2004,11,7), MyMoneySplit::ActionWithdrawal, moChild, acCredit, acChild );
+
+ TransactionHelper t1q2( QDate(2004,4,1), MyMoneySplit::ActionWithdrawal, moSolo, acChecking, acSolo );
+ TransactionHelper t2q2( QDate(2004,5,1), MyMoneySplit::ActionWithdrawal, moParent1, acCredit, acParent );
+ TransactionHelper t3q2( QDate(2004,6,1), MyMoneySplit::ActionWithdrawal, moParent2, acCredit, acParent );
+ TransactionHelper t4q2( QDate(2004,11,7), MyMoneySplit::ActionWithdrawal, moChild, acCredit, acChild );
+
+ TransactionHelper t1y2( QDate(2005,1,1), MyMoneySplit::ActionWithdrawal, moSolo, acChecking, acSolo );
+ TransactionHelper t2y2( QDate(2005,5,1), MyMoneySplit::ActionWithdrawal, moParent1, acCredit, acParent );
+ TransactionHelper t3y2( QDate(2005,9,1), MyMoneySplit::ActionWithdrawal, moParent2, acCredit, acParent );
+ TransactionHelper t4y2( QDate(2004,11,7), MyMoneySplit::ActionWithdrawal, moChild, acCredit, acChild );
+
+ filter.setRowType( MyMoneyReport::eInstitution );
+ filter.setName("Accounts by Institution (With Transactions)");
+ XMLandback(filter);
+ QueryTable qtbl_2(filter);
+
+ rows = qtbl_2.rows();
+
+ CPPUNIT_ASSERT(rows.count() == 2);
+ CPPUNIT_ASSERT(rows[0]["account"]=="Checking Account");
+ CPPUNIT_ASSERT(MyMoneyMoney(rows[0]["value"])==(moCheckingOpen-moSolo*3));
+ CPPUNIT_ASSERT(rows[1]["account"]=="Credit Card");
+ CPPUNIT_ASSERT(MyMoneyMoney(rows[1]["value"])==(moCreditOpen-(moParent1 + moParent2 + moChild) * 3));
+
+ html = qtbl_2.renderHTML();
+ CPPUNIT_ASSERT( searchHTML(html,i18n("Grand Total")) == moCheckingOpen+moCreditOpen-(moParent1 + moParent2 + moSolo + moChild) * 3 );
+
+ //
+ // Account TYPES
+ //
+
+ filter.setRowType( MyMoneyReport::eAccountType );
+ filter.setName("Accounts by Type");
+ XMLandback(filter);
+ QueryTable qtbl_3(filter);
+
+ rows = qtbl_3.rows();
+
+ CPPUNIT_ASSERT(rows.count() == 2);
+ CPPUNIT_ASSERT(rows[0]["account"]=="Checking Account");
+ CPPUNIT_ASSERT(MyMoneyMoney(rows[0]["value"])==(moCheckingOpen-moSolo*3));
+ CPPUNIT_ASSERT(rows[1]["account"]=="Credit Card");
+ CPPUNIT_ASSERT(MyMoneyMoney(rows[1]["value"])==(moCreditOpen-(moParent1 + moParent2 + moChild) * 3));
+
+ html = qtbl_3.renderHTML();
+ CPPUNIT_ASSERT( searchHTML(html,i18n("Total")+" "+i18n("Checking")) == moCheckingOpen-moSolo*3 );
+ CPPUNIT_ASSERT( searchHTML(html,i18n("Total")+" "+i18n("Credit Card")) == moCreditOpen-(moParent1 + moParent2 + moChild) * 3 );
+ CPPUNIT_ASSERT( searchHTML(html,i18n("Grand Total")) == moCheckingOpen+moCreditOpen-(moParent1 + moParent2 + moSolo + moChild) * 3 );
+ }
+ catch(MyMoneyException *e)
+ {
+ CPPUNIT_FAIL(e->what());
+ delete e;
+ }
+}
+
+void QueryTableTest::testInvestment(void)
+{
+ try
+ {
+ // Equities
+ eqStock1 = makeEquity("Stock1","STK1");
+ eqStock2 = makeEquity("Stock2","STK2");
+
+ // Accounts
+ acInvestment = makeAccount("Investment",MyMoneyAccount::Investment,moZero,QDate(2004,1,1),acAsset);
+ acStock1 = makeAccount("Stock 1",MyMoneyAccount::Stock,moZero,QDate(2004,1,1),acInvestment,eqStock1);
+ acStock2 = makeAccount("Stock 2",MyMoneyAccount::Stock,moZero,QDate(2004,1,1),acInvestment,eqStock2);
+ acDividends = makeAccount("Dividends",MyMoneyAccount::Income,moZero,QDate(2004,1,1),acIncome);
+ acInterest = makeAccount("Interest",MyMoneyAccount::Income,moZero,QDate(2004,1,1),acIncome);
+
+ // Transactions
+ // Date Action Shares Price Stock Asset Income
+ InvTransactionHelper s1b1( QDate(2004,2,1), MyMoneySplit::ActionBuyShares, 1000.00, 100.00, acStock1, acChecking, QString() );
+ InvTransactionHelper s1b2( QDate(2004,3,1), MyMoneySplit::ActionBuyShares, 1000.00, 110.00, acStock1, acChecking, QString() );
+ InvTransactionHelper s1s1( QDate(2004,4,1), MyMoneySplit::ActionBuyShares, -200.00, 120.00, acStock1, acChecking, QString() );
+ InvTransactionHelper s1s2( QDate(2004,5,1), MyMoneySplit::ActionBuyShares, -200.00, 100.00, acStock1, acChecking, QString() );
+ InvTransactionHelper s1r1( QDate(2004,6,1), MyMoneySplit::ActionReinvestDividend, 50.00, 100.00, acStock1, QString(), acDividends );
+ InvTransactionHelper s1r2( QDate(2004,7,1), MyMoneySplit::ActionReinvestDividend, 50.00, 80.00, acStock1, QString(), acDividends );
+ InvTransactionHelper s1c1( QDate(2004,8,1), MyMoneySplit::ActionDividend, 10.00, 100.00, acStock1, acChecking, acDividends );
+ InvTransactionHelper s1c2( QDate(2004,9,1), MyMoneySplit::ActionDividend, 10.00, 120.00, acStock1, acChecking, acDividends );
+ InvTransactionHelper s1y1( QDate(2004,9,15), MyMoneySplit::ActionYield, 10.00, 110.00, acStock1, acChecking, acInterest );
+
+ makeEquityPrice( eqStock1, QDate(2004,10,1), 100.00 );
+
+ //
+ // Investment Transactions Report
+ //
+
+ MyMoneyReport invtran_r(
+ MyMoneyReport::eTopAccount,
+ MyMoneyReport::eQCaction|MyMoneyReport::eQCshares|MyMoneyReport::eQCprice,
+ MyMoneyTransactionFilter::userDefined,
+ MyMoneyReport::eDetailAll,
+ i18n("Investment Transactions"),
+ i18n("Test Report")
+ );
+ invtran_r.setDateFilter(QDate(2004,1,1),QDate(2004,12,31));
+ invtran_r.setInvestmentsOnly(true);
+ XMLandback(invtran_r);
+ QueryTable invtran(invtran_r);
+
+#if 1
+ writeTabletoHTML(invtran,"investment_transactions_test.html");
+
+ QValueList<ListTable::TableRow> rows = invtran.rows();
+
+ CPPUNIT_ASSERT(rows.count()==17);
+ CPPUNIT_ASSERT(MyMoneyMoney(rows[1]["value"])==MyMoneyMoney(100000.00));
+ CPPUNIT_ASSERT(MyMoneyMoney(rows[2]["value"])==MyMoneyMoney(110000.00));
+ CPPUNIT_ASSERT(MyMoneyMoney(rows[3]["value"])==MyMoneyMoney(-24000.00));
+ CPPUNIT_ASSERT(MyMoneyMoney(rows[4]["value"])==MyMoneyMoney(-20000.00));
+ CPPUNIT_ASSERT(MyMoneyMoney(rows[5]["value"])==MyMoneyMoney( 5000.00));
+ CPPUNIT_ASSERT(MyMoneyMoney(rows[6]["value"])==MyMoneyMoney( 4000.00));
+ // need to fix these... fundamentally different from the original test
+ //CPPUNIT_ASSERT(MyMoneyMoney(invtran.m_rows[8]["value"])==MyMoneyMoney( -1000.00));
+ //CPPUNIT_ASSERT(MyMoneyMoney(invtran.m_rows[11]["value"])==MyMoneyMoney( -1200.00));
+ //CPPUNIT_ASSERT(MyMoneyMoney(invtran.m_rows[14]["value"])==MyMoneyMoney( -1100.00));
+
+ CPPUNIT_ASSERT(MyMoneyMoney(rows[1]["price"])==MyMoneyMoney(100.00));
+ CPPUNIT_ASSERT(MyMoneyMoney(rows[3]["price"])==MyMoneyMoney(120.00));
+ CPPUNIT_ASSERT(MyMoneyMoney(rows[5]["price"])==MyMoneyMoney(100.00));
+ CPPUNIT_ASSERT(MyMoneyMoney(rows[7]["price"])==MyMoneyMoney(100.00));
+ CPPUNIT_ASSERT(MyMoneyMoney(rows[10]["price"])==MyMoneyMoney(120.00));
+
+ CPPUNIT_ASSERT(MyMoneyMoney(rows[2]["shares"])==MyMoneyMoney(1000.00));
+ CPPUNIT_ASSERT(MyMoneyMoney(rows[4]["shares"])==MyMoneyMoney(-200.00));
+ CPPUNIT_ASSERT(MyMoneyMoney(rows[6]["shares"])==MyMoneyMoney( 50.00));
+ CPPUNIT_ASSERT(MyMoneyMoney(rows[8]["shares"])==MyMoneyMoney( 0.00));
+ CPPUNIT_ASSERT(MyMoneyMoney(rows[11]["shares"])==MyMoneyMoney( 0.00));
+
+ CPPUNIT_ASSERT(rows[1]["action"]=="Buy");
+ CPPUNIT_ASSERT(rows[3]["action"]=="Sell");
+ CPPUNIT_ASSERT(rows[5]["action"]=="Reinvest");
+ CPPUNIT_ASSERT(rows[7]["action"]=="Dividend");
+ CPPUNIT_ASSERT(rows[13]["action"]=="Yield");
+#else
+ CPPUNIT_ASSERT(rows.count()==9);
+ CPPUNIT_ASSERT(MyMoneyMoney(rows[0]["value"])==MyMoneyMoney(100000.00));
+ CPPUNIT_ASSERT(MyMoneyMoney(rows[1]["value"])==MyMoneyMoney(110000.00));
+ CPPUNIT_ASSERT(MyMoneyMoney(rows[2]["value"])==MyMoneyMoney(-24000.00));
+ CPPUNIT_ASSERT(MyMoneyMoney(rows[3]["value"])==MyMoneyMoney(-20000.00));
+ CPPUNIT_ASSERT(MyMoneyMoney(rows[4]["value"])==MyMoneyMoney( 5000.00));
+ CPPUNIT_ASSERT(MyMoneyMoney(rows[5]["value"])==MyMoneyMoney( 4000.00));
+ CPPUNIT_ASSERT(MyMoneyMoney(rows[6]["value"])==MyMoneyMoney( -1000.00));
+ CPPUNIT_ASSERT(MyMoneyMoney(rows[7]["value"])==MyMoneyMoney( -1200.00));
+ CPPUNIT_ASSERT(MyMoneyMoney(rows[8]["value"])==MyMoneyMoney( -1100.00));
+
+ CPPUNIT_ASSERT(MyMoneyMoney(rows[0]["price"])==MyMoneyMoney(100.00));
+ CPPUNIT_ASSERT(MyMoneyMoney(rows[2]["price"])==MyMoneyMoney(120.00));
+ CPPUNIT_ASSERT(MyMoneyMoney(rows[4]["price"])==MyMoneyMoney(100.00));
+ CPPUNIT_ASSERT(MyMoneyMoney(rows[6]["price"])==MyMoneyMoney( 0.00));
+ CPPUNIT_ASSERT(MyMoneyMoney(rows[8]["price"])==MyMoneyMoney( 0.00));
+
+ CPPUNIT_ASSERT(MyMoneyMoney(rows[1]["shares"])==MyMoneyMoney(1000.00));
+ CPPUNIT_ASSERT(MyMoneyMoney(rows[3]["shares"])==MyMoneyMoney(-200.00));
+ CPPUNIT_ASSERT(MyMoneyMoney(rows[5]["shares"])==MyMoneyMoney( 50.00));
+ CPPUNIT_ASSERT(MyMoneyMoney(rows[7]["shares"])==MyMoneyMoney( 0.00));
+ CPPUNIT_ASSERT(MyMoneyMoney(rows[8]["shares"])==MyMoneyMoney( 0.00));
+
+ CPPUNIT_ASSERT(rows[0]["action"]=="Buy");
+ CPPUNIT_ASSERT(rows[2]["action"]=="Sell");
+ CPPUNIT_ASSERT(rows[4]["action"]=="Reinvest");
+ CPPUNIT_ASSERT(rows[6]["action"]=="Dividend");
+ CPPUNIT_ASSERT(rows[8]["action"]=="Yield");
+#endif
+
+ QString html = invtran.renderHTML();
+#if 1
+ // i think this is the correct amount. different treatment of dividend and yield
+ CPPUNIT_ASSERT( searchHTML(html,i18n("Total Stock 1")) == MyMoneyMoney(175000.00) );
+ CPPUNIT_ASSERT( searchHTML(html,i18n("Grand Total")) == MyMoneyMoney(175000.00) );
+#else
+ CPPUNIT_ASSERT( searchHTML(html,i18n("Total Stock 1")) == MyMoneyMoney(171700.00) );
+ CPPUNIT_ASSERT( searchHTML(html,i18n("Grand Total")) == MyMoneyMoney(171700.00) );
+#endif
+
+ //
+ // Investment Performance Report
+ //
+
+ MyMoneyReport invhold_r(
+ MyMoneyReport::eAccountByTopAccount,
+ MyMoneyReport::eQCperformance,
+ MyMoneyTransactionFilter::userDefined,
+ MyMoneyReport::eDetailAll,
+ i18n("Investment Performance by Account"),
+ i18n("Test Report")
+ );
+ invhold_r.setDateFilter(QDate(2004,1,1),QDate(2004,10,1));
+ invhold_r.setInvestmentsOnly(true);
+ XMLandback(invhold_r);
+ QueryTable invhold(invhold_r);
+
+ writeTabletoHTML(invhold,"Investment Performance by Account.html");
+
+ rows = invhold.rows();
+
+ CPPUNIT_ASSERT(rows.count()==2);
+ CPPUNIT_ASSERT(MyMoneyMoney(rows[0]["return"])==MyMoneyMoney("669/10000"));
+ CPPUNIT_ASSERT(MyMoneyMoney(rows[0]["buys"])==MyMoneyMoney(210000.00));
+ CPPUNIT_ASSERT(MyMoneyMoney(rows[0]["sells"])==MyMoneyMoney(-44000.00));
+ CPPUNIT_ASSERT(MyMoneyMoney(rows[0]["reinvestincome"])==MyMoneyMoney(9000.00));
+ CPPUNIT_ASSERT(MyMoneyMoney(rows[0]["cashincome"])==MyMoneyMoney(3300.00));
+ CPPUNIT_ASSERT(MyMoneyMoney(rows[0]["shares"])==MyMoneyMoney(1700.00));
+ CPPUNIT_ASSERT(MyMoneyMoney(rows[0]["price"])==MyMoneyMoney(100.00));
+ CPPUNIT_ASSERT(MyMoneyMoney(rows[1]["return"]).isZero());
+
+ html = invhold.renderHTML();
+ CPPUNIT_ASSERT( searchHTML(html,i18n("Grand Total")) == MyMoneyMoney(170000.00) );
+
+#if 0
+ // Dump file & reports
+ QFile g( "investmentkmy.xml" );
+ g.open( IO_WriteOnly );
+ MyMoneyStorageXML xml;
+ IMyMoneyStorageFormat& interface = xml;
+ interface.writeFile(&g, dynamic_cast<IMyMoneySerialize*> (MyMoneyFile::instance()->storage()));
+ g.close();
+
+ invtran.dump("invtran.html","<html><head></head><body>%1</body></html>");
+ invhold.dump("invhold.html","<html><head></head><body>%1</body></html>");
+#endif
+
+ }
+ catch(MyMoneyException *e)
+ {
+ CPPUNIT_FAIL(e->what());
+ delete e;
+ }
+}
+ //this is to prevent me from making mistakes again when modifying balances - asoliverez
+ //this case tests only the opening and ending balance of the accounts
+ void QueryTableTest::testBalanceColumn()
+ {
+ try
+ {
+ TransactionHelper t1q1( QDate(2004,1,1), MyMoneySplit::ActionWithdrawal, moSolo, acChecking, acSolo );
+ TransactionHelper t2q1( QDate(2004,2,1), MyMoneySplit::ActionWithdrawal, moParent1, acCredit, acParent );
+ TransactionHelper t3q1( QDate(2004,3,1), MyMoneySplit::ActionWithdrawal, moParent2, acCredit, acParent );
+ TransactionHelper t4y1( QDate(2004,11,7), MyMoneySplit::ActionWithdrawal, moChild, acCredit, acChild );
+
+ TransactionHelper t1q2( QDate(2004,4,1), MyMoneySplit::ActionWithdrawal, moSolo, acChecking, acSolo );
+ TransactionHelper t2q2( QDate(2004,5,1), MyMoneySplit::ActionWithdrawal, moParent1, acCredit, acParent );
+ TransactionHelper t3q2( QDate(2004,6,1), MyMoneySplit::ActionWithdrawal, moParent2, acCredit, acParent );
+ TransactionHelper t4q2( QDate(2004,11,7), MyMoneySplit::ActionWithdrawal, moChild, acCredit, acChild );
+
+ TransactionHelper t1y2( QDate(2005,1,1), MyMoneySplit::ActionWithdrawal, moSolo, acChecking, acSolo );
+ TransactionHelper t2y2( QDate(2005,5,1), MyMoneySplit::ActionWithdrawal, moParent1, acCredit, acParent );
+ TransactionHelper t3y2( QDate(2005,9,1), MyMoneySplit::ActionWithdrawal, moParent2, acCredit, acParent );
+ TransactionHelper t4y2( QDate(2004,11,7), MyMoneySplit::ActionWithdrawal, moChild, acCredit, acChild );
+
+ unsigned cols;
+
+ MyMoneyReport filter;
+
+ filter.setRowType( MyMoneyReport::eAccount );
+ filter.setName("Transactions by Account");
+ cols = MyMoneyReport::eQCnumber | MyMoneyReport::eQCpayee | MyMoneyReport::eQCcategory | MyMoneyReport::eQCbalance;
+ filter.setQueryColumns( static_cast<MyMoneyReport::EQueryColumns>(cols) ); //
+ XMLandback(filter);
+ QueryTable qtbl_3(filter);
+
+ writeTabletoHTML(qtbl_3,"Transactions by Account.html");
+
+ QString html = qtbl_3.renderHTML();
+
+ QValueList<ListTable::TableRow> rows = qtbl_3.rows();
+
+ CPPUNIT_ASSERT(rows.count() == 16);
+
+ //this is to make sure that the dates of closing and opening balances and the balance numbers are ok
+ QString openingDate = KGlobal::locale()->formatDate(QDate(2004,1,1), true);
+ QString closingDate = KGlobal::locale()->formatDate(QDate(2005,9,1), true);
+ CPPUNIT_ASSERT( html.find(openingDate + "</td><td class=\"left\"></td><td class=\"left\">"+i18n("Opening Balance")) > 0);
+ CPPUNIT_ASSERT( html.find(closingDate + "</td><td class=\"left\"></td><td class=\"left\">"+i18n("Closing Balance")+"</td><td class=\"left\"></td><td class=\"value\"></td><td>&nbsp;-702.36</td></tr>") > 0);
+ CPPUNIT_ASSERT( html.find(closingDate + "</td><td class=\"left\"></td><td class=\"left\">"+i18n("Closing Balance")+"</td><td class=\"left\"></td><td class=\"value\"></td><td>&nbsp;-705.69</td></tr>") > 0);
+
+ }
+ catch(MyMoneyException *e)
+ {
+ CPPUNIT_FAIL(e->what());
+ delete e;
+ }
+
+ }
+
+void QueryTableTest::testTaxReport()
+{
+ try {
+ TransactionHelper t1q1( QDate(2004,1,1), MyMoneySplit::ActionWithdrawal, moSolo, acChecking, acSolo );
+ TransactionHelper t2q1( QDate(2004,2,1), MyMoneySplit::ActionWithdrawal, moParent1, acChecking, acTax );
+
+ unsigned cols;
+ MyMoneyReport filter;
+
+ filter.setRowType( MyMoneyReport::eCategory );
+ filter.setName("Tax Transactions");
+ cols = MyMoneyReport::eQCnumber | MyMoneyReport::eQCpayee | MyMoneyReport::eQCaccount;
+ filter.setQueryColumns( static_cast<MyMoneyReport::EQueryColumns>(cols) );
+ filter.setTax(true);
+
+ XMLandback(filter);
+ QueryTable qtbl_3(filter);
+
+ writeTabletoHTML(qtbl_3,"Tax Transactions.html");
+
+ QValueList<ListTable::TableRow> rows = qtbl_3.rows();
+
+ QString html = qtbl_3.renderHTML();
+ CPPUNIT_ASSERT(rows.count() == 1);
+ } catch(MyMoneyException *e) {
+ CPPUNIT_FAIL(e->what());
+ delete e;
+ }
+}
+
+// vim:cin:si:ai:et:ts=2:sw=2:
diff --git a/kmymoney2/reports/querytabletest.h b/kmymoney2/reports/querytabletest.h
new file mode 100644
index 0000000..36f3075
--- /dev/null
+++ b/kmymoney2/reports/querytabletest.h
@@ -0,0 +1,53 @@
+/***************************************************************************
+ querytabletest.h
+ -------------------
+ copyright : (C) 2002 by Thomas Baumgart
+ email : ipwizard@users.sourceforge.net
+ Ace Jones <ace.jones@hotpop.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. *
+ * *
+ ***************************************************************************/
+
+#ifndef QUERYTABLETEST_H
+#define QUERYTABLETEST_H
+
+#include <cppunit/extensions/HelperMacros.h>
+#include "../mymoney/mymoneyfile.h"
+#include "../mymoney/storage/mymoneyseqaccessmgr.h"
+
+class QueryTableTest : public CppUnit::TestFixture {
+ CPPUNIT_TEST_SUITE(QueryTableTest);
+ CPPUNIT_TEST(testQueryBasics);
+ CPPUNIT_TEST(testCashFlowAnalysis);
+ CPPUNIT_TEST(testAccountQuery);
+ CPPUNIT_TEST(testInvestment);
+ CPPUNIT_TEST(testBalanceColumn);
+ CPPUNIT_TEST(testTaxReport);
+ CPPUNIT_TEST_SUITE_END();
+
+private:
+ MyMoneyAccount *m;
+
+ MyMoneySeqAccessMgr* storage;
+ MyMoneyFile* file;
+
+public:
+ QueryTableTest();
+ void setUp ();
+ void tearDown ();
+ void testQueryBasics();
+ void testCashFlowAnalysis();
+ void testAccountQuery();
+ void testInvestment();
+ void testBalanceColumn();
+ void testTaxReport();
+};
+
+#endif
diff --git a/kmymoney2/reports/reportaccount.cpp b/kmymoney2/reports/reportaccount.cpp
new file mode 100644
index 0000000..5f7e7f8
--- /dev/null
+++ b/kmymoney2/reports/reportaccount.cpp
@@ -0,0 +1,355 @@
+/***************************************************************************
+ reportaccount.cpp
+ -------------------
+ begin : Mon May 17 2004
+ copyright : (C) 2004-2005 by Ace Jones
+ email : <ace.j@hotpop.com>
+ Thomas Baumgart <ipwizard@users.sourceforge.net>
+ ***************************************************************************/
+
+/***************************************************************************
+ * *
+ * 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
+
+// ----------------------------------------------------------------------------
+// KDE Includes
+// This is just needed for i18n(). 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 <klocale.h>
+
+// ----------------------------------------------------------------------------
+// Project Includes
+
+#include "../mymoney/mymoneyfile.h"
+#include "../mymoney/mymoneysecurity.h"
+#include "reportdebug.h"
+#include "reportaccount.h"
+
+namespace reports {
+
+ReportAccount::ReportAccount( void )
+{
+}
+
+ReportAccount::ReportAccount( const ReportAccount& copy ):
+ MyMoneyAccount( copy ), m_nameHierarchy( copy.m_nameHierarchy )
+{
+ // NOTE: I implemented the copy constructor solely for debugging reasons
+
+ DEBUG_ENTER(__PRETTY_FUNCTION__);
+}
+
+ReportAccount::ReportAccount( const QString& accountid ):
+ MyMoneyAccount( MyMoneyFile::instance()->account(accountid) )
+{
+ DEBUG_ENTER(__PRETTY_FUNCTION__);
+ DEBUG_OUTPUT(QString("Account %1").arg(accountid));
+ calculateAccountHierarchy();
+}
+
+ReportAccount::ReportAccount( const MyMoneyAccount& account ):
+ MyMoneyAccount( account )
+{
+ DEBUG_ENTER(__PRETTY_FUNCTION__);
+ DEBUG_OUTPUT(QString("Account %1").arg(account.id()));
+ calculateAccountHierarchy();
+}
+
+void ReportAccount::calculateAccountHierarchy( void )
+{
+ DEBUG_ENTER(__PRETTY_FUNCTION__);
+
+ MyMoneyFile* file = MyMoneyFile::instance();
+ QString resultid = id();
+ QString parentid = parentAccountId();
+
+#ifdef DEBUG_HIDE_SENSITIVE
+ m_nameHierarchy.prepend(file->account(resultid).id());
+#else
+ m_nameHierarchy.prepend(file->account(resultid).name());
+#endif
+ while (!file->isStandardAccount(parentid))
+ {
+ // take on the identity of our parent
+ resultid = parentid;
+
+ // and try again
+ parentid = file->account(resultid).parentAccountId();
+#ifdef DEBUG_HIDE_SENSITIVE
+ m_nameHierarchy.prepend(file->account(resultid).id());
+#else
+ m_nameHierarchy.prepend(file->account(resultid).name());
+#endif
+ }
+}
+
+MyMoneyMoney ReportAccount::deepCurrencyPrice( const QDate& date ) const
+{
+ DEBUG_ENTER(__PRETTY_FUNCTION__);
+
+ MyMoneyMoney result(1, 1);
+ MyMoneyFile* file = MyMoneyFile::instance();
+
+ MyMoneySecurity undersecurity = file->security( currencyId() );
+ if ( ! undersecurity.isCurrency() )
+ {
+ MyMoneyPrice price = file->price(undersecurity.id(),undersecurity.tradingCurrency(),date);
+ if ( price.isValid() )
+ {
+ result = price.rate(undersecurity.tradingCurrency());
+
+ DEBUG_OUTPUT(QString("Converting under %1 to deep %2, price on %3 is %4")
+ .arg(undersecurity.name())
+ .arg(file->security(undersecurity.tradingCurrency()).name())
+ .arg(date.toString())
+ .arg(result.toDouble()));
+ }
+ else
+ {
+ DEBUG_OUTPUT(QString("No price to convert under %1 to deep %2 on %3")
+ .arg(undersecurity.name())
+ .arg(file->security(undersecurity.tradingCurrency()).name())
+ .arg(date.toString()));
+ }
+ }
+
+ return result;
+}
+
+MyMoneyMoney ReportAccount::baseCurrencyPrice( const QDate& date ) const
+{
+ // Note that whether or not the user chooses to convert to base currency, all the values
+ // for a given account/category are converted to the currency for THAT account/category
+ // The "Convert to base currency" tells the report to convert from the account/category
+ // currency to the file's base currency.
+ //
+ // An example where this matters is if Category 'C' and account 'U' are in USD, but
+ // Account 'J' is in JPY. Say there are two transactions, one is US$100 from U to C,
+ // the other is JPY10,000 from J to C. Given a JPY price of USD$0.01, this means
+ // C will show a balance of $200 NO MATTER WHAT the user chooses for 'convert to base
+ // currency. This confused me for a while, which is why I wrote this comment.
+ // --acejones
+
+ DEBUG_ENTER(__PRETTY_FUNCTION__);
+
+ MyMoneyMoney result(1, 1);
+ MyMoneyFile* file = MyMoneyFile::instance();
+
+ if(isForeignCurrency())
+ {
+ result = foreignCurrencyPrice(file->baseCurrency().id(), date);
+ }
+
+ return result;
+}
+
+MyMoneyMoney ReportAccount::foreignCurrencyPrice( const QString foreignCurrency, const QDate& date ) const
+{
+ DEBUG_ENTER(__PRETTY_FUNCTION__);
+
+ MyMoneyPrice price;
+ MyMoneyMoney result(1, 1);
+ MyMoneyFile* file = MyMoneyFile::instance();
+ MyMoneySecurity security = file->security(foreignCurrency);
+
+ //check whether it is a currency or a commodity. In the latter case case, get the trading currency
+ QString tradingCurrency;
+ if(security.isCurrency()) {
+ tradingCurrency = foreignCurrency;
+ } else {
+ tradingCurrency = security.tradingCurrency();
+ }
+
+ //It makes no sense to get the price if both currencies are the same
+ if(currency().id() != tradingCurrency) {
+ price = file->price(currency().id(), tradingCurrency, date);
+
+ if(price.isValid())
+ {
+ result = price.rate(tradingCurrency);
+ DEBUG_OUTPUT(QString("Converting deep %1 to currency %2, price on %3 is %4")
+ .arg(file->currency(currency().id()).name())
+ .arg(file->currency(foreignCurrency).name())
+ .arg(date.toString())
+ .arg(result.toDouble()));
+ }
+ else
+ {
+ DEBUG_OUTPUT(QString("No price to convert deep %1 to currency %2 on %3")
+ .arg(file->currency(currency().id()).name())
+ .arg(file->currency(foreignCurrency).name())
+ .arg(date.toString()));
+ }
+ }
+ return result;
+}
+
+/**
+ * Fetch the trading currency of this account's currency
+ *
+ * @return The account's currency trading currency
+ */
+MyMoneySecurity ReportAccount::currency( void ) const
+{
+ MyMoneyFile* file = MyMoneyFile::instance();
+
+ // First, get the deep currency
+ MyMoneySecurity deepcurrency = file->security( currencyId() );
+ if ( ! deepcurrency.isCurrency() )
+ deepcurrency = file->security( deepcurrency.tradingCurrency() );
+
+ // Return the deep currency's ID
+ return deepcurrency;
+}
+
+/**
+ * Determine if this account's deep currency is different from the file's
+ * base currency
+ *
+ * @return bool True if this account is in a foreign currency
+ */
+bool ReportAccount::isForeignCurrency( void ) const
+{
+ return ( currency().id() != MyMoneyFile::instance()->baseCurrency().id() );
+}
+
+bool ReportAccount::operator<(const ReportAccount& second) const
+{
+// DEBUG_ENTER(__PRETTY_FUNCTION__);
+
+ bool result = false;
+ bool haveresult = false;
+ QStringList::const_iterator it_first = m_nameHierarchy.begin();
+ QStringList::const_iterator it_second = second.m_nameHierarchy.begin();
+ while ( it_first != m_nameHierarchy.end() )
+ {
+ // The first string is longer than the second, but otherwise identical
+ if ( it_second == second.m_nameHierarchy.end() )
+ {
+ result = false;
+ haveresult = true;
+ break;
+ }
+
+ if ( (*it_first) < (*it_second) )
+ {
+ result = true;
+ haveresult = true;
+ break;
+ }
+ else if ( (*it_first) > (*it_second) )
+ {
+ result = false;
+ haveresult = true;
+ break;
+ }
+
+ ++it_first;
+ ++it_second;
+ }
+
+ // The second string is longer than the first, but otherwise identical
+ if ( !haveresult && ( it_second != second.m_nameHierarchy.end() ) )
+ result = true;
+
+// DEBUG_OUTPUT(QString("%1 < %2 is %3").arg(debugName(),second.debugName()).arg(result));
+ return result;
+}
+
+/**
+ * The name of only this account. No matter how deep the hierarchy, this
+ * method only returns the last name in the list, which is the engine name]
+ * of this account.
+ *
+ * @return QString The account's name
+ */
+QString ReportAccount::name( void ) const
+{
+ return m_nameHierarchy.back();
+}
+
+// MyMoneyAccount:fullHierarchyDebug()
+QString ReportAccount::debugName( void ) const
+{
+ return m_nameHierarchy.join("|");
+}
+
+// MyMoneyAccount:fullHierarchy()
+QString ReportAccount::fullName( void ) const
+{
+ return m_nameHierarchy.join(": ");
+}
+
+// MyMoneyAccount:isTopCategory()
+bool ReportAccount::isTopLevel( void ) const
+{
+ return ( m_nameHierarchy.size() == 1 );
+}
+
+// MyMoneyAccount:hierarchyDepth()
+unsigned ReportAccount::hierarchyDepth( void ) const
+{
+ return ( m_nameHierarchy.size() );
+}
+
+ReportAccount ReportAccount::parent( void ) const
+{
+ return ReportAccount( parentAccountId() );
+}
+
+ReportAccount ReportAccount::topParent( void ) const
+{
+ DEBUG_ENTER(__PRETTY_FUNCTION__);
+
+ MyMoneyFile* file = MyMoneyFile::instance();
+ QString resultid = id();
+ QString parentid = parentAccountId();
+
+ while (!file->isStandardAccount(parentid))
+ {
+ // take on the identity of our parent
+ resultid = parentid;
+
+ // and try again
+ parentid = file->account(resultid).parentAccountId();
+ }
+
+ return ReportAccount( resultid );
+}
+
+QString ReportAccount::topParentName( void ) const
+{
+ return m_nameHierarchy.first();
+}
+
+bool ReportAccount::isLiquidAsset( void ) const
+{
+ return accountType() == MyMoneyAccount::Cash ||
+ accountType() == MyMoneyAccount::Checkings ||
+ accountType() == MyMoneyAccount::Savings;
+}
+
+
+bool ReportAccount::isLiquidLiability( void ) const
+{
+ return accountType() == MyMoneyAccount::CreditCard;
+
+}
+
+
+
+
+} // end namespace reports
diff --git a/kmymoney2/reports/reportaccount.h b/kmymoney2/reports/reportaccount.h
new file mode 100644
index 0000000..e07f9b1
--- /dev/null
+++ b/kmymoney2/reports/reportaccount.h
@@ -0,0 +1,238 @@
+/***************************************************************************
+ reportaccount.h
+ -------------------
+ begin : Sat May 22 2004
+ copyright : (C) 2004-2005 by Ace Jones
+ email : <ace.j@hotpop.com>
+ Thomas Baumgart <ipwizard@users.sourceforge.net>
+ ***************************************************************************/
+
+/***************************************************************************
+ * *
+ * This program is free software; you can redistribute it and/or modify *
+ * it under the terms of the GNU General Public License as published by *
+ * the Free Software Foundation; either version 2 of the License, or *
+ * (at your option) any later version. *
+ * *
+ ***************************************************************************/
+
+#ifndef REPORTACCOUNT_H
+#define REPORTACCOUNT_H
+
+// ----------------------------------------------------------------------------
+// QT Includes
+
+#include <qstringlist.h>
+
+// ----------------------------------------------------------------------------
+// KDE Includes
+
+// ----------------------------------------------------------------------------
+// Project Includes
+#include "../mymoney/mymoneyaccount.h"
+
+namespace reports {
+
+/**
+ * This is a MyMoneyAccount as viewed from the reporting engine.
+ *
+ * All reporting methods should use ReportAccount INSTEAD OF
+ * MyMoneyAccount at all times.
+ *
+ * The primary functionality this provides is a full chain of account
+ * hierarchy that is easy to traverse. It's needed because the PivotTable
+ * grid needs to store and sort by the full account hierarchy, while still
+ * having access to the account itself for currency conversion.
+ *
+ * In addition, several other convenience functions are provided that may
+ * be worth moving into MyMoneyAccount at some point.
+ *
+ * @author Ace Jones
+ *
+ * @short
+**/
+class ReportAccount: public MyMoneyAccount
+{
+private:
+ QStringList m_nameHierarchy;
+
+public:
+ /**
+ * Default constructor
+ *
+ * Needed to allow this object to be stored in a QMap.
+ */
+ ReportAccount( void );
+
+ /**
+ * Copy constructor
+ *
+ * Needed to allow this object to be stored in a QMap.
+ */
+ ReportAccount( const ReportAccount& );
+
+ /**
+ * Regular constructor
+ *
+ * @param accountid Account which this account descriptor should be based off of
+ */
+ ReportAccount( const QString& accountid );
+
+ /**
+ * Regular constructor
+ *
+ * @param accountid Account which this account descriptor should be based off of
+ */
+ ReportAccount( const MyMoneyAccount& accountid );
+
+ /**
+ * @param right The object to compare against
+ * @return bool True if this account's fully-qualified hierarchy name
+ * is less than that of the given qccount
+ */
+ bool operator<( const ReportAccount& right ) const;
+
+ /**
+ * Returns the price of this account's underlying currency on the indicated date,
+ * translated into the account's deep currency
+ *
+ * There are three different currencies in play with a single Account:
+ * - The underlying currency: What currency the account itself is denominated in
+ * - The deep currency: The underlying currency's own underlying currency. This
+ * is only a factor if the underlying currency of this account IS NOT a
+ * currency itself, but is some other kind of security. In that case, the
+ * underlying security has its own currency. The deep currency is the
+ * currency of the underlying security. On the other hand, if the account
+ * has a currency itself, then the deep currency == the underlying currency,
+ * and this function will return 1.0.
+ * - The base currency: The base currency of the user's overall file
+ *
+ * @param date The date in question
+ * @return MyMoneyMoney The value of the account's currency on that date
+ */
+ MyMoneyMoney deepCurrencyPrice( const QDate& date ) const;
+
+ /**
+ * Returns the price of this account's deep currency on the indicated date,
+ * translated into the base currency
+ *
+ * @param date The date in question
+ * @return MyMoneyMoney The value of the account's currency on that date
+ */
+ MyMoneyMoney baseCurrencyPrice( const QDate& date ) const;
+
+ /**
+ * Returns the price of this account's deep currency on the indicated date,
+ * translated into the base currency
+ *
+ * @param foreignCurrency The currency on which the price will be returned
+ * @param date The date in question
+ * @return MyMoneyMoney The value of the account's currency on that date
+ */
+ MyMoneyMoney foreignCurrencyPrice( const QString foreignCurrency, const QDate& date ) const;
+
+ /**
+ * Fetch the trading symbol of this account's deep currency
+ *
+ * @return The account's currency trading currency object
+ */
+ MyMoneySecurity currency( void ) const;
+
+ /**
+ * Determine if this account's deep currency is different from the file's
+ * base currency
+ *
+ * @return bool True if this account is in a foreign currency
+ */
+ bool isForeignCurrency( void ) const;
+
+ /**
+ * The name of only this account. No matter how deep the hierarchy, this
+ * method only returns the last name in the list, which is the engine name]
+ * of this account.
+ *
+ * @return QString The account's name
+ */
+ QString name( void ) const;
+
+ /**
+ * The entire hierarchy of this account descriptor
+ * This is similiar to debugName(), however debugName() is not guaranteed
+ * to always look pretty, while fullName() is. So if the user is ever
+ * going to see the results, use fullName().
+ *
+ * @return QString The account's full hierarchy
+ */
+ QString fullName( void ) const;
+
+ /**
+ * The entire hierarchy of this account descriptor, suitable for displaying
+ * in debugging output
+ *
+ * @return QString The account's full hierarchy (suitable for debugging)
+ */
+ QString debugName( void ) const;
+
+ /**
+ * Whether this account is a 'top level' parent account. This means that
+ * it's parent is an account class, like asset, liability, expense or income
+ *
+ * @return bool True if this account is a top level parent account
+ */
+ /*inline*/ bool isTopLevel( void ) const;
+
+ /**
+ * Returns the name of the top level parent account
+ *
+ * (See isTopLevel for a definition of 'top level parent')
+ *
+ * @return QString The name of the top level parent account
+ */
+ /*inline*/ QString topParentName( void ) const;
+
+ /**
+ * Returns a report account containing the top parent account
+ *
+ * @return ReportAccount The account of the top parent
+ */
+ ReportAccount topParent( void ) const;
+
+ /**
+ * Returns a report account containing the immediate parent account
+ *
+ * @return ReportAccount The account of the immediate parent
+ */
+ ReportAccount parent( void ) const;
+
+ /**
+ * Returns the number of accounts in this account's hierarchy. If this is a
+ * Top Category, it returns 1. If it's parent is a Top Category, returns 2,
+ * etc.
+ *
+ * @return unsigned Hierarchy depth
+ */
+ unsigned hierarchyDepth( void ) const;
+
+ /**
+ * Returns whether this account is a liquid asset
+ *
+ */
+ bool isLiquidAsset( void ) const;
+
+ /**
+ * Returns whether this account is a liquid liability
+ *
+ */
+ bool isLiquidLiability( void ) const;
+
+protected:
+ /**
+ * Calculates the full account hierarchy of this account
+ */
+ void calculateAccountHierarchy( void );
+
+};
+
+} // end namespace reports
+
+#endif // REPORTACCOUNT_H
diff --git a/kmymoney2/reports/reportdebug.h b/kmymoney2/reports/reportdebug.h
new file mode 100644
index 0000000..3a95465
--- /dev/null
+++ b/kmymoney2/reports/reportdebug.h
@@ -0,0 +1,83 @@
+/***************************************************************************
+ reportdebug.h
+ -------------------
+ begin : Sat May 22 2004
+ copyright : (C) 2004-2005 by Ace Jones
+ email : <ace.j@hotpop.com>
+ Thomas Baumgart <ipwizard@users.sourceforge.net>
+ ***************************************************************************/
+
+/***************************************************************************
+ * *
+ * This program is free software; you can redistribute it and/or modify *
+ * it under the terms of the GNU General Public License as published by *
+ * the Free Software Foundation; either version 2 of the License, or *
+ * (at your option) any later version. *
+ * *
+ ***************************************************************************/
+
+#ifndef REPORTDEBUG_H
+#define REPORTDEBUG_H
+
+// ----------------------------------------------------------------------------
+// QT Includes
+
+// ----------------------------------------------------------------------------
+// KDE Includes
+
+// ----------------------------------------------------------------------------
+// Project Includes
+
+namespace reports {
+
+// define to enable massive debug logging to stderr
+#undef DEBUG_REPORTS
+// #define DEBUG_REPORTS
+
+#define DEBUG_ENABLED_BY_DEFAULT false
+
+#ifdef DEBUG_REPORTS
+
+// define to filter out account names & transaction amounts
+// DO NOT check into CVS with this defined!! It breaks all
+// unit tests.
+#undef DEBUG_HIDE_SENSITIVE
+
+#define DEBUG_ENTER(x) Debug ___DEBUG(x)
+#define DEBUG_OUTPUT(x) ___DEBUG.output(x)
+#define DEBUG_OUTPUT_IF(x,y) { if (x) ___DEBUG.output(y); }
+#define DEBUG_ENABLE(x) Debug::enable(x)
+#define DEBUG_ENABLE_KEY(x) Debug::setEnableKey(x)
+#ifdef DEBUG_HIDE_SENSITIVE
+#define DEBUG_SENSITIVE(x) QString("hidden")
+#else
+#define DEBUG_SENSITIVE(x) (x)
+#endif
+
+#else
+
+#define DEBUG_ENTER(x)
+#define DEBUG_OUTPUT(x)
+#define DEBUG_OUTPUT_IF(x,y)
+#define DEBUG_ENABLE(x)
+#define DEBUG_SENSITIVE(x)
+#endif
+
+class Debug
+{
+ QString m_methodName;
+ static QString m_sTabs;
+ static bool m_sEnabled;
+ bool m_enabled;
+ static QString m_sEnableKey;
+public:
+ Debug( const QString& _name );
+ ~Debug();
+ void output( const QString& _text );
+ static void enable( bool _e ) { m_sEnabled = _e; }
+ static void setEnableKey( const QString& _s ) { m_sEnableKey = _s; }
+};
+
+} // end namespace reports
+
+#endif // REPORTDEBUG_H
diff --git a/kmymoney2/reports/reportstestcommon.cpp b/kmymoney2/reports/reportstestcommon.cpp
new file mode 100644
index 0000000..31e6c1d
--- /dev/null
+++ b/kmymoney2/reports/reportstestcommon.cpp
@@ -0,0 +1,494 @@
+/***************************************************************************
+ reportstestcommon.cpp
+ -------------------
+ copyright : (C) 2002-2005 by Thomas Baumgart
+ email : ipwizard@users.sourceforge.net
+ Ace Jones <ace.j@hotpop.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. *
+ * *
+ ***************************************************************************/
+
+#include <qvaluelist.h>
+#include <qvaluevector.h>
+#include <qdom.h>
+#include <qfile.h>
+
+#include <kdebug.h>
+#include <kdeversion.h>
+#include <kglobal.h>
+#include <kglobalsettings.h>
+#include <klocale.h>
+#include <kstandarddirs.h>
+
+#include "kreportsviewtest.h"
+
+#define private public
+#include "pivottable.h"
+#include "querytable.h"
+#undef private
+using namespace reports;
+
+#include "../mymoney/mymoneysecurity.h"
+#include "../mymoney/mymoneyprice.h"
+#include "../mymoney/storage/mymoneystoragedump.h"
+#include "../mymoney/mymoneyreport.h"
+#include "../mymoney/mymoneystatement.h"
+#include "../mymoney/storage/mymoneystoragexml.h"
+#include "reportstestcommon.h"
+
+namespace test {
+
+const MyMoneyMoney moCheckingOpen(0.0);
+const MyMoneyMoney moCreditOpen(-0.0);
+const MyMoneyMoney moConverterCheckingOpen(1418.0);
+const MyMoneyMoney moConverterCreditOpen(-418.0);
+const MyMoneyMoney moZero(0.0);
+const MyMoneyMoney moSolo(234.12);
+const MyMoneyMoney moParent1(88.01);
+const MyMoneyMoney moParent2(133.22);
+const MyMoneyMoney moParent(moParent1+moParent2);
+const MyMoneyMoney moChild(14.00);
+const MyMoneyMoney moThomas(5.11);
+const MyMoneyMoney moNoPayee(8944.70);
+
+QString acAsset;
+QString acLiability;
+QString acExpense;
+QString acIncome;
+QString acChecking;
+QString acCredit;
+QString acSolo;
+QString acParent;
+QString acChild;
+QString acSecondChild;
+QString acGrandChild1;
+QString acGrandChild2;
+QString acForeign;
+QString acCanChecking;
+QString acJpyChecking;
+QString acCanCash;
+QString acJpyCash;
+QString inBank;
+QString eqStock1;
+QString eqStock2;
+QString acInvestment;
+QString acStock1;
+QString acStock2;
+QString acDividends;
+QString acInterest;
+QString acTax;
+QString acCash;
+
+TransactionHelper::TransactionHelper( const QDate& _date, const QString& _action, MyMoneyMoney _value, const QString& _accountid, const QString& _categoryid, const QString& _currencyid, const QString& _payee )
+{
+ // _currencyid is the currency of the transaction, and of the _value
+ // both the account and category can have their own currency (athough the category having
+ // a foreign currency is not yet supported by the program, the reports will still allow it,
+ // so it must be tested.)
+
+ MyMoneyFile* file = MyMoneyFile::instance();
+ bool haspayee = ! _payee.isEmpty();
+ MyMoneyPayee payeeTest = file->payeeByName(_payee);
+
+ MyMoneyFileTransaction ft;
+ setPostDate(_date);
+
+ QString currencyid = _currencyid;
+ if ( currencyid.isEmpty() )
+ currencyid=MyMoneyFile::instance()->baseCurrency().id();
+ setCommodity(currencyid);
+
+ MyMoneyMoney price;
+ MyMoneySplit splitLeft;
+ if ( haspayee )
+ splitLeft.setPayeeId(payeeTest.id());
+ splitLeft.setAction(_action);
+ splitLeft.setValue(-_value);
+ price = MyMoneyFile::instance()->price(currencyid, file->account(_accountid).currencyId(),_date).rate(file->account(_accountid).currencyId());
+ splitLeft.setShares(-_value * price);
+ splitLeft.setAccountId(_accountid);
+ addSplit(splitLeft);
+
+ MyMoneySplit splitRight;
+ if ( haspayee )
+ splitRight.setPayeeId(payeeTest.id());
+ splitRight.setAction(_action);
+ splitRight.setValue(_value);
+ price = MyMoneyFile::instance()->price(currencyid, file->account(_categoryid).currencyId(),_date).rate(file->account(_categoryid).currencyId());
+ splitRight.setShares(_value * price );
+ splitRight.setAccountId(_categoryid);
+ addSplit(splitRight);
+
+ MyMoneyFile::instance()->addTransaction(*this);
+ ft.commit();
+}
+
+TransactionHelper::~TransactionHelper()
+{
+ MyMoneyFileTransaction ft;
+ MyMoneyFile::instance()->removeTransaction(*this);
+ ft.commit();
+}
+
+void TransactionHelper::update(void)
+{
+ MyMoneyFileTransaction ft;
+ MyMoneyFile::instance()->modifyTransaction(*this);
+ ft.commit();
+}
+
+InvTransactionHelper::InvTransactionHelper( const QDate& _date, const QString& _action, MyMoneyMoney _shares, MyMoneyMoney _price, const QString& _stockaccountid, const QString& _transferid, const QString& _categoryid )
+{
+ init(_date, _action, _shares, _price, _stockaccountid, _transferid, _categoryid);
+}
+
+void InvTransactionHelper::init( const QDate& _date, const QString& _action, MyMoneyMoney _shares, MyMoneyMoney _price, const QString& _stockaccountid, const QString& _transferid, const QString& _categoryid )
+{
+ MyMoneyFile* file = MyMoneyFile::instance();
+ MyMoneyAccount stockaccount = file->account(_stockaccountid);
+ MyMoneyMoney value = _shares * _price;
+
+ setPostDate(_date);
+
+ setCommodity("USD");
+ MyMoneySplit s1;
+ s1.setValue(value);
+ s1.setAccountId(_stockaccountid);
+
+ if ( _action == MyMoneySplit::ActionReinvestDividend )
+ {
+ s1.setShares(_shares);
+ s1.setAction(MyMoneySplit::ActionReinvestDividend);
+
+ MyMoneySplit s2;
+ s2.setAccountId(_categoryid);
+ s2.setShares(-value);
+ s2.setValue(-value);
+ addSplit(s2);
+ }
+ else if ( _action == MyMoneySplit::ActionDividend || _action == MyMoneySplit::ActionYield )
+ {
+ s1.setAccountId(_categoryid);
+ s1.setShares(-value);
+ s1.setValue(-value);
+
+ // Split 2 will be the zero-amount investment split that serves to
+ // mark this transaction as a cash dividend and note which stock account
+ // it belongs to.
+ MyMoneySplit s2;
+ s2.setValue(0);
+ s2.setShares(0);
+ s2.setAction(_action);
+ s2.setAccountId(_stockaccountid);
+ addSplit(s2);
+
+ MyMoneySplit s3;
+ s3.setAccountId(_transferid);
+ s3.setShares(value);
+ s3.setValue(value);
+ addSplit(s3);
+ }
+ else if ( _action == MyMoneySplit::ActionBuyShares )
+ {
+ s1.setShares(_shares);
+ s1.setAction(MyMoneySplit::ActionBuyShares);
+
+ MyMoneySplit s3;
+ s3.setAccountId(_transferid);
+ s3.setShares(-value);
+ s3.setValue(-value);
+ addSplit(s3);
+ }
+ addSplit(s1);
+
+ //kdDebug(2) << "created transaction, now adding..." << endl;
+
+ MyMoneyFileTransaction ft;
+ file->addTransaction(*this);
+
+ //kdDebug(2) << "updating price..." << endl;
+
+ // update the price, while we're here
+ QString stockid = stockaccount.currencyId();
+ QString basecurrencyid = file->baseCurrency().id();
+ MyMoneyPrice price = file->price( stockid, basecurrencyid, _date, true );
+ if ( !price.isValid() )
+ {
+ MyMoneyPrice newprice( stockid, basecurrencyid, _date, _price, "test" );
+ file->addPrice(newprice);
+ }
+ ft.commit();
+ //kdDebug(2) << "successfully added " << id() << endl;
+}
+
+QString makeAccount( const QString& _name, MyMoneyAccount::accountTypeE _type, MyMoneyMoney _balance, const QDate& _open, const QString& _parent, QString _currency, bool _taxReport )
+{
+ MyMoneyAccount info;
+ MyMoneyFileTransaction ft;
+
+ info.setName(_name);
+ info.setAccountType(_type);
+ info.setOpeningDate(_open);
+ if ( _currency != "" )
+ info.setCurrencyId(_currency);
+ else
+ info.setCurrencyId(MyMoneyFile::instance()->baseCurrency().id());
+
+ if(_taxReport)
+ info.setValue("Tax", "Yes");
+
+ MyMoneyAccount parent = MyMoneyFile::instance()->account(_parent);
+ MyMoneyFile::instance()->addAccount( info, parent );
+ // create the opening balance transaction if any
+ if(!_balance.isZero()) {
+ MyMoneySecurity sec = MyMoneyFile::instance()->currency(info.currencyId());
+ MyMoneyFile::instance()->openingBalanceAccount(sec);
+ MyMoneyFile::instance()->createOpeningBalanceTransaction(info, _balance);
+ }
+ ft.commit();
+
+ return info.id();
+}
+
+void makePrice(const QString& _currencyid, const QDate& _date, const MyMoneyMoney& _price )
+{
+ MyMoneyFileTransaction ft;
+ MyMoneyFile* file = MyMoneyFile::instance();
+ MyMoneySecurity curr = file->currency(_currencyid);
+ MyMoneyPrice price(_currencyid, file->baseCurrency().id(), _date, _price, "test");
+ file->addPrice(price);
+ ft.commit();
+}
+
+QString makeEquity(const QString& _name, const QString& _symbol )
+{
+ MyMoneySecurity equity;
+ MyMoneyFileTransaction ft;
+
+ equity.setName( _name );
+ equity.setTradingSymbol( _symbol );
+ equity.setSmallestAccountFraction( 1000 );
+ equity.setSecurityType( MyMoneySecurity::SECURITY_NONE /*MyMoneyEquity::ETYPE_STOCK*/ );
+ MyMoneyFile::instance()->addSecurity( equity );
+ ft.commit();
+
+ return equity.id();
+}
+
+void makeEquityPrice(const QString& _id, const QDate& _date, const MyMoneyMoney& _price )
+{
+ MyMoneyFile* file = MyMoneyFile::instance();
+ MyMoneyFileTransaction ft;
+ QString basecurrencyid = file->baseCurrency().id();
+ MyMoneyPrice price = file->price( _id, basecurrencyid, _date, true );
+ if ( !price.isValid() )
+ {
+ MyMoneyPrice newprice( _id, basecurrencyid, _date, _price, "test" );
+ file->addPrice(newprice);
+ }
+ ft.commit();
+}
+
+void writeRCFtoXMLDoc( const MyMoneyReport& filter, QDomDocument* doc )
+{
+ QDomProcessingInstruction instruct = doc->createProcessingInstruction(QString("xml"), QString("version=\"1.0\" encoding=\"utf-8\""));
+ doc->appendChild(instruct);
+
+ QDomElement root = doc->createElement("KMYMONEY-FILE");
+ doc->appendChild(root);
+
+ QDomElement reports = doc->createElement("REPORTS");
+ root.appendChild(reports);
+
+ QDomElement report = doc->createElement("REPORT");
+ filter.write(report,doc);
+ reports.appendChild(report);
+
+}
+
+void writeTabletoHTML( const PivotTable& table, const QString& _filename )
+{
+ static unsigned filenumber = 1;
+ QString filename = _filename;
+ if ( filename.isEmpty() )
+ {
+ filename = QString("report-%1%2.html").arg((filenumber<10)?"0":"").arg(filenumber);
+ ++filenumber;
+ }
+
+ QFile g( filename );
+ g.open( IO_WriteOnly );
+ QTextStream(&g) << table.renderHTML();
+ g.close();
+
+}
+
+void writeTabletoHTML( const QueryTable& table, const QString& _filename )
+{
+ static unsigned filenumber = 1;
+ QString filename = _filename;
+ if ( filename.isEmpty() )
+ {
+ filename = QString("report-%1%2.html").arg((filenumber<10)?"0":"").arg(filenumber);
+ ++filenumber;
+ }
+
+ QFile g( filename );
+ g.open( IO_WriteOnly );
+ QTextStream(&g) << table.renderHTML();
+ g.close();
+}
+
+void writeTabletoCSV( const PivotTable& table, const QString& _filename )
+{
+ static unsigned filenumber = 1;
+ QString filename = _filename;
+ if ( filename.isEmpty() )
+ {
+ filename = QString("report-%1%2.csv").arg((filenumber<10)?"0":"").arg(filenumber);
+ ++filenumber;
+ }
+
+ QFile g( filename );
+ g.open( IO_WriteOnly );
+ QTextStream(&g) << table.renderCSV();
+ g.close();
+
+}
+
+void writeTabletoCSV( const QueryTable& table, const QString& _filename )
+{
+ static unsigned filenumber = 1;
+ QString filename = _filename;
+ if ( filename.isEmpty() )
+ {
+ filename = QString("qreport-%1%2.csv").arg((filenumber<10)?"0":"").arg(filenumber);
+ ++filenumber;
+ }
+
+ QFile g( filename );
+ g.open( IO_WriteOnly );
+ QTextStream(&g) << table.renderCSV();
+ g.close();
+
+}
+
+void writeRCFtoXML( const MyMoneyReport& filter, const QString& _filename )
+{
+ static unsigned filenum = 1;
+ QString filename = _filename;
+ if ( filename.isEmpty() ) {
+ filename = QString("report-%1%2.xml").arg(QString::number(filenum).rightJustify(2, '0'));
+ ++filenum;
+ }
+
+ QDomDocument* doc = new QDomDocument("KMYMONEY-FILE");
+ Q_CHECK_PTR(doc);
+
+ writeRCFtoXMLDoc(filter,doc);
+
+ QFile g( filename );
+ g.open( IO_WriteOnly );
+
+ QTextStream stream(&g);
+#if KDE_IS_VERSION(3,2,0)
+ stream.setEncoding(QTextStream::UnicodeUTF8);
+ stream << doc->toString();
+#else
+ //stream.setEncoding(QTextStream::Locale);
+ QString temp = doc->toString();
+ stream << temp.data();
+#endif
+ g.close();
+
+ delete doc;
+}
+
+bool readRCFfromXMLDoc( QValueList<MyMoneyReport>& list, QDomDocument* doc )
+{
+ bool result = false;
+
+ QDomElement rootElement = doc->documentElement();
+ if(!rootElement.isNull())
+ {
+ QDomNode child = rootElement.firstChild();
+ while(!child.isNull() && child.isElement())
+ {
+ QDomElement childElement = child.toElement();
+ if("REPORTS" == childElement.tagName())
+ {
+ result = true;
+ QDomNode subchild = child.firstChild();
+ while(!subchild.isNull() && subchild.isElement())
+ {
+ MyMoneyReport filter;
+ if ( filter.read(subchild.toElement()))
+ {
+ list += filter;
+ }
+ subchild = subchild.nextSibling();
+ }
+ }
+ child = child.nextSibling();
+ }
+ }
+ return result;
+}
+
+bool readRCFfromXML( QValueList<MyMoneyReport>& list, const QString& filename )
+{
+ int result = false;
+ QFile f( filename );
+ f.open( IO_ReadOnly );
+ QDomDocument* doc = new QDomDocument;
+ if(doc->setContent(&f, FALSE))
+ {
+ result = readRCFfromXMLDoc(list,doc);
+ }
+ delete doc;
+
+ return result;
+
+}
+
+void XMLandback( MyMoneyReport& filter )
+{
+ // this function writes the filter to XML, and then reads
+ // it back from XML overwriting the original filter;
+ // in all cases, the result should be the same if the read
+ // & write methods are working correctly.
+
+ QDomDocument* doc = new QDomDocument("KMYMONEY-FILE");
+ Q_CHECK_PTR(doc);
+
+ writeRCFtoXMLDoc(filter,doc);
+ QValueList<MyMoneyReport> list;
+ if ( readRCFfromXMLDoc(list,doc) && list.count() > 0 )
+ filter = list[0];
+ else
+ throw new MYMONEYEXCEPTION("Failed to load report from XML");
+
+ delete doc;
+
+}
+
+MyMoneyMoney searchHTML(const QString& _html, const QString& _search)
+{
+ QRegExp re(QString("%1[<>/td]*([\\-.0-9,]*)").arg(_search));
+ re.search(_html);
+ QString found = re.cap(1);
+ found.remove(',');
+
+ return MyMoneyMoney(found.toDouble());
+}
+
+} // end namespace test
+
+// vim:cin:si:ai:et:ts=2:sw=2:
diff --git a/kmymoney2/reports/reportstestcommon.h b/kmymoney2/reports/reportstestcommon.h
new file mode 100644
index 0000000..6f4826e
--- /dev/null
+++ b/kmymoney2/reports/reportstestcommon.h
@@ -0,0 +1,133 @@
+/***************************************************************************
+ reportstestcommon.h
+ -------------------
+ copyright : (C) 2002-2005 by Thomas Baumgart
+ email : ipwizard@users.sourceforge.net
+ Ace Jones <ace.j@hotpop.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. *
+ * *
+ ***************************************************************************/
+
+#ifndef REPORTSTESTCOMMON_H
+#define REPORTSTESTCOMMON_H
+
+#include <qvaluelist.h>
+class QDomDocument;
+
+#include "../mymoney/mymoneyaccount.h"
+#include "../mymoney/mymoneytransaction.h"
+#include "../mymoney/mymoneymoney.h"
+class MyMoneyReport;
+
+namespace reports {
+class PivotTable;
+class QueryTable;
+}
+
+namespace test {
+
+extern const MyMoneyMoney moCheckingOpen;
+extern const MyMoneyMoney moCreditOpen;
+extern const MyMoneyMoney moConverterCheckingOpen;
+extern const MyMoneyMoney moConverterCreditOpen;
+extern const MyMoneyMoney moZero;
+extern const MyMoneyMoney moSolo;
+extern const MyMoneyMoney moParent1;
+extern const MyMoneyMoney moParent2;
+extern const MyMoneyMoney moParent;
+extern const MyMoneyMoney moChild;
+extern const MyMoneyMoney moThomas;
+extern const MyMoneyMoney moNoPayee;
+
+extern QString acAsset;
+extern QString acLiability;
+extern QString acExpense;
+extern QString acIncome;
+extern QString acChecking;
+extern QString acCredit;
+extern QString acSolo;
+extern QString acParent;
+extern QString acChild;
+extern QString acSecondChild;
+extern QString acGrandChild1;
+extern QString acGrandChild2;
+extern QString acForeign;
+extern QString acCanChecking;
+extern QString acJpyChecking;
+extern QString acCanCash;
+extern QString acJpyCash;
+extern QString inBank;
+extern QString eqStock1;
+extern QString eqStock2;
+extern QString acInvestment;
+extern QString acStock1;
+extern QString acStock2;
+extern QString acDividends;
+extern QString acInterest;
+extern QString acTax;
+extern QString acCash;
+
+class TransactionHelper: public MyMoneyTransaction
+{
+private:
+ QString m_id;
+public:
+ TransactionHelper( const QDate& _date, const QString& _action, MyMoneyMoney _value, const QString& _accountid, const QString& _categoryid, const QString& _currencyid = QString(), const QString& _payee="Test Payee" );
+ ~TransactionHelper();
+ void update(void);
+protected:
+ TransactionHelper(void) {}
+};
+
+class InvTransactionHelper: public TransactionHelper
+{
+public:
+ InvTransactionHelper( const QDate& _date, const QString& _action, MyMoneyMoney _shares, MyMoneyMoney _value, const QString& _stockaccountid, const QString& _transferid, const QString& _categoryid );
+ void init( const QDate& _date, const QString& _action, MyMoneyMoney _shares, MyMoneyMoney _value, const QString& _stockaccountid, const QString& _transferid, const QString& _categoryid );
+};
+
+class BudgetEntryHelper
+{
+private:
+ QDate m_date;
+ QString m_categoryid;
+ bool m_applytosub;
+ MyMoneyMoney m_amount;
+
+public:
+ BudgetEntryHelper( void ): m_applytosub(false) {}
+ BudgetEntryHelper( const QDate& _date, const QString& _categoryid, bool _applytosub, const MyMoneyMoney& _amount ): m_date(_date), m_categoryid(_categoryid), m_applytosub(_applytosub), m_amount(_amount) {}
+};
+
+class BudgetHelper: public QValueList<BudgetEntryHelper>
+{
+ MyMoneyMoney budgetAmount( const QDate& _date, const QString& _categoryid, bool& _applytosub );
+};
+
+extern QString makeAccount( const QString& _name, MyMoneyAccount::accountTypeE _type, MyMoneyMoney _balance, const QDate& _open, const QString& _parent, QString _currency="", bool _taxReport = false );
+extern void makePrice(const QString& _currencyid, const QDate& _date, const MyMoneyMoney& _price );
+QString makeEquity(const QString& _name, const QString& _symbol );
+extern void makeEquityPrice(const QString& _id, const QDate& _date, const MyMoneyMoney& _price );
+extern void writeRCFtoXMLDoc( const MyMoneyReport& filter, QDomDocument* doc );
+extern void writeTabletoHTML( const reports::PivotTable& table, const QString& _filename = QString() );
+extern void writeTabletoHTML( const reports::QueryTable& table, const QString& _filename = QString() );
+extern void writeTabletoCSV( const reports::PivotTable& table, const QString& _filename = QString() );
+extern void writeTabletoCSV( const reports::QueryTable& table, const QString& _filename = QString() );
+extern void writeRCFtoXML( const MyMoneyReport& filter, const QString& _filename = QString() );
+extern bool readRCFfromXMLDoc( QValueList<MyMoneyReport>& list, QDomDocument* doc );
+extern bool readRCFfromXML( QValueList<MyMoneyReport>& list, const QString& filename );
+extern void XMLandback( MyMoneyReport& filter );
+extern MyMoneyMoney searchHTML(const QString& _html, const QString& _search);
+
+} // end namespace test
+
+#endif // REPORTSTESTCOMMON_H
+
+// vim:cin:si:ai:et:ts=2:sw=2:
diff --git a/kmymoney2/reports/reporttable.h b/kmymoney2/reports/reporttable.h
new file mode 100644
index 0000000..3bab330
--- /dev/null
+++ b/kmymoney2/reports/reporttable.h
@@ -0,0 +1,54 @@
+/***************************************************************************
+ reporttable.h
+ -------------------
+ begin : Mon May 7 2007
+ copyright : (C) 2007 Thomas Baumgart
+ email : Thomas Baumgart <ipwizard@users.sourceforge.net>
+ ***************************************************************************/
+
+/***************************************************************************
+ * *
+ * This program is free software; you can redistribute it and/or modify *
+ * it under the terms of the GNU General Public License as published by *
+ * the Free Software Foundation; either version 2 of the License, or *
+ * (at your option) any later version. *
+ * *
+ ***************************************************************************/
+
+#ifndef REPORTTABLE_H
+#define REPORTTABLE_H
+
+// ----------------------------------------------------------------------------
+// QT Includes
+
+// ----------------------------------------------------------------------------
+// KDE Includes
+
+// ----------------------------------------------------------------------------
+// Project Includes
+
+
+namespace reports {
+
+class KReportChartView;
+
+/**
+ * This class serves as interface definition for both a pivottable
+ * and a querytable object
+ */
+class ReportTable
+{
+protected:
+ ReportTable() {}
+public:
+ virtual ~ReportTable() {}
+ virtual QString renderHTML(void) const = 0;
+ virtual QString renderCSV(void) const = 0;
+ virtual void drawChart(KReportChartView& view) const = 0;
+ virtual void dump(const QString& file, const QString& context=QString()) const = 0;
+};
+
+}
+#endif
+// REPORTTABLE_H
+// vim:cin:si:ai:et:ts=2:sw=2: