diff --git a/kmymoney/CMakeLists.txt b/kmymoney/CMakeLists.txt index 8c6bce765..04ee39ca4 100644 --- a/kmymoney/CMakeLists.txt +++ b/kmymoney/CMakeLists.txt @@ -1,188 +1,189 @@ include(ECMAddAppIcon) include_directories(${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_BINARY_DIR}/dialogs/ ${CMAKE_CURRENT_SOURCE_DIR}/widgets/ ${CMAKE_CURRENT_BINARY_DIR}/widgets/ ${CMAKE_CURRENT_SOURCE_DIR}/mymoney/ ${CMAKE_CURRENT_SOURCE_DIR}/mymoney/storage/ ${CMAKE_CURRENT_SOURCE_DIR}/plugins/ ${CMAKE_CURRENT_BINARY_DIR}/plugins/ ${CMAKE_CURRENT_SOURCE_DIR}/views/ ${CMAKE_CURRENT_SOURCE_DIR}/dialogs/ ${CMAKE_CURRENT_SOURCE_DIR}/converter/ ${CMAKE_CURRENT_BINARY_DIR}/dialogs/settings/ ${CMAKE_CURRENT_BINARY_DIR}/mymoney/storage/ ${CMAKE_CURRENT_BINARY_DIR}/mymoney/ ${CMAKE_CURRENT_SOURCE_DIR}/reports/ ${CMAKE_CURRENT_SOURCE_DIR}/wizards/endingbalancedlg/ ${CMAKE_CURRENT_BINARY_DIR}/wizards/endingbalancedlg/ ${CMAKE_CURRENT_SOURCE_DIR}/wizards/newinvestmentwizard/ ${CMAKE_CURRENT_BINARY_DIR}/wizards/newinvestmentwizard/ ${CMAKE_CURRENT_SOURCE_DIR}/wizards/newloanwizard/ ${CMAKE_CURRENT_BINARY_DIR}/wizards/newloanwizard/ ${CMAKE_CURRENT_SOURCE_DIR}/wizards/wizardpages/ ${CMAKE_CURRENT_SOURCE_DIR}/models/ ${CMAKE_CURRENT_BINARY_DIR}/models/ ${CMAKE_CURRENT_SOURCE_DIR}/icons/ ${CMAKE_CURRENT_BINARY_DIR}/icons/ ${CMAKE_CURRENT_BINARY_DIR}/payeeidentifier/ibanandbic/widgets/ # TODO: this line should be moved to the target it belongs ${CMAKE_CURRENT_BINARY_DIR}/payeeidentifier/ibanandbic/ ${CMAKE_CURRENT_BINARY_DIR}/payeeidentifier/nationalaccount/ ${KMyMoney_SOURCE_DIR}/libkgpgfile/ ) add_subdirectory( mymoney ) add_subdirectory( models ) add_subdirectory( plugins ) add_subdirectory( reports ) add_subdirectory( widgets ) add_subdirectory( dialogs ) add_subdirectory( views ) add_subdirectory( converter ) add_subdirectory( wizards ) add_subdirectory( pics ) add_subdirectory( html ) add_subdirectory( templates ) add_subdirectory( misc ) add_subdirectory( payeeidentifier ) add_subdirectory( icons ) if(BUILD_TESTING) add_subdirectory( tests ) endif() set( _HEADERS kmymoneyutils.h kmymoneyglobalsettings.h ) ########### settings code (kmm_config) STATIC ############### set( kmm_config_SRCS kmymoneyglobalsettings.cpp ) kconfig_add_kcfg_files( kmm_config_SRCS kmymoneysettings.kcfgc ) add_library(kmm_config STATIC ${kmm_config_SRCS}) target_link_libraries(kmm_config KF5::WidgetsAddons KF5::ConfigWidgets Qt5::Sql Alkimia::alkimia) ########### common code (kmymoney_common) STATIC ############### # will be linked into kmymoney, kmymoneytest, and libkmymoney.so set( kmymoney_common_SRCS kmymoneyutils.cpp kstartuplogo.cpp kcreditswindow.cpp ) add_library(kmymoney_common STATIC ${kmymoney_common_SRCS}) target_link_libraries(kmymoney_common PUBLIC Qt5::Core KF5::ConfigGui KF5::WidgetsAddons Alkimia::alkimia kmm_widgets kmm_mymoney kmm_utils_webconnect kmm_utils_platformtools ) # must build kmymoney/transactionsortoption.h # from transactionsortoption.ui first add_dependencies(kmymoney_common generate_base_ui_srcs kmm_config) add_dependencies(wizardpages widgets) if(USE_MODELTEST) set( kmymoney_common_LIBS ${kmymoney_common_LIBS} ${QT_QTTEST_LIBRARY}) endif(USE_MODELTEST) ########### kmymoney executable ############### set( kmymoney_SRCS main.cpp kmymoney.cpp pluginloader.cpp ) qt5_add_dbus_adaptor(kmymoney_SRCS org.kde.kmymoney.xml kmymoney.h KMyMoneyApp) qt5_add_resources(kmymoney_SRCS kmymoney.qrc) # collect application icons file(GLOB_RECURSE KMYMONEY_APP_ICONS "${CMAKE_CURRENT_SOURCE_DIR}/icons/kmymoney/apps/*.png") # add icons to application sources, to have them bundled ecm_add_app_icon(kmymoney_SRCS ICONS ${KMYMONEY_APP_ICONS}) add_executable( kmymoney ${kmymoney_SRCS} ) target_link_libraries(kmymoney views reports kmymoney_base kmymoney_common newuserwizard newaccountwizard newinvestmentwizard newloanwizard endingbalancedlg wizardpages dialogs widgets settings converter models kmm_config kmm_widgets kmm_storage kmm_mymoney kgpgfile interfaces kmm_plugin Qt5::Core Qt5::Sql KF5::Archive KF5::ConfigGui KF5::WidgetsAddons + KF5::KIOCore KF5::CoreAddons KChart $<$:Qt5::Test> $<$:KF5::Holidays> ) # own plist magic for mac os if(APPLE) # own plist template set_target_properties(kmymoney PROPERTIES MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/MacOSXBundleInfo.plist.in) # the MacOSX bundle display name property (CFBundleDisplayName) is not currently supported by cmake, # so has to be set for all targets in this cmake file set(MACOSX_BUNDLE_DISPLAY_NAME KMyMoney) set_target_properties(kmymoney PROPERTIES MACOSX_BUNDLE_GUI_IDENTIFIER "org.kde.KMyMoney") set_target_properties(kmymoney PROPERTIES MACOSX_BUNDLE_BUNDLE_NAME "KMyMoney") set_target_properties(kmymoney PROPERTIES MACOSX_BUNDLE_DISPLAY_NAME "KMyMoney") set_target_properties(kmymoney PROPERTIES MACOSX_BUNDLE_INFO_STRING "KMyMoney - Personal Finances Manager") set_target_properties(kmymoney PROPERTIES MACOSX_BUNDLE_LONG_VERSION_STRING "KMyMoney ${KDE_APPLICATIONS_VERSION}") set_target_properties(kmymoney PROPERTIES MACOSX_BUNDLE_SHORT_VERSION_STRING "${KDE_APPLICATIONS_VERSION_MAJOR}.${KDE_APPLICATIONS_VERSION_MINOR}") set_target_properties(kmymoney PROPERTIES MACOSX_BUNDLE_BUNDLE_VERSION "${KDE_APPLICATIONS_VERSION}") set_target_properties(kmymoney PROPERTIES MACOSX_BUNDLE_COPYRIGHT "2000-2016 The KMyMoney Authors") endif() ########### install files ############### install(TARGETS kmymoney ${INSTALL_TARGETS_DEFAULT_ARGS}) install(FILES kmymoney.kcfg DESTINATION ${KCFG_INSTALL_DIR} ) install(FILES kmymoney.upd DESTINATION ${KCONF_UPDATE_INSTALL_DIR} ) install(FILES ${_HEADERS} DESTINATION ${INCLUDE_INSTALL_DIR}/kmymoney ) install(FILES org.kde.kmymoney.desktop DESTINATION ${XDG_APPS_INSTALL_DIR} ) install(FILES org.kde.kmymoney.appdata.xml DESTINATION ${KDE_INSTALL_METAINFODIR} ) install(FILES x-kmymoney.xml DESTINATION ${XDG_MIME_INSTALL_DIR}) install(FILES tips DESTINATION ${CMAKE_INSTALL_DATADIR}/kmymoney) #UPDATE_XDG_MIMETYPES(${XDG_MIME_INSTALL_DIR}) diff --git a/kmymoney/converter/mymoneytemplate.cpp b/kmymoney/converter/mymoneytemplate.cpp index 06939bdd5..b21c32dc6 100644 --- a/kmymoney/converter/mymoneytemplate.cpp +++ b/kmymoney/converter/mymoneytemplate.cpp @@ -1,489 +1,505 @@ /*************************************************************************** mymoneytemplate.cpp - description ------------------- begin : Sat Aug 14 2004 copyright : (C) 2004 by Thomas Baumgart email : ipwizard@users.sourceforge.net ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "mymoneytemplate.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include #include +#include +#include // ---------------------------------------------------------------------------- // Project Includes #include "kmymoneyutils.h" #include "mymoneyfile.h" #include "mymoneyaccount.h" #include "mymoneyexception.h" #include "mymoneyenums.h" MyMoneyTemplate::MyMoneyTemplate() : m_progressCallback(0), m_accountsRead(0) { } MyMoneyTemplate::MyMoneyTemplate(const QUrl& url) : m_progressCallback(0), m_accountsRead(0) { loadTemplate(url); } MyMoneyTemplate::~MyMoneyTemplate() { } bool MyMoneyTemplate::loadTemplate(const QUrl &url) { QString filename; - + bool downloadedFile = false; if (!url.isValid()) { qDebug("Invalid template URL '%s'", qPrintable(url.url())); return false; } m_source = url; if (url.isLocalFile()) { filename = url.toLocalFile(); } else { bool rc = false; - // TODO: port to kf5 - //rc = KIO::NetAccess::download(url, filename, KMyMoneyUtils::mainWindow()); - if (!rc) { - KMessageBox::detailedError(KMyMoneyUtils::mainWindow(), + downloadedFile = true; + KIO::StoredTransferJob *transferjob = KIO::storedGet (url); + KJobWidgets::setWindow(transferjob, KMyMoneyUtils::mainWindow()); + rc = transferjob->exec(); + if (! rc) { + KMessageBox::detailedError(KMyMoneyUtils::mainWindow(), i18n("Error while loading file '%1'.", url.url()), - // TODO: port to kf5 - QString(),//KIO::NetAccess::lastErrorString(), + transferjob->errorString(), i18n("File access error")); - return false; + return false; } + QTemporaryFile file; + file.setAutoRemove(false); + file.open(); + file.write(transferjob->data()); + filename = file.fileName(); + file.close(); } bool rc = true; QFile file(filename); QFileInfo info(file); if (!info.isFile()) { QString msg = i18n("

%1 is not a template file.

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

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

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

Invalid tag %1 in template file %2

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

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

", childElement.attribute("type"), m_source.toDisplayString())); rc = false; } if (rc == true) { if (childElement.attribute("name").isEmpty()) rc = createAccounts(parent, childElement.firstChild()); else rc = createAccounts(parent, childElement); } } else { rc = false; } m_accounts = m_accounts.nextSibling(); } signalProgress(-1, -1); return rc; } bool MyMoneyTemplate::createAccounts(MyMoneyAccount& parent, QDomNode account) { bool rc = true; while (rc == true && !account.isNull()) { MyMoneyAccount acc; if (account.isElement()) { QDomElement accountElement = account.toElement(); if (accountElement.tagName() == "account") { signalProgress(++m_accountsRead, 0); QList subAccountList; QList::ConstIterator it; it = subAccountList.constEnd(); if (!parent.accountList().isEmpty()) { MyMoneyFile::instance()->accountList(subAccountList, parent.accountList()); for (it = subAccountList.constBegin(); it != subAccountList.constEnd(); ++it) { if ((*it).name() == accountElement.attribute("name")) { acc = *it; break; } } } if (it == subAccountList.constEnd()) { // not found, we need to create it acc.setName(accountElement.attribute("name")); acc.setAccountType(static_cast(accountElement.attribute("type").toUInt())); setFlags(acc, account.firstChild()); try { MyMoneyFile::instance()->addAccount(acc, parent); } catch (const MyMoneyException &) { } } createAccounts(acc, account.firstChild()); } } account = account.nextSibling(); } return rc; } bool MyMoneyTemplate::setFlags(MyMoneyAccount& acc, QDomNode flags) { bool rc = true; while (rc == true && !flags.isNull()) { if (flags.isElement()) { QDomElement flagElement = flags.toElement(); if (flagElement.tagName() == "flag") { // make sure, we only store flags we know! QString value = flagElement.attribute("name"); if (value == "Tax") { acc.setValue(value.toLatin1(), "Yes"); } else if (value == "OpeningBalanceAccount") { acc.setValue("OpeningBalanceAccount", "Yes"); } else { KMessageBox::error(KMyMoneyUtils::mainWindow(), i18n("

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

", flagElement.attribute("name"), m_source.toDisplayString(), acc.name())); rc = false; } QString currency = flagElement.attribute("currency"); if (!currency.isEmpty()) acc.setCurrencyId(currency); } } flags = flags.nextSibling(); } return rc; } void MyMoneyTemplate::signalProgress(int current, int total, const QString& msg) { if (m_progressCallback != 0) (*m_progressCallback)(current, total, msg); } bool MyMoneyTemplate::exportTemplate(void(*callback)(int, int, const QString&)) { m_progressCallback = callback; m_doc = QDomDocument("KMYMONEY-TEMPLATE"); QDomProcessingInstruction instruct = m_doc.createProcessingInstruction(QString("xml"), QString("version=\"1.0\" encoding=\"utf-8\"")); m_doc.appendChild(instruct); QDomElement mainElement = m_doc.createElement("kmymoney-account-template"); m_doc.appendChild(mainElement); QDomElement title = m_doc.createElement("title"); QDomText t = m_doc.createTextNode(m_title); title.appendChild(t); mainElement.appendChild(title); QDomElement shortDesc = m_doc.createElement("shortdesc"); t = m_doc.createTextNode(m_shortDesc); shortDesc.appendChild(t); mainElement.appendChild(shortDesc); QDomElement longDesc = m_doc.createElement("longdesc"); t = m_doc.createTextNode(m_longDesc); longDesc.appendChild(t); mainElement.appendChild(longDesc); QDomElement accounts = m_doc.createElement("accounts"); mainElement.appendChild(accounts); addAccountStructure(accounts, MyMoneyFile::instance()->asset()); addAccountStructure(accounts, MyMoneyFile::instance()->expense()); addAccountStructure(accounts, MyMoneyFile::instance()->income()); addAccountStructure(accounts, MyMoneyFile::instance()->liability()); addAccountStructure(accounts, MyMoneyFile::instance()->equity()); return true; } const QString& MyMoneyTemplate::title() const { return m_title; } const QString& MyMoneyTemplate::shortDescription() const { return m_shortDesc; } const QString& MyMoneyTemplate::longDescription() const { return m_longDesc; } void MyMoneyTemplate::setTitle(const QString &s) { m_title = s; } void MyMoneyTemplate::setShortDescription(const QString &s) { m_shortDesc = s; } void MyMoneyTemplate::setLongDescription(const QString &s) { m_longDesc = s; } static bool nameLessThan(MyMoneyAccount &a1, MyMoneyAccount &a2) { return a1.name() < a2.name(); } bool MyMoneyTemplate::addAccountStructure(QDomElement& parent, const MyMoneyAccount& acc) { QDomElement account = m_doc.createElement("account"); parent.appendChild(account); if (MyMoneyFile::instance()->isStandardAccount(acc.id())) account.setAttribute(QString("name"), QString()); else account.setAttribute(QString("name"), acc.name()); account.setAttribute(QString("type"), (int)acc.accountType()); // FIXME: add tax flag stuff if (acc.pairs().contains("OpeningBalanceAccount")) { QString openingBalanceAccount = acc.value("OpeningBalanceAccount"); if (openingBalanceAccount == "Yes") { QDomElement flag = m_doc.createElement("flag"); flag.setAttribute(QString("name"), "OpeningBalanceAccount"); flag.setAttribute(QString("currency"), acc.currencyId()); account.appendChild(flag); } } // any child accounts? if (acc.accountList().count() > 0) { QList list; MyMoneyFile::instance()->accountList(list, acc.accountList(), false); qSort(list.begin(), list.end(), nameLessThan); QList::Iterator it; for (it = list.begin(); it != list.end(); ++it) { addAccountStructure(account, *it); } } return true; } bool MyMoneyTemplate::saveTemplate(const QUrl &url) { QString filename; if (!url.isValid()) { qDebug("Invalid template URL '%s'", qPrintable(url.url())); return false; } if (url.isLocalFile()) { filename = url.toLocalFile(); QSaveFile qfile(filename/*, 0600*/); if (qfile.open(QIODevice::WriteOnly)) { saveToLocalFile(&qfile); if (!qfile.commit()) { throw MYMONEYEXCEPTION(i18n("Unable to write changes to '%1'", filename)); } } else { throw MYMONEYEXCEPTION(i18n("Unable to write changes to '%1'", filename)); } } else { QTemporaryFile tmpfile; + tmpfile.open(); QSaveFile qfile(tmpfile.fileName()); if (qfile.open(QIODevice::WriteOnly)) { saveToLocalFile(&qfile); if (!qfile.commit()) { - throw MYMONEYEXCEPTION(i18n("Unable to upload to '%1'", url.url())); + throw MYMONEYEXCEPTION(i18n("Unable to upload to '%1'", url.toDisplayString())); } } else { - throw MYMONEYEXCEPTION(i18n("Unable to upload to '%1'", url.url())); + throw MYMONEYEXCEPTION(i18n("Unable to upload to '%1'", url.toDisplayString())); } - // TODO: port to kf5 - //if (!KIO::NetAccess::upload(tmpfile.fileName(), url, 0)) - // throw MYMONEYEXCEPTION(i18n("Unable to upload to '%1'", url.url())); + int permission = -1; + QFile file(tmpfile.fileName()); + file.open(QIODevice::ReadOnly); + KIO::StoredTransferJob *putjob = KIO::storedPut(file.readAll(), url, permission, KIO::JobFlag::Overwrite); + if (!putjob->exec()) { + throw MYMONEYEXCEPTION(i18n("Unable to upload to '%1'.
%2", url.toDisplayString(), putjob->errorString())); + } + file.close(); } return true; } bool MyMoneyTemplate::saveToLocalFile(QSaveFile* qfile) { QTextStream stream(qfile); stream.setCodec("UTF-8"); stream << m_doc.toString(); stream.flush(); return true; } diff --git a/kmymoney/kmymoney.cpp b/kmymoney/kmymoney.cpp index 2136f3240..50b04dd5f 100644 --- a/kmymoney/kmymoney.cpp +++ b/kmymoney/kmymoney.cpp @@ -1,7540 +1,7540 @@ /*************************************************************************** kmymoney.cpp ------------------- copyright : (C) 2000 by Michael Edwardes (C) 2007 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 #include "kmymoney.h" // ---------------------------------------------------------------------------- // Std C++ / STL Includes #include #include #include // ---------------------------------------------------------------------------- // QT Includes #include #include // only for performance tests #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef KF5Holidays_FOUND #include #include #endif // ---------------------------------------------------------------------------- // Project Includes #include "kmymoneyglobalsettings.h" #include "kmymoneyadaptor.h" #include "dialogs/settings/ksettingskmymoney.h" #include "dialogs/kbackupdlg.h" #include "dialogs/kenterscheduledlg.h" #include "dialogs/kconfirmmanualenterdlg.h" #include "dialogs/kmymoneypricedlg.h" #include "dialogs/kcurrencyeditdlg.h" #include "dialogs/kequitypriceupdatedlg.h" #include "dialogs/kmymoneyfileinfodlg.h" #include "dialogs/kfindtransactiondlg.h" #include "dialogs/knewbankdlg.h" #include "wizards/newinvestmentwizard/knewinvestmentwizard.h" #include "dialogs/knewaccountdlg.h" #include "dialogs/editpersonaldatadlg.h" #include "dialogs/kselectdatabasedlg.h" #include "dialogs/kcurrencycalculator.h" #include "dialogs/keditscheduledlg.h" #include "wizards/newloanwizard/keditloanwizard.h" #include "dialogs/kpayeereassigndlg.h" #include "dialogs/ktagreassigndlg.h" #include "dialogs/kcategoryreassigndlg.h" #include "wizards/endingbalancedlg/kendingbalancedlg.h" #include "dialogs/kbalancechartdlg.h" #include "dialogs/kgeneratesqldlg.h" #include "dialogs/kloadtemplatedlg.h" #include "dialogs/kgpgkeyselectiondlg.h" #include "dialogs/ktemplateexportdlg.h" #include "dialogs/transactionmatcher.h" #include "wizards/newuserwizard/knewuserwizard.h" #include "wizards/newaccountwizard/knewaccountwizard.h" #include "dialogs/kbalancewarning.h" #include "widgets/kmymoneyaccountselector.h" #include "widgets/kmymoneypayeecombo.h" #include "widgets/onlinejobmessagesview.h" #include "widgets/kmymoneymvccombo.h" #include "views/kmymoneyview.h" #include "views/konlinejoboutbox.h" #include "models/onlinejobmessagesmodel.h" #include "mymoney/mymoneyobject.h" #include "mymoney/mymoneyfile.h" #include "mymoney/mymoneyinstitution.h" #include "mymoney/mymoneyaccount.h" #include "mymoney/mymoneyaccountloan.h" #include "mymoney/mymoneysecurity.h" #include "mymoney/mymoneypayee.h" #include "mymoney/mymoneytag.h" #include "mymoney/mymoneybudget.h" #include "mymoney/mymoneysplit.h" #include "mymoney/mymoneyutils.h" #include "mymoney/mymoneystatement.h" #include "mymoney/storage/mymoneystoragedump.h" #include "mymoney/storage/imymoneystorage.h" #include "mymoney/mymoneyforecast.h" #include "mymoney/onlinejobmessage.h" #include "converter/mymoneystatementreader.h" #include "converter/mymoneytemplate.h" #include "plugins/interfaces/kmmviewinterface.h" #include "plugins/interfaces/kmmstatementinterface.h" #include "plugins/interfaces/kmmimportinterface.h" #include "plugins/interfaceloader.h" #include "plugins/onlinepluginextended.h" #include "pluginloader.h" #include "tasks/credittransfer.h" #include "icons/icons.h" #include "misc/webconnect.h" #include "storage/imymoneyserialize.h" #include "storage/mymoneystoragesql.h" #include #include "transactioneditor.h" #include "konlinetransferform.h" #include #include #include "kmymoneyutils.h" #include "kcreditswindow.h" #include "ledgerdelegate.h" #include "storageenums.h" #include "mymoneyenums.h" #include "dialogenums.h" #include "misc/platformtools.h" #ifdef KMM_DEBUG #include "mymoneytracer.h" #endif using namespace Icons; static constexpr char recoveryKeyId[] = "59B0F826D2B08440"; // define the default period to warn about an expiring recoverkey to 30 days // but allows to override this setting during build time #ifndef RECOVER_KEY_EXPIRATION_WARNING #define RECOVER_KEY_EXPIRATION_WARNING 30 #endif const QHash KMyMoneyApp::s_Actions { {Action::FileOpenDatabase, QStringLiteral("open_database")}, {Action::FileSaveAsDatabase, QStringLiteral("saveas_database")}, {Action::FileBackup, QStringLiteral("file_backup")}, {Action::FileImportStatement, QStringLiteral("file_import_statement")}, {Action::FileImportTemplate, QStringLiteral("file_import_template")}, {Action::FileExportTemplate, QStringLiteral("file_export_template")}, {Action::FilePersonalData, QStringLiteral("view_personal_data")}, #ifdef KMM_DEBUG {Action::FileDump, QStringLiteral("file_dump")}, #endif {Action::FileInformation, QStringLiteral("view_file_info")}, {Action::EditFindTransaction, QStringLiteral("edit_find_transaction")}, {Action::ViewTransactionDetail, QStringLiteral("view_show_transaction_detail")}, {Action::ViewHideReconciled, QStringLiteral("view_hide_reconciled_transactions")}, {Action::ViewHideCategories, QStringLiteral("view_hide_unused_categories")}, {Action::ViewShowAll, QStringLiteral("view_show_all_accounts")}, {Action::InstitutionNew, QStringLiteral("institution_new")}, {Action::InstitutionEdit, QStringLiteral("institution_edit")}, {Action::InstitutionDelete, QStringLiteral("institution_delete")}, {Action::AccountNew, QStringLiteral("account_new")}, {Action::AccountOpen, QStringLiteral("account_open")}, {Action::AccountStartReconciliation, QStringLiteral("account_reconcile")}, {Action::AccountFinishReconciliation, QStringLiteral("account_reconcile_finish")}, {Action::AccountPostponeReconciliation, QStringLiteral("account_reconcile_postpone")}, {Action::AccountEdit, QStringLiteral("account_edit")}, {Action::AccountDelete, QStringLiteral("account_delete")}, {Action::AccountClose, QStringLiteral("account_close")}, {Action::AccountReopen, QStringLiteral("account_reopen")}, {Action::AccountTransactionReport, QStringLiteral("account_transaction_report")}, {Action::AccountBalanceChart, QStringLiteral("account_chart")}, {Action::AccountOnlineMap, QStringLiteral("account_online_map")}, {Action::AccountOnlineUnmap, QStringLiteral("account_online_unmap")}, {Action::AccountUpdateMenu, QStringLiteral("account_online_update_menu")}, {Action::AccountUpdate, QStringLiteral("account_online_update")}, {Action::AccountUpdateAll, QStringLiteral("account_online_update_all")}, {Action::AccountCreditTransfer, QStringLiteral("account_online_new_credit_transfer")}, {Action::CategoryNew, QStringLiteral("category_new")}, {Action::CategoryEdit, QStringLiteral("category_edit")}, {Action::CategoryDelete, QStringLiteral("category_delete")}, {Action::ToolCurrencies, QStringLiteral("tools_currency_editor")}, {Action::ToolPrices, QStringLiteral("tools_price_editor")}, {Action::ToolUpdatePrices, QStringLiteral("tools_update_prices")}, {Action::ToolConsistency, QStringLiteral("tools_consistency_check")}, {Action::ToolPerformance, QStringLiteral("tools_performancetest")}, {Action::ToolSQL, QStringLiteral("tools_generate_sql")}, {Action::ToolCalculator, QStringLiteral("tools_kcalc")}, {Action::SettingsAllMessages, QStringLiteral("settings_enable_messages")}, {Action::HelpShow, QStringLiteral("help_show_tip")}, {Action::TransactionNew, QStringLiteral("transaction_new")}, {Action::TransactionEdit, QStringLiteral("transaction_edit")}, {Action::TransactionEnter, QStringLiteral("transaction_enter")}, {Action::TransactionEditSplits, QStringLiteral("transaction_editsplits")}, {Action::TransactionCancel, QStringLiteral("transaction_cancel")}, {Action::TransactionDelete, QStringLiteral("transaction_delete")}, {Action::TransactionDuplicate, QStringLiteral("transaction_duplicate")}, {Action::TransactionMatch, QStringLiteral("transaction_match")}, {Action::TransactionAccept, QStringLiteral("transaction_accept")}, {Action::TransactionToggleReconciled, QStringLiteral("transaction_mark_toggle")}, {Action::TransactionToggleCleared, QStringLiteral("transaction_mark_cleared")}, {Action::TransactionReconciled, QStringLiteral("transaction_mark_reconciled")}, {Action::TransactionNotReconciled, QStringLiteral("transaction_mark_notreconciled")}, {Action::TransactionSelectAll, QStringLiteral("transaction_select_all")}, {Action::TransactionGoToAccount, QStringLiteral("transaction_goto_account")}, {Action::TransactionGoToPayee, QStringLiteral("transaction_goto_payee")}, {Action::TransactionCreateSchedule, QStringLiteral("transaction_create_schedule")}, {Action::TransactionAssignNumber, QStringLiteral("transaction_assign_number")}, {Action::TransactionCombine, QStringLiteral("transaction_combine")}, {Action::TransactionCopySplits, QStringLiteral("transaction_copy_splits")}, {Action::TransactionMoveMenu, QStringLiteral("transaction_move_menu")}, {Action::TransactionMarkMenu, QStringLiteral("transaction_mark_menu")}, {Action::TransactionContextMarkMenu, QStringLiteral("transaction_context_mark_menu")}, {Action::InvestmentNew, QStringLiteral("investment_new")}, {Action::InvestmentEdit, QStringLiteral("investment_edit")}, {Action::InvestmentDelete, QStringLiteral("investment_delete")}, {Action::InvestmentOnlinePrice, QStringLiteral("investment_online_price_update")}, {Action::InvestmentManualPrice, QStringLiteral("investment_manual_price_update")}, {Action::ScheduleNew, QStringLiteral("schedule_new")}, {Action::ScheduleEdit, QStringLiteral("schedule_edit")}, {Action::ScheduleDelete, QStringLiteral("schedule_delete")}, {Action::ScheduleDuplicate, QStringLiteral("schedule_duplicate")}, {Action::ScheduleEnter, QStringLiteral("schedule_enter")}, {Action::ScheduleSkip, QStringLiteral("schedule_skip")}, {Action::PayeeNew, QStringLiteral("payee_new")}, {Action::PayeeRename, QStringLiteral("payee_rename")}, {Action::PayeeDelete, QStringLiteral("payee_delete")}, {Action::PayeeMerge, QStringLiteral("payee_merge")}, {Action::TagNew, QStringLiteral("tag_new")}, {Action::TagRename, QStringLiteral("tag_rename")}, {Action::TagDelete, QStringLiteral("tag_delete")}, {Action::BudgetNew, QStringLiteral("budget_new")}, {Action::BudgetRename, QStringLiteral("budget_rename")}, {Action::BudgetDelete, QStringLiteral("budget_delete")}, {Action::BudgetCopy, QStringLiteral("budget_copy")}, {Action::BudgetChangeYear, QStringLiteral("budget_change_year")}, {Action::BudgetForecast, QStringLiteral("budget_forecast")}, {Action::CurrencyNew, QStringLiteral("currency_new")}, {Action::CurrencyRename, QStringLiteral("currency_rename")}, {Action::CurrencyDelete, QStringLiteral("currency_delete")}, {Action::CurrencySetBase, QStringLiteral("currency_setbase")}, {Action::PriceNew, QStringLiteral("price_new")}, {Action::PriceEdit, QStringLiteral("price_edit")}, {Action::PriceUpdate, QStringLiteral("price_update")}, {Action::PriceDelete, QStringLiteral("price_delete")}, #ifdef KMM_DEBUG {Action::WizardNewUser, QStringLiteral("new_user_wizard")}, {Action::DebugTraces, QStringLiteral("debug_traces")}, #endif {Action::DebugTimers, QStringLiteral("debug_timers")}, {Action::OnlineJobDelete, QStringLiteral("onlinejob_delete")}, {Action::OnlineJobEdit, QStringLiteral("onlinejob_edit")}, {Action::OnlineJobLog, QStringLiteral("onlinejob_log")}, }; enum backupStateE { BACKUP_IDLE = 0, BACKUP_MOUNTING, BACKUP_COPYING, BACKUP_UNMOUNTING }; class KMyMoneyApp::Private { public: Private(KMyMoneyApp *app) : q(app), m_ft(0), m_moveToAccountSelector(0), m_statementXMLindex(0), m_balanceWarning(0), m_collectingStatements(false), m_pluginLoader(0), m_backupResult(0), m_backupMount(0), m_ignoreBackupExitCode(false), m_myMoneyView(0), m_progressBar(0), m_smtReader(0), m_searchDlg(0), m_autoSaveTimer(0), m_progressTimer(0), m_inAutoSaving(false), m_transactionEditor(0), m_endingBalanceDlg(0), m_saveEncrypted(0), m_additionalKeyLabel(0), m_additionalKeyButton(0), m_recentFiles(0), #ifdef KF5Holidays_FOUND m_holidayRegion(0), #endif m_applicationIsReady(true), m_webConnect(new WebConnect(app)) { // since the days of the week are from 1 to 7, // and a day of the week is used to index this bit array, // resize the array to 8 elements (element 0 is left unused) m_processingDays.resize(8); } void closeFile(); void unlinkStatementXML(); void moveInvestmentTransaction(const QString& fromId, const QString& toId, const MyMoneyTransaction& t); QList > automaticReconciliation(const MyMoneyAccount &account, const QList > &transactions, const MyMoneyMoney &amount); /** * The public interface. */ KMyMoneyApp * const q; MyMoneyFileTransaction* m_ft; KMyMoneyAccountSelector* m_moveToAccountSelector; int m_statementXMLindex; KBalanceWarning* m_balanceWarning; bool m_collectingStatements; QStringList m_statementResults; KMyMoneyPlugin::PluginLoader* m_pluginLoader; QString m_lastPayeeEnteredId; /** the configuration object of the application */ KSharedConfigPtr m_config; /** * @brief List of all plugged plugins * * The key is the file name of the plugin. */ QMap m_plugins; /** * @brief List of plugged importer plugins * * The key is the objectName of the plugin. */ QMap m_importerPlugins; /** * @brief List of plugged online plugins * * The key is the objectName of the plugin. */ QMap m_onlinePlugins; /** * The following variable represents the state while crafting a backup. * It can have the following values * * - IDLE: the default value if not performing a backup * - MOUNTING: when a mount command has been issued * - COPYING: when a copy command has been issued * - UNMOUNTING: when an unmount command has been issued */ backupStateE m_backupState; /** * This variable keeps the result of the backup operation. */ int m_backupResult; /** * This variable is set, when the user selected to mount/unmount * the backup volume. */ bool m_backupMount; /** * Flag for internal run control */ bool m_ignoreBackupExitCode; KProcess m_proc; /// A pointer to the view holding the tabs. KMyMoneyView *m_myMoneyView; /// The URL of the file currently being edited when open. QUrl m_fileName; bool m_startDialog; QString m_mountpoint; QProgressBar* m_progressBar; QTime m_lastUpdate; QLabel* m_statusLabel; MyMoneyStatementReader* m_smtReader; // allows multiple imports to be launched trough web connect and to be executed sequentially QQueue m_importUrlsQueue; KFindTransactionDlg* m_searchDlg; QObject* m_pluginInterface; MyMoneyAccount m_selectedAccount; MyMoneyAccount m_reconciliationAccount; MyMoneyAccount m_selectedInvestment; MyMoneyInstitution m_selectedInstitution; MyMoneySchedule m_selectedSchedule; MyMoneySecurity m_selectedCurrency; MyMoneyPrice m_selectedPrice; QList m_selectedPayees; QList m_selectedTags; QList m_selectedBudgets; KMyMoneyRegister::SelectedTransactions m_selectedTransactions; // This is Auto Saving related bool m_autoSaveEnabled; QTimer* m_autoSaveTimer; QTimer* m_progressTimer; int m_autoSavePeriod; bool m_inAutoSaving; // pointer to the current transaction editor TransactionEditor* m_transactionEditor; // Reconciliation dialog KEndingBalanceDlg* m_endingBalanceDlg; // Pointer to the combo box used for key selection during // File/Save as KComboBox* m_saveEncrypted; // id's that need to be remembered QString m_accountGoto, m_payeeGoto; QStringList m_additionalGpgKeys; QLabel* m_additionalKeyLabel; QPushButton* m_additionalKeyButton; KRecentFilesAction* m_recentFiles; #ifdef KF5Holidays_FOUND // used by the calendar interface for schedules KHolidays::HolidayRegion* m_holidayRegion; #endif QBitArray m_processingDays; QMap m_holidayMap; QStringList m_consistencyCheckResult; bool m_applicationIsReady; WebConnect* m_webConnect; // methods void consistencyCheck(bool alwaysDisplayResults); static void setThemedCSS(); void copyConsistencyCheckResults(); void saveConsistencyCheckResults(); }; KMyMoneyApp::KMyMoneyApp(QWidget* parent) : KXmlGuiWindow(parent), d(new Private(this)) { #ifdef KMM_DBUS new KmymoneyAdaptor(this); QDBusConnection::sessionBus().registerObject("/KMymoney", this); QDBusConnection::sessionBus().interface()->registerService( "org.kde.kmymoney", QDBusConnectionInterface::DontQueueService); #endif // Register the main engine types used as meta-objects qRegisterMetaType("MyMoneyMoney"); qRegisterMetaType("MyMoneySecurity"); // preset the pointer because we need it during the course of this constructor kmymoney = this; d->m_config = KSharedConfig::openConfig(); d->setThemedCSS(); MyMoneyTransactionFilter::setFiscalYearStart(KMyMoneyGlobalSettings::firstFiscalMonth(), KMyMoneyGlobalSettings::firstFiscalDay()); updateCaption(true); QFrame* frame = new QFrame; frame->setFrameStyle(QFrame::NoFrame); // values for margin (11) and spacing(6) taken from KDialog implementation QBoxLayout* layout = new QBoxLayout(QBoxLayout::TopToBottom, frame); layout->setContentsMargins(2, 2, 2, 2); layout->setSpacing(6); { QString themeName = KMyMoneySettings::iconsTheme(); // get theme user wants if (!themeName.isEmpty() && themeName != QLatin1Literal("system")) // if it isn't default theme then set it QIcon::setThemeName(themeName); Icons::setIconThemeNames(QIcon::themeName()); // get whatever theme user ends up with and hope our icon names will fit that theme } initStatusBar(); initActions(); initDynamicMenus(); d->m_myMoneyView = new KMyMoneyView(this/*the global variable kmymoney is not yet assigned. So we pass it here*/); layout->addWidget(d->m_myMoneyView, 10); connect(d->m_myMoneyView, &KMyMoneyView::aboutToChangeView, this, &KMyMoneyApp::slotResetSelections); connect(d->m_myMoneyView, SIGNAL(currentPageChanged(KPageWidgetItem*,KPageWidgetItem*)), this, SLOT(slotUpdateActions())); connectActionsAndViews(); /////////////////////////////////////////////////////////////////// // call inits to invoke all other construction parts readOptions(); // now initialize the plugin structure createInterfaces(); loadPlugins(); setCentralWidget(frame); connect(&d->m_proc, SIGNAL(finished(int,QProcess::ExitStatus)), this, SLOT(slotBackupHandleEvents())); // force to show the home page if the file is closed connect(actionCollection()->action(s_Actions[Action::ViewTransactionDetail]), &QAction::toggled, d->m_myMoneyView, &KMyMoneyView::slotShowTransactionDetail); d->m_backupState = BACKUP_IDLE; QLocale locale; int weekStart = locale.firstDayOfWeek(); int weekEnd = weekStart-1; if (weekEnd < Qt::Monday) { weekEnd = Qt::Sunday; } bool startFirst = (weekStart < weekEnd); for (int i = 0; i < 8; i++) { if (startFirst) d->m_processingDays.setBit(i, (i >= weekStart && i <= weekEnd)); else d->m_processingDays.setBit(i, (i >= weekStart || i <= weekEnd)); } d->m_autoSaveTimer = new QTimer(this); d->m_progressTimer = new QTimer(this); connect(d->m_autoSaveTimer, SIGNAL(timeout()), this, SLOT(slotAutoSave())); connect(d->m_progressTimer, SIGNAL(timeout()), this, SLOT(slotStatusProgressDone())); // make sure, we get a note when the engine changes state connect(MyMoneyFile::instance(), SIGNAL(dataChanged()), this, SLOT(slotDataChanged())); // connect the WebConnect server connect(d->m_webConnect, SIGNAL(gotUrl(QUrl)), this, SLOT(webConnect(QUrl))); // make sure we have a balance warning object d->m_balanceWarning = new KBalanceWarning(this); // setup the initial configuration slotUpdateConfiguration(); // kickstart date change timer slotDateChanged(); connect(this, SIGNAL(fileLoaded(QUrl)), onlineJobAdministration::instance(), SLOT(updateOnlineTaskProperties())); } KMyMoneyApp::~KMyMoneyApp() { // delete cached objects since the are in the way // when unloading the plugins onlineJobAdministration::instance()->clearCaches(); // we need to unload all plugins before we destroy anything else unloadPlugins(); delete d->m_searchDlg; delete d->m_transactionEditor; delete d->m_endingBalanceDlg; delete d->m_moveToAccountSelector; #ifdef KF5Holidays_FOUND delete d->m_holidayRegion; #endif delete d; } QUrl KMyMoneyApp::lastOpenedURL() { QUrl url = d->m_startDialog ? QUrl() : d->m_fileName; if (!url.isValid()) { url = QUrl::fromUserInput(readLastUsedFile()); } ready(); return url; } void KMyMoneyApp::slotObjectDestroyed(QObject* o) { if (o == d->m_moveToAccountSelector) { d->m_moveToAccountSelector = 0; } } void KMyMoneyApp::slotInstallConsistencyCheckContextMenu() { // this code relies on the implementation of KMessageBox::informationList to add a context menu to that list, // please adjust it if it's necessary or rewrite the way the consistency check results are displayed if (QWidget* dialog = QApplication::activeModalWidget()) { if (QListWidget* widget = dialog->findChild()) { // give the user a hint that the data can be saved widget->setToolTip(i18n("This is the consistency check log, use the context menu to copy or save it.")); widget->setWhatsThis(widget->toolTip()); widget->setContextMenuPolicy(Qt::CustomContextMenu); connect(widget, SIGNAL(customContextMenuRequested(QPoint)), SLOT(slotShowContextMenuForConsistencyCheck(QPoint))); } } } void KMyMoneyApp::slotShowContextMenuForConsistencyCheck(const QPoint &pos) { // allow the user to save the consistency check results if (QWidget* widget = qobject_cast< QWidget* >(sender())) { QMenu contextMenu(widget); QAction* copy = new QAction(i18n("Copy to clipboard"), widget); QAction* save = new QAction(i18n("Save to file"), widget); contextMenu.addAction(copy); contextMenu.addAction(save); QAction *result = contextMenu.exec(widget->mapToGlobal(pos)); if (result == copy) { // copy the consistency check results to the clipboard d->copyConsistencyCheckResults(); } else if (result == save) { // save the consistency check results to a file d->saveConsistencyCheckResults(); } } } void KMyMoneyApp::createTransactionMoveMenu() { if (!d->m_moveToAccountSelector) { QWidget* w = factory()->container("transaction_move_menu", this); QMenu *menu = dynamic_cast(w); if (menu) { QWidgetAction *accountSelectorAction = new QWidgetAction(menu); d->m_moveToAccountSelector = new KMyMoneyAccountSelector(menu, 0, false); d->m_moveToAccountSelector->setObjectName("transaction_move_menu_selector"); accountSelectorAction->setDefaultWidget(d->m_moveToAccountSelector); menu->addAction(accountSelectorAction); connect(d->m_moveToAccountSelector, SIGNAL(destroyed(QObject*)), this, SLOT(slotObjectDestroyed(QObject*))); connect(d->m_moveToAccountSelector, SIGNAL(itemSelected(QString)), this, SLOT(slotMoveToAccount(QString))); } } } void KMyMoneyApp::initDynamicMenus() { connect(this, SIGNAL(accountSelected(MyMoneyAccount)), this, SLOT(slotUpdateMoveToAccountMenu())); connect(this, SIGNAL(transactionsSelected(KMyMoneyRegister::SelectedTransactions)), this, SLOT(slotUpdateMoveToAccountMenu())); connect(MyMoneyFile::instance(), SIGNAL(dataChanged()), this, SLOT(slotUpdateMoveToAccountMenu())); } void KMyMoneyApp::initActions() { KActionCollection *aC = actionCollection(); // ************* // Adding standard actions // ************* KStandardAction::openNew(this, &KMyMoneyApp::slotFileNew, aC); KStandardAction::open(this, &KMyMoneyApp::slotFileOpen, aC); d->m_recentFiles = KStandardAction::openRecent(this, &KMyMoneyApp::slotFileOpenRecent, aC); KStandardAction::save(this, &KMyMoneyApp::slotFileSave, aC); KStandardAction::saveAs(this, &KMyMoneyApp::slotFileSaveAs, aC); KStandardAction::close(this, &KMyMoneyApp::slotFileClose, aC); KStandardAction::quit(this, &KMyMoneyApp::slotFileQuit, aC); KStandardAction::print(this, &KMyMoneyApp::slotPrintView, aC); KStandardAction::preferences(this, &KMyMoneyApp::slotSettings, aC); /* Look-up table for all custom actions. It's required for: 1) building QList with QActions to be added to ActionCollection 2) adding custom features to QActions like e.g. overlaid icon or keyboard shortcut */ QHash lutActions; // ************* // Adding all actions // ************* { // struct for QAction's vital (except icon) information struct actionInfo { Action action; KMyMoneyAppFunc callback; QString text; Icon icon; }; const QVector actionInfos { // ************* // The File menu // ************* {Action::FileOpenDatabase, &KMyMoneyApp::slotOpenDatabase, i18n("Open database..."), Icon::SVNUpdate}, {Action::FileSaveAsDatabase, &KMyMoneyApp::slotSaveAsDatabase, i18n("Save as database..."), Icon::FileArchiver}, {Action::FileBackup, &KMyMoneyApp::slotBackupFile, i18n("Backup..."), Icon::Empty}, {Action::FileImportStatement, &KMyMoneyApp::slotStatementImport, i18n("Statement file..."), Icon::Empty}, {Action::FileImportTemplate, &KMyMoneyApp::slotLoadAccountTemplates, i18n("Account Template..."), Icon::Empty}, {Action::FileExportTemplate, &KMyMoneyApp::slotSaveAccountTemplates, i18n("Account Template..."), Icon::Empty}, {Action::FilePersonalData, &KMyMoneyApp::slotFileViewPersonal, i18n("Personal Data..."), Icon::UserProperties}, #ifdef KMM_DEBUG {Action::FileDump, &KMyMoneyApp::slotFileFileInfo, i18n("Dump Memory"), Icon::Empty}, #endif {Action::FileInformation, &KMyMoneyApp::slotFileInfoDialog, i18n("File-Information..."), Icon::DocumentProperties}, // ************* // The Edit menu // ************* {Action::EditFindTransaction, &KMyMoneyApp::slotFindTransaction, i18n("Find transaction..."), Icon::Empty}, // ************* // The View menu // ************* {Action::ViewTransactionDetail, &KMyMoneyApp::slotShowTransactionDetail, i18n("Show Transaction Detail"), Icon::ViewTransactionDetail}, {Action::ViewHideReconciled, &KMyMoneyApp::slotHideReconciledTransactions, i18n("Hide reconciled transactions"), Icon::HideReconciled}, {Action::ViewHideCategories, &KMyMoneyApp::slotHideUnusedCategories, i18n("Hide unused categories"), Icon::HideCategories}, {Action::ViewShowAll, &KMyMoneyApp::slotShowAllAccounts, i18n("Show all accounts"), Icon::Empty}, // ********************* // The institutions menu // ********************* {Action::InstitutionNew, &KMyMoneyApp::slotInstitutionNew, i18n("New institution..."), Icon::Empty}, {Action::InstitutionEdit, &KMyMoneyApp::slotInstitutionEditEmpty, i18n("Edit institution..."), Icon::Empty}, {Action::InstitutionDelete, &KMyMoneyApp::slotInstitutionDelete, i18n("Delete institution..."), Icon::Empty}, // ***************** // The accounts menu // ***************** {Action::AccountNew, &KMyMoneyApp::slotAccountNew, i18n("New account..."), Icon::Empty}, {Action::AccountOpen, &KMyMoneyApp::slotAccountOpenEmpty, i18n("Open ledger"), Icon::ViewFinancialList}, {Action::AccountStartReconciliation, &KMyMoneyApp::slotAccountReconcileStart, i18n("Reconcile..."), Icon::Reconcile}, {Action::AccountFinishReconciliation, &KMyMoneyApp::slotAccountReconcileFinish, i18nc("Finish reconciliation", "Finish"), Icon::Empty}, {Action::AccountPostponeReconciliation, &KMyMoneyApp::slotAccountReconcilePostpone, i18n("Postpone reconciliation"), Icon::MediaPlaybackPause}, {Action::AccountEdit, &KMyMoneyApp::slotAccountEdit, i18n("Edit account..."), Icon::Empty}, {Action::AccountDelete, &KMyMoneyApp::slotAccountDelete, i18n("Delete account..."), Icon::Empty}, {Action::AccountClose, &KMyMoneyApp::slotAccountClose, i18n("Close account"), Icon::Empty}, {Action::AccountReopen, &KMyMoneyApp::slotAccountReopen, i18n("Reopen account"), Icon::Empty}, {Action::AccountTransactionReport, &KMyMoneyApp::slotAccountTransactionReport, i18n("Transaction report"), Icon::ViewFinancialList}, {Action::AccountBalanceChart, &KMyMoneyApp::slotAccountChart, i18n("Show balance chart..."), Icon::OfficeChartLine}, {Action::AccountOnlineMap, &KMyMoneyApp::slotAccountMapOnline, i18n("Map account..."), Icon::NewsSubscribe}, {Action::AccountOnlineUnmap, &KMyMoneyApp::slotAccountUnmapOnline, i18n("Unmap account..."), Icon::NewsUnsubscribe}, {Action::AccountUpdateMenu, &KMyMoneyApp::slotAccountUpdateOnline, i18nc("Update online accounts menu", "Update"), Icon::Empty}, {Action::AccountUpdate, &KMyMoneyApp::slotAccountUpdateOnline, i18n("Update account..."), Icon::Empty}, {Action::AccountUpdateAll, &KMyMoneyApp::slotAccountUpdateOnlineAll, i18n("Update all accounts..."), Icon::Empty}, {Action::AccountCreditTransfer, &KMyMoneyApp::slotNewOnlineTransfer, i18n("New credit transfer"), Icon::Empty}, // ******************* // The categories menu // ******************* {Action::CategoryNew, &KMyMoneyApp::slotCategoryNew, i18n("New category..."), Icon::Empty}, {Action::CategoryEdit, &KMyMoneyApp::slotAccountEdit, i18n("Edit category..."), Icon::Empty}, {Action::CategoryDelete, &KMyMoneyApp::slotAccountDelete, i18n("Delete category..."), Icon::Empty}, // ************** // The tools menu // ************** {Action::ToolCurrencies, &KMyMoneyApp::slotCurrencyDialog, i18n("Currencies..."), Icon::ViewCurrencyList}, {Action::ToolPrices, &KMyMoneyApp::slotPriceDialog, i18n("Prices..."), Icon::Empty}, {Action::ToolUpdatePrices, &KMyMoneyApp::slotEquityPriceUpdate, i18n("Update Stock and Currency Prices..."), Icon::Empty}, {Action::ToolConsistency, &KMyMoneyApp::slotFileConsistencyCheck, i18n("Consistency Check"), Icon::Empty}, {Action::ToolPerformance, &KMyMoneyApp::slotPerformanceTest, i18n("Performance-Test"), Icon::Fork}, {Action::ToolSQL, &KMyMoneyApp::slotGenerateSql, i18n("Generate Database SQL"), Icon::Empty}, {Action::ToolCalculator, &KMyMoneyApp::slotToolsStartKCalc, i18n("Calculator..."), Icon::AccessoriesCalculator}, // ***************** // The settings menu // ***************** {Action::SettingsAllMessages, &KMyMoneyApp::slotEnableMessages, i18n("Enable all messages"), Icon::Empty}, // ************* // The help menu // ************* {Action::HelpShow, &KMyMoneyApp::slotShowTipOfTheDay, i18n("&Show tip of the day"), Icon::Tip}, // *************************** // Actions w/o main menu entry // *************************** {Action::TransactionNew, &KMyMoneyApp::slotTransactionsNew, i18nc("New transaction button", "New"), Icon::Empty}, {Action::TransactionEdit, &KMyMoneyApp::slotTransactionsEdit, i18nc("Edit transaction button", "Edit"), Icon::Empty}, {Action::TransactionEnter, &KMyMoneyApp::slotTransactionsEnter, i18nc("Enter transaction", "Enter"), Icon::DialogOK}, {Action::TransactionEditSplits, &KMyMoneyApp::slotTransactionsEditSplits, i18nc("Edit split button", "Edit splits"), Icon::Split}, {Action::TransactionCancel, &KMyMoneyApp::slotTransactionsCancel, i18nc("Cancel transaction edit", "Cancel"), Icon::DialogCancel}, {Action::TransactionDelete, &KMyMoneyApp::slotTransactionsDelete, i18nc("Delete transaction", "Delete"), Icon::EditDelete}, {Action::TransactionDuplicate, &KMyMoneyApp::slotTransactionDuplicate, i18nc("Duplicate transaction", "Duplicate"), Icon::EditCopy}, {Action::TransactionMatch, &KMyMoneyApp::slotTransactionMatch, i18nc("Button text for match transaction", "Match"),Icon::Empty}, {Action::TransactionAccept, &KMyMoneyApp::slotTransactionsAccept, i18nc("Accept 'imported' and 'matched' transaction", "Accept"), Icon::Empty}, {Action::TransactionToggleReconciled, &KMyMoneyApp::slotToggleReconciliationFlag, i18nc("Toggle reconciliation flag", "Toggle"), Icon::Empty}, {Action::TransactionToggleCleared, &KMyMoneyApp::slotMarkTransactionCleared, i18nc("Mark transaction cleared", "Cleared"), Icon::Empty}, {Action::TransactionReconciled, &KMyMoneyApp::slotMarkTransactionReconciled, i18nc("Mark transaction reconciled", "Reconciled"), Icon::Empty}, {Action::TransactionNotReconciled, &KMyMoneyApp::slotMarkTransactionNotReconciled, i18nc("Mark transaction not reconciled", "Not reconciled"), Icon::Empty}, {Action::TransactionSelectAll, &KMyMoneyApp::selectAllTransactions, i18nc("Select all transactions", "Select all"), Icon::Empty}, {Action::TransactionGoToAccount, &KMyMoneyApp::slotTransactionGotoAccount, i18n("Go to account"), Icon::GoJump}, {Action::TransactionGoToPayee, &KMyMoneyApp::slotTransactionGotoPayee, i18n("Go to payee"), Icon::GoJump}, {Action::TransactionCreateSchedule, &KMyMoneyApp::slotTransactionCreateSchedule, i18n("Create scheduled transaction..."), Icon::AppointmentNew}, {Action::TransactionAssignNumber, &KMyMoneyApp::slotTransactionAssignNumber, i18n("Assign next number"), Icon::Empty}, {Action::TransactionCombine, &KMyMoneyApp::slotTransactionCombine, i18nc("Combine transactions", "Combine"), Icon::Empty}, {Action::TransactionCopySplits, &KMyMoneyApp::slotTransactionCopySplits, i18n("Copy splits"), Icon::Empty}, //Investment {Action::InvestmentNew, &KMyMoneyApp::slotInvestmentNew, i18n("New investment..."), Icon::Empty}, {Action::InvestmentEdit, &KMyMoneyApp::slotInvestmentEdit, i18n("Edit investment..."), Icon::Empty}, {Action::InvestmentDelete, &KMyMoneyApp::slotInvestmentDelete, i18n("Delete investment..."), Icon::Empty}, {Action::InvestmentOnlinePrice, &KMyMoneyApp::slotOnlinePriceUpdate, i18n("Online price update..."), Icon::Empty}, {Action::InvestmentManualPrice, &KMyMoneyApp::slotManualPriceUpdate, i18n("Manual price update..."), Icon::Empty}, //Schedule {Action::ScheduleNew, &KMyMoneyApp::slotScheduleNew, i18n("New scheduled transaction"), Icon::AppointmentNew}, {Action::ScheduleEdit, &KMyMoneyApp::slotScheduleEdit, i18n("Edit scheduled transaction"), Icon::DocumentEdit}, {Action::ScheduleDelete, &KMyMoneyApp::slotScheduleDelete, i18n("Delete scheduled transaction"), Icon::EditDelete}, {Action::ScheduleDuplicate, &KMyMoneyApp::slotScheduleDuplicate, i18n("Duplicate scheduled transaction"), Icon::EditCopy}, {Action::ScheduleEnter, &KMyMoneyApp::slotScheduleEnter, i18n("Enter next transaction..."), Icon::KeyEnter}, {Action::ScheduleSkip, &KMyMoneyApp::slotScheduleSkip, i18n("Skip next transaction..."), Icon::MediaSeekForward}, //Payees {Action::PayeeNew, &KMyMoneyApp::slotPayeeNew, i18n("New payee"), Icon::ListAddUser}, {Action::PayeeRename, &KMyMoneyApp::payeeRename, i18n("Rename payee"), Icon::PayeeRename}, {Action::PayeeDelete, &KMyMoneyApp::slotPayeeDelete, i18n("Delete payee"), Icon::ListRemoveUser}, {Action::PayeeMerge, &KMyMoneyApp::slotPayeeMerge, i18n("Merge payees"), Icon::PayeeMerge}, //Tags {Action::TagNew, &KMyMoneyApp::slotTagNew, i18n("New tag"), Icon::ListAddTag}, {Action::TagRename, &KMyMoneyApp::slotTagRename, i18n("Rename tag"), Icon::TagRename}, {Action::TagDelete, &KMyMoneyApp::slotTagDelete, i18n("Delete tag"), Icon::ListRemoveTag}, //Budget {Action::BudgetNew, &KMyMoneyApp::slotBudgetNew, i18n("New budget"), Icon::Empty}, {Action::BudgetRename, &KMyMoneyApp::budgetRename, i18n("Rename budget"), Icon::Empty}, {Action::BudgetDelete, &KMyMoneyApp::slotBudgetDelete, i18n("Delete budget"), Icon::Empty}, {Action::BudgetCopy, &KMyMoneyApp::slotBudgetCopy, i18n("Copy budget"), Icon::Empty}, {Action::BudgetChangeYear, &KMyMoneyApp::slotBudgetChangeYear, i18n("Change budget year"), Icon::ViewCalendar}, {Action::BudgetForecast, &KMyMoneyApp::slotBudgetForecast, i18n("Budget based on forecast"), Icon::ViewForecast}, //Currency actions {Action::CurrencyNew, &KMyMoneyApp::slotCurrencyNew, i18n("New currency"), Icon::Empty}, {Action::CurrencyRename, &KMyMoneyApp::currencyRename, i18n("Rename currency"), Icon::EditRename}, {Action::CurrencyDelete, &KMyMoneyApp::slotCurrencyDelete, i18n("Delete currency"), Icon::EditDelete}, {Action::CurrencySetBase, &KMyMoneyApp::slotCurrencySetBase, i18n("Select as base currency"), Icon::KMyMoney}, //Price actions {Action::PriceNew, &KMyMoneyApp::priceNew, i18n("New price..."), Icon::DocumentNew}, {Action::PriceEdit, &KMyMoneyApp::priceEdit, i18n("Edit price..."), Icon::DocumentEdit}, {Action::PriceUpdate, &KMyMoneyApp::priceOnlineUpdate, i18n("Online Price Update..."), Icon::Empty}, {Action::PriceDelete, &KMyMoneyApp::priceDelete, i18n("Delete price..."), Icon::EditDelete}, //debug actions #ifdef KMM_DEBUG {Action::WizardNewUser, &KMyMoneyApp::slotNewFeature, i18n("Test new feature"), Icon::Empty}, {Action::DebugTraces, &KMyMoneyApp::slotToggleTraces, i18n("Debug Traces"), Icon::Empty}, #endif {Action::DebugTimers, &KMyMoneyApp::slotToggleTimers, i18n("Debug Timers"), Icon::Empty}, // onlineJob actions {Action::OnlineJobDelete, &KMyMoneyApp::slotRemoveJob, i18n("Remove credit transfer"), Icon::EditDelete}, {Action::OnlineJobEdit, &KMyMoneyApp::slotEditJob, i18n("Edit credit transfer"), Icon::DocumentEdit}, {Action::OnlineJobLog, &KMyMoneyApp::slotOnlineJobLog, i18n("Show log"), Icon::Empty}, }; foreach (const auto info, actionInfos) { QAction *a = new QAction(0); // KActionCollection::addAction by name sets object name anyways, // so, as better alternative, set it here right from the start a->setObjectName(s_Actions.value(info.action)); a->setText(info.text); if (info.icon != Icon::Empty) // no need to set empty icon a->setIcon(QIcon::fromTheme(g_Icons.value(info.icon))); connect(a, &QAction::triggered, this, info.callback); lutActions.insert(info.action, a); // store QAction's pointer for later processing } } // ************* // Setting some of added actions checkable // ************* { // Some of acitions schould be checkable, // so set them here const QVector checkableActions { Action::ViewTransactionDetail, Action::ViewHideReconciled, Action::ViewHideCategories, #ifdef KMM_DEBUG Action::DebugTraces, #endif Action::ViewShowAll }; foreach (const auto it, checkableActions) lutActions[it]->setCheckable(true); } // ************* // Setting custom icons for some of added actions // ************* { // struct for QAction's icon overlays struct iconOverlayInfo { Action action; Icon icon; Icon overlay; Qt::Corner corner; }; const QVector actionOverlaidIcons { {Action::EditFindTransaction, Icon::ViewFinancialTransfer, Icon::EditFind, Qt::BottomRightCorner}, {Action::InstitutionNew, Icon::ViewBank, Icon::ListAdd, Qt::BottomRightCorner}, {Action::InstitutionEdit, Icon::ViewBank, Icon::DocumentEdit, Qt::BottomRightCorner}, {Action::InstitutionDelete, Icon::ViewBank, Icon::EditDelete, Qt::BottomRightCorner}, {Action::AccountNew, Icon::ViewBankAccount, Icon::ListAdd, Qt::TopRightCorner}, {Action::AccountFinishReconciliation, Icon::Merge, Icon::DialogOK, Qt::BottomRightCorner}, {Action::AccountEdit, Icon::ViewBankAccount, Icon::DocumentEdit, Qt::BottomRightCorner}, {Action::AccountDelete, Icon::ViewBankAccount, Icon::EditDelete, Qt::BottomRightCorner}, {Action::AccountClose, Icon::ViewBankAccount, Icon::DialogClose, Qt::BottomRightCorner}, {Action::AccountReopen, Icon::ViewBankAccount, Icon::DialogOK, Qt::BottomRightCorner}, {Action::AccountUpdateMenu, Icon::ViewBankAccount, Icon::Download, Qt::BottomRightCorner}, {Action::AccountUpdate, Icon::ViewBankAccount, Icon::Download, Qt::BottomRightCorner}, {Action::AccountUpdateAll, Icon::ViewBankAccount, Icon::Download, Qt::BottomRightCorner}, {Action::AccountCreditTransfer, Icon::ViewBankAccount, Icon::MailMessageNew, Qt::BottomRightCorner}, {Action::CategoryNew, Icon::ViewFinancialCategories, Icon::ListAdd, Qt::TopRightCorner}, {Action::CategoryEdit, Icon::ViewFinancialCategories, Icon::DocumentEdit, Qt::BottomRightCorner}, {Action::CategoryDelete, Icon::ViewFinancialCategories, Icon::EditDelete, Qt::BottomRightCorner}, {Action::ToolUpdatePrices, Icon::ViewInvestment, Icon::Download, Qt::BottomRightCorner}, {Action::TransactionNew, Icon::ViewFinancialTransfer, Icon::ListAdd, Qt::TopRightCorner}, {Action::TransactionEdit, Icon::ViewFinancialTransfer, Icon::DocumentEdit, Qt::BottomRightCorner}, {Action::TransactionMatch, Icon::ViewFinancialTransfer, Icon::DocumentImport, Qt::BottomRightCorner}, {Action::TransactionAccept, Icon::ViewFinancialTransfer, Icon::DialogOKApply, Qt::BottomRightCorner}, {Action::InvestmentNew, Icon::ViewInvestment, Icon::ListAdd, Qt::TopRightCorner}, {Action::InvestmentEdit, Icon::ViewInvestment, Icon::DocumentEdit, Qt::BottomRightCorner}, {Action::InvestmentDelete, Icon::ViewInvestment, Icon::EditDelete, Qt::BottomRightCorner}, {Action::InvestmentOnlinePrice, Icon::ViewInvestment, Icon::Download, Qt::BottomRightCorner}, {Action::BudgetNew, Icon::ViewTimeScheduleCalculus, Icon::ListAdd, Qt::TopRightCorner}, {Action::BudgetRename, Icon::ViewTimeScheduleCalculus, Icon::DocumentEdit, Qt::BottomRightCorner}, {Action::BudgetDelete, Icon::ViewTimeScheduleCalculus, Icon::EditDelete, Qt::BottomRightCorner}, {Action::BudgetCopy, Icon::ViewTimeScheduleCalculus, Icon::EditCopy, Qt::BottomRightCorner}, {Action::PriceUpdate, Icon::ViewCurrencyList, Icon::Download, Qt::BottomRightCorner} }; for(const auto& it : actionOverlaidIcons) lutActions[it.action]->setIcon(KMyMoneyUtils::overlayIcon(g_Icons[it.icon], g_Icons[it.overlay], it.corner)); } // ************* // Setting keyboard shortcuts for some of added actions // ************* { const QVector> actionShortcuts { {qMakePair(Action::EditFindTransaction, Qt::CTRL + Qt::Key_F)}, {qMakePair(Action::ViewTransactionDetail, Qt::CTRL + Qt::Key_T)}, {qMakePair(Action::ViewHideReconciled, Qt::CTRL + Qt::Key_R)}, {qMakePair(Action::ViewHideCategories, Qt::CTRL + Qt::Key_U)}, {qMakePair(Action::ViewShowAll, Qt::CTRL + Qt::SHIFT + Qt::Key_A)}, {qMakePair(Action::AccountStartReconciliation, Qt::CTRL + Qt::SHIFT + Qt::Key_R)}, {qMakePair(Action::TransactionNew, Qt::CTRL + Qt::Key_Insert)}, {qMakePair(Action::TransactionToggleReconciled, Qt::CTRL + Qt::Key_Space)}, {qMakePair(Action::TransactionToggleCleared, Qt::CTRL + Qt::Key_Alt + Qt::Key_Space)}, {qMakePair(Action::TransactionReconciled, Qt::CTRL + Qt::Key_Shift + Qt::Key_Space)}, {qMakePair(Action::TransactionSelectAll, Qt::CTRL + Qt::Key_A)}, #ifdef KMM_DEBUG {qMakePair(Action::WizardNewUser, Qt::CTRL + Qt::Key_G)}, #endif {qMakePair(Action::TransactionAssignNumber, Qt::CTRL + Qt::Key_Shift + Qt::Key_N)} }; for(const auto& it : actionShortcuts) aC->setDefaultShortcut(lutActions[it.first], it.second); } // ************* // Misc settings // ************* connect(onlineJobAdministration::instance(), &onlineJobAdministration::canSendCreditTransferChanged, lutActions.value(Action::AccountCreditTransfer), &QAction::setEnabled); // Setup transaction detail switch lutActions[Action::ViewTransactionDetail]->setChecked(KMyMoneyGlobalSettings::showRegisterDetailed()); lutActions[Action::ViewHideReconciled]->setChecked(KMyMoneyGlobalSettings::hideReconciledTransactions()); lutActions[Action::ViewHideCategories]->setChecked(KMyMoneyGlobalSettings::hideUnusedCategory()); lutActions[Action::ViewShowAll]->setChecked(false); // ************* // Adding actions to ActionCollection // ************* actionCollection()->addActions(lutActions.values()); // ************************ // Currently unused actions // ************************ #if 0 new KToolBarPopupAction(i18n("View back"), "go-previous", 0, this, SLOT(slotShowPreviousView()), actionCollection(), "go_back"); new KToolBarPopupAction(i18n("View forward"), "go-next", 0, this, SLOT(slotShowNextView()), actionCollection(), "go_forward"); action("go_back")->setEnabled(false); action("go_forward")->setEnabled(false); #endif // use the absolute path to your kmymoneyui.rc file for testing purpose in createGUI(); setupGUI(); // reconnect about app entry to dialog with full credits information QAction *aboutApp = aC->action(QString::fromLatin1(KStandardAction::name(KStandardAction::AboutApp))); aboutApp->disconnect(); connect(aboutApp, &QAction::triggered, this, &KMyMoneyApp::slotShowCredits); QMenu *menuContainer; menuContainer = static_cast(factory()->container(QStringLiteral("import"), this)); menuContainer->setIcon(QIcon::fromTheme(g_Icons[Icon::DocumentImport])); menuContainer = static_cast(factory()->container(QStringLiteral("export"), this)); menuContainer->setIcon(QIcon::fromTheme(g_Icons[Icon::DocumentExport])); } void KMyMoneyApp::connectActionsAndViews() { KOnlineJobOutbox *const outbox = d->m_myMoneyView->getOnlineJobOutbox(); Q_CHECK_PTR(outbox); QAction *const onlineJob_delete = actionCollection()->action(s_Actions[Action::OnlineJobDelete]); Q_CHECK_PTR(onlineJob_delete); connect(onlineJob_delete, SIGNAL(triggered()), outbox, SLOT(slotRemoveJob())); QAction *const onlineJob_edit = actionCollection()->action(s_Actions[Action::OnlineJobEdit]); Q_CHECK_PTR(onlineJob_edit); connect(onlineJob_edit, SIGNAL(triggered()), outbox, SLOT(slotEditJob())); } #ifdef KMM_DEBUG void KMyMoneyApp::dumpActions() const { const QList list = actionCollection()->actions(); foreach (const auto it, list) std::cout << qPrintable(it->objectName()) << ": " << qPrintable(it->text()) << std::endl; } #endif bool KMyMoneyApp::isActionToggled(const Action _a) { return actionCollection()->action(s_Actions[_a])->isChecked(); } void KMyMoneyApp::initStatusBar() { /////////////////////////////////////////////////////////////////// // STATUSBAR d->m_statusLabel = new QLabel; statusBar()->addWidget(d->m_statusLabel); ready(); // Initialization of progress bar taken from KDevelop ;-) d->m_progressBar = new QProgressBar; statusBar()->addWidget(d->m_progressBar); d->m_progressBar->setFixedHeight(d->m_progressBar->sizeHint().height() - 8); // hide the progress bar for now slotStatusProgressBar(-1, -1); } void KMyMoneyApp::saveOptions() { KConfigGroup grp = d->m_config->group("General Options"); grp.writeEntry("Geometry", size()); grp.writeEntry("Show Statusbar", actionCollection()->action(KStandardAction::name(KStandardAction::ShowStatusbar))->isChecked()); KConfigGroup toolbarGrp = d->m_config->group("mainToolBar"); toolBar("mainToolBar")->saveSettings(toolbarGrp); d->m_recentFiles->saveEntries(d->m_config->group("Recent Files")); } void KMyMoneyApp::readOptions() { KConfigGroup grp = d->m_config->group("General Options"); actionCollection()->action(s_Actions[Action::ViewHideReconciled])->setChecked(KMyMoneyGlobalSettings::hideReconciledTransactions()); actionCollection()->action(s_Actions[Action::ViewHideCategories])->setChecked(KMyMoneyGlobalSettings::hideUnusedCategory()); d->m_recentFiles->loadEntries(d->m_config->group("Recent Files")); // Startdialog is written in the settings dialog d->m_startDialog = grp.readEntry("StartDialog", true); } void KMyMoneyApp::resizeEvent(QResizeEvent* ev) { KMainWindow::resizeEvent(ev); updateCaption(true); } int KMyMoneyApp::askSaveOnClose() { int ans; if (KMyMoneyGlobalSettings::autoSaveOnClose()) { ans = KMessageBox::Yes; } else { ans = KMessageBox::warningYesNoCancel(this, i18n("The file has been changed, save it?")); } return ans; } bool KMyMoneyApp::queryClose() { if (!isReady()) return false; if (d->m_myMoneyView->dirty()) { int ans = askSaveOnClose(); if (ans == KMessageBox::Cancel) return false; else if (ans == KMessageBox::Yes) { bool saved = slotFileSave(); saveOptions(); return saved; } } if (d->m_myMoneyView->isDatabase()) slotFileClose(); // close off the database saveOptions(); return true; } ///////////////////////////////////////////////////////////////////// // SLOT IMPLEMENTATION ///////////////////////////////////////////////////////////////////// void KMyMoneyApp::slotFileInfoDialog() { QPointer dlg = new KMyMoneyFileInfoDlg(0); dlg->exec(); delete dlg; } void KMyMoneyApp::slotPerformanceTest() { // dump performance report to stderr int measurement[2]; QTime timer; MyMoneyAccount acc; qDebug("--- Starting performance tests ---"); // AccountList MyMoneyFile::instance()->preloadCache(); measurement[0] = measurement[1] = 0; timer.start(); for (int i = 0; i < 1000; ++i) { QList list; MyMoneyFile::instance()->accountList(list); measurement[i != 0] = timer.elapsed(); } std::cerr << "accountList()" << std::endl; std::cerr << "First time: " << measurement[0] << " msec" << std::endl; std::cerr << "Total time: " << (measurement[0] + measurement[1]) << " msec" << std::endl; std::cerr << "Average : " << (measurement[0] + measurement[1]) / 1000 << " msec" << std::endl; // Balance of asset account(s) MyMoneyFile::instance()->preloadCache(); measurement[0] = measurement[1] = 0; acc = MyMoneyFile::instance()->asset(); for (int i = 0; i < 1000; ++i) { timer.start(); MyMoneyMoney result = MyMoneyFile::instance()->balance(acc.id()); measurement[i != 0] += timer.elapsed(); } std::cerr << "balance(Asset)" << std::endl; std::cerr << "First time: " << measurement[0] << " msec" << std::endl; std::cerr << "Average : " << (measurement[0] + measurement[1]) / 1000 << " msec" << std::endl; // total balance of asset account MyMoneyFile::instance()->preloadCache(); measurement[0] = measurement[1] = 0; acc = MyMoneyFile::instance()->asset(); for (int i = 0; i < 1000; ++i) { timer.start(); MyMoneyMoney result = MyMoneyFile::instance()->totalBalance(acc.id()); measurement[i != 0] += timer.elapsed(); } std::cerr << "totalBalance(Asset)" << std::endl; std::cerr << "First time: " << measurement[0] << " msec" << std::endl; std::cerr << "Average : " << (measurement[0] + measurement[1]) / 1000 << " msec" << std::endl; // Balance of expense account(s) MyMoneyFile::instance()->preloadCache(); measurement[0] = measurement[1] = 0; acc = MyMoneyFile::instance()->expense(); for (int i = 0; i < 1000; ++i) { timer.start(); MyMoneyMoney result = MyMoneyFile::instance()->balance(acc.id()); measurement[i != 0] += timer.elapsed(); } std::cerr << "balance(Expense)" << std::endl; std::cerr << "First time: " << measurement[0] << " msec" << std::endl; std::cerr << "Average : " << (measurement[0] + measurement[1]) / 1000 << " msec" << std::endl; // total balance of expense account MyMoneyFile::instance()->preloadCache(); measurement[0] = measurement[1] = 0; acc = MyMoneyFile::instance()->expense(); timer.start(); for (int i = 0; i < 1000; ++i) { MyMoneyMoney result = MyMoneyFile::instance()->totalBalance(acc.id()); measurement[i != 0] = timer.elapsed(); } std::cerr << "totalBalance(Expense)" << std::endl; std::cerr << "First time: " << measurement[0] << " msec" << std::endl; std::cerr << "Total time: " << (measurement[0] + measurement[1]) << " msec" << std::endl; std::cerr << "Average : " << (measurement[0] + measurement[1]) / 1000 << " msec" << std::endl; // transaction list MyMoneyFile::instance()->preloadCache(); measurement[0] = measurement[1] = 0; if (MyMoneyFile::instance()->asset().accountCount()) { MyMoneyTransactionFilter filter(MyMoneyFile::instance()->asset().accountList()[0]); filter.setDateFilter(QDate(), QDate::currentDate()); QList list; timer.start(); for (int i = 0; i < 100; ++i) { list = MyMoneyFile::instance()->transactionList(filter); measurement[i != 0] = timer.elapsed(); } std::cerr << "transactionList()" << std::endl; std::cerr << "First time: " << measurement[0] << " msec" << std::endl; std::cerr << "Total time: " << (measurement[0] + measurement[1]) << " msec" << std::endl; std::cerr << "Average : " << (measurement[0] + measurement[1]) / 100 << " msec" << std::endl; } // transaction list MyMoneyFile::instance()->preloadCache(); measurement[0] = measurement[1] = 0; if (MyMoneyFile::instance()->asset().accountCount()) { MyMoneyTransactionFilter filter(MyMoneyFile::instance()->asset().accountList()[0]); filter.setDateFilter(QDate(), QDate::currentDate()); QList list; timer.start(); for (int i = 0; i < 100; ++i) { MyMoneyFile::instance()->transactionList(list, filter); measurement[i != 0] = timer.elapsed(); } std::cerr << "transactionList(list)" << std::endl; std::cerr << "First time: " << measurement[0] << " msec" << std::endl; std::cerr << "Total time: " << (measurement[0] + measurement[1]) << " msec" << std::endl; std::cerr << "Average : " << (measurement[0] + measurement[1]) / 100 << " msec" << std::endl; } MyMoneyFile::instance()->preloadCache(); } void KMyMoneyApp::slotFileNew() { KMSTATUS(i18n("Creating new document...")); slotFileClose(); if (!d->m_myMoneyView->fileOpen()) { // next line required until we move all file handling out of KMyMoneyView d->m_myMoneyView->newFile(); d->m_fileName = QUrl(); updateCaption(); NewUserWizard::Wizard *wizard = new NewUserWizard::Wizard(); if (wizard->exec() == QDialog::Accepted) { MyMoneyFileTransaction ft; MyMoneyFile* file = MyMoneyFile::instance(); try { // store the user info file->setUser(wizard->user()); // create and setup base currency file->addCurrency(wizard->baseCurrency()); file->setBaseCurrency(wizard->baseCurrency()); // create a possible institution MyMoneyInstitution inst = wizard->institution(); if (inst.name().length()) { file->addInstitution(inst); } // create a possible checking account auto acc = wizard->account(); if (acc.name().length()) { acc.setInstitutionId(inst.id()); MyMoneyAccount asset = file->asset(); file->addAccount(acc, asset); // create possible opening balance transaction if (!wizard->openingBalance().isZero()) { file->createOpeningBalanceTransaction(acc, wizard->openingBalance()); } } // import the account templates QList templates = wizard->templates(); QList::iterator it_t; for (it_t = templates.begin(); it_t != templates.end(); ++it_t) { (*it_t).importTemplate(&progressCallback); } d->m_fileName = wizard->url(); ft.commit(); KMyMoneyGlobalSettings::setFirstTimeRun(false); // FIXME This is a bit clumsy. We re-read the freshly // created file to be able to run through all the // fixup logic and then save it to keep the modified // flag off. slotFileSave(); d->m_myMoneyView->readFile(d->m_fileName); slotFileSave(); // now keep the filename in the recent files used list //KRecentFilesAction *p = dynamic_cast(action(KStandardAction::name(KStandardAction::OpenRecent))); //if(p) d->m_recentFiles->addUrl(d->m_fileName); writeLastUsedFile(d->m_fileName.url()); } catch (const MyMoneyException &) { // next line required until we move all file handling out of KMyMoneyView d->m_myMoneyView->closeFile(); } if (wizard->startSettingsAfterFinished()) slotSettings(); } else { // next line required until we move all file handling out of KMyMoneyView d->m_myMoneyView->closeFile(); } delete wizard; updateCaption(); emit fileLoaded(d->m_fileName); } } QUrl KMyMoneyApp::selectFile(const QString& title, const QString& _path, const QString& mask, QFileDialog::FileMode mode, QWidget* widget) { QString path(_path); // if the path is not specified open the file dialog in the last used directory // 'kmymoney' is the keyword that identifies the last used directory in KFileDialog if (path.isEmpty()) { path = KRecentDirs::dir(":kmymoney-import"); } QPointer dialog = new QFileDialog(this, title, path, mask); dialog->setFileMode(mode); QUrl url; if (dialog->exec() == QDialog::Accepted && dialog != 0) { QList selectedUrls = dialog->selectedUrls(); if (!selectedUrls.isEmpty()) { url = selectedUrls.first(); if (_path.isEmpty()) { KRecentDirs::add(":kmymoney-import", url.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).path()); } } } // in case we have an additional widget, we remove it from the // dialog, so that the caller can still access it. Therefore, it is // the callers responsibility to delete the object if (widget) widget->setParent(0); delete dialog; return url; } // General open void KMyMoneyApp::slotFileOpen() { KMSTATUS(i18n("Open a file.")); QString prevDir = readLastUsedDir(); QPointer dialog = new QFileDialog(this, QString(), prevDir, i18n("KMyMoney files (*.kmy *.xml);;All files")); dialog->setFileMode(QFileDialog::ExistingFile); dialog->setAcceptMode(QFileDialog::AcceptOpen); if (dialog->exec() == QDialog::Accepted && dialog != nullptr) { slotFileOpenRecent(dialog->selectedUrls().first()); } delete dialog; } void KMyMoneyApp::slotOpenDatabase() { KMSTATUS(i18n("Open a file.")); QPointer dialog = new KSelectDatabaseDlg(QIODevice::ReadWrite); if (!dialog->checkDrivers()) { delete dialog; return; } if (dialog->exec() == QDialog::Accepted && dialog != 0) { slotFileOpenRecent(dialog->selectedURL()); } delete dialog; } bool KMyMoneyApp::isImportableFile(const QUrl &url) { bool result = false; // Iterate through the plugins and see if there's a loaded plugin who can handle it QMap::const_iterator it_plugin = d->m_importerPlugins.constBegin(); while (it_plugin != d->m_importerPlugins.constEnd()) { if ((*it_plugin)->isMyFormat(url.path())) { result = true; break; } ++it_plugin; } // If we did not find a match, try importing it as a KMM statement file, // which is really just for testing. the statement file is not exposed // to users. if (it_plugin == d->m_importerPlugins.constEnd()) if (MyMoneyStatement::isStatementFile(url.path())) result = true; // Place code here to test for QIF and other locally-supported formats // (i.e. not a plugin). If you add them here, be sure to add it to // the webConnect function. return result; } + void KMyMoneyApp::slotFileOpenRecent(const QUrl &url) { KMSTATUS(i18n("Loading file...")); QUrl lastFile = d->m_fileName; // check if there are other instances which might have this file open QList list = instanceList(); QList::ConstIterator it; bool duplicate = false; #ifdef KMM_DBUS for (it = list.constBegin(); duplicate == false && it != list.constEnd(); ++it) { QDBusInterface remoteApp(*it, "/KMymoney", "org.kde.kmymoney"); QDBusReply reply = remoteApp.call("filename"); if (!reply.isValid()) { qDebug("D-Bus error while calling app->filename()"); } else { if (reply.value() == url.url()) { duplicate = true; } } } #endif if (!duplicate) { QUrl newurl = url; if ((newurl.scheme() == QLatin1String("sql"))) { const QString key = QLatin1String("driver"); // take care and convert some old url to their new counterpart QUrlQuery query(newurl); if (query.queryItemValue(key) == QLatin1String("QMYSQL3")) { // fix any old urls query.removeQueryItem(key); query.addQueryItem(key, QLatin1String("QMYSQL")); } if (query.queryItemValue(key) == QLatin1String("QSQLITE3")) { query.removeQueryItem(key); query.addQueryItem(key, QLatin1String("QSQLITE")); } newurl.setQuery(query); if (query.queryItemValue(key) == QLatin1String("QSQLITE")) { newurl.setUserInfo(QString()); newurl.setHost(QString()); } // check if a password is needed. it may be if the URL came from the last/recent file list QPointer dialog = new KSelectDatabaseDlg(QIODevice::ReadWrite, newurl); if (!dialog->checkDrivers()) { delete dialog; return; } // if we need to supply a password, then show the dialog // otherwise it isn't needed if ((query.queryItemValue("secure").toLower() == QLatin1String("yes")) && newurl.password().isEmpty()) { if (dialog->exec() == QDialog::Accepted && dialog != nullptr) { newurl = dialog->selectedURL(); } else { delete dialog; return; } } delete dialog; } - // TODO: port KF5 (NetAccess) - if ((newurl.scheme() == QLatin1String("sql")) || (newurl.isValid() /*&& KIO::NetAccess::exists(newurl, KIO::NetAccess::SourceSide, this)*/)) { + + if (newurl.scheme() == QLatin1String("sql") || KMyMoneyUtils::fileExists(newurl)) { slotFileClose(); if (!d->m_myMoneyView->fileOpen()) { try { if (d->m_myMoneyView->readFile(newurl)) { if ((d->m_myMoneyView->isNativeFile())) { d->m_fileName = newurl; updateCaption(); d->m_recentFiles->addUrl(newurl); writeLastUsedFile(newurl.toDisplayString(QUrl::PreferLocalFile)); } else { d->m_fileName = QUrl(); // imported files have no filename } // Check the schedules slotCheckSchedules(); } } catch (const MyMoneyException &e) { KMessageBox::sorry(this, i18n("Cannot open file as requested. Error was: %1", e.what())); } updateCaption(); emit fileLoaded(d->m_fileName); } else { /*fileOpen failed - should we do something or maybe fileOpen puts out the message... - it does for database*/ } } else { // newurl invalid slotFileClose(); KMessageBox::sorry(this, i18n("

%1 is either an invalid filename or the file does not exist. You can open another file or create a new one.

", url.toDisplayString(QUrl::PreferLocalFile)), i18n("File not found")); } } else { // isDuplicate KMessageBox::sorry(this, i18n("

File %1 is already opened in another instance of KMyMoney

", url.toDisplayString(QUrl::PreferLocalFile)), i18n("Duplicate open")); } } bool KMyMoneyApp::slotFileSave() { // if there's nothing changed, there's no need to save anything if (!d->m_myMoneyView->dirty()) return true; bool rc = false; KMSTATUS(i18n("Saving file...")); if (d->m_fileName.isEmpty()) return slotFileSaveAs(); d->consistencyCheck(false); /*if (myMoneyView->isDatabase()) { rc = myMoneyView->saveDatabase(m_fileName); // the 'save' function is no longer relevant for a database*/ setEnabled(false); rc = d->m_myMoneyView->saveFile(d->m_fileName, MyMoneyFile::instance()->value("kmm-encryption-key")); setEnabled(true); d->m_autoSaveTimer->stop(); updateCaption(); return rc; } void KMyMoneyApp::slotFileSaveAsFilterChanged(const QString& filter) { if (!d->m_saveEncrypted) return; if (filter.compare(QLatin1String("*.kmy"), Qt::CaseInsensitive) != 0) { d->m_saveEncrypted->setCurrentItem(0); d->m_saveEncrypted->setEnabled(false); } else { d->m_saveEncrypted->setEnabled(true); } } void KMyMoneyApp::slotManageGpgKeys() { QPointer dlg = new KGpgKeySelectionDlg(this); dlg->setKeys(d->m_additionalGpgKeys); if (dlg->exec() == QDialog::Accepted && dlg != 0) { d->m_additionalGpgKeys = dlg->keys(); d->m_additionalKeyLabel->setText(i18n("Additional encryption keys to be used: %1", d->m_additionalGpgKeys.count())); } delete dlg; } void KMyMoneyApp::slotKeySelected(int idx) { int cnt = 0; if (idx != 0) { cnt = d->m_additionalGpgKeys.count(); } d->m_additionalKeyLabel->setEnabled(idx != 0); d->m_additionalKeyButton->setEnabled(idx != 0); d->m_additionalKeyLabel->setText(i18n("Additional encryption keys to be used: %1", cnt)); } bool KMyMoneyApp::slotFileSaveAs() { bool rc = false; // in event of it being a database, ensure that all data is read into storage for saveas if (d->m_myMoneyView->isDatabase()) dynamic_cast(MyMoneyFile::instance()->storage())->fillStorage(); KMSTATUS(i18n("Saving file with a new filename...")); // fill the additional key list with the default d->m_additionalGpgKeys = KMyMoneyGlobalSettings::gpgRecipientList(); QWidget* vbox = new QWidget(); QVBoxLayout *vboxVBoxLayout = new QVBoxLayout(vbox); vboxVBoxLayout->setMargin(0); if (KGPGFile::GPGAvailable()) { QWidget* keyBox = new QWidget(vbox); QHBoxLayout *keyBoxHBoxLayout = new QHBoxLayout(keyBox); keyBoxHBoxLayout->setMargin(0); vboxVBoxLayout->addWidget(keyBox); QLabel *keyLabel = new QLabel(i18n("Encryption key to be used"), keyBox); keyBoxHBoxLayout->addWidget(keyLabel); d->m_saveEncrypted = new KComboBox(keyBox); keyBoxHBoxLayout->addWidget(d->m_saveEncrypted); QWidget* labelBox = new QWidget(vbox); QHBoxLayout *labelBoxHBoxLayout = new QHBoxLayout(labelBox); labelBoxHBoxLayout->setMargin(0); vboxVBoxLayout->addWidget(labelBox); d->m_additionalKeyLabel = new QLabel(i18n("Additional encryption keys to be used: %1", d->m_additionalGpgKeys.count()), labelBox); labelBoxHBoxLayout->addWidget(d->m_additionalKeyLabel); d->m_additionalKeyButton = new QPushButton(i18n("Manage additional keys"), labelBox); labelBoxHBoxLayout->addWidget(d->m_additionalKeyButton); connect(d->m_additionalKeyButton, SIGNAL(clicked()), this, SLOT(slotManageGpgKeys())); connect(d->m_saveEncrypted, SIGNAL(activated(int)), this, SLOT(slotKeySelected(int))); // fill the secret key list and combo box QStringList keyList; KGPGFile::secretKeyList(keyList); d->m_saveEncrypted->addItem(i18n("No encryption")); for (QStringList::iterator it = keyList.begin(); it != keyList.end(); ++it) { QStringList fields = (*it).split(':', QString::SkipEmptyParts); if (fields[0] != recoveryKeyId) { // replace parenthesis in name field with brackets auto name = fields[1]; name.replace('(', "["); name.replace(')', "]"); name = QString("%1 (0x%2)").arg(name).arg(fields[0]); d->m_saveEncrypted->addItem(name); if (name.contains(KMyMoneyGlobalSettings::gpgRecipient())) { d->m_saveEncrypted->setCurrentItem(name); } } } } QString prevDir; // don't prompt file name if not a native file if (d->m_myMoneyView->isNativeFile()) prevDir = readLastUsedDir(); QPointer dlg = new QFileDialog(this, i18n("Save As"), prevDir, QString(QLatin1String("%2 (%1);;")).arg(QStringLiteral("*.kmy")).arg(i18nc("KMyMoney (Filefilter)", "KMyMoney files")) + QString(QLatin1String("%2 (%1);;")).arg(QStringLiteral("*.xml")).arg(i18nc("XML (Filefilter)", "XML files")) + QString(QLatin1String("%2 (%1);;")).arg(QStringLiteral("*.anon.xml")).arg(i18nc("Anonymous (Filefilter)", "Anonymous files")) + QString(QLatin1String("%2 (%1);;")).arg(QStringLiteral("*")).arg(i18nc("All files (Filefilter)", "All files"))); dlg->setAcceptMode(QFileDialog::AcceptSave); connect(dlg, SIGNAL(filterChanged(QString)), this, SLOT(slotFileSaveAsFilterChanged(QString))); if (dlg->exec() == QDialog::Accepted && dlg != 0) { QUrl newURL = dlg->selectedUrls().first(); if (!newURL.fileName().isEmpty()) { d->consistencyCheck(false); // deleting the dialog will delete the combobox pointed to by d->m_saveEncrypted so get the key name here QString selectedKeyName; if (d->m_saveEncrypted && d->m_saveEncrypted->currentIndex() != 0) selectedKeyName = d->m_saveEncrypted->currentText(); d->m_saveEncrypted = 0; QString newName = newURL.toDisplayString(QUrl::PreferLocalFile); // append extension if not present if (!newName.endsWith(QLatin1String(".kmy"), Qt::CaseInsensitive) && !newName.endsWith(QLatin1String(".xml"), Qt::CaseInsensitive)) newName.append(QLatin1String(".kmy")); newURL = QUrl::fromUserInput(newName); d->m_recentFiles->addUrl(newURL); setEnabled(false); // If this is the anonymous file export, just save it, don't actually take the // name, or remember it! Don't even try to encrypt it if (newName.endsWith(QLatin1String(".anon.xml"), Qt::CaseInsensitive)) rc = d->m_myMoneyView->saveFile(newURL); else { d->m_fileName = newURL; QString encryptionKeys; QRegExp keyExp(".* \\((.*)\\)"); if (keyExp.indexIn(selectedKeyName) != -1) { encryptionKeys = keyExp.cap(1); } if (!d->m_additionalGpgKeys.isEmpty()) { if (!encryptionKeys.isEmpty()) encryptionKeys.append(QLatin1Char(',')); encryptionKeys.append(d->m_additionalGpgKeys.join(QLatin1Char(','))); } rc = d->m_myMoneyView->saveFile(d->m_fileName, encryptionKeys); //write the directory used for this file as the default one for next time. writeLastUsedDir(newURL.toDisplayString(QUrl::RemoveFilename | QUrl::PreferLocalFile | QUrl::StripTrailingSlash)); writeLastUsedFile(newName); } d->m_autoSaveTimer->stop(); setEnabled(true); } } delete dlg; updateCaption(); return rc; } void KMyMoneyApp::slotSaveAsDatabase() { saveAsDatabase(); } bool KMyMoneyApp::saveAsDatabase() { bool rc = false; QUrl oldUrl; // in event of it being a database, ensure that all data is read into storage for saveas if (d->m_myMoneyView->isDatabase()) { dynamic_cast(MyMoneyFile::instance()->storage())->fillStorage(); oldUrl = d->m_fileName.isEmpty() ? lastOpenedURL() : d->m_fileName; } KMSTATUS(i18n("Saving file to database...")); QPointer dialog = new KSelectDatabaseDlg(QIODevice::WriteOnly); QUrl url = oldUrl; if (!dialog->checkDrivers()) { delete dialog; return (false); } while (oldUrl == url && dialog->exec() == QDialog::Accepted && dialog != 0) { url = dialog->selectedURL(); // If the protocol is SQL for the old and new, and the hostname and database names match // Let the user know that the current database cannot be saved on top of itself. if (url.scheme() == "sql" && oldUrl.scheme() == "sql" && oldUrl.host() == url.host() && QUrlQuery(oldUrl).queryItemValue("driver") == QUrlQuery(url).queryItemValue("driver") && oldUrl.path().right(oldUrl.path().length() - 1) == url.path().right(url.path().length() - 1)) { KMessageBox::sorry(this, i18n("Cannot save to current database.")); } else { try { rc = d->m_myMoneyView->saveAsDatabase(url); } catch (const MyMoneyException &e) { KMessageBox::sorry(this, i18n("Cannot save to current database: %1", e.what())); } } } delete dialog; if (rc) { //KRecentFilesAction *p = dynamic_cast(action("file_open_recent")); //if(p) d->m_recentFiles->addUrl(url); writeLastUsedFile(url.toDisplayString(QUrl::PreferLocalFile)); } d->m_autoSaveTimer->stop(); updateCaption(); return rc; } void KMyMoneyApp::slotFileCloseWindow() { KMSTATUS(i18n("Closing window...")); if (d->m_myMoneyView->dirty()) { int answer = askSaveOnClose(); if (answer == KMessageBox::Cancel) return; else if (answer == KMessageBox::Yes) slotFileSave(); } close(); } void KMyMoneyApp::slotFileClose() { bool okToSelect = true; // check if transaction editor is open and ask user what he wants to do slotTransactionsCancelOrEnter(okToSelect); if (!okToSelect) return; // no update status here, as we might delete the status too early. if (d->m_myMoneyView->dirty()) { int answer = askSaveOnClose(); if (answer == KMessageBox::Cancel) return; else if (answer == KMessageBox::Yes) slotFileSave(); } d->closeFile(); } void KMyMoneyApp::slotFileQuit() { // don't modify the status message here as this will prevent quit from working!! // See the beginning of queryClose() and isReady() why. Thomas Baumgart 2005-10-17 bool quitApplication = true; QList memberList = KMainWindow::memberList(); if (!memberList.isEmpty()) { QList::const_iterator w_it = memberList.constBegin(); for (; w_it != memberList.constEnd(); ++w_it) { // only close the window if the closeEvent is accepted. If the user presses Cancel on the saveModified() dialog, // the window and the application stay open. if (!(*w_it)->close()) { quitApplication = false; break; } } } // We will only quit if all windows were processed and not cancelled if (quitApplication) { QCoreApplication::quit(); } } void KMyMoneyApp::slotShowTransactionDetail() { } void KMyMoneyApp::slotHideReconciledTransactions() { KMyMoneyGlobalSettings::setHideReconciledTransactions(actionCollection()->action(s_Actions[Action::ViewHideReconciled])->isChecked()); d->m_myMoneyView->slotRefreshViews(); } void KMyMoneyApp::slotHideUnusedCategories() { KMyMoneyGlobalSettings::setHideUnusedCategory(actionCollection()->action(s_Actions[Action::ViewHideCategories])->isChecked()); d->m_myMoneyView->slotRefreshViews(); } void KMyMoneyApp::slotShowAllAccounts() { d->m_myMoneyView->slotRefreshViews(); } #ifdef KMM_DEBUG void KMyMoneyApp::slotToggleTraces() { MyMoneyTracer::onOff(actionCollection()->action(s_Actions[Action::DebugTraces])->isChecked() ? 1 : 0); } #endif void KMyMoneyApp::slotToggleTimers() { extern bool timersOn; // main.cpp timersOn = actionCollection()->action(s_Actions[Action::DebugTimers])->isChecked(); } QString KMyMoneyApp::slotStatusMsg(const QString &text) { /////////////////////////////////////////////////////////////////// // change status message permanently QString previousMessage = d->m_statusLabel->text(); d->m_applicationIsReady = false; QString currentMessage = text; if (currentMessage.isEmpty() || currentMessage == i18nc("Application is ready to use", "Ready.")) { d->m_applicationIsReady = true; currentMessage = i18nc("Application is ready to use", "Ready."); } statusBar()->clearMessage(); d->m_statusLabel->setText(currentMessage); return previousMessage; } void KMyMoneyApp::ready() { slotStatusMsg(QString()); } bool KMyMoneyApp::isReady() { return d->m_applicationIsReady; } void KMyMoneyApp::slotStatusProgressBar(int current, int total) { if (total == -1 && current == -1) { // reset if (d->m_progressTimer) { d->m_progressTimer->start(500); // remove from screen in 500 msec d->m_progressBar->setValue(d->m_progressBar->maximum()); } } else if (total != 0) { // init d->m_progressTimer->stop(); d->m_progressBar->setMaximum(total); d->m_progressBar->setValue(0); d->m_progressBar->show(); } else { // update QTime currentTime = QTime::currentTime(); // only process painting if last update is at least 250 ms ago if (abs(d->m_lastUpdate.msecsTo(currentTime)) > 250) { d->m_progressBar->setValue(current); d->m_lastUpdate = currentTime; } } } void KMyMoneyApp::slotStatusProgressDone() { d->m_progressTimer->stop(); d->m_progressBar->reset(); d->m_progressBar->hide(); d->m_progressBar->setValue(0); } void KMyMoneyApp::progressCallback(int current, int total, const QString& msg) { if (!msg.isEmpty()) kmymoney->slotStatusMsg(msg); kmymoney->slotStatusProgressBar(current, total); } void KMyMoneyApp::slotFileViewPersonal() { if (!d->m_myMoneyView->fileOpen()) { KMessageBox::information(this, i18n("No KMyMoneyFile open")); return; } KMSTATUS(i18n("Viewing personal data...")); MyMoneyFile* file = MyMoneyFile::instance(); MyMoneyPayee user = file->user(); QPointer editPersonalDataDlg = new EditPersonalDataDlg(user.name(), user.address(), user.city(), user.state(), user.postcode(), user.telephone(), user.email(), this, i18n("Edit Personal Data")); if (editPersonalDataDlg->exec() == QDialog::Accepted && editPersonalDataDlg != 0) { user.setName(editPersonalDataDlg->userName()); user.setAddress(editPersonalDataDlg->userStreet()); user.setCity(editPersonalDataDlg->userTown()); user.setState(editPersonalDataDlg->userCountry()); user.setPostcode(editPersonalDataDlg->userPostcode()); user.setTelephone(editPersonalDataDlg->userTelephone()); user.setEmail(editPersonalDataDlg->userEmail()); MyMoneyFileTransaction ft; try { file->setUser(user); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::information(this, i18n("Unable to store user information: %1", e.what())); } } delete editPersonalDataDlg; } void KMyMoneyApp::slotFileFileInfo() { if (!d->m_myMoneyView->fileOpen()) { KMessageBox::information(this, i18n("No KMyMoneyFile open")); return; } QFile g("kmymoney.dump"); g.open(QIODevice::WriteOnly); QDataStream st(&g); MyMoneyStorageDump dumper; dumper.writeStream(st, dynamic_cast(MyMoneyFile::instance()->storage())); g.close(); } void KMyMoneyApp::slotLoadAccountTemplates() { KMSTATUS(i18n("Importing account templates.")); int rc; QPointer dlg = new KLoadTemplateDlg(); if ((rc = dlg->exec()) == QDialog::Accepted && dlg != 0) { MyMoneyFileTransaction ft; try { // import the account templates QList templates = dlg->templates(); QList::iterator it_t; for (it_t = templates.begin(); it_t != templates.end(); ++it_t) { (*it_t).importTemplate(&progressCallback); } ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(0, i18n("Error"), i18n("Unable to import template(s): %1, thrown in %2:%3", e.what(), e.file(), e.line())); } } delete dlg; } void KMyMoneyApp::slotSaveAccountTemplates() { KMSTATUS(i18n("Exporting account templates.")); QString savePath = QStandardPaths::writableLocation(QStandardPaths::DataLocation) + "/templates/" + QLocale().name(); QDir d(savePath); if (!d.exists()) d.mkpath(savePath); QString newName = QFileDialog::getSaveFileName(this, i18n("Save as..."), savePath, i18n("KMyMoney template files (*.kmt);;All files (*)")); // // If there is no file extension, then append a .kmt at the end of the file name. // If there is a file extension, make sure it is .kmt, delete any others. // if (!newName.isEmpty()) { // find last . delimiter int nLoc = newName.lastIndexOf('.'); if (nLoc != -1) { QString strExt, strTemp; strTemp = newName.left(nLoc + 1); strExt = newName.right(newName.length() - (nLoc + 1)); if ((strExt.indexOf("kmt", 0, Qt::CaseInsensitive) == -1)) { strTemp.append("kmt"); //append to make complete file name newName = strTemp; } } else { newName.append(".kmt"); } if (okToWriteFile(QUrl::fromLocalFile(newName))) { QPointer dlg = new KTemplateExportDlg(this); if (dlg->exec() == QDialog::Accepted && dlg) { MyMoneyTemplate templ; templ.setTitle(dlg->title()); templ.setShortDescription(dlg->shortDescription()); templ.setLongDescription(dlg->longDescription()); templ.exportTemplate(&progressCallback); templ.saveTemplate(QUrl::fromLocalFile(newName)); } delete dlg; } } } void KMyMoneyApp::slotAccountChart() { if (!d->m_selectedAccount.id().isEmpty()) { QPointer dlg = new KBalanceChartDlg(d->m_selectedAccount, this); dlg->exec(); delete dlg; } } // // KMyMoneyApp::slotStatementImport() is for testing only. The MyMoneyStatement // is not intended to be exposed to users in XML form. // void KMyMoneyApp::slotStatementImport() { bool result = false; KMSTATUS(i18n("Importing an XML Statement.")); QList files{QFileDialog::getOpenFileUrls(this, QString(), QUrl(), i18n("XML files (*.xml);;All files (*)"))}; if (!files.isEmpty()) { d->m_collectingStatements = (files.count() > 1); foreach (const QUrl &url, files) { qDebug("Processing '%s'", qPrintable(url.path())); result |= slotStatementImport(url.path()); } /* QFile f( dialog->selectedURL().path() ); f.open( QIODevice::ReadOnly ); QString error = "Unable to parse file"; QDomDocument* doc = new QDomDocument; if(doc->setContent(&f, FALSE)) { if ( doc->doctype().name() == "KMYMONEY-STATEMENT" ) { QDomElement rootElement = doc->documentElement(); if(!rootElement.isNull()) { QDomNode child = rootElement.firstChild(); if(!child.isNull() && child.isElement()) { MyMoneyStatement s; if ( s.read(child.toElement()) ) result = slotStatementImport(s); else error = "File does not contain any statements"; } } } else error = "File is not a KMyMoney Statement"; } delete doc; if ( !result ) { QMessageBox::critical( this, i18n("Critical Error"), i18n("Unable to read file %1: %2").arg( dialog->selectedURL().path()).arg(error), QMessageBox::Ok, 0 ); }*/ } if (!result) { // re-enable all standard widgets setEnabled(true); } } bool KMyMoneyApp::slotStatementImport(const QString& url) { bool result = false; MyMoneyStatement s; if (MyMoneyStatement::readXMLFile(s, url)) result = slotStatementImport(s); else KMessageBox::error(this, i18n("Error importing %1: This file is not a valid KMM statement file.", url), i18n("Invalid Statement")); return result; } bool KMyMoneyApp::slotStatementImport(const MyMoneyStatement& s, bool silent) { bool result = false; // keep a copy of the statement if (KMyMoneySettings::logImportedStatements()) { QString logFile = QString("%1/kmm-statement-%2.txt").arg(KMyMoneySettings::logPath()).arg(d->m_statementXMLindex++); MyMoneyStatement::writeXMLFile(s, logFile); } // we use an object on the heap here, so that we can check the presence // of it during slotUpdateActions() by looking at the pointer. d->m_smtReader = new MyMoneyStatementReader; d->m_smtReader->setAutoCreatePayee(true); d->m_smtReader->setProgressCallback(&progressCallback); connect(d->m_smtReader, &MyMoneyStatementReader::createAccount, this, static_cast(&KMyMoneyApp::slotAccountNew)); connect(d->m_smtReader, &MyMoneyStatementReader::createCategory, this, static_cast(&KMyMoneyApp::slotCategoryNew)); // disable all standard widgets during the import setEnabled(false); QStringList messages; result = d->m_smtReader->import(s, messages); bool transactionAdded = d->m_smtReader->anyTransactionAdded(); // get rid of the statement reader and tell everyone else // about the destruction by setting the pointer to zero delete d->m_smtReader; d->m_smtReader = 0; slotStatusProgressBar(-1, -1); ready(); // re-enable all standard widgets setEnabled(true); if (!d->m_collectingStatements && !silent) KMessageBox::informationList(this, i18n("The statement has been processed with the following results:"), messages, i18n("Statement stats")); else if (transactionAdded) d->m_statementResults += messages; slotUpdateActions();// Re-enable menu items after import via plugin. return result; } bool KMyMoneyApp::okToWriteFile(const QUrl &url) { Q_UNUSED(url) // check if the file exists and warn the user bool reallySaveFile = true; - // TODO: port KF5 (NetAccess) - //if (KIO::NetAccess::exists(url, KIO::NetAccess::SourceSide, this)) { - // if (KMessageBox::warningYesNo(this, QLatin1String("") + i18n("The file %1 already exists. Do you really want to overwrite it?", url.toDisplayString(QUrl::PreferLocalFile)) + QLatin1String(""), i18n("File already exists")) != KMessageBox::Yes) - // reallySaveFile = false; - //} + if (KMyMoneyUtils::fileExists(url)) { + if (KMessageBox::warningYesNo(this, QLatin1String("") + i18n("The file %1 already exists. Do you really want to overwrite it?", url.toDisplayString(QUrl::PreferLocalFile)) + QLatin1String(""), i18n("File already exists")) != KMessageBox::Yes) + reallySaveFile = false; + } return reallySaveFile; } void KMyMoneyApp::slotSettings() { // if we already have an instance of the settings dialog, then use it if (KConfigDialog::showDialog("KMyMoney-Settings")) return; // otherwise, we have to create it KConfigDialog* dlg = new KSettingsKMyMoney(this, "KMyMoney-Settings", KMyMoneyGlobalSettings::self()); connect(dlg, &KConfigDialog::settingsChanged, this, &KMyMoneyApp::slotUpdateConfiguration); dlg->show(); } void KMyMoneyApp::slotShowCredits() { KAboutData aboutData = initializeCreditsData(); KAboutApplicationDialog dlg(aboutData, this); dlg.exec(); } void KMyMoneyApp::slotUpdateConfiguration() { MyMoneyTransactionFilter::setFiscalYearStart(KMyMoneyGlobalSettings::firstFiscalMonth(), KMyMoneyGlobalSettings::firstFiscalDay()); LedgerSeperator::setFirstFiscalDate(KMyMoneyGlobalSettings::firstFiscalMonth(), KMyMoneyGlobalSettings::firstFiscalDay()); d->m_myMoneyView->updateViewType(); // update the sql storage module settings MyMoneyStorageSql::setStartDate(KMyMoneyGlobalSettings::startDate().date()); // update the report module settings MyMoneyReport::setLineWidth(KMyMoneyGlobalSettings::lineWidth()); // update the holiday region configuration setHolidayRegion(KMyMoneyGlobalSettings::holidayRegion()); d->m_myMoneyView->slotRefreshViews(); // re-read autosave configuration d->m_autoSaveEnabled = KMyMoneyGlobalSettings::autoSaveFile(); d->m_autoSavePeriod = KMyMoneyGlobalSettings::autoSavePeriod(); // stop timer if turned off but running if (d->m_autoSaveTimer->isActive() && !d->m_autoSaveEnabled) { d->m_autoSaveTimer->stop(); } // start timer if turned on and needed but not running if (!d->m_autoSaveTimer->isActive() && d->m_autoSaveEnabled && d->m_myMoneyView->dirty()) { d->m_autoSaveTimer->setSingleShot(true); d->m_autoSaveTimer->start(d->m_autoSavePeriod * 60 * 1000); } d->setThemedCSS(); // check if the recovery key is still valid or expires soon if (KMyMoneySettings::writeDataEncrypted() && KMyMoneySettings::encryptRecover()) { if (KGPGFile::GPGAvailable()) { KGPGFile file; QDateTime expirationDate = file.keyExpires(QLatin1String(recoveryKeyId)); if (expirationDate.isValid() && QDateTime::currentDateTime().daysTo(expirationDate) <= RECOVER_KEY_EXPIRATION_WARNING) { bool skipMessage = false; //get global config object for our app. KSharedConfigPtr kconfig = KSharedConfig::openConfig(); KConfigGroup grp; QDate lastWarned; if (kconfig) { grp = d->m_config->group("General Options"); lastWarned = grp.readEntry("LastRecoverKeyExpirationWarning", QDate()); if (QDate::currentDate() == lastWarned) { skipMessage = true; } } if (!skipMessage) { if (kconfig) { grp.writeEntry("LastRecoverKeyExpirationWarning", QDate::currentDate()); } KMessageBox::information(this, i18np("You have configured KMyMoney to use GPG to protect your data and to encrypt your data also with the KMyMoney recover key. This key is about to expire in %1 day. Please update the key from a keyserver using your GPG frontend (e.g. KGPG).", "You have configured KMyMoney to use GPG to protect your data and to encrypt your data also with the KMyMoney recover key. This key is about to expire in %1 days. Please update the key from a keyserver using your GPG frontend (e.g. KGPG).", QDateTime::currentDateTime().daysTo(expirationDate)), i18n("Recover key expires soon")); } } } } } void KMyMoneyApp::slotBackupFile() { // Save the file first so isLocalFile() works if (d->m_myMoneyView && d->m_myMoneyView->dirty()) { if (KMessageBox::questionYesNo(this, i18n("The file must be saved first " "before it can be backed up. Do you want to continue?")) == KMessageBox::No) { return; } slotFileSave(); } if (d->m_fileName.isEmpty()) return; if (!d->m_fileName.isLocalFile()) { KMessageBox::sorry(this, i18n("The current implementation of the backup functionality only supports local files as source files. Your current source file is '%1'.", d->m_fileName.url()), i18n("Local files only")); return; } QPointer backupDlg = new KBackupDlg(this); #ifdef Q_OS_WIN backupDlg->mountCheckBox->setEnabled(false); #endif int returncode = backupDlg->exec(); if (returncode == QDialog::Accepted && backupDlg != 0) { d->m_backupMount = backupDlg->mountCheckBox(); d->m_proc.clearProgram(); d->m_backupState = BACKUP_MOUNTING; d->m_mountpoint = backupDlg->mountPoint(); if (d->m_backupMount) { slotBackupMount(); } else { progressCallback(0, 300, ""); #ifdef Q_OS_WIN d->m_ignoreBackupExitCode = true; QTimer::singleShot(0, this, SLOT(slotBackupHandleEvents())); #else // If we don't have to mount a device, we just issue // a dummy command to start the copy operation d->m_proc.setProgram("true"); d->m_proc.start(); #endif } } delete backupDlg; } void KMyMoneyApp::slotBackupMount() { progressCallback(0, 300, i18n("Mounting %1", d->m_mountpoint)); d->m_proc.setProgram("mount"); d->m_proc << d->m_mountpoint; d->m_proc.start(); } bool KMyMoneyApp::slotBackupWriteFile() { QFileInfo fi(d->m_fileName.fileName()); QString today = QDate::currentDate().toString("-yyyy-MM-dd.") + fi.suffix(); QString backupfile = d->m_mountpoint + '/' + d->m_fileName.fileName(); KMyMoneyUtils::appendCorrectFileExt(backupfile, today); // check if file already exists and ask what to do QFile f(backupfile); if (f.exists()) { int answer = KMessageBox::warningContinueCancel(this, i18n("Backup file for today exists on that device. Replace?"), i18n("Backup"), KGuiItem(i18n("&Replace"))); if (answer == KMessageBox::Cancel) { return false; } } progressCallback(50, 0, i18n("Writing %1", backupfile)); d->m_proc.clearProgram(); #ifdef Q_OS_WIN d->m_proc << "cmd.exe" << "/c" << "copy" << "/b" << "/y"; d->m_proc << (QDir::toNativeSeparators(d->m_fileName.toLocalFile()) + "+ nul") << QDir::toNativeSeparators(backupfile); #else d->m_proc << "cp" << "-f"; d->m_proc << d->m_fileName.toLocalFile() << backupfile; #endif d->m_backupState = BACKUP_COPYING; d->m_proc.start(); return true; } void KMyMoneyApp::slotBackupUnmount() { progressCallback(250, 0, i18n("Unmounting %1", d->m_mountpoint)); d->m_proc.clearProgram(); d->m_proc.setProgram("umount"); d->m_proc << d->m_mountpoint; d->m_backupState = BACKUP_UNMOUNTING; d->m_proc.start(); } void KMyMoneyApp::slotBackupFinish() { d->m_backupState = BACKUP_IDLE; progressCallback(-1, -1, QString()); ready(); } void KMyMoneyApp::slotBackupHandleEvents() { switch (d->m_backupState) { case BACKUP_MOUNTING: if (d->m_ignoreBackupExitCode || (d->m_proc.exitStatus() == QProcess::NormalExit && d->m_proc.exitCode() == 0)) { d->m_ignoreBackupExitCode = false; d->m_backupResult = 0; if (!slotBackupWriteFile()) { d->m_backupResult = 1; if (d->m_backupMount) slotBackupUnmount(); else slotBackupFinish(); } } else { KMessageBox::information(this, i18n("Error mounting device"), i18n("Backup")); d->m_backupResult = 1; if (d->m_backupMount) slotBackupUnmount(); else slotBackupFinish(); } break; case BACKUP_COPYING: if (d->m_proc.exitStatus() == QProcess::NormalExit && d->m_proc.exitCode() == 0) { if (d->m_backupMount) { slotBackupUnmount(); } else { progressCallback(300, 0, i18nc("Backup done", "Done")); KMessageBox::information(this, i18n("File successfully backed up"), i18n("Backup")); slotBackupFinish(); } } else { qDebug("copy exit code is %d", d->m_proc.exitCode()); d->m_backupResult = 1; KMessageBox::information(this, i18n("Error copying file to device"), i18n("Backup")); if (d->m_backupMount) slotBackupUnmount(); else slotBackupFinish(); } break; case BACKUP_UNMOUNTING: if (d->m_proc.exitStatus() == QProcess::NormalExit && d->m_proc.exitCode() == 0) { progressCallback(300, 0, i18nc("Backup done", "Done")); if (d->m_backupResult == 0) KMessageBox::information(this, i18n("File successfully backed up"), i18n("Backup")); } else { KMessageBox::information(this, i18n("Error unmounting device"), i18n("Backup")); } slotBackupFinish(); break; default: qWarning("Unknown state for backup operation!"); progressCallback(-1, -1, QString()); ready(); break; } } void KMyMoneyApp::slotShowTipOfTheDay() { KTipDialog::showTip(d->m_myMoneyView, "", true); } void KMyMoneyApp::slotShowPreviousView() { } void KMyMoneyApp::slotShowNextView() { } void KMyMoneyApp::slotGenerateSql() { QPointer editor = new KGenerateSqlDlg(this); editor->setObjectName("Generate Database SQL"); editor->exec(); delete editor; } void KMyMoneyApp::slotToolsStartKCalc() { QString cmd = KMyMoneyGlobalSettings::externalCalculator(); // if none is present, we fall back to the default if (cmd.isEmpty()) { #if defined(Q_OS_WIN32) cmd = QLatin1String("calc"); #elif defined(Q_OS_MAC) cmd = QLatin1String("open -a Calculator"); #else cmd = QLatin1String("kcalc"); #endif } KRun::runCommand(cmd, this); } void KMyMoneyApp::slotFindTransaction() { if (d->m_searchDlg == 0) { d->m_searchDlg = new KFindTransactionDlg(this); connect(d->m_searchDlg, SIGNAL(destroyed()), this, SLOT(slotCloseSearchDialog())); connect(d->m_searchDlg, SIGNAL(transactionSelected(QString,QString)), d->m_myMoneyView, SLOT(slotLedgerSelected(QString,QString))); } d->m_searchDlg->show(); d->m_searchDlg->raise(); d->m_searchDlg->activateWindow(); } void KMyMoneyApp::slotCloseSearchDialog() { if (d->m_searchDlg) d->m_searchDlg->deleteLater(); d->m_searchDlg = 0; } void KMyMoneyApp::createInstitution(MyMoneyInstitution& institution) { MyMoneyFile* file = MyMoneyFile::instance(); MyMoneyFileTransaction ft; try { file->addInstitution(institution); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::information(this, i18n("Cannot add institution: %1", e.what())); } } void KMyMoneyApp::slotInstitutionNew() { MyMoneyInstitution institution; slotInstitutionNew(institution); } void KMyMoneyApp::slotInstitutionNew(MyMoneyInstitution& institution) { institution.clearId(); QPointer dlg = new KNewBankDlg(institution); if (dlg->exec() == QDialog::Accepted && dlg != 0) { institution = dlg->institution(); createInstitution(institution); } delete dlg; } void KMyMoneyApp::slotInstitutionEditEmpty() { slotInstitutionEdit(MyMoneyInstitution()); } void KMyMoneyApp::slotInstitutionEdit(const MyMoneyObject& obj) { if (typeid(obj) != typeid(MyMoneyInstitution)) return; // make sure the selected object has an id if (d->m_selectedInstitution.id().isEmpty()) return; try { MyMoneyFile* file = MyMoneyFile::instance(); //grab a pointer to the view, regardless of it being a account or institution view. MyMoneyInstitution institution = file->institution(d->m_selectedInstitution.id()); // bankSuccess is not checked anymore because d->m_file->institution will throw anyway QPointer dlg = new KNewBankDlg(institution); if (dlg->exec() == QDialog::Accepted && dlg != 0) { MyMoneyFileTransaction ft; try { file->modifyInstitution(dlg->institution()); ft.commit(); slotSelectInstitution(file->institution(dlg->institution().id())); } catch (const MyMoneyException &e) { KMessageBox::information(this, i18n("Unable to store institution: %1", e.what())); } } delete dlg; } catch (const MyMoneyException &e) { if (!obj.id().isEmpty()) KMessageBox::information(this, i18n("Unable to edit institution: %1", e.what())); } } void KMyMoneyApp::slotInstitutionDelete() { MyMoneyFile *file = MyMoneyFile::instance(); try { MyMoneyInstitution institution = file->institution(d->m_selectedInstitution.id()); if ((KMessageBox::questionYesNo(this, i18n("

Do you really want to delete the institution %1?

", institution.name()))) == KMessageBox::No) return; MyMoneyFileTransaction ft; try { file->removeInstitution(institution); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::information(this, i18n("Unable to delete institution: %1", e.what())); } } catch (const MyMoneyException &e) { KMessageBox::information(this, i18n("Unable to delete institution: %1", e.what())); } } void KMyMoneyApp::createAccount(MyMoneyAccount& newAccount, MyMoneyAccount& parentAccount, MyMoneyAccount& brokerageAccount, MyMoneyMoney openingBal) { MyMoneyFile *file = MyMoneyFile::instance(); try { const MyMoneySecurity& sec = file->security(newAccount.currencyId()); // Check the opening balance if (openingBal.isPositive() && newAccount.accountGroup() == eMyMoney::Account::Type::Liability) { QString message = i18n("This account is a liability and if the " "opening balance represents money owed, then it should be negative. " "Negate the amount?\n\n" "Please click Yes to change the opening balance to %1,\n" "Please click No to leave the amount as %2,\n" "Please click Cancel to abort the account creation." , MyMoneyUtils::formatMoney(-openingBal, newAccount, sec) , MyMoneyUtils::formatMoney(openingBal, newAccount, sec)); int ans = KMessageBox::questionYesNoCancel(this, message); if (ans == KMessageBox::Yes) { openingBal = -openingBal; } else if (ans == KMessageBox::Cancel) return; } file->createAccount(newAccount, parentAccount, brokerageAccount, openingBal); } catch (const MyMoneyException &e) { KMessageBox::information(this, i18n("Unable to add account: %1", e.what())); } } void KMyMoneyApp::slotCategoryNew(const QString& name, QString& id) { MyMoneyAccount account; account.setName(name); slotCategoryNew(account, MyMoneyFile::instance()->expense()); id = account.id(); } void KMyMoneyApp::slotCategoryNew(MyMoneyAccount& account, const MyMoneyAccount& parent) { if (KMessageBox::questionYesNo(this, QString("%1").arg(i18n("

The category %1 currently does not exist. Do you want to create it?

The parent account will default to %2 but can be changed in the following dialog.

", account.name(), parent.name())), i18n("Create category"), KStandardGuiItem::yes(), KStandardGuiItem::no(), "CreateNewCategories") == KMessageBox::Yes) { createCategory(account, parent); } else { // we should not keep the 'no' setting because that can confuse people like // I have seen in some usability tests. So we just delete it right away. KSharedConfigPtr kconfig = KSharedConfig::openConfig(); if (kconfig) { kconfig->group(QLatin1String("Notification Messages")).deleteEntry(QLatin1String("CreateNewCategories")); } } } void KMyMoneyApp::slotCategoryNew(MyMoneyAccount& account) { slotCategoryNew(account, MyMoneyAccount()); } void KMyMoneyApp::slotCategoryNew() { MyMoneyAccount parent; MyMoneyAccount account; // Preselect the parent account by looking at the current selected account/category if (!d->m_selectedAccount.id().isEmpty() && d->m_selectedAccount.isIncomeExpense()) { MyMoneyFile* file = MyMoneyFile::instance(); try { parent = file->account(d->m_selectedAccount.id()); } catch (const MyMoneyException &) { } } createCategory(account, parent); } void KMyMoneyApp::createCategory(MyMoneyAccount& account, const MyMoneyAccount& parent) { if (!parent.id().isEmpty()) { try { // make sure parent account exists MyMoneyFile::instance()->account(parent.id()); account.setParentAccountId(parent.id()); account.setAccountType(parent.accountType()); } catch (const MyMoneyException &) { } } QPointer dialog = new KNewAccountDlg(account, false, true, 0, i18n("Create a new Category")); dialog->setOpeningBalanceShown(false); dialog->setOpeningDateShown(false); if (dialog->exec() == QDialog::Accepted && dialog != 0) { MyMoneyAccount parentAccount, brokerageAccount; account = dialog->account(); parentAccount = dialog->parentAccount(); MyMoneyFile::instance()->createAccount(account, parentAccount, brokerageAccount, MyMoneyMoney()); } delete dialog; } void KMyMoneyApp::slotAccountNew() { MyMoneyAccount acc; acc.setInstitutionId(d->m_selectedInstitution.id()); acc.setOpeningDate(KMyMoneyGlobalSettings::firstFiscalDate()); slotAccountNew(acc); } void KMyMoneyApp::slotAccountNew(MyMoneyAccount& account) { NewAccountWizard::Wizard* wizard = new NewAccountWizard::Wizard(); connect(wizard, SIGNAL(createInstitution(MyMoneyInstitution&)), this, SLOT(slotInstitutionNew(MyMoneyInstitution&))); connect(wizard, SIGNAL(createAccount(MyMoneyAccount&)), this, SLOT(slotAccountNew(MyMoneyAccount&))); connect(wizard, SIGNAL(createPayee(QString,QString&)), this, SLOT(slotPayeeNew(QString,QString&))); connect(wizard, SIGNAL(createCategory(MyMoneyAccount&,MyMoneyAccount)), this, SLOT(slotCategoryNew(MyMoneyAccount&,MyMoneyAccount))); wizard->setAccount(account); if (wizard->exec() == QDialog::Accepted) { auto acc = wizard->account(); if (!(acc == MyMoneyAccount())) { MyMoneyFileTransaction ft; MyMoneyFile* file = MyMoneyFile::instance(); try { // create the account MyMoneyAccount parent = wizard->parentAccount(); file->addAccount(acc, parent); // tell the wizard about the account id which it // needs to create a possible schedule and transactions wizard->setAccount(acc); // store a possible conversion rate for the currency if (acc.currencyId() != file->baseCurrency().id()) { file->addPrice(wizard->conversionRate()); } // create the opening balance transaction if any file->createOpeningBalanceTransaction(acc, wizard->openingBalance()); // create the payout transaction for loans if any MyMoneyTransaction payoutTransaction = wizard->payoutTransaction(); if (payoutTransaction.splits().count() > 0) { file->addTransaction(payoutTransaction); } // create a brokerage account if selected MyMoneyAccount brokerageAccount = wizard->brokerageAccount(); if (!(brokerageAccount == MyMoneyAccount())) { file->addAccount(brokerageAccount, parent); } // create a possible schedule MyMoneySchedule sch = wizard->schedule(); if (!(sch == MyMoneySchedule())) { MyMoneyFile::instance()->addSchedule(sch); if (acc.isLoan()) { MyMoneyAccountLoan accLoan = MyMoneyFile::instance()->account(acc.id()); accLoan.setSchedule(sch.id()); acc = accLoan; MyMoneyFile::instance()->modifyAccount(acc); } } ft.commit(); account = acc; } catch (const MyMoneyException &e) { KMessageBox::error(this, i18n("Unable to create account: %1", e.what())); } } } delete wizard; } void KMyMoneyApp::slotInvestmentNew(MyMoneyAccount& account, const MyMoneyAccount& parent) { QString dontShowAgain = "CreateNewInvestments"; if (KMessageBox::questionYesNo(this, QString("") + i18n("The security %1 currently does not exist as sub-account of %2. " "Do you want to create it?", account.name(), parent.name()) + QString(""), i18n("Create security"), KStandardGuiItem::yes(), KStandardGuiItem::no(), dontShowAgain) == KMessageBox::Yes) { KNewInvestmentWizard dlg; dlg.setName(account.name()); if (dlg.exec() == QDialog::Accepted) { dlg.createObjects(parent.id()); account = dlg.account(); } } else { // in case the user said no but turned on the don't show again selection, we will enable // the message no matter what. Otherwise, the user is not able to use this feature // in the future anymore. KMessageBox::enableMessage(dontShowAgain); } } void KMyMoneyApp::slotInvestmentNew() { QPointer dlg = new KNewInvestmentWizard(this); if (dlg->exec() == QDialog::Accepted) dlg->createObjects(d->m_selectedAccount.id()); delete dlg; } void KMyMoneyApp::slotInvestmentEdit() { QPointer dlg = new KNewInvestmentWizard(d->m_selectedInvestment); if (dlg->exec() == QDialog::Accepted) dlg->createObjects(d->m_selectedAccount.id()); delete dlg; } void KMyMoneyApp::slotInvestmentDelete() { if (KMessageBox::questionYesNo(this, i18n("

Do you really want to delete the investment %1?

", d->m_selectedInvestment.name()), i18n("Delete investment"), KStandardGuiItem::yes(), KStandardGuiItem::no(), "DeleteInvestment") == KMessageBox::Yes) { MyMoneyFile* file = MyMoneyFile::instance(); MyMoneyFileTransaction ft; try { d->m_selectedAccount = MyMoneyAccount(); // CAUTION: deleting equity from investments view needs this, if ID of the equity to be deleted is the smallest from all file->removeAccount(d->m_selectedInvestment); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::information(this, i18n("Unable to delete investment: %1", e.what())); } } else { // we should not keep the 'no' setting because that can confuse people like // I have seen in some usability tests. So we just delete it right away. KSharedConfigPtr kconfig = KSharedConfig::openConfig(); if (kconfig) { kconfig->group(QLatin1String("Notification Messages")).deleteEntry(QLatin1String("DeleteInvestment")); } } } void KMyMoneyApp::slotOnlinePriceUpdate() { if (!d->m_selectedInvestment.id().isEmpty()) { QPointer dlg = new KEquityPriceUpdateDlg(0, d->m_selectedInvestment.currencyId()); if (dlg->exec() == QDialog::Accepted && dlg != 0) { dlg->storePrices(); } delete dlg; } } void KMyMoneyApp::slotManualPriceUpdate() { if (!d->m_selectedInvestment.id().isEmpty()) { try { MyMoneySecurity security = MyMoneyFile::instance()->security(d->m_selectedInvestment.currencyId()); MyMoneySecurity currency = MyMoneyFile::instance()->security(security.tradingCurrency()); const MyMoneyPrice &price = MyMoneyFile::instance()->price(security.id(), currency.id()); QPointer calc = new KCurrencyCalculator(security, currency, MyMoneyMoney::ONE, price.rate(currency.id()), price.date(), MyMoneyMoney::precToDenom(security.pricePrecision())); calc->setupPriceEditor(); // The dialog takes care of adding the price if necessary calc->exec(); delete calc; } catch (const MyMoneyException &e) { qDebug("Error in price update: %s", qPrintable(e.what())); } } } void KMyMoneyApp::createSchedule(MyMoneySchedule newSchedule, MyMoneyAccount& newAccount) { MyMoneyFile* file = MyMoneyFile::instance(); // Add the schedule only if one exists // // Remember to modify the first split to reference the newly created account if (!newSchedule.name().isEmpty()) { try { // We assume at least 2 splits in the transaction MyMoneyTransaction t = newSchedule.transaction(); if (t.splitCount() < 2) { throw MYMONEYEXCEPTION("Transaction for schedule has less than 2 splits!"); } MyMoneyFileTransaction ft; try { file->addSchedule(newSchedule); // in case of a loan account, we keep a reference to this // schedule in the account if (newAccount.accountType() == eMyMoney::Account::Type::Loan || newAccount.accountType() == eMyMoney::Account::Type::AssetLoan) { newAccount.setValue("schedule", newSchedule.id()); file->modifyAccount(newAccount); } ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::information(this, i18n("Unable to add scheduled transaction: %1", e.what())); } } catch (const MyMoneyException &e) { KMessageBox::information(this, i18n("Unable to add scheduled transaction: %1", e.what())); } } } void KMyMoneyApp::slotAccountDelete() { if (d->m_selectedAccount.id().isEmpty()) return; // need an account ID MyMoneyFile* file = MyMoneyFile::instance(); // can't delete standard accounts or account which still have transactions assigned if (file->isStandardAccount(d->m_selectedAccount.id())) return; // check if the account is referenced by a transaction or schedule QBitArray skip((int)eStorage::Reference::Count); skip.fill(false); skip.setBit((int)eStorage::Reference::Account); skip.setBit((int)eStorage::Reference::Institution); skip.setBit((int)eStorage::Reference::Payee); skip.setBit((int)eStorage::Reference::Tag); skip.setBit((int)eStorage::Reference::Security); skip.setBit((int)eStorage::Reference::Currency); skip.setBit((int)eStorage::Reference::Price); bool hasReference = file->isReferenced(d->m_selectedAccount, skip); // make sure we only allow transactions in a 'category' (income/expense account) switch (d->m_selectedAccount.accountType()) { case eMyMoney::Account::Type::Income: case eMyMoney::Account::Type::Expense: break; default: // if the account is still referenced if (hasReference) { return; } break; } // if we get here and still have transactions referencing the account, we // need to check with the user to possibly re-assign them to a different account bool needAskUser = true; bool exit = false; MyMoneyFileTransaction ft; if (hasReference) { // show transaction reassignment dialog needAskUser = false; KCategoryReassignDlg* dlg = new KCategoryReassignDlg(this); QString categoryId = dlg->show(d->m_selectedAccount); delete dlg; // and kill the dialog if (categoryId.isEmpty()) return; // the user aborted the dialog, so let's abort as well MyMoneyAccount newCategory = file->account(categoryId); try { { KMSTATUS(i18n("Adjusting transactions...")); /* d->m_selectedAccount.id() is the old id, categoryId the new one Now search all transactions and schedules that reference d->m_selectedAccount.id() and replace that with categoryId. */ // get the list of all transactions that reference the old account MyMoneyTransactionFilter filter(d->m_selectedAccount.id()); filter.setReportAllSplits(false); QList tlist; QList::iterator it_t; file->transactionList(tlist, filter); slotStatusProgressBar(0, tlist.count()); int cnt = 0; for (it_t = tlist.begin(); it_t != tlist.end(); ++it_t) { slotStatusProgressBar(++cnt, 0); MyMoneyTransaction t = (*it_t); if (t.replaceId(categoryId, d->m_selectedAccount.id())) file->modifyTransaction(t); } slotStatusProgressBar(tlist.count(), 0); } // now fix all schedules { KMSTATUS(i18n("Adjusting scheduled transactions...")); QList slist = file->scheduleList(d->m_selectedAccount.id()); QList::iterator it_s; int cnt = 0; slotStatusProgressBar(0, slist.count()); for (it_s = slist.begin(); it_s != slist.end(); ++it_s) { slotStatusProgressBar(++cnt, 0); MyMoneySchedule sch = (*it_s); if (sch.replaceId(categoryId, d->m_selectedAccount.id())) { file->modifySchedule(sch); } } slotStatusProgressBar(slist.count(), 0); } // now fix all budgets { KMSTATUS(i18n("Adjusting budgets...")); QList blist = file->budgetList(); QList::const_iterator it_b; for (it_b = blist.constBegin(); it_b != blist.constEnd(); ++it_b) { if ((*it_b).hasReferenceTo(d->m_selectedAccount.id())) { MyMoneyBudget b = (*it_b); MyMoneyBudget::AccountGroup fromBudget = b.account(d->m_selectedAccount.id()); MyMoneyBudget::AccountGroup toBudget = b.account(categoryId); toBudget += fromBudget; b.setAccount(toBudget, categoryId); b.removeReference(d->m_selectedAccount.id()); file->modifyBudget(b); } } slotStatusProgressBar(blist.count(), 0); } } catch (MyMoneyException &e) { KMessageBox::error(this, i18n("Unable to exchange category %1 with category %2. Reason: %3", d->m_selectedAccount.name(), newCategory.name(), e.what())); exit = true; } slotStatusProgressBar(-1, -1); } if (exit) return; // retain the account name for a possible later usage in the error message box // since the account removal notifies the views the selected account can be changed // so we make sure by doing this that we display the correct name in the error message QString selectedAccountName = d->m_selectedAccount.name(); // at this point, we must not have a reference to the account // to be deleted anymore switch (d->m_selectedAccount.accountGroup()) { // special handling for categories to allow deleting of empty subcategories case eMyMoney::Account::Type::Income: case eMyMoney::Account::Type::Expense: { // open a compound statement here to be able to declare variables // which would otherwise not work within a case label. // case A - only a single, unused category without subcats selected if (d->m_selectedAccount.accountList().isEmpty()) { if (!needAskUser || (KMessageBox::questionYesNo(this, QString("") + i18n("Do you really want to delete category %1?", selectedAccountName) + QString("")) == KMessageBox::Yes)) { try { file->removeAccount(d->m_selectedAccount); d->m_selectedAccount.clearId(); slotUpdateActions(); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::error(this, QString("") + i18n("Unable to delete category %1. Cause: %2", selectedAccountName, e.what()) + QString("")); } } return; } // case B - we have some subcategories, maybe the user does not want to // delete them all, but just the category itself? MyMoneyAccount parentAccount = file->account(d->m_selectedAccount.parentAccountId()); QStringList accountsToReparent; int result = KMessageBox::questionYesNoCancel(this, QString("") + i18n("Do you want to delete category %1 with all its sub-categories or only " "the category itself? If you only delete the category itself, all its sub-categories " "will be made sub-categories of %2.", selectedAccountName, parentAccount.name()) + QString(""), QString(), KGuiItem(i18n("Delete all")), KGuiItem(i18n("Just the category"))); if (result == KMessageBox::Cancel) return; // cancel pressed? ok, no delete then... // "No" means "Just the category" and that means we need to reparent all subaccounts bool need_confirmation = false; // case C - User only wants to delete the category itself if (result == KMessageBox::No) accountsToReparent = d->m_selectedAccount.accountList(); else { // case D - User wants to delete all subcategories, now check all subcats of // d->m_selectedAccount and remember all that cannot be deleted and // must be "reparented" foreach (const auto accountID, d->m_selectedAccount.accountList()) { // reparent account if a transaction is assigned if (file->transactionCount(accountID) != 0) accountsToReparent.push_back(accountID); else if (!file->account(accountID).accountList().isEmpty()) { // or if we have at least one sub-account that is used for transactions if (!file->hasOnlyUnusedAccounts(file->account(accountID).accountList())) { accountsToReparent.push_back(accountID); //qDebug() << "subaccount not empty"; } } } if (!accountsToReparent.isEmpty()) need_confirmation = true; } if (!accountsToReparent.isEmpty() && need_confirmation) { if (KMessageBox::questionYesNo(this, i18n("

Some sub-categories of category %1 cannot " "be deleted, because they are still used. They will be made sub-categories of %2. Proceed?

", selectedAccountName, parentAccount.name())) != KMessageBox::Yes) { return; // user gets wet feet... } } // all good, now first reparent selected sub-categories try { MyMoneyAccount parent = file->account(d->m_selectedAccount.parentAccountId()); for (QStringList::const_iterator it = accountsToReparent.constBegin(); it != accountsToReparent.constEnd(); ++it) { MyMoneyAccount child = file->account(*it); file->reparentAccount(child, parent); } // reload the account because the sub-account list might have changed d->m_selectedAccount = file->account(d->m_selectedAccount.id()); // now recursively delete remaining sub-categories file->removeAccountList(d->m_selectedAccount.accountList()); // don't forget to update d->m_selectedAccount, because we still have a copy of // the old account list, which is no longer valid d->m_selectedAccount = file->account(d->m_selectedAccount.id()); } catch (const MyMoneyException &e) { KMessageBox::error(this, QString("") + i18n("Unable to delete a sub-category of category %1. Reason: %2", selectedAccountName, e.what()) + QString("")); return; } } break; // the category/account is deleted after the switch default: if (!d->m_selectedAccount.accountList().isEmpty()) return; // can't delete accounts which still have subaccounts if (KMessageBox::questionYesNo(this, i18n("

Do you really want to " "delete account %1?

", selectedAccountName)) != KMessageBox::Yes) { return; // ok, you don't want to? why did you click then, hmm? } } // switch; try { file->removeAccount(d->m_selectedAccount); d->m_selectedAccount.clearId(); slotUpdateActions(); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::error(this, i18n("Unable to delete account '%1'. Cause: %2", selectedAccountName, e.what())); } } void KMyMoneyApp::slotAccountEdit() { MyMoneyFile* file = MyMoneyFile::instance(); if (!d->m_selectedAccount.id().isEmpty()) { if (!file->isStandardAccount(d->m_selectedAccount.id())) { if (d->m_selectedAccount.accountType() != eMyMoney::Account::Type::Loan && d->m_selectedAccount.accountType() != eMyMoney::Account::Type::AssetLoan) { QString caption; bool category = false; switch (d->m_selectedAccount.accountGroup()) { default: caption = i18n("Edit account '%1'", d->m_selectedAccount.name()); break; case eMyMoney::Account::Type::Expense: case eMyMoney::Account::Type::Income: caption = i18n("Edit category '%1'", d->m_selectedAccount.name()); category = true; break; } // set a status message so that the application can't be closed until the editing is done slotStatusMsg(caption); QString tid = file->openingBalanceTransaction(d->m_selectedAccount); MyMoneyTransaction t; MyMoneySplit s0, s1; QPointer dlg = new KNewAccountDlg(d->m_selectedAccount, true, category, 0, caption); if (category) { dlg->setOpeningBalanceShown(false); dlg->setOpeningDateShown(false); tid.clear(); } else { if (!tid.isEmpty()) { try { t = file->transaction(tid); s0 = t.splitByAccount(d->m_selectedAccount.id()); s1 = t.splitByAccount(d->m_selectedAccount.id(), false); dlg->setOpeningBalance(s0.shares()); if (d->m_selectedAccount.accountGroup() == eMyMoney::Account::Type::Liability) { dlg->setOpeningBalance(-s0.shares()); } } catch (const MyMoneyException &e) { qDebug() << "Error retrieving opening balance transaction " << tid << ": " << e.what() << "\n"; tid.clear(); } } } // check for online modules QMap::const_iterator it_plugin = d->m_onlinePlugins.constEnd(); const MyMoneyKeyValueContainer& kvp = d->m_selectedAccount.onlineBankingSettings(); if (!kvp["provider"].isEmpty()) { // if we have an online provider for this account, we need to check // that we have the corresponding plugin. If that exists, we ask it // to provide an additional tab for the account editor. it_plugin = d->m_onlinePlugins.constFind(kvp["provider"]); if (it_plugin != d->m_onlinePlugins.constEnd()) { QString name; QWidget *w = (*it_plugin)->accountConfigTab(d->m_selectedAccount, name); dlg->addTab(w, name); } } if (dlg != 0 && dlg->exec() == QDialog::Accepted) { try { MyMoneyFileTransaction ft; MyMoneyAccount account = dlg->account(); MyMoneyAccount parent = dlg->parentAccount(); if (it_plugin != d->m_onlinePlugins.constEnd()) { account.setOnlineBankingSettings((*it_plugin)->onlineBankingSettings(account.onlineBankingSettings())); } MyMoneyMoney bal = dlg->openingBalance(); if (d->m_selectedAccount.accountGroup() == eMyMoney::Account::Type::Liability) { bal = -bal; } // we need to modify first, as reparent would override all other changes file->modifyAccount(account); if (account.parentAccountId() != parent.id()) { file->reparentAccount(account, parent); } if (!tid.isEmpty() && dlg->openingBalance().isZero()) { file->removeTransaction(t); } else if (!tid.isEmpty() && !dlg->openingBalance().isZero()) { s0.setShares(bal); s0.setValue(bal); t.modifySplit(s0); s1.setShares(-bal); s1.setValue(-bal); t.modifySplit(s1); t.setPostDate(account.openingDate()); file->modifyTransaction(t); } else if (tid.isEmpty() && !dlg->openingBalance().isZero()) { file->createOpeningBalanceTransaction(d->m_selectedAccount, bal); } ft.commit(); // reload the account object as it might have changed in the meantime slotSelectAccount(file->account(account.id())); } catch (const MyMoneyException &e) { KMessageBox::error(this, i18n("Unable to modify account '%1'. Cause: %2", d->m_selectedAccount.name(), e.what())); } } delete dlg; ready(); } else { QPointer wizard = new KEditLoanWizard(d->m_selectedAccount); connect(wizard, SIGNAL(newCategory(MyMoneyAccount&)), this, SLOT(slotCategoryNew(MyMoneyAccount&))); connect(wizard, SIGNAL(createPayee(QString,QString&)), this, SLOT(slotPayeeNew(QString,QString&))); if (wizard->exec() == QDialog::Accepted && wizard != 0) { MyMoneySchedule sch; try { MyMoneySchedule sch = file->schedule(d->m_selectedAccount.value("schedule").toLatin1()); } catch (const MyMoneyException &e) { qDebug() << "schedule" << d->m_selectedAccount.value("schedule").toLatin1() << "not found"; } if (!(d->m_selectedAccount == wizard->account()) || !(sch == wizard->schedule())) { MyMoneyFileTransaction ft; try { file->modifyAccount(wizard->account()); if (!sch.id().isEmpty()) { sch = wizard->schedule(); } try { file->schedule(sch.id()); file->modifySchedule(sch); ft.commit(); } catch (const MyMoneyException &) { try { if(sch.transaction().splitCount() >= 2) { file->addSchedule(sch); } ft.commit(); } catch (const MyMoneyException &e) { qDebug("Cannot add schedule: '%s'", qPrintable(e.what())); } } } catch (const MyMoneyException &e) { qDebug("Unable to modify account %s: '%s'", qPrintable(d->m_selectedAccount.name()), qPrintable(e.what())); } } } delete wizard; } } } } QList > KMyMoneyApp::Private::automaticReconciliation(const MyMoneyAccount &account, const QList > &transactions, const MyMoneyMoney &amount) { static const int NR_OF_STEPS_LIMIT = 300000; static const int PROGRESSBAR_STEPS = 1000; QList > result = transactions; KMSTATUS(i18n("Running automatic reconciliation")); int progressBarIndex = 0; kmymoney->slotStatusProgressBar(progressBarIndex, NR_OF_STEPS_LIMIT / PROGRESSBAR_STEPS); // optimize the most common case - all transactions should be cleared QListIterator > itTransactionSplitResult(result); MyMoneyMoney transactionsBalance; while (itTransactionSplitResult.hasNext()) { const QPair &transactionSplit = itTransactionSplitResult.next(); transactionsBalance += transactionSplit.second.shares(); } if (amount == transactionsBalance) { result = transactions; return result; } kmymoney->slotStatusProgressBar(progressBarIndex++, 0); // only one transaction is uncleared itTransactionSplitResult.toFront(); int index = 0; while (itTransactionSplitResult.hasNext()) { const QPair &transactionSplit = itTransactionSplitResult.next(); if (transactionsBalance - transactionSplit.second.shares() == amount) { result.removeAt(index); return result; } index++; } kmymoney->slotStatusProgressBar(progressBarIndex++, 0); // more than one transaction is uncleared - apply the algorithm result.clear(); const MyMoneySecurity &security = MyMoneyFile::instance()->security(account.currencyId()); double precision = 0.1 / account.fraction(security); QList sumList; sumList << MyMoneyMoney(); QMap > > sumToComponentsMap; // compute the possible matches QListIterator > itTransactionSplit(transactions); while (itTransactionSplit.hasNext()) { const QPair &transactionSplit = itTransactionSplit.next(); QListIterator itSum(sumList); QList tempList; while (itSum.hasNext()) { const MyMoneyMoney &sum = itSum.next(); QList > splitIds; splitIds << qMakePair(transactionSplit.first.id(), transactionSplit.second.id()); if (sumToComponentsMap.contains(sum)) { if (sumToComponentsMap.value(sum).contains(qMakePair(transactionSplit.first.id(), transactionSplit.second.id()))) { continue; } splitIds.append(sumToComponentsMap.value(sum)); } tempList << transactionSplit.second.shares() + sum; sumToComponentsMap[transactionSplit.second.shares() + sum] = splitIds; int size = sumToComponentsMap.size(); if (size % PROGRESSBAR_STEPS == 0) { kmymoney->slotStatusProgressBar(progressBarIndex++, 0); } if (size > NR_OF_STEPS_LIMIT) { return result; // it's taking too much resources abort the algorithm } } QList unionList; unionList.append(tempList); unionList.append(sumList); qSort(unionList); sumList.clear(); MyMoneyMoney smallestSumFromUnion = unionList.first(); sumList.append(smallestSumFromUnion); QListIterator itUnion(unionList); while (itUnion.hasNext()) { MyMoneyMoney sumFromUnion = itUnion.next(); if (smallestSumFromUnion < MyMoneyMoney(1 - precision / transactions.size())*sumFromUnion) { smallestSumFromUnion = sumFromUnion; sumList.append(sumFromUnion); } } } kmymoney->slotStatusProgressBar(NR_OF_STEPS_LIMIT / PROGRESSBAR_STEPS, 0); if (sumToComponentsMap.contains(amount)) { QListIterator > itTransactionSplit(transactions); while (itTransactionSplit.hasNext()) { const QPair &transactionSplit = itTransactionSplit.next(); const QList > &splitIds = sumToComponentsMap.value(amount); if (splitIds.contains(qMakePair(transactionSplit.first.id(), transactionSplit.second.id()))) { result.append(transactionSplit); } } } #ifdef KMM_DEBUG qDebug("For the amount %s a number of %d possible sums where computed from the set of %d transactions: ", qPrintable(MyMoneyUtils::formatMoney(amount, security)), sumToComponentsMap.size(), transactions.size()); #endif kmymoney->slotStatusProgressBar(-1, -1); return result; } void KMyMoneyApp::slotAccountReconcileStart() { MyMoneyFile* file = MyMoneyFile::instance(); MyMoneyAccount account; // we cannot reconcile standard accounts if (!file->isStandardAccount(d->m_selectedAccount.id())) { // check if we can reconcile this account // it make's sense for asset and liability accounts try { // check if we have overdue schedules for this account QList schedules = file->scheduleList(d->m_selectedAccount.id(), eMyMoney::Schedule::Type::Any, eMyMoney::Schedule::Occurrence::Any, eMyMoney::Schedule::PaymentType::Any, QDate(), QDate(), true); if (schedules.count() > 0) { if (KMessageBox::questionYesNo(this, i18n("KMyMoney has detected some overdue scheduled transactions for this account. Do you want to enter those scheduled transactions now?"), i18n("Scheduled transactions found")) == KMessageBox::Yes) { QMap skipMap; bool processedOne; eDialogs::ScheduleResultCode rc = eDialogs::ScheduleResultCode::Enter; do { processedOne = false; QList::const_iterator it_sch; for (it_sch = schedules.constBegin(); (rc != eDialogs::ScheduleResultCode::Cancel) && (it_sch != schedules.constEnd()); ++it_sch) { MyMoneySchedule sch(*(it_sch)); // and enter it if it is not on the skip list if (skipMap.find((*it_sch).id()) == skipMap.end()) { rc = enterSchedule(sch, false, true); if (rc == eDialogs::ScheduleResultCode::Ignore) { skipMap[(*it_sch).id()] = true; } } } // reload list (maybe this schedule needs to be added again) schedules = file->scheduleList(d->m_selectedAccount.id(), eMyMoney::Schedule::Type::Any, eMyMoney::Schedule::Occurrence::Any, eMyMoney::Schedule::PaymentType::Any, QDate(), QDate(), true); } while (processedOne); } } account = file->account(d->m_selectedAccount.id()); // get rid of previous run. delete d->m_endingBalanceDlg; d->m_endingBalanceDlg = new KEndingBalanceDlg(account, this); if (account.isAssetLiability()) { connect(d->m_endingBalanceDlg, SIGNAL(createPayee(QString,QString&)), this, SLOT(slotPayeeNew(QString,QString&))); connect(d->m_endingBalanceDlg, SIGNAL(createCategory(MyMoneyAccount&,MyMoneyAccount)), this, SLOT(slotCategoryNew(MyMoneyAccount&,MyMoneyAccount))); if (d->m_endingBalanceDlg->exec() == QDialog::Accepted) { if (KMyMoneyGlobalSettings::autoReconciliation()) { MyMoneyMoney startBalance = d->m_endingBalanceDlg->previousBalance(); MyMoneyMoney endBalance = d->m_endingBalanceDlg->endingBalance(); QDate endDate = d->m_endingBalanceDlg->statementDate(); QList > transactionList; MyMoneyTransactionFilter filter(account.id()); filter.addState((int)eMyMoney::TransactionFilter::State::Cleared); filter.addState((int)eMyMoney::TransactionFilter::State::NotReconciled); filter.setDateFilter(QDate(), endDate); filter.setConsiderCategory(false); filter.setReportAllSplits(true); file->transactionList(transactionList, filter); QList > result = d->automaticReconciliation(account, transactionList, endBalance - startBalance); if (!result.empty()) { QString message = i18n("KMyMoney has detected transactions matching your reconciliation data.\nWould you like KMyMoney to clear these transactions for you?"); if (KMessageBox::questionYesNo(this, message, i18n("Automatic reconciliation"), KStandardGuiItem::yes(), KStandardGuiItem::no(), "AcceptAutomaticReconciliation") == KMessageBox::Yes) { // mark the transactions cleared KMyMoneyRegister::SelectedTransactions oldSelection = d->m_selectedTransactions; d->m_selectedTransactions.clear(); QListIterator > itTransactionSplitResult(result); while (itTransactionSplitResult.hasNext()) { const QPair &transactionSplit = itTransactionSplitResult.next(); d->m_selectedTransactions.append(KMyMoneyRegister::SelectedTransaction(transactionSplit.first, transactionSplit.second, QString())); } // mark all transactions in d->m_selectedTransactions as 'Cleared' markTransaction(eMyMoney::Split::State::Cleared); d->m_selectedTransactions = oldSelection; } } } if (d->m_myMoneyView->startReconciliation(account, d->m_endingBalanceDlg->statementDate(), d->m_endingBalanceDlg->endingBalance())) { // check if the user requests us to create interest // or charge transactions. MyMoneyTransaction ti = d->m_endingBalanceDlg->interestTransaction(); MyMoneyTransaction tc = d->m_endingBalanceDlg->chargeTransaction(); MyMoneyFileTransaction ft; try { if (ti != MyMoneyTransaction()) { MyMoneyFile::instance()->addTransaction(ti); } if (tc != MyMoneyTransaction()) { MyMoneyFile::instance()->addTransaction(tc); } ft.commit(); } catch (const MyMoneyException &e) { qWarning("interest transaction not stored: '%s'", qPrintable(e.what())); } // reload the account object as it might have changed in the meantime d->m_reconciliationAccount = file->account(account.id()); slotUpdateActions(); } } } } catch (const MyMoneyException &) { } } } void KMyMoneyApp::slotAccountReconcileFinish() { MyMoneyFile* file = MyMoneyFile::instance(); if (!d->m_reconciliationAccount.id().isEmpty()) { // retrieve list of all transactions that are not reconciled or cleared QList > transactionList; MyMoneyTransactionFilter filter(d->m_reconciliationAccount.id()); filter.addState((int)eMyMoney::TransactionFilter::State::Cleared); filter.addState((int)eMyMoney::TransactionFilter::State::NotReconciled); filter.setDateFilter(QDate(), d->m_endingBalanceDlg->statementDate()); filter.setConsiderCategory(false); filter.setReportAllSplits(true); file->transactionList(transactionList, filter); MyMoneyMoney balance = MyMoneyFile::instance()->balance(d->m_reconciliationAccount.id(), d->m_endingBalanceDlg->statementDate()); MyMoneyMoney actBalance, clearedBalance; actBalance = clearedBalance = balance; // walk the list of transactions to figure out the balance(s) QList >::const_iterator it; for (it = transactionList.constBegin(); it != transactionList.constEnd(); ++it) { if ((*it).second.reconcileFlag() == eMyMoney::Split::State::NotReconciled) { clearedBalance -= (*it).second.shares(); } } if (d->m_endingBalanceDlg->endingBalance() != clearedBalance) { QString message = i18n("You are about to finish the reconciliation of this account with a difference between your bank statement and the transactions marked as cleared.\n" "Are you sure you want to finish the reconciliation?"); if (KMessageBox::questionYesNo(this, message, i18n("Confirm end of reconciliation"), KStandardGuiItem::yes(), KStandardGuiItem::no()) == KMessageBox::No) return; } MyMoneyFileTransaction ft; // refresh object d->m_reconciliationAccount = file->account(d->m_reconciliationAccount.id()); // Turn off reconciliation mode d->m_myMoneyView->finishReconciliation(d->m_reconciliationAccount); // only update the last statement balance here, if we haven't a newer one due // to download of online statements. if (d->m_reconciliationAccount.value("lastImportedTransactionDate").isEmpty() || QDate::fromString(d->m_reconciliationAccount.value("lastImportedTransactionDate"), Qt::ISODate) < d->m_endingBalanceDlg->statementDate()) { d->m_reconciliationAccount.setValue("lastStatementBalance", d->m_endingBalanceDlg->endingBalance().toString()); // in case we override the last statement balance here, we have to make sure // that we don't show the online balance anymore, as it might be different d->m_reconciliationAccount.deletePair("lastImportedTransactionDate"); } d->m_reconciliationAccount.setLastReconciliationDate(d->m_endingBalanceDlg->statementDate()); // keep a record of this reconciliation d->m_reconciliationAccount.addReconciliation(d->m_endingBalanceDlg->statementDate(), d->m_endingBalanceDlg->endingBalance()); d->m_reconciliationAccount.deletePair("lastReconciledBalance"); d->m_reconciliationAccount.deletePair("statementBalance"); d->m_reconciliationAccount.deletePair("statementDate"); try { // update the account data file->modifyAccount(d->m_reconciliationAccount); /* // collect the list of cleared splits for this account filter.clear(); filter.addAccount(d->m_reconciliationAccount.id()); filter.addState(eMyMoney::TransactionFilter::Cleared); filter.setConsiderCategory(false); filter.setReportAllSplits(true); file->transactionList(transactionList, filter); */ // walk the list of transactions/splits and mark the cleared ones as reconciled QList >::iterator it; for (it = transactionList.begin(); it != transactionList.end(); ++it) { MyMoneySplit sp = (*it).second; // skip the ones that are not marked cleared if (sp.reconcileFlag() != eMyMoney::Split::State::Cleared) continue; // always retrieve a fresh copy of the transaction because we // might have changed it already with another split MyMoneyTransaction t = file->transaction((*it).first.id()); sp.setReconcileFlag(eMyMoney::Split::State::Reconciled); sp.setReconcileDate(d->m_endingBalanceDlg->statementDate()); t.modifySplit(sp); // update the engine ... file->modifyTransaction(t); // ... and the list (*it) = qMakePair(t, sp); } ft.commit(); // reload account data from engine as the data might have changed in the meantime d->m_reconciliationAccount = file->account(d->m_reconciliationAccount.id()); emit accountReconciled(d->m_reconciliationAccount, d->m_endingBalanceDlg->statementDate(), d->m_endingBalanceDlg->previousBalance(), d->m_endingBalanceDlg->endingBalance(), transactionList); } catch (const MyMoneyException &) { qDebug("Unexpected exception when setting cleared to reconcile"); } } // Turn off reconciliation mode d->m_reconciliationAccount = MyMoneyAccount(); slotUpdateActions(); } void KMyMoneyApp::slotAccountReconcilePostpone() { MyMoneyFileTransaction ft; MyMoneyFile* file = MyMoneyFile::instance(); if (!d->m_reconciliationAccount.id().isEmpty()) { // refresh object d->m_reconciliationAccount = file->account(d->m_reconciliationAccount.id()); // Turn off reconciliation mode d->m_myMoneyView->finishReconciliation(d->m_reconciliationAccount); d->m_reconciliationAccount.setValue("lastReconciledBalance", d->m_endingBalanceDlg->previousBalance().toString()); d->m_reconciliationAccount.setValue("statementBalance", d->m_endingBalanceDlg->endingBalance().toString()); d->m_reconciliationAccount.setValue("statementDate", d->m_endingBalanceDlg->statementDate().toString(Qt::ISODate)); try { file->modifyAccount(d->m_reconciliationAccount); ft.commit(); d->m_reconciliationAccount = MyMoneyAccount(); slotUpdateActions(); } catch (const MyMoneyException &) { qDebug("Unexpected exception when setting last reconcile info into account"); ft.rollback(); d->m_reconciliationAccount = file->account(d->m_reconciliationAccount.id()); } } } void KMyMoneyApp::slotAccountOpenEmpty() { slotAccountOpen(MyMoneyAccount()); } void KMyMoneyApp::slotAccountOpen(const MyMoneyObject& obj) { if (typeid(obj) != typeid(MyMoneyAccount)) return; MyMoneyFile* file = MyMoneyFile::instance(); QString id = d->m_selectedAccount.id(); // if the caller passed a non-empty object, we need to select that if (!obj.id().isEmpty()) { id = obj.id(); } // we cannot reconcile standard accounts if (!file->isStandardAccount(id)) { // check if we can open this account // currently it make's sense for asset and liability accounts try { MyMoneyAccount account = file->account(id); d->m_myMoneyView->slotLedgerSelected(account.id()); } catch (const MyMoneyException &) { } } } void KMyMoneyApp::enableCloseAccountAction(const MyMoneyAccount& acc) { QAction *a = actionCollection()->action(s_Actions[Action::AccountClose]); switch (canCloseAccount(acc)) { case KMyMoneyUtils::AccountCanClose: { a->setEnabled(true); break; } case KMyMoneyUtils::AccountBalanceNonZero: { a->setEnabled(false); a->setToolTip(i18n("The balance of the account must be zero before the account can be closed")); break; } case KMyMoneyUtils::AccountChildrenOpen: { a->setEnabled(false); a->setToolTip(i18n("All subaccounts must be closed before the account can be closed")); break; } case KMyMoneyUtils::AccountScheduleReference: { a->setEnabled(false); a->setToolTip(i18n("This account is still included in an active schedule")); break; } } } KMyMoneyUtils::CanCloseAccountCodeE KMyMoneyApp::canCloseAccount(const MyMoneyAccount& acc) const { // balance must be zero if (!acc.balance().isZero()) return KMyMoneyUtils::AccountBalanceNonZero; // all children must be already closed foreach (const auto sAccount, acc.accountList()) { if (!MyMoneyFile::instance()->account(sAccount).isClosed()) { return KMyMoneyUtils::AccountChildrenOpen; } } // there must be no unfinished schedule referencing the account QList list = MyMoneyFile::instance()->scheduleList(); QList::const_iterator it_l; for (it_l = list.constBegin(); it_l != list.constEnd(); ++it_l) { if ((*it_l).isFinished()) continue; if ((*it_l).hasReferenceTo(acc.id())) return KMyMoneyUtils::AccountScheduleReference; } return KMyMoneyUtils::AccountCanClose; } void KMyMoneyApp::slotAccountClose() { MyMoneyAccount a; if (!d->m_selectedInvestment.id().isEmpty()) a = d->m_selectedInvestment; else if (!d->m_selectedAccount.id().isEmpty()) a = d->m_selectedAccount; if (a.id().isEmpty()) return; // need an account ID MyMoneyFileTransaction ft; try { a.setClosed(true); MyMoneyFile::instance()->modifyAccount(a); ft.commit(); if (KMyMoneyGlobalSettings::hideClosedAccounts()) { KMessageBox::information(this, QString("") + i18n("You have closed this account. It remains in the system because you have transactions which still refer to it, but it is not shown in the views. You can make it visible again by going to the View menu and selecting Show all accounts or by deselecting the Do not show closed accounts setting.") + QString(""), i18n("Information"), "CloseAccountInfo"); } } catch (const MyMoneyException &) { } } void KMyMoneyApp::slotAccountReopen() { MyMoneyAccount a; if (!d->m_selectedInvestment.id().isEmpty()) a = d->m_selectedInvestment; else if (!d->m_selectedAccount.id().isEmpty()) a = d->m_selectedAccount; if (a.id().isEmpty()) return; // need an account ID MyMoneyFile* file = MyMoneyFile::instance(); MyMoneyFileTransaction ft; try { while (a.isClosed()) { a.setClosed(false); file->modifyAccount(a); a = file->account(a.parentAccountId()); } ft.commit(); } catch (const MyMoneyException &) { } } void KMyMoneyApp::slotReparentAccount(const MyMoneyAccount& _src, const MyMoneyInstitution& _dst) { MyMoneyAccount src(_src); src.setInstitutionId(_dst.id()); MyMoneyFileTransaction ft; try { MyMoneyFile::instance()->modifyAccount(src); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::sorry(this, i18n("

%1 cannot be moved to institution %2. Reason: %3

", src.name(), _dst.name(), e.what())); } } void KMyMoneyApp::slotReparentAccount(const MyMoneyAccount& _src, const MyMoneyAccount& _dst) { MyMoneyAccount src(_src); MyMoneyAccount dst(_dst); MyMoneyFileTransaction ft; try { MyMoneyFile::instance()->reparentAccount(src, dst); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::sorry(this, i18n("

%1 cannot be moved to %2. Reason: %3

", src.name(), dst.name(), e.what())); } } void KMyMoneyApp::slotAccountTransactionReport() { // Generate a transaction report that contains transactions for only the // currently selected account. if (!d->m_selectedAccount.id().isEmpty()) { MyMoneyReport report( MyMoneyReport::eAccount, MyMoneyReport::eQCnumber | MyMoneyReport::eQCpayee | MyMoneyReport::eQCcategory, eMyMoney::TransactionFilter::Date::YearToDate, MyMoneyReport::eDetailAll, i18n("%1 YTD Account Transactions", d->m_selectedAccount.name()), i18n("Generated Report") ); report.setGroup(i18n("Transactions")); report.addAccount(d->m_selectedAccount.id()); d->m_myMoneyView->slotShowReport(report); } } void KMyMoneyApp::slotScheduleNew() { slotScheduleNew(MyMoneyTransaction()); } void KMyMoneyApp::slotScheduleNew(const MyMoneyTransaction& _t, eMyMoney::Schedule::Occurrence occurrence) { MyMoneySchedule schedule; schedule.setOccurrence(occurrence); // if the schedule is based on an existing transaction, // we take the post date and project it to the next // schedule in a month. if (_t != MyMoneyTransaction()) { MyMoneyTransaction t(_t); schedule.setTransaction(t); if (occurrence != eMyMoney::Schedule::Occurrence::Once) schedule.setNextDueDate(schedule.nextPayment(t.postDate())); } QPointer dlg = new KEditScheduleDlg(schedule, this); TransactionEditor* transactionEditor = dlg->startEdit(); if (transactionEditor) { KMyMoneyMVCCombo::setSubstringSearchForChildren(dlg, !KMyMoneySettings::stringMatchFromStart()); if (dlg->exec() == QDialog::Accepted && dlg != 0) { MyMoneyFileTransaction ft; try { schedule = dlg->schedule(); MyMoneyFile::instance()->addSchedule(schedule); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::error(this, i18n("Unable to add scheduled transaction: %1", e.what()), i18n("Add scheduled transaction")); } } } delete transactionEditor; delete dlg; } void KMyMoneyApp::slotScheduleEdit() { if (!d->m_selectedSchedule.id().isEmpty()) { try { MyMoneySchedule schedule = MyMoneyFile::instance()->schedule(d->m_selectedSchedule.id()); KEditScheduleDlg* sched_dlg = 0; KEditLoanWizard* loan_wiz = 0; switch (schedule.type()) { case eMyMoney::Schedule::Type::Bill: case eMyMoney::Schedule::Type::Deposit: case eMyMoney::Schedule::Type::Transfer: sched_dlg = new KEditScheduleDlg(schedule, this); d->m_transactionEditor = sched_dlg->startEdit(); if (d->m_transactionEditor) { KMyMoneyMVCCombo::setSubstringSearchForChildren(sched_dlg, !KMyMoneySettings::stringMatchFromStart()); if (sched_dlg->exec() == QDialog::Accepted) { MyMoneyFileTransaction ft; try { MyMoneySchedule sched = sched_dlg->schedule(); // Check whether the new Schedule Date // is at or before the lastPaymentDate // If it is, ask the user whether to clear the // lastPaymentDate const QDate& next = sched.nextDueDate(); const QDate& last = sched.lastPayment(); if (next.isValid() && last.isValid() && next <= last) { // Entered a date effectively no later // than previous payment. Date would be // updated automatically so we probably // want to clear it. Let's ask the user. if (KMessageBox::questionYesNo(this, QString("") + i18n("You have entered a scheduled transaction date of %1. Because the scheduled transaction was last paid on %2, KMyMoney will automatically adjust the scheduled transaction date to the next date unless the last payment date is reset. Do you want to reset the last payment date?", QLocale().toString(next, QLocale::ShortFormat), QLocale().toString(last, QLocale::ShortFormat)) + QString(""), i18n("Reset Last Payment Date"), KStandardGuiItem::yes(), KStandardGuiItem::no()) == KMessageBox::Yes) { sched.setLastPayment(QDate()); } } MyMoneyFile::instance()->modifySchedule(sched); // delete the editor before we emit the dataChanged() signal from the // engine. Calling this twice in a row does not hurt. deleteTransactionEditor(); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(this, i18n("Unable to modify scheduled transaction '%1'", d->m_selectedSchedule.name()), e.what()); } } deleteTransactionEditor(); } delete sched_dlg; break; case eMyMoney::Schedule::Type::LoanPayment: loan_wiz = new KEditLoanWizard(schedule.account(2)); connect(loan_wiz, SIGNAL(newCategory(MyMoneyAccount&)), this, SLOT(slotCategoryNew(MyMoneyAccount&))); connect(loan_wiz, SIGNAL(createPayee(QString,QString&)), this, SLOT(slotPayeeNew(QString,QString&))); if (loan_wiz->exec() == QDialog::Accepted) { MyMoneyFileTransaction ft; try { MyMoneyFile::instance()->modifySchedule(loan_wiz->schedule()); MyMoneyFile::instance()->modifyAccount(loan_wiz->account()); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(this, i18n("Unable to modify scheduled transaction '%1'", d->m_selectedSchedule.name()), e.what()); } } delete loan_wiz; break; case eMyMoney::Schedule::Type::Any: break; } } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(this, i18n("Unable to modify scheduled transaction '%1'", d->m_selectedSchedule.name()), e.what()); } } } void KMyMoneyApp::slotScheduleDelete() { if (!d->m_selectedSchedule.id().isEmpty()) { MyMoneyFileTransaction ft; try { MyMoneySchedule sched = MyMoneyFile::instance()->schedule(d->m_selectedSchedule.id()); QString msg = i18n("

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

", d->m_selectedSchedule.name()); if (sched.type() == eMyMoney::Schedule::Type::LoanPayment) { msg += QString(" "); msg += i18n("In case of loan payments it is currently not possible to recreate the scheduled transaction."); } if (KMessageBox::questionYesNo(this, msg) == KMessageBox::No) return; MyMoneyFile::instance()->removeSchedule(sched); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(this, i18n("Unable to remove scheduled transaction '%1'", d->m_selectedSchedule.name()), e.what()); } } } void KMyMoneyApp::slotScheduleDuplicate() { // since we may jump here via code, we have to make sure to react only // if the action is enabled if (kmymoney->actionCollection()->action(s_Actions[Action::ScheduleDuplicate])->isEnabled()) { MyMoneySchedule sch = d->m_selectedSchedule; sch.clearId(); sch.setLastPayment(QDate()); sch.setName(i18nc("Copy of scheduled transaction name", "Copy of %1", sch.name())); // make sure that we set a valid next due date if the original next due date is invalid if (!sch.nextDueDate().isValid()) sch.setNextDueDate(QDate::currentDate()); MyMoneyFileTransaction ft; try { MyMoneyFile::instance()->addSchedule(sch); ft.commit(); // select the new schedule in the view if (!d->m_selectedSchedule.id().isEmpty()) d->m_myMoneyView->slotScheduleSelected(sch.id()); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(0, i18n("Unable to duplicate scheduled transaction: '%1'", d->m_selectedSchedule.name()), e.what()); } } } void KMyMoneyApp::slotScheduleSkip() { if (!d->m_selectedSchedule.id().isEmpty()) { try { MyMoneySchedule schedule = MyMoneyFile::instance()->schedule(d->m_selectedSchedule.id()); skipSchedule(schedule); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(this, i18n("Unknown scheduled transaction '%1'", d->m_selectedSchedule.name()), e.what()); } } } void KMyMoneyApp::skipSchedule(MyMoneySchedule& schedule) { if (!schedule.id().isEmpty()) { try { schedule = MyMoneyFile::instance()->schedule(schedule.id()); if (!schedule.isFinished()) { if (schedule.occurrence() != eMyMoney::Schedule::Occurrence::Once) { QDate next = schedule.nextDueDate(); if (!schedule.isFinished() && (KMessageBox::questionYesNo(this, QString("") + i18n("Do you really want to skip the %1 transaction scheduled for %2?", schedule.name(), QLocale().toString(next, QLocale::ShortFormat)) + QString(""))) == KMessageBox::Yes) { MyMoneyFileTransaction ft; schedule.setLastPayment(next); schedule.setNextDueDate(schedule.nextPayment(next)); MyMoneyFile::instance()->modifySchedule(schedule); ft.commit(); } } } } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(this, QString("") + i18n("Unable to skip scheduled transaction %1.", schedule.name()) + QString(""), e.what()); } } } void KMyMoneyApp::slotScheduleEnter() { if (!d->m_selectedSchedule.id().isEmpty()) { try { MyMoneySchedule schedule = MyMoneyFile::instance()->schedule(d->m_selectedSchedule.id()); enterSchedule(schedule); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(this, i18n("Unknown scheduled transaction '%1'", d->m_selectedSchedule.name()), e.what()); } } } eDialogs::ScheduleResultCode KMyMoneyApp::enterSchedule(MyMoneySchedule& schedule, bool autoEnter, bool extendedKeys) { eDialogs::ScheduleResultCode rc = eDialogs::ScheduleResultCode::Cancel; if (!schedule.id().isEmpty()) { try { schedule = MyMoneyFile::instance()->schedule(schedule.id()); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(this, i18n("Unable to enter scheduled transaction '%1'", schedule.name()), e.what()); return rc; } QPointer dlg = new KEnterScheduleDlg(this, schedule); try { QDate origDueDate = schedule.nextDueDate(); dlg->showExtendedKeys(extendedKeys); d->m_transactionEditor = dlg->startEdit(); if (d->m_transactionEditor) { KMyMoneyMVCCombo::setSubstringSearchForChildren(dlg, !KMyMoneySettings::stringMatchFromStart()); MyMoneyTransaction torig, taccepted; d->m_transactionEditor->createTransaction(torig, dlg->transaction(), schedule.transaction().splits().isEmpty() ? MyMoneySplit() : schedule.transaction().splits().front(), true); // force actions to be available no matter what (will be updated according to the state during // slotTransactionsEnter or slotTransactionsCancel) kmymoney->actionCollection()->action(s_Actions[Action::TransactionCancel])->setEnabled(true); kmymoney->actionCollection()->action(s_Actions[Action::TransactionEnter])->setEnabled(true); KConfirmManualEnterDlg::Action action = KConfirmManualEnterDlg::ModifyOnce; if (!autoEnter || !schedule.isFixed()) { for (; dlg != 0;) { rc = eDialogs::ScheduleResultCode::Cancel; if (dlg->exec() == QDialog::Accepted && dlg != 0) { rc = dlg->resultCode(); if (rc == eDialogs::ScheduleResultCode::Enter) { d->m_transactionEditor->createTransaction(taccepted, torig, torig.splits().isEmpty() ? MyMoneySplit() : torig.splits().front(), true); // make sure to suppress comparison of some data: postDate torig.setPostDate(taccepted.postDate()); if (torig != taccepted) { QPointer cdlg = new KConfirmManualEnterDlg(schedule, this); cdlg->loadTransactions(torig, taccepted); if (cdlg->exec() == QDialog::Accepted) { action = cdlg->action(); delete cdlg; break; } delete cdlg; // the user has chosen 'cancel' during confirmation, // we go back to the editor continue; } } else if (rc == eDialogs::ScheduleResultCode::Skip) { slotTransactionsCancel(); skipSchedule(schedule); } else { slotTransactionsCancel(); } } else { if (autoEnter) { if (KMessageBox::warningYesNo(this, i18n("Are you sure you wish to stop this scheduled transaction from being entered into the register?\n\nKMyMoney will prompt you again next time it starts unless you manually enter it later.")) == KMessageBox::No) { // the user has chosen 'No' for the above question, // we go back to the editor continue; } } slotTransactionsCancel(); } break; } } // if we still have the editor around here, the user did not cancel if ((d->m_transactionEditor != 0) && (dlg != 0)) { MyMoneyFileTransaction ft; try { MyMoneyTransaction t; // add the new transaction switch (action) { case KConfirmManualEnterDlg::UseOriginal: // setup widgets with original transaction data d->m_transactionEditor->setTransaction(dlg->transaction(), dlg->transaction().splits().isEmpty() ? MyMoneySplit() : dlg->transaction().splits().front()); // and create a transaction based on that data taccepted = MyMoneyTransaction(); d->m_transactionEditor->createTransaction(taccepted, dlg->transaction(), dlg->transaction().splits().isEmpty() ? MyMoneySplit() : dlg->transaction().splits().front(), true); break; case KConfirmManualEnterDlg::ModifyAlways: torig = taccepted; torig.setPostDate(origDueDate); schedule.setTransaction(torig); break; case KConfirmManualEnterDlg::ModifyOnce: break; } QString newId; connect(d->m_transactionEditor, SIGNAL(balanceWarning(QWidget*,MyMoneyAccount,QString)), d->m_balanceWarning, SLOT(slotShowMessage(QWidget*,MyMoneyAccount,QString))); if (d->m_transactionEditor->enterTransactions(newId, false)) { if (!newId.isEmpty()) { MyMoneyTransaction t = MyMoneyFile::instance()->transaction(newId); schedule.setLastPayment(t.postDate()); } // in case the next due date is invalid, the schedule is finished // we mark it as such by setting the next due date to one day past the end QDate nextDueDate = schedule.nextPayment(origDueDate); if (!nextDueDate.isValid()) { schedule.setNextDueDate(schedule.endDate().addDays(1)); } else { schedule.setNextDueDate(nextDueDate); } MyMoneyFile::instance()->modifySchedule(schedule); rc = eDialogs::ScheduleResultCode::Enter; // delete the editor before we emit the dataChanged() signal from the // engine. Calling this twice in a row does not hurt. deleteTransactionEditor(); ft.commit(); } } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(this, i18n("Unable to enter scheduled transaction '%1'", schedule.name()), e.what()); } deleteTransactionEditor(); } } } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(this, i18n("Unable to enter scheduled transaction '%1'", schedule.name()), e.what()); } delete dlg; } return rc; } void KMyMoneyApp::slotPayeeNew(const QString& newnameBase, QString& id) { createPayeeNew(newnameBase, id); } bool KMyMoneyApp::createPayeeNew(const QString& newnameBase, QString& id) { bool doit = true; if (newnameBase != i18n("New Payee")) { // Ask the user if that is what he intended to do? QString msg = QLatin1String("") + i18n("Do you want to add %1 as payer/receiver?", newnameBase) + QLatin1String(""); if (KMessageBox::questionYesNo(this, msg, i18n("New payee/receiver"), KStandardGuiItem::yes(), KStandardGuiItem::no(), "NewPayee") == KMessageBox::No) { doit = false; // we should not keep the 'no' setting because that can confuse people like // I have seen in some usability tests. So we just delete it right away. KSharedConfigPtr kconfig = KSharedConfig::openConfig(); if (kconfig) { kconfig->group(QLatin1String("Notification Messages")).deleteEntry(QLatin1String("NewPayee")); } } } if (doit) { MyMoneyFileTransaction ft; try { QString newname(newnameBase); // adjust name until a unique name has been created int count = 0; for (;;) { try { MyMoneyFile::instance()->payeeByName(newname); newname = QString("%1 [%2]").arg(newnameBase).arg(++count); } catch (const MyMoneyException &) { break; } } MyMoneyPayee p; p.setName(newname); MyMoneyFile::instance()->addPayee(p); id = p.id(); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(this, i18n("Unable to add payee"), i18n("%1 thrown in %2:%3", e.what(), e.file(), e.line())); doit = false; } } return doit; } void KMyMoneyApp::slotPayeeNew() { QString id; slotPayeeNew(i18n("New Payee"), id); // the callbacks should have made sure, that the payees view has been // updated already. So we search for the id in the list of items // and select it. emit payeeCreated(id); } bool KMyMoneyApp::payeeInList(const QList& list, const QString& id) const { bool rc = false; QList::const_iterator it_p = list.begin(); while (it_p != list.end()) { if ((*it_p).id() == id) { rc = true; break; } ++it_p; } return rc; } void KMyMoneyApp::slotPayeeDelete() { if (d->m_selectedPayees.isEmpty()) return; // shouldn't happen // get confirmation from user QString prompt; if (d->m_selectedPayees.size() == 1) prompt = i18n("

Do you really want to remove the payee %1?

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

Do you really want to merge the selected payees?"), i18n("Merge Payees")) == KMessageBox::No) return; if (payeeReassign(KPayeeReassignDlg::TypeMerge)) // clean selection since we just deleted the selected payees slotSelectPayees(QList()); } bool KMyMoneyApp::payeeReassign(int type) { if (!(type >= 0 && type < KPayeeReassignDlg::TypeCount)) return false; MyMoneyFile * file = MyMoneyFile::instance(); MyMoneyFileTransaction ft; try { // create a transaction filter that contains all payees selected for removal MyMoneyTransactionFilter f = MyMoneyTransactionFilter(); for (QList::const_iterator it = d->m_selectedPayees.constBegin(); it != d->m_selectedPayees.constEnd(); ++it) { f.addPayee((*it).id()); } // request a list of all transactions that still use the payees in question QList translist = file->transactionList(f); // qDebug() << "[KPayeesView::slotDeletePayee] " << translist.count() << " transaction still assigned to payees"; // now get a list of all schedules that make use of one of the payees QList used_schedules; foreach (const auto schedule, file->scheduleList()) { // loop over all splits in the transaction of the schedule foreach (const auto split, schedule.transaction().splits()) { // is the payee in the split to be deleted? if (payeeInList(d->m_selectedPayees, split.payeeId())) { used_schedules.push_back(schedule); // remember this schedule break; } } } // qDebug() << "[KPayeesView::slotDeletePayee] " << used_schedules.count() << " schedules use one of the selected payees"; // and a list of all loan accounts that references one of the payees QList allAccounts; QList usedAccounts; file->accountList(allAccounts); foreach (const MyMoneyAccount &account, allAccounts) { if (account.isLoan()) { MyMoneyAccountLoan loanAccount(account); foreach (const MyMoneyPayee &payee, d->m_selectedPayees) { if (loanAccount.hasReferenceTo(payee.id())) { usedAccounts.append(account); } } } } MyMoneyPayee newPayee; bool addToMatchList = false; // if at least one payee is still referenced, we need to reassign its transactions first if (!translist.isEmpty() || !used_schedules.isEmpty() || !usedAccounts.isEmpty()) { // first create list with all non-selected payees QList remainingPayees; if (type == KPayeeReassignDlg::TypeMerge) { remainingPayees = d->m_selectedPayees; } else { remainingPayees = file->payeeList(); QList::iterator it_p; for (it_p = remainingPayees.begin(); it_p != remainingPayees.end();) { if (d->m_selectedPayees.contains(*it_p)) { it_p = remainingPayees.erase(it_p); } else { ++it_p; } } } // show error message if no payees remain if (remainingPayees.isEmpty()) { KMessageBox::sorry(this, i18n("At least one transaction/scheduled transaction or loan account is still referenced by a payee. " "Currently you have all payees selected. However, at least one payee must remain so " "that the transaction/scheduled transaction or loan account can be reassigned.")); return false; } // show transaction reassignment dialog KPayeeReassignDlg * dlg = new KPayeeReassignDlg(static_cast(type), this); KMyMoneyMVCCombo::setSubstringSearchForChildren(dlg, !KMyMoneySettings::stringMatchFromStart()); QString payee_id = dlg->show(remainingPayees); addToMatchList = dlg->addToMatchList(); delete dlg; // and kill the dialog if (payee_id.isEmpty()) return false; // the user aborted the dialog, so let's abort as well // try to get selected payee. If not possible and we are merging payees, // then we create a new one try { newPayee = file->payee(payee_id); } catch (const MyMoneyException &e) { if (type == KPayeeReassignDlg::TypeMerge) { // it's ok to use payee_id for both arguments since the first is const, // so it's garantee not to change its content if (!createPayeeNew(payee_id, payee_id)) return false; // the user aborted the dialog, so let's abort as well newPayee = file->payee(payee_id); } else { return false; } } // TODO : check if we have a report that explicitively uses one of our payees // and issue an appropriate warning try { // now loop over all transactions and reassign payee for (auto& transaction : translist) { // create a copy of the splits list in the transaction // loop over all splits for (auto& split : transaction.splits()) { // if the split is assigned to one of the selected payees, we need to modify it if (payeeInList(d->m_selectedPayees, split.payeeId())) { split.setPayeeId(payee_id); // first modify payee in current split // then modify the split in our local copy of the transaction list transaction.modifySplit(split); // this does not modify the list object 'splits'! } } // for - Splits file->modifyTransaction(transaction); // modify the transaction in the MyMoney object } // for - Transactions // now loop over all schedules and reassign payees for (auto& schedule : used_schedules) { // create copy of transaction in current schedule auto trans = schedule.transaction(); // create copy of lists of splits for (auto& split : trans.splits()) { if (payeeInList(d->m_selectedPayees, split.payeeId())) { split.setPayeeId(payee_id); trans.modifySplit(split); // does not modify the list object 'splits'! } } // for - Splits // store transaction in current schedule schedule.setTransaction(trans); file->modifySchedule(schedule); // modify the schedule in the MyMoney engine } // for - Schedules // reassign the payees in the loans that reference the deleted payees foreach (const MyMoneyAccount &account, usedAccounts) { MyMoneyAccountLoan loanAccount(account); loanAccount.setPayee(payee_id); file->modifyAccount(loanAccount); } } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(0, i18n("Unable to reassign payee of transaction/split"), i18n("%1 thrown in %2:%3", e.what(), e.file(), e.line())); } } else { // if !translist.isEmpty() if (type == KPayeeReassignDlg::TypeMerge) { KMessageBox::sorry(this, i18n("Nothing to merge."), i18n("Merge Payees")); return false; } } bool ignorecase; QStringList payeeNames; MyMoneyPayee::payeeMatchType matchType = newPayee.matchData(ignorecase, payeeNames); QStringList deletedPayeeNames; // now loop over all selected payees and remove them for (QList::iterator it = d->m_selectedPayees.begin(); it != d->m_selectedPayees.end(); ++it) { if (newPayee.id() != (*it).id()) { if (addToMatchList) { deletedPayeeNames << (*it).name(); } file->removePayee(*it); } } // if we initially have no matching turned on, we just ignore the case (default) if (matchType == MyMoneyPayee::matchDisabled) ignorecase = true; // update the destination payee if this was requested by the user if (addToMatchList && deletedPayeeNames.count() > 0) { // add new names to the list // TODO: it would be cool to somehow shrink the list to make better use // of regular expressions at this point. For now, we leave this task // to the user himeself. QStringList::const_iterator it_n; for (it_n = deletedPayeeNames.constBegin(); it_n != deletedPayeeNames.constEnd(); ++it_n) { if (matchType == MyMoneyPayee::matchKey) { // make sure we really need it and it is not caught by an existing regexp QStringList::const_iterator it_k; for (it_k = payeeNames.constBegin(); it_k != payeeNames.constEnd(); ++it_k) { QRegExp exp(*it_k, ignorecase ? Qt::CaseInsensitive : Qt::CaseSensitive); if (exp.indexIn(*it_n) != -1) break; } if (it_k == payeeNames.constEnd()) payeeNames << QRegExp::escape(*it_n); } else if (payeeNames.contains(*it_n) == 0) payeeNames << QRegExp::escape(*it_n); } // and update the payee in the engine context // make sure to turn on matching for this payee in the right mode newPayee.setMatchData(MyMoneyPayee::matchKey, ignorecase, payeeNames); file->modifyPayee(newPayee); } ft.commit(); // If we just deleted the payees, they sure don't exist anymore slotSelectPayees(QList()); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(0, i18n("Unable to remove payee(s)"), i18n("%1 thrown in %2:%3", e.what(), e.file(), e.line())); } return true; } void KMyMoneyApp::slotTagNew(const QString& newnameBase, QString& id) { bool doit = true; if (newnameBase != i18n("New Tag")) { // Ask the user if that is what he intended to do? QString msg = QString("") + i18n("Do you want to add %1 as tag?", newnameBase) + QString(""); if (KMessageBox::questionYesNo(this, msg, i18n("New tag"), KStandardGuiItem::yes(), KStandardGuiItem::no(), "NewTag") == KMessageBox::No) { doit = false; // we should not keep the 'no' setting because that can confuse people like // I have seen in some usability tests. So we just delete it right away. KSharedConfigPtr kconfig = KSharedConfig::openConfig(); if (kconfig) { kconfig->group(QLatin1String("Notification Messages")).deleteEntry(QLatin1String("NewTag")); } } } if (doit) { MyMoneyFileTransaction ft; try { QString newname(newnameBase); // adjust name until a unique name has been created int count = 0; for (;;) { try { MyMoneyFile::instance()->tagByName(newname); newname = QString("%1 [%2]").arg(newnameBase).arg(++count); } catch (const MyMoneyException &) { break; } } MyMoneyTag ta; ta.setName(newname); MyMoneyFile::instance()->addTag(ta); id = ta.id(); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(this, i18n("Unable to add tag"), i18n("%1 thrown in %2:%3", e.what(), e.file(), e.line())); } } } void KMyMoneyApp::slotTagNew() { QString id; slotTagNew(i18n("New Tag"), id); // the callbacks should have made sure, that the tags view has been // updated already. So we search for the id in the list of items // and select it. emit tagCreated(id); } bool KMyMoneyApp::tagInList(const QList& list, const QString& id) const { bool rc = false; QList::const_iterator it_p = list.begin(); while (it_p != list.end()) { if ((*it_p).id() == id) { rc = true; break; } ++it_p; } return rc; } void KMyMoneyApp::slotTagDelete() { if (d->m_selectedTags.isEmpty()) return; // shouldn't happen MyMoneyFile * file = MyMoneyFile::instance(); // first create list with all non-selected tags QList remainingTags = file->tagList(); QList::iterator it_ta; for (it_ta = remainingTags.begin(); it_ta != remainingTags.end();) { if (d->m_selectedTags.contains(*it_ta)) { it_ta = remainingTags.erase(it_ta); } else { ++it_ta; } } // get confirmation from user QString prompt; if (d->m_selectedTags.size() == 1) prompt = i18n("

Do you really want to remove the tag %1?

", d->m_selectedTags.front().name()); else prompt = i18n("Do you really want to remove all selected tags?"); if (KMessageBox::questionYesNo(this, prompt, i18n("Remove Tag")) == KMessageBox::No) return; MyMoneyFileTransaction ft; try { // create a transaction filter that contains all tags selected for removal MyMoneyTransactionFilter f = MyMoneyTransactionFilter(); for (QList::const_iterator it = d->m_selectedTags.constBegin(); it != d->m_selectedTags.constEnd(); ++it) { f.addTag((*it).id()); } // request a list of all transactions that still use the tags in question QList translist = file->transactionList(f); // qDebug() << "[KTagsView::slotDeleteTag] " << translist.count() << " transaction still assigned to tags"; // now get a list of all schedules that make use of one of the tags QList used_schedules; foreach (const auto schedule, file->scheduleList()) { // loop over all splits in the transaction of the schedule foreach (const auto split, schedule.transaction().splits()) { for (auto i = 0; i < split.tagIdList().size(); i++) { // is the tag in the split to be deleted? if (tagInList(d->m_selectedTags, split.tagIdList()[i])) { used_schedules.push_back(schedule); // remember this schedule break; } } } } // qDebug() << "[KTagsView::slotDeleteTag] " << used_schedules.count() << " schedules use one of the selected tags"; MyMoneyTag newTag; // if at least one tag is still referenced, we need to reassign its transactions first if (!translist.isEmpty() || !used_schedules.isEmpty()) { // show error message if no tags remain //FIXME-ALEX Tags are optional so we can delete all of them and simply delete every tagId from every transaction if (remainingTags.isEmpty()) { KMessageBox::sorry(this, i18n("At least one transaction/scheduled transaction is still referenced by a tag. " "Currently you have all tags selected. However, at least one tag must remain so " "that the transaction/scheduled transaction can be reassigned.")); return; } // show transaction reassignment dialog KTagReassignDlg * dlg = new KTagReassignDlg(this); KMyMoneyMVCCombo::setSubstringSearchForChildren(dlg, !KMyMoneySettings::stringMatchFromStart()); QString tag_id = dlg->show(remainingTags); delete dlg; // and kill the dialog if (tag_id.isEmpty()) //FIXME-ALEX Let the user choose to not reassign a to-be deleted tag to another one. return; // the user aborted the dialog, so let's abort as well newTag = file->tag(tag_id); // TODO : check if we have a report that explicitively uses one of our tags // and issue an appropriate warning try { // now loop over all transactions and reassign tag for (auto& transaction : translist) { // create a copy of the splits list in the transaction // loop over all splits for (auto& split : transaction.splits()) { QList tagIdList = split.tagIdList(); for (int i = 0; i < tagIdList.size(); i++) { // if the split is assigned to one of the selected tags, we need to modify it if (tagInList(d->m_selectedTags, tagIdList[i])) { tagIdList.removeAt(i); if (tagIdList.indexOf(tag_id) == -1) tagIdList.append(tag_id); i = -1; // restart from the first element } } split.setTagIdList(tagIdList); // first modify tag list in current split // then modify the split in our local copy of the transaction list transaction.modifySplit(split); // this does not modify the list object 'splits'! } // for - Splits file->modifyTransaction(transaction); // modify the transaction in the MyMoney object } // for - Transactions // now loop over all schedules and reassign tags for (auto& schedule : used_schedules) { // create copy of transaction in current schedule auto trans = schedule.transaction(); // create copy of lists of splits for (auto& split : trans.splits()) { QList tagIdList = split.tagIdList(); for (int i = 0; i < tagIdList.size(); i++) { if (tagInList(d->m_selectedTags, tagIdList[i])) { tagIdList.removeAt(i); if (tagIdList.indexOf(tag_id) == -1) tagIdList.append(tag_id); i = -1; // restart from the first element } } split.setTagIdList(tagIdList); trans.modifySplit(split); // does not modify the list object 'splits'! } // for - Splits // store transaction in current schedule schedule.setTransaction(trans); file->modifySchedule(schedule); // modify the schedule in the MyMoney engine } // for - Schedules } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(0, i18n("Unable to reassign tag of transaction/split"), i18n("%1 thrown in %2:%3", e.what(), e.file(), e.line())); } } // if !translist.isEmpty() // now loop over all selected tags and remove them for (QList::iterator it = d->m_selectedTags.begin(); it != d->m_selectedTags.end(); ++it) { file->removeTag(*it); } ft.commit(); // If we just deleted the tags, they sure don't exist anymore slotSelectTags(QList()); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(0, i18n("Unable to remove tag(s)"), i18n("%1 thrown in %2:%3", e.what(), e.file(), e.line())); } } void KMyMoneyApp::slotCurrencyNew() { QString sid = QInputDialog::getText(0, i18n("New currency"), i18n("Enter ISO 4217 code for the new currency")); if (!sid.isEmpty()) { QString id(sid); MyMoneySecurity currency(id, i18n("New currency")); MyMoneyFileTransaction ft; try { MyMoneyFile::instance()->addCurrency(currency); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::sorry(this, i18n("Cannot create new currency. %1", e.what()), i18n("New currency")); } emit currencyCreated(id); } } void KMyMoneyApp::slotCurrencyUpdate(const QString ¤cyId, const QString& currencyName, const QString& currencyTradingSymbol) { MyMoneyFile* file = MyMoneyFile::instance(); try { if (currencyName != d->m_selectedCurrency.name() || currencyTradingSymbol != d->m_selectedCurrency.tradingSymbol()) { MyMoneySecurity currency = file->currency(currencyId); currency.setName(currencyName); currency.setTradingSymbol(currencyTradingSymbol); MyMoneyFileTransaction ft; try { file->modifyCurrency(currency); d->m_selectedCurrency = currency; ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::sorry(this, i18n("Cannot update currency. %1", e.what()), i18n("Update currency")); } } } catch (const MyMoneyException &e) { KMessageBox::sorry(this, i18n("Cannot update currency. %1", e.what()), i18n("Update currency")); } } void KMyMoneyApp::slotCurrencyDelete() { if (!d->m_selectedCurrency.id().isEmpty()) { MyMoneyFileTransaction ft; try { MyMoneyFile::instance()->removeCurrency(d->m_selectedCurrency); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::sorry(this, i18n("Cannot delete currency %1. %2", d->m_selectedCurrency.name(), e.what()), i18n("Delete currency")); } } } void KMyMoneyApp::slotCurrencySetBase() { if (!d->m_selectedCurrency.id().isEmpty()) { if (d->m_selectedCurrency.id() != MyMoneyFile::instance()->baseCurrency().id()) { MyMoneyFileTransaction ft; try { MyMoneyFile::instance()->setBaseCurrency(d->m_selectedCurrency); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::sorry(this, i18n("Cannot set %1 as base currency: %2", d->m_selectedCurrency.name(), e.what()), i18n("Set base currency")); } } } } void KMyMoneyApp::slotBudgetNew() { QDate date = QDate::currentDate(); date.setDate(date.year(), KMyMoneyGlobalSettings::firstFiscalMonth(), KMyMoneyGlobalSettings::firstFiscalDay()); QString newname = i18n("Budget %1", date.year()); MyMoneyBudget budget; // make sure we have a unique name try { int i = 1; // Exception thrown when the name is not found while (1) { MyMoneyFile::instance()->budgetByName(newname); newname = i18n("Budget %1 %2", date.year(), i++); } } catch (const MyMoneyException &) { // all ok, the name is unique } MyMoneyFileTransaction ft; try { budget.setName(newname); budget.setBudgetStart(date); MyMoneyFile::instance()->addBudget(budget); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(0, i18n("Error"), i18n("Unable to add budget: %1, thrown in %2:%3", e.what(), e.file(), e.line())); } } void KMyMoneyApp::slotBudgetDelete() { if (d->m_selectedBudgets.isEmpty()) return; // shouldn't happen MyMoneyFile * file = MyMoneyFile::instance(); // get confirmation from user QString prompt; if (d->m_selectedBudgets.size() == 1) prompt = i18n("

Do you really want to remove the budget %1?

", d->m_selectedBudgets.front().name()); else prompt = i18n("Do you really want to remove all selected budgets?"); if (KMessageBox::questionYesNo(this, prompt, i18n("Remove Budget")) == KMessageBox::No) return; MyMoneyFileTransaction ft; try { // now loop over all selected budgets and remove them for (QList::iterator it = d->m_selectedBudgets.begin(); it != d->m_selectedBudgets.end(); ++it) { file->removeBudget(*it); } ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(0, i18n("Error"), i18n("Unable to remove budget: %1, thrown in %2:%3", e.what(), e.file(), e.line())); } } void KMyMoneyApp::slotBudgetCopy() { if (d->m_selectedBudgets.size() == 1) { MyMoneyFileTransaction ft; try { MyMoneyBudget budget = d->m_selectedBudgets.first(); budget.clearId(); budget.setName(i18n("Copy of %1", budget.name())); MyMoneyFile::instance()->addBudget(budget); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(0, i18n("Error"), i18n("Unable to add budget: %1, thrown in %2:%3", e.what(), e.file(), e.line())); } } } void KMyMoneyApp::slotBudgetChangeYear() { if (d->m_selectedBudgets.size() == 1) { QStringList years; int current = 0; bool haveCurrent = false; MyMoneyBudget budget = *(d->m_selectedBudgets.begin()); for (int i = (QDate::currentDate().year() - 3); i < (QDate::currentDate().year() + 5); ++i) { years << QString("%1").arg(i); if (i == budget.budgetStart().year()) { haveCurrent = true; } if (!haveCurrent) ++current; } if (!haveCurrent) current = 0; bool ok = false; QString yearString = QInputDialog::getItem(this, i18n("Select year"), i18n("Budget year"), years, current, false, &ok); if (ok) { int year = yearString.toInt(0, 0); QDate newYear = QDate(year, budget.budgetStart().month(), budget.budgetStart().day()); if (newYear != budget.budgetStart()) { MyMoneyFileTransaction ft; try { budget.setBudgetStart(newYear); MyMoneyFile::instance()->modifyBudget(budget); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(0, i18n("Error"), i18n("Unable to modify budget: %1, thrown in %2:%3", e.what(), e.file(), e.line())); } } } } } void KMyMoneyApp::slotBudgetForecast() { if (d->m_selectedBudgets.size() == 1) { MyMoneyFileTransaction ft; try { MyMoneyBudget budget = d->m_selectedBudgets.first(); bool calcBudget = budget.getaccounts().count() == 0; if (!calcBudget) { if (KMessageBox::warningContinueCancel(0, i18n("The current budget already contains data. Continuing will replace all current values of this budget."), i18nc("Warning message box", "Warning")) == KMessageBox::Continue) calcBudget = true; } if (calcBudget) { QDate historyStart; QDate historyEnd; QDate budgetStart; QDate budgetEnd; budgetStart = budget.budgetStart(); budgetEnd = budgetStart.addYears(1).addDays(-1); historyStart = budgetStart.addYears(-1); historyEnd = budgetEnd.addYears(-1); MyMoneyForecast forecast = KMyMoneyGlobalSettings::forecast(); forecast.createBudget(budget, historyStart, historyEnd, budgetStart, budgetEnd, true); MyMoneyFile::instance()->modifyBudget(budget); ft.commit(); } } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(0, i18n("Error"), i18n("Unable to modify budget: %1, thrown in %2:%3", e.what(), e.file(), e.line())); } } } void KMyMoneyApp::slotNewFeature() { } void KMyMoneyApp::slotTransactionsDelete() { // since we may jump here via code, we have to make sure to react only // if the action is enabled if (!kmymoney->actionCollection()->action(s_Actions[Action::TransactionDelete])->isEnabled()) return; if (d->m_selectedTransactions.isEmpty()) return; if (d->m_selectedTransactions.warnLevel() == 1) { if (KMessageBox::warningContinueCancel(0, i18n("At least one split of the selected transactions has been reconciled. " "Do you wish to delete the transactions anyway?"), i18n("Transaction already reconciled")) == KMessageBox::Cancel) return; } QString msg = i18np("Do you really want to delete the selected transaction?", "Do you really want to delete all %1 selected transactions?", d->m_selectedTransactions.count()); if (KMessageBox::questionYesNo(this, msg, i18n("Delete transaction")) == KMessageBox::Yes) { KMSTATUS(i18n("Deleting transactions")); doDeleteTransactions(); } } void KMyMoneyApp::slotTransactionDuplicate() { // since we may jump here via code, we have to make sure to react only // if the action is enabled if (kmymoney->actionCollection()->action(s_Actions[Action::TransactionDuplicate])->isEnabled()) { KMyMoneyRegister::SelectedTransactions selectionList = d->m_selectedTransactions; KMyMoneyRegister::SelectedTransactions::iterator it_t; int i = 0; int cnt = d->m_selectedTransactions.count(); KMSTATUS(i18n("Duplicating transactions")); slotStatusProgressBar(0, cnt); MyMoneyFileTransaction ft; MyMoneyTransaction lt; try { foreach (const auto selection, selectionList) { auto t = selection.transaction(); // wipe out any reconciliation information for (auto& split : t.splits()) { split.setReconcileFlag(eMyMoney::Split::State::NotReconciled); split.setReconcileDate(QDate()); split.setBankID(QString()); } // clear invalid data t.setEntryDate(QDate()); t.clearId(); // and set the post date to today t.setPostDate(QDate::currentDate()); MyMoneyFile::instance()->addTransaction(t); lt = t; slotStatusProgressBar(i++, 0); } ft.commit(); // select the new transaction in the ledger if (!d->m_selectedAccount.id().isEmpty()) d->m_myMoneyView->slotLedgerSelected(d->m_selectedAccount.id(), lt.id()); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(0, i18n("Error"), i18n("Unable to duplicate transaction(s): %1, thrown in %2:%3", e.what(), e.file(), e.line())); } // switch off the progress bar slotStatusProgressBar(-1, -1); } } void KMyMoneyApp::doDeleteTransactions() { KMyMoneyRegister::SelectedTransactions list = d->m_selectedTransactions; KMyMoneyRegister::SelectedTransactions::iterator it_t; int cnt = list.count(); int i = 0; slotStatusProgressBar(0, cnt); MyMoneyFileTransaction ft; MyMoneyFile* file = MyMoneyFile::instance(); try { it_t = list.begin(); while (it_t != list.end()) { // only remove those transactions that do not reference a closed account if (!file->referencesClosedAccount((*it_t).transaction())) { file->removeTransaction((*it_t).transaction()); // remove all those references in the list of selected transactions // that refer to the same transaction we just removed so that we // will not be caught by an exception later on (see bko #285310) KMyMoneyRegister::SelectedTransactions::iterator it_td = it_t; ++it_td; while (it_td != list.end()) { if ((*it_t).transaction().id() == (*it_td).transaction().id()) { it_td = list.erase(it_td); i++; // bump count of deleted transactions } else { ++it_td; } } } // need to ensure "nextCheckNumber" is still correct auto acc = file->account((*it_t).split().accountId()); // the "lastNumberUsed" might have been the txn number deleted // so adjust it QString deletedNum = (*it_t).split().number(); // decrement deletedNum and set new "lastNumberUsed" QString num = KMyMoneyUtils::getAdjacentNumber(deletedNum, -1); acc.setValue("lastNumberUsed", num); file->modifyAccount(acc); list.erase(it_t); it_t = list.begin(); slotStatusProgressBar(i++, 0); } ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(0, i18n("Error"), i18n("Unable to delete transaction(s): %1, thrown in %2:%3", e.what(), e.file(), e.line())); } slotStatusProgressBar(-1, -1); } void KMyMoneyApp::slotTransactionsNew() { // since we jump here via code, we have to make sure to react only // if the action is enabled if (kmymoney->actionCollection()->action(s_Actions[Action::TransactionNew])->isEnabled()) { if (d->m_myMoneyView->createNewTransaction()) { d->m_transactionEditor = d->m_myMoneyView->startEdit(d->m_selectedTransactions); if (d->m_transactionEditor) { KMyMoneyMVCCombo::setSubstringSearchForChildren(d->m_myMoneyView, !KMyMoneySettings::stringMatchFromStart()); KMyMoneyPayeeCombo* payeeEdit = dynamic_cast(d->m_transactionEditor->haveWidget("payee")); if (payeeEdit && !d->m_lastPayeeEnteredId.isEmpty()) { // in case we entered a new transaction before and used a payee, // we reuse it here. Save the text to the edit widget, select it // so that hitting any character will start entering another payee. payeeEdit->setSelectedItem(d->m_lastPayeeEnteredId); payeeEdit->lineEdit()->selectAll(); } if (d->m_transactionEditor) { connect(d->m_transactionEditor, SIGNAL(statusProgress(int,int)), this, SLOT(slotStatusProgressBar(int,int))); connect(d->m_transactionEditor, SIGNAL(statusMsg(QString)), this, SLOT(slotStatusMsg(QString))); connect(d->m_transactionEditor, SIGNAL(scheduleTransaction(MyMoneyTransaction,eMyMoney::Schedule::Occurrence)), this, SLOT(slotScheduleNew(MyMoneyTransaction,eMyMoney::Schedule::Occurrence))); } slotUpdateActions(); } } } } void KMyMoneyApp::slotTransactionsEdit() { // qDebug("KMyMoneyApp::slotTransactionsEdit()"); // since we jump here via code, we have to make sure to react only // if the action is enabled if (kmymoney->actionCollection()->action(s_Actions[Action::TransactionEdit])->isEnabled()) { // as soon as we edit a transaction, we don't remember the last payee entered d->m_lastPayeeEnteredId.clear(); d->m_transactionEditor = d->m_myMoneyView->startEdit(d->m_selectedTransactions); KMyMoneyMVCCombo::setSubstringSearchForChildren(d->m_myMoneyView, !KMyMoneySettings::stringMatchFromStart()); slotUpdateActions(); } } void KMyMoneyApp::deleteTransactionEditor() { // make sure, we don't use the transaction editor pointer // anymore from now on TransactionEditor* p = d->m_transactionEditor; d->m_transactionEditor = 0; delete p; } void KMyMoneyApp::slotTransactionsEditSplits() { // since we jump here via code, we have to make sure to react only // if the action is enabled if (kmymoney->actionCollection()->action(s_Actions[Action::TransactionEditSplits])->isEnabled()) { // as soon as we edit a transaction, we don't remember the last payee entered d->m_lastPayeeEnteredId.clear(); d->m_transactionEditor = d->m_myMoneyView->startEdit(d->m_selectedTransactions); slotUpdateActions(); if (d->m_transactionEditor) { KMyMoneyMVCCombo::setSubstringSearchForChildren(d->m_myMoneyView, !KMyMoneySettings::stringMatchFromStart()); if (d->m_transactionEditor->slotEditSplits() == QDialog::Accepted) { MyMoneyFileTransaction ft; try { QString id; connect(d->m_transactionEditor, SIGNAL(balanceWarning(QWidget*,MyMoneyAccount,QString)), d->m_balanceWarning, SLOT(slotShowMessage(QWidget*,MyMoneyAccount,QString))); d->m_transactionEditor->enterTransactions(id); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(0, i18n("Error"), i18n("Unable to modify transaction: %1, thrown in %2:%3", e.what(), e.file(), e.line())); } } } deleteTransactionEditor(); slotUpdateActions(); } } void KMyMoneyApp::slotTransactionsCancel() { // since we jump here via code, we have to make sure to react only // if the action is enabled if (kmymoney->actionCollection()->action(s_Actions[Action::TransactionCancel])->isEnabled()) { // make sure, we block the enter function actionCollection()->action(s_Actions[Action::TransactionEnter])->setEnabled(false); // qDebug("KMyMoneyApp::slotTransactionsCancel"); deleteTransactionEditor(); slotUpdateActions(); } } void KMyMoneyApp::slotTransactionsEnter() { // since we jump here via code, we have to make sure to react only // if the action is enabled if (kmymoney->actionCollection()->action(s_Actions[Action::TransactionEnter])->isEnabled()) { // disable the action while we process it to make sure it's processed only once since // d->m_transactionEditor->enterTransactions(newId) will run QCoreApplication::processEvents // we could end up here twice which will cause a crash slotUpdateActions() will enable the action again kmymoney->actionCollection()->action(s_Actions[Action::TransactionEnter])->setEnabled(false); if (d->m_transactionEditor) { QString accountId = d->m_selectedAccount.id(); QString newId; connect(d->m_transactionEditor, SIGNAL(balanceWarning(QWidget*,MyMoneyAccount,QString)), d->m_balanceWarning, SLOT(slotShowMessage(QWidget*,MyMoneyAccount,QString))); if (d->m_transactionEditor->enterTransactions(newId)) { KMyMoneyPayeeCombo* payeeEdit = dynamic_cast(d->m_transactionEditor->haveWidget("payee")); if (payeeEdit && !newId.isEmpty()) { d->m_lastPayeeEnteredId = payeeEdit->selectedItem(); } deleteTransactionEditor(); } if (!newId.isEmpty()) { d->m_myMoneyView->slotLedgerSelected(accountId, newId); } } slotUpdateActions(); } } void KMyMoneyApp::slotTransactionsCancelOrEnter(bool& okToSelect) { static bool oneTime = false; if (!oneTime) { oneTime = true; QString dontShowAgain = "CancelOrEditTransaction"; // qDebug("KMyMoneyApp::slotCancelOrEndEdit"); if (d->m_transactionEditor) { if (KMyMoneyGlobalSettings::focusChangeIsEnter() && kmymoney->actionCollection()->action(s_Actions[Action::TransactionEnter])->isEnabled()) { slotTransactionsEnter(); if (d->m_transactionEditor) { // if at this stage the editor is still there that means that entering the transaction was cancelled // for example by pressing cancel on the exchange rate editor so we must stay in edit mode okToSelect = false; } } else { // okToSelect is preset to true if a cancel of the dialog is useful and false if it is not int rc; KGuiItem noGuiItem = KStandardGuiItem::save(); KGuiItem yesGuiItem = KStandardGuiItem::discard(); KGuiItem cancelGuiItem = KStandardGuiItem::cont(); // if the transaction can't be entered make sure that it can't be entered by pressing no either if (!kmymoney->actionCollection()->action(s_Actions[Action::TransactionEnter])->isEnabled()) { noGuiItem.setEnabled(false); noGuiItem.setToolTip(kmymoney->actionCollection()->action(s_Actions[Action::TransactionEnter])->toolTip()); } if (okToSelect == true) { rc = KMessageBox::warningYesNoCancel(0, i18n("

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

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

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

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

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

"), i18n("End transaction edit"), yesGuiItem, noGuiItem, dontShowAgain); } switch (rc) { case KMessageBox::Yes: slotTransactionsCancel(); break; case KMessageBox::No: slotTransactionsEnter(); // make sure that we'll see this message the next time no matter // if the user has chosen the 'Don't show again' checkbox KMessageBox::enableMessage(dontShowAgain); if (d->m_transactionEditor) { // if at this stage the editor is still there that means that entering the transaction was cancelled // for example by pressing cancel on the exchange rate editor so we must stay in edit mode okToSelect = false; } break; case KMessageBox::Cancel: // make sure that we'll see this message the next time no matter // if the user has chosen the 'Don't show again' checkbox KMessageBox::enableMessage(dontShowAgain); okToSelect = false; break; } } } oneTime = false; } } void KMyMoneyApp::slotToggleReconciliationFlag() { markTransaction(eMyMoney::Split::State::Unknown); } void KMyMoneyApp::slotMarkTransactionCleared() { markTransaction(eMyMoney::Split::State::Cleared); } void KMyMoneyApp::slotMarkTransactionReconciled() { markTransaction(eMyMoney::Split::State::Reconciled); } void KMyMoneyApp::slotMarkTransactionNotReconciled() { markTransaction(eMyMoney::Split::State::NotReconciled); } void KMyMoneyApp::markTransaction(eMyMoney::Split::State flag) { KMyMoneyRegister::SelectedTransactions list = d->m_selectedTransactions; KMyMoneyRegister::SelectedTransactions::const_iterator it_t; int cnt = list.count(); int i = 0; slotStatusProgressBar(0, cnt); MyMoneyFileTransaction ft; try { for (it_t = list.constBegin(); it_t != list.constEnd(); ++it_t) { // turn on signals before we modify the last entry in the list cnt--; MyMoneyFile::instance()->blockSignals(cnt != 0); // get a fresh copy MyMoneyTransaction t = MyMoneyFile::instance()->transaction((*it_t).transaction().id()); MyMoneySplit sp = t.splitById((*it_t).split().id()); if (sp.reconcileFlag() != flag) { if (flag == eMyMoney::Split::State::Unknown) { if (d->m_reconciliationAccount.id().isEmpty()) { // in normal mode we cycle through all states switch (sp.reconcileFlag()) { case eMyMoney::Split::State::NotReconciled: sp.setReconcileFlag(eMyMoney::Split::State::Cleared); break; case eMyMoney::Split::State::Cleared: sp.setReconcileFlag(eMyMoney::Split::State::Reconciled); break; case eMyMoney::Split::State::Reconciled: sp.setReconcileFlag(eMyMoney::Split::State::NotReconciled); break; default: break; } } else { // in reconciliation mode we skip the reconciled state switch (sp.reconcileFlag()) { case eMyMoney::Split::State::NotReconciled: sp.setReconcileFlag(eMyMoney::Split::State::Cleared); break; case eMyMoney::Split::State::Cleared: sp.setReconcileFlag(eMyMoney::Split::State::NotReconciled); break; default: break; } } } else { sp.setReconcileFlag(flag); } t.modifySplit(sp); MyMoneyFile::instance()->modifyTransaction(t); } slotStatusProgressBar(i++, 0); } slotStatusProgressBar(-1, -1); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(0, i18n("Error"), i18n("Unable to modify transaction: %1, thrown in %2:%3", e.what(), e.file(), e.line())); } } void KMyMoneyApp::slotTransactionsAccept() { KMyMoneyRegister::SelectedTransactions list = d->m_selectedTransactions; KMyMoneyRegister::SelectedTransactions::const_iterator it_t; int cnt = list.count(); int i = 0; slotStatusProgressBar(0, cnt); MyMoneyFileTransaction ft; try { for (it_t = list.constBegin(); it_t != list.constEnd(); ++it_t) { // reload transaction in case it got changed during the course of this loop MyMoneyTransaction t = MyMoneyFile::instance()->transaction((*it_t).transaction().id()); if (t.isImported()) { t.setImported(false); if (!d->m_selectedAccount.id().isEmpty()) { foreach (const auto split, t.splits()) { if (split.accountId() == d->m_selectedAccount.id()) { if (split.reconcileFlag() == eMyMoney::Split::State::NotReconciled) { MyMoneySplit s = split; s.setReconcileFlag(eMyMoney::Split::State::Cleared); t.modifySplit(s); } } } } MyMoneyFile::instance()->modifyTransaction(t); } if ((*it_t).split().isMatched()) { // reload split in case it got changed during the course of this loop MyMoneySplit s = t.splitById((*it_t).split().id()); TransactionMatcher matcher(d->m_selectedAccount); matcher.accept(t, s); } slotStatusProgressBar(i++, 0); } slotStatusProgressBar(-1, -1); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(0, i18n("Error"), i18n("Unable to accept transaction: %1, thrown in %2:%3", e.what(), e.file(), e.line())); } } void KMyMoneyApp::slotTransactionGotoAccount() { if (!d->m_accountGoto.isEmpty()) { try { QString transactionId; if (d->m_selectedTransactions.count() == 1) { transactionId = d->m_selectedTransactions[0].transaction().id(); } // make sure to pass a copy, as d->myMoneyView->slotLedgerSelected() overrides // d->m_accountGoto while calling slotUpdateActions() QString accountId = d->m_accountGoto; d->m_myMoneyView->slotLedgerSelected(accountId, transactionId); } catch (const MyMoneyException &) { } } } void KMyMoneyApp::slotTransactionGotoPayee() { if (!d->m_payeeGoto.isEmpty()) { try { QString transactionId; if (d->m_selectedTransactions.count() == 1) { transactionId = d->m_selectedTransactions[0].transaction().id(); } // make sure to pass copies, as d->myMoneyView->slotPayeeSelected() overrides // d->m_payeeGoto and d->m_selectedAccount while calling slotUpdateActions() QString payeeId = d->m_payeeGoto; QString accountId = d->m_selectedAccount.id(); d->m_myMoneyView->slotPayeeSelected(payeeId, accountId, transactionId); } catch (const MyMoneyException &) { } } } void KMyMoneyApp::slotTransactionCreateSchedule() { if (d->m_selectedTransactions.count() == 1) { // make sure to have the current selected split as first split in the schedule MyMoneyTransaction t = d->m_selectedTransactions[0].transaction(); MyMoneySplit s = d->m_selectedTransactions[0].split(); QString splitId = s.id(); s.clearId(); s.setReconcileFlag(eMyMoney::Split::State::NotReconciled); s.setReconcileDate(QDate()); t.removeSplits(); t.addSplit(s); foreach (const auto split, d->m_selectedTransactions[0].transaction().splits()) { if (split.id() != splitId) { MyMoneySplit s0 = split; s0.clearId(); s0.setReconcileFlag(eMyMoney::Split::State::NotReconciled); s0.setReconcileDate(QDate()); t.addSplit(s0); } } slotScheduleNew(t); } } void KMyMoneyApp::slotTransactionAssignNumber() { if (d->m_transactionEditor) d->m_transactionEditor->assignNextNumber(); } void KMyMoneyApp::slotTransactionCombine() { qDebug("slotTransactionCombine() not implemented yet"); } void KMyMoneyApp::slotTransactionCopySplits() { MyMoneyFile* file = MyMoneyFile::instance(); if (d->m_selectedTransactions.count() >= 2) { int singleSplitTransactions = 0; int multipleSplitTransactions = 0; KMyMoneyRegister::SelectedTransaction selectedSourceTransaction; foreach (const KMyMoneyRegister::SelectedTransaction& st, d->m_selectedTransactions) { switch (st.transaction().splitCount()) { case 0: break; case 1: singleSplitTransactions++; break; default: selectedSourceTransaction = st; multipleSplitTransactions++; break; } } if (singleSplitTransactions > 0 && multipleSplitTransactions == 1) { MyMoneyFileTransaction ft; try { const MyMoneyTransaction& sourceTransaction = selectedSourceTransaction.transaction(); const MyMoneySplit& sourceSplit = selectedSourceTransaction.split(); foreach (const KMyMoneyRegister::SelectedTransaction& st, d->m_selectedTransactions) { MyMoneyTransaction t = st.transaction(); // don't process the source transaction if (sourceTransaction.id() == t.id()) { continue; } const MyMoneySplit& baseSplit = st.split(); if (t.splitCount() == 1) { foreach (const MyMoneySplit& split, sourceTransaction.splits()) { // Don't copy the source split, as we already have that // as part of the destination transaction if (split.id() == sourceSplit.id()) { continue; } MyMoneySplit sp(split); // clear the ID and reconciliation state sp.clearId(); sp.setReconcileFlag(eMyMoney::Split::State::NotReconciled); sp.setReconcileDate(QDate()); // in case it is a simple transaction consisting of two splits, // we can adjust the share and value part of the second split we // just created. We need to keep a possible price in mind in case // of different currencies if (sourceTransaction.splitCount() == 2) { sp.setValue(-baseSplit.value()); sp.setShares(-(baseSplit.shares() * baseSplit.price())); } t.addSplit(sp); } file->modifyTransaction(t); } } ft.commit(); } catch (const MyMoneyException &) { qDebug() << "transactionCopySplits() failed"; } } } } void KMyMoneyApp::slotMoveToAccount(const QString& id) { // close the menu, if it is still open QWidget* w = factory()->container("transaction_context_menu", this); if (w && w->isVisible()) { w->close(); } if (!d->m_selectedTransactions.isEmpty()) { MyMoneyFileTransaction ft; try { foreach (const auto selection, d->m_selectedTransactions) { if (d->m_selectedAccount.accountType() == eMyMoney::Account::Type::Investment) { d->moveInvestmentTransaction(d->m_selectedAccount.id(), id, selection.transaction()); } else { auto changed = false; auto t = selection.transaction(); foreach (const auto split, selection.transaction().splits()) { if (split.accountId() == d->m_selectedAccount.id()) { MyMoneySplit s = split; s.setAccountId(id); t.modifySplit(s); changed = true; } } if (changed) { MyMoneyFile::instance()->modifyTransaction(t); } } } ft.commit(); } catch (const MyMoneyException &) { } } } // move a stock transaction from one investment account to another void KMyMoneyApp::Private::moveInvestmentTransaction(const QString& /*fromId*/, const QString& toId, const MyMoneyTransaction& tx) { MyMoneyAccount toInvAcc = MyMoneyFile::instance()->account(toId); MyMoneyTransaction t(tx); // first determine which stock we are dealing with. // fortunately, investment transactions have only one stock involved QString stockAccountId; QString stockSecurityId; MyMoneySplit s; foreach (const auto split, t.splits()) { stockAccountId = split.accountId(); stockSecurityId = MyMoneyFile::instance()->account(stockAccountId).currencyId(); if (!MyMoneyFile::instance()->security(stockSecurityId).isCurrency()) { s = split; break; } } // Now check the target investment account to see if it // contains a stock with this id QString newStockAccountId; foreach (const auto sAccount, toInvAcc.accountList()) { if (MyMoneyFile::instance()->account(sAccount).currencyId() == stockSecurityId) { newStockAccountId = sAccount; break; } } // if it doesn't exist, we need to add it as a copy of the old one // no 'copyAccount()' function?? if (newStockAccountId.isEmpty()) { MyMoneyAccount stockAccount = MyMoneyFile::instance()->account(stockAccountId); MyMoneyAccount newStock; newStock.setName(stockAccount.name()); newStock.setNumber(stockAccount.number()); newStock.setDescription(stockAccount.description()); newStock.setInstitutionId(stockAccount.institutionId()); newStock.setOpeningDate(stockAccount.openingDate()); newStock.setAccountType(stockAccount.accountType()); newStock.setCurrencyId(stockAccount.currencyId()); newStock.setClosed(stockAccount.isClosed()); MyMoneyFile::instance()->addAccount(newStock, toInvAcc); newStockAccountId = newStock.id(); } // now update the split and the transaction s.setAccountId(newStockAccountId); t.modifySplit(s); MyMoneyFile::instance()->modifyTransaction(t); } void KMyMoneyApp::slotUpdateMoveToAccountMenu() { createTransactionMoveMenu(); // in case we were not able to create the selector, we // better get out of here. Anything else would cause // a crash later on (accountSet.load) if (!d->m_moveToAccountSelector) return; if (!d->m_selectedAccount.id().isEmpty()) { AccountSet accountSet; if (d->m_selectedAccount.accountType() == eMyMoney::Account::Type::Investment) { accountSet.addAccountType(eMyMoney::Account::Type::Investment); } else if (d->m_selectedAccount.isAssetLiability()) { accountSet.addAccountType(eMyMoney::Account::Type::Checkings); accountSet.addAccountType(eMyMoney::Account::Type::Savings); accountSet.addAccountType(eMyMoney::Account::Type::Cash); accountSet.addAccountType(eMyMoney::Account::Type::AssetLoan); accountSet.addAccountType(eMyMoney::Account::Type::CertificateDep); accountSet.addAccountType(eMyMoney::Account::Type::MoneyMarket); accountSet.addAccountType(eMyMoney::Account::Type::Asset); accountSet.addAccountType(eMyMoney::Account::Type::Currency); accountSet.addAccountType(eMyMoney::Account::Type::CreditCard); accountSet.addAccountType(eMyMoney::Account::Type::Loan); accountSet.addAccountType(eMyMoney::Account::Type::Liability); } else if (d->m_selectedAccount.isIncomeExpense()) { accountSet.addAccountType(eMyMoney::Account::Type::Income); accountSet.addAccountType(eMyMoney::Account::Type::Expense); } accountSet.load(d->m_moveToAccountSelector); // remove those accounts that we currently reference foreach (const auto selection, d->m_selectedTransactions) { foreach (const auto split, selection.transaction().splits()) { d->m_moveToAccountSelector->removeItem(split.accountId()); } } // remove those accounts from the list that are denominated // in a different currency QStringList list = d->m_moveToAccountSelector->accountList(); QList::const_iterator it_a; for (it_a = list.constBegin(); it_a != list.constEnd(); ++it_a) { auto acc = MyMoneyFile::instance()->account(*it_a); if (acc.currencyId() != d->m_selectedAccount.currencyId()) d->m_moveToAccountSelector->removeItem((*it_a)); } } } void KMyMoneyApp::slotTransactionMatch() { // if the menu action is retrieved it can contain an '&' character for the accelerator causing the comparison to fail if not removed QString transactionActionText = actionCollection()->action(s_Actions[Action::TransactionMatch])->text(); transactionActionText.remove('&'); if (transactionActionText == i18nc("Button text for match transaction", "Match")) transactionMatch(); else transactionUnmatch(); } void KMyMoneyApp::transactionUnmatch() { KMyMoneyRegister::SelectedTransactions::const_iterator it; MyMoneyFileTransaction ft; try { for (it = d->m_selectedTransactions.constBegin(); it != d->m_selectedTransactions.constEnd(); ++it) { if ((*it).split().isMatched()) { TransactionMatcher matcher(d->m_selectedAccount); matcher.unmatch((*it).transaction(), (*it).split()); } } ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(0, i18n("Unable to unmatch the selected transactions"), e.what()); } } void KMyMoneyApp::transactionMatch() { if (d->m_selectedTransactions.count() != 2) return; MyMoneyTransaction startMatchTransaction; MyMoneyTransaction endMatchTransaction; MyMoneySplit startSplit; MyMoneySplit endSplit; KMyMoneyRegister::SelectedTransactions::const_iterator it; KMyMoneyRegister::SelectedTransactions toBeDeleted; for (it = d->m_selectedTransactions.constBegin(); it != d->m_selectedTransactions.constEnd(); ++it) { if ((*it).transaction().isImported()) { if (endMatchTransaction.id().isEmpty()) { endMatchTransaction = (*it).transaction(); endSplit = (*it).split(); toBeDeleted << *it; } else { //This is a second imported transaction, we still want to merge startMatchTransaction = (*it).transaction(); startSplit = (*it).split(); } } else if (!(*it).split().isMatched()) { if (startMatchTransaction.id().isEmpty()) { startMatchTransaction = (*it).transaction(); startSplit = (*it).split(); } else { endMatchTransaction = (*it).transaction(); endSplit = (*it).split(); toBeDeleted << *it; } } } #if 0 KMergeTransactionsDlg dlg(d->m_selectedAccount); dlg.addTransaction(startMatchTransaction); dlg.addTransaction(endMatchTransaction); if (dlg.exec() == QDialog::Accepted) #endif { MyMoneyFileTransaction ft; try { if (startMatchTransaction.id().isEmpty()) throw MYMONEYEXCEPTION(i18n("No manually entered transaction selected for matching")); if (endMatchTransaction.id().isEmpty()) throw MYMONEYEXCEPTION(i18n("No imported transaction selected for matching")); TransactionMatcher matcher(d->m_selectedAccount); matcher.match(startMatchTransaction, startSplit, endMatchTransaction, endSplit, true); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(0, i18n("Unable to match the selected transactions"), e.what()); } } } void KMyMoneyApp::showContextMenu(const QString& containerName) { QWidget* w = factory()->container(containerName, this); QMenu *menu = dynamic_cast(w); if (menu) menu->exec(QCursor::pos()); else qDebug("menu '%s' not found: w = %p, menu = %p", qPrintable(containerName), w, menu); } void KMyMoneyApp::slotShowTransactionContextMenu() { if (d->m_selectedTransactions.isEmpty() && d->m_selectedSchedule != MyMoneySchedule()) { showContextMenu("schedule_context_menu"); } else { showContextMenu("transaction_context_menu"); } } void KMyMoneyApp::slotShowInvestmentContextMenu() { showContextMenu("investment_context_menu"); } void KMyMoneyApp::slotShowScheduleContextMenu() { showContextMenu("schedule_context_menu"); } void KMyMoneyApp::slotShowAccountContextMenu(const MyMoneyObject& obj) { // qDebug("KMyMoneyApp::slotShowAccountContextMenu"); if (typeid(obj) != typeid(MyMoneyAccount)) return; const MyMoneyAccount& acc = dynamic_cast(obj); // if the selected account is actually a stock account, we // call the right slot instead if (acc.isInvest()) { showContextMenu("investment_context_menu"); } else if (acc.isIncomeExpense()) { showContextMenu("category_context_menu"); } else { showContextMenu("account_context_menu"); } } void KMyMoneyApp::slotShowInstitutionContextMenu(const MyMoneyObject& obj) { if (typeid(obj) != typeid(MyMoneyInstitution)) return; showContextMenu("institution_context_menu"); } void KMyMoneyApp::slotShowPayeeContextMenu() { showContextMenu("payee_context_menu"); } void KMyMoneyApp::slotShowTagContextMenu() { showContextMenu("tag_context_menu"); } void KMyMoneyApp::slotShowBudgetContextMenu() { showContextMenu("budget_context_menu"); } void KMyMoneyApp::slotShowCurrencyContextMenu() { showContextMenu("currency_context_menu"); } void KMyMoneyApp::slotShowPriceContextMenu() { showContextMenu("price_context_menu"); } void KMyMoneyApp::slotShowOnlineJobContextMenu() { showContextMenu("onlinejob_context_menu"); } void KMyMoneyApp::slotPrintView() { d->m_myMoneyView->slotPrintView(); } void KMyMoneyApp::updateCaption(bool skipActions) { QString caption; caption = d->m_fileName.fileName(); if (caption.isEmpty() && d->m_myMoneyView && d->m_myMoneyView->fileOpen()) caption = i18n("Untitled"); // MyMoneyFile::instance()->dirty() throws an exception, if // there's no storage object available. In this case, we // assume that the storage object is not changed. Actually, // this can only happen if we are newly created early on. bool modified; try { modified = MyMoneyFile::instance()->dirty(); } catch (const MyMoneyException &) { modified = false; skipActions = true; } #ifdef KMM_DEBUG caption += QString(" (%1 x %2)").arg(width()).arg(height()); #endif setCaption(caption, modified); if (!skipActions) { d->m_myMoneyView->enableViewsIfFileOpen(); slotUpdateActions(); } } void KMyMoneyApp::slotUpdateActions() { MyMoneyFile* file = MyMoneyFile::instance(); const bool fileOpen = d->m_myMoneyView->fileOpen(); const bool modified = file->dirty(); const bool importRunning = (d->m_smtReader != 0); QWidget* w; KActionCollection *aC = actionCollection(); // ************* // Disabling actions to be disabled at this point // ************* { static const QVector disabledActions { Action::AccountStartReconciliation, Action::AccountFinishReconciliation, Action::AccountPostponeReconciliation, Action::AccountEdit, Action::AccountDelete, Action::AccountOpen, Action::AccountClose, Action::AccountReopen, Action::AccountTransactionReport, Action::AccountOnlineMap, Action::AccountOnlineUnmap, Action::AccountUpdate, Action::AccountUpdateAll, Action::AccountBalanceChart, Action::CategoryEdit, Action::CategoryDelete, Action::InstitutionEdit, Action::InstitutionDelete, Action::InvestmentEdit, Action::InvestmentNew, Action::InvestmentDelete, Action::InvestmentOnlinePrice, Action::InvestmentManualPrice, Action::ScheduleEdit, Action::ScheduleDelete, Action::ScheduleEnter, Action::ScheduleSkip, Action::PayeeDelete, Action::PayeeRename, Action::PayeeMerge, Action::TagDelete, Action::TagRename, Action::BudgetDelete, Action::BudgetRename, Action::BudgetChangeYear, Action::BudgetNew, Action::BudgetCopy, Action::BudgetForecast, Action::TransactionEdit, Action::TransactionEditSplits, Action::TransactionEnter, Action::TransactionCancel, Action::TransactionDelete, Action::TransactionMatch, Action::TransactionAccept, Action::TransactionDuplicate, Action::TransactionToggleReconciled, Action::TransactionToggleCleared, Action::TransactionGoToAccount, Action::TransactionGoToPayee, Action::TransactionAssignNumber, Action::TransactionCreateSchedule, Action::TransactionCombine, Action::TransactionSelectAll, Action::TransactionCopySplits, Action::ScheduleEdit, Action::ScheduleDelete, Action::ScheduleDuplicate, Action::ScheduleEnter, Action::ScheduleSkip, Action::CurrencyRename, Action::CurrencyDelete, Action::CurrencySetBase, Action::PriceEdit, Action::PriceDelete, Action::PriceUpdate }; foreach (const auto a, disabledActions) aC->action(s_Actions.value(a))->setEnabled(false); } // ************* // Disabling actions based on conditions // ************* { QString tooltip = i18n("Create a new transaction"); const QVector> actionStates { {qMakePair(Action::FileOpenDatabase, true)}, {qMakePair(Action::FileSaveAsDatabase, fileOpen)}, {qMakePair(Action::FilePersonalData, fileOpen)}, {qMakePair(Action::FileBackup, (fileOpen && !d->m_myMoneyView->isDatabase()))}, {qMakePair(Action::FileInformation, fileOpen)}, {qMakePair(Action::FileImportTemplate, fileOpen && !importRunning)}, {qMakePair(Action::FileExportTemplate, fileOpen && !importRunning)}, #ifdef KMM_DEBUG {qMakePair(Action::FileDump, fileOpen)}, #endif {qMakePair(Action::EditFindTransaction, fileOpen)}, {qMakePair(Action::ToolCurrencies, fileOpen)}, {qMakePair(Action::ToolPrices, fileOpen)}, {qMakePair(Action::ToolUpdatePrices, fileOpen)}, {qMakePair(Action::ToolConsistency, fileOpen)}, {qMakePair(Action::AccountNew, fileOpen)}, {qMakePair(Action::AccountCreditTransfer, onlineJobAdministration::instance()->canSendCreditTransfer())}, {qMakePair(Action::CategoryDelete, fileOpen)}, {qMakePair(Action::InstitutionNew, fileOpen)}, {qMakePair(Action::TransactionNew, (fileOpen && d->m_myMoneyView->canCreateTransactions(KMyMoneyRegister::SelectedTransactions(), tooltip)))}, {qMakePair(Action::ScheduleNew, fileOpen)}, {qMakePair(Action::CurrencyNew, fileOpen)}, {qMakePair(Action::PriceNew, fileOpen)}, }; foreach (const auto a, actionStates) aC->action(s_Actions.value(a.first))->setEnabled(a.second); } // ************* // Disabling standard actions based on conditions // ************* aC->action(QString::fromLatin1(KStandardAction::name(KStandardAction::Save)))->setEnabled(modified && !d->m_myMoneyView->isDatabase()); aC->action(QString::fromLatin1(KStandardAction::name(KStandardAction::SaveAs)))->setEnabled(fileOpen); aC->action(QString::fromLatin1(KStandardAction::name(KStandardAction::Close)))->setEnabled(fileOpen); aC->action(QString::fromLatin1(KStandardAction::name(KStandardAction::Print)))->setEnabled(fileOpen && d->m_myMoneyView->canPrint()); aC->action(s_Actions[Action::TransactionMatch])->setText(i18nc("Button text for match transaction", "Match")); aC->action(s_Actions[Action::TransactionNew])->setToolTip(i18n("Create a new transaction")); w = factory()->container(s_Actions[Action::TransactionMoveMenu], this); if (w) w->setEnabled(false); w = factory()->container(s_Actions[Action::TransactionMarkMenu], this); if (w) w->setEnabled(false); w = factory()->container(s_Actions[Action::TransactionContextMarkMenu], this); if (w) w->setEnabled(false); // ************* // Enabling actions based on conditions // ************* // FIXME for now it's always on, but we should only allow it, if we // can select at least a single transaction aC->action(s_Actions[Action::TransactionSelectAll])->setEnabled(true); if (!d->m_selectedTransactions.isEmpty()) { // enable 'delete transaction' only if at least one of the // selected transactions does not reference a closed account bool enable = false; KMyMoneyRegister::SelectedTransactions::const_iterator it_t; for (it_t = d->m_selectedTransactions.constBegin(); (enable == false) && (it_t != d->m_selectedTransactions.constEnd()); ++it_t) { enable = !(*it_t).transaction().id().isEmpty() && !file->referencesClosedAccount((*it_t).transaction()); } aC->action(s_Actions[Action::TransactionDelete])->setEnabled(enable); if (!d->m_transactionEditor) { QString tooltip = i18n("Duplicate the current selected transactions"); aC->action(s_Actions[Action::TransactionDuplicate])->setEnabled(d->m_myMoneyView->canDuplicateTransactions(d->m_selectedTransactions, tooltip) && !d->m_selectedTransactions[0].transaction().id().isEmpty()); aC->action(s_Actions[Action::TransactionDuplicate])->setToolTip(tooltip); if (d->m_myMoneyView->canEditTransactions(d->m_selectedTransactions, tooltip)) { aC->action(s_Actions[Action::TransactionEdit])->setEnabled(true); // editing splits is allowed only if we have one transaction selected if (d->m_selectedTransactions.count() == 1) { aC->action(s_Actions[Action::TransactionEditSplits])->setEnabled(true); } if (d->m_selectedAccount.isAssetLiability() && d->m_selectedAccount.accountType() != eMyMoney::Account::Type::Investment) { aC->action(s_Actions[Action::TransactionCreateSchedule])->setEnabled(d->m_selectedTransactions.count() == 1); } } aC->action(s_Actions[Action::TransactionEdit])->setToolTip(tooltip); if (!d->m_selectedAccount.isClosed()) { w = factory()->container(s_Actions[Action::TransactionMoveMenu], this); if (w) w->setEnabled(true); } w = factory()->container(s_Actions[Action::TransactionMarkMenu], this); if (w) w->setEnabled(true); w = factory()->container(s_Actions[Action::TransactionContextMarkMenu], this); if (w) w->setEnabled(true); // Allow marking the transaction if at least one is selected aC->action(s_Actions[Action::TransactionToggleCleared])->setEnabled(true); aC->action(s_Actions[Action::TransactionReconciled])->setEnabled(true); aC->action(s_Actions[Action::TransactionNotReconciled])->setEnabled(true); aC->action(s_Actions[Action::TransactionToggleReconciled])->setEnabled(true); if (!d->m_accountGoto.isEmpty()) aC->action(s_Actions[Action::TransactionGoToAccount])->setEnabled(true); if (!d->m_payeeGoto.isEmpty()) aC->action(s_Actions[Action::TransactionGoToPayee])->setEnabled(true); // Matching is enabled as soon as one regular and one imported transaction is selected int matchedCount = 0; int importedCount = 0; KMyMoneyRegister::SelectedTransactions::const_iterator it; for (it = d->m_selectedTransactions.constBegin(); it != d->m_selectedTransactions.constEnd(); ++it) { if ((*it).transaction().isImported()) ++importedCount; if ((*it).split().isMatched()) ++matchedCount; } if (d->m_selectedTransactions.count() == 2 /* && aC->action(s_Actions[Action::TransactionEdit])->isEnabled() */) { aC->action(s_Actions[Action::TransactionMatch])->setEnabled(true); } if (importedCount != 0 || matchedCount != 0) aC->action(s_Actions[Action::TransactionAccept])->setEnabled(true); if (matchedCount != 0) { aC->action(s_Actions[Action::TransactionMatch])->setEnabled(true); aC->action(s_Actions[Action::TransactionMatch])->setText(i18nc("Button text for unmatch transaction", "Unmatch")); aC->action(s_Actions[Action::TransactionMatch])->setIcon(QIcon("process-stop")); } if (d->m_selectedTransactions.count() > 1) { aC->action(s_Actions[Action::TransactionCombine])->setEnabled(true); } if (d->m_selectedTransactions.count() >= 2) { int singleSplitTransactions = 0; int multipleSplitTransactions = 0; foreach (const KMyMoneyRegister::SelectedTransaction& st, d->m_selectedTransactions) { switch (st.transaction().splitCount()) { case 0: break; case 1: singleSplitTransactions++; break; default: multipleSplitTransactions++; break; } } if (singleSplitTransactions > 0 && multipleSplitTransactions == 1) { aC->action(s_Actions[Action::TransactionCopySplits])->setEnabled(true); } } if (d->m_selectedTransactions.count() >= 2) { int singleSplitTransactions = 0; int multipleSplitTransactions = 0; foreach(const KMyMoneyRegister::SelectedTransaction& st, d->m_selectedTransactions) { switch(st.transaction().splitCount()) { case 0: break; case 1: singleSplitTransactions++; break; default: multipleSplitTransactions++; break; } } if(singleSplitTransactions > 0 && multipleSplitTransactions == 1) { aC->action(s_Actions[Action::TransactionCopySplits])->setEnabled(true); } } } else { aC->action(s_Actions[Action::TransactionAssignNumber])->setEnabled(d->m_transactionEditor->canAssignNumber()); aC->action(s_Actions[Action::TransactionNew])->setEnabled(false); aC->action(s_Actions[Action::TransactionDelete])->setEnabled(false); QString reason; aC->action(s_Actions[Action::TransactionEnter])->setEnabled(d->m_transactionEditor->isComplete(reason)); //FIXME: Port to KDE4 // the next line somehow worked in KDE3 but does not have // any influence under KDE4 /// Works for me when 'reason' is set. Allan aC->action(s_Actions[Action::TransactionEnter])->setToolTip(reason); aC->action(s_Actions[Action::TransactionCancel])->setEnabled(true); } } QList accList; file->accountList(accList); QList::const_iterator it_a; QMap::const_iterator it_p = d->m_onlinePlugins.constEnd(); for (it_a = accList.constBegin(); (it_p == d->m_onlinePlugins.constEnd()) && (it_a != accList.constEnd()); ++it_a) { if (!(*it_a).onlineBankingSettings().value("provider").isEmpty()) { // check if provider is available it_p = d->m_onlinePlugins.constFind((*it_a).onlineBankingSettings().value("provider")); if (it_p != d->m_onlinePlugins.constEnd()) { QStringList protocols; (*it_p)->protocols(protocols); if (protocols.count() > 0) { aC->action(s_Actions[Action::AccountUpdateAll])->setEnabled(true); aC->action(s_Actions[Action::AccountUpdateMenu])->setEnabled(true); } } } } QBitArray skip((int)eStorage::Reference::Count); if (!d->m_selectedAccount.id().isEmpty()) { if (!file->isStandardAccount(d->m_selectedAccount.id())) { switch (d->m_selectedAccount.accountGroup()) { case eMyMoney::Account::Type::Asset: case eMyMoney::Account::Type::Liability: case eMyMoney::Account::Type::Equity: aC->action(s_Actions[Action::AccountTransactionReport])->setEnabled(true); aC->action(s_Actions[Action::AccountEdit])->setEnabled(true); aC->action(s_Actions[Action::AccountDelete])->setEnabled(!file->isReferenced(d->m_selectedAccount)); aC->action(s_Actions[Action::AccountOpen])->setEnabled(true); if (d->m_selectedAccount.accountGroup() != eMyMoney::Account::Type::Equity) { if (d->m_reconciliationAccount.id().isEmpty()) { aC->action(s_Actions[Action::AccountStartReconciliation])->setEnabled(true); aC->action(s_Actions[Action::AccountStartReconciliation])->setToolTip(i18n("Reconcile")); } else { QString tip; tip = i18n("Reconcile - disabled because you are currently reconciling %1", d->m_reconciliationAccount.name()); aC->action(s_Actions[Action::AccountStartReconciliation])->setToolTip(tip); if (!d->m_transactionEditor) { aC->action(s_Actions[Action::AccountFinishReconciliation])->setEnabled(d->m_selectedAccount.id() == d->m_reconciliationAccount.id()); aC->action(s_Actions[Action::AccountPostponeReconciliation])->setEnabled(d->m_selectedAccount.id() == d->m_reconciliationAccount.id()); } } } if (d->m_selectedAccount.accountType() == eMyMoney::Account::Type::Investment) aC->action(s_Actions[Action::InvestmentNew])->setEnabled(true); if (d->m_selectedAccount.isClosed()) aC->action(s_Actions[Action::AccountReopen])->setEnabled(true); else enableCloseAccountAction(d->m_selectedAccount); if (!d->m_selectedAccount.onlineBankingSettings().value("provider").isEmpty()) { aC->action(s_Actions[Action::AccountOnlineUnmap])->setEnabled(true); // check if provider is available QMap::const_iterator it_p; it_p = d->m_onlinePlugins.constFind(d->m_selectedAccount.onlineBankingSettings().value("provider")); if (it_p != d->m_onlinePlugins.constEnd()) { QStringList protocols; (*it_p)->protocols(protocols); if (protocols.count() > 0) { aC->action(s_Actions[Action::AccountUpdate])->setEnabled(true); aC->action(s_Actions[Action::AccountUpdateMenu])->setEnabled(true); } } } else { aC->action(s_Actions[Action::AccountOnlineMap])->setEnabled(d->m_onlinePlugins.count() > 0); } aC->action(s_Actions[Action::AccountBalanceChart])->setEnabled(true); break; case eMyMoney::Account::Type::Income : case eMyMoney::Account::Type::Expense : aC->action(s_Actions[Action::CategoryEdit])->setEnabled(true); // enable delete action, if category/account itself is not referenced // by any object except accounts, because we want to allow // deleting of sub-categories. Also, we allow transactions, schedules and budgets // to be present because we can re-assign them during the delete process skip.fill(false); skip.setBit((int)eStorage::Reference::Transaction); skip.setBit((int)eStorage::Reference::Account); skip.setBit((int)eStorage::Reference::Schedule); skip.setBit((int)eStorage::Reference::Budget); aC->action(s_Actions[Action::CategoryDelete])->setEnabled(!file->isReferenced(d->m_selectedAccount, skip)); aC->action(s_Actions[Action::AccountOpen])->setEnabled(true); break; default: break; } } } if (!d->m_selectedInstitution.id().isEmpty()) { aC->action(s_Actions[Action::InstitutionEdit])->setEnabled(true); aC->action(s_Actions[Action::InstitutionDelete])->setEnabled(!file->isReferenced(d->m_selectedInstitution)); } if (!d->m_selectedInvestment.id().isEmpty()) { aC->action(s_Actions[Action::InvestmentEdit])->setEnabled(true); aC->action(s_Actions[Action::InvestmentDelete])->setEnabled(!file->isReferenced(d->m_selectedInvestment)); aC->action(s_Actions[Action::InvestmentManualPrice])->setEnabled(true); try { MyMoneySecurity security = MyMoneyFile::instance()->security(d->m_selectedInvestment.currencyId()); if (!security.value("kmm-online-source").isEmpty()) aC->action(s_Actions[Action::InvestmentOnlinePrice])->setEnabled(true); } catch (const MyMoneyException &e) { qDebug("Error retrieving security for investment %s: %s", qPrintable(d->m_selectedInvestment.name()), qPrintable(e.what())); } if (d->m_selectedInvestment.isClosed()) aC->action(s_Actions[Action::AccountReopen])->setEnabled(true); else enableCloseAccountAction(d->m_selectedInvestment); } if (!d->m_selectedSchedule.id().isEmpty()) { aC->action(s_Actions[Action::ScheduleEdit])->setEnabled(true); aC->action(s_Actions[Action::ScheduleDuplicate])->setEnabled(true); aC->action(s_Actions[Action::ScheduleDelete])->setEnabled(!file->isReferenced(d->m_selectedSchedule)); if (!d->m_selectedSchedule.isFinished()) { aC->action(s_Actions[Action::ScheduleEnter])->setEnabled(true); // a schedule with a single occurrence cannot be skipped if (d->m_selectedSchedule.occurrence() != eMyMoney::Schedule::Occurrence::Once) { aC->action(s_Actions[Action::ScheduleSkip])->setEnabled(true); } } } if (d->m_selectedPayees.count() >= 1) { aC->action(s_Actions[Action::PayeeRename])->setEnabled(d->m_selectedPayees.count() == 1); aC->action(s_Actions[Action::PayeeMerge])->setEnabled(d->m_selectedPayees.count() > 1); aC->action(s_Actions[Action::PayeeDelete])->setEnabled(true); } if (d->m_selectedTags.count() >= 1) { aC->action(s_Actions[Action::TagRename])->setEnabled(d->m_selectedTags.count() == 1); aC->action(s_Actions[Action::TagDelete])->setEnabled(true); } aC->action(s_Actions[Action::BudgetNew])->setEnabled(true); if (d->m_selectedBudgets.count() >= 1) { aC->action(s_Actions[Action::BudgetDelete])->setEnabled(true); if (d->m_selectedBudgets.count() == 1) { aC->action(s_Actions[Action::BudgetChangeYear])->setEnabled(true); aC->action(s_Actions[Action::BudgetCopy])->setEnabled(true); aC->action(s_Actions[Action::BudgetRename])->setEnabled(true); aC->action(s_Actions[Action::BudgetForecast])->setEnabled(true); } } if (!d->m_selectedCurrency.id().isEmpty()) { aC->action(s_Actions[Action::CurrencyRename])->setEnabled(true); // no need to check each transaction. accounts are enough in this case skip.fill(false); skip.setBit((int)eStorage::Reference::Transaction); aC->action(s_Actions[Action::CurrencyDelete])->setEnabled(!file->isReferenced(d->m_selectedCurrency, skip)); if (d->m_selectedCurrency.id() != file->baseCurrency().id()) aC->action(s_Actions[Action::CurrencySetBase])->setEnabled(true); } if (!d->m_selectedPrice.from().isEmpty() && d->m_selectedPrice.source() != QLatin1String("KMyMoney")) { aC->action(s_Actions[Action::PriceEdit])->setEnabled(true); aC->action(s_Actions[Action::PriceDelete])->setEnabled(true); //enable online update if it is a currency MyMoneySecurity security = MyMoneyFile::instance()->security(d->m_selectedPrice.from()); aC->action(s_Actions[Action::PriceUpdate])->setEnabled(security.isCurrency()); } } void KMyMoneyApp::slotResetSelections() { slotSelectAccount(MyMoneyAccount()); slotSelectInstitution(MyMoneyInstitution()); slotSelectInvestment(MyMoneyAccount()); slotSelectSchedule(); slotSelectCurrency(); slotSelectPrice(); slotSelectPayees(QList()); slotSelectTags(QList()); slotSelectBudget(QList()); slotSelectTransactions(KMyMoneyRegister::SelectedTransactions()); slotUpdateActions(); } void KMyMoneyApp::slotSelectCurrency() { slotSelectCurrency(MyMoneySecurity()); } void KMyMoneyApp::slotSelectCurrency(const MyMoneySecurity& currency) { d->m_selectedCurrency = currency; slotUpdateActions(); emit currencySelected(d->m_selectedCurrency); } void KMyMoneyApp::slotSelectPrice() { slotSelectPrice(MyMoneyPrice()); } void KMyMoneyApp::slotSelectPrice(const MyMoneyPrice& price) { d->m_selectedPrice = price; slotUpdateActions(); emit priceSelected(d->m_selectedPrice); } void KMyMoneyApp::slotSelectBudget(const QList& list) { d->m_selectedBudgets = list; slotUpdateActions(); emit budgetSelected(d->m_selectedBudgets); } void KMyMoneyApp::slotSelectPayees(const QList& list) { d->m_selectedPayees = list; slotUpdateActions(); emit payeesSelected(d->m_selectedPayees); } void KMyMoneyApp::slotSelectTags(const QList& list) { d->m_selectedTags = list; slotUpdateActions(); emit tagsSelected(d->m_selectedTags); } void KMyMoneyApp::slotSelectTransactions(const KMyMoneyRegister::SelectedTransactions& list) { // list can either contain a list of transactions or a single selected scheduled transaction // in the latter case, the transaction id is actually the one of the schedule. In order // to differentiate between the two, we just ask for the schedule. If we don't find one - because // we passed the id of a real transaction - then we know that fact. We use the schedule here, // because the list of schedules is kept in a cache by MyMoneyFile. This way, we save some trips // to the backend which we would have to do if we check for the transaction. d->m_selectedTransactions.clear(); d->m_selectedSchedule = MyMoneySchedule(); d->m_accountGoto.clear(); d->m_payeeGoto.clear(); if (!list.isEmpty() && !list.first().isScheduled()) { d->m_selectedTransactions = list; if (list.count() == 1) { const MyMoneySplit& sp = d->m_selectedTransactions[0].split(); if (!sp.payeeId().isEmpty()) { try { auto payee = MyMoneyFile::instance()->payee(sp.payeeId()); if (!payee.name().isEmpty()) { d->m_payeeGoto = payee.id(); auto name = payee.name(); name.replace(QRegExp("&(?!&)"), "&&"); actionCollection()->action(s_Actions[Action::TransactionGoToPayee])->setText(i18n("Go to '%1'", name)); } } catch (const MyMoneyException &) { } } try { const MyMoneyTransaction& t = d->m_selectedTransactions[0].transaction(); // search the first non-income/non-expense accunt and use it for the 'goto account' const MyMoneySplit& sp = d->m_selectedTransactions[0].split(); foreach (const auto split, t.splits()) { if (split.id() != sp.id()) { auto acc = MyMoneyFile::instance()->account(split.accountId()); if (!acc.isIncomeExpense()) { // for stock accounts we show the portfolio account if (acc.isInvest()) { acc = MyMoneyFile::instance()->account(acc.parentAccountId()); } d->m_accountGoto = acc.id(); auto name = acc.name(); name.replace(QRegExp("&(?!&)"), "&&"); actionCollection()->action(s_Actions[Action::TransactionGoToAccount])->setText(i18n("Go to '%1'", name)); break; } } } } catch (const MyMoneyException &) { } } slotUpdateActions(); emit transactionsSelected(d->m_selectedTransactions); } else if (!list.isEmpty()) { slotSelectSchedule(MyMoneyFile::instance()->schedule(list.first().scheduleId())); } else { slotUpdateActions(); } // make sure, we show some neutral menu entry if we don't have an object if (d->m_payeeGoto.isEmpty()) actionCollection()->action(s_Actions[Action::TransactionGoToPayee])->setText(i18n("Go to payee")); if (d->m_accountGoto.isEmpty()) actionCollection()->action(s_Actions[Action::TransactionGoToAccount])->setText(i18n("Go to account")); } void KMyMoneyApp::slotSelectInstitution(const MyMoneyObject& institution) { if (typeid(institution) != typeid(MyMoneyInstitution)) return; d->m_selectedInstitution = dynamic_cast(institution); // qDebug("slotSelectInstitution('%s')", d->m_selectedInstitution.name().data()); slotUpdateActions(); emit institutionSelected(d->m_selectedInstitution); } void KMyMoneyApp::slotSelectAccount(const MyMoneyObject& obj) { if (typeid(obj) != typeid(MyMoneyAccount)) return; d->m_selectedAccount = MyMoneyAccount(); const MyMoneyAccount& acc = dynamic_cast(obj); if (!acc.isInvest()) d->m_selectedAccount = acc; // qDebug("slotSelectAccount('%s')", d->m_selectedAccount.name().data()); slotUpdateActions(); emit accountSelected(d->m_selectedAccount); } void KMyMoneyApp::slotSelectInvestment(const MyMoneyObject& obj) { if (typeid(obj) != typeid(MyMoneyAccount)) return; // qDebug("slotSelectInvestment('%s')", account.name().data()); d->m_selectedInvestment = MyMoneyAccount(); const MyMoneyAccount& acc = dynamic_cast(obj); if (acc.isInvest()) d->m_selectedInvestment = acc; slotUpdateActions(); emit investmentSelected(d->m_selectedInvestment); } void KMyMoneyApp::slotSelectSchedule() { slotSelectSchedule(MyMoneySchedule()); } void KMyMoneyApp::slotSelectSchedule(const MyMoneySchedule& schedule) { // qDebug("slotSelectSchedule('%s')", schedule.name().data()); d->m_selectedSchedule = schedule; slotUpdateActions(); emit scheduleSelected(d->m_selectedSchedule); } void KMyMoneyApp::slotDataChanged() { // As this method is called every time the MyMoneyFile instance // notifies a modification, it's the perfect place to start the timer if needed if (d->m_autoSaveEnabled && !d->m_autoSaveTimer->isActive()) { d->m_autoSaveTimer->setSingleShot(true); d->m_autoSaveTimer->start(d->m_autoSavePeriod * 60 * 1000); //miliseconds } updateCaption(); } void KMyMoneyApp::slotCurrencyDialog() { QPointer dlg = new KCurrencyEditDlg(this); connect(dlg, SIGNAL(selectObject(MyMoneySecurity)), this, SLOT(slotSelectCurrency(MyMoneySecurity))); connect(dlg, SIGNAL(openContextMenu(MyMoneySecurity)), this, SLOT(slotShowCurrencyContextMenu())); connect(this, SIGNAL(currencyRename()), dlg, SLOT(slotStartRename())); connect(dlg, SIGNAL(updateCurrency(QString,QString,QString)), this, SLOT(slotCurrencyUpdate(QString,QString,QString))); connect(this, SIGNAL(currencyCreated(QString)), dlg, SLOT(slotSelectCurrency(QString))); connect(dlg, SIGNAL(selectBaseCurrency(MyMoneySecurity)), this, SLOT(slotCurrencySetBase())); dlg->exec(); delete dlg; slotSelectCurrency(MyMoneySecurity()); } void KMyMoneyApp::slotPriceDialog() { QPointer dlg = new KMyMoneyPriceDlg(this); connect(dlg, SIGNAL(selectObject(MyMoneyPrice)), this, SLOT(slotSelectPrice(MyMoneyPrice))); connect(dlg, SIGNAL(openContextMenu(MyMoneyPrice)), this, SLOT(slotShowPriceContextMenu())); connect(this, SIGNAL(priceNew()), dlg, SLOT(slotNewPrice())); connect(this, SIGNAL(priceEdit()), dlg, SLOT(slotEditPrice())); connect(this, SIGNAL(priceDelete()), dlg, SLOT(slotDeletePrice())); connect(this, SIGNAL(priceOnlineUpdate()), dlg, SLOT(slotOnlinePriceUpdate())); dlg->exec(); } void KMyMoneyApp::slotFileConsistencyCheck() { d->consistencyCheck(true); updateCaption(); } void KMyMoneyApp::Private::consistencyCheck(bool alwaysDisplayResult) { KMSTATUS(i18n("Running consistency check...")); MyMoneyFileTransaction ft; try { m_consistencyCheckResult = MyMoneyFile::instance()->consistencyCheck(); ft.commit(); } catch (const MyMoneyException &e) { m_consistencyCheckResult.append(i18n("Consistency check failed: %1", e.what())); // always display the result if the check failed alwaysDisplayResult = true; } // in case the consistency check was OK, we get a single line as result // in all errneous cases, we get more than one line and force the // display of them. if (alwaysDisplayResult || m_consistencyCheckResult.size() > 1) { QString msg = i18n("The consistency check has found no issues in your data. Details are presented below."); if (m_consistencyCheckResult.size() > 1) msg = i18n("The consistency check has found some issues in your data. Details are presented below. Those issues that could not be corrected automatically need to be solved by the user."); // install a context menu for the list after the dialog is displayed QTimer::singleShot(500, q, SLOT(slotInstallConsistencyCheckContextMenu())); KMessageBox::informationList(0, msg, m_consistencyCheckResult, i18n("Consistency check result")); } // this data is no longer needed m_consistencyCheckResult.clear(); } void KMyMoneyApp::Private::copyConsistencyCheckResults() { QClipboard *clipboard = QApplication::clipboard(); clipboard->setText(m_consistencyCheckResult.join(QLatin1String("\n"))); } void KMyMoneyApp::Private::saveConsistencyCheckResults() { QUrl fileUrl = QFileDialog::getSaveFileUrl(q); if (!fileUrl.isEmpty()) { QFile file(fileUrl.toLocalFile()); if (file.open(QFile::WriteOnly | QFile::Append | QFile::Text)) { QTextStream out(&file); out << m_consistencyCheckResult.join(QLatin1String("\n")); file.close(); } } } void KMyMoneyApp::Private::setThemedCSS() { const QStringList CSSnames {QStringLiteral("kmymoney.css"), QStringLiteral("welcome.css")}; const QString rcDir("/html/"); const QStringList defaultCSSDirs = QStandardPaths::locateAll(QStandardPaths::AppDataLocation, rcDir, QStandardPaths::LocateDirectory); // scan the list of directories to find the ones that really // contains all files we look for QString defaultCSSDir; foreach (const auto dir, defaultCSSDirs) { defaultCSSDir = dir; foreach (const auto CSSname, CSSnames) { QFileInfo fileInfo(defaultCSSDir + CSSname); if (!fileInfo.exists()) { defaultCSSDir.clear(); break; } } if (!defaultCSSDir.isEmpty()) { break; } } // make sure we have the local directory where the themed version is stored const QString themedCSSDir = QStandardPaths::standardLocations(QStandardPaths::AppConfigLocation).first() + rcDir; QDir().mkpath(themedCSSDir); foreach (const auto CSSname, CSSnames) { const QString defaultCSSFilename = defaultCSSDir + CSSname; QFileInfo fileInfo(defaultCSSFilename); if (fileInfo.exists()) { const QString themedCSSFilename = themedCSSDir + CSSname; QFile::remove(themedCSSFilename); if (QFile::copy(defaultCSSFilename, themedCSSFilename)) { QFile cssFile (themedCSSFilename); if (cssFile.open(QIODevice::ReadWrite)) { QTextStream cssStream(&cssFile); auto cssText = cssStream.readAll(); cssText.replace(QLatin1String("./"), defaultCSSDir, Qt::CaseSensitive); cssText.replace(QLatin1String("WindowText"), KMyMoneyGlobalSettings::schemeColor(SchemeColor::WindowText).name(), Qt::CaseSensitive); cssText.replace(QLatin1String("Window"), KMyMoneyGlobalSettings::schemeColor(SchemeColor::WindowBackground).name(), Qt::CaseSensitive); cssText.replace(QLatin1String("HighlightText"), KMyMoneyGlobalSettings::schemeColor(SchemeColor::ListHighlightText).name(), Qt::CaseSensitive); cssText.replace(QLatin1String("Highlight"), KMyMoneyGlobalSettings::schemeColor(SchemeColor::ListHighlight).name(), Qt::CaseSensitive); cssText.replace(QLatin1String("black"), KMyMoneyGlobalSettings::schemeColor(SchemeColor::ListGrid).name(), Qt::CaseSensitive); cssStream.seek(0); cssStream << cssText; cssFile.close(); } } } } } void KMyMoneyApp::slotCheckSchedules() { if (KMyMoneyGlobalSettings::checkSchedule() == true) { KMSTATUS(i18n("Checking for overdue scheduled transactions...")); MyMoneyFile *file = MyMoneyFile::instance(); QDate checkDate = QDate::currentDate().addDays(KMyMoneyGlobalSettings::checkSchedulePreview()); QList scheduleList = file->scheduleList(); QList::Iterator it; eDialogs::ScheduleResultCode rc = eDialogs::ScheduleResultCode::Enter; for (it = scheduleList.begin(); (it != scheduleList.end()) && (rc != eDialogs::ScheduleResultCode::Cancel); ++it) { // Get the copy in the file because it might be modified by commitTransaction MyMoneySchedule schedule = file->schedule((*it).id()); if (schedule.autoEnter()) { try { while (!schedule.isFinished() && (schedule.adjustedNextDueDate() <= checkDate) && rc != eDialogs::ScheduleResultCode::Ignore && rc != eDialogs::ScheduleResultCode::Cancel) { rc = enterSchedule(schedule, true, true); schedule = file->schedule((*it).id()); // get a copy of the modified schedule } } catch (const MyMoneyException &) { } } if (rc == eDialogs::ScheduleResultCode::Ignore) { // if the current schedule was ignored then we must make sure that the user can still enter the next scheduled transaction rc = eDialogs::ScheduleResultCode::Enter; } } updateCaption(); } } void KMyMoneyApp::writeLastUsedDir(const QString& directory) { //get global config object for our app. KSharedConfigPtr kconfig = KSharedConfig::openConfig(); if (kconfig) { KConfigGroup grp = kconfig->group("General Options"); //write path entry, no error handling since its void. grp.writeEntry("LastUsedDirectory", directory); } } void KMyMoneyApp::writeLastUsedFile(const QString& fileName) { //get global config object for our app. KSharedConfigPtr kconfig = KSharedConfig::openConfig(); if (kconfig) { KConfigGroup grp = d->m_config->group("General Options"); // write path entry, no error handling since its void. // use a standard string, as fileName could contain a protocol // e.g. file:/home/thb/.... grp.writeEntry("LastUsedFile", fileName); } } QString KMyMoneyApp::readLastUsedDir() const { QString str; //get global config object for our app. KSharedConfigPtr kconfig = KSharedConfig::openConfig(); if (kconfig) { KConfigGroup grp = d->m_config->group("General Options"); //read path entry. Second parameter is the default if the setting is not found, which will be the default document path. str = grp.readEntry("LastUsedDirectory", QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation)); // if the path stored is empty, we use the default nevertheless if (str.isEmpty()) str = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation); } return str; } QString KMyMoneyApp::readLastUsedFile() const { QString str; // get global config object for our app. KSharedConfigPtr kconfig = KSharedConfig::openConfig(); if (kconfig) { KConfigGroup grp = d->m_config->group("General Options"); // read filename entry. str = grp.readEntry("LastUsedFile", ""); } return str; } QString KMyMoneyApp::filename() const { return d->m_fileName.url(); } WebConnect* KMyMoneyApp::webConnect() const { return d->m_webConnect; } QList KMyMoneyApp::instanceList() const { QList list; #ifdef KMM_DBUS QDBusReply reply = QDBusConnection::sessionBus().interface()->registeredServiceNames(); if (reply.isValid()) { QStringList apps = reply.value(); QStringList::ConstIterator it; // build a list of service names of all running kmymoney applications without this one for (it = apps.constBegin(); it != apps.constEnd(); ++it) { // please change this method of creating a list of 'all the other kmymoney instances that are running on the system' // since assuming that D-Bus creates service names with org.kde.kmymoney-PID is an observation I don't think that it's documented somwhere if ((*it).indexOf("org.kde.kmymoney-") == 0) { uint thisProcPid = platformTools::processId(); if ((*it).indexOf(QString("org.kde.kmymoney-%1").arg(thisProcPid)) != 0) list += (*it); } } } else { qDebug("D-Bus returned the following error while obtaining instances: %s", qPrintable(reply.error().message())); } #endif return list; } void KMyMoneyApp::slotEquityPriceUpdate() { QPointer dlg = new KEquityPriceUpdateDlg(this); if (dlg->exec() == QDialog::Accepted && dlg != 0) dlg->storePrices(); delete dlg; } void KMyMoneyApp::webConnect(const QString& sourceUrl, const QByteArray& asn_id) { // // Web connect attempts to go through the known importers and see if the file // can be importing using that method. If so, it will import it using that // plugin // Q_UNUSED(asn_id) d->m_importUrlsQueue.enqueue(sourceUrl); // only start processing if this is the only import so far if (d->m_importUrlsQueue.count() == 1) { while (!d->m_importUrlsQueue.isEmpty()) { // get the value of the next item from the queue // but leave it on the queue for now QString url = d->m_importUrlsQueue.head(); // Bring this window to the forefront. This method was suggested by // Lubos Lunak of the KDE core development team. // TODO: port KF5 (WebConnect) //KStartupInfo::setNewStartupId(this, asn_id); // Make sure we have an open file if (! d->m_myMoneyView->fileOpen() && KMessageBox::warningContinueCancel(kmymoney, i18n("You must first select a KMyMoney file before you can import a statement.")) == KMessageBox::Continue) kmymoney->slotFileOpen(); // only continue if the user really did open a file. if (d->m_myMoneyView->fileOpen()) { KMSTATUS(i18n("Importing a statement via Web Connect")); // remove the statement files d->unlinkStatementXML(); QMap::const_iterator it_plugin = d->m_importerPlugins.constBegin(); while (it_plugin != d->m_importerPlugins.constEnd()) { if ((*it_plugin)->isMyFormat(url)) { QList statements; if (!(*it_plugin)->import(url)) { KMessageBox::error(this, i18n("Unable to import %1 using %2 plugin. The plugin returned the following error: %3", url, (*it_plugin)->formatName(), (*it_plugin)->lastError()), i18n("Importing error")); } break; } ++it_plugin; } // If we did not find a match, try importing it as a KMM statement file, // which is really just for testing. the statement file is not exposed // to users. if (it_plugin == d->m_importerPlugins.constEnd()) if (MyMoneyStatement::isStatementFile(url)) slotStatementImport(url); } // remove the current processed item from the queue d->m_importUrlsQueue.dequeue(); } } } void KMyMoneyApp::slotEnableMessages() { KMessageBox::enableAllMessages(); KMessageBox::information(this, i18n("All messages have been enabled."), i18n("All messages")); } void KMyMoneyApp::createInterfaces() { // Sets up the plugin interface KMyMoneyPlugin::pluginInterfaces().importInterface = new KMyMoneyPlugin::KMMImportInterface(this, this); KMyMoneyPlugin::pluginInterfaces().statementInterface = new KMyMoneyPlugin::KMMStatementInterface(this, this); KMyMoneyPlugin::pluginInterfaces().viewInterface = new KMyMoneyPlugin::KMMViewInterface(this, d->m_myMoneyView, this); // setup the calendar interface for schedules MyMoneySchedule::setProcessingCalendar(this); } void KMyMoneyApp::loadPlugins() { Q_ASSERT(!d->m_pluginLoader); d->m_pluginLoader = new KMyMoneyPlugin::PluginLoader(this); //! @todo Junior Job: Improve the config read system KSharedConfigPtr config = KSharedConfig::openConfig(); KConfigGroup group{ config->group("Plugins") }; const auto plugins = KPluginLoader::findPlugins("kmymoney"); d->m_pluginLoader->addPluginInfo(plugins); for (const KPluginMetaData & pluginData : plugins) { // Only load plugins which are enabled and have the right serviceType. Other serviceTypes are loaded on demand. if (isPluginEnabled(pluginData, group)) slotPluginLoad(pluginData); } connect(d->m_pluginLoader, &KMyMoneyPlugin::PluginLoader::pluginEnabled, this, &KMyMoneyApp::slotPluginLoad); connect(d->m_pluginLoader, &KMyMoneyPlugin::PluginLoader::pluginDisabled, this, &KMyMoneyApp::slotPluginUnload); } void KMyMoneyApp::unloadPlugins() { Q_ASSERT(d->m_pluginLoader); delete d->m_pluginLoader; } inline bool KMyMoneyApp::isPluginEnabled(const KPluginMetaData& metaData, const KConfigGroup& configGroup) { //! @fixme: there is a function in KMyMoneyPlugin::PluginLoader which has to have the same content if (metaData.serviceTypes().contains("KMyMoney/Plugin")) { const QString keyName{metaData.name() + "Enabled"}; if (configGroup.hasKey(keyName)) return configGroup.readEntry(keyName, true); return metaData.isEnabledByDefault(); } return false; } void KMyMoneyApp::slotPluginLoad(const KPluginMetaData& metaData) { std::unique_ptr loader{new QPluginLoader{metaData.fileName()}}; QObject* plugin = loader->instance(); if (!plugin) { qWarning("Could not load plugin '%s', error: %s", qPrintable(metaData.fileName()), qPrintable(loader->errorString())); return; } if ( d->m_plugins.contains(metaData.fileName()) ) { /** @fixme Handle a reload e.g. objectNames are equal but the object are different (plugin != d->m_plugins[plugin->objectName()]) * Also it could be useful to drop the dependence on objectName() */ /* Note: there is nothing to delete here because if the plugin was loaded already, plugin points to the same object as the previously loaded one. */ return; } KMyMoneyPlugin::Plugin* kmmPlugin = qobject_cast(plugin); if (!kmmPlugin) { qWarning("Could not load plugin '%s'.", qPrintable(metaData.fileName())); return; } // check for online plugin KMyMoneyPlugin::OnlinePlugin* op = dynamic_cast(plugin); // check for extended online plugin KMyMoneyPlugin::OnlinePluginExtended* ope = dynamic_cast(plugin); // check for importer plugin KMyMoneyPlugin::ImporterPlugin* ip = dynamic_cast(plugin); // Tell the plugin it is about to get plugged kmmPlugin->plug(); // plug the plugin guiFactory()->addClient(kmmPlugin); d->m_plugins[metaData.fileName()] = kmmPlugin; if (op) d->m_onlinePlugins[plugin->objectName()] = op; if (ope) onlineJobAdministration::instance()->addPlugin(plugin->objectName(), ope); if (ip) d->m_importerPlugins[plugin->objectName()] = ip; slotUpdateActions(); } void KMyMoneyApp::slotPluginUnload(const KPluginMetaData& metaData) { KMyMoneyPlugin::Plugin* plugin = d->m_plugins[metaData.fileName()]; // Remove and test if the plugin was actually loaded if (!d->m_plugins.remove(metaData.fileName()) || plugin == nullptr) return; // check for online plugin KMyMoneyPlugin::OnlinePlugin* op = dynamic_cast(plugin); // check for importer plugin KMyMoneyPlugin::ImporterPlugin* ip = dynamic_cast(plugin); // unplug the plugin guiFactory()->removeClient(plugin); if (op) d->m_onlinePlugins.remove(plugin->objectName()); if (ip) d->m_importerPlugins.remove(plugin->objectName()); plugin->unplug(); slotUpdateActions(); } void KMyMoneyApp::slotAutoSave() { if (!d->m_inAutoSaving) { // store the focus widget so we can restore it after save QPointer focusWidget = qApp->focusWidget(); d->m_inAutoSaving = true; KMSTATUS(i18n("Auto saving...")); //calls slotFileSave if needed, and restart the timer //it the file is not saved, reinitializes the countdown. if (d->m_myMoneyView->dirty() && d->m_autoSaveEnabled) { if (!slotFileSave() && d->m_autoSavePeriod > 0) { d->m_autoSaveTimer->setSingleShot(true); d->m_autoSaveTimer->start(d->m_autoSavePeriod * 60 * 1000); } } d->m_inAutoSaving = false; if (focusWidget && focusWidget != qApp->focusWidget()) { // we have a valid focus widget so restore it focusWidget->setFocus(); } } } void KMyMoneyApp::slotDateChanged() { QDateTime dt = QDateTime::currentDateTime(); QDateTime nextDay(QDate(dt.date().addDays(1)), QTime(0, 0, 0)); // +1 is to make sure that we're already in the next day when the // signal is sent (this way we also avoid setting the timer to 0) QTimer::singleShot((dt.secsTo(nextDay) + 1)*1000, this, SLOT(slotDateChanged())); d->m_myMoneyView->slotRefreshViews(); } MyMoneyAccount KMyMoneyApp::account(const QString& key, const QString& value) const { QList list; QList::const_iterator it_a; MyMoneyFile::instance()->accountList(list); QString accId; for (it_a = list.constBegin(); it_a != list.constEnd(); ++it_a) { // search in the account's kvp container const QString& accountKvpValue = (*it_a).value(key); // search in the account's online settings kvp container const QString& onlineSettingsKvpValue = (*it_a).onlineBankingSettings().value(key); if (accountKvpValue.contains(value) || onlineSettingsKvpValue.contains(value)) { if(accId.isEmpty()) { accId = (*it_a).id(); } } if (accountKvpValue == value || onlineSettingsKvpValue == value) { accId = (*it_a).id(); break; } } // return the account found or an empty element return MyMoneyFile::instance()->account(accId); } void KMyMoneyApp::setAccountOnlineParameters(const MyMoneyAccount& _acc, const MyMoneyKeyValueContainer& kvps) { MyMoneyFileTransaction ft; try { auto acc = MyMoneyFile::instance()->account(_acc.id()); acc.setOnlineBankingSettings(kvps); MyMoneyFile::instance()->modifyAccount(acc); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(0, i18n("Unable to setup online parameters for account '%1'", _acc.name()), e.what()); } } void KMyMoneyApp::slotAccountUnmapOnline() { // no account selected if (d->m_selectedAccount.id().isEmpty()) return; // not a mapped account if (d->m_selectedAccount.onlineBankingSettings().value("provider").isEmpty()) return; if (KMessageBox::warningYesNo(this, QString("%1").arg(i18n("Do you really want to remove the mapping of account %1 to an online account? Depending on the details of the online banking method used, this action cannot be reverted.", d->m_selectedAccount.name())), i18n("Remove mapping to online account")) == KMessageBox::Yes) { MyMoneyFileTransaction ft; try { d->m_selectedAccount.setOnlineBankingSettings(MyMoneyKeyValueContainer()); // delete the kvp that is used in MyMoneyStatementReader too // we should really get rid of it, but since I don't know what it // is good for, I'll keep it around. (ipwizard) d->m_selectedAccount.deletePair("StatementKey"); MyMoneyFile::instance()->modifyAccount(d->m_selectedAccount); ft.commit(); // The mapping could disable the online task system onlineJobAdministration::instance()->updateOnlineTaskProperties(); } catch (const MyMoneyException &e) { KMessageBox::error(this, i18n("Unable to unmap account from online account: %1", e.what())); } } } void KMyMoneyApp::slotAccountMapOnline() { // no account selected if (d->m_selectedAccount.id().isEmpty()) return; // already an account mapped if (!d->m_selectedAccount.onlineBankingSettings().value("provider").isEmpty()) return; // check if user tries to map a brokerageAccount if (d->m_selectedAccount.name().contains(i18n(" (Brokerage)"))) { if (KMessageBox::warningContinueCancel(this, i18n("You try to map a brokerage account to an online account. This is usually not advisable. In general, the investment account should be mapped to the online account. Please cancel if you intended to map the investment account, continue otherwise"), i18n("Mapping brokerage account")) == KMessageBox::Cancel) { return; } } // if we have more than one provider let the user select the current provider QString provider; QMap::const_iterator it_p; switch (d->m_onlinePlugins.count()) { case 0: break; case 1: provider = d->m_onlinePlugins.begin().key(); break; default: { QMenu popup(this); popup.setTitle(i18n("Select online banking plugin")); // Populate the pick list with all the provider for (it_p = d->m_onlinePlugins.constBegin(); it_p != d->m_onlinePlugins.constEnd(); ++it_p) { popup.addAction(it_p.key())->setData(it_p.key()); } QAction *item = popup.actions()[0]; if (item) { popup.setActiveAction(item); } // cancelled if ((item = popup.exec(QCursor::pos(), item)) == 0) { return; } provider = item->data().toString(); } break; } if (provider.isEmpty()) return; // find the provider it_p = d->m_onlinePlugins.constFind(provider); if (it_p != d->m_onlinePlugins.constEnd()) { // plugin found, call it MyMoneyKeyValueContainer settings; if ((*it_p)->mapAccount(d->m_selectedAccount, settings)) { settings["provider"] = provider; MyMoneyAccount acc(d->m_selectedAccount); acc.setOnlineBankingSettings(settings); MyMoneyFileTransaction ft; try { MyMoneyFile::instance()->modifyAccount(acc); ft.commit(); // The mapping could enable the online task system onlineJobAdministration::instance()->updateOnlineTaskProperties(); } catch (const MyMoneyException &e) { KMessageBox::error(this, i18n("Unable to map account to online account: %1", e.what())); } } } } void KMyMoneyApp::slotAccountUpdateOnlineAll() { QList accList; MyMoneyFile::instance()->accountList(accList); QList::iterator it_a; QMap::const_iterator it_p; d->m_statementResults.clear(); d->m_collectingStatements = true; // remove all those from the list, that don't have a 'provider' or the // provider is not currently present for (it_a = accList.begin(); it_a != accList.end();) { if ((*it_a).onlineBankingSettings().value("provider").isEmpty() || d->m_onlinePlugins.find((*it_a).onlineBankingSettings().value("provider")) == d->m_onlinePlugins.end()) { it_a = accList.erase(it_a); } else ++it_a; } QVector disabledActions {Action::AccountUpdate, Action::AccountUpdateMenu, Action::AccountUpdateAll}; foreach (const auto a, disabledActions) actionCollection()->action(s_Actions.value(a))->setEnabled(false); // now work on the remaining list of accounts int cnt = accList.count() - 1; for (it_a = accList.begin(); it_a != accList.end(); ++it_a) { it_p = d->m_onlinePlugins.constFind((*it_a).onlineBankingSettings().value("provider")); (*it_p)->updateAccount(*it_a, cnt != 0); --cnt; } d->m_collectingStatements = false; if (!d->m_statementResults.isEmpty()) KMessageBox::informationList(this, i18n("The statements have been processed with the following results:"), d->m_statementResults, i18n("Statement stats")); // re-enable the disabled actions slotUpdateActions(); } void KMyMoneyApp::slotAccountUpdateOnline() { // no account selected if (d->m_selectedAccount.id().isEmpty()) return; // no online account mapped if (d->m_selectedAccount.onlineBankingSettings().value("provider").isEmpty()) return; QVector disabledActions {Action::AccountUpdate, Action::AccountUpdateMenu, Action::AccountUpdateAll}; foreach (const auto a, disabledActions) actionCollection()->action(s_Actions.value(a))->setEnabled(false); // find the provider QMap::const_iterator it_p; it_p = d->m_onlinePlugins.constFind(d->m_selectedAccount.onlineBankingSettings().value("provider")); if (it_p != d->m_onlinePlugins.constEnd()) { // plugin found, call it d->m_collectingStatements = true; d->m_statementResults.clear(); (*it_p)->updateAccount(d->m_selectedAccount); d->m_collectingStatements = false; if (!d->m_statementResults.isEmpty()) KMessageBox::informationList(this, i18n("The statements have been processed with the following results:"), d->m_statementResults, i18n("Statement stats")); } // re-enable the disabled actions slotUpdateActions(); } void KMyMoneyApp::slotNewOnlineTransfer() { kOnlineTransferForm *transferForm = new kOnlineTransferForm(this); if (!d->m_selectedAccount.id().isEmpty()) { transferForm->setCurrentAccount(d->m_selectedAccount.id()); } connect(transferForm, SIGNAL(rejected()), transferForm, SLOT(deleteLater())); connect(transferForm, SIGNAL(acceptedForSave(onlineJob)), this, SLOT(slotOnlineJobSave(onlineJob))); connect(transferForm, SIGNAL(acceptedForSend(onlineJob)), this, SLOT(slotOnlineJobSend(onlineJob))); connect(transferForm, SIGNAL(accepted()), transferForm, SLOT(deleteLater())); transferForm->show(); } void KMyMoneyApp::slotEditOnlineJob(const QString jobId) { try { const onlineJob constJob = MyMoneyFile::instance()->getOnlineJob(jobId); slotEditOnlineJob(constJob); } catch (MyMoneyException&) { // Prevent a crash in very rare cases } } void KMyMoneyApp::slotEditOnlineJob(onlineJob job) { try { slotEditOnlineJob(onlineJobTyped(job)); } catch (MyMoneyException&) { } } void KMyMoneyApp::slotEditOnlineJob(const onlineJobTyped job) { kOnlineTransferForm *transferForm = new kOnlineTransferForm(this); transferForm->setOnlineJob(job); connect(transferForm, SIGNAL(rejected()), transferForm, SLOT(deleteLater())); connect(transferForm, SIGNAL(acceptedForSave(onlineJob)), this, SLOT(slotOnlineJobSave(onlineJob))); connect(transferForm, SIGNAL(acceptedForSend(onlineJob)), this, SLOT(slotOnlineJobSend(onlineJob))); connect(transferForm, SIGNAL(accepted()), transferForm, SLOT(deleteLater())); transferForm->show(); } void KMyMoneyApp::slotOnlineJobSave(onlineJob job) { MyMoneyFileTransaction fileTransaction; if (job.id().isEmpty()) MyMoneyFile::instance()->addOnlineJob(job); else MyMoneyFile::instance()->modifyOnlineJob(job); fileTransaction.commit(); } /** @todo when onlineJob queue is used, continue here */ void KMyMoneyApp::slotOnlineJobSend(onlineJob job) { MyMoneyFileTransaction fileTransaction; if (job.id().isEmpty()) MyMoneyFile::instance()->addOnlineJob(job); else MyMoneyFile::instance()->modifyOnlineJob(job); fileTransaction.commit(); QList jobList; jobList.append(job); slotOnlineJobSend(jobList); } void KMyMoneyApp::slotOnlineJobSend(QList jobs) { MyMoneyFile *const kmmFile = MyMoneyFile::instance(); QMultiMap jobsByPlugin; // Sort jobs by online plugin & lock them foreach (onlineJob job, jobs) { Q_ASSERT(!job.id().isEmpty()); // find the provider const MyMoneyAccount originAcc = job.responsibleMyMoneyAccount(); job.setLock(); job.addJobMessage(onlineJobMessage(eMyMoney::OnlineJob::MessageType::Debug, "KMyMoneyApp::slotOnlineJobSend", "Added to queue for plugin '" + originAcc.onlineBankingSettings().value("provider") + '\'')); MyMoneyFileTransaction fileTransaction; kmmFile->modifyOnlineJob(job); fileTransaction.commit(); jobsByPlugin.insert(originAcc.onlineBankingSettings().value("provider"), job); } // Send onlineJobs to plugins QList usedPlugins = jobsByPlugin.keys(); std::sort(usedPlugins.begin(), usedPlugins.end()); const QList::iterator newEnd = std::unique(usedPlugins.begin(), usedPlugins.end()); usedPlugins.erase(newEnd, usedPlugins.end()); foreach (const QString& pluginKey, usedPlugins) { QMap::const_iterator it_p = d->m_onlinePlugins.constFind(pluginKey); if (it_p != d->m_onlinePlugins.constEnd()) { // plugin found, call it KMyMoneyPlugin::OnlinePluginExtended *pluginExt = dynamic_cast< KMyMoneyPlugin::OnlinePluginExtended* >(*it_p); if (pluginExt == 0) { qWarning("Job given for plugin which is not an extended plugin"); continue; } //! @fixme remove debug message qDebug() << "Sending " << jobsByPlugin.count(pluginKey) << " job(s) to online plugin " << pluginKey; QList jobsToExecute = jobsByPlugin.values(pluginKey); QList executedJobs = jobsToExecute; pluginExt->sendOnlineJob(executedJobs); // Save possible changes of the online job and remove lock MyMoneyFileTransaction fileTransaction; foreach (onlineJob job, executedJobs) { fileTransaction.restart(); job.setLock(false); kmmFile->modifyOnlineJob(job); fileTransaction.commit(); } if (Q_UNLIKELY(executedJobs.size() != jobsToExecute.size())) { // OnlinePlugin did not return all jobs qWarning() << "Error saving send online tasks. After restart you should see at minimum all successfully executed jobs marked send. Imperfect plugin: " << pluginExt->objectName(); } } else { qWarning() << "Error, got onlineJob for an account without online plugin."; /** @FIXME can this actually happen? */ } } } void KMyMoneyApp::slotRemoveJob() { } void KMyMoneyApp::slotEditJob() { } void KMyMoneyApp::slotOnlineJobLog() { QStringList jobIds = d->m_myMoneyView->getOnlineJobOutbox()->selectedOnlineJobs(); slotOnlineJobLog(jobIds); } void KMyMoneyApp::slotOnlineJobLog(const QStringList& onlineJobIds) { onlineJobMessagesView *const dialog = new onlineJobMessagesView(); onlineJobMessagesModel *const model = new onlineJobMessagesModel(dialog); model->setOnlineJob(MyMoneyFile::instance()->getOnlineJob(onlineJobIds.first())); dialog->setModel(model); dialog->setAttribute(Qt::WA_DeleteOnClose); dialog->show(); // Note: Objects are not deleted here, Qt's parent-child system has to do that. } void KMyMoneyApp::setHolidayRegion(const QString& holidayRegion) { #ifdef KF5Holidays_FOUND //since the cost of updating the cache is now not negligible //check whether the region has been modified if (!d->m_holidayRegion || d->m_holidayRegion->regionCode() != holidayRegion) { // Delete the previous holidayRegion before creating a new one. delete d->m_holidayRegion; // Create a new holidayRegion. d->m_holidayRegion = new KHolidays::HolidayRegion(holidayRegion); //clear and update the holiday cache preloadHolidays(); } #else Q_UNUSED(holidayRegion); #endif } bool KMyMoneyApp::isProcessingDate(const QDate& date) const { #ifdef KF5Holidays_FOUND if (!d->m_processingDays.testBit(date.dayOfWeek())) return false; if (!d->m_holidayRegion || !d->m_holidayRegion->isValid()) return true; //check first whether it's already in cache if (d->m_holidayMap.contains(date)) { return d->m_holidayMap.value(date, true); } else { bool processingDay = !d->m_holidayRegion->isHoliday(date); d->m_holidayMap.insert(date, processingDay); return processingDay; } #else Q_UNUSED(date); return true; #endif } void KMyMoneyApp::preloadHolidays() { #ifdef KF5Holidays_FOUND //clear the cache before loading d->m_holidayMap.clear(); //only do this if it is a valid region if (d->m_holidayRegion && d->m_holidayRegion->isValid()) { //load holidays for the forecast days plus 1 cycle, to be on the safe side int forecastDays = KMyMoneyGlobalSettings::forecastDays() + KMyMoneyGlobalSettings::forecastAccountCycle(); QDate endDate = QDate::currentDate().addDays(forecastDays); //look for holidays for the next 2 years as a minimum. That should give a good margin for the cache if (endDate < QDate::currentDate().addYears(2)) endDate = QDate::currentDate().addYears(2); KHolidays::Holiday::List holidayList = d->m_holidayRegion->holidays(QDate::currentDate(), endDate); KHolidays::Holiday::List::const_iterator holiday_it; for (holiday_it = holidayList.constBegin(); holiday_it != holidayList.constEnd(); ++holiday_it) { for (QDate holidayDate = (*holiday_it).observedStartDate(); holidayDate <= (*holiday_it).observedEndDate(); holidayDate = holidayDate.addDays(1)) d->m_holidayMap.insert(holidayDate, false); } for (QDate date = QDate::currentDate(); date <= endDate; date = date.addDays(1)) { //if it is not a processing day, set it to false if (!d->m_processingDays.testBit(date.dayOfWeek())) { d->m_holidayMap.insert(date, false); } else if (!d->m_holidayMap.contains(date)) { //if it is not a holiday nor a weekend, it is a processing day d->m_holidayMap.insert(date, true); } } } #endif } KMStatus::KMStatus(const QString &text) { m_prevText = kmymoney->slotStatusMsg(text); } KMStatus::~KMStatus() { kmymoney->slotStatusMsg(m_prevText); } void KMyMoneyApp::Private::unlinkStatementXML() { QDir d(KMyMoneySettings::logPath(), "kmm-statement*"); for (uint i = 0; i < d.count(); ++i) { qDebug("Remove %s", qPrintable(d[i])); d.remove(KMyMoneySettings::logPath() + QString("/%1").arg(d[i])); } m_statementXMLindex = 0; } void KMyMoneyApp::Private::closeFile() { q->slotSelectAccount(MyMoneyAccount()); q->slotSelectInstitution(MyMoneyInstitution()); q->slotSelectInvestment(MyMoneyAccount()); q->slotSelectSchedule(); q->slotSelectCurrency(); q->slotSelectBudget(QList()); q->slotSelectPayees(QList()); q->slotSelectTags(QList()); q->slotSelectTransactions(KMyMoneyRegister::SelectedTransactions()); m_reconciliationAccount = MyMoneyAccount(); m_myMoneyView->finishReconciliation(m_reconciliationAccount); m_myMoneyView->closeFile(); m_fileName = QUrl(); q->updateCaption(); // just create a new balance warning object delete m_balanceWarning; m_balanceWarning = new KBalanceWarning(q); emit q->fileLoaded(m_fileName); } diff --git a/kmymoney/kmymoneyutils.cpp b/kmymoney/kmymoneyutils.cpp index 082980e63..5b6b86fb4 100644 --- a/kmymoney/kmymoneyutils.cpp +++ b/kmymoney/kmymoneyutils.cpp @@ -1,649 +1,665 @@ /*************************************************************************** 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 +#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 &) { } } } + +bool KMyMoneyUtils::fileExists(const QUrl &url) +{ + bool fileExists = false; + if (url.isValid()) { + short int detailLevel = 0; // Lowest level: file/dir/symlink/none + KIO::StatJob* statjob = KIO::stat(url, KIO::StatJob::SourceSide, detailLevel); + bool noerror = statjob->exec(); + if (noerror) { + // We want a file + fileExists = !statjob->statResult().isDir(); + } + } + return fileExists; +} diff --git a/kmymoney/kmymoneyutils.h b/kmymoney/kmymoneyutils.h index 72aabb0b0..ef18f4b5c 100644 --- a/kmymoney/kmymoneyutils.h +++ b/kmymoney/kmymoneyutils.h @@ -1,371 +1,378 @@ /*************************************************************************** kmymoneyutils.h - 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 ***************************************************************************/ /*************************************************************************** * * * 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 KMYMONEYUTILS_H #define KMYMONEYUTILS_H // ---------------------------------------------------------------------------- // QT Includes #include #include - +#include // ---------------------------------------------------------------------------- // KDE Headers // ---------------------------------------------------------------------------- // Project Includes /** * @author Thomas Baumgart */ static QString m_lastNumberUsed; class QPixmap; class QWizard; class QWidget; class KGuiItem; class KXmlGuiWindow; class MyMoneyMoney; class MyMoneyAccount; class MyMoneySecurity; class MyMoneySchedule; class MyMoneySplit; class MyMoneyTransaction; namespace eMyMoney { namespace Schedule { enum class Occurrence; enum class PaymentType; enum class WeekendOption; enum class Type; } namespace Split { enum class State; enum class InvestmentTransactionType; } } class KMyMoneyUtils { public: enum transactionTypeE { /** * Unknown transaction type (e.g. used for a transaction with only * a single split) */ Unknown, /** * A 'normal' transaction is one that consists out two splits: one * referencing an income/expense account, the other referencing * an asset/liability account. */ Normal, /** * A transfer denotes a transaction consisting of two splits. * Both of the splits reference an asset/liability * account. */ Transfer, /** * Whenever a transaction consists of more than 2 splits, * it is treated as 'split transaction'. */ SplitTransaction, /** * This transaction denotes a specific transaction where * a loan account is involved. Usually, a special dialog * is used to modify this transaction. */ LoanPayment, /** * This transaction denotes a specific transaction where * an investment is involved. Usually, a special dialog * is used to modify this transaction. */ InvestmentTransaction }; enum CanCloseAccountCodeE { AccountCanClose = 0, // can close the account AccountBalanceNonZero, // balance is non zero AccountChildrenOpen, // account has open children account AccountScheduleReference // account is referenced in a schedule }; static const int maxHomePageItems = 5; KMyMoneyUtils(); ~KMyMoneyUtils(); /** * This method is used to convert the occurrence type from its * internal representation into a human readable format. * * @param occurrence numerical representation of the MyMoneySchedule * occurrence type * * @return QString representing the human readable format translated according to the language cataglogue * * @sa MyMoneySchedule::occurrenceToString() * * @deprecated Use i18n(MyMoneySchedule::occurrenceToString(occurrence)) instead */ static const QString occurrenceToString(const eMyMoney::Schedule::Occurrence occurrence); /** * This method is used to convert the payment type from its * internal representation into a human readable format. * * @param paymentType numerical representation of the MyMoneySchedule * payment type * * @return QString representing the human readable format translated according to the language cataglogue * * @sa MyMoneySchedule::paymentMethodToString() */ static const QString paymentMethodToString(eMyMoney::Schedule::PaymentType paymentType); /** * This method is used to convert the schedule weekend option from its * internal representation into a human readable format. * * @param weekendOption numerical representation of the MyMoneySchedule * weekend option * * @return QString representing the human readable format translated according to the language cataglogue * * @sa MyMoneySchedule::weekendOptionToString() */ static const QString weekendOptionToString(eMyMoney::Schedule::WeekendOption weekendOption); /** * This method is used to convert the schedule type from its * internal representation into a human readable format. * * @param type numerical representation of the MyMoneySchedule * schedule type * * @return QString representing the human readable format translated according to the language cataglogue * * @sa MyMoneySchedule::scheduleTypeToString() */ static const QString scheduleTypeToString(eMyMoney::Schedule::Type type); /** * This method is used to convert a numeric index of an item * represented on the home page into its string form. * * @param idx numeric index of item * * @return QString with text of this item */ static const QString homePageItemToString(const int idx); /** * This method is used to convert the name of a home page item * to its internal numerical representation * * @param txt QString reference of the items name * * @retval 0 @p txt is unknown * @retval >0 numeric value for @p txt */ static int stringToHomePageItem(const QString& txt); /** * Retrieve a KDE KGuiItem for the new schedule button. * * @return The KGuiItem that can be used to display the icon and text */ static KGuiItem scheduleNewGuiItem(); /** * Retrieve a KDE KGuiItem for the account filter button * * @return The KGuiItem that can be used to display the icon and text */ static KGuiItem accountsFilterGuiItem(); /** * This method adds the file extension passed as argument @p extension * to the end of the file name passed as argument @p name if it is not present. * If @p name contains an extension it will be removed. * * @param name filename to be checked * @param extension extension to be added (w/o the dot) * * @retval true if @p name was changed * @retval false if @p name remained unchanged */ static bool appendCorrectFileExt(QString& name, const QString& extension); /** * Check that internal MyMoney engine constants use the same * values as the KDE constants. */ static void checkConstants(); static QString variableCSS(); /** * This method searches a KDE specific resource and applies country and * language settings during the search. Therefore, the parameter @p filename must contain * the characters '%1' which gets replaced with the language/country values. * * The search is performed in the following order (stopped immediately if a file was found): * - @c \%1 is replaced with _\.\ * - @c \%1 is replaced with _\ * - @c \%1 is replaced with _\ * - @c \%1 is replaced with the empty string * * @c \ and @c \ denote the respective KDE settings. * * Example: The KDE settings for country is Spain (es) and language is set * to Galician (gl). The code for looking up a file looks like this: * * @code * * : * QString fname = KMyMoneyUtils::findResource("appdata", "html/home%1.html") * : * * @endcode * * The method calls KStandardDirs::findResource() with the following values for the * parameter @p filename: * * - html/home_es.gl.html * - html/home_gl.html * - html/home_es.html * - html/home.html * * @note See KStandardDirs::findResource() for details on the parameters */ static QString findResource(QStandardPaths::StandardLocation type, const QString& filename); /** * This method returns the split referencing a stock account if * one exists in the transaction passed as @p t. If none is present * in @p t, an empty MyMoneySplit() object will be returned. * * @param t transaction to be checked for a stock account * @return MyMoneySplit object referencing a stock account or an * empty MyMoneySplit object. */ static const MyMoneySplit stockSplit(const MyMoneyTransaction& t); /** * This method analyses the splits of a transaction and returns * the type of transaction. Possible values are defined by the * KMyMoneyUtils::transactionTypeE enum. * * @param t const reference to the transaction * * @return KMyMoneyUtils::transactionTypeE value of the action */ static transactionTypeE transactionType(const MyMoneyTransaction& t); /** * This method modifies a scheduled loan transaction such that all * references to automatic calculated values are resolved to actual values. * * @param schedule const reference to the schedule the transaction is based on * @param transaction reference to the transaction to be checked and modified * @param balances QMap of (account-id,balance) pairs to be used as current balance * for the calculation of interest. If map is empty, the engine * will be interrogated for current balances. */ static void calculateAutoLoan(const MyMoneySchedule& schedule, MyMoneyTransaction& transaction, const QMap& balances); /** * Return next check number for account @a acc. */ static QString nextCheckNumber(const MyMoneyAccount& acc); static void updateLastNumberUsed(const MyMoneyAccount& acc, const QString& number); static void setLastNumberUsed(const QString& num); static QString lastNumberUsed(); /** * Returns previous number if offset is -1 or * the following number if offset is 1. */ static QString getAdjacentNumber(const QString& number, int offset = 1); /** * remove any non-numeric characters from check number * to allow validity check */ static quint64 numericPart(const QString & num); /** * Returns the text representing the reconcile flag. If @a text is @p true * then the full text will be returned otherwise a short form (usually one character). */ static QString reconcileStateToString(eMyMoney::Split::State flag, bool text = false); /** * Returns the transaction for @a schedule. In case of a loan payment the * transaction will be modified by calculateAutoLoan(). * The ID of the transaction as well as the entryDate will be reset. * * @returns adjusted transaction */ static MyMoneyTransaction scheduledTransaction(const MyMoneySchedule& schedule); /** * This method replaces the deprecated QApplication::mainWidget() from Qt 3.x. * It assumes that there is only one KXmlGuiWindow in the application, and * returns it. * * @return the first KXmlGuiWindow found in QApplication::topLevelWidgets() */ static KXmlGuiWindow* mainWindow(); /** * This method sets the button text and icons to the KDE standard ones * for the QWizard passed as argument. */ static void updateWizardButtons(QWizard *); /** * This method overlays an icon over another one, to get a composite one * eg. an icon to add accounts */ static QPixmap overlayIcon(const QString &iconName, const QString &overlayName, const Qt::Corner corner = Qt::BottomRightCorner, const int size = 64); static void dissectTransaction(const MyMoneyTransaction& transaction, const MyMoneySplit& split, MyMoneySplit& assetAccountSplit, QList& feeSplits, QList& interestSplits, MyMoneySecurity& security, MyMoneySecurity& currency, eMyMoney::Split::InvestmentTransactionType& transactionType); /** * This method deletes security and associated price list but asks beforehand. */ static void deleteSecurity(const MyMoneySecurity &security, QWidget *parent = nullptr); + + + /** + * Check whether the url links to an existing file or not + * @returns whether the file exists or not + */ + static bool fileExists(const QUrl &url); }; #endif diff --git a/kmymoney/plugins/csv/export/csvexporterplugin.cpp b/kmymoney/plugins/csv/export/csvexporterplugin.cpp index cbd1866f0..531da8762 100644 --- a/kmymoney/plugins/csv/export/csvexporterplugin.cpp +++ b/kmymoney/plugins/csv/export/csvexporterplugin.cpp @@ -1,90 +1,99 @@ /*************************************************************************** csvexporterplugin.cpp (based on ofximporterplugin.cpp) ------------------- begin : Wed Mar 20 2013 copyright : (C) 2013 by Allan Anderson email : agander93@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 "csvexporterplugin.h" #include "csvexportdlg.h" #include "csvwriter.h" // ---------------------------------------------------------------------------- // QT Includes #include // ---------------------------------------------------------------------------- // KDE Includes #include #include +#include +#include // ---------------------------------------------------------------------------- // Project Includes CsvExporterPlugin::CsvExporterPlugin() : KMyMoneyPlugin::Plugin(nullptr, "csvexport"/*must be the same as X-KDE-PluginInfo-Name*/) { setComponentName("kmm_csvexport", i18n("CSV exporter")); setXMLFile("kmm_csvexport.rc"); createActions(); // For information, announce that we have been loaded. qDebug("KMyMoney csvexport plugin loaded"); } CsvExporterPlugin::~CsvExporterPlugin() { } void CsvExporterPlugin::createActions() { m_action = actionCollection()->addAction("file_export_csv"); m_action->setText(i18n("&CSV...")); connect(m_action, SIGNAL(triggered(bool)), this, SLOT(slotCsvExport())); } void CsvExporterPlugin::slotCsvExport() { m_dlg = new CsvExportDlg(); if (m_dlg->exec()) { if (okToWriteFile(QUrl::fromUserInput(m_dlg->filename()))) { m_dlg->setWindowTitle(i18nc("CSV Exporter dialog title", "CSV Exporter")); CsvWriter* writer = new CsvWriter; writer->m_plugin = this; connect(writer, SIGNAL(signalProgress(int,int)), m_dlg, SLOT(slotStatusProgressBar(int,int))); writer->write(m_dlg->filename(), m_dlg->accountId(), m_dlg->accountSelected(), m_dlg->categorySelected(), m_dlg->startDate(), m_dlg->endDate(), m_dlg->separator()); } } } bool CsvExporterPlugin::okToWriteFile(const QUrl &url) { - Q_UNUSED(url) - // check if the file exists and warn the user bool reallySaveFile = true; - // TODO: port this to KF5 (NetAccess) -#if 0 - if (KIO::NetAccess::exists(url, KIO::NetAccess::SourceSide, 0)) { + bool fileExists = false; + + if (url.isValid()) { + short int detailLevel = 0; // Lowest level: file/dir/symlink/none + KIO::StatJob* statjob = KIO::stat(url, KIO::StatJob::SourceSide, detailLevel); + bool noerror = statjob->exec(); + if (noerror) { + // We want a file + fileExists = !statjob->statResult().isDir(); + } + } + + if (fileExists) { if (KMessageBox::warningYesNo(0, QString("") + i18n("The file %1 already exists. Do you really want to overwrite it?", url.toDisplayString(QUrl::PreferLocalFile)) + QString(""), i18n("File already exists")) != KMessageBox::Yes) reallySaveFile = false; } -#endif return reallySaveFile; } diff --git a/kmymoney/plugins/qif/import/kimportdlg.cpp b/kmymoney/plugins/qif/import/kimportdlg.cpp index 5f27e8436..291f06c80 100644 --- a/kmymoney/plugins/qif/import/kimportdlg.cpp +++ b/kmymoney/plugins/qif/import/kimportdlg.cpp @@ -1,197 +1,206 @@ /*************************************************************************** kimportdlg.cpp - description ------------------- begin : Wed May 16 2001 copyright : (C) 2001 by Michael Edwardes email : mte@users.sourceforge.net Javier Campos Morales Felix Rodriguez 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 "kimportdlg.h" // ---------------------------------------------------------------------------- // QT Headers #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Headers #include -#include +#include #include #include #include +#include +#include // ---------------------------------------------------------------------------- // Project Headers #include "kmymoneyutils.h" #include "mymoneyfile.h" #include "mymoneyaccount.h" -#include + #include "../config/mymoneyqifprofile.h" #include using namespace Icons; KImportDlg::KImportDlg(QWidget *parent) : KImportDlgDecl(parent) { // Set all the last used options readConfig(); loadProfiles(true); // load button icons KGuiItem okButtenItem(i18n("&Import"), QIcon::fromTheme(g_Icons[Icon::DocumentImport]), i18n("Start operation"), i18n("Use this to start the import operation")); KGuiItem::assign(m_buttonBox->button(QDialogButtonBox::Ok), okButtenItem); KGuiItem browseButtenItem(i18n("&Browse..."), QIcon::fromTheme(g_Icons[Icon::DocumentOpen]), i18n("Select filename"), i18n("Use this to select a filename to export to")); KGuiItem::assign(m_qbuttonBrowse, browseButtenItem); KGuiItem newButtenItem(i18nc("New profile", "&New..."), QIcon::fromTheme(g_Icons[Icon::DocumentNew]), i18n("Create a new profile"), i18n("Use this to open the profile editor")); // connect the buttons to their functionality connect(m_qbuttonBrowse, SIGNAL(clicked()), this, SLOT(slotBrowse())); connect(m_buttonBox, SIGNAL(accepted()), this, SLOT(slotOkClicked())); connect(m_buttonBox, SIGNAL(rejected()), this, SLOT(reject())); // connect the change signals to the check slot and perform initial check connect(m_qlineeditFile, SIGNAL(textChanged(QString)), this, SLOT(slotFileTextChanged(QString))); // setup button enable status slotFileTextChanged(m_qlineeditFile->text()); } KImportDlg::~KImportDlg() { } void KImportDlg::slotBrowse() { // determine what the browse prefix should be from the current profile MyMoneyQifProfile tmpprofile; tmpprofile.loadProfile("Profile-" + profile()); QUrl file = QFileDialog::getOpenFileUrl(this, i18n("Import File..."), QUrl("kfiledialog:///kmymoney-import"), i18n("Import files (%1);;All files (%2)", tmpprofile.filterFileType(), "*") ); if (!file.isEmpty()) { m_qlineeditFile->setText(file.toDisplayString(QUrl::PreferLocalFile)); } } void KImportDlg::slotOkClicked() { // Save the used options. writeConfig(); // leave dialog directly accept(); } void KImportDlg::readConfig() { KSharedConfigPtr kconfig = KSharedConfig::openConfig(); KConfigGroup kgrp = kconfig->group("Last Use Settings"); m_qlineeditFile->setText(kgrp.readEntry("KImportDlg_LastFile")); } void KImportDlg::writeConfig() { KSharedConfigPtr kconfig = KSharedConfig::openConfig(); KConfigGroup grp = kconfig->group("Last Use Settings"); grp.writeEntry("KImportDlg_LastFile", m_qlineeditFile->text()); grp.writeEntry("KImportDlg_LastProfile", m_profileComboBox->currentText()); kconfig->sync(); } /** Make sure the text input is ok */ void KImportDlg::slotFileTextChanged(const QString& text) { - // TODO: port to kf5 - Q_UNUSED(text) -#if 0 - if (!text.isEmpty() && KIO::NetAccess::exists(file(), KIO::NetAccess::SourceSide, KMyMoneyUtils::mainWindow())) { + bool fileExists = false; + if (file().isValid()) { + Q_CONSTEXPR short int detailLevel = 0; // it's a file or a directory or a symlink, or it doesn't exist + KIO::StatJob* statjob = KIO::stat(file(), KIO::StatJob::SourceSide, detailLevel); + bool noerror = statjob->exec(); + if (noerror) { + // We want a file + fileExists = !statjob->statResult().isDir(); + } + } + + if (!text.isEmpty() && fileExists) { // m_qcomboboxDateFormat->setEnabled(true); m_buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true); m_qlineeditFile->setText(text); } else { // m_qcomboboxDateFormat->setEnabled(false); m_buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); } -#endif } void KImportDlg::loadProfiles(const bool selectLast) { QString current = m_profileComboBox->currentText(); m_profileComboBox->clear(); QStringList list; KSharedConfigPtr config = KSharedConfig::openConfig(); KConfigGroup grp = config->group("Profiles"); list = grp.readEntry("profiles", QStringList()); list.sort(); m_profileComboBox->addItems(list); if (selectLast == true) { config->group("Last Use Settings"); current = grp.readEntry("KImportDlg_LastProfile"); } int index = m_profileComboBox->findText(current, Qt::MatchExactly); if (index > -1) { m_profileComboBox->setCurrentIndex(index); } else { m_profileComboBox->setCurrentIndex(0); } } void KImportDlg::addCategories(QStringList& strList, const QString& id, const QString& leadIn) const { MyMoneyFile *file = MyMoneyFile::instance(); QString name; MyMoneyAccount account = file->account(id); QStringList accList = account.accountList(); QStringList::ConstIterator it_a; for (it_a = accList.constBegin(); it_a != accList.constEnd(); ++it_a) { account = file->account(*it_a); strList << leadIn + account.name(); addCategories(strList, *it_a, leadIn + account.name() + MyMoneyFile::AccountSeperator); } } diff --git a/kmymoney/views/kmymoneyview.cpp b/kmymoney/views/kmymoneyview.cpp index 8424a8aeb..3e08490f0 100644 --- a/kmymoney/views/kmymoneyview.cpp +++ b/kmymoney/views/kmymoneyview.cpp @@ -1,2323 +1,2339 @@ /*************************************************************************** kmymoneyview.cpp ------------------- copyright : (C) 2000-2001 by Michael Edwardes 2004 by Thomas Baumgart 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 "kmymoneyview.h" // ---------------------------------------------------------------------------- // Std Includes #include // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include #include #include #include +#include +#include #ifdef KF5Activities_FOUND #include #endif // ---------------------------------------------------------------------------- // Project Includes #include "kmymoneyglobalsettings.h" #include "kmymoneytitlelabel.h" #include #include "kcurrencyeditdlg.h" #include "mymoneyseqaccessmgr.h" #include "mymoneydatabasemgr.h" #include "imymoneystorageformat.h" #include "mymoneystoragebin.h" #include "mymoneyexception.h" #include "mymoneystoragexml.h" #include "mymoneystoragesql.h" #include "mymoneystorageanon.h" #include "khomeview.h" #include "kaccountsview.h" #include "kcategoriesview.h" #include "kinstitutionsview.h" #include "kpayeesview.h" #include "ktagsview.h" #include "kscheduledview.h" #include "kgloballedgerview.h" #include "simpleledgerview.h" #include "kinvestmentview.h" #include "kreportsview.h" #include "kbudgetview.h" #include "kforecastview.h" #include "konlinejoboutbox.h" #include "kmymoney.h" #include "models.h" #include "accountsmodel.h" #include "equitiesmodel.h" #include "securitiesmodel.h" #include "icons.h" #include "amountedit.h" #include "kmymoneyaccounttreeview.h" #include "accountsviewproxymodel.h" #include "mymoneybudget.h" #include "mymoneyprice.h" #include "mymoneyschedule.h" #include "mymoneysplit.h" #include "mymoneyaccount.h" #include "kmymoneyedit.h" #include "mymoneyfile.h" #include "mymoneysecurity.h" #include "mymoneyreport.h" #include "mymoneyenums.h" using namespace Icons; using namespace eMyMoney; static constexpr KCompressionDevice::CompressionType const& COMPRESSION_TYPE = KCompressionDevice::GZip; static constexpr char recoveryKeyId[] = "0xD2B08440"; typedef void(KMyMoneyView::*KMyMoneyViewFunc)(); KMyMoneyView::KMyMoneyView(KMyMoneyApp *kmymoney) : KPageWidget(nullptr), m_header(0), m_inConstructor(true), m_fileOpen(false), m_fmode(QFileDevice::ReadUser | QFileDevice::WriteUser), m_lastViewSelected(0) #ifdef KF5Activities_FOUND , m_activityResourceInstance(0) #endif { // this is a workaround for the bug in KPageWidget that causes the header to be shown // for a short while during page switch which causes a kind of bouncing of the page's // content and if the page's content is at it's minimum size then during a page switch // the main window's size is also increased to fit the header that is shown for a sort // period - reading the code in kpagewidget.cpp we know that the header should be at (1,1) // in a grid layout so if we find it there remove it for good to avoid the described issues QGridLayout* gridLayout = qobject_cast(layout()); if (gridLayout) { QLayoutItem* headerItem = gridLayout->itemAtPosition(1, 1); // make sure that we remove only the header - we avoid surprises if the header is not at (1,1) in the layout if (headerItem && qobject_cast(headerItem->widget()) != NULL) { gridLayout->removeItem(headerItem); // after we remove the KPageWidget standard header replace it with our own title label m_header = new KMyMoneyTitleLabel(this); m_header->setObjectName("titleLabel"); m_header->setMinimumSize(QSize(100, 30)); m_header->setRightImageFile("pics/titlelabel_background.png"); m_header->setVisible(KMyMoneyGlobalSettings::showTitleBar()); gridLayout->addWidget(m_header, 1, 1); } } newStorage(); m_model = new KPageWidgetModel(this); // cannot be parentless, otherwise segfaults at exit connect(kmymoney, SIGNAL(fileLoaded(QUrl)), this, SLOT(slotRefreshViews())); // let the accounts model know which account is being currently reconciled connect(this, SIGNAL(reconciliationStarts(MyMoneyAccount,QDate,MyMoneyMoney)), Models::instance()->accountsModel(), SLOT(slotReconcileAccount(MyMoneyAccount,QDate,MyMoneyMoney))); // Page 0 m_homeView = new KHomeView(); viewFrames[View::Home] = m_model->addPage(m_homeView, i18n("Home")); viewFrames[View::Home]->setIcon(QIcon::fromTheme(g_Icons[Icon::ViewHome])); connect(m_homeView, SIGNAL(ledgerSelected(QString,QString)), this, SLOT(slotLedgerSelected(QString,QString))); connect(m_homeView, SIGNAL(scheduleSelected(QString)), this, SLOT(slotScheduleSelected(QString))); connect(m_homeView, SIGNAL(reportSelected(QString)), this, SLOT(slotShowReport(QString))); connect(m_homeView, &KMyMoneyViewBase::aboutToShow, this, &KMyMoneyView::resetViewSelection); // Page 1 m_institutionsView = new KInstitutionsView; viewFrames[View::Institutions] = m_model->addPage(m_institutionsView, i18n("Institutions")); viewFrames[View::Institutions]->setIcon(QIcon::fromTheme(g_Icons[Icon::ViewInstitutions])); connect(m_institutionsView, &KMyMoneyViewBase::aboutToShow, this, &KMyMoneyView::connectView); connect(m_institutionsView, &KMyMoneyViewBase::aboutToShow, this, &KMyMoneyView::resetViewSelection); // Page 2 m_accountsView = new KAccountsView; viewFrames[View::Accounts] = m_model->addPage(m_accountsView, i18n("Accounts")); viewFrames[View::Accounts]->setIcon(QIcon::fromTheme(g_Icons[Icon::ViewAccounts])); connect(m_accountsView, &KMyMoneyViewBase::aboutToShow, this, &KMyMoneyView::connectView); connect(m_accountsView, &KMyMoneyViewBase::aboutToShow, this, &KMyMoneyView::resetViewSelection); // Page 3 m_scheduledView = new KScheduledView(); //this is to solve the way long strings are handled differently among versions of KPageWidget viewFrames[View::Schedules] = m_model->addPage(m_scheduledView, i18n("Scheduled transactions")); viewFrames[View::Schedules]->setIcon(QIcon::fromTheme(g_Icons[Icon::ViewSchedules])); connect(m_scheduledView, SIGNAL(scheduleSelected(MyMoneySchedule)), kmymoney, SLOT(slotSelectSchedule(MyMoneySchedule))); connect(m_scheduledView, SIGNAL(openContextMenu()), kmymoney, SLOT(slotShowScheduleContextMenu())); connect(m_scheduledView, SIGNAL(enterSchedule()), kmymoney, SLOT(slotScheduleEnter())); connect(m_scheduledView, SIGNAL(skipSchedule()), kmymoney, SLOT(slotScheduleSkip())); connect(m_scheduledView, SIGNAL(editSchedule()), kmymoney, SLOT(slotScheduleEdit())); connect(m_scheduledView, SIGNAL(aboutToShow()), this, SIGNAL(aboutToChangeView())); // Page 4 m_categoriesView = new KCategoriesView; viewFrames[View::Categories] = m_model->addPage(m_categoriesView, i18n("Categories")); viewFrames[View::Categories]->setIcon(QIcon::fromTheme(g_Icons[Icon::ViewCategories])); connect(m_categoriesView, &KMyMoneyViewBase::aboutToShow, this, &KMyMoneyView::connectView); connect(m_categoriesView, &KMyMoneyViewBase::aboutToShow, this, &KMyMoneyView::resetViewSelection); // Page 5 m_tagsView = new KTagsView; viewFrames[View::Tags] = m_model->addPage(m_tagsView, i18n("Tags")); viewFrames[View::Tags]->setIcon(QIcon::fromTheme(g_Icons[Icon::ViewTags])); connect(m_tagsView, &KMyMoneyViewBase::aboutToShow, this, &KMyMoneyView::connectView); connect(m_tagsView, &KMyMoneyViewBase::aboutToShow, this, &KMyMoneyView::resetViewSelection); // Page 6 m_payeesView = new KPayeesView(); viewFrames[View::Payees] = m_model->addPage(m_payeesView, i18n("Payees")); viewFrames[View::Payees]->setIcon(QIcon::fromTheme(g_Icons[Icon::ViewPayees])); connect(kmymoney, SIGNAL(payeeCreated(QString)), m_payeesView, SLOT(slotSelectPayeeAndTransaction(QString))); connect(kmymoney, SIGNAL(payeeRename()), m_payeesView, SLOT(slotRenameButtonCliked())); connect(m_payeesView, SIGNAL(openContextMenu(MyMoneyObject)), kmymoney, SLOT(slotShowPayeeContextMenu())); connect(m_payeesView, SIGNAL(selectObjects(QList)), kmymoney, SLOT(slotSelectPayees(QList))); connect(m_payeesView, SIGNAL(transactionSelected(QString,QString)), this, SLOT(slotLedgerSelected(QString,QString))); connect(m_payeesView, SIGNAL(aboutToShow()), this, SIGNAL(aboutToChangeView())); // Page 7 m_ledgerView = new KGlobalLedgerView(); viewFrames[View::Ledgers] = m_model->addPage(m_ledgerView, i18n("Ledgers")); viewFrames[View::Ledgers]->setIcon(QIcon::fromTheme(g_Icons[Icon::ViewLedgers])); connect(m_ledgerView, SIGNAL(accountSelected(MyMoneyObject)), kmymoney, SLOT(slotSelectAccount(MyMoneyObject))); connect(m_ledgerView, SIGNAL(openContextMenu()), kmymoney, SLOT(slotShowTransactionContextMenu())); connect(m_ledgerView, SIGNAL(transactionsSelected(KMyMoneyRegister::SelectedTransactions)), kmymoney, SLOT(slotSelectTransactions(KMyMoneyRegister::SelectedTransactions))); connect(m_ledgerView, SIGNAL(newTransaction()), kmymoney, SLOT(slotTransactionsNew())); connect(m_ledgerView, SIGNAL(cancelOrEndEdit(bool&)), kmymoney, SLOT(slotTransactionsCancelOrEnter(bool&))); connect(m_ledgerView, SIGNAL(startEdit()), kmymoney, SLOT(slotTransactionsEdit())); connect(m_ledgerView, SIGNAL(endEdit()), kmymoney, SLOT(slotTransactionsEnter())); connect(m_ledgerView, SIGNAL(toggleReconciliationFlag()), kmymoney, SLOT(slotToggleReconciliationFlag())); connect(this, SIGNAL(reconciliationStarts(MyMoneyAccount,QDate,MyMoneyMoney)), m_ledgerView, SLOT(slotSetReconcileAccount(MyMoneyAccount,QDate,MyMoneyMoney))); connect(kmymoney, SIGNAL(selectAllTransactions()), m_ledgerView, SLOT(slotSelectAllTransactions())); connect(m_ledgerView, &KAccountsView::aboutToShow, this, &KMyMoneyView::resetViewSelection); // Page 8 m_investmentView = new KInvestmentView(kmymoney, this); viewFrames[View::Investments] = m_model->addPage(m_investmentView, i18n("Investments")); viewFrames[View::Investments]->setIcon(QIcon::fromTheme(g_Icons[Icon::ViewInvestment])); // Page 9 m_reportsView = new KReportsView(); viewFrames[View::Reports] = m_model->addPage(m_reportsView, i18n("Reports")); viewFrames[View::Reports]->setIcon(QIcon::fromTheme(g_Icons[Icon::ViewReports])); connect(m_reportsView, SIGNAL(ledgerSelected(QString,QString)), this, SLOT(slotLedgerSelected(QString,QString))); connect(m_reportsView, &KMyMoneyViewBase::aboutToShow, this, &KMyMoneyView::resetViewSelection); // Page 10 m_budgetView = new KBudgetView(kmymoney); viewFrames[View::Budget] = m_model->addPage(m_budgetView, i18n("Budgets")); viewFrames[View::Budget]->setIcon(QIcon::fromTheme(g_Icons[Icon::ViewBudgets])); connect(m_budgetView, &KMyMoneyViewBase::aboutToShow, this, &KMyMoneyView::connectView); connect(m_budgetView, &KMyMoneyViewBase::aboutToShow, this, &KMyMoneyView::resetViewSelection); // Page 11 m_forecastView = new KForecastView(); viewFrames[View::Forecast] = m_model->addPage(m_forecastView, i18n("Forecast")); viewFrames[View::Forecast]->setIcon(QIcon::fromTheme(g_Icons[Icon::ViewForecast])); // Page 12 m_onlineJobOutboxView = new KOnlineJobOutbox(); viewFrames[View::OnlineJobOutbox] = m_model->addPage(m_onlineJobOutboxView, i18n("Outbox")); viewFrames[View::OnlineJobOutbox]->setIcon(QIcon::fromTheme(g_Icons[Icon::ViewOutbox])); connect(m_onlineJobOutboxView, SIGNAL(sendJobs(QList)), kmymoney, SLOT(slotOnlineJobSend(QList))); connect(m_onlineJobOutboxView, SIGNAL(editJob(QString)), kmymoney, SLOT(slotEditOnlineJob(QString))); connect(m_onlineJobOutboxView, SIGNAL(newCreditTransfer()), kmymoney, SLOT(slotNewOnlineTransfer())); connect(m_onlineJobOutboxView, SIGNAL(aboutToShow()), this, SIGNAL(aboutToChangeView())); connect(m_onlineJobOutboxView, SIGNAL(showContextMenu(onlineJob)), kmymoney, SLOT(slotShowOnlineJobContextMenu())); SimpleLedgerView* view = new SimpleLedgerView(kmymoney, this); KPageWidgetItem* frame = m_model->addPage(view, i18n("New ledger")); frame->setIcon(QIcon::fromTheme(g_Icons[Icon::DocumentProperties])); //set the model setModel(m_model); setCurrentPage(viewFrames[View::Home]); connect(this, SIGNAL(currentPageChanged(QModelIndex,QModelIndex)), this, SLOT(slotCurrentPageChanged(QModelIndex,QModelIndex))); updateViewType(); m_inConstructor = false; // add fast switching of main views through Ctrl + NUM_X struct pageInfo { View view; KMyMoneyViewFunc callback; QString text; QKeySequence shortcut = QKeySequence(); }; const QVector pageInfos { {View::Home, &KMyMoneyView::slotShowHomePage, i18n("Show home page"), Qt::CTRL + Qt::Key_1}, {View::Institutions, &KMyMoneyView::slotShowInstitutionsPage, i18n("Show institutions page"), Qt::CTRL + Qt::Key_2}, {View::Accounts, &KMyMoneyView::slotShowAccountsPage, i18n("Show accounts page"), Qt::CTRL + Qt::Key_3}, {View::Schedules, &KMyMoneyView::slotShowSchedulesPage, i18n("Show scheduled transactions page"), Qt::CTRL + Qt::Key_4}, {View::Categories, &KMyMoneyView::slotShowCategoriesPage, i18n("Show categories page"), Qt::CTRL + Qt::Key_5}, {View::Tags, &KMyMoneyView::slotShowTagsPage, i18n("Show tags page"), }, {View::Payees, &KMyMoneyView::slotShowPayeesPage, i18n("Show payees page"), Qt::CTRL + Qt::Key_6}, {View::Ledgers, &KMyMoneyView::slotShowLedgersPage, i18n("Show ledgers page"), Qt::CTRL + Qt::Key_7}, {View::Investments, &KMyMoneyView::slotShowInvestmentsPage, i18n("Show investments page"), Qt::CTRL + Qt::Key_8}, {View::Reports, &KMyMoneyView::slotShowReportsPage, i18n("Show reports page"), Qt::CTRL + Qt::Key_9}, {View::Budget, &KMyMoneyView::slotShowBudgetPage, i18n("Show budget page"), }, {View::Forecast, &KMyMoneyView::slotShowForecastPage, i18n("Show forecast page"), }, {View::OnlineJobOutbox, &KMyMoneyView::slotShowOutboxPage, i18n("Show outbox page") } }; QHash lutActions; auto aC = kmymoney->actionCollection(); auto pageCount = 0; foreach (const pageInfo info, pageInfos) { auto a = new QAction(0); // KActionCollection::addAction by name sets object name anyways, // so, as better alternative, set it here right from the start a->setObjectName(QLatin1String("ShowPage") + QString::number(pageCount++)); a->setText(info.text); connect(a, &QAction::triggered, this, info.callback); lutActions.insert(info.view, a); // store QAction's pointer for later processing if (!info.shortcut.isEmpty()) aC->setDefaultShortcut(a, info.shortcut); } aC->addActions(lutActions.values()); // Initialize kactivities resource instance #ifdef KF5Activities_FOUND m_activityResourceInstance = new KActivities::ResourceInstance(window()->winId(), this); connect(kmymoney, SIGNAL(fileLoaded(QUrl)), m_activityResourceInstance, SLOT(setUri(QUrl))); #endif } KMyMoneyView::~KMyMoneyView() { KMyMoneyGlobalSettings::setLastViewSelected(m_lastViewSelected); removeStorage(); } void KMyMoneyView::slotShowHomePage() { showPage(viewFrames[View::Home]); } void KMyMoneyView::slotShowInstitutionsPage() { showPage(viewFrames[View::Institutions]); m_institutionsView->setDefaultFocus(); } void KMyMoneyView::slotShowAccountsPage() { showPage(viewFrames[View::Accounts]); m_accountsView->setDefaultFocus(); } void KMyMoneyView::slotShowSchedulesPage() { showPage(viewFrames[View::Schedules]); m_scheduledView->setDefaultFocus(); } void KMyMoneyView::slotShowCategoriesPage() { showPage(viewFrames[View::Categories]); m_categoriesView->setDefaultFocus(); } void KMyMoneyView::slotShowTagsPage() { showPage(viewFrames[View::Tags]); m_tagsView->setDefaultFocus(); } void KMyMoneyView::slotShowPayeesPage() { showPage(viewFrames[View::Payees]); m_payeesView->setDefaultFocus(); } void KMyMoneyView::slotShowLedgersPage() { showPage(viewFrames[View::Ledgers]); m_ledgerView->setDefaultFocus(); } void KMyMoneyView::slotShowInvestmentsPage() { showPage(viewFrames[View::Investments]); m_investmentView->setDefaultFocus(); } void KMyMoneyView::slotShowReportsPage() { showPage(viewFrames[View::Reports]); m_reportsView->setDefaultFocus(); } void KMyMoneyView::slotShowBudgetPage() { showPage(viewFrames[View::Budget]); m_budgetView->setDefaultFocus(); } void KMyMoneyView::slotShowForecastPage() { showPage(viewFrames[View::Forecast]); m_forecastView->setDefaultFocus(); } void KMyMoneyView::slotShowOutboxPage() { showPage(viewFrames[View::OnlineJobOutbox]); m_onlineJobOutboxView->setDefaultFocus(); } void KMyMoneyView::showTitleBar(bool show) { if (m_header) m_header->setVisible(show); } void KMyMoneyView::updateViewType() { // set the face type KPageView::FaceType faceType = KPageView::List; switch (KMyMoneySettings::viewType()) { case 0: faceType = KPageView::List; break; case 1: faceType = KPageView::Tree; break; case 2: faceType = KPageView::Tabbed; break; } if (faceType != KMyMoneyView::faceType()) { setFaceType(faceType); if (faceType == KPageView::Tree) { QList views = findChildren(); foreach (QTreeView * view, views) { if (view && (view->parent() == this)) { view->setRootIsDecorated(false); break; } } } } } void KMyMoneyView::slotAccountTreeViewChanged(const eAccountsModel::Column column, const bool show) { QVector proxyModels {m_institutionsView->getProxyModel(), m_accountsView->getProxyModel(), - m_categoriesView->getProxyModel(), m_budgetView->getProxyModel()}; + m_categoriesView->getProxyModel(), m_budgetView->getProxyModel()}; for (auto i = proxyModels.count() - 1; i >= 0; --i) { // weed out unloaded views if (!proxyModels.at(i)) proxyModels.removeAt(i); } QString question; if (show) question = i18n("Do you want to show %1 column on every loaded view?", AccountsModel::getHeaderName(column)); else question = i18n("Do you want to hide %1 column on every loaded view?", AccountsModel::getHeaderName(column)); if (proxyModels.count() == 1 || // no need to ask what to do with other views because they aren't loaded KMessageBox::questionYesNo(this, question, QString(), KStandardGuiItem::yes(), KStandardGuiItem::no(), QStringLiteral("ShowColumnOnEveryView")) == KMessageBox::Yes) { Models::instance()->accountsModel()->setColumnVisibility(column, show); Models::instance()->institutionsModel()->setColumnVisibility(column, show); foreach(AccountsViewProxyModel *proxyModel, proxyModels) { if (!proxyModel) continue; proxyModel->setColumnVisibility(column, show); proxyModel->invalidate(); } } } bool KMyMoneyView::showPageHeader() const { return false; } void KMyMoneyView::showPage(KPageWidgetItem* pageItem) { // reset all selected items before showing the selected view // but not while we're in our own constructor if (!m_inConstructor && pageItem != currentPage()) { kmymoney->slotResetSelections(); } // pretend we're in the constructor to avoid calling the // above resets. For some reason which I don't know the details // of, KJanusWidget::showPage() calls itself recursively. This // screws up the action handling, as items could have been selected // in the meantime. We prevent this by setting the m_inConstructor // to true and reset it to the previos value when we leave this method. bool prevConstructor = m_inConstructor; m_inConstructor = true; setCurrentPage(pageItem); m_inConstructor = prevConstructor; if (!m_inConstructor) { // fixup some actions that are dependant on the view // this does not work during construction kmymoney->slotUpdateActions(); } } bool KMyMoneyView::canPrint() { bool rc = ( viewFrames[View::Reports] == currentPage() || viewFrames[View::Home] == currentPage() ); return rc; } bool KMyMoneyView::canCreateTransactions(const KMyMoneyRegister::SelectedTransactions& /* list */, QString& tooltip) const { // we can only create transactions in the ledger view so // we check that this is the active page bool rc = (viewFrames[View::Ledgers] == currentPage()); if (rc) rc = m_ledgerView->canCreateTransactions(tooltip); else tooltip = i18n("Creating transactions can only be performed in the ledger view"); return rc; } bool KMyMoneyView::canModifyTransactions(const KMyMoneyRegister::SelectedTransactions& list, QString& tooltip) const { // we can only modify transactions in the ledger view so // we check that this is the active page bool rc = (viewFrames[View::Ledgers] == currentPage()); if (rc) { rc = m_ledgerView->canModifyTransactions(list, tooltip); } else { tooltip = i18n("Modifying transactions can only be performed in the ledger view"); } return rc; } bool KMyMoneyView::canDuplicateTransactions(const KMyMoneyRegister::SelectedTransactions& list, QString& tooltip) const { // we can only duplicate transactions in the ledger view so // we check that this is the active page bool rc = (viewFrames[View::Ledgers] == currentPage()); if (rc) { rc = m_ledgerView->canDuplicateTransactions(list, tooltip); } else { tooltip = i18n("Duplicating transactions can only be performed in the ledger view"); } return rc; } bool KMyMoneyView::canEditTransactions(const KMyMoneyRegister::SelectedTransactions& list, QString& tooltip) const { bool rc; // we can only edit transactions in the ledger view so // we check that this is the active page if ((rc = canModifyTransactions(list, tooltip)) == true) { tooltip = i18n("Edit the current selected transactions"); rc = m_ledgerView->canEditTransactions(list, tooltip); } return rc; } bool KMyMoneyView::createNewTransaction() { bool rc = false; KMyMoneyRegister::SelectedTransactions list; QString txt; if (canCreateTransactions(list, txt)) { rc = m_ledgerView->selectEmptyTransaction(); } return rc; } TransactionEditor* KMyMoneyView::startEdit(const KMyMoneyRegister::SelectedTransactions& list) { TransactionEditor* editor = 0; QString txt; if (canEditTransactions(list, txt) || canCreateTransactions(list, txt)) { editor = m_ledgerView->startEdit(list); } return editor; } void KMyMoneyView::newStorage(storageTypeE t) { removeStorage(); auto file = MyMoneyFile::instance(); if (t == Memory) file->attachStorage(new MyMoneySeqAccessMgr); else file->attachStorage(new MyMoneyDatabaseMgr); } void KMyMoneyView::removeStorage() { auto file = MyMoneyFile::instance(); IMyMoneyStorage* p = file->storage(); if (p != 0) { file->detachStorage(p); delete p; } } void KMyMoneyView::enableViewsIfFileOpen() { // call set enabled only if the state differs to avoid widgets 'bouncing on the screen' while doing this if (viewFrames[View::Accounts]->isEnabled() != m_fileOpen) viewFrames[View::Accounts]->setEnabled(m_fileOpen); if (viewFrames[View::Institutions]->isEnabled() != m_fileOpen) viewFrames[View::Institutions]->setEnabled(m_fileOpen); if (viewFrames[View::Schedules]->isEnabled() != m_fileOpen) viewFrames[View::Schedules]->setEnabled(m_fileOpen); if (viewFrames[View::Categories]->isEnabled() != m_fileOpen) viewFrames[View::Categories]->setEnabled(m_fileOpen); if (viewFrames[View::Payees]->isEnabled() != m_fileOpen) viewFrames[View::Payees]->setEnabled(m_fileOpen); if (viewFrames[View::Tags]->isEnabled() != m_fileOpen) viewFrames[View::Tags]->setEnabled(m_fileOpen); if (viewFrames[View::Budget]->isEnabled() != m_fileOpen) viewFrames[View::Budget]->setEnabled(m_fileOpen); if (viewFrames[View::Ledgers]->isEnabled() != m_fileOpen) viewFrames[View::Ledgers]->setEnabled(m_fileOpen); if (viewFrames[View::Investments]->isEnabled() != m_fileOpen) viewFrames[View::Investments]->setEnabled(m_fileOpen); if (viewFrames[View::Reports]->isEnabled() != m_fileOpen) viewFrames[View::Reports]->setEnabled(m_fileOpen); if (viewFrames[View::Forecast]->isEnabled() != m_fileOpen) viewFrames[View::Forecast]->setEnabled(m_fileOpen); if (viewFrames[View::OnlineJobOutbox]->isEnabled() != m_fileOpen) viewFrames[View::OnlineJobOutbox]->setEnabled(m_fileOpen); emit viewStateChanged(m_fileOpen); } void KMyMoneyView::slotLedgerSelected(const QString& _accId, const QString& transaction) { auto acc = MyMoneyFile::instance()->account(_accId); QString accId(_accId); switch (acc.accountType()) { case Account::Type::Stock: // if a stock account is selected, we show the // the corresponding parent (investment) account acc = MyMoneyFile::instance()->account(acc.parentAccountId()); accId = acc.id(); // intentional fall through case Account::Type::Checkings: case Account::Type::Savings: case Account::Type::Cash: case Account::Type::CreditCard: case Account::Type::Loan: case Account::Type::Asset: case Account::Type::Liability: case Account::Type::AssetLoan: case Account::Type::Income: case Account::Type::Expense: case Account::Type::Investment: case Account::Type::Equity: setCurrentPage(viewFrames[View::Ledgers]); m_ledgerView->slotSelectAccount(accId, transaction); break; case Account::Type::CertificateDep: case Account::Type::MoneyMarket: case Account::Type::Currency: qDebug("No ledger view available for account type %d", (int)acc.accountType()); break; default: qDebug("Unknown account type %d in KMyMoneyView::slotLedgerSelected", (int)acc.accountType()); break; } } void KMyMoneyView::slotPayeeSelected(const QString& payee, const QString& account, const QString& transaction) { showPage(viewFrames[View::Payees]); m_payeesView->slotSelectPayeeAndTransaction(payee, account, transaction); } void KMyMoneyView::slotTagSelected(const QString& tag, const QString& account, const QString& transaction) { showPage(viewFrames[View::Tags]); m_tagsView->slotSelectTagAndTransaction(tag, account, transaction); } void KMyMoneyView::slotScheduleSelected(const QString& scheduleId) { MyMoneySchedule sched = MyMoneyFile::instance()->schedule(scheduleId); kmymoney->slotSelectSchedule(sched); } void KMyMoneyView::slotShowReport(const QString& reportid) { showPage(viewFrames[View::Reports]); m_reportsView->slotOpenReport(reportid); } void KMyMoneyView::slotShowReport(const MyMoneyReport& report) { showPage(viewFrames[View::Reports]); m_reportsView->slotOpenReport(report); } bool KMyMoneyView::fileOpen() { return m_fileOpen; } void KMyMoneyView::closeFile() { if (m_reportsView) m_reportsView->slotCloseAll(); // disconnect the signals disconnect(MyMoneyFile::instance(), &MyMoneyFile::objectAdded, Models::instance()->accountsModel(), &AccountsModel::slotObjectAdded); disconnect(MyMoneyFile::instance(), &MyMoneyFile::objectModified, Models::instance()->accountsModel(), &AccountsModel::slotObjectModified); disconnect(MyMoneyFile::instance(), &MyMoneyFile::objectRemoved, Models::instance()->accountsModel(), &AccountsModel::slotObjectRemoved); disconnect(MyMoneyFile::instance(), &MyMoneyFile::balanceChanged, Models::instance()->accountsModel(), &AccountsModel::slotBalanceOrValueChanged); disconnect(MyMoneyFile::instance(), &MyMoneyFile::valueChanged, Models::instance()->accountsModel(), &AccountsModel::slotBalanceOrValueChanged); disconnect(MyMoneyFile::instance(), &MyMoneyFile::objectAdded, Models::instance()->institutionsModel(), &InstitutionsModel::slotObjectAdded); disconnect(MyMoneyFile::instance(), &MyMoneyFile::objectModified, Models::instance()->institutionsModel(), &InstitutionsModel::slotObjectModified); disconnect(MyMoneyFile::instance(), &MyMoneyFile::objectRemoved, Models::instance()->institutionsModel(), &InstitutionsModel::slotObjectRemoved); disconnect(MyMoneyFile::instance(), &MyMoneyFile::balanceChanged, Models::instance()->institutionsModel(), &AccountsModel::slotBalanceOrValueChanged); disconnect(MyMoneyFile::instance(), &MyMoneyFile::valueChanged, Models::instance()->institutionsModel(), &AccountsModel::slotBalanceOrValueChanged); disconnect(MyMoneyFile::instance(), &MyMoneyFile::objectAdded, Models::instance()->equitiesModel(), &EquitiesModel::slotObjectAdded); disconnect(MyMoneyFile::instance(), &MyMoneyFile::objectModified, Models::instance()->equitiesModel(), &EquitiesModel::slotObjectModified); disconnect(MyMoneyFile::instance(), &MyMoneyFile::objectRemoved, Models::instance()->equitiesModel(), &EquitiesModel::slotObjectRemoved); disconnect(MyMoneyFile::instance(), &MyMoneyFile::balanceChanged, Models::instance()->equitiesModel(), &EquitiesModel::slotBalanceOrValueChanged); disconnect(MyMoneyFile::instance(), &MyMoneyFile::valueChanged, Models::instance()->equitiesModel(), &EquitiesModel::slotBalanceOrValueChanged); disconnect(MyMoneyFile::instance(), &MyMoneyFile::objectAdded, Models::instance()->securitiesModel(), &SecuritiesModel::slotObjectAdded); disconnect(MyMoneyFile::instance(), &MyMoneyFile::objectModified, Models::instance()->securitiesModel(), &SecuritiesModel::slotObjectModified); disconnect(MyMoneyFile::instance(), &MyMoneyFile::objectRemoved, Models::instance()->securitiesModel(), &SecuritiesModel::slotObjectRemoved); disconnect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, m_homeView, &KHomeView::slotLoadView); // notify the models that the file is going to be closed (we should have something like dataChanged that reaches the models first) Models::instance()->fileClosed(); emit kmmFilePlugin(preClose); if (isDatabase()) MyMoneyFile::instance()->storage()->close(); // to log off a database user newStorage(); slotShowHomePage(); emit kmmFilePlugin(postClose); m_fileOpen = false; emit fileClosed(); } void KMyMoneyView::ungetString(QIODevice *qfile, char *buf, int len) { buf = &buf[len-1]; while (len--) { qfile->ungetChar(*buf--); } } bool KMyMoneyView::readFile(const QUrl &url, IMyMoneyStorageFormat* pExtReader) { QString filename; - + bool downloadedFile = false; m_fileOpen = false; bool isEncrypted = false; IMyMoneyStorageFormat* pReader = 0; if (!url.isValid()) { qDebug("Invalid URL '%s'", qPrintable(url.url())); return false; } // disconnect the current storga manager from the engine MyMoneyFile::instance()->detachStorage(); if (url.scheme() == QLatin1String("sql")) { // handle reading of database m_fileType = KmmDb; // get rid of the mode parameter which is now redundant QUrl newUrl(url); QUrlQuery query(url); query.removeQueryItem("mode"); newUrl.setQuery(query); return (openDatabase(newUrl)); // on error, any message will have been displayed } IMyMoneyStorage *storage = new MyMoneySeqAccessMgr; if (url.isLocalFile()) { filename = url.toLocalFile(); } else { - // TODO: port to kf5 (NetAccess) -#if 0 - if (!KIO::NetAccess::download(url, filename, 0)) { - KMessageBox::detailedError(this, + downloadedFile = true; + KIO::StoredTransferJob *transferjob = KIO::storedGet (url); + KJobWidgets::setWindow(transferjob, this); + if (! transferjob->exec()) { + KMessageBox::detailedError(this, i18n("Error while loading file '%1'.", url.url()), - KIO::NetAccess::lastErrorString(), + transferjob->errorString(), i18n("File access error")); - return false; + return false; } -#endif + QTemporaryFile file; + file.setAutoRemove(false); + file.open(); + file.write(transferjob->data()); + filename = file.fileName(); + file.close(); } // let's glimps into the file to figure out, if it's one // of the old (uncompressed) or new (compressed) files. QFile file(filename); QFileInfo info(file); if (!info.isFile()) { QString msg = i18n("

%1 is not a KMyMoney file.

", filename); KMessageBox::error(this, msg, i18n("Filetype Error")); return false; } m_fmode = QFileDevice::ReadUser | QFileDevice::WriteUser; m_fmode |= info.permissions(); bool rc = true; // There's a problem with the KFilterDev and KGPGFile classes: // One supports the at(n) member but not ungetch() together with // read() and the other does not provide an at(n) method but // supports read() that considers the ungetch() buffer. QFile // supports everything so this is not a problem. We solve the problem // for now by keeping track of which method can be used. bool haveAt = true; emit kmmFilePlugin(preOpen); if (file.open(QIODevice::ReadOnly)) { QByteArray hdr(2, '\0'); int cnt; cnt = file.read(hdr.data(), 2); file.close(); if (cnt == 2) { QIODevice* qfile = nullptr; if (QString(hdr) == QString("\037\213")) { // gzipped? qfile = new KCompressionDevice(filename, COMPRESSION_TYPE); } else if (QString(hdr) == QString("--") // PGP ASCII armored? || QString(hdr) == QString("\205\001") // PGP binary? || QString(hdr) == QString("\205\002")) { // PGP binary? if (KGPGFile::GPGAvailable()) { qfile = new KGPGFile(filename); haveAt = false; isEncrypted = true; } else { KMessageBox::sorry(this, QString("%1"). arg(i18n("GPG is not available for decryption of file %1", filename))); qfile = new QFile(file.fileName()); } } else { // we can't use file directly, as we delete qfile later on qfile = new QFile(file.fileName()); } if (qfile->open(QIODevice::ReadOnly)) { try { hdr.resize(8); if (qfile->read(hdr.data(), 8) == 8) { if (haveAt) qfile->seek(0); else ungetString(qfile, hdr.data(), 8); // Ok, we got the first block of 8 bytes. Read in the two // unsigned long int's by preserving endianess. This is // achieved by reading them through a QDataStream object qint32 magic0, magic1; QDataStream s(&hdr, QIODevice::ReadOnly); s >> magic0; s >> magic1; // If both magic numbers match (we actually read in the // text 'KMyMoney' then we assume a binary file and // construct a reader for it. Otherwise, we construct // an XML reader object. // // The expression magic0 < 30 is only used to create // a binary reader if we assume an old binary file. This // should be removed at some point. An alternative is to // check the beginning of the file against an pattern // of the XML file (e.g. '?read(hdr.data(), 70) == 70) { if (haveAt) qfile->seek(0); else ungetString(qfile, hdr.data(), 70); QRegExp kmyexp(""); QRegExp gncexp("attachStorage(storage); loadAllCurrencies(); // currency list required for gnc MyMoneyFile::instance()->detachStorage(storage); pReader = pExtReader; m_fileType = GncXML; } } } } if (pReader) { pReader->setProgressCallback(&KMyMoneyView::progressCallback); pReader->readFile(qfile, dynamic_cast(storage)); } else { if (m_fileType == KmmBinary) { KMessageBox::sorry(this, QString("%1"). arg(i18n("File %1 contains the old binary format used by KMyMoney. Please use an older version of KMyMoney (0.8.x) that still supports this format to convert it to the new XML based format.", filename))); } else { KMessageBox::sorry(this, QString("%1"). arg(i18n("File %1 contains an unknown file format.", filename))); } rc = false; } } else { KMessageBox::sorry(this, QString("%1"). arg(i18n("Cannot read from file %1.", filename))); rc = false; } } catch (const MyMoneyException &e) { KMessageBox::sorry(this, QString("%1"). arg(i18n("Cannot load file %1. Reason: %2", filename, e.what()))); rc = false; } if (pReader) { pReader->setProgressCallback(0); delete pReader; } qfile->close(); } else { KGPGFile *gpgFile = qobject_cast(qfile); if (gpgFile && !gpgFile->errorToString().isEmpty()) { KMessageBox::sorry(this, QString("%1"). arg(i18n("The following error was encountered while decrypting file %1: %2", filename, gpgFile->errorToString()))); } else { KMessageBox::sorry(this, QString("%1"). arg(i18n("File %1 not found.", filename))); } rc = false; } delete qfile; } } else { KMessageBox::sorry(this, QString("%1"). arg(i18n("File %1 not found.", filename))); rc = false; } // things are finished, now we connect the storage to the engine // which forces a reload of the cache in the engine with those // objects that are cached MyMoneyFile::instance()->attachStorage(storage); if (rc == false) return rc; // encapsulate transactions to the engine to be able to commit/rollback MyMoneyFileTransaction ft; // make sure we setup the encryption key correctly if (isEncrypted && MyMoneyFile::instance()->value("kmm-encryption-key").isEmpty()) { MyMoneyFile::instance()->setValue("kmm-encryption-key", KMyMoneyGlobalSettings::gpgRecipientList().join(",")); } // make sure we setup the name of the base accounts in translated form try { MyMoneyFile *file = MyMoneyFile::instance(); checkAccountName(file->asset(), i18n("Asset")); checkAccountName(file->liability(), i18n("Liability")); checkAccountName(file->income(), i18n("Income")); checkAccountName(file->expense(), i18n("Expense")); checkAccountName(file->equity(), i18n("Equity")); ft.commit(); } catch (const MyMoneyException &) { } - // if a temporary file was constructed by NetAccess::download, - // then it will be removed with the next call. Otherwise, it - // stays untouched on the local filesystem - // TODO: port to KF5 (NetAccess) - //KIO::NetAccess::removeTempFile(filename); + // if a temporary file was downloaded, then it will be removed + // with the next call. Otherwise, it stays untouched on the local + // filesystem. + if (downloadedFile) { + QFile::remove(filename); + } + return initializeStorage(); } void KMyMoneyView::checkAccountName(const MyMoneyAccount& _acc, const QString& name) const { auto file = MyMoneyFile::instance(); if (_acc.name() != name) { MyMoneyAccount acc(_acc); acc.setName(name); file->modifyAccount(acc); } } bool KMyMoneyView::openDatabase(const QUrl &url) { m_fileOpen = false; // open the database IMyMoneySerialize* pStorage = dynamic_cast(MyMoneyFile::instance()->storage()); MyMoneyDatabaseMgr* pDBMgr = 0; if (! pStorage) { pDBMgr = new MyMoneyDatabaseMgr; pStorage = dynamic_cast(pDBMgr); } QExplicitlySharedDataPointer reader = pStorage->connectToDatabase(url); QUrl dbURL(url); bool retry = true; while (retry) { switch (reader->open(dbURL, QIODevice::ReadWrite)) { case 0: // opened okay retry = false; break; case 1: // permanent error KMessageBox::detailedError(this, i18n("Cannot open database %1\n", dbURL.toDisplayString()), reader->lastError()); if (pDBMgr) { removeStorage(); delete pDBMgr; } return false; case -1: // retryable error if (KMessageBox::warningYesNo(this, reader->lastError(), PACKAGE) == KMessageBox::No) { if (pDBMgr) { removeStorage(); delete pDBMgr; } return false; } else { QUrlQuery query(dbURL); const QString optionKey = QLatin1String("options"); QString options = query.queryItemValue(optionKey); if(!options.isEmpty()) { options += QLatin1Char(','); } options += QLatin1String("override"); query.removeQueryItem(QLatin1String("mode")); query.removeQueryItem(optionKey); query.addQueryItem(optionKey, options); dbURL.setQuery(query); } } } if (pDBMgr) { removeStorage(); MyMoneyFile::instance()->attachStorage(pDBMgr); } // single user mode; read some of the data into memory // FIXME - readFile no longer relevant? // tried removing it but then got no indication that loading was complete // also, didn't show home page reader->setProgressCallback(&KMyMoneyView::progressCallback); if (!reader->readFile()) { KMessageBox::detailedError(0, i18n("An unrecoverable error occurred while reading the database"), reader->lastError().toLatin1(), i18n("Database malfunction")); return false; } m_fileOpen = true; reader->setProgressCallback(0); return initializeStorage(); } bool KMyMoneyView::initializeStorage() { bool blocked = MyMoneyFile::instance()->signalsBlocked(); MyMoneyFile::instance()->blockSignals(true); // we check, if we have any currency in the file. If not, we load // all the default currencies we know. MyMoneyFileTransaction ft; try { updateCurrencyNames(); ft.commit(); } catch (const MyMoneyException &) { MyMoneyFile::instance()->blockSignals(blocked); return false; } // make sure, we have a base currency and all accounts are // also assigned to a currency. QString baseId; try { baseId = MyMoneyFile::instance()->baseCurrency().id(); } catch (const MyMoneyException &e) { qDebug() << e.what(); } if (baseId.isEmpty()) { // Stay in this endless loop until we have a base currency, // as without it the application does not work anymore. while (baseId.isEmpty()) { selectBaseCurrency(); try { baseId = MyMoneyFile::instance()->baseCurrency().id(); } catch (const MyMoneyException &e) { qDebug() << e.what(); } } } else { // in some odd intermediate cases there could be files out there // that have a base currency set, but still have accounts that // do not have a base currency assigned. This call will take // care of it. We can safely remove it later. // // Another work-around for this scenario is to remove the base // currency setting from the XML file by removing the line // // // // and restart the application with this file. This will force to // run the above loop. selectBaseCurrency(); } // setup the standard precision AmountEdit::setStandardPrecision(MyMoneyMoney::denomToPrec(MyMoneyFile::instance()->baseCurrency().smallestAccountFraction())); KMyMoneyEdit::setStandardPrecision(MyMoneyMoney::denomToPrec(MyMoneyFile::instance()->baseCurrency().smallestAccountFraction())); KSharedConfigPtr config = KSharedConfig::openConfig(); KPageWidgetItem* page; KConfigGroup grp = config->group("General Options"); if (KMyMoneyGlobalSettings::startLastViewSelected() != 0) page = viewFrames.value(static_cast(KMyMoneyGlobalSettings::lastViewSelected())); else page = viewFrames[View::Home]; // For debugging purposes, we can turn off the automatic fix manually // by setting the entry in kmymoneyrc to true grp = config->group("General Options"); if (grp.readEntry("SkipFix", false) != true) { MyMoneyFileTransaction ft; try { // Check if we have to modify the file before we allow to work with it IMyMoneyStorage* s = MyMoneyFile::instance()->storage(); while (s->fileFixVersion() < s->currentFixVersion()) { qDebug("%s", qPrintable((QString("testing fileFixVersion %1 < %2").arg(s->fileFixVersion()).arg(s->currentFixVersion())))); switch (s->fileFixVersion()) { case 0: fixFile_0(); s->setFileFixVersion(1); break; case 1: fixFile_1(); s->setFileFixVersion(2); break; case 2: fixFile_2(); s->setFileFixVersion(3); break; case 3: fixFile_3(); s->setFileFixVersion(4); break; // add new levels above. Don't forget to increase currentFixVersion() for all // the storage backends this fix applies to default: throw MYMONEYEXCEPTION(i18n("Unknown fix level in input file")); } } ft.commit(); } catch (const MyMoneyException &) { MyMoneyFile::instance()->blockSignals(blocked); return false; } } else { qDebug("Skipping automatic transaction fix!"); } MyMoneyFile::instance()->blockSignals(blocked); // FIXME: we need to check, if it's necessary to have this // automatic funcitonality // if there's no asset account, then automatically start the // new account wizard // kmymoney->createInitialAccount(); m_fileOpen = true; emit kmmFilePlugin(postOpen); Models::instance()->fileOpened(); // connect the needed signals connect(MyMoneyFile::instance(), &MyMoneyFile::objectAdded, Models::instance()->accountsModel(), &AccountsModel::slotObjectAdded); connect(MyMoneyFile::instance(), &MyMoneyFile::objectModified, Models::instance()->accountsModel(), &AccountsModel::slotObjectModified); connect(MyMoneyFile::instance(), &MyMoneyFile::objectRemoved, Models::instance()->accountsModel(), &AccountsModel::slotObjectRemoved); connect(MyMoneyFile::instance(), &MyMoneyFile::balanceChanged, Models::instance()->accountsModel(), &AccountsModel::slotBalanceOrValueChanged); connect(MyMoneyFile::instance(), &MyMoneyFile::valueChanged, Models::instance()->accountsModel(), &AccountsModel::slotBalanceOrValueChanged); connect(MyMoneyFile::instance(), &MyMoneyFile::objectAdded, Models::instance()->institutionsModel(), &InstitutionsModel::slotObjectAdded); connect(MyMoneyFile::instance(), &MyMoneyFile::objectModified, Models::instance()->institutionsModel(), &InstitutionsModel::slotObjectModified); connect(MyMoneyFile::instance(), &MyMoneyFile::objectRemoved, Models::instance()->institutionsModel(), &InstitutionsModel::slotObjectRemoved); connect(MyMoneyFile::instance(), &MyMoneyFile::balanceChanged, Models::instance()->institutionsModel(), &AccountsModel::slotBalanceOrValueChanged); connect(MyMoneyFile::instance(), &MyMoneyFile::valueChanged, Models::instance()->institutionsModel(), &AccountsModel::slotBalanceOrValueChanged); connect(MyMoneyFile::instance(), &MyMoneyFile::objectAdded, Models::instance()->equitiesModel(), &EquitiesModel::slotObjectAdded); connect(MyMoneyFile::instance(), &MyMoneyFile::objectModified, Models::instance()->equitiesModel(), &EquitiesModel::slotObjectModified); connect(MyMoneyFile::instance(), &MyMoneyFile::objectRemoved, Models::instance()->equitiesModel(), &EquitiesModel::slotObjectRemoved); connect(MyMoneyFile::instance(), &MyMoneyFile::balanceChanged, Models::instance()->equitiesModel(), &EquitiesModel::slotBalanceOrValueChanged); connect(MyMoneyFile::instance(), &MyMoneyFile::valueChanged, Models::instance()->equitiesModel(), &EquitiesModel::slotBalanceOrValueChanged); connect(MyMoneyFile::instance(), &MyMoneyFile::objectAdded, Models::instance()->securitiesModel(), &SecuritiesModel::slotObjectAdded); connect(MyMoneyFile::instance(), &MyMoneyFile::objectModified, Models::instance()->securitiesModel(), &SecuritiesModel::slotObjectModified); connect(MyMoneyFile::instance(), &MyMoneyFile::objectRemoved, Models::instance()->securitiesModel(), &SecuritiesModel::slotObjectRemoved); // inform everyone about new data MyMoneyFile::instance()->preloadCache(); MyMoneyFile::instance()->forceDataChanged(); // views can wait since they are going to be refresed in slotRefreshViews connect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, m_homeView, &KHomeView::slotLoadView); // if we currently see a different page, then select the right one if (page != currentPage()) { showPage(page); } emit fileOpened(); return true; } void KMyMoneyView::saveToLocalFile(const QString& localFile, IMyMoneyStorageFormat* pWriter, bool plaintext, const QString& keyList) { // Check GPG encryption bool encryptFile = true; bool encryptRecover = false; if (!keyList.isEmpty()) { if (!KGPGFile::GPGAvailable()) { KMessageBox::sorry(this, i18n("GPG does not seem to be installed on your system. Please make sure that GPG can be found using the standard search path. This time, encryption is disabled."), i18n("GPG not found")); encryptFile = false; } else { if (KMyMoneyGlobalSettings::encryptRecover()) { encryptRecover = true; if (!KGPGFile::keyAvailable(QString(recoveryKeyId))) { KMessageBox::sorry(this, i18n("

You have selected to encrypt your data also with the KMyMoney recover key, but the key with id

%1

has not been found in your keyring at this time. Please make sure to import this key into your keyring. You can find it on the KMyMoney web-site. This time your data will not be encrypted with the KMyMoney recover key.

", QString(recoveryKeyId)), i18n("GPG Key not found")); encryptRecover = false; } } for(const QString& key: keyList.split(',', QString::SkipEmptyParts)) { if (!KGPGFile::keyAvailable(key)) { KMessageBox::sorry(this, i18n("

You have specified to encrypt your data for the user-id

%1.

Unfortunately, a valid key for this user-id was not found in your keyring. Please make sure to import a valid key for this user-id. This time, encryption is disabled.

", key), i18n("GPG Key not found")); encryptFile = false; break; } } if (encryptFile == true) { QString msg = i18n("

You have configured to save your data in encrypted form using GPG. Make sure you understand that you might lose all your data if you encrypt it, but cannot decrypt it later on. If unsure, answer No.

"); if (KMessageBox::questionYesNo(this, msg, i18n("Store GPG encrypted"), KStandardGuiItem::yes(), KStandardGuiItem::no(), "StoreEncrypted") == KMessageBox::No) { encryptFile = false; } } } } // Create a temporary file if needed QString writeFile = localFile; QTemporaryFile tmpFile; if (QFile::exists(localFile)) { tmpFile.open(); writeFile = tmpFile.fileName(); tmpFile.close(); } /** * @brief Automatically restore settings when scope is left */ struct restorePreviousSettingsHelper { restorePreviousSettingsHelper() : m_signalsWereBlocked{MyMoneyFile::instance()->signalsBlocked()} { MyMoneyFile::instance()->blockSignals(true); } ~restorePreviousSettingsHelper() { MyMoneyFile::instance()->blockSignals(m_signalsWereBlocked); } const bool m_signalsWereBlocked; } restoreHelper; MyMoneyFileTransaction ft; MyMoneyFile::instance()->deletePair("kmm-encryption-key"); std::unique_ptr device; if (!keyList.isEmpty() && encryptFile && !plaintext) { std::unique_ptr kgpg = std::unique_ptr(new KGPGFile{writeFile}); if (kgpg) { for(const QString& key: keyList.split(',', QString::SkipEmptyParts)) { kgpg->addRecipient(key.toLatin1()); } if (encryptRecover) { kgpg->addRecipient(recoveryKeyId); } MyMoneyFile::instance()->setValue("kmm-encryption-key", keyList); device = std::unique_ptr(kgpg.release()); } } else { QFile *file = new QFile(writeFile); // The second parameter of KCompressionDevice means that KCompressionDevice will delete the QFile object device = std::unique_ptr(new KCompressionDevice{file, true, (plaintext) ? KCompressionDevice::None : COMPRESSION_TYPE}); } ft.commit(); if (!device || !device->open(QIODevice::WriteOnly)) { throw MYMONEYEXCEPTION(i18n("Unable to open file '%1' for writing.", localFile)); } pWriter->setProgressCallback(&KMyMoneyView::progressCallback); pWriter->writeFile(device.get(), dynamic_cast(MyMoneyFile::instance()->storage())); device->close(); // Check for errors if possible, only possible for KGPGFile QFileDevice *fileDevice = qobject_cast(device.get()); if (fileDevice && fileDevice->error() != QFileDevice::NoError) { throw MYMONEYEXCEPTION(i18n("Failure while writing to '%1'", localFile)); } if (writeFile != localFile) { // This simple comparison is possible because the strings are equal if no temporary file was created. // If a temporary file was created, it is made in a way that the name is definitely different. So no // symlinks etc. have to be evaluated. if (!QFile::remove(localFile) || !QFile::rename(writeFile, localFile)) throw MYMONEYEXCEPTION(i18n("Failure while writing to '%1'", localFile)); } QFile::setPermissions(localFile, m_fmode); pWriter->setProgressCallback(0); } bool KMyMoneyView::saveFile(const QUrl &url, const QString& keyList) { QString filename = url.path(); if (!fileOpen()) { KMessageBox::error(this, i18n("Tried to access a file when it has not been opened")); return false; } emit kmmFilePlugin(preSave); std::unique_ptr storageWriter; // If this file ends in ".ANON.XML" then this should be written using the // anonymous writer. bool plaintext = filename.right(4).toLower() == ".xml"; if (filename.right(9).toLower() == ".anon.xml") { //! @todo C++14: use std::make_unique, also some lines below storageWriter = std::unique_ptr(new MyMoneyStorageANON); } else { storageWriter = std::unique_ptr(new MyMoneyStorageXML); } // actually, url should be the parameter to this function // but for now, this would involve too many changes bool rc = true; try { if (! url.isValid()) { throw MYMONEYEXCEPTION(i18n("Malformed URL '%1'", url.url())); } if (url.isLocalFile()) { filename = url.toLocalFile(); try { const unsigned int nbak = KMyMoneyGlobalSettings::autoBackupCopies(); if (nbak) { KBackup::numberedBackupFile(filename, QString(), QString::fromLatin1("~"), nbak); } saveToLocalFile(filename, storageWriter.get(), plaintext, keyList); } catch (const MyMoneyException &) { throw MYMONEYEXCEPTION(i18n("Unable to write changes to '%1'", filename)); } } else { QTemporaryFile tmpfile; tmpfile.open(); // to obtain the name tmpfile.close(); saveToLocalFile(tmpfile.fileName(), storageWriter.get(), plaintext, keyList); - // TODO: port to kf5 - //if (!KIO::NetAccess::upload(tmpfile.fileName(), url, 0)) - // throw MYMONEYEXCEPTION(i18n("Unable to upload to '%1'", url.toDisplayString())); + + Q_CONSTEXPR int permission = -1; + QFile file(tmpfile.fileName()); + file.open(QIODevice::ReadOnly); + KIO::StoredTransferJob *putjob = KIO::storedPut(file.readAll(), url, permission, KIO::JobFlag::Overwrite); + if (!putjob->exec()) { + throw MYMONEYEXCEPTION(i18n("Unable to upload to '%1'.
%2", url.toDisplayString(), putjob->errorString())); + } + file.close(); } m_fileType = KmmXML; } catch (const MyMoneyException &e) { KMessageBox::error(this, e.what()); MyMoneyFile::instance()->setDirty(); rc = false; } emit kmmFilePlugin(postSave); return rc; } bool KMyMoneyView::saveAsDatabase(const QUrl &url) { bool rc = false; if (!fileOpen()) { KMessageBox::error(this, i18n("Tried to access a file when it has not been opened")); return (rc); } MyMoneyStorageSql *writer = new MyMoneyStorageSql(dynamic_cast(MyMoneyFile::instance()->storage()), url); bool canWrite = false; switch (writer->open(url, QIODevice::WriteOnly)) { case 0: canWrite = true; break; case -1: // dbase already has data, see if he wants to clear it out if (KMessageBox::warningContinueCancel(0, i18n("Database contains data which must be removed before using Save As.\n" "Do you wish to continue?"), "Database not empty") == KMessageBox::Continue) { if (writer->open(url, QIODevice::WriteOnly, true) == 0) canWrite = true; } else { delete writer; return false; } break; } if (canWrite) { writer->setProgressCallback(&KMyMoneyView::progressCallback); if (!writer->writeFile()) { KMessageBox::detailedError(0, i18n("An unrecoverable error occurred while writing to the database.\n" "It may well be corrupt."), writer->lastError().toLatin1(), i18n("Database malfunction")); rc = false; } writer->setProgressCallback(0); rc = true; } else { KMessageBox::detailedError(this, i18n("Cannot open or create database %1.\n" "Retry Save As Database and click Help" " for further info.", url.toDisplayString()), writer->lastError()); } delete writer; return (rc); } bool KMyMoneyView::dirty() { if (!fileOpen()) return false; return MyMoneyFile::instance()->dirty(); } bool KMyMoneyView::startReconciliation(const MyMoneyAccount& account, const QDate& reconciliationDate, const MyMoneyMoney& endingBalance) { bool ok = true; // we cannot reconcile standard accounts if (MyMoneyFile::instance()->isStandardAccount(account.id())) ok = false; // check if we can reconcile this account // it makes sense for asset and liability accounts only if (ok == true) { if (account.isAssetLiability()) { showPage(viewFrames[View::Ledgers]); // prepare reconciliation mode emit reconciliationStarts(account, reconciliationDate, endingBalance); } else { ok = false; } } return ok; } void KMyMoneyView::finishReconciliation(const MyMoneyAccount& /* account */) { emit reconciliationStarts(MyMoneyAccount(), QDate(), MyMoneyMoney()); } void KMyMoneyView::newFile() { closeFile(); m_fileType = KmmXML; // assume native type until saved m_fileOpen = true; } void KMyMoneyView::slotSetBaseCurrency(const MyMoneySecurity& baseCurrency) { if (!baseCurrency.id().isEmpty()) { QString baseId; try { baseId = MyMoneyFile::instance()->baseCurrency().id(); } catch (const MyMoneyException &e) { qDebug("%s", qPrintable(e.what())); } if (baseCurrency.id() != baseId) { MyMoneyFileTransaction ft; try { MyMoneyFile::instance()->setBaseCurrency(baseCurrency); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::sorry(this, i18n("Cannot set %1 as base currency: %2", baseCurrency.name(), e.what()), i18n("Set base currency")); } } AmountEdit::setStandardPrecision(MyMoneyMoney::denomToPrec(MyMoneyFile::instance()->baseCurrency().smallestAccountFraction())); KMyMoneyEdit::setStandardPrecision(MyMoneyMoney::denomToPrec(MyMoneyFile::instance()->baseCurrency().smallestAccountFraction())); } } void KMyMoneyView::selectBaseCurrency() { auto file = MyMoneyFile::instance(); // check if we have a base currency. If not, we need to select one QString baseId; try { baseId = MyMoneyFile::instance()->baseCurrency().id(); } catch (const MyMoneyException &e) { qDebug("%s", qPrintable(e.what())); } if (baseId.isEmpty()) { QPointer dlg = new KCurrencyEditDlg(this); connect(dlg, SIGNAL(selectBaseCurrency(MyMoneySecurity)), this, SLOT(slotSetBaseCurrency(MyMoneySecurity))); dlg->exec(); delete dlg; } try { baseId = MyMoneyFile::instance()->baseCurrency().id(); } catch (const MyMoneyException &e) { qDebug("%s", qPrintable(e.what())); } if (!baseId.isEmpty()) { // check that all accounts have a currency QList list; file->accountList(list); QList::Iterator it; // don't forget those standard accounts list << file->asset(); list << file->liability(); list << file->income(); list << file->expense(); list << file->equity(); for (it = list.begin(); it != list.end(); ++it) { QString cid; try { if (!(*it).currencyId().isEmpty() || (*it).currencyId().length() != 0) cid = MyMoneyFile::instance()->currency((*it).currencyId()).id(); } catch (const MyMoneyException &) { } if (cid.isEmpty()) { (*it).setCurrencyId(baseId); MyMoneyFileTransaction ft; try { file->modifyAccount(*it); ft.commit(); } catch (const MyMoneyException &e) { qDebug("Unable to setup base currency in account %s (%s): %s", qPrintable((*it).name()), qPrintable((*it).id()), qPrintable(e.what())); } } } } } void KMyMoneyView::updateCurrencyNames() { auto file = MyMoneyFile::instance(); MyMoneyFileTransaction ft; QList storedCurrencies = MyMoneyFile::instance()->currencyList(); QList availableCurrencies = MyMoneyFile::instance()->availableCurrencyList(); QStringList currencyIDs; foreach (auto currency, availableCurrencies) currencyIDs.append(currency.id()); try { foreach (auto currency, storedCurrencies) { int i = currencyIDs.indexOf(currency.id()); if (i != -1 && availableCurrencies.at(i).name() != currency.name()) { currency.setName(availableCurrencies.at(i).name()); file->modifyCurrency(currency); } } ft.commit(); } catch (const MyMoneyException &e) { qDebug("Error %s updating currency names", qPrintable(e.what())); } } void KMyMoneyView::loadAllCurrencies() { auto file = MyMoneyFile::instance(); MyMoneyFileTransaction ft; if (!file->currencyList().isEmpty()) return; QMap ancientCurrencies = file->ancientCurrencies(); try { foreach (auto currency, file->availableCurrencyList()) { file->addCurrency(currency); MyMoneyPrice price = ancientCurrencies.value(currency, MyMoneyPrice()); if (price != MyMoneyPrice()) file->addPrice(price); } ft.commit(); } catch (const MyMoneyException &e) { qDebug("Error %s loading currency", qPrintable(e.what())); } } void KMyMoneyView::viewAccountList(const QString& /*selectAccount*/) { if (viewFrames[View::Accounts] != currentPage()) showPage(viewFrames[View::Accounts]); m_accountsView->show(); } void KMyMoneyView::slotRefreshViews() { // turn off sync between ledger and investment view disconnect(m_investmentView, SIGNAL(accountSelected(MyMoneyObject)), m_ledgerView, SLOT(slotSelectAccount(MyMoneyObject))); disconnect(m_ledgerView, SIGNAL(accountSelected(MyMoneyObject)), m_investmentView, SLOT(slotSelectAccount(MyMoneyObject))); // TODO turn sync between ledger and investment view if selected by user if (KMyMoneyGlobalSettings::syncLedgerInvestment()) { connect(m_investmentView, SIGNAL(accountSelected(MyMoneyObject)), m_ledgerView, SLOT(slotSelectAccount(MyMoneyObject))); connect(m_ledgerView, SIGNAL(accountSelected(MyMoneyObject)), m_investmentView, SLOT(slotSelectAccount(MyMoneyObject))); } showTitleBar(KMyMoneyGlobalSettings::showTitleBar()); m_accountsView->refresh(); m_institutionsView->refresh(); m_categoriesView->refresh(); m_payeesView->slotLoadPayees(); m_tagsView->refresh(); m_ledgerView->slotLoadView(); m_budgetView->refresh(); m_homeView->slotLoadView(); m_investmentView->slotLoadView(); m_reportsView->slotLoadView(); m_forecastView->slotLoadForecast(); m_scheduledView->slotReloadView(); } void KMyMoneyView::slotShowTransactionDetail(bool detailed) { KMyMoneyGlobalSettings::setShowRegisterDetailed(detailed); slotRefreshViews(); } void KMyMoneyView::progressCallback(int current, int total, const QString& msg) { kmymoney->progressCallback(current, total, msg); } void KMyMoneyView::slotCurrentPageChanged(const QModelIndex current, const QModelIndex) { // remember the current page m_lastViewSelected = current.row(); // set the current page's title in the header if (m_header) m_header->setText(m_model->data(current, KPageModel::HeaderRole).toString()); } /* DO NOT ADD code to this function or any of it's called ones. Instead, create a new function, fixFile_n, and modify the initializeStorage() logic above to call it */ void KMyMoneyView::fixFile_3() { // make sure each storage object contains a (unique) id MyMoneyFile::instance()->storageId(); } void KMyMoneyView::fixFile_2() { auto file = MyMoneyFile::instance(); MyMoneyTransactionFilter filter; filter.setReportAllSplits(false); QList transactionList; file->transactionList(transactionList, filter); // scan the transactions and modify transactions with two splits // which reference an account and a category to have the memo text // of the account. auto count = 0; foreach (const auto transaction, transactionList) { if (transaction.splitCount() == 2) { QString accountId; QString categoryId; QString accountMemo; QString categoryMemo; foreach (const auto split, transaction.splits()) { auto acc = file->account(split.accountId()); if (acc.isIncomeExpense()) { categoryId = split.id(); categoryMemo = split.memo(); } else { accountId = split.id(); accountMemo = split.memo(); } } if (!accountId.isEmpty() && !categoryId.isEmpty() && accountMemo != categoryMemo) { MyMoneyTransaction t(transaction); MyMoneySplit s(t.splitById(categoryId)); s.setMemo(accountMemo); t.modifySplit(s); file->modifyTransaction(t); ++count; } } } qDebug("%d transactions fixed in fixFile_2", count); } void KMyMoneyView::fixFile_1() { // we need to fix reports. If the account filter list contains // investment accounts, we need to add the stock accounts to the list // as well if we don't have the expert mode enabled if (!KMyMoneyGlobalSettings::expertMode()) { try { QList reports = MyMoneyFile::instance()->reportList(); QList::iterator it_r; for (it_r = reports.begin(); it_r != reports.end(); ++it_r) { QStringList list; (*it_r).accounts(list); QStringList missing; QStringList::const_iterator it_a, it_b; for (it_a = list.constBegin(); it_a != list.constEnd(); ++it_a) { auto acc = MyMoneyFile::instance()->account(*it_a); if (acc.accountType() == Account::Type::Investment) { foreach (const auto accountID, acc.accountList()) { if (!list.contains(accountID)) { missing.append(accountID); } } } } if (!missing.isEmpty()) { (*it_r).addAccount(missing); MyMoneyFile::instance()->modifyReport(*it_r); } } } catch (const MyMoneyException &) { } } } #if 0 if (!m_accountsView->allItemsSelected()) { // retrieve a list of selected accounts QStringList list; m_accountsView->selectedItems(list); // if we're not in expert mode, we need to make sure // that all stock accounts for the selected investment // account are also selected if (!KMyMoneyGlobalSettings::expertMode()) { QStringList missing; QStringList::const_iterator it_a, it_b; for (it_a = list.begin(); it_a != list.end(); ++it_a) { auto acc = MyMoneyFile::instance()->account(*it_a); if (acc.accountType() == Account::Type::Investment) { foreach (const auto accountID, acc.accountList()) { if (!list.contains(accountID)) { missing.append(accountID); } } } } list += missing; } m_filter.addAccount(list); } #endif void KMyMoneyView::fixFile_0() { /* (Ace) I am on a crusade against file fixups. Whenever we have to fix the * file, it is really a warning. So I'm going to print a debug warning, and * then go track them down when I see them to figure out how they got saved * out needing fixing anyway. */ auto file = MyMoneyFile::instance(); QList accountList; file->accountList(accountList); QList::Iterator it_a; QList scheduleList = file->scheduleList(); QList::Iterator it_s; MyMoneyAccount equity = file->equity(); MyMoneyAccount asset = file->asset(); bool equityListEmpty = equity.accountList().count() == 0; for (it_a = accountList.begin(); it_a != accountList.end(); ++it_a) { if ((*it_a).accountType() == Account::Type::Loan || (*it_a).accountType() == Account::Type::AssetLoan) { fixLoanAccount_0(*it_a); } // until early before 0.8 release, the equity account was not saved to // the file. If we have an equity account with no sub-accounts but // find and equity account that has equity() as it's parent, we reparent // this account. Need to move it to asset() first, because otherwise // MyMoneyFile::reparent would act as NOP. if (equityListEmpty && (*it_a).accountType() == Account::Type::Equity) { if ((*it_a).parentAccountId() == equity.id()) { auto acc = *it_a; // tricky, force parent account to be empty so that we really // can re-parent it acc.setParentAccountId(QString()); file->reparentAccount(acc, equity); qDebug() << Q_FUNC_INFO << " fixed account " << acc.id() << " reparented to " << equity.id(); } } } for (it_s = scheduleList.begin(); it_s != scheduleList.end(); ++it_s) { fixSchedule_0(*it_s); } fixTransactions_0(); } void KMyMoneyView::fixSchedule_0(MyMoneySchedule sched) { MyMoneyTransaction t = sched.transaction(); QList splitList = t.splits(); QList::ConstIterator it_s; bool updated = false; try { // Check if the splits contain valid data and set it to // be valid. for (it_s = splitList.constBegin(); it_s != splitList.constEnd(); ++it_s) { // the first split is always the account on which this transaction operates // and if the transaction commodity is not set, we take this if (it_s == splitList.constBegin() && t.commodity().isEmpty()) { qDebug() << Q_FUNC_INFO << " " << t.id() << " has no commodity"; try { auto acc = MyMoneyFile::instance()->account((*it_s).accountId()); t.setCommodity(acc.currencyId()); updated = true; } catch (const MyMoneyException &) { } } // make sure the account exists. If not, remove the split try { MyMoneyFile::instance()->account((*it_s).accountId()); } catch (const MyMoneyException &) { qDebug() << Q_FUNC_INFO << " " << sched.id() << " " << (*it_s).id() << " removed, because account '" << (*it_s).accountId() << "' does not exist."; t.removeSplit(*it_s); updated = true; } if ((*it_s).reconcileFlag() != eMyMoney::Split::State::NotReconciled) { qDebug() << Q_FUNC_INFO << " " << sched.id() << " " << (*it_s).id() << " should be 'not reconciled'"; MyMoneySplit split = *it_s; split.setReconcileDate(QDate()); split.setReconcileFlag(eMyMoney::Split::State::NotReconciled); t.modifySplit(split); updated = true; } // the schedule logic used to operate only on the value field. // This is now obsolete. if ((*it_s).shares().isZero() && !(*it_s).value().isZero()) { MyMoneySplit split = *it_s; split.setShares(split.value()); t.modifySplit(split); updated = true; } } // If there have been changes, update the schedule and // the engine data. if (updated) { sched.setTransaction(t); MyMoneyFile::instance()->modifySchedule(sched); } } catch (const MyMoneyException &e) { qWarning("Unable to update broken schedule: %s", qPrintable(e.what())); } } void KMyMoneyView::fixLoanAccount_0(MyMoneyAccount acc) { if (acc.value("final-payment").isEmpty() || acc.value("term").isEmpty() || acc.value("periodic-payment").isEmpty() || acc.value("loan-amount").isEmpty() || acc.value("interest-calculation").isEmpty() || acc.value("schedule").isEmpty() || acc.value("fixed-interest").isEmpty()) { KMessageBox::information(this, i18n("

The account \"%1\" was previously created as loan account but some information is missing.

The new loan wizard will be started to collect all relevant information.

Please use KMyMoney version 0.8.7 or later and earlier than version 0.9 to correct the problem.

" , acc.name()), i18n("Account problem")); throw MYMONEYEXCEPTION("Fix LoanAccount0 not supported anymore"); } } void KMyMoneyView::createSchedule(MyMoneySchedule newSchedule, MyMoneyAccount& newAccount) { // Add the schedule only if one exists // // Remember to modify the first split to reference the newly created account if (!newSchedule.name().isEmpty()) { MyMoneyFileTransaction ft; try { // We assume at least 2 splits in the transaction MyMoneyTransaction t = newSchedule.transaction(); if (t.splitCount() < 2) { throw MYMONEYEXCEPTION("Transaction for schedule has less than 2 splits!"); } // now search the split that does not have an account reference // and set it up to be the one of the account we just added // to the account pool. Note: the schedule code used to leave // this always the first split, but the loan code leaves it as // the second one. So I thought, searching is a good alternative .... foreach (const auto split, t.splits()) { if (split.accountId().isEmpty()) { MyMoneySplit s = split; s.setAccountId(newAccount.id()); t.modifySplit(s); break; } } newSchedule.setTransaction(t); MyMoneyFile::instance()->addSchedule(newSchedule); // in case of a loan account, we keep a reference to this // schedule in the account if (newAccount.isLoan()) { newAccount.setValue("schedule", newSchedule.id()); MyMoneyFile::instance()->modifyAccount(newAccount); } ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::information(this, i18n("Unable to add schedule: %1", e.what())); } } } void KMyMoneyView::fixTransactions_0() { auto file = MyMoneyFile::instance(); QList scheduleList = file->scheduleList(); MyMoneyTransactionFilter filter; filter.setReportAllSplits(false); QList transactionList; file->transactionList(transactionList, filter); QList::Iterator it_x; QStringList interestAccounts; KMSTATUS(i18n("Fix transactions")); kmymoney->slotStatusProgressBar(0, scheduleList.count() + transactionList.count()); int cnt = 0; // scan the schedules to find interest accounts for (it_x = scheduleList.begin(); it_x != scheduleList.end(); ++it_x) { MyMoneyTransaction t = (*it_x).transaction(); QList::ConstIterator it_s; QStringList accounts; bool hasDuplicateAccounts = false; foreach (const auto split, t.splits()) { if (accounts.contains(split.accountId())) { hasDuplicateAccounts = true; qDebug() << Q_FUNC_INFO << " " << t.id() << " has multiple splits with account " << split.accountId(); } else { accounts << split.accountId(); } if (split.action() == MyMoneySplit::ActionInterest) { if (interestAccounts.contains(split.accountId()) == 0) { interestAccounts << split.accountId(); } } } if (hasDuplicateAccounts) { fixDuplicateAccounts_0(t); } ++cnt; if (!(cnt % 10)) kmymoney->slotStatusProgressBar(cnt); } // scan the transactions and modify loan transactions for (auto& transaction : transactionList) { const char *defaultAction = 0; QList splits = transaction.splits(); QStringList accounts; // check if base commodity is set. if not, set baseCurrency if (transaction.commodity().isEmpty()) { qDebug() << Q_FUNC_INFO << " " << transaction.id() << " has no base currency"; transaction.setCommodity(file->baseCurrency().id()); file->modifyTransaction(transaction); } bool isLoan = false; // Determine default action if (transaction.splitCount() == 2) { // check for transfer int accountCount = 0; MyMoneyMoney val; foreach (const auto split, splits) { auto acc = file->account(split.accountId()); if (acc.accountGroup() == Account::Type::Asset || acc.accountGroup() == Account::Type::Liability) { val = split.value(); accountCount++; if (acc.accountType() == Account::Type::Loan || acc.accountType() == Account::Type::AssetLoan) isLoan = true; } else break; } if (accountCount == 2) { if (isLoan) defaultAction = MyMoneySplit::ActionAmortization; else defaultAction = MyMoneySplit::ActionTransfer; } else { if (val.isNegative()) defaultAction = MyMoneySplit::ActionWithdrawal; else defaultAction = MyMoneySplit::ActionDeposit; } } isLoan = false; foreach (const auto split, splits) { auto acc = file->account(split.accountId()); MyMoneyMoney val = split.value(); if (acc.accountGroup() == Account::Type::Asset || acc.accountGroup() == Account::Type::Liability) { if (!val.isPositive()) { defaultAction = MyMoneySplit::ActionWithdrawal; break; } else { defaultAction = MyMoneySplit::ActionDeposit; break; } } } #if 0 // Check for correct actions in transactions referencing credit cards bool needModify = false; // The action fields are actually not used anymore in the ledger view logic // so we might as well skip this whole thing here! for (it_s = splits.begin(); needModify == false && it_s != splits.end(); ++it_s) { auto acc = file->account((*it_s).accountId()); MyMoneyMoney val = (*it_s).value(); if (acc.accountType() == Account::Type::CreditCard) { if (val < 0 && (*it_s).action() != MyMoneySplit::ActionWithdrawal && (*it_s).action() != MyMoneySplit::ActionTransfer) needModify = true; if (val >= 0 && (*it_s).action() != MyMoneySplit::ActionDeposit && (*it_s).action() != MyMoneySplit::ActionTransfer) needModify = true; } } // (Ace) Extended the #endif down to cover this conditional, because as-written // it will ALWAYS be skipped. if (needModify == true) { for (it_s = splits.begin(); it_s != splits.end(); ++it_s) { (*it_s).setAction(defaultAction); transaction.modifySplit(*it_s); file->modifyTransaction(transaction); } splits = transaction.splits(); // update local copy qDebug("Fixed credit card assignment in %s", transaction.id().data()); } #endif // Check for correct assignment of ActionInterest in all splits // and check if there are any duplicates in this transactions for (auto& split : splits) { MyMoneyAccount splitAccount = file->account(split.accountId()); if (!accounts.contains(split.accountId())) { accounts << split.accountId(); } // if this split references an interest account, the action // must be of type ActionInterest if (interestAccounts.contains(split.accountId())) { if (split.action() != MyMoneySplit::ActionInterest) { qDebug() << Q_FUNC_INFO << " " << transaction.id() << " contains an interest account (" << split.accountId() << ") but does not have ActionInterest"; split.setAction(MyMoneySplit::ActionInterest); transaction.modifySplit(split); file->modifyTransaction(transaction); qDebug("Fixed interest action in %s", qPrintable(transaction.id())); } // if it does not reference an interest account, it must not be // of type ActionInterest } else { if (split.action() == MyMoneySplit::ActionInterest) { qDebug() << Q_FUNC_INFO << " " << transaction.id() << " does not contain an interest account so it should not have ActionInterest"; split.setAction(defaultAction); transaction.modifySplit(split); file->modifyTransaction(transaction); qDebug("Fixed interest action in %s", qPrintable(transaction.id())); } } // check that for splits referencing an account that has // the same currency as the transactions commodity the value // and shares field are the same. if (transaction.commodity() == splitAccount.currencyId() && split.value() != split.shares()) { qDebug() << Q_FUNC_INFO << " " << transaction.id() << " " << split.id() << " uses the transaction currency, but shares != value"; split.setShares(split.value()); transaction.modifySplit(split); file->modifyTransaction(transaction); } // fix the shares and values to have the correct fraction if (!splitAccount.isInvest()) { try { int fract = splitAccount.fraction(); if (split.shares() != split.shares().convert(fract)) { qDebug("adjusting fraction in %s,%s", qPrintable(transaction.id()), qPrintable(split.id())); split.setShares(split.shares().convert(fract)); split.setValue(split.value().convert(fract)); transaction.modifySplit(split); file->modifyTransaction(transaction); } } catch (const MyMoneyException &) { qDebug("Missing security '%s', split not altered", qPrintable(splitAccount.currencyId())); } } } ++cnt; if (!(cnt % 10)) kmymoney->slotStatusProgressBar(cnt); } kmymoney->slotStatusProgressBar(-1, -1); } void KMyMoneyView::fixDuplicateAccounts_0(MyMoneyTransaction& t) { qDebug("Duplicate account in transaction %s", qPrintable(t.id())); } void KMyMoneyView::slotPrintView() { if (viewFrames[View::Reports] == currentPage()) m_reportsView->slotPrintView(); else if (viewFrames[View::Home] == currentPage()) m_homeView->slotPrintView(); } void KMyMoneyView::resetViewSelection(const View) { emit aboutToChangeView(); } void KMyMoneyView::connectView(const View view) { KMyMoneyAccountTreeView *treeView; switch (view) { case View::Accounts: disconnect(m_accountsView, &KAccountsView::aboutToShow, this, &KMyMoneyView::connectView); treeView = m_accountsView->getTreeView(); connect(treeView, &KMyMoneyAccountTreeView::selectObject, kmymoney, &KMyMoneyApp::slotSelectAccount); connect(treeView, &KMyMoneyAccountTreeView::selectObject, kmymoney, &KMyMoneyApp::slotSelectInstitution); connect(treeView, &KMyMoneyAccountTreeView::selectObject, kmymoney, &KMyMoneyApp::slotSelectInvestment); connect(treeView, &KMyMoneyAccountTreeView::openObject, kmymoney, &KMyMoneyApp::slotAccountOpen); connect(treeView, &KMyMoneyAccountTreeView::openContextMenu, kmymoney, &KMyMoneyApp::slotShowAccountContextMenu); connect(treeView, &KMyMoneyAccountTreeView::columnToggled , this, &KMyMoneyView::slotAccountTreeViewChanged); connect(Models::instance()->accountsModel(), &AccountsModel::netWorthChanged, m_accountsView, &KAccountsView::slotNetWorthChanged); connect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, m_accountsView, &KAccountsView::refresh); break; case View::Institutions: disconnect(m_institutionsView, &KInstitutionsView::aboutToShow, this, &KMyMoneyView::connectView); treeView = m_institutionsView->getTreeView(); connect(treeView, &KMyMoneyAccountTreeView::selectObject, kmymoney, &KMyMoneyApp::slotSelectAccount); connect(treeView, &KMyMoneyAccountTreeView::selectObject, kmymoney, &KMyMoneyApp::slotSelectInstitution); connect(treeView, &KMyMoneyAccountTreeView::openObject, kmymoney, &KMyMoneyApp::slotAccountOpen); connect(treeView, &KMyMoneyAccountTreeView::openObject, kmymoney, &KMyMoneyApp::slotInstitutionEdit); connect(treeView, &KMyMoneyAccountTreeView::openContextMenu, kmymoney, &KMyMoneyApp::slotShowAccountContextMenu); connect(treeView, &KMyMoneyAccountTreeView::openContextMenu, kmymoney, &KMyMoneyApp::slotShowInstitutionContextMenu); connect(treeView, &KMyMoneyAccountTreeView::columnToggled , this, &KMyMoneyView::slotAccountTreeViewChanged); connect(Models::instance()->institutionsModel(), &AccountsModel::netWorthChanged, m_institutionsView, &KInstitutionsView::slotNetWorthChanged); connect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, m_institutionsView, &KInstitutionsView::refresh); break; case View::Categories: disconnect(m_categoriesView, &KCategoriesView::aboutToShow, this, &KMyMoneyView::connectView); treeView = m_categoriesView->getTreeView(); connect(treeView, &KMyMoneyAccountTreeView::selectObject, kmymoney, &KMyMoneyApp::slotSelectAccount); connect(treeView, &KMyMoneyAccountTreeView::openObject, kmymoney, &KMyMoneyApp::slotAccountOpen); connect(treeView, &KMyMoneyAccountTreeView::openContextMenu, kmymoney, &KMyMoneyApp::slotShowAccountContextMenu); connect(treeView, &KMyMoneyAccountTreeView::columnToggled , this, &KMyMoneyView::slotAccountTreeViewChanged); connect(Models::instance()->institutionsModel(), &AccountsModel::profitChanged, m_categoriesView, &KCategoriesView::slotProfitChanged); connect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, m_categoriesView, &KCategoriesView::refresh); break; case View::Tags: disconnect(m_tagsView, &KTagsView::aboutToShow, this, &KMyMoneyView::connectView); connect(kmymoney, &KMyMoneyApp::tagCreated, m_tagsView, static_cast(&KTagsView::slotSelectTagAndTransaction)); connect(kmymoney, &KMyMoneyApp::slotTagRename, m_tagsView, &KTagsView::slotRenameButtonCliked); connect(m_tagsView, &KTagsView::tagNewClicked, kmymoney, static_cast(&KMyMoneyApp::slotTagNew)); connect(m_tagsView, &KTagsView::tagDeleteClicked, kmymoney, static_cast(&KMyMoneyApp::slotTagDelete)); connect(m_tagsView, &KTagsView::openContextMenu, kmymoney, &KMyMoneyApp::slotShowTagContextMenu); connect(m_tagsView, &KTagsView::selectObjects, kmymoney, &KMyMoneyApp::slotSelectTags); connect(m_tagsView, &KTagsView::transactionSelected, this, &KMyMoneyView::slotLedgerSelected); connect(m_tagsView, &KMyMoneyViewBase::aboutToShow, this, &KMyMoneyView::aboutToChangeView); break; case View::Budget: disconnect(m_budgetView, &KBudgetView::aboutToShow, this, &KMyMoneyView::connectView); treeView = m_budgetView->getTreeView(); connect(treeView, &KMyMoneyAccountTreeView::selectObject, kmymoney, &KMyMoneyApp::slotSelectAccount); connect(treeView, &KMyMoneyAccountTreeView::selectObject, kmymoney, &KMyMoneyApp::slotSelectInstitution); connect(treeView, &KMyMoneyAccountTreeView::selectObject, kmymoney, &KMyMoneyApp::slotSelectInvestment); connect(treeView, &KMyMoneyAccountTreeView::openObject, kmymoney, &KMyMoneyApp::slotAccountOpen); connect(treeView, &KMyMoneyAccountTreeView::openContextMenu, kmymoney, &KMyMoneyApp::slotShowAccountContextMenu); connect(m_budgetView, &KBudgetView::openContextMenu, kmymoney, &KMyMoneyApp::slotShowBudgetContextMenu); connect(m_budgetView, &KBudgetView::selectObjects, kmymoney, &KMyMoneyApp::slotSelectBudget); connect(kmymoney, &KMyMoneyApp::budgetRename, m_budgetView, &KBudgetView::slotStartRename); connect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, m_budgetView, &KBudgetView::refresh); connect(treeView, &KMyMoneyAccountTreeView::columnToggled , this, &KMyMoneyView::slotAccountTreeViewChanged); break; default: break; } }