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

634 lines
25 KiB

/***************************************************************************
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 <tqvaluelist.h>
#include <tqfile.h>
#include <tqtextstream.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 {
TQStringList ListTable::TableRow::m_sortCriteria;
// ****************************************************************************
//
// Group Iterator
//
// ****************************************************************************
class GroupIterator
{
public:
GroupIterator ( const TQString& _group, const TQString& _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 TQString& name ( void ) const { return m_currentGroup; }
const TQString& oldName ( void ) const { return m_previousGroup; }
const TQString& groupField ( void ) const { return m_groupField; }
const TQString& 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;
TQString m_currentGroup;
TQString m_previousGroup;
TQString m_groupField;
TQString m_subtotalField;
};
// ****************************************************************************
//
// ListTable implementation
//
// ****************************************************************************
bool ListTable::TableRow::operator< ( const TableRow& _compare ) const
{
bool result = false;
TQStringList::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 ( TQString& result, TQString& csv ) const
{
MyMoneyMoney grandtotal;
MyMoneyFile* file = MyMoneyFile::instance();
result = "";
csv = "";
result += TQString ( "<h2 class=\"report\">%1</h2>\n" ).arg ( m_config.name() );
csv += "\"Report: " + m_config.name() + "\"\n";
//actual dates of the report
result += TQString("<div class=\"subtitle\">");
if(!m_config.fromDate().isNull()) {
result += i18n("Report date range", "%1 through %2").arg(TDEGlobal::locale()->formatDate(m_config.fromDate(), true)).arg(TDEGlobal::locale()->formatDate(m_config.toDate(), true));
result += TQString("</div>\n");
result += TQString("<div class=\"gap\">&nbsp;</div>\n");
csv += i18n("Report date range", "%1 through %2").arg(TDEGlobal::locale()->formatDate(m_config.fromDate(), true)).arg(TDEGlobal::locale()->formatDate(m_config.toDate(), true));
csv += TQString("\n");
}
result += TQString ( "<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 += TQString ( "</div>\n" );
result += TQString ( "<div class=\"gap\">&nbsp;</div>\n" );
// retrieve the configuration parameters from the report definition.
// the things that we care about for query reports are:
// how to group the rows, what columns to display, and what field
// to subtotal on
TQStringList groups = TQStringList::split ( ",", m_group );
TQStringList columns = TQStringList::split ( ",", m_columns );
columns += m_subtotal;
TQStringList postcolumns = TQStringList::split ( ",", m_postcolumns );
columns += postcolumns;
//
// Table header
//
TQMap<TQString, TQString> 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
TQStringList moneyColumns = TQStringList::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
TQStringList sharesColumns = TQStringList::split ( ",", "shares" );
// the list of columns which represent a percentage, so we can display them correctly
TQStringList percentColumns = TQStringList::split ( ",", "return,returninvestment,interestrate" );
// the list of columns which represent dates, so we can display them correctly
TQStringList dateColumns = TQStringList::split ( ",", "postdate,entrydate,nextduedate,openingdate,nextinterestchange" );
result += "<table class=\"report\">\n<thead><tr class=\"itemheader\">";
TQStringList::const_iterator it_column = columns.begin();
while ( it_column != columns.end() )
{
TQString 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;
TQValueList<GroupIterator> groupIteratorList;
TQStringList::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 ( TQValueList<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
TQValueList<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);
TQString subtotal_html = ( *it_group ).subtotal().formatMoney ( fraction );
TQString subtotal_csv = ( *it_group ).subtotal().formatMoney ( fraction, false );
// ***DV*** HACK fix the side-effiect from .force() method above
TQString oldName = TQString ( ( *it_group ).oldName() ).stripWhiteSpace();
result +=
"<tr class=\"sectionfooter\">"
"<td class=\"left" + TQString::number ( ( ( *it_group ).depth() - 1 ) ) + "\" "
"colspan=\"" +
TQString::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" + TQString::number ( ( ( *it_group ).depth() - 1 ) ) + "\" "
"colspan=\"" + TQString::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 += TQString ( "<tr class=\"item%1\">" ).arg ( ( * it_row ) ["id"] );
else
if ( ( * it_row ) ["rank"] == "1" )
result += TQString ( "<tr class=\"%1\">" ).arg ( row_odd ? "item1" : "item0" );
else
result += TQString ( "<tr class=\"%1\">" ).arg ( row_odd ? "row-odd " : "row-even" );
TQStringList::const_iterator it_column = columns.begin();
while ( it_column != columns.end() )
{
TQString 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 += TQString ( "<td></td>" );
csv += "\"\",";
}
else {
result += TQString ( "<td>%1</td>" ).arg ( MyMoneyMoney ( data ).formatMoney ( "", 3 ) );
csv += "\"" + MyMoneyMoney ( data ).formatMoney ( "", 3, false ) + "\",";
}
}
else if ( moneyColumns.contains ( *it_column ) )
{
if ( data.isEmpty() ) {
result += TQString ( "<td%1></td>" )
.arg ( ( *it_column == "value" ) ? " class=\"value\"" : "" );
csv += "\"\",";
} else if ( MyMoneyMoney( data ) == MyMoneyMoney::autoCalc ) {
result += TQString ( "<td%1>%2</td>" )
.arg ( ( *it_column == "value" ) ? " class=\"value\"" : "" )
.arg (i18n("Calculated"));
csv += "\""+ i18n("Calculated") +"\",";
} else if ( *it_column == "price" ) {
result += TQString ( "<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 += TQString ( "<td%1>%2&nbsp;%3</td>" )
.arg ( ( *it_column == "value" ) ? " class=\"value\"" : "" )
.arg ( ( *it_row ) ["currency"] )
.arg ( MyMoneyMoney ( data ).formatMoney ( fraction ) );
csv += "\"" + ( *it_row ) ["currency"] + " " + MyMoneyMoney ( data ).formatMoney ( fraction, false ) + "\",";
}
}
else if ( percentColumns.contains ( *it_column ) )
{
data = ( MyMoneyMoney ( data ) * MyMoneyMoney ( 100, 1 ) ).formatMoney ( fraction );
result += TQString ( "<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 ( TDEGlobal::locale() && ! data.isEmpty() ) {
TQDate qd = TQDate::fromString ( data, Qt::ISODate );
data = TDEGlobal::locale()->formatDate ( qd, true );
}
result += TQString ( "<td class=\"left\">%1</td>" ).arg ( data );
}
else
{
result += TQString ( "<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();
TQValueList<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);
}
TQString subtotal_html = ( *it_group ).subtotal().formatMoney ( fraction );
TQString subtotal_csv = ( *it_group ).subtotal().formatMoney ( fraction, false );
result += "<tr class=\"sectionfooter\">"
"<td class=\"left" + TQString::number ( ( *it_group ).depth() - 1 ) + "\" "
"colspan=\"" + TQString::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
//
TQString grandtotal_html = grandtotal.formatMoney ( fraction );
TQString grandtotal_csv = grandtotal.formatMoney ( fraction, false );
result += "<tr class=\"sectionfooter\">"
"<td class=\"left0\" "
"colspan=\"" + TQString::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";
}
TQString ListTable::renderHTML ( void ) const
{
TQString html, csv;
render ( html, csv );
return html;
}
TQString ListTable::renderCSV ( void ) const
{
TQString html, csv;
render ( html, csv );
return csv;
}
void ListTable::dump ( const TQString& file, const TQString& context ) const
{
TQFile g ( file );
g.open ( IO_WriteOnly );
if ( ! context.isEmpty() )
TQTextStream ( &g ) << context.arg ( renderHTML() );
else
TQTextStream ( &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
TQStringList accountList;
if(m_config.accounts(accountList)) {
if(!KMyMoneyGlobalSettings::expertMode()) {
TQStringList::const_iterator it_a, it_b;
for(it_a = accountList.begin(); it_a != accountList.end(); ++it_a) {
MyMoneyAccount acc = MyMoneyFile::instance()->account(*it_a);
if(acc.accountType() == MyMoneyAccount::Investment) {
for(it_b = acc.accountList().begin(); it_b != acc.accountList().end(); ++it_b) {
if(!accountList.contains(*it_b)) {
m_config.addAccount(*it_b);
}
}
}
}
}
}
}
}