/*************************************************************************** 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. * * * ***************************************************************************/ #ifdef HAVE_CONFIG_H #include #endif // ---------------------------------------------------------------------------- // 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 // // **************************************************************************** TQDate CashFlowListItem::m_sToday = TQDate::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(TQt::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 { TQDate date; double r = rate + 1.0; double res = 0.00000;//back().value().toDouble(); TQValueList::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 { TQDate date; double r = rate + 1.0; double res = 0.00000; TQValueList::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) << TQString((*it_item).date().toString(TQt::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"); } TQString 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::eTQCnumber ) m_columns += ",number"; if ( qc & MyMoneyReport::eTQCpayee ) m_columns += ",payee"; if ( qc & MyMoneyReport::eTQCcategory ) m_columns += ",category"; if ( qc & MyMoneyReport::eTQCaccount ) m_columns += ",account"; if ( qc & MyMoneyReport::eTQCreconciled ) m_columns += ",reconcileflag"; if ( qc & MyMoneyReport::eTQCmemo ) m_columns += ",memo"; if ( qc & MyMoneyReport::eTQCaction ) m_columns += ",action"; if ( qc & MyMoneyReport::eTQCshares ) m_columns += ",shares"; if ( qc & MyMoneyReport::eTQCprice ) m_columns += ",price"; if ( qc & MyMoneyReport::eTQCperformance ) m_columns += ",startingbal,buys,sells,reinvestincome,cashincome,return,returninvestment"; if ( qc & MyMoneyReport::eTQCloan ) { m_columns += ",payment,interest,fees"; m_postcolumns = "balance"; } if ( qc & MyMoneyReport::eTQCbalance) 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 TQMap accts; //get all transactions for this report TQValueList transactions = file->transactionList(report); for (TQValueList::const_iterator it_transaction = transactions.begin(); it_transaction != transactions.end(); ++it_transaction) { TableRow qA, qS; TQDate pd; qA["id"] = qS["id"] = (* it_transaction).id(); qA["entrydate"] = qS["entrydate"] = (* it_transaction).entryDate().toString(TQt::ISODate); qA["postdate"] = qS["postdate"] = (* it_transaction).postDate().toString(TQt::ISODate); qA["commodity"] = qS["commodity"] = (* it_transaction).commodity(); pd = (* it_transaction).postDate(); qA["month"] = qS["month"] = i18n("Month of %1").arg(TQDate(pd.year(),pd.month(),1).toString(TQt::ISODate)); qA["week"] = qS["week"] = i18n("Week of %1").arg(pd.addDays(1-pd.dayOfWeek()).toString(TQt::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 TQValueList& splits = (*it_transaction).splits(); TQValueList::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::eTQCloan) { 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 TQString a_fullname = ""; TQString a_memo = ""; unsigned int pass = 1; TQString 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(); TQString institution = splitAcc.institutionId(); TQString 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(TQt::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; } TQDate startDate, endDate; report.validDateRange(startDate, endDate); TQString strStartDate = startDate.toString(TQt::ISODate); TQString strEndDate = endDate.toString(TQt::ISODate); startDate = startDate.addDays(-1); TQMap::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(); TQString 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"] = TQString(); 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; TQDate startingDate; TQDate 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(TQDate(), startingDate); filter.addAccount(account.id()); filter.setReportAllSplits(true); TQValueList 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()); TQValueList transactions = file->transactionList( report ); TQValueList::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 TQString& 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 TQValueList splits = (*it_transaction).splits(); TQValueList::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 (TQString 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(); TQValueList accounts; file->accountList(accounts); TQValueList::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(); TQString 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"] = TQString(); // 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 TQMap accts; //get all transactions for this report TQValueList transactions = file->transactionList(report); for (TQValueList::const_iterator it_transaction = transactions.begin(); it_transaction != transactions.end(); ++it_transaction) { TableRow qA, qS; TQDate pd; qA["id"] = qS["id"] = (* it_transaction).id(); qA["entrydate"] = qS["entrydate"] = (* it_transaction).entryDate().toString(TQt::ISODate); qA["postdate"] = qS["postdate"] = (* it_transaction).postDate().toString(TQt::ISODate); qA["commodity"] = qS["commodity"] = (* it_transaction).commodity(); pd = (* it_transaction).postDate(); qA["month"] = qS["month"] = i18n("Month of %1").arg(TQDate(pd.year(),pd.month(),1).toString(TQt::ISODate)); qA["week"] = qS["week"] = i18n("Week of %1").arg(pd.addDays(1-pd.dayOfWeek()).toString(TQt::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 TQValueList& splits = (*it_transaction).splits(); TQValueList::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::eTQCloan) { 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 TQString a_fullname = ""; TQString 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(); TQString institution = splitAcc.institutionId(); TQString 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(TQt::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 TQValueList::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; } TQDate startDate, endDate; report.validDateRange(startDate, endDate); TQString strStartDate = startDate.toString(TQt::ISODate); TQString strEndDate = endDate.toString(TQt::ISODate); startDate = startDate.addDays(-1); TQMap::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(); TQString 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"] = TQString(); 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; } } }