diff options
author | tpearson <tpearson@283d02a7-25f6-0310-bc7c-ecb5cbfe19da> | 2011-07-04 22:38:03 +0000 |
---|---|---|
committer | tpearson <tpearson@283d02a7-25f6-0310-bc7c-ecb5cbfe19da> | 2011-07-04 22:38:03 +0000 |
commit | dadc34655c3ab961b0b0b94a10eaaba710f0b5e8 (patch) | |
tree | 99e72842fe687baea16376a147619b6048d7e441 /kmymoney2/reports | |
download | kmymoney-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')
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\"> </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\"> </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 %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 %2 - %3 %4") + .arg(KGlobal::locale()->calendar()->monthName(prv.month(), prv.year(), true)) + .arg(prv.day()) + .arg(KGlobal::locale()->calendar()->monthName(day.month(), day.year(), true)) + .arg(day.day())); + prv = day.addDays(1); + } + day = day.addDays(1); + dow++; + } + } + } + + // else it's a months-based report + else + { + if ( columnpitch == 12 ) + { + unsigned year = m_beginDate.year(); + unsigned column = 1; + while ( column++ < m_numColumns ) + m_columnHeadings.append(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\"> </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\"> </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(" "), " ")) + .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\"> %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(" "), " ")) + .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 %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> </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> </td></tr>\n"); + result += QString("<tr class=\"spacer\"><td> </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(" ", " "); + } + } + 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> -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> -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: |