diff --git a/kmymoney/mymoney/mymoneyaccount.cpp b/kmymoney/mymoney/mymoneyaccount.cpp index 46be12dfb..2d35f0c29 100644 --- a/kmymoney/mymoney/mymoneyaccount.cpp +++ b/kmymoney/mymoney/mymoneyaccount.cpp @@ -1,854 +1,864 @@ /*************************************************************************** mymoneyaccount.cpp ------------------- copyright : (C) 2000 by Michael Edwardes (C) 2002 by Thomas Baumgart ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "mymoneyaccount.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include +#include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "mymoneyexception.h" #include "mymoneysplit.h" #include "mymoneyfile.h" #include "payeeidentifier/payeeidentifiertyped.h" #include "payeeidentifier/ibanandbic/ibanbic.h" #include "payeeidentifier/nationalaccount/nationalaccount.h" MyMoneyAccount::MyMoneyAccount() : m_accountType(UnknownAccountType), m_fraction(-1) { } MyMoneyAccount::~MyMoneyAccount() { } MyMoneyAccount::MyMoneyAccount(const QString& id, const MyMoneyAccount& right) : MyMoneyObject(id) { *this = right; setId(id); } MyMoneyAccount::MyMoneyAccount(const QDomElement& node) : MyMoneyObject(node), MyMoneyKeyValueContainer(node.elementsByTagName("KEYVALUEPAIRS").item(0).toElement()), m_accountType(UnknownAccountType), m_fraction(-1) { if ("ACCOUNT" != node.tagName()) throw MYMONEYEXCEPTION("Node was not ACCOUNT"); setName(node.attribute("name")); // qDebug("Reading information for account %s", acc.name().data()); setParentAccountId(QStringEmpty(node.attribute("parentaccount"))); setLastModified(stringToDate(QStringEmpty(node.attribute("lastmodified")))); setLastReconciliationDate(stringToDate(QStringEmpty(node.attribute("lastreconciled")))); if (!m_lastReconciliationDate.isValid()) { // for some reason, I was unable to access our own kvp at this point through // the value() method. It always returned empty strings. The workaround for // this is to construct a local kvp the same way as we have done before and // extract the value from it. // // Since we want to get rid of the lastStatementDate record anyway, this seems // to be ok for now. (ipwizard - 2008-08-14) QString txt = MyMoneyKeyValueContainer(node.elementsByTagName("KEYVALUEPAIRS").item(0).toElement()).value("lastStatementDate"); if (!txt.isEmpty()) { setLastReconciliationDate(QDate::fromString(txt, Qt::ISODate)); } } setInstitutionId(QStringEmpty(node.attribute("institution"))); setNumber(QStringEmpty(node.attribute("number"))); setOpeningDate(stringToDate(QStringEmpty(node.attribute("opened")))); setCurrencyId(QStringEmpty(node.attribute("currency"))); QString tmp = QStringEmpty(node.attribute("type")); bool bOK = false; int type = tmp.toInt(&bOK); if (bOK) { setAccountType(static_cast(type)); } else { qWarning("XMLREADER: Account %s had invalid or no account type information.", qPrintable(name())); } if (node.hasAttribute("openingbalance")) { if (!MyMoneyMoney(node.attribute("openingbalance")).isZero()) { QString msg = i18n("Account %1 contains an opening balance. Please use KMyMoney version 0.8 or later and earlier than version 0.9 to correct the problem.", m_name); throw MYMONEYEXCEPTION(msg); } } setDescription(node.attribute("description")); m_id = QStringEmpty(node.attribute("id")); // qDebug("Account %s has id of %s, type of %d, parent is %s.", acc.name().data(), id.data(), type, acc.parentAccountId().data()); // Process any Sub-Account information found inside the account entry. m_accountList.clear(); QDomNodeList nodeList = node.elementsByTagName("SUBACCOUNTS"); if (nodeList.count() > 0) { nodeList = nodeList.item(0).toElement().elementsByTagName("SUBACCOUNT"); for (int i = 0; i < nodeList.count(); ++i) { addAccountId(QString(nodeList.item(i).toElement().attribute("id"))); } } nodeList = node.elementsByTagName("ONLINEBANKING"); if (nodeList.count() > 0) { QDomNamedNodeMap attributes = nodeList.item(0).toElement().attributes(); for (int i = 0; i < attributes.count(); ++i) { const QDomAttr& it_attr = attributes.item(i).toAttr(); m_onlineBankingSettings.setValue(it_attr.name(), it_attr.value()); } } // Up to and including version 4.6.6 the new account dialog stored the iban in the kvp-key "IBAN". // But the rest of the software uses "iban". So correct this: if (!value("IBAN").isEmpty()) { // If "iban" was not set, set it now. If it is set, the user reseted it already, so remove // the garbage. if (value("iban").isEmpty()) setValue("iban", value("IBAN")); deletePair("IBAN"); } } void MyMoneyAccount::setName(const QString& name) { m_name = name; } void MyMoneyAccount::setNumber(const QString& number) { m_number = number; } void MyMoneyAccount::setDescription(const QString& desc) { m_description = desc; } void MyMoneyAccount::setInstitutionId(const QString& id) { m_institution = id; } void MyMoneyAccount::setLastModified(const QDate& date) { m_lastModified = date; } void MyMoneyAccount::setOpeningDate(const QDate& date) { m_openingDate = date; } void MyMoneyAccount::setLastReconciliationDate(const QDate& date) { // FIXME: for a limited time (maybe until we delivered 1.0) we // keep the last reconciliation date also in the KVP for backward // compatibility. After that, the setValue() statemetn should be removed // and the XML ctor should remove the value completely from the KVP setValue("lastStatementDate", date.toString(Qt::ISODate)); m_lastReconciliationDate = date; } void MyMoneyAccount::setParentAccountId(const QString& parent) { m_parentAccount = parent; } void MyMoneyAccount::setAccountType(const accountTypeE type) { m_accountType = type; } void MyMoneyAccount::addAccountId(const QString& account) { if (!m_accountList.contains(account)) m_accountList += account; } void MyMoneyAccount::removeAccountIds() { m_accountList.clear(); } void MyMoneyAccount::removeAccountId(const QString& account) { int pos; pos = m_accountList.indexOf(account); if (pos != -1) m_accountList.removeAt(pos); } bool MyMoneyAccount::operator == (const MyMoneyAccount& right) const { return (MyMoneyKeyValueContainer::operator==(right) && MyMoneyObject::operator==(right) && (m_accountList == right.m_accountList) && (m_accountType == right.m_accountType) && (m_lastModified == right.m_lastModified) && (m_lastReconciliationDate == right.m_lastReconciliationDate) && ((m_name.length() == 0 && right.m_name.length() == 0) || (m_name == right.m_name)) && ((m_number.length() == 0 && right.m_number.length() == 0) || (m_number == right.m_number)) && ((m_description.length() == 0 && right.m_description.length() == 0) || (m_description == right.m_description)) && (m_openingDate == right.m_openingDate) && (m_parentAccount == right.m_parentAccount) && (m_currencyId == right.m_currencyId) && (m_institution == right.m_institution)); } MyMoneyAccount::accountTypeE MyMoneyAccount::accountGroup() const { switch (m_accountType) { case MyMoneyAccount::Checkings: case MyMoneyAccount::Savings: case MyMoneyAccount::Cash: case MyMoneyAccount::Currency: case MyMoneyAccount::Investment: case MyMoneyAccount::MoneyMarket: case MyMoneyAccount::CertificateDep: case MyMoneyAccount::AssetLoan: case MyMoneyAccount::Stock: return MyMoneyAccount::Asset; case MyMoneyAccount::CreditCard: case MyMoneyAccount::Loan: return MyMoneyAccount::Liability; default: return m_accountType; } } void MyMoneyAccount::setCurrencyId(const QString& id) { m_currencyId = id; } bool MyMoneyAccount::isAssetLiability() const { return accountGroup() == Asset || accountGroup() == Liability; } bool MyMoneyAccount::isIncomeExpense() const { return accountGroup() == Income || accountGroup() == Expense; } bool MyMoneyAccount::isLoan() const { return accountType() == Loan || accountType() == AssetLoan; } bool MyMoneyAccount::isInvest() const { return accountType() == Stock; } bool MyMoneyAccount::isLiquidAsset() const { return accountType() == Checkings || accountType() == Savings || accountType() == Cash; } template<> QList< payeeIdentifierTyped< ::payeeIdentifiers::ibanBic> > MyMoneyAccount::payeeIdentifiersByType() const { payeeIdentifierTyped ident = payeeIdentifierTyped(new payeeIdentifiers::ibanBic); ident->setIban(value(QLatin1String("iban"))); if (!institutionId().isEmpty()) { const MyMoneyInstitution institution = MyMoneyFile::instance()->institution(institutionId()); ident->setBic(institution.value("bic")); } ident->setOwnerName(MyMoneyFile::instance()->user().name()); QList< payeeIdentifierTyped > typedList; typedList << ident; return typedList; } MyMoneyAccountLoan::MyMoneyAccountLoan(const MyMoneyAccount& acc) : MyMoneyAccount(acc) { } const MyMoneyMoney MyMoneyAccountLoan::loanAmount() const { return MyMoneyMoney(value("loan-amount")); } void MyMoneyAccountLoan::setLoanAmount(const MyMoneyMoney& amount) { setValue("loan-amount", amount.toString()); } const MyMoneyMoney MyMoneyAccountLoan::interestRate(const QDate& date) const { MyMoneyMoney rate; QString key; QString val; if (!date.isValid()) return rate; key.sprintf("ir-%04d-%02d-%02d", date.year(), date.month(), date.day()); QRegExp regExp("ir-(\\d{4})-(\\d{2})-(\\d{2})"); QMap::ConstIterator it; for (it = pairs().begin(); it != pairs().end(); ++it) { if (regExp.indexIn(it.key()) > -1) { if (qstrcmp(it.key().toLatin1(), key.toLatin1()) <= 0) val = *it; else break; } else if (!val.isEmpty()) break; } if (!val.isEmpty()) { rate = MyMoneyMoney(val); } return rate; } void MyMoneyAccountLoan::setInterestRate(const QDate& date, const MyMoneyMoney& value) { if (!date.isValid()) return; QString key; key.sprintf("ir-%04d-%02d-%02d", date.year(), date.month(), date.day()); setValue(key, value.toString()); } MyMoneyAccountLoan::interestDueE MyMoneyAccountLoan::interestCalculation() const { QString payTime(value("interest-calculation")); if (payTime == "paymentDue") return paymentDue; return paymentReceived; } void MyMoneyAccountLoan::setInterestCalculation(const MyMoneyAccountLoan::interestDueE onReception) { if (onReception == paymentDue) setValue("interest-calculation", "paymentDue"); else setValue("interest-calculation", "paymentReceived"); } const QDate MyMoneyAccountLoan::nextInterestChange() const { QDate rc; QRegExp regExp("(\\d{4})-(\\d{2})-(\\d{2})"); if (regExp.indexIn(value("interest-nextchange")) != -1) { rc.setYMD(regExp.cap(1).toInt(), regExp.cap(2).toInt(), regExp.cap(3).toInt()); } return rc; } void MyMoneyAccountLoan::setNextInterestChange(const QDate& date) { setValue("interest-nextchange", date.toString(Qt::ISODate)); } int MyMoneyAccountLoan::interestChangeFrequency(int* unit) const { int rc = -1; if (unit) *unit = 1; QRegExp regExp("(\\d+)/(\\d{1})"); if (regExp.indexIn(value("interest-changefrequency")) != -1) { rc = regExp.cap(1).toInt(); if (unit != 0) { *unit = regExp.cap(2).toInt(); } } return rc; } void MyMoneyAccountLoan::setInterestChangeFrequency(const int amount, const int unit) { QString val; val.sprintf("%d/%d", amount, unit); setValue("interest-changeFrequency", val); } const QString MyMoneyAccountLoan::schedule() const { return QString(value("schedule").toLatin1()); } void MyMoneyAccountLoan::setSchedule(const QString& sched) { setValue("schedule", sched); } bool MyMoneyAccountLoan::fixedInterestRate() const { // make sure, that an empty kvp element returns true return !(value("fixed-interest") == "no"); } void MyMoneyAccountLoan::setFixedInterestRate(const bool fixed) { setValue("fixed-interest", fixed ? "yes" : "no"); if (fixed) { deletePair("interest-nextchange"); deletePair("interest-changeFrequency"); } } const MyMoneyMoney MyMoneyAccountLoan::finalPayment() const { return MyMoneyMoney(value("final-payment")); } void MyMoneyAccountLoan::setFinalPayment(const MyMoneyMoney& finalPayment) { setValue("final-payment", finalPayment.toString()); } unsigned int MyMoneyAccountLoan::term() const { return value("term").toUInt(); } void MyMoneyAccountLoan::setTerm(const unsigned int payments) { setValue("term", QString::number(payments)); } const MyMoneyMoney MyMoneyAccountLoan::periodicPayment() const { return MyMoneyMoney(value("periodic-payment")); } void MyMoneyAccountLoan::setPeriodicPayment(const MyMoneyMoney& payment) { setValue("periodic-payment", payment.toString()); } const QString MyMoneyAccountLoan::payee() const { return value("payee"); } void MyMoneyAccountLoan::setPayee(const QString& payee) { setValue("payee", payee); } const QString MyMoneyAccountLoan::interestAccountId() const { return QString(); } void MyMoneyAccountLoan::setInterestAccountId(const QString& /* id */) { } bool MyMoneyAccountLoan::hasReferenceTo(const QString& id) const { return MyMoneyAccount::hasReferenceTo(id) || (id == payee()) || (id == schedule()); } void MyMoneyAccountLoan::setInterestCompounding(int frequency) { setValue("compoundingFrequency", QString("%1").arg(frequency)); } int MyMoneyAccountLoan::interestCompounding() const { return value("compoundingFrequency").toInt(); } void MyMoneyAccount::writeXML(QDomDocument& document, QDomElement& parent) const { QDomElement el = document.createElement("ACCOUNT"); writeBaseXML(document, el); el.setAttribute("parentaccount", parentAccountId()); el.setAttribute("lastreconciled", dateToString(lastReconciliationDate())); el.setAttribute("lastmodified", dateToString(lastModified())); el.setAttribute("institution", institutionId()); el.setAttribute("opened", dateToString(openingDate())); el.setAttribute("number", number()); // el.setAttribute("openingbalance", openingBalance().toString()); el.setAttribute("type", accountType()); el.setAttribute("name", name()); el.setAttribute("description", description()); if (!currencyId().isEmpty()) el.setAttribute("currency", currencyId()); //Add in subaccount information, if this account has subaccounts. if (accountCount()) { QDomElement subAccounts = document.createElement("SUBACCOUNTS"); QStringList::ConstIterator it; for (it = accountList().begin(); it != accountList().end(); ++it) { QDomElement temp = document.createElement("SUBACCOUNT"); temp.setAttribute("id", (*it)); subAccounts.appendChild(temp); } el.appendChild(subAccounts); } // Write online banking settings if (m_onlineBankingSettings.pairs().count()) { QDomElement onlinesettings = document.createElement("ONLINEBANKING"); QMap::const_iterator it_key = m_onlineBankingSettings.pairs().begin(); while (it_key != m_onlineBankingSettings.pairs().end()) { onlinesettings.setAttribute(it_key.key(), it_key.value()); ++it_key; } el.appendChild(onlinesettings); } // FIXME drop the lastStatementDate record from the KVP when it is // not stored there after setLastReconciliationDate() has been changed // See comment there when this will happen // deletePair("lastStatementDate"); //Add in Key-Value Pairs for accounts. MyMoneyKeyValueContainer::writeXML(document, el); parent.appendChild(el); } bool MyMoneyAccount::hasReferenceTo(const QString& id) const { return (id == m_institution) || (id == m_parentAccount) || (id == m_currencyId); } void MyMoneyAccount::setOnlineBankingSettings(const MyMoneyKeyValueContainer& values) { m_onlineBankingSettings = values; } const MyMoneyKeyValueContainer& MyMoneyAccount::onlineBankingSettings() const { return m_onlineBankingSettings; } void MyMoneyAccount::setClosed(bool closed) { if (closed) setValue("mm-closed", "yes"); else deletePair("mm-closed"); } bool MyMoneyAccount::isClosed() const { return !(value("mm-closed").isEmpty()); } int MyMoneyAccount::fraction(const MyMoneySecurity& sec) const { int fraction; if (m_accountType == Cash) fraction = sec.smallestCashFraction(); else fraction = sec.smallestAccountFraction(); return fraction; } int MyMoneyAccount::fraction(const MyMoneySecurity& sec) { if (m_accountType == Cash) m_fraction = sec.smallestCashFraction(); else m_fraction = sec.smallestAccountFraction(); return m_fraction; } int MyMoneyAccount::fraction() const { return m_fraction; } bool MyMoneyAccount::isCategory() const { return m_accountType == Income || m_accountType == Expense; } QString MyMoneyAccount::brokerageName() const { if (m_accountType == Investment) return QString("%1 (%2)").arg(m_name, i18nc("Brokerage (suffix for account names)", "Brokerage")); return m_name; } void MyMoneyAccount::adjustBalance(const MyMoneySplit& s, bool reverse) { if (s.action() == MyMoneySplit::ActionSplitShares) { if (reverse) m_balance = m_balance / s.shares(); else m_balance = m_balance * s.shares(); } else { if (reverse) m_balance -= s.shares(); else m_balance += s.shares(); } } QPixmap MyMoneyAccount::accountPixmap(bool reconcileFlag, int size) const { QString icon; switch (accountType()) { default: if (accountGroup() == MyMoneyAccount::Asset) icon = "view-bank-account"; else icon = "view-loan"; break; case MyMoneyAccount::Investment: case MyMoneyAccount::Stock: case MyMoneyAccount::MoneyMarket: case MyMoneyAccount::CertificateDep: icon = "view-stock-account"; break; case MyMoneyAccount::Checkings: icon = "view-bank-account-checking"; break; case MyMoneyAccount::Savings: icon = "view-bank-account-savings"; break; case MyMoneyAccount::AssetLoan: icon = "view-loan-asset"; break; case MyMoneyAccount::Loan: icon = "view-loan"; break; case MyMoneyAccount::CreditCard: icon = "view-credit-card-account"; break; case MyMoneyAccount::Asset: icon = "view-bank-account"; break; case MyMoneyAccount::Cash: icon = "account-types-cash"; break; case MyMoneyAccount::Income: icon = "view-income-categories"; break; case MyMoneyAccount::Expense: icon = "view-expenses-categories"; break; case MyMoneyAccount::Equity: icon = "view-bank-account"; break; } QString iconKey = icon + QString(size); QPixmap result; if (!QPixmapCache::find(iconKey, result)) { result = DesktopIcon(icon, size); QPixmapCache::insert(iconKey, result); } QPainter pixmapPainter(&result); if (isClosed()) { QPixmap ovly = DesktopIcon("dialog-close", size); pixmapPainter.drawPixmap(ovly.width() / 2, ovly.height() / 2, ovly.width() / 2, ovly.height() / 2, ovly); } else if (reconcileFlag) { QPixmap ovly = DesktopIcon("flag-green", size); pixmapPainter.drawPixmap(size / 2, size / 2, ovly.width() / 2, ovly.height() / 2, ovly); } else if (!onlineBankingSettings().value("provider").isEmpty()) { QPixmap ovly = DesktopIcon("download", size); pixmapPainter.drawPixmap(size / 2, size / 2, ovly.width() / 2, ovly.height() / 2, ovly); } return result; } QString MyMoneyAccount::accountTypeToString(const MyMoneyAccount::accountTypeE accountType) { QString returnString; switch (accountType) { case MyMoneyAccount::Checkings: returnString = i18n("Checking"); break; case MyMoneyAccount::Savings: returnString = i18n("Savings"); break; case MyMoneyAccount::CreditCard: returnString = i18n("Credit Card"); break; case MyMoneyAccount::Cash: returnString = i18n("Cash"); break; case MyMoneyAccount::Loan: returnString = i18n("Loan"); break; case MyMoneyAccount::CertificateDep: returnString = i18n("Certificate of Deposit"); break; case MyMoneyAccount::Investment: returnString = i18n("Investment"); break; case MyMoneyAccount::MoneyMarket: returnString = i18n("Money Market"); break; case MyMoneyAccount::Asset: returnString = i18n("Asset"); break; case MyMoneyAccount::Liability: returnString = i18n("Liability"); break; case MyMoneyAccount::Currency: returnString = i18n("Currency"); break; case MyMoneyAccount::Income: returnString = i18n("Income"); break; case MyMoneyAccount::Expense: returnString = i18n("Expense"); break; case MyMoneyAccount::AssetLoan: returnString = i18n("Investment Loan"); break; case MyMoneyAccount::Stock: returnString = i18n("Stock"); break; case MyMoneyAccount::Equity: returnString = i18n("Equity"); break; default: returnString = i18nc("Unknown account type", "Unknown"); } return returnString; } bool MyMoneyAccount::addReconciliation(const QDate& date, const MyMoneyMoney& amount) { m_reconciliationHistory[date] = amount; QString history, sep; QMap::const_iterator it; for (it = m_reconciliationHistory.constBegin(); it != m_reconciliationHistory.constEnd(); ++it) { history += QString("%1%2:%3").arg(sep, it.key().toString(Qt::ISODate), (*it).toString()); sep = QLatin1Char(';'); } setValue("reconciliationHistory", history); return true; } const QMap& MyMoneyAccount::reconciliationHistory() { // check if the internal history member is already loaded if (m_reconciliationHistory.count() == 0 && !value("reconciliationHistory").isEmpty()) { QStringList entries = value("reconciliationHistory").split(';'); foreach (const QString& entry, entries) { QStringList parts = entry.split(':'); QDate date = QDate::fromString(parts[0], Qt::ISODate); MyMoneyMoney amount(parts[1]); if (parts.count() == 2 && date.isValid()) { m_reconciliationHistory[date] = amount; } } } return m_reconciliationHistory; } /** * @todo Improve setting of country for nationalAccount */ QList< payeeIdentifier > MyMoneyAccount::payeeIdentifiers() const { QList< payeeIdentifier > list; MyMoneyFile* file = MyMoneyFile::instance(); // Iban & Bic if (!value(QLatin1String("iban")).isEmpty()) { payeeIdentifierTyped iban(new payeeIdentifiers::ibanBic); iban->setIban(value("iban")); iban->setBic(file->institution(institutionId()).value("bic")); iban->setOwnerName(file->user().name()); list.append(iban); } // National Account number if (!number().isEmpty()) { payeeIdentifierTyped national(new payeeIdentifiers::nationalAccount); national->setAccountNumber(number()); national->setBankCode(file->institution(institutionId()).sortcode()); if (file->user().state().length() == 2) national->setCountry(file->user().state()); national->setOwnerName(file->user().name()); list.append(national); } return list; } +QDebug operator<<(QDebug dbg, const MyMoneyAccount &a) +{ + dbg << "MyMoneyAccount(" + << "id" << a.id() + << "accountType" << MyMoneyAccount::accountTypeToString(a.accountType()); + dbg = operator<<(dbg, static_cast(a)) + << ")"; + return dbg; +} diff --git a/kmymoney/mymoney/mymoneyaccount.h b/kmymoney/mymoney/mymoneyaccount.h index 9b840b5ed..d0a7a1f3d 100644 --- a/kmymoney/mymoney/mymoneyaccount.h +++ b/kmymoney/mymoney/mymoneyaccount.h @@ -1,781 +1,783 @@ /*************************************************************************** mymoneyaccount.h ------------------- copyright : (C) 2002 by Thomas Baumgart ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #ifndef MYMONEYACCOUNT_H #define MYMONEYACCOUNT_H // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "mymoneymoney.h" #include "mymoneyobject.h" #include "mymoneykeyvaluecontainer.h" #include "mymoneysecurity.h" #include "kmm_mymoney_export.h" #include "mymoneyunittestable.h" #include "mymoneyutils.h" #include "payeeidentifier/payeeidentifier.h" #include "payeeidentifier/ibanandbic/ibanbic.h" #include "mymoneypayeeidentifiercontainer.h" class MyMoneyTransaction; class MyMoneySplit; class MyMoneyObjectContainer; /** * A representation of an account. * This object represents any type of account, those held at an * institution as well as the accounts used for double entry * accounting. * * Currently, the following account types are known: * * @li UnknownAccountType * @li Checkings * @li Savings * @li Cash * @li CreditCard * @li Loan (collected) * @li CertificateDep * @li Investment * @li MoneyMarket * @li Currency * @li Asset * @li Liability * @li Income * @li Expense * @li Loan (given) * @li Stock * @li Equity * * @see MyMoneyInstitution * @see MyMoneyFile * * @author Michael Edwardes 2000-2001 * @author Thomas Baumgart 2002 * **/ class KMM_MYMONEY_EXPORT MyMoneyAccount : public MyMoneyObject, public MyMoneyKeyValueContainer /*, public MyMoneyPayeeIdentifierContainer */ { friend class MyMoneyObjectContainer; KMM_MYMONEY_UNIT_TESTABLE public: /** * Account types currently supported. */ typedef enum _accountTypeE { UnknownAccountType = 0, /**< For error handling */ Checkings, /**< Standard checking account */ Savings, /**< Typical savings account */ Cash, /**< Denotes a shoe-box or pillowcase stuffed with cash */ CreditCard, /**< Credit card accounts */ Loan, /**< Loan and mortgage accounts (liability) */ CertificateDep, /**< Certificates of Deposit */ Investment, /**< Investment account */ MoneyMarket, /**< Money Market Account */ Asset, /**< Denotes a generic asset account.*/ Liability, /**< Denotes a generic liability account.*/ Currency, /**< Denotes a currency trading account. */ Income, /**< Denotes an income account */ Expense, /**< Denotes an expense account */ AssetLoan, /**< Denotes a loan (asset of the owner of this object) */ Stock, /**< Denotes an security account as sub-account for an investment */ Equity, /**< Denotes an equity account e.g. opening/closeing balance */ /* insert new account types above this line */ MaxAccountTypes /**< Denotes the number of different account types */ } accountTypeE; /** * This is the constructor for a new empty account */ MyMoneyAccount(); /** * This is the constructor for a new account known to the current file * This is the only constructor that will set the attribute m_openingDate * to a correct value. * * @param id id assigned to the account * @param right account definition */ MyMoneyAccount(const QString& id, const MyMoneyAccount& right); /** * This is the constructor for an account that is described by a * QDomElement (e.g. from a file). * * @param el const reference to the QDomElement from which to * create the object */ MyMoneyAccount(const QDomElement& el); /** * This is the destructor for any MyMoneyAccount object */ ~MyMoneyAccount(); /** * This operator tests for equality of two MyMoneyAccount objects */ bool operator == (const MyMoneyAccount &) const; /** * This converts the account type into one of the four * major account types liability, asset, expense or income. * * The current assignment is as follows: * * - Asset * - Asset * - Checkings * - Savings * - Cash * - Currency * - Investment * - MoneyMarket * - CertificateDep * - AssetLoan * - Stock * * - Liability * - Liability * - CreditCard * - Loan * * - Income * - Income * * - Expense * - Expense * * @return accountTypeE of major account type */ MyMoneyAccount::accountTypeE accountGroup() const; /** * This method returns the id of the MyMoneyInstitution object this account * belongs to. * @return id of MyMoneyInstitution object. QString() if it is * an internal account * @see setInstitution */ const QString& institutionId() const { return m_institution; } /** * This method returns the name of the account * @return name of account * @see setName() */ const QString& name() const { return m_name; } /** * This method returns the number of the account at the institution * @return number of account at the institution * @see setNumber */ const QString& number() const { return m_number; } /** * This method returns the descriptive text of the account. * @return description of account * @see setDescription */ const QString& description() const { return m_description; } /** * This method returns the opening date of this account * @return date of opening of this account as const QDate value * @see setOpeningDate() */ const QDate& openingDate() const { return m_openingDate; } /** * This method returns the date of the last reconciliation of this account * @return date of last reconciliation as const QDate value * @see setLastReconciliationDate */ const QDate& lastReconciliationDate() const { return m_lastReconciliationDate; } /** * This method returns the date the account was last modified * @return date of last modification as const QDate value * @see setLastModified */ const QDate& lastModified() const { return m_lastModified; } /** * This method is used to return the ID of the parent account * @return QString with the ID of the parent of this account */ const QString& parentAccountId() const { return m_parentAccount; }; /** * This method returns the list of the account id's of * subordinate accounts * @return QStringList account ids */ const QStringList& accountList() const { return m_accountList; }; /** * This method returns the number of entries in the m_accountList * @return number of entries in the accountList */ int accountCount() const { return m_accountList.count(); }; /** * This method is used to add an account id as sub-ordinate account * @param account const QString reference to account ID */ void addAccountId(const QString& account); /** * This method is used to remove an account from the list of * sub-ordinate accounts. * @param account const QString reference to account ID to be removed */ void removeAccountId(const QString& account); /** * This method is used to remove all accounts from the list of * sub-ordinate accounts. */ void removeAccountIds(); /** * This method is used to modify the date of the last * modification access. * @param date date of last modification * @see lastModified */ void setLastModified(const QDate& date); /** * This method is used to set the name of the account * @param name name of the account * @see name */ void setName(const QString& name); /** * This method is used to set the number of the account at the institution * @param number number of the account * @see number */ void setNumber(const QString& number); /** * Return the stored account identifiers * * @internal This method is temporary until MyMoneyAccount is a MyMoneyPayeeIdentifierContainer. But before this * can happen the account numbers and iban kvp data must be moved there. */ QList< payeeIdentifier > payeeIdentifiers() const; /** * @see MyMoneyPayeeIdentifierContainer::payeeIdentifiersByType() */ template< class type > QList< ::payeeIdentifierTyped > payeeIdentifiersByType() const; /** * This method is used to set the descriptive text of the account * * @param desc text that serves as description * @see setDescription */ void setDescription(const QString& desc); /** * This method is used to set the id of the institution this account * belongs to. * * @param id id of the institution this account belongs to * @see institution */ void setInstitutionId(const QString& id); /** * This method is used to set the opening date information of an * account. * * @param date QDate of opening date * @see openingDate */ void setOpeningDate(const QDate& date); /** * This method is used to set the date of the last reconciliation * of an account. * @param date QDate of last reconciliation * @see lastReconciliationDate */ void setLastReconciliationDate(const QDate& date); /** * This method is used to change the account type * * @param type account type */ void setAccountType(const accountTypeE type); /** * This method is used to set a new parent account id * @param parent QString reference to new parent account */ void setParentAccountId(const QString& parent); /** * This method is used to update m_lastModified to the current date */ void touch() { setLastModified(QDate::currentDate()); } /** * This method returns the type of the account. */ accountTypeE accountType() const { return m_accountType; } /** * This method retrieves the id of the currency used with this account. * If the return value is empty, the base currency should be used. * * @return id of currency */ const QString& currencyId() const { return m_currencyId; }; /** * This method sets the id of the currency used with this account. * * @param id ID of currency to be associated with this account. */ void setCurrencyId(const QString& id); void writeXML(QDomDocument& document, QDomElement& parent) const; /** * This method checks if a reference to the given object exists. It returns, * a @p true if the object is referencing the one requested by the * parameter @p id. If it does not, this method returns @p false. * * @param id id of the object to be checked for references * @retval true This object references object with id @p id. * @retval false This object does not reference the object with id @p id. */ virtual bool hasReferenceTo(const QString& id) const; /** * This member returns the balance of this account based on * all transactions stored in the journal. */ const MyMoneyMoney& balance() const { return m_balance; } /** * This method adjusts the balance of this account * according to the difference contained in the split @p s. * If the s.action() is MyMoneySplit::ActionSplitShares then * the balance will be adjusted accordingly. * * @param s const reference to MyMoneySplit object containing the * value to be added/subtracted to/from the balance * @param reverse add (false) or subtract (true) the shares contained in the split. * It also affects the balance for share splits in the opposite direction. */ void adjustBalance(const MyMoneySplit& s, bool reverse = false); /** * This method sets the balance of this account * according to the value provided by @p val. * * @param val const reference to MyMoneyMoney object containing the * value to be assigned to the balance */ void setBalance(const MyMoneyMoney& val) { m_balance = val; } /** * This method sets the kvp's for online banking with this account * * @param values The container of kvp's needed when connecting to this account */ void setOnlineBankingSettings(const MyMoneyKeyValueContainer& values); /** * This method retrieves the kvp's for online banking with this account * * @return The container of kvp's needed when connecting to this account */ const MyMoneyKeyValueContainer& onlineBankingSettings() const; /** * This method sets the closed flag for the account. This is just * an informational flag for the application. It has no other influence * on the behaviour of the account object. The default for * new objects @p open. * * @param isClosed mark the account closed (@p true) or open (@p false). */ void setClosed(bool isClosed); /** * Return the closed flag for the account. * * @retval false account is marked open (the default for new accounts) * @retval true account is marked closed */ bool isClosed() const; /** * returns the applicable smallest fraction for this account * for the given security based on the account type. At the same * time, m_fraction is updated to the value returned. * * @param sec const reference to currency (security) * * @retval sec.smallestCashFraction() for account type Cash * @retval sec.smallestAccountFraction() for all other account types */ int fraction(const MyMoneySecurity& sec); /** * Same as the above method, but does not modify m_fraction. */ int fraction(const MyMoneySecurity& sec) const; /** * This method returns the stored value for the fraction of this * account or -1 if not initialized. It can be initialized by * calling fraction(const MyMoneySecurity& sec) once. * * @note Don't use this method outside of KMyMoney application context (eg. testcases). * Use the above method instead. */ int fraction() const; /** * This method returns @a true if the account type is * either Income or Expense * * @retval true account is of type income or expense * @retval false for all other account types * * @deprecated use isIncomeExpense() instead */ KMM_MYMONEY_DEPRECATED bool isCategory() const; /** * This method returns @a true if the account type is * either Income or Expense * * @retval true account is of type income or expense * @retval false for all other account types */ bool isIncomeExpense() const; /** * This method returns @a true if the account type is * either Asset or Liability * * @retval true account is of type asset or liability * @retval false for all other account types */ bool isAssetLiability() const; /** * This method returns @a true if the account type is * either AssetLoan or Loan * * @retval true account is of type Loan or AssetLoan * @retval false for all other account types */ bool isLoan() const; /** * This method returns @a true if the account type is * Stock * * @retval true account is of type Stock * @retval false for all other account types */ bool isInvest() const; /** * This method returns @a true if the account type is * Checkings, Savings or Cash * * @retval true account is of type Checkings, Savings or Cash * @retval false for all other account types */ bool isLiquidAsset() const; /** * This method returns a name that has a brokerage suffix of * the current name. It only works on investment accounts and * returns the name for all other cases. */ QString brokerageName() const; /** * @param reconcileFlag if set to @a true a reconcile overlay will be * added to the pixmap returned * @param size is a hint for the size of the icon * @return a pixmap using DesktopIcon for the account type */ QPixmap accountPixmap(bool reconcileFlag = false, int size = 0) const; /** * This method is used to convert the internal representation of * an account type into a human readable format * * @param accountType numerical representation of the account type. * For possible values, see MyMoneyAccount::accountTypeE * @return QString representing the human readable form */ static QString accountTypeToString(const MyMoneyAccount::accountTypeE accountType); /** * keeps a history record of a reconciliation for this account on @a date * with @a amount. * * @return @p true in case entry was added, @p false otherwise * * @sa reconciliationHistory() */ bool addReconciliation(const QDate& date, const MyMoneyMoney& amount); /** * @return QMap with the reconciliation history for the account * * @sa addReconciliation() */ const QMap& reconciliationHistory(); QDataStream &operator<<(const MyMoneyAccount &); QDataStream &operator>>(MyMoneyAccount &); private: /** * This member variable identifies the type of account */ accountTypeE m_accountType; /** * This member variable keeps the ID of the MyMoneyInstitution object * that this object belongs to. */ QString m_institution; /** * This member variable keeps the name of the account * It is solely for documentation purposes and it's contents is not * used otherwise by the mymoney-engine. */ QString m_name; /** * This member variable keeps the account number at the institution * It is solely for documentation purposes and it's contents is not * used otherwise by the mymoney-engine. */ QString m_number; /** * This member variable is a description of the account. * It is solely for documentation purposes and it's contents is not * used otherwise by the mymoney-engine. */ QString m_description; /** * This member variable keeps the date when the account * was last modified. */ QDate m_lastModified; /** * This member variable keeps the date when the * account was created as an object in a MyMoneyFile */ QDate m_openingDate; /** * This member variable keeps the date of the last * reconciliation of this account */ QDate m_lastReconciliationDate; /** * This member holds the ID's of all sub-ordinate accounts */ QStringList m_accountList; /** * This member contains the ID of the parent account */ QString m_parentAccount; /** * This member contains the ID of the currency associated with this account */ QString m_currencyId; /** * This member holds the balance of all transactions stored in the journal * for this account. */ MyMoneyMoney m_balance; /** * This member variable keeps the set of kvp's needed to establish * online banking sessions to this account. */ MyMoneyKeyValueContainer m_onlineBankingSettings; /** * This member keeps the fraction for the account. It is filled by MyMoneyFile * when set to -1. See also @sa fraction(const MyMoneySecurity&). */ int m_fraction; /** * This member keeps the reconciliation history */ QMap m_reconciliationHistory; }; /** * This class is a convenience class to access data for loan accounts. * It does contain the same member variables as a MyMoneyAccount object, * but serves a set of getter/setter methods to ease the access to * laon relevant data stored in the key value container of the MyMoneyAccount * object. */ class KMM_MYMONEY_EXPORT MyMoneyAccountLoan : public MyMoneyAccount { public: enum interestDueE { paymentDue = 0, paymentReceived }; enum interestChangeUnitE { changeDaily = 0, changeWeekly, changeMonthly, changeYearly }; MyMoneyAccountLoan() {} MyMoneyAccountLoan(const MyMoneyAccount&); ~MyMoneyAccountLoan() {} const MyMoneyMoney loanAmount() const; void setLoanAmount(const MyMoneyMoney& amount); const MyMoneyMoney interestRate(const QDate& date) const; void setInterestRate(const QDate& date, const MyMoneyMoney& rate); interestDueE interestCalculation() const; void setInterestCalculation(const interestDueE onReception); const QDate nextInterestChange() const; void setNextInterestChange(const QDate& date); const QString schedule() const; void setSchedule(const QString& sched); bool fixedInterestRate() const; void setFixedInterestRate(const bool fixed); const MyMoneyMoney finalPayment() const; void setFinalPayment(const MyMoneyMoney& finalPayment); unsigned int term() const; void setTerm(const unsigned int payments); int interestChangeFrequency(int* unit = 0) const; void setInterestChangeFrequency(const int amount, const int unit); const MyMoneyMoney periodicPayment() const; void setPeriodicPayment(const MyMoneyMoney& payment); int interestCompounding() const; void setInterestCompounding(int frequency); const QString payee() const; void setPayee(const QString& payee); const QString interestAccountId() const; void setInterestAccountId(const QString& id); /** * This method checks if a reference to the given object exists. It returns, * a @p true if the object is referencing the one requested by the * parameter @p id. If it does not, this method returns @p false. * * @param id id of the object to be checked for references * @retval true This object references object with id @p id. * @retval false This object does not reference the object with id @p id. */ virtual bool hasReferenceTo(const QString& id) const; }; template< class type > QList< payeeIdentifierTyped > MyMoneyAccount::payeeIdentifiersByType() const { QList< payeeIdentifierTyped > typedList; return typedList; } template<> QList< payeeIdentifierTyped< ::payeeIdentifiers::ibanBic> > MyMoneyAccount::payeeIdentifiersByType() const; /** * Make it possible to hold @ref MyMoneyAccount objects inside @ref QVariant objects. */ Q_DECLARE_METATYPE(MyMoneyAccount) +QDebug operator<<(QDebug dbg, const MyMoneyAccount &a); + #endif diff --git a/kmymoney/reports/pivotgrid.cpp b/kmymoney/reports/pivotgrid.cpp index 4b7fece21..7b82476b3 100644 --- a/kmymoney/reports/pivotgrid.cpp +++ b/kmymoney/reports/pivotgrid.cpp @@ -1,135 +1,224 @@ /*************************************************************************** pivotgrid.cpp ------------------- begin : Mon May 17 2004 copyright : (C) 2004-2005 by Ace Jones email : Thomas Baumgart ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include // ---------------------------------------------------------------------------- // QT Includes +#include + // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes namespace reports { const unsigned PivotOuterGroup::m_kDefaultSortOrder = 100; PivotCell::PivotCell(const MyMoneyMoney& value) : MyMoneyMoney(value), m_stockSplit(MyMoneyMoney::ONE), m_cellUsed(!value.isZero()) { } PivotCell::~PivotCell() { } PivotCell PivotCell::operator += (const PivotCell& right) { const MyMoneyMoney& r = static_cast(right); *this += r; m_postSplit = m_postSplit * right.m_stockSplit; m_stockSplit = m_stockSplit * right.m_stockSplit; m_postSplit += right.m_postSplit; m_cellUsed |= right.m_cellUsed; return *this; } PivotCell PivotCell::operator += (const MyMoneyMoney& value) { m_cellUsed |= !value.isZero(); if (m_stockSplit != MyMoneyMoney::ONE) m_postSplit += value; else MyMoneyMoney::operator += (value); return *this; } PivotCell PivotCell::stockSplit(const MyMoneyMoney& factor) { PivotCell s; s.m_stockSplit = factor; return s; } const QString PivotCell::formatMoney(int fraction, bool showThousandSeparator) const { return formatMoney("", MyMoneyMoney::denomToPrec(fraction), showThousandSeparator); } const QString PivotCell::formatMoney(const QString& currency, const int prec, bool showThousandSeparator) const { // construct the result MyMoneyMoney res = (*this * m_stockSplit) + m_postSplit; return res.formatMoney(currency, prec, showThousandSeparator); } MyMoneyMoney PivotCell::calculateRunningSum(const MyMoneyMoney& runningSum) { MyMoneyMoney::operator += (runningSum); MyMoneyMoney::operator = ((*this * m_stockSplit) + m_postSplit); m_postSplit = MyMoneyMoney(); m_stockSplit = MyMoneyMoney::ONE; return *this; } MyMoneyMoney PivotCell::cellBalance(const MyMoneyMoney& _balance) { MyMoneyMoney balance(_balance); balance += *this; balance = (balance * m_stockSplit) + m_postSplit; return balance; } PivotGridRowSet::PivotGridRowSet(unsigned _numcolumns) { insert(eActual, PivotGridRow(_numcolumns)); insert(eBudget, PivotGridRow(_numcolumns)); insert(eBudgetDiff, PivotGridRow(_numcolumns)); insert(eForecast, PivotGridRow(_numcolumns)); insert(eAverage, PivotGridRow(_numcolumns)); insert(ePrice, PivotGridRow(_numcolumns)); } PivotGridRowSet PivotGrid::rowSet(QString id) { //go through the data and get the row that matches the id PivotGrid::iterator it_outergroup = begin(); while (it_outergroup != end()) { PivotOuterGroup::iterator it_innergroup = (*it_outergroup).begin(); while (it_innergroup != (*it_outergroup).end()) { PivotInnerGroup::iterator it_row = (*it_innergroup).begin(); while (it_row != (*it_innergroup).end()) { if (it_row.key().id() == id) return it_row.value(); ++it_row; } ++it_innergroup; } ++it_outergroup; } return PivotGridRowSet(); } } // namespace + + +QDebug operator<<(QDebug dbg, reports::PivotGrid &a) +{ + dbg.nospace() << "reports::PivotGrid(" + << "QMap("; + foreach(const QString &key, a.keys()) { + dbg.nospace() << key << ": " << a[key] << ","; + } + dbg << ")" + << ")"; + return dbg; +} + +QDebug operator<<(QDebug dbg, const reports::PivotCell &a) +{ + dbg.space() << "reports::PivotCell(" + //;dbg = operator<<(dbg, static_cast(a)) + << "MyMoneyMoney(" + << "isAutoCalc" << a.isAutoCalc() + << "isNegative" << a.isNegative() + << "isPositive" << a.isPositive() + << "isZero" << a.isZero() + << "value" << a.toString() + << ")" + << ")"; + return dbg; +} + +QDebug operator<<(QDebug dbg, const reports::ReportAccount &a) +{ + dbg.space() << "reports::ReportAccount(" + //;dbg = operator<<(dbg, static_cast(a)) + << "id" << a.id() + << "accountType" << MyMoneyAccount::accountTypeToString(a.accountType()) + << ")"; + return dbg; +} + +QDebug operator<<(QDebug dbg, const QMap &a) +{ + dbg.space() << "reports::PivotOuterGroup(" + << "QMap("; + foreach(const QString &key, a.keys()) { + dbg << key << ":" << a[key] << ","; + } + dbg << ")" + << ")"; + return dbg; +} + +QDebug operator<<(QDebug dbg, const QMap &a) +{ + dbg.space() << "reports::PivotInnerGroup(" + << "QMap("; + foreach(const QString &key, a.keys()) { + dbg << key << ":" << a[key] << ","; + } + dbg << ")" + << ")"; + return dbg; +} + +QDebug operator<<(QDebug dbg, const QMap &a) +{ + dbg.space() << "reports::PivotGridRowSet(" + << "QMap("; + foreach(const reports::ReportAccount &key, a.keys()) { + dbg << key << ":" << a[key] << ","; + } + dbg << ")" + << ")"; + return dbg; +} + +QDebug operator<<(QDebug dbg, const QMap &a) +{ + dbg.space() << "reports::PivotGridRow(" + << "QMap("; + foreach(const reports::ERowType &key, a.keys()) { + dbg << key << ":" << a[key] << ","; + } + dbg << ")" + << ")"; + return dbg; +} + diff --git a/kmymoney/reports/pivotgrid.h b/kmymoney/reports/pivotgrid.h index 020cb53d5..e2376041c 100644 --- a/kmymoney/reports/pivotgrid.h +++ b/kmymoney/reports/pivotgrid.h @@ -1,154 +1,161 @@ /*************************************************************************** pivotgrid.h ------------------- begin : Sat May 22 2004 copyright : (C) 2004-2005 by Ace Jones email : Thomas Baumgart ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #ifndef PIVOTGRID_H #define PIVOTGRID_H // ---------------------------------------------------------------------------- // QT Includes #include #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes #include "reportaccount.h" namespace reports { enum ERowType {eActual, eBudget, eBudgetDiff, eForecast, eAverage, ePrice }; /** * The fundamental data construct of this class is a 'grid'. It is organized as follows: * * A 'Row' is a row of money values, each column is a month. The first month corresponds to * m_beginDate. * * A 'Row Pair' is two rows of money values. Each column is the SAME month. One row is the * 'actual' values for the period, the other row is the 'budgetted' values for the same * period. For ease of implementation, a Row Pair is implemented as a Row which contains * another Row. The inherited Row is the 'actual', the contained row is the 'Budget'. * * An 'Inner Group' contains a rows for each subordinate account within a single top-level * account. It also contains a mapping from the account descriptor for the subordinate account * to its row data. So if we have an Expense account called "Computers", with sub-accounts called * "Hardware", "Software", and "Peripherals", there will be one Inner Group for "Computers" * which contains three Rows. * * An 'Outer Group' contains Inner Row Groups for all the top-level accounts in a given * account class. Account classes are Expense, Income, Asset, Liability. In the case above, * the "Computers" Inner Group is contained within the "Expense" Outer Group. * * A 'Grid' is the set of all Outer Groups contained in this report. * */ class PivotCell: public MyMoneyMoney { KMM_MYMONEY_UNIT_TESTABLE public: PivotCell() : m_stockSplit(MyMoneyMoney::ONE), m_cellUsed(false) {} PivotCell(const MyMoneyMoney& value); virtual ~PivotCell(); static PivotCell stockSplit(const MyMoneyMoney& factor); PivotCell operator += (const PivotCell& right); PivotCell operator += (const MyMoneyMoney& value); const QString formatMoney(int fraction, bool showThousandSeparator = true) const; const QString formatMoney(const QString& currency, const int prec, bool showThousandSeparator = true) const; MyMoneyMoney calculateRunningSum(const MyMoneyMoney& runningSum); MyMoneyMoney cellBalance(const MyMoneyMoney& _balance); bool isUsed() const { return m_cellUsed; } private: MyMoneyMoney m_stockSplit; MyMoneyMoney m_postSplit; bool m_cellUsed; }; class PivotGridRow: public QList { public: PivotGridRow(unsigned _numcolumns = 0) { for (uint i = 0; i < _numcolumns; i++) append(PivotCell()); } MyMoneyMoney m_total; }; class PivotGridRowSet: public QMap { public: PivotGridRowSet(unsigned _numcolumns = 0); }; class PivotInnerGroup: public QMap { public: PivotInnerGroup(unsigned _numcolumns = 0): m_total(_numcolumns) {} PivotGridRowSet m_total; }; class PivotOuterGroup: public QMap { public: explicit PivotOuterGroup(unsigned _numcolumns = 0, unsigned _sort = m_kDefaultSortOrder, bool _inverted = false): m_total(_numcolumns), m_inverted(_inverted), m_sortOrder(_sort) {} bool operator<(const PivotOuterGroup& _right) const { if (m_sortOrder != _right.m_sortOrder) return m_sortOrder < _right.m_sortOrder; else return m_displayName < _right.m_displayName; } PivotGridRowSet m_total; // An inverted outergroup means that all values placed in subordinate rows // should have their sign inverted from typical cash-flow notation. Also it // means that when the report is summed, the values should be inverted again // so that the grand total is really "non-inverted outergroup MINUS inverted outergroup". bool m_inverted; // The localized name of the group for display in the report. Outergoups need this // independently, because they will lose their association with the TGrid when the // report is rendered. QString m_displayName; // lower numbers sort toward the top of the report. defaults to 100, which is a nice // middle-of-the-road value unsigned m_sortOrder; // default sort order static const unsigned m_kDefaultSortOrder; }; class PivotGrid: public QMap { public: PivotGridRowSet rowSet(QString id); PivotGridRowSet m_total; }; } +QDebug operator<<(QDebug dbg, reports::PivotGrid &a); +QDebug operator<<(QDebug dbg, const reports::PivotCell &a); +QDebug operator<<(QDebug dbg, const reports::ReportAccount &a); +QDebug operator<<(QDebug dbg, const QMap &a); +QDebug operator<<(QDebug dbg, const QMap &a); +QDebug operator<<(QDebug dbg, const QMap &a); +QDebug operator<<(QDebug dbg, const QMap &a); #endif // PIVOTGRID_H diff --git a/kmymoney/reports/pivottable.cpp b/kmymoney/reports/pivottable.cpp index fe83dd8c1..5fa7c8b81 100644 --- a/kmymoney/reports/pivottable.cpp +++ b/kmymoney/reports/pivottable.cpp @@ -1,2294 +1,2303 @@ /*************************************************************************** pivottable.cpp ------------------- begin : Mon May 17 2004 copyright : (C) 2004-2005 by Ace Jones email : Thomas Baumgart Alvaro Soliverez ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "pivottable.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "pivotgrid.h" #include "reportdebug.h" #include "kreportchartview.h" #include "kmymoneyglobalsettings.h" #include "kmymoneyutils.h" #include "mymoneyforecast.h" #include namespace reports { using KDChart::Widget; QString Debug::m_sTabs; bool Debug::m_sEnabled = DEBUG_ENABLED_BY_DEFAULT; QString Debug::m_sEnableKey; Debug::Debug(const QString& _name): m_methodName(_name), m_enabled(m_sEnabled) { if (!m_enabled && _name == m_sEnableKey) m_enabled = true; if (m_enabled) { qDebug("%s%s(): ENTER", qPrintable(m_sTabs), qPrintable(m_methodName)); m_sTabs.append("--"); } } Debug::~Debug() { if (m_enabled) { m_sTabs.remove(0, 2); qDebug("%s%s(): EXIT", qPrintable(m_sTabs), qPrintable(m_methodName)); if (m_methodName == m_sEnableKey) m_enabled = false; } } void Debug::output(const QString& _text) { if (m_enabled) qDebug("%s%s(): %s", qPrintable(m_sTabs), qPrintable(m_methodName), qPrintable(_text)); } PivotTable::PivotTable(const MyMoneyReport& _config_f): ReportTable(), m_runningSumsCalculated(false), m_config_f(_config_f) { init(); } void PivotTable::init() { DEBUG_ENTER(Q_FUNC_INFO); // // Initialize locals // MyMoneyFile* file = MyMoneyFile::instance(); // // Initialize member variables // //make sure we have all subaccounts of investment accounts includeInvestmentSubAccounts(); m_config_f.validDateRange(m_beginDate, m_endDate); // If we need to calculate running sums, it does not make sense // to show a row total column if (m_config_f.isRunningSum()) m_config_f.setShowingRowTotals(false); // if this is a months-based report if (! m_config_f.isColumnsAreDays()) { // strip out the 'days' component of the begin and end dates. // we're only using these variables to contain year and month. m_beginDate = QDate(m_beginDate.year(), m_beginDate.month(), 1); m_endDate = QDate(m_endDate.year(), m_endDate.month(), 1); } m_numColumns = columnValue(m_endDate) - columnValue(m_beginDate) + 2; //Load what types of row the report is going to show loadRowTypeList(); // // Initialize outer groups of the grid // if (m_config_f.rowType() == MyMoneyReport::Row::AssetLiability) { m_grid.insert(KMyMoneyUtils::accountTypeToString(MyMoneyAccount::Asset), PivotOuterGroup(m_numColumns)); m_grid.insert(KMyMoneyUtils::accountTypeToString(MyMoneyAccount::Liability), PivotOuterGroup(m_numColumns, PivotOuterGroup::m_kDefaultSortOrder, true /* inverted */)); } else { m_grid.insert(KMyMoneyUtils::accountTypeToString(MyMoneyAccount::Income), PivotOuterGroup(m_numColumns, PivotOuterGroup::m_kDefaultSortOrder - 2)); m_grid.insert(KMyMoneyUtils::accountTypeToString(MyMoneyAccount::Expense), PivotOuterGroup(m_numColumns, PivotOuterGroup::m_kDefaultSortOrder - 1, true /* inverted */)); // // Create rows for income/expense reports with all accounts included // if (m_config_f.isIncludingUnusedAccounts()) createAccountRows(); } // // Initialize grid totals // m_grid.m_total = PivotGridRowSet(m_numColumns); // // Get opening balances // (for running sum reports only) // if (m_config_f.isRunningSum()) calculateOpeningBalances(); // // Calculate budget mapping // (for budget reports only) // if (m_config_f.hasBudget()) calculateBudgetMapping(); // // Populate all transactions into the row/column pivot grid // QList transactions; m_config_f.setReportAllSplits(false); m_config_f.setConsiderCategory(true); try { transactions = file->transactionList(m_config_f); } catch (const MyMoneyException &e) { qDebug("ERR: %s thrown in %s(%ld)", qPrintable(e.what()), qPrintable(e.file()), e.line()); throw e; } DEBUG_OUTPUT(QString("Found %1 matching transactions").arg(transactions.count())); // Include scheduled transactions if required if (m_config_f.isIncludingSchedules()) { // Create a custom version of the report filter, excluding date // We'll use this to compare the transaction against MyMoneyTransactionFilter schedulefilter(m_config_f); schedulefilter.setDateFilter(QDate(), QDate()); // Get the real dates from the config filter QDate configbegin, configend; m_config_f.validDateRange(configbegin, configend); QList schedules = file->scheduleList(); QList::const_iterator it_schedule = schedules.constBegin(); while (it_schedule != schedules.constEnd()) { // If the transaction meets the filter MyMoneyTransaction tx = (*it_schedule).transaction(); if (!(*it_schedule).isFinished() && schedulefilter.match(tx)) { // Keep the id of the schedule with the transaction so that // we can do the autocalc later on in case of a loan payment tx.setValue("kmm-schedule-id", (*it_schedule).id()); // Get the dates when a payment will be made within the report window QDate nextpayment = (*it_schedule).adjustedNextPayment(configbegin); if (nextpayment.isValid()) { // Add one transaction for each date QList paymentDates = (*it_schedule).paymentDates(nextpayment, configend); QList::const_iterator it_date = paymentDates.constBegin(); while (it_date != paymentDates.constEnd()) { //if the payment occurs in the past, enter it tomorrow if (QDate::currentDate() >= *it_date) { tx.setPostDate(QDate::currentDate().addDays(1)); } else { tx.setPostDate(*it_date); } if (tx.postDate() <= configend && tx.postDate() >= configbegin) { transactions += tx; } DEBUG_OUTPUT(QString("Added transaction for schedule %1 on %2").arg((*it_schedule).id()).arg((*it_date).toString())); ++it_date; } } } ++it_schedule; } } // whether asset & liability transactions are actually to be considered // transfers bool al_transfers = (m_config_f.rowType() == MyMoneyReport::Row::ExpenseIncome) && (m_config_f.isIncludingTransfers()); //this is to store balance for loan accounts when not included in the report QMap loanBalances; QList::const_iterator it_transaction = transactions.constBegin(); int colofs = columnValue(m_beginDate) - 1; while (it_transaction != transactions.constEnd()) { MyMoneyTransaction tx = (*it_transaction); QDate postdate = tx.postDate(); if (postdate < m_beginDate) { qDebug("MyMoneyFile::transactionList returned a transaction that is outside the date filter, skipping it"); ++it_transaction; continue; } int column = columnValue(postdate) - colofs; // check if we need to call the autocalculation routine if (tx.isLoanPayment() && tx.hasAutoCalcSplit() && (tx.value("kmm-schedule-id").length() > 0)) { // make sure to consider any autocalculation for loan payments MyMoneySchedule sched = file->schedule(tx.value("kmm-schedule-id")); const MyMoneySplit& split = tx.amortizationSplit(); if (!split.id().isEmpty()) { ReportAccount splitAccount = file->account(split.accountId()); MyMoneyAccount::accountTypeE type = splitAccount.accountGroup(); QString outergroup = KMyMoneyUtils::accountTypeToString(type); //if the account is included in the report, calculate the balance from the cells if (m_config_f.includes(splitAccount)) { loanBalances[splitAccount.id()] = cellBalance(outergroup, splitAccount, column, false); } else { //if it is not in the report and also not in loanBalances, get the balance from the file if (!loanBalances.contains(splitAccount.id())) { QDate dueDate = sched.nextDueDate(); //if the payment is overdue, use current date if (dueDate < QDate::currentDate()) dueDate = QDate::currentDate(); //get the balance from the file for the date loanBalances[splitAccount.id()] = file->balance(splitAccount.id(), dueDate.addDays(-1)); } } KMyMoneyUtils::calculateAutoLoan(sched, tx, loanBalances); //if the loan split is not included in the report, update the balance for the next occurrence if (!m_config_f.includes(splitAccount)) { QList::ConstIterator it_loanSplits; for (it_loanSplits = tx.splits().constBegin(); it_loanSplits != tx.splits().constEnd(); ++it_loanSplits) { if ((*it_loanSplits).isAmortizationSplit() && (*it_loanSplits).accountId() == splitAccount.id()) loanBalances[splitAccount.id()] = loanBalances[splitAccount.id()] + (*it_loanSplits).shares(); } } } } QList splits = tx.splits(); QList::const_iterator it_split = splits.constBegin(); while (it_split != splits.constEnd()) { ReportAccount splitAccount = (*it_split).accountId(); // Each split must be further filtered, because if even one split matches, // the ENTIRE transaction is returned with all splits (even non-matching ones) if (m_config_f.includes(splitAccount) && m_config_f.match(&(*it_split))) { // reverse sign to match common notation for cash flow direction, only for expense/income splits MyMoneyMoney reverse(splitAccount.isIncomeExpense() ? -1 : 1, 1); MyMoneyMoney value; // the outer group is the account class (major account type) MyMoneyAccount::accountTypeE type = splitAccount.accountGroup(); QString outergroup = KMyMoneyUtils::accountTypeToString(type); value = (*it_split).shares(); bool stockSplit = tx.isStockSplit(); if (!stockSplit) { // retrieve the value in the account's underlying currency if (value != MyMoneyMoney::autoCalc) { value = value * reverse; } else { qDebug("PivotTable::PivotTable(): This must not happen"); value = MyMoneyMoney(); // keep it 0 so far } // Except in the case of transfers on an income/expense report if (al_transfers && (type == MyMoneyAccount::Asset || type == MyMoneyAccount::Liability)) { outergroup = i18n("Transfers"); value = -value; } } // add the value to its correct position in the pivot table assignCell(outergroup, splitAccount, column, value, false, stockSplit); } ++it_split; } ++it_transaction; } // // Get forecast data // if (m_config_f.isIncludingForecast()) calculateForecast(); // //Insert Price data // if (m_config_f.isIncludingPrice()) fillBasePriceUnit(ePrice); // //Insert Average Price data // if (m_config_f.isIncludingAveragePrice()) { fillBasePriceUnit(eActual); calculateMovingAverage(); } // // Collapse columns to match column type // if (m_config_f.columnPitch() > 1) collapseColumns(); // // Calculate the running sums // (for running sum reports only) // if (m_config_f.isRunningSum()) calculateRunningSums(); // // Calculate Moving Average // if (m_config_f.isIncludingMovingAverage()) calculateMovingAverage(); // // Calculate Budget Difference // if (m_config_f.isIncludingBudgetActuals()) calculateBudgetDiff(); // // Convert all values to the deep currency // convertToDeepCurrency(); // // Convert all values to the base currency // if (m_config_f.isConvertCurrency()) convertToBaseCurrency(); // // Determine column headings // calculateColumnHeadings(); // // Calculate row and column totals // calculateTotals(); // // If using mixed time, calculate column for current date // m_config_f.setCurrentDateColumn(currentDateColumn()); } void PivotTable::collapseColumns() { DEBUG_ENTER(Q_FUNC_INFO); int columnpitch = m_config_f.columnPitch(); if (columnpitch != 1) { int sourcemonth = (m_config_f.isColumnsAreDays()) // use the user's locale to determine the week's start ? (m_beginDate.dayOfWeek() + 8 - KGlobal::locale()->weekStartDay()) % 7 : m_beginDate.month(); int sourcecolumn = 1; int destcolumn = 1; while (sourcecolumn < m_numColumns) { if (sourcecolumn != destcolumn) { #if 0 // TODO: Clean up this rather inefficient kludge. We really should jump by an entire // destcolumn at a time on RS reports, and calculate the proper sourcecolumn to use, // allowing us to clear and accumulate only ONCE per destcolumn if (m_config_f.isRunningSum()) clearColumn(destcolumn); #endif accumulateColumn(destcolumn, sourcecolumn); } if (++sourcecolumn < m_numColumns) { if ((sourcemonth++ % columnpitch) == 0) { if (sourcecolumn != ++destcolumn) clearColumn(destcolumn); } } } m_numColumns = destcolumn + 1; } } void PivotTable::accumulateColumn(int destcolumn, int sourcecolumn) { DEBUG_ENTER(Q_FUNC_INFO); DEBUG_OUTPUT(QString("From Column %1 to %2").arg(sourcecolumn).arg(destcolumn)); // iterate over outer groups PivotGrid::iterator it_outergroup = m_grid.begin(); while (it_outergroup != m_grid.end()) { // iterate over inner groups PivotOuterGroup::iterator it_innergroup = (*it_outergroup).begin(); while (it_innergroup != (*it_outergroup).end()) { // iterator over rows PivotInnerGroup::iterator it_row = (*it_innergroup).begin(); while (it_row != (*it_innergroup).end()) { if ((*it_row)[eActual].count() <= sourcecolumn) throw MYMONEYEXCEPTION(QString("Sourcecolumn %1 out of grid range (%2) in PivotTable::accumulateColumn").arg(sourcecolumn).arg((*it_row)[eActual].count())); if ((*it_row)[eActual].count() <= destcolumn) throw MYMONEYEXCEPTION(QString("Destcolumn %1 out of grid range (%2) in PivotTable::accumulateColumn").arg(sourcecolumn).arg((*it_row)[eActual].count())); (*it_row)[eActual][destcolumn] += (*it_row)[eActual][sourcecolumn]; ++it_row; } ++it_innergroup; } ++it_outergroup; } } void PivotTable::clearColumn(int column) { DEBUG_ENTER(Q_FUNC_INFO); DEBUG_OUTPUT(QString("Column %1").arg(column)); // iterate over outer groups PivotGrid::iterator it_outergroup = m_grid.begin(); while (it_outergroup != m_grid.end()) { // iterate over inner groups PivotOuterGroup::iterator it_innergroup = (*it_outergroup).begin(); while (it_innergroup != (*it_outergroup).end()) { // iterator over rows PivotInnerGroup::iterator it_row = (*it_innergroup).begin(); while (it_row != (*it_innergroup).end()) { if ((*it_row)[eActual].count() <= column) throw MYMONEYEXCEPTION(QString("Column %1 out of grid range (%2) in PivotTable::accumulateColumn").arg(column).arg((*it_row)[eActual].count())); (*it_row++)[eActual][column] = PivotCell(); } ++it_innergroup; } ++it_outergroup; } } void PivotTable::calculateColumnHeadings() { DEBUG_ENTER(Q_FUNC_INFO); // one column for the opening balance m_columnHeadings.append("Opening"); int columnpitch = m_config_f.columnPitch(); if (columnpitch == 0) { // output the warning but don't crash by dividing with 0 qWarning("PivotTable::calculateColumnHeadings() Invalid column pitch"); return; } // if this is a days-based report if (m_config_f.isColumnsAreDays()) { if (columnpitch == 1) { QDate columnDate = m_beginDate; int column = 1; while (column++ < m_numColumns) { QString heading = KGlobal::locale()->calendar()->monthName(columnDate.month(), columnDate.year(), KCalendarSystem::ShortName) + ' ' + QString::number(columnDate.day()); columnDate = columnDate.addDays(1); m_columnHeadings.append(heading); } } else { QDate day = m_beginDate; QDate prv = m_beginDate; // use the user's locale to determine the week's start int dow = (day.dayOfWeek() + 8 - KGlobal::locale()->weekStartDay()) % 7; while (day <= m_endDate) { if (((dow % columnpitch) == 0) || (day == m_endDate)) { m_columnHeadings.append(QString("%1 %2 - %3 %4") .arg(KGlobal::locale()->calendar()->monthName(prv.month(), prv.year(), KCalendarSystem::ShortName)) .arg(prv.day()) .arg(KGlobal::locale()->calendar()->monthName(day.month(), day.year(), KCalendarSystem::ShortName)) .arg(day.day())); prv = day.addDays(1); } day = day.addDays(1); dow++; } } } // else it's a months-based report else { if (columnpitch == 12) { int year = m_beginDate.year(); int column = 1; while (column++ < m_numColumns) m_columnHeadings.append(QString::number(year++)); } else { int year = m_beginDate.year(); bool includeyear = (m_beginDate.year() != m_endDate.year()); int segment = (m_beginDate.month() - 1) / columnpitch; int column = 1; while (column++ < m_numColumns) { QString heading = KGlobal::locale()->calendar()->monthName(1 + segment * columnpitch, 2000, KCalendarSystem::ShortName); if (columnpitch != 1) heading += '-' + KGlobal::locale()->calendar()->monthName((1 + segment) * columnpitch, 2000, KCalendarSystem::ShortName); if (includeyear) heading += ' ' + QString::number(year); m_columnHeadings.append(heading); if (++segment >= 12 / columnpitch) { segment -= 12 / columnpitch; ++year; } } } } } void PivotTable::createAccountRows() { DEBUG_ENTER(Q_FUNC_INFO); MyMoneyFile* file = MyMoneyFile::instance(); QList accounts; file->accountList(accounts); QList::const_iterator it_account = accounts.constBegin(); while (it_account != accounts.constEnd()) { ReportAccount account = *it_account; // only include this item if its account group is included in this report // and if the report includes this account if (m_config_f.includes(*it_account)) { DEBUG_OUTPUT(QString("Includes account %1").arg(account.name())); // the row group is the account class (major account type) QString outergroup = KMyMoneyUtils::accountTypeToString(account.accountGroup()); // place into the 'opening' column... assignCell(outergroup, account, 0, MyMoneyMoney()); } ++it_account; } } void PivotTable::calculateOpeningBalances() { DEBUG_ENTER(Q_FUNC_INFO); // First, determine the inclusive dates of the report. Normally, that's just // the begin & end dates of m_config_f. However, if either of those dates are // blank, we need to use m_beginDate and/or m_endDate instead. QDate from = m_config_f.fromDate(); QDate to = m_config_f.toDate(); if (! from.isValid()) from = m_beginDate; if (! to.isValid()) to = m_endDate; MyMoneyFile* file = MyMoneyFile::instance(); QList accounts; file->accountList(accounts); QList::const_iterator it_account = accounts.constBegin(); while (it_account != accounts.constEnd()) { ReportAccount account = *it_account; // only include this item if its account group is included in this report // and if the report includes this account if (m_config_f.includes(*it_account)) { //do not include account if it is closed and it has no transactions in the report period if (account.isClosed()) { //check if the account has transactions for the report timeframe MyMoneyTransactionFilter filter; filter.addAccount(account.id()); filter.setDateFilter(m_beginDate, m_endDate); filter.setReportAllSplits(false); QList transactions = file->transactionList(filter); //if a closed account has no transactions in that timeframe, do not include it if (transactions.size() == 0) { DEBUG_OUTPUT(QString("DOES NOT INCLUDE account %1").arg(account.name())); ++it_account; continue; } } DEBUG_OUTPUT(QString("Includes account %1").arg(account.name())); // the row group is the account class (major account type) QString outergroup = KMyMoneyUtils::accountTypeToString(account.accountGroup()); // extract the balance of the account for the given begin date, which is // the opening balance plus the sum of all transactions prior to the begin // date // this is in the underlying currency MyMoneyMoney value = file->balance(account.id(), from.addDays(-1)); // place into the 'opening' column... assignCell(outergroup, account, 0, value); } else { DEBUG_OUTPUT(QString("DOES NOT INCLUDE account %1").arg(account.name())); } ++it_account; } } void PivotTable::calculateRunningSums(PivotInnerGroup::iterator& it_row) { MyMoneyMoney runningsum = it_row.value()[eActual][0].calculateRunningSum(MyMoneyMoney()); int column = 1; while (column < m_numColumns) { if (it_row.value()[eActual].count() <= column) throw MYMONEYEXCEPTION(QString("Column %1 out of grid range (%2) in PivotTable::calculateRunningSums").arg(column).arg(it_row.value()[eActual].count())); runningsum = it_row.value()[eActual][column].calculateRunningSum(runningsum); ++column; } } void PivotTable::calculateRunningSums() { DEBUG_ENTER(Q_FUNC_INFO); m_runningSumsCalculated = true; PivotGrid::iterator it_outergroup = m_grid.begin(); while (it_outergroup != m_grid.end()) { PivotOuterGroup::iterator it_innergroup = (*it_outergroup).begin(); while (it_innergroup != (*it_outergroup).end()) { PivotInnerGroup::iterator it_row = (*it_innergroup).begin(); while (it_row != (*it_innergroup).end()) { #if 0 MyMoneyMoney runningsum = it_row.value()[0]; int column = 1; while (column < m_numColumns) { if (it_row.value()[eActual].count() <= column) throw MYMONEYEXCEPTION(QString("Column %1 out of grid range (%2) in PivotTable::calculateRunningSums").arg(column).arg(it_row.value()[eActual].count())); runningsum = (it_row.value()[eActual][column] += runningsum); ++column; } #endif calculateRunningSums(it_row); ++it_row; } ++it_innergroup; } ++it_outergroup; } } MyMoneyMoney PivotTable::cellBalance(const QString& outergroup, const ReportAccount& _row, int _column, bool budget) { if (m_runningSumsCalculated) { qDebug("You must not call PivotTable::cellBalance() after calling PivotTable::calculateRunningSums()"); throw MYMONEYEXCEPTION(QString("You must not call PivotTable::cellBalance() after calling PivotTable::calculateRunningSums()")); } // for budget reports, if this is the actual value, map it to the account which // holds its budget ReportAccount row = _row; if (!budget && m_config_f.hasBudget()) { QString newrow = m_budgetMap[row.id()]; // if there was no mapping found, then the budget report is not interested // in this account. if (newrow.isEmpty()) return MyMoneyMoney(); row = newrow; } // ensure the row already exists (and its parental hierarchy) createRow(outergroup, row, true); // Determine the inner group from the top-most parent account QString innergroup(row.topParentName()); if (m_numColumns <= _column) throw MYMONEYEXCEPTION(QString("Column %1 out of m_numColumns range (%2) in PivotTable::cellBalance").arg(_column).arg(m_numColumns)); if (m_grid[outergroup][innergroup][row][eActual].count() <= _column) throw MYMONEYEXCEPTION(QString("Column %1 out of grid range (%2) in PivotTable::cellBalance").arg(_column).arg(m_grid[outergroup][innergroup][row][eActual].count())); MyMoneyMoney balance; if (budget) balance = m_grid[outergroup][innergroup][row][eBudget][0].cellBalance(MyMoneyMoney()); else balance = m_grid[outergroup][innergroup][row][eActual][0].cellBalance(MyMoneyMoney()); int column = 1; while (column < _column) { if (m_grid[outergroup][innergroup][row][eActual].count() <= column) throw MYMONEYEXCEPTION(QString("Column %1 out of grid range (%2) in PivotTable::cellBalance").arg(column).arg(m_grid[outergroup][innergroup][row][eActual].count())); balance = m_grid[outergroup][innergroup][row][eActual][column].cellBalance(balance); ++column; } return balance; } void PivotTable::calculateBudgetMapping() { DEBUG_ENTER(Q_FUNC_INFO); MyMoneyFile* file = MyMoneyFile::instance(); // Only do this if there is at least one budget in the file if (file->countBudgets()) { // Select a budget // // It will choose the first budget in the list for the start year of the report if no budget is selected MyMoneyBudget budget = MyMoneyBudget(); QList budgets = file->budgetList(); bool validBudget = false; //check that the selected budget is valid if (m_config_f.budget() != "Any") { QList::const_iterator budgets_it = budgets.constBegin(); while (budgets_it != budgets.constEnd()) { //pick the budget by id if ((*budgets_it).id() == m_config_f.budget()) { budget = file->budget((*budgets_it).id()); validBudget = true; break; } ++budgets_it; } } //if no valid budget has been selected if (!validBudget) { //if the budget list is empty, just return if (budgets.count() == 0) { return; } QList::const_iterator budgets_it = budgets.constBegin(); while (budgets_it != budgets.constEnd()) { //pick the first budget that matches the report start year if ((*budgets_it).budgetStart().year() == QDate::currentDate().year()) { budget = file->budget((*budgets_it).id()); break; } ++budgets_it; } //if it can't find a matching budget, take the first one on the list if (budget.id().isEmpty()) { budget = budgets[0]; } //assign the budget to the report m_config_f.setBudget(budget.id(), m_config_f.isIncludingBudgetActuals()); } // Dump the budget //kDebug(2) << "Budget " << budget.name() << ": "; // Go through all accounts in the system to build the mapping QList accounts; file->accountList(accounts); QList::const_iterator it_account = accounts.constBegin(); while (it_account != accounts.constEnd()) { //include only the accounts selected for the report if (m_config_f.includes(*it_account)) { QString id = (*it_account).id(); QString acid = id; // If the budget contains this account outright if (budget.contains(id)) { // Add it to the mapping m_budgetMap[acid] = id; // kDebug(2) << ReportAccount(acid).debugName() << " self-maps / type =" << budget.account(id).budgetLevel(); } // Otherwise, search for a parent account which includes sub-accounts else { //if includeBudgetActuals, include all accounts regardless of whether in budget or not if (m_config_f.isIncludingBudgetActuals()) { m_budgetMap[acid] = id; // kDebug(2) << ReportAccount(acid).debugName() << " maps to " << ReportAccount(id).debugName(); } do { id = file->account(id).parentAccountId(); if (budget.contains(id)) { if (budget.account(id).budgetSubaccounts()) { m_budgetMap[acid] = id; // kDebug(2) << ReportAccount(acid).debugName() << " maps to " << ReportAccount(id).debugName(); break; } } } while (! id.isEmpty()); } } ++it_account; } // end while looping through the accounts in the file // Place the budget values into the budget grid QList baccounts = budget.getaccounts(); QList::const_iterator it_bacc = baccounts.constBegin(); while (it_bacc != baccounts.constEnd()) { ReportAccount splitAccount = (*it_bacc).id(); //include the budget account only if it is included in the report if (m_config_f.includes(splitAccount)) { MyMoneyAccount::accountTypeE type = splitAccount.accountGroup(); QString outergroup = KMyMoneyUtils::accountTypeToString(type); // reverse sign to match common notation for cash flow direction, only for expense/income splits MyMoneyMoney reverse((splitAccount.accountType() == MyMoneyAccount::Expense) ? -1 : 1, 1); const QMap& periods = (*it_bacc).getPeriods(); // skip the account if it has no periods if (periods.count() < 1) { ++it_bacc; continue; } MyMoneyMoney value = (*periods.begin()).amount() * reverse; int column = 1; // based on the kind of budget it is, deal accordingly switch ((*it_bacc).budgetLevel()) { case MyMoneyBudget::AccountGroup::eYearly: // divide the single yearly value by 12 and place it in each column value /= MyMoneyMoney(12, 1); case MyMoneyBudget::AccountGroup::eNone: case MyMoneyBudget::AccountGroup::eMax: case MyMoneyBudget::AccountGroup::eMonthly: // place the single monthly value in each column of the report // only add the value if columns are monthly or longer if (m_config_f.columnType() == MyMoneyReport::Column::BiMonths || m_config_f.columnType() == MyMoneyReport::Column::Months || m_config_f.columnType() == MyMoneyReport::Column::Years || m_config_f.columnType() == MyMoneyReport::Column::Quarters) { QDate budgetDate = budget.budgetStart(); while (column < m_numColumns && budget.budgetStart().addYears(1) > budgetDate) { //only show budget values if the budget year and the column date match //no currency conversion is done here because that is done for all columns later if (budgetDate > columnDate(column)) { ++column; } else { if (budgetDate >= m_beginDate.addDays(-m_beginDate.day() + 1) && budgetDate <= m_endDate.addDays(m_endDate.daysInMonth() - m_endDate.day()) && budgetDate > (columnDate(column).addMonths(-m_config_f.columnType()))) { assignCell(outergroup, splitAccount, column, value, true /*budget*/); } budgetDate = budgetDate.addMonths(1); } } } break; case MyMoneyBudget::AccountGroup::eMonthByMonth: // place each value in the appropriate column // budget periods are supposed to come in order just like columns { QMap::const_iterator it_period = periods.begin(); while (it_period != periods.end() && column < m_numColumns) { if ((*it_period).startDate() > columnDate(column)) { ++column; } else { switch (m_config_f.columnType()) { case MyMoneyReport::Column::Years: case MyMoneyReport::Column::BiMonths: case MyMoneyReport::Column::Quarters: case MyMoneyReport::Column::Months: { if ((*it_period).startDate() >= m_beginDate.addDays(-m_beginDate.day() + 1) && (*it_period).startDate() <= m_endDate.addDays(m_endDate.daysInMonth() - m_endDate.day()) && (*it_period).startDate() > (columnDate(column).addMonths(-m_config_f.columnType()))) { //no currency conversion is done here because that is done for all columns later value = (*it_period).amount() * reverse; assignCell(outergroup, splitAccount, column, value, true /*budget*/); } ++it_period; break; } default: break; } } } break; } } } ++it_bacc; } } // end if there was a budget } void PivotTable::convertToBaseCurrency() { DEBUG_ENTER(Q_FUNC_INFO); int fraction = MyMoneyFile::instance()->baseCurrency().smallestAccountFraction(); PivotGrid::iterator it_outergroup = m_grid.begin(); while (it_outergroup != m_grid.end()) { PivotOuterGroup::iterator it_innergroup = (*it_outergroup).begin(); while (it_innergroup != (*it_outergroup).end()) { PivotInnerGroup::iterator it_row = (*it_innergroup).begin(); while (it_row != (*it_innergroup).end()) { int column = 1; while (column < m_numColumns) { if (it_row.value()[eActual].count() <= column) throw MYMONEYEXCEPTION(QString("Column %1 out of grid range (%2) in PivotTable::convertToBaseCurrency").arg(column).arg(it_row.value()[eActual].count())); QDate valuedate = columnDate(column); //get base price for that date MyMoneyMoney conversionfactor = it_row.key().baseCurrencyPrice(valuedate, m_config_f.isSkippingZero()); for (int i = 0; i < m_rowTypeList.size(); ++i) { if (m_rowTypeList[i] != eAverage) { //calculate base value MyMoneyMoney oldval = it_row.value()[ m_rowTypeList[i] ][column]; MyMoneyMoney value = (oldval * conversionfactor).reduce(); //convert to lowest fraction it_row.value()[ m_rowTypeList[i] ][column] = PivotCell(value.convert(fraction)); DEBUG_OUTPUT_IF(conversionfactor != MyMoneyMoney::ONE , QString("Factor of %1, value was %2, now %3").arg(conversionfactor.toDouble()).arg(DEBUG_SENSITIVE(oldval.toDouble())).arg(DEBUG_SENSITIVE(it_row.value()[m_rowTypeList[i]][column].toDouble()))); } } ++column; } ++it_row; } ++it_innergroup; } ++it_outergroup; } } void PivotTable::convertToDeepCurrency() { DEBUG_ENTER(Q_FUNC_INFO); MyMoneyFile* file = MyMoneyFile::instance(); PivotGrid::iterator it_outergroup = m_grid.begin(); while (it_outergroup != m_grid.end()) { PivotOuterGroup::iterator it_innergroup = (*it_outergroup).begin(); while (it_innergroup != (*it_outergroup).end()) { PivotInnerGroup::iterator it_row = (*it_innergroup).begin(); while (it_row != (*it_innergroup).end()) { int column = 1; while (column < m_numColumns) { if (it_row.value()[eActual].count() <= column) throw MYMONEYEXCEPTION(QString("Column %1 out of grid range (%2) in PivotTable::convertToDeepCurrency").arg(column).arg(it_row.value()[eActual].count())); QDate valuedate = columnDate(column); //get conversion factor for the account and date MyMoneyMoney conversionfactor = it_row.key().deepCurrencyPrice(valuedate, m_config_f.isSkippingZero()); //use the fraction relevant to the account at hand int fraction = it_row.key().currency().smallestAccountFraction(); //use base currency fraction if not initialized if (fraction == -1) fraction = file->baseCurrency().smallestAccountFraction(); //convert to deep currency MyMoneyMoney oldval = it_row.value()[eActual][column]; MyMoneyMoney value = (oldval * conversionfactor).reduce(); //reduce to lowest fraction it_row.value()[eActual][column] = PivotCell(value.convert(fraction)); //convert price data if (m_config_f.isIncludingPrice()) { MyMoneyMoney oldPriceVal = it_row.value()[ePrice][column]; MyMoneyMoney priceValue = (oldPriceVal * conversionfactor).reduce(); it_row.value()[ePrice][column] = PivotCell(priceValue.convert(10000)); } DEBUG_OUTPUT_IF(conversionfactor != MyMoneyMoney::ONE , QString("Factor of %1, value was %2, now %3").arg(conversionfactor.toDouble()).arg(DEBUG_SENSITIVE(oldval.toDouble())).arg(DEBUG_SENSITIVE(it_row.value()[eActual][column].toDouble()))); ++column; } ++it_row; } ++it_innergroup; } ++it_outergroup; } } void PivotTable::calculateTotals() { //insert the row type that is going to be used for (int i = 0; i < m_rowTypeList.size(); ++i) { for (int k = 0; k < m_numColumns; ++k) { m_grid.m_total[ m_rowTypeList[i] ].append(PivotCell()); } } // // Outer groups // // iterate over outer groups PivotGrid::iterator it_outergroup = m_grid.begin(); while (it_outergroup != m_grid.end()) { for (int i = 0; i < m_rowTypeList.size(); ++i) { for (int k = 0; k < m_numColumns; ++k) { (*it_outergroup).m_total[ m_rowTypeList[i] ].append(PivotCell()); } } // // Inner Groups // PivotOuterGroup::iterator it_innergroup = (*it_outergroup).begin(); while (it_innergroup != (*it_outergroup).end()) { for (int i = 0; i < m_rowTypeList.size(); ++i) { for (int k = 0; k < m_numColumns; ++k) { (*it_innergroup).m_total[ m_rowTypeList[i] ].append(PivotCell()); } } // // Rows // PivotInnerGroup::iterator it_row = (*it_innergroup).begin(); while (it_row != (*it_innergroup).end()) { // // Columns // int column = 1; while (column < m_numColumns) { for (int i = 0; i < m_rowTypeList.size(); ++i) { if (it_row.value()[ m_rowTypeList[i] ].count() <= column) throw MYMONEYEXCEPTION(QString("Column %1 out of grid range (%2) in PivotTable::calculateTotals, row columns").arg(column).arg(it_row.value()[ m_rowTypeList[i] ].count())); if ((*it_innergroup).m_total[ m_rowTypeList[i] ].count() <= column) throw MYMONEYEXCEPTION(QString("Column %1 out of grid range (%2) in PivotTable::calculateTotals, inner group totals").arg(column).arg((*it_innergroup).m_total[ m_rowTypeList[i] ].count())); //calculate total MyMoneyMoney value = it_row.value()[ m_rowTypeList[i] ][column]; (*it_innergroup).m_total[ m_rowTypeList[i] ][column] += value; (*it_row)[ m_rowTypeList[i] ].m_total += value; } ++column; } ++it_row; } // // Inner Row Group Totals // int column = 1; while (column < m_numColumns) { for (int i = 0; i < m_rowTypeList.size(); ++i) { if ((*it_innergroup).m_total[ m_rowTypeList[i] ].count() <= column) throw MYMONEYEXCEPTION(QString("Column %1 out of grid range (%2) in PivotTable::calculateTotals, inner group totals").arg(column).arg((*it_innergroup).m_total[ m_rowTypeList[i] ].count())); if ((*it_outergroup).m_total[ m_rowTypeList[i] ].count() <= column) throw MYMONEYEXCEPTION(QString("Column %1 out of grid range (%2) in PivotTable::calculateTotals, outer group totals").arg(column).arg((*it_innergroup).m_total[ m_rowTypeList[i] ].count())); //calculate totals MyMoneyMoney value = (*it_innergroup).m_total[ m_rowTypeList[i] ][column]; (*it_outergroup).m_total[ m_rowTypeList[i] ][column] += value; (*it_innergroup).m_total[ m_rowTypeList[i] ].m_total += value; } ++column; } ++it_innergroup; } // // Outer Row Group Totals // const bool isIncomeExpense = (m_config_f.rowType() == MyMoneyReport::Row::ExpenseIncome); const bool invert_total = (*it_outergroup).m_inverted; int column = 1; while (column < m_numColumns) { for (int i = 0; i < m_rowTypeList.size(); ++i) { if (m_grid.m_total[ m_rowTypeList[i] ].count() <= column) throw MYMONEYEXCEPTION(QString("Column %1 out of grid range (%2) in PivotTable::calculateTotals, grid totals").arg(column).arg((*it_innergroup).m_total[ m_rowTypeList[i] ].count())); //calculate actual totals MyMoneyMoney value = (*it_outergroup).m_total[ m_rowTypeList[i] ][column]; (*it_outergroup).m_total[ m_rowTypeList[i] ].m_total += value; //so far the invert only applies to actual and budget if (invert_total && m_rowTypeList[i] != eBudgetDiff && m_rowTypeList[i] != eForecast) value = -value; // forecast income expense reports should be inverted as oposed to asset/liability reports if (invert_total && isIncomeExpense && m_rowTypeList[i] == eForecast) value = -value; m_grid.m_total[ m_rowTypeList[i] ][column] += value; } ++column; } ++it_outergroup; } // // Report Totals // int totalcolumn = 1; while (totalcolumn < m_numColumns) { for (int i = 0; i < m_rowTypeList.size(); ++i) { if (m_grid.m_total[ m_rowTypeList[i] ].count() <= totalcolumn) throw MYMONEYEXCEPTION(QString("Total column %1 out of grid range (%2) in PivotTable::calculateTotals, grid totals").arg(totalcolumn).arg(m_grid.m_total[ m_rowTypeList[i] ].count())); //calculate actual totals MyMoneyMoney value = m_grid.m_total[ m_rowTypeList[i] ][totalcolumn]; m_grid.m_total[ m_rowTypeList[i] ].m_total += value; } ++totalcolumn; } } void PivotTable::assignCell(const QString& outergroup, const ReportAccount& _row, int column, MyMoneyMoney value, bool budget, bool stockSplit) { DEBUG_ENTER(Q_FUNC_INFO); DEBUG_OUTPUT(QString("Parameters: %1,%2,%3,%4,%5").arg(outergroup).arg(_row.debugName()).arg(column).arg(DEBUG_SENSITIVE(value.toDouble())).arg(budget)); // for budget reports, if this is the actual value, map it to the account which // holds its budget ReportAccount row = _row; if (!budget && m_config_f.hasBudget()) { QString newrow = m_budgetMap[row.id()]; // if there was no mapping found, then the budget report is not interested // in this account. if (newrow.isEmpty()) return; row = newrow; } // ensure the row already exists (and its parental hierarchy) createRow(outergroup, row, true); // Determine the inner group from the top-most parent account QString innergroup(row.topParentName()); if (m_numColumns <= column) throw MYMONEYEXCEPTION(QString("Column %1 out of m_numColumns range (%2) in PivotTable::assignCell").arg(column).arg(m_numColumns)); if (m_grid[outergroup][innergroup][row][eActual].count() <= column) throw MYMONEYEXCEPTION(QString("Column %1 out of grid range (%2) in PivotTable::assignCell").arg(column).arg(m_grid[outergroup][innergroup][row][eActual].count())); if (m_grid[outergroup][innergroup][row][eBudget].count() <= column) throw MYMONEYEXCEPTION(QString("Column %1 out of grid range (%2) in PivotTable::assignCell").arg(column).arg(m_grid[outergroup][innergroup][row][eBudget].count())); if (!stockSplit) { // Determine whether the value should be inverted before being placed in the row if (m_grid[outergroup].m_inverted) value = -value; // Add the value to the grid cell if (budget) { m_grid[outergroup][innergroup][row][eBudget][column] += value; } else { // If it is loading an actual value for a budget report // check whether it is a subaccount of a budget account (include subaccounts) // If so, check if is the same currency and convert otherwise if (m_config_f.hasBudget() && row.id() != _row.id() && row.currencyId() != _row.currencyId()) { ReportAccount origAcc = _row; MyMoneyMoney rate = origAcc.foreignCurrencyPrice(row.currencyId(), columnDate(column), false); m_grid[outergroup][innergroup][row][eActual][column] += (value * rate).reduce(); } else { m_grid[outergroup][innergroup][row][eActual][column] += value; } } } else { m_grid[outergroup][innergroup][row][eActual][column] += PivotCell::stockSplit(value); } } void PivotTable::createRow(const QString& outergroup, const ReportAccount& row, bool recursive) { DEBUG_ENTER(Q_FUNC_INFO); // Determine the inner group from the top-most parent account QString innergroup(row.topParentName()); if (! m_grid.contains(outergroup)) { DEBUG_OUTPUT(QString("Adding group [%1]").arg(outergroup)); m_grid[outergroup] = PivotOuterGroup(m_numColumns); } if (! m_grid[outergroup].contains(innergroup)) { DEBUG_OUTPUT(QString("Adding group [%1][%2]").arg(outergroup).arg(innergroup)); m_grid[outergroup][innergroup] = PivotInnerGroup(m_numColumns); } if (! m_grid[outergroup][innergroup].contains(row)) { DEBUG_OUTPUT(QString("Adding row [%1][%2][%3]").arg(outergroup).arg(innergroup).arg(row.debugName())); m_grid[outergroup][innergroup][row] = PivotGridRowSet(m_numColumns); if (recursive && !row.isTopLevel()) createRow(outergroup, row.parent(), recursive); } } int PivotTable::columnValue(const QDate& _date) const { if (m_config_f.isColumnsAreDays()) return (m_beginDate.daysTo(_date)); else return (_date.year() * 12 + _date.month()); } QDate PivotTable::columnDate(int column) const { if (m_config_f.isColumnsAreDays()) return m_beginDate.addDays(m_config_f.columnPitch() * column - 1); else return m_beginDate.addMonths(m_config_f.columnPitch() * column).addDays(-1); } QString PivotTable::renderCSV() const { DEBUG_ENTER(Q_FUNC_INFO); // // Report Title // QString result = QString("\"Report: %1\"\n").arg(m_config_f.name()); result += i18nc("Report date range", "%1 through %2\n", KGlobal::locale()->formatDate(m_config_f.fromDate(), KLocale::ShortDate), KGlobal::locale()->formatDate(m_config_f.toDate(), KLocale::ShortDate)); if (m_config_f.isConvertCurrency()) result += i18n("All currencies converted to %1\n", MyMoneyFile::instance()->baseCurrency().name()); else result += i18n("All values shown in %1 unless otherwise noted\n", MyMoneyFile::instance()->baseCurrency().name()); // // Table Header // result += i18n("Account"); int column = 1; while (column < m_numColumns) { result += QString(",%1").arg(QString(m_columnHeadings[column++])); if (m_rowTypeList.size() > 1) { QString separator; separator = separator.fill(',', m_rowTypeList.size() - 1); result += separator; } } //show total columns if (m_config_f.isShowingRowTotals()) result += QString(",%1").arg(i18nc("Total balance", "Total")); result += '\n'; // Row Type Header if (m_rowTypeList.size() > 1) { int column = 1; while (column < m_numColumns) { for (int i = 0; i < m_rowTypeList.size(); ++i) { result += QString(",%1").arg(m_columnTypeHeaderList[i]); } column++; } if (m_config_f.isShowingRowTotals()) { for (int i = 0; i < m_rowTypeList.size(); ++i) { result += QString(",%1").arg(m_columnTypeHeaderList[i]); } } result += '\n'; } int fraction = MyMoneyFile::instance()->baseCurrency().smallestAccountFraction(); // // Outer groups // // iterate over outer groups PivotGrid::const_iterator it_outergroup = m_grid.begin(); while (it_outergroup != m_grid.end()) { // // Outer Group Header // result += it_outergroup.key() + '\n'; // // Inner Groups // PivotOuterGroup::const_iterator it_innergroup = (*it_outergroup).begin(); int rownum = 0; while (it_innergroup != (*it_outergroup).end()) { // // Rows // QString innergroupdata; PivotInnerGroup::const_iterator it_row = (*it_innergroup).begin(); while (it_row != (*it_innergroup).end()) { ReportAccount rowname = it_row.key(); int fraction = rowname.currency().smallestAccountFraction(); // // Columns // QString rowdata; int column = 1; bool isUsed = false; for (int i = 0; i < m_rowTypeList.size(); ++i) isUsed |= it_row.value()[ m_rowTypeList[i] ][0].isUsed(); while (column < m_numColumns) { //show columns for (int i = 0; i < m_rowTypeList.size(); ++i) { isUsed |= it_row.value()[ m_rowTypeList[i] ][column].isUsed(); rowdata += QString(",\"%1\"").arg(it_row.value()[ m_rowTypeList[i] ][column].formatMoney(fraction, false)); } column++; } if (m_config_f.isShowingRowTotals()) { for (int i = 0; i < m_rowTypeList.size(); ++i) rowdata += QString(",\"%1\"").arg((*it_row)[ m_rowTypeList[i] ].m_total.formatMoney(fraction, false)); } // // Row Header // if (!rowname.isClosed() || isUsed) { innergroupdata += "\"" + QString().fill(' ', rowname.hierarchyDepth() - 1) + rowname.name(); // if we don't convert the currencies to the base currency and the // current row contains a foreign currency, then we append the currency // to the name of the account if (!m_config_f.isConvertCurrency() && rowname.isForeignCurrency()) innergroupdata += QString(" (%1)").arg(rowname.currencyId()); innergroupdata += '\"'; if (isUsed) innergroupdata += rowdata; innergroupdata += '\n'; } ++it_row; } // // Inner Row Group Totals // bool finishrow = true; QString finalRow; bool isUsed = false; if (m_config_f.detailLevel() == MyMoneyReport::DetailLevel::All && ((*it_innergroup).size() > 1)) { // Print the individual rows result += innergroupdata; if (m_config_f.isShowingColumnTotals()) { // Start the TOTALS row finalRow = i18nc("Total balance", "Total"); isUsed = true; } else { ++rownum; finishrow = false; } } else { // Start the single INDIVIDUAL ACCOUNT row ReportAccount rowname = (*it_innergroup).begin().key(); isUsed |= !rowname.isClosed(); finalRow = "\"" + QString().fill(' ', rowname.hierarchyDepth() - 1) + rowname.name(); if (!m_config_f.isConvertCurrency() && rowname.isForeignCurrency()) finalRow += QString(" (%1)").arg(rowname.currencyId()); finalRow += "\""; } // Finish the row started above, unless told not to if (finishrow) { int column = 1; for (int i = 0; i < m_rowTypeList.size(); ++i) isUsed |= (*it_innergroup).m_total[ m_rowTypeList[i] ][0].isUsed(); while (column < m_numColumns) { for (int i = 0; i < m_rowTypeList.size(); ++i) { isUsed |= (*it_innergroup).m_total[ m_rowTypeList[i] ][column].isUsed(); finalRow += QString(",\"%1\"").arg((*it_innergroup).m_total[ m_rowTypeList[i] ][column].formatMoney(fraction, false)); } column++; } if (m_config_f.isShowingRowTotals()) { for (int i = 0; i < m_rowTypeList.size(); ++i) finalRow += QString(",\"%1\"").arg((*it_innergroup).m_total[ m_rowTypeList[i] ].m_total.formatMoney(fraction, false)); } finalRow += '\n'; } if (isUsed) { result += finalRow; ++rownum; } ++it_innergroup; } // // Outer Row Group Totals // if (m_config_f.isShowingColumnTotals()) { result += QString("%1 %2").arg(i18nc("Total balance", "Total")).arg(it_outergroup.key()); int column = 1; while (column < m_numColumns) { for (int i = 0; i < m_rowTypeList.size(); ++i) result += QString(",\"%1\"").arg((*it_outergroup).m_total[ m_rowTypeList[i] ][column].formatMoney(fraction, false)); column++; } if (m_config_f.isShowingRowTotals()) { for (int i = 0; i < m_rowTypeList.size(); ++i) result += QString(",\"%1\"").arg((*it_outergroup).m_total[ m_rowTypeList[i] ].m_total.formatMoney(fraction, false)); } result += '\n'; } ++it_outergroup; } // // Report Totals // if (m_config_f.isShowingColumnTotals()) { result += i18n("Grand Total"); int totalcolumn = 1; while (totalcolumn < m_numColumns) { for (int i = 0; i < m_rowTypeList.size(); ++i) result += QString(",\"%1\"").arg(m_grid.m_total[ m_rowTypeList[i] ][totalcolumn].formatMoney(fraction, false)); totalcolumn++; } if (m_config_f.isShowingRowTotals()) { for (int i = 0; i < m_rowTypeList.size(); ++i) result += QString(",\"%1\"").arg(m_grid.m_total[ m_rowTypeList[i] ].m_total.formatMoney(fraction, false)); } result += '\n'; } return result; } QString PivotTable::renderBody() const { DEBUG_ENTER(Q_FUNC_INFO); QString colspan = QString(" colspan=\"%1\"").arg(m_numColumns + 1 + (m_config_f.isShowingRowTotals() ? 1 : 0)); // // Report Title // QString result = QString("

%1

\n").arg(m_config_f.name()); //actual dates of the report result += QString("
"); result += i18nc("Report date range", "%1 through %2", KGlobal::locale()->formatDate(m_config_f.fromDate(), KLocale::ShortDate), KGlobal::locale()->formatDate(m_config_f.toDate(), KLocale::ShortDate)); result += QString("
\n"); result += QString("
 
\n"); //currency conversion message result += QString("
"); if (m_config_f.isConvertCurrency()) result += i18n("All currencies converted to %1", MyMoneyFile::instance()->baseCurrency().name()); else result += i18n("All values shown in %1 unless otherwise noted", MyMoneyFile::instance()->baseCurrency().name()); result += QString("
\n"); result += QString("
 
\n"); // setup a leftborder for better readability of budget vs actual reports QString leftborder; if (m_rowTypeList.size() > 1) leftborder = " class=\"leftborder\""; // // Table Header // result += QString("\n\n\n" "\n").arg(i18n("Account")); QString headerspan; int span = m_rowTypeList.size(); headerspan = QString(" colspan=\"%1\"").arg(span); int column = 1; while (column < m_numColumns) result += QString("%2").arg(headerspan, QString(m_columnHeadings[column++]).replace(QRegExp(" "), "
")); if (m_config_f.isShowingRowTotals()) result += QString("%2").arg(headerspan).arg(i18nc("Total balance", "Total")); result += "
\n"; // // Header for multiple columns // if (span > 1) { result += ""; int column = 1; while (column < m_numColumns) { QString lb; if (column != 1) lb = leftborder; for (int i = 0; i < m_rowTypeList.size(); ++i) { result += QString("%1") .arg(m_columnTypeHeaderList[i]) .arg(i == 0 ? lb : QString()); } column++; } if (m_config_f.isShowingRowTotals()) { for (int i = 0; i < m_rowTypeList.size(); ++i) { result += QString("%1") .arg(m_columnTypeHeaderList[i]) .arg(i == 0 ? leftborder : QString()); } } result += ""; } // Skip the body of the report if the report only calls for totals to be shown if (m_config_f.detailLevel() != MyMoneyReport::DetailLevel::Total) { // // Outer groups // // Need to sort the outergroups. They can't always be sorted by name. So we create a list of // map iterators, and sort that. Then we'll iterate through the map iterators and use those as // before. // // I hope this doesn't bog the performance of reports, given that we're copying the entire report // data. If this is a perf hit, we could change to storing outergroup pointers, I think. QList outergroups; PivotGrid::const_iterator it_outergroup_map = m_grid.begin(); while (it_outergroup_map != m_grid.end()) { outergroups.push_back(it_outergroup_map.value()); // copy the name into the outergroup, because we will now lose any association with // the map iterator outergroups.back().m_displayName = it_outergroup_map.key(); ++it_outergroup_map; } qSort(outergroups.begin(), outergroups.end()); QList::const_iterator it_outergroup = outergroups.constBegin(); while (it_outergroup != outergroups.constEnd()) { // // Outer Group Header // result += QString("\n").arg(colspan).arg((*it_outergroup).m_displayName); // Skip the inner groups if the report only calls for outer group totals to be shown if (m_config_f.detailLevel() != MyMoneyReport::DetailLevel::Group) { // // Inner Groups // PivotOuterGroup::const_iterator it_innergroup = (*it_outergroup).begin(); int rownum = 0; while (it_innergroup != (*it_outergroup).end()) { // // Rows // QString innergroupdata; PivotInnerGroup::const_iterator it_row = (*it_innergroup).begin(); while (it_row != (*it_innergroup).end()) { // // Columns // QString rowdata; int column = 1; bool isUsed = it_row.value()[eActual][0].isUsed(); while (column < m_numColumns) { QString lb; if (column != 1) lb = leftborder; for (int i = 0; i < m_rowTypeList.size(); ++i) { rowdata += QString("%1") .arg(coloredAmount(it_row.value()[ m_rowTypeList[i] ][column])) .arg(i == 0 ? lb : QString()); isUsed |= it_row.value()[ m_rowTypeList[i] ][column].isUsed(); } column++; } if (m_config_f.isShowingRowTotals()) { for (int i = 0; i < m_rowTypeList.size(); ++i) { rowdata += QString("%1") .arg(coloredAmount(it_row.value()[ m_rowTypeList[i] ].m_total)) .arg(i == 0 ? leftborder : QString()); } } // // Row Header // ReportAccount rowname = it_row.key(); // don't show closed accounts if they have not been used if (!rowname.isClosed() || isUsed) { innergroupdata += QString("%5%6") .arg(rownum & 0x01 ? "even" : "odd") .arg(rowname.isTopLevel() ? " id=\"topparent\"" : "") .arg("") //.arg((*it_row).m_total.isZero() ? colspan : "") // colspan the distance if this row will be blank .arg(rowname.hierarchyDepth() - 1) .arg(rowname.name().replace(QRegExp(" "), " ")) .arg((m_config_f.isConvertCurrency() || !rowname.isForeignCurrency()) ? QString() : QString(" (%1)").arg(rowname.currency().id())); // Don't print this row if it's going to be all zeros // TODO: Uncomment this, and deal with the case where the data // is zero, but the budget is non-zero //if ( !(*it_row).m_total.isZero() ) innergroupdata += rowdata; innergroupdata += "\n"; } ++it_row; } // // Inner Row Group Totals // bool finishrow = true; QString finalRow; bool isUsed = false; if (m_config_f.detailLevel() == MyMoneyReport::DetailLevel::All && ((*it_innergroup).size() > 1)) { // Print the individual rows result += innergroupdata; if (m_config_f.isShowingColumnTotals()) { // Start the TOTALS row finalRow = QString("") .arg(rownum & 0x01 ? "even" : "odd") .arg(i18nc("Total balance", "Total")); // don't suppress display of totals isUsed = true; } else { finishrow = false; ++rownum; } } else { // Start the single INDIVIDUAL ACCOUNT row // FIXME: There is a bit of a bug here with class=leftX. There's only a finite number // of classes I can define in the .CSS file, and the user can theoretically nest deeper. // The right solution is to use style=Xem, and calculate X. Let's see if anyone complains // first :) Also applies to the row header case above. // FIXED: I found it in one of my reports and changed it to the proposed method. // This works for me (ipwizard) ReportAccount rowname = (*it_innergroup).begin().key(); isUsed |= !rowname.isClosed(); finalRow = QString("") .arg(rownum & 0x01 ? "even" : "odd") .arg(m_config_f.detailLevel() == MyMoneyReport::DetailLevel::All ? "id=\"solo\"" : "") .arg(rowname.hierarchyDepth() - 1) .arg(rowname.name().replace(QRegExp(" "), " ")) .arg((m_config_f.isConvertCurrency() || !rowname.isForeignCurrency()) ? QString() : QString(" (%1)").arg(rowname.currency().id())); } // Finish the row started above, unless told not to if (finishrow) { int column = 1; isUsed |= (*it_innergroup).m_total[eActual][0].isUsed(); while (column < m_numColumns) { QString lb; if (column != 1) lb = leftborder; for (int i = 0; i < m_rowTypeList.size(); ++i) { finalRow += QString("%1") .arg(coloredAmount((*it_innergroup).m_total[ m_rowTypeList[i] ][column])) .arg(i == 0 ? lb : QString()); isUsed |= (*it_innergroup).m_total[ m_rowTypeList[i] ][column].isUsed(); } column++; } if (m_config_f.isShowingRowTotals()) { for (int i = 0; i < m_rowTypeList.size(); ++i) { finalRow += QString("%1") .arg(coloredAmount((*it_innergroup).m_total[ m_rowTypeList[i] ].m_total)) .arg(i == 0 ? leftborder : QString()); } } finalRow += "\n"; if (isUsed) { result += finalRow; ++rownum; } } ++it_innergroup; } // end while iterating on the inner groups } // end if detail level is not "group" // // Outer Row Group Totals // if (m_config_f.isShowingColumnTotals()) { result += QString("").arg(i18nc("Total balance", "Total")).arg((*it_outergroup).m_displayName); int column = 1; while (column < m_numColumns) { QString lb; if (column != 1) lb = leftborder; for (int i = 0; i < m_rowTypeList.size(); ++i) { result += QString("%1") .arg(coloredAmount((*it_outergroup).m_total[ m_rowTypeList[i] ][column])) .arg(i == 0 ? lb : QString()); } column++; } if (m_config_f.isShowingRowTotals()) { for (int i = 0; i < m_rowTypeList.size(); ++i) { result += QString("%1") .arg(coloredAmount((*it_outergroup).m_total[ m_rowTypeList[i] ].m_total)) .arg(i == 0 ? leftborder : QString()); } } result += "\n"; } ++it_outergroup; } // end while iterating on the outergroups } // end if detail level is not "total" // // Report Totals // if (m_config_f.isShowingColumnTotals()) { result += QString("\n"); result += QString("").arg(i18n("Grand Total")); int totalcolumn = 1; while (totalcolumn < m_numColumns) { QString lb; if (totalcolumn != 1) lb = leftborder; for (int i = 0; i < m_rowTypeList.size(); ++i) { result += QString("%1") .arg(coloredAmount(m_grid.m_total[ m_rowTypeList[i] ][totalcolumn])) .arg(i == 0 ? lb : QString()); } totalcolumn++; } if (m_config_f.isShowingRowTotals()) { for (int i = 0; i < m_rowTypeList.size(); ++i) { result += QString("%1") .arg(coloredAmount(m_grid.m_total[ m_rowTypeList[i] ].m_total)) .arg(i == 0 ? leftborder : QString()); } } result += "\n"; } result += QString("\n"); result += QString("\n"); result += "
%1
%2
  %2
%5%6
%1 %2
 
%1
 
 
\n"; return result; } void PivotTable::dump(const QString& file, const QString& /* context */) const { QFile g(file); g.open(QIODevice::WriteOnly); QTextStream(&g) << renderBody(); g.close(); } void PivotTable::drawChart(KReportChartView& chartView) const { chartView.drawPivotChart(m_grid, m_config_f, m_numColumns, m_columnHeadings, m_rowTypeList, m_columnTypeHeaderList); } QString PivotTable::coloredAmount(const MyMoneyMoney& amount, const QString& currencySymbol, int prec) const { QString result; if (amount.isNegative()) result += QString("") .arg(KMyMoneyGlobalSettings::listNegativeValueColor().red()) .arg(KMyMoneyGlobalSettings::listNegativeValueColor().green()) .arg(KMyMoneyGlobalSettings::listNegativeValueColor().blue()); result += amount.formatMoney(currencySymbol, prec); if (amount.isNegative()) result += QString(""); return result; } void PivotTable::calculateBudgetDiff() { PivotGrid::iterator it_outergroup = m_grid.begin(); while (it_outergroup != m_grid.end()) { PivotOuterGroup::iterator it_innergroup = (*it_outergroup).begin(); while (it_innergroup != (*it_outergroup).end()) { PivotInnerGroup::iterator it_row = (*it_innergroup).begin(); while (it_row != (*it_innergroup).end()) { int column = 1; switch (it_row.key().accountGroup()) { case MyMoneyAccount::Income: case MyMoneyAccount::Asset: while (column < m_numColumns) { it_row.value()[eBudgetDiff][column] = it_row.value()[eActual][column] - it_row.value()[eBudget][column]; ++column; } break; case MyMoneyAccount::Expense: case MyMoneyAccount::Liability: while (column < m_numColumns) { it_row.value()[eBudgetDiff][column] = it_row.value()[eBudget][column] - it_row.value()[eActual][column]; ++column; } break; default: break; } ++it_row; } ++it_innergroup; } ++it_outergroup; } } void PivotTable::calculateForecast() { //setup forecast MyMoneyForecast forecast = KMyMoneyGlobalSettings::forecast(); //since this is a net worth forecast we want to include all account even those that are not in use forecast.setIncludeUnusedAccounts(true); //setup forecast dates if (m_endDate > QDate::currentDate()) { forecast.setForecastEndDate(m_endDate); forecast.setForecastStartDate(QDate::currentDate()); forecast.setForecastDays(QDate::currentDate().daysTo(m_endDate)); } else { forecast.setForecastStartDate(m_beginDate); forecast.setForecastEndDate(m_endDate); forecast.setForecastDays(m_beginDate.daysTo(m_endDate) + 1); } //adjust history dates if beginning date is before today if (m_beginDate < QDate::currentDate()) { forecast.setHistoryEndDate(m_beginDate.addDays(-1)); forecast.setHistoryStartDate(forecast.historyEndDate().addDays(-forecast.accountsCycle()*forecast.forecastCycles())); } //run forecast if (m_config_f.rowType() == MyMoneyReport::Row::AssetLiability) { //asset and liability forecast.doForecast(); } else { //income and expenses MyMoneyBudget budget; forecast.createBudget(budget, m_beginDate.addYears(-1), m_beginDate.addDays(-1), m_beginDate, m_endDate, false); } //go through the data and add forecast PivotGrid::iterator it_outergroup = m_grid.begin(); while (it_outergroup != m_grid.end()) { PivotOuterGroup::iterator it_innergroup = (*it_outergroup).begin(); while (it_innergroup != (*it_outergroup).end()) { PivotInnerGroup::iterator it_row = (*it_innergroup).begin(); while (it_row != (*it_innergroup).end()) { int column = 1; QDate forecastDate = m_beginDate; //check whether columns are days or months if (m_config_f.isColumnsAreDays()) { while (column < m_numColumns) { it_row.value()[eForecast][column] = forecast.forecastBalance(it_row.key(), forecastDate); forecastDate = forecastDate.addDays(1); ++column; } } else { //if columns are months while (column < m_numColumns) { // the forecast balance is on the first day of the month see MyMoneyForecast::calculateScheduledMonthlyBalances() forecastDate = QDate(forecastDate.year(), forecastDate.month(), 1); //check that forecastDate is not over ending date if (forecastDate > m_endDate) forecastDate = m_endDate; //get forecast balance and set the corresponding column it_row.value()[eForecast][column] = forecast.forecastBalance(it_row.key(), forecastDate); forecastDate = forecastDate.addMonths(1); ++column; } } ++it_row; } ++it_innergroup; } ++it_outergroup; } } void PivotTable::loadRowTypeList() { if ((m_config_f.isIncludingBudgetActuals()) || (!m_config_f.hasBudget() && !m_config_f.isIncludingForecast() && !m_config_f.isIncludingMovingAverage() && !m_config_f.isIncludingPrice() && !m_config_f.isIncludingAveragePrice()) ) { m_rowTypeList.append(eActual); m_columnTypeHeaderList.append(i18n("Actual")); } if (m_config_f.hasBudget()) { m_rowTypeList.append(eBudget); m_columnTypeHeaderList.append(i18n("Budget")); } if (m_config_f.isIncludingBudgetActuals()) { m_rowTypeList.append(eBudgetDiff); m_columnTypeHeaderList.append(i18n("Difference")); } if (m_config_f.isIncludingForecast()) { m_rowTypeList.append(eForecast); m_columnTypeHeaderList.append(i18n("Forecast")); } if (m_config_f.isIncludingMovingAverage()) { m_rowTypeList.append(eAverage); m_columnTypeHeaderList.append(i18n("Moving Average")); } if (m_config_f.isIncludingAveragePrice()) { m_rowTypeList.append(eAverage); m_columnTypeHeaderList.append(i18n("Moving Average Price")); } if (m_config_f.isIncludingPrice()) { m_rowTypeList.append(ePrice); m_columnTypeHeaderList.append(i18n("Price")); } } void PivotTable::calculateMovingAverage() { int delta = m_config_f.movingAverageDays() / 2; //go through the data and add the moving average PivotGrid::iterator it_outergroup = m_grid.begin(); while (it_outergroup != m_grid.end()) { PivotOuterGroup::iterator it_innergroup = (*it_outergroup).begin(); while (it_innergroup != (*it_outergroup).end()) { PivotInnerGroup::iterator it_row = (*it_innergroup).begin(); while (it_row != (*it_innergroup).end()) { int column = 1; //check whether columns are days or months if (m_config_f.columnType() == MyMoneyReport::Column::Days) { while (column < m_numColumns) { MyMoneyMoney totalPrice = MyMoneyMoney(); QDate averageStart = columnDate(column).addDays(-delta); QDate averageEnd = columnDate(column).addDays(delta); for (QDate averageDate = averageStart; averageDate <= averageEnd; averageDate = averageDate.addDays(1)) { if (m_config_f.isConvertCurrency()) { totalPrice += it_row.key().deepCurrencyPrice(averageDate) * it_row.key().baseCurrencyPrice(averageDate); } else { totalPrice += it_row.key().deepCurrencyPrice(averageDate); } totalPrice = totalPrice.convert(10000); } //calculate the average price MyMoneyMoney averagePrice = totalPrice / MyMoneyMoney((averageStart.daysTo(averageEnd) + 1), 1); //get the actual value, multiply by the average price and save that value MyMoneyMoney averageValue = it_row.value()[eActual][column] * averagePrice; it_row.value()[eAverage][column] = averageValue.convert(10000); ++column; } } else { //if columns are months while (column < m_numColumns) { QDate averageStart = columnDate(column); //set the right start date depending on the column type switch (m_config_f.columnType()) { case MyMoneyReport::Column::Years: { averageStart = QDate(columnDate(column).year(), 1, 1); break; } case MyMoneyReport::Column::BiMonths: { averageStart = QDate(columnDate(column).year(), columnDate(column).month(), 1).addMonths(-1); break; } case MyMoneyReport::Column::Quarters: { averageStart = QDate(columnDate(column).year(), columnDate(column).month(), 1).addMonths(-1); break; } case MyMoneyReport::Column::Months: { averageStart = QDate(columnDate(column).year(), columnDate(column).month(), 1); break; } case MyMoneyReport::Column::Weeks: { averageStart = columnDate(column).addDays(-columnDate(column).dayOfWeek() + 1); break; } default: break; } //gather the actual data and calculate the average MyMoneyMoney totalPrice = MyMoneyMoney(); QDate averageEnd = columnDate(column); for (QDate averageDate = averageStart; averageDate <= averageEnd; averageDate = averageDate.addDays(1)) { if (m_config_f.isConvertCurrency()) { totalPrice += it_row.key().deepCurrencyPrice(averageDate) * it_row.key().baseCurrencyPrice(averageDate); } else { totalPrice += it_row.key().deepCurrencyPrice(averageDate); } totalPrice = totalPrice.convert(10000); } MyMoneyMoney averagePrice = totalPrice / MyMoneyMoney((averageStart.daysTo(averageEnd) + 1), 1); MyMoneyMoney averageValue = it_row.value()[eActual][column] * averagePrice; //fill in the average it_row.value()[eAverage][column] = averageValue.convert(10000); ++column; } } ++it_row; } ++it_innergroup; } ++it_outergroup; } } void PivotTable::fillBasePriceUnit(ERowType rowType) { MyMoneyFile* file = MyMoneyFile::instance(); QString baseCurrencyId = file->baseCurrency().id(); //get the first price date for securities QMap securityDates = securityFirstPrice(); //go through the data PivotGrid::iterator it_outergroup = m_grid.begin(); while (it_outergroup != m_grid.end()) { PivotOuterGroup::iterator it_innergroup = (*it_outergroup).begin(); while (it_innergroup != (*it_outergroup).end()) { PivotInnerGroup::iterator it_row = (*it_innergroup).begin(); while (it_row != (*it_innergroup).end()) { int column = 1; //if it is a base currency fill all the values bool firstPriceExists = false; if (it_row.key().currencyId() == baseCurrencyId) { firstPriceExists = true; } while (column < m_numColumns) { //check whether the date for that column is on or after the first price if (!firstPriceExists && securityDates.contains(it_row.key().currencyId()) && columnDate(column) >= securityDates.value(it_row.key().currencyId())) { firstPriceExists = true; } //only add the dummy value if there is a price for that date if (firstPriceExists) { //insert a unit of currency for each account it_row.value()[rowType][column] = MyMoneyMoney::ONE; } ++column; } ++it_row; } ++it_innergroup; } ++it_outergroup; } } QMap PivotTable::securityFirstPrice() { MyMoneyFile* file = MyMoneyFile::instance(); MyMoneyPriceList priceList = file->priceList(); QMap securityPriceDate; MyMoneyPriceList::const_iterator prices_it; for (prices_it = priceList.constBegin(); prices_it != priceList.constEnd(); ++prices_it) { MyMoneyPrice firstPrice = (*((*prices_it).constBegin())); //check the security in the from field //if it is there, check if it is older if (securityPriceDate.contains(firstPrice.from())) { if (securityPriceDate.value(firstPrice.from()) > firstPrice.date()) { securityPriceDate[firstPrice.from()] = firstPrice.date(); } } else { securityPriceDate.insert(firstPrice.from(), firstPrice.date()); } //check the security in the to field //if it is there, check if it is older if (securityPriceDate.contains(firstPrice.to())) { if (securityPriceDate.value(firstPrice.to()) > firstPrice.date()) { securityPriceDate[firstPrice.to()] = firstPrice.date(); } } else { securityPriceDate.insert(firstPrice.to(), firstPrice.date()); } } return securityPriceDate; } void PivotTable::includeInvestmentSubAccounts() { // if we're not in expert mode, we need to make sure // that all stock accounts for the selected investment // account are also selected QStringList accountList; if (m_config_f.accounts(accountList)) { if (!KMyMoneyGlobalSettings::expertMode()) { QStringList::const_iterator it_a, it_b; for (it_a = accountList.constBegin(); it_a != accountList.constEnd(); ++it_a) { MyMoneyAccount acc = MyMoneyFile::instance()->account(*it_a); if (acc.accountType() == MyMoneyAccount::Investment) { for (it_b = acc.accountList().constBegin(); it_b != acc.accountList().constEnd(); ++it_b) { if (!accountList.contains(*it_b)) { m_config_f.addAccount(*it_b); } } } } } } } int PivotTable::currentDateColumn() { //return -1 if the columns do not include the current date if (m_beginDate > QDate::currentDate() || m_endDate < QDate::currentDate()) { return -1; } //check the date of each column and return if it is the one for the current date //if columns are not days, return the one for the current month or year int column = 1; while (column < m_numColumns) { if (columnDate(column) >= QDate::currentDate()) { break; } column++; } //if there is no column matching the current date, return -1 if (column == m_numColumns) { column = -1; } return column; } } // namespace + + +QDebug operator<<(QDebug dbg, reports::PivotTable &a) +{ + dbg << "reports::PivotTable(" + << "grid" << a.grid() + << ")"; + return dbg; +} diff --git a/kmymoney/reports/pivottable.h b/kmymoney/reports/pivottable.h index 69f2bfe65..402c3d18c 100644 --- a/kmymoney/reports/pivottable.h +++ b/kmymoney/reports/pivottable.h @@ -1,374 +1,376 @@ /*************************************************************************** pivottable.h ------------------- begin : Sat May 22 2004 copyright : (C) 2004-2005 by Ace Jones Thomas Baumgart Alvaro Soliverez ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #ifndef PIVOTTABLE_H #define PIVOTTABLE_H // ---------------------------------------------------------------------------- // QT Includes #include #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes #include "kreportchartview.h" #include "mymoneyfile.h" #include "mymoneyreport.h" #include "reporttable.h" #include "pivotgrid.h" #include "reportaccount.h" using namespace KDChart; namespace reports { /** * Calculates a 'pivot table' of information about the transaction database. * Based on pivot tables in MS Excel, and implemented as 'Data Pilot' in * OpenOffice.Org Calc. * * | Month,etc * -------------+------------ * Expense Type | Sum(Value) * Category | * * This is a middle-layer class, between the UI and the engine. The * MyMoneyReport class holds only the CONFIGURATION parameters. This * class actually does the work of retrieving the data from the engine * and formatting it for the user. * * @author Ace Jones * * @short **/ class PivotTable : public ReportTable { KMM_MYMONEY_UNIT_TESTABLE public: /** * Create a Pivot table style report * * @param _config_f The configuration parameters for this report */ PivotTable(const MyMoneyReport& _config_f); /** * virtual Destructur */ virtual ~PivotTable() {} /** * Render the report body to an HTML stream. * * @return QString HTML string representing the report body */ QString renderBody() const; /** * Render the report to a comma-separated-values stream. * * @return QString CSV string representing the report */ QString renderCSV() const; /** * Render the report to a graphical chart * * @param view The KReportChartView into which to draw the chart. */ void drawChart(KReportChartView& view) const; /** * Dump the report's HTML to a file * * @param file The filename to dump into * @param context unused, but provided for interface compatibility */ void dump(const QString& file, const QString& context = QString()) const; /** * Returns the grid generated by the report * */ PivotGrid &grid() { return m_grid; } protected: void init(); // used for debugging the constructor private: PivotGrid m_grid; QStringList m_columnHeadings; int m_numColumns; QDate m_beginDate; QDate m_endDate; bool m_runningSumsCalculated; /** * For budget-vs-actual reports only, maps each account to the account which holds * the budget for it. If an account is not contained in this map, it is not included * in the budget. */ QMap m_budgetMap; /** * This list contains the types of PivotGridRows that are going to be shown in the report */ QList m_rowTypeList; /** * This list contains the i18n headers for the column types */ QStringList m_columnTypeHeaderList; MyMoneyReport m_config_f; /** * This method returns the formatted value of @a amount with * a possible @a currencySymbol added and @a prec fractional digits. * @a currencySymbol defaults to be empty and @a prec defaults to 2. * * If @a amount is negative the formatted value is enclosed in an * HTML font tag to modify the color to reflect the user settings for * negtive numbers. * * Example: 1.23 is returned as '1.23' whereas -1.23 is returned as * @verbatim -1.23@endverbatim * with $red, $green and $blue being the actual value for the * chosen color. */ QString coloredAmount(const MyMoneyMoney& amount, const QString& currencySymbol = QString(), int prec = 2) const; protected: /** * Creates a row in the grid if it doesn't already exist * * Downsteam assignment functions will assume that this row already * exists, so this function creates a row of the needed length populated * with zeros. * * @param outergroup The outer row group * @param row The row itself * @param recursive Whether to also recursively create rows for our parent accounts */ void createRow(const QString& outergroup, const ReportAccount& row, bool recursive); /** * Assigns a value into the grid * * Adds the given value to the value which already exists at the specified grid position * * @param outergroup The outer row group * @param row The row itself * @param column The column * @param value The value to be added in * @param budget Whether this is a budget value (@p true) or an actual * value (@p false). Defaults to @p false. * @param stockSplit Whether this is a stock split (@p true) or an actual * value (@p false). Defaults to @p false. */ inline void assignCell(const QString& outergroup, const ReportAccount& row, int column, MyMoneyMoney value, bool budget = false, bool stockSplit = false); /** * Create a row for each included account. This is used when * the config parameter isIncludingUnusedAccount() is true */ void createAccountRows(); /** * Record the opening balances of all qualifying accounts into the grid. * * For accounts opened before the report period, places the balance into the '0' column. * For those opened during the report period, places the balance into the appropriate column * for the month when it was opened. */ void calculateOpeningBalances(); /** * Calculate budget mapping * * For budget-vs-actual reports, this creates a mapping between each account * in the user's hierarchy and the account where the budget is held for it. * This is needed because the user can budget on a given account for that * account and all its descendants. Also if NO budget is placed on the * account or any of its parents, the account is not included in the map. */ void calculateBudgetMapping(); /** * Calculate the running sums. * * After calling this method, each cell of the report will contain the running sum of all * the cells in its row in this and earlier columns. * * For example, consider a row with these values: * 01 02 03 04 05 06 07 08 09 10 * * After calling this function, the row will look like this: * 01 03 06 10 15 21 28 36 45 55 */ void calculateRunningSums(); void calculateRunningSums(PivotInnerGroup::iterator& it_row); /** * This method calculates the difference between a @a budgeted and an @a * actual amount. The calculation is based on the type of the * @a repAccount. The difference value is calculated as follows: * * If @a repAccount is of type MyMoneyAccount::Income * * @code * diff = actual - budgeted * @endcode * * If @a repAccount is of type MyMoneyAccount::Expense * * @code * diff = budgeted - actual * @endcode * * In all other cases, 0 is returned. */ void calculateBudgetDiff(); /** * This method calculates forecast for a report */ void calculateForecast(); /** * This method inserts units to be used to display prices */ void fillBasePriceUnit(ERowType rowType); /** * This method collects the first date for which there is a price for every security */ QMap securityFirstPrice(); /** * This method calculates moving average for a report */ void calculateMovingAverage(); /** * Calculate the row and column totals * * This function will set the m_total members of all the TGrid objects. Be sure the values are * all converted to the base currency first!! * */ void calculateTotals(); /** * Convert each value in the grid to the base currency * */ void convertToBaseCurrency(); /** * Convert each value in the grid to the account/category's deep currency * * See AccountDescriptor::deepCurrencyPrice() for a description of 'deep' currency * */ void convertToDeepCurrency(); /** * Turn month-long columns into larger time periods if needed * * For example, consider a row with these values: * 01 02 03 04 05 06 07 08 09 10 * * If the column pitch is 3 (i.e. quarterly), after calling this function, * the row will look like this: * 06 15 26 10 */ void collapseColumns(); /** * Determine the proper column headings based on the time periods covered by each column * */ void calculateColumnHeadings(); /** * Helper methods for collapseColumns * */ void accumulateColumn(int destcolumn, int sourcecolumn); void clearColumn(int column); /** * Calculate the column of a given date. This is the absolute column in a * hypothetical report that covers all of known time. In reality an actual * report will be a subset of that. * * @param _date The date */ int columnValue(const QDate& _date) const; /** * Calculate the date of the last day covered by a given column. * * @param column The column */ QDate columnDate(int column) const; /** * Returns the balance of a given cell. Throws an exception once calculateRunningSums() has been run. */ MyMoneyMoney cellBalance(const QString& outergroup, const ReportAccount& _row, int column, bool budget); /** * Draws a PivotGridRowSet in a chart for the given ERowType */ unsigned drawChartRowSet(int rowNum, const bool seriesTotals, const bool accountSeries, KReportChartView& chartView, const PivotGridRowSet& rowSet, const ERowType rowType) const; /** * Loads m_rowTypeList with the list of PivotGridRow types that the reporttable * should show */ void loadRowTypeList(); /** * If not in expert mode, include all subaccounts for each selected * investment account */ void includeInvestmentSubAccounts(); /** * Returns the column which holds the current date * Returns -1 if the current date is not within range */ int currentDateColumn(); }; } + +QDebug operator<<(QDebug dbg, reports::PivotTable &a); #endif // PIVOTTABLE_H diff --git a/kmymoney/reports/reporttable.cpp b/kmymoney/reports/reporttable.cpp index 2e2ddd851..31385a4c8 100644 --- a/kmymoney/reports/reporttable.cpp +++ b/kmymoney/reports/reporttable.cpp @@ -1,138 +1,149 @@ /*************************************************************************** reporttable.cpp - description ------------------- begin : Fr Apr 16 2010 copyright : (C) 2010 Bernd Gonsior email : bgo@freeplexx.de ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "reporttable.h" // ---------------------------------------------------------------------------- // QT Includes // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes #include "kmymoneyglobalsettings.h" reports::ReportTable::ReportTable(): m_resourceHtml("html"), m_reportStyleSheet("reportstylesheet"), m_cssFileDefault("kmymoney.css") { // set up default values m_resourceType = QLatin1String("appdata").latin1(); } QString reports::ReportTable::cssFileNameGet() { QString cssfilename; if (!MyMoneyFile::instance()->value(m_reportStyleSheet).isEmpty()) { // try to find the stylesheet specific for this report cssfilename = KGlobal::dirs()-> findResource(m_resourceType, m_resourceHtml + '/' + MyMoneyFile::instance()-> value(m_reportStyleSheet)); } if (cssfilename.isEmpty()) { // if no report specific stylesheet was found, try to use the configured one cssfilename = KMyMoneyGlobalSettings::cssFileDefault(); } if (cssfilename.isEmpty()) { // if there still is nothing, try to use the installation default cssfilename = KGlobal::dirs()-> findResource(m_resourceType, m_resourceHtml + '/' + m_cssFileDefault); } return cssfilename; } QString reports::ReportTable::renderHeader(const QString& title, bool includeCSS) { QString header = QString("" + "\n\n" + "\n" + "\n" + title + ""; QString cssfilename = cssFileNameGet(); if (includeCSS) { // include css inline QFile cssFile(cssfilename); if (cssFile.open(QIODevice::ReadOnly)) { QTextStream cssStream(&cssFile); header += QString("\n\n"; cssFile.close(); } else { qDebug() << "reports::ReportTable::htmlHeaderGet: could not open file " << cssfilename << " readonly"; } } else { // do not include css inline instead use a link to the css file header += "\n\n"; } header += KMyMoneyUtils::variableCSS(); header += "\n\n"; return header; } QString reports::ReportTable::renderFooter() { return "\n\n"; } QString reports::ReportTable::renderHTML(QWidget* widget, const QByteArray& encoding, const QString& title, bool includeCSS) { m_encoding = encoding; //this render the HEAD tag and sets the correct css file QString html = renderHeader(title, includeCSS); try { //this method is implemented by each concrete class html += renderBody(); } catch (const MyMoneyException &e) { kDebug(2) << "reports::ReportTable::renderHTML(): ERROR " << e.what(); QString error = i18n("There was an error creating your report: \"%1\".\nPlease report this error to the developer's list: kmymoney-devel@kde.org", e.what()); KMessageBox::error(widget, error, i18n("Critical Error")); html += "

" + i18n("Unable to generate report") + "

" + error + "

"; } //this renders a common footer html += renderFooter(); return html; } + +QDebug operator<<(QDebug dbg, const reports::ReportTable &a) +{ + dbg << "reports::ReportTable(" + //<< "encoding" << a.m_encoding + //<< "resourceHtml" << a.m_resourceHtml + //<< "reportStyleSheet" << a.m_reportStyleSheet + //<< "cssFileDefault" << a.m_cssFileDefault + << ")"; + return dbg; +} diff --git a/kmymoney/reports/reporttable.h b/kmymoney/reports/reporttable.h index ac5c45f0c..59ee538b5 100644 --- a/kmymoney/reports/reporttable.h +++ b/kmymoney/reports/reporttable.h @@ -1,164 +1,165 @@ /*************************************************************************** reporttable.h ------------------- begin : Mon May 7 2007 copyright : (C) 2007 Thomas Baumgart email : Thomas Baumgart ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #ifndef REPORTTABLE_H #define REPORTTABLE_H // ---------------------------------------------------------------------------- // QT Includes #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "kmymoneyutils.h" #include "mymoneyfile.h" #include "mymoneyreport.h" namespace reports { class KReportChartView; /** * This class serves as base class definition for the concrete report classes * This class is abstract but it contains common code used by all children classes */ class ReportTable { private: /** * Tries to find a css file for the report. * * Search is done in following order: *
    *
  1. report specific stylesheet *
  2. configured stylesheet *
  3. installation default of stylesheet *
* * @retval css-filename if a css-file was found * @retval empty-string if no css-file was found */ QString cssFileNameGet(); /** * Name of application resource type. * * @see KGlobal::dirs()->findResource() */ const char* m_resourceType; /** * Subdirectory for html-resources of application. * * @see KGlobal::dirs()->findResource() */ QString m_resourceHtml; /** * Notation of @c reportstylesheet as used by: * @code * MyMoneyFile::instance()::value(); * @endcode */ QString m_reportStyleSheet; /** * Filename of default css file. */ QString m_cssFileDefault; /** * Character set encoding for the report. */ QByteArray m_encoding; protected: ReportTable(); /** * Constructs html header. * * @param title html title of report * @param[in] includeCSS flag, whether the generated html has to include the css inline or whether * the css is referenced as a link to a file * @return html header */ QString renderHeader(const QString& title, bool includeCSS); /** * Constructs html footer. * * @return html footer */ QString renderFooter(); /** * Constructs the body of the report. Implemented by the concrete classes * @see PivotTable * @see ListTable * @return QString with the html body of the report */ virtual QString renderBody() const = 0; public: virtual ~ReportTable() {} /** * Constructs a comma separated-file of the report. Implemented by the concrete classes * @see PivotTable * @see ListTable */ virtual QString renderCSV() const = 0; /** * Renders a graph from the report. Implemented by the concrete classes * @see PivotTable */ virtual void drawChart(KReportChartView& view) const = 0; virtual void dump(const QString& file, const QString& context = QString()) const = 0; /** * Creates the complete html document. * * @param widget parent widget * @param encoding character set encoding * @param title html title of report * @param includeCSS flag, whether the generated html has * to include the css inline or whether * the css is referenced as a link to a file * * @return complete html document */ QString renderHTML(QWidget* widget, const QByteArray& encoding, const QString& title, bool includeCSS = false); }; } +QDebug operator<<(QDebug dbg, const reports::ReportTable& a); #endif // REPORTTABLE_H