From dadc34655c3ab961b0b0b94a10eaaba710f0b5e8 Mon Sep 17 00:00:00 2001 From: tpearson Date: Mon, 4 Jul 2011 22:38:03 +0000 Subject: Added kmymoney git-svn-id: svn://anonsvn.kde.org/home/kde/branches/trinity/applications/kmymoney@1239792 283d02a7-25f6-0310-bc7c-ecb5cbfe19da --- kmymoney2/reports/querytable.cpp | 1522 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 1522 insertions(+) create mode 100644 kmymoney2/reports/querytable.cpp (limited to 'kmymoney2/reports/querytable.cpp') 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 +#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/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(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::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::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 accts; + + //get all transactions for this report + QValueList transactions = file->transactionList(report); + for (QValueList::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& splits = (*it_transaction).splits(); + QValueList::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::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 , Buys, Sells, Income, Value on , 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 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 transactions = file->transactionList( report ); + QValueList::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 splits = (*it_transaction).splits(); + QValueList::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 accounts; + file->accountList(accounts); + QValueList::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 accts; + + //get all transactions for this report + QValueList transactions = file->transactionList(report); + for (QValueList::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& splits = (*it_transaction).splits(); + QValueList::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::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::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: -- cgit v1.2.3