diff --git a/kmymoney/converter/mymoneytemplate.cpp b/kmymoney/converter/mymoneytemplate.cpp index fd658326a..bec05c995 100644 --- a/kmymoney/converter/mymoneytemplate.cpp +++ b/kmymoney/converter/mymoneytemplate.cpp @@ -1,503 +1,564 @@ /*************************************************************************** mymoneytemplate.cpp - description ------------------- begin : Sat Aug 14 2004 copyright : (C) 2004 by Thomas Baumgart email : ipwizard@users.sourceforge.net ***************************************************************************/ /*************************************************************************** * * * 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 "mymoneytemplate.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "kmymoneyutils.h" #include "mymoneyfile.h" #include "mymoneyaccount.h" #include "mymoneyexception.h" #include "mymoneyenums.h" MyMoneyTemplate::MyMoneyTemplate() : m_progressCallback(0), m_accountsRead(0) { } MyMoneyTemplate::MyMoneyTemplate(const QUrl& url) : m_progressCallback(0), m_accountsRead(0) { loadTemplate(url); } MyMoneyTemplate::~MyMoneyTemplate() { } bool MyMoneyTemplate::loadTemplate(const QUrl &url) { QString filename; bool downloadedFile = false; if (!url.isValid()) { qDebug("Invalid template URL '%s'", qPrintable(url.url())); return false; } m_source = url; if (url.isLocalFile()) { filename = url.toLocalFile(); } else { downloadedFile = true; KIO::StoredTransferJob *transferjob = KIO::storedGet (url); KJobWidgets::setWindow(transferjob, KMyMoneyUtils::mainWindow()); if (! transferjob->exec()) { KMessageBox::detailedError(KMyMoneyUtils::mainWindow(), i18n("Error while loading file '%1'.", url.url()), transferjob->errorString(), i18n("File access error")); return false; } QTemporaryFile file; file.setAutoRemove(false); file.open(); file.write(transferjob->data()); filename = file.fileName(); file.close(); } bool rc = true; QFile file(filename); QFileInfo info(file); if (!info.isFile()) { QString msg = i18n("

%1 is not a template file.

", filename); KMessageBox::error(KMyMoneyUtils::mainWindow(), msg, i18n("Filetype Error")); return false; } if (file.open(QIODevice::ReadOnly)) { QString errMsg; int errLine, errColumn; if (!m_doc.setContent(&file, &errMsg, &errLine, &errColumn)) { QString msg = i18n("

Error while reading template file %1 in line %2, column %3

", filename, errLine, errColumn); KMessageBox::detailedError(KMyMoneyUtils::mainWindow(), msg, errMsg, i18n("Template Error")); rc = false; } else { rc = loadDescription(); } file.close(); } else { KMessageBox::sorry(KMyMoneyUtils::mainWindow(), i18n("File '%1' not found.", filename)); rc = false; } // if a temporary file was downloaded, then it will be removed // with the next call. Otherwise, it stays untouched on the local // filesystem. if (downloadedFile) { QFile::remove(filename); } return rc; } bool MyMoneyTemplate::loadDescription() { int validMask = 0x00; const int validAccount = 0x01; const int validTitle = 0x02; const int validShort = 0x04; const int validLong = 0x08; const int invalid = 0x10; const int validHeader = 0x0F; QDomElement rootElement = m_doc.documentElement(); if (!rootElement.isNull() && rootElement.tagName() == "kmymoney-account-template") { QDomNode child = rootElement.firstChild(); while (!child.isNull() && child.isElement()) { QDomElement childElement = child.toElement(); // qDebug("MyMoneyTemplate::import: Processing child node %s", childElement.tagName().data()); if (childElement.tagName() == "accounts") { m_accounts = childElement.firstChild(); validMask |= validAccount; } else if (childElement.tagName() == "title") { m_title = childElement.text(); validMask |= validTitle; } else if (childElement.tagName() == "shortdesc") { m_shortDesc = childElement.text(); validMask |= validShort; } else if (childElement.tagName() == "longdesc") { m_longDesc = childElement.text(); validMask |= validLong; } else { KMessageBox::error(KMyMoneyUtils::mainWindow(), i18n("

Invalid tag %1 in template file %2

", childElement.tagName(), m_source.toDisplayString())); validMask |= invalid; } child = child.nextSibling(); } } return validMask == validHeader; } bool MyMoneyTemplate::hierarchy(QMap& list, const QString& parent, QDomNode account) { bool rc = true; while (rc == true && !account.isNull()) { if (account.isElement()) { QDomElement accountElement = account.toElement(); if (accountElement.tagName() == "account") { QString name = QString("%1:%2").arg(parent).arg(accountElement.attribute("name")); list[name] = 0; hierarchy(list, name, account.firstChild()); } } account = account.nextSibling(); } return rc; } void MyMoneyTemplate::hierarchy(QMap& list) { bool rc = !m_accounts.isNull(); QDomNode accounts = m_accounts; while (rc == true && !accounts.isNull() && accounts.isElement()) { QDomElement rootNode = accounts.toElement(); QString name = rootNode.attribute("name"); if (rootNode.tagName() == "account") { rootNode = rootNode.firstChild().toElement(); eMyMoney::Account::Type type = static_cast(accounts.toElement().attribute("type").toUInt()); switch (type) { case eMyMoney::Account::Type::Asset: case eMyMoney::Account::Type::Liability: case eMyMoney::Account::Type::Income: case eMyMoney::Account::Type::Expense: case eMyMoney::Account::Type::Equity: if (name.isEmpty()) name = MyMoneyAccount::accountTypeToString(type); list[name] = 0; rc = hierarchy(list, name, rootNode); break; default: rc = false; break; } } else { rc = false; } accounts = accounts.nextSibling(); } } bool MyMoneyTemplate::importTemplate(void(*callback)(int, int, const QString&)) { m_progressCallback = callback; bool rc = !m_accounts.isNull(); MyMoneyFile* file = MyMoneyFile::instance(); signalProgress(0, m_doc.elementsByTagName("account").count(), i18n("Loading template %1", m_source.url())); m_accountsRead = 0; while (rc == true && !m_accounts.isNull() && m_accounts.isElement()) { QDomElement childElement = m_accounts.toElement(); if (childElement.tagName() == "account") { ++m_accountsRead; MyMoneyAccount parent; switch (childElement.attribute("type").toUInt()) { case (uint)eMyMoney::Account::Type::Asset: parent = file->asset(); break; case (uint)eMyMoney::Account::Type::Liability: parent = file->liability(); break; case (uint)eMyMoney::Account::Type::Income: parent = file->income(); break; case (uint)eMyMoney::Account::Type::Expense: parent = file->expense(); break; case (uint)eMyMoney::Account::Type::Equity: parent = file->equity(); break; default: KMessageBox::error(KMyMoneyUtils::mainWindow(), i18n("

Invalid top-level account type %1 in template file %2

", childElement.attribute("type"), m_source.toDisplayString())); rc = false; } if (rc == true) { if (childElement.attribute("name").isEmpty()) rc = createAccounts(parent, childElement.firstChild()); else rc = createAccounts(parent, childElement); } } else { rc = false; } m_accounts = m_accounts.nextSibling(); } + + /* + * Resolve imported vat account assignments + * + * The template account id of the assigned vat account + * is stored temporarly in the account key/value pair + * 'UnresolvedVatAccount' and resolved below. + */ + QList accounts; + file->accountList(accounts); + foreach (MyMoneyAccount acc, accounts) { + if (!acc.pairs().contains("UnresolvedVatAccount")) + continue; + QString id = acc.value("UnresolvedVatAccount"); + acc.setValue("VatAccount", m_vatAccountMap[id]); + acc.deletePair("UnresolvedVatAccount"); + MyMoneyFile::instance()->modifyAccount(acc); + } + signalProgress(-1, -1); return rc; } bool MyMoneyTemplate::createAccounts(MyMoneyAccount& parent, QDomNode account) { bool rc = true; while (rc == true && !account.isNull()) { MyMoneyAccount acc; if (account.isElement()) { QDomElement accountElement = account.toElement(); if (accountElement.tagName() == "account") { signalProgress(++m_accountsRead, 0); QList subAccountList; QList::ConstIterator it; it = subAccountList.constEnd(); if (!parent.accountList().isEmpty()) { MyMoneyFile::instance()->accountList(subAccountList, parent.accountList()); for (it = subAccountList.constBegin(); it != subAccountList.constEnd(); ++it) { if ((*it).name() == accountElement.attribute("name")) { acc = *it; + QString id = accountElement.attribute("id"); + if (!id.isEmpty()) + m_vatAccountMap[id] = acc.id(); break; } } } if (it == subAccountList.constEnd()) { // not found, we need to create it acc.setName(accountElement.attribute("name")); acc.setAccountType(static_cast(accountElement.attribute("type").toUInt())); setFlags(acc, account.firstChild()); try { MyMoneyFile::instance()->addAccount(acc, parent); } catch (const MyMoneyException &) { } + QString id = accountElement.attribute("id"); + if (!id.isEmpty()) + m_vatAccountMap[id] = acc.id(); } createAccounts(acc, account.firstChild()); } } account = account.nextSibling(); } return rc; } bool MyMoneyTemplate::setFlags(MyMoneyAccount& acc, QDomNode flags) { bool rc = true; while (rc == true && !flags.isNull()) { if (flags.isElement()) { QDomElement flagElement = flags.toElement(); if (flagElement.tagName() == "flag") { // make sure, we only store flags we know! QString value = flagElement.attribute("name"); if (value == "Tax") { - acc.setValue(value.toLatin1(), "Yes"); + acc.setValue(value, "Yes"); + } else if (value == "VatRate") { + acc.setValue(value, flagElement.attribute("value")); + } else if (value == "VatAccount") { + // will be resolved later in importTemplate() + acc.setValue("UnresolvedVatAccount", flagElement.attribute("value")); } else if (value == "OpeningBalanceAccount") { acc.setValue("OpeningBalanceAccount", "Yes"); } else { KMessageBox::error(KMyMoneyUtils::mainWindow(), i18n("

Invalid flag type %1 for account %3 in template file %2

", flagElement.attribute("name"), m_source.toDisplayString(), acc.name())); rc = false; } QString currency = flagElement.attribute("currency"); if (!currency.isEmpty()) acc.setCurrencyId(currency); } } flags = flags.nextSibling(); } return rc; } void MyMoneyTemplate::signalProgress(int current, int total, const QString& msg) { if (m_progressCallback != 0) (*m_progressCallback)(current, total, msg); } bool MyMoneyTemplate::exportTemplate(void(*callback)(int, int, const QString&)) { m_progressCallback = callback; + // prepare vat account map + QList accountList; + MyMoneyFile::instance()->accountList(accountList); + int i = 0; + QList::Iterator it; + for (it = accountList.begin(); it != accountList.end(); ++it) { + if (!(*it).pairs().contains("VatAccount")) + continue; + m_vatAccountMap[(*it).value("VatAccount")] = QString::fromLatin1("%1").arg(i++, 3, 10, QLatin1Char('0')); + } + m_doc = QDomDocument("KMYMONEY-TEMPLATE"); QDomProcessingInstruction instruct = m_doc.createProcessingInstruction(QString("xml"), QString("version=\"1.0\" encoding=\"utf-8\"")); m_doc.appendChild(instruct); QDomElement mainElement = m_doc.createElement("kmymoney-account-template"); m_doc.appendChild(mainElement); QDomElement title = m_doc.createElement("title"); QDomText t = m_doc.createTextNode(m_title); title.appendChild(t); mainElement.appendChild(title); QDomElement shortDesc = m_doc.createElement("shortdesc"); t = m_doc.createTextNode(m_shortDesc); shortDesc.appendChild(t); mainElement.appendChild(shortDesc); QDomElement longDesc = m_doc.createElement("longdesc"); t = m_doc.createTextNode(m_longDesc); longDesc.appendChild(t); mainElement.appendChild(longDesc); QDomElement accounts = m_doc.createElement("accounts"); mainElement.appendChild(accounts); addAccountStructure(accounts, MyMoneyFile::instance()->asset()); addAccountStructure(accounts, MyMoneyFile::instance()->expense()); addAccountStructure(accounts, MyMoneyFile::instance()->income()); addAccountStructure(accounts, MyMoneyFile::instance()->liability()); addAccountStructure(accounts, MyMoneyFile::instance()->equity()); return true; } const QString& MyMoneyTemplate::title() const { return m_title; } const QString& MyMoneyTemplate::shortDescription() const { return m_shortDesc; } const QString& MyMoneyTemplate::longDescription() const { return m_longDesc; } void MyMoneyTemplate::setTitle(const QString &s) { m_title = s; } void MyMoneyTemplate::setShortDescription(const QString &s) { m_shortDesc = s; } void MyMoneyTemplate::setLongDescription(const QString &s) { m_longDesc = s; } static bool nameLessThan(MyMoneyAccount &a1, MyMoneyAccount &a2) { return a1.name() < a2.name(); } bool MyMoneyTemplate::addAccountStructure(QDomElement& parent, const MyMoneyAccount& acc) { QDomElement account = m_doc.createElement("account"); parent.appendChild(account); if (MyMoneyFile::instance()->isStandardAccount(acc.id())) account.setAttribute(QString("name"), QString()); else account.setAttribute(QString("name"), acc.name()); account.setAttribute(QString("type"), (int)acc.accountType()); - // FIXME: add tax flag stuff + if (acc.pairs().contains("Tax")) { + QDomElement flag = m_doc.createElement("flag"); + flag.setAttribute(QString("name"), "Tax"); + flag.setAttribute(QString("value"), acc.value("Tax")); + account.appendChild(flag); + } + if (m_vatAccountMap.contains(acc.id())) + account.setAttribute(QString("id"), m_vatAccountMap[acc.id()]); + + if (acc.pairs().contains("VatRate")) { + QDomElement flag = m_doc.createElement("flag"); + flag.setAttribute(QString("name"), "VatRate"); + flag.setAttribute(QString("value"), acc.value("VatRate")); + account.appendChild(flag); + } + if (acc.pairs().contains("VatAccount")) { + QDomElement flag = m_doc.createElement("flag"); + flag.setAttribute(QString("name"), "VatAccount"); + flag.setAttribute(QString("value"), m_vatAccountMap[acc.value("VatAccount")]); + account.appendChild(flag); + } if (acc.pairs().contains("OpeningBalanceAccount")) { QString openingBalanceAccount = acc.value("OpeningBalanceAccount"); if (openingBalanceAccount == "Yes") { QDomElement flag = m_doc.createElement("flag"); flag.setAttribute(QString("name"), "OpeningBalanceAccount"); flag.setAttribute(QString("currency"), acc.currencyId()); account.appendChild(flag); } } // any child accounts? if (acc.accountList().count() > 0) { QList list; MyMoneyFile::instance()->accountList(list, acc.accountList(), false); qSort(list.begin(), list.end(), nameLessThan); QList::Iterator it; for (it = list.begin(); it != list.end(); ++it) { addAccountStructure(account, *it); } } return true; } bool MyMoneyTemplate::saveTemplate(const QUrl &url) { QString filename; if (!url.isValid()) { qDebug("Invalid template URL '%s'", qPrintable(url.url())); return false; } if (url.isLocalFile()) { filename = url.toLocalFile(); QSaveFile qfile(filename/*, 0600*/); if (qfile.open(QIODevice::WriteOnly)) { saveToLocalFile(&qfile); if (!qfile.commit()) { throw MYMONEYEXCEPTION(QString::fromLatin1("Unable to write changes to '%1'").arg(filename)); } } else { throw MYMONEYEXCEPTION(QString::fromLatin1("Unable to write changes to '%1'").arg(filename)); } } else { QTemporaryFile tmpfile; tmpfile.open(); QSaveFile qfile(tmpfile.fileName()); if (qfile.open(QIODevice::WriteOnly)) { saveToLocalFile(&qfile); if (!qfile.commit()) { throw MYMONEYEXCEPTION(QString::fromLatin1("Unable to upload to '%1'").arg(url.toDisplayString())); } } else { throw MYMONEYEXCEPTION(QString::fromLatin1("Unable to upload to '%1'").arg(url.toDisplayString())); } int permission = -1; QFile file(tmpfile.fileName()); file.open(QIODevice::ReadOnly); KIO::StoredTransferJob *putjob = KIO::storedPut(file.readAll(), url, permission, KIO::JobFlag::Overwrite); if (!putjob->exec()) { throw MYMONEYEXCEPTION(QString::fromLatin1("Unable to upload to '%1'.
%2").arg(url.toDisplayString(), putjob->errorString())); } file.close(); } return true; } bool MyMoneyTemplate::saveToLocalFile(QSaveFile* qfile) { QTextStream stream(qfile); stream.setCodec("UTF-8"); stream << m_doc.toString(); stream.flush(); return true; } diff --git a/kmymoney/converter/mymoneytemplate.h b/kmymoney/converter/mymoneytemplate.h index ad35923ad..bdc0f1483 100644 --- a/kmymoney/converter/mymoneytemplate.h +++ b/kmymoney/converter/mymoneytemplate.h @@ -1,98 +1,99 @@ /*************************************************************************** mymoneytemplate.h - description ------------------- begin : Sat Aug 14 2004 copyright : (C) 2004 by Thomas Baumgart email : ipwizard@users.sourceforge.net ***************************************************************************/ /*************************************************************************** * * * 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 MYMONEYTEMPLATE_H #define MYMONEYTEMPLATE_H // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include class QTreeWidgetItem; class QSaveFile; // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes /** * @author Thomas Baumgart */ /** * This class represents an account template handler. It is capable * to read an XML formatted account template file and import it into * the current engine. Also, it can save the current account structure * of the engine to an XML formatted template file. */ class MyMoneyAccount; class MyMoneyTemplate { public: MyMoneyTemplate(); explicit MyMoneyTemplate(const QUrl &url); ~MyMoneyTemplate(); bool loadTemplate(const QUrl &url); bool saveTemplate(const QUrl &url); bool importTemplate(void(*callback)(int, int, const QString&)); bool exportTemplate(void(*callback)(int, int, const QString&)); const QString& title() const; const QString& shortDescription() const; const QString& longDescription() const; void setTitle(const QString &s); void setShortDescription(const QString &s); void setLongDescription(const QString &s); void hierarchy(QMap& list); protected: bool loadDescription(); bool createAccounts(MyMoneyAccount& parent, QDomNode account); bool setFlags(MyMoneyAccount& acc, QDomNode flags); bool saveToLocalFile(QSaveFile* qfile); bool addAccountStructure(QDomElement& parent, const MyMoneyAccount& acc); bool hierarchy(QMap& list, const QString& parent, QDomNode account); /** * This method is used to update the progress information. It * checks if an appropriate function is known and calls it. * * For a parameter description see KMyMoneyView::progressCallback(). */ void signalProgress(int current, int total, const QString& = ""); private: QDomDocument m_doc; QDomNode m_accounts; QString m_title; QString m_shortDesc; QString m_longDesc; QUrl m_source; void (*m_progressCallback)(int, int, const QString&); int m_accountsRead; + QMap m_vatAccountMap; }; #endif diff --git a/kmymoney/plugins/views/reports/core/CMakeLists.txt b/kmymoney/plugins/views/reports/core/CMakeLists.txt index 8652df58d..0e79c4a26 100644 --- a/kmymoney/plugins/views/reports/core/CMakeLists.txt +++ b/kmymoney/plugins/views/reports/core/CMakeLists.txt @@ -1,28 +1,29 @@ if(BUILD_TESTING) add_subdirectory(tests) endif() set (libreports_a_SOURCES + cashflowlist.cpp kreportchartview.cpp reportaccount.cpp listtable.cpp objectinfotable.cpp pivotgrid.cpp pivottable.cpp querytable.cpp reporttable.cpp ) add_library(reports STATIC ${libreports_a_SOURCES}) target_link_libraries(reports PUBLIC KChart Alkimia::alkimia Qt5::PrintSupport kmymoney_common kmm_settings PRIVATE KF5::I18n ) add_dependencies(reports kmm_settings) diff --git a/kmymoney/plugins/views/reports/core/cashflowlist.cpp b/kmymoney/plugins/views/reports/core/cashflowlist.cpp new file mode 100644 index 000000000..323fc6413 --- /dev/null +++ b/kmymoney/plugins/views/reports/core/cashflowlist.cpp @@ -0,0 +1,161 @@ +/* + * Copyright 2007 Sascha Pfau + * Copyright 2018 Ralf Habacker + * + * 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. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/* + * This file contains code from the func_xirr and related methods of + * financial.cpp from KOffice by Sascha Pfau. Sascha agreed to relicense + * those methods under GPLv2 or later. + */ + +#include "cashflowlist.h" + +#include "mymoneyexception.h" + +#include +#include + +#include + +/** + * Calculates the internal rate of return for a non-periodic + * series of cash flows. The calculation is based on a 365 days + * per year basis, ignoring leap years. + * In case the internal rate of return could not be calculated + * a MyMoneyException is raised. + * + * @param rate optional guess for rate + * @return internal rate of return + */ +double CashFlowList::XIRR(double rate) const +{ + if (size() < 2) + throw MYMONEYEXCEPTION("illegal argument exception"); + + double resultRate = rate; + + // define max epsilon + static const double maxEpsilon = 1e-10; + + // max number of iterations + static const int maxIter = 50; + + // Newton's method - try to find a res, with a accuracy of maxEpsilon + double newRate, rateEpsilon, resultValue; + int iter = 0; + bool contLoop = false; + int iterScan = 0; + bool resultRateScanEnd = false; + + // First the inner while-loop will be executed using the default Value resultRate + // or the user guessed resultRate if those do not deliver a solution for the + // Newton's method then the range from -0.99 to +0.99 will be scanned with a + // step size of 0.01 to find resultRate's value which can deliver a solution + // source hint: + // - outer loop from libreoffice + // - inner loop from KOffice + do { + if (iterScan >=1) + resultRate = -0.99 + (iterScan -1)* 0.01; + + do { + resultValue = xirrResult(resultRate); + newRate = resultRate - resultValue / xirrResultDerive(resultRate); + rateEpsilon = fabs(newRate - resultRate); + resultRate = newRate; + contLoop = (rateEpsilon > maxEpsilon) && (fabs(resultValue) > maxEpsilon); + } while (contLoop && (++iter < maxIter)); + iter = 0; + if (std::isinf(resultRate) || std::isnan(resultRate) || + std::isinf(resultValue) || std::isnan(resultValue)) + contLoop = true; + iterScan++; + resultRateScanEnd = (iterScan >= 200); + } while(contLoop && !resultRateScanEnd); + + if (contLoop) + throw MYMONEYEXCEPTION("illegal argument exception"); + return resultRate; +} + +/** + * Calculates the resulting amount for the passed interest rate + * + * @param rate interest rate + * @return resulting amount + */ +double CashFlowList::xirrResult(double rate) const +{ + double r = rate + 1.0; + double result = at(0).value().toDouble(); + const QDate &date0 = at(0).date(); + + for(int i = 1; i < size(); i++) { + double e_i = date0.daysTo(at(i).date()) / 365.0; + result += at(i).value().toDouble() / pow(r, e_i); + } + return result; +} + +/** + * Calculates the first derivation of the resulting amount + * + * @param rate interest rate + * @return first derivation of resulting amount + */ +double CashFlowList::xirrResultDerive(double rate) const +{ + double r = rate + 1.0; + double result = 0; + const QDate &date0 = at(0).date(); + + for(int i = 1; i < size(); i++) { + double e_i = date0.daysTo(at(i).date()) / 365.0; + result -= e_i * at(i).value().toDouble() / pow(r, e_i + 1.0); + } + return result; +} + +/** + * Return the sum of all payments + * + * @return sum of all payments + */ +MyMoneyMoney CashFlowList::total() const +{ + MyMoneyMoney result; + + const_iterator it_cash = begin(); + while (it_cash != end()) { + result += (*it_cash).value(); + ++it_cash; + } + + return result; +} + +/** + * dump all payments + */ +void CashFlowList::dumpDebug() const +{ + const_iterator it_item = begin(); + while (it_item != end()) { + qDebug() << (*it_item).date().toString(Qt::ISODate) << " " << (*it_item).value().toString(); + ++it_item; + } +} diff --git a/kmymoney/plugins/views/reports/core/cashflowlist.h b/kmymoney/plugins/views/reports/core/cashflowlist.h new file mode 100644 index 000000000..15efad4fb --- /dev/null +++ b/kmymoney/plugins/views/reports/core/cashflowlist.h @@ -0,0 +1,75 @@ +/* + * Copyright 2007 Sascha Pfau + * Copyright 2018 Ralf Habacker + * + * 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. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/* + * This file contains code from the func_xirr and related methods of + * financial.cpp from KOffice by Sascha Pfau. Sascha agreed to relicense + * those methods under GPLv2 or later. + */ + +#ifndef CASHFLOWLIST_H +#define CASHFLOWLIST_H + +#include "mymoneymoney.h" + +#include +#include + +class CashFlowListItem +{ +public: + CashFlowListItem() {} + CashFlowListItem(const QDate& _date, const MyMoneyMoney& _value): m_date(_date), m_value(_value) {} + bool operator<(const CashFlowListItem& _second) const { + return m_date < _second.m_date; + } + bool operator<=(const CashFlowListItem& _second) const { + return m_date <= _second.m_date; + } + bool operator>(const CashFlowListItem& _second) const { + return m_date > _second.m_date; + } + const QDate& date() const { + return m_date; + } + const MyMoneyMoney& value() const { + return m_value; + } + +private: + QDate m_date; + MyMoneyMoney m_value; +}; + +/** + * Cash flow analysis tools for investment reports + */ +class CashFlowList: public QList +{ +public: + CashFlowList() {} + double XIRR(double rate = 0.1) const; + MyMoneyMoney total() const; + void dumpDebug() const; + +private: + double xirrResult(double rate) const; + double xirrResultDerive(double rate) const; +}; + +#endif // CASHFLOWLIST_H diff --git a/kmymoney/plugins/views/reports/core/querytable.cpp b/kmymoney/plugins/views/reports/core/querytable.cpp index 965d06bf0..377c7a59f 100644 --- a/kmymoney/plugins/views/reports/core/querytable.cpp +++ b/kmymoney/plugins/views/reports/core/querytable.cpp @@ -1,2170 +1,1972 @@ /* * Copyright 2005 Ace Jones * Copyright 2017-2018 Łukasz Wojniłowicz * * 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. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -/*************************************************************************** - * * - * 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. * - * * - ***************************************************************************/ - -/**************************************************************************** - Contains code from the func_xirr and related methods of financial.cpp - - KOffice 1.6 by Sascha Pfau. Sascha agreed to relicense those methods under - GPLv2 or later. -*****************************************************************************/ - #include "querytable.h" #include // ---------------------------------------------------------------------------- // QT Includes #include #include // ---------------------------------------------------------------------------- // KDE Includes #include // ---------------------------------------------------------------------------- // Project Includes +#include "cashflowlist.h" #include "mymoneyfile.h" #include "mymoneyaccount.h" #include "mymoneysecurity.h" #include "mymoneyinstitution.h" #include "mymoneyprice.h" #include "mymoneypayee.h" #include "mymoneytag.h" #include "mymoneysplit.h" #include "mymoneytransaction.h" #include "mymoneyreport.h" #include "mymoneyexception.h" #include "kmymoneyutils.h" #include "reportaccount.h" #include "mymoneyenums.h" namespace reports { -// **************************************************************************** -// -// CashFlowListItem implementation -// -// Cash flow analysis tools for investment reports -// -// **************************************************************************** - -QDate CashFlowListItem::m_sToday = QDate::currentDate(); - -MyMoneyMoney CashFlowListItem::NPV(double _rate) const -{ - double T = static_cast(m_sToday.daysTo(m_date)) / 365.0; - MyMoneyMoney result(m_value.toDouble() / pow(1 + _rate, T), 100); - - //qDebug() << "CashFlowListItem::NPV( " << _rate << " ) == " << result; - - return result; -} - -// **************************************************************************** -// -// CashFlowList implementation -// -// Cash flow analysis tools for investment reports -// -// **************************************************************************** - -CashFlowListItem CashFlowList::mostRecent() const -{ - CashFlowList dupe(*this); - - qSort(dupe); - - //qDebug() << " CashFlowList::mostRecent() == " << dupe.back().date().toString(Qt::ISODate); - - return dupe.back(); -} - -MyMoneyMoney CashFlowList::NPV(double _rate) const -{ - MyMoneyMoney result; - - const_iterator it_cash = constBegin(); - while (it_cash != constEnd()) { - result += (*it_cash).NPV(_rate); - ++it_cash; - } - - //qDebug() << "CashFlowList::NPV( " << _rate << " ) == " << result << "------------------------" << endl; - - return result; -} - -double CashFlowList::calculateXIRR() const -{ - double resultRate = 0.00001; - - double resultZero = 0.00000; - //if ( args.count() > 2 ) - // resultRate = calc->conv()->asFloat ( args[2] ).asFloat(); - -// check pairs and count >= 2 and guess > -1.0 - //if ( args[0].count() != args[1].count() || args[1].count() < 2 || resultRate <= -1.0 ) - // return Value::errorVALUE(); - -// define max epsilon - static const double maxEpsilon = 1e-5; - -// max number of iterations - static const int maxIter = 50; - -// Newton's method - try to find a res, with a accuracy of maxEpsilon - double rateEpsilon, newRate, resultValue; - int i = 0; - bool contLoop; - - do { - resultValue = xirrResult(resultRate); - - double resultDerive = xirrResultDerive(resultRate); - - //check what happens if xirrResultDerive is zero - //Don't know if it is correct to dismiss the result - if (resultDerive != 0) { - newRate = resultRate - resultValue / resultDerive; - } else { - - newRate = resultRate - resultValue; - } - - rateEpsilon = fabs(newRate - resultRate); - - resultRate = newRate; - contLoop = (rateEpsilon > maxEpsilon) && (fabs(resultValue) > maxEpsilon); - } while (contLoop && (++i < maxIter)); - - if (contLoop) - return resultZero; - - return resultRate; -} - -double CashFlowList::xirrResult(double& rate) const -{ - double r = rate + 1.0; - double res = 0.00000;//back().value().toDouble(); - - QList::const_iterator list_it = constBegin(); - while (list_it != constEnd()) { - double e_i = ((* list_it).today().daysTo((* list_it).date())) / 365.0; - MyMoneyMoney val = (* list_it).value(); - - if (e_i < 0) { - res += val.toDouble() * pow(r, -e_i); - } else { - res += val.toDouble() / pow(r, e_i); - } - ++list_it; - } - - return res; -} - - -double CashFlowList::xirrResultDerive(double& rate) const -{ - double r = rate + 1.0; - double res = 0.00000; - - QList::const_iterator list_it = constBegin(); - while (list_it != constEnd()) { - double e_i = ((* list_it).today().daysTo((* list_it).date())) / 365.0; - MyMoneyMoney val = (* list_it).value(); - - res -= e_i * val.toDouble() / pow(r, e_i + 1.0); - ++list_it; - } - - return res; -} - -double CashFlowList::IRR() const -{ - double result = 0.0; - - // set 'today', which is the most recent of all dates in the list - CashFlowListItem::setToday(mostRecent().date()); - - result = calculateXIRR(); - return result; -} - -MyMoneyMoney CashFlowList::total() const -{ - MyMoneyMoney result; - - const_iterator it_cash = constBegin(); - while (it_cash != constEnd()) { - result += (*it_cash).value(); - ++it_cash; - } - - return result; -} - -void CashFlowList::dumpDebug() const -{ - const_iterator it_item = constBegin(); - while (it_item != constEnd()) { - qDebug() << (*it_item).date().toString(Qt::ISODate) << " " << (*it_item).value().toString(); - ++it_item; - } -} - // **************************************************************************** // // QueryTable implementation // // **************************************************************************** /** * TODO * * - Collapse 2- & 3- groups when they are identical * - Way more test cases (especially splits & transfers) * - Option to collapse splits * - Option to exclude transfers * */ QueryTable::QueryTable(const MyMoneyReport& _report): ListTable(_report) { // separated into its own method to allow debugging (setting breakpoints // directly in ctors somehow does not work for me (ipwizard)) // TODO: remove the init() method and move the code back to the ctor init(); } void QueryTable::init() { m_columns.clear(); m_group.clear(); m_subtotal.clear(); m_postcolumns.clear(); switch (m_config.rowType()) { case eMyMoney::Report::RowType::AccountByTopAccount: case eMyMoney::Report::RowType::EquityType: case eMyMoney::Report::RowType::AccountType: case eMyMoney::Report::RowType::Institution: constructAccountTable(); m_columns << ctAccount; break; case eMyMoney::Report::RowType::Account: constructTransactionTable(); m_columns << ctAccountID << ctPostDate; break; case eMyMoney::Report::RowType::Payee: case eMyMoney::Report::RowType::Tag: case eMyMoney::Report::RowType::Month: case eMyMoney::Report::RowType::Week: constructTransactionTable(); m_columns << ctPostDate << ctAccount; break; case eMyMoney::Report::RowType::CashFlow: constructSplitsTable(); m_columns << ctPostDate; break; default: constructTransactionTable(); m_columns << ctPostDate; } // Sort the data to match the report definition m_subtotal << ctValue; switch (m_config.rowType()) { case eMyMoney::Report::RowType::CashFlow: m_group << ctCategoryType << ctTopCategory << ctCategory; break; case eMyMoney::Report::RowType::Category: m_group << ctCategoryType << ctTopCategory << ctCategory; break; case eMyMoney::Report::RowType::TopCategory: m_group << ctCategoryType << ctTopCategory; break; case eMyMoney::Report::RowType::TopAccount: m_group << ctTopAccount << ctAccount; break; case eMyMoney::Report::RowType::Account: m_group << ctAccount; break; case eMyMoney::Report::RowType::AccountReconcile: m_group << ctAccount << ctReconcileFlag; break; case eMyMoney::Report::RowType::Payee: m_group << ctPayee; break; case eMyMoney::Report::RowType::Tag: m_group << ctTag; break; case eMyMoney::Report::RowType::Month: m_group << ctMonth; break; case eMyMoney::Report::RowType::Week: m_group << ctWeek; break; case eMyMoney::Report::RowType::AccountByTopAccount: m_group << ctTopAccount; break; case eMyMoney::Report::RowType::EquityType: m_group << ctEquityType; break; case eMyMoney::Report::RowType::AccountType: m_group << ctType; break; case eMyMoney::Report::RowType::Institution: m_group << ctInstitution << ctTopAccount; break; default: throw MYMONEYEXCEPTION_CSTRING("QueryTable::QueryTable(): unhandled row type"); } QVector sort = QVector::fromList(m_group) << QVector::fromList(m_columns) << ctID << ctRank; m_columns.clear(); switch (m_config.rowType()) { case eMyMoney::Report::RowType::AccountByTopAccount: case eMyMoney::Report::RowType::EquityType: case eMyMoney::Report::RowType::AccountType: case eMyMoney::Report::RowType::Institution: m_columns << ctAccount; break; default: m_columns << ctPostDate; } unsigned qc = m_config.queryColumns(); if (qc & eMyMoney::Report::QueryColumn::Number) m_columns << ctNumber; if (qc & eMyMoney::Report::QueryColumn::Payee) m_columns << ctPayee; if (qc & eMyMoney::Report::QueryColumn::Tag) m_columns << ctTag; if (qc & eMyMoney::Report::QueryColumn::Category) m_columns << ctCategory; if (qc & eMyMoney::Report::QueryColumn::Account) m_columns << ctAccount; if (qc & eMyMoney::Report::QueryColumn::Reconciled) m_columns << ctReconcileFlag; if (qc & eMyMoney::Report::QueryColumn::Memo) m_columns << ctMemo; if (qc & eMyMoney::Report::QueryColumn::Action) m_columns << ctAction; if (qc & eMyMoney::Report::QueryColumn::Shares) m_columns << ctShares; if (qc & eMyMoney::Report::QueryColumn::Price) m_columns << ctPrice; if (qc & eMyMoney::Report::QueryColumn::Performance) { m_subtotal.clear(); switch (m_config.investmentSum()) { case eMyMoney::Report::InvestmentSum::OwnedAndSold: m_columns << ctBuys << ctSells << ctReinvestIncome << ctCashIncome << ctEndingBalance << ctReturn << ctReturnInvestment; m_subtotal << ctBuys << ctSells << ctReinvestIncome << ctCashIncome << ctEndingBalance << ctReturn << ctReturnInvestment; break; case eMyMoney::Report::InvestmentSum::Owned: m_columns << ctBuys << ctReinvestIncome << ctMarketValue << ctReturn << ctReturnInvestment; m_subtotal << ctBuys << ctReinvestIncome << ctMarketValue << ctReturn << ctReturnInvestment; break; case eMyMoney::Report::InvestmentSum::Sold: m_columns << ctBuys << ctSells << ctCashIncome << ctReturn << ctReturnInvestment; m_subtotal << ctBuys << ctSells << ctCashIncome << ctReturn << ctReturnInvestment; break; case eMyMoney::Report::InvestmentSum::Period: default: m_columns << ctStartingBalance << ctBuys << ctSells << ctReinvestIncome << ctCashIncome << ctEndingBalance << ctReturn << ctReturnInvestment; m_subtotal << ctStartingBalance << ctBuys << ctSells << ctReinvestIncome << ctCashIncome << ctEndingBalance << ctReturn << ctReturnInvestment; break; } } if (qc & eMyMoney::Report::QueryColumn::CapitalGain) { m_subtotal.clear(); switch (m_config.investmentSum()) { case eMyMoney::Report::InvestmentSum::Owned: m_columns << ctShares << ctBuyPrice << ctLastPrice << ctBuys << ctMarketValue << ctPercentageGain << ctCapitalGain; m_subtotal << ctShares << ctBuyPrice << ctLastPrice << ctBuys << ctMarketValue << ctPercentageGain << ctCapitalGain; break; case eMyMoney::Report::InvestmentSum::Sold: default: m_columns << ctBuys << ctSells << ctCapitalGain; m_subtotal << ctBuys << ctSells << ctCapitalGain; if (m_config.isShowingSTLTCapitalGains()) { m_columns << ctBuysST << ctSellsST << ctCapitalGainST << ctBuysLT << ctSellsLT << ctCapitalGainLT; m_subtotal << ctBuysST << ctSellsST << ctCapitalGainST << ctBuysLT << ctSellsLT << ctCapitalGainLT; } break; } } if (qc & eMyMoney::Report::QueryColumn::Loan) { m_columns << ctPayment << ctInterest << ctFees; m_postcolumns << ctBalance; } if (qc & eMyMoney::Report::QueryColumn::Balance) m_postcolumns << ctBalance; TableRow::setSortCriteria(sort); qSort(m_rows); if (m_config.isShowingColumnTotals()) constructTotalRows(); // adds total rows to m_rows } void QueryTable::constructTotalRows() { if (m_rows.isEmpty()) return; // qSort places grand total at last position, because it doesn't belong to any group for (int i = 0; i < m_rows.count(); ++i) { if (m_rows.at(0)[ctRank] == QLatin1String("4") || m_rows.at(0)[ctRank] == QLatin1String("5")) // it should be unlikely that total row is at the top of rows, so... m_rows.move(0, m_rows.count() - 1 - i); // ...move it at the bottom else break; } MyMoneyFile* file = MyMoneyFile::instance(); QList subtotals = m_subtotal; QList groups = m_group; QList columns = m_columns; if (!m_subtotal.isEmpty() && subtotals.count() == 1) columns.append(m_subtotal); QList postcolumns = m_postcolumns; if (!m_postcolumns.isEmpty()) columns.append(postcolumns); QMap>> totalCurrency; QList> totalGroups; QMap totalsValues; // initialize all total values under summed columns to be zero foreach (auto subtotal, subtotals) { totalsValues.insert(subtotal, MyMoneyMoney()); } totalsValues.insert(ctRowsCount, MyMoneyMoney()); // create total groups containing totals row for each group totalGroups.append(totalsValues); // prepend with extra group for grand total for (int j = 0; j < groups.count(); ++j) { totalGroups.append(totalsValues); } QList stashedTotalRows; int iCurrentRow, iNextRow; for (iCurrentRow = 0; iCurrentRow < m_rows.count();) { iNextRow = iCurrentRow + 1; // total rows are useless at summing so remove whole block of them at once while (iNextRow != m_rows.count() && (m_rows.at(iNextRow).value(ctRank) == QLatin1String("4") || m_rows.at(iNextRow).value(ctRank) == QLatin1String("5"))) { stashedTotalRows.append(m_rows.takeAt(iNextRow)); // ...but stash them just in case } bool lastRow = (iNextRow == m_rows.count()); // sum all subtotal values for lowest group QString currencyID = m_rows.at(iCurrentRow).value(ctCurrency); if (m_rows.at(iCurrentRow).value(ctRank) == QLatin1String("1")) { // don't sum up on balance (rank = 0 || rank = 3) and minor split (rank = 2) foreach (auto subtotal, subtotals) { if (!totalCurrency.contains(currencyID)) totalCurrency[currencyID].append(totalGroups); totalCurrency[currencyID].last()[subtotal] += MyMoneyMoney(m_rows.at(iCurrentRow)[subtotal]); } totalCurrency[currencyID].last()[ctRowsCount] += MyMoneyMoney::ONE; } // iterate over groups from the lowest to the highest to find group change for (int i = groups.count() - 1; i >= 0 ; --i) { // if any of groups from next row changes (or next row is the last row), then it's time to put totals row if (lastRow || m_rows.at(iCurrentRow)[groups.at(i)] != m_rows.at(iNextRow)[groups.at(i)]) { bool isMainCurrencyTotal = true; QMap>>::iterator currencyGrp = totalCurrency.begin(); while (currencyGrp != totalCurrency.end()) { if (!MyMoneyMoney((*currencyGrp).at(i + 1).value(ctRowsCount)).isZero()) { // if no rows summed up, then no totals row TableRow totalsRow; // sum all subtotal values for higher groups (excluding grand total) and reset lowest group values QMap::iterator upperGrp = (*currencyGrp)[i].begin(); QMap::iterator lowerGrp = (*currencyGrp)[i + 1].begin(); while(upperGrp != (*currencyGrp)[i].end()) { totalsRow[lowerGrp.key()] = lowerGrp.value().toString(); // fill totals row with subtotal values... (*upperGrp) += (*lowerGrp); // (*lowerGrp) = MyMoneyMoney(); ++upperGrp; ++lowerGrp; } // custom total values calculations foreach (auto subtotal, subtotals) { if (subtotal == ctReturnInvestment) totalsRow[subtotal] = helperROI((*currencyGrp).at(i + 1).value(ctBuys) - (*currencyGrp).at(i + 1).value(ctReinvestIncome), (*currencyGrp).at(i + 1).value(ctSells), (*currencyGrp).at(i + 1).value(ctStartingBalance), (*currencyGrp).at(i + 1).value(ctEndingBalance) + (*currencyGrp).at(i + 1).value(ctMarketValue), - (*currencyGrp).at(i + 1).value(ctCashIncome)).toString(); + (*currencyGrp).at(i + 1).value(ctCashIncome)); else if (subtotal == ctPercentageGain) totalsRow[subtotal] = (((*currencyGrp).at(i + 1).value(ctBuys) + (*currencyGrp).at(i + 1).value(ctMarketValue)) / (*currencyGrp).at(i + 1).value(ctBuys).abs()).toString(); else if (subtotal == ctPrice) totalsRow[subtotal] = MyMoneyMoney((*currencyGrp).at(i + 1).value(ctPrice) / (*currencyGrp).at(i + 1).value(ctRowsCount)).toString(); } // total values that aren't calculated here, but are taken untouched from external source, e.g. constructPerformanceRow if (!stashedTotalRows.isEmpty()) { for (int j = 0; j < stashedTotalRows.count(); ++j) { if (stashedTotalRows.at(j).value(ctCurrency) != currencyID) continue; foreach (auto subtotal, subtotals) { if (subtotal == ctReturn) totalsRow[ctReturn] = stashedTotalRows.takeAt(j)[ctReturn]; } break; } } (*currencyGrp).replace(i + 1, totalsValues); for (int j = 0; j < groups.count(); ++j) { totalsRow[groups.at(j)] = m_rows.at(iCurrentRow)[groups.at(j)]; // ...and identification } currencyID = currencyGrp.key(); if (currencyID.isEmpty() && totalCurrency.count() > 1) currencyID = file->baseCurrency().id(); totalsRow[ctCurrency] = currencyID; if (isMainCurrencyTotal) { totalsRow[ctRank] = QLatin1Char('4'); isMainCurrencyTotal = false; } else totalsRow[ctRank] = QLatin1Char('5'); totalsRow[ctDepth] = QString::number(i); totalsRow.remove(ctRowsCount); m_rows.insert(iNextRow++, totalsRow); // iCurrentRow and iNextRow can diverge here by more than one } ++currencyGrp; } } } // code to put grand total row if (lastRow) { bool isMainCurrencyTotal = true; QMap>>::iterator currencyGrp = totalCurrency.begin(); while (currencyGrp != totalCurrency.end()) { TableRow totalsRow; QMap::const_iterator grandTotalGrp = (*currencyGrp)[0].constBegin(); while(grandTotalGrp != (*currencyGrp)[0].constEnd()) { totalsRow[grandTotalGrp.key()] = grandTotalGrp.value().toString(); ++grandTotalGrp; } foreach (auto subtotal, subtotals) { if (subtotal == ctReturnInvestment) totalsRow[subtotal] = helperROI((*currencyGrp).at(0).value(ctBuys) - (*currencyGrp).at(0).value(ctReinvestIncome), (*currencyGrp).at(0).value(ctSells), (*currencyGrp).at(0).value(ctStartingBalance), (*currencyGrp).at(0).value(ctEndingBalance) + (*currencyGrp).at(0).value(ctMarketValue), - (*currencyGrp).at(0).value(ctCashIncome)).toString(); + (*currencyGrp).at(0).value(ctCashIncome)); else if (subtotal == ctPercentageGain) totalsRow[subtotal] = (((*currencyGrp).at(0).value(ctBuys) + (*currencyGrp).at(0).value(ctMarketValue)) / (*currencyGrp).at(0).value(ctBuys).abs()).toString(); else if (subtotal == ctPrice) totalsRow[subtotal] = MyMoneyMoney((*currencyGrp).at(0).value(ctPrice) / (*currencyGrp).at(0).value(ctRowsCount)).toString(); } if (!stashedTotalRows.isEmpty()) { for (int j = 0; j < stashedTotalRows.count(); ++j) { foreach (auto subtotal, subtotals) { if (subtotal == ctReturn) totalsRow[ctReturn] = stashedTotalRows.takeAt(j)[ctReturn]; } } } for (int j = 0; j < groups.count(); ++j) { totalsRow[groups.at(j)] = QString(); // no identification } currencyID = currencyGrp.key(); if (currencyID.isEmpty() && totalCurrency.count() > 1) currencyID = file->baseCurrency().id(); totalsRow[ctCurrency] = currencyID; if (isMainCurrencyTotal) { totalsRow[ctRank] = QLatin1Char('4'); isMainCurrencyTotal = false; } else totalsRow[ctRank] = QLatin1Char('5'); totalsRow[ctDepth] = QString(); m_rows.append(totalsRow); ++currencyGrp; } break; // no use to loop further } iCurrentRow = iNextRow; // iCurrent makes here a leap forward by at least one } } void QueryTable::constructTransactionTable() { MyMoneyFile* file = MyMoneyFile::instance(); //make sure we have all subaccounts of investment accounts includeInvestmentSubAccounts(); MyMoneyReport report(m_config); report.setReportAllSplits(false); report.setConsiderCategory(true); bool use_transfers; bool use_summary; bool hide_details; bool tag_special_case = false; switch (m_config.rowType()) { case eMyMoney::Report::RowType::Category: case eMyMoney::Report::RowType::TopCategory: use_summary = false; use_transfers = false; hide_details = false; break; case eMyMoney::Report::RowType::Payee: use_summary = false; use_transfers = false; hide_details = (m_config.detailLevel() == eMyMoney::Report::DetailLevel::None); break; case eMyMoney::Report::RowType::Tag: use_summary = false; use_transfers = false; hide_details = (m_config.detailLevel() == eMyMoney::Report::DetailLevel::None); tag_special_case = true; break; default: use_summary = true; use_transfers = true; hide_details = (m_config.detailLevel() == eMyMoney::Report::DetailLevel::None); break; } // support for opening and closing balances QMap accts; //get all transactions for this report QList transactions = file->transactionList(report); for (QList::const_iterator it_transaction = transactions.constBegin(); it_transaction != transactions.constEnd(); ++it_transaction) { TableRow qA, qS; QDate pd; QList tagIdListCache; qA[ctID] = qS[ctID] = (* it_transaction).id(); qA[ctEntryDate] = qS[ctEntryDate] = (* it_transaction).entryDate().toString(Qt::ISODate); qA[ctPostDate] = qS[ctPostDate] = (* it_transaction).postDate().toString(Qt::ISODate); qA[ctCommodity] = qS[ctCommodity] = (* it_transaction).commodity(); pd = (* it_transaction).postDate(); qA[ctMonth] = qS[ctMonth] = i18n("Month of %1", QDate(pd.year(), pd.month(), 1).toString(Qt::ISODate)); qA[ctWeek] = qS[ctWeek] = i18n("Week of %1", pd.addDays(1 - pd.dayOfWeek()).toString(Qt::ISODate)); if (!m_containsNonBaseCurrency && (*it_transaction).commodity() != file->baseCurrency().id()) m_containsNonBaseCurrency = true; if (report.isConvertCurrency()) qA[ctCurrency] = qS[ctCurrency] = file->baseCurrency().id(); else qA[ctCurrency] = qS[ctCurrency] = (*it_transaction).commodity(); // to handle splits, we decide on which account to base the split // (a reference point or point of view so to speak). here we take the // first account that is a stock account or loan account (or the first account // that is not an income or expense account if there is no stock or loan account) // to be the account (qA) that will have the sub-item "split" entries. we add // one transaction entry (qS) for each subsequent entry in the split. const QList& splits = (*it_transaction).splits(); QList::const_iterator myBegin, it_split; for (it_split = splits.constBegin(), myBegin = splits.constEnd(); it_split != splits.constEnd(); ++it_split) { ReportAccount splitAcc((* it_split).accountId()); // always put split with a "stock" account if it exists if (splitAcc.isInvest()) break; // prefer to put splits with a "loan" account if it exists if (splitAcc.isLoan()) myBegin = it_split; if ((myBegin == splits.end()) && ! splitAcc.isIncomeExpense()) { myBegin = it_split; } } // select our "reference" split if (it_split == splits.end()) { it_split = myBegin; } else { myBegin = it_split; } // skip this transaction if we didn't find a valid base account - see the above description // for the base account's description - if we don't find it avoid a crash by skipping the transaction if (myBegin == splits.end()) continue; // if the split is still unknown, use the first one. I have seen this // happen with a transaction that has only a single split referencing an income or expense // account and has an amount and value of 0. Such a transaction will fall through // the above logic and leave 'it_split' pointing to splits.end() which causes the remainder // of this to end in an infinite loop. if (it_split == splits.end()) { it_split = splits.begin(); } // for "loan" reports, the loan transaction gets special treatment. // the splits of a loan transaction are placed on one line in the // reference (loan) account (qA). however, we process the matching // split entries (qS) normally. bool loan_special_case = false; if (m_config.queryColumns() & eMyMoney::Report::QueryColumn::Loan) { ReportAccount splitAcc((*it_split).accountId()); loan_special_case = splitAcc.isLoan(); } bool include_me = true; bool transaction_text = false; //indicates whether a text should be considered as a match for the transaction or for a split only QString a_fullname; QString a_memo; int pass = 1; QString myBeginCurrency; QString baseCurrency = file->baseCurrency().id(); QMap xrMap; // container for conversion rates from given currency to myBeginCurrency do { MyMoneyMoney xr; ReportAccount splitAcc((* it_split).accountId()); QString splitCurrency; if (splitAcc.isInvest()) splitCurrency = file->account(file->account((*it_split).accountId()).parentAccountId()).currencyId(); else splitCurrency = file->account((*it_split).accountId()).currencyId(); if (it_split == myBegin) myBeginCurrency = splitCurrency; //get fraction for account int fraction = splitAcc.currency().smallestAccountFraction(); //use base currency fraction if not initialized if (fraction == -1) fraction = file->baseCurrency().smallestAccountFraction(); QString institution = splitAcc.institutionId(); QString payee = (*it_split).payeeId(); const QList tagIdList = (*it_split).tagIdList(); //convert to base currency if (m_config.isConvertCurrency()) { xr = xrMap.value(splitCurrency, xr); // check if there is conversion rate to myBeginCurrency already stored... if (xr == MyMoneyMoney()) // ...if not... xr = (*it_split).price(); // ...take conversion rate to myBeginCurrency from split else if (splitAcc.isInvest()) // if it's stock split... xr *= (*it_split).price(); // ...multiply it by stock price stored in split if (!m_containsNonBaseCurrency && myBeginCurrency != baseCurrency) m_containsNonBaseCurrency = true; if (myBeginCurrency != baseCurrency) { // myBeginCurrency can differ from baseCurrency... MyMoneyPrice price = file->price(myBeginCurrency, baseCurrency, (*it_transaction).postDate()); // ...so check conversion rate... if (price.isValid()) { xr *= price.rate(baseCurrency); // ...and multiply it by current price... qA[ctCurrency] = qS[ctCurrency] = baseCurrency; } else qA[ctCurrency] = qS[ctCurrency] = myBeginCurrency; // ...and set information about non-baseCurrency } } else if (splitAcc.isInvest()) xr = (*it_split).price(); else xr = MyMoneyMoney::ONE; if (it_split == myBegin) { include_me = m_config.includes(splitAcc); if (include_me) // track accts that will need opening and closing balances //FIXME in some cases it will show the opening and closing //balances but no transactions if the splits are all filtered out -- asoliverez accts.insert(splitAcc.id(), splitAcc); qA[ctAccount] = splitAcc.name(); qA[ctAccountID] = splitAcc.id(); qA[ctTopAccount] = splitAcc.topParentName(); if (splitAcc.isInvest()) { // use the institution of the parent for stock accounts institution = splitAcc.parent().institutionId(); MyMoneyMoney shares = (*it_split).shares(); int pricePrecision = file->security(splitAcc.currencyId()).pricePrecision(); qA[ctAction] = (*it_split).action(); qA[ctShares] = shares.isZero() ? QString() : shares.toString(); qA[ctPrice] = shares.isZero() ? QString() : xr.convertPrecision(pricePrecision).toString(); if (((*it_split).action() == MyMoneySplit::actionName(eMyMoney::Split::Action::BuyShares)) && shares.isNegative()) qA[ctAction] = "Sell"; qA[ctInvestAccount] = splitAcc.parent().name(); MyMoneySplit stockSplit = (*it_split); MyMoneySplit assetAccountSplit; QList feeSplits; QList interestSplits; MyMoneySecurity currency; MyMoneySecurity security; eMyMoney::Split::InvestmentTransactionType transactionType; KMyMoneyUtils::dissectTransaction((*it_transaction), stockSplit, assetAccountSplit, feeSplits, interestSplits, security, currency, transactionType); if (!(assetAccountSplit == MyMoneySplit())) { for (it_split = splits.begin(); it_split != splits.end(); ++it_split) { if ((*it_split) == assetAccountSplit) { splitAcc = ReportAccount(assetAccountSplit.accountId()); // switch over from stock split to asset split because amount in stock split doesn't take fees/interests into account myBegin = it_split; // set myBegin to asset split, so stock split can be listed in details under splits myBeginCurrency = (file->account((*myBegin).accountId())).currencyId(); if (!m_containsNonBaseCurrency && myBeginCurrency != baseCurrency) m_containsNonBaseCurrency = true; if (m_config.isConvertCurrency()) { if (myBeginCurrency != baseCurrency) { MyMoneyPrice price = file->price(myBeginCurrency, baseCurrency, (*it_transaction).postDate()); if (price.isValid()) { xr = price.rate(baseCurrency); qA[ctCurrency] = qS[ctCurrency] = baseCurrency; } else qA[ctCurrency] = qS[ctCurrency] = myBeginCurrency; } else xr = MyMoneyMoney::ONE; qA[ctPrice] = shares.isZero() ? QString() : (stockSplit.price() * xr / (*it_split).price()).toString(); // put conversion rate for all splits with this currency, so... // every split of transaction have the same conversion rate xrMap.insert(splitCurrency, MyMoneyMoney::ONE / (*it_split).price()); } else xr = (*it_split).price(); break; } } } } else qA[ctPrice] = xr.toString(); a_fullname = splitAcc.fullName(); a_memo = (*it_split).memo(); transaction_text = m_config.match((*it_split)); qA[ctInstitution] = institution.isEmpty() ? i18n("No Institution") : file->institution(institution).name(); qA[ctPayee] = payee.isEmpty() ? i18n("[Empty Payee]") : file->payee(payee).name().simplified(); if (tag_special_case) { tagIdListCache = tagIdList; } else { QString delimiter; foreach(const auto tagId, tagIdList) { qA[ctTag] += delimiter + file->tag(tagId).name().simplified(); delimiter = QLatin1Char(','); } } qA[ctReconcileDate] = (*it_split).reconcileDate().toString(Qt::ISODate); qA[ctReconcileFlag] = KMyMoneyUtils::reconcileStateToString((*it_split).reconcileFlag(), true); qA[ctNumber] = (*it_split).number(); qA[ctMemo] = a_memo; qA[ctValue] = ((*it_split).shares() * xr).convert(fraction).toString(); qS[ctReconcileDate] = qA[ctReconcileDate]; qS[ctReconcileFlag] = qA[ctReconcileFlag]; qS[ctNumber] = qA[ctNumber]; qS[ctTopCategory] = splitAcc.topParentName(); qS[ctCategoryType] = i18n("Transfer"); // only include the configured accounts if (include_me) { if (loan_special_case) { // put the principal amount in the "value" column and convert to lowest fraction qA[ctValue] = (-(*it_split).shares() * xr).convert(fraction).toString(); qA[ctRank] = QLatin1Char('1'); qA[ctSplit].clear(); } else { if ((splits.count() > 2) && use_summary) { // add the "summarized" split transaction // this is the sub-total of the split detail // convert to lowest fraction qA[ctRank] = QLatin1Char('1'); qA[ctCategory] = i18n("[Split Transaction]"); qA[ctTopCategory] = i18nc("Split transaction", "Split"); qA[ctCategoryType] = i18nc("Split transaction", "Split"); m_rows += qA; } } } } else { if (include_me) { if (loan_special_case) { MyMoneyMoney value = (-(* it_split).shares() * xr).convert(fraction); if ((*it_split).action() == MyMoneySplit::actionName(eMyMoney::Split::Action::Amortization)) { // put the payment in the "payment" column and convert to lowest fraction qA[ctPayee] = value.toString(); } else if ((*it_split).action() == MyMoneySplit::actionName(eMyMoney::Split::Action::Interest)) { // put the interest in the "interest" column and convert to lowest fraction qA[ctInterest] = value.toString(); } else if (splits.count() > 2) { // [dv: This comment carried from the original code. I am // not exactly clear on what it means or why we do this.] // Put the initial pay-in nowhere (that is, ignore it). This // is dangerous, though. The only way I can tell the initial // pay-in apart from fees is if there are only 2 splits in // the transaction. I wish there was a better way. } else { // accumulate everything else in the "fees" column MyMoneyMoney n0 = MyMoneyMoney(qA[ctFees]); qA[ctFees] = (n0 + value).toString(); } // we don't add qA here for a loan transaction. we'll add one // qA after all of the split components have been processed. // (see below) } //--- special case to hide split transaction details else if (hide_details && (splits.count() > 2)) { // essentially, don't add any qA entries } //--- default case includes all transaction details else { //this is when the splits are going to be shown as children of the main split if ((splits.count() > 2) && use_summary) { qA[ctValue].clear(); //convert to lowest fraction qA[ctSplit] = (-(*it_split).shares() * xr).convert(fraction).toString(); qA[ctRank] = QLatin1Char('2'); } else { //this applies when the transaction has only 2 splits, or each split is going to be //shown separately, eg. transactions by category switch (m_config.rowType()) { case eMyMoney::Report::RowType::Category: case eMyMoney::Report::RowType::TopCategory: if (splitAcc.isIncomeExpense()) qA[ctValue] = (-(*it_split).shares() * xr).convert(fraction).toString(); // needed for category reports, in case of multicurrency transaction it breaks it break; default: break; } qA[ctSplit].clear(); qA[ctRank] = QLatin1Char('1'); } qA [ctMemo] = (*it_split).memo(); if (!m_containsNonBaseCurrency && splitAcc.currencyId() != file->baseCurrency().id()) m_containsNonBaseCurrency = true; if (report.isConvertCurrency()) qS[ctCurrency] = file->baseCurrency().id(); else qS[ctCurrency] = splitAcc.currency().id(); if (! splitAcc.isIncomeExpense()) { qA[ctCategory] = ((*it_split).shares().isNegative()) ? i18n("Transfer from %1", splitAcc.fullName()) : i18n("Transfer to %1", splitAcc.fullName()); qA[ctTopCategory] = splitAcc.topParentName(); qA[ctCategoryType] = i18n("Transfer"); } else { qA [ctCategory] = splitAcc.fullName(); qA [ctTopCategory] = splitAcc.topParentName(); qA [ctCategoryType] = MyMoneyAccount::accountTypeToString(splitAcc.accountGroup()); } if (use_transfers || (splitAcc.isIncomeExpense() && m_config.includes(splitAcc))) { //if it matches the text of the main split of the transaction or //it matches this particular split, include it //otherwise, skip it //if the filter is "does not contain" exclude the split if it does not match //even it matches the whole split if ((m_config.isInvertingText() && m_config.match((*it_split))) || (!m_config.isInvertingText() && (transaction_text || m_config.match((*it_split))))) { if (tag_special_case) { if (!tagIdListCache.size()) qA[ctTag] = i18n("[No Tag]"); else for (int i = 0; i < tagIdListCache.size(); i++) { qA[ctTag] = file->tag(tagIdListCache[i]).name().simplified(); m_rows += qA; } } else { m_rows += qA; } } } } } if (m_config.includes(splitAcc) && use_transfers && !(splitAcc.isInvest() && include_me)) { // otherwise stock split is displayed twice in report if (! splitAcc.isIncomeExpense()) { //multiply by currency and convert to lowest fraction qS[ctValue] = ((*it_split).shares() * xr).convert(fraction).toString(); qS[ctRank] = QLatin1Char('1'); qS[ctAccount] = splitAcc.name(); qS[ctAccountID] = splitAcc.id(); qS[ctTopAccount] = splitAcc.topParentName(); qS[ctCategory] = ((*it_split).shares().isNegative()) ? i18n("Transfer to %1", a_fullname) : i18n("Transfer from %1", a_fullname); qS[ctInstitution] = institution.isEmpty() ? i18n("No Institution") : file->institution(institution).name(); qS[ctMemo] = (*it_split).memo().isEmpty() ? a_memo : (*it_split).memo(); //FIXME-ALEX When is used this? I can't find in which condition we arrive here... maybe this code is useless? QString delimiter; for (int i = 0; i < tagIdList.size(); i++) { qA[ctTag] += delimiter + file->tag(tagIdList[i]).name().simplified(); delimiter = '+'; } qS[ctPayee] = payee.isEmpty() ? qA[ctPayee] : file->payee(payee).name().simplified(); //check the specific split against the filter for text and amount //TODO this should be done at the engine, but I have no clear idea how -- asoliverez //if the filter is "does not contain" exclude the split if it does not match //even it matches the whole split if ((m_config.isInvertingText() && m_config.match((*it_split))) || (!m_config.isInvertingText() && (transaction_text || m_config.match((*it_split))))) { m_rows += qS; // track accts that will need opening and closing balances accts.insert(splitAcc.id(), splitAcc); } } } } ++it_split; // look for wrap-around if (it_split == splits.end()) it_split = splits.begin(); // but terminate if this transaction has only a single split if (splits.count() < 2) break; //check if there have been more passes than there are splits //this is to prevent infinite loops in cases of data inconsistency -- asoliverez ++pass; if (pass > splits.count()) break; } while (it_split != myBegin); if (loan_special_case) { m_rows += qA; } } // now run through our accts list and add opening and closing balances switch (m_config.rowType()) { case eMyMoney::Report::RowType::Account: case eMyMoney::Report::RowType::TopAccount: break; // case eMyMoney::Report::RowType::Category: // case MyMoneyReport::eTopCategory: // case MyMoneyReport::ePayee: // case MyMoneyReport::eMonth: // case MyMoneyReport::eWeek: default: return; } QDate startDate, endDate; report.validDateRange(startDate, endDate); QString strStartDate = startDate.toString(Qt::ISODate); QString strEndDate = endDate.toString(Qt::ISODate); startDate = startDate.addDays(-1); for (auto it_account = accts.constBegin(); it_account != accts.constEnd(); ++it_account) { TableRow qA; ReportAccount account(*it_account); //get fraction for account int fraction = account.currency().smallestAccountFraction(); //use base currency fraction if not initialized if (fraction == -1) fraction = file->baseCurrency().smallestAccountFraction(); QString institution = account.institutionId(); // use the institution of the parent for stock accounts if (account.isInvest()) institution = account.parent().institutionId(); MyMoneyMoney startBalance, endBalance, startPrice, endPrice; MyMoneyMoney startShares, endShares; //get price and convert currency if necessary if (m_config.isConvertCurrency()) { startPrice = (account.deepCurrencyPrice(startDate) * account.baseCurrencyPrice(startDate)).reduce(); endPrice = (account.deepCurrencyPrice(endDate) * account.baseCurrencyPrice(endDate)).reduce(); } else { startPrice = account.deepCurrencyPrice(startDate).reduce(); endPrice = account.deepCurrencyPrice(endDate).reduce(); } startShares = file->balance(account.id(), startDate); endShares = file->balance(account.id(), endDate); //get starting and ending balances startBalance = startShares * startPrice; endBalance = endShares * endPrice; //starting balance // don't show currency if we're converting or if it's not foreign if (!m_containsNonBaseCurrency && account.currency().id() != file->baseCurrency().id()) m_containsNonBaseCurrency = true; if (m_config.isConvertCurrency()) qA[ctCurrency] = file->baseCurrency().id(); else qA[ctCurrency] = account.currency().id(); qA[ctAccountID] = account.id(); qA[ctAccount] = account.name(); qA[ctTopAccount] = account.topParentName(); qA[ctInstitution] = institution.isEmpty() ? i18n("No Institution") : file->institution(institution).name(); qA[ctRank] = QLatin1Char('0'); qA[ctPrice] = startPrice.convertPrecision(account.currency().pricePrecision()).toString(); if (account.isInvest()) { qA[ctShares] = startShares.toString(); } qA[ctPostDate] = strStartDate; qA[ctBalance] = startBalance.convert(fraction).toString(); qA[ctValue].clear(); qA[ctID] = QLatin1Char('A'); m_rows += qA; //ending balance qA[ctPrice] = endPrice.convertPrecision(account.currency().pricePrecision()).toString(); if (account.isInvest()) { qA[ctShares] = endShares.toString(); } qA[ctPostDate] = strEndDate; qA[ctBalance] = endBalance.toString(); qA[ctRank] = QLatin1Char('3'); qA[ctID] = QLatin1Char('Z'); m_rows += qA; } } -MyMoneyMoney QueryTable::helperROI(const MyMoneyMoney &buys, const MyMoneyMoney &sells, const MyMoneyMoney &startingBal, const MyMoneyMoney &endingBal, const MyMoneyMoney &cashIncome) const +QString QueryTable::helperROI(const MyMoneyMoney &buys, const MyMoneyMoney &sells, const MyMoneyMoney &startingBal, const MyMoneyMoney &endingBal, const MyMoneyMoney &cashIncome) const { MyMoneyMoney returnInvestment; if (!buys.isZero() || !startingBal.isZero()) { returnInvestment = (sells + buys + cashIncome + endingBal - startingBal) / (startingBal - buys); - returnInvestment = returnInvestment.convert(10000); + return returnInvestment.convert(10000).toString(); } else - returnInvestment = MyMoneyMoney(); // if no investment then no return on investment - return returnInvestment; + return QString(); } -MyMoneyMoney QueryTable::helperIRR(const CashFlowList &all) const +QString QueryTable::helperIRR(const CashFlowList &all) const { - MyMoneyMoney annualReturn; try { - double irr = all.IRR(); -#ifdef Q_CC_MSVC - annualReturn = MyMoneyMoney(_isnan(irr) ? 0 : irr, 10000); -#else - annualReturn = MyMoneyMoney(std::isnan(irr) ? 0 : irr, 10000); -#endif - } catch (QString e) { - qDebug() << e; + return MyMoneyMoney(all.XIRR(), 10000).toString(); + } catch (MyMoneyException &e) { + qDebug() << e.what(); + all.dumpDebug(); + return QString(); } - return annualReturn; } void QueryTable::sumInvestmentValues(const ReportAccount& account, QList& cfList, QList& shList) const { for (int i = InvestmentValue::Buys; i < InvestmentValue::End; ++i) cfList.append(CashFlowList()); for (int i = InvestmentValue::Buys; i <= InvestmentValue::BuysOfOwned; ++i) shList.append(MyMoneyMoney()); MyMoneyFile* file = MyMoneyFile::instance(); MyMoneyReport report = m_config; QDate startingDate; QDate endingDate; QDate newStartingDate; QDate newEndingDate; const bool isSTLT = report.isShowingSTLTCapitalGains(); const int settlementPeriod = report.settlementPeriod(); QDate termSeparator = report.termSeparator().addDays(-settlementPeriod); report.validDateRange(startingDate, endingDate); newStartingDate = startingDate; newEndingDate = endingDate; if (report.queryColumns() & eMyMoney::Report::QueryColumn::CapitalGain) { // Saturday and Sunday aren't valid settlement dates if (endingDate.dayOfWeek() == Qt::Saturday) endingDate = endingDate.addDays(-1); else if (endingDate.dayOfWeek() == Qt::Sunday) endingDate = endingDate.addDays(-2); if (termSeparator.dayOfWeek() == Qt::Saturday) termSeparator = termSeparator.addDays(-1); else if (termSeparator.dayOfWeek() == Qt::Sunday) termSeparator = termSeparator.addDays(-2); if (startingDate.daysTo(endingDate) <= settlementPeriod) // no days to check for return; termSeparator = termSeparator.addDays(-settlementPeriod); newEndingDate = endingDate.addDays(-settlementPeriod); } shList[BuysOfOwned] = file->balance(account.id(), newEndingDate); // get how many shares there are at the end of period MyMoneyMoney stashedBuysOfOwned = shList.at(BuysOfOwned); bool reportedDateRange = true; // flag marking sell transactions between startingDate and endingDate report.setReportAllSplits(false); report.setConsiderCategory(true); report.clearAccountFilter(); report.addAccount(account.id()); report.setDateFilter(newStartingDate, newEndingDate); do { QList transactions = file->transactionList(report); for (QList::const_reverse_iterator it_t = transactions.crbegin(); it_t != transactions.crend(); ++it_t) { MyMoneySplit shareSplit = (*it_t).splitByAccount(account.id()); MyMoneySplit assetAccountSplit; QList feeSplits; QList interestSplits; MyMoneySecurity security; MyMoneySecurity currency; eMyMoney::Split::InvestmentTransactionType transactionType; KMyMoneyUtils::dissectTransaction((*it_t), shareSplit, assetAccountSplit, feeSplits, interestSplits, security, currency, transactionType); QDate postDate = (*it_t).postDate(); MyMoneyMoney price; //get price for the day of the transaction if we have to calculate base currency //we are using the value of the split which is in deep currency if (m_config.isConvertCurrency()) price = account.baseCurrencyPrice(postDate); //we only need base currency because the value is in deep currency else price = MyMoneyMoney::ONE; MyMoneyMoney value = assetAccountSplit.value() * price; MyMoneyMoney shares = shareSplit.shares(); if (transactionType == eMyMoney::Split::InvestmentTransactionType::BuyShares) { if (reportedDateRange) { cfList[Buys].append(CashFlowListItem(postDate, value)); shList[Buys] += shares; } if (shList.at(BuysOfOwned).isZero()) { // add sold shares if (shList.at(BuysOfSells) + shares > shList.at(Sells).abs()) { // add partially sold shares MyMoneyMoney tempVal = (((shList.at(Sells).abs() - shList.at(BuysOfSells))) / shares) * value; cfList[BuysOfSells].append(CashFlowListItem(postDate, tempVal)); shList[BuysOfSells] = shList.at(Sells).abs(); if (isSTLT && postDate < termSeparator) { cfList[LongTermBuysOfSells].append(CashFlowListItem(postDate, tempVal)); shList[LongTermBuysOfSells] = shList.at(BuysOfSells); } } else { // add wholly sold shares cfList[BuysOfSells].append(CashFlowListItem(postDate, value)); shList[BuysOfSells] += shares; if (isSTLT && postDate < termSeparator) { cfList[LongTermBuysOfSells].append(CashFlowListItem(postDate, value)); shList[LongTermBuysOfSells] += shares; } } } else if (shList.at(BuysOfOwned) >= shares) { // subtract not-sold shares shList[BuysOfOwned] -= shares; cfList[BuysOfOwned].append(CashFlowListItem(postDate, value)); } else { // subtract partially not-sold shares MyMoneyMoney tempVal = ((shares - shList.at(BuysOfOwned)) / shares) * value; MyMoneyMoney tempVal2 = (shares - shList.at(BuysOfOwned)); cfList[BuysOfSells].append(CashFlowListItem(postDate, tempVal)); shList[BuysOfSells] += tempVal2; if (isSTLT && postDate < termSeparator) { cfList[LongTermBuysOfSells].append(CashFlowListItem(postDate, tempVal)); shList[LongTermBuysOfSells] += tempVal2; } cfList[BuysOfOwned].append(CashFlowListItem(postDate, (shList.at(BuysOfOwned) / shares) * value)); shList[BuysOfOwned] = MyMoneyMoney(); } } else if (transactionType == eMyMoney::Split::InvestmentTransactionType::SellShares && reportedDateRange) { cfList[Sells].append(CashFlowListItem(postDate, value)); shList[Sells] += shares; } else if (transactionType == eMyMoney::Split::InvestmentTransactionType::SplitShares) { // shares variable is denominator of split ratio here for (int i = Buys; i <= InvestmentValue::BuysOfOwned; ++i) shList[i] /= shares; } else if (transactionType == eMyMoney::Split::InvestmentTransactionType::AddShares || // added shares, when sold give 100% capital gain transactionType == eMyMoney::Split::InvestmentTransactionType::ReinvestDividend) { if (shList.at(BuysOfOwned).isZero()) { // add added/reinvested shares if (shList.at(BuysOfSells) + shares > shList.at(Sells).abs()) { // add partially added/reinvested shares shList[BuysOfSells] = shList.at(Sells).abs(); if (postDate < termSeparator) shList[LongTermBuysOfSells] = shList[BuysOfSells]; } else { // add wholly added/reinvested shares shList[BuysOfSells] += shares; if (postDate < termSeparator) shList[LongTermBuysOfSells] += shares; } } else if (shList.at(BuysOfOwned) >= shares) { // subtract not-added/not-reinvested shares shList[BuysOfOwned] -= shares; cfList[BuysOfOwned].append(CashFlowListItem(postDate, value)); } else { // subtract partially not-added/not-reinvested shares MyMoneyMoney tempVal = (shares - shList.at(BuysOfOwned)); shList[BuysOfSells] += tempVal; if (postDate < termSeparator) shList[LongTermBuysOfSells] += tempVal; cfList[BuysOfOwned].append(CashFlowListItem(postDate, (shList.at(BuysOfOwned) / shares) * value)); shList[BuysOfOwned] = MyMoneyMoney(); } if (transactionType == eMyMoney::Split::InvestmentTransactionType::ReinvestDividend) { value = MyMoneyMoney(); foreach (const auto split, interestSplits) value += split.value(); value *= price; cfList[ReinvestIncome].append(CashFlowListItem(postDate, -value)); } } else if (transactionType == eMyMoney::Split::InvestmentTransactionType::RemoveShares && reportedDateRange) // removed shares give no value in return so no capital gain on them shList[Sells] += shares; else if (transactionType == eMyMoney::Split::InvestmentTransactionType::Dividend || transactionType == eMyMoney::Split::InvestmentTransactionType::Yield) cfList[CashIncome].append(CashFlowListItem(postDate, value)); } reportedDateRange = false; newEndingDate = newStartingDate; newStartingDate = newStartingDate.addYears(-1); report.setDateFilter(newStartingDate, newEndingDate); // search for matching buy transactions year earlier } while ( ( (report.investmentSum() == eMyMoney::Report::InvestmentSum::Owned && !shList[BuysOfOwned].isZero()) || (report.investmentSum() == eMyMoney::Report::InvestmentSum::Sold && !shList.at(Sells).isZero() && shList.at(Sells).abs() > shList.at(BuysOfSells).abs()) || (report.investmentSum() == eMyMoney::Report::InvestmentSum::OwnedAndSold && (!shList[BuysOfOwned].isZero() || (!shList.at(Sells).isZero() && shList.at(Sells).abs() > shList.at(BuysOfSells).abs()))) ) && account.openingDate() <= newEndingDate ); // we've got buy value and no sell value of long-term shares, so get them if (isSTLT && !shList[LongTermBuysOfSells].isZero()) { newStartingDate = startingDate; newEndingDate = endingDate.addDays(-settlementPeriod); report.setDateFilter(newStartingDate, newEndingDate); // search for matching buy transactions year earlier QList transactions = file->transactionList(report); shList[BuysOfOwned] = shList[LongTermBuysOfSells]; foreach (const auto transaction, transactions) { MyMoneySplit shareSplit = transaction.splitByAccount(account.id()); MyMoneySplit assetAccountSplit; QList feeSplits; QList interestSplits; MyMoneySecurity security; MyMoneySecurity currency; eMyMoney::Split::InvestmentTransactionType transactionType; KMyMoneyUtils::dissectTransaction(transaction, shareSplit, assetAccountSplit, feeSplits, interestSplits, security, currency, transactionType); QDate postDate = transaction.postDate(); MyMoneyMoney price; if (m_config.isConvertCurrency()) price = account.baseCurrencyPrice(postDate); //we only need base currency because the value is in deep currency else price = MyMoneyMoney::ONE; MyMoneyMoney value = assetAccountSplit.value() * price; MyMoneyMoney shares = shareSplit.shares(); if (transactionType == eMyMoney::Split::InvestmentTransactionType::SellShares) { if ((shList.at(LongTermSellsOfBuys) + shares).abs() >= shList.at(LongTermBuysOfSells)) { // add partially sold long-term shares cfList[LongTermSellsOfBuys].append(CashFlowListItem(postDate, (shList.at(LongTermSellsOfBuys).abs() - shList.at(LongTermBuysOfSells)) / shares * value)); shList[LongTermSellsOfBuys] = shList.at(LongTermBuysOfSells); break; } else { // add wholly sold long-term shares cfList[LongTermSellsOfBuys].append(CashFlowListItem(postDate, value)); shList[LongTermSellsOfBuys] += shares; } } else if (transactionType == eMyMoney::Split::InvestmentTransactionType::RemoveShares) { if ((shList.at(LongTermSellsOfBuys) + shares).abs() >= shList.at(LongTermBuysOfSells)) { shList[LongTermSellsOfBuys] = shList.at(LongTermBuysOfSells); break; } else shList[LongTermSellsOfBuys] += shares; } } } shList[BuysOfOwned] = stashedBuysOfOwned; report.setDateFilter(startingDate, endingDate); // reset data filter for next security return; } void QueryTable::constructPerformanceRow(const ReportAccount& account, TableRow& result, CashFlowList &all) const { MyMoneyReport report = m_config; QDate startingDate; QDate endingDate; report.validDateRange(startingDate, endingDate); startingDate = startingDate.addDays(-1); MyMoneyFile* file = MyMoneyFile::instance(); //get fraction depending on type of account int fraction = account.currency().smallestAccountFraction(); MyMoneyMoney price; if (m_config.isConvertCurrency()) price = account.deepCurrencyPrice(startingDate) * account.baseCurrencyPrice(startingDate); else price = account.deepCurrencyPrice(startingDate); MyMoneyMoney startingBal = file->balance(account.id(), startingDate) * price; //convert to lowest fraction startingBal = startingBal.convert(fraction); //calculate ending balance if (m_config.isConvertCurrency()) price = account.deepCurrencyPrice(endingDate) * account.baseCurrencyPrice(endingDate); else price = account.deepCurrencyPrice(endingDate); MyMoneyMoney endingBal = file->balance((account).id(), endingDate) * price; //convert to lowest fraction endingBal = endingBal.convert(fraction); QList cfList; QList shList; sumInvestmentValues(account, cfList, shList); MyMoneyMoney buysTotal; MyMoneyMoney sellsTotal; MyMoneyMoney cashIncomeTotal; MyMoneyMoney reinvestIncomeTotal; switch (m_config.investmentSum()) { case eMyMoney::Report::InvestmentSum::OwnedAndSold: buysTotal = cfList.at(BuysOfSells).total() + cfList.at(BuysOfOwned).total(); sellsTotal = cfList.at(Sells).total(); cashIncomeTotal = cfList.at(CashIncome).total(); reinvestIncomeTotal = cfList.at(ReinvestIncome).total(); startingBal = MyMoneyMoney(); if (buysTotal.isZero() && sellsTotal.isZero() && cashIncomeTotal.isZero() && reinvestIncomeTotal.isZero()) return; all.append(cfList.at(BuysOfSells)); all.append(cfList.at(BuysOfOwned)); all.append(cfList.at(Sells)); all.append(cfList.at(CashIncome)); result[ctSells] = sellsTotal.toString(); result[ctCashIncome] = cashIncomeTotal.toString(); result[ctReinvestIncome] = reinvestIncomeTotal.toString(); result[ctEndingBalance] = endingBal.toString(); break; case eMyMoney::Report::InvestmentSum::Owned: buysTotal = cfList.at(BuysOfOwned).total(); startingBal = MyMoneyMoney(); if (buysTotal.isZero() && endingBal.isZero()) return; all.append(cfList.at(BuysOfOwned)); all.append(CashFlowListItem(endingDate, endingBal)); result[ctReinvestIncome] = reinvestIncomeTotal.toString(); result[ctMarketValue] = endingBal.toString(); break; case eMyMoney::Report::InvestmentSum::Sold: buysTotal = cfList.at(BuysOfSells).total(); sellsTotal = cfList.at(Sells).total(); cashIncomeTotal = cfList.at(CashIncome).total(); startingBal = endingBal = MyMoneyMoney(); // check if there are any meaningfull values before adding them to results if (buysTotal.isZero() && sellsTotal.isZero() && cashIncomeTotal.isZero()) return; all.append(cfList.at(BuysOfSells)); all.append(cfList.at(Sells)); all.append(cfList.at(CashIncome)); result[ctSells] = sellsTotal.toString(); result[ctCashIncome] = cashIncomeTotal.toString(); break; case eMyMoney::Report::InvestmentSum::Period: default: buysTotal = cfList.at(Buys).total(); sellsTotal = cfList.at(Sells).total(); cashIncomeTotal = cfList.at(CashIncome).total(); reinvestIncomeTotal = cfList.at(ReinvestIncome).total(); if (buysTotal.isZero() && sellsTotal.isZero() && cashIncomeTotal.isZero() && reinvestIncomeTotal.isZero() && startingBal.isZero() && endingBal.isZero()) return; all.append(cfList.at(Buys)); all.append(cfList.at(Sells)); all.append(cfList.at(CashIncome)); all.append(CashFlowListItem(startingDate, -startingBal)); all.append(CashFlowListItem(endingDate, endingBal)); result[ctSells] = sellsTotal.toString(); result[ctCashIncome] = cashIncomeTotal.toString(); result[ctReinvestIncome] = reinvestIncomeTotal.toString(); result[ctStartingBalance] = startingBal.toString(); result[ctEndingBalance] = endingBal.toString(); break; } - MyMoneyMoney returnInvestment = helperROI(buysTotal - reinvestIncomeTotal, sellsTotal, startingBal, endingBal, cashIncomeTotal); - MyMoneyMoney annualReturn = helperIRR(all); - result[ctBuys] = buysTotal.toString(); - result[ctReturn] = annualReturn.toString(); - result[ctReturnInvestment] = returnInvestment.toString(); + result[ctReturn] = helperIRR(all); + result[ctReturnInvestment] = helperROI(buysTotal - reinvestIncomeTotal, sellsTotal, startingBal, endingBal, cashIncomeTotal); result[ctEquityType] = MyMoneySecurity::securityTypeToString(file->security(account.currencyId()).securityType()); } void QueryTable::constructCapitalGainRow(const ReportAccount& account, TableRow& result) const { MyMoneyFile* file = MyMoneyFile::instance(); QList cfList; QList shList; sumInvestmentValues(account, cfList, shList); MyMoneyMoney buysTotal = cfList.at(BuysOfSells).total(); MyMoneyMoney sellsTotal = cfList.at(Sells).total(); MyMoneyMoney longTermBuysOfSellsTotal = cfList.at(LongTermBuysOfSells).total(); MyMoneyMoney longTermSellsOfBuys = cfList.at(LongTermSellsOfBuys).total(); switch (m_config.investmentSum()) { case eMyMoney::Report::InvestmentSum::Owned: { if (shList.at(BuysOfOwned).isZero()) return; MyMoneyReport report = m_config; QDate startingDate; QDate endingDate; report.validDateRange(startingDate, endingDate); //get fraction depending on type of account int fraction = account.currency().smallestAccountFraction(); MyMoneyMoney price; //calculate ending balance if (m_config.isConvertCurrency()) price = account.deepCurrencyPrice(endingDate) * account.baseCurrencyPrice(endingDate); else price = account.deepCurrencyPrice(endingDate); MyMoneyMoney endingBal = shList.at(BuysOfOwned) * price; //convert to lowest fraction endingBal = endingBal.convert(fraction); buysTotal = cfList.at(BuysOfOwned).total() - cfList.at(ReinvestIncome).total(); int pricePrecision = file->security(account.currencyId()).pricePrecision(); result[ctBuys] = buysTotal.toString(); result[ctShares] = shList.at(BuysOfOwned).toString(); result[ctBuyPrice] = (buysTotal.abs() / shList.at(BuysOfOwned)).convertPrecision(pricePrecision).toString(); result[ctLastPrice] = price.toString(); result[ctMarketValue] = endingBal.toString(); result[ctCapitalGain] = (buysTotal + endingBal).toString(); result[ctPercentageGain] = ((buysTotal + endingBal)/buysTotal.abs()).toString(); break; } case eMyMoney::Report::InvestmentSum::Sold: default: buysTotal = cfList.at(BuysOfSells).total() - cfList.at(ReinvestIncome).total(); sellsTotal = cfList.at(Sells).total(); longTermBuysOfSellsTotal = cfList.at(LongTermBuysOfSells).total(); longTermSellsOfBuys = cfList.at(LongTermSellsOfBuys).total(); // check if there are any meaningfull values before adding them to results if (buysTotal.isZero() && sellsTotal.isZero() && longTermBuysOfSellsTotal.isZero() && longTermSellsOfBuys.isZero()) return; result[ctBuys] = buysTotal.toString(); result[ctSells] = sellsTotal.toString(); result[ctCapitalGain] = (buysTotal + sellsTotal).toString(); if (m_config.isShowingSTLTCapitalGains()) { result[ctBuysLT] = longTermBuysOfSellsTotal.toString(); result[ctSellsLT] = longTermSellsOfBuys.toString(); result[ctCapitalGainLT] = (longTermBuysOfSellsTotal + longTermSellsOfBuys).toString(); result[ctBuysST] = (buysTotal - longTermBuysOfSellsTotal).toString(); result[ctSellsST] = (sellsTotal - longTermSellsOfBuys).toString(); result[ctCapitalGainST] = ((buysTotal - longTermBuysOfSellsTotal) + (sellsTotal - longTermSellsOfBuys)).toString(); } break; } result[ctEquityType] = MyMoneySecurity::securityTypeToString(file->security(account.currencyId()).securityType()); } void QueryTable::constructAccountTable() { MyMoneyFile* file = MyMoneyFile::instance(); //make sure we have all subaccounts of investment accounts includeInvestmentSubAccounts(); QMap> currencyCashFlow; // for total calculation QList accounts; file->accountList(accounts); for (auto it_account = accounts.constBegin(); it_account != accounts.constEnd(); ++it_account) { // Note, "Investment" accounts are never included in account rows because // they don't contain anything by themselves. In reports, they are only // useful as a "topaccount" aggregator of stock accounts if ((*it_account).isAssetLiability() && m_config.includes((*it_account)) && (*it_account).accountType() != eMyMoney::Account::Type::Investment) { // don't add the account if it is closed. In fact, the business logic // should prevent that an account can be closed with a balance not equal // to zero, but we never know. MyMoneyMoney shares = file->balance((*it_account).id(), m_config.toDate()); if (shares.isZero() && (*it_account).isClosed()) continue; ReportAccount account(*it_account); TableRow qaccountrow; CashFlowList accountCashflow; // for total calculation switch(m_config.queryColumns()) { case eMyMoney::Report::QueryColumn::Performance: { constructPerformanceRow(account, qaccountrow, accountCashflow); if (!qaccountrow.isEmpty()) { // assuming that that report is grouped by topaccount qaccountrow[ctTopAccount] = account.topParentName(); if (!m_containsNonBaseCurrency && account.currency().id() != file->baseCurrency().id()) m_containsNonBaseCurrency = true; if (m_config.isConvertCurrency()) qaccountrow[ctCurrency] = file->baseCurrency().id(); else qaccountrow[ctCurrency] = account.currency().id(); if (!currencyCashFlow.value(qaccountrow.value(ctCurrency)).contains(qaccountrow.value(ctTopAccount))) currencyCashFlow[qaccountrow.value(ctCurrency)].insert(qaccountrow.value(ctTopAccount), accountCashflow); // create cashflow for unknown account... else currencyCashFlow[qaccountrow.value(ctCurrency)][qaccountrow.value(ctTopAccount)] += accountCashflow; // ...or add cashflow for known account } break; } case eMyMoney::Report::QueryColumn::CapitalGain: constructCapitalGainRow(account, qaccountrow); break; default: { //get fraction for account int fraction = account.currency().smallestAccountFraction() != -1 ? account.currency().smallestAccountFraction() : file->baseCurrency().smallestAccountFraction(); MyMoneyMoney netprice = account.deepCurrencyPrice(m_config.toDate()); if (m_config.isConvertCurrency() && account.isForeignCurrency()) netprice *= account.baseCurrencyPrice(m_config.toDate()); // display currency is base currency, so set the price netprice = netprice.reduce(); shares = shares.reduce(); int pricePrecision = file->security(account.currencyId()).pricePrecision(); qaccountrow[ctPrice] = netprice.convertPrecision(pricePrecision).toString(); qaccountrow[ctValue] = (netprice * shares).convert(fraction).toString(); qaccountrow[ctShares] = shares.toString(); QString iid = account.institutionId(); // If an account does not have an institution, get it from the top-parent. if (iid.isEmpty() && !account.isTopLevel()) iid = account.topParent().institutionId(); if (iid.isEmpty()) qaccountrow[ctInstitution] = i18nc("No institution", "None"); else qaccountrow[ctInstitution] = file->institution(iid).name(); qaccountrow[ctType] = MyMoneyAccount::accountTypeToString(account.accountType()); } } if (qaccountrow.isEmpty()) // don't add the account if there are no calculated values continue; qaccountrow[ctRank] = QLatin1Char('1'); qaccountrow[ctAccount] = account.name(); qaccountrow[ctAccountID] = account.id(); qaccountrow[ctTopAccount] = account.topParentName(); if (!m_containsNonBaseCurrency && account.currency().id() != file->baseCurrency().id()) m_containsNonBaseCurrency = true; if (m_config.isConvertCurrency()) qaccountrow[ctCurrency] = file->baseCurrency().id(); else qaccountrow[ctCurrency] = account.currency().id(); m_rows.append(qaccountrow); } } if (m_config.queryColumns() == eMyMoney::Report::QueryColumn::Performance && m_config.isShowingColumnTotals()) { TableRow qtotalsrow; qtotalsrow[ctRank] = QLatin1Char('4'); // add identification of row as total QMap currencyGrandCashFlow; QMap>::iterator currencyAccGrp = currencyCashFlow.begin(); while (currencyAccGrp != currencyCashFlow.end()) { // convert map of top accounts with cashflows to TableRow for (QMap::iterator topAccount = (*currencyAccGrp).begin(); topAccount != (*currencyAccGrp).end(); ++topAccount) { qtotalsrow[ctTopAccount] = topAccount.key(); - qtotalsrow[ctReturn] = helperIRR(topAccount.value()).toString(); + qtotalsrow[ctReturn] = helperIRR(topAccount.value()); qtotalsrow[ctCurrency] = currencyAccGrp.key(); currencyGrandCashFlow[currencyAccGrp.key()] += topAccount.value(); // cumulative sum of cashflows of each topaccount m_rows.append(qtotalsrow); // rows aren't sorted yet, so no problem with adding them randomly at the end } ++currencyAccGrp; } QMap::iterator currencyGrp = currencyGrandCashFlow.begin(); qtotalsrow[ctTopAccount].clear(); // empty topaccount because it's grand cashflow while (currencyGrp != currencyGrandCashFlow.end()) { - qtotalsrow[ctReturn] = helperIRR(currencyGrp.value()).toString(); + qtotalsrow[ctReturn] = helperIRR(currencyGrp.value()); qtotalsrow[ctCurrency] = currencyGrp.key(); m_rows.append(qtotalsrow); ++currencyGrp; } } } void QueryTable::constructSplitsTable() { MyMoneyFile* file = MyMoneyFile::instance(); //make sure we have all subaccounts of investment accounts includeInvestmentSubAccounts(); MyMoneyReport report(m_config); report.setReportAllSplits(false); report.setConsiderCategory(true); // support for opening and closing balances QMap accts; //get all transactions for this report QList transactions = file->transactionList(report); for (QList::const_iterator it_transaction = transactions.constBegin(); it_transaction != transactions.constEnd(); ++it_transaction) { TableRow qA, qS; QDate pd; qA[ctID] = qS[ctID] = (* it_transaction).id(); qA[ctEntryDate] = qS[ctEntryDate] = (* it_transaction).entryDate().toString(Qt::ISODate); qA[ctPostDate] = qS[ctPostDate] = (* it_transaction).postDate().toString(Qt::ISODate); qA[ctCommodity] = qS[ctCommodity] = (* it_transaction).commodity(); pd = (* it_transaction).postDate(); qA[ctMonth] = qS[ctMonth] = i18n("Month of %1", QDate(pd.year(), pd.month(), 1).toString(Qt::ISODate)); qA[ctWeek] = qS[ctWeek] = i18n("Week of %1", pd.addDays(1 - pd.dayOfWeek()).toString(Qt::ISODate)); if (!m_containsNonBaseCurrency && (*it_transaction).commodity() != file->baseCurrency().id()) m_containsNonBaseCurrency = true; if (report.isConvertCurrency()) qA[ctCurrency] = qS[ctCurrency] = file->baseCurrency().id(); else qA[ctCurrency] = qS[ctCurrency] = (*it_transaction).commodity(); // to handle splits, we decide on which account to base the split // (a reference point or point of view so to speak). here we take the // first account that is a stock account or loan account (or the first account // that is not an income or expense account if there is no stock or loan account) // to be the account (qA) that will have the sub-item "split" entries. we add // one transaction entry (qS) for each subsequent entry in the split. const QList& splits = (*it_transaction).splits(); QList::const_iterator myBegin, it_split; //S_end = splits.end(); for (it_split = splits.constBegin(), myBegin = splits.constEnd(); it_split != splits.constEnd(); ++it_split) { ReportAccount splitAcc((* it_split).accountId()); // always put split with a "stock" account if it exists if (splitAcc.isInvest()) break; // prefer to put splits with a "loan" account if it exists if (splitAcc.isLoan()) myBegin = it_split; if ((myBegin == splits.end()) && ! splitAcc.isIncomeExpense()) { myBegin = it_split; } } // select our "reference" split if (it_split == splits.end()) { it_split = myBegin; } else { myBegin = it_split; } // if the split is still unknown, use the first one. I have seen this // happen with a transaction that has only a single split referencing an income or expense // account and has an amount and value of 0. Such a transaction will fall through // the above logic and leave 'it_split' pointing to splits.end() which causes the remainder // of this to end in an infinite loop. if (it_split == splits.end()) { it_split = splits.begin(); } // for "loan" reports, the loan transaction gets special treatment. // the splits of a loan transaction are placed on one line in the // reference (loan) account (qA). however, we process the matching // split entries (qS) normally. bool loan_special_case = false; if (m_config.queryColumns() & eMyMoney::Report::QueryColumn::Loan) { ReportAccount splitAcc((*it_split).accountId()); loan_special_case = splitAcc.isLoan(); } // There is a slight chance that at this point myBegin is still pointing to splits.end() if the // transaction only has income and expense splits (which should not happen). In that case, point // it to the first split if (myBegin == splits.end()) { myBegin = splits.begin(); } //the account of the beginning splits ReportAccount myBeginAcc((*myBegin).accountId()); bool include_me = true; QString a_fullname; QString a_memo; int pass = 1; do { MyMoneyMoney xr; ReportAccount splitAcc((* it_split).accountId()); //get fraction for account int fraction = splitAcc.currency().smallestAccountFraction(); //use base currency fraction if not initialized if (fraction == -1) fraction = file->baseCurrency().smallestAccountFraction(); QString institution = splitAcc.institutionId(); QString payee = (*it_split).payeeId(); const QList tagIdList = (*it_split).tagIdList(); if (m_config.isConvertCurrency()) { xr = (splitAcc.deepCurrencyPrice((*it_transaction).postDate()) * splitAcc.baseCurrencyPrice((*it_transaction).postDate())).reduce(); } else { xr = splitAcc.deepCurrencyPrice((*it_transaction).postDate()).reduce(); } // reverse the sign of incomes and expenses to keep consistency in the way it is displayed in other reports if (splitAcc.isIncomeExpense()) { xr = -xr; } if (splitAcc.isInvest()) { // use the institution of the parent for stock accounts institution = splitAcc.parent().institutionId(); MyMoneyMoney shares = (*it_split).shares(); int pricePrecision = file->security(splitAcc.currencyId()).pricePrecision(); qA[ctAction] = (*it_split).action(); qA[ctShares] = shares.isZero() ? QString() : (*it_split).shares().toString(); qA[ctPrice] = shares.isZero() ? QString() : xr.convertPrecision(pricePrecision).toString(); if (((*it_split).action() == MyMoneySplit::actionName(eMyMoney::Split::Action::BuyShares)) && (*it_split).shares().isNegative()) qA[ctAction] = "Sell"; qA[ctInvestAccount] = splitAcc.parent().name(); } include_me = m_config.includes(splitAcc); a_fullname = splitAcc.fullName(); a_memo = (*it_split).memo(); int pricePrecision = file->security(splitAcc.currencyId()).pricePrecision(); qA[ctPrice] = xr.convertPrecision(pricePrecision).toString(); qA[ctAccount] = splitAcc.name(); qA[ctAccountID] = splitAcc.id(); qA[ctTopAccount] = splitAcc.topParentName(); qA[ctInstitution] = institution.isEmpty() ? i18n("No Institution") : file->institution(institution).name(); //FIXME-ALEX Is this useless? Isn't constructSplitsTable called only for cashflow type report? QString delimiter; foreach(const auto tagId, tagIdList) { qA[ctTag] += delimiter + file->tag(tagId).name().simplified(); delimiter = QLatin1Char(','); } qA[ctPayee] = payee.isEmpty() ? i18n("[Empty Payee]") : file->payee(payee).name().simplified(); qA[ctReconcileDate] = (*it_split).reconcileDate().toString(Qt::ISODate); qA[ctReconcileFlag] = KMyMoneyUtils::reconcileStateToString((*it_split).reconcileFlag(), true); qA[ctNumber] = (*it_split).number(); qA[ctMemo] = a_memo; qS[ctReconcileDate] = qA[ctReconcileDate]; qS[ctReconcileFlag] = qA[ctReconcileFlag]; qS[ctNumber] = qA[ctNumber]; qS[ctTopCategory] = splitAcc.topParentName(); // only include the configured accounts if (include_me) { // add the "summarized" split transaction // this is the sub-total of the split detail // convert to lowest fraction qA[ctValue] = ((*it_split).shares() * xr).convert(fraction).toString(); qA[ctRank] = QLatin1Char('1'); //fill in account information if (! splitAcc.isIncomeExpense() && it_split != myBegin) { qA[ctAccount] = ((*it_split).shares().isNegative()) ? i18n("Transfer to %1", myBeginAcc.fullName()) : i18n("Transfer from %1", myBeginAcc.fullName()); } else if (it_split == myBegin) { //handle the main split if ((splits.count() > 2)) { //if it is the main split and has multiple splits, note that qA[ctAccount] = i18n("[Split Transaction]"); } else { //fill the account name of the second split QList::const_iterator tempSplit = splits.constBegin(); //there are supposed to be only 2 splits if we ever get here if (tempSplit == myBegin && splits.count() > 1) ++tempSplit; //show the name of the category, or "transfer to/from" if it as an account ReportAccount tempSplitAcc((*tempSplit).accountId()); if (! tempSplitAcc.isIncomeExpense()) { qA[ctAccount] = ((*it_split).shares().isNegative()) ? i18n("Transfer to %1", tempSplitAcc.fullName()) : i18n("Transfer from %1", tempSplitAcc.fullName()); } else { qA[ctAccount] = tempSplitAcc.fullName(); } } } else { //in any other case, fill in the account name of the main split qA[ctAccount] = myBeginAcc.fullName(); } //category data is always the one of the split qA [ctCategory] = splitAcc.fullName(); qA [ctTopCategory] = splitAcc.topParentName(); qA [ctCategoryType] = MyMoneyAccount::accountTypeToString(splitAcc.accountGroup()); m_rows += qA; // track accts that will need opening and closing balances accts.insert(splitAcc.id(), splitAcc); } ++it_split; // look for wrap-around if (it_split == splits.end()) it_split = splits.begin(); //check if there have been more passes than there are splits //this is to prevent infinite loops in cases of data inconsistency -- asoliverez ++pass; if (pass > splits.count()) break; } while (it_split != myBegin); if (loan_special_case) { m_rows += qA; } } // now run through our accts list and add opening and closing balances switch (m_config.rowType()) { case eMyMoney::Report::RowType::Account: case eMyMoney::Report::RowType::TopAccount: break; // case eMyMoney::Report::RowType::Category: // case MyMoneyReport::eTopCategory: // case MyMoneyReport::ePayee: // case MyMoneyReport::eMonth: // case MyMoneyReport::eWeek: default: return; } QDate startDate, endDate; report.validDateRange(startDate, endDate); QString strStartDate = startDate.toString(Qt::ISODate); QString strEndDate = endDate.toString(Qt::ISODate); startDate = startDate.addDays(-1); for (auto it_account = accts.constBegin(); it_account != accts.constEnd(); ++it_account) { TableRow qA; ReportAccount account((* it_account)); //get fraction for account int fraction = account.currency().smallestAccountFraction(); //use base currency fraction if not initialized if (fraction == -1) fraction = file->baseCurrency().smallestAccountFraction(); QString institution = account.institutionId(); // use the institution of the parent for stock accounts if (account.isInvest()) institution = account.parent().institutionId(); MyMoneyMoney startBalance, endBalance, startPrice, endPrice; MyMoneyMoney startShares, endShares; //get price and convert currency if necessary if (m_config.isConvertCurrency()) { startPrice = (account.deepCurrencyPrice(startDate) * account.baseCurrencyPrice(startDate)).reduce(); endPrice = (account.deepCurrencyPrice(endDate) * account.baseCurrencyPrice(endDate)).reduce(); } else { startPrice = account.deepCurrencyPrice(startDate).reduce(); endPrice = account.deepCurrencyPrice(endDate).reduce(); } startShares = file->balance(account.id(), startDate); endShares = file->balance(account.id(), endDate); //get starting and ending balances startBalance = startShares * startPrice; endBalance = endShares * endPrice; //starting balance // don't show currency if we're converting or if it's not foreign if (!m_containsNonBaseCurrency && account.currency().id() != file->baseCurrency().id()) m_containsNonBaseCurrency = true; if (m_config.isConvertCurrency()) qA[ctCurrency] = file->baseCurrency().id(); else qA[ctCurrency] = account.currency().id(); qA[ctAccountID] = account.id(); qA[ctAccount] = account.name(); qA[ctTopAccount] = account.topParentName(); qA[ctInstitution] = institution.isEmpty() ? i18n("No Institution") : file->institution(institution).name(); qA[ctRank] = QLatin1Char('0'); int pricePrecision = file->security(account.currencyId()).pricePrecision(); qA[ctPrice] = startPrice.convertPrecision(pricePrecision).toString(); if (account.isInvest()) { qA[ctShares] = startShares.toString(); } qA[ctPostDate] = strStartDate; qA[ctBalance] = startBalance.convert(fraction).toString(); qA[ctValue].clear(); qA[ctID] = QLatin1Char('A'); m_rows += qA; qA[ctRank] = QLatin1Char('3'); //ending balance qA[ctPrice] = endPrice.convertPrecision(pricePrecision).toString(); if (account.isInvest()) { qA[ctShares] = endShares.toString(); } qA[ctPostDate] = strEndDate; qA[ctBalance] = endBalance.toString(); qA[ctID] = QLatin1Char('Z'); m_rows += qA; } } } diff --git a/kmymoney/plugins/views/reports/core/querytable.h b/kmymoney/plugins/views/reports/core/querytable.h index b5420bc30..a34366863 100644 --- a/kmymoney/plugins/views/reports/core/querytable.h +++ b/kmymoney/plugins/views/reports/core/querytable.h @@ -1,171 +1,82 @@ /* * Copyright 2005 Ace Jones * Copyright 2017-2018 Łukasz Wojniłowicz * * 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. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -/*************************************************************************** - * * - * 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 QUERYTABLE_H #define QUERYTABLE_H // ---------------------------------------------------------------------------- // QT Includes #include #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes #include "listtable.h" #include "mymoneymoney.h" class MyMoneyReport; +class CashFlowList; namespace reports { class ReportAccount; -class CashFlowList; /** * Calculates a query of information about the transaction database. * * 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 QueryTable : public ListTable { public: explicit QueryTable(const MyMoneyReport&); void init(); protected: void constructAccountTable(); void constructTotalRows(); void constructTransactionTable(); void sumInvestmentValues(const ReportAccount &account, QList &cfList, QList &shList) const; void constructPerformanceRow(const ReportAccount& account, TableRow& result, CashFlowList &all) const; void constructCapitalGainRow(const ReportAccount& account, TableRow& result) const; - MyMoneyMoney helperROI(const MyMoneyMoney& buys, const MyMoneyMoney& sells, const MyMoneyMoney& startingBal, const MyMoneyMoney& endingBal, const MyMoneyMoney& cashIncome) const; - MyMoneyMoney helperIRR(const CashFlowList& all) const; + QString helperROI(const MyMoneyMoney& buys, const MyMoneyMoney& sells, const MyMoneyMoney& startingBal, const MyMoneyMoney& endingBal, const MyMoneyMoney& cashIncome) const; + QString helperIRR(const CashFlowList& all) const; void constructSplitsTable(); bool linkEntries() const final override { return true; } private: enum InvestmentValue {Buys = 0, Sells, BuysOfSells, SellsOfBuys, LongTermBuysOfSells, LongTermSellsOfBuys, BuysOfOwned, ReinvestIncome, CashIncome, End}; }; -// -// Cash Flow analysis tools for investment reports -// - -class CashFlowListItem -{ -public: - CashFlowListItem() {} - CashFlowListItem(const QDate& _date, const MyMoneyMoney& _value): m_date(_date), m_value(_value) {} - bool operator<(const CashFlowListItem& _second) const { - return m_date < _second.m_date; - } - bool operator<=(const CashFlowListItem& _second) const { - return m_date <= _second.m_date; - } - bool operator>(const CashFlowListItem& _second) const { - return m_date > _second.m_date; - } - const QDate& date() const { - return m_date; - } - const MyMoneyMoney& value() const { - return m_value; - } - MyMoneyMoney NPV(double _rate) const; - - static void setToday(const QDate& _today) { - m_sToday = _today; - } - const QDate& today() const { - return m_sToday; - } - -private: - QDate m_date; - MyMoneyMoney m_value; - - static QDate m_sToday; -}; - -class CashFlowList: public QList -{ -public: - CashFlowList() {} - MyMoneyMoney NPV(double rate) const; - double IRR() const; - MyMoneyMoney total() const; - void dumpDebug() const; - - /** - * Function: XIRR - * - * Compute the internal rate of return for a non-periodic series of cash flows. - * - * XIRR ( Values; Dates; [ Guess = 0.1 ] ) - **/ - double calculateXIRR() const; - -protected: - CashFlowListItem mostRecent() const; - -private: - /** - * helper: xirrResult - * - * args[0] = values - * args[1] = dates - **/ - double xirrResult(double& rate) const; - - /** - * - * helper: xirrResultDerive - * - * args[0] = values - * args[1] = dates - **/ - double xirrResultDerive(double& rate) const; -}; - } #endif // QUERYREPORT_H diff --git a/kmymoney/plugins/views/reports/core/tests/querytable-test.cpp b/kmymoney/plugins/views/reports/core/tests/querytable-test.cpp index 7b102647f..8db4deff3 100644 --- a/kmymoney/plugins/views/reports/core/tests/querytable-test.cpp +++ b/kmymoney/plugins/views/reports/core/tests/querytable-test.cpp @@ -1,936 +1,977 @@ /* * Copyright 2005-2018 Thomas Baumgart * Copyright 2005-2006 Ace Jones * * 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. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "querytable-test.h" #include #include #include #include "tests/testutilities.h" +#include "cashflowlist.h" #include "querytable.h" #include "mymoneyinstitution.h" #include "mymoneyaccount.h" #include "mymoneysecurity.h" #include "mymoneyprice.h" #include "mymoneystoragedump.h" #include "mymoneyreport.h" #include "mymoneysplit.h" #include "mymoneypayee.h" #include "mymoneystatement.h" #include "mymoneyexception.h" #include "kmymoneysettings.h" using namespace reports; using namespace test; QTEST_GUILESS_MAIN(QueryTableTest) void writeTabletoHTML(const QueryTable& table, const QString& _filename = QString()) { static unsigned filenumber = 1; QString filename = _filename; if (filename.isEmpty()) { filename = QString::fromLatin1("report-%1.html").arg(filenumber, 2, 10,QLatin1Char('0')); ++filenumber; } QFile g(filename); g.open(QIODevice::WriteOnly); QTextStream(&g) << table.renderHTML(); g.close(); } void writeTabletoCSV(const QueryTable& table, const QString& _filename = QString()) { static unsigned filenumber = 1; QString filename = _filename; if (filename.isEmpty()) { filename = QString::fromLatin1("report-%1.csv").arg(filenumber, 2, 10,QLatin1Char('0')); ++filenumber; } QFile g(filename); g.open(QIODevice::WriteOnly); QTextStream(&g) << table.renderCSV(); g.close(); } void QueryTableTest::setup() { } void QueryTableTest::init() { storage = new MyMoneyStorageMgr; file = MyMoneyFile::instance(); file->attachStorage(storage); MyMoneyFileTransaction ft; file->addCurrency(MyMoneySecurity("CAD", "Canadian Dollar", "C$")); file->addCurrency(MyMoneySecurity("USD", "US Dollar", "$")); file->addCurrency(MyMoneySecurity("JPY", "Japanese Yen", QChar(0x00A5), 1)); file->addCurrency(MyMoneySecurity("GBP", "British Pound", "#")); file->setBaseCurrency(file->currency("USD")); MyMoneyPayee payeeTest; payeeTest.setName("Test Payee"); file->addPayee(payeeTest); MyMoneyPayee payeeTest2; payeeTest2.setName("Thomas Baumgart"); file->addPayee(payeeTest2); acAsset = (MyMoneyFile::instance()->asset().id()); acLiability = (MyMoneyFile::instance()->liability().id()); acExpense = (MyMoneyFile::instance()->expense().id()); acIncome = (MyMoneyFile::instance()->income().id()); acChecking = makeAccount(QString("Checking Account"), eMyMoney::Account::Type::Checkings, moCheckingOpen, QDate(2004, 5, 15), acAsset); acCredit = makeAccount(QString("Credit Card"), eMyMoney::Account::Type::CreditCard, moCreditOpen, QDate(2004, 7, 15), acLiability); acSolo = makeAccount(QString("Solo"), eMyMoney::Account::Type::Expense, MyMoneyMoney(), QDate(2004, 1, 11), acExpense); acParent = makeAccount(QString("Parent"), eMyMoney::Account::Type::Expense, MyMoneyMoney(), QDate(2004, 1, 11), acExpense); acChild = makeAccount(QString("Child"), eMyMoney::Account::Type::Expense, MyMoneyMoney(), QDate(2004, 2, 11), acParent); acForeign = makeAccount(QString("Foreign"), eMyMoney::Account::Type::Expense, MyMoneyMoney(), QDate(2004, 1, 11), acExpense); acTax = makeAccount(QString("Tax"), eMyMoney::Account::Type::Expense, MyMoneyMoney(), QDate(2005, 1, 11), acExpense, "", true); MyMoneyInstitution i("Bank of the World", "", "", "", "", "", ""); file->addInstitution(i); inBank = i.id(); ft.commit(); } void QueryTableTest::cleanup() { file->detachStorage(storage); delete storage; } void QueryTableTest::testQueryBasics() { try { TransactionHelper t1q1(QDate(2004, 1, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moSolo, acChecking, acSolo); TransactionHelper t2q1(QDate(2004, 2, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent1, acCredit, acParent); TransactionHelper t3q1(QDate(2004, 3, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent2, acCredit, acParent); TransactionHelper t4y1(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moChild, acCredit, acChild); TransactionHelper t1q2(QDate(2004, 4, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moSolo, acChecking, acSolo); TransactionHelper t2q2(QDate(2004, 5, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent1, acCredit, acParent); TransactionHelper t3q2(QDate(2004, 6, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent2, acCredit, acParent); TransactionHelper t4q2(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moChild, acCredit, acChild); TransactionHelper t1y2(QDate(2005, 1, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moSolo, acChecking, acSolo); TransactionHelper t2y2(QDate(2005, 5, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent1, acCredit, acParent); TransactionHelper t3y2(QDate(2005, 9, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent2, acCredit, acParent); TransactionHelper t4y2(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moChild, acCredit, acChild); unsigned cols; MyMoneyReport filter; filter.setRowType(eMyMoney::Report::RowType::Category); cols = eMyMoney::Report::QueryColumn::Number | eMyMoney::Report::QueryColumn::Payee | eMyMoney::Report::QueryColumn::Account; filter.setQueryColumns(static_cast(cols)); // filter.setName("Transactions by Category"); XMLandback(filter); QueryTable qtbl_1(filter); writeTabletoHTML(qtbl_1, "Transactions by Category.html"); QList rows = qtbl_1.rows(); QVERIFY(rows.count() == 19); QVERIFY(rows[0][ListTable::ctCategoryType] == "Expense"); QVERIFY(rows[0][ListTable::ctCategory] == "Parent"); QVERIFY(rows[0][ListTable::ctPostDate] == "2004-02-01"); QVERIFY(rows[14][ListTable::ctCategoryType] == "Expense"); QVERIFY(rows[14][ListTable::ctCategory] == "Solo"); QVERIFY(rows[14][ListTable::ctPostDate] == "2005-01-01"); QVERIFY(MyMoneyMoney(rows[6][ListTable::ctValue]) == -(moParent1 + moParent2) * 3); QVERIFY(MyMoneyMoney(rows[10][ListTable::ctValue]) == -(moChild) * 3); QVERIFY(MyMoneyMoney(rows[16][ListTable::ctValue]) == -(moSolo) * 3); QVERIFY(MyMoneyMoney(rows[17][ListTable::ctValue]) == -(moParent1 + moParent2 + moSolo + moChild) * 3); QVERIFY(MyMoneyMoney(rows[18][ListTable::ctValue]) == -(moParent1 + moParent2 + moSolo + moChild) * 3 + moCheckingOpen + moCreditOpen); filter.setRowType(eMyMoney::Report::RowType::TopCategory); cols = eMyMoney::Report::QueryColumn::Number | eMyMoney::Report::QueryColumn::Payee | eMyMoney::Report::QueryColumn::Account; filter.setQueryColumns(static_cast(cols)); // filter.setName("Transactions by Top Category"); XMLandback(filter); QueryTable qtbl_2(filter); writeTabletoHTML(qtbl_2, "Transactions by Top Category.html"); rows = qtbl_2.rows(); QVERIFY(rows.count() == 16); QVERIFY(rows[0][ListTable::ctCategoryType] == "Expense"); QVERIFY(rows[0][ListTable::ctTopCategory] == "Parent"); QVERIFY(rows[0][ListTable::ctPostDate] == "2004-02-01"); QVERIFY(rows[8][ListTable::ctCategoryType] == "Expense"); QVERIFY(rows[8][ListTable::ctTopCategory] == "Parent"); QVERIFY(rows[8][ListTable::ctPostDate] == "2005-09-01"); QVERIFY(rows[12][ListTable::ctCategoryType] == "Expense"); QVERIFY(rows[12][ListTable::ctTopCategory] == "Solo"); QVERIFY(rows[12][ListTable::ctPostDate] == "2005-01-01"); QVERIFY(MyMoneyMoney(rows[9][ListTable::ctValue]) == -(moParent1 + moParent2 + moChild) * 3); QVERIFY(MyMoneyMoney(rows[13][ListTable::ctValue]) == -(moSolo) * 3); QVERIFY(MyMoneyMoney(rows[14][ListTable::ctValue]) == -(moParent1 + moParent2 + moSolo + moChild) * 3); QVERIFY(MyMoneyMoney(rows[15][ListTable::ctValue]) == -(moParent1 + moParent2 + moSolo + moChild) * 3 + moCheckingOpen + moCreditOpen); filter.setRowType(eMyMoney::Report::RowType::Account); filter.setName("Transactions by Account"); cols = eMyMoney::Report::QueryColumn::Number | eMyMoney::Report::QueryColumn::Payee | eMyMoney::Report::QueryColumn::Category; filter.setQueryColumns(static_cast(cols)); // XMLandback(filter); QueryTable qtbl_3(filter); writeTabletoHTML(qtbl_3, "Transactions by Account.html"); rows = qtbl_3.rows(); #if 1 QVERIFY(rows.count() == 19); QVERIFY(rows[1][ListTable::ctAccount] == "Checking Account"); QVERIFY(rows[1][ListTable::ctCategory] == "Solo"); QVERIFY(rows[1][ListTable::ctPostDate] == "2004-01-01"); QVERIFY(rows[15][ListTable::ctAccount] == "Credit Card"); QVERIFY(rows[15][ListTable::ctCategory] == "Parent"); QVERIFY(rows[15][ListTable::ctPostDate] == "2005-09-01"); #else QVERIFY(rows.count() == 12); QVERIFY(rows[0][ListTable::ctAccount] == "Checking Account"); QVERIFY(rows[0][ListTable::ctCategory] == "Solo"); QVERIFY(rows[0][ListTable::ctPostDate] == "2004-01-01"); QVERIFY(rows[11][ListTable::ctAccount] == "Credit Card"); QVERIFY(rows[11][ListTable::ctCategory] == "Parent"); QVERIFY(rows[11][ListTable::ctPostDate] == "2005-09-01"); #endif QVERIFY(MyMoneyMoney(rows[5][ListTable::ctValue]) == -(moSolo) * 3 + moCheckingOpen); QVERIFY(MyMoneyMoney(rows[17][ListTable::ctValue]) == -(moParent1 + moParent2 + moChild) * 3 + moCreditOpen); QVERIFY(MyMoneyMoney(rows[18][ListTable::ctValue]) == -(moParent1 + moParent2 + moSolo + moChild) * 3 + moCheckingOpen + moCreditOpen); filter.setRowType(eMyMoney::Report::RowType::Payee); filter.setName("Transactions by Payee"); cols = eMyMoney::Report::QueryColumn::Number | eMyMoney::Report::QueryColumn::Memo | eMyMoney::Report::QueryColumn::Category; filter.setQueryColumns(static_cast(cols)); // XMLandback(filter); QueryTable qtbl_4(filter); writeTabletoHTML(qtbl_4, "Transactions by Payee.html"); rows = qtbl_4.rows(); QVERIFY(rows.count() == 14); QVERIFY(rows[0][ListTable::ctPayee] == "Test Payee"); QVERIFY(rows[0][ListTable::ctCategory] == "Solo"); QVERIFY(rows[0][ListTable::ctPostDate] == "2004-01-01"); QVERIFY(rows[7][ListTable::ctPayee] == "Test Payee"); QVERIFY(rows[7][ListTable::ctCategory] == "Parent: Child"); QVERIFY(rows[7][ListTable::ctPostDate] == "2004-11-07"); QVERIFY(rows[11][ListTable::ctPayee] == "Test Payee"); QVERIFY(rows[11][ListTable::ctCategory] == "Parent"); QVERIFY(rows[11][ListTable::ctPostDate] == "2005-09-01"); QVERIFY(MyMoneyMoney(rows[12][ListTable::ctValue]) == -(moParent1 + moParent2 + moSolo + moChild) * 3); QVERIFY(MyMoneyMoney(rows[13][ListTable::ctValue]) == -(moParent1 + moParent2 + moSolo + moChild) * 3 + moCheckingOpen + moCreditOpen); filter.setRowType(eMyMoney::Report::RowType::Month); filter.setName("Transactions by Month"); cols = eMyMoney::Report::QueryColumn::Number | eMyMoney::Report::QueryColumn::Payee | eMyMoney::Report::QueryColumn::Category; filter.setQueryColumns(static_cast(cols)); // XMLandback(filter); QueryTable qtbl_5(filter); writeTabletoHTML(qtbl_5, "Transactions by Month.html"); rows = qtbl_5.rows(); QVERIFY(rows.count() == 23); QVERIFY(rows[0][ListTable::ctPayee] == "Test Payee"); QVERIFY(rows[0][ListTable::ctCategory] == "Solo"); QVERIFY(rows[0][ListTable::ctPostDate] == "2004-01-01"); QVERIFY(rows[12][ListTable::ctPayee] == "Test Payee"); QVERIFY(rows[12][ListTable::ctCategory] == "Parent: Child"); QVERIFY(rows[12][ListTable::ctPostDate] == "2004-11-07"); QVERIFY(rows[20][ListTable::ctPayee] == "Test Payee"); QVERIFY(rows[20][ListTable::ctCategory] == "Parent"); QVERIFY(rows[20][ListTable::ctPostDate] == "2005-09-01"); QVERIFY(MyMoneyMoney(rows[1][ListTable::ctValue]) == -moSolo); QVERIFY(MyMoneyMoney(rows[15][ListTable::ctValue]) == -(moChild) * 3); QVERIFY(MyMoneyMoney(rows[9][ListTable::ctValue]) == -moParent1 + moCheckingOpen); QVERIFY(MyMoneyMoney(rows[22][ListTable::ctValue]) == -(moParent1 + moParent2 + moSolo + moChild) * 3 + moCheckingOpen + moCreditOpen); filter.setRowType(eMyMoney::Report::RowType::Week); filter.setName("Transactions by Week"); cols = eMyMoney::Report::QueryColumn::Number | eMyMoney::Report::QueryColumn::Payee | eMyMoney::Report::QueryColumn::Category; filter.setQueryColumns(static_cast(cols)); // XMLandback(filter); QueryTable qtbl_6(filter); writeTabletoHTML(qtbl_6, "Transactions by Week.html"); rows = qtbl_6.rows(); QVERIFY(rows.count() == 23); QVERIFY(rows[0][ListTable::ctPayee] == "Test Payee"); QVERIFY(rows[0][ListTable::ctCategory] == "Solo"); QVERIFY(rows[0][ListTable::ctPostDate] == "2004-01-01"); QVERIFY(rows[20][ListTable::ctPayee] == "Test Payee"); QVERIFY(rows[20][ListTable::ctCategory] == "Parent"); QVERIFY(rows[20][ListTable::ctPostDate] == "2005-09-01"); QVERIFY(MyMoneyMoney(rows[1][ListTable::ctValue]) == -moSolo); QVERIFY(MyMoneyMoney(rows[15][ListTable::ctValue]) == -(moChild) * 3); QVERIFY(MyMoneyMoney(rows[21][ListTable::ctValue]) == -moParent2); QVERIFY(MyMoneyMoney(rows[22][ListTable::ctValue]) == -(moParent1 + moParent2 + moSolo + moChild) * 3 + moCheckingOpen + moCreditOpen); } catch (const MyMoneyException &e) { QFAIL(e.what()); } // Test querytable::TableRow::operator> and operator== QueryTable::TableRow low; low[ListTable::ctPrice] = 'A'; low[ListTable::ctLastPrice] = 'B'; low[ListTable::ctBuyPrice] = 'C'; QueryTable::TableRow high; high[ListTable::ctPrice] = 'A'; high[ListTable::ctLastPrice] = 'C'; high[ListTable::ctBuyPrice] = 'B'; QueryTable::TableRow::setSortCriteria({ListTable::ctPrice, ListTable::ctLastPrice, ListTable::ctBuyPrice}); QVERIFY(low < high); QVERIFY(low <= high); QVERIFY(high > low); QVERIFY(high <= high); QVERIFY(high == high); } void QueryTableTest::testCashFlowAnalysis() { // - // Test IRR calculations + // Test XIRR calculations // CashFlowList list; list += CashFlowListItem(QDate(2004, 5, 3), MyMoneyMoney(1000.0)); list += CashFlowListItem(QDate(2004, 5, 20), MyMoneyMoney(59.0)); list += CashFlowListItem(QDate(2004, 6, 3), MyMoneyMoney(14.0)); list += CashFlowListItem(QDate(2004, 6, 24), MyMoneyMoney(92.0)); list += CashFlowListItem(QDate(2004, 7, 6), MyMoneyMoney(63.0)); list += CashFlowListItem(QDate(2004, 7, 25), MyMoneyMoney(15.0)); list += CashFlowListItem(QDate(2004, 8, 5), MyMoneyMoney(92.0)); list += CashFlowListItem(QDate(2004, 9, 2), MyMoneyMoney(18.0)); list += CashFlowListItem(QDate(2004, 9, 21), MyMoneyMoney(5.0)); list += CashFlowListItem(QDate(2004, 10, 16), MyMoneyMoney(-2037.0)); - MyMoneyMoney IRR(list.IRR(), 1000); - - QVERIFY(IRR == MyMoneyMoney(1676, 1000)); + MyMoneyMoney XIRR(list.XIRR(), 1000); + QVERIFY(XIRR == MyMoneyMoney(1676, 1000)); list.pop_back(); list += CashFlowListItem(QDate(2004, 10, 16), MyMoneyMoney(-1358.0)); - IRR = MyMoneyMoney(list.IRR(), 1000); + XIRR = MyMoneyMoney(list.XIRR(), 1000); + QVERIFY(XIRR.isZero()); + + // two entries + list.clear(); + list += CashFlowListItem(QDate(2005, 9, 22), MyMoneyMoney(-3472.57)); + list += CashFlowListItem(QDate(2009, 3, 18), MyMoneyMoney(6051.36)); + + XIRR = MyMoneyMoney(list.XIRR(), 1000); + QCOMPARE(XIRR.toDouble(), MyMoneyMoney(173, 1000).toDouble()); + + // check ignoring zero values + list += CashFlowListItem(QDate(1998, 11, 1), MyMoneyMoney(0.0)); + list += CashFlowListItem(QDate(2017, 8, 11), MyMoneyMoney(0.0)); + + XIRR = MyMoneyMoney(list.XIRR(), 1000); + QCOMPARE(XIRR.toDouble(), MyMoneyMoney(173, 1000).toDouble()); + + list.pop_back(); + // former implementation crashed + list += CashFlowListItem(QDate(2014, 8, 11), MyMoneyMoney(0.0)); + + XIRR = MyMoneyMoney(list.XIRR(), 1000); + QCOMPARE(XIRR.toDouble(), MyMoneyMoney(173, 1000).toDouble()); - QVERIFY(IRR.isZero()); + // different ordering + list.clear(); + list += CashFlowListItem(QDate(2004, 3, 18), MyMoneyMoney(6051.36)); + list += CashFlowListItem(QDate(2005, 9, 22), MyMoneyMoney(-3472.57)); + + XIRR = MyMoneyMoney(list.XIRR(), 1000); + QCOMPARE(XIRR.toDouble(), MyMoneyMoney(-307, 1000).toDouble()); + + + // not enough entries + list.pop_back(); + + bool result = false; + try { + XIRR = MyMoneyMoney(list.XIRR(), 1000); + } catch (MyMoneyException &e) { + result = true; + } + QVERIFY(result); } void QueryTableTest::testAccountQuery() { try { QString htmlcontext = QString("\n\n%1\n\n"); // // No transactions, opening balances only // MyMoneyReport filter; filter.setRowType(eMyMoney::Report::RowType::Institution); filter.setName("Accounts by Institution (No transactions)"); XMLandback(filter); QueryTable qtbl_1(filter); writeTabletoHTML(qtbl_1, "Accounts by Institution (No transactions).html"); QList rows = qtbl_1.rows(); QVERIFY(rows.count() == 6); QVERIFY(rows[0][ListTable::ctAccount] == "Checking Account"); QVERIFY(MyMoneyMoney(rows[0][ListTable::ctValue]) == moCheckingOpen); QVERIFY(rows[0][ListTable::ctEquityType].isEmpty()); QVERIFY(rows[2][ListTable::ctAccount] == "Credit Card"); QVERIFY(MyMoneyMoney(rows[1][ListTable::ctValue]) == moCreditOpen); QVERIFY(rows[2][ListTable::ctEquityType].isEmpty()); QVERIFY(MyMoneyMoney(rows[4][ListTable::ctValue]) == moCheckingOpen + moCreditOpen); QVERIFY(MyMoneyMoney(rows[5][ListTable::ctValue]) == moCheckingOpen + moCreditOpen); // // Adding in transactions // TransactionHelper t1q1(QDate(2004, 1, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moSolo, acChecking, acSolo); TransactionHelper t2q1(QDate(2004, 2, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent1, acCredit, acParent); TransactionHelper t3q1(QDate(2004, 3, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent2, acCredit, acParent); TransactionHelper t4y1(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moChild, acCredit, acChild); TransactionHelper t1q2(QDate(2004, 4, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moSolo, acChecking, acSolo); TransactionHelper t2q2(QDate(2004, 5, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent1, acCredit, acParent); TransactionHelper t3q2(QDate(2004, 6, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent2, acCredit, acParent); TransactionHelper t4q2(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moChild, acCredit, acChild); TransactionHelper t1y2(QDate(2005, 1, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moSolo, acChecking, acSolo); TransactionHelper t2y2(QDate(2005, 5, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent1, acCredit, acParent); TransactionHelper t3y2(QDate(2005, 9, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent2, acCredit, acParent); TransactionHelper t4y2(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moChild, acCredit, acChild); filter.setRowType(eMyMoney::Report::RowType::Institution); filter.setName("Accounts by Institution (With Transactions)"); XMLandback(filter); QueryTable qtbl_2(filter); rows = qtbl_2.rows(); QVERIFY(rows.count() == 6); QVERIFY(rows[0][ListTable::ctAccount] == "Checking Account"); QVERIFY(MyMoneyMoney(rows[0][ListTable::ctValue]) == (moCheckingOpen - moSolo*3)); QVERIFY(rows[2][ListTable::ctAccount] == "Credit Card"); QVERIFY(MyMoneyMoney(rows[2][ListTable::ctValue]) == (moCreditOpen - (moParent1 + moParent2 + moChild) * 3)); QVERIFY(MyMoneyMoney(rows[5][ListTable::ctValue]) == moCheckingOpen + moCreditOpen - (moParent1 + moParent2 + moSolo + moChild) * 3); // // Account TYPES // filter.setRowType(eMyMoney::Report::RowType::AccountType); filter.setName("Accounts by Type"); XMLandback(filter); QueryTable qtbl_3(filter); rows = qtbl_3.rows(); QVERIFY(rows.count() == 5); QVERIFY(rows[0][ListTable::ctAccount] == "Checking Account"); QVERIFY(MyMoneyMoney(rows[0][ListTable::ctValue]) == (moCheckingOpen - moSolo * 3)); QVERIFY(rows[2][ListTable::ctAccount] == "Credit Card"); QVERIFY(MyMoneyMoney(rows[2][ListTable::ctValue]) == (moCreditOpen - (moParent1 + moParent2 + moChild) * 3)); QVERIFY(MyMoneyMoney(rows[1][ListTable::ctValue]) == moCheckingOpen - moSolo * 3); QVERIFY(MyMoneyMoney(rows[3][ListTable::ctValue]) == moCreditOpen - (moParent1 + moParent2 + moChild) * 3); QVERIFY(MyMoneyMoney(rows[4][ListTable::ctValue]) == moCheckingOpen + moCreditOpen - (moParent1 + moParent2 + moSolo + moChild) * 3); } catch (const MyMoneyException &e) { QFAIL(e.what()); } } void QueryTableTest::testInvestment() { try { // Equities eqStock1 = makeEquity("Stock1", "STK1"); eqStock2 = makeEquity("Stock2", "STK2"); eqStock3 = makeEquity("Stock3", "STK3"); eqStock4 = makeEquity("Stock4", "STK4"); // Accounts acInvestment = makeAccount("Investment", eMyMoney::Account::Type::Investment, moZero, QDate(2003, 11, 1), acAsset); acStock1 = makeAccount("Stock 1", eMyMoney::Account::Type::Stock, moZero, QDate(2004, 1, 1), acInvestment, eqStock1); acStock2 = makeAccount("Stock 2", eMyMoney::Account::Type::Stock, moZero, QDate(2004, 1, 1), acInvestment, eqStock2); acStock3 = makeAccount("Stock 3", eMyMoney::Account::Type::Stock, moZero, QDate(2003, 11, 1), acInvestment, eqStock3); acStock4 = makeAccount("Stock 4", eMyMoney::Account::Type::Stock, moZero, QDate(2004, 1, 1), acInvestment, eqStock4); acDividends = makeAccount("Dividends", eMyMoney::Account::Type::Income, moZero, QDate(2004, 1, 1), acIncome); acInterest = makeAccount("Interest", eMyMoney::Account::Type::Income, moZero, QDate(2004, 1, 1), acIncome); acFees = makeAccount("Fees", eMyMoney::Account::Type::Expense, moZero, QDate(2003, 11, 1), acExpense); // Transactions // Date Action Shares Price Stock Asset Income InvTransactionHelper s1b1(QDate(2003, 12, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::BuyShares), MyMoneyMoney(1000.00), MyMoneyMoney(100.00), acStock3, acChecking, QString()); InvTransactionHelper s1b2(QDate(2004, 1, 30), MyMoneySplit::actionName(eMyMoney::Split::Action::BuyShares), MyMoneyMoney(500.00), MyMoneyMoney(100.00), acStock4, acChecking, acFees, MyMoneyMoney(100.00)); InvTransactionHelper s1b3(QDate(2004, 1, 30), MyMoneySplit::actionName(eMyMoney::Split::Action::BuyShares), MyMoneyMoney(500.00), MyMoneyMoney(90.00), acStock4, acChecking, acFees, MyMoneyMoney(100.00)); InvTransactionHelper s1b4(QDate(2004, 2, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::BuyShares), MyMoneyMoney(1000.00), MyMoneyMoney(100.00), acStock1, acChecking, QString()); InvTransactionHelper s1b5(QDate(2004, 3, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::BuyShares), MyMoneyMoney(1000.00), MyMoneyMoney(110.00), acStock1, acChecking, QString()); InvTransactionHelper s1s1(QDate(2004, 4, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::BuyShares), MyMoneyMoney(-200.00), MyMoneyMoney(120.00), acStock1, acChecking, QString()); InvTransactionHelper s1s2(QDate(2004, 5, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::BuyShares), MyMoneyMoney(-200.00), MyMoneyMoney(100.00), acStock1, acChecking, QString()); InvTransactionHelper s1s3(QDate(2004, 5, 30), MyMoneySplit::actionName(eMyMoney::Split::Action::BuyShares), MyMoneyMoney(-1000.00), MyMoneyMoney(120.00), acStock4, acChecking, acFees, MyMoneyMoney(200.00)); InvTransactionHelper s1r1(QDate(2004, 6, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::ReinvestDividend), MyMoneyMoney(50.00), MyMoneyMoney(100.00), acStock1, QString(), acDividends); InvTransactionHelper s1r2(QDate(2004, 7, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::ReinvestDividend), MyMoneyMoney(50.00), MyMoneyMoney(80.00), acStock1, QString(), acDividends); InvTransactionHelper s1c1(QDate(2004, 8, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Dividend), MyMoneyMoney(10.00), MyMoneyMoney(100.00), acStock1, acChecking, acDividends); InvTransactionHelper s1c2(QDate(2004, 9, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Dividend), MyMoneyMoney(10.00), MyMoneyMoney(120.00), acStock1, acChecking, acDividends); InvTransactionHelper s1y1(QDate(2004, 9, 15), MyMoneySplit::actionName(eMyMoney::Split::Action::Yield), MyMoneyMoney(10.00), MyMoneyMoney(110.00), acStock1, acChecking, acInterest); makeEquityPrice(eqStock1, QDate(2004, 10, 1), MyMoneyMoney(100.00)); makeEquityPrice(eqStock3, QDate(2004, 10, 1), MyMoneyMoney(110.00)); makeEquityPrice(eqStock4, QDate(2004, 10, 1), MyMoneyMoney(110.00)); // // Investment Transactions Report // MyMoneyReport invtran_r( eMyMoney::Report::RowType::TopAccount, eMyMoney::Report::QueryColumn::Action | eMyMoney::Report::QueryColumn::Shares | eMyMoney::Report::QueryColumn::Price, eMyMoney::TransactionFilter::Date::UserDefined, eMyMoney::Report::DetailLevel::All, i18n("Investment Transactions"), i18n("Test Report") ); invtran_r.setDateFilter(QDate(2004, 1, 1), QDate(2004, 12, 31)); invtran_r.setInvestmentsOnly(true); XMLandback(invtran_r); QueryTable invtran(invtran_r); #if 1 writeTabletoHTML(invtran, "investment_transactions_test.html"); QList rows = invtran.rows(); QVERIFY(rows.count() == 32); QVERIFY(MyMoneyMoney(rows[1][ListTable::ctValue]) == MyMoneyMoney(-100000.00)); QVERIFY(MyMoneyMoney(rows[2][ListTable::ctValue]) == MyMoneyMoney(-110000.00)); QVERIFY(MyMoneyMoney(rows[3][ListTable::ctValue]) == MyMoneyMoney(24000.00)); QVERIFY(MyMoneyMoney(rows[4][ListTable::ctValue]) == MyMoneyMoney(20000.00)); QVERIFY(MyMoneyMoney(rows[5][ListTable::ctValue]) == MyMoneyMoney(5000.00)); QVERIFY(MyMoneyMoney(rows[6][ListTable::ctValue]) == MyMoneyMoney(4000.00)); QVERIFY(MyMoneyMoney(rows[19][ListTable::ctValue]) == MyMoneyMoney(-50100.00)); QVERIFY(MyMoneyMoney(rows[22][ListTable::ctValue]) == MyMoneyMoney(-45100.00)); // need to fix these... fundamentally different from the original test //QVERIFY(MyMoneyMoney(invtran.m_rows[8][ListTable::ctValue])==MyMoneyMoney( -1000.00)); //QVERIFY(MyMoneyMoney(invtran.m_rows[11][ListTable::ctValue])==MyMoneyMoney( -1200.00)); //QVERIFY(MyMoneyMoney(invtran.m_rows[14][ListTable::ctValue])==MyMoneyMoney( -1100.00)); QVERIFY(MyMoneyMoney(rows[1][ListTable::ctPrice]) == MyMoneyMoney(100.00)); QVERIFY(MyMoneyMoney(rows[3][ListTable::ctPrice]) == MyMoneyMoney(120.00)); QVERIFY(MyMoneyMoney(rows[5][ListTable::ctPrice]) == MyMoneyMoney(100.00)); QVERIFY(MyMoneyMoney(rows[7][ListTable::ctPrice]) == MyMoneyMoney()); QVERIFY(MyMoneyMoney(rows[10][ListTable::ctPrice]) == MyMoneyMoney()); QVERIFY(MyMoneyMoney(rows[19][ListTable::ctPrice]) == MyMoneyMoney(100.00)); QVERIFY(MyMoneyMoney(rows[22][ListTable::ctPrice]) == MyMoneyMoney(90.00)); QVERIFY(MyMoneyMoney(rows[2][ListTable::ctShares]) == MyMoneyMoney(1000.00)); QVERIFY(MyMoneyMoney(rows[4][ListTable::ctShares]) == MyMoneyMoney(-200.00)); QVERIFY(MyMoneyMoney(rows[6][ListTable::ctShares]) == MyMoneyMoney(50.00)); QVERIFY(MyMoneyMoney(rows[8][ListTable::ctShares]) == MyMoneyMoney(0.00)); QVERIFY(MyMoneyMoney(rows[11][ListTable::ctShares]) == MyMoneyMoney(0.00)); QVERIFY(MyMoneyMoney(rows[19][ListTable::ctShares]) == MyMoneyMoney(500.00)); QVERIFY(MyMoneyMoney(rows[22][ListTable::ctShares]) == MyMoneyMoney(500.00)); QVERIFY(rows[1][ListTable::ctAction] == "Buy"); QVERIFY(rows[3][ListTable::ctAction] == "Sell"); QVERIFY(rows[5][ListTable::ctAction] == "Reinvest"); QVERIFY(rows[7][ListTable::ctAction] == "Dividend"); QVERIFY(rows[13][ListTable::ctAction] == "Yield"); QVERIFY(rows[19][ListTable::ctAction] == "Buy"); QVERIFY(rows[22][ListTable::ctAction] == "Buy"); #else QVERIFY(rows.count() == 9); QVERIFY(MyMoneyMoney(rows[0][ListTable::ctValue]) == MyMoneyMoney(100000.00)); QVERIFY(MyMoneyMoney(rows[1][ListTable::ctValue]) == MyMoneyMoney(110000.00)); QVERIFY(MyMoneyMoney(rows[2][ListTable::ctValue]) == MyMoneyMoney(-24000.00)); QVERIFY(MyMoneyMoney(rows[3][ListTable::ctValue]) == MyMoneyMoney(-20000.00)); QVERIFY(MyMoneyMoney(rows[4][ListTable::ctValue]) == MyMoneyMoney(5000.00)); QVERIFY(MyMoneyMoney(rows[5][ListTable::ctValue]) == MyMoneyMoney(4000.00)); QVERIFY(MyMoneyMoney(rows[6][ListTable::ctValue]) == MyMoneyMoney(-1000.00)); QVERIFY(MyMoneyMoney(rows[7][ListTable::ctValue]) == MyMoneyMoney(-1200.00)); QVERIFY(MyMoneyMoney(rows[8][ListTable::ctValue]) == MyMoneyMoney(-1100.00)); QVERIFY(MyMoneyMoney(rows[0][ListTable::ctPrice]) == MyMoneyMoney(100.00)); QVERIFY(MyMoneyMoney(rows[2][ListTable::ctPrice]) == MyMoneyMoney(120.00)); QVERIFY(MyMoneyMoney(rows[4][ListTable::ctPrice]) == MyMoneyMoney(100.00)); QVERIFY(MyMoneyMoney(rows[6][ListTable::ctPrice]) == MyMoneyMoney(0.00)); QVERIFY(MyMoneyMoney(rows[8][ListTable::ctPrice]) == MyMoneyMoney(0.00)); QVERIFY(MyMoneyMoney(rows[1][ListTable::ctShares]) == MyMoneyMoney(1000.00)); QVERIFY(MyMoneyMoney(rows[3][ListTable::ctShares]) == MyMoneyMoney(-200.00)); QVERIFY(MyMoneyMoney(rows[5][ListTable::ctShares]) == MyMoneyMoney(50.00)); QVERIFY(MyMoneyMoney(rows[7][ListTable::ctShares]) == MyMoneyMoney(0.00)); QVERIFY(MyMoneyMoney(rows[8][ListTable::ctShares]) == MyMoneyMoney(0.00)); QVERIFY(rows[0][ListTable::ctAction] == "Buy"); QVERIFY(rows[2][ListTable::ctAction] == "Sell"); QVERIFY(rows[4][ListTable::ctAction] == "Reinvest"); QVERIFY(rows[6][ListTable::ctAction] == "Dividend"); QVERIFY(rows[8][ListTable::ctAction] == "Yield"); #endif #if 1 // i think this is the correct amount. different treatment of dividend and yield QVERIFY(MyMoneyMoney(rows[17][ListTable::ctValue]) == MyMoneyMoney(-153700.00)); QVERIFY(MyMoneyMoney(rows[29][ListTable::ctValue]) == MyMoneyMoney(24600.00)); QVERIFY(MyMoneyMoney(rows[31][ListTable::ctValue]) == MyMoneyMoney(-129100.00)); #else QVERIFY(searchHTML(html, i18n("Total Stock 1")) == MyMoneyMoney(171700.00)); QVERIFY(searchHTML(html, i18n("Grand Total")) == MyMoneyMoney(171700.00)); #endif // // Investment Performance Report // MyMoneyReport invhold_r( eMyMoney::Report::RowType::AccountByTopAccount, eMyMoney::Report::QueryColumn::Performance, eMyMoney::TransactionFilter::Date::UserDefined, eMyMoney::Report::DetailLevel::All, i18n("Investment Performance by Account"), i18n("Test Report") ); invhold_r.setDateFilter(QDate(2004, 1, 1), QDate(2004, 10, 1)); invhold_r.setInvestmentsOnly(true); XMLandback(invhold_r); QueryTable invhold(invhold_r); writeTabletoHTML(invhold, "Investment Performance by Account.html"); rows = invhold.rows(); QVERIFY(rows.count() == 5); QVERIFY(MyMoneyMoney(rows[0][ListTable::ctReturn]) == MyMoneyMoney("669/10000")); QVERIFY(MyMoneyMoney(rows[0][ListTable::ctReturnInvestment]) == MyMoneyMoney("-39/5000")); QVERIFY(MyMoneyMoney(rows[0][ListTable::ctBuys]) == MyMoneyMoney(-210000.00)); QVERIFY(MyMoneyMoney(rows[0][ListTable::ctSells]) == MyMoneyMoney(44000.00)); QVERIFY(MyMoneyMoney(rows[0][ListTable::ctReinvestIncome]) == MyMoneyMoney(9000.00)); QVERIFY(MyMoneyMoney(rows[0][ListTable::ctCashIncome]) == MyMoneyMoney(3300.00)); QVERIFY(MyMoneyMoney(rows[1][ListTable::ctReturn]) == MyMoneyMoney("1349/10000")); QVERIFY(MyMoneyMoney(rows[1][ListTable::ctReturnInvestment]) == MyMoneyMoney("1/10")); QVERIFY(MyMoneyMoney(rows[1][ListTable::ctStartingBalance]) == MyMoneyMoney(100000.00)); // this should stay non-zero to check if investment performance is calculated at non-zero starting balance QVERIFY(MyMoneyMoney(rows[2][ListTable::ctReturn]) == MyMoneyMoney("2501/2500")); QVERIFY(MyMoneyMoney(rows[2][ListTable::ctReturnInvestment]) == MyMoneyMoney("323/1250")); QVERIFY(MyMoneyMoney(rows[2][ListTable::ctBuys]) == MyMoneyMoney(-95200.00)); QVERIFY(MyMoneyMoney(rows[2][ListTable::ctSells]) == MyMoneyMoney(119800.00)); QVERIFY(MyMoneyMoney(rows[2][ListTable::ctEndingBalance]) == MyMoneyMoney(0.00)); // this should stay zero to check if investment performance is calculated at zero ending balance QVERIFY(MyMoneyMoney(rows[4][ListTable::ctEndingBalance]) == MyMoneyMoney(280000.00)); #if 0 // Dump file & reports QFile g("investmentkmy.xml"); g.open(QIODevice::WriteOnly); MyMoneyStorageXML xml; IMyMoneyOperationsFormat& interface = xml; interface.writeFile(&g, dynamic_cast(MyMoneyFile::instance()->storage())); g.close(); invtran.dump("invtran.html", "%1"); invhold.dump("invhold.html", "%1"); #endif } catch (const MyMoneyException &e) { QFAIL(e.what()); } } // prevents bug #312135 void QueryTableTest::testSplitShares() { try { MyMoneyMoney firstSharesPurchase(16); MyMoneyMoney splitFactor(2); MyMoneyMoney secondSharesPurchase(1); MyMoneyMoney sharesAtTheEnd = firstSharesPurchase / splitFactor + secondSharesPurchase; MyMoneyMoney priceBeforeSplit(74.99, 100); MyMoneyMoney priceAfterSplit = splitFactor * priceBeforeSplit; // Equities eqStock1 = makeEquity("Stock1", "STK1"); // Accounts acInvestment = makeAccount("Investment", eMyMoney::Account::Type::Investment, moZero, QDate(2017, 8, 1), acAsset); acStock1 = makeAccount("Stock 1", eMyMoney::Account::Type::Stock, moZero, QDate(2017, 8, 1), acInvestment, eqStock1); // Transactions // Date Action Shares Price Stock Asset Income InvTransactionHelper s1b1(QDate(2017, 8, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::BuyShares), firstSharesPurchase, priceBeforeSplit, acStock1, acChecking, QString()); InvTransactionHelper s1s1(QDate(2017, 8, 2), MyMoneySplit::actionName(eMyMoney::Split::Action::SplitShares), splitFactor, MyMoneyMoney(), acStock1, QString(), QString()); InvTransactionHelper s1b2(QDate(2017, 8, 3), MyMoneySplit::actionName(eMyMoney::Split::Action::BuyShares), secondSharesPurchase, priceAfterSplit, acStock1, acChecking, QString()); // // Investment Performance Report // MyMoneyReport invhold_r( eMyMoney::Report::RowType::AccountByTopAccount, eMyMoney::Report::QueryColumn::Performance, eMyMoney::TransactionFilter::Date::UserDefined, eMyMoney::Report::DetailLevel::All, i18n("Investment Performance by Account"), i18n("Test Report") ); invhold_r.setDateFilter(QDate(2017, 8, 1), QDate(2017, 8, 3)); invhold_r.setInvestmentsOnly(true); XMLandback(invhold_r); QueryTable invhold(invhold_r); writeTabletoHTML(invhold, "Investment Performance by Account (with stock split).html"); const auto rows = invhold.rows(); QVERIFY(rows.count() == 3); QVERIFY(MyMoneyMoney(rows[0][ListTable::ctBuys]) == sharesAtTheEnd * priceAfterSplit * MyMoneyMoney(-1)); } catch (const MyMoneyException &e) { QFAIL(e.what()); } } // prevents bug #118159 void QueryTableTest::testConversionRate() { try { MyMoneyMoney firsConversionRate(1.1800, 10000); MyMoneyMoney secondConversionRate(1.1567, 10000); MyMoneyMoney amountWithdrawn(100); const auto acCadChecking = makeAccount(QString("Canadian Checking"), eMyMoney::Account::Type::Checkings, moZero, QDate(2017, 8, 1), acAsset, "CAD"); makePrice("CAD", QDate(2017, 8, 1), firsConversionRate); makePrice("CAD", QDate(2017, 8, 2), secondConversionRate); TransactionHelper t1(QDate(2017, 8, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), amountWithdrawn, acCadChecking, acSolo, "CAD"); MyMoneyReport filter; filter.setRowType(eMyMoney::Report::RowType::Account); filter.setDateFilter(QDate(2017, 8, 1), QDate(2017, 8, 2)); filter.setName("Transactions by Account"); auto cols = eMyMoney::Report::QueryColumn::Number | eMyMoney::Report::QueryColumn::Payee | eMyMoney::Report::QueryColumn::Category | eMyMoney::Report::QueryColumn::Balance; filter.setQueryColumns(static_cast(cols)); // XMLandback(filter); QueryTable qtbl(filter); writeTabletoHTML(qtbl, "Transactions by Account (conversion rate).html"); const auto rows = qtbl.rows(); QVERIFY(rows.count() == 5); QVERIFY(MyMoneyMoney(rows[1][ListTable::ctValue]) == amountWithdrawn * firsConversionRate * MyMoneyMoney(-1)); QVERIFY(MyMoneyMoney(rows[1][ListTable::ctPrice]) == firsConversionRate); QVERIFY(MyMoneyMoney(rows[2][ListTable::ctBalance]) == amountWithdrawn * secondConversionRate * MyMoneyMoney(-1)); QVERIFY(MyMoneyMoney(rows[2][ListTable::ctPrice]) == secondConversionRate); } catch (const MyMoneyException &e) { QFAIL(e.what()); } } //this is to prevent me from making mistakes again when modifying balances - asoliverez //this case tests only the opening and ending balance of the accounts void QueryTableTest::testBalanceColumn() { try { TransactionHelper t1q1(QDate(2004, 1, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moSolo, acChecking, acSolo); TransactionHelper t2q1(QDate(2004, 2, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent1, acCredit, acParent); TransactionHelper t3q1(QDate(2004, 3, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent2, acCredit, acParent); TransactionHelper t4y1(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moChild, acCredit, acChild); TransactionHelper t1q2(QDate(2004, 4, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moSolo, acChecking, acSolo); TransactionHelper t2q2(QDate(2004, 5, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent1, acCredit, acParent); TransactionHelper t3q2(QDate(2004, 6, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent2, acCredit, acParent); TransactionHelper t4q2(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moChild, acCredit, acChild); TransactionHelper t1y2(QDate(2005, 1, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moSolo, acChecking, acSolo); TransactionHelper t2y2(QDate(2005, 5, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent1, acCredit, acParent); TransactionHelper t3y2(QDate(2005, 9, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent2, acCredit, acParent); TransactionHelper t4y2(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moChild, acCredit, acChild); unsigned cols; MyMoneyReport filter; filter.setRowType(eMyMoney::Report::RowType::Account); filter.setName("Transactions by Account"); cols = eMyMoney::Report::QueryColumn::Number | eMyMoney::Report::QueryColumn::Payee | eMyMoney::Report::QueryColumn::Category | eMyMoney::Report::QueryColumn::Balance; filter.setQueryColumns(static_cast(cols)); // XMLandback(filter); QueryTable qtbl_3(filter); writeTabletoHTML(qtbl_3, "Transactions by Account.html"); QString html = qtbl_3.renderHTML(); QList rows = qtbl_3.rows(); QVERIFY(rows.count() == 19); //this is to make sure that the dates of closing and opening balances and the balance numbers are ok QString openingDate = QLocale().toString(QDate(2004, 1, 1), QLocale::ShortFormat); QString closingDate = QLocale().toString(QDate(2005, 9, 1), QLocale::ShortFormat); QVERIFY(html.indexOf(openingDate + "" + i18n("Opening Balance")) > 0); QVERIFY(html.indexOf(closingDate + "" + i18n("Closing Balance") + " -702.36") > 0); QVERIFY(html.indexOf(closingDate + "" + i18n("Closing Balance") + " -705.69") > 0); } catch (const MyMoneyException &e) { QFAIL(e.what()); } } void QueryTableTest::testBalanceColumnWithMultipleCurrencies() { try { MyMoneyMoney moJpyOpening(0.0, 1); MyMoneyMoney moJpyPrice(0.010, 100); MyMoneyMoney moJpyPrice2(0.011, 100); MyMoneyMoney moJpyPrice3(0.024, 100); MyMoneyMoney moTransaction(100, 1); MyMoneyMoney moJpyTransaction(100, 1); QString acJpyChecking = makeAccount(QString("Japanese Checking"), eMyMoney::Account::Type::Checkings, moJpyOpening, QDate(2003, 11, 15), acAsset, "JPY"); makePrice("JPY", QDate(2004, 1, 1), MyMoneyMoney(moJpyPrice)); makePrice("JPY", QDate(2004, 5, 1), MyMoneyMoney(moJpyPrice2)); makePrice("JPY", QDate(2004, 6, 30), MyMoneyMoney(moJpyPrice3)); QDate openingDate(2004, 2, 20); QDate intermediateDate(2004, 5, 20); QDate closingDate(2004, 7, 20); TransactionHelper t1(openingDate, MyMoneySplit::actionName(eMyMoney::Split::Action::Transfer), MyMoneyMoney(moJpyTransaction), acJpyChecking, acChecking, "JPY"); TransactionHelper t4(openingDate, MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit), MyMoneyMoney(moTransaction), acCredit, acChecking); TransactionHelper t2(intermediateDate, MyMoneySplit::actionName(eMyMoney::Split::Action::Transfer), MyMoneyMoney(moJpyTransaction), acJpyChecking, acChecking, "JPY"); TransactionHelper t5(intermediateDate, MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit), MyMoneyMoney(moTransaction), acCredit, acChecking); TransactionHelper t3(closingDate, MyMoneySplit::actionName(eMyMoney::Split::Action::Transfer), MyMoneyMoney(moJpyTransaction), acJpyChecking, acChecking, "JPY"); TransactionHelper t6(closingDate, MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit), MyMoneyMoney(moTransaction), acCredit, acChecking); // test that an income/expense transaction that involves a currency exchange is properly reported TransactionHelper t7(intermediateDate, MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), MyMoneyMoney(moJpyTransaction), acJpyChecking, acSolo, "JPY"); unsigned cols; MyMoneyReport filter; filter.setRowType(eMyMoney::Report::RowType::Account); filter.setName("Transactions by Account"); cols = eMyMoney::Report::QueryColumn::Number | eMyMoney::Report::QueryColumn::Payee | eMyMoney::Report::QueryColumn::Category | eMyMoney::Report::QueryColumn::Balance; filter.setQueryColumns(static_cast(cols)); // don't convert values to the default currency filter.setConvertCurrency(false); XMLandback(filter); QueryTable qtbl_3(filter); writeTabletoHTML(qtbl_3, "Transactions by Account (multiple currencies).html"); QString html = qtbl_3.renderHTML(); QList rows = qtbl_3.rows(); QVERIFY(rows.count() == 24); //this is to make sure that the dates of closing and opening balances and the balance numbers are ok QString openingDateString = QLocale().toString(openingDate, QLocale::ShortFormat); QString intermediateDateString = QLocale().toString(intermediateDate, QLocale::ShortFormat); QString closingDateString = QLocale().toString(closingDate, QLocale::ShortFormat); // check the opening and closing balances QVERIFY(html.indexOf(openingDateString + "" + i18n("Opening Balance") + "USD 0.00") > 0); QVERIFY(html.indexOf(closingDateString + "" + i18n("Closing Balance") + "USD 304.00") > 0); QVERIFY(html.indexOf(closingDateString + "" + i18n("Closing Balance") + "USD -300.00") > 0); QVERIFY(html.indexOf(closingDateString + "" + i18n("Closing Balance") + "JPY -400.00") > 0); // after a transfer of 100 JPY the balance should be 1.00 - price is 0.010 (precision of 2) QVERIFY(html.indexOf("" + openingDateString + "Test PayeeTransfer from Japanese CheckingUSD 1.00USD 1.00") > 0); // after a transfer of 100 the balance should be 101.00 QVERIFY(html.indexOf("" + openingDateString + "Test PayeeTransfer from Credit CardUSD 100.00USD 101.00") > 0); // after a transfer of 100 JPY the balance should be 102.00 - price is 0.011 (precision of 2) QVERIFY(html.indexOf("" + intermediateDateString + "Test PayeeTransfer from Japanese CheckingUSD 1.00USD 102.00") > 0); // after a transfer of 100 the balance should be 202.00 QVERIFY(html.indexOf("" + intermediateDateString + "Test PayeeTransfer from Credit CardUSD 100.00USD 202.00") > 0); // after a transfer of 100 JPY the balance should be 204.00 - price is 0.024 (precision of 2) QVERIFY(html.indexOf("" + closingDateString + "Test PayeeTransfer from Japanese CheckingUSD 2.00USD 204.00") > 0); // after a transfer of 100 the balance should be 304.00 QVERIFY(html.indexOf("" + closingDateString + "Test PayeeTransfer from Credit CardUSD 100.00USD 304.00") > 0); // a 100.00 JPY withdrawal should be displayed as such even if the expense account uses another currency QVERIFY(html.indexOf("" + intermediateDateString + "Test PayeeSoloJPY -100.00JPY -300.00") > 0); // now run the same report again but this time convert all values to the base currency and make sure the values are correct filter.setConvertCurrency(true); XMLandback(filter); QueryTable qtbl_4(filter); writeTabletoHTML(qtbl_4, "Transactions by Account (multiple currencies converted to base).html"); html = qtbl_4.renderHTML(); rows = qtbl_4.rows(); QVERIFY(rows.count() == 23); // check the opening and closing balances QVERIFY(html.indexOf(openingDateString + "" + i18n("Opening Balance") + " 0.00") > 0); QVERIFY(html.indexOf(closingDateString + "" + i18n("Closing Balance") + " 304.00") > 0); QVERIFY(html.indexOf(closingDateString + "" + i18n("Closing Balance") + " -300.00") > 0); // although the balance should be -5.00 it's -8.00 because the foreign currency balance is converted using the closing date price (0.024) QVERIFY(html.indexOf(closingDateString + "" + i18n("Closing Balance") + " -8.00") > 0); // a 100.00 JPY transfer should be displayed as -1.00 when converted to the base currency using the opening date price QVERIFY(html.indexOf("" + openingDateString + "Test PayeeTransfer to Checking Account -1.00 -1.00") > 0); // a 100.00 JPY transfer should be displayed as -1.00 when converted to the base currency using the intermediate date price QVERIFY(html.indexOf("" + intermediateDateString + "Test PayeeTransfer to Checking Account -1.00 -2.00") > 0); // a 100.00 JPY transfer should be displayed as -2.00 when converted to the base currency using the closing date price (notice the balance is -5.00) QVERIFY(html.indexOf("" + closingDateString + "Test PayeeTransfer to Checking Account -2.00 -5.00") > 0); // a 100.00 JPY withdrawal should be displayed as -1.00 when converted to the base currency using the intermediate date price QVERIFY(html.indexOf("" + intermediateDateString + "Test PayeeSolo -1.00 -3.00") > 0); } catch (const MyMoneyException &e) { QFAIL(e.what()); } } void QueryTableTest::testTaxReport() { try { TransactionHelper t1q1(QDate(2004, 1, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moSolo, acChecking, acSolo); TransactionHelper t2q1(QDate(2004, 2, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent1, acChecking, acTax); unsigned cols; MyMoneyReport filter; filter.setRowType(eMyMoney::Report::RowType::Category); filter.setName("Tax Transactions"); cols = eMyMoney::Report::QueryColumn::Number | eMyMoney::Report::QueryColumn::Payee | eMyMoney::Report::QueryColumn::Account; filter.setQueryColumns(static_cast(cols)); filter.setTax(true); XMLandback(filter); QueryTable qtbl_3(filter); writeTabletoHTML(qtbl_3, "Tax Transactions.html"); QList rows = qtbl_3.rows(); QString html = qtbl_3.renderHTML(); QVERIFY(rows.count() == 5); } catch (const MyMoneyException &e) { QFAIL(e.what()); } } diff --git a/kmymoney/plugins/views/reports/kreportsview.cpp b/kmymoney/plugins/views/reports/kreportsview.cpp index 7db64d11d..72a6294bf 100644 --- a/kmymoney/plugins/views/reports/kreportsview.cpp +++ b/kmymoney/plugins/views/reports/kreportsview.cpp @@ -1,720 +1,762 @@ /*************************************************************************** kreportsview.cpp - description ------------------- begin : Sat Mar 27 2004 copyright : (C) 2000-2004 by Michael Edwardes email : mte@users.sourceforge.net Javier Campos Morales Felix Rodriguez John C Thomas Baumgart Kevin Tambascio Ace Jones (C) 2017 Łukasz Wojniłowicz ***************************************************************************/ /*************************************************************************** * * * 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 "kreportsview_p.h" #include // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef ENABLE_WEBENGINE #include #else #include #endif // ---------------------------------------------------------------------------- // KDE Includes #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "ui_reportcontrol.h" #include "mymoneyfile.h" #include "mymoneyreport.h" #include "mymoneyexception.h" #include "kmymoneysettings.h" #include "querytable.h" #include "objectinfotable.h" #include "kreportconfigurationfilterdlg.h" #include "icons/icons.h" #include "kbalancechartdlg.h" #include #include "tocitem.h" #include "tocitemgroup.h" #include "tocitemreport.h" #include "kreportchartview.h" #include "pivottable.h" #include "reporttable.h" #include "reportcontrolimpl.h" #include "mymoneyenums.h" #include "menuenums.h" using namespace reports; using namespace eMyMoney; using namespace Icons; #define VIEW_LEDGER "ledger" #define VIEW_SCHEDULE "schedule" #define VIEW_WELCOME "welcome" #define VIEW_HOME "home" #define VIEW_REPORTS "reports" /** * KReportsView Implementation */ KReportsView::KReportsView(QWidget *parent) : KMyMoneyViewBase(*new KReportsViewPrivate(this), parent) { connect(pActions[eMenu::Action::ReportAccountTransactions], &QAction::triggered, this, &KReportsView::slotReportAccountTransactions); } KReportsView::~KReportsView() { } void KReportsView::executeCustomAction(eView::Action action) { switch(action) { case eView::Action::Refresh: refresh(); break; case eView::Action::SetDefaultFocus: { Q_D(KReportsView); QTimer::singleShot(0, d->m_tocTreeWidget, SLOT(setFocus())); } break; case eView::Action::Print: slotPrintView(); break; case eView::Action::CleanupBeforeFileClose: slotCloseAll(); break; case eView::Action::ShowBalanceChart: { Q_D(KReportsView); QPointer dlg = new KBalanceChartDlg(d->m_currentAccount, this); dlg->exec(); delete dlg; } break; default: break; } } void KReportsView::refresh() { Q_D(KReportsView); if (isVisible()) { d->loadView(); d->m_needsRefresh = false; } else { d->m_needsRefresh = true; } } void KReportsView::showEvent(QShowEvent * event) { - Q_D(KReportsView); - if (d->m_needLoad) - d->init(); + if (MyMoneyFile::instance()->storageAttached()) { + Q_D(KReportsView); + if (d->m_needLoad) + d->init(); - emit customActionRequested(View::Reports, eView::Action::AboutToShow); + emit customActionRequested(View::Reports, eView::Action::AboutToShow); - if (d->m_needsRefresh) - refresh(); - - if (auto tab = dynamic_cast(d->m_reportTabWidget->currentWidget())) - emit reportSelected(tab->report()); - else - emit reportSelected(MyMoneyReport()); + if (d->m_needsRefresh) + refresh(); + if (auto tab = dynamic_cast(d->m_reportTabWidget->currentWidget())) + emit reportSelected(tab->report()); + else + emit reportSelected(MyMoneyReport()); + } // don't forget base class implementation QWidget::showEvent(event); } void KReportsView::updateActions(const MyMoneyObject& obj) { Q_D(KReportsView); if (typeid(obj) != typeid(MyMoneyAccount) && (obj.id().isEmpty() && d->m_currentAccount.id().isEmpty())) // do not disable actions that were already disabled))) return; const auto& acc = static_cast(obj); bool b; if (MyMoneyFile::instance()->isStandardAccount(acc.id())) { b = false; } else { switch (acc.accountType()) { case eMyMoney::Account::Type::Asset: case eMyMoney::Account::Type::Liability: case eMyMoney::Account::Type::Equity: case eMyMoney::Account::Type::Checkings: b = true; break; default: b = false; break; } } pActions[eMenu::Action::ReportAccountTransactions]->setEnabled(b); d->m_currentAccount = acc; } void KReportsView::slotOpenUrl(const QUrl &url) { QString view = url.fileName(); if (view.isEmpty()) return; QString command = QUrlQuery(url).queryItemValue("command"); QString id = QUrlQuery(url).queryItemValue("id"); QString tid = QUrlQuery(url).queryItemValue("tid"); if (view == VIEW_REPORTS) { if (command.isEmpty()) { // slotRefreshView(); } else if (command == QLatin1String("print")) slotPrintView(); else if (command == QLatin1String("copy")) slotCopyView(); else if (command == QLatin1String("save")) slotSaveView(); else if (command == QLatin1String("configure")) slotConfigure(); else if (command == QLatin1String("duplicate")) slotDuplicate(); else if (command == QLatin1String("close")) slotCloseCurrent(); else if (command == QLatin1String("delete")) slotDelete(); else qWarning() << i18n("Unknown command '%1' in KReportsView::slotOpenUrl()", qPrintable(command)); } else if (view == VIEW_LEDGER) { emit selectByVariant(QVariantList {QVariant(id), QVariant(tid)}, eView::Intent::ShowTransaction); } else { qWarning() << i18n("Unknown view '%1' in KReportsView::slotOpenUrl()", qPrintable(view)); } } void KReportsView::slotPrintView() { Q_D(KReportsView); if (auto tab = dynamic_cast(d->m_reportTabWidget->currentWidget())) tab->print(); } void KReportsView::slotCopyView() { Q_D(KReportsView); if (auto tab = dynamic_cast(d->m_reportTabWidget->currentWidget())) tab->copyToClipboard(); } void KReportsView::slotSaveView() { Q_D(KReportsView); if (auto tab = dynamic_cast(d->m_reportTabWidget->currentWidget())) { QString filterList = i18nc("CSV (Filefilter)", "CSV files") + QLatin1String(" (*.csv);;") + i18nc("HTML (Filefilter)", "HTML files") + QLatin1String(" (*.html)"); QUrl newURL = QFileDialog::getSaveFileUrl(this, i18n("Export as"), QUrl::fromLocalFile(KRecentDirs::dir(":kmymoney-export")), filterList, &d->m_selectedExportFilter); if (!newURL.isEmpty()) { KRecentDirs::add(":kmymoney-export", newURL.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).path()); QString newName = newURL.toDisplayString(QUrl::PreferLocalFile); try { tab->saveAs(newName, true); } catch (const MyMoneyException &e) { KMessageBox::error(this, i18n("Failed to save: %1", QString::fromLatin1(e.what()))); } } } } void KReportsView::slotConfigure() { Q_D(KReportsView); QString cm = "KReportsView::slotConfigure"; auto tab = dynamic_cast(d->m_reportTabWidget->currentWidget()); if (!tab) // nothing to do return; int tabNr = d->m_reportTabWidget->currentIndex(); tab->updateDataRange(); // range will be needed during configuration, but cannot be obtained earlier MyMoneyReport report = tab->report(); if (report.comment() == i18n("Default Report") || report.comment() == i18n("Generated Report")) { report.setComment(i18n("Custom Report")); report.setName(i18n("%1 (Customized)", report.name())); } QPointer dlg = new KReportConfigurationFilterDlg(report); if (dlg->exec()) { MyMoneyReport newreport = dlg->getConfig(); // If this report has an ID, then MODIFY it, otherwise ADD it MyMoneyFileTransaction ft; try { if (! newreport.id().isEmpty()) { MyMoneyFile::instance()->modifyReport(newreport); ft.commit(); tab->modifyReport(newreport); d->m_reportTabWidget->setTabText(tabNr, newreport.name()); d->m_reportTabWidget->setCurrentIndex(tabNr) ; } else { MyMoneyFile::instance()->addReport(newreport); ft.commit(); QString reportGroupName = newreport.group(); // find report group TocItemGroup* tocItemGroup = d->m_allTocItemGroups[reportGroupName]; if (!tocItemGroup) { QString error = i18n("Could not find reportgroup \"%1\" for report \"%2\".\nPlease report this error to the developer's list: kmymoney-devel@kde.org", reportGroupName, newreport.name()); // write to messagehandler qWarning() << cm << error; // also inform user KMessageBox::error(d->m_reportTabWidget, error, i18n("Critical Error")); // cleanup delete dlg; return; } // do not add TocItemReport to TocItemGroup here, // this is done in loadView d->addReportTab(newreport); } } catch (const MyMoneyException &e) { KMessageBox::error(this, i18n("Failed to configure report: %1", QString::fromLatin1(e.what()))); } } delete dlg; } void KReportsView::slotDuplicate() { Q_D(KReportsView); QString cm = "KReportsView::slotDuplicate"; auto tab = dynamic_cast(d->m_reportTabWidget->currentWidget()); if (!tab) { // nothing to do return; } MyMoneyReport dupe = tab->report(); dupe.setName(i18n("Copy of %1", dupe.name())); if (dupe.comment() == i18n("Default Report")) dupe.setComment(i18n("Custom Report")); dupe.clearId(); QPointer dlg = new KReportConfigurationFilterDlg(dupe); if (dlg->exec()) { MyMoneyReport newReport = dlg->getConfig(); MyMoneyFileTransaction ft; try { MyMoneyFile::instance()->addReport(newReport); ft.commit(); QString reportGroupName = newReport.group(); // find report group TocItemGroup* tocItemGroup = d->m_allTocItemGroups[reportGroupName]; if (!tocItemGroup) { QString error = i18n("Could not find reportgroup \"%1\" for report \"%2\".\nPlease report this error to the developer's list: kmymoney-devel@kde.org", reportGroupName, newReport.name()); // write to messagehandler qWarning() << cm << error; // also inform user KMessageBox::error(d->m_reportTabWidget, error, i18n("Critical Error")); // cleanup delete dlg; return; } // do not add TocItemReport to TocItemGroup here, // this is done in loadView d->addReportTab(newReport); } catch (const MyMoneyException &e) { QString error = i18n("Cannot add report, reason: \"%1\"", e.what()); // write to messagehandler qWarning() << cm << error; // also inform user KMessageBox::error(d->m_reportTabWidget, error, i18n("Critical Error")); } } delete dlg; } void KReportsView::slotDelete() { Q_D(KReportsView); auto tab = dynamic_cast(d->m_reportTabWidget->currentWidget()); if (!tab) { // nothing to do return; } MyMoneyReport report = tab->report(); if (! report.id().isEmpty()) { if (KMessageBox::Continue == d->deleteReportDialog(report.name())) { // close the tab and then remove the report so that it is not // generated again during the following loadView() call slotClose(d->m_reportTabWidget->currentIndex()); MyMoneyFileTransaction ft; MyMoneyFile::instance()->removeReport(report); ft.commit(); } } else { KMessageBox::information(this, QString("") + i18n("%1 is a default report, so it cannot be deleted.", report.name()) + QString(""), i18n("Delete Report?")); } } void KReportsView::slotOpenReport(const QString& id) { Q_D(KReportsView); if (id.isEmpty()) { // nothing to do return; } KReportTab* page = 0; // Find the tab which contains the report int index = 1; while (index < d->m_reportTabWidget->count()) { auto current = dynamic_cast(d->m_reportTabWidget->widget(index)); if (current && current->report().id() == id) { page = current; break; } ++index; } // Show the tab, or create a new one, as needed if (page) d->m_reportTabWidget->setCurrentIndex(index); else d->addReportTab(MyMoneyFile::instance()->report(id)); } void KReportsView::slotOpenReport(const MyMoneyReport& report) { Q_D(KReportsView); if (d->m_needLoad) d->init(); qDebug() << Q_FUNC_INFO << " " << report.name(); KReportTab* page = 0; // Find the tab which contains the report indicated by this list item int index = 1; while (index < d->m_reportTabWidget->count()) { auto current = dynamic_cast(d->m_reportTabWidget->widget(index)); if (current && current->report().name() == report.name()) { page = current; break; } ++index; } // Show the tab, or create a new one, as needed if (page) d->m_reportTabWidget->setCurrentIndex(index); else d->addReportTab(report); if (!isVisible()) emit switchViewRequested(View::Reports); } void KReportsView::slotItemDoubleClicked(QTreeWidgetItem* item, int) { Q_D(KReportsView); auto tocItem = dynamic_cast(item); if (tocItem && !tocItem->isReport()) { // toggle the expanded-state for reportgroup-items item->setExpanded(item->isExpanded() ? false : true); // nothing else to do for reportgroup-items return; } TocItemReport* reportTocItem = dynamic_cast(tocItem); MyMoneyReport& report = reportTocItem->getReport(); KReportTab* page = 0; // Find the tab which contains the report indicated by this list item int index = 1; while (index < d->m_reportTabWidget->count()) { auto current = dynamic_cast(d->m_reportTabWidget->widget(index)); if (current) { // If this report has an ID, we'll use the ID to match if (! report.id().isEmpty()) { if (current->report().id() == report.id()) { page = current; break; } } // Otherwise, use the name to match. THIS ASSUMES that no 2 default reports // have the same name...but that would be pretty a boneheaded thing to do. else { if (current->report().name() == report.name()) { page = current; break; } } } ++index; } // Show the tab, or create a new one, as needed if (page) d->m_reportTabWidget->setCurrentIndex(index); else d->addReportTab(report); } void KReportsView::slotToggleChart() { Q_D(KReportsView); if (auto tab = dynamic_cast(d->m_reportTabWidget->currentWidget())) tab->toggleChart(); } void KReportsView::slotCloseCurrent() { Q_D(KReportsView); slotClose(d->m_reportTabWidget->currentIndex()); } void KReportsView::slotClose(int index) { Q_D(KReportsView); if (auto tab = dynamic_cast(d->m_reportTabWidget->widget(index))) { d->m_reportTabWidget->removeTab(index); tab->setReadyToDelete(true); } } void KReportsView::slotCloseAll() { Q_D(KReportsView); if(!d->m_needLoad) { while (true) { if (auto tab = dynamic_cast(d->m_reportTabWidget->widget(1))) { d->m_reportTabWidget->removeTab(1); tab->setReadyToDelete(true); } else break; } } } void KReportsView::slotListContextMenu(const QPoint & p) { Q_D(KReportsView); - QTreeWidgetItem *item = d->m_tocTreeWidget->itemAt(p); + auto items = d->m_tocTreeWidget->selectedItems(); - if (!item) { + if (items.isEmpty()) { return; } - auto tocItem = dynamic_cast(item); + QList tocItems; + foreach(auto item, items) { + auto tocItem = dynamic_cast(item); + if (tocItem && tocItem->isReport()) { + tocItems.append(tocItem); + } + } - if (tocItem && !tocItem->isReport()) { - // currently there is no context menu for reportgroup items + if (tocItems.isEmpty()) { return; } - QMenu* contextmenu = new QMenu(this); + auto contextmenu = new QMenu(this); contextmenu->addAction(i18nc("To open a new report", "&Open"), this, SLOT(slotOpenFromList())); - contextmenu->addAction(i18nc("Configure a report", "&Configure"), - this, SLOT(slotConfigureFromList())); + contextmenu->addAction(i18nc("To print a report", "&Print"), + this, SLOT(slotPrintFromList())); + + if (tocItems.count() == 1) { + contextmenu->addAction(i18nc("Configure a report", "&Configure"), + this, SLOT(slotConfigureFromList())); + + contextmenu->addAction(i18n("&New report"), + this, SLOT(slotNewFromList())); - contextmenu->addAction(i18n("&New report"), - this, SLOT(slotNewFromList())); + // Only add this option if it's a custom report. Default reports cannot be deleted - // Only add this option if it's a custom report. Default reports cannot be deleted - auto reportTocItem = dynamic_cast(tocItem); + auto reportTocItem = dynamic_cast(tocItems.at(0)); - if (reportTocItem) { - MyMoneyReport& report = reportTocItem->getReport(); - if (! report.id().isEmpty()) { - contextmenu->addAction(i18n("&Delete"), - this, SLOT(slotDeleteFromList())); + if (reportTocItem) { + MyMoneyReport& report = reportTocItem->getReport(); + if (! report.id().isEmpty()) { + contextmenu->addAction(i18n("&Delete"), + this, SLOT(slotDeleteFromList())); + } } } contextmenu->popup(d->m_tocTreeWidget->mapToGlobal(p)); } void KReportsView::slotOpenFromList() { Q_D(KReportsView); - if (auto tocItem = dynamic_cast(d->m_tocTreeWidget->currentItem())) - slotItemDoubleClicked(tocItem, 0); + + auto items = d->m_tocTreeWidget->selectedItems(); + + if (items.isEmpty()) { + return; + } + + foreach(auto item, items) { + auto tocItem = dynamic_cast(item); + if (tocItem && tocItem->isReport()) { + slotItemDoubleClicked(tocItem, 0); + } + } +} + +void KReportsView::slotPrintFromList() +{ + Q_D(KReportsView); + + auto items = d->m_tocTreeWidget->selectedItems(); + + if (items.isEmpty()) { + return; + } + + foreach(auto item, items) { + auto tocItem = dynamic_cast(item); + if (tocItem && tocItem->isReport()) { + slotItemDoubleClicked(tocItem, 0); + slotPrintView(); + } + } } void KReportsView::slotConfigureFromList() { Q_D(KReportsView); if (auto tocItem = dynamic_cast(d->m_tocTreeWidget->currentItem())) { slotItemDoubleClicked(tocItem, 0); slotConfigure(); } } void KReportsView::slotNewFromList() { Q_D(KReportsView); if (auto tocItem = dynamic_cast(d->m_tocTreeWidget->currentItem())) { slotItemDoubleClicked(tocItem, 0); slotDuplicate(); } } void KReportsView::slotDeleteFromList() { Q_D(KReportsView); if (auto tocItem = dynamic_cast(d->m_tocTreeWidget->currentItem())) { if (auto reportTocItem = dynamic_cast(tocItem)) { MyMoneyReport& report = reportTocItem->getReport(); // If this report does not have an ID, it's a default report and cannot be deleted if (! report.id().isEmpty() && KMessageBox::Continue == d->deleteReportDialog(report.name())) { // check if report's tab is open; start from 1 because 0 is toc tab for (int i = 1; i < d->m_reportTabWidget->count(); ++i) { auto tab = dynamic_cast(d->m_reportTabWidget->widget(i)); if (tab && tab->report().id() == report.id()) { slotClose(i); // if open, close it, so no crash when switching to it break; } } MyMoneyFileTransaction ft; MyMoneyFile::instance()->removeReport(report); ft.commit(); } } } } void KReportsView::slotSelectByObject(const MyMoneyObject& obj, eView::Intent intent) { switch(intent) { case eView::Intent::UpdateActions: updateActions(obj); break; case eView::Intent::OpenObject: slotOpenReport(static_cast(obj)); default: break; } } void KReportsView::slotReportAccountTransactions() { Q_D(KReportsView); // Generate a transaction report that contains transactions for only the // currently selected account. if (!d->m_currentAccount.id().isEmpty()) { MyMoneyReport report( eMyMoney::Report::RowType::Account, eMyMoney::Report::QueryColumn::Number | eMyMoney::Report::QueryColumn::Payee | eMyMoney::Report::QueryColumn::Category, eMyMoney::TransactionFilter::Date::YearToDate, eMyMoney::Report::DetailLevel::All, i18n("%1 YTD Account Transactions", d->m_currentAccount.name()), i18n("Generated Report") ); report.setGroup(i18n("Transactions")); report.addAccount(d->m_currentAccount.id()); emit customActionRequested(View::Reports, eView::Action::SwitchView); slotOpenReport(report); } } // Make sure, that these definitions are only used within this file // this does not seem to be necessary, but when building RPMs the // build option 'final' is used and all CPP files are concatenated. // So it could well be, that in another CPP file these definitions // are also used. #undef VIEW_LEDGER #undef VIEW_SCHEDULE #undef VIEW_WELCOME #undef VIEW_HOME #undef VIEW_REPORTS diff --git a/kmymoney/plugins/views/reports/kreportsview.h b/kmymoney/plugins/views/reports/kreportsview.h index bf9578e42..cf20a8b10 100644 --- a/kmymoney/plugins/views/reports/kreportsview.h +++ b/kmymoney/plugins/views/reports/kreportsview.h @@ -1,149 +1,150 @@ /*************************************************************************** kreportsview.h - description ------------------- begin : Sat Mar 27 2004 copyright : (C) 2000-2004 by Michael Edwardes email : mte@users.sourceforge.net Javier Campos Morales Felix Rodriguez John C Thomas Baumgart Kevin Tambascio Ace Jones (C) 2017 Łukasz Wojniłowicz ***************************************************************************/ /*************************************************************************** * * * 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 KREPORTSVIEW_H #define KREPORTSVIEW_H #include // ---------------------------------------------------------------------------- // QT Includes // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes #include "kmymoneyviewbase.h" #ifdef _CHECK_MEMORY #include "mymoneyutils.h" #endif #include "mymoneyreport.h" namespace reports { class KReportChartView; } namespace reports { class ReportTable; } class QTreeWidget; class QTreeWidgetItem; class QListWidget; class MyQWebEnginePage; class TocItemGroup; class ReportControl; class ReportGroup; #ifdef ENABLE_WEBENGINE class QWebEngineView; #else class KWebView; #endif /** * Displays a page where reports can be placed. * * @author Ace Jones * * @short A view for reports. **/ class KReportsViewPrivate; class KReportsView : public KMyMoneyViewBase { Q_OBJECT public: /** * Standard constructor. * * @param parent The QWidget this is used in. * @param name The QT name. * * @return An object of type KReportsView * * @see ~KReportsView */ explicit KReportsView(QWidget *parent = nullptr); ~KReportsView() override; void executeCustomAction(eView::Action action) override; void refresh(); void updateActions(const MyMoneyObject &obj); Q_SIGNALS: /** * This signal is emitted whenever a report is selected */ void reportSelected(const MyMoneyReport&); /** * This signal is emitted whenever a transaction is selected */ void transactionSelected(const QString&, const QString&); void switchViewRequested(View view); protected: /** * Overridden so we can reload the view if necessary. * * @return Nothing. */ void showEvent(QShowEvent * event) override; public Q_SLOTS: void slotOpenUrl(const QUrl &url); void slotPrintView(); void slotCopyView(); void slotSaveView(); void slotConfigure(); void slotDuplicate(); void slotToggleChart(); void slotItemDoubleClicked(QTreeWidgetItem* item, int); void slotOpenReport(const QString&); void slotOpenReport(const MyMoneyReport&); void slotCloseCurrent(); void slotClose(int index); void slotCloseAll(); void slotDelete(); void slotListContextMenu(const QPoint &); void slotOpenFromList(); + void slotPrintFromList(); void slotConfigureFromList(); void slotNewFromList(); void slotDeleteFromList(); void slotSelectByObject(const MyMoneyObject& obj, eView::Intent intent) override; private: Q_DECLARE_PRIVATE(KReportsView) private Q_SLOTS: /** * This slot creates a transaction report for the selected account * and opens it in the reports view. */ void slotReportAccountTransactions(); }; #endif diff --git a/kmymoney/plugins/views/reports/kreportsview_p.h b/kmymoney/plugins/views/reports/kreportsview_p.h index 6da311a43..be8297475 100644 --- a/kmymoney/plugins/views/reports/kreportsview_p.h +++ b/kmymoney/plugins/views/reports/kreportsview_p.h @@ -1,1453 +1,1451 @@ /*************************************************************************** kreportsview_p.h - description ------------------- begin : Sat Mar 27 2004 copyright : (C) 2000-2004 by Michael Edwardes email : mte@users.sourceforge.net Javier Campos Morales Felix Rodriguez John C Thomas Baumgart Kevin Tambascio Ace Jones (C) 2017 Łukasz Wojniłowicz ***************************************************************************/ /*************************************************************************** * * * 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 KREPORTSVIEW_P_H #define KREPORTSVIEW_P_H #include "kreportsview.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef ENABLE_WEBENGINE #include #else #include #endif // ---------------------------------------------------------------------------- // KDE Includes #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "ui_reportcontrol.h" #include "kmymoneyviewbase_p.h" #include "kreportconfigurationfilterdlg.h" #include "mymoneyfile.h" #include "mymoneyreport.h" #include "mymoneyexception.h" #include "kmymoneysettings.h" #include "querytable.h" #include "objectinfotable.h" #include "icons/icons.h" #include #include "tocitem.h" #include "tocitemgroup.h" #include "tocitemreport.h" #include "kreportchartview.h" #include "pivottable.h" #include "reporttable.h" #include "reportcontrolimpl.h" #include "mymoneyenums.h" using namespace reports; using namespace eMyMoney; using namespace Icons; #define VIEW_LEDGER "ledger" #define VIEW_SCHEDULE "schedule" #define VIEW_WELCOME "welcome" #define VIEW_HOME "home" #define VIEW_REPORTS "reports" /** * Helper class for KReportView. * * This is the widget which displays a single report in the TabWidget that comprises this view. * * @author Ace Jones */ class KReportTab: public QWidget { private: #ifdef ENABLE_WEBENGINE QWebEngineView *m_tableView; #else KWebView *m_tableView; #endif reports::KReportChartView *m_chartView; ReportControl *m_control; QVBoxLayout *m_layout; QPrinter *m_currentPrinter; MyMoneyReport m_report; bool m_deleteMe; bool m_chartEnabled; bool m_showingChart; bool m_needReload; bool m_isChartViewValid; bool m_isTableViewValid; QPointer m_table; /** * Users character set encoding. */ QByteArray m_encoding; public: KReportTab(QTabWidget* parent, const MyMoneyReport& report, const KReportsView *eventHandler); ~KReportTab(); const MyMoneyReport& report() const { return m_report; } void print(); void toggleChart(); /** * Updates information about plotted chart in report's data */ void updateDataRange(); void copyToClipboard(); void saveAs(const QString& filename, bool includeCSS = false); void updateReport(); QString createTable(const QString& links = QString()); const ReportControl* control() const { return m_control; } bool isReadyToDelete() const { return m_deleteMe; } void setReadyToDelete(bool f) { m_deleteMe = f; } void modifyReport(const MyMoneyReport& report) { m_report = report; } void showEvent(QShowEvent * event) final override; void loadTab(); }; /** * Helper class for KReportView. * * This is a named list of reports, which will be one section * in the list of default reports * * @author Ace Jones */ class ReportGroup: public QList { private: QString m_name; ///< the title of the group in non-translated form QString m_title; ///< the title of the group in i18n-ed form public: ReportGroup() {} ReportGroup(const QString& name, const QString& title): m_name(name), m_title(title) {} const QString& name() const { return m_name; } const QString& title() const { return m_title; } }; /** * KReportTab Implementation */ KReportTab::KReportTab(QTabWidget* parent, const MyMoneyReport& report, const KReportsView* eventHandler): QWidget(parent), #ifdef ENABLE_WEBENGINE m_tableView(new QWebEngineView(this)), #else m_tableView(new KWebView(this)), #endif m_chartView(new KReportChartView(this)), m_control(new ReportControl(this)), m_layout(new QVBoxLayout(this)), m_currentPrinter(nullptr), m_report(report), m_deleteMe(false), m_chartEnabled(false), m_showingChart(report.isChartByDefault()), m_needReload(true), m_isChartViewValid(false), m_isTableViewValid(false), m_table(0) { m_layout->setSpacing(6); m_tableView->setPage(new MyQWebEnginePage(m_tableView)); m_tableView->setZoomFactor(KMyMoneySettings::zoomFactor()); //set button icons m_control->ui->buttonChart->setIcon(Icons::get(Icon::OfficeChartLine)); m_control->ui->buttonClose->setIcon(Icons::get(Icon::DocumentClose)); m_control->ui->buttonConfigure->setIcon(Icons::get(Icon::Configure)); m_control->ui->buttonCopy->setIcon(Icons::get(Icon::EditCopy)); m_control->ui->buttonDelete->setIcon(Icons::get(Icon::EditDelete)); m_control->ui->buttonExport->setIcon(Icons::get(Icon::DocumentExport)); m_control->ui->buttonNew->setIcon(Icons::get(Icon::DocumentNew)); m_chartView->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); m_chartView->hide(); m_tableView->hide(); m_layout->addWidget(m_control); m_layout->addWidget(m_tableView); m_layout->addWidget(m_chartView); m_layout->setStretch(1, 10); m_layout->setStretch(2, 10); connect(m_control->ui->buttonChart, &QAbstractButton::clicked, eventHandler, &KReportsView::slotToggleChart); connect(m_control->ui->buttonConfigure, &QAbstractButton::clicked, eventHandler, &KReportsView::slotConfigure); connect(m_control->ui->buttonNew, &QAbstractButton::clicked, eventHandler, &KReportsView::slotDuplicate); connect(m_control->ui->buttonCopy, &QAbstractButton::clicked, eventHandler, &KReportsView::slotCopyView); connect(m_control->ui->buttonExport, &QAbstractButton::clicked, eventHandler, &KReportsView::slotSaveView); connect(m_control->ui->buttonDelete, &QAbstractButton::clicked, eventHandler, &KReportsView::slotDelete); connect(m_control->ui->buttonClose, &QAbstractButton::clicked, eventHandler, &KReportsView::slotCloseCurrent); #ifdef ENABLE_WEBENGINE connect(m_tableView->page(), &QWebEnginePage::urlChanged, eventHandler, &KReportsView::slotOpenUrl); #else m_tableView->page()->setLinkDelegationPolicy(QWebPage::DelegateAllLinks); connect(m_tableView->page(), &KWebPage::linkClicked, eventHandler, &KReportsView::slotOpenUrl); #endif // if this is a default report, then you can't delete it! if (report.id().isEmpty()) m_control->ui->buttonDelete->setEnabled(false); int tabNr = parent->addTab(this, Icons::get(Icon::Spreadsheet), report.name()); parent->setTabEnabled(tabNr, true); parent->setCurrentIndex(tabNr); // get users character set encoding m_encoding = QTextCodec::codecForLocale()->name(); } KReportTab::~KReportTab() { delete m_table; } void KReportTab::print() { if (m_tableView) { m_currentPrinter = new QPrinter(); QPointer dialog = new QPrintDialog(m_currentPrinter, this); dialog->setWindowTitle(QString()); if (dialog->exec() != QDialog::Accepted) { delete m_currentPrinter; m_currentPrinter = nullptr; return; } #ifdef ENABLE_WEBENGINE m_tableView->page()->print(m_currentPrinter, [=] (bool) {delete m_currentPrinter; m_currentPrinter = nullptr;}); #else m_tableView->print(m_currentPrinter); #endif } } void KReportTab::copyToClipboard() { QMimeData* pMimeData = new QMimeData(); pMimeData->setHtml(m_table->renderReport(QLatin1String("html"), m_encoding, m_report.name(), true)); QApplication::clipboard()->setMimeData(pMimeData); } void KReportTab::saveAs(const QString& filename, bool includeCSS) { QFile file(filename); if (file.open(QIODevice::WriteOnly)) { if (QFileInfo(filename).suffix().toLower() == QLatin1String("csv")) { QTextStream(&file) << m_table->renderReport(QLatin1String("csv"), m_encoding, QString()); } else { QString table = m_table->renderReport(QLatin1String("html"), m_encoding, m_report.name(), includeCSS); QTextStream stream(&file); stream << table; } file.close(); } } void KReportTab::loadTab() { m_needReload = true; if (isVisible()) { m_needReload = false; updateReport(); } } void KReportTab::showEvent(QShowEvent * event) { if (m_needReload) { m_needReload = false; updateReport(); } QWidget::showEvent(event); } void KReportTab::updateReport() { m_isChartViewValid = false; m_isTableViewValid = false; // reload the report from the engine. It might have // been changed by the user try { // Don't try to reload default reports from the engine if (!m_report.id().isEmpty()) m_report = MyMoneyFile::instance()->report(m_report.id()); } catch (const MyMoneyException &) { } delete m_table; m_table = 0; if (m_report.reportType() == eMyMoney::Report::ReportType::PivotTable) { m_table = new PivotTable(m_report); m_chartEnabled = true; } else if (m_report.reportType() == eMyMoney::Report::ReportType::QueryTable) { m_table = new QueryTable(m_report); m_chartEnabled = false; } else if (m_report.reportType() == eMyMoney::Report::ReportType::InfoTable) { m_table = new ObjectInfoTable(m_report); m_chartEnabled = false; } m_control->ui->buttonChart->setEnabled(m_chartEnabled); m_showingChart = !m_showingChart; toggleChart(); } void KReportTab::toggleChart() { // for now it will just SHOW the chart. In the future it actually has to toggle it. if (m_showingChart) { if (!m_isTableViewValid) { m_tableView->setHtml(m_table->renderReport(QLatin1String("html"), m_encoding, m_report.name()), QUrl("file://")); // workaround for access permission to css file } m_isTableViewValid = true; m_tableView->show(); m_chartView->hide(); m_control->ui->buttonChart->setText(i18n("Chart")); m_control->ui->buttonChart->setToolTip(i18n("Show the chart version of this report")); m_control->ui->buttonChart->setIcon(Icons::get(Icon::OfficeChartLine)); } else { if (!m_isChartViewValid) m_table->drawChart(*m_chartView); m_isChartViewValid = true; m_tableView->hide(); m_chartView->show(); m_control->ui->buttonChart->setText(i18n("Report")); m_control->ui->buttonChart->setToolTip(i18n("Show the report version of this chart")); m_control->ui->buttonChart->setIcon(Icons::get(Icon::ViewFinancialList)); } m_showingChart = ! m_showingChart; } void KReportTab::updateDataRange() { QList grids = m_chartView->coordinatePlane()->gridDimensionsList(); // get dimensions of plotted graph if (grids.isEmpty()) return; QChar separator = locale().groupSeparator(); QChar decimalPoint = locale().decimalPoint(); int precision = m_report.yLabelsPrecision(); QList> dims; // create list of dimension values in string and qreal // get qreal values dims.append(qMakePair(QString(), grids.at(1).start)); dims.append(qMakePair(QString(), grids.at(1).end)); dims.append(qMakePair(QString(), grids.at(1).stepWidth)); dims.append(qMakePair(QString(), grids.at(1).subStepWidth)); // convert qreal values to string variables for (int i = 0; i < 4; ++i) { if (i > 2) ++precision; if (precision == 0) dims[i].first = locale().toString(qRound(dims.at(i).second)); else dims[i].first = locale().toString(dims.at(i).second, 'f', precision).remove(separator).remove(QRegularExpression("0+$")).remove(QRegularExpression("\\" + decimalPoint + "$")); } // save string variables in report's data m_report.setDataRangeStart(dims.at(0).first); m_report.setDataRangeEnd(dims.at(1).first); m_report.setDataMajorTick(dims.at(2).first); m_report.setDataMinorTick(dims.at(3).first); } class KReportsViewPrivate : public KMyMoneyViewBasePrivate { Q_DECLARE_PUBLIC(KReportsView) public: explicit KReportsViewPrivate(KReportsView *qq): q_ptr(qq), m_needLoad(true), m_reportListView(nullptr), m_reportTabWidget(nullptr), m_listTab(nullptr), m_listTabLayout(nullptr), m_tocTreeWidget(nullptr), m_columnsAlreadyAdjusted(false) { } ~KReportsViewPrivate() { } void init() { Q_Q(KReportsView); m_needLoad = false; auto vbox = new QVBoxLayout(q); q->setLayout(vbox); vbox->setSpacing(6); vbox->setMargin(0); // build reports toc setColumnsAlreadyAdjusted(false); m_reportTabWidget = new QTabWidget(q); vbox->addWidget(m_reportTabWidget); m_reportTabWidget->setTabsClosable(true); m_listTab = new QWidget(m_reportTabWidget); m_listTabLayout = new QVBoxLayout(m_listTab); m_listTabLayout->setSpacing(6); m_tocTreeWidget = new QTreeWidget(m_listTab); // report-group items have only 1 column (name of group), // report items have 2 columns (report name and comment) m_tocTreeWidget->setColumnCount(2); // headers QStringList headers; headers << i18n("Reports") << i18n("Comment"); m_tocTreeWidget->setHeaderLabels(headers); m_tocTreeWidget->setAlternatingRowColors(true); m_tocTreeWidget->setSortingEnabled(true); m_tocTreeWidget->sortByColumn(0, Qt::AscendingOrder); // for report group items: // doubleclick toggles the expand-state, - // so avoid any further action in case of doubleclick - // (see slotItemDoubleClicked) m_tocTreeWidget->setExpandsOnDoubleClick(false); m_tocTreeWidget->setContextMenuPolicy(Qt::CustomContextMenu); - m_tocTreeWidget->setSelectionMode(QAbstractItemView::SingleSelection); + m_tocTreeWidget->setSelectionMode(QAbstractItemView::ExtendedSelection); m_listTabLayout->addWidget(m_tocTreeWidget); m_reportTabWidget->addTab(m_listTab, i18n("Reports")); q->connect(m_reportTabWidget, &QTabWidget::tabCloseRequested, q, &KReportsView::slotClose); - q->connect(m_tocTreeWidget, &QTreeWidget::itemActivated, + q->connect(m_tocTreeWidget, &QTreeWidget::itemDoubleClicked, q, &KReportsView::slotItemDoubleClicked); q->connect(m_tocTreeWidget, &QWidget::customContextMenuRequested, q, &KReportsView::slotListContextMenu); q->connect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, q, &KReportsView::refresh); } void restoreTocExpandState(QMap& expandStates) { for (auto i = 0; i < m_tocTreeWidget->topLevelItemCount(); ++i) { QTreeWidgetItem* item = m_tocTreeWidget->topLevelItem(i); if (item) { QString itemLabel = item->text(0); if (expandStates.contains(itemLabel)) { item->setExpanded(expandStates[itemLabel]); } else { item->setExpanded(false); } } } } /** * Display a dialog to confirm report deletion */ int deleteReportDialog(const QString &reportName) { Q_Q(KReportsView); return KMessageBox::warningContinueCancel(q, i18n("Are you sure you want to delete report %1? There is no way to recover it.", reportName), i18n("Delete Report?")); } void addReportTab(const MyMoneyReport& report) { Q_Q(KReportsView); new KReportTab(m_reportTabWidget, report, q); } void loadView() { // remember the id of the current selected item QTreeWidgetItem* item = m_tocTreeWidget->currentItem(); QString selectedItem = (item) ? item->text(0) : QString(); // save expand states of all top-level items QMap expandStates; for (int i = 0; i < m_tocTreeWidget->topLevelItemCount(); ++i) { item = m_tocTreeWidget->topLevelItem(i); if (item) { QString itemLabel = item->text(0); if (item->isExpanded()) { expandStates.insert(itemLabel, true); } else { expandStates.insert(itemLabel, false); } } } // find the item visible on top QTreeWidgetItem* visibleTopItem = m_tocTreeWidget->itemAt(0, 0); // text of column 0 identifies the item visible on top QString visibleTopItemText; bool visibleTopItemFound = true; if (visibleTopItem == NULL) { visibleTopItemFound = false; } else { // this assumes, that all item-texts in column 0 are unique, // no matter, whether the item is a report- or a group-item visibleTopItemText = visibleTopItem->text(0); } // turn off updates to avoid flickering during reload //m_reportListView->setUpdatesEnabled(false); // // Rebuild the list page // m_tocTreeWidget->clear(); // Default Reports QList defaultreports; defaultReports(defaultreports); QList::const_iterator it_group = defaultreports.constBegin(); // the item to be set as current item QTreeWidgetItem* currentItem = 0L; // group number, this will be used as sort key for reportgroup items // we have: // 1st some default groups // 2nd a chart group // 3rd maybe a favorite group // 4th maybe an orphan group (for old reports) int defaultGroupNo = 1; int chartGroupNo = defaultreports.size() + 1; // group for diagrams QString groupName = I18N_NOOP("Charts"); TocItemGroup* chartTocItemGroup = new TocItemGroup(m_tocTreeWidget, chartGroupNo, i18n(groupName.toLatin1().data())); m_allTocItemGroups.insert(groupName, chartTocItemGroup); while (it_group != defaultreports.constEnd()) { groupName = (*it_group).name(); TocItemGroup* defaultTocItemGroup = new TocItemGroup(m_tocTreeWidget, defaultGroupNo++, i18n(groupName.toLatin1().data())); m_allTocItemGroups.insert(groupName, defaultTocItemGroup); if (groupName == selectedItem) { currentItem = defaultTocItemGroup; } QList::const_iterator it_report = (*it_group).begin(); while (it_report != (*it_group).end()) { MyMoneyReport report = *it_report; report.setGroup(groupName); TocItemReport* reportTocItemReport = new TocItemReport(defaultTocItemGroup, report); if (report.name() == selectedItem) { currentItem = reportTocItemReport; } // ALSO place it into the Charts list if it's displayed as a chart by default if (report.isChartByDefault()) { new TocItemReport(chartTocItemGroup, report); } ++it_report; } ++it_group; } // group for custom (favorite) reports int favoriteGroupNo = chartGroupNo + 1; groupName = I18N_NOOP("Favorite Reports"); TocItemGroup* favoriteTocItemGroup = new TocItemGroup(m_tocTreeWidget, favoriteGroupNo, i18n(groupName.toLatin1().data())); m_allTocItemGroups.insert(groupName, favoriteTocItemGroup); TocItemGroup* orphanTocItemGroup = 0; QList customreports = MyMoneyFile::instance()->reportList(); QList::const_iterator it_report = customreports.constBegin(); while (it_report != customreports.constEnd()) { MyMoneyReport report = *it_report; groupName = (*it_report).group(); // If this report is in a known group, place it there // KReportGroupListItem* groupnode = groupitems[(*it_report).group()]; TocItemGroup* groupNode = m_allTocItemGroups[groupName]; if (groupNode) { new TocItemReport(groupNode, report); } else { // otherwise, place it in the orphanage if (!orphanTocItemGroup) { // group for orphaned reports int orphanGroupNo = favoriteGroupNo + 1; groupName = I18N_NOOP("Old Customized Reports"); orphanTocItemGroup = new TocItemGroup(m_tocTreeWidget, orphanGroupNo, i18n(groupName.toLatin1().data())); m_allTocItemGroups.insert(groupName, orphanTocItemGroup); } new TocItemReport(orphanTocItemGroup, report); } // ALSO place it into the Favorites list if it's a favorite if ((*it_report).isFavorite()) { new TocItemReport(favoriteTocItemGroup, report); } // ALSO place it into the Charts list if it's displayed as a chart by default if ((*it_report).isChartByDefault()) { new TocItemReport(chartTocItemGroup, report); } ++it_report; } // // Go through the tabs to set their update flag or delete them if needed // int index = 1; while (index < m_reportTabWidget->count()) { // TODO: Find some way of detecting the file is closed and kill these tabs!! if (auto tab = dynamic_cast(m_reportTabWidget->widget(index))) { if (tab->isReadyToDelete() /* || ! reports.count() */) { delete tab; --index; } else { tab->loadTab(); } } ++index; } if (visibleTopItemFound) { // try to find the visibleTopItem that we had at the start of this method // intentionally not using 'Qt::MatchCaseSensitive' here // to avoid 'item not found' if someone corrected a typo only QList visibleTopItemList = m_tocTreeWidget->findItems(visibleTopItemText, Qt::MatchFixedString | Qt::MatchRecursive); if (visibleTopItemList.isEmpty()) { // the item could not be found, it was deleted or renamed visibleTopItemFound = false; } else { visibleTopItem = visibleTopItemList.at(0); if (visibleTopItem == NULL) { visibleTopItemFound = false; } } } // adjust column widths, // but only the first time when the view is loaded, // maybe the user sets other column widths later, // so don't disturb him if (columnsAlreadyAdjusted()) { // restore expand states of all top-level items restoreTocExpandState(expandStates); // restore current item m_tocTreeWidget->setCurrentItem(currentItem); // try to scroll to the item visible on top // when this method started if (visibleTopItemFound) { m_tocTreeWidget->scrollToItem(visibleTopItem); } else { m_tocTreeWidget->scrollToTop(); } return; } // avoid flickering m_tocTreeWidget->setUpdatesEnabled(false); // expand all top-level items m_tocTreeWidget->expandAll(); // resize columns m_tocTreeWidget->resizeColumnToContents(0); m_tocTreeWidget->resizeColumnToContents(1); // restore expand states of all top-level items restoreTocExpandState(expandStates); // restore current item m_tocTreeWidget->setCurrentItem(currentItem); // try to scroll to the item visible on top // when this method started if (visibleTopItemFound) { m_tocTreeWidget->scrollToItem(visibleTopItem); } else { m_tocTreeWidget->scrollToTop(); } setColumnsAlreadyAdjusted(true); m_tocTreeWidget->setUpdatesEnabled(true); } void defaultReports(QList& groups) { { ReportGroup list("Income and Expenses", i18n("Income and Expenses")); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::ExpenseIncome, static_cast(eMyMoney::Report::ColumnType::Months), TransactionFilter::Date::CurrentMonth, eMyMoney::Report::DetailLevel::All, i18n("Income and Expenses This Month"), i18n("Default Report") )); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::ExpenseIncome, static_cast(eMyMoney::Report::ColumnType::Months), TransactionFilter::Date::YearToDate, eMyMoney::Report::DetailLevel::All, i18n("Income and Expenses This Year"), i18n("Default Report") )); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::ExpenseIncome, static_cast(eMyMoney::Report::ColumnType::Years), TransactionFilter::Date::All, eMyMoney::Report::DetailLevel::All, i18n("Income and Expenses By Year"), i18n("Default Report") )); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::ExpenseIncome, static_cast(eMyMoney::Report::ColumnType::Months), TransactionFilter::Date::Last12Months, eMyMoney::Report::DetailLevel::Top, i18n("Income and Expenses Graph"), i18n("Default Report") )); list.back().setChartByDefault(true); list.back().setChartType(eMyMoney::Report::ChartType::Line); list.back().setChartDataLabels(false); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::ExpenseIncome, static_cast(eMyMoney::Report::ColumnType::Months), TransactionFilter::Date::YearToDate, eMyMoney::Report::DetailLevel::Group, i18n("Income and Expenses Pie Chart"), i18n("Default Report") )); list.back().setChartByDefault(true); list.back().setChartType(eMyMoney::Report::ChartType::Pie); list.back().setShowingRowTotals(false); groups.push_back(list); } { ReportGroup list("Net Worth", i18n("Net Worth")); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::AssetLiability, static_cast(eMyMoney::Report::ColumnType::Months), TransactionFilter::Date::YearToDate, eMyMoney::Report::DetailLevel::Top, i18n("Net Worth By Month"), i18n("Default Report") )); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::AssetLiability, static_cast(eMyMoney::Report::ColumnType::Months), TransactionFilter::Date::Today, eMyMoney::Report::DetailLevel::Top, i18n("Net Worth Today"), i18n("Default Report") )); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::AssetLiability, static_cast(eMyMoney::Report::ColumnType::Years), TransactionFilter::Date::All, eMyMoney::Report::DetailLevel::Top, i18n("Net Worth By Year"), i18n("Default Report") )); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::AssetLiability, static_cast(eMyMoney::Report::ColumnType::Months), TransactionFilter::Date::Next7Days, eMyMoney::Report::DetailLevel::Top, i18n("7-day Cash Flow Forecast"), i18n("Default Report") )); list.back().setIncludingSchedules(true); list.back().setColumnsAreDays(true); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::AssetLiability, static_cast(eMyMoney::Report::ColumnType::Months), TransactionFilter::Date::Last12Months, eMyMoney::Report::DetailLevel::Total, i18n("Net Worth Graph"), i18n("Default Report") )); list.back().setChartByDefault(true); list.back().setChartCHGridLines(false); list.back().setChartSVGridLines(false); list.back().setChartType(eMyMoney::Report::ChartType::Line); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::Institution, eMyMoney::Report::QueryColumn::None, TransactionFilter::Date::YearToDate, eMyMoney::Report::DetailLevel::Top, i18n("Account Balances by Institution"), i18n("Default Report") )); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::AccountType, eMyMoney::Report::QueryColumn::None, TransactionFilter::Date::YearToDate, eMyMoney::Report::DetailLevel::Top, i18n("Account Balances by Type"), i18n("Default Report") )); groups.push_back(list); } { ReportGroup list("Transactions", i18n("Transactions")); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::Account, eMyMoney::Report::QueryColumn::Number | eMyMoney::Report::QueryColumn::Payee | eMyMoney::Report::QueryColumn::Category | eMyMoney::Report::QueryColumn::Tag | eMyMoney::Report::QueryColumn::Balance, TransactionFilter::Date::YearToDate, eMyMoney::Report::DetailLevel::All, i18n("Transactions by Account"), i18n("Default Report") )); //list.back().setConvertCurrency(false); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::Category, eMyMoney::Report::QueryColumn::Number | eMyMoney::Report::QueryColumn::Payee | eMyMoney::Report::QueryColumn::Account | eMyMoney::Report::QueryColumn::Tag, TransactionFilter::Date::YearToDate, eMyMoney::Report::DetailLevel::All, i18n("Transactions by Category"), i18n("Default Report") )); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::Payee, eMyMoney::Report::QueryColumn::Number | eMyMoney::Report::QueryColumn::Category | eMyMoney::Report::QueryColumn::Tag, TransactionFilter::Date::YearToDate, eMyMoney::Report::DetailLevel::All, i18n("Transactions by Payee"), i18n("Default Report") )); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::Tag, eMyMoney::Report::QueryColumn::Number | eMyMoney::Report::QueryColumn::Category, TransactionFilter::Date::YearToDate, eMyMoney::Report::DetailLevel::All, i18n("Transactions by Tag"), i18n("Default Report") )); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::Month, eMyMoney::Report::QueryColumn::Number | eMyMoney::Report::QueryColumn::Payee | eMyMoney::Report::QueryColumn::Category | eMyMoney::Report::QueryColumn::Tag, TransactionFilter::Date::YearToDate, eMyMoney::Report::DetailLevel::All, i18n("Transactions by Month"), i18n("Default Report") )); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::Week, eMyMoney::Report::QueryColumn::Number | eMyMoney::Report::QueryColumn::Payee | eMyMoney::Report::QueryColumn::Category | eMyMoney::Report::QueryColumn::Tag, TransactionFilter::Date::YearToDate, eMyMoney::Report::DetailLevel::All, i18n("Transactions by Week"), i18n("Default Report") )); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::Account, eMyMoney::Report::QueryColumn::Loan, TransactionFilter::Date::All, eMyMoney::Report::DetailLevel::All, i18n("Loan Transactions"), i18n("Default Report") )); list.back().setLoansOnly(true); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::AccountReconcile, eMyMoney::Report::QueryColumn::Number | eMyMoney::Report::QueryColumn::Payee | eMyMoney::Report::QueryColumn::Category | eMyMoney::Report::QueryColumn::Balance, TransactionFilter::Date::Last3Months, eMyMoney::Report::DetailLevel::All, i18n("Transactions by Reconciliation Status"), i18n("Default Report") )); groups.push_back(list); } { ReportGroup list("CashFlow", i18n("Cash Flow")); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::CashFlow, eMyMoney::Report::QueryColumn::Number | eMyMoney::Report::QueryColumn::Payee | eMyMoney::Report::QueryColumn::Account, TransactionFilter::Date::YearToDate, eMyMoney::Report::DetailLevel::All, i18n("Cash Flow Transactions This Month"), i18n("Default Report") )); groups.push_back(list); } { ReportGroup list("Investments", i18n("Investments")); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::TopAccount, eMyMoney::Report::QueryColumn::Action | eMyMoney::Report::QueryColumn::Shares | eMyMoney::Report::QueryColumn::Price, TransactionFilter::Date::YearToDate, eMyMoney::Report::DetailLevel::All, i18n("Investment Transactions"), i18n("Default Report") )); list.back().setInvestmentsOnly(true); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::AccountByTopAccount, eMyMoney::Report::QueryColumn::Shares | eMyMoney::Report::QueryColumn::Price, TransactionFilter::Date::YearToDate, eMyMoney::Report::DetailLevel::All, i18n("Investment Holdings by Account"), i18n("Default Report") )); list.back().setInvestmentsOnly(true); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::EquityType, eMyMoney::Report::QueryColumn::Shares | eMyMoney::Report::QueryColumn::Price, TransactionFilter::Date::YearToDate, eMyMoney::Report::DetailLevel::All, i18n("Investment Holdings by Type"), i18n("Default Report") )); list.back().setInvestmentsOnly(true); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::AccountByTopAccount, eMyMoney::Report::QueryColumn::Performance, TransactionFilter::Date::YearToDate, eMyMoney::Report::DetailLevel::All, i18n("Investment Performance by Account"), i18n("Default Report") )); list.back().setInvestmentsOnly(true); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::EquityType, eMyMoney::Report::QueryColumn::Performance, TransactionFilter::Date::YearToDate, eMyMoney::Report::DetailLevel::All, i18n("Investment Performance by Type"), i18n("Default Report") )); list.back().setInvestmentsOnly(true); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::AccountByTopAccount, eMyMoney::Report::QueryColumn::CapitalGain, TransactionFilter::Date::YearToDate, eMyMoney::Report::DetailLevel::All, i18n("Investment Capital Gains by Account"), i18n("Default Report") )); list.back().setInvestmentsOnly(true); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::EquityType, eMyMoney::Report::QueryColumn::CapitalGain, TransactionFilter::Date::YearToDate, eMyMoney::Report::DetailLevel::All, i18n("Investment Capital Gains by Type"), i18n("Default Report") )); list.back().setInvestmentsOnly(true); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::AssetLiability, static_cast(eMyMoney::Report::ColumnType::Months), TransactionFilter::Date::Today, eMyMoney::Report::DetailLevel::All, i18n("Investment Holdings Pie"), i18n("Default Report") )); list.back().setChartByDefault(true); list.back().setChartCHGridLines(false); list.back().setChartSVGridLines(false); list.back().setChartType(eMyMoney::Report::ChartType::Pie); list.back().setInvestmentsOnly(true); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::AssetLiability, static_cast(eMyMoney::Report::ColumnType::Months), TransactionFilter::Date::Last12Months, eMyMoney::Report::DetailLevel::All, i18n("Investment Worth Graph"), i18n("Default Report") )); list.back().setChartByDefault(true); list.back().setChartCHGridLines(false); list.back().setChartSVGridLines(false); list.back().setChartType(eMyMoney::Report::ChartType::Line); list.back().setColumnsAreDays(true); list.back().setInvestmentsOnly(true); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::AssetLiability, static_cast(eMyMoney::Report::ColumnType::Months), TransactionFilter::Date::Last12Months, eMyMoney::Report::DetailLevel::All, i18n("Investment Price Graph"), i18n("Default Report") )); list.back().setChartByDefault(true); list.back().setChartCHGridLines(false); list.back().setChartSVGridLines(false); list.back().setChartType(eMyMoney::Report::ChartType::Line); list.back().setColumnsAreDays(true); list.back().setInvestmentsOnly(true); list.back().setIncludingBudgetActuals(false); list.back().setIncludingPrice(true); list.back().setConvertCurrency(true); list.back().setChartDataLabels(false); list.back().setSkipZero(true); list.back().setShowingColumnTotals(false); list.back().setShowingRowTotals(false); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::AssetLiability, static_cast(eMyMoney::Report::ColumnType::Months), TransactionFilter::Date::Last12Months, eMyMoney::Report::DetailLevel::All, i18n("Investment Moving Average Price Graph"), i18n("Default Report") )); list.back().setChartByDefault(true); list.back().setChartCHGridLines(false); list.back().setChartSVGridLines(false); list.back().setChartType(eMyMoney::Report::ChartType::Line); list.back().setColumnsAreDays(true); list.back().setInvestmentsOnly(true); list.back().setIncludingBudgetActuals(false); list.back().setIncludingAveragePrice(true); list.back().setMovingAverageDays(10); list.back().setConvertCurrency(true); list.back().setChartDataLabels(false); list.back().setShowingColumnTotals(false); list.back().setShowingRowTotals(false); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::AssetLiability, static_cast(eMyMoney::Report::ColumnType::Months), TransactionFilter::Date::Last30Days, eMyMoney::Report::DetailLevel::All, i18n("Investment Moving Average"), i18n("Default Report") )); list.back().setChartCHGridLines(false); list.back().setChartSVGridLines(false); list.back().setChartType(eMyMoney::Report::ChartType::Line); list.back().setColumnsAreDays(true); list.back().setInvestmentsOnly(true); list.back().setIncludingBudgetActuals(false); list.back().setIncludingMovingAverage(true); list.back().setMovingAverageDays(10); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::AssetLiability, static_cast(eMyMoney::Report::ColumnType::Months), TransactionFilter::Date::Last30Days, eMyMoney::Report::DetailLevel::All, i18n("Investment Moving Average vs Actual"), i18n("Default Report") )); list.back().setChartByDefault(true); list.back().setChartCHGridLines(false); list.back().setChartSVGridLines(false); list.back().setChartType(eMyMoney::Report::ChartType::Line); list.back().setColumnsAreDays(true); list.back().setInvestmentsOnly(true); list.back().setIncludingBudgetActuals(true); list.back().setIncludingMovingAverage(true); list.back().setMovingAverageDays(10); groups.push_back(list); } { ReportGroup list("Taxes", i18n("Taxes")); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::Category, eMyMoney::Report::QueryColumn::Number | eMyMoney::Report::QueryColumn::Payee | eMyMoney::Report::QueryColumn::Account, TransactionFilter::Date::YearToDate, eMyMoney::Report::DetailLevel::All, i18n("Tax Transactions by Category"), i18n("Default Report") )); list.back().setTax(true); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::Payee, eMyMoney::Report::QueryColumn::Number | eMyMoney::Report::QueryColumn::Category | eMyMoney::Report::QueryColumn::Account, TransactionFilter::Date::YearToDate, eMyMoney::Report::DetailLevel::All, i18n("Tax Transactions by Payee"), i18n("Default Report") )); list.back().setTax(true); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::Category, eMyMoney::Report::QueryColumn::Number | eMyMoney::Report::QueryColumn::Payee | eMyMoney::Report::QueryColumn::Account, TransactionFilter::Date::LastFiscalYear, eMyMoney::Report::DetailLevel::All, i18n("Tax Transactions by Category Last Fiscal Year"), i18n("Default Report") )); list.back().setTax(true); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::Payee, eMyMoney::Report::QueryColumn::Number | eMyMoney::Report::QueryColumn::Category | eMyMoney::Report::QueryColumn::Account, TransactionFilter::Date::LastFiscalYear, eMyMoney::Report::DetailLevel::All, i18n("Tax Transactions by Payee Last Fiscal Year"), i18n("Default Report") )); list.back().setTax(true); groups.push_back(list); } { ReportGroup list("Budgeting", i18n("Budgeting")); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::BudgetActual, static_cast(eMyMoney::Report::ColumnType::Months), TransactionFilter::Date::YearToDate, eMyMoney::Report::DetailLevel::All, i18n("Budgeted vs. Actual This Year"), i18n("Default Report") )); list.back().setShowingRowTotals(true); list.back().setBudget("Any", true); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::BudgetActual, static_cast(eMyMoney::Report::ColumnType::Months), TransactionFilter::Date::YearToMonth, eMyMoney::Report::DetailLevel::All, i18n("Budgeted vs. Actual This Year (YTM)"), i18n("Default Report") )); list.back().setShowingRowTotals(true); list.back().setBudget("Any", true); // in case we're in January, we show the last year if (QDate::currentDate().month() == 1) { list.back().setDateFilter(TransactionFilter::Date::LastYear); } list.push_back(MyMoneyReport( eMyMoney::Report::RowType::BudgetActual, static_cast(eMyMoney::Report::ColumnType::Months), TransactionFilter::Date::CurrentMonth, eMyMoney::Report::DetailLevel::All, i18n("Monthly Budgeted vs. Actual"), i18n("Default Report") )); list.back().setBudget("Any", true); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::BudgetActual, static_cast(eMyMoney::Report::ColumnType::Months), TransactionFilter::Date::CurrentYear, eMyMoney::Report::DetailLevel::All, i18n("Yearly Budgeted vs. Actual"), i18n("Default Report") )); list.back().setBudget("Any", true); list.back().setShowingRowTotals(true); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::Budget, static_cast(eMyMoney::Report::ColumnType::Months), TransactionFilter::Date::CurrentMonth, eMyMoney::Report::DetailLevel::All, i18n("Monthly Budget"), i18n("Default Report") )); list.back().setBudget("Any", false); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::Budget, static_cast(eMyMoney::Report::ColumnType::Months), TransactionFilter::Date::CurrentYear, eMyMoney::Report::DetailLevel::All, i18n("Yearly Budget"), i18n("Default Report") )); list.back().setBudget("Any", false); list.back().setShowingRowTotals(true); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::BudgetActual, static_cast(eMyMoney::Report::ColumnType::Months), TransactionFilter::Date::CurrentYear, eMyMoney::Report::DetailLevel::Group, i18n("Yearly Budgeted vs Actual Graph"), i18n("Default Report") )); list.back().setChartByDefault(true); list.back().setChartCHGridLines(false); list.back().setChartSVGridLines(false); list.back().setBudget("Any", true); list.back().setChartType(eMyMoney::Report::ChartType::Line); groups.push_back(list); } { ReportGroup list("Forecast", i18n("Forecast")); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::AssetLiability, static_cast(eMyMoney::Report::ColumnType::Months), TransactionFilter::Date::Next12Months, eMyMoney::Report::DetailLevel::Top, i18n("Forecast By Month"), i18n("Default Report") )); list.back().setIncludingForecast(true); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::AssetLiability, static_cast(eMyMoney::Report::ColumnType::Months), TransactionFilter::Date::NextQuarter, eMyMoney::Report::DetailLevel::Top, i18n("Forecast Next Quarter"), i18n("Default Report") )); list.back().setColumnsAreDays(true); list.back().setIncludingForecast(true); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::ExpenseIncome, static_cast(eMyMoney::Report::ColumnType::Months), TransactionFilter::Date::CurrentYear, eMyMoney::Report::DetailLevel::Top, i18n("Income and Expenses Forecast This Year"), i18n("Default Report") )); list.back().setIncludingForecast(true); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::AssetLiability, static_cast(eMyMoney::Report::ColumnType::Months), TransactionFilter::Date::Next3Months, eMyMoney::Report::DetailLevel::Total, i18n("Net Worth Forecast Graph"), i18n("Default Report") )); list.back().setColumnsAreDays(true); list.back().setIncludingForecast(true); list.back().setChartByDefault(true); list.back().setChartCHGridLines(false); list.back().setChartSVGridLines(false); list.back().setChartType(eMyMoney::Report::ChartType::Line); groups.push_back(list); } { ReportGroup list("Information", i18n("General Information")); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::Schedule, static_cast(eMyMoney::Report::ColumnType::Months), TransactionFilter::Date::Next12Months, eMyMoney::Report::DetailLevel::All, i18n("Schedule Information"), i18n("Default Report") )); list.back().setDetailLevel(eMyMoney::Report::DetailLevel::All); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::Schedule, static_cast(eMyMoney::Report::ColumnType::Months), TransactionFilter::Date::Next12Months, eMyMoney::Report::DetailLevel::All, i18n("Schedule Summary Information"), i18n("Default Report") )); list.back().setDetailLevel(eMyMoney::Report::DetailLevel::Top); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::AccountInfo, static_cast(eMyMoney::Report::ColumnType::Months), TransactionFilter::Date::Today, eMyMoney::Report::DetailLevel::All, i18n("Account Information"), i18n("Default Report") )); list.back().setConvertCurrency(false); list.push_back(MyMoneyReport( eMyMoney::Report::RowType::AccountLoanInfo, static_cast(eMyMoney::Report::ColumnType::Months), TransactionFilter::Date::Today, eMyMoney::Report::DetailLevel::All, i18n("Loan Information"), i18n("Default Report") )); list.back().setConvertCurrency(false); groups.push_back(list); } } bool columnsAlreadyAdjusted() const { return m_columnsAlreadyAdjusted; } void setColumnsAlreadyAdjusted(bool adjusted) { m_columnsAlreadyAdjusted = adjusted; } KReportsView *q_ptr; /** * This member holds the load state of page */ bool m_needLoad; QListWidget* m_reportListView; QTabWidget* m_reportTabWidget; QWidget* m_listTab; QVBoxLayout* m_listTabLayout; QTreeWidget* m_tocTreeWidget; QMap m_allTocItemGroups; QString m_selectedExportFilter; bool m_columnsAlreadyAdjusted; MyMoneyAccount m_currentAccount; }; #endif diff --git a/kmymoney/views/kgloballedgerview.cpp b/kmymoney/views/kgloballedgerview.cpp index e84cebc18..9002135e6 100644 --- a/kmymoney/views/kgloballedgerview.cpp +++ b/kmymoney/views/kgloballedgerview.cpp @@ -1,2089 +1,2093 @@ /*************************************************************************** kgloballedgerview.cpp - description ------------------- begin : Wed Jul 26 2006 copyright : (C) 2006 by Thomas Baumgart email : Thomas Baumgart (C) 2017 by Łukasz Wojniłowicz ***************************************************************************/ /*************************************************************************** * * * 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 "kgloballedgerview_p.h" #include // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "mymoneyaccount.h" #include "mymoneyfile.h" #include "kmymoneyaccountcombo.h" #include "kmymoneypayeecombo.h" #include "keditscheduledlg.h" #include "kendingbalancedlg.h" #include "register.h" #include "transactioneditor.h" #include "selectedtransactions.h" #include "kmymoneysettings.h" #include "registersearchline.h" #include "kfindtransactiondlg.h" #include "accountsmodel.h" #include "models.h" #include "mymoneyschedule.h" #include "mymoneysecurity.h" #include "mymoneytransaction.h" #include "mymoneytransactionfilter.h" #include "mymoneysplit.h" #include "transaction.h" #include "transactionform.h" #include "widgetenums.h" #include "mymoneyenums.h" #include "menuenums.h" using namespace eMenu; QDate KGlobalLedgerViewPrivate::m_lastPostDate; KGlobalLedgerView::KGlobalLedgerView(QWidget *parent) : KMyMoneyViewBase(*new KGlobalLedgerViewPrivate(this), parent) { typedef void(KGlobalLedgerView::*KGlobalLedgerViewFunc)(); const QHash actionConnections { {Action::NewTransaction, &KGlobalLedgerView::slotNewTransaction}, {Action::EditTransaction, &KGlobalLedgerView::slotEditTransaction}, {Action::DeleteTransaction, &KGlobalLedgerView::slotDeleteTransaction}, {Action::DuplicateTransaction, &KGlobalLedgerView::slotDuplicateTransaction}, {Action::EnterTransaction, &KGlobalLedgerView::slotEnterTransaction}, {Action::AcceptTransaction, &KGlobalLedgerView::slotAcceptTransaction}, {Action::CancelTransaction, &KGlobalLedgerView::slotCancelTransaction}, {Action::EditSplits, &KGlobalLedgerView::slotEditSplits}, {Action::CopySplits, &KGlobalLedgerView::slotCopySplits}, {Action::GoToPayee, &KGlobalLedgerView::slotGoToPayee}, {Action::GoToAccount, &KGlobalLedgerView::slotGoToAccount}, {Action::MatchTransaction, &KGlobalLedgerView::slotMatchTransactions}, {Action::CombineTransactions, &KGlobalLedgerView::slotCombineTransactions}, {Action::ToggleReconciliationFlag, &KGlobalLedgerView::slotToggleReconciliationFlag}, {Action::MarkCleared, &KGlobalLedgerView::slotMarkCleared}, {Action::MarkReconciled, &KGlobalLedgerView::slotMarkReconciled}, {Action::MarkNotReconciled, &KGlobalLedgerView::slotMarkNotReconciled}, {Action::SelectAllTransactions, &KGlobalLedgerView::slotSelectAllTransactions}, {Action::NewScheduledTransaction, &KGlobalLedgerView::slotCreateScheduledTransaction}, {Action::AssignTransactionsNumber, &KGlobalLedgerView::slotAssignNumber}, {Action::StartReconciliation, &KGlobalLedgerView::slotStartReconciliation}, {Action::FinishReconciliation, &KGlobalLedgerView::slotFinishReconciliation}, {Action::PostponeReconciliation, &KGlobalLedgerView::slotPostponeReconciliation}, {Action::OpenAccount, &KGlobalLedgerView::slotOpenAccount}, {Action::EditFindTransaction, &KGlobalLedgerView::slotFindTransaction}, }; for (auto a = actionConnections.cbegin(); a != actionConnections.cend(); ++a) connect(pActions[a.key()], &QAction::triggered, this, a.value()); Q_D(KGlobalLedgerView); d->m_balanceWarning.reset(new KBalanceWarning(this)); } KGlobalLedgerView::~KGlobalLedgerView() { } void KGlobalLedgerView::executeCustomAction(eView::Action action) { Q_D(KGlobalLedgerView); switch(action) { case eView::Action::Refresh: refresh(); break; case eView::Action::SetDefaultFocus: // delay the setFocus call until the event loop is running QMetaObject::invokeMethod(d->m_registerSearchLine->searchLine(), "setFocus", Qt::QueuedConnection); break; case eView::Action::DisableViewDepenedendActions: pActions[Action::SelectAllTransactions]->setEnabled(false); break; case eView::Action::InitializeAfterFileOpen: d->m_lastSelectedAccountID.clear(); d->m_currentAccount = MyMoneyAccount(); if (d->m_accountComboBox) { d->m_accountComboBox->setSelected(QString()); } break; default: break; } } void KGlobalLedgerView::refresh() { Q_D(KGlobalLedgerView); if (isVisible()) { if (!d->m_inEditMode) { setUpdatesEnabled(false); d->loadView(); setUpdatesEnabled(true); d->m_needsRefresh = false; // force a new account if the current one is empty d->m_newAccountLoaded = d->m_currentAccount.id().isEmpty(); } } else { d->m_needsRefresh = true; } } void KGlobalLedgerView::showEvent(QShowEvent* event) { - Q_D(KGlobalLedgerView); - if (d->m_needLoad) - d->init(); - - emit customActionRequested(View::Ledgers, eView::Action::AboutToShow); - - if (d->m_needsRefresh) { - if (!d->m_inEditMode) { - setUpdatesEnabled(false); - d->loadView(); - setUpdatesEnabled(true); - d->m_needsRefresh = false; - d->m_newAccountLoaded = false; - } - - } else { - if (!d->m_lastSelectedAccountID.isEmpty()) { - try { - const auto acc = MyMoneyFile::instance()->account(d->m_lastSelectedAccountID); - slotSelectAccount(acc.id()); - } catch (const MyMoneyException &) { - d->m_lastSelectedAccountID.clear(); // account is invalid + if (MyMoneyFile::instance()->storageAttached()) { + Q_D(KGlobalLedgerView); + if (d->m_needLoad) + d->init(); + + emit customActionRequested(View::Ledgers, eView::Action::AboutToShow); + + if (d->m_needsRefresh) { + if (!d->m_inEditMode) { + setUpdatesEnabled(false); + d->loadView(); + setUpdatesEnabled(true); + d->m_needsRefresh = false; + d->m_newAccountLoaded = false; } + } else { - slotSelectAccount(d->m_accountComboBox->getSelected()); - } + if (!d->m_lastSelectedAccountID.isEmpty()) { + try { + const auto acc = MyMoneyFile::instance()->account(d->m_lastSelectedAccountID); + slotSelectAccount(acc.id()); + } catch (const MyMoneyException &) { + d->m_lastSelectedAccountID.clear(); // account is invalid + } + } else { + slotSelectAccount(d->m_accountComboBox->getSelected()); + } - KMyMoneyRegister::SelectedTransactions list(d->m_register); - updateLedgerActions(list); - emit selectByVariant(QVariantList {QVariant::fromValue(list)}, eView::Intent::SelectRegisterTransactions); + KMyMoneyRegister::SelectedTransactions list(d->m_register); + updateLedgerActions(list); + emit selectByVariant(QVariantList {QVariant::fromValue(list)}, eView::Intent::SelectRegisterTransactions); + } } pActions[Action::SelectAllTransactions]->setEnabled(true); // don't forget base class implementation QWidget::showEvent(event); } void KGlobalLedgerView::updateActions(const MyMoneyObject& obj) { Q_D(KGlobalLedgerView); // if (typeid(obj) != typeid(MyMoneyAccount) && // (obj.id().isEmpty() && d->m_currentAccount.id().isEmpty())) // do not disable actions that were already disabled)) // return; const auto& acc = static_cast(obj); const QVector actionsToBeDisabled { Action::StartReconciliation, Action::FinishReconciliation, Action::PostponeReconciliation, Action::OpenAccount, Action::NewTransaction }; for (const auto& a : actionsToBeDisabled) pActions[a]->setEnabled(false); auto b = acc.isClosed() ? false : true; pMenus[Menu::MoveTransaction]->setEnabled(b); QString tooltip; pActions[Action::NewTransaction]->setEnabled(canCreateTransactions(tooltip)); pActions[Action::NewTransaction]->setToolTip(tooltip); const auto file = MyMoneyFile::instance(); if (!acc.id().isEmpty() && !file->isStandardAccount(acc.id())) { switch (acc.accountGroup()) { case eMyMoney::Account::Type::Asset: case eMyMoney::Account::Type::Liability: case eMyMoney::Account::Type::Equity: pActions[Action::OpenAccount]->setEnabled(true); if (acc.accountGroup() != eMyMoney::Account::Type::Equity) { if (d->m_reconciliationAccount.id().isEmpty()) { pActions[Action::StartReconciliation]->setEnabled(true); pActions[Action::StartReconciliation]->setToolTip(i18n("Reconcile")); } else { auto tip = i18n("Reconcile - disabled because you are currently reconciling %1", d->m_reconciliationAccount.name()); pActions[Action::StartReconciliation]->setToolTip(tip); if (!d->m_transactionEditor) { pActions[Action::FinishReconciliation]->setEnabled(acc.id() == d->m_reconciliationAccount.id()); pActions[Action::PostponeReconciliation]->setEnabled(acc.id() == d->m_reconciliationAccount.id()); } } } break; case eMyMoney::Account::Type::Income : case eMyMoney::Account::Type::Expense : pActions[Action::OpenAccount]->setEnabled(true); break; default: break; } } d->m_currentAccount = acc; // slotSelectAccount(acc); } void KGlobalLedgerView::updateLedgerActions(const KMyMoneyRegister::SelectedTransactions& list) { Q_D(KGlobalLedgerView); d->selectTransactions(list); updateLedgerActionsInternal(); } void KGlobalLedgerView::updateLedgerActionsInternal() { Q_D(KGlobalLedgerView); const QVector actionsToBeDisabled { Action::EditTransaction, Action::EditSplits, Action::EnterTransaction, Action::CancelTransaction, Action::DeleteTransaction, Action::MatchTransaction, Action::AcceptTransaction, Action::DuplicateTransaction, Action::ToggleReconciliationFlag, Action::MarkCleared, Action::GoToAccount, Action::GoToPayee, Action::AssignTransactionsNumber, Action::NewScheduledTransaction, Action::CombineTransactions, Action::CopySplits, }; for (const auto& a : actionsToBeDisabled) pActions[a]->setEnabled(false); const auto file = MyMoneyFile::instance(); pActions[Action::MatchTransaction]->setText(i18nc("Button text for match transaction", "Match")); // pActions[Action::TransactionNew]->setToolTip(i18n("Create a new transaction")); pMenus[Menu::MoveTransaction]->setEnabled(false); pMenus[Menu::MarkTransaction]->setEnabled(false); pMenus[Menu::MarkTransactionContext]->setEnabled(false); if (!d->m_selectedTransactions.isEmpty() && !d->m_selectedTransactions.first().isScheduled()) { // enable 'delete transaction' only if at least one of the // selected transactions does not reference a closed account bool enable = false; KMyMoneyRegister::SelectedTransactions::const_iterator it_t; for (it_t = d->m_selectedTransactions.constBegin(); (enable == false) && (it_t != d->m_selectedTransactions.constEnd()); ++it_t) { enable = !(*it_t).transaction().id().isEmpty() && !file->referencesClosedAccount((*it_t).transaction()); } pActions[Action::DeleteTransaction]->setEnabled(enable); if (!d->m_transactionEditor) { QString tooltip = i18n("Duplicate the current selected transactions"); pActions[Action::DuplicateTransaction]->setEnabled(canDuplicateTransactions(d->m_selectedTransactions, tooltip) && !d->m_selectedTransactions[0].transaction().id().isEmpty()); pActions[Action::DuplicateTransaction]->setToolTip(tooltip); if (canEditTransactions(d->m_selectedTransactions, tooltip)) { pActions[Action::EditTransaction]->setEnabled(true); // editing splits is allowed only if we have one transaction selected if (d->m_selectedTransactions.count() == 1) { pActions[Action::EditSplits]->setEnabled(true); } if (d->m_currentAccount.isAssetLiability() && d->m_currentAccount.accountType() != eMyMoney::Account::Type::Investment) { pActions[Action::NewScheduledTransaction]->setEnabled(d->m_selectedTransactions.count() == 1); } } pActions[Action::EditTransaction]->setToolTip(tooltip); if (!d->m_currentAccount.isClosed()) pMenus[Menu::MoveTransaction]->setEnabled(true); pMenus[Menu::MarkTransaction]->setEnabled(true); pMenus[Menu::MarkTransactionContext]->setEnabled(true); // Allow marking the transaction if at least one is selected pActions[Action::MarkCleared]->setEnabled(true); pActions[Action::MarkReconciled]->setEnabled(true); pActions[Action::MarkNotReconciled]->setEnabled(true); pActions[Action::ToggleReconciliationFlag]->setEnabled(true); if (!d->m_accountGoto.isEmpty()) pActions[Action::GoToAccount]->setEnabled(true); if (!d->m_payeeGoto.isEmpty()) pActions[Action::GoToPayee]->setEnabled(true); // Matching is enabled as soon as one regular and one imported transaction is selected int matchedCount = 0; int importedCount = 0; KMyMoneyRegister::SelectedTransactions::const_iterator it; for (it = d->m_selectedTransactions.constBegin(); it != d->m_selectedTransactions.constEnd(); ++it) { if ((*it).transaction().isImported()) ++importedCount; if ((*it).split().isMatched()) ++matchedCount; } if (d->m_selectedTransactions.count() == 2 /* && pActions[Action::TransactionEdit]->isEnabled() */) { pActions[Action::MatchTransaction]->setEnabled(true); } if (importedCount != 0 || matchedCount != 0) pActions[Action::AcceptTransaction]->setEnabled(true); if (matchedCount != 0) { pActions[Action::MatchTransaction]->setEnabled(true); pActions[Action::MatchTransaction]->setText(i18nc("Button text for unmatch transaction", "Unmatch")); pActions[Action::MatchTransaction]->setIcon(QIcon("process-stop")); } if (d->m_selectedTransactions.count() > 1) { pActions[Action::CombineTransactions]->setEnabled(true); } if (d->m_selectedTransactions.count() >= 2) { int singleSplitTransactions = 0; int multipleSplitTransactions = 0; foreach (const KMyMoneyRegister::SelectedTransaction& st, d->m_selectedTransactions) { switch (st.transaction().splitCount()) { case 0: break; case 1: singleSplitTransactions++; break; default: multipleSplitTransactions++; break; } } if (singleSplitTransactions > 0 && multipleSplitTransactions == 1) { pActions[Action::CopySplits]->setEnabled(true); } } if (d->m_selectedTransactions.count() >= 2) { int singleSplitTransactions = 0; int multipleSplitTransactions = 0; foreach(const KMyMoneyRegister::SelectedTransaction& st, d->m_selectedTransactions) { switch(st.transaction().splitCount()) { case 0: break; case 1: singleSplitTransactions++; break; default: multipleSplitTransactions++; break; } } if(singleSplitTransactions > 0 && multipleSplitTransactions == 1) { pActions[Action::CopySplits]->setEnabled(true); } } } else { pActions[Action::AssignTransactionsNumber]->setEnabled(d->m_transactionEditor->canAssignNumber()); pActions[Action::NewTransaction]->setEnabled(false); pActions[Action::DeleteTransaction]->setEnabled(false); QString reason; pActions[Action::EnterTransaction]->setEnabled(d->m_transactionEditor->isComplete(reason)); //FIXME: Port to KDE4 // the next line somehow worked in KDE3 but does not have // any influence under KDE4 /// Works for me when 'reason' is set. Allan pActions[Action::EnterTransaction]->setToolTip(reason); pActions[Action::CancelTransaction]->setEnabled(true); } } } void KGlobalLedgerView::slotAboutToSelectItem(KMyMoneyRegister::RegisterItem* item, bool& okToSelect) { Q_UNUSED(item); slotCancelOrEnterTransactions(okToSelect); } void KGlobalLedgerView::slotUpdateSummaryLine(const KMyMoneyRegister::SelectedTransactions& selection) { Q_D(KGlobalLedgerView); if (selection.count() > 1) { MyMoneyMoney balance; foreach (const KMyMoneyRegister::SelectedTransaction& t, selection) { if (!t.isScheduled()) { balance += t.split().shares(); } } d->m_rightSummaryLabel->setText(QString("%1: %2").arg(QChar(0x2211), balance.formatMoney("", d->m_precision))); } else { if (d->isReconciliationAccount()) { d->m_rightSummaryLabel->setText(i18n("Difference: %1", d->m_totalBalance.formatMoney("", d->m_precision))); } else { if (d->m_currentAccount.accountType() != eMyMoney::Account::Type::Investment) { d->m_rightSummaryLabel->setText(i18n("Balance: %1", d->m_totalBalance.formatMoney("", d->m_precision))); bool showNegative = d->m_totalBalance.isNegative(); if (d->m_currentAccount.accountGroup() == eMyMoney::Account::Type::Liability && !d->m_totalBalance.isZero()) showNegative = !showNegative; if (showNegative) { QPalette palette = d->m_rightSummaryLabel->palette(); palette.setColor(d->m_rightSummaryLabel->foregroundRole(), KMyMoneySettings::schemeColor(SchemeColor::Negative)); d->m_rightSummaryLabel->setPalette(palette); } } else { d->m_rightSummaryLabel->setText(i18n("Investment value: %1%2", d->m_balanceIsApproximated ? "~" : "", d->m_totalBalance.formatMoney(MyMoneyFile::instance()->baseCurrency().tradingSymbol(), d->m_precision))); } } } } void KGlobalLedgerView::resizeEvent(QResizeEvent* ev) { - Q_D(KGlobalLedgerView); - if (d->m_needLoad) - d->init(); + if (MyMoneyFile::instance()->storageAttached()) { + Q_D(KGlobalLedgerView); + if (d->m_needLoad) + d->init(); - d->m_register->resize((int)eWidgets::eTransaction::Column::Detail); - d->m_form->resize((int)eWidgets::eTransactionForm::Column::Value1); + d->m_register->resize((int)eWidgets::eTransaction::Column::Detail); + d->m_form->resize((int)eWidgets::eTransactionForm::Column::Value1); + } KMyMoneyViewBase::resizeEvent(ev); } void KGlobalLedgerView::slotSetReconcileAccount(const MyMoneyAccount& acc, const QDate& reconciliationDate, const MyMoneyMoney& endingBalance) { Q_D(KGlobalLedgerView); if(d->m_needLoad) d->init(); if (d->m_reconciliationAccount.id() != acc.id()) { // make sure the account is selected if (!acc.id().isEmpty()) slotSelectAccount(acc.id()); d->m_reconciliationAccount = acc; d->m_reconciliationDate = reconciliationDate; d->m_endingBalance = endingBalance; if (acc.accountGroup() == eMyMoney::Account::Type::Liability) d->m_endingBalance = -endingBalance; d->m_newAccountLoaded = true; if (acc.id().isEmpty()) { d->m_buttonbar->removeAction(pActions[Action::PostponeReconciliation]); d->m_buttonbar->removeAction(pActions[Action::FinishReconciliation]); } else { d->m_buttonbar->addAction(pActions[Action::PostponeReconciliation]); d->m_buttonbar->addAction(pActions[Action::FinishReconciliation]); // when we start reconciliation, we need to reload the view // because no data has been changed. When postponing or finishing // reconciliation, the data change in the engine takes care of updating // the view. refresh(); } } } void KGlobalLedgerView::slotSetReconcileAccount(const MyMoneyAccount& acc, const QDate& reconciliationDate) { slotSetReconcileAccount(acc, reconciliationDate, MyMoneyMoney()); } void KGlobalLedgerView::slotSetReconcileAccount(const MyMoneyAccount& acc) { slotSetReconcileAccount(acc, QDate(), MyMoneyMoney()); } void KGlobalLedgerView::slotSetReconcileAccount() { slotSetReconcileAccount(MyMoneyAccount(), QDate(), MyMoneyMoney()); } void KGlobalLedgerView::slotShowTransactionMenu(const MyMoneySplit& sp) { Q_UNUSED(sp) pMenus[Menu::Transaction]->exec(QCursor::pos()); } void KGlobalLedgerView::slotContinueReconciliation() { Q_D(KGlobalLedgerView); const auto file = MyMoneyFile::instance(); MyMoneyAccount account; try { account = file->account(d->m_currentAccount.id()); // get rid of previous run. delete d->m_endingBalanceDlg; d->m_endingBalanceDlg = new KEndingBalanceDlg(account, this); if (account.isAssetLiability()) { if (d->m_endingBalanceDlg->exec() == QDialog::Accepted) { if (KMyMoneySettings::autoReconciliation()) { MyMoneyMoney startBalance = d->m_endingBalanceDlg->previousBalance(); MyMoneyMoney endBalance = d->m_endingBalanceDlg->endingBalance(); QDate endDate = d->m_endingBalanceDlg->statementDate(); QList > transactionList; MyMoneyTransactionFilter filter(account.id()); filter.addState((int)eMyMoney::TransactionFilter::State::Cleared); filter.addState((int)eMyMoney::TransactionFilter::State::NotReconciled); filter.setDateFilter(QDate(), endDate); filter.setConsiderCategory(false); filter.setReportAllSplits(true); file->transactionList(transactionList, filter); QList > result = d->automaticReconciliation(account, transactionList, endBalance - startBalance); if (!result.empty()) { QString message = i18n("KMyMoney has detected transactions matching your reconciliation data.\nWould you like KMyMoney to clear these transactions for you?"); if (KMessageBox::questionYesNo(this, message, i18n("Automatic reconciliation"), KStandardGuiItem::yes(), KStandardGuiItem::no(), "AcceptAutomaticReconciliation") == KMessageBox::Yes) { // mark the transactions cleared KMyMoneyRegister::SelectedTransactions oldSelection = d->m_selectedTransactions; d->m_selectedTransactions.clear(); QListIterator > itTransactionSplitResult(result); while (itTransactionSplitResult.hasNext()) { const QPair &transactionSplit = itTransactionSplitResult.next(); d->m_selectedTransactions.append(KMyMoneyRegister::SelectedTransaction(transactionSplit.first, transactionSplit.second, QString())); } // mark all transactions in d->m_selectedTransactions as 'Cleared' d->markTransaction(eMyMoney::Split::State::Cleared); d->m_selectedTransactions = oldSelection; } } } if (!file->isStandardAccount(account.id()) && account.isAssetLiability()) { if (!isVisible()) emit customActionRequested(View::Ledgers, eView::Action::SwitchView); Models::instance()->accountsModel()->slotReconcileAccount(account, d->m_endingBalanceDlg->statementDate(), d->m_endingBalanceDlg->endingBalance()); slotSetReconcileAccount(account, d->m_endingBalanceDlg->statementDate(), d->m_endingBalanceDlg->endingBalance()); // check if the user requests us to create interest // or charge transactions. auto ti = d->m_endingBalanceDlg->interestTransaction(); auto tc = d->m_endingBalanceDlg->chargeTransaction(); MyMoneyFileTransaction ft; try { if (ti != MyMoneyTransaction()) { MyMoneyFile::instance()->addTransaction(ti); } if (tc != MyMoneyTransaction()) { MyMoneyFile::instance()->addTransaction(tc); } ft.commit(); } catch (const MyMoneyException &e) { qWarning("interest transaction not stored: '%s'", e.what()); } // reload the account object as it might have changed in the meantime d->m_reconciliationAccount = file->account(account.id()); updateActions(d->m_currentAccount); updateLedgerActionsInternal(); // slotUpdateActions(); } } } } catch (const MyMoneyException &) { } } void KGlobalLedgerView::slotLedgerSelected(const QString& _accId, const QString& transaction) { auto acc = MyMoneyFile::instance()->account(_accId); QString accId(_accId); switch (acc.accountType()) { case Account::Type::Stock: // if a stock account is selected, we show the // the corresponding parent (investment) account acc = MyMoneyFile::instance()->account(acc.parentAccountId()); accId = acc.id(); // intentional fall through case Account::Type::Checkings: case Account::Type::Savings: case Account::Type::Cash: case Account::Type::CreditCard: case Account::Type::Loan: case Account::Type::Asset: case Account::Type::Liability: case Account::Type::AssetLoan: case Account::Type::Income: case Account::Type::Expense: case Account::Type::Investment: case Account::Type::Equity: if (!isVisible()) emit customActionRequested(View::Ledgers, eView::Action::SwitchView); slotSelectAccount(accId, transaction); break; case Account::Type::CertificateDep: case Account::Type::MoneyMarket: case Account::Type::Currency: qDebug("No ledger view available for account type %d", (int)acc.accountType()); break; default: qDebug("Unknown account type %d in KMyMoneyView::slotLedgerSelected", (int)acc.accountType()); break; } } void KGlobalLedgerView::slotSelectByObject(const MyMoneyObject& obj, eView::Intent intent) { switch(intent) { case eView::Intent::UpdateActions: updateActions(obj); break; case eView::Intent::FinishEnteringOverdueScheduledTransactions: slotContinueReconciliation(); break; case eView::Intent::SynchronizeAccountInLedgersView: slotSelectAccount(obj); break; default: break; } } void KGlobalLedgerView::slotSelectByVariant(const QVariantList& variant, eView::Intent intent) { switch(intent) { case eView::Intent::ShowTransaction: if (variant.count() == 2) slotLedgerSelected(variant.at(0).toString(), variant.at(1).toString()); break; case eView::Intent::SelectRegisterTransactions: if (variant.count() == 1) updateLedgerActions(variant.at(0).value()); break; default: break; } } void KGlobalLedgerView::slotSelectAccount(const MyMoneyObject& obj) { Q_D(KGlobalLedgerView); if (typeid(obj) != typeid(MyMoneyAccount)) return/* false */; d->m_lastSelectedAccountID = obj.id(); } void KGlobalLedgerView::slotSelectAccount(const QString& id) { slotSelectAccount(id, QString()); } bool KGlobalLedgerView::slotSelectAccount(const QString& id, const QString& transactionId) { Q_D(KGlobalLedgerView); auto rc = true; if (!id.isEmpty()) { if (d->m_currentAccount.id() != id) { try { d->m_currentAccount = MyMoneyFile::instance()->account(id); // if a stock account is selected, we show the // the corresponding parent (investment) account if (d->m_currentAccount.isInvest()) { d->m_currentAccount = MyMoneyFile::instance()->account(d->m_currentAccount.parentAccountId()); } d->m_lastSelectedAccountID = d->m_currentAccount.id(); d->m_newAccountLoaded = true; refresh(); } catch (const MyMoneyException &) { qDebug("Unable to retrieve account %s", qPrintable(id)); rc = false; } } else { // we need to refresh m_account.m_accountList, a child could have been deleted d->m_currentAccount = MyMoneyFile::instance()->account(id); emit selectByObject(d->m_currentAccount, eView::Intent::None); emit selectByObject(d->m_currentAccount, eView::Intent::SynchronizeAccountInInvestmentView); } d->selectTransaction(transactionId); } return rc; } bool KGlobalLedgerView::selectEmptyTransaction() { Q_D(KGlobalLedgerView); bool rc = false; if (!d->m_inEditMode) { // in case we don't know the type of transaction to be created, // have at least one selected transaction and the id of // this transaction is not empty, we take it as template for the // transaction to be created KMyMoneyRegister::SelectedTransactions list(d->m_register); if ((d->m_action == eWidgets::eRegister::Action::None) && (!list.isEmpty()) && (!list[0].transaction().id().isEmpty())) { // the new transaction to be created will have the same type // as the one that currently has the focus KMyMoneyRegister::Transaction* t = dynamic_cast(d->m_register->focusItem()); if (t) d->m_action = t->actionType(); d->m_register->clearSelection(); } // if we still don't have an idea which type of transaction // to create, we use the default. if (d->m_action == eWidgets::eRegister::Action::None) { d->setupDefaultAction(); } d->m_register->selectItem(d->m_register->lastItem()); d->m_register->updateRegister(); rc = true; } return rc; } TransactionEditor* KGlobalLedgerView::startEdit(const KMyMoneyRegister::SelectedTransactions& list) { Q_D(KGlobalLedgerView); // we use the warnlevel to keep track, if we have to warn the // user that some or all splits have been reconciled or if the // user cannot modify the transaction if at least one split // has the status frozen. The following value are used: // // 0 - no sweat, user can modify // 1 - user should be warned that at least one split has been reconciled // already // 2 - user will be informed, that this transaction cannot be changed anymore int warnLevel = list.warnLevel(); Q_ASSERT(warnLevel < 2); // otherwise the edit action should not be enabled switch (warnLevel) { case 0: break; case 1: if (KMessageBox::warningContinueCancel(this, i18n( "At least one split of the selected transactions has been reconciled. " "Do you wish to continue to edit the transactions anyway?" ), i18n("Transaction already reconciled"), KStandardGuiItem::cont(), KStandardGuiItem::cancel(), "EditReconciledTransaction") == KMessageBox::Cancel) { warnLevel = 2; } break; case 2: KMessageBox::sorry(this, i18n("At least one split of the selected transactions has been frozen. " "Editing the transactions is therefore prohibited."), i18n("Transaction already frozen")); break; case 3: KMessageBox::sorry(this, i18n("At least one split of the selected transaction references an account that has been closed. " "Editing the transactions is therefore prohibited."), i18n("Account closed")); break; } if (warnLevel > 1) { d->m_register->endEdit(); return 0; } TransactionEditor* editor = 0; KMyMoneyRegister::Transaction* item = dynamic_cast(d->m_register->focusItem()); if (item) { // in case the current focus item is not selected, we move the focus to the first selected transaction if (!item->isSelected()) { KMyMoneyRegister::RegisterItem* p; for (p = d->m_register->firstItem(); p; p = p->nextItem()) { KMyMoneyRegister::Transaction* t = dynamic_cast(p); if (t && t->isSelected()) { d->m_register->setFocusItem(t); item = t; break; } } } // decide, if we edit in the register or in the form TransactionEditorContainer* parent; if (d->m_formFrame->isVisible()) parent = d->m_form; else { parent = d->m_register; } editor = item->createEditor(parent, list, KGlobalLedgerViewPrivate::m_lastPostDate); // check that we use the same transaction commodity in all selected transactions // if not, we need to update this in the editor's list. The user can also bail out // of this operation which means that we have to stop editing here. if (editor) { if (!editor->fixTransactionCommodity(d->m_currentAccount)) { // if the user wants to quit, we need to destroy the editor // and bail out delete editor; editor = 0; } } if (editor) { if (parent == d->m_register) { // make sure, the height of the table is correct d->m_register->updateRegister(KMyMoneySettings::ledgerLens() | !KMyMoneySettings::transactionForm()); } d->m_inEditMode = true; connect(editor, &TransactionEditor::transactionDataSufficient, pActions[Action::EnterTransaction], &QAction::setEnabled); connect(editor, &TransactionEditor::returnPressed, pActions[Action::EnterTransaction], &QAction::trigger); connect(editor, &TransactionEditor::escapePressed, pActions[Action::CancelTransaction], &QAction::trigger); connect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, editor, &TransactionEditor::slotReloadEditWidgets); connect(editor, &TransactionEditor::finishEdit, this, &KGlobalLedgerView::slotLeaveEditMode); connect(editor, &TransactionEditor::objectCreation, d->m_mousePressFilter, &MousePressFilter::setFilterDeactive); connect(editor, &TransactionEditor::assignNumber, this, &KGlobalLedgerView::slotAssignNumber); connect(editor, &TransactionEditor::lastPostDateUsed, this, &KGlobalLedgerView::slotKeepPostDate); // create the widgets, place them in the parent and load them with data // setup tab order d->m_tabOrderWidgets.clear(); editor->setup(d->m_tabOrderWidgets, d->m_currentAccount, d->m_action); Q_ASSERT(!d->m_tabOrderWidgets.isEmpty()); // install event filter in all taborder widgets QWidgetList::const_iterator it_w = d->m_tabOrderWidgets.constBegin(); for (; it_w != d->m_tabOrderWidgets.constEnd(); ++it_w) { (*it_w)->installEventFilter(this); } // Install a filter that checks if a mouse press happened outside // of one of our own widgets. qApp->installEventFilter(d->m_mousePressFilter); // Check if the editor has some preference on where to set the focus // If not, set the focus to the first widget in the tab order QWidget* focusWidget = editor->firstWidget(); if (!focusWidget) focusWidget = d->m_tabOrderWidgets.first(); // for some reason, this only works reliably if delayed a bit QTimer::singleShot(10, focusWidget, SLOT(setFocus())); // preset to 'I have no idea which type to create' for the next round. d->m_action = eWidgets::eRegister::Action::None; } } return editor; } void KGlobalLedgerView::slotTransactionsContextMenuRequested() { Q_D(KGlobalLedgerView); auto transactions = d->m_selectedTransactions; updateLedgerActionsInternal(); // emit transactionsSelected(d->m_selectedTransactions); // that should select MyMoneySchedule in KScheduledView if (!transactions.isEmpty() && transactions.first().isScheduled()) emit selectByObject(MyMoneyFile::instance()->schedule(transactions.first().scheduleId()), eView::Intent::OpenContextMenu); else slotShowTransactionMenu(MyMoneySplit()); } void KGlobalLedgerView::slotLeaveEditMode(const KMyMoneyRegister::SelectedTransactions& list) { Q_D(KGlobalLedgerView); d->m_inEditMode = false; qApp->removeEventFilter(d->m_mousePressFilter); // a possible focusOut event may have removed the focus, so we // install it back again. d->m_register->focusItem()->setFocus(true); // if we come back from editing a new item, we make sure that // we always select the very last known transaction entry no // matter if the transaction has been created or not. if (list.count() && list[0].transaction().id().isEmpty()) { // block signals to prevent some infinite loops that might occur here. d->m_register->blockSignals(true); d->m_register->clearSelection(); KMyMoneyRegister::RegisterItem* p = d->m_register->lastItem(); if (p && p->prevItem()) p = p->prevItem(); d->m_register->selectItem(p); d->m_register->updateRegister(true); d->m_register->blockSignals(false); // we need to update the form manually as sending signals was blocked KMyMoneyRegister::Transaction* t = dynamic_cast(p); if (t) d->m_form->slotSetTransaction(t); } else { if (!KMyMoneySettings::transactionForm()) { // update the row height of the transactions because it might differ between viewing/editing mode when not using the transaction form d->m_register->blockSignals(true); d->m_register->updateRegister(true); d->m_register->blockSignals(false); } } d->m_needsRefresh = true; // TODO: Why transaction in view doesn't update without this? if (d->m_needsRefresh) refresh(); d->m_register->endEdit(); d->m_register->setFocus(); } bool KGlobalLedgerView::focusNextPrevChild(bool next) { Q_D(KGlobalLedgerView); bool rc = false; // qDebug("KGlobalLedgerView::focusNextPrevChild(editmode=%s)", m_inEditMode ? "true" : "false"); if (d->m_inEditMode) { QWidget *w = 0; w = qApp->focusWidget(); // qDebug("w = %p", w); int currentWidgetIndex = d->m_tabOrderWidgets.indexOf(w); while (w && currentWidgetIndex == -1) { // qDebug("'%s' not in list, use parent", qPrintable(w->objectName())); w = w->parentWidget(); currentWidgetIndex = d->m_tabOrderWidgets.indexOf(w); } if (currentWidgetIndex != -1) { // if(w) qDebug("tab order is at '%s'", qPrintable(w->objectName())); currentWidgetIndex += next ? 1 : -1; if (currentWidgetIndex < 0) currentWidgetIndex = d->m_tabOrderWidgets.size() - 1; else if (currentWidgetIndex >= d->m_tabOrderWidgets.size()) currentWidgetIndex = 0; w = d->m_tabOrderWidgets[currentWidgetIndex]; // qDebug("currentWidgetIndex = %d, w = %p", currentWidgetIndex, w); if (((w->focusPolicy() & Qt::TabFocus) == Qt::TabFocus) && w->isVisible() && w->isEnabled()) { // qDebug("Selecting '%s' (%p) as focus", qPrintable(w->objectName()), w); w->setFocus(); rc = true; } } } else rc = KMyMoneyViewBase::focusNextPrevChild(next); return rc; } bool KGlobalLedgerView::eventFilter(QObject* o, QEvent* e) { Q_D(KGlobalLedgerView); bool rc = false; // Need to capture mouse position here as QEvent::ToolTip is too slow d->m_tooltipPosn = QCursor::pos(); if (e->type() == QEvent::KeyPress) { if (d->m_inEditMode) { // qDebug("object = %s, key = %d", o->className(), k->key()); if (o == d->m_register) { // we hide all key press events from the register // while editing a transaction rc = true; } } } if (!rc) rc = KMyMoneyViewBase::eventFilter(o, e); return rc; } void KGlobalLedgerView::slotSortOptions() { Q_D(KGlobalLedgerView); QPointer dlg = new KSortOptionDlg(this); QString key; QString sortOrder, def; if (d->isReconciliationAccount()) { key = "kmm-sort-reconcile"; def = KMyMoneySettings::sortReconcileView(); } else { key = "kmm-sort-std"; def = KMyMoneySettings::sortNormalView(); } // check if we have an account override of the sort order if (!d->m_currentAccount.value(key).isEmpty()) sortOrder = d->m_currentAccount.value(key); QString oldOrder = sortOrder; dlg->setSortOption(sortOrder, def); if (dlg->exec() == QDialog::Accepted) { if (dlg != 0) { sortOrder = dlg->sortOption(); if (sortOrder != oldOrder) { if (sortOrder.isEmpty()) { d->m_currentAccount.deletePair(key); } else { d->m_currentAccount.setValue(key, sortOrder); } MyMoneyFileTransaction ft; try { MyMoneyFile::instance()->modifyAccount(d->m_currentAccount); ft.commit(); } catch (const MyMoneyException &e) { qDebug("Unable to update sort order for account '%s': %s", qPrintable(d->m_currentAccount.name()), e.what()); } } } } delete dlg; } void KGlobalLedgerView::slotToggleTransactionMark(KMyMoneyRegister::Transaction* /* t */) { Q_D(KGlobalLedgerView); if (!d->m_inEditMode) { slotToggleReconciliationFlag(); } } void KGlobalLedgerView::slotKeepPostDate(const QDate& date) { KGlobalLedgerViewPrivate::m_lastPostDate = date; } QString KGlobalLedgerView::accountId() const { Q_D(const KGlobalLedgerView); return d->m_currentAccount.id(); } bool KGlobalLedgerView::canCreateTransactions(QString& tooltip) const { Q_D(const KGlobalLedgerView); bool rc = true; // we can only create transactions in the ledger view so // we check that this is the active page if(!isVisible()) { tooltip = i18n("Creating transactions can only be performed in the ledger view"); rc = false; } if (d->m_currentAccount.id().isEmpty()) { tooltip = i18n("Cannot create transactions when no account is selected."); rc = false; } if (d->m_currentAccount.accountGroup() == eMyMoney::Account::Type::Income || d->m_currentAccount.accountGroup() == eMyMoney::Account::Type::Expense) { tooltip = i18n("Cannot create transactions in the context of a category."); d->showTooltip(tooltip); rc = false; } if (d->m_currentAccount.isClosed()) { tooltip = i18n("Cannot create transactions in a closed account."); d->showTooltip(tooltip); rc = false; } return rc; } bool KGlobalLedgerView::canModifyTransactions(const KMyMoneyRegister::SelectedTransactions& list, QString& tooltip) const { Q_D(const KGlobalLedgerView); return d->canProcessTransactions(list, tooltip) && list.canModify(); } bool KGlobalLedgerView::canDuplicateTransactions(const KMyMoneyRegister::SelectedTransactions& list, QString& tooltip) const { Q_D(const KGlobalLedgerView); return d->canProcessTransactions(list, tooltip) && list.canDuplicate(); } bool KGlobalLedgerView::canEditTransactions(const KMyMoneyRegister::SelectedTransactions& list, QString& tooltip) const { Q_D(const KGlobalLedgerView); // check if we can edit the list of transactions. We can edit, if // // a) no mix of standard and investment transactions exist // b) if a split transaction is selected, this is the only selection // c) none of the splits is frozen // d) the transaction having the current focus is selected // check for d) if (!d->canProcessTransactions(list, tooltip)) return false; // check for c) if (list.warnLevel() == 2) { tooltip = i18n("Cannot edit transactions with frozen splits."); d->showTooltip(tooltip); return false; } bool rc = true; int investmentTransactions = 0; int normalTransactions = 0; if (d->m_currentAccount.accountGroup() == eMyMoney::Account::Type::Income || d->m_currentAccount.accountGroup() == eMyMoney::Account::Type::Expense) { tooltip = i18n("Cannot edit transactions in the context of a category."); d->showTooltip(tooltip); rc = false; } if (d->m_currentAccount.isClosed()) { tooltip = i18n("Cannot create or edit any transactions in Account %1 as it is closed", d->m_currentAccount.name()); d->showTooltip(tooltip); rc = false; } KMyMoneyRegister::SelectedTransactions::const_iterator it_t; QString action; for (it_t = list.begin(); rc && it_t != list.end(); ++it_t) { if ((*it_t).transaction().id().isEmpty()) { tooltip.clear(); rc = false; continue; } if (KMyMoneyUtils::transactionType((*it_t).transaction()) == KMyMoneyUtils::InvestmentTransaction) { if (action.isEmpty()) { action = (*it_t).split().action(); continue; } if (action == (*it_t).split().action()) { continue; } else { tooltip = (i18n("Cannot edit mixed investment action/type transactions together.")); d->showTooltip(tooltip); rc = false; break; } } if (KMyMoneyUtils::transactionType((*it_t).transaction()) == KMyMoneyUtils::InvestmentTransaction) ++investmentTransactions; else ++normalTransactions; // check for a) if (investmentTransactions != 0 && normalTransactions != 0) { tooltip = i18n("Cannot edit investment transactions and non-investment transactions together."); d->showTooltip(tooltip); rc = false; break; } // check for b) but only for normalTransactions if ((*it_t).transaction().splitCount() > 2 && normalTransactions != 0) { if (list.count() > 1) { tooltip = i18n("Cannot edit multiple split transactions at once."); d->showTooltip(tooltip); rc = false; break; } } } // check for multiple transactions being selected in an investment account // we do not allow editing in this case: https://bugs.kde.org/show_bug.cgi?id=240816 // later on, we might allow to edit investment transactions of the same type /// Can now disable the following check. /* if (rc == true && investmentTransactions > 1) { tooltip = i18n("Cannot edit multiple investment transactions at once"); rc = false; }*/ // now check that we have the correct account type for investment transactions if (rc == true && investmentTransactions != 0) { if (d->m_currentAccount.accountType() != eMyMoney::Account::Type::Investment) { tooltip = i18n("Cannot edit investment transactions in the context of this account."); rc = false; } } return rc; } void KGlobalLedgerView::slotMoveToAccount(const QString& id) { Q_D(KGlobalLedgerView); // close the menu, if it is still open if (pMenus[Menu::Transaction]->isVisible()) pMenus[Menu::Transaction]->close(); if (!d->m_selectedTransactions.isEmpty()) { MyMoneyFileTransaction ft; try { foreach (const auto selection, d->m_selectedTransactions) { if (d->m_currentAccount.accountType() == eMyMoney::Account::Type::Investment) { d->moveInvestmentTransaction(d->m_currentAccount.id(), id, selection.transaction()); } else { auto changed = false; auto t = selection.transaction(); foreach (const auto split, selection.transaction().splits()) { if (split.accountId() == d->m_currentAccount.id()) { MyMoneySplit s = split; s.setAccountId(id); t.modifySplit(s); changed = true; } } if (changed) { MyMoneyFile::instance()->modifyTransaction(t); } } } ft.commit(); } catch (const MyMoneyException &) { } } } void KGlobalLedgerView::slotUpdateMoveToAccountMenu() { Q_D(KGlobalLedgerView); d->createTransactionMoveMenu(); // in case we were not able to create the selector, we // better get out of here. Anything else would cause // a crash later on (accountSet.load) if (!d->m_moveToAccountSelector) return; if (!d->m_currentAccount.id().isEmpty()) { AccountSet accountSet; if (d->m_currentAccount.accountType() == eMyMoney::Account::Type::Investment) { accountSet.addAccountType(eMyMoney::Account::Type::Investment); } else if (d->m_currentAccount.isAssetLiability()) { accountSet.addAccountType(eMyMoney::Account::Type::Checkings); accountSet.addAccountType(eMyMoney::Account::Type::Savings); accountSet.addAccountType(eMyMoney::Account::Type::Cash); accountSet.addAccountType(eMyMoney::Account::Type::AssetLoan); accountSet.addAccountType(eMyMoney::Account::Type::CertificateDep); accountSet.addAccountType(eMyMoney::Account::Type::MoneyMarket); accountSet.addAccountType(eMyMoney::Account::Type::Asset); accountSet.addAccountType(eMyMoney::Account::Type::Currency); accountSet.addAccountType(eMyMoney::Account::Type::CreditCard); accountSet.addAccountType(eMyMoney::Account::Type::Loan); accountSet.addAccountType(eMyMoney::Account::Type::Liability); } else if (d->m_currentAccount.isIncomeExpense()) { accountSet.addAccountType(eMyMoney::Account::Type::Income); accountSet.addAccountType(eMyMoney::Account::Type::Expense); } accountSet.load(d->m_moveToAccountSelector); // remove those accounts that we currently reference foreach (const auto selection, d->m_selectedTransactions) { foreach (const auto split, selection.transaction().splits()) { d->m_moveToAccountSelector->removeItem(split.accountId()); } } // remove those accounts from the list that are denominated // in a different currency auto list = d->m_moveToAccountSelector->accountList(); QList::const_iterator it_a; for (it_a = list.constBegin(); it_a != list.constEnd(); ++it_a) { auto acc = MyMoneyFile::instance()->account(*it_a); if (acc.currencyId() != d->m_currentAccount.currencyId()) d->m_moveToAccountSelector->removeItem((*it_a)); } } } void KGlobalLedgerView::slotObjectDestroyed(QObject* o) { Q_D(KGlobalLedgerView); if (o == d->m_moveToAccountSelector) { d->m_moveToAccountSelector = nullptr; } } void KGlobalLedgerView::slotCancelOrEnterTransactions(bool& okToSelect) { Q_D(KGlobalLedgerView); static bool oneTime = false; if (!oneTime) { oneTime = true; auto dontShowAgain = "CancelOrEditTransaction"; // qDebug("KMyMoneyApp::slotCancelOrEndEdit"); if (d->m_transactionEditor) { if (KMyMoneySettings::focusChangeIsEnter() && pActions[Action::EnterTransaction]->isEnabled()) { slotEnterTransaction(); if (d->m_transactionEditor) { // if at this stage the editor is still there that means that entering the transaction was cancelled // for example by pressing cancel on the exchange rate editor so we must stay in edit mode okToSelect = false; } } else { // okToSelect is preset to true if a cancel of the dialog is useful and false if it is not int rc; KGuiItem noGuiItem = KStandardGuiItem::save(); KGuiItem yesGuiItem = KStandardGuiItem::discard(); KGuiItem cancelGuiItem = KStandardGuiItem::cont(); // if the transaction can't be entered make sure that it can't be entered by pressing no either if (!pActions[Action::EnterTransaction]->isEnabled()) { noGuiItem.setEnabled(false); noGuiItem.setToolTip(pActions[Action::EnterTransaction]->toolTip()); } if (okToSelect == true) { rc = KMessageBox::warningYesNoCancel(this, i18n("

Please select what you want to do: discard the changes, save the changes or continue to edit the transaction.

You can also set an option to save the transaction automatically when e.g. selecting another transaction.

"), i18n("End transaction edit"), yesGuiItem, noGuiItem, cancelGuiItem, dontShowAgain); } else { rc = KMessageBox::warningYesNo(this, i18n("

Please select what you want to do: discard the changes, save the changes or continue to edit the transaction.

You can also set an option to save the transaction automatically when e.g. selecting another transaction.

"), i18n("End transaction edit"), yesGuiItem, noGuiItem, dontShowAgain); } switch (rc) { case KMessageBox::Yes: slotCancelTransaction(); break; case KMessageBox::No: slotEnterTransaction(); // make sure that we'll see this message the next time no matter // if the user has chosen the 'Don't show again' checkbox KMessageBox::enableMessage(dontShowAgain); if (d->m_transactionEditor) { // if at this stage the editor is still there that means that entering the transaction was cancelled // for example by pressing cancel on the exchange rate editor so we must stay in edit mode okToSelect = false; } break; case KMessageBox::Cancel: // make sure that we'll see this message the next time no matter // if the user has chosen the 'Don't show again' checkbox KMessageBox::enableMessage(dontShowAgain); okToSelect = false; break; } } } oneTime = false; } } void KGlobalLedgerView::slotNewSchedule(const MyMoneyTransaction& _t, eMyMoney::Schedule::Occurrence occurrence) { KEditScheduleDlg::newSchedule(_t, occurrence); } void KGlobalLedgerView::slotNewTransactionForm(eWidgets::eRegister::Action id) { Q_D(KGlobalLedgerView); if (!d->m_inEditMode) { d->m_action = id; // since we jump here via code, we have to make sure to react only // if the action is enabled if (pActions[Action::NewTransaction]->isEnabled()) { if (d->createNewTransaction()) { d->m_transactionEditor = d->startEdit(d->m_selectedTransactions); if (d->m_transactionEditor) { KMyMoneyMVCCombo::setSubstringSearchForChildren(this/*d->m_myMoneyView*/, !KMyMoneySettings::stringMatchFromStart()); KMyMoneyPayeeCombo* payeeEdit = dynamic_cast(d->m_transactionEditor->haveWidget("payee")); if (payeeEdit && !d->m_lastPayeeEnteredId.isEmpty()) { // in case we entered a new transaction before and used a payee, // we reuse it here. Save the text to the edit widget, select it // so that hitting any character will start entering another payee. payeeEdit->setSelectedItem(d->m_lastPayeeEnteredId); payeeEdit->lineEdit()->selectAll(); } if (d->m_transactionEditor) { connect(d->m_transactionEditor.data(), &TransactionEditor::statusProgress, this, &KGlobalLedgerView::slotStatusProgress); connect(d->m_transactionEditor.data(), &TransactionEditor::statusMsg, this, &KGlobalLedgerView::slotStatusMsg); connect(d->m_transactionEditor.data(), &TransactionEditor::scheduleTransaction, this, &KGlobalLedgerView::slotNewSchedule); } updateLedgerActionsInternal(); // emit transactionsSelected(d->m_selectedTransactions); } } } } } void KGlobalLedgerView::slotNewTransaction() { slotNewTransactionForm(eWidgets::eRegister::Action::None); } void KGlobalLedgerView::slotEditTransaction() { Q_D(KGlobalLedgerView); // qDebug("KMyMoneyApp::slotTransactionsEdit()"); // since we jump here via code, we have to make sure to react only // if the action is enabled if (pActions[Action::EditTransaction]->isEnabled()) { // as soon as we edit a transaction, we don't remember the last payee entered d->m_lastPayeeEnteredId.clear(); d->m_transactionEditor = d->startEdit(d->m_selectedTransactions); KMyMoneyMVCCombo::setSubstringSearchForChildren(this/*d->m_myMoneyView*/, !KMyMoneySettings::stringMatchFromStart()); updateLedgerActionsInternal(); } } void KGlobalLedgerView::slotDeleteTransaction() { Q_D(KGlobalLedgerView); // since we may jump here via code, we have to make sure to react only // if the action is enabled if (!pActions[Action::DeleteTransaction]->isEnabled()) return; if (d->m_selectedTransactions.isEmpty()) return; if (d->m_selectedTransactions.warnLevel() == 1) { if (KMessageBox::warningContinueCancel(this, i18n("At least one split of the selected transactions has been reconciled. " "Do you wish to delete the transactions anyway?"), i18n("Transaction already reconciled")) == KMessageBox::Cancel) return; } auto msg = i18np("Do you really want to delete the selected transaction?", "Do you really want to delete all %1 selected transactions?", d->m_selectedTransactions.count()); if (KMessageBox::questionYesNo(this, msg, i18n("Delete transaction")) == KMessageBox::Yes) { //KMSTATUS(i18n("Deleting transactions")); d->doDeleteTransactions(); } } void KGlobalLedgerView::slotDuplicateTransaction() { Q_D(KGlobalLedgerView); // since we may jump here via code, we have to make sure to react only // if the action is enabled if (pActions[Action::DuplicateTransaction]->isEnabled()) { KMyMoneyRegister::SelectedTransactions selectionList = d->m_selectedTransactions; KMyMoneyRegister::SelectedTransactions::iterator it_t; int i = 0; int cnt = d->m_selectedTransactions.count(); // KMSTATUS(i18n("Duplicating transactions")); emit selectByVariant(QVariantList {QVariant(0), QVariant(cnt)}, eView::Intent::ReportProgress); MyMoneyFileTransaction ft; MyMoneyTransaction lt; try { foreach (const auto selection, selectionList) { auto t = selection.transaction(); // wipe out any reconciliation information for (auto& split : t.splits()) { split.setReconcileFlag(eMyMoney::Split::State::NotReconciled); split.setReconcileDate(QDate()); split.setBankID(QString()); } // clear invalid data t.setEntryDate(QDate()); t.clearId(); // and set the post date to today t.setPostDate(QDate::currentDate()); MyMoneyFile::instance()->addTransaction(t); lt = t; emit selectByVariant(QVariantList {QVariant(i++), QVariant(0)}, eView::Intent::ReportProgress); } ft.commit(); // select the new transaction in the ledger if (!d->m_currentAccount.id().isEmpty()) slotLedgerSelected(d->m_currentAccount.id(), lt.id()); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(this, i18n("Unable to duplicate transaction(s)"), QString::fromLatin1(e.what())); } // switch off the progress bar emit selectByVariant(QVariantList {QVariant(-1), QVariant(-1)}, eView::Intent::ReportProgress); } } void KGlobalLedgerView::slotEnterTransaction() { Q_D(KGlobalLedgerView); // since we jump here via code, we have to make sure to react only // if the action is enabled if (pActions[Action::EnterTransaction]->isEnabled()) { // disable the action while we process it to make sure it's processed only once since // d->m_transactionEditor->enterTransactions(newId) will run QCoreApplication::processEvents // we could end up here twice which will cause a crash slotUpdateActions() will enable the action again pActions[Action::EnterTransaction]->setEnabled(false); if (d->m_transactionEditor) { QString accountId = d->m_currentAccount.id(); QString newId; connect(d->m_transactionEditor.data(), &TransactionEditor::balanceWarning, d->m_balanceWarning.data(), &KBalanceWarning::slotShowMessage); if (d->m_transactionEditor->enterTransactions(newId)) { KMyMoneyPayeeCombo* payeeEdit = dynamic_cast(d->m_transactionEditor->haveWidget("payee")); if (payeeEdit && !newId.isEmpty()) { d->m_lastPayeeEnteredId = payeeEdit->selectedItem(); } d->deleteTransactionEditor(); } if (!newId.isEmpty()) { slotLedgerSelected(accountId, newId); } } updateLedgerActionsInternal(); } } void KGlobalLedgerView::slotAcceptTransaction() { Q_D(KGlobalLedgerView); KMyMoneyRegister::SelectedTransactions list = d->m_selectedTransactions; KMyMoneyRegister::SelectedTransactions::const_iterator it_t; int cnt = list.count(); int i = 0; emit selectByVariant(QVariantList {QVariant(0), QVariant(cnt)}, eView::Intent::ReportProgress); MyMoneyFileTransaction ft; try { for (it_t = list.constBegin(); it_t != list.constEnd(); ++it_t) { // reload transaction in case it got changed during the course of this loop MyMoneyTransaction t = MyMoneyFile::instance()->transaction((*it_t).transaction().id()); if (t.isImported()) { t.setImported(false); if (!d->m_currentAccount.id().isEmpty()) { foreach (const auto split, t.splits()) { if (split.accountId() == d->m_currentAccount.id()) { if (split.reconcileFlag() == eMyMoney::Split::State::NotReconciled) { MyMoneySplit s = split; s.setReconcileFlag(eMyMoney::Split::State::Cleared); t.modifySplit(s); } } } } MyMoneyFile::instance()->modifyTransaction(t); } if ((*it_t).split().isMatched()) { // reload split in case it got changed during the course of this loop MyMoneySplit s = t.splitById((*it_t).split().id()); TransactionMatcher matcher(d->m_currentAccount); matcher.accept(t, s); } emit selectByVariant(QVariantList {QVariant(i++), QVariant(0)}, eView::Intent::ReportProgress); } emit selectByVariant(QVariantList {QVariant(-1), QVariant(-1)}, eView::Intent::ReportProgress); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(this, i18n("Unable to accept transaction"), QString::fromLatin1(e.what())); } } void KGlobalLedgerView::slotCancelTransaction() { Q_D(KGlobalLedgerView); // since we jump here via code, we have to make sure to react only // if the action is enabled if (pActions[Action::CancelTransaction]->isEnabled()) { // make sure, we block the enter function pActions[Action::EnterTransaction]->setEnabled(false); // qDebug("KMyMoneyApp::slotTransactionsCancel"); d->deleteTransactionEditor(); updateLedgerActions(d->m_selectedTransactions); emit selectByVariant(QVariantList {QVariant::fromValue(d->m_selectedTransactions)}, eView::Intent::SelectRegisterTransactions); } } void KGlobalLedgerView::slotEditSplits() { Q_D(KGlobalLedgerView); // since we jump here via code, we have to make sure to react only // if the action is enabled if (pActions[Action::EditSplits]->isEnabled()) { // as soon as we edit a transaction, we don't remember the last payee entered d->m_lastPayeeEnteredId.clear(); d->m_transactionEditor = d->startEdit(d->m_selectedTransactions); updateLedgerActions(d->m_selectedTransactions); emit selectByVariant(QVariantList {QVariant::fromValue(d->m_selectedTransactions)}, eView::Intent::SelectRegisterTransactions); if (d->m_transactionEditor) { KMyMoneyMVCCombo::setSubstringSearchForChildren(this/*d->m_myMoneyView*/, !KMyMoneySettings::stringMatchFromStart()); if (d->m_transactionEditor->slotEditSplits() == QDialog::Accepted) { MyMoneyFileTransaction ft; try { QString id; connect(d->m_transactionEditor.data(), &TransactionEditor::balanceWarning, d->m_balanceWarning.data(), &KBalanceWarning::slotShowMessage); d->m_transactionEditor->enterTransactions(id); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(this, i18n("Unable to modify transaction"), QString::fromLatin1(e.what())); } } } d->deleteTransactionEditor(); updateLedgerActions(d->m_selectedTransactions); emit selectByVariant(QVariantList {QVariant::fromValue(d->m_selectedTransactions)}, eView::Intent::SelectRegisterTransactions); } } void KGlobalLedgerView::slotCopySplits() { Q_D(KGlobalLedgerView); const auto file = MyMoneyFile::instance(); if (d->m_selectedTransactions.count() >= 2) { int singleSplitTransactions = 0; int multipleSplitTransactions = 0; KMyMoneyRegister::SelectedTransaction selectedSourceTransaction; foreach (const auto& st, d->m_selectedTransactions) { switch (st.transaction().splitCount()) { case 0: break; case 1: singleSplitTransactions++; break; default: selectedSourceTransaction = st; multipleSplitTransactions++; break; } } if (singleSplitTransactions > 0 && multipleSplitTransactions == 1) { MyMoneyFileTransaction ft; try { const auto& sourceTransaction = selectedSourceTransaction.transaction(); const auto& sourceSplit = selectedSourceTransaction.split(); foreach (const KMyMoneyRegister::SelectedTransaction& st, d->m_selectedTransactions) { auto t = st.transaction(); // don't process the source transaction if (sourceTransaction.id() == t.id()) { continue; } const auto& baseSplit = st.split(); if (t.splitCount() == 1) { foreach (const auto& split, sourceTransaction.splits()) { // Don't copy the source split, as we already have that // as part of the destination transaction if (split.id() == sourceSplit.id()) { continue; } MyMoneySplit sp(split); // clear the ID and reconciliation state sp.clearId(); sp.setReconcileFlag(eMyMoney::Split::State::NotReconciled); sp.setReconcileDate(QDate()); // in case it is a simple transaction consisting of two splits, // we can adjust the share and value part of the second split we // just created. We need to keep a possible price in mind in case // of different currencies if (sourceTransaction.splitCount() == 2) { sp.setValue(-baseSplit.value()); sp.setShares(-(baseSplit.shares() * baseSplit.price())); } t.addSplit(sp); } file->modifyTransaction(t); } } ft.commit(); } catch (const MyMoneyException &) { qDebug() << "transactionCopySplits() failed"; } } } } void KGlobalLedgerView::slotGoToPayee() { Q_D(KGlobalLedgerView); if (!d->m_payeeGoto.isEmpty()) { try { QString transactionId; if (d->m_selectedTransactions.count() == 1) { transactionId = d->m_selectedTransactions[0].transaction().id(); } // make sure to pass copies, as d->myMoneyView->slotPayeeSelected() overrides // d->m_payeeGoto and d->m_currentAccount while calling slotUpdateActions() QString payeeId = d->m_payeeGoto; QString accountId = d->m_currentAccount.id(); emit selectByVariant(QVariantList {QVariant(payeeId), QVariant(accountId), QVariant(transactionId)}, eView::Intent::ShowPayee); // emit openPayeeRequested(payeeId, accountId, transactionId); } catch (const MyMoneyException &) { } } } void KGlobalLedgerView::slotGoToAccount() { Q_D(KGlobalLedgerView); if (!d->m_accountGoto.isEmpty()) { try { QString transactionId; if (d->m_selectedTransactions.count() == 1) { transactionId = d->m_selectedTransactions[0].transaction().id(); } // make sure to pass a copy, as d->myMoneyView->slotLedgerSelected() overrides // d->m_accountGoto while calling slotUpdateActions() slotLedgerSelected(d->m_accountGoto, transactionId); } catch (const MyMoneyException &) { } } } void KGlobalLedgerView::slotMatchTransactions() { Q_D(KGlobalLedgerView); // if the menu action is retrieved it can contain an '&' character for the accelerator causing the comparison to fail if not removed QString transactionActionText = pActions[Action::MatchTransaction]->text(); transactionActionText.remove('&'); if (transactionActionText == i18nc("Button text for match transaction", "Match")) d->transactionMatch(); else d->transactionUnmatch(); } void KGlobalLedgerView::slotCombineTransactions() { qDebug("slotTransactionCombine() not implemented yet"); } void KGlobalLedgerView::slotToggleReconciliationFlag() { Q_D(KGlobalLedgerView); d->markTransaction(eMyMoney::Split::State::Unknown); } void KGlobalLedgerView::slotMarkCleared() { Q_D(KGlobalLedgerView); d->markTransaction(eMyMoney::Split::State::Cleared); } void KGlobalLedgerView::slotMarkReconciled() { Q_D(KGlobalLedgerView); d->markTransaction(eMyMoney::Split::State::Reconciled); } void KGlobalLedgerView::slotMarkNotReconciled() { Q_D(KGlobalLedgerView); d->markTransaction(eMyMoney::Split::State::NotReconciled); } void KGlobalLedgerView::slotSelectAllTransactions() { Q_D(KGlobalLedgerView); if(d->m_needLoad) d->init(); d->m_register->clearSelection(); KMyMoneyRegister::RegisterItem* p = d->m_register->firstItem(); while (p) { KMyMoneyRegister::Transaction* t = dynamic_cast(p); if (t) { if (t->isVisible() && t->isSelectable() && !t->isScheduled() && !t->id().isEmpty()) { t->setSelected(true); } } p = p->nextItem(); } // this is here only to re-paint the items without selecting anything because the data (including the selection) is not really held in the model right now d->m_register->selectAll(); // inform everyone else about the selected items KMyMoneyRegister::SelectedTransactions list(d->m_register); updateLedgerActions(list); emit selectByVariant(QVariantList {QVariant::fromValue(list)}, eView::Intent::SelectRegisterTransactions); } void KGlobalLedgerView::slotCreateScheduledTransaction() { Q_D(KGlobalLedgerView); if (d->m_selectedTransactions.count() == 1) { // make sure to have the current selected split as first split in the schedule MyMoneyTransaction t = d->m_selectedTransactions[0].transaction(); MyMoneySplit s = d->m_selectedTransactions[0].split(); QString splitId = s.id(); s.clearId(); s.setReconcileFlag(eMyMoney::Split::State::NotReconciled); s.setReconcileDate(QDate()); t.removeSplits(); t.addSplit(s); foreach (const auto split, d->m_selectedTransactions[0].transaction().splits()) { if (split.id() != splitId) { MyMoneySplit s0 = split; s0.clearId(); s0.setReconcileFlag(eMyMoney::Split::State::NotReconciled); s0.setReconcileDate(QDate()); t.addSplit(s0); } } KEditScheduleDlg::newSchedule(t, eMyMoney::Schedule::Occurrence::Monthly); } } void KGlobalLedgerView::slotAssignNumber() { Q_D(KGlobalLedgerView); if (d->m_transactionEditor) d->m_transactionEditor->assignNextNumber(); } void KGlobalLedgerView::slotStartReconciliation() { Q_D(KGlobalLedgerView); // we cannot reconcile standard accounts if (!MyMoneyFile::instance()->isStandardAccount(d->m_currentAccount.id())) emit selectByObject(d->m_currentAccount, eView::Intent::StartEnteringOverdueScheduledTransactions); // asynchronous call to KScheduledView::slotEnterOverdueSchedules is made here // after that all activity should be continued in KGlobalLedgerView::slotContinueReconciliation() } void KGlobalLedgerView::slotFinishReconciliation() { Q_D(KGlobalLedgerView); const auto file = MyMoneyFile::instance(); if (!d->m_reconciliationAccount.id().isEmpty()) { // retrieve list of all transactions that are not reconciled or cleared QList > transactionList; MyMoneyTransactionFilter filter(d->m_reconciliationAccount.id()); filter.addState((int)eMyMoney::TransactionFilter::State::Cleared); filter.addState((int)eMyMoney::TransactionFilter::State::NotReconciled); filter.setDateFilter(QDate(), d->m_endingBalanceDlg->statementDate()); filter.setConsiderCategory(false); filter.setReportAllSplits(true); file->transactionList(transactionList, filter); auto balance = MyMoneyFile::instance()->balance(d->m_reconciliationAccount.id(), d->m_endingBalanceDlg->statementDate()); MyMoneyMoney actBalance, clearedBalance; actBalance = clearedBalance = balance; // walk the list of transactions to figure out the balance(s) for (auto it = transactionList.constBegin(); it != transactionList.constEnd(); ++it) { if ((*it).second.reconcileFlag() == eMyMoney::Split::State::NotReconciled) { clearedBalance -= (*it).second.shares(); } } if (d->m_endingBalanceDlg->endingBalance() != clearedBalance) { auto message = i18n("You are about to finish the reconciliation of this account with a difference between your bank statement and the transactions marked as cleared.\n" "Are you sure you want to finish the reconciliation?"); if (KMessageBox::questionYesNo(this, message, i18n("Confirm end of reconciliation"), KStandardGuiItem::yes(), KStandardGuiItem::no()) == KMessageBox::No) return; } MyMoneyFileTransaction ft; // refresh object d->m_reconciliationAccount = file->account(d->m_reconciliationAccount.id()); // Turn off reconciliation mode // Models::instance()->accountsModel()->slotReconcileAccount(MyMoneyAccount(), QDate(), MyMoneyMoney()); // slotSetReconcileAccount(MyMoneyAccount(), QDate(), MyMoneyMoney()); // d->m_myMoneyView->finishReconciliation(d->m_reconciliationAccount); // only update the last statement balance here, if we haven't a newer one due // to download of online statements. if (d->m_reconciliationAccount.value("lastImportedTransactionDate").isEmpty() || QDate::fromString(d->m_reconciliationAccount.value("lastImportedTransactionDate"), Qt::ISODate) < d->m_endingBalanceDlg->statementDate()) { d->m_reconciliationAccount.setValue("lastStatementBalance", d->m_endingBalanceDlg->endingBalance().toString()); // in case we override the last statement balance here, we have to make sure // that we don't show the online balance anymore, as it might be different d->m_reconciliationAccount.deletePair("lastImportedTransactionDate"); } d->m_reconciliationAccount.setLastReconciliationDate(d->m_endingBalanceDlg->statementDate()); // keep a record of this reconciliation d->m_reconciliationAccount.addReconciliation(d->m_endingBalanceDlg->statementDate(), d->m_endingBalanceDlg->endingBalance()); d->m_reconciliationAccount.deletePair("lastReconciledBalance"); d->m_reconciliationAccount.deletePair("statementBalance"); d->m_reconciliationAccount.deletePair("statementDate"); try { // update the account data file->modifyAccount(d->m_reconciliationAccount); /* // collect the list of cleared splits for this account filter.clear(); filter.addAccount(d->m_reconciliationAccount.id()); filter.addState(eMyMoney::TransactionFilter::Cleared); filter.setConsiderCategory(false); filter.setReportAllSplits(true); file->transactionList(transactionList, filter); */ // walk the list of transactions/splits and mark the cleared ones as reconciled for (auto it = transactionList.begin(); it != transactionList.end(); ++it) { MyMoneySplit sp = (*it).second; // skip the ones that are not marked cleared if (sp.reconcileFlag() != eMyMoney::Split::State::Cleared) continue; // always retrieve a fresh copy of the transaction because we // might have changed it already with another split MyMoneyTransaction t = file->transaction((*it).first.id()); sp.setReconcileFlag(eMyMoney::Split::State::Reconciled); sp.setReconcileDate(d->m_endingBalanceDlg->statementDate()); t.modifySplit(sp); // update the engine ... file->modifyTransaction(t); // ... and the list (*it) = qMakePair(t, sp); } ft.commit(); // reload account data from engine as the data might have changed in the meantime d->m_reconciliationAccount = file->account(d->m_reconciliationAccount.id()); /** * This signal is emitted when an account has been successfully reconciled * and all transactions are updated in the engine. It can be used by plugins * to create reconciliation reports. * * @param account the account data * @param date the reconciliation date as provided through the dialog * @param startingBalance the starting balance as provided through the dialog * @param endingBalance the ending balance as provided through the dialog * @param transactionList reference to QList of QPair containing all * transaction/split pairs processed by the reconciliation. */ emit selectByVariant(QVariantList { QVariant::fromValue(d->m_reconciliationAccount), QVariant::fromValue(d->m_endingBalanceDlg->statementDate()), QVariant::fromValue(d->m_endingBalanceDlg->previousBalance()), QVariant::fromValue(d->m_endingBalanceDlg->endingBalance()), QVariant::fromValue(transactionList) }, eView::Intent::AccountReconciled); } catch (const MyMoneyException &) { qDebug("Unexpected exception when setting cleared to reconcile"); } // Turn off reconciliation mode Models::instance()->accountsModel()->slotReconcileAccount(MyMoneyAccount(), QDate(), MyMoneyMoney()); slotSetReconcileAccount(MyMoneyAccount(), QDate(), MyMoneyMoney()); } // Turn off reconciliation mode d->m_reconciliationAccount = MyMoneyAccount(); updateActions(d->m_currentAccount); updateLedgerActionsInternal(); d->loadView(); // slotUpdateActions(); } void KGlobalLedgerView::slotPostponeReconciliation() { Q_D(KGlobalLedgerView); MyMoneyFileTransaction ft; const auto file = MyMoneyFile::instance(); if (!d->m_reconciliationAccount.id().isEmpty()) { // refresh object d->m_reconciliationAccount = file->account(d->m_reconciliationAccount.id()); // Turn off reconciliation mode // Models::instance()->accountsModel()->slotReconcileAccount(MyMoneyAccount(), QDate(), MyMoneyMoney()); // slotSetReconcileAccount(MyMoneyAccount(), QDate(), MyMoneyMoney()); // d->m_myMoneyView->finishReconciliation(d->m_reconciliationAccount); d->m_reconciliationAccount.setValue("lastReconciledBalance", d->m_endingBalanceDlg->previousBalance().toString()); d->m_reconciliationAccount.setValue("statementBalance", d->m_endingBalanceDlg->endingBalance().toString()); d->m_reconciliationAccount.setValue("statementDate", d->m_endingBalanceDlg->statementDate().toString(Qt::ISODate)); try { file->modifyAccount(d->m_reconciliationAccount); ft.commit(); d->m_reconciliationAccount = MyMoneyAccount(); updateActions(d->m_currentAccount); updateLedgerActionsInternal(); // slotUpdateActions(); } catch (const MyMoneyException &) { qDebug("Unexpected exception when setting last reconcile info into account"); ft.rollback(); d->m_reconciliationAccount = file->account(d->m_reconciliationAccount.id()); } // Turn off reconciliation mode Models::instance()->accountsModel()->slotReconcileAccount(MyMoneyAccount(), QDate(), MyMoneyMoney()); slotSetReconcileAccount(MyMoneyAccount(), QDate(), MyMoneyMoney()); d->loadView(); } } void KGlobalLedgerView::slotOpenAccount() { Q_D(KGlobalLedgerView); if (!MyMoneyFile::instance()->isStandardAccount(d->m_currentAccount.id())) slotLedgerSelected(d->m_currentAccount.id(), QString()); } void KGlobalLedgerView::slotFindTransaction() { Q_D(KGlobalLedgerView); if (!d->m_searchDlg) { d->m_searchDlg = new KFindTransactionDlg(this); connect(d->m_searchDlg, &QObject::destroyed, this, &KGlobalLedgerView::slotCloseSearchDialog); connect(d->m_searchDlg, &KFindTransactionDlg::transactionSelected, this, &KGlobalLedgerView::slotLedgerSelected); } d->m_searchDlg->show(); d->m_searchDlg->raise(); d->m_searchDlg->activateWindow(); } void KGlobalLedgerView::slotCloseSearchDialog() { Q_D(KGlobalLedgerView); if (d->m_searchDlg) d->m_searchDlg->deleteLater(); d->m_searchDlg = nullptr; } void KGlobalLedgerView::slotStatusMsg(const QString& txt) { emit selectByVariant(QVariantList {QVariant(txt)}, eView::Intent::ReportProgressMessage); } void KGlobalLedgerView::slotStatusProgress(int cnt, int base) { emit selectByVariant(QVariantList {QVariant(cnt), QVariant(base)}, eView::Intent::ReportProgress); } void KGlobalLedgerView::slotTransactionsSelected(const KMyMoneyRegister::SelectedTransactions& list) { updateLedgerActions(list); emit selectByVariant(QVariantList {QVariant::fromValue(list)}, eView::Intent::SelectRegisterTransactions); } diff --git a/kmymoney/views/kpayeesview.cpp b/kmymoney/views/kpayeesview.cpp index eee3f7192..19828f386 100644 --- a/kmymoney/views/kpayeesview.cpp +++ b/kmymoney/views/kpayeesview.cpp @@ -1,719 +1,721 @@ /*************************************************************************** kpayeesview.cpp --------------- begin : Thu Jan 24 2002 copyright : (C) 2000-2002 by Michael Edwardes Javier Campos Morales Felix Rodriguez John C Thomas Baumgart Kevin Tambascio Andreas Nicolai (C) 2017 by Łukasz Wojniłowicz ***************************************************************************/ /*************************************************************************** * * * 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 "kpayeesview_p.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include #include "ui_kpayeesview.h" #include "kmymoneyviewbase_p.h" #include "kpayeeidentifierview.h" #include "mymoneypayee.h" #include "mymoneyexception.h" #include "mymoneyfile.h" #include "mymoneyaccount.h" #include "mymoneymoney.h" #include "mymoneytransactionfilter.h" #include "kmymoneysettings.h" #include "models.h" #include "accountsmodel.h" #include "mymoneysecurity.h" #include "mymoneycontact.h" #include "mymoneyprice.h" #include "mymoneysplit.h" #include "mymoneytransaction.h" #include "icons/icons.h" #include "transaction.h" #include "widgetenums.h" #include "mymoneyenums.h" #include "modelenums.h" #include "menuenums.h" using namespace Icons; // *** KPayeesView Implementation *** KPayeesView::KPayeesView(QWidget *parent) : KMyMoneyViewBase(*new KPayeesViewPrivate(this), parent) { connect(pActions[eMenu::Action::NewPayee], &QAction::triggered, this, &KPayeesView::slotNewPayee); connect(pActions[eMenu::Action::RenamePayee], &QAction::triggered, this, &KPayeesView::slotRenamePayee); connect(pActions[eMenu::Action::DeletePayee], &QAction::triggered, this, &KPayeesView::slotDeletePayee); connect(pActions[eMenu::Action::MergePayee], &QAction::triggered, this, &KPayeesView::slotMergePayee); } KPayeesView::~KPayeesView() { } void KPayeesView::slotChooseDefaultAccount() { Q_D(KPayeesView); MyMoneyFile* file = MyMoneyFile::instance(); QMap account_count; KMyMoneyRegister::RegisterItem* item = d->ui->m_register->firstItem(); while (item) { //only walk through selectable items. eg. transactions and not group markers if (item->isSelectable()) { auto t = dynamic_cast(item); if (!t) return; MyMoneySplit s = t->transaction().splitByPayee(d->m_payee.id()); const MyMoneyAccount& acc = file->account(s.accountId()); QString txt; if (s.action() != MyMoneySplit::actionName(eMyMoney::Split::Action::Amortization) && acc.accountType() != eMyMoney::Account::Type::AssetLoan && !file->isTransfer(t->transaction()) && t->transaction().splitCount() == 2) { MyMoneySplit s0 = t->transaction().splitByAccount(s.accountId(), false); if (account_count.contains(s0.accountId())) { account_count[s0.accountId()]++; } else { account_count[s0.accountId()] = 1; } } } item = item->nextItem(); } QMap::Iterator most_frequent, iter; most_frequent = account_count.begin(); for (iter = account_count.begin(); iter != account_count.end(); ++iter) { if (iter.value() > most_frequent.value()) { most_frequent = iter; } } if (most_frequent != account_count.end()) { d->ui->checkEnableDefaultCategory->setChecked(true); d->ui->comboDefaultCategory->setSelected(most_frequent.key()); d->setDirty(true); } } void KPayeesView::slotClosePayeeIdentifierSource() { Q_D(KPayeesView); if (!d->m_needLoad) d->ui->payeeIdentifiers->closeSource(); } void KPayeesView::slotSelectByVariant(const QVariantList& variant, eView::Intent intent) { switch (intent) { case eView::Intent::ShowPayee: if (variant.count() == 3) slotSelectPayeeAndTransaction(variant.at(0).toString(), variant.at(1).toString(), variant.at(2).toString()); break; default: break; } } void KPayeesView::slotStartRename(QListWidgetItem* item) { Q_D(KPayeesView); d->m_allowEditing = true; d->ui->m_payeesList->editItem(item); } // This variant is only called when a single payee is selected and renamed. void KPayeesView::slotRenameSinglePayee(QListWidgetItem* p) { Q_D(KPayeesView); //if there is no current item selected, exit if (d->m_allowEditing == false || !d->ui->m_payeesList->currentItem() || p != d->ui->m_payeesList->currentItem()) return; //qDebug() << "[KPayeesView::slotRenamePayee]"; // create a copy of the new name without appended whitespaces QString new_name = p->text(); if (d->m_payee.name() != new_name) { MyMoneyFileTransaction ft; try { // check if we already have a payee with the new name try { // this function call will throw an exception, if the payee // hasn't been found. MyMoneyFile::instance()->payeeByName(new_name); // the name already exists, ask the user whether he's sure to keep the name if (KMessageBox::questionYesNo(this, i18n("A payee with the name '%1' already exists. It is not advisable to have " "multiple payees with the same identification name. Are you sure you would like " "to rename the payee?", new_name)) != KMessageBox::Yes) { p->setText(d->m_payee.name()); return; } } catch (const MyMoneyException &) { // all ok, the name is unique } d->m_payee.setName(new_name); d->m_newName = new_name; MyMoneyFile::instance()->modifyPayee(d->m_payee); // the above call to modifyPayee will reload the view so // all references and pointers to the view have to be // re-established. // make sure, that the record is visible even if it moved // out of sight due to the rename operation d->ensurePayeeVisible(d->m_payee.id()); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(this, i18n("Unable to modify payee"), QString::fromLatin1(e.what())); } } else { p->setText(new_name); } } void KPayeesView::slotSelectPayee(QListWidgetItem* cur, QListWidgetItem* prev) { Q_D(KPayeesView); Q_UNUSED(cur); Q_UNUSED(prev); d->m_allowEditing = false; } void KPayeesView::slotSelectPayee() { Q_D(KPayeesView); // check if the content of a currently selected payee was modified // and ask to store the data if (d->isDirty()) { if (KMessageBox::questionYesNo(this, i18n("Do you want to save the changes for %1?", d->m_newName), i18n("Save changes")) == KMessageBox::Yes) { d->m_inSelection = true; slotUpdatePayee(); d->m_inSelection = false; } } // make sure we always clear the selected list when listing again d->m_selectedPayeesList.clear(); // loop over all payees and count the number of payees, also // obtain last selected payee d->selectedPayees(d->m_selectedPayeesList); updatePayeeActions(d->m_selectedPayeesList); emit selectObjects(d->m_selectedPayeesList); if (d->m_selectedPayeesList.isEmpty()) { d->ui->m_tabWidget->setEnabled(false); // disable tab widget d->ui->m_balanceLabel->hide(); d->ui->m_deleteButton->setEnabled(false); //disable delete, rename and merge buttons d->ui->m_renameButton->setEnabled(false); d->ui->m_mergeButton->setEnabled(false); d->clearItemData(); d->m_payee = MyMoneyPayee(); d->ui->m_syncAddressbook->setEnabled(false); return; // make sure we don't access an undefined payee } d->ui->m_deleteButton->setEnabled(true); //re-enable delete button d->ui->m_syncAddressbook->setEnabled(true); // if we have multiple payees selected, clear and disable the payee information if (d->m_selectedPayeesList.count() > 1) { d->ui->m_tabWidget->setEnabled(false); // disable tab widget d->ui->m_renameButton->setEnabled(false); // disable also the rename button d->ui->m_mergeButton->setEnabled(true); d->ui->m_balanceLabel->hide(); d->clearItemData(); } else { d->ui->m_mergeButton->setEnabled(false); d->ui->m_renameButton->setEnabled(true); } // otherwise we have just one selected, enable payee information widget d->ui->m_tabWidget->setEnabled(true); d->ui->m_balanceLabel->show(); // as of now we are updating only the last selected payee, and until // selection mode of the QListView has been changed to Extended, this // will also be the only selection and behave exactly as before - Andreas try { d->m_payee = d->m_selectedPayeesList[0]; d->m_newName = d->m_payee.name(); d->ui->addressEdit->setEnabled(true); d->ui->addressEdit->setText(d->m_payee.address()); d->ui->postcodeEdit->setEnabled(true); d->ui->postcodeEdit->setText(d->m_payee.postcode()); d->ui->telephoneEdit->setEnabled(true); d->ui->telephoneEdit->setText(d->m_payee.telephone()); d->ui->emailEdit->setEnabled(true); d->ui->emailEdit->setText(d->m_payee.email()); d->ui->notesEdit->setText(d->m_payee.notes()); QStringList keys; bool ignorecase = false; auto type = d->m_payee.matchData(ignorecase, keys); d->ui->matchTypeCombo->setCurrentIndex(d->ui->matchTypeCombo->findData(static_cast(type))); d->ui->matchKeyEditList->clear(); d->ui->matchKeyEditList->insertStringList(keys); d->ui->checkMatchIgnoreCase->setChecked(ignorecase); d->ui->checkEnableDefaultCategory->setChecked(d->m_payee.defaultAccountEnabled()); d->ui->comboDefaultCategory->setSelected(d->m_payee.defaultAccountId()); d->ui->payeeIdentifiers->setSource(d->m_payee); slotPayeeDataChanged(); d->showTransactions(); } catch (const MyMoneyException &e) { qDebug("exception during display of payee: %s", e.what()); d->ui->m_register->clear(); d->m_selectedPayeesList.clear(); d->m_payee = MyMoneyPayee(); } d->m_allowEditing = true; } void KPayeesView::slotKeyListChanged() { Q_D(KPayeesView); bool rc = false; bool ignorecase = false; QStringList keys; d->m_payee.matchData(ignorecase, keys); if (static_cast(d->ui->matchTypeCombo->currentData().toUInt()) == eMyMoney::Payee::MatchType::Key) { rc |= (keys != d->ui->matchKeyEditList->items()); } d->setDirty(rc); } void KPayeesView::slotPayeeDataChanged() { Q_D(KPayeesView); bool rc = false; if (d->ui->m_tabWidget->isEnabled()) { rc |= ((d->m_payee.email().isEmpty() != d->ui->emailEdit->text().isEmpty()) || (!d->ui->emailEdit->text().isEmpty() && d->m_payee.email() != d->ui->emailEdit->text())); rc |= ((d->m_payee.address().isEmpty() != d->ui->addressEdit->toPlainText().isEmpty()) || (!d->ui->addressEdit->toPlainText().isEmpty() && d->m_payee.address() != d->ui->addressEdit->toPlainText())); rc |= ((d->m_payee.postcode().isEmpty() != d->ui->postcodeEdit->text().isEmpty()) || (!d->ui->postcodeEdit->text().isEmpty() && d->m_payee.postcode() != d->ui->postcodeEdit->text())); rc |= ((d->m_payee.telephone().isEmpty() != d->ui->telephoneEdit->text().isEmpty()) || (!d->ui->telephoneEdit->text().isEmpty() && d->m_payee.telephone() != d->ui->telephoneEdit->text())); rc |= ((d->m_payee.name().isEmpty() != d->m_newName.isEmpty()) || (!d->m_newName.isEmpty() && d->m_payee.name() != d->m_newName)); rc |= ((d->m_payee.notes().isEmpty() != d->ui->notesEdit->toPlainText().isEmpty()) || (!d->ui->notesEdit->toPlainText().isEmpty() && d->m_payee.notes() != d->ui->notesEdit->toPlainText())); bool ignorecase = false; QStringList keys; auto type = d->m_payee.matchData(ignorecase, keys); rc |= (static_cast(type) != d->ui->matchTypeCombo->currentData().toUInt()); d->ui->checkMatchIgnoreCase->setEnabled(false); d->ui->matchKeyEditList->setEnabled(false); if (static_cast(d->ui->matchTypeCombo->currentData().toUInt()) != eMyMoney::Payee::MatchType::Disabled) { d->ui->checkMatchIgnoreCase->setEnabled(true); // if we turn matching on, we default to 'ignore case' // TODO maybe make the default a user option if (type == eMyMoney::Payee::MatchType::Disabled && static_cast(d->ui->matchTypeCombo->currentData().toUInt()) != eMyMoney::Payee::MatchType::Disabled) d->ui->checkMatchIgnoreCase->setChecked(true); rc |= (ignorecase != d->ui->checkMatchIgnoreCase->isChecked()); if (static_cast(d->ui->matchTypeCombo->currentData().toUInt()) == eMyMoney::Payee::MatchType::Key) { d->ui->matchKeyEditList->setEnabled(true); rc |= (keys != d->ui->matchKeyEditList->items()); } } rc |= (d->ui->checkEnableDefaultCategory->isChecked() != d->m_payee.defaultAccountEnabled()); if (d->ui->checkEnableDefaultCategory->isChecked()) { d->ui->comboDefaultCategory->setEnabled(true); d->ui->labelDefaultCategory->setEnabled(true); // this is only going to understand the first in the list of selected accounts if (d->ui->comboDefaultCategory->getSelected().isEmpty()) { rc |= !d->m_payee.defaultAccountId().isEmpty(); } else { QString temp = d->ui->comboDefaultCategory->getSelected(); rc |= (temp.isEmpty() != d->m_payee.defaultAccountId().isEmpty()) || (!d->m_payee.defaultAccountId().isEmpty() && temp != d->m_payee.defaultAccountId()); } } else { d->ui->comboDefaultCategory->setEnabled(false); d->ui->labelDefaultCategory->setEnabled(false); } rc |= (d->m_payee.payeeIdentifiers() != d->ui->payeeIdentifiers->identifiers()); } d->setDirty(rc); } void KPayeesView::slotUpdatePayee() { Q_D(KPayeesView); if (d->isDirty()) { MyMoneyFileTransaction ft; d->setDirty(false); try { d->m_payee.setName(d->m_newName); d->m_payee.setAddress(d->ui->addressEdit->toPlainText()); d->m_payee.setPostcode(d->ui->postcodeEdit->text()); d->m_payee.setTelephone(d->ui->telephoneEdit->text()); d->m_payee.setEmail(d->ui->emailEdit->text()); d->m_payee.setNotes(d->ui->notesEdit->toPlainText()); d->m_payee.setMatchData(static_cast(d->ui->matchTypeCombo->currentData().toUInt()), d->ui->checkMatchIgnoreCase->isChecked(), d->ui->matchKeyEditList->items()); d->m_payee.setDefaultAccountId(); d->m_payee.resetPayeeIdentifiers(d->ui->payeeIdentifiers->identifiers()); if (d->ui->checkEnableDefaultCategory->isChecked()) { QString temp; if (!d->ui->comboDefaultCategory->getSelected().isEmpty()) { temp = d->ui->comboDefaultCategory->getSelected(); d->m_payee.setDefaultAccountId(temp); } } MyMoneyFile::instance()->modifyPayee(d->m_payee); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(this, i18n("Unable to modify payee"), QString::fromLatin1(e.what())); } } } void KPayeesView::slotSyncAddressBook() { Q_D(KPayeesView); if (d->m_payeeRows.isEmpty()) { // empty list means no syncing is pending... foreach (auto item, d->ui->m_payeesList->selectedItems()) { d->m_payeeRows.append(d->ui->m_payeesList->row(item)); // ...so initialize one } d->ui->m_payeesList->clearSelection(); // otherwise slotSelectPayee will be run after every payee update // d->ui->m_syncAddressbook->setEnabled(false); // disallow concurrent syncs } if (d->m_payeeRows.count() <= d->m_payeeRow) { if (auto item = dynamic_cast(d->ui->m_payeesList->currentItem())) { // update ui if something is selected d->m_payee = item->payee(); d->ui->addressEdit->setText(d->m_payee.address()); d->ui->postcodeEdit->setText(d->m_payee.postcode()); d->ui->telephoneEdit->setText(d->m_payee.telephone()); } d->m_payeeRows.clear(); // that means end of sync d->m_payeeRow = 0; return; } if (auto item = dynamic_cast(d->ui->m_payeesList->item(d->m_payeeRows.at(d->m_payeeRow)))) d->m_payee = item->payee(); ++d->m_payeeRow; d->m_contact->fetchContact(d->m_payee.email()); // search for payee's data in addressbook and receive it in slotContactFetched } void KPayeesView::slotContactFetched(const ContactData &identity) { Q_D(KPayeesView); if (!identity.email.isEmpty()) { // empty e-mail means no identity fetched QString txt; if (!identity.street.isEmpty()) txt.append(identity.street + '\n'); if (!identity.locality.isEmpty()) { txt.append(identity.locality); if (!identity.postalCode.isEmpty()) txt.append(' ' + identity.postalCode + '\n'); else txt.append('\n'); } if (!identity.country.isEmpty()) txt.append(identity.country + '\n'); if (!txt.isEmpty() && d->m_payee.address().compare(txt) != 0) d->m_payee.setAddress(txt); if (!identity.postalCode.isEmpty() && d->m_payee.postcode().compare(identity.postalCode) != 0) d->m_payee.setPostcode(identity.postalCode); if (!identity.phoneNumber.isEmpty() && d->m_payee.telephone().compare(identity.phoneNumber) != 0) d->m_payee.setTelephone(identity.phoneNumber); MyMoneyFileTransaction ft; try { MyMoneyFile::instance()->modifyPayee(d->m_payee); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(this, i18n("Unable to modify payee"), QString::fromLatin1(e.what())); } } slotSyncAddressBook(); // process next payee } void KPayeesView::slotSendMail() { Q_D(KPayeesView); QRegularExpression re(".+@.+"); if (re.match(d->m_payee.email()).hasMatch()) QDesktopServices::openUrl(QUrl(QStringLiteral("mailto:?to=") + d->m_payee.email(), QUrl::TolerantMode)); } void KPayeesView::executeCustomAction(eView::Action action) { switch(action) { case eView::Action::Refresh: refresh(); break; case eView::Action::SetDefaultFocus: { Q_D(KPayeesView); QTimer::singleShot(0, d->m_searchWidget, SLOT(setFocus())); } break; case eView::Action::ClosePayeeIdentifierSource: slotClosePayeeIdentifierSource(); break; default: break; } } void KPayeesView::refresh() { Q_D(KPayeesView); if (isVisible()) { if (d->m_inSelection) { QTimer::singleShot(0, this, SLOT(refresh())); } else { d->loadPayees(); d->m_needsRefresh = false; } } else { d->m_needsRefresh = true; } } void KPayeesView::showEvent(QShowEvent* event) { - Q_D(KPayeesView); - if (d->m_needLoad) - d->init(); + if (MyMoneyFile::instance()->storageAttached()) { + Q_D(KPayeesView); + if (d->m_needLoad) + d->init(); - emit customActionRequested(View::Payees, eView::Action::AboutToShow); + emit customActionRequested(View::Payees, eView::Action::AboutToShow); - if (d->m_needsRefresh) - refresh(); + if (d->m_needsRefresh) + refresh(); + + QList list; + d->selectedPayees(list); + emit selectObjects(list); + } // don't forget base class implementation QWidget::showEvent(event); - - QList list; - d->selectedPayees(list); - emit selectObjects(list); } void KPayeesView::updatePayeeActions(const QList &payees) { pActions[eMenu::Action::NewPayee]->setEnabled(true); const auto payeesCount = payees.count(); auto b = payeesCount == 1 ? true : false; pActions[eMenu::Action::RenamePayee]->setEnabled(b); b = payeesCount > 1 ? true : false; pActions[eMenu::Action::MergePayee]->setEnabled(b); b = payeesCount == 0 ? false : true; pActions[eMenu::Action::DeletePayee]->setEnabled(b); } void KPayeesView::slotSelectTransaction() { Q_D(KPayeesView); auto list = d->ui->m_register->selectedItems(); if (!list.isEmpty()) { const auto t = dynamic_cast(list[0]); if (t) emit selectByVariant(QVariantList {QVariant(t->split().accountId()), QVariant(t->transaction().id()) }, eView::Intent::ShowTransaction); } } void KPayeesView::slotSelectPayeeAndTransaction(const QString& payeeId, const QString& accountId, const QString& transactionId) { Q_D(KPayeesView); if (!isVisible()) return; try { // clear filter d->m_searchWidget->clear(); d->m_searchWidget->updateSearch(); // deselect all other selected items QList selectedItems = d->ui->m_payeesList->selectedItems(); QList::const_iterator payeesIt = selectedItems.constBegin(); while (payeesIt != selectedItems.constEnd()) { if (auto item = dynamic_cast(*payeesIt)) item->setSelected(false); ++payeesIt; } // find the payee in the list QListWidgetItem* it; for (int i = 0; i < d->ui->m_payeesList->count(); ++i) { it = d->ui->m_payeesList->item(i); auto item = dynamic_cast(it); if (item && item->payee().id() == payeeId) { d->ui->m_payeesList->scrollToItem(it, QAbstractItemView::PositionAtCenter); d->ui->m_payeesList->setCurrentItem(it); // active item and deselect all others d->ui->m_payeesList->setCurrentRow(i, QItemSelectionModel::ClearAndSelect); // and select it //make sure the payee selection is updated and transactions are updated accordingly slotSelectPayee(); KMyMoneyRegister::RegisterItem *registerItem = 0; for (i = 0; i < d->ui->m_register->rowCount(); ++i) { registerItem = d->ui->m_register->itemAtRow(i); if (auto t = dynamic_cast(registerItem)) { if (t->transaction().id() == transactionId && t->transaction().accountReferenced(accountId)) { d->ui->m_register->selectItem(registerItem); d->ui->m_register->ensureItemVisible(registerItem); break; } } } // quit out of outer for() loop break; } } } catch (const MyMoneyException &e) { qWarning("Unexpected exception in KPayeesView::slotSelectPayeeAndTransaction %s", e.what()); } } void KPayeesView::slotShowPayeesMenu(const QPoint& /*p*/) { Q_D(KPayeesView); if (dynamic_cast(d->ui->m_payeesList->currentItem())) { slotSelectPayee(); pMenus[eMenu::Menu::Payee]->exec(QCursor::pos()); } } void KPayeesView::slotHelp() { KHelpClient::invokeHelp("details.payees"); } void KPayeesView::slotChangeFilter(int index) { Q_D(KPayeesView); //update the filter type then reload the payees list d->m_payeeFilterType = index; d->loadPayees(); } void KPayeesView::slotNewPayee() { QString id; KMyMoneyUtils::newPayee(i18n("New Payee"), id); slotSelectPayeeAndTransaction(id); } void KPayeesView::slotRenamePayee() { Q_D(KPayeesView); if (d->ui->m_payeesList->currentItem() && d->ui->m_payeesList->selectedItems().count() == 1) { slotStartRename(d->ui->m_payeesList->currentItem()); } } void KPayeesView::slotDeletePayee() { Q_D(KPayeesView); if (d->m_selectedPayeesList.isEmpty()) return; // shouldn't happen // get confirmation from user QString prompt; if (d->m_selectedPayeesList.size() == 1) prompt = i18n("

Do you really want to remove the payee %1?

", d->m_selectedPayeesList.front().name()); else prompt = i18n("Do you really want to remove all selected payees?"); if (KMessageBox::questionYesNo(this, prompt, i18n("Remove Payee")) == KMessageBox::No) return; d->payeeReassign(KPayeeReassignDlg::TypeDelete); } void KPayeesView::slotMergePayee() { Q_D(KPayeesView); if (d->m_selectedPayeesList.size() < 1) return; // shouldn't happen if (KMessageBox::questionYesNo(this, i18n("

Do you really want to merge the selected payees?"), i18n("Merge Payees")) == KMessageBox::No) return; if (d->payeeReassign(KPayeeReassignDlg::TypeMerge)) // clean selection since we just deleted the selected payees d->m_selectedPayeesList.clear(); } diff --git a/kmymoney/views/kscheduledview.cpp b/kmymoney/views/kscheduledview.cpp index 500e71a75..0663778da 100644 --- a/kmymoney/views/kscheduledview.cpp +++ b/kmymoney/views/kscheduledview.cpp @@ -1,468 +1,468 @@ /*************************************************************************** kscheduledview.cpp - description ------------------- begin : Sun Jan 27 2002 copyright : (C) 2000-2002 by Michael Edwardes email : mte@users.sourceforge.net Javier Campos Morales Felix Rodriguez John C Thomas Baumgart Kevin Tambascio (C) 2017 by Łukasz Wojniłowicz ***************************************************************************/ /*************************************************************************** * * * 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 "kscheduledview_p.h" #include // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "ui_kscheduledview.h" #include "keditloanwizard.h" #include "kmymoneyutils.h" #include "kmymoneysettings.h" #include "mymoneyexception.h" #include "kscheduletreeitem.h" #include "keditscheduledlg.h" #include "ktreewidgetfilterlinewidget.h" #include "icons/icons.h" #include "mymoneyutils.h" #include "mymoneyaccount.h" #include "mymoneymoney.h" #include "mymoneysecurity.h" #include "mymoneyschedule.h" #include "mymoneyfile.h" #include "mymoneypayee.h" #include "mymoneysplit.h" #include "mymoneytransaction.h" #include "mymoneyenums.h" #include "menuenums.h" using namespace Icons; KScheduledView::KScheduledView(QWidget *parent) : KMyMoneyViewBase(*new KScheduledViewPrivate(this), parent) { typedef void(KScheduledView::*KScheduledViewFunc)(); const QHash actionConnections { {eMenu::Action::NewSchedule, &KScheduledView::slotNewSchedule}, {eMenu::Action::EditSchedule, &KScheduledView::slotEditSchedule}, {eMenu::Action::DeleteSchedule, &KScheduledView::slotDeleteSchedule}, {eMenu::Action::DuplicateSchedule, &KScheduledView::slotDuplicateSchedule}, {eMenu::Action::EnterSchedule, &KScheduledView::slotEnterSchedule}, {eMenu::Action::SkipSchedule, &KScheduledView::slotSkipSchedule}, }; for (auto a = actionConnections.cbegin(); a != actionConnections.cend(); ++a) connect(pActions[a.key()], &QAction::triggered, this, a.value()); Q_D(KScheduledView); d->m_balanceWarning.reset(new KBalanceWarning(this)); } KScheduledView::~KScheduledView() { } void KScheduledView::slotTimerDone() { Q_D(KScheduledView); QTreeWidgetItem* item; item = d->ui->m_scheduleTree->currentItem(); if (item) { d->ui->m_scheduleTree->scrollToItem(item); } // force a repaint of all items to update the branches /*for (item = d->ui->m_scheduleTree->item(0); item != 0; item = d->ui->m_scheduleTree->item(d->ui->m_scheduleTree->row(item) + 1)) { d->ui->m_scheduleTree->repaintItem(item); } resize(width(), height() + 1);*/ } void KScheduledView::executeCustomAction(eView::Action action) { switch(action) { case eView::Action::Refresh: refresh(); break; case eView::Action::SetDefaultFocus: { Q_D(KScheduledView); QTimer::singleShot(0, d->m_searchWidget->searchLine(), SLOT(setFocus())); } break; case eView::Action::EditSchedule: slotEditSchedule(); break; default: break; } } void KScheduledView::refresh() { Q_D(KScheduledView); if (isVisible()) { d->ui->m_qbuttonNew->setEnabled(true); d->refreshSchedule(true, d->m_currentSchedule.id()); d->m_needsRefresh = false; QTimer::singleShot(50, this, SLOT(slotRearrange())); } else { d->m_needsRefresh = true; } } void KScheduledView::showEvent(QShowEvent* event) { Q_D(KScheduledView); if (d->m_needLoad) d->init(); emit customActionRequested(View::Schedules, eView::Action::AboutToShow); - if (d->m_needsRefresh) + if (d->m_needsRefresh && MyMoneyFile::instance()->storageAttached()) refresh(); QWidget::showEvent(event); } void KScheduledView::updateActions(const MyMoneyObject& obj) { Q_D(KScheduledView); if (typeid(obj) != typeid(MyMoneySchedule) && (obj.id().isEmpty() && d->m_currentSchedule.id().isEmpty())) // do not disable actions that were already disabled)) return; const auto& sch = static_cast(obj); const QVector actionsToBeDisabled { eMenu::Action::EditSchedule, eMenu::Action::DuplicateSchedule, eMenu::Action::DeleteSchedule, eMenu::Action::EnterSchedule, eMenu::Action::SkipSchedule, }; for (const auto& a : actionsToBeDisabled) pActions[a]->setEnabled(false); pActions[eMenu::Action::NewSchedule]->setEnabled(true); if (!sch.id().isEmpty()) { pActions[eMenu::Action::EditSchedule]->setEnabled(true); pActions[eMenu::Action::DuplicateSchedule]->setEnabled(true); pActions[eMenu::Action::DeleteSchedule]->setEnabled(!MyMoneyFile::instance()->isReferenced(sch)); if (!sch.isFinished()) { pActions[eMenu::Action::EnterSchedule]->setEnabled(true); // a schedule with a single occurrence cannot be skipped if (sch.occurrence() != eMyMoney::Schedule::Occurrence::Once) { pActions[eMenu::Action::SkipSchedule]->setEnabled(true); } } } d->m_currentSchedule = sch; } eDialogs::ScheduleResultCode KScheduledView::enterSchedule(MyMoneySchedule& schedule, bool autoEnter, bool extendedKeys) { Q_D(KScheduledView); return d->enterSchedule(schedule, autoEnter, extendedKeys); } void KScheduledView::slotRearrange() { resizeEvent(0); } void KScheduledView::slotEnterOverdueSchedules(const MyMoneyAccount& acc) { Q_D(KScheduledView); const auto file = MyMoneyFile::instance(); auto schedules = file->scheduleList(acc.id(), eMyMoney::Schedule::Type::Any, eMyMoney::Schedule::Occurrence::Any, eMyMoney::Schedule::PaymentType::Any, QDate(), QDate(), true); if (!schedules.isEmpty()) { if (KMessageBox::questionYesNo(this, i18n("KMyMoney has detected some overdue scheduled transactions for this account. Do you want to enter those scheduled transactions now?"), i18n("Scheduled transactions found")) == KMessageBox::Yes) { QMap skipMap; bool processedOne; auto rc = eDialogs::ScheduleResultCode::Enter; do { processedOne = false; QList::const_iterator it_sch; for (it_sch = schedules.constBegin(); (rc != eDialogs::ScheduleResultCode::Cancel) && (it_sch != schedules.constEnd()); ++it_sch) { MyMoneySchedule sch(*(it_sch)); // and enter it if it is not on the skip list if (skipMap.find((*it_sch).id()) == skipMap.end()) { rc = d->enterSchedule(sch, false, true); if (rc == eDialogs::ScheduleResultCode::Ignore) { skipMap[(*it_sch).id()] = true; } } } // reload list (maybe this schedule needs to be added again) schedules = file->scheduleList(acc.id(), eMyMoney::Schedule::Type::Any, eMyMoney::Schedule::Occurrence::Any, eMyMoney::Schedule::PaymentType::Any, QDate(), QDate(), true); } while (processedOne); } } emit selectByObject(MyMoneySchedule(), eView::Intent::FinishEnteringOverdueScheduledTransactions); } void KScheduledView::slotSelectByObject(const MyMoneyObject& obj, eView::Intent intent) { switch(intent) { case eView::Intent::UpdateActions: updateActions(obj); break; case eView::Intent::StartEnteringOverdueScheduledTransactions: slotEnterOverdueSchedules(static_cast(obj)); break; case eView::Intent::OpenContextMenu: slotShowScheduleMenu(static_cast(obj)); break; default: break; } } void KScheduledView::customContextMenuRequested(const QPoint) { Q_D(KScheduledView); emit selectByObject(d->m_currentSchedule, eView::Intent::None); emit selectByObject(d->m_currentSchedule, eView::Intent::OpenContextMenu); } void KScheduledView::slotListItemExecuted(QTreeWidgetItem* item, int) { if (!item) return; try { const auto sch = item->data(0, Qt::UserRole).value(); emit selectByObject(sch, eView::Intent::None); emit selectByObject(sch, eView::Intent::OpenContextMenu); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(this, i18n("Error executing item"), QString::fromLatin1(e.what())); } } void KScheduledView::slotAccountActivated() { Q_D(KScheduledView); d->m_filterAccounts.clear(); try { int accountCount = 0; MyMoneyFile* file = MyMoneyFile::instance(); // extract a list of all accounts under the asset and liability groups // and sort them by name QList list; QStringList accountList = file->asset().accountList(); accountList.append(file->liability().accountList()); file->accountList(list, accountList, true); qStableSort(list.begin(), list.end(), KScheduledViewPrivate::accountNameLessThan); QList::ConstIterator it_a; for (it_a = list.constBegin(); it_a != list.constEnd(); ++it_a) { if (!(*it_a).isClosed()) { if (!d->m_kaccPopup->actions().value(accountCount)->isChecked()) { d->m_filterAccounts.append((*it_a).id()); } ++accountCount; } } d->refreshSchedule(false, d->m_currentSchedule.id()); } catch (const MyMoneyException &e) { KMessageBox::detailedError(this, i18n("Unable to filter account"), QString::fromLatin1(e.what())); } } void KScheduledView::slotListViewExpanded(QTreeWidgetItem* item) { Q_D(KScheduledView); if (item) { if (item->text(0) == i18n("Bills")) d->m_openBills = true; else if (item->text(0) == i18n("Deposits")) d->m_openDeposits = true; else if (item->text(0) == i18n("Transfers")) d->m_openTransfers = true; else if (item->text(0) == i18n("Loans")) d->m_openLoans = true; } } void KScheduledView::slotListViewCollapsed(QTreeWidgetItem* item) { Q_D(KScheduledView); if (item) { if (item->text(0) == i18n("Bills")) d->m_openBills = false; else if (item->text(0) == i18n("Deposits")) d->m_openDeposits = false; else if (item->text(0) == i18n("Transfers")) d->m_openTransfers = false; else if (item->text(0) == i18n("Loans")) d->m_openLoans = false; } } void KScheduledView::slotSelectSchedule(const QString& schedule) { Q_D(KScheduledView); d->refreshSchedule(true, schedule); } void KScheduledView::slotShowScheduleMenu(const MyMoneySchedule& sch) { Q_UNUSED(sch) pMenus[eMenu::Menu::Schedule]->exec(QCursor::pos()); } void KScheduledView::slotSetSelectedItem() { Q_D(KScheduledView); MyMoneySchedule sch; const auto item = d->ui->m_scheduleTree->currentItem(); if (item) { try { sch = item->data(0, Qt::UserRole).value(); } catch (const MyMoneyException &e) { qDebug("KScheduledView::slotSetSelectedItem: %s", e.what()); } } emit selectByObject(sch, eView::Intent::None); } void KScheduledView::slotNewSchedule() { KEditScheduleDlg::newSchedule(MyMoneyTransaction(), eMyMoney::Schedule::Occurrence::Monthly); } void KScheduledView::slotEditSchedule() { Q_D(KScheduledView); KEditScheduleDlg::editSchedule(d->m_currentSchedule); } void KScheduledView::slotDeleteSchedule() { Q_D(KScheduledView); if (!d->m_currentSchedule.id().isEmpty()) { MyMoneyFileTransaction ft; try { MyMoneySchedule sched = MyMoneyFile::instance()->schedule(d->m_currentSchedule.id()); QString msg = i18n("

Are you sure you want to delete the scheduled transaction %1?

", d->m_currentSchedule.name()); if (sched.type() == eMyMoney::Schedule::Type::LoanPayment) { msg += QString(" "); msg += i18n("In case of loan payments it is currently not possible to recreate the scheduled transaction."); } if (KMessageBox::questionYesNo(this, msg) == KMessageBox::No) return; MyMoneyFile::instance()->removeSchedule(sched); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(this, i18n("Unable to remove scheduled transaction '%1'", d->m_currentSchedule.name()), QString::fromLatin1(e.what())); } } } void KScheduledView::slotDuplicateSchedule() { Q_D(KScheduledView); // since we may jump here via code, we have to make sure to react only // if the action is enabled if (pActions[eMenu::Action::DuplicateSchedule]->isEnabled()) { MyMoneySchedule sch = d->m_currentSchedule; sch.clearId(); sch.setLastPayment(QDate()); sch.setName(i18nc("Copy of scheduled transaction name", "Copy of %1", sch.name())); // make sure that we set a valid next due date if the original next due date is invalid if (!sch.nextDueDate().isValid()) sch.setNextDueDate(QDate::currentDate()); MyMoneyFileTransaction ft; try { MyMoneyFile::instance()->addSchedule(sch); ft.commit(); // select the new schedule in the view if (!d->m_currentSchedule.id().isEmpty()) emit selectByObject(sch, eView::Intent::None); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(this, i18n("Unable to duplicate scheduled transaction: '%1'", d->m_currentSchedule.name()), QString::fromLatin1(e.what())); } } } void KScheduledView::slotEnterSchedule() { Q_D(KScheduledView); if (!d->m_currentSchedule.id().isEmpty()) { try { auto schedule = MyMoneyFile::instance()->schedule(d->m_currentSchedule.id()); d->enterSchedule(schedule); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(this, i18n("Unknown scheduled transaction '%1'", d->m_currentSchedule.name()), QString::fromLatin1(e.what())); } } } void KScheduledView::slotSkipSchedule() { Q_D(KScheduledView); if (!d->m_currentSchedule.id().isEmpty()) { try { auto schedule = MyMoneyFile::instance()->schedule(d->m_currentSchedule.id()); d->skipSchedule(schedule); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(this, i18n("Unknown scheduled transaction '%1'", d->m_currentSchedule.name()), QString::fromLatin1(e.what())); } } } diff --git a/kmymoney/views/ktagsview.cpp b/kmymoney/views/ktagsview.cpp index 3829e160e..3f148b05f 100644 --- a/kmymoney/views/ktagsview.cpp +++ b/kmymoney/views/ktagsview.cpp @@ -1,754 +1,756 @@ /*************************************************************************** ktagsview.cpp ------------- begin : Sat Oct 13 2012 copyright : (C) 2012 by Alessandro Russo (C) 2017 by Łukasz Wojniłowicz ***************************************************************************/ /*************************************************************************** * * * 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 "ktagsview.h" #include "ktagsview_p.h" // ---------------------------------------------------------------------------- // QT Includes #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include // ---------------------------------------------------------------------------- // Project Includes #include "mymoneyexception.h" #include "mymoneymoney.h" #include "mymoneyprice.h" #include "kmymoneysettings.h" #include "ktagreassigndlg.h" #include "kmymoneyutils.h" #include "kmymoneymvccombo.h" #include "mymoneysecurity.h" #include "mymoneysplit.h" #include "mymoneytransaction.h" #include "mymoneyschedule.h" #include "transaction.h" #include "menuenums.h" using namespace Icons; /* -------------------------------------------------------------------------*/ /* KTransactionPtrVector */ /* -------------------------------------------------------------------------*/ // *** KTagsView Implementation *** KTagsView::KTagsView(QWidget *parent) : KMyMoneyViewBase(*new KTagsViewPrivate(this), parent) { typedef void(KTagsView::*KTagsViewFunc)(); const QHash actionConnections { {eMenu::Action::NewTag, &KTagsView::slotNewTag}, {eMenu::Action::RenameTag, &KTagsView::slotRenameTag}, {eMenu::Action::DeleteTag, &KTagsView::slotDeleteTag} }; for (auto a = actionConnections.cbegin(); a != actionConnections.cend(); ++a) connect(pActions[a.key()], &QAction::triggered, this, a.value()); } KTagsView::~KTagsView() { } void KTagsView::executeCustomAction(eView::Action action) { switch(action) { case eView::Action::Refresh: refresh(); break; case eView::Action::SetDefaultFocus: { Q_D(KTagsView); QTimer::singleShot(0, d->m_searchWidget, SLOT(setFocus())); } break; default: break; } } void KTagsView::refresh() { Q_D(KTagsView); if (isVisible()) { if (d->m_inSelection) QTimer::singleShot(0, this, SLOT(refresh())); else loadTags(); d->m_needsRefresh = false; } else { d->m_needsRefresh = true; } } void KTagsView::slotStartRename(QListWidgetItem* item) { Q_D(KTagsView); d->m_allowEditing = true; d->ui->m_tagsList->editItem(item); } // This variant is only called when a single tag is selected and renamed. void KTagsView::slotRenameSingleTag(QListWidgetItem* ta) { Q_D(KTagsView); //if there is no current item selected, exit if (d->m_allowEditing == false || !d->ui->m_tagsList->currentItem() || ta != d->ui->m_tagsList->currentItem()) return; //qDebug() << "[KTagsView::slotRenameTag]"; // create a copy of the new name without appended whitespaces auto new_name = ta->text(); if (d->m_tag.name() != new_name) { MyMoneyFileTransaction ft; try { // check if we already have a tag with the new name try { // this function call will throw an exception, if the tag // hasn't been found. MyMoneyFile::instance()->tagByName(new_name); // the name already exists, ask the user whether he's sure to keep the name if (KMessageBox::questionYesNo(this, i18n("A tag with the name '%1' already exists. It is not advisable to have " "multiple tags with the same identification name. Are you sure you would like " "to rename the tag?", new_name)) != KMessageBox::Yes) { ta->setText(d->m_tag.name()); return; } } catch (const MyMoneyException &) { // all ok, the name is unique } d->m_tag.setName(new_name); d->m_newName = new_name; MyMoneyFile::instance()->modifyTag(d->m_tag); // the above call to modifyTag will reload the view so // all references and pointers to the view have to be // re-established. // make sure, that the record is visible even if it moved // out of sight due to the rename operation ensureTagVisible(d->m_tag.id()); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(this, i18n("Unable to modify tag"), QString::fromLatin1(e.what())); } } else { ta->setText(new_name); } } void KTagsView::ensureTagVisible(const QString& id) { Q_D(KTagsView); for (int i = 0; i < d->ui->m_tagsList->count(); ++i) { KTagListItem* ta = dynamic_cast(d->ui->m_tagsList->item(0)); if (ta && ta->tag().id() == id) { d->ui->m_tagsList->scrollToItem(ta, QAbstractItemView::PositionAtCenter); d->ui->m_tagsList->setCurrentItem(ta); // active item and deselect all others d->ui->m_tagsList->setCurrentRow(i, QItemSelectionModel::ClearAndSelect); // and select it break; } } } void KTagsView::selectedTags(QList& tagsList) const { Q_D(const KTagsView); QList selectedItems = d->ui->m_tagsList->selectedItems(); QList::ConstIterator itemsIt = selectedItems.constBegin(); while (itemsIt != selectedItems.constEnd()) { KTagListItem* item = dynamic_cast(*itemsIt); if (item) tagsList << item->tag(); ++itemsIt; } } void KTagsView::slotSelectTag(QListWidgetItem* cur, QListWidgetItem* prev) { Q_D(KTagsView); Q_UNUSED(cur); Q_UNUSED(prev); d->m_allowEditing = false; } void KTagsView::slotSelectTag() { Q_D(KTagsView); // check if the content of a currently selected tag was modified // and ask to store the data if (d->ui->m_updateButton->isEnabled()) { if (KMessageBox::questionYesNo(this, QString("%1").arg( i18n("Do you want to save the changes for %1?", d->m_newName)), i18n("Save changes")) == KMessageBox::Yes) { d->m_inSelection = true; slotUpdateTag(); d->m_inSelection = false; } } // loop over all tags and count the number of tags, also // obtain last selected tag QList tagsList; selectedTags(tagsList); slotSelectTags(tagsList); if (tagsList.isEmpty()) { d->ui->m_tabWidget->setEnabled(false); // disable tab widget d->ui->m_balanceLabel->hide(); d->ui->m_deleteButton->setEnabled(false); //disable delete and rename button d->ui->m_renameButton->setEnabled(false); clearItemData(); d->m_tag = MyMoneyTag(); return; // make sure we don't access an undefined tag } d->ui->m_deleteButton->setEnabled(true); //re-enable delete button // if we have multiple tags selected, clear and disable the tag information if (tagsList.count() > 1) { d->ui->m_tabWidget->setEnabled(false); // disable tab widget d->ui->m_renameButton->setEnabled(false); // disable also the rename button d->ui->m_balanceLabel->hide(); clearItemData(); } else d->ui->m_renameButton->setEnabled(true); // otherwise we have just one selected, enable tag information widget and renameButton d->ui->m_tabWidget->setEnabled(true); d->ui->m_balanceLabel->show(); // as of now we are updating only the last selected tag, and until // selection mode of the QListView has been changed to Extended, this // will also be the only selection and behave exactly as before - Andreas try { d->m_tag = tagsList[0]; d->m_newName = d->m_tag.name(); d->ui->m_colorbutton->setEnabled(true); d->ui->m_colorbutton->setColor(d->m_tag.tagColor()); d->ui->m_closed->setEnabled(true); d->ui->m_closed->setChecked(d->m_tag.isClosed()); d->ui->m_notes->setEnabled(true); d->ui->m_notes->setText(d->m_tag.notes()); slotTagDataChanged(); showTransactions(); } catch (const MyMoneyException &e) { qDebug("exception during display of tag: %s", e.what()); d->ui->m_register->clear(); d->m_tag = MyMoneyTag(); } d->m_allowEditing = true; } void KTagsView::clearItemData() { Q_D(KTagsView); d->ui->m_colorbutton->setColor(QColor()); d->ui->m_closed->setChecked(false); d->ui->m_notes->setText(QString()); showTransactions(); } void KTagsView::showTransactions() { Q_D(KTagsView); MyMoneyMoney balance; auto file = MyMoneyFile::instance(); MyMoneySecurity base = file->baseCurrency(); // setup sort order d->ui->m_register->setSortOrder(KMyMoneySettings::sortSearchView()); // clear the register d->ui->m_register->clear(); if (d->m_tag.id().isEmpty() || !d->ui->m_tabWidget->isEnabled()) { d->ui->m_balanceLabel->setText(i18n("Balance: %1", balance.formatMoney(file->baseCurrency().smallestAccountFraction()))); return; } // setup the list and the pointer vector MyMoneyTransactionFilter filter; filter.addTag(d->m_tag.id()); filter.setDateFilter(KMyMoneySettings::startDate().date(), QDate()); // retrieve the list from the engine file->transactionList(d->m_transactionList, filter); // create the elements for the register QList >::const_iterator it; QMap uniqueMap; MyMoneyMoney deposit, payment; int splitCount = 0; bool balanceAccurate = true; for (it = d->m_transactionList.constBegin(); it != d->m_transactionList.constEnd(); ++it) { const MyMoneySplit& split = (*it).second; MyMoneyAccount acc = file->account(split.accountId()); ++splitCount; uniqueMap[(*it).first.id()]++; KMyMoneyRegister::Register::transactionFactory(d->ui->m_register, (*it).first, (*it).second, uniqueMap[(*it).first.id()]); // take care of foreign currencies MyMoneyMoney val = split.shares().abs(); if (acc.currencyId() != base.id()) { const MyMoneyPrice &price = file->price(acc.currencyId(), base.id()); // in case the price is valid, we use it. Otherwise, we keep // a flag that tells us that the balance is somewhat inaccurate if (price.isValid()) { val *= price.rate(base.id()); } else { balanceAccurate = false; } } if (split.shares().isNegative()) { payment += val; } else { deposit += val; } } balance = deposit - payment; // add the group markers d->ui->m_register->addGroupMarkers(); // sort the transactions according to the sort setting d->ui->m_register->sortItems(); // remove trailing and adjacent markers d->ui->m_register->removeUnwantedGroupMarkers(); d->ui->m_register->updateRegister(true); // we might end up here with updates disabled on the register so // make sure that we enable updates here d->ui->m_register->setUpdatesEnabled(true); d->ui->m_balanceLabel->setText(i18n("Balance: %1%2", balanceAccurate ? "" : "~", balance.formatMoney(file->baseCurrency().smallestAccountFraction()))); } void KTagsView::slotTagDataChanged() { Q_D(KTagsView); auto rc = false; if (d->ui->m_tabWidget->isEnabled()) { rc |= ((d->m_tag.tagColor().isValid() != d->ui->m_colorbutton->color().isValid()) || (d->ui->m_colorbutton->color().isValid() && d->m_tag.tagColor() != d->ui->m_colorbutton->color())); rc |= (d->ui->m_closed->isChecked() != d->m_tag.isClosed()); rc |= ((d->m_tag.notes().isEmpty() != d->ui->m_notes->toPlainText().isEmpty()) || (!d->ui->m_notes->toPlainText().isEmpty() && d->m_tag.notes() != d->ui->m_notes->toPlainText())); } d->ui->m_updateButton->setEnabled(rc); } void KTagsView::slotUpdateTag() { Q_D(KTagsView); if (d->ui->m_updateButton->isEnabled()) { MyMoneyFileTransaction ft; d->ui->m_updateButton->setEnabled(false); try { d->m_tag.setName(d->m_newName); d->m_tag.setTagColor(d->ui->m_colorbutton->color()); d->m_tag.setClosed(d->ui->m_closed->isChecked()); d->m_tag.setNotes(d->ui->m_notes->toPlainText()); MyMoneyFile::instance()->modifyTag(d->m_tag); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(this, i18n("Unable to modify tag"), QString::fromLatin1(e.what())); } } } void KTagsView::showEvent(QShowEvent* event) { - Q_D(KTagsView); - if (d->m_needLoad) - d->init(); + if (MyMoneyFile::instance()->storageAttached()) { + Q_D(KTagsView); + if (d->m_needLoad) + d->init(); - emit customActionRequested(View::Tags, eView::Action::AboutToShow); + emit customActionRequested(View::Tags, eView::Action::AboutToShow); - if (d->m_needsRefresh) - refresh(); + if (d->m_needsRefresh) + refresh(); + + QList list; + selectedTags(list); + slotSelectTags(list); + } // don't forget base class implementation QWidget::showEvent(event); - - QList list; - selectedTags(list); - slotSelectTags(list); } void KTagsView::updateTagActions(const QList& tags) { pActions[eMenu::Action::NewTag]->setEnabled(true); const auto tagsCount = tags.count(); auto b = tagsCount == 1 ? true : false; pActions[eMenu::Action::RenameTag]->setEnabled(b); b = tagsCount >= 1 ? true : false; pActions[eMenu::Action::DeleteTag]->setEnabled(b); } void KTagsView::loadTags() { Q_D(KTagsView); if (d->m_inSelection) return; QMap isSelected; QString id; MyMoneyFile* file = MyMoneyFile::instance(); // remember which items are selected in the list QList selectedItems = d->ui->m_tagsList->selectedItems(); QList::const_iterator tagsIt = selectedItems.constBegin(); while (tagsIt != selectedItems.constEnd()) { KTagListItem* item = dynamic_cast(*tagsIt); if (item) isSelected[item->tag().id()] = true; ++tagsIt; } // keep current selected item KTagListItem *currentItem = static_cast(d->ui->m_tagsList->currentItem()); if (currentItem) id = currentItem->tag().id(); d->m_allowEditing = false; // clear the list d->m_searchWidget->clear(); d->m_searchWidget->updateSearch(); d->ui->m_tagsList->clear(); d->ui->m_register->clear(); currentItem = 0; QListlist = file->tagList(); QList::ConstIterator it; for (it = list.constBegin(); it != list.constEnd(); ++it) { if (d->m_tagFilterType == (int)eView::Tag::All || (d->m_tagFilterType == (int)eView::Tag::Referenced && file->isReferenced(*it)) || (d->m_tagFilterType == (int)eView::Tag::Unused && !file->isReferenced(*it)) || (d->m_tagFilterType == (int)eView::Tag::Opened && !(*it).isClosed()) || (d->m_tagFilterType == (int)eView::Tag::Closed && (*it).isClosed())) { KTagListItem* item = new KTagListItem(d->ui->m_tagsList, *it); if (item->tag().id() == id) currentItem = item; if (isSelected[item->tag().id()]) item->setSelected(true); } } d->ui->m_tagsList->sortItems(); if (currentItem) { d->ui->m_tagsList->setCurrentItem(currentItem); d->ui->m_tagsList->scrollToItem(currentItem); } slotSelectTag(0, 0); d->m_allowEditing = true; } void KTagsView::slotSelectTransaction() { Q_D(KTagsView); QList list = d->ui->m_register->selectedItems(); if (!list.isEmpty()) { KMyMoneyRegister::Transaction* t = dynamic_cast(list[0]); if (t) emit selectByVariant(QVariantList {QVariant(t->split().accountId()), QVariant(t->transaction().id())}, eView::Intent::ShowTransaction); } } void KTagsView::slotSelectTagAndTransaction(const QString& tagId, const QString& accountId, const QString& transactionId) { if (!isVisible()) return; Q_D(KTagsView); try { // clear filter d->m_searchWidget->clear(); d->m_searchWidget->updateSearch(); // deselect all other selected items QList selectedItems = d->ui->m_tagsList->selectedItems(); QList::const_iterator tagsIt = selectedItems.constBegin(); while (tagsIt != selectedItems.constEnd()) { KTagListItem* item = dynamic_cast(*tagsIt); if (item) item->setSelected(false); ++tagsIt; } // find the tag in the list QListWidgetItem* it; for (int i = 0; i < d->ui->m_tagsList->count(); ++i) { it = d->ui->m_tagsList->item(i); KTagListItem* item = dynamic_cast(it); if (item && item->tag().id() == tagId) { d->ui->m_tagsList->scrollToItem(it, QAbstractItemView::PositionAtCenter); d->ui->m_tagsList->setCurrentItem(it); // active item and deselect all others d->ui->m_tagsList->setCurrentRow(i, QItemSelectionModel::ClearAndSelect); // and select it //make sure the tag selection is updated and transactions are updated accordingly slotSelectTag(); KMyMoneyRegister::RegisterItem *registerItem = 0; for (i = 0; i < d->ui->m_register->rowCount(); ++i) { registerItem = d->ui->m_register->itemAtRow(i); KMyMoneyRegister::Transaction* t = dynamic_cast(registerItem); if (t) { if (t->transaction().id() == transactionId && t->transaction().accountReferenced(accountId)) { d->ui->m_register->selectItem(registerItem); d->ui->m_register->ensureItemVisible(registerItem); break; } } } // quit out of outer for() loop break; } } } catch (const MyMoneyException &e) { qWarning("Unexpected exception in KTagsView::slotSelectTagAndTransaction %s", e.what()); } } void KTagsView::slotSelectTagAndTransaction(const QString& tagId) { slotSelectTagAndTransaction(tagId, QString(), QString()); } void KTagsView::slotShowTagsMenu(const QPoint& /*ta*/) { Q_D(KTagsView); auto item = dynamic_cast(d->ui->m_tagsList->currentItem()); if (item) { slotSelectTag(); pMenus[eMenu::Menu::Tag]->exec(QCursor::pos()); } } void KTagsView::slotHelp() { KHelpClient::invokeHelp("details.tags.attributes"); //FIXME-ALEX update help file } void KTagsView::slotChangeFilter(int index) { Q_D(KTagsView); //update the filter type then reload the tags list d->m_tagFilterType = index; loadTags(); } void KTagsView::slotSelectTags(const QList& list) { Q_D(KTagsView); d->m_selectedTags = list; updateTagActions(list); } void KTagsView::slotNewTag() { QString id; KMyMoneyUtils::newTag(i18n("New Tag"), id); slotSelectTagAndTransaction(id); } void KTagsView::slotRenameTag() { Q_D(KTagsView); if (d->ui->m_tagsList->currentItem() && d->ui->m_tagsList->selectedItems().count() == 1) { slotStartRename(d->ui->m_tagsList->currentItem()); } } void KTagsView::slotDeleteTag() { Q_D(KTagsView); if (d->m_selectedTags.isEmpty()) return; // shouldn't happen const auto file = MyMoneyFile::instance(); // first create list with all non-selected tags QList remainingTags = file->tagList(); QList::iterator it_ta; for (it_ta = remainingTags.begin(); it_ta != remainingTags.end();) { if (d->m_selectedTags.contains(*it_ta)) { it_ta = remainingTags.erase(it_ta); } else { ++it_ta; } } // get confirmation from user QString prompt; if (d->m_selectedTags.size() == 1) prompt = i18n("

Do you really want to remove the tag %1?

", d->m_selectedTags.front().name()); else prompt = i18n("Do you really want to remove all selected tags?"); if (KMessageBox::questionYesNo(this, prompt, i18n("Remove Tag")) == KMessageBox::No) return; MyMoneyFileTransaction ft; try { // create a transaction filter that contains all tags selected for removal MyMoneyTransactionFilter f = MyMoneyTransactionFilter(); for (QList::const_iterator it = d->m_selectedTags.constBegin(); it != d->m_selectedTags.constEnd(); ++it) { f.addTag((*it).id()); } // request a list of all transactions that still use the tags in question auto translist = file->transactionList(f); // qDebug() << "[KTagsView::slotDeleteTag] " << translist.count() << " transaction still assigned to tags"; // now get a list of all schedules that make use of one of the tags QList used_schedules; foreach (const auto schedule, file->scheduleList()) { // loop over all splits in the transaction of the schedule foreach (const auto split, schedule.transaction().splits()) { for (auto i = 0; i < split.tagIdList().size(); ++i) { // is the tag in the split to be deleted? if (d->tagInList(d->m_selectedTags, split.tagIdList()[i])) { used_schedules.push_back(schedule); // remember this schedule break; } } } } // qDebug() << "[KTagsView::slotDeleteTag] " << used_schedules.count() << " schedules use one of the selected tags"; MyMoneyTag newTag; // if at least one tag is still referenced, we need to reassign its transactions first if (!translist.isEmpty() || !used_schedules.isEmpty()) { // show error message if no tags remain //FIXME-ALEX Tags are optional so we can delete all of them and simply delete every tagId from every transaction if (remainingTags.isEmpty()) { KMessageBox::sorry(this, i18n("At least one transaction/scheduled transaction is still referenced by a tag. " "Currently you have all tags selected. However, at least one tag must remain so " "that the transaction/scheduled transaction can be reassigned.")); return; } // show transaction reassignment dialog auto dlg = new KTagReassignDlg(this); KMyMoneyMVCCombo::setSubstringSearchForChildren(dlg, !KMyMoneySettings::stringMatchFromStart()); auto tag_id = dlg->show(remainingTags); delete dlg; // and kill the dialog if (tag_id.isEmpty()) //FIXME-ALEX Let the user choose to not reassign a to-be deleted tag to another one. return; // the user aborted the dialog, so let's abort as well newTag = file->tag(tag_id); // TODO : check if we have a report that explicitly uses one of our tags // and issue an appropriate warning try { // now loop over all transactions and reassign tag for (auto& transaction : translist) { // create a copy of the splits list in the transaction // loop over all splits for (auto& split : transaction.splits()) { QList tagIdList = split.tagIdList(); for (int i = 0; i < tagIdList.size(); ++i) { // if the split is assigned to one of the selected tags, we need to modify it if (d->tagInList(d->m_selectedTags, tagIdList[i])) { tagIdList.removeAt(i); if (tagIdList.indexOf(tag_id) == -1) tagIdList.append(tag_id); i = -1; // restart from the first element } } split.setTagIdList(tagIdList); // first modify tag list in current split // then modify the split in our local copy of the transaction list transaction.modifySplit(split); // this does not modify the list object 'splits'! } // for - Splits file->modifyTransaction(transaction); // modify the transaction in the MyMoney object } // for - Transactions // now loop over all schedules and reassign tags for (auto& schedule : used_schedules) { // create copy of transaction in current schedule auto trans = schedule.transaction(); // create copy of lists of splits for (auto& split : trans.splits()) { QList tagIdList = split.tagIdList(); for (auto i = 0; i < tagIdList.size(); ++i) { if (d->tagInList(d->m_selectedTags, tagIdList[i])) { tagIdList.removeAt(i); if (tagIdList.indexOf(tag_id) == -1) tagIdList.append(tag_id); i = -1; // restart from the first element } } split.setTagIdList(tagIdList); trans.modifySplit(split); // does not modify the list object 'splits'! } // for - Splits // store transaction in current schedule schedule.setTransaction(trans); file->modifySchedule(schedule); // modify the schedule in the MyMoney engine } // for - Schedules } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(this, i18n("Unable to reassign tag of transaction/split"), QString::fromLatin1(e.what())); } } // if !translist.isEmpty() // now loop over all selected tags and remove them for (QList::iterator it = d->m_selectedTags.begin(); it != d->m_selectedTags.end(); ++it) { file->removeTag(*it); } ft.commit(); // If we just deleted the tags, they sure don't exist anymore slotSelectTags(QList()); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(this, i18n("Unable to remove tag(s)"), QString::fromLatin1(e.what())); } } diff --git a/kmymoney/views/simpleledgerview.cpp b/kmymoney/views/simpleledgerview.cpp index be70c8df9..3b79e0e2b 100644 --- a/kmymoney/views/simpleledgerview.cpp +++ b/kmymoney/views/simpleledgerview.cpp @@ -1,312 +1,314 @@ /*************************************************************************** simpleledgerview.cpp ------------------- begin : Sat Aug 8 2015 copyright : (C) 2015 by 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. * * * ***************************************************************************/ #include "simpleledgerview.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes #include "kmymoneyviewbase_p.h" #include "ledgerviewpage.h" #include "models.h" #include "accountsmodel.h" #include "kmymoneyaccountcombo.h" #include "ui_simpleledgerview.h" #include "icons/icons.h" #include "mymoneyfile.h" #include "mymoneyaccount.h" #include "mymoneyinstitution.h" #include "mymoneyenums.h" #include "modelenums.h" using namespace Icons; class SimpleLedgerViewPrivate : public KMyMoneyViewBasePrivate { Q_DECLARE_PUBLIC(SimpleLedgerView) public: explicit SimpleLedgerViewPrivate(SimpleLedgerView* qq) : q_ptr(qq) , ui(new Ui_SimpleLedgerView) , accountsModel(nullptr) , newTabWidget(nullptr) , webSiteButton(nullptr) , lastIdx(-1) , inModelUpdate(false) , m_needLoad(true) {} ~SimpleLedgerViewPrivate() { delete ui; } void init() { Q_Q(SimpleLedgerView); m_needLoad = false; ui->setupUi(q); ui->ledgerTab->setTabIcon(0, Icons::get(Icon::ListAdd)); ui->ledgerTab->setTabText(0, QString()); newTabWidget = ui->ledgerTab->widget(0); accountsModel= new AccountNamesFilterProxyModel(q); // remove close button from new page QTabBar* bar = ui->ledgerTab->findChild(); if(bar) { QTabBar::ButtonPosition closeSide = (QTabBar::ButtonPosition)q->style()->styleHint(QStyle::SH_TabBar_CloseButtonPosition, 0, newTabWidget); QWidget *w = bar->tabButton(0, closeSide); bar->setTabButton(0, closeSide, 0); w->deleteLater(); q->connect(bar, SIGNAL(tabMoved(int,int)), q, SLOT(checkTabOrder(int,int))); } webSiteButton = new QToolButton; ui->ledgerTab->setCornerWidget(webSiteButton); q->connect(webSiteButton, &QToolButton::pressed, q, [=] { QDesktopServices::openUrl(webSiteUrl); }); q->connect(ui->accountCombo, SIGNAL(accountSelected(QString)), q, SLOT(openNewLedger(QString))); q->connect(ui->ledgerTab, &QTabWidget::currentChanged, q, &SimpleLedgerView::tabSelected); q->connect(Models::instance(), &Models::modelsLoaded, q, &SimpleLedgerView::updateModels); q->connect(ui->ledgerTab, &QTabWidget::tabCloseRequested, q, &SimpleLedgerView::closeLedger); // we reload the icon if the institution data changed q->connect(Models::instance()->institutionsModel(), &InstitutionsModel::dataChanged, q, &SimpleLedgerView::setupCornerWidget); accountsModel->addAccountGroup(QVector {eMyMoney::Account::Type::Asset, eMyMoney::Account::Type::Liability, eMyMoney::Account::Type::Equity}); accountsModel->setHideEquityAccounts(false); auto const model = Models::instance()->accountsModel(); accountsModel->setSourceModel(model); accountsModel->setSourceColumns(model->getColumns()); accountsModel->sort((int)eAccountsModel::Column::Account); ui->accountCombo->setModel(accountsModel); q->tabSelected(0); q->updateModels(); q->openFavoriteLedgers(); } SimpleLedgerView* q_ptr; Ui_SimpleLedgerView* ui; AccountNamesFilterProxyModel* accountsModel; QWidget* newTabWidget; QToolButton* webSiteButton; QUrl webSiteUrl; int lastIdx; bool inModelUpdate; bool m_needLoad; }; SimpleLedgerView::SimpleLedgerView(QWidget *parent) : KMyMoneyViewBase(*new SimpleLedgerViewPrivate(this), parent) { } SimpleLedgerView::~SimpleLedgerView() { } void SimpleLedgerView::openNewLedger(QString accountId) { Q_D(SimpleLedgerView); if(d->inModelUpdate || accountId.isEmpty()) return; LedgerViewPage* view = 0; // check if ledger is already opened for(int idx=0; idx < d->ui->ledgerTab->count()-1; ++idx) { view = qobject_cast(d->ui->ledgerTab->widget(idx)); if(view) { if(accountId == view->accountId()) { d->ui->ledgerTab->setCurrentIndex(idx); return; } } } // need a new tab, we insert it before the rightmost one QModelIndex index = Models::instance()->accountsModel()->accountById(accountId); if(index.isValid()) { // create new ledger view page MyMoneyAccount acc = Models::instance()->accountsModel()->data(index, (int)eAccountsModel::Role::Account).value(); view = new LedgerViewPage(this); view->setShowEntryForNewTransaction(); view->setAccount(acc); /// @todo setup current global setting for form visibility // view->showTransactionForm(...); // insert new ledger view page in tab view int newIdx = d->ui->ledgerTab->insertTab(d->ui->ledgerTab->count()-1, view, acc.name()); d->ui->ledgerTab->setCurrentIndex(d->ui->ledgerTab->count()-1); d->ui->ledgerTab->setCurrentIndex(newIdx); } } void SimpleLedgerView::tabSelected(int idx) { Q_D(SimpleLedgerView); // qDebug() << "tabSelected" << idx << (d->ui->ledgerTab->count()-1); if(idx != (d->ui->ledgerTab->count()-1)) { d->lastIdx = idx; } setupCornerWidget(); } void SimpleLedgerView::updateModels() { Q_D(SimpleLedgerView); d->inModelUpdate = true; // d->ui->accountCombo-> d->ui->accountCombo->expandAll(); d->ui->accountCombo->setSelected(MyMoneyFile::instance()->asset().id()); d->inModelUpdate = false; } void SimpleLedgerView::closeLedger(int idx) { Q_D(SimpleLedgerView); // don't react on the close request for the new ledger function if(idx != (d->ui->ledgerTab->count()-1)) { d->ui->ledgerTab->removeTab(idx); } } void SimpleLedgerView::checkTabOrder(int from, int to) { Q_D(SimpleLedgerView); if(d->inModelUpdate) return; QTabBar* bar = d->ui->ledgerTab->findChild(); if(bar) { const int rightMostIdx = d->ui->ledgerTab->count()-1; if(from == rightMostIdx) { // someone tries to move the new account tab away from the rightmost position d->inModelUpdate = true; bar->moveTab(to, from); d->inModelUpdate = false; } } } void SimpleLedgerView::showTransactionForm(bool show) { emit showForms(show); } void SimpleLedgerView::closeLedgers() { Q_D(SimpleLedgerView); if (d->m_needLoad) return; auto tabCount = d->ui->ledgerTab->count(); // check that we have a least one tab that can be closed if(tabCount > 1) { // we keep the tab with the selector open at all times // which is located in the right most position --tabCount; do { --tabCount; closeLedger(tabCount); } while(tabCount > 0); } } void SimpleLedgerView::openFavoriteLedgers() { Q_D(SimpleLedgerView); if (d->m_needLoad) return; AccountsModel* model = Models::instance()->accountsModel(); QModelIndex start = model->index(0, 0); QModelIndexList indexes = model->match(start, (int)eAccountsModel::Role::Favorite, QVariant(true), -1, Qt::MatchRecursive); // indexes now has a list of favorite accounts but two entries for each. // that doesn't matter here, since openNewLedger() can handle duplicates Q_FOREACH(QModelIndex index, indexes) { openNewLedger(model->data(index, (int)eAccountsModel::Role::ID).toString()); } d->ui->ledgerTab->setCurrentIndex(0); } void SimpleLedgerView::showEvent(QShowEvent* event) { - Q_D(SimpleLedgerView); - if (d->m_needLoad) - d->init(); + if (MyMoneyFile::instance()->storageAttached()) { + Q_D(SimpleLedgerView); + if (d->m_needLoad) + d->init(); + } // don't forget base class implementation QWidget::showEvent(event); } void SimpleLedgerView::setupCornerWidget() { Q_D(SimpleLedgerView); // check if we already have the button, quit if not if (!d->webSiteButton) return; d->webSiteButton->hide(); auto view = qobject_cast(d->ui->ledgerTab->currentWidget()); if (view) { auto index = Models::instance()->accountsModel()->accountById(view->accountId()); if(index.isValid()) { // get icon name and url via account and institution object const auto acc = Models::instance()->accountsModel()->data(index, (int)eAccountsModel::Role::Account).value(); if (!acc.institutionId().isEmpty()) { index = Models::instance()->institutionsModel()->accountById(acc.institutionId()); const auto institution = Models::instance()->institutionsModel()->data(index, (int)eAccountsModel::Role::Account).value(); const auto url = institution.value(QStringLiteral("url")); const auto iconName = institution.value(QStringLiteral("icon")); if (!url.isEmpty() && !iconName.isEmpty()) { const auto favIcon = Icons::loadIconFromApplicationCache(iconName); if (!favIcon.isNull()) { d->webSiteButton->show(); d->webSiteButton->setIcon(favIcon); d->webSiteButton->setText(url); d->webSiteButton->setToolTip(i18n("Open website of %1 in your browser.", institution.name())); d->webSiteUrl.setUrl(QString::fromLatin1("https://%1/").arg(url)); } } } } } } diff --git a/tools/xea2kmt.cpp b/tools/xea2kmt.cpp index b3b8a2762..21ca6020f 100644 --- a/tools/xea2kmt.cpp +++ b/tools/xea2kmt.cpp @@ -1,656 +1,656 @@ /*************************************************************************** xea2kmt.cpp ------------------- copyright : (C) 2014 by Ralf Habacker ****************************************************************************/ /*************************************************************************** * * * 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 "../kmymoney/mymoney/mymoneyaccount.h" #include #include #include #include #include #include #include #include "mymoneyenums.h" using namespace eMyMoney; QDebug operator <<(QDebug out, const QXmlStreamNamespaceDeclaration &a) { out << "QXmlStreamNamespaceDeclaration(" << "prefix:" << a.prefix().toString() << "namespaceuri:" << a.namespaceUri().toString() << ")"; return out; } QDebug operator <<(QDebug out, const QXmlStreamAttribute &a) { out << "QXmlStreamAttribute(" << "prefix:" << a.prefix().toString() << "namespaceuri:" << a.namespaceUri().toString() << "name:" << a.name().toString() << " value:" << a.value().toString() << ")"; return out; } bool debug = false; bool withID = false; bool noLevel1Names = false; bool withTax = false; bool prefixNameWithCode = false; typedef QMap DirNameMapType; /** * map to hold differences from gnucash to kmymoney template directory * @return directory name map */ DirNameMapType &getDirNameMap() { static DirNameMapType dirNameMap; dirNameMap["cs"] = "cs_CZ"; dirNameMap["da"] = "dk"; dirNameMap["ja"] = "ja_JP"; dirNameMap["ko"] = "ko_KR"; dirNameMap["nb"] = "nb_NO"; dirNameMap["nl"] = "nl_NL"; dirNameMap["ru"] = "ru_RU"; return dirNameMap; } int toKMyMoneyAccountType(const QString &type) { if(type == "ROOT") return (int)Account::Type::Unknown; else if (type == "BANK") return (int)Account::Type::Checkings; else if (type == "CASH") return (int)Account::Type::Cash; else if (type == "CREDIT") return (int)Account::Type::CreditCard; else if (type == "INVEST") return (int)Account::Type::Investment; else if (type == "RECEIVABLE") return (int)Account::Type::Asset; else if (type == "ASSET") return (int)Account::Type::Asset; else if (type == "PAYABLE") return (int)Account::Type::Liability; else if (type == "LIABILITY") return (int)Account::Type::Liability; else if (type == "CURRENCY") return (int)Account::Type::Currency; else if (type == "INCOME") return (int)Account::Type::Income; else if (type == "EXPENSE") return (int)Account::Type::Expense; else if (type == "STOCK") return (int)Account::Type::Stock; else if (type == "MUTUAL") return (int)Account::Type::Stock; else if (type == "EQUITY") return (int)Account::Type::Equity; else return 99; // unknown } class TemplateAccount { public: typedef QList List; typedef QList PointerList; typedef QMap SlotList; QString id; QString m_type; QString m_name; QString code; QString parent; SlotList slotList; TemplateAccount() { } TemplateAccount(const TemplateAccount &b) : id(b.id), m_type(b.m_type), m_name(b.m_name), code(b.code), parent(b.parent), slotList(b.slotList) { } void clear() { id = ""; m_type = ""; m_name = ""; code = ""; parent = ""; slotList.clear(); } bool readSlots(QXmlStreamReader &xml) { while (!xml.atEnd()) { QXmlStreamReader::TokenType type = xml.readNext(); if (type == QXmlStreamReader::StartElement) { QStringRef _name = xml.name(); if (_name == "slot") { type = xml.readNext(); if (type == QXmlStreamReader::Characters) type = xml.readNext(); if (type == QXmlStreamReader::StartElement) { QStringRef name = xml.name(); QString key, value; if (name == "key") key = xml.readElementText(QXmlStreamReader::SkipChildElements).trimmed(); type = xml.readNext(); if (type == QXmlStreamReader::Characters) type = xml.readNext(); if (type == QXmlStreamReader::StartElement) { name = xml.name(); if (name == "value") value = xml.readElementText(QXmlStreamReader::SkipChildElements).trimmed(); } if (!key.isEmpty() && !value.isEmpty()) slotList[key] = value; } } } else if (type == QXmlStreamReader::EndElement) { QStringRef _name = xml.name(); if (_name == "slots") return true; } } return true; } bool read(QXmlStreamReader &xml) { while (!xml.atEnd()) { xml.readNext(); QStringRef _name = xml.name(); if (xml.isEndElement() && _name == "account") { if (prefixNameWithCode && !code.isEmpty() && !m_name.startsWith(code)) m_name = code + ' ' + m_name; return true; } if (xml.isStartElement()) { if (_name == "name") m_name = xml.readElementText(QXmlStreamReader::SkipChildElements).trimmed(); else if (_name == "id") id = xml.readElementText(QXmlStreamReader::SkipChildElements).trimmed(); else if (_name == "type") m_type = xml.readElementText(QXmlStreamReader::SkipChildElements).trimmed(); else if (_name == "code") code = xml.readElementText(QXmlStreamReader::SkipChildElements).trimmed(); else if (_name == "parent") parent = xml.readElementText(QXmlStreamReader::SkipChildElements).trimmed(); else if (_name == "slots") readSlots(xml); else { if (debug) qDebug() << "skipping" << _name.toString(); } } } return false; } }; QDebug operator <<(QDebug out, const TemplateAccount &a) { out << "TemplateAccount(" << "name:" << a.m_name << "id:" << a.id << "type:" << a.m_type << "code:" << a.code << "parent:" << a.parent << "slotList:" << a.slotList << ")\n"; return out; } QDebug operator <<(QDebug out, const TemplateAccount::PointerList &a) { out << "TemplateAccount::List("; foreach(const TemplateAccount *account, a) out << *account; out << ")"; return out; } class TemplateFile { public: QString title; QString longDescription; QString shortDescription; TemplateAccount::List accounts; bool read(QXmlStreamReader &xml) { Q_ASSERT(xml.isStartElement() && xml.name() == "gnc-account-example"); while (xml.readNextStartElement()) { QStringRef name = xml.name(); if (xml.name() == "title") title = xml.readElementText().trimmed(); else if (xml.name() == "short-description") shortDescription = xml.readElementText().trimmed().replace(" ", " "); else if (xml.name() == "long-description") longDescription = xml.readElementText().trimmed().replace(" ", " "); else if (xml.name() == "account") { TemplateAccount account; if (account.read(xml)) accounts.append(account); } else { if (debug) qDebug() << "skipping" << name.toString(); xml.skipCurrentElement(); } } return true; } bool writeAsXml(QXmlStreamWriter &xml) { xml.writeStartElement("","title"); xml.writeCharacters(title); xml.writeEndElement(); xml.writeStartElement("","shortdesc"); xml.writeCharacters(shortDescription); xml.writeEndElement(); xml.writeStartElement("","longdesc"); xml.writeCharacters(longDescription); xml.writeEndElement(); xml.writeStartElement("","accounts"); bool result = writeAccountsAsXml(xml); xml.writeEndElement(); return result; } bool writeAccountsAsXml(QXmlStreamWriter &xml, const QString &id="", int index=0) { TemplateAccount::PointerList list; if (index == 0) list = accountsByType("ROOT"); else list = accountsByParentID(id); foreach(TemplateAccount *account, list) { if (account->m_type != "ROOT") { xml.writeStartElement("","account"); xml.writeAttribute("type", QString::number(toKMyMoneyAccountType(account->m_type))); xml.writeAttribute("name", noLevel1Names && index < 2 ? "" : account->m_name); if (withID) xml.writeAttribute("id", account->id); if (withTax) { if (account->slotList.contains("tax-related")) { xml.writeStartElement("flag"); xml.writeAttribute("name","Tax"); - xml.writeAttribute("value",account->slotList["tax-related"]); + xml.writeAttribute("value",account->slotList["tax-related"] == "1" ? "Yes" : "No"); xml.writeEndElement(); } } } index++; writeAccountsAsXml(xml, account->id, index); index--; xml.writeEndElement(); } return true; } TemplateAccount *account(const QString &id) { for(int i=0; i < accounts.size(); i++) { TemplateAccount &account = accounts[i]; if (account.id == id) return &account; } return 0; } TemplateAccount::PointerList accountsByType(const QString &type) { TemplateAccount::PointerList list; for(int i=0; i < accounts.size(); i++) { TemplateAccount &account = accounts[i]; if (account.m_type == type) list.append(&account); } return list; } static bool nameLessThan(TemplateAccount *a1, TemplateAccount *a2) { return a1->m_name < a2->m_name; } TemplateAccount::PointerList accountsByParentID(const QString &parentID) { TemplateAccount::PointerList list; for(int i=0; i < accounts.size(); i++) { TemplateAccount &account = accounts[i]; if (account.parent == parentID) list.append(&account); } qSort(list.begin(), list.end(), nameLessThan); return list; } bool dumpTemplates(const QString &id="", int index=0) { TemplateAccount::PointerList list; if (index == 0) list = accountsByType("ROOT"); else list = accountsByParentID(id); foreach(TemplateAccount *account, list) { QString a; a.fill(' ', index); qDebug() << a << account->m_name << toKMyMoneyAccountType(account->m_type); index++; dumpTemplates(account->id, index); index--; } return true; } }; QDebug operator <<(QDebug out, const TemplateFile &a) { out << "TemplateFile(" << "title:" << a.title << "short description:" << a.shortDescription << "long description:" << a.longDescription << "accounts:"; foreach(const TemplateAccount &account, a.accounts) out << account; out << ")"; return out; } class GnuCashAccountTemplateReader { public: GnuCashAccountTemplateReader() { } bool read(const QString &filename) { QFile file(filename); QTextStream in(&file); in.setCodec("utf-8"); if(!file.open(QIODevice::ReadOnly)) return false; inFileName = filename; return read(in.device()); } TemplateFile &result() { return _template; } bool dumpTemplates() { return _template.dumpTemplates(); } bool writeAsXml(const QString &filename=QString()) { if (filename.isEmpty()) { QTextStream stream(stdout); return writeAsXml(stream.device()); } else { QFile file(filename); if(!file.open(QIODevice::WriteOnly)) return false; return writeAsXml(&file); } } protected: bool checkAndUpdateAvailableNamespaces(QXmlStreamReader &xml) { if (xml.namespaceDeclarations().size() < 5) { qWarning() << "gnucash template file is missing required name space declarations; adding by self"; } xml.addExtraNamespaceDeclaration(QXmlStreamNamespaceDeclaration("act", "http://www.gnucash.org/XML/act")); xml.addExtraNamespaceDeclaration(QXmlStreamNamespaceDeclaration("gnc", "http://www.gnucash.org/XML/gnc")); xml.addExtraNamespaceDeclaration(QXmlStreamNamespaceDeclaration("gnc-act", "http://www.gnucash.org/XML/gnc-act")); xml.addExtraNamespaceDeclaration(QXmlStreamNamespaceDeclaration("cmdty","http://www.gnucash.org/XML/cmdty")); xml.addExtraNamespaceDeclaration(QXmlStreamNamespaceDeclaration("slot","http://www.gnucash.org/XML/slot")); return true; } bool read(QIODevice *device) { m_xml.setDevice(device); while(!m_xml.atEnd()) { m_xml.readNext(); if (m_xml.isStartElement()) { if (m_xml.name() == "gnc-account-example") { checkAndUpdateAvailableNamespaces(m_xml); _template.read(m_xml); } else m_xml.raiseError(QObject::tr("The file is not an gnucash account template file.")); } } if (m_xml.error() != QXmlStreamReader::NoError) qWarning() << m_xml.errorString(); return !m_xml.error(); } bool writeAsXml(QIODevice *device) { QXmlStreamWriter xml(device); xml.setAutoFormatting(true); xml.setAutoFormattingIndent(1); xml.setCodec("utf-8"); xml.writeStartDocument(); QString fileName = inFileName; fileName.replace(QRegExp(".*/accounts"),"accounts"); xml.writeComment(QString("\n" " Converted using xea2kmt from GnuCash sources\n" "\n" " %1\n" "\n" " Please check the source file for possible copyright\n" " and license information.\n" ).arg(fileName)); xml.writeDTD(""); xml.writeStartElement("","kmymoney-account-template"); bool result = _template.writeAsXml(xml); xml.writeEndElement(); xml.writeEndDocument(); return result; } QXmlStreamReader m_xml; TemplateFile _template; QString inFileName; }; void scanDir(QDir dir, QStringList &files) { dir.setNameFilters(QStringList("*.gnucash-xea")); dir.setFilter(QDir::Files | QDir::NoDotAndDotDot | QDir::NoSymLinks); if (debug) qDebug() << "Scanning: " << dir.path(); QStringList fileList = dir.entryList(); for (int i=0; i []"; qWarning() << argv[0] << " --in-dir --out-dir "; qWarning() << "options:"; qWarning() << " --debug - output debug information"; qWarning() << " --help - this page"; qWarning() << " --no-level1-names - do not export account names for top level accounts"; qWarning() << " --prefix-name-with-code - prefix account name with account code if present"; qWarning() << " --with-id - write account id attribute"; qWarning() << " --with-tax-related - parse and export gnucash 'tax-related' flag"; qWarning() << " --in-dir - search for gnucash templates files in "; qWarning() << " --out-dir - generate kmymoney templates below