/*************************************************************************** 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 #include #include // ---------------------------------------------------------------------------- // 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 #include // ---------------------------------------------------------------------------- // 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 ( "

%1

\n" ).arg ( m_config.name() ); csv += "\"Report: " + m_config.name() + "\"\n"; //actual dates of the report result += TQString("
"); if(!m_config.fromDate().isNull()) { result += i18n("Report date range", "%1 through %2").tqarg(KGlobal::locale()->formatDate(m_config.fromDate(), true)).tqarg(KGlobal::locale()->formatDate(m_config.toDate(), true)); result += TQString("
\n"); result += TQString("
 
\n"); csv += i18n("Report date range", "%1 through %2").tqarg(KGlobal::locale()->formatDate(m_config.fromDate(), true)).tqarg(KGlobal::locale()->formatDate(m_config.toDate(), true)); csv += TQString("\n"); } result += TQString ( "
" ); 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 ( "
\n" ); result += TQString ( "
 
\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 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 += "\n"; TQStringList::const_iterator it_column = columns.begin(); while ( it_column != columns.end() ) { TQString i18nName = i18nHeaders[*it_column]; if ( i18nName.isEmpty() ) i18nName = *it_column; result += ""; csv += i18nName + ","; ++it_column; } result += "\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 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::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::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 += "" "" "\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 += "" "\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 ( "" ).arg ( ( * it_row ) ["id"] ); else if ( ( * it_row ) ["rank"] == "1" ) result += TQString ( "" ).arg ( row_odd ? "item1" : "item0" ); else result += TQString ( "" ).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 ( "" ); csv += "\"\","; } else { result += TQString ( "" ).arg ( MyMoneyMoney ( data ).formatMoney ( "", 3 ) ); csv += "\"" + MyMoneyMoney ( data ).formatMoney ( "", 3, false ) + "\","; } } else if ( moneyColumns.contains ( *it_column ) ) { if ( data.isEmpty() ) { result += TQString ( "" ) .arg ( ( *it_column == "value" ) ? " class=\"value\"" : "" ); csv += "\"\","; } else if ( MyMoneyMoney( data ) == MyMoneyMoney::autoCalc ) { result += TQString ( "%2" ) .arg ( ( *it_column == "value" ) ? " class=\"value\"" : "" ) .arg (i18n("Calculated")); csv += "\""+ i18n("Calculated") +"\","; } else if ( *it_column == "price" ) { result += TQString ( "" ) .arg ( MyMoneyMoney ( data ).formatMoney ( MyMoneyMoney::precToDenom(KMyMoneyGlobalSettings::pricePrecision()) ) ); csv += "\"" + ( *it_row ) ["currency"] + " " + MyMoneyMoney ( data ).formatMoney ( MyMoneyMoney::precToDenom(KMyMoneyGlobalSettings::pricePrecision()), false ) + "\","; } else { result += TQString ( "%2 %3" ) .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 ( "" ).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() ) { TQDate qd = TQDate::fromString ( data, Qt::ISODate ); data = KGlobal::locale()->formatDate ( qd, true ); } result += TQString ( "" ).arg ( data ); } else { result += TQString ( "" ).arg ( data ); csv += "\"" + data + "\","; } ++it_column; } result += "\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::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 += "" "" "\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 += "" "" "\n"; csv += "\"" + i18n ( "Grand Total" ) + "\",\"" + grandtotal_csv + "\"\n"; } result += "
" + i18nName + "
" + i18n ( "Total" ) + " " + oldName + "" + subtotal_html + "
" + ( *it_group ).name() + "
%1%2%1%%1%1
" + i18n ( "Total" ) + " " + ( *it_group ).oldName() + "" + subtotal_html + "
" + i18n ( "Grand Total" ) + "" + grandtotal_html + "
\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); } } } } } } } }