diff --git a/kmymoney/converter/mymoneygncreader.cpp b/kmymoney/converter/mymoneygncreader.cpp index 55b30c61e..8fd4851e5 100644 --- a/kmymoney/converter/mymoneygncreader.cpp +++ b/kmymoney/converter/mymoneygncreader.cpp @@ -1,2653 +1,2653 @@ /*************************************************************************** mymoneygncreader - description ------------------- begin : Wed Mar 3 2004 copyright : (C) 2000-2004 by Michael Edwardes email : mte@users.sourceforge.net Javier Campos Morales Felix Rodriguez John C Thomas Baumgart Kevin Tambascio ***************************************************************************/ /*************************************************************************** * * * 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 "mymoneygncreader.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #ifndef _GNCFILEANON #include #include #endif #include // ---------------------------------------------------------------------------- // Third party Includes // ------------------------------------------------------------Box21---------------- // Project Includes #include #include "imymoneyserialize.h" #ifndef _GNCFILEANON #include "storage/imymoneystorage.h" #include "kmymoneyutils.h" #include "mymoneyfile.h" #include "mymoneyaccount.h" #include "mymoneysecurity.h" #include "mymoneyschedule.h" #include "mymoneyprice.h" #include "mymoneypayee.h" #include "mymoneysplit.h" #include "mymoneytransaction.h" #include "mymoneyexception.h" #include "kgncimportoptionsdlg.h" #include "kgncpricesourcedlg.h" #include "keditscheduledlg.h" #include "kmymoneyedit.h" #include "kmymoneymoneyvalidator.h" #define TRY try #define CATCH catch (const MyMoneyException &) #define PASS catch (const MyMoneyException &) { throw; } #else #include "mymoneymoney.h" #include // #define i18n QObject::tr #define TRY #define CATCH #define PASS #define MYMONEYEXCEPTION QString #define MyMoneyException QString #define PACKAGE "KMyMoney" #endif // _GNCFILEANON #include "mymoneyenums.h" using namespace eMyMoney; // init static variables double MyMoneyGncReader::m_fileHideFactor = 0.0; double GncObject::m_moneyHideFactor; // user options void MyMoneyGncReader::setOptions() { #ifndef _GNCFILEANON KGncImportOptionsDlg dlg; // display the dialog to allow the user to set own options if (dlg.exec()) { // set users input options m_dropSuspectSchedules = dlg.scheduleOption(); m_investmentOption = dlg.investmentOption(); m_useFinanceQuote = dlg.quoteOption(); m_useTxNotes = dlg.txNotesOption(); m_decoder = dlg.decodeOption(); gncdebug = dlg.generalDebugOption(); xmldebug = dlg.xmlDebugOption(); bAnonymize = dlg.anonymizeOption(); } else { // user declined, so set some sensible defaults m_dropSuspectSchedules = false; // investment option - 0, create investment a/c per stock a/c, 1 = single new investment account, 2 = prompt for each stock // option 2 doesn't really work too well at present m_investmentOption = 0; m_useFinanceQuote = false; m_useTxNotes = false; m_decoder = 0; gncdebug = false; // general debug messages xmldebug = false; // xml trace bAnonymize = false; // anonymize input } // no dialog option for the following; it will set base currency, and print actual XML data developerDebug = false; // set your fave currency here to save getting that enormous dialog each time you run a test // especially if you have to scroll down to USD... if (developerDebug) m_storage->setValue("kmm-baseCurrency", "GBP"); #endif // _GNCFILEANON } GncObject::GncObject() : pMain(0), m_subElementList(0), m_subElementListCount(0), m_dataElementList(0), m_dataElementListCount(0), m_dataPtr(0), m_state(0), m_anonClassList(0), m_anonClass(0) { } // Check that the current element is of a version we are coded for void GncObject::checkVersion(const QString& elName, const QXmlAttributes& elAttrs, const map_elementVersions& map) { TRY { if (map.contains(elName)) { // if it's not in the map, there's nothing to check if (!map[elName].contains(elAttrs.value("version"))) { QString em = Q_FUNC_INFO + i18n(": Sorry. This importer cannot handle version %1 of element %2" , elAttrs.value("version"), elName); throw MYMONEYEXCEPTION(em); } } return ; } PASS } // Check if this element is in the current object's sub element list GncObject *GncObject::isSubElement(const QString& elName, const QXmlAttributes& elAttrs) { TRY { uint i; GncObject *next = 0; for (i = 0; i < m_subElementListCount; i++) { if (elName == m_subElementList[i]) { m_state = i; next = startSubEl(); // go create the sub object if (next != 0) { next->initiate(elName, elAttrs); // initialize it next->m_elementName = elName; // save it's name so we can identify the end } break; } } return (next); } PASS } // Check if this element is in the current object's data element list bool GncObject::isDataElement(const QString &elName, const QXmlAttributes& elAttrs) { TRY { uint i; for (i = 0; i < m_dataElementListCount; i++) { if (elName == m_dataElementList[i]) { m_state = i; dataEl(elAttrs); // go set the pointer so the data can be stored return (true); } } m_dataPtr = 0; // we don't need this, so make sure we don't store extraneous data return (false); } PASS } // return the variable string, decoded if required QString GncObject::var(int i) const { /* This code was needed because the Qt3 XML reader apparently did not process the encoding parameter in the m_decoder == 0 ? m_v[i] : pMain->m_decoder->toUnicode(m_v[i].toUtf8())); } const QString GncObject::getKvpValue(const QString& key, const QString& type) const { QList::const_iterator it; // first check for exact match for (it = m_kvpList.begin(); it != m_kvpList.end(); ++it) { if (((*it).key() == key) && ((type.isEmpty()) || ((*it).type() == type))) return (*it).value(); } // then for partial match for (it = m_kvpList.begin(); it != m_kvpList.end(); ++it) { if (((*it).key().contains(key)) && ((type.isEmpty()) || ((*it).type() == type))) return (*it).value(); } return (QString()); } void GncObject::adjustHideFactor() { m_moneyHideFactor = pMain->m_fileHideFactor * (1.0 + (int)(200.0 * rand() / (RAND_MAX + 1.0))) / 100.0; } // data anonymizer QString GncObject::hide(QString data, unsigned int anonClass) { TRY { if (!pMain->bAnonymize) return (data); // no anonymizing required // counters used to generate names for anonymizer static int nextAccount; static int nextEquity; static int nextPayee; static int nextSched; static QMap anonPayees; // to check for duplicate payee names static QMap anonStocks; // for reference to equities QString result(data); QMap::const_iterator it; MyMoneyMoney in, mresult; switch (anonClass) { case ASIS: // this is not personal data break; case SUPPRESS: // this is personal and is not essential result = ""; break; case NXTACC: // generate account name result = ki18n("Account%1").subs(++nextAccount, -6).toString(); break; case NXTEQU: // generate/return an equity name it = anonStocks.constFind(data); if (it == anonStocks.constEnd()) { result = ki18n("Stock%1").subs(++nextEquity, -6).toString(); anonStocks.insert(data, result); } else { result = (*it); } break; case NXTPAY: // generate/return a payee name it = anonPayees.constFind(data); if (it == anonPayees.constEnd()) { result = ki18n("Payee%1").subs(++nextPayee, -6).toString(); anonPayees.insert(data, result); } else { result = (*it); } break; case NXTSCHD: // generate a schedule name result = ki18n("Schedule%1").subs(++nextSched, -6).toString(); break; case MONEY1: in = MyMoneyMoney(data); if (data == "-1/0") in = MyMoneyMoney(); // spurious gnucash data - causes a crash sometimes mresult = MyMoneyMoney(m_moneyHideFactor) * in; mresult.convert(10000); result = mresult.toString(); break; case MONEY2: in = MyMoneyMoney(data); if (data == "-1/0") in = MyMoneyMoney(); mresult = MyMoneyMoney(m_moneyHideFactor) * in; mresult.convert(10000); mresult.setThousandSeparator(' '); result = mresult.formatMoney("", 2); break; } return (result); } PASS } // dump current object data values // only called if gncdebug set void GncObject::debugDump() { uint i; qDebug() << "Object" << m_elementName; for (i = 0; i < m_dataElementListCount; i++) { qDebug() << m_dataElementList[i] << "=" << m_v[i]; } } //***************************************************************** GncFile::GncFile() { static const QString subEls[] = {"gnc:book", "gnc:count-data", "gnc:commodity", "price", "gnc:account", "gnc:transaction", "gnc:template-transactions", "gnc:schedxaction" }; m_subElementList = subEls; m_subElementListCount = END_FILE_SELS; m_dataElementListCount = 0; m_processingTemplates = false; m_bookFound = false; } GncFile::~GncFile() {} GncObject *GncFile::startSubEl() { TRY { if (pMain->xmldebug) qDebug("File start subel m_state %d", m_state); GncObject *next = 0; switch (m_state) { case BOOK: if (m_bookFound) throw MYMONEYEXCEPTION(i18n("This version of the importer cannot handle multi-book files.")); m_bookFound = true; break; case COUNT: next = new GncCountData; break; case CMDTY: next = new GncCommodity; break; case PRICE: next = new GncPrice; break; case ACCT: // accounts within the template section are ignored if (!m_processingTemplates) next = new GncAccount; break; case TX: next = new GncTransaction(m_processingTemplates); break; case TEMPLATES: m_processingTemplates = true; break; case SCHEDULES: m_processingTemplates = false; next = new GncSchedule; break; default: throw MYMONEYEXCEPTION("GncFile rcvd invalid state"); } return (next); } PASS } void GncFile::endSubEl(GncObject *subObj) { if (pMain->xmldebug) qDebug("File end subel"); if (!m_processingTemplates) delete subObj; // template txs must be saved awaiting schedules m_dataPtr = 0; return ; } //****************************************** GncDate ********************************************* GncDate::GncDate() { m_subElementListCount = 0; static const QString dEls[] = {"ts:date", "gdate"}; m_dataElementList = dEls; m_dataElementListCount = END_Date_DELS; static const unsigned int anonClasses[] = {ASIS, ASIS}; m_anonClassList = anonClasses; for (uint i = 0; i < m_dataElementListCount; i++) m_v.append(QString()); } GncDate::~GncDate() {} //*************************************GncCmdtySpec*************************************** GncCmdtySpec::GncCmdtySpec() { m_subElementListCount = 0; static const QString dEls[] = {"cmdty:space", "cmdty:id"}; m_dataElementList = dEls; m_dataElementListCount = END_CmdtySpec_DELS; static const unsigned int anonClasses[] = {ASIS, ASIS}; m_anonClassList = anonClasses; for (uint i = 0; i < m_dataElementListCount; i++) m_v.append(QString()); } GncCmdtySpec::~GncCmdtySpec() {} QString GncCmdtySpec::hide(QString data, unsigned int) { // hide equity names, but not currency names unsigned int newClass = ASIS; switch (m_state) { case CMDTYID: if (!isCurrency()) newClass = NXTEQU; } return (GncObject::hide(data, newClass)); } //************* GncKvp******************************************** GncKvp::GncKvp() { m_subElementListCount = END_Kvp_SELS; static const QString subEls[] = {"slot"}; // kvp's may be nested m_subElementList = subEls; m_dataElementListCount = END_Kvp_DELS; static const QString dataEls[] = {"slot:key", "slot:value"}; m_dataElementList = dataEls; static const unsigned int anonClasses[] = {ASIS, ASIS}; m_anonClassList = anonClasses; for (uint i = 0; i < m_dataElementListCount; i++) m_v.append(QString()); } GncKvp::~GncKvp() {} void GncKvp::dataEl(const QXmlAttributes& elAttrs) { switch (m_state) { case VALUE: m_kvpType = elAttrs.value("type"); } m_dataPtr = &(m_v[m_state]); if (key().contains("formula")) { m_anonClass = MONEY2; } else { m_anonClass = ASIS; } return ; } GncObject *GncKvp::startSubEl() { if (pMain->xmldebug) qDebug("Kvp start subel m_state %d", m_state); TRY { GncObject *next = 0; switch (m_state) { case KVP: next = new GncKvp; break; default: throw MYMONEYEXCEPTION("GncKvp rcvd invalid m_state "); } return (next); } PASS } void GncKvp::endSubEl(GncObject *subObj) { if (pMain->xmldebug) qDebug("Kvp end subel"); m_kvpList.append(*(static_cast (subObj))); m_dataPtr = 0; return ; } //*********************************GncLot********************************************* GncLot::GncLot() { m_subElementListCount = 0; m_dataElementListCount = 0; } GncLot::~GncLot() {} //*********************************GncCountData*************************************** GncCountData::GncCountData() { m_subElementListCount = 0; m_dataElementListCount = 0; m_v.append(QString()); // only 1 data item } GncCountData::~GncCountData() {} void GncCountData::initiate(const QString&, const QXmlAttributes& elAttrs) { m_countType = elAttrs.value("cd:type"); m_dataPtr = &(m_v[0]); return ; } void GncCountData::terminate() { int i = m_v[0].toInt(); if (m_countType == "commodity") { pMain->setGncCommodityCount(i); return ; } if (m_countType == "account") { pMain->setGncAccountCount(i); return ; } if (m_countType == "transaction") { pMain->setGncTransactionCount(i); return ; } if (m_countType == "schedxaction") { pMain->setGncScheduleCount(i); return ; } if (i != 0) { if (m_countType == "budget") pMain->setBudgetsFound(true); else if (m_countType.left(7) == "gnc:Gnc") pMain->setSmallBusinessFound(true); else if (pMain->xmldebug) qDebug() << "Unknown count type" << m_countType; } return ; } //*********************************GncCommodity*************************************** GncCommodity::GncCommodity() { m_subElementListCount = 0; static const QString dEls[] = {"cmdty:space", "cmdty:id", "cmdty:name", "cmdty:fraction"}; m_dataElementList = dEls; m_dataElementListCount = END_Commodity_DELS; static const unsigned int anonClasses[] = {ASIS, NXTEQU, SUPPRESS, ASIS}; m_anonClassList = anonClasses; for (uint i = 0; i < m_dataElementListCount; i++) m_v.append(QString()); } GncCommodity::~GncCommodity() {} void GncCommodity::terminate() { TRY { pMain->convertCommodity(this); return ; } PASS } //************* GncPrice******************************************** GncPrice::GncPrice() { static const QString subEls[] = {"price:commodity", "price:currency", "price:time"}; m_subElementList = subEls; m_subElementListCount = END_Price_SELS; m_dataElementListCount = END_Price_DELS; static const QString dataEls[] = {"price:value"}; m_dataElementList = dataEls; static const unsigned int anonClasses[] = {ASIS}; m_anonClassList = anonClasses; for (uint i = 0; i < m_dataElementListCount; i++) m_v.append(QString()); m_vpCommodity = 0; m_vpCurrency = 0; m_vpPriceDate = 0; } GncPrice::~GncPrice() { delete m_vpCommodity; delete m_vpCurrency; delete m_vpPriceDate; } GncObject *GncPrice::startSubEl() { TRY { GncObject *next = 0; switch (m_state) { case CMDTY: next = new GncCmdtySpec; break; case CURR: next = new GncCmdtySpec; break; case PRICEDATE: next = new GncDate; break; default: throw MYMONEYEXCEPTION("GncPrice rcvd invalid m_state"); } return (next); } PASS } void GncPrice::endSubEl(GncObject *subObj) { TRY { switch (m_state) { case CMDTY: m_vpCommodity = static_cast(subObj); break; case CURR: m_vpCurrency = static_cast(subObj); break; case PRICEDATE: m_vpPriceDate = static_cast(subObj); break; default: throw MYMONEYEXCEPTION("GncPrice rcvd invalid m_state"); } return; } PASS } void GncPrice::terminate() { TRY { pMain->convertPrice(this); return ; } PASS } //************* GncAccount******************************************** GncAccount::GncAccount() { m_subElementListCount = END_Account_SELS; static const QString subEls[] = {"act:commodity", "slot", "act:lots"}; m_subElementList = subEls; m_dataElementListCount = END_Account_DELS; static const QString dataEls[] = {"act:id", "act:name", "act:description", "act:type", "act:parent" }; m_dataElementList = dataEls; static const unsigned int anonClasses[] = {ASIS, NXTACC, SUPPRESS, ASIS, ASIS}; m_anonClassList = anonClasses; for (uint i = 0; i < m_dataElementListCount; i++) m_v.append(QString()); m_vpCommodity = 0; } GncAccount::~GncAccount() { delete m_vpCommodity; } GncObject *GncAccount::startSubEl() { TRY { if (pMain->xmldebug) qDebug("Account start subel m_state %d", m_state); GncObject *next = 0; switch (m_state) { case CMDTY: next = new GncCmdtySpec; break; case KVP: next = new GncKvp; break; case LOTS: next = new GncLot(); pMain->setLotsFound(true); // we don't handle lots; just set flag to report break; default: throw MYMONEYEXCEPTION("GncAccount rcvd invalid m_state"); } return (next); } PASS } void GncAccount::endSubEl(GncObject *subObj) { if (pMain->xmldebug) qDebug("Account end subel"); switch (m_state) { case CMDTY: m_vpCommodity = static_cast(subObj); break; case KVP: m_kvpList.append(*(static_cast (subObj))); } return ; } void GncAccount::terminate() { TRY { pMain->convertAccount(this); return ; } PASS } //************* GncTransaction******************************************** GncTransaction::GncTransaction(bool processingTemplates) { m_subElementListCount = END_Transaction_SELS; static const QString subEls[] = {"trn:currency", "trn:date-posted", "trn:date-entered", "trn:split", "slot" }; m_subElementList = subEls; m_dataElementListCount = END_Transaction_DELS; static const QString dataEls[] = {"trn:id", "trn:num", "trn:description"}; m_dataElementList = dataEls; static const unsigned int anonClasses[] = {ASIS, SUPPRESS, NXTPAY}; m_anonClassList = anonClasses; adjustHideFactor(); m_template = processingTemplates; for (uint i = 0; i < m_dataElementListCount; i++) m_v.append(QString()); m_vpCurrency = 0; m_vpDateEntered = m_vpDatePosted = 0; } GncTransaction::~GncTransaction() { delete m_vpCurrency; delete m_vpDatePosted; delete m_vpDateEntered; } GncObject *GncTransaction::startSubEl() { TRY { if (pMain->xmldebug) qDebug("Transaction start subel m_state %d", m_state); GncObject *next = 0; switch (m_state) { case CURRCY: next = new GncCmdtySpec; break; case POSTED: case ENTERED: next = new GncDate; break; case SPLIT: if (isTemplate()) { next = new GncTemplateSplit; } else { next = new GncSplit; } break; case KVP: next = new GncKvp; break; default: throw MYMONEYEXCEPTION("GncTransaction rcvd invalid m_state"); } return (next); } PASS } void GncTransaction::endSubEl(GncObject *subObj) { if (pMain->xmldebug) qDebug("Transaction end subel"); switch (m_state) { case CURRCY: m_vpCurrency = static_cast(subObj); break; case POSTED: m_vpDatePosted = static_cast(subObj); break; case ENTERED: m_vpDateEntered = static_cast(subObj); break; case SPLIT: m_splitList.append(subObj); break; case KVP: m_kvpList.append(*(static_cast (subObj))); } return ; } void GncTransaction::terminate() { TRY { if (isTemplate()) { pMain->saveTemplateTransaction(this); } else { pMain->convertTransaction(this); } return ; } PASS } //************* GncSplit******************************************** GncSplit::GncSplit() { m_subElementListCount = END_Split_SELS; static const QString subEls[] = {"split:reconcile-date"}; m_subElementList = subEls; m_dataElementListCount = END_Split_DELS; static const QString dataEls[] = {"split:id", "split:memo", "split:reconciled-state", "split:value", "split:quantity", "split:account" }; m_dataElementList = dataEls; static const unsigned int anonClasses[] = {ASIS, SUPPRESS, ASIS, MONEY1, MONEY1, ASIS}; m_anonClassList = anonClasses; for (uint i = 0; i < m_dataElementListCount; i++) m_v.append(QString()); m_vpDateReconciled = 0; } GncSplit::~GncSplit() { delete m_vpDateReconciled; } GncObject *GncSplit::startSubEl() { TRY { GncObject *next = 0; switch (m_state) { case RECDATE: next = new GncDate; break; default: throw MYMONEYEXCEPTION("GncTemplateSplit rcvd invalid m_state "); } return (next); } PASS } void GncSplit::endSubEl(GncObject *subObj) { if (pMain->xmldebug) qDebug("Split end subel"); switch (m_state) { case RECDATE: m_vpDateReconciled = static_cast(subObj); break; } return ; } //************* GncTemplateSplit******************************************** GncTemplateSplit::GncTemplateSplit() { m_subElementListCount = END_TemplateSplit_SELS; static const QString subEls[] = {"slot"}; m_subElementList = subEls; m_dataElementListCount = END_TemplateSplit_DELS; static const QString dataEls[] = {"split:id", "split:memo", "split:reconciled-state", "split:value", "split:quantity", "split:account" }; m_dataElementList = dataEls; static const unsigned int anonClasses[] = {ASIS, SUPPRESS, ASIS, MONEY1, MONEY1, ASIS}; m_anonClassList = anonClasses; for (uint i = 0; i < m_dataElementListCount; i++) m_v.append(QString()); } GncTemplateSplit::~GncTemplateSplit() {} GncObject *GncTemplateSplit::startSubEl() { if (pMain->xmldebug) qDebug("TemplateSplit start subel m_state %d", m_state); TRY { GncObject *next = 0; switch (m_state) { case KVP: next = new GncKvp; break; default: throw MYMONEYEXCEPTION("GncTemplateSplit rcvd invalid m_state"); } return (next); } PASS } void GncTemplateSplit::endSubEl(GncObject *subObj) { if (pMain->xmldebug) qDebug("TemplateSplit end subel"); m_kvpList.append(*(static_cast (subObj))); m_dataPtr = 0; return ; } //************* GncSchedule******************************************** GncSchedule::GncSchedule() { m_subElementListCount = END_Schedule_SELS; static const QString subEls[] = {"sx:start", "sx:last", "sx:end", "gnc:freqspec", "gnc:recurrence", "sx:deferredInstance"}; m_subElementList = subEls; m_dataElementListCount = END_Schedule_DELS; static const QString dataEls[] = {"sx:name", "sx:enabled", "sx:autoCreate", "sx:autoCreateNotify", "sx:autoCreateDays", "sx:advanceCreateDays", "sx:advanceRemindDays", "sx:instanceCount", "sx:num-occur", "sx:rem-occur", "sx:templ-acct" }; m_dataElementList = dataEls; static const unsigned int anonClasses[] = {NXTSCHD, ASIS, ASIS, ASIS, ASIS, ASIS, ASIS, ASIS, ASIS, ASIS, ASIS}; m_anonClassList = anonClasses; for (uint i = 0; i < m_dataElementListCount; i++) m_v.append(QString()); m_vpStartDate = m_vpLastDate = m_vpEndDate = 0; m_vpFreqSpec = 0; m_vpRecurrence.clear(); m_vpSchedDef = 0; } GncSchedule::~GncSchedule() { delete m_vpStartDate; delete m_vpLastDate; delete m_vpEndDate; delete m_vpFreqSpec; delete m_vpSchedDef; } GncObject *GncSchedule::startSubEl() { if (pMain->xmldebug) qDebug("Schedule start subel m_state %d", m_state); TRY { GncObject *next = 0; switch (m_state) { case STARTDATE: case LASTDATE: case ENDDATE: next = new GncDate; break; case FREQ: next = new GncFreqSpec; break; case RECURRENCE: next = new GncRecurrence; break; case DEFINST: next = new GncSchedDef; break; default: throw MYMONEYEXCEPTION("GncSchedule rcvd invalid m_state"); } return (next); } PASS } void GncSchedule::endSubEl(GncObject *subObj) { if (pMain->xmldebug) qDebug("Schedule end subel"); switch (m_state) { case STARTDATE: m_vpStartDate = static_cast(subObj); break; case LASTDATE: m_vpLastDate = static_cast(subObj); break; case ENDDATE: m_vpEndDate = static_cast(subObj); break; case FREQ: m_vpFreqSpec = static_cast(subObj); break; case RECURRENCE: m_vpRecurrence.append(static_cast(subObj)); break; case DEFINST: m_vpSchedDef = static_cast(subObj); break; } return ; } void GncSchedule::terminate() { TRY { pMain->convertSchedule(this); return ; } PASS } //************* GncFreqSpec******************************************** GncFreqSpec::GncFreqSpec() { m_subElementListCount = END_FreqSpec_SELS; static const QString subEls[] = {"gnc:freqspec"}; m_subElementList = subEls; m_dataElementListCount = END_FreqSpec_DELS; static const QString dataEls[] = {"fs:ui_type", "fs:monthly", "fs:daily", "fs:weekly", "fs:interval", "fs:offset", "fs:day" }; m_dataElementList = dataEls; static const unsigned int anonClasses[] = {ASIS, ASIS, ASIS, ASIS, ASIS, ASIS, ASIS }; m_anonClassList = anonClasses; for (uint i = 0; i < m_dataElementListCount; i++) m_v.append(QString()); } GncFreqSpec::~GncFreqSpec() {} GncObject *GncFreqSpec::startSubEl() { TRY { if (pMain->xmldebug) qDebug("FreqSpec start subel m_state %d", m_state); GncObject *next = 0; switch (m_state) { case COMPO: next = new GncFreqSpec; break; default: throw MYMONEYEXCEPTION("GncFreqSpec rcvd invalid m_state"); } return (next); } PASS } void GncFreqSpec::endSubEl(GncObject *subObj) { if (pMain->xmldebug) qDebug("FreqSpec end subel"); switch (m_state) { case COMPO: m_fsList.append(subObj); break; } m_dataPtr = 0; return ; } void GncFreqSpec::terminate() { pMain->convertFreqSpec(this); return ; } //************* GncRecurrence******************************************** GncRecurrence::GncRecurrence() : m_vpStartDate(0) { m_subElementListCount = END_Recurrence_SELS; static const QString subEls[] = {"recurrence:start"}; m_subElementList = subEls; m_dataElementListCount = END_Recurrence_DELS; static const QString dataEls[] = {"recurrence:mult", "recurrence:period_type"}; m_dataElementList = dataEls; static const unsigned int anonClasses[] = {ASIS, ASIS}; m_anonClassList = anonClasses; for (uint i = 0; i < m_dataElementListCount; i++) m_v.append(QString()); } GncRecurrence::~GncRecurrence() { delete m_vpStartDate; } GncObject *GncRecurrence::startSubEl() { TRY { if (pMain->xmldebug) qDebug("Recurrence start subel m_state %d", m_state); GncObject *next = 0; switch (m_state) { case STARTDATE: next = new GncDate; break; default: throw MYMONEYEXCEPTION("GncRecurrence rcvd invalid m_state"); } return (next); } PASS } void GncRecurrence::endSubEl(GncObject *subObj) { if (pMain->xmldebug) qDebug("Recurrence end subel"); switch (m_state) { case STARTDATE: m_vpStartDate = static_cast(subObj); break; } m_dataPtr = 0; return ; } void GncRecurrence::terminate() { pMain->convertRecurrence(this); return ; } QString GncRecurrence::getFrequency() const { // This function converts a gnucash 2.2 recurrence specification into it's previous equivalent // This will all need re-writing when MTE finishes the schedule re-write if (periodType() == "once") return("once"); if ((periodType() == "day") && (mult() == "1")) return("daily"); if (periodType() == "week") { if (mult() == "1") return ("weekly"); if (mult() == "2") return ("bi_weekly"); if (mult() == "4") return ("four-weekly"); } if (periodType() == "month") { if (mult() == "1") return ("monthly"); if (mult() == "2") return ("two-monthly"); if (mult() == "3") return ("quarterly"); if (mult() == "4") return ("tri_annually"); if (mult() == "6") return ("semi_yearly"); if (mult() == "12") return ("yearly"); if (mult() == "24") return ("two-yearly"); } return ("unknown"); } //************* GncSchedDef******************************************** GncSchedDef::GncSchedDef() { // process ing for this sub-object is undefined at the present time m_subElementListCount = 0; m_dataElementListCount = 0; } GncSchedDef::~GncSchedDef() {} /************************************************************************************************ XML Reader ************************************************************************************************/ XmlReader::XmlReader(MyMoneyGncReader *pM) : m_source(0), m_reader(0), m_co(0), pMain(pM), m_headerFound(false) { } void XmlReader::processFile(QIODevice* pDevice) { m_source = new QXmlInputSource(pDevice); // set up the Qt XML reader m_reader = new QXmlSimpleReader; m_reader->setContentHandler(this); // go read the file if (!m_reader->parse(m_source)) { throw MYMONEYEXCEPTION(i18n("Input file cannot be parsed; may be corrupt\n%1", errorString())); } delete m_reader; delete m_source; return ; } // XML handling routines bool XmlReader::startDocument() { m_co = new GncFile; // create initial object, push to stack , pass it the 'main' pointer m_os.push(m_co); m_co->setPm(pMain); m_headerFound = false; #ifdef _GNCFILEANON pMain->oStream << ""; lastType = -1; indentCount = 0; #endif // _GNCFILEANON return (true); } bool XmlReader::startElement(const QString&, const QString&, const QString& elName , const QXmlAttributes& elAttrs) { try { if (pMain->gncdebug) qDebug() << "XML start -" << elName; #ifdef _GNCFILEANON int i; QString spaces; // anonymizer - write data if (elName == "gnc:book" || elName == "gnc:count-data" || elName == "book:id") lastType = -1; pMain->oStream << endl; switch (lastType) { case 0: indentCount += 2; // tricky fall through here case 2: spaces.fill(' ', indentCount); pMain->oStream << spaces.toLatin1(); break; } pMain->oStream << '<' << elName; for (i = 0; i < elAttrs.count(); ++i) { pMain->oStream << ' ' << elAttrs.qName(i) << '=' << '"' << elAttrs.value(i) << '"'; } pMain->oStream << '>'; lastType = 0; #else if ((!m_headerFound) && (elName != "gnc-v2")) throw MYMONEYEXCEPTION(i18n("Invalid header for file. Should be 'gnc-v2'")); m_headerFound = true; #endif // _GNCFILEANON m_co->checkVersion(elName, elAttrs, pMain->m_versionList); // check if this is a sub object element; if so, push stack and initialize GncObject *temp = m_co->isSubElement(elName, elAttrs); if (temp != 0) { m_os.push(temp); m_co = m_os.top(); m_co->setVersion(elAttrs.value("version")); m_co->setPm(pMain); // pass the 'main' pointer to the sub object // return true; // removed, as we hit a return true anyway } #if 0 // check for a data element if (m_co->isDataElement(elName, elAttrs)) return (true); #endif else { // reduced the above to m_co->isDataElement(elName, elAttrs); } } catch (const MyMoneyException &e) { #ifndef _GNCFILEANON // we can't pass on exceptions here coz the XML reader won't catch them and we just abort KMessageBox::error(0, i18n("Import failed:\n\n%1", e.what()), PACKAGE); qWarning("%s", qPrintable(e.what())); #else qWarning("%s", e->toLatin1()); #endif // _GNCFILEANON } return true; // to keep compiler happy } bool XmlReader::endElement(const QString&, const QString&, const QString&elName) { try { if (pMain->xmldebug) qDebug() << "XML end -" << elName; #ifdef _GNCFILEANON QString spaces; switch (lastType) { case 2: indentCount -= 2; spaces.fill(' ', indentCount); pMain->oStream << endl << spaces.toLatin1(); break; } pMain->oStream << "' ; lastType = 2; #endif // _GNCFILEANON m_co->resetDataPtr(); // so we don't get extraneous data loaded into the variables if (elName == m_co->getElName()) { // check if this is the end of the current object if (pMain->gncdebug) m_co->debugDump(); // dump the object data (temp) // call the terminate routine, pop the stack, and advise the parent that it's done m_co->terminate(); GncObject *temp = m_co; m_os.pop(); m_co = m_os.top(); m_co->endSubEl(temp); } return (true); } catch (const MyMoneyException &e) { #ifndef _GNCFILEANON // we can't pass on exceptions here coz the XML reader won't catch them and we just abort KMessageBox::error(0, i18n("Import failed:\n\n%1", e.what()), PACKAGE); qWarning("%s", qPrintable(e.what())); #else qWarning("%s", e->toLatin1()); #endif // _GNCFILEANON } return (true); // to keep compiler happy } bool XmlReader::characters(const QString &data) { if (pMain->xmldebug) qDebug("XML Data received - %d bytes", data.length()); QString pData = data.trimmed(); // data may contain line feeds and indentation spaces if (!pData.isEmpty()) { if (pMain->developerDebug) qDebug() << "XML Data -" << pData; m_co->storeData(pData); //go store it #ifdef _GNCFILEANON QString anonData = m_co->getData(); if (anonData.isEmpty()) anonData = pData; // there must be a Qt standard way of doing the following but I can't ... find it anonData.replace('<', "<"); anonData.replace('>', ">"); anonData.replace('&', "&"); pMain->oStream << anonData; // write original data lastType = 1; #endif // _GNCFILEANON } return (true); } bool XmlReader::endDocument() { #ifdef _GNCFILEANON pMain->oStream << endl << endl; pMain->oStream << "" << endl; pMain->oStream << "" << endl; pMain->oStream << "" << endl; #endif // _GNCFILEANON return (true); } /******************************************************************************************* Main class for this module Controls overall operation of the importer ********************************************************************************************/ //***************** Constructor *********************** MyMoneyGncReader::MyMoneyGncReader() : m_dropSuspectSchedules(0), m_investmentOption(0), m_useFinanceQuote(0), m_useTxNotes(0), gncdebug(0), xmldebug(0), bAnonymize(0), developerDebug(0), m_xr(0), m_progressCallback(0), m_ccCount(0), m_orCount(0), m_scCount(0), m_potentialTransfer(0), m_suspectSchedule(false) { #ifndef _GNCFILEANON m_storage = 0; #endif // _GNCFILEANON // to hold gnucash count data (only used for progress bar) m_gncCommodityCount = m_gncAccountCount = m_gncTransactionCount = m_gncScheduleCount = 0; m_smallBusinessFound = m_budgetsFound = m_lotsFound = false; m_commodityCount = m_priceCount = m_accountCount = m_transactionCount = m_templateCount = m_scheduleCount = 0; m_decoder = 0; // build a list of valid versions static const QString versionList[] = {"gnc:book 2.0.0", "gnc:commodity 2.0.0", "gnc:pricedb 1", "gnc:account 2.0.0", "gnc:transaction 2.0.0", "gnc:schedxaction 1.0.0", "gnc:schedxaction 2.0.0", // for gnucash 2.2 onward "gnc:freqspec 1.0.0", "zzz" // zzz = stopper }; unsigned int i; for (i = 0; versionList[i] != "zzz"; ++i) m_versionList[versionList[i].section(' ', 0, 0)].append(versionList[i].section(' ', 1, 1)); } //***************** Destructor ************************* MyMoneyGncReader::~MyMoneyGncReader() {} //**************************** Main Entry Point ************************************ #ifndef _GNCFILEANON void MyMoneyGncReader::readFile(QIODevice* pDevice, IMyMoneySerialize* storage) { Q_CHECK_PTR(pDevice); Q_CHECK_PTR(storage); m_storage = dynamic_cast(storage); qDebug("Entering gnucash importer"); setOptions(); // get a file anonymization factor from the user if (bAnonymize) setFileHideFactor(); //m_defaultPayee = createPayee (i18n("Unknown payee")); MyMoneyFile::instance()->attachStorage(m_storage); MyMoneyFileTransaction ft; m_xr = new XmlReader(this); bool blocked = MyMoneyFile::instance()->signalsBlocked(); MyMoneyFile::instance()->blockSignals(true); try { m_xr->processFile(pDevice); terminate(); // do all the wind-up things ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::error(0, i18n("Import failed:\n\n%1", e.what()), PACKAGE); qWarning("%s", qPrintable(e.what())); } // end catch MyMoneyFile::instance()->blockSignals(blocked); MyMoneyFile::instance()->detachStorage(m_storage); signalProgress(0, 1, i18n("Import complete")); // switch off progress bar delete m_xr; qDebug("Exiting gnucash importer"); } #else // Control code for the file anonymizer void MyMoneyGncReader::readFile(QString in, QString out) { QFile pDevice(in); if (!pDevice.open(QIODevice::ReadOnly)) qWarning("Can't open input file"); QFile outFile(out); if (!outFile.open(QIODevice::WriteOnly)) qWarning("Can't open output file"); oStream.setDevice(&outFile); bAnonymize = true; // get a file anonymization factor from the user setFileHideFactor(); m_xr = new XmlReader(this); try { m_xr->processFile(&pDevice); } catch (const MyMoneyException &e) { qWarning("%s", e->toLatin1()); } // end catch delete m_xr; pDevice.close(); outFile.close(); return ; } #include int main(int argc, char ** argv) { QApplication a(argc, argv); MyMoneyGncReader m; QString inFile, outFile; if (argc > 0) inFile = a.argv()[1]; if (argc > 1) outFile = a.argv()[2]; if (inFile.isEmpty()) { inFile = KFileDialog::getOpenFileName("", "Gnucash files(*.nc *)", 0); } if (inFile.isEmpty()) qWarning("Input file required"); if (outFile.isEmpty()) outFile = inFile + ".anon"; m.readFile(inFile, outFile); exit(0); } #endif // _GNCFILEANON void MyMoneyGncReader::setFileHideFactor() { #define MINFILEHIDEF 0.01 #define MAXFILEHIDEF 99.99 srand(QTime::currentTime().second()); // seed randomizer for anonymize m_fileHideFactor = 0.0; while (m_fileHideFactor == 0.0) { m_fileHideFactor = QInputDialog::getDouble(0, i18n("Disguise your wealth"), i18n("Each monetary value on your file will be multiplied by a random number between 0.01 and 1.99\n" "with a different value used for each transaction. In addition, to further disguise the true\n" "values, you may enter a number between %1 and %2 which will be applied to all values.\n" "These numbers will not be stored in the file.", MINFILEHIDEF, MAXFILEHIDEF), (1.0 + (int)(1000.0 * rand() / (RAND_MAX + 1.0))) / 100.0, MINFILEHIDEF, MAXFILEHIDEF, 2); } } #ifndef _GNCFILEANON //********************************* convertCommodity ******************************************* void MyMoneyGncReader::convertCommodity(const GncCommodity *gcm) { Q_CHECK_PTR(gcm); MyMoneySecurity equ; if (m_commodityCount == 0) signalProgress(0, m_gncCommodityCount, i18n("Loading commodities...")); if (!gcm->isCurrency()) { // currencies should not be present here but... equ.setName(gcm->name()); equ.setTradingSymbol(gcm->id()); equ.setTradingMarket(gcm->space()); // the 'space' may be market or quote source, dep on what the user did // don't set the source here since he may not want quotes //equ.setValue ("kmm-online-source", gcm->space()); // we don't know, so use it as both equ.setTradingCurrency(""); // not available here, will set from pricedb or transaction equ.setSecurityType(Security::Type::Stock); // default to it being a stock //tell the storage objects we have a new equity object. equ.setSmallestAccountFraction(gcm->fraction().toInt()); m_storage->addSecurity(equ); //assign the gnucash id as the key into the map to find our id if (gncdebug) qDebug() << "mapping, key =" << gcm->id() << "id =" << equ.id(); m_mapEquities[gcm->id().toUtf8()] = equ.id(); } signalProgress(++m_commodityCount, 0); return ; } //******************************* convertPrice ************************************************ void MyMoneyGncReader::convertPrice(const GncPrice *gpr) { Q_CHECK_PTR(gpr); // add this to our price history if (m_priceCount == 0) signalProgress(0, 1, i18n("Loading prices...")); MyMoneyMoney rate(convBadValue(gpr->value())); if (gpr->commodity()->isCurrency()) { MyMoneyPrice exchangeRate(gpr->commodity()->id().toUtf8(), gpr->currency()->id().toUtf8(), gpr->priceDate(), rate, i18n("Imported History")); if (!exchangeRate.rate(QString()).isZero()) m_storage->addPrice(exchangeRate); } else { MyMoneySecurity e = m_storage->security(m_mapEquities[gpr->commodity()->id().toUtf8()]); if (gncdebug) qDebug() << "Searching map, key = " << gpr->commodity()->id() << ", found id =" << e.id().data(); e.setTradingCurrency(gpr->currency()->id().toUtf8()); MyMoneyPrice stockPrice(e.id(), gpr->currency()->id().toUtf8(), gpr->priceDate(), rate, i18n("Imported History")); if (!stockPrice.rate(QString()).isZero()) m_storage->addPrice(stockPrice); m_storage->modifySecurity(e); } signalProgress(++m_priceCount, 0); return ; } //*********************************convertAccount **************************************** void MyMoneyGncReader::convertAccount(const GncAccount* gac) { Q_CHECK_PTR(gac); TRY { // we don't care about the GNC root account if ("ROOT" == gac->type()) { m_rootId = gac->id().toUtf8(); return; } MyMoneyAccount acc; if (m_accountCount == 0) signalProgress(0, m_gncAccountCount, i18n("Loading accounts...")); acc.setName(gac->name()); acc.setDescription(gac->desc()); QDate currentDate = QDate::currentDate(); acc.setOpeningDate(currentDate); acc.setLastModified(currentDate); acc.setLastReconciliationDate(currentDate); if (gac->commodity()->isCurrency()) { acc.setCurrencyId(gac->commodity()->id().toUtf8()); m_currencyCount[gac->commodity()->id()]++; } acc.setParentAccountId(gac->parent().toUtf8()); // now determine the account type and its parent id /* This list taken from # Feb 2006: A RELAX NG Compact schema for gnucash "v2" XML files. # Copyright (C) 2006 Joshua Sled "NO_TYPE" "BANK" "CASH" "CREDIT" "ASSET" "LIABILITY" "STOCK" "MUTUAL" "CURRENCY" "INCOME" "EXPENSE" "EQUITY" "RECEIVABLE" "PAYABLE" "CHECKING" "SAVINGS" "MONEYMRKT" "CREDITLINE" Some don't seem to be used in practice. Not sure what CREDITLINE s/be converted as. */ if ("BANK" == gac->type() || "CHECKING" == gac->type()) { acc.setAccountType(Account::Type::Checkings); } else if ("SAVINGS" == gac->type()) { acc.setAccountType(Account::Type::Savings); } else if ("ASSET" == gac->type()) { acc.setAccountType(Account::Type::Asset); } else if ("CASH" == gac->type()) { acc.setAccountType(Account::Type::Cash); } else if ("CURRENCY" == gac->type()) { acc.setAccountType(Account::Type::Cash); } else if ("STOCK" == gac->type() || "MUTUAL" == gac->type()) { // gnucash allows a 'broker' account to be denominated as type STOCK, but with // a currency balance. We do not need to create a stock account for this // actually, the latest version of gnc (1.8.8) doesn't seem to allow you to do // this any more, though I do have one in my own account... if (gac->commodity()->isCurrency()) { acc.setAccountType(Account::Type::Investment); } else { acc.setAccountType(Account::Type::Stock); } } else if ("EQUITY" == gac->type()) { acc.setAccountType(Account::Type::Equity); } else if ("LIABILITY" == gac->type()) { acc.setAccountType(Account::Type::Liability); } else if ("CREDIT" == gac->type()) { acc.setAccountType(Account::Type::CreditCard); } else if ("INCOME" == gac->type()) { acc.setAccountType(Account::Type::Income); } else if ("EXPENSE" == gac->type()) { acc.setAccountType(Account::Type::Expense); } else if ("RECEIVABLE" == gac->type()) { acc.setAccountType(Account::Type::Asset); } else if ("PAYABLE" == gac->type()) { acc.setAccountType(Account::Type::Liability); } else if ("MONEYMRKT" == gac->type()) { acc.setAccountType(Account::Type::MoneyMarket); } else { // we have here an account type we can't currently handle QString em = i18n("Current importer does not recognize GnuCash account type %1", gac->type()); throw MYMONEYEXCEPTION(em); } // if no parent account is present, assign to one of our standard accounts if ((acc.parentAccountId().isEmpty()) || (acc.parentAccountId() == m_rootId)) { switch (acc.accountGroup()) { case Account::Type::Asset: acc.setParentAccountId(m_storage->asset().id()); break; case Account::Type::Liability: acc.setParentAccountId(m_storage->liability().id()); break; case Account::Type::Income: acc.setParentAccountId(m_storage->income().id()); break; case Account::Type::Expense: acc.setParentAccountId(m_storage->expense().id()); break; case Account::Type::Equity: acc.setParentAccountId(m_storage->equity().id()); break; default: break; // not necessary but avoids compiler warnings } } // extra processing for a stock account if (acc.accountType() == Account::Type::Stock) { // save the id for later linking to investment account m_stockList.append(gac->id()); // set the equity type MyMoneySecurity e = m_storage->security(m_mapEquities[gac->commodity()->id().toUtf8()]); if (gncdebug) qDebug() << "Acct equity search, key =" << gac->commodity()->id() << "found id =" << e.id(); acc.setCurrencyId(e.id()); // actually, the security id if ("MUTUAL" == gac->type()) { e.setSecurityType(Security::Type::MutualFund); if (gncdebug) qDebug() << "Setting" << e.name() << "to mutual"; m_storage->modifySecurity(e); } QString priceSource = gac->getKvpValue("price-source", "string"); if (!priceSource.isEmpty()) getPriceSource(e, priceSource); } if (gac->getKvpValue("tax-related", "integer") == QChar('1')) acc.setValue("Tax", "Yes"); // all the details from the file about the account should be known by now. // calling addAccount will automatically fill in the account ID. m_storage->addAccount(acc); m_mapIds[gac->id().toUtf8()] = acc.id(); // to link gnucash id to ours for tx posting if (gncdebug) qDebug() << "Gnucash account" << gac->id() << "has id of" << acc.id() << ", type of" << MyMoneyAccount::accountTypeToString(acc.accountType()) << "parent is" << acc.parentAccountId(); signalProgress(++m_accountCount, 0); return ; } PASS } //********************************************** convertTransaction ***************************** void MyMoneyGncReader::convertTransaction(const GncTransaction *gtx) { Q_CHECK_PTR(gtx); MyMoneyTransaction tx; MyMoneySplit split; unsigned int i; if (m_transactionCount == 0) signalProgress(0, m_gncTransactionCount, i18n("Loading transactions...")); // initialize class variables related to transactions m_txCommodity = ""; m_txPayeeId = ""; m_potentialTransfer = true; m_splitList.clear(); m_liabilitySplitList.clear(); m_otherSplitList.clear(); // payee, dates, commodity if (!gtx->desc().isEmpty()) m_txPayeeId = createPayee(gtx->desc()); tx.setEntryDate(gtx->dateEntered()); tx.setPostDate(gtx->datePosted()); m_txDatePosted = tx.postDate(); // save for use in splits m_txChequeNo = gtx->no(); // ditto tx.setCommodity(gtx->currency().toUtf8()); m_txCommodity = tx.commodity(); // save in storage, maybe needed for Orphan accounts // process splits for (i = 0; i < gtx->splitCount(); i++) { convertSplit(static_cast(gtx->getSplit(i))); } // handle the odd case of just one split, which gnc allows, // by just duplicating the split // of course, we should change the sign but this case has only ever been seen // when the balance is zero, and can cause kmm to crash, so... if (gtx->splitCount() == 1) { convertSplit(static_cast(gtx->getSplit(0))); } m_splitList += m_liabilitySplitList += m_otherSplitList; // the splits are in order in splitList. Link them to the tx. also, determine the // action type, and fill in some fields which gnc holds at transaction level // first off, is it a transfer (can only have 2 splits?) // also, a tx with just 2 splits is shown by GnuCash as non-split bool nonSplitTx = true; if (m_splitList.count() != 2) { m_potentialTransfer = false; nonSplitTx = false; } QString slotMemo = gtx->getKvpValue(QString("notes")); if (!slotMemo.isEmpty()) tx.setMemo(slotMemo); QList::iterator it = m_splitList.begin(); while (!m_splitList.isEmpty()) { split = *it; // at this point, if m_potentialTransfer is still true, it is actually one! if (m_potentialTransfer) split.setAction(MyMoneySplit::ActionTransfer); if ((m_useTxNotes) // if use txnotes option is set && (nonSplitTx) // and it's a (GnuCash) non-split transaction && (!tx.memo().isEmpty())) // and tx notes are present split.setMemo(tx.memo()); // use the tx notes as memo tx.addSplit(split); it = m_splitList.erase(it); } m_storage->addTransaction(tx, true); // all done, add the transaction to storage signalProgress(++m_transactionCount, 0); return ; } //******************************************convertSplit******************************** void MyMoneyGncReader::convertSplit(const GncSplit *gsp) { Q_CHECK_PTR(gsp); MyMoneySplit split; MyMoneyAccount splitAccount; // find the kmm account id corresponding to the gnc id QString kmmAccountId; map_accountIds::const_iterator id = m_mapIds.constFind(gsp->acct().toUtf8()); if (id != m_mapIds.constEnd()) { kmmAccountId = id.value(); } else { // for the case where the acs not found (which shouldn't happen?), create an account with gnc name kmmAccountId = createOrphanAccount(gsp->acct()); } // find the account pointer and save for later splitAccount = m_storage->account(kmmAccountId); // print some data so we can maybe identify this split later // TODO : prints personal data //if (gncdebug) qDebug ("Split data - gncid %s, kmmid %s, memo %s, value %s, recon state %s", // gsp->acct().toLatin1(), kmmAccountId.data(), gsp->memo().toLatin1(), gsp->value().toLatin1(), // gsp->recon().toLatin1()); // payee id split.setPayeeId(m_txPayeeId.toUtf8()); // reconciled state and date switch (gsp->recon().at(0).toLatin1()) { case 'n': split.setReconcileFlag(eMyMoney::Split::State::NotReconciled); break; case 'c': split.setReconcileFlag(eMyMoney::Split::State::Cleared); break; case 'y': split.setReconcileFlag(eMyMoney::Split::State::Reconciled); break; } split.setReconcileDate(gsp->reconDate()); // memo split.setMemo(gsp->memo()); // accountId split.setAccountId(kmmAccountId); // cheque no split.setNumber(m_txChequeNo); // value and quantity MyMoneyMoney splitValue(convBadValue(gsp->value())); if (gsp->value() == "-1/0") { // treat gnc invalid value as zero // it's not quite a consistency check, but easier to treat it as such m_messageList["CC"].append (i18n("Account or Category %1, transaction date %2; split contains invalid value; please check", splitAccount.name(), m_txDatePosted.toString(Qt::ISODate))); } MyMoneyMoney splitQuantity(convBadValue(gsp->qty())); split.setValue(splitValue); // if split currency = tx currency, set shares = value (14/10/05) if (splitAccount.currencyId() == m_txCommodity) { split.setShares(splitValue); } else { split.setShares(splitQuantity); } // in kmm, the first split is important. in this routine we will // save the splits in our split list with the priority: // 1. assets // 2. liabilities // 3. others (categories) // but keeping each in same order as gnucash switch (splitAccount.accountGroup()) { case Account::Type::Asset: if (splitAccount.accountType() == Account::Type::Stock) { split.value().isZero() ? split.setAction(MyMoneySplit::ActionAddShares) : // free shares? split.setAction(MyMoneySplit::ActionBuyShares); m_potentialTransfer = false; // ? // add a price history entry MyMoneySecurity e = m_storage->security(splitAccount.currencyId()); MyMoneyMoney price; if (!split.shares().isZero()) { static const signed64 NEW_DENOM = 10000; price = split.value() / split.shares(); price = MyMoneyMoney(price.toDouble(), NEW_DENOM); } if (!price.isZero()) { TRY { // we can't use m_storage->security coz security list is not built yet m_storage->currency(m_txCommodity); // will throw exception if not currency e.setTradingCurrency(m_txCommodity); if (gncdebug) qDebug() << "added price for" << e.name() << price.toString() << "date" << m_txDatePosted.toString(Qt::ISODate); m_storage->modifySecurity(e); MyMoneyPrice dealPrice(e.id(), m_txCommodity, m_txDatePosted, price, i18n("Imported Transaction")); m_storage->addPrice(dealPrice); } CATCH { // stock transfer; treat like free shares? split.setAction(MyMoneySplit::ActionAddShares); } } } else { // not stock if (split.value().isNegative()) { bool isNumeric = false; if (!split.number().isEmpty()) { split.number().toLong(&isNumeric); // No QString.isNumeric()?? } if (isNumeric) { split.setAction(MyMoneySplit::ActionCheck); } else { split.setAction(MyMoneySplit::ActionWithdrawal); } } else { split.setAction(MyMoneySplit::ActionDeposit); } } m_splitList.append(split); break; case Account::Type::Liability: split.value().isNegative() ? split.setAction(MyMoneySplit::ActionWithdrawal) : split.setAction(MyMoneySplit::ActionDeposit); m_liabilitySplitList.append(split); break; default: m_potentialTransfer = false; m_otherSplitList.append(split); } // backdate the account opening date if necessary if (m_txDatePosted < splitAccount.openingDate()) { splitAccount.setOpeningDate(m_txDatePosted); m_storage->modifyAccount(splitAccount); } return ; } //********************************* convertTemplateTransaction ********************************************** MyMoneyTransaction MyMoneyGncReader::convertTemplateTransaction(const QString& schedName, const GncTransaction *gtx) { Q_CHECK_PTR(gtx); MyMoneyTransaction tx; MyMoneySplit split; unsigned int i; if (m_templateCount == 0) signalProgress(0, 1, i18n("Loading templates...")); // initialize class variables related to transactions m_txCommodity = ""; m_txPayeeId = ""; m_potentialTransfer = true; m_splitList.clear(); m_liabilitySplitList.clear(); m_otherSplitList.clear(); // payee, dates, commodity if (!gtx->desc().isEmpty()) { m_txPayeeId = createPayee(gtx->desc()); } else { m_txPayeeId = createPayee(i18n("Unknown payee")); // schedules require a payee tho normal tx's don't. not sure why... } tx.setEntryDate(gtx->dateEntered()); tx.setPostDate(gtx->datePosted()); m_txDatePosted = tx.postDate(); tx.setCommodity(gtx->currency().toUtf8()); m_txCommodity = tx.commodity(); // save for possible use in orphan account // process splits for (i = 0; i < gtx->splitCount(); i++) { convertTemplateSplit(schedName, static_cast(gtx->getSplit(i))); } // determine the action type for the splits and link them to the template tx if (!m_otherSplitList.isEmpty()) m_potentialTransfer = false; // tfrs can occur only between assets and asset/liabilities m_splitList += m_liabilitySplitList += m_otherSplitList; // the splits are in order in splitList. Transfer them to the tx // also, determine the action type. first off, is it a transfer (can only have 2 splits?) if (m_splitList.count() != 2) m_potentialTransfer = false; // at this point, if m_potentialTransfer is still true, it is actually one! QString txMemo = ""; QList::iterator it = m_splitList.begin(); while (!m_splitList.isEmpty()) { split = *it; if (m_potentialTransfer) { split.setAction(MyMoneySplit::ActionTransfer); } else { if (split.value().isNegative()) { //split.setAction (negativeActionType); split.setAction(MyMoneySplit::ActionWithdrawal); } else { //split.setAction (positiveActionType); split.setAction(MyMoneySplit::ActionDeposit); } } split.setNumber(gtx->no()); // set cheque no (or equivalent description) // Arbitrarily, save the first non-null split memo as the memo for the whole tx // I think this is necessary because txs with just 2 splits (the majority) // are not viewable as split transactions in kmm so the split memo is not seen if ((txMemo.isEmpty()) && (!split.memo().isEmpty())) txMemo = split.memo(); tx.addSplit(split); it = m_splitList.erase(it); } // memo - set from split tx.setMemo(txMemo); signalProgress(++m_templateCount, 0); return (tx); } //********************************* convertTemplateSplit **************************************************** void MyMoneyGncReader::convertTemplateSplit(const QString& schedName, const GncTemplateSplit *gsp) { Q_CHECK_PTR(gsp); // convertTemplateSplit MyMoneySplit split; MyMoneyAccount splitAccount; unsigned int i, j; bool nonNumericFormula = false; // action, value and account will be set from slots // reconcile state, always Not since it hasn't even been posted yet (?) split.setReconcileFlag(eMyMoney::Split::State::NotReconciled); // memo split.setMemo(gsp->memo()); // payee id split.setPayeeId(m_txPayeeId.toUtf8()); // read split slots (KVPs) int xactionCount = 0; int validSlotCount = 0; QString gncAccountId; for (i = 0; i < gsp->kvpCount(); i++) { const GncKvp& slot = gsp->getKvp(i); if ((slot.key() == "sched-xaction") && (slot.type() == "frame")) { bool bFoundStringCreditFormula = false; bool bFoundStringDebitFormula = false; bool bFoundGuidAccountId = false; QString gncCreditFormula, gncDebitFormula; for (j = 0; j < slot.kvpCount(); j++) { const GncKvp& subSlot = slot.getKvp(j); // again, see comments above. when we have a full specification // of all the options available to us, we can no doubt improve on this if ((subSlot.key() == "credit-formula") && (subSlot.type() == "string")) { gncCreditFormula = subSlot.value(); bFoundStringCreditFormula = true; } if ((subSlot.key() == "debit-formula") && (subSlot.type() == "string")) { gncDebitFormula = subSlot.value(); bFoundStringDebitFormula = true; } if ((subSlot.key() == "account") && (subSlot.type() == "guid")) { gncAccountId = subSlot.value(); bFoundGuidAccountId = true; } } // all data read, now check we have everything if ((bFoundStringCreditFormula) && (bFoundStringDebitFormula) && (bFoundGuidAccountId)) { if (gncdebug) qDebug() << "Found valid slot; credit" << gncCreditFormula << "debit" << gncDebitFormula << "acct" << gncAccountId; validSlotCount++; } // validate numeric, work out sign MyMoneyMoney exFormula; - exFormula.setNegativeMonetarySignPosition(MyMoneyMoney::BeforeQuantityMoney); + exFormula.setNegativeMonetarySignPosition(eMyMoney::Money::BeforeQuantityMoney); QString numericTest; char crdr = 0 ; if (!gncCreditFormula.isEmpty()) { crdr = 'C'; numericTest = gncCreditFormula; } else if (!gncDebitFormula.isEmpty()) { crdr = 'D'; numericTest = gncDebitFormula; } KMyMoneyMoneyValidator v(0); int pos; // useless, but required for validator if (v.validate(numericTest, pos) == QValidator::Acceptable) { switch (crdr) { case 'C': exFormula = QString("-" + numericTest); break; case 'D': exFormula = numericTest; } } else { if (gncdebug) qDebug() << numericTest << "is not numeric"; nonNumericFormula = true; } split.setValue(exFormula); xactionCount++; } else { m_messageList["SC"].append( i18n("Schedule %1 contains unknown action (key = %2, type = %3)", schedName, slot.key(), slot.type())); m_suspectSchedule = true; } } // report this as untranslatable tx if (xactionCount > 1) { m_messageList["SC"].append( i18n("Schedule %1 contains multiple actions; only one has been imported", schedName)); m_suspectSchedule = true; } if (validSlotCount == 0) { m_messageList["SC"].append( i18n("Schedule %1 contains no valid splits", schedName)); m_suspectSchedule = true; } if (nonNumericFormula) { m_messageList["SC"].append( i18n("Schedule %1 appears to contain a formula. GnuCash formulae are not convertible", schedName)); m_suspectSchedule = true; } // find the kmm account id corresponding to the gnc id QString kmmAccountId; map_accountIds::const_iterator id = m_mapIds.constFind(gncAccountId.toUtf8()); if (id != m_mapIds.constEnd()) { kmmAccountId = id.value(); } else { // for the case where the acs not found (which shouldn't happen?), create an account with gnc name kmmAccountId = createOrphanAccount(gncAccountId); } splitAccount = m_storage->account(kmmAccountId); split.setAccountId(kmmAccountId); // if split currency = tx currency, set shares = value (14/10/05) if (splitAccount.currencyId() == m_txCommodity) { split.setShares(split.value()); } /* else { //FIXME: scheduled currency or investment tx needs to be investigated split.setShares (splitQuantity); } */ // add the split to one of the lists switch (splitAccount.accountGroup()) { case Account::Type::Asset: m_splitList.append(split); break; case Account::Type::Liability: m_liabilitySplitList.append(split); break; default: m_otherSplitList.append(split); } // backdate the account opening date if necessary if (m_txDatePosted < splitAccount.openingDate()) { splitAccount.setOpeningDate(m_txDatePosted); m_storage->modifyAccount(splitAccount); } return ; } //********************************* convertSchedule ******************************************************** void MyMoneyGncReader::convertSchedule(const GncSchedule *gsc) { TRY { Q_CHECK_PTR(gsc); MyMoneySchedule sc; MyMoneyTransaction tx; m_suspectSchedule = false; QDate startDate, nextDate, lastDate, endDate; // for date calculations QDate today = QDate::currentDate(); int numOccurs, remOccurs; if (m_scheduleCount == 0) signalProgress(0, m_gncScheduleCount, i18n("Loading schedules...")); // schedule name sc.setName(gsc->name()); // find the transaction template as stored earlier QList::const_iterator itt; for (itt = m_templateList.constBegin(); itt != m_templateList.constEnd(); ++itt) { // the id to match against is the split:account value in the splits if (static_cast((*itt)->getSplit(0))->acct() == gsc->templId()) break; } if (itt == m_templateList.constEnd()) { throw MYMONEYEXCEPTION(i18n("Cannot find template transaction for schedule %1", sc.name())); } else { tx = convertTemplateTransaction(sc.name(), *itt); } tx.clearId(); // define the conversion table for intervals struct convIntvl { QString gncType; // the gnucash name unsigned char interval; // for date calculation unsigned int intervalCount; Schedule::Occurrence occ; // equivalent occurrence code Schedule::WeekendOption wo; }; /* other intervals supported by gnc according to Josh Sled's schema (see above) "none" "semi_monthly" */ /* some of these type names do not appear in gnucash and are difficult to generate for pre 2.2 files.They can be generated for 2.2 however, by GncRecurrence::getFrequency() */ static convIntvl vi [] = { {"once", 'o', 1, Schedule::Occurrence::Once, Schedule::WeekendOption::MoveNothing }, {"daily" , 'd', 1, Schedule::Occurrence::Daily, Schedule::WeekendOption::MoveNothing }, //{"daily_mf", 'd', 1, Schedule::Occurrence::Daily, Schedule::WeekendOption::MoveAfter }, doesn't work, need new freq in kmm {"30-days" , 'd', 30, Schedule::Occurrence::EveryThirtyDays, Schedule::WeekendOption::MoveNothing }, {"weekly", 'w', 1, Schedule::Occurrence::Weekly, Schedule::WeekendOption::MoveNothing }, {"bi_weekly", 'w', 2, Schedule::Occurrence::EveryOtherWeek, Schedule::WeekendOption::MoveNothing }, {"three-weekly", 'w', 3, Schedule::Occurrence::EveryThreeWeeks, Schedule::WeekendOption::MoveNothing }, {"four-weekly", 'w', 4, Schedule::Occurrence::EveryFourWeeks, Schedule::WeekendOption::MoveNothing }, {"eight-weekly", 'w', 8, Schedule::Occurrence::EveryEightWeeks, Schedule::WeekendOption::MoveNothing }, {"monthly", 'm', 1, Schedule::Occurrence::Monthly, Schedule::WeekendOption::MoveNothing }, {"two-monthly", 'm', 2, Schedule::Occurrence::EveryOtherMonth, Schedule::WeekendOption::MoveNothing }, {"quarterly", 'm', 3, Schedule::Occurrence::Quarterly, Schedule::WeekendOption::MoveNothing }, {"tri_annually", 'm', 4, Schedule::Occurrence::EveryFourMonths, Schedule::WeekendOption::MoveNothing }, {"semi_yearly", 'm', 6, Schedule::Occurrence::TwiceYearly, Schedule::WeekendOption::MoveNothing }, {"yearly", 'y', 1, Schedule::Occurrence::Yearly, Schedule::WeekendOption::MoveNothing }, {"two-yearly", 'y', 2, Schedule::Occurrence::EveryOtherYear, Schedule::WeekendOption::MoveNothing }, {"zzz", 'y', 1, Schedule::Occurrence::Yearly, Schedule::WeekendOption::MoveNothing} // zzz = stopper, may cause problems. what else can we do? }; QString frequency = "unknown"; // set default to unknown frequency bool unknownOccurs = false; // may have zero, or more than one frequency/recurrence spec QString schedEnabled; if (gsc->version() == "2.0.0") { if (gsc->m_vpRecurrence.count() != 1) { unknownOccurs = true; } else { const GncRecurrence *gre = gsc->m_vpRecurrence.first(); //qDebug (QString("Sched %1, pt %2, mu %3, sd %4").arg(gsc->name()).arg(gre->periodType()) // .arg(gre->mult()).arg(gre->startDate().toString(Qt::ISODate))); frequency = gre->getFrequency(); schedEnabled = gsc->enabled(); } sc.setOccurrence(Schedule::Occurrence::Once); // FIXME - how to convert } else { // find this interval const GncFreqSpec *fs = gsc->getFreqSpec(); if (fs == 0) { unknownOccurs = true; } else { frequency = fs->intervalType(); if (!fs->m_fsList.isEmpty()) unknownOccurs = true; // nested freqspec } schedEnabled = 'y'; // earlier versions did not have an enable flag } int i; for (i = 0; vi[i].gncType != "zzz"; i++) { if (frequency == vi[i].gncType) break; } if (vi[i].gncType == "zzz") { m_messageList["SC"].append( i18n("Schedule %1 has interval of %2 which is not currently available", sc.name(), frequency)); i = 0; // treat as single occurrence m_suspectSchedule = true; } if (unknownOccurs) { m_messageList["SC"].append( i18n("Schedule %1 contains unknown interval specification; please check for correct operation", sc.name())); m_suspectSchedule = true; } // set the occurrence interval, weekend option, start date sc.setOccurrence(vi[i].occ); sc.setWeekendOption(vi[i].wo); sc.setStartDate(gsc->startDate()); // if a last date was specified, use it, otherwise try to work out the last date sc.setLastPayment(gsc->lastDate()); numOccurs = gsc->numOccurs().toInt(); if (sc.lastPayment() == QDate()) { nextDate = lastDate = gsc->startDate(); while ((nextDate < today) && (numOccurs-- != 0)) { lastDate = nextDate; nextDate = incrDate(lastDate, vi[i].interval, vi[i].intervalCount); } sc.setLastPayment(lastDate); } // under Tom's new regime, the tx dates are the next due date (I think) tx.setPostDate(incrDate(sc.lastPayment(), vi[i].interval, vi[i].intervalCount)); tx.setEntryDate(incrDate(sc.lastPayment(), vi[i].interval, vi[i].intervalCount)); // if an end date was specified, use it, otherwise if the input file had a number // of occurs remaining, work out the end date sc.setEndDate(gsc->endDate()); numOccurs = gsc->numOccurs().toInt(); remOccurs = gsc->remOccurs().toInt(); if ((sc.endDate() == QDate()) && (remOccurs > 0)) { endDate = sc.lastPayment(); while (remOccurs-- > 0) { endDate = incrDate(endDate, vi[i].interval, vi[i].intervalCount); } sc.setEndDate(endDate); } // Check for sched deferred interval. Don't know how/if we can handle it, or even what it means... if (gsc->getSchedDef() != 0) { m_messageList["SC"].append( i18n("Schedule %1 contains a deferred interval specification; please check for correct operation", sc.name())); m_suspectSchedule = true; } // payment type, options sc.setPaymentType((Schedule::PaymentType)Schedule::PaymentType::Other); sc.setFixed(!m_suspectSchedule); // if any probs were found, set it as variable so user will always be prompted // we don't currently have a 'disable' option, but just make sure auto-enter is off if not enabled //qDebug(QString("%1 and %2").arg(gsc->autoCreate()).arg(schedEnabled)); sc.setAutoEnter((gsc->autoCreate() == QChar('y')) && (schedEnabled == QChar('y'))); //qDebug(QString("autoEnter set to %1").arg(sc.autoEnter())); // type QString actionType = tx.splits().first().action(); if (actionType == MyMoneySplit::ActionDeposit) { sc.setType((Schedule::Type)Schedule::Type::Deposit); } else if (actionType == MyMoneySplit::ActionTransfer) { sc.setType((Schedule::Type)Schedule::Type::Transfer); } else { sc.setType((Schedule::Type)Schedule::Type::Bill); } // finally, set the transaction pointer sc.setTransaction(tx); //tell the storage objects we have a new schedule object. if (m_suspectSchedule && m_dropSuspectSchedules) { m_messageList["SC"].append( i18n("Schedule %1 dropped at user request", sc.name())); } else { m_storage->addSchedule(sc); if (m_suspectSchedule) m_suspectList.append(sc.id()); } signalProgress(++m_scheduleCount, 0); return ; } PASS } //********************************* convertFreqSpec ******************************************************** void MyMoneyGncReader::convertFreqSpec(const GncFreqSpec *) { // Nowt to do here at the moment, convertSched only retrieves the interval type // but we will probably need to look into the nested freqspec when we properly implement semi-monthly and stuff return ; } //********************************* convertRecurrence ******************************************************** void MyMoneyGncReader::convertRecurrence(const GncRecurrence *) { return ; } //********************************************************************************************************** //************************************* terminate ********************************************************** void MyMoneyGncReader::terminate() { TRY { // All data has been converted and added to storage // this code is just temporary to show us what is in the file. if (gncdebug) qDebug("%d accounts found in the GnuCash file", (unsigned int)m_mapIds.count()); for (map_accountIds::const_iterator it = m_mapIds.constBegin(); it != m_mapIds.constEnd(); ++it) { if (gncdebug) qDebug() << "key =" << it.key() << "value =" << it.value(); } // first step is to implement the users investment option, now we // have all the accounts available QList::iterator stocks; for (stocks = m_stockList.begin(); stocks != m_stockList.end(); ++stocks) { checkInvestmentOption(*stocks); } // Next step is to walk the list and assign the parent/child relationship between the objects. unsigned int i = 0; signalProgress(0, m_accountCount, i18n("Reorganizing accounts...")); QList list; QList::iterator acc; m_storage->accountList(list); for (acc = list.begin(); acc != list.end(); ++acc) { if ((*acc).parentAccountId() == m_storage->asset().id()) { MyMoneyAccount assets = m_storage->asset(); m_storage->addAccount(assets, (*acc)); if (gncdebug) qDebug() << "Account id" << (*acc).id() << "is a child of the main asset account"; } else if ((*acc).parentAccountId() == m_storage->liability().id()) { MyMoneyAccount liabilities = m_storage->liability(); m_storage->addAccount(liabilities, (*acc)); if (gncdebug) qDebug() << "Account id" << (*acc).id() << "is a child of the main liability account"; } else if ((*acc).parentAccountId() == m_storage->income().id()) { MyMoneyAccount incomes = m_storage->income(); m_storage->addAccount(incomes, (*acc)); if (gncdebug) qDebug() << "Account id" << (*acc).id() << "is a child of the main income account"; } else if ((*acc).parentAccountId() == m_storage->expense().id()) { MyMoneyAccount expenses = m_storage->expense(); m_storage->addAccount(expenses, (*acc)); if (gncdebug) qDebug() << "Account id" << (*acc).id() << "is a child of the main expense account"; } else if ((*acc).parentAccountId() == m_storage->equity().id()) { MyMoneyAccount equity = m_storage->equity(); m_storage->addAccount(equity, (*acc)); if (gncdebug) qDebug() << "Account id" << (*acc).id() << "is a child of the main equity account"; } else if ((*acc).parentAccountId() == m_rootId) { if (gncdebug) qDebug() << "Account id" << (*acc).id() << "is a child of the main root account"; } else { // it is not under one of the main accounts, so find gnucash parent QString parentKey = (*acc).parentAccountId(); if (gncdebug) qDebug() << "Account id" << (*acc).id() << "is a child of " << (*acc).parentAccountId(); map_accountIds::const_iterator id = m_mapIds.constFind(parentKey); if (id != m_mapIds.constEnd()) { if (gncdebug) qDebug() << "Setting account id" << (*acc).id() << "parent account id to" << id.value(); MyMoneyAccount parent = m_storage->account(id.value()); parent = checkConsistency(parent, (*acc)); m_storage->addAccount(parent, (*acc)); } else { throw MYMONEYEXCEPTION("terminate() could not find account id"); } } signalProgress(++i, 0); } // end for account signalProgress(0, 1, (".")); // debug - get rid of reorg message // offer the most common account currency as a default QString mainCurrency = ""; unsigned int maxCount = 0; QMap::ConstIterator it; for (it = m_currencyCount.constBegin(); it != m_currencyCount.constEnd(); ++it) { if (it.value() > maxCount) { maxCount = it.value(); mainCurrency = it.key(); } } if (mainCurrency != "") { QString question = i18n("Your main currency seems to be %1 (%2); do you want to set this as your base currency?", mainCurrency, m_storage->currency(mainCurrency.toUtf8()).name()); if (KMessageBox::questionYesNo(0, question, PACKAGE) == KMessageBox::Yes) { m_storage->setValue("kmm-baseCurrency", mainCurrency); } } // now produce the end of job reports - first, work out which ones are required QList sectionsToReport; // list of sections needing report sectionsToReport.append("MN"); // always build the main section if ((m_ccCount = m_messageList["CC"].count()) > 0) sectionsToReport.append("CC"); if ((m_orCount = m_messageList["OR"].count()) > 0) sectionsToReport.append("OR"); if ((m_scCount = m_messageList["SC"].count()) > 0) sectionsToReport.append("SC"); // produce the sections in separate message boxes bool exit = false; int si; for (si = 0; (si < sectionsToReport.count()) && !exit; ++si) { QString button0Text = i18nc("Button to show more detailed data", "More"); if (si + 1 == sectionsToReport.count()) button0Text = i18nc("Button to close the current dialog", "Done"); // last section KGuiItem yesItem(button0Text, QIcon(), "", ""); KGuiItem noItem(i18n("Save Report"), QIcon(), "", ""); switch (KMessageBox::questionYesNoCancel(0, buildReportSection(sectionsToReport[si]), PACKAGE, yesItem, noItem)) { case KMessageBox::Yes: break; case KMessageBox::No: exit = writeReportToFile(sectionsToReport); break; default: exit = true; break; } } for (si = 0; si < m_suspectList.count(); ++si) { MyMoneySchedule sc = m_storage->schedule(m_suspectList[si]); KEditScheduleDlg *s; switch (KMessageBox::warningYesNo(0, i18n("Problems were encountered in converting schedule '%1'.\nDo you want to review or edit it now?", sc.name()), PACKAGE)) { case KMessageBox::Yes: s = new KEditScheduleDlg(sc); if (s->exec()) m_storage->modifySchedule(s->schedule()); delete s; break; default: break; } } } PASS } //************************************ buildReportSection************************************ QString MyMoneyGncReader::buildReportSection(const QString& source) { TRY { QString s = ""; bool more = false; if (source == "MN") { s.append(i18n("Found:\n\n")); s.append(i18np("%1 commodity (equity)\n", "%1 commodities (equities)\n", m_commodityCount)); s.append(i18np("%1 price\n", "%1 prices\n", m_priceCount)); s.append(i18np("%1 account\n", "%1 accounts\n", m_accountCount)); s.append(i18np("%1 transaction\n", "%1 transactions\n", m_transactionCount)); s.append(i18np("%1 schedule\n", "%1 schedules\n", m_scheduleCount)); s.append("\n\n"); if (m_ccCount == 0) { s.append(i18n("No inconsistencies were detected\n")); } else { s.append(i18np("%1 inconsistency was detected and corrected\n", "%1 inconsistencies were detected and corrected\n", m_ccCount)); more = true; } if (m_orCount > 0) { s.append("\n\n"); s.append(i18np("%1 orphan account was created\n", "%1 orphan accounts were created\n", m_orCount)); more = true; } if (m_scCount > 0) { s.append("\n\n"); s.append(i18np("%1 possible schedule problem was noted\n", "%1 possible schedule problems were noted\n", m_scCount)); more = true; } QString unsupported(""); QString lineSep("\n - "); if (m_smallBusinessFound) unsupported.append(lineSep + i18n("Small Business Features (Customers, Invoices, etc.)")); if (m_budgetsFound) unsupported.append(lineSep + i18n("Budgets")); if (m_lotsFound) unsupported.append(lineSep + i18n("Lots")); if (!unsupported.isEmpty()) { unsupported.prepend(i18n("The following features found in your file are not currently supported:")); s.append(unsupported); } if (more) s.append(i18n("\n\nPress More for further information")); } else { s = m_messageList[source].join(QChar('\n')); } if (gncdebug) qDebug() << s; return (static_cast(s)); } PASS } //************************ writeReportToFile********************************* bool MyMoneyGncReader::writeReportToFile(const QList& sectionsToReport) { TRY { int i; QString fd = QFileDialog::getSaveFileName(0, QString(), QString(), i18n("Save report as")); if (fd.isEmpty()) return (false); QFile reportFile(fd); if (!reportFile.open(QIODevice::WriteOnly)) { return (false); } QTextStream stream(&reportFile); for (i = 0; i < sectionsToReport.count(); i++) stream << buildReportSection(sectionsToReport[i]) << endl; reportFile.close(); return (true); } PASS } /**************************************************************************** Utility routines *****************************************************************************/ //************************ createPayee *************************** QString MyMoneyGncReader::createPayee(const QString& gncDescription) { MyMoneyPayee payee; TRY { payee = m_storage->payeeByName(gncDescription); } CATCH { // payee not found, create one payee.setName(gncDescription); m_storage->addPayee(payee); } return (payee.id()); } //************************************** createOrphanAccount ******************************* QString MyMoneyGncReader::createOrphanAccount(const QString& gncName) { MyMoneyAccount acc; acc.setName("orphan_" + gncName); acc.setDescription(i18n("Orphan created from unknown GnuCash account")); QDate today = QDate::currentDate(); acc.setOpeningDate(today); acc.setLastModified(today); acc.setLastReconciliationDate(today); acc.setCurrencyId(m_txCommodity); acc.setAccountType(Account::Type::Asset); acc.setParentAccountId(m_storage->asset().id()); m_storage->addAccount(acc); // assign the gnucash id as the key into the map to find our id m_mapIds[gncName.toUtf8()] = acc.id(); m_messageList["OR"].append( i18n("One or more transactions contain a reference to an otherwise unknown account\n" "An asset account with the name %1 has been created to hold the data", acc.name())); return (acc.id()); } //****************************** incrDate ********************************************* QDate MyMoneyGncReader::incrDate(QDate lastDate, unsigned char interval, unsigned int intervalCount) { TRY { switch (interval) { case 'd': return (lastDate.addDays(intervalCount)); case 'w': return (lastDate.addDays(intervalCount * 7)); case 'm': return (lastDate.addMonths(intervalCount)); case 'y': return (lastDate.addYears(intervalCount)); case 'o': // once-only return (lastDate); } throw MYMONEYEXCEPTION(i18n("Internal error - invalid interval char in incrDate")); QDate r = QDate(); return (r); // to keep compiler happy } PASS } //********************************* checkConsistency ********************************** MyMoneyAccount MyMoneyGncReader::checkConsistency(MyMoneyAccount& parent, MyMoneyAccount& child) { TRY { // gnucash is flexible/weird enough to allow various inconsistencies // these are a couple I found in my file, no doubt more will be discovered if ((child.accountType() == Account::Type::Investment) && (parent.accountType() != Account::Type::Asset)) { m_messageList["CC"].append( i18n("An Investment account must be a child of an Asset account\n" "Account %1 will be stored under the main Asset account", child.name())); return m_storage->asset(); } if ((child.accountType() == Account::Type::Income) && (parent.accountType() != Account::Type::Income)) { m_messageList["CC"].append( i18n("An Income account must be a child of an Income account\n" "Account %1 will be stored under the main Income account", child.name())); return m_storage->income(); } if ((child.accountType() == Account::Type::Expense) && (parent.accountType() != Account::Type::Expense)) { m_messageList["CC"].append( i18n("An Expense account must be a child of an Expense account\n" "Account %1 will be stored under the main Expense account", child.name())); return m_storage->expense(); } return (parent); } PASS } //*********************************** checkInvestmentOption ************************* void MyMoneyGncReader::checkInvestmentOption(QString stockId) { // implement the investment option for stock accounts // first check whether the parent account (gnucash id) is actually an // investment account. if it is, no further action is needed MyMoneyAccount stockAcc = m_storage->account(m_mapIds[stockId.toUtf8()]); MyMoneyAccount parent; QString parentKey = stockAcc.parentAccountId(); map_accountIds::const_iterator id = m_mapIds.constFind(parentKey); if (id != m_mapIds.constEnd()) { parent = m_storage->account(id.value()); if (parent.accountType() == Account::Type::Investment) return ; } // so now, check the investment option requested by the user // option 0 creates a separate investment account for each stock account if (m_investmentOption == 0) { MyMoneyAccount invAcc(stockAcc); invAcc.setAccountType(Account::Type::Investment); invAcc.setCurrencyId(QString("")); // we don't know what currency it is!! invAcc.setParentAccountId(parentKey); // intersperse it between old parent and child stock acct m_storage->addAccount(invAcc); m_mapIds [invAcc.id()] = invAcc.id(); // so stock account gets parented (again) to investment account later if (gncdebug) qDebug() << "Created investment account" << invAcc.name() << "as id" << invAcc.id() << "parent" << invAcc.parentAccountId(); if (gncdebug) qDebug() << "Setting stock" << stockAcc.name() << "id" << stockAcc.id() << "as child of" << invAcc.id(); stockAcc.setParentAccountId(invAcc.id()); m_storage->addAccount(invAcc, stockAcc); // investment option 1 creates a single investment account for all stocks } else if (m_investmentOption == 1) { static QString singleInvAccId = ""; MyMoneyAccount singleInvAcc; bool ok = false; if (singleInvAccId.isEmpty()) { // if the account has not yet been created QString invAccName; while (!ok) { invAccName = QInputDialog::getText(0, QStringLiteral(PACKAGE), i18n("Enter the investment account name "), QLineEdit::Normal, i18n("My Investments"), &ok); } singleInvAcc.setName(invAccName); singleInvAcc.setAccountType(Account::Type::Investment); singleInvAcc.setCurrencyId(QString("")); singleInvAcc.setParentAccountId(m_storage->asset().id()); m_storage->addAccount(singleInvAcc); m_mapIds [singleInvAcc.id()] = singleInvAcc.id(); // so stock account gets parented (again) to investment account later if (gncdebug) qDebug() << "Created investment account" << singleInvAcc.name() << "as id" << singleInvAcc.id() << "parent" << singleInvAcc.parentAccountId() << "reparenting stock"; singleInvAccId = singleInvAcc.id(); } else { // the account has already been created singleInvAcc = m_storage->account(singleInvAccId); } m_storage->addAccount(singleInvAcc, stockAcc); // add stock as child // the original intention of option 2 was to allow any asset account to be converted to an investment (broker) account // however, since we have already stored the accounts as asset, we have no way at present of changing their type // the only alternative would be to hold all the gnucash data in memory, then implement this option, then convert all the data // that would mean a major overhaul of the code. Perhaps I'll think of another way... } else if (m_investmentOption == 2) { static int lastSelected = 0; MyMoneyAccount invAcc(stockAcc); QStringList accList; QList list; QList::iterator acc; m_storage->accountList(list); // build a list of candidates for the input box for (acc = list.begin(); acc != list.end(); ++acc) { // if (((*acc).accountGroup() == Account::Type::Asset) && ((*acc).accountType() != Account::Type::Stock)) accList.append ((*acc).name()); if ((*acc).accountType() == Account::Type::Investment) accList.append((*acc).name()); } //if (accList.isEmpty()) qWarning ("No available accounts"); bool ok = false; while (!ok) { // keep going till we have a valid investment parent QString invAccName = QInputDialog::getItem(0, PACKAGE, i18n("Select parent investment account or enter new name. Stock %1", stockAcc.name()), accList, lastSelected, true, &ok); if (ok) { lastSelected = accList.indexOf(invAccName); // preserve selection for next time for (acc = list.begin(); acc != list.end(); ++acc) { if ((*acc).name() == invAccName) break; } if (acc != list.end()) { // an account was selected invAcc = *acc; } else { // a new account name was entered invAcc.setAccountType(Account::Type::Investment); invAcc.setName(invAccName); invAcc.setCurrencyId(QString("")); invAcc.setParentAccountId(m_storage->asset().id()); m_storage->addAccount(invAcc); ok = true; } if (invAcc.accountType() == Account::Type::Investment) { ok = true; } else { // this code is probably not going to be implemented coz we can't change account types (??) #if 0 QMessageBox mb(PACKAGE, i18n("%1 is not an Investment Account. Do you wish to make it one?", invAcc.name()), QMessageBox::Question, QMessageBox::Yes | QMessageBox::Default, QMessageBox::No | QMessageBox::Escape, Qt::NoButton); switch (mb.exec()) { case QMessageBox::No : ok = false; break; default: // convert it - but what if it has splits??? qWarning("Not yet implemented"); ok = true; break; } #endif switch (KMessageBox::questionYesNo(0, i18n("%1 is not an Investment Account. Do you wish to make it one?", invAcc.name()), PACKAGE)) { case KMessageBox::Yes: // convert it - but what if it has splits??? qWarning("Not yet implemented"); ok = true; break; default: ok = false; break; } } } // end if ok - user pressed Cancel } // end while !ok m_mapIds [invAcc.id()] = invAcc.id(); // so stock account gets parented (again) to investment account later m_storage->addAccount(invAcc, stockAcc); } else { // investment option != 0, 1, 2 qWarning("Invalid investment option %d", m_investmentOption); } } // get the price source for a stock (gnc account) where online quotes are requested void MyMoneyGncReader::getPriceSource(MyMoneySecurity stock, QString gncSource) { // if he wants to use Finance::Quote, no conversion of source name is needed if (m_useFinanceQuote) { stock.setValue("kmm-online-quote-system", "Finance::Quote"); stock.setValue("kmm-online-source", gncSource.toLower()); m_storage->modifySecurity(stock); return; } // first check if we have already asked about this source // (mapSources is initialy empty. We may be able to pre-fill it with some equivalent // sources, if such things do exist. User feedback may help here.) QMap::const_iterator it; for (it = m_mapSources.constBegin(); it != m_mapSources.constEnd(); ++it) { if (it.key() == gncSource) { stock.setValue("kmm-online-source", it.value()); m_storage->modifySecurity(stock); return; } } // not found in map, so ask the user QPointer dlg = new KGncPriceSourceDlg(stock.name(), gncSource); dlg->exec(); QString s = dlg->selectedSource(); if (!s.isEmpty()) { stock.setValue("kmm-online-source", s); m_storage->modifySecurity(stock); } if (dlg->alwaysUse()) m_mapSources[gncSource] = s; delete dlg; return; } // functions to control the progress bar //*********************** setProgressCallback ***************************** void MyMoneyGncReader::setProgressCallback(void(*callback)(int, int, const QString&)) { m_progressCallback = callback; return ; } //************************** signalProgress ******************************* void MyMoneyGncReader::signalProgress(int current, int total, const QString& msg) { if (m_progressCallback != 0) (*m_progressCallback)(current, total, msg); return ; } #endif // _GNCFILEANON diff --git a/kmymoney/converter/mymoneystatementreader.cpp b/kmymoney/converter/mymoneystatementreader.cpp index 2db29057c..d4af30458 100644 --- a/kmymoney/converter/mymoneystatementreader.cpp +++ b/kmymoney/converter/mymoneystatementreader.cpp @@ -1,1549 +1,1550 @@ /*************************************************************************** mymoneystatementreader.cpp ------------------- begin : Mon Aug 30 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 ***************************************************************************/ /*************************************************************************** * * * 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 "mymoneystatementreader.h" #include // ---------------------------------------------------------------------------- // QT Headers #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Headers #include #include #include #include #include #include // ---------------------------------------------------------------------------- // Project Headers #include "mymoneyfile.h" #include "mymoneyaccount.h" #include "mymoneyprice.h" +#include "mymoneyexception.h" #include "mymoneytransactionfilter.h" #include "mymoneypayee.h" #include "mymoneystatement.h" #include "mymoneysecurity.h" #include "kmymoneyglobalsettings.h" #include "transactioneditor.h" #include "stdtransactioneditor.h" #include "kmymoneyedit.h" #include "kaccountselectdlg.h" #include "transactionmatcher.h" #include "kenterscheduledlg.h" #include "kmymoneyaccountcombo.h" #include "accountsmodel.h" #include "models.h" #include "existingtransactionmatchfinder.h" #include "scheduledtransactionmatchfinder.h" #include "dialogenums.h" #include "mymoneyenums.h" #include "modelenums.h" #include "kmymoneyutils.h" using namespace eMyMoney; bool matchNotEmpty(const QString &l, const QString &r) { return !l.isEmpty() && QString::compare(l, r, Qt::CaseInsensitive) == 0; } class MyMoneyStatementReader::Private { public: Private() : transactionsCount(0), transactionsAdded(0), transactionsMatched(0), transactionsDuplicate(0), scannedCategories(false) {} const QString& feeId(const MyMoneyAccount& invAcc); const QString& interestId(const MyMoneyAccount& invAcc); QString interestId(const QString& name); QString expenseId(const QString& name); QString feeId(const QString& name); void assignUniqueBankID(MyMoneySplit& s, const MyMoneyStatement::Transaction& t_in); void setupPrice(MyMoneySplit &s, const MyMoneyAccount &splitAccount, const MyMoneyAccount &transactionAccount, const QDate &postDate); MyMoneyAccount lastAccount; MyMoneyAccount m_account; MyMoneyAccount m_brokerageAccount; QList transactions; QList payees; int transactionsCount; int transactionsAdded; int transactionsMatched; int transactionsDuplicate; QMap uniqIds; QMap securitiesBySymbol; QMap securitiesByName; bool m_skipCategoryMatching; private: void scanCategories(QString& id, const MyMoneyAccount& invAcc, const MyMoneyAccount& parentAccount, const QString& defaultName); /** * This method tries to figure out the category to be used for fees and interest * from previous transactions in the given @a investmentAccount and returns the * ids of those categories in @a feesId and @a interestId. The last used category * will be returned. */ void previouslyUsedCategories(const QString& investmentAccount, QString& feesId, QString& interestId); QString nameToId(const QString&name, MyMoneyAccount& parent); private: QString m_feeId; QString m_interestId; bool scannedCategories; }; const QString& MyMoneyStatementReader::Private::feeId(const MyMoneyAccount& invAcc) { scanCategories(m_feeId, invAcc, MyMoneyFile::instance()->expense(), i18n("_Fees")); return m_feeId; } const QString& MyMoneyStatementReader::Private::interestId(const MyMoneyAccount& invAcc) { scanCategories(m_interestId, invAcc, MyMoneyFile::instance()->income(), i18n("_Dividend")); return m_interestId; } QString MyMoneyStatementReader::Private::nameToId(const QString& name, MyMoneyAccount& parent) { // Adapted from KMyMoneyApp::createAccount(MyMoneyAccount& newAccount, MyMoneyAccount& parentAccount, MyMoneyAccount& brokerageAccount, MyMoneyMoney openingBal) // Needed to find/create category:sub-categories MyMoneyFile* file = MyMoneyFile::instance(); QString id = file->categoryToAccount(name, Account::Type::Unknown); // if it does not exist, we have to create it if (id.isEmpty()) { MyMoneyAccount newAccount; MyMoneyAccount parentAccount = parent; newAccount.setName(name) ; int pos; // check for ':' in the name and use it as separator for a hierarchy while ((pos = newAccount.name().indexOf(MyMoneyFile::AccountSeperator)) != -1) { QString part = newAccount.name().left(pos); QString remainder = newAccount.name().mid(pos + 1); const MyMoneyAccount& existingAccount = file->subAccountByName(parentAccount, part); if (existingAccount.id().isEmpty()) { newAccount.setName(part); newAccount.setAccountType(parentAccount.accountType()); file->addAccount(newAccount, parentAccount); parentAccount = newAccount; } else { parentAccount = existingAccount; } newAccount.setParentAccountId(QString()); // make sure, there's no parent newAccount.clearId(); // and no id set for adding newAccount.removeAccountIds(); // and no sub-account ids newAccount.setName(remainder); }//end while newAccount.setAccountType(parentAccount.accountType()); // make sure we have a currency. If none is assigned, we assume base currency if (newAccount.currencyId().isEmpty()) newAccount.setCurrencyId(file->baseCurrency().id()); file->addAccount(newAccount, parentAccount); id = newAccount.id(); } return id; } QString MyMoneyStatementReader::Private::expenseId(const QString& name) { MyMoneyAccount parent = MyMoneyFile::instance()->expense(); return nameToId(name, parent); } QString MyMoneyStatementReader::Private::interestId(const QString& name) { MyMoneyAccount parent = MyMoneyFile::instance()->income(); return nameToId(name, parent); } QString MyMoneyStatementReader::Private::feeId(const QString& name) { MyMoneyAccount parent = MyMoneyFile::instance()->expense(); return nameToId(name, parent); } void MyMoneyStatementReader::Private::previouslyUsedCategories(const QString& investmentAccount, QString& feesId, QString& interestId) { feesId.clear(); interestId.clear(); MyMoneyFile* file = MyMoneyFile::instance(); try { MyMoneyAccount acc = file->account(investmentAccount); MyMoneyTransactionFilter filter(investmentAccount); filter.setReportAllSplits(false); // since we assume an investment account here, we need to collect the stock accounts as well filter.addAccount(acc.accountList()); QList< QPair > list; file->transactionList(list, filter); QList< QPair >::const_iterator it_t; for (it_t = list.constBegin(); it_t != list.constEnd(); ++it_t) { const MyMoneyTransaction& t = (*it_t).first; MyMoneySplit s = (*it_t).second; MyMoneyAccount acc = file->account(s.accountId()); // stock split shouldn't be fee or interest bacause it won't play nice with dissectTransaction // it was caused by processTransactionEntry adding splits in wrong order != with manual transaction entering if (acc.accountGroup() == Account::Type::Expense || acc.accountGroup() == Account::Type::Income) { foreach (auto sNew , t.splits()) { acc = file->account(sNew.accountId()); if (acc.accountGroup() != Account::Type::Expense && // shouldn't be fee acc.accountGroup() != Account::Type::Income && // shouldn't be interest (sNew.value() != sNew.shares() || // shouldn't be checking account... (sNew.value() == sNew.shares() && sNew.price() != MyMoneyMoney::ONE))) { // ...but sometimes it may look like checking account s = sNew; break; } } } MyMoneySplit assetAccountSplit; QList feeSplits; QList interestSplits; MyMoneySecurity security; MyMoneySecurity currency; eMyMoney::Split::InvestmentTransactionType transactionType; KMyMoneyUtils::dissectTransaction(t, s, assetAccountSplit, feeSplits, interestSplits, security, currency, transactionType); if (!feeSplits.isEmpty()) { feesId = feeSplits.first().accountId(); if (!interestId.isEmpty()) break; } if (!interestSplits.isEmpty()) { interestId = interestSplits.first().accountId(); if (!feesId.isEmpty()) break; } } } catch (const MyMoneyException &) { } } void MyMoneyStatementReader::Private::scanCategories(QString& id, const MyMoneyAccount& invAcc, const MyMoneyAccount& parentAccount, const QString& defaultName) { if (!scannedCategories) { previouslyUsedCategories(invAcc.id(), m_feeId, m_interestId); scannedCategories = true; } if (id.isEmpty()) { MyMoneyFile* file = MyMoneyFile::instance(); MyMoneyAccount acc = file->accountByName(defaultName); // if it does not exist, we have to create it if (acc.id().isEmpty()) { MyMoneyAccount parent = parentAccount; acc.setName(defaultName); acc.setAccountType(parent.accountType()); acc.setCurrencyId(parent.currencyId()); file->addAccount(acc, parent); } id = acc.id(); } } void MyMoneyStatementReader::Private::assignUniqueBankID(MyMoneySplit& s, const MyMoneyStatement::Transaction& t_in) { if (! t_in.m_strBankID.isEmpty()) { // make sure that id's are unique from this point on by appending a -# // postfix if needed QString base(t_in.m_strBankID); QString hash(base); int idx = 1; for (;;) { QMap::const_iterator it; it = uniqIds.constFind(hash); if (it == uniqIds.constEnd()) { uniqIds[hash] = true; break; } hash = QString("%1-%2").arg(base).arg(idx); ++idx; } s.setBankID(hash); } } void MyMoneyStatementReader::Private::setupPrice(MyMoneySplit &s, const MyMoneyAccount &splitAccount, const MyMoneyAccount &transactionAccount, const QDate &postDate) { if (transactionAccount.currencyId() != splitAccount.currencyId()) { // a currency converstion is needed assume that split has already a proper value MyMoneyFile* file = MyMoneyFile::instance(); MyMoneySecurity toCurrency = file->security(splitAccount.currencyId()); MyMoneySecurity fromCurrency = file->security(transactionAccount.currencyId()); // get the price for the transaction's date const MyMoneyPrice &price = file->price(fromCurrency.id(), toCurrency.id(), postDate); // if the price is valid calculate the shares if (price.isValid()) { const int fract = splitAccount.fraction(toCurrency); const MyMoneyMoney &shares = s.value() * price.rate(toCurrency.id()); s.setShares(shares.convert(fract)); qDebug("Setting second split shares to %s", qPrintable(s.shares().formatMoney(toCurrency.id(), 2))); } else { qDebug("No price entry was found to convert from '%s' to '%s' on '%s'", qPrintable(fromCurrency.tradingSymbol()), qPrintable(toCurrency.tradingSymbol()), qPrintable(postDate.toString(Qt::ISODate))); } } } MyMoneyStatementReader::MyMoneyStatementReader() : d(new Private), m_userAbort(false), m_autoCreatePayee(false), m_ft(0), m_progressCallback(0) { m_askPayeeCategory = KMyMoneyGlobalSettings::askForPayeeCategory(); } MyMoneyStatementReader::~MyMoneyStatementReader() { delete d; } bool MyMoneyStatementReader::anyTransactionAdded() const { return (d->transactionsAdded != 0) ? true : false; } void MyMoneyStatementReader::setAutoCreatePayee(bool create) { m_autoCreatePayee = create; } void MyMoneyStatementReader::setAskPayeeCategory(bool ask) { m_askPayeeCategory = ask; } bool MyMoneyStatementReader::import(const MyMoneyStatement& s, QStringList& messages) { // // For testing, save the statement to an XML file // (uncomment this line) // //MyMoneyStatement::writeXMLFile(s, "Imported.Xml"); // // Select the account // d->m_account = MyMoneyAccount(); d->m_brokerageAccount = MyMoneyAccount(); m_ft = new MyMoneyFileTransaction(); d->m_skipCategoryMatching = s.m_skipCategoryMatching; // if the statement source left some information about // the account, we use it to get the current data of it if (!s.m_accountId.isEmpty()) { try { d->m_account = MyMoneyFile::instance()->account(s.m_accountId); } catch (const MyMoneyException &) { qDebug("Received reference '%s' to unknown account in statement", qPrintable(s.m_accountId)); } } if (d->m_account.id().isEmpty()) { d->m_account.setName(s.m_strAccountName); d->m_account.setNumber(s.m_strAccountNumber); switch (s.m_eType) { case eMyMoney::Statement::Type::Checkings: d->m_account.setAccountType(Account::Type::Checkings); break; case eMyMoney::Statement::Type::Savings: d->m_account.setAccountType(Account::Type::Savings); break; case eMyMoney::Statement::Type::Investment: //testing support for investment statements! //m_userAbort = true; //KMessageBox::error(kmymoney, i18n("This is an investment statement. These are not supported currently."), i18n("Critical Error")); d->m_account.setAccountType(Account::Type::Investment); break; case eMyMoney::Statement::Type::CreditCard: d->m_account.setAccountType(Account::Type::CreditCard); break; default: d->m_account.setAccountType(Account::Type::Unknown); break; } // we ask the user only if we have some transactions to process if (!m_userAbort && s.m_listTransactions.count() > 0) m_userAbort = ! selectOrCreateAccount(Select, d->m_account); } // see if we need to update some values stored with the account if (d->m_account.value("lastStatementBalance") != s.m_closingBalance.toString() || d->m_account.value("lastImportedTransactionDate") != s.m_dateEnd.toString(Qt::ISODate)) { if (s.m_closingBalance != MyMoneyMoney::autoCalc) { d->m_account.setValue("lastStatementBalance", s.m_closingBalance.toString()); if (s.m_dateEnd.isValid()) { d->m_account.setValue("lastImportedTransactionDate", s.m_dateEnd.toString(Qt::ISODate)); } } try { MyMoneyFile::instance()->modifyAccount(d->m_account); } catch (const MyMoneyException &) { qDebug("Updating account in MyMoneyStatementReader::startImport failed"); } } if (!d->m_account.name().isEmpty()) messages += i18n("Importing statement for account %1", d->m_account.name()); else if (s.m_listTransactions.count() == 0) messages += i18n("Importing statement without transactions"); qDebug("Importing statement for '%s'", qPrintable(d->m_account.name())); // // Process the securities // signalProgress(0, s.m_listSecurities.count(), "Importing Statement ..."); int progress = 0; QList::const_iterator it_s = s.m_listSecurities.begin(); while (it_s != s.m_listSecurities.end()) { processSecurityEntry(*it_s); signalProgress(++progress, 0); ++it_s; } signalProgress(-1, -1); // // Process the transactions // if (!m_userAbort) { try { qDebug("Processing transactions (%s)", qPrintable(d->m_account.name())); signalProgress(0, s.m_listTransactions.count(), "Importing Statement ..."); int progress = 0; QList::const_iterator it_t = s.m_listTransactions.begin(); while (it_t != s.m_listTransactions.end() && !m_userAbort) { processTransactionEntry(*it_t); signalProgress(++progress, 0); ++it_t; } qDebug("Processing transactions done (%s)", qPrintable(d->m_account.name())); } catch (const MyMoneyException &e) { if (e.what() == "USERABORT") m_userAbort = true; else qDebug("Caught exception from processTransactionEntry() not caused by USERABORT: %s", qPrintable(e.what())); } signalProgress(-1, -1); } // // process price entries // if (!m_userAbort) { try { signalProgress(0, s.m_listPrices.count(), "Importing Statement ..."); QList slist = MyMoneyFile::instance()->securityList(); QList::const_iterator it_s; for (it_s = slist.constBegin(); it_s != slist.constEnd(); ++it_s) { d->securitiesBySymbol[(*it_s).tradingSymbol()] = *it_s; d->securitiesByName[(*it_s).name()] = *it_s; } int progress = 0; QList::const_iterator it_p = s.m_listPrices.begin(); while (it_p != s.m_listPrices.end()) { processPriceEntry(*it_p); signalProgress(++progress, 0); ++it_p; } } catch (const MyMoneyException &e) { if (e.what() == "USERABORT") m_userAbort = true; else qDebug("Caught exception from processPriceEntry() not caused by USERABORT: %s", qPrintable(e.what())); } signalProgress(-1, -1); } bool rc = false; // delete all payees created in vain int payeeCount = d->payees.count(); QList::const_iterator it_p; for (it_p = d->payees.constBegin(); it_p != d->payees.constEnd(); ++it_p) { try { MyMoneyFile::instance()->removePayee(*it_p); --payeeCount; } catch (const MyMoneyException &) { // if we can't delete it, it must be in use which is ok for us } } if (s.m_closingBalance.isAutoCalc()) { messages += i18n(" Statement balance is not contained in statement."); } else { messages += i18n(" Statement balance on %1 is reported to be %2", s.m_dateEnd.toString(Qt::ISODate), s.m_closingBalance.formatMoney("", 2)); } messages += i18n(" Transactions"); messages += i18np(" %1 processed", " %1 processed", d->transactionsCount); messages += i18ncp("x transactions have been added", " %1 added", " %1 added", d->transactionsAdded); messages += i18np(" %1 matched", " %1 matched", d->transactionsMatched); messages += i18np(" %1 duplicate", " %1 duplicates", d->transactionsDuplicate); messages += i18n(" Payees"); messages += i18ncp("x transactions have been created", " %1 created", " %1 created", payeeCount); messages += QString(); // remove the Don't ask again entries KSharedConfigPtr config = KSharedConfig::openConfig(); KConfigGroup grp = config->group(QString::fromLatin1("Notification Messages")); QStringList::ConstIterator it; for (it = m_dontAskAgain.constBegin(); it != m_dontAskAgain.constEnd(); ++it) { grp.deleteEntry(*it); } config->sync(); m_dontAskAgain.clear(); rc = !m_userAbort; // finish the transaction if (rc) m_ft->commit(); delete m_ft; m_ft = 0; qDebug("Importing statement for '%s' done", qPrintable(d->m_account.name())); return rc; } void MyMoneyStatementReader::processPriceEntry(const MyMoneyStatement::Price& p_in) { MyMoneyFile* file = MyMoneyFile::instance(); QString currency = file->baseCurrency().id(); QString security; if (!p_in.m_strCurrency.isEmpty()) { security = p_in.m_strSecurity; currency = p_in.m_strCurrency; } else if (d->securitiesBySymbol.contains(p_in.m_strSecurity)) { security = d->securitiesBySymbol[p_in.m_strSecurity].id(); currency = file->security(file->security(security).tradingCurrency()).id(); } else if (d->securitiesByName.contains(p_in.m_strSecurity)) { security = d->securitiesByName[p_in.m_strSecurity].id(); currency = file->security(file->security(security).tradingCurrency()).id(); } else return; MyMoneyPrice price(security, currency, p_in.m_date, p_in.m_amount, p_in.m_sourceName.isEmpty() ? i18n("Prices Importer") : p_in.m_sourceName); MyMoneyFile::instance()->addPrice(price); } void MyMoneyStatementReader::processSecurityEntry(const MyMoneyStatement::Security& sec_in) { // For a security entry, we will just make sure the security exists in the // file. It will not get added to the investment account until it's called // for in a transaction. MyMoneyFile* file = MyMoneyFile::instance(); // check if we already have the security // In a statement, we do not know what type of security this is, so we will // not use type as a matching factor. MyMoneySecurity security; QList list = file->securityList(); QList::ConstIterator it = list.constBegin(); while (it != list.constEnd() && security.id().isEmpty()) { if (matchNotEmpty(sec_in.m_strSymbol, (*it).tradingSymbol()) || matchNotEmpty(sec_in.m_strName, (*it).name())) { security = *it; } ++it; } // if the security was not found, we have to create it while not forgetting // to setup the type if (security.id().isEmpty()) { security.setName(sec_in.m_strName); security.setTradingSymbol(sec_in.m_strSymbol); security.setTradingCurrency(file->baseCurrency().id()); security.setValue("kmm-security-id", sec_in.m_strId); security.setValue("kmm-online-source", "Stooq"); security.setSecurityType(Security::Type::Stock); MyMoneyFileTransaction ft; try { file->addSecurity(security); ft.commit(); qDebug() << "Created " << security.name() << " with id " << security.id(); } catch (const MyMoneyException &e) { KMessageBox::error(0, i18n("Error creating security record: %1", e.what()), i18n("Error")); } } else { qDebug() << "Found " << security.name() << " with id " << security.id(); } } void MyMoneyStatementReader::processTransactionEntry(const MyMoneyStatement::Transaction& statementTransactionUnderImport) { MyMoneyFile* file = MyMoneyFile::instance(); MyMoneyTransaction transactionUnderImport; QString dbgMsg; dbgMsg = QString("Process on: '%1', id: '%3', amount: '%2', fees: '%4'") .arg(statementTransactionUnderImport.m_datePosted.toString(Qt::ISODate)) .arg(statementTransactionUnderImport.m_amount.formatMoney("", 2)) .arg(statementTransactionUnderImport.m_strBankID) .arg(statementTransactionUnderImport.m_fees.formatMoney("", 2)); qDebug("%s", qPrintable(dbgMsg)); // mark it imported for the view transactionUnderImport.setImported(); // TODO (Ace) We can get the commodity from the statement!! // Although then we would need UI to verify transactionUnderImport.setCommodity(d->m_account.currencyId()); transactionUnderImport.setPostDate(statementTransactionUnderImport.m_datePosted); transactionUnderImport.setMemo(statementTransactionUnderImport.m_strMemo); MyMoneySplit s1; MyMoneySplit s2; MyMoneySplit sFees; MyMoneySplit sBrokerage; s1.setMemo(statementTransactionUnderImport.m_strMemo); s1.setValue(statementTransactionUnderImport.m_amount + statementTransactionUnderImport.m_fees); s1.setShares(s1.value()); s1.setNumber(statementTransactionUnderImport.m_strNumber); // set these values if a transfer split is needed at the very end. MyMoneyMoney transfervalue; // If the user has chosen to import into an investment account, determine the correct account to use MyMoneyAccount thisaccount = d->m_account; QString brokerageactid; if (thisaccount.accountType() == Account::Type::Investment) { // determine the brokerage account brokerageactid = d->m_account.value("kmm-brokerage-account").toUtf8(); if (brokerageactid.isEmpty()) { brokerageactid = file->accountByName(statementTransactionUnderImport.m_strBrokerageAccount).id(); } if (brokerageactid.isEmpty()) { brokerageactid = file->nameToAccount(statementTransactionUnderImport.m_strBrokerageAccount); } if (brokerageactid.isEmpty()) { brokerageactid = file->nameToAccount(thisaccount.brokerageName()); } if (brokerageactid.isEmpty()) { brokerageactid = SelectBrokerageAccount(); } // find the security transacted, UNLESS this transaction didn't // involve any security. if ((statementTransactionUnderImport.m_eAction != eMyMoney::Transaction::Action::None) // eaInterest transactions MAY have a security. // && (t_in.m_eAction != MyMoneyStatement::Transaction::eaInterest) && (statementTransactionUnderImport.m_eAction != eMyMoney::Transaction::Action::Fees)) { // the correct account is the stock account which matches two criteria: // (1) it is a sub-account of the selected investment account, and // (2a) the symbol of the underlying security matches the security of the // transaction, or // (2b) the name of the security matches the name of the security of the transaction. // search through each subordinate account auto found = false; QString currencyid; foreach (const auto sAccount, thisaccount.accountList()) { currencyid = file->account(sAccount).currencyId(); auto security = file->security(currencyid); if (matchNotEmpty(statementTransactionUnderImport.m_strSymbol, security.tradingSymbol()) || matchNotEmpty(statementTransactionUnderImport.m_strSecurity, security.name())) { thisaccount = file->account(sAccount); found = true; break; } } // If there was no stock account under the m_acccount investment account, // add one using the security. if (!found) { // The security should always be available, because the statement file // should separately list all the securities referred to in the file, // and when we found a security, we added it to the file. if (statementTransactionUnderImport.m_strSecurity.isEmpty()) { KMessageBox::information(0, i18n("This imported statement contains investment transactions with no security. These transactions will be ignored."), i18n("Security not found"), QString("BlankSecurity")); return; } else { MyMoneySecurity security; QList list = MyMoneyFile::instance()->securityList(); QList::ConstIterator it = list.constBegin(); while (it != list.constEnd() && security.id().isEmpty()) { if (matchNotEmpty(statementTransactionUnderImport.m_strSymbol, (*it).tradingSymbol()) || matchNotEmpty(statementTransactionUnderImport.m_strSecurity, (*it).name())) { security = *it; } ++it; } if (!security.id().isEmpty()) { thisaccount = MyMoneyAccount(); thisaccount.setName(security.name()); thisaccount.setAccountType(Account::Type::Stock); thisaccount.setCurrencyId(security.id()); currencyid = thisaccount.currencyId(); file->addAccount(thisaccount, d->m_account); qDebug() << Q_FUNC_INFO << ": created account " << thisaccount.id() << " for security " << statementTransactionUnderImport.m_strSecurity << " under account " << d->m_account.id(); } // this security does not exist in the file. else { // This should be rare. A statement should have a security entry for any // of the securities referred to in the transactions. The only way to get // here is if that's NOT the case. int ret = KMessageBox::warningContinueCancel(0, i18n("
This investment account does not contain the \"%1\" security.
" "
Transactions involving this security will be ignored.
", statementTransactionUnderImport.m_strSecurity), i18n("Security not found"), KStandardGuiItem::cont(), KStandardGuiItem::cancel()); if (ret == KMessageBox::Cancel) { m_userAbort = true; } return; } } } // Don't update price if there is no price information contained in the transaction if (statementTransactionUnderImport.m_eAction != eMyMoney::Transaction::Action::CashDividend && statementTransactionUnderImport.m_eAction != eMyMoney::Transaction::Action::Shrsin && statementTransactionUnderImport.m_eAction != eMyMoney::Transaction::Action::Shrsout) { // update the price, while we're here. in the future, this should be // an option QString basecurrencyid = file->baseCurrency().id(); const MyMoneyPrice &price = file->price(currencyid, basecurrencyid, statementTransactionUnderImport.m_datePosted, true); if (!price.isValid() && ((!statementTransactionUnderImport.m_amount.isZero() && !statementTransactionUnderImport.m_shares.isZero()) || !statementTransactionUnderImport.m_price.isZero())) { MyMoneyPrice newprice; if (!statementTransactionUnderImport.m_price.isZero()) { newprice = MyMoneyPrice(currencyid, basecurrencyid, statementTransactionUnderImport.m_datePosted, statementTransactionUnderImport.m_price.abs(), i18n("Statement Importer")); } else { newprice = MyMoneyPrice(currencyid, basecurrencyid, statementTransactionUnderImport.m_datePosted, (statementTransactionUnderImport.m_amount / statementTransactionUnderImport.m_shares).abs(), i18n("Statement Importer")); } file->addPrice(newprice); } } } s1.setAccountId(thisaccount.id()); d->assignUniqueBankID(s1, statementTransactionUnderImport); if (statementTransactionUnderImport.m_eAction == eMyMoney::Transaction::Action::ReinvestDividend) { s1.setAction(MyMoneySplit::ActionReinvestDividend); s1.setShares(statementTransactionUnderImport.m_shares); if (!statementTransactionUnderImport.m_price.isZero()) { s1.setPrice(statementTransactionUnderImport.m_price); } else { if (statementTransactionUnderImport.m_shares.isZero()) { KMessageBox::information(0, i18n("This imported statement contains investment transactions with no share amount. These transactions will be ignored."), i18n("No share amount provided"), QString("BlankAmount")); return; } MyMoneyMoney total = -statementTransactionUnderImport.m_amount - statementTransactionUnderImport.m_fees; s1.setPrice(MyMoneyMoney((total / statementTransactionUnderImport.m_shares).convertPrecision(file->security(thisaccount.currencyId()).pricePrecision()))); } s2.setMemo(statementTransactionUnderImport.m_strMemo); if (statementTransactionUnderImport.m_strInterestCategory.isEmpty()) s2.setAccountId(d->interestId(thisaccount)); else s2.setAccountId(d->interestId(statementTransactionUnderImport.m_strInterestCategory)); s2.setShares(-statementTransactionUnderImport.m_amount - statementTransactionUnderImport.m_fees); s2.setValue(s2.shares()); } else if (statementTransactionUnderImport.m_eAction == eMyMoney::Transaction::Action::CashDividend) { // Cash dividends require setting 2 splits to get all of the information // in. Split #1 will be the income split, and we'll set it to the first // income account. This is a hack, but it's needed in order to get the // amount into the transaction. if (statementTransactionUnderImport.m_strInterestCategory.isEmpty()) s1.setAccountId(d->interestId(thisaccount)); else {// Ensure category sub-accounts are dealt with properly s1.setAccountId(d->interestId(statementTransactionUnderImport.m_strInterestCategory)); } s1.setShares(-statementTransactionUnderImport.m_amount - statementTransactionUnderImport.m_fees); s1.setValue(-statementTransactionUnderImport.m_amount - statementTransactionUnderImport.m_fees); // Split 2 will be the zero-amount investment split that serves to // mark this transaction as a cash dividend and note which stock account // it belongs to. s2.setMemo(statementTransactionUnderImport.m_strMemo); s2.setAction(MyMoneySplit::ActionDividend); s2.setAccountId(thisaccount.id()); /* at this point any fees have been taken into account already * so don't deduct them again. * BUG 322381 */ transfervalue = statementTransactionUnderImport.m_amount; } else if (statementTransactionUnderImport.m_eAction == eMyMoney::Transaction::Action::Interest) { if (statementTransactionUnderImport.m_strInterestCategory.isEmpty()) s1.setAccountId(d->interestId(thisaccount)); else {// Ensure category sub-accounts are dealt with properly if (statementTransactionUnderImport.m_amount.isPositive()) s1.setAccountId(d->interestId(statementTransactionUnderImport.m_strInterestCategory)); else s1.setAccountId(d->expenseId(statementTransactionUnderImport.m_strInterestCategory)); } s1.setShares(-statementTransactionUnderImport.m_amount - statementTransactionUnderImport.m_fees); s1.setValue(-statementTransactionUnderImport.m_amount - statementTransactionUnderImport.m_fees); /// *********** Add split as per Div ********** // Split 2 will be the zero-amount investment split that serves to // mark this transaction as a cash dividend and note which stock account // it belongs to. s2.setMemo(statementTransactionUnderImport.m_strMemo); s2.setAction(MyMoneySplit::ActionInterestIncome); s2.setAccountId(thisaccount.id()); transfervalue = statementTransactionUnderImport.m_amount; } else if (statementTransactionUnderImport.m_eAction == eMyMoney::Transaction::Action::Fees) { if (statementTransactionUnderImport.m_strInterestCategory.isEmpty()) s1.setAccountId(d->feeId(thisaccount)); else// Ensure category sub-accounts are dealt with properly s1.setAccountId(d->feeId(statementTransactionUnderImport.m_strInterestCategory)); s1.setShares(statementTransactionUnderImport.m_amount); s1.setValue(statementTransactionUnderImport.m_amount); transfervalue = statementTransactionUnderImport.m_amount; } else if ((statementTransactionUnderImport.m_eAction == eMyMoney::Transaction::Action::Buy) || (statementTransactionUnderImport.m_eAction == eMyMoney::Transaction::Action::Sell)) { s1.setAction(MyMoneySplit::ActionBuyShares); if (!statementTransactionUnderImport.m_price.isZero()) { s1.setPrice(statementTransactionUnderImport.m_price.abs()); } else if (!statementTransactionUnderImport.m_shares.isZero()) { MyMoneyMoney total = statementTransactionUnderImport.m_amount + statementTransactionUnderImport.m_fees.abs(); s1.setPrice(MyMoneyMoney((total / statementTransactionUnderImport.m_shares).abs().convertPrecision(file->security(thisaccount.currencyId()).pricePrecision()))); } if (statementTransactionUnderImport.m_eAction == eMyMoney::Transaction::Action::Buy) s1.setShares(statementTransactionUnderImport.m_shares.abs()); else s1.setShares(-statementTransactionUnderImport.m_shares.abs()); s1.setValue(-(statementTransactionUnderImport.m_amount + statementTransactionUnderImport.m_fees.abs())); transfervalue = statementTransactionUnderImport.m_amount; } else if ((statementTransactionUnderImport.m_eAction == eMyMoney::Transaction::Action::Shrsin) || (statementTransactionUnderImport.m_eAction == eMyMoney::Transaction::Action::Shrsout)) { s1.setValue(MyMoneyMoney()); s1.setShares(statementTransactionUnderImport.m_shares); s1.setAction(MyMoneySplit::ActionAddShares); } else if (statementTransactionUnderImport.m_eAction == eMyMoney::Transaction::Action::None) { // User is attempting to import a non-investment transaction into this // investment account. This is not supportable the way KMyMoney is // written. However, if a user has an associated brokerage account, // we can stuff the transaction there. QString brokerageactid = d->m_account.value("kmm-brokerage-account").toUtf8(); if (brokerageactid.isEmpty()) { brokerageactid = file->accountByName(d->m_account.brokerageName()).id(); } if (! brokerageactid.isEmpty()) { s1.setAccountId(brokerageactid); d->assignUniqueBankID(s1, statementTransactionUnderImport); // Needed to satisfy the bankid check below. thisaccount = file->account(brokerageactid); } else { // Warning!! Your transaction is being thrown away. } } if (!statementTransactionUnderImport.m_fees.isZero()) { sFees.setMemo(i18n("(Fees) %1", statementTransactionUnderImport.m_strMemo)); sFees.setValue(statementTransactionUnderImport.m_fees); sFees.setShares(statementTransactionUnderImport.m_fees); sFees.setAccountId(d->feeId(thisaccount)); } } else { // For non-investment accounts, just use the selected account // Note that it is perfectly reasonable to import an investment statement into a non-investment account // if you really want. The investment-specific information, such as number of shares and action will // be discarded in that case. s1.setAccountId(d->m_account.id()); d->assignUniqueBankID(s1, statementTransactionUnderImport); } QString payeename = statementTransactionUnderImport.m_strPayee; if (!payeename.isEmpty()) { qDebug() << QLatin1String("Start matching payee") << payeename; QString payeeid; try { QList pList = file->payeeList(); QList::const_iterator it_p; QMap matchMap; for (it_p = pList.constBegin(); it_p != pList.constEnd(); ++it_p) { bool ignoreCase; QStringList keys; QStringList::const_iterator it_s; const MyMoneyPayee::payeeMatchType matchType = (*it_p).matchData(ignoreCase, keys); switch (matchType) { case MyMoneyPayee::matchDisabled: break; case MyMoneyPayee::matchName: case MyMoneyPayee::matchNameExact: keys << QString("%1").arg(QRegExp::escape((*it_p).name())); if(matchType == MyMoneyPayee::matchNameExact) { keys.clear(); keys << QString("^%1$").arg(QRegExp::escape((*it_p).name())); } // intentional fall through case MyMoneyPayee::matchKey: for (it_s = keys.constBegin(); it_s != keys.constEnd(); ++it_s) { QRegExp exp(*it_s, ignoreCase ? Qt::CaseInsensitive : Qt::CaseSensitive); if (exp.indexIn(payeename) != -1) { qDebug("Found match with '%s' on '%s'", qPrintable(payeename), qPrintable((*it_p).name())); matchMap[exp.matchedLength()] = (*it_p).id(); } } break; } } // at this point we can have several scenarios: // a) multiple matches // b) a single match // c) no match at all // // for c) we just do nothing, for b) we take the one we found // in case of a) we take the one with the largest matchedLength() // which happens to be the last one in the map if (matchMap.count() > 1) { qDebug("Multiple matches"); QMap::const_iterator it_m = matchMap.constEnd(); --it_m; payeeid = *it_m; } else if (matchMap.count() == 1) { qDebug("Single matches"); payeeid = *(matchMap.constBegin()); } // if we did not find a matching payee, we throw an exception and try to create it if (payeeid.isEmpty()) throw MYMONEYEXCEPTION("payee not matched"); s1.setPayeeId(payeeid); } catch (const MyMoneyException &) { MyMoneyPayee payee; int rc = KMessageBox::Yes; if (m_autoCreatePayee == false) { // Ask the user if that is what he intended to do? QString msg = i18n("Do you want to add \"%1\" as payee/receiver?\n\n", payeename); msg += i18n("Selecting \"Yes\" will create the payee, \"No\" will skip " "creation of a payee record and remove the payee information " "from this transaction. Selecting \"Cancel\" aborts the import " "operation.\n\nIf you select \"No\" here and mark the \"Do not ask " "again\" checkbox, the payee information for all following transactions " "referencing \"%1\" will be removed.", payeename); QString askKey = QString("Statement-Import-Payee-") + payeename; if (!m_dontAskAgain.contains(askKey)) { m_dontAskAgain += askKey; } rc = KMessageBox::questionYesNoCancel(0, msg, i18n("New payee/receiver"), KStandardGuiItem::yes(), KStandardGuiItem::no(), KStandardGuiItem::cancel(), askKey); } if (rc == KMessageBox::Yes) { // for now, we just add the payee to the pool and turn // on simple name matching, so that future transactions // with the same name don't get here again. // // In the future, we could open a dialog and ask for // all the other attributes of the payee, but since this // is called in the context of an automatic procedure it // might distract the user. payee.setName(payeename); payee.setMatchData(MyMoneyPayee::matchKey, true, QStringList() << QString("^%1$").arg(QRegExp::escape(payeename))); if (m_askPayeeCategory) { // We use a QPointer because the dialog may get deleted // during exec() if the parent of the dialog gets deleted. // In that case the guarded ptr will reset to 0. QPointer dialog = new QDialog; dialog->setWindowTitle(i18n("Default Category for Payee")); dialog->setModal(true); QWidget *mainWidget = new QWidget; QVBoxLayout *topcontents = new QVBoxLayout(mainWidget); //add in caption? and account combo here QLabel *label1 = new QLabel(i18n("Please select a default category for payee '%1'", payeename)); topcontents->addWidget(label1); auto filterProxyModel = new AccountNamesFilterProxyModel(this); filterProxyModel->setHideEquityAccounts(!KMyMoneyGlobalSettings::expertMode()); filterProxyModel->addAccountGroup(QVector {Account::Type::Asset, Account::Type::Liability, Account::Type::Equity, Account::Type::Income, Account::Type::Expense}); auto const model = Models::instance()->accountsModel(); filterProxyModel->setSourceModel(model); filterProxyModel->setSourceColumns(model->getColumns()); filterProxyModel->sort((int)eAccountsModel::Column::Account); QPointer accountCombo = new KMyMoneyAccountCombo(filterProxyModel); topcontents->addWidget(accountCombo); mainWidget->setLayout(topcontents); QVBoxLayout *mainLayout = new QVBoxLayout; QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Cancel|QDialogButtonBox::No|QDialogButtonBox::Yes); dialog->setLayout(mainLayout); mainLayout->addWidget(mainWidget); dialog->connect(buttonBox, SIGNAL(accepted()), dialog, SLOT(accept())); dialog->connect(buttonBox, SIGNAL(rejected()), dialog, SLOT(reject())); mainLayout->addWidget(buttonBox); KGuiItem::assign(buttonBox->button(QDialogButtonBox::Yes), KGuiItem(i18n("Save Category"))); KGuiItem::assign(buttonBox->button(QDialogButtonBox::No), KGuiItem(i18n("No Category"))); KGuiItem::assign(buttonBox->button(QDialogButtonBox::Cancel), KGuiItem(i18n("Abort"))); int result = dialog->exec(); QString accountId; if (accountCombo && !accountCombo->getSelected().isEmpty()) { accountId = accountCombo->getSelected(); } delete dialog; //if they hit yes instead of no, then grab setting of account combo if (result == QDialog::Accepted) { payee.setDefaultAccountId(accountId); } else if (result != QDialog::Rejected) { //add cancel button? and throw exception like below throw MYMONEYEXCEPTION("USERABORT"); } } try { file->addPayee(payee); qDebug("Payee '%s' created", qPrintable(payee.name())); d->payees << payee; payeeid = payee.id(); s1.setPayeeId(payeeid); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(0, i18n("Unable to add payee/receiver"), i18n("%1 thrown in %2:%3", e.what(), e.file(), e.line())); } } else if (rc == KMessageBox::No) { s1.setPayeeId(QString()); } else { throw MYMONEYEXCEPTION("USERABORT"); } } if (thisaccount.accountType() != Account::Type::Stock) { // // Fill in other side of the transaction (category/etc) based on payee // // Note, this logic is lifted from KLedgerView::slotPayeeChanged(), // however this case is more complicated, because we have an amount and // a memo. We just don't have the other side of the transaction. // // We'll search for the most recent transaction in this account with // this payee. If this reference transaction is a simple 2-split // transaction, it's simple. If it's a complex split, and the amounts // are different, we have a problem. Somehow we have to balance the // transaction. For now, we'll leave it unbalanced, and let the user // handle it. // const MyMoneyPayee& payeeObj = MyMoneyFile::instance()->payee(payeeid); if (statementTransactionUnderImport.m_listSplits.isEmpty() && payeeObj.defaultAccountEnabled()) { MyMoneyAccount splitAccount = file->account(payeeObj.defaultAccountId()); MyMoneySplit s; s.setReconcileFlag(eMyMoney::Split::State::Cleared); s.clearId(); s.setBankID(QString()); s.setShares(-s1.shares()); s.setValue(-s1.value()); s.setAccountId(payeeObj.defaultAccountId()); s.setMemo(transactionUnderImport.memo()); s.setPayeeId(payeeid); d->setupPrice(s, splitAccount, d->m_account, statementTransactionUnderImport.m_datePosted); transactionUnderImport.addSplit(s); file->addVATSplit(transactionUnderImport, d->m_account, splitAccount, statementTransactionUnderImport.m_amount); } else if (statementTransactionUnderImport.m_listSplits.isEmpty() && !d->m_skipCategoryMatching) { MyMoneyTransactionFilter filter(thisaccount.id()); filter.addPayee(payeeid); QList list = file->transactionList(filter); if (!list.empty()) { // Default to using the most recent transaction as the reference MyMoneyTransaction t_old = list.last(); // if there is more than one matching transaction, try to be a little // smart about which one we take. for now, we'll see if there's one // with the same VALUE as our imported transaction, and if so take that one. if (list.count() > 1) { QList::ConstIterator it_trans = list.constEnd(); if (it_trans != list.constBegin()) --it_trans; while (it_trans != list.constBegin()) { MyMoneySplit s = (*it_trans).splitByAccount(thisaccount.id()); if (s.value() == s1.value()) { // keep searching if this transaction references a closed account if (!MyMoneyFile::instance()->referencesClosedAccount(*it_trans)) { t_old = *it_trans; break; } } --it_trans; } // check constBegin, just in case if (it_trans == list.constBegin()) { MyMoneySplit s = (*it_trans).splitByAccount(thisaccount.id()); if (s.value() == s1.value()) { t_old = *it_trans; } } } // Only copy the splits if the transaction found does not reference a closed account if (!MyMoneyFile::instance()->referencesClosedAccount(t_old)) { foreach (const auto split, t_old.splits()) { // We don't need the split that covers this account, // we just need the other ones. if (split.accountId() != thisaccount.id()) { MyMoneySplit s(split); s.setReconcileFlag(eMyMoney::Split::State::NotReconciled); s.clearId(); s.setBankID(QString()); s.removeMatch(); if (t_old.splits().count() == 2) { s.setShares(-s1.shares()); s.setValue(-s1.value()); s.setMemo(s1.memo()); } MyMoneyAccount splitAccount = file->account(s.accountId()); qDebug("Adding second split to %s(%s)", qPrintable(splitAccount.name()), qPrintable(s.accountId())); d->setupPrice(s, splitAccount, d->m_account, statementTransactionUnderImport.m_datePosted); transactionUnderImport.addSplit(s); } } } } } } } s1.setReconcileFlag(statementTransactionUnderImport.m_reconcile); // Add the 'account' split if it's needed if (! transfervalue.isZero()) { // in case the transaction has a reference to the brokerage account, we use it // but if brokerageactid has already been set, keep that. if (!statementTransactionUnderImport.m_strBrokerageAccount.isEmpty() && brokerageactid.isEmpty()) { brokerageactid = file->nameToAccount(statementTransactionUnderImport.m_strBrokerageAccount); } if (brokerageactid.isEmpty()) { brokerageactid = file->accountByName(statementTransactionUnderImport.m_strBrokerageAccount).id(); } // There is no BrokerageAccount so have to nowhere to put this split. if (!brokerageactid.isEmpty()) { sBrokerage.setMemo(statementTransactionUnderImport.m_strMemo); sBrokerage.setValue(transfervalue); sBrokerage.setShares(transfervalue); sBrokerage.setAccountId(brokerageactid); sBrokerage.setReconcileFlag(statementTransactionUnderImport.m_reconcile); MyMoneyAccount splitAccount = file->account(sBrokerage.accountId()); d->setupPrice(sBrokerage, splitAccount, d->m_account, statementTransactionUnderImport.m_datePosted); } } if (!(sBrokerage == MyMoneySplit())) transactionUnderImport.addSplit(sBrokerage); if (!(sFees == MyMoneySplit())) transactionUnderImport.addSplit(sFees); if (!(s2 == MyMoneySplit())) transactionUnderImport.addSplit(s2); transactionUnderImport.addSplit(s1); if ((statementTransactionUnderImport.m_eAction != eMyMoney::Transaction::Action::ReinvestDividend) && (statementTransactionUnderImport.m_eAction != eMyMoney::Transaction::Action::CashDividend) && (statementTransactionUnderImport.m_eAction != eMyMoney::Transaction::Action::Interest) ) { //****************************************** // process splits //****************************************** QList::const_iterator it_s; for (it_s = statementTransactionUnderImport.m_listSplits.begin(); it_s != statementTransactionUnderImport.m_listSplits.end(); ++it_s) { MyMoneySplit s3; s3.setAccountId((*it_s).m_accountId); MyMoneyAccount acc = file->account(s3.accountId()); s3.setPayeeId(s1.payeeId()); s3.setMemo((*it_s).m_strMemo); s3.setShares((*it_s).m_amount); s3.setValue((*it_s).m_amount); s3.setReconcileFlag((*it_s).m_reconcile); d->setupPrice(s3, acc, d->m_account, statementTransactionUnderImport.m_datePosted); transactionUnderImport.addSplit(s3); } } // Add the transaction try { // check for matches already stored in the engine TransactionMatchFinder::MatchResult result; TransactionMatcher matcher(thisaccount); d->transactionsCount++; ExistingTransactionMatchFinder existingTrMatchFinder(KMyMoneyGlobalSettings::matchInterval()); result = existingTrMatchFinder.findMatch(transactionUnderImport, s1); if (result != TransactionMatchFinder::MatchNotFound) { MyMoneyTransaction matchedTransaction = existingTrMatchFinder.getMatchedTransaction(); if (!matchedTransaction.isImported() || result == TransactionMatchFinder::MatchPrecise) { // don't match with just imported transaction MyMoneySplit matchedSplit = existingTrMatchFinder.getMatchedSplit(); handleMatchingOfExistingTransaction(matcher, matchedTransaction, matchedSplit, transactionUnderImport, s1, result); return; } } addTransaction(transactionUnderImport); ScheduledTransactionMatchFinder scheduledTrMatchFinder(thisaccount, KMyMoneyGlobalSettings::matchInterval()); result = scheduledTrMatchFinder.findMatch(transactionUnderImport, s1); if (result != TransactionMatchFinder::MatchNotFound) { MyMoneySplit matchedSplit = scheduledTrMatchFinder.getMatchedSplit(); MyMoneySchedule matchedSchedule = scheduledTrMatchFinder.getMatchedSchedule(); handleMatchingOfScheduledTransaction(matcher, matchedSchedule, matchedSplit, transactionUnderImport, s1); return; } } catch (const MyMoneyException &e) { QString message(i18n("Problem adding or matching imported transaction with id '%1': %2", statementTransactionUnderImport.m_strBankID, e.what())); qDebug("%s", qPrintable(message)); int result = KMessageBox::warningContinueCancel(0, message); if (result == KMessageBox::Cancel) throw MYMONEYEXCEPTION("USERABORT"); } } QString MyMoneyStatementReader::SelectBrokerageAccount() { if (d->m_brokerageAccount.id().isEmpty()) { d->m_brokerageAccount.setAccountType(Account::Type::Checkings); if (!m_userAbort) m_userAbort = ! selectOrCreateAccount(Select, d->m_brokerageAccount); } return d->m_brokerageAccount.id(); } bool MyMoneyStatementReader::selectOrCreateAccount(const SelectCreateMode /*mode*/, MyMoneyAccount& account) { bool result = false; MyMoneyFile* file = MyMoneyFile::instance(); QString accountId; // Try to find an existing account in the engine which matches this one. // There are two ways to be a "matching account". The account number can // match the statement account OR the "StatementKey" property can match. // Either way, we'll update the "StatementKey" property for next time. QString accountNumber = account.number(); if (! accountNumber.isEmpty()) { // Get a list of all accounts QList accounts; file->accountList(accounts); // Iterate through them QList::const_iterator it_account = accounts.constBegin(); while (it_account != accounts.constEnd()) { if ( ((*it_account).value("StatementKey") == accountNumber) || ((*it_account).number() == accountNumber) ) { MyMoneyAccount newAccount((*it_account).id(), account); account = newAccount; accountId = (*it_account).id(); break; } ++it_account; } } QString msg = i18n("You have downloaded a statement for the following account:

"); msg += i18n(" - Account Name: %1", account.name()) + "
"; msg += i18n(" - Account Type: %1", MyMoneyAccount::accountTypeToString(account.accountType())) + "
"; msg += i18n(" - Account Number: %1", account.number()) + "
"; msg += "
"; if (!account.name().isEmpty()) { if (!accountId.isEmpty()) msg += i18n("Do you want to import transactions to this account?"); else msg += i18n("KMyMoney cannot determine which of your accounts to use. You can " "create a new account by pressing the Create button " "or select another one manually from the selection box below."); } else { msg += i18n("No account information has been found in the selected statement file. " "Please select an account using the selection box in the dialog or " "create a new account by pressing the Create button."); } eDialogs::Category type; if (account.accountType() == Account::Type::Checkings) { type = eDialogs::Category::checking; } else if (account.accountType() == Account::Type::Savings) { type = eDialogs::Category::savings; } else if (account.accountType() == Account::Type::Investment) { type = eDialogs::Category::investment; } else if (account.accountType() == Account::Type::CreditCard) { type = eDialogs::Category::creditCard; } else { type = static_cast(eDialogs::Category::asset | eDialogs::Category::liability); } QPointer accountSelect = new KAccountSelectDlg(type, "StatementImport", 0); connect(accountSelect, &KAccountSelectDlg::createAccount, this, &MyMoneyStatementReader::createAccount); connect(accountSelect, &KAccountSelectDlg::createCategory, this, &MyMoneyStatementReader::createCategory); accountSelect->setHeader(i18n("Import transactions")); accountSelect->setDescription(msg); accountSelect->setAccount(account, accountId); accountSelect->setMode(false); accountSelect->showAbortButton(true); accountSelect->hideQifEntry(); QString accname; bool done = false; while (!done) { if (accountSelect->exec() == QDialog::Accepted && !accountSelect->selectedAccount().isEmpty()) { result = true; done = true; accountId = accountSelect->selectedAccount(); account = file->account(accountId); if (! accountNumber.isEmpty() && account.value("StatementKey") != accountNumber) { account.setValue("StatementKey", accountNumber); MyMoneyFileTransaction ft; try { MyMoneyFile::instance()->modifyAccount(account); ft.commit(); accname = account.name(); } catch (const MyMoneyException &) { qDebug("Updating account in MyMoneyStatementReader::selectOrCreateAccount failed"); } } } else { if (accountSelect->aborted()) //throw MYMONEYEXCEPTION("USERABORT"); done = true; else KMessageBox::error(0, QLatin1String("") + i18n("You must select an account, create a new one, or press the Abort button.") + QLatin1String("")); } } delete accountSelect; return result; } const MyMoneyAccount& MyMoneyStatementReader::account() const { return d->m_account; } void MyMoneyStatementReader::setProgressCallback(void(*callback)(int, int, const QString&)) { m_progressCallback = callback; } void MyMoneyStatementReader::signalProgress(int current, int total, const QString& msg) { if (m_progressCallback != 0) (*m_progressCallback)(current, total, msg); } void MyMoneyStatementReader::handleMatchingOfExistingTransaction(TransactionMatcher & matcher, MyMoneyTransaction matchedTransaction, MyMoneySplit matchedSplit, MyMoneyTransaction & importedTransaction, const MyMoneySplit & importedSplit, const TransactionMatchFinder::MatchResult & matchResult) { switch (matchResult) { case TransactionMatchFinder::MatchNotFound: break; case TransactionMatchFinder::MatchDuplicate: d->transactionsDuplicate++; qDebug("Detected transaction duplicate"); break; case TransactionMatchFinder::MatchImprecise: case TransactionMatchFinder::MatchPrecise: addTransaction(importedTransaction); qDebug("Detected as match to transaction '%s'", qPrintable(matchedTransaction.id())); matcher.match(matchedTransaction, matchedSplit, importedTransaction, importedSplit, true); d->transactionsMatched++; break; } } void MyMoneyStatementReader::handleMatchingOfScheduledTransaction(TransactionMatcher & matcher, MyMoneySchedule matchedSchedule, MyMoneySplit matchedSplit, const MyMoneyTransaction & importedTransaction, const MyMoneySplit & importedSplit) { QPointer editor; if (askUserToEnterScheduleForMatching(matchedSchedule, importedSplit, importedTransaction)) { KEnterScheduleDlg dlg(0, matchedSchedule); editor = dlg.startEdit(); if (editor) { MyMoneyTransaction torig; try { // in case the amounts of the scheduled transaction and the // imported transaction differ, we need to update the amount // using the transaction editor. if (matchedSplit.shares() != importedSplit.shares() && !matchedSchedule.isFixed()) { // for now this only works with regular transactions and not // for investment transactions. As of this, we don't have // scheduled investment transactions anyway. auto se = dynamic_cast(editor.data()); if (se) { // the following call will update the amount field in the // editor and also adjust a possible VAT assignment. Make // sure to use only the absolute value of the amount, because // the editor keeps the sign in a different position (deposit, // withdrawal tab) KMyMoneyEdit* amount = dynamic_cast(se->haveWidget("amount")); if (amount) { amount->setValue(importedSplit.shares().abs()); se->slotUpdateAmount(importedSplit.shares().abs().toString()); // we also need to update the matchedSplit variable to // have the modified share/value. matchedSplit.setShares(importedSplit.shares()); matchedSplit.setValue(importedSplit.value()); } } } editor->createTransaction(torig, dlg.transaction(), dlg.transaction().splits().isEmpty() ? MyMoneySplit() : dlg.transaction().splits().front(), true); QString newId; if (editor->enterTransactions(newId, false, true)) { if (!newId.isEmpty()) { torig = MyMoneyFile::instance()->transaction(newId); matchedSchedule.setLastPayment(torig.postDate()); } matchedSchedule.setNextDueDate(matchedSchedule.nextPayment(matchedSchedule.nextDueDate())); MyMoneyFile::instance()->modifySchedule(matchedSchedule); } // now match the two transactions matcher.match(torig, matchedSplit, importedTransaction, importedSplit); d->transactionsMatched++; } catch (const MyMoneyException &e) { // make sure we get rid of the editor before // the KEnterScheduleDlg is destroyed delete editor; throw e; // rethrow } } // delete the editor delete editor; } } void MyMoneyStatementReader::addTransaction(MyMoneyTransaction& transaction) { MyMoneyFile* file = MyMoneyFile::instance(); file->addTransaction(transaction); d->transactionsAdded++; } bool MyMoneyStatementReader::askUserToEnterScheduleForMatching(const MyMoneySchedule& matchedSchedule, const MyMoneySplit& importedSplit, const MyMoneyTransaction & importedTransaction) const { QString scheduleName = matchedSchedule.name(); int currencyDenom = d->m_account.fraction(MyMoneyFile::instance()->currency(d->m_account.currencyId())); QString splitValue = importedSplit.value().formatMoney(currencyDenom); QString payeeName = MyMoneyFile::instance()->payee(importedSplit.payeeId()).name(); QString questionMsg = i18n("KMyMoney has found a scheduled transaction which matches an imported transaction.
" "Schedule name: %1
" "Transaction: %2 %3
" "Do you want KMyMoney to enter this schedule now so that the transaction can be matched?", scheduleName, splitValue, payeeName); // check that dates are within user's setting const int gap = std::abs(matchedSchedule.transaction().postDate().toJulianDay() - importedTransaction.postDate().toJulianDay()); if (gap > KMyMoneyGlobalSettings::matchInterval()) questionMsg = i18np("KMyMoney has found a scheduled transaction which matches an imported transaction.
" "Schedule name: %2
" "Transaction: %3 %4
" "The transaction dates are one day apart.
" "Do you want KMyMoney to enter this schedule now so that the transaction can be matched?", "KMyMoney has found a scheduled transaction which matches an imported transaction.
" "Schedule name: %2
" "Transaction: %3 %4
" "The transaction dates are %1 days apart.
" "Do you want KMyMoney to enter this schedule now so that the transaction can be matched?", gap ,scheduleName, splitValue, payeeName); const int userAnswer = KMessageBox::questionYesNo(0, QLatin1String("") + questionMsg + QLatin1String(""), i18n("Schedule found")); return (userAnswer == KMessageBox::Yes); } diff --git a/kmymoney/dialogs/investtransactioneditor.cpp b/kmymoney/dialogs/investtransactioneditor.cpp index afb4ccfc2..c1adc9f0e 100644 --- a/kmymoney/dialogs/investtransactioneditor.cpp +++ b/kmymoney/dialogs/investtransactioneditor.cpp @@ -1,1219 +1,1220 @@ /*************************************************************************** investtransactioneditor.cpp ---------- begin : Fri Dec 15 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 "investtransactioneditor.h" #include "transactioneditor_p.h" #include // ---------------------------------------------------------------------------- // QT Includes #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include // ---------------------------------------------------------------------------- // Project Includes #include "kmymoneyreconcilecombo.h" #include "kmymoneyactivitycombo.h" #include "kmymoneytagcombo.h" #include "ktagcontainer.h" #include "investtransaction.h" #include "selectedtransactions.h" #include "transactioneditorcontainer.h" #include "kmymoneycategory.h" #include "kmymoneydateinput.h" #include "kmymoneyedit.h" #include "kmymoneyaccountselector.h" #include "kmymoneymvccombo.h" #include "mymoneyfile.h" +#include "mymoneyexception.h" #include "mymoneysecurity.h" #include "mymoneyprice.h" #include "ksplittransactiondlg.h" #include "kcurrencycalculator.h" #include "kmymoneyglobalsettings.h" #include "investactivities.h" #include "kmymoneycompletion.h" #include "dialogenums.h" using namespace eMyMoney; using namespace KMyMoneyRegister; using namespace KMyMoneyTransactionForm; using namespace Invest; class InvestTransactionEditorPrivate : public TransactionEditorPrivate { Q_DISABLE_COPY(InvestTransactionEditorPrivate) Q_DECLARE_PUBLIC(InvestTransactionEditor) friend class Invest::Activity; public: InvestTransactionEditorPrivate(InvestTransactionEditor* qq) : TransactionEditorPrivate(qq), m_activity(0) { m_phonyAccount = MyMoneyAccount("Phony-ID", MyMoneyAccount()); } ~InvestTransactionEditorPrivate() { delete m_activity; } void hideCategory(const QString& name) { Q_Q(InvestTransactionEditor); if (KMyMoneyCategory* cat = dynamic_cast(q->haveWidget(name))) { cat->hide(); cat->splitButton()->hide(); } } void activityFactory(eMyMoney::Split::InvestmentTransactionType type) { Q_Q(InvestTransactionEditor); if (!m_activity || type != m_activity->type()) { delete m_activity; switch (type) { default: case eMyMoney::Split::InvestmentTransactionType::BuyShares: m_activity = new Buy(q); break; case eMyMoney::Split::InvestmentTransactionType::SellShares: m_activity = new Sell(q); break; case eMyMoney::Split::InvestmentTransactionType::Dividend: case eMyMoney::Split::InvestmentTransactionType::Yield: m_activity = new Div(q); break; case eMyMoney::Split::InvestmentTransactionType::ReinvestDividend: m_activity = new Reinvest(q); break; case eMyMoney::Split::InvestmentTransactionType::AddShares: m_activity = new Add(q); break; case eMyMoney::Split::InvestmentTransactionType::RemoveShares: m_activity = new Remove(q); break; case eMyMoney::Split::InvestmentTransactionType::SplitShares: m_activity = new Invest::Split(q); break; case eMyMoney::Split::InvestmentTransactionType::InterestIncome: m_activity = new IntInc(q); break; } } } MyMoneyMoney subtotal(const QList& splits) const { MyMoneyMoney sum; foreach (const auto split, splits) sum += split.value(); return sum; } /** * This method creates a transaction to be used for the split fee/interest editor. * It has a reference to a phony account and the splits contained in @a splits . */ bool createPseudoTransaction(MyMoneyTransaction& t, const QList& splits) { t.removeSplits(); MyMoneySplit split; split.setAccountId(m_phonyAccount.id()); split.setValue(-subtotal(splits)); split.setShares(split.value()); t.addSplit(split); m_phonySplit = split; foreach (const auto it_s, splits) { split = it_s; split.clearId(); t.addSplit(split); } return true; } /** * Convenience method used by slotEditInterestSplits() and slotEditFeeSplits(). * * @param categoryWidgetName name of the category widget * @param amountWidgetName name of the amount widget * @param splits the splits that make up the transaction to be edited * @param isIncome @c false for fees, @c true for interest * @param slotEditSplits name of the slot to be connected to the focusIn signal of the * category widget named @p categoryWidgetName in case of multiple splits * in @p splits . */ int editSplits(const QString& categoryWidgetName, const QString& amountWidgetName, QList& splits, bool isIncome, const char* slotEditSplits) { Q_Q(InvestTransactionEditor); int rc = QDialog::Rejected; if (!m_openEditSplits) { // only get in here in a single instance m_openEditSplits = true; // force focus change to update all data KMyMoneyCategory* category = dynamic_cast(m_editWidgets[categoryWidgetName]); QWidget* w = category->splitButton(); if (w) w->setFocus(); KMyMoneyEdit* amount = dynamic_cast(q->haveWidget(amountWidgetName)); MyMoneyTransaction transaction; transaction.setCommodity(m_currency.id()); if (splits.count() == 0 && !category->selectedItem().isEmpty()) { MyMoneySplit s; s.setAccountId(category->selectedItem()); s.setShares(amount->value()); s.setValue(s.shares()); splits << s; } // use the transactions commodity as the currency indicator for the splits // this is used to allow some useful setting for the fractions in the amount fields try { m_phonyAccount.setCurrencyId(m_transaction.commodity()); m_phonyAccount.fraction(MyMoneyFile::instance()->security(m_transaction.commodity())); } catch (const MyMoneyException &) { qDebug("Unable to setup precision"); } if (createPseudoTransaction(transaction, splits)) { MyMoneyMoney value; QPointer dlg = new KSplitTransactionDlg(transaction, m_phonySplit, m_phonyAccount, false, isIncome, MyMoneyMoney(), m_priceInfo, m_regForm); // q->connect(dlg, SIGNAL(newCategory(MyMoneyAccount&)), q, SIGNAL(newCategory(MyMoneyAccount&))); if ((rc = dlg->exec()) == QDialog::Accepted) { transaction = dlg->transaction(); // collect splits out of the transaction splits.clear(); MyMoneyMoney fees; foreach (const auto split, transaction.splits()) { if (split.accountId() == m_phonyAccount.id()) continue; splits << split; fees += split.shares(); } if (isIncome) fees = -fees; QString categoryId; q->setupCategoryWidget(category, splits, categoryId, slotEditSplits); amount->setValue(fees); q->slotUpdateTotalAmount(); } delete dlg; } // focus jumps into the memo field if ((w = q->haveWidget("memo")) != 0) { w->setFocus(); } m_openEditSplits = false; } return rc; } void updatePriceMode(const MyMoneySplit& split = MyMoneySplit()) { Q_Q(InvestTransactionEditor); auto label = dynamic_cast(q->haveWidget("price-label")); if (label) { auto sharesEdit = dynamic_cast(q->haveWidget("shares")); auto priceEdit = dynamic_cast(q->haveWidget("price")); MyMoneyMoney price; if (!split.id().isEmpty()) price = split.price().reduce(); else price = priceEdit->value().abs(); if (q->priceMode() == eDialogs::PriceMode::PricePerTransaction) { priceEdit->setPrecision(m_currency.pricePrecision()); label->setText(i18n("Transaction amount")); if (!sharesEdit->value().isZero()) priceEdit->setValue(sharesEdit->value().abs() * price); } else if (q->priceMode() == eDialogs::PriceMode::PricePerShare) { priceEdit->setPrecision(m_security.pricePrecision()); label->setText(i18n("Price/Share")); priceEdit->setValue(price); } else priceEdit->setValue(price); } } Activity* m_activity; MyMoneyAccount m_phonyAccount; MyMoneySplit m_phonySplit; MyMoneySplit m_assetAccountSplit; QList m_interestSplits; QList m_feeSplits; MyMoneySecurity m_security; MyMoneySecurity m_currency; eMyMoney::Split::InvestmentTransactionType m_transactionType; }; InvestTransactionEditor::InvestTransactionEditor() : TransactionEditor(*new InvestTransactionEditorPrivate(this)) { Q_D(InvestTransactionEditor); d->m_transactionType = eMyMoney::Split::InvestmentTransactionType::UnknownTransactionType; } InvestTransactionEditor::~InvestTransactionEditor() { } InvestTransactionEditor::InvestTransactionEditor(TransactionEditorContainer* regForm, KMyMoneyRegister::InvestTransaction* item, const KMyMoneyRegister::SelectedTransactions& list, const QDate& lastPostDate) : TransactionEditor(*new InvestTransactionEditorPrivate(this), regForm, item, list, lastPostDate) { Q_D(InvestTransactionEditor); // after the gometries of the container are updated hide the widgets which are not needed by the current activity connect(d->m_regForm, &TransactionEditorContainer::geometriesUpdated, this, &InvestTransactionEditor::slotTransactionContainerGeometriesUpdated); // dissect the transaction into its type, splits, currency, security etc. KMyMoneyUtils::dissectTransaction(d->m_transaction, d->m_split, d->m_assetAccountSplit, d->m_feeSplits, d->m_interestSplits, d->m_security, d->m_currency, d->m_transactionType); // determine initial activity object d->activityFactory(d->m_transactionType); } void InvestTransactionEditor::createEditWidgets() { Q_D(InvestTransactionEditor); auto activity = new KMyMoneyActivityCombo(); d->m_editWidgets["activity"] = activity; connect(activity, &KMyMoneyActivityCombo::activitySelected, this, &InvestTransactionEditor::slotUpdateActivity); connect(activity, &KMyMoneyActivityCombo::activitySelected, this, &InvestTransactionEditor::slotUpdateButtonState); d->m_editWidgets["postdate"] = new KMyMoneyDateInput; auto security = new KMyMoneySecurity; security->setPlaceholderText(i18n("Security")); d->m_editWidgets["security"] = security; connect(security, &KMyMoneyCombo::itemSelected, this, &InvestTransactionEditor::slotUpdateSecurity); connect(security, &QComboBox::editTextChanged, this, &InvestTransactionEditor::slotUpdateButtonState); connect(security, &KMyMoneyCombo::createItem, this, &InvestTransactionEditor::slotCreateSecurity); connect(security, &KMyMoneyCombo::objectCreation, this, &TransactionEditor::objectCreation); auto asset = new KMyMoneyCategory(false, nullptr); asset->setPlaceholderText(i18n("Asset account")); d->m_editWidgets["asset-account"] = asset; connect(asset, &QComboBox::editTextChanged, this, &InvestTransactionEditor::slotUpdateButtonState); connect(asset, &KMyMoneyCombo::objectCreation, this, &TransactionEditor::objectCreation); auto fees = new KMyMoneyCategory(true, nullptr); fees->setPlaceholderText(i18n("Fees")); d->m_editWidgets["fee-account"] = fees; connect(fees, &KMyMoneyCombo::itemSelected, this, &InvestTransactionEditor::slotUpdateFeeCategory); connect(fees, &QComboBox::editTextChanged, this, &InvestTransactionEditor::slotUpdateButtonState); connect(fees, &QComboBox::editTextChanged, this, &InvestTransactionEditor::slotUpdateFeeVisibility); connect(fees, &KMyMoneyCombo::createItem, this, &InvestTransactionEditor::slotCreateFeeCategory); connect(fees, &KMyMoneyCombo::objectCreation, this, &TransactionEditor::objectCreation); connect(fees->splitButton(), &QAbstractButton::clicked, this, &InvestTransactionEditor::slotEditFeeSplits); auto interest = new KMyMoneyCategory(true, nullptr); interest->setPlaceholderText(i18n("Interest")); d->m_editWidgets["interest-account"] = interest; connect(interest, &KMyMoneyCombo::itemSelected, this, &InvestTransactionEditor::slotUpdateInterestCategory); connect(interest, &QComboBox::editTextChanged, this, &InvestTransactionEditor::slotUpdateButtonState); connect(interest, &QComboBox::editTextChanged, this, &InvestTransactionEditor::slotUpdateInterestVisibility); connect(interest, &KMyMoneyCombo::createItem, this, &InvestTransactionEditor::slotCreateInterestCategory); connect(interest, &KMyMoneyCombo::objectCreation, this, &TransactionEditor::objectCreation); connect(interest->splitButton(), &QAbstractButton::clicked, this, &InvestTransactionEditor::slotEditInterestSplits); auto tag = new KTagContainer; tag->tagCombo()->setPlaceholderText(i18n("Tag")); tag->tagCombo()->setObjectName(QLatin1String("Tag")); d->m_editWidgets["tag"] = tag; connect(tag->tagCombo(), &QComboBox::editTextChanged, this, &InvestTransactionEditor::slotUpdateButtonState); connect(tag->tagCombo(), &KMyMoneyMVCCombo::createItem, this, &TransactionEditor::createTag); connect(tag->tagCombo(), &KMyMoneyMVCCombo::objectCreation, this, &TransactionEditor::objectCreation); auto memo = new KTextEdit; memo->setTabChangesFocus(true); d->m_editWidgets["memo"] = memo; connect(memo, &QTextEdit::textChanged, this, &InvestTransactionEditor::slotUpdateInvestMemoState); connect(memo, &QTextEdit::textChanged, this, &InvestTransactionEditor::slotUpdateButtonState); d->m_activity->memoText().clear(); d->m_activity->memoChanged() = false; KMyMoneyEdit* value = new KMyMoneyEdit; value->setPlaceholderText(i18n("Shares")); value->setResetButtonVisible(false); d->m_editWidgets["shares"] = value; connect(value, &KMyMoneyEdit::textChanged, this, &InvestTransactionEditor::slotUpdateButtonState); connect(value, &KMyMoneyEdit::valueChanged, this, &InvestTransactionEditor::slotUpdateTotalAmount); value = new KMyMoneyEdit; value->setPlaceholderText(i18n("Price")); value->setResetButtonVisible(false); d->m_editWidgets["price"] = value; connect(value, &KMyMoneyEdit::textChanged, this, &InvestTransactionEditor::slotUpdateButtonState); connect(value, &KMyMoneyEdit::valueChanged, this, &InvestTransactionEditor::slotUpdateTotalAmount); value = new KMyMoneyEdit; // TODO once we have the selected transactions as array of Transaction // we can allow multiple splits for fee and interest value->setResetButtonVisible(false); d->m_editWidgets["fee-amount"] = value; connect(value, &KMyMoneyEdit::textChanged, this, &InvestTransactionEditor::slotUpdateButtonState); connect(value, &KMyMoneyEdit::valueChanged, this, &InvestTransactionEditor::slotUpdateTotalAmount); value = new KMyMoneyEdit; // TODO once we have the selected transactions as array of Transaction // we can allow multiple splits for fee and interest value->setResetButtonVisible(false); d->m_editWidgets["interest-amount"] = value; connect(value, &KMyMoneyEdit::textChanged, this, &InvestTransactionEditor::slotUpdateButtonState); connect(value, &KMyMoneyEdit::valueChanged, this, &InvestTransactionEditor::slotUpdateTotalAmount); auto reconcile = new KMyMoneyReconcileCombo; d->m_editWidgets["status"] = reconcile; connect(reconcile, &KMyMoneyMVCCombo::itemSelected, this, &InvestTransactionEditor::slotUpdateButtonState); KMyMoneyRegister::QWidgetContainer::iterator it_w; for (it_w = d->m_editWidgets.begin(); it_w != d->m_editWidgets.end(); ++it_w) { (*it_w)->installEventFilter(this); } QLabel* label; d->m_editWidgets["activity-label"] = label = new QLabel(i18n("Activity")); label->setAlignment(Qt::AlignVCenter); d->m_editWidgets["postdate-label"] = label = new QLabel(i18n("Date")); label->setAlignment(Qt::AlignVCenter); d->m_editWidgets["security-label"] = label = new QLabel(i18n("Security")); label->setAlignment(Qt::AlignVCenter); d->m_editWidgets["shares-label"] = label = new QLabel(i18n("Shares")); label->setAlignment(Qt::AlignVCenter); d->m_editWidgets["asset-label"] = label = new QLabel(i18n("Account")); label->setAlignment(Qt::AlignVCenter); d->m_editWidgets["price-label"] = label = new QLabel(i18n("Price/share")); label->setAlignment(Qt::AlignVCenter); d->m_editWidgets["fee-label"] = label = new QLabel(i18n("Fees")); label->setAlignment(Qt::AlignVCenter); d->m_editWidgets["fee-amount-label"] = label = new QLabel(""); label->setAlignment(Qt::AlignVCenter); d->m_editWidgets["interest-label"] = label = new QLabel(i18n("Interest")); label->setAlignment(Qt::AlignVCenter); d->m_editWidgets["interest-amount-label"] = label = new QLabel(i18n("Interest")); label->setAlignment(Qt::AlignVCenter); d->m_editWidgets["memo-label"] = label = new QLabel(i18n("Memo")); label->setAlignment(Qt::AlignVCenter); d->m_editWidgets["total"] = label = new QLabel(""); label->setAlignment(Qt::AlignVCenter | Qt::AlignRight); d->m_editWidgets["total-label"] = label = new QLabel(i18nc("Total value", "Total")); label->setAlignment(Qt::AlignVCenter); d->m_editWidgets["status-label"] = label = new QLabel(i18n("Status")); label->setAlignment(Qt::AlignVCenter); // if we don't have more than 1 selected transaction, we don't need // the "don't change" item in some of the combo widgets if (d->m_transactions.count() < 2) { reconcile->removeDontCare(); } } int InvestTransactionEditor::slotEditFeeSplits() { Q_D(InvestTransactionEditor); return d->editSplits("fee-account", "fee-amount", d->m_feeSplits, false, SLOT(slotEditFeeSplits())); } int InvestTransactionEditor::slotEditInterestSplits() { Q_D(InvestTransactionEditor); return d->editSplits("interest-account", "interest-amount", d->m_interestSplits, true, SLOT(slotEditInterestSplits())); } void InvestTransactionEditor::slotCreateSecurity(const QString& name, QString& id) { Q_D(InvestTransactionEditor); MyMoneyAccount acc; QRegExp exp("([^:]+)"); if (exp.indexIn(name) != -1) { acc.setName(exp.cap(1)); emit createSecurity(acc, d->m_account); // return id id = acc.id(); if (!id.isEmpty()) { slotUpdateSecurity(id); } } } void InvestTransactionEditor::slotCreateFeeCategory(const QString& name, QString& id) { MyMoneyAccount acc; acc.setName(name); emit createCategory(acc, MyMoneyFile::instance()->expense()); // return id id = acc.id(); } void InvestTransactionEditor::slotUpdateFeeCategory(const QString& id) { haveWidget("fee-amount")->setDisabled(id.isEmpty()); } void InvestTransactionEditor::slotUpdateFeeVisibility(const QString& txt) { Q_D(InvestTransactionEditor); static const QSet transactionTypesWithoutFee = QSet() << eMyMoney::Split::InvestmentTransactionType::AddShares << eMyMoney::Split::InvestmentTransactionType::RemoveShares << eMyMoney::Split::InvestmentTransactionType::SplitShares; KMyMoneyEdit* feeAmount = dynamic_cast(haveWidget("fee-amount")); feeAmount->setHidden(txt.isEmpty()); QLabel* l = dynamic_cast(haveWidget("fee-amount-label")); KMyMoneyCategory* fee = dynamic_cast(haveWidget("fee-account")); const bool hideFee = txt.isEmpty() || transactionTypesWithoutFee.contains(d->m_activity->type()); // no fee expected so hide if (hideFee) { if (l) { l->setText(""); } feeAmount->hide(); fee->splitButton()->hide(); } else { if (l) { l->setText(i18n("Fee Amount")); } feeAmount->show(); fee->splitButton()->show(); } } void InvestTransactionEditor::slotUpdateInterestCategory(const QString& id) { haveWidget("interest-amount")->setDisabled(id.isEmpty()); } void InvestTransactionEditor::slotUpdateInterestVisibility(const QString& txt) { Q_D(InvestTransactionEditor); static const QSet transactionTypesWithInterest = QSet() << eMyMoney::Split::InvestmentTransactionType::BuyShares << eMyMoney::Split::InvestmentTransactionType::SellShares << eMyMoney::Split::InvestmentTransactionType::Dividend << eMyMoney::Split::InvestmentTransactionType::InterestIncome << eMyMoney::Split::InvestmentTransactionType::Yield; QWidget* w = haveWidget("interest-amount"); w->setHidden(txt.isEmpty()); QLabel* l = dynamic_cast(haveWidget("interest-amount-label")); KMyMoneyCategory* interest = dynamic_cast(haveWidget("interest-account")); const bool showInterest = !txt.isEmpty() && transactionTypesWithInterest.contains(d->m_activity->type()); if (interest && showInterest) { interest->splitButton()->show(); w->show(); if (l) l->setText(i18n("Interest")); } else { if (interest) { interest->splitButton()->hide(); w->hide(); if (l) l->setText(QString()); } } } void InvestTransactionEditor::slotCreateInterestCategory(const QString& name, QString& id) { MyMoneyAccount acc; acc.setName(name); emit createCategory(acc, MyMoneyFile::instance()->income()); id = acc.id(); } void InvestTransactionEditor::slotReloadEditWidgets() { Q_D(InvestTransactionEditor); auto interest = dynamic_cast(haveWidget("interest-account")); auto fees = dynamic_cast(haveWidget("fee-account")); auto security = dynamic_cast(haveWidget("security")); AccountSet aSet; QString id; // interest-account aSet.clear(); aSet.addAccountGroup(Account::Type::Income); aSet.load(interest->selector()); setupCategoryWidget(interest, d->m_interestSplits, id, SLOT(slotEditInterestSplits())); // fee-account aSet.clear(); aSet.addAccountGroup(Account::Type::Expense); aSet.load(fees->selector()); setupCategoryWidget(fees, d->m_feeSplits, id, SLOT(slotEditFeeSplits())); // security aSet.clear(); aSet.load(security->selector(), i18n("Security"), d->m_account.accountList(), true); } void InvestTransactionEditor::loadEditWidgets(eWidgets::eRegister::Action) { loadEditWidgets(); } void InvestTransactionEditor::loadEditWidgets() { Q_D(InvestTransactionEditor); QString id; auto postDate = dynamic_cast(haveWidget("postdate")); auto reconcile = dynamic_cast(haveWidget("status")); auto security = dynamic_cast(haveWidget("security")); auto activity = dynamic_cast(haveWidget("activity")); auto asset = dynamic_cast(haveWidget("asset-account")); auto memo = dynamic_cast(d->m_editWidgets["memo"]); KMyMoneyEdit* value; auto interest = dynamic_cast(haveWidget("interest-account")); auto fees = dynamic_cast(haveWidget("fee-account")); // check if the current transaction has a reference to an equity account auto haveEquityAccount = false; foreach (const auto split, d->m_transaction.splits()) { auto acc = MyMoneyFile::instance()->account(split.accountId()); if (acc.accountType() == Account::Type::Equity) { haveEquityAccount = true; break; } } // asset-account AccountSet aSet; aSet.clear(); aSet.addAccountType(Account::Type::Checkings); aSet.addAccountType(Account::Type::Savings); aSet.addAccountType(Account::Type::Cash); aSet.addAccountType(Account::Type::Asset); aSet.addAccountType(Account::Type::Currency); aSet.addAccountType(Account::Type::CreditCard); if (KMyMoneyGlobalSettings::expertMode() || haveEquityAccount) aSet.addAccountGroup(Account::Type::Equity); aSet.load(asset->selector()); // security security->setSuppressObjectCreation(false); // allow object creation on the fly aSet.clear(); aSet.load(security->selector(), i18n("Security"), d->m_account.accountList(), true); // memo memo->setText(d->m_split.memo()); d->m_activity->memoText() = d->m_split.memo(); d->m_activity->memoChanged() = false; if (!isMultiSelection()) { // date if (d->m_transaction.postDate().isValid()) postDate->setDate(d->m_transaction.postDate()); else if (d->m_lastPostDate.isValid()) postDate->setDate(d->m_lastPostDate); else postDate->setDate(QDate::currentDate()); // security (but only if it's not the investment account) if (d->m_split.accountId() != d->m_account.id()) { security->completion()->setSelected(d->m_split.accountId()); security->slotItemSelected(d->m_split.accountId()); } // activity activity->setActivity(d->m_activity->type()); slotUpdateActivity(activity->activity()); asset->completion()->setSelected(d->m_assetAccountSplit.accountId()); asset->slotItemSelected(d->m_assetAccountSplit.accountId()); // interest-account aSet.clear(); aSet.addAccountGroup(Account::Type::Income); aSet.load(interest->selector()); setupCategoryWidget(interest, d->m_interestSplits, id, SLOT(slotEditInterestSplits())); slotUpdateInterestVisibility(interest->currentText()); // fee-account aSet.clear(); aSet.addAccountGroup(Account::Type::Expense); aSet.load(fees->selector()); setupCategoryWidget(fees, d->m_feeSplits, id, SLOT(slotEditFeeSplits())); slotUpdateFeeVisibility(fees->currentText()); // shares // don't set the value if the number of shares is zero so that // we can see the hint value = dynamic_cast(haveWidget("shares")); if (typeid(*(d->m_activity)) != typeid(Invest::Split(this))) value->setPrecision(MyMoneyMoney::denomToPrec(d->m_security.smallestAccountFraction())); else value->setPrecision(-1); if (!d->m_split.shares().isZero()) value->setValue(d->m_split.shares().abs()); // price d->updatePriceMode(d->m_split); // fee amount value = dynamic_cast(haveWidget("fee-amount")); value->setValue(d->subtotal(d->m_feeSplits)); // interest amount value = dynamic_cast(haveWidget("interest-amount")); value->setValue(-d->subtotal(d->m_interestSplits)); // total slotUpdateTotalAmount(); // status if (d->m_split.reconcileFlag() == eMyMoney::Split::State::Unknown) d->m_split.setReconcileFlag(eMyMoney::Split::State::NotReconciled); reconcile->setState(d->m_split.reconcileFlag()); } else { postDate->loadDate(QDate()); reconcile->setState(eMyMoney::Split::State::Unknown); // We don't allow to change the activity activity->setActivity(d->m_activity->type()); slotUpdateActivity(activity->activity()); activity->setDisabled(true); // scan the list of selected transactions and check that they have // the same activity. KMyMoneyRegister::SelectedTransactions::iterator it_t = d->m_transactions.begin(); const QString& action = d->m_item->split().action(); bool isNegative = d->m_item->split().shares().isNegative(); bool allSameActivity = true; for (it_t = d->m_transactions.begin(); allSameActivity && (it_t != d->m_transactions.end()); ++it_t) { allSameActivity = (action == (*it_t).split().action() && (*it_t).split().shares().isNegative() == isNegative); } QStringList fields; fields << "shares" << "price" << "fee-amount" << "interest-amount"; QStringList::const_iterator it_f; for (it_f = fields.constBegin(); it_f != fields.constEnd(); ++it_f) { value = dynamic_cast(haveWidget((*it_f))); value->setText(""); value->setAllowEmpty(); } // if we have transactions with different activities, disable some more widgets if (!allSameActivity) { fields << "asset-account" << "fee-account" << "interest-account"; QStringList::const_iterator it_f; for (it_f = fields.constBegin(); it_f != fields.constEnd(); ++it_f) { haveWidget(*it_f)->setDisabled(true); } } } } QWidget* InvestTransactionEditor::firstWidget() const { return nullptr; // let the creator use the first widget in the tab order } bool InvestTransactionEditor::isComplete(QString& reason) const { Q_D(const InvestTransactionEditor); reason.clear(); return d->m_activity->isComplete(reason); } void InvestTransactionEditor::slotUpdateSecurity(const QString& stockId) { Q_D(InvestTransactionEditor); auto file = MyMoneyFile::instance(); MyMoneyAccount stock = file->account(stockId); d->m_security = file->security(stock.currencyId()); d->m_currency = file->security(d->m_security.tradingCurrency()); bool currencyKnown = !d->m_currency.id().isEmpty(); if (!currencyKnown) { d->m_currency.setTradingSymbol("???"); } else { if (typeid(*(d->m_activity)) != typeid(Invest::Split(this))) { dynamic_cast(haveWidget("shares"))->setPrecision(MyMoneyMoney::denomToPrec(d->m_security.smallestAccountFraction())); } else { dynamic_cast(haveWidget("shares"))->setPrecision(-1); } } d->updatePriceMode(); d->m_activity->preloadAssetAccount(); haveWidget("shares")->setEnabled(currencyKnown); haveWidget("price")->setEnabled(currencyKnown); haveWidget("fee-amount")->setEnabled(currencyKnown); haveWidget("interest-amount")->setEnabled(currencyKnown); slotUpdateTotalAmount(); slotUpdateButtonState(); resizeForm(); } bool InvestTransactionEditor::fixTransactionCommodity(const MyMoneyAccount& /* account */) { return true; } void InvestTransactionEditor::totalAmount(MyMoneyMoney& amount) const { auto activityCombo = dynamic_cast(haveWidget("activity")); auto sharesEdit = dynamic_cast(haveWidget("shares")); auto priceEdit = dynamic_cast(haveWidget("price")); auto feesEdit = dynamic_cast(haveWidget("fee-amount")); auto interestEdit = dynamic_cast(haveWidget("interest-amount")); if (priceMode() == eDialogs::PriceMode::PricePerTransaction) amount = priceEdit->value().abs(); else amount = sharesEdit->value().abs() * priceEdit->value().abs(); if (feesEdit->isVisible()) { MyMoneyMoney fee = feesEdit->value(); MyMoneyMoney factor(-1, 1); switch (activityCombo->activity()) { case eMyMoney::Split::InvestmentTransactionType::BuyShares: case eMyMoney::Split::InvestmentTransactionType::ReinvestDividend: factor = MyMoneyMoney::ONE; break; default: break; } amount += (fee * factor); } if (interestEdit->isVisible()) { MyMoneyMoney interest = interestEdit->value(); MyMoneyMoney factor(1, 1); switch (activityCombo->activity()) { case eMyMoney::Split::InvestmentTransactionType::BuyShares: factor = MyMoneyMoney::MINUS_ONE; break; default: break; } amount += (interest * factor); } } void InvestTransactionEditor::slotUpdateTotalAmount() { Q_D(InvestTransactionEditor); QLabel* total = dynamic_cast(haveWidget("total")); if (total && total->isVisible()) { MyMoneyMoney amount; totalAmount(amount); - total->setText(amount.convert(d->m_currency.smallestAccountFraction(), static_cast(d->m_security.roundingMethod())) + total->setText(amount.convert(d->m_currency.smallestAccountFraction(), d->m_security.roundingMethod()) .formatMoney(d->m_currency.tradingSymbol(), MyMoneyMoney::denomToPrec(d->m_currency.smallestAccountFraction()))); } } void InvestTransactionEditor::slotTransactionContainerGeometriesUpdated() { Q_D(InvestTransactionEditor); // when the geometries of the transaction container are updated some edit widgets that were // previously hidden are being shown (see QAbstractItemView::updateEditorGeometries) so we // need to update the activity with the current activity in order to show only the widgets // which are needed by the current activity if (d->m_editWidgets.isEmpty()) return; slotUpdateActivity(d->m_activity->type()); } void InvestTransactionEditor::slotUpdateActivity(eMyMoney::Split::InvestmentTransactionType activity) { Q_D(InvestTransactionEditor); // create new activity object if required d->activityFactory(activity); // hide all dynamic widgets d->hideCategory("interest-account"); d->hideCategory("fee-account"); QStringList dynwidgets; dynwidgets << "total-label" << "asset-label" << "fee-label" << "fee-amount-label" << "interest-label" << "interest-amount-label" << "price-label" << "shares-label"; // hiding labels works by clearing them. hide() does not do the job // as the underlying text in the QTable object will shine through QStringList::const_iterator it_s; for (it_s = dynwidgets.constBegin(); it_s != dynwidgets.constEnd(); ++it_s) { QLabel* w = dynamic_cast(haveWidget(*it_s)); if (w) w->setText(" "); } // real widgets can be hidden dynwidgets.clear(); dynwidgets << "asset-account" << "interest-amount" << "fee-amount" << "shares" << "price" << "total"; for (it_s = dynwidgets.constBegin(); it_s != dynwidgets.constEnd(); ++it_s) { QWidget* w = haveWidget(*it_s); if (w) w->hide(); } d->m_activity->showWidgets(); d->m_activity->preloadAssetAccount(); if (KMyMoneyCategory* cat = dynamic_cast(haveWidget("interest-account"))) { if (cat->parentWidget()->isVisible()) slotUpdateInterestVisibility(cat->currentText()); else cat->splitButton()->hide(); } if (KMyMoneyCategory* cat = dynamic_cast(haveWidget("fee-account"))) { if (cat->parentWidget()->isVisible()) slotUpdateFeeVisibility(cat->currentText()); else cat->splitButton()->hide(); } } eDialogs::PriceMode InvestTransactionEditor::priceMode() const { Q_D(const InvestTransactionEditor); eDialogs::PriceMode mode = static_cast(eDialogs::PriceMode::Price); KMyMoneySecurity* sec = dynamic_cast(d->m_editWidgets["security"]); QString accId; if (!sec->currentText().isEmpty()) { accId = sec->selectedItem(); if (accId.isEmpty()) accId = d->m_account.id(); } while (!accId.isEmpty() && mode == eDialogs::PriceMode::Price) { auto acc = MyMoneyFile::instance()->account(accId); if (acc.value("priceMode").isEmpty()) accId = acc.parentAccountId(); else mode = static_cast(acc.value("priceMode").toInt()); } // if mode is still then use that if (mode == eDialogs::PriceMode::Price) mode = eDialogs::PriceMode::PricePerShare; return mode; } MyMoneySecurity InvestTransactionEditor::security() const { Q_D(const InvestTransactionEditor); return d->m_security; } QList InvestTransactionEditor::feeSplits() const { Q_D(const InvestTransactionEditor); return d->m_feeSplits; } QList InvestTransactionEditor::interestSplits() const { Q_D(const InvestTransactionEditor); return d->m_interestSplits; } bool InvestTransactionEditor::setupPrice(const MyMoneyTransaction& t, MyMoneySplit& split) { Q_D(InvestTransactionEditor); auto file = MyMoneyFile::instance(); auto acc = file->account(split.accountId()); MyMoneySecurity toCurrency(file->security(acc.currencyId())); int fract = acc.fraction(); if (acc.currencyId() != t.commodity()) { if (acc.currencyId().isEmpty()) acc.setCurrencyId(t.commodity()); QMap::Iterator it_p; QString key = t.commodity() + '-' + acc.currencyId(); it_p = d->m_priceInfo.find(key); // if it's not found, then collect it from the user first MyMoneyMoney price; if (it_p == d->m_priceInfo.end()) { MyMoneySecurity fromCurrency = file->security(t.commodity()); MyMoneyMoney fromValue, toValue; fromValue = split.value(); const MyMoneyPrice &priceInfo = MyMoneyFile::instance()->price(fromCurrency.id(), toCurrency.id(), t.postDate()); toValue = split.value() * priceInfo.rate(toCurrency.id()); QPointer calc = new KCurrencyCalculator(fromCurrency, toCurrency, fromValue, toValue, t.postDate(), fract, d->m_regForm); if (calc->exec() == QDialog::Rejected) { delete calc; return false; } price = calc->price(); delete calc; d->m_priceInfo[key] = price; } else { price = (*it_p); } // update shares if the transaction commodity is the currency // of the current selected account split.setShares(split.value() * price); } else { split.setShares(split.value()); } return true; } bool InvestTransactionEditor::createTransaction(MyMoneyTransaction& t, const MyMoneyTransaction& torig, const MyMoneySplit& sorig, bool /* skipPriceDialog */) { Q_D(InvestTransactionEditor); auto file = MyMoneyFile::instance(); // we start with the previous values, make sure we can add them later on t = torig; MyMoneySplit s0 = sorig; s0.clearId(); KMyMoneySecurity* sec = dynamic_cast(d->m_editWidgets["security"]); if (!isMultiSelection() || (isMultiSelection() && !sec->currentText().isEmpty())) { QString securityId = sec->selectedItem(); if (!securityId.isEmpty()) { s0.setAccountId(securityId); MyMoneyAccount stockAccount = file->account(securityId); QString currencyId = stockAccount.currencyId(); MyMoneySecurity security = file->security(currencyId); t.setCommodity(security.tradingCurrency()); } else { s0.setAccountId(d->m_account.id()); t.setCommodity(d->m_account.currencyId()); } } // extract price info from original transaction d->m_priceInfo.clear(); if (!torig.id().isEmpty()) { foreach (const auto split, torig.splits()) { if (split.id() != sorig.id()) { auto cat = file->account(split.accountId()); if (cat.currencyId() != d->m_account.currencyId()) { if (cat.currencyId().isEmpty()) cat.setCurrencyId(d->m_account.currencyId()); if (!split.shares().isZero() && !split.value().isZero()) { d->m_priceInfo[cat.currencyId()] = (split.shares() / split.value()).reduce(); } } } } } t.removeSplits(); KMyMoneyDateInput* postDate = dynamic_cast(d->m_editWidgets["postdate"]); if (postDate->date().isValid()) { t.setPostDate(postDate->date()); } // memo and number field are special: if we have multiple transactions selected // and the edit field is empty, we treat it as "not modified". // FIXME a better approach would be to have a 'dirty' flag with the widgets // which identifies if the originally loaded value has been modified // by the user KTextEdit* memo = dynamic_cast(d->m_editWidgets["memo"]); if (memo) { if (!isMultiSelection() || (isMultiSelection() && d->m_activity->memoChanged())) s0.setMemo(memo->toPlainText()); } MyMoneySplit assetAccountSplit; QList feeSplits; QList interestSplits; MyMoneySecurity security, currency; eMyMoney::Split::InvestmentTransactionType transactionType; // extract the splits from the original transaction KMyMoneyUtils::dissectTransaction(torig, sorig, assetAccountSplit, feeSplits, interestSplits, security, currency, transactionType); // check if the trading currency is the same if the security has changed // in case it differs, check that we have a price (request from user) // and convert all splits // TODO // do the conversions here // TODO // keep the current activity object and create a new one // that can be destroyed later on auto activity = d->m_activity; d->m_activity = 0; // make sure we create a new one d->activityFactory(activity->type()); // if the activity is not set in the combo widget, we keep // the one which is used in the original transaction auto activityCombo = dynamic_cast(haveWidget("activity")); if (activityCombo->activity() == eMyMoney::Split::InvestmentTransactionType::UnknownTransactionType) { d->activityFactory(transactionType); } // if we mark the split reconciled here, we'll use today's date if no reconciliation date is given auto status = dynamic_cast(d->m_editWidgets["status"]); if (status->state() != eMyMoney::Split::State::Unknown) s0.setReconcileFlag(status->state()); if (s0.reconcileFlag() == eMyMoney::Split::State::Reconciled && !s0.reconcileDate().isValid()) s0.setReconcileDate(QDate::currentDate()); // call the creation logic for the current selected activity bool rc = d->m_activity->createTransaction(t, s0, assetAccountSplit, feeSplits, d->m_feeSplits, interestSplits, d->m_interestSplits, security, currency); // now switch back to the original activity delete d->m_activity; d->m_activity = activity; // add the splits to the transaction if (rc) { if (security.name().isEmpty()) // new transaction has no security filled... security = file->security(file->account(s0.accountId()).currencyId()); // ...so fetch it from s0 split QList resultSplits; // concatenates splits for easy processing if (!assetAccountSplit.accountId().isEmpty()) resultSplits.append(assetAccountSplit); if (!feeSplits.isEmpty()) resultSplits.append(feeSplits); if (!interestSplits.isEmpty()) resultSplits.append(interestSplits); AlkValue::RoundingMethod roundingMethod = AlkValue::RoundRound; if (security.roundingMethod() != AlkValue::RoundNever) roundingMethod = security.roundingMethod(); int currencyFraction = currency.smallestAccountFraction(); int securityFraction = security.smallestAccountFraction(); // assuming that all non-stock splits are monetary foreach (auto split, resultSplits) { split.clearId(); split.setShares(MyMoneyMoney(split.shares().convertDenominator(currencyFraction, roundingMethod))); split.setValue(MyMoneyMoney(split.value().convertDenominator(currencyFraction, roundingMethod))); t.addSplit(split); } s0.setShares(MyMoneyMoney(s0.shares().convertDenominator(securityFraction, roundingMethod))); // only shares variable from stock split isn't evaluated in currency s0.setValue(MyMoneyMoney(s0.value().convertDenominator(currencyFraction, roundingMethod))); t.addSplit(s0); } return rc; } void InvestTransactionEditor::setupFinalWidgets() { addFinalWidget(haveWidget("memo")); } void InvestTransactionEditor::slotUpdateInvestMemoState() { Q_D(InvestTransactionEditor); auto memo = dynamic_cast(d->m_editWidgets["memo"]); if (memo) { d->m_activity->memoChanged() = (memo->toPlainText() != d->m_activity->memoText()); } } diff --git a/kmymoney/dialogs/kcurrencycalculator.cpp b/kmymoney/dialogs/kcurrencycalculator.cpp index 06d1f9f38..fdc1025be 100644 --- a/kmymoney/dialogs/kcurrencycalculator.cpp +++ b/kmymoney/dialogs/kcurrencycalculator.cpp @@ -1,385 +1,386 @@ /*************************************************************************** kcurrencycalculator.cpp - description ------------------- begin : Thu Apr 8 2004 copyright : (C) 2000-2004 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 "kcurrencycalculator.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include // ---------------------------------------------------------------------------- // Project Includes #include "ui_kcurrencycalculator.h" #include "mymoneyfile.h" #include "mymoneyaccount.h" #include "mymoneysecurity.h" #include "kmymoneyedit.h" #include "kmymoneydateinput.h" #include "mymoneyprice.h" #include "mymoneymoney.h" +#include "mymoneyexception.h" #include "mymoneysplit.h" #include "mymoneytransaction.h" #include "kmymoneyglobalsettings.h" class KCurrencyCalculatorPrivate { Q_DISABLE_COPY(KCurrencyCalculatorPrivate) Q_DECLARE_PUBLIC(KCurrencyCalculator) public: explicit KCurrencyCalculatorPrivate(KCurrencyCalculator *qq, const MyMoneySecurity& from, const MyMoneySecurity& to, const MyMoneyMoney& value, const MyMoneyMoney& shares, const QDate& date, const signed64 resultFraction) : q_ptr(qq), ui(new Ui::KCurrencyCalculator), m_fromCurrency(from), m_toCurrency(to), m_result(shares.abs()), m_value(value.abs()), m_date(date), m_resultFraction(resultFraction) { } ~KCurrencyCalculatorPrivate() { delete ui; } void init() { Q_Q(KCurrencyCalculator); ui->setupUi(q); auto file = MyMoneyFile::instance(); //set main widget of QDialog ui->buttonGroup1->setId(ui->m_amountButton, 0); ui->buttonGroup1->setId(ui->m_rateButton, 1); ui->m_dateFrame->hide(); if (m_date.isValid()) ui->m_dateEdit->setDate(m_date); else ui->m_dateEdit->setDate(QDate::currentDate()); ui->m_fromCurrencyText->setText(QString(MyMoneySecurity::securityTypeToString(m_fromCurrency.securityType()) + ' ' + (m_fromCurrency.isCurrency() ? m_fromCurrency.id() : m_fromCurrency.tradingSymbol()))); ui->m_toCurrencyText->setText(QString(MyMoneySecurity::securityTypeToString(m_toCurrency.securityType()) + ' ' + (m_toCurrency.isCurrency() ? m_toCurrency.id() : m_toCurrency.tradingSymbol()))); //set bold font auto boldFont = ui->m_fromCurrencyText->font(); boldFont.setBold(true); ui->m_fromCurrencyText->setFont(boldFont); boldFont = ui->m_toCurrencyText->font(); boldFont.setBold(true); ui->m_toCurrencyText->setFont(boldFont); ui->m_fromAmount->setText(m_value.formatMoney(QString(), MyMoneyMoney::denomToPrec(m_fromCurrency.smallestAccountFraction()))); ui->m_dateText->setText(QLocale().toString(m_date)); ui->m_updateButton->setChecked(KMyMoneyGlobalSettings::priceHistoryUpdate()); // setup initial result if (m_result == MyMoneyMoney() && !m_value.isZero()) { const MyMoneyPrice &pr = file->price(m_fromCurrency.id(), m_toCurrency.id(), m_date); if (pr.isValid()) { m_result = m_value * pr.rate(m_toCurrency.id()); } } // fill in initial values ui->m_toAmount->loadText(m_result.formatMoney(QString(), MyMoneyMoney::denomToPrec(m_resultFraction))); ui->m_toAmount->setPrecision(MyMoneyMoney::denomToPrec(m_resultFraction)); ui->m_conversionRate->setPrecision(m_fromCurrency.pricePrecision()); q->connect(ui->m_amountButton, &QAbstractButton::clicked, q, &KCurrencyCalculator::slotSetToAmount); q->connect(ui->m_rateButton, &QAbstractButton::clicked, q, &KCurrencyCalculator::slotSetExchangeRate); q->connect(ui->m_toAmount, &KMyMoneyEdit::valueChanged, q, &KCurrencyCalculator::slotUpdateResult); q->connect(ui->m_conversionRate, &KMyMoneyEdit::valueChanged, q, &KCurrencyCalculator::slotUpdateRate); // use this as the default ui->m_amountButton->animateClick(); q->slotUpdateResult(ui->m_toAmount->text()); // If the from security is not a currency, we only allow entering a price if (!m_fromCurrency.isCurrency()) { ui->m_rateButton->animateClick(); ui->m_amountButton->hide(); ui->m_toAmount->hide(); } } void updateExample(const MyMoneyMoney& price) { QString msg; if (price.isZero()) { msg = QString("1 %1 = ? %2").arg(m_fromCurrency.tradingSymbol()) .arg(m_toCurrency.tradingSymbol()); if (m_fromCurrency.isCurrency()) { msg += QString("\n"); msg += QString("1 %1 = ? %2").arg(m_toCurrency.tradingSymbol()) .arg(m_fromCurrency.tradingSymbol()); } } else { msg = QString("1 %1 = %2 %3").arg(m_fromCurrency.tradingSymbol()) .arg(price.formatMoney(QString(), m_fromCurrency.pricePrecision())) .arg(m_toCurrency.tradingSymbol()); if (m_fromCurrency.isCurrency()) { msg += QString("\n"); msg += QString("1 %1 = %2 %3").arg(m_toCurrency.tradingSymbol()) .arg((MyMoneyMoney::ONE / price).formatMoney(QString(), m_toCurrency.pricePrecision())) .arg(m_fromCurrency.tradingSymbol()); } } ui->m_conversionExample->setText(msg); ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(!price.isZero()); } KCurrencyCalculator *q_ptr; Ui::KCurrencyCalculator *ui; MyMoneySecurity m_fromCurrency; MyMoneySecurity m_toCurrency; MyMoneyMoney m_result; MyMoneyMoney m_value; QDate m_date; signed64 m_resultFraction; }; KCurrencyCalculator::KCurrencyCalculator(const MyMoneySecurity& from, const MyMoneySecurity& to, const MyMoneyMoney& value, const MyMoneyMoney& shares, const QDate& date, const signed64 resultFraction, QWidget *parent) : QDialog(parent), d_ptr(new KCurrencyCalculatorPrivate(this, from, to, value, shares, date, resultFraction)) { Q_D(KCurrencyCalculator); d->init(); } KCurrencyCalculator::~KCurrencyCalculator() { Q_D(KCurrencyCalculator); delete d; } bool KCurrencyCalculator::setupSplitPrice(MyMoneyMoney& shares, const MyMoneyTransaction& t, const MyMoneySplit& s, const QMap& priceInfo, QWidget* parentWidget) { auto rc = true; auto file = MyMoneyFile::instance(); if (!s.value().isZero()) { auto cat = file->account(s.accountId()); MyMoneySecurity toCurrency; toCurrency = file->security(cat.currencyId()); // determine the fraction required for this category/account int fract = cat.fraction(toCurrency); if (cat.currencyId() != t.commodity()) { MyMoneyMoney toValue; auto fromCurrency = file->security(t.commodity()); // display only positive values to the user auto fromValue = s.value().abs(); // if we had a price info in the beginning, we use it here if (priceInfo.find(cat.currencyId()) != priceInfo.end()) { toValue = (fromValue * priceInfo[cat.currencyId()]).convert(fract); } // if the shares are still 0, we need to change that if (toValue.isZero()) { const MyMoneyPrice &price = file->price(fromCurrency.id(), toCurrency.id(), t.postDate()); // if the price is valid calculate the shares. If it is invalid // assume a conversion rate of 1.0 if (price.isValid()) { toValue = (price.rate(toCurrency.id()) * fromValue).convert(fract); } else { toValue = fromValue; } } // now present all that to the user QPointer calc = new KCurrencyCalculator(fromCurrency, toCurrency, fromValue, toValue, t.postDate(), fract, parentWidget); if (calc->exec() == QDialog::Rejected) { rc = false; } else shares = (s.value() * calc->price()).convert(fract); delete calc; } else { shares = s.value().convert(fract); } } else shares = s.value(); return rc; } void KCurrencyCalculator::setupPriceEditor() { Q_D(KCurrencyCalculator); d->ui->m_dateFrame->show(); d->ui->m_amountDateFrame->hide(); d->ui->m_updateButton->setChecked(true); d->ui->m_updateButton->hide(); } void KCurrencyCalculator::slotSetToAmount() { Q_D(KCurrencyCalculator); d->ui->m_rateButton->setChecked(false); d->ui->m_toAmount->setEnabled(true); d->ui->m_conversionRate->setEnabled(false); } void KCurrencyCalculator::slotSetExchangeRate() { Q_D(KCurrencyCalculator); d->ui->m_amountButton->setChecked(false); d->ui->m_toAmount->setEnabled(false); d->ui->m_conversionRate->setEnabled(true); } void KCurrencyCalculator::slotUpdateResult(const QString& /*txt*/) { Q_D(KCurrencyCalculator); MyMoneyMoney result = d->ui->m_toAmount->value(); MyMoneyMoney price(0, 1); if (result.isNegative()) { d->ui->m_toAmount->setValue(-result); slotUpdateResult(QString()); return; } if (!result.isZero()) { price = result / d->m_value; d->ui->m_conversionRate->loadText(price.formatMoney(QString(), d->m_fromCurrency.pricePrecision())); d->m_result = (d->m_value * price).convert(d->m_resultFraction); d->ui->m_toAmount->loadText(d->m_result.formatMoney(d->m_resultFraction)); } d->updateExample(price); } void KCurrencyCalculator::slotUpdateRate(const QString& /*txt*/) { Q_D(KCurrencyCalculator); auto price = d->ui->m_conversionRate->value(); if (price.isNegative()) { d->ui->m_conversionRate->setValue(-price); slotUpdateRate(QString()); return; } if (!price.isZero()) { d->ui->m_conversionRate->loadText(price.formatMoney(QString(), d->m_fromCurrency.pricePrecision())); d->m_result = (d->m_value * price).convert(d->m_resultFraction); d->ui->m_toAmount->loadText(d->m_result.formatMoney(QString(), MyMoneyMoney::denomToPrec(d->m_resultFraction))); } d->updateExample(price); } void KCurrencyCalculator::accept() { Q_D(KCurrencyCalculator); if (d->ui->m_conversionRate->isEnabled()) slotUpdateRate(QString()); else slotUpdateResult(QString()); if (d->ui->m_updateButton->isChecked()) { auto pr = MyMoneyFile::instance()->price(d->m_fromCurrency.id(), d->m_toCurrency.id(), d->ui->m_dateEdit->date()); if (!pr.isValid() || pr.date() != d->ui->m_dateEdit->date() || (pr.date() == d->ui->m_dateEdit->date() && pr.rate(d->m_fromCurrency.id()) != price())) { pr = MyMoneyPrice(d->m_fromCurrency.id(), d->m_toCurrency.id(), d->ui->m_dateEdit->date(), price(), i18n("User")); MyMoneyFileTransaction ft; try { MyMoneyFile::instance()->addPrice(pr); ft.commit(); } catch (const MyMoneyException &) { qDebug("Cannot add price"); } } } // remember setting for next round KMyMoneyGlobalSettings::setPriceHistoryUpdate(d->ui->m_updateButton->isChecked()); QDialog::accept(); } MyMoneyMoney KCurrencyCalculator::price() const { Q_D(const KCurrencyCalculator); // This should fix https://bugs.kde.org/show_bug.cgi?id=205254 and // https://bugs.kde.org/show_bug.cgi?id=325953 as well as // https://bugs.kde.org/show_bug.cgi?id=300965 if (d->ui->m_amountButton->isChecked()) return d->ui->m_toAmount->value().abs() / d->m_value.abs(); else return d->ui->m_conversionRate->value(); } diff --git a/kmymoney/dialogs/kmymoneypricedlg.cpp b/kmymoney/dialogs/kmymoneypricedlg.cpp index 049d02606..c9b9718d3 100644 --- a/kmymoney/dialogs/kmymoneypricedlg.cpp +++ b/kmymoney/dialogs/kmymoneypricedlg.cpp @@ -1,351 +1,352 @@ /*************************************************************************** kmymoneypricedlg.cpp ------------------- begin : Wed Nov 24 2004 copyright : (C) 2000-2004 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 "kmymoneypricedlg.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "ui_kmymoneypricedlg.h" #include "ui_kupdatestockpricedlg.h" #include "kupdatestockpricedlg.h" #include "kcurrencycalculator.h" #include "mymoneyprice.h" #include "kequitypriceupdatedlg.h" #include "kmymoneycurrencyselector.h" #include "mymoneyfile.h" #include "mymoneyaccount.h" #include "mymoneysecurity.h" #include "mymoneymoney.h" +#include "mymoneyexception.h" #include "kmymoneyutils.h" #include "kpricetreeitem.h" #include "icons/icons.h" using namespace Icons; class KMyMoneyPriceDlgPrivate { Q_DISABLE_COPY(KMyMoneyPriceDlgPrivate) Q_DECLARE_PUBLIC(KMyMoneyPriceDlg) public: explicit KMyMoneyPriceDlgPrivate(KMyMoneyPriceDlg *qq) : q_ptr(qq), ui(new Ui::KMyMoneyPriceDlg), m_searchWidget(nullptr) { } ~KMyMoneyPriceDlgPrivate() { delete ui; } KMyMoneyPriceDlg *q_ptr; Ui::KMyMoneyPriceDlg *ui; QTreeWidgetItem* m_currentItem; /** * Search widget for the list */ KTreeWidgetSearchLineWidget* m_searchWidget; QMap m_stockNameMap; }; KMyMoneyPriceDlg::KMyMoneyPriceDlg(QWidget* parent) : QDialog(parent), d_ptr(new KMyMoneyPriceDlgPrivate(this)) { Q_D(KMyMoneyPriceDlg); d->ui->setupUi(this); // create the searchline widget // and insert it into the existing layout d->m_searchWidget = new KTreeWidgetSearchLineWidget(this, d->ui->m_priceList); d->m_searchWidget->setSizePolicy(QSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed)); d->ui->m_listLayout->insertWidget(0, d->m_searchWidget); d->ui->m_priceList->header()->setSortIndicator(0, Qt::AscendingOrder); d->ui->m_priceList->header()->setStretchLastSection(true); d->ui->m_priceList->setContextMenuPolicy(Qt::CustomContextMenu); d->ui->m_deleteButton->setIcon(QIcon::fromTheme(g_Icons[Icon::EditDelete])); d->ui->m_newButton->setIcon(QIcon::fromTheme(g_Icons[Icon::DocumentNew])); d->ui->m_editButton->setIcon(QIcon::fromTheme(g_Icons[Icon::DocumentEdit])); d->ui->m_onlineQuoteButton->setIcon(KMyMoneyUtils::overlayIcon(g_Icons[Icon::ViewInvestment], g_Icons[Icon::Download])); connect(d->ui->m_editButton, &QAbstractButton::clicked, this, &KMyMoneyPriceDlg::slotEditPrice); connect(d->ui->m_deleteButton, &QAbstractButton::clicked, this, &KMyMoneyPriceDlg::slotDeletePrice); connect(d->ui->m_newButton, &QAbstractButton::clicked, this, &KMyMoneyPriceDlg::slotNewPrice); connect(d->ui->m_priceList, &QTreeWidget::itemSelectionChanged, this, &KMyMoneyPriceDlg::slotSelectPrice); connect(d->ui->m_onlineQuoteButton, &QAbstractButton::clicked, this, &KMyMoneyPriceDlg::slotOnlinePriceUpdate); connect(d->ui->m_priceList, &QWidget::customContextMenuRequested, this, &KMyMoneyPriceDlg::slotOpenContextMenu); connect(d->ui->m_showAllPrices, &QAbstractButton::toggled, this, &KMyMoneyPriceDlg::slotLoadWidgets); connect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, this, &KMyMoneyPriceDlg::slotLoadWidgets); slotLoadWidgets(); slotSelectPrice(); } KMyMoneyPriceDlg::~KMyMoneyPriceDlg() { Q_D(KMyMoneyPriceDlg); delete d; } void KMyMoneyPriceDlg::slotLoadWidgets() { Q_D(KMyMoneyPriceDlg); auto file = MyMoneyFile::instance(); //clear the list and disable the sorting while it loads the widgets, for performance d->ui->m_priceList->setSortingEnabled(false); d->ui->m_priceList->clear(); d->m_stockNameMap.clear(); //load the currencies for investments, which we'll need later QList accList; file->accountList(accList); QList::const_iterator acc_it; for (acc_it = accList.constBegin(); acc_it != accList.constEnd(); ++acc_it) { if ((*acc_it).isInvest()) { if (d->m_stockNameMap.contains((*acc_it).currencyId())) { d->m_stockNameMap[(*acc_it).currencyId()] = QString(d->m_stockNameMap.value((*acc_it).currencyId()) + ", " + (*acc_it).name()); } else { d->m_stockNameMap[(*acc_it).currencyId()] = (*acc_it).name(); } } } //get the price list MyMoneyPriceList list = file->priceList(); MyMoneyPriceList::ConstIterator it_allPrices; for (it_allPrices = list.constBegin(); it_allPrices != list.constEnd(); ++it_allPrices) { MyMoneyPriceEntries::ConstIterator it_priceItem; if (d->ui->m_showAllPrices->isChecked()) { for (it_priceItem = (*it_allPrices).constBegin(); it_priceItem != (*it_allPrices).constEnd(); ++it_priceItem) { loadPriceItem(*it_priceItem); } } else { //if it doesn't show all prices, it only shows the most recent occurrence for each price if ((*it_allPrices).count() > 0) { //the prices for each currency are ordered by date in ascending order //it gets the last item of the item, which is supposed to be the most recent price it_priceItem = (*it_allPrices).constEnd(); --it_priceItem; loadPriceItem(*it_priceItem); } } } //reenable sorting and sort by the commodity column d->ui->m_priceList->setSortingEnabled(true); d->ui->m_priceList->sortByColumn(KPriceTreeItem::ePriceCommodity); //update the search widget so the list gets refreshed correctly if it was being filtered if (!d->m_searchWidget->searchLine()->text().isEmpty()) d->m_searchWidget->searchLine()->updateSearch(d->m_searchWidget->searchLine()->text()); } QTreeWidgetItem* KMyMoneyPriceDlg::loadPriceItem(const MyMoneyPrice& basePrice) { Q_D(KMyMoneyPriceDlg); MyMoneySecurity from, to; auto price = MyMoneyPrice(basePrice); auto priceTreeItem = new KPriceTreeItem(d->ui->m_priceList); if (!price.isValid()) price = MyMoneyFile::instance()->price(price.from(), price.to(), price.date()); if (price.isValid()) { QString priceBase = price.to(); from = MyMoneyFile::instance()->security(price.from()); to = MyMoneyFile::instance()->security(price.to()); if (!to.isCurrency()) { from = MyMoneyFile::instance()->security(price.to()); to = MyMoneyFile::instance()->security(price.from()); priceBase = price.from(); } priceTreeItem->setData(KPriceTreeItem::ePriceCommodity, Qt::UserRole, QVariant::fromValue(price)); priceTreeItem->setText(KPriceTreeItem::ePriceCommodity, (from.isCurrency()) ? from.id() : from.tradingSymbol()); priceTreeItem->setText(KPriceTreeItem::ePriceStockName, (from.isCurrency()) ? QString() : d->m_stockNameMap.value(from.id())); priceTreeItem->setToolTip(KPriceTreeItem::ePriceStockName, (from.isCurrency()) ? QString() : d->m_stockNameMap.value(from.id())); priceTreeItem->setText(KPriceTreeItem::ePriceCurrency, to.id()); priceTreeItem->setText(KPriceTreeItem::ePriceDate, QLocale().toString(price.date(), QLocale::ShortFormat)); priceTreeItem->setData(KPriceTreeItem::ePriceDate, KPriceTreeItem::OrderRole, QVariant(price.date())); priceTreeItem->setText(KPriceTreeItem::ePricePrice, price.rate(priceBase).formatMoney("", from.pricePrecision())); priceTreeItem->setTextAlignment(KPriceTreeItem::ePricePrice, Qt::AlignRight | Qt::AlignVCenter); priceTreeItem->setData(KPriceTreeItem::ePricePrice, KPriceTreeItem::OrderRole, QVariant::fromValue(price.rate(priceBase))); priceTreeItem->setText(KPriceTreeItem::ePriceSource, price.source()); } return priceTreeItem; } void KMyMoneyPriceDlg::slotSelectPrice() { Q_D(KMyMoneyPriceDlg); QTreeWidgetItem* item = 0; if (d->ui->m_priceList->selectedItems().count() > 0) { item = d->ui->m_priceList->selectedItems().at(0); } d->m_currentItem = item; d->ui->m_editButton->setEnabled(item != 0); bool deleteEnabled = (item != 0); //if one of the selected entries is a default, then deleting is disabled QList itemsList = d->ui->m_priceList->selectedItems(); QList::const_iterator item_it; for (item_it = itemsList.constBegin(); item_it != itemsList.constEnd(); ++item_it) { MyMoneyPrice price = (*item_it)->data(0, Qt::UserRole).value(); if (price.source() == "KMyMoney") deleteEnabled = false; } d->ui->m_deleteButton->setEnabled(deleteEnabled); // Modification of automatically added entries is not allowed // Multiple entries cannot be edited at once if (item) { MyMoneyPrice price = item->data(0, Qt::UserRole).value(); if (price.source() == "KMyMoney" || itemsList.count() > 1) d->ui->m_editButton->setEnabled(false); emit selectObject(price); } } void KMyMoneyPriceDlg::slotNewPrice() { Q_D(KMyMoneyPriceDlg); QPointer dlg = new KUpdateStockPriceDlg(this); try { auto item = d->ui->m_priceList->currentItem(); if (item) { MyMoneySecurity security; security = MyMoneyFile::instance()->security(item->data(0, Qt::UserRole).value().from()); dlg->ui->m_security->setSecurity(security); security = MyMoneyFile::instance()->security(item->data(0, Qt::UserRole).value().to()); dlg->ui->m_currency->setSecurity(security); } if (dlg->exec()) { MyMoneyPrice price(dlg->ui->m_security->security().id(), dlg->ui->m_currency->security().id(), dlg->date(), MyMoneyMoney::ONE, QString()); QTreeWidgetItem* p = loadPriceItem(price); d->ui->m_priceList->setCurrentItem(p, true); // If the user cancels the following operation, we delete the new item // and re-select any previously selected one if (slotEditPrice() == Rejected) { delete p; if (item) d->ui->m_priceList->setCurrentItem(item, true); } } } catch (...) { delete dlg; throw; } delete dlg; } int KMyMoneyPriceDlg::slotEditPrice() { Q_D(KMyMoneyPriceDlg); int rc = Rejected; auto item = d->ui->m_priceList->currentItem(); if (item) { MyMoneySecurity from(MyMoneyFile::instance()->security(item->data(0, Qt::UserRole).value().from())); MyMoneySecurity to(MyMoneyFile::instance()->security(item->data(0, Qt::UserRole).value().to())); signed64 fract = MyMoneyMoney::precToDenom(from.pricePrecision()); QPointer calc = new KCurrencyCalculator(from, to, MyMoneyMoney::ONE, item->data(0, Qt::UserRole).value().rate(to.id()), item->data(0, Qt::UserRole).value().date(), fract, this); calc->setupPriceEditor(); rc = calc->exec(); delete calc; } return rc; } void KMyMoneyPriceDlg::slotDeletePrice() { Q_D(KMyMoneyPriceDlg); QList listItems = d->ui->m_priceList->selectedItems(); if (listItems.count() > 0) { if (KMessageBox::questionYesNo(this, i18np("Do you really want to delete the selected price entry?", "Do you really want to delete the selected price entries?", listItems.count()), i18n("Delete price information"), KStandardGuiItem::yes(), KStandardGuiItem::no(), "DeletePrice") == KMessageBox::Yes) { MyMoneyFileTransaction ft; try { QList::const_iterator price_it; for (price_it = listItems.constBegin(); price_it != listItems.constEnd(); ++price_it) { MyMoneyFile::instance()->removePrice((*price_it)->data(0, Qt::UserRole).value()); } ft.commit(); } catch (const MyMoneyException &) { qDebug("Cannot delete price"); } } } } void KMyMoneyPriceDlg::slotOnlinePriceUpdate() { QPointer dlg = new KEquityPriceUpdateDlg(this); if (dlg->exec() == Accepted && dlg) dlg->storePrices(); delete dlg; } void KMyMoneyPriceDlg::slotOpenContextMenu(const QPoint& p) { Q_D(KMyMoneyPriceDlg); auto item = d->ui->m_priceList->itemAt(p); if (item) { d->ui->m_priceList->setCurrentItem(item, QItemSelectionModel::ClearAndSelect); emit openContextMenu(item->data(0, Qt::UserRole).value()); } } diff --git a/kmymoney/dialogs/kmymoneysplittable.cpp b/kmymoney/dialogs/kmymoneysplittable.cpp index b91cd4579..35e97c6a1 100644 --- a/kmymoney/dialogs/kmymoneysplittable.cpp +++ b/kmymoney/dialogs/kmymoneysplittable.cpp @@ -1,1087 +1,1088 @@ /*************************************************************************** kmymoneysplittable.cpp - description ------------------- begin : Thu Jan 10 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 "kmymoneysplittable.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "mymoneysplit.h" #include "mymoneytransaction.h" #include "mymoneyaccount.h" #include "mymoneyfile.h" #include "mymoneyprice.h" #include "kmymoneyedit.h" #include "kmymoneycategory.h" #include "kmymoneyaccountselector.h" #include "kmymoneylineedit.h" #include "mymoneysecurity.h" #include "kmymoneyglobalsettings.h" #include "kcurrencycalculator.h" #include "mymoneyutils.h" #include "mymoneytracer.h" +#include "mymoneyexception.h" #include "icons.h" #include "mymoneyenums.h" using namespace Icons; class KMyMoneySplitTablePrivate { Q_DISABLE_COPY(KMyMoneySplitTablePrivate) public: KMyMoneySplitTablePrivate() : m_currentRow(0), m_maxRows(0), m_precision(2), m_editCategory(0), m_editMemo(0), m_editAmount(0) { } ~KMyMoneySplitTablePrivate() { } /// the currently selected row (will be printed as selected) int m_currentRow; /// the number of rows filled with data int m_maxRows; MyMoneyTransaction m_transaction; MyMoneyAccount m_account; MyMoneySplit m_split; MyMoneySplit m_hiddenSplit; /** * This member keeps the precision for the values */ int m_precision; /** * This member keeps a pointer to the context menu */ QMenu* m_contextMenu; /// keeps the QAction of the delete entry in the context menu QAction* m_contextMenuDelete; /// keeps the QAction of the duplicate entry in the context menu QAction* m_contextMenuDuplicate; /** * This member contains a pointer to the input widget for the category. * The widget will be created and destroyed dynamically in createInputWidgets() * and destroyInputWidgets(). */ QPointer m_editCategory; /** * This member contains a pointer to the input widget for the memo. * The widget will be created and destroyed dynamically in createInputWidgets() * and destroyInputWidgets(). */ QPointer m_editMemo; /** * This member contains a pointer to the input widget for the amount. * The widget will be created and destroyed dynamically in createInputWidgets() * and destroyInputWidgets(). */ QPointer m_editAmount; /** * This member keeps the tab order for the above widgets */ QWidgetList m_tabOrderWidgets; QPointer m_registerButtonFrame; QPointer m_registerEnterButton; QPointer m_registerCancelButton; QMap m_priceInfo; }; KMyMoneySplitTable::KMyMoneySplitTable(QWidget *parent) : QTableWidget(parent), d_ptr(new KMyMoneySplitTablePrivate) { Q_D(KMyMoneySplitTable); // used for custom coloring with the help of the application's stylesheet setObjectName(QLatin1String("splittable")); // setup the transactions table setRowCount(1); setColumnCount(3); QStringList labels; labels << i18n("Category") << i18n("Memo") << i18n("Amount"); setHorizontalHeaderLabels(labels); setSelectionMode(QAbstractItemView::SingleSelection); setSelectionBehavior(QAbstractItemView::SelectRows); int left, top, right, bottom; getContentsMargins(&left, &top, &right, &bottom); setContentsMargins(0, top, right, bottom); setFont(KMyMoneyGlobalSettings::listCellFont()); setAlternatingRowColors(true); verticalHeader()->hide(); horizontalHeader()->setSectionsMovable(false); horizontalHeader()->setFont(KMyMoneyGlobalSettings::listHeaderFont()); KConfigGroup grp = KSharedConfig::openConfig()->group("SplitTable"); QByteArray columns; columns = grp.readEntry("HeaderState", columns); horizontalHeader()->restoreState(columns); horizontalHeader()->setStretchLastSection(true); setShowGrid(KMyMoneyGlobalSettings::showGrid()); setEditTriggers(QAbstractItemView::NoEditTriggers); // setup the context menu d->m_contextMenu = new QMenu(this); d->m_contextMenu->setTitle(i18n("Split Options")); d->m_contextMenu->setIcon(QIcon::fromTheme(g_Icons[Icon::ViewFinancialTransfer])); d->m_contextMenu->addAction(QIcon::fromTheme(g_Icons[Icon::DocumentEdit]), i18n("Edit..."), this, SLOT(slotStartEdit())); d->m_contextMenuDuplicate = d->m_contextMenu->addAction(QIcon::fromTheme(g_Icons[Icon::EditCopy]), i18nc("To duplicate a split", "Duplicate"), this, SLOT(slotDuplicateSplit())); d->m_contextMenuDelete = d->m_contextMenu->addAction(QIcon::fromTheme(g_Icons[Icon::EditDelete]), i18n("Delete..."), this, SLOT(slotDeleteSplit())); connect(this, &QAbstractItemView::clicked, this, static_cast(&KMyMoneySplitTable::slotSetFocus)); connect(this, &KMyMoneySplitTable::transactionChanged, this, &KMyMoneySplitTable::slotUpdateData); installEventFilter(this); } KMyMoneySplitTable::~KMyMoneySplitTable() { Q_D(KMyMoneySplitTable); auto grp = KSharedConfig::openConfig()->group("SplitTable"); QByteArray columns = horizontalHeader()->saveState(); grp.writeEntry("HeaderState", columns); grp.sync(); delete d; } int KMyMoneySplitTable::currentRow() const { Q_D(const KMyMoneySplitTable); return d->m_currentRow; } void KMyMoneySplitTable::setup(const QMap& priceInfo, int precision) { Q_D(KMyMoneySplitTable); d->m_priceInfo = priceInfo; d->m_precision = precision; } bool KMyMoneySplitTable::eventFilter(QObject *o, QEvent *e) { Q_D(KMyMoneySplitTable); // MYMONEYTRACER(tracer); QKeyEvent *k = static_cast(e); bool rc = false; int row = currentRow(); int lines = viewport()->height() / rowHeight(0); if (e->type() == QEvent::KeyPress && !isEditMode()) { rc = true; switch (k->key()) { case Qt::Key_Up: if (row) slotSetFocus(model()->index(row - 1, 0)); break; case Qt::Key_Down: if (row < d->m_transaction.splits().count() - 1) slotSetFocus(model()->index(row + 1, 0)); break; case Qt::Key_Home: slotSetFocus(model()->index(0, 0)); break; case Qt::Key_End: slotSetFocus(model()->index(d->m_transaction.splits().count() - 1, 0)); break; case Qt::Key_PageUp: if (lines) { while (lines-- > 0 && row) --row; slotSetFocus(model()->index(row, 0)); } break; case Qt::Key_PageDown: if (row < d->m_transaction.splits().count() - 1) { while (lines-- > 0 && row < d->m_transaction.splits().count() - 1) ++row; slotSetFocus(model()->index(row, 0)); } break; case Qt::Key_Delete: slotDeleteSplit(); break; case Qt::Key_Return: case Qt::Key_Enter: if (row < d->m_transaction.splits().count() - 1 && KMyMoneyGlobalSettings::enterMovesBetweenFields()) { slotStartEdit(); } else emit returnPressed(); break; case Qt::Key_Escape: emit escapePressed(); break; case Qt::Key_F2: slotStartEdit(); break; default: rc = true; // duplicate split if (Qt::Key_C == k->key() && Qt::ControlModifier == k->modifiers()) { slotDuplicateSplit(); // new split } else if (Qt::Key_Insert == k->key() && Qt::ControlModifier == k->modifiers()) { slotSetFocus(model()->index(d->m_transaction.splits().count() - 1, 0)); slotStartEdit(); } else if (k->text()[ 0 ].isPrint()) { KMyMoneyCategory* cat = createEditWidgets(false); if (cat) { KMyMoneyLineEdit *le = qobject_cast(cat->lineEdit()); if (le) { // make sure, the widget receives the key again // and does not select the text this time le->setText(k->text()); le->end(false); le->deselect(); le->skipSelectAll(true); le->setFocus(); } } } break; } } else if (e->type() == QEvent::KeyPress && isEditMode()) { bool terminate = true; rc = true; switch (k->key()) { // suppress the F2 functionality to start editing in inline edit mode case Qt::Key_F2: // suppress the cursor movement in inline edit mode case Qt::Key_Up: case Qt::Key_Down: case Qt::Key_PageUp: case Qt::Key_PageDown: break; case Qt::Key_Return: case Qt::Key_Enter: // we cannot call the slot directly, as it destroys the caller of // this method :-( So we let the event handler take care of calling // the respective slot using a timeout. For a KLineEdit derived object // it could be, that at this point the user selected a value from // a completion list. In this case, we close the completion list and // do not end editing of the transaction. if (o->inherits("KLineEdit")) { KLineEdit* le = dynamic_cast(o); KCompletionBox* box = le->completionBox(false); if (box && box->isVisible()) { terminate = false; le->completionBox(false)->hide(); } } // in case we have the 'enter moves focus between fields', we need to simulate // a TAB key when the object 'o' points to the category or memo field. if (KMyMoneyGlobalSettings::enterMovesBetweenFields()) { if (o == d->m_editCategory->lineEdit() || o == d->m_editMemo) { terminate = false; QKeyEvent evt(e->type(), Qt::Key_Tab, k->modifiers(), QString(), k->isAutoRepeat(), k->count()); QApplication::sendEvent(o, &evt); } } if (terminate) { QTimer::singleShot(0, this, SLOT(slotEndEditKeyboard())); } break; case Qt::Key_Escape: // we cannot call the slot directly, as it destroys the caller of // this method :-( So we let the event handler take care of calling // the respective slot using a timeout. QTimer::singleShot(0, this, SLOT(slotCancelEdit())); break; default: rc = false; break; } } else if (e->type() == QEvent::KeyRelease && !isEditMode()) { // for some reason, we only see a KeyRelease event of the Menu key // here. In other locations (e.g. Register::eventFilter()) we see // a KeyPress event. Strange. (ipwizard - 2008-05-10) switch (k->key()) { case Qt::Key_Menu: // if the very last entry is selected, the delete // operation is not available otherwise it is d->m_contextMenuDelete->setEnabled( row < d->m_transaction.splits().count() - 1); d->m_contextMenuDuplicate->setEnabled( row < d->m_transaction.splits().count() - 1); d->m_contextMenu->exec(QCursor::pos()); rc = true; break; default: break; } } // if the event has not been processed here, forward it to // the base class implementation if it's not a key event if (rc == false) { if (e->type() != QEvent::KeyPress && e->type() != QEvent::KeyRelease) { rc = QTableWidget::eventFilter(o, e); } } return rc; } void KMyMoneySplitTable::slotSetFocus(const QModelIndex& index) { slotSetFocus(index, Qt::LeftButton); } void KMyMoneySplitTable::slotSetFocus(const QModelIndex& index, int button) { Q_D(KMyMoneySplitTable); MYMONEYTRACER(tracer); auto row = index.row(); // adjust row to used area if (row > d->m_transaction.splits().count() - 1) row = d->m_transaction.splits().count() - 1; if (row < 0) row = 0; // make sure the row will be on the screen scrollTo(model()->index(row, 0)); if (isEditMode()) { // in edit mode? if (isEditSplitValid() && KMyMoneyGlobalSettings::focusChangeIsEnter()) endEdit(false/*keyboard driven*/, false/*set focus to next row*/); else slotCancelEdit(); } if (button == Qt::LeftButton) { // left mouse button if (row != currentRow()) { // setup new current row and update visible selection selectRow(row); slotUpdateData(d->m_transaction); } } else if (button == Qt::RightButton) { // context menu is only available when cursor is on // an existing transaction or the first line after this area if (row == index.row()) { // setup new current row and update visible selection selectRow(row); slotUpdateData(d->m_transaction); // if the very last entry is selected, the delete // operation is not available otherwise it is d->m_contextMenuDelete->setEnabled( row < d->m_transaction.splits().count() - 1); d->m_contextMenuDuplicate->setEnabled( row < d->m_transaction.splits().count() - 1); d->m_contextMenu->exec(QCursor::pos()); } } } void KMyMoneySplitTable::mousePressEvent(QMouseEvent* e) { slotSetFocus(indexAt(e->pos()), e->button()); } /* turn off QTable behaviour */ void KMyMoneySplitTable::mouseReleaseEvent(QMouseEvent* /* e */) { } void KMyMoneySplitTable::mouseDoubleClickEvent(QMouseEvent *e) { Q_D(KMyMoneySplitTable); MYMONEYTRACER(tracer); int col = columnAt(e->pos().x()); slotSetFocus(model()->index(rowAt(e->pos().y()), col), e->button()); createEditWidgets(false); QLineEdit* editWidget = 0; //krazy:exclude=qmethods switch (col) { case 0: editWidget = d->m_editCategory->lineEdit(); break; case 1: editWidget = d->m_editMemo; break; case 2: editWidget = d->m_editAmount->lineedit(); break; default: break; } if (editWidget) { editWidget->setFocus(); editWidget->selectAll(); } } void KMyMoneySplitTable::selectRow(int row) { Q_D(KMyMoneySplitTable); MYMONEYTRACER(tracer); if (row > d->m_maxRows) row = d->m_maxRows; d->m_currentRow = row; QTableWidget::selectRow(row); QList list = getSplits(d->m_transaction); if (row < list.count()) d->m_split = list[row]; else d->m_split = MyMoneySplit(); } void KMyMoneySplitTable::setRowCount(int irows) { QTableWidget::setRowCount(irows); // determine row height according to the edit widgets // we use the category widget as the base QFontMetrics fm(KMyMoneyGlobalSettings::listCellFont()); int height = fm.lineSpacing() + 6; #if 0 // recalculate row height hint KMyMoneyCategory cat; height = qMax(cat.sizeHint().height(), height); #endif verticalHeader()->setUpdatesEnabled(false); for (auto i = 0; i < irows; ++i) verticalHeader()->resizeSection(i, height); verticalHeader()->setUpdatesEnabled(true); } void KMyMoneySplitTable::setTransaction(const MyMoneyTransaction& t, const MyMoneySplit& s, const MyMoneyAccount& acc) { Q_D(KMyMoneySplitTable); MYMONEYTRACER(tracer); d->m_transaction = t; d->m_account = acc; d->m_hiddenSplit = s; selectRow(0); slotUpdateData(d->m_transaction); } MyMoneyTransaction KMyMoneySplitTable::transaction() const { Q_D(const KMyMoneySplitTable); return d->m_transaction; } QList KMyMoneySplitTable::getSplits(const MyMoneyTransaction& t) const { Q_D(const KMyMoneySplitTable); // get list of splits QList list = t.splits(); // and ignore the one that should be hidden QList::Iterator it; for (it = list.begin(); it != list.end(); ++it) { if ((*it).id() == d->m_hiddenSplit.id()) { list.erase(it); break; } } return list; } void KMyMoneySplitTable::slotUpdateData(const MyMoneyTransaction& t) { Q_D(KMyMoneySplitTable); MYMONEYTRACER(tracer); unsigned long numRows = 0; QTableWidgetItem* textItem; QList list = getSplits(t); updateTransactionTableSize(); // fill the part that is used by transactions QList::Iterator it; for (it = list.begin(); it != list.end(); ++it) { QString colText; MyMoneyMoney value = (*it).value(); if (!(*it).accountId().isEmpty()) { try { colText = MyMoneyFile::instance()->accountToCategory((*it).accountId()); } catch (const MyMoneyException &) { qDebug("Unexpected exception in KMyMoneySplitTable::slotUpdateData()"); } } QString amountTxt = value.formatMoney(d->m_account.fraction()); if (value == MyMoneyMoney::autoCalc) { amountTxt = i18n("will be calculated"); } if (colText.isEmpty() && (*it).memo().isEmpty() && value.isZero()) amountTxt.clear(); unsigned width = fontMetrics().width(amountTxt); KMyMoneyEdit* valfield = new KMyMoneyEdit(); valfield->setMinimumWidth(width); width = valfield->minimumSizeHint().width(); delete valfield; textItem = item(numRows, 0); if (textItem) textItem->setText(colText); else setItem(numRows, 0, new QTableWidgetItem(colText)); textItem = item(numRows, 1); if (textItem) textItem->setText((*it).memo()); else setItem(numRows, 1, new QTableWidgetItem((*it).memo())); textItem = item(numRows, 2); if (textItem) textItem->setText(amountTxt); else setItem(numRows, 2, new QTableWidgetItem(amountTxt)); item(numRows, 2)->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter); ++numRows; } // now clean out the remainder of the table while (numRows < static_cast(rowCount())) { for (auto i = 0 ; i < 3; ++i) { textItem = item(numRows, i); if (textItem) textItem->setText(""); else setItem(numRows, i, new QTableWidgetItem("")); } item(numRows, 2)->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter); ++numRows; } } void KMyMoneySplitTable::updateTransactionTableSize() { Q_D(KMyMoneySplitTable); // get current size of transactions table int tableHeight = height(); int splitCount = d->m_transaction.splits().count() - 1; if (splitCount < 0) splitCount = 0; // see if we need some extra lines to fill the current size with the grid int numExtraLines = (tableHeight / rowHeight(0)) - splitCount; if (numExtraLines < 2) numExtraLines = 2; setRowCount(splitCount + numExtraLines); d->m_maxRows = splitCount; } void KMyMoneySplitTable::resizeEvent(QResizeEvent* ev) { QTableWidget::resizeEvent(ev); if (!isEditMode()) { // update the size of the transaction table only if a split is not being edited // otherwise the height of the editors would be altered in an undesired way updateTransactionTableSize(); } } void KMyMoneySplitTable::slotDuplicateSplit() { Q_D(KMyMoneySplitTable); MYMONEYTRACER(tracer); QList list = getSplits(d->m_transaction); if (d->m_currentRow < list.count()) { MyMoneySplit split = list[d->m_currentRow]; split.clearId(); try { d->m_transaction.addSplit(split); emit transactionChanged(d->m_transaction); } catch (const MyMoneyException &e) { qDebug("Cannot duplicate split: %s", qPrintable(e.what())); } } } void KMyMoneySplitTable::slotDeleteSplit() { Q_D(KMyMoneySplitTable); MYMONEYTRACER(tracer); QList list = getSplits(d->m_transaction); if (d->m_currentRow < list.count()) { if (KMessageBox::warningContinueCancel(this, i18n("You are about to delete the selected split. " "Do you really want to continue?"), i18n("KMyMoney") ) == KMessageBox::Continue) { try { d->m_transaction.removeSplit(list[d->m_currentRow]); // if we removed the last split, select the previous if (d->m_currentRow && d->m_currentRow == list.count() - 1) selectRow(d->m_currentRow - 1); else selectRow(d->m_currentRow); emit transactionChanged(d->m_transaction); } catch (const MyMoneyException &e) { qDebug("Cannot remove split: %s", qPrintable(e.what())); } } } } KMyMoneyCategory* KMyMoneySplitTable::slotStartEdit() { MYMONEYTRACER(tracer); return createEditWidgets(true); } void KMyMoneySplitTable::slotEndEdit() { endEdit(false); } void KMyMoneySplitTable::slotEndEditKeyboard() { endEdit(true); } void KMyMoneySplitTable::endEdit(bool keyboardDriven, bool setFocusToNextRow) { Q_D(KMyMoneySplitTable); auto file = MyMoneyFile::instance(); MYMONEYTRACER(tracer); MyMoneySplit s1 = d->m_split; if (!isEditSplitValid()) { KMessageBox::information(this, i18n("You need to assign a category to this split before it can be entered."), i18n("Enter split"), "EnterSplitWithEmptyCategory"); d->m_editCategory->setFocus(); return; } bool needUpdate = false; if (d->m_editCategory->selectedItem() != d->m_split.accountId()) { s1.setAccountId(d->m_editCategory->selectedItem()); needUpdate = true; } if (d->m_editMemo->text() != d->m_split.memo()) { s1.setMemo(d->m_editMemo->text()); needUpdate = true; } if (d->m_editAmount->value() != d->m_split.value()) { s1.setValue(d->m_editAmount->value()); needUpdate = true; } if (needUpdate) { if (!s1.value().isZero()) { MyMoneyAccount cat = file->account(s1.accountId()); if (cat.currencyId() != d->m_transaction.commodity()) { MyMoneySecurity fromCurrency, toCurrency; MyMoneyMoney fromValue, toValue; fromCurrency = file->security(d->m_transaction.commodity()); toCurrency = file->security(cat.currencyId()); // determine the fraction required for this category int fract = toCurrency.smallestAccountFraction(); if (cat.accountType() == eMyMoney::Account::Type::Cash) fract = toCurrency.smallestCashFraction(); // display only positive values to the user fromValue = s1.value().abs(); // if we had a price info in the beginning, we use it here if (d->m_priceInfo.find(cat.currencyId()) != d->m_priceInfo.end()) { toValue = (fromValue * d->m_priceInfo[cat.currencyId()]).convert(fract); } // if the shares are still 0, we need to change that if (toValue.isZero()) { const MyMoneyPrice &price = MyMoneyFile::instance()->price(fromCurrency.id(), toCurrency.id()); // if the price is valid calculate the shares. If it is invalid // assume a conversion rate of 1.0 if (price.isValid()) { toValue = (price.rate(toCurrency.id()) * fromValue).convert(fract); } else { toValue = fromValue; } } // now present all that to the user QPointer calc = new KCurrencyCalculator(fromCurrency, toCurrency, fromValue, toValue, d->m_transaction.postDate(), fract, this); if (calc->exec() == QDialog::Rejected) { delete calc; return; } else { s1.setShares((s1.value() * calc->price()).convert(fract)); delete calc; } } else { s1.setShares(s1.value()); } } else s1.setShares(s1.value()); d->m_split = s1; try { if (d->m_split.id().isEmpty()) { d->m_transaction.addSplit(d->m_split); } else { d->m_transaction.modifySplit(d->m_split); } emit transactionChanged(d->m_transaction); } catch (const MyMoneyException &e) { qDebug("Cannot add/modify split: %s", qPrintable(e.what())); } } this->setFocus(); destroyEditWidgets(); if (setFocusToNextRow) { slotSetFocus(model()->index(currentRow() + 1, 0)); } // if we still have more splits, we start editing right away // in case we have selected 'enter moves between fields' if (keyboardDriven && currentRow() < d->m_transaction.splits().count() - 1 && KMyMoneyGlobalSettings::enterMovesBetweenFields()) { slotStartEdit(); } } void KMyMoneySplitTable::slotCancelEdit() { MYMONEYTRACER(tracer); if (isEditMode()) { destroyEditWidgets(); this->setFocus(); } } bool KMyMoneySplitTable::isEditMode() const { Q_D(const KMyMoneySplitTable); // while the edit widgets exist we're in edit mode return d->m_editAmount || d->m_editMemo || d->m_editCategory; } bool KMyMoneySplitTable::isEditSplitValid() const { Q_D(const KMyMoneySplitTable); return isEditMode() && !d->m_editCategory->selectedItem().isEmpty(); } void KMyMoneySplitTable::destroyEditWidgets() { MYMONEYTRACER(tracer); Q_D(KMyMoneySplitTable); emit editFinished(); disconnect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, this, &KMyMoneySplitTable::slotLoadEditWidgets); destroyEditWidget(d->m_currentRow, 0); destroyEditWidget(d->m_currentRow, 1); destroyEditWidget(d->m_currentRow, 2); destroyEditWidget(d->m_currentRow + 1, 0); } void KMyMoneySplitTable::destroyEditWidget(int r, int c) { if (QWidget* cw = cellWidget(r, c)) cw->hide(); removeCellWidget(r, c); } KMyMoneyCategory* KMyMoneySplitTable::createEditWidgets(bool setFocus) { MYMONEYTRACER(tracer); emit editStarted(); Q_D(KMyMoneySplitTable); auto cellFont = KMyMoneyGlobalSettings::listCellFont(); d->m_tabOrderWidgets.clear(); // create the widgets d->m_editAmount = new KMyMoneyEdit(0); d->m_editAmount->setFont(cellFont); d->m_editAmount->setResetButtonVisible(false); d->m_editAmount->setPrecision(d->m_precision); d->m_editCategory = new KMyMoneyCategory(); d->m_editCategory->setPlaceholderText(i18n("Category")); d->m_editCategory->setFont(cellFont); connect(d->m_editCategory, SIGNAL(createItem(QString,QString&)), this, SIGNAL(createCategory(QString,QString&))); connect(d->m_editCategory, SIGNAL(objectCreation(bool)), this, SIGNAL(objectCreation(bool))); d->m_editMemo = new KMyMoneyLineEdit(0, false, Qt::AlignLeft | Qt::AlignVCenter); d->m_editMemo->setPlaceholderText(i18n("Memo")); d->m_editMemo->setFont(cellFont); // create buttons for the mouse users d->m_registerButtonFrame = new QFrame(this); d->m_registerButtonFrame->setContentsMargins(0, 0, 0, 0); d->m_registerButtonFrame->setAutoFillBackground(true); QHBoxLayout* l = new QHBoxLayout(d->m_registerButtonFrame); l->setContentsMargins(0, 0, 0, 0); l->setSpacing(0); d->m_registerEnterButton = new QPushButton(QIcon::fromTheme(g_Icons[Icon::DialogOK]) , QString(), d->m_registerButtonFrame); d->m_registerCancelButton = new QPushButton(QIcon::fromTheme(g_Icons[Icon::DialogCancel]) , QString(), d->m_registerButtonFrame); l->addWidget(d->m_registerEnterButton); l->addWidget(d->m_registerCancelButton); l->addStretch(2); connect(d->m_registerEnterButton.data(), &QAbstractButton::clicked, this, &KMyMoneySplitTable::slotEndEdit); connect(d->m_registerCancelButton.data(), &QAbstractButton::clicked, this, &KMyMoneySplitTable::slotCancelEdit); // setup tab order addToTabOrder(d->m_editCategory); addToTabOrder(d->m_editMemo); addToTabOrder(d->m_editAmount); addToTabOrder(d->m_registerEnterButton); addToTabOrder(d->m_registerCancelButton); if (!d->m_split.accountId().isEmpty()) { d->m_editCategory->setSelectedItem(d->m_split.accountId()); } else { // check if the transaction is balanced or not. If not, // assign the remainder to the amount. MyMoneyMoney diff; QList list = d->m_transaction.splits(); QList::ConstIterator it_s; for (it_s = list.constBegin(); it_s != list.constEnd(); ++it_s) { if (!(*it_s).accountId().isEmpty()) diff += (*it_s).value(); } d->m_split.setValue(-diff); } d->m_editMemo->loadText(d->m_split.memo()); // don't allow automatically calculated values to be modified if (d->m_split.value() == MyMoneyMoney::autoCalc) { d->m_editAmount->setEnabled(false); d->m_editAmount->loadText("will be calculated"); } else d->m_editAmount->setValue(d->m_split.value()); setCellWidget(d->m_currentRow, 0, d->m_editCategory); setCellWidget(d->m_currentRow, 1, d->m_editMemo); setCellWidget(d->m_currentRow, 2, d->m_editAmount); setCellWidget(d->m_currentRow + 1, 0, d->m_registerButtonFrame); // load e.g. the category widget with the account list slotLoadEditWidgets(); connect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, this, &KMyMoneySplitTable::slotLoadEditWidgets); foreach (QWidget* w, d->m_tabOrderWidgets) { if (w) { w->installEventFilter(this); } } if (setFocus) { d->m_editCategory->lineEdit()->setFocus(); d->m_editCategory->lineEdit()->selectAll(); } // resize the rows so the added edit widgets would fit appropriately resizeRowsToContents(); return d->m_editCategory; } void KMyMoneySplitTable::slotLoadEditWidgets() { Q_D(KMyMoneySplitTable); // reload category widget auto categoryId = d->m_editCategory->selectedItem(); AccountSet aSet; aSet.addAccountGroup(eMyMoney::Account::Type::Asset); aSet.addAccountGroup(eMyMoney::Account::Type::Liability); aSet.addAccountGroup(eMyMoney::Account::Type::Income); aSet.addAccountGroup(eMyMoney::Account::Type::Expense); if (KMyMoneyGlobalSettings::expertMode()) aSet.addAccountGroup(eMyMoney::Account::Type::Equity); // remove the accounts with invalid types at this point aSet.removeAccountType(eMyMoney::Account::Type::CertificateDep); aSet.removeAccountType(eMyMoney::Account::Type::Investment); aSet.removeAccountType(eMyMoney::Account::Type::Stock); aSet.removeAccountType(eMyMoney::Account::Type::MoneyMarket); aSet.load(d->m_editCategory->selector()); // if an account is specified then remove it from the widget so that the user // cannot create a transfer with from and to account being the same account if (!d->m_account.id().isEmpty()) d->m_editCategory->selector()->removeItem(d->m_account.id()); if (!categoryId.isEmpty()) d->m_editCategory->setSelectedItem(categoryId); } void KMyMoneySplitTable::addToTabOrder(QWidget* w) { Q_D(KMyMoneySplitTable); if (w) { while (w->focusProxy()) w = w->focusProxy(); d->m_tabOrderWidgets.append(w); } } bool KMyMoneySplitTable::focusNextPrevChild(bool next) { MYMONEYTRACER(tracer); Q_D(KMyMoneySplitTable); auto rc = false; if (isEditMode()) { QWidget *w = 0; w = qApp->focusWidget(); int currentWidgetIndex = d->m_tabOrderWidgets.indexOf(w); while (w && currentWidgetIndex == -1) { // qDebug("'%s' not in list, use parent", w->className()); w = w->parentWidget(); currentWidgetIndex = d->m_tabOrderWidgets.indexOf(w); } if (currentWidgetIndex != -1) { // if(w) qDebug("tab order is at '%s'", w->className()); 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]; if (((w->focusPolicy() & Qt::TabFocus) == Qt::TabFocus) && w->isVisible() && w->isEnabled()) { // qDebug("Selecting '%s' as focus", w->className()); w->setFocus(); rc = true; } } } else rc = QTableWidget::focusNextPrevChild(next); return rc; } diff --git a/kmymoney/dialogs/kreportconfigurationfilterdlg.cpp b/kmymoney/dialogs/kreportconfigurationfilterdlg.cpp index 221b3232c..1fdcdedda 100644 --- a/kmymoney/dialogs/kreportconfigurationfilterdlg.cpp +++ b/kmymoney/dialogs/kreportconfigurationfilterdlg.cpp @@ -1,798 +1,799 @@ /*************************************************************************** kreportconfigurationdlg.cpp - description ------------------- begin : Mon Jun 21 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 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 "kreportconfigurationfilterdlg.h" #include "kfindtransactiondlg_p.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "kmymoneydateinput.h" #include "kmymoneyedit.h" #include "mymoneyfile.h" +#include "mymoneyexception.h" #include "mymoneybudget.h" #include "mymoneyreport.h" #include "daterangedlg.h" #include "reporttabimpl.h" #include "ui_kfindtransactiondlg.h" #include #include #include #include #include #include #include class KReportConfigurationFilterDlgPrivate : public KFindTransactionDlgPrivate { Q_DISABLE_COPY(KReportConfigurationFilterDlgPrivate) public: KReportConfigurationFilterDlgPrivate(KReportConfigurationFilterDlg *qq) : KFindTransactionDlgPrivate(qq), m_tabRowColPivot(nullptr), m_tabRowColQuery(nullptr), m_tabChart(nullptr), m_tabRange(nullptr) { } QPointer m_tabGeneral; QPointer m_tabRowColPivot; QPointer m_tabRowColQuery; QPointer m_tabChart; QPointer m_tabRange; QPointer m_tabCapitalGain; QPointer m_tabPerformance; MyMoneyReport m_initialState; MyMoneyReport m_currentState; QVector m_budgets; }; KReportConfigurationFilterDlg::KReportConfigurationFilterDlg(MyMoneyReport report, QWidget *parent) : KFindTransactionDlg(*new KReportConfigurationFilterDlgPrivate(this), parent, (report.rowType() == MyMoneyReport::eAccount)) { Q_D(KReportConfigurationFilterDlg); d->m_initialState = report; d->m_currentState = report; // // Rework labeling // setWindowTitle(i18n("Report Configuration")); delete d->ui->TextLabel1; // // Rework the buttons // // the Apply button is always enabled disconnect(SIGNAL(selectionNotEmpty(bool))); d->ui->buttonBox->button(QDialogButtonBox::Apply)->setEnabled(true); d->ui->buttonBox->button(QDialogButtonBox::Apply)->setToolTip(i18nc("@info:tooltip for report configuration apply button", "Apply the configuration changes to the report")); // // Add new tabs // d->m_tabGeneral = new ReportTabGeneral(d->ui->m_criteriaTab); d->ui->m_criteriaTab->insertTab(0, d->m_tabGeneral, i18nc("General tab", "General")); if (d->m_initialState.reportType() == MyMoneyReport::ePivotTable) { int tabNr = 1; if (!(d->m_initialState.isIncludingPrice() || d->m_initialState.isIncludingAveragePrice())) { d->m_tabRowColPivot = new ReportTabRowColPivot(d->ui->m_criteriaTab); d->ui->m_criteriaTab->insertTab(tabNr++, d->m_tabRowColPivot, i18n("Rows/Columns")); connect(d->m_tabRowColPivot->ui->m_comboRows, static_cast(&QComboBox::activated), this, static_cast(&KReportConfigurationFilterDlg::slotRowTypeChanged)); connect(d->m_tabRowColPivot->ui->m_comboRows, static_cast(&QComboBox::activated), this, static_cast(&KReportConfigurationFilterDlg::slotUpdateColumnsCombo)); //control the state of the includeTransfer check connect(d->ui->m_categoriesView, &KMyMoneySelector::stateChanged, this, &KReportConfigurationFilterDlg::slotUpdateCheckTransfers); } d->m_tabChart = new ReportTabChart(d->ui->m_criteriaTab); d->ui->m_criteriaTab->insertTab(tabNr++, d->m_tabChart, i18n("Chart")); d->m_tabRange = new ReportTabRange(d->ui->m_criteriaTab); d->ui->m_criteriaTab->insertTab(tabNr++, d->m_tabRange, i18n("Range")); // date tab is going to be replaced by range tab, so delete it d->ui->dateRangeLayout->removeWidget(d->m_dateRange); d->m_dateRange->deleteLater(); d->ui->m_criteriaTab->removeTab(d->ui->m_criteriaTab->indexOf(d->ui->m_dateTab)); d->ui->m_dateTab->deleteLater(); d->m_dateRange = d->m_tabRange->m_dateRange; // reconnect signal connect(d->m_dateRange, &DateRangeDlg::rangeChanged, this, &KReportConfigurationFilterDlg::slotUpdateSelections); if (!(d->m_initialState.isIncludingPrice() || d->m_initialState.isIncludingAveragePrice())) { connect(d->m_tabRange->ui->m_comboColumns, static_cast(&QComboBox::activated), this, &KReportConfigurationFilterDlg::slotColumnTypeChanged); connect(d->m_tabRange->ui->m_comboColumns, static_cast(&QComboBox::activated), this, static_cast(&KReportConfigurationFilterDlg::slotUpdateColumnsCombo)); } connect(d->m_tabChart->ui->m_logYaxis, &QCheckBox::stateChanged, this, &KReportConfigurationFilterDlg::slotLogAxisChanged); } else if (d->m_initialState.reportType() == MyMoneyReport::eQueryTable) { // eInvestmentHoldings is a special-case report, and you cannot configure the // rows & columns of that report. if (d->m_initialState.rowType() < MyMoneyReport::eAccountByTopAccount) { d->m_tabRowColQuery = new ReportTabRowColQuery(d->ui->m_criteriaTab); d->ui->m_criteriaTab->insertTab(1, d->m_tabRowColQuery, i18n("Rows/Columns")); } if (d->m_initialState.queryColumns() & MyMoneyReport::eQCcapitalgain) { d->m_tabCapitalGain = new ReportTabCapitalGain(d->ui->m_criteriaTab); d->ui->m_criteriaTab->insertTab(1, d->m_tabCapitalGain, i18n("Report")); } if (d->m_initialState.queryColumns() & MyMoneyReport::eQCperformance) { d->m_tabPerformance = new ReportTabPerformance(d->ui->m_criteriaTab); d->ui->m_criteriaTab->insertTab(1, d->m_tabPerformance, i18n("Report")); } } d->ui->m_criteriaTab->setCurrentIndex(d->ui->m_criteriaTab->indexOf(d->m_tabGeneral)); d->ui->m_criteriaTab->setMinimumSize(500, 200); QList list = MyMoneyFile::instance()->budgetList(); QList::const_iterator it_b; for (it_b = list.constBegin(); it_b != list.constEnd(); ++it_b) { d->m_budgets.push_back(*it_b); } // // Now set up the widgets with proper values // slotReset(); } KReportConfigurationFilterDlg::~KReportConfigurationFilterDlg() { } MyMoneyReport KReportConfigurationFilterDlg::getConfig() const { Q_D(const KReportConfigurationFilterDlg); return d->m_currentState; } void KReportConfigurationFilterDlg::slotSearch() { Q_D(KReportConfigurationFilterDlg); // setup the filter from the dialog widgets d->setupFilter(); // Copy the m_filter over to the filter part of m_currentConfig. d->m_currentState.assignFilter(d->m_filter); // Then extract the report properties d->m_currentState.setName(d->m_tabGeneral->ui->m_editName->text()); d->m_currentState.setComment(d->m_tabGeneral->ui->m_editComment->text()); d->m_currentState.setConvertCurrency(d->m_tabGeneral->ui->m_checkCurrency->isChecked()); d->m_currentState.setFavorite(d->m_tabGeneral->ui->m_checkFavorite->isChecked()); d->m_currentState.setSkipZero(d->m_tabGeneral->ui->m_skipZero->isChecked()); if (d->m_tabRowColPivot) { MyMoneyReport::EDetailLevel dl[4] = { MyMoneyReport::eDetailAll, MyMoneyReport::eDetailTop, MyMoneyReport::eDetailGroup, MyMoneyReport::eDetailTotal }; d->m_currentState.setDetailLevel(dl[d->m_tabRowColPivot->ui->m_comboDetail->currentIndex()]); // modify the rowtype only if the widget is enabled if (d->m_tabRowColPivot->ui->m_comboRows->isEnabled()) { MyMoneyReport::ERowType rt[2] = { MyMoneyReport::eExpenseIncome, MyMoneyReport::eAssetLiability }; d->m_currentState.setRowType(rt[d->m_tabRowColPivot->ui->m_comboRows->currentIndex()]); } d->m_currentState.setShowingRowTotals(false); if (d->m_tabRowColPivot->ui->m_comboRows->currentIndex() == 0) d->m_currentState.setShowingRowTotals(d->m_tabRowColPivot->ui->m_checkTotalColumn->isChecked()); d->m_currentState.setShowingColumnTotals(d->m_tabRowColPivot->ui->m_checkTotalRow->isChecked()); d->m_currentState.setIncludingSchedules(d->m_tabRowColPivot->ui->m_checkScheduled->isChecked()); d->m_currentState.setIncludingTransfers(d->m_tabRowColPivot->ui->m_checkTransfers->isChecked()); d->m_currentState.setIncludingUnusedAccounts(d->m_tabRowColPivot->ui->m_checkUnused->isChecked()); if (d->m_tabRowColPivot->ui->m_comboBudget->isEnabled()) { d->m_currentState.setBudget(d->m_budgets[d->m_tabRowColPivot->ui->m_comboBudget->currentItem()].id(), d->m_initialState.rowType() == MyMoneyReport::eBudgetActual); } else { d->m_currentState.setBudget(QString(), false); } //set moving average days if (d->m_tabRowColPivot->ui->m_movingAverageDays->isEnabled()) { d->m_currentState.setMovingAverageDays(d->m_tabRowColPivot->ui->m_movingAverageDays->value()); } } else if (d->m_tabRowColQuery) { MyMoneyReport::ERowType rtq[8] = { MyMoneyReport::eCategory, MyMoneyReport::eTopCategory, MyMoneyReport::eTag, MyMoneyReport::ePayee, MyMoneyReport::eAccount, MyMoneyReport::eTopAccount, MyMoneyReport::eMonth, MyMoneyReport::eWeek }; d->m_currentState.setRowType(rtq[d->m_tabRowColQuery->ui->m_comboOrganizeBy->currentIndex()]); unsigned qc = MyMoneyReport::eQCnone; if (d->m_currentState.queryColumns() & MyMoneyReport::eQCloan) // once a loan report, always a loan report qc = MyMoneyReport::eQCloan; if (d->m_tabRowColQuery->ui->m_checkNumber->isChecked()) qc |= MyMoneyReport::eQCnumber; if (d->m_tabRowColQuery->ui->m_checkPayee->isChecked()) qc |= MyMoneyReport::eQCpayee; if (d->m_tabRowColQuery->ui->m_checkTag->isChecked()) qc |= MyMoneyReport::eQCtag; if (d->m_tabRowColQuery->ui->m_checkCategory->isChecked()) qc |= MyMoneyReport::eQCcategory; if (d->m_tabRowColQuery->ui->m_checkMemo->isChecked()) qc |= MyMoneyReport::eQCmemo; if (d->m_tabRowColQuery->ui->m_checkAccount->isChecked()) qc |= MyMoneyReport::eQCaccount; if (d->m_tabRowColQuery->ui->m_checkReconciled->isChecked()) qc |= MyMoneyReport::eQCreconciled; if (d->m_tabRowColQuery->ui->m_checkAction->isChecked()) qc |= MyMoneyReport::eQCaction; if (d->m_tabRowColQuery->ui->m_checkShares->isChecked()) qc |= MyMoneyReport::eQCshares; if (d->m_tabRowColQuery->ui->m_checkPrice->isChecked()) qc |= MyMoneyReport::eQCprice; if (d->m_tabRowColQuery->ui->m_checkBalance->isChecked()) qc |= MyMoneyReport::eQCbalance; d->m_currentState.setQueryColumns(static_cast(qc)); d->m_currentState.setTax(d->m_tabRowColQuery->ui->m_checkTax->isChecked()); d->m_currentState.setInvestmentsOnly(d->m_tabRowColQuery->ui->m_checkInvestments->isChecked()); d->m_currentState.setLoansOnly(d->m_tabRowColQuery->ui->m_checkLoans->isChecked()); d->m_currentState.setDetailLevel(d->m_tabRowColQuery->ui->m_checkHideSplitDetails->isChecked() ? MyMoneyReport::eDetailNone : MyMoneyReport::eDetailAll); d->m_currentState.setHideTransactions(d->m_tabRowColQuery->ui->m_checkHideTransactions->isChecked()); d->m_currentState.setShowingColumnTotals(!d->m_tabRowColQuery->ui->m_checkHideTotals->isChecked()); } if (d->m_tabChart) { MyMoneyReport::EChartType ct[5] = { MyMoneyReport::eChartLine, MyMoneyReport::eChartBar, MyMoneyReport::eChartStackedBar, MyMoneyReport::eChartPie, MyMoneyReport::eChartRing }; d->m_currentState.setChartType(ct[d->m_tabChart->ui->m_comboType->currentIndex()]); d->m_currentState.setChartCHGridLines(d->m_tabChart->ui->m_checkCHGridLines->isChecked()); d->m_currentState.setChartSVGridLines(d->m_tabChart->ui->m_checkSVGridLines->isChecked()); d->m_currentState.setChartDataLabels(d->m_tabChart->ui->m_checkValues->isChecked()); d->m_currentState.setChartByDefault(d->m_tabChart->ui->m_checkShowChart->isChecked()); d->m_currentState.setChartLineWidth(d->m_tabChart->ui->m_lineWidth->value()); d->m_currentState.setLogYAxis(d->m_tabChart->ui->m_logYaxis->isChecked()); } if (d->m_tabRange) { d->m_currentState.setDataRangeStart(d->m_tabRange->ui->m_dataRangeStart->text()); d->m_currentState.setDataRangeEnd(d->m_tabRange->ui->m_dataRangeEnd->text()); d->m_currentState.setDataMajorTick(d->m_tabRange->ui->m_dataMajorTick->text()); d->m_currentState.setDataMinorTick(d->m_tabRange->ui->m_dataMinorTick->text()); d->m_currentState.setYLabelsPrecision(d->m_tabRange->ui->m_yLabelsPrecision->value()); d->m_currentState.setDataFilter((MyMoneyReport::dataOptionE)d->m_tabRange->ui->m_dataLock->currentIndex()); MyMoneyReport::EColumnType ct[6] = { MyMoneyReport::eDays, MyMoneyReport::eWeeks, MyMoneyReport::eMonths, MyMoneyReport::eBiMonths, MyMoneyReport::eQuarters, MyMoneyReport::eYears }; bool dy[6] = { true, true, false, false, false, false }; d->m_currentState.setColumnType(ct[d->m_tabRange->ui->m_comboColumns->currentIndex()]); //TODO (Ace) This should be implicit in the call above. MMReport needs fixin' d->m_currentState.setColumnsAreDays(dy[d->m_tabRange->ui->m_comboColumns->currentIndex()]); } // setup the date lock eMyMoney::TransactionFilter::Date range = d->m_dateRange->dateRange(); d->m_currentState.setDateFilter(range); if (d->m_tabCapitalGain) { d->m_currentState.setTermSeparator(d->m_tabCapitalGain->ui->m_termSeparator->date()); d->m_currentState.setShowSTLTCapitalGains(d->m_tabCapitalGain->ui->m_showSTLTCapitalGains->isChecked()); d->m_currentState.setSettlementPeriod(d->m_tabCapitalGain->ui->m_settlementPeriod->value()); d->m_currentState.setShowingColumnTotals(!d->m_tabCapitalGain->ui->m_checkHideTotals->isChecked()); d->m_currentState.setInvestmentSum(static_cast(d->m_tabCapitalGain->ui->m_investmentSum->currentData().toInt())); } if (d->m_tabPerformance) { d->m_currentState.setShowingColumnTotals(!d->m_tabPerformance->ui->m_checkHideTotals->isChecked()); d->m_currentState.setInvestmentSum(static_cast(d->m_tabPerformance->ui->m_investmentSum->currentData().toInt())); } done(true); } void KReportConfigurationFilterDlg::slotRowTypeChanged(int row) { Q_D(KReportConfigurationFilterDlg); d->m_tabRowColPivot->ui->m_checkTotalColumn->setEnabled(row == 0); } void KReportConfigurationFilterDlg::slotColumnTypeChanged(int row) { Q_D(KReportConfigurationFilterDlg); if ((d->m_tabRowColPivot->ui->m_comboBudget->isEnabled() && row < 2)) { d->m_tabRange->ui->m_comboColumns->setCurrentItem(i18nc("@item the columns will display monthly data", "Monthly"), false); } } void KReportConfigurationFilterDlg::slotUpdateColumnsCombo() { Q_D(KReportConfigurationFilterDlg); const int monthlyIndex = 2; const int incomeExpenseIndex = 0; const bool isIncomeExpenseForecast = d->m_currentState.isIncludingForecast() && d->m_tabRowColPivot->ui->m_comboRows->currentIndex() == incomeExpenseIndex; if (isIncomeExpenseForecast && d->m_tabRange->ui->m_comboColumns->currentIndex() != monthlyIndex) { d->m_tabRange->ui->m_comboColumns->setCurrentItem(i18nc("@item the columns will display monthly data", "Monthly"), false); } } void KReportConfigurationFilterDlg::slotUpdateColumnsCombo(int) { slotUpdateColumnsCombo(); } void KReportConfigurationFilterDlg::slotLogAxisChanged(int state) { Q_D(KReportConfigurationFilterDlg); if (state == Qt::Checked) d->m_tabRange->setRangeLogarythmic(true); else d->m_tabRange->setRangeLogarythmic(false); } void KReportConfigurationFilterDlg::slotReset() { Q_D(KReportConfigurationFilterDlg); // // Set up the widget from the initial filter // d->m_currentState = d->m_initialState; // // Report Properties // d->m_tabGeneral->ui->m_editName->setText(d->m_initialState.name()); d->m_tabGeneral->ui->m_editComment->setText(d->m_initialState.comment()); d->m_tabGeneral->ui->m_checkCurrency->setChecked(d->m_initialState.isConvertCurrency()); d->m_tabGeneral->ui->m_checkFavorite->setChecked(d->m_initialState.isFavorite()); if (d->m_initialState.isIncludingPrice() || d->m_initialState.isSkippingZero()) { d->m_tabGeneral->ui->m_skipZero->setChecked(d->m_initialState.isSkippingZero()); } else { d->m_tabGeneral->ui->m_skipZero->setEnabled(false); } if (d->m_tabRowColPivot) { KComboBox *combo = d->m_tabRowColPivot->ui->m_comboDetail; switch (d->m_initialState.detailLevel()) { case MyMoneyReport::eDetailNone: case MyMoneyReport::eDetailEnd: case MyMoneyReport::eDetailAll: combo->setCurrentItem(i18nc("All accounts", "All"), false); break; case MyMoneyReport::eDetailTop: combo->setCurrentItem(i18n("Top-Level"), false); break; case MyMoneyReport::eDetailGroup: combo->setCurrentItem(i18n("Groups"), false); break; case MyMoneyReport::eDetailTotal: combo->setCurrentItem(i18n("Totals"), false); break; } combo = d->m_tabRowColPivot->ui->m_comboRows; switch (d->m_initialState.rowType()) { case MyMoneyReport::eExpenseIncome: case MyMoneyReport::eBudget: case MyMoneyReport::eBudgetActual: combo->setCurrentItem(i18n("Income & Expenses"), false); // income / expense break; default: combo->setCurrentItem(i18n("Assets & Liabilities"), false); // asset / liability break; } d->m_tabRowColPivot->ui->m_checkTotalColumn->setChecked(d->m_initialState.isShowingRowTotals()); d->m_tabRowColPivot->ui->m_checkTotalRow->setChecked(d->m_initialState.isShowingColumnTotals()); slotRowTypeChanged(combo->currentIndex()); //load budgets combo if (d->m_initialState.rowType() == MyMoneyReport::eBudget || d->m_initialState.rowType() == MyMoneyReport::eBudgetActual) { d->m_tabRowColPivot->ui->m_comboRows->setEnabled(false); d->m_tabRowColPivot->ui->m_budgetFrame->setEnabled(!d->m_budgets.empty()); auto i = 0; for (QVector::const_iterator it_b = d->m_budgets.constBegin(); it_b != d->m_budgets.constEnd(); ++it_b) { d->m_tabRowColPivot->ui->m_comboBudget->insertItem((*it_b).name(), i); //set the current selected item if ((d->m_initialState.budget() == "Any" && (*it_b).budgetStart().year() == QDate::currentDate().year()) || d->m_initialState.budget() == (*it_b).id()) d->m_tabRowColPivot->ui->m_comboBudget->setCurrentItem(i); i++; } } //set moving average days spinbox QSpinBox *spinbox = d->m_tabRowColPivot->ui->m_movingAverageDays; spinbox->setEnabled(d->m_initialState.isIncludingMovingAverage()); if (d->m_initialState.isIncludingMovingAverage()) { spinbox->setValue(d->m_initialState.movingAverageDays()); } d->m_tabRowColPivot->ui->m_checkScheduled->setChecked(d->m_initialState.isIncludingSchedules()); d->m_tabRowColPivot->ui->m_checkTransfers->setChecked(d->m_initialState.isIncludingTransfers()); d->m_tabRowColPivot->ui->m_checkUnused->setChecked(d->m_initialState.isIncludingUnusedAccounts()); } else if (d->m_tabRowColQuery) { KComboBox *combo = d->m_tabRowColQuery->ui->m_comboOrganizeBy; switch (d->m_initialState.rowType()) { case MyMoneyReport::eNoColumns: case MyMoneyReport::eCategory: combo->setCurrentItem(i18n("Categories"), false); break; case MyMoneyReport::eTopCategory: combo->setCurrentItem(i18n("Top Categories"), false); break; case MyMoneyReport::eTag: combo->setCurrentItem(i18n("Tags"), false); break; case MyMoneyReport::ePayee: combo->setCurrentItem(i18n("Payees"), false); break; case MyMoneyReport::eAccount: combo->setCurrentItem(i18n("Accounts"), false); break; case MyMoneyReport::eTopAccount: combo->setCurrentItem(i18n("Top Accounts"), false); break; case MyMoneyReport::eMonth: combo->setCurrentItem(i18n("Month"), false); break; case MyMoneyReport::eWeek: combo->setCurrentItem(i18n("Week"), false); break; default: throw MYMONEYEXCEPTION("KReportConfigurationFilterDlg::slotReset(): QueryTable report has invalid rowtype"); } unsigned qc = d->m_initialState.queryColumns(); d->m_tabRowColQuery->ui->m_checkNumber->setChecked(qc & MyMoneyReport::eQCnumber); d->m_tabRowColQuery->ui->m_checkPayee->setChecked(qc & MyMoneyReport::eQCpayee); d->m_tabRowColQuery->ui->m_checkTag->setChecked(qc & MyMoneyReport::eQCtag); d->m_tabRowColQuery->ui->m_checkCategory->setChecked(qc & MyMoneyReport::eQCcategory); d->m_tabRowColQuery->ui->m_checkMemo->setChecked(qc & MyMoneyReport::eQCmemo); d->m_tabRowColQuery->ui->m_checkAccount->setChecked(qc & MyMoneyReport::eQCaccount); d->m_tabRowColQuery->ui->m_checkReconciled->setChecked(qc & MyMoneyReport::eQCreconciled); d->m_tabRowColQuery->ui->m_checkAction->setChecked(qc & MyMoneyReport::eQCaction); d->m_tabRowColQuery->ui->m_checkShares->setChecked(qc & MyMoneyReport::eQCshares); d->m_tabRowColQuery->ui->m_checkPrice->setChecked(qc & MyMoneyReport::eQCprice); d->m_tabRowColQuery->ui->m_checkBalance->setChecked(qc & MyMoneyReport::eQCbalance); d->m_tabRowColQuery->ui->m_checkTax->setChecked(d->m_initialState.isTax()); d->m_tabRowColQuery->ui->m_checkInvestments->setChecked(d->m_initialState.isInvestmentsOnly()); d->m_tabRowColQuery->ui->m_checkLoans->setChecked(d->m_initialState.isLoansOnly()); d->m_tabRowColQuery->ui->m_checkHideTransactions->setChecked(d->m_initialState.isHideTransactions()); d->m_tabRowColQuery->ui->m_checkHideTotals->setChecked(!d->m_initialState.isShowingColumnTotals()); d->m_tabRowColQuery->ui->m_checkHideSplitDetails->setEnabled(!d->m_initialState.isHideTransactions()); d->m_tabRowColQuery->ui->m_checkHideSplitDetails->setChecked (d->m_initialState.detailLevel() == MyMoneyReport::eDetailNone || d->m_initialState.isHideTransactions()); } if (d->m_tabChart) { KMyMoneyGeneralCombo* combo = d->m_tabChart->ui->m_comboType; switch (d->m_initialState.chartType()) { case MyMoneyReport::eChartNone: combo->setCurrentItem(MyMoneyReport::eChartLine); break; case MyMoneyReport::eChartLine: case MyMoneyReport::eChartBar: case MyMoneyReport::eChartStackedBar: case MyMoneyReport::eChartPie: case MyMoneyReport::eChartRing: combo->setCurrentItem(d->m_initialState.chartType()); break; default: throw MYMONEYEXCEPTION("KReportConfigurationFilterDlg::slotReset(): Report has invalid charttype"); } d->m_tabChart->ui->m_checkCHGridLines->setChecked(d->m_initialState.isChartCHGridLines()); d->m_tabChart->ui->m_checkSVGridLines->setChecked(d->m_initialState.isChartSVGridLines()); d->m_tabChart->ui->m_checkValues->setChecked(d->m_initialState.isChartDataLabels()); d->m_tabChart->ui->m_checkShowChart->setChecked(d->m_initialState.isChartByDefault()); d->m_tabChart->ui->m_lineWidth->setValue(d->m_initialState.chartLineWidth()); d->m_tabChart->ui->m_logYaxis->setChecked(d->m_initialState.isLogYAxis()); } if (d->m_tabRange) { d->m_tabRange->ui->m_dataRangeStart->setText(d->m_initialState.dataRangeStart()); d->m_tabRange->ui->m_dataRangeEnd->setText(d->m_initialState.dataRangeEnd()); d->m_tabRange->ui->m_dataMajorTick->setText(d->m_initialState.dataMajorTick()); d->m_tabRange->ui->m_dataMinorTick->setText(d->m_initialState.dataMinorTick()); d->m_tabRange->ui->m_yLabelsPrecision->setValue(d->m_initialState.yLabelsPrecision()); d->m_tabRange->ui->m_dataLock->setCurrentIndex((int)d->m_initialState.dataFilter()); KComboBox *combo = d->m_tabRange->ui->m_comboColumns; if (d->m_initialState.isColumnsAreDays()) { switch (d->m_initialState.columnType()) { case MyMoneyReport::eNoColumns: case MyMoneyReport::eDays: combo->setCurrentItem(i18nc("@item the columns will display daily data", "Daily"), false); break; case MyMoneyReport::eWeeks: combo->setCurrentItem(i18nc("@item the columns will display weekly data", "Weekly"), false); break; default: break; } } else { switch (d->m_initialState.columnType()) { case MyMoneyReport::eNoColumns: case MyMoneyReport::eMonths: combo->setCurrentItem(i18nc("@item the columns will display monthly data", "Monthly"), false); break; case MyMoneyReport::eBiMonths: combo->setCurrentItem(i18nc("@item the columns will display bi-monthly data", "Bi-Monthly"), false); break; case MyMoneyReport::eQuarters: combo->setCurrentItem(i18nc("@item the columns will display quarterly data", "Quarterly"), false); break; case MyMoneyReport::eYears: combo->setCurrentItem(i18nc("@item the columns will display yearly data", "Yearly"), false); break; default: break; } } } if (d->m_tabCapitalGain) { d->m_tabCapitalGain->ui->m_termSeparator->setDate(d->m_initialState.termSeparator()); d->m_tabCapitalGain->ui->m_showSTLTCapitalGains->setChecked(d->m_initialState.isShowingSTLTCapitalGains()); d->m_tabCapitalGain->ui->m_settlementPeriod->setValue(d->m_initialState.settlementPeriod()); d->m_tabCapitalGain->ui->m_checkHideTotals->setChecked(!d->m_initialState.isShowingColumnTotals()); d->m_tabCapitalGain->ui->m_investmentSum->blockSignals(true); d->m_tabCapitalGain->ui->m_investmentSum->clear(); d->m_tabCapitalGain->ui->m_investmentSum->addItem(i18n("Only owned"), MyMoneyReport::eSumOwned); d->m_tabCapitalGain->ui->m_investmentSum->addItem(i18n("Only sold"), MyMoneyReport::eSumSold); d->m_tabCapitalGain->ui->m_investmentSum->blockSignals(false); d->m_tabCapitalGain->ui->m_investmentSum->setCurrentIndex(d->m_tabCapitalGain->ui->m_investmentSum->findData(d->m_initialState.investmentSum())); } if (d->m_tabPerformance) { d->m_tabPerformance->ui->m_checkHideTotals->setChecked(!d->m_initialState.isShowingColumnTotals()); d->m_tabPerformance->ui->m_investmentSum->blockSignals(true); d->m_tabPerformance->ui->m_investmentSum->clear(); d->m_tabPerformance->ui->m_investmentSum->addItem(i18n("From period"), MyMoneyReport::eSumPeriod); d->m_tabPerformance->ui->m_investmentSum->addItem(i18n("Owned and sold"), MyMoneyReport::eSumOwnedAndSold); d->m_tabPerformance->ui->m_investmentSum->addItem(i18n("Only owned"), MyMoneyReport::eSumOwned); d->m_tabPerformance->ui->m_investmentSum->addItem(i18n("Only sold"), MyMoneyReport::eSumSold); d->m_tabPerformance->ui->m_investmentSum->blockSignals(false); d->m_tabPerformance->ui->m_investmentSum->setCurrentIndex(d->m_tabPerformance->ui->m_investmentSum->findData(d->m_initialState.investmentSum())); } // // Text Filter // QRegExp textfilter; if (d->m_initialState.textFilter(textfilter)) { d->ui->m_textEdit->setText(textfilter.pattern()); d->ui->m_caseSensitive->setChecked(Qt::CaseSensitive == textfilter.caseSensitivity()); d->ui->m_regExp->setChecked(QRegExp::RegExp == textfilter.patternSyntax()); d->ui->m_textNegate->setCurrentIndex(d->m_initialState.isInvertingText()); } // // Type & State Filters // int type; if (d->m_initialState.firstType(type)) d->ui->m_typeBox->setCurrentIndex(type); int state; if (d->m_initialState.firstState(state)) d->ui->m_stateBox->setCurrentIndex(state); // // Number Filter // QString nrFrom, nrTo; if (d->m_initialState.numberFilter(nrFrom, nrTo)) { if (nrFrom == nrTo) { d->ui->m_nrEdit->setEnabled(true); d->ui->m_nrFromEdit->setEnabled(false); d->ui->m_nrToEdit->setEnabled(false); d->ui->m_nrEdit->setText(nrFrom); d->ui->m_nrFromEdit->setText(QString()); d->ui->m_nrToEdit->setText(QString()); d->ui->m_nrButton->setChecked(true); d->ui->m_nrRangeButton->setChecked(false); } else { d->ui->m_nrEdit->setEnabled(false); d->ui->m_nrFromEdit->setEnabled(true); d->ui->m_nrToEdit->setEnabled(false); d->ui->m_nrEdit->setText(QString()); d->ui->m_nrFromEdit->setText(nrFrom); d->ui->m_nrToEdit->setText(nrTo); d->ui->m_nrButton->setChecked(false); d->ui->m_nrRangeButton->setChecked(true); } } else { d->ui->m_nrEdit->setEnabled(true); d->ui->m_nrFromEdit->setEnabled(false); d->ui->m_nrToEdit->setEnabled(false); d->ui->m_nrEdit->setText(QString()); d->ui->m_nrFromEdit->setText(QString()); d->ui->m_nrToEdit->setText(QString()); d->ui->m_nrButton->setChecked(true); d->ui->m_nrRangeButton->setChecked(false); } // // Amount Filter // MyMoneyMoney from, to; if (d->m_initialState.amountFilter(from, to)) { // bool getAmountFilter(MyMoneyMoney&,MyMoneyMoney&); if (from == to) { d->ui->m_amountEdit->setEnabled(true); d->ui->m_amountFromEdit->setEnabled(false); d->ui->m_amountToEdit->setEnabled(false); d->ui->m_amountEdit->loadText(QString::number(from.toDouble())); d->ui->m_amountFromEdit->loadText(QString()); d->ui->m_amountToEdit->loadText(QString()); d->ui->m_amountButton->setChecked(true); d->ui->m_amountRangeButton->setChecked(false); } else { d->ui->m_amountEdit->setEnabled(false); d->ui->m_amountFromEdit->setEnabled(true); d->ui->m_amountToEdit->setEnabled(true); d->ui->m_amountEdit->loadText(QString()); d->ui->m_amountFromEdit->loadText(QString::number(from.toDouble())); d->ui->m_amountToEdit->loadText(QString::number(to.toDouble())); d->ui->m_amountButton->setChecked(false); d->ui->m_amountRangeButton->setChecked(true); } } else { d->ui->m_amountEdit->setEnabled(true); d->ui->m_amountFromEdit->setEnabled(false); d->ui->m_amountToEdit->setEnabled(false); d->ui->m_amountEdit->loadText(QString()); d->ui->m_amountFromEdit->loadText(QString()); d->ui->m_amountToEdit->loadText(QString()); d->ui->m_amountButton->setChecked(true); d->ui->m_amountRangeButton->setChecked(false); } // // Payees Filter // QStringList payees; if (d->m_initialState.payees(payees)) { if (payees.empty()) { d->ui->m_emptyPayeesButton->setChecked(true); } else { d->selectAllItems(d->ui->m_payeesView, false); d->selectItems(d->ui->m_payeesView, payees, true); } } else { d->selectAllItems(d->ui->m_payeesView, true); } // // Tags Filter // QStringList tags; if (d->m_initialState.tags(tags)) { if (tags.empty()) { d->ui->m_emptyTagsButton->setChecked(true); } else { d->selectAllItems(d->ui->m_tagsView, false); d->selectItems(d->ui->m_tagsView, tags, true); } } else { d->selectAllItems(d->ui->m_tagsView, true); } // // Accounts Filter // QStringList accounts; if (d->m_initialState.accounts(accounts)) { d->ui->m_accountsView->selectAllItems(false); d->ui->m_accountsView->selectItems(accounts, true); } else d->ui->m_accountsView->selectAllItems(true); // // Categories Filter // if (d->m_initialState.categories(accounts)) { d->ui->m_categoriesView->selectAllItems(false); d->ui->m_categoriesView->selectItems(accounts, true); } else d->ui->m_categoriesView->selectAllItems(true); // // Date Filter // // the following call implies a call to slotUpdateSelections, // that's why we call it last d->m_initialState.updateDateFilter(); QDate dateFrom, dateTo; if (d->m_initialState.dateFilter(dateFrom, dateTo)) { if (d->m_initialState.isDateUserDefined()) { d->m_dateRange->setDateRange(dateFrom, dateTo); } else { d->m_dateRange->setDateRange(d->m_initialState.dateRange()); } } else { d->m_dateRange->setDateRange(eMyMoney::TransactionFilter::Date::All); } slotRightSize(); } void KReportConfigurationFilterDlg::slotShowHelp() { KHelpClient::invokeHelp("details.reports.config"); } //TODO Fix the reports and engine to include transfers even if categories are filtered - bug #1523508 void KReportConfigurationFilterDlg::slotUpdateCheckTransfers() { Q_D(KReportConfigurationFilterDlg); auto cb = d->m_tabRowColPivot->ui->m_checkTransfers; if (!d->ui->m_categoriesView->allItemsSelected()) { cb->setChecked(false); cb->setDisabled(true); } else { cb->setEnabled(true); } } diff --git a/kmymoney/dialogs/ksplittransactiondlg.cpp b/kmymoney/dialogs/ksplittransactiondlg.cpp index 23993691e..0bdf6e599 100644 --- a/kmymoney/dialogs/ksplittransactiondlg.cpp +++ b/kmymoney/dialogs/ksplittransactiondlg.cpp @@ -1,566 +1,567 @@ /*************************************************************************** ksplittransactiondlg.cpp - description ------------------- begin : Thu Jan 10 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 "ksplittransactiondlg.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "ui_ksplittransactiondlg.h" #include "ui_ksplitcorrectiondlg.h" #include "mymoneyfile.h" #include "kmymoneysplittable.h" #include "mymoneymoney.h" +#include "mymoneyexception.h" #include "mymoneyaccount.h" #include "mymoneysecurity.h" #include "mymoneysplit.h" #include "mymoneytransaction.h" #include "icons/icons.h" using namespace Icons; KSplitCorrectionDlg::KSplitCorrectionDlg(QWidget *parent) : QDialog(parent), ui(new Ui::KSplitCorrectionDlg) { ui->setupUi(this); } KSplitCorrectionDlg::~KSplitCorrectionDlg() { delete ui; } class KSplitTransactionDlgPrivate { Q_DISABLE_COPY(KSplitTransactionDlgPrivate) Q_DECLARE_PUBLIC(KSplitTransactionDlg) public: explicit KSplitTransactionDlgPrivate(KSplitTransactionDlg *qq) : q_ptr(qq), ui(new Ui::KSplitTransactionDlg) { } ~KSplitTransactionDlgPrivate() { delete ui; } void init(const MyMoneyTransaction& t, const QMap& priceInfo) { Q_Q(KSplitTransactionDlg); ui->setupUi(q); q->setModal(true); auto okButton = ui->buttonBox->button(QDialogButtonBox::Ok); okButton->setDefault(true); okButton->setShortcut(Qt::CTRL | Qt::Key_Return); auto user1Button = new QPushButton; ui->buttonBox->addButton(user1Button, QDialogButtonBox::ActionRole); auto user2Button = new QPushButton; ui->buttonBox->addButton(user2Button, QDialogButtonBox::ActionRole); auto user3Button = new QPushButton; ui->buttonBox->addButton(user3Button, QDialogButtonBox::ActionRole); //set custom buttons //clearAll button user1Button->setText(i18n("Clear &All")); user1Button->setToolTip(i18n("Clear all splits")); user1Button->setWhatsThis(i18n("Use this to clear all splits of this transaction")); user1Button->setIcon(QIcon::fromTheme(g_Icons[Icon::EditClear])); //clearZero button user2Button->setText(i18n("Clear &Zero")); user2Button->setToolTip(i18n("Removes all splits that have a value of zero")); user2Button->setIcon(QIcon::fromTheme(g_Icons[Icon::EditClear])); //merge button user3Button->setText(i18n("&Merge")); user3Button->setToolTip(i18n("Merges splits with the same category to one split")); user3Button->setWhatsThis(i18n("In case you have multiple split entries to the same category and you like to keep them as a single split")); // make finish the default ui->buttonBox->button(QDialogButtonBox::Cancel)->setDefault(true); // setup the focus ui->buttonBox->button(QDialogButtonBox::Cancel)->setFocusPolicy(Qt::NoFocus); okButton->setFocusPolicy(Qt::NoFocus); user1Button->setFocusPolicy(Qt::NoFocus); // q->connect signals with slots q->connect(ui->transactionsTable, &KMyMoneySplitTable::transactionChanged, q, &KSplitTransactionDlg::slotSetTransaction); q->connect(ui->transactionsTable, &KMyMoneySplitTable::createCategory, q, &KSplitTransactionDlg::slotCreateCategory); q->connect(ui->transactionsTable, &KMyMoneySplitTable::objectCreation, q, &KSplitTransactionDlg::objectCreation); q->connect(ui->transactionsTable, &KMyMoneySplitTable::returnPressed, q, &KSplitTransactionDlg::accept); q->connect(ui->transactionsTable, &KMyMoneySplitTable::escapePressed, q, &KSplitTransactionDlg::reject); q->connect(ui->transactionsTable, &KMyMoneySplitTable::editStarted, q, &KSplitTransactionDlg::slotEditStarted); q->connect(ui->transactionsTable, &KMyMoneySplitTable::editFinished, q, &KSplitTransactionDlg::slotUpdateButtons); q->connect(ui->buttonBox->button(QDialogButtonBox::Cancel), &QAbstractButton::clicked, q, &KSplitTransactionDlg::reject); q->connect(ui->buttonBox->button(QDialogButtonBox::Ok), &QAbstractButton::clicked, q, &KSplitTransactionDlg::accept); q->connect(user1Button, &QAbstractButton::clicked, q, &KSplitTransactionDlg::slotClearAllSplits); q->connect(user3Button, &QAbstractButton::clicked, q, &KSplitTransactionDlg::slotMergeSplits); q->connect(user2Button, &QAbstractButton::clicked, q, &KSplitTransactionDlg::slotClearUnusedSplits); // setup the precision try { auto currency = MyMoneyFile::instance()->currency(t.commodity()); m_precision = MyMoneyMoney::denomToPrec(m_account.fraction(currency)); } catch (const MyMoneyException &) { } q->slotSetTransaction(t); // pass on those vars ui->transactionsTable->setup(priceInfo, m_precision); QSize size(q->width(), q->height()); KConfigGroup grp = KSharedConfig::openConfig()->group("SplitTransactionEditor"); size = grp.readEntry("Geometry", size); size.setHeight(size.height() - 1); q->resize(size.expandedTo(q->minimumSizeHint())); // Trick: it seems, that the initial sizing of the dialog does // not work correctly. At least, the columns do not get displayed // correct. Reason: the return value of ui->transactionsTable->visibleWidth() // is incorrect. If the widget is visible, resizing works correctly. // So, we let the dialog show up and resize it then. It's not really // clean, but the only way I got the damned thing working. QTimer::singleShot(10, q, SLOT(initSize())); } /** * This method updates the display of the sums below the register */ void updateSums() { Q_Q(KSplitTransactionDlg); MyMoneyMoney splits(q->splitsValue()); if (m_amountValid == false) { m_split.setValue(-splits); m_transaction.modifySplit(m_split); } ui->splitSum->setText("" + splits.formatMoney(QString(), m_precision) + ' '); ui->splitUnassigned->setText("" + q->diffAmount().formatMoney(QString(), m_precision) + ' '); ui->transactionAmount->setText("" + (-m_split.value()).formatMoney(QString(), m_precision) + ' '); } KSplitTransactionDlg *q_ptr; Ui::KSplitTransactionDlg *ui; QDialogButtonBox *m_buttonBox; /** * This member keeps a copy of the current selected transaction */ MyMoneyTransaction m_transaction; /** * This member keeps a copy of the currently selected account */ MyMoneyAccount m_account; /** * This member keeps a copy of the currently selected split */ MyMoneySplit m_split; /** * This member keeps the precision for the values */ int m_precision; /** * flag that shows that the amount specified in the constructor * should be used as fix value (true) or if it can be changed (false) */ bool m_amountValid; /** * This member keeps track if the current transaction is of type * deposit (true) or withdrawal (false). */ bool m_isDeposit; /** * This member keeps the amount that will be assigned to all the * splits that are marked 'will be calculated'. */ MyMoneyMoney m_calculatedValue; }; KSplitTransactionDlg::KSplitTransactionDlg(const MyMoneyTransaction& t, const MyMoneySplit& s, const MyMoneyAccount& acc, const bool amountValid, const bool deposit, const MyMoneyMoney& calculatedValue, const QMap& priceInfo, QWidget* parent) : QDialog(parent), d_ptr(new KSplitTransactionDlgPrivate(this)) { Q_D(KSplitTransactionDlg); d->ui->buttonBox = nullptr; d->m_account = acc; d->m_split = s; d->m_precision = 2; d->m_amountValid = amountValid; d->m_isDeposit = deposit; d->m_calculatedValue = calculatedValue; d->init(t, priceInfo); } KSplitTransactionDlg::~KSplitTransactionDlg() { Q_D(KSplitTransactionDlg); auto grp = KSharedConfig::openConfig()->group("SplitTransactionEditor"); grp.writeEntry("Geometry", size()); delete d; } int KSplitTransactionDlg::exec() { Q_D(KSplitTransactionDlg); // for deposits, we invert the sign of all splits. // don't forget to revert when we're done ;-) if (d->m_isDeposit) { for (auto i = 0; i < d->m_transaction.splits().count(); ++i) { MyMoneySplit split = d->m_transaction.splits()[i]; split.setValue(-split.value()); split.setShares(-split.shares()); d->m_transaction.modifySplit(split); } } int rc; do { d->ui->transactionsTable->setFocus(); // initialize the display d->ui->transactionsTable->setTransaction(d->m_transaction, d->m_split, d->m_account); d->updateSums(); rc = QDialog::exec(); if (rc == Accepted) { if (!diffAmount().isZero()) { QPointer corrDlg = new KSplitCorrectionDlg(this); connect(corrDlg->ui->buttonBox, &QDialogButtonBox::accepted, corrDlg.data(), &QDialog::accept); connect(corrDlg->ui->buttonBox, &QDialogButtonBox::rejected, corrDlg.data(), &QDialog::reject); corrDlg->ui->buttonGroup->setId(corrDlg->ui->continueBtn, 0); corrDlg->ui->buttonGroup->setId(corrDlg->ui->changeBtn, 1); corrDlg->ui->buttonGroup->setId(corrDlg->ui->distributeBtn, 2); corrDlg->ui->buttonGroup->setId(corrDlg->ui->leaveBtn, 3); MyMoneySplit split = d->m_transaction.splits()[0]; QString total = (-split.value()).formatMoney(QString(), d->m_precision); QString sums = splitsValue().formatMoney(QString(), d->m_precision); QString diff = diffAmount().formatMoney(QString(), d->m_precision); // now modify the text items of the dialog to contain the correct values QString q = i18n("The total amount of this transaction is %1 while " "the sum of the splits is %2. The remaining %3 are " "unassigned.", total, sums, diff); corrDlg->ui->explanation->setText(q); q = i18n("Change &total amount of transaction to %1.", sums); corrDlg->ui->changeBtn->setText(q); q = i18n("&Distribute difference of %1 among all splits.", diff); corrDlg->ui->distributeBtn->setText(q); // FIXME remove the following line once distribution among // all splits is implemented corrDlg->ui->distributeBtn->hide(); // if we have only two splits left, we don't allow leaving sth. unassigned. if (d->m_transaction.splitCount() < 3) { q = i18n("&Leave total amount of transaction at %1.", total); } else { q = i18n("&Leave %1 unassigned.", diff); } corrDlg->ui->leaveBtn->setText(q); if ((rc = corrDlg->exec()) == Accepted) { switch (corrDlg->ui->buttonGroup->checkedId()) { case 0: // continue to edit rc = Rejected; break; case 1: // modify total split.setValue(-splitsValue()); split.setShares(-splitsValue()); d->m_transaction.modifySplit(split); break; case 2: // distribute difference qDebug("distribution of difference not yet supported in KSplitTransactionDlg::slotFinishClicked()"); break; case 3: // leave unassigned break; } } delete corrDlg; } } else break; } while (rc != Accepted); // for deposits, we inverted the sign of all splits. // now we revert it back, so that things are left correct if (d->m_isDeposit) { for (auto i = 0; i < d->m_transaction.splits().count(); ++i) { auto split = d->m_transaction.splits()[i]; split.setValue(-split.value()); split.setShares(-split.shares()); d->m_transaction.modifySplit(split); } } return rc; } void KSplitTransactionDlg::initSize() { QDialog::resize(width(), height() + 1); } void KSplitTransactionDlg::accept() { Q_D(KSplitTransactionDlg); d->ui->transactionsTable->slotCancelEdit(); QDialog::accept(); } void KSplitTransactionDlg::reject() { Q_D(KSplitTransactionDlg); // cancel any edit activity in the split register d->ui->transactionsTable->slotCancelEdit(); QDialog::reject(); } void KSplitTransactionDlg::slotClearAllSplits() { Q_D(KSplitTransactionDlg); int answer; answer = KMessageBox::warningContinueCancel(this, i18n("You are about to delete all splits of this transaction. " "Do you really want to continue?"), i18n("KMyMoney")); if (answer == KMessageBox::Continue) { d->ui->transactionsTable->slotCancelEdit(); QList list = d->ui->transactionsTable->getSplits(d->m_transaction); QList::ConstIterator it; // clear all but the one referencing the account for (it = list.constBegin(); it != list.constEnd(); ++it) { d->m_transaction.removeSplit(*it); } d->ui->transactionsTable->setTransaction(d->m_transaction, d->m_split, d->m_account); slotSetTransaction(d->m_transaction); } } void KSplitTransactionDlg::slotClearUnusedSplits() { Q_D(KSplitTransactionDlg); QList list = d->ui->transactionsTable->getSplits(d->m_transaction); QList::ConstIterator it; try { // remove all splits that don't have a value assigned for (it = list.constBegin(); it != list.constEnd(); ++it) { if ((*it).shares().isZero()) { d->m_transaction.removeSplit(*it); } } d->ui->transactionsTable->setTransaction(d->m_transaction, d->m_split, d->m_account); slotSetTransaction(d->m_transaction); } catch (const MyMoneyException &) { } } void KSplitTransactionDlg::slotMergeSplits() { Q_D(KSplitTransactionDlg); try { // collect all splits, merge them if needed and remove from transaction QList splits; foreach (const auto lsplit, d->ui->transactionsTable->getSplits(d->m_transaction)) { auto found = false; for (auto& split : splits) { if (split.accountId() == lsplit.accountId() && split.memo().isEmpty() && lsplit.memo().isEmpty()) { split.setShares(lsplit.shares() + split.shares()); split.setValue(lsplit.value() + split.value()); found = true; break; } } if (!found) splits << lsplit; d->m_transaction.removeSplit(lsplit); } // now add them back to the transaction for (auto& split : splits) { split.clearId(); d->m_transaction.addSplit(split); } d->ui->transactionsTable->setTransaction(d->m_transaction, d->m_split, d->m_account); slotSetTransaction(d->m_transaction); } catch (const MyMoneyException &) { } } void KSplitTransactionDlg::slotSetTransaction(const MyMoneyTransaction& t) { Q_D(KSplitTransactionDlg); d->m_transaction = t; slotUpdateButtons(); d->updateSums(); } void KSplitTransactionDlg::slotUpdateButtons() { Q_D(KSplitTransactionDlg); QList list = d->ui->transactionsTable->getSplits(d->m_transaction); // check if we can merge splits or not, have zero splits or not QMap splits; bool haveZeroSplit = false; for (QList::const_iterator it = list.constBegin(); it != list.constEnd(); ++it) { splits[(*it).accountId()]++; if (((*it).id() != d->m_split.id()) && ((*it).shares().isZero())) haveZeroSplit = true; } QMap::const_iterator it_s; for (it_s = splits.constBegin(); it_s != splits.constEnd(); ++it_s) { if ((*it_s) > 1) break; } d->ui->buttonBox->buttons().at(4)->setEnabled(it_s != splits.constEnd()); d->ui->buttonBox->buttons().at(3)->setEnabled(haveZeroSplit); } void KSplitTransactionDlg::slotEditStarted() { Q_D(KSplitTransactionDlg); d->ui->buttonBox->buttons().at(4)->setEnabled(false); d->ui->buttonBox->buttons().at(3)->setEnabled(false); } MyMoneyMoney KSplitTransactionDlg::splitsValue() { Q_D(KSplitTransactionDlg); MyMoneyMoney splitsValue(d->m_calculatedValue); QList list = d->ui->transactionsTable->getSplits(d->m_transaction); QList::ConstIterator it; // calculate the current sum of all split parts for (it = list.constBegin(); it != list.constEnd(); ++it) { if ((*it).value() != MyMoneyMoney::autoCalc) splitsValue += (*it).value(); } return splitsValue; } MyMoneyTransaction KSplitTransactionDlg::transaction() const { Q_D(const KSplitTransactionDlg); return d->m_transaction; } MyMoneyMoney KSplitTransactionDlg::diffAmount() { Q_D(KSplitTransactionDlg); MyMoneyMoney diff; // if there is an amount specified in the transaction, we need to calculate the // difference, otherwise we display the difference as 0 and display the same sum. if (d->m_amountValid) { MyMoneySplit split = d->m_transaction.splits()[0]; diff = -(splitsValue() + split.value()); } return diff; } void KSplitTransactionDlg::slotCreateCategory(const QString& name, QString& id) { Q_D(KSplitTransactionDlg); MyMoneyAccount acc, parent; acc.setName(name); if (d->m_isDeposit) parent = MyMoneyFile::instance()->income(); else parent = MyMoneyFile::instance()->expense(); // TODO extract possible first part of a hierarchy and check if it is one // of our top categories. If so, remove it and select the parent // according to this information. emit createCategory(acc, parent); // return id id = acc.id(); } diff --git a/kmymoney/dialogs/transactioneditor.cpp b/kmymoney/dialogs/transactioneditor.cpp index f6a25bb60..4f2e52251 100644 --- a/kmymoney/dialogs/transactioneditor.cpp +++ b/kmymoney/dialogs/transactioneditor.cpp @@ -1,826 +1,827 @@ /*************************************************************************** transactioneditor.cpp ---------- begin : Wed Jun 07 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 "transactioneditor.h" #include "transactioneditor_p.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "kmymoneytagcombo.h" #include "ktagcontainer.h" #include "tabbar.h" #include "mymoneyutils.h" +#include "mymoneyexception.h" #include "kmymoneycategory.h" #include "kmymoneymvccombo.h" #include "kmymoneyedit.h" #include "kmymoneylineedit.h" #include "mymoneyfile.h" #include "mymoneyprice.h" #include "mymoneysecurity.h" #include "kmymoneyutils.h" #include "kmymoneycompletion.h" #include "transaction.h" #include "transactionform.h" #include "kmymoneyglobalsettings.h" #include "transactioneditorcontainer.h" #include "kcurrencycalculator.h" #include "icons.h" using namespace KMyMoneyRegister; using namespace KMyMoneyTransactionForm; using namespace Icons; TransactionEditor::TransactionEditor() : d_ptr(new TransactionEditorPrivate(this)) { Q_D(TransactionEditor); d->init(); } TransactionEditor::TransactionEditor(TransactionEditorPrivate &dd, TransactionEditorContainer* regForm, KMyMoneyRegister::Transaction* item, const KMyMoneyRegister::SelectedTransactions& list, const QDate& lastPostDate) : d_ptr(&dd) // d_ptr(new TransactionEditorPrivate) { Q_D(TransactionEditor); d->m_paymentMethod = eMyMoney::Schedule::PaymentType::Any; d->m_transactions = list; d->m_regForm = regForm; d->m_item = item; d->m_transaction = item->transaction(); d->m_split = item->split(); d->m_lastPostDate = lastPostDate; d->m_initialAction = eWidgets::eRegister::Action::None; d->m_openEditSplits = false; d->m_memoChanged = false; d->m_item->startEditMode(); connect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, this, static_cast(&TransactionEditor::slotUpdateAccount)); } TransactionEditor::TransactionEditor(TransactionEditorPrivate &dd) : d_ptr(&dd) { Q_D(TransactionEditor); d->init(); } TransactionEditor::~TransactionEditor() { Q_D(TransactionEditor); // Make sure the widgets do not send out signals to the editor anymore // After all, the editor is about to die //disconnect first tagCombo: KTagContainer *w = dynamic_cast(haveWidget("tag")); if (w && w->tagCombo()) { w->tagCombo()->disconnect(this); } QMap::iterator it_w; for (it_w = d->m_editWidgets.begin(); it_w != d->m_editWidgets.end(); ++it_w) { (*it_w)->disconnect(this); } d->m_regForm->removeEditWidgets(d->m_editWidgets); d->m_item->leaveEditMode(); emit finishEdit(d->m_transactions); } void TransactionEditor::slotUpdateAccount(const QString& id) { Q_D(TransactionEditor); d->m_account = MyMoneyFile::instance()->account(id); setupPrecision(); } void TransactionEditor::slotUpdateAccount() { Q_D(TransactionEditor); // reload m_account as it might have been changed d->m_account = MyMoneyFile::instance()->account(d->m_account.id()); setupPrecision(); } void TransactionEditor::setupPrecision() { Q_D(TransactionEditor); const int prec = (d->m_account.id().isEmpty()) ? 2 : MyMoneyMoney::denomToPrec(d->m_account.fraction()); QStringList widgets = QString("amount,deposit,payment").split(','); QStringList::const_iterator it_w; for (it_w = widgets.constBegin(); it_w != widgets.constEnd(); ++it_w) { QWidget * w; if ((w = haveWidget(*it_w)) != 0) { dynamic_cast(w)->setPrecision(prec); } } } void TransactionEditor::setup(QWidgetList& tabOrderWidgets, const MyMoneyAccount& account, eWidgets::eRegister::Action action) { Q_D(TransactionEditor); d->m_account = account; d->m_initialAction = action; createEditWidgets(); d->m_regForm->arrangeEditWidgets(d->m_editWidgets, d->m_item); d->m_regForm->tabOrder(tabOrderWidgets, d->m_item); QWidget* w = haveWidget("tabbar"); if (w) { tabOrderWidgets.append(w); auto tabbar = dynamic_cast(w); if ((tabbar) && (action == eWidgets::eRegister::Action::None)) { action = static_cast(tabbar->currentIndex()); } } loadEditWidgets(action); // remove all unused widgets and don't forget to remove them // from the tab order list as well d->m_editWidgets.removeOrphans(); QWidgetList::iterator it_w; const QWidgetList editWidgets(d->m_editWidgets.values()); for (it_w = tabOrderWidgets.begin(); it_w != tabOrderWidgets.end();) { if (editWidgets.contains(*it_w)) { ++it_w; } else { // before we remove the widget, we make sure it's not a part of a known one. // these could be a direct child in case of KMyMoneyDateInput and KMyMoneyEdit // where we store the pointer to the surrounding frame in editWidgets // or the parent is called "KMyMoneyCategoryFrame" if (*it_w) { if (editWidgets.contains((*it_w)->parentWidget()) || ((*it_w)->parentWidget() && (*it_w)->parentWidget()->objectName() == QLatin1String("KMyMoneyCategoryFrame"))) { ++it_w; } else { // qDebug("Remove '%s' from taborder", qPrintable((*it_w)->objectName())); it_w = tabOrderWidgets.erase(it_w); } } else { it_w = tabOrderWidgets.erase(it_w); } } } clearFinalWidgets(); setupFinalWidgets(); slotUpdateButtonState(); } void TransactionEditor::setup(QWidgetList& tabOrderWidgets, const MyMoneyAccount& account) { setup(tabOrderWidgets, account, eWidgets::eRegister::Action::None); } void TransactionEditor::setup(QWidgetList& tabOrderWidgets) { setup(tabOrderWidgets, MyMoneyAccount(), eWidgets::eRegister::Action::None); } MyMoneyAccount TransactionEditor::account() const { Q_D(const TransactionEditor); return d->m_account; } void TransactionEditor::setScheduleInfo(const QString& si) { Q_D(TransactionEditor); d->m_scheduleInfo = si; } void TransactionEditor::setPaymentMethod(eMyMoney::Schedule::PaymentType pm) { Q_D(TransactionEditor); d->m_paymentMethod = pm; } void TransactionEditor::clearFinalWidgets() { Q_D(TransactionEditor); d->m_finalEditWidgets.clear(); } void TransactionEditor::addFinalWidget(const QWidget* w) { Q_D(TransactionEditor); if (w) { d->m_finalEditWidgets << w; } } void TransactionEditor::slotReloadEditWidgets() { } bool TransactionEditor::eventFilter(QObject* o, QEvent* e) { Q_D(TransactionEditor); bool rc = false; if (o == haveWidget("number")) { if (e->type() == QEvent::MouseButtonDblClick) { emit assignNumber(); rc = true; } } // if the object is a widget, the event is a key press event and // the object is one of our edit widgets, then .... if (o->isWidgetType() && (e->type() == QEvent::KeyPress) && d->m_editWidgets.values().contains(dynamic_cast(o))) { QKeyEvent* k = dynamic_cast(e); if ((k->modifiers() & Qt::KeyboardModifierMask) == 0 || (k->modifiers() & Qt::KeypadModifier) != 0) { bool isFinal = false; QList::const_iterator it_w; switch (k->key()) { case Qt::Key_Return: case Qt::Key_Enter: // we check, if the object is one of the m_finalEditWidgets and if it's // a KMyMoneyEdit object that the value is not 0. If any of that is the // case, it's the final object. In other cases, we convert the enter // key into a TAB key to move between the fields. Of course, we only need // to do this as long as the appropriate option is set. In all other cases, // we treat the return/enter key as such. if (KMyMoneyGlobalSettings::enterMovesBetweenFields()) { for (it_w = d->m_finalEditWidgets.constBegin(); !isFinal && it_w != d->m_finalEditWidgets.constEnd(); ++it_w) { if (*it_w == o) { if (dynamic_cast(*it_w)) { isFinal = !(dynamic_cast(*it_w)->value().isZero()); } else isFinal = true; } } } else isFinal = true; // for the non-final objects, we treat the return key as a TAB if (!isFinal) { QKeyEvent evt(e->type(), Qt::Key_Tab, k->modifiers(), QString(), k->isAutoRepeat(), k->count()); QApplication::sendEvent(o, &evt); // in case of a category item and the split button is visible // send a second event so that we get passed the button. if (dynamic_cast(o) && dynamic_cast(o)->splitButton()) QApplication::sendEvent(o, &evt); } else { QTimer::singleShot(0, this, SIGNAL(returnPressed())); } // don't process any further rc = true; break; case Qt::Key_Escape: QTimer::singleShot(0, this, SIGNAL(escapePressed())); break; } } } return rc; } void TransactionEditor::slotNumberChanged(const QString& txt) { Q_D(TransactionEditor); auto next = txt; KMyMoneyLineEdit* number = dynamic_cast(haveWidget("number")); QString schedInfo; if (!d->m_scheduleInfo.isEmpty()) { schedInfo = i18n("
Processing schedule for %1.
", d->m_scheduleInfo); } while (MyMoneyFile::instance()->checkNoUsed(d->m_account.id(), next)) { if (KMessageBox::questionYesNo(d->m_regForm, QString("") + schedInfo + i18n("
Check number %1 has already been used in account %2.
" "
Do you want to replace it with the next available number?
", next, d->m_account.name()) + QString("
"), i18n("Duplicate number")) == KMessageBox::Yes) { assignNextNumber(); next = KMyMoneyUtils::nextCheckNumber(d->m_account); } else { number->setText(QString()); break; } } } void TransactionEditor::slotUpdateMemoState() { Q_D(TransactionEditor); KTextEdit* memo = dynamic_cast(d->m_editWidgets["memo"]); if (memo) { d->m_memoChanged = (memo->toPlainText() != d->m_memoText); } } void TransactionEditor::slotUpdateButtonState() { QString reason; emit transactionDataSufficient(isComplete(reason)); } QWidget* TransactionEditor::haveWidget(const QString& name) const { Q_D(const TransactionEditor); return d->m_editWidgets.haveWidget(name); } int TransactionEditor::slotEditSplits() { return QDialog::Rejected; } void TransactionEditor::setTransaction(const MyMoneyTransaction& t, const MyMoneySplit& s) { Q_D(TransactionEditor); d->m_transaction = t; d->m_split = s; loadEditWidgets(); } bool TransactionEditor::isMultiSelection() const { Q_D(const TransactionEditor); return d->m_transactions.count() > 1; } bool TransactionEditor::fixTransactionCommodity(const MyMoneyAccount& account) { Q_D(TransactionEditor); bool rc = true; bool firstTimeMultiCurrency = true; d->m_account = account; auto file = MyMoneyFile::instance(); // determine the max fraction for this account MyMoneySecurity sec = file->security(d->m_account.currencyId()); int fract = d->m_account.fraction(); // scan the list of selected transactions KMyMoneyRegister::SelectedTransactions::iterator it_t; for (it_t = d->m_transactions.begin(); (rc == true) && (it_t != d->m_transactions.end()); ++it_t) { // there was a time when the schedule editor did not setup the transaction commodity // let's give a helping hand here for those old schedules if ((*it_t).transaction().commodity().isEmpty()) (*it_t).transaction().setCommodity(d->m_account.currencyId()); // we need to check things only if a different commodity is used if (d->m_account.currencyId() != (*it_t).transaction().commodity()) { MyMoneySecurity osec = file->security((*it_t).transaction().commodity()); switch ((*it_t).transaction().splitCount()) { case 0: // new transaction, guess nothing's here yet ;) break; case 1: try { // make sure, that the value is equal to the shares, don't forget our own copy MyMoneySplit& splitB = (*it_t).split(); // reference usage wanted here if (d->m_split == splitB) d->m_split.setValue(splitB.shares()); splitB.setValue(splitB.shares()); (*it_t).transaction().modifySplit(splitB); } catch (const MyMoneyException &e) { qDebug("Unable to update commodity to second splits currency in %s: '%s'", qPrintable((*it_t).transaction().id()), qPrintable(e.what())); } break; case 2: // If we deal with multiple currencies we make sure, that for // transactions with two splits, the transaction's commodity is the // currency of the currently selected account. This saves us from a // lot of grieve later on. We just have to switch the // transactions commodity. Let's assume the following scenario: // - transactions commodity is CA // - splitB and account's currencyId is CB // - splitA is of course in CA (otherwise we have a real problem) // - Value is V in both splits // - Shares in splitB is SB // - Shares in splitA is SA (and equal to V) // // We do the following: // - change transactions commodity to CB // - set V in both splits to SB // - modify the splits in the transaction try { // retrieve the splits MyMoneySplit& splitB = (*it_t).split(); // reference usage wanted here MyMoneySplit splitA = (*it_t).transaction().splitByAccount(d->m_account.id(), false); // - set V in both splits to SB. Don't forget our own copy if (d->m_split == splitB) { d->m_split.setValue(splitB.shares()); } splitB.setValue(splitB.shares()); splitA.setValue(-splitB.shares()); (*it_t).transaction().modifySplit(splitA); (*it_t).transaction().modifySplit(splitB); } catch (const MyMoneyException &e) { qDebug("Unable to update commodity to second splits currency in %s: '%s'", qPrintable((*it_t).transaction().id()), qPrintable(e.what())); } break; default: // TODO: use new logic by adjusting all splits by the price // extracted from the selected split. Inform the user that // this will happen and allow him to stop the processing (rc = false) try { QString msg; if (firstTimeMultiCurrency) { firstTimeMultiCurrency = false; if (!isMultiSelection()) { msg = i18n("This transaction has more than two splits and is originally based on a different currency (%1). Using this account to modify the transaction may result in rounding errors. Do you want to continue?", osec.name()); } else { msg = i18n("At least one of the selected transactions has more than two splits and is originally based on a different currency (%1). Using this account to modify the transactions may result in rounding errors. Do you want to continue?", osec.name()); } if (KMessageBox::warningContinueCancel(0, QString("%1").arg(msg)) == KMessageBox::Cancel) { rc = false; } } if (rc == true) { MyMoneyMoney price; if (!(*it_t).split().shares().isZero() && !(*it_t).split().value().isZero()) price = (*it_t).split().shares() / (*it_t).split().value(); MyMoneySplit& mySplit = (*it_t).split(); foreach (const auto split, (*it_t).transaction().splits()) { auto s = split; if (s == mySplit) { s.setValue(s.shares()); if (mySplit == d->m_split) { d->m_split = s; } mySplit = s; } else { s.setValue((s.value() * price).convert(fract)); } (*it_t).transaction().modifySplit(s); } } } catch (const MyMoneyException &e) { qDebug("Unable to update commodity of split currency in %s: '%s'", qPrintable((*it_t).transaction().id()), qPrintable(e.what())); } break; } // set the transaction's ommodity to this account's currency (*it_t).transaction().setCommodity(d->m_account.currencyId()); // update our copy of the transaction that has the focus if ((*it_t).transaction().id() == d->m_transaction.id()) { d->m_transaction = (*it_t).transaction(); } } } return rc; } void TransactionEditor::assignNextNumber() { Q_D(TransactionEditor); if (canAssignNumber()) { KMyMoneyLineEdit* number = dynamic_cast(haveWidget("number")); QString num = KMyMoneyUtils::nextCheckNumber(d->m_account); bool showMessage = true; int rc = KMessageBox::No; QString schedInfo; if (!d->m_scheduleInfo.isEmpty()) { schedInfo = i18n("
Processing schedule for %1.
", d->m_scheduleInfo); } while (MyMoneyFile::instance()->checkNoUsed(d->m_account.id(), num)) { if (showMessage) { rc = KMessageBox::questionYesNo(d->m_regForm, QString("") + schedInfo + i18n("Check number %1 has already been used in account %2." "
Do you want to replace it with the next available number?
", num, d->m_account.name()) + QString("
"), i18n("Duplicate number")); showMessage = false; } if (rc == KMessageBox::Yes) { num = KMyMoneyUtils::nextCheckNumber(d->m_account); KMyMoneyUtils::updateLastNumberUsed(d->m_account, num); d->m_account.setValue("lastNumberUsed", num); number->loadText(num); } else { num = QString(); break; } } number->setText(num); } } bool TransactionEditor::canAssignNumber() const { auto number = dynamic_cast(haveWidget("number")); return (number != 0); } void TransactionEditor::setupCategoryWidget(KMyMoneyCategory* category, const QList& splits, QString& categoryId, const char* splitEditSlot, bool /* allowObjectCreation */) { disconnect(category, SIGNAL(focusIn()), this, splitEditSlot); #if 0 // FIXME must deal with the logic that suppressObjectCreation is // automatically turned off when the createItem() signal is connected if (allowObjectCreation) category->setSuppressObjectCreation(false); #endif switch (splits.count()) { case 0: categoryId.clear(); if (!category->currentText().isEmpty()) { // category->clearEditText(); // don't clear as could be from another widget - Bug 322768 // make sure, we don't see the selector category->completion()->hide(); } category->completion()->setSelected(QString()); break; case 1: categoryId = splits[0].accountId(); category->completion()->setSelected(categoryId); category->slotItemSelected(categoryId); break; default: categoryId.clear(); category->setSplitTransaction(); connect(category, SIGNAL(focusIn()), this, splitEditSlot); #if 0 // FIXME must deal with the logic that suppressObjectCreation is // automatically turned off when the createItem() signal is connected if (allowObjectCreation) category->setSuppressObjectCreation(true); #endif break; } } bool TransactionEditor::enterTransactions(QString& newId, bool askForSchedule, bool suppressBalanceWarnings) { Q_D(TransactionEditor); newId.clear(); auto file = MyMoneyFile::instance(); // make sure to run through all stuff that is tied to 'focusout events'. d->m_regForm->parentWidget()->setFocus(); QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents, 10); // we don't need to update our widgets anymore, so we just disconnect the signal disconnect(file, &MyMoneyFile::dataChanged, this, &TransactionEditor::slotReloadEditWidgets); KMyMoneyRegister::SelectedTransactions::iterator it_t; MyMoneyTransaction t; bool newTransactionCreated = false; // make sure, that only a single new transaction can be created. // we need to update m_transactions to contain the new transaction // which is then stored in the variable t when we leave the loop. // m_transactions will be sent out in finishEdit() and forces // the new transaction to be selected in the ledger view // collect the transactions to be stored in the engine in a local // list first, so that the user has a chance to interrupt the storage // process QList list; auto storeTransactions = true; // collect transactions for (it_t = d->m_transactions.begin(); storeTransactions && !newTransactionCreated && it_t != d->m_transactions.end(); ++it_t) { storeTransactions = createTransaction(t, (*it_t).transaction(), (*it_t).split()); // if the transaction was created successfully, append it to the list if (storeTransactions) list.append(t); // if we created a new transaction keep that in mind if (t.id().isEmpty()) newTransactionCreated = true; } // if not interrupted by user, continue to store them in the engine if (storeTransactions) { auto i = 0; emit statusMsg(i18n("Storing transactions")); emit statusProgress(0, list.count()); MyMoneyFileTransaction ft; try { QMap minBalanceEarly; QMap minBalanceAbsolute; QMap maxCreditEarly; QMap maxCreditAbsolute; QMap accountIds; for (MyMoneyTransaction& transaction : list) { // if we have a categorization, make sure we remove // the 'imported' flag automagically if (transaction.splitCount() > 1) transaction.setImported(false); // create information about min and max balances foreach (const auto split, transaction.splits()) { auto acc = file->account(split.accountId()); accountIds[acc.id()] = true; MyMoneyMoney balance = file->balance(acc.id()); if (!acc.value("minBalanceEarly").isEmpty()) { minBalanceEarly[acc.id()] = balance < MyMoneyMoney(acc.value("minBalanceEarly")); } if (!acc.value("minBalanceAbsolute").isEmpty()) { minBalanceAbsolute[acc.id()] = balance < MyMoneyMoney(acc.value("minBalanceAbsolute")); minBalanceEarly[acc.id()] = false; } if (!acc.value("maxCreditEarly").isEmpty()) { maxCreditEarly[acc.id()] = balance < MyMoneyMoney(acc.value("maxCreditEarly")); } if (!acc.value("maxCreditAbsolute").isEmpty()) { maxCreditAbsolute[acc.id()] = balance < MyMoneyMoney(acc.value("maxCreditAbsolute")); maxCreditEarly[acc.id()] = false; } } if (transaction.id().isEmpty()) { bool enter = true; if (askForSchedule && transaction.postDate() > QDate::currentDate()) { KGuiItem enterButton(i18n("&Enter"), QIcon::fromTheme(g_Icons[Icon::DialogOK]), i18n("Accepts the entered data and stores it"), i18n("Use this to enter the transaction into the ledger.")); KGuiItem scheduleButton(i18n("&Schedule"), QIcon::fromTheme(g_Icons[Icon::AppointmentNew]), i18n("Accepts the entered data and stores it as schedule"), i18n("Use this to schedule the transaction for later entry into the ledger.")); enter = KMessageBox::questionYesNo(d->m_regForm, QString("%1").arg(i18n("The transaction you are about to enter has a post date in the future.

Do you want to enter it in the ledger or add it to the schedules?")), i18nc("Dialog caption for 'Enter or schedule' dialog", "Enter or schedule?"), enterButton, scheduleButton, "EnterOrScheduleTransactionInFuture") == KMessageBox::Yes; } if (enter) { // add new transaction file->addTransaction(transaction); // pass the newly assigned id on to the caller newId = transaction.id(); // refresh account object for transactional changes // refresh account and transaction object because they might have changed d->m_account = file->account(d->m_account.id()); t = transaction; // if a new transaction has a valid number, keep it with the account d->keepNewNumber(transaction); } else { // turn object creation on, so that moving the focus does // not screw up the dialog that might be popping up emit objectCreation(true); emit scheduleTransaction(transaction, eMyMoney::Schedule::Occurrence::Once); emit objectCreation(false); newTransactionCreated = false; } // send out the post date of this transaction emit lastPostDateUsed(transaction.postDate()); } else { // modify existing transaction // its number might have been edited // bearing in mind it could contain alpha characters d->keepNewNumber(transaction); file->modifyTransaction(transaction); } } emit statusProgress(i++, 0); // update m_transactions to contain the newly created transaction so that // it is selected as the current one // we need to do that before we commit the transaction to the engine // as we need it during the update of the views that is caused by committing already. if (newTransactionCreated) { d->m_transactions.clear(); MyMoneySplit s; // a transaction w/o a single split should not exist and adding it // should throw an exception in MyMoneyFile::addTransaction, but we // remain on the save side of things to check for it if (t.splitCount() > 0) s = t.splits().front(); KMyMoneyRegister::SelectedTransaction st(t, s, QString()); d->m_transactions.append(st); } // Save pricing information foreach (const auto split, t.splits()) { if ((split.action() != "Buy") && (split.action() != "Reinvest")) { continue; } QString id = split.accountId(); auto acc = file->account(id); MyMoneySecurity sec = file->security(acc.currencyId()); MyMoneyPrice price(acc.currencyId(), sec.tradingCurrency(), t.postDate(), split.price(), "Transaction"); file->addPrice(price); break; } ft.commit(); // now analyze the balances and spit out warnings to the user QMap::const_iterator it_a; if (!suppressBalanceWarnings) { for (it_a = accountIds.constBegin(); it_a != accountIds.constEnd(); ++it_a) { QString msg; auto acc = file->account(it_a.key()); MyMoneyMoney balance = file->balance(acc.id()); const MyMoneySecurity& sec = file->security(acc.currencyId()); QString key; key = "minBalanceEarly"; if (!acc.value(key).isEmpty()) { if (minBalanceEarly[acc.id()] == false && balance < MyMoneyMoney(acc.value(key))) { msg = QString("%1").arg(i18n("The balance of account %1 dropped below the warning balance of %2.", acc.name(), MyMoneyUtils::formatMoney(MyMoneyMoney(acc.value(key)), acc, sec))); } } key = "minBalanceAbsolute"; if (!acc.value(key).isEmpty()) { if (minBalanceAbsolute[acc.id()] == false && balance < MyMoneyMoney(acc.value(key))) { msg = QString("%1").arg(i18n("The balance of account %1 dropped below the minimum balance of %2.", acc.name(), MyMoneyUtils::formatMoney(MyMoneyMoney(acc.value(key)), acc, sec))); } } key = "maxCreditEarly"; if (!acc.value(key).isEmpty()) { if (maxCreditEarly[acc.id()] == false && balance < MyMoneyMoney(acc.value(key))) { msg = QString("%1").arg(i18n("The balance of account %1 dropped below the maximum credit warning limit of %2.", acc.name(), MyMoneyUtils::formatMoney(MyMoneyMoney(acc.value(key)), acc, sec))); } } key = "maxCreditAbsolute"; if (!acc.value(key).isEmpty()) { if (maxCreditAbsolute[acc.id()] == false && balance < MyMoneyMoney(acc.value(key))) { msg = QString("%1").arg(i18n("The balance of account %1 dropped below the maximum credit limit of %2.", acc.name(), MyMoneyUtils::formatMoney(MyMoneyMoney(acc.value(key)), acc, sec))); } } if (!msg.isEmpty()) { emit balanceWarning(d->m_regForm, acc, msg); } } } } catch (const MyMoneyException &e) { qDebug("Unable to store transaction within engine: %s", qPrintable(e.what())); newTransactionCreated = false; } emit statusProgress(-1, -1); emit statusMsg(QString()); } return storeTransactions; } void TransactionEditor::resizeForm() { Q_D(TransactionEditor); // force resizeing of the columns in the form auto form = dynamic_cast(d->m_regForm); if (form) { QMetaObject::invokeMethod(form, "resize", Qt::QueuedConnection, QGenericReturnArgument(), Q_ARG(int, (int)eWidgets::eTransactionForm::Column::Value1)); } } diff --git a/kmymoney/kmymoneyutils.cpp b/kmymoney/kmymoneyutils.cpp index 5efa6c3d1..082980e63 100644 --- a/kmymoney/kmymoneyutils.cpp +++ b/kmymoney/kmymoneyutils.cpp @@ -1,648 +1,649 @@ /*************************************************************************** kmymoneyutils.cpp - description ------------------- begin : Wed Feb 5 2003 copyright : (C) 2000-2003 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 "kmymoneyutils.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Headers #include #include #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "mymoneymoney.h" +#include "mymoneyexception.h" #include "mymoneyfile.h" #include "mymoneyaccount.h" #include "mymoneysecurity.h" #include "mymoneyschedule.h" #include "mymoneyprice.h" #include "mymoneyforecast.h" #include "mymoneysplit.h" #include "mymoneytransaction.h" #include "kmymoneyglobalsettings.h" #include "icons.h" #include "storageenums.h" #include "mymoneyenums.h" using namespace Icons; KMyMoneyUtils::KMyMoneyUtils() { } KMyMoneyUtils::~KMyMoneyUtils() { } const QString KMyMoneyUtils::occurrenceToString(const eMyMoney::Schedule::Occurrence occurrence) { return i18nc("Frequency of schedule", MyMoneySchedule::occurrenceToString(occurrence).toLatin1()); } const QString KMyMoneyUtils::paymentMethodToString(eMyMoney::Schedule::PaymentType paymentType) { return i18nc("Scheduled Transaction payment type", MyMoneySchedule::paymentMethodToString(paymentType).toLatin1()); } const QString KMyMoneyUtils::weekendOptionToString(eMyMoney::Schedule::WeekendOption weekendOption) { return i18n(MyMoneySchedule::weekendOptionToString(weekendOption).toLatin1()); } const QString KMyMoneyUtils::scheduleTypeToString(eMyMoney::Schedule::Type type) { return i18nc("Scheduled transaction type", MyMoneySchedule::scheduleTypeToString(type).toLatin1()); } KGuiItem KMyMoneyUtils::scheduleNewGuiItem() { KGuiItem splitGuiItem(i18n("&New Schedule..."), QIcon::fromTheme(g_Icons[Icon::DocumentNew]), i18n("Create a new schedule."), i18n("Use this to create a new schedule.")); return splitGuiItem; } KGuiItem KMyMoneyUtils::accountsFilterGuiItem() { KGuiItem splitGuiItem(i18n("&Filter"), QIcon::fromTheme(g_Icons[Icon::ViewFilter]), i18n("Filter out accounts"), i18n("Use this to filter out accounts")); return splitGuiItem; } const char* homePageItems[] = { I18N_NOOP("Payments"), I18N_NOOP("Preferred accounts"), I18N_NOOP("Payment accounts"), I18N_NOOP("Favorite reports"), I18N_NOOP("Forecast (schedule)"), I18N_NOOP("Net worth forecast"), I18N_NOOP("Forecast (history)"), I18N_NOOP("Assets and Liabilities"), I18N_NOOP("Budget"), I18N_NOOP("CashFlow"), // insert new items above this comment 0 }; const QString KMyMoneyUtils::homePageItemToString(const int idx) { QString rc; if (abs(idx) > 0 && abs(idx) < static_cast(sizeof(homePageItems) / sizeof(homePageItems[0]))) { rc = i18n(homePageItems[abs(idx-1)]); } return rc; } int KMyMoneyUtils::stringToHomePageItem(const QString& txt) { int idx = 0; for (idx = 0; homePageItems[idx] != 0; ++idx) { if (txt == i18n(homePageItems[idx])) return idx + 1; } return 0; } bool KMyMoneyUtils::appendCorrectFileExt(QString& str, const QString& strExtToUse) { bool rc = false; if (!str.isEmpty()) { //find last . delminator int nLoc = str.lastIndexOf('.'); if (nLoc != -1) { QString strExt, strTemp; strTemp = str.left(nLoc + 1); strExt = str.right(str.length() - (nLoc + 1)); if (strExt.indexOf(strExtToUse, 0, Qt::CaseInsensitive) == -1) { // if the extension given contains a period, we remove our's if (strExtToUse.indexOf('.') != -1) strTemp = strTemp.left(strTemp.length() - 1); //append extension to make complete file name strTemp.append(strExtToUse); str = strTemp; rc = true; } } else { str.append("."); str.append(strExtToUse); rc = true; } } return rc; } void KMyMoneyUtils::checkConstants() { // TODO: port to kf5 #if 0 Q_ASSERT(static_cast(KLocale::ParensAround) == static_cast(MyMoneyMoney::ParensAround)); Q_ASSERT(static_cast(KLocale::BeforeQuantityMoney) == static_cast(MyMoneyMoney::BeforeQuantityMoney)); Q_ASSERT(static_cast(KLocale::AfterQuantityMoney) == static_cast(MyMoneyMoney::AfterQuantityMoney)); Q_ASSERT(static_cast(KLocale::BeforeMoney) == static_cast(MyMoneyMoney::BeforeMoney)); Q_ASSERT(static_cast(KLocale::AfterMoney) == static_cast(MyMoneyMoney::AfterMoney)); #endif } QString KMyMoneyUtils::variableCSS() { QColor tcolor = KColorScheme(QPalette::Active).foreground(KColorScheme::NormalText).color(); QColor link = KColorScheme(QPalette::Active).foreground(KColorScheme::LinkText).color(); QString css; css += "\n"; return css; } QString KMyMoneyUtils::findResource(QStandardPaths::StandardLocation type, const QString& filename) { QLocale locale; QString country; QString localeName = locale.bcp47Name(); QString language = localeName; // extract language and country from the bcp47name QRegularExpression regExp(QLatin1String("(\\w+)_(\\w+)")); QRegularExpressionMatch match = regExp.match(localeName); if (match.hasMatch()) { language = match.captured(1); country = match.captured(2); } QString rc; // check that the placeholder is present and set things up if (filename.indexOf("%1") != -1) { /// @fixme somehow I have the impression, that language and country /// mappings to the filename are not correct. This certainly must /// be overhauled at some point in time (ipwizard, 2017-10-22) QString mask = filename.arg("_%1.%2"); rc = QStandardPaths::locate(type, mask.arg(country).arg(language)); // search the given resource if (rc.isEmpty()) { mask = filename.arg("_%1"); rc = QStandardPaths::locate(type, mask.arg(language)); } if (rc.isEmpty()) { // qDebug(QString("html/home_%1.html not found").arg(country).toLatin1()); rc = QStandardPaths::locate(type, mask.arg(country)); } if (rc.isEmpty()) { rc = QStandardPaths::locate(type, filename.arg("")); } } else { rc = QStandardPaths::locate(type, filename); } if (rc.isEmpty()) { qWarning("No resource found for (%s,%s)", qPrintable(QStandardPaths::displayName(type)), qPrintable(filename)); } return rc; } const MyMoneySplit KMyMoneyUtils::stockSplit(const MyMoneyTransaction& t) { MyMoneySplit investmentAccountSplit; foreach (const auto split, t.splits()) { if (!split.accountId().isEmpty()) { auto acc = MyMoneyFile::instance()->account(split.accountId()); if (acc.isInvest()) { return split; } // if we have a reference to an investment account, we remember it here if (acc.accountType() == eMyMoney::Account::Type::Investment) investmentAccountSplit = split; } } // if we haven't found a stock split, we see if we've seen // an investment account on the way. If so, we return it. if (!investmentAccountSplit.id().isEmpty()) return investmentAccountSplit; // if none was found, we return an empty split. return MyMoneySplit(); } KMyMoneyUtils::transactionTypeE KMyMoneyUtils::transactionType(const MyMoneyTransaction& t) { if (!stockSplit(t).id().isEmpty()) return InvestmentTransaction; if (t.splitCount() < 2) { return Unknown; } else if (t.splitCount() > 2) { // FIXME check for loan transaction here return SplitTransaction; } QString ida, idb; if (t.splits().size() > 0) ida = t.splits()[0].accountId(); if (t.splits().size() > 1) idb = t.splits()[1].accountId(); if (ida.isEmpty() || idb.isEmpty()) return Unknown; MyMoneyAccount a, b; a = MyMoneyFile::instance()->account(ida); b = MyMoneyFile::instance()->account(idb); if ((a.accountGroup() == eMyMoney::Account::Type::Asset || a.accountGroup() == eMyMoney::Account::Type::Liability) && (b.accountGroup() == eMyMoney::Account::Type::Asset || b.accountGroup() == eMyMoney::Account::Type::Liability)) return Transfer; return Normal; } void KMyMoneyUtils::calculateAutoLoan(const MyMoneySchedule& schedule, MyMoneyTransaction& transaction, const QMap& balances) { try { MyMoneyForecast::calculateAutoLoan(schedule, transaction, balances); } catch (const MyMoneyException &e) { KMessageBox::detailedError(0, i18n("Unable to load schedule details"), e.what()); } } QString KMyMoneyUtils::nextCheckNumber(const MyMoneyAccount& acc) { QString number; // +-#1--+ +#2++-#3-++-#4--+ QRegExp exp(QString("(.*\\D)?(0*)(\\d+)(\\D.*)?")); if (exp.indexIn(acc.value("lastNumberUsed")) != -1) { setLastNumberUsed(acc.value("lastNumberUsed")); QString arg1 = exp.cap(1); QString arg2 = exp.cap(2); QString arg3 = QString::number(exp.cap(3).toULong() + 1); QString arg4 = exp.cap(4); number = QString("%1%2%3%4").arg(arg1).arg(arg2).arg(arg3).arg(arg4); // if new number is longer than previous one and we identified // preceding 0s, then remove one of the preceding zeros if (arg2.length() > 0 && (number.length() != acc.value("lastNumberUsed").length())) { arg2 = arg2.mid(1); number = QString("%1%2%3%4").arg(arg1).arg(arg2).arg(arg3).arg(arg4); } } else { number = '1'; } return number; } void KMyMoneyUtils::updateLastNumberUsed(const MyMoneyAccount& acc, const QString& number) { MyMoneyAccount accnt = acc; QString num = number; // now check if this number has been used already auto file = MyMoneyFile::instance(); if (file->checkNoUsed(accnt.id(), num)) { // if a number has been entered which is immediately prior to // an existing number, the next new number produced would clash // so need to look ahead for free next number bool free = false; for (int i = 0; i < 10; i++) { // find next unused number - 10 tries (arbitrary) if (file->checkNoUsed(accnt.id(), num)) { // increment and try again num = getAdjacentNumber(num); } else { // found a free number free = true; break; } } if (!free) { qDebug() << "No free number found - set to '1'"; num = '1'; } setLastNumberUsed(getAdjacentNumber(num, - 1)); } } void KMyMoneyUtils::setLastNumberUsed(const QString& num) { m_lastNumberUsed = num; } QString KMyMoneyUtils::lastNumberUsed() { return m_lastNumberUsed; } QString KMyMoneyUtils::getAdjacentNumber(const QString& number, int offset) { QString num = number; // +-#1--+ +#2++-#3-++-#4--+ QRegExp exp(QString("(.*\\D)?(0*)(\\d+)(\\D.*)?")); if (exp.indexIn(num) != -1) { QString arg1 = exp.cap(1); QString arg2 = exp.cap(2); QString arg3 = QString::number(exp.cap(3).toULong() + offset); QString arg4 = exp.cap(4); num = QString("%1%2%3%4").arg(arg1).arg(arg2).arg(arg3).arg(arg4); } else { num = '1'; } // next free number return num; } quint64 KMyMoneyUtils::numericPart(const QString & num) { quint64 num64 = 0; QRegExp exp(QString("(.*\\D)?(0*)(\\d+)(\\D.*)?")); if (exp.indexIn(num) != -1) { QString arg1 = exp.cap(1); QString arg2 = exp.cap(2); QString arg3 = QString::number(exp.cap(3).toULongLong()); QString arg4 = exp.cap(4); num64 = QString("%2%3").arg(arg2).arg(arg3).toULongLong(); } return num64; } QString KMyMoneyUtils::reconcileStateToString(eMyMoney::Split::State flag, bool text) { QString txt; if (text) { switch (flag) { case eMyMoney::Split::State::NotReconciled: txt = i18nc("Reconciliation state 'Not reconciled'", "Not reconciled"); break; case eMyMoney::Split::State::Cleared: txt = i18nc("Reconciliation state 'Cleared'", "Cleared"); break; case eMyMoney::Split::State::Reconciled: txt = i18nc("Reconciliation state 'Reconciled'", "Reconciled"); break; case eMyMoney::Split::State::Frozen: txt = i18nc("Reconciliation state 'Frozen'", "Frozen"); break; default: txt = i18nc("Unknown reconciliation state", "Unknown"); break; } } else { switch (flag) { case eMyMoney::Split::State::NotReconciled: break; case eMyMoney::Split::State::Cleared: txt = i18nc("Reconciliation flag C", "C"); break; case eMyMoney::Split::State::Reconciled: txt = i18nc("Reconciliation flag R", "R"); break; case eMyMoney::Split::State::Frozen: txt = i18nc("Reconciliation flag F", "F"); break; default: txt = i18nc("Flag for unknown reconciliation state", "?"); break; } } return txt; } MyMoneyTransaction KMyMoneyUtils::scheduledTransaction(const MyMoneySchedule& schedule) { MyMoneyTransaction t = schedule.transaction(); try { if (schedule.type() == eMyMoney::Schedule::Type::LoanPayment) { calculateAutoLoan(schedule, t, QMap()); } } catch (const MyMoneyException &e) { qDebug("Unable to load schedule details for '%s' during transaction match: %s", qPrintable(schedule.name()), qPrintable(e.what())); } t.clearId(); t.setEntryDate(QDate()); return t; } KXmlGuiWindow* KMyMoneyUtils::mainWindow() { foreach (QWidget *widget, QApplication::topLevelWidgets()) { KXmlGuiWindow* result = dynamic_cast(widget); if (result) return result; } return 0; } void KMyMoneyUtils::updateWizardButtons(QWizard* wizard) { // setup text on buttons wizard->setButtonText(QWizard::NextButton, i18nc("Go to next page of the wizard", "&Next")); wizard->setButtonText(QWizard::BackButton, KStandardGuiItem::back().text()); // setup icons wizard->button(QWizard::FinishButton)->setIcon(KStandardGuiItem::ok().icon()); wizard->button(QWizard::CancelButton)->setIcon(KStandardGuiItem::cancel().icon()); wizard->button(QWizard::NextButton)->setIcon(KStandardGuiItem::forward(KStandardGuiItem::UseRTL).icon()); wizard->button(QWizard::BackButton)->setIcon(KStandardGuiItem::back(KStandardGuiItem::UseRTL).icon()); } QPixmap KMyMoneyUtils::overlayIcon(const QString &iconName, const QString &overlayName, const Qt::Corner corner, const int size) { QPixmap pxIcon; QString kyIcon = iconName + overlayName; // If found in the cache, return quickly if (QPixmapCache::find(kyIcon, pxIcon)) return pxIcon; // try to retrieve the main icon from cache if (!QPixmapCache::find(iconName, pxIcon)) { pxIcon = QIcon::fromTheme(iconName).pixmap(size); QPixmapCache::insert(iconName, pxIcon); } if (overlayName.isEmpty()) // call from MyMoneyAccount::accountPixmap can have no overlay icon, so handle that appropriately return pxIcon; QPainter pixmapPainter(&pxIcon); QPixmap pxOverlay = QIcon::fromTheme(overlayName).pixmap(size); int x, y; switch (corner) { case Qt::TopLeftCorner: x = 0; y = 0; break; case Qt::TopRightCorner: x = pxIcon.width() / 2; y = 0; break; case Qt::BottomLeftCorner: x = 0; y = pxIcon.height() / 2; break; case Qt::BottomRightCorner: default: x = pxIcon.width() / 2; y = pxIcon.height() / 2; break; } pixmapPainter.drawPixmap(x, y, pxIcon.width() / 2, pxIcon.height() / 2, pxOverlay); //save for later use QPixmapCache::insert(kyIcon, pxIcon); return pxIcon; } void KMyMoneyUtils::dissectTransaction(const MyMoneyTransaction& transaction, const MyMoneySplit& split, MyMoneySplit& assetAccountSplit, QList& feeSplits, QList& interestSplits, MyMoneySecurity& security, MyMoneySecurity& currency, eMyMoney::Split::InvestmentTransactionType& transactionType) { // collect the splits. split references the stock account and should already // be set up. assetAccountSplit references the corresponding asset account (maybe // empty), feeSplits is the list of all expenses and interestSplits // the list of all incomes assetAccountSplit = MyMoneySplit(); // set to none to check later if it was assigned auto file = MyMoneyFile::instance(); foreach (const auto tsplit, transaction.splits()) { auto acc = file->account(tsplit.accountId()); if (tsplit.id() == split.id()) { security = file->security(acc.currencyId()); } else if (acc.accountGroup() == eMyMoney::Account::Type::Expense) { feeSplits.append(tsplit); // feeAmount += tsplit.value(); } else if (acc.accountGroup() == eMyMoney::Account::Type::Income) { interestSplits.append(tsplit); // interestAmount += tsplit.value(); } else { if (assetAccountSplit == MyMoneySplit()) // first asset Account should be our requested brokerage account assetAccountSplit = tsplit; else if (tsplit.value().isNegative()) // the rest (if present) is handled as fee or interest feeSplits.append(tsplit); // and shouldn't be allowed to override assetAccountSplit else if (tsplit.value().isPositive()) interestSplits.append(tsplit); } } // determine transaction type if (split.action() == MyMoneySplit::ActionAddShares) { transactionType = (!split.shares().isNegative()) ? eMyMoney::Split::InvestmentTransactionType::AddShares : eMyMoney::Split::InvestmentTransactionType::RemoveShares; } else if (split.action() == MyMoneySplit::ActionBuyShares) { transactionType = (!split.value().isNegative()) ? eMyMoney::Split::InvestmentTransactionType::BuyShares : eMyMoney::Split::InvestmentTransactionType::SellShares; } else if (split.action() == MyMoneySplit::ActionDividend) { transactionType = eMyMoney::Split::InvestmentTransactionType::Dividend; } else if (split.action() == MyMoneySplit::ActionReinvestDividend) { transactionType = eMyMoney::Split::InvestmentTransactionType::ReinvestDividend; } else if (split.action() == MyMoneySplit::ActionYield) { transactionType = eMyMoney::Split::InvestmentTransactionType::Yield; } else if (split.action() == MyMoneySplit::ActionSplitShares) { transactionType = eMyMoney::Split::InvestmentTransactionType::SplitShares; } else if (split.action() == MyMoneySplit::ActionInterestIncome) { transactionType = eMyMoney::Split::InvestmentTransactionType::InterestIncome; } else transactionType = eMyMoney::Split::InvestmentTransactionType::BuyShares; currency.setTradingSymbol("???"); try { currency = file->security(transaction.commodity()); } catch (const MyMoneyException &) { } } void KMyMoneyUtils::deleteSecurity(const MyMoneySecurity& security, QWidget* parent) { QString msg, msg2; QString dontAsk, dontAsk2; if (security.isCurrency()) { msg = i18n("

Do you really want to remove the currency %1 from the file?

", security.name()); msg2 = i18n("

All exchange rates for currency %1 will be lost.

Do you still want to continue?

", security.name()); dontAsk = "DeleteCurrency"; dontAsk2 = "DeleteCurrencyRates"; } else { msg = i18n("

Do you really want to remove the %1 %2 from the file?

", MyMoneySecurity::securityTypeToString(security.securityType()), security.name()); msg2 = i18n("

All price quotes for %1 %2 will be lost.

Do you still want to continue?

", MyMoneySecurity::securityTypeToString(security.securityType()), security.name()); dontAsk = "DeleteSecurity"; dontAsk2 = "DeleteSecurityPrices"; } if (KMessageBox::questionYesNo(parent, msg, i18n("Delete security"), KStandardGuiItem::yes(), KStandardGuiItem::no(), dontAsk) == KMessageBox::Yes) { MyMoneyFileTransaction ft; auto file = MyMoneyFile::instance(); QBitArray skip((int)eStorage::Reference::Count); skip.fill(true); skip.clearBit((int)eStorage::Reference::Price); if (file->isReferenced(security, skip)) { if (KMessageBox::questionYesNo(parent, msg2, i18n("Delete prices"), KStandardGuiItem::yes(), KStandardGuiItem::no(), dontAsk2) == KMessageBox::Yes) { try { QString secID = security.id(); foreach (auto priceEntry, file->priceList()) { const MyMoneyPrice& price = priceEntry.first(); if (price.from() == secID || price.to() == secID) file->removePrice(price); } ft.commit(); ft.restart(); } catch (const MyMoneyException &) { qDebug("Cannot delete price"); return; } } else return; } try { if (security.isCurrency()) file->removeCurrency(security); else file->removeSecurity(security); ft.commit(); } catch (const MyMoneyException &) { } } } diff --git a/kmymoney/models/accountsmodel.cpp b/kmymoney/models/accountsmodel.cpp index 9a7adbc4c..c2f4702cd 100644 --- a/kmymoney/models/accountsmodel.cpp +++ b/kmymoney/models/accountsmodel.cpp @@ -1,1231 +1,1232 @@ /*************************************************************************** * Copyright 2010 Cristian Onet onet.cristian@gmail.com * * Copyright 2017 Łukasz Wojniłowicz lukasz.wojnilowicz@gmail.com * * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License as * * published by the Free Software Foundation; either version 2 of * * the License or (at your option) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of version 3 of the license. * * * * 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 "accountsmodel.h" // ---------------------------------------------------------------------------- // QT Includes #include // ---------------------------------------------------------------------------- // KDE Includes #include #include // ---------------------------------------------------------------------------- // Project Includes #include "mymoneyutils.h" #include "mymoneymoney.h" +#include "mymoneyexception.h" #include "mymoneyfile.h" #include "mymoneyinstitution.h" #include "mymoneyaccount.h" #include "mymoneysecurity.h" #include "mymoneyprice.h" #include "kmymoneyglobalsettings.h" #include "icons.h" #include "modelenums.h" #include "mymoneyenums.h" using namespace Icons; using namespace eAccountsModel; using namespace eMyMoney; class AccountsModel::Private { public: /** * The pimpl. */ Private() : m_file(MyMoneyFile::instance()) { m_columns.append(Column::Account); } ~Private() { } void loadPreferredAccount(const MyMoneyAccount &acc, QStandardItem *fromNode /*accounts' regular node*/, const int row, QStandardItem *toNode /*accounts' favourite node*/) { if (acc.value(QStringLiteral("PreferredAccount")) != QLatin1String("Yes")) return; auto favRow = toNode->rowCount(); auto favItem = itemFromAccountId(toNode, acc.id()); if (favItem) favRow = favItem->row(); for (auto i = 0; i < fromNode->columnCount(); ++i) { auto itemToClone = fromNode->child(row, i); if (itemToClone) toNode->setChild(favRow, i, itemToClone->clone()); } } /** * Load all the sub-accounts recursively. * * @param model The model in which to load the data. * @param accountsItem The item from the model of the parent account of the sub-accounts which are being loaded. * @param favoriteAccountsItem The item of the favorites accounts groups so favorite accounts can be added here also. * @param list The list of the account id's of the sub-accounts which are being loaded. * */ void loadSubaccounts(QStandardItem *node, QStandardItem *favoriteAccountsItem, const QStringList& subaccounts) { if (subaccounts.isEmpty()) return; foreach (const auto subaccStr, subaccounts) { const auto subacc = m_file->account(subaccStr); auto item = new QStandardItem(subacc.name()); // initialize first column of subaccount node->appendRow(item); // add subaccount row to node item->setEditable(false); item->setData(node->data((int)Role::DisplayOrder), (int)Role::DisplayOrder); // inherit display order role from node loadSubaccounts(item, favoriteAccountsItem, subacc.accountList()); // subaccount may have subaccounts as well // set the account data after the children have been loaded const auto row = item->row(); setAccountData(node, row, subacc, m_columns); // initialize rest of columns of subaccount loadPreferredAccount(subacc, node, row, favoriteAccountsItem); // add to favourites node if preferred } } /** * Note: this functions should only be called after the child account data has been set. */ void setAccountData(QStandardItem *node, const int row, const MyMoneyAccount &account, const QList &columns) { QStandardItem *cell; auto getCell = [&, row](const auto column) { cell = node->child(row, column); // try to get QStandardItem if (!cell) { // it may be uninitialized cell = new QStandardItem; // so create one node->setChild(row, column, cell); // and add it under the node } }; auto colNum = m_columns.indexOf(Column::Account); if (colNum == -1) return; getCell(colNum); auto font = cell->data(Qt::FontRole).value(); // display the names of closed accounts with strikeout font if (account.isClosed() != font.strikeOut()) font.setStrikeOut(account.isClosed()); if (columns.contains(Column::Account)) { // setting account column cell->setData(account.name(), Qt::DisplayRole); // cell->setData(QVariant::fromValue(account), (int)Role::Account); // is set in setAccountBalanceAndValue cell->setData(QVariant(account.id()), (int)Role::ID); cell->setData(QVariant(account.value("PreferredAccount") == QLatin1String("Yes")), (int)Role::Favorite); cell->setData(QVariant(QIcon(account.accountPixmap(m_reconciledAccount.id().isEmpty() ? false : account.id() == m_reconciledAccount.id()))), Qt::DecorationRole); cell->setData(MyMoneyFile::instance()->accountToCategory(account.id(), true), (int)Role::FullName); cell->setData(font, Qt::FontRole); } // Type if (columns.contains(Column::Type)) { colNum = m_columns.indexOf(Column::Type); if (colNum != -1) { getCell(colNum); cell->setData(account.accountTypeToString(account.accountType()), Qt::DisplayRole); cell->setData(font, Qt::FontRole); } } // Account's number if (columns.contains(Column::AccountNumber)) { colNum = m_columns.indexOf(Column::AccountNumber); if (colNum != -1) { getCell(colNum); cell->setData(account.number(), Qt::DisplayRole); cell->setData(font, Qt::FontRole); } } // Account's sort code if (columns.contains(Column::AccountSortCode)) { colNum = m_columns.indexOf(Column::AccountSortCode); if (colNum != -1) { getCell(colNum); cell->setData(account.value("iban"), Qt::DisplayRole); cell->setData(font, Qt::FontRole); } } const auto checkMark = QIcon::fromTheme(g_Icons[Icon::DialogOK]); switch (account.accountType()) { case Account::Type::Income: case Account::Type::Expense: case Account::Type::Asset: case Account::Type::Liability: // Tax if (columns.contains(Column::Tax)) { colNum = m_columns.indexOf(Column::Tax); if (colNum != -1) { getCell(colNum); if (account.value("Tax").toLower() == "yes") cell->setData(checkMark, Qt::DecorationRole); else cell->setData(QIcon(), Qt::DecorationRole); } } // VAT Account if (columns.contains(Column::VAT)) { colNum = m_columns.indexOf(Column::VAT); if (colNum != -1) { getCell(colNum); if (!account.value("VatAccount").isEmpty()) { const auto vatAccount = MyMoneyFile::instance()->account(account.value("VatAccount")); cell->setData(vatAccount.name(), Qt::DisplayRole); cell->setData(QVariant(Qt::AlignLeft | Qt::AlignVCenter), Qt::TextAlignmentRole); // VAT Rate } else if (!account.value("VatRate").isEmpty()) { const auto vatRate = MyMoneyMoney(account.value("VatRate")) * MyMoneyMoney(100, 1); cell->setData(QString::fromLatin1("%1 %").arg(vatRate.formatMoney(QString(), 1)), Qt::DisplayRole); cell->setData(QVariant(Qt::AlignRight | Qt::AlignVCenter), Qt::TextAlignmentRole); } else { cell->setData(QString(), Qt::DisplayRole); } } } // CostCenter if (columns.contains(Column::CostCenter)) { colNum = m_columns.indexOf(Column::CostCenter); if (colNum != -1) { getCell(colNum); if (account.isCostCenterRequired()) cell->setData(checkMark, Qt::DecorationRole); else cell->setData(QIcon(), Qt::DecorationRole); } } break; default: break; } // balance and value setAccountBalanceAndValue(node, row, account, columns); } void setInstitutionTotalValue(QStandardItem *node, const int row) { const auto colInstitution = m_columns.indexOf(Column::Account); auto itInstitution = node->child(row, colInstitution); const auto valInstitution = childrenTotalValue(itInstitution, true); itInstitution->setData(QVariant::fromValue(valInstitution ), (int)Role::TotalValue); const auto colTotalValue = m_columns.indexOf(Column::TotalValue); if (colTotalValue == -1) return; auto cell = node->child(row, colTotalValue); if (!cell) { cell = new QStandardItem; node->setChild(row, colTotalValue, cell); } QColor color; if (valInstitution.isNegative()) color = KMyMoneyGlobalSettings::schemeColor(SchemeColor::Negative); else color = KColorScheme(QPalette::Active).foreground(KColorScheme::NormalText).color(); cell->setData(QVariant(color), Qt::ForegroundRole); cell->setData(QVariant(itInstitution->data(Qt::FontRole).value()), Qt::FontRole); cell->setData(QVariant(Qt::AlignRight | Qt::AlignVCenter), Qt::TextAlignmentRole); cell->setData(MyMoneyUtils::formatMoney(valInstitution, m_file->baseCurrency()), Qt::DisplayRole); } void setAccountBalanceAndValue(QStandardItem *node, const int row, const MyMoneyAccount &account, const QList &columns) { QStandardItem *cell; auto getCell = [&, row](auto column) { cell = node->child(row, column); if (!cell) { cell = new QStandardItem; node->setChild(row, column, cell); } }; // setting account column auto colNum = m_columns.indexOf(Column::Account); if (colNum == -1) return; getCell(colNum); MyMoneyMoney accountBalance, accountValue, accountTotalValue; if (columns.contains(Column::Account)) { // update values only when requested accountBalance = balance(account); accountValue = value(account, accountBalance); accountTotalValue = childrenTotalValue(cell) + accountValue; cell->setData(QVariant::fromValue(account), (int)Role::Account); cell->setData(QVariant::fromValue(accountBalance), (int)Role::Balance); cell->setData(QVariant::fromValue(accountValue), (int)Role::Value); cell->setData(QVariant::fromValue(accountTotalValue), (int)Role::TotalValue); } else { // otherwise save up on tedious calculations accountBalance = cell->data((int)Role::Balance).value(); accountValue = cell->data((int)Role::Value).value(); accountTotalValue = cell->data((int)Role::TotalValue).value(); } const auto font = QVariant(cell->data(Qt::FontRole).value()); const auto alignment = QVariant(Qt::AlignRight | Qt::AlignVCenter); // setting total balance column if (columns.contains(Column::TotalBalance)) { colNum = m_columns.indexOf(Column::TotalBalance); if (colNum != -1) { const auto accountBalanceStr = QVariant::fromValue(MyMoneyUtils::formatMoney(accountBalance, m_file->security(account.currencyId()))); getCell(colNum); // only show the balance, if its a different security/currency if (m_file->security(account.currencyId()) != m_file->baseCurrency()) { cell->setData(accountBalanceStr, Qt::DisplayRole); } cell->setData(font, Qt::FontRole); cell->setData(alignment, Qt::TextAlignmentRole); } } // setting posted value column if (columns.contains(Column::PostedValue)) { colNum = m_columns.indexOf(Column::PostedValue); if (colNum != -1) { const auto accountValueStr = QVariant::fromValue(MyMoneyUtils::formatMoney(accountValue, m_file->baseCurrency())); getCell(colNum); cell->setData(accountValueStr, Qt::DisplayRole); cell->setData(font, Qt::FontRole); cell->setData(alignment, Qt::TextAlignmentRole); } } // setting total value column if (columns.contains(Column::TotalValue)) { colNum = m_columns.indexOf(Column::TotalValue); if (colNum != -1) { const auto accountTotalValueStr = QVariant::fromValue(MyMoneyUtils::formatMoney(accountTotalValue, m_file->baseCurrency())); getCell(colNum); QColor color; if (accountTotalValue.isNegative()) color = KMyMoneyGlobalSettings::schemeColor(SchemeColor::Negative); else color = KColorScheme(QPalette::Active).foreground(KColorScheme::NormalText).color(); cell->setData(accountTotalValueStr, Qt::DisplayRole); cell->setData(font, Qt::FontRole); cell->setData(QVariant(color), Qt::ForegroundRole); cell->setData(alignment, Qt::TextAlignmentRole); } } } /** * Compute the balance of the given account. * * @param account The account for which the balance is being computed. */ MyMoneyMoney balance(const MyMoneyAccount &account) { MyMoneyMoney balance; // a closed account has a zero balance by definition if (!account.isClosed()) { // account.balance() is not compatable with stock accounts if (account.isInvest()) balance = m_file->balance(account.id()); else balance = account.balance(); } // for income and liability accounts, we reverse the sign switch (account.accountGroup()) { case Account::Type::Income: case Account::Type::Liability: case Account::Type::Equity: balance = -balance; break; default: break; } return balance; } /** * Compute the value of the given account using the provided balance. * The value is defined as the balance of the account converted to the base currency. * * @param account The account for which the value is being computed. * @param balance The balance which should be used. * * @see balance */ MyMoneyMoney value(const MyMoneyAccount &account, const MyMoneyMoney &balance) { if (account.isClosed()) return MyMoneyMoney(); QList prices; MyMoneySecurity security = m_file->baseCurrency(); try { if (account.isInvest()) { security = m_file->security(account.currencyId()); prices += m_file->price(account.currencyId(), security.tradingCurrency()); if (security.tradingCurrency() != m_file->baseCurrency().id()) { MyMoneySecurity sec = m_file->security(security.tradingCurrency()); prices += m_file->price(sec.id(), m_file->baseCurrency().id()); } } else if (account.currencyId() != m_file->baseCurrency().id()) { security = m_file->security(account.currencyId()); prices += m_file->price(account.currencyId(), m_file->baseCurrency().id()); } } catch (const MyMoneyException &e) { qDebug() << Q_FUNC_INFO << " caught exception while adding " << account.name() << "[" << account.id() << "]: " << e.what(); } MyMoneyMoney value = balance; { QList::const_iterator it_p; QString security = account.currencyId(); for (it_p = prices.constBegin(); it_p != prices.constEnd(); ++it_p) { value = (value * (MyMoneyMoney::ONE / (*it_p).rate(security))).convertPrecision(m_file->security(security).pricePrecision()); if ((*it_p).from() == security) security = (*it_p).to(); else security = (*it_p).from(); } value = value.convert(m_file->baseCurrency().smallestAccountFraction()); } return value; } /** * Compute the total value of the child accounts of the given account. * Note that the value of the current account is not in this sum. Also, * before calling this function, the caller must make sure that the values * of all sub-account must be already in the model in the @ref Role::Value. * * @param index The index of the account in the model. * @see value */ MyMoneyMoney childrenTotalValue(const QStandardItem *node, const bool isInstitutionsModel = false) { MyMoneyMoney totalValue; if (!node) return totalValue; for (auto i = 0; i < node->rowCount(); ++i) { const auto childNode = node->child(i, (int)Column::Account); if (childNode->hasChildren()) totalValue += childrenTotalValue(childNode, isInstitutionsModel); const auto data = childNode->data((int)Role::Value); if (data.isValid()) { auto value = data.value(); if (isInstitutionsModel) { const auto account = childNode->data((int)Role::Account).value(); if (account.accountGroup() == Account::Type::Liability) value = -value; } totalValue += value; } } return totalValue; } /** * Function to get the item from an account id. * * @param parent The parent to localize the search in the child items of this parameter. * @param accountId Search based on this parameter. * * @return The item corresponding to the given account id, NULL if the account was not found. */ QStandardItem *itemFromAccountId(QStandardItem *parent, const QString &accountId) { auto const model = parent->model(); const auto list = model->match(model->index(0, 0, parent->index()), (int)Role::ID, QVariant(accountId), 1, Qt::MatchFlags(Qt::MatchExactly | Qt::MatchCaseSensitive)); if (!list.isEmpty()) return model->itemFromIndex(list.front()); // TODO: if not found at this item search for it in the model and if found reparent it. return nullptr; } /** * Function to get the item from an account id without knowing it's parent item. * Note that for the accounts which have two items in the model (favorite accounts) * the account item which is not the child of the favorite accounts item is always returned. * * @param model The model in which to search. * @param accountId Search based on this parameter. * * @return The item corresponding to the given account id, NULL if the account was not found. */ QStandardItem *itemFromAccountId(QStandardItemModel *model, const QString &accountId) { const auto list = model->match(model->index(0, 0), (int)Role::ID, QVariant(accountId), -1, Qt::MatchFlags(Qt::MatchExactly | Qt::MatchCaseSensitive | Qt::MatchRecursive)); foreach (const QModelIndex &index, list) { // always return the account which is not the child of the favorite accounts item if (index.parent().data((int)Role::ID).toString() != AccountsModel::favoritesAccountId) return model->itemFromIndex(index); } return nullptr; } /** * Used to load the accounts data. */ MyMoneyFile *m_file; /** * Used to emit the @ref netWorthChanged signal. */ MyMoneyMoney m_lastNetWorth; /** * Used to emit the @ref profitChanged signal. */ MyMoneyMoney m_lastProfit; /** * Used to set the reconciliation flag. */ MyMoneyAccount m_reconciledAccount; QList m_columns; static const QString m_accountsModelConfGroup; static const QString m_accountsModelColumnSelection; }; const QString AccountsModel::Private::m_accountsModelConfGroup = QStringLiteral("AccountsModel"); const QString AccountsModel::Private::m_accountsModelColumnSelection = QStringLiteral("ColumnSelection"); const QString AccountsModel::favoritesAccountId(QStringLiteral("Favorites")); /** * The constructor is private so that only the @ref Models object can create such an object. */ AccountsModel::AccountsModel(QObject *parent /*= 0*/) : QStandardItemModel(parent), d(new Private) { init(); } AccountsModel::AccountsModel(Private* const priv, QObject *parent /*= 0*/) : QStandardItemModel(parent), d(priv) { init(); } AccountsModel::~AccountsModel() { delete d; } void AccountsModel::init() { QStringList headerLabels; foreach (const auto column, d->m_columns) headerLabels.append(getHeaderName(column)); setHorizontalHeaderLabels(headerLabels); } /** * Perform the initial load of the model data * from the @ref MyMoneyFile. * */ void AccountsModel::load() { this->blockSignals(true); QStandardItem *rootItem = invisibleRootItem(); QFont font; font.setBold(true); // adding favourite accounts node auto favoriteAccountsItem = new QStandardItem(); favoriteAccountsItem->setEditable(false); rootItem->appendRow(favoriteAccountsItem); { QMap itemData; itemData[Qt::DisplayRole] = itemData[Qt::EditRole] = itemData[(int)Role::FullName] = i18n("Favorites"); itemData[Qt::FontRole] = font; itemData[Qt::DecorationRole] = QIcon::fromTheme(g_Icons.value(Icon::ViewBankAccount)); itemData[(int)Role::ID] = favoritesAccountId; itemData[(int)Role::DisplayOrder] = 0; this->setItemData(favoriteAccountsItem->index(), itemData); } // adding account categories (asset, liability, etc.) node QVector categories { Account::Type::Asset, Account::Type::Liability, Account::Type::Income, Account::Type::Expense, Account::Type::Equity }; foreach (const auto category, categories) { MyMoneyAccount account; QString accountName; int displayOrder; switch (category) { case Account::Type::Asset: // Asset accounts account = d->m_file->asset(); accountName = i18n("Asset accounts"); displayOrder = 1; break; case Account::Type::Liability: // Liability accounts account = d->m_file->liability(); accountName = i18n("Liability accounts"); displayOrder = 2; break; case Account::Type::Income: // Income categories account = d->m_file->income(); accountName = i18n("Income categories"); displayOrder = 3; break; case Account::Type::Expense: // Expense categories account = d->m_file->expense(); accountName = i18n("Expense categories"); displayOrder = 4; break; case Account::Type::Equity: // Equity accounts account = d->m_file->equity(); accountName = i18n("Equity accounts"); displayOrder = 5; break; default: continue; } auto accountsItem = new QStandardItem(accountName); accountsItem->setEditable(false); rootItem->appendRow(accountsItem); { QMap itemData; itemData[Qt::DisplayRole] = accountName; itemData[(int)Role::FullName] = itemData[Qt::EditRole] = QVariant::fromValue(MyMoneyFile::instance()->accountToCategory(account.id(), true)); itemData[Qt::FontRole] = font; itemData[(int)Role::DisplayOrder] = displayOrder; this->setItemData(accountsItem->index(), itemData); } // adding accounts (specific bank/investment accounts) belonging to given accounts category foreach (const auto accStr, account.accountList()) { const auto acc = d->m_file->account(accStr); auto item = new QStandardItem(acc.name()); accountsItem->appendRow(item); item->setEditable(false); auto subaccountsStr = acc.accountList(); // filter out stocks with zero balance if requested by user for (auto subaccStr = subaccountsStr.begin(); subaccStr != subaccountsStr.end();) { const auto subacc = d->m_file->account(*subaccStr); if (subacc.isInvest() && KMyMoneyGlobalSettings::hideZeroBalanceEquities() && subacc.balance().isZero()) subaccStr = subaccountsStr.erase(subaccStr); else ++subaccStr; } // adding subaccounts (e.g. stocks under given investment account) belonging to given account d->loadSubaccounts(item, favoriteAccountsItem, subaccountsStr); const auto row = item->row(); d->setAccountData(accountsItem, row, acc, d->m_columns); d->loadPreferredAccount(acc, accountsItem, row, favoriteAccountsItem); } d->setAccountData(rootItem, accountsItem->row(), account, d->m_columns); } checkNetWorth(); checkProfit(); this->blockSignals(false); } QModelIndex AccountsModel::accountById(const QString& id) const { QModelIndexList accountList = match(index(0, 0), (int)Role::ID, id, 1, Qt::MatchFlags(Qt::MatchExactly | Qt::MatchRecursive)); if(accountList.count() == 1) { return accountList.first(); } return QModelIndex(); } QList *AccountsModel::getColumns() { return &d->m_columns; } void AccountsModel::setColumnVisibility(const Column column, const bool show) { const auto ixCol = d->m_columns.indexOf(column); // get column index in our column's map if (!show && ixCol != -1) { // start removing column row by row from bottom to up d->m_columns.removeOne(column); // remove it from our column's map blockSignals(true); // block signals to not emit resources consuming dataChanged for (auto i = 0; i < rowCount(); ++i) { // recursive lambda function to remove cell belonging to unwanted column from all rows auto removeCellFromRow = [=](auto &&self, QStandardItem *item) -> bool { for(auto j = 0; j < item->rowCount(); ++j) { auto childItem = item->child(j); if (childItem->hasChildren()) self(self, childItem); childItem->removeColumn(ixCol); } return true; }; auto topItem = item(i); if (topItem->hasChildren()) removeCellFromRow(removeCellFromRow, topItem); topItem->removeColumn(ixCol); } blockSignals(false); // unblock signals, so model can update itself with new column removeColumn(ixCol); // remove column from invisible root item which triggers model's view update } else if (show && ixCol == -1) { // start inserting columns row by row from up to bottom (otherwise columns will be inserted automatically) auto model = qobject_cast(this); const auto isInstitutionsModel = model ? true : false; // if it's institution's model, then don't set any data on institution nodes auto newColPos = 0; for(; newColPos < d->m_columns.count(); ++newColPos) { if (d->m_columns.at(newColPos) > column) break; } d->m_columns.insert(newColPos, column); // insert columns according to enum order for cleanliness insertColumn(newColPos); setHorizontalHeaderItem(newColPos, new QStandardItem(getHeaderName(column))); blockSignals(true); for (auto i = 0; i < rowCount(); ++i) { // recursive lambda function to remove cell belonging to unwanted column from all rows auto addCellToRow = [&, newColPos](auto &&self, QStandardItem *item) -> bool { for(auto j = 0; j < item->rowCount(); ++j) { auto childItem = item->child(j); childItem->insertColumns(newColPos, 1); if (childItem->hasChildren()) self(self, childItem); this->d->setAccountData(item, j, childItem->data((int)Role::Account).value(), QList {column}); } return true; }; auto topItem = item(i); topItem->insertColumns(newColPos, 1); if (topItem->hasChildren()) addCellToRow(addCellToRow, topItem); if (isInstitutionsModel) d->setInstitutionTotalValue(invisibleRootItem(), i); else if (i !=0) // favourites node doesn't play well here, so exclude it from update d->setAccountData(invisibleRootItem(), i, topItem->data((int)Role::Account).value(), QList {column}); } blockSignals(false); } } QString AccountsModel::getHeaderName(const Column column) { switch(column) { case Column::Account: return i18n("Account"); case Column::Type: return i18n("Type"); case Column::Tax: return i18nc("Column heading for category in tax report", "Tax"); case Column::VAT: return i18nc("Column heading for VAT category", "VAT"); case Column::CostCenter: return i18nc("Column heading for Cost Center", "CC"); case Column::TotalBalance: return i18n("Total Balance"); case Column::PostedValue: return i18n("Posted Value"); case Column::TotalValue: return i18n("Total Value"); case Column::AccountNumber: return i18n("Number"); case Column::AccountSortCode: return i18nc("IBAN, SWIFT, etc.", "Sort Code"); default: return QString(); } } /** * Check if netWorthChanged should be emitted. */ void AccountsModel::checkNetWorth() { // compute the net woth QModelIndexList assetList = match(index(0, 0), (int)Role::ID, MyMoneyFile::instance()->asset().id(), 1, Qt::MatchFlags(Qt::MatchExactly | Qt::MatchCaseSensitive)); QModelIndexList liabilityList = match(index(0, 0), (int)Role::ID, MyMoneyFile::instance()->liability().id(), 1, Qt::MatchFlags(Qt::MatchExactly | Qt::MatchCaseSensitive)); MyMoneyMoney netWorth; if (!assetList.isEmpty() && !liabilityList.isEmpty()) { const auto assetValue = data(assetList.front(), (int)Role::TotalValue); const auto liabilityValue = data(liabilityList.front(), (int)Role::TotalValue); if (assetValue.isValid() && liabilityValue.isValid()) netWorth = assetValue.value() - liabilityValue.value(); } if (d->m_lastNetWorth != netWorth) { d->m_lastNetWorth = netWorth; emit netWorthChanged(d->m_lastNetWorth); } } /** * Check if profitChanged should be emitted. */ void AccountsModel::checkProfit() { // compute the profit const auto incomeList = match(index(0, 0), (int)Role::ID, MyMoneyFile::instance()->income().id(), 1, Qt::MatchFlags(Qt::MatchExactly | Qt::MatchCaseSensitive)); const auto expenseList = match(index(0, 0), (int)Role::ID, MyMoneyFile::instance()->expense().id(), 1, Qt::MatchFlags(Qt::MatchExactly | Qt::MatchCaseSensitive)); MyMoneyMoney profit; if (!incomeList.isEmpty() && !expenseList.isEmpty()) { const auto incomeValue = data(incomeList.front(), (int)Role::TotalValue); const auto expenseValue = data(expenseList.front(), (int)Role::TotalValue); if (incomeValue.isValid() && expenseValue.isValid()) profit = incomeValue.value() - expenseValue.value(); } if (d->m_lastProfit != profit) { d->m_lastProfit = profit; emit profitChanged(d->m_lastProfit); } } MyMoneyMoney AccountsModel::accountValue(const MyMoneyAccount &account, const MyMoneyMoney &balance) { return d->value(account, balance); } /** * This slot should be connected so that the model will be notified which account is being reconciled. */ void AccountsModel::slotReconcileAccount(const MyMoneyAccount &account, const QDate &reconciliationDate, const MyMoneyMoney &endingBalance) { Q_UNUSED(reconciliationDate) Q_UNUSED(endingBalance) if (d->m_reconciledAccount.id() != account.id()) { // first clear the flag of the old reconciliation account if (!d->m_reconciledAccount.id().isEmpty()) { const auto list = match(index(0, 0), (int)Role::ID, QVariant(d->m_reconciledAccount.id()), -1, Qt::MatchFlags(Qt::MatchExactly | Qt::MatchCaseSensitive | Qt::MatchRecursive)); foreach (const auto index, list) setData(index, QVariant(QIcon(account.accountPixmap(false))), Qt::DecorationRole); } // then set the reconciliation flag of the new reconciliation account const auto list = match(index(0, 0), (int)Role::ID, QVariant(account.id()), -1, Qt::MatchFlags(Qt::MatchExactly | Qt::MatchCaseSensitive | Qt::MatchRecursive)); foreach (const auto index, list) setData(index, QVariant(QIcon(account.accountPixmap(true))), Qt::DecorationRole); d->m_reconciledAccount = account; } } /** * Notify the model that an object has been added. An action is performed only if the object is an account. * */ void AccountsModel::slotObjectAdded(File::Object objType, const MyMoneyObject * const obj) { if (objType != File::Object::Account) return; const MyMoneyAccount * const account = dynamic_cast(obj); if (!account) return; auto favoriteAccountsItem = d->itemFromAccountId(this, favoritesAccountId); auto parentAccountItem = d->itemFromAccountId(this, account->parentAccountId()); auto item = d->itemFromAccountId(parentAccountItem, account->id()); if (!item) { item = new QStandardItem(account->name()); parentAccountItem->appendRow(item); item->setEditable(false); } // load the sub-accounts if there are any - there could be sub accounts if this is an add operation // that was triggered in slotObjectModified on an already existing account which went trough a hierarchy change d->loadSubaccounts(item, favoriteAccountsItem, account->accountList()); const auto row = item->row(); d->setAccountData(parentAccountItem, row, *account, d->m_columns); d->loadPreferredAccount(*account, parentAccountItem, row, favoriteAccountsItem); checkNetWorth(); checkProfit(); } /** * Notify the model that an object has been modified. An action is performed only if the object is an account. * */ void AccountsModel::slotObjectModified(File::Object objType, const MyMoneyObject * const obj) { if (objType != File::Object::Account) return; const MyMoneyAccount * const account = dynamic_cast(obj); if (!account) return; auto favoriteAccountsItem = d->itemFromAccountId(this, favoritesAccountId); auto accountItem = d->itemFromAccountId(this, account->id()); const auto oldAccount = accountItem->data((int)Role::Account).value(); if (oldAccount.parentAccountId() == account->parentAccountId()) { // the hierarchy did not change so update the account data auto parentAccountItem = accountItem->parent(); if (!parentAccountItem) parentAccountItem = this->invisibleRootItem(); const auto row = accountItem->row(); d->setAccountData(parentAccountItem, row, *account, d->m_columns); // and the child of the favorite item if the account is a favorite account or it's favorite status has just changed auto favItem = d->itemFromAccountId(favoriteAccountsItem, account->id()); if (account->value("PreferredAccount") == QLatin1String("Yes")) d->loadPreferredAccount(*account, parentAccountItem, row, favoriteAccountsItem); else if (favItem) favoriteAccountsItem->removeRow(favItem->row()); // it's not favorite anymore } else { // this means that the hierarchy was changed - simulate this with a remove followed by and add operation slotObjectRemoved(File::Object::Account, oldAccount.id()); slotObjectAdded(File::Object::Account, obj); } checkNetWorth(); checkProfit(); } /** * Notify the model that an object has been removed. An action is performed only if the object is an account. * */ void AccountsModel::slotObjectRemoved(File::Object objType, const QString& id) { if (objType != File::Object::Account) return; auto list = match(index(0, 0), (int)Role::ID, id, -1, Qt::MatchFlags(Qt::MatchExactly | Qt::MatchRecursive)); foreach (const auto index, list) removeRow(index.row(), index.parent()); checkNetWorth(); checkProfit(); } /** * Notify the model that the account balance has been changed. */ void AccountsModel::slotBalanceOrValueChanged(const MyMoneyAccount &account) { auto itParent = d->itemFromAccountId(this, account.id()); // get node of account in model auto isTopLevel = false; // it could be top-level but we don't know it yet while (itParent && !isTopLevel) { // loop in which we set total values and balances from the bottom to the top auto itCurrent = itParent; const auto accCurrent = d->m_file->account(itCurrent->data((int)Role::Account).value().id()); if (accCurrent.id().isEmpty()) { // this is institution d->setInstitutionTotalValue(invisibleRootItem(), itCurrent->row()); break; // it's top-level node so nothing above that; } itParent = itCurrent->parent(); if (!itParent) { itParent = this->invisibleRootItem(); isTopLevel = true; } d->setAccountBalanceAndValue(itParent, itCurrent->row(), accCurrent, d->m_columns); } checkNetWorth(); checkProfit(); } /** * The pimpl of the @ref InstitutionsModel derived from the pimpl of the @ref AccountsModel. */ class InstitutionsModel::InstitutionsPrivate : public AccountsModel::Private { public: /** * Function to get the institution item from an institution id. * * @param model The model in which to look for the item. * @param institutionId Search based on this parameter. * * @return The item corresponding to the given institution id, NULL if the institution was not found. */ QStandardItem *institutionItemFromId(QStandardItemModel *model, const QString &institutionId) { const auto list = model->match(model->index(0, 0), (int)Role::ID, QVariant(institutionId), 1, Qt::MatchFlags(Qt::MatchExactly | Qt::MatchCaseSensitive)); if (!list.isEmpty()) return model->itemFromIndex(list.front()); return nullptr; // this should rarely fail as we add all institutions early on } /** * Function to add the account item to it's corresponding institution item. * * @param model The model where to add the item. * @param account The account for which to create the item. * */ void loadInstitution(QStandardItemModel *model, const MyMoneyAccount &account) { if (!account.isAssetLiability() && !account.isInvest()) return; // we've got account but don't know under which institution it should be added, so we find it out auto idInstitution = account.institutionId(); if (account.isInvest()) { // if it's stock account then... const auto investmentAccount = m_file->account(account.parentAccountId()); // ...get investment account it's under and... idInstitution = investmentAccount.institutionId(); // ...get institution from investment account } auto itInstitution = institutionItemFromId(model, idInstitution); auto itAccount = itemFromAccountId(itInstitution, account.id()); // check if account already exists under institution // only stock accounts are added to their parent in the institutions view // this makes hierarchy maintenance a lot easier since the stock accounts // are the only ones that always have the same institution as their parent auto itInvestmentAccount = account.isInvest() ? itemFromAccountId(itInstitution, account.parentAccountId()) : nullptr; if (!itAccount) { itAccount = new QStandardItem(account.name()); if (itInvestmentAccount) // stock account nodes go under investment account nodes and... itInvestmentAccount->appendRow(itAccount); else if (itInstitution) // ...the rest goes under institution's node itInstitution->appendRow(itAccount); else return; itAccount->setEditable(false); } if (itInvestmentAccount) { setAccountData(itInvestmentAccount, itAccount->row(), account, m_columns); // set data for stock account node setAccountData(itInstitution, itInvestmentAccount->row(), m_file->account(account.parentAccountId()), m_columns); // set data for investment account node } else if (itInstitution) { setAccountData(itInstitution, itAccount->row(), account, m_columns); } } /** * Function to add an institution item to the model. * * @param model The model in which to add the item. * @param institution The institution object which should be represented by the item. * */ void addInstitutionItem(QStandardItemModel *model, const MyMoneyInstitution &institution) { QFont font; font.setBold(true); auto itInstitution = new QStandardItem(QIcon::fromTheme(g_Icons.value(Icon::ViewInstitutions)), institution.name()); itInstitution->setFont(font); itInstitution->setData(QVariant::fromValue(MyMoneyMoney()), (int)Role::TotalValue); itInstitution->setData(institution.id(), (int)Role::ID); itInstitution->setData(QVariant::fromValue(institution), (int)Role::Account); itInstitution->setData(6, (int)Role::DisplayOrder); itInstitution->setEditable(false); model->invisibleRootItem()->appendRow(itInstitution); setInstitutionTotalValue(model->invisibleRootItem(), itInstitution->row()); } }; /** * The institution model contains the accounts grouped by institution. * */ InstitutionsModel::InstitutionsModel(QObject *parent /*= 0*/) : AccountsModel(new InstitutionsPrivate, parent) { } /** * Perform the initial load of the model data * from the @ref MyMoneyFile. * */ void InstitutionsModel::load() { // create items for all the institutions QList institutionList; d->m_file->institutionList(institutionList); MyMoneyInstitution none; none.setName(i18n("Accounts with no institution assigned")); institutionList.append(none); auto modelUtils = static_cast(d); foreach (const auto institution, institutionList) // add all known institutions as top-level nodes modelUtils->addInstitutionItem(this, institution); QList accountsList; QList stocksList; d->m_file->accountList(accountsList); foreach (const auto account, accountsList) { // add account nodes under institution nodes... if (account.isInvest()) // ...but wait with stocks until investment accounts appear stocksList.append(account); else modelUtils->loadInstitution(this, account); } foreach (const auto stock, stocksList) { if (!(KMyMoneyGlobalSettings::hideZeroBalanceEquities() && stock.balance().isZero())) modelUtils->loadInstitution(this, stock); } for (auto i = 0 ; i < rowCount(); ++i) d->setInstitutionTotalValue(invisibleRootItem(), i); } /** * Notify the model that an object has been added. An action is performed only if the object is an account or an institution. * */ void InstitutionsModel::slotObjectAdded(File::Object objType, const MyMoneyObject * const obj) { auto modelUtils = static_cast(d); if (objType == File::Object::Institution) { // if an institution was added then add the item which will represent it const MyMoneyInstitution * const institution = dynamic_cast(obj); if (!institution) return; modelUtils->addInstitutionItem(this, *institution); } if (objType != File::Object::Account) return; // if an account was added then add the item which will represent it only for real accounts const MyMoneyAccount * const account = dynamic_cast(obj); // nothing to do for root accounts and categories if (!account || account->parentAccountId().isEmpty() || account->isIncomeExpense()) return; // load the account into the institution modelUtils->loadInstitution(this, *account); // load the investment sub-accounts if there are any - there could be sub-accounts if this is an add operation // that was triggered in slotObjectModified on an already existing account which went trough a hierarchy change const auto sAccounts = account->accountList(); if (!sAccounts.isEmpty()) { QList subAccounts; d->m_file->accountList(subAccounts, sAccounts); foreach (const auto subAccount, subAccounts) { if (subAccount.isInvest()) { modelUtils->loadInstitution(this, subAccount); } } } } /** * Notify the model that an object has been modified. An action is performed only if the object is an account or an institution. * */ void InstitutionsModel::slotObjectModified(File::Object objType, const MyMoneyObject * const obj) { if (objType == File::Object::Institution) { // if an institution was modified then modify the item which represents it const MyMoneyInstitution * const institution = dynamic_cast(obj); if (!institution) return; auto institutionItem = static_cast(d)->institutionItemFromId(this, institution->id()); institutionItem->setData(institution->name(), Qt::DisplayRole); institutionItem->setData(QVariant::fromValue(*institution), (int)Role::Account); institutionItem->setIcon(institution->pixmap()); } if (objType != File::Object::Account) return; // if an account was modified then modify the item which represents it const MyMoneyAccount * const account = dynamic_cast(obj); // nothing to do for root accounts, categories and equity accounts since they don't have a representation in this model if (!account || account->parentAccountId().isEmpty() || account->isIncomeExpense() || account->accountType() == Account::Type::Equity) return; auto accountItem = d->itemFromAccountId(this, account->id()); const auto oldAccount = accountItem->data((int)Role::Account).value(); if (oldAccount.institutionId() == account->institutionId()) { // the hierarchy did not change so update the account data d->setAccountData(accountItem->parent(), accountItem->row(), *account, d->m_columns); } else { // this means that the hierarchy was changed - simulate this with a remove followed by and add operation slotObjectRemoved(File::Object::Account, oldAccount.id()); slotObjectAdded(File::Object::Account, obj); } } /** * Notify the model that an object has been removed. An action is performed only if the object is an account or an institution. * */ void InstitutionsModel::slotObjectRemoved(File::Object objType, const QString& id) { if (objType == File::Object::Institution) { // if an institution was removed then remove the item which represents it auto itInstitution = static_cast(d)->institutionItemFromId(this, id); if (itInstitution) removeRow(itInstitution->row(), itInstitution->index().parent()); } if (objType != File::Object::Account) return; // if an account was removed then remove the item which represents it and recompute the institution's value auto itAccount = d->itemFromAccountId(this, id); if (!itAccount) return; // this could happen if the account isIncomeExpense const auto account = itAccount->data((int)Role::Account).value(); auto itInstitution = d->itemFromAccountId(this, account.institutionId()); AccountsModel::slotObjectRemoved(objType, id); d->setInstitutionTotalValue(invisibleRootItem(), itInstitution->row()); } diff --git a/kmymoney/models/ledgermodel.cpp b/kmymoney/models/ledgermodel.cpp index 25d589866..e5f5738d0 100644 --- a/kmymoney/models/ledgermodel.cpp +++ b/kmymoney/models/ledgermodel.cpp @@ -1,653 +1,654 @@ /*************************************************************************** ledgermodel.cpp ------------------- begin : Sat Aug 8 2015 copyright : (C) 2015 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 "ledgermodel.h" // ---------------------------------------------------------------------------- // QT Includes #include #include // ---------------------------------------------------------------------------- // KDE Includes #include // ---------------------------------------------------------------------------- // Project Includes #include "ledgerschedule.h" #include "mymoneyschedule.h" #include "mymoneysplit.h" #include "mymoneytransaction.h" #include "mymoneytransactionfilter.h" #include "mymoneyfile.h" #include "mymoneymoney.h" +#include "mymoneyexception.h" #include "kmymoneyutils.h" #include "kmymoneyglobalsettings.h" #include "mymoneyenums.h" #include "modelenums.h" using namespace eLedgerModel; using namespace eMyMoney; class LedgerModelPrivate { public: ~LedgerModelPrivate() { qDeleteAll(m_ledgerItems); m_ledgerItems.clear(); } MyMoneyTransaction m_lastTransactionStored; QVector m_ledgerItems; }; LedgerModel::LedgerModel(QObject* parent) : QAbstractTableModel(parent), d_ptr(new LedgerModelPrivate) { MyMoneyFile* file = MyMoneyFile::instance(); connect(file, &MyMoneyFile::objectAdded, this, static_cast(&LedgerModel::addTransaction)); connect(file, &MyMoneyFile::objectModified, this, &LedgerModel::modifyTransaction); connect(file, &MyMoneyFile::objectRemoved, this, &LedgerModel::removeTransaction); connect(file, &MyMoneyFile::objectAdded, this, &LedgerModel::addSchedule); connect(file, &MyMoneyFile::objectModified, this, &LedgerModel::modifySchedule); connect(file, &MyMoneyFile::objectRemoved, this, &LedgerModel::removeSchedule); } LedgerModel::~LedgerModel() { } int LedgerModel::rowCount(const QModelIndex& parent) const { // since the ledger model is a simple table model, we only // return the rowCount for the hiddenRootItem. and zero otherwise if(parent.isValid()) { return 0; } Q_D(const LedgerModel); return d->m_ledgerItems.count(); } int LedgerModel::columnCount(const QModelIndex& parent) const { Q_UNUSED(parent); return (int)Column::LastColumn; } Qt::ItemFlags LedgerModel::flags(const QModelIndex& index) const { Q_D(const LedgerModel); Qt::ItemFlags flags; if(!index.isValid()) return flags; if(index.row() < 0 || index.row() >= d->m_ledgerItems.count()) return flags; return d->m_ledgerItems[index.row()]->flags(); } QVariant LedgerModel::headerData(int section, Qt::Orientation orientation, int role) const { if(orientation == Qt::Horizontal && role == Qt::DisplayRole) { switch(section) { case (int)Column::Number: return i18nc("Cheque Number", "No."); case (int)Column::Date: return i18n("Date"); case (int)Column::Security: return i18n("Security"); case (int)Column::CostCenter: return i18n("CC"); case (int)Column::Detail: return i18n("Detail"); case (int)Column::Reconciliation: return i18n("C"); case (int)Column::Payment: return i18nc("Payment made from account", "Payment"); case (int)Column::Deposit: return i18nc("Deposit into account", "Deposit"); case (int)Column::Quantity: return i18n("Quantity"); case (int)Column::Price: return i18n("Price"); case (int)Column::Amount: return i18n("Amount"); case (int)Column::Value: return i18n("Value"); case (int)Column::Balance: return i18n("Balance"); } } else if(orientation == Qt::Vertical && role == Qt::SizeHintRole) { // as small as possible, so that the delegate has a chance // to override the information return QSize(10, 10); } return QAbstractItemModel::headerData(section, orientation, role); } QVariant LedgerModel::data(const QModelIndex& index, int role) const { Q_D(const LedgerModel); if(!index.isValid()) return QVariant(); if(index.row() < 0 || index.row() >= d->m_ledgerItems.count()) return QVariant(); QVariant rc; switch(role) { case Qt::DisplayRole: // make sure to never return any displayable text for the dummy entry if(!d->m_ledgerItems[index.row()]->transactionSplitId().isEmpty()) { switch(index.column()) { case (int)Column::Number: rc = d->m_ledgerItems[index.row()]->transactionNumber(); break; case (int)Column::Date: rc = QLocale().toString(d->m_ledgerItems[index.row()]->postDate(), QLocale::ShortFormat); break; case (int)Column::Detail: rc = d->m_ledgerItems[index.row()]->counterAccount(); break; case (int)Column::Reconciliation: rc = d->m_ledgerItems[index.row()]->reconciliationStateShort(); break; case (int)Column::Payment: rc = d->m_ledgerItems[index.row()]->payment(); break; case (int)Column::Deposit: rc = d->m_ledgerItems[index.row()]->deposit(); break; case (int)Column::Amount: rc = d->m_ledgerItems[index.row()]->signedSharesAmount(); break; case (int)Column::Balance: rc = d->m_ledgerItems[index.row()]->balance(); break; } } break; case Qt::TextAlignmentRole: switch(index.column()) { case (int)Column::Payment: case (int)Column::Deposit: case (int)Column::Amount: case (int)Column::Balance: case (int)Column::Value: rc = QVariant(Qt::AlignRight| Qt::AlignTop); break; case (int)Column::Reconciliation: rc = QVariant(Qt::AlignHCenter | Qt::AlignTop); break; default: rc = QVariant(Qt::AlignLeft | Qt::AlignTop); break; } break; case Qt::BackgroundColorRole: if(d->m_ledgerItems[index.row()]->isImported()) { return KMyMoneyGlobalSettings::schemeColor(SchemeColor::TransactionImported); } break; case (int)Role::CounterAccount: rc = d->m_ledgerItems[index.row()]->counterAccount(); break; case (int)Role::SplitCount: rc = d->m_ledgerItems[index.row()]->splitCount(); break; case (int)Role::CostCenterId: rc = d->m_ledgerItems[index.row()]->costCenterId(); break; case (int)Role::PostDate: rc = d->m_ledgerItems[index.row()]->postDate(); break; case (int)Role::PayeeName: rc = d->m_ledgerItems[index.row()]->payeeName(); break; case (int)Role::PayeeId: rc = d->m_ledgerItems[index.row()]->payeeId(); break; case (int)Role::AccountId: rc = d->m_ledgerItems[index.row()]->accountId(); break; case Qt::EditRole: case (int)Role::TransactionSplitId: rc = d->m_ledgerItems[index.row()]->transactionSplitId(); break; case (int)Role::TransactionId: rc = d->m_ledgerItems[index.row()]->transactionId(); break; case (int)Role::Reconciliation: rc = (int)d->m_ledgerItems[index.row()]->reconciliationState(); break; case (int)Role::ReconciliationShort: rc = d->m_ledgerItems[index.row()]->reconciliationStateShort(); break; case (int)Role::ReconciliationLong: rc = d->m_ledgerItems[index.row()]->reconciliationStateLong(); break; case (int)Role::SplitValue: rc.setValue(d->m_ledgerItems[index.row()]->value()); break; case (int)Role::SplitShares: rc.setValue(d->m_ledgerItems[index.row()]->shares()); break; case (int)Role::ShareAmount: rc.setValue(d->m_ledgerItems[index.row()]->sharesAmount()); break; case (int)Role::ShareAmountSuffix: rc.setValue(d->m_ledgerItems[index.row()]->sharesSuffix()); break; case (int)Role::ScheduleId: { LedgerSchedule* schedule = 0; schedule = dynamic_cast(d->m_ledgerItems[index.row()]); if(schedule) { rc = schedule->scheduleId(); } break; } case (int)Role::Memo: case (int)Role::SingleLineMemo: rc.setValue(d->m_ledgerItems[index.row()]->memo()); if(role == (int)Role::SingleLineMemo) { QString txt = rc.toString(); // remove empty lines txt.replace("\n\n", "\n"); // replace '\n' with ", " txt.replace('\n', ", "); rc.setValue(txt); } break; case (int)Role::Number: rc = d->m_ledgerItems[index.row()]->transactionNumber(); break; case (int)Role::Erroneous: rc = d->m_ledgerItems[index.row()]->isErroneous(); break; case (int)Role::Import: rc = d->m_ledgerItems[index.row()]->isImported(); break; case (int)Role::CounterAccountId: rc = d->m_ledgerItems[index.row()]->counterAccountId(); break; case (int)Role::TransactionCommodity: rc = d->m_ledgerItems[index.row()]->transactionCommodity(); break; case (int)Role::Transaction: rc.setValue(d->m_ledgerItems[index.row()]->transaction()); break; case (int)Role::Split: rc.setValue(d->m_ledgerItems[index.row()]->split()); break; } return rc; } bool LedgerModel::setData(const QModelIndex& index, const QVariant& value, int role) { Q_D(LedgerModel); if(!index.isValid()) { return false; } if(role == Qt::DisplayRole && index.column() == (int)Column::Balance) { d->m_ledgerItems[index.row()]->setBalance(value.toString()); return true; } qDebug() << "setData(" << index.row() << index.column() << ")" << value << role; return QAbstractItemModel::setData(index, value, role); } void LedgerModel::unload() { Q_D(LedgerModel); if(rowCount() > 0) { beginRemoveRows(QModelIndex(), 0, rowCount() - 1); for(int i = 0; i < rowCount(); ++i) { delete d->m_ledgerItems[i]; } d->m_ledgerItems.clear(); endRemoveRows(); } } void LedgerModel::addTransactions(const QList< QPair >& list) { Q_D(LedgerModel); if(list.count() > 0) { beginInsertRows(QModelIndex(), rowCount(), rowCount() + list.count() - 1); QList< QPair >::const_iterator it; for(it = list.constBegin(); it != list.constEnd(); ++it) { d->m_ledgerItems.append(new LedgerTransaction((*it).first, (*it).second)); } endInsertRows(); } } void LedgerModel::addTransaction(const LedgerTransaction& t) { Q_D(LedgerModel); beginInsertRows(QModelIndex(), rowCount(), rowCount()); d->m_ledgerItems.append(new LedgerTransaction(t.transaction(), t.split())); endInsertRows(); } void LedgerModel::addTransaction(const QString& transactionSplitId) { Q_D(LedgerModel); QRegExp transactionSplitIdExp("^(\\w+)-(\\w+)$"); if(transactionSplitIdExp.exactMatch(transactionSplitId)) { const QString transactionId = transactionSplitIdExp.cap(1); const QString splitId = transactionSplitIdExp.cap(2); if(transactionId != d->m_lastTransactionStored.id()) { try { d->m_lastTransactionStored = MyMoneyFile::instance()->transaction(transactionId); } catch(MyMoneyException& e) { d->m_lastTransactionStored = MyMoneyTransaction(); } } try { MyMoneySplit split = d->m_lastTransactionStored.splitById(splitId); beginInsertRows(QModelIndex(), rowCount(), rowCount()); d->m_ledgerItems.append(new LedgerTransaction(d->m_lastTransactionStored, split)); endInsertRows(); } catch(MyMoneyException& e) { d->m_lastTransactionStored = MyMoneyTransaction(); } } } void LedgerModel::addSchedules(const QList & list, int previewPeriod) { Q_D(LedgerModel); if(list.count() > 0) { QVector newList; // create dummy entries for the scheduled transactions if sorted by postdate // show scheduled transactions which have a scheduled postdate // within the next 'previewPeriod' days. In reconciliation mode, the // previewPeriod starts on the statement date. QDate endDate = QDate::currentDate().addDays(previewPeriod); #if 0 if (isReconciliationAccount()) endDate = reconciliationDate.addDays(previewPeriod); #endif QList::const_iterator it; for(it = list.constBegin(); it != list.constEnd(); ++it) { MyMoneySchedule schedule = *it; // now create entries for this schedule until the endDate is reached for (;;) { if (schedule.isFinished() || schedule.adjustedNextDueDate() > endDate) { break; } MyMoneyTransaction t(schedule.id(), KMyMoneyUtils::scheduledTransaction(schedule)); // if the transaction is scheduled and overdue, it can't // certainly be posted in the past. So we take today's date // as the alternative if (schedule.isOverdue()) { t.setPostDate(schedule.adjustedDate(QDate::currentDate(), schedule.weekendOption())); } else { t.setPostDate(schedule.adjustedNextDueDate()); } // create a model entry for each split of the schedule foreach (const auto split, t.splits()) newList.append(new LedgerSchedule(schedule, t, split)); // keep track of this payment locally (not in the engine) if (schedule.isOverdue()) { schedule.setLastPayment(QDate::currentDate()); } else { schedule.setLastPayment(schedule.nextDueDate()); } // if this is a one time schedule, we can bail out here as we're done if (schedule.occurrence() == Schedule::Occurrence::Once) break; // for all others, we check if the next payment date is still 'in range' QDate nextDueDate = schedule.nextPayment(schedule.nextDueDate()); if (nextDueDate.isValid()) { schedule.setNextDueDate(nextDueDate); } else { break; } } } beginInsertRows(QModelIndex(), rowCount(), rowCount() + newList.count() - 1); d->m_ledgerItems += newList; endInsertRows(); } } void LedgerModel::load() { qDebug() << "Start loading splits"; // load all transactions and splits into the model QList > tList; MyMoneyTransactionFilter filter; MyMoneyFile::instance()->transactionList(tList, filter); addTransactions(tList); qDebug() << "Loaded" << rowCount() << "elements"; // load all scheduled transactoins and splits into the model const int splitCount = rowCount(); QList sList = MyMoneyFile::instance()->scheduleList(); addSchedules(sList, KMyMoneyGlobalSettings::schedulePreview()); qDebug() << "Loaded" << rowCount()-splitCount << "elements"; // create a dummy entry for new transactions addTransaction(LedgerTransaction::newTransactionEntry()); qDebug() << "Loaded" << rowCount() << "elements"; } void LedgerModel::addTransaction(File::Object objType, const MyMoneyObject * const obj) { if(objType != File::Object::Transaction) { return; } Q_D(LedgerModel); qDebug() << "Adding transaction" << obj->id(); const MyMoneyTransaction * const t = static_cast(obj); beginInsertRows(QModelIndex(), rowCount(), rowCount() + t->splitCount() - 1); foreach (auto s, t->splits()) d->m_ledgerItems.append(new LedgerTransaction(*t, s)); endInsertRows(); // just make sure we're in sync Q_ASSERT(d->m_ledgerItems.count() == rowCount()); } void LedgerModel::modifyTransaction(File::Object objType, const MyMoneyObject* const obj) { if(objType != File::Object::Transaction) { return; } Q_D(LedgerModel); const MyMoneyTransaction * const t = static_cast(obj); // get indexes of all existing splits for this transaction QModelIndexList list = match(index(0, 0), (int)Role::TransactionId, obj->id(), -1); // get list of splits to be stored QList splits = t->splits(); int lastRowUsed = -1; int firstRowUsed = 99999999; if(list.count()) { firstRowUsed = list.first().row(); lastRowUsed = list.last().row(); } qDebug() << "first:" << firstRowUsed << "last:" << lastRowUsed; while(!list.isEmpty() && !splits.isEmpty()) { QModelIndex index = list.takeFirst(); MyMoneySplit split = splits.takeFirst(); // get rid of the old split and store new split qDebug() << "Modify split in row:" << index.row() << t->id() << split.id(); delete d->m_ledgerItems[index.row()]; d->m_ledgerItems[index.row()] = new LedgerTransaction(*t, split); } // inform every one else about the changes if(lastRowUsed != -1) { qDebug() << "emit dataChanged from" << firstRowUsed << "to" << lastRowUsed; emit dataChanged(index(firstRowUsed, 0), index(lastRowUsed, columnCount()-1)); } else { lastRowUsed = rowCount(); } // now check if we need to add more splits ... if(!splits.isEmpty() && list.isEmpty()) { beginInsertRows(QModelIndex(), lastRowUsed, lastRowUsed + splits.count() - 1); d->m_ledgerItems.insert(lastRowUsed, splits.count(), 0); while(!splits.isEmpty()) { MyMoneySplit split = splits.takeFirst(); d->m_ledgerItems[lastRowUsed] = new LedgerTransaction(*t, split); lastRowUsed++; } endInsertRows(); } // ... or remove some leftovers if(splits.isEmpty() && !list.isEmpty()) { firstRowUsed = lastRowUsed - list.count() + 1; beginRemoveRows(QModelIndex(), firstRowUsed, lastRowUsed); int count = 0; while(!list.isEmpty()) { ++count; QModelIndex index = list.takeFirst(); // get rid of the old split and store new split qDebug() << "Delete split in row:" << index.row() << data(index, (int)Role::TransactionSplitId).toString(); delete d->m_ledgerItems[index.row()]; } d->m_ledgerItems.remove(firstRowUsed, count); endRemoveRows(); } // just make sure we're in sync Q_ASSERT(d->m_ledgerItems.count() == rowCount()); } void LedgerModel::removeTransaction(File::Object objType, const QString& id) { if(objType != File::Object::Transaction) { return; } Q_D(LedgerModel); QModelIndexList list = match(index(0, 0), (int)Role::TransactionId, id, -1); if(list.count()) { const int firstRowUsed = list[0].row(); beginRemoveRows(QModelIndex(), firstRowUsed, firstRowUsed + list.count() - 1); for(int row = firstRowUsed; row < firstRowUsed + list.count(); ++row) { delete d->m_ledgerItems[row]; } d->m_ledgerItems.remove(firstRowUsed, list.count()); endRemoveRows(); // just make sure we're in sync Q_ASSERT(d->m_ledgerItems.count() == rowCount()); } } void LedgerModel::addSchedule(File::Object objType, const MyMoneyObject*const obj) { Q_UNUSED(obj); if(objType != File::Object::Schedule) { return; } /// @todo implement LedgerModel::addSchedule } void LedgerModel::modifySchedule(File::Object objType, const MyMoneyObject*const obj) { Q_UNUSED(obj); if(objType != File::Object::Schedule) { return; } /// @todo implement LedgerModel::modifySchedule } void LedgerModel::removeSchedule(File::Object objType, const QString& id) { Q_UNUSED(id); if(objType != File::Object::Schedule) { return; } /// @todo implement LedgerModel::removeSchedule } QString LedgerModel::transactionIdFromTransactionSplitId(const QString& transactionSplitId) const { QRegExp transactionSplitIdExp("^(\\w+)-\\w+$"); if(transactionSplitIdExp.exactMatch(transactionSplitId)) { return transactionSplitIdExp.cap(1); } return QString(); } diff --git a/kmymoney/models/splitmodel.cpp b/kmymoney/models/splitmodel.cpp index 1eab7508b..8e411cb2c 100644 --- a/kmymoney/models/splitmodel.cpp +++ b/kmymoney/models/splitmodel.cpp @@ -1,446 +1,447 @@ /*************************************************************************** ledgermodel.cpp ------------------- begin : Sat Aug 8 2015 copyright : (C) 2015 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 "splitmodel.h" // ---------------------------------------------------------------------------- // QT Includes #include #include // ---------------------------------------------------------------------------- // KDE Includes #include // ---------------------------------------------------------------------------- // Project Includes #include "models.h" #include "costcentermodel.h" #include "mymoneysplit.h" #include "mymoneytransaction.h" #include "mymoneyfile.h" #include "mymoneyaccount.h" #include "mymoneypayee.h" #include "mymoneymoney.h" +#include "mymoneyexception.h" #include "kmymoneyutils.h" #include "modelenums.h" using namespace eLedgerModel; using namespace eMyMoney; class SplitModelPrivate { public: SplitModelPrivate() : m_invertValues(false) {} bool isCreateSplitEntry(const QString& id) const { return id.isEmpty(); } MyMoneyTransaction m_transaction; QVector m_splits; bool m_invertValues; }; SplitModel::SplitModel(QObject* parent) : QAbstractTableModel(parent), d_ptr(new SplitModelPrivate) { } SplitModel::~SplitModel() { } QString SplitModel::newSplitId() { return QLatin1String("New-ID"); } bool SplitModel::isNewSplitId(const QString& id) { return id.compare(newSplitId()) == 0; } int SplitModel::rowCount(const QModelIndex& parent) const { Q_D(const SplitModel); Q_UNUSED(parent); return d->m_splits.count(); } int SplitModel::columnCount(const QModelIndex& parent) const { Q_UNUSED(parent); return (int)Column::LastColumn; } void SplitModel::deepCopy(const SplitModel& right, bool revertSplitSign) { Q_D(SplitModel); beginInsertRows(QModelIndex(), 0, right.rowCount()); d->m_splits = right.d_func()->m_splits; d->m_transaction = right.d_func()->m_transaction; if(revertSplitSign) { for(int idx = 0; idx < d->m_splits.count(); ++idx) { MyMoneySplit& split = d->m_splits[idx]; split.setShares(-split.shares()); split.setValue(-split.value()); } } endInsertRows(); } QVariant SplitModel::headerData(int section, Qt::Orientation orientation, int role) const { if(orientation == Qt::Horizontal && role == Qt::DisplayRole) { switch(section) { case (int)Column::CostCenter: return i18n("Cost Center"); case (int)Column::Detail: return i18n("Category"); case (int)Column::Number: return i18n("No"); case (int)Column::Date: return i18n("Date"); case (int)Column::Security: return i18n("Security"); case (int)Column::Reconciliation: return i18n("C"); case (int)Column::Payment: return i18n("Payment"); case (int)Column::Deposit: return i18n("Deposit"); case (int)Column::Quantity: return i18n("Quantity"); case (int)Column::Price: return i18n("Price"); case (int)Column::Amount: return i18n("Amount"); case (int)Column::Value: return i18n("Value"); case (int)Column::Balance: return i18n("Balance"); } } return QAbstractItemModel::headerData(section, orientation, role); } QVariant SplitModel::data(const QModelIndex& index, int role) const { Q_D(const SplitModel); if(!index.isValid()) return QVariant(); if(index.row() < 0 || index.row() >= d->m_splits.count()) return QVariant(); QVariant rc; MyMoneyAccount acc; MyMoneyMoney value; const MyMoneySplit& split = d->m_splits[index.row()]; QModelIndex subIndex; CostCenterModel* ccModel = Models::instance()->costCenterModel(); switch(role) { case Qt::DisplayRole: // make sure to never return any displayable text for the dummy entry if(!d->isCreateSplitEntry(split.id())) { switch(index.column()) { case (int)Column::Detail: rc = MyMoneyFile::instance()->accountToCategory(split.accountId()); break; case (int)Column::CostCenter: subIndex = Models::indexById(ccModel, CostCenterModel::CostCenterIdRole, split.costCenterId()); rc = ccModel->data(subIndex); break; case (int)Column::Number: rc = split.number(); break; case (int)Column::Reconciliation: rc = KMyMoneyUtils::reconcileStateToString(split.reconcileFlag(), false); break; case (int)Column::Payment: if(split.value().isNegative()) { acc = MyMoneyFile::instance()->account(split.accountId()); rc = (-split).value(d->m_transaction.commodity(), acc.currencyId()).formatMoney(acc.fraction()); } break; case (int)Column::Deposit: if(!split.value().isNegative()) { acc = MyMoneyFile::instance()->account(split.accountId()); rc = split.value(d->m_transaction.commodity(), acc.currencyId()).formatMoney(acc.fraction()); } break; default: break; } } break; case Qt::TextAlignmentRole: switch(index.column()) { case (int)Column::Payment: case (int)Column::Deposit: case (int)Column::Amount: case (int)Column::Balance: case (int)Column::Value: rc = QVariant(Qt::AlignRight| Qt::AlignTop); break; case (int)Column::Reconciliation: rc = QVariant(Qt::AlignHCenter | Qt::AlignTop); break; default: rc = QVariant(Qt::AlignLeft | Qt::AlignTop); break; } break; case (int)Role::AccountId: rc = split.accountId(); break; case (int)Role::Account: rc = MyMoneyFile::instance()->accountToCategory(split.accountId()); break; case (int)Role::TransactionId: rc = QString("%1").arg(d->m_transaction.id()); break; case (int)Role::TransactionSplitId: rc = QString("%1-%2").arg(d->m_transaction.id()).arg(split.id()); break; case (int)Role::SplitId: rc = split.id(); break; case (int)Role::Memo: case (int)Role::SingleLineMemo: rc = split.memo(); if(role == (int)Role::SingleLineMemo) { QString txt = rc.toString(); // remove empty lines txt.replace("\n\n", "\n"); // replace '\n' with ", " txt.replace('\n', ", "); rc = txt; } break; case (int)Role::SplitShares: rc = QVariant::fromValue(split.shares()); break; case (int)Role::SplitValue: acc = MyMoneyFile::instance()->account(split.accountId()); rc = QVariant::fromValue(split.value(d->m_transaction.commodity(), acc.currencyId())); break; case (int)Role::PayeeName: try { rc = MyMoneyFile::instance()->payee(split.payeeId()).name(); } catch(MyMoneyException&e) { } break; case (int)Role::CostCenterId: rc = split.costCenterId(); break; case (int)Role::TransactionCommodity: return d->m_transaction.commodity(); break; case (int)Role::Number: rc = split.number(); break; case (int)Role::PayeeId: rc = split.payeeId(); break; default: if(role >= Qt::UserRole) { qWarning() << "Undefined role" << role << "(" << role-Qt::UserRole << ") in SplitModel::data"; } break; } return rc; } bool SplitModel::setData(const QModelIndex& index, const QVariant& value, int role) { Q_D(SplitModel); bool rc = false; if(index.isValid()) { MyMoneySplit& split = d->m_splits[index.row()]; if(split.id().isEmpty()) { split = MyMoneySplit(newSplitId(), split); } QString val; rc = true; switch(role) { case (int)Role::PayeeId: split.setPayeeId(value.toString()); break; case (int)Role::AccountId: split.setAccountId(value.toString()); break; case (int)Role::Memo: split.setMemo(value.toString()); break; case (int)Role::CostCenterId: val = value.toString(); split.setCostCenterId(value.toString()); break; case (int)Role::Number: split.setNumber(value.toString()); break; case (int)Role::SplitShares: split.setShares(value.value()); break; case (int)Role::SplitValue: split.setValue(value.value()); break; case (int)Role::EmitDataChanged: { // the whole row changed QModelIndex topLeft = this->index(index.row(), 0); QModelIndex bottomRight = this->index(index.row(), this->columnCount()-1); emit dataChanged(topLeft, bottomRight); } break; default: rc = false; break; } } return rc; } void SplitModel::addSplit(const QString& transactionSplitId) { Q_D(SplitModel); QRegExp transactionSplitIdExp("^(\\w+)-(\\w+)$"); if(transactionSplitIdExp.exactMatch(transactionSplitId)) { const QString transactionId = transactionSplitIdExp.cap(1); const QString splitId = transactionSplitIdExp.cap(2); if(transactionId != d->m_transaction.id()) { try { d->m_transaction = MyMoneyFile::instance()->transaction(transactionId); } catch(MyMoneyException& e) { d->m_transaction = MyMoneyTransaction(); } } try { beginInsertRows(QModelIndex(), rowCount(), rowCount()); d->m_splits.append(d->m_transaction.splitById(splitId)); endInsertRows(); } catch(MyMoneyException& e) { d->m_transaction = MyMoneyTransaction(); } } } void SplitModel::addEmptySplitEntry() { Q_D(SplitModel); QModelIndexList list = match(index(0, 0), (int)Role::SplitId, QString(), -1, Qt::MatchExactly); if(list.count() == 0) { beginInsertRows(QModelIndex(), rowCount(), rowCount()); // d->m_splits.append(MyMoneySplit(d->newSplitEntryId(), MyMoneySplit())); d->m_splits.append(MyMoneySplit()); endInsertRows(); } } void SplitModel::removeEmptySplitEntry() { Q_D(SplitModel); // QModelIndexList list = match(index(0, 0), SplitIdRole, d->newSplitEntryId(), -1, Qt::MatchExactly); QModelIndexList list = match(index(0, 0), (int)Role::SplitId, QString(), -1, Qt::MatchExactly); if(list.count()) { QModelIndex index = list.at(0); beginRemoveRows(QModelIndex(), index.row(), index.row()); d->m_splits.remove(index.row(), 1); endRemoveRows(); } } bool SplitModel::removeRows(int row, int count, const QModelIndex& parent) { Q_D(SplitModel); bool rc = false; if(count > 0) { beginRemoveRows(parent, row, row + count - 1); d->m_splits.remove(row, count); endRemoveRows(); rc = true; } return rc; } Qt::ItemFlags SplitModel::flags(const QModelIndex& index) const { Q_D(const SplitModel); Qt::ItemFlags flags; if(!index.isValid()) return flags; if(index.row() < 0 || index.row() >= d->m_splits.count()) return flags; return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable; } #if 0 void SplitModel::removeSplit(const LedgerTransaction& t) { Q_D(SplitModel); QModelIndexList list = match(index(0, 0), TransactionSplitIdRole, t.transactionSplitId(), -1, Qt::MatchExactly); if(list.count()) { QModelIndex index = list.at(0); beginRemoveRows(QModelIndex(), index.row(), index.row()); delete d->m_ledgerItems[index.row()]; d->m_ledgerItems.remove(index.row(), 1); endRemoveRows(); // just make sure we're in sync Q_ASSERT(d->m_ledgerItems.count() == rowCount()); } } #endif diff --git a/kmymoney/mymoney/mymoneyenums.h b/kmymoney/mymoney/mymoneyenums.h index 920428474..514c608c1 100644 --- a/kmymoney/mymoney/mymoneyenums.h +++ b/kmymoney/mymoney/mymoneyenums.h @@ -1,323 +1,334 @@ /*************************************************************************** mymoneyenums.h ------------------- copyright : (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 #ifndef MYMONEYENUMS_H #define MYMONEYENUMS_H namespace eMyMoney { /** * Account types currently supported. */ namespace Account { enum class Type { Unknown = 0, /**< For error handling */ Checkings, /**< Standard checking account */ Savings, /**< Typical savings account */ Cash, /**< Denotes a shoe-box or pillowcase stuffed with cash */ CreditCard, /**< Credit card accounts */ Loan, /**< Loan and mortgage accounts (liability) */ CertificateDep, /**< Certificates of Deposit */ Investment, /**< Investment account */ MoneyMarket, /**< Money Market Account */ Asset, /**< Denotes a generic asset account.*/ Liability, /**< Denotes a generic liability account.*/ Currency, /**< Denotes a currency trading account. */ Income, /**< Denotes an income account */ Expense, /**< Denotes an expense account */ AssetLoan, /**< Denotes a loan (asset of the owner of this object) */ Stock, /**< Denotes an security account as sub-account for an investment */ Equity, /**< Denotes an equity account e.g. opening/closeing balance */ /* insert new account types above this line */ MaxAccountTypes /**< Denotes the number of different account types */ }; inline uint qHash(const Type key, uint seed) { return ::qHash(static_cast(key), seed); } } namespace Security { enum class Type { Stock, MutualFund, Bond, Currency, None }; inline uint qHash(const Type key, uint seed) { return ::qHash(static_cast(key), seed); } } namespace Schedule { /** * This enum is used to describe all the possible schedule frequencies. * The special entry, Any, is used to combine all the other types. */ enum class Occurrence { Any = 0, Once = 1, Daily = 2, Weekly = 4, Fortnightly = 8, EveryOtherWeek = 16, EveryHalfMonth = 18, EveryThreeWeeks = 20, EveryThirtyDays = 30, Monthly = 32, EveryFourWeeks = 64, EveryEightWeeks = 126, EveryOtherMonth = 128, EveryThreeMonths = 256, TwiceYearly = 1024, EveryOtherYear = 2048, Quarterly = 4096, EveryFourMonths = 8192, Yearly = 16384 }; /** * This enum is used to describe the schedule type. */ enum class Type { Any = 0, Bill = 1, Deposit = 2, Transfer = 4, LoanPayment = 5 }; /** * This enum is used to describe the schedule's payment type. */ enum class PaymentType { Any = 0, DirectDebit = 1, DirectDeposit = 2, ManualDeposit = 4, Other = 8, WriteChecque = 16, StandingOrder = 32, BankTransfer = 64 }; /** * This enum is used by the auto-commit functionality. * * Depending upon the value of m_weekendOption the schedule can * be entered on a different date **/ enum class WeekendOption { MoveBefore = 0, MoveAfter = 1, MoveNothing = 2 }; } namespace TransactionFilter { // Make sure to keep the following enum valus in sync with the values // used by the GUI (for KMyMoney in kfindtransactiondlgdecl.ui) enum class Type { All = 0, Payments, Deposits, Transfers, // insert new constants above of this line LastType }; // Make sure to keep the following enum valus in sync with the values // used by the GUI (for KMyMoney in kfindtransactiondlgdecl.ui) enum class State { All = 0, NotReconciled, Cleared, Reconciled, Frozen, // insert new constants above of this line LastState }; // Make sure to keep the following enum valus in sync with the values // used by the GUI (for KMyMoney in kfindtransactiondlgdecl.ui) enum class Validity { Any = 0, Valid, Invalid, // insert new constants above of this line LastValidity }; // Make sure to keep the following enum valus in sync with the values // used by the GUI (for KMyMoney in kfindtransactiondlgdecl.ui) enum class Date { All = 0, AsOfToday, CurrentMonth, CurrentYear, MonthToDate, YearToDate, YearToMonth, LastMonth, LastYear, Last7Days, Last30Days, Last3Months, Last6Months, Last12Months, Next7Days, Next30Days, Next3Months, Next6Months, Next12Months, UserDefined, Last3ToNext3Months, Last11Months, CurrentQuarter, LastQuarter, NextQuarter, CurrentFiscalYear, LastFiscalYear, Today, Next18Months, // insert new constants above of this line LastDateItem }; } namespace Split { /** * This enum defines the possible reconciliation states a split * can be in. Possible values are as follows: * * @li NotReconciled * @li Cleared * @li Reconciled * @li Frozen * * Whenever a new split is created, it has the status NotReconciled. It * can be set to cleared when the transaction has been performed. Once the * account is reconciled, cleared splits will be set to Reconciled. The * state Frozen will be used, when the concept of books is introduced into * the engine and a split must not be changed anymore. */ enum class State { Unknown = -1, NotReconciled = 0, Cleared, Reconciled, Frozen, // insert new values above MaxReconcileState }; enum class InvestmentTransactionType { UnknownTransactionType = -1, BuyShares = 0, SellShares, Dividend, ReinvestDividend, Yield, AddShares, RemoveShares, SplitShares, InterestIncome/// }; inline uint qHash(const InvestmentTransactionType key, uint seed) { return ::qHash(static_cast(key), seed); } } namespace File { /** * notificationObject identifies the type of the object * for which this notification is stored */ enum class Object { Account = 1, Institution, Payee, Transaction, Tag, Schedule, Security, OnlineJob }; /** * notificationMode identifies the type of notifiation * (add, modify, remove) */ enum class Mode { Add = 1, Modify, Remove }; } /** * @brief Type of message * * An usually it is not easy to categorise log messages. This description is only a hint. */ namespace OnlineJob { enum class MessageType { Debug, /**< Just for debug purposes. In normal scenarios the user should not see this. No need to store this message. Plugins should not create them at all if debug mode is not enabled. */ Log, /**< A piece of information the user should not see during normal operation. It is not shown in any UI by default. It is stored persistantly. */ Information, /**< Information that should be kept but without the need to burden the user. The user can see this during normal operation. */ Warning, /**< A piece of information the user should see but not be enforced to do so (= no modal dialog). E.g. a task is expected to have direct effect but insted you have to wait a day (and that is commen behavior). */ Error /**< Important for the user - he must be warned. E.g. a task could unexpectedly not be executed */ }; } namespace Statement { enum class Type { None = 0, Checkings, Savings, Investment, CreditCard, Invalid }; inline uint qHash(const Type key, uint seed) { return ::qHash(static_cast(key), seed); } } namespace Transaction { // the following members are only used for investment accounts (m_eType==etInvestment) // eaNone means the action, shares, and security can be ignored. enum class Action { None = 0, Buy, Sell, ReinvestDividend, CashDividend, Shrsin, Shrsout, Stksplit, Fees, Interest, Invalid }; inline uint qHash(const Action key, uint seed) { return ::qHash(static_cast(key), seed); } } + namespace Money { + enum signPosition : int { + // keep those in sync with the ones defined in klocale.h + ParensAround = 0, + BeforeQuantityMoney = 1, + AfterQuantityMoney = 2, + BeforeMoney = 3, + AfterMoney = 4 + }; + } + } #endif diff --git a/kmymoney/mymoney/mymoneyexception.cpp b/kmymoney/mymoney/mymoneyexception.cpp index b3274c18b..310f095d7 100644 --- a/kmymoney/mymoney/mymoneyexception.cpp +++ b/kmymoney/mymoney/mymoneyexception.cpp @@ -1,93 +1,104 @@ /*************************************************************************** mymoneyexception.cpp - description ------------------- begin : Sun Apr 28 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 "mymoneyexception.h" +// ---------------------------------------------------------------------------- +// QT Includes + +#include + +// ---------------------------------------------------------------------------- +// KDE Includes + +// ---------------------------------------------------------------------------- +// Project Includes + class MyMoneyExceptionPrivate { public: MyMoneyExceptionPrivate() { } /** * This member variable holds the message */ QString m_msg; /** * This member variable holds the filename */ QString m_file; /** * This member variable holds the line number */ unsigned long m_line; }; MyMoneyException::MyMoneyException() : d_ptr(new MyMoneyExceptionPrivate) { } MyMoneyException::MyMoneyException(const QString& msg, const QString& file, const unsigned long line) : d_ptr(new MyMoneyExceptionPrivate) { Q_D(MyMoneyException); // qDebug("MyMoneyException(%s,%s,%ul)", qPrintable(msg), qPrintable(file), line); d->m_msg = msg; d->m_file = file; d->m_line = line; } MyMoneyException::MyMoneyException(const MyMoneyException& other) : d_ptr(new MyMoneyExceptionPrivate(*other.d_func())) { } MyMoneyException::~MyMoneyException() { Q_D(MyMoneyException); delete d; } QString MyMoneyException::what() const { Q_D(const MyMoneyException); return d->m_msg; } QString MyMoneyException::file() const { Q_D(const MyMoneyException); return d->m_file; } unsigned long MyMoneyException::line() const { Q_D(const MyMoneyException); return d->m_line; } diff --git a/kmymoney/mymoney/mymoneyexception.h b/kmymoney/mymoney/mymoneyexception.h index 0e1111e50..86456d452 100644 --- a/kmymoney/mymoney/mymoneyexception.h +++ b/kmymoney/mymoney/mymoneyexception.h @@ -1,130 +1,133 @@ /*************************************************************************** mymoneyexception.h - description ------------------- begin : Sun Apr 28 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. * * * ***************************************************************************/ #ifndef MYMONEYEXCEPTION_H #define MYMONEYEXCEPTION_H #include "kmm_mymoney_export.h" // ---------------------------------------------------------------------------- // QT Includes -#include +#include + +class QString; + /** * @file * @author Thomas Baumgart */ /** * This class describes an exception that is thrown by the engine * in case of a failure. */ class MyMoneyExceptionPrivate; class KMM_MYMONEY_EXPORT MyMoneyException { public: /** * @def MYMONEYEXCEPTION(text) * This is the preferred constructor to create a new exception * object. It automatically inserts the filename and the source * code line into the object upon creation. * * It is equivilant to MyMoneyException(text, __FILE__, __LINE__) */ #define MYMONEYEXCEPTION(what) MyMoneyException(what, __FILE__, __LINE__) /** * The constructor to create a new MyMoneyException object. * * @param msg reference to QString containing the message * @param file reference to QString containing the name of the sourcefile where * the exception was thrown * @param line unsigned long containing the line number of the line where * the exception was thrown in the file. * * An easier way to use this constructor is to use the macro * MYMONEYEXCEPTION(text) instead. It automatically assigns the file * and line parameter to the correct values. */ explicit MyMoneyException(const QString& msg, const QString& file, const unsigned long line); MyMoneyException(const MyMoneyException & other); MyMoneyException(MyMoneyException && other); MyMoneyException & operator=(MyMoneyException other); friend void swap(MyMoneyException& first, MyMoneyException& second); ~MyMoneyException(); /** * This method is used to return the message that was passed * during the creation of the exception object. * * @return reference to QString containing the message */ QString what() const; /** * This method is used to return the filename that was passed * during the creation of the exception object. * * @return reference to QString containing the filename */ QString file() const; /** * This method is used to return the linenumber that was passed * during the creation of the exception object. * * @return long integer containing the line number */ unsigned long line() const; private: // #define MYMONEYEXCEPTION(what) requires non-const d_ptr MyMoneyExceptionPrivate * d_ptr; // krazy:exclude=dpointer Q_DECLARE_PRIVATE(MyMoneyException) MyMoneyException(); }; inline void swap(MyMoneyException& first, MyMoneyException& second) // krazy:exclude=inline { using std::swap; swap(first.d_ptr, second.d_ptr); } inline MyMoneyException::MyMoneyException(MyMoneyException && other) : MyMoneyException() // krazy:exclude=inline { swap(*this, other); } inline MyMoneyException & MyMoneyException::operator=(MyMoneyException other) // krazy:exclude=inline { swap(*this, other); return *this; } #endif diff --git a/kmymoney/mymoney/mymoneyfinancialcalculator_p.h b/kmymoney/mymoney/mymoneyfinancialcalculator_p.h index 314036f3e..b7e505227 100644 --- a/kmymoney/mymoney/mymoneyfinancialcalculator_p.h +++ b/kmymoney/mymoney/mymoneyfinancialcalculator_p.h @@ -1,159 +1,161 @@ /*************************************************************************** mymoneyfinancialcalculator_p.h - description ------------------- begin : Tue Oct 21 2003 copyright : (C) 2000-2003 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. * * * ***************************************************************************/ #ifndef MYMONEYFINANCIALCALCULATOR_P_H #define MYMONEYFINANCIALCALCULATOR_P_H #include "mymoneyfinancialcalculator.h" #include // ---------------------------------------------------------------------------- // QT Includes +#include + // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes #include "mymoneyexception.h" class MyMoneyFinancialCalculatorPrivate { Q_DISABLE_COPY(MyMoneyFinancialCalculatorPrivate) public: MyMoneyFinancialCalculatorPrivate() { } double _fi(const double eint) const { return _Ax(eint) *(m_pv + _Cx(eint)) + m_pv + m_fv; } double _fip(const double eint) const { double AA = _Ax(eint); double CC = _Cx(eint); double D = (AA + 1.0) / (eint + 1.0); return m_npp *(m_pv + CC) * D - (AA * CC) / eint; } double _Ax(const double eint) const { return pow((eint + 1.0), m_npp) - 1.0; } double _Bx(const double eint) const { if (eint == 0.0) throw MYMONEYEXCEPTION("Zero interest"); if (m_bep == false) return static_cast(1.0) / eint; return (eint + 1.0) / eint; } double _Cx(const double eint) const { return m_pmt * _Bx(eint); } double eff_int() const { double nint = m_ir / 100.0; double eint; if (m_disc) { // periodically compound? if (m_CF == m_PF) { // same frequency? eint = nint / static_cast(m_CF); } else { eint = pow((static_cast(1.0) + (nint / static_cast(m_CF))), (static_cast(m_CF) / static_cast(m_PF))) - 1.0; } } else { eint = exp(nint / static_cast(m_PF)) - 1.0; } return eint; } double nom_int(const double eint) const { double nint; if (m_disc) { if (m_CF == m_PF) { nint = m_CF * eint; } else { nint = m_CF * (pow((eint + 1.0), (static_cast(m_PF) / static_cast(m_CF))) - 1.0); } } else nint = log(pow(eint + 1.0, m_PF)); return nint; } double rnd(const double x) const { double r, f; if (m_prec > 0) { f = pow(10.0, m_prec); r = qRound64(x * f) / f; } else { r = qRound64(x); } return r; } double m_ir; // nominal interest rate double m_pv; // present value double m_pmt; // periodic payment double m_fv; // future value double m_npp; // number of payment periods unsigned short m_CF; // compounding frequency unsigned short m_PF; // payment frequency unsigned short m_prec; // precision for roundoff for pv, pmt and fv // i is not rounded, n is integer bool m_bep; // beginning/end of period payment flag bool m_disc; // discrete/continuous compounding flag unsigned short m_mask; // available value mask }; #endif diff --git a/kmymoney/mymoney/mymoneyforecast.cpp b/kmymoney/mymoney/mymoneyforecast.cpp index 52f4f664e..a8e8bc1b8 100644 --- a/kmymoney/mymoney/mymoneyforecast.cpp +++ b/kmymoney/mymoney/mymoneyforecast.cpp @@ -1,1707 +1,1708 @@ /*************************************************************************** mymoneyforecast.cpp ------------------- begin : Wed May 30 2007 copyright : (C) 2007 by Alvaro Soliverez email : asoliverez@gmail.com (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 "mymoneyforecast.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes #include "mymoneyfile.h" #include "mymoneyaccount.h" #include "mymoneyaccountloan.h" #include "mymoneysecurity.h" #include "mymoneybudget.h" #include "mymoneyschedule.h" #include "mymoneyprice.h" #include "mymoneymoney.h" #include "mymoneysplit.h" #include "mymoneytransaction.h" #include "mymoneytransactionfilter.h" #include "mymoneyfinancialcalculator.h" +#include "mymoneyexception.h" #include "mymoneyenums.h" enum class eForecastMethod {Scheduled = 0, Historic = 1 }; /** * daily balances of an account */ typedef QMap dailyBalances; /** * map of trends of an account */ typedef QMap trendBalances; class MyMoneyForecastPrivate { Q_DECLARE_PUBLIC(MyMoneyForecast) public: explicit MyMoneyForecastPrivate(MyMoneyForecast *qq) : q_ptr(qq), m_accountsCycle(30), m_forecastCycles(3), m_forecastDays(90), m_beginForecastDay(0), m_forecastMethod(eForecastMethod::Scheduled), m_historyMethod(1), m_skipOpeningDate(true), m_includeUnusedAccounts(false), m_forecastDone(false), m_includeFutureTransactions(true), m_includeScheduledTransactions(true) { } eForecastMethod forecastMethod() const { return m_forecastMethod; } /** * Returns the list of accounts to create a budget. Only Income and Expenses are returned. */ QList budgetAccountList() { auto file = MyMoneyFile::instance(); QList accList; QStringList emptyStringList; //Get all accounts from the file and check if they are of the right type to calculate forecast file->accountList(accList, emptyStringList, false); QList::iterator accList_t = accList.begin(); for (; accList_t != accList.end();) { auto acc = *accList_t; if (acc.isClosed() //check the account is not closed || (!acc.isIncomeExpense())) { //remove the account if it is not of the correct type accList_t = accList.erase(accList_t); } else { ++accList_t; } } return accList; } /** * calculate daily forecast balance based on historic transactions */ void calculateHistoricDailyBalances() { Q_Q(MyMoneyForecast); auto file = MyMoneyFile::instance(); calculateAccountTrendList(); //Calculate account daily balances QSet::ConstIterator it_n; for (it_n = m_forecastAccounts.constBegin(); it_n != m_forecastAccounts.constEnd(); ++it_n) { auto acc = file->account(*it_n); //set the starting balance of the account setStartingBalance(acc); switch (q->historyMethod()) { case 0: case 1: { for (QDate f_day = q->forecastStartDate(); f_day <= q->forecastEndDate();) { for (int t_day = 1; t_day <= q->accountsCycle(); ++t_day) { MyMoneyMoney balanceDayBefore = m_accountList[acc.id()][(f_day.addDays(-1))];//balance of the day before MyMoneyMoney accountDailyTrend = m_accountTrendList[acc.id()][t_day]; //trend for that day //balance of the day is the balance of the day before multiplied by the trend for the day m_accountList[acc.id()][f_day] = balanceDayBefore; m_accountList[acc.id()][f_day] += accountDailyTrend; //movement trend for that particular day m_accountList[acc.id()][f_day] = m_accountList[acc.id()][f_day].convert(acc.fraction()); //m_accountList[acc.id()][f_day] += m_accountListPast[acc.id()][f_day.addDays(-q->historyDays())]; f_day = f_day.addDays(1); } } } break; case 2: { QDate baseDate = QDate::currentDate().addDays(-q->accountsCycle()); for (int t_day = 1; t_day <= q->accountsCycle(); ++t_day) { int f_day = 1; QDate fDate = baseDate.addDays(q->accountsCycle() + 1); while (fDate <= q->forecastEndDate()) { //the calculation is based on the balance for the last month, that is then multiplied by the trend m_accountList[acc.id()][fDate] = m_accountListPast[acc.id()][baseDate] + (m_accountTrendList[acc.id()][t_day] * MyMoneyMoney(f_day, 1)); m_accountList[acc.id()][fDate] = m_accountList[acc.id()][fDate].convert(acc.fraction()); ++f_day; fDate = baseDate.addDays(q->accountsCycle() * f_day); } baseDate = baseDate.addDays(1); } } } } } /** * calculate monthly budget balance based on historic transactions */ void calculateHistoricMonthlyBalances() { Q_Q(MyMoneyForecast); auto file = MyMoneyFile::instance(); //Calculate account monthly balances QSet::ConstIterator it_n; for (it_n = m_forecastAccounts.constBegin(); it_n != m_forecastAccounts.constEnd(); ++it_n) { auto acc = file->account(*it_n); for (QDate f_date = q->forecastStartDate(); f_date <= q->forecastEndDate();) { for (int f_day = 1; f_day <= q->accountsCycle() && f_date <= q->forecastEndDate(); ++f_day) { MyMoneyMoney accountDailyTrend = m_accountTrendList[acc.id()][f_day]; //trend for that day //check for leap year if (f_date.month() == 2 && f_date.day() == 29) f_date = f_date.addDays(1); //skip 1 day m_accountList[acc.id()][QDate(f_date.year(), f_date.month(), 1)] += accountDailyTrend; //movement trend for that particular day f_date = f_date.addDays(1); } } } } /** * calculate monthly budget balance based on historic transactions */ void calculateScheduledMonthlyBalances() { Q_Q(MyMoneyForecast); auto file = MyMoneyFile::instance(); //Calculate account monthly balances QSet::ConstIterator it_n; for (it_n = m_forecastAccounts.constBegin(); it_n != m_forecastAccounts.constEnd(); ++it_n) { auto acc = file->account(*it_n); for (QDate f_date = q->forecastStartDate(); f_date <= q->forecastEndDate(); f_date = f_date.addDays(1)) { //get the trend for the day MyMoneyMoney accountDailyBalance = m_accountList[acc.id()][f_date]; //do not add if it is the beginning of the month //otherwise we end up with duplicated values as reported by Marko Käning if (f_date != QDate(f_date.year(), f_date.month(), 1)) m_accountList[acc.id()][QDate(f_date.year(), f_date.month(), 1)] += accountDailyBalance; } } } /** * calculate forecast based on future and scheduled transactions */ void doFutureScheduledForecast() { Q_Q(MyMoneyForecast); auto file = MyMoneyFile::instance(); if (q->isIncludingFutureTransactions()) addFutureTransactions(); if (q->isIncludingScheduledTransactions()) addScheduledTransactions(); //do not show accounts with no transactions if (!q->isIncludingUnusedAccounts()) purgeForecastAccountsList(m_accountList); //adjust value of investments to deep currency QSet::ConstIterator it_n; for (it_n = m_forecastAccounts.constBegin(); it_n != m_forecastAccounts.constEnd(); ++it_n) { auto acc = file->account(*it_n); if (acc.isInvest()) { //get the id of the security for that account MyMoneySecurity undersecurity = file->security(acc.currencyId()); //only do it if the security is not an actual currency if (! undersecurity.isCurrency()) { //set the default value MyMoneyMoney rate = MyMoneyMoney::ONE; for (QDate it_day = QDate::currentDate(); it_day <= q->forecastEndDate();) { //get the price for the tradingCurrency that day const MyMoneyPrice &price = file->price(undersecurity.id(), undersecurity.tradingCurrency(), it_day); if (price.isValid()) { rate = price.rate(undersecurity.tradingCurrency()); } //value is the amount of shares multiplied by the rate of the deep currency m_accountList[acc.id()][it_day] = m_accountList[acc.id()][it_day] * rate; it_day = it_day.addDays(1); } } } } } /** * add future transactions to forecast */ void addFutureTransactions() { Q_Q(MyMoneyForecast); MyMoneyTransactionFilter filter; auto file = MyMoneyFile::instance(); // collect and process all transactions that have already been entered but // are located in the future. filter.setDateFilter(q->forecastStartDate(), q->forecastEndDate()); filter.setReportAllSplits(false); foreach (const auto transaction, file->transactionList(filter)) { foreach (const auto split, transaction.splits()) { if (!split.shares().isZero()) { auto acc = file->account(split.accountId()); if (q->isForecastAccount(acc)) { dailyBalances balance; balance = m_accountList[acc.id()]; //if it is income, the balance is stored as negative number if (acc.accountType() == eMyMoney::Account::Type::Income) { balance[transaction.postDate()] += (split.shares() * MyMoneyMoney::MINUS_ONE); } else { balance[transaction.postDate()] += split.shares(); } m_accountList[acc.id()] = balance; } } } } #if 0 QFile trcFile("forecast.csv"); trcFile.open(QIODevice::WriteOnly); QTextStream s(&trcFile); { s << "Already present transactions\n"; QMap::Iterator it_a; QSet::ConstIterator it_n; for (it_n = m_nameIdx.begin(); it_n != m_nameIdx.end(); ++it_n) { auto acc = file->account(*it_n); it_a = m_accountList.find(*it_n); s << "\"" << acc.name() << "\","; for (int i = 0; i < 90; ++i) { s << "\"" << (*it_a)[i].formatMoney("") << "\","; } s << "\n"; } } #endif } /** * add scheduled transactions to forecast */ void addScheduledTransactions() { Q_Q(MyMoneyForecast); auto file = MyMoneyFile::instance(); // now process all the schedules that may have an impact QList schedule; schedule = file->scheduleList(QString(), eMyMoney::Schedule::Type::Any, eMyMoney::Schedule::Occurrence::Any, eMyMoney::Schedule::PaymentType::Any, QDate(), q->forecastEndDate(), false); if (schedule.count() > 0) { QList::Iterator it; do { qSort(schedule); it = schedule.begin(); if (it == schedule.end()) break; if ((*it).isFinished()) { schedule.erase(it); continue; } QDate date = (*it).nextPayment((*it).lastPayment()); if (!date.isValid()) { schedule.erase(it); continue; } QDate nextDate = (*it).adjustedNextPayment((*it).adjustedDate((*it).lastPayment(), (*it).weekendOption())); if (nextDate > q->forecastEndDate()) { // We're done with this schedule, let's move on to the next schedule.erase(it); continue; } // found the next schedule. process it auto acc = (*it).account(); if (!acc.id().isEmpty()) { try { if (acc.accountType() != eMyMoney::Account::Type::Investment) { auto t = (*it).transaction(); // only process the entry, if it is still active if (!(*it).isFinished() && nextDate != QDate()) { // make sure we have all 'starting balances' so that the autocalc works QMap balanceMap; foreach (const auto split, t.splits()) { auto acc = file->account(split.accountId()); if (q->isForecastAccount(acc)) { // collect all overdues on the first day QDate forecastDate = nextDate; if (QDate::currentDate() >= nextDate) forecastDate = QDate::currentDate().addDays(1); dailyBalances balance; balance = m_accountList[acc.id()]; for (QDate f_day = QDate::currentDate(); f_day < forecastDate;) { balanceMap[acc.id()] += m_accountList[acc.id()][f_day]; f_day = f_day.addDays(1); } } } // take care of the autoCalc stuff q->calculateAutoLoan(*it, t, balanceMap); // now add the splits to the balances foreach (const auto split, t.splits()) { auto acc = file->account(split.accountId()); if (q->isForecastAccount(acc)) { dailyBalances balance; balance = m_accountList[acc.id()]; //int offset = QDate::currentDate().daysTo(nextDate); //if(offset <= 0) { // collect all overdues on the first day // offset = 1; //} // collect all overdues on the first day QDate forecastDate = nextDate; if (QDate::currentDate() >= nextDate) forecastDate = QDate::currentDate().addDays(1); if (acc.accountType() == eMyMoney::Account::Type::Income) { balance[forecastDate] += (split.shares() * MyMoneyMoney::MINUS_ONE); } else { balance[forecastDate] += split.shares(); } m_accountList[acc.id()] = balance; } } } } (*it).setLastPayment(date); } catch (const MyMoneyException &e) { qDebug() << Q_FUNC_INFO << " Schedule " << (*it).id() << " (" << (*it).name() << "): " << e.what(); schedule.erase(it); } } else { // remove schedule from list schedule.erase(it); } } while (1); } #if 0 { s << "\n\nAdded scheduled transactions\n"; QMap::Iterator it_a; QSet::ConstIterator it_n; for (it_n = m_nameIdx.begin(); it_n != m_nameIdx.end(); ++it_n) { auto acc = file->account(*it_n); it_a = m_accountList.find(*it_n); s << "\"" << acc.name() << "\","; for (int i = 0; i < 90; ++i) { s << "\"" << (*it_a)[i].formatMoney("") << "\","; } s << "\n"; } } #endif } /** * calculate daily forecast balance based on future and scheduled transactions */ void calculateScheduledDailyBalances() { Q_Q(MyMoneyForecast); auto file = MyMoneyFile::instance(); //Calculate account daily balances QSet::ConstIterator it_n; for (it_n = m_forecastAccounts.constBegin(); it_n != m_forecastAccounts.constEnd(); ++it_n) { auto acc = file->account(*it_n); //set the starting balance of the account setStartingBalance(acc); for (QDate f_day = q->forecastStartDate(); f_day <= q->forecastEndDate();) { MyMoneyMoney balanceDayBefore = m_accountList[acc.id()][(f_day.addDays(-1))];//balance of the day before m_accountList[acc.id()][f_day] += balanceDayBefore; //running sum f_day = f_day.addDays(1); } } } /** * set the starting balance for an accounts */ void setStartingBalance(const MyMoneyAccount& acc) { Q_Q(MyMoneyForecast); auto file = MyMoneyFile::instance(); //Get current account balance if (acc.isInvest()) { //investments require special treatment //get the security id of that account MyMoneySecurity undersecurity = file->security(acc.currencyId()); //only do it if the security is not an actual currency if (! undersecurity.isCurrency()) { //set the default value MyMoneyMoney rate = MyMoneyMoney::ONE; //get te const MyMoneyPrice &price = file->price(undersecurity.id(), undersecurity.tradingCurrency(), QDate::currentDate()); if (price.isValid()) { rate = price.rate(undersecurity.tradingCurrency()); } m_accountList[acc.id()][QDate::currentDate()] = file->balance(acc.id(), QDate::currentDate()) * rate; } } else { m_accountList[acc.id()][QDate::currentDate()] = file->balance(acc.id(), QDate::currentDate()); } //if the method is linear regression, we have to add the opening balance to m_accountListPast if (forecastMethod() == eForecastMethod::Historic && q->historyMethod() == 2) { //FIXME workaround for stock opening dates QDate openingDate; if (acc.accountType() == eMyMoney::Account::Type::Stock) { auto parentAccount = file->account(acc.parentAccountId()); openingDate = parentAccount.openingDate(); } else { openingDate = acc.openingDate(); } //add opening balance only if it opened after the history start if (openingDate >= q->historyStartDate()) { MyMoneyMoney openingBalance; openingBalance = file->balance(acc.id(), openingDate); //calculate running sum for (QDate it_date = openingDate; it_date <= q->historyEndDate(); it_date = it_date.addDays(1)) { //investments require special treatment if (acc.isInvest()) { //get the security id of that account MyMoneySecurity undersecurity = file->security(acc.currencyId()); //only do it if the security is not an actual currency if (! undersecurity.isCurrency()) { //set the default value MyMoneyMoney rate = MyMoneyMoney::ONE; //get the rate for that specific date const MyMoneyPrice &price = file->price(undersecurity.id(), undersecurity.tradingCurrency(), it_date); if (price.isValid()) { rate = price.rate(undersecurity.tradingCurrency()); } m_accountListPast[acc.id()][it_date] += openingBalance * rate; } } else { m_accountListPast[acc.id()][it_date] += openingBalance; } } } } } /** * Returns the day moving average for the account @a acc based on the daily balances of a given number of @p forecastTerms * It returns the moving average for a given @p trendDay of the forecastTerm * With a term of 1 month and 3 terms, it calculates the trend taking the transactions occurred * at that day and the day before,for the last 3 months */ MyMoneyMoney accountMovingAverage(const MyMoneyAccount& acc, const int trendDay, const int forecastTerms) { Q_Q(MyMoneyForecast); //Calculate a daily trend for the account based on the accounts of a given number of terms //With a term of 1 month and 3 terms, it calculates the trend taking the transactions occurred at that day and the day before, //for the last 3 months MyMoneyMoney balanceVariation; for (int it_terms = 0; (trendDay + (q->accountsCycle()*it_terms)) <= q->historyDays(); ++it_terms) { //sum for each term MyMoneyMoney balanceBefore = m_accountListPast[acc.id()][q->historyStartDate().addDays(trendDay+(q->accountsCycle()*it_terms)-2)]; //get balance for the day before MyMoneyMoney balanceAfter = m_accountListPast[acc.id()][q->historyStartDate().addDays(trendDay+(q->accountsCycle()*it_terms)-1)]; balanceVariation += (balanceAfter - balanceBefore); //add the balance variation between days } //calculate average of the variations return (balanceVariation / MyMoneyMoney(forecastTerms, 1)).convert(10000); } /** * Returns the weighted moving average for a given @p trendDay */ MyMoneyMoney accountWeightedMovingAverage(const MyMoneyAccount& acc, const int trendDay, const int totalWeight) { Q_Q(MyMoneyForecast); MyMoneyMoney balanceVariation; for (int it_terms = 0, weight = 1; (trendDay + (q->accountsCycle()*it_terms)) <= q->historyDays(); ++it_terms, ++weight) { //sum for each term multiplied by weight MyMoneyMoney balanceBefore = m_accountListPast[acc.id()][q->historyStartDate().addDays(trendDay+(q->accountsCycle()*it_terms)-2)]; //get balance for the day before MyMoneyMoney balanceAfter = m_accountListPast[acc.id()][q->historyStartDate().addDays(trendDay+(q->accountsCycle()*it_terms)-1)]; balanceVariation += ((balanceAfter - balanceBefore) * MyMoneyMoney(weight, 1)); //add the balance variation between days multiplied by its weight } //calculate average of the variations return (balanceVariation / MyMoneyMoney(totalWeight, 1)).convert(10000); } /** * Returns the linear regression for a given @p trendDay */ MyMoneyMoney accountLinearRegression(const MyMoneyAccount &acc, const int trendDay, const int actualTerms, const MyMoneyMoney& meanTerms) { Q_Q(MyMoneyForecast); MyMoneyMoney meanBalance, totalBalance, totalTerms; totalTerms = MyMoneyMoney(actualTerms, 1); //calculate mean balance for (int it_terms = q->forecastCycles() - actualTerms; (trendDay + (q->accountsCycle()*it_terms)) <= q->historyDays(); ++it_terms) { //sum for each term totalBalance += m_accountListPast[acc.id()][q->historyStartDate().addDays(trendDay+(q->accountsCycle()*it_terms)-1)]; } meanBalance = totalBalance / MyMoneyMoney(actualTerms, 1); meanBalance = meanBalance.convert(10000); //calculate b1 //first calculate x - mean x multiplied by y - mean y MyMoneyMoney totalXY, totalSqX; for (int it_terms = q->forecastCycles() - actualTerms, term = 1; (trendDay + (q->accountsCycle()*it_terms)) <= q->historyDays(); ++it_terms, ++term) { //sum for each term MyMoneyMoney balance = m_accountListPast[acc.id()][q->historyStartDate().addDays(trendDay+(q->accountsCycle()*it_terms)-1)]; MyMoneyMoney balMeanBal = balance - meanBalance; MyMoneyMoney termMeanTerm = (MyMoneyMoney(term, 1) - meanTerms); totalXY += (balMeanBal * termMeanTerm).convert(10000); totalSqX += (termMeanTerm * termMeanTerm).convert(10000); } totalXY = (totalXY / MyMoneyMoney(actualTerms, 1)).convert(10000); totalSqX = (totalSqX / MyMoneyMoney(actualTerms, 1)).convert(10000); //check zero if (totalSqX.isZero()) return MyMoneyMoney(); MyMoneyMoney linReg = (totalXY / totalSqX).convert(10000); return linReg; } /** * calculate daily forecast trend based on historic transactions */ void calculateAccountTrendList() { Q_Q(MyMoneyForecast); auto file = MyMoneyFile::instance(); int auxForecastTerms; int totalWeight = 0; //Calculate account trends QSet::ConstIterator it_n; for (it_n = m_forecastAccounts.begin(); it_n != m_forecastAccounts.end(); ++it_n) { auto acc = file->account(*it_n); m_accountTrendList[acc.id()][0] = MyMoneyMoney(); // for today, the trend is 0 auxForecastTerms = q->forecastCycles(); if (q->skipOpeningDate()) { QDate openingDate; if (acc.accountType() == eMyMoney::Account::Type::Stock) { auto parentAccount = file->account(acc.parentAccountId()); openingDate = parentAccount.openingDate(); } else { openingDate = acc.openingDate(); } if (openingDate > q->historyStartDate()) { //if acc opened after forecast period auxForecastTerms = 1 + ((openingDate.daysTo(q->historyEndDate()) + 1) / q->accountsCycle()); // set forecastTerms to a lower value, to calculate only based on how long this account was opened } } switch (q->historyMethod()) { //moving average case 0: { for (int t_day = 1; t_day <= q->accountsCycle(); t_day++) m_accountTrendList[acc.id()][t_day] = accountMovingAverage(acc, t_day, auxForecastTerms); //moving average break; } //weighted moving average case 1: { //calculate total weight for moving average if (auxForecastTerms == q->forecastCycles()) { totalWeight = (auxForecastTerms * (auxForecastTerms + 1)) / 2; //totalWeight is the triangular number of auxForecastTerms } else { //if only taking a few periods, totalWeight is the sum of the weight for most recent periods for (int i = 1, w = q->forecastCycles(); i <= auxForecastTerms; ++i, --w) totalWeight += w; } for (int t_day = 1; t_day <= q->accountsCycle(); t_day++) m_accountTrendList[acc.id()][t_day] = accountWeightedMovingAverage(acc, t_day, totalWeight); break; } case 2: { //calculate mean term MyMoneyMoney meanTerms = MyMoneyMoney((auxForecastTerms * (auxForecastTerms + 1)) / 2, 1) / MyMoneyMoney(auxForecastTerms, 1); for (int t_day = 1; t_day <= q->accountsCycle(); t_day++) m_accountTrendList[acc.id()][t_day] = accountLinearRegression(acc, t_day, auxForecastTerms, meanTerms); break; } default: break; } } } /** * set the internal list of accounts to be forecast */ void setForecastAccountList() { Q_Q(MyMoneyForecast); //get forecast accounts QList accList; accList = q->forecastAccountList(); QList::const_iterator accList_t = accList.constBegin(); for (; accList_t != accList.constEnd(); ++accList_t) { m_forecastAccounts.insert((*accList_t).id()); } } /** * set the internal list of accounts to create a budget */ void setBudgetAccountList() { //get budget accounts QList accList; accList = budgetAccountList(); QList::const_iterator accList_t = accList.constBegin(); for (; accList_t != accList.constEnd(); ++accList_t) { m_forecastAccounts.insert((*accList_t).id()); } } /** * get past transactions for the accounts to be forecast */ void pastTransactions() { Q_Q(MyMoneyForecast); auto file = MyMoneyFile::instance(); MyMoneyTransactionFilter filter; filter.setDateFilter(q->historyStartDate(), q->historyEndDate()); filter.setReportAllSplits(false); //Check past transactions foreach (const auto transaction, file->transactionList(filter)) { foreach (const auto split, transaction.splits()) { if (!split.shares().isZero()) { auto acc = file->account(split.accountId()); //workaround for stock accounts which have faulty opening dates QDate openingDate; if (acc.accountType() == eMyMoney::Account::Type::Stock) { auto parentAccount = file->account(acc.parentAccountId()); openingDate = parentAccount.openingDate(); } else { openingDate = acc.openingDate(); } if (q->isForecastAccount(acc) //If it is one of the accounts we are checking, add the amount of the transaction && ((openingDate < transaction.postDate() && q->skipOpeningDate()) || !q->skipOpeningDate())) { //don't take the opening day of the account to calculate balance dailyBalances balance; //FIXME deal with leap years balance = m_accountListPast[acc.id()]; if (acc.accountType() == eMyMoney::Account::Type::Income) {//if it is income, the balance is stored as negative number balance[transaction.postDate()] += (split.shares() * MyMoneyMoney::MINUS_ONE); } else { balance[transaction.postDate()] += split.shares(); } // check if this is a new account for us m_accountListPast[acc.id()] = balance; } } } } //purge those accounts with no transactions on the period if (q->isIncludingUnusedAccounts() == false) purgeForecastAccountsList(m_accountListPast); //calculate running sum QSet::ConstIterator it_n; for (it_n = m_forecastAccounts.begin(); it_n != m_forecastAccounts.end(); ++it_n) { auto acc = file->account(*it_n); m_accountListPast[acc.id()][q->historyStartDate().addDays(-1)] = file->balance(acc.id(), q->historyStartDate().addDays(-1)); for (QDate it_date = q->historyStartDate(); it_date <= q->historyEndDate();) { m_accountListPast[acc.id()][it_date] += m_accountListPast[acc.id()][it_date.addDays(-1)]; //Running sum it_date = it_date.addDays(1); } } //adjust value of investments to deep currency for (it_n = m_forecastAccounts.begin(); it_n != m_forecastAccounts.end(); ++it_n) { auto acc = file->account(*it_n); if (acc.isInvest()) { //get the id of the security for that account MyMoneySecurity undersecurity = file->security(acc.currencyId()); if (! undersecurity.isCurrency()) { //only do it if the security is not an actual currency MyMoneyMoney rate = MyMoneyMoney::ONE; //set the default value for (QDate it_date = q->historyStartDate().addDays(-1) ; it_date <= q->historyEndDate();) { //get the price for the tradingCurrency that day const MyMoneyPrice &price = file->price(undersecurity.id(), undersecurity.tradingCurrency(), it_date); if (price.isValid()) { rate = price.rate(undersecurity.tradingCurrency()); } //value is the amount of shares multiplied by the rate of the deep currency m_accountListPast[acc.id()][it_date] = m_accountListPast[acc.id()][it_date] * rate; it_date = it_date.addDays(1); } } } } } /** * calculate the day to start forecast and sets the begin date * The quantity of forecast days will be counted from this date * Depends on the values of begin day and accounts cycle * The rules to calculate begin day are as follows: * - if beginDay is 0, begin date is current date * - if the day of the month set by beginDay has not passed, that will be used * - if adding an account cycle to beginDay, will not go past the beginDay of next month, * that date will be used, otherwise it will add account cycle to beginDay until it is past current date * It returns the total amount of Forecast Days from current date. */ int calculateBeginForecastDay() { Q_Q(MyMoneyForecast); int fDays = q->forecastDays(); int beginDay = q->beginForecastDay(); int accCycle = q->accountsCycle(); QDate beginDate; //if 0, beginDate is current date and forecastDays remains unchanged if (beginDay == 0) { q->setBeginForecastDate(QDate::currentDate()); return fDays; } //adjust if beginDay more than days of current month if (QDate::currentDate().daysInMonth() < beginDay) beginDay = QDate::currentDate().daysInMonth(); //if beginDay still to come, calculate and return if (QDate::currentDate().day() <= beginDay) { beginDate = QDate(QDate::currentDate().year(), QDate::currentDate().month(), beginDay); fDays += QDate::currentDate().daysTo(beginDate); q->setBeginForecastDate(beginDate); return fDays; } //adjust beginDay for next month if (QDate::currentDate().addMonths(1).daysInMonth() < beginDay) beginDay = QDate::currentDate().addMonths(1).daysInMonth(); //if beginDay of next month comes before 1 interval, use beginDay next month if (QDate::currentDate().addDays(accCycle) >= (QDate(QDate::currentDate().addMonths(1).year(), QDate::currentDate().addMonths(1).month(), 1).addDays(beginDay - 1))) { beginDate = QDate(QDate::currentDate().addMonths(1).year(), QDate::currentDate().addMonths(1).month(), 1).addDays(beginDay - 1); fDays += QDate::currentDate().daysTo(beginDate); } else { //add intervals to current beginDay and take the first after current date beginDay = ((((QDate::currentDate().day() - beginDay) / accCycle) + 1) * accCycle) + beginDay; beginDate = QDate::currentDate().addDays(beginDay - QDate::currentDate().day()); fDays += QDate::currentDate().daysTo(beginDate); } q->setBeginForecastDate(beginDate); return fDays; } /** * remove accounts from the list if the accounts has no transactions in the forecast timeframe. * Used for scheduled-forecast method. */ void purgeForecastAccountsList(QMap& accountList) { m_forecastAccounts.intersect(accountList.keys().toSet()); } MyMoneyForecast *q_ptr; /** * daily forecast balance of accounts */ QMap m_accountList; /** * daily past balance of accounts */ QMap m_accountListPast; /** * daily forecast trends of accounts */ QMap m_accountTrendList; /** * list of forecast account ids. */ QSet m_forecastAccounts; /** * cycle of accounts in days */ int m_accountsCycle; /** * number of cycles to use in forecast */ int m_forecastCycles; /** * number of days to forecast */ int m_forecastDays; /** * date to start forecast */ QDate m_beginForecastDate; /** * day to start forecast */ int m_beginForecastDay; /** * forecast method */ eForecastMethod m_forecastMethod; /** * history method */ int m_historyMethod; /** * start date of history */ QDate m_historyStartDate; /** * end date of history */ QDate m_historyEndDate; /** * start date of forecast */ QDate m_forecastStartDate; /** * end date of forecast */ QDate m_forecastEndDate; /** * skip opening date when fetching transactions of an account */ bool m_skipOpeningDate; /** * include accounts with no transactions in the forecast timeframe. default is false. */ bool m_includeUnusedAccounts; /** * forecast already done */ bool m_forecastDone; /** * include future transactions when doing a scheduled-based forecast */ bool m_includeFutureTransactions; /** * include scheduled transactions when doing a scheduled-based forecast */ bool m_includeScheduledTransactions; }; MyMoneyForecast::MyMoneyForecast() : d_ptr(new MyMoneyForecastPrivate(this)) { setHistoryStartDate(QDate::currentDate().addDays(-forecastCycles()*accountsCycle())); setHistoryEndDate(QDate::currentDate().addDays(-1)); } MyMoneyForecast::MyMoneyForecast(const MyMoneyForecast& other) : d_ptr(new MyMoneyForecastPrivate(*other.d_func())) { this->d_ptr->q_ptr = this; } void swap(MyMoneyForecast& first, MyMoneyForecast& second) { using std::swap; swap(first.d_ptr, second.d_ptr); swap(first.d_ptr->q_ptr, second.d_ptr->q_ptr); } MyMoneyForecast::MyMoneyForecast(MyMoneyForecast && other) : MyMoneyForecast() { swap(*this, other); } MyMoneyForecast & MyMoneyForecast::operator=(MyMoneyForecast other) { swap(*this, other); return *this; } MyMoneyForecast::~MyMoneyForecast() { Q_D(MyMoneyForecast); delete d; } void MyMoneyForecast::doForecast() { Q_D(MyMoneyForecast); auto fDays = d->calculateBeginForecastDay(); auto fMethod = d->forecastMethod(); auto fAccCycle = accountsCycle(); auto fCycles = forecastCycles(); //validate settings if (fAccCycle < 1 || fCycles < 1 || fDays < 1) { throw MYMONEYEXCEPTION("Illegal settings when calling doForecast. Settings must be higher than 0"); } //initialize global variables setForecastDays(fDays); setForecastStartDate(QDate::currentDate().addDays(1)); setForecastEndDate(QDate::currentDate().addDays(fDays)); setAccountsCycle(fAccCycle); setForecastCycles(fCycles); setHistoryStartDate(forecastCycles() * accountsCycle()); setHistoryEndDate(QDate::currentDate().addDays(-1)); //yesterday //clear all data before calculating d->m_accountListPast.clear(); d->m_accountList.clear(); d->m_accountTrendList.clear(); //set forecast accounts d->setForecastAccountList(); switch (fMethod) { case eForecastMethod::Scheduled: d->doFutureScheduledForecast(); d->calculateScheduledDailyBalances(); break; case eForecastMethod::Historic: d->pastTransactions(); d->calculateHistoricDailyBalances(); break; default: break; } //flag the forecast as done d->m_forecastDone = true; } bool MyMoneyForecast::isForecastAccount(const MyMoneyAccount& acc) { Q_D(MyMoneyForecast); if (d->m_forecastAccounts.isEmpty()) { d->setForecastAccountList(); } return d->m_forecastAccounts.contains(acc.id()); } QList MyMoneyForecast::accountList() { auto file = MyMoneyFile::instance(); QList accList; QStringList emptyStringList; //Get all accounts from the file and check if they are present file->accountList(accList, emptyStringList, false); QList::iterator accList_t = accList.begin(); for (; accList_t != accList.end();) { auto acc = *accList_t; if (!isForecastAccount(acc)) { accList_t = accList.erase(accList_t); //remove the account } else { ++accList_t; } } return accList; } MyMoneyMoney MyMoneyForecast::calculateAccountTrend(const MyMoneyAccount& acc, int trendDays) { auto file = MyMoneyFile::instance(); MyMoneyTransactionFilter filter; MyMoneyMoney netIncome; QDate startDate; QDate openingDate = acc.openingDate(); //validate arguments if (trendDays < 1) { throw MYMONEYEXCEPTION("Illegal arguments when calling calculateAccountTrend. trendDays must be higher than 0"); } //If it is a new account, we don't take into account the first day //because it is usually a weird one and it would mess up the trend if (openingDate.daysTo(QDate::currentDate()) < trendDays) { startDate = (acc.openingDate()).addDays(1); } else { startDate = QDate::currentDate().addDays(-trendDays); } //get all transactions for the period filter.setDateFilter(startDate, QDate::currentDate()); if (acc.accountGroup() == eMyMoney::Account::Type::Income || acc.accountGroup() == eMyMoney::Account::Type::Expense) { filter.addCategory(acc.id()); } else { filter.addAccount(acc.id()); } filter.setReportAllSplits(false); //add all transactions for that account foreach (const auto transaction, file->transactionList(filter)) { foreach (const auto split, transaction.splits()) { if (!split.shares().isZero()) { if (acc.id() == split.accountId()) netIncome += split.value(); } } } //calculate trend of the account in the past period MyMoneyMoney accTrend; //don't take into account the first day of the account if (openingDate.daysTo(QDate::currentDate()) < trendDays) { accTrend = netIncome / MyMoneyMoney(openingDate.daysTo(QDate::currentDate()) - 1, 1); } else { accTrend = netIncome / MyMoneyMoney(trendDays, 1); } return accTrend; } MyMoneyMoney MyMoneyForecast::forecastBalance(const MyMoneyAccount& acc, const QDate &forecastDate) { Q_D(MyMoneyForecast); dailyBalances balance; MyMoneyMoney MM_amount = MyMoneyMoney(); //Check if acc is not a forecast account, return 0 if (!isForecastAccount(acc)) { return MM_amount; } if (d->m_accountList.contains(acc.id())) { balance = d->m_accountList.value(acc.id()); } if (balance.contains(forecastDate)) { //if the date is not in the forecast, it returns 0 MM_amount = balance.value(forecastDate); } return MM_amount; } /** * Returns the forecast balance trend for account @a acc for offset @p int * offset is days from current date, inside forecast days. * Returns 0 if offset not in range of forecast days. */ MyMoneyMoney MyMoneyForecast::forecastBalance(const MyMoneyAccount& acc, int offset) { QDate forecastDate = QDate::currentDate().addDays(offset); return forecastBalance(acc, forecastDate); } int MyMoneyForecast::daysToMinimumBalance(const MyMoneyAccount& acc) { Q_D(MyMoneyForecast); QString minimumBalance = acc.value("minBalanceAbsolute"); MyMoneyMoney minBalance = MyMoneyMoney(minimumBalance); dailyBalances balance; //Check if acc is not a forecast account, return -1 if (!isForecastAccount(acc)) { return -1; } balance = d->m_accountList[acc.id()]; for (QDate it_day = QDate::currentDate() ; it_day <= forecastEndDate();) { if (minBalance > balance[it_day]) { return QDate::currentDate().daysTo(it_day); } it_day = it_day.addDays(1); } return -1; } int MyMoneyForecast::daysToZeroBalance(const MyMoneyAccount& acc) { Q_D(MyMoneyForecast); dailyBalances balance; //Check if acc is not a forecast account, return -1 if (!isForecastAccount(acc)) { return -2; } balance = d->m_accountList[acc.id()]; if (acc.accountGroup() == eMyMoney::Account::Type::Asset) { for (QDate it_day = QDate::currentDate() ; it_day <= forecastEndDate();) { if (balance[it_day] < MyMoneyMoney()) { return QDate::currentDate().daysTo(it_day); } it_day = it_day.addDays(1); } } else if (acc.accountGroup() == eMyMoney::Account::Type::Liability) { for (QDate it_day = QDate::currentDate() ; it_day <= forecastEndDate();) { if (balance[it_day] > MyMoneyMoney()) { return QDate::currentDate().daysTo(it_day); } it_day = it_day.addDays(1); } } return -1; } MyMoneyMoney MyMoneyForecast::accountCycleVariation(const MyMoneyAccount& acc) { Q_D(MyMoneyForecast); MyMoneyMoney cycleVariation; if (d->forecastMethod() == eForecastMethod::Historic) { switch (historyMethod()) { case 0: case 1: { for (int t_day = 1; t_day <= accountsCycle() ; ++t_day) { cycleVariation += d->m_accountTrendList[acc.id()][t_day]; } } break; case 2: { cycleVariation = d->m_accountList[acc.id()][QDate::currentDate().addDays(accountsCycle())] - d->m_accountList[acc.id()][QDate::currentDate()]; break; } } } return cycleVariation; } MyMoneyMoney MyMoneyForecast::accountTotalVariation(const MyMoneyAccount& acc) { MyMoneyMoney totalVariation; totalVariation = forecastBalance(acc, forecastEndDate()) - forecastBalance(acc, QDate::currentDate()); return totalVariation; } QList MyMoneyForecast::accountMinimumBalanceDateList(const MyMoneyAccount& acc) { QList minBalanceList; int daysToBeginDay; daysToBeginDay = QDate::currentDate().daysTo(beginForecastDate()); for (int t_cycle = 0; ((t_cycle * accountsCycle()) + daysToBeginDay) < forecastDays() ; ++t_cycle) { MyMoneyMoney minBalance = forecastBalance(acc, (t_cycle * accountsCycle() + daysToBeginDay)); QDate minDate = QDate::currentDate().addDays(t_cycle * accountsCycle() + daysToBeginDay); for (int t_day = 1; t_day <= accountsCycle() ; ++t_day) { if (minBalance > forecastBalance(acc, (t_cycle * accountsCycle()) + daysToBeginDay + t_day)) { minBalance = forecastBalance(acc, (t_cycle * accountsCycle()) + daysToBeginDay + t_day); minDate = QDate::currentDate().addDays((t_cycle * accountsCycle()) + daysToBeginDay + t_day); } } minBalanceList.append(minDate); } return minBalanceList; } QList MyMoneyForecast::accountMaximumBalanceDateList(const MyMoneyAccount& acc) { QList maxBalanceList; int daysToBeginDay; daysToBeginDay = QDate::currentDate().daysTo(beginForecastDate()); for (int t_cycle = 0; ((t_cycle * accountsCycle()) + daysToBeginDay) < forecastDays() ; ++t_cycle) { MyMoneyMoney maxBalance = forecastBalance(acc, ((t_cycle * accountsCycle()) + daysToBeginDay)); QDate maxDate = QDate::currentDate().addDays((t_cycle * accountsCycle()) + daysToBeginDay); for (int t_day = 0; t_day < accountsCycle() ; ++t_day) { if (maxBalance < forecastBalance(acc, (t_cycle * accountsCycle()) + daysToBeginDay + t_day)) { maxBalance = forecastBalance(acc, (t_cycle * accountsCycle()) + daysToBeginDay + t_day); maxDate = QDate::currentDate().addDays((t_cycle * accountsCycle()) + daysToBeginDay + t_day); } } maxBalanceList.append(maxDate); } return maxBalanceList; } MyMoneyMoney MyMoneyForecast::accountAverageBalance(const MyMoneyAccount& acc) { MyMoneyMoney totalBalance; for (int f_day = 1; f_day <= forecastDays() ; ++f_day) { totalBalance += forecastBalance(acc, f_day); } return totalBalance / MyMoneyMoney(forecastDays(), 1); } void MyMoneyForecast::createBudget(MyMoneyBudget& budget, QDate historyStart, QDate historyEnd, QDate budgetStart, QDate budgetEnd, const bool returnBudget) { Q_D(MyMoneyForecast); // clear all data except the id and name QString name = budget.name(); budget = MyMoneyBudget(budget.id(), MyMoneyBudget()); budget.setName(name); //check parameters if (historyStart > historyEnd || budgetStart > budgetEnd || budgetStart <= historyEnd) { throw MYMONEYEXCEPTION("Illegal parameters when trying to create budget"); } //get forecast method auto fMethod = d->forecastMethod(); //set start date to 1st of month and end dates to last day of month, since we deal with full months in budget historyStart = QDate(historyStart.year(), historyStart.month(), 1); historyEnd = QDate(historyEnd.year(), historyEnd.month(), historyEnd.daysInMonth()); budgetStart = QDate(budgetStart.year(), budgetStart.month(), 1); budgetEnd = QDate(budgetEnd.year(), budgetEnd.month(), budgetEnd.daysInMonth()); //set forecast parameters setHistoryStartDate(historyStart); setHistoryEndDate(historyEnd); setForecastStartDate(budgetStart); setForecastEndDate(budgetEnd); setForecastDays(budgetStart.daysTo(budgetEnd) + 1); if (budgetStart.daysTo(budgetEnd) > historyStart.daysTo(historyEnd)) { //if history period is shorter than budget, use that one as the trend length setAccountsCycle(historyStart.daysTo(historyEnd)); //we set the accountsCycle to the base timeframe we will use to calculate the average (eg. 180 days, 365, etc) } else { //if one timeframe is larger than the other, but not enough to be 1 time larger, we take the lowest value setAccountsCycle(budgetStart.daysTo(budgetEnd)); } setForecastCycles((historyStart.daysTo(historyEnd) / accountsCycle())); if (forecastCycles() == 0) //the cycles must be at least 1 setForecastCycles(1); //do not skip opening date setSkipOpeningDate(false); //clear and set accounts list we are going to use. Categories, in this case d->m_forecastAccounts.clear(); d->setBudgetAccountList(); //calculate budget according to forecast method switch (fMethod) { case eForecastMethod::Scheduled: d->doFutureScheduledForecast(); d->calculateScheduledMonthlyBalances(); break; case eForecastMethod::Historic: d->pastTransactions(); //get all transactions for history period d->calculateAccountTrendList(); d->calculateHistoricMonthlyBalances(); //add all balances of each month and put at the 1st day of each month break; default: break; } //flag the forecast as done d->m_forecastDone = true; //only fill the budget if it is going to be used if (returnBudget) { //setup the budget itself auto file = MyMoneyFile::instance(); budget.setBudgetStart(budgetStart); //go through all the accounts and add them to budget for (auto it_nc = d->m_forecastAccounts.constBegin(); it_nc != d->m_forecastAccounts.constEnd(); ++it_nc) { auto acc = file->account(*it_nc); MyMoneyBudget::AccountGroup budgetAcc; budgetAcc.setId(acc.id()); budgetAcc.setBudgetLevel(MyMoneyBudget::AccountGroup::eMonthByMonth); for (QDate f_date = forecastStartDate(); f_date <= forecastEndDate();) { MyMoneyBudget::PeriodGroup period; //add period to budget account period.setStartDate(f_date); period.setAmount(forecastBalance(acc, f_date)); budgetAcc.addPeriod(f_date, period); //next month f_date = f_date.addMonths(1); } //add budget account to budget budget.setAccount(budgetAcc, acc.id()); } } } int MyMoneyForecast::historyDays() const { Q_D(const MyMoneyForecast); return (d->m_historyStartDate.daysTo(d->m_historyEndDate) + 1); } void MyMoneyForecast::setAccountsCycle(int accountsCycle) { Q_D(MyMoneyForecast); d->m_accountsCycle = accountsCycle; } void MyMoneyForecast::setForecastCycles(int forecastCycles) { Q_D(MyMoneyForecast); d->m_forecastCycles = forecastCycles; } void MyMoneyForecast::setForecastDays(int forecastDays) { Q_D(MyMoneyForecast); d->m_forecastDays = forecastDays; } void MyMoneyForecast::setBeginForecastDate(const QDate &beginForecastDate) { Q_D(MyMoneyForecast); d->m_beginForecastDate = beginForecastDate; } void MyMoneyForecast::setBeginForecastDay(int beginDay) { Q_D(MyMoneyForecast); d->m_beginForecastDay = beginDay; } void MyMoneyForecast::setForecastMethod(int forecastMethod) { Q_D(MyMoneyForecast); d->m_forecastMethod = static_cast(forecastMethod); } void MyMoneyForecast::setHistoryStartDate(const QDate &historyStartDate) { Q_D(MyMoneyForecast); d->m_historyStartDate = historyStartDate; } void MyMoneyForecast::setHistoryEndDate(const QDate &historyEndDate) { Q_D(MyMoneyForecast); d->m_historyEndDate = historyEndDate; } void MyMoneyForecast::setHistoryStartDate(int daysToStartDate) { setHistoryStartDate(QDate::currentDate().addDays(-daysToStartDate)); } void MyMoneyForecast::setHistoryEndDate(int daysToEndDate) { setHistoryEndDate(QDate::currentDate().addDays(-daysToEndDate)); } void MyMoneyForecast::setForecastStartDate(const QDate &_startDate) { Q_D(MyMoneyForecast); d->m_forecastStartDate = _startDate; } void MyMoneyForecast::setForecastEndDate(const QDate &_endDate) { Q_D(MyMoneyForecast); d->m_forecastEndDate = _endDate; } void MyMoneyForecast::setSkipOpeningDate(bool _skip) { Q_D(MyMoneyForecast); d->m_skipOpeningDate = _skip; } void MyMoneyForecast::setHistoryMethod(int historyMethod) { Q_D(MyMoneyForecast); d->m_historyMethod = historyMethod; } void MyMoneyForecast::setIncludeUnusedAccounts(bool _bool) { Q_D(MyMoneyForecast); d->m_includeUnusedAccounts = _bool; } void MyMoneyForecast::setForecastDone(bool _bool) { Q_D(MyMoneyForecast); d->m_forecastDone = _bool; } void MyMoneyForecast::setIncludeFutureTransactions(bool _bool) { Q_D(MyMoneyForecast); d->m_includeFutureTransactions = _bool; } void MyMoneyForecast::setIncludeScheduledTransactions(bool _bool) { Q_D(MyMoneyForecast); d->m_includeScheduledTransactions = _bool; } int MyMoneyForecast::accountsCycle() const { Q_D(const MyMoneyForecast); return d->m_accountsCycle; } int MyMoneyForecast::forecastCycles() const { Q_D(const MyMoneyForecast); return d->m_forecastCycles; } int MyMoneyForecast::forecastDays() const { Q_D(const MyMoneyForecast); return d->m_forecastDays; } QDate MyMoneyForecast::beginForecastDate() const { Q_D(const MyMoneyForecast); return d->m_beginForecastDate; } int MyMoneyForecast::beginForecastDay() const { Q_D(const MyMoneyForecast); return d->m_beginForecastDay; } QDate MyMoneyForecast::historyStartDate() const { Q_D(const MyMoneyForecast); return d->m_historyStartDate; } QDate MyMoneyForecast::historyEndDate() const { Q_D(const MyMoneyForecast); return d->m_historyEndDate; } QDate MyMoneyForecast::forecastStartDate() const { Q_D(const MyMoneyForecast); return d->m_forecastStartDate; } QDate MyMoneyForecast::forecastEndDate() const { Q_D(const MyMoneyForecast); return d->m_forecastEndDate; } bool MyMoneyForecast::skipOpeningDate() const { Q_D(const MyMoneyForecast); return d->m_skipOpeningDate; } int MyMoneyForecast::historyMethod() const { Q_D(const MyMoneyForecast); return d->m_historyMethod; } bool MyMoneyForecast::isIncludingUnusedAccounts() const { Q_D(const MyMoneyForecast); return d->m_includeUnusedAccounts; } bool MyMoneyForecast::isForecastDone() const { Q_D(const MyMoneyForecast); return d->m_forecastDone; } bool MyMoneyForecast::isIncludingFutureTransactions() const { Q_D(const MyMoneyForecast); return d->m_includeFutureTransactions; } bool MyMoneyForecast::isIncludingScheduledTransactions() const { Q_D(const MyMoneyForecast); return d->m_includeScheduledTransactions; } void MyMoneyForecast::calculateAutoLoan(const MyMoneySchedule& schedule, MyMoneyTransaction& transaction, const QMap& balances) { if (schedule.type() == eMyMoney::Schedule::Type::LoanPayment) { //get amortization and interest autoCalc splits MyMoneySplit amortizationSplit = transaction.amortizationSplit(); MyMoneySplit interestSplit = transaction.interestSplit(); const bool interestSplitValid = !interestSplit.id().isEmpty(); if (!amortizationSplit.id().isEmpty()) { MyMoneyAccountLoan acc(MyMoneyFile::instance()->account(amortizationSplit.accountId())); MyMoneyFinancialCalculator calc; QDate dueDate; // FIXME: setup dueDate according to when the interest should be calculated // current implementation: take the date of the next payment according to // the schedule. If the calculation is based on the payment reception, and // the payment is overdue then take the current date dueDate = schedule.nextDueDate(); if (acc.interestCalculation() == MyMoneyAccountLoan::paymentReceived) { if (dueDate < QDate::currentDate()) dueDate = QDate::currentDate(); } // we need to calculate the balance at the time the payment is due MyMoneyMoney balance; if (balances.count() == 0) balance = MyMoneyFile::instance()->balance(acc.id(), dueDate.addDays(-1)); else balance = balances[acc.id()]; // FIXME: for now, we only support interest calculation at the end of the period calc.setBep(); // FIXME: for now, we only support periodic compounding calc.setDisc(); calc.setPF(MyMoneySchedule::eventsPerYear(schedule.occurrence())); eMyMoney::Schedule::Occurrence compoundingOccurrence = static_cast(acc.interestCompounding()); if (compoundingOccurrence == eMyMoney::Schedule::Occurrence::Any) compoundingOccurrence = schedule.occurrence(); calc.setCF(MyMoneySchedule::eventsPerYear(compoundingOccurrence)); calc.setPv(balance.toDouble()); calc.setIr(acc.interestRate(dueDate).abs().toDouble()); calc.setPmt(acc.periodicPayment().toDouble()); MyMoneyMoney interest(calc.interestDue(), 100), amortization; interest = interest.abs(); // make sure it's positive for now amortization = acc.periodicPayment() - interest; if (acc.accountType() == eMyMoney::Account::Type::AssetLoan) { interest = -interest; amortization = -amortization; } amortizationSplit.setShares(amortization); if (interestSplitValid) interestSplit.setShares(interest); // FIXME: for now we only assume loans to be in the currency of the transaction amortizationSplit.setValue(amortization); if (interestSplitValid) interestSplit.setValue(interest); transaction.modifySplit(amortizationSplit); if (interestSplitValid) transaction.modifySplit(interestSplit); } } } QList MyMoneyForecast::forecastAccountList() { auto file = MyMoneyFile::instance(); QList accList; //Get all accounts from the file and check if they are of the right type to calculate forecast file->accountList(accList); QList::iterator accList_t = accList.begin(); for (; accList_t != accList.end();) { auto acc = *accList_t; if (acc.isClosed() //check the account is not closed || (!acc.isAssetLiability())) { //|| (acc.accountType() == eMyMoney::Account::Type::Investment) ) {//check that it is not an Investment account and only include Stock accounts //remove the account if it is not of the correct type accList_t = accList.erase(accList_t); } else { ++accList_t; } } return accList; } diff --git a/kmymoney/mymoney/mymoneymoney.cpp b/kmymoney/mymoney/mymoneymoney.cpp index a419a57e2..9a1946380 100644 --- a/kmymoney/mymoney/mymoneymoney.cpp +++ b/kmymoney/mymoney/mymoneymoney.cpp @@ -1,353 +1,421 @@ /*************************************************************************** mymoneymymoney.cpp - description ------------------- begin : Thu Feb 21 2002 copyright : (C) 2000-2002 by Michael Edwardes (C) 2011 by Carlos Eduardo da Silva (C) 2001-2017 by 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. * * * ***************************************************************************/ // make sure, that this is defined before we even include any other header file #ifndef __STDC_LIMIT_MACROS #define __STDC_LIMIT_MACROS // force definition of min and max values #endif #include "mymoneymoney.h" #include +#include // ---------------------------------------------------------------------------- // QT Includes +#include + // ---------------------------------------------------------------------------- // Project Includes +#include "mymoneyexception.h" +#include "mymoneyenums.h" const MyMoneyMoney MyMoneyMoney::ONE = MyMoneyMoney(1, 1); const MyMoneyMoney MyMoneyMoney::MINUS_ONE = MyMoneyMoney(-1, 1); +namespace eMyMoney +{ + namespace Money { + + enum fileVersionE : int { + FILE_4_BYTE_VALUE = 0, + FILE_8_BYTE_VALUE + }; + + QChar _thousandSeparator = QLatin1Char(','); + QChar _decimalSeparator = QLatin1Char('.'); + eMyMoney::Money::signPosition _negativeMonetarySignPosition = BeforeQuantityMoney; + eMyMoney::Money::signPosition _positiveMonetarySignPosition = BeforeQuantityMoney; + bool _negativePrefixCurrencySymbol = false; + bool _positivePrefixCurrencySymbol = false; + eMyMoney::Money::fileVersionE _fileVersion = fileVersionE::FILE_4_BYTE_VALUE; + } +} -QChar MyMoneyMoney::_thousandSeparator = ','; -QChar MyMoneyMoney::_decimalSeparator = '.'; -MyMoneyMoney::signPosition MyMoneyMoney::_negativeMonetarySignPosition = BeforeQuantityMoney; -MyMoneyMoney::signPosition MyMoneyMoney::_positiveMonetarySignPosition = BeforeQuantityMoney; -bool MyMoneyMoney::_negativePrefixCurrencySymbol = false; -bool MyMoneyMoney::_positivePrefixCurrencySymbol = false; +//eMyMoney::Money::_thousandSeparator = QLatin1Char(','); +//eMyMoney::Money::_decimalSeparator = QLatin1Char('.'); +//eMyMoney::Money::signPosition eMyMoney::Money::_negativeMonetarySignPosition = BeforeQuantityMoney; +//eMyMoney::Money::signPosition eMyMoney::Money::_positiveMonetarySignPosition = BeforeQuantityMoney; +//bool eMyMoney::Money::_negativePrefixCurrencySymbol = false; +//bool eMyMoney::Money::_positivePrefixCurrencySymbol = false; -MyMoneyMoney::fileVersionE MyMoneyMoney::_fileVersion = MyMoneyMoney::FILE_4_BYTE_VALUE; +//MyMoneyMoney::fileVersionE eMyMoney::Money::_fileVersion = MyMoneyMoney::FILE_4_BYTE_VALUE; MyMoneyMoney MyMoneyMoney::maxValue = MyMoneyMoney(INT64_MAX, 100); MyMoneyMoney MyMoneyMoney::minValue = MyMoneyMoney(INT64_MIN, 100); MyMoneyMoney MyMoneyMoney::autoCalc = MyMoneyMoney(INT64_MIN + 1, 100); void MyMoneyMoney::setNegativePrefixCurrencySymbol(const bool flag) { - _negativePrefixCurrencySymbol = flag; + eMyMoney::Money::_negativePrefixCurrencySymbol = flag; } void MyMoneyMoney::setPositivePrefixCurrencySymbol(const bool flag) { - _positivePrefixCurrencySymbol = flag; + eMyMoney::Money::_positivePrefixCurrencySymbol = flag; } -void MyMoneyMoney::setNegativeMonetarySignPosition(const signPosition pos) +void MyMoneyMoney::setNegativeMonetarySignPosition(const eMyMoney::Money::signPosition pos) { - _negativeMonetarySignPosition = pos; + eMyMoney::Money::_negativeMonetarySignPosition = pos; } -MyMoneyMoney::signPosition MyMoneyMoney::negativeMonetarySignPosition() +eMyMoney::Money::signPosition MyMoneyMoney::negativeMonetarySignPosition() { - return _negativeMonetarySignPosition; + return eMyMoney::Money::_negativeMonetarySignPosition; } -void MyMoneyMoney::setPositiveMonetarySignPosition(const signPosition pos) +void MyMoneyMoney::setPositiveMonetarySignPosition(const eMyMoney::Money::signPosition pos) { - _positiveMonetarySignPosition = pos; + eMyMoney::Money::_positiveMonetarySignPosition = pos; } -MyMoneyMoney::signPosition MyMoneyMoney::positiveMonetarySignPosition() +eMyMoney::Money::signPosition MyMoneyMoney::positiveMonetarySignPosition() { - return _positiveMonetarySignPosition; + return eMyMoney::Money::_positiveMonetarySignPosition; } void MyMoneyMoney::setThousandSeparator(const QChar &separator) { - if (separator != ' ') - _thousandSeparator = separator; + if (separator != QLatin1Char(' ')) + eMyMoney::Money::_thousandSeparator = separator; else - _thousandSeparator = 0; + eMyMoney::Money::_thousandSeparator = 0; } const QChar MyMoneyMoney::thousandSeparator() { - return _thousandSeparator; + return eMyMoney::Money::_thousandSeparator; } void MyMoneyMoney::setDecimalSeparator(const QChar &separator) { - if (separator != ' ') - _decimalSeparator = separator; + if (separator != QLatin1Char(' ')) + eMyMoney::Money::_decimalSeparator = separator; else - _decimalSeparator = 0; + eMyMoney::Money::_decimalSeparator = 0; } const QChar MyMoneyMoney::decimalSeparator() { - return _decimalSeparator; + return eMyMoney::Money::_decimalSeparator; } -void MyMoneyMoney::setFileVersion(fileVersionE version) +MyMoneyMoney::MyMoneyMoney(const QString& pszAmount) + : AlkValue(pszAmount, eMyMoney::Money::_decimalSeparator) { - _fileVersion = version; } +//////////////////////////////////////////////////////////////////////////////// +// Name: MyMoneyMoney +// Purpose: Constructor - constructs object from an amount in a signed64 value +// Returns: None +// Throws: Nothing. +// Arguments: Amount - signed 64 object containing amount +// denom - denominator of the object +// +//////////////////////////////////////////////////////////////////////////////// +MyMoneyMoney::MyMoneyMoney(signed64 Amount, const signed64 denom) +{ + if (!denom) + throw MYMONEYEXCEPTION("Denominator 0 not allowed!"); -MyMoneyMoney::MyMoneyMoney(const QString& pszAmount) - : AlkValue(pszAmount, _decimalSeparator) + *this = AlkValue(QString::fromLatin1("%1/%2").arg(Amount).arg(denom), eMyMoney::Money::_decimalSeparator); +} + +//////////////////////////////////////////////////////////////////////////////// +// Name: MyMoneyMoney +// Purpose: Constructor - constructs object from an amount in a integer value +// Returns: None +// Throws: Nothing. +// Arguments: iAmount - integer object containing amount +// denom - denominator of the object +// +//////////////////////////////////////////////////////////////////////////////// +MyMoneyMoney::MyMoneyMoney(const int iAmount, const signed64 denom) +{ + if (!denom) + throw MYMONEYEXCEPTION("Denominator 0 not allowed!"); + *this = AlkValue(iAmount, denom); +} + +//////////////////////////////////////////////////////////////////////////////// +// Name: MyMoneyMoney +// Purpose: Constructor - constructs object from an amount in a long integer value +// Returns: None +// Throws: Nothing. +// Arguments: iAmount - integer object containing amount +// denom - denominator of the object +// +//////////////////////////////////////////////////////////////////////////////// +MyMoneyMoney::MyMoneyMoney(const long int iAmount, const signed64 denom) { + if (!denom) + throw MYMONEYEXCEPTION("Denominator 0 not allowed!"); + *this = AlkValue(QString::fromLatin1("%1/%2").arg(iAmount).arg(denom), eMyMoney::Money::_decimalSeparator); } + MyMoneyMoney::~MyMoneyMoney() { } MyMoneyMoney MyMoneyMoney::abs() const { return static_cast(AlkValue::abs()); } QString MyMoneyMoney::formatMoney(int denom, bool showThousandSeparator) const { - return formatMoney("", denomToPrec(denom), showThousandSeparator); + return formatMoney(QString(), denomToPrec(denom), showThousandSeparator); } QString MyMoneyMoney::formatMoney(const QString& currency, const int prec, bool showThousandSeparator) const { QString res; QString tmpCurrency = currency; int tmpPrec = prec; mpz_class denom = 1; mpz_class value; // if prec == -1 we want the maximum possible but w/o trailing zeroes if (tmpPrec > -1) { while (tmpPrec--) { denom *= 10; } } else { // fix it to a max of 9 digits on the right side for now denom = 1000000000; } // as long as AlkValue::convertDenominator() does not take an // mpz_class as the denominator, we need to use a signed int // and limit the precision to 9 digits (the max we can // present with 31 bits #if 1 signed int d; if (mpz_fits_sint_p(denom.get_mpz_t())) { d = mpz_get_si(denom.get_mpz_t()); } else { d = 1000000000; } value = static_cast(convertDenominator(d)).valueRef().get_num(); #else value = static_cast(convertDenominator(denom)).valueRef().get_num(); #endif // Once we really support multiple currencies then this method will // be much better than using KLocale::global()->formatMoney. bool bNegative = false; mpz_class left = value / static_cast(convertDenominator(d)).valueRef().get_den(); mpz_class right = mpz_class((valueRef() - mpq_class(left)) * denom); if (right < 0) { right = -right; bNegative = true; } if (left < 0) { left = -left; bNegative = true; } // convert the integer (left) part to a string res.append(left.get_str().c_str()); // if requested, insert thousand separators every three digits if (showThousandSeparator) { int pos = res.length(); while ((0 < (pos -= 3)) && thousandSeparator() != 0) res.insert(pos, thousandSeparator()); } // take care of the fractional part if (prec > 0 || (prec == -1 && right != 0)) { if (decimalSeparator() != 0) res += decimalSeparator(); - QString rs = QString("%1").arg(right.get_str().c_str()); + auto rs = QString::fromLatin1("%1").arg(right.get_str().c_str()); if (prec != -1) - rs = rs.rightJustified(prec, '0', true); + rs = rs.rightJustified(prec, QLatin1Char('0'), true); else { - rs = rs.rightJustified(9, '0', true); + rs = rs.rightJustified(9, QLatin1Char('0'), true); // no trailing zeroes or decimal separators - while (rs.endsWith('0')) + while (rs.endsWith(QLatin1Char('0'))) rs.truncate(rs.length() - 1); - while (rs.endsWith(QChar(decimalSeparator()))) + while (rs.endsWith(decimalSeparator())) rs.truncate(rs.length() - 1); } res += rs; } - signPosition signpos = bNegative ? _negativeMonetarySignPosition : _positiveMonetarySignPosition; - QString sign = bNegative ? "-" : ""; + eMyMoney::Money::signPosition signpos = bNegative ? eMyMoney::Money::_negativeMonetarySignPosition : eMyMoney::Money::_positiveMonetarySignPosition; + auto sign = bNegative ? QString::fromLatin1("-") : QString(); switch (signpos) { - case ParensAround: - res.prepend('('); - res.append(')'); + case eMyMoney::Money::ParensAround: + res.prepend(QLatin1Char('(')); + res.append(QLatin1Char(')')); break; - case BeforeQuantityMoney: + case eMyMoney::Money::BeforeQuantityMoney: res.prepend(sign); break; - case AfterQuantityMoney: + case eMyMoney::Money::AfterQuantityMoney: res.append(sign); break; - case BeforeMoney: + case eMyMoney::Money::BeforeMoney: tmpCurrency.prepend(sign); break; - case AfterMoney: + case eMyMoney::Money::AfterMoney: tmpCurrency.append(sign); break; } if (!tmpCurrency.isEmpty()) { - if (bNegative ? _negativePrefixCurrencySymbol : _positivePrefixCurrencySymbol) { - res.prepend(' '); + if (bNegative ? eMyMoney::Money::_negativePrefixCurrencySymbol : eMyMoney::Money::_positivePrefixCurrencySymbol) { + res.prepend(QLatin1Char(' ')); res.prepend(tmpCurrency); } else { - res.append(' '); + res.append(QLatin1Char(' ')); res.append(tmpCurrency); } } return res; } //////////////////////////////////////////////////////////////////////////////// // Name: operator+ // Purpose: Addition operator - adds the input amount to the object // Returns: The current object // Throws: Nothing. // Arguments: b - MyMoneyMoney object to be added // //////////////////////////////////////////////////////////////////////////////// const MyMoneyMoney MyMoneyMoney::operator+(const MyMoneyMoney& _b) const { return static_cast(AlkValue::operator+(_b)); } //////////////////////////////////////////////////////////////////////////////// // Name: operator- // Purpose: Addition operator - subtracts the input amount from the object // Returns: The current object // Throws: Nothing. // Arguments: AmountInPence - MyMoneyMoney object to be subtracted // //////////////////////////////////////////////////////////////////////////////// const MyMoneyMoney MyMoneyMoney::operator-(const MyMoneyMoney& _b) const { return static_cast(AlkValue::operator-(_b)); } //////////////////////////////////////////////////////////////////////////////// // Name: operator* // Purpose: Multiplication operator - multiplies the input amount to the object // Returns: The current object // Throws: Nothing. // Arguments: b - MyMoneyMoney object to be multiplied // //////////////////////////////////////////////////////////////////////////////// const MyMoneyMoney MyMoneyMoney::operator*(const MyMoneyMoney& _b) const { return static_cast(AlkValue::operator*(_b)); } //////////////////////////////////////////////////////////////////////////////// // Name: operator/ // Purpose: Division operator - divides the object by the input amount // Returns: The current object // Throws: Nothing. // Arguments: b - MyMoneyMoney object to be used as dividend // //////////////////////////////////////////////////////////////////////////////// const MyMoneyMoney MyMoneyMoney::operator/(const MyMoneyMoney& _b) const { return static_cast(AlkValue::operator/(_b)); } bool MyMoneyMoney::isNegative() const { return (valueRef() < 0) ? true : false; } bool MyMoneyMoney::isPositive() const { return (valueRef() > 0) ? true : false; } bool MyMoneyMoney::isZero() const { return valueRef() == 0; } bool MyMoneyMoney::isAutoCalc() const { return (*this == autoCalc); } -MyMoneyMoney MyMoneyMoney::convert(const signed64 _denom, const roundingMethod how) const +MyMoneyMoney MyMoneyMoney::convert(const signed64 _denom, const AlkValue::RoundingMethod how) const { - return static_cast(convertDenominator(_denom, static_cast(how))); + return static_cast(convertDenominator(_denom, how)); } MyMoneyMoney MyMoneyMoney::reduce() const { MyMoneyMoney out(*this); out.canonicalize(); return out; } signed64 MyMoneyMoney::precToDenom(int prec) { signed64 denom = 1; while (prec--) denom *= 10; return denom; } double MyMoneyMoney::toDouble() const { return valueRef().get_d(); } int MyMoneyMoney::denomToPrec(signed64 fract) { int rc = 0; while (fract > 1) { rc++; fract /= 10; } return rc; } diff --git a/kmymoney/mymoney/mymoneymoney.h b/kmymoney/mymoney/mymoneymoney.h index 837574f7f..265e03113 100644 --- a/kmymoney/mymoney/mymoneymoney.h +++ b/kmymoney/mymoney/mymoneymoney.h @@ -1,459 +1,368 @@ /*************************************************************************** mymoneymoney.h ------------------- copyright : (C) 2000-2002 by Michael Edwardes email : mte@users.sourceforge.net + (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. * * * ***************************************************************************/ #ifndef MYMONEYMONEY_H #define MYMONEYMONEY_H -// #include -#include - -//FIXME workaround for dealing with lond double -#include - // So we can save this object -#include -#include #include #include "kmm_mymoney_export.h" #include "mymoneyunittestable.h" -#include "mymoneyexception.h" #include typedef qint64 signed64; typedef quint64 unsigned64; +namespace eMyMoney { namespace Money { enum signPosition : int; } } /** * This class represents a value within the MyMoney Engine * * @author Michael Edwardes * @author Thomas Baumgart */ class KMM_MYMONEY_EXPORT MyMoneyMoney : public AlkValue { KMM_MYMONEY_UNIT_TESTABLE public: - enum fileVersionE { - FILE_4_BYTE_VALUE = 0, - FILE_8_BYTE_VALUE - }; - - enum signPosition { - // keep those in sync with the ones defined in klocale.h - ParensAround = 0, - BeforeQuantityMoney = 1, - AfterQuantityMoney = 2, - BeforeMoney = 3, - AfterMoney = 4 - }; - - enum roundingMethod { - RndNever = 0, - RndFloor, - RndCeil, - RndTrunc, - RndPromote, - RndHalfDown, - RndHalfUp, - RndRound - }; - // construction MyMoneyMoney(); explicit MyMoneyMoney(const int iAmount, const signed64 denom); explicit MyMoneyMoney(const long int iAmount, const signed64 denom); explicit MyMoneyMoney(const QString& pszAmount); explicit MyMoneyMoney(const signed64 Amount, const signed64 denom); explicit MyMoneyMoney(const double dAmount, const signed64 denom = 100); // copy constructor MyMoneyMoney(const MyMoneyMoney& Amount); explicit MyMoneyMoney(const AlkValue& Amount); virtual ~MyMoneyMoney(); MyMoneyMoney abs() const; /** * This method returns a formatted string according to the settings * of _thousandSeparator, _decimalSeparator, _negativeMonetarySignPosition, * _positiveMonetaryPosition, _negativePrefixCurrencySymbol and * _positivePrefixCurrencySymbol. Those values can be modified using * the appropriate set-methods. * * @param currency The currency symbol * @param prec The number of fractional digits * @param showThousandSeparator should the thousandSeparator symbol * be inserted (@a true) * or not (@a false) (default true) */ QString formatMoney(const QString& currency, const int prec, bool showThousandSeparator = true) const; /** * This is a convenience method. It behaves exactly as the above one, * but takes the information about precision out of the denomination * @a denom. No currency symbol is shown. If you want to add a currency * symbol, please use MyMoneyUtils::formatMoney(const MyMoneyAccount& acc, const MyMoneySecurity& sec, bool showThousandSeparator) * instead. * * @note denom is often set to account.fraction(security). */ QString formatMoney(int denom, bool showThousandSeparator = true) const; /** * This method is used to convert the smallest fraction information into * the corresponding number of digits used for precision. * * @param fract smallest fractional part (e.g. 100 for cents) * @return number of precision digits (e.g. 2 for cents) */ static int denomToPrec(signed64 fract); - MyMoneyMoney convert(const signed64 denom = 100, const roundingMethod how = RndRound) const; + MyMoneyMoney convert(const signed64 denom = 100, const AlkValue::RoundingMethod how = AlkValue::RoundRound) const; static signed64 precToDenom(int prec); double toDouble() const; static void setThousandSeparator(const QChar &); static void setDecimalSeparator(const QChar &); - static void setNegativeMonetarySignPosition(const signPosition pos); - static void setPositiveMonetarySignPosition(const signPosition pos); + static void setNegativeMonetarySignPosition(const eMyMoney::Money::signPosition pos); + static void setPositiveMonetarySignPosition(const eMyMoney::Money::signPosition pos); static void setNegativePrefixCurrencySymbol(const bool flags); static void setPositivePrefixCurrencySymbol(const bool flags); static const QChar thousandSeparator(); static const QChar decimalSeparator(); - static signPosition negativeMonetarySignPosition(); - static signPosition positiveMonetarySignPosition(); - static void setFileVersion(const fileVersionE version); + static eMyMoney::Money::signPosition negativeMonetarySignPosition(); + static eMyMoney::Money::signPosition positiveMonetarySignPosition(); const MyMoneyMoney& operator=(const QString& pszAmount); const MyMoneyMoney& operator=(const AlkValue& val); // comparison bool operator==(const MyMoneyMoney& Amount) const; bool operator!=(const MyMoneyMoney& Amount) const; bool operator<(const MyMoneyMoney& Amount) const; bool operator>(const MyMoneyMoney& Amount) const; bool operator<=(const MyMoneyMoney& Amount) const; bool operator>=(const MyMoneyMoney& Amount) const; bool operator==(const QString& pszAmount) const; bool operator!=(const QString& pszAmount) const; bool operator<(const QString& pszAmount) const; bool operator>(const QString& pszAmount) const; bool operator<=(const QString& pszAmount) const; bool operator>=(const QString& pszAmount) const; // calculation const MyMoneyMoney operator+(const MyMoneyMoney& Amount) const; const MyMoneyMoney operator-(const MyMoneyMoney& Amount) const; const MyMoneyMoney operator*(const MyMoneyMoney& factor) const; const MyMoneyMoney operator/(const MyMoneyMoney& Amount) const; const MyMoneyMoney operator-() const; const MyMoneyMoney operator*(int factor) const; static MyMoneyMoney maxValue; static MyMoneyMoney minValue; static MyMoneyMoney autoCalc; bool isNegative() const; bool isPositive() const; bool isZero() const; bool isAutoCalc() const; MyMoneyMoney reduce() const; static const MyMoneyMoney ONE; static const MyMoneyMoney MINUS_ONE; - -private: - - static QChar _thousandSeparator; - static QChar _decimalSeparator; - static signPosition _negativeMonetarySignPosition; - static signPosition _positiveMonetarySignPosition; - static bool _negativePrefixCurrencySymbol; - static bool _positivePrefixCurrencySymbol; - static MyMoneyMoney::fileVersionE _fileVersion; }; //============================================================================= // // Inline functions // //============================================================================= //////////////////////////////////////////////////////////////////////////////// // Name: MyMoneyMoney // Purpose: Constructor - constructs object set to 0. // Returns: None // Throws: Nothing. // Arguments: None // //////////////////////////////////////////////////////////////////////////////// inline MyMoneyMoney::MyMoneyMoney() : AlkValue() { } -//////////////////////////////////////////////////////////////////////////////// -// Name: MyMoneyMoney -// Purpose: Constructor - constructs object from an amount in a signed64 value -// Returns: None -// Throws: Nothing. -// Arguments: Amount - signed 64 object containing amount -// denom - denominator of the object -// -//////////////////////////////////////////////////////////////////////////////// -inline MyMoneyMoney::MyMoneyMoney(signed64 Amount, const signed64 denom) -{ - if (!denom) - throw MYMONEYEXCEPTION("Denominator 0 not allowed!"); - - *this = AlkValue(QString("%1/%2").arg(Amount).arg(denom), _decimalSeparator); -} //////////////////////////////////////////////////////////////////////////////// // Name: MyMoneyMoney // Purpose: Constructor - constructs object from an amount in a double value // Returns: None // Throws: Nothing. // Arguments: dAmount - double object containing amount // denom - denominator of the object // //////////////////////////////////////////////////////////////////////////////// inline MyMoneyMoney::MyMoneyMoney(const double dAmount, const signed64 denom) : AlkValue(dAmount, denom) { } -//////////////////////////////////////////////////////////////////////////////// -// Name: MyMoneyMoney -// Purpose: Constructor - constructs object from an amount in a integer value -// Returns: None -// Throws: Nothing. -// Arguments: iAmount - integer object containing amount -// denom - denominator of the object -// -//////////////////////////////////////////////////////////////////////////////// -inline MyMoneyMoney::MyMoneyMoney(const int iAmount, const signed64 denom) -{ - if (!denom) - throw MYMONEYEXCEPTION("Denominator 0 not allowed!"); - *this = AlkValue(iAmount, denom); -} - -//////////////////////////////////////////////////////////////////////////////// -// Name: MyMoneyMoney -// Purpose: Constructor - constructs object from an amount in a long integer value -// Returns: None -// Throws: Nothing. -// Arguments: iAmount - integer object containing amount -// denom - denominator of the object -// -//////////////////////////////////////////////////////////////////////////////// -inline MyMoneyMoney::MyMoneyMoney(const long int iAmount, const signed64 denom) -{ - if (!denom) - throw MYMONEYEXCEPTION("Denominator 0 not allowed!"); - *this = AlkValue(QString("%1/%2").arg(iAmount).arg(denom), _decimalSeparator); -} - //////////////////////////////////////////////////////////////////////////////// // Name: MyMoneyMoney // Purpose: Copy Constructor - constructs object from another // MyMoneyMoney object // Returns: None // Throws: Nothing. // Arguments: Amount - MyMoneyMoney object to be copied // //////////////////////////////////////////////////////////////////////////////// inline MyMoneyMoney::MyMoneyMoney(const MyMoneyMoney& Amount) : AlkValue(Amount) { } inline MyMoneyMoney::MyMoneyMoney(const AlkValue& Amount) : AlkValue(Amount) { } inline const MyMoneyMoney& MyMoneyMoney::operator=(const AlkValue & val) { AlkValue::operator=(val); return *this; } //////////////////////////////////////////////////////////////////////////////// // Name: operator= // Purpose: Assignment operator - modifies object from input NULL terminated // string // Returns: Const reference to the object // Throws: Nothing. // Arguments: pszAmount - NULL terminated string that contains amount // //////////////////////////////////////////////////////////////////////////////// inline const MyMoneyMoney& MyMoneyMoney::operator=(const QString & pszAmount) { AlkValue::operator=(pszAmount); return *this; } //////////////////////////////////////////////////////////////////////////////// // Name: operator== // Purpose: Compare equal operator - compares object with input MyMoneyMoney object // Returns: true if equal, otherwise false // Throws: Nothing. // Arguments: Amount - MyMoneyMoney object to be compared with // //////////////////////////////////////////////////////////////////////////////// inline bool MyMoneyMoney::operator==(const MyMoneyMoney& Amount) const { return AlkValue::operator==(Amount); } //////////////////////////////////////////////////////////////////////////////// // Name: operator!= // Purpose: Compare not equal operator - compares object with input MyMoneyMoney object // Returns: true if not equal, otherwise false // Throws: Nothing. // Arguments: Amount - MyMoneyMoney object to be compared with // //////////////////////////////////////////////////////////////////////////////// inline bool MyMoneyMoney::operator!=(const MyMoneyMoney& Amount) const { return AlkValue::operator!=(Amount); } //////////////////////////////////////////////////////////////////////////////// // Name: operator< // Purpose: Compare less than operator - compares object with input MyMoneyMoney object // Returns: true if object less than input amount // Throws: Nothing. // Arguments: Amount - MyMoneyMoney object to be compared with // //////////////////////////////////////////////////////////////////////////////// inline bool MyMoneyMoney::operator<(const MyMoneyMoney& Amount) const { return AlkValue::operator<(Amount); } //////////////////////////////////////////////////////////////////////////////// // Name: operator> // Purpose: Compare greater than operator - compares object with input MyMoneyMoney // object // Returns: true if object greater than input amount // Throws: Nothing. // Arguments: Amount - MyMoneyMoney object to be compared with // //////////////////////////////////////////////////////////////////////////////// inline bool MyMoneyMoney::operator>(const MyMoneyMoney& Amount) const { return AlkValue::operator>(Amount); } //////////////////////////////////////////////////////////////////////////////// // Name: operator<= // Purpose: Compare less than equal to operator - compares object with input // MyMoneyMoney object // Returns: true if object less than or equal to input amount // Throws: Nothing. // Arguments: Amount - MyMoneyMoney object to be compared with // //////////////////////////////////////////////////////////////////////////////// inline bool MyMoneyMoney::operator<=(const MyMoneyMoney& Amount) const { return AlkValue::operator<=(Amount); } //////////////////////////////////////////////////////////////////////////////// // Name: operator>= // Purpose: Compare greater than equal to operator - compares object with input // MyMoneyMoney object // Returns: true if object greater than or equal to input amount // Throws: Nothing. // Arguments: Amount - MyMoneyMoney object to be compared with // //////////////////////////////////////////////////////////////////////////////// inline bool MyMoneyMoney::operator>=(const MyMoneyMoney& Amount) const { return AlkValue::operator>=(Amount); } //////////////////////////////////////////////////////////////////////////////// // Name: operator== // Purpose: Compare equal operator - compares object with input amount in a // NULL terminated string // Returns: true if equal, otherwise false // Throws: Nothing. // Arguments: pszAmount - NULL terminated string that contains amount // //////////////////////////////////////////////////////////////////////////////// inline bool MyMoneyMoney::operator==(const QString& pszAmount) const { return *this == MyMoneyMoney(pszAmount); } //////////////////////////////////////////////////////////////////////////////// // Name: operator!= // Purpose: Compare not equal operator - compares object with input amount in // a NULL terminated string // Returns: true if not equal, otherwise false // Throws: Nothing. // Arguments: pszAmount - NULL terminated string that contains amount // //////////////////////////////////////////////////////////////////////////////// inline bool MyMoneyMoney::operator!=(const QString& pszAmount) const { return ! operator==(pszAmount) ; } //////////////////////////////////////////////////////////////////////////////// // Name: operator- // Purpose: Unary operator - returns the negative value from the object // Returns: The current object // Throws: Nothing. // Arguments: None // //////////////////////////////////////////////////////////////////////////////// inline const MyMoneyMoney MyMoneyMoney::operator-() const { return static_cast(AlkValue::operator-()); } //////////////////////////////////////////////////////////////////////////////// // Name: operator* // Purpose: Multiplication operator - multiplies the object with factor // Returns: The current object // Throws: Nothing. // Arguments: AmountInPence - long object to be multiplied // //////////////////////////////////////////////////////////////////////////////// inline const MyMoneyMoney MyMoneyMoney::operator*(int factor) const { return static_cast(AlkValue::operator*(factor)); } /** * Make it possible to hold @ref MyMoneyMoney objects * inside @ref QVariant objects. */ Q_DECLARE_METATYPE(MyMoneyMoney) #endif diff --git a/kmymoney/mymoney/mymoneysplit.cpp b/kmymoney/mymoney/mymoneysplit.cpp index b3667a393..982597e57 100644 --- a/kmymoney/mymoney/mymoneysplit.cpp +++ b/kmymoney/mymoney/mymoneysplit.cpp @@ -1,512 +1,513 @@ /*************************************************************************** mymoneysplit.cpp - description ------------------- begin : Sun Apr 28 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 "mymoneysplit.h" #include "mymoneysplit_p.h" // ---------------------------------------------------------------------------- // QT Includes #include #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes #include "mymoneyutils.h" #include "mymoneytransaction.h" +#include "mymoneyexception.h" const char MyMoneySplit::ActionCheck[] = "Check"; const char MyMoneySplit::ActionDeposit[] = "Deposit"; const char MyMoneySplit::ActionTransfer[] = "Transfer"; const char MyMoneySplit::ActionWithdrawal[] = "Withdrawal"; const char MyMoneySplit::ActionATM[] = "ATM"; const char MyMoneySplit::ActionAmortization[] = "Amortization"; const char MyMoneySplit::ActionInterest[] = "Interest"; const char MyMoneySplit::ActionBuyShares[] = "Buy"; const char MyMoneySplit::ActionDividend[] = "Dividend"; const char MyMoneySplit::ActionReinvestDividend[] = "Reinvest"; const char MyMoneySplit::ActionYield[] = "Yield"; const char MyMoneySplit::ActionAddShares[] = "Add"; const char MyMoneySplit::ActionSplitShares[] = "Split"; const char MyMoneySplit::ActionInterestIncome[] = "IntIncome"; MyMoneySplit::MyMoneySplit() : MyMoneyObject(*new MyMoneySplitPrivate) { Q_D(MyMoneySplit); d->m_reconcileFlag = eMyMoney::Split::State::NotReconciled; } MyMoneySplit::MyMoneySplit(const QDomElement& node) : MyMoneyObject(*new MyMoneySplitPrivate, node, false), MyMoneyKeyValueContainer(node.elementsByTagName(MyMoneySplitPrivate::getElName(Split::Element::KeyValuePairs)).item(0).toElement()) { Q_D(MyMoneySplit); if (d->getElName(Split::Element::Split) != node.tagName()) throw MYMONEYEXCEPTION("Node was not SPLIT"); clearId(); d->m_payee = MyMoneyUtils::QStringEmpty(node.attribute(d->getAttrName(Split::Attribute::Payee))); QDomNodeList nodeList = node.elementsByTagName(d->getElName(Split::Element::Tag)); for (int i = 0; i < nodeList.count(); i++) d->m_tagList << MyMoneyUtils::QStringEmpty(nodeList.item(i).toElement().attribute(d->getAttrName(Split::Attribute::ID))); d->m_reconcileDate = MyMoneyUtils::stringToDate(MyMoneyUtils::QStringEmpty(node.attribute(d->getAttrName(Split::Attribute::ReconcileDate)))); d->m_action = MyMoneyUtils::QStringEmpty(node.attribute(d->getAttrName(Split::Attribute::Action))); d->m_reconcileFlag = static_cast(node.attribute(d->getAttrName(Split::Attribute::ReconcileFlag)).toInt()); d->m_memo = MyMoneyUtils::QStringEmpty(node.attribute(d->getAttrName(Split::Attribute::Memo))); d->m_value = MyMoneyMoney(MyMoneyUtils::QStringEmpty(node.attribute(d->getAttrName(Split::Attribute::Value)))); d->m_shares = MyMoneyMoney(MyMoneyUtils::QStringEmpty(node.attribute(d->getAttrName(Split::Attribute::Shares)))); d->m_price = MyMoneyMoney(MyMoneyUtils::QStringEmpty(node.attribute(d->getAttrName(Split::Attribute::Price)))); d->m_account = MyMoneyUtils::QStringEmpty(node.attribute(d->getAttrName(Split::Attribute::Account))); d->m_costCenter = MyMoneyUtils::QStringEmpty(node.attribute(d->getAttrName(Split::Attribute::CostCenter))); d->m_number = MyMoneyUtils::QStringEmpty(node.attribute(d->getAttrName(Split::Attribute::Number))); d->m_bankID = MyMoneyUtils::QStringEmpty(node.attribute(d->getAttrName(Split::Attribute::BankID))); } MyMoneySplit::MyMoneySplit(const MyMoneySplit& other) : MyMoneyObject(*new MyMoneySplitPrivate(*other.d_func()), other.id()), MyMoneyKeyValueContainer(other) { } MyMoneySplit::MyMoneySplit(const QString& id, const MyMoneySplit& other) : MyMoneyObject(*new MyMoneySplitPrivate(*other.d_func()), id), MyMoneyKeyValueContainer(other) { } MyMoneySplit::~MyMoneySplit() { } bool MyMoneySplit::operator == (const MyMoneySplit& right) const { Q_D(const MyMoneySplit); auto d2 = static_cast(right.d_func()); return MyMoneyObject::operator==(right) && MyMoneyKeyValueContainer::operator==(right) && d->m_account == d2->m_account && d->m_costCenter == d2->m_costCenter && d->m_payee == d2->m_payee && d->m_tagList == d2->m_tagList && d->m_memo == d2->m_memo && d->m_action == d2->m_action && d->m_reconcileDate == d2->m_reconcileDate && d->m_reconcileFlag == d2->m_reconcileFlag && ((d->m_number.length() == 0 && d2->m_number.length() == 0) || d->m_number == d2->m_number) && d->m_shares == d2->m_shares && d->m_value == d2->m_value && d->m_price == d2->m_price && d->m_transactionId == d2->m_transactionId; } MyMoneySplit MyMoneySplit::operator-() const { MyMoneySplit rc(*this); rc.d_func()->m_shares = -rc.d_func()->m_shares; rc.d_func()->m_value = -rc.d_func()->m_value; return rc; } QString MyMoneySplit::accountId() const { Q_D(const MyMoneySplit); return d->m_account; } void MyMoneySplit::setAccountId(const QString& account) { Q_D(MyMoneySplit); d->m_account = account; } QString MyMoneySplit::costCenterId() const { Q_D(const MyMoneySplit); return d->m_costCenter; } void MyMoneySplit::setCostCenterId(const QString& costCenter) { Q_D(MyMoneySplit); d->m_costCenter = costCenter; } QString MyMoneySplit::memo() const { Q_D(const MyMoneySplit); return d->m_memo; } void MyMoneySplit::setMemo(const QString& memo) { Q_D(MyMoneySplit); d->m_memo = memo; } eMyMoney::Split::State MyMoneySplit::reconcileFlag() const { Q_D(const MyMoneySplit); return d->m_reconcileFlag; } QDate MyMoneySplit::reconcileDate() const { Q_D(const MyMoneySplit); return d->m_reconcileDate; } void MyMoneySplit::setReconcileDate(const QDate& date) { Q_D(MyMoneySplit); d->m_reconcileDate = date; } void MyMoneySplit::setReconcileFlag(const eMyMoney::Split::State flag) { Q_D(MyMoneySplit); d->m_reconcileFlag = flag; } MyMoneyMoney MyMoneySplit::shares() const { Q_D(const MyMoneySplit); return d->m_shares; } void MyMoneySplit::setShares(const MyMoneyMoney& shares) { Q_D(MyMoneySplit); d->m_shares = shares; } QString MyMoneySplit::value(const QString& key) const { return MyMoneyKeyValueContainer::value(key); } void MyMoneySplit::setValue(const QString& key, const QString& value) { MyMoneyKeyValueContainer::setValue(key, value); } void MyMoneySplit::setValue(const MyMoneyMoney& value) { Q_D(MyMoneySplit); d->m_value = value; } void MyMoneySplit::setValue(const MyMoneyMoney& value, const QString& transactionCurrencyId, const QString& splitCurrencyId) { if (transactionCurrencyId == splitCurrencyId) setValue(value); else setShares(value); } QString MyMoneySplit::payeeId() const { Q_D(const MyMoneySplit); return d->m_payee; } void MyMoneySplit::setPayeeId(const QString& payee) { Q_D(MyMoneySplit); d->m_payee = payee; } QList MyMoneySplit::tagIdList() const { Q_D(const MyMoneySplit); return d->m_tagList; } void MyMoneySplit::setTagIdList(const QList& tagList) { Q_D(MyMoneySplit); d->m_tagList = tagList; } void MyMoneySplit::setAction(eMyMoney::Split::InvestmentTransactionType type) { switch (type) { case eMyMoney::Split::InvestmentTransactionType::BuyShares: case eMyMoney::Split::InvestmentTransactionType::SellShares: setAction(ActionBuyShares); break; case eMyMoney::Split::InvestmentTransactionType::Dividend: setAction(ActionDividend); break; case eMyMoney::Split::InvestmentTransactionType::Yield: setAction(ActionYield); break; case eMyMoney::Split::InvestmentTransactionType::ReinvestDividend: setAction(ActionReinvestDividend); break; case eMyMoney::Split::InvestmentTransactionType::AddShares: case eMyMoney::Split::InvestmentTransactionType::RemoveShares: setAction(ActionAddShares); break; case eMyMoney::Split::InvestmentTransactionType::SplitShares: setAction(ActionSplitShares); break; case eMyMoney::Split::InvestmentTransactionType::InterestIncome: setAction(ActionInterestIncome); break; case eMyMoney::Split::InvestmentTransactionType::UnknownTransactionType: break; } } QString MyMoneySplit::action() const { Q_D(const MyMoneySplit); return d->m_action; } void MyMoneySplit::setAction(const QString& action) { Q_D(MyMoneySplit); d->m_action = action; } bool MyMoneySplit::isAmortizationSplit() const { Q_D(const MyMoneySplit); return d->m_action == ActionAmortization; } bool MyMoneySplit::isInterestSplit() const { Q_D(const MyMoneySplit); return d->m_action == ActionInterest; } QString MyMoneySplit::number() const { Q_D(const MyMoneySplit); return d->m_number; } void MyMoneySplit::setNumber(const QString& number) { Q_D(MyMoneySplit); d->m_number = number; } bool MyMoneySplit::isAutoCalc() const { Q_D(const MyMoneySplit); return (d->m_shares == MyMoneyMoney::autoCalc) || (d->m_value == MyMoneyMoney::autoCalc); } QString MyMoneySplit::bankID() const { Q_D(const MyMoneySplit); return d->m_bankID; } void MyMoneySplit::setBankID(const QString& bankID) { Q_D(MyMoneySplit); d->m_bankID = bankID; } QString MyMoneySplit::transactionId() const { Q_D(const MyMoneySplit); return d->m_transactionId; } void MyMoneySplit::setTransactionId(const QString& id) { Q_D(MyMoneySplit); d->m_transactionId = id; } MyMoneyMoney MyMoneySplit::value() const { Q_D(const MyMoneySplit); return d->m_value; } MyMoneyMoney MyMoneySplit::value(const QString& transactionCurrencyId, const QString& splitCurrencyId) const { Q_D(const MyMoneySplit); return (transactionCurrencyId == splitCurrencyId) ? d->m_value : d->m_shares; } MyMoneyMoney MyMoneySplit::actualPrice() const { Q_D(const MyMoneySplit); return d->m_price; } void MyMoneySplit::setPrice(const MyMoneyMoney& price) { Q_D(MyMoneySplit); d->m_price = price; } MyMoneyMoney MyMoneySplit::price() const { Q_D(const MyMoneySplit); if (!d->m_price.isZero()) return d->m_price; if (!d->m_value.isZero() && !d->m_shares.isZero()) return d->m_value / d->m_shares; return MyMoneyMoney::ONE; } void MyMoneySplit::writeXML(QDomDocument& document, QDomElement& parent) const { Q_D(const MyMoneySplit); auto el = document.createElement(d->getElName(Split::Element::Split)); d->writeBaseXML(document, el); el.setAttribute(d->getAttrName(Split::Attribute::Payee), d->m_payee); //el.setAttribute(getAttrName(Split::Attribute::Tag), m_tag); el.setAttribute(d->getAttrName(Split::Attribute::ReconcileDate), MyMoneyUtils::dateToString(d->m_reconcileDate)); el.setAttribute(d->getAttrName(Split::Attribute::Action), d->m_action); el.setAttribute(d->getAttrName(Split::Attribute::ReconcileFlag), (int)d->m_reconcileFlag); el.setAttribute(d->getAttrName(Split::Attribute::Value), d->m_value.toString()); el.setAttribute(d->getAttrName(Split::Attribute::Shares), d->m_shares.toString()); if (!d->m_price.isZero()) el.setAttribute(d->getAttrName(Split::Attribute::Price), d->m_price.toString()); el.setAttribute(d->getAttrName(Split::Attribute::Memo), d->m_memo); // No need to write the split id as it will be re-assigned when the file is read // el.setAttribute(getAttrName(Split::Attribute::ID), split.id()); el.setAttribute(d->getAttrName(Split::Attribute::Account), d->m_account); el.setAttribute(d->getAttrName(Split::Attribute::Number), d->m_number); el.setAttribute(d->getAttrName(Split::Attribute::BankID), d->m_bankID); if(!d->m_costCenter.isEmpty()) el.setAttribute(d->getAttrName(Split::Attribute::CostCenter), d->m_costCenter); for (int i = 0; i < d->m_tagList.count(); i++) { QDomElement sel = document.createElement(d->getElName(Split::Element::Tag)); sel.setAttribute(d->getAttrName(Split::Attribute::ID), d->m_tagList[i]); el.appendChild(sel); } MyMoneyKeyValueContainer::writeXML(document, el); parent.appendChild(el); } bool MyMoneySplit::hasReferenceTo(const QString& id) const { Q_D(const MyMoneySplit); auto rc = false; if (isMatched()) { rc = matchedTransaction().hasReferenceTo(id); } for (int i = 0; i < d->m_tagList.size(); i++) if (id == d->m_tagList[i]) return true; return rc || (id == d->m_account) || (id == d->m_payee) || (id == d->m_costCenter); } bool MyMoneySplit::isMatched() const { Q_D(const MyMoneySplit); return !(value(d->getAttrName(Split::Attribute::KMMatchedTx)).isEmpty()); } void MyMoneySplit::addMatch(const MyMoneyTransaction& _t) { Q_D(MyMoneySplit); // now we allow matching of two manual transactions if (!isMatched()) { MyMoneyTransaction t(_t); t.clearId(); QDomDocument doc(d->getElName(Split::Element::Match)); QDomElement el = doc.createElement(d->getElName(Split::Element::Container)); doc.appendChild(el); t.writeXML(doc, el); QString xml = doc.toString(); xml.replace('<', "<"); setValue(d->getAttrName(Split::Attribute::KMMatchedTx), xml); } } void MyMoneySplit::removeMatch() { Q_D(MyMoneySplit); deletePair(d->getAttrName(Split::Attribute::KMMatchedTx)); } MyMoneyTransaction MyMoneySplit::matchedTransaction() const { Q_D(const MyMoneySplit); auto xml = value(d->getAttrName(Split::Attribute::KMMatchedTx)); if (!xml.isEmpty()) { xml.replace("<", "<"); QDomDocument doc; QDomElement node; doc.setContent(xml); node = doc.documentElement().firstChild().toElement(); MyMoneyTransaction t(node, false); return t; } return MyMoneyTransaction(); } bool MyMoneySplit::replaceId(const QString& newId, const QString& oldId) { auto changed = false; Q_D(MyMoneySplit); if (d->m_payee == oldId) { d->m_payee = newId; changed = true; } else if (d->m_account == oldId) { d->m_account = newId; changed = true; } else if (d->m_costCenter == oldId) { d->m_costCenter = newId; changed = true; } if (isMatched()) { MyMoneyTransaction t = matchedTransaction(); if (t.replaceId(newId, oldId)) { removeMatch(); addMatch(t); changed = true; } } return changed; } diff --git a/kmymoney/mymoney/mymoneytransaction.cpp b/kmymoney/mymoney/mymoneytransaction.cpp index b7e0e338a..c8a970408 100644 --- a/kmymoney/mymoney/mymoneytransaction.cpp +++ b/kmymoney/mymoney/mymoneytransaction.cpp @@ -1,505 +1,506 @@ /*************************************************************************** mymoneytransaction.cpp ------------------- copyright : (C) 2000 by Michael Edwardes 2002 by Thomas Baumgart ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "mymoneytransaction.h" #include "mymoneytransaction_p.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "mymoneystoragenames.h" #include "mymoneyutils.h" #include "mymoneymoney.h" +#include "mymoneyexception.h" using namespace MyMoneyStorageNodes; MyMoneyTransaction::MyMoneyTransaction() : MyMoneyObject(*new MyMoneyTransactionPrivate) { Q_D(MyMoneyTransaction); d->m_nextSplitID = 1; d->m_entryDate = QDate(); d->m_postDate = QDate(); } MyMoneyTransaction::MyMoneyTransaction(const QDomElement& node, const bool forceId) : MyMoneyObject(*new MyMoneyTransactionPrivate, node, forceId) { Q_D(MyMoneyTransaction); if (nodeNames[nnTransaction] != node.tagName()) throw MYMONEYEXCEPTION("Node was not TRANSACTION"); d->m_nextSplitID = 1; d->m_postDate = MyMoneyUtils::stringToDate(node.attribute(d->getAttrName(Transaction::Attribute::PostDate))); d->m_entryDate = MyMoneyUtils::stringToDate(node.attribute(d->getAttrName(Transaction::Attribute::EntryDate))); d->m_bankID = MyMoneyUtils::QStringEmpty(node.attribute(d->getAttrName(Transaction::Attribute::BankID))); d->m_memo = MyMoneyUtils::QStringEmpty(node.attribute(d->getAttrName(Transaction::Attribute::Memo))); d->m_commodity = MyMoneyUtils::QStringEmpty(node.attribute(d->getAttrName(Transaction::Attribute::Commodity))); QDomNode child = node.firstChild(); while (!child.isNull() && child.isElement()) { QDomElement c = child.toElement(); if (c.tagName() == d->getElName(Transaction::Element::Splits)) { // Process any split information found inside the transaction entry. QDomNodeList nodeList = c.elementsByTagName(d->getElName(Transaction::Element::Split)); for (int i = 0; i < nodeList.count(); ++i) { MyMoneySplit s(nodeList.item(i).toElement()); if (!d->m_bankID.isEmpty()) s.setBankID(d->m_bankID); if (!s.accountId().isEmpty()) addSplit(s); else qDebug("Dropped split because it did not have an account id"); } } else if (c.tagName() == nodeNames[nnKeyValuePairs]) { MyMoneyKeyValueContainer kvp(c); setPairs(kvp.pairs()); } child = child.nextSibling(); } d->m_bankID.clear(); } MyMoneyTransaction::MyMoneyTransaction(const MyMoneyTransaction& other) : MyMoneyObject(*new MyMoneyTransactionPrivate(*other.d_func()), other.id()), MyMoneyKeyValueContainer(other) { } MyMoneyTransaction::MyMoneyTransaction(const QString& id, const MyMoneyTransaction& other) : MyMoneyObject(*new MyMoneyTransactionPrivate(*other.d_func()), id), MyMoneyKeyValueContainer(other) { Q_D(MyMoneyTransaction); if (d->m_entryDate == QDate()) d->m_entryDate = QDate::currentDate(); foreach (auto split, d->m_splits) split.setTransactionId(id); } MyMoneyTransaction::~MyMoneyTransaction() { } QDate MyMoneyTransaction::entryDate() const { Q_D(const MyMoneyTransaction); return d->m_entryDate; } void MyMoneyTransaction::setEntryDate(const QDate& date) { Q_D(MyMoneyTransaction); d->m_entryDate = date; } QDate MyMoneyTransaction::postDate() const { Q_D(const MyMoneyTransaction); return d->m_postDate; } void MyMoneyTransaction::setPostDate(const QDate& date) { Q_D(MyMoneyTransaction); d->m_postDate = date; } QString MyMoneyTransaction::memo() const { Q_D(const MyMoneyTransaction); return d->m_memo; } void MyMoneyTransaction::setMemo(const QString& memo) { Q_D(MyMoneyTransaction); d->m_memo = memo; } QList MyMoneyTransaction::splits() const { Q_D(const MyMoneyTransaction); return d->m_splits; } QList& MyMoneyTransaction::splits() { Q_D(MyMoneyTransaction); return d->m_splits; } uint MyMoneyTransaction::splitCount() const { Q_D(const MyMoneyTransaction); return d->m_splits.count(); } QString MyMoneyTransaction::commodity() const { Q_D(const MyMoneyTransaction); return d->m_commodity; } void MyMoneyTransaction::setCommodity(const QString& commodityId) { Q_D(MyMoneyTransaction); d->m_commodity = commodityId; } QString MyMoneyTransaction::bankID() const { Q_D(const MyMoneyTransaction); return d->m_bankID; } void MyMoneyTransaction::setBankID(const QString& bankID) { Q_D(MyMoneyTransaction); d->m_bankID = bankID; } bool MyMoneyTransaction::operator == (const MyMoneyTransaction& right) const { Q_D(const MyMoneyTransaction); auto d2 = static_cast(right.d_func()); return (MyMoneyObject::operator==(right) && MyMoneyKeyValueContainer::operator==(right) && (d->m_commodity == d2->m_commodity) && ((d->m_memo.length() == 0 && d2->m_memo.length() == 0) || (d->m_memo == d2->m_memo)) && (d->m_splits == d2->m_splits) && (d->m_entryDate == d2->m_entryDate) && (d->m_postDate == d2->m_postDate)); } bool MyMoneyTransaction::operator != (const MyMoneyTransaction& r) const { return !(*this == r); } bool MyMoneyTransaction::operator< (const MyMoneyTransaction& r) const { return postDate() < r.postDate(); } bool MyMoneyTransaction::operator<= (const MyMoneyTransaction& r) const { return postDate() <= r.postDate(); } bool MyMoneyTransaction::operator> (const MyMoneyTransaction& r) const { return postDate() > r.postDate(); } bool MyMoneyTransaction::accountReferenced(const QString& id) const { Q_D(const MyMoneyTransaction); foreach (const auto split, d->m_splits) { if (split.accountId() == id) return true; } return false; } void MyMoneyTransaction::addSplit(MyMoneySplit &split) { if (!split.id().isEmpty()) throw MYMONEYEXCEPTION("Cannot add split with assigned id (" + split.id() + ')'); if (split.accountId().isEmpty()) throw MYMONEYEXCEPTION("Cannot add split that does not contain an account reference"); Q_D(MyMoneyTransaction); MyMoneySplit newSplit(d->nextSplitID(), split); split = newSplit; split.setTransactionId(id()); d->m_splits.append(split); } void MyMoneyTransaction::modifySplit(const MyMoneySplit& split) { // This is the other version which allows having more splits referencing // the same account. if (split.accountId().isEmpty()) throw MYMONEYEXCEPTION("Cannot modify split that does not contain an account reference"); Q_D(MyMoneyTransaction); for (auto& it_split : d->m_splits) { if (split.id() == it_split.id()) { it_split = split; return; } } throw MYMONEYEXCEPTION(QString("Invalid split id '%1'").arg(split.id())); } void MyMoneyTransaction::removeSplit(const MyMoneySplit& split) { Q_D(MyMoneyTransaction); for (auto end = d->m_splits.size(), i = 0; i < end; ++i) { if (split.id() == d->m_splits.at(i).id()) { d->m_splits.removeAt(i); return; } } throw MYMONEYEXCEPTION(QString("Invalid split id '%1'").arg(split.id())); } void MyMoneyTransaction::removeSplits() { Q_D(MyMoneyTransaction); d->m_splits.clear(); d->m_nextSplitID = 1; } MyMoneySplit MyMoneyTransaction::splitByPayee(const QString& payeeId) const { Q_D(const MyMoneyTransaction); foreach (const auto split, d->m_splits) { if (split.payeeId() == payeeId) return split; } throw MYMONEYEXCEPTION(QString("Split not found for payee '%1'").arg(QString(payeeId))); } MyMoneySplit MyMoneyTransaction::splitByAccount(const QString& accountId, const bool match) const { Q_D(const MyMoneyTransaction); foreach (const auto split, d->m_splits) { if ((match == true && split.accountId() == accountId) || (match == false && split.accountId() != accountId)) return split; } throw MYMONEYEXCEPTION(QString("Split not found for account %1%2").arg(match ? "" : "!").arg(QString(accountId))); } MyMoneySplit MyMoneyTransaction::splitByAccount(const QStringList& accountIds, const bool match) const { Q_D(const MyMoneyTransaction); foreach (const auto split, d->m_splits) { if ((match == true && accountIds.contains(split.accountId())) || (match == false && !accountIds.contains(split.accountId()))) return split; } throw MYMONEYEXCEPTION(QString("Split not found for account %1%1...%2").arg(match ? "" : "!").arg(accountIds.front(), accountIds.back())); } MyMoneySplit MyMoneyTransaction::splitById(const QString& splitId) const { Q_D(const MyMoneyTransaction); foreach (const auto split, d->m_splits) { if (split.id() == splitId) return split; } throw MYMONEYEXCEPTION(QString("Split not found for id '%1'").arg(QString(splitId))); } QString MyMoneyTransaction::firstSplitID() { QString id; id = 'S' + id.setNum(1).rightJustified(MyMoneyTransactionPrivate::SPLIT_ID_SIZE, '0'); return id; } MyMoneyMoney MyMoneyTransaction::splitSum() const { MyMoneyMoney result; Q_D(const MyMoneyTransaction); foreach (const auto split, d->m_splits) result += split.value(); return result; } bool MyMoneyTransaction::isLoanPayment() const { try { Q_D(const MyMoneyTransaction); foreach (const auto split, d->m_splits) { if (split.isAmortizationSplit()) return true; } } catch (const MyMoneyException &) { } return false; } MyMoneySplit MyMoneyTransaction::amortizationSplit() const { static MyMoneySplit nullSplit; Q_D(const MyMoneyTransaction); foreach (const auto split, d->m_splits) { if (split.isAmortizationSplit() && split.isAutoCalc()) return split; } return nullSplit; } MyMoneySplit MyMoneyTransaction::interestSplit() const { static MyMoneySplit nullSplit; Q_D(const MyMoneyTransaction); foreach (const auto split, d->m_splits) { if (split.isInterestSplit() && split.isAutoCalc()) return split; } return nullSplit; } unsigned long MyMoneyTransaction::hash(const QString& txt, unsigned long h) { unsigned long g; for (int i = 0; i < txt.length(); ++i) { unsigned short uc = txt[i].unicode(); for (unsigned j = 0; j < 2; ++j) { unsigned char c = uc & 0xff; // if either the cell or the row of the Unicode char is 0, stop processing if (!c) break; h = (h << 4) + c; if ((g = (h & 0xf0000000))) { h = h ^(g >> 24); h = h ^ g; } uc >>= 8; } } return h; } bool MyMoneyTransaction::isStockSplit() const { Q_D(const MyMoneyTransaction); return (d->m_splits.count() == 1 && d->m_splits[0].action() == MyMoneySplit::ActionSplitShares); } bool MyMoneyTransaction::isImported() const { return value("Imported").toLower() == QString("true"); } void MyMoneyTransaction::setImported(bool state) { if (state) setValue("Imported", "true"); else deletePair("Imported"); } void MyMoneyTransaction::writeXML(QDomDocument& document, QDomElement& parent) const { Q_D(const MyMoneyTransaction); auto el = document.createElement(nodeNames[nnTransaction]); d->writeBaseXML(document, el); el.setAttribute(d->getAttrName(Transaction::Attribute::PostDate), MyMoneyUtils::dateToString(d->m_postDate)); el.setAttribute(d->getAttrName(Transaction::Attribute::Memo), d->m_memo); el.setAttribute(d->getAttrName(Transaction::Attribute::EntryDate), MyMoneyUtils::dateToString(d->m_entryDate)); el.setAttribute(d->getAttrName(Transaction::Attribute::Commodity), d->m_commodity); auto splits = document.createElement(d->getElName(Transaction::Element::Splits)); foreach (const auto split, d->m_splits) split.writeXML(document, splits); el.appendChild(splits); MyMoneyKeyValueContainer::writeXML(document, el); parent.appendChild(el); } bool MyMoneyTransaction::hasReferenceTo(const QString& id) const { Q_D(const MyMoneyTransaction); if (id == d->m_commodity) return true; foreach (const auto split, d->m_splits) { if (split.hasReferenceTo(id)) return true; } return false; } bool MyMoneyTransaction::hasAutoCalcSplit() const { Q_D(const MyMoneyTransaction); foreach (const auto split, d->m_splits) if (split.isAutoCalc()) return true; return false; } QString MyMoneyTransaction::accountSignature(bool includeSplitCount) const { Q_D(const MyMoneyTransaction); QMap accountList; foreach (const auto split, d->m_splits) accountList[split.accountId()] += 1; QMap::const_iterator it_a; QString rc; for (it_a = accountList.constBegin(); it_a != accountList.constEnd(); ++it_a) { if (it_a != accountList.constBegin()) rc += '-'; rc += it_a.key(); if (includeSplitCount) rc += QString("*%1").arg(*it_a); } return rc; } QString MyMoneyTransaction::uniqueSortKey() const { Q_D(const MyMoneyTransaction); QString year, month, day, key; const auto postdate = postDate(); year = year.setNum(postdate.year()).rightJustified(MyMoneyTransactionPrivate::YEAR_SIZE, '0'); month = month.setNum(postdate.month()).rightJustified(MyMoneyTransactionPrivate::MONTH_SIZE, '0'); day = day.setNum(postdate.day()).rightJustified(MyMoneyTransactionPrivate::DAY_SIZE, '0'); key = QString::fromLatin1("%1-%2-%3-%4").arg(year, month, day, d->m_id); return key; } bool MyMoneyTransaction::replaceId(const QString& newId, const QString& oldId) { auto changed = false; Q_D(MyMoneyTransaction); for (MyMoneySplit& split : d->m_splits) changed |= split.replaceId(newId, oldId); return changed; } diff --git a/kmymoney/mymoney/storage/mymoneystorageanon.cpp b/kmymoney/mymoney/storage/mymoneystorageanon.cpp index d118396b7..3422285f3 100644 --- a/kmymoney/mymoney/storage/mymoneystorageanon.cpp +++ b/kmymoney/mymoney/storage/mymoneystorageanon.cpp @@ -1,325 +1,326 @@ /*************************************************************************** mymoneystorageanon.cpp ------------------- begin : Thu Oct 24 2002 copyright : (C) 2000-2002 by Michael Edwardes email : mte@users.sourceforge.net Javier Campos Morales Felix Rodriguez John C Thomas Baumgart Kevin Tambascio 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. * * * ***************************************************************************/ #include "mymoneystorageanon.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes #include "imymoneyserialize.h" #include "mymoneyreport.h" #include "mymoneyschedule.h" #include "mymoneysplit.h" #include "mymoneybudget.h" #include "mymoneytransaction.h" #include "mymoneyinstitution.h" #include "mymoneyaccount.h" #include "mymoneysecurity.h" #include "mymoneypayee.h" #include "mymoneytag.h" #include "mymoneykeyvaluecontainer.h" +#include "mymoneyexception.h" QStringList MyMoneyStorageANON::zKvpNoModify = QString("kmm-baseCurrency,OpeningBalanceAccount,PreferredAccount,Tax,fixed-interest,interest-calculation,payee,schedule,term,kmm-online-source,kmm-brokerage-account,lastStatementDate,kmm-sort-reconcile,kmm-sort-std,kmm-iconpos,mm-closed,payee,schedule,term,lastImportedTransactionDate,VatAccount,VatRate,kmm-matched-tx,Imported,priceMode").split(','); QStringList MyMoneyStorageANON::zKvpXNumber = QString("final-payment,loan-amount,periodic-payment,lastStatementBalance").split(','); MyMoneyStorageANON::MyMoneyStorageANON() : MyMoneyStorageXML() { // Choose a quasi-random 0.0-100.0 factor which will be applied to all splits this time // around. int msec; do { msec = QTime::currentTime().msec(); } while (msec == 0); m_factor = MyMoneyMoney(msec, 10).reduce(); } MyMoneyStorageANON::~MyMoneyStorageANON() { } void MyMoneyStorageANON::readFile(QIODevice* , IMyMoneySerialize*) { throw MYMONEYEXCEPTION("Cannot read a file through MyMoneyStorageANON!!"); } void MyMoneyStorageANON::writeUserInformation(QDomElement& userInfo) { MyMoneyPayee user = m_storage->user(); userInfo.setAttribute(QString("name"), hideString(user.name())); userInfo.setAttribute(QString("email"), hideString(user.email())); QDomElement address = m_doc->createElement("ADDRESS"); address.setAttribute(QString("street"), hideString(user.address())); address.setAttribute(QString("city"), hideString(user.city())); address.setAttribute(QString("county"), hideString(user.state())); address.setAttribute(QString("zipcode"), hideString(user.postcode())); address.setAttribute(QString("telephone"), hideString(user.telephone())); userInfo.appendChild(address); } void MyMoneyStorageANON::writeInstitution(QDomElement& institution, const MyMoneyInstitution& _i) { MyMoneyInstitution i(_i); // mangle fields i.setName(i.id()); i.setManager(hideString(i.manager())); i.setSortcode(hideString(i.sortcode())); i.setStreet(hideString(i.street())); i.setCity(hideString(i.city())); i.setPostcode(hideString(i.postcode())); i.setTelephone(hideString(i.telephone())); MyMoneyStorageXML::writeInstitution(institution, i); } void MyMoneyStorageANON::writePayee(QDomElement& payee, const MyMoneyPayee& _p) { MyMoneyPayee p(_p); p.setName(p.id()); p.setReference(hideString(p.reference())); p.setAddress(hideString(p.address())); p.setCity(hideString(p.city())); p.setPostcode(hideString(p.postcode())); p.setState(hideString(p.state())); p.setTelephone(hideString(p.telephone())); p.setNotes(hideString(p.notes())); bool ignoreCase; QStringList keys; MyMoneyPayee::payeeMatchType matchType = p.matchData(ignoreCase, keys); QRegExp exp("[A-Za-z]"); p.setMatchData(matchType, ignoreCase, keys.join(";").replace(exp, "x").split(';')); // Data from plugins cannot be estranged, yet. p.resetPayeeIdentifiers(); MyMoneyStorageXML::writePayee(payee, p); } void MyMoneyStorageANON::writeTag(QDomElement& tag, const MyMoneyTag& _ta) { MyMoneyTag ta(_ta); ta.setName(ta.id()); ta.setNotes(hideString(ta.notes())); MyMoneyStorageXML::writeTag(tag, ta); } void MyMoneyStorageANON::writeAccount(QDomElement& account, const MyMoneyAccount& _p) { MyMoneyAccount p(_p); p.setNumber(hideString(p.number())); p.setName(p.id()); p.setDescription(hideString(p.description())); fakeKeyValuePair(p); // Remove the online banking settings entirely. p.setOnlineBankingSettings(MyMoneyKeyValueContainer()); MyMoneyStorageXML::writeAccount(account, p); } void MyMoneyStorageANON::fakeTransaction(MyMoneyTransaction& tx) { MyMoneyTransaction tn = tx; // hide transaction data tn.setMemo(tx.id()); tn.setBankID(hideString(tx.bankID())); // hide split data foreach (const auto split, tx.splits()) { MyMoneySplit s = split; s.setMemo(QString("%1/%2").arg(tn.id()).arg(s.id())); if (s.value() != MyMoneyMoney::autoCalc) { s.setValue((s.value() * m_factor)); s.setShares((s.shares() * m_factor)); } s.setNumber(hideString(s.number())); // obfuscate a possibly matched transaction as well if (s.isMatched()) { MyMoneyTransaction t = s.matchedTransaction(); fakeTransaction(t); s.removeMatch(); s.addMatch(t); } tn.modifySplit(s); } tx = tn; fakeKeyValuePair(tx); } void MyMoneyStorageANON::fakeKeyValuePair(MyMoneyKeyValueContainer& kvp) { QMap pairs; QMap::const_iterator it; for (it = kvp.pairs().constBegin(); it != kvp.pairs().constEnd(); ++it) { if (zKvpXNumber.contains(it.key()) || it.key().left(3) == "ir-") pairs[it.key()] = hideNumber(MyMoneyMoney(it.value())).toString(); else if (zKvpNoModify.contains(it.key())) pairs[it.key()] = it.value(); else pairs[it.key()] = hideString(it.value()); } kvp.setPairs(pairs); } void MyMoneyStorageANON::writeTransaction(QDomElement& transactions, const MyMoneyTransaction& tx) { MyMoneyTransaction tn = tx; fakeTransaction(tn); MyMoneyStorageXML::writeTransaction(transactions, tn); } void MyMoneyStorageANON::writeSchedule(QDomElement& scheduledTx, const MyMoneySchedule& sx) { MyMoneySchedule sn = sx; MyMoneyTransaction tn = sn.transaction(); fakeTransaction(tn); sn.setName(sx.id()); sn.setTransaction(tn, true); MyMoneyStorageXML::writeSchedule(scheduledTx, sn); } void MyMoneyStorageANON::writeSecurity(QDomElement& securityElement, const MyMoneySecurity& security) { MyMoneySecurity s = security; s.setName(security.id()); fakeKeyValuePair(s); MyMoneyStorageXML::writeSecurity(securityElement, s); } QString MyMoneyStorageANON::hideString(const QString& _in) const { return QString(_in).fill('x'); } MyMoneyMoney MyMoneyStorageANON::hideNumber(const MyMoneyMoney& _in) const { MyMoneyMoney result; static MyMoneyMoney counter = MyMoneyMoney(100, 100); // preserve sign if (_in.isNegative()) result = MyMoneyMoney::MINUS_ONE; else result = MyMoneyMoney::ONE; result = result * counter; counter += MyMoneyMoney("10/100"); // preserve > 1000 if (_in >= MyMoneyMoney(1000, 1)) result = result * MyMoneyMoney(1000, 1); if (_in <= MyMoneyMoney(-1000, 1)) result = result * MyMoneyMoney(1000, 1); return result.convert(); } void MyMoneyStorageANON::fakeBudget(MyMoneyBudget& bx) { MyMoneyBudget bn; bn.setName(bx.id()); bn.setBudgetStart(bx.budgetStart()); bn = MyMoneyBudget(bx.id(), bn); QList list = bx.getaccounts(); QList::iterator it; for (it = list.begin(); it != list.end(); ++it) { // only add the account if there is a budget entered if (!(*it).balance().isZero()) { MyMoneyBudget::AccountGroup account; account.setId((*it).id()); account.setBudgetLevel((*it).budgetLevel()); account.setBudgetSubaccounts((*it).budgetSubaccounts()); QMap plist = (*it).getPeriods(); QMap::const_iterator it_p; for (it_p = plist.constBegin(); it_p != plist.constEnd(); ++it_p) { MyMoneyBudget::PeriodGroup pGroup; pGroup.setAmount((*it_p).amount() * m_factor); pGroup.setStartDate((*it_p).startDate()); account.addPeriod(pGroup.startDate(), pGroup); } bn.setAccount(account, account.id()); } } bx = bn; } void MyMoneyStorageANON::writeBudget(QDomElement& budgets, const MyMoneyBudget& b) { MyMoneyBudget bn = b; fakeBudget(bn); MyMoneyStorageXML::writeBudget(budgets, bn); } void MyMoneyStorageANON::writeReport(QDomElement& reports, const MyMoneyReport& r) { MyMoneyReport rn = r; rn.setName(rn.id()); rn.setComment(hideString(rn.comment())); MyMoneyStorageXML::writeReport(reports, rn); } void MyMoneyStorageANON::writeOnlineJob(QDomElement& onlineJobs, const onlineJob& job) { Q_UNUSED(onlineJobs); Q_UNUSED(job); } diff --git a/kmymoney/mymoney/tests/mymoneymoney-test.cpp b/kmymoney/mymoney/tests/mymoneymoney-test.cpp index 4ae6c7ae1..7badfe0d9 100644 --- a/kmymoney/mymoney/tests/mymoneymoney-test.cpp +++ b/kmymoney/mymoney/tests/mymoneymoney-test.cpp @@ -1,707 +1,707 @@ /*************************************************************************** mymoneymoneytest.cpp ------------------- copyright : (C) 2002 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 "mymoneymoney-test.h" #include #include #include #define KMM_MYMONEY_UNIT_TESTABLE friend class MyMoneyMoneyTest; #include #include "mymoneyexception.h" #include "mymoneymoney.h" - +#include "mymoneyenums.h" QTEST_GUILESS_MAIN(MyMoneyMoneyTest) void MyMoneyMoneyTest::init() { m_0 = new MyMoneyMoney(12, 100); m_1 = new MyMoneyMoney(-10, 100); m_2 = new MyMoneyMoney(2, 100); m_3 = new MyMoneyMoney(123, 1); m_4 = new MyMoneyMoney(1234, 1000); m_5 = new MyMoneyMoney(195883, 100000); m_6 = new MyMoneyMoney(1.247658435, 1000000000); MyMoneyMoney::setDecimalSeparator('.'); MyMoneyMoney::setThousandSeparator(','); - MyMoneyMoney::setNegativeMonetarySignPosition(MyMoneyMoney::BeforeQuantityMoney); + MyMoneyMoney::setNegativeMonetarySignPosition(eMyMoney::Money::BeforeQuantityMoney); } void MyMoneyMoneyTest::cleanup() { delete m_0; delete m_1; delete m_2; delete m_3; delete m_4; delete m_5; delete m_6; } void MyMoneyMoneyTest::testEmptyConstructor() { //qDebug("testing %s", qPrintable(m_0->toString())); MyMoneyMoney *m = new MyMoneyMoney(); QVERIFY(m->valueRef() == 0); QVERIFY(m->toString() == QString("0/1")); QVERIFY(m->valueRef().get_den() == 1); delete m; } void MyMoneyMoneyTest::testIntConstructor() { //qDebug("Current value: %s",qPrintable( m_0->toString()) ); //QVERIFY(m_0->valueRef().get_num() == 12); //QVERIFY(m_0->valueRef().get_den() == 100); QVERIFY(m_0->valueRef().get_num() == 3); QVERIFY(m_0->valueRef().get_den() == 25); MyMoneyMoney a(123, 10000); QVERIFY(a.valueRef().get_num() == 123); QVERIFY(a.valueRef().get_den() == 10000); } void MyMoneyMoneyTest::testAssignment() { MyMoneyMoney *m = new MyMoneyMoney(); *m = *m_1; //qDebug() << "Current value: "<< qPrintable( m->toString()) ; QVERIFY(m->valueRef().get_num() == -1); QVERIFY(m->valueRef().get_den() == 10); //QVERIFY(m->valueRef().get_num() == -10); //QVERIFY(m->valueRef().get_den() == 100); #if 0 *m = 0; QVERIFY(m->valueRef().get_num() == 0); QVERIFY(m->valueRef().get_den() == 100); *m = 777888999; QVERIFY(m->valueRef().get_num() == 777888999); QVERIFY(m->valueRef().get_den() == 100); *m = (int) - 5678; QVERIFY(m->valueRef().get_num() == -5678); QVERIFY(m->valueRef().get_den() == 100); *m = QString("-987"); QVERIFY(m->valueRef().get_num() == -987); QVERIFY(m->valueRef().get_den() == 1); *m = QString("9998887776665554.44"); QVERIFY(m->valueRef().get_num() == 999888777666555444LL); QVERIFY(m->valueRef().get_den() == 100); *m = QString("-99988877766655.444"); QVERIFY(m->valueRef().get_num() == -99988877766655444LL); QVERIFY(m->valueRef().get_den() == 1000); *m = -666555444333222111LL; QVERIFY(m->valueRef().get_num() == -666555444333222111LL); QVERIFY(m->valueRef().get_den() == 100); #endif delete m; } void MyMoneyMoneyTest::testStringConstructor() { MyMoneyMoney *m1 = new MyMoneyMoney("-999666555444"); mpz_class testnum = mpz_class("-999666555444"); //qDebug("Created %s", qPrintable(m1->toString())); QVERIFY(m1->valueRef().get_num() == testnum); QVERIFY(m1->valueRef().get_den() == 1); testnum = mpz_class("444555666999"); MyMoneyMoney *m2 = new MyMoneyMoney("4445556669.99"); QVERIFY(m2->valueRef().get_num() == testnum); QVERIFY(m2->valueRef().get_den() == 100); delete m1; delete m2; //new tests m1 = new MyMoneyMoney("0.01"); QVERIFY(m1->valueRef().get_num() == 1); QVERIFY(m1->valueRef().get_den() == 100); delete m1; m1 = new MyMoneyMoney("0.07"); QVERIFY(m1->valueRef().get_num() == 7); QVERIFY(m1->valueRef().get_den() == 100); delete m1; m1 = new MyMoneyMoney("0.08"); QVERIFY(m1->valueRef().get_num() == 2); QVERIFY(m1->valueRef().get_den() == 25); delete m1; m1 = new MyMoneyMoney("."); //qDebug("Created %s", qPrintable(m1->toString())); QVERIFY(m1->valueRef().get_num() == 0); QVERIFY(m1->valueRef().get_den() == 1); delete m1; m1 = new MyMoneyMoney(""); QVERIFY(m1->valueRef().get_num() == 0); QVERIFY(m1->valueRef().get_den() == 1); delete m1; m1 = new MyMoneyMoney("1,123."); QVERIFY(m1->valueRef().get_num() == (1123)); QVERIFY(m1->valueRef().get_den() == 1); delete m1; m1 = new MyMoneyMoney("123.1"); QVERIFY(m1->valueRef().get_num() == (1231)); QVERIFY(m1->valueRef().get_den() == 10); delete m1; m1 = new MyMoneyMoney("123.456"); //qDebug("Created: %s", m1->valueRef().get_str().c_str()); QVERIFY(m1->valueRef().get_num() == 15432); QVERIFY(m1->valueRef().get_den() == 125); //QVERIFY(m1->valueRef().get_num() == 123456); //QVERIFY(m1->valueRef().get_den() == 1000); delete m1; m1 = new MyMoneyMoney("12345/100"); //qDebug("Created: %s", m1->valueRef().get_str().c_str()); QVERIFY(m1->valueRef().get_num() == 2469); QVERIFY(m1->valueRef().get_den() == 20); // QVERIFY(m1->valueRef().get_num() == (12345)); // QVERIFY(m1->valueRef().get_den() == 100); delete m1; m1 = new MyMoneyMoney("-54321/100"); // qDebug("Created: %s", m1->valueRef().get_str().c_str()); QVERIFY(m1->valueRef().get_num() == (-54321)); QVERIFY(m1->valueRef().get_den() == 100); delete m1; MyMoneyMoney::setDecimalSeparator(','); MyMoneyMoney::setThousandSeparator('.'); - MyMoneyMoney::setNegativeMonetarySignPosition(MyMoneyMoney::ParensAround); + MyMoneyMoney::setNegativeMonetarySignPosition(eMyMoney::Money::ParensAround); m1 = new MyMoneyMoney("x1.234,567 EUR"); QVERIFY(m1->valueRef().get_num() == (1234567)); QVERIFY(m1->valueRef().get_den() == 1000); delete m1; m1 = new MyMoneyMoney("x(1.234,567) EUR"); QVERIFY(m1->valueRef().get_num() == (-1234567)); QVERIFY(m1->valueRef().get_den() == 1000); delete m1; m1 = new MyMoneyMoney("1 5/8"); QVERIFY(m1->valueRef().get_num() == (13)); QVERIFY(m1->valueRef().get_den() == 8); delete m1; m1 = new MyMoneyMoney("09"); QVERIFY(m1->valueRef().get_num() == (9)); QVERIFY(m1->valueRef().get_den() == 1); delete m1; } void MyMoneyMoneyTest::testConvert() { MyMoneyMoney a(123.456); MyMoneyMoney b(a.convertDenominator(100)); QVERIFY(b == MyMoneyMoney(12346 , 100)); a = QString("-123.456"); b = a.convert(100); QVERIFY(b == MyMoneyMoney(-12346 , 100)); a = QString("123.1"); b = a.convert(100); QVERIFY(b == MyMoneyMoney(12310, 100)); a = QString("-73010.28"); b = QString("1.95583"); QVERIFY((a * b).convert(100) == QString("-142795.70")); QVERIFY((a * b).convert(100) == QString("-14279570/100")); // QVERIFY((a * b).convert(100).toString() == QString("-14279570/100")); a = QString("-142795.69"); QVERIFY((a / b).convert(100) == QString("-73010.28")); //QVERIFY((a / b).convert(100).toString() == QString("-7301028/100")); } void MyMoneyMoneyTest::testEquality() { QVERIFY(*m_1 == *m_1); QVERIFY(!(*m_1 == *m_0)); MyMoneyMoney m1(std::int64_t(999666555444), 100); MyMoneyMoney m2(std::int64_t(999666555444), 100); QVERIFY(m1 == m2); MyMoneyMoney m3(std::int64_t(-999666555444), 100); MyMoneyMoney m4(std::int64_t(-999666555444), 100); QVERIFY(m3 == m4); MyMoneyMoney m5(1230, 100); MyMoneyMoney m6(123, 10); MyMoneyMoney m7(246, 20); QVERIFY(m5 == m6); QVERIFY(m5 == m7); QVERIFY(m5 == QString("369/30")); QVERIFY(MyMoneyMoney::autoCalc == MyMoneyMoney::autoCalc); MyMoneyMoney mm1, mm2; mm1 = QLatin1String("-14279570/100"); mm2 = QLatin1String("-1427957/10"); QVERIFY(mm1 == mm2); QVERIFY(mm1 == QLatin1String("-14279570/100")); mm1 = QLatin1String("-7301028/100"); mm2 = QLatin1String("-1825257/25"); QVERIFY(mm1 == mm2); } void MyMoneyMoneyTest::testInequality() { QVERIFY(*m_1 != *m_0); QVERIFY(!(*m_1 != *m_1)); MyMoneyMoney m1(std::int64_t(999666555444), 100); MyMoneyMoney m2(std::int64_t(-999666555444), 100); QVERIFY(m1 != m2); MyMoneyMoney m3(std::int64_t(-999666555444), 100); MyMoneyMoney m4(std::int64_t(999666555444), 100); QVERIFY(m3 != m4); QVERIFY(m4 != QString("999666555444")); QVERIFY(MyMoneyMoney::autoCalc != MyMoneyMoney(1, 100)); QVERIFY(MyMoneyMoney(1, 100) != MyMoneyMoney::autoCalc); } void MyMoneyMoneyTest::testAddition() { QVERIFY(*m_0 + *m_1 == *m_2); MyMoneyMoney m1(100, 100); // QVERIFY((m1 + 50) == MyMoneyMoney(51,1)); // QVERIFY((m1 + 1000000000) == MyMoneyMoney(1000000001,1)); // QVERIFY((m1 + -50) == MyMoneyMoney(-49,1)); QVERIFY((m1 += *m_0) == MyMoneyMoney(112, 100)); // QVERIFY((m1 += -12) == MyMoneyMoney(100)); // m1++; // QVERIFY(m1 == MyMoneyMoney(101)); // QVERIFY((++m1) == MyMoneyMoney(102)); m1 = QString("123.20"); MyMoneyMoney m2(40, 1000); QVERIFY((m1 + m2) == QString("123.24")); m1 += m2; //FIXME check after deciding about normalization QVERIFY(m1.valueRef().get_num() == 3081); QVERIFY(m1.valueRef().get_den() == 25); //QVERIFY(m1.valueRef().get_num() == 123240); //QVERIFY(m1.valueRef().get_den() == 1000); } void MyMoneyMoneyTest::testSubtraction() { QVERIFY(*m_2 - *m_1 == *m_0); MyMoneyMoney m1(100, 100); // QVERIFY((m1-50) == MyMoneyMoney(-49,1)); // QVERIFY((m1-1000000000) == MyMoneyMoney(-999999999,1)); // QVERIFY((m1 - -50) == MyMoneyMoney(51,1)); QVERIFY((m1 -= *m_0) == MyMoneyMoney(88, 100)); // QVERIFY((m1 -= -12) == MyMoneyMoney(100)); // m1--; // QVERIFY(m1 == MyMoneyMoney(99)); // QVERIFY((--m1) == MyMoneyMoney(98)); m1 = QString("123.20"); MyMoneyMoney m2(1, 5); QVERIFY((m1 - m2) == MyMoneyMoney(123, 1)); m1 -= m2; //FIXME check after deciding about normalization QVERIFY(m1.valueRef().get_num() == 123); QVERIFY(m1.valueRef().get_den() == 1); //QVERIFY(m1.valueRef().get_num() == 12300); //QVERIFY(m1.valueRef().get_den() == 100); } void MyMoneyMoneyTest::testMultiplication() { MyMoneyMoney m1(100, 1); QVERIFY((m1 * MyMoneyMoney(50, 1)) == MyMoneyMoney(5000, 1)); QVERIFY((m1 * MyMoneyMoney(10000000, 1)) == MyMoneyMoney(1000000000, 1)); QVERIFY((m1 *(*m_0)) == MyMoneyMoney(1200, 100)); MyMoneyMoney m2(QString("-73010.28")); m1 = QString("1.95583"); QVERIFY((m1 * m2) == QString("-142795.6959324")); MyMoneyMoney m3(100, 1); QVERIFY((m3 * 10) == MyMoneyMoney(1000, 1)); //QVERIFY( (m3 *= (*m_0)) == MyMoneyMoney(1200)); QVERIFY((m3 *= (*m_0)) == MyMoneyMoney(1200, 100)); } void MyMoneyMoneyTest::testDivision() { MyMoneyMoney m1(100, 100); QVERIFY((m1 / MyMoneyMoney(50, 100)) == MyMoneyMoney(2, 1)); MyMoneyMoney m2(QString("-142795.69")); m1 = QString("1.95583"); QVERIFY((m2 / m1).convert(100000000) == QString("-73010.27696681")); MyMoneyMoney m3 = MyMoneyMoney() / MyMoneyMoney(100, 100); QVERIFY(m3.valueRef().get_num() == 0); QVERIFY(m3.valueRef().get_den() != 0); } void MyMoneyMoneyTest::testSetDecimalSeparator() { MyMoneyMoney m1(100000, 100); MyMoneyMoney m2(200000, 100); QVERIFY(m1.formatMoney("", 2) == QString("1,000.00")); QVERIFY(MyMoneyMoney::decimalSeparator() == '.'); MyMoneyMoney::setDecimalSeparator(':'); QVERIFY(m1.formatMoney("", 2) == QString("1,000:00")); QVERIFY(m2.formatMoney("", 2) == QString("2,000:00")); QVERIFY(MyMoneyMoney::decimalSeparator() == ':'); } void MyMoneyMoneyTest::testSetThousandSeparator() { MyMoneyMoney m1(100000, 100); MyMoneyMoney m2(200000, 100); QVERIFY(m1.formatMoney("", 2) == QString("1,000.00")); QVERIFY(MyMoneyMoney::thousandSeparator() == ','); MyMoneyMoney::setThousandSeparator(':'); QVERIFY(m1.formatMoney("", 2) == QString("1:000.00")); QVERIFY(m2.formatMoney("", 2) == QString("2:000.00")); QVERIFY(MyMoneyMoney::thousandSeparator() == ':'); } void MyMoneyMoneyTest::testFormatMoney() { qDebug() << "Value:" << qPrintable(m_0->toString()); qDebug() << "Converted: " << qPrintable(m_0->convert(100).toString()); qDebug() << " Formatted: " << qPrintable(m_0->formatMoney("", 2)); QVERIFY(m_0->formatMoney("", 2) == QString("0.12")); QVERIFY(m_1->formatMoney("", 2) == QString("-0.10")); MyMoneyMoney m1(10099, 100); qDebug() << "Value:" << qPrintable(m1.toString()); qDebug() << "Converted: " << qPrintable(m1.convert(100).toString()); qDebug() << " Formatted: " << qPrintable(m1.formatMoney("", 2)); QVERIFY(m1.formatMoney("", 2) == QString("100.99")); m1 = MyMoneyMoney(100, 1); qDebug() << "Value:" << qPrintable(m1.toString()); qDebug() << "Converted: " << qPrintable(m1.convert(100).toString()); qDebug() << " Formatted: " << qPrintable(m1.formatMoney("", 2)); QVERIFY(m1.formatMoney("", 2) == QString("100.00")); QVERIFY(m1.formatMoney("", -1) == QString("100")); MyMoneyMoney mTemp(100099, 100); m1 = m1 * MyMoneyMoney(10, 1); QVERIFY(m1 == MyMoneyMoney(1000, 1)); QVERIFY(m1.formatMoney("", 2) == QString("1,000.00")); QVERIFY(m1.formatMoney("", -1) == QString("1,000")); QVERIFY(m1.formatMoney("", -1, false) == QString("1000")); QVERIFY(m1.formatMoney("", 3, false) == QString("1000.000")); m1 = MyMoneyMoney(std::numeric_limits::max(), 100); QVERIFY(m1.formatMoney("", 2) == QString("92,233,720,368,547,758.07")); QVERIFY(m1.formatMoney(100) == QString("92,233,720,368,547,758.07")); QVERIFY(m1.formatMoney("", 2, false) == QString("92233720368547758.07")); QVERIFY(m1.formatMoney(100, false) == QString("92233720368547758.07")); m1 = MyMoneyMoney(std::numeric_limits::min(), 100); QVERIFY(m1.formatMoney("", 2) == QString("-92,233,720,368,547,758.08")); QVERIFY(m1.formatMoney(100) == QString("-92,233,720,368,547,758.08")); QVERIFY(m1.formatMoney("", 2, false) == QString("-92233720368547758.08")); QVERIFY(m1.formatMoney(100, false) == QString("-92233720368547758.08")); // make sure we support numbers that need more than 64 bit m1 = MyMoneyMoney(321, 100) * MyMoneyMoney(std::numeric_limits::max(), 100); QVERIFY(m1.formatMoney("", 2) == QString("296,070,242,383,038,303.40")); QVERIFY(m1.formatMoney("", 4) == QString("296,070,242,383,038,303.4047")); QVERIFY(m1.formatMoney("", 6) == QString("296,070,242,383,038,303.404700")); m1 = MyMoneyMoney(1, 5); QVERIFY(m1.formatMoney("", 2) == QString("0.20")); QVERIFY(m1.formatMoney(1000) == QString("0.200")); QVERIFY(m1.formatMoney(100) == QString("0.20")); QVERIFY(m1.formatMoney(10) == QString("0.2")); m1 = MyMoneyMoney(13333, 5000); QVERIFY(m1.formatMoney("", 10) == QString("2.6666000000")); m1 = MyMoneyMoney(-1404, 100); QVERIFY(m1.formatMoney("", -1) == QString("-14.04")); } void MyMoneyMoneyTest::testRelation() { MyMoneyMoney m1(100, 100); MyMoneyMoney m2(50, 100); MyMoneyMoney m3(100, 100); // tests with same denominator QVERIFY(m1 > m2); QVERIFY(m2 < m1); QVERIFY(m1 <= m3); QVERIFY(m3 >= m1); QVERIFY(m1 <= m1); QVERIFY(m3 >= m3); // tests with different denominator m1 = QString("1/8"); m2 = QString("1/7"); QVERIFY(m1 < m2); QVERIFY(m2 > m1); m2 = QString("-1/7"); QVERIFY(m2 < m1); QVERIFY(m1 > m2); QVERIFY(m1 >= m2); QVERIFY(m2 <= m1); m1 = QString("-2/14"); QVERIFY(m1 >= m2); QVERIFY(m1 <= m2); } void MyMoneyMoneyTest::testUnaryMinus() { MyMoneyMoney m1(100, 100); MyMoneyMoney m2; m2 = -m1; QVERIFY(m1 == MyMoneyMoney(100, 100)); QVERIFY(m2 == MyMoneyMoney(-100, 100)); } void MyMoneyMoneyTest::testDoubleConstructor() { for (int i = -123456; i < 123456; ++i) { // int i = -123456; double d = i; MyMoneyMoney r(i, 100); d /= 100; MyMoneyMoney t(d, 100); MyMoneyMoney s(i); QVERIFY(t == r); QVERIFY(i == s.toDouble()); } } void MyMoneyMoneyTest::testAbsoluteFunction() { MyMoneyMoney m1(-100, 100); MyMoneyMoney m2(100, 100); QVERIFY(m2.abs() == MyMoneyMoney(100, 100)); QVERIFY(m1.abs() == MyMoneyMoney(100, 100)); } void MyMoneyMoneyTest::testToString() { MyMoneyMoney m1(-100, 100); MyMoneyMoney m2(1234, 100); MyMoneyMoney m3; //qDebug("Created: %s", m3.valueRef().get_str().c_str()); //QVERIFY(m1.toString() == QString("-100/100")); QVERIFY(m1.toString() == QString("-1/1")); // qDebug("Current value: %s",qPrintable( m2.toString()) ); //QVERIFY(m2.toString() == QString("1234/100")); QVERIFY(m2.toString() == QString("617/50")); QVERIFY(m3.toString() == QString("0/1")); //FIXME check the impact of the canonicalize in the whole code //QVERIFY(m3.toString() == QString("0")); } void MyMoneyMoneyTest::testNegativeSignPos() { MyMoneyMoney m("-123456/100"); - MyMoneyMoney::signPosition pos = MyMoneyMoney::negativeMonetarySignPosition(); + eMyMoney::Money::signPosition pos = MyMoneyMoney::negativeMonetarySignPosition(); MyMoneyMoney::setNegativePrefixCurrencySymbol(false); - MyMoneyMoney::setNegativeMonetarySignPosition(MyMoneyMoney::ParensAround); + MyMoneyMoney::setNegativeMonetarySignPosition(eMyMoney::Money::ParensAround); QVERIFY(m.formatMoney("CUR", 2) == "(1,234.56) CUR"); - MyMoneyMoney::setNegativeMonetarySignPosition(MyMoneyMoney::BeforeQuantityMoney); + MyMoneyMoney::setNegativeMonetarySignPosition(eMyMoney::Money::BeforeQuantityMoney); QVERIFY(m.formatMoney("CUR", 2) == "-1,234.56 CUR"); - MyMoneyMoney::setNegativeMonetarySignPosition(MyMoneyMoney::AfterQuantityMoney); + MyMoneyMoney::setNegativeMonetarySignPosition(eMyMoney::Money::AfterQuantityMoney); QVERIFY(m.formatMoney("CUR", 2) == "1,234.56- CUR"); - MyMoneyMoney::setNegativeMonetarySignPosition(MyMoneyMoney::BeforeMoney); + MyMoneyMoney::setNegativeMonetarySignPosition(eMyMoney::Money::BeforeMoney); QVERIFY(m.formatMoney("CUR", 2) == "1,234.56 -CUR"); - MyMoneyMoney::setNegativeMonetarySignPosition(MyMoneyMoney::AfterMoney); + MyMoneyMoney::setNegativeMonetarySignPosition(eMyMoney::Money::AfterMoney); QVERIFY(m.formatMoney("CUR", 2) == "1,234.56 CUR-"); MyMoneyMoney::setNegativePrefixCurrencySymbol(true); - MyMoneyMoney::setNegativeMonetarySignPosition(MyMoneyMoney::ParensAround); + MyMoneyMoney::setNegativeMonetarySignPosition(eMyMoney::Money::ParensAround); QVERIFY(m.formatMoney("CUR", 2) == "CUR (1,234.56)"); - MyMoneyMoney::setNegativeMonetarySignPosition(MyMoneyMoney::BeforeQuantityMoney); + MyMoneyMoney::setNegativeMonetarySignPosition(eMyMoney::Money::BeforeQuantityMoney); QVERIFY(m.formatMoney("CUR", 2) == "CUR -1,234.56"); - MyMoneyMoney::setNegativeMonetarySignPosition(MyMoneyMoney::AfterQuantityMoney); + MyMoneyMoney::setNegativeMonetarySignPosition(eMyMoney::Money::AfterQuantityMoney); QVERIFY(m.formatMoney("CUR", 2) == "CUR 1,234.56-"); - MyMoneyMoney::setNegativeMonetarySignPosition(MyMoneyMoney::BeforeMoney); + MyMoneyMoney::setNegativeMonetarySignPosition(eMyMoney::Money::BeforeMoney); QVERIFY(m.formatMoney("CUR", 2) == "-CUR 1,234.56"); - MyMoneyMoney::setNegativeMonetarySignPosition(MyMoneyMoney::AfterMoney); + MyMoneyMoney::setNegativeMonetarySignPosition(eMyMoney::Money::AfterMoney); QVERIFY(m.formatMoney("CUR", 2) == "CUR- 1,234.56"); MyMoneyMoney::setNegativeMonetarySignPosition(pos); } void MyMoneyMoneyTest::testPositiveSignPos() { MyMoneyMoney m("123456/100"); - MyMoneyMoney::signPosition pos = MyMoneyMoney::positiveMonetarySignPosition(); + eMyMoney::Money::signPosition pos = MyMoneyMoney::positiveMonetarySignPosition(); MyMoneyMoney::setPositivePrefixCurrencySymbol(false); - MyMoneyMoney::setPositiveMonetarySignPosition(MyMoneyMoney::ParensAround); + MyMoneyMoney::setPositiveMonetarySignPosition(eMyMoney::Money::ParensAround); QVERIFY(m.formatMoney("CUR", 2) == "(1,234.56) CUR"); - MyMoneyMoney::setPositiveMonetarySignPosition(MyMoneyMoney::BeforeQuantityMoney); + MyMoneyMoney::setPositiveMonetarySignPosition(eMyMoney::Money::BeforeQuantityMoney); QVERIFY(m.formatMoney("CUR", 2) == "1,234.56 CUR"); - MyMoneyMoney::setPositiveMonetarySignPosition(MyMoneyMoney::AfterQuantityMoney); + MyMoneyMoney::setPositiveMonetarySignPosition(eMyMoney::Money::AfterQuantityMoney); QVERIFY(m.formatMoney("CUR", 2) == "1,234.56 CUR"); - MyMoneyMoney::setPositiveMonetarySignPosition(MyMoneyMoney::BeforeMoney); + MyMoneyMoney::setPositiveMonetarySignPosition(eMyMoney::Money::BeforeMoney); QVERIFY(m.formatMoney("CUR", 2) == "1,234.56 CUR"); - MyMoneyMoney::setPositiveMonetarySignPosition(MyMoneyMoney::AfterMoney); + MyMoneyMoney::setPositiveMonetarySignPosition(eMyMoney::Money::AfterMoney); QVERIFY(m.formatMoney("CUR", 2) == "1,234.56 CUR"); MyMoneyMoney::setPositivePrefixCurrencySymbol(true); - MyMoneyMoney::setPositiveMonetarySignPosition(MyMoneyMoney::ParensAround); + MyMoneyMoney::setPositiveMonetarySignPosition(eMyMoney::Money::ParensAround); QVERIFY(m.formatMoney("CUR", 2) == "CUR (1,234.56)"); - MyMoneyMoney::setPositiveMonetarySignPosition(MyMoneyMoney::BeforeQuantityMoney); + MyMoneyMoney::setPositiveMonetarySignPosition(eMyMoney::Money::BeforeQuantityMoney); QVERIFY(m.formatMoney("CUR", 2) == "CUR 1,234.56"); - MyMoneyMoney::setPositiveMonetarySignPosition(MyMoneyMoney::AfterQuantityMoney); + MyMoneyMoney::setPositiveMonetarySignPosition(eMyMoney::Money::AfterQuantityMoney); QVERIFY(m.formatMoney("CUR", 2) == "CUR 1,234.56"); - MyMoneyMoney::setPositiveMonetarySignPosition(MyMoneyMoney::BeforeMoney); + MyMoneyMoney::setPositiveMonetarySignPosition(eMyMoney::Money::BeforeMoney); QVERIFY(m.formatMoney("CUR", 2) == "CUR 1,234.56"); - MyMoneyMoney::setPositiveMonetarySignPosition(MyMoneyMoney::AfterMoney); + MyMoneyMoney::setPositiveMonetarySignPosition(eMyMoney::Money::AfterMoney); QVERIFY(m.formatMoney("CUR", 2) == "CUR 1,234.56"); MyMoneyMoney::setPositiveMonetarySignPosition(pos); } void MyMoneyMoneyTest::testNegativeStringConstructor() { MyMoneyMoney *m1; MyMoneyMoney::setDecimalSeparator(','); MyMoneyMoney::setThousandSeparator('.'); - MyMoneyMoney::setNegativeMonetarySignPosition(MyMoneyMoney::ParensAround); + MyMoneyMoney::setNegativeMonetarySignPosition(eMyMoney::Money::ParensAround); m1 = new MyMoneyMoney("x(1.234,567) EUR"); QVERIFY(m1->valueRef().get_num() == (-1234567)); QVERIFY(m1->valueRef().get_den() == 1000); delete m1; - MyMoneyMoney::setNegativeMonetarySignPosition(MyMoneyMoney::BeforeQuantityMoney); + MyMoneyMoney::setNegativeMonetarySignPosition(eMyMoney::Money::BeforeQuantityMoney); m1 = new MyMoneyMoney("x1.234,567- EUR"); //qDebug("Created: %s", m1->valueRef().get_str().c_str()); QVERIFY(m1->valueRef().get_num() == (-1234567)); QVERIFY(m1->valueRef().get_den() == 1000); delete m1; m1 = new MyMoneyMoney("x1.234,567 -EUR"); QVERIFY(m1->valueRef().get_num() == (-1234567)); QVERIFY(m1->valueRef().get_den() == 1000); delete m1; m1 = new MyMoneyMoney("-1.234,567 EUR"); QVERIFY(m1->valueRef().get_num() == (-1234567)); QVERIFY(m1->valueRef().get_den() == 1000); delete m1; } void MyMoneyMoneyTest::testReduce() { MyMoneyMoney a(36488100, 1267390000); MyMoneyMoney b(-a); a = a.reduce(); QVERIFY(a.valueRef().get_num() == 364881); QVERIFY(a.valueRef().get_den() == 12673900); b = b.reduce(); QVERIFY(b.valueRef().get_num() == -364881); QVERIFY(b.valueRef().get_den() == 12673900); } void MyMoneyMoneyTest::testZeroDenominator() { try { MyMoneyMoney m((int)1, 0); QFAIL("Missing expected exception"); } catch (const MyMoneyException &) { } try { MyMoneyMoney m((signed64)1, 0); QFAIL("Missing expected exception"); } catch (const MyMoneyException &) { } } diff --git a/kmymoney/plugins/ofximport/ofximporterplugin.cpp b/kmymoney/plugins/ofximport/ofximporterplugin.cpp index 774c15eb5..d91ffc129 100644 --- a/kmymoney/plugins/ofximport/ofximporterplugin.cpp +++ b/kmymoney/plugins/ofximport/ofximporterplugin.cpp @@ -1,843 +1,844 @@ /*************************************************************************** ofximporterplugin.cpp ------------------- begin : Sat Jan 01 2005 copyright : (C) 2005 by Ace Jones email : 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. * * * ***************************************************************************/ #include "ofximporterplugin.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include #include "konlinebankingstatus.h" #include "konlinebankingsetupwizard.h" #include "kofxdirectconnectdlg.h" #include "mymoneyaccount.h" +#include "mymoneyexception.h" #include "mymoneystatement.h" #include "statementinterface.h" #include "importinterface.h" #include "ui_importoption.h" //#define DEBUG_LIBOFX using KWallet::Wallet; class OfxImporterPlugin::Private { public: Private() : m_valid(false), m_preferName(PreferId), m_walletIsOpen(false), m_statusDlg(0), m_wallet(0), m_updateStartDate(QDate(1900,1,1)) {} bool m_valid; enum NamePreference { PreferId = 0, PreferName, PreferMemo } m_preferName; bool m_walletIsOpen; QList m_statementlist; QList m_securitylist; QString m_fatalerror; QStringList m_infos; QStringList m_warnings; QStringList m_errors; KOnlineBankingStatus* m_statusDlg; Wallet *m_wallet; QDate m_updateStartDate; }; OfxImporterPlugin::OfxImporterPlugin() : KMyMoneyPlugin::Plugin(nullptr, "KMyMoney OFX"), /* * the string in the line above must be the same as * X-KDE-PluginInfo-Name and the provider name assigned in * OfxImporterPlugin::onlineBankingSettings() */ KMyMoneyPlugin::ImporterPlugin(), d(new Private) { setComponentName("kmm_ofximport", i18n("KMyMoney OFX")); setXMLFile("kmm_ofximport.rc"); createActions(); // For ease announce that we have been loaded. qDebug("KMyMoney ofximport plugin loaded"); } OfxImporterPlugin::~OfxImporterPlugin() { delete d; } void OfxImporterPlugin::createActions() { QAction *action = actionCollection()->addAction("file_import_ofx"); action->setText(i18n("OFX...")); connect(action, SIGNAL(triggered(bool)), this, SLOT(slotImportFile())); } void OfxImporterPlugin::slotImportFile() { QWidget * widget = new QWidget; Ui_ImportOption* option = new Ui_ImportOption; option->setupUi(widget); QUrl url = importInterface()->selectFile(i18n("OFX import file selection"), "", "*.ofx *.qfx *.ofc|OFX files (*.ofx, *.qfx, *.ofc)\n*|All files", QFileDialog::ExistingFile, widget); d->m_preferName = static_cast(option->m_preferName->currentIndex()); if (url.isValid()) { if (isMyFormat(url.path())) { slotImportFile(url.path()); } else { KMessageBox::error(0, i18n("Unable to import %1 using the OFX importer plugin. This file is not the correct format.", url.toDisplayString()), i18n("Incorrect format")); } } delete widget; } QString OfxImporterPlugin::formatName() const { return "OFX"; } QString OfxImporterPlugin::formatFilenameFilter() const { return "*.ofx *.qfx *.ofc"; } bool OfxImporterPlugin::isMyFormat(const QString& filename) const { // filename is considered an Ofx file if it contains // the tag "" or "" in the first 20 lines. // which contain some data bool result = false; QFile f(filename); if (f.open(QIODevice::ReadOnly | QIODevice::Text)) { QTextStream ts(&f); int lineCount = 20; while (!ts.atEnd() && !result && lineCount != 0) { // get a line of data and remove all unnecessary whitepace chars QString line = ts.readLine().simplified(); if (line.contains("", Qt::CaseInsensitive) || line.contains("", Qt::CaseInsensitive)) result = true; // count only lines that contain some non white space chars if (!line.isEmpty()) lineCount--; } f.close(); } return result; } bool OfxImporterPlugin::import(const QString& filename) { d->m_fatalerror = i18n("Unable to parse file"); d->m_valid = false; d->m_errors.clear(); d->m_warnings.clear(); d->m_infos.clear(); d->m_statementlist.clear(); d->m_securitylist.clear(); QByteArray filename_deep = QFile::encodeName(filename); ofx_STATUS_msg = true; ofx_INFO_msg = true; ofx_WARNING_msg = true; ofx_ERROR_msg = true; #ifdef DEBUG_LIBOFX ofx_PARSER_msg = true; ofx_DEBUG_msg = true; ofx_DEBUG1_msg = true; ofx_DEBUG2_msg = true; ofx_DEBUG3_msg = true; ofx_DEBUG4_msg = true; ofx_DEBUG5_msg = true; #endif LibofxContextPtr ctx = libofx_get_new_context(); Q_CHECK_PTR(ctx); qDebug("setup callback routines"); ofx_set_transaction_cb(ctx, ofxTransactionCallback, this); ofx_set_statement_cb(ctx, ofxStatementCallback, this); ofx_set_account_cb(ctx, ofxAccountCallback, this); ofx_set_security_cb(ctx, ofxSecurityCallback, this); ofx_set_status_cb(ctx, ofxStatusCallback, this); qDebug("process data"); libofx_proc_file(ctx, filename_deep, AUTODETECT); libofx_free_context(ctx); if (d->m_valid) { d->m_fatalerror.clear(); d->m_valid = storeStatements(d->m_statementlist); } return d->m_valid; } QString OfxImporterPlugin::lastError() const { if (d->m_errors.count() == 0) return d->m_fatalerror; return d->m_errors.join("

"); } /* __________________________________________________________________________ * AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA * * Static callbacks for LibOFX * * YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY */ int OfxImporterPlugin::ofxTransactionCallback(struct OfxTransactionData data, void * pv) { // kDebug(2) << Q_FUNC_INFO; OfxImporterPlugin* pofx = reinterpret_cast(pv); MyMoneyStatement& s = pofx->back(); MyMoneyStatement::Transaction t; if (data.date_posted_valid) { QDateTime dt; dt.setTime_t(data.date_posted); t.m_datePosted = dt.date(); } else if (data.date_initiated_valid) { QDateTime dt; dt.setTime_t(data.date_initiated); t.m_datePosted = dt.date(); } if (t.m_datePosted.isValid()) { // verify the transaction date is one we want if (t.m_datePosted < pofx->d->m_updateStartDate) { //kDebug(0) << "discarding transaction dated" << qPrintable(t.m_datePosted.toString(Qt::ISODate)); return 0; } } if (data.amount_valid) { t.m_amount = MyMoneyMoney(data.amount, 1000); } if (data.check_number_valid) { t.m_strNumber = QString::fromUtf8(data.check_number); } if (data.fi_id_valid) { t.m_strBankID = QString("ID ") + QString::fromUtf8(data.fi_id); } else if (data.reference_number_valid) { t.m_strBankID = QString("REF ") + QString::fromUtf8(data.reference_number); } // Decide whether to use NAME, PAYEEID or MEMO to construct the payee bool validity[3] = {false, false, false}; QStringList values; switch (pofx->d->m_preferName) { case OfxImporterPlugin::Private::PreferId: // PAYEEID default: validity[0] = data.payee_id_valid; validity[1] = data.name_valid; validity[2] = data.memo_valid; values += QString::fromUtf8(data.payee_id); values += QString::fromUtf8(data.name); values += QString::fromUtf8(data.memo); break; case OfxImporterPlugin::Private::PreferName: // NAME validity[0] = data.name_valid; validity[1] = data.payee_id_valid; validity[2] = data.memo_valid; values += QString::fromUtf8(data.name); values += QString::fromUtf8(data.payee_id); values += QString::fromUtf8(data.memo); break; case OfxImporterPlugin::Private::PreferMemo: // MEMO validity[0] = data.memo_valid; validity[1] = data.payee_id_valid; validity[2] = data.name_valid; values += QString::fromUtf8(data.memo); values += QString::fromUtf8(data.payee_id); values += QString::fromUtf8(data.name); break; } for (int idx = 0; idx < 3; ++idx) { if (validity[idx]) { t.m_strPayee = values[idx]; break; } } // extract memo field if we haven't used it as payee if ((data.memo_valid) && (pofx->d->m_preferName != OfxImporterPlugin::Private::PreferMemo)) { t.m_strMemo = QString::fromUtf8(data.memo); } // If the payee or memo fields are blank, set them to // the other one which is NOT blank. (acejones) if (t.m_strPayee.isEmpty()) { // But we only create a payee for non-investment transactions (ipwizard) if (! t.m_strMemo.isEmpty() && data.invtransactiontype_valid == false) t.m_strPayee = t.m_strMemo; } else { if (t.m_strMemo.isEmpty()) t.m_strMemo = t.m_strPayee; } if (data.security_data_valid) { struct OfxSecurityData* secdata = data.security_data_ptr; if (secdata->ticker_valid) { t.m_strSymbol = QString::fromUtf8(secdata->ticker); } if (secdata->secname_valid) { t.m_strSecurity = QString::fromUtf8(secdata->secname); } } t.m_shares = MyMoneyMoney(); if (data.units_valid) { t.m_shares = MyMoneyMoney(data.units, 100000).reduce(); } t.m_price = MyMoneyMoney(); if (data.unitprice_valid) { t.m_price = MyMoneyMoney(data.unitprice, 100000).reduce(); } t.m_fees = MyMoneyMoney(); if (data.fees_valid) { t.m_fees += MyMoneyMoney(data.fees, 1000).reduce(); } if (data.commission_valid) { t.m_fees += MyMoneyMoney(data.commission, 1000).reduce(); } bool unhandledtype = false; QString type; if (data.invtransactiontype_valid) { switch (data.invtransactiontype) { case OFX_BUYDEBT: case OFX_BUYMF: case OFX_BUYOPT: case OFX_BUYOTHER: case OFX_BUYSTOCK: t.m_eAction = eMyMoney::Transaction::Action::Buy; break; case OFX_REINVEST: t.m_eAction = eMyMoney::Transaction::Action::ReinvestDividend; break; case OFX_SELLDEBT: case OFX_SELLMF: case OFX_SELLOPT: case OFX_SELLOTHER: case OFX_SELLSTOCK: t.m_eAction = eMyMoney::Transaction::Action::Sell; break; case OFX_INCOME: t.m_eAction = eMyMoney::Transaction::Action::CashDividend; // NOTE: With CashDividend, the amount of the dividend should // be in data.amount. Since I've never seen an OFX file with // cash dividends, this is an assumption on my part. (acejones) break; // // These types are all not handled. We will generate a warning for them. // case OFX_CLOSUREOPT: unhandledtype = true; type = "CLOSUREOPT (Close a position for an option)"; break; case OFX_INVEXPENSE: unhandledtype = true; type = "INVEXPENSE (Misc investment expense that is associated with a specific security)"; break; case OFX_JRNLFUND: unhandledtype = true; type = "JRNLFUND (Journaling cash holdings between subaccounts within the same investment account)"; break; case OFX_MARGININTEREST: unhandledtype = true; type = "MARGININTEREST (Margin interest expense)"; break; case OFX_RETOFCAP: unhandledtype = true; type = "RETOFCAP (Return of capital)"; break; case OFX_SPLIT: unhandledtype = true; type = "SPLIT (Stock or mutial fund split)"; break; case OFX_TRANSFER: unhandledtype = true; type = "TRANSFER (Transfer holdings in and out of the investment account)"; break; default: unhandledtype = true; type = QString("UNKNOWN %1").arg(data.invtransactiontype); break; } } else t.m_eAction = eMyMoney::Transaction::Action::None; // In the case of investment transactions, the 'total' is supposed to the total amount // of the transaction. units * unitprice +/- commission. Easy, right? Sadly, it seems // some ofx creators do not follow this in all circumstances. Therefore, we have to double- // check the total here and adjust it if it's wrong. #if 0 // Even more sadly, this logic is BROKEN. It consistently results in bogus total // values, because of rounding errors in the price. A more through solution would // be to test if the comission alone is causing a discrepency, and adjust in that case. if (data.invtransactiontype_valid && data.unitprice_valid) { double proper_total = t.m_dShares * data.unitprice + t.m_moneyFees; if (proper_total != t.m_moneyAmount) { pofx->addWarning(QString("Transaction %1 has an incorrect total of %2. Using calculated total of %3 instead.").arg(t.m_strBankID).arg(t.m_moneyAmount).arg(proper_total)); t.m_moneyAmount = proper_total; } } #endif if (unhandledtype) pofx->addWarning(QString("Transaction %1 has an unsupported type (%2).").arg(t.m_strBankID, type)); else s.m_listTransactions += t; // kDebug(2) << Q_FUNC_INFO << "return 0 "; return 0; } int OfxImporterPlugin::ofxStatementCallback(struct OfxStatementData data, void* pv) { // kDebug(2) << Q_FUNC_INFO; OfxImporterPlugin* pofx = reinterpret_cast(pv); MyMoneyStatement& s = pofx->back(); pofx->setValid(); if (data.currency_valid) { s.m_strCurrency = QString::fromUtf8(data.currency); } if (data.account_id_valid) { s.m_strAccountNumber = QString::fromUtf8(data.account_id); } if (data.date_start_valid) { QDateTime dt; dt.setTime_t(data.date_start); s.m_dateBegin = dt.date(); } if (data.date_end_valid) { QDateTime dt; dt.setTime_t(data.date_end); s.m_dateEnd = dt.date(); } if (data.ledger_balance_valid && data.ledger_balance_date_valid) { s.m_closingBalance = MyMoneyMoney(data.ledger_balance); QDateTime dt; dt.setTime_t(data.ledger_balance_date); s.m_dateEnd = dt.date(); } // kDebug(2) << Q_FUNC_INFO << " return 0"; return 0; } int OfxImporterPlugin::ofxAccountCallback(struct OfxAccountData data, void * pv) { // kDebug(2) << Q_FUNC_INFO; OfxImporterPlugin* pofx = reinterpret_cast(pv); pofx->addnew(); MyMoneyStatement& s = pofx->back(); // Having any account at all makes an ofx statement valid pofx->d->m_valid = true; if (data.account_id_valid) { s.m_strAccountName = QString::fromUtf8(data.account_name); s.m_strAccountNumber = QString::fromUtf8(data.account_id); } if (data.bank_id_valid) { s.m_strRoutingNumber = QString::fromUtf8(data.bank_id); } if (data.broker_id_valid) { s.m_strRoutingNumber = QString::fromUtf8(data.broker_id); } if (data.currency_valid) { s.m_strCurrency = QString::fromUtf8(data.currency); } if (data.account_type_valid) { switch (data.account_type) { case OfxAccountData::OFX_CHECKING : s.m_eType = eMyMoney::Statement::Type::Checkings; break; case OfxAccountData::OFX_SAVINGS : s.m_eType = eMyMoney::Statement::Type::Savings; break; case OfxAccountData::OFX_MONEYMRKT : s.m_eType = eMyMoney::Statement::Type::Investment; break; case OfxAccountData::OFX_CREDITLINE : s.m_eType = eMyMoney::Statement::Type::CreditCard; break; case OfxAccountData::OFX_CMA : s.m_eType = eMyMoney::Statement::Type::CreditCard; break; case OfxAccountData::OFX_CREDITCARD : s.m_eType = eMyMoney::Statement::Type::CreditCard; break; case OfxAccountData::OFX_INVESTMENT : s.m_eType = eMyMoney::Statement::Type::Investment; break; } } // ask KMyMoney for an account id s.m_accountId = pofx->account("kmmofx-acc-ref", QString("%1-%2").arg(s.m_strRoutingNumber, s.m_strAccountNumber)).id(); // copy over the securities s.m_listSecurities = pofx->d->m_securitylist; // kDebug(2) << Q_FUNC_INFO << " return 0"; return 0; } int OfxImporterPlugin::ofxSecurityCallback(struct OfxSecurityData data, void* pv) { // kDebug(2) << Q_FUNC_INFO; OfxImporterPlugin* pofx = reinterpret_cast(pv); MyMoneyStatement::Security sec; if (data.unique_id_valid) { sec.m_strId = QString::fromUtf8(data.unique_id); } if (data.secname_valid) { sec.m_strName = QString::fromUtf8(data.secname); } if (data.ticker_valid) { sec.m_strSymbol = QString::fromUtf8(data.ticker); } pofx->d->m_securitylist += sec; return 0; } int OfxImporterPlugin::ofxStatusCallback(struct OfxStatusData data, void * pv) { // kDebug(2) << Q_FUNC_INFO; OfxImporterPlugin* pofx = reinterpret_cast(pv); QString message; // if we got this far, we know we were able to parse the file. // so if it fails after here it can only because there were no actual // accounts in the file! pofx->d->m_fatalerror = i18n("No accounts found."); if (data.ofx_element_name_valid) message.prepend(QString("%1: ").arg(QString::fromUtf8(data.ofx_element_name))); if (data.code_valid) message += QString("%1 (Code %2): %3").arg(QString::fromUtf8(data.name)).arg(data.code).arg(QString::fromUtf8(data.description)); if (data.server_message_valid) message += QString(" (%1)").arg(QString::fromUtf8(data.server_message)); if (data.severity_valid) { switch (data.severity) { case OfxStatusData::INFO: pofx->addInfo(message); break; case OfxStatusData::ERROR: pofx->addError(message); break; case OfxStatusData::WARN: pofx->addWarning(message); break; default: pofx->addWarning(message); pofx->addWarning("Previous message was an unknown type. 'WARNING' was assumed."); break; } } // kDebug(2) << Q_FUNC_INFO << " return 0 "; return 0; } bool OfxImporterPlugin::importStatement(const MyMoneyStatement& s) { qDebug("OfxImporterPlugin::importStatement start"); return statementInterface()->import(s); } MyMoneyAccount OfxImporterPlugin::account(const QString& key, const QString& value) const { return statementInterface()->account(key, value); } void OfxImporterPlugin::protocols(QStringList& protocolList) const { protocolList.clear(); protocolList << "OFX"; } QWidget* OfxImporterPlugin::accountConfigTab(const MyMoneyAccount& acc, QString& name) { name = i18n("Online settings"); d->m_statusDlg = new KOnlineBankingStatus(acc, 0); return d->m_statusDlg; } MyMoneyKeyValueContainer OfxImporterPlugin::onlineBankingSettings(const MyMoneyKeyValueContainer& current) { MyMoneyKeyValueContainer kvp(current); // keep the provider name in sync with the one found in kmm_ofximport.desktop kvp["provider"] = "KMyMoney OFX"; if (d->m_statusDlg) { kvp.deletePair("appId"); kvp.deletePair("kmmofx-headerVersion"); kvp.deletePair("password"); d->m_wallet = openSynchronousWallet(); if (d->m_wallet && (d->m_wallet->hasFolder(KWallet::Wallet::PasswordFolder()) || d->m_wallet->createFolder(KWallet::Wallet::PasswordFolder())) && d->m_wallet->setFolder(KWallet::Wallet::PasswordFolder())) { QString key = OFX_PASSWORD_KEY(kvp.value("url"), kvp.value("uniqueId")); if (d->m_statusDlg->m_storePassword->isChecked()) { d->m_wallet->writePassword(key, d->m_statusDlg->m_password->text()); } else { if (d->m_wallet->hasEntry(key)) { d->m_wallet->removeEntry(key); } } } else { if (d->m_statusDlg->m_storePassword->isChecked()) { kvp.setValue("password", d->m_statusDlg->m_password->text()); } } if (!d->m_statusDlg->appId().isEmpty()) kvp.setValue("appId", d->m_statusDlg->appId()); kvp.setValue("kmmofx-headerVersion", d->m_statusDlg->headerVersion()); kvp.setValue("kmmofx-numRequestDays", QString::number(d->m_statusDlg->m_numdaysSpin->value())); kvp.setValue("kmmofx-todayMinus", QString::number(d->m_statusDlg->m_todayRB->isChecked())); kvp.setValue("kmmofx-lastUpdate", QString::number(d->m_statusDlg->m_lastUpdateRB->isChecked())); kvp.setValue("kmmofx-pickDate", QString::number(d->m_statusDlg->m_pickDateRB->isChecked())); kvp.setValue("kmmofx-specificDate", d->m_statusDlg->m_specificDate->date().toString()); kvp.setValue("kmmofx-preferName", QString::number(d->m_statusDlg->m_preferredPayee->currentIndex())); if (!d->m_statusDlg->m_clientUidEdit->text().isEmpty()) kvp.setValue("clientUid", d->m_statusDlg->m_clientUidEdit->text()); else kvp.deletePair("clientUid"); // get rid of pre 4.6 values kvp.deletePair("kmmofx-preferPayeeid"); } return kvp; } bool OfxImporterPlugin::mapAccount(const MyMoneyAccount& acc, MyMoneyKeyValueContainer& settings) { Q_UNUSED(acc); bool rc = false; QPointer wiz = new KOnlineBankingSetupWizard(0); if (wiz->isInit()) { if (wiz->exec() == QDialog::Accepted) { rc = wiz->chosenSettings(settings); } } delete wiz; return rc; } bool OfxImporterPlugin::updateAccount(const MyMoneyAccount& acc, bool moreAccounts) { Q_UNUSED(moreAccounts); qDebug("OfxImporterPlugin::updateAccount"); try { if (!acc.id().isEmpty()) { // Save the value of preferName to be used by ofxTransactionCallback d->m_preferName = static_cast(acc.onlineBankingSettings().value("kmmofx-preferName").toInt()); QPointer dlg = new KOfxDirectConnectDlg(acc); connect(dlg, SIGNAL(statementReady(QString)), this, SLOT(slotImportFile(QString))); // get the date of the earliest transaction that we are interested in // from the settings for this account MyMoneyKeyValueContainer settings = acc.onlineBankingSettings(); if (!settings.value("provider").isEmpty()) { if ((settings.value("kmmofx-todayMinus").toInt() != 0) && !settings.value("kmmofx-numRequestDays").isEmpty()) { //kDebug(0) << "start date = today minus"; d->m_updateStartDate = QDate::currentDate().addDays(-settings.value("kmmofx-numRequestDays").toInt()); } else if ((settings.value("kmmofx-lastUpdate").toInt() != 0) && !acc.value("lastImportedTransactionDate").isEmpty()) { //kDebug(0) << "start date = last update"; d->m_updateStartDate = QDate::fromString(acc.value("lastImportedTransactionDate"), Qt::ISODate); } else if ((settings.value("kmmofx-pickDate").toInt() != 0) && !settings.value("kmmofx-specificDate").isEmpty()) { //kDebug(0) << "start date = pick date"; d->m_updateStartDate = QDate::fromString(settings.value("kmmofx-specificDate")); } else { //kDebug(0) << "start date = today - 2 months"; d->m_updateStartDate = QDate::currentDate().addMonths(-2); } } //kDebug(0) << "ofx plugin: account" << acc.name() << "earliest transaction date to process =" << qPrintable(d->m_updateStartDate.toString(Qt::ISODate)); if (dlg->init()) dlg->exec(); delete dlg; // reset the earliest-interesting-transaction date to the non-specific account setting d->m_updateStartDate = QDate(1900,1,1); } } catch (const MyMoneyException &e) { KMessageBox::information(0 , i18n("Error connecting to bank: %1", e.what())); } return false; } void OfxImporterPlugin::slotImportFile(const QString& url) { qDebug("OfxImporterPlugin::slotImportFile"); if (!import(url)) { KMessageBox::error(0, QString("%1").arg(i18n("

Unable to import '%1' using the OFX importer plugin. The plugin returned the following error:

%2

", url, lastError())), i18n("Importing error")); } } bool OfxImporterPlugin::storeStatements(QList& statements) { bool hasstatements = (statements.count() > 0); bool ok = true; bool abort = false; // FIXME Deal with warnings/errors coming back from plugins /*if ( ofx.errors().count() ) { if ( KMessageBox::warningContinueCancelList(this,i18n("The following errors were returned from your bank"),ofx.errors(),i18n("OFX Errors")) == KMessageBox::Cancel ) abort = true; } if ( ofx.warnings().count() ) { if ( KMessageBox::warningContinueCancelList(this,i18n("The following warnings were returned from your bank"),ofx.warnings(),i18n("OFX Warnings"),KStandardGuiItem::cont(),"ofxwarnings") == KMessageBox::Cancel ) abort = true; }*/ qDebug("OfxImporterPlugin::storeStatements() with %d statements called", static_cast(statements.count())); QList::const_iterator it_s = statements.constBegin(); while (it_s != statements.constEnd() && !abort) { ok = ok && importStatement((*it_s)); ++it_s; } if (hasstatements && !ok) { KMessageBox::error(0, i18n("Importing process terminated unexpectedly."), i18n("Failed to import all statements.")); } return (!hasstatements || ok); } void OfxImporterPlugin::addnew() { d->m_statementlist.push_back(MyMoneyStatement()); } MyMoneyStatement& OfxImporterPlugin::back() { return d->m_statementlist.back(); } bool OfxImporterPlugin::isValid() const { return d->m_valid; } void OfxImporterPlugin::setValid() { d->m_valid = true; } void OfxImporterPlugin::addInfo(const QString& _msg) { d->m_infos += _msg; } void OfxImporterPlugin::addWarning(const QString& _msg) { d->m_warnings += _msg; } void OfxImporterPlugin::addError(const QString& _msg) { d->m_errors += _msg; } const QStringList& OfxImporterPlugin::infos() const // krazy:exclude=spelling { return d->m_infos; } const QStringList& OfxImporterPlugin::warnings() const { return d->m_warnings; } const QStringList& OfxImporterPlugin::errors() const { return d->m_errors; } diff --git a/kmymoney/plugins/qif/config/mymoneyqifprofile.cpp b/kmymoney/plugins/qif/config/mymoneyqifprofile.cpp index 0500d8f3f..647d56d41 100644 --- a/kmymoney/plugins/qif/config/mymoneyqifprofile.cpp +++ b/kmymoney/plugins/qif/config/mymoneyqifprofile.cpp @@ -1,1002 +1,1003 @@ /*************************************************************************** mymoneyqifprofile.cpp - description ------------------- begin : Tue Dec 24 2002 copyright : (C) 2002 by Thomas Baumgart email : thb@net-bembel.de ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "mymoneyqifprofile.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "mymoneyexception.h" #include "mymoneymoney.h" +#include "mymoneyenums.h" /* * CENTURY_BREAK is used to identfy the century for a two digit year * * if yr is < CENTURY_BREAK it is in 2000 * if yr is >= CENTURY_BREAK it is in 1900 * * so with CENTURY_BREAK being 70 the following will happen: * * 00..69 -> 2000..2069 * 70..99 -> 1970..1999 */ const int CENTURY_BREAK = 70; class MyMoneyQifProfile::Private { public: Private() : m_changeCount(3, 0), m_lastValue(3, 0), m_largestValue(3, 0) { } void getThirdPosition(); void dissectDate(QVector& parts, const QString& txt) const; QVector m_changeCount; QVector m_lastValue; QVector m_largestValue; QMap m_partPos; }; void MyMoneyQifProfile::Private::dissectDate(QVector& parts, const QString& txt) const { QRegExp nonDelimChars("[ 0-9a-zA-Z]"); int part = 0; // the current part we scan int pos; // the current scan position int maxPartSize = txt.length() > 6 ? 4 : 2; // the maximum size of a part // some fu... up MS-Money versions write two delimiter in a row // so we need to keep track of them. Example: D14/12/'08 bool lastWasDelim = false; // separate the parts of the date and keep the locations of the delimiters for (pos = 0; pos < txt.length() && part < 3; ++pos) { if (nonDelimChars.indexIn(txt[pos]) == -1) { if (!lastWasDelim) { ++part; maxPartSize = 0; // make sure to pick the right one depending if next char is numeric or not lastWasDelim = true; } } else { lastWasDelim = false; // check if the part is over and we did not see a delimiter if ((maxPartSize != 0) && (parts[part].length() == maxPartSize)) { ++part; maxPartSize = 0; } if (maxPartSize == 0) { maxPartSize = txt[pos].isDigit() ? 2 : 3; if (part == 2) maxPartSize = 4; } if (part < 3) parts[part] += txt[pos]; } } if (part == 3) { // invalid date for (int i = 0; i < 3; ++i) { parts[i] = '0'; } } } void MyMoneyQifProfile::Private::getThirdPosition() { // if we have detected two parts we can calculate the third and its position if (m_partPos.count() == 2) { QList partsPresent = m_partPos.keys(); QStringList partsAvail = QString("d,m,y").split(','); int missingIndex = -1; int value = 0; for (int i = 0; i < 3; ++i) { if (!partsPresent.contains(partsAvail[i][0])) { missingIndex = i; } else { value += m_partPos[partsAvail[i][0]]; } } m_partPos[partsAvail[missingIndex][0]] = 3 - value; } } MyMoneyQifProfile::MyMoneyQifProfile() : d(new Private), m_isDirty(false) { clear(); } MyMoneyQifProfile::MyMoneyQifProfile(const QString& name) : d(new Private), m_isDirty(false) { loadProfile(name); } MyMoneyQifProfile::~MyMoneyQifProfile() { delete d; } void MyMoneyQifProfile::clear() { m_dateFormat = "%d.%m.%yyyy"; m_apostropheFormat = "2000-2099"; m_valueMode = ""; m_filterScriptImport = ""; m_filterScriptExport = ""; m_filterFileType = "*.qif"; m_decimal.clear(); m_decimal['$'] = m_decimal['Q'] = m_decimal['T'] = m_decimal['O'] = m_decimal['I'] = QLocale().decimalPoint(); m_thousands.clear(); m_thousands['$'] = m_thousands['Q'] = m_thousands['T'] = m_thousands['O'] = m_thousands['I'] = QLocale().groupSeparator(); m_openingBalanceText = "Opening Balance"; m_voidMark = "VOID "; m_accountDelimiter = '['; m_profileName = ""; m_profileDescription = ""; m_profileType = "Bank"; m_attemptMatchDuplicates = true; } void MyMoneyQifProfile::loadProfile(const QString& name) { KSharedConfigPtr config = KSharedConfig::openConfig(); KConfigGroup grp = config->group(name); clear(); m_profileName = name; m_profileDescription = grp.readEntry("Description", m_profileDescription); m_profileType = grp.readEntry("Type", m_profileType); m_dateFormat = grp.readEntry("DateFormat", m_dateFormat); m_apostropheFormat = grp.readEntry("ApostropheFormat", m_apostropheFormat); m_accountDelimiter = grp.readEntry("AccountDelimiter", m_accountDelimiter); m_openingBalanceText = grp.readEntry("OpeningBalance", m_openingBalanceText); m_voidMark = grp.readEntry("VoidMark", m_voidMark); m_filterScriptImport = grp.readEntry("FilterScriptImport", m_filterScriptImport); m_filterScriptExport = grp.readEntry("FilterScriptExport", m_filterScriptExport); m_filterFileType = grp.readEntry("FilterFileType", m_filterFileType); m_attemptMatchDuplicates = grp.readEntry("AttemptMatchDuplicates", m_attemptMatchDuplicates); // make sure, we remove any old stuff for now grp.deleteEntry("FilterScript"); QString tmp = QString(m_decimal['Q']) + m_decimal['T'] + m_decimal['I'] + m_decimal['$'] + m_decimal['O']; tmp = grp.readEntry("Decimal", tmp); m_decimal['Q'] = tmp[0]; m_decimal['T'] = tmp[1]; m_decimal['I'] = tmp[2]; m_decimal['$'] = tmp[3]; m_decimal['O'] = tmp[4]; tmp = QString(m_thousands['Q']) + m_thousands['T'] + m_thousands['I'] + m_thousands['$'] + m_thousands['O']; tmp = grp.readEntry("Thousand", tmp); m_thousands['Q'] = tmp[0]; m_thousands['T'] = tmp[1]; m_thousands['I'] = tmp[2]; m_thousands['$'] = tmp[3]; m_thousands['O'] = tmp[4]; m_isDirty = false; } void MyMoneyQifProfile::saveProfile() { if (m_isDirty == true) { KSharedConfigPtr config = KSharedConfig::openConfig(); KConfigGroup grp = config->group(m_profileName); grp.writeEntry("Description", m_profileDescription); grp.writeEntry("Type", m_profileType); grp.writeEntry("DateFormat", m_dateFormat); grp.writeEntry("ApostropheFormat", m_apostropheFormat); grp.writeEntry("AccountDelimiter", m_accountDelimiter); grp.writeEntry("OpeningBalance", m_openingBalanceText); grp.writeEntry("VoidMark", m_voidMark); grp.writeEntry("FilterScriptImport", m_filterScriptImport); grp.writeEntry("FilterScriptExport", m_filterScriptExport); grp.writeEntry("FilterFileType", m_filterFileType); grp.writeEntry("AttemptMatchDuplicates", m_attemptMatchDuplicates); QString tmp; tmp = QString(m_decimal['Q']) + m_decimal['T'] + m_decimal['I'] + m_decimal['$'] + m_decimal['O']; grp.writeEntry("Decimal", tmp); tmp = QString(m_thousands['Q']) + m_thousands['T'] + m_thousands['I'] + m_thousands['$'] + m_thousands['O']; grp.writeEntry("Thousand", tmp); } m_isDirty = false; } void MyMoneyQifProfile::setProfileName(const QString& name) { if (m_profileName != name) m_isDirty = true; m_profileName = name; } void MyMoneyQifProfile::setProfileDescription(const QString& desc) { if (m_profileDescription != desc) m_isDirty = true; m_profileDescription = desc; } void MyMoneyQifProfile::setProfileType(const QString& type) { if (m_profileType != type) m_isDirty = true; m_profileType = type; } void MyMoneyQifProfile::setOutputDateFormat(const QString& dateFormat) { if (m_dateFormat != dateFormat) m_isDirty = true; m_dateFormat = dateFormat; } void MyMoneyQifProfile::setInputDateFormat(const QString& dateFormat) { int j = -1; if (dateFormat.length() > 0) { for (int i = 0; i < dateFormat.length() - 1; ++i) { if (dateFormat[i] == '%') { d->m_partPos[dateFormat[++i]] = ++j; } } } } void MyMoneyQifProfile::setApostropheFormat(const QString& apostropheFormat) { if (m_apostropheFormat != apostropheFormat) m_isDirty = true; m_apostropheFormat = apostropheFormat; } void MyMoneyQifProfile::setAmountDecimal(const QChar& def, const QChar& chr) { QChar ch(chr); if (ch == QChar()) ch = ' '; if (m_decimal[def] != ch) m_isDirty = true; m_decimal[def] = ch; } void MyMoneyQifProfile::setAmountThousands(const QChar& def, const QChar& chr) { QChar ch(chr); if (ch == QChar()) ch = ' '; if (m_thousands[def] != ch) m_isDirty = true; m_thousands[def] = ch; } const QChar MyMoneyQifProfile::amountDecimal(const QChar& def) const { QChar chr = m_decimal[def]; return chr; } const QChar MyMoneyQifProfile::amountThousands(const QChar& def) const { QChar chr = m_thousands[def]; return chr; } void MyMoneyQifProfile::setAccountDelimiter(const QString& delim) { QString txt(delim); if (txt.isEmpty()) txt = ' '; else if (txt[0] != '[') txt = '['; if (m_accountDelimiter[0] != txt[0]) m_isDirty = true; m_accountDelimiter = txt[0]; } void MyMoneyQifProfile::setOpeningBalanceText(const QString& txt) { if (m_openingBalanceText != txt) m_isDirty = true; m_openingBalanceText = txt; } void MyMoneyQifProfile::setVoidMark(const QString& txt) { if (m_voidMark != txt) m_isDirty = true; m_voidMark = txt; } const QString MyMoneyQifProfile::accountDelimiter() const { QString rc; if (m_accountDelimiter[0] == ' ') rc = ' '; else rc = "[]"; return rc; } const QString MyMoneyQifProfile::date(const QDate& datein) const { QString::const_iterator format = m_dateFormat.begin();; QString buffer; QChar delim; int maskLen; QChar maskChar; while (format != m_dateFormat.end()) { if (*format == '%') { maskLen = 0; maskChar = *(++format); while ((format != m_dateFormat.end()) && (*format == maskChar)) { ++maskLen; ++format; } if (maskChar == 'd') { if (! delim.isNull()) buffer += delim; buffer += QString::number(datein.day()).rightJustified(2, '0'); } else if (maskChar == 'm') { if (! delim.isNull()) buffer += delim; if (maskLen == 3) buffer += QLocale().monthName(datein.month(), QLocale::ShortFormat); else buffer += QString::number(datein.month()).rightJustified(2, '0'); } else if (maskChar == 'y') { if (maskLen == 2) { buffer += twoDigitYear(delim, datein.year()); } else { if (! delim.isNull()) buffer += delim; buffer += QString::number(datein.year()); } break; } else { throw MYMONEYEXCEPTION("Invalid char in QifProfile date field"); } delim = 0; } else { if (! delim.isNull()) buffer += delim; delim = *format++; } } return buffer; } const QDate MyMoneyQifProfile::date(const QString& datein) const { // in case we don't know the format, we return an invalid date if (d->m_partPos.count() != 3) return QDate(); QVector scannedParts(3); d->dissectDate(scannedParts, datein); int yr, mon, day; bool ok; yr = scannedParts[d->m_partPos['y']].toInt(); mon = scannedParts[d->m_partPos['m']].toInt(&ok); if (!ok) { QStringList monthNames = QString("jan,feb,mar,apr,may,jun,jul,aug,sep,oct,nov,dec").split(','); int j; for (j = 1; j <= 12; ++j) { if ((QLocale().monthName(j, QLocale::ShortFormat).toLower() == scannedParts[d->m_partPos['m']].toLower()) || (monthNames[j-1] == scannedParts[d->m_partPos['m']].toLower())) { mon = j; break; } } if (j == 13) { qWarning("Unknown month '%s'", qPrintable(scannedParts[d->m_partPos['m']])); return QDate(); } } day = scannedParts[d->m_partPos['d']].toInt(); if (yr < 100) { // two digit year information? if (yr < CENTURY_BREAK) // less than the CENTURY_BREAK we assume this century yr += 2000; else yr += 1900; } return QDate(yr, mon, day); #if 0 QString scannedDelim[2]; QString formatParts[3]; QString formatDelim[2]; int part; int delim; unsigned int i, j; part = -1; delim = 0; for (i = 0; i < m_dateFormat.length(); ++i) { if (m_dateFormat[i] == '%') { ++part; if (part == 3) { qWarning("MyMoneyQifProfile::date(const QString& datein) Too many parts in date format"); return QDate(); } ++i; } switch (m_dateFormat[i].toLatin1()) { case 'm': case 'd': case 'y': formatParts[part] += m_dateFormat[i]; break; case '/': case '-': case '.': case '\'': if (delim == 2) { qWarning("MyMoneyQifProfile::date(const QString& datein) Too many delimiters in date format"); return QDate(); } formatDelim[delim] = m_dateFormat[i]; ++delim; break; default: qWarning("MyMoneyQifProfile::date(const QString& datein) Invalid char in date format"); return QDate(); } } part = 0; delim = 0; bool prevWasChar = false; for (i = 0; i < datein.length(); ++i) { switch (datein[i].toLatin1()) { case '/': case '.': case '-': case '\'': if (delim == 2) { qWarning("MyMoneyQifProfile::date(const QString& datein) Too many delimiters in date field"); return QDate(); } scannedDelim[delim] = datein[i]; ++delim; ++part; prevWasChar = false; break; default: if (prevWasChar && datein[i].isDigit()) { ++part; prevWasChar = false; } if (datein[i].isLetter()) prevWasChar = true; // replace blank with 0 scannedParts[part] += (datein[i] == ' ') ? QChar('0') : datein[i]; break; } } int day = 1, mon = 1, yr = 1900; bool ok = false; for (i = 0; i < 2; ++i) { if (scannedDelim[i] != formatDelim[i] && scannedDelim[i] != QChar('\'')) { qWarning("MyMoneyQifProfile::date(const QString& datein) Invalid delimiter '%s' when '%s' was expected", scannedDelim[i].toLatin1(), formatDelim[i].toLatin1()); return QDate(); } } QString msg; for (i = 0; i < 3; ++i) { switch (formatParts[i][0].toLatin1()) { case 'd': day = scannedParts[i].toUInt(&ok); if (!ok) msg = "Invalid numeric character in day string"; break; case 'm': if (formatParts[i].length() != 3) { mon = scannedParts[i].toUInt(&ok); if (!ok) msg = "Invalid numeric character in month string"; } else { for (j = 1; j <= 12; ++j) { if (QLocale().monthName(j, 2000, true).toLower() == formatParts[i].toLower()) { mon = j; ok = true; break; } } if (j == 13) { msg = "Unknown month '" + scannedParts[i] + "'"; } } break; case 'y': ok = false; if (scannedParts[i].length() == formatParts[i].length()) { yr = scannedParts[i].toUInt(&ok); if (!ok) msg = "Invalid numeric character in month string"; if (yr < 100) { // two digit year info if (i > 1) { ok = true; if (scannedDelim[i-1] == QChar('\'')) { if (m_apostropheFormat == "1900-1949") { if (yr < 50) yr += 1900; else yr += 2000; } else if (m_apostropheFormat == "1900-1999") { yr += 1900; } else if (m_apostropheFormat == "2000-2099") { yr += 2000; } else { msg = "Unsupported apostropheFormat!"; ok = false; } } else { if (m_apostropheFormat == "1900-1949") { if (yr < 50) yr += 2000; else yr += 1900; } else if (m_apostropheFormat == "1900-1999") { yr += 2000; } else if (m_apostropheFormat == "2000-2099") { yr += 1900; } else { msg = "Unsupported apostropheFormat!"; ok = false; } } } else { msg = "Year as first parameter is not supported!"; } } else if (yr < 1900) { msg = "Year not in range < 100 or >= 1900!"; } else { ok = true; } } else { msg = QString("Length of year (%1) does not match expected length (%2).") .arg(scannedParts[i].length()).arg(formatParts[i].length()); } break; } if (!msg.isEmpty()) { qWarning("MyMoneyQifProfile::date(const QString& datein) %s", msg.toLatin1()); return QDate(); } } return QDate(yr, mon, day); #endif } const QString MyMoneyQifProfile::twoDigitYear(const QChar& delim, int yr) const { QChar realDelim = delim; QString buffer; if (!delim.isNull()) { if ((m_apostropheFormat == "1900-1949" && yr <= 1949) || (m_apostropheFormat == "1900-1999" && yr <= 1999) || (m_apostropheFormat == "2000-2099" && yr >= 2000)) realDelim = '\''; buffer += realDelim; } yr -= 1900; if (yr > 100) yr -= 100; if (yr < 10) buffer += '0'; buffer += QString::number(yr); return buffer; } const QString MyMoneyQifProfile::value(const QChar& def, const MyMoneyMoney& valuein) const { QChar _decimalSeparator; QChar _thousandsSeparator; QString res; _decimalSeparator = MyMoneyMoney::decimalSeparator(); _thousandsSeparator = MyMoneyMoney::thousandSeparator(); - MyMoneyMoney::signPosition _signPosition = MyMoneyMoney::negativeMonetarySignPosition(); + eMyMoney::Money::signPosition _signPosition = MyMoneyMoney::negativeMonetarySignPosition(); MyMoneyMoney::setDecimalSeparator(amountDecimal(def).toLatin1()); MyMoneyMoney::setThousandSeparator(amountThousands(def).toLatin1()); - MyMoneyMoney::setNegativeMonetarySignPosition(MyMoneyMoney::BeforeQuantityMoney); + MyMoneyMoney::setNegativeMonetarySignPosition(eMyMoney::Money::BeforeQuantityMoney); res = valuein.formatMoney("", 2); MyMoneyMoney::setDecimalSeparator(_decimalSeparator); MyMoneyMoney::setThousandSeparator(_thousandsSeparator); MyMoneyMoney::setNegativeMonetarySignPosition(_signPosition); return res; } const MyMoneyMoney MyMoneyQifProfile::value(const QChar& def, const QString& valuein) const { QChar _decimalSeparator; QChar _thousandsSeparator; MyMoneyMoney res; _decimalSeparator = MyMoneyMoney::decimalSeparator(); _thousandsSeparator = MyMoneyMoney::thousandSeparator(); - MyMoneyMoney::signPosition _signPosition = MyMoneyMoney::negativeMonetarySignPosition(); + eMyMoney::Money::signPosition _signPosition = MyMoneyMoney::negativeMonetarySignPosition(); MyMoneyMoney::setDecimalSeparator(amountDecimal(def).toLatin1()); MyMoneyMoney::setThousandSeparator(amountThousands(def).toLatin1()); - MyMoneyMoney::setNegativeMonetarySignPosition(MyMoneyMoney::BeforeQuantityMoney); + MyMoneyMoney::setNegativeMonetarySignPosition(eMyMoney::Money::BeforeQuantityMoney); res = MyMoneyMoney(valuein); MyMoneyMoney::setDecimalSeparator(_decimalSeparator); MyMoneyMoney::setThousandSeparator(_thousandsSeparator); MyMoneyMoney::setNegativeMonetarySignPosition(_signPosition); return res; } void MyMoneyQifProfile::setFilterScriptImport(const QString& script) { if (m_filterScriptImport != script) m_isDirty = true; m_filterScriptImport = script; } void MyMoneyQifProfile::setFilterScriptExport(const QString& script) { if (m_filterScriptExport != script) m_isDirty = true; m_filterScriptExport = script; } void MyMoneyQifProfile::setFilterFileType(const QString& txt) { if (m_filterFileType != txt) m_isDirty = true; m_filterFileType = txt; } void MyMoneyQifProfile::setAttemptMatchDuplicates(bool f) { if (m_attemptMatchDuplicates != f) m_isDirty = true; m_attemptMatchDuplicates = f; } const QString MyMoneyQifProfile::inputDateFormat() const { QStringList list; possibleDateFormats(list); if (list.count() == 1) return list.first(); return QString(); } void MyMoneyQifProfile::possibleDateFormats(QStringList& list) const { QStringList defaultList = QString("y,m,d:y,d,m:m,d,y:m,y,d:d,m,y:d,y,m").split(':'); list.clear(); QStringList::const_iterator it_d; for (it_d = defaultList.constBegin(); it_d != defaultList.constEnd(); ++it_d) { const QStringList parts = (*it_d).split(',', QString::SkipEmptyParts); int i; for (i = 0; i < 3; ++i) { if (d->m_partPos.contains(parts[i][0])) { if (d->m_partPos[parts[i][0]] != i) break; } // months can't be larger than 12 if (parts[i] == "m" && d->m_largestValue[i] > 12) break; // days can't be larger than 31 if (parts[i] == "d" && d->m_largestValue[i] > 31) break; } // matches all tests if (i == 3) { QString format = *it_d; format.replace('y', "%y"); format.replace('m', "%m"); format.replace('d', "%d"); format.replace(',', " "); list << format; } } // if we haven't found any, then there's something wrong. // in this case, we present the full list and let the user decide if (list.count() == 0) { for (it_d = defaultList.constBegin(); it_d != defaultList.constEnd(); ++it_d) { QString format = *it_d; format.replace('y', "%y"); format.replace('m', "%m"); format.replace('d', "%d"); format.replace(',', " "); list << format; } } } void MyMoneyQifProfile::autoDetect(const QStringList& lines) { m_dateFormat.clear(); m_decimal.clear(); m_thousands.clear(); QString numericRecords = "BT$OIQ"; QStringList::const_iterator it; int datesScanned = 0; // section: used to switch between different QIF sections, // because the Record identifiers are ambigous between sections // eg. in transaction records, T identifies a total amount, in // account sections it's the type. // // 0 - unknown // 1 - account // 2 - transactions // 3 - prices int section = 0; QRegExp price("\"(.*)\",(.*),\"(.*)\""); for (it = lines.begin(); it != lines.end(); ++it) { QChar c((*it)[0]); if (c == '!') { QString sname = (*it).toLower(); if (!sname.startsWith(QLatin1String("!option:"))) { section = 0; if (sname.startsWith(QLatin1String("!account"))) section = 1; else if (sname.startsWith(QLatin1String("!type"))) { if (sname.startsWith(QLatin1String("!type:cat")) || sname.startsWith(QLatin1String("!type:payee")) || sname.startsWith(QLatin1String("!type:security")) || sname.startsWith(QLatin1String("!type:class"))) { section = 0; } else if (sname.startsWith(QLatin1String("!type:price"))) { section = 3; } else section = 2; } } } switch (section) { case 1: if (c == 'B') { scanNumeric((*it).mid(1), m_decimal[c], m_thousands[c]); } break; case 2: if (numericRecords.contains(c)) { scanNumeric((*it).mid(1), m_decimal[c], m_thousands[c]); } else if ((c == 'D') && (m_dateFormat.isEmpty())) { if (d->m_partPos.count() != 3) { scanDate((*it).mid(1)); ++datesScanned; if (d->m_partPos.count() == 2) { // if we have detected two parts we can calculate the third and its position d->getThirdPosition(); } } } break; case 3: if (price.indexIn(*it) != -1) { scanNumeric(price.cap(2), m_decimal['P'], m_thousands['P']); scanDate(price.cap(3)); ++datesScanned; } break; } } // the following algorithm is only applied if we have more // than 20 dates found. Smaller numbers have shown that the // results are inaccurate which leads to a reduced number of // date formats presented to choose from. if (d->m_partPos.count() != 3 && datesScanned > 20) { QMap sortedPos; // make sure to reset the known parts for the following algorithm if (d->m_partPos.contains('y')) { d->m_changeCount[d->m_partPos['y']] = -1; for (int i = 0; i < 3; ++i) { if (d->m_partPos['y'] == i) continue; // can we say for sure that we hit the day field? if (d->m_largestValue[i] > 12) { d->m_partPos['d'] = i; } } } if (d->m_partPos.contains('d')) d->m_changeCount[d->m_partPos['d']] = -1; if (d->m_partPos.contains('m')) d->m_changeCount[d->m_partPos['m']] = -1; for (int i = 0; i < 3; ++i) { if (d->m_changeCount[i] != -1) { sortedPos[d->m_changeCount[i]] = i; } } QMap::const_iterator it_a; QMap::const_iterator it_b; switch (sortedPos.count()) { case 1: // all the same // let the user decide, we can't figure it out break; case 2: // two are the same, we treat the largest as the day // if it's 20% larger than the other one and let the // user pick the other two { it_b = sortedPos.constBegin(); it_a = it_b; ++it_b; double a = d->m_changeCount[*it_a]; double b = d->m_changeCount[*it_b]; if (b > (a * 1.2)) { d->m_partPos['d'] = *it_b; } } break; case 3: // three different, we check if they are 20% apart each it_b = sortedPos.constBegin(); for (int i = 0; i < 2; ++i) { it_a = it_b; ++it_b; double a = d->m_changeCount[*it_a]; double b = d->m_changeCount[*it_b]; if (b > (a * 1.2)) { switch (i) { case 0: d->m_partPos['y'] = *it_a; break; case 1: d->m_partPos['d'] = *it_b; break; } } } break; } // extract the last if necessary and possible date position d->getThirdPosition(); } } void MyMoneyQifProfile::scanNumeric(const QString& txt, QChar& decimal, QChar& thousands) const { QChar first, second; QRegExp numericChars("[0-9-()]"); for (int i = 0; i < txt.length(); ++i) { const QChar& c = txt[i]; if (numericChars.indexIn(c) == -1) { if (c == '.' || c == ',') { first = second; second = c; } } } if (!second.isNull()) decimal = second; if (!first.isNull()) thousands = first; } void MyMoneyQifProfile::scanDate(const QString& txt) const { // extract the parts from the txt QVector parts(3); // the various parts of the date d->dissectDate(parts, txt); // now analyze the parts for (int i = 0; i < 3; ++i) { bool ok; int value = parts[i].toInt(&ok); if (!ok) { // this should happen only if the part is non-numeric -> month d->m_partPos['m'] = i; } else if (value != 0) { if (value != d->m_lastValue[i]) { d->m_changeCount[i]++; d->m_lastValue[i] = value; if (value > d->m_largestValue[i]) d->m_largestValue[i] = value; } // if it's > 31 it can only be years if (value > 31) { d->m_partPos['y'] = i; } // and if it's in between 12 and 32 and we already identified the // position for the year it must be days if ((value > 12) && (value < 32) && d->m_partPos.contains('y')) { d->m_partPos['d'] = i; } } } } diff --git a/kmymoney/plugins/qif/import/mymoneyqifreader.cpp b/kmymoney/plugins/qif/import/mymoneyqifreader.cpp index 51c3be06c..8604ad226 100644 --- a/kmymoney/plugins/qif/import/mymoneyqifreader.cpp +++ b/kmymoney/plugins/qif/import/mymoneyqifreader.cpp @@ -1,2072 +1,2073 @@ /*************************************************************************** mymoneyqifreader.cpp ------------------- begin : Mon Jan 27 2003 copyright : (C) 2000-2003 by Michael Edwardes email : mte@users.sourceforge.net Javier Campos Morales Felix Rodriguez John C Thomas Baumgart Kevin Tambascio 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. * * * ***************************************************************************/ #include "mymoneyqifreader.h" // ---------------------------------------------------------------------------- // QT Headers #include #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Headers #include #include #include #include #include #include "kjobwidgets.h" #include "kio/job.h" // ---------------------------------------------------------------------------- // Project Headers #include "mymoneyfile.h" #include "mymoneysecurity.h" #include "mymoneysplit.h" +#include "mymoneyexception.h" #include "kmymoneyglobalsettings.h" #include "mymoneystatement.h" // define this to debug the code. Using external filters // while debugging did not work too good for me, so I added // this code. // #define DEBUG_IMPORT #ifdef DEBUG_IMPORT #ifdef __GNUC__ #warning "DEBUG_IMPORT defined --> external filter not available!!!!!!!" #endif #endif class MyMoneyQifReader::Private { public: Private() : accountType(eMyMoney::Account::Type::Checkings), mapCategories(true) {} const QString accountTypeToQif(eMyMoney::Account::Type type) const; /** * finalize the current statement and add it to the statement list */ void finishStatement(); bool isTransfer(QString& name, const QString& leftDelim, const QString& rightDelim); /** * Converts the QIF specific N-record of investment transactions into * a category name */ const QString typeToAccountName(const QString& type) const; /** * Converts the QIF reconcile state to the KMyMoney reconcile state */ eMyMoney::Split::State reconcileState(const QString& state) const; /** */ void fixMultiLineMemo(QString& memo) const; public: /** * the statement that is currently collected/processed */ MyMoneyStatement st; /** * the list of all statements to be sent to MyMoneyStatementReader */ QList statements; /** * a list of already used hashes in this file */ QMap m_hashMap; QString st_AccountName; QString st_AccountId; eMyMoney::Account::Type accountType; bool firstTransaction; bool mapCategories; MyMoneyQifReader::QifEntryTypeE transactionType; }; void MyMoneyQifReader::Private::fixMultiLineMemo(QString& memo) const { memo.replace("\\n", "\n"); } void MyMoneyQifReader::Private::finishStatement() { // in case we have collected any data in the statement, we keep it if ((st.m_listTransactions.count() + st.m_listPrices.count() + st.m_listSecurities.count()) > 0) { statements += st; qDebug("Statement with %d transactions, %d prices and %d securities added to the statement list", st.m_listTransactions.count(), st.m_listPrices.count(), st.m_listSecurities.count()); } eMyMoney::Statement::Type type = st.m_eType; //stash type and... // start with a fresh statement st = MyMoneyStatement(); st.m_skipCategoryMatching = !mapCategories; st.m_eType = type; } const QString MyMoneyQifReader::Private::accountTypeToQif(eMyMoney::Account::Type type) const { QString rc = "Bank"; switch (type) { default: break; case eMyMoney::Account::Type::Cash: rc = "Cash"; break; case eMyMoney::Account::Type::CreditCard: rc = "CCard"; break; case eMyMoney::Account::Type::Asset: rc = "Oth A"; break; case eMyMoney::Account::Type::Liability: rc = "Oth L"; break; case eMyMoney::Account::Type::Investment: rc = "Port"; break; } return rc; } const QString MyMoneyQifReader::Private::typeToAccountName(const QString& type) const { if (type == "reinvint") return i18nc("Category name", "Reinvested interest"); if (type == "reinvdiv") return i18nc("Category name", "Reinvested dividend"); if (type == "reinvlg") return i18nc("Category name", "Reinvested dividend (long term)"); if (type == "reinvsh") return i18nc("Category name", "Reinvested dividend (short term)"); if (type == "div") return i18nc("Category name", "Dividend"); if (type == "intinc") return i18nc("Category name", "Interest"); if (type == "cgshort") return i18nc("Category name", "Capital Gain (short term)"); if (type == "cgmid") return i18nc("Category name", "Capital Gain (mid term)"); if (type == "cglong") return i18nc("Category name", "Capital Gain (long term)"); if (type == "rtrncap") return i18nc("Category name", "Returned capital"); if (type == "miscinc") return i18nc("Category name", "Miscellaneous income"); if (type == "miscexp") return i18nc("Category name", "Miscellaneous expense"); if (type == "sell" || type == "buy") return i18nc("Category name", "Investment fees"); return i18n("Unknown QIF type %1", type); } bool MyMoneyQifReader::Private::isTransfer(QString& tmp, const QString& leftDelim, const QString& rightDelim) { // it's a transfer, extract the account name // I've seen entries like this // // S[Mehrwertsteuer]/_VATCode_N_I (The '/' is the Quicken class symbol) // // so extracting is a bit more complex and we use a regexp for it QRegExp exp(QString("\\%1(.*)\\%2(.*)").arg(leftDelim, rightDelim)); bool rc; if ((rc = (exp.indexIn(tmp) != -1)) == true) { tmp = exp.cap(1) + exp.cap(2); tmp = tmp.trimmed(); } return rc; } eMyMoney::Split::State MyMoneyQifReader::Private::reconcileState(const QString& state) const { if (state == "X" || state == "R") // Reconciled return eMyMoney::Split::State::Reconciled; if (state == "*") // Cleared return eMyMoney::Split::State::Cleared; return eMyMoney::Split::State::NotReconciled; } MyMoneyQifReader::MyMoneyQifReader() : d(new Private) { m_skipAccount = false; m_transactionsProcessed = m_transactionsSkipped = 0; m_progressCallback = 0; m_file = 0; m_entryType = EntryUnknown; m_processingData = false; m_userAbort = false; m_warnedInvestment = false; m_warnedSecurity = false; m_warnedPrice = false; connect(&m_filter, SIGNAL(bytesWritten(qint64)), this, SLOT(slotSendDataToFilter())); connect(&m_filter, SIGNAL(readyReadStandardOutput()), this, SLOT(slotReceivedDataFromFilter())); connect(&m_filter, SIGNAL(finished(int,QProcess::ExitStatus)), this, SLOT(slotImportFinished())); connect(&m_filter, SIGNAL(readyReadStandardError()), this, SLOT(slotReceivedErrorFromFilter())); } MyMoneyQifReader::~MyMoneyQifReader() { delete m_file; delete d; } void MyMoneyQifReader::setCategoryMapping(bool map) { d->mapCategories = map; } void MyMoneyQifReader::setURL(const QUrl &url) { m_url = url; } void MyMoneyQifReader::setProfile(const QString& profile) { m_qifProfile.loadProfile("Profile-" + profile); } void MyMoneyQifReader::slotSendDataToFilter() { long len; if (m_file->atEnd()) { m_filter.closeWriteChannel(); } else { len = m_file->read(m_buffer, sizeof(m_buffer)); if (len == -1) { qWarning("Failed to read block from QIF import file"); m_filter.closeWriteChannel(); m_filter.kill(); } else { m_filter.write(m_buffer, len); } } } void MyMoneyQifReader::slotReceivedErrorFromFilter() { qWarning("%s", qPrintable(QString(m_filter.readAllStandardError()))); } void MyMoneyQifReader::slotReceivedDataFromFilter() { parseReceivedData(m_filter.readAllStandardOutput()); } void MyMoneyQifReader::parseReceivedData(const QByteArray& data) { const char* buff = data.data(); int len = data.length(); m_pos += len; // signalProgress(m_pos, 0); while (len) { // process char if (*buff == '\n' || *buff == '\r') { // found EOL if (!m_lineBuffer.isEmpty()) { m_qifLines << QString::fromUtf8(m_lineBuffer.trimmed()); } m_lineBuffer = QByteArray(); } else { // collect all others m_lineBuffer += (*buff); } ++buff; --len; } } void MyMoneyQifReader::slotImportFinished() { // check if the last EOL char was missing and add the trailing line if (!m_lineBuffer.isEmpty()) { m_qifLines << QString::fromUtf8(m_lineBuffer.trimmed()); } qDebug("Read %ld bytes", m_pos); QTimer::singleShot(0, this, SLOT(slotProcessData())); } void MyMoneyQifReader::slotProcessData() { signalProgress(-1, -1); // scan the file and try to determine numeric and date formats m_qifProfile.autoDetect(m_qifLines); // the detection is accurate for numeric values, but it could be // that the dates were too ambiguous so that we have to let the user // decide which one to pick. QStringList dateFormats; m_qifProfile.possibleDateFormats(dateFormats); QString format; if (dateFormats.count() > 1) { bool ok; format = QInputDialog::getItem(0, i18n("Date format selection"), i18n("Pick the date format that suits your input file"), dateFormats, 05, false, &ok); if (!ok) { m_userAbort = true; } } else format = dateFormats.first(); if (!format.isEmpty()) { m_qifProfile.setInputDateFormat(format); qDebug("Selected date format: '%s'", qPrintable(format)); } else { // cancel the process because there is probably nothing to work with m_userAbort = true; } signalProgress(0, m_qifLines.count(), i18n("Importing QIF...")); QStringList::iterator it; for (it = m_qifLines.begin(); m_userAbort == false && it != m_qifLines.end(); ++it) { ++m_linenumber; // qDebug("Proc: '%s'", (*it).data()); if ((*it).startsWith('!')) { processQifSpecial(*it); m_qifEntry.clear(); } else if (*it == "^") { if (m_qifEntry.count() > 0) { signalProgress(m_linenumber, 0); processQifEntry(); m_qifEntry.clear(); } } else { m_qifEntry += *it; } } d->finishStatement(); qDebug("%d lines processed", m_linenumber); signalProgress(-1, -1); emit statementsReady(d->statements); } bool MyMoneyQifReader::startImport() { bool rc = false; d->st = MyMoneyStatement(); d->st.m_skipCategoryMatching = !d->mapCategories; m_dontAskAgain.clear(); m_accountTranslation.clear(); m_userAbort = false; m_pos = 0; m_linenumber = 0; m_filename.clear(); m_data.clear(); if (m_url.isEmpty()) { return rc; } else if (m_url.isLocalFile()) { m_filename = m_url.toLocalFile(); } else { m_filename = QDir::tempPath(); if(!m_filename.endsWith(QDir::separator())) m_filename += QDir::separator(); m_filename += m_url.fileName(); qDebug() << "Source:" << m_url.toDisplayString() << "Destination:" << m_filename; KIO::FileCopyJob *job = KIO::file_copy(m_url, QUrl::fromUserInput(m_filename), -1, KIO::Overwrite); // KJobWidgets::setWindow(job, kmymoney); job->exec(); if (job->error()) { KMessageBox::detailedError(0, i18n("Error while loading file '%1'.", m_url.toDisplayString()), job->errorString(), i18n("File access error")); return rc; } } m_file = new QFile(m_filename); if (m_file->open(QIODevice::ReadOnly)) { #ifdef DEBUG_IMPORT qint64 len; while (!m_file->atEnd()) { len = m_file->read(m_buffer, sizeof(m_buffer)); if (len == -1) { qWarning("Failed to read block from QIF import file"); } else { parseReceivedData(QByteArray(m_buffer, len)); } } QTimer::singleShot(0, this, SLOT(slotImportFinished())); rc = true; #else QString program; QStringList arguments; program.clear(); arguments.clear(); // start filter process, use 'cat -' as the default filter if (m_qifProfile.filterScriptImport().isEmpty()) { #ifdef Q_OS_WIN32 //krazy:exclude=cpp // this is the Windows equivalent of 'cat -' but since 'type' does not work with stdin // we pass the filename converted to native separators as a parameter program = "cmd.exe"; arguments << "/c"; arguments << "type"; arguments << QDir::toNativeSeparators(m_filename); #else program = "cat"; arguments << "-"; #endif } else { arguments << m_qifProfile.filterScriptImport().split(' ', QString::KeepEmptyParts); } m_entryType = EntryUnknown; m_filter.setProcessChannelMode(QProcess::MergedChannels); m_filter.start(program, arguments); if (m_filter.waitForStarted()) { signalProgress(0, m_file->size(), i18n("Reading QIF...")); slotSendDataToFilter(); rc = true; // emit statementsReady(d->statements); } else { KMessageBox::detailedError(0, i18n("Error while running the filter '%1'.", m_filter.program()), m_filter.errorString(), i18n("Filter error")); } #endif } return rc; } void MyMoneyQifReader::processQifSpecial(const QString& _line) { QString line = _line.mid(1); // get rid of exclamation mark if (line.left(5).toLower() == QString("type:")) { line = line.mid(5); // exportable accounts if (line.toLower() == "ccard" || KMyMoneyGlobalSettings::qifCreditCard().toLower().contains(line.toLower())) { d->accountType = eMyMoney::Account::Type::CreditCard; d->firstTransaction = true; d->transactionType = m_entryType = EntryTransaction; } else if (line.toLower() == "bank" || KMyMoneyGlobalSettings::qifBank().toLower().contains(line.toLower())) { d->accountType = eMyMoney::Account::Type::Checkings; d->firstTransaction = true; d->transactionType = m_entryType = EntryTransaction; } else if (line.toLower() == "cash" || KMyMoneyGlobalSettings::qifCash().toLower().contains(line.toLower())) { d->accountType = eMyMoney::Account::Type::Cash; d->firstTransaction = true; d->transactionType = m_entryType = EntryTransaction; } else if (line.toLower() == "oth a" || KMyMoneyGlobalSettings::qifAsset().toLower().contains(line.toLower())) { d->accountType = eMyMoney::Account::Type::Asset; d->firstTransaction = true; d->transactionType = m_entryType = EntryTransaction; } else if (line.toLower() == "oth l" || line.toLower() == i18nc("QIF tag for liability account", "Oth L").toLower()) { d->accountType = eMyMoney::Account::Type::Liability; d->firstTransaction = true; d->transactionType = m_entryType = EntryTransaction; } else if (line.toLower() == "invst" || line.toLower() == i18nc("QIF tag for investment account", "Invst").toLower()) { d->accountType = eMyMoney::Account::Type::Investment; d->transactionType = m_entryType = EntryInvestmentTransaction; } else if (line.toLower() == "invoice" || KMyMoneyGlobalSettings::qifInvoice().toLower().contains(line.toLower())) { m_entryType = EntrySkip; } else if (line.toLower() == "tax") { m_entryType = EntrySkip; } else if (line.toLower() == "bill") { m_entryType = EntrySkip; // exportable lists } else if (line.toLower() == "cat" || line.toLower() == i18nc("QIF tag for category", "Cat").toLower()) { m_entryType = EntryCategory; } else if (line.toLower() == "security" || line.toLower() == i18nc("QIF tag for security", "Security").toLower()) { m_entryType = EntrySecurity; } else if (line.toLower() == "prices" || line.toLower() == i18nc("QIF tag for prices", "Prices").toLower()) { m_entryType = EntryPrice; } else if (line.toLower() == "payee") { m_entryType = EntryPayee; } else if (line.toLower() == "memorized") { m_entryType = EntryMemorizedTransaction; } else if (line.toLower() == "class" || line.toLower() == i18nc("QIF tag for a class", "Class").toLower()) { m_entryType = EntryClass; } else if (line.toLower() == "budget") { m_entryType = EntrySkip; } else if (line.toLower() == "invitem") { m_entryType = EntrySkip; } else if (line.toLower() == "template") { m_entryType = EntrySkip; } else { qWarning("Unknown type code '%s' in QIF file on line %d", qPrintable(line), m_linenumber); m_entryType = EntrySkip; } // option headers } else if (line.toLower() == "account") { m_entryType = EntryAccount; } else if (line.toLower() == "option:autoswitch") { m_entryType = EntryAccount; } else if (line.toLower() == "clear:autoswitch") { m_entryType = d->transactionType; } } void MyMoneyQifReader::processQifEntry() { // This method processes a 'QIF Entry' which is everything between two caret // signs // try { switch (m_entryType) { case EntryCategory: processCategoryEntry(); break; case EntryUnknown: qDebug() << "Line " << m_linenumber << ": Warning: Found an entry without a type being specified. Checking assumed."; processTransactionEntry(); break; case EntryTransaction: processTransactionEntry(); break; case EntryInvestmentTransaction: processInvestmentTransactionEntry(); break; case EntryAccount: processAccountEntry(); break; case EntrySecurity: processSecurityEntry(); break; case EntryPrice: processPriceEntry(); break; case EntryPayee: processPayeeEntry(); break; case EntryClass: qDebug() << "Line " << m_linenumber << ": Classes are not yet supported!"; break; case EntryMemorizedTransaction: qDebug() << "Line " << m_linenumber << ": Memorized transactions are not yet implemented!"; break; case EntrySkip: break; default: qDebug() << "Line " << m_linenumber << ": EntryType " << m_entryType << " not yet implemented!"; break; } } catch (const MyMoneyException &e) { if (e.what() != "USERABORT") { qDebug() << "Line " << m_linenumber << ": Unhandled error: " << e.what(); } else { m_userAbort = true; } } } const QString MyMoneyQifReader::extractLine(const QChar& id, int cnt) { QStringList::ConstIterator it; m_extractedLine = -1; for (it = m_qifEntry.constBegin(); it != m_qifEntry.constEnd(); ++it) { ++m_extractedLine; if ((*it)[0] == id) { if (cnt-- == 1) { return (*it).mid(1); } } } m_extractedLine = -1; return QString(); } bool MyMoneyQifReader::extractSplits(QList& listqSplits) const { // *** With apologies to QString MyMoneyQifReader::extractLine *** QStringList::ConstIterator it; bool ret = false; bool memoPresent = false; int neededCount = 0; qSplit q; for (it = m_qifEntry.constBegin(); it != m_qifEntry.constEnd(); ++it) { if (((*it)[0] == 'S') || ((*it)[0] == '$') || ((*it)[0] == 'E')) { memoPresent = false; // in case no memo line in this split if ((*it)[0] == 'E') { q.m_strMemo = (*it).mid(1); // 'E' = Memo d->fixMultiLineMemo(q.m_strMemo); memoPresent = true; // This transaction contains memo } else if ((*it)[0] == 'S') { q.m_strCategoryName = (*it).mid(1); // 'S' = CategoryName neededCount ++; } else if ((*it)[0] == '$') { q.m_amount = (*it).mid(1); // '$' = Amount neededCount ++; } if (neededCount > 1) { // CategoryName & Amount essential listqSplits += q; // Add valid split if (!memoPresent) { // If no memo, clear previous q.m_strMemo.clear(); } qSplit q; // Start new split neededCount = 0; ret = true; } } } return ret; } #if 0 void MyMoneyQifReader::processMSAccountEntry(const eMyMoney::Account::Type accountType) { if (extractLine('P').toLower() == m_qifProfile.openingBalanceText().toLower()) { m_account = MyMoneyAccount(); m_account.setAccountType(accountType); QString txt = extractLine('T'); MyMoneyMoney balance = m_qifProfile.value('T', txt); QDate date = m_qifProfile.date(extractLine('D')); m_account.setOpeningDate(date); QString name = extractLine('L'); if (name.left(1) == m_qifProfile.accountDelimiter().left(1)) { name = name.mid(1, name.length() - 2); } d->st_AccountName = name; m_account.setName(name); selectOrCreateAccount(Select, m_account, balance); d->st.m_accountId = m_account.id(); if (! balance.isZero()) { MyMoneyFile* file = MyMoneyFile::instance(); QString openingtxid = file->openingBalanceTransaction(m_account); MyMoneyFileTransaction ft; if (! openingtxid.isEmpty()) { MyMoneyTransaction openingtx = file->transaction(openingtxid); MyMoneySplit split = openingtx.splitByAccount(m_account.id()); if (split.shares() != balance) { const MyMoneySecurity& sec = file->security(m_account.currencyId()); if (KMessageBox::questionYesNo( KMyMoneyUtils::mainWindow(), i18n("The %1 account currently has an opening balance of %2. This QIF file reports an opening balance of %3. Would you like to overwrite the current balance with the one from the QIF file?", m_account.name(), split.shares().formatMoney(m_account, sec), balance.formatMoney(m_account, sec)), i18n("Overwrite opening balance"), KStandardGuiItem::yes(), KStandardGuiItem::no(), "OverwriteOpeningBalance") == KMessageBox::Yes) { file->removeTransaction(openingtx); m_account.setOpeningDate(date); file->createOpeningBalanceTransaction(m_account, balance); } } } else { // Add an opening balance m_account.setOpeningDate(date); file->createOpeningBalanceTransaction(m_account, balance); } ft.commit(); } } else { // for some unknown reason, Quicken 2001 generates the following (somewhat // misleading) sequence of lines: // // 1: !Account // 2: NAT&T Universal // 3: DAT&T Univers(...xxxx) [CLOSED] // 4: TCCard // 5: ^ // 6: !Type:CCard // 7: !Account // 8: NCFCU Visa // 9: DRick's CFCU Visa card (...xxxx) // 10: TCCard // 11: ^ // 12: !Type:CCard // 13: D1/ 4' 1 // // Lines 1-5 are processed via processQifEntry() and processAccountEntry() // Then Quicken issues line 6 but since the account does not carry any // transaction does not write an end delimiter. Arrrgh! So we end up with // a QIF entry comprising of lines 6-11 and end up in this routine. Actually, // lines 7-11 are the leadin for the next account. So we check here if // the !Type:xxx record also contains an !Account line and process the // entry as required. // // (Ace) I think a better solution here is to handle exclamation point // lines separately from entries. In the above case: // Line 1 would set the mode to "account entries". // Lines 2-5 would be interpreted as an account entry. This would set m_account. // Line 6 would set the mode to "cc transaction entries". // Line 7 would immediately set the mode to "account entries" again // Lines 8-11 would be interpreted as an account entry. This would set m_account. // Line 12 would set the mode to "cc transaction entries" // Lines 13+ would be interpreted as cc transaction entries, and life is good int exclamationCnt = 1; QString category; do { category = extractLine('!', exclamationCnt++); } while (!category.isEmpty() && category != "Account"); // we have such a weird empty account if (category == "Account") { processAccountEntry(); } else { selectOrCreateAccount(Select, m_account); d->st_AccountName = m_account.name(); d->st.m_strAccountName = m_account.name(); d->st.m_accountId = m_account.id(); d->st.m_strAccountNumber = m_account.id(); m_account.setNumber(m_account.id()); if (m_entryType == EntryInvestmentTransaction) processInvestmentTransactionEntry(); else processTransactionEntry(); } } } #endif void MyMoneyQifReader::processPayeeEntry() { // TODO } void MyMoneyQifReader::processCategoryEntry() { MyMoneyFile* file = MyMoneyFile::instance(); MyMoneyAccount account = MyMoneyAccount(); account.setName(extractLine('N')); account.setDescription(extractLine('D')); MyMoneyAccount parentAccount; //The extractline routine will more than likely return 'empty', // so also have to test that either the 'I' or 'E' was detected //and set up accounts accordingly. if ((!extractLine('I').isEmpty()) || (m_extractedLine != -1)) { account.setAccountType(eMyMoney::Account::Type::Income); parentAccount = file->income(); } else if ((!extractLine('E').isEmpty()) || (m_extractedLine != -1)) { account.setAccountType(eMyMoney::Account::Type::Expense); parentAccount = file->expense(); } // check if we can find the account already in the file auto acc = findAccount(account, MyMoneyAccount()); // if not, we just create it if (acc.id().isEmpty()) { MyMoneyAccount brokerage; file->createAccount(account, parentAccount, brokerage, MyMoneyMoney()); } } MyMoneyAccount MyMoneyQifReader::findAccount(const MyMoneyAccount& acc, const MyMoneyAccount& parent) const { static MyMoneyAccount nullAccount; MyMoneyFile* file = MyMoneyFile::instance(); QList parents; try { // search by id if (!acc.id().isEmpty()) { return file->account(acc.id()); } // collect the parents. in case parent does not have an id, we scan the all top-level accounts if (parent.id().isEmpty()) { parents << file->asset(); parents << file->liability(); parents << file->income(); parents << file->expense(); parents << file->equity(); } else { parents << parent; } QList::const_iterator it_p; for (it_p = parents.constBegin(); it_p != parents.constEnd(); ++it_p) { MyMoneyAccount parentAccount = *it_p; // search by name (allow hierarchy) int pos; // check for ':' in the name and use it as separator for a hierarchy QString name = acc.name(); bool notFound = false; while ((pos = name.indexOf(MyMoneyFile::AccountSeperator)) != -1) { QString part = name.left(pos); QString remainder = name.mid(pos + 1); const auto existingAccount = file->subAccountByName(parentAccount, part); // if account has not been found, continue with next top level parent if (existingAccount.id().isEmpty()) { notFound = true; break; } parentAccount = existingAccount; name = remainder; } if (notFound) continue; const auto existingAccount = file->subAccountByName(parentAccount, name); if (!existingAccount.id().isEmpty()) { if (acc.accountType() != eMyMoney::Account::Type::Unknown) { if (acc.accountType() != existingAccount.accountType()) continue; } return existingAccount; } } } catch (const MyMoneyException &e) { KMessageBox::error(0, i18n("Unable to find account: %1", e.what())); } return nullAccount; } const QString MyMoneyQifReader::transferAccount(const QString& name, bool useBrokerage) { QString accountId; QStringList tmpEntry = m_qifEntry; // keep temp copies MyMoneyAccount tmpAccount = m_account; m_qifEntry.clear(); // and construct a temp entry to create/search the account m_qifEntry << QString("N%1").arg(name); m_qifEntry << QString("Tunknown"); m_qifEntry << QString("D%1").arg(i18n("Autogenerated by QIF importer")); accountId = processAccountEntry(false); // in case we found a reference to an investment account, we need // to switch to the brokerage account instead. MyMoneyAccount acc = MyMoneyFile::instance()->account(accountId); if (useBrokerage && (acc.accountType() == eMyMoney::Account::Type::Investment)) { m_qifEntry.clear(); // and construct a temp entry to create/search the account m_qifEntry << QString("N%1").arg(acc.brokerageName()); m_qifEntry << QString("Tunknown"); m_qifEntry << QString("D%1").arg(i18n("Autogenerated by QIF importer")); accountId = processAccountEntry(false); } m_qifEntry = tmpEntry; // restore local copies m_account = tmpAccount; return accountId; } void MyMoneyQifReader::createOpeningBalance(eMyMoney::Account::Type accType) { MyMoneyFile* file = MyMoneyFile::instance(); // if we don't have a name for the current account we need to extract the name from the L-record if (m_account.name().isEmpty()) { QString name = extractLine('L'); if (name.isEmpty()) { name = i18n("QIF imported, no account name supplied"); } d->isTransfer(name, m_qifProfile.accountDelimiter().left(1), m_qifProfile.accountDelimiter().mid(1, 1)); QStringList entry = m_qifEntry; // keep a temp copy m_qifEntry.clear(); // and construct a temp entry to create/search the account m_qifEntry << QString("N%1").arg(name); m_qifEntry << QString("T%1").arg(d->accountTypeToQif(accType)); m_qifEntry << QString("D%1").arg(i18n("Autogenerated by QIF importer")); processAccountEntry(); m_qifEntry = entry; // restore local copy } MyMoneyFileTransaction ft; try { bool needCreate = true; MyMoneyAccount acc = m_account; // in case we're dealing with an investment account, we better use // the accompanying brokerage account for the opening balance acc = file->accountByName(m_account.brokerageName()); // check if we already have an opening balance transaction QString tid = file->openingBalanceTransaction(acc); MyMoneyTransaction ot; if (!tid.isEmpty()) { ot = file->transaction(tid); MyMoneySplit s0 = ot.splitByAccount(acc.id()); // if the value is the same, we can silently skip this transaction if (s0.shares() == m_qifProfile.value('T', extractLine('T'))) { needCreate = false; } if (needCreate) { // in case we create it anyway, we issue a warning to the user to check it manually KMessageBox::sorry(0, QString("%1").arg(i18n("KMyMoney has imported a second opening balance transaction into account %1 which differs from the one found already on file. Please correct this manually once the import is done.", acc.name())), i18n("Opening balance problem")); } } if (needCreate) { acc.setOpeningDate(m_qifProfile.date(extractLine('D'))); file->modifyAccount(acc); MyMoneyTransaction t = file->createOpeningBalanceTransaction(acc, m_qifProfile.value('T', extractLine('T'))); if (!t.id().isEmpty()) { t.setImported(); file->modifyTransaction(t); } ft.commit(); } // make sure to use the updated version of the account if (m_account.id() == acc.id()) m_account = acc; // remember which account we created d->st.m_accountId = m_account.id(); } catch (const MyMoneyException &e) { KMessageBox::detailedError(0, i18n("Error while creating opening balance transaction"), QString("%1(%2):%3").arg(e.file()).arg(e.line()).arg(e.what()), i18n("File access error")); } } void MyMoneyQifReader::processTransactionEntry() { ++m_transactionsProcessed; // in case the user selected to skip the account or the account // was not found we skip this transaction /* if(m_account.id().isEmpty()) { m_transactionsSkipped++; return; } */ MyMoneyFile* file = MyMoneyFile::instance(); MyMoneyStatement::Split s1; MyMoneyStatement::Transaction tr; QString tmp; QString accountId; int pos; QString payee = extractLine('P'); unsigned long h; h = MyMoneyTransaction::hash(m_qifEntry.join(";")); QString hashBase; hashBase.sprintf("%s-%07lx", qPrintable(m_qifProfile.date(extractLine('D')).toString(Qt::ISODate)), h); int idx = 1; QString hash; for (;;) { hash = QString("%1-%2").arg(hashBase).arg(idx); QMap::const_iterator it; it = d->m_hashMap.constFind(hash); if (it == d->m_hashMap.constEnd()) { d->m_hashMap[hash] = true; break; } ++idx; } tr.m_strBankID = hash; if (d->firstTransaction) { // check if this is an opening balance transaction and process it out of the statement if (!payee.isEmpty() && ((payee.toLower() == "opening balance") || KMyMoneyGlobalSettings::qifOpeningBalance().toLower().contains(payee.toLower()))) { createOpeningBalance(d->accountType); d->firstTransaction = false; return; } } // Process general transaction data if (d->st.m_accountId.isEmpty()) d->st.m_accountId = m_account.id(); s1.m_accountId = d->st.m_accountId; switch (d->accountType) { case eMyMoney::Account::Type::Checkings: d->st.m_eType=eMyMoney::Statement::Type::Checkings; break; case eMyMoney::Account::Type::Savings: d->st.m_eType=eMyMoney::Statement::Type::Savings; break; case eMyMoney::Account::Type::Investment: d->st.m_eType=eMyMoney::Statement::Type::Investment; break; case eMyMoney::Account::Type::CreditCard: d->st.m_eType=eMyMoney::Statement::Type::CreditCard; break; default: d->st.m_eType=eMyMoney::Statement::Type::None; break; } tr.m_datePosted = (m_qifProfile.date(extractLine('D'))); if (!tr.m_datePosted.isValid()) { int rc = KMessageBox::warningContinueCancel(0, i18n("The date entry \"%1\" read from the file cannot be interpreted through the current " "date profile setting of \"%2\".\n\nPressing \"Continue\" will " "assign todays date to the transaction. Pressing \"Cancel\" will abort " "the import operation. You can then restart the import and select a different " "QIF profile or create a new one.", extractLine('D'), m_qifProfile.inputDateFormat()), i18n("Invalid date format")); switch (rc) { case KMessageBox::Continue: tr.m_datePosted = (QDate::currentDate()); break; case KMessageBox::Cancel: throw MYMONEYEXCEPTION("USERABORT"); break; } } tmp = extractLine('L'); pos = tmp.lastIndexOf("--"); if (tmp.left(1) == m_qifProfile.accountDelimiter().left(1)) { // it's a transfer, so we wipe the memo // tmp = ""; why?? // st.m_strAccountName = tmp; } else if (pos != -1) { // what's this? // t.setValue("Dialog", tmp.mid(pos+2)); tmp = tmp.left(pos); } // t.setMemo(tmp); // Assign the "#" field to the transaction's bank id // This is the custom KMM extension to QIF for a unique ID tmp = extractLine('#'); if (!tmp.isEmpty()) { tr.m_strBankID = QString("ID %1").arg(tmp); } #if 0 // Collect data for the account's split s1.m_accountId = m_account.id(); tmp = extractLine('S'); pos = tmp.findRev("--"); if (pos != -1) { tmp = tmp.left(pos); } if (tmp.left(1) == m_qifProfile.accountDelimiter().left(1)) // it's a transfer, extract the account name tmp = tmp.mid(1, tmp.length() - 2); s1.m_strCategoryName = tmp; #endif // TODO (Ace) Deal with currencies more gracefully. QIF cannot deal with multiple // currencies, so we should assume that transactions imported into a given // account are in THAT ACCOUNT's currency. If one of those involves a transfer // to an account with a different currency, value and shares should be // different. (Shares is in the target account's currency, value is in the // transaction's) s1.m_amount = m_qifProfile.value('T', extractLine('T')); tr.m_amount = m_qifProfile.value('T', extractLine('T')); tr.m_shares = m_qifProfile.value('T', extractLine('T')); tmp = extractLine('N'); if (!tmp.isEmpty()) tr.m_strNumber = tmp; if (!payee.isEmpty()) { tr.m_strPayee = payee; } tr.m_reconcile = d->reconcileState(extractLine('C')); tr.m_strMemo = extractLine('M'); d->fixMultiLineMemo(tr.m_strMemo); s1.m_strMemo = tr.m_strMemo; // tr.m_listSplits.append(s1); // split transaction // ****** ensure each field is ****** // * attached to correct split * QList listqSplits; if (! extractSplits(listqSplits)) { MyMoneyAccount account; // use the same values for the second split, but clear the ID and reverse the value MyMoneyStatement::Split s2 = s1; s2.m_reconcile = tr.m_reconcile; s2.m_amount = (-s1.m_amount); // s2.clearId(); // standard transaction tmp = extractLine('L'); if (d->isTransfer(tmp, m_qifProfile.accountDelimiter().left(1), m_qifProfile.accountDelimiter().mid(1, 1))) { accountId = transferAccount(tmp, false); } else { /* pos = tmp.findRev("--"); if(pos != -1) { t.setValue("Dialog", tmp.mid(pos+2)); tmp = tmp.left(pos); }*/ // it's an expense / income tmp = tmp.trimmed(); accountId = file->checkCategory(tmp, s1.m_amount, s2.m_amount); } if (!accountId.isEmpty()) { try { MyMoneyAccount account = file->account(accountId); // FIXME: check that the type matches and ask if not if (account.accountType() == eMyMoney::Account::Type::Investment) { qDebug() << "Line " << m_linenumber << ": Cannot transfer to/from an investment account. Transaction ignored."; return; } if (account.id() == m_account.id()) { qDebug() << "Line " << m_linenumber << ": Cannot transfer to the same account. Transfer ignored."; accountId.clear(); } } catch (const MyMoneyException &) { qDebug() << "Line " << m_linenumber << ": Account with id " << accountId.data() << " not found"; accountId.clear(); } } if (!accountId.isEmpty()) { s2.m_accountId = accountId; s2.m_strCategoryName = tmp; tr.m_listSplits.append(s2); } } else { int count; for (count = 1; count <= listqSplits.count(); ++count) { // Use true splits count MyMoneyStatement::Split s2 = s1; s2.m_amount = (-m_qifProfile.value('$', listqSplits[count-1].m_amount)); // Amount of split s2.m_strMemo = listqSplits[count-1].m_strMemo; // Memo in split tmp = listqSplits[count-1].m_strCategoryName; // Category in split if (d->isTransfer(tmp, m_qifProfile.accountDelimiter().left(1), m_qifProfile.accountDelimiter().mid(1, 1))) { accountId = transferAccount(tmp, false); } else { pos = tmp.lastIndexOf("--"); if (pos != -1) { tmp = tmp.left(pos); } tmp = tmp.trimmed(); accountId = file->checkCategory(tmp, s1.m_amount, s2.m_amount); } if (!accountId.isEmpty()) { try { MyMoneyAccount account = file->account(accountId); // FIXME: check that the type matches and ask if not if (account.accountType() == eMyMoney::Account::Type::Investment) { qDebug() << "Line " << m_linenumber << ": Cannot convert a split transfer to/from an investment account. Split removed. Total amount adjusted from " << tr.m_amount.formatMoney("", 2) << " to " << (tr.m_amount + s2.m_amount).formatMoney("", 2) << "\n"; tr.m_amount += s2.m_amount; continue; } if (account.id() == m_account.id()) { qDebug() << "Line " << m_linenumber << ": Cannot transfer to the same account. Transfer ignored."; accountId.clear(); } } catch (const MyMoneyException &) { qDebug() << "Line " << m_linenumber << ": Account with id " << accountId.data() << " not found"; accountId.clear(); } } if (!accountId.isEmpty()) { s2.m_accountId = accountId; s2.m_strCategoryName = tmp; tr.m_listSplits += s2; // in case the transaction does not have a memo and we // process the first split just copy the memo over if (tr.m_listSplits.count() == 1 && tr.m_strMemo.isEmpty()) tr.m_strMemo = s2.m_strMemo; } else { // TODO add an option to create a "Unassigned" category // for now, we just drop the split which will show up as unbalanced // transaction in the KMyMoney ledger view } } } // Add the transaction to the statement d->st.m_listTransactions += tr; } void MyMoneyQifReader::processInvestmentTransactionEntry() { // qDebug() << "Investment Transaction:" << m_qifEntry.count() << " lines"; /* Items for Investment Accounts Field Indicator Explanation D Date N Action Y Security (NAME, not symbol) I Price Q Quantity (number of shares or split ratio) T Transaction amount C Cleared status P Text in the first line for transfers and reminders (Payee) M Memo O Commission L Account for the transfer $ Amount transferred ^ End of the entry It will be presumed all transactions are to the associated cash account, if one exists, unless otherwise noted by the 'L' field. Expense/Income categories will be automatically generated, "_Dividend", "_InterestIncome", etc. */ MyMoneyStatement::Transaction tr; d->st.m_eType = eMyMoney::Statement::Type::Investment; // t.setCommodity(m_account.currencyId()); // 'D' field: Date QDate date = m_qifProfile.date(extractLine('D')); if (date.isValid()) tr.m_datePosted = date; else { int rc = KMessageBox::warningContinueCancel(0, i18n("The date entry \"%1\" read from the file cannot be interpreted through the current " "date profile setting of \"%2\".\n\nPressing \"Continue\" will " "assign todays date to the transaction. Pressing \"Cancel\" will abort " "the import operation. You can then restart the import and select a different " "QIF profile or create a new one.", extractLine('D'), m_qifProfile.inputDateFormat()), i18n("Invalid date format")); switch (rc) { case KMessageBox::Continue: tr.m_datePosted = QDate::currentDate(); break; case KMessageBox::Cancel: throw MYMONEYEXCEPTION("USERABORT"); break; } } // 'M' field: Memo QString memo = extractLine('M'); d->fixMultiLineMemo(memo); tr.m_strMemo = memo; unsigned long h; h = MyMoneyTransaction::hash(m_qifEntry.join(";")); QString hashBase; hashBase.sprintf("%s-%07lx", qPrintable(m_qifProfile.date(extractLine('D')).toString(Qt::ISODate)), h); int idx = 1; QString hash; for (;;) { hash = QString("%1-%2").arg(hashBase).arg(idx); QMap::const_iterator it; it = d->m_hashMap.constFind(hash); if (it == d->m_hashMap.constEnd()) { d->m_hashMap[hash] = true; break; } ++idx; } tr.m_strBankID = hash; // '#' field: BankID QString tmp = extractLine('#'); if (! tmp.isEmpty()) tr.m_strBankID = QString("ID %1").arg(tmp); // Reconciliation flag tr.m_reconcile = d->reconcileState(extractLine('C')); // 'O' field: Fees tr.m_fees = m_qifProfile.value('T', extractLine('O')); // 'T' field: Amount MyMoneyMoney amount = m_qifProfile.value('T', extractLine('T')); tr.m_amount = amount; MyMoneyStatement::Price price; price.m_date = date; price.m_strSecurity = extractLine('Y'); price.m_amount = m_qifProfile.value('T', extractLine('I')); #if 0 // we must check for that later, because certain activities don't need a security // 'Y' field: Security name QString securityname = extractLine('Y').toLower(); if (securityname.isEmpty()) { qDebug() << "Line " << m_linenumber << ": Investment transaction without a security is not supported."; return; } tr.m_strSecurity = securityname; #endif #if 0 // For now, we let the statement reader take care of that. // The big problem here is that the Y field is not the SYMBOL, it's the NAME. // The name is not very unique, because people could have used slightly different // abbreviations or ordered words differently, etc. // // If there is a perfect name match with a subordinate stock account, great. // More likely, we have to rely on the QIF file containing !Type:Security // records, which tell us the mapping from name to symbol. // // Therefore, generally it is not recommended to import a QIF file containing // investment transactions but NOT containing security records. QString securitysymbol = m_investmentMap[securityname]; // the correct account is the stock account which matches two criteria: // (1) it is a sub-account of the selected investment account, and either // (2a) the security name of the transaction matches the name of the security, OR // (2b) the security name of the transaction maps to a symbol which matches the symbol of the security // search through each subordinate account bool found = false; MyMoneyAccount thisaccount = m_account; QStringList accounts = thisaccount.accountList(); QStringList::const_iterator it_account = accounts.begin(); while (!found && it_account != accounts.end()) { QString currencyid = file->account(*it_account).currencyId(); MyMoneySecurity security = file->security(currencyid); QString symbol = security.tradingSymbol().toLower(); QString name = security.name().toLower(); if (securityname == name || securitysymbol == symbol) { d->st_AccountId = *it_account; s1.m_accountId = *it_account; thisaccount = file->account(*it_account); found = true; #if 0 // update the price, while we're here. in the future, this should be // an option QString basecurrencyid = file->baseCurrency().id(); MyMoneyPrice price = file->price(currencyid, basecurrencyid, t_in.m_datePosted, true); if (!price.isValid()) { MyMoneyPrice newprice(currencyid, basecurrencyid, t_in.m_datePosted, t_in.m_moneyAmount / t_in.m_dShares, i18n("Statement Importer")); file->addPrice(newprice); } #endif } ++it_account; } if (!found) { qDebug() << "Line " << m_linenumber << ": Security " << securityname << " not found in this account. Transaction ignored."; // If the security is not known, notify the user // TODO (Ace) A "SelectOrCreateAccount" interface for investments KMessageBox::information(0, i18n("This investment account does not contain the \"%1\" security. " "Transactions involving this security will be ignored.", securityname), i18n("Security not found"), QString("MissingSecurity%1").arg(securityname.trimmed())); return; } #endif // 'Y' field: Security tr.m_strSecurity = extractLine('Y'); // 'Q' field: Quantity MyMoneyMoney quantity = m_qifProfile.value('T', extractLine('Q')); // 'N' field: Action QString action = extractLine('N').toLower(); // remove trailing X, which seems to have no purpose (?!) bool xAction = false; if (action.endsWith('x')) { action = action.left(action.length() - 1); xAction = true; } tmp = extractLine('L'); // if the action ends in an X, the L-Record contains the asset account // to which the dividend should be transferred. In the other cases, it // may contain a category that identifies the income category for the // dividend payment if ((xAction == true) || (d->isTransfer(tmp, m_qifProfile.accountDelimiter().left(1), m_qifProfile.accountDelimiter().mid(1, 1)) == true)) { tmp = tmp.remove(QRegExp("[\\[\\]]")); // xAction != true so ignore any'[ and ]' if (!tmp.isEmpty()) { // use 'L' record name tr.m_strBrokerageAccount = tmp; transferAccount(tmp); // make sure the account exists } else { tr.m_strBrokerageAccount = m_account.brokerageName();// use brokerage account transferAccount(m_account.brokerageName()); // make sure the account exists } } else { tmp = tmp.remove(QRegExp("[\\[\\]]")); // xAction != true so ignore any'[ and ]' tr.m_strInterestCategory = tmp; tr.m_strBrokerageAccount = m_account.brokerageName(); } // Whether to create a cash split for the other side of the value QString accountname; //= extractLine('L'); if (action == "reinvint" || action == "reinvdiv" || action == "reinvlg" || action == "reinvsh") { d->st.m_listPrices += price; tr.m_shares = quantity; tr.m_eAction = (eMyMoney::Transaction::Action::ReinvestDividend); tr.m_price = m_qifProfile.value('I', extractLine('I')); tr.m_strInterestCategory = extractLine('L'); if (tr.m_strInterestCategory.isEmpty()) { tr.m_strInterestCategory = d->typeToAccountName(action); } } else if (action == "div" || action == "cgshort" || action == "cgmid" || action == "cglong" || action == "rtrncap") { tr.m_eAction = (eMyMoney::Transaction::Action::CashDividend); // make sure, we have valid category. Either taken from the L-Record above, // or derived from the action code if (tr.m_strInterestCategory.isEmpty()) { tr.m_strInterestCategory = d->typeToAccountName(action); } // For historic reasons (coming from the OFX importer) the statement // reader expects the dividend with a reverse sign. So we just do that. tr.m_amount -= tr.m_fees; // We need an extra split which will be the zero-amount investment split // that serves to mark this transaction as a cash dividend and note which // stock account it belongs to. MyMoneyStatement::Split s2; s2.m_amount = MyMoneyMoney(); s2.m_strCategoryName = extractLine('Y'); tr.m_listSplits.append(s2); } else if (action == "intinc" || action == "miscinc" || action == "miscexp") { tr.m_eAction = (eMyMoney::Transaction::Action::Interest); if (action == "miscexp") tr.m_eAction = (eMyMoney::Transaction::Action::Fees); // make sure, we have a valid category. Either taken from the L-Record above, // or derived from the action code if (tr.m_strInterestCategory.isEmpty()) { tr.m_strInterestCategory = d->typeToAccountName(action); } if (action == "intinc") { MyMoneyMoney price = m_qifProfile.value('I', extractLine('I')); tr.m_amount -= tr.m_fees; if ((!quantity.isZero()) && (!price.isZero())) tr.m_amount = -(quantity * price); } else // For historic reasons (coming from the OFX importer) the statement // reader expects the dividend with a reverse sign. So we just do that. if (action != "miscexp") tr.m_amount = -(amount - tr.m_fees); if (tr.m_strMemo.isEmpty()) tr.m_strMemo = (QString("%1 %2").arg(extractLine('Y')).arg(d->typeToAccountName(action))).trimmed(); } else if (action == "xin" || action == "xout") { QString payee = extractLine('P'); if (!payee.isEmpty() && ((payee.toLower() == "opening balance") || KMyMoneyGlobalSettings::qifOpeningBalance().toLower().contains(payee.toLower()))) { createOpeningBalance(eMyMoney::Account::Type::Investment); return; } tr.m_eAction = (eMyMoney::Transaction::Action::None); MyMoneyStatement::Split s2; QString tmp = extractLine('L'); if (d->isTransfer(tmp, m_qifProfile.accountDelimiter().left(1), m_qifProfile.accountDelimiter().mid(1, 1))) { s2.m_accountId = transferAccount(tmp); s2.m_strCategoryName = tmp; } else { s2.m_strCategoryName = extractLine('L'); if (tr.m_strInterestCategory.isEmpty()) { s2.m_strCategoryName = d->typeToAccountName(action); } } if (action == "xout") tr.m_amount = -tr.m_amount; s2.m_amount = -tr.m_amount; tr.m_listSplits.append(s2); } else if (action == "buy") { d->st.m_listPrices += price; tr.m_price = m_qifProfile.value('I', extractLine('I')); tr.m_shares = quantity; tr.m_amount = -amount; tr.m_eAction = (eMyMoney::Transaction::Action::Buy); } else if (action == "sell") { d->st.m_listPrices += price; tr.m_price = m_qifProfile.value('I', extractLine('I')); tr.m_shares = -quantity; tr.m_amount = amount; tr.m_eAction = (eMyMoney::Transaction::Action::Sell); } else if (action == "shrsin") { tr.m_shares = quantity; tr.m_eAction = (eMyMoney::Transaction::Action::Shrsin); } else if (action == "shrsout") { tr.m_shares = -quantity; tr.m_eAction = (eMyMoney::Transaction::Action::Shrsout); } else if (action == "stksplit") { MyMoneyMoney splitfactor = (quantity / MyMoneyMoney(10, 1)).reduce(); // Stock splits not supported // qDebug() << "Line " << m_linenumber << ": Stock split not supported (date=" << date << " security=" << securityname << " factor=" << splitfactor.toString() << ")"; // s1.setShares(splitfactor); // s1.setValue(0); // s1.setAction(MyMoneySplit::ActionSplitShares); // return; } else { // Unsupported action type qDebug() << "Line " << m_linenumber << ": Unsupported transaction action (" << action << ")"; return; } d->st.m_strAccountName = accountname; // accountname appears not to get set d->st.m_listTransactions += tr; /************************************************************************* * * These transactions are natively supported by KMyMoney * *************************************************************************/ /* D1/ 3' 5 NShrsIn YGENERAL MOTORS CORP 52BR1 I20 Q200 U4,000.00 T4,000.00 M200 shares added to account @ $20/share ^ */ /* ^ D1/14' 5 NShrsOut YTEMPLETON GROWTH 97GJ0 Q50 90 ^ */ /* D1/28' 5 NBuy YGENERAL MOTORS CORP 52BR1 I24.35 Q100 U2,435.00 T2,435.00 ^ */ /* D1/ 5' 5 NSell YUnited Vanguard I8.41 Q50 U420.50 T420.50 ^ */ /* D1/ 7' 5 NReinvDiv YFRANKLIN INCOME 97GM2 I38 Q1 U38.00 T38.00 ^ */ /************************************************************************* * * These transactions are all different kinds of income. (Anything that * follows the DNYUT pattern). They are all handled the same, the only * difference is which income account the income is placed into. By * default, it's placed into _xxx where xxx is the right side of the * N field. e.g. NDiv transaction goes into the _Div account * *************************************************************************/ /* D1/10' 5 NDiv YTEMPLETON GROWTH 97GJ0 U10.00 T10.00 ^ */ /* D1/10' 5 NIntInc YTEMPLETON GROWTH 97GJ0 U20.00 T20.00 ^ */ /* D1/10' 5 NCGShort YTEMPLETON GROWTH 97GJ0 U111.00 T111.00 ^ */ /* D1/10' 5 NCGLong YTEMPLETON GROWTH 97GJ0 U333.00 T333.00 ^ */ /* D1/10' 5 NCGMid YTEMPLETON GROWTH 97GJ0 U222.00 T222.00 ^ */ /* D2/ 2' 5 NRtrnCap YFRANKLIN INCOME 97GM2 U1,234.00 T1,234.00 ^ */ /************************************************************************* * * These transactions deal with miscellaneous activity that KMyMoney * does not support, but may support in the future. * *************************************************************************/ /* Note the Q field is the split ratio per 10 shares, so Q12.5 is a 12.5:10 split, otherwise known as 5:4. D1/14' 5 NStkSplit YIBM Q12.5 ^ */ /************************************************************************* * * These transactions deal with short positions and options, which are * not supported at all by KMyMoney. They will be ignored for now. * There may be a way to hack around this, by creating a new security * "IBM_Short". * *************************************************************************/ /* D1/21' 5 NShtSell YIBM I92.38 Q100 U9,238.00 T9,238.00 ^ */ /* D1/28' 5 NCvrShrt YIBM I92.89 Q100 U9,339.00 T9,339.00 O50.00 ^ */ /* D6/ 1' 5 NVest YIBM Option Q20 ^ */ /* D6/ 8' 5 NExercise YIBM Option I60.952381 Q20 MFrom IBM Option Grant 6/1/2004 ^ */ /* D6/ 1'14 NExpire YIBM Option Q5 ^ */ /************************************************************************* * * These transactions do not have an associated investment ("Y" field) * so presumably they are only valid for the cash account. Once I * understand how these are really implemented, they can probably be * handled without much trouble. * *************************************************************************/ /* D1/14' 5 NCash U-100.00 T-100.00 LBank Chrg ^ */ /* D1/15' 5 NXOut U500.00 T500.00 L[CU Savings] $500.00 ^ */ /* D1/28' 5 NXIn U1,000.00 T1,000.00 L[CU Checking] $1,000.00 ^ */ /* D1/25' 5 NMargInt U25.00 T25.00 ^ */ } const QString MyMoneyQifReader::findOrCreateIncomeAccount(const QString& searchname) { QString result; MyMoneyFile *file = MyMoneyFile::instance(); // First, try to find this account as an income account MyMoneyAccount acc = file->income(); QStringList list = acc.accountList(); QStringList::ConstIterator it_accid = list.constBegin(); while (it_accid != list.constEnd()) { acc = file->account(*it_accid); if (acc.name() == searchname) { result = *it_accid; break; } ++it_accid; } // If we did not find the account, now we must create one. if (result.isEmpty()) { MyMoneyAccount acc; acc.setName(searchname); acc.setAccountType(eMyMoney::Account::Type::Income); MyMoneyAccount income = file->income(); MyMoneyFileTransaction ft; file->addAccount(acc, income); ft.commit(); result = acc.id(); } return result; } // TODO (Ace) Combine this and the previous function const QString MyMoneyQifReader::findOrCreateExpenseAccount(const QString& searchname) { QString result; MyMoneyFile *file = MyMoneyFile::instance(); // First, try to find this account as an income account MyMoneyAccount acc = file->expense(); QStringList list = acc.accountList(); QStringList::ConstIterator it_accid = list.constBegin(); while (it_accid != list.constEnd()) { acc = file->account(*it_accid); if (acc.name() == searchname) { result = *it_accid; break; } ++it_accid; } // If we did not find the account, now we must create one. if (result.isEmpty()) { MyMoneyAccount acc; acc.setName(searchname); acc.setAccountType(eMyMoney::Account::Type::Expense); MyMoneyFileTransaction ft; MyMoneyAccount expense = file->expense(); file->addAccount(acc, expense); ft.commit(); result = acc.id(); } return result; } const QString MyMoneyQifReader::processAccountEntry(bool resetAccountId) { MyMoneyFile* file = MyMoneyFile::instance(); MyMoneyAccount account; QString tmp; account.setName(extractLine('N')); // qDebug("Process account '%s'", account.name().data()); account.setDescription(extractLine('D')); tmp = extractLine('$'); if (tmp.length() > 0) account.setValue("lastStatementBalance", tmp); tmp = extractLine('/'); if (tmp.length() > 0) account.setValue("lastStatementDate", m_qifProfile.date(tmp).toString("yyyy-MM-dd")); QifEntryTypeE transactionType = EntryTransaction; QString type = extractLine('T').toLower().remove(QRegExp("\\s+")); if (type == m_qifProfile.profileType().toLower().remove(QRegExp("\\s+"))) { account.setAccountType(eMyMoney::Account::Type::Checkings); } else if (type == "ccard" || type == "creditcard") { account.setAccountType(eMyMoney::Account::Type::CreditCard); } else if (type == "cash") { account.setAccountType(eMyMoney::Account::Type::Cash); } else if (type == "otha") { account.setAccountType(eMyMoney::Account::Type::Asset); } else if (type == "othl") { account.setAccountType(eMyMoney::Account::Type::Liability); } else if (type == "invst" || type == "port") { account.setAccountType(eMyMoney::Account::Type::Investment); transactionType = EntryInvestmentTransaction; } else if (type == "mutual") { // stock account w/o umbrella investment account account.setAccountType(eMyMoney::Account::Type::Stock); transactionType = EntryInvestmentTransaction; } else if (type == "unknown") { // don't do anything with the type, leave it unknown } else { account.setAccountType(eMyMoney::Account::Type::Checkings); qDebug() << "Line " << m_linenumber << ": Unknown account type '" << type << "', checkings assumed"; } // check if we can find the account already in the file auto acc = findAccount(account, MyMoneyAccount()); if (acc.id().isEmpty()) { // in case the account is not found by name and the type is // unknown, we have to assume something and create a checking account. // this might be wrong, but we have no choice at this point. if (account.accountType() == eMyMoney::Account::Type::Unknown) account.setAccountType(eMyMoney::Account::Type::Checkings); MyMoneyAccount parentAccount; MyMoneyAccount brokerage; // in case it's a stock account, we need to setup a fix investment account if (account.isInvest()) { acc.setName(i18n("%1 (Investment)", account.name())); // use the same name for the investment account acc.setDescription(i18n("Autogenerated by QIF importer from type Mutual account entry")); acc.setAccountType(eMyMoney::Account::Type::Investment); parentAccount = file->asset(); file->createAccount(acc, parentAccount, brokerage, MyMoneyMoney()); parentAccount = acc; qDebug("We still need to create the stock account in MyMoneyQifReader::processAccountEntry()"); } else { // setup parent according the type of the account switch (account.accountGroup()) { case eMyMoney::Account::Type::Asset: default: parentAccount = file->asset(); break; case eMyMoney::Account::Type::Liability: parentAccount = file->liability(); break; case eMyMoney::Account::Type::Equity: parentAccount = file->equity(); break; } } // investment accounts will receive a brokerage account, as KMyMoney // currently does not allow to store funds in the investment account directly // but only create it (not here, but later) if it is needed if (account.accountType() == eMyMoney::Account::Type::Investment) { brokerage.setName(QString()); // brokerage name empty so account not created yet brokerage.setAccountType(eMyMoney::Account::Type::Checkings); brokerage.setCurrencyId(MyMoneyFile::instance()->baseCurrency().id()); } file->createAccount(account, parentAccount, brokerage, MyMoneyMoney()); acc = account; // qDebug("Account created"); } else { // qDebug("Existing account found"); } if (resetAccountId) { // possibly start a new statement d->finishStatement(); m_account = acc; d->st.m_accountId = m_account.id(); // needed here for account selection d->transactionType = transactionType; } return acc.id(); } void MyMoneyQifReader::setProgressCallback(void(*callback)(int, int, const QString&)) { m_progressCallback = callback; } void MyMoneyQifReader::signalProgress(int current, int total, const QString& msg) { if (m_progressCallback != 0) (*m_progressCallback)(current, total, msg); } void MyMoneyQifReader::processPriceEntry() { /* !Type:Prices "IBM",141 9/16,"10/23/98" ^ !Type:Prices "GMW",21.28," 3/17' 5" ^ !Type:Prices "GMW",71652181.001,"67/128/ 0" ^ Note that Quicken will often put in a price with a bogus date and number. We will ignore prices with bogus dates. Hopefully that will catch all of these. Also note that prices can be in fractional units, e.g. 141 9/16. */ QStringList::const_iterator it_line = m_qifEntry.constBegin(); // Make a price for each line QRegExp priceExp("\"(.*)\",(.*),\"(.*)\""); while (it_line != m_qifEntry.constEnd()) { if (priceExp.indexIn(*it_line) != -1) { MyMoneyStatement::Price price; price.m_strSecurity = priceExp.cap(1); QString pricestr = priceExp.cap(2); QString datestr = priceExp.cap(3); qDebug() << "Price:" << price.m_strSecurity << " / " << pricestr << " / " << datestr; // Only add the price if the date is valid. If invalid, fail silently. See note above. // Also require the price value to not have any slashes. Old prices will be something like // "25 9/16", which we do not support. So we'll skip the price for now. QDate date = m_qifProfile.date(datestr); MyMoneyMoney rate(m_qifProfile.value('P', pricestr)); if (date.isValid() && !rate.isZero()) { price.m_amount = rate; price.m_date = date; d->st.m_listPrices += price; } } ++it_line; } } void MyMoneyQifReader::processSecurityEntry() { /* !Type:Security NVANGUARD 500 INDEX SVFINX TMutual Fund ^ */ MyMoneyStatement::Security security; security.m_strName = extractLine('N'); security.m_strSymbol = extractLine('S'); d->st.m_listSecurities += security; } diff --git a/kmymoney/reports/pivottable.cpp b/kmymoney/reports/pivottable.cpp index edf709b42..a9567f6ae 100644 --- a/kmymoney/reports/pivottable.cpp +++ b/kmymoney/reports/pivottable.cpp @@ -1,2342 +1,2343 @@ /*************************************************************************** pivottable.cpp ------------------- begin : Mon May 17 2004 copyright : (C) 2004-2005 by Ace Jones email : Thomas Baumgart Alvaro Soliverez ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "pivottable.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include // ---------------------------------------------------------------------------- // Project Includes #include "pivotgrid.h" #include "reportdebug.h" #include "kreportchartview.h" #include "kmymoneyglobalsettings.h" #include "kmymoneyutils.h" #include "mymoneyforecast.h" #include "mymoneyprice.h" #include "mymoneyfile.h" #include "mymoneysecurity.h" #include "mymoneybudget.h" #include "mymoneyreport.h" #include "mymoneyschedule.h" #include "mymoneysplit.h" #include "mymoneytransaction.h" +#include "mymoneyexception.h" #include "mymoneyenums.h" namespace KChart { class Widget; } namespace reports { using KChart::Widget; QString Debug::m_sTabs; bool Debug::m_sEnabled = DEBUG_ENABLED_BY_DEFAULT; QString Debug::m_sEnableKey; Debug::Debug(const QString& _name): m_methodName(_name), m_enabled(m_sEnabled) { if (!m_enabled && _name == m_sEnableKey) m_enabled = true; if (m_enabled) { qDebug("%s%s(): ENTER", qPrintable(m_sTabs), qPrintable(m_methodName)); m_sTabs.append("--"); } } Debug::~Debug() { if (m_enabled) { m_sTabs.remove(0, 2); qDebug("%s%s(): EXIT", qPrintable(m_sTabs), qPrintable(m_methodName)); if (m_methodName == m_sEnableKey) m_enabled = false; } } void Debug::output(const QString& _text) { if (m_enabled) qDebug("%s%s(): %s", qPrintable(m_sTabs), qPrintable(m_methodName), qPrintable(_text)); } PivotTable::PivotTable(const MyMoneyReport& _report): ReportTable(_report), m_runningSumsCalculated(false) { init(); } void PivotTable::init() { DEBUG_ENTER(Q_FUNC_INFO); // // Initialize locals // MyMoneyFile* file = MyMoneyFile::instance(); // // Initialize member variables // //make sure we have all subaccounts of investment accounts includeInvestmentSubAccounts(); m_config.validDateRange(m_beginDate, m_endDate); // If we need to calculate running sums, it does not make sense // to show a row total column if (m_config.isRunningSum()) m_config.setShowingRowTotals(false); if (m_config.isRunningSum() && !m_config.isIncludingPrice() && !m_config.isIncludingAveragePrice() && !m_config.isIncludingMovingAverage()) m_startColumn = 1; else m_startColumn = 0; m_numColumns = columnValue(m_endDate) - columnValue(m_beginDate) + 1 + m_startColumn; // 1 for m_beginDate values and m_startColumn for opening balance values //Load what types of row the report is going to show loadRowTypeList(); // // Initialize outer groups of the grid // if (m_config.rowType() == MyMoneyReport::eAssetLiability) { m_grid.insert(MyMoneyAccount::accountTypeToString(eMyMoney::Account::Type::Asset), PivotOuterGroup(m_numColumns)); m_grid.insert(MyMoneyAccount::accountTypeToString(eMyMoney::Account::Type::Liability), PivotOuterGroup(m_numColumns, PivotOuterGroup::m_kDefaultSortOrder, true /* inverted */)); } else { m_grid.insert(MyMoneyAccount::accountTypeToString(eMyMoney::Account::Type::Income), PivotOuterGroup(m_numColumns, PivotOuterGroup::m_kDefaultSortOrder - 2)); m_grid.insert(MyMoneyAccount::accountTypeToString(eMyMoney::Account::Type::Expense), PivotOuterGroup(m_numColumns, PivotOuterGroup::m_kDefaultSortOrder - 1, true /* inverted */)); // // Create rows for income/expense reports with all accounts included // if (m_config.isIncludingUnusedAccounts()) createAccountRows(); } // // Initialize grid totals // m_grid.m_total = PivotGridRowSet(m_numColumns); // // Get opening balances // Only net worth report qualifies if (m_startColumn == 1) calculateOpeningBalances(); // // Calculate budget mapping // (for budget reports only) // if (m_config.hasBudget()) calculateBudgetMapping(); // prices report doesn't need transactions, but it needs account stub // otherwise fillBasePriceUnit won't do nothing if (m_config.isIncludingPrice() || m_config.isIncludingAveragePrice()) { QList accounts; file->accountList(accounts); foreach (const auto acc, accounts) { if (acc.isInvest()) { const ReportAccount repAcc(acc); if (m_config.includes(repAcc)) { const auto outergroup = acc.accountTypeToString(acc.accountType()); assignCell(outergroup, repAcc, 0, MyMoneyMoney(), false, false); // add account stub } } } } else { // // Populate all transactions into the row/column pivot grid // QList transactions; m_config.setReportAllSplits(false); m_config.setConsiderCategory(true); try { transactions = file->transactionList(m_config); } catch (const MyMoneyException &e) { qDebug("ERR: %s thrown in %s(%ld)", qPrintable(e.what()), qPrintable(e.file()), e.line()); throw e; } DEBUG_OUTPUT(QString("Found %1 matching transactions").arg(transactions.count())); // Include scheduled transactions if required if (m_config.isIncludingSchedules()) { // Create a custom version of the report filter, excluding date // We'll use this to compare the transaction against MyMoneyTransactionFilter schedulefilter(m_config); schedulefilter.setDateFilter(QDate(), QDate()); // Get the real dates from the config filter QDate configbegin, configend; m_config.validDateRange(configbegin, configend); QList schedules = file->scheduleList(); QList::const_iterator it_schedule = schedules.constBegin(); while (it_schedule != schedules.constEnd()) { // If the transaction meets the filter MyMoneyTransaction tx = (*it_schedule).transaction(); if (!(*it_schedule).isFinished() && schedulefilter.match(tx)) { // Keep the id of the schedule with the transaction so that // we can do the autocalc later on in case of a loan payment tx.setValue("kmm-schedule-id", (*it_schedule).id()); // Get the dates when a payment will be made within the report window QDate nextpayment = (*it_schedule).adjustedNextPayment(configbegin); if (nextpayment.isValid()) { // Add one transaction for each date QList paymentDates = (*it_schedule).paymentDates(nextpayment, configend); QList::const_iterator it_date = paymentDates.constBegin(); while (it_date != paymentDates.constEnd()) { //if the payment occurs in the past, enter it tomorrow if (QDate::currentDate() >= *it_date) { tx.setPostDate(QDate::currentDate().addDays(1)); } else { tx.setPostDate(*it_date); } if (tx.postDate() <= configend && tx.postDate() >= configbegin) { transactions += tx; } DEBUG_OUTPUT(QString("Added transaction for schedule %1 on %2").arg((*it_schedule).id()).arg((*it_date).toString())); ++it_date; } } } ++it_schedule; } } // whether asset & liability transactions are actually to be considered // transfers bool al_transfers = (m_config.rowType() == MyMoneyReport::eExpenseIncome) && (m_config.isIncludingTransfers()); //this is to store balance for loan accounts when not included in the report QMap loanBalances; QList::const_iterator it_transaction = transactions.constBegin(); int colofs = columnValue(m_beginDate) - m_startColumn; while (it_transaction != transactions.constEnd()) { MyMoneyTransaction tx = (*it_transaction); QDate postdate = tx.postDate(); if (postdate < m_beginDate) { qDebug("MyMoneyFile::transactionList returned a transaction that is outside the date filter, skipping it"); ++it_transaction; continue; } int column = columnValue(postdate) - colofs; // check if we need to call the autocalculation routine if (tx.isLoanPayment() && tx.hasAutoCalcSplit() && (tx.value("kmm-schedule-id").length() > 0)) { // make sure to consider any autocalculation for loan payments MyMoneySchedule sched = file->schedule(tx.value("kmm-schedule-id")); const MyMoneySplit& split = tx.amortizationSplit(); if (!split.id().isEmpty()) { ReportAccount splitAccount(file->account(split.accountId())); eMyMoney::Account::Type type = splitAccount.accountGroup(); QString outergroup = MyMoneyAccount::accountTypeToString(type); //if the account is included in the report, calculate the balance from the cells if (m_config.includes(splitAccount)) { loanBalances[splitAccount.id()] = cellBalance(outergroup, splitAccount, column, false); } else { //if it is not in the report and also not in loanBalances, get the balance from the file if (!loanBalances.contains(splitAccount.id())) { QDate dueDate = sched.nextDueDate(); //if the payment is overdue, use current date if (dueDate < QDate::currentDate()) dueDate = QDate::currentDate(); //get the balance from the file for the date loanBalances[splitAccount.id()] = file->balance(splitAccount.id(), dueDate.addDays(-1)); } } KMyMoneyUtils::calculateAutoLoan(sched, tx, loanBalances); //if the loan split is not included in the report, update the balance for the next occurrence if (!m_config.includes(splitAccount)) { foreach (const auto split, tx.splits()) { if (split.isAmortizationSplit() && split.accountId() == splitAccount.id()) loanBalances[splitAccount.id()] = loanBalances[splitAccount.id()] + split.shares(); } } } } QList splits = tx.splits(); QList::const_iterator it_split = splits.constBegin(); while (it_split != splits.constEnd()) { ReportAccount splitAccount((*it_split).accountId()); // Each split must be further filtered, because if even one split matches, // the ENTIRE transaction is returned with all splits (even non-matching ones) if (m_config.includes(splitAccount) && m_config.match(&(*it_split))) { // reverse sign to match common notation for cash flow direction, only for expense/income splits MyMoneyMoney reverse(splitAccount.isIncomeExpense() ? -1 : 1, 1); MyMoneyMoney value; // the outer group is the account class (major account type) eMyMoney::Account::Type type = splitAccount.accountGroup(); QString outergroup = MyMoneyAccount::accountTypeToString(type); value = (*it_split).shares(); bool stockSplit = tx.isStockSplit(); if (!stockSplit) { // retrieve the value in the account's underlying currency if (value != MyMoneyMoney::autoCalc) { value = value * reverse; } else { qDebug("PivotTable::PivotTable(): This must not happen"); value = MyMoneyMoney(); // keep it 0 so far } // Except in the case of transfers on an income/expense report if (al_transfers && (type == eMyMoney::Account::Type::Asset || type == eMyMoney::Account::Type::Liability)) { outergroup = i18n("Transfers"); value = -value; } } // add the value to its correct position in the pivot table assignCell(outergroup, splitAccount, column, value, false, stockSplit); } ++it_split; } ++it_transaction; } } // // Get forecast data // if (m_config.isIncludingForecast()) calculateForecast(); // //Insert Price data // if (m_config.isIncludingPrice()) fillBasePriceUnit(ePrice); // //Insert Average Price data // if (m_config.isIncludingAveragePrice()) { fillBasePriceUnit(eActual); calculateMovingAverage(); } // // Collapse columns to match column type // if (m_config.columnPitch() > 1) collapseColumns(); // // Calculate the running sums // (for running sum reports only) // if (m_config.isRunningSum()) calculateRunningSums(); // // Calculate Moving Average // if (m_config.isIncludingMovingAverage()) calculateMovingAverage(); // // Calculate Budget Difference // if (m_config.isIncludingBudgetActuals()) calculateBudgetDiff(); // // Convert all values to the deep currency // convertToDeepCurrency(); // // Convert all values to the base currency // if (m_config.isConvertCurrency()) convertToBaseCurrency(); // // Determine column headings // calculateColumnHeadings(); // // Calculate row and column totals // calculateTotals(); // // If using mixed time, calculate column for current date // m_config.setCurrentDateColumn(currentDateColumn()); } void PivotTable::collapseColumns() { DEBUG_ENTER(Q_FUNC_INFO); int columnpitch = m_config.columnPitch(); if (columnpitch != 1) { int sourcemonth = (m_config.isColumnsAreDays()) // use the user's locale to determine the week's start ? (m_beginDate.dayOfWeek() + 8 - QLocale().firstDayOfWeek()) % 7 : m_beginDate.month(); int sourcecolumn = m_startColumn; int destcolumn = m_startColumn; while (sourcecolumn < m_numColumns) { if (sourcecolumn != destcolumn) { #if 0 // TODO: Clean up this rather inefficient kludge. We really should jump by an entire // destcolumn at a time on RS reports, and calculate the proper sourcecolumn to use, // allowing us to clear and accumulate only ONCE per destcolumn if (m_config_f.isRunningSum()) clearColumn(destcolumn); #endif accumulateColumn(destcolumn, sourcecolumn); } if (++sourcecolumn < m_numColumns) { if ((sourcemonth++ % columnpitch) == 0) { if (sourcecolumn != ++destcolumn) clearColumn(destcolumn); } } } m_numColumns = destcolumn + 1; } } void PivotTable::accumulateColumn(int destcolumn, int sourcecolumn) { DEBUG_ENTER(Q_FUNC_INFO); DEBUG_OUTPUT(QString("From Column %1 to %2").arg(sourcecolumn).arg(destcolumn)); // iterate over outer groups PivotGrid::iterator it_outergroup = m_grid.begin(); while (it_outergroup != m_grid.end()) { // iterate over inner groups PivotOuterGroup::iterator it_innergroup = (*it_outergroup).begin(); while (it_innergroup != (*it_outergroup).end()) { // iterator over rows PivotInnerGroup::iterator it_row = (*it_innergroup).begin(); while (it_row != (*it_innergroup).end()) { if ((*it_row)[eActual].count() <= sourcecolumn) throw MYMONEYEXCEPTION(QString("Sourcecolumn %1 out of grid range (%2) in PivotTable::accumulateColumn").arg(sourcecolumn).arg((*it_row)[eActual].count())); if ((*it_row)[eActual].count() <= destcolumn) throw MYMONEYEXCEPTION(QString("Destcolumn %1 out of grid range (%2) in PivotTable::accumulateColumn").arg(sourcecolumn).arg((*it_row)[eActual].count())); (*it_row)[eActual][destcolumn] += (*it_row)[eActual][sourcecolumn]; ++it_row; } ++it_innergroup; } ++it_outergroup; } } void PivotTable::clearColumn(int column) { DEBUG_ENTER(Q_FUNC_INFO); DEBUG_OUTPUT(QString("Column %1").arg(column)); // iterate over outer groups PivotGrid::iterator it_outergroup = m_grid.begin(); while (it_outergroup != m_grid.end()) { // iterate over inner groups PivotOuterGroup::iterator it_innergroup = (*it_outergroup).begin(); while (it_innergroup != (*it_outergroup).end()) { // iterator over rows PivotInnerGroup::iterator it_row = (*it_innergroup).begin(); while (it_row != (*it_innergroup).end()) { if ((*it_row)[eActual].count() <= column) throw MYMONEYEXCEPTION(QString("Column %1 out of grid range (%2) in PivotTable::accumulateColumn").arg(column).arg((*it_row)[eActual].count())); (*it_row++)[eActual][column] = PivotCell(); } ++it_innergroup; } ++it_outergroup; } } void PivotTable::calculateColumnHeadings() { DEBUG_ENTER(Q_FUNC_INFO); // one column for the opening balance if (m_startColumn == 1) m_columnHeadings.append("Opening"); int columnpitch = m_config.columnPitch(); if (columnpitch == 0) { // output the warning but don't crash by dividing with 0 qWarning("PivotTable::calculateColumnHeadings() Invalid column pitch"); return; } // if this is a days-based report if (m_config.isColumnsAreDays()) { if (columnpitch == 1) { QDate columnDate = m_beginDate; int column = m_startColumn; while (column++ < m_numColumns) { QString heading = QLocale().monthName(columnDate.month(), QLocale::ShortFormat) + ' ' + QString::number(columnDate.day()); columnDate = columnDate.addDays(1); m_columnHeadings.append(heading); } } else { QDate day = m_beginDate; QDate prv = m_beginDate; // use the user's locale to determine the week's start int dow = (day.dayOfWeek() + 8 - QLocale().firstDayOfWeek()) % 7; while (day <= m_endDate) { if (((dow % columnpitch) == 0) || (day == m_endDate)) { m_columnHeadings.append(QString("%1 %2 - %3 %4") .arg(QLocale().monthName(prv.month(), QLocale::ShortFormat)) .arg(prv.day()) .arg(QLocale().monthName(day.month(), QLocale::ShortFormat)) .arg(day.day())); prv = day.addDays(1); } day = day.addDays(1); dow++; } } } // else it's a months-based report else { if (columnpitch == 12) { int year = m_beginDate.year(); int column = m_startColumn; while (column++ < m_numColumns) m_columnHeadings.append(QString::number(year++)); } else { int year = m_beginDate.year(); bool includeyear = (m_beginDate.year() != m_endDate.year()); int segment = (m_beginDate.month() - 1) / columnpitch; int column = m_startColumn; while (column++ < m_numColumns) { QString heading = QLocale().monthName(1 + segment * columnpitch, QLocale::ShortFormat); if (columnpitch != 1) heading += '-' + QLocale().monthName((1 + segment) * columnpitch, QLocale::ShortFormat); if (includeyear) heading += ' ' + QString::number(year); m_columnHeadings.append(heading); if (++segment >= 12 / columnpitch) { segment -= 12 / columnpitch; ++year; } } } } } void PivotTable::createAccountRows() { DEBUG_ENTER(Q_FUNC_INFO); MyMoneyFile* file = MyMoneyFile::instance(); QList accounts; file->accountList(accounts); QList::const_iterator it_account = accounts.constBegin(); while (it_account != accounts.constEnd()) { ReportAccount account(*it_account); // only include this item if its account group is included in this report // and if the report includes this account if (m_config.includes(*it_account)) { DEBUG_OUTPUT(QString("Includes account %1").arg(account.name())); // the row group is the account class (major account type) QString outergroup = MyMoneyAccount::accountTypeToString(account.accountGroup()); // place into the 'opening' column... assignCell(outergroup, account, 0, MyMoneyMoney()); } ++it_account; } } void PivotTable::calculateOpeningBalances() { DEBUG_ENTER(Q_FUNC_INFO); // First, determine the inclusive dates of the report. Normally, that's just // the begin & end dates of m_config_f. However, if either of those dates are // blank, we need to use m_beginDate and/or m_endDate instead. QDate from = m_config.fromDate(); QDate to = m_config.toDate(); if (! from.isValid()) from = m_beginDate; if (! to.isValid()) to = m_endDate; MyMoneyFile* file = MyMoneyFile::instance(); QList accounts; file->accountList(accounts); QList::const_iterator it_account = accounts.constBegin(); while (it_account != accounts.constEnd()) { ReportAccount account(*it_account); // only include this item if its account group is included in this report // and if the report includes this account if (m_config.includes(*it_account)) { //do not include account if it is closed and it has no transactions in the report period if (account.isClosed()) { //check if the account has transactions for the report timeframe MyMoneyTransactionFilter filter; filter.addAccount(account.id()); filter.setDateFilter(m_beginDate, m_endDate); filter.setReportAllSplits(false); QList transactions = file->transactionList(filter); //if a closed account has no transactions in that timeframe, do not include it if (transactions.size() == 0) { DEBUG_OUTPUT(QString("DOES NOT INCLUDE account %1").arg(account.name())); ++it_account; continue; } } DEBUG_OUTPUT(QString("Includes account %1").arg(account.name())); // the row group is the account class (major account type) QString outergroup = MyMoneyAccount::accountTypeToString(account.accountGroup()); // extract the balance of the account for the given begin date, which is // the opening balance plus the sum of all transactions prior to the begin // date // this is in the underlying currency MyMoneyMoney value = file->balance(account.id(), from.addDays(-1)); // place into the 'opening' column... assignCell(outergroup, account, 0, value); } else { DEBUG_OUTPUT(QString("DOES NOT INCLUDE account %1").arg(account.name())); } ++it_account; } } void PivotTable::calculateRunningSums(PivotInnerGroup::iterator& it_row) { MyMoneyMoney runningsum = it_row.value()[eActual][0].calculateRunningSum(MyMoneyMoney()); int column = m_startColumn; while (column < m_numColumns) { if (it_row.value()[eActual].count() <= column) throw MYMONEYEXCEPTION(QString("Column %1 out of grid range (%2) in PivotTable::calculateRunningSums").arg(column).arg(it_row.value()[eActual].count())); runningsum = it_row.value()[eActual][column].calculateRunningSum(runningsum); ++column; } } void PivotTable::calculateRunningSums() { DEBUG_ENTER(Q_FUNC_INFO); m_runningSumsCalculated = true; PivotGrid::iterator it_outergroup = m_grid.begin(); while (it_outergroup != m_grid.end()) { PivotOuterGroup::iterator it_innergroup = (*it_outergroup).begin(); while (it_innergroup != (*it_outergroup).end()) { PivotInnerGroup::iterator it_row = (*it_innergroup).begin(); while (it_row != (*it_innergroup).end()) { #if 0 MyMoneyMoney runningsum = it_row.value()[0]; int column = m_startColumn; while (column < m_numColumns) { if (it_row.value()[eActual].count() <= column) throw MYMONEYEXCEPTION(QString("Column %1 out of grid range (%2) in PivotTable::calculateRunningSums").arg(column).arg(it_row.value()[eActual].count())); runningsum = (it_row.value()[eActual][column] += runningsum); ++column; } #endif calculateRunningSums(it_row); ++it_row; } ++it_innergroup; } ++it_outergroup; } } MyMoneyMoney PivotTable::cellBalance(const QString& outergroup, const ReportAccount& _row, int _column, bool budget) { if (m_runningSumsCalculated) { qDebug("You must not call PivotTable::cellBalance() after calling PivotTable::calculateRunningSums()"); throw MYMONEYEXCEPTION(QString("You must not call PivotTable::cellBalance() after calling PivotTable::calculateRunningSums()")); } // for budget reports, if this is the actual value, map it to the account which // holds its budget ReportAccount row = _row; if (!budget && m_config.hasBudget()) { QString newrow = m_budgetMap[row.id()]; // if there was no mapping found, then the budget report is not interested // in this account. if (newrow.isEmpty()) return MyMoneyMoney(); row = ReportAccount(newrow); } // ensure the row already exists (and its parental hierarchy) createRow(outergroup, row, true); // Determine the inner group from the top-most parent account QString innergroup(row.topParentName()); if (m_numColumns <= _column) throw MYMONEYEXCEPTION(QString("Column %1 out of m_numColumns range (%2) in PivotTable::cellBalance").arg(_column).arg(m_numColumns)); if (m_grid[outergroup][innergroup][row][eActual].count() <= _column) throw MYMONEYEXCEPTION(QString("Column %1 out of grid range (%2) in PivotTable::cellBalance").arg(_column).arg(m_grid[outergroup][innergroup][row][eActual].count())); MyMoneyMoney balance; if (budget) balance = m_grid[outergroup][innergroup][row][eBudget][0].cellBalance(MyMoneyMoney()); else balance = m_grid[outergroup][innergroup][row][eActual][0].cellBalance(MyMoneyMoney()); int column = m_startColumn; while (column < _column) { if (m_grid[outergroup][innergroup][row][eActual].count() <= column) throw MYMONEYEXCEPTION(QString("Column %1 out of grid range (%2) in PivotTable::cellBalance").arg(column).arg(m_grid[outergroup][innergroup][row][eActual].count())); balance = m_grid[outergroup][innergroup][row][eActual][column].cellBalance(balance); ++column; } return balance; } void PivotTable::calculateBudgetMapping() { DEBUG_ENTER(Q_FUNC_INFO); MyMoneyFile* file = MyMoneyFile::instance(); // Only do this if there is at least one budget in the file if (file->countBudgets()) { // Select a budget // // It will choose the first budget in the list for the start year of the report if no budget is selected MyMoneyBudget budget = MyMoneyBudget(); QList budgets = file->budgetList(); bool validBudget = false; //check that the selected budget is valid if (m_config.budget() != "Any") { QList::const_iterator budgets_it = budgets.constBegin(); while (budgets_it != budgets.constEnd()) { //pick the budget by id if ((*budgets_it).id() == m_config.budget()) { budget = file->budget((*budgets_it).id()); validBudget = true; break; } ++budgets_it; } } //if no valid budget has been selected if (!validBudget) { //if the budget list is empty, just return if (budgets.count() == 0) { return; } QList::const_iterator budgets_it = budgets.constBegin(); while (budgets_it != budgets.constEnd()) { //pick the first budget that matches the report start year if ((*budgets_it).budgetStart().year() == QDate::currentDate().year()) { budget = file->budget((*budgets_it).id()); break; } ++budgets_it; } //if it can't find a matching budget, take the first one on the list if (budget.id().isEmpty()) { budget = budgets[0]; } //assign the budget to the report m_config.setBudget(budget.id(), m_config.isIncludingBudgetActuals()); } // Dump the budget //qDebug() << "Budget " << budget.name() << ": "; // Go through all accounts in the system to build the mapping QList accounts; file->accountList(accounts); QList::const_iterator it_account = accounts.constBegin(); while (it_account != accounts.constEnd()) { //include only the accounts selected for the report if (m_config.includes(*it_account)) { QString id = (*it_account).id(); QString acid = id; // If the budget contains this account outright if (budget.contains(id)) { // Add it to the mapping m_budgetMap[acid] = id; // qDebug() << ReportAccount(acid).debugName() << " self-maps / type =" << budget.account(id).budgetLevel(); } // Otherwise, search for a parent account which includes sub-accounts else { //if includeBudgetActuals, include all accounts regardless of whether in budget or not if (m_config.isIncludingBudgetActuals()) { m_budgetMap[acid] = id; // qDebug() << ReportAccount(acid).debugName() << " maps to " << ReportAccount(id).debugName(); } do { id = file->account(id).parentAccountId(); if (budget.contains(id)) { if (budget.account(id).budgetSubaccounts()) { m_budgetMap[acid] = id; // qDebug() << ReportAccount(acid).debugName() << " maps to " << ReportAccount(id).debugName(); break; } } } while (! id.isEmpty()); } } ++it_account; } // end while looping through the accounts in the file // Place the budget values into the budget grid QList baccounts = budget.getaccounts(); QList::const_iterator it_bacc = baccounts.constBegin(); while (it_bacc != baccounts.constEnd()) { ReportAccount splitAccount((*it_bacc).id()); //include the budget account only if it is included in the report if (m_config.includes(splitAccount)) { eMyMoney::Account::Type type = splitAccount.accountGroup(); QString outergroup = MyMoneyAccount::accountTypeToString(type); // reverse sign to match common notation for cash flow direction, only for expense/income splits MyMoneyMoney reverse((splitAccount.accountType() == eMyMoney::Account::Type::Expense) ? -1 : 1, 1); const QMap& periods = (*it_bacc).getPeriods(); // skip the account if it has no periods if (periods.count() < 1) { ++it_bacc; continue; } MyMoneyMoney value = (*periods.begin()).amount() * reverse; int column = m_startColumn; // based on the kind of budget it is, deal accordingly switch ((*it_bacc).budgetLevel()) { case MyMoneyBudget::AccountGroup::eYearly: // divide the single yearly value by 12 and place it in each column value /= MyMoneyMoney(12, 1); // intentional fall through case MyMoneyBudget::AccountGroup::eNone: case MyMoneyBudget::AccountGroup::eMax: case MyMoneyBudget::AccountGroup::eMonthly: // place the single monthly value in each column of the report // only add the value if columns are monthly or longer if (m_config.columnType() == MyMoneyReport::eBiMonths || m_config.columnType() == MyMoneyReport::eMonths || m_config.columnType() == MyMoneyReport::eYears || m_config.columnType() == MyMoneyReport::eQuarters) { QDate budgetDate = budget.budgetStart(); while (column < m_numColumns && budget.budgetStart().addYears(1) > budgetDate) { //only show budget values if the budget year and the column date match //no currency conversion is done here because that is done for all columns later if (budgetDate > columnDate(column)) { ++column; } else { if (budgetDate >= m_beginDate.addDays(-m_beginDate.day() + 1) && budgetDate <= m_endDate.addDays(m_endDate.daysInMonth() - m_endDate.day()) && budgetDate > (columnDate(column).addMonths(-m_config.columnType()))) { assignCell(outergroup, splitAccount, column, value, true /*budget*/); } budgetDate = budgetDate.addMonths(1); } } } break; case MyMoneyBudget::AccountGroup::eMonthByMonth: // place each value in the appropriate column // budget periods are supposed to come in order just like columns { QMap::const_iterator it_period = periods.begin(); while (it_period != periods.end() && column < m_numColumns) { if ((*it_period).startDate() > columnDate(column)) { ++column; } else { switch (m_config.columnType()) { case MyMoneyReport::eYears: case MyMoneyReport::eBiMonths: case MyMoneyReport::eQuarters: case MyMoneyReport::eMonths: { if ((*it_period).startDate() >= m_beginDate.addDays(-m_beginDate.day() + 1) && (*it_period).startDate() <= m_endDate.addDays(m_endDate.daysInMonth() - m_endDate.day()) && (*it_period).startDate() > (columnDate(column).addMonths(-m_config.columnType()))) { //no currency conversion is done here because that is done for all columns later value = (*it_period).amount() * reverse; assignCell(outergroup, splitAccount, column, value, true /*budget*/); } ++it_period; break; } default: break; } } } break; } } } ++it_bacc; } } // end if there was a budget } void PivotTable::convertToBaseCurrency() { DEBUG_ENTER(Q_FUNC_INFO); MyMoneyFile* file = MyMoneyFile::instance(); int fraction = file->baseCurrency().smallestAccountFraction(); QList rowTypeList = m_rowTypeList; rowTypeList.removeOne(eAverage); PivotGrid::iterator it_outergroup = m_grid.begin(); while (it_outergroup != m_grid.end()) { PivotOuterGroup::iterator it_innergroup = (*it_outergroup).begin(); while (it_innergroup != (*it_outergroup).end()) { PivotInnerGroup::iterator it_row = (*it_innergroup).begin(); while (it_row != (*it_innergroup).end()) { auto column = 0; while (column < m_numColumns) { if (it_row.value()[eActual].count() <= column) throw MYMONEYEXCEPTION(QString("Column %1 out of grid range (%2) in PivotTable::convertToBaseCurrency").arg(column).arg(it_row.value()[eActual].count())); QDate valuedate = columnDate(column); //get base price for that date MyMoneyMoney conversionfactor = it_row.key().baseCurrencyPrice(valuedate, m_config.isSkippingZero()); int pricePrecision; if (it_row.key().isInvest()) pricePrecision = file->security(it_row.key().currencyId()).pricePrecision(); else pricePrecision = MyMoneyMoney::denomToPrec(fraction); foreach (const auto rowType, rowTypeList) { //calculate base value MyMoneyMoney oldval = it_row.value()[rowType][column]; MyMoneyMoney value = (oldval * conversionfactor).reduce(); //convert to lowest fraction if (rowType == ePrice) it_row.value()[rowType][column] = PivotCell(MyMoneyMoney(value.convertPrecision(pricePrecision))); else it_row.value()[rowType][column] = PivotCell(value.convert(fraction)); DEBUG_OUTPUT_IF(conversionfactor != MyMoneyMoney::ONE , QString("Factor of %1, value was %2, now %3").arg(conversionfactor).arg(DEBUG_SENSITIVE(oldval)).arg(DEBUG_SENSITIVE(it_row.value()[rowType][column].toDouble()))); } ++column; } ++it_row; } ++it_innergroup; } ++it_outergroup; } } void PivotTable::convertToDeepCurrency() { DEBUG_ENTER(Q_FUNC_INFO); MyMoneyFile* file = MyMoneyFile::instance(); PivotGrid::iterator it_outergroup = m_grid.begin(); while (it_outergroup != m_grid.end()) { PivotOuterGroup::iterator it_innergroup = (*it_outergroup).begin(); while (it_innergroup != (*it_outergroup).end()) { PivotInnerGroup::iterator it_row = (*it_innergroup).begin(); while (it_row != (*it_innergroup).end()) { auto column = 0; while (column < m_numColumns) { if (it_row.value()[eActual].count() <= column) throw MYMONEYEXCEPTION(QString("Column %1 out of grid range (%2) in PivotTable::convertToDeepCurrency").arg(column).arg(it_row.value()[eActual].count())); QDate valuedate = columnDate(column); //get conversion factor for the account and date MyMoneyMoney conversionfactor = it_row.key().deepCurrencyPrice(valuedate, m_config.isSkippingZero()); //use the fraction relevant to the account at hand int fraction = it_row.key().currency().smallestAccountFraction(); //use base currency fraction if not initialized if (fraction == -1) fraction = file->baseCurrency().smallestAccountFraction(); //convert to deep currency MyMoneyMoney oldval = it_row.value()[eActual][column]; MyMoneyMoney value = (oldval * conversionfactor).reduce(); //reduce to lowest fraction it_row.value()[eActual][column] = PivotCell(value.convert(fraction)); //convert price data if (m_config.isIncludingPrice()) { MyMoneyMoney oldPriceVal = it_row.value()[ePrice][column]; MyMoneyMoney priceValue = (oldPriceVal * conversionfactor).reduce(); it_row.value()[ePrice][column] = PivotCell(priceValue.convert(10000)); } DEBUG_OUTPUT_IF(conversionfactor != MyMoneyMoney::ONE , QString("Factor of %1, value was %2, now %3").arg(conversionfactor).arg(DEBUG_SENSITIVE(oldval)).arg(DEBUG_SENSITIVE(it_row.value()[eActual][column].toDouble()))); ++column; } ++it_row; } ++it_innergroup; } ++it_outergroup; } } void PivotTable::calculateTotals() { //insert the row type that is going to be used for (int i = 0; i < m_rowTypeList.size(); ++i) { for (int k = 0; k < m_numColumns; ++k) { m_grid.m_total[ m_rowTypeList[i] ].append(PivotCell()); } } // // Outer groups // // iterate over outer groups PivotGrid::iterator it_outergroup = m_grid.begin(); while (it_outergroup != m_grid.end()) { for (int i = 0; i < m_rowTypeList.size(); ++i) { for (int k = 0; k < m_numColumns; ++k) { (*it_outergroup).m_total[ m_rowTypeList[i] ].append(PivotCell()); } } // // Inner Groups // PivotOuterGroup::iterator it_innergroup = (*it_outergroup).begin(); while (it_innergroup != (*it_outergroup).end()) { for (int i = 0; i < m_rowTypeList.size(); ++i) { for (int k = 0; k < m_numColumns; ++k) { (*it_innergroup).m_total[ m_rowTypeList[i] ].append(PivotCell()); } } // // Rows // PivotInnerGroup::iterator it_row = (*it_innergroup).begin(); while (it_row != (*it_innergroup).end()) { // // Columns // auto column = 0; while (column < m_numColumns) { for (int i = 0; i < m_rowTypeList.size(); ++i) { if (it_row.value()[ m_rowTypeList[i] ].count() <= column) throw MYMONEYEXCEPTION(QString("Column %1 out of grid range (%2) in PivotTable::calculateTotals, row columns").arg(column).arg(it_row.value()[ m_rowTypeList[i] ].count())); if ((*it_innergroup).m_total[ m_rowTypeList[i] ].count() <= column) throw MYMONEYEXCEPTION(QString("Column %1 out of grid range (%2) in PivotTable::calculateTotals, inner group totals").arg(column).arg((*it_innergroup).m_total[ m_rowTypeList[i] ].count())); //calculate total MyMoneyMoney value = it_row.value()[ m_rowTypeList[i] ][column]; (*it_innergroup).m_total[ m_rowTypeList[i] ][column] += value; (*it_row)[ m_rowTypeList[i] ].m_total += value; } ++column; } ++it_row; } // // Inner Row Group Totals // auto column = 0; while (column < m_numColumns) { for (int i = 0; i < m_rowTypeList.size(); ++i) { if ((*it_innergroup).m_total[ m_rowTypeList[i] ].count() <= column) throw MYMONEYEXCEPTION(QString("Column %1 out of grid range (%2) in PivotTable::calculateTotals, inner group totals").arg(column).arg((*it_innergroup).m_total[ m_rowTypeList[i] ].count())); if ((*it_outergroup).m_total[ m_rowTypeList[i] ].count() <= column) throw MYMONEYEXCEPTION(QString("Column %1 out of grid range (%2) in PivotTable::calculateTotals, outer group totals").arg(column).arg((*it_innergroup).m_total[ m_rowTypeList[i] ].count())); //calculate totals MyMoneyMoney value = (*it_innergroup).m_total[ m_rowTypeList[i] ][column]; (*it_outergroup).m_total[ m_rowTypeList[i] ][column] += value; (*it_innergroup).m_total[ m_rowTypeList[i] ].m_total += value; } ++column; } ++it_innergroup; } // // Outer Row Group Totals // const bool isIncomeExpense = (m_config.rowType() == MyMoneyReport::eExpenseIncome); const bool invert_total = (*it_outergroup).m_inverted; auto column = 0; while (column < m_numColumns) { for (int i = 0; i < m_rowTypeList.size(); ++i) { if (m_grid.m_total[ m_rowTypeList[i] ].count() <= column) throw MYMONEYEXCEPTION(QString("Column %1 out of grid range (%2) in PivotTable::calculateTotals, grid totals").arg(column).arg((*it_innergroup).m_total[ m_rowTypeList[i] ].count())); //calculate actual totals MyMoneyMoney value = (*it_outergroup).m_total[ m_rowTypeList[i] ][column]; (*it_outergroup).m_total[ m_rowTypeList[i] ].m_total += value; //so far the invert only applies to actual and budget if (invert_total && m_rowTypeList[i] != eBudgetDiff && m_rowTypeList[i] != eForecast) value = -value; // forecast income expense reports should be inverted as oposed to asset/liability reports if (invert_total && isIncomeExpense && m_rowTypeList[i] == eForecast) value = -value; m_grid.m_total[ m_rowTypeList[i] ][column] += value; } ++column; } ++it_outergroup; } // // Report Totals // auto totalcolumn = 0; while (totalcolumn < m_numColumns) { for (int i = 0; i < m_rowTypeList.size(); ++i) { if (m_grid.m_total[ m_rowTypeList[i] ].count() <= totalcolumn) throw MYMONEYEXCEPTION(QString("Total column %1 out of grid range (%2) in PivotTable::calculateTotals, grid totals").arg(totalcolumn).arg(m_grid.m_total[ m_rowTypeList[i] ].count())); //calculate actual totals MyMoneyMoney value = m_grid.m_total[ m_rowTypeList[i] ][totalcolumn]; m_grid.m_total[ m_rowTypeList[i] ].m_total += value; } ++totalcolumn; } } void PivotTable::assignCell(const QString& outergroup, const ReportAccount& _row, int column, MyMoneyMoney value, bool budget, bool stockSplit) { DEBUG_ENTER(Q_FUNC_INFO); DEBUG_OUTPUT(QString("Parameters: %1,%2,%3,%4,%5").arg(outergroup).arg(_row.debugName()).arg(column).arg(DEBUG_SENSITIVE(value.toDouble())).arg(budget)); // for budget reports, if this is the actual value, map it to the account which // holds its budget ReportAccount row = _row; if (!budget && m_config.hasBudget()) { QString newrow = m_budgetMap[row.id()]; // if there was no mapping found, then the budget report is not interested // in this account. if (newrow.isEmpty()) return; row = ReportAccount(newrow); } // ensure the row already exists (and its parental hierarchy) createRow(outergroup, row, true); // Determine the inner group from the top-most parent account QString innergroup(row.topParentName()); if (m_numColumns <= column) throw MYMONEYEXCEPTION(QString("Column %1 out of m_numColumns range (%2) in PivotTable::assignCell").arg(column).arg(m_numColumns)); if (m_grid[outergroup][innergroup][row][eActual].count() <= column) throw MYMONEYEXCEPTION(QString("Column %1 out of grid range (%2) in PivotTable::assignCell").arg(column).arg(m_grid[outergroup][innergroup][row][eActual].count())); if (m_grid[outergroup][innergroup][row][eBudget].count() <= column) throw MYMONEYEXCEPTION(QString("Column %1 out of grid range (%2) in PivotTable::assignCell").arg(column).arg(m_grid[outergroup][innergroup][row][eBudget].count())); if (!stockSplit) { // Determine whether the value should be inverted before being placed in the row if (m_grid[outergroup].m_inverted) value = -value; // Add the value to the grid cell if (budget) { m_grid[outergroup][innergroup][row][eBudget][column] += value; } else { // If it is loading an actual value for a budget report // check whether it is a subaccount of a budget account (include subaccounts) // If so, check if is the same currency and convert otherwise if (m_config.hasBudget() && row.id() != _row.id() && row.currencyId() != _row.currencyId()) { ReportAccount origAcc = _row; MyMoneyMoney rate = origAcc.foreignCurrencyPrice(row.currencyId(), columnDate(column), false); m_grid[outergroup][innergroup][row][eActual][column] += (value * rate).reduce(); } else { m_grid[outergroup][innergroup][row][eActual][column] += value; } } } else { m_grid[outergroup][innergroup][row][eActual][column] += PivotCell::stockSplit(value); } } void PivotTable::createRow(const QString& outergroup, const ReportAccount& row, bool recursive) { DEBUG_ENTER(Q_FUNC_INFO); // Determine the inner group from the top-most parent account QString innergroup(row.topParentName()); if (! m_grid.contains(outergroup)) { DEBUG_OUTPUT(QString("Adding group [%1]").arg(outergroup)); m_grid[outergroup] = PivotOuterGroup(m_numColumns); } if (! m_grid[outergroup].contains(innergroup)) { DEBUG_OUTPUT(QString("Adding group [%1][%2]").arg(outergroup).arg(innergroup)); m_grid[outergroup][innergroup] = PivotInnerGroup(m_numColumns); } if (! m_grid[outergroup][innergroup].contains(row)) { DEBUG_OUTPUT(QString("Adding row [%1][%2][%3]").arg(outergroup).arg(innergroup).arg(row.debugName())); m_grid[outergroup][innergroup][row] = PivotGridRowSet(m_numColumns); if (recursive && !row.isTopLevel()) createRow(outergroup, row.parent(), recursive); } } int PivotTable::columnValue(const QDate& _date) const { if (m_config.isColumnsAreDays()) return (m_beginDate.daysTo(_date)); else return (_date.year() * 12 + _date.month()); } QDate PivotTable::columnDate(int column) const { if (m_config.isColumnsAreDays()) return m_beginDate.addDays(m_config.columnPitch() * column - m_startColumn); else return m_beginDate.addMonths(m_config.columnPitch() * column).addDays(-m_startColumn); } QString PivotTable::renderCSV() const { DEBUG_ENTER(Q_FUNC_INFO); MyMoneyFile* file = MyMoneyFile::instance(); int pricePrecision = 0; int currencyPrecision = 0; int precision = MyMoneyMoney::denomToPrec(file->baseCurrency().smallestAccountFraction()); bool isMultipleCurrencies = false; // // Table Header // QString result = i18n("Account"); auto column = 0; while (column < m_numColumns) { result += QString(",%1").arg(QString(m_columnHeadings[column++])); if (m_rowTypeList.size() > 1) { QString separator; separator = separator.fill(',', m_rowTypeList.size() - 1); result += separator; } } //show total columns if (m_config.isShowingRowTotals()) result += QString(",%1").arg(i18nc("Total balance", "Total")); result += '\n'; // Row Type Header if (m_rowTypeList.size() > 1) { auto column = 0; while (column < m_numColumns) { for (int i = 0; i < m_rowTypeList.size(); ++i) { result += QString(",%1").arg(m_columnTypeHeaderList[i]); } column++; } if (m_config.isShowingRowTotals()) { for (int i = 0; i < m_rowTypeList.size(); ++i) { result += QString(",%1").arg(m_columnTypeHeaderList[i]); } } result += '\n'; } // // Outer groups // // iterate over outer groups PivotGrid::const_iterator it_outergroup = m_grid.begin(); while (it_outergroup != m_grid.end()) { // // Outer Group Header // if (!(m_config.isIncludingPrice() || m_config.isIncludingAveragePrice())) result += it_outergroup.key() + '\n'; // // Inner Groups // PivotOuterGroup::const_iterator it_innergroup = (*it_outergroup).begin(); int rownum = 0; while (it_innergroup != (*it_outergroup).end()) { // // Rows // QString innergroupdata; PivotInnerGroup::const_iterator it_row = (*it_innergroup).begin(); while (it_row != (*it_innergroup).end()) { ReportAccount rowname = it_row.key(); // // Columns // QString rowdata; auto column = 0; bool isUsed = false; for (int i = 0; i < m_rowTypeList.size(); ++i) isUsed |= it_row.value()[ m_rowTypeList[i] ][0].isUsed(); if (it_row.key().accountType() != eMyMoney::Account::Type::Investment) { while (column < m_numColumns) { //show columns foreach (const auto rowType, m_rowTypeList) { if (rowType == ePrice) { if (pricePrecision == 0) { if (it_row.key().isInvest()) { pricePrecision = file->currency(it_row.key().currencyId()).pricePrecision(); precision = pricePrecision; } else precision = MyMoneyMoney::denomToPrec(file->baseCurrency().smallestAccountFraction()); } else precision = pricePrecision; } else { if (currencyPrecision == 0) { if (it_row.key().isInvest()) // stock account isn't eveluated in currency, so take investment account instead currencyPrecision = MyMoneyMoney::denomToPrec(it_row.key().parent().fraction()); else currencyPrecision = MyMoneyMoney::denomToPrec(it_row.key().fraction()); precision = currencyPrecision; } else precision = currencyPrecision; } rowdata += QString(",\"%1\"").arg(it_row.value()[rowType][column].formatMoney(QString(), precision, false)); isUsed |= it_row.value()[rowType][column].isUsed(); } column++; } if (m_config.isShowingRowTotals()) { for (int i = 0; i < m_rowTypeList.size(); ++i) rowdata += QString(",\"%1\"").arg((*it_row)[ m_rowTypeList[i] ].m_total.formatMoney(QString(), precision, false)); } } else { for (auto i = 0; i < m_numColumns + m_rowTypeList.size(); ++i) rowdata.append(',');; } // // Row Header // if (!rowname.isClosed() || isUsed) { innergroupdata += "\"" + QString().fill(' ', rowname.hierarchyDepth() - 1) + rowname.name(); // if we don't convert the currencies to the base currency and the // current row contains a foreign currency, then we append the currency // to the name of the account if (!m_config.isConvertCurrency() && rowname.isForeignCurrency()) innergroupdata += QString(" (%1)").arg(rowname.currencyId()); innergroupdata += '\"'; if (isUsed) innergroupdata += rowdata; innergroupdata += '\n'; if (!isMultipleCurrencies && rowname.isForeignCurrency()) isMultipleCurrencies = true; if (!m_containsNonBaseCurrency && rowname.isForeignCurrency()) m_containsNonBaseCurrency = true; } ++it_row; } // // Inner Row Group Totals // bool finishrow = true; QString finalRow; bool isUsed = false; if (m_config.detailLevel() == MyMoneyReport::eDetailAll && ((*it_innergroup).size() > 1)) { // Print the individual rows result += innergroupdata; if (m_config.isConvertCurrency() && m_config.isShowingColumnTotals()) { // Start the TOTALS row finalRow = i18nc("Total balance", "Total"); isUsed = true; } else { ++rownum; finishrow = false; } } else { // Start the single INDIVIDUAL ACCOUNT row ReportAccount rowname = (*it_innergroup).begin().key(); isUsed |= !rowname.isClosed(); finalRow = "\"" + QString().fill(' ', rowname.hierarchyDepth() - 1) + rowname.name(); if (!m_config.isConvertCurrency() && rowname.isForeignCurrency()) finalRow += QString(" (%1)").arg(rowname.currencyId()); finalRow += "\""; } // Finish the row started above, unless told not to if (finishrow) { auto column = 0; for (int i = 0; i < m_rowTypeList.size(); ++i) isUsed |= (*it_innergroup).m_total[ m_rowTypeList[i] ][0].isUsed(); while (column < m_numColumns) { for (int i = 0; i < m_rowTypeList.size(); ++i) { isUsed |= (*it_innergroup).m_total[ m_rowTypeList[i] ][column].isUsed(); finalRow += QString(",\"%1\"").arg((*it_innergroup).m_total[ m_rowTypeList[i] ][column].formatMoney(QString(), precision, false)); } column++; } if (m_config.isShowingRowTotals()) { for (int i = 0; i < m_rowTypeList.size(); ++i) finalRow += QString(",\"%1\"").arg((*it_innergroup).m_total[ m_rowTypeList[i] ].m_total.formatMoney(QString(), precision, false)); } finalRow += '\n'; } if (isUsed) { result += finalRow; ++rownum; } ++it_innergroup; } // // Outer Row Group Totals // if (m_config.isConvertCurrency() && m_config.isShowingColumnTotals()) { result += QString("%1 %2").arg(i18nc("Total balance", "Total")).arg(it_outergroup.key()); auto column = 0; while (column < m_numColumns) { for (int i = 0; i < m_rowTypeList.size(); ++i) result += QString(",\"%1\"").arg((*it_outergroup).m_total[ m_rowTypeList[i] ][column].formatMoney(QString(), precision, false)); column++; } if (m_config.isShowingRowTotals()) { for (int i = 0; i < m_rowTypeList.size(); ++i) result += QString(",\"%1\"").arg((*it_outergroup).m_total[ m_rowTypeList[i] ].m_total.formatMoney(QString(), precision, false)); } result += '\n'; } ++it_outergroup; } // // Report Totals // if (m_config.isConvertCurrency() && m_config.isShowingColumnTotals()) { result += i18n("Grand Total"); auto totalcolumn = 0; while (totalcolumn < m_numColumns) { for (int i = 0; i < m_rowTypeList.size(); ++i) result += QString(",\"%1\"").arg(m_grid.m_total[ m_rowTypeList[i] ][totalcolumn].formatMoney(QString(), precision, false)); totalcolumn++; } if (m_config.isShowingRowTotals()) { for (int i = 0; i < m_rowTypeList.size(); ++i) result += QString(",\"%1\"").arg(m_grid.m_total[ m_rowTypeList[i] ].m_total.formatMoney(QString(), precision, false)); } result += '\n'; } return result; } QString PivotTable::renderHTML() const { DEBUG_ENTER(Q_FUNC_INFO); MyMoneyFile* file = MyMoneyFile::instance(); int pricePrecision = 0; int currencyPrecision = 0; int precision = MyMoneyMoney::denomToPrec(file->baseCurrency().smallestAccountFraction()); QString colspan = QString(" colspan=\"%1\"").arg(m_numColumns + 1 + (m_config.isShowingRowTotals() ? 1 : 0)); // setup a leftborder for better readability of budget vs actual reports QString leftborder; if (m_rowTypeList.size() > 1) leftborder = " class=\"leftborder\""; // // Table Header // QString result = QString("\n\n\n" "\n").arg(i18n("Account")); QString headerspan; int span = m_rowTypeList.size(); headerspan = QString(" colspan=\"%1\"").arg(span); auto column = 0; while (column < m_numColumns) result += QString("%2").arg(headerspan, QString(m_columnHeadings[column++]).replace(QRegExp(" "), "
")); if (m_config.isShowingRowTotals()) result += QString("%2").arg(headerspan).arg(i18nc("Total balance", "Total")); result += "
\n"; // // Header for multiple columns // if (span > 1) { result += ""; auto column = 0; while (column < m_numColumns) { QString lb; if (column != 0) lb = leftborder; for (int i = 0; i < m_rowTypeList.size(); ++i) { result += QString("%1") .arg(m_columnTypeHeaderList[i]) .arg(i == 0 ? lb : QString()); } column++; } if (m_config.isShowingRowTotals()) { for (int i = 0; i < m_rowTypeList.size(); ++i) { result += QString("%1") .arg(m_columnTypeHeaderList[i]) .arg(i == 0 ? leftborder : QString()); } } result += ""; } // Skip the body of the report if the report only calls for totals to be shown if (m_config.detailLevel() != MyMoneyReport::eDetailTotal) { // // Outer groups // // Need to sort the outergroups. They can't always be sorted by name. So we create a list of // map iterators, and sort that. Then we'll iterate through the map iterators and use those as // before. // // I hope this doesn't bog the performance of reports, given that we're copying the entire report // data. If this is a perf hit, we could change to storing outergroup pointers, I think. QList outergroups; PivotGrid::const_iterator it_outergroup_map = m_grid.begin(); while (it_outergroup_map != m_grid.end()) { outergroups.push_back(it_outergroup_map.value()); // copy the name into the outergroup, because we will now lose any association with // the map iterator outergroups.back().m_displayName = it_outergroup_map.key(); ++it_outergroup_map; } qSort(outergroups.begin(), outergroups.end()); QList::const_iterator it_outergroup = outergroups.constBegin(); while (it_outergroup != outergroups.constEnd()) { // // Outer Group Header // if (!(m_config.isIncludingPrice() || m_config.isIncludingAveragePrice())) result += QString("\n").arg(colspan).arg((*it_outergroup).m_displayName); // Skip the inner groups if the report only calls for outer group totals to be shown if (m_config.detailLevel() != MyMoneyReport::eDetailGroup) { // // Inner Groups // PivotOuterGroup::const_iterator it_innergroup = (*it_outergroup).begin(); int rownum = 0; while (it_innergroup != (*it_outergroup).end()) { // // Rows // QString innergroupdata; PivotInnerGroup::const_iterator it_row = (*it_innergroup).begin(); while (it_row != (*it_innergroup).end()) { // // Columns // QString rowdata; auto column = 0; pricePrecision = 0; // new row => new account => new precision currencyPrecision = 0; bool isUsed = it_row.value()[eActual][0].isUsed(); if (it_row.key().accountType() != eMyMoney::Account::Type::Investment) { while (column < m_numColumns) { QString lb; if (column > 0) lb = leftborder; foreach (const auto rowType, m_rowTypeList) { if (rowType == ePrice) { if (pricePrecision == 0) { if (it_row.key().isInvest()) { pricePrecision = file->currency(it_row.key().currencyId()).pricePrecision(); precision = pricePrecision; } else precision = MyMoneyMoney::denomToPrec(file->baseCurrency().smallestAccountFraction()); } else precision = pricePrecision; } else { if (currencyPrecision == 0) { if (it_row.key().isInvest()) // stock account isn't eveluated in currency, so take investment account instead currencyPrecision = MyMoneyMoney::denomToPrec(it_row.key().parent().fraction()); else currencyPrecision = MyMoneyMoney::denomToPrec(it_row.key().fraction()); precision = currencyPrecision; } else precision = currencyPrecision; } rowdata += QString("%1") .arg(coloredAmount(it_row.value()[rowType][column], QString(), precision)) .arg(lb); lb.clear(); isUsed |= it_row.value()[rowType][column].isUsed(); } ++column; } if (m_config.isShowingRowTotals()) { for (int i = 0; i < m_rowTypeList.size(); ++i) { rowdata += QString("%1") .arg(coloredAmount(it_row.value()[ m_rowTypeList[i] ].m_total, QString(), precision)) .arg(i == 0 ? leftborder : QString()); } } } else rowdata += QString(QLatin1Literal("")).arg(m_numColumns + m_rowTypeList.size()); // // Row Header // ReportAccount rowname = it_row.key(); // don't show closed accounts if they have not been used if (!rowname.isClosed() || isUsed) { innergroupdata += QString("%5%6") .arg(rownum & 0x01 ? "even" : "odd") .arg(rowname.isTopLevel() ? " id=\"topparent\"" : "") .arg("") //.arg((*it_row).m_total.isZero() ? colspan : "") // colspan the distance if this row will be blank .arg(rowname.hierarchyDepth() - 1) .arg(rowname.name().replace(QRegExp(" "), " ")) .arg((m_config.isConvertCurrency() || !rowname.isForeignCurrency()) ? QString() : QString(" (%1)").arg(rowname.currency().id())); // Don't print this row if it's going to be all zeros // TODO: Uncomment this, and deal with the case where the data // is zero, but the budget is non-zero //if ( !(*it_row).m_total.isZero() ) innergroupdata += rowdata; innergroupdata += "\n"; if (!m_containsNonBaseCurrency && rowname.isForeignCurrency()) m_containsNonBaseCurrency = true; } ++it_row; } // // Inner Row Group Totals // bool finishrow = true; QString finalRow; bool isUsed = false; if (m_config.detailLevel() == MyMoneyReport::eDetailAll && ((*it_innergroup).size() > 1)) { // Print the individual rows result += innergroupdata; if (m_config.isConvertCurrency() && m_config.isShowingColumnTotals()) { // Start the TOTALS row finalRow = QString("") .arg(rownum & 0x01 ? "even" : "odd") .arg(i18nc("Total balance", "Total")); // don't suppress display of totals isUsed = true; } else { finishrow = false; ++rownum; } } else { // Start the single INDIVIDUAL ACCOUNT row // FIXME: There is a bit of a bug here with class=leftX. There's only a finite number // of classes I can define in the .CSS file, and the user can theoretically nest deeper. // The right solution is to use style=Xem, and calculate X. Let's see if anyone complains // first :) Also applies to the row header case above. // FIXED: I found it in one of my reports and changed it to the proposed method. // This works for me (ipwizard) ReportAccount rowname = (*it_innergroup).begin().key(); isUsed |= !rowname.isClosed(); finalRow = QString("") .arg(rownum & 0x01 ? "even" : "odd") .arg(m_config.detailLevel() == MyMoneyReport::eDetailAll ? "id=\"solo\"" : "") .arg(rowname.hierarchyDepth() - 1) .arg(rowname.name().replace(QRegExp(" "), " ")) .arg((m_config.isConvertCurrency() || !rowname.isForeignCurrency()) ? QString() : QString(" (%1)").arg(rowname.currency().id())); } // Finish the row started above, unless told not to if (finishrow) { auto column = 0; isUsed |= (*it_innergroup).m_total[eActual][0].isUsed(); while (column < m_numColumns) { QString lb; if (column != 0) lb = leftborder; for (int i = 0; i < m_rowTypeList.size(); ++i) { finalRow += QString("%1") .arg(coloredAmount((*it_innergroup).m_total[ m_rowTypeList[i] ][column], QString(), precision)) .arg(i == 0 ? lb : QString()); isUsed |= (*it_innergroup).m_total[ m_rowTypeList[i] ][column].isUsed(); } column++; } if (m_config.isShowingRowTotals()) { for (int i = 0; i < m_rowTypeList.size(); ++i) { finalRow += QString("%1") .arg(coloredAmount((*it_innergroup).m_total[ m_rowTypeList[i] ].m_total, QString(), precision)) .arg(i == 0 ? leftborder : QString()); } } finalRow += "\n"; if (isUsed) { result += finalRow; ++rownum; } } ++it_innergroup; } // end while iterating on the inner groups } // end if detail level is not "group" // // Outer Row Group Totals // if (m_config.isConvertCurrency() && m_config.isShowingColumnTotals()) { result += QString("").arg(i18nc("Total balance", "Total")).arg((*it_outergroup).m_displayName); auto column = 0; while (column < m_numColumns) { QString lb; if (column != 0) lb = leftborder; for (int i = 0; i < m_rowTypeList.size(); ++i) { result += QString("%1") .arg(coloredAmount((*it_outergroup).m_total[ m_rowTypeList[i] ][column], QString(), precision)) .arg(i == 0 ? lb : QString()); } column++; } if (m_config.isShowingRowTotals()) { for (int i = 0; i < m_rowTypeList.size(); ++i) { result += QString("%1") .arg(coloredAmount((*it_outergroup).m_total[ m_rowTypeList[i] ].m_total, QString(), precision)) .arg(i == 0 ? leftborder : QString()); } } result += "\n"; } ++it_outergroup; } // end while iterating on the outergroups } // end if detail level is not "total" // // Report Totals // if (m_config.isConvertCurrency() && m_config.isShowingColumnTotals()) { result += QString("\n"); result += QString("").arg(i18n("Grand Total")); auto totalcolumn = 0; while (totalcolumn < m_numColumns) { QString lb; if (totalcolumn != 0) lb = leftborder; for (int i = 0; i < m_rowTypeList.size(); ++i) { result += QString("%1") .arg(coloredAmount(m_grid.m_total[ m_rowTypeList[i] ][totalcolumn], QString(), precision)) .arg(i == 0 ? lb : QString()); } totalcolumn++; } if (m_config.isShowingRowTotals()) { for (int i = 0; i < m_rowTypeList.size(); ++i) { result += QString("%1") .arg(coloredAmount(m_grid.m_total[ m_rowTypeList[i] ].m_total, QString(), precision)) .arg(i == 0 ? leftborder : QString()); } } result += "\n"; } result += "
%1
%2
  %2
%5%6
%1 %2
 
%1
\n"; return result; } void PivotTable::dump(const QString& file, const QString& /* context */) const { QFile g(file); g.open(QIODevice::WriteOnly); QTextStream(&g) << renderHTML(); g.close(); } void PivotTable::drawChart(KReportChartView& chartView) const { chartView.drawPivotChart(m_grid, m_config, m_numColumns, m_columnHeadings, m_rowTypeList, m_columnTypeHeaderList); } QString PivotTable::coloredAmount(const MyMoneyMoney& amount, const QString& currencySymbol, int prec) const { const auto value = amount.formatMoney(currencySymbol, prec); if (amount.isNegative()) return QString::fromLatin1("%2") .arg(KMyMoneyGlobalSettings::schemeColor(SchemeColor::Negative).name(), value); else return value; } void PivotTable::calculateBudgetDiff() { PivotGrid::iterator it_outergroup = m_grid.begin(); while (it_outergroup != m_grid.end()) { PivotOuterGroup::iterator it_innergroup = (*it_outergroup).begin(); while (it_innergroup != (*it_outergroup).end()) { PivotInnerGroup::iterator it_row = (*it_innergroup).begin(); while (it_row != (*it_innergroup).end()) { int column = m_startColumn; switch (it_row.key().accountGroup()) { case eMyMoney::Account::Type::Income: case eMyMoney::Account::Type::Asset: while (column < m_numColumns) { it_row.value()[eBudgetDiff][column] = PivotCell(it_row.value()[eActual][column] - it_row.value()[eBudget][column]); ++column; } break; case eMyMoney::Account::Type::Expense: case eMyMoney::Account::Type::Liability: while (column < m_numColumns) { it_row.value()[eBudgetDiff][column] = PivotCell(it_row.value()[eBudget][column] - it_row.value()[eActual][column]); ++column; } break; default: break; } ++it_row; } ++it_innergroup; } ++it_outergroup; } } void PivotTable::calculateForecast() { //setup forecast MyMoneyForecast forecast = KMyMoneyGlobalSettings::forecast(); //since this is a net worth forecast we want to include all account even those that are not in use forecast.setIncludeUnusedAccounts(true); //setup forecast dates if (m_endDate > QDate::currentDate()) { forecast.setForecastEndDate(m_endDate); forecast.setForecastStartDate(QDate::currentDate()); forecast.setForecastDays(QDate::currentDate().daysTo(m_endDate)); } else { forecast.setForecastStartDate(m_beginDate); forecast.setForecastEndDate(m_endDate); forecast.setForecastDays(m_beginDate.daysTo(m_endDate) + 1); } //adjust history dates if beginning date is before today if (m_beginDate < QDate::currentDate()) { forecast.setHistoryEndDate(m_beginDate.addDays(-1)); forecast.setHistoryStartDate(forecast.historyEndDate().addDays(-forecast.accountsCycle()*forecast.forecastCycles())); } //run forecast if (m_config.rowType() == MyMoneyReport::eAssetLiability) { //asset and liability forecast.doForecast(); } else { //income and expenses MyMoneyBudget budget; forecast.createBudget(budget, m_beginDate.addYears(-1), m_beginDate.addDays(-1), m_beginDate, m_endDate, false); } //go through the data and add forecast PivotGrid::iterator it_outergroup = m_grid.begin(); while (it_outergroup != m_grid.end()) { PivotOuterGroup::iterator it_innergroup = (*it_outergroup).begin(); while (it_innergroup != (*it_outergroup).end()) { PivotInnerGroup::iterator it_row = (*it_innergroup).begin(); while (it_row != (*it_innergroup).end()) { int column = m_startColumn; QDate forecastDate = m_beginDate; //check whether columns are days or months if (m_config.isColumnsAreDays()) { while (column < m_numColumns) { it_row.value()[eForecast][column] = PivotCell(forecast.forecastBalance(it_row.key(), forecastDate)); forecastDate = forecastDate.addDays(1); ++column; } } else { //if columns are months while (column < m_numColumns) { // the forecast balance is on the first day of the month see MyMoneyForecast::calculateScheduledMonthlyBalances() forecastDate = QDate(forecastDate.year(), forecastDate.month(), 1); //check that forecastDate is not over ending date if (forecastDate > m_endDate) forecastDate = m_endDate; //get forecast balance and set the corresponding column it_row.value()[eForecast][column] = PivotCell(forecast.forecastBalance(it_row.key(), forecastDate)); forecastDate = forecastDate.addMonths(1); ++column; } } ++it_row; } ++it_innergroup; } ++it_outergroup; } } void PivotTable::loadRowTypeList() { if ((m_config.isIncludingBudgetActuals()) || (!m_config.hasBudget() && !m_config.isIncludingForecast() && !m_config.isIncludingMovingAverage() && !m_config.isIncludingPrice() && !m_config.isIncludingAveragePrice()) ) { m_rowTypeList.append(eActual); m_columnTypeHeaderList.append(i18n("Actual")); } if (m_config.hasBudget()) { m_rowTypeList.append(eBudget); m_columnTypeHeaderList.append(i18n("Budget")); } if (m_config.isIncludingBudgetActuals()) { m_rowTypeList.append(eBudgetDiff); m_columnTypeHeaderList.append(i18n("Difference")); } if (m_config.isIncludingForecast()) { m_rowTypeList.append(eForecast); m_columnTypeHeaderList.append(i18n("Forecast")); } if (m_config.isIncludingMovingAverage()) { m_rowTypeList.append(eAverage); m_columnTypeHeaderList.append(i18n("Moving Average")); } if (m_config.isIncludingAveragePrice()) { m_rowTypeList.append(eAverage); m_columnTypeHeaderList.append(i18n("Moving Average Price")); } if (m_config.isIncludingPrice()) { m_rowTypeList.append(ePrice); m_columnTypeHeaderList.append(i18n("Price")); } } void PivotTable::calculateMovingAverage() { int delta = m_config.movingAverageDays() / 2; //go through the data and add the moving average PivotGrid::iterator it_outergroup = m_grid.begin(); while (it_outergroup != m_grid.end()) { PivotOuterGroup::iterator it_innergroup = (*it_outergroup).begin(); while (it_innergroup != (*it_outergroup).end()) { PivotInnerGroup::iterator it_row = (*it_innergroup).begin(); while (it_row != (*it_innergroup).end()) { int column = m_startColumn; //check whether columns are days or months if (m_config.columnType() == MyMoneyReport::eDays) { while (column < m_numColumns) { MyMoneyMoney totalPrice = MyMoneyMoney(); QDate averageStart = columnDate(column).addDays(-delta); QDate averageEnd = columnDate(column).addDays(delta); for (QDate averageDate = averageStart; averageDate <= averageEnd; averageDate = averageDate.addDays(1)) { if (m_config.isConvertCurrency()) { totalPrice += it_row.key().deepCurrencyPrice(averageDate) * it_row.key().baseCurrencyPrice(averageDate); } else { totalPrice += it_row.key().deepCurrencyPrice(averageDate); } totalPrice = totalPrice.convert(10000); } //calculate the average price MyMoneyMoney averagePrice = totalPrice / MyMoneyMoney((averageStart.daysTo(averageEnd) + 1), 1); //get the actual value, multiply by the average price and save that value MyMoneyMoney averageValue = it_row.value()[eActual][column] * averagePrice; it_row.value()[eAverage][column] = PivotCell(averageValue.convert(10000)); ++column; } } else { //if columns are months while (column < m_numColumns) { QDate averageStart = columnDate(column); //set the right start date depending on the column type switch (m_config.columnType()) { case MyMoneyReport::eYears: { averageStart = QDate(columnDate(column).year(), 1, 1); break; } case MyMoneyReport::eBiMonths: { averageStart = QDate(columnDate(column).year(), columnDate(column).month(), 1).addMonths(-1); break; } case MyMoneyReport::eQuarters: { averageStart = QDate(columnDate(column).year(), columnDate(column).month(), 1).addMonths(-1); break; } case MyMoneyReport::eMonths: { averageStart = QDate(columnDate(column).year(), columnDate(column).month(), 1); break; } case MyMoneyReport::eWeeks: { averageStart = columnDate(column).addDays(-columnDate(column).dayOfWeek() + 1); break; } default: break; } //gather the actual data and calculate the average MyMoneyMoney totalPrice = MyMoneyMoney(); QDate averageEnd = columnDate(column); for (QDate averageDate = averageStart; averageDate <= averageEnd; averageDate = averageDate.addDays(1)) { if (m_config.isConvertCurrency()) { totalPrice += it_row.key().deepCurrencyPrice(averageDate) * it_row.key().baseCurrencyPrice(averageDate); } else { totalPrice += it_row.key().deepCurrencyPrice(averageDate); } totalPrice = totalPrice.convert(10000); } MyMoneyMoney averagePrice = totalPrice / MyMoneyMoney((averageStart.daysTo(averageEnd) + 1), 1); MyMoneyMoney averageValue = it_row.value()[eActual][column] * averagePrice; //fill in the average it_row.value()[eAverage][column] = PivotCell(averageValue.convert(10000)); ++column; } } ++it_row; } ++it_innergroup; } ++it_outergroup; } } void PivotTable::fillBasePriceUnit(ERowType rowType) { MyMoneyFile* file = MyMoneyFile::instance(); QString baseCurrencyId = file->baseCurrency().id(); //get the first price date for securities QMap securityDates = securityFirstPrice(); //go through the data PivotGrid::iterator it_outergroup = m_grid.begin(); while (it_outergroup != m_grid.end()) { PivotOuterGroup::iterator it_innergroup = (*it_outergroup).begin(); while (it_innergroup != (*it_outergroup).end()) { PivotInnerGroup::iterator it_row = (*it_innergroup).begin(); while (it_row != (*it_innergroup).end()) { int column = m_startColumn; //if it is a base currency fill all the values bool firstPriceExists = false; if (it_row.key().currencyId() == baseCurrencyId) { firstPriceExists = true; } while (column < m_numColumns) { //check whether the date for that column is on or after the first price if (!firstPriceExists && securityDates.contains(it_row.key().currencyId()) && columnDate(column) >= securityDates.value(it_row.key().currencyId())) { firstPriceExists = true; } //only add the dummy value if there is a price for that date if (firstPriceExists) { //insert a unit of currency for each account it_row.value()[rowType][column] = PivotCell(MyMoneyMoney::ONE); } ++column; } ++it_row; } ++it_innergroup; } ++it_outergroup; } } QMap PivotTable::securityFirstPrice() { MyMoneyFile* file = MyMoneyFile::instance(); MyMoneyPriceList priceList = file->priceList(); QMap securityPriceDate; MyMoneyPriceList::const_iterator prices_it; for (prices_it = priceList.constBegin(); prices_it != priceList.constEnd(); ++prices_it) { MyMoneyPrice firstPrice = (*((*prices_it).constBegin())); //check the security in the from field //if it is there, check if it is older if (securityPriceDate.contains(firstPrice.from())) { if (securityPriceDate.value(firstPrice.from()) > firstPrice.date()) { securityPriceDate[firstPrice.from()] = firstPrice.date(); } } else { securityPriceDate.insert(firstPrice.from(), firstPrice.date()); } //check the security in the to field //if it is there, check if it is older if (securityPriceDate.contains(firstPrice.to())) { if (securityPriceDate.value(firstPrice.to()) > firstPrice.date()) { securityPriceDate[firstPrice.to()] = firstPrice.date(); } } else { securityPriceDate.insert(firstPrice.to(), firstPrice.date()); } } return securityPriceDate; } void PivotTable::includeInvestmentSubAccounts() { // if we're not in expert mode, we need to make sure // that all stock accounts for the selected investment // account are also selected QStringList accountList; if (m_config.accounts(accountList)) { if (!KMyMoneyGlobalSettings::expertMode()) { foreach (const auto sAccount, accountList) { auto acc = MyMoneyFile::instance()->account(sAccount); if (acc.accountType() == eMyMoney::Account::Type::Investment) { foreach (const auto sSubAccount, acc.accountList()) { if (!accountList.contains(sSubAccount)) { m_config.addAccount(sSubAccount); } } } } } } } int PivotTable::currentDateColumn() { //return -1 if the columns do not include the current date if (m_beginDate > QDate::currentDate() || m_endDate < QDate::currentDate()) { return -1; } //check the date of each column and return if it is the one for the current date //if columns are not days, return the one for the current month or year int column = m_startColumn; while (column < m_numColumns) { if (columnDate(column) >= QDate::currentDate()) { break; } column++; } //if there is no column matching the current date, return -1 if (column == m_numColumns) { column = -1; } return column; } } // namespace diff --git a/kmymoney/views/kforecastview.cpp b/kmymoney/views/kforecastview.cpp index 5253f172f..715b22215 100644 --- a/kmymoney/views/kforecastview.cpp +++ b/kmymoney/views/kforecastview.cpp @@ -1,1058 +1,1059 @@ /*************************************************************************** kforecastview.cpp ------------------- copyright : (C) 2007 by Alvaro Soliverez email : asoliverez@gmail.com ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "kforecastview.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "mymoneyutils.h" #include "mymoneyfile.h" #include "mymoneyaccount.h" +#include "mymoneyexception.h" #include "mymoneyprice.h" #include "mymoneysecurity.h" #include "kmymoneyglobalsettings.h" #include "mymoneyforecast.h" #include "mymoneybudget.h" #include "pivottable.h" #include "fixedcolumntreeview.h" #include "kreportchartview.h" #include "reportaccount.h" #include "icons.h" #include "mymoneyenums.h" using namespace reports; using namespace Icons; KForecastView::KForecastView(QWidget *parent) : QWidget(parent), m_needLoad(true), m_totalItem(0), m_assetItem(0), m_liabilityItem(0), m_incomeItem(0), m_expenseItem(0), m_chartLayout(0), m_forecastChart(0) { } KForecastView::~KForecastView() { } void KForecastView::setDefaultFocus() { QTimer::singleShot(0, m_forecastButton, SLOT(setFocus())); } void KForecastView::init() { m_needLoad = false; setupUi(this); m_forecastChart = new KReportChartView(m_tabChart); for (int i = 0; i < MaxViewTabs; ++i) m_needReload[i] = false; KSharedConfigPtr config = KSharedConfig::openConfig(); KConfigGroup grp = config->group("Last Use Settings"); m_tab->setCurrentIndex(grp.readEntry("KForecastView_LastType", 0)); m_forecastButton->setIcon(QIcon::fromTheme(g_Icons[Icon::ViewForecast])); connect(m_tab, SIGNAL(currentChanged(int)), this, SLOT(slotTabChanged(int))); connect(MyMoneyFile::instance(), SIGNAL(dataChanged()), this, SLOT(slotLoadForecast())); connect(m_forecastButton, SIGNAL(clicked()), this, SLOT(slotManualForecast())); m_forecastList->setUniformRowHeights(true); m_forecastList->setAllColumnsShowFocus(true); m_summaryList->setAllColumnsShowFocus(true); m_budgetList->setAllColumnsShowFocus(true); m_advancedList->setAlternatingRowColors(true); connect(m_forecastList, SIGNAL(itemExpanded(QTreeWidgetItem*)), this, SLOT(itemExpanded(QTreeWidgetItem*))); connect(m_forecastList, SIGNAL(itemCollapsed(QTreeWidgetItem*)), this, SLOT(itemCollapsed(QTreeWidgetItem*))); connect(m_summaryList, SIGNAL(itemExpanded(QTreeWidgetItem*)), this, SLOT(itemExpanded(QTreeWidgetItem*))); connect(m_summaryList, SIGNAL(itemCollapsed(QTreeWidgetItem*)), this, SLOT(itemCollapsed(QTreeWidgetItem*))); connect(m_budgetList, SIGNAL(itemExpanded(QTreeWidgetItem*)), this, SLOT(itemExpanded(QTreeWidgetItem*))); connect(m_budgetList, SIGNAL(itemCollapsed(QTreeWidgetItem*)), this, SLOT(itemCollapsed(QTreeWidgetItem*))); m_forecastChart->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); m_chartLayout = m_tabChart->layout(); m_chartLayout->setSpacing(6); m_chartLayout->addWidget(m_forecastChart); loadForecastSettings(); } void KForecastView::slotTabChanged(int index) { ForecastViewTab tab = static_cast(index); // remember this setting for startup KSharedConfigPtr config = KSharedConfig::openConfig(); KConfigGroup grp = config->group("Last Use Settings"); grp.writeEntry("KForecastView_LastType", QVariant(tab).toString()); loadForecast(tab); } void KForecastView::loadForecast(ForecastViewTab tab) { if (m_needReload[tab]) { switch (tab) { case ListView: loadListView(); break; case SummaryView: loadSummaryView(); break; case AdvancedView: loadAdvancedView(); break; case BudgetView: loadBudgetView(); break; case ChartView: loadChartView(); break; default: break; } m_needReload[tab] = false; } } void KForecastView::showEvent(QShowEvent* event) { if (m_needLoad) { init(); loadForecastSettings(); } emit aboutToShow(); slotTabChanged(m_tab->currentIndex()); // don't forget base class implementation QWidget::showEvent(event); } void KForecastView::slotLoadForecast() { m_needReload[SummaryView] = true; m_needReload[ListView] = true; m_needReload[AdvancedView] = true; m_needReload[BudgetView] = true; m_needReload[ChartView] = true; if (isVisible()) { //refresh settings loadForecastSettings(); slotTabChanged(m_tab->currentIndex()); } } void KForecastView::slotManualForecast() { m_needReload[SummaryView] = true; m_needReload[ListView] = true; m_needReload[AdvancedView] = true; m_needReload[BudgetView] = true; m_needReload[ChartView] = true; if (isVisible()) slotTabChanged(m_tab->currentIndex()); } void KForecastView::loadForecastSettings() { //fill the settings controls m_forecastDays->setValue(KMyMoneyGlobalSettings::forecastDays()); m_accountsCycle->setValue(KMyMoneyGlobalSettings::forecastAccountCycle()); m_beginDay->setValue(KMyMoneyGlobalSettings::beginForecastDay()); m_forecastCycles->setValue(KMyMoneyGlobalSettings::forecastCycles()); m_historyMethod->setId(radioButton11, 0); // simple moving avg m_historyMethod->setId(radioButton12, 1); // weighted moving avg m_historyMethod->setId(radioButton13, 2); // linear regression m_historyMethod->button(KMyMoneyGlobalSettings::historyMethod())->setChecked(true); switch (KMyMoneyGlobalSettings::forecastMethod()) { case 0: m_forecastMethod->setText(i18nc("Scheduled method", "Scheduled")); m_forecastCycles->setDisabled(true); m_historyMethodGroupBox->setDisabled(true); break; case 1: m_forecastMethod->setText(i18nc("History-based method", "History")); m_forecastCycles->setEnabled(true); m_historyMethodGroupBox->setEnabled(true); break; default: m_forecastMethod->setText(i18nc("Unknown forecast method", "Unknown")); break; } } void KForecastView::loadListView() { MyMoneyForecast forecast = KMyMoneyGlobalSettings::forecast(); MyMoneyFile* file = MyMoneyFile::instance(); //get the settings from current page forecast.setForecastDays(m_forecastDays->value()); forecast.setAccountsCycle(m_accountsCycle->value()); forecast.setBeginForecastDay(m_beginDay->value()); forecast.setForecastCycles(m_forecastCycles->value()); forecast.setHistoryMethod(m_historyMethod->checkedId()); forecast.doForecast(); m_forecastList->clear(); m_forecastList->setColumnCount(0); m_forecastList->setIconSize(QSize(22, 22)); m_forecastList->setSortingEnabled(true); m_forecastList->sortByColumn(0, Qt::AscendingOrder); //add columns QStringList headerLabels; headerLabels << i18n("Account"); //add cycle interval columns headerLabels << i18nc("Today's forecast", "Current"); for (int i = 1; i <= forecast.forecastDays(); ++i) { QDate forecastDate = QDate::currentDate().addDays(i); headerLabels << QLocale().toString(forecastDate, QLocale::LongFormat); } //add variation columns headerLabels << i18n("Total variation"); //set the columns m_forecastList->setHeaderLabels(headerLabels); //add default rows addTotalRow(m_forecastList, forecast); addAssetLiabilityRows(forecast); //load asset and liability forecast accounts loadAccounts(forecast, file->asset(), m_assetItem, eDetailed); loadAccounts(forecast, file->liability(), m_liabilityItem, eDetailed); adjustHeadersAndResizeToContents(m_forecastList); // add the fixed column only if the horizontal scroll bar is visible m_fixedColumnView.reset(m_forecastList->horizontalScrollBar()->isVisible() ? new FixedColumnTreeView(m_forecastList) : 0); } void KForecastView::loadSummaryView() { MyMoneyForecast forecast = KMyMoneyGlobalSettings::forecast(); QList accList; int dropMinimum; int dropZero; MyMoneyFile* file = MyMoneyFile::instance(); //get the settings from current page forecast.setForecastDays(m_forecastDays->value()); forecast.setAccountsCycle(m_accountsCycle->value()); forecast.setBeginForecastDay(m_beginDay->value()); forecast.setForecastCycles(m_forecastCycles->value()); forecast.setHistoryMethod(m_historyMethod->checkedId()); forecast.doForecast(); //add columns QStringList headerLabels; headerLabels << i18n("Account"); headerLabels << i18nc("Today's forecast", "Current"); //if beginning of forecast is today, set the begin day to next cycle to avoid repeating the first cycle int daysToBeginDay; if (QDate::currentDate() < forecast.beginForecastDate()) { daysToBeginDay = QDate::currentDate().daysTo(forecast.beginForecastDate()); } else { daysToBeginDay = forecast.accountsCycle(); } for (int i = 0; ((i*forecast.accountsCycle()) + daysToBeginDay) <= forecast.forecastDays(); ++i) { int intervalDays = ((i * forecast.accountsCycle()) + daysToBeginDay); headerLabels << i18np("1 day", "%1 days", intervalDays); } //add variation columns headerLabels << i18n("Total variation"); m_summaryList->clear(); //set the columns m_summaryList->setHeaderLabels(headerLabels); m_summaryList->setIconSize(QSize(22, 22)); m_summaryList->setSortingEnabled(true); m_summaryList->sortByColumn(0, Qt::AscendingOrder); //add default rows addTotalRow(m_summaryList, forecast); addAssetLiabilityRows(forecast); loadAccounts(forecast, file->asset(), m_assetItem, eSummary); loadAccounts(forecast, file->liability(), m_liabilityItem, eSummary); adjustHeadersAndResizeToContents(m_summaryList); //Add comments to the advice list m_adviceText->clear(); //Get all accounts of the right type to calculate forecast m_nameIdx.clear(); accList = forecast.accountList(); QList::const_iterator accList_t = accList.constBegin(); for (; accList_t != accList.constEnd(); ++accList_t) { MyMoneyAccount acc = *accList_t; if (m_nameIdx[acc.id()] != acc.id()) { //Check if the account is there m_nameIdx[acc.id()] = acc.id(); } } QMap::ConstIterator it_nc; for (it_nc = m_nameIdx.constBegin(); it_nc != m_nameIdx.constEnd(); ++it_nc) { const MyMoneyAccount& acc = file->account(*it_nc); MyMoneySecurity currency; //change currency to deep currency if account is an investment if (acc.isInvest()) { MyMoneySecurity underSecurity = file->security(acc.currencyId()); currency = file->security(underSecurity.tradingCurrency()); } else { currency = file->security(acc.currencyId()); } //Check if the account is going to be below zero or below the minimal balance in the forecast period QString minimumBalance = acc.value("minimumBalance"); MyMoneyMoney minBalance = MyMoneyMoney(minimumBalance); //Check if the account is going to be below minimal balance dropMinimum = forecast.daysToMinimumBalance(acc); //Check if the account is going to be below zero in the future dropZero = forecast.daysToZeroBalance(acc); // spit out possible warnings QString msg; // if a minimum balance has been specified, an appropriate warning will // only be shown, if the drop below 0 is on a different day or not present if (dropMinimum != -1 && !minBalance.isZero() && (dropMinimum < dropZero || dropZero == -1)) { switch (dropMinimum) { case -1: break; case 0: msg = QString("").arg(KMyMoneyGlobalSettings::schemeColor(SchemeColor::Negative).name()); msg += i18n("The balance of %1 is below the minimum balance %2 today.", acc.name(), MyMoneyUtils::formatMoney(minBalance, acc, currency)); msg += QString(""); break; default: msg = QString("").arg(KMyMoneyGlobalSettings::schemeColor(SchemeColor::Negative).name()); msg += i18np("The balance of %2 will drop below the minimum balance %3 in %1 day.", "The balance of %2 will drop below the minimum balance %3 in %1 days.", dropMinimum - 1, acc.name(), MyMoneyUtils::formatMoney(minBalance, acc, currency)); msg += QString(""); } if (!msg.isEmpty()) { m_adviceText->append(msg); } } // a drop below zero is always shown msg.clear(); switch (dropZero) { case -1: break; case 0: if (acc.accountGroup() == eMyMoney::Account::Type::Asset) { msg = QString("").arg(KMyMoneyGlobalSettings::schemeColor(SchemeColor::Negative).name()); msg += i18n("The balance of %1 is below %2 today.", acc.name(), MyMoneyUtils::formatMoney(MyMoneyMoney(), acc, currency)); msg += QString(""); break; } if (acc.accountGroup() == eMyMoney::Account::Type::Liability) { msg = i18n("The balance of %1 is above %2 today.", acc.name(), MyMoneyUtils::formatMoney(MyMoneyMoney(), acc, currency)); break; } break; default: if (acc.accountGroup() == eMyMoney::Account::Type::Asset) { msg = QString("").arg(KMyMoneyGlobalSettings::schemeColor(SchemeColor::Negative).name()); msg += i18np("The balance of %2 will drop below %3 in %1 day.", "The balance of %2 will drop below %3 in %1 days.", dropZero, acc.name(), MyMoneyUtils::formatMoney(MyMoneyMoney(), acc, currency)); msg += QString(""); break; } if (acc.accountGroup() == eMyMoney::Account::Type::Liability) { msg = i18np("The balance of %2 will raise above %3 in %1 day.", "The balance of %2 will raise above %3 in %1 days.", dropZero, acc.name(), MyMoneyUtils::formatMoney(MyMoneyMoney(), acc, currency)); break; } } if (!msg.isEmpty()) { m_adviceText->append(msg); } //advice about trends msg.clear(); MyMoneyMoney accCycleVariation = forecast.accountCycleVariation(acc); if (accCycleVariation < MyMoneyMoney()) { msg = QString("").arg(KMyMoneyGlobalSettings::schemeColor(SchemeColor::Negative).name()); msg += i18n("The account %1 is decreasing %2 per cycle.", acc.name(), MyMoneyUtils::formatMoney(accCycleVariation, acc, currency)); msg += QString(""); } if (!msg.isEmpty()) { m_adviceText->append(msg); } } m_adviceText->show(); } void KForecastView::loadAdvancedView() { MyMoneyFile* file = MyMoneyFile::instance(); QList accList; MyMoneySecurity baseCurrency = file->baseCurrency(); MyMoneyForecast forecast = KMyMoneyGlobalSettings::forecast(); int daysToBeginDay; //get the settings from current page forecast.setForecastDays(m_forecastDays->value()); forecast.setAccountsCycle(m_accountsCycle->value()); forecast.setBeginForecastDay(m_beginDay->value()); forecast.setForecastCycles(m_forecastCycles->value()); forecast.setHistoryMethod(m_historyMethod->checkedId()); forecast.doForecast(); //Get all accounts of the right type to calculate forecast m_nameIdx.clear(); accList = forecast.accountList(); QList::const_iterator accList_t = accList.constBegin(); for (; accList_t != accList.constEnd(); ++accList_t) { MyMoneyAccount acc = *accList_t; if (m_nameIdx[acc.id()] != acc.id()) { //Check if the account is there m_nameIdx[acc.id()] = acc.id(); } } //clear the list, including columns m_advancedList->clear(); m_advancedList->setColumnCount(0); m_advancedList->setIconSize(QSize(22, 22)); QStringList headerLabels; //add first column of both lists headerLabels << i18n("Account"); //if beginning of forecast is today, set the begin day to next cycle to avoid repeating the first cycle if (QDate::currentDate() < forecast.beginForecastDate()) { daysToBeginDay = QDate::currentDate().daysTo(forecast.beginForecastDate()); } else { daysToBeginDay = forecast.accountsCycle(); } //add columns for (int i = 1; ((i * forecast.accountsCycle()) + daysToBeginDay) <= forecast.forecastDays(); ++i) { headerLabels << i18n("Min Bal %1", i); headerLabels << i18n("Min Date %1", i); } for (int i = 1; ((i * forecast.accountsCycle()) + daysToBeginDay) <= forecast.forecastDays(); ++i) { headerLabels << i18n("Max Bal %1", i); headerLabels << i18n("Max Date %1", i); } headerLabels << i18nc("Average balance", "Average"); m_advancedList->setHeaderLabels(headerLabels); QTreeWidgetItem *advancedItem = 0; QMap::ConstIterator it_nc; for (it_nc = m_nameIdx.constBegin(); it_nc != m_nameIdx.constEnd(); ++it_nc) { const MyMoneyAccount& acc = file->account(*it_nc); QString amount; MyMoneyMoney amountMM; MyMoneySecurity currency; //change currency to deep currency if account is an investment if (acc.isInvest()) { MyMoneySecurity underSecurity = file->security(acc.currencyId()); currency = file->security(underSecurity.tradingCurrency()); } else { currency = file->security(acc.currencyId()); } advancedItem = new QTreeWidgetItem(m_advancedList, advancedItem, false); advancedItem->setText(0, acc.name()); advancedItem->setIcon(0, acc.accountPixmap()); int it_c = 1; // iterator for the columns of the listview //get minimum balance list QList minBalanceList = forecast.accountMinimumBalanceDateList(acc); QList::Iterator t_min; for (t_min = minBalanceList.begin(); t_min != minBalanceList.end() ; ++t_min) { QDate minDate = *t_min; amountMM = forecast.forecastBalance(acc, minDate); amount = MyMoneyUtils::formatMoney(amountMM, acc, currency); advancedItem->setText(it_c, amount); advancedItem->setTextAlignment(it_c, Qt::AlignRight | Qt::AlignVCenter); if (amountMM.isNegative()) { advancedItem->setForeground(it_c, KMyMoneyGlobalSettings::schemeColor(SchemeColor::Negative)); } it_c++; QString dateString = QLocale().toString(minDate, QLocale::ShortFormat); advancedItem->setText(it_c, dateString); advancedItem->setTextAlignment(it_c, Qt::AlignRight | Qt::AlignVCenter); if (amountMM.isNegative()) { advancedItem->setForeground(it_c, KMyMoneyGlobalSettings::schemeColor(SchemeColor::Negative)); } it_c++; } //get maximum balance list QList maxBalanceList = forecast.accountMaximumBalanceDateList(acc); QList::Iterator t_max; for (t_max = maxBalanceList.begin(); t_max != maxBalanceList.end() ; ++t_max) { QDate maxDate = *t_max; amountMM = forecast.forecastBalance(acc, maxDate); amount = MyMoneyUtils::formatMoney(amountMM, acc, currency); advancedItem->setText(it_c, amount); advancedItem->setTextAlignment(it_c, Qt::AlignRight | Qt::AlignVCenter); if (amountMM.isNegative()) { advancedItem->setForeground(it_c, KMyMoneyGlobalSettings::schemeColor(SchemeColor::Negative)); } it_c++; QString dateString = QLocale().toString(maxDate, QLocale::ShortFormat); advancedItem->setText(it_c, dateString); advancedItem->setTextAlignment(it_c, Qt::AlignRight | Qt::AlignVCenter); if (amountMM.isNegative()) { advancedItem->setForeground(it_c, KMyMoneyGlobalSettings::schemeColor(SchemeColor::Negative)); } it_c++; } //get average balance amountMM = forecast.accountAverageBalance(acc); amount = MyMoneyUtils::formatMoney(amountMM, acc, currency); advancedItem->setText(it_c, amount); advancedItem->setTextAlignment(it_c, Qt::AlignRight | Qt::AlignVCenter); if (amountMM.isNegative()) { advancedItem->setForeground(it_c, KMyMoneyGlobalSettings::schemeColor(SchemeColor::Negative)); } it_c++; } // make sure all data is shown adjustHeadersAndResizeToContents(m_advancedList); m_advancedList->show(); } void KForecastView::loadBudgetView() { MyMoneyFile* file = MyMoneyFile::instance(); MyMoneyForecast forecast = KMyMoneyGlobalSettings::forecast(); //get the settings from current page and calculate this year based on last year QDate historyEndDate = QDate(QDate::currentDate().year() - 1, 12, 31); QDate historyStartDate = historyEndDate.addDays(-m_accountsCycle->value() * m_forecastCycles->value()); QDate forecastStartDate = QDate(QDate::currentDate().year(), 1, 1); QDate forecastEndDate = QDate::currentDate().addDays(m_forecastDays->value()); forecast.setHistoryMethod(m_historyMethod->checkedId()); MyMoneyBudget budget; forecast.createBudget(budget, historyStartDate, historyEndDate, forecastStartDate, forecastEndDate, false); m_budgetList->clear(); m_budgetList->setIconSize(QSize(22, 22)); m_budgetList->setSortingEnabled(true); m_budgetList->sortByColumn(0, Qt::AscendingOrder); //add columns QStringList headerLabels; headerLabels << i18n("Account"); { QDate forecastStartDate = forecast.forecastStartDate(); QDate forecastEndDate = forecast.forecastEndDate(); //add cycle interval columns QDate f_date = forecastStartDate; for (; f_date <= forecastEndDate; f_date = f_date.addMonths(1)) { headerLabels << QDate::longMonthName(f_date.month()); } } //add total column headerLabels << i18nc("Total balance", "Total"); //set the columns m_budgetList->setHeaderLabels(headerLabels); //add default rows addTotalRow(m_budgetList, forecast); addIncomeExpenseRows(forecast); //load income and expense budget accounts loadAccounts(forecast, file->income(), m_incomeItem, eBudget); loadAccounts(forecast, file->expense(), m_expenseItem, eBudget); adjustHeadersAndResizeToContents(m_budgetList); } QList KForecastView::getAccountPrices(const MyMoneyAccount& acc) { MyMoneyFile* file = MyMoneyFile::instance(); QList prices; MyMoneySecurity security = file->baseCurrency(); try { if (acc.isInvest()) { security = file->security(acc.currencyId()); if (security.tradingCurrency() != file->baseCurrency().id()) { MyMoneySecurity sec = file->security(security.tradingCurrency()); prices += file->price(sec.id(), file->baseCurrency().id()); } } else if (acc.currencyId() != file->baseCurrency().id()) { if (acc.currencyId() != file->baseCurrency().id()) { security = file->security(acc.currencyId()); prices += file->price(acc.currencyId(), file->baseCurrency().id()); } } } catch (const MyMoneyException &e) { qDebug() << Q_FUNC_INFO << " caught exception while adding " << acc.name() << "[" << acc.id() << "]: " << e.what(); } return prices; } void KForecastView::addAssetLiabilityRows(const MyMoneyForecast& forecast) { MyMoneyFile* file = MyMoneyFile::instance(); m_assetItem = new QTreeWidgetItem(m_totalItem); m_assetItem->setText(0, file->asset().name()); m_assetItem->setIcon(0, file->asset().accountPixmap()); m_assetItem->setData(0, ForecastRole, QVariant::fromValue(forecast)); m_assetItem->setData(0, AccountRole, QVariant::fromValue(file->asset())); m_assetItem->setExpanded(true); m_liabilityItem = new QTreeWidgetItem(m_totalItem); m_liabilityItem->setText(0, file->liability().name()); m_liabilityItem->setIcon(0, file->liability().accountPixmap()); m_liabilityItem->setData(0, ForecastRole, QVariant::fromValue(forecast)); m_liabilityItem->setData(0, AccountRole, QVariant::fromValue(file->liability())); m_liabilityItem->setExpanded(true); } void KForecastView::addIncomeExpenseRows(const MyMoneyForecast& forecast) { MyMoneyFile* file = MyMoneyFile::instance(); m_incomeItem = new QTreeWidgetItem(m_totalItem); m_incomeItem->setText(0, file->income().name()); m_incomeItem->setIcon(0, file->income().accountPixmap()); m_incomeItem->setData(0, ForecastRole, QVariant::fromValue(forecast)); m_incomeItem->setData(0, AccountRole, QVariant::fromValue(file->income())); m_incomeItem->setExpanded(true); m_expenseItem = new QTreeWidgetItem(m_totalItem); m_expenseItem->setText(0, file->expense().name()); m_expenseItem->setIcon(0, file->expense().accountPixmap()); m_expenseItem->setData(0, ForecastRole, QVariant::fromValue(forecast)); m_expenseItem->setData(0, AccountRole, QVariant::fromValue(file->expense())); m_expenseItem->setExpanded(true); } void KForecastView::addTotalRow(QTreeWidget* forecastList, const MyMoneyForecast& forecast) { MyMoneyFile* file = MyMoneyFile::instance(); m_totalItem = new QTreeWidgetItem(forecastList); QFont font; font.setBold(true); m_totalItem->setFont(0, font); m_totalItem->setText(0, i18nc("Total balance", "Total")); m_totalItem->setIcon(0, file->asset().accountPixmap()); m_totalItem->setData(0, ForecastRole, QVariant::fromValue(forecast)); m_totalItem->setData(0, AccountRole, QVariant::fromValue(file->asset())); m_totalItem->setExpanded(true); } bool KForecastView::includeAccount(MyMoneyForecast& forecast, const MyMoneyAccount& acc) { MyMoneyFile* file = MyMoneyFile::instance(); if (forecast.isForecastAccount(acc)) return true; foreach (const auto sAccount, acc.accountList()) { auto account = file->account(sAccount); if (includeAccount(forecast, account)) return true; } return false; } void KForecastView::adjustHeadersAndResizeToContents(QTreeWidget *widget) { QSize sizeHint(0, widget->sizeHintForRow(0)); QTreeWidgetItem *header = widget->headerItem(); for (int i = 0; i < header->columnCount(); ++i) { if (i > 0) { header->setData(i, Qt::TextAlignmentRole, Qt::AlignRight); // make sure that the row height stays the same even when the column that has icons is not visible if (m_totalItem) { m_totalItem->setSizeHint(i, sizeHint); } } widget->resizeColumnToContents(i); } } void KForecastView::loadAccounts(MyMoneyForecast& forecast, const MyMoneyAccount& account, QTreeWidgetItem* parentItem, int forecastType) { QMap nameIdx; MyMoneyFile* file = MyMoneyFile::instance(); QTreeWidgetItem *forecastItem = 0; //Get all accounts of the right type to calculate forecast const auto accList = account.accountList(); if (accList.isEmpty()) return; foreach (const auto sAccount, accList) { auto subAccount = file->account(sAccount); //only add the account if it is a forecast account or the parent of a forecast account if (includeAccount(forecast, subAccount)) { nameIdx[subAccount.id()] = subAccount.id(); } } QMap::ConstIterator it_nc; for (it_nc = nameIdx.constBegin(); it_nc != nameIdx.constEnd(); ++it_nc) { const MyMoneyAccount subAccount = file->account(*it_nc); MyMoneySecurity currency; if (subAccount.isInvest()) { MyMoneySecurity underSecurity = file->security(subAccount.currencyId()); currency = file->security(underSecurity.tradingCurrency()); } else { currency = file->security(subAccount.currencyId()); } forecastItem = new QTreeWidgetItem(parentItem); forecastItem->setText(0, subAccount.name()); forecastItem->setIcon(0, subAccount.accountPixmap()); forecastItem->setData(0, ForecastRole, QVariant::fromValue(forecast)); forecastItem->setData(0, AccountRole, QVariant::fromValue(subAccount)); forecastItem->setExpanded(true); switch (forecastType) { case eSummary: updateSummary(forecastItem); break; case eDetailed: updateDetailed(forecastItem); break; case eBudget: updateBudget(forecastItem); break; default: break; } loadAccounts(forecast, subAccount, forecastItem, forecastType); } } void KForecastView::updateSummary(QTreeWidgetItem *item) { MyMoneyMoney amountMM; int it_c = 1; // iterator for the columns of the listview MyMoneyFile* file = MyMoneyFile::instance(); int daysToBeginDay; MyMoneyForecast forecast = item->data(0, ForecastRole).value(); if (QDate::currentDate() < forecast.beginForecastDate()) { daysToBeginDay = QDate::currentDate().daysTo(forecast.beginForecastDate()); } else { daysToBeginDay = forecast.accountsCycle(); } MyMoneyAccount account = item->data(0, AccountRole).value(); MyMoneySecurity currency; if (account.isInvest()) { MyMoneySecurity underSecurity = file->security(account.currencyId()); currency = file->security(underSecurity.tradingCurrency()); } else { currency = file->security(account.currencyId()); } //add current balance column QDate summaryDate = QDate::currentDate(); amountMM = forecast.forecastBalance(account, summaryDate); //calculate the balance in base currency for the total row setAmount(item, it_c, amountMM); setValue(item, it_c, amountMM, summaryDate); showAmount(item, it_c, amountMM, currency); it_c++; //iterate through all other columns for (QDate summaryDate = QDate::currentDate().addDays(daysToBeginDay); summaryDate <= forecast.forecastEndDate(); summaryDate = summaryDate.addDays(forecast.accountsCycle()), ++it_c) { amountMM = forecast.forecastBalance(account, summaryDate); //calculate the balance in base currency for the total row setAmount(item, it_c, amountMM); setValue(item, it_c, amountMM, summaryDate); showAmount(item, it_c, amountMM, currency); } //calculate and add variation per cycle setNegative(item, forecast.accountTotalVariation(account).isNegative()); setAmount(item, it_c, forecast.accountTotalVariation(account)); setValue(item, it_c, forecast.accountTotalVariation(account), forecast.forecastEndDate()); showAmount(item, it_c, forecast.accountTotalVariation(account), currency); } void KForecastView::updateDetailed(QTreeWidgetItem *item) { QString amount; QString vAmount; MyMoneyMoney vAmountMM; MyMoneyFile* file = MyMoneyFile::instance(); MyMoneyAccount account = item->data(0, AccountRole).value(); MyMoneySecurity currency; if (account.isInvest()) { MyMoneySecurity underSecurity = file->security(account.currencyId()); currency = file->security(underSecurity.tradingCurrency()); } else { currency = file->security(account.currencyId()); } int it_c = 1; // iterator for the columns of the listview MyMoneyForecast forecast = item->data(0, ForecastRole).value(); for (QDate forecastDate = QDate::currentDate(); forecastDate <= forecast.forecastEndDate(); ++it_c, forecastDate = forecastDate.addDays(1)) { MyMoneyMoney amountMM = forecast.forecastBalance(account, forecastDate); //calculate the balance in base currency for the total row setAmount(item, it_c, amountMM); setValue(item, it_c, amountMM, forecastDate); showAmount(item, it_c, amountMM, currency); } //calculate and add variation per cycle vAmountMM = forecast.accountTotalVariation(account); setAmount(item, it_c, vAmountMM); setValue(item, it_c, vAmountMM, forecast.forecastEndDate()); showAmount(item, it_c, vAmountMM, currency); } void KForecastView::updateBudget(QTreeWidgetItem *item) { MyMoneySecurity currency; MyMoneyMoney tAmountMM; MyMoneyForecast forecast = item->data(0, ForecastRole).value(); MyMoneyFile* file = MyMoneyFile::instance(); int it_c = 1; // iterator for the columns of the listview QDate forecastDate = forecast.forecastStartDate(); MyMoneyAccount account = item->data(0, AccountRole).value(); if (account.isInvest()) { MyMoneySecurity underSecurity = file->security(account.currencyId()); currency = file->security(underSecurity.tradingCurrency()); } else { currency = file->security(account.currencyId()); } //iterate columns for (; forecastDate <= forecast.forecastEndDate(); forecastDate = forecastDate.addMonths(1), ++it_c) { MyMoneyMoney amountMM; amountMM = forecast.forecastBalance(account, forecastDate); if (account.accountType() == eMyMoney::Account::Type::Expense) amountMM = -amountMM; tAmountMM += amountMM; setAmount(item, it_c, amountMM); setValue(item, it_c, amountMM, forecastDate); showAmount(item, it_c, amountMM, currency); } //set total column setAmount(item, it_c, tAmountMM); setValue(item, it_c, tAmountMM, forecast.forecastEndDate()); showAmount(item, it_c, tAmountMM, currency); } void KForecastView::setNegative(QTreeWidgetItem *item, bool isNegative) { if (isNegative) { for (int i = 0; i < item->columnCount(); ++i) { item->setForeground(i, KMyMoneyGlobalSettings::schemeColor(SchemeColor::Negative)); } } } void KForecastView::showAmount(QTreeWidgetItem* item, int column, const MyMoneyMoney& amount, const MyMoneySecurity& security) { item->setText(column, MyMoneyUtils::formatMoney(amount, security)); item->setTextAlignment(column, Qt::AlignRight | Qt::AlignVCenter); item->setFont(column, item->font(0)); if (amount.isNegative()) { item->setForeground(column, KMyMoneyGlobalSettings::schemeColor(SchemeColor::Negative)); } } void KForecastView::adjustParentValue(QTreeWidgetItem *item, int column, const MyMoneyMoney& value) { if (!item) return; item->setData(column, ValueRole, QVariant::fromValue(item->data(column, ValueRole).value() + value)); item->setData(column, ValueRole, QVariant::fromValue(item->data(column, ValueRole).value().convert(MyMoneyFile::instance()->baseCurrency().smallestAccountFraction()))); // if the entry has no children, // or it is the top entry // or it is currently not open // we need to display the value of it if (item->childCount() == 0 || !item->parent() || (!item->isExpanded() && item->childCount() > 0) || (item->parent() && !item->parent()->parent())) { if (item->childCount() > 0) item->setText(column, " "); MyMoneyMoney amount = item->data(column, ValueRole).value(); showAmount(item, column, amount, MyMoneyFile::instance()->baseCurrency()); } // now make sure, the upstream accounts also get notified about the value change adjustParentValue(item->parent(), column, value); } void KForecastView::setValue(QTreeWidgetItem* item, int column, const MyMoneyMoney& amount, const QDate& forecastDate) { MyMoneyAccount account = item->data(0, AccountRole).value(); //calculate the balance in base currency for the total row if (account.currencyId() != MyMoneyFile::instance()->baseCurrency().id()) { ReportAccount repAcc = ReportAccount(account.id()); MyMoneyMoney curPrice = repAcc.baseCurrencyPrice(forecastDate); MyMoneyMoney baseAmountMM = amount * curPrice; MyMoneyMoney value = baseAmountMM.convert(MyMoneyFile::instance()->baseCurrency().smallestAccountFraction()); item->setData(column, ValueRole, QVariant::fromValue(value)); adjustParentValue(item->parent(), column, value); } else { item->setData(column, ValueRole, QVariant::fromValue(item->data(column, ValueRole).value() + amount)); adjustParentValue(item->parent(), column, amount); } } void KForecastView::setAmount(QTreeWidgetItem* item, int column, const MyMoneyMoney& amount) { item->setData(column, AmountRole, QVariant::fromValue(amount)); item->setTextAlignment(column, Qt::AlignRight | Qt::AlignVCenter); } void KForecastView::itemExpanded(QTreeWidgetItem *item) { if (!item->parent() || !item->parent()->parent()) return; for (int i = 1; i < item->columnCount(); ++i) { showAmount(item, i, item->data(i, AmountRole).value(), MyMoneyFile::instance()->security(item->data(0, AccountRole).value().currencyId())); } } void KForecastView::itemCollapsed(QTreeWidgetItem *item) { for (int i = 1; i < item->columnCount(); ++i) { showAmount(item, i, item->data(i, ValueRole).value(), MyMoneyFile::instance()->baseCurrency()); } } void KForecastView::loadChartView() { MyMoneyReport::EDetailLevel detailLevel[4] = { MyMoneyReport::eDetailAll, MyMoneyReport::eDetailTop, MyMoneyReport::eDetailGroup, MyMoneyReport::eDetailTotal }; MyMoneyReport reportCfg = MyMoneyReport( MyMoneyReport::eAssetLiability, MyMoneyReport::eMonths, eMyMoney::TransactionFilter::Date::UserDefined, // overridden by the setDateFilter() call below detailLevel[m_comboDetail->currentIndex()], i18n("Net Worth Forecast"), i18n("Generated Report")); reportCfg.setChartByDefault(true); reportCfg.setChartCHGridLines(false); reportCfg.setChartSVGridLines(false); reportCfg.setChartType(MyMoneyReport::eChartLine); reportCfg.setIncludingSchedules(false); // FIXME: this causes a crash //reportCfg.setColumnsAreDays( true ); reportCfg.setChartDataLabels(false); reportCfg.setConvertCurrency(true); reportCfg.setIncludingForecast(true); reportCfg.setDateFilter(QDate::currentDate(), QDate::currentDate().addDays(m_forecastDays->value())); reports::PivotTable table(reportCfg); table.drawChart(*m_forecastChart); // Adjust the size m_forecastChart->resize(m_tab->width() - 10, m_tab->height()); //m_forecastChart->show(); m_forecastChart->update(); } diff --git a/kmymoney/views/kreportsview.cpp b/kmymoney/views/kreportsview.cpp index 96f64bc76..7e0491873 100644 --- a/kmymoney/views/kreportsview.cpp +++ b/kmymoney/views/kreportsview.cpp @@ -1,1819 +1,1820 @@ /*************************************************************************** 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.h" // ---------------------------------------------------------------------------- // 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 "kmymoneyglobalsettings.h" #include "querytable.h" #include "objectinfotable.h" #include "kreportconfigurationfilterdlg.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 "../widgets/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" /** * KReportsView::KReportTab Implementation */ KReportsView::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_report(report), m_deleteMe(false), m_chartEnabled(false), m_showingChart(report.isChartByDefault()), m_needReload(true), m_table(0) { m_layout->setSpacing(6); m_tableView->setPage(new MyQWebEnginePage(m_tableView)); m_tableView->setZoomFactor(KMyMoneyGlobalSettings::zoomFactor()); //set button icons m_control->ui->buttonChart->setIcon(QIcon::fromTheme(g_Icons[Icon::OfficeChartLine])); m_control->ui->buttonClose->setIcon(QIcon::fromTheme(g_Icons[Icon::DocumentClose])); m_control->ui->buttonConfigure->setIcon(QIcon::fromTheme(g_Icons[Icon::Configure])); m_control->ui->buttonCopy->setIcon(QIcon::fromTheme(g_Icons[Icon::EditCopy])); m_control->ui->buttonDelete->setIcon(QIcon::fromTheme(g_Icons[Icon::EditDelete])); m_control->ui->buttonExport->setIcon(QIcon::fromTheme(g_Icons[Icon::DocumentExport])); m_control->ui->buttonNew->setIcon(QIcon::fromTheme(g_Icons[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); connect(m_control->ui->buttonChart, SIGNAL(clicked()), eventHandler, SLOT(slotToggleChart())); connect(m_control->ui->buttonConfigure, SIGNAL(clicked()), eventHandler, SLOT(slotConfigure())); connect(m_control->ui->buttonNew, SIGNAL(clicked()), eventHandler, SLOT(slotDuplicate())); connect(m_control->ui->buttonCopy, SIGNAL(clicked()), eventHandler, SLOT(slotCopyView())); connect(m_control->ui->buttonExport, SIGNAL(clicked()), eventHandler, SLOT(slotSaveView())); connect(m_control->ui->buttonDelete, SIGNAL(clicked()), eventHandler, SLOT(slotDelete())); connect(m_control->ui->buttonClose, SIGNAL(clicked()), eventHandler, SLOT(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, QIcon::fromTheme(g_Icons[Icon::Spreadsheet]), report.name()); parent->setTabEnabled(tabNr, true); parent->setCurrentIndex(tabNr); // get users character set encoding m_encoding = QTextCodec::codecForLocale()->name(); } KReportsView::KReportTab::~KReportTab() { delete m_table; } void KReportsView::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 KReportsView::KReportTab::copyToClipboard() { QMimeData* pMimeData = new QMimeData(); pMimeData->setHtml(m_table->renderReport(QLatin1String("html"), m_encoding, m_report.name(), true)); QApplication::clipboard()->setMimeData(pMimeData); } void KReportsView::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 KReportsView::KReportTab::loadTab() { m_needReload = true; if (isVisible()) { m_needReload = false; updateReport(); } } void KReportsView::KReportTab::showEvent(QShowEvent * event) { if (m_needReload) { m_needReload = false; updateReport(); } QWidget::showEvent(event); } void KReportsView::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() == MyMoneyReport::ePivotTable) { m_table = new PivotTable(m_report); m_chartEnabled = true; } else if (m_report.reportType() == MyMoneyReport::eQueryTable) { m_table = new QueryTable(m_report); m_chartEnabled = false; } else if (m_report.reportType() == MyMoneyReport::eInfoTable) { m_table = new ObjectInfoTable(m_report); m_chartEnabled = false; } m_control->ui->buttonChart->setEnabled(m_chartEnabled); m_showingChart = !m_showingChart; toggleChart(); } void KReportsView::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(QIcon::fromTheme(g_Icons[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(QIcon::fromTheme(g_Icons[Icon::ViewFinancialList])); } m_showingChart = ! m_showingChart; } void KReportsView::KReportTab::updateDataRange() { QList grids = m_chartView->coordinatePlane()->gridDimensionsList(); // get dimmensions of ploted 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); } /** * KReportsView Implementation */ KReportsView::KReportsView(QWidget *parent) : KMyMoneyViewBase(parent), m_needReload(false), m_needLoad(true), m_reportListView(0) { } void KReportsView::setDefaultFocus() { QTimer::singleShot(0, m_tocTreeWidget, SLOT(setFocus())); } void KReportsView::init() { m_needLoad = false; auto vbox = new QVBoxLayout(this); setLayout(vbox); vbox->setSpacing(6); vbox->setMargin(0); // build reports toc setColumnsAlreadyAdjusted(false); m_reportTabWidget = new QTabWidget(this); 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_listTabLayout->addWidget(m_tocTreeWidget); m_reportTabWidget->addTab(m_listTab, i18n("Reports")); connect(m_reportTabWidget, SIGNAL(tabCloseRequested(int)), this, SLOT(slotClose(int))); connect(m_tocTreeWidget, &QTreeWidget::itemActivated, this, &KReportsView::slotItemDoubleClicked); connect(m_tocTreeWidget, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(slotListContextMenu(QPoint))); connect(MyMoneyFile::instance(), SIGNAL(dataChanged()), this, SLOT(slotLoadView())); } void KReportsView::showEvent(QShowEvent * event) { if (m_needLoad) init(); emit aboutToShow(View::Reports); if (m_needReload) { loadView(); m_needReload = false; } KReportTab* tab = dynamic_cast(m_reportTabWidget->currentWidget()); if (tab) emit reportSelected(tab->report()); else emit reportSelected(MyMoneyReport()); // don't forget base class implementation KMyMoneyViewBase::showEvent(event); } void KReportsView::slotLoadView() { m_needReload = true; if (isVisible()) { loadView(); m_needReload = false; } } void KReportsView::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++) { QTreeWidgetItem* 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()) { QString 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; QString 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; QString 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!! KReportTab* 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 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 ledgerSelected(id, tid); } else { qWarning() << i18n("Unknown view '%1' in KReportsView::slotOpenUrl()", qPrintable(view)); } } void KReportsView::slotPrintView() { KReportTab* tab = dynamic_cast(m_reportTabWidget->currentWidget()); if (tab) tab->print(); } void KReportsView::slotCopyView() { KReportTab* tab = dynamic_cast(m_reportTabWidget->currentWidget()); if (tab) tab->copyToClipboard(); } void KReportsView::slotSaveView() { KReportTab* tab = dynamic_cast(m_reportTabWidget->currentWidget()); if (tab) { 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, &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", e.what())); } } } } void KReportsView::slotConfigure() { QString cm = "KReportsView::slotConfigure"; KReportTab* tab = dynamic_cast(m_reportTabWidget->currentWidget()); if (!tab) // nothing to do return; int tabNr = 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); m_reportTabWidget->setTabText(tabNr, newreport.name()); m_reportTabWidget->setCurrentIndex(tabNr) ; } else { MyMoneyFile::instance()->addReport(newreport); ft.commit(); QString reportGroupName = newreport.group(); // find report group TocItemGroup* tocItemGroup = 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(m_reportTabWidget, error, i18n("Critical Error")); // cleanup delete dlg; return; } // do not add TocItemReport to TocItemGroup here, // this is done in loadView addReportTab(newreport); } } catch (const MyMoneyException &e) { KMessageBox::error(this, i18n("Failed to configure report: %1", e.what())); } } delete dlg; } void KReportsView::slotDuplicate() { QString cm = "KReportsView::slotDuplicate"; KReportTab* tab = dynamic_cast(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 = 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(m_reportTabWidget, error, i18n("Critical Error")); // cleanup delete dlg; return; } // do not add TocItemReport to TocItemGroup here, // this is done in loadView 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(m_reportTabWidget, error, i18n("Critical Error")); } } delete dlg; } void KReportsView::slotDelete() { KReportTab* tab = dynamic_cast(m_reportTabWidget->currentWidget()); if (!tab) { // nothing to do return; } MyMoneyReport report = tab->report(); if (! report.id().isEmpty()) { if (KMessageBox::Continue == deleteReportDialog(report.name())) { // close the tab and then remove the report so that it is not // generated again during the following loadView() call slotClose(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?")); } } int KReportsView::deleteReportDialog(const QString &reportName) { return KMessageBox::warningContinueCancel(this, QString("") + i18n("Are you sure you want to delete report %1? There is no way to recover it.", reportName) + QString(""), i18n("Delete Report?")); } void KReportsView::slotOpenReport(const QString& id) { if (id.isEmpty()) { // nothing to do return; } KReportTab* page = 0; // Find the tab which contains the report int index = 1; while (index < m_reportTabWidget->count()) { KReportTab* current = dynamic_cast(m_reportTabWidget->widget(index)); if (current->report().id() == id) { page = current; break; } ++index; } // Show the tab, or create a new one, as needed if (page) m_reportTabWidget->setCurrentIndex(index); else addReportTab(MyMoneyFile::instance()->report(id)); } void KReportsView::slotOpenReport(const MyMoneyReport& report) { 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 < m_reportTabWidget->count()) { KReportTab* current = dynamic_cast(m_reportTabWidget->widget(index)); if (current->report().name() == report.name()) { page = current; break; } ++index; } // Show the tab, or create a new one, as needed if (page) m_reportTabWidget->setCurrentIndex(index); else addReportTab(report); } void KReportsView::slotItemDoubleClicked(QTreeWidgetItem* item, int) { TocItem* tocItem = dynamic_cast(item); if (!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 < m_reportTabWidget->count()) { KReportTab* current = dynamic_cast(m_reportTabWidget->widget(index)); // 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) m_reportTabWidget->setCurrentIndex(index); else addReportTab(report); } void KReportsView::slotToggleChart() { KReportTab* tab = dynamic_cast(m_reportTabWidget->currentWidget()); if (tab) tab->toggleChart(); } void KReportsView::slotCloseCurrent() { slotClose(m_reportTabWidget->currentIndex()); } void KReportsView::slotClose(int index) { KReportTab* tab = dynamic_cast(m_reportTabWidget->widget(index)); if (tab) { m_reportTabWidget->removeTab(index); tab->setReadyToDelete(true); } } void KReportsView::slotCloseAll() { if(!m_needLoad) { while (true) { KReportTab* tab = dynamic_cast(m_reportTabWidget->widget(1)); if (tab) { m_reportTabWidget->removeTab(1); tab->setReadyToDelete(true); } else break; } } } void KReportsView::addReportTab(const MyMoneyReport& report) { new KReportTab(m_reportTabWidget, report, this); } void KReportsView::slotListContextMenu(const QPoint & p) { QTreeWidgetItem *item = m_tocTreeWidget->itemAt(p); if (!item) { return; } TocItem* tocItem = dynamic_cast(item); if (!tocItem->isReport()) { // currently there is no context menu for reportgroup items return; } QMenu* 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(i18n("&New report"), this, SLOT(slotNewFromList())); // Only add this option if it's a custom report. Default reports cannot be deleted TocItemReport* reportTocItem = dynamic_cast(tocItem); MyMoneyReport& report = reportTocItem->getReport(); if (! report.id().isEmpty()) { contextmenu->addAction(i18n("&Delete"), this, SLOT(slotDeleteFromList())); } contextmenu->popup(m_tocTreeWidget->mapToGlobal(p)); } void KReportsView::slotOpenFromList() { TocItem* tocItem = dynamic_cast(m_tocTreeWidget->currentItem()); if (tocItem) slotItemDoubleClicked(tocItem, 0); } void KReportsView::slotConfigureFromList() { TocItem* tocItem = dynamic_cast(m_tocTreeWidget->currentItem()); if (tocItem) { slotItemDoubleClicked(tocItem, 0); slotConfigure(); } } void KReportsView::slotNewFromList() { TocItem* tocItem = dynamic_cast(m_tocTreeWidget->currentItem()); if (tocItem) { slotItemDoubleClicked(tocItem, 0); slotDuplicate(); } } void KReportsView::slotDeleteFromList() { TocItem* tocItem = dynamic_cast(m_tocTreeWidget->currentItem()); if (tocItem) { TocItemReport* 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 == deleteReportDialog(report.name())) { // check if report's tab is open; start from 1 because 0 is toc tab for (int i = 1; i < m_reportTabWidget->count(); ++i) { KReportTab* tab = dynamic_cast(m_reportTabWidget->widget(i)); if (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::defaultReports(QList& groups) { { ReportGroup list("Income and Expenses", i18n("Income and Expenses")); list.push_back(MyMoneyReport( MyMoneyReport::eExpenseIncome, MyMoneyReport::eMonths, TransactionFilter::Date::CurrentMonth, MyMoneyReport::eDetailAll, i18n("Income and Expenses This Month"), i18n("Default Report") )); list.push_back(MyMoneyReport( MyMoneyReport::eExpenseIncome, MyMoneyReport::eMonths, TransactionFilter::Date::YearToDate, MyMoneyReport::eDetailAll, i18n("Income and Expenses This Year"), i18n("Default Report") )); list.push_back(MyMoneyReport( MyMoneyReport::eExpenseIncome, MyMoneyReport::eYears, TransactionFilter::Date::All, MyMoneyReport::eDetailAll, i18n("Income and Expenses By Year"), i18n("Default Report") )); list.push_back(MyMoneyReport( MyMoneyReport::eExpenseIncome, MyMoneyReport::eMonths, TransactionFilter::Date::Last12Months, MyMoneyReport::eDetailTop, i18n("Income and Expenses Graph"), i18n("Default Report") )); list.back().setChartByDefault(true); list.back().setChartType(MyMoneyReport::eChartLine); list.back().setChartDataLabels(false); list.push_back(MyMoneyReport( MyMoneyReport::eExpenseIncome, MyMoneyReport::eMonths, TransactionFilter::Date::YearToDate, MyMoneyReport::eDetailGroup, i18n("Income and Expenses Pie Chart"), i18n("Default Report") )); list.back().setChartByDefault(true); list.back().setChartType(MyMoneyReport::eChartPie); list.back().setShowingRowTotals(false); groups.push_back(list); } { ReportGroup list("Net Worth", i18n("Net Worth")); list.push_back(MyMoneyReport( MyMoneyReport::eAssetLiability, MyMoneyReport::eMonths, TransactionFilter::Date::YearToDate, MyMoneyReport::eDetailTop, i18n("Net Worth By Month"), i18n("Default Report") )); list.push_back(MyMoneyReport( MyMoneyReport::eAssetLiability, MyMoneyReport::eMonths, TransactionFilter::Date::Today, MyMoneyReport::eDetailTop, i18n("Net Worth Today"), i18n("Default Report") )); list.push_back(MyMoneyReport( MyMoneyReport::eAssetLiability, MyMoneyReport::eYears, TransactionFilter::Date::All, MyMoneyReport::eDetailTop, i18n("Net Worth By Year"), i18n("Default Report") )); list.push_back(MyMoneyReport( MyMoneyReport::eAssetLiability, MyMoneyReport::eMonths, TransactionFilter::Date::Next7Days, MyMoneyReport::eDetailTop, i18n("7-day Cash Flow Forecast"), i18n("Default Report") )); list.back().setIncludingSchedules(true); list.back().setColumnsAreDays(true); list.push_back(MyMoneyReport( MyMoneyReport::eAssetLiability, MyMoneyReport::eMonths, TransactionFilter::Date::Last12Months, MyMoneyReport::eDetailTotal, i18n("Net Worth Graph"), i18n("Default Report") )); list.back().setChartByDefault(true); list.back().setChartCHGridLines(false); list.back().setChartSVGridLines(false); list.back().setChartType(MyMoneyReport::eChartLine); list.push_back(MyMoneyReport( MyMoneyReport::eInstitution, MyMoneyReport::eQCnone, TransactionFilter::Date::YearToDate, MyMoneyReport::eDetailTop, i18n("Account Balances by Institution"), i18n("Default Report") )); list.push_back(MyMoneyReport( MyMoneyReport::eAccountType, MyMoneyReport::eQCnone, TransactionFilter::Date::YearToDate, MyMoneyReport::eDetailTop, i18n("Account Balances by Type"), i18n("Default Report") )); groups.push_back(list); } { ReportGroup list("Transactions", i18n("Transactions")); list.push_back(MyMoneyReport( MyMoneyReport::eAccount, MyMoneyReport::eQCnumber | MyMoneyReport::eQCpayee | MyMoneyReport::eQCcategory | MyMoneyReport::eQCtag | MyMoneyReport::eQCbalance, TransactionFilter::Date::YearToDate, MyMoneyReport::eDetailAll, i18n("Transactions by Account"), i18n("Default Report") )); //list.back().setConvertCurrency(false); list.push_back(MyMoneyReport( MyMoneyReport::eCategory, MyMoneyReport::eQCnumber | MyMoneyReport::eQCpayee | MyMoneyReport::eQCaccount | MyMoneyReport::eQCtag, TransactionFilter::Date::YearToDate, MyMoneyReport::eDetailAll, i18n("Transactions by Category"), i18n("Default Report") )); list.push_back(MyMoneyReport( MyMoneyReport::ePayee, MyMoneyReport::eQCnumber | MyMoneyReport::eQCcategory | MyMoneyReport::eQCtag, TransactionFilter::Date::YearToDate, MyMoneyReport::eDetailAll, i18n("Transactions by Payee"), i18n("Default Report") )); list.push_back(MyMoneyReport( MyMoneyReport::eTag, MyMoneyReport::eQCnumber | MyMoneyReport::eQCcategory, TransactionFilter::Date::YearToDate, MyMoneyReport::eDetailAll, i18n("Transactions by Tag"), i18n("Default Report") )); list.push_back(MyMoneyReport( MyMoneyReport::eMonth, MyMoneyReport::eQCnumber | MyMoneyReport::eQCpayee | MyMoneyReport::eQCcategory | MyMoneyReport::eQCtag, TransactionFilter::Date::YearToDate, MyMoneyReport::eDetailAll, i18n("Transactions by Month"), i18n("Default Report") )); list.push_back(MyMoneyReport( MyMoneyReport::eWeek, MyMoneyReport::eQCnumber | MyMoneyReport::eQCpayee | MyMoneyReport::eQCcategory | MyMoneyReport::eQCtag, TransactionFilter::Date::YearToDate, MyMoneyReport::eDetailAll, i18n("Transactions by Week"), i18n("Default Report") )); list.push_back(MyMoneyReport( MyMoneyReport::eAccount, MyMoneyReport::eQCloan, TransactionFilter::Date::All, MyMoneyReport::eDetailAll, i18n("Loan Transactions"), i18n("Default Report") )); list.back().setLoansOnly(true); list.push_back(MyMoneyReport( MyMoneyReport::eAccountReconcile, MyMoneyReport::eQCnumber | MyMoneyReport::eQCpayee | MyMoneyReport::eQCcategory | MyMoneyReport::eQCbalance, TransactionFilter::Date::Last3Months, MyMoneyReport::eDetailAll, i18n("Transactions by Reconciliation Status"), i18n("Default Report") )); groups.push_back(list); } { ReportGroup list("CashFlow", i18n("Cash Flow")); list.push_back(MyMoneyReport( MyMoneyReport::eCashFlow, MyMoneyReport::eQCnumber | MyMoneyReport::eQCpayee | MyMoneyReport::eQCaccount, TransactionFilter::Date::YearToDate, MyMoneyReport::eDetailAll, i18n("Cash Flow Transactions This Month"), i18n("Default Report") )); groups.push_back(list); } { ReportGroup list("Investments", i18n("Investments")); list.push_back(MyMoneyReport( MyMoneyReport::eTopAccount, MyMoneyReport::eQCaction | MyMoneyReport::eQCshares | MyMoneyReport::eQCprice, TransactionFilter::Date::YearToDate, MyMoneyReport::eDetailAll, i18n("Investment Transactions"), i18n("Default Report") )); list.back().setInvestmentsOnly(true); list.push_back(MyMoneyReport( MyMoneyReport::eAccountByTopAccount, MyMoneyReport::eQCshares | MyMoneyReport::eQCprice, TransactionFilter::Date::YearToDate, MyMoneyReport::eDetailAll, i18n("Investment Holdings by Account"), i18n("Default Report") )); list.back().setInvestmentsOnly(true); list.push_back(MyMoneyReport( MyMoneyReport::eEquityType, MyMoneyReport::eQCshares | MyMoneyReport::eQCprice, TransactionFilter::Date::YearToDate, MyMoneyReport::eDetailAll, i18n("Investment Holdings by Type"), i18n("Default Report") )); list.back().setInvestmentsOnly(true); list.push_back(MyMoneyReport( MyMoneyReport::eAccountByTopAccount, MyMoneyReport::eQCperformance, TransactionFilter::Date::YearToDate, MyMoneyReport::eDetailAll, i18n("Investment Performance by Account"), i18n("Default Report") )); list.back().setInvestmentsOnly(true); list.push_back(MyMoneyReport( MyMoneyReport::eEquityType, MyMoneyReport::eQCperformance, TransactionFilter::Date::YearToDate, MyMoneyReport::eDetailAll, i18n("Investment Performance by Type"), i18n("Default Report") )); list.back().setInvestmentsOnly(true); list.push_back(MyMoneyReport( MyMoneyReport::eAccountByTopAccount, MyMoneyReport::eQCcapitalgain, TransactionFilter::Date::YearToDate, MyMoneyReport::eDetailAll, i18n("Investment Capital Gains by Account"), i18n("Default Report") )); list.back().setInvestmentsOnly(true); list.push_back(MyMoneyReport( MyMoneyReport::eEquityType, MyMoneyReport::eQCcapitalgain, TransactionFilter::Date::YearToDate, MyMoneyReport::eDetailAll, i18n("Investment Capital Gains by Type"), i18n("Default Report") )); list.back().setInvestmentsOnly(true); list.push_back(MyMoneyReport( MyMoneyReport::eAssetLiability, MyMoneyReport::eMonths, TransactionFilter::Date::Today, MyMoneyReport::eDetailAll, i18n("Investment Holdings Pie"), i18n("Default Report") )); list.back().setChartByDefault(true); list.back().setChartCHGridLines(false); list.back().setChartSVGridLines(false); list.back().setChartType(MyMoneyReport::eChartPie); list.back().setInvestmentsOnly(true); list.push_back(MyMoneyReport( MyMoneyReport::eAssetLiability, MyMoneyReport::eMonths, TransactionFilter::Date::Last12Months, MyMoneyReport::eDetailAll, i18n("Investment Worth Graph"), i18n("Default Report") )); list.back().setChartByDefault(true); list.back().setChartCHGridLines(false); list.back().setChartSVGridLines(false); list.back().setChartType(MyMoneyReport::eChartLine); list.back().setColumnsAreDays(true); list.back().setInvestmentsOnly(true); list.push_back(MyMoneyReport( MyMoneyReport::eAssetLiability, MyMoneyReport::eMonths, TransactionFilter::Date::Last12Months, MyMoneyReport::eDetailAll, i18n("Investment Price Graph"), i18n("Default Report") )); list.back().setChartByDefault(true); list.back().setChartCHGridLines(false); list.back().setChartSVGridLines(false); list.back().setChartType(MyMoneyReport::eChartLine); 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( MyMoneyReport::eAssetLiability, MyMoneyReport::eMonths, TransactionFilter::Date::Last12Months, MyMoneyReport::eDetailAll, i18n("Investment Moving Average Price Graph"), i18n("Default Report") )); list.back().setChartByDefault(true); list.back().setChartCHGridLines(false); list.back().setChartSVGridLines(false); list.back().setChartType(MyMoneyReport::eChartLine); 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( MyMoneyReport::eAssetLiability, MyMoneyReport::eMonths, TransactionFilter::Date::Last30Days, MyMoneyReport::eDetailAll, i18n("Investment Moving Average"), i18n("Default Report") )); list.back().setChartCHGridLines(false); list.back().setChartSVGridLines(false); list.back().setChartType(MyMoneyReport::eChartLine); list.back().setColumnsAreDays(true); list.back().setInvestmentsOnly(true); list.back().setIncludingBudgetActuals(false); list.back().setIncludingMovingAverage(true); list.back().setMovingAverageDays(10); list.push_back(MyMoneyReport( MyMoneyReport::eAssetLiability, MyMoneyReport::eMonths, TransactionFilter::Date::Last30Days, MyMoneyReport::eDetailAll, i18n("Investment Moving Average vs Actual"), i18n("Default Report") )); list.back().setChartByDefault(true); list.back().setChartCHGridLines(false); list.back().setChartSVGridLines(false); list.back().setChartType(MyMoneyReport::eChartLine); 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( MyMoneyReport::eCategory, MyMoneyReport::eQCnumber | MyMoneyReport::eQCpayee | MyMoneyReport::eQCaccount, TransactionFilter::Date::YearToDate, MyMoneyReport::eDetailAll, i18n("Tax Transactions by Category"), i18n("Default Report") )); list.back().setTax(true); list.push_back(MyMoneyReport( MyMoneyReport::ePayee, MyMoneyReport::eQCnumber | MyMoneyReport::eQCcategory | MyMoneyReport::eQCaccount, TransactionFilter::Date::YearToDate, MyMoneyReport::eDetailAll, i18n("Tax Transactions by Payee"), i18n("Default Report") )); list.back().setTax(true); list.push_back(MyMoneyReport( MyMoneyReport::eCategory, MyMoneyReport::eQCnumber | MyMoneyReport::eQCpayee | MyMoneyReport::eQCaccount, TransactionFilter::Date::LastFiscalYear, MyMoneyReport::eDetailAll, i18n("Tax Transactions by Category Last Fiscal Year"), i18n("Default Report") )); list.back().setTax(true); list.push_back(MyMoneyReport( MyMoneyReport::ePayee, MyMoneyReport::eQCnumber | MyMoneyReport::eQCcategory | MyMoneyReport::eQCaccount, TransactionFilter::Date::LastFiscalYear, MyMoneyReport::eDetailAll, 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( MyMoneyReport::eBudgetActual, MyMoneyReport::eMonths, TransactionFilter::Date::YearToDate, MyMoneyReport::eDetailAll, i18n("Budgeted vs. Actual This Year"), i18n("Default Report") )); list.back().setShowingRowTotals(true); list.back().setBudget("Any", true); list.push_back(MyMoneyReport( MyMoneyReport::eBudgetActual, MyMoneyReport::eMonths, TransactionFilter::Date::YearToMonth, MyMoneyReport::eDetailAll, 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( MyMoneyReport::eBudgetActual, MyMoneyReport::eMonths, TransactionFilter::Date::CurrentMonth, MyMoneyReport::eDetailAll, i18n("Monthly Budgeted vs. Actual"), i18n("Default Report") )); list.back().setBudget("Any", true); list.push_back(MyMoneyReport( MyMoneyReport::eBudgetActual, MyMoneyReport::eMonths, TransactionFilter::Date::CurrentYear, MyMoneyReport::eDetailAll, i18n("Yearly Budgeted vs. Actual"), i18n("Default Report") )); list.back().setBudget("Any", true); list.back().setShowingRowTotals(true); list.push_back(MyMoneyReport( MyMoneyReport::eBudget, MyMoneyReport::eMonths, TransactionFilter::Date::CurrentMonth, MyMoneyReport::eDetailAll, i18n("Monthly Budget"), i18n("Default Report") )); list.back().setBudget("Any", false); list.push_back(MyMoneyReport( MyMoneyReport::eBudget, MyMoneyReport::eMonths, TransactionFilter::Date::CurrentYear, MyMoneyReport::eDetailAll, i18n("Yearly Budget"), i18n("Default Report") )); list.back().setBudget("Any", false); list.back().setShowingRowTotals(true); list.push_back(MyMoneyReport( MyMoneyReport::eBudgetActual, MyMoneyReport::eMonths, TransactionFilter::Date::CurrentYear, MyMoneyReport::eDetailGroup, 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(MyMoneyReport::eChartLine); groups.push_back(list); } { ReportGroup list("Forecast", i18n("Forecast")); list.push_back(MyMoneyReport( MyMoneyReport::eAssetLiability, MyMoneyReport::eMonths, TransactionFilter::Date::Next12Months, MyMoneyReport::eDetailTop, i18n("Forecast By Month"), i18n("Default Report") )); list.back().setIncludingForecast(true); list.push_back(MyMoneyReport( MyMoneyReport::eAssetLiability, MyMoneyReport::eMonths, TransactionFilter::Date::NextQuarter, MyMoneyReport::eDetailTop, i18n("Forecast Next Quarter"), i18n("Default Report") )); list.back().setColumnsAreDays(true); list.back().setIncludingForecast(true); list.push_back(MyMoneyReport( MyMoneyReport::eExpenseIncome, MyMoneyReport::eMonths, TransactionFilter::Date::CurrentYear, MyMoneyReport::eDetailTop, i18n("Income and Expenses Forecast This Year"), i18n("Default Report") )); list.back().setIncludingForecast(true); list.push_back(MyMoneyReport( MyMoneyReport::eAssetLiability, MyMoneyReport::eMonths, TransactionFilter::Date::Next3Months, MyMoneyReport::eDetailTotal, 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(MyMoneyReport::eChartLine); groups.push_back(list); } { ReportGroup list("Information", i18n("General Information")); list.push_back(MyMoneyReport( MyMoneyReport::eSchedule, MyMoneyReport::eMonths, TransactionFilter::Date::Next12Months, MyMoneyReport::eDetailAll, i18n("Schedule Information"), i18n("Default Report") )); list.back().setDetailLevel(MyMoneyReport::eDetailAll); list.push_back(MyMoneyReport( MyMoneyReport::eSchedule, MyMoneyReport::eMonths, TransactionFilter::Date::Next12Months, MyMoneyReport::eDetailAll, i18n("Schedule Summary Information"), i18n("Default Report") )); list.back().setDetailLevel(MyMoneyReport::eDetailTop); list.push_back(MyMoneyReport( MyMoneyReport::eAccountInfo, MyMoneyReport::eMonths, TransactionFilter::Date::Today, MyMoneyReport::eDetailAll, i18n("Account Information"), i18n("Default Report") )); list.back().setConvertCurrency(false); list.push_back(MyMoneyReport( MyMoneyReport::eAccountLoanInfo, MyMoneyReport::eMonths, TransactionFilter::Date::Today, MyMoneyReport::eDetailAll, i18n("Loan Information"), i18n("Default Report") )); list.back().setConvertCurrency(false); groups.push_back(list); } } bool KReportsView::columnsAlreadyAdjusted() { return m_columnsAlreadyAdjusted; } void KReportsView::setColumnsAlreadyAdjusted(bool adjusted) { m_columnsAlreadyAdjusted = adjusted; } void KReportsView::restoreTocExpandState(QMap& expandStates) { for (int 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); } } } } // 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/views/newspliteditor.cpp b/kmymoney/views/newspliteditor.cpp index 9e61c5f14..29541f3db 100644 --- a/kmymoney/views/newspliteditor.cpp +++ b/kmymoney/views/newspliteditor.cpp @@ -1,384 +1,385 @@ /*************************************************************************** newspliteditor.cpp ------------------- begin : Sat Apr 9 2016 copyright : (C) 2016 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 "newspliteditor.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include // ---------------------------------------------------------------------------- // Project Includes #include "creditdebithelper.h" #include "kmymoneyutils.h" #include "kmymoneyaccountcombo.h" #include "models.h" #include "accountsmodel.h" #include "costcentermodel.h" #include "ledgermodel.h" #include "splitmodel.h" #include "mymoneyaccount.h" +#include "mymoneyexception.h" #include "ui_newspliteditor.h" #include "widgethintframe.h" #include "ledgerview.h" #include "icons/icons.h" #include "mymoneyenums.h" #include "modelenums.h" using namespace Icons; struct NewSplitEditor::Private { Private(NewSplitEditor* parent) : ui(new Ui_NewSplitEditor) , accountsModel(new AccountNamesFilterProxyModel(parent)) , costCenterModel(new QSortFilterProxyModel(parent)) , splitModel(0) , accepted(false) , costCenterRequired(false) , costCenterOk(false) , showValuesInverted(false) { accountsModel->setObjectName("AccountNamesFilterProxyModel"); costCenterModel->setObjectName("SortedCostCenterModel"); statusModel.setObjectName("StatusModel"); costCenterModel->setSortLocaleAware(true); costCenterModel->setSortCaseSensitivity(Qt::CaseInsensitive); createStatusEntry(eMyMoney::Split::State::NotReconciled); createStatusEntry(eMyMoney::Split::State::Cleared); createStatusEntry(eMyMoney::Split::State::Reconciled); // createStatusEntry(eMyMoney::Split::State::Frozen); } void createStatusEntry(eMyMoney::Split::State status); bool checkForValidSplit(bool doUserInteraction = true); bool costCenterChanged(int costCenterIndex); bool categoryChanged(const QString& accountId); bool numberChanged(const QString& newNumber); bool amountChanged(CreditDebitHelper* valueHelper); Ui_NewSplitEditor* ui; AccountNamesFilterProxyModel* accountsModel; QSortFilterProxyModel* costCenterModel; SplitModel* splitModel; bool accepted; bool costCenterRequired; bool costCenterOk; bool showValuesInverted; QStandardItemModel statusModel; QString transactionSplitId; MyMoneyAccount counterAccount; MyMoneyAccount category; CreditDebitHelper* amountHelper; }; void NewSplitEditor::Private::createStatusEntry(eMyMoney::Split::State status) { QStandardItem* p = new QStandardItem(KMyMoneyUtils::reconcileStateToString(status, true)); p->setData((int)status); statusModel.appendRow(p); } bool NewSplitEditor::Private::checkForValidSplit(bool doUserInteraction) { QStringList infos; bool rc = true; if(!costCenterChanged(ui->costCenterCombo->currentIndex())) { infos << ui->costCenterCombo->toolTip(); rc = false; } if(doUserInteraction) { /// @todo add dialog here that shows the @a infos } return rc; } bool NewSplitEditor::Private::costCenterChanged(int costCenterIndex) { bool rc = true; WidgetHintFrame::hide(ui->costCenterCombo, i18n("The cost center this transaction should be assigned to.")); if(costCenterIndex != -1) { if(costCenterRequired && ui->costCenterCombo->currentText().isEmpty()) { WidgetHintFrame::show(ui->costCenterCombo, i18n("A cost center assignment is required for a transaction in the selected category.")); rc = false; } } return rc; } bool NewSplitEditor::Private::categoryChanged(const QString& accountId) { bool rc = true; if(!accountId.isEmpty()) { try { QModelIndex index = Models::instance()->accountsModel()->accountById(accountId); category = Models::instance()->accountsModel()->data(index, (int)eAccountsModel::Role::Account).value(); const bool isIncomeExpense = category.isIncomeExpense(); ui->costCenterCombo->setEnabled(isIncomeExpense); ui->costCenterLabel->setEnabled(isIncomeExpense); ui->numberEdit->setDisabled(isIncomeExpense); ui->numberLabel->setDisabled(isIncomeExpense); costCenterRequired = category.isCostCenterRequired(); rc &= costCenterChanged(ui->costCenterCombo->currentIndex()); } catch (MyMoneyException &e) { qDebug() << "Ooops: invalid account id" << accountId << "in" << Q_FUNC_INFO; } } return rc; } bool NewSplitEditor::Private::numberChanged(const QString& newNumber) { bool rc = true; WidgetHintFrame::hide(ui->numberEdit, i18n("The check number used for this transaction.")); if(!newNumber.isEmpty()) { const LedgerModel* model = Models::instance()->ledgerModel(); QModelIndexList list = model->match(model->index(0, 0), (int)eLedgerModel::Role::Number, QVariant(newNumber), -1, // all splits Qt::MatchFlags(Qt::MatchExactly | Qt::MatchCaseSensitive | Qt::MatchRecursive)); foreach(QModelIndex index, list) { if(model->data(index, (int)eLedgerModel::Role::AccountId) == ui->accountCombo->getSelected() && model->data(index, (int)eLedgerModel::Role::TransactionSplitId) != transactionSplitId) { WidgetHintFrame::show(ui->numberEdit, i18n("The check number %1 has already been used in this account.", newNumber)); rc = false; break; } } } return rc; } bool NewSplitEditor::Private::amountChanged(CreditDebitHelper* valueHelper) { Q_UNUSED(valueHelper); bool rc = true; return rc; } NewSplitEditor::NewSplitEditor(QWidget* parent, const QString& counterAccountId) : QFrame(parent, Qt::FramelessWindowHint /* | Qt::X11BypassWindowManagerHint */) , d(new Private(this)) { SplitView* view = qobject_cast(parent->parentWidget()); Q_ASSERT(view != 0); d->splitModel = qobject_cast(view->model()); QModelIndex index = Models::instance()->accountsModel()->accountById(counterAccountId); d->counterAccount = Models::instance()->accountsModel()->data(index, (int)eAccountsModel::Role::Account).value(); d->ui->setupUi(this); d->ui->enterButton->setIcon(QIcon::fromTheme(g_Icons[Icon::DialogOK])); d->ui->cancelButton->setIcon(QIcon::fromTheme(g_Icons[Icon::DialogCancel])); d->accountsModel->addAccountGroup(QVector {eMyMoney::Account::Type::Asset, eMyMoney::Account::Type::Liability, eMyMoney::Account::Type::Income, eMyMoney::Account::Type::Expense, eMyMoney::Account::Type::Equity}); d->accountsModel->setHideEquityAccounts(false); auto const model = Models::instance()->accountsModel(); d->accountsModel->setSourceModel(model); d->accountsModel->setSourceColumns(model->getColumns()); d->accountsModel->sort((int)eAccountsModel::Column::Account); d->ui->accountCombo->setModel(d->accountsModel); d->costCenterModel->setSortRole(Qt::DisplayRole); d->costCenterModel->setSourceModel(Models::instance()->costCenterModel()); d->costCenterModel->sort((int)eAccountsModel::Column::Account); d->ui->costCenterCombo->setEditable(true); d->ui->costCenterCombo->setModel(d->costCenterModel); d->ui->costCenterCombo->setModelColumn(0); d->ui->costCenterCombo->completer()->setFilterMode(Qt::MatchContains); WidgetHintFrameCollection* frameCollection = new WidgetHintFrameCollection(this); frameCollection->addFrame(new WidgetHintFrame(d->ui->costCenterCombo)); frameCollection->addFrame(new WidgetHintFrame(d->ui->numberEdit, WidgetHintFrame::Warning)); frameCollection->addWidget(d->ui->enterButton); d->amountHelper = new CreditDebitHelper(this, d->ui->amountEditCredit, d->ui->amountEditDebit); connect(d->ui->numberEdit, SIGNAL(textChanged(QString)), this, SLOT(numberChanged(QString))); connect(d->ui->costCenterCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(costCenterChanged(int))); connect(d->ui->accountCombo, SIGNAL(accountSelected(QString)), this, SLOT(categoryChanged(QString))); connect(d->amountHelper, SIGNAL(valueChanged()), this, SLOT(amountChanged())); connect(d->ui->cancelButton, SIGNAL(clicked(bool)), this, SLOT(reject())); connect(d->ui->enterButton, SIGNAL(clicked(bool)), this, SLOT(acceptEdit())); } NewSplitEditor::~NewSplitEditor() { } void NewSplitEditor::setShowValuesInverted(bool inverse) { d->showValuesInverted = inverse; } bool NewSplitEditor::showValuesInverted() { return d->showValuesInverted; } bool NewSplitEditor::accepted() const { return d->accepted; } void NewSplitEditor::acceptEdit() { if(d->checkForValidSplit()) { d->accepted = true; emit done(); } } void NewSplitEditor::reject() { emit done(); } void NewSplitEditor::keyPressEvent(QKeyEvent* e) { if (!e->modifiers() || (e->modifiers() & Qt::KeypadModifier && e->key() == Qt::Key_Enter)) { switch (e->key()) { case Qt::Key_Enter: case Qt::Key_Return: { if(focusWidget() == d->ui->cancelButton) { reject(); } else { if(d->ui->enterButton->isEnabled()) { d->ui->enterButton->click(); } return; } } break; case Qt::Key_Escape: reject(); break; default: e->ignore(); return; } } else { e->ignore(); } } QString NewSplitEditor::accountId() const { return d->ui->accountCombo->getSelected(); } void NewSplitEditor::setAccountId(const QString& id) { d->ui->accountCombo->clearEditText(); d->ui->accountCombo->setSelected(id); } QString NewSplitEditor::memo() const { return d->ui->memoEdit->toPlainText(); } void NewSplitEditor::setMemo(const QString& memo) { d->ui->memoEdit->setPlainText(memo); } MyMoneyMoney NewSplitEditor::amount() const { return d->amountHelper->value(); } void NewSplitEditor::setAmount(MyMoneyMoney value) { d->amountHelper->setValue(value); } QString NewSplitEditor::costCenterId() const { const int row = d->ui->costCenterCombo->currentIndex(); QModelIndex index = d->ui->costCenterCombo->model()->index(row, 0); return d->ui->costCenterCombo->model()->data(index, CostCenterModel::CostCenterIdRole).toString(); } void NewSplitEditor::setCostCenterId(const QString& id) { QModelIndex index = Models::indexById(d->costCenterModel, CostCenterModel::CostCenterIdRole, id); if(index.isValid()) { d->ui->costCenterCombo->setCurrentIndex(index.row()); } } QString NewSplitEditor::number() const { return d->ui->numberEdit->text(); } void NewSplitEditor::setNumber(const QString& number) { d->ui->numberEdit->setText(number); } QString NewSplitEditor::splitId() const { return d->transactionSplitId; } void NewSplitEditor::numberChanged(const QString& newNumber) { d->numberChanged(newNumber); } void NewSplitEditor::categoryChanged(const QString& accountId) { d->categoryChanged(accountId); } void NewSplitEditor::costCenterChanged(int costCenterIndex) { d->costCenterChanged(costCenterIndex); } void NewSplitEditor::amountChanged() { d->amountChanged(d->amountHelper); } diff --git a/kmymoney/views/newtransactioneditor.cpp b/kmymoney/views/newtransactioneditor.cpp index 733910a35..afa302416 100644 --- a/kmymoney/views/newtransactioneditor.cpp +++ b/kmymoney/views/newtransactioneditor.cpp @@ -1,721 +1,722 @@ /*************************************************************************** newtransactioneditor.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 "newtransactioneditor.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include // ---------------------------------------------------------------------------- // Project Includes #include "creditdebithelper.h" #include "mymoneyfile.h" #include "mymoneyaccount.h" +#include "mymoneyexception.h" #include "kmymoneyutils.h" #include "kmymoneyaccountcombo.h" #include "models.h" #include "accountsmodel.h" #include "costcentermodel.h" #include "ledgermodel.h" #include "splitmodel.h" #include "payeesmodel.h" #include "mymoneysplit.h" #include "mymoneytransaction.h" #include "ui_newtransactioneditor.h" #include "splitdialog.h" #include "widgethintframe.h" #include "icons/icons.h" #include "modelenums.h" #include "mymoneyenums.h" using namespace Icons; Q_GLOBAL_STATIC(QDate, lastUsedPostDate) class NewTransactionEditor::Private { public: Private(NewTransactionEditor* parent) : ui(new Ui_NewTransactionEditor) , accountsModel(new AccountNamesFilterProxyModel(parent)) , costCenterModel(new QSortFilterProxyModel(parent)) , payeesModel(new QSortFilterProxyModel(parent)) , accepted(false) , costCenterRequired(false) { accountsModel->setObjectName("NewTransactionEditor::accountsModel"); costCenterModel->setObjectName("SortedCostCenterModel"); payeesModel->setObjectName("SortedPayeesModel"); statusModel.setObjectName("StatusModel"); splitModel.setObjectName("SplitModel"); costCenterModel->setSortLocaleAware(true); costCenterModel->setSortCaseSensitivity(Qt::CaseInsensitive); payeesModel->setSortLocaleAware(true); payeesModel->setSortCaseSensitivity(Qt::CaseInsensitive); createStatusEntry(eMyMoney::Split::State::NotReconciled); createStatusEntry(eMyMoney::Split::State::Cleared); createStatusEntry(eMyMoney::Split::State::Reconciled); // createStatusEntry(eMyMoney::Split::State::Frozen); } void createStatusEntry(eMyMoney::Split::State status); void updateWidgetState(); bool checkForValidTransaction(bool doUserInteraction = true); bool isDatePostOpeningDate(const QDate& date, const QString& accountId); bool postdateChanged(const QDate& date); bool costCenterChanged(int costCenterIndex); bool categoryChanged(const QString& accountId); bool numberChanged(const QString& newNumber); bool valueChanged(CreditDebitHelper* valueHelper); Ui_NewTransactionEditor* ui; AccountNamesFilterProxyModel* accountsModel; QSortFilterProxyModel* costCenterModel; QSortFilterProxyModel* payeesModel; bool accepted; bool costCenterRequired; bool costCenterOk; SplitModel splitModel; QStandardItemModel statusModel; QString transactionSplitId; MyMoneyAccount account; MyMoneyTransaction transaction; MyMoneySplit split; CreditDebitHelper* amountHelper; }; void NewTransactionEditor::Private::createStatusEntry(eMyMoney::Split::State status) { QStandardItem* p = new QStandardItem(KMyMoneyUtils::reconcileStateToString(status, true)); p->setData((int)status); statusModel.appendRow(p); } void NewTransactionEditor::Private::updateWidgetState() { // just in case it is disabled we turn it on ui->costCenterCombo->setEnabled(true); // setup the category/account combo box. If we have a split transaction, we disable the // combo box altogether. Changes can only be made via the split dialog editor bool blocked = false; QModelIndex index; // update the category combo box ui->accountCombo->setEnabled(true); switch(splitModel.rowCount()) { case 0: ui->accountCombo->setSelected(QString()); break; case 1: index = splitModel.index(0, 0); ui->accountCombo->setSelected(splitModel.data(index, (int)eLedgerModel::Role::AccountId).toString()); break; default: index = splitModel.index(0, 0); blocked = ui->accountCombo->lineEdit()->blockSignals(true); ui->accountCombo->lineEdit()->setText(i18n("Split transaction")); ui->accountCombo->setDisabled(true); ui->accountCombo->lineEdit()->blockSignals(blocked); ui->costCenterCombo->setDisabled(true); ui->costCenterLabel->setDisabled(true); break; } ui->accountCombo->hidePopup(); // update the costcenter combo box if(ui->costCenterCombo->isEnabled()) { // extract the cost center index = splitModel.index(0, 0); QModelIndexList ccList = costCenterModel->match(costCenterModel->index(0, 0), CostCenterModel::CostCenterIdRole, splitModel.data(index, (int)eLedgerModel::Role::CostCenterId), 1, Qt::MatchFlags(Qt::MatchExactly | Qt::MatchCaseSensitive | Qt::MatchRecursive)); if (ccList.count() > 0) { index = ccList.front(); ui->costCenterCombo->setCurrentIndex(index.row()); } } } bool NewTransactionEditor::Private::checkForValidTransaction(bool doUserInteraction) { QStringList infos; bool rc = true; if(!postdateChanged(ui->dateEdit->date())) { infos << ui->dateEdit->toolTip(); rc = false; } if(!costCenterChanged(ui->costCenterCombo->currentIndex())) { infos << ui->costCenterCombo->toolTip(); rc = false; } if(doUserInteraction) { /// @todo add dialog here that shows the @a infos about the problem } return rc; } bool NewTransactionEditor::Private::isDatePostOpeningDate(const QDate& date, const QString& accountId) { bool rc = true; try { MyMoneyAccount account = MyMoneyFile::instance()->account(accountId); const bool isIncomeExpense = account.isIncomeExpense(); // we don't check for categories if(!isIncomeExpense) { if(date < account.openingDate()) rc = false; } } catch (MyMoneyException &e) { qDebug() << "Ooops: invalid account id" << accountId << "in" << Q_FUNC_INFO; } return rc; } bool NewTransactionEditor::Private::postdateChanged(const QDate& date) { bool rc = true; WidgetHintFrame::hide(ui->dateEdit, i18n("The posting date of the transaction.")); // collect all account ids QStringList accountIds; accountIds << account.id(); for(int row = 0; row < splitModel.rowCount(); ++row) { QModelIndex index = splitModel.index(row, 0); accountIds << splitModel.data(index, (int)eLedgerModel::Role::AccountId).toString();; } Q_FOREACH(QString accountId, accountIds) { if(!isDatePostOpeningDate(date, accountId)) { MyMoneyAccount account = MyMoneyFile::instance()->account(accountId); WidgetHintFrame::show(ui->dateEdit, i18n("The posting date is prior to the opening date of account %1.", account.name())); rc = false; break; } } return rc; } bool NewTransactionEditor::Private::costCenterChanged(int costCenterIndex) { bool rc = true; WidgetHintFrame::hide(ui->costCenterCombo, i18n("The cost center this transaction should be assigned to.")); if(costCenterIndex != -1) { if(costCenterRequired && ui->costCenterCombo->currentText().isEmpty()) { WidgetHintFrame::show(ui->costCenterCombo, i18n("A cost center assignment is required for a transaction in the selected category.")); rc = false; } if(rc == true && splitModel.rowCount() == 1) { QModelIndex index = costCenterModel->index(costCenterIndex, 0); QString costCenterId = costCenterModel->data(index, CostCenterModel::CostCenterIdRole).toString(); index = splitModel.index(0, 0); splitModel.setData(index, costCenterId, (int)eLedgerModel::Role::CostCenterId); } } return rc; } bool NewTransactionEditor::Private::categoryChanged(const QString& accountId) { bool rc = true; if(!accountId.isEmpty() && splitModel.rowCount() <= 1) { try { MyMoneyAccount category = MyMoneyFile::instance()->account(accountId); const bool isIncomeExpense = category.isIncomeExpense(); ui->costCenterCombo->setEnabled(isIncomeExpense); ui->costCenterLabel->setEnabled(isIncomeExpense); costCenterRequired = category.isCostCenterRequired(); rc &= costCenterChanged(ui->costCenterCombo->currentIndex()); rc &= postdateChanged(ui->dateEdit->date()); // make sure we have a split in the model bool newSplit = false; if(splitModel.rowCount() == 0) { splitModel.addEmptySplitEntry(); newSplit = true; } const QModelIndex index = splitModel.index(0, 0); splitModel.setData(index, accountId, (int)eLedgerModel::Role::AccountId); if(newSplit) { costCenterChanged(ui->costCenterCombo->currentIndex()); if(amountHelper->haveValue()) { splitModel.setData(index, QVariant::fromValue(-amountHelper->value()), (int)eLedgerModel::Role::SplitValue); /// @todo make sure to convert initial value to shares according to price information splitModel.setData(index, QVariant::fromValue(-amountHelper->value()), (int)eLedgerModel::Role::SplitShares); } } /// @todo we need to make sure to support multiple currencies here } catch (MyMoneyException &e) { qDebug() << "Ooops: invalid account id" << accountId << "in" << Q_FUNC_INFO; } } return rc; } bool NewTransactionEditor::Private::numberChanged(const QString& newNumber) { bool rc = true; WidgetHintFrame::hide(ui->numberEdit, i18n("The check number used for this transaction.")); if(!newNumber.isEmpty()) { const LedgerModel* model = Models::instance()->ledgerModel(); QModelIndexList list = model->match(model->index(0, 0), (int)eLedgerModel::Role::Number, QVariant(newNumber), -1, // all splits Qt::MatchFlags(Qt::MatchExactly | Qt::MatchCaseSensitive | Qt::MatchRecursive)); foreach(QModelIndex index, list) { if(model->data(index, (int)eLedgerModel::Role::AccountId) == account.id() && model->data(index, (int)eLedgerModel::Role::TransactionSplitId) != transactionSplitId) { WidgetHintFrame::show(ui->numberEdit, i18n("The check number %1 has already been used in this account.", newNumber)); rc = false; break; } } } return rc; } bool NewTransactionEditor::Private::valueChanged(CreditDebitHelper* valueHelper) { bool rc = true; if(valueHelper->haveValue() && splitModel.rowCount() <= 1) { rc = false; try { MyMoneyMoney shares; if(splitModel.rowCount() == 1) { const QModelIndex index = splitModel.index(0, 0); splitModel.setData(index, QVariant::fromValue(-amountHelper->value()), (int)eLedgerModel::Role::SplitValue); /// @todo make sure to support multiple currencies splitModel.setData(index, QVariant::fromValue(-amountHelper->value()), (int)eLedgerModel::Role::SplitShares); } else { /// @todo ask what to do: if the rest of the splits is the same amount we could simply reverse the sign /// of all splits, otherwise we could ask if the user wants to start the split editor or anything else. } rc = true; } catch (MyMoneyException &e) { qDebug() << "Ooops: somwthing went wrong in" << Q_FUNC_INFO; } } return rc; } NewTransactionEditor::NewTransactionEditor(QWidget* parent, const QString& accountId) : QFrame(parent, Qt::FramelessWindowHint /* | Qt::X11BypassWindowManagerHint */) , d(new Private(this)) { auto const model = Models::instance()->accountsModel(); // extract account information from model const auto index = model->accountById(accountId); d->account = model->data(index, (int)eAccountsModel::Role::Account).value(); d->ui->setupUi(this); d->accountsModel->addAccountGroup(QVector {eMyMoney::Account::Type::Asset, eMyMoney::Account::Type::Liability, eMyMoney::Account::Type::Income, eMyMoney::Account::Type::Expense, eMyMoney::Account::Type::Equity}); d->accountsModel->setHideEquityAccounts(false); d->accountsModel->setSourceModel(model); d->accountsModel->setSourceColumns(model->getColumns()); d->accountsModel->sort((int)eAccountsModel::Column::Account); d->ui->accountCombo->setModel(d->accountsModel); d->costCenterModel->setSortRole(Qt::DisplayRole); d->costCenterModel->setSourceModel(Models::instance()->costCenterModel()); d->costCenterModel->sort(0); d->ui->costCenterCombo->setEditable(true); d->ui->costCenterCombo->setModel(d->costCenterModel); d->ui->costCenterCombo->setModelColumn(0); d->ui->costCenterCombo->completer()->setFilterMode(Qt::MatchContains); d->payeesModel->setSortRole(Qt::DisplayRole); d->payeesModel->setSourceModel(Models::instance()->payeesModel()); d->payeesModel->sort(0); d->ui->payeeEdit->setEditable(true); d->ui->payeeEdit->setModel(d->payeesModel); d->ui->payeeEdit->setModelColumn(0); d->ui->payeeEdit->completer()->setFilterMode(Qt::MatchContains); d->ui->enterButton->setIcon(QIcon::fromTheme(g_Icons[Icon::DialogOK])); d->ui->cancelButton->setIcon(QIcon::fromTheme(g_Icons[Icon::DialogCancel])); d->ui->statusCombo->setModel(&d->statusModel); d->ui->dateEdit->setDisplayFormat(QLocale().dateFormat(QLocale::ShortFormat)); d->ui->amountEditCredit->setAllowEmpty(true); d->ui->amountEditDebit->setAllowEmpty(true); d->amountHelper = new CreditDebitHelper(this, d->ui->amountEditCredit, d->ui->amountEditDebit); WidgetHintFrameCollection* frameCollection = new WidgetHintFrameCollection(this); frameCollection->addFrame(new WidgetHintFrame(d->ui->dateEdit)); frameCollection->addFrame(new WidgetHintFrame(d->ui->costCenterCombo)); frameCollection->addFrame(new WidgetHintFrame(d->ui->numberEdit, WidgetHintFrame::Warning)); frameCollection->addWidget(d->ui->enterButton); connect(d->ui->numberEdit, SIGNAL(textChanged(QString)), this, SLOT(numberChanged(QString))); connect(d->ui->costCenterCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(costCenterChanged(int))); connect(d->ui->accountCombo, SIGNAL(accountSelected(QString)), this, SLOT(categoryChanged(QString))); connect(d->ui->dateEdit, SIGNAL(dateChanged(QDate)), this, SLOT(postdateChanged(QDate))); connect(d->amountHelper, SIGNAL(valueChanged()), this, SLOT(valueChanged())); connect(d->ui->cancelButton, SIGNAL(clicked(bool)), this, SLOT(reject())); connect(d->ui->enterButton, SIGNAL(clicked(bool)), this, SLOT(acceptEdit())); connect(d->ui->splitEditorButton, SIGNAL(clicked(bool)), this, SLOT(editSplits())); // handle some events in certain conditions different from default d->ui->payeeEdit->installEventFilter(this); d->ui->costCenterCombo->installEventFilter(this); d->ui->tagComboBox->installEventFilter(this); d->ui->statusCombo->installEventFilter(this); // setup tooltip // setWindowFlags(Qt::FramelessWindowHint | Qt::X11BypassWindowManagerHint); } NewTransactionEditor::~NewTransactionEditor() { } bool NewTransactionEditor::accepted() const { return d->accepted; } void NewTransactionEditor::acceptEdit() { if(d->checkForValidTransaction()) { d->accepted = true; emit done(); } } void NewTransactionEditor::reject() { emit done(); } void NewTransactionEditor::keyPressEvent(QKeyEvent* e) { if (!e->modifiers() || (e->modifiers() & Qt::KeypadModifier && e->key() == Qt::Key_Enter)) { switch (e->key()) { case Qt::Key_Enter: case Qt::Key_Return: { if(focusWidget() == d->ui->cancelButton) { reject(); } else { if(d->ui->enterButton->isEnabled()) { d->ui->enterButton->click(); } return; } } break; case Qt::Key_Escape: reject(); break; default: e->ignore(); return; } } else { e->ignore(); } } void NewTransactionEditor::loadTransaction(const QString& id) { const LedgerModel* model = Models::instance()->ledgerModel(); const QString transactionId = model->transactionIdFromTransactionSplitId(id); if(id.isEmpty()) { d->transactionSplitId.clear(); d->transaction = MyMoneyTransaction(); if(lastUsedPostDate()->isValid()) { d->ui->dateEdit->setDate(*lastUsedPostDate()); } else { d->ui->dateEdit->setDate(QDate::currentDate()); } bool blocked = d->ui->accountCombo->lineEdit()->blockSignals(true); d->ui->accountCombo->lineEdit()->clear(); d->ui->accountCombo->lineEdit()->blockSignals(blocked); } else { // find which item has this id and set is as the current item QModelIndexList list = model->match(model->index(0, 0), (int)eLedgerModel::Role::TransactionId, QVariant(transactionId), -1, // all splits Qt::MatchFlags(Qt::MatchExactly | Qt::MatchCaseSensitive | Qt::MatchRecursive)); Q_FOREACH(QModelIndex index, list) { // the selected split? const QString transactionSplitId = model->data(index, (int)eLedgerModel::Role::TransactionSplitId).toString(); if(transactionSplitId == id) { d->transactionSplitId = id; d->transaction = model->data(index, (int)eLedgerModel::Role::Transaction).value(); d->split = model->data(index, (int)eLedgerModel::Role::Split).value(); d->ui->dateEdit->setDate(model->data(index, (int)eLedgerModel::Role::PostDate).toDate()); d->ui->payeeEdit->lineEdit()->setText(model->data(index, (int)eLedgerModel::Role::PayeeName).toString()); d->ui->memoEdit->clear(); d->ui->memoEdit->insertPlainText(model->data(index, (int)eLedgerModel::Role::Memo).toString()); d->ui->memoEdit->moveCursor(QTextCursor::Start); d->ui->memoEdit->ensureCursorVisible(); // The calculator for the amount field can simply be added as an icon to the line edit widget. // See http://stackoverflow.com/questions/11381865/how-to-make-an-extra-icon-in-qlineedit-like-this howto do it d->ui->amountEditCredit->setText(model->data(model->index(index.row(), (int)eLedgerModel::Column::Payment)).toString()); d->ui->amountEditDebit->setText(model->data(model->index(index.row(), (int)eLedgerModel::Column::Deposit)).toString()); d->ui->numberEdit->setText(model->data(index, (int)eLedgerModel::Role::Number).toString()); d->ui->statusCombo->setCurrentIndex(model->data(index, (int)eLedgerModel::Role::Number).toInt()); QModelIndexList stList = d->statusModel.match(d->statusModel.index(0, 0), Qt::UserRole+1, model->data(index, (int)eLedgerModel::Role::Reconciliation).toInt()); if(stList.count()) { QModelIndex stIndex = stList.front(); d->ui->statusCombo->setCurrentIndex(stIndex.row()); } } else { d->splitModel.addSplit(transactionSplitId); } } d->updateWidgetState(); } // set focus to payee edit once we return to event loop QMetaObject::invokeMethod(d->ui->payeeEdit, "setFocus", Qt::QueuedConnection); } void NewTransactionEditor::numberChanged(const QString& newNumber) { d->numberChanged(newNumber); } void NewTransactionEditor::categoryChanged(const QString& accountId) { d->categoryChanged(accountId); } void NewTransactionEditor::costCenterChanged(int costCenterIndex) { d->costCenterChanged(costCenterIndex); } void NewTransactionEditor::postdateChanged(const QDate& date) { d->postdateChanged(date); } void NewTransactionEditor::valueChanged() { d->valueChanged(d->amountHelper); } void NewTransactionEditor::editSplits() { SplitModel splitModel; splitModel.deepCopy(d->splitModel, true); // create an empty split at the end splitModel.addEmptySplitEntry(); QPointer splitDialog = new SplitDialog(d->account, transactionAmount(), this); splitDialog->setModel(&splitModel); int rc = splitDialog->exec(); if(splitDialog && (rc == QDialog::Accepted)) { // remove that empty split again before we update the splits splitModel.removeEmptySplitEntry(); // copy the splits model contents d->splitModel.deepCopy(splitModel, true); // update the transaction amount d->amountHelper->setValue(splitDialog->transactionAmount()); d->updateWidgetState(); QWidget *next = d->ui->tagComboBox; if(d->ui->costCenterCombo->isEnabled()) { next = d->ui->costCenterCombo; } next->setFocus(); } if(splitDialog) { splitDialog->deleteLater(); } } MyMoneyMoney NewTransactionEditor::transactionAmount() const { return d->amountHelper->value(); } void NewTransactionEditor::saveTransaction() { MyMoneyTransaction t; if(!d->transactionSplitId.isEmpty()) { t = d->transaction; } else { // we keep the date when adding a new transaction // for the next new one *lastUsedPostDate() = d->ui->dateEdit->date(); } QList splits = t.splits(); // first remove the splits that are gone foreach (const auto split, t.splits()) { if(split.id() == d->split.id()) { continue; } int row; for(row = 0; row < d->splitModel.rowCount(); ++row) { QModelIndex index = d->splitModel.index(row, 0); if(d->splitModel.data(index, (int)eLedgerModel::Role::SplitId).toString() == split.id()) { break; } } // if the split is not in the model, we get rid of it if(d->splitModel.rowCount() == row) { t.removeSplit(split); } } MyMoneyFileTransaction ft; try { // new we update the split we are opened for MyMoneySplit sp(d->split); sp.setNumber(d->ui->numberEdit->text()); sp.setMemo(d->ui->memoEdit->toPlainText()); sp.setShares(d->amountHelper->value()); if(t.commodity().isEmpty()) { t.setCommodity(d->account.currencyId()); sp.setValue(d->amountHelper->value()); } else { /// @todo check that the transactions commodity is the same /// as the one of the account this split references. If /// that is not the case, the next statement would create /// a problem sp.setValue(d->amountHelper->value()); } if(sp.reconcileFlag() != eMyMoney::Split::State::Reconciled && !sp.reconcileDate().isValid() && d->ui->statusCombo->currentIndex() == (int)eMyMoney::Split::State::Reconciled) { sp.setReconcileDate(QDate::currentDate()); } sp.setReconcileFlag(static_cast(d->ui->statusCombo->currentIndex())); // sp.setPayeeId(d->ui->payeeEdit->cu) if(sp.id().isEmpty()) { t.addSplit(sp); } else { t.modifySplit(sp); } t.setPostDate(d->ui->dateEdit->date()); // now update and add what we have in the model const SplitModel * model = &d->splitModel; for(int row = 0; row < model->rowCount(); ++row) { QModelIndex index = model->index(row, 0); MyMoneySplit s; const QString splitId = model->data(index, (int)eLedgerModel::Role::SplitId).toString(); if(!SplitModel::isNewSplitId(splitId)) { s = t.splitById(splitId); } s.setNumber(model->data(index, (int)eLedgerModel::Role::Number).toString()); s.setMemo(model->data(index, (int)eLedgerModel::Role::Memo).toString()); s.setAccountId(model->data(index, (int)eLedgerModel::Role::AccountId).toString()); s.setShares(model->data(index, (int)eLedgerModel::Role::SplitShares).value()); s.setValue(model->data(index, (int)eLedgerModel::Role::SplitValue).value()); s.setCostCenterId(model->data(index, (int)eLedgerModel::Role::CostCenterId).toString()); s.setPayeeId(model->data(index, (int)eLedgerModel::Role::PayeeId).toString()); // reconcile flag and date if(s.id().isEmpty()) { t.addSplit(s); } else { t.modifySplit(s); } } if(t.id().isEmpty()) { MyMoneyFile::instance()->addTransaction(t); } else { MyMoneyFile::instance()->modifyTransaction(t); } ft.commit(); } catch (const MyMoneyException &e) { qDebug() << Q_FUNC_INFO << "something went wrong" << e.what(); } } bool NewTransactionEditor::eventFilter(QObject* o, QEvent* e) { auto cb = qobject_cast(o); if (o) { // filter out wheel events for combo boxes if the popup view is not visible if ((e->type() == QEvent::Wheel) && !cb->view()->isVisible()) { return true; } } return QFrame::eventFilter(o, e); } // kate: space-indent on; indent-width 2; remove-trailing-space on; remove-trailing-space-save on; diff --git a/kmymoney/widgets/kmymoneybriefschedule.cpp b/kmymoney/widgets/kmymoneybriefschedule.cpp index cdf6a414d..be8f3003d 100644 --- a/kmymoney/widgets/kmymoneybriefschedule.cpp +++ b/kmymoney/widgets/kmymoneybriefschedule.cpp @@ -1,212 +1,213 @@ /*************************************************************************** kmymoneybriefschedule.cpp - description ------------------- begin : Sun Jul 6 2003 copyright : (C) 2000-2003 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 "kmymoneybriefschedule.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include // ---------------------------------------------------------------------------- // Project Includes #include "ui_kmymoneybriefschedule.h" +#include "mymoneyexception.h" #include "mymoneymoney.h" #include "mymoneyaccount.h" #include "mymoneyschedule.h" #include "mymoneysplit.h" #include "mymoneytransaction.h" #include "kmymoneyutils.h" #include "icons/icons.h" #include "mymoneyenums.h" using namespace Icons; class KMyMoneyBriefSchedulePrivate { Q_DISABLE_COPY(KMyMoneyBriefSchedulePrivate) public: KMyMoneyBriefSchedulePrivate() : ui(new Ui::KMyMoneyBriefSchedule), m_index(0) { } ~KMyMoneyBriefSchedulePrivate() { delete ui; } void loadSchedule() { try { if (m_index < m_scheduleList.count()) { MyMoneySchedule sched = m_scheduleList[m_index]; ui->m_indexLabel->setText(i18n("%1 of %2", m_index + 1, m_scheduleList.count())); ui->m_name->setText(sched.name()); ui->m_type->setText(KMyMoneyUtils::scheduleTypeToString(sched.type())); ui->m_account->setText(sched.account().name()); QString text; MyMoneyMoney amount = sched.transaction().splitByAccount(sched.account().id()).value(); amount = amount.abs(); if (sched.willEnd()) { int transactions = sched.paymentDates(m_date, sched.endDate()).count() - 1; text = i18np("Payment on %2 for %3 with %1 transaction remaining occurring %4.", "Payment on %2 for %3 with %1 transactions remaining occurring %4.", transactions, QLocale().toString(m_date, QLocale::ShortFormat), amount.formatMoney(sched.account().fraction()), i18n(sched.occurrenceToString().toLatin1())); } else { text = i18n("Payment on %1 for %2 occurring %3.", QLocale().toString(m_date, QLocale::ShortFormat), amount.formatMoney(sched.account().fraction()), i18n(sched.occurrenceToString().toLatin1())); } if (m_date < QDate::currentDate()) { if (sched.isOverdue()) { QDate startD = (sched.lastPayment().isValid()) ? sched.lastPayment() : sched.startDate(); if (m_date.isValid()) startD = m_date; int days = startD.daysTo(QDate::currentDate()); int transactions = sched.paymentDates(startD, QDate::currentDate()).count(); text += "
"; text += i18np("%1 day overdue", "%1 days overdue", days); text += QString(" "); text += i18np("(%1 occurrence.)", "(%1 occurrences.)", transactions); text += ""; } } ui->m_details->setText(text); ui->m_prevButton->setEnabled(true); ui->m_nextButton->setEnabled(true); ui->m_skipButton->setEnabled(sched.occurrencePeriod() != eMyMoney::Schedule::Occurrence::Once); if (m_index == 0) ui->m_prevButton->setEnabled(false); if (m_index == (m_scheduleList.count() - 1)) ui->m_nextButton->setEnabled(false); } } catch (const MyMoneyException &) { } } Ui::KMyMoneyBriefSchedule *ui; QList m_scheduleList; int m_index; QDate m_date; }; KMyMoneyBriefSchedule::KMyMoneyBriefSchedule(QWidget *parent) : QWidget(parent), d_ptr(new KMyMoneyBriefSchedulePrivate) { Q_D(KMyMoneyBriefSchedule); d->ui->setupUi(this); d->ui->m_nextButton->setIcon(QIcon::fromTheme(g_Icons[Icon::ArrowRight])); d->ui->m_prevButton->setIcon(QIcon::fromTheme(g_Icons[Icon::ArrowLeft])); d->ui->m_skipButton->setIcon(QIcon::fromTheme(g_Icons[Icon::MediaSeekForward])); d->ui->m_buttonEnter->setIcon(QIcon::fromTheme(g_Icons[Icon::KeyEnter])); connect(d->ui->m_prevButton, &QAbstractButton::clicked, this, &KMyMoneyBriefSchedule::slotPrevClicked); connect(d->ui->m_nextButton, &QAbstractButton::clicked, this, &KMyMoneyBriefSchedule::slotNextClicked); connect(d->ui->m_closeButton, &QAbstractButton::clicked, this, &QWidget::hide); connect(d->ui->m_skipButton, &QAbstractButton::clicked, this, &KMyMoneyBriefSchedule::slotSkipClicked); connect(d->ui->m_buttonEnter, &QAbstractButton::clicked, this, &KMyMoneyBriefSchedule::slotEnterClicked); } KMyMoneyBriefSchedule::~KMyMoneyBriefSchedule() { Q_D(KMyMoneyBriefSchedule); delete d; } void KMyMoneyBriefSchedule::setSchedules(QList list, const QDate& date) { Q_D(KMyMoneyBriefSchedule); d->m_scheduleList = list; d->m_date = date; d->m_index = 0; if (list.count() >= 1) { d->loadSchedule(); } } void KMyMoneyBriefSchedule::slotPrevClicked() { Q_D(KMyMoneyBriefSchedule); if (d->m_index >= 1) { --d->m_index; d->loadSchedule(); } } void KMyMoneyBriefSchedule::slotNextClicked() { Q_D(KMyMoneyBriefSchedule); if (d->m_index < (d->m_scheduleList.count() - 1)) { d->m_index++; d->loadSchedule(); } } void KMyMoneyBriefSchedule::slotEnterClicked() { Q_D(KMyMoneyBriefSchedule); hide(); emit enterClicked(d->m_scheduleList[d->m_index], d->m_date); } void KMyMoneyBriefSchedule::slotSkipClicked() { Q_D(KMyMoneyBriefSchedule); hide(); emit skipClicked(d->m_scheduleList[d->m_index], d->m_date); } diff --git a/kmymoney/widgets/register.cpp b/kmymoney/widgets/register.cpp index 38475963c..6bd21a302 100644 --- a/kmymoney/widgets/register.cpp +++ b/kmymoney/widgets/register.cpp @@ -1,1883 +1,1884 @@ /*************************************************************************** register.cpp - description ------------------- begin : Fri Mar 10 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 "register.h" #include // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include // ---------------------------------------------------------------------------- // Project Includes #include "mymoneysplit.h" #include "mymoneytransaction.h" +#include "mymoneyexception.h" #include "mymoneyaccount.h" #include "stdtransactiondownloaded.h" #include "stdtransactionmatched.h" #include "selectedtransactions.h" #include "scheduledtransaction.h" #include "kmymoneyglobalsettings.h" #include "mymoneymoney.h" #include "mymoneyfile.h" #include "groupmarkers.h" #include "fancydategroupmarkers.h" #include "registeritemdelegate.h" #include "itemptrvector.h" #include "mymoneyenums.h" #include "widgetenums.h" using namespace KMyMoneyRegister; using namespace eWidgets; using namespace eMyMoney; namespace KMyMoneyRegister { class RegisterPrivate { public: RegisterPrivate() : m_selectAnchor(0), m_focusItem(0), m_firstItem(0), m_lastItem(0), m_firstErroneous(0), m_lastErroneous(0), m_rowHeightHint(0), m_ledgerLensForced(false), m_selectionMode(QTableWidget::MultiSelection), m_needResize(true), m_listsDirty(false), m_ignoreNextButtonRelease(false), m_needInitialColumnResize(false), m_usedWithEditor(false), m_mouseButton(Qt::MouseButtons(Qt::NoButton)), m_modifiers(Qt::KeyboardModifiers(Qt::NoModifier)), m_detailsColumnType(eRegister::DetailColumn::PayeeFirst) { } ~RegisterPrivate() { } ItemPtrVector m_items; QVector m_itemIndex; RegisterItem* m_selectAnchor; RegisterItem* m_focusItem; RegisterItem* m_ensureVisibleItem; RegisterItem* m_firstItem; RegisterItem* m_lastItem; RegisterItem* m_firstErroneous; RegisterItem* m_lastErroneous; int m_markErroneousTransactions; int m_rowHeightHint; MyMoneyAccount m_account; bool m_ledgerLensForced; QAbstractItemView::SelectionMode m_selectionMode; bool m_needResize; bool m_listsDirty; bool m_ignoreNextButtonRelease; bool m_needInitialColumnResize; bool m_usedWithEditor; Qt::MouseButtons m_mouseButton; Qt::KeyboardModifiers m_modifiers; eTransaction::Column m_lastCol; QList m_sortOrder; QRect m_lastRepaintRect; eRegister::DetailColumn m_detailsColumnType; }; Register::Register(QWidget *parent) : TransactionEditorContainer(parent), d_ptr(new RegisterPrivate) { // used for custom coloring with the help of the application's stylesheet setObjectName(QLatin1String("register")); setItemDelegate(new RegisterItemDelegate(this)); setEditTriggers(QAbstractItemView::NoEditTriggers); setColumnCount((int)eTransaction::Column::LastColumn); setSelectionBehavior(QAbstractItemView::SelectRows); setAcceptDrops(true); setShowGrid(false); setContextMenuPolicy(Qt::DefaultContextMenu); setHorizontalHeaderItem((int)eTransaction::Column::Number, new QTableWidgetItem()); setHorizontalHeaderItem((int)eTransaction::Column::Date, new QTableWidgetItem()); setHorizontalHeaderItem((int)eTransaction::Column::Account, new QTableWidgetItem()); setHorizontalHeaderItem((int)eTransaction::Column::Security, new QTableWidgetItem()); setHorizontalHeaderItem((int)eTransaction::Column::Detail, new QTableWidgetItem()); setHorizontalHeaderItem((int)eTransaction::Column::ReconcileFlag, new QTableWidgetItem()); setHorizontalHeaderItem((int)eTransaction::Column::Payment, new QTableWidgetItem()); setHorizontalHeaderItem((int)eTransaction::Column::Deposit, new QTableWidgetItem()); setHorizontalHeaderItem((int)eTransaction::Column::Quantity, new QTableWidgetItem()); setHorizontalHeaderItem((int)eTransaction::Column::Price, new QTableWidgetItem()); setHorizontalHeaderItem((int)eTransaction::Column::Value, new QTableWidgetItem()); setHorizontalHeaderItem((int)eTransaction::Column::Balance, new QTableWidgetItem()); // keep the following list in sync with KMyMoneyRegister::Column in transaction.h horizontalHeaderItem((int)eTransaction::Column::Number)->setText(i18nc("Cheque Number", "No.")); horizontalHeaderItem((int)eTransaction::Column::Date)->setText(i18n("Date")); horizontalHeaderItem((int)eTransaction::Column::Account)->setText(i18n("Account")); horizontalHeaderItem((int)eTransaction::Column::Security)->setText(i18n("Security")); horizontalHeaderItem((int)eTransaction::Column::Detail)->setText(i18n("Details")); horizontalHeaderItem((int)eTransaction::Column::ReconcileFlag)->setText(i18n("C")); horizontalHeaderItem((int)eTransaction::Column::Payment)->setText(i18n("Payment")); horizontalHeaderItem((int)eTransaction::Column::Deposit)->setText(i18n("Deposit")); horizontalHeaderItem((int)eTransaction::Column::Quantity)->setText(i18n("Quantity")); horizontalHeaderItem((int)eTransaction::Column::Price)->setText(i18n("Price")); horizontalHeaderItem((int)eTransaction::Column::Value)->setText(i18n("Value")); horizontalHeaderItem((int)eTransaction::Column::Balance)->setText(i18n("Balance")); verticalHeader()->hide(); horizontalHeader()->setSectionResizeMode(QHeaderView::Fixed); horizontalHeader()->setSortIndicatorShown(false); horizontalHeader()->setSectionsMovable(false); horizontalHeader()->setSectionsClickable(false); horizontalHeader()->setContextMenuPolicy(Qt::CustomContextMenu); connect(this, &QTableWidget::cellClicked, this, static_cast(&Register::selectItem)); connect(this, &QTableWidget::cellDoubleClicked, this, &Register::slotDoubleClicked); } Register::~Register() { Q_D(Register); clear(); delete d; } bool Register::eventFilter(QObject* o, QEvent* e) { if (o == this && e->type() == QEvent::KeyPress) { QKeyEvent* ke = dynamic_cast(e); if (ke->key() == Qt::Key_Menu) { emit openContextMenu(); return true; } } return QTableWidget::eventFilter(o, e); } void Register::setupRegister(const MyMoneyAccount& account, const QList& cols) { Q_D(Register); d->m_account = account; setUpdatesEnabled(false); for (auto i = 0; i < (int)eTransaction::Column::LastColumn; ++i) hideColumn(i); d->m_needInitialColumnResize = true; d->m_lastCol = static_cast(0); QList::const_iterator it_c; for (it_c = cols.begin(); it_c != cols.end(); ++it_c) { if ((*it_c) > eTransaction::Column::LastColumn) continue; showColumn((int)*it_c); if (*it_c > d->m_lastCol) d->m_lastCol = *it_c; } setUpdatesEnabled(true); } void Register::setupRegister(const MyMoneyAccount& account, bool showAccountColumn) { Q_D(Register); d->m_account = account; setUpdatesEnabled(false); for (auto i = 0; i < (int)eTransaction::Column::LastColumn; ++i) hideColumn(i); horizontalHeaderItem((int)eTransaction::Column::Payment)->setText(i18nc("Payment made from account", "Payment")); horizontalHeaderItem((int)eTransaction::Column::Deposit)->setText(i18nc("Deposit into account", "Deposit")); if (account.id().isEmpty()) { setUpdatesEnabled(true); return; } d->m_needInitialColumnResize = true; // turn on standard columns showColumn((int)eTransaction::Column::Date); showColumn((int)eTransaction::Column::Detail); showColumn((int)eTransaction::Column::ReconcileFlag); // balance switch (account.accountType()) { case Account::Type::Stock: break; default: showColumn((int)eTransaction::Column::Balance); break; } // Number column switch (account.accountType()) { case Account::Type::Savings: case Account::Type::Cash: case Account::Type::Loan: case Account::Type::AssetLoan: case Account::Type::Asset: case Account::Type::Liability: case Account::Type::Equity: if (KMyMoneyGlobalSettings::alwaysShowNrField()) showColumn((int)eTransaction::Column::Number); break; case Account::Type::Checkings: case Account::Type::CreditCard: showColumn((int)eTransaction::Column::Number); break; default: hideColumn((int)eTransaction::Column::Number); break; } switch (account.accountType()) { case Account::Type::Income: case Account::Type::Expense: showAccountColumn = true; break; default: break; } if (showAccountColumn) showColumn((int)eTransaction::Column::Account); // Security, activity, payment, deposit, amount, price and value column switch (account.accountType()) { default: showColumn((int)eTransaction::Column::Payment); showColumn((int)eTransaction::Column::Deposit); break; case Account::Type::Investment: showColumn((int)eTransaction::Column::Security); showColumn((int)eTransaction::Column::Quantity); showColumn((int)eTransaction::Column::Price); showColumn((int)eTransaction::Column::Value); break; } // headings switch (account.accountType()) { case Account::Type::CreditCard: horizontalHeaderItem((int)eTransaction::Column::Payment)->setText(i18nc("Payment made with credit card", "Charge")); horizontalHeaderItem((int)eTransaction::Column::Deposit)->setText(i18nc("Payment towards credit card", "Payment")); break; case Account::Type::Asset: case Account::Type::AssetLoan: horizontalHeaderItem((int)eTransaction::Column::Payment)->setText(i18nc("Decrease of asset/liability value", "Decrease")); horizontalHeaderItem((int)eTransaction::Column::Deposit)->setText(i18nc("Increase of asset/liability value", "Increase")); break; case Account::Type::Liability: case Account::Type::Loan: horizontalHeaderItem((int)eTransaction::Column::Payment)->setText(i18nc("Increase of asset/liability value", "Increase")); horizontalHeaderItem((int)eTransaction::Column::Deposit)->setText(i18nc("Decrease of asset/liability value", "Decrease")); break; case Account::Type::Income: case Account::Type::Expense: horizontalHeaderItem((int)eTransaction::Column::Payment)->setText(i18n("Income")); horizontalHeaderItem((int)eTransaction::Column::Deposit)->setText(i18n("Expense")); break; default: break; } d->m_lastCol = eTransaction::Column::Balance; setUpdatesEnabled(true); } bool Register::focusNextPrevChild(bool next) { return QFrame::focusNextPrevChild(next); } void Register::setSortOrder(const QString& order) { Q_D(Register); const QStringList orderList = order.split(',', QString::SkipEmptyParts); QStringList::const_iterator it; d->m_sortOrder.clear(); for (it = orderList.constBegin(); it != orderList.constEnd(); ++it) { d->m_sortOrder << static_cast((*it).toInt()); } } const QList& Register::sortOrder() const { Q_D(const Register); return d->m_sortOrder; } void Register::sortItems() { Q_D(Register); if (d->m_items.count() == 0) return; // sort the array of pointers to the transactions d->m_items.sort(); // update the next/prev item chains RegisterItem* prev = 0; RegisterItem* item; d->m_firstItem = d->m_lastItem = 0; for (QVector::size_type i = 0; i < d->m_items.size(); ++i) { item = d->m_items[i]; if (!item) continue; if (!d->m_firstItem) d->m_firstItem = item; d->m_lastItem = item; if (prev) prev->setNextItem(item); item->setPrevItem(prev); item->setNextItem(0); prev = item; } // update the balance visibility settings item = d->m_lastItem; bool showBalance = true; while (item) { Transaction* t = dynamic_cast(item); if (t) { t->setShowBalance(showBalance); if (!t->isVisible()) { showBalance = false; } } item = item->prevItem(); } // force update of the item index (row to item array) d->m_listsDirty = true; } eTransaction::Column Register::lastCol() const { Q_D(const Register); return d->m_lastCol; } SortField Register::primarySortKey() const { Q_D(const Register); if (!d->m_sortOrder.isEmpty()) return static_cast(d->m_sortOrder.first()); return SortField::Unknown; } void Register::clear() { Q_D(Register); d->m_firstErroneous = d->m_lastErroneous = 0; d->m_ensureVisibleItem = 0; d->m_items.clear(); RegisterItem* p; while ((p = firstItem()) != 0) { delete p; } d->m_firstItem = d->m_lastItem = 0; d->m_listsDirty = true; d->m_selectAnchor = 0; d->m_focusItem = 0; #ifndef KMM_DESIGNER // recalculate row height hint QFontMetrics fm(KMyMoneyGlobalSettings::listCellFont()); d->m_rowHeightHint = fm.lineSpacing() + 6; #endif d->m_needInitialColumnResize = true; d->m_needResize = true; updateRegister(true); } void Register::insertItemAfter(RegisterItem*p, RegisterItem* prev) { Q_D(Register); RegisterItem* next = 0; if (!prev) prev = lastItem(); if (prev) { next = prev->nextItem(); prev->setNextItem(p); } if (next) next->setPrevItem(p); p->setPrevItem(prev); p->setNextItem(next); if (!d->m_firstItem) d->m_firstItem = p; if (!d->m_lastItem) d->m_lastItem = p; if (prev == d->m_lastItem) d->m_lastItem = p; d->m_listsDirty = true; d->m_needResize = true; } void Register::addItem(RegisterItem* p) { Q_D(Register); RegisterItem* q = lastItem(); if (q) q->setNextItem(p); p->setPrevItem(q); p->setNextItem(0); d->m_items.append(p); if (!d->m_firstItem) d->m_firstItem = p; d->m_lastItem = p; d->m_listsDirty = true; d->m_needResize = true; } void Register::removeItem(RegisterItem* p) { Q_D(Register); // remove item from list if (p->prevItem()) p->prevItem()->setNextItem(p->nextItem()); if (p->nextItem()) p->nextItem()->setPrevItem(p->prevItem()); // update first and last pointer if required if (p == d->m_firstItem) d->m_firstItem = p->nextItem(); if (p == d->m_lastItem) d->m_lastItem = p->prevItem(); // make sure we don't do it twice p->setNextItem(0); p->setPrevItem(0); // remove it from the m_items array int i = d->m_items.indexOf(p); if (-1 != i) { d->m_items[i] = 0; } d->m_listsDirty = true; d->m_needResize = true; } RegisterItem* Register::firstItem() const { Q_D(const Register); return d->m_firstItem; } RegisterItem* Register::nextItem(RegisterItem* item) const { return item->nextItem(); } RegisterItem* Register::lastItem() const { Q_D(const Register); return d->m_lastItem; } void Register::setupItemIndex(int rowCount) { Q_D(Register); // setup index array d->m_itemIndex.clear(); d->m_itemIndex.reserve(rowCount); // fill index array rowCount = 0; RegisterItem* prev = 0; d->m_firstItem = d->m_lastItem = 0; for (QVector::size_type i = 0; i < d->m_items.size(); ++i) { RegisterItem* item = d->m_items[i]; if (!item) continue; if (!d->m_firstItem) d->m_firstItem = item; d->m_lastItem = item; if (prev) prev->setNextItem(item); item->setPrevItem(prev); item->setNextItem(0); prev = item; for (int j = item->numRowsRegister(); j; --j) { d->m_itemIndex.push_back(item); } } } void Register::updateAlternate() const { Q_D(const Register); bool alternate = false; for (QVector::size_type i = 0; i < d->m_items.size(); ++i) { RegisterItem* item = d->m_items[i]; if (!item) continue; if (item->isVisible()) { item->setAlternate(alternate); alternate ^= true; } } } void Register::suppressAdjacentMarkers() { bool lastWasGroupMarker = false; KMyMoneyRegister::RegisterItem* p = lastItem(); KMyMoneyRegister::Transaction* t = dynamic_cast(p); if (t && t->transaction().id().isEmpty()) { lastWasGroupMarker = true; p = p->prevItem(); } while (p) { KMyMoneyRegister::GroupMarker* m = dynamic_cast(p); if (m) { // make adjacent group marker invisible except those that show statement information if (lastWasGroupMarker && (dynamic_cast(m) == 0)) { m->setVisible(false); } lastWasGroupMarker = true; } else if (p->isVisible()) lastWasGroupMarker = false; p = p->prevItem(); } } void Register::updateRegister(bool forceUpdateRowHeight) { Q_D(Register); if (d->m_listsDirty || forceUpdateRowHeight) { // don't get in here recursively d->m_listsDirty = false; int rowCount = 0; // determine the number of rows we need to display all items // while going through the list, check for erroneous transactions for (QVector::size_type i = 0; i < d->m_items.size(); ++i) { RegisterItem* item = d->m_items[i]; if (!item) continue; item->setStartRow(rowCount); item->setNeedResize(); rowCount += item->numRowsRegister(); if (item->isErroneous()) { if (!d->m_firstErroneous) d->m_firstErroneous = item; d->m_lastErroneous = item; } } updateAlternate(); // create item index setupItemIndex(rowCount); bool needUpdateHeaders = (QTableWidget::rowCount() != rowCount) | forceUpdateRowHeight; // setup QTable. Make sure to suppress screen updates for now setRowCount(rowCount); // if we need to update the headers, we do it now for all rows // again we make sure to suppress screen updates if (needUpdateHeaders) { for (auto i = 0; i < rowCount; ++i) { RegisterItem* item = itemAtRow(i); if (item->isVisible()) { showRow(i); } else { hideRow(i); } verticalHeader()->resizeSection(i, item->rowHeightHint()); } verticalHeader()->setUpdatesEnabled(true); } // force resizeing of the columns if necessary if (d->m_needInitialColumnResize) { QTimer::singleShot(0, this, SLOT(resize())); d->m_needInitialColumnResize = false; } else { update(); // if the number of rows changed, we might need to resize the register // to make sure we reflect the current visibility of the scrollbars. if (needUpdateHeaders) QTimer::singleShot(0, this, SLOT(resize())); } } } int Register::rowHeightHint() const { Q_D(const Register); if (!d->m_rowHeightHint) { qDebug("Register::rowHeightHint(): m_rowHeightHint is zero!!"); } return d->m_rowHeightHint; } void Register::focusInEvent(QFocusEvent* ev) { Q_D(const Register); QTableWidget::focusInEvent(ev); if (d->m_focusItem) { d->m_focusItem->setFocus(true, false); } } bool Register::event(QEvent* event) { if (event->type() == QEvent::ToolTip) { QHelpEvent *helpEvent = static_cast(event); // get the row, if it's the header, then we're done // otherwise, adjust the row to be 0 based. int row = rowAt(helpEvent->y()); if (!row) return true; --row; int col = columnAt(helpEvent->x()); RegisterItem* item = itemAtRow(row); if (!item) return true; row = row - item->startRow(); QString msg; QRect rect; if (!item->maybeTip(helpEvent->pos(), row, col, rect, msg)) return true; if (!msg.isEmpty()) { QToolTip::showText(helpEvent->globalPos(), msg); } else { QToolTip::hideText(); event->ignore(); } return true; } return TransactionEditorContainer::event(event); } void Register::focusOutEvent(QFocusEvent* ev) { Q_D(Register); if (d->m_focusItem) { d->m_focusItem->setFocus(false, false); } QTableWidget::focusOutEvent(ev); } void Register::resizeEvent(QResizeEvent* ev) { TransactionEditorContainer::resizeEvent(ev); resize((int)eTransaction::Column::Detail, true); } void Register::resize() { resize((int)eTransaction::Column::Detail); } void Register::resize(int col, bool force) { Q_D(Register); if (!d->m_needResize && !force) return; d->m_needResize = false; // resize the register int w = viewport()->width(); // TODO I was playing a bit with manual ledger resizing but could not get // a good solution. I just leave the code around, so that maybe others // pick it up again. So far, it's not clear to me where to store the // size of the sections: // // a) with the account (as it is done now) // b) with the application for the specific account type // c) ???? // // Ideas are welcome (ipwizard: 2007-07-19) // Note: currently there's no way to switch back to automatic // column sizing once the manual sizing option has been saved #if 0 if (m_account.value("kmm-ledger-column-width").isEmpty()) { #endif // check which space we need if (columnWidth((int)eTransaction::Column::Number)) adjustColumn((int)eTransaction::Column::Number); if (columnWidth((int)eTransaction::Column::Account)) adjustColumn((int)eTransaction::Column::Account); if (columnWidth((int)eTransaction::Column::Payment)) adjustColumn((int)eTransaction::Column::Payment); if (columnWidth((int)eTransaction::Column::Deposit)) adjustColumn((int)eTransaction::Column::Deposit); if (columnWidth((int)eTransaction::Column::Quantity)) adjustColumn((int)eTransaction::Column::Quantity); if (columnWidth((int)eTransaction::Column::Balance)) adjustColumn((int)eTransaction::Column::Balance); if (columnWidth((int)eTransaction::Column::Price)) adjustColumn((int)eTransaction::Column::Price); if (columnWidth((int)eTransaction::Column::Value)) adjustColumn((int)eTransaction::Column::Value); // make amount columns all the same size // only extend the entry columns to make sure they fit // the widget int dwidth = 0; int ewidth = 0; if (ewidth < columnWidth((int)eTransaction::Column::Payment)) ewidth = columnWidth((int)eTransaction::Column::Payment); if (ewidth < columnWidth((int)eTransaction::Column::Deposit)) ewidth = columnWidth((int)eTransaction::Column::Deposit); if (ewidth < columnWidth((int)eTransaction::Column::Quantity)) ewidth = columnWidth((int)eTransaction::Column::Quantity); if (dwidth < columnWidth((int)eTransaction::Column::Balance)) dwidth = columnWidth((int)eTransaction::Column::Balance); if (ewidth < columnWidth((int)eTransaction::Column::Price)) ewidth = columnWidth((int)eTransaction::Column::Price); if (dwidth < columnWidth((int)eTransaction::Column::Value)) dwidth = columnWidth((int)eTransaction::Column::Value); int swidth = columnWidth((int)eTransaction::Column::Security); if (swidth > 0) { adjustColumn((int)eTransaction::Column::Security); swidth = columnWidth((int)eTransaction::Column::Security); } adjustColumn((int)eTransaction::Column::Date); #ifndef KMM_DESIGNER // Resize the date and money fields to either // a) the size required by the input widget if no transaction form is shown and the register is used with an editor // b) the adjusted value for the input widget if the transaction form is visible or an editor is not used if (d->m_usedWithEditor && !KMyMoneyGlobalSettings::transactionForm()) { QPushButton *pushButton = new QPushButton; const int pushButtonSpacing = pushButton->sizeHint().width() + 5; setColumnWidth((int)eTransaction::Column::Date, columnWidth((int)eTransaction::Column::Date) + pushButtonSpacing + 4/* space for the spinbox arrows */); ewidth += pushButtonSpacing; if (swidth > 0) { // extend the security width to make space for the selector arrow swidth = columnWidth((int)eTransaction::Column::Security) + 40; } delete pushButton; } #endif if (columnWidth((int)eTransaction::Column::Payment)) setColumnWidth((int)eTransaction::Column::Payment, ewidth); if (columnWidth((int)eTransaction::Column::Deposit)) setColumnWidth((int)eTransaction::Column::Deposit, ewidth); if (columnWidth((int)eTransaction::Column::Quantity)) setColumnWidth((int)eTransaction::Column::Quantity, ewidth); if (columnWidth((int)eTransaction::Column::Balance)) setColumnWidth((int)eTransaction::Column::Balance, dwidth); if (columnWidth((int)eTransaction::Column::Price)) setColumnWidth((int)eTransaction::Column::Price, ewidth); if (columnWidth((int)eTransaction::Column::Value)) setColumnWidth((int)eTransaction::Column::Value, dwidth); if (columnWidth((int)eTransaction::Column::ReconcileFlag)) setColumnWidth((int)eTransaction::Column::ReconcileFlag, 20); if (swidth > 0) setColumnWidth((int)eTransaction::Column::Security, swidth); #if 0 // see comment above } else { QStringList colSizes = QStringList::split(",", m_account.value("kmm-ledger-column-width"), true); for (int i; i < colSizes.count(); ++i) { int colWidth = colSizes[i].toInt(); if (colWidth == 0) continue; setColumnWidth(i, w * colWidth / 100); } } #endif for (auto i = 0; i < columnCount(); ++i) { if (i == col) continue; w -= columnWidth(i); } setColumnWidth(col, w); } void Register::forceUpdateLists() { Q_D(Register); d->m_listsDirty = true; } int Register::minimumColumnWidth(int col) { Q_D(Register); QHeaderView *topHeader = horizontalHeader(); int w = topHeader->fontMetrics().width(horizontalHeaderItem(col) ? horizontalHeaderItem(col)->text() : QString()) + 10; w = qMax(w, 20); #ifdef KMM_DESIGNER return w; #else int maxWidth = 0; int minWidth = 0; QFontMetrics cellFontMetrics(KMyMoneyGlobalSettings::listCellFont()); switch (col) { case (int)eTransaction::Column::Date: minWidth = cellFontMetrics.width(QLocale().toString(QDate(6999, 12, 29), QLocale::ShortFormat) + " "); break; default: break; } // scan through the transactions for (auto i = 0; i < d->m_items.size(); ++i) { RegisterItem* const item = d->m_items[i]; if (!item) continue; Transaction* t = dynamic_cast(item); if (t) { int nw = 0; try { nw = t->registerColWidth(col, cellFontMetrics); } catch (const MyMoneyException &) { // This should only be reached if the data in the file disappeared // from under us, such as when the account was deleted from a // different view, then this view is restored. In this case, new // data is about to be loaded into the view anyway, so just remove // the item from the register and swallow the exception. //qDebug("%s", qPrintable(e.what())); removeItem(t); } w = qMax(w, nw); if (maxWidth) { if (w > maxWidth) { w = maxWidth; break; } } if (w < minWidth) { w = minWidth; break; } } } return w; #endif } void Register::adjustColumn(int col) { setColumnWidth(col, minimumColumnWidth(col)); } void Register::clearSelection() { unselectItems(); TransactionEditorContainer::clearSelection(); } void Register::doSelectItems(int from, int to, bool selected) { Q_D(Register); int start, end; // make sure start is smaller than end if (from <= to) { start = from; end = to; } else { start = to; end = from; } // make sure we stay in bounds if (start < 0) start = 0; if ((end <= -1) || (end > (d->m_items.size() - 1))) end = d->m_items.size() - 1; RegisterItem* firstItem; RegisterItem* lastItem; firstItem = lastItem = 0; for (int i = start; i <= end; ++i) { RegisterItem* const item = d->m_items[i]; if (item) { if (selected != item->isSelected()) { if (!firstItem) firstItem = item; item->setSelected(selected); lastItem = item; } } } } RegisterItem* Register::itemAtRow(int row) const { Q_D(const Register); if (row >= 0 && row < d->m_itemIndex.size()) { return d->m_itemIndex[row]; } return 0; } int Register::rowToIndex(int row) const { Q_D(const Register); for (auto i = 0; i < d->m_items.size(); ++i) { RegisterItem* const item = d->m_items[i]; if (!item) continue; if (row >= item->startRow() && row < (item->startRow() + item->numRowsRegister())) return i; } return -1; } void Register::selectedTransactions(SelectedTransactions& list) const { Q_D(const Register); if (d->m_focusItem && d->m_focusItem->isSelected() && d->m_focusItem->isVisible()) { Transaction* t = dynamic_cast(d->m_focusItem); if (t) { QString id; if (t->isScheduled()) id = t->transaction().id(); SelectedTransaction s(t->transaction(), t->split(), id); list << s; } } for (auto i = 0; i < d->m_items.size(); ++i) { RegisterItem* const item = d->m_items[i]; // make sure, we don't include the focus item twice if (item == d->m_focusItem) continue; if (item && item->isSelected() && item->isVisible()) { Transaction* t = dynamic_cast(item); if (t) { QString id; if (t->isScheduled()) id = t->transaction().id(); SelectedTransaction s(t->transaction(), t->split(), id); list << s; } } } } QList Register::selectedItems() const { Q_D(const Register); QList list; RegisterItem* item = d->m_firstItem; while (item) { if (item && item->isSelected() && item->isVisible()) { list << item; } item = item->nextItem(); } return list; } int Register::selectedItemsCount() const { Q_D(const Register); auto cnt = 0; RegisterItem* item = d->m_firstItem; while (item) { if (item->isSelected() && item->isVisible()) ++cnt; item = item->nextItem(); } return cnt; } void Register::mouseReleaseEvent(QMouseEvent *e) { Q_D(Register); if (e->button() == Qt::RightButton) { // see the comment in Register::contextMenuEvent // on Linux we never get here but on Windows this // event is fired before the contextMenuEvent which // causes the loss of the multiple selection; to avoid // this just ignore the event and act like on Linux return; } if (d->m_ignoreNextButtonRelease) { d->m_ignoreNextButtonRelease = false; return; } d->m_mouseButton = e->button(); d->m_modifiers = QApplication::keyboardModifiers(); QTableWidget::mouseReleaseEvent(e); } void Register::contextMenuEvent(QContextMenuEvent *e) { Q_D(Register); if (e->reason() == QContextMenuEvent::Mouse) { // since mouse release event is not called, we need // to reset the mouse button and the modifiers here d->m_mouseButton = Qt::NoButton; d->m_modifiers = Qt::NoModifier; // if a selected item is clicked don't change the selection RegisterItem* item = itemAtRow(rowAt(e->y())); if (item && !item->isSelected()) selectItem(rowAt(e->y()), columnAt(e->x())); } openContextMenu(); } void Register::unselectItems(int from, int to) { doSelectItems(from, to, false); } void Register::selectItems(int from, int to) { doSelectItems(from, to, true); } void Register::selectItem(int row, int col) { Q_D(Register); if (row >= 0 && row < d->m_itemIndex.size()) { RegisterItem* item = d->m_itemIndex[row]; // don't support selecting when the item has an editor // or the item itself is not selectable if (item->hasEditorOpen() || !item->isSelectable()) { d->m_mouseButton = Qt::NoButton; return; } QString id = item->id(); selectItem(item); // selectItem() might have changed the pointers, so we // need to reconstruct it here item = itemById(id); Transaction* t = dynamic_cast(item); if (t) { if (!id.isEmpty()) { if (t && col == (int)eTransaction::Column::ReconcileFlag && selectedItemsCount() == 1 && !t->isScheduled()) emit reconcileStateColumnClicked(t); } else { emit emptyItemSelected(); } } } } void Register::setAnchorItem(RegisterItem* anchorItem) { Q_D(Register); d->m_selectAnchor = anchorItem; } bool Register::setFocusItem(RegisterItem* focusItem) { Q_D(Register); if (focusItem && focusItem->canHaveFocus()) { if (d->m_focusItem) { d->m_focusItem->setFocus(false); } Transaction* item = dynamic_cast(focusItem); if (d->m_focusItem != focusItem && item) { emit focusChanged(item); } d->m_focusItem = focusItem; d->m_focusItem->setFocus(true); if (d->m_listsDirty) updateRegister(KMyMoneyGlobalSettings::ledgerLens() | !KMyMoneyGlobalSettings::transactionForm()); ensureItemVisible(d->m_focusItem); return true; } else return false; } bool Register::setFocusToTop() { Q_D(Register); RegisterItem* rgItem = d->m_firstItem; while (rgItem) { if (setFocusItem(rgItem)) return true; rgItem = rgItem->nextItem(); } return false; } void Register::selectItem(RegisterItem* item, bool dontChangeSelections) { Q_D(Register); if (!item) return; Qt::MouseButtons buttonState = d->m_mouseButton; Qt::KeyboardModifiers modifiers = d->m_modifiers; d->m_mouseButton = Qt::NoButton; d->m_modifiers = Qt::NoModifier; if (d->m_selectionMode == NoSelection) return; if (item->isSelectable()) { QString id = item->id(); QList itemList = selectedItems(); bool okToSelect = true; auto cnt = itemList.count(); const bool scheduledTransactionSelected = (cnt > 0 && itemList.front() && (typeid(*(itemList.front())) == typeid(StdTransactionScheduled))); if (buttonState & Qt::LeftButton) { if (!(modifiers & (Qt::ShiftModifier | Qt::ControlModifier)) || (d->m_selectAnchor == 0)) { if ((cnt != 1) || ((cnt == 1) && !item->isSelected())) { emit aboutToSelectItem(item, okToSelect); if (okToSelect) { // pointer 'item' might have changed. reconstruct it. item = itemById(id); unselectItems(); item->setSelected(true); setFocusItem(item); } } if (okToSelect) d->m_selectAnchor = item; } if (d->m_selectionMode == MultiSelection) { switch (modifiers & (Qt::ShiftModifier | Qt::ControlModifier)) { case Qt::ControlModifier: if (scheduledTransactionSelected || typeid(*item) == typeid(StdTransactionScheduled)) okToSelect = false; // toggle selection state of current item emit aboutToSelectItem(item, okToSelect); if (okToSelect) { // pointer 'item' might have changed. reconstruct it. item = itemById(id); item->setSelected(!item->isSelected()); setFocusItem(item); } break; case Qt::ShiftModifier: if (scheduledTransactionSelected || typeid(*item) == typeid(StdTransactionScheduled)) okToSelect = false; emit aboutToSelectItem(item, okToSelect); if (okToSelect) { // pointer 'item' might have changed. reconstruct it. item = itemById(id); unselectItems(); selectItems(rowToIndex(d->m_selectAnchor->startRow()), rowToIndex(item->startRow())); setFocusItem(item); } break; } } } else { // we get here when called by application logic emit aboutToSelectItem(item, okToSelect); if (okToSelect) { // pointer 'item' might have changed. reconstruct it. item = itemById(id); if (!dontChangeSelections) unselectItems(); item->setSelected(true); setFocusItem(item); d->m_selectAnchor = item; } } if (okToSelect) { SelectedTransactions list(this); emit transactionsSelected(list); } } } void Register::ensureItemVisible(RegisterItem* item) { Q_D(Register); if (!item) return; d->m_ensureVisibleItem = item; QTimer::singleShot(0, this, SLOT(slotEnsureItemVisible())); } void Register::slotDoubleClicked(int row, int) { Q_D(Register); if (row >= 0 && row < d->m_itemIndex.size()) { RegisterItem* p = d->m_itemIndex[row]; if (p->isSelectable()) { d->m_ignoreNextButtonRelease = true; // double click to start editing only works if the focus // item is among the selected ones if (!focusItem()) { setFocusItem(p); if (d->m_selectionMode != NoSelection) p->setSelected(true); } if (d->m_focusItem->isSelected()) { // don't emit the signal right away but wait until // we come back to the Qt main loop QTimer::singleShot(0, this, SIGNAL(editTransaction())); } } } } void Register::slotEnsureItemVisible() { Q_D(Register); // if clear() has been called since the timer was // started, we just ignore the call if (!d->m_ensureVisibleItem) return; // make sure to catch latest changes setUpdatesEnabled(false); updateRegister(); setUpdatesEnabled(true); // since the item will be made visible at the top of the viewport make the bottom index visible first to make the whole item visible scrollTo(model()->index(d->m_ensureVisibleItem->startRow() + d->m_ensureVisibleItem->numRowsRegister() - 1, (int)eTransaction::Column::Detail)); scrollTo(model()->index(d->m_ensureVisibleItem->startRow(), (int)eTransaction::Column::Detail)); } QString Register::text(int /*row*/, int /*col*/) const { return QString("a"); } QWidget* Register::createEditor(int /*row*/, int /*col*/, bool /*initFromCell*/) const { return 0; } void Register::setCellContentFromEditor(int /*row*/, int /*col*/) { } void Register::endEdit(int /*row*/, int /*col*/, bool /*accept*/, bool /*replace*/) { } RegisterItem* Register::focusItem() const { Q_D(const Register); return d->m_focusItem; } RegisterItem* Register::anchorItem() const { Q_D(const Register); return d->m_selectAnchor; } void Register::arrangeEditWidgets(QMap& editWidgets, KMyMoneyRegister::Transaction* t) { t->arrangeWidgetsInRegister(editWidgets); ensureItemVisible(t); // updateContents(); } void Register::tabOrder(QWidgetList& tabOrderWidgets, KMyMoneyRegister::Transaction* t) const { t->tabOrderInRegister(tabOrderWidgets); } void Register::removeEditWidgets(QMap& editWidgets) { // remove pointers from map QMap::iterator it; for (it = editWidgets.begin(); it != editWidgets.end();) { if ((*it)->parentWidget() == this) { editWidgets.erase(it); it = editWidgets.begin(); } else ++it; } // now delete the widgets KMyMoneyRegister::Transaction* t = dynamic_cast(focusItem()); for (int row = t->startRow(); row < t->startRow() + t->numRowsRegister(true); ++row) { for (int col = 0; col < columnCount(); ++col) { if (cellWidget(row, col)) { cellWidget(row, col)->hide(); setCellWidget(row, col, 0); } } // make sure to reduce the possibly size to what it was before editing started setRowHeight(row, t->rowHeightHint()); } } RegisterItem* Register::itemById(const QString& id) const { Q_D(const Register); if (id.isEmpty()) return d->m_lastItem; for (QVector::size_type i = 0; i < d->m_items.size(); ++i) { RegisterItem* item = d->m_items[i]; if (!item) continue; if (item->id() == id) return item; } return 0; } void Register::handleItemChange(RegisterItem* old, bool shift, bool control) { Q_D(Register); if (d->m_selectionMode == MultiSelection) { if (shift) { selectRange(d->m_selectAnchor ? d->m_selectAnchor : old, d->m_focusItem, false, true, (d->m_selectAnchor && !control) ? true : false); } else if (!control) { selectItem(d->m_focusItem, false); } } } void Register::selectRange(RegisterItem* from, RegisterItem* to, bool invert, bool includeFirst, bool clearSel) { if (!from || !to) return; if (from == to && !includeFirst) return; bool swap = false; if (to == from->prevItem()) swap = true; RegisterItem* item; if (!swap && from != to && from != to->prevItem()) { bool found = false; for (item = from; item; item = item->nextItem()) { if (item == to) { found = true; break; } } if (!found) swap = true; } if (swap) { item = from; from = to; to = item; if (!includeFirst) to = to->prevItem(); } else if (!includeFirst) { from = from->nextItem(); } if (clearSel) { for (item = firstItem(); item; item = item->nextItem()) { if (item->isSelected() && item->isVisible()) { item->setSelected(false); } } } for (item = from; item; item = item->nextItem()) { if (item->isSelectable()) { if (!invert) { if (!item->isSelected() && item->isVisible()) { item->setSelected(true); } } else { bool sel = !item->isSelected(); if ((item->isSelected() != sel) && item->isVisible()) { item->setSelected(sel); } } } if (item == to) break; } } void Register::scrollPage(int key, Qt::KeyboardModifiers modifiers) { Q_D(Register); RegisterItem* oldFocusItem = d->m_focusItem; // make sure we have a focus item if (!d->m_focusItem) setFocusItem(d->m_firstItem); if (!d->m_focusItem && d->m_firstItem) setFocusItem(d->m_firstItem->nextItem()); if (!d->m_focusItem) return; RegisterItem* item = d->m_focusItem; int height = 0; switch (key) { case Qt::Key_PageUp: while (height < viewport()->height() && item->prevItem()) { do { item = item->prevItem(); if (item->isVisible()) height += item->rowHeightHint(); } while ((!item->isSelectable() || !item->isVisible()) && item->prevItem()); while ((!item->isSelectable() || !item->isVisible()) && item->nextItem()) item = item->nextItem(); } break; case Qt::Key_PageDown: while (height < viewport()->height() && item->nextItem()) { do { if (item->isVisible()) height += item->rowHeightHint(); item = item->nextItem(); } while ((!item->isSelectable() || !item->isVisible()) && item->nextItem()); while ((!item->isSelectable() || !item->isVisible()) && item->prevItem()) item = item->prevItem(); } break; case Qt::Key_Up: if (item->prevItem()) { do { item = item->prevItem(); } while ((!item->isSelectable() || !item->isVisible()) && item->prevItem()); } break; case Qt::Key_Down: if (item->nextItem()) { do { item = item->nextItem(); } while ((!item->isSelectable() || !item->isVisible()) && item->nextItem()); } break; case Qt::Key_Home: item = d->m_firstItem; while ((!item->isSelectable() || !item->isVisible()) && item->nextItem()) item = item->nextItem(); break; case Qt::Key_End: item = d->m_lastItem; while ((!item->isSelectable() || !item->isVisible()) && item->prevItem()) item = item->prevItem(); break; } // make sure to avoid selecting a possible empty transaction at the end Transaction* t = dynamic_cast(item); if (t && t->transaction().id().isEmpty()) { if (t->prevItem()) { item = t->prevItem(); } } if (!(modifiers & Qt::ShiftModifier) || !d->m_selectAnchor) d->m_selectAnchor = item; setFocusItem(item); if (item->isSelectable()) { handleItemChange(oldFocusItem, modifiers & Qt::ShiftModifier, modifiers & Qt::ControlModifier); // tell the world about the changes in selection SelectedTransactions list(this); emit transactionsSelected(list); } if (d->m_focusItem && !d->m_focusItem->isSelected() && d->m_selectionMode == SingleSelection) selectItem(item); } void Register::keyPressEvent(QKeyEvent* ev) { Q_D(Register); switch (ev->key()) { case Qt::Key_Space: if (d->m_selectionMode != NoSelection) { // get the state out of the event ... d->m_modifiers = ev->modifiers(); // ... and pretend that we have pressed the left mouse button ;) d->m_mouseButton = Qt::LeftButton; selectItem(d->m_focusItem); } break; case Qt::Key_PageUp: case Qt::Key_PageDown: case Qt::Key_Home: case Qt::Key_End: case Qt::Key_Down: case Qt::Key_Up: scrollPage(ev->key(), ev->modifiers()); break; case Qt::Key_Enter: case Qt::Key_Return: // don't emit the signal right away but wait until // we come back to the Qt main loop QTimer::singleShot(0, this, SIGNAL(editTransaction())); break; default: QTableWidget::keyPressEvent(ev); break; } } Transaction* Register::transactionFactory(Register *parent, const MyMoneyTransaction& transaction, const MyMoneySplit& split, int uniqueId) { Transaction* t = 0; MyMoneySplit s = split; if (parent->account() == MyMoneyAccount()) { t = new KMyMoneyRegister::StdTransaction(parent, transaction, s, uniqueId); return t; } switch (parent->account().accountType()) { 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::Currency: case Account::Type::Income: case Account::Type::Expense: case Account::Type::AssetLoan: case Account::Type::Equity: if (s.accountId().isEmpty()) s.setAccountId(parent->account().id()); if (s.isMatched()) t = new KMyMoneyRegister::StdTransactionMatched(parent, transaction, s, uniqueId); else if (transaction.isImported()) t = new KMyMoneyRegister::StdTransactionDownloaded(parent, transaction, s, uniqueId); else t = new KMyMoneyRegister::StdTransaction(parent, transaction, s, uniqueId); break; case Account::Type::Investment: if (s.isMatched()) t = new KMyMoneyRegister::InvestTransaction/* Matched */(parent, transaction, s, uniqueId); else if (transaction.isImported()) t = new KMyMoneyRegister::InvestTransactionDownloaded(parent, transaction, s, uniqueId); else t = new KMyMoneyRegister::InvestTransaction(parent, transaction, s, uniqueId); break; case Account::Type::CertificateDep: case Account::Type::MoneyMarket: case Account::Type::Stock: default: qDebug("Register::transactionFactory: invalid accountTypeE %d", (int)parent->account().accountType()); break; } return t; } const MyMoneyAccount& Register::account() const { Q_D(const Register); return d->m_account; } void Register::addGroupMarkers() { Q_D(Register); QMap list; QMap::const_iterator it; KMyMoneyRegister::RegisterItem* p = firstItem(); KMyMoneyRegister::Transaction* t; QString name; QDate today; QDate yesterday, thisWeek, lastWeek; QDate thisMonth, lastMonth; QDate thisYear; int weekStartOfs; switch (primarySortKey()) { case SortField::PostDate: case SortField::EntryDate: today = QDate::currentDate(); thisMonth.setDate(today.year(), today.month(), 1); lastMonth = thisMonth.addMonths(-1); yesterday = today.addDays(-1); // a = QDate::dayOfWeek() todays weekday (1 = Monday, 7 = Sunday) // b = QLocale().firstDayOfWeek() first day of week (1 = Monday, 7 = Sunday) weekStartOfs = today.dayOfWeek() - QLocale().firstDayOfWeek(); if (weekStartOfs < 0) { weekStartOfs = 7 + weekStartOfs; } thisWeek = today.addDays(-weekStartOfs); lastWeek = thisWeek.addDays(-7); thisYear.setDate(today.year(), 1, 1); if (KMyMoneyGlobalSettings::startDate().date() != QDate(1900, 1, 1)) new KMyMoneyRegister::FancyDateGroupMarker(this, KMyMoneyGlobalSettings::startDate().date(), i18n("Prior transactions possibly filtered")); if (KMyMoneyGlobalSettings::showFancyMarker()) { if (d->m_account.lastReconciliationDate().isValid()) new KMyMoneyRegister::StatementGroupMarker(this, eRegister::CashFlowDirection::Deposit, d->m_account.lastReconciliationDate(), i18n("Last reconciliation")); if (!d->m_account.value("lastImportedTransactionDate").isEmpty() && !d->m_account.value("lastStatementBalance").isEmpty()) { MyMoneyMoney balance(d->m_account.value("lastStatementBalance")); if (d->m_account.accountGroup() == Account::Type::Liability) balance = -balance; auto txt = i18n("Online Statement Balance: %1", balance.formatMoney(d->m_account.fraction())); KMyMoneyRegister::StatementGroupMarker *p = new KMyMoneyRegister::StatementGroupMarker(this, eRegister::CashFlowDirection::Deposit, QDate::fromString(d->m_account.value("lastImportedTransactionDate"), Qt::ISODate), txt); p->setErroneous(!MyMoneyFile::instance()->hasMatchingOnlineBalance(d->m_account)); } new KMyMoneyRegister::FancyDateGroupMarker(this, thisYear, i18n("This year")); new KMyMoneyRegister::FancyDateGroupMarker(this, lastMonth, i18n("Last month")); new KMyMoneyRegister::FancyDateGroupMarker(this, thisMonth, i18n("This month")); new KMyMoneyRegister::FancyDateGroupMarker(this, lastWeek, i18n("Last week")); new KMyMoneyRegister::FancyDateGroupMarker(this, thisWeek, i18n("This week")); new KMyMoneyRegister::FancyDateGroupMarker(this, yesterday, i18n("Yesterday")); new KMyMoneyRegister::FancyDateGroupMarker(this, today, i18n("Today")); new KMyMoneyRegister::FancyDateGroupMarker(this, today.addDays(1), i18n("Future transactions")); new KMyMoneyRegister::FancyDateGroupMarker(this, thisWeek.addDays(7), i18n("Next week")); new KMyMoneyRegister::FancyDateGroupMarker(this, thisMonth.addMonths(1), i18n("Next month")); } else { new KMyMoneyRegister::SimpleDateGroupMarker(this, today.addDays(1), i18n("Future transactions")); } if (KMyMoneyGlobalSettings::showFiscalMarker()) { QDate currentFiscalYear = KMyMoneyGlobalSettings::firstFiscalDate(); new KMyMoneyRegister::FiscalYearGroupMarker(this, currentFiscalYear, i18n("Current fiscal year")); new KMyMoneyRegister::FiscalYearGroupMarker(this, currentFiscalYear.addYears(-1), i18n("Previous fiscal year")); new KMyMoneyRegister::FiscalYearGroupMarker(this, currentFiscalYear.addYears(1), i18n("Next fiscal year")); } break; case SortField::Type: if (KMyMoneyGlobalSettings::showFancyMarker()) { new KMyMoneyRegister::TypeGroupMarker(this, eRegister::CashFlowDirection::Deposit, d->m_account.accountType()); new KMyMoneyRegister::TypeGroupMarker(this, eRegister::CashFlowDirection::Payment, d->m_account.accountType()); } break; case SortField::ReconcileState: if (KMyMoneyGlobalSettings::showFancyMarker()) { new KMyMoneyRegister::ReconcileGroupMarker(this, eMyMoney::Split::State::NotReconciled); new KMyMoneyRegister::ReconcileGroupMarker(this, eMyMoney::Split::State::Cleared); new KMyMoneyRegister::ReconcileGroupMarker(this, eMyMoney::Split::State::Reconciled); new KMyMoneyRegister::ReconcileGroupMarker(this, eMyMoney::Split::State::Frozen); } break; case SortField::Payee: if (KMyMoneyGlobalSettings::showFancyMarker()) { while (p) { t = dynamic_cast(p); if (t) { list[t->sortPayee()] = 1; } p = p->nextItem(); } for (it = list.constBegin(); it != list.constEnd(); ++it) { name = it.key(); if (name.isEmpty()) { name = i18nc("Unknown payee", "Unknown"); } new KMyMoneyRegister::PayeeGroupMarker(this, name); } } break; case SortField::Category: if (KMyMoneyGlobalSettings::showFancyMarker()) { while (p) { t = dynamic_cast(p); if (t) { list[t->sortCategory()] = 1; } p = p->nextItem(); } for (it = list.constBegin(); it != list.constEnd(); ++it) { name = it.key(); if (name.isEmpty()) { name = i18nc("Unknown category", "Unknown"); } new KMyMoneyRegister::CategoryGroupMarker(this, name); } } break; case SortField::Security: if (KMyMoneyGlobalSettings::showFancyMarker()) { while (p) { t = dynamic_cast(p); if (t) { list[t->sortSecurity()] = 1; } p = p->nextItem(); } for (it = list.constBegin(); it != list.constEnd(); ++it) { name = it.key(); if (name.isEmpty()) { name = i18nc("Unknown security", "Unknown"); } new KMyMoneyRegister::CategoryGroupMarker(this, name); } } break; default: // no markers supported break; } } void Register::removeUnwantedGroupMarkers() { // remove all trailing group markers except statement markers KMyMoneyRegister::RegisterItem* q; KMyMoneyRegister::RegisterItem* p = lastItem(); while (p) { q = p; if (dynamic_cast(p) || dynamic_cast(p)) break; p = p->prevItem(); delete q; } // remove all adjacent group markers bool lastWasGroupMarker = false; p = lastItem(); while (p) { q = p; KMyMoneyRegister::GroupMarker* m = dynamic_cast(p); p = p->prevItem(); if (m) { m->markVisible(true); // make adjacent group marker invisible except those that show statement information if (lastWasGroupMarker && (dynamic_cast(m) == 0)) { m->markVisible(false); } lastWasGroupMarker = true; } else if (q->isVisible()) lastWasGroupMarker = false; } } void Register::setLedgerLensForced(bool forced) { Q_D(Register); d->m_ledgerLensForced = forced; } bool Register::ledgerLens() const { Q_D(const Register); return d->m_ledgerLensForced; } void Register::setSelectionMode(SelectionMode mode) { Q_D(Register); d->m_selectionMode = mode; } void Register::setUsedWithEditor(bool value) { Q_D(Register); d->m_usedWithEditor = value; } eRegister::DetailColumn Register::getDetailsColumnType() const { Q_D(const Register); return d->m_detailsColumnType; } void Register::setDetailsColumnType(eRegister::DetailColumn detailsColumnType) { Q_D(Register); d->m_detailsColumnType = detailsColumnType; } } diff --git a/kmymoney/widgets/stdtransaction.cpp b/kmymoney/widgets/stdtransaction.cpp index f6de13d6f..8eb3bf1fe 100644 --- a/kmymoney/widgets/stdtransaction.cpp +++ b/kmymoney/widgets/stdtransaction.cpp @@ -1,701 +1,702 @@ /*************************************************************************** stdtransaction.cpp - description ------------------- begin : Tue Jun 13 2006 copyright : (C) 2000-2006 by 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 "stdtransaction.h" #include "stdtransaction_p.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include // ---------------------------------------------------------------------------- // Project Includes #include "kmymoneypayeecombo.h" #include "kmymoneycombo.h" #include "kmymoneytagcombo.h" #include "tabbar.h" #include "ktagcontainer.h" #include "mymoneytransaction.h" #include "mymoneysplit.h" +#include "mymoneyexception.h" #include "mymoneyfile.h" #include "register.h" #include "transactionform.h" #include "kmymoneylineedit.h" #include "kmymoneyutils.h" #ifndef KMM_DESIGNER #include "stdtransactioneditor.h" #endif #include "kmymoneyglobalsettings.h" #include "widgetenums.h" #include "mymoneyenums.h" using namespace eWidgets; using namespace KMyMoneyRegister; using namespace KMyMoneyTransactionForm; StdTransaction::StdTransaction(Register *parent, const MyMoneyTransaction& transaction, const MyMoneySplit& split, int uniqueId) : Transaction(*new StdTransactionPrivate, parent, transaction, split, uniqueId) { Q_D(StdTransaction); d->m_showAccountRow = false; try { d->m_categoryHeader = i18n("Category"); switch (transaction.splitCount()) { default: d->m_category = i18nc("Split transaction (category replacement)", "Split transaction"); break; case 0: // the empty transaction case 1: break; case 2: setupFormHeader(d->m_transaction.splitByAccount(d->m_split.accountId(), false).accountId()); break; } } catch (const MyMoneyException &e) { qDebug() << "Problem determining the category for transaction '" << d->m_transaction.id() << "'. Reason: " << e.what() << "\n"; } d->m_rowsForm = 6; if (KMyMoneyUtils::transactionType(d->m_transaction) == KMyMoneyUtils::InvestmentTransaction) { MyMoneySplit split = KMyMoneyUtils::stockSplit(d->m_transaction); d->m_payee = MyMoneyFile::instance()->account(split.accountId()).name(); QString addon; if (split.action() == MyMoneySplit::ActionBuyShares) { if (split.value().isNegative()) { addon = i18n("Sell"); } else { addon = i18n("Buy"); } } else if (split.action() == MyMoneySplit::ActionDividend) { addon = i18n("Dividend"); } else if (split.action() == MyMoneySplit::ActionYield) { addon = i18n("Yield"); } else if (split.action() == MyMoneySplit::ActionInterestIncome) { addon = i18n("Interest Income"); } if (!addon.isEmpty()) { d->m_payee += QString(" (%1)").arg(addon); } d->m_payeeHeader = i18n("Activity"); d->m_category = i18n("Investment transaction"); } // setup initial size setNumRowsRegister(numRowsRegister(KMyMoneyGlobalSettings::showRegisterDetailed())); emit parent->itemAdded(this); } StdTransaction::~StdTransaction() { } const char* StdTransaction::className() { return "StdTransaction"; } void StdTransaction::setupFormHeader(const QString& id) { Q_D(StdTransaction); d->m_category = MyMoneyFile::instance()->accountToCategory(id); switch (MyMoneyFile::instance()->account(id).accountGroup()) { case eMyMoney::Account::Type::Asset: case eMyMoney::Account::Type::Liability: d->m_categoryHeader = d->m_split.shares().isNegative() ? i18n("Transfer to") : i18n("Transfer from"); break; default: d->m_categoryHeader = i18n("Category"); break; } } eRegister::Action StdTransaction::actionType() const { Q_D(const StdTransaction); eRegister::Action action = eRegister::Action::None; // if at least one split is referencing an income or // expense account, we will not call it a transfer auto found = false; foreach (const auto split, d->m_transaction.splits()) { if (split.accountId() == d->m_split.accountId()) continue; auto acc = MyMoneyFile::instance()->account(split.accountId()); if (acc.accountGroup() == eMyMoney::Account::Type::Income || acc.accountGroup() == eMyMoney::Account::Type::Expense) { // otherwise, we have to determine between deposit and withdrawal action = d->m_split.shares().isNegative() ? eRegister::Action::Withdrawal : eRegister::Action::Deposit; found = true; break; } } // otherwise, it's a transfer if (!found) action = eRegister::Action::Transfer; return action; } void StdTransaction::loadTab(TransactionForm* form) { Q_D(StdTransaction); KMyMoneyTransactionForm::TabBar* bar = form->getTabBar(); bar->setSignalEmission(eTabBar::SignalEmission::Never); for (auto i = 0; i < bar->count(); ++i) { bar->setTabEnabled(i, true); } if (d->m_transaction.splitCount() > 0) { bar->setCurrentIndex((int)actionType()); } bar->setSignalEmission(eTabBar::SignalEmission::Always); } int StdTransaction::numColsForm() const { return 4; } void StdTransaction::setupForm(TransactionForm* form) { Transaction::setupForm(form); form->setSpan(4, (int)eTransactionForm::Column::Value1, 3, 1); } bool StdTransaction::showRowInForm(int row) const { Q_D(const StdTransaction); return row == 0 ? d->m_showAccountRow : true; } void StdTransaction::setShowRowInForm(int row, bool show) { Q_D(StdTransaction); if (row == 0) d->m_showAccountRow = show; } bool StdTransaction::formCellText(QString& txt, Qt::Alignment& align, int row, int col, QPainter* /* painter */) { Q_D(const StdTransaction); // if(m_transaction != MyMoneyTransaction()) { switch (row) { case 0: switch (col) { case (int)eTransactionForm::Column::Label1: align |= Qt::AlignLeft; txt = i18n("Account"); break; } break; case 1: switch (col) { case (int)eTransactionForm::Column::Label1: align |= Qt::AlignLeft; txt = d->m_payeeHeader; break; case (int)eTransactionForm::Column::Value1: align |= Qt::AlignLeft; txt = d->m_payee; break; case (int)eTransactionForm::Column::Label2: align |= Qt::AlignLeft; if (haveNumberField()) txt = i18n("Number"); break; case (int)eTransactionForm::Column::Value2: align |= Qt::AlignRight; if (haveNumberField()) txt = d->m_split.number(); break; } break; case 2: switch (col) { case (int)eTransactionForm::Column::Label1: align |= Qt::AlignLeft; txt = d->m_categoryHeader; break; case (int)eTransactionForm::Column::Value1: align |= Qt::AlignLeft; txt = d->m_category; if (d->m_transaction != MyMoneyTransaction()) { if (txt.isEmpty() && !d->m_split.value().isZero()) txt = i18n("*** UNASSIGNED ***"); } break; case (int)eTransactionForm::Column::Label2: align |= Qt::AlignLeft; txt = i18n("Date"); break; case (int)eTransactionForm::Column::Value2: align |= Qt::AlignRight; if (d->m_transaction != MyMoneyTransaction()) txt = QLocale().toString(d->m_transaction.postDate(), QLocale::ShortFormat); break; } break; case 3: switch (col) { case (int)eTransactionForm::Column::Label1: align |= Qt::AlignLeft; txt = i18n("Tags"); break; case (int)eTransactionForm::Column::Value1: align |= Qt::AlignLeft; if (!d->m_tagList.isEmpty()) { for (auto i = 0; i < d->m_tagList.size() - 1; ++i) txt += d->m_tagList[i] + ", "; txt += d->m_tagList.last(); } //if (m_transaction != MyMoneyTransaction()) // txt = m_split.tagId(); break; case (int)eTransactionForm::Column::Label2: align |= Qt::AlignLeft; txt = i18n("Amount"); break; case (int)eTransactionForm::Column::Value2: align |= Qt::AlignRight; if (d->m_transaction != MyMoneyTransaction()) { txt = (d->m_split.value(d->m_transaction.commodity(), d->m_splitCurrencyId).abs()).formatMoney(d->m_account.fraction()); } break; } break; case 4: switch (col) { case (int)eTransactionForm::Column::Label1: align |= Qt::AlignLeft; txt = i18n("Memo"); break; case (int)eTransactionForm::Column::Value1: align &= ~Qt::AlignVCenter; align |= Qt::AlignTop; align |= Qt::AlignLeft; if (d->m_transaction != MyMoneyTransaction()) txt = d->m_split.memo().section('\n', 0, 2); break; } break; case 5: switch (col) { case (int)eTransactionForm::Column::Label2: align |= Qt::AlignLeft; txt = i18n("Status"); break; case (int)eTransactionForm::Column::Value2: align |= Qt::AlignRight; txt = reconcileState(); break; } } // } if (col == (int)eTransactionForm::Column::Value2 && row == 1) { return haveNumberField(); } return (col == (int)eTransactionForm::Column::Value1 && row < 5) || (col == (int)eTransactionForm::Column::Value2 && row > 0 && row != 4); } void StdTransaction::registerCellText(QString& txt, Qt::Alignment& align, int row, int col, QPainter* painter) { Q_D(const StdTransaction); switch (row) { case 0: switch (col) { case (int)eTransaction::Column::Number: align |= Qt::AlignLeft; if (haveNumberField()) txt = d->m_split.number(); break; case (int)eTransaction::Column::Date: align |= Qt::AlignLeft; txt = QLocale().toString(d->m_transaction.postDate(), QLocale::ShortFormat); break; case (int)eTransaction::Column::Detail: switch (d->m_parent->getDetailsColumnType()) { case eRegister::DetailColumn::PayeeFirst: txt = d->m_payee; break; case eRegister::DetailColumn::AccountFirst: txt = d->m_category; if (!d->m_tagList.isEmpty()) { txt += " ( "; for (auto i = 0; i < d->m_tagList.size() - 1; ++i) { txt += " " + d->m_tagList[i] + ", "; } txt += " " + d->m_tagList.last() + " )"; } break; } align |= Qt::AlignLeft; if (txt.isEmpty() && d->m_rowsRegister < 3) { singleLineMemo(txt, d->m_split); } if (txt.isEmpty() && d->m_rowsRegister < 2) { if (d->m_account.accountType() != eMyMoney::Account::Type::Income && d->m_account.accountType() != eMyMoney::Account::Type::Expense) { txt = d->m_category; if (txt.isEmpty() && !d->m_split.value().isZero()) { txt = i18n("*** UNASSIGNED ***"); if (painter) painter->setPen(KMyMoneyGlobalSettings::schemeColor(SchemeColor::TransactionErroneous)); } } } break; case (int)eTransaction::Column::ReconcileFlag: align |= Qt::AlignHCenter; txt = reconcileState(false); break; case (int)eTransaction::Column::Payment: align |= Qt::AlignRight; if (d->m_split.value().isNegative()) { txt = (-d->m_split.value(d->m_transaction.commodity(), d->m_splitCurrencyId)).formatMoney(d->m_account.fraction()); } break; case (int)eTransaction::Column::Deposit: align |= Qt::AlignRight; if (!d->m_split.value().isNegative()) { txt = d->m_split.value(d->m_transaction.commodity(), d->m_splitCurrencyId).formatMoney(d->m_account.fraction()); } break; case (int)eTransaction::Column::Balance: align |= Qt::AlignRight; if (d->m_showBalance) txt = d->m_balance.formatMoney(d->m_account.fraction()); else txt = "----"; break; case (int)eTransaction::Column::Account: // txt = m_objects->account(m_transaction.splits()[0].accountId()).name(); txt = MyMoneyFile::instance()->account(d->m_split.accountId()).name(); break; default: break; } break; case 1: switch (col) { case (int)eTransaction::Column::Detail: switch (d->m_parent->getDetailsColumnType()) { case eRegister::DetailColumn::PayeeFirst: txt = d->m_category; if (!d->m_tagList.isEmpty()) { txt += " ( "; for (auto i = 0; i < d->m_tagList.size() - 1; ++i) { txt += " " + d->m_tagList[i] + ", "; } txt += " " + d->m_tagList.last() + " )"; } break; case eRegister::DetailColumn::AccountFirst: txt = d->m_payee; break; } align |= Qt::AlignLeft; if (txt.isEmpty() && !d->m_split.value().isZero()) { txt = i18n("*** UNASSIGNED ***"); if (painter) painter->setPen(KMyMoneyGlobalSettings::schemeColor(SchemeColor::TransactionErroneous)); } break; default: break; } break; case 2: switch (col) { case (int)eTransaction::Column::Detail: align |= Qt::AlignLeft; singleLineMemo(txt, d->m_split); break; default: break; } break; } } int StdTransaction::registerColWidth(int col, const QFontMetrics& cellFontMetrics) { QString txt; int firstRow = 0, lastRow = numRowsRegister(); int nw = 0; for (int i = firstRow; i <= lastRow; ++i) { Qt::Alignment align; registerCellText(txt, align, i, col, 0); int w = cellFontMetrics.width(txt + " "); if (w > nw) nw = w; } return nw; } void StdTransaction::arrangeWidgetsInForm(QMap& editWidgets) { Q_D(StdTransaction); if (!d->m_form || !d->m_parent) return; setupFormPalette(editWidgets); arrangeWidget(d->m_form, 0, (int)eTransactionForm::Column::Label1, editWidgets["account-label"]); arrangeWidget(d->m_form, 0, (int)eTransactionForm::Column::Value1, editWidgets["account"]); arrangeWidget(d->m_form, 1, (int)eTransactionForm::Column::Label1, editWidgets["cashflow"]); arrangeWidget(d->m_form, 1, (int)eTransactionForm::Column::Value1, editWidgets["payee"]); arrangeWidget(d->m_form, 2, (int)eTransactionForm::Column::Label1, editWidgets["category-label"]); arrangeWidget(d->m_form, 2, (int)eTransactionForm::Column::Value1, editWidgets["category"]->parentWidget()); arrangeWidget(d->m_form, 3, (int)eTransactionForm::Column::Label1, editWidgets["tag-label"]); arrangeWidget(d->m_form, 3, (int)eTransactionForm::Column::Value1, editWidgets["tag"]); arrangeWidget(d->m_form, 4, (int)eTransactionForm::Column::Label1, editWidgets["memo-label"]); arrangeWidget(d->m_form, 4, (int)eTransactionForm::Column::Value1, editWidgets["memo"]); if (haveNumberField()) { arrangeWidget(d->m_form, 1, (int)eTransactionForm::Column::Label2, editWidgets["number-label"]); arrangeWidget(d->m_form, 1, (int)eTransactionForm::Column::Value2, editWidgets["number"]); } arrangeWidget(d->m_form, 2, (int)eTransactionForm::Column::Label2, editWidgets["date-label"]); arrangeWidget(d->m_form, 2, (int)eTransactionForm::Column::Value2, editWidgets["postdate"]); arrangeWidget(d->m_form, 3, (int)eTransactionForm::Column::Label2, editWidgets["amount-label"]); arrangeWidget(d->m_form, 3, (int)eTransactionForm::Column::Value2, editWidgets["amount"]); arrangeWidget(d->m_form, 5, (int)eTransactionForm::Column::Label2, editWidgets["status-label"]); arrangeWidget(d->m_form, 5, (int)eTransactionForm::Column::Value2, editWidgets["status"]); // get rid of the hints. we don't need them for the form QMap::iterator it; for (it = editWidgets.begin(); it != editWidgets.end(); ++it) { KMyMoneyCombo* combo = dynamic_cast(*it); KMyMoneyLineEdit* edit = dynamic_cast(*it); KMyMoneyPayeeCombo* payee = dynamic_cast(*it); KTagContainer* tag = dynamic_cast(*it); if (combo) combo->setPlaceholderText(QString()); if (edit) edit->setPlaceholderText(QString()); if (payee) payee->setPlaceholderText(QString()); if (tag) tag->tagCombo()->setPlaceholderText(QString()); } auto form = dynamic_cast(d->m_form); auto w = dynamic_cast(editWidgets["tabbar"]); if (w) { // insert the tabbar in the boxlayout so it will take the place of the original tabbar which was hidden QBoxLayout* boxLayout = dynamic_cast(form->getTabBar()->parentWidget()->layout()); boxLayout->insertWidget(0, w); } } void StdTransaction::tabOrderInForm(QWidgetList& tabOrderWidgets) const { Q_D(const StdTransaction); QStringList taborder = KMyMoneyGlobalSettings::stdTransactionFormTabOrder().split(',', QString::SkipEmptyParts); QStringList::const_iterator it_s = taborder.constBegin(); QWidget* w; while (it_s != taborder.constEnd()) { if (*it_s == "account") { tabOrderWidgets.append(focusWidget(d->m_form->cellWidget(0, (int)eTransactionForm::Column::Value1))); } else if (*it_s == "cashflow") { tabOrderWidgets.append(focusWidget(d->m_form->cellWidget(1, (int)eTransactionForm::Column::Label1))); } else if (*it_s == "payee") { tabOrderWidgets.append(focusWidget(d->m_form->cellWidget(1, (int)eTransactionForm::Column::Value1))); } else if (*it_s == "category") { // make sure to have the category field and the split button as separate tab order widgets // ok, we have to have some internal knowledge about the KMyMoneyCategory object, but // it's one of our own widgets, so we actually don't care. Just make sure, that we don't // go haywire when someone changes the KMyMoneyCategory object ... QWidget* w = d->m_form->cellWidget(2, (int)eTransactionForm::Column::Value1); tabOrderWidgets.append(focusWidget(w)); w = w->findChild("splitButton"); if (w) tabOrderWidgets.append(w); } else if (*it_s == "tag") { tabOrderWidgets.append(focusWidget(d->m_form->cellWidget(3, (int)eTransactionForm::Column::Value1))); } else if (*it_s == "memo") { tabOrderWidgets.append(focusWidget(d->m_form->cellWidget(4, (int)eTransactionForm::Column::Value1))); } else if (*it_s == "number") { if (haveNumberField()) { if ((w = focusWidget(d->m_form->cellWidget(1, (int)eTransactionForm::Column::Value2)))) tabOrderWidgets.append(w); } } else if (*it_s == "date") { tabOrderWidgets.append(focusWidget(d->m_form->cellWidget(2, (int)eTransactionForm::Column::Value2))); } else if (*it_s == "amount") { tabOrderWidgets.append(focusWidget(d->m_form->cellWidget(3, (int)eTransactionForm::Column::Value2))); } else if (*it_s == "state") { tabOrderWidgets.append(focusWidget(d->m_form->cellWidget(5, (int)eTransactionForm::Column::Value2))); } ++it_s; } } void StdTransaction::arrangeWidgetsInRegister(QMap& editWidgets) { Q_D(StdTransaction); if (!d->m_parent) return; setupRegisterPalette(editWidgets); if (haveNumberField()) arrangeWidget(d->m_parent, d->m_startRow + 0, (int)eTransaction::Column::Number, editWidgets["number"]); arrangeWidget(d->m_parent, d->m_startRow + 0, (int)eTransaction::Column::Date, editWidgets["postdate"]); arrangeWidget(d->m_parent, d->m_startRow + 1, (int)eTransaction::Column::Date, editWidgets["status"]); arrangeWidget(d->m_parent, d->m_startRow + 0, (int)eTransaction::Column::Detail, editWidgets["payee"]); arrangeWidget(d->m_parent, d->m_startRow + 1, (int)eTransaction::Column::Detail, editWidgets["category"]->parentWidget()); arrangeWidget(d->m_parent, d->m_startRow + 2, (int)eTransaction::Column::Detail, editWidgets["tag"]); arrangeWidget(d->m_parent, d->m_startRow + 3, (int)eTransaction::Column::Detail, editWidgets["memo"]); arrangeWidget(d->m_parent, d->m_startRow + 0, (int)eTransaction::Column::Payment, editWidgets["payment"]); arrangeWidget(d->m_parent, d->m_startRow + 0, (int)eTransaction::Column::Deposit, editWidgets["deposit"]); // increase the height of the row containing the memo widget d->m_parent->setRowHeight(d->m_startRow + 3, d->m_parent->rowHeightHint() * 3); } void StdTransaction::tabOrderInRegister(QWidgetList& tabOrderWidgets) const { Q_D(const StdTransaction); QStringList taborder = KMyMoneyGlobalSettings::stdTransactionRegisterTabOrder().split(',', QString::SkipEmptyParts); QStringList::const_iterator it_s = taborder.constBegin(); QWidget* w; while (it_s != taborder.constEnd()) { if (*it_s == "number") { if (haveNumberField()) { if ((w = focusWidget(d->m_parent->cellWidget(d->m_startRow + 0, (int)eTransaction::Column::Number)))) tabOrderWidgets.append(w); } } else if (*it_s == "date") { tabOrderWidgets.append(focusWidget(d->m_parent->cellWidget(d->m_startRow + 0, (int)eTransaction::Column::Date))); } else if (*it_s == "payee") { tabOrderWidgets.append(focusWidget(d->m_parent->cellWidget(d->m_startRow + 0, (int)eTransaction::Column::Detail))); } else if (*it_s == "category") { // make sure to have the category field and the split button as separate tab order widgets // ok, we have to have some internal knowledge about the KMyMoneyCategory object, but // it's one of our own widgets, so we actually don't care. Just make sure, that we don't // go haywire when someone changes the KMyMoneyCategory object ... w = d->m_parent->cellWidget(d->m_startRow + 1, (int)eTransaction::Column::Detail); tabOrderWidgets.append(focusWidget(w)); w = w->findChild("splitButton"); if (w) tabOrderWidgets.append(w); } else if (*it_s == "tag") { tabOrderWidgets.append(focusWidget(d->m_parent->cellWidget(d->m_startRow + 2, (int)eTransaction::Column::Detail))); } else if (*it_s == "memo") { tabOrderWidgets.append(focusWidget(d->m_parent->cellWidget(d->m_startRow + 3, (int)eTransaction::Column::Detail))); } else if (*it_s == "payment") { tabOrderWidgets.append(focusWidget(d->m_parent->cellWidget(d->m_startRow + 0, (int)eTransaction::Column::Payment))); } else if (*it_s == "deposit") { tabOrderWidgets.append(focusWidget(d->m_parent->cellWidget(d->m_startRow + 0, (int)eTransaction::Column::Deposit))); } else if (*it_s == "state") { tabOrderWidgets.append(focusWidget(d->m_parent->cellWidget(d->m_startRow + 1, (int)eTransaction::Column::Date))); } ++it_s; } } int StdTransaction::numRowsRegister(bool expanded) const { Q_D(const StdTransaction); int numRows = 1; if (expanded) { numRows = 4; if (!d->m_inEdit) { //When not in edit Tags haven't a separate row; numRows--; if (d->m_payee.isEmpty()) { numRows--; } if (d->m_split.memo().isEmpty()) { numRows--; } // For income and expense accounts that only have // two splits we only show one line, because the // account name is already contained in the account column. if (d->m_account.accountType() == eMyMoney::Account::Type::Income || d->m_account.accountType() == eMyMoney::Account::Type::Expense) { if (numRows > 2 && d->m_transaction.splitCount() == 2) numRows = 1; } } } return numRows; } int StdTransaction::numRowsRegister() const { return RegisterItem::numRowsRegister(); } TransactionEditor* StdTransaction::createEditor(TransactionEditorContainer* regForm, const KMyMoneyRegister::SelectedTransactions& list, const QDate& lastPostDate) { #ifndef KMM_DESIGNER Q_D(StdTransaction); d->m_inRegisterEdit = regForm == d->m_parent; return new StdTransactionEditor(regForm, this, list, lastPostDate); #else return NULL; #endif } diff --git a/kmymoney/widgets/stdtransactionmatched.cpp b/kmymoney/widgets/stdtransactionmatched.cpp index b5505e386..b32b59fa5 100644 --- a/kmymoney/widgets/stdtransactionmatched.cpp +++ b/kmymoney/widgets/stdtransactionmatched.cpp @@ -1,204 +1,205 @@ /*************************************************************************** stdtransactionmatched.cpp ------------------- begin : Sat May 11 2008 copyright : (C) 2008 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 "stdtransactionmatched.h" #include "stdtransaction_p.h" // ---------------------------------------------------------------------------- // QT Includes #include #include // ---------------------------------------------------------------------------- // KDE Includes #include // ---------------------------------------------------------------------------- // Project Includes #include "kmymoneyglobalsettings.h" #include "mymoneyaccount.h" #include "mymoneymoney.h" #include "mymoneysplit.h" #include "mymoneytransaction.h" +#include "mymoneyexception.h" #include "widgetenums.h" using namespace KMyMoneyRegister; using namespace KMyMoneyTransactionForm; StdTransactionMatched::StdTransactionMatched(Register *parent, const MyMoneyTransaction& transaction, const MyMoneySplit& split, int uniqueId) : StdTransaction(parent, transaction, split, uniqueId) { // setup initial size setNumRowsRegister(numRowsRegister(KMyMoneyGlobalSettings::showRegisterDetailed())); } StdTransactionMatched::~StdTransactionMatched() { } const char* StdTransactionMatched::className() { return "StdTransactionMatched"; } bool StdTransactionMatched::paintRegisterCellSetup(QPainter *painter, QStyleOptionViewItem &option, const QModelIndex &index) { auto rc = Transaction::paintRegisterCellSetup(painter, option, index); // if not selected paint in matched background color if (!isSelected()) { option.palette.setColor(QPalette::Base, KMyMoneyGlobalSettings::schemeColor(SchemeColor::TransactionMatched)); option.palette.setColor(QPalette::AlternateBase, KMyMoneyGlobalSettings::schemeColor(SchemeColor::TransactionMatched)); } //TODO: the first line needs to be painted across all columns return rc; } void StdTransactionMatched::registerCellText(QString& txt, Qt::Alignment& align, int row, int col, QPainter* painter) { Q_D(StdTransaction); // run through the standard StdTransaction::registerCellText(txt, align, row, col, painter); // we only cover the additional rows if (row >= RegisterItem::numRowsRegister() - m_additionalRows) { // make row relative to the last three rows row += m_additionalRows - RegisterItem::numRowsRegister(); // remove anything that had been added by the standard method txt = QString(); // and we draw this information in italics if (painter) { QFont font = painter->font(); font.setItalic(true); painter->setFont(font); } MyMoneyTransaction matchedTransaction = d->m_split.matchedTransaction(); MyMoneySplit matchedSplit; try { matchedSplit = matchedTransaction.splitById(d->m_split.value("kmm-match-split")); } catch (const MyMoneyException &) { } MyMoneyMoney importedValue; foreach (const auto split, matchedTransaction.splits()) { if (split.accountId() == d->m_account.id()) { importedValue += split.shares(); } } QDate postDate; QString memo; switch (row) { case 0: if (painter && col == (int)eWidgets::eTransaction::Column::Detail) txt = QString(" ") + i18n("KMyMoney has matched the two selected transactions (result above)"); // return true for the first visible column only break; case 1: switch (col) { case (int)eWidgets::eTransaction::Column::Date: align |= Qt::AlignLeft; txt = i18n("Bank entry:"); break; case (int)eWidgets::eTransaction::Column::Detail: align |= Qt::AlignLeft; memo = matchedTransaction.memo(); memo.replace("\n\n", "\n"); memo.replace('\n', ", "); txt = QString("%1 %2").arg(matchedTransaction.postDate().toString(Qt::ISODate)).arg(memo); break; case (int)eWidgets::eTransaction::Column::Payment: align |= Qt::AlignRight; if (importedValue.isNegative()) { txt = (-importedValue).formatMoney(d->m_account.fraction()); } break; case (int)eWidgets::eTransaction::Column::Deposit: align |= Qt::AlignRight; if (!importedValue.isNegative()) { txt = importedValue.formatMoney(d->m_account.fraction()); } break; } break; case 2: switch (col) { case (int)eWidgets::eTransaction::Column::Date: align |= Qt::AlignLeft; txt = i18n("Your entry:"); break; case (int)eWidgets::eTransaction::Column::Detail: align |= Qt::AlignLeft; postDate = d->m_transaction.postDate(); if (!d->m_split.value("kmm-orig-postdate").isEmpty()) { postDate = QDate::fromString(d->m_split.value("kmm-orig-postdate"), Qt::ISODate); } memo = d->m_split.memo(); if (!matchedSplit.memo().isEmpty() && memo != matchedSplit.memo()) { int pos = memo.lastIndexOf(matchedSplit.memo()); if (pos != -1) { memo = memo.left(pos); // replace all new line characters because we only have one line available for the displayed data } } memo.replace("\n\n", "\n"); memo.replace('\n', ", "); txt = QString("%1 %2").arg(postDate.toString(Qt::ISODate)).arg(memo); break; case (int)eWidgets::eTransaction::Column::Payment: align |= Qt::AlignRight; if (d->m_split.value().isNegative()) { txt = (-d->m_split.value(d->m_transaction.commodity(), d->m_splitCurrencyId)).formatMoney(d->m_account.fraction()); } break; case (int)eWidgets::eTransaction::Column::Deposit: align |= Qt::AlignRight; if (!d->m_split.value().isNegative()) { txt = d->m_split.value(d->m_transaction.commodity(), d->m_splitCurrencyId).formatMoney(d->m_account.fraction()); } break; } break; } } } int StdTransactionMatched::numRowsRegister(bool expanded) const { return StdTransaction::numRowsRegister(expanded) + m_additionalRows; } int StdTransactionMatched::numRowsRegister() const { return StdTransaction::numRowsRegister(); } diff --git a/kmymoney/wizards/endingbalancedlg/kendingbalancedlg.cpp b/kmymoney/wizards/endingbalancedlg/kendingbalancedlg.cpp index db3bb968a..49ec39fee 100644 --- a/kmymoney/wizards/endingbalancedlg/kendingbalancedlg.cpp +++ b/kmymoney/wizards/endingbalancedlg/kendingbalancedlg.cpp @@ -1,427 +1,428 @@ /*************************************************************************** kendingbalancedlg.cpp ------------------- copyright : (C) 2000,2003 by Michael Edwardes, Thomas Baumgart email : mte@users.sourceforge.net 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 "kendingbalancedlg.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include // ---------------------------------------------------------------------------- // Project Includes #include "ui_kendingbalancedlg.h" #include "ui_checkingstatementinfowizardpage.h" #include "ui_interestchargecheckingswizardpage.h" #include "mymoneymoney.h" +#include "mymoneyexception.h" #include "mymoneyutils.h" #include "mymoneytracer.h" #include "kmymoneyedit.h" #include "mymoneysplit.h" #include "mymoneyfile.h" #include "mymoneyinstitution.h" #include "mymoneyaccount.h" #include "mymoneypayee.h" #include "mymoneysecurity.h" #include "mymoneytransaction.h" #include "mymoneytransactionfilter.h" #include "kmymoneycategory.h" #include "kmymoneyaccountselector.h" #include "kmymoneyutils.h" #include "kcurrencycalculator.h" #include "kmymoneysettings.h" #include "mymoneyenums.h" class KEndingBalanceDlgPrivate { Q_DISABLE_COPY(KEndingBalanceDlgPrivate) public: KEndingBalanceDlgPrivate(int numPages) : ui(new Ui::KEndingBalanceDlg), m_pages(numPages, true) { } ~KEndingBalanceDlgPrivate() { delete ui; } Ui::KEndingBalanceDlg *ui; MyMoneyTransaction m_tInterest; MyMoneyTransaction m_tCharges; MyMoneyAccount m_account; QMap m_helpAnchor; QBitArray m_pages; }; KEndingBalanceDlg::KEndingBalanceDlg(const MyMoneyAccount& account, QWidget *parent) : QWizard(parent), d_ptr(new KEndingBalanceDlgPrivate(Page_InterestChargeCheckings + 1)) { Q_D(KEndingBalanceDlg); setModal(true); QString value; MyMoneyMoney endBalance, startBalance; d->m_account = account; MyMoneySecurity currency = MyMoneyFile::instance()->security(account.currencyId()); //FIXME: port d->ui->m_statementInfoPageCheckings->ui->m_enterInformationLabel->setText(QString("") + i18n("Please enter the following fields with the information as you find them on your statement. Make sure to enter all values in %1.", currency.name()) + QString("")); // If the previous reconciliation was postponed, // we show a different first page value = account.value("lastReconciledBalance"); if (value.isEmpty()) { // if the last statement has been entered long enough ago (more than one month), // then take the last statement date and add one month and use that as statement // date. QDate lastStatementDate = account.lastReconciliationDate(); if (lastStatementDate.addMonths(1) < QDate::currentDate()) { setField("statementDate", lastStatementDate.addMonths(1)); } slotUpdateBalances(); d->m_pages.clearBit(Page_PreviousPostpone); } else { d->m_pages.clearBit(Page_CheckingStart); d->m_pages.clearBit(Page_InterestChargeCheckings); //removePage(d->ui->m_interestChargeCheckings); // make sure, we show the correct start page setStartId(Page_PreviousPostpone); MyMoneyMoney factor(1, 1); if (d->m_account.accountGroup() == eMyMoney::Account::Type::Liability) factor = -factor; startBalance = MyMoneyMoney(value) * factor; value = account.value("statementBalance"); endBalance = MyMoneyMoney(value) * factor; //FIXME: port d->ui->m_statementInfoPageCheckings->ui->m_previousBalance->setValue(startBalance); d->ui->m_statementInfoPageCheckings->ui->m_endingBalance->setValue(endBalance); } // We don't need to add the default into the list (see ::help() why) // m_helpAnchor[m_startPageCheckings] = QString(QString()); d->m_helpAnchor[d->ui->m_interestChargeCheckings] = QString("details.reconcile.wizard.interest"); d->m_helpAnchor[d->ui->m_statementInfoPageCheckings] = QString("details.reconcile.wizard.statement"); value = account.value("statementDate"); if (!value.isEmpty()) setField("statementDate", QDate::fromString(value, Qt::ISODate)); //FIXME: port d->ui->m_statementInfoPageCheckings->ui->m_lastStatementDate->setText(QString()); if (account.lastReconciliationDate().isValid()) { d->ui->m_statementInfoPageCheckings->ui->m_lastStatementDate->setText(i18n("Last reconciled statement: %1", QLocale().toString(account.lastReconciliationDate()))); } // connect the signals with the slots connect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, this, &KEndingBalanceDlg::slotReloadEditWidgets); connect(d->ui->m_statementInfoPageCheckings->ui->m_statementDate, &KMyMoneyDateInput::dateChanged, this, &KEndingBalanceDlg::slotUpdateBalances); connect(d->ui->m_interestChargeCheckings->ui->m_interestCategoryEdit, &KMyMoneyCombo::createItem, this, &KEndingBalanceDlg::slotCreateInterestCategory); connect(d->ui->m_interestChargeCheckings->ui->m_chargesCategoryEdit, &KMyMoneyCombo::createItem, this, &KEndingBalanceDlg::slotCreateChargesCategory); connect(d->ui->m_interestChargeCheckings->ui->m_payeeEdit, &KMyMoneyMVCCombo::createItem, this, &KEndingBalanceDlg::createPayee); KMyMoneyMVCCombo::setSubstringSearchForChildren(d->ui->m_interestChargeCheckings, !KMyMoneySettings::stringMatchFromStart()); slotReloadEditWidgets(); // preset payee if possible try { // if we find a payee with the same name as the institution, // than this is what we use as payee. if (!d->m_account.institutionId().isEmpty()) { MyMoneyInstitution inst = MyMoneyFile::instance()->institution(d->m_account.institutionId()); MyMoneyPayee payee = MyMoneyFile::instance()->payeeByName(inst.name()); setField("payeeEdit", payee.id()); } } catch (const MyMoneyException &) { } KMyMoneyUtils::updateWizardButtons(this); // setup different text and icon on finish button setButtonText(QWizard::FinishButton, KStandardGuiItem::cont().text()); button(QWizard::FinishButton)->setIcon(KStandardGuiItem::cont().icon()); } KEndingBalanceDlg::~KEndingBalanceDlg() { Q_D(KEndingBalanceDlg); delete d; } void KEndingBalanceDlg::slotUpdateBalances() { Q_D(KEndingBalanceDlg); MYMONEYTRACER(tracer); // determine the beginning balance and ending balance based on the following // forumulas: // // end balance = current balance - sum(all non cleared transactions) // - sum(all cleared transactions posted // after statement date) // start balance = end balance - sum(all cleared transactions // up to statement date) MyMoneyTransactionFilter filter(d->m_account.id()); filter.addState((int)eMyMoney::TransactionFilter::State::NotReconciled); filter.setReportAllSplits(true); QList > transactionList; QList >::const_iterator it; // retrieve the list from the engine MyMoneyFile::instance()->transactionList(transactionList, filter); //first retrieve the oldest not reconciled transaction QDate oldestTransactionDate; it = transactionList.constBegin(); if (it != transactionList.constEnd()) { oldestTransactionDate = (*it).first.postDate(); d->ui->m_statementInfoPageCheckings->ui->m_oldestTransactionDate->setText(i18n("Oldest unmarked transaction: %1", QLocale().toString(oldestTransactionDate))); } filter.addState((int)eMyMoney::TransactionFilter::State::Cleared); // retrieve the list from the engine to calculate the starting and ending balance MyMoneyFile::instance()->transactionList(transactionList, filter); MyMoneyMoney balance = MyMoneyFile::instance()->balance(d->m_account.id()); MyMoneyMoney factor(1, 1); if (d->m_account.accountGroup() == eMyMoney::Account::Type::Liability) factor = -factor; MyMoneyMoney endBalance, startBalance; balance = balance * factor; endBalance = startBalance = balance; tracer.printf("total balance = %s", qPrintable(endBalance.formatMoney(QString(), 2))); for (it = transactionList.constBegin(); it != transactionList.constEnd(); ++it) { const MyMoneySplit& split = (*it).second; balance -= split.shares() * factor; if ((*it).first.postDate() > field("statementDate").toDate()) { tracer.printf("Reducing balances by %s because postdate of %s/%s(%s) is past statement date", qPrintable((split.shares() * factor).formatMoney(QString(), 2)), qPrintable((*it).first.id()), qPrintable(split.id()), qPrintable((*it).first.postDate().toString(Qt::ISODate))); endBalance -= split.shares() * factor; startBalance -= split.shares() * factor; } else { switch (split.reconcileFlag()) { case eMyMoney::Split::State::NotReconciled: tracer.printf("Reducing balances by %s because %s/%s(%s) is not reconciled", qPrintable((split.shares() * factor).formatMoney(QString(), 2)), qPrintable((*it).first.id()), qPrintable(split.id()), qPrintable((*it).first.postDate().toString(Qt::ISODate))); endBalance -= split.shares() * factor; startBalance -= split.shares() * factor; break; case eMyMoney::Split::State::Cleared: tracer.printf("Reducing start balance by %s because %s/%s(%s) is cleared", qPrintable((split.shares() * factor).formatMoney(QString(), 2)), qPrintable((*it).first.id()), qPrintable(split.id()), qPrintable((*it).first.postDate().toString(Qt::ISODate))); startBalance -= split.shares() * factor; break; default: break; } } } //FIXME: port d->ui->m_statementInfoPageCheckings->ui->m_previousBalance->setValue(startBalance); d->ui->m_statementInfoPageCheckings->ui->m_endingBalance->setValue(endBalance); tracer.printf("total balance = %s", qPrintable(endBalance.formatMoney(QString(), 2))); tracer.printf("start balance = %s", qPrintable(startBalance.formatMoney(QString(), 2))); setField("interestDateEdit", field("statementDate").toDate()); setField("chargesDateEdit", field("statementDate").toDate()); } void KEndingBalanceDlg::accept() { Q_D(KEndingBalanceDlg); if ((!field("interestEditValid").toBool() || createTransaction(d->m_tInterest, -1, field("interestEdit").value(), field("interestCategoryEdit").toString(), field("interestDateEdit").toDate())) && (!field("chargesEditValid").toBool() || createTransaction(d->m_tCharges, 1, field("chargesEdit").value(), field("chargesCategoryEdit").toString(), field("chargesDateEdit").toDate()))) QWizard::accept(); } void KEndingBalanceDlg::slotCreateInterestCategory(const QString& txt, QString& id) { createCategory(txt, id, MyMoneyFile::instance()->income()); } void KEndingBalanceDlg::slotCreateChargesCategory(const QString& txt, QString& id) { createCategory(txt, id, MyMoneyFile::instance()->expense()); } void KEndingBalanceDlg::createCategory(const QString& txt, QString& id, const MyMoneyAccount& parent) { MyMoneyAccount acc; acc.setName(txt); emit createCategory(acc, parent); id = acc.id(); } MyMoneyMoney KEndingBalanceDlg::endingBalance() const { Q_D(const KEndingBalanceDlg); return adjustedReturnValue(d->ui->m_statementInfoPageCheckings->ui->m_endingBalance->value()); } MyMoneyMoney KEndingBalanceDlg::previousBalance() const { Q_D(const KEndingBalanceDlg); return adjustedReturnValue(d->ui->m_statementInfoPageCheckings->ui->m_previousBalance->value()); } QDate KEndingBalanceDlg::statementDate() const { return field("statementDate").toDate(); } MyMoneyMoney KEndingBalanceDlg::adjustedReturnValue(const MyMoneyMoney& v) const { Q_D(const KEndingBalanceDlg); return d->m_account.accountGroup() == eMyMoney::Account::Type::Liability ? -v : v; } void KEndingBalanceDlg::slotReloadEditWidgets() { Q_D(KEndingBalanceDlg); QString payeeId, interestId, chargesId; // keep current selected items payeeId = field("payeeEdit").toString(); interestId = field("interestCategoryEdit").toString(); chargesId = field("chargesCategoryEdit").toString(); // load the payee and category widgets with data from the engine //FIXME: port d->ui->m_interestChargeCheckings->ui->m_payeeEdit->loadPayees(MyMoneyFile::instance()->payeeList()); // a user request to show all categories in both selectors due to a valid use case. AccountSet aSet; aSet.addAccountGroup(eMyMoney::Account::Type::Expense); aSet.addAccountGroup(eMyMoney::Account::Type::Income); //FIXME: port aSet.load(d->ui->m_interestChargeCheckings->ui->m_interestCategoryEdit->selector()); aSet.load(d->ui->m_interestChargeCheckings->ui->m_chargesCategoryEdit->selector()); // reselect currently selected items if (!payeeId.isEmpty()) setField("payeeEdit", payeeId); if (!interestId.isEmpty()) setField("interestCategoryEdit", interestId); if (!chargesId.isEmpty()) setField("chargesCategoryEdit", chargesId); } MyMoneyTransaction KEndingBalanceDlg::interestTransaction() { Q_D(KEndingBalanceDlg); return d->m_tInterest; } MyMoneyTransaction KEndingBalanceDlg::chargeTransaction() { Q_D(KEndingBalanceDlg); return d->m_tCharges; } bool KEndingBalanceDlg::createTransaction(MyMoneyTransaction &t, const int sign, const MyMoneyMoney& amount, const QString& category, const QDate& date) { Q_D(KEndingBalanceDlg); t = MyMoneyTransaction(); if (category.isEmpty() || !date.isValid()) return true; MyMoneySplit s1, s2; MyMoneyMoney val = amount * MyMoneyMoney(sign, 1); try { t.setPostDate(date); t.setCommodity(d->m_account.currencyId()); s1.setPayeeId(field("payeeEdit").toString()); s1.setReconcileFlag(eMyMoney::Split::State::Cleared); s1.setAccountId(d->m_account.id()); s1.setValue(-val); s1.setShares(-val); s2 = s1; s2.setAccountId(category); s2.setValue(val); t.addSplit(s1); t.addSplit(s2); QMap priceInfo; // just empty MyMoneyMoney shares; if (!KCurrencyCalculator::setupSplitPrice(shares, t, s2, priceInfo, this)) { t = MyMoneyTransaction(); return false; } s2.setShares(shares); t.modifySplit(s2); } catch (const MyMoneyException &e) { qDebug("%s", qPrintable(e.what())); t = MyMoneyTransaction(); return false; } return true; } void KEndingBalanceDlg::help() { Q_D(KEndingBalanceDlg); QString anchor = d->m_helpAnchor[currentPage()]; if (anchor.isEmpty()) anchor = QString("details.reconcile.whatis"); KHelpClient::invokeHelp(anchor); } int KEndingBalanceDlg::nextId() const { Q_D(const KEndingBalanceDlg); // Starting from the current page, look for the first enabled page // and return that value // If the end of the list is encountered first, then return -1. for (int i = currentId() + 1; i < d->m_pages.size() && i < pageIds().size(); ++i) { if (d->m_pages.testBit(i)) return pageIds()[i]; } return -1; } diff --git a/kmymoney/wizards/newinvestmentwizard/knewinvestmentwizard.cpp b/kmymoney/wizards/newinvestmentwizard/knewinvestmentwizard.cpp index 86731157c..e7a8f0c94 100644 --- a/kmymoney/wizards/newinvestmentwizard/knewinvestmentwizard.cpp +++ b/kmymoney/wizards/newinvestmentwizard/knewinvestmentwizard.cpp @@ -1,284 +1,285 @@ /*************************************************************************** knewinvestmentwizard - description ------------------- begin : Sat Dec 4 2004 copyright : (C) 2004 by Thomas Baumgart email : kmymoney-devel@kde.org (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 "knewinvestmentwizard.h" // ---------------------------------------------------------------------------- // QT Includes #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "ui_knewinvestmentwizard.h" #include "mymoneyaccount.h" #include "mymoneysecurity.h" #include "mymoneyfile.h" #include "webpricequote.h" #include "kmymoneyutils.h" +#include "mymoneyexception.h" class KNewInvestmentWizardPrivate { Q_DISABLE_COPY(KNewInvestmentWizardPrivate) Q_DECLARE_PUBLIC(KNewInvestmentWizard) public: explicit KNewInvestmentWizardPrivate(KNewInvestmentWizard *qq) : q_ptr(qq), ui(new Ui::KNewInvestmentWizard) { } ~KNewInvestmentWizardPrivate() { delete ui; } void init1() { Q_Q(KNewInvestmentWizard); ui->m_onlineUpdatePage->slotSourceChanged(false); // make sure, the back button does not clear fields q->setOption(QWizard::IndependentPages, true); // enable the help button q->setOption(q->HaveHelpButton, true); q->connect(q, &KNewInvestmentWizard::helpRequested, q, &KNewInvestmentWizard::slotHelp); m_createAccount = true; // Update label in case of edit if (!m_account.id().isEmpty()) { ui->m_investmentTypePage->setIntroLabelText(i18n("This wizard allows you to modify the selected investment.")); } if (!m_security.id().isEmpty()) { ui->m_investmentTypePage->setIntroLabelText(i18n("This wizard allows you to modify the selected security.")); } KMyMoneyUtils::updateWizardButtons(q); } void init2() { ui->m_investmentTypePage->init2(m_security); ui->m_investmentDetailsPage->init2(m_security); ui->m_onlineUpdatePage->init2(m_security); ui->m_onlineUpdatePage->slotCheckPage(m_security.value("kmm-online-source")); } KNewInvestmentWizard *q_ptr; Ui::KNewInvestmentWizard *ui; MyMoneyAccount m_account; MyMoneySecurity m_security; bool m_createAccount; }; KNewInvestmentWizard::KNewInvestmentWizard(QWidget *parent) : QWizard(parent), d_ptr(new KNewInvestmentWizardPrivate(this)) { Q_D(KNewInvestmentWizard); d->ui->setupUi(this); d->init1(); d->ui->m_onlineUpdatePage->slotCheckPage(QString()); d->ui->m_investmentDetailsPage->setupInvestmentSymbol(); connect(d->ui->m_investmentDetailsPage, &KInvestmentDetailsWizardPage::checkForExistingSymbol, this, &KNewInvestmentWizard::slotCheckForExistingSymbol); } KNewInvestmentWizard::KNewInvestmentWizard(const MyMoneyAccount& acc, QWidget *parent) : QWizard(parent), d_ptr(new KNewInvestmentWizardPrivate(this)) { Q_D(KNewInvestmentWizard); d->ui->setupUi(this); d->m_account = acc; setWindowTitle(i18n("Investment detail wizard")); d->init1(); // load the widgets with the data setName(d->m_account.name()); d->m_security = MyMoneyFile::instance()->security(d->m_account.currencyId()); d->init2(); int priceMode = 0; if (!d->m_account.value("priceMode").isEmpty()) priceMode = d->m_account.value("priceMode").toInt(); d->ui->m_investmentDetailsPage->setCurrentPriceMode(priceMode); } KNewInvestmentWizard::KNewInvestmentWizard(const MyMoneySecurity& security, QWidget *parent) : QWizard(parent), d_ptr(new KNewInvestmentWizardPrivate(this)) { Q_D(KNewInvestmentWizard); d->ui->setupUi(this); d->m_security = security; setWindowTitle(i18n("Security detail wizard")); d->init1(); d->m_createAccount = false; // load the widgets with the data setName(security.name()); d->init2(); // no chance to change the price mode here d->ui->m_investmentDetailsPage->setCurrentPriceMode(0); d->ui->m_investmentDetailsPage->setPriceModeEnabled(false); } KNewInvestmentWizard::~KNewInvestmentWizard() { } void KNewInvestmentWizard::setName(const QString& name) { Q_D(KNewInvestmentWizard); d->ui->m_investmentDetailsPage->setName(name); } void KNewInvestmentWizard::slotCheckForExistingSymbol(const QString& symbol) { Q_D(KNewInvestmentWizard); Q_UNUSED(symbol); if (field("investmentName").toString().isEmpty()) { QList list = MyMoneyFile::instance()->securityList(); auto type = static_cast(field("securityType").toInt()); foreach (const MyMoneySecurity& it_s, list) { if (it_s.securityType() == type && it_s.tradingSymbol() == field("investmentSymbol").toString()) { d->m_security = MyMoneySecurity(); if (KMessageBox::questionYesNo(this, i18n("The selected symbol is already on file. Do you want to reuse the existing security?"), i18n("Security found")) == KMessageBox::Yes) { d->m_security = it_s; d->init2(); d->ui->m_investmentDetailsPage->loadName(d->m_security.name()); } break; } } } } void KNewInvestmentWizard::slotHelp() { KHelpClient::invokeHelp("details.investments.newinvestmentwizard"); } void KNewInvestmentWizard::createObjects(const QString& parentId) { Q_D(KNewInvestmentWizard); auto file = MyMoneyFile::instance(); auto type = static_cast(field("securityType").toInt()); auto roundingMethod = static_cast(field("roundingMethod").toInt()); MyMoneyFileTransaction ft; try { // update all relevant attributes only, if we create a stock // account and the security is unknown or we modifiy the security MyMoneySecurity newSecurity(d->m_security); newSecurity.setName(field("investmentName").toString()); newSecurity.setTradingSymbol(field("investmentSymbol").toString()); newSecurity.setTradingMarket(field("tradingMarket").toString()); newSecurity.setSmallestAccountFraction(field("fraction").value().formatMoney("", 0, false).toUInt()); newSecurity.setPricePrecision(MyMoneyMoney(field("pricePrecision").toUInt()).formatMoney("", 0, false).toUInt()); newSecurity.setTradingCurrency(field("tradingCurrencyEdit").value().id()); newSecurity.setSecurityType(type); newSecurity.setRoundingMethod(roundingMethod); newSecurity.deletePair("kmm-online-source"); newSecurity.deletePair("kmm-online-quote-system"); newSecurity.deletePair("kmm-online-factor"); newSecurity.deletePair("kmm-security-id"); if (!field("onlineSourceCombo").toString().isEmpty()) { if (field("useFinanceQuote").toBool()) { FinanceQuoteProcess p; newSecurity.setValue("kmm-online-quote-system", "Finance::Quote"); newSecurity.setValue("kmm-online-source", p.crypticName(field("onlineSourceCombo").toString())); } else { newSecurity.setValue("kmm-online-source", field("onlineSourceCombo").toString()); } } if (d->ui->m_onlineUpdatePage->isOnlineFactorEnabled() && (field("onlineFactor").value() != MyMoneyMoney::ONE)) newSecurity.setValue("kmm-online-factor", field("onlineFactor").value().toString()); if (!field("investmentIdentification").toString().isEmpty()) newSecurity.setValue("kmm-security-id", field("investmentIdentification").toString()); if (d->m_security.id().isEmpty() || newSecurity != d->m_security) { d->m_security = newSecurity; // add or update it if (d->m_security.id().isEmpty()) { file->addSecurity(d->m_security); } else { file->modifySecurity(d->m_security); } } if (d->m_createAccount) { // now that the security exists, we can add the account to store it d->m_account.setName(field("investmentName").toString()); if (d->m_account.accountType() == eMyMoney::Account::Type::Unknown) d->m_account.setAccountType(eMyMoney::Account::Type::Stock); d->m_account.setCurrencyId(d->m_security.id()); switch (d->ui->m_investmentDetailsPage->priceMode()) { case 0: d->m_account.deletePair("priceMode"); break; case 1: case 2: d->m_account.setValue("priceMode", QString("%1").arg(d->ui->m_investmentDetailsPage->priceMode())); break; } // update account's fraction in case its security fraction has changed // otherwise KMM restart is required because this won't happen automatically d->m_account.fraction(d->m_security); if (d->m_account.id().isEmpty()) { MyMoneyAccount parent = file->account(parentId); file->addAccount(d->m_account, parent); } else file->modifyAccount(d->m_account); } ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(0, i18n("Unable to create all objects for the investment"), QString("%1 caugt in %2:%3").arg(e.what()).arg(e.file()).arg(e.line())); } } MyMoneyAccount KNewInvestmentWizard::account() const { Q_D(const KNewInvestmentWizard); return d->m_account; } diff --git a/kmymoney/wizards/newloanwizard/knewloanwizard_p.h b/kmymoney/wizards/newloanwizard/knewloanwizard_p.h index 70e046986..30935272b 100644 --- a/kmymoney/wizards/newloanwizard/knewloanwizard_p.h +++ b/kmymoney/wizards/newloanwizard/knewloanwizard_p.h @@ -1,541 +1,542 @@ /*************************************************************************** knewloanwizard_p.cpp - description ------------------- copyright : (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. * * * ***************************************************************************/ #ifndef KNEWLOANWIZARD_P_H #define KNEWLOANWIZARD_P_H #include "knewloanwizard.h" // ---------------------------------------------------------------------------- // QT Includes #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include // ---------------------------------------------------------------------------- // Project Includes #include "ui_knewloanwizard.h" #include "ui_namewizardpage.h" #include "ui_firstpaymentwizardpage.h" #include "ui_loanamountwizardpage.h" #include "ui_interestwizardpage.h" #include "ui_paymenteditwizardpage.h" #include "ui_finalpaymentwizardpage.h" #include "ui_interestcategorywizardpage.h" #include "ui_assetaccountwizardpage.h" #include "ui_schedulewizardpage.h" #include "ui_paymentwizardpage.h" #include "kmymoneyutils.h" #include "kmymoneysettings.h" #include "mymoneyfinancialcalculator.h" #include "mymoneyfile.h" +#include "mymoneyexception.h" #include "mymoneysecurity.h" #include "mymoneyaccountloan.h" #include "mymoneyschedule.h" #include "mymoneysplit.h" #include "mymoneytransaction.h" #include "mymoneyenums.h" namespace Ui { class KNewLoanWizard; } class KNewLoanWizard; class KNewLoanWizardPrivate { Q_DISABLE_COPY(KNewLoanWizardPrivate) Q_DECLARE_PUBLIC(KNewLoanWizard) public: explicit KNewLoanWizardPrivate(KNewLoanWizard *qq) : q_ptr(qq), ui(new Ui::KNewLoanWizard) { } ~KNewLoanWizardPrivate() { delete ui; } void init() { Q_Q(KNewLoanWizard); ui->setupUi(q); m_pages = QBitArray(KNewLoanWizard::Page_Summary + 1, true); q->setModal(true); KMyMoneyMVCCombo::setSubstringSearchForChildren(ui->m_namePage, !KMyMoneySettings::stringMatchFromStart()); // make sure, the back button does not clear fields q->setOption(QWizard::IndependentPages, true); // connect(m_payeeEdit, SIGNAL(newPayee(QString)), this, SLOT(slotNewPayee(QString))); q->connect(ui->m_namePage->ui->m_payeeEdit, &KMyMoneyMVCCombo::createItem, q, &KNewLoanWizard::createPayee); q->connect(ui->m_additionalFeesPage, &AdditionalFeesWizardPage::newCategory, q, &KNewLoanWizard::newCategory); q->connect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, q, &KNewLoanWizard::slotReloadEditWidgets); resetCalculator(); q->slotReloadEditWidgets(); // As default we assume a liability loan, with fixed interest rate, // with a first payment due on the 30th of this month. All payments // should be recorded and none have been made so far. //FIXME: port ui->m_firstPaymentPage->ui->m_firstDueDateEdit->loadDate(QDate(QDate::currentDate().year(), QDate::currentDate().month(), 30)); // FIXME: we currently only support interest calculation on reception m_pages.clearBit(KNewLoanWizard::Page_InterestCalculation); // turn off all pages that are contained here for derived classes m_pages.clearBit(KNewLoanWizard::Page_EditIntro); m_pages.clearBit(KNewLoanWizard::Page_EditSelection); m_pages.clearBit(KNewLoanWizard::Page_EffectiveDate); m_pages.clearBit(KNewLoanWizard::Page_PaymentEdit); m_pages.clearBit(KNewLoanWizard::Page_InterestEdit); m_pages.clearBit(KNewLoanWizard::Page_SummaryEdit); // for now, we don't have online help :-( q->setOption(QWizard::HaveHelpButton, false); // setup a phony transaction for additional fee processing m_account = MyMoneyAccount("Phony-ID", MyMoneyAccount()); m_split.setAccountId(m_account.id()); m_split.setValue(MyMoneyMoney()); m_transaction.addSplit(m_split); KMyMoneyUtils::updateWizardButtons(q); } void resetCalculator() { Q_Q(KNewLoanWizard); ui->m_loanAmountPage->resetCalculator(); ui->m_interestPage->resetCalculator(); ui->m_durationPage->resetCalculator(); ui->m_paymentPage->resetCalculator(); ui->m_finalPaymentPage->resetCalculator(); q->setField("additionalCost", MyMoneyMoney().formatMoney(m_account.fraction(MyMoneyFile::instance()->security(m_account.currencyId())))); } void updateLoanAmount() { Q_Q(KNewLoanWizard); QString txt; //FIXME: port if (! q->field("loanAmountEditValid").toBool()) { txt = QString("<") + i18n("calculate") + QString(">"); } else { txt = q->field("loanAmountEdit").value().formatMoney(m_account.fraction(MyMoneyFile::instance()->security(m_account.currencyId()))); } q->setField("loanAmount1", txt); q->setField("loanAmount2", txt); q->setField("loanAmount3", txt); q->setField("loanAmount4", txt); q->setField("loanAmount5", txt); } void updateInterestRate() { Q_Q(KNewLoanWizard); QString txt; //FIXME: port if (! q->field("interestRateEditValid").toBool()) { txt = QString("<") + i18n("calculate") + QString(">"); } else { txt = q->field("interestRateEdit").value().formatMoney(QString(), 3) + QString("%"); } q->setField("interestRate1", txt); q->setField("interestRate2", txt); q->setField("interestRate3", txt); q->setField("interestRate4", txt); q->setField("interestRate5", txt); } void updateDuration() { Q_Q(KNewLoanWizard); QString txt; //FIXME: port if (q->field("durationValueEdit").toInt() == 0) { txt = QString("<") + i18n("calculate") + QString(">"); } else { txt = QString().sprintf("%d ", q->field("durationValueEdit").toInt()) + q->field("durationUnitEdit").toString(); } q->setField("duration1", txt); q->setField("duration2", txt); q->setField("duration3", txt); q->setField("duration4", txt); q->setField("duration5", txt); } void updatePayment() { Q_Q(KNewLoanWizard); QString txt; //FIXME: port if (! q->field("paymentEditValid").toBool()) { txt = QString("<") + i18n("calculate") + QString(">"); } else { txt = q->field("paymentEdit").value().formatMoney(m_account.fraction(MyMoneyFile::instance()->security(m_account.currencyId()))); } q->setField("payment1", txt); q->setField("payment2", txt); q->setField("payment3", txt); q->setField("payment4", txt); q->setField("payment5", txt); q->setField("basePayment", txt); } void updateFinalPayment() { Q_Q(KNewLoanWizard); QString txt; //FIXME: port if (! q->field("finalPaymentEditValid").toBool()) { txt = QString("<") + i18n("calculate") + QString(">"); } else { txt = q->field("finalPaymentEdit").value().formatMoney(m_account.fraction(MyMoneyFile::instance()->security(m_account.currencyId()))); } q->setField("balloon1", txt); q->setField("balloon2", txt); q->setField("balloon3", txt); q->setField("balloon4", txt); q->setField("balloon5", txt); } void updateLoanInfo() { Q_Q(KNewLoanWizard); updateLoanAmount(); updateInterestRate(); updateDuration(); updatePayment(); updateFinalPayment(); ui->m_additionalFeesPage->updatePeriodicPayment(m_account); QString txt; int fraction = m_account.fraction(MyMoneyFile::instance()->security(m_account.currencyId())); q->setField("loanAmount6", q->field("loanAmountEdit").value().formatMoney(fraction)); q->setField("interestRate6", QString(q->field("interestRateEdit").value().formatMoney("", 3) + QString("%"))); txt = QString().sprintf("%d ", q->field("durationValueEdit").toInt()) + q->field("durationUnitEdit").toString(); q->setField("duration6", txt); q->setField("payment6", q->field("paymentEdit").value().formatMoney(fraction)); q->setField("balloon6", q->field("finalPaymentEdit").value().formatMoney(fraction)); } int calculateLoan() { Q_Q(KNewLoanWizard); MyMoneyFinancialCalculator calc; double val; int PF; QString result; // FIXME: for now, we only support interest calculation at the end of the period calc.setBep(); // FIXME: for now, we only support periodic compounding calc.setDisc(); PF = MyMoneySchedule::eventsPerYear(eMyMoney::Schedule::Occurrence(q->field("paymentFrequencyUnitEdit").toInt())); if (PF == 0) return 0; calc.setPF(PF); // FIXME: for now we only support compounding frequency == payment frequency calc.setCF(PF); if (q->field("loanAmountEditValid").toBool()) { val = q->field("loanAmountEdit").value().abs().toDouble(); if (q->field("borrowButton").toBool()) val = -val; calc.setPv(val); } if (q->field("interestRateEditValid").toBool()) { val = q->field("interestRateEdit").value().abs().toDouble(); calc.setIr(val); } if (q->field("paymentEditValid").toBool()) { val = q->field("paymentEdit").value().abs().toDouble(); if (q->field("lendButton").toBool()) val = -val; calc.setPmt(val); } if (q->field("finalPaymentEditValid").toBool()) { val = q->field("finalPaymentEditValid").value().abs().toDouble(); if (q->field("lendButton").toBool()) val = -val; calc.setFv(val); } if (q->field("durationValueEdit").toInt() != 0) { calc.setNpp(ui->m_durationPage->term()); } int fraction = m_account.fraction(MyMoneyFile::instance()->security(m_account.currencyId())); // setup of parameters is done, now do the calculation try { //FIXME: port if (!q->field("loanAmountEditValid").toBool()) { // calculate the amount of the loan out of the other information val = calc.presentValue(); ui->m_loanAmountPage->ui->m_loanAmountEdit->loadText(MyMoneyMoney(static_cast(val)).abs().formatMoney(fraction)); result = i18n("KMyMoney has calculated the amount of the loan as %1.", ui->m_loanAmountPage->ui->m_loanAmountEdit->lineedit()->text()); } else if (!q->field("interestRateEditValid").toBool()) { // calculate the interest rate out of the other information val = calc.interestRate(); ui->m_interestPage->ui->m_interestRateEdit->loadText(MyMoneyMoney(static_cast(val)).abs().formatMoney("", 3)); result = i18n("KMyMoney has calculated the interest rate to %1%.", ui->m_interestPage->ui->m_interestRateEdit->lineedit()->text()); } else if (!q->field("paymentEditValid").toBool()) { // calculate the periodical amount of the payment out of the other information val = calc.payment(); q->setField("paymentEdit", QVariant::fromValue(MyMoneyMoney(val).abs())); // reset payment as it might have changed due to rounding val = q->field("paymentEdit").value().abs().toDouble(); if (q->field("lendButton").toBool()) val = -val; calc.setPmt(val); result = i18n("KMyMoney has calculated a periodic payment of %1 to cover principal and interest.", ui->m_paymentPage->ui->m_paymentEdit->lineedit()->text()); val = calc.futureValue(); if ((q->field("borrowButton").toBool() && val < 0 && qAbs(val) >= qAbs(calc.payment())) || (q->field("lendButton").toBool() && val > 0 && qAbs(val) >= qAbs(calc.payment()))) { calc.setNpp(calc.npp() - 1); ui->m_durationPage->updateTermWidgets(calc.npp()); val = calc.futureValue(); MyMoneyMoney refVal(static_cast(val)); ui->m_finalPaymentPage->ui->m_finalPaymentEdit->loadText(refVal.abs().formatMoney(fraction)); result += QString(" "); result += i18n("The number of payments has been decremented and the final payment has been modified to %1.", ui->m_finalPaymentPage->ui->m_finalPaymentEdit->lineedit()->text()); } else if ((q->field("borrowButton").toBool() && val < 0 && qAbs(val) < qAbs(calc.payment())) || (q->field("lendButton").toBool() && val > 0 && qAbs(val) < qAbs(calc.payment()))) { ui->m_finalPaymentPage->ui->m_finalPaymentEdit->loadText(MyMoneyMoney().formatMoney(fraction)); } else { MyMoneyMoney refVal(static_cast(val)); ui->m_finalPaymentPage->ui->m_finalPaymentEdit->loadText(refVal.abs().formatMoney(fraction)); result += i18n("The final payment has been modified to %1.", ui->m_finalPaymentPage->ui->m_finalPaymentEdit->lineedit()->text()); } } else if (q->field("durationValueEdit").toInt() == 0) { // calculate the number of payments out of the other information val = calc.numPayments(); if (val == 0) throw MYMONEYEXCEPTION("incorrect fincancial calculation"); // if the number of payments has a fractional part, then we // round it to the smallest integer and calculate the balloon payment result = i18n("KMyMoney has calculated the term of your loan as %1. ", ui->m_durationPage->updateTermWidgets(qFloor(val))); if (val != qFloor(val)) { calc.setNpp(qFloor(val)); val = calc.futureValue(); MyMoneyMoney refVal(static_cast(val)); ui->m_finalPaymentPage->ui->m_finalPaymentEdit->loadText(refVal.abs().formatMoney(fraction)); result += i18n("The final payment has been modified to %1.", ui->m_finalPaymentPage->ui->m_finalPaymentEdit->lineedit()->text()); } } else { // calculate the future value of the loan out of the other information val = calc.futureValue(); // we differentiate between the following cases: // a) the future value is greater than a payment // b) the future value is less than a payment or the loan is overpaid // c) all other cases // // a) means, we have paid more than we owed. This can't be // b) means, we paid more than we owed but the last payment is // less in value than regular payments. That means, that the // future value is to be treated as (fully payed back) // c) the loan is not payed back yet if ((q->field("borrowButton").toBool() && val < 0 && qAbs(val) > qAbs(calc.payment())) || (q->field("lendButton").toBool() && val > 0 && qAbs(val) > qAbs(calc.payment()))) { // case a) qDebug("Future Value is %f", val); throw MYMONEYEXCEPTION("incorrect fincancial calculation"); } else if ((q->field("borrowButton").toBool() && val < 0 && qAbs(val) <= qAbs(calc.payment())) || (q->field("lendButton").toBool() && val > 0 && qAbs(val) <= qAbs(calc.payment()))) { // case b) val = 0; } MyMoneyMoney refVal(static_cast(val)); result = i18n("KMyMoney has calculated a final payment of %1 for this loan.", refVal.abs().formatMoney(fraction)); if (q->field("finalPaymentEditValid").toBool()) { if ((q->field("finalPaymentEdit").value().abs() - refVal.abs()).abs().toDouble() > 1) { throw MYMONEYEXCEPTION("incorrect fincancial calculation"); } result = i18n("KMyMoney has successfully verified your loan information."); } //FIXME: port ui->m_finalPaymentPage->ui->m_finalPaymentEdit->loadText(refVal.abs().formatMoney(fraction)); } } catch (const MyMoneyException &) { KMessageBox::error(0, i18n("You have entered mis-matching information. Please backup to the " "appropriate page and update your figures or leave one value empty " "to let KMyMoney calculate it for you"), i18n("Calculation error")); return 0; } result += i18n("\n\nAccept this or modify the loan information and recalculate."); KMessageBox::information(0, result, i18n("Calculation successful")); return 1; } /** * This method returns the transaction that is stored within * the schedule. See schedule(). * * @return MyMoneyTransaction object to be used within the schedule */ MyMoneyTransaction transaction() const { Q_Q(const KNewLoanWizard); MyMoneyTransaction t; bool hasInterest = !q->field("interestRateEdit").value().isZero(); MyMoneySplit sPayment, sInterest, sAmortization; // setup accounts. at this point, we cannot fill in the id of the // account that the amortization will be performed on, because we // create the account. So the id is yet unknown. sPayment.setAccountId(q->field("paymentAccountEdit").toStringList().first()); //Only create the interest split if not zero if (hasInterest) { sInterest.setAccountId(q->field("interestAccountEdit").toStringList().first()); sInterest.setValue(MyMoneyMoney::autoCalc); sInterest.setShares(sInterest.value()); sInterest.setAction(MyMoneySplit::ActionInterest); } // values if (q->field("borrowButton").toBool()) { sPayment.setValue(-q->field("paymentEdit").value()); } else { sPayment.setValue(q->field("paymentEdit").value()); } sAmortization.setValue(MyMoneyMoney::autoCalc); // don't forget the shares sPayment.setShares(sPayment.value()); sAmortization.setShares(sAmortization.value()); // setup the commodity MyMoneyAccount acc = MyMoneyFile::instance()->account(sPayment.accountId()); t.setCommodity(acc.currencyId()); // actions sPayment.setAction(MyMoneySplit::ActionAmortization); sAmortization.setAction(MyMoneySplit::ActionAmortization); // payee QString payeeId = q->field("payeeEdit").toString(); sPayment.setPayeeId(payeeId); sAmortization.setPayeeId(payeeId); MyMoneyAccount account("Phony-ID", MyMoneyAccount()); sAmortization.setAccountId(account.id()); // IMPORTANT: Payment split must be the first one, because // the schedule view expects it this way during display t.addSplit(sPayment); t.addSplit(sAmortization); if (hasInterest) { t.addSplit(sInterest); } // copy the splits from the other costs and update the payment split foreach (const MyMoneySplit& it, m_transaction.splits()) { if (it.accountId() != account.id()) { MyMoneySplit sp = it; sp.clearId(); t.addSplit(sp); sPayment.setValue(sPayment.value() - sp.value()); sPayment.setShares(sPayment.value()); t.modifySplit(sPayment); } } return t; } void loadAccountList() { Q_Q(KNewLoanWizard); AccountSet interestSet, assetSet; if (q->field("borrowButton").toBool()) { interestSet.addAccountType(eMyMoney::Account::Type::Expense); } else { interestSet.addAccountType(eMyMoney::Account::Type::Income); } if (ui->m_interestCategoryPage) interestSet.load(ui->m_interestCategoryPage->ui->m_interestAccountEdit); assetSet.addAccountType(eMyMoney::Account::Type::Checkings); assetSet.addAccountType(eMyMoney::Account::Type::Savings); assetSet.addAccountType(eMyMoney::Account::Type::Cash); assetSet.addAccountType(eMyMoney::Account::Type::Asset); assetSet.addAccountType(eMyMoney::Account::Type::Currency); if (ui->m_assetAccountPage) assetSet.load(ui->m_assetAccountPage->ui->m_assetAccountEdit); assetSet.addAccountType(eMyMoney::Account::Type::CreditCard); assetSet.addAccountType(eMyMoney::Account::Type::Liability); if (ui->m_schedulePage) assetSet.load(ui->m_schedulePage->ui->m_paymentAccountEdit); } KNewLoanWizard *q_ptr; Ui::KNewLoanWizard *ui; MyMoneyAccountLoan m_account; MyMoneyTransaction m_transaction; MyMoneySplit m_split; QBitArray m_pages; }; #endif