diff --git a/kmymoney/CMakeLists.txt b/kmymoney/CMakeLists.txt index 8aac6d132..e4cd0e4e4 100644 --- a/kmymoney/CMakeLists.txt +++ b/kmymoney/CMakeLists.txt @@ -1,196 +1,193 @@ include(ECMAddAppIcon) include_directories(${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/settings/ ${CMAKE_CURRENT_BINARY_DIR}/settings/ ${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_SOURCE_DIR}/menus/ ${CMAKE_CURRENT_BINARY_DIR}/menus/ ${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/ ) add_subdirectory( mymoney ) add_subdirectory( settings ) 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 ) add_subdirectory( menus ) if(BUILD_TESTING) add_subdirectory( tests ) endif() set( _HEADERS kmymoneyutils.h ) ########### 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 KF5::KIOCore KF5::KIOFileWidgets KF5::KIOWidgets KF5::KIONTLM Alkimia::alkimia kmm_mymoney kmm_utils_webconnect kmm_utils_platformtools PRIVATE KF5::I18n kmm_settings ) # must build kmymoney/transactionsortoption.h # from transactionsortoption.ui first add_dependencies(kmymoney_common generate_base_ui_srcs kmm_settings) add_dependencies(wizardpages widgets) add_dependencies(dialogs widgets) if(USE_MODELTEST) set( kmymoney_common_LIBS ${kmymoney_common_LIBS} ${QT_QTTEST_LIBRARY}) endif(USE_MODELTEST) # remove these generated files, they are in the way and leftovers from 5.0 if(EXISTS ${CMAKE_CURRENT_BINARY_DIR}/kmymoneysettings.h) file(REMOVE ${CMAKE_CURRENT_BINARY_DIR}/kmymoneysettings.h) endif() if(EXISTS ${CMAKE_CURRENT_BINARY_DIR}/kmymoneysettings.cpp) file(REMOVE ${CMAKE_CURRENT_BINARY_DIR}/kmymoneysettings.cpp) endif() ########### 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 kmm_models kmm_settings kmm_menus kmm_widgets kmm_mymoney interfaces kmm_plugin Qt5::Core KF5::Archive KF5::ConfigGui KF5::WidgetsAddons KF5::KIOCore KF5::CoreAddons KChart $<$:Qt5::Test> $<$:KF5::Holidays> $<$:KF5::Activities> ) # 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 ${_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/tests/converter-test.cpp b/kmymoney/converter/tests/converter-test.cpp index 9fae5381e..273c58fee 100644 --- a/kmymoney/converter/tests/converter-test.cpp +++ b/kmymoney/converter/tests/converter-test.cpp @@ -1,202 +1,202 @@ /*************************************************************************** convertertest.cpp ------------------- copyright : (C) 2002 by Thomas Baumgart email : ipwizard@users.sourceforge.net Ace Jones ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "converter-test.h" #include #include // uses helper functions from reports tests -#include "reportstestcommon.h" +#include "views/reports/core/tests/reportstestcommon.h" using namespace test; #include "mymoneyinstitution.h" #include "mymoneysecurity.h" #include "mymoneyprice.h" #include "mymoneyreport.h" #include "mymoneypayee.h" #include "mymoneystatement.h" #include "mymoneyexception.h" #include "storage/mymoneystoragedump.h" #include "webpricequote.h" QTEST_GUILESS_MAIN(ConverterTest) using namespace convertertest; void ConverterTest::init() { storage = new MyMoneyStorageMgr; file = MyMoneyFile::instance(); file->attachStorage(storage); MyMoneyFileTransaction ft; file->addCurrency(MyMoneySecurity("CAD", "Canadian Dollar", "C$")); file->addCurrency(MyMoneySecurity("USD", "US Dollar", "$")); file->addCurrency(MyMoneySecurity("JPY", "Japanese Yen", QChar(0x00A5), 1)); file->addCurrency(MyMoneySecurity("GBP", "British Pound", "#")); file->setBaseCurrency(file->currency("USD")); MyMoneyPayee payeeTest; payeeTest.setName("Test Payee"); file->addPayee(payeeTest); MyMoneyPayee payeeTest2; payeeTest2.setName("Thomas Baumgart"); file->addPayee(payeeTest2); acAsset = (MyMoneyFile::instance()->asset().id()); acLiability = (MyMoneyFile::instance()->liability().id()); acExpense = (MyMoneyFile::instance()->expense().id()); acIncome = (MyMoneyFile::instance()->income().id()); acChecking = makeAccount("Checking Account", eMyMoney::Account::Type::Checkings, moConverterCheckingOpen, QDate(2004, 5, 15), acAsset); acCredit = makeAccount("Credit Card", eMyMoney::Account::Type::CreditCard, moConverterCreditOpen, QDate(2004, 7, 15), acLiability); acSolo = makeAccount("Solo", eMyMoney::Account::Type::Expense, MyMoneyMoney(), QDate(2004, 1, 11), acExpense); acParent = makeAccount("Parent", eMyMoney::Account::Type::Expense, MyMoneyMoney(), QDate(2004, 1, 11), acExpense); acChild = makeAccount("Child", eMyMoney::Account::Type::Expense, MyMoneyMoney(), QDate(2004, 2, 11), acParent); acForeign = makeAccount("Foreign", eMyMoney::Account::Type::Expense, MyMoneyMoney(), QDate(2004, 1, 11), acExpense); MyMoneyInstitution i("Bank of the World", "", "", "", "", "", ""); file->addInstitution(i); inBank = i.id(); ft.commit(); } void ConverterTest::cleanup() { file->detachStorage(storage); delete storage; } void ConverterTest::testWebQuotes_data() { QTest::addColumn("symbol"); QTest::addColumn("testname"); QTest::addColumn("source"); QTest::newRow("Yahoo UK") << "VOD.L" << "test Yahoo UK" << "Yahoo UK"; QTest::newRow("Yahoo Currency") << "EUR > USD" << "test Yahoo Currency" << "Yahoo Currency"; QTest::newRow("Financial Express") << "0585239" << "test Financial Express" << "Financial Express"; QTest::newRow("Yahoo France") << "EAD.PA" << "test Yahoo France" << "Yahoo France"; QTest::newRow("Globe & Mail") << "50492" << "test Globe-Mail" << "Globe & Mail"; QTest::newRow("MSN Canada") << "TDB647" << "test MSN.CA" << "MSN.CA"; // QTest::newRow("Finanztreff") << "BASF.SE" << "test Finanztreff" << "Finanztreff"; // QTest::newRow("boerseonline") << "symbol" << "test boerseonline" << "boerseonline"; // QTest::newRow("Wallstreet-Online.DE (Default)") << "symbol" << "test Wallstreet-Online.DE (Default)" << "Wallstreet-Online.DE (Default)"; // QTest::newRow("Financial Times UK") << "DZGEAE" << "test Financial Times UK Funds" << "Financial Times UK Funds"); QTest::newRow("Yahoo Canada") << "UTS.TO" << "test Yahoo Canada" << "Yahoo Canada"; // QTest::newRow("Wallstreed-Online.DE (Hamburg)") << "TDB647" << "test Wallstreet-Online.DE (Hamburg)" << "Wallstreet-Online.DE (Hamburg)"; // QTest::newRow("Gielda Papierow Wartosciowych (GPW)") << "TDB647" << "test Gielda Papierow Wartosciowych (GPW)" << "Gielda Papierow Wartosciowych (GPW)"; // QTest::newRow("OMX Baltic") << "TDB647" << "test OMX Baltic funds" << "OMX Baltic funds"; QTest::newRow("Finance::Quote usa") << "DIS" << "test F::Q usa" << "Finance::Quote usa"; //UNTESTED: Other F::Q sources, local files, user custom sources } void ConverterTest::testWebQuotesDefault() { #ifdef PERFORM_ONLINE_TESTS try { WebPriceQuote q; QuoteReceiver qr(&q); q.launch("DIS", "test default"); // qDebug() << "ConverterTest::testWebQuotes(): quote for " << q.m_symbol << " on " << qr.m_date.toString() << " is " << qr.m_price.toString() << " errors(" << qr.m_errors.count() << "): " << qr.m_errors.join(" /// "); // No errors allowed QVERIFY(qr.m_errors.count() == 0); // Quote date should be within the last week, or something bad is going on. QVERIFY(qr.m_date <= QDate::currentDate()); QVERIFY(qr.m_date >= QDate::currentDate().addDays(-7)); // Quote value should at least be positive QVERIFY(qr.m_price.isPositive()); } catch (const MyMoneyException &e) { QFAIL(e.what()); } #endif } void ConverterTest::testWebQuotes() { #ifdef PERFORM_ONLINE_TESTS try { WebPriceQuote q; QuoteReceiver qr(&q); QFETCH(QString, symbol); QFETCH(QString, testname); QFETCH(QString, source); q.launch(symbol, testname, source); QVERIFY(qr.m_errors.count() == 0); QVERIFY(qr.m_date <= QDate::currentDate().addDays(1)); QVERIFY(qr.m_date >= QDate::currentDate().addDays(-7)); QVERIFY(qr.m_price.isPositive()); } catch (const MyMoneyException &e) { QFAIL(e.what()); } #endif } void ConverterTest::testDateFormat() { try { MyMoneyDateFormat format("%mm-%dd-%yyyy"); QVERIFY(format.convertString("1-5-2005") == QDate(2005, 1, 5)); QVERIFY(format.convertString("jan-15-2005") == QDate(2005, 1, 15)); QVERIFY(format.convertString("august-25-2005") == QDate(2005, 8, 25)); format = MyMoneyDateFormat("%mm/%dd/%yy"); QVERIFY(format.convertString("1/5/05") == QDate(2005, 1, 5)); QVERIFY(format.convertString("jan/15/05") == QDate(2005, 1, 15)); QVERIFY(format.convertString("august/25/05") == QDate(2005, 8, 25)); format = MyMoneyDateFormat("%d\\.%m\\.%yy"); QVERIFY(format.convertString("1.5.05") == QDate(2005, 5, 1)); QVERIFY(format.convertString("15.jan.05") == QDate(2005, 1, 15)); QVERIFY(format.convertString("25.august.05") == QDate(2005, 8, 25)); format = MyMoneyDateFormat("%yyyy\\\\%dddd\\\\%mmmmmmmmmmm"); QVERIFY(format.convertString("2005\\31\\12") == QDate(2005, 12, 31)); QVERIFY(format.convertString("2005\\15\\jan") == QDate(2005, 1, 15)); QVERIFY(format.convertString("2005\\25\\august") == QDate(2005, 8, 25)); format = MyMoneyDateFormat("%m %dd, %yyyy"); QVERIFY(format.convertString("jan 15, 2005") == QDate(2005, 1, 15)); QVERIFY(format.convertString("august 25, 2005") == QDate(2005, 8, 25)); QVERIFY(format.convertString("january 1st, 2005") == QDate(2005, 1, 1)); format = MyMoneyDateFormat("%m %d %y"); QVERIFY(format.convertString("12/31/50", false, 2000) == QDate(1950, 12, 31)); QVERIFY(format.convertString("1/1/90", false, 2000) == QDate(1990, 1, 1)); QVERIFY(format.convertString("december 31st, 5", false) == QDate(2005, 12, 31)); } catch (const MyMoneyException &e) { QFAIL(e.what()); } } diff --git a/kmymoney/dialogs/CMakeLists.txt b/kmymoney/dialogs/CMakeLists.txt index fb03728a9..90140617b 100644 --- a/kmymoney/dialogs/CMakeLists.txt +++ b/kmymoney/dialogs/CMakeLists.txt @@ -1,105 +1,104 @@ add_subdirectory( settings ) ########### next target ############### set(libdialogs_a_SOURCES splitadjustdialog.cpp investactivities.cpp investtransactioneditor.cpp kaccountselectdlg.cpp kbackupdlg.cpp - kbalancechartdlg.cpp kbalancewarning.cpp kcategoryreassigndlg.cpp kchooseimportexportdlg.cpp kconfirmmanualenterdlg.cpp kcurrencycalculator.cpp kcurrencyeditdlg.cpp kavailablecurrencydlg.cpp kcurrencyeditordlg.cpp keditscheduledlg.cpp kenterscheduledlg.cpp kequitypriceupdatedlg.cpp kequitypriceupdateconfdlg.cpp kfindtransactiondlg.cpp kloadtemplatedlg.cpp kmergetransactionsdlg.cpp kmymoneyfileinfodlg.cpp kmymoneypricedlg.cpp kmymoneysplittable.cpp knewaccountdlg.cpp hierarchyfilterproxymodel.cpp knewbankdlg.cpp knewequityentrydlg.cpp editpersonaldatadlg.cpp kpayeereassigndlg.cpp ktagreassigndlg.cpp kselecttransactionsdlg.cpp ksplittransactiondlg.cpp ktemplateexportdlg.cpp kupdatestockpricedlg.cpp transactioneditor.cpp stdtransactioneditor.cpp transactionmatcher.cpp ksaveasquestion.cpp ) set(dialogs_HEADERS splitadjustdialog.h investtransactioneditor.h kcurrencycalculator.h transactioneditor.h stdtransactioneditor.h ) set(dialogs_UI splitadjustdialog.ui kaccountselectdlg.ui kbackupdlg.ui kcategoryreassigndlg.ui kchooseimportexportdlg.ui kconfirmmanualenterdlg.ui kcurrencycalculator.ui kcurrencyeditdlg.ui kavailablecurrencydlg.ui kcurrencyeditordlg.ui keditscheduledlg.ui kenterscheduledlg.ui kequitypriceupdatedlg.ui kequitypriceupdateconfdlg.ui kfindtransactiondlg.ui kloadtemplatedlg.ui kmymoneyfileinfodlg.ui kmymoneypricedlg.ui knewaccountdlg.ui knewbankdlg.ui knewequityentrydlg.ui editpersonaldatadlg.ui kpayeereassigndlg.ui ktagreassigndlg.ui kselecttransactionsdlg.ui ksortoptiondlg.ui ksplitcorrectiondlg.ui ksplittransactiondlg.ui ktemplateexportdlg.ui kupdatestockpricedlg.ui ksaveasquestion.ui ) ki18n_wrap_ui(libdialogs_a_SOURCES ${dialogs_UI} ) add_library(dialogs STATIC ${libdialogs_a_SOURCES}) target_link_libraries(dialogs PUBLIC KChart KF5::ItemViews KF5::I18n KF5::TextWidgets KF5::Completion Qt5::Widgets Alkimia::alkimia kmm_mymoney onlinetask_interfaces kmm_widgets kmm_utils_platformtools PRIVATE KF5::KIOGui ) target_link_libraries(dialogs LINK_PUBLIC kmm_widgets kmm_mymoney onlinetask_interfaces ) ########### install files ############### install(FILES ${dialogs_HEADERS} DESTINATION ${INCLUDE_INSTALL_DIR}/kmymoney COMPONENT Devel) diff --git a/kmymoney/kmymoney.cpp b/kmymoney/kmymoney.cpp index 2635de34c..936c6c270 100644 --- a/kmymoney/kmymoney.cpp +++ b/kmymoney/kmymoney.cpp @@ -1,3678 +1,3678 @@ /*************************************************************************** kmymoney.cpp ------------------- copyright : (C) 2000 by Michael Edwardes (C) 2007 by Thomas Baumgart (C) 2017, 2018 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 #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 #include #include #ifdef KF5Holidays_FOUND #include #include #endif #ifdef KF5Activities_FOUND #include #endif // ---------------------------------------------------------------------------- // Project Includes #include "kmymoneysettings.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/knewbankdlg.h" #include "dialogs/ksaveasquestion.h" #include "wizards/newinvestmentwizard/knewinvestmentwizard.h" #include "dialogs/knewaccountdlg.h" #include "dialogs/editpersonaldatadlg.h" #include "dialogs/kcurrencycalculator.h" #include "dialogs/keditscheduledlg.h" #include "wizards/newloanwizard/keditloanwizard.h" #include "dialogs/kpayeereassigndlg.h" #include "dialogs/kcategoryreassigndlg.h" #include "wizards/endingbalancedlg/kendingbalancedlg.h" -#include "dialogs/kbalancechartdlg.h" #include "dialogs/kloadtemplatedlg.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/amountedit.h" #include "widgets/kmymoneyedit.h" #include "widgets/kmymoneymvccombo.h" #include "views/kmymoneyview.h" #include "models/models.h" #include "models/accountsmodel.h" #include "models/equitiesmodel.h" #include "models/securitiesmodel.h" #ifdef ENABLE_UNFINISHEDFEATURES #include "models/ledgermodel.h" #endif #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/mymoneyprice.h" #include "mymoney/mymoneytag.h" #include "mymoney/mymoneybudget.h" #include "mymoney/mymoneyreport.h" #include "mymoney/mymoneysplit.h" #include "mymoney/mymoneyutils.h" #include "mymoney/mymoneystatement.h" #include "mymoney/mymoneyforecast.h" #include "mymoney/mymoneytransactionfilter.h" #include "converter/mymoneystatementreader.h" #include "converter/mymoneytemplate.h" #include "plugins/interfaces/kmmappinterface.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 "kmymoneyplugin.h" #include "tasks/credittransfer.h" #include "icons/icons.h" #include "misc/webconnect.h" #include "storage/mymoneystoragemgr.h" #include "imymoneystorageformat.h" #include "transactioneditor.h" #include #include #include "kmymoneyutils.h" #include "kcreditswindow.h" #include "ledgerdelegate.h" #include "storageenums.h" #include "mymoneyenums.h" #include "dialogenums.h" #include "viewenums.h" #include "menuenums.h" #include "kmymoneyenums.h" #include "misc/platformtools.h" #ifdef KMM_DEBUG #include "mymoney/storage/mymoneystoragedump.h" #include "mymoneytracer.h" #endif using namespace Icons; using namespace eMenu; enum backupStateE { BACKUP_IDLE = 0, BACKUP_MOUNTING, BACKUP_COPYING, BACKUP_UNMOUNTING }; class KMyMoneyApp::Private { public: Private(KMyMoneyApp *app) : q(app), m_backupState(backupStateE::BACKUP_IDLE), m_backupResult(0), m_backupMount(0), m_ignoreBackupExitCode(false), m_myMoneyView(nullptr), m_startDialog(false), m_progressBar(nullptr), m_statusLabel(nullptr), m_autoSaveEnabled(true), m_autoSaveTimer(nullptr), m_progressTimer(nullptr), m_autoSavePeriod(0), m_inAutoSaving(false), m_recentFiles(nullptr), #ifdef KF5Holidays_FOUND m_holidayRegion(nullptr), #endif #ifdef KF5Activities_FOUND m_activityResourceInstance(nullptr), #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 unlinkStatementXML(); void moveInvestmentTransaction(const QString& fromId, const QString& toId, const MyMoneyTransaction& t); QList > automaticReconciliation(const MyMoneyAccount &account, const QList > &transactions, const MyMoneyMoney &amount); struct storageInfo { eKMyMoney::StorageType type {eKMyMoney::StorageType::None}; bool isOpened {false}; QUrl url; }; storageInfo m_storageInfo; /** * The public interface. */ KMyMoneyApp * const q; /** the configuration object of the application */ KSharedConfigPtr m_config; /** * 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; bool m_startDialog; QString m_mountpoint; QProgressBar* m_progressBar; QTime m_lastUpdate; QLabel* m_statusLabel; // allows multiple imports to be launched trough web connect and to be executed sequentially QQueue m_importUrlsQueue; // This is Auto Saving related bool m_autoSaveEnabled; QTimer* m_autoSaveTimer; QTimer* m_progressTimer; int m_autoSavePeriod; bool m_inAutoSaving; // id's that need to be remembered QString m_accountGoto, m_payeeGoto; KRecentFilesAction* m_recentFiles; #ifdef KF5Holidays_FOUND // used by the calendar interface for schedules KHolidays::HolidayRegion* m_holidayRegion; #endif #ifdef KF5Activities_FOUND KActivities::ResourceInstance * m_activityResourceInstance; #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(); void 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); } } /** * This method updates names of currencies from file to localized names */ void 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", e.what()); } } void updateAccountNames() { // make sure we setup the name of the base accounts in translated form try { MyMoneyFileTransaction ft; const auto 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 &) { } } void ungetString(QIODevice *qfile, char *buf, int len) { buf = &buf[len-1]; while (len--) { qfile->ungetChar(*buf--); } } bool applyFileFixes() { const auto blocked = MyMoneyFile::instance()->blockSignals(true); KSharedConfigPtr config = KSharedConfig::openConfig(); KConfigGroup grp = config->group("General Options"); // 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 auto 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(QString::fromLatin1("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); return true; } void connectStorageToModels() { const auto file = MyMoneyFile::instance(); const auto accountsModel = Models::instance()->accountsModel(); q->connect(file, &MyMoneyFile::objectAdded, accountsModel, &AccountsModel::slotObjectAdded); q->connect(file, &MyMoneyFile::objectModified, accountsModel, &AccountsModel::slotObjectModified); q->connect(file, &MyMoneyFile::objectRemoved, accountsModel, &AccountsModel::slotObjectRemoved); q->connect(file, &MyMoneyFile::balanceChanged, accountsModel, &AccountsModel::slotBalanceOrValueChanged); q->connect(file, &MyMoneyFile::valueChanged, accountsModel, &AccountsModel::slotBalanceOrValueChanged); const auto institutionsModel = Models::instance()->institutionsModel(); q->connect(file, &MyMoneyFile::objectAdded, institutionsModel, &InstitutionsModel::slotObjectAdded); q->connect(file, &MyMoneyFile::objectModified, institutionsModel, &InstitutionsModel::slotObjectModified); q->connect(file, &MyMoneyFile::objectRemoved, institutionsModel, &InstitutionsModel::slotObjectRemoved); q->connect(file, &MyMoneyFile::balanceChanged, institutionsModel, &AccountsModel::slotBalanceOrValueChanged); q->connect(file, &MyMoneyFile::valueChanged, institutionsModel, &AccountsModel::slotBalanceOrValueChanged); const auto equitiesModel = Models::instance()->equitiesModel(); q->connect(file, &MyMoneyFile::objectAdded, equitiesModel, &EquitiesModel::slotObjectAdded); q->connect(file, &MyMoneyFile::objectModified, equitiesModel, &EquitiesModel::slotObjectModified); q->connect(file, &MyMoneyFile::objectRemoved, equitiesModel, &EquitiesModel::slotObjectRemoved); q->connect(file, &MyMoneyFile::balanceChanged, equitiesModel, &EquitiesModel::slotBalanceOrValueChanged); q->connect(file, &MyMoneyFile::valueChanged, equitiesModel, &EquitiesModel::slotBalanceOrValueChanged); const auto securitiesModel = Models::instance()->securitiesModel(); q->connect(file, &MyMoneyFile::objectAdded, securitiesModel, &SecuritiesModel::slotObjectAdded); q->connect(file, &MyMoneyFile::objectModified, securitiesModel, &SecuritiesModel::slotObjectModified); q->connect(file, &MyMoneyFile::objectRemoved, securitiesModel, &SecuritiesModel::slotObjectRemoved); #ifdef ENABLE_UNFINISHEDFEATURES const auto ledgerModel = Models::instance()->ledgerModel(); q->connect(file, &MyMoneyFile::objectAdded, ledgerModel, &LedgerModel::slotAddTransaction); q->connect(file, &MyMoneyFile::objectModified, ledgerModel, &LedgerModel::slotModifyTransaction); q->connect(file, &MyMoneyFile::objectRemoved, ledgerModel, &LedgerModel::slotRemoveTransaction); q->connect(file, &MyMoneyFile::objectAdded, ledgerModel, &LedgerModel::slotAddSchedule); q->connect(file, &MyMoneyFile::objectModified, ledgerModel, &LedgerModel::slotModifySchedule); q->connect(file, &MyMoneyFile::objectRemoved, ledgerModel, &LedgerModel::slotRemoveSchedule); #endif } void disconnectStorageFromModels() { const auto file = MyMoneyFile::instance(); q->disconnect(file, nullptr, Models::instance()->accountsModel(), nullptr); q->disconnect(file, nullptr, Models::instance()->institutionsModel(), nullptr); q->disconnect(file, nullptr, Models::instance()->equitiesModel(), nullptr); q->disconnect(file, nullptr, Models::instance()->securitiesModel(), nullptr); #ifdef ENABLE_UNFINISHEDFEATURES q->disconnect(file, nullptr, Models::instance()->ledgerModel(), nullptr); #endif } bool askAboutSaving() { const auto isFileNotSaved = q->actionCollection()->action(QString::fromLatin1(KStandardAction::name(KStandardAction::Save)))->isEnabled(); const auto isNewFileNotSaved = m_storageInfo.isOpened && m_storageInfo.url.isEmpty(); auto fileNeedsToBeSaved = false; if (isFileNotSaved && KMyMoneySettings::autoSaveOnClose()) { fileNeedsToBeSaved = true; } else if (isFileNotSaved || isNewFileNotSaved) { switch (KMessageBox::warningYesNoCancel(q, i18n("The file has been changed, save it?"))) { case KMessageBox::ButtonCode::Yes: fileNeedsToBeSaved = true; break; case KMessageBox::ButtonCode::No: fileNeedsToBeSaved = false; break; case KMessageBox::ButtonCode::Cancel: default: return false; break; } } if (fileNeedsToBeSaved) { if (isFileNotSaved) return q->slotFileSave(); else if (isNewFileNotSaved) return q->slotFileSaveAs(); } return true; } /** * This method attaches an empty storage object to the MyMoneyFile * object. It calls removeStorage() to remove a possibly attached * storage object. */ void newStorage() { removeStorage(); auto file = MyMoneyFile::instance(); file->attachStorage(new MyMoneyStorageMgr); } /** * This method removes an attached storage from the MyMoneyFile * object. */ void removeStorage() { auto file = MyMoneyFile::instance(); auto p = file->storage(); if (p) { file->detachStorage(p); delete p; } } /** * if no base currency is defined, start the dialog and force it to be set */ void 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", e.what()); } if (baseId.isEmpty()) { QPointer dlg = new KCurrencyEditDlg(q); // 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", 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 &e) { qDebug() << QLatin1String("Account") << (*it).id() << (*it).name() << e.what(); } 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()), e.what()); } } } } } /** * Call this to see if the MyMoneyFile contains any unsaved data. * * @retval true if any data has been modified but not saved * @retval false otherwise */ bool dirty() { if (!m_storageInfo.isOpened) return false; return MyMoneyFile::instance()->dirty(); } /* 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 fixFile_3() { // make sure each storage object contains a (unique) id MyMoneyFile::instance()->storageId(); } void 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 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 (!KMyMoneySettings::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() == eMyMoney::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 (!KMyMoneySettings::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 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() == eMyMoney::Account::Type::Loan || (*it_a).accountType() == eMyMoney::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() == eMyMoney::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 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", e.what()); } } void 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(q, 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_CSTRING("Fix LoanAccount0 not supported anymore"); } } void 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")); q->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::actionName(eMyMoney::Split::Action::Interest)) { if (interestAccounts.contains(split.accountId()) == 0) { interestAccounts << split.accountId(); } } } if (hasDuplicateAccounts) { fixDuplicateAccounts_0(t); } ++cnt; if (!(cnt % 10)) q->slotStatusProgressBar(cnt); } // scan the transactions and modify loan transactions for (auto& transaction : transactionList) { QString defaultAction; 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() == eMyMoney::Account::Type::Asset || acc.accountGroup() == eMyMoney::Account::Type::Liability) { val = split.value(); accountCount++; if (acc.accountType() == eMyMoney::Account::Type::Loan || acc.accountType() == eMyMoney::Account::Type::AssetLoan) isLoan = true; } else break; } if (accountCount == 2) { if (isLoan) defaultAction = MyMoneySplit::actionName(eMyMoney::Split::Action::Amortization); else defaultAction = MyMoneySplit::actionName(eMyMoney::Split::Action::Transfer); } else { if (val.isNegative()) defaultAction = MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal); else defaultAction = MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit); } } isLoan = false; foreach (const auto split, splits) { auto acc = file->account(split.accountId()); MyMoneyMoney val = split.value(); if (acc.accountGroup() == eMyMoney::Account::Type::Asset || acc.accountGroup() == eMyMoney::Account::Type::Liability) { if (!val.isPositive()) { defaultAction = MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal); break; } else { defaultAction = MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit); 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::actionName(eMyMoney::Split::Action::Withdrawal) && (*it_s).action() != MyMoneySplit::actionName(eMyMoney::Split::Action::Transfer)) needModify = true; if (val >= 0 && (*it_s).action() != MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit) && (*it_s).action() != MyMoneySplit::actionName(eMyMoney::Split::Action::Transfer)) 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::actionName(eMyMoney::Split::Action::Interest)) { qDebug() << Q_FUNC_INFO << " " << transaction.id() << " contains an interest account (" << split.accountId() << ") but does not have ActionInterest"; split.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::Interest)); 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::actionName(eMyMoney::Split::Action::Interest)) { 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)) q->slotStatusProgressBar(cnt); } q->slotStatusProgressBar(-1, -1); } void fixDuplicateAccounts_0(MyMoneyTransaction& t) { qDebug("Duplicate account in transaction %s", qPrintable(t.id())); } /** * This method is used to update the caption of the application window. * It sets the caption to "filename [modified] - KMyMoney". * * @param skipActions if true, the actions will not be updated. This * is usually onyl required by some early calls when * these widgets are not yet created (the default is false). */ void updateCaption(); void updateActions(); bool canFileSaveAs() const; bool canUpdateAllAccounts() const; void fileAction(eKMyMoney::FileAction action); }; 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-" + QString::number(platformTools::processId()), 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(KMyMoneySettings::firstFiscalMonth(), KMyMoneySettings::firstFiscalDay()); 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); { #if defined(Q_OS_WIN) || defined(Q_OS_MACOS) QString themeName = QLatin1Literal("system"); // using QIcon::setThemeName on Craft build system causes icons to disappear #else QString themeName = KMyMoneySettings::iconsTheme(); // get theme user wants #endif 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(); pActions = initActions(); pMenus = initMenus(); d->m_myMoneyView = new KMyMoneyView; layout->addWidget(d->m_myMoneyView, 10); connect(d->m_myMoneyView, &KMyMoneyView::viewActivated, this, &KMyMoneyApp::slotViewSelected); connect(d->m_myMoneyView, &KMyMoneyView::statusMsg, this, &KMyMoneyApp::slotStatusMsg); connect(d->m_myMoneyView, &KMyMoneyView::statusProgress, this, &KMyMoneyApp::slotStatusProgressBar); // Initialize kactivities resource instance #ifdef KF5Activities_FOUND d->m_activityResourceInstance = new KActivities::ResourceInstance(window()->winId(), this); #endif const auto viewActions = d->m_myMoneyView->actionsToBeConnected(); actionCollection()->addActions(viewActions.values()); for (auto it = viewActions.cbegin(); it != viewActions.cend(); ++it) pActions.insert(it.key(), it.value()); /////////////////////////////////////////////////////////////////// // call inits to invoke all other construction parts readOptions(); // now initialize the plugin structure createInterfaces(); KMyMoneyPlugin::pluginHandling(KMyMoneyPlugin::Action::Load, pPlugins, this, guiFactory()); onlineJobAdministration::instance()->setOnlinePlugins(pPlugins.extended); d->m_myMoneyView->setOnlinePlugins(pPlugins.online); 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(pActions[Action::ViewTransactionDetail], &QAction::toggled, d->m_myMoneyView, &KMyMoneyView::slotShowTransactionDetail); d->m_backupState = BACKUP_IDLE; QLocale locale; for (auto const& weekDay: locale.weekdays()) { d->m_processingDays.setBit(static_cast(weekDay)); } 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())); // connect the WebConnect server connect(d->m_webConnect, SIGNAL(gotUrl(QUrl)), this, SLOT(webConnect(QUrl))); // setup the initial configuration slotUpdateConfiguration(QString()); // kickstart date change timer slotDateChanged(); d->fileAction(eKMyMoney::FileAction::Closed); } KMyMoneyApp::~KMyMoneyApp() { // don't keep track of selected view anymore as this might change by unloading plugins disconnect(d->m_myMoneyView, &KMyMoneyView::viewActivated, this, &KMyMoneyApp::slotViewSelected); // delete cached objects since they are in the way // when unloading the plugins onlineJobAdministration::instance()->clearCaches(); // we need to unload all plugins before we destroy anything else KMyMoneyPlugin::pluginHandling(KMyMoneyPlugin::Action::Unload, pPlugins, this, guiFactory()); d->removeStorage(); #ifdef KF5Holidays_FOUND delete d->m_holidayRegion; #endif #ifdef KF5Activities_FOUND delete d->m_activityResourceInstance; #endif // make sure all settings are written to disk KMyMoneySettings::self()->save(); delete d; } QUrl KMyMoneyApp::lastOpenedURL() { QUrl url = d->m_startDialog ? QUrl() : d->m_storageInfo.url; if (!url.isValid()) { url = QUrl::fromUserInput(readLastUsedFile()); } ready(); return url; } 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(); } } } QHash KMyMoneyApp::initMenus() { QHash lutMenus; const QHash menuNames { {Menu::Institution, QStringLiteral("institution_context_menu")}, {Menu::Account, QStringLiteral("account_context_menu")}, {Menu::Schedule, QStringLiteral("schedule_context_menu")}, {Menu::Category, QStringLiteral("category_context_menu")}, {Menu::Tag, QStringLiteral("tag_context_menu")}, {Menu::Payee, QStringLiteral("payee_context_menu")}, {Menu::Investment, QStringLiteral("investment_context_menu")}, {Menu::Transaction, QStringLiteral("transaction_context_menu")}, {Menu::MoveTransaction, QStringLiteral("transaction_move_menu")}, {Menu::MarkTransaction, QStringLiteral("transaction_mark_menu")}, {Menu::MarkTransactionContext, QStringLiteral("transaction_context_mark_menu")}, {Menu::OnlineJob, QStringLiteral("onlinejob_context_menu")} }; for (auto it = menuNames.cbegin(); it != menuNames.cend(); ++it) lutMenus.insert(it.key(), qobject_cast(factory()->container(it.value(), this))); return lutMenus; } QHash KMyMoneyApp::initActions() { auto aC = actionCollection(); /* Look-up table for all custom and standard actions. It's required for: 1) building QList with QActions to be added to ActionCollection 2) adding custom features to QActions like e.g. keyboard shortcut */ QHash lutActions; // ************* // 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); lutActions.insert(Action::Print, KStandardAction::print(this, &KMyMoneyApp::slotPrintView, aC)); KStandardAction::preferences(this, &KMyMoneyApp::slotSettings, aC); // ************* // Adding all actions // ************* { // struct for creating useless (unconnected) QAction struct actionInfo { Action action; QString name; QString text; Icon icon; }; const QVector actionInfos { // ************* // The File menu // ************* {Action::FileBackup, QStringLiteral("file_backup"), i18n("Backup..."), Icon::Empty}, {Action::FileImportStatement, QStringLiteral("file_import_statement"), i18n("Statement file..."), Icon::Empty}, {Action::FileImportTemplate, QStringLiteral("file_import_template"), i18n("Account Template..."), Icon::Empty}, {Action::FileExportTemplate, QStringLiteral("file_export_template"), i18n("Account Template..."), Icon::Empty}, {Action::FilePersonalData, QStringLiteral("view_personal_data"), i18n("Personal Data..."), Icon::UserProperties}, #ifdef KMM_DEBUG {Action::FileDump, QStringLiteral("file_dump"), i18n("Dump Memory"), Icon::Empty}, #endif {Action::FileInformation, QStringLiteral("view_file_info"), i18n("File-Information..."), Icon::DocumentProperties}, // ************* // The Edit menu // ************* {Action::EditFindTransaction, QStringLiteral("edit_find_transaction"), i18n("Find transaction..."), Icon::EditFindTransaction}, // ************* // The View menu // ************* {Action::ViewTransactionDetail, QStringLiteral("view_show_transaction_detail"), i18n("Show Transaction Detail"), Icon::ViewTransactionDetail}, {Action::ViewHideReconciled, QStringLiteral("view_hide_reconciled_transactions"), i18n("Hide reconciled transactions"), Icon::HideReconciled}, {Action::ViewHideCategories, QStringLiteral("view_hide_unused_categories"), i18n("Hide unused categories"), Icon::HideCategories}, {Action::ViewShowAll, QStringLiteral("view_show_all_accounts"), i18n("Show all accounts"), Icon::Empty}, // ********************* // The institutions menu // ********************* {Action::NewInstitution, QStringLiteral("institution_new"), i18n("New institution..."), Icon::InstitutionNew}, {Action::EditInstitution, QStringLiteral("institution_edit"), i18n("Edit institution..."), Icon::InstitutionEdit}, {Action::DeleteInstitution, QStringLiteral("institution_delete"), i18n("Delete institution..."), Icon::InstitutionDelete}, // ***************** // The accounts menu // ***************** {Action::NewAccount, QStringLiteral("account_new"), i18n("New account..."), Icon::AccountNew}, {Action::OpenAccount, QStringLiteral("account_open"), i18n("Open ledger"), Icon::ViewFinancialList}, {Action::StartReconciliation, QStringLiteral("account_reconcile"), i18n("Reconcile..."), Icon::Reconcile}, {Action::FinishReconciliation, QStringLiteral("account_reconcile_finish"), i18nc("Finish reconciliation", "Finish"), Icon::AccountFinishReconciliation}, {Action::PostponeReconciliation, QStringLiteral("account_reconcile_postpone"), i18n("Postpone reconciliation"), Icon::MediaPlaybackPause}, {Action::EditAccount, QStringLiteral("account_edit"), i18n("Edit account..."), Icon::AccountEdit}, {Action::DeleteAccount, QStringLiteral("account_delete"), i18n("Delete account..."), Icon::AccountDelete}, {Action::CloseAccount, QStringLiteral("account_close"), i18n("Close account"), Icon::AccountClose}, {Action::ReopenAccount, QStringLiteral("account_reopen"), i18n("Reopen account"), Icon::AccountReopen}, {Action::ReportAccountTransactions, QStringLiteral("account_transaction_report"), i18n("Transaction report"), Icon::ViewFinancialList}, {Action::ChartAccountBalance, QStringLiteral("account_chart"), i18n("Show balance chart..."), Icon::OfficeChartLine}, {Action::MapOnlineAccount, QStringLiteral("account_online_map"), i18n("Map account..."), Icon::NewsSubscribe}, {Action::UnmapOnlineAccount, QStringLiteral("account_online_unmap"), i18n("Unmap account..."), Icon::NewsUnsubscribe}, {Action::UpdateAccount, QStringLiteral("account_online_update"), i18n("Update account..."), Icon::AccountUpdate}, {Action::UpdateAllAccounts, QStringLiteral("account_online_update_all"), i18n("Update all accounts..."), Icon::AccountUpdateAll}, {Action::AccountCreditTransfer, QStringLiteral("account_online_new_credit_transfer"), i18n("New credit transfer"), Icon::AccountCreditTransfer}, // ******************* // The categories menu // ******************* {Action::NewCategory, QStringLiteral("category_new"), i18n("New category..."), Icon::CategoryNew}, {Action::EditCategory, QStringLiteral("category_edit"), i18n("Edit category..."), Icon::CategoryEdit}, {Action::DeleteCategory, QStringLiteral("category_delete"), i18n("Delete category..."), Icon::CategoryDelete}, // ************** // The tools menu // ************** {Action::ToolCurrencies, QStringLiteral("tools_currency_editor"), i18n("Currencies..."), Icon::ViewCurrencyList}, {Action::ToolPrices, QStringLiteral("tools_price_editor"), i18n("Prices..."), Icon::Empty}, {Action::ToolUpdatePrices, QStringLiteral("tools_update_prices"), i18n("Update Stock and Currency Prices..."), Icon::ToolUpdatePrices}, {Action::ToolConsistency, QStringLiteral("tools_consistency_check"), i18n("Consistency Check"), Icon::Empty}, {Action::ToolPerformance, QStringLiteral("tools_performancetest"), i18n("Performance-Test"), Icon::Fork}, {Action::ToolCalculator, QStringLiteral("tools_kcalc"), i18n("Calculator..."), Icon::AccessoriesCalculator}, // ***************** // The settings menu // ***************** {Action::SettingsAllMessages, QStringLiteral("settings_enable_messages"), i18n("Enable all messages"), Icon::Empty}, // ************* // The help menu // ************* {Action::HelpShow, QStringLiteral("help_show_tip"), i18n("&Show tip of the day"), Icon::Tip}, // *************************** // Actions w/o main menu entry // *************************** {Action::NewTransaction, QStringLiteral("transaction_new"), i18nc("New transaction button", "New"), Icon::TransactionNew}, {Action::EditTransaction, QStringLiteral("transaction_edit"), i18nc("Edit transaction button", "Edit"), Icon::TransactionEdit}, {Action::EnterTransaction, QStringLiteral("transaction_enter"), i18nc("Enter transaction", "Enter"), Icon::DialogOK}, {Action::EditSplits, QStringLiteral("transaction_editsplits"), i18nc("Edit split button", "Edit splits"), Icon::Split}, {Action::CancelTransaction, QStringLiteral("transaction_cancel"), i18nc("Cancel transaction edit", "Cancel"), Icon::DialogCancel}, {Action::DeleteTransaction, QStringLiteral("transaction_delete"), i18nc("Delete transaction", "Delete"), Icon::EditDelete}, {Action::DuplicateTransaction, QStringLiteral("transaction_duplicate"), i18nc("Duplicate transaction", "Duplicate"), Icon::EditCopy}, {Action::MatchTransaction, QStringLiteral("transaction_match"), i18nc("Button text for match transaction", "Match"),Icon::TransactionMatch}, {Action::AcceptTransaction, QStringLiteral("transaction_accept"), i18nc("Accept 'imported' and 'matched' transaction", "Accept"), Icon::TransactionAccept}, {Action::ToggleReconciliationFlag, QStringLiteral("transaction_mark_toggle"), i18nc("Toggle reconciliation flag", "Toggle"), Icon::Empty}, {Action::MarkCleared, QStringLiteral("transaction_mark_cleared"), i18nc("Mark transaction cleared", "Cleared"), Icon::Empty}, {Action::MarkReconciled, QStringLiteral("transaction_mark_reconciled"), i18nc("Mark transaction reconciled", "Reconciled"), Icon::Empty}, {Action::MarkNotReconciled, QStringLiteral("transaction_mark_notreconciled"), i18nc("Mark transaction not reconciled", "Not reconciled"), Icon::Empty}, {Action::SelectAllTransactions, QStringLiteral("transaction_select_all"), i18nc("Select all transactions", "Select all"), Icon::Empty}, {Action::GoToAccount, QStringLiteral("transaction_goto_account"), i18n("Go to account"), Icon::GoJump}, {Action::GoToPayee, QStringLiteral("transaction_goto_payee"), i18n("Go to payee"), Icon::GoJump}, {Action::NewScheduledTransaction, QStringLiteral("transaction_create_schedule"), i18n("Create scheduled transaction..."), Icon::AppointmentNew}, {Action::AssignTransactionsNumber, QStringLiteral("transaction_assign_number"), i18n("Assign next number"), Icon::Empty}, {Action::CombineTransactions, QStringLiteral("transaction_combine"), i18nc("Combine transactions", "Combine"), Icon::Empty}, {Action::CopySplits, QStringLiteral("transaction_copy_splits"), i18n("Copy splits"), Icon::Empty}, //Investment {Action::NewInvestment, QStringLiteral("investment_new"), i18n("New investment..."), Icon::InvestmentNew}, {Action::EditInvestment, QStringLiteral("investment_edit"), i18n("Edit investment..."), Icon::InvestmentEdit}, {Action::DeleteInvestment, QStringLiteral("investment_delete"), i18n("Delete investment..."), Icon::InvestmentDelete}, {Action::UpdatePriceOnline, QStringLiteral("investment_online_price_update"), i18n("Online price update..."), Icon::InvestmentOnlinePrice}, {Action::UpdatePriceManually, QStringLiteral("investment_manual_price_update"), i18n("Manual price update..."), Icon::Empty}, //Schedule {Action::NewSchedule, QStringLiteral("schedule_new"), i18n("New scheduled transaction"), Icon::AppointmentNew}, {Action::EditSchedule, QStringLiteral("schedule_edit"), i18n("Edit scheduled transaction"), Icon::DocumentEdit}, {Action::DeleteSchedule, QStringLiteral("schedule_delete"), i18n("Delete scheduled transaction"), Icon::EditDelete}, {Action::DuplicateSchedule, QStringLiteral("schedule_duplicate"), i18n("Duplicate scheduled transaction"), Icon::EditCopy}, {Action::EnterSchedule, QStringLiteral("schedule_enter"), i18n("Enter next transaction..."), Icon::KeyEnter}, {Action::SkipSchedule, QStringLiteral("schedule_skip"), i18n("Skip next transaction..."), Icon::MediaSeekForward}, //Payees {Action::NewPayee, QStringLiteral("payee_new"), i18n("New payee"), Icon::ListAddUser}, {Action::RenamePayee, QStringLiteral("payee_rename"), i18n("Rename payee"), Icon::PayeeRename}, {Action::DeletePayee, QStringLiteral("payee_delete"), i18n("Delete payee"), Icon::ListRemoveUser}, {Action::MergePayee, QStringLiteral("payee_merge"), i18n("Merge payees"), Icon::PayeeMerge}, //Tags {Action::NewTag, QStringLiteral("tag_new"), i18n("New tag"), Icon::ListAddTag}, {Action::RenameTag, QStringLiteral("tag_rename"), i18n("Rename tag"), Icon::TagRename}, {Action::DeleteTag, QStringLiteral("tag_delete"), i18n("Delete tag"), Icon::ListRemoveTag}, //debug actions #ifdef KMM_DEBUG {Action::WizardNewUser, QStringLiteral("new_user_wizard"), i18n("Test new feature"), Icon::Empty}, {Action::DebugTraces, QStringLiteral("debug_traces"), i18n("Debug Traces"), Icon::Empty}, #endif {Action::DebugTimers, QStringLiteral("debug_timers"), i18n("Debug Timers"), Icon::Empty}, // onlineJob actions {Action::DeleteOnlineJob, QStringLiteral("onlinejob_delete"), i18n("Remove credit transfer"), Icon::EditDelete}, {Action::EditOnlineJob, QStringLiteral("onlinejob_edit"), i18n("Edit credit transfer"), Icon::DocumentEdit}, {Action::LogOnlineJob, QStringLiteral("onlinejob_log"), i18n("Show log"), Icon::Empty}, }; for (const auto& info : actionInfos) { auto a = new QAction(this); // KActionCollection::addAction by name sets object name anyways, // so, as better alternative, set it here right from the start a->setObjectName(info.name); a->setText(info.text); if (info.icon != Icon::Empty) // no need to set empty icon a->setIcon(Icons::get(info.icon)); a->setEnabled(false); lutActions.insert(info.action, a); // store QAction's pointer for later processing } } { // List with slots that get connected here. Other slots get connected in e.g. appropriate views typedef void(KMyMoneyApp::*KMyMoneyAppFunc)(); const QHash actionConnections { // ************* // The File menu // ************* // {Action::FileOpenDatabase, &KMyMoneyApp::slotOpenDatabase}, // {Action::FileSaveAsDatabase, &KMyMoneyApp::slotSaveAsDatabase}, {Action::FileBackup, &KMyMoneyApp::slotBackupFile}, {Action::FileImportTemplate, &KMyMoneyApp::slotLoadAccountTemplates}, {Action::FileExportTemplate, &KMyMoneyApp::slotSaveAccountTemplates}, {Action::FilePersonalData, &KMyMoneyApp::slotFileViewPersonal}, #ifdef KMM_DEBUG {Action::FileDump, &KMyMoneyApp::slotFileFileInfo}, #endif {Action::FileInformation, &KMyMoneyApp::slotFileInfoDialog}, // ************* // The View menu // ************* {Action::ViewTransactionDetail, &KMyMoneyApp::slotShowTransactionDetail}, {Action::ViewHideReconciled, &KMyMoneyApp::slotHideReconciledTransactions}, {Action::ViewHideCategories, &KMyMoneyApp::slotHideUnusedCategories}, {Action::ViewShowAll, &KMyMoneyApp::slotShowAllAccounts}, // ************** // The tools menu // ************** {Action::ToolCurrencies, &KMyMoneyApp::slotCurrencyDialog}, {Action::ToolPrices, &KMyMoneyApp::slotPriceDialog}, {Action::ToolUpdatePrices, &KMyMoneyApp::slotEquityPriceUpdate}, {Action::ToolConsistency, &KMyMoneyApp::slotFileConsistencyCheck}, {Action::ToolPerformance, &KMyMoneyApp::slotPerformanceTest}, // {Action::ToolSQL, &KMyMoneyApp::slotGenerateSql}, {Action::ToolCalculator, &KMyMoneyApp::slotToolsStartKCalc}, // ***************** // The settings menu // ***************** {Action::SettingsAllMessages, &KMyMoneyApp::slotEnableMessages}, // ************* // The help menu // ************* {Action::HelpShow, &KMyMoneyApp::slotShowTipOfTheDay}, // *************************** // Actions w/o main menu entry // *************************** //debug actions #ifdef KMM_DEBUG {Action::WizardNewUser, &KMyMoneyApp::slotNewFeature}, {Action::DebugTraces, &KMyMoneyApp::slotToggleTraces}, #endif {Action::DebugTimers, &KMyMoneyApp::slotToggleTimers}, }; for (auto connection = actionConnections.cbegin(); connection != actionConnections.cend(); ++connection) connect(lutActions[connection.key()], &QAction::triggered, this, connection.value()); } // ************* // Setting some of added actions checkable // ************* { // Some actions are checkable, // so set them here const QVector checkableActions { Action::ViewTransactionDetail, Action::ViewHideReconciled, Action::ViewHideCategories, #ifdef KMM_DEBUG Action::DebugTraces, Action::DebugTimers, #endif Action::ViewShowAll }; for (const auto& it : checkableActions) { lutActions[it]->setCheckable(true); lutActions[it]->setEnabled(true); } } // ************* // Setting actions that are always enabled // ************* { const QVector alwaysEnabled { Action::HelpShow, Action::SettingsAllMessages, Action::ToolPerformance, Action::ToolCalculator }; for (const auto& action : alwaysEnabled) { lutActions[action]->setEnabled(true); } } // ************* // 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::StartReconciliation, Qt::CTRL + Qt::SHIFT + Qt::Key_R)}, {qMakePair(Action::NewTransaction, Qt::CTRL + Qt::Key_Insert)}, {qMakePair(Action::ToggleReconciliationFlag, Qt::CTRL + Qt::Key_Space)}, {qMakePair(Action::MarkCleared, Qt::CTRL + Qt::ALT+ Qt::Key_Space)}, {qMakePair(Action::MarkReconciled, Qt::CTRL + Qt::SHIFT + Qt::Key_Space)}, {qMakePair(Action::SelectAllTransactions, Qt::CTRL + Qt::Key_A)}, #ifdef KMM_DEBUG {qMakePair(Action::WizardNewUser, Qt::CTRL + Qt::Key_G)}, #endif {qMakePair(Action::AssignTransactionsNumber, Qt::CTRL + Qt::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(KMyMoneySettings::showRegisterDetailed()); lutActions[Action::ViewHideReconciled]->setChecked(KMyMoneySettings::hideReconciledTransactions()); lutActions[Action::ViewHideCategories]->setChecked(KMyMoneySettings::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 auto 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(Icons::get(Icon::DocumentImport)); menuContainer = static_cast(factory()->container(QStringLiteral("export"), this)); menuContainer->setIcon(Icons::get(Icon::DocumentExport)); return lutActions; } #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 pActions[_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"); pActions[Action::ViewHideReconciled]->setChecked(KMyMoneySettings::hideReconciledTransactions()); pActions[Action::ViewHideCategories]->setChecked(KMyMoneySettings::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); } #ifdef KMM_DEBUG void KMyMoneyApp::resizeEvent(QResizeEvent* ev) { KMainWindow::resizeEvent(ev); d->updateCaption(); } #endif bool KMyMoneyApp::queryClose() { if (!isReady()) return false; if (!slotFileClose()) return false; 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(); } bool KMyMoneyApp::isDatabase() { return (d->m_storageInfo.isOpened && ((d->m_storageInfo.type == eKMyMoney::StorageType::SQL))); } bool KMyMoneyApp::isNativeFile() { return (d->m_storageInfo.isOpened && (d->m_storageInfo.type == eKMyMoney::StorageType::SQL || d->m_storageInfo.type == eKMyMoney::StorageType::XML)); } bool KMyMoneyApp::fileOpen() const { return d->m_storageInfo.isOpened; } KMyMoneyAppCallback KMyMoneyApp::progressCallback() { return &KMyMoneyApp::progressCallback; } void KMyMoneyApp::consistencyCheck(bool alwaysDisplayResult) { d->consistencyCheck(alwaysDisplayResult); } 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 = pPlugins.importer.constBegin(); while (it_plugin != pPlugins.importer.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 == pPlugins.importer.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; } bool KMyMoneyApp::isFileOpenedInAnotherInstance(const QUrl &url) { const auto instances = instanceList(); #ifdef KMM_DBUS // check if there are other instances which might have this file open for (const auto& instance : instances) { QDBusInterface remoteApp(instance, "/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()) return true; } #else Q_UNUSED(url) #endif return false; } void KMyMoneyApp::slotShowTransactionDetail() { } void KMyMoneyApp::slotHideReconciledTransactions() { KMyMoneySettings::setHideReconciledTransactions(pActions[Action::ViewHideReconciled]->isChecked()); d->m_myMoneyView->slotRefreshViews(); } void KMyMoneyApp::slotHideUnusedCategories() { KMyMoneySettings::setHideUnusedCategory(pActions[Action::ViewHideCategories]->isChecked()); d->m_myMoneyView->slotRefreshViews(); } void KMyMoneyApp::slotShowAllAccounts() { KMyMoneySettings::setShowAllAccounts(pActions[Action::ViewShowAll]->isChecked()); d->m_myMoneyView->slotRefreshViews(); } #ifdef KMM_DEBUG void KMyMoneyApp::slotFileFileInfo() { if (!d->m_storageInfo.isOpened) { KMessageBox::information(this, i18n("No KMyMoneyFile open")); return; } QFile g("kmymoney.dump"); g.open(QIODevice::WriteOnly); QDataStream st(&g); MyMoneyStorageDump dumper; dumper.writeStream(st, MyMoneyFile::instance()->storage()); g.close(); } void KMyMoneyApp::slotToggleTraces() { MyMoneyTracer::onOff(pActions[Action::DebugTraces]->isChecked() ? 1 : 0); } #endif void KMyMoneyApp::slotToggleTimers() { extern bool timersOn; // main.cpp timersOn = pActions[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_storageInfo.isOpened) { 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", QString::fromLatin1(e.what()))); } } delete editPersonalDataDlg; } 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(this, i18n("Unable to import template(s)"), QString::fromLatin1(e.what())); } } delete dlg; } void KMyMoneyApp::slotSaveAccountTemplates() { KMSTATUS(i18n("Exporting account templates.")); QString savePath = QStandardPaths::writableLocation(QStandardPaths::DataLocation) + "/templates/" + QLocale().name(); QDir templatesDir(savePath); if (!templatesDir.exists()) templatesDir.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; } } } bool KMyMoneyApp::okToWriteFile(const QUrl &url) { Q_UNUSED(url) // check if the file exists and warn the user bool reallySaveFile = true; 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 auto dlg = new KSettingsKMyMoney(this, "KMyMoney-Settings", KMyMoneySettings::self()); connect(dlg, &KSettingsKMyMoney::settingsChanged, this, &KMyMoneyApp::slotUpdateConfiguration); dlg->show(); } void KMyMoneyApp::slotShowCredits() { KAboutData aboutData = initializeCreditsData(); KAboutApplicationDialog dlg(aboutData, this); dlg.exec(); } void KMyMoneyApp::slotUpdateConfiguration(const QString &dialogName) { if(dialogName.compare(QLatin1String("Plugins")) == 0) { KMyMoneyPlugin::pluginHandling(KMyMoneyPlugin::Action::Reorganize, pPlugins, this, guiFactory()); actionCollection()->action(QString::fromLatin1(KStandardAction::name(KStandardAction::SaveAs)))->setEnabled(d->canFileSaveAs()); onlineJobAdministration::instance()->updateActions(); onlineJobAdministration::instance()->setOnlinePlugins(pPlugins.extended); d->m_myMoneyView->setOnlinePlugins(pPlugins.online); d->updateActions(); + d->m_myMoneyView->slotRefreshViews(); return; } MyMoneyTransactionFilter::setFiscalYearStart(KMyMoneySettings::firstFiscalMonth(), KMyMoneySettings::firstFiscalDay()); #ifdef ENABLE_UNFINISHEDFEATURES LedgerSeparator::setFirstFiscalDate(KMyMoneySettings::firstFiscalMonth(), KMyMoneySettings::firstFiscalDay()); #endif d->m_myMoneyView->updateViewType(); // update the sql storage module settings // MyMoneyStorageSql::setStartDate(KMyMoneySettings::startDate().date()); // update the report module settings MyMoneyReport::setLineWidth(KMyMoneySettings::lineWidth()); // update the holiday region configuration setHolidayRegion(KMyMoneySettings::holidayRegion()); d->m_myMoneyView->slotRefreshViews(); // re-read autosave configuration d->m_autoSaveEnabled = KMyMoneySettings::autoSaveFile(); d->m_autoSavePeriod = KMyMoneySettings::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->dirty()) { d->m_autoSaveTimer->setSingleShot(true); d->m_autoSaveTimer->start(d->m_autoSavePeriod * 60 * 1000); } d->setThemedCSS(); } void KMyMoneyApp::slotBackupFile() { // Save the file first so isLocalFile() works if (d->m_myMoneyView && d->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_storageInfo.url.isEmpty()) return; if (!d->m_storageInfo.url.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_storageInfo.url.url()), i18n("Local files only")); return; } QPointer backupDlg = new KBackupDlg(this); int returncode = backupDlg->exec(); if (returncode == QDialog::Accepted && backupDlg != 0) { d->m_backupMount = backupDlg->mountCheckBoxChecked(); 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_storageInfo.url.fileName()); QString today = QDate::currentDate().toString("-yyyy-MM-dd.") + fi.suffix(); QString backupfile = d->m_mountpoint + '/' + d->m_storageInfo.url.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_storageInfo.url.toLocalFile()) + "+ nul") << QDir::toNativeSeparators(backupfile); #else d->m_proc << "cp" << "-f"; d->m_proc << d->m_storageInfo.url.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::slotViewSelected(View view) { KMyMoneySettings::setLastViewSelected(static_cast(view)); } void KMyMoneyApp::slotGenerateSql() { // QPointer editor = new KGenerateSqlDlg(this); // editor->setObjectName("Generate Database SQL"); // editor->exec(); // delete editor; } void KMyMoneyApp::slotToolsStartKCalc() { QString cmd = KMyMoneySettings::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::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", QString::fromLatin1(e.what()))); } } void KMyMoneyApp::slotInvestmentNew(MyMoneyAccount& account, const MyMoneyAccount& parent) { KNewInvestmentWizard::newInvestment(account, parent); } void KMyMoneyApp::slotCategoryNew(MyMoneyAccount& account, const MyMoneyAccount& parent) { KNewAccountDlg::newCategory(account, parent); } void KMyMoneyApp::slotCategoryNew(MyMoneyAccount& account) { KNewAccountDlg::newCategory(account, MyMoneyAccount()); } void KMyMoneyApp::slotAccountNew(MyMoneyAccount& account) { NewAccountWizard::Wizard::newAccount(account); } 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_CSTRING("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", QString::fromLatin1(e.what()))); } } catch (const MyMoneyException &e) { KMessageBox::information(this, i18n("Unable to add scheduled transaction: %1", QString::fromLatin1(e.what()))); } } } 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; q->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; } q->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++; } q->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) { q->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); } } } q->slotStatusProgressBar(NR_OF_STEPS_LIMIT / PROGRESSBAR_STEPS, 0); if (sumToComponentsMap.contains(amount)) { QListIterator > itTransactionSplit2(transactions); while (itTransactionSplit2.hasNext()) { const QPair &transactionSplit = itTransactionSplit2.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 q->slotStatusProgressBar(-1, -1); return result; } 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(), QString::fromLatin1(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(), QString::fromLatin1(e.what()))); } } void KMyMoneyApp::slotScheduleNew(const MyMoneyTransaction& _t, eMyMoney::Schedule::Occurrence occurrence) { KEditScheduleDlg::newSchedule(_t, occurrence); } void KMyMoneyApp::slotPayeeNew(const QString& newnameBase, QString& id) { KMyMoneyUtils::newPayee(newnameBase, id); } void KMyMoneyApp::slotNewFeature() { } // 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::showContextMenu(const QString& containerName) { QWidget* w = factory()->container(containerName, this); if (auto menu = dynamic_cast(w)) menu->exec(QCursor::pos()); else qDebug("menu '%s' not found: w = %p, menu = %p", qPrintable(containerName), w, menu); } void KMyMoneyApp::slotPrintView() { d->m_myMoneyView->slotPrintView(); } void KMyMoneyApp::Private::updateCaption() { auto caption = m_storageInfo.url.isEmpty() && m_myMoneyView && m_storageInfo.isOpened ? i18n("Untitled") : m_storageInfo.url.fileName(); #ifdef KMM_DEBUG caption += QString(" (%1 x %2)").arg(q->width()).arg(q->height()); #endif q->setCaption(caption, MyMoneyFile::instance()->dirty()); } void KMyMoneyApp::Private::updateActions() { const QVector actions { Action::FilePersonalData, Action::FileInformation, Action::FileImportTemplate, Action::FileExportTemplate, #ifdef KMM_DEBUG Action::FileDump, #endif Action::EditFindTransaction, Action::NewCategory, Action::ToolCurrencies, Action::ToolPrices, Action::ToolUpdatePrices, Action::ToolConsistency, Action::ToolPerformance, Action::NewAccount, Action::NewInstitution, Action::NewSchedule }; for (const auto &action : actions) pActions[action]->setEnabled(m_storageInfo.isOpened); pActions[Action::FileBackup]->setEnabled(m_storageInfo.isOpened && m_storageInfo.type == eKMyMoney::StorageType::XML); auto aC = q->actionCollection(); aC->action(QString::fromLatin1(KStandardAction::name(KStandardAction::SaveAs)))->setEnabled(canFileSaveAs()); aC->action(QString::fromLatin1(KStandardAction::name(KStandardAction::Close)))->setEnabled(m_storageInfo.isOpened); pActions[eMenu::Action::UpdateAllAccounts]->setEnabled(canUpdateAllAccounts()); } bool KMyMoneyApp::Private::canFileSaveAs() const { return (m_storageInfo.isOpened && (!pPlugins.storage.isEmpty() && !(pPlugins.storage.count() == 1 && pPlugins.storage.first()->storageType() == eKMyMoney::StorageType::GNC))); } bool KMyMoneyApp::Private::canUpdateAllAccounts() const { const auto file = MyMoneyFile::instance(); auto rc = false; if (!file->storageAttached()) return rc; QList accList; file->accountList(accList); QList::const_iterator it_a; auto it_p = pPlugins.online.constEnd(); for (it_a = accList.constBegin(); (it_p == pPlugins.online.constEnd()) && (it_a != accList.constEnd()); ++it_a) { if ((*it_a).hasOnlineMapping()) { // check if provider is available it_p = pPlugins.online.constFind((*it_a).onlineBankingSettings().value("provider").toLower()); if (it_p != pPlugins.online.constEnd()) { QStringList protocols; (*it_p)->protocols(protocols); if (!protocols.isEmpty()) { rc = true; break; } } } } return rc; } void KMyMoneyApp::slotDataChanged() { d->fileAction(eKMyMoney::FileAction::Changed); } void KMyMoneyApp::slotCurrencyDialog() { QPointer dlg = new KCurrencyEditDlg(this); dlg->exec(); delete dlg; } void KMyMoneyApp::slotPriceDialog() { QPointer dlg = new KMyMoneyPriceDlg(this); dlg->exec(); delete dlg; } void KMyMoneyApp::slotFileConsistencyCheck() { d->consistencyCheck(true); } 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"), KMyMoneySettings::schemeColor(SchemeColor::WindowText).name(), Qt::CaseSensitive); cssText.replace(QLatin1String("Window"), KMyMoneySettings::schemeColor(SchemeColor::WindowBackground).name(), Qt::CaseSensitive); cssText.replace(QLatin1String("HighlightText"), KMyMoneySettings::schemeColor(SchemeColor::ListHighlightText).name(), Qt::CaseSensitive); cssText.replace(QLatin1String("Highlight"), KMyMoneySettings::schemeColor(SchemeColor::ListHighlight).name(), Qt::CaseSensitive); cssText.replace(QLatin1String("black"), KMyMoneySettings::schemeColor(SchemeColor::ListGrid).name(), Qt::CaseSensitive); cssStream.seek(0); cssStream << cssText; cssFile.close(); } } } } } void KMyMoneyApp::slotCheckSchedules() { if (KMyMoneySettings::checkSchedule() == true) { KMSTATUS(i18n("Checking for overdue scheduled transactions...")); MyMoneyFile *file = MyMoneyFile::instance(); QDate checkDate = QDate::currentDate().addDays(KMyMoneySettings::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 = d->m_myMoneyView->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; } } } } 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_storageInfo.url.url(); } QUrl KMyMoneyApp::filenameURL() const { return d->m_storageInfo.url; } void KMyMoneyApp::writeFilenameURL(const QUrl &url) { d->m_storageInfo.url = url; } void KMyMoneyApp::addToRecentFiles(const QUrl& url) { d->m_recentFiles->addUrl(url); } QTimer* KMyMoneyApp::autosaveTimer() { return d->m_autoSaveTimer; } 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_storageInfo.isOpened && KMessageBox::warningContinueCancel(this, i18n("You must first select a KMyMoney file before you can import a statement.")) == KMessageBox::Continue) slotFileOpen(); // only continue if the user really did open a file. if (d->m_storageInfo.isOpened) { KMSTATUS(i18n("Importing a statement via Web Connect")); // remove the statement files d->unlinkStatementXML(); QMap::const_iterator it_plugin = pPlugins.importer.constBegin(); while (it_plugin != pPlugins.importer.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 == pPlugins.importer.constEnd()) if (MyMoneyStatement::isStatementFile(url)) MyMoneyStatementReader::importStatement(url, false, progressCallback); } // 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().appInterface = new KMyMoneyPlugin::KMMAppInterface(this, this); KMyMoneyPlugin::pluginInterfaces().importInterface = new KMyMoneyPlugin::KMMImportInterface(this); KMyMoneyPlugin::pluginInterfaces().statementInterface = new KMyMoneyPlugin::KMMStatementInterface(this); KMyMoneyPlugin::pluginInterfaces().viewInterface = new KMyMoneyPlugin::KMMViewInterface(d->m_myMoneyView, this); // setup the calendar interface for schedules MyMoneySchedule::setProcessingCalendar(this); } 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->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((static_cast(dt.secsTo(nextDay)) + 1)*1000, this, SLOT(slotDateChanged())); d->m_myMoneyView->slotRefreshViews(); } 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 { if (!d->m_processingDays.testBit(date.dayOfWeek())) return false; #ifdef KF5Holidays_FOUND 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 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 auto forecastDays = KMyMoneySettings::forecastDays() + KMyMoneySettings::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 } bool KMyMoneyApp::slotFileNew() { KMSTATUS(i18n("Creating new document...")); if (!slotFileClose()) return false; NewUserWizard::Wizard wizard; if (wizard.exec() != QDialog::Accepted) return false; d->m_storageInfo.isOpened = true; d->m_storageInfo.type = eKMyMoney::StorageType::None; d->m_storageInfo.url = QUrl(); try { auto storage = new MyMoneyStorageMgr; MyMoneyFile::instance()->attachStorage(storage); MyMoneyFileTransaction ft; auto file = MyMoneyFile::instance(); // 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 for (auto &tmpl : wizard.templates()) tmpl.importTemplate(progressCallback); ft.commit(); KMyMoneySettings::setFirstTimeRun(false); d->fileAction(eKMyMoney::FileAction::Opened); if (actionCollection()->action(QString::fromLatin1(KStandardAction::name(KStandardAction::SaveAs)))->isEnabled()) slotFileSaveAs(); } catch (const MyMoneyException & e) { slotFileClose(); d->removeStorage(); KMessageBox::detailedError(this, i18n("Couldn't create a new file."), e.what()); return false; } if (wizard.startSettingsAfterFinished()) slotSettings(); return true; } void KMyMoneyApp::slotFileOpen() { KMSTATUS(i18n("Open a file.")); const QVector desiredFileExtensions {eKMyMoney::StorageType::XML, eKMyMoney::StorageType::GNC}; QString fileExtensions; for (const auto &extension : desiredFileExtensions) { for (const auto &plugin : pPlugins.storage) { if (plugin->storageType() == extension) { fileExtensions += plugin->fileExtension() + QLatin1String(";;"); break; } } } if (fileExtensions.isEmpty()) { KMessageBox::error(this, i18n("Couldn't find any plugin for opening storage.")); return; } fileExtensions.append(i18n("All files (*)")); QPointer dialog = new QFileDialog(this, QString(), readLastUsedDir(), fileExtensions); dialog->setFileMode(QFileDialog::ExistingFile); dialog->setAcceptMode(QFileDialog::AcceptOpen); if (dialog->exec() == QDialog::Accepted && dialog != nullptr) slotFileOpenRecent(dialog->selectedUrls().first()); delete dialog; } bool KMyMoneyApp::slotFileOpenRecent(const QUrl &url) { KMSTATUS(i18n("Loading file...")); if (!url.isValid()) throw MYMONEYEXCEPTION(QString::fromLatin1("Invalid URL %1").arg(qPrintable(url.url()))); if (isFileOpenedInAnotherInstance(url)) { KMessageBox::sorry(this, i18n("

File %1 is already opened in another instance of KMyMoney

", url.toDisplayString(QUrl::PreferLocalFile)), i18n("Duplicate open")); return false; } if (url.scheme() != QLatin1String("sql") && !KMyMoneyUtils::fileExists(url)) { 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")); return false; } if (d->m_storageInfo.isOpened) if (!slotFileClose()) return false; // open the database d->m_storageInfo.type = eKMyMoney::StorageType::None; for (auto &plugin : pPlugins.storage) { try { if (auto pStorage = plugin->open(url)) { MyMoneyFile::instance()->attachStorage(pStorage); d->m_storageInfo.type = plugin->storageType(); if (plugin->storageType() != eKMyMoney::StorageType::GNC) { d->m_storageInfo.url = url; writeLastUsedFile(url.toDisplayString(QUrl::PreferLocalFile)); /* Don't use url variable after KRecentFilesAction::addUrl * as it might delete it. * More in API reference to this method */ d->m_recentFiles->addUrl(url); } d->m_storageInfo.isOpened = true; break; } } catch (const MyMoneyException &e) { KMessageBox::sorry(this, i18n("Cannot open file as requested. Error was: %1", QString::fromLatin1(e.what()))); return false; } } if(d->m_storageInfo.type == eKMyMoney::StorageType::None) { KMessageBox::error(this, i18n("Could not read your data source. Please check the KMyMoney settings that the necessary plugin is enabled.")); return false; } d->fileAction(eKMyMoney::FileAction::Opened); return true; } bool KMyMoneyApp::slotFileSave() { KMSTATUS(i18n("Saving file...")); for (const auto& plugin : pPlugins.storage) { if (plugin->storageType() == d->m_storageInfo.type) { d->consistencyCheck(false); try { if (plugin->save(d->m_storageInfo.url)) { d->fileAction(eKMyMoney::FileAction::Saved); return true; } return false; } catch (const MyMoneyException &e) { KMessageBox::detailedError(this, i18n("Failed to save your storage."), e.what()); return false; } } } KMessageBox::error(this, i18n("Couldn't find suitable plugin to save your storage.")); return false; } bool KMyMoneyApp::slotFileSaveAs() { KMSTATUS(i18n("Saving file as....")); QVector availableFileTypes; for (const auto& plugin : pPlugins.storage) { switch (plugin->storageType()) { case eKMyMoney::StorageType::GNC: break; default: availableFileTypes.append(plugin->storageType()); break; } } auto chosenFileType = eKMyMoney::StorageType::None; switch (availableFileTypes.count()) { case 0: KMessageBox::error(this, i18n("Couldn't find any plugin for saving storage.")); return false; case 1: chosenFileType = availableFileTypes.first(); break; default: { KSaveAsQuestion dlg(availableFileTypes, this); if (dlg.exec() != QDialog::Accepted) return false; chosenFileType = dlg.fileType(); } } for (const auto &plugin : pPlugins.storage) { if (chosenFileType == plugin->storageType()) { try { d->consistencyCheck(false); if (plugin->saveAs()) { d->fileAction(eKMyMoney::FileAction::Saved); d->m_storageInfo.type = plugin->storageType(); return true; } } catch (const MyMoneyException &e) { KMessageBox::detailedError(this, i18n("Failed to save your storage."), e.what()); } } } return false; } bool KMyMoneyApp::slotFileClose() { if (!d->m_storageInfo.isOpened) return true; if (!d->askAboutSaving()) return false; d->fileAction(eKMyMoney::FileAction::Closing); d->removeStorage(); d->m_storageInfo = KMyMoneyApp::Private::storageInfo(); d->fileAction(eKMyMoney::FileAction::Closed); return true; } 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::Private::fileAction(eKMyMoney::FileAction action) { switch(action) { case eKMyMoney::FileAction::Opened: q->actionCollection()->action(QString::fromLatin1(KStandardAction::name(KStandardAction::Save)))->setEnabled(false); updateAccountNames(); updateCurrencyNames(); selectBaseCurrency(); // setup the standard precision AmountEdit::setStandardPrecision(MyMoneyMoney::denomToPrec(MyMoneyFile::instance()->baseCurrency().smallestAccountFraction())); KMyMoneyEdit::setStandardPrecision(MyMoneyMoney::denomToPrec(MyMoneyFile::instance()->baseCurrency().smallestAccountFraction())); applyFileFixes(); Models::instance()->fileOpened(); connectStorageToModels(); // inform everyone about new data MyMoneyFile::instance()->forceDataChanged(); updateActions(); m_myMoneyView->slotFileOpened(); onlineJobAdministration::instance()->updateActions(); m_myMoneyView->enableViewsIfFileOpen(m_storageInfo.isOpened); m_myMoneyView->slotRefreshViews(); onlineJobAdministration::instance()->updateOnlineTaskProperties(); q->connect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, q, &KMyMoneyApp::slotDataChanged); #ifdef KF5Activities_FOUND m_activityResourceInstance->setUri(m_storageInfo.url); #endif break; case eKMyMoney::FileAction::Saved: q->connect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, q, &KMyMoneyApp::slotDataChanged); q->actionCollection()->action(QString::fromLatin1(KStandardAction::name(KStandardAction::Save)))->setEnabled(false); m_autoSaveTimer->stop(); break; case eKMyMoney::FileAction::Closing: disconnect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, q, &KMyMoneyApp::slotDataChanged); m_myMoneyView->slotFileClosed(); // 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(); break; case eKMyMoney::FileAction::Closed: q->disconnect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, q, &KMyMoneyApp::slotDataChanged); disconnectStorageFromModels(); q->actionCollection()->action(QString::fromLatin1(KStandardAction::name(KStandardAction::Save)))->setEnabled(false); m_myMoneyView->enableViewsIfFileOpen(m_storageInfo.isOpened); updateActions(); break; case eKMyMoney::FileAction::Changed: q->disconnect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, q, &KMyMoneyApp::slotDataChanged); q->actionCollection()->action(QString::fromLatin1(KStandardAction::name(KStandardAction::Save)))->setEnabled(true && !m_storageInfo.url.isEmpty()); // 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 (m_autoSaveEnabled && !m_autoSaveTimer->isActive()) { m_autoSaveTimer->setSingleShot(true); m_autoSaveTimer->start(m_autoSavePeriod * 60 * 1000); //miliseconds } pActions[eMenu::Action::UpdateAllAccounts]->setEnabled(canUpdateAllAccounts()); break; default: break; } updateCaption(); } 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])); } } diff --git a/kmymoney/mymoney/mymoneyaccount.cpp b/kmymoney/mymoney/mymoneyaccount.cpp index d515b28e4..0739ca4bb 100644 --- a/kmymoney/mymoney/mymoneyaccount.cpp +++ b/kmymoney/mymoney/mymoneyaccount.cpp @@ -1,736 +1,759 @@ /* * Copyright 2000-2002 Michael Edwardes * Copyright 2001 Felix Rodriguez * Copyright 2002-2003 Kevin Tambascio * Copyright 2006-2017 Thomas Baumgart * Copyright 2006 Ace Jones * Copyright 2017-2018 Łukasz Wojniłowicz * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "mymoneyaccount.h" #include "mymoneyaccount_p.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include // ---------------------------------------------------------------------------- // Project Includes #include "mymoneyutils.h" #include "mymoneyexception.h" #include "mymoneysplit.h" #include "mymoneyfile.h" #include "mymoneysecurity.h" #include "mymoneyinstitution.h" #include "mymoneypayee.h" #include "payeeidentifier/payeeidentifiertyped.h" #include "payeeidentifier/ibanbic/ibanbic.h" #include "payeeidentifier/nationalaccount/nationalaccount.h" #include "mymoneystoragenames.h" #include "icons/icons.h" using namespace MyMoneyStorageNodes; using namespace Icons; MyMoneyAccount::MyMoneyAccount() : MyMoneyObject(*new MyMoneyAccountPrivate), MyMoneyKeyValueContainer() { } MyMoneyAccount::MyMoneyAccount(const QString &id): MyMoneyObject(*new MyMoneyAccountPrivate, id), MyMoneyKeyValueContainer() { } MyMoneyAccount::MyMoneyAccount(const QDomElement& node) : MyMoneyObject(*new MyMoneyAccountPrivate, node), MyMoneyKeyValueContainer(node.elementsByTagName(nodeNames[nnKeyValuePairs]).item(0).toElement()) { if (nodeNames[nnAccount] != node.tagName()) throw MYMONEYEXCEPTION_CSTRING("Node was not ACCOUNT"); Q_D(MyMoneyAccount); setName(node.attribute(d->getAttrName(Account::Attribute::Name))); // qDebug("Reading information for account %s", acc.name().data()); setParentAccountId(MyMoneyUtils::QStringEmpty(node.attribute(d->getAttrName(Account::Attribute::ParentAccount)))); setLastModified(MyMoneyUtils::stringToDate(MyMoneyUtils::QStringEmpty(node.attribute(d->getAttrName(Account::Attribute::LastModified))))); setLastReconciliationDate(MyMoneyUtils::stringToDate(MyMoneyUtils::QStringEmpty(node.attribute(d->getAttrName(Account::Attribute::LastReconciled))))); if (!d->m_lastReconciliationDate.isValid()) { // for some reason, I was unable to access our own kvp at this point through // the value() method. It always returned empty strings. The workaround for // this is to construct a local kvp the same way as we have done before and // extract the value from it. // // Since we want to get rid of the lastStatementDate record anyway, this seems // to be ok for now. (ipwizard - 2008-08-14) QString txt = MyMoneyKeyValueContainer(node.elementsByTagName(nodeNames[nnKeyValuePairs]).item(0).toElement()).value("lastStatementDate"); if (!txt.isEmpty()) { setLastReconciliationDate(QDate::fromString(txt, Qt::ISODate)); } } setInstitutionId(MyMoneyUtils::QStringEmpty(node.attribute(d->getAttrName(Account::Attribute::Institution)))); setNumber(MyMoneyUtils::QStringEmpty(node.attribute(d->getAttrName(Account::Attribute::Number)))); setOpeningDate(MyMoneyUtils::stringToDate(MyMoneyUtils::QStringEmpty(node.attribute(d->getAttrName(Account::Attribute::Opened))))); setCurrencyId(MyMoneyUtils::QStringEmpty(node.attribute(d->getAttrName(Account::Attribute::Currency)))); QString tmp = MyMoneyUtils::QStringEmpty(node.attribute(d->getAttrName(Account::Attribute::Type))); bool bOK = false; int type = tmp.toInt(&bOK); if (bOK) { setAccountType(static_cast(type)); } else { qWarning("XMLREADER: Account %s had invalid or no account type information.", qPrintable(name())); } if (node.hasAttribute(d->getAttrName(Account::Attribute::OpeningBalance))) if (!MyMoneyMoney(node.attribute(d->getAttrName(Account::Attribute::OpeningBalance))).isZero()) throw MYMONEYEXCEPTION(QString::fromLatin1("Account %1 contains an opening balance. Please use KMyMoney version 0.8 or later and earlier than version 0.9 to correct the problem.").arg(d->m_name)); setDescription(node.attribute(d->getAttrName(Account::Attribute::Description))); d->m_id = MyMoneyUtils::QStringEmpty(node.attribute(d->getAttrName(Account::Attribute::ID))); // qDebug("Account %s has id of %s, type of %d, parent is %s.", acc.name().data(), id.data(), type, acc.parentAccountId().data()); // Process any Sub-Account information found inside the account entry. d->m_accountList.clear(); QDomNodeList nodeList = node.elementsByTagName(d->getElName(Account::Element::SubAccounts)); if (nodeList.count() > 0) { nodeList = nodeList.item(0).toElement().elementsByTagName(d->getElName(Account::Element::SubAccount)); for (int i = 0; i < nodeList.count(); ++i) { addAccountId(QString(nodeList.item(i).toElement().attribute(d->getAttrName(Account::Attribute::ID)))); } } nodeList = node.elementsByTagName(d->getElName(Account::Element::OnlineBanking)); if (nodeList.count() > 0) { QDomNamedNodeMap attributes = nodeList.item(0).toElement().attributes(); for (int i = 0; i < attributes.count(); ++i) { const QDomAttr& it_attr = attributes.item(i).toAttr(); d->m_onlineBankingSettings.setValue(it_attr.name(), it_attr.value()); } } // Up to and including version 4.6.6 the new account dialog stored the iban in the kvp-key "IBAN". // But the rest of the software uses "iban". So correct this: if (!value("IBAN").isEmpty()) { // If "iban" was not set, set it now. If it is set, the user reseted it already, so remove // the garbage. if (value(d->getAttrName(Account::Attribute::IBAN)).isEmpty()) setValue(d->getAttrName(Account::Attribute::IBAN), value("IBAN")); deletePair("IBAN"); } } MyMoneyAccount::MyMoneyAccount(const MyMoneyAccount& other) : MyMoneyObject(*new MyMoneyAccountPrivate(*other.d_func()), other.id()), MyMoneyKeyValueContainer(other) { } MyMoneyAccount::MyMoneyAccount(const QString& id, const MyMoneyAccount& other) : MyMoneyObject(*new MyMoneyAccountPrivate(*other.d_func()), id), MyMoneyKeyValueContainer(other) { } MyMoneyAccount::~MyMoneyAccount() { } void MyMoneyAccount::touch() { setLastModified(QDate::currentDate()); } eMyMoney::Account::Type MyMoneyAccount::accountType() const { Q_D(const MyMoneyAccount); return d->m_accountType; } void MyMoneyAccount::setAccountType(const Account::Type type) { Q_D(MyMoneyAccount); d->m_accountType = type; } QString MyMoneyAccount::institutionId() const { Q_D(const MyMoneyAccount); return d->m_institution; } void MyMoneyAccount::setInstitutionId(const QString& id) { Q_D(MyMoneyAccount); d->m_institution = id; } QString MyMoneyAccount::name() const { Q_D(const MyMoneyAccount); return d->m_name; } void MyMoneyAccount::setName(const QString& name) { Q_D(MyMoneyAccount); d->m_name = name; } QString MyMoneyAccount::number() const { Q_D(const MyMoneyAccount); return d->m_number; } void MyMoneyAccount::setNumber(const QString& number) { Q_D(MyMoneyAccount); d->m_number = number; } QString MyMoneyAccount::description() const { Q_D(const MyMoneyAccount); return d->m_description; } void MyMoneyAccount::setDescription(const QString& desc) { Q_D(MyMoneyAccount); d->m_description = desc; } QDate MyMoneyAccount::openingDate() const { Q_D(const MyMoneyAccount); return d->m_openingDate; } void MyMoneyAccount::setOpeningDate(const QDate& date) { Q_D(MyMoneyAccount); d->m_openingDate = date; } QDate MyMoneyAccount::lastReconciliationDate() const { Q_D(const MyMoneyAccount); return d->m_lastReconciliationDate; } void MyMoneyAccount::setLastReconciliationDate(const QDate& date) { Q_D(MyMoneyAccount); // FIXME: for a limited time (maybe until we delivered 1.0) we // keep the last reconciliation date also in the KVP for backward // compatibility. After that, the setValue() statemetn should be removed // and the XML ctor should remove the value completely from the KVP setValue("lastStatementDate", date.toString(Qt::ISODate)); d->m_lastReconciliationDate = date; } QDate MyMoneyAccount::lastModified() const { Q_D(const MyMoneyAccount); return d->m_lastModified; } void MyMoneyAccount::setLastModified(const QDate& date) { Q_D(MyMoneyAccount); d->m_lastModified = date; } QString MyMoneyAccount::parentAccountId() const { Q_D(const MyMoneyAccount); return d->m_parentAccount; } void MyMoneyAccount::setParentAccountId(const QString& parent) { Q_D(MyMoneyAccount); d->m_parentAccount = parent; } QStringList MyMoneyAccount::accountList() const { Q_D(const MyMoneyAccount); return d->m_accountList; } int MyMoneyAccount::accountCount() const { Q_D(const MyMoneyAccount); return d->m_accountList.count(); } void MyMoneyAccount::addAccountId(const QString& account) { Q_D(MyMoneyAccount); if (!d->m_accountList.contains(account)) d->m_accountList += account; } void MyMoneyAccount::removeAccountIds() { Q_D(MyMoneyAccount); d->m_accountList.clear(); } void MyMoneyAccount::removeAccountId(const QString& account) { Q_D(MyMoneyAccount); const auto pos = d->m_accountList.indexOf(account); if (pos != -1) d->m_accountList.removeAt(pos); } bool MyMoneyAccount::operator == (const MyMoneyAccount& right) const { Q_D(const MyMoneyAccount); auto d2 = static_cast(right.d_func()); return (MyMoneyKeyValueContainer::operator==(right) && MyMoneyObject::operator==(right) && (d->m_accountList == d2->m_accountList) && (d->m_accountType == d2->m_accountType) && (d->m_lastModified == d2->m_lastModified) && (d->m_lastReconciliationDate == d2->m_lastReconciliationDate) && ((d->m_name.length() == 0 && d2->m_name.length() == 0) || (d->m_name == d2->m_name)) && ((d->m_number.length() == 0 && d2->m_number.length() == 0) || (d->m_number == d2->m_number)) && ((d->m_description.length() == 0 && d2->m_description.length() == 0) || (d->m_description == d2->m_description)) && (d->m_openingDate == d2->m_openingDate) && (d->m_parentAccount == d2->m_parentAccount) && (d->m_currencyId == d2->m_currencyId) && (d->m_institution == d2->m_institution)); } Account::Type MyMoneyAccount::accountGroup() const { Q_D(const MyMoneyAccount); switch (d->m_accountType) { case Account::Type::Checkings: case Account::Type::Savings: case Account::Type::Cash: case Account::Type::Currency: case Account::Type::Investment: case Account::Type::MoneyMarket: case Account::Type::CertificateDep: case Account::Type::AssetLoan: case Account::Type::Stock: return Account::Type::Asset; case Account::Type::CreditCard: case Account::Type::Loan: return Account::Type::Liability; default: return d->m_accountType; } } QString MyMoneyAccount::currencyId() const { Q_D(const MyMoneyAccount); return d->m_currencyId; } +QString MyMoneyAccount::tradingCurrencyId() const +{ + const auto file = MyMoneyFile::instance(); + + // First, get the trading currency (formerly deep currency) + auto deepcurrency = file->security(currencyId()); + if (!deepcurrency.isCurrency()) + deepcurrency = file->security(deepcurrency.tradingCurrency()); + + // Return the trading currency's ID + return deepcurrency.id(); +} + +bool MyMoneyAccount::isForeignCurrency() const +{ + return (tradingCurrencyId() != MyMoneyFile::instance()->baseCurrency().id()); +} + void MyMoneyAccount::setCurrencyId(const QString& id) { Q_D(MyMoneyAccount); d->m_currencyId = id; } bool MyMoneyAccount::isAssetLiability() const { return accountGroup() == Account::Type::Asset || accountGroup() == Account::Type::Liability; } bool MyMoneyAccount::isIncomeExpense() const { return accountGroup() == Account::Type::Income || accountGroup() == Account::Type::Expense; } bool MyMoneyAccount::isLoan() const { return accountType() == Account::Type::Loan || accountType() == Account::Type::AssetLoan; } bool MyMoneyAccount::isInvest() const { return accountType() == Account::Type::Stock; } bool MyMoneyAccount::isLiquidAsset() const { return accountType() == Account::Type::Checkings || accountType() == Account::Type::Savings || accountType() == Account::Type::Cash; } +bool MyMoneyAccount::isLiquidLiability() const +{ + return accountType() == Account::Type::CreditCard; +} + bool MyMoneyAccount::isCostCenterRequired() const { return value("CostCenter").toLower() == QLatin1String("yes"); } void MyMoneyAccount::setCostCenterRequired(bool required) { if(required) { setValue("CostCenter", "yes"); } else { deletePair("CostCenter"); } } void MyMoneyAccount::writeXML(QDomDocument& document, QDomElement& parent) const { auto el = document.createElement(nodeNames[nnAccount]); Q_D(const MyMoneyAccount); d->writeBaseXML(document, el); el.setAttribute(d->getAttrName(Account::Attribute::ParentAccount), parentAccountId()); el.setAttribute(d->getAttrName(Account::Attribute::LastReconciled), MyMoneyUtils::dateToString(lastReconciliationDate())); el.setAttribute(d->getAttrName(Account::Attribute::LastModified), MyMoneyUtils::dateToString(lastModified())); el.setAttribute(d->getAttrName(Account::Attribute::Institution), institutionId()); el.setAttribute(d->getAttrName(Account::Attribute::Opened), MyMoneyUtils::dateToString(openingDate())); el.setAttribute(d->getAttrName(Account::Attribute::Number), number()); // el.setAttribute(getAttrName(anOpeningBalance), openingBalance().toString()); el.setAttribute(d->getAttrName(Account::Attribute::Type), (int)accountType()); el.setAttribute(d->getAttrName(Account::Attribute::Name), name()); el.setAttribute(d->getAttrName(Account::Attribute::Description), description()); if (!currencyId().isEmpty()) el.setAttribute(d->getAttrName(Account::Attribute::Currency), currencyId()); //Add in subaccount information, if this account has subaccounts. if (accountCount()) { QDomElement subAccounts = document.createElement(d->getElName(Account::Element::SubAccounts)); foreach (const auto accountID, accountList()) { QDomElement temp = document.createElement(d->getElName(Account::Element::SubAccount)); temp.setAttribute(d->getAttrName(Account::Attribute::ID), accountID); subAccounts.appendChild(temp); } el.appendChild(subAccounts); } // Write online banking settings if (d->m_onlineBankingSettings.pairs().count()) { QDomElement onlinesettings = document.createElement(d->getElName(Account::Element::OnlineBanking)); QMap::const_iterator it_key = d->m_onlineBankingSettings.pairs().constBegin(); while (it_key != d->m_onlineBankingSettings.pairs().constEnd()) { onlinesettings.setAttribute(it_key.key(), it_key.value()); ++it_key; } el.appendChild(onlinesettings); } // FIXME drop the lastStatementDate record from the KVP when it is // not stored there after setLastReconciliationDate() has been changed // See comment there when this will happen // deletePair("lastStatementDate"); //Add in Key-Value Pairs for accounts. MyMoneyKeyValueContainer::writeXML(document, el); parent.appendChild(el); } bool MyMoneyAccount::hasReferenceTo(const QString& id) const { Q_D(const MyMoneyAccount); return (id == d->m_institution) || (id == d->m_parentAccount) || (id == d->m_currencyId); } void MyMoneyAccount::setOnlineBankingSettings(const MyMoneyKeyValueContainer& values) { Q_D(MyMoneyAccount); d->m_onlineBankingSettings = values; } MyMoneyKeyValueContainer MyMoneyAccount::onlineBankingSettings() const { Q_D(const MyMoneyAccount); return d->m_onlineBankingSettings; } void MyMoneyAccount::setClosed(bool closed) { if (closed) setValue("mm-closed", "yes"); else deletePair("mm-closed"); } bool MyMoneyAccount::isClosed() const { return !(value("mm-closed").isEmpty()); } int MyMoneyAccount::fraction(const MyMoneySecurity& sec) const { Q_D(const MyMoneyAccount); int fraction; if (d->m_accountType == Account::Type::Cash) fraction = sec.smallestCashFraction(); else fraction = sec.smallestAccountFraction(); return fraction; } int MyMoneyAccount::fraction(const MyMoneySecurity& sec) { Q_D(MyMoneyAccount); if (d->m_accountType == Account::Type::Cash) d->m_fraction = sec.smallestCashFraction(); else d->m_fraction = sec.smallestAccountFraction(); return d->m_fraction; } int MyMoneyAccount::fraction() const { Q_D(const MyMoneyAccount); return d->m_fraction; } bool MyMoneyAccount::isCategory() const { Q_D(const MyMoneyAccount); return d->m_accountType == Account::Type::Income || d->m_accountType == Account::Type::Expense; } QString MyMoneyAccount::brokerageName() const { Q_D(const MyMoneyAccount); if (d->m_accountType == Account::Type::Investment) return QString("%1 (%2)").arg(d->m_name, i18nc("Brokerage (suffix for account names)", "Brokerage")); return d->m_name; } MyMoneyMoney MyMoneyAccount::balance() const { Q_D(const MyMoneyAccount); return d->m_balance; } void MyMoneyAccount::adjustBalance(const MyMoneySplit& s, bool reverse) { Q_D(MyMoneyAccount); if (s.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::SplitShares)) { if (reverse) d->m_balance = d->m_balance / s.shares(); else d->m_balance = d->m_balance * s.shares(); } else { if (reverse) d->m_balance -= s.shares(); else d->m_balance += s.shares(); } } void MyMoneyAccount::setBalance(const MyMoneyMoney& val) { Q_D(MyMoneyAccount); d->m_balance = val; } QPixmap MyMoneyAccount::accountPixmap(const bool reconcileFlag, const int size) const { static const QHash accToIco { {Account::Type::Asset, Icon::ViewAsset}, {Account::Type::Investment, Icon::ViewStock}, {Account::Type::Stock, Icon::ViewStock}, {Account::Type::MoneyMarket, Icon::ViewStock}, {Account::Type::Checkings, Icon::ViewChecking}, {Account::Type::Savings, Icon::ViewSaving}, {Account::Type::AssetLoan, Icon::ViewLoanAsset}, {Account::Type::Loan, Icon::ViewLoan}, {Account::Type::CreditCard, Icon::ViewCreditCard}, {Account::Type::Asset, Icon::ViewAsset}, {Account::Type::Cash, Icon::ViewCash}, {Account::Type::Income, Icon::ViewIncome}, {Account::Type::Expense, Icon::ViewExpense}, {Account::Type::Equity, Icon::ViewEquity} }; Icon ixIcon = accToIco.value(accountType(), Icon::ViewLiability); QString kyIcon = accountTypeToString(accountType()) + QString::number(size); QPixmap pxIcon; if (!QPixmapCache::find(kyIcon, pxIcon)) { pxIcon = Icons::get(ixIcon).pixmap(size); // Qt::AA_UseHighDpiPixmaps (in Qt 5.7) doesn't return highdpi pixmap QPixmapCache::insert(kyIcon, pxIcon); } if (isClosed()) ixIcon = Icon::AccountClosed; else if (reconcileFlag) ixIcon = Icon::FlagGreen; else if (hasOnlineMapping()) ixIcon = Icon::Download; else return pxIcon; QPixmap pxOverlay = Icons::get(ixIcon).pixmap(size); QPainter pxPainter(&pxIcon); const QSize szIcon = pxIcon.size(); pxPainter.drawPixmap(szIcon.width() / 2, szIcon.height() / 2, szIcon.width() / 2, szIcon.height() / 2, pxOverlay); return pxIcon; } QString MyMoneyAccount::accountTypeToString(const Account::Type accountType) { switch (accountType) { case Account::Type::Checkings: return i18nc("Account type", "Checking"); case Account::Type::Savings: return i18nc("Account type", "Savings"); case Account::Type::CreditCard: return i18nc("Account type", "Credit Card"); case Account::Type::Cash: return i18nc("Account type", "Cash"); case Account::Type::Loan: return i18nc("Account type", "Loan"); case Account::Type::CertificateDep: return i18nc("Account type", "Certificate of Deposit"); case Account::Type::Investment: return i18nc("Account type", "Investment"); case Account::Type::MoneyMarket: return i18nc("Account type", "Money Market"); case Account::Type::Asset: return i18nc("Account type", "Asset"); case Account::Type::Liability: return i18nc("Account type", "Liability"); case Account::Type::Currency: return i18nc("Account type", "Currency"); case Account::Type::Income: return i18nc("Account type", "Income"); case Account::Type::Expense: return i18nc("Account type", "Expense"); case Account::Type::AssetLoan: return i18nc("Account type", "Investment Loan"); case Account::Type::Stock: return i18nc("Account type", "Stock"); case Account::Type::Equity: return i18nc("Account type", "Equity"); default: return i18nc("Account type", "Unknown"); } } bool MyMoneyAccount::addReconciliation(const QDate& date, const MyMoneyMoney& amount) { Q_D(MyMoneyAccount); d->m_reconciliationHistory[date] = amount; QString history, sep; QMap::const_iterator it; for (it = d->m_reconciliationHistory.constBegin(); it != d->m_reconciliationHistory.constEnd(); ++it) { history += QString("%1%2:%3").arg(sep, it.key().toString(Qt::ISODate), (*it).toString()); sep = QLatin1Char(';'); } setValue("reconciliationHistory", history); return true; } QMap MyMoneyAccount::reconciliationHistory() { Q_D(MyMoneyAccount); // check if the internal history member is already loaded if (d->m_reconciliationHistory.count() == 0 && !value("reconciliationHistory").isEmpty()) { QStringList entries = value("reconciliationHistory").split(';'); foreach (const QString& entry, entries) { QStringList parts = entry.split(':'); QDate date = QDate::fromString(parts[0], Qt::ISODate); MyMoneyMoney amount(parts[1]); if (parts.count() == 2 && date.isValid()) { d->m_reconciliationHistory[date] = amount; } } } return d->m_reconciliationHistory; } /** * @todo Improve setting of country for nationalAccount */ QList< payeeIdentifier > MyMoneyAccount::payeeIdentifiers() const { Q_D(const MyMoneyAccount); QList< payeeIdentifier > list; MyMoneyFile* file = MyMoneyFile::instance(); // Iban & Bic if (!value(d->getAttrName(Account::Attribute::IBAN)).isEmpty()) { payeeIdentifierTyped iban(new payeeIdentifiers::ibanBic); iban->setIban(value(d->getAttrName(Account::Attribute::IBAN))); iban->setBic(file->institution(institutionId()).value(d->getAttrName(Account::Attribute::BIC))); iban->setOwnerName(file->user().name()); list.append(iban); } // National Account number if (!number().isEmpty()) { payeeIdentifierTyped national(new payeeIdentifiers::nationalAccount); national->setAccountNumber(number()); national->setBankCode(file->institution(institutionId()).sortcode()); if (file->user().state().length() == 2) national->setCountry(file->user().state()); national->setOwnerName(file->user().name()); list.append(national); } return list; } bool MyMoneyAccount::hasOnlineMapping() const { Q_D(const MyMoneyAccount); return !d->m_onlineBankingSettings.value(QLatin1String("provider")).isEmpty(); } diff --git a/kmymoney/mymoney/mymoneyaccount.h b/kmymoney/mymoney/mymoneyaccount.h index 13ab74bc1..5a534cbf4 100644 --- a/kmymoney/mymoney/mymoneyaccount.h +++ b/kmymoney/mymoney/mymoneyaccount.h @@ -1,601 +1,630 @@ /* * Copyright 2000-2002 Michael Edwardes * Copyright 2001 Felix Rodriguez * Copyright 2002-2003 Kevin Tambascio * Copyright 2006-2017 Thomas Baumgart * Copyright 2006 Ace Jones * Copyright 2017-2018 Łukasz Wojniłowicz * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef MYMONEYACCOUNT_H #define MYMONEYACCOUNT_H // ---------------------------------------------------------------------------- // QT Includes #include // ---------------------------------------------------------------------------- // Project Includes #include "mymoneykeyvaluecontainer.h" #include "mymoneyobject.h" #include "kmm_mymoney_export.h" #include "mymoneyunittestable.h" class QString; class QDate; class QPixmap; class QDomElement; class MyMoneySecurity; class MyMoneyMoney; class MyMoneySplit; class payeeIdentifier; namespace eMyMoney { namespace Account { enum class Type; } } template class payeeIdentifierTyped; /** * A representation of an account. * This object represents any type of account, those held at an * institution as well as the accounts used for double entry * accounting. * * Currently, the following account types are known: * * @li Unknown * @li Checkings * @li Savings * @li Cash * @li CreditCard * @li Loan (collected) * @li CertificateDep * @li Investment * @li MoneyMarket * @li Currency * @li Asset * @li Liability * @li Income * @li Expense * @li Loan (given) * @li Stock * @li Equity * * @see MyMoneyInstitution * @see MyMoneyFile * * @author Michael Edwardes 2000-2001 * @author Thomas Baumgart 2002 * @author Łukasz Wojniłowicz 2017 * **/ class MyMoneyAccountPrivate; class KMM_MYMONEY_EXPORT MyMoneyAccount : public MyMoneyObject, public MyMoneyKeyValueContainer /*, public MyMoneyPayeeIdentifierContainer */ { Q_DECLARE_PRIVATE_D(MyMoneyObject::d_ptr, MyMoneyAccount) KMM_MYMONEY_UNIT_TESTABLE public: /** * This is the constructor for a new empty account */ MyMoneyAccount(); explicit MyMoneyAccount(const QString &id); /** * This is the constructor for a new account known to the current file * This is the only constructor that will set the attribute m_openingDate * to a correct value. * * @param id id assigned to the account * @param right account definition */ MyMoneyAccount(const QString& id, const MyMoneyAccount& other); /** * This is the constructor for an account that is described by a * QDomElement (e.g. from a file). * * @param el const reference to the QDomElement from which to * create the object */ explicit MyMoneyAccount(const QDomElement& el); MyMoneyAccount(const MyMoneyAccount & other); MyMoneyAccount(MyMoneyAccount && other); MyMoneyAccount & operator=(MyMoneyAccount other); friend void swap(MyMoneyAccount& first, MyMoneyAccount& second); /** * This is the destructor for any MyMoneyAccount object */ ~MyMoneyAccount(); /** * This operator tests for equality of two MyMoneyAccount objects */ bool operator == (const MyMoneyAccount &) const; /** * This converts the account type into one of the four * major account types liability, asset, expense or income. * * The current assignment is as follows: * * - Asset * - Asset * - Checkings * - Savings * - Cash * - Currency * - Investment * - MoneyMarket * - CertificateDep * - AssetLoan * - Stock * * - Liability * - Liability * - CreditCard * - Loan * * - Income * - Income * * - Expense * - Expense * * @return accountTypeE of major account type */ eMyMoney::Account::Type accountGroup() const; /** * This method returns the id of the MyMoneyInstitution object this account * belongs to. * @return id of MyMoneyInstitution object. QString() if it is * an internal account * @see setInstitution */ QString institutionId() const; /** * This method is used to set the id of the institution this account * belongs to. * * @param id id of the institution this account belongs to * @see institution */ void setInstitutionId(const QString& id); /** * This method returns the name of the account * @return name of account * @see setName() */ QString name() const; /** * This method is used to set the name of the account * @param name name of the account * @see name */ void setName(const QString& name); /** * This method returns the number of the account at the institution * @return number of account at the institution * @see setNumber */ QString number() const; /** * This method is used to set the number of the account at the institution * @param number number of the account * @see number */ void setNumber(const QString& number); /** * This method returns the descriptive text of the account. * @return description of account * @see setDescription */ QString description() const; /** * This method is used to set the descriptive text of the account * * @param desc text that serves as description * @see setDescription */ void setDescription(const QString& desc); /** * This method returns the opening date of this account * @return date of opening of this account as const QDate value * @see setOpeningDate() */ QDate openingDate() const; /** * This method is used to set the opening date information of an * account. * * @param date QDate of opening date * @see openingDate */ void setOpeningDate(const QDate& date); /** * This method returns the date of the last reconciliation of this account * @return date of last reconciliation as const QDate value * @see setLastReconciliationDate */ QDate lastReconciliationDate() const; /** * This method is used to set the date of the last reconciliation * of an account. * @param date QDate of last reconciliation * @see lastReconciliationDate */ void setLastReconciliationDate(const QDate& date); /** * This method returns the date the account was last modified * @return date of last modification as const QDate value * @see setLastModified */ QDate lastModified() const; /** * This method is used to modify the date of the last * modification access. * @param date date of last modification * @see lastModified */ void setLastModified(const QDate& date); /** * This method is used to return the ID of the parent account * @return QString with the ID of the parent of this account */ QString parentAccountId() const; /** * This method is used to set a new parent account id * @param parent QString reference to new parent account */ void setParentAccountId(const QString& parent); /** * This method returns the list of the account id's of * subordinate accounts * @return QStringList account ids */ QStringList accountList() const; /** * This method returns the number of entries in the m_accountList * @return number of entries in the accountList */ int accountCount() const; /** * This method is used to add an account id as sub-ordinate account * @param account const QString reference to account ID */ void addAccountId(const QString& account); /** * This method is used to remove an account from the list of * sub-ordinate accounts. * @param account const QString reference to account ID to be removed */ void removeAccountId(const QString& account); /** * This method is used to remove all accounts from the list of * sub-ordinate accounts. */ void removeAccountIds(); /** * Return the stored account identifiers * * @internal This method is temporary until MyMoneyAccount is a MyMoneyPayeeIdentifierContainer. But before this * can happen the account numbers and iban kvp data must be moved there. */ QList< payeeIdentifier > payeeIdentifiers() const; /** * This method is used to update m_lastModified to the current date */ void touch(); /** * This method returns the type of the account. */ eMyMoney::Account::Type accountType() const; /** * This method is used to change the account type * * @param type account type */ void setAccountType(const eMyMoney::Account::Type type); /** * This method retrieves the id of the currency used with this account. * If the return value is empty, the base currency should be used. * * @return id of currency */ QString currencyId() const; + /** There are three different currencies in play with a single Account: + * - The underlying currency: What currency the account itself is denominated in + * - The deep currency: The underlying currency's own underlying currency. This + * is only a factor if the underlying currency of this account IS NOT a + * currency itself, but is some other kind of security. In that case, the + * underlying security has its own currency. The deep currency is the + * currency of the underlying security. On the other hand, if the account + * has a currency itself, then the deep currency == the underlying currency, + * and this function will return 1.0. + * - The base currency: The base currency of the user's overall file + * + * @return id of deep currency + */ + QString tradingCurrencyId() const; + + /** + * Determine if this account's deep currency is different from the file's + * base currency + * + * @return bool True if this account is in a foreign currency + */ + bool isForeignCurrency() const; + /** * This method sets the id of the currency used with this account. * * @param id ID of currency to be associated with this account. */ void setCurrencyId(const QString& id); void writeXML(QDomDocument& document, QDomElement& parent) const override; /** * This method checks if a reference to the given object exists. It returns, * a @p true if the object is referencing the one requested by the * parameter @p id. If it does not, this method returns @p false. * * @param id id of the object to be checked for references * @retval true This object references object with id @p id. * @retval false This object does not reference the object with id @p id. */ bool hasReferenceTo(const QString& id) const override; /** * This member returns the balance of this account based on * all transactions stored in the journal. */ MyMoneyMoney balance() const; /** * This method adjusts the balance of this account * according to the difference contained in the split @p s. * If the s.action() is MyMoneySplit::actionName(eMyMoney::Split::Action::SplitShares) then * the balance will be adjusted accordingly. * * @param s const reference to MyMoneySplit object containing the * value to be added/subtracted to/from the balance * @param reverse add (false) or subtract (true) the shares contained in the split. * It also affects the balance for share splits in the opposite direction. */ void adjustBalance(const MyMoneySplit& s, bool reverse = false); /** * This method sets the balance of this account * according to the value provided by @p val. * * @param val const reference to MyMoneyMoney object containing the * value to be assigned to the balance */ void setBalance(const MyMoneyMoney& val); /** * This method sets the kvp's for online banking with this account * * @param values The container of kvp's needed when connecting to this account */ void setOnlineBankingSettings(const MyMoneyKeyValueContainer& values); /** * This method retrieves the kvp's for online banking with this account * * @return The container of kvp's needed when connecting to this account */ MyMoneyKeyValueContainer onlineBankingSettings() const; /** * This method sets the closed flag for the account. This is just * an informational flag for the application. It has no other influence * on the behaviour of the account object. The default for * new objects @p open. * * @param isClosed mark the account closed (@p true) or open (@p false). */ void setClosed(bool isClosed); /** * Return the closed flag for the account. * * @retval false account is marked open (the default for new accounts) * @retval true account is marked closed */ bool isClosed() const; /** * returns the applicable smallest fraction for this account * for the given security based on the account type. At the same * time, m_fraction is updated to the value returned. * * @param sec const reference to currency (security) * * @retval sec.smallestCashFraction() for account type Cash * @retval sec.smallestAccountFraction() for all other account types */ int fraction(const MyMoneySecurity& sec); /** * Same as the above method, but does not modify m_fraction. */ int fraction(const MyMoneySecurity& sec) const; /** * This method returns the stored value for the fraction of this * account or -1 if not initialized. It can be initialized by * calling fraction(const MyMoneySecurity& sec) once. * * @note Don't use this method outside of KMyMoney application context (eg. testcases). * Use the above method instead. */ int fraction() const; /** * This method returns @a true if the account type is * either Income or Expense * * @retval true account is of type income or expense * @retval false for all other account types * * @deprecated use isIncomeExpense() instead */ KMM_MYMONEY_DEPRECATED bool isCategory() const; /** * This method returns @a true if the account type is * either Income or Expense * * @retval true account is of type income or expense * @retval false for all other account types */ bool isIncomeExpense() const; /** * This method returns @a true if the account type is * either Asset or Liability * * @retval true account is of type asset or liability * @retval false for all other account types */ bool isAssetLiability() const; /** * This method returns @a true if the account type is * either AssetLoan or Loan * * @retval true account is of type Loan or AssetLoan * @retval false for all other account types */ bool isLoan() const; /** * This method returns @a true if the account type is * Stock * * @retval true account is of type Stock * @retval false for all other account types */ bool isInvest() const; /** * This method returns @a true if the account type is * Checkings, Savings or Cash * * @retval true account is of type Checkings, Savings or Cash * @retval false for all other account types */ bool isLiquidAsset() const; + /** + * Returns whether this account is a liquid liability + * + */ + bool isLiquidLiability() const; + /** * This method returns true if a costcenter assignment is required for this account */ bool isCostCenterRequired() const; /** * This method allows to control if a cost center assignment is required * for this account. It is if @a required is @c true (the default). */ void setCostCenterRequired(bool required = true); /** * This method returns a name that has a brokerage suffix of * the current name. It only works on investment accounts and * returns the name for all other cases. */ QString brokerageName() const; /** * @param reconcileFlag if set to @a true a reconcile overlay will be * added to the pixmap returned * @param size is a hint for the size of the icon * @return a pixmap using DesktopIcon for the account type */ QPixmap accountPixmap(const bool reconcileFlag = false, const int size = 64) const; /** * This method is used to convert the internal representation of * an account type into a human readable format * * @param accountType numerical representation of the account type. * For possible values, see eMyMoney::Account::Type * @return QString representing the human readable form */ static QString accountTypeToString(const eMyMoney::Account::Type accountType); /** * keeps a history record of a reconciliation for this account on @a date * with @a amount. * * @return @p true in case entry was added, @p false otherwise * * @sa reconciliationHistory() */ bool addReconciliation(const QDate& date, const MyMoneyMoney& amount); /** * @return QMap with the reconciliation history for the account * * @sa addReconciliation() */ QMap reconciliationHistory(); /** * @return @c true if account has an online mapping, @c false otherwise */ bool hasOnlineMapping() const; QDataStream &operator<<(const MyMoneyAccount &); QDataStream &operator>>(MyMoneyAccount &); }; inline void swap(MyMoneyAccount& first, MyMoneyAccount& second) // krazy:exclude=inline { using std::swap; swap(first.MyMoneyObject::d_ptr, second.MyMoneyObject::d_ptr); swap(first.MyMoneyKeyValueContainer::d_ptr, second.MyMoneyKeyValueContainer::d_ptr); } inline MyMoneyAccount::MyMoneyAccount(MyMoneyAccount && other) : MyMoneyAccount() // krazy:exclude=inline { swap(*this, other); } inline MyMoneyAccount & MyMoneyAccount::operator=(MyMoneyAccount other) // krazy:exclude=inline { swap(*this, other); return *this; } /** * Make it possible to hold @ref MyMoneyAccount objects, * @ref accountTypeE and @ref amountTypeE inside @ref QVariant objects. */ Q_DECLARE_METATYPE(MyMoneyAccount) Q_DECLARE_METATYPE(eMyMoney::Account::Type) #endif diff --git a/kmymoney/mymoney/storage/tests/CMakeLists.txt b/kmymoney/mymoney/storage/tests/CMakeLists.txt index 97e455106..b4c15fb53 100644 --- a/kmymoney/mymoney/storage/tests/CMakeLists.txt +++ b/kmymoney/mymoney/storage/tests/CMakeLists.txt @@ -1,16 +1,15 @@ include(ECMAddTests) file(GLOB tests_sources "*-test.cpp") ecm_add_tests(${tests_sources} LINK_LIBRARIES Qt5::Core Qt5::Test Qt5::Xml Qt5::Gui kmm_mymoney kmm_testutils - reports_testcommon kmm_utils_platformtools onlinetask_interfaces ) diff --git a/kmymoney/mymoney/tests/mymoneyforecast-test.cpp b/kmymoney/mymoney/tests/mymoneyforecast-test.cpp index a3531a7d3..1295f77e0 100644 --- a/kmymoney/mymoney/tests/mymoneyforecast-test.cpp +++ b/kmymoney/mymoney/tests/mymoneyforecast-test.cpp @@ -1,992 +1,992 @@ /* * Copyright 2007-2010 Alvaro Soliverez * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "mymoneyforecast-test.h" #include #include #include #include "mymoneybudget.h" #include "mymoneyexception.h" #include "mymoneystoragedump.h" -#include "reportstestcommon.h" +#include "views/reports/core/tests/reportstestcommon.h" #include "mymoneyinstitution.h" #include "mymoneysecurity.h" #include "mymoneymoney.h" #include "mymoneysplit.h" #include "mymoneyschedule.h" #include "mymoneypayee.h" #include "mymoneyenums.h" using namespace eMyMoney; using namespace test; QTEST_GUILESS_MAIN(MyMoneyForecastTest) MyMoneyForecastTest::MyMoneyForecastTest() : m(nullptr), storage(nullptr), file(nullptr) { try { this->moT1 = MyMoneyMoney(57, 1); this->moT2 = MyMoneyMoney(63, 1); this->moT3 = MyMoneyMoney(84, 1); this->moT4 = MyMoneyMoney(62, 1); this->moT5 = MyMoneyMoney(104, 1); } catch (const MyMoneyException &e) { qDebug() << e.what(); } } void MyMoneyForecastTest::init() { //all this has been taken from pivottabletest.cpp, by Thomas Baumgart and Ace Jones storage = new MyMoneyStorageMgr; file = MyMoneyFile::instance(); file->attachStorage(storage); MyMoneyFileTransaction ft; file->addCurrency(MyMoneySecurity("CAD", "Canadian Dollar", "C$")); file->addCurrency(MyMoneySecurity("USD", "US Dollar", "$")); file->addCurrency(MyMoneySecurity("JPY", "Japanese Yen", QChar(0x00A5), 1)); file->addCurrency(MyMoneySecurity("GBP", "British Pound", "#")); file->setBaseCurrency(file->currency("USD")); MyMoneyPayee payeeTest; payeeTest.setName("Test Payee"); file->addPayee(payeeTest); MyMoneyPayee payeeTest2; payeeTest2.setName("Alvaro Soliverez"); file->addPayee(payeeTest2); acAsset = (MyMoneyFile::instance()->asset().id()); acLiability = (MyMoneyFile::instance()->liability().id()); acExpense = (MyMoneyFile::instance()->expense().id()); acIncome = (MyMoneyFile::instance()->income().id()); acChecking = makeAccount(QString("Checking Account"), Account::Type::Checkings, moCheckingOpen, QDate(2004, 5, 15), acAsset, "USD"); acCredit = makeAccount(QString("Credit Card"), Account::Type::CreditCard, moCreditOpen, QDate(2004, 7, 15), acLiability, "USD"); acSolo = makeAccount(QString("Solo"), Account::Type::Expense, MyMoneyMoney(), QDate(2004, 1, 11), acExpense, "USD"); acParent = makeAccount(QString("Parent"), Account::Type::Expense, MyMoneyMoney(), QDate(2004, 1, 11), acExpense, "USD"); acChild = makeAccount(QString("Child"), Account::Type::Expense, MyMoneyMoney(), QDate(2004, 2, 11), acParent, "USD"); acForeign = makeAccount(QString("Foreign"), Account::Type::Expense, MyMoneyMoney(), QDate(2004, 1, 11), acExpense, "USD"); acInvestment = makeAccount("Investment", Account::Type::Investment, moZero, QDate(2004, 1, 1), acAsset, "USD"); acSecondChild = makeAccount(QString("Second Child"), Account::Type::Expense, MyMoneyMoney(), QDate(2004, 2, 11), acParent, "USD"); acGrandChild1 = makeAccount(QString("Grand Child 1"), Account::Type::Expense, MyMoneyMoney(), QDate(2004, 2, 11), acChild, "USD"); acGrandChild2 = makeAccount(QString("Grand Child 2"), Account::Type::Expense, MyMoneyMoney(), QDate(2004, 2, 11), acChild, "USD"); //this account added to have an account to test opening date calculations acCash = makeAccount(QString("Cash"), Account::Type::Cash, moCreditOpen, QDate::currentDate().addDays(-2), acAsset, "USD"); MyMoneyInstitution i("Bank of the World", "", "", "", "", "", ""); file->addInstitution(i); inBank = i.id(); ft.commit(); } void MyMoneyForecastTest::cleanup() { file->detachStorage(storage); delete storage; } void MyMoneyForecastTest::testEmptyConstructor() { MyMoneyForecast a; MyMoneyAccount b; QVERIFY(a.forecastBalance(b, QDate::currentDate()).isZero()); QVERIFY(!a.isForecastAccount(b)); QVERIFY(a.forecastBalance(b, QDate::currentDate()) == MyMoneyMoney()); QVERIFY(a.daysToMinimumBalance(b) == -1); QVERIFY(a.daysToZeroBalance(b) == -2); QVERIFY(a.forecastDays() == 90); QVERIFY(a.accountsCycle() == 30); QVERIFY(a.forecastCycles() == 3); QVERIFY(a.historyStartDate() == QDate::currentDate().addDays(-3*30)); QVERIFY(a.historyEndDate() == QDate::currentDate().addDays(-1)); QVERIFY(a.historyDays() == 30 * 3); } void MyMoneyForecastTest::testDoForecastInit() { MyMoneyForecast a; a.doForecast(); /* //check the illegal argument validation try { KMyMoneySettings::setForecastDays(-10); a.doForecast(); } catch (const MyMoneyException &e) { QFAIL("Unexpected exception"); } try { KMyMoneySettings::setForecastAccountCycle(-20); a.doForecast(); } catch (const MyMoneyException &e) { QFAIL("Unexpected exception"); } try { KMyMoneySettings::setForecastCycles(-10); a.doForecast(); } catch (const MyMoneyException &e) { QFAIL("Unexpected exception"); } try { KMyMoneySettings::setForecastAccountCycle(0); a.doForecast(); } catch (const MyMoneyException &e) { QFAIL("Unexpected exception"); } try { KMyMoneySettings::setForecastDays(0); KMyMoneySettings::setForecastCycles(0); KMyMoneySettings::setForecastAccountCycle(0); a.doForecast(); } catch (const MyMoneyException &e) { QVERIFY("Unexpected exception"); }*/ } //test that it forecasts correctly with transactions in the period of forecast void MyMoneyForecastTest::testDoForecast() { //set up environment MyMoneyForecast a; MyMoneyAccount a_checking = file->account(acChecking); MyMoneyAccount a_credit = file->account(acCredit); //test empty forecast a.doForecast(); //this is just to check nothing goes wrong if forecast is run agains an empty template //setup some transactions TransactionHelper t1(QDate::currentDate().addDays(-1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), this->moT1, acChecking, acSolo); TransactionHelper t2(QDate::currentDate().addDays(-1), MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit), -(this->moT2), acCredit, acParent); TransactionHelper t3(QDate::currentDate().addDays(-1), MyMoneySplit::actionName(eMyMoney::Split::Action::Transfer), this->moT1, acCredit, acChecking); a.setForecastMethod(1); a.setForecastDays(3); a.setAccountsCycle(1); a.setForecastCycles(1); a.setBeginForecastDay(0); a.setHistoryMethod(0); //moving average a.doForecast(); //checking didn't have balance variations, so the forecast should be equal to the current balance MyMoneyMoney b_checking = file->balance(a_checking.id(), QDate::currentDate()); QVERIFY(a.forecastBalance(a_checking, QDate::currentDate().addDays(1)) == b_checking); QVERIFY(a.forecastBalance(a_checking, QDate::currentDate().addDays(2)) == b_checking); QVERIFY(a.forecastBalance(a_checking, QDate::currentDate().addDays(3)) == b_checking); QVERIFY(a.forecastBalance(a_checking, QDate::currentDate()) == b_checking); //credit had a variation so the forecast should be different for each day MyMoneyMoney b_credit = file->balance(a_credit.id(), QDate::currentDate()); QVERIFY(a.forecastBalance(a_credit, 0) == b_credit); QVERIFY(a.forecastBalance(a_credit, QDate::currentDate().addDays(1)) == (b_credit + (moT2 - moT1))); QVERIFY(a.forecastBalance(a_credit, QDate::currentDate().addDays(2)) == (b_credit + ((moT2 - moT1)*2))); QVERIFY(a.forecastBalance(a_credit, QDate::currentDate().addDays(3)) == b_credit + ((moT2 - moT1)*3)); a.setHistoryMethod(1); //weighted moving average a.doForecast(); QVERIFY(a.forecastBalance(a_credit, 0) == b_credit); QVERIFY(a.forecastBalance(a_credit, QDate::currentDate().addDays(1)) == (b_credit + (moT2 - moT1))); QVERIFY(a.forecastBalance(a_credit, QDate::currentDate().addDays(2)) == (b_credit + ((moT2 - moT1)*2))); QVERIFY(a.forecastBalance(a_credit, QDate::currentDate().addDays(3)) == b_credit + ((moT2 - moT1)*3)); //insert transactions outside the forecast period. The calculation should be the same. TransactionHelper t4(QDate::currentDate().addDays(-2), MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit), -moT2, acCredit, acParent); TransactionHelper t5(QDate::currentDate().addDays(-10), MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit), -moT2, acCredit, acParent); TransactionHelper t6(QDate::currentDate().addDays(-3), MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit), -moT2, acCredit, acParent); a.setForecastMethod(1); a.setForecastDays(3); a.setAccountsCycle(1); a.setForecastCycles(1); a.setBeginForecastDay(0); a.setHistoryMethod(0); //moving average a.doForecast(); //check forecast b_credit = file->balance(a_credit.id(), QDate::currentDate()); MyMoneyMoney b_credit_1_exp = (b_credit + ((moT2 - moT1))); MyMoneyMoney b_credit_2 = a.forecastBalance(a_credit, QDate::currentDate().addDays(2)); MyMoneyMoney b_credit_2_exp = (b_credit + ((moT2 - moT1) * 2)); QVERIFY(a.forecastBalance(a_credit, QDate::currentDate()) == file->balance(a_credit.id(), QDate::currentDate())); QVERIFY(a.forecastBalance(a_credit, QDate::currentDate().addDays(1)) == b_credit + (moT2 - moT1)); QVERIFY(a.forecastBalance(a_credit, QDate::currentDate().addDays(2)) == b_credit + ((moT2 - moT1)*2)); QVERIFY(a.forecastBalance(a_credit, QDate::currentDate().addDays(3)) == b_credit + ((moT2 - moT1)*3)); //test weighted moving average a.setForecastMethod(1); a.setForecastDays(3); a.setAccountsCycle(1); a.setForecastCycles(3); a.setBeginForecastDay(0); a.setHistoryMethod(1); a.doForecast(); QVERIFY(a.forecastBalance(a_credit, 0) == b_credit); QVERIFY(a.forecastBalance(a_credit, QDate::currentDate().addDays(1)) == (b_credit + (((moT2 - moT1)*3 + moT2*2 + moT2) / MyMoneyMoney(6, 1)))); } void MyMoneyForecastTest::testGetForecastAccountList() { MyMoneyForecast a; MyMoneyAccount a_checking = file->account(acChecking); MyMoneyAccount a_parent = file->account(acParent); QList b; b = a.forecastAccountList(); //check that it contains asset account, but not expense accounts QVERIFY(b.contains(a_checking)); QVERIFY(!b.contains(a_parent)); } void MyMoneyForecastTest::testCalculateAccountTrend() { //set up environment TransactionHelper t1(QDate::currentDate().addDays(-3), MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit), -moT2, acChecking, acSolo); MyMoneyAccount a_checking = file->account(acChecking); //test invalid arguments try { MyMoneyForecast::calculateAccountTrend(a_checking, 0); } catch (const MyMoneyException &e) { QVERIFY(QString::fromLatin1(e.what()).startsWith("Illegal arguments when calling calculateAccountTrend. trendDays must be higher than 0")); } try { MyMoneyForecast::calculateAccountTrend(a_checking, -10); } catch (const MyMoneyException &e) { QVERIFY(QString::fromLatin1(e.what()).startsWith("Illegal arguments when calling calculateAccountTrend. trendDays must be higher than 0")); } //test that it calculates correctly QVERIFY(MyMoneyForecast::calculateAccountTrend(a_checking , 3) == moT2 / MyMoneyMoney(3, 1)); //test that it works for all kind of accounts MyMoneyAccount a_solo = file->account(acSolo); MyMoneyMoney soloTrend = MyMoneyForecast::calculateAccountTrend(a_solo, 3); MyMoneyMoney soloTrendExp = -moT2 / MyMoneyMoney(3, 1); QVERIFY(MyMoneyForecast::calculateAccountTrend(a_solo, 3) == -moT2 / MyMoneyMoney(3, 1)); //test that it does not take into account the transactions of the opening date of the account MyMoneyAccount a_cash = file->account(acCash); TransactionHelper t2(QDate::currentDate().addDays(-2), MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit), moT2, acCash, acParent); TransactionHelper t3(QDate::currentDate().addDays(-1), MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit), moT1, acCash, acParent); QVERIFY(MyMoneyForecast::calculateAccountTrend(a_cash, 3) == -moT1); } void MyMoneyForecastTest::testGetForecastBalance() { //set up environment MyMoneyForecast a; TransactionHelper t1(QDate::currentDate().addDays(-1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), this->moT1, acChecking, acSolo); TransactionHelper t2(QDate::currentDate().addDays(-1), MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit), -(this->moT2), acCredit, acParent); TransactionHelper t3(QDate::currentDate().addDays(-1), MyMoneySplit::actionName(eMyMoney::Split::Action::Transfer), this->moT1, acCredit, acChecking); a.setForecastMethod(1); a.setForecastDays(3); a.setAccountsCycle(1); a.setForecastCycles(1); a.setHistoryMethod(0); a.doForecast(); MyMoneyAccount a_checking = file->account(acChecking); MyMoneyAccount a_credit = file->account(acCredit); //test invalid arguments QVERIFY(a.forecastBalance(a_checking, QDate::currentDate().addDays(-1)) == MyMoneyMoney()); QVERIFY(a.forecastBalance(a_checking, QDate::currentDate().addDays(-10)) == MyMoneyMoney()); QVERIFY(a.forecastBalance(a_checking, -1) == MyMoneyMoney()); QVERIFY(a.forecastBalance(a_checking, -100) == MyMoneyMoney()); //test a date outside the forecast days QVERIFY(a.forecastBalance(a_checking, QDate::currentDate().addDays(4)) == MyMoneyMoney()); QVERIFY(a.forecastBalance(a_checking, 4) == MyMoneyMoney()); QVERIFY(a.forecastBalance(a_checking, QDate::currentDate().addDays(10)) == MyMoneyMoney()); QVERIFY(a.forecastBalance(a_checking, 10) == MyMoneyMoney()); //test it returns valid results MyMoneyMoney b_credit = file->balance(a_credit.id(), QDate::currentDate()); QVERIFY(a.forecastBalance(a_credit, QDate::currentDate()) == file->balance(a_credit.id(), QDate::currentDate())); QVERIFY(a.forecastBalance(a_credit, QDate::currentDate().addDays(1)) == b_credit + (moT2 - moT1)); QVERIFY(a.forecastBalance(a_credit, QDate::currentDate().addDays(2)) == b_credit + ((moT2 - moT1)*2)); QVERIFY(a.forecastBalance(a_credit, QDate::currentDate().addDays(3)) == b_credit + ((moT2 - moT1)*3)); } void MyMoneyForecastTest::testIsForecastAccount() { MyMoneyForecast a; MyMoneyAccount a_checking = file->account(acChecking); MyMoneyAccount a_solo = file->account(acSolo); MyMoneyAccount a_investment = file->account(acInvestment); //test an invalid account QVERIFY(a.isForecastAccount(a_solo) == false); QVERIFY(a.isForecastAccount(a_investment) == true); //test a valid account QVERIFY(a.isForecastAccount(a_checking) == true); } void MyMoneyForecastTest::testDoFutureScheduledForecast() { //set up future transactions MyMoneyForecast a; MyMoneyAccount a_cash = file->account(acCash); TransactionHelper t1(QDate::currentDate().addDays(1), MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit), -moT1, acCash, acParent); TransactionHelper t2(QDate::currentDate().addDays(2), MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit), -moT2, acCash, acParent); TransactionHelper t3(QDate::currentDate().addDays(3), MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit), -moT3, acCash, acParent); TransactionHelper t4(QDate::currentDate().addDays(10), MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit), -moT4, acCash, acParent); a.setForecastMethod(0); a.setForecastDays(3); a.setAccountsCycle(1); a.setForecastCycles(1); a.doForecast(); MyMoneyMoney b_cash = file->balance(a_cash.id(), QDate::currentDate()); //test valid results QVERIFY(a.forecastBalance(a_cash, QDate::currentDate()) == b_cash); QVERIFY(a.forecastBalance(a_cash, QDate::currentDate().addDays(1)) == b_cash + moT1); QVERIFY(a.forecastBalance(a_cash, QDate::currentDate().addDays(2)) == b_cash + moT1 + moT2); QVERIFY(a.forecastBalance(a_cash, QDate::currentDate().addDays(3)) == b_cash + moT1 + moT2 + moT3); } void MyMoneyForecastTest::testScheduleForecast() { //set up schedule environment for testing MyMoneyAccount a_cash = file->account(acCash); MyMoneyAccount a_parent = file->account(acParent); MyMoneyFileTransaction ft; MyMoneySchedule sch("A Name", Schedule::Type::Bill, Schedule::Occurrence::Weekly, 1, Schedule::PaymentType::DirectDebit, QDate::currentDate().addDays(1), QDate(), true, true); MyMoneyTransaction t; t.setPostDate(QDate::currentDate().addDays(1)); t.setEntryDate(QDate::currentDate().addDays(1)); //t.setId("T000000000000000001"); t.setBankID("BID"); t.setMemo("Wohnung:Miete"); t.setCommodity("USD"); t.setValue("key", "value"); MyMoneySplit s; s.setPayeeId("P000001"); s.setShares(moT2); s.setValue(moT2); s.setAccountId(a_parent.id()); s.setBankID("SPID1"); s.setReconcileFlag(eMyMoney::Split::State::Reconciled); t.addSplit(s); s.setPayeeId("P000001"); s.setShares(-moT2); s.setValue(-moT2); s.setAccountId(a_cash.id()); s.setBankID("SPID2"); s.setReconcileFlag(eMyMoney::Split::State::Cleared); s.clearId(); t.addSplit(s); sch.setTransaction(t); file->addSchedule(sch); ft.commit(); MyMoneyFileTransaction ft3; MyMoneySchedule sch3("A Name1", Schedule::Type::Bill, Schedule::Occurrence::Weekly, 1, Schedule::PaymentType::DirectDebit, QDate::currentDate().addDays(5), QDate(), true, true); //sch.setLastPayment(QDate::currentDate()); //sch.recordPayment(QDate::currentDate().addDays(1)); //sch.setId("SCH0001"); MyMoneyTransaction t3; t3.setPostDate(QDate::currentDate().addDays(5)); t3.setEntryDate(QDate::currentDate().addDays(5)); //t.setId("T000000000000000001"); t3.setBankID("BID"); t3.setMemo("Wohnung:Miete"); t3.setCommodity("USD"); t3.setValue("key", "value"); MyMoneySplit s3; s3.setPayeeId("P000001"); s3.setShares(moT2); s3.setValue(moT2); s3.setAccountId(a_parent.id()); s3.setBankID("SPID1"); s3.setReconcileFlag(eMyMoney::Split::State::Reconciled); t3.addSplit(s3); s3.setPayeeId("P000001"); s3.setShares(-moT2); s3.setValue(-moT2); s3.setAccountId(a_cash.id()); s3.setBankID("SPID2"); s3.setReconcileFlag(eMyMoney::Split::State::Cleared); s3.clearId(); t3.addSplit(s3); sch3.setTransaction(t3); file->addSchedule(sch3); ft3.commit(); MyMoneyFileTransaction ft2; MyMoneySchedule sch2("A Name2", Schedule::Type::Bill, Schedule::Occurrence::Weekly, 1, Schedule::PaymentType::DirectDebit, QDate::currentDate().addDays(2), QDate(), true, true); //sch.setLastPayment(QDate::currentDate()); //sch.recordPayment(QDate::currentDate().addDays(1)); //sch.setId("SCH0001"); MyMoneyTransaction t2; t2.setPostDate(QDate::currentDate().addDays(2)); t2.setEntryDate(QDate::currentDate().addDays(2)); //t.setId("T000000000000000001"); t2.setBankID("BID"); t2.setMemo("Wohnung:Miete"); t2.setCommodity("USD"); t2.setValue("key", "value"); MyMoneySplit s2; s2.setPayeeId("P000001"); s2.setShares(moT1); s2.setValue(moT1); s2.setAccountId(a_parent.id()); s2.setBankID("SPID1"); s2.setReconcileFlag(eMyMoney::Split::State::Reconciled); t2.addSplit(s2); s2.setPayeeId("P000001"); s2.setShares(-moT1); s2.setValue(-moT1); s2.setAccountId(a_cash.id()); s2.setBankID("SPID2"); s2.setReconcileFlag(eMyMoney::Split::State::Cleared); s2.clearId(); t2.addSplit(s2); sch2.setTransaction(t2); file->addSchedule(sch2); ft2.commit(); //run forecast MyMoneyForecast a; a.setForecastMethod(0); a.setForecastDays(3); a.setAccountsCycle(1); a.setForecastCycles(1); a.doForecast(); //check result for single schedule MyMoneyMoney b_cash = file->balance(a_cash.id(), QDate::currentDate()); MyMoneyMoney b_cash1 = a.forecastBalance(a_cash, QDate::currentDate().addDays(1)); //test valid results QVERIFY(a.forecastBalance(a_cash, QDate::currentDate()) == b_cash); QVERIFY(a.forecastBalance(a_cash, QDate::currentDate().addDays(1)) == b_cash - moT2); QVERIFY(a.forecastBalance(a_cash, QDate::currentDate().addDays(2)) == b_cash - moT2 - moT1); } void MyMoneyForecastTest::testDaysToMinimumBalance() { //setup environment MyMoneyForecast a; MyMoneyAccount a_cash = file->account(acCash); MyMoneyAccount a_credit = file->account(acCredit); MyMoneyAccount a_parent = file->account(acParent); a_cash.setValue("minBalanceAbsolute", "50"); a_credit.setValue("minBalanceAbsolute", "50"); TransactionHelper t1(QDate::currentDate().addDays(-1), MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit), -moT1, acCash, acParent); TransactionHelper t2(QDate::currentDate().addDays(2), MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit), moT2, acCash, acParent); TransactionHelper t3(QDate::currentDate().addDays(-1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), -moT1, acCredit, acParent); TransactionHelper t4(QDate::currentDate().addDays(4), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moT5, acCredit, acParent); a.setForecastMethod(0); a.setForecastDays(3); a.setAccountsCycle(1); a.setForecastCycles(1); a.setBeginForecastDay(0); a.doForecast(); //test invalid arguments MyMoneyAccount nullAcc; QVERIFY(a.daysToMinimumBalance(nullAcc) == -1); //test when not a forecast account QVERIFY(a.daysToMinimumBalance(a_parent) == -1); //test it warns when inside the forecast period QVERIFY(a.daysToMinimumBalance(a_cash) == 2); //test it does not warn when it will be outside of the forecast period QVERIFY(a.daysToMinimumBalance(a_credit) == -1); } void MyMoneyForecastTest::testDaysToZeroBalance() { //set up environment MyMoneyAccount a_Solo = file->account(acSolo); MyMoneyAccount a_Cash = file->account(acCash); MyMoneyAccount a_Credit = file->account(acCredit); //MyMoneyFileTransaction ft; TransactionHelper t1(QDate::currentDate().addDays(2), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), -moT1, acChecking, acSolo); TransactionHelper t2(QDate::currentDate().addDays(2), MyMoneySplit::actionName(eMyMoney::Split::Action::Transfer), (moT5), acCash, acCredit); TransactionHelper t3(QDate::currentDate().addDays(2), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), (moT5*100), acCredit, acParent); //ft.commit(); MyMoneyForecast a; a.setForecastMethod(0); a.setForecastDays(30); a.setAccountsCycle(1); a.setForecastCycles(3); a.doForecast(); //test invalid arguments MyMoneyAccount nullAcc; try { auto days = a.daysToZeroBalance(nullAcc); Q_UNUSED(days) } catch (const MyMoneyException &) { QFAIL("Unexpected exception"); } //test when not a forecast account MyMoneyAccount a_solo = file->account(acSolo); auto iSolo = a.daysToZeroBalance(a_Solo); QVERIFY(iSolo == -2); //test it warns when inside the forecast period MyMoneyMoney fCash = a.forecastBalance(a_Cash, QDate::currentDate().addDays(2)); QVERIFY(a.daysToZeroBalance(a_Cash) == 2); //test it does not warn when it will be outside of the forecast period } void MyMoneyForecastTest::testSkipOpeningDate() { //set up environment MyMoneyForecast a; TransactionHelper t1(QDate::currentDate().addDays(-2), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), this->moT1, acCash, acSolo); TransactionHelper t2(QDate::currentDate().addDays(-1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), this->moT2, acCash, acSolo); a.setForecastMethod(1); a.setForecastDays(3); a.setAccountsCycle(2); a.setForecastCycles(1); a.setHistoryMethod(0); a.doForecast(); MyMoneyAccount a_cash = file->account(acCash); //test it has no variation because it skipped the variation of the opening date MyMoneyMoney b_cash = file->balance(a_cash.id(), QDate::currentDate()); QVERIFY(a.skipOpeningDate() == true); QVERIFY(a.forecastBalance(a_cash, QDate::currentDate()) == b_cash); QVERIFY(a.forecastBalance(a_cash, QDate::currentDate().addDays(1)) == b_cash); QVERIFY(a.forecastBalance(a_cash, QDate::currentDate().addDays(2)) == b_cash - moT2); QVERIFY(a.forecastBalance(a_cash, QDate::currentDate().addDays(3)) == b_cash - moT2); } void MyMoneyForecastTest::testAccountMinimumBalanceDateList() { //set up environment MyMoneyForecast a; TransactionHelper t1(QDate::currentDate().addDays(-2), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), this->moT1, acCash, acSolo); TransactionHelper t2(QDate::currentDate().addDays(-1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), this->moT2, acCash, acSolo); a.setForecastMethod(1); a.setForecastDays(6); a.setAccountsCycle(2); a.setForecastCycles(3); a.setHistoryMethod(0); a.setBeginForecastDay(QDate::currentDate().addDays(1).day()); a.doForecast(); MyMoneyAccount a_cash = file->account(acCash); //test QList dateList; dateList = a.accountMinimumBalanceDateList(a_cash); QList::iterator it = dateList.begin(); QDate minDate = *it; QVERIFY(minDate == QDate::currentDate().addDays(2)); it++; minDate = *it; QVERIFY(minDate == QDate::currentDate().addDays(4)); it++; minDate = *it; QVERIFY(minDate == QDate::currentDate().addDays(6)); } void MyMoneyForecastTest::testAccountMaximumBalanceDateList() { //set up environment MyMoneyForecast a; TransactionHelper t1(QDate::currentDate().addDays(-2), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), this->moT1, acCash, acSolo); TransactionHelper t2(QDate::currentDate().addDays(-1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), this->moT2, acCash, acSolo); a.setForecastMethod(1); a.setForecastDays(6); a.setAccountsCycle(2); a.setForecastCycles(3); a.setHistoryMethod(0); a.setBeginForecastDay(QDate::currentDate().addDays(1).day()); a.doForecast(); MyMoneyAccount a_cash = file->account(acCash); //test QList dateList; dateList = a.accountMaximumBalanceDateList(a_cash); QList::iterator it = dateList.begin(); QDate maxDate = *it; QVERIFY(maxDate == QDate::currentDate().addDays(1)); it++; maxDate = *it; QVERIFY(maxDate == QDate::currentDate().addDays(3)); it++; maxDate = *it; QVERIFY(maxDate == QDate::currentDate().addDays(5)); } void MyMoneyForecastTest::testAccountAverageBalance() { //set up environment MyMoneyForecast a; TransactionHelper t1(QDate::currentDate().addDays(-2), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), this->moT1, acCash, acSolo); TransactionHelper t2(QDate::currentDate().addDays(-1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), this->moT2, acCash, acSolo); a.setForecastMethod(1); a.setForecastDays(3); a.setAccountsCycle(2); a.setForecastCycles(1); a.setBeginForecastDay(0); a.doForecast(); MyMoneyAccount a_cash = file->account(acCash); //test MyMoneyMoney b_cash1 = a.forecastBalance(a_cash, QDate::currentDate().addDays(1)); MyMoneyMoney b_cash2 = a.forecastBalance(a_cash, QDate::currentDate().addDays(2)); MyMoneyMoney b_cash3 = a.forecastBalance(a_cash, QDate::currentDate().addDays(3)); MyMoneyMoney average = (b_cash1 + b_cash2 + b_cash3) / MyMoneyMoney(3, 1); QVERIFY(a.accountAverageBalance(a_cash) == average); } void MyMoneyForecastTest::testBeginForecastDate() { //set up environment MyMoneyForecast a; QDate beginDate; qint64 beginDay; a.setForecastMethod(1); a.setForecastDays(90); a.setAccountsCycle(14); a.setForecastCycles(3); a.setBeginForecastDay(0); a.doForecast(); //test when using old method without begin day QVERIFY(QDate::currentDate() == a.beginForecastDate()); //setup begin to last day of month a.setBeginForecastDay(31); beginDay = a.beginForecastDay(); a.doForecast(); //test if (QDate::currentDate().day() < beginDay) { if (QDate::currentDate().daysInMonth() < beginDay) beginDay = QDate::currentDate().daysInMonth(); beginDate = QDate(QDate::currentDate().year(), QDate::currentDate().month(), beginDay); QVERIFY(beginDate == a.beginForecastDate()); } //setup begin day to same date a.setBeginForecastDay(QDate::currentDate().day()); beginDay = a.beginForecastDay(); a.doForecast(); QVERIFY(QDate::currentDate() == a.beginForecastDate()); //setup to first day of month with small interval a.setBeginForecastDay(1); a.setAccountsCycle(1); beginDay = a.beginForecastDay(); a.doForecast(); //test if (QDate::currentDate() == a.beginForecastDate()) { QVERIFY(QDate::currentDate() == a.beginForecastDate()); } else { beginDay = ((((QDate::currentDate().day() - beginDay) / a.accountsCycle()) + 1) * a.accountsCycle()) + beginDay; if (beginDay > QDate::currentDate().daysInMonth()) beginDay = QDate::currentDate().daysInMonth(); beginDate = QDate(QDate::currentDate().year(), QDate::currentDate().month(), beginDay); if (QDate::currentDate().day() == QDate::currentDate().daysInMonth()) { std::cout << std::endl << "testBeginForecastDate(): test of first day of month with small interval skipped because it is the last day of month" << std::endl; } else { QVERIFY(beginDate == a.beginForecastDate()); } } //setup to test when current date plus cycle equals begin day a.setAccountsCycle(14); beginDay = QDate::currentDate().addDays(14).day(); a.setBeginForecastDay(beginDay); beginDate = QDate::currentDate().addDays(14); a.doForecast(); //test QVERIFY(beginDate == a.beginForecastDate()); //setup to test when the begin day will be next month a.setBeginForecastDay(1); a.setAccountsCycle(40); a.doForecast(); beginDate = QDate(QDate::currentDate().addMonths(1).year(), QDate::currentDate().addMonths(1).month(), 1); //test if (QDate::currentDate().day() > 1) { QVERIFY(beginDate == a.beginForecastDate()); } else { //test is not valid if today is 1st of month std::cout << std::endl << "testBeginForecastDate(): test of first day of month skipped because current day is 1st of month" << std::endl; } } void MyMoneyForecastTest::testHistoryDays() { MyMoneyForecast a; QVERIFY(a.historyStartDate() == QDate::currentDate().addDays(-a.forecastCycles()*a.accountsCycle())); QVERIFY(a.historyEndDate() == QDate::currentDate().addDays(-1)); QVERIFY(a.historyDays() == a.forecastCycles()*a.accountsCycle()); a.setForecastMethod(1); a.setForecastDays(90); a.setAccountsCycle(14); a.setForecastCycles(3); a.setBeginForecastDay(0); a.doForecast(); QVERIFY(a.historyStartDate() == QDate::currentDate().addDays(-14*3)); QVERIFY(a.historyDays() == (14*3)); QVERIFY(a.historyEndDate() == (QDate::currentDate().addDays(-1))); } void MyMoneyForecastTest::testCreateBudget() { //set up environment MyMoneyForecast a; MyMoneyForecast b; MyMoneyBudget budget; TransactionHelper t1(QDate(2005, 1, 3), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), this->moT1, acCash, acSolo); TransactionHelper t2(QDate(2005, 1, 15), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), this->moT2, acCash, acParent); TransactionHelper t3(QDate(2005, 1, 30), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), this->moT3, acCash, acSolo); TransactionHelper t4(QDate(2006, 1, 25), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), this->moT4, acCash, acParent); TransactionHelper t5(QDate(2005, 4, 3), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), this->moT1, acCash, acSolo); TransactionHelper t6(QDate(2006, 5, 15), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), this->moT2, acCash, acParent); TransactionHelper t7(QDate(2005, 8, 3), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), this->moT3, acCash, acSolo); TransactionHelper t8(QDate(2006, 9, 15), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), this->moT4, acCash, acParent); a.setHistoryMethod(0); a.setForecastMethod(1); a.createBudget(budget, QDate(2005, 1, 1), QDate(2006, 12, 31), QDate(2007, 1, 1), QDate(2007, 12, 31), true); //test MyMoneyAccount a_solo = file->account(acSolo); MyMoneyAccount a_parent = file->account(acParent); //test it has no variation because it skipped the variation of the opening date QVERIFY(a.forecastBalance(a_solo, QDate(2007, 1, 1)) == ((moT1 + moT3) / MyMoneyMoney(2, 1))); QVERIFY(a.forecastBalance(a_parent, QDate(2007, 1, 1)) == ((moT2 + moT4) / MyMoneyMoney(2, 1))); QVERIFY(a.forecastBalance(a_solo, QDate(2007, 4, 1)) == ((moT1) / MyMoneyMoney(2, 1))); QVERIFY(a.forecastBalance(a_parent, QDate(2007, 5, 1)) == ((moT2) / MyMoneyMoney(2, 1))); QVERIFY(a.forecastBalance(a_solo, QDate(2007, 8, 1)) == ((moT3) / MyMoneyMoney(2, 1))); QVERIFY(a.forecastBalance(a_parent, QDate(2007, 9, 1)) == ((moT4) / MyMoneyMoney(2, 1))); //test the budget object returned by the method QVERIFY(budget.account(a_parent.id()).period(QDate(2007, 9, 1)).amount() == ((moT4) / MyMoneyMoney(2, 1))); //setup test for a length lower than a year b.setForecastMethod(1); b.setHistoryMethod(0); b.createBudget(budget, QDate(2005, 1, 1), QDate(2005, 6, 30), QDate(2007, 1, 1), QDate(2007, 6, 30), true); //test QVERIFY(b.forecastBalance(a_solo, QDate(2007, 1, 1)) == (moT1 + moT3)); QVERIFY(b.forecastBalance(a_parent, QDate(2007, 1, 1)) == (moT2)); QVERIFY(b.forecastBalance(a_solo, QDate(2007, 4, 1)) == (moT1)); QVERIFY(b.forecastBalance(a_parent, QDate(2007, 5, 1)) == (MyMoneyMoney())); //set up schedule environment for testing MyMoneyAccount a_cash = file->account(acCash); MyMoneyFileTransaction ft; MyMoneySchedule sch("A Name", Schedule::Type::Bill, Schedule::Occurrence::Monthly, 1, Schedule::PaymentType::DirectDebit, QDate::currentDate(), QDate(), true, true); MyMoneyTransaction t10; t10.setPostDate(QDate::currentDate().addMonths(1)); t10.setEntryDate(QDate::currentDate().addMonths(1)); //t.setId("T000000000000000001"); t10.setBankID("BID"); t10.setMemo("Wohnung:Miete"); t10.setCommodity("USD"); t10.setValue("key", "value"); MyMoneySplit s; s.setPayeeId("P000001"); s.setShares(moT2); s.setValue(moT2); s.setAccountId(a_parent.id()); s.setBankID("SPID1"); s.setReconcileFlag(eMyMoney::Split::State::Reconciled); t10.addSplit(s); s.setPayeeId("P000001"); s.setShares(-moT2); s.setValue(-moT2); s.setAccountId(a_cash.id()); s.setBankID("SPID2"); s.setReconcileFlag(eMyMoney::Split::State::Cleared); s.clearId(); t10.addSplit(s); sch.setTransaction(t10); file->addSchedule(sch); ft.commit(); //run forecast MyMoneyForecast c; c.setForecastMethod(0); c.setForecastCycles(1); c.createBudget(budget, QDate::currentDate().addYears(-2), QDate::currentDate().addYears(-1), QDate::currentDate().addMonths(-2), QDate::currentDate().addMonths(6), true); MyMoneyMoney c_parent = c.forecastBalance(a_parent, QDate(QDate::currentDate().addMonths(1).year(), QDate::currentDate().addMonths(1).month(), 1)); //test valid results QVERIFY(c.forecastBalance(a_parent, QDate(QDate::currentDate().addMonths(1).year(), QDate::currentDate().addMonths(1).month(), 1)) == (moT2)); } void MyMoneyForecastTest::testLinearRegression() { //set up environment MyMoneyForecast a; MyMoneyAccount a_checking = file->account(acChecking); MyMoneyAccount a_credit = file->account(acCredit); //setup some transactions TransactionHelper t1(QDate::currentDate().addDays(-1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), this->moT1, acChecking, acSolo); TransactionHelper t2(QDate::currentDate().addDays(-1), MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit), -(this->moT2), acCredit, acParent); TransactionHelper t3(QDate::currentDate().addDays(-1), MyMoneySplit::actionName(eMyMoney::Split::Action::Transfer), this->moT1, acCredit, acChecking); //TODO Add tests specific for linear regression } diff --git a/kmymoney/plugins/views/CMakeLists.txt b/kmymoney/plugins/views/CMakeLists.txt index 2093eaa71..7ef97e671 100644 --- a/kmymoney/plugins/views/CMakeLists.txt +++ b/kmymoney/plugins/views/CMakeLists.txt @@ -1,15 +1,15 @@ if(ENABLE_FORECASTVIEW) add_subdirectory(forecast) endif() -if(ENABLE_REPORTSVIEW) +if(ENABLE_REPORTSVIEW OR BUILD_TESTING) add_subdirectory(reports) endif() if(ENABLE_BUDGETVIEW) add_subdirectory(budget) endif() if(ENABLE_ONLINEJOBOUTBOXVIEW) add_subdirectory(onlinejoboutbox) endif() diff --git a/kmymoney/plugins/views/forecast/CMakeLists.txt b/kmymoney/plugins/views/forecast/CMakeLists.txt index 8e0dab202..653b813e3 100644 --- a/kmymoney/plugins/views/forecast/CMakeLists.txt +++ b/kmymoney/plugins/views/forecast/CMakeLists.txt @@ -1,62 +1,62 @@ configure_file(${CMAKE_CURRENT_SOURCE_DIR}/forecastview.json.in ${CMAKE_CURRENT_BINARY_DIR}/forecastview.json @ONLY) set(forecastview_SOURCES forecastview.cpp kforecastview.cpp fixedcolumntreeview.cpp ) ki18n_wrap_ui(forecastview_SOURCES kforecastview.ui) kconfig_add_kcfg_files(forecastview_SOURCES forecastviewsettings.kcfgc) kcoreaddons_add_plugin(forecastview SOURCES ${forecastview_SOURCES} JSON "${CMAKE_CURRENT_BINARY_DIR}/forecastview.json" INSTALL_NAMESPACE "kmymoney") #kcoreaddons_add_plugin sets LIBRARY_OUTPUT_DIRECTORY to ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/${INSTALL_NAMESPACE} set_target_properties(forecastview PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}") target_link_libraries(forecastview PUBLIC kmm_plugin KF5::TextWidgets - reports kmm_widgets + kmymoney_common ) # the KCM module set(kcm_forecastview_PART_SRCS kcm_forecastview.cpp ) kconfig_add_kcfg_files(kcm_forecastview_PART_SRCS forecastviewsettings.kcfgc) ki18n_wrap_ui(kcm_forecastview_PART_SRCS forecastviewsettings.ui) kcoreaddons_add_plugin(kcm_forecastview SOURCES ${kcm_forecastview_PART_SRCS} JSON "${CMAKE_CURRENT_BINARY_DIR}/kcm_forecastview.json" INSTALL_NAMESPACE "kmymoney") kcoreaddons_desktop_to_json(kcm_forecastview kcm_forecastview.desktop) #kcoreaddons_add_plugin sets LIBRARY_OUTPUT_DIRECTORY to ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/${INSTALL_NAMESPACE} set_target_properties(kcm_forecastview PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}") target_link_libraries(kcm_forecastview KF5::I18n KF5::ConfigWidgets KF5::Completion KF5::KIOWidgets KF5::CoreAddons kmm_settings ) install(FILES kcm_forecastview.desktop DESTINATION "${SERVICES_INSTALL_DIR}") diff --git a/kmymoney/plugins/views/forecast/kforecastview_p.h b/kmymoney/plugins/views/forecast/kforecastview_p.h index 08113b707..e9dca99ed 100644 --- a/kmymoney/plugins/views/forecast/kforecastview_p.h +++ b/kmymoney/plugins/views/forecast/kforecastview_p.h @@ -1,1046 +1,1028 @@ /*************************************************************************** kforecastview.cpp ------------------- copyright : (C) 2007 by Alvaro Soliverez email : asoliverez@gmail.com (C) 2017 Łukasz Wojniłowicz ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #ifndef KFORECASTVIEW_P_H #define KFORECASTVIEW_P_H #include "kforecastview.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "ui_kforecastview.h" #include "forecastviewsettings.h" #include "kmymoneyviewbase_p.h" #include "mymoneymoney.h" #include "mymoneyforecast.h" #include "mymoneyprice.h" #include "mymoneyutils.h" #include "mymoneyfile.h" #include "mymoneyaccount.h" #include "mymoneyexception.h" #include "mymoneysecurity.h" #include "kmymoneysettings.h" #include "mymoneybudget.h" -#include "pivottable.h" #include "fixedcolumntreeview.h" -#include "kreportchartview.h" -#include "reportaccount.h" #include "icons.h" #include "mymoneyenums.h" #include "kmymoneyutils.h" +#include "kmymoneyplugin.h" +#include "plugins/views/reports/reportsviewenums.h" -using namespace reports; using namespace Icons; typedef enum { SummaryView = 0, ListView, AdvancedView, BudgetView, ChartView, // insert new values above this line MaxViewTabs } ForecastViewTab; enum ForecastViewRoles { ForecastRole = Qt::UserRole, /**< The forecast is held in this role.*/ AccountRole = Qt::UserRole + 1, /**< The MyMoneyAccount is stored in this role in column 0.*/ AmountRole = Qt::UserRole + 2, /**< The amount.*/ ValueRole = Qt::UserRole + 3, /**< The value.*/ }; enum EForecastViewType { eSummary = 0, eDetailed, eAdvanced, eBudget, eUndefined }; class KForecastViewPrivate : public KMyMoneyViewBasePrivate { Q_DECLARE_PUBLIC(KForecastView) public: explicit KForecastViewPrivate(KForecastView *qq) : KMyMoneyViewBasePrivate(), q_ptr(qq), ui(new Ui::KForecastView), m_needLoad(true), m_totalItem(0), m_assetItem(0), m_liabilityItem(0), m_incomeItem(0), m_expenseItem(0), m_chartLayout(0), - m_forecastChart(0) + m_forecastChart(nullptr) { } ~KForecastViewPrivate() { delete ui; } void init() { Q_Q(KForecastView); m_needLoad = false; ui->setupUi(q); - m_forecastChart = new KReportChartView(ui->m_tabChart); - for (int i = 0; i < MaxViewTabs; ++i) m_needReload[i] = false; KSharedConfigPtr config = KSharedConfig::openConfig(); KConfigGroup grp = config->group("Last Use Settings"); ui->m_tab->setCurrentIndex(grp.readEntry("KForecastView_LastType", 0)); ui->m_forecastButton->setIcon(Icons::get(Icon::ViewForecast)); q->connect(ui->m_tab, &QTabWidget::currentChanged, q, &KForecastView::slotTabChanged); q->connect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, q, &KForecastView::refresh); q->connect(ui->m_forecastButton, &QAbstractButton::clicked, q, &KForecastView::slotManualForecast); ui->m_forecastList->setUniformRowHeights(true); ui->m_forecastList->setAllColumnsShowFocus(true); ui->m_summaryList->setAllColumnsShowFocus(true); ui->m_budgetList->setAllColumnsShowFocus(true); ui->m_advancedList->setAlternatingRowColors(true); q->connect(ui->m_forecastList, &QTreeWidget::itemExpanded, q, &KForecastView::itemExpanded); q->connect(ui->m_forecastList, &QTreeWidget::itemCollapsed, q, &KForecastView::itemCollapsed); q->connect(ui->m_summaryList, &QTreeWidget::itemExpanded, q, &KForecastView::itemExpanded); q->connect(ui->m_summaryList, &QTreeWidget::itemCollapsed, q, &KForecastView::itemCollapsed); q->connect(ui->m_budgetList, &QTreeWidget::itemExpanded, q, &KForecastView::itemExpanded); q->connect(ui->m_budgetList, &QTreeWidget::itemCollapsed, q, &KForecastView::itemCollapsed); - m_forecastChart->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); m_chartLayout = ui->m_tabChart->layout(); m_chartLayout->setSpacing(6); - m_chartLayout->addWidget(m_forecastChart); loadForecastSettings(); } void loadForecast(ForecastViewTab tab) { if (m_needReload[tab]) { switch (tab) { case ListView: loadListView(); break; case SummaryView: loadSummaryView(); break; case AdvancedView: loadAdvancedView(); break; case BudgetView: loadBudgetView(); break; case ChartView: loadChartView(); break; default: break; } m_needReload[tab] = false; } } void loadListView() { MyMoneyForecast forecast = KMyMoneyUtils::forecast(); const auto file = MyMoneyFile::instance(); //get the settings from current page forecast.setForecastDays(ui->m_forecastDays->value()); forecast.setAccountsCycle(ui->m_accountsCycle->value()); forecast.setBeginForecastDay(ui->m_beginDay->value()); forecast.setForecastCycles(ui->m_forecastCycles->value()); forecast.setHistoryMethod(ui->m_historyMethod->checkedId()); forecast.doForecast(); ui->m_forecastList->clear(); ui->m_forecastList->setColumnCount(0); ui->m_forecastList->setIconSize(QSize(22, 22)); ui->m_forecastList->setSortingEnabled(true); ui->m_forecastList->sortByColumn(0, Qt::AscendingOrder); //add columns QStringList headerLabels; headerLabels << i18n("Account"); //add cycle interval columns headerLabels << i18nc("Today's forecast", "Current"); for (int i = 1; i <= forecast.forecastDays(); ++i) { QDate forecastDate = QDate::currentDate().addDays(i); headerLabels << QLocale().toString(forecastDate, QLocale::LongFormat); } //add variation columns headerLabels << i18n("Total variation"); //set the columns ui->m_forecastList->setHeaderLabels(headerLabels); //add default rows addTotalRow(ui->m_forecastList, forecast); addAssetLiabilityRows(forecast); //load asset and liability forecast accounts loadAccounts(forecast, file->asset(), m_assetItem, eDetailed); loadAccounts(forecast, file->liability(), m_liabilityItem, eDetailed); adjustHeadersAndResizeToContents(ui->m_forecastList); // add the fixed column only if the horizontal scroll bar is visible m_fixedColumnView.reset(ui->m_forecastList->horizontalScrollBar()->isVisible() ? new FixedColumnTreeView(ui->m_forecastList) : 0); } void loadAccounts(MyMoneyForecast& forecast, const MyMoneyAccount& account, QTreeWidgetItem* parentItem, int forecastType) { QMap nameIdx; const auto file = MyMoneyFile::instance(); QTreeWidgetItem *forecastItem = 0; //Get all accounts of the right type to calculate forecast const auto accList = account.accountList(); if (accList.isEmpty()) return; foreach (const auto sAccount, accList) { auto subAccount = file->account(sAccount); //only add the account if it is a forecast account or the parent of a forecast account if (includeAccount(forecast, subAccount)) { nameIdx[subAccount.id()] = subAccount.id(); } } QMap::ConstIterator it_nc; for (it_nc = nameIdx.constBegin(); it_nc != nameIdx.constEnd(); ++it_nc) { const MyMoneyAccount subAccount = file->account(*it_nc); MyMoneySecurity currency; if (subAccount.isInvest()) { MyMoneySecurity underSecurity = file->security(subAccount.currencyId()); currency = file->security(underSecurity.tradingCurrency()); } else { currency = file->security(subAccount.currencyId()); } forecastItem = new QTreeWidgetItem(parentItem); forecastItem->setText(0, subAccount.name()); forecastItem->setIcon(0, subAccount.accountPixmap()); forecastItem->setData(0, ForecastRole, QVariant::fromValue(forecast)); forecastItem->setData(0, AccountRole, QVariant::fromValue(subAccount)); forecastItem->setExpanded(true); switch (forecastType) { case eSummary: updateSummary(forecastItem); break; case eDetailed: updateDetailed(forecastItem); break; case EForecastViewType::eBudget: updateBudget(forecastItem); break; default: break; } loadAccounts(forecast, subAccount, forecastItem, forecastType); } } void loadSummaryView() { MyMoneyForecast forecast = KMyMoneyUtils::forecast(); QList accList; const auto file = MyMoneyFile::instance(); //get the settings from current page forecast.setForecastDays(ui->m_forecastDays->value()); forecast.setAccountsCycle(ui->m_accountsCycle->value()); forecast.setBeginForecastDay(ui->m_beginDay->value()); forecast.setForecastCycles(ui->m_forecastCycles->value()); forecast.setHistoryMethod(ui->m_historyMethod->checkedId()); forecast.doForecast(); //add columns QStringList headerLabels; headerLabels << i18n("Account"); headerLabels << i18nc("Today's forecast", "Current"); //if beginning of forecast is today, set the begin day to next cycle to avoid repeating the first cycle qint64 daysToBeginDay; if (QDate::currentDate() < forecast.beginForecastDate()) { daysToBeginDay = QDate::currentDate().daysTo(forecast.beginForecastDate()); } else { daysToBeginDay = forecast.accountsCycle(); } for (auto i = 0; ((i*forecast.accountsCycle()) + daysToBeginDay) <= forecast.forecastDays(); ++i) { auto intervalDays = ((i * forecast.accountsCycle()) + daysToBeginDay); headerLabels << i18np("1 day", "%1 days", intervalDays); } //add variation columns headerLabels << i18n("Total variation"); ui->m_summaryList->clear(); //set the columns ui->m_summaryList->setHeaderLabels(headerLabels); ui->m_summaryList->setIconSize(QSize(22, 22)); ui->m_summaryList->setSortingEnabled(true); ui->m_summaryList->sortByColumn(0, Qt::AscendingOrder); //add default rows addTotalRow(ui->m_summaryList, forecast); addAssetLiabilityRows(forecast); loadAccounts(forecast, file->asset(), m_assetItem, eSummary); loadAccounts(forecast, file->liability(), m_liabilityItem, eSummary); adjustHeadersAndResizeToContents(ui->m_summaryList); //Add comments to the advice list ui->m_adviceText->clear(); //Get all accounts of the right type to calculate forecast m_nameIdx.clear(); accList = forecast.accountList(); QList::const_iterator accList_t = accList.constBegin(); for (; accList_t != accList.constEnd(); ++accList_t) { MyMoneyAccount acc = *accList_t; if (m_nameIdx[acc.id()] != acc.id()) { //Check if the account is there m_nameIdx[acc.id()] = acc.id(); } } QMap::ConstIterator it_nc; for (it_nc = m_nameIdx.constBegin(); it_nc != m_nameIdx.constEnd(); ++it_nc) { const MyMoneyAccount& acc = file->account(*it_nc); MyMoneySecurity currency; //change currency to deep currency if account is an investment if (acc.isInvest()) { MyMoneySecurity underSecurity = file->security(acc.currencyId()); currency = file->security(underSecurity.tradingCurrency()); } else { currency = file->security(acc.currencyId()); } //Check if the account is going to be below zero or below the minimal balance in the forecast period QString minimumBalance = acc.value("minimumBalance"); MyMoneyMoney minBalance = MyMoneyMoney(minimumBalance); //Check if the account is going to be below minimal balance auto dropMinimum = forecast.daysToMinimumBalance(acc); //Check if the account is going to be below zero in the future auto dropZero = forecast.daysToZeroBalance(acc); // spit out possible warnings QString msg; // if a minimum balance has been specified, an appropriate warning will // only be shown, if the drop below 0 is on a different day or not present if (dropMinimum != -1 && !minBalance.isZero() && (dropMinimum < dropZero || dropZero == -1)) { switch (dropMinimum) { case 0: msg = QString("").arg(KMyMoneySettings::schemeColor(SchemeColor::Negative).name()); msg += i18n("The balance of %1 is below the minimum balance %2 today.", acc.name(), MyMoneyUtils::formatMoney(minBalance, acc, currency)); msg += QString(""); break; default: msg = QString("").arg(KMyMoneySettings::schemeColor(SchemeColor::Negative).name()); msg += i18np("The balance of %2 will drop below the minimum balance %3 in %1 day.", "The balance of %2 will drop below the minimum balance %3 in %1 days.", dropMinimum - 1, acc.name(), MyMoneyUtils::formatMoney(minBalance, acc, currency)); msg += QString(""); } if (!msg.isEmpty()) { ui->m_adviceText->append(msg); } } // a drop below zero is always shown msg.clear(); switch (dropZero) { case -1: break; case 0: if (acc.accountGroup() == eMyMoney::Account::Type::Asset) { msg = QString("").arg(KMyMoneySettings::schemeColor(SchemeColor::Negative).name()); msg += i18n("The balance of %1 is below %2 today.", acc.name(), MyMoneyUtils::formatMoney(MyMoneyMoney(), acc, currency)); msg += QString(""); break; } if (acc.accountGroup() == eMyMoney::Account::Type::Liability) { msg = i18n("The balance of %1 is above %2 today.", acc.name(), MyMoneyUtils::formatMoney(MyMoneyMoney(), acc, currency)); break; } break; default: if (acc.accountGroup() == eMyMoney::Account::Type::Asset) { msg = QString("").arg(KMyMoneySettings::schemeColor(SchemeColor::Negative).name()); msg += i18np("The balance of %2 will drop below %3 in %1 day.", "The balance of %2 will drop below %3 in %1 days.", dropZero, acc.name(), MyMoneyUtils::formatMoney(MyMoneyMoney(), acc, currency)); msg += QString(""); break; } if (acc.accountGroup() == eMyMoney::Account::Type::Liability) { msg = i18np("The balance of %2 will raise above %3 in %1 day.", "The balance of %2 will raise above %3 in %1 days.", dropZero, acc.name(), MyMoneyUtils::formatMoney(MyMoneyMoney(), acc, currency)); break; } } if (!msg.isEmpty()) { ui->m_adviceText->append(msg); } //advice about trends msg.clear(); MyMoneyMoney accCycleVariation = forecast.accountCycleVariation(acc); if (accCycleVariation < MyMoneyMoney()) { msg = QString("").arg(KMyMoneySettings::schemeColor(SchemeColor::Negative).name()); msg += i18n("The account %1 is decreasing %2 per cycle.", acc.name(), MyMoneyUtils::formatMoney(accCycleVariation, acc, currency)); msg += QString(""); } if (!msg.isEmpty()) { ui->m_adviceText->append(msg); } } ui->m_adviceText->show(); } void loadAdvancedView() { const auto file = MyMoneyFile::instance(); QList accList; MyMoneySecurity baseCurrency = file->baseCurrency(); MyMoneyForecast forecast = KMyMoneyUtils::forecast(); qint64 daysToBeginDay; //get the settings from current page forecast.setForecastDays(ui->m_forecastDays->value()); forecast.setAccountsCycle(ui->m_accountsCycle->value()); forecast.setBeginForecastDay(ui->m_beginDay->value()); forecast.setForecastCycles(ui->m_forecastCycles->value()); forecast.setHistoryMethod(ui->m_historyMethod->checkedId()); forecast.doForecast(); //Get all accounts of the right type to calculate forecast m_nameIdx.clear(); accList = forecast.accountList(); QList::const_iterator accList_t = accList.constBegin(); for (; accList_t != accList.constEnd(); ++accList_t) { MyMoneyAccount acc = *accList_t; if (m_nameIdx[acc.id()] != acc.id()) { //Check if the account is there m_nameIdx[acc.id()] = acc.id(); } } //clear the list, including columns ui->m_advancedList->clear(); ui->m_advancedList->setColumnCount(0); ui->m_advancedList->setIconSize(QSize(22, 22)); QStringList headerLabels; //add first column of both lists headerLabels << i18n("Account"); //if beginning of forecast is today, set the begin day to next cycle to avoid repeating the first cycle if (QDate::currentDate() < forecast.beginForecastDate()) { daysToBeginDay = QDate::currentDate().daysTo(forecast.beginForecastDate()); } else { daysToBeginDay = forecast.accountsCycle(); } //add columns for (auto i = 1; ((i * forecast.accountsCycle()) + daysToBeginDay) <= forecast.forecastDays(); ++i) { headerLabels << i18n("Min Bal %1", i); headerLabels << i18n("Min Date %1", i); } for (auto i = 1; ((i * forecast.accountsCycle()) + daysToBeginDay) <= forecast.forecastDays(); ++i) { headerLabels << i18n("Max Bal %1", i); headerLabels << i18n("Max Date %1", i); } headerLabels << i18nc("Average balance", "Average"); ui->m_advancedList->setHeaderLabels(headerLabels); QTreeWidgetItem *advancedItem = 0; QMap::ConstIterator it_nc; for (it_nc = m_nameIdx.constBegin(); it_nc != m_nameIdx.constEnd(); ++it_nc) { const MyMoneyAccount& acc = file->account(*it_nc); QString amount; MyMoneyMoney amountMM; MyMoneySecurity currency; //change currency to deep currency if account is an investment if (acc.isInvest()) { MyMoneySecurity underSecurity = file->security(acc.currencyId()); currency = file->security(underSecurity.tradingCurrency()); } else { currency = file->security(acc.currencyId()); } advancedItem = new QTreeWidgetItem(ui->m_advancedList, advancedItem, false); advancedItem->setText(0, acc.name()); advancedItem->setIcon(0, acc.accountPixmap()); auto it_c = 1; // iterator for the columns of the listview //get minimum balance list QList minBalanceList = forecast.accountMinimumBalanceDateList(acc); QList::Iterator t_min; for (t_min = minBalanceList.begin(); t_min != minBalanceList.end() ; ++t_min) { QDate minDate = *t_min; amountMM = forecast.forecastBalance(acc, minDate); amount = MyMoneyUtils::formatMoney(amountMM, acc, currency); advancedItem->setText(it_c, amount); advancedItem->setTextAlignment(it_c, Qt::AlignRight | Qt::AlignVCenter); if (amountMM.isNegative()) { advancedItem->setForeground(it_c, KMyMoneySettings::schemeColor(SchemeColor::Negative)); } it_c++; QString dateString = QLocale().toString(minDate, QLocale::ShortFormat); advancedItem->setText(it_c, dateString); advancedItem->setTextAlignment(it_c, Qt::AlignRight | Qt::AlignVCenter); if (amountMM.isNegative()) { advancedItem->setForeground(it_c, KMyMoneySettings::schemeColor(SchemeColor::Negative)); } it_c++; } //get maximum balance list QList maxBalanceList = forecast.accountMaximumBalanceDateList(acc); QList::Iterator t_max; for (t_max = maxBalanceList.begin(); t_max != maxBalanceList.end() ; ++t_max) { QDate maxDate = *t_max; amountMM = forecast.forecastBalance(acc, maxDate); amount = MyMoneyUtils::formatMoney(amountMM, acc, currency); advancedItem->setText(it_c, amount); advancedItem->setTextAlignment(it_c, Qt::AlignRight | Qt::AlignVCenter); if (amountMM.isNegative()) { advancedItem->setForeground(it_c, KMyMoneySettings::schemeColor(SchemeColor::Negative)); } it_c++; QString dateString = QLocale().toString(maxDate, QLocale::ShortFormat); advancedItem->setText(it_c, dateString); advancedItem->setTextAlignment(it_c, Qt::AlignRight | Qt::AlignVCenter); if (amountMM.isNegative()) { advancedItem->setForeground(it_c, KMyMoneySettings::schemeColor(SchemeColor::Negative)); } it_c++; } //get average balance amountMM = forecast.accountAverageBalance(acc); amount = MyMoneyUtils::formatMoney(amountMM, acc, currency); advancedItem->setText(it_c, amount); advancedItem->setTextAlignment(it_c, Qt::AlignRight | Qt::AlignVCenter); if (amountMM.isNegative()) { advancedItem->setForeground(it_c, KMyMoneySettings::schemeColor(SchemeColor::Negative)); } it_c++; } // make sure all data is shown adjustHeadersAndResizeToContents(ui->m_advancedList); ui->m_advancedList->show(); } void loadBudgetView() { const auto file = MyMoneyFile::instance(); MyMoneyForecast forecast = KMyMoneyUtils::forecast(); //get the settings from current page and calculate this year based on last year QDate historyEndDate = QDate(QDate::currentDate().year() - 1, 12, 31); QDate historyStartDate = historyEndDate.addDays(-ui->m_accountsCycle->value() * ui->m_forecastCycles->value()); QDate forecastStartDate = QDate(QDate::currentDate().year(), 1, 1); QDate forecastEndDate = QDate::currentDate().addDays(ui->m_forecastDays->value()); forecast.setHistoryMethod(ui->m_historyMethod->checkedId()); MyMoneyBudget budget; forecast.createBudget(budget, historyStartDate, historyEndDate, forecastStartDate, forecastEndDate, false); ui->m_budgetList->clear(); ui->m_budgetList->setIconSize(QSize(22, 22)); ui->m_budgetList->setSortingEnabled(true); ui->m_budgetList->sortByColumn(0, Qt::AscendingOrder); //add columns QStringList headerLabels; headerLabels << i18n("Account"); { forecastStartDate = forecast.forecastStartDate(); forecastEndDate = forecast.forecastEndDate(); //add cycle interval columns QDate f_date = forecastStartDate; for (; f_date <= forecastEndDate; f_date = f_date.addMonths(1)) { headerLabels << QDate::longMonthName(f_date.month()); } } //add total column headerLabels << i18nc("Total balance", "Total"); //set the columns ui->m_budgetList->setHeaderLabels(headerLabels); //add default rows addTotalRow(ui->m_budgetList, forecast); addIncomeExpenseRows(forecast); //load income and expense budget accounts loadAccounts(forecast, file->income(), m_incomeItem, EForecastViewType::eBudget); loadAccounts(forecast, file->expense(), m_expenseItem, EForecastViewType::eBudget); adjustHeadersAndResizeToContents(ui->m_budgetList); } void loadChartView() { - eMyMoney::Report::DetailLevel detailLevel[4] = { eMyMoney::Report::DetailLevel::All, eMyMoney::Report::DetailLevel::Top, eMyMoney::Report::DetailLevel::Group, eMyMoney::Report::DetailLevel::Total }; - - MyMoneyReport reportCfg = MyMoneyReport( - eMyMoney::Report::RowType::AssetLiability, - static_cast(eMyMoney::Report::ColumnType::Months), - eMyMoney::TransactionFilter::Date::UserDefined, // overridden by the setDateFilter() call below - detailLevel[ui->m_comboDetail->currentIndex()], - i18n("Net Worth Forecast"), - i18n("Generated Report")); - - reportCfg.setChartByDefault(true); - reportCfg.setChartCHGridLines(false); - reportCfg.setChartSVGridLines(false); - reportCfg.setChartType(eMyMoney::Report::ChartType::Line); - reportCfg.setIncludingSchedules(false); - // FIXME: this causes a crash - //reportCfg.setColumnsAreDays( true ); - reportCfg.setChartDataLabels(false); - reportCfg.setConvertCurrency(true); - reportCfg.setIncludingForecast(true); - reportCfg.setDateFilter(QDate::currentDate(), QDate::currentDate().addDays(ui->m_forecastDays->value())); - reports::PivotTable table(reportCfg); - - table.drawChart(*m_forecastChart); - - // Adjust the size - m_forecastChart->resize(ui->m_tab->width() - 10, ui->m_tab->height()); - //m_forecastChart->show(); - m_forecastChart->update(); - + if (m_forecastChart) + delete m_forecastChart; + + if (const auto reportsPlugin = pPlugins.data.value("reportsview", nullptr)) { + const auto args = + QString::number(ui->m_comboDetail->currentIndex()) + ';' + + QString::number(ui->m_forecastDays->value()) + ';' + + QString::number(ui->m_tab->width()) + ';' + + QString::number(ui->m_tab->height()); + + const auto variantReport = reportsPlugin->requestData(args, eWidgetPlugin::WidgetType::NetWorthForecastWithArgs); + if (!variantReport.isNull()) + m_forecastChart = variantReport.value(); + } else { + m_forecastChart = new QLabel(i18n("Enable reports plugin to see this chart.")); + } + m_chartLayout->addWidget(m_forecastChart); } void loadForecastSettings() { //fill the settings controls ui->m_forecastDays->setValue(KMyMoneySettings::forecastDays()); ui->m_accountsCycle->setValue(KMyMoneySettings::forecastAccountCycle()); ui->m_beginDay->setValue(KMyMoneySettings::beginForecastDay()); ui->m_forecastCycles->setValue(KMyMoneySettings::forecastCycles()); ui->m_historyMethod->setId(ui->radioButton11, 0); // simple moving avg ui->m_historyMethod->setId(ui->radioButton12, 1); // weighted moving avg ui->m_historyMethod->setId(ui->radioButton13, 2); // linear regression ui->m_historyMethod->button(KMyMoneySettings::historyMethod())->setChecked(true); switch (KMyMoneySettings::forecastMethod()) { case 0: ui->m_forecastMethod->setText(i18nc("Scheduled method", "Scheduled")); ui->m_forecastCycles->setDisabled(true); ui->m_historyMethodGroupBox->setDisabled(true); break; case 1: ui->m_forecastMethod->setText(i18nc("History-based method", "History")); ui->m_forecastCycles->setEnabled(true); ui->m_historyMethodGroupBox->setEnabled(true); break; default: ui->m_forecastMethod->setText(i18nc("Unknown forecast method", "Unknown")); break; } } void addAssetLiabilityRows(const MyMoneyForecast& forecast) { const auto file = MyMoneyFile::instance(); m_assetItem = new QTreeWidgetItem(m_totalItem); m_assetItem->setText(0, file->asset().name()); m_assetItem->setIcon(0, file->asset().accountPixmap()); m_assetItem->setData(0, ForecastRole, QVariant::fromValue(forecast)); m_assetItem->setData(0, AccountRole, QVariant::fromValue(file->asset())); m_assetItem->setExpanded(true); m_liabilityItem = new QTreeWidgetItem(m_totalItem); m_liabilityItem->setText(0, file->liability().name()); m_liabilityItem->setIcon(0, file->liability().accountPixmap()); m_liabilityItem->setData(0, ForecastRole, QVariant::fromValue(forecast)); m_liabilityItem->setData(0, AccountRole, QVariant::fromValue(file->liability())); m_liabilityItem->setExpanded(true); } void addIncomeExpenseRows(const MyMoneyForecast& forecast) { const auto file = MyMoneyFile::instance(); m_incomeItem = new QTreeWidgetItem(m_totalItem); m_incomeItem->setText(0, file->income().name()); m_incomeItem->setIcon(0, file->income().accountPixmap()); m_incomeItem->setData(0, ForecastRole, QVariant::fromValue(forecast)); m_incomeItem->setData(0, AccountRole, QVariant::fromValue(file->income())); m_incomeItem->setExpanded(true); m_expenseItem = new QTreeWidgetItem(m_totalItem); m_expenseItem->setText(0, file->expense().name()); m_expenseItem->setIcon(0, file->expense().accountPixmap()); m_expenseItem->setData(0, ForecastRole, QVariant::fromValue(forecast)); m_expenseItem->setData(0, AccountRole, QVariant::fromValue(file->expense())); m_expenseItem->setExpanded(true); } void addTotalRow(QTreeWidget* forecastList, const MyMoneyForecast& forecast) { const auto file = MyMoneyFile::instance(); m_totalItem = new QTreeWidgetItem(forecastList); QFont font; font.setBold(true); m_totalItem->setFont(0, font); m_totalItem->setText(0, i18nc("Total balance", "Total")); m_totalItem->setIcon(0, file->asset().accountPixmap()); m_totalItem->setData(0, ForecastRole, QVariant::fromValue(forecast)); m_totalItem->setData(0, AccountRole, QVariant::fromValue(file->asset())); m_totalItem->setExpanded(true); } bool includeAccount(MyMoneyForecast& forecast, const MyMoneyAccount& acc) { const auto file = MyMoneyFile::instance(); if (forecast.isForecastAccount(acc)) return true; foreach (const auto sAccount, acc.accountList()) { auto account = file->account(sAccount); if (includeAccount(forecast, account)) return true; } return false; } void adjustHeadersAndResizeToContents(QTreeWidget *widget) { QSize sizeHint(0, widget->sizeHintForRow(0)); QTreeWidgetItem *header = widget->headerItem(); for (int i = 0; i < header->columnCount(); ++i) { if (i > 0) { header->setData(i, Qt::TextAlignmentRole, Qt::AlignRight); // make sure that the row height stays the same even when the column that has icons is not visible if (m_totalItem) { m_totalItem->setSizeHint(i, sizeHint); } } widget->resizeColumnToContents(i); } } void setNegative(QTreeWidgetItem *item, bool isNegative) { if (isNegative) { for (int i = 0; i < item->columnCount(); ++i) { item->setForeground(i, KMyMoneySettings::schemeColor(SchemeColor::Negative)); } } } void showAmount(QTreeWidgetItem* item, int column, const MyMoneyMoney& amount, const MyMoneySecurity& security) { item->setText(column, MyMoneyUtils::formatMoney(amount, security)); item->setTextAlignment(column, Qt::AlignRight | Qt::AlignVCenter); item->setFont(column, item->font(0)); if (amount.isNegative()) { item->setForeground(column, KMyMoneySettings::schemeColor(SchemeColor::Negative)); } } void adjustParentValue(QTreeWidgetItem *item, int column, const MyMoneyMoney& value) { if (!item) return; item->setData(column, ValueRole, QVariant::fromValue(item->data(column, ValueRole).value() + value)); item->setData(column, ValueRole, QVariant::fromValue(item->data(column, ValueRole).value().convert(MyMoneyFile::instance()->baseCurrency().smallestAccountFraction()))); // if the entry has no children, // or it is the top entry // or it is currently not open // we need to display the value of it if (item->childCount() == 0 || !item->parent() || (!item->isExpanded() && item->childCount() > 0) || (item->parent() && !item->parent()->parent())) { if (item->childCount() > 0) item->setText(column, " "); MyMoneyMoney amount = item->data(column, ValueRole).value(); showAmount(item, column, amount, MyMoneyFile::instance()->baseCurrency()); } // now make sure, the upstream accounts also get notified about the value change adjustParentValue(item->parent(), column, value); } void setValue(QTreeWidgetItem* item, int column, const MyMoneyMoney& amount, const QDate& forecastDate) { MyMoneyAccount account = item->data(0, AccountRole).value(); //calculate the balance in base currency for the total row if (account.currencyId() != MyMoneyFile::instance()->baseCurrency().id()) { - ReportAccount repAcc = ReportAccount(account.id()); - MyMoneyMoney curPrice = repAcc.baseCurrencyPrice(forecastDate); - MyMoneyMoney baseAmountMM = amount * curPrice; - MyMoneyMoney value = baseAmountMM.convert(MyMoneyFile::instance()->baseCurrency().smallestAccountFraction()); + const auto file = MyMoneyFile::instance(); + const auto curPrice = file->price(account.tradingCurrencyId(), file->baseCurrency().id(), forecastDate); + const auto curRate = curPrice.rate(file->baseCurrency().id()); + auto baseAmountMM = amount * curRate; + auto value = baseAmountMM.convert(file->baseCurrency().smallestAccountFraction()); item->setData(column, ValueRole, QVariant::fromValue(value)); adjustParentValue(item->parent(), column, value); } else { item->setData(column, ValueRole, QVariant::fromValue(item->data(column, ValueRole).value() + amount)); adjustParentValue(item->parent(), column, amount); } } void setAmount(QTreeWidgetItem* item, int column, const MyMoneyMoney& amount) { item->setData(column, AmountRole, QVariant::fromValue(amount)); item->setTextAlignment(column, Qt::AlignRight | Qt::AlignVCenter); } void updateSummary(QTreeWidgetItem *item) { MyMoneyMoney amountMM; auto it_c = 1; // iterator for the columns of the listview const auto file = MyMoneyFile::instance(); qint64 daysToBeginDay; MyMoneyForecast forecast = item->data(0, ForecastRole).value(); if (QDate::currentDate() < forecast.beginForecastDate()) { daysToBeginDay = QDate::currentDate().daysTo(forecast.beginForecastDate()); } else { daysToBeginDay = forecast.accountsCycle(); } MyMoneyAccount account = item->data(0, AccountRole).value(); MyMoneySecurity currency; if (account.isInvest()) { MyMoneySecurity underSecurity = file->security(account.currencyId()); currency = file->security(underSecurity.tradingCurrency()); } else { currency = file->security(account.currencyId()); } //add current balance column QDate summaryDate = QDate::currentDate(); amountMM = forecast.forecastBalance(account, summaryDate); //calculate the balance in base currency for the total row setAmount(item, it_c, amountMM); setValue(item, it_c, amountMM, summaryDate); showAmount(item, it_c, amountMM, currency); it_c++; //iterate through all other columns for (summaryDate = QDate::currentDate().addDays(daysToBeginDay); summaryDate <= forecast.forecastEndDate(); summaryDate = summaryDate.addDays(forecast.accountsCycle()), ++it_c) { amountMM = forecast.forecastBalance(account, summaryDate); //calculate the balance in base currency for the total row setAmount(item, it_c, amountMM); setValue(item, it_c, amountMM, summaryDate); showAmount(item, it_c, amountMM, currency); } //calculate and add variation per cycle setNegative(item, forecast.accountTotalVariation(account).isNegative()); setAmount(item, it_c, forecast.accountTotalVariation(account)); setValue(item, it_c, forecast.accountTotalVariation(account), forecast.forecastEndDate()); showAmount(item, it_c, forecast.accountTotalVariation(account), currency); } void updateDetailed(QTreeWidgetItem *item) { QString amount; QString vAmount; MyMoneyMoney vAmountMM; const auto file = MyMoneyFile::instance(); MyMoneyAccount account = item->data(0, AccountRole).value(); MyMoneySecurity currency; if (account.isInvest()) { MyMoneySecurity underSecurity = file->security(account.currencyId()); currency = file->security(underSecurity.tradingCurrency()); } else { currency = file->security(account.currencyId()); } int it_c = 1; // iterator for the columns of the listview MyMoneyForecast forecast = item->data(0, ForecastRole).value(); for (QDate forecastDate = QDate::currentDate(); forecastDate <= forecast.forecastEndDate(); ++it_c, forecastDate = forecastDate.addDays(1)) { MyMoneyMoney amountMM = forecast.forecastBalance(account, forecastDate); //calculate the balance in base currency for the total row setAmount(item, it_c, amountMM); setValue(item, it_c, amountMM, forecastDate); showAmount(item, it_c, amountMM, currency); } //calculate and add variation per cycle vAmountMM = forecast.accountTotalVariation(account); setAmount(item, it_c, vAmountMM); setValue(item, it_c, vAmountMM, forecast.forecastEndDate()); showAmount(item, it_c, vAmountMM, currency); } void updateBudget(QTreeWidgetItem *item) { MyMoneySecurity currency; MyMoneyMoney tAmountMM; MyMoneyForecast forecast = item->data(0, ForecastRole).value(); const auto file = MyMoneyFile::instance(); int it_c = 1; // iterator for the columns of the listview QDate forecastDate = forecast.forecastStartDate(); MyMoneyAccount account = item->data(0, AccountRole).value(); if (account.isInvest()) { MyMoneySecurity underSecurity = file->security(account.currencyId()); currency = file->security(underSecurity.tradingCurrency()); } else { currency = file->security(account.currencyId()); } //iterate columns for (; forecastDate <= forecast.forecastEndDate(); forecastDate = forecastDate.addMonths(1), ++it_c) { MyMoneyMoney amountMM; amountMM = forecast.forecastBalance(account, forecastDate); if (account.accountType() == eMyMoney::Account::Type::Expense) amountMM = -amountMM; tAmountMM += amountMM; setAmount(item, it_c, amountMM); setValue(item, it_c, amountMM, forecastDate); showAmount(item, it_c, amountMM, currency); } //set total column setAmount(item, it_c, tAmountMM); setValue(item, it_c, tAmountMM, forecast.forecastEndDate()); showAmount(item, it_c, tAmountMM, currency); } /** * Get the list of prices for an account * This is used later to create an instance of KMyMoneyAccountTreeForecastItem * */ // QList getAccountPrices(const MyMoneyAccount& acc) // { // const auto file = MyMoneyFile::instance(); // QList prices; // MyMoneySecurity security = file->baseCurrency(); // try { // if (acc.isInvest()) { // security = file->security(acc.currencyId()); // if (security.tradingCurrency() != file->baseCurrency().id()) { // MyMoneySecurity sec = file->security(security.tradingCurrency()); // prices += file->price(sec.id(), file->baseCurrency().id()); // } // } else if (acc.currencyId() != file->baseCurrency().id()) { // if (acc.currencyId() != file->baseCurrency().id()) { // security = file->security(acc.currencyId()); // prices += file->price(acc.currencyId(), file->baseCurrency().id()); // } // } // } catch (const MyMoneyException &e) { // qDebug() << Q_FUNC_INFO << " caught exception while adding " << acc.name() << "[" << acc.id() << "]: " << e.what(); // } // return prices; // } KForecastView *q_ptr; Ui::KForecastView *ui; bool m_needReload[MaxViewTabs]; /** * This member holds the load state of page */ bool m_needLoad; QTreeWidgetItem* m_totalItem; QTreeWidgetItem* m_assetItem; QTreeWidgetItem* m_liabilityItem; QTreeWidgetItem* m_incomeItem; QTreeWidgetItem* m_expenseItem; QLayout* m_chartLayout; - reports::KReportChartView* m_forecastChart; + QWidget *m_forecastChart; QScopedPointer m_fixedColumnView; QMap m_nameIdx; }; #endif diff --git a/kmymoney/plugins/views/reports/CMakeLists.txt b/kmymoney/plugins/views/reports/CMakeLists.txt index 65b61dec3..0420307fb 100644 --- a/kmymoney/plugins/views/reports/CMakeLists.txt +++ b/kmymoney/plugins/views/reports/CMakeLists.txt @@ -1,89 +1,92 @@ +add_subdirectory(core) + configure_file(${CMAKE_CURRENT_SOURCE_DIR}/reportsview.json.in ${CMAKE_CURRENT_BINARY_DIR}/reportsview.json @ONLY) set(reportsview_SOURCES reportsview.cpp kreportsview.cpp tocitem.cpp tocitemgroup.cpp tocitemreport.cpp kreportconfigurationfilterdlg.cpp reporttabimpl.cpp reportcontrolimpl.cpp + kbalancechartdlg.cpp ../../../views/kmymoneywebpage.cpp ) ki18n_wrap_ui(reportsview_SOURCES kreportconfigurationfilterdlg.ui reportcontrol.ui reporttabgeneral.ui reporttabrowcolquery.ui reporttabrowcolpivot.ui reporttabrange.ui reporttabchart.ui reporttabcapitalgain.ui reporttabperformance.ui ) # kconfig_add_kcfg_files(reportsview_SOURCES reportsviewsettings.kcfgc) kcoreaddons_add_plugin(reportsview SOURCES ${reportsview_SOURCES} JSON "${CMAKE_CURRENT_BINARY_DIR}/reportsview.json" INSTALL_NAMESPACE "kmymoney") #kcoreaddons_add_plugin sets LIBRARY_OUTPUT_DIRECTORY to ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/${INSTALL_NAMESPACE} set_target_properties(reportsview PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}") target_link_libraries(reportsview PUBLIC kmm_plugin KF5::TextWidgets KF5::KIOCore KF5::KIOFileWidgets KF5::KIOWidgets KF5::KIONTLM reports kmm_widgets kmm_menus ) if(ENABLE_WEBENGINE) target_link_libraries(reportsview PRIVATE Qt5::WebEngineWidgets) else(ENABLE_WEBENGINE) target_link_libraries(reportsview PRIVATE KF5::WebKit) endif(ENABLE_WEBENGINE) # the KCM module set(kcm_reportsview_PART_SRCS kcm_reportsview.cpp ) kconfig_add_kcfg_files(kcm_reportsview_PART_SRCS reportsviewsettings.kcfgc) ki18n_wrap_ui(kcm_reportsview_PART_SRCS reportsviewsettings.ui) kcoreaddons_add_plugin(kcm_reportsview SOURCES ${kcm_reportsview_PART_SRCS} JSON "${CMAKE_CURRENT_BINARY_DIR}/kcm_reportsview.json" INSTALL_NAMESPACE "kmymoney") kcoreaddons_desktop_to_json(kcm_reportsview kcm_reportsview.desktop) #kcoreaddons_add_plugin sets LIBRARY_OUTPUT_DIRECTORY to ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/${INSTALL_NAMESPACE} set_target_properties(kcm_reportsview PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}") target_link_libraries(kcm_reportsview KF5::I18n KF5::ConfigWidgets KF5::Completion KF5::KIOWidgets KF5::CoreAddons kmm_settings ) install(FILES kcm_reportsview.desktop DESTINATION "${SERVICES_INSTALL_DIR}") diff --git a/kmymoney/reports/CMakeLists.txt b/kmymoney/plugins/views/reports/core/CMakeLists.txt similarity index 100% rename from kmymoney/reports/CMakeLists.txt rename to kmymoney/plugins/views/reports/core/CMakeLists.txt diff --git a/kmymoney/reports/kbalanceaxis.cpp b/kmymoney/plugins/views/reports/core/kbalanceaxis.cpp similarity index 100% rename from kmymoney/reports/kbalanceaxis.cpp rename to kmymoney/plugins/views/reports/core/kbalanceaxis.cpp diff --git a/kmymoney/reports/kbalanceaxis.h b/kmymoney/plugins/views/reports/core/kbalanceaxis.h similarity index 100% rename from kmymoney/reports/kbalanceaxis.h rename to kmymoney/plugins/views/reports/core/kbalanceaxis.h diff --git a/kmymoney/reports/kreportchartview.cpp b/kmymoney/plugins/views/reports/core/kreportchartview.cpp similarity index 100% rename from kmymoney/reports/kreportchartview.cpp rename to kmymoney/plugins/views/reports/core/kreportchartview.cpp diff --git a/kmymoney/reports/kreportchartview.h b/kmymoney/plugins/views/reports/core/kreportchartview.h similarity index 100% rename from kmymoney/reports/kreportchartview.h rename to kmymoney/plugins/views/reports/core/kreportchartview.h diff --git a/kmymoney/reports/listtable.cpp b/kmymoney/plugins/views/reports/core/listtable.cpp similarity index 100% rename from kmymoney/reports/listtable.cpp rename to kmymoney/plugins/views/reports/core/listtable.cpp diff --git a/kmymoney/reports/listtable.h b/kmymoney/plugins/views/reports/core/listtable.h similarity index 100% rename from kmymoney/reports/listtable.h rename to kmymoney/plugins/views/reports/core/listtable.h diff --git a/kmymoney/reports/objectinfotable.cpp b/kmymoney/plugins/views/reports/core/objectinfotable.cpp similarity index 100% rename from kmymoney/reports/objectinfotable.cpp rename to kmymoney/plugins/views/reports/core/objectinfotable.cpp diff --git a/kmymoney/reports/objectinfotable.h b/kmymoney/plugins/views/reports/core/objectinfotable.h similarity index 100% rename from kmymoney/reports/objectinfotable.h rename to kmymoney/plugins/views/reports/core/objectinfotable.h diff --git a/kmymoney/reports/pivotgrid.cpp b/kmymoney/plugins/views/reports/core/pivotgrid.cpp similarity index 100% rename from kmymoney/reports/pivotgrid.cpp rename to kmymoney/plugins/views/reports/core/pivotgrid.cpp diff --git a/kmymoney/reports/pivotgrid.h b/kmymoney/plugins/views/reports/core/pivotgrid.h similarity index 100% rename from kmymoney/reports/pivotgrid.h rename to kmymoney/plugins/views/reports/core/pivotgrid.h diff --git a/kmymoney/reports/pivottable.cpp b/kmymoney/plugins/views/reports/core/pivottable.cpp similarity index 100% rename from kmymoney/reports/pivottable.cpp rename to kmymoney/plugins/views/reports/core/pivottable.cpp diff --git a/kmymoney/reports/pivottable.h b/kmymoney/plugins/views/reports/core/pivottable.h similarity index 100% rename from kmymoney/reports/pivottable.h rename to kmymoney/plugins/views/reports/core/pivottable.h diff --git a/kmymoney/reports/querytable.cpp b/kmymoney/plugins/views/reports/core/querytable.cpp similarity index 100% rename from kmymoney/reports/querytable.cpp rename to kmymoney/plugins/views/reports/core/querytable.cpp diff --git a/kmymoney/reports/querytable.h b/kmymoney/plugins/views/reports/core/querytable.h similarity index 100% rename from kmymoney/reports/querytable.h rename to kmymoney/plugins/views/reports/core/querytable.h diff --git a/kmymoney/reports/reportaccount.cpp b/kmymoney/plugins/views/reports/core/reportaccount.cpp similarity index 95% rename from kmymoney/reports/reportaccount.cpp rename to kmymoney/plugins/views/reports/core/reportaccount.cpp index e3d43adc7..b490b9d9d 100644 --- a/kmymoney/reports/reportaccount.cpp +++ b/kmymoney/plugins/views/reports/core/reportaccount.cpp @@ -1,335 +1,315 @@ /* * Copyright 2005 Ace Jones * Copyright 2006-2012 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. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "reportaccount.h" // ---------------------------------------------------------------------------- // QT Includes // ---------------------------------------------------------------------------- // KDE Includes // This is just needed for i18n(). Once I figure out how to handle i18n // without using this macro directly, I'll be freed of KDE dependency. This // is a minor problem because we use these terms when rendering to HTML, // and a more major problem because we need it to translate account types // (e.g. eMyMoney::Account::Type::Checkings) into their text representation. We also // use that text representation in the core data structure of the report. (Ace) // ---------------------------------------------------------------------------- // Project Includes #include "mymoneymoney.h" #include "mymoneyfile.h" #include "mymoneyprice.h" #include "mymoneysecurity.h" #include "reportdebug.h" #include "mymoneyenums.h" namespace reports { ReportAccount::ReportAccount() { } ReportAccount::ReportAccount(const ReportAccount& copy): MyMoneyAccount(copy), m_nameHierarchy(copy.m_nameHierarchy) { // NOTE: I implemented the copy constructor solely for debugging reasons DEBUG_ENTER(Q_FUNC_INFO); } ReportAccount::ReportAccount(const QString& accountid): MyMoneyAccount(MyMoneyFile::instance()->account(accountid)) { DEBUG_ENTER(Q_FUNC_INFO); DEBUG_OUTPUT(QString("Account %1").arg(accountid)); calculateAccountHierarchy(); } ReportAccount::ReportAccount(const MyMoneyAccount& account): MyMoneyAccount(account) { DEBUG_ENTER(Q_FUNC_INFO); DEBUG_OUTPUT(QString("Account %1").arg(account.id())); calculateAccountHierarchy(); } void ReportAccount::calculateAccountHierarchy() { DEBUG_ENTER(Q_FUNC_INFO); MyMoneyFile* file = MyMoneyFile::instance(); QString resultid = id(); QString parentid = parentAccountId(); #ifdef DEBUG_HIDE_SENSITIVE m_nameHierarchy.prepend(file->account(resultid).id()); #else m_nameHierarchy.prepend(file->account(resultid).name()); #endif while (!parentid.isEmpty() && !file->isStandardAccount(parentid)) { // take on the identity of our parent resultid = parentid; // and try again parentid = file->account(resultid).parentAccountId(); #ifdef DEBUG_HIDE_SENSITIVE m_nameHierarchy.prepend(file->account(resultid).id()); #else m_nameHierarchy.prepend(file->account(resultid).name()); #endif } } MyMoneyMoney ReportAccount::deepCurrencyPrice(const QDate& date, bool exactDate) const { DEBUG_ENTER(Q_FUNC_INFO); MyMoneyMoney result(1, 1); MyMoneyFile* file = MyMoneyFile::instance(); MyMoneySecurity undersecurity = file->security(currencyId()); if (! undersecurity.isCurrency()) { const MyMoneyPrice &price = file->price(undersecurity.id(), undersecurity.tradingCurrency(), date, exactDate); if (price.isValid()) { result = price.rate(undersecurity.tradingCurrency()); DEBUG_OUTPUT(QString("Converting under %1 to deep %2, price on %3 is %4") .arg(undersecurity.name()) .arg(file->security(undersecurity.tradingCurrency()).name()) .arg(date.toString()) .arg(result.toDouble())); } else { DEBUG_OUTPUT(QString("No price to convert under %1 to deep %2 on %3") .arg(undersecurity.name()) .arg(file->security(undersecurity.tradingCurrency()).name()) .arg(date.toString())); result = MyMoneyMoney(); } } return result; } MyMoneyMoney ReportAccount::baseCurrencyPrice(const QDate& date, bool exactDate) const { // Note that whether or not the user chooses to convert to base currency, all the values // for a given account/category are converted to the currency for THAT account/category // The "Convert to base currency" tells the report to convert from the account/category // currency to the file's base currency. // // An example where this matters is if Category 'C' and account 'U' are in USD, but // Account 'J' is in JPY. Say there are two transactions, one is US$100 from U to C, // the other is JPY10,000 from J to C. Given a JPY price of USD$0.01, this means // C will show a balance of $200 NO MATTER WHAT the user chooses for 'convert to base // currency. This confused me for a while, which is why I wrote this comment. // --acejones DEBUG_ENTER(Q_FUNC_INFO); MyMoneyMoney result(1, 1); MyMoneyFile* file = MyMoneyFile::instance(); if (isForeignCurrency()) { result = foreignCurrencyPrice(file->baseCurrency().id(), date, exactDate); } return result; } MyMoneyMoney ReportAccount::foreignCurrencyPrice(const QString foreignCurrency, const QDate& date, bool exactDate) const { DEBUG_ENTER(Q_FUNC_INFO); MyMoneyMoney result(1, 1); MyMoneyFile* file = MyMoneyFile::instance(); MyMoneySecurity security = file->security(foreignCurrency); //check whether it is a currency or a commodity. In the latter case case, get the trading currency QString tradingCurrency; if (security.isCurrency()) { tradingCurrency = foreignCurrency; } else { tradingCurrency = security.tradingCurrency(); } //It makes no sense to get the price if both currencies are the same if (currency().id() != tradingCurrency) { const MyMoneyPrice &price = file->price(currency().id(), tradingCurrency, date, exactDate); if (price.isValid()) { result = price.rate(tradingCurrency); DEBUG_OUTPUT(QString("Converting deep %1 to currency %2, price on %3 is %4") .arg(file->currency(currency().id()).name()) .arg(file->currency(foreignCurrency).name()) .arg(date.toString()) .arg(result.toDouble())); } else { DEBUG_OUTPUT(QString("No price to convert deep %1 to currency %2 on %3") .arg(file->currency(currency().id()).name()) .arg(file->currency(foreignCurrency).name()) .arg(date.toString())); } } return result; } /** * Fetch the trading currency of this account's currency * * @return The account's currency trading currency */ MyMoneySecurity ReportAccount::currency() const { MyMoneyFile* file = MyMoneyFile::instance(); // First, get the deep currency MyMoneySecurity deepcurrency = file->security(currencyId()); if (! deepcurrency.isCurrency()) deepcurrency = file->security(deepcurrency.tradingCurrency()); // Return the deep currency's ID return deepcurrency; } -/** - * Determine if this account's deep currency is different from the file's - * base currency - * - * @return bool True if this account is in a foreign currency - */ -bool ReportAccount::isForeignCurrency() const -{ - return (currency().id() != MyMoneyFile::instance()->baseCurrency().id()); -} - bool ReportAccount::operator<(const ReportAccount& second) const { // DEBUG_ENTER(Q_FUNC_INFO); bool result = false; bool haveresult = false; QStringList::const_iterator it_first = m_nameHierarchy.begin(); QStringList::const_iterator it_second = second.m_nameHierarchy.begin(); while (it_first != m_nameHierarchy.end()) { // The first string is longer than the second, but otherwise identical if (it_second == second.m_nameHierarchy.end()) { result = false; haveresult = true; break; } if ((*it_first) < (*it_second)) { result = true; haveresult = true; break; } else if ((*it_first) > (*it_second)) { result = false; haveresult = true; break; } ++it_first; ++it_second; } // The second string is longer than the first, but otherwise identical if (!haveresult && (it_second != second.m_nameHierarchy.end())) result = true; // DEBUG_OUTPUT(QString("%1 < %2 is %3").arg(debugName(),second.debugName()).arg(result)); return result; } /** * The name of only this account. No matter how deep the hierarchy, this * method only returns the last name in the list, which is the engine name] * of this account. * * @return QString The account's name */ QString ReportAccount::name() const { return m_nameHierarchy.back(); } // MyMoneyAccount:fullHierarchyDebug() QString ReportAccount::debugName() const { return m_nameHierarchy.join("|"); } // MyMoneyAccount:fullHierarchy() QString ReportAccount::fullName() const { return m_nameHierarchy.join(": "); } // MyMoneyAccount:isTopCategory() bool ReportAccount::isTopLevel() const { return (m_nameHierarchy.size() == 1); } // MyMoneyAccount:hierarchyDepth() unsigned ReportAccount::hierarchyDepth() const { return (m_nameHierarchy.size()); } ReportAccount ReportAccount::parent() const { return ReportAccount(parentAccountId()); } ReportAccount ReportAccount::topParent() const { DEBUG_ENTER(Q_FUNC_INFO); MyMoneyFile* file = MyMoneyFile::instance(); QString resultid = id(); QString parentid = parentAccountId(); while (!parentid.isEmpty() && !file->isStandardAccount(parentid)) { // take on the identity of our parent resultid = parentid; // and try again parentid = file->account(resultid).parentAccountId(); } return ReportAccount(resultid); } QString ReportAccount::topParentName() const { return m_nameHierarchy.first(); } -bool ReportAccount::isLiquidLiability() const -{ - return accountType() == eMyMoney::Account::Type::CreditCard; - -} - - - - } // end namespace reports diff --git a/kmymoney/reports/reportaccount.h b/kmymoney/plugins/views/reports/core/reportaccount.h similarity index 95% rename from kmymoney/reports/reportaccount.h rename to kmymoney/plugins/views/reports/core/reportaccount.h index 90d236808..86b7c88e9 100644 --- a/kmymoney/reports/reportaccount.h +++ b/kmymoney/plugins/views/reports/core/reportaccount.h @@ -1,242 +1,228 @@ /* * Copyright 2005 Ace Jones * Copyright 2006-2012 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. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef REPORTACCOUNT_H #define REPORTACCOUNT_H // ---------------------------------------------------------------------------- // QT Includes #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes #include "mymoneyaccount.h" namespace reports { /** * This is a MyMoneyAccount as viewed from the reporting engine. * * All reporting methods should use ReportAccount INSTEAD OF * MyMoneyAccount at all times. * * The primary functionality this provides is a full chain of account * hierarchy that is easy to traverse. It's needed because the PivotTable * grid needs to store and sort by the full account hierarchy, while still * having access to the account itself for currency conversion. * * In addition, several other convenience functions are provided that may * be worth moving into MyMoneyAccount at some point. * * @author Ace Jones * * @short **/ class ReportAccount: public MyMoneyAccount { private: QStringList m_nameHierarchy; public: /** * Default constructor * * Needed to allow this object to be stored in a QMap. */ ReportAccount(); /** * Copy constructor * * Needed to allow this object to be stored in a QMap. */ ReportAccount(const ReportAccount&); /** * Regular constructor * * @param accountid Account which this account descriptor should be based off of */ explicit ReportAccount(const QString& accountid); /** * Regular constructor * * @param accountid Account which this account descriptor should be based off of */ explicit ReportAccount(const MyMoneyAccount& accountid); /** * @param right The object to compare against * @return bool True if this account's fully-qualified hierarchy name * is less than that of the given qccount */ bool operator<(const ReportAccount& right) const; /** * Returns the price of this account's underlying currency on the indicated date, * translated into the account's deep currency * * There are three different currencies in play with a single Account: * - The underlying currency: What currency the account itself is denominated in * - The deep currency: The underlying currency's own underlying currency. This * is only a factor if the underlying currency of this account IS NOT a * currency itself, but is some other kind of security. In that case, the * underlying security has its own currency. The deep currency is the * currency of the underlying security. On the other hand, if the account * has a currency itself, then the deep currency == the underlying currency, * and this function will return 1.0. * - The base currency: The base currency of the user's overall file * * @param date The date in question * @param exactDate if @a true, the @a date must be exact, otherwise * the last known price prior to this date can also be used * @a false is the default * @return MyMoneyMoney The value of the account's currency on that date */ MyMoneyMoney deepCurrencyPrice(const QDate& date, bool exactDate = false) const; /** * Returns the price of this account's deep currency on the indicated date, * translated into the base currency * * @param date The date in question * @param exactDate if @a true, the @a date must be exact, otherwise * the last known price prior to this date can also be used * @a false is the default * @return MyMoneyMoney The value of the account's currency on that date */ MyMoneyMoney baseCurrencyPrice(const QDate& date, bool exactDate = false) const; /** * Returns the price of this account's deep currency on the indicated date, * translated into the base currency * * @param foreignCurrency The currency on which the price will be returned * @param date The date in question * @param exactDate if @a true, the @a date must be exact, otherwise * the last known price prior to this date can also be used * @a false is the default * @return MyMoneyMoney The value of the account's currency on that date */ MyMoneyMoney foreignCurrencyPrice(const QString foreignCurrency, const QDate& date, bool exactDate = false) const; /** * Fetch the trading symbol of this account's deep currency * * @return The account's currency trading currency object */ MyMoneySecurity currency() const; - /** - * Determine if this account's deep currency is different from the file's - * base currency - * - * @return bool True if this account is in a foreign currency - */ - bool isForeignCurrency() const; - /** * The name of only this account. No matter how deep the hierarchy, this * method only returns the last name in the list, which is the engine name] * of this account. * * @return QString The account's name */ QString name() const; /** * The entire hierarchy of this account descriptor * This is similar to debugName(), however debugName() is not guaranteed * to always look pretty, while fullName() is. So if the user is ever * going to see the results, use fullName(). * * @return QString The account's full hierarchy */ QString fullName() const; /** * The entire hierarchy of this account descriptor, suitable for displaying * in debugging output * * @return QString The account's full hierarchy (suitable for debugging) */ QString debugName() const; /** * Whether this account is a 'top level' parent account. This means that * it's parent is an account class, like asset, liability, expense or income * * @return bool True if this account is a top level parent account */ /*inline*/ bool isTopLevel() const; /** * Returns the name of the top level parent account * * (See isTopLevel for a definition of 'top level parent') * * @return QString The name of the top level parent account */ /*inline*/ QString topParentName() const; /** * Returns a report account containing the top parent account * * @return ReportAccount The account of the top parent */ ReportAccount topParent() const; /** * Returns a report account containing the immediate parent account * * @return ReportAccount The account of the immediate parent */ ReportAccount parent() const; /** * Returns the number of accounts in this account's hierarchy. If this is a * Top Category, it returns 1. If it's parent is a Top Category, returns 2, * etc. * * @return unsigned Hierarchy depth */ unsigned hierarchyDepth() const; - /** - * Returns whether this account is a liquid liability - * - */ - bool isLiquidLiability() const; - protected: /** * Calculates the full account hierarchy of this account */ void calculateAccountHierarchy(); }; } // end namespace reports #endif // REPORTACCOUNT_H diff --git a/kmymoney/reports/reportdebug.h b/kmymoney/plugins/views/reports/core/reportdebug.h similarity index 100% rename from kmymoney/reports/reportdebug.h rename to kmymoney/plugins/views/reports/core/reportdebug.h diff --git a/kmymoney/reports/reporttable.cpp b/kmymoney/plugins/views/reports/core/reporttable.cpp similarity index 100% rename from kmymoney/reports/reporttable.cpp rename to kmymoney/plugins/views/reports/core/reporttable.cpp diff --git a/kmymoney/reports/reporttable.h b/kmymoney/plugins/views/reports/core/reporttable.h similarity index 100% rename from kmymoney/reports/reporttable.h rename to kmymoney/plugins/views/reports/core/reporttable.h diff --git a/kmymoney/reports/tests/CMakeLists.txt b/kmymoney/plugins/views/reports/core/tests/CMakeLists.txt similarity index 100% rename from kmymoney/reports/tests/CMakeLists.txt rename to kmymoney/plugins/views/reports/core/tests/CMakeLists.txt diff --git a/kmymoney/reports/tests/chart-test.cpp b/kmymoney/plugins/views/reports/core/tests/chart-test.cpp similarity index 100% rename from kmymoney/reports/tests/chart-test.cpp rename to kmymoney/plugins/views/reports/core/tests/chart-test.cpp diff --git a/kmymoney/reports/tests/chart-test.h b/kmymoney/plugins/views/reports/core/tests/chart-test.h similarity index 100% copy from kmymoney/reports/tests/chart-test.h copy to kmymoney/plugins/views/reports/core/tests/chart-test.h diff --git a/kmymoney/reports/tests/kreportsview-test.h b/kmymoney/plugins/views/reports/core/tests/kreportsview-test.h similarity index 100% rename from kmymoney/reports/tests/kreportsview-test.h rename to kmymoney/plugins/views/reports/core/tests/kreportsview-test.h diff --git a/kmymoney/reports/tests/pivotgrid-test.cpp b/kmymoney/plugins/views/reports/core/tests/pivotgrid-test.cpp similarity index 100% rename from kmymoney/reports/tests/pivotgrid-test.cpp rename to kmymoney/plugins/views/reports/core/tests/pivotgrid-test.cpp diff --git a/kmymoney/reports/tests/pivotgrid-test.h b/kmymoney/plugins/views/reports/core/tests/pivotgrid-test.h similarity index 100% rename from kmymoney/reports/tests/pivotgrid-test.h rename to kmymoney/plugins/views/reports/core/tests/pivotgrid-test.h diff --git a/kmymoney/reports/tests/pivottable-test.cpp b/kmymoney/plugins/views/reports/core/tests/pivottable-test.cpp similarity index 100% rename from kmymoney/reports/tests/pivottable-test.cpp rename to kmymoney/plugins/views/reports/core/tests/pivottable-test.cpp diff --git a/kmymoney/reports/tests/pivottable-test.h b/kmymoney/plugins/views/reports/core/tests/pivottable-test.h similarity index 100% rename from kmymoney/reports/tests/pivottable-test.h rename to kmymoney/plugins/views/reports/core/tests/pivottable-test.h diff --git a/kmymoney/reports/tests/querytable-test.cpp b/kmymoney/plugins/views/reports/core/tests/querytable-test.cpp similarity index 100% rename from kmymoney/reports/tests/querytable-test.cpp rename to kmymoney/plugins/views/reports/core/tests/querytable-test.cpp diff --git a/kmymoney/reports/tests/querytable-test.h b/kmymoney/plugins/views/reports/core/tests/querytable-test.h similarity index 100% rename from kmymoney/reports/tests/querytable-test.h rename to kmymoney/plugins/views/reports/core/tests/querytable-test.h diff --git a/kmymoney/reports/tests/reportstestcommon.cpp b/kmymoney/plugins/views/reports/core/tests/reportstestcommon.cpp similarity index 100% rename from kmymoney/reports/tests/reportstestcommon.cpp rename to kmymoney/plugins/views/reports/core/tests/reportstestcommon.cpp diff --git a/kmymoney/reports/tests/reportstestcommon.h b/kmymoney/plugins/views/reports/core/tests/reportstestcommon.h similarity index 100% rename from kmymoney/reports/tests/reportstestcommon.h rename to kmymoney/plugins/views/reports/core/tests/reportstestcommon.h diff --git a/kmymoney/dialogs/kbalancechartdlg.cpp b/kmymoney/plugins/views/reports/kbalancechartdlg.cpp similarity index 100% rename from kmymoney/dialogs/kbalancechartdlg.cpp rename to kmymoney/plugins/views/reports/kbalancechartdlg.cpp diff --git a/kmymoney/dialogs/kbalancechartdlg.h b/kmymoney/plugins/views/reports/kbalancechartdlg.h similarity index 100% rename from kmymoney/dialogs/kbalancechartdlg.h rename to kmymoney/plugins/views/reports/kbalancechartdlg.h diff --git a/kmymoney/plugins/views/reports/kreportsview.cpp b/kmymoney/plugins/views/reports/kreportsview.cpp index d7ba8400b..7db64d11d 100644 --- a/kmymoney/plugins/views/reports/kreportsview.cpp +++ b/kmymoney/plugins/views/reports/kreportsview.cpp @@ -1,710 +1,720 @@ /*************************************************************************** kreportsview.cpp - description ------------------- begin : Sat Mar 27 2004 copyright : (C) 2000-2004 by Michael Edwardes email : mte@users.sourceforge.net Javier Campos Morales Felix Rodriguez John C Thomas Baumgart Kevin Tambascio Ace Jones (C) 2017 Łukasz Wojniłowicz ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "kreportsview_p.h" #include // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef ENABLE_WEBENGINE #include #else #include #endif // ---------------------------------------------------------------------------- // KDE Includes #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "ui_reportcontrol.h" #include "mymoneyfile.h" #include "mymoneyreport.h" #include "mymoneyexception.h" #include "kmymoneysettings.h" #include "querytable.h" #include "objectinfotable.h" #include "kreportconfigurationfilterdlg.h" #include "icons/icons.h" +#include "kbalancechartdlg.h" #include #include "tocitem.h" #include "tocitemgroup.h" #include "tocitemreport.h" #include "kreportchartview.h" #include "pivottable.h" #include "reporttable.h" #include "reportcontrolimpl.h" #include "mymoneyenums.h" #include "menuenums.h" using namespace reports; using namespace eMyMoney; using namespace Icons; #define VIEW_LEDGER "ledger" #define VIEW_SCHEDULE "schedule" #define VIEW_WELCOME "welcome" #define VIEW_HOME "home" #define VIEW_REPORTS "reports" /** * KReportsView Implementation */ KReportsView::KReportsView(QWidget *parent) : KMyMoneyViewBase(*new KReportsViewPrivate(this), parent) { connect(pActions[eMenu::Action::ReportAccountTransactions], &QAction::triggered, this, &KReportsView::slotReportAccountTransactions); } KReportsView::~KReportsView() { } void KReportsView::executeCustomAction(eView::Action action) { switch(action) { case eView::Action::Refresh: refresh(); break; case eView::Action::SetDefaultFocus: { Q_D(KReportsView); QTimer::singleShot(0, d->m_tocTreeWidget, SLOT(setFocus())); } break; case eView::Action::Print: slotPrintView(); break; case eView::Action::CleanupBeforeFileClose: slotCloseAll(); break; + case eView::Action::ShowBalanceChart: + { + Q_D(KReportsView); + QPointer dlg = new KBalanceChartDlg(d->m_currentAccount, this); + dlg->exec(); + delete dlg; + } + break; + default: break; } } void KReportsView::refresh() { Q_D(KReportsView); if (isVisible()) { d->loadView(); d->m_needsRefresh = false; } else { d->m_needsRefresh = true; } } void KReportsView::showEvent(QShowEvent * event) { Q_D(KReportsView); if (d->m_needLoad) d->init(); emit customActionRequested(View::Reports, eView::Action::AboutToShow); if (d->m_needsRefresh) refresh(); if (auto tab = dynamic_cast(d->m_reportTabWidget->currentWidget())) emit reportSelected(tab->report()); else emit reportSelected(MyMoneyReport()); // don't forget base class implementation QWidget::showEvent(event); } void KReportsView::updateActions(const MyMoneyObject& obj) { Q_D(KReportsView); if (typeid(obj) != typeid(MyMoneyAccount) && (obj.id().isEmpty() && d->m_currentAccount.id().isEmpty())) // do not disable actions that were already disabled))) return; const auto& acc = static_cast(obj); bool b; if (MyMoneyFile::instance()->isStandardAccount(acc.id())) { b = false; } else { switch (acc.accountType()) { case eMyMoney::Account::Type::Asset: case eMyMoney::Account::Type::Liability: case eMyMoney::Account::Type::Equity: case eMyMoney::Account::Type::Checkings: b = true; break; default: b = false; break; } } pActions[eMenu::Action::ReportAccountTransactions]->setEnabled(b); d->m_currentAccount = acc; } void KReportsView::slotOpenUrl(const QUrl &url) { QString view = url.fileName(); if (view.isEmpty()) return; QString command = QUrlQuery(url).queryItemValue("command"); QString id = QUrlQuery(url).queryItemValue("id"); QString tid = QUrlQuery(url).queryItemValue("tid"); if (view == VIEW_REPORTS) { if (command.isEmpty()) { // slotRefreshView(); } else if (command == QLatin1String("print")) slotPrintView(); else if (command == QLatin1String("copy")) slotCopyView(); else if (command == QLatin1String("save")) slotSaveView(); else if (command == QLatin1String("configure")) slotConfigure(); else if (command == QLatin1String("duplicate")) slotDuplicate(); else if (command == QLatin1String("close")) slotCloseCurrent(); else if (command == QLatin1String("delete")) slotDelete(); else qWarning() << i18n("Unknown command '%1' in KReportsView::slotOpenUrl()", qPrintable(command)); } else if (view == VIEW_LEDGER) { emit selectByVariant(QVariantList {QVariant(id), QVariant(tid)}, eView::Intent::ShowTransaction); } else { qWarning() << i18n("Unknown view '%1' in KReportsView::slotOpenUrl()", qPrintable(view)); } } void KReportsView::slotPrintView() { Q_D(KReportsView); if (auto tab = dynamic_cast(d->m_reportTabWidget->currentWidget())) tab->print(); } void KReportsView::slotCopyView() { Q_D(KReportsView); if (auto tab = dynamic_cast(d->m_reportTabWidget->currentWidget())) tab->copyToClipboard(); } void KReportsView::slotSaveView() { Q_D(KReportsView); if (auto tab = dynamic_cast(d->m_reportTabWidget->currentWidget())) { QString filterList = i18nc("CSV (Filefilter)", "CSV files") + QLatin1String(" (*.csv);;") + i18nc("HTML (Filefilter)", "HTML files") + QLatin1String(" (*.html)"); QUrl newURL = QFileDialog::getSaveFileUrl(this, i18n("Export as"), QUrl::fromLocalFile(KRecentDirs::dir(":kmymoney-export")), filterList, &d->m_selectedExportFilter); if (!newURL.isEmpty()) { KRecentDirs::add(":kmymoney-export", newURL.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).path()); QString newName = newURL.toDisplayString(QUrl::PreferLocalFile); try { tab->saveAs(newName, true); } catch (const MyMoneyException &e) { KMessageBox::error(this, i18n("Failed to save: %1", QString::fromLatin1(e.what()))); } } } } void KReportsView::slotConfigure() { Q_D(KReportsView); QString cm = "KReportsView::slotConfigure"; auto tab = dynamic_cast(d->m_reportTabWidget->currentWidget()); if (!tab) // nothing to do return; int tabNr = d->m_reportTabWidget->currentIndex(); tab->updateDataRange(); // range will be needed during configuration, but cannot be obtained earlier MyMoneyReport report = tab->report(); if (report.comment() == i18n("Default Report") || report.comment() == i18n("Generated Report")) { report.setComment(i18n("Custom Report")); report.setName(i18n("%1 (Customized)", report.name())); } QPointer dlg = new KReportConfigurationFilterDlg(report); if (dlg->exec()) { MyMoneyReport newreport = dlg->getConfig(); // If this report has an ID, then MODIFY it, otherwise ADD it MyMoneyFileTransaction ft; try { if (! newreport.id().isEmpty()) { MyMoneyFile::instance()->modifyReport(newreport); ft.commit(); tab->modifyReport(newreport); d->m_reportTabWidget->setTabText(tabNr, newreport.name()); d->m_reportTabWidget->setCurrentIndex(tabNr) ; } else { MyMoneyFile::instance()->addReport(newreport); ft.commit(); QString reportGroupName = newreport.group(); // find report group TocItemGroup* tocItemGroup = d->m_allTocItemGroups[reportGroupName]; if (!tocItemGroup) { QString error = i18n("Could not find reportgroup \"%1\" for report \"%2\".\nPlease report this error to the developer's list: kmymoney-devel@kde.org", reportGroupName, newreport.name()); // write to messagehandler qWarning() << cm << error; // also inform user KMessageBox::error(d->m_reportTabWidget, error, i18n("Critical Error")); // cleanup delete dlg; return; } // do not add TocItemReport to TocItemGroup here, // this is done in loadView d->addReportTab(newreport); } } catch (const MyMoneyException &e) { KMessageBox::error(this, i18n("Failed to configure report: %1", QString::fromLatin1(e.what()))); } } delete dlg; } void KReportsView::slotDuplicate() { Q_D(KReportsView); QString cm = "KReportsView::slotDuplicate"; auto tab = dynamic_cast(d->m_reportTabWidget->currentWidget()); if (!tab) { // nothing to do return; } MyMoneyReport dupe = tab->report(); dupe.setName(i18n("Copy of %1", dupe.name())); if (dupe.comment() == i18n("Default Report")) dupe.setComment(i18n("Custom Report")); dupe.clearId(); QPointer dlg = new KReportConfigurationFilterDlg(dupe); if (dlg->exec()) { MyMoneyReport newReport = dlg->getConfig(); MyMoneyFileTransaction ft; try { MyMoneyFile::instance()->addReport(newReport); ft.commit(); QString reportGroupName = newReport.group(); // find report group TocItemGroup* tocItemGroup = d->m_allTocItemGroups[reportGroupName]; if (!tocItemGroup) { QString error = i18n("Could not find reportgroup \"%1\" for report \"%2\".\nPlease report this error to the developer's list: kmymoney-devel@kde.org", reportGroupName, newReport.name()); // write to messagehandler qWarning() << cm << error; // also inform user KMessageBox::error(d->m_reportTabWidget, error, i18n("Critical Error")); // cleanup delete dlg; return; } // do not add TocItemReport to TocItemGroup here, // this is done in loadView d->addReportTab(newReport); } catch (const MyMoneyException &e) { QString error = i18n("Cannot add report, reason: \"%1\"", e.what()); // write to messagehandler qWarning() << cm << error; // also inform user KMessageBox::error(d->m_reportTabWidget, error, i18n("Critical Error")); } } delete dlg; } void KReportsView::slotDelete() { Q_D(KReportsView); auto tab = dynamic_cast(d->m_reportTabWidget->currentWidget()); if (!tab) { // nothing to do return; } MyMoneyReport report = tab->report(); if (! report.id().isEmpty()) { if (KMessageBox::Continue == d->deleteReportDialog(report.name())) { // close the tab and then remove the report so that it is not // generated again during the following loadView() call slotClose(d->m_reportTabWidget->currentIndex()); MyMoneyFileTransaction ft; MyMoneyFile::instance()->removeReport(report); ft.commit(); } } else { KMessageBox::information(this, QString("") + i18n("%1 is a default report, so it cannot be deleted.", report.name()) + QString(""), i18n("Delete Report?")); } } void KReportsView::slotOpenReport(const QString& id) { Q_D(KReportsView); if (id.isEmpty()) { // nothing to do return; } KReportTab* page = 0; // Find the tab which contains the report int index = 1; while (index < d->m_reportTabWidget->count()) { auto current = dynamic_cast(d->m_reportTabWidget->widget(index)); if (current && current->report().id() == id) { page = current; break; } ++index; } // Show the tab, or create a new one, as needed if (page) d->m_reportTabWidget->setCurrentIndex(index); else d->addReportTab(MyMoneyFile::instance()->report(id)); } void KReportsView::slotOpenReport(const MyMoneyReport& report) { Q_D(KReportsView); if (d->m_needLoad) d->init(); qDebug() << Q_FUNC_INFO << " " << report.name(); KReportTab* page = 0; // Find the tab which contains the report indicated by this list item int index = 1; while (index < d->m_reportTabWidget->count()) { auto current = dynamic_cast(d->m_reportTabWidget->widget(index)); if (current && current->report().name() == report.name()) { page = current; break; } ++index; } // Show the tab, or create a new one, as needed if (page) d->m_reportTabWidget->setCurrentIndex(index); else d->addReportTab(report); if (!isVisible()) emit switchViewRequested(View::Reports); } void KReportsView::slotItemDoubleClicked(QTreeWidgetItem* item, int) { Q_D(KReportsView); auto tocItem = dynamic_cast(item); if (tocItem && !tocItem->isReport()) { // toggle the expanded-state for reportgroup-items item->setExpanded(item->isExpanded() ? false : true); // nothing else to do for reportgroup-items return; } TocItemReport* reportTocItem = dynamic_cast(tocItem); MyMoneyReport& report = reportTocItem->getReport(); KReportTab* page = 0; // Find the tab which contains the report indicated by this list item int index = 1; while (index < d->m_reportTabWidget->count()) { auto current = dynamic_cast(d->m_reportTabWidget->widget(index)); if (current) { // If this report has an ID, we'll use the ID to match if (! report.id().isEmpty()) { if (current->report().id() == report.id()) { page = current; break; } } // Otherwise, use the name to match. THIS ASSUMES that no 2 default reports // have the same name...but that would be pretty a boneheaded thing to do. else { if (current->report().name() == report.name()) { page = current; break; } } } ++index; } // Show the tab, or create a new one, as needed if (page) d->m_reportTabWidget->setCurrentIndex(index); else d->addReportTab(report); } void KReportsView::slotToggleChart() { Q_D(KReportsView); if (auto tab = dynamic_cast(d->m_reportTabWidget->currentWidget())) tab->toggleChart(); } void KReportsView::slotCloseCurrent() { Q_D(KReportsView); slotClose(d->m_reportTabWidget->currentIndex()); } void KReportsView::slotClose(int index) { Q_D(KReportsView); if (auto tab = dynamic_cast(d->m_reportTabWidget->widget(index))) { d->m_reportTabWidget->removeTab(index); tab->setReadyToDelete(true); } } void KReportsView::slotCloseAll() { Q_D(KReportsView); if(!d->m_needLoad) { while (true) { if (auto tab = dynamic_cast(d->m_reportTabWidget->widget(1))) { d->m_reportTabWidget->removeTab(1); tab->setReadyToDelete(true); } else break; } } } void KReportsView::slotListContextMenu(const QPoint & p) { Q_D(KReportsView); QTreeWidgetItem *item = d->m_tocTreeWidget->itemAt(p); if (!item) { return; } auto tocItem = dynamic_cast(item); if (tocItem && !tocItem->isReport()) { // currently there is no context menu for reportgroup items return; } QMenu* contextmenu = new QMenu(this); contextmenu->addAction(i18nc("To open a new report", "&Open"), this, SLOT(slotOpenFromList())); contextmenu->addAction(i18nc("Configure a report", "&Configure"), this, SLOT(slotConfigureFromList())); contextmenu->addAction(i18n("&New report"), this, SLOT(slotNewFromList())); // Only add this option if it's a custom report. Default reports cannot be deleted auto reportTocItem = dynamic_cast(tocItem); if (reportTocItem) { MyMoneyReport& report = reportTocItem->getReport(); if (! report.id().isEmpty()) { contextmenu->addAction(i18n("&Delete"), this, SLOT(slotDeleteFromList())); } } contextmenu->popup(d->m_tocTreeWidget->mapToGlobal(p)); } void KReportsView::slotOpenFromList() { Q_D(KReportsView); if (auto tocItem = dynamic_cast(d->m_tocTreeWidget->currentItem())) slotItemDoubleClicked(tocItem, 0); } void KReportsView::slotConfigureFromList() { Q_D(KReportsView); if (auto tocItem = dynamic_cast(d->m_tocTreeWidget->currentItem())) { slotItemDoubleClicked(tocItem, 0); slotConfigure(); } } void KReportsView::slotNewFromList() { Q_D(KReportsView); if (auto tocItem = dynamic_cast(d->m_tocTreeWidget->currentItem())) { slotItemDoubleClicked(tocItem, 0); slotDuplicate(); } } void KReportsView::slotDeleteFromList() { Q_D(KReportsView); if (auto tocItem = dynamic_cast(d->m_tocTreeWidget->currentItem())) { if (auto reportTocItem = dynamic_cast(tocItem)) { MyMoneyReport& report = reportTocItem->getReport(); // If this report does not have an ID, it's a default report and cannot be deleted if (! report.id().isEmpty() && KMessageBox::Continue == d->deleteReportDialog(report.name())) { // check if report's tab is open; start from 1 because 0 is toc tab for (int i = 1; i < d->m_reportTabWidget->count(); ++i) { auto tab = dynamic_cast(d->m_reportTabWidget->widget(i)); if (tab && tab->report().id() == report.id()) { slotClose(i); // if open, close it, so no crash when switching to it break; } } MyMoneyFileTransaction ft; MyMoneyFile::instance()->removeReport(report); ft.commit(); } } } } void KReportsView::slotSelectByObject(const MyMoneyObject& obj, eView::Intent intent) { switch(intent) { case eView::Intent::UpdateActions: updateActions(obj); break; case eView::Intent::OpenObject: slotOpenReport(static_cast(obj)); default: break; } } void KReportsView::slotReportAccountTransactions() { Q_D(KReportsView); // Generate a transaction report that contains transactions for only the // currently selected account. if (!d->m_currentAccount.id().isEmpty()) { MyMoneyReport report( eMyMoney::Report::RowType::Account, eMyMoney::Report::QueryColumn::Number | eMyMoney::Report::QueryColumn::Payee | eMyMoney::Report::QueryColumn::Category, eMyMoney::TransactionFilter::Date::YearToDate, eMyMoney::Report::DetailLevel::All, i18n("%1 YTD Account Transactions", d->m_currentAccount.name()), i18n("Generated Report") ); report.setGroup(i18n("Transactions")); report.addAccount(d->m_currentAccount.id()); emit customActionRequested(View::Reports, eView::Action::SwitchView); slotOpenReport(report); } } // Make sure, that these definitions are only used within this file // this does not seem to be necessary, but when building RPMs the // build option 'final' is used and all CPP files are concatenated. // So it could well be, that in another CPP file these definitions // are also used. #undef VIEW_LEDGER #undef VIEW_SCHEDULE #undef VIEW_WELCOME #undef VIEW_HOME #undef VIEW_REPORTS diff --git a/kmymoney/plugins/views/reports/reportsview.cpp b/kmymoney/plugins/views/reports/reportsview.cpp index ac0cf795e..5ed1bfaf2 100644 --- a/kmymoney/plugins/views/reports/reportsview.cpp +++ b/kmymoney/plugins/views/reports/reportsview.cpp @@ -1,63 +1,331 @@ /*************************************************************************** reportsview.cpp ------------------- copyright : (C) 2018 by Łukasz Wojniłowicz email : lukasz.wojnilowicz@gmail.com ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "reportsview.h" // ---------------------------------------------------------------------------- // QT Includes // ---------------------------------------------------------------------------- // KDE Includes #include #include // ---------------------------------------------------------------------------- // Project Includes #include "viewinterface.h" #include "kreportsview.h" +#include "kreportchartview.h" +#include "kmymoneysettings.h" +#include "pivottable.h" +#include "pivotgrid.h" +#include "mymoneyfile.h" +#include "mymoneysecurity.h" +#include "mymoneyenums.h" +#include "reportsviewenums.h" + +#define VIEW_LEDGER "ledger" ReportsView::ReportsView(QObject *parent, const QVariantList &args) : KMyMoneyPlugin::Plugin(parent, "reportsview"/*must be the same as X-KDE-PluginInfo-Name*/), m_view(nullptr) { Q_UNUSED(args) setComponentName("reportsview", i18n("Reports view")); // For information, announce that we have been loaded. qDebug("Plugins: reportsview loaded"); } ReportsView::~ReportsView() { qDebug("Plugins: reportsview unloaded"); } void ReportsView::plug() { m_view = new KReportsView; viewInterface()->addView(m_view, i18n("Reports"), View::Reports); } void ReportsView::unplug() { viewInterface()->removeView(View::Reports); } +QVariant ReportsView::requestData(const QString &arg, uint type) +{ + switch(type) { + case eWidgetPlugin::WidgetType::NetWorthForecast: + return QVariant::fromValue(netWorthForecast()); + case eWidgetPlugin::WidgetType::NetWorthForecastWithArgs: + return QVariant::fromValue(netWorthForecast(arg)); + case eWidgetPlugin::WidgetType::Budget: + return QVariant(budget()); + default: + return QVariant(); + } + +} + +QWidget *ReportsView::netWorthForecast() const +{ + MyMoneyReport reportCfg = MyMoneyReport( + eMyMoney::Report::RowType::AssetLiability, + static_cast(eMyMoney::Report::ColumnType::Months), + eMyMoney::TransactionFilter::Date::UserDefined, // overridden by the setDateFilter() call below + eMyMoney::Report::DetailLevel::Total, + i18n("Net Worth Forecast"), + i18n("Generated Report")); + + reportCfg.setChartByDefault(true); + reportCfg.setChartCHGridLines(false); + reportCfg.setChartSVGridLines(false); + reportCfg.setChartDataLabels(false); + reportCfg.setChartType(eMyMoney::Report::ChartType::Line); + reportCfg.setIncludingSchedules(false); + reportCfg.addAccountGroup(eMyMoney::Account::Type::Asset); + reportCfg.addAccountGroup(eMyMoney::Account::Type::Liability); + reportCfg.setColumnsAreDays(true); + reportCfg.setConvertCurrency(true); + reportCfg.setIncludingForecast(true); + reportCfg.setDateFilter(QDate::currentDate(), QDate::currentDate().addDays(+ 90)); + reports::PivotTable table(reportCfg); + + auto chartWidget = new reports::KReportChartView(nullptr); + + table.drawChart(*chartWidget); + return chartWidget; +} + +QWidget *ReportsView::netWorthForecast(const QString &arg) const +{ + const QStringList liArgs = arg.split(';'); + if (liArgs.count() != 4) + return new QWidget(); + + eMyMoney::Report::DetailLevel detailLevel[4] = { eMyMoney::Report::DetailLevel::All, eMyMoney::Report::DetailLevel::Top, eMyMoney::Report::DetailLevel::Group, eMyMoney::Report::DetailLevel::Total }; + + MyMoneyReport reportCfg = MyMoneyReport( + eMyMoney::Report::RowType::AssetLiability, + static_cast(eMyMoney::Report::ColumnType::Months), + eMyMoney::TransactionFilter::Date::UserDefined, // overridden by the setDateFilter() call below + detailLevel[liArgs.at(0).toInt()], + i18n("Net Worth Forecast"), + i18n("Generated Report")); + + reportCfg.setChartByDefault(true); + reportCfg.setChartCHGridLines(false); + reportCfg.setChartSVGridLines(false); + reportCfg.setChartType(eMyMoney::Report::ChartType::Line); + reportCfg.setIncludingSchedules(false); + // FIXME: this causes a crash + //reportCfg.setColumnsAreDays( true ); + reportCfg.setChartDataLabels(false); + reportCfg.setConvertCurrency(true); + reportCfg.setIncludingForecast(true); + reportCfg.setDateFilter(QDate::currentDate(), QDate::currentDate().addDays(liArgs.at(2).toLongLong())); + reports::PivotTable table(reportCfg); + + auto forecastChart = new reports::KReportChartView(nullptr); + forecastChart->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + table.drawChart(*forecastChart); + + // Adjust the size + forecastChart->resize(liArgs.at(2).toInt() - 10, liArgs.at(3).toInt()); + //forecastChart->show(); + forecastChart->update(); + return forecastChart; +} + +QString ReportsView::budget() const +{ + const auto file = MyMoneyFile::instance(); + + QString html; + if (file->countBudgets() == 0) { + html += QString(""); + html += QString("
%1
").arg(i18n("You have no budgets to display.")); + html += QString(""); + return html; + } + + auto prec = MyMoneyMoney::denomToPrec(file->baseCurrency().smallestAccountFraction()); + auto isOverrun = false; + int i = 0; + + //config report just like "Monthly Budgeted vs Actual + MyMoneyReport reportCfg = MyMoneyReport( + eMyMoney::Report::RowType::BudgetActual, + static_cast(eMyMoney::Report::ColumnType::Months), + eMyMoney::TransactionFilter::Date::CurrentMonth, + eMyMoney::Report::DetailLevel::All, + i18n("Monthly Budgeted vs. Actual"), + i18n("Generated Report")); + + reportCfg.setBudget("Any", true); + + reports::PivotTable table(reportCfg); + + reports::PivotGrid grid = table.grid(); + + //div header + html += "
" + i18n("Budget") + "
\n
 
\n"; + + //display budget summary + html += ""; + html += ""; + html += ""; + html += ""; + html += ""; + html += ""; + html += ""; + + html += QString(""); + + MyMoneyMoney totalBudgetValue = grid.m_total[reports::eBudget].m_total; + MyMoneyMoney totalActualValue = grid.m_total[reports::eActual].m_total; + MyMoneyMoney totalBudgetDiffValue = grid.m_total[reports::eBudgetDiff].m_total; + + QString totalBudgetAmount = totalBudgetValue.formatMoney(file->baseCurrency().tradingSymbol(), prec); + QString totalActualAmount = totalActualValue.formatMoney(file->baseCurrency().tradingSymbol(), prec); + QString totalBudgetDiffAmount = totalBudgetDiffValue.formatMoney(file->baseCurrency().tradingSymbol(), prec); + + html += QString("").arg(showColoredAmount(totalBudgetAmount, totalBudgetValue.isNegative())); + html += QString("").arg(showColoredAmount(totalActualAmount, totalActualValue.isNegative())); + html += QString("").arg(showColoredAmount(totalBudgetDiffAmount, totalBudgetDiffValue.isNegative())); + html += ""; + html += "
"; + html += i18n("Current Month Summary"); + html += "
"; + html += i18n("Budgeted"); + html += ""; + html += i18n("Actual"); + html += ""; + html += i18n("Difference"); + html += "
%1%1%1
"; + + //budget overrun + html += "
 
\n"; + html += ""; + html += ""; + html += ""; + html += ""; + html += ""; + html += ""; + html += ""; + html += ""; + + reports::PivotGrid::iterator it_outergroup = grid.begin(); + while (it_outergroup != grid.end()) { + i = 0; + reports::PivotOuterGroup::iterator it_innergroup = (*it_outergroup).begin(); + while (it_innergroup != (*it_outergroup).end()) { + reports::PivotInnerGroup::iterator it_row = (*it_innergroup).begin(); + while (it_row != (*it_innergroup).end()) { + //column number is 1 because the report includes only current month + if (it_row.value()[reports::eBudgetDiff].value(1).isNegative()) { + //get report account to get the name later + reports::ReportAccount rowname = it_row.key(); + + //write the outergroup if it is the first row of outergroup being shown + if (i == 0) { + html += ""; + html += QString("").arg(MyMoneyAccount::accountTypeToString(rowname.accountType())); + html += ""; + } + html += QString("").arg(i++ & 0x01 ? "even" : "odd"); + + //get values from grid + MyMoneyMoney actualValue = it_row.value()[reports::eActual][1]; + MyMoneyMoney budgetValue = it_row.value()[reports::eBudget][1]; + MyMoneyMoney budgetDiffValue = it_row.value()[reports::eBudgetDiff][1]; + + //format amounts + QString actualAmount = actualValue.formatMoney(file->baseCurrency().tradingSymbol(), prec); + QString budgetAmount = budgetValue.formatMoney(file->baseCurrency().tradingSymbol(), prec); + QString budgetDiffAmount = budgetDiffValue.formatMoney(file->baseCurrency().tradingSymbol(), prec); + + //account name + html += QString(""; + + //show amounts + html += QString("").arg(showColoredAmount(budgetAmount, budgetValue.isNegative())); + html += QString("").arg(showColoredAmount(actualAmount, actualValue.isNegative())); + html += QString("").arg(showColoredAmount(budgetDiffAmount, budgetDiffValue.isNegative())); + html += ""; + + //set the flag that there are overruns + isOverrun = true; + } + ++it_row; + } + ++it_innergroup; + } + ++it_outergroup; + } + + //if no negative differences are found, then inform that + if (!isOverrun) { + html += QString::fromLatin1("").arg(((i++ & 1) == 1) ? QLatin1String("even") : QLatin1String("odd")); + html += QString::fromLatin1("").arg(i18n("No Budget Categories have been overrun")); + html += ""; + } + html += "
"; + html += i18n("Budget Overruns"); + html += "
"; + html += i18n("Account"); + html += ""; + html += i18n("Budgeted"); + html += ""; + html += i18n("Actual"); + html += ""; + html += i18n("Difference"); + html += "
%1
") + link(VIEW_LEDGER, QString("?id=%1").arg(rowname.id()), QString()) + rowname.name() + linkend() + "%1%1%1
%1
"; + return html; +} + +QString ReportsView::showColoredAmount(const QString &amount, bool isNegative) const +{ + if (isNegative) { + //if negative, get the settings for negative numbers + return QString("%2").arg(KMyMoneySettings::schemeColor(SchemeColor::Negative).name(), amount); + } + + //if positive, return the same string + return amount; +} + +QString ReportsView::link(const QString& view, const QString& query, const QString& _title) const +{ + QString titlePart; + QString title(_title); + if (!title.isEmpty()) + titlePart = QString(" title=\"%1\"").arg(title.replace(QLatin1Char(' '), " ")); + + return QString("").arg(view, query, titlePart); +} + +QString ReportsView::linkend() const +{ + return QStringLiteral(""); +} + K_PLUGIN_FACTORY_WITH_JSON(ReportsViewFactory, "reportsview.json", registerPlugin();) #include "reportsview.moc" diff --git a/kmymoney/plugins/views/reports/reportsview.h b/kmymoney/plugins/views/reports/reportsview.h index 134abe57d..9c25356f9 100644 --- a/kmymoney/plugins/views/reports/reportsview.h +++ b/kmymoney/plugins/views/reports/reportsview.h @@ -1,47 +1,67 @@ /*************************************************************************** reportsview.h ------------------- copyright : (C) 2018 by Łukasz Wojniłowicz email : lukasz.wojnilowicz@gmail.com ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #ifndef REPORTSVIEW_H #define REPORTSVIEW_H // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // QT Includes // Project Includes #include "kmymoneyplugin.h" class KReportsView; -class ReportsView : public KMyMoneyPlugin::Plugin +class ReportsView : public KMyMoneyPlugin::Plugin, public KMyMoneyPlugin::DataPlugin { Q_OBJECT + Q_INTERFACES(KMyMoneyPlugin::DataPlugin) public: explicit ReportsView(QObject *parent, const QVariantList &args); ~ReportsView() final; void plug() final override; void unplug() final override; + QVariant requestData(const QString &arg, uint type) final override; + private: + QWidget *netWorthForecast() const; + /** + * @brief netWorthForecast + * @param arg consists of following arguments: + * 1) detail level of a chart (eMyMoney::Report::DetailLevel) + * 2) forecast days of a chart + * 3) width of a chart + * 3) height a chart + * correctly written arg variable looks as follows: + * "2;5;800;600" + * @return self-handling widget with a chart + */ + QWidget *netWorthForecast(const QString &arg) const; + QString budget() const; + QString showColoredAmount(const QString &amount, bool isNegative) const; + QString link(const QString& view, const QString& query, const QString& _title) const; + QString linkend() const; KReportsView* m_view; }; #endif diff --git a/kmymoney/reports/tests/chart-test.h b/kmymoney/plugins/views/reports/reportsviewenums.h similarity index 68% rename from kmymoney/reports/tests/chart-test.h rename to kmymoney/plugins/views/reports/reportsviewenums.h index 415b8b832..f36aa67cc 100644 --- a/kmymoney/reports/tests/chart-test.h +++ b/kmymoney/plugins/views/reports/reportsviewenums.h @@ -1,33 +1,29 @@ /* - * Copyright 2016-2018 Christian Dávid - * Copyright 2017 Ralf Habacker + * Copyright 2018 Łukasz Wojniłowicz * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -#ifndef CHARTTEST_H -#define CHARTTEST_H +#ifndef REPORTSVIEWENUMS_H +#define REPORTSVIEWENUMS_H -#include +namespace eWidgetPlugin { + enum WidgetType { + NetWorthForecast, + NetWorthForecastWithArgs, // for forecast view + Budget + }; +} -class ChartTest: public QObject -{ - Q_OBJECT - -private slots: - void createChart(); - -}; - -#endif // CHARTTEST_H +#endif diff --git a/kmymoney/views/kaccountsview.cpp b/kmymoney/views/kaccountsview.cpp index ecd5b6754..6eb204ba9 100644 --- a/kmymoney/views/kaccountsview.cpp +++ b/kmymoney/views/kaccountsview.cpp @@ -1,565 +1,562 @@ /*************************************************************************** kaccountsview.cpp ------------------- copyright : (C) 2007 by Thomas Baumgart (C) 2017, 2018 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 "kaccountsview_p.h" #include // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include // ---------------------------------------------------------------------------- // Project Includes #include "onlinejobadministration.h" #include "knewaccountwizard.h" -#include "kbalancechartdlg.h" #include "kmymoneyutils.h" #include "kmymoneysettings.h" #include "storageenums.h" #include "menuenums.h" using namespace Icons; KAccountsView::KAccountsView(QWidget *parent) : KMyMoneyAccountsViewBase(*new KAccountsViewPrivate(this), parent) { connect(pActions[eMenu::Action::NewAccount], &QAction::triggered, this, &KAccountsView::slotNewAccount); connect(pActions[eMenu::Action::EditAccount], &QAction::triggered, this, &KAccountsView::slotEditAccount); connect(pActions[eMenu::Action::DeleteAccount], &QAction::triggered, this, &KAccountsView::slotDeleteAccount); connect(pActions[eMenu::Action::CloseAccount], &QAction::triggered, this, &KAccountsView::slotCloseAccount); connect(pActions[eMenu::Action::ReopenAccount], &QAction::triggered, this, &KAccountsView::slotReopenAccount); connect(pActions[eMenu::Action::ChartAccountBalance], &QAction::triggered, this, &KAccountsView::slotChartAccountBalance); connect(pActions[eMenu::Action::MapOnlineAccount], &QAction::triggered, this, &KAccountsView::slotAccountMapOnline); connect(pActions[eMenu::Action::UnmapOnlineAccount], &QAction::triggered, this, &KAccountsView::slotAccountUnmapOnline); connect(pActions[eMenu::Action::UpdateAccount], &QAction::triggered, this, &KAccountsView::slotAccountUpdateOnline); connect(pActions[eMenu::Action::UpdateAllAccounts], &QAction::triggered, this, &KAccountsView::slotAccountUpdateOnlineAll); } KAccountsView::~KAccountsView() { } void KAccountsView::executeCustomAction(eView::Action action) { switch(action) { case eView::Action::Refresh: refresh(); break; case eView::Action::SetDefaultFocus: { Q_D(KAccountsView); QTimer::singleShot(0, d->ui->m_accountTree, SLOT(setFocus())); } break; default: break; } } void KAccountsView::refresh() { Q_D(KAccountsView); if (!isVisible()) { d->m_needsRefresh = true; return; } d->m_needsRefresh = false; // TODO: check why the invalidate is needed here d->m_proxyModel->invalidate(); d->m_proxyModel->setHideClosedAccounts(KMyMoneySettings::hideClosedAccounts()); d->m_proxyModel->setHideEquityAccounts(!KMyMoneySettings::expertMode()); if (KMyMoneySettings::showCategoriesInAccountsView()) { d->m_proxyModel->addAccountGroup(QVector {eMyMoney::Account::Type::Income, eMyMoney::Account::Type::Expense}); } else { d->m_proxyModel->removeAccountType(eMyMoney::Account::Type::Income); d->m_proxyModel->removeAccountType(eMyMoney::Account::Type::Expense); } // reinitialize the default state of the hidden categories label d->m_haveUnusedCategories = false; d->ui->m_hiddenCategories->hide(); // hides label d->m_proxyModel->setHideUnusedIncomeExpenseAccounts(KMyMoneySettings::hideUnusedCategory()); } void KAccountsView::showEvent(QShowEvent * event) { Q_D(KAccountsView); if (!d->m_proxyModel) d->init(); emit customActionRequested(View::Accounts, eView::Action::AboutToShow); if (d->m_needsRefresh) refresh(); // don't forget base class implementation QWidget::showEvent(event); } void KAccountsView::updateActions(const MyMoneyObject& obj) { Q_D(KAccountsView); const auto file = MyMoneyFile::instance(); if (typeid(obj) != typeid(MyMoneyAccount) && (obj.id().isEmpty() && d->m_currentAccount.id().isEmpty())) // do not disable actions that were already disabled) return; const auto& acc = static_cast(obj); const QVector actionsToBeDisabled { eMenu::Action::NewAccount, eMenu::Action::EditAccount, eMenu::Action::DeleteAccount, eMenu::Action::CloseAccount, eMenu::Action::ReopenAccount, eMenu::Action::ChartAccountBalance, eMenu::Action::UnmapOnlineAccount, eMenu::Action::MapOnlineAccount, eMenu::Action::UpdateAccount }; for (const auto& a : actionsToBeDisabled) pActions[a]->setEnabled(false); pActions[eMenu::Action::NewAccount]->setEnabled(true); if (acc.id().isEmpty()) { d->m_currentAccount = MyMoneyAccount(); return; } else if (file->isStandardAccount(acc.id())) { d->m_currentAccount = acc; return; } d->m_currentAccount = acc; switch (acc.accountGroup()) { case eMyMoney::Account::Type::Asset: case eMyMoney::Account::Type::Liability: case eMyMoney::Account::Type::Equity: { pActions[eMenu::Action::EditAccount]->setEnabled(true); pActions[eMenu::Action::DeleteAccount]->setEnabled(!file->isReferenced(acc)); auto b = acc.isClosed() ? true : false; pActions[eMenu::Action::ReopenAccount]->setEnabled(b); pActions[eMenu::Action::CloseAccount]->setEnabled(!b); if (!acc.isClosed()) { b = (d->canCloseAccount(acc) == KAccountsViewPrivate::AccountCanClose) ? true : false; pActions[eMenu::Action::CloseAccount]->setEnabled(b); d->hintCloseAccountAction(acc, pActions[eMenu::Action::CloseAccount]); } pActions[eMenu::Action::ChartAccountBalance]->setEnabled(true); if (d->m_currentAccount.hasOnlineMapping()) { pActions[eMenu::Action::UnmapOnlineAccount]->setEnabled(true); if (d->m_onlinePlugins) { // check if provider is available QMap::const_iterator it_p; it_p = d->m_onlinePlugins->constFind(d->m_currentAccount.onlineBankingSettings().value(QLatin1String("provider")).toLower()); if (it_p != d->m_onlinePlugins->constEnd()) { QStringList protocols; (*it_p)->protocols(protocols); if (protocols.count() > 0) { pActions[eMenu::Action::UpdateAccount]->setEnabled(true); } } } } else { pActions[eMenu::Action::MapOnlineAccount]->setEnabled(d->m_onlinePlugins && !d->m_onlinePlugins->isEmpty()); } break; } default: break; } QBitArray skip((int)eStorage::Reference::Count); if (!d->m_currentAccount.id().isEmpty()) { if (!file->isStandardAccount(d->m_currentAccount.id())) { switch (d->m_currentAccount.accountGroup()) { case eMyMoney::Account::Type::Asset: case eMyMoney::Account::Type::Liability: case eMyMoney::Account::Type::Equity: break; default: break; } } } } /** * The view is notified that an unused income expense account has been hidden. */ void KAccountsView::slotUnusedIncomeExpenseAccountHidden() { Q_D(KAccountsView); d->m_haveUnusedCategories = true; d->ui->m_hiddenCategories->setVisible(d->m_haveUnusedCategories); } void KAccountsView::slotNetWorthChanged(const MyMoneyMoney &netWorth) { Q_D(KAccountsView); d->netBalProChanged(netWorth, d->ui->m_totalProfitsLabel, View::Accounts); } void KAccountsView::slotShowAccountMenu(const MyMoneyAccount& acc) { Q_UNUSED(acc); pMenus[eMenu::Menu::Account]->exec(QCursor::pos()); } void KAccountsView::slotSelectByObject(const MyMoneyObject& obj, eView::Intent intent) { switch(intent) { case eView::Intent::UpdateActions: updateActions(obj); break; case eView::Intent::OpenContextMenu: slotShowAccountMenu(static_cast(obj)); break; default: break; } } void KAccountsView::slotSelectByVariant(const QVariantList& variant, eView::Intent intent) { Q_D(KAccountsView); switch (intent) { case eView::Intent::UpdateNetWorth: if (variant.count() == 1 && d->m_proxyModel) slotNetWorthChanged(variant.first().value()); break; case eView::Intent::SetOnlinePlugins: if (variant.count() == 1) d->m_onlinePlugins = static_cast*>(variant.first().value()); break; default: break; } } void KAccountsView::slotNewAccount() { MyMoneyAccount account; account.setOpeningDate(KMyMoneySettings::firstFiscalDate()); NewAccountWizard::Wizard::newAccount(account); } void KAccountsView::slotEditAccount() { Q_D(KAccountsView); switch (d->m_currentAccount.accountType()) { case eMyMoney::Account::Type::Loan: case eMyMoney::Account::Type::AssetLoan: d->editLoan(); break; default: d->editAccount(); break; } emit selectByObject(d->m_currentAccount, eView::Intent::None); } void KAccountsView::slotDeleteAccount() { Q_D(KAccountsView); if (d->m_currentAccount.id().isEmpty()) return; // need an account ID const auto file = MyMoneyFile::instance(); // can't delete standard accounts or account which still have transactions assigned if (file->isStandardAccount(d->m_currentAccount.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); if (file->isReferenced(d->m_currentAccount, skip)) return; MyMoneyFileTransaction ft; // 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 auto selectedAccountName = d->m_currentAccount.name(); try { file->removeAccount(d->m_currentAccount); d->m_currentAccount.clearId(); emit selectByObject(MyMoneyAccount(), eView::Intent::None); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::error(this, i18n("Unable to delete account '%1'. Cause: %2", selectedAccountName, QString::fromLatin1(e.what()))); } } void KAccountsView::slotCloseAccount() { Q_D(KAccountsView); MyMoneyFileTransaction ft; try { d->m_currentAccount.setClosed(true); MyMoneyFile::instance()->modifyAccount(d->m_currentAccount); emit selectByObject(d->m_currentAccount, eView::Intent::None); ft.commit(); if (KMyMoneySettings::hideClosedAccounts()) KMessageBox::information(this, 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."), i18n("Information"), "CloseAccountInfo"); } catch (const MyMoneyException &) { } } void KAccountsView::slotReopenAccount() { Q_D(KAccountsView); const auto file = MyMoneyFile::instance(); MyMoneyFileTransaction ft; try { auto& acc = d->m_currentAccount; while (acc.isClosed()) { acc.setClosed(false); file->modifyAccount(acc); acc = file->account(acc.parentAccountId()); } emit selectByObject(d->m_currentAccount, eView::Intent::None); ft.commit(); } catch (const MyMoneyException &) { } } void KAccountsView::slotChartAccountBalance() { Q_D(KAccountsView); if (!d->m_currentAccount.id().isEmpty()) { - QPointer dlg = new KBalanceChartDlg(d->m_currentAccount, this); - dlg->exec(); - delete dlg; + emit customActionRequested(View::Accounts, eView::Action::ShowBalanceChart); } } void KAccountsView::slotNewCategory() { Q_D(KAccountsView); KNewAccountDlg::newCategory(d->m_currentAccount, MyMoneyAccount()); } void KAccountsView::slotNewPayee(const QString& nameBase, QString& id) { KMyMoneyUtils::newPayee(nameBase, id); } void KAccountsView::slotAccountUnmapOnline() { Q_D(KAccountsView); // no account selected if (d->m_currentAccount.id().isEmpty()) return; // not a mapped account if (!d->m_currentAccount.hasOnlineMapping()) 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_currentAccount.name())), i18n("Remove mapping to online account")) == KMessageBox::Yes) { MyMoneyFileTransaction ft; try { d->m_currentAccount.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_currentAccount.deletePair("StatementKey"); MyMoneyFile::instance()->modifyAccount(d->m_currentAccount); 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", QString::fromLatin1(e.what()))); } } updateActions(d->m_currentAccount); } void KAccountsView::slotAccountMapOnline() { Q_D(KAccountsView); // no account selected if (d->m_currentAccount.id().isEmpty()) return; // already an account mapped if (d->m_currentAccount.hasOnlineMapping()) return; // check if user tries to map a brokerageAccount if (d->m_currentAccount.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 (!d->m_onlinePlugins) 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.toLower()); if (it_p != d->m_onlinePlugins->constEnd()) { // plugin found, call it MyMoneyKeyValueContainer settings; if ((*it_p)->mapAccount(d->m_currentAccount, settings)) { settings["provider"] = provider.toLower(); MyMoneyAccount acc(d->m_currentAccount); 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", QString::fromLatin1(e.what()))); } } } updateActions(d->m_currentAccount); } void KAccountsView::slotAccountUpdateOnlineAll() { Q_D(KAccountsView); QList accList; MyMoneyFile::instance()->accountList(accList); QList::iterator it_a; QMap::const_iterator it_p; // 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).hasOnlineMapping() || d->m_onlinePlugins->find((*it_a).onlineBankingSettings().value("provider").toLower()) == d->m_onlinePlugins->end()) { it_a = accList.erase(it_a); } else ++it_a; } const QVector disabledActions {eMenu::Action::UpdateAccount, eMenu::Action::UpdateAllAccounts}; for (const auto& a : disabledActions) pActions[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").toLower()); (*it_p)->updateAccount(*it_a, cnt != 0); --cnt; } // re-enable the disabled actions updateActions(d->m_currentAccount); } void KAccountsView::slotAccountUpdateOnline() { Q_D(KAccountsView); // no account selected if (d->m_currentAccount.id().isEmpty()) return; // no online account mapped if (!d->m_currentAccount.hasOnlineMapping()) return; const QVector disabledActions {eMenu::Action::UpdateAccount, eMenu::Action::UpdateAllAccounts}; for (const auto& a : disabledActions) pActions[a]->setEnabled(false); // find the provider QMap::const_iterator it_p; it_p = d->m_onlinePlugins->constFind(d->m_currentAccount.onlineBankingSettings().value("provider").toLower()); if (it_p != d->m_onlinePlugins->constEnd()) { // plugin found, call it (*it_p)->updateAccount(d->m_currentAccount); } // re-enable the disabled actions updateActions(d->m_currentAccount); } diff --git a/kmymoney/views/khomeview_p.h b/kmymoney/views/khomeview_p.h index a8606353d..822bb2b6f 100644 --- a/kmymoney/views/khomeview_p.h +++ b/kmymoney/views/khomeview_p.h @@ -1,2001 +1,1853 @@ /*************************************************************************** khomeview_p.h - description ------------------- begin : Tue Jan 22 2002 copyright : (C) 2000-2002 by Michael Edwardes Javier Campos Morales Felix Rodriguez John C Thomas Baumgart Kevin Tambascio (C) 2017 by Łukasz Wojniłowicz ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #ifndef KHOMEVIEW_P_H #define KHOMEVIEW_P_H #include "khomeview.h" #include // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include #include #include #include #include #ifdef ENABLE_WEBENGINE #include #else #include #include #endif // ---------------------------------------------------------------------------- // KDE Includes #include #include #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "kmymoneyviewbase_p.h" #include "mymoneyutils.h" #include "kmymoneyutils.h" #include "kwelcomepage.h" #include "kmymoneysettings.h" #include "mymoneyfile.h" #include "mymoneyaccount.h" #include "mymoneyprice.h" +#include "mymoneyreport.h" +#include "mymoneymoney.h" #include "mymoneyforecast.h" -#include "kreportchartview.h" -#include "pivottable.h" -#include "pivotgrid.h" -#include "reportaccount.h" #include "mymoneysplit.h" #include "mymoneytransaction.h" #include "icons.h" #include "kmymoneywebpage.h" #include "mymoneyschedule.h" #include "mymoneysecurity.h" #include "mymoneyexception.h" +#include "kmymoneyplugin.h" #include "mymoneyenums.h" #include "menuenums.h" +#include "plugins/views/reports/reportsviewenums.h" #define VIEW_LEDGER "ledger" #define VIEW_SCHEDULE "schedule" #define VIEW_WELCOME "welcome" #define VIEW_HOME "home" #define VIEW_REPORTS "reports" using namespace Icons; using namespace eMyMoney; /** * @brief Converts a QPixmap to an data URI scheme * * According to RFC 2397 * * @param pixmap Source to convert * @return full data URI */ QString QPixmapToDataUri(const QPixmap& pixmap) { QImage image(pixmap.toImage()); QByteArray byteArray; QBuffer buffer(&byteArray); buffer.open(QIODevice::WriteOnly); image.save(&buffer, "PNG"); // writes the image in PNG format inside the buffer return QLatin1String("data:image/png;base64,") + QString(byteArray.toBase64()); } bool accountNameLess(const MyMoneyAccount &acc1, const MyMoneyAccount &acc2) { return acc1.name().localeAwareCompare(acc2.name()) < 0; } -using namespace reports; - class KHomeViewPrivate : public KMyMoneyViewBasePrivate { Q_DECLARE_PUBLIC(KHomeView) public: explicit KHomeViewPrivate(KHomeView *qq) : KMyMoneyViewBasePrivate(), q_ptr(qq), m_view(nullptr), m_showAllSchedules(false), m_needLoad(true), m_netWorthGraphLastValidSize(400, 300), m_currentPrinter(nullptr) { } ~KHomeViewPrivate() { // if user wants to remember the font size, store it here if (KMyMoneySettings::rememberZoomFactor() && m_view) { KMyMoneySettings::setZoomFactor(m_view->zoomFactor()); KMyMoneySettings::self()->save(); } } /** * Definition of bitmap used as argument for showAccounts(). */ enum paymentTypeE { Preferred = 1, ///< show preferred accounts Payment = 2 ///< show payment accounts }; void init() { Q_Q(KHomeView); m_needLoad = false; auto vbox = new QVBoxLayout(q); q->setLayout(vbox); vbox->setSpacing(6); vbox->setMargin(0); #ifdef ENABLE_WEBENGINE m_view = new QWebEngineView(q); #else m_view = new KWebView(q); #endif m_view->setPage(new MyQWebEnginePage(m_view)); vbox->addWidget(m_view); m_view->setHtml(KWelcomePage::welcomePage(), QUrl("file://")); #ifdef ENABLE_WEBENGINE q->connect(m_view->page(), &QWebEnginePage::urlChanged, q, &KHomeView::slotOpenUrl); #else m_view->page()->setLinkDelegationPolicy(QWebPage::DelegateAllLinks); q->connect(m_view->page(), &KWebPage::linkClicked, q, &KHomeView::slotOpenUrl); #endif q->connect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, q, &KHomeView::refresh); } /** * Print an account and its balance and limit */ void showAccountEntry(const MyMoneyAccount& acc, const MyMoneyMoney& value, const MyMoneyMoney& valueToMinBal, const bool showMinBal) { MyMoneyFile* file = MyMoneyFile::instance(); QString tmp; MyMoneySecurity currency = file->currency(acc.currencyId()); QString amount; QString amountToMinBal; //format amounts amount = MyMoneyUtils::formatMoney(value, acc, currency); amount.replace(QChar(' '), " "); if (showMinBal) { amountToMinBal = MyMoneyUtils::formatMoney(valueToMinBal, acc, currency); amountToMinBal.replace(QChar(' '), " "); } QString cellStatus, cellCounts, pathOK, pathTODO, pathNotOK; if (KMyMoneySettings::showBalanceStatusOfOnlineAccounts()) { //show account's online-status pathOK = QPixmapToDataUri(Icons::get(Icon::DialogOKApply).pixmap(QSize(16,16))); pathTODO = QPixmapToDataUri(Icons::get(Icon::MailReceive).pixmap(QSize(16,16))); pathNotOK = QPixmapToDataUri(Icons::get(Icon::DialogCancel).pixmap(QSize(16,16))); if (acc.value("lastImportedTransactionDate").isEmpty() || acc.value("lastStatementBalance").isEmpty()) cellStatus = '-'; else if (file->hasMatchingOnlineBalance(acc)) { if (file->hasNewerTransaction(acc.id(), QDate::fromString(acc.value("lastImportedTransactionDate"), Qt::ISODate))) cellStatus = QString("").arg(pathTODO); else cellStatus = QString("").arg(pathOK); } else cellStatus = QString("").arg(pathNotOK); tmp = QString("%1").arg(cellStatus); } tmp += QString("") + link(VIEW_LEDGER, QString("?id=%1").arg(acc.id())) + acc.name() + linkend() + ""; int countNotMarked = 0, countCleared = 0, countNotReconciled = 0; QString countStr; if (KMyMoneySettings::showCountOfUnmarkedTransactions() || KMyMoneySettings::showCountOfNotReconciledTransactions()) countNotMarked = file->countTransactionsWithSpecificReconciliationState(acc.id(), TransactionFilter::State::NotReconciled); if (KMyMoneySettings::showCountOfClearedTransactions() || KMyMoneySettings::showCountOfNotReconciledTransactions()) countCleared = file->countTransactionsWithSpecificReconciliationState(acc.id(), TransactionFilter::State::Cleared); if (KMyMoneySettings::showCountOfNotReconciledTransactions()) countNotReconciled = countNotMarked + countCleared; if (KMyMoneySettings::showCountOfUnmarkedTransactions()) { if (countNotMarked) countStr = QString("%1").arg(countNotMarked); else countStr = '-'; tmp += QString("%1").arg(countStr); } if (KMyMoneySettings::showCountOfClearedTransactions()) { if (countCleared) countStr = QString("%1").arg(countCleared); else countStr = '-'; tmp += QString("%1").arg(countStr); } if (KMyMoneySettings::showCountOfNotReconciledTransactions()) { if (countNotReconciled) countStr = QString("%1").arg(countNotReconciled); else countStr = '-'; tmp += QString("%1").arg(countStr); } if (KMyMoneySettings::showDateOfLastReconciliation()) { const auto lastReconciliationDate = acc.lastReconciliationDate().toString(Qt::SystemLocaleShortDate).replace(QChar(' '), " "); tmp += QString("%1").arg(lastReconciliationDate); } //show account balance tmp += QString("%1").arg(showColoredAmount(amount, value.isNegative())); //show minimum balance column if requested if (showMinBal) { //if it is an investment, show minimum balance empty if (acc.accountType() == Account::Type::Investment) { tmp += QString(" "); } else { //show minimum balance entry tmp += QString("%1").arg(showColoredAmount(amountToMinBal, valueToMinBal.isNegative())); } } // qDebug("accountEntry = '%s'", tmp.toLatin1()); m_html += tmp; } void showAccountEntry(const MyMoneyAccount& acc) { - MyMoneyFile* file = MyMoneyFile::instance(); - MyMoneySecurity currency = file->currency(acc.currencyId()); + const auto file = MyMoneyFile::instance(); MyMoneyMoney value; bool showLimit = KMyMoneySettings::showLimitInfo(); if (acc.accountType() == Account::Type::Investment) { //investment accounts show the balances of all its subaccounts value = investmentBalance(acc); //investment accounts have no minimum balance showAccountEntry(acc, value, MyMoneyMoney(), showLimit); } else { //get balance for normal accounts value = file->balance(acc.id(), QDate::currentDate()); if (acc.currencyId() != file->baseCurrency().id()) { - ReportAccount repAcc = ReportAccount(acc.id()); - MyMoneyMoney curPrice = repAcc.baseCurrencyPrice(QDate::currentDate()); - MyMoneyMoney baseValue = value * curPrice; + const auto curPrice = file->price(acc.tradingCurrencyId(), file->baseCurrency().id(), QDate::currentDate()); + const auto curRate = curPrice.rate(file->baseCurrency().id()); + auto baseValue = value * curRate; baseValue = baseValue.convert(file->baseCurrency().smallestAccountFraction()); m_total += baseValue; } else { m_total += value; } //if credit card or checkings account, show maximum credit if (acc.accountType() == Account::Type::CreditCard || acc.accountType() == Account::Type::Checkings) { QString maximumCredit = acc.value("maxCreditAbsolute"); if (maximumCredit.isEmpty()) { maximumCredit = acc.value("minBalanceAbsolute"); } MyMoneyMoney maxCredit = MyMoneyMoney(maximumCredit); showAccountEntry(acc, value, value - maxCredit, showLimit); } else { //otherwise use minimum balance QString minimumBalance = acc.value("minBalanceAbsolute"); MyMoneyMoney minBalance = MyMoneyMoney(minimumBalance); showAccountEntry(acc, value, value - minBalance, showLimit); } } } /** * @param acc the investment account * @return the balance in the currency of the investment account */ MyMoneyMoney investmentBalance(const MyMoneyAccount& acc) { auto file = MyMoneyFile::instance(); auto value = file->balance(acc.id(), QDate::currentDate()); foreach (const auto accountID, acc.accountList()) { auto stock = file->account(accountID); if (!stock.isClosed()) { try { MyMoneyMoney val; MyMoneyMoney balance = file->balance(stock.id(), QDate::currentDate()); MyMoneySecurity security = file->security(stock.currencyId()); const MyMoneyPrice &price = file->price(stock.currencyId(), security.tradingCurrency()); val = (balance * price.rate(security.tradingCurrency())).convertPrecision(security.pricePrecision()); // adjust value of security to the currency of the account MyMoneySecurity accountCurrency = file->currency(acc.currencyId()); val = val * file->price(security.tradingCurrency(), accountCurrency.id()).rate(accountCurrency.id()); val = val.convert(acc.fraction()); value += val; } catch (const MyMoneyException &e) { qWarning("%s", qPrintable(QString("cannot convert stock balance of %1 to base currency: %2").arg(stock.name(), e.what()))); } } } return value; } /** * Print text in the color set for negative numbers, if @p amount is negative * abd @p isNegative is true */ QString showColoredAmount(const QString& amount, bool isNegative) { if (isNegative) { //if negative, get the settings for negative numbers return QString("%2").arg(KMyMoneySettings::schemeColor(SchemeColor::Negative).name(), amount); } //if positive, return the same string return amount; } /** * Run the forecast */ void doForecast() { //clear m_accountList because forecast is about to changed m_accountList.clear(); //reinitialize the object m_forecast = KMyMoneyUtils::forecast(); //If forecastDays lower than accountsCycle, adjust to the first cycle if (m_forecast.accountsCycle() > m_forecast.forecastDays()) m_forecast.setForecastDays(m_forecast.accountsCycle()); //Get all accounts of the right type to calculate forecast m_forecast.doForecast(); } /** * Calculate the forecast balance after a payment has been made */ MyMoneyMoney forecastPaymentBalance(const MyMoneyAccount& acc, const MyMoneyMoney& payment, QDate& paymentDate) { //if paymentDate before or equal to currentDate set it to current date plus 1 //so we get to accumulate forecast balance correctly if (paymentDate <= QDate::currentDate()) paymentDate = QDate::currentDate().addDays(1); //check if the account is already there if (m_accountList.find(acc.id()) == m_accountList.end() || m_accountList[acc.id()].find(paymentDate) == m_accountList[acc.id()].end()) { if (paymentDate == QDate::currentDate()) { m_accountList[acc.id()][paymentDate] = m_forecast.forecastBalance(acc, paymentDate); } else { m_accountList[acc.id()][paymentDate] = m_forecast.forecastBalance(acc, paymentDate.addDays(-1)); } } m_accountList[acc.id()][paymentDate] = m_accountList[acc.id()][paymentDate] + payment; return m_accountList[acc.id()][paymentDate]; } void loadView() { m_view->setZoomFactor(KMyMoneySettings::zoomFactor()); QList list; if (MyMoneyFile::instance()->storage()) MyMoneyFile::instance()->accountList(list); if (list.isEmpty()) { m_view->setHtml(KWelcomePage::welcomePage(), QUrl("file://")); } else { // keep current location on page int scrollBarPos = 0; #ifdef ENABLE_WEBENGINE /// @todo cannot test this #else scrollBarPos = m_view->page()->mainFrame()->scrollBarValue(Qt::Vertical); #endif //clear the forecast flag so it will be reloaded m_forecast.setForecastDone(false); const QString filename = QStandardPaths::locate(QStandardPaths::AppConfigLocation, "html/kmymoney.css"); QString header = QString("\n\n").arg(QUrl::fromLocalFile(filename).url()); header += KMyMoneyUtils::variableCSS(); header += "\n"; QString footer = "\n"; m_html.clear(); m_html += header; m_html += QString("
%1
").arg(i18n("Your Financial Summary")); QStringList settings = KMyMoneySettings::listOfItems(); QStringList::ConstIterator it; for (it = settings.constBegin(); it != settings.constEnd(); ++it) { int option = (*it).toInt(); if (option > 0) { switch (option) { case 1: // payments showPayments(); break; case 2: // preferred accounts showAccounts(Preferred, i18n("Preferred Accounts")); break; case 3: // payment accounts // Check if preferred accounts are shown separately if (settings.contains("2")) { showAccounts(static_cast(Payment | Preferred), i18n("Payment Accounts")); } else { showAccounts(Payment, i18n("Payment Accounts")); } break; case 4: // favorite reports showFavoriteReports(); break; case 5: // forecast showForecast(); break; case 6: // net worth graph over all accounts showNetWorthGraph(); break; case 8: // assets and liabilities showAssetsLiabilities(); break; case 9: // budget showBudget(); break; case 10: // cash flow summary showCashFlowSummary(); break; } m_html += "
 
\n"; } } m_html += "
"; m_html += link(VIEW_WELCOME, QString()) + i18n("Show KMyMoney welcome page") + linkend(); m_html += "
"; m_html += "
"; m_html += footer; m_view->setHtml(m_html, QUrl("file://")); if (scrollBarPos) { #ifdef ENABLE_WEBENGINE /// @todo cannot test this #else m_view->page()->mainFrame()->setScrollBarValue(Qt::Vertical, scrollBarPos); #endif } } } void showNetWorthGraph() { Q_Q(KHomeView); - m_html += QString("
%1
\n
 
\n").arg(i18n("Net Worth Forecast")); - - MyMoneyReport reportCfg = MyMoneyReport( - eMyMoney::Report::RowType::AssetLiability, - static_cast(eMyMoney::Report::ColumnType::Months), - TransactionFilter::Date::UserDefined, // overridden by the setDateFilter() call below - eMyMoney::Report::DetailLevel::Total, - i18n("Net Worth Forecast"), - i18n("Generated Report")); - - reportCfg.setChartByDefault(true); - reportCfg.setChartCHGridLines(false); - reportCfg.setChartSVGridLines(false); - reportCfg.setChartDataLabels(false); - reportCfg.setChartType(eMyMoney::Report::ChartType::Line); - reportCfg.setIncludingSchedules(false); - reportCfg.addAccountGroup(Account::Type::Asset); - reportCfg.addAccountGroup(Account::Type::Liability); - reportCfg.setColumnsAreDays(true); - reportCfg.setConvertCurrency(true); - reportCfg.setIncludingForecast(true); - reportCfg.setDateFilter(QDate::currentDate(), QDate::currentDate().addDays(+ 90)); - reports::PivotTable table(reportCfg); - - reports::KReportChartView* chartWidget = new reports::KReportChartView(0); - - table.drawChart(*chartWidget); // Adjust the size QSize netWorthGraphSize = q->size(); netWorthGraphSize -= QSize(80, 30); - // consider the computed size valid only if it's smaller on both axes that the applications size -// if (netWorthGraphSize.width() < kmymoney->width() || netWorthGraphSize.height() < kmymoney->height()) { - m_netWorthGraphLastValidSize = netWorthGraphSize; -// } - chartWidget->resize(m_netWorthGraphLastValidSize); - - //save the chart to an image - QString chart = QPixmapToDataUri(chartWidget->coordinatePlane()->parent()->grab()); + m_netWorthGraphLastValidSize = netWorthGraphSize; + m_html += QString("
%1
\n
 
\n").arg(i18n("Net Worth Forecast")); m_html += QString(""); m_html += QString(""); - m_html += QString("").arg(chart); + + if (const auto reportsPlugin = pPlugins.data.value(QStringLiteral("reportsview"), nullptr)) { + const auto variantReport = reportsPlugin->requestData(QString(), eWidgetPlugin::WidgetType::NetWorthForecast); + if (!variantReport.isNull()) { + auto report = variantReport.value(); + report->resize(m_netWorthGraphLastValidSize); + m_html += QString("").arg(QPixmapToDataUri(report->grab())); + delete report; + } + } else { + m_html += QString("").arg(i18n("Enable reports plugin to see this chart.")); + } + m_html += QString(""); m_html += QString("
\"Networth\"
\"Networth\"
%1
"); - - //delete the widget since we no longer need it - delete chartWidget; } void showPayments() { MyMoneyFile* file = MyMoneyFile::instance(); QList overdues; QList schedule; int i = 0; //if forecast has not been executed yet, do it. if (!m_forecast.isForecastDone()) doForecast(); schedule = file->scheduleList(QString(), Schedule::Type::Any, Schedule::Occurrence::Any, Schedule::PaymentType::Any, QDate::currentDate(), QDate::currentDate().addMonths(1), false); overdues = file->scheduleList(QString(), Schedule::Type::Any, Schedule::Occurrence::Any, Schedule::PaymentType::Any, QDate(), QDate(), true); if (schedule.empty() && overdues.empty()) return; // HACK // Remove the finished schedules QList::Iterator d_it; //regular schedules d_it = schedule.begin(); while (d_it != schedule.end()) { if ((*d_it).isFinished()) { d_it = schedule.erase(d_it); continue; } ++d_it; } //overdue schedules d_it = overdues.begin(); while (d_it != overdues.end()) { if ((*d_it).isFinished()) { d_it = overdues.erase(d_it); continue; } ++d_it; } m_html += "
"; m_html += QString("
%1
\n").arg(i18n("Payments")); if (!overdues.isEmpty()) { m_html += "
 
\n"; qSort(overdues); QList::Iterator it; QList::Iterator it_f; m_html += ""; m_html += QString("\n").arg(showColoredAmount(i18n("Overdue payments"), true)); m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; for (it = overdues.begin(); it != overdues.end(); ++it) { // determine number of overdue payments int cnt = (*it).transactionsRemainingUntil(QDate::currentDate().addDays(-1)); m_html += QString("").arg(i++ & 0x01 ? "even" : "odd"); showPaymentEntry(*it, cnt); m_html += ""; } m_html += "
%1
"; m_html += i18n("Date"); m_html += ""; m_html += i18n("Schedule"); m_html += ""; m_html += i18n("Account"); m_html += ""; m_html += i18n("Amount"); m_html += ""; m_html += i18n("Balance after"); m_html += "
"; } if (!schedule.isEmpty()) { qSort(schedule); // Extract todays payments if any QList todays; QList::Iterator t_it; for (t_it = schedule.begin(); t_it != schedule.end();) { if ((*t_it).adjustedNextDueDate() == QDate::currentDate()) { todays.append(*t_it); (*t_it).setNextDueDate((*t_it).nextPayment(QDate::currentDate())); // if adjustedNextDueDate is still currentDate then remove it from // scheduled payments if ((*t_it).adjustedNextDueDate() == QDate::currentDate()) { t_it = schedule.erase(t_it); continue; } } ++t_it; } if (todays.count() > 0) { m_html += "
 
\n"; m_html += ""; m_html += QString("\n").arg(i18n("Today's due payments")); m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; for (t_it = todays.begin(); t_it != todays.end(); ++t_it) { m_html += QString("").arg(i++ & 0x01 ? "even" : "odd"); showPaymentEntry(*t_it); m_html += ""; } m_html += "
%1
"; m_html += i18n("Date"); m_html += ""; m_html += i18n("Schedule"); m_html += ""; m_html += i18n("Account"); m_html += ""; m_html += i18n("Amount"); m_html += ""; m_html += i18n("Balance after"); m_html += "
"; } if (!schedule.isEmpty()) { m_html += "
 
\n"; QList::Iterator it; m_html += ""; m_html += QString("\n").arg(i18n("Future payments")); m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; // show all or the first 6 entries int cnt; cnt = (m_showAllSchedules) ? -1 : 6; bool needMoreLess = m_showAllSchedules; QDate lastDate = QDate::currentDate().addMonths(1); qSort(schedule); do { it = schedule.begin(); if (it == schedule.end()) break; // if the next due date is invalid (schedule is finished) // we remove it from the list QDate nextDate = (*it).nextDueDate(); if (!nextDate.isValid()) { schedule.erase(it); continue; } if (nextDate > lastDate) break; if (cnt == 0) { needMoreLess = true; break; } // in case we've shown the current recurrence as overdue, // we don't show it here again, but keep the schedule // as it might show up later in the list again if (!(*it).isOverdue()) { if (cnt > 0) --cnt; m_html += QString("").arg(i++ & 0x01 ? "even" : "odd"); showPaymentEntry(*it); m_html += ""; // for single occurrence we have reported everything so we // better get out of here. if ((*it).occurrence() == Schedule::Occurrence::Once) { schedule.erase(it); continue; } } // if nextPayment returns an invalid date, setNextDueDate will // just skip it, resulting in a loop // we check the resulting date and erase the schedule if invalid if (!((*it).nextPayment((*it).nextDueDate())).isValid()) { schedule.erase(it); continue; } (*it).setNextDueDate((*it).nextPayment((*it).nextDueDate())); qSort(schedule); } while (1); if (needMoreLess) { m_html += QString("").arg(i++ & 0x01 ? "even" : "odd"); m_html += ""; m_html += ""; } m_html += "
%1
"; m_html += i18n("Date"); m_html += ""; m_html += i18n("Schedule"); m_html += ""; m_html += i18n("Account"); m_html += ""; m_html += i18n("Amount"); m_html += ""; m_html += i18n("Balance after"); m_html += "
"; if (m_showAllSchedules) { m_html += link(VIEW_SCHEDULE, QString("?mode=%1").arg("reduced")) + i18nc("Less...", "Show fewer schedules on the list") + linkend(); } else { m_html += link(VIEW_SCHEDULE, QString("?mode=%1").arg("full")) + i18nc("More...", "Show more schedules on the list") + linkend(); } m_html += "
"; } } m_html += "
"; } void showPaymentEntry(const MyMoneySchedule& sched, int cnt = 1) { QString tmp; MyMoneyFile* file = MyMoneyFile::instance(); try { MyMoneyAccount acc = sched.account(); if (!acc.id().isEmpty()) { MyMoneyTransaction t = sched.transaction(); // only show the entry, if it is still active if (!sched.isFinished()) { MyMoneySplit sp = t.splitByAccount(acc.id(), true); QString pathEnter = QPixmapToDataUri(Icons::get(Icon::KeyEnter).pixmap(QSize(16,16))); QString pathSkip = QPixmapToDataUri(Icons::get(Icon::MediaSkipForward).pixmap(QSize(16,16))); //show payment date tmp = QString("") + QLocale().toString(sched.adjustedNextDueDate(), QLocale::ShortFormat) + ""; if (!pathEnter.isEmpty()) tmp += link(VIEW_SCHEDULE, QString("?id=%1&mode=enter").arg(sched.id()), i18n("Enter schedule")) + QString("").arg(pathEnter) + linkend(); if (!pathSkip.isEmpty()) tmp += " " + link(VIEW_SCHEDULE, QString("?id=%1&mode=skip").arg(sched.id()), i18n("Skip schedule")) + QString("").arg(pathSkip) + linkend(); tmp += QString(" "); tmp += link(VIEW_SCHEDULE, QString("?id=%1&mode=edit").arg(sched.id()), i18n("Edit schedule")) + sched.name() + linkend(); //show quantity of payments overdue if any if (cnt > 1) tmp += i18np(" (%1 payment)", " (%1 payments)", cnt); //show account of the main split tmp += ""; tmp += QString(file->account(acc.id()).name()); //show amount of the schedule tmp += ""; const MyMoneySecurity& currency = MyMoneyFile::instance()->currency(acc.currencyId()); MyMoneyMoney payment = MyMoneyMoney(sp.value(t.commodity(), acc.currencyId()) * cnt); QString amount = MyMoneyUtils::formatMoney(payment, acc, currency); amount.replace(QChar(' '), " "); tmp += showColoredAmount(amount, payment.isNegative()); tmp += ""; //show balance after payments tmp += ""; QDate paymentDate = QDate(sched.adjustedNextDueDate()); MyMoneyMoney balanceAfter = forecastPaymentBalance(acc, payment, paymentDate); QString balance = MyMoneyUtils::formatMoney(balanceAfter, acc, currency); balance.replace(QChar(' '), " "); tmp += showColoredAmount(balance, balanceAfter.isNegative()); tmp += ""; // qDebug("paymentEntry = '%s'", tmp.toLatin1()); m_html += tmp; } } } catch (const MyMoneyException &e) { qDebug("Unable to display schedule entry: %s", e.what()); } } void showAccounts(paymentTypeE type, const QString& header) { MyMoneyFile* file = MyMoneyFile::instance(); int prec = MyMoneyMoney::denomToPrec(file->baseCurrency().smallestAccountFraction()); QList accounts; auto showClosedAccounts = KMyMoneySettings::showAllAccounts(); // get list of all accounts file->accountList(accounts); for (QList::Iterator it = accounts.begin(); it != accounts.end();) { bool removeAccount = false; if (!(*it).isClosed() || showClosedAccounts) { switch ((*it).accountType()) { case Account::Type::Expense: case Account::Type::Income: // never show a category account // Note: This might be different in a future version when // the homepage also shows category based information removeAccount = true; break; // Asset and Liability accounts are only shown if they // have the preferred flag set case Account::Type::Asset: case Account::Type::Liability: case Account::Type::Investment: // if preferred accounts are requested, then keep in list if ((*it).value("PreferredAccount") != "Yes" || (type & Preferred) == 0) { removeAccount = true; } break; // Check payment accounts. If payment and preferred is selected, // then always show them. If only payment is selected, then // show only if preferred flag is not set. case Account::Type::Checkings: case Account::Type::Savings: case Account::Type::Cash: case Account::Type::CreditCard: switch (type & (Payment | Preferred)) { case Payment: if ((*it).value("PreferredAccount") == "Yes") removeAccount = true; break; case Preferred: if ((*it).value("PreferredAccount") != "Yes") removeAccount = true; break; case Payment | Preferred: break; default: removeAccount = true; break; } break; // filter all accounts that are not used on homepage views default: removeAccount = true; break; } } else if ((*it).isClosed() || (*it).isInvest()) { // don't show if closed or a stock account removeAccount = true; } if (removeAccount) it = accounts.erase(it); else ++it; } if (!accounts.isEmpty()) { // sort the accounts by name qStableSort(accounts.begin(), accounts.end(), accountNameLess); QString tmp; int i = 0; tmp = "
" + header + "
\n
 
\n"; m_html += tmp; m_html += ""; m_html += ""; if (KMyMoneySettings::showBalanceStatusOfOnlineAccounts()) { QString pathStatusHeader = QPixmapToDataUri(Icons::get(Icon::Download).pixmap(QSize(16,16))); m_html += QString("").arg(pathStatusHeader); } m_html += ""; if (KMyMoneySettings::showCountOfUnmarkedTransactions()) m_html += QString(""); if (KMyMoneySettings::showCountOfClearedTransactions()) m_html += QString(""); if (KMyMoneySettings::showCountOfNotReconciledTransactions()) m_html += QString(""); if (KMyMoneySettings::showDateOfLastReconciliation()) m_html += QString("").arg(i18n("Last Reconciled")); m_html += ""; //only show limit info if user chose to do so if (KMyMoneySettings::showLimitInfo()) { m_html += ""; } m_html += ""; m_total = 0; QList::const_iterator it_m; for (it_m = accounts.constBegin(); it_m != accounts.constEnd(); ++it_m) { m_html += QString("").arg(i++ & 0x01 ? "even" : "odd"); showAccountEntry(*it_m); m_html += ""; } m_html += QString("").arg(i++ & 0x01 ? "even" : "odd"); QString amount = m_total.formatMoney(file->baseCurrency().tradingSymbol(), prec); if (KMyMoneySettings::showBalanceStatusOfOnlineAccounts()) m_html += ""; m_html += QString("").arg(i18n("Total")); if (KMyMoneySettings::showCountOfUnmarkedTransactions()) m_html += ""; if (KMyMoneySettings::showCountOfClearedTransactions()) m_html += ""; if (KMyMoneySettings::showCountOfNotReconciledTransactions()) m_html += ""; if (KMyMoneySettings::showDateOfLastReconciliation()) m_html += ""; m_html += QString("").arg(showColoredAmount(amount, m_total.isNegative())); m_html += "
"; m_html += i18n("Account"); m_html += "!MC!R%1"; m_html += i18n("Current Balance"); m_html += ""; m_html += i18n("To Minimum Balance / Maximum Credit"); m_html += "
%1%1
"; } } void showFavoriteReports() { QList reports = MyMoneyFile::instance()->reportList(); if (!reports.isEmpty()) { bool firstTime = 1; int row = 0; QList::const_iterator it_report = reports.constBegin(); while (it_report != reports.constEnd()) { if ((*it_report).isFavorite()) { if (firstTime) { m_html += QString("
%1
\n
 
\n").arg(i18n("Favorite Reports")); m_html += ""; m_html += ""; firstTime = false; } m_html += QString("") .arg(row++ & 0x01 ? "even" : "odd") .arg(link(VIEW_REPORTS, QString("?id=%1").arg((*it_report).id()))) .arg((*it_report).name()) .arg(linkend()) .arg((*it_report).comment()); } ++it_report; } if (!firstTime) m_html += "
"; m_html += i18n("Report"); m_html += ""; m_html += i18n("Comment"); m_html += "
%2%3%4%5
"; } } void showForecast() { MyMoneyFile* file = MyMoneyFile::instance(); QList accList; //if forecast has not been executed yet, do it. if (!m_forecast.isForecastDone()) doForecast(); accList = m_forecast.accountList(); if (accList.count() > 0) { // sort the accounts by name qStableSort(accList.begin(), accList.end(), accountNameLess); auto i = 0; auto colspan = 1; //get begin day auto beginDay = QDate::currentDate().daysTo(m_forecast.beginForecastDate()); //if begin day is today skip to next cycle if (beginDay == 0) beginDay = m_forecast.accountsCycle(); // Now output header m_html += QString("
%1
\n
 
\n").arg(i18n("%1 Day Forecast", m_forecast.forecastDays())); m_html += ""; m_html += ""; auto colWidth = 55 / (m_forecast.forecastDays() / m_forecast.accountsCycle()); for (i = 0; (i*m_forecast.accountsCycle() + beginDay) <= m_forecast.forecastDays(); ++i) { m_html += QString(""; colspan++; } m_html += ""; // Now output entries i = 0; QList::ConstIterator it_account; for (it_account = accList.constBegin(); it_account != accList.constEnd(); ++it_account) { //MyMoneyAccount acc = (*it_n); m_html += QString("").arg(i++ & 0x01 ? "even" : "odd"); m_html += QString(""; qint64 dropZero = -1; //account dropped below zero qint64 dropMinimum = -1; //account dropped below minimum balance QString minimumBalance = (*it_account).value("minimumBalance"); MyMoneyMoney minBalance = MyMoneyMoney(minimumBalance); MyMoneySecurity currency; MyMoneyMoney forecastBalance; //change account to deep currency if account is an investment if ((*it_account).isInvest()) { MyMoneySecurity underSecurity = file->security((*it_account).currencyId()); currency = file->security(underSecurity.tradingCurrency()); } else { currency = file->security((*it_account).currencyId()); } for (auto f = beginDay; f <= m_forecast.forecastDays(); f += m_forecast.accountsCycle()) { forecastBalance = m_forecast.forecastBalance(*it_account, QDate::currentDate().addDays(f)); QString amount; amount = MyMoneyUtils::formatMoney(forecastBalance, *it_account, currency); amount.replace(QChar(' '), " "); m_html += QString("").arg(showColoredAmount(amount, forecastBalance.isNegative())); } m_html += ""; //Check if the account is going to be below zero or below the minimal balance in the forecast period //Check if the account is going to be below minimal balance dropMinimum = m_forecast.daysToMinimumBalance(*it_account); //Check if the account is going to be below zero in the future dropZero = m_forecast.daysToZeroBalance(*it_account); // spit out possible warnings QString msg; // if a minimum balance has been specified, an appropriate warning will // only be shown, if the drop below 0 is on a different day or not present if (dropMinimum != -1 && !minBalance.isZero() && (dropMinimum < dropZero || dropZero == -1)) { switch (dropMinimum) { case 0: msg = i18n("The balance of %1 is below the minimum balance %2 today.", (*it_account).name(), MyMoneyUtils::formatMoney(minBalance, *it_account, currency)); msg = showColoredAmount(msg, true); break; default: msg = i18np("The balance of %2 will drop below the minimum balance %3 in %1 day.", "The balance of %2 will drop below the minimum balance %3 in %1 days.", dropMinimum - 1, (*it_account).name(), MyMoneyUtils::formatMoney(minBalance, *it_account, currency)); msg = showColoredAmount(msg, true); break; } if (!msg.isEmpty()) { m_html += QString("").arg(msg).arg(colspan); } } // a drop below zero is always shown msg.clear(); switch (dropZero) { case -1: break; case 0: if ((*it_account).accountGroup() == Account::Type::Asset) { msg = i18n("The balance of %1 is below %2 today.", (*it_account).name(), MyMoneyUtils::formatMoney(MyMoneyMoney(), *it_account, currency)); msg = showColoredAmount(msg, true); break; } if ((*it_account).accountGroup() == Account::Type::Liability) { msg = i18n("The balance of %1 is above %2 today.", (*it_account).name(), MyMoneyUtils::formatMoney(MyMoneyMoney(), *it_account, currency)); break; } break; default: if ((*it_account).accountGroup() == Account::Type::Asset) { msg = i18np("The balance of %2 will drop below %3 in %1 day.", "The balance of %2 will drop below %3 in %1 days.", dropZero, (*it_account).name(), MyMoneyUtils::formatMoney(MyMoneyMoney(), *it_account, currency)); msg = showColoredAmount(msg, true); break; } if ((*it_account).accountGroup() == Account::Type::Liability) { msg = i18np("The balance of %2 will raise above %3 in %1 day.", "The balance of %2 will raise above %3 in %1 days.", dropZero, (*it_account).name(), MyMoneyUtils::formatMoney(MyMoneyMoney(), *it_account, currency)); break; } } if (!msg.isEmpty()) { m_html += QString("").arg(msg).arg(colspan); } } m_html += "
"; m_html += i18n("Account"); m_html += "").arg(colWidth); m_html += i18ncp("Forecast days", "%1 day", "%1 days", i * m_forecast.accountsCycle() + beginDay); m_html += "
") + link(VIEW_LEDGER, QString("?id=%1").arg((*it_account).id())) + (*it_account).name() + linkend() + "").arg(colWidth); m_html += QString("%1
%1
%1
"; } } QString link(const QString& view, const QString& query, const QString& _title = QString()) const { QString titlePart; QString title(_title); if (!title.isEmpty()) titlePart = QString(" title=\"%1\"").arg(title.replace(QLatin1Char(' '), " ")); return QString("").arg(view, query, titlePart); } QString linkend() const { return QStringLiteral(""); } void showAssetsLiabilities() { QList accounts; QList::ConstIterator it; QList assets; QList liabilities; MyMoneyMoney netAssets; MyMoneyMoney netLiabilities; QString fontStart, fontEnd; MyMoneyFile* file = MyMoneyFile::instance(); int prec = MyMoneyMoney::denomToPrec(file->baseCurrency().smallestAccountFraction()); int i = 0; // get list of all accounts file->accountList(accounts); for (it = accounts.constBegin(); it != accounts.constEnd();) { if (!(*it).isClosed()) { switch ((*it).accountType()) { // group all assets into one list but make sure that investment accounts always show up case Account::Type::Investment: assets << *it; break; case Account::Type::Checkings: case Account::Type::Savings: case Account::Type::Cash: case Account::Type::Asset: case Account::Type::AssetLoan: // list account if it's the last in the hierarchy or has transactions in it if ((*it).accountList().isEmpty() || (file->transactionCount((*it).id()) > 0)) { assets << *it; } break; // group the liabilities into the other case Account::Type::CreditCard: case Account::Type::Liability: case Account::Type::Loan: // list account if it's the last in the hierarchy or has transactions in it if ((*it).accountList().isEmpty() || (file->transactionCount((*it).id()) > 0)) { liabilities << *it; } break; default: break; } } ++it; } //only do it if we have assets or liabilities account if (assets.count() > 0 || liabilities.count() > 0) { // sort the accounts by name qStableSort(assets.begin(), assets.end(), accountNameLess); qStableSort(liabilities.begin(), liabilities.end(), accountNameLess); QString statusHeader; if (KMyMoneySettings::showBalanceStatusOfOnlineAccounts()) { QString pathStatusHeader; pathStatusHeader = QPixmapToDataUri(Icons::get(Icon::ViewOutbox).pixmap(QSize(16,16))); statusHeader = QString("").arg(pathStatusHeader); } //print header m_html += "
" + i18n("Assets and Liabilities Summary") + "
\n
 
\n"; m_html += ""; //column titles m_html += ""; if (KMyMoneySettings::showBalanceStatusOfOnlineAccounts()) { m_html += ""; } m_html += ""; if (KMyMoneySettings::showCountOfUnmarkedTransactions()) m_html += ""; if (KMyMoneySettings::showCountOfClearedTransactions()) m_html += ""; if (KMyMoneySettings::showCountOfNotReconciledTransactions()) m_html += ""; if (KMyMoneySettings::showDateOfLastReconciliation()) m_html += ""; m_html += ""; //intermediate row to separate both columns m_html += ""; if (KMyMoneySettings::showBalanceStatusOfOnlineAccounts()) { m_html += ""; } m_html += ""; if (KMyMoneySettings::showCountOfUnmarkedTransactions()) m_html += ""; if (KMyMoneySettings::showCountOfClearedTransactions()) m_html += ""; if (KMyMoneySettings::showCountOfNotReconciledTransactions()) m_html += ""; if (KMyMoneySettings::showDateOfLastReconciliation()) m_html += ""; m_html += ""; QString placeHolder_Status, placeHolder_Counts; if (KMyMoneySettings::showBalanceStatusOfOnlineAccounts()) placeHolder_Status = ""; if (KMyMoneySettings::showCountOfUnmarkedTransactions()) placeHolder_Counts = ""; if (KMyMoneySettings::showCountOfClearedTransactions()) placeHolder_Counts += ""; if (KMyMoneySettings::showCountOfNotReconciledTransactions()) placeHolder_Counts += ""; if (KMyMoneySettings::showDateOfLastReconciliation()) placeHolder_Counts += ""; //get asset and liability accounts QList::const_iterator asset_it = assets.constBegin(); QList::const_iterator liabilities_it = liabilities.constBegin(); for (; asset_it != assets.constEnd() || liabilities_it != liabilities.constEnd();) { m_html += QString("").arg(i++ & 0x01 ? "even" : "odd"); //write an asset account if we still have any if (asset_it != assets.constEnd()) { MyMoneyMoney value; //investment accounts consolidate the balance of its subaccounts if ((*asset_it).accountType() == Account::Type::Investment) { value = investmentBalance(*asset_it); } else { value = MyMoneyFile::instance()->balance((*asset_it).id(), QDate::currentDate()); } //calculate balance for foreign currency accounts if ((*asset_it).currencyId() != file->baseCurrency().id()) { - ReportAccount repAcc = ReportAccount((*asset_it).id()); - MyMoneyMoney curPrice = repAcc.baseCurrencyPrice(QDate::currentDate()); - MyMoneyMoney baseValue = value * curPrice; + const auto curPrice = file->price((*asset_it).tradingCurrencyId(), file->baseCurrency().id(), QDate::currentDate()); + const auto curRate = curPrice.rate(file->baseCurrency().id()); + auto baseValue = value * curRate; baseValue = baseValue.convert(10000); netAssets += baseValue; } else { netAssets += value; } //show the account without minimum balance showAccountEntry(*asset_it, value, MyMoneyMoney(), false); ++asset_it; } else { //write a white space if we don't m_html += QString("%1%2").arg(placeHolder_Status).arg(placeHolder_Counts); } //leave the intermediate column empty m_html += ""; //write a liability account if (liabilities_it != liabilities.constEnd()) { MyMoneyMoney value; value = MyMoneyFile::instance()->balance((*liabilities_it).id(), QDate::currentDate()); //calculate balance if foreign currency if ((*liabilities_it).currencyId() != file->baseCurrency().id()) { - ReportAccount repAcc = ReportAccount((*liabilities_it).id()); - MyMoneyMoney curPrice = repAcc.baseCurrencyPrice(QDate::currentDate()); - MyMoneyMoney baseValue = value * curPrice; + const auto curPrice = file->price((*liabilities_it).tradingCurrencyId(), file->baseCurrency().id(), QDate::currentDate()); + const auto curRate = curPrice.rate(file->baseCurrency().id()); + auto baseValue = value * curRate; baseValue = baseValue.convert(10000); netLiabilities += baseValue; } else { netLiabilities += value; } //show the account without minimum balance showAccountEntry(*liabilities_it, value, MyMoneyMoney(), false); ++liabilities_it; } else { //leave the space empty if we run out of liabilities m_html += QString("%1%2").arg(placeHolder_Status).arg(placeHolder_Counts); } m_html += ""; } //calculate net worth MyMoneyMoney netWorth = netAssets + netLiabilities; //format assets, liabilities and net worth QString amountAssets = netAssets.formatMoney(file->baseCurrency().tradingSymbol(), prec); QString amountLiabilities = netLiabilities.formatMoney(file->baseCurrency().tradingSymbol(), prec); QString amountNetWorth = netWorth.formatMoney(file->baseCurrency().tradingSymbol(), prec); amountAssets.replace(QChar(' '), " "); amountLiabilities.replace(QChar(' '), " "); amountNetWorth.replace(QChar(' '), " "); m_html += QString("").arg(i++ & 0x01 ? "even" : "odd"); //print total for assets m_html += QString("%1%3").arg(placeHolder_Status).arg(i18n("Total Assets")).arg(placeHolder_Counts).arg(showColoredAmount(amountAssets, netAssets.isNegative())); //leave the intermediate column empty m_html += ""; //print total liabilities m_html += QString("%1%3").arg(placeHolder_Status).arg(i18n("Total Liabilities")).arg(placeHolder_Counts).arg(showColoredAmount(amountLiabilities, netLiabilities.isNegative())); m_html += ""; //print net worth m_html += QString("").arg(i++ & 0x01 ? "even" : "odd"); m_html += QString("%1%2").arg(placeHolder_Status).arg(placeHolder_Counts); m_html += QString("%1%3").arg(placeHolder_Status).arg(i18n("Net Worth")).arg(placeHolder_Counts).arg(showColoredAmount(amountNetWorth, netWorth.isNegative())); m_html += ""; m_html += "
"; m_html += statusHeader; m_html += ""; m_html += i18n("Asset Accounts"); m_html += "!MC!R" + i18n("Last Reconciled") + ""; m_html += i18n("Current Balance"); m_html += ""; m_html += statusHeader; m_html += ""; m_html += i18n("Liability Accounts"); m_html += "!MC!R" + i18n("Last Reconciled") + ""; m_html += i18n("Current Balance"); m_html += "
%2%4%2%4
%2%4
"; m_html += "
"; } } void showBudget() { - MyMoneyFile* file = MyMoneyFile::instance(); - - if (file->countBudgets()) { - int prec = MyMoneyMoney::denomToPrec(file->baseCurrency().smallestAccountFraction()); - bool isOverrun = false; - int i = 0; - - //config report just like "Monthly Budgeted vs Actual - MyMoneyReport reportCfg = MyMoneyReport( - eMyMoney::Report::RowType::BudgetActual, - static_cast(eMyMoney::Report::ColumnType::Months), - TransactionFilter::Date::CurrentMonth, - eMyMoney::Report::DetailLevel::All, - i18n("Monthly Budgeted vs. Actual"), - i18n("Generated Report")); - - reportCfg.setBudget("Any", true); - - reports::PivotTable table(reportCfg); - - PivotGrid grid = table.grid(); - - //div header - m_html += "
" + i18n("Budget") + "
\n
 
\n"; - - //display budget summary - m_html += ""; - m_html += ""; - m_html += ""; - m_html += ""; - m_html += ""; - m_html += ""; - m_html += ""; - - m_html += QString(""); - - MyMoneyMoney totalBudgetValue = grid.m_total[eBudget].m_total; - MyMoneyMoney totalActualValue = grid.m_total[eActual].m_total; - MyMoneyMoney totalBudgetDiffValue = grid.m_total[eBudgetDiff].m_total; - - QString totalBudgetAmount = totalBudgetValue.formatMoney(file->baseCurrency().tradingSymbol(), prec); - QString totalActualAmount = totalActualValue.formatMoney(file->baseCurrency().tradingSymbol(), prec); - QString totalBudgetDiffAmount = totalBudgetDiffValue.formatMoney(file->baseCurrency().tradingSymbol(), prec); - - m_html += QString("").arg(showColoredAmount(totalBudgetAmount, totalBudgetValue.isNegative())); - m_html += QString("").arg(showColoredAmount(totalActualAmount, totalActualValue.isNegative())); - m_html += QString("").arg(showColoredAmount(totalBudgetDiffAmount, totalBudgetDiffValue.isNegative())); - m_html += ""; - m_html += "
"; - m_html += i18n("Current Month Summary"); - m_html += "
"; - m_html += i18n("Budgeted"); - m_html += ""; - m_html += i18n("Actual"); - m_html += ""; - m_html += i18n("Difference"); - m_html += "
%1%1%1
"; - - //budget overrun - m_html += "
 
\n"; - m_html += ""; - m_html += ""; - m_html += ""; - m_html += ""; - m_html += ""; - m_html += ""; - m_html += ""; - m_html += ""; - - - PivotGrid::iterator it_outergroup = grid.begin(); - while (it_outergroup != grid.end()) { - i = 0; - PivotOuterGroup::iterator it_innergroup = (*it_outergroup).begin(); - while (it_innergroup != (*it_outergroup).end()) { - PivotInnerGroup::iterator it_row = (*it_innergroup).begin(); - while (it_row != (*it_innergroup).end()) { - //column number is 1 because the report includes only current month - if (it_row.value()[eBudgetDiff].value(1).isNegative()) { - //get report account to get the name later - ReportAccount rowname = it_row.key(); - - //write the outergroup if it is the first row of outergroup being shown - if (i == 0) { - m_html += ""; - m_html += QString("").arg(MyMoneyAccount::accountTypeToString(rowname.accountType())); - m_html += ""; - } - m_html += QString("").arg(i++ & 0x01 ? "even" : "odd"); - - //get values from grid - MyMoneyMoney actualValue = it_row.value()[eActual][1]; - MyMoneyMoney budgetValue = it_row.value()[eBudget][1]; - MyMoneyMoney budgetDiffValue = it_row.value()[eBudgetDiff][1]; - - //format amounts - QString actualAmount = actualValue.formatMoney(file->baseCurrency().tradingSymbol(), prec); - QString budgetAmount = budgetValue.formatMoney(file->baseCurrency().tradingSymbol(), prec); - QString budgetDiffAmount = budgetDiffValue.formatMoney(file->baseCurrency().tradingSymbol(), prec); - - //account name - m_html += QString(""; - - //show amounts - m_html += QString("").arg(showColoredAmount(budgetAmount, budgetValue.isNegative())); - m_html += QString("").arg(showColoredAmount(actualAmount, actualValue.isNegative())); - m_html += QString("").arg(showColoredAmount(budgetDiffAmount, budgetDiffValue.isNegative())); - m_html += ""; - - //set the flag that there are overruns - isOverrun = true; - } - ++it_row; - } - ++it_innergroup; - } - ++it_outergroup; - } + m_html += "
" + i18n("Budget") + "
\n
 
\n"; + m_html += "
"; - m_html += i18n("Budget Overruns"); - m_html += "
"; - m_html += i18n("Account"); - m_html += ""; - m_html += i18n("Budgeted"); - m_html += ""; - m_html += i18n("Actual"); - m_html += ""; - m_html += i18n("Difference"); - m_html += "
%1
") + link(VIEW_LEDGER, QString("?id=%1").arg(rowname.id())) + rowname.name() + linkend() + "%1%1%1
"; - //if no negative differences are found, then inform that - if (!isOverrun) { - m_html += QString::fromLatin1("").arg(((i++ & 1) == 1) ? QLatin1String("even") : QLatin1String("odd")); - m_html += QString::fromLatin1("").arg(i18n("No Budget Categories have been overrun")); - m_html += ""; - } - m_html += "
%1
"; + if (const auto reportsPlugin = pPlugins.data.value(QStringLiteral("reportsview"), nullptr)) { + const auto variantReport = reportsPlugin->requestData(QString(), eWidgetPlugin::WidgetType::Budget); + if (!variantReport.isNull()) + m_html.append(variantReport.toString()); + } else { + m_html += QString(""); + m_html += QString("
%1
").arg(i18n("Enable reports plugin to see this chart.")); + m_html += QString(""); } + + m_html += QString("
"); } void showCashFlowSummary() { MyMoneyTransactionFilter filter; MyMoneyMoney incomeValue; MyMoneyMoney expenseValue; MyMoneyFile* file = MyMoneyFile::instance(); int prec = MyMoneyMoney::denomToPrec(file->baseCurrency().smallestAccountFraction()); //set start and end of month dates QDate startOfMonth = QDate(QDate::currentDate().year(), QDate::currentDate().month(), 1); QDate endOfMonth = QDate(QDate::currentDate().year(), QDate::currentDate().month(), QDate::currentDate().daysInMonth()); //Add total income and expenses for this month //get transactions for current month filter.setDateFilter(startOfMonth, endOfMonth); filter.setReportAllSplits(false); QList transactions = file->transactionList(filter); //if no transaction then skip and print total in zero if (transactions.size() > 0) { //get all transactions for this month foreach (const auto transaction, transactions) { //get the splits for each transaction foreach (const auto split, transaction.splits()) { if (!split.shares().isZero()) { - ReportAccount repSplitAcc = ReportAccount(split.accountId()); + auto repSplitAcc = file->account(split.accountId()); //only add if it is an income or expense if (repSplitAcc.isIncomeExpense()) { MyMoneyMoney value; //convert to base currency if necessary if (repSplitAcc.currencyId() != file->baseCurrency().id()) { - MyMoneyMoney curPrice = repSplitAcc.baseCurrencyPrice(transaction.postDate()); - value = (split.shares() * MyMoneyMoney::MINUS_ONE) * curPrice; + const auto curPrice = file->price(repSplitAcc.tradingCurrencyId(), file->baseCurrency().id(), QDate::currentDate()); + const auto curRate = curPrice.rate(file->baseCurrency().id()); + value = (split.shares() * MyMoneyMoney::MINUS_ONE) * curRate; value = value.convert(10000); } else { value = (split.shares() * MyMoneyMoney::MINUS_ONE); } //store depending on account type if (repSplitAcc.accountType() == Account::Type::Income) { incomeValue += value; } else { expenseValue += value; } } } } } } //format income and expenses QString amountIncome = incomeValue.formatMoney(file->baseCurrency().tradingSymbol(), prec); QString amountExpense = expenseValue.formatMoney(file->baseCurrency().tradingSymbol(), prec); amountIncome.replace(QChar(' '), " "); amountExpense.replace(QChar(' '), " "); //calculate schedules //Add all schedules for this month MyMoneyMoney scheduledIncome; MyMoneyMoney scheduledExpense; MyMoneyMoney scheduledLiquidTransfer; MyMoneyMoney scheduledOtherTransfer; //get overdues and schedules until the end of this month QList schedule = file->scheduleList(QString(), Schedule::Type::Any, Schedule::Occurrence::Any, Schedule::PaymentType::Any, QDate(), endOfMonth, false); //Remove the finished schedules QList::Iterator finished_it; for (finished_it = schedule.begin(); finished_it != schedule.end();) { if ((*finished_it).isFinished()) { finished_it = schedule.erase(finished_it); continue; } ++finished_it; } //add income and expenses QList::Iterator sched_it; for (sched_it = schedule.begin(); sched_it != schedule.end();) { QDate nextDate = (*sched_it).nextDueDate(); int cnt = 0; while (nextDate.isValid() && nextDate <= endOfMonth) { ++cnt; nextDate = (*sched_it).nextPayment(nextDate); // for single occurrence nextDate will not change, so we // better get out of here. if ((*sched_it).occurrence() == Schedule::Occurrence::Once) break; } MyMoneyAccount acc = (*sched_it).account(); if (!acc.id().isEmpty()) { MyMoneyTransaction transaction = (*sched_it).transaction(); // only show the entry, if it is still active MyMoneySplit sp = transaction.splitByAccount(acc.id(), true); // take care of the autoCalc stuff if ((*sched_it).type() == Schedule::Type::LoanPayment) { nextDate = (*sched_it).nextPayment((*sched_it).lastPayment()); //make sure we have all 'starting balances' so that the autocalc works QMap balanceMap; foreach (const auto split, transaction.splits()) { acc = file->account(split.accountId()); // collect all overdues on the first day QDate schedDate = nextDate; if (QDate::currentDate() >= nextDate) schedDate = QDate::currentDate().addDays(1); balanceMap[acc.id()] += file->balance(acc.id(), QDate::currentDate()); } KMyMoneyUtils::calculateAutoLoan(*sched_it, transaction, balanceMap); } //go through the splits and assign to liquid or other transfers const QList splits = transaction.splits(); QList::const_iterator split_it; for (split_it = splits.constBegin(); split_it != splits.constEnd(); ++split_it) { if ((*split_it).accountId() != acc.id()) { - ReportAccount repSplitAcc = ReportAccount((*split_it).accountId()); + auto repSplitAcc = file->account((*split_it).accountId()); //get the shares and multiply by the quantity of occurrences in the period MyMoneyMoney value = (*split_it).shares() * cnt; //convert to foreign currency if needed if (repSplitAcc.currencyId() != file->baseCurrency().id()) { - MyMoneyMoney curPrice = repSplitAcc.baseCurrencyPrice(QDate::currentDate()); - value = value * curPrice; + const auto curPrice = file->price(repSplitAcc.tradingCurrencyId(), file->baseCurrency().id(), QDate::currentDate()); + const auto curRate = curPrice.rate(file->baseCurrency().id()); + value = value * curRate; value = value.convert(10000); } if ((repSplitAcc.isLiquidLiability() || repSplitAcc.isLiquidAsset()) && acc.accountGroup() != repSplitAcc.accountGroup()) { scheduledLiquidTransfer += value; } else if (repSplitAcc.isAssetLiability() && !repSplitAcc.isLiquidLiability() && !repSplitAcc.isLiquidAsset()) { scheduledOtherTransfer += value; } else if (repSplitAcc.isIncomeExpense()) { //income and expenses are stored as negative values if (repSplitAcc.accountType() == Account::Type::Income) scheduledIncome -= value; if (repSplitAcc.accountType() == Account::Type::Expense) scheduledExpense -= value; } } } } ++sched_it; } //format the currency strings QString amountScheduledIncome = scheduledIncome.formatMoney(file->baseCurrency().tradingSymbol(), prec); QString amountScheduledExpense = scheduledExpense.formatMoney(file->baseCurrency().tradingSymbol(), prec); QString amountScheduledLiquidTransfer = scheduledLiquidTransfer.formatMoney(file->baseCurrency().tradingSymbol(), prec); QString amountScheduledOtherTransfer = scheduledOtherTransfer.formatMoney(file->baseCurrency().tradingSymbol(), prec); amountScheduledIncome.replace(QChar(' '), " "); amountScheduledExpense.replace(QChar(' '), " "); amountScheduledLiquidTransfer.replace(QChar(' '), " "); amountScheduledOtherTransfer.replace(QChar(' '), " "); //get liquid assets and liabilities QList accounts; QList::const_iterator account_it; MyMoneyMoney liquidAssets; MyMoneyMoney liquidLiabilities; // get list of all accounts file->accountList(accounts); for (account_it = accounts.constBegin(); account_it != accounts.constEnd();) { if (!(*account_it).isClosed()) { switch ((*account_it).accountType()) { //group all assets into one list case Account::Type::Checkings: case Account::Type::Savings: case Account::Type::Cash: { MyMoneyMoney value = MyMoneyFile::instance()->balance((*account_it).id(), QDate::currentDate()); //calculate balance for foreign currency accounts if ((*account_it).currencyId() != file->baseCurrency().id()) { - ReportAccount repAcc = ReportAccount((*account_it).id()); - MyMoneyMoney curPrice = repAcc.baseCurrencyPrice(QDate::currentDate()); - MyMoneyMoney baseValue = value * curPrice; + const auto curPrice = file->price((*account_it).tradingCurrencyId(), file->baseCurrency().id(), QDate::currentDate()); + const auto curRate = curPrice.rate(file->baseCurrency().id()); + auto baseValue = value * curRate; liquidAssets += baseValue; liquidAssets = liquidAssets.convert(10000); } else { liquidAssets += value; } break; } //group the liabilities into the other case Account::Type::CreditCard: { MyMoneyMoney value; value = MyMoneyFile::instance()->balance((*account_it).id(), QDate::currentDate()); //calculate balance if foreign currency if ((*account_it).currencyId() != file->baseCurrency().id()) { - ReportAccount repAcc = ReportAccount((*account_it).id()); - MyMoneyMoney curPrice = repAcc.baseCurrencyPrice(QDate::currentDate()); - MyMoneyMoney baseValue = value * curPrice; + const auto curPrice = file->price((*account_it).tradingCurrencyId(), file->baseCurrency().id(), QDate::currentDate()); + const auto curRate = curPrice.rate(file->baseCurrency().id()); + auto baseValue = value * curRate; liquidLiabilities += baseValue; liquidLiabilities = liquidLiabilities.convert(10000); } else { liquidLiabilities += value; } break; } default: break; } } ++account_it; } //calculate net worth MyMoneyMoney liquidWorth = liquidAssets + liquidLiabilities; //format assets, liabilities and net worth QString amountLiquidAssets = liquidAssets.formatMoney(file->baseCurrency().tradingSymbol(), prec); QString amountLiquidLiabilities = liquidLiabilities.formatMoney(file->baseCurrency().tradingSymbol(), prec); QString amountLiquidWorth = liquidWorth.formatMoney(file->baseCurrency().tradingSymbol(), prec); amountLiquidAssets.replace(QChar(' '), " "); amountLiquidLiabilities.replace(QChar(' '), " "); amountLiquidWorth.replace(QChar(' '), " "); //show the summary m_html += "
" + i18n("Cash Flow Summary") + "
\n
 
\n"; //print header m_html += ""; //income and expense title m_html += ""; m_html += ""; //column titles m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; //add row with banding m_html += QString(""); //print current income m_html += QString("").arg(showColoredAmount(amountIncome, incomeValue.isNegative())); //print the scheduled income m_html += QString("").arg(showColoredAmount(amountScheduledIncome, scheduledIncome.isNegative())); //print current expenses m_html += QString("").arg(showColoredAmount(amountExpense, expenseValue.isNegative())); //print the scheduled expenses m_html += QString("").arg(showColoredAmount(amountScheduledExpense, scheduledExpense.isNegative())); m_html += ""; m_html += "
"; m_html += i18n("Income and Expenses of Current Month"); m_html += "
"; m_html += i18n("Income"); m_html += ""; m_html += i18n("Scheduled Income"); m_html += ""; m_html += i18n("Expenses"); m_html += ""; m_html += i18n("Scheduled Expenses"); m_html += "
%2%2%2%2
"; //print header of assets and liabilities m_html += "
 
\n"; m_html += ""; //assets and liabilities title m_html += ""; m_html += ""; //column titles m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; //add row with banding m_html += QString(""); //print current liquid assets m_html += QString("").arg(showColoredAmount(amountLiquidAssets, liquidAssets.isNegative())); //print the scheduled transfers m_html += QString("").arg(showColoredAmount(amountScheduledLiquidTransfer, scheduledLiquidTransfer.isNegative())); //print current liabilities m_html += QString("").arg(showColoredAmount(amountLiquidLiabilities, liquidLiabilities.isNegative())); //print the scheduled transfers m_html += QString("").arg(showColoredAmount(amountScheduledOtherTransfer, scheduledOtherTransfer.isNegative())); m_html += ""; m_html += "
"; m_html += i18n("Liquid Assets and Liabilities"); m_html += "
"; m_html += i18n("Liquid Assets"); m_html += ""; m_html += i18n("Transfers to Liquid Liabilities"); m_html += ""; m_html += i18n("Liquid Liabilities"); m_html += ""; m_html += i18n("Other Transfers"); m_html += "
%2%2%2%2
"; //final conclusion MyMoneyMoney profitValue = incomeValue + expenseValue + scheduledIncome + scheduledExpense; MyMoneyMoney expectedAsset = liquidAssets + scheduledIncome + scheduledExpense + scheduledLiquidTransfer + scheduledOtherTransfer; MyMoneyMoney expectedLiabilities = liquidLiabilities + scheduledLiquidTransfer; QString amountExpectedAsset = expectedAsset.formatMoney(file->baseCurrency().tradingSymbol(), prec); QString amountExpectedLiabilities = expectedLiabilities.formatMoney(file->baseCurrency().tradingSymbol(), prec); QString amountProfit = profitValue.formatMoney(file->baseCurrency().tradingSymbol(), prec); amountProfit.replace(QChar(' '), " "); amountExpectedAsset.replace(QChar(' '), " "); amountExpectedLiabilities.replace(QChar(' '), " "); //print header of cash flow status m_html += "
 
\n"; m_html += ""; //income and expense title m_html += ""; m_html += ""; //column titles m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; //add row with banding m_html += QString(""); m_html += ""; //print expected assets m_html += QString("").arg(showColoredAmount(amountExpectedAsset, expectedAsset.isNegative())); //print expected liabilities m_html += QString("").arg(showColoredAmount(amountExpectedLiabilities, expectedLiabilities.isNegative())); //print expected profit m_html += QString("").arg(showColoredAmount(amountProfit, profitValue.isNegative())); m_html += ""; m_html += "
"; m_html += i18n("Cash Flow Status"); m_html += "
 "; m_html += i18n("Expected Liquid Assets"); m_html += ""; m_html += i18n("Expected Liquid Liabilities"); m_html += ""; m_html += i18n("Expected Profit/Loss"); m_html += "
 %2%2%2
"; m_html += "
"; } KHomeView *q_ptr; /** * daily balances of an account */ typedef QMap dailyBalances; #ifdef ENABLE_WEBENGINE QWebEngineView *m_view; #else KWebView *m_view; #endif QString m_html; bool m_showAllSchedules; bool m_needLoad; MyMoneyForecast m_forecast; MyMoneyMoney m_total; /** * Hold the last valid size of the net worth graph * for the times when the needed size can't be computed. */ QSize m_netWorthGraphLastValidSize; /** * daily forecast balance of accounts */ QMap m_accountList; QPrinter *m_currentPrinter; }; #endif diff --git a/kmymoney/views/kmymoneyview.cpp b/kmymoney/views/kmymoneyview.cpp index 2738dc99a..78dda2fb5 100644 --- a/kmymoney/views/kmymoneyview.cpp +++ b/kmymoney/views/kmymoneyview.cpp @@ -1,856 +1,860 @@ /*************************************************************************** kmymoneyview.cpp ------------------- copyright : (C) 2000-2001 by Michael Edwardes 2004 by Thomas Baumgart (C) 2017, 2018 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 #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 #include // ---------------------------------------------------------------------------- // Project Includes #ifdef ENABLE_UNFINISHEDFEATURES #include "simpleledgerview.h" #endif #include "kmymoneysettings.h" #include "kmymoneytitlelabel.h" #include "kcurrencyeditdlg.h" #include "mymoneyexception.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 "kinvestmentview.h" #include "models.h" #include "accountsmodel.h" #include "equitiesmodel.h" #include "securitiesmodel.h" #include "icons.h" #include "amountedit.h" #include "onlinejobadministration.h" #include "kmymoneyaccounttreeview.h" #include "accountsviewproxymodel.h" #include "mymoneyprice.h" #include "mymoneyschedule.h" #include "mymoneysplit.h" #include "mymoneyaccount.h" #include "mymoneyinstitution.h" #include "mymoneytag.h" #include "kmymoneyedit.h" #include "mymoneyfile.h" #include "mymoneysecurity.h" #include "mymoneyreport.h" #include "kmymoneyplugin.h" #include "mymoneyenums.h" #include "menuenums.h" using namespace Icons; using namespace eMyMoney; typedef void(KMyMoneyView::*KMyMoneyViewFunc)(); KMyMoneyView::KMyMoneyView() : KPageWidget(nullptr), m_header(0) { // 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(KMyMoneySettings::showTitleBar()); gridLayout->addWidget(m_header, 1, 1); } } // newStorage(); m_model = new KPageWidgetModel(this); // cannot be parentless, otherwise segfaults at exit viewBases[View::Home] = new KHomeView; viewBases[View::Institutions] = new KInstitutionsView; viewBases[View::Accounts] = new KAccountsView; viewBases[View::Schedules] = new KScheduledView; viewBases[View::Categories] = new KCategoriesView; viewBases[View::Tags] = new KTagsView; viewBases[View::Payees] = new KPayeesView; viewBases[View::Ledgers] = new KGlobalLedgerView; viewBases[View::Investments] = new KInvestmentView; #ifdef ENABLE_UNFINISHEDFEATURES viewBases[View::NewLedgers] = new SimpleLedgerView; #endif struct viewInfo { View id; QString name; Icon icon; }; const QVector viewsInfo { {View::Home, i18n("Home"), Icon::ViewHome}, {View::Institutions, i18n("Institutions"), Icon::ViewInstitutions}, {View::Accounts, i18n("Accounts"), Icon::ViewAccounts}, {View::Schedules, i18n("Scheduled\ntransactions"), Icon::ViewSchedules}, {View::Categories, i18n("Categories"), Icon::ViewCategories}, {View::Tags, i18n("Tags"), Icon::ViewTags}, {View::Payees, i18n("Payees"), Icon::ViewPayees}, {View::Ledgers, i18n("Ledgers"), Icon::ViewLedgers}, {View::Investments, i18n("Investments"), Icon::ViewInvestment}, #ifdef ENABLE_UNFINISHEDFEATURES {View::NewLedgers, i18n("New ledger"), Icon::DocumentProperties}, #endif }; for (const viewInfo& view : viewsInfo) { /* There is a bug in static int layoutText(QTextLayout *layout, int maxWidth) from kpageview_p.cpp from kwidgetsaddons. The method doesn't break strings that are too long. Following line workarounds this by using LINE SEPARATOR character which is accepted by QTextLayout::createLine().*/ viewFrames[view.id] = m_model->addPage(viewBases[view.id], QString(view.name).replace('\n', QString::fromLocal8Bit("\xe2\x80\xa8"))); viewFrames[view.id]->setIcon(Icons::get(view.icon)); connect(viewBases[view.id], &KMyMoneyViewBase::selectByObject, this, &KMyMoneyView::slotSelectByObject); connect(viewBases[view.id], &KMyMoneyViewBase::selectByVariant, this, &KMyMoneyView::slotSelectByVariant); connect(viewBases[view.id], &KMyMoneyViewBase::customActionRequested, this, &KMyMoneyView::slotCustomActionRequested); } connect(Models::instance()->accountsModel(), &AccountsModel::netWorthChanged, this, &KMyMoneyView::slotSelectByVariant); connect(Models::instance()->institutionsModel(), &AccountsModel::netWorthChanged, this, &KMyMoneyView::slotSelectByVariant); connect(Models::instance()->institutionsModel(), &AccountsModel::profitChanged, this, &KMyMoneyView::slotSelectByVariant); //set the model setModel(m_model); setCurrentPage(viewFrames[View::Home]); connect(this, SIGNAL(currentPageChanged(QModelIndex,QModelIndex)), this, SLOT(slotCurrentPageChanged(QModelIndex,QModelIndex))); updateViewType(); } KMyMoneyView::~KMyMoneyView() { } void KMyMoneyView::slotFileOpened() { if (viewBases.contains(View::OnlineJobOutbox)) viewBases[View::OnlineJobOutbox]->executeCustomAction(eView::Action::InitializeAfterFileOpen); #ifdef ENABLE_UNFINISHEDFEATURES static_cast(viewBases[View::NewLedgers])->openFavoriteLedgers(); #endif switchToDefaultView(); slotObjectSelected(MyMoneyAccount()); // in order to enable update all accounts on file reload } void KMyMoneyView::slotFileClosed() { slotShowHomePage(); if (viewBases.contains(View::Home)) viewBases[View::Home]->executeCustomAction(eView::Action::CleanupBeforeFileClose); if (viewBases.contains(View::Reports)) viewBases[View::Reports]->executeCustomAction(eView::Action::CleanupBeforeFileClose); if (viewBases.contains(View::OnlineJobOutbox)) viewBases[View::OnlineJobOutbox]->executeCustomAction(eView::Action::CleanupBeforeFileClose); #ifdef ENABLE_UNFINISHEDFEATURES static_cast(viewBases[View::NewLedgers])->closeLedgers(); #endif pActions[eMenu::Action::Print]->setEnabled(false); pActions[eMenu::Action::AccountCreditTransfer]->setEnabled(false); pActions[eMenu::Action::UpdateAllAccounts]->setEnabled(false); } void KMyMoneyView::slotShowHomePage() { showPageAndFocus(View::Home); } void KMyMoneyView::slotShowInstitutionsPage() { showPageAndFocus(View::Institutions); } void KMyMoneyView::slotShowAccountsPage() { showPageAndFocus(View::Accounts); } void KMyMoneyView::slotShowSchedulesPage() { showPageAndFocus(View::Schedules); } void KMyMoneyView::slotShowCategoriesPage() { showPageAndFocus(View::Categories); } void KMyMoneyView::slotShowTagsPage() { showPageAndFocus(View::Tags); } void KMyMoneyView::slotShowPayeesPage() { showPageAndFocus(View::Payees); } void KMyMoneyView::slotShowLedgersPage() { showPageAndFocus(View::Ledgers); } void KMyMoneyView::slotShowInvestmentsPage() { showPageAndFocus(View::Investments); } void KMyMoneyView::slotShowReportsPage() { showPageAndFocus(View::Reports); } void KMyMoneyView::slotShowBudgetPage() { showPageAndFocus(View::Budget); } void KMyMoneyView::slotShowForecastPage() { showPageAndFocus(View::Forecast); } void KMyMoneyView::slotShowOutboxPage() { showPageAndFocus(View::OnlineJobOutbox); } 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 { static_cast(viewBases[View::Institutions])->getProxyModel(), static_cast(viewBases[View::Accounts])->getProxyModel(), static_cast(viewBases[View::Categories])->getProxyModel() }; if (viewBases.contains(View::Budget)) proxyModels.append(static_cast(viewBases[View::Budget])->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(); } } else if(show) { // in case we need to show it, we have to make sure to set the visibility // in the base model as well. Otherwise, we don't see the column through the proxy model Models::instance()->accountsModel()->setColumnVisibility(column, show); Models::instance()->institutionsModel()->setColumnVisibility(column, show); } } void KMyMoneyView::setOnlinePlugins(QMap& plugins) { if (viewBases.contains(View::Accounts)) viewBases[View::Accounts]->slotSelectByVariant(QVariantList {QVariant::fromValue(static_cast(&plugins))}, eView::Intent::SetOnlinePlugins); if (viewBases.contains(View::OnlineJobOutbox)) viewBases[View::OnlineJobOutbox]->slotSelectByVariant(QVariantList {QVariant::fromValue(static_cast(&plugins))}, eView::Intent::SetOnlinePlugins); } eDialogs::ScheduleResultCode KMyMoneyView::enterSchedule(MyMoneySchedule& schedule, bool autoEnter, bool extendedKeys) { return static_cast(viewBases[View::Schedules])->enterSchedule(schedule, autoEnter, extendedKeys); } void KMyMoneyView::addView(KMyMoneyViewBase* view, const QString& name, View idView) { auto isViewInserted = false; for (auto i = (int)idView; i < (int)View::None; ++i) { if (viewFrames.contains((View)i)) { viewFrames[idView] = m_model->insertPage(viewFrames[(View)i],view, name); isViewInserted = true; break; } } if (!isViewInserted) viewFrames[idView] = m_model->addPage(view, name); viewBases[idView] = view; connect(viewBases[idView], &KMyMoneyViewBase::selectByObject, this, &KMyMoneyView::slotSelectByObject); connect(viewBases[idView], &KMyMoneyViewBase::selectByVariant, this, &KMyMoneyView::slotSelectByVariant); connect(viewBases[idView], &KMyMoneyViewBase::customActionRequested, this, &KMyMoneyView::slotCustomActionRequested); auto icon = Icon::ViewForecast; switch (idView) { case View::Reports: icon = Icon::ViewReports; break; case View::Budget: icon = Icon::ViewBudgets; break; case View::Forecast: icon = Icon::ViewForecast; break; case View::OnlineJobOutbox: icon = Icon::ViewOutbox; break; default: break; } viewFrames[idView]->setIcon(Icons::get(icon)); } void KMyMoneyView::removeView(View idView) { if (!viewBases.contains(idView)) return; disconnect(viewBases[idView], &KMyMoneyViewBase::selectByObject, this, &KMyMoneyView::slotSelectByObject); disconnect(viewBases[idView], &KMyMoneyViewBase::selectByVariant, this, &KMyMoneyView::slotSelectByVariant); disconnect(viewBases[idView], &KMyMoneyViewBase::customActionRequested, this, &KMyMoneyView::slotCustomActionRequested); m_model->removePage(viewFrames[idView]); viewFrames.remove(idView); viewBases.remove(idView); } QHash KMyMoneyView::actionsToBeConnected() { using namespace eMenu; // add fast switching of main views through Ctrl + NUM_X struct pageInfo { Action view; KMyMoneyViewFunc callback; QString text; QKeySequence shortcut = QKeySequence(); }; const QVector pageInfos { {Action::ShowHomeView, &KMyMoneyView::slotShowHomePage, i18n("Show home page"), Qt::CTRL + Qt::Key_1}, {Action::ShowInstitutionsView, &KMyMoneyView::slotShowInstitutionsPage, i18n("Show institutions page"), Qt::CTRL + Qt::Key_2}, {Action::ShowAccountsView, &KMyMoneyView::slotShowAccountsPage, i18n("Show accounts page"), Qt::CTRL + Qt::Key_3}, {Action::ShowSchedulesView, &KMyMoneyView::slotShowSchedulesPage, i18n("Show scheduled transactions page"), Qt::CTRL + Qt::Key_4}, {Action::ShowCategoriesView, &KMyMoneyView::slotShowCategoriesPage, i18n("Show categories page"), Qt::CTRL + Qt::Key_5}, {Action::ShowTagsView, &KMyMoneyView::slotShowTagsPage, i18n("Show tags page"), }, {Action::ShowPayeesView, &KMyMoneyView::slotShowPayeesPage, i18n("Show payees page"), Qt::CTRL + Qt::Key_6}, {Action::ShowLedgersView, &KMyMoneyView::slotShowLedgersPage, i18n("Show ledgers page"), Qt::CTRL + Qt::Key_7}, {Action::ShowInvestmentsView, &KMyMoneyView::slotShowInvestmentsPage, i18n("Show investments page"), Qt::CTRL + Qt::Key_8}, {Action::ShowReportsView, &KMyMoneyView::slotShowReportsPage, i18n("Show reports page"), Qt::CTRL + Qt::Key_9}, {Action::ShowBudgetView, &KMyMoneyView::slotShowBudgetPage, i18n("Show budget page"), }, {Action::ShowForecastView, &KMyMoneyView::slotShowForecastPage, i18n("Show forecast page"), }, {Action::ShowOnlineJobOutboxView, &KMyMoneyView::slotShowOutboxPage, i18n("Show outbox page") } }; QHash lutActions; auto pageCount = 0; for (const pageInfo& info : pageInfos) { auto a = new QAction(this); // KActionCollection::addAction by name sets object name anyways, // so, as better alternative, set it here right from the start a->setObjectName(QString::fromLatin1("ShowPage%1").arg(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()) a->setShortcut(info.shortcut); } return lutActions; } bool KMyMoneyView::showPageHeader() const { return false; } void KMyMoneyView::showPageAndFocus(View idView) { if (viewFrames.contains(idView)) { showPage(idView); viewBases[idView]->executeCustomAction(eView::Action::SetDefaultFocus); } } void KMyMoneyView::showPage(View idView) { if (!viewFrames.contains(idView) || currentPage() == viewFrames[idView]) return; setCurrentPage(viewFrames[idView]); resetViewSelection(); } bool KMyMoneyView::canPrint() { return (MyMoneyFile::instance()->storageAttached() && ((viewFrames.contains(View::Reports) && viewFrames[View::Reports] == currentPage()) || (viewFrames.contains(View::Home) && viewFrames[View::Home] == currentPage())) ); } void KMyMoneyView::enableViewsIfFileOpen(bool fileOpen) { // call set enabled only if the state differs to avoid widgets 'bouncing on the screen' while doing this Q_ASSERT_X(((int)(View::Home)+1) == (int)View::Institutions, "viewenums.h", "View::Home must be first and View::Institutions second entry"); for (auto i = (int)View::Institutions; i < (int)View::None; ++i) if (viewFrames.contains(View(i))) if (viewFrames[View(i)]->isEnabled() != fileOpen) viewFrames[View(i)]->setEnabled(fileOpen); emit viewStateChanged(fileOpen); } void KMyMoneyView::switchToDefaultView() { const auto idView = KMyMoneySettings::startLastViewSelected() ? static_cast(KMyMoneySettings::lastViewSelected()) : View::Home; // if we currently see a different page, then select the right one if (viewFrames.contains(idView) && viewFrames[idView] != currentPage()) showPage(idView); } void KMyMoneyView::slotPayeeSelected(const QString& payee, const QString& account, const QString& transaction) { showPage(View::Payees); static_cast(viewBases[View::Payees])->slotSelectPayeeAndTransaction(payee, account, transaction); } void KMyMoneyView::slotTagSelected(const QString& tag, const QString& account, const QString& transaction) { showPage(View::Tags); static_cast(viewBases[View::Tags])->slotSelectTagAndTransaction(tag, account, transaction); } void KMyMoneyView::finishReconciliation(const MyMoneyAccount& /* account */) { Models::instance()->accountsModel()->slotReconcileAccount(MyMoneyAccount(), QDate(), MyMoneyMoney()); static_cast(viewBases[View::Ledgers])->slotSetReconcileAccount(MyMoneyAccount(), QDate(), MyMoneyMoney()); } void KMyMoneyView::viewAccountList(const QString& /*selectAccount*/) { if (viewFrames[View::Accounts] != currentPage()) showPage(View::Accounts); viewBases[View::Accounts]->show(); } void KMyMoneyView::slotRefreshViews() { showTitleBar(KMyMoneySettings::showTitleBar()); for (auto i = (int)View::Home; i < (int)View::None; ++i) if (viewBases.contains(View(i))) viewBases[View(i)]->executeCustomAction(eView::Action::Refresh); viewBases[View::Payees]->executeCustomAction(eView::Action::ClosePayeeIdentifierSource); } void KMyMoneyView::slotShowTransactionDetail(bool detailed) { KMyMoneySettings::setShowRegisterDetailed(detailed); slotRefreshViews(); } void KMyMoneyView::slotCurrentPageChanged(const QModelIndex current, const QModelIndex previous) { // set the current page's title in the header if (m_header) m_header->setText(m_model->data(current, KPageModel::HeaderRole).toString()); const auto view = currentPage(); // remember the selected view if there is a real change if (previous.isValid()) { QHash::const_iterator it; for(it = viewFrames.cbegin(); it != viewFrames.cend(); ++it) { if ((*it) == view) { emit viewActivated(it.key()); break; } } } if (viewBases.contains(View::Ledgers) && view != viewFrames.value(View::Ledgers)) viewBases[View::Ledgers]->executeCustomAction(eView::Action::DisableViewDepenedendActions); pActions[eMenu::Action::Print]->setEnabled(canPrint()); pActions[eMenu::Action::AccountCreditTransfer]->setEnabled(onlineJobAdministration::instance()->canSendCreditTransfer()); } 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_CSTRING("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", QString::fromLatin1(e.what()))); } } } void KMyMoneyView::slotPrintView() { if (viewFrames.contains(View::Reports) && viewFrames[View::Reports] == currentPage()) viewBases[View::Reports]->executeCustomAction(eView::Action::Print); else if (viewFrames.contains(View::Home) && viewFrames[View::Home] == currentPage()) viewBases[View::Home]->executeCustomAction(eView::Action::Print); } void KMyMoneyView::resetViewSelection() { if (!MyMoneyFile::instance()->storageAttached()) return; slotObjectSelected(MyMoneyAccount()); slotObjectSelected(MyMoneyInstitution()); slotObjectSelected(MyMoneySchedule()); slotObjectSelected(MyMoneyTag()); slotSelectByVariant(QVariantList {QVariant::fromValue(KMyMoneyRegister::SelectedTransactions())}, eView::Intent::SelectRegisterTransactions); } void KMyMoneyView::slotOpenObjectRequested(const MyMoneyObject& obj) { if (typeid(obj) == typeid(MyMoneyAccount)) { const auto& acc = static_cast(obj); // check if we can open this account // currently it make's sense for asset and liability accounts if (!MyMoneyFile::instance()->isStandardAccount(acc.id())) if (viewBases.contains(View::Ledgers)) viewBases[View::Ledgers]->slotSelectByVariant(QVariantList {QVariant(acc.id()), QVariant(QString()) }, eView::Intent::ShowTransaction ); } else if (typeid(obj) == typeid(MyMoneyInstitution)) { // const auto& inst = static_cast(obj); if (viewBases.contains(View::Institutions)) viewBases[View::Institutions]->executeCustomAction(eView::Action::EditInstitution); } else if (typeid(obj) == typeid(MyMoneySchedule)) { if (viewBases.contains(View::Schedules)) viewBases[View::Schedules]->executeCustomAction(eView::Action::EditSchedule); } else if (typeid(obj) == typeid(MyMoneyReport)) { // const auto& rep = static_cast(obj); showPage(View::Reports); if (viewBases.contains(View::Reports)) viewBases[View::Reports]->slotSelectByObject(obj, eView::Intent::OpenObject); } } void KMyMoneyView::slotSelectByObject(const MyMoneyObject& obj, eView::Intent intent) { switch (intent) { case eView::Intent::None: slotObjectSelected(obj); break; case eView::Intent::SynchronizeAccountInInvestmentView: if (viewBases.contains(View::Investments)) viewBases[View::Investments]->slotSelectByObject(obj, intent); break; case eView::Intent::SynchronizeAccountInLedgersView: if (viewBases.contains(View::Ledgers)) viewBases[View::Ledgers]->slotSelectByObject(obj, intent); break; case eView::Intent::OpenObject: slotOpenObjectRequested(obj); break; case eView::Intent::OpenContextMenu: slotContextMenuRequested(obj); break; case eView::Intent::StartEnteringOverdueScheduledTransactions: if (viewBases.contains(View::Schedules)) viewBases[View::Schedules]->slotSelectByObject(obj, intent); break; case eView::Intent::FinishEnteringOverdueScheduledTransactions: if (viewBases.contains(View::Ledgers)) { showPage(View::Ledgers); viewBases[View::Ledgers]->slotSelectByObject(obj, intent); } break; default: break; } } void KMyMoneyView::slotSelectByVariant(const QVariantList& variant, eView::Intent intent) { switch(intent) { case eView::Intent::ReportProgress: if (variant.count() == 2) emit statusProgress(variant.at(0).toInt(), variant.at(1).toInt()); break; case eView::Intent::ReportProgressMessage: if (variant.count() == 1) emit statusMsg(variant.first().toString()); break; case eView::Intent::UpdateNetWorth: if (viewBases.contains(View::Accounts)) viewBases[View::Accounts]->slotSelectByVariant(variant, intent); if (viewBases.contains(View::Institutions)) viewBases[View::Institutions]->slotSelectByVariant(variant, intent); break; case eView::Intent::UpdateProfit: if (viewBases.contains(View::Categories)) viewBases[View::Categories]->slotSelectByVariant(variant, intent); break; case eView::Intent::ShowTransaction: if (viewBases.contains(View::Ledgers)) { showPage(View::Ledgers); viewBases[View::Ledgers]->slotSelectByVariant(variant, intent); } break; case eView::Intent::ToggleColumn: if (variant.count() == 2) slotAccountTreeViewChanged(variant.at(0).value(), variant.at(1).value()); break; case eView::Intent::ShowPayee: if (viewBases.contains(View::Payees)) { showPage(View::Payees); viewBases[View::Payees]->slotSelectByVariant(variant, intent); } break; case eView::Intent::SelectRegisterTransactions: if (variant.count() == 1) { emit transactionsSelected(variant.at(0).value()); // for plugins if (viewBases.contains(View::Ledgers)) viewBases[View::Ledgers]->slotSelectByVariant(variant, intent); } break; case eView::Intent::AccountReconciled: if (variant.count() == 5) emit accountReconciled(variant.at(0).value(), variant.at(1).value(), variant.at(2).value(), variant.at(3).value(), variant.at(4).value>>()); // for plugins break; default: break; } } void KMyMoneyView::slotCustomActionRequested(View view, eView::Action action) { switch (action) { case eView::Action::AboutToShow: resetViewSelection(); break; case eView::Action::SwitchView: showPage(view); break; + case eView::Action::ShowBalanceChart: + if (viewBases.contains(View::Reports)) + viewBases[View::Reports]->executeCustomAction(action); + break; default: break; } } void KMyMoneyView::slotObjectSelected(const MyMoneyObject& obj) { // carrying some slots over to views isn't easy for all slots... // ...so calls to kmymoney still must be here if (typeid(obj) == typeid(MyMoneyAccount)) { QVector views {View::Investments, View::Categories, View::Accounts, View::Ledgers, View::Reports, View::OnlineJobOutbox}; for (const auto view : views) if (viewBases.contains(view)) viewBases[view]->slotSelectByObject(obj, eView::Intent::UpdateActions); // for plugin only const auto& acc = static_cast(obj); if (!acc.isIncomeExpense() && !MyMoneyFile::instance()->isStandardAccount(acc.id())) emit accountSelected(acc); } else if (typeid(obj) == typeid(MyMoneyInstitution)) { viewBases[View::Institutions]->slotSelectByObject(obj, eView::Intent::UpdateActions); } else if (typeid(obj) == typeid(MyMoneySchedule)) { viewBases[View::Schedules]->slotSelectByObject(obj, eView::Intent::UpdateActions); } } void KMyMoneyView::slotContextMenuRequested(const MyMoneyObject& obj) { if (typeid(obj) == typeid(MyMoneyAccount)) { const auto& acc = static_cast(obj); if (acc.isInvest()) viewBases[View::Investments]->slotSelectByObject(obj, eView::Intent::OpenContextMenu); else if (acc.isIncomeExpense()) viewBases[View::Categories]->slotSelectByObject(obj, eView::Intent::OpenContextMenu); else viewBases[View::Accounts]->slotSelectByObject(obj, eView::Intent::OpenContextMenu); } else if (typeid(obj) == typeid(MyMoneyInstitution)) { viewBases[View::Institutions]->slotSelectByObject(obj, eView::Intent::OpenContextMenu); } else if (typeid(obj) == typeid(MyMoneySchedule)) { viewBases[View::Schedules]->slotSelectByObject(obj, eView::Intent::OpenContextMenu); } } diff --git a/kmymoney/views/viewenums.h b/kmymoney/views/viewenums.h index cd457e752..86e91e864 100644 --- a/kmymoney/views/viewenums.h +++ b/kmymoney/views/viewenums.h @@ -1,74 +1,75 @@ /*************************************************************************** viewenums.h ------------------- copyright : (C) 2017 by Łukasz Wojniłowicz ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #ifndef VIEWENUMS_H #define VIEWENUMS_H #include enum class View { Home = 0, Institutions, Accounts, Schedules, Categories, Tags, Payees, Ledgers, Investments, Reports, Budget, Forecast, OnlineJobOutbox, NewLedgers, None }; inline uint qHash(const View key, uint seed) { return ::qHash(static_cast(key), seed); } namespace eView { enum class Tag { All = 0, Referenced, // used tags Unused, // unused tags Opened, // not closed tags Closed // closed tags }; enum class Intent { None, UpdateActions, OpenContextMenu, OpenObject, ShowPayee, ShowTransaction, SynchronizeAccountInInvestmentView, SynchronizeAccountInLedgersView, ToggleColumn, UpdateNetWorth, UpdateProfit, StartEnteringOverdueScheduledTransactions, FinishEnteringOverdueScheduledTransactions, EnterSchedule, ReportProgress, ReportProgressMessage, SelectRegisterTransactions, AccountReconciled, SetOnlinePlugins }; enum class Action { None, Refresh, SetDefaultFocus, AboutToShow, Print, SwitchView, ClosePayeeIdentifierSource, EditInstitution, EditSchedule, CleanupBeforeFileClose, InitializeAfterFileOpen, - DisableViewDepenedendActions + DisableViewDepenedendActions, + ShowBalanceChart }; } #endif