diff --git a/kmymoney/converter/tests/converter-test.cpp b/kmymoney/converter/tests/converter-test.cpp index 588119336..acd2bd188 100644 --- a/kmymoney/converter/tests/converter-test.cpp +++ b/kmymoney/converter/tests/converter-test.cpp @@ -1,201 +1,200 @@ /*************************************************************************** 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" 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/mymoneystoragexml.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("Test Payee"); file->addPayee(payeeTest); MyMoneyPayee payeeTest2("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/kmymoney.cpp b/kmymoney/kmymoney.cpp index 091242994..d69bd3cf3 100644 --- a/kmymoney/kmymoney.cpp +++ b/kmymoney/kmymoney.cpp @@ -1,4311 +1,3990 @@ /*************************************************************************** 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 "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/kgpgkeyselectiondlg.h" #include "dialogs/ktemplateexportdlg.h" #include "dialogs/transactionmatcher.h" #include "wizards/newuserwizard/knewuserwizard.h" #include "wizards/newaccountwizard/knewaccountwizard.h" #include "dialogs/kbalancewarning.h" #include "widgets/kmymoneyaccountselector.h" #include "widgets/kmymoneypayeecombo.h" #include "widgets/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" #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 "storage/mymoneystoragexml.h" -#include "storage/mymoneystoragebin.h" -#include "storage/mymoneystorageanon.h" +#include "imymoneystorageformat.h" #include #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 "misc/platformtools.h" #ifdef KMM_DEBUG #include "mymoney/storage/mymoneystoragedump.h" #include "mymoneytracer.h" #endif using namespace Icons; using namespace eMenu; static constexpr KCompressionDevice::CompressionType const& COMPRESSION_TYPE = KCompressionDevice::GZip; static constexpr char recoveryKeyId[] = "0xD2B08440"; static constexpr char recoveryKeyId2[] = "59B0F826D2B08440"; // define the default period to warn about an expiring recoverkey to 30 days // but allows to override this setting during build time #ifndef RECOVER_KEY_EXPIRATION_WARNING #define RECOVER_KEY_EXPIRATION_WARNING 30 #endif enum backupStateE { BACKUP_IDLE = 0, BACKUP_MOUNTING, BACKUP_COPYING, BACKUP_UNMOUNTING }; class KMyMoneyApp::Private { public: Private(KMyMoneyApp *app) : q(app), m_statementXMLindex(0), m_balanceWarning(0), m_backupState(backupStateE::BACKUP_IDLE), m_backupResult(0), m_backupMount(0), m_ignoreBackupExitCode(false), m_fileOpen(false), m_fmode(QFileDevice::ReadUser | QFileDevice::WriteUser), m_fileType(KMyMoneyApp::KmmXML), 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_saveEncrypted(nullptr), m_additionalKeyLabel(nullptr), m_additionalKeyButton(nullptr), 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 closeFile(); void unlinkStatementXML(); void moveInvestmentTransaction(const QString& fromId, const QString& toId, const MyMoneyTransaction& t); QList > automaticReconciliation(const MyMoneyAccount &account, const QList > &transactions, const MyMoneyMoney &amount); /** * The public interface. */ KMyMoneyApp * const q; int m_statementXMLindex; KBalanceWarning* m_balanceWarning; /** 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; bool m_fileOpen; QFileDevice::Permissions m_fmode; KMyMoneyApp::fileTypeE m_fileType; KProcess m_proc; /// A pointer to the view holding the tabs. KMyMoneyView *m_myMoneyView; /// The URL of the file currently being edited when open. QUrl m_fileName; bool m_startDialog; QString m_mountpoint; QProgressBar* m_progressBar; QTime m_lastUpdate; QLabel* m_statusLabel; // 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; // Pointer to the combo box used for key selection during // File/Save as KComboBox* m_saveEncrypted; // id's that need to be remembered QString m_accountGoto, m_payeeGoto; QStringList m_additionalGpgKeys; QLabel* m_additionalKeyLabel; QPushButton* m_additionalKeyButton; KRecentFilesAction* m_recentFiles; #ifdef KF5Holidays_FOUND // used by the calendar interface for schedules KHolidays::HolidayRegion* m_holidayRegion; #endif #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() { q->connect(MyMoneyFile::instance(), &MyMoneyFile::objectAdded, Models::instance()->accountsModel(), &AccountsModel::slotObjectAdded); q->connect(MyMoneyFile::instance(), &MyMoneyFile::objectModified, Models::instance()->accountsModel(), &AccountsModel::slotObjectModified); q->connect(MyMoneyFile::instance(), &MyMoneyFile::objectRemoved, Models::instance()->accountsModel(), &AccountsModel::slotObjectRemoved); q->connect(MyMoneyFile::instance(), &MyMoneyFile::balanceChanged, Models::instance()->accountsModel(), &AccountsModel::slotBalanceOrValueChanged); q->connect(MyMoneyFile::instance(), &MyMoneyFile::valueChanged, Models::instance()->accountsModel(), &AccountsModel::slotBalanceOrValueChanged); q->connect(MyMoneyFile::instance(), &MyMoneyFile::objectAdded, Models::instance()->institutionsModel(), &InstitutionsModel::slotObjectAdded); q->connect(MyMoneyFile::instance(), &MyMoneyFile::objectModified, Models::instance()->institutionsModel(), &InstitutionsModel::slotObjectModified); q->connect(MyMoneyFile::instance(), &MyMoneyFile::objectRemoved, Models::instance()->institutionsModel(), &InstitutionsModel::slotObjectRemoved); q->connect(MyMoneyFile::instance(), &MyMoneyFile::balanceChanged, Models::instance()->institutionsModel(), &AccountsModel::slotBalanceOrValueChanged); q->connect(MyMoneyFile::instance(), &MyMoneyFile::valueChanged, Models::instance()->institutionsModel(), &AccountsModel::slotBalanceOrValueChanged); q->connect(MyMoneyFile::instance(), &MyMoneyFile::objectAdded, Models::instance()->equitiesModel(), &EquitiesModel::slotObjectAdded); q->connect(MyMoneyFile::instance(), &MyMoneyFile::objectModified, Models::instance()->equitiesModel(), &EquitiesModel::slotObjectModified); q->connect(MyMoneyFile::instance(), &MyMoneyFile::objectRemoved, Models::instance()->equitiesModel(), &EquitiesModel::slotObjectRemoved); q->connect(MyMoneyFile::instance(), &MyMoneyFile::balanceChanged, Models::instance()->equitiesModel(), &EquitiesModel::slotBalanceOrValueChanged); q->connect(MyMoneyFile::instance(), &MyMoneyFile::valueChanged, Models::instance()->equitiesModel(), &EquitiesModel::slotBalanceOrValueChanged); q->connect(MyMoneyFile::instance(), &MyMoneyFile::objectAdded, Models::instance()->securitiesModel(), &SecuritiesModel::slotObjectAdded); q->connect(MyMoneyFile::instance(), &MyMoneyFile::objectModified, Models::instance()->securitiesModel(), &SecuritiesModel::slotObjectModified); q->connect(MyMoneyFile::instance(), &MyMoneyFile::objectRemoved, Models::instance()->securitiesModel(), &SecuritiesModel::slotObjectRemoved); } void disconnectStorageFromModels() { q->disconnect(MyMoneyFile::instance(), &MyMoneyFile::objectAdded, Models::instance()->accountsModel(), &AccountsModel::slotObjectAdded); q->disconnect(MyMoneyFile::instance(), &MyMoneyFile::objectModified, Models::instance()->accountsModel(), &AccountsModel::slotObjectModified); q->disconnect(MyMoneyFile::instance(), &MyMoneyFile::objectRemoved, Models::instance()->accountsModel(), &AccountsModel::slotObjectRemoved); q->disconnect(MyMoneyFile::instance(), &MyMoneyFile::balanceChanged, Models::instance()->accountsModel(), &AccountsModel::slotBalanceOrValueChanged); q->disconnect(MyMoneyFile::instance(), &MyMoneyFile::valueChanged, Models::instance()->accountsModel(), &AccountsModel::slotBalanceOrValueChanged); q->disconnect(MyMoneyFile::instance(), &MyMoneyFile::objectAdded, Models::instance()->institutionsModel(), &InstitutionsModel::slotObjectAdded); q->disconnect(MyMoneyFile::instance(), &MyMoneyFile::objectModified, Models::instance()->institutionsModel(), &InstitutionsModel::slotObjectModified); q->disconnect(MyMoneyFile::instance(), &MyMoneyFile::objectRemoved, Models::instance()->institutionsModel(), &InstitutionsModel::slotObjectRemoved); q->disconnect(MyMoneyFile::instance(), &MyMoneyFile::balanceChanged, Models::instance()->institutionsModel(), &AccountsModel::slotBalanceOrValueChanged); q->disconnect(MyMoneyFile::instance(), &MyMoneyFile::valueChanged, Models::instance()->institutionsModel(), &AccountsModel::slotBalanceOrValueChanged); q->disconnect(MyMoneyFile::instance(), &MyMoneyFile::objectAdded, Models::instance()->equitiesModel(), &EquitiesModel::slotObjectAdded); q->disconnect(MyMoneyFile::instance(), &MyMoneyFile::objectModified, Models::instance()->equitiesModel(), &EquitiesModel::slotObjectModified); q->disconnect(MyMoneyFile::instance(), &MyMoneyFile::objectRemoved, Models::instance()->equitiesModel(), &EquitiesModel::slotObjectRemoved); q->disconnect(MyMoneyFile::instance(), &MyMoneyFile::balanceChanged, Models::instance()->equitiesModel(), &EquitiesModel::slotBalanceOrValueChanged); q->disconnect(MyMoneyFile::instance(), &MyMoneyFile::valueChanged, Models::instance()->equitiesModel(), &EquitiesModel::slotBalanceOrValueChanged); q->disconnect(MyMoneyFile::instance(), &MyMoneyFile::objectAdded, Models::instance()->securitiesModel(), &SecuritiesModel::slotObjectAdded); q->disconnect(MyMoneyFile::instance(), &MyMoneyFile::objectModified, Models::instance()->securitiesModel(), &SecuritiesModel::slotObjectModified); q->disconnect(MyMoneyFile::instance(), &MyMoneyFile::objectRemoved, Models::instance()->securitiesModel(), &SecuritiesModel::slotObjectRemoved); } /** * This method is used after a file or database has been * read into storage, and performs various initialization tasks * * @retval true all went okay * @retval false an exception occurred during this process */ bool initializeStorage() { const auto blocked = MyMoneyFile::instance()->blockSignals(true); updateAccountNames(); updateCurrencyNames(); selectBaseCurrency(); // setup the standard precision AmountEdit::setStandardPrecision(MyMoneyMoney::denomToPrec(MyMoneyFile::instance()->baseCurrency().smallestAccountFraction())); KMyMoneyEdit::setStandardPrecision(MyMoneyMoney::denomToPrec(MyMoneyFile::instance()->baseCurrency().smallestAccountFraction())); if (!applyFileFixes()) return false; MyMoneyFile::instance()->blockSignals(blocked); emit q->kmmFilePlugin(KMyMoneyApp::postOpen); Models::instance()->fileOpened(); connectStorageToModels(); // inform everyone about new data MyMoneyFile::instance()->forceDataChanged(); q->slotCheckSchedules(); m_myMoneyView->slotFileOpened(); onlineJobAdministration::instance()->updateActions(); 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()); } } } } } /** * Calls MyMoneyFile::readAllData which reads a MyMoneyFile into appropriate * data structures in memory. The return result is examined to make sure no * errors occurred whilst parsing. * * @param url The URL to read from. * If no protocol is specified, file:// is assumed. * * @return Whether the read was successful. */ - bool openNondatabase(const QUrl &url) + bool openXMLFile(const QUrl &url) { - if (!url.isValid()) - throw MYMONEYEXCEPTION(QString::fromLatin1("Invalid URL %1").arg(qPrintable(url.url()))); + // open the database + auto pStorage = MyMoneyFile::instance()->storage(); + if (!pStorage) + pStorage = new MyMoneyStorageMgr; - QString fileName; - auto downloadedFile = false; - if (url.isLocalFile()) { - fileName = url.toLocalFile(); - } else { - fileName = KMyMoneyUtils::downloadFile(url); - downloadedFile = true; + auto rc = false; + auto pluginFound = false; + for (const auto& plugin : pPlugins.storage) { + if (plugin->formatName().compare(QLatin1String("XML")) == 0) { + rc = plugin->open(pStorage, url); + pluginFound = true; + break; + } + } + + if(!pluginFound) + KMessageBox::error(q, i18n("Couldn't find suitable plugin to read your storage.")); + + if(!rc) { + removeStorage(); + return false; + } + + if (pStorage) { + MyMoneyFile::instance()->detachStorage(); + MyMoneyFile::instance()->attachStorage(pStorage); } - if (!KMyMoneyUtils::fileExists(QUrl::fromLocalFile(fileName))) - throw MYMONEYEXCEPTION(QString::fromLatin1("Error opening the file.\n" - "Requested file: '%1'.\n" - "Downloaded file: '%2'").arg(qPrintable(url.url()), fileName)); + m_fileType = KMyMoneyApp::KmmXML; + return true; + } + bool isGNCFile(const QUrl &url) + { + if (!url.isValid()) + throw MYMONEYEXCEPTION(QString::fromLatin1("Invalid URL %1").arg(qPrintable(url.url()))); + if (!url.isLocalFile()) + return false; + + const auto fileName = url.toLocalFile(); + const auto sFileToShort = QString::fromLatin1("File %1 is too short.").arg(fileName); QFile file(fileName); if (!file.open(QIODevice::ReadOnly)) throw MYMONEYEXCEPTION(QString::fromLatin1("Cannot read the file: %1").arg(fileName)); QByteArray qbaFileHeader(2, '\0'); - const auto sFileToShort = QString::fromLatin1("File %1 is too short.").arg(fileName); if (file.read(qbaFileHeader.data(), 2) != 2) throw MYMONEYEXCEPTION(sFileToShort); file.close(); - // There's a problem with the KFilterDev and KGPGFile classes: - // One supports the at(n) member but not ungetch() together with - // read() and the other does not provide an at(n) method but - // supports read() that considers the ungetch() buffer. QFile - // supports everything so this is not a problem. We solve the problem - // for now by keeping track of which method can be used. - auto haveAt = true; - auto isEncrypted = false; - - emit q->kmmFilePlugin(preOpen); - QIODevice* qfile = nullptr; QString sFileHeader(qbaFileHeader); - if (sFileHeader == QString("\037\213")) { // gzipped? + if (sFileHeader == QString("\037\213")) // gzipped? qfile = new KCompressionDevice(fileName, COMPRESSION_TYPE); - } else if (sFileHeader == QString("--") || // PGP ASCII armored? - sFileHeader == QString("\205\001") || // PGP binary? - sFileHeader == QString("\205\002")) { // PGP binary? - if (KGPGFile::GPGAvailable()) { - qfile = new KGPGFile(fileName); - haveAt = false; - isEncrypted = true; - } else { - throw MYMONEYEXCEPTION(QString::fromLatin1("GPG is not available for decryption of file %1").arg(fileName)); - } - } else { - // we can't use file directly, as we delete qfile later on - qfile = new QFile(file.fileName()); - } + else + return false; if (!qfile->open(QIODevice::ReadOnly)) { delete qfile; throw MYMONEYEXCEPTION(QString::fromLatin1("Cannot read the file: %1").arg(fileName)); } - qbaFileHeader.resize(8); - if (qfile->read(qbaFileHeader.data(), 8) != 8) - throw MYMONEYEXCEPTION(sFileToShort); - - if (haveAt) - qfile->seek(0); - else - ungetString(qfile, qbaFileHeader.data(), 8); - - // Ok, we got the first block of 8 bytes. Read in the two - // unsigned long int's by preserving endianess. This is - // achieved by reading them through a QDataStream object - qint32 magic0, magic1; - QDataStream s(&qbaFileHeader, QIODevice::ReadOnly); - s >> magic0; - s >> magic1; - - // If both magic numbers match (we actually read in the - // text 'KMyMoney' then we assume a binary file and - // construct a reader for it. Otherwise, we construct - // an XML reader object. - // - // The expression magic0 < 30 is only used to create - // a binary reader if we assume an old binary file. This - // should be removed at some point. An alternative is to - // check the beginning of the file against an pattern - // of the XML file (e.g. '?File %1 contains the old binary format used by KMyMoney. Please use an older version of KMyMoney (0.8.x) that still supports this format to convert it to the new XML based format.").arg(fileName)); - } - // Scan the first 70 bytes to see if we find something // we know. For now, we support our own XML format and // GNUCash XML format. If the file is smaller, then it // contains no valid data and we reject it anyway. qbaFileHeader.resize(70); if (qfile->read(qbaFileHeader.data(), 70) != 70) throw MYMONEYEXCEPTION(sFileToShort); - if (haveAt) - qfile->seek(0); - else - ungetString(qfile, qbaFileHeader.data(), 70); + QString txt(qbaFileHeader); + + qfile->close(); + delete qfile; - IMyMoneyOperationsFormat* pReader = nullptr; - QRegExp kmyexp(""); QRegExp gncexp("formatName().compare(QLatin1String("GNC")) == 0) { - pReader = plugin->reader(); - break; - } - } - if (!pReader) { - KMessageBox::error(q, i18n("Couldn't find suitable plugin to read your storage.")); - return false; + if (!(gncexp.indexIn(txt) != -1)) + return false; + return true; + } + + bool openGNCFile(const QUrl &url) + { + IMyMoneyOperationsFormat* pReader = nullptr; + for (const auto& plugin : pPlugins.storage) { + if (plugin->formatName().compare(QLatin1String("GNC")) == 0) { + pReader = plugin->reader(); + break; } - m_fileType = KMyMoneyApp::GncXML; - } else { - throw MYMONEYEXCEPTION(QString::fromLatin1("%1").arg(i18n("File %1 contains an unknown file format.", fileName))); } + if (!pReader) { + KMessageBox::error(q, i18n("Couldn't find suitable plugin to read your storage.")); + return false; + } + m_fileType = KMyMoneyApp::GncXML; // disconnect the current storga manager from the engine MyMoneyFile::instance()->detachStorage(); // create a new empty storage object auto storage = new MyMoneyStorageMgr; - // attach the storage before reading the file, since the online - // onlineJobAdministration object queries the engine during - // loading. - MyMoneyFile::instance()->attachStorage(storage); - + QIODevice* qfile = new KCompressionDevice(url.toLocalFile(), COMPRESSION_TYPE); pReader->setProgressCallback(&KMyMoneyApp::progressCallback); pReader->readFile(qfile, storage); pReader->setProgressCallback(0); delete pReader; + // attach the storage before reading the file, since the online + // onlineJobAdministration object queries the engine during + // loading. + MyMoneyFile::instance()->attachStorage(storage); + qfile->close(); delete qfile; - - // if a temporary file was downloaded, then it will be removed - // with the next call. Otherwise, it stays untouched on the local - // filesystem. - if (downloadedFile) - QFile::remove(fileName); - - // encapsulate transactions to the engine to be able to commit/rollback - MyMoneyFileTransaction ft; - // make sure we setup the encryption key correctly - if (isEncrypted && MyMoneyFile::instance()->value("kmm-encryption-key").isEmpty()) - MyMoneyFile::instance()->setValue("kmm-encryption-key", KMyMoneySettings::gpgRecipientList().join(",")); - ft.commit(); return true; } /** * This method is called from readFile to open a database file which * is to be processed in 'proper' database mode, i.e. in-place updates * * @param dbaseURL pseudo-QUrl representation of database * * @retval true Database opened successfully * @retval false Could not open or read database */ bool openDatabase(const QUrl &url) { // open the database auto pStorage = MyMoneyFile::instance()->storage(); if (!pStorage) pStorage = new MyMoneyStorageMgr; auto rc = false; auto pluginFound = false; for (const auto& plugin : pPlugins.storage) { if (plugin->formatName().compare(QLatin1String("SQL")) == 0) { rc = plugin->open(pStorage, url); pluginFound = true; break; } } if(!pluginFound) KMessageBox::error(q, i18n("Couldn't find suitable plugin to read your storage.")); if(!rc) { removeStorage(); return false; } if (pStorage) { MyMoneyFile::instance()->detachStorage(); MyMoneyFile::instance()->attachStorage(pStorage); } + + m_fileType = KMyMoneyApp::KmmDb; return true; } /** * Close the currently opened file and create an empty new file. * * @see MyMoneyFile */ void newFile() { closeFile(); m_fileType = KMyMoneyApp::KmmXML; // assume native type until saved m_fileOpen = true; } - /** - * Saves the data into permanent storage using the XML format. - * - * @param url The URL to save into. - * If no protocol is specified, file:// is assumed. - * @param keyList QString containing a comma separated list of keys - * to be used for encryption. If @p keyList is empty, - * the file will be saved unencrypted (the default) - * - * @retval false save operation failed - * @retval true save operation was successful - */ - bool saveFile(const QUrl &url, const QString& keyList = QString()) - { - QString filename = url.path(); - - if (!m_fileOpen) { - KMessageBox::error(q, i18n("Tried to access a file when it has not been opened")); - return false; - } - - emit q->kmmFilePlugin(KMyMoneyApp::preSave); - std::unique_ptr storageWriter; - - // If this file ends in ".ANON.XML" then this should be written using the - // anonymous writer. - bool plaintext = filename.right(4).toLower() == ".xml"; - if (filename.right(9).toLower() == ".anon.xml") - storageWriter = std::make_unique(); - else - storageWriter = std::make_unique(); - - // actually, url should be the parameter to this function - // but for now, this would involve too many changes - auto rc = true; - try { - if (! url.isValid()) { - throw MYMONEYEXCEPTION(QString::fromLatin1("Malformed URL '%1'").arg(url.url())); - } - - if (url.scheme() == QLatin1String("sql")) { - rc = false; - auto pluginFound = false; - for (const auto& plugin : pPlugins.storage) { - if (plugin->formatName().compare(QLatin1String("SQL")) == 0) { - rc = plugin->save(url); - pluginFound = true; - break; - } - } - - if(!pluginFound) - throw MYMONEYEXCEPTION(QString::fromLatin1("Couldn't find suitable plugin to save your storage.")); - - } else if (url.isLocalFile()) { - filename = url.toLocalFile(); - try { - const unsigned int nbak = KMyMoneySettings::autoBackupCopies(); - if (nbak) { - KBackup::numberedBackupFile(filename, QString(), QStringLiteral("~"), nbak); - } - saveToLocalFile(filename, storageWriter.get(), plaintext, keyList); - } catch (const MyMoneyException &) { - throw MYMONEYEXCEPTION(QString::fromLatin1("Unable to write changes to '%1'").arg(filename)); - } - } else { - - QTemporaryFile tmpfile; - tmpfile.open(); // to obtain the name - tmpfile.close(); - saveToLocalFile(tmpfile.fileName(), storageWriter.get(), plaintext, keyList); - - Q_CONSTEXPR int permission = -1; - QFile file(tmpfile.fileName()); - file.open(QIODevice::ReadOnly); - KIO::StoredTransferJob *putjob = KIO::storedPut(file.readAll(), url, permission, KIO::JobFlag::Overwrite); - if (!putjob->exec()) { - throw MYMONEYEXCEPTION(QString::fromLatin1("Unable to upload to '%1'.
%2").arg(url.toDisplayString(), putjob->errorString())); - } - file.close(); - } - m_fileType = KMyMoneyApp::KmmXML; - } catch (const MyMoneyException &e) { - KMessageBox::error(q, QString::fromLatin1(e.what())); - MyMoneyFile::instance()->setDirty(); - rc = false; - } - emit q->kmmFilePlugin(postSave); - return rc; - } - - /** - * This method is used by saveFile() to store the data - * either directly in the destination file if it is on - * the local file system or in a temporary file when - * the final destination is reached over a network - * protocol (e.g. FTP) - * - * @param localFile the name of the local file - * @param writer pointer to the formatter - * @param plaintext whether to override any compression & encryption settings - * @param keyList QString containing a comma separated list of keys to be used for encryption - * If @p keyList is empty, the file will be saved unencrypted - * - * @note This method will close the file when it is written. - */ - void saveToLocalFile(const QString& localFile, IMyMoneyOperationsFormat* pWriter, bool plaintext, const QString& keyList) - { - // Check GPG encryption - bool encryptFile = true; - bool encryptRecover = false; - if (!keyList.isEmpty()) { - if (!KGPGFile::GPGAvailable()) { - KMessageBox::sorry(q, i18n("GPG does not seem to be installed on your system. Please make sure that GPG can be found using the standard search path. This time, encryption is disabled."), i18n("GPG not found")); - encryptFile = false; - } else { - if (KMyMoneySettings::encryptRecover()) { - encryptRecover = true; - if (!KGPGFile::keyAvailable(QString(recoveryKeyId))) { - KMessageBox::sorry(q, i18n("

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

%1

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

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

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

%1.

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

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

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

"); - if (KMessageBox::questionYesNo(q, msg, i18n("Store GPG encrypted"), KStandardGuiItem::yes(), KStandardGuiItem::no(), "StoreEncrypted") == KMessageBox::No) { - encryptFile = false; - } - } - } - } - - - // Create a temporary file if needed - QString writeFile = localFile; - QTemporaryFile tmpFile; - if (QFile::exists(localFile)) { - tmpFile.open(); - writeFile = tmpFile.fileName(); - tmpFile.close(); - } - - /** - * @brief Automatically restore settings when scope is left - */ - struct restorePreviousSettingsHelper { - restorePreviousSettingsHelper() - : m_signalsWereBlocked{MyMoneyFile::instance()->signalsBlocked()} - { - MyMoneyFile::instance()->blockSignals(true); - } - - ~restorePreviousSettingsHelper() - { - MyMoneyFile::instance()->blockSignals(m_signalsWereBlocked); - } - const bool m_signalsWereBlocked; - } restoreHelper; - - MyMoneyFileTransaction ft; - MyMoneyFile::instance()->deletePair("kmm-encryption-key"); - std::unique_ptr device; - - if (!keyList.isEmpty() && encryptFile && !plaintext) { - std::unique_ptr kgpg = std::unique_ptr(new KGPGFile{writeFile}); - if (kgpg) { - for(const QString& key: keyList.split(',', QString::SkipEmptyParts)) { - kgpg->addRecipient(key.toLatin1()); - } - - if (encryptRecover) { - kgpg->addRecipient(recoveryKeyId); - } - MyMoneyFile::instance()->setValue("kmm-encryption-key", keyList); - device = std::unique_ptr(kgpg.release()); - } - } else { - QFile *file = new QFile(writeFile); - // The second parameter of KCompressionDevice means that KCompressionDevice will delete the QFile object - device = std::unique_ptr(new KCompressionDevice{file, true, (plaintext) ? KCompressionDevice::None : COMPRESSION_TYPE}); - } - - ft.commit(); - - if (!device || !device->open(QIODevice::WriteOnly)) { - throw MYMONEYEXCEPTION(QString::fromLatin1("Unable to open file '%1' for writing.").arg(localFile)); - } - - pWriter->setProgressCallback(&KMyMoneyApp::progressCallback); - pWriter->writeFile(device.get(), MyMoneyFile::instance()->storage()); - device->close(); - - // Check for errors if possible, only possible for KGPGFile - QFileDevice *fileDevice = qobject_cast(device.get()); - if (fileDevice && fileDevice->error() != QFileDevice::NoError) { - throw MYMONEYEXCEPTION(QString::fromLatin1("Failure while writing to '%1'").arg(localFile)); - } - - if (writeFile != localFile) { - // This simple comparison is possible because the strings are equal if no temporary file was created. - // If a temporary file was created, it is made in a way that the name is definitely different. So no - // symlinks etc. have to be evaluated. - if (!QFile::remove(localFile) || !QFile::rename(writeFile, localFile)) - throw MYMONEYEXCEPTION(QString::fromLatin1("Failure while writing to '%1'").arg(localFile)); - } - QFile::setPermissions(localFile, m_fmode); - pWriter->setProgressCallback(0); - } - /** * 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_fileOpen) 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())); } }; 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()); updateCaption(true); QFrame* frame = new QFrame; frame->setFrameStyle(QFrame::NoFrame); // values for margin (11) and spacing(6) taken from KDialog implementation QBoxLayout* layout = new QBoxLayout(QBoxLayout::TopToBottom, frame); layout->setContentsMargins(2, 2, 2, 2); layout->setSpacing(6); { #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->newStorage(); d->m_myMoneyView = new KMyMoneyView; layout->addWidget(d->m_myMoneyView, 10); connect(d->m_myMoneyView, &KMyMoneyView::aboutToChangeView, this, &KMyMoneyApp::slotResetSelections); connect(d->m_myMoneyView, SIGNAL(currentPageChanged(KPageWidgetItem*,KPageWidgetItem*)), this, SLOT(slotUpdateActions())); connect(d->m_myMoneyView, &KMyMoneyView::statusMsg, this, &KMyMoneyApp::slotStatusMsg); connect(d->m_myMoneyView, &KMyMoneyView::statusProgress, this, &KMyMoneyApp::slotStatusProgressBar); connect(this, &KMyMoneyApp::fileLoaded, d->m_myMoneyView, &KMyMoneyView::slotRefreshViews); // Initialize kactivities resource instance #ifdef KF5Activities_FOUND d->m_activityResourceInstance = new KActivities::ResourceInstance(window()->winId(), this); connect(this, &KMyMoneyApp::fileLoaded, d->m_activityResourceInstance, &KActivities::ResourceInstance::setUri); #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); d->m_myMoneyView->setStoragePlugins(pPlugins.storage); 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())); // make sure, we get a note when the engine changes state connect(MyMoneyFile::instance(), SIGNAL(dataChanged()), this, SLOT(slotDataChanged())); // connect the WebConnect server connect(d->m_webConnect, SIGNAL(gotUrl(QUrl)), this, SLOT(webConnect(QUrl))); // make sure we have a balance warning object d->m_balanceWarning = new KBalanceWarning(this); // setup the initial configuration slotUpdateConfiguration(QString()); // kickstart date change timer slotDateChanged(); connect(this, SIGNAL(fileLoaded(QUrl)), onlineJobAdministration::instance(), SLOT(updateOnlineTaskProperties())); } KMyMoneyApp::~KMyMoneyApp() { // 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 delete d; } QUrl KMyMoneyApp::lastOpenedURL() { QUrl url = d->m_startDialog ? QUrl() : d->m_fileName; if (!url.isValid()) { url = QUrl::fromUserInput(readLastUsedFile()); } ready(); return url; } void KMyMoneyApp::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::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); } void KMyMoneyApp::resizeEvent(QResizeEvent* ev) { KMainWindow::resizeEvent(ev); updateCaption(true); } int KMyMoneyApp::askSaveOnClose() { int ans; if (KMyMoneySettings::autoSaveOnClose()) { ans = KMessageBox::Yes; } else { ans = KMessageBox::warningYesNoCancel(this, i18n("The file has been changed, save it?")); } return ans; } bool KMyMoneyApp::queryClose() { if (!isReady()) return false; if (d->dirty()) { int ans = askSaveOnClose(); if (ans == KMessageBox::Cancel) return false; else if (ans == KMessageBox::Yes) { bool saved = slotFileSave(); saveOptions(); return saved; } } // if (d->m_myMoneyView->isDatabase()) // slotFileClose(); // close off the database saveOptions(); return true; } ///////////////////////////////////////////////////////////////////// // SLOT IMPLEMENTATION ///////////////////////////////////////////////////////////////////// void KMyMoneyApp::slotFileInfoDialog() { QPointer dlg = new KMyMoneyFileInfoDlg(0); dlg->exec(); delete dlg; } void KMyMoneyApp::slotPerformanceTest() { // dump performance report to stderr int measurement[2]; QTime timer; MyMoneyAccount acc; qDebug("--- Starting performance tests ---"); // AccountList // MyMoneyFile::instance()->preloadCache(); measurement[0] = measurement[1] = 0; timer.start(); for (int i = 0; i < 1000; ++i) { QList list; MyMoneyFile::instance()->accountList(list); measurement[i != 0] = timer.elapsed(); } std::cerr << "accountList()" << std::endl; std::cerr << "First time: " << measurement[0] << " msec" << std::endl; std::cerr << "Total time: " << (measurement[0] + measurement[1]) << " msec" << std::endl; std::cerr << "Average : " << (measurement[0] + measurement[1]) / 1000 << " msec" << std::endl; // Balance of asset account(s) // MyMoneyFile::instance()->preloadCache(); measurement[0] = measurement[1] = 0; acc = MyMoneyFile::instance()->asset(); for (int i = 0; i < 1000; ++i) { timer.start(); MyMoneyMoney result = MyMoneyFile::instance()->balance(acc.id()); measurement[i != 0] += timer.elapsed(); } std::cerr << "balance(Asset)" << std::endl; std::cerr << "First time: " << measurement[0] << " msec" << std::endl; std::cerr << "Average : " << (measurement[0] + measurement[1]) / 1000 << " msec" << std::endl; // total balance of asset account // MyMoneyFile::instance()->preloadCache(); measurement[0] = measurement[1] = 0; acc = MyMoneyFile::instance()->asset(); for (int i = 0; i < 1000; ++i) { timer.start(); MyMoneyMoney result = MyMoneyFile::instance()->totalBalance(acc.id()); measurement[i != 0] += timer.elapsed(); } std::cerr << "totalBalance(Asset)" << std::endl; std::cerr << "First time: " << measurement[0] << " msec" << std::endl; std::cerr << "Average : " << (measurement[0] + measurement[1]) / 1000 << " msec" << std::endl; // Balance of expense account(s) // MyMoneyFile::instance()->preloadCache(); measurement[0] = measurement[1] = 0; acc = MyMoneyFile::instance()->expense(); for (int i = 0; i < 1000; ++i) { timer.start(); MyMoneyMoney result = MyMoneyFile::instance()->balance(acc.id()); measurement[i != 0] += timer.elapsed(); } std::cerr << "balance(Expense)" << std::endl; std::cerr << "First time: " << measurement[0] << " msec" << std::endl; std::cerr << "Average : " << (measurement[0] + measurement[1]) / 1000 << " msec" << std::endl; // total balance of expense account // MyMoneyFile::instance()->preloadCache(); measurement[0] = measurement[1] = 0; acc = MyMoneyFile::instance()->expense(); timer.start(); for (int i = 0; i < 1000; ++i) { MyMoneyMoney result = MyMoneyFile::instance()->totalBalance(acc.id()); measurement[i != 0] = timer.elapsed(); } std::cerr << "totalBalance(Expense)" << std::endl; std::cerr << "First time: " << measurement[0] << " msec" << std::endl; std::cerr << "Total time: " << (measurement[0] + measurement[1]) << " msec" << std::endl; std::cerr << "Average : " << (measurement[0] + measurement[1]) / 1000 << " msec" << std::endl; // transaction list // MyMoneyFile::instance()->preloadCache(); measurement[0] = measurement[1] = 0; if (MyMoneyFile::instance()->asset().accountCount()) { MyMoneyTransactionFilter filter(MyMoneyFile::instance()->asset().accountList()[0]); filter.setDateFilter(QDate(), QDate::currentDate()); QList list; timer.start(); for (int i = 0; i < 100; ++i) { list = MyMoneyFile::instance()->transactionList(filter); measurement[i != 0] = timer.elapsed(); } std::cerr << "transactionList()" << std::endl; std::cerr << "First time: " << measurement[0] << " msec" << std::endl; std::cerr << "Total time: " << (measurement[0] + measurement[1]) << " msec" << std::endl; std::cerr << "Average : " << (measurement[0] + measurement[1]) / 100 << " msec" << std::endl; } // transaction list // MyMoneyFile::instance()->preloadCache(); measurement[0] = measurement[1] = 0; if (MyMoneyFile::instance()->asset().accountCount()) { MyMoneyTransactionFilter filter(MyMoneyFile::instance()->asset().accountList()[0]); filter.setDateFilter(QDate(), QDate::currentDate()); QList list; timer.start(); for (int i = 0; i < 100; ++i) { MyMoneyFile::instance()->transactionList(list, filter); measurement[i != 0] = timer.elapsed(); } std::cerr << "transactionList(list)" << std::endl; std::cerr << "First time: " << measurement[0] << " msec" << std::endl; std::cerr << "Total time: " << (measurement[0] + measurement[1]) << " msec" << std::endl; std::cerr << "Average : " << (measurement[0] + measurement[1]) / 100 << " msec" << std::endl; } // MyMoneyFile::instance()->preloadCache(); } void KMyMoneyApp::slotFileNew() { KMSTATUS(i18n("Creating new document...")); slotFileClose(); if (!d->m_fileOpen) { // next line required until we move all file handling out of KMyMoneyView d->newFile(); d->m_fileName = QUrl(); updateCaption(); NewUserWizard::Wizard *wizard = new NewUserWizard::Wizard(); if (wizard->exec() == QDialog::Accepted) { MyMoneyFileTransaction ft; MyMoneyFile* file = MyMoneyFile::instance(); try { // store the user info file->setUser(wizard->user()); // create and setup base currency file->addCurrency(wizard->baseCurrency()); file->setBaseCurrency(wizard->baseCurrency()); // create a possible institution MyMoneyInstitution inst = wizard->institution(); if (inst.name().length()) { file->addInstitution(inst); } // create a possible checking account auto acc = wizard->account(); if (acc.name().length()) { acc.setInstitutionId(inst.id()); MyMoneyAccount asset = file->asset(); file->addAccount(acc, asset); // create possible opening balance transaction if (!wizard->openingBalance().isZero()) { file->createOpeningBalanceTransaction(acc, wizard->openingBalance()); } } // import the account templates QList templates = wizard->templates(); QList::iterator it_t; for (it_t = templates.begin(); it_t != templates.end(); ++it_t) { (*it_t).importTemplate(&progressCallback); } d->m_fileName = wizard->url(); ft.commit(); KMyMoneySettings::setFirstTimeRun(false); // FIXME This is a bit clumsy. We re-read the freshly // created file to be able to run through all the // fixup logic and then save it to keep the modified // flag off. slotFileSave(); - if (d->openNondatabase(d->m_fileName)) { + if (d->openXMLFile(d->m_fileName)) { d->m_fileOpen = true; d->initializeStorage(); } slotFileSave(); // now keep the filename in the recent files used list //KRecentFilesAction *p = dynamic_cast(action(KStandardAction::name(KStandardAction::OpenRecent))); //if(p) d->m_recentFiles->addUrl(d->m_fileName); writeLastUsedFile(d->m_fileName.url()); } catch (const MyMoneyException &) { // next line required until we move all file handling out of KMyMoneyView d->closeFile(); } if (wizard->startSettingsAfterFinished()) slotSettings(); } else { // next line required until we move all file handling out of KMyMoneyView d->closeFile(); } delete wizard; updateCaption(); emit fileLoaded(d->m_fileName); } } bool KMyMoneyApp::isDatabase() { return (d->m_fileOpen && ((d->m_fileType == KmmDb))); } bool KMyMoneyApp::isNativeFile() { return (d->m_fileOpen && (d->m_fileType < MaxNativeFileType)); } bool KMyMoneyApp::fileOpen() const { return d->m_fileOpen; } +KMyMoneyAppCallback KMyMoneyApp::progressCallback() +{ + return &KMyMoneyApp::progressCallback; +} + +void KMyMoneyApp::consistencyCheck(bool alwaysDisplayResult) +{ + d->consistencyCheck(alwaysDisplayResult); +} + // General open void KMyMoneyApp::slotFileOpen() { KMSTATUS(i18n("Open a file.")); QString prevDir = readLastUsedDir(); QString fileExtensions; fileExtensions.append(i18n("KMyMoney files (*.kmy *.xml)")); fileExtensions.append(QLatin1String(";;")); for (const auto& plugin : pPlugins.storage) { const auto fileExtension = plugin->fileExtension(); if (!fileExtension.isEmpty()) { fileExtensions.append(fileExtension); fileExtensions.append(QLatin1String(";;")); } } fileExtensions.append(i18n("All files (*)")); QPointer dialog = new QFileDialog(this, QString(), prevDir, fileExtensions); dialog->setFileMode(QFileDialog::ExistingFile); dialog->setAcceptMode(QFileDialog::AcceptOpen); if (dialog->exec() == QDialog::Accepted && dialog != nullptr) { slotFileOpenRecent(dialog->selectedUrls().first()); } delete dialog; } bool KMyMoneyApp::isImportableFile(const QUrl &url) { bool result = false; // Iterate through the plugins and see if there's a loaded plugin who can handle it QMap::const_iterator it_plugin = 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::slotFileOpenRecent(const QUrl &url) { KMSTATUS(i18n("Loading file...")); 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; } 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; } if (d->m_fileOpen) slotFileClose(); if (d->m_fileOpen) return; try { auto isOpened = false; if (url.scheme() == QLatin1String("sql")) isOpened = d->openDatabase(url); + else if (d->isGNCFile(url)) + isOpened = d->openGNCFile(url); else - isOpened = d->openNondatabase(url); + isOpened = d->openXMLFile(url); if (!isOpened) return; d->m_fileOpen = true; if (!d->initializeStorage()) { d->m_fileOpen = false; return; } if (isNativeFile()) { d->m_fileName = url; updateCaption(); 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); } else { d->m_fileName = QUrl(); // imported files have no filename } } catch (const MyMoneyException &e) { KMessageBox::sorry(this, i18n("Cannot open file as requested. Error was: %1", QString::fromLatin1(e.what()))); } updateCaption(); emit fileLoaded(d->m_fileName); } bool KMyMoneyApp::slotFileSave() { // if there's nothing changed, there's no need to save anything if (!d->dirty()) return true; bool rc = false; KMSTATUS(i18n("Saving file...")); if (d->m_fileName.isEmpty()) - return slotFileSaveAs(); + return false; d->consistencyCheck(false); setEnabled(false); - if (isDatabase()) { - auto pluginFound = false; - for (const auto& plugin : pPlugins.storage) { - if (plugin->formatName().compare(QLatin1String("SQL")) == 0) { - rc = plugin->save(d->m_fileName); - pluginFound = true; - break; - } - } - if(!pluginFound) - KMessageBox::error(this, i18n("Couldn't find suitable plugin to save your storage.")); - } else { - rc = d->saveFile(d->m_fileName, MyMoneyFile::instance()->value("kmm-encryption-key")); - } - setEnabled(true); - - d->m_autoSaveTimer->stop(); - - updateCaption(); - return rc; -} - -bool KMyMoneyApp::slotFileSaveAs() -{ - bool rc = false; - KMSTATUS(i18n("Saving file with a new filename...")); - - QString selectedKeyName; - if (KGPGFile::GPGAvailable() && KMyMoneySettings::writeDataEncrypted()) { - // fill the secret key list and combo box - QStringList keyList; - KGPGFile::secretKeyList(keyList); - - QPointer dlg = new KGpgKeySelectionDlg(this); - dlg->setSecretKeys(keyList, KMyMoneySettings::gpgRecipient()); - dlg->setAdditionalKeys(KMyMoneySettings::gpgRecipientList()); - rc = dlg->exec(); - if ((rc == QDialog::Accepted) && (dlg != 0)) { - d->m_additionalGpgKeys = dlg->additionalKeys(); - selectedKeyName = dlg->secretKey(); - } - delete dlg; - if (rc != QDialog::Accepted) { + QString format; + switch (d->m_fileType) { + case KMyMoneyApp::KmmXML: + case KMyMoneyApp::GncXML: + format = QStringLiteral("XML"); + break; + case KMyMoneyApp::KmmDb: + format = QStringLiteral("SQL"); + break; + default: return false; - } } - QString prevDir; // don't prompt file name if not a native file - if (isNativeFile()) - prevDir = readLastUsedDir(); - - QPointer dlg = - new QFileDialog(this, i18n("Save As"), prevDir, - QString(QLatin1String("%2 (%1);;")).arg(QStringLiteral("*.kmy")).arg(i18nc("KMyMoney (Filefilter)", "KMyMoney files")) + - QString(QLatin1String("%2 (%1);;")).arg(QStringLiteral("*.xml")).arg(i18nc("XML (Filefilter)", "XML files")) + - QString(QLatin1String("%2 (%1);;")).arg(QStringLiteral("*.anon.xml")).arg(i18nc("Anonymous (Filefilter)", "Anonymous files")) + - QString(QLatin1String("%2 (%1);;")).arg(QStringLiteral("*")).arg(i18nc("All files (Filefilter)", "All files"))); - dlg->setAcceptMode(QFileDialog::AcceptSave); - - if (dlg->exec() == QDialog::Accepted && dlg != 0) { - QUrl newURL = dlg->selectedUrls().first(); - if (!newURL.fileName().isEmpty()) { - d->consistencyCheck(false); - QString newName = newURL.toDisplayString(QUrl::PreferLocalFile); - - // append extension if not present - if (!newName.endsWith(QLatin1String(".kmy"), Qt::CaseInsensitive) && - !newName.endsWith(QLatin1String(".xml"), Qt::CaseInsensitive)) - newName.append(QLatin1String(".kmy")); - newURL = QUrl::fromUserInput(newName); - d->m_recentFiles->addUrl(newURL); - - setEnabled(false); - // If this is the anonymous file export, just save it, don't actually take the - // name, or remember it! Don't even try to encrypt it - if (newName.endsWith(QLatin1String(".anon.xml"), Qt::CaseInsensitive)) - rc = d->saveFile(newURL); - else { - d->m_fileName = newURL; - QString encryptionKeys; - QRegExp keyExp(".* \\((.*)\\)"); - if (keyExp.indexIn(selectedKeyName) != -1) { - encryptionKeys = keyExp.cap(1); - if (!d->m_additionalGpgKeys.isEmpty()) { - if (!encryptionKeys.isEmpty()) - encryptionKeys.append(QLatin1Char(',')); - encryptionKeys.append(d->m_additionalGpgKeys.join(QLatin1Char(','))); - } - } - rc = d->saveFile(d->m_fileName, encryptionKeys); - //write the directory used for this file as the default one for next time. - writeLastUsedDir(newURL.toDisplayString(QUrl::RemoveFilename | QUrl::PreferLocalFile | QUrl::StripTrailingSlash)); - writeLastUsedFile(newName); - } - d->m_autoSaveTimer->stop(); - setEnabled(true); + auto pluginFound = false; + for (const auto& plugin : pPlugins.storage) { + if (plugin->formatName().compare(format) == 0) { + rc = plugin->save(d->m_fileName); + pluginFound = true; + break; } } + if(!pluginFound) + KMessageBox::error(this, i18n("Couldn't find suitable plugin to save your storage.")); + + setEnabled(true); + + d->m_autoSaveTimer->stop(); - delete dlg; updateCaption(); return rc; } void KMyMoneyApp::slotFileCloseWindow() { KMSTATUS(i18n("Closing window...")); if (d->dirty()) { int answer = askSaveOnClose(); if (answer == KMessageBox::Cancel) return; else if (answer == KMessageBox::Yes) slotFileSave(); } close(); } void KMyMoneyApp::slotFileClose() { // bool okToSelect = true; // check if transaction editor is open and ask user what he wants to do // slotTransactionsCancelOrEnter(okToSelect); // if (!okToSelect) // return; // no update status here, as we might delete the status too early. if (d->dirty()) { int answer = askSaveOnClose(); if (answer == KMessageBox::Cancel) return; else if (answer == KMessageBox::Yes) slotFileSave(); } d->closeFile(); } void KMyMoneyApp::slotFileQuit() { // don't modify the status message here as this will prevent quit from working!! // See the beginning of queryClose() and isReady() why. Thomas Baumgart 2005-10-17 bool quitApplication = true; QList memberList = KMainWindow::memberList(); if (!memberList.isEmpty()) { QList::const_iterator w_it = memberList.constBegin(); for (; w_it != memberList.constEnd(); ++w_it) { // only close the window if the closeEvent is accepted. If the user presses Cancel on the saveModified() dialog, // the window and the application stay open. if (!(*w_it)->close()) { quitApplication = false; break; } } } // We will only quit if all windows were processed and not cancelled if (quitApplication) { QCoreApplication::quit(); } } void KMyMoneyApp::slotShowTransactionDetail() { } void KMyMoneyApp::slotHideReconciledTransactions() { 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_fileOpen) { KMessageBox::information(this, i18n("No KMyMoneyFile open")); return; } QFile g("kmymoney.dump"); g.open(QIODevice::WriteOnly); QDataStream st(&g); MyMoneyStorageDump dumper; dumper.writeStream(st, 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_fileOpen) { KMessageBox::information(this, i18n("No KMyMoneyFile open")); return; } KMSTATUS(i18n("Viewing personal data...")); MyMoneyFile* file = MyMoneyFile::instance(); MyMoneyPayee user = file->user(); QPointer editPersonalDataDlg = new EditPersonalDataDlg(user.name(), user.address(), user.city(), user.state(), user.postcode(), user.telephone(), user.email(), this, i18n("Edit Personal Data")); if (editPersonalDataDlg->exec() == QDialog::Accepted && editPersonalDataDlg != 0) { user.setName(editPersonalDataDlg->userName()); user.setAddress(editPersonalDataDlg->userStreet()); user.setCity(editPersonalDataDlg->userTown()); user.setState(editPersonalDataDlg->userCountry()); user.setPostcode(editPersonalDataDlg->userPostcode()); user.setTelephone(editPersonalDataDlg->userTelephone()); user.setEmail(editPersonalDataDlg->userEmail()); MyMoneyFileTransaction ft; try { file->setUser(user); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::information(this, i18n("Unable to store user information: %1", 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()); onlineJobAdministration::instance()->updateActions(); onlineJobAdministration::instance()->setOnlinePlugins(pPlugins.extended); d->m_myMoneyView->setOnlinePlugins(pPlugins.online); d->m_myMoneyView->setStoragePlugins(pPlugins.storage); 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(); // check if the recovery key is still valid or expires soon if (KMyMoneySettings::writeDataEncrypted() && KMyMoneySettings::encryptRecover()) { if (KGPGFile::GPGAvailable()) { KGPGFile file; QDateTime expirationDate = file.keyExpires(QLatin1String(recoveryKeyId2)); if (expirationDate.isValid() && QDateTime::currentDateTime().daysTo(expirationDate) <= RECOVER_KEY_EXPIRATION_WARNING) { bool skipMessage = false; //get global config object for our app. KSharedConfigPtr kconfig = KSharedConfig::openConfig(); KConfigGroup grp; QDate lastWarned; if (kconfig) { grp = d->m_config->group("General Options"); lastWarned = grp.readEntry("LastRecoverKeyExpirationWarning", QDate()); if (QDate::currentDate() == lastWarned) { skipMessage = true; } } if (!skipMessage) { if (kconfig) { grp.writeEntry("LastRecoverKeyExpirationWarning", QDate::currentDate()); } KMessageBox::information(this, i18np("You have configured KMyMoney to use GPG to protect your data and to encrypt your data also with the KMyMoney recover key. This key is about to expire in %1 day. Please update the key from a keyserver using your GPG frontend (e.g. KGPG).", "You have configured KMyMoney to use GPG to protect your data and to encrypt your data also with the KMyMoney recover key. This key is about to expire in %1 days. Please update the key from a keyserver using your GPG frontend (e.g. KGPG).", QDateTime::currentDateTime().daysTo(expirationDate)), i18n("Recover key expires soon")); } } } } } void KMyMoneyApp::slotBackupFile() { // Save the file first so isLocalFile() works if (d->m_myMoneyView && d->dirty()) { if (KMessageBox::questionYesNo(this, i18n("The file must be saved first " "before it can be backed up. Do you want to continue?")) == KMessageBox::No) { return; } slotFileSave(); } if (d->m_fileName.isEmpty()) return; if (!d->m_fileName.isLocalFile()) { KMessageBox::sorry(this, i18n("The current implementation of the backup functionality only supports local files as source files. Your current source file is '%1'.", d->m_fileName.url()), i18n("Local files only")); return; } QPointer backupDlg = new KBackupDlg(this); 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_fileName.fileName()); QString today = QDate::currentDate().toString("-yyyy-MM-dd.") + fi.suffix(); QString backupfile = d->m_mountpoint + '/' + d->m_fileName.fileName(); KMyMoneyUtils::appendCorrectFileExt(backupfile, today); // check if file already exists and ask what to do QFile f(backupfile); if (f.exists()) { int answer = KMessageBox::warningContinueCancel(this, i18n("Backup file for today exists on that device. Replace?"), i18n("Backup"), KGuiItem(i18n("&Replace"))); if (answer == KMessageBox::Cancel) { return false; } } progressCallback(50, 0, i18n("Writing %1", backupfile)); d->m_proc.clearProgram(); #ifdef Q_OS_WIN d->m_proc << "cmd.exe" << "/c" << "copy" << "/b" << "/y"; d->m_proc << (QDir::toNativeSeparators(d->m_fileName.toLocalFile()) + "+ nul") << QDir::toNativeSeparators(backupfile); #else d->m_proc << "cp" << "-f"; d->m_proc << d->m_fileName.toLocalFile() << backupfile; #endif d->m_backupState = BACKUP_COPYING; d->m_proc.start(); return true; } void KMyMoneyApp::slotBackupUnmount() { progressCallback(250, 0, i18n("Unmounting %1", d->m_mountpoint)); d->m_proc.clearProgram(); d->m_proc.setProgram("umount"); d->m_proc << d->m_mountpoint; d->m_backupState = BACKUP_UNMOUNTING; d->m_proc.start(); } void KMyMoneyApp::slotBackupFinish() { d->m_backupState = BACKUP_IDLE; progressCallback(-1, -1, QString()); ready(); } void KMyMoneyApp::slotBackupHandleEvents() { switch (d->m_backupState) { case BACKUP_MOUNTING: if (d->m_ignoreBackupExitCode || (d->m_proc.exitStatus() == QProcess::NormalExit && d->m_proc.exitCode() == 0)) { d->m_ignoreBackupExitCode = false; d->m_backupResult = 0; if (!slotBackupWriteFile()) { d->m_backupResult = 1; if (d->m_backupMount) slotBackupUnmount(); else slotBackupFinish(); } } else { KMessageBox::information(this, i18n("Error mounting device"), i18n("Backup")); d->m_backupResult = 1; if (d->m_backupMount) slotBackupUnmount(); else slotBackupFinish(); } break; case BACKUP_COPYING: if (d->m_proc.exitStatus() == QProcess::NormalExit && d->m_proc.exitCode() == 0) { if (d->m_backupMount) { slotBackupUnmount(); } else { progressCallback(300, 0, i18nc("Backup done", "Done")); KMessageBox::information(this, i18n("File successfully backed up"), i18n("Backup")); slotBackupFinish(); } } else { qDebug("copy exit code is %d", d->m_proc.exitCode()); d->m_backupResult = 1; KMessageBox::information(this, i18n("Error copying file to device"), i18n("Backup")); if (d->m_backupMount) slotBackupUnmount(); else slotBackupFinish(); } break; case BACKUP_UNMOUNTING: if (d->m_proc.exitStatus() == QProcess::NormalExit && d->m_proc.exitCode() == 0) { progressCallback(300, 0, i18nc("Backup done", "Done")); if (d->m_backupResult == 0) KMessageBox::information(this, i18n("File successfully backed up"), i18n("Backup")); } else { KMessageBox::information(this, i18n("Error unmounting device"), i18n("Backup")); } slotBackupFinish(); break; default: qWarning("Unknown state for backup operation!"); progressCallback(-1, -1, QString()); ready(); break; } } void KMyMoneyApp::slotShowTipOfTheDay() { KTipDialog::showTip(d->m_myMoneyView, "", true); } void KMyMoneyApp::slotShowPreviousView() { } void KMyMoneyApp::slotShowNextView() { } void KMyMoneyApp::slotGenerateSql() { // QPointer editor = new KGenerateSqlDlg(this); // editor->setObjectName("Generate Database SQL"); // editor->exec(); // delete editor; } void KMyMoneyApp::slotToolsStartKCalc() { QString cmd = 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::updateCaption(bool skipActions) { QString caption; caption = d->m_fileName.fileName(); if (caption.isEmpty() && d->m_myMoneyView && d->m_fileOpen) caption = i18n("Untitled"); // MyMoneyFile::instance()->dirty() throws an exception, if // there's no storage object available. In this case, we // assume that the storage object is not changed. Actually, // this can only happen if we are newly created early on. bool modified; try { modified = MyMoneyFile::instance()->dirty(); } catch (const MyMoneyException &) { modified = false; skipActions = true; } #ifdef KMM_DEBUG caption += QString(" (%1 x %2)").arg(width()).arg(height()); #endif setCaption(caption, modified); if (!skipActions) { d->m_myMoneyView->enableViewsIfFileOpen(d->m_fileOpen); slotUpdateActions(); } } void KMyMoneyApp::slotUpdateActions() { const auto file = MyMoneyFile::instance(); const bool fileOpen = d->m_fileOpen; const bool modified = file->dirty(); // const bool importRunning = (d->m_smtReader != 0); auto aC = actionCollection(); // ************* // Disabling actions based on conditions // ************* { QString tooltip = i18n("Create a new transaction"); const QVector> actionStates { // {qMakePair(Action::FileOpenDatabase, true)}, // {qMakePair(Action::FileSaveAsDatabase, fileOpen)}, {qMakePair(Action::FilePersonalData, fileOpen)}, {qMakePair(Action::FileBackup, (fileOpen && !isDatabase()))}, {qMakePair(Action::FileInformation, fileOpen)}, {qMakePair(Action::FileImportTemplate, fileOpen/* && !importRunning*/)}, {qMakePair(Action::FileExportTemplate, fileOpen/* && !importRunning*/)}, #ifdef KMM_DEBUG {qMakePair(Action::FileDump, fileOpen)}, #endif {qMakePair(Action::EditFindTransaction, fileOpen)}, {qMakePair(Action::ToolCurrencies, fileOpen)}, {qMakePair(Action::ToolPrices, fileOpen)}, {qMakePair(Action::ToolUpdatePrices, fileOpen)}, {qMakePair(Action::ToolConsistency, fileOpen)}, {qMakePair(Action::NewAccount, fileOpen)}, {qMakePair(Action::AccountCreditTransfer, onlineJobAdministration::instance()->canSendCreditTransfer())}, {qMakePair(Action::NewInstitution, fileOpen)}, // {qMakePair(Action::TransactionNew, (fileOpen && d->m_myMoneyView->canCreateTransactions(KMyMoneyRegister::SelectedTransactions(), tooltip)))}, {qMakePair(Action::NewSchedule, fileOpen)}, // {qMakePair(Action::CurrencyNew, fileOpen)}, // {qMakePair(Action::PriceNew, fileOpen)}, }; for (const auto& a : actionStates) pActions[a.first]->setEnabled(a.second); } // ************* // Disabling standard actions based on conditions // ************* aC->action(QString::fromLatin1(KStandardAction::name(KStandardAction::Save)))->setEnabled(modified /*&& !d->m_myMoneyView->isDatabase()*/); - aC->action(QString::fromLatin1(KStandardAction::name(KStandardAction::SaveAs)))->setEnabled(fileOpen); +// aC->action(QString::fromLatin1(KStandardAction::name(KStandardAction::SaveAs)))->setEnabled(fileOpen); aC->action(QString::fromLatin1(KStandardAction::name(KStandardAction::Close)))->setEnabled(fileOpen); aC->action(QString::fromLatin1(KStandardAction::name(KStandardAction::Print)))->setEnabled(fileOpen && d->m_myMoneyView->canPrint()); } void KMyMoneyApp::slotResetSelections() { d->m_myMoneyView->slotObjectSelected(MyMoneyAccount()); d->m_myMoneyView->slotObjectSelected(MyMoneyInstitution()); d->m_myMoneyView->slotObjectSelected(MyMoneySchedule()); d->m_myMoneyView->slotObjectSelected(MyMoneyTag()); d->m_myMoneyView->slotSelectByVariant(QVariantList {QVariant::fromValue(KMyMoneyRegister::SelectedTransactions())}, eView::Intent::SelectRegisterTransactions); slotUpdateActions(); } void KMyMoneyApp::slotDataChanged() { // As this method is called every time the MyMoneyFile instance // notifies a modification, it's the perfect place to start the timer if needed if (d->m_autoSaveEnabled && !d->m_autoSaveTimer->isActive()) { d->m_autoSaveTimer->setSingleShot(true); d->m_autoSaveTimer->start(d->m_autoSavePeriod * 60 * 1000); //miliseconds } updateCaption(); } void KMyMoneyApp::slotCurrencyDialog() { QPointer dlg = new KCurrencyEditDlg(this); dlg->exec(); delete dlg; } void KMyMoneyApp::slotPriceDialog() { QPointer dlg = new KMyMoneyPriceDlg(this); dlg->exec(); delete dlg; } void KMyMoneyApp::slotFileConsistencyCheck() { d->consistencyCheck(true); updateCaption(); } void KMyMoneyApp::Private::consistencyCheck(bool alwaysDisplayResult) { KMSTATUS(i18n("Running consistency check...")); MyMoneyFileTransaction ft; try { m_consistencyCheckResult = MyMoneyFile::instance()->consistencyCheck(); ft.commit(); } catch (const MyMoneyException &e) { m_consistencyCheckResult.append(i18n("Consistency check failed: %1", e.what())); // always display the result if the check failed alwaysDisplayResult = true; } // in case the consistency check was OK, we get a single line as result // in all errneous cases, we get more than one line and force the // display of them. if (alwaysDisplayResult || m_consistencyCheckResult.size() > 1) { QString msg = i18n("The consistency check has found no issues in your data. Details are presented below."); if (m_consistencyCheckResult.size() > 1) msg = i18n("The consistency check has found some issues in your data. Details are presented below. Those issues that could not be corrected automatically need to be solved by the user."); // install a context menu for the list after the dialog is displayed QTimer::singleShot(500, q, SLOT(slotInstallConsistencyCheckContextMenu())); KMessageBox::informationList(0, msg, m_consistencyCheckResult, i18n("Consistency check result")); } // this data is no longer needed m_consistencyCheckResult.clear(); } void KMyMoneyApp::Private::copyConsistencyCheckResults() { QClipboard *clipboard = QApplication::clipboard(); clipboard->setText(m_consistencyCheckResult.join(QLatin1String("\n"))); } void KMyMoneyApp::Private::saveConsistencyCheckResults() { QUrl fileUrl = QFileDialog::getSaveFileUrl(q); if (!fileUrl.isEmpty()) { QFile file(fileUrl.toLocalFile()); if (file.open(QFile::WriteOnly | QFile::Append | QFile::Text)) { QTextStream out(&file); out << m_consistencyCheckResult.join(QLatin1String("\n")); file.close(); } } } void KMyMoneyApp::Private::setThemedCSS() { const QStringList CSSnames {QStringLiteral("kmymoney.css"), QStringLiteral("welcome.css")}; const QString rcDir("/html/"); const QStringList defaultCSSDirs = QStandardPaths::locateAll(QStandardPaths::AppDataLocation, rcDir, QStandardPaths::LocateDirectory); // scan the list of directories to find the ones that really // contains all files we look for QString defaultCSSDir; foreach (const auto dir, defaultCSSDirs) { defaultCSSDir = dir; foreach (const auto CSSname, CSSnames) { QFileInfo fileInfo(defaultCSSDir + CSSname); if (!fileInfo.exists()) { defaultCSSDir.clear(); break; } } if (!defaultCSSDir.isEmpty()) { break; } } // make sure we have the local directory where the themed version is stored const QString themedCSSDir = QStandardPaths::standardLocations(QStandardPaths::AppConfigLocation).first() + rcDir; QDir().mkpath(themedCSSDir); foreach (const auto CSSname, CSSnames) { const QString defaultCSSFilename = defaultCSSDir + CSSname; QFileInfo fileInfo(defaultCSSFilename); if (fileInfo.exists()) { const QString themedCSSFilename = themedCSSDir + CSSname; QFile::remove(themedCSSFilename); if (QFile::copy(defaultCSSFilename, themedCSSFilename)) { QFile cssFile (themedCSSFilename); if (cssFile.open(QIODevice::ReadWrite)) { QTextStream cssStream(&cssFile); auto cssText = cssStream.readAll(); cssText.replace(QLatin1String("./"), defaultCSSDir, Qt::CaseSensitive); cssText.replace(QLatin1String("WindowText"), 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; } } updateCaption(); } } void KMyMoneyApp::writeLastUsedDir(const QString& directory) { //get global config object for our app. KSharedConfigPtr kconfig = KSharedConfig::openConfig(); if (kconfig) { KConfigGroup grp = kconfig->group("General Options"); //write path entry, no error handling since its void. grp.writeEntry("LastUsedDirectory", directory); } } void KMyMoneyApp::writeLastUsedFile(const QString& fileName) { //get global config object for our app. KSharedConfigPtr kconfig = KSharedConfig::openConfig(); if (kconfig) { KConfigGroup grp = d->m_config->group("General Options"); // write path entry, no error handling since its void. // use a standard string, as fileName could contain a protocol // e.g. file:/home/thb/.... grp.writeEntry("LastUsedFile", fileName); } } QString KMyMoneyApp::readLastUsedDir() const { QString str; //get global config object for our app. KSharedConfigPtr kconfig = KSharedConfig::openConfig(); if (kconfig) { KConfigGroup grp = d->m_config->group("General Options"); //read path entry. Second parameter is the default if the setting is not found, which will be the default document path. str = grp.readEntry("LastUsedDirectory", QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation)); // if the path stored is empty, we use the default nevertheless if (str.isEmpty()) str = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation); } return str; } QString KMyMoneyApp::readLastUsedFile() const { QString str; // get global config object for our app. KSharedConfigPtr kconfig = KSharedConfig::openConfig(); if (kconfig) { KConfigGroup grp = d->m_config->group("General Options"); // read filename entry. str = grp.readEntry("LastUsedFile", ""); } return str; } QString KMyMoneyApp::filename() const { return d->m_fileName.url(); } QUrl KMyMoneyApp::filenameURL() const { return d->m_fileName; } +void KMyMoneyApp::writeFilenameURL(const QUrl &url) +{ + d->m_fileName = 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_fileOpen && 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_fileOpen) { 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((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 int 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 } KMStatus::KMStatus(const QString &text) { m_prevText = kmymoney->slotStatusMsg(text); } KMStatus::~KMStatus() { kmymoney->slotStatusMsg(m_prevText); } void KMyMoneyApp::Private::unlinkStatementXML() { QDir d(KMyMoneySettings::logPath(), "kmm-statement*"); for (uint i = 0; i < d.count(); ++i) { qDebug("Remove %s", qPrintable(d[i])); d.remove(KMyMoneySettings::logPath() + QString("/%1").arg(d[i])); } m_statementXMLindex = 0; } void KMyMoneyApp::Private::closeFile() { m_myMoneyView->slotObjectSelected(MyMoneyAccount()); m_myMoneyView->slotObjectSelected(MyMoneyInstitution()); m_myMoneyView->slotObjectSelected(MyMoneySchedule()); m_myMoneyView->slotObjectSelected(MyMoneyTag()); m_myMoneyView->slotSelectByVariant(QVariantList {QVariant::fromValue(KMyMoneyRegister::SelectedTransactions())}, eView::Intent::SelectRegisterTransactions); m_myMoneyView->finishReconciliation(MyMoneyAccount()); m_myMoneyView->slotFileClosed(); disconnectStorageFromModels(); // notify the models that the file is going to be closed (we should have something like dataChanged that reaches the models first) Models::instance()->fileClosed(); emit q->kmmFilePlugin(KMyMoneyApp::preClose); if (q->isDatabase()) MyMoneyFile::instance()->storage()->close(); // to log off a database user newStorage(); emit q->kmmFilePlugin(postClose); m_fileOpen = false; m_fileName = QUrl(); q->updateCaption(); // just create a new balance warning object delete m_balanceWarning; m_balanceWarning = new KBalanceWarning(q); emit q->fileLoaded(m_fileName); } diff --git a/kmymoney/kmymoney.h b/kmymoney/kmymoney.h index 223276a52..2f9802d87 100644 --- a/kmymoney/kmymoney.h +++ b/kmymoney/kmymoney.h @@ -1,704 +1,702 @@ /*************************************************************************** kmymoney.h ------------------- copyright : (C) 2000-2001 by Michael Edwardes (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. * * * ***************************************************************************/ #ifndef KMYMONEY_H #define KMYMONEY_H // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include // ---------------------------------------------------------------------------- // Project Includes #include #include "kmymoneyutils.h" #include "mymoneyaccount.h" #include "mymoney/onlinejob.h" #include "onlinejobtyped.h" #include "mymoneykeyvaluecontainer.h" #include "mymoneymoney.h" #include "selectedtransactions.h" #include "mymoneysplit.h" #include "mymoneytransaction.h" #include "mymoneyenums.h" class QResizeEvent; class MyMoneyObject; class MyMoneyInstitution; class MyMoneyAccount; class MyMoneySecurity; class MyMoneyPayee; class MyMoneyPrice; class MyMoneyTag; class MyMoneySplit; class MyMoneyTransaction; class WebConnect; class creditTransfer; class IMyMoneyOperationsFormat; template class onlineJobTyped; +typedef void (*KMyMoneyAppCallback)(int, int, const QString &); namespace eDialogs { enum class ScheduleResultCode; } namespace eMenu { enum class Action; enum class Menu; } /*! \mainpage KMyMoney Main Page for API documentation. * * \section intro Introduction * * This is the API documentation for KMyMoney. It should be used as a reference * for KMyMoney developers and users who wish to see how KMyMoney works. This * documentation will be kept up-to-date as development progresses and should be * read for new features that have been developed in KMyMoney. */ /** * The base class for KMyMoney application windows. It sets up the main * window and reads the config file as well as providing a menubar, toolbar * and statusbar. * * @see KMyMoneyView * * @author Michael Edwardes 2000-2001 * @author Thomas Baumgart 2006-2008 * * @short Main application class. */ class KMyMoneyApp : public KXmlGuiWindow, public IMyMoneyProcessingCalendar { Q_OBJECT private Q_SLOTS: /** * Add a context menu to the list used by KMessageBox::informationList to display the consistency check results. */ void slotInstallConsistencyCheckContextMenu(); /** * Handle the context menu of the list used by KMessageBox::informationList to display the consistency check results. */ void slotShowContextMenuForConsistencyCheck(const QPoint &); protected Q_SLOTS: /** * This slot is intended to be used as part of auto saving. This is used when the * QTimer emits the timeout signal and simply checks that the file is dirty (has * received modifications to its contents), and call the appropriate method to * save the file. Furthermore, re-starts the timer (possibly not needed). * @author mvillarino 2005 * @see KMyMoneyApp::slotDataChanged() */ void slotAutoSave(); /** * This slot re-enables all message for which the "Don't show again" * option had been selected. */ void slotEnableMessages(); /** * Called to run performance test. */ void slotPerformanceTest(); /** * Called to generate the sql to create kmymoney database tables etc. */ void slotGenerateSql(); #ifdef KMM_DEBUG /** * Called when the user asks for file information. */ void slotFileFileInfo(); /** * Debugging only: turn on/off traces */ void slotToggleTraces(); #endif /** * Debugging only: turn on/off timers */ void slotToggleTimers(); /** * Called when the user asks for the personal information. */ void slotFileViewPersonal(); void slotLoadAccountTemplates(); void slotSaveAccountTemplates(); /** * Open up the application wide settings dialog. * * @see KSettingsDlg */ void slotSettings(); /** * Called to show credits window. */ void slotShowCredits(); /** * Called when the user wishes to backup the current file */ void slotBackupFile(); /** * Perform mount operation before making a backup of the current file */ void slotBackupMount(); /** * Perform the backup write operation */ bool slotBackupWriteFile(); /** * Perform unmount operation after making a backup of the current file */ void slotBackupUnmount(); /** * Finish backup of the current file */ void slotBackupFinish(); /** * Handle events on making a backup of the current file */ void slotBackupHandleEvents(); void slotShowTipOfTheDay(); void slotShowPreviousView(); void slotShowNextView(); /** * Calls the print logic for the current view */ void slotPrintView(); /** * Call this slot, if any configuration parameter has changed */ void slotUpdateConfiguration(const QString &dialogName); /** * This slot is used to start new features during the development cycle */ void slotNewFeature(); /** * This slot triggers an update of all views and restarts * a single shot timer to call itself again at beginning of * the next day. */ void slotDateChanged(); /** * This slot will be called when the engine data changed * and the application object needs to update its state. */ void slotDataChanged(); /** * This slot collects information for a new scheduled transaction * based on transaction @a t and @a occurrence and saves it in the engine. */ void slotScheduleNew(const MyMoneyTransaction& t, eMyMoney::Schedule::Occurrence occurrence = eMyMoney::Schedule::Occurrence::Monthly); void slotStatusProgressDone(); public: enum fileActions { preOpen, postOpen, preSave, postSave, preClose, postClose }; // Keep a note of the file type typedef enum _fileTypeE { KmmBinary = 0, // native, binary KmmXML, // native, XML KmmDb, // SQL database /* insert new native file types above this line */ MaxNativeFileType, /* and non-native types below */ GncXML // Gnucash XML } fileTypeE; /** * This method checks if there is at least one asset or liability account * in the current storage object. If not, it starts the new account wizard. */ void createInitialAccount(); /** * This method returns the last URL used or an empty URL * depending on the option setting if the last file should * be opened during startup or the open file dialog should * be displayed. * * @return URL of last opened file or empty if the program * should start with the open file dialog */ QUrl lastOpenedURL(); /** * construtor of KMyMoneyApp, calls all init functions to create the application. */ explicit KMyMoneyApp(QWidget* parent = 0); /** * Destructor */ ~KMyMoneyApp(); static void progressCallback(int current, int total, const QString&); void writeLastUsedDir(const QString& directory); QString readLastUsedDir() const; void writeLastUsedFile(const QString& fileName); QString readLastUsedFile() const; /** * Returns whether there is an importer available that can handle this file */ bool isImportableFile(const QUrl &url); /** * 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(bool skipActions = false); /** * This method returns a list of all 'other' dcop registered kmymoney processes. * It's a subset of the return of DCOPclient()->registeredApplications(). * * @retval QStringList of process ids */ QList instanceList() const; #ifdef KMM_DEBUG /** * Dump a list of the names of all defined KActions to stdout. */ void dumpActions() const; #endif /** * Popup the context menu with the respective @p containerName. * Valid container names are defined in kmymoneyui.rc */ void showContextMenu(const QString& containerName); void createAccount(MyMoneyAccount& newAccount, MyMoneyAccount& parentAccount, MyMoneyAccount& brokerageAccount, MyMoneyMoney openingBal); QString filename() const; QUrl filenameURL() const; + void writeFilenameURL(const QUrl &url); - void addToRecentFiles(const QUrl& url); + void addToRecentFiles(const QUrl &url); QTimer* autosaveTimer(); /** * Checks if the file with the @a url already exists. If so, * the user is asked if he/she wants to override the file. * If the user's answer is negative, @p false will be returned. * @p true will be returned in all other cases. */ bool okToWriteFile(const QUrl &url); /** * Return pointer to the WebConnect object */ WebConnect* webConnect() const; /** * Call this to find out if the currently open file is a sql database * * @retval true file is database * @retval false file is serial */ bool isDatabase(); /** * Call this to find out if the currently open file is native KMM * * @retval true file is native * @retval false file is foreign */ bool isNativeFile(); bool fileOpen() const; + KMyMoneyAppCallback progressCallback(); + + void consistencyCheck(bool alwaysDisplayResult); + protected: /** save general Options like all bar positions and status as well as the geometry and the recent file list to the configuration * file */ void saveOptions(); /** * Creates the interfaces necessary for the plugins to work. Therefore, * this method must be called prior to loadPlugins(). */ void createInterfaces(); /** * read general options again and initialize all variables like the recent file list */ void readOptions(); /** * Gets pointers for menus preset by KXMLGUIFactory * @return pointers for menus */ QHash initMenus(); /** * Initializes QActions (names, object names, icons, some connections, shortcuts) * @return pointers for actions */ QHash initActions(); /** initializes the dynamic menus (account selectors) */ void initDynamicMenus(); /** * sets up the statusbar for the main window by initialzing a statuslabel. */ void initStatusBar(); /** queryClose is called by KMainWindow on each closeEvent of a window. Against the * default implementation (only returns true), this calls saveModified() on the document object to ask if the document shall * be saved if Modified; on cancel the closeEvent is rejected. * The settings are saved using saveOptions() if we are about to close. * @see KMainWindow#queryClose * @see QWidget#closeEvent */ bool queryClose() final override; void slotCheckSchedules(); void resizeEvent(QResizeEvent*) final override; void createSchedule(MyMoneySchedule newSchedule, MyMoneyAccount& newAccount); /** * This method preloads the holidays for the duration of the default forecast period */ void preloadHolidays(); public Q_SLOTS: void slotFileInfoDialog(); /** */ void slotFileNew(); /** open a file and load it into the document*/ void slotFileOpen(); bool isFileOpenedInAnotherInstance(const QUrl &url); /** opens a file from the recent files menu */ void slotFileOpenRecent(const QUrl &url); /** * saves the current document. If it has no name yet, the user * will be queried for it. * * @retval false save operation failed * @retval true save operation was successful */ bool slotFileSave(); - /** - * ask the user for the filename and save the current document - * - * @retval false save operation failed - * @retval true save operation was successful - */ - bool slotFileSaveAs(); - /** asks for saving if the file is modified, then closes the actual file and window */ void slotFileCloseWindow(); /** asks for saving if the file is modified, then closes the actual file */ void slotFileClose(); /** * closes all open windows by calling close() on each memberList item * until the list is empty, then quits the application. * If queryClose() returns false because the user canceled the * saveModified() dialog, the closing breaks. */ void slotFileQuit(); void slotFileConsistencyCheck(); /** * fires up the price table editor */ void slotPriceDialog(); /** * fires up the currency table editor */ void slotCurrencyDialog(); /** * dummy method needed just for initialization */ void slotShowTransactionDetail(); /** * Toggles the hide reconciled transactions setting */ void slotHideReconciledTransactions(); /** * Toggles the hide unused categories setting */ void slotHideUnusedCategories(); /** * Toggles the show all accounts setting */ void slotShowAllAccounts(); /** * changes the statusbar contents for the standard label permanently, * used to indicate current actions. Returns the previous value for * 'stacked' usage. * * @param text the text that is displayed in the statusbar */ QString slotStatusMsg(const QString &text); /** * This method changes the progress bar in the status line according * to the parameters @p current and @p total. The following special * cases exist: * * - current = -1 and total = -1 will reset the progress bar * - current = ?? and total != 0 will setup the 100% mark to @p total * - current = xx and total == 0 will set the percentage * * @param current the current value with respect to the initialised * 100% mark * @param total the total value (100%) */ void slotStatusProgressBar(int current, int total = 0); /** * Called to update stock and currency prices from the user menu */ void slotEquityPriceUpdate(); /** * This slot reparents account @p src to be a child of account @p dest * * @param src account to be reparented * @param dest new parent */ void slotReparentAccount(const MyMoneyAccount& src, const MyMoneyAccount& dest); /** * This slot reparents account @p src to be a held at institution @p dest * * @param src account to be reparented * @param dest new parent institution */ void slotReparentAccount(const MyMoneyAccount& src, const MyMoneyInstitution& dest); /** * Create a new investment in a given @p parent investment account */ void slotInvestmentNew(MyMoneyAccount& account, const MyMoneyAccount& parent); /** * Brings up the new category editor and saves the information. * The dialog will be preset with the name and parent account. * * @param account reference of category to be created. The @p name member * should be filled by the caller. The object will be filled * with additional information during the creation process * esp. the @p id member. * @param parent reference to parent account (defaults to none) */ void slotCategoryNew(MyMoneyAccount& account, const MyMoneyAccount& parent); void slotCategoryNew(MyMoneyAccount& account); /** */ void slotPayeeNew(const QString& newnameBase, QString& id); /** * This slot fires up the KCalc application */ void slotToolsStartKCalc(); void slotResetSelections(); /** * Brings up the new account wizard and saves the information. */ void slotAccountNew(MyMoneyAccount&); /** * This method updates all KAction items to the current state. */ void slotUpdateActions(); void webConnect(const QString& sourceUrl, const QByteArray &asn_id); void webConnect(const QUrl url) { webConnect(url.path(), QByteArray()); } private: /** * This method sets the holidayRegion for use by the processing calendar. */ void setHolidayRegion(const QString& holidayRegion); /** * Load the status bar with the 'ready' message. This is hold in a single * place, so that is consistent with isReady(). */ void ready(); /** * Check if the status bar contains the 'ready' message. The return * value is used e.g. to detect if a quit operation is allowed or not. * * @retval true application is idle * @retval false application is active working on a longer operation */ bool isReady(); /** * Re-implemented from IMyMoneyProcessingCalendar */ bool isProcessingDate(const QDate& date) const final override; /** * Depending on the setting of AutoSaveOnQuit, this method * asks the user to save the file or not. * * @returns see return values of KMessageBox::warningYesNoCancel() */ int askSaveOnClose(); Q_SIGNALS: /** * This signal is emitted when a new file is loaded. In the case file * is closed, this signal is also emitted with an empty url. */ void fileLoaded(const QUrl &url); /** * This signal is emitted when a transaction/list of transactions has been selected by * the GUI. If no transaction is selected or the selection is removed, * @p transactions is identical to an empty QList. This signal is used * by plugins to get information about changes. */ void transactionsSelected(const KMyMoneyRegister::SelectedTransactions& transactions); /** * This signal is emitted if a specific transaction with @a transactionId in account * @a accountId is selected */ void transactionSelected(const QString accountId, const QString& transactionId); /** * This signal is sent out, when the user presses Ctrl+A or activates * the Select all transactions action. */ void selectAllTransactions(); /** * This signal is emitted when a new institution has been selected by * the GUI. If no institution is selected or the selection is removed, * @a institution is identical to MyMoneyInstitution(). This signal is used * by plugins to get information about changes. */ void institutionSelected(const MyMoneyInstitution& institution); /** * This signal is emitted when a new schedule has been selected by * the GUI. If no schedule is selected or the selection is removed, * @a schedule is identical to MyMoneySchedule(). This signal is used * by plugins to get information about changes. */ void scheduleSelected(const MyMoneySchedule& schedule); void startMatchTransaction(const MyMoneyTransaction& t); void cancelMatchTransaction(); void kmmFilePlugin(unsigned int); public: bool isActionToggled(const eMenu::Action _a); static const QHash s_Actions; private: /// \internal d-pointer class. class Private; /* * Actually, one should write "Private * const d" but that confuses the KIDL * compiler in this context. It complains about the const keyword. So we leave * it out here */ /// \internal d-pointer instance. Private* d; }; extern KMyMoneyApp *kmymoney; class KMStatus { public: explicit KMStatus(const QString &text); ~KMStatus(); private: QString m_prevText; }; #define KMSTATUS(msg) KMStatus _thisStatus(msg) #endif // KMYMONEY_H diff --git a/kmymoney/mymoney/CMakeLists.txt b/kmymoney/mymoney/CMakeLists.txt index 22766a39c..d946435e8 100644 --- a/kmymoney/mymoney/CMakeLists.txt +++ b/kmymoney/mymoney/CMakeLists.txt @@ -1,124 +1,122 @@ add_subdirectory( storage ) add_subdirectory( payeeidentifier ) ########### next target ############### set(kmm_mymoney_LIB_SRCS mymoneymoney.cpp mymoneyfinancialcalculator.cpp mymoneytransactionfilter.cpp mymoneyfile.cpp mymoneykeyvaluecontainer.cpp mymoneyobject.cpp mymoneypayeeidentifiercontainer.cpp mymoneysplit.cpp mymoneyinstitution.cpp mymoneyinvesttransaction.cpp mymoneyutils.cpp mymoneysecurity.cpp mymoneytransaction.cpp mymoneyschedule.cpp mymoneypayee.cpp mymoneytracer.cpp mymoneytag.cpp mymoneycategory.cpp mymoneycostcenter.cpp mymoneyaccount.cpp mymoneyaccountloan.cpp mymoneyreport.cpp mymoneystatement.cpp mymoneyprice.cpp mymoneybudget.cpp mymoneyforecast.cpp mymoneybalancecache.cpp onlinejob.cpp onlinejobadministration.cpp onlinejobmessage.cpp onlinejobfolder.cpp mymoneycontact.cpp payeeidentifiermodel.cpp ) # storage_a_SOURCES cannot be set in storage directory on MS Windows # because, while building kmm_storage, linker reports many undefined symbols # which are in fact available in kmm_mymoney set(storage_a_SOURCES ./storage/imymoneystorageformat.cpp - ./storage/mymoneystoragexml.cpp ./storage/mymoneystoragemgr.cpp - ./storage/mymoneystorageanon.cpp ./storage/mymoneystoragenames.cpp ) set(onlineTask_a_SOURCES ./onlinetasks/sepa/sepaonlinetransferimpl.cpp ./onlinetasks/sepa/sepaonlinetransfer.cpp ./onlinetasks/unavailabletask/tasks/unavailabletask.cpp ) # NOTE: this payeeIdentifier and its cmake file cannot be used as a template # This payeeIdentifier must be linked with payeeIdentifierLoader because it # is a fallback if something failed during loading of plugins (for xml files only) set(payeeidentifier_a_SOURCES ./payeeidentifier/ibanbic/ibanbic.cpp ./payeeidentifier/nationalaccount/nationalaccount.cpp ./payeeidentifier/unavailableplugin/unavailableplugin.cpp ) list(APPEND storage_a_SOURCES $<$,$,$>: ./storage/mymoneystoragedump.cpp>) list(APPEND kmm_mymoney_LIB_SRCS ${storage_a_SOURCES}) list(APPEND kmm_mymoney_LIB_SRCS ${onlineTask_a_SOURCES}) list(APPEND kmm_mymoney_LIB_SRCS ${payeeidentifier_a_SOURCES}) set(mymoney_HEADERS ${CMAKE_CURRENT_BINARY_DIR}/kmm_mymoney_export.h mymoneyobject.h mymoneyaccount.h mymoneycategory.h mymoneyexception.h mymoneyfile.h mymoneyfinancialcalculator.h mymoneyinstitution.h mymoneyinvesttransaction.h mymoneykeyvaluecontainer.h mymoneymoney.h mymoneypayee.h mymoneytag.h mymoneyprice.h mymoneyreport.h mymoneyschedule.h mymoneysecurity.h mymoneysplit.h mymoneystatement.h mymoneytransactionfilter.h mymoneytransaction.h mymoneyutils.h mymoneybudget.h mymoneyforecast.h imymoneyprocessingcalendar.h mymoneycostcenter.h mymoneyunittestable.h mymoneypayeeidentifiercontainer.h onlinejob.h onlinejobtyped.h onlinejobmessage.h onlinejobfolder.h ) add_library(kmm_mymoney SHARED ${kmm_mymoney_LIB_SRCS}) kde_target_enable_exceptions(kmm_mymoney PUBLIC) target_compile_features(kmm_mymoney PUBLIC cxx_nullptr PRIVATE cxx_decltype cxx_lambdas cxx_constexpr cxx_range_for) generate_export_header(kmm_mymoney BASE_NAME kmm_mymoney) target_link_libraries(kmm_mymoney PUBLIC kmm_icons Qt5::Xml Qt5::Core Qt5::Gui KF5::Service KF5::I18n Alkimia::alkimia kmm_payeeidentifier_loader kmm_payeeidentifier kmm_plugin # TODO: fix this KF5::XmlGui PRIVATE onlinetask_interfaces ) if(KMM_ADDRESSBOOK_FOUND) target_link_libraries(kmm_mymoney PUBLIC KF5::IdentityManagement KF5::AkonadiCore KF5::Contacts) endif() set_target_properties(kmm_mymoney PROPERTIES VERSION ${PROJECT_VERSION} SOVERSION ${PROJECT_VERSION_MAJOR}) ########### install files ############### install(TARGETS kmm_mymoney ${KDE_INSTALL_TARGETS_DEFAULT_ARGS} ) install(FILES ${mymoney_HEADERS} DESTINATION ${INCLUDE_INSTALL_DIR}/kmymoney COMPONENT Devel) ############## tests #################### if(BUILD_TESTING) add_subdirectory(tests) endif() diff --git a/kmymoney/mymoney/tests/mymoneyforecast-test.cpp b/kmymoney/mymoney/tests/mymoneyforecast-test.cpp index f971476ad..aceaf75d3 100644 --- a/kmymoney/mymoney/tests/mymoneyforecast-test.cpp +++ b/kmymoney/mymoney/tests/mymoneyforecast-test.cpp @@ -1,991 +1,990 @@ /* * 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 "mymoneystoragexml.h" #include "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("Test Payee"); file->addPayee(payeeTest); MyMoneyPayee payeeTest2("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); int 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; int 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/CMakeLists.txt b/kmymoney/plugins/CMakeLists.txt index 8237de51e..a90de2acd 100644 --- a/kmymoney/plugins/CMakeLists.txt +++ b/kmymoney/plugins/CMakeLists.txt @@ -1,89 +1,93 @@ add_subdirectory( onlinetasks ) add_subdirectory( interfaces ) add_subdirectory( csv ) add_subdirectory( qif ) add_subdirectory( gnc ) add_subdirectory( ofx ) add_subdirectory( icalendar ) add_subdirectory( reconciliationreport ) add_subdirectory( checkprinting ) add_subdirectory( views ) if(ENABLE_SQLSTORAGE) add_subdirectory(sql) endif() +# if(ENABLE_XMLSTORAGE) + add_subdirectory(xml) +# endif() + if(ENABLE_IBANBICDATA) add_subdirectory(ibanbicdata) endif() if (ENABLE_KBANKING) add_subdirectory( kbanking ) endif (ENABLE_KBANKING) option(ENABLE_SQLCIPHER "Enable SQLCipher plugin" OFF) if (ENABLE_SQLCIPHER) add_subdirectory(sqlcipher) endif(ENABLE_SQLCIPHER) option(ENABLE_ONLINEJOBPLUGINMOCKUP "Enable onlineJob-plugin mockup (only for developers)" OFF) if (ENABLE_ONLINEJOBPLUGINMOCKUP) add_subdirectory(onlinejobpluginmockup) endif() if (ENABLE_WEBOOB) add_subdirectory( weboob ) endif () ########### next target ############### set(kmm_plugin_LIB_SRCS appinterface.cpp importinterface.cpp kmymoneyplugin.cpp statementinterface.cpp viewinterface.cpp onlinepluginextended.cpp interfaceloader.cpp ) set(plugin_HEADERS appinterface.h importinterface.h kmymoneyplugin.h statementinterface.h viewinterface.h ${CMAKE_CURRENT_BINARY_DIR}/kmm_plugin_export.h onlinepluginextended.h ) add_library(kmm_plugin SHARED ${kmm_plugin_LIB_SRCS}) generate_export_header(kmm_plugin BASE_NAME kmm_plugin) target_link_libraries(kmm_plugin PUBLIC Qt5::Core KF5::XmlGui PRIVATE KF5::KCMUtils KF5::KIOWidgets Qt5::Gui Qt5::Widgets INTERFACE kmm_mymoney ) set_target_properties(kmm_plugin PROPERTIES VERSION ${PROJECT_VERSION} SOVERSION ${PROJECT_VERSION_MAJOR}) ########### install files ############### install(TARGETS kmm_plugin ${INSTALL_TARGETS_DEFAULT_ARGS} ) install(FILES ${plugin_HEADERS} DESTINATION ${INCLUDE_INSTALL_DIR}/kmymoney COMPONENT Devel) install(FILES kmymoney-payeeidentifierdelegate.desktop kmymoney-importerplugin.desktop DESTINATION ${SERVICETYPES_INSTALL_DIR} ) diff --git a/kmymoney/plugins/appinterface.h b/kmymoney/plugins/appinterface.h index ff4faaacf..adae97e8f 100644 --- a/kmymoney/plugins/appinterface.h +++ b/kmymoney/plugins/appinterface.h @@ -1,69 +1,77 @@ /*************************************************************************** appinterface.h ------------------- copyright : (C) 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. * * * ***************************************************************************/ #ifndef APPINTERFACE_H #define APPINTERFACE_H // ---------------------------------------------------------------------------- // QT Includes #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes #include class QTimer; class IMyMoneyOperationsFormat; +typedef void (*KMyMoneyAppCallback)(int, int, const QString &); namespace KMyMoneyPlugin { class KMM_PLUGIN_EXPORT AppInterface : public QObject { Q_OBJECT public: explicit AppInterface(QObject* parent, const char* name = 0); virtual ~AppInterface(); /** * Makes sure that a MyMoneyFile is open and has been created successfully. * * @return Whether the file is open and initialised */ virtual bool fileOpen() = 0; virtual bool isDatabase() = 0; + virtual bool isNativeFile() = 0; virtual QUrl filenameURL() const = 0; + virtual void writeFilenameURL(const QUrl &url) = 0; virtual QUrl lastOpenedURL() = 0; virtual void writeLastUsedFile(const QString& fileName) = 0; virtual void slotFileOpenRecent(const QUrl &url) = 0; virtual void addToRecentFiles(const QUrl& url) = 0; virtual void updateCaption(bool skipActions = false) = 0; virtual QTimer* autosaveTimer() = 0; + virtual KMyMoneyAppCallback progressCallback() = 0; + virtual void writeLastUsedDir(const QString &directory) = 0; + virtual QString readLastUsedDir() const = 0; + virtual void consistencyCheck(bool alwaysDisplayResult) = 0; + Q_SIGNALS: void kmmFilePlugin(unsigned int); }; } #endif diff --git a/kmymoney/plugins/interfaces/kmmappinterface.cpp b/kmymoney/plugins/interfaces/kmmappinterface.cpp index 6ef774828..69bc277ba 100644 --- a/kmymoney/plugins/interfaces/kmmappinterface.cpp +++ b/kmymoney/plugins/interfaces/kmmappinterface.cpp @@ -1,82 +1,112 @@ /*************************************************************************** kmmappinterface.cpp ------------------- begin : Mon Apr 14 2008 copyright : (C) 2008 Thomas Baumgart email : ipwizard@users.sourceforge.net (C) 2017 by Łukasz Wojniłowicz ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "kmmappinterface.h" // ---------------------------------------------------------------------------- // QT Includes // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes #include "kmymoney.h" KMyMoneyPlugin::KMMAppInterface::KMMAppInterface(KMyMoneyApp* app, QObject* parent, const char* name) : AppInterface(parent, name), m_app(app) { connect(m_app, &KMyMoneyApp::kmmFilePlugin, this, &AppInterface::kmmFilePlugin); } bool KMyMoneyPlugin::KMMAppInterface::fileOpen() { return m_app->fileOpen(); } bool KMyMoneyPlugin::KMMAppInterface::isDatabase() { return m_app->isDatabase(); } +bool KMyMoneyPlugin::KMMAppInterface::isNativeFile() +{ + return m_app->isNativeFile(); +} + QUrl KMyMoneyPlugin::KMMAppInterface::filenameURL() const { return m_app->filenameURL(); } +void KMyMoneyPlugin::KMMAppInterface::writeFilenameURL(const QUrl &url) +{ + m_app->writeFilenameURL(url); +} + QUrl KMyMoneyPlugin::KMMAppInterface::lastOpenedURL() { return m_app->lastOpenedURL(); } void KMyMoneyPlugin::KMMAppInterface::writeLastUsedFile(const QString& fileName) { m_app->writeLastUsedFile(fileName); } void KMyMoneyPlugin::KMMAppInterface::slotFileOpenRecent(const QUrl &url) { m_app->slotFileOpenRecent(url); } void KMyMoneyPlugin::KMMAppInterface::addToRecentFiles(const QUrl& url) { m_app->addToRecentFiles(url); } void KMyMoneyPlugin::KMMAppInterface::updateCaption(bool skipActions) { m_app->updateCaption(skipActions); } QTimer* KMyMoneyPlugin::KMMAppInterface::autosaveTimer() { return m_app->autosaveTimer(); } + +KMyMoneyAppCallback KMyMoneyPlugin::KMMAppInterface::progressCallback() +{ + return m_app->progressCallback(); +} + +void KMyMoneyPlugin::KMMAppInterface::writeLastUsedDir(const QString &directory) +{ + m_app->writeLastUsedDir(directory); +} + +QString KMyMoneyPlugin::KMMAppInterface::readLastUsedDir() const +{ + return m_app->readLastUsedDir(); +} + +void KMyMoneyPlugin::KMMAppInterface::consistencyCheck(bool alwaysDisplayResult) +{ + m_app->consistencyCheck(alwaysDisplayResult); +} diff --git a/kmymoney/plugins/interfaces/kmmappinterface.h b/kmymoney/plugins/interfaces/kmmappinterface.h index f059f4834..1af3cded9 100644 --- a/kmymoney/plugins/interfaces/kmmappinterface.h +++ b/kmymoney/plugins/interfaces/kmmappinterface.h @@ -1,73 +1,79 @@ /*************************************************************************** kmmappinterface.h ------------------- begin : Mon Apr 14 2008 copyright : (C) 2008 Thomas Baumgart email : ipwizard@users.sourceforge.net (C) 2017 by Łukasz Wojniłowicz ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #ifndef KMMAPPINTERFACE_H #define KMMAPPINTERFACE_H // ---------------------------------------------------------------------------- // QT Includes #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes #include "appinterface.h" class KMyMoneyApp; namespace KMyMoneyPlugin { /** * This class represents the implementation of the * AppInterface. */ class KMMAppInterface : public AppInterface { Q_OBJECT public: explicit KMMAppInterface(KMyMoneyApp* app, QObject* parent, const char* name = 0); ~KMMAppInterface() override = default; /** * Makes sure that a MyMoneyFile is open and has been created successfully. * * @return Whether the file is open and initialised */ bool fileOpen() override; bool isDatabase() override; + bool isNativeFile() override; QUrl filenameURL() const override; + void writeFilenameURL(const QUrl &url) override; QUrl lastOpenedURL() override; void writeLastUsedFile(const QString& fileName) override; void slotFileOpenRecent(const QUrl &url) override; void addToRecentFiles(const QUrl& url) override; void updateCaption(bool skipActions = false) override; QTimer* autosaveTimer() override; + KMyMoneyAppCallback progressCallback() override; + void writeLastUsedDir(const QString &directory) override; + QString readLastUsedDir() const override; + void consistencyCheck(bool alwaysDisplayResult) override; private: KMyMoneyApp* m_app; }; } #endif diff --git a/kmymoney/plugins/xml/CMakeLists.txt b/kmymoney/plugins/xml/CMakeLists.txt new file mode 100644 index 000000000..1bd6250c9 --- /dev/null +++ b/kmymoney/plugins/xml/CMakeLists.txt @@ -0,0 +1,46 @@ +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/xmlstorage.json.in ${CMAKE_CURRENT_BINARY_DIR}/xmlstorage.json @ONLY) + +set(xmlstorage_SOURCES + xmlstorage.cpp + mymoneystoragexml.cpp + mymoneystoragenames.cpp + mymoneystorageanon.cpp + kgpgkeyselectiondlg.cpp + ) + +ki18n_wrap_ui(xmlstorage_SOURCES kgpgkeyselectiondlg.ui) + +kcoreaddons_add_plugin(xmlstorage + SOURCES ${xmlstorage_SOURCES} + JSON "${CMAKE_CURRENT_BINARY_DIR}/xmlstorage.json" + INSTALL_NAMESPACE "kmymoney") + +#kcoreaddons_add_plugin sets LIBRARY_OUTPUT_DIRECTORY to ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/${INSTALL_NAMESPACE} +set_target_properties(xmlstorage + PROPERTIES + LIBRARY_OUTPUT_DIRECTORY "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}") + +# kde_target_enable_exceptions(xmlstorage PUBLIC) + +target_link_libraries(xmlstorage + PUBLIC + kmm_plugin + PRIVATE + Qt5::Xml + KF5::Archive + KF5::I18n + KF5::CoreAddons + kgpgfile + kmymoney_common +) + +install(FILES xmlstorage.rc + DESTINATION "${KXMLGUI_INSTALL_DIR}/xmlstorage") + +# install(FILES kmymoney-xmlstorageplugin.desktop +# DESTINATION ${SERVICETYPES_INSTALL_DIR} +# ) + +# if(BUILD_TESTING) +# add_subdirectory(tests) +# endif() diff --git a/kmymoney/plugins/xml/kgpgkeyselectiondlg.cpp b/kmymoney/plugins/xml/kgpgkeyselectiondlg.cpp new file mode 100644 index 000000000..271b6e615 --- /dev/null +++ b/kmymoney/plugins/xml/kgpgkeyselectiondlg.cpp @@ -0,0 +1,200 @@ +/*************************************************************************** + kgpgkeyselectiondlg.cpp + ------------------- + copyright : (C) 2008 by Thomas Baumgart + email : ipwizard@users.sourceforge.net + (C) 2017 by Łukasz Wojniłowicz + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "kgpgkeyselectiondlg.h" + +// ---------------------------------------------------------------------------- +// QT Includes +#include +#include + +// ---------------------------------------------------------------------------- +// KDE Includes + +// ---------------------------------------------------------------------------- +// Project Includes + +#include +#include + +class KGpgKeySelectionDlgPrivate +{ + Q_DISABLE_COPY(KGpgKeySelectionDlgPrivate) + +public: + KGpgKeySelectionDlgPrivate() + : ui(new Ui::KGpgKeySelectionDlg) + , needCheckList(true) + , listOk(false) + , checkCount(0) + { + } + + ~KGpgKeySelectionDlgPrivate() + { + delete ui; + } + + Ui::KGpgKeySelectionDlg* ui; + bool needCheckList; + bool listOk; + int checkCount; +}; + + +KGpgKeySelectionDlg::KGpgKeySelectionDlg(QWidget *parent) : + QDialog(parent), + d_ptr(new KGpgKeySelectionDlgPrivate) +{ + Q_D(KGpgKeySelectionDlg); + d->ui->setupUi(this); + connect(d->ui->m_secretKey, SIGNAL(currentIndexChanged(int)), this, SLOT(slotIdChanged())); + connect(d->ui->m_listWidget, &KEditListWidget::changed, this, &KGpgKeySelectionDlg::slotIdChanged); + connect(d->ui->m_listWidget, &KEditListWidget::added, this, &KGpgKeySelectionDlg::slotKeyListChanged); + connect(d->ui->m_listWidget, &KEditListWidget::removed, this, &KGpgKeySelectionDlg::slotKeyListChanged); +} + +KGpgKeySelectionDlg::~KGpgKeySelectionDlg() +{ + Q_D(KGpgKeySelectionDlg); + delete d; +} + +void KGpgKeySelectionDlg::setSecretKeys(const QStringList& keyList, const QString& defaultKey) +{ + static constexpr char recoveryKeyId[] = "59B0F826D2B08440"; + + Q_D(KGpgKeySelectionDlg); + d->ui->m_secretKey->addItem(i18n("No encryption")); + + foreach(auto key, keyList) { + QStringList fields = key.split(':', QString::SkipEmptyParts); + if (fields[0] != recoveryKeyId) { + // replace parenthesis in name field with brackets + auto name = fields[1]; + name.replace('(', "["); + name.replace(')', "]"); + name = QString("%1 (0x%2)").arg(name).arg(fields[0]); + d->ui->m_secretKey->addItem(name); + if (name.contains(defaultKey)) { + d->ui->m_secretKey->setCurrentText(name); + } + } + } +} + +QString KGpgKeySelectionDlg::secretKey() const +{ + Q_D(const KGpgKeySelectionDlg); + const bool enabled = (d->ui->m_secretKey->currentIndex() != 0); + QString key; + if (enabled) { + key = d->ui->m_secretKey->currentText(); + } + return key; +} + +void KGpgKeySelectionDlg::setAdditionalKeys(const QStringList& list) +{ + Q_D(KGpgKeySelectionDlg); + d->ui->m_listWidget->clear(); + d->ui->m_listWidget->insertStringList(list); + slotKeyListChanged(); +} + +QStringList KGpgKeySelectionDlg::additionalKeys() const +{ + Q_D(const KGpgKeySelectionDlg); + return d->ui->m_listWidget->items(); +} + +#if 0 +void KGpgKeySelectionDlg::slotShowHelp() +{ + QString anchor = m_helpAnchor[m_criteriaTab->currentPage()]; + if (anchor.isEmpty()) + anchor = QString("details.search"); + + KHelpClient::invokeHelp(anchor); +} +#endif + +void KGpgKeySelectionDlg::slotKeyListChanged() +{ + Q_D(KGpgKeySelectionDlg); + d->needCheckList = true; + slotIdChanged(); +} + +void KGpgKeySelectionDlg::slotIdChanged() +{ + Q_D(KGpgKeySelectionDlg); + // this looks a bit awkward. Here's why: KGPGFile::keyAvailable() starts + // an external task and processes UI events while it waits for the external + // process to finish. Thus, the first time we get here, the external process + // is started and the user may press a second key which calls this routine + // again. + // + // The second invocation is counted, but the check is not started until the + // first one finishes. Once the external process finishes, we check if we + // were called in the meantime and restart the check. + if (++d->checkCount == 1) { + const bool enabled = (d->ui->m_secretKey->currentIndex() != 0); + d->ui->m_listWidget->setEnabled(enabled); + d->ui->m_keyLed->setState(enabled ? KLed::On : KLed::Off); + while (enabled) { + // first we check the current edit field if filled + bool keysOk = true; + if (!d->ui->m_listWidget->currentText().isEmpty()) { + keysOk = KGPGFile::keyAvailable(d->ui->m_listWidget->currentText()); + } + + // if it is available, then scan the current list if we need to + if (keysOk) { + if (d->needCheckList) { + QStringList keys = d->ui->m_listWidget->items(); + QStringList::const_iterator it_s; + for (it_s = keys.constBegin(); keysOk && it_s != keys.constEnd(); ++it_s) { + if (!KGPGFile::keyAvailable(*it_s)) + keysOk = false; + } + d->listOk = keysOk; + d->needCheckList = false; + + } else { + keysOk = d->listOk; + } + } + + // did we receive some more requests to check? + if (d->checkCount > 1) { + d->checkCount = 1; + continue; + } + + if (!d->ui->m_listWidget->items().isEmpty()) { + d->ui->m_keyLed->setState(static_cast(keysOk ? KLed::On : KLed::Off)); + } else { + d->ui->m_keyLed->setState(KLed::On); + } + break; + } + + --d->checkCount; + d->ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(!enabled || (d->ui->m_keyLed->state() == KLed::On)); + } +} diff --git a/kmymoney/plugins/interfaces/kmmappinterface.h b/kmymoney/plugins/xml/kgpgkeyselectiondlg.h similarity index 51% copy from kmymoney/plugins/interfaces/kmmappinterface.h copy to kmymoney/plugins/xml/kgpgkeyselectiondlg.h index f059f4834..165b4a9f8 100644 --- a/kmymoney/plugins/interfaces/kmmappinterface.h +++ b/kmymoney/plugins/xml/kgpgkeyselectiondlg.h @@ -1,73 +1,77 @@ /*************************************************************************** - kmmappinterface.h + kgpgkeyselectiondlg.h ------------------- - begin : Mon Apr 14 2008 - copyright : (C) 2008 Thomas Baumgart + copyright : (C) 2008 by Thomas Baumgart email : ipwizard@users.sourceforge.net (C) 2017 by Łukasz Wojniłowicz ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ -#ifndef KMMAPPINTERFACE_H -#define KMMAPPINTERFACE_H +#ifndef KGPGKEYSELECTIONDLG_H +#define KGPGKEYSELECTIONDLG_H // ---------------------------------------------------------------------------- // QT Includes -#include +#include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes -#include "appinterface.h" +/** + * @author Thomas Baumgart + */ +class KGpgKeySelectionDlgPrivate; +class KGpgKeySelectionDlg : public QDialog +{ + Q_OBJECT + Q_DISABLE_COPY(KGpgKeySelectionDlg) -class KMyMoneyApp; +public: + + explicit KGpgKeySelectionDlg(QWidget* parent = nullptr); + ~KGpgKeySelectionDlg(); -namespace KMyMoneyPlugin -{ /** - * This class represents the implementation of the - * AppInterface. - */ - class KMMAppInterface : public AppInterface - { - Q_OBJECT - - public: - explicit KMMAppInterface(KMyMoneyApp* app, QObject* parent, const char* name = 0); - ~KMMAppInterface() override = default; - - /** - * Makes sure that a MyMoneyFile is open and has been created successfully. - * - * @return Whether the file is open and initialised - */ - bool fileOpen() override; - - bool isDatabase() override; - QUrl filenameURL() const override; - QUrl lastOpenedURL() override; - void writeLastUsedFile(const QString& fileName) override; - void slotFileOpenRecent(const QUrl &url) override; - void addToRecentFiles(const QUrl& url) override; - void updateCaption(bool skipActions = false) override; - QTimer* autosaveTimer() override; - - private: - KMyMoneyApp* m_app; - }; - -} + * preset the key selector with the keys contained in @a keyList. + * The key contained in @a defaultKey is made the current selection. + */ + void setSecretKeys(const QStringList& keyList, const QString& defaultKey); + + /** + * preset the additional key list with the given key ids in @a list + */ + void setAdditionalKeys(const QStringList& list); + + /** + * Returns the selected secret key. In case "No encryption" is selected, + * the string is empty. + */ + QString secretKey() const; + + /** + * Returns the list of keys currently listed in the KEditListWidget + */ + QStringList additionalKeys() const; + +protected Q_SLOTS: + void slotIdChanged(); + void slotKeyListChanged(); + +private: + KGpgKeySelectionDlgPrivate * const d_ptr; + Q_DECLARE_PRIVATE(KGpgKeySelectionDlg) +}; #endif diff --git a/kmymoney/plugins/xml/kgpgkeyselectiondlg.ui b/kmymoney/plugins/xml/kgpgkeyselectiondlg.ui new file mode 100644 index 000000000..bae6fe640 --- /dev/null +++ b/kmymoney/plugins/xml/kgpgkeyselectiondlg.ui @@ -0,0 +1,139 @@ + + + KGpgKeySelectionDlg + + + + 0 + 0 + 575 + 480 + + + + Select additional keys + + + true + + + true + + + + + + You have configured KMyMoney to save your data secured with GPG. Please choose the key you want to use for encryption of your data. + + + true + + + + + + + + + + Add additional keys here + + + + + + + Enter the id of the key you want to use for data encryption. This can either be an e-mail address or the hexadecimal key id. In case of the key id, do not forget the leading 0x. + + + + + + + + + + + + Keys for all of the above user ids found + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + KEditListWidget + QWidget +
keditlistwidget.h
+
+ + KLed + QWidget +
kled.h
+
+
+ + buttonBox + + + + + buttonBox + accepted() + KGpgKeySelectionDlg + accept() + + + 244 + 415 + + + 157 + 274 + + + + + buttonBox + rejected() + KGpgKeySelectionDlg + reject() + + + 312 + 415 + + + 286 + 274 + + + + +
diff --git a/kmymoney/mymoney/storage/mymoneystorageanon.cpp b/kmymoney/plugins/xml/mymoneystorageanon.cpp similarity index 100% rename from kmymoney/mymoney/storage/mymoneystorageanon.cpp rename to kmymoney/plugins/xml/mymoneystorageanon.cpp diff --git a/kmymoney/mymoney/storage/mymoneystorageanon.h b/kmymoney/plugins/xml/mymoneystorageanon.h similarity index 100% rename from kmymoney/mymoney/storage/mymoneystorageanon.h rename to kmymoney/plugins/xml/mymoneystorageanon.h diff --git a/kmymoney/mymoney/storage/mymoneystoragebin.h b/kmymoney/plugins/xml/mymoneystoragebin.h similarity index 100% rename from kmymoney/mymoney/storage/mymoneystoragebin.h rename to kmymoney/plugins/xml/mymoneystoragebin.h diff --git a/kmymoney/plugins/xml/mymoneystoragenames.cpp b/kmymoney/plugins/xml/mymoneystoragenames.cpp new file mode 100644 index 000000000..c928aee7c --- /dev/null +++ b/kmymoney/plugins/xml/mymoneystoragenames.cpp @@ -0,0 +1,100 @@ +/* + * 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 "mymoneystoragenames.h" + +namespace MyMoneyStorageTags { + +const QHash tagNames = { + {tnInstitutions, QStringLiteral("INSTITUTIONS")}, + {tnPayees, QStringLiteral("PAYEES")}, + {tnCostCenters, QStringLiteral("COSTCENTERS")}, + {tnTags, QStringLiteral("TAGS")}, + {tnAccounts, QStringLiteral("ACCOUNTS")}, + {tnTransactions, QStringLiteral("TRANSACTIONS")}, + {tnSchedules, QStringLiteral("SCHEDULES")}, + {tnSecurities, QStringLiteral("SECURITIES")}, + {tnCurrencies, QStringLiteral("CURRENCIES")}, + {tnPrices, QStringLiteral("PRICES")}, + {tnReports, QStringLiteral("REPORTS")}, + {tnBudgets, QStringLiteral("BUDGETS")}, + {tnOnlineJobs, QStringLiteral("ONLINEJOBS")}, + {tnKMMFile, QStringLiteral("KMYMONEY-FILE")}, + {tnFileInfo, QStringLiteral("FILEINFO")}, + {tnUser, QStringLiteral("USER")} +}; + +} + +namespace MyMoneyStorageNodes { + +const QHash nodeNames = { + {nnInstitution, QStringLiteral("INSTITUTION")}, + {nnPayee, QStringLiteral("PAYEE")}, + {nnCostCenter, QStringLiteral("COSTCENTER")}, + {nnTag, QStringLiteral("TAG")}, + {nnAccount, QStringLiteral("ACCOUNT")}, + {nnTransaction, QStringLiteral("TRANSACTION")}, + {nnScheduleTX, QStringLiteral("SCHEDULED_TX")}, + {nnSecurity, QStringLiteral("SECURITY")}, + {nnCurrency, QStringLiteral("CURRENCY")}, + {nnPrice, QStringLiteral("PRICE")}, + {nnPricePair, QStringLiteral("PRICEPAIR")}, + {nnReport, QStringLiteral("REPORT")}, + {nnBudget, QStringLiteral("BUDGET")}, + {nnOnlineJob, QStringLiteral("ONLINEJOB")}, + {nnKeyValuePairs, QStringLiteral("KEYVALUEPAIRS")}, + {nnEquity, QStringLiteral("EQUITY")}, +}; + +} + +namespace MyMoneyStorageAttributes { + +const QHash attrNames = { + {anID, QStringLiteral("id")}, + {anDate, QStringLiteral("date")}, + {anCount, QStringLiteral("count")}, + {anFrom, QStringLiteral("from")}, + {anTo, QStringLiteral("to")}, + {anSource, QStringLiteral("source")}, + {anKey, QStringLiteral("key")}, + {anValue, QStringLiteral("value")}, + {anPrice, QStringLiteral("price")}, + {anName, QStringLiteral("name")}, + {anEmail, QStringLiteral("email")}, + {anCountry, QStringLiteral("county")}, + {anCity, QStringLiteral("city")}, + {anZipCode, QStringLiteral("zipcode")}, + {anStreet, QStringLiteral("street")}, + {anTelephone, QStringLiteral("telephone")} +}; + +} + +namespace MyMoneyStandardAccounts { + + // definitions for the ID's of the standard accounts + const QHash stdAccNames { + {stdAccLiability, QStringLiteral("AStd::Liability")}, + {stdAccAsset, QStringLiteral("AStd::Asset")}, + {stdAccExpense, QStringLiteral("AStd::Expense")}, + {stdAccIncome, QStringLiteral("AStd::Income")}, + {stdAccEquity, QStringLiteral("AStd::Equity")}, + }; + +} diff --git a/kmymoney/plugins/xml/mymoneystoragenames.h b/kmymoney/plugins/xml/mymoneystoragenames.h new file mode 100644 index 000000000..f1e44b7a9 --- /dev/null +++ b/kmymoney/plugins/xml/mymoneystoragenames.h @@ -0,0 +1,71 @@ +/* + * 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 MYMONEYSTORAGENAMES_H +#define MYMONEYSTORAGENAMES_H + +// ---------------------------------------------------------------------------- +// QT Includes + +#include + +// ---------------------------------------------------------------------------- +// Project Includes + +namespace MyMoneyStorageTags { + +enum tagNameE { tnInstitutions, tnPayees, tnCostCenters, + tnTags, tnAccounts, tnTransactions, + tnSchedules, tnSecurities, tnCurrencies, + tnPrices, tnReports, tnBudgets, tnOnlineJobs, + tnKMMFile, tnFileInfo, tnUser + }; + +extern const QHash tagNames; +} + +namespace MyMoneyStorageNodes { + +enum ndNameE { nnInstitution, nnPayee, nnCostCenter, + nnTag, nnAccount, nnTransaction, + nnScheduleTX, nnSecurity, nnCurrency, + nnPrice, nnPricePair, nnReport, nnBudget, nnOnlineJob, + nnKeyValuePairs, nnEquity + }; + +extern const QHash nodeNames; +} + +namespace MyMoneyStorageAttributes { + +enum attrNameE { anID, anDate, anCount, anFrom, anTo, + anSource, anKey, anValue, anPrice, + anName, anEmail, anCountry, anCity, + anZipCode, anStreet, anTelephone + }; + +extern const QHash attrNames; +} + +namespace MyMoneyStandardAccounts { + +enum idNameE { stdAccLiability, stdAccAsset, stdAccExpense, stdAccIncome, stdAccEquity }; + +extern const QHash stdAccNames; +} + +#endif diff --git a/kmymoney/mymoney/storage/mymoneystoragexml.cpp b/kmymoney/plugins/xml/mymoneystoragexml.cpp similarity index 100% rename from kmymoney/mymoney/storage/mymoneystoragexml.cpp rename to kmymoney/plugins/xml/mymoneystoragexml.cpp diff --git a/kmymoney/mymoney/storage/mymoneystoragexml.h b/kmymoney/plugins/xml/mymoneystoragexml.h similarity index 98% rename from kmymoney/mymoney/storage/mymoneystoragexml.h rename to kmymoney/plugins/xml/mymoneystoragexml.h index d12f5c15e..83c3b98ce 100644 --- a/kmymoney/mymoney/storage/mymoneystoragexml.h +++ b/kmymoney/plugins/xml/mymoneystoragexml.h @@ -1,199 +1,197 @@ /* * Copyright 2002-2004 Kevin Tambascio * Copyright 2002-2016 Thomas Baumgart * Copyright 2004-2005 Ace Jones * Copyright 2006 Darren Gould * 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 MYMONEYSTORAGEXML_H #define MYMONEYSTORAGEXML_H -#include "kmm_mymoney_export.h" - // ---------------------------------------------------------------------------- // QT Includes #include #include // ---------------------------------------------------------------------------- // Project Includes #include "imymoneystorageformat.h" /** *@author Kevin Tambascio (ktambascio@users.sourceforge.net) */ #define VERSION_0_60_XML 0x10000010 // Version 0.5 file version info #define VERSION_0_61_XML 0x10000011 // use 8 bytes for MyMoneyMoney objects class QString; class QIODevice; class QDomElement; class QDomDocument; class QDate; class MyMoneyStorageMgr; class MyMoneyInstitution; class MyMoneyAccount; class MyMoneySecurity; class MyMoneySchedule; class MyMoneyPayee; class MyMoneyTag; class MyMoneyBudget; class MyMoneyReport; class MyMoneyPrice; class MyMoneyTransaction; class MyMoneyCostCenter; class onlineJob; template class QList; typedef QPair MyMoneySecurityPair; typedef QMap MyMoneyPriceEntries; typedef QMap MyMoneyPriceList; -class KMM_MYMONEY_EXPORT MyMoneyStorageXML : public IMyMoneyOperationsFormat +class MyMoneyStorageXML : public IMyMoneyOperationsFormat { friend class MyMoneyXmlContentHandler; public: MyMoneyStorageXML(); virtual ~MyMoneyStorageXML(); enum fileVersionDirectionType { Reading = 0, /**< version of file to be read */ Writing = 1 /**< version to be used when writing a file */ }; protected: void setProgressCallback(void(*callback)(int, int, const QString&)) override; void signalProgress(int current, int total, const QString& = ""); /** * This method returns the version of the underlying file. It * is used by the MyMoney objects contained in a MyMoneyStorageBin object (e.g. * MyMoneyAccount, MyMoneyInstitution, MyMoneyTransaction, etc.) to * determine the layout used when reading/writing a persistant file. * A parameter is used to determine the direction. * * @param dir information about the direction (reading/writing). The * default is reading. * * @return version QString of file's version * * @see m_fileVersionRead, m_fileVersionWrite */ static unsigned int fileVersion(fileVersionDirectionType dir = Reading); QList readElements(QString groupTag, QString itemTag = QString()); bool readFileInformation(const QDomElement& fileInfo); virtual void writeFileInformation(QDomElement& fileInfo); virtual void writeUserInformation(QDomElement& userInfo); virtual void writeInstitution(QDomElement& institutions, const MyMoneyInstitution& i); virtual void writeInstitutions(QDomElement& institutions); virtual void writePrices(QDomElement& prices); virtual void writePricePair(QDomElement& price, const MyMoneyPriceEntries& p); virtual void writePrice(QDomElement& prices, const MyMoneyPrice& p); virtual void writePayees(QDomElement& payees); virtual void writePayee(QDomElement& payees, const MyMoneyPayee& p); virtual void writeTags(QDomElement& tags); virtual void writeTag(QDomElement& tags, const MyMoneyTag& ta); virtual void writeAccounts(QDomElement& accounts); virtual void writeAccount(QDomElement& accounts, const MyMoneyAccount& p); virtual void writeTransactions(QDomElement& transactions); virtual void writeTransaction(QDomElement& transactions, const MyMoneyTransaction& tx); virtual void writeSchedules(QDomElement& scheduled); virtual void writeSchedule(QDomElement& scheduledTx, const MyMoneySchedule& tx); virtual void writeReports(QDomElement& e); virtual void writeReport(QDomElement& report, const MyMoneyReport& r); virtual void writeBudgets(QDomElement& e); virtual void writeBudget(QDomElement& budget, const MyMoneyBudget& b); virtual void writeOnlineJobs(QDomElement& onlineJobs); virtual void writeOnlineJob(QDomElement& onlineJobs, const onlineJob& job); virtual void writeSecurities(QDomElement& securities); virtual void writeSecurity(QDomElement& securityElement, const MyMoneySecurity& security); virtual void writeCostCenters(QDomElement& parent); virtual void writeCostCenter(QDomElement& costCenters, const MyMoneyCostCenter& costCenter); virtual void writeCurrencies(QDomElement& currencies); virtual QDomElement writeKeyValuePairs(const QMap pairs); virtual void readFile(QIODevice* s, MyMoneyStorageMgr* storage) override; virtual void writeFile(QIODevice* s, MyMoneyStorageMgr* storage) override; bool readUserInformation(const QDomElement& userElement); void readPricePair(const QDomElement& pricePair); const MyMoneyPrice readPrice(const QString& from, const QString& to, const QDomElement& price); QDomElement findChildElement(const QString& name, const QDomElement& root); private: void (*m_progressCallback)(int, int, const QString&); enum elNameE { enAddress, enCreationDate, enLastModifiedDate, enVersion, enFixVersion, enPair }; static const QString getElName(const elNameE _el); protected: MyMoneyStorageMgr *m_storage; QDomDocument *m_doc; private: /// \internal d-pointer class. class Private; /// \internal d-pointer instance. Private* const d; /** * This member is used to store the file version information * obtained while reading a file. */ static unsigned int fileVersionRead; /** * This member is used to store the file version information * to be used when writing a file. */ static unsigned int fileVersionWrite; /** * This member keeps the id of the base currency. We need this * temporarily to convert the price history from the old to the * new format. This should go at some time beyond 0.8 (ipwizard) */ QString m_baseCurrencyId; }; #endif diff --git a/kmymoney/plugins/xml/xmlstorage.cpp b/kmymoney/plugins/xml/xmlstorage.cpp new file mode 100644 index 000000000..22c96691b --- /dev/null +++ b/kmymoney/plugins/xml/xmlstorage.cpp @@ -0,0 +1,517 @@ +/* + * 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 . + */ + +#include "xmlstorage.h" + +#include +#include + +// ---------------------------------------------------------------------------- +// QT Includes + +#include +#include +#include +#include + +// ---------------------------------------------------------------------------- +// KDE Includes + +#include +#include +#include +#include +#include +#include +#include + +// ---------------------------------------------------------------------------- +// Project Includes + +#include "appinterface.h" +#include "viewinterface.h" +#include "mymoneyfile.h" +#include "mymoneyexception.h" +#include "mymoneystoragebin.h" +#include "mymoneystoragexml.h" +#include "mymoneystorageanon.h" +#include "icons.h" +#include "kmymoneysettings.h" +#include "kmymoneyutils.h" +#include "kgpgfile.h" +#include "kgpgkeyselectiondlg.h" + +using namespace Icons; + +static constexpr KCompressionDevice::CompressionType const& COMPRESSION_TYPE = KCompressionDevice::GZip; +static constexpr char recoveryKeyId[] = "0xD2B08440"; +static constexpr char recoveryKeyId2[] = "59B0F826D2B08440"; + +XMLStorage::XMLStorage(QObject *parent, const QVariantList &args) : + KMyMoneyPlugin::Plugin(parent, "xmlstorage"/*must be the same as X-KDE-PluginInfo-Name*/) +{ + Q_UNUSED(args) + setComponentName("xmlstorage", i18n("XML storage")); + setXMLFile("xmlstorage.rc"); + createActions(); + // For information, announce that we have been loaded. + qDebug("Plugins: xmlstorage loaded"); +} + +XMLStorage::~XMLStorage() +{ + qDebug("Plugins: xmlstorage unloaded"); +} + +bool XMLStorage::open(MyMoneyStorageMgr *storage, const QUrl &url) +{ + if (!url.isValid()) + throw MYMONEYEXCEPTION(QString::fromLatin1("Invalid URL %1").arg(qPrintable(url.url()))); + + QString fileName; + auto downloadedFile = false; + if (url.isLocalFile()) { + fileName = url.toLocalFile(); + } else { + fileName = KMyMoneyUtils::downloadFile(url); + downloadedFile = true; + } + + if (!KMyMoneyUtils::fileExists(QUrl::fromLocalFile(fileName))) + throw MYMONEYEXCEPTION(QString::fromLatin1("Error opening the file.\n" + "Requested file: '%1'.\n" + "Downloaded file: '%2'").arg(qPrintable(url.url()), fileName)); + + + QFile file(fileName); + if (!file.open(QIODevice::ReadOnly)) + throw MYMONEYEXCEPTION(QString::fromLatin1("Cannot read the file: %1").arg(fileName)); + + QByteArray qbaFileHeader(2, '\0'); + const auto sFileToShort = QString::fromLatin1("File %1 is too short.").arg(fileName); + if (file.read(qbaFileHeader.data(), 2) != 2) + throw MYMONEYEXCEPTION(sFileToShort); + + file.close(); + + // There's a problem with the KFilterDev and KGPGFile classes: + // One supports the at(n) member but not ungetch() together with + // read() and the other does not provide an at(n) method but + // supports read() that considers the ungetch() buffer. QFile + // supports everything so this is not a problem. We solve the problem + // for now by keeping track of which method can be used. + auto haveAt = true; + auto isEncrypted = false; + + QIODevice* qfile = nullptr; + QString sFileHeader(qbaFileHeader); + if (sFileHeader == QString("\037\213")) { // gzipped? + qfile = new KCompressionDevice(fileName, COMPRESSION_TYPE); + } else if (sFileHeader == QString("--") || // PGP ASCII armored? + sFileHeader == QString("\205\001") || // PGP binary? + sFileHeader == QString("\205\002")) { // PGP binary? + if (KGPGFile::GPGAvailable()) { + qfile = new KGPGFile(fileName); + haveAt = false; + isEncrypted = true; + } else { + throw MYMONEYEXCEPTION(QString::fromLatin1("GPG is not available for decryption of file %1").arg(fileName)); + } + } else { + // we can't use file directly, as we delete qfile later on + qfile = new QFile(file.fileName()); + } + + if (!qfile->open(QIODevice::ReadOnly)) { + delete qfile; + throw MYMONEYEXCEPTION(QString::fromLatin1("Cannot read the file: %1").arg(fileName)); + } + + qbaFileHeader.resize(8); + if (qfile->read(qbaFileHeader.data(), 8) != 8) + throw MYMONEYEXCEPTION(sFileToShort); + + if (haveAt) + qfile->seek(0); + else + ungetString(qfile, qbaFileHeader.data(), 8); + + // Ok, we got the first block of 8 bytes. Read in the two + // unsigned long int's by preserving endianess. This is + // achieved by reading them through a QDataStream object + qint32 magic0, magic1; + QDataStream s(&qbaFileHeader, QIODevice::ReadOnly); + s >> magic0; + s >> magic1; + + // If both magic numbers match (we actually read in the + // text 'KMyMoney' then we assume a binary file and + // construct a reader for it. Otherwise, we construct + // an XML reader object. + // + // The expression magic0 < 30 is only used to create + // a binary reader if we assume an old binary file. This + // should be removed at some point. An alternative is to + // check the beginning of the file against an pattern + // of the XML file (e.g. '?File %1 contains the old binary format used by KMyMoney. Please use an older version of KMyMoney (0.8.x) that still supports this format to convert it to the new XML based format.").arg(fileName)); + } + + // Scan the first 70 bytes to see if we find something + // we know. For now, we support our own XML format and + // GNUCash XML format. If the file is smaller, then it + // contains no valid data and we reject it anyway. + qbaFileHeader.resize(70); + if (qfile->read(qbaFileHeader.data(), 70) != 70) + throw MYMONEYEXCEPTION(sFileToShort); + + if (haveAt) + qfile->seek(0); + else + ungetString(qfile, qbaFileHeader.data(), 70); + + IMyMoneyOperationsFormat* pReader = nullptr; + QRegExp kmyexp(""); + QByteArray txt(qbaFileHeader, 70); + if (kmyexp.indexIn(txt) != -1) { + pReader = new MyMoneyStorageXML; + } else { + throw MYMONEYEXCEPTION(QString::fromLatin1("%1").arg(i18n("File %1 contains an unknown file format.", fileName))); + } + + // disconnect the current storga manager from the engine + MyMoneyFile::instance()->detachStorage(); + + // attach the storage before reading the file, since the online + // onlineJobAdministration object queries the engine during + // loading. + MyMoneyFile::instance()->attachStorage(storage); + + pReader->setProgressCallback(appInterface()->progressCallback()); + pReader->readFile(qfile, storage); + pReader->setProgressCallback(0); + delete pReader; + + qfile->close(); + delete qfile; + + // if a temporary file was downloaded, then it will be removed + // with the next call. Otherwise, it stays untouched on the local + // filesystem. + if (downloadedFile) + QFile::remove(fileName); + + // encapsulate transactions to the engine to be able to commit/rollback + MyMoneyFileTransaction ft; + // make sure we setup the encryption key correctly + if (isEncrypted && MyMoneyFile::instance()->value("kmm-encryption-key").isEmpty()) + MyMoneyFile::instance()->setValue("kmm-encryption-key", KMyMoneySettings::gpgRecipientList().join(",")); + ft.commit(); + return true; +} + +bool XMLStorage::save(const QUrl &url) +{ + QString filename = url.path(); + + if (!appInterface()->fileOpen()) { + KMessageBox::error(nullptr, i18n("Tried to access a file when it has not been opened")); + return false; + } + + std::unique_ptr storageWriter; + + // If this file ends in ".ANON.XML" then this should be written using the + // anonymous writer. + bool plaintext = filename.right(4).toLower() == ".xml"; + if (filename.right(9).toLower() == ".anon.xml") + storageWriter = std::make_unique(); + else + storageWriter = std::make_unique(); + + QString keyList; + if (!appInterface()->filenameURL().isEmpty()) + keyList = MyMoneyFile::instance()->value("kmm-encryption-key"); + else + keyList = m_encryptionKeys; + + // actually, url should be the parameter to this function + // but for now, this would involve too many changes + auto rc = true; + try { + if (! url.isValid()) { + throw MYMONEYEXCEPTION(QString::fromLatin1("Malformed URL '%1'").arg(url.url())); + } + + if (url.isLocalFile()) { + filename = url.toLocalFile(); + try { + const unsigned int nbak = KMyMoneySettings::autoBackupCopies(); + if (nbak) { + KBackup::numberedBackupFile(filename, QString(), QStringLiteral("~"), nbak); + } + saveToLocalFile(filename, storageWriter.get(), plaintext, keyList); + } catch (const MyMoneyException &) { + throw MYMONEYEXCEPTION(QString::fromLatin1("Unable to write changes to '%1'").arg(filename)); + } + } else { + + QTemporaryFile tmpfile; + tmpfile.open(); // to obtain the name + tmpfile.close(); + saveToLocalFile(tmpfile.fileName(), storageWriter.get(), plaintext, keyList); + + Q_CONSTEXPR int permission = -1; + QFile file(tmpfile.fileName()); + file.open(QIODevice::ReadOnly); + KIO::StoredTransferJob *putjob = KIO::storedPut(file.readAll(), url, permission, KIO::JobFlag::Overwrite); + if (!putjob->exec()) { + throw MYMONEYEXCEPTION(QString::fromLatin1("Unable to upload to '%1'.
%2").arg(url.toDisplayString(), putjob->errorString())); + } + file.close(); + } + } catch (const MyMoneyException &e) { + KMessageBox::error(nullptr, QString::fromLatin1(e.what())); + MyMoneyFile::instance()->setDirty(); + rc = false; + } + return rc; +} + +QString XMLStorage::formatName() const +{ + return QStringLiteral("XML"); +} + +QString XMLStorage::fileExtension() const +{ + return QString(); +} + +void XMLStorage::createActions() +{ + m_saveAsXMLaction = actionCollection()->addAction("saveas_xml"); + m_saveAsXMLaction->setText(i18n("Save as XML...")); + m_saveAsXMLaction->setIcon(Icons::get(Icon::FileArchiver)); + connect(m_saveAsXMLaction, &QAction::triggered, this, &XMLStorage::slotSaveAsXML); +} + +void XMLStorage::ungetString(QIODevice *qfile, char *buf, int len) +{ + buf = &buf[len-1]; + while (len--) { + qfile->ungetChar(*buf--); + } +} + +void XMLStorage::saveToLocalFile(const QString& localFile, IMyMoneyOperationsFormat* pWriter, bool plaintext, const QString& keyList) +{ + // Check GPG encryption + bool encryptFile = true; + bool encryptRecover = false; + if (!keyList.isEmpty()) { + if (!KGPGFile::GPGAvailable()) { + KMessageBox::sorry(nullptr, i18n("GPG does not seem to be installed on your system. Please make sure that GPG can be found using the standard search path. This time, encryption is disabled."), i18n("GPG not found")); + encryptFile = false; + } else { + if (KMyMoneySettings::encryptRecover()) { + encryptRecover = true; + if (!KGPGFile::keyAvailable(QString(recoveryKeyId))) { + KMessageBox::sorry(nullptr, i18n("

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

%1

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

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

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

%1.

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

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

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

"); + if (KMessageBox::questionYesNo(nullptr, msg, i18n("Store GPG encrypted"), KStandardGuiItem::yes(), KStandardGuiItem::no(), "StoreEncrypted") == KMessageBox::No) { + encryptFile = false; + } + } + } + } + + + // Create a temporary file if needed + QString writeFile = localFile; + QTemporaryFile tmpFile; + if (QFile::exists(localFile)) { + tmpFile.open(); + writeFile = tmpFile.fileName(); + tmpFile.close(); + } + + /** + * @brief Automatically restore settings when scope is left + */ + struct restorePreviousSettingsHelper { + restorePreviousSettingsHelper() + : m_signalsWereBlocked{MyMoneyFile::instance()->signalsBlocked()} + { + MyMoneyFile::instance()->blockSignals(true); + } + + ~restorePreviousSettingsHelper() + { + MyMoneyFile::instance()->blockSignals(m_signalsWereBlocked); + } + const bool m_signalsWereBlocked; + } restoreHelper; + + MyMoneyFileTransaction ft; + MyMoneyFile::instance()->deletePair("kmm-encryption-key"); + std::unique_ptr device; + + if (!keyList.isEmpty() && encryptFile && !plaintext) { + std::unique_ptr kgpg = std::unique_ptr(new KGPGFile{writeFile}); + if (kgpg) { + for(const QString& key: keyList.split(',', QString::SkipEmptyParts)) { + kgpg->addRecipient(key.toLatin1()); + } + + if (encryptRecover) { + kgpg->addRecipient(recoveryKeyId); + } + MyMoneyFile::instance()->setValue("kmm-encryption-key", keyList); + device = std::unique_ptr(kgpg.release()); + } + } else { + QFile *file = new QFile(writeFile); + // The second parameter of KCompressionDevice means that KCompressionDevice will delete the QFile object + device = std::unique_ptr(new KCompressionDevice{file, true, (plaintext) ? KCompressionDevice::None : COMPRESSION_TYPE}); + } + + ft.commit(); + + if (!device || !device->open(QIODevice::WriteOnly)) { + throw MYMONEYEXCEPTION(QString::fromLatin1("Unable to open file '%1' for writing.").arg(localFile)); + } + + pWriter->setProgressCallback(appInterface()->progressCallback()); + pWriter->writeFile(device.get(), MyMoneyFile::instance()->storage()); + device->close(); + + // Check for errors if possible, only possible for KGPGFile + QFileDevice *fileDevice = qobject_cast(device.get()); + if (fileDevice && fileDevice->error() != QFileDevice::NoError) { + throw MYMONEYEXCEPTION(QString::fromLatin1("Failure while writing to '%1'").arg(localFile)); + } + + if (writeFile != localFile) { + // This simple comparison is possible because the strings are equal if no temporary file was created. + // If a temporary file was created, it is made in a way that the name is definitely different. So no + // symlinks etc. have to be evaluated. + if (!QFile::remove(localFile) || !QFile::rename(writeFile, localFile)) + throw MYMONEYEXCEPTION(QString::fromLatin1("Failure while writing to '%1'").arg(localFile)); + } + QFile::setPermissions(localFile, QFileDevice::ReadUser | QFileDevice::WriteUser); + pWriter->setProgressCallback(0); +} + +void XMLStorage::slotSaveAsXML() +{ + bool rc = false; + QStringList m_additionalGpgKeys; + m_encryptionKeys.clear(); + + QString selectedKeyName; + if (KGPGFile::GPGAvailable() && KMyMoneySettings::writeDataEncrypted()) { + // fill the secret key list and combo box + QStringList keyList; + KGPGFile::secretKeyList(keyList); + + QPointer dlg = new KGpgKeySelectionDlg(nullptr); + dlg->setSecretKeys(keyList, KMyMoneySettings::gpgRecipient()); + dlg->setAdditionalKeys(KMyMoneySettings::gpgRecipientList()); + rc = dlg->exec(); + if ((rc == QDialog::Accepted) && (dlg != 0)) { + m_additionalGpgKeys = dlg->additionalKeys(); + selectedKeyName = dlg->secretKey(); + } + delete dlg; + if (rc != QDialog::Accepted) { + return; + } + } + + QString prevDir; // don't prompt file name if not a native file + if (appInterface()->isNativeFile()) + prevDir = appInterface()->readLastUsedDir(); + + QPointer dlg = + new QFileDialog(nullptr, i18n("Save As"), prevDir, + QString(QLatin1String("%2 (%1);;")).arg(QStringLiteral("*.kmy")).arg(i18nc("KMyMoney (Filefilter)", "KMyMoney files")) + + QString(QLatin1String("%2 (%1);;")).arg(QStringLiteral("*.xml")).arg(i18nc("XML (Filefilter)", "XML files")) + + QString(QLatin1String("%2 (%1);;")).arg(QStringLiteral("*.anon.xml")).arg(i18nc("Anonymous (Filefilter)", "Anonymous files")) + + QString(QLatin1String("%2 (%1);;")).arg(QStringLiteral("*")).arg(i18nc("All files (Filefilter)", "All files"))); + dlg->setAcceptMode(QFileDialog::AcceptSave); + + if (dlg->exec() == QDialog::Accepted && dlg != 0) { + QUrl newURL = dlg->selectedUrls().first(); + if (!newURL.fileName().isEmpty()) { + appInterface()->consistencyCheck(false); + QString newName = newURL.toDisplayString(QUrl::PreferLocalFile); + + // append extension if not present + if (!newName.endsWith(QLatin1String(".kmy"), Qt::CaseInsensitive) && + !newName.endsWith(QLatin1String(".xml"), Qt::CaseInsensitive)) + newName.append(QLatin1String(".kmy")); + newURL = QUrl::fromUserInput(newName); + appInterface()->addToRecentFiles(newURL); + + // If this is the anonymous file export, just save it, don't actually take the + // name, or remember it! Don't even try to encrypt it + if (newName.endsWith(QLatin1String(".anon.xml"), Qt::CaseInsensitive)) + rc = save(newURL); + else { + appInterface()->writeFilenameURL(newURL); + QRegExp keyExp(".* \\((.*)\\)"); + if (keyExp.indexIn(selectedKeyName) != -1) { + m_encryptionKeys = keyExp.cap(1); + if (!m_additionalGpgKeys.isEmpty()) { + if (!m_encryptionKeys.isEmpty()) + m_encryptionKeys.append(QLatin1Char(',')); + m_encryptionKeys.append(m_additionalGpgKeys.join(QLatin1Char(','))); + } + } + rc = save(newURL); + //write the directory used for this file as the default one for next time. + appInterface()->writeLastUsedDir(newURL.toDisplayString(QUrl::RemoveFilename | QUrl::PreferLocalFile | QUrl::StripTrailingSlash)); + appInterface()->writeLastUsedFile(newName); + } + appInterface()->autosaveTimer()->stop(); + } + } + (*appInterface()->progressCallback())(0,0, i18nc("Application is ready to use", "Ready.")); + delete dlg; + appInterface()->updateCaption(); +} + +K_PLUGIN_FACTORY_WITH_JSON(XMLStorageFactory, "xmlstorage.json", registerPlugin();) + +#include "xmlstorage.moc" diff --git a/kmymoney/plugins/xml/xmlstorage.h b/kmymoney/plugins/xml/xmlstorage.h new file mode 100644 index 000000000..d595c31e5 --- /dev/null +++ b/kmymoney/plugins/xml/xmlstorage.h @@ -0,0 +1,79 @@ +/* + * 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 XMLSTORAGE_H +#define XMLSTORAGE_H + +// ---------------------------------------------------------------------------- +// KDE Includes + +// ---------------------------------------------------------------------------- +// QT Includes + +// Project Includes + +#include "kmymoneyplugin.h" + +class QIODevice; + +class MyMoneyStorageMgr; + +class XMLStorage : public KMyMoneyPlugin::Plugin, public KMyMoneyPlugin::StoragePlugin +{ + Q_OBJECT + Q_INTERFACES(KMyMoneyPlugin::StoragePlugin) + +public: + explicit XMLStorage(QObject *parent, const QVariantList &args); + ~XMLStorage() override; + + QAction *m_saveAsXMLaction; + + bool open(MyMoneyStorageMgr *storage, const QUrl &url) override; + bool save(const QUrl &url) override; + QString formatName() const override; + QString fileExtension() const override; + +private: + void createActions(); + void ungetString(QIODevice *qfile, char *buf, int len); + + /** + * This method is used by saveFile() to store the data + * either directly in the destination file if it is on + * the local file system or in a temporary file when + * the final destination is reached over a network + * protocol (e.g. FTP) + * + * @param localFile the name of the local file + * @param writer pointer to the formatter + * @param plaintext whether to override any compression & encryption settings + * @param keyList QString containing a comma separated list of keys to be used for encryption + * If @p keyList is empty, the file will be saved unencrypted + * + * @note This method will close the file when it is written. + */ + void saveToLocalFile(const QString& localFile, IMyMoneyOperationsFormat* pWriter, bool plaintext, const QString& keyList); + + QString m_encryptionKeys; +private Q_SLOTS: + void slotSaveAsXML(); + + +}; + +#endif diff --git a/kmymoney/plugins/xml/xmlstorage.json.in b/kmymoney/plugins/xml/xmlstorage.json.in new file mode 100644 index 000000000..0925b92ab --- /dev/null +++ b/kmymoney/plugins/xml/xmlstorage.json.in @@ -0,0 +1,21 @@ +{ + "KPlugin": { + "Authors": [ + { + "Email": "ktambascio@users.sourceforge.net,tbaumgart@kde.org,acejones@users.sourceforge.net,lukasz.wojnilowicz@gmail.com", + "Name": "Kevin Tambascio,Thomas Baumgart,Ace Jones,Łukasz Wojniłowicz" + } + ], + "Description": "Adds XML storage support to KMyMoney", + "EnabledByDefault": true, + "Icon": "server-database", + "Id": "xmlstorage", + "License": "GPL", + "Name": "XML Storage", + "ServiceTypes": [ + "KMyMoney/Plugin" + ], + "Version": "@PROJECT_VERSION@@PROJECT_VERSION_SUFFIX@", + "Website": "https://kmymoney.org/plugins.html" + } +} diff --git a/kmymoney/plugins/xml/xmlstorage.rc b/kmymoney/plugins/xml/xmlstorage.rc new file mode 100644 index 000000000..567b1ee86 --- /dev/null +++ b/kmymoney/plugins/xml/xmlstorage.rc @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/kmymoney/reports/tests/pivottable-test.cpp b/kmymoney/reports/tests/pivottable-test.cpp index 7e8caeabc..5d8f55dcf 100644 --- a/kmymoney/reports/tests/pivottable-test.cpp +++ b/kmymoney/reports/tests/pivottable-test.cpp @@ -1,1077 +1,1076 @@ /*************************************************************************** pivottabletest.cpp ------------------- copyright : (C) 2002-2005 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 "pivottable-test.h" #include #include #include #include // DOH, mmreport.h uses this without including it!! #include "mymoneyinstitution.h" #include "mymoneyaccount.h" #include "mymoneysecurity.h" #include "mymoneyprice.h" #include "mymoneyreport.h" #include "mymoneystatement.h" #include "mymoneysplit.h" #include "mymoneypayee.h" #include "mymoneyexception.h" #include "mymoneystoragedump.h" -#include "mymoneystoragexml.h" #include "mymoneyenums.h" #include "pivottable.h" #include "reportstestcommon.h" #include "kmymoneysettings.h" using namespace reports; using namespace test; QTEST_GUILESS_MAIN(PivotTableTest) void PivotTableTest::setup() { } void PivotTableTest::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("Test Payee"); file->addPayee(payeeTest); MyMoneyPayee payeeTest2("Thomas Baumgart"); file->addPayee(payeeTest2); acAsset = (MyMoneyFile::instance()->asset().id()); acLiability = (MyMoneyFile::instance()->liability().id()); acExpense = (MyMoneyFile::instance()->expense().id()); acIncome = (MyMoneyFile::instance()->income().id()); acChecking = makeAccount(QString("Checking Account"), eMyMoney::Account::Type::Checkings, moCheckingOpen, QDate(2004, 5, 15), acAsset); acCredit = makeAccount(QString("Credit Card"), eMyMoney::Account::Type::CreditCard, moCreditOpen, QDate(2004, 7, 15), acLiability); acSolo = makeAccount(QString("Solo"), eMyMoney::Account::Type::Expense, MyMoneyMoney(), QDate(2004, 1, 11), acExpense); acParent = makeAccount(QString("Parent"), eMyMoney::Account::Type::Expense, MyMoneyMoney(), QDate(2004, 1, 11), acExpense); acChild = makeAccount(QString("Child"), eMyMoney::Account::Type::Expense, MyMoneyMoney(), QDate(2004, 2, 11), acParent); acForeign = makeAccount(QString("Foreign"), eMyMoney::Account::Type::Expense, MyMoneyMoney(), QDate(2004, 1, 11), acExpense); acSecondChild = makeAccount(QString("Second Child"), eMyMoney::Account::Type::Expense, MyMoneyMoney(), QDate(2004, 2, 11), acParent); acGrandChild1 = makeAccount(QString("Grand Child 1"), eMyMoney::Account::Type::Expense, MyMoneyMoney(), QDate(2004, 2, 11), acChild); acGrandChild2 = makeAccount(QString("Grand Child 2"), eMyMoney::Account::Type::Expense, MyMoneyMoney(), QDate(2004, 2, 11), acChild); MyMoneyInstitution i("Bank of the World", "", "", "", "", "", ""); file->addInstitution(i); inBank = i.id(); ft.commit(); } void PivotTableTest::cleanup() { file->detachStorage(storage); delete storage; } void PivotTableTest::testNetWorthSingle() { try { MyMoneyReport filter; filter.setRowType(MyMoneyReport::eAssetLiability); filter.setDateFilter(QDate(2004, 1, 1), QDate(2004, 7, 1).addDays(-1)); XMLandback(filter); PivotTable networth_f(filter); writeTabletoCSV(networth_f); QVERIFY(networth_f.m_grid["Asset"]["Checking Account"][ReportAccount(acChecking)][eActual][5] == moCheckingOpen); QVERIFY(networth_f.m_grid["Asset"]["Checking Account"][ReportAccount(acChecking)][eActual][6] == moCheckingOpen); QVERIFY(networth_f.m_grid["Asset"]["Checking Account"].m_total[eActual][5] == moCheckingOpen); QVERIFY(networth_f.m_grid.m_total[eActual][0] == moZero); QVERIFY(networth_f.m_grid.m_total[eActual][4] == moZero); QVERIFY(networth_f.m_grid.m_total[eActual][5] == moCheckingOpen); QVERIFY(networth_f.m_grid.m_total[eActual][6] == moCheckingOpen); } catch (const MyMoneyException &e) { QFAIL(e.what()); } } void PivotTableTest::testNetWorthOfsetting() { // Test the net worth report to make sure it picks up the opening balance for two // accounts opened during the period of the report, one asset & one liability. Test // that it calculates the totals correctly. MyMoneyReport filter; filter.setRowType(MyMoneyReport::eAssetLiability); filter.setDateFilter(QDate(2004, 1, 1), QDate(2005, 1, 1).addDays(-1)); XMLandback(filter); PivotTable networth_f(filter); QVERIFY(networth_f.m_grid["Liability"]["Credit Card"][ReportAccount(acCredit)][eActual][7] == -moCreditOpen); QVERIFY(networth_f.m_grid.m_total[eActual][0] == moZero); QVERIFY(networth_f.m_grid.m_total[eActual][12] == moCheckingOpen + moCreditOpen); } void PivotTableTest::testNetWorthOpeningPrior() { // Test the net worth report to make sure it's picking up opening balances PRIOR to // the period of the report. MyMoneyReport filter; filter.setRowType(MyMoneyReport::eAssetLiability); filter.setDateFilter(QDate(2005, 8, 1), QDate(2005, 12, 31)); filter.setName("Net Worth Opening Prior 1"); XMLandback(filter); PivotTable networth_f(filter); writeTabletoCSV(networth_f); QVERIFY(networth_f.m_grid["Liability"]["Credit Card"].m_total[eActual][0] == -moCreditOpen); QVERIFY(networth_f.m_grid["Asset"]["Checking Account"].m_total[eActual][0] == moCheckingOpen); QVERIFY(networth_f.m_grid.m_total[eActual][0] == moCheckingOpen + moCreditOpen); QVERIFY(networth_f.m_grid.m_total[eActual][1] == moCheckingOpen + moCreditOpen); // Test the net worth report to make sure that transactions prior to the report // period are included in the opening balance TransactionHelper t1(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent1, acCredit, acParent); TransactionHelper t2(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent2, acCredit, acParent); TransactionHelper t3(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moChild, acChecking, acChild); filter.setName("Net Worth Opening Prior 2"); PivotTable networth_f2(filter); writeTabletoCSV(networth_f2); MyMoneyMoney m1 = (networth_f2.m_grid["Liability"]["Credit Card"].m_total[eActual][1]); MyMoneyMoney m2 = (-moCreditOpen + moParent); QVERIFY((networth_f2.m_grid["Liability"]["Credit Card"].m_total[eActual][1]) == (-moCreditOpen + moParent)); QVERIFY(networth_f2.m_grid["Asset"]["Checking Account"].m_total[eActual][1] == moCheckingOpen - moChild); QVERIFY(networth_f2.m_grid.m_total[eActual][1] == moCheckingOpen + moCreditOpen - moChild - moParent); } void PivotTableTest::testNetWorthDateFilter() { // Test a net worth report whose period is prior to the time any accounts are open, // so the report should be zero. MyMoneyReport filter; filter.setRowType(MyMoneyReport::eAssetLiability); filter.setDateFilter(QDate(2004, 1, 1), QDate(2004, 2, 1).addDays(-1)); XMLandback(filter); PivotTable networth_f(filter); QVERIFY(networth_f.m_grid.m_total[eActual][1] == moZero); } void PivotTableTest::testNetWorthOpening() { MyMoneyMoney openingBalance(12000000); auto acBasicAccount = makeAccount(QString("Basic Account"), eMyMoney::Account::Type::Checkings, openingBalance, QDate(2016, 1, 1), acAsset); auto ctBasicIncome = makeAccount(QString("Basic Income"), eMyMoney::Account::Type::Income, MyMoneyMoney(), QDate(2016, 1, 1), acIncome); auto ctBasicExpense = makeAccount(QString("Basic Expense"), eMyMoney::Account::Type::Expense, MyMoneyMoney(), QDate(2016, 1, 1), acExpense); TransactionHelper t1(QDate(2016, 7, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit), MyMoneyMoney(-6200000), acBasicAccount, ctBasicIncome); TransactionHelper t2(QDate(2016, 8, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit), MyMoneyMoney(-200000), acBasicAccount, ctBasicIncome); TransactionHelper t3(QDate(2016, 9, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit), MyMoneyMoney(-200000), acBasicAccount, ctBasicIncome); TransactionHelper t4(QDate(2016, 10, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit), MyMoneyMoney(-100000), acBasicAccount, ctBasicIncome); TransactionHelper t5(QDate(2016, 11, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit), MyMoneyMoney(-100000), acBasicAccount, ctBasicIncome); TransactionHelper t6(QDate(2016, 12, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), MyMoneyMoney(100000), acBasicAccount, ctBasicExpense); TransactionHelper t7(QDate(2017, 1, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit), MyMoneyMoney(-100000), acBasicAccount, ctBasicIncome); TransactionHelper t8(QDate(2017, 2, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit), MyMoneyMoney(-100000), acBasicAccount, ctBasicIncome); TransactionHelper t9(QDate(2017, 3, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit), MyMoneyMoney(-100000), acBasicAccount, ctBasicIncome); TransactionHelper t10(QDate(2017, 4, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit), MyMoneyMoney(-100000), acBasicAccount, ctBasicIncome); TransactionHelper t11(QDate(2017, 5, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), MyMoneyMoney(4500000), acBasicAccount, ctBasicExpense); TransactionHelper t12(QDate(2017, 6, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit), MyMoneyMoney(-100000), acBasicAccount, ctBasicIncome); TransactionHelper t13(QDate(2017, 7, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit), MyMoneyMoney(-100000), acBasicAccount, ctBasicIncome); MyMoneyReport filter; filter.setRowType(MyMoneyReport::eAssetLiability); filter.setDateFilter(QDate(2016, 1, 1), QDate(2017, 12, 31)); filter.addAccount(acBasicAccount); XMLandback(filter); PivotTable nt_opening1(filter); writeTabletoCSV(nt_opening1, "networth-opening-1.csv"); QVERIFY(nt_opening1.m_grid["Asset"]["Basic Account"][ReportAccount(acBasicAccount)][eActual][0] == MyMoneyMoney()); // opening value on 1st Jan 2016 is 12000000, but before that i.e. 31st Dec 2015 opening value is 0 for (auto i = 1; i <= 6; ++i) QVERIFY(nt_opening1.m_grid["Asset"]["Basic Account"][ReportAccount(acBasicAccount)][eActual][i] == openingBalance); QVERIFY(nt_opening1.m_grid["Asset"]["Basic Account"][ReportAccount(acBasicAccount)][eActual][7] == openingBalance + MyMoneyMoney(6200000)); QVERIFY(nt_opening1.m_grid["Asset"]["Basic Account"][ReportAccount(acBasicAccount)][eActual][12] == MyMoneyMoney(18700000)); // value after t6 transaction filter.setDateFilter(QDate(2017, 1, 1), QDate(2017, 12, 31)); XMLandback(filter); PivotTable nt_opening2(filter); writeTabletoCSV(nt_opening2, "networth-opening-2.csv"); QVERIFY(nt_opening2.m_grid["Asset"]["Basic Account"][ReportAccount(acBasicAccount)][eActual][0] == MyMoneyMoney(18700000)); // opening value is equall to the value after t6 transaction QVERIFY(nt_opening2.m_grid["Asset"]["Basic Account"][ReportAccount(acBasicAccount)][eActual][12] == MyMoneyMoney(14800000)); } void PivotTableTest::testSpendingEmpty() { // test a spending report with no entries MyMoneyReport filter; filter.setRowType(MyMoneyReport::eExpenseIncome); XMLandback(filter); PivotTable spending_f1(filter); QVERIFY(spending_f1.m_grid.m_total[eActual].m_total == moZero); filter.setDateFilter(QDate(2004, 9, 1), QDate(2005, 1, 1).addDays(-1)); PivotTable spending_f2(filter); QVERIFY(spending_f2.m_grid.m_total[eActual].m_total == moZero); } void PivotTableTest::testSingleTransaction() { // Test a single transaction TransactionHelper t(QDate(2004, 10, 31), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moSolo, acChecking, acSolo); MyMoneyReport filter; filter.setRowType(MyMoneyReport::eExpenseIncome); filter.setDateFilter(QDate(2004, 9, 1), QDate(2005, 1, 1).addDays(-1)); filter.setName("Spending with Single Transaction.html"); XMLandback(filter); PivotTable spending_f(filter); writeTabletoHTML(spending_f, "Spending with Single Transaction.html"); QVERIFY(spending_f.m_grid["Expense"]["Solo"][ReportAccount(acSolo)][eActual][1] == moSolo); QVERIFY(spending_f.m_grid["Expense"]["Solo"].m_total[eActual][1] == moSolo); QVERIFY(spending_f.m_grid["Expense"]["Solo"].m_total[eActual][0] == moZero); QVERIFY(spending_f.m_grid.m_total[eActual][1] == (-moSolo)); QVERIFY(spending_f.m_grid.m_total[eActual].m_total == (-moSolo)); filter.clearTransactionFilter(); filter.setRowType(MyMoneyReport::eAssetLiability); filter.setDateFilter(QDate(2004, 9, 1), QDate(2005, 1, 1).addDays(-1)); XMLandback(filter); PivotTable networth_f(filter); QVERIFY(networth_f.m_grid["Asset"]["Checking Account"].m_total[eActual][2] == (moCheckingOpen - moSolo)); } void PivotTableTest::testSubAccount() { // Test a sub-account with a value, under an account with a value TransactionHelper t1(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent1, acCredit, acParent); TransactionHelper t2(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent2, acCredit, acParent); TransactionHelper t3(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moChild, acCredit, acChild); MyMoneyReport filter; filter.setRowType(MyMoneyReport::eExpenseIncome); filter.setDateFilter(QDate(2004, 9, 1), QDate(2005, 1, 1).addDays(-1)); filter.setDetailLevel(MyMoneyReport::eDetailAll); filter.setName("Spending with Sub-Account"); XMLandback(filter); PivotTable spending_f(filter); writeTabletoHTML(spending_f, "Spending with Sub-Account.html"); QVERIFY(spending_f.m_grid["Expense"]["Parent"][ReportAccount(acParent)][eActual][2] == moParent); QVERIFY(spending_f.m_grid["Expense"]["Parent"][ReportAccount(acChild)][eActual][2] == moChild); QVERIFY(spending_f.m_grid["Expense"]["Parent"].m_total[eActual][2] == moParent + moChild); QVERIFY(spending_f.m_grid["Expense"]["Parent"].m_total[eActual][1] == moZero); QVERIFY(spending_f.m_grid["Expense"]["Parent"].m_total[eActual].m_total == moParent + moChild); QVERIFY(spending_f.m_grid.m_total[eActual][2] == (-moParent - moChild)); QVERIFY(spending_f.m_grid.m_total[eActual].m_total == (-moParent - moChild)); filter.clearTransactionFilter(); filter.setRowType(MyMoneyReport::eAssetLiability); filter.setDateFilter(QDate(2004, 9, 1), QDate(2005, 1, 1).addDays(-1)); filter.setName("Net Worth with Sub-Account"); XMLandback(filter); PivotTable networth_f(filter); writeTabletoHTML(networth_f, "Net Worth with Sub-Account.html"); QVERIFY(networth_f.m_grid["Liability"]["Credit Card"].m_total[eActual][3] == moParent + moChild - moCreditOpen); QVERIFY(networth_f.m_grid.m_total[eActual][4] == -moParent - moChild + moCreditOpen + moCheckingOpen); } void PivotTableTest::testFilterIEvsIE() { // Test that removing an income/spending account will remove the entry from an income/spending report TransactionHelper t1(QDate(2004, 10, 31), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moSolo, acChecking, acSolo); TransactionHelper t2(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent1, acCredit, acParent); TransactionHelper t3(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moChild, acCredit, acChild); MyMoneyReport filter; filter.setRowType(MyMoneyReport::eExpenseIncome); filter.setDateFilter(QDate(2004, 9, 1), QDate(2005, 1, 1).addDays(-1)); filter.addCategory(acChild); filter.addCategory(acSolo); XMLandback(filter); PivotTable spending_f(filter); QVERIFY(spending_f.m_grid["Expense"]["Parent"].m_total[eActual][2] == moChild); QVERIFY(spending_f.m_grid["Expense"].m_total[eActual][1] == moSolo); QVERIFY(spending_f.m_grid.m_total[eActual].m_total == -moSolo - moChild); } void PivotTableTest::testFilterALvsAL() { // Test that removing an asset/liability account will remove the entry from an asset/liability report TransactionHelper t1(QDate(2004, 10, 31), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moSolo, acChecking, acSolo); TransactionHelper t2(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent1, acCredit, acParent); TransactionHelper t3(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moChild, acCredit, acChild); MyMoneyReport filter; filter.setRowType(MyMoneyReport::eAssetLiability); filter.setDateFilter(QDate(2004, 9, 1), QDate(2005, 1, 1).addDays(-1)); filter.addAccount(acChecking); filter.addCategory(acChild); filter.addCategory(acSolo); XMLandback(filter); PivotTable networth_f(filter); QVERIFY(networth_f.m_grid.m_total[eActual][2] == -moSolo + moCheckingOpen); } void PivotTableTest::testFilterALvsIE() { // Test that removing an asset/liability account will remove the entry from an income/spending report TransactionHelper t1(QDate(2004, 10, 31), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moSolo, acChecking, acSolo); TransactionHelper t2(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent1, acCredit, acParent); TransactionHelper t3(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moChild, acCredit, acChild); MyMoneyReport filter; filter.setRowType(MyMoneyReport::eExpenseIncome); filter.setDateFilter(QDate(2004, 9, 1), QDate(2005, 1, 1).addDays(-1)); filter.addAccount(acChecking); QVERIFY(file->transactionList(filter).count() == 1); XMLandback(filter); PivotTable spending_f(filter); QVERIFY(spending_f.m_grid["Expense"].m_total[eActual][2] == moZero); QVERIFY(spending_f.m_grid["Expense"].m_total[eActual][1] == moSolo); QVERIFY(spending_f.m_grid.m_total[eActual].m_total == -moSolo); } void PivotTableTest::testFilterAllvsIE() { // Test that removing an asset/liability account AND an income/expense // category will remove the entry from an income/spending report TransactionHelper t1(QDate(2004, 10, 31), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moSolo, acChecking, acSolo); TransactionHelper t2(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent1, acCredit, acParent); TransactionHelper t3(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moChild, acCredit, acChild); MyMoneyReport filter; filter.setRowType(MyMoneyReport::eExpenseIncome); filter.setDateFilter(QDate(2004, 9, 1), QDate(2005, 1, 1).addDays(-1)); filter.addAccount(acCredit); filter.addCategory(acChild); PivotTable spending_f(filter); QVERIFY(spending_f.m_grid["Expense"].m_total[eActual][1] == moZero); QVERIFY(spending_f.m_grid["Expense"].m_total[eActual][2] == moChild); QVERIFY(spending_f.m_grid.m_total[eActual].m_total == -moChild); } void PivotTableTest::testFilterBasics() { // Test that the filters are operating the way that the reports expect them to TransactionHelper t1(QDate(2004, 10, 31), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moSolo, acChecking, acSolo); TransactionHelper t2(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent1, acCredit, acParent); TransactionHelper t3(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent2, acCredit, acParent); TransactionHelper t4(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moChild, acCredit, acChild); MyMoneyTransactionFilter filter; filter.clear(); filter.setDateFilter(QDate(2004, 9, 1), QDate(2005, 1, 1).addDays(-1)); filter.addCategory(acSolo); filter.setReportAllSplits(false); filter.setConsiderCategory(true); QVERIFY(file->transactionList(filter).count() == 1); filter.addCategory(acParent); QVERIFY(file->transactionList(filter).count() == 3); filter.addAccount(acChecking); QVERIFY(file->transactionList(filter).count() == 1); filter.clear(); filter.setDateFilter(QDate(2004, 9, 1), QDate(2005, 1, 1).addDays(-1)); filter.addCategory(acParent); filter.addAccount(acCredit); filter.setReportAllSplits(false); filter.setConsiderCategory(true); QVERIFY(file->transactionList(filter).count() == 2); } void PivotTableTest::testMultipleCurrencies() { MyMoneyMoney moCanOpening(0.0, 10); MyMoneyMoney moJpyOpening(0.0, 10); MyMoneyMoney moCanPrice(0.75, 100); MyMoneyMoney moJpyPrice(0.010, 1000); MyMoneyMoney moJpyPrice2(0.011, 1000); MyMoneyMoney moJpyPrice3(0.014, 1000); MyMoneyMoney moJpyPrice4(0.0395, 10000); MyMoneyMoney moCanTransaction(100.0, 10); MyMoneyMoney moJpyTransaction(100.0, 10); QString acCanChecking = makeAccount(QString("Canadian Checking"), eMyMoney::Account::Type::Checkings, moCanOpening, QDate(2003, 11, 15), acAsset, "CAD"); QString acJpyChecking = makeAccount(QString("Japanese Checking"), eMyMoney::Account::Type::Checkings, moJpyOpening, QDate(2003, 11, 15), acAsset, "JPY"); QString acCanCash = makeAccount(QString("Canadian"), eMyMoney::Account::Type::Expense, MyMoneyMoney(), QDate(2004, 2, 11), acForeign, "CAD"); QString acJpyCash = makeAccount(QString("Japanese"), eMyMoney::Account::Type::Expense, MyMoneyMoney(), QDate(2004, 2, 11), acForeign, "JPY"); makePrice("CAD", QDate(2004, 1, 1), MyMoneyMoney(moCanPrice)); makePrice("JPY", QDate(2004, 1, 1), MyMoneyMoney(moJpyPrice)); makePrice("JPY", QDate(2004, 5, 1), MyMoneyMoney(moJpyPrice2)); makePrice("JPY", QDate(2004, 6, 30), MyMoneyMoney(moJpyPrice3)); makePrice("JPY", QDate(2004, 7, 15), MyMoneyMoney(moJpyPrice4)); TransactionHelper t1(QDate(2004, 2, 20), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), MyMoneyMoney(moJpyTransaction), acJpyChecking, acJpyCash, "JPY"); TransactionHelper t2(QDate(2004, 3, 20), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), MyMoneyMoney(moJpyTransaction), acJpyChecking, acJpyCash, "JPY"); TransactionHelper t3(QDate(2004, 4, 20), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), MyMoneyMoney(moJpyTransaction), acJpyChecking, acJpyCash, "JPY"); TransactionHelper t4(QDate(2004, 2, 20), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), MyMoneyMoney(moCanTransaction), acCanChecking, acCanCash, "CAD"); TransactionHelper t5(QDate(2004, 3, 20), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), MyMoneyMoney(moCanTransaction), acCanChecking, acCanCash, "CAD"); TransactionHelper t6(QDate(2004, 4, 20), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), MyMoneyMoney(moCanTransaction), acCanChecking, acCanCash, "CAD"); #if 0 QFile g("multicurrencykmy.xml"); g.open(QIODevice::WriteOnly); MyMoneyStorageXML xml; IMyMoneyOperationsFormat& interface = xml; interface.writeFile(&g, dynamic_cast(MyMoneyFile::instance()->storage())); g.close(); #endif MyMoneyReport filter; filter.setRowType(MyMoneyReport::eExpenseIncome); filter.setDateFilter(QDate(2004, 1, 1), QDate(2005, 1, 1).addDays(-1)); filter.setDetailLevel(MyMoneyReport::eDetailAll); filter.setConvertCurrency(true); filter.setName("Multiple Currency Spending Rerport (with currency conversion)"); XMLandback(filter); PivotTable spending_f(filter); writeTabletoCSV(spending_f); // test single foreign currency QVERIFY(spending_f.m_grid["Expense"]["Foreign"][ReportAccount(acCanCash)][eActual][1] == (moCanTransaction*moCanPrice)); QVERIFY(spending_f.m_grid["Expense"]["Foreign"][ReportAccount(acCanCash)][eActual][2] == (moCanTransaction*moCanPrice)); QVERIFY(spending_f.m_grid["Expense"]["Foreign"][ReportAccount(acCanCash)][eActual][3] == (moCanTransaction*moCanPrice)); // test multiple foreign currencies under a common parent QVERIFY(spending_f.m_grid["Expense"]["Foreign"][ReportAccount(acJpyCash)][eActual][1] == (moJpyTransaction*moJpyPrice)); QVERIFY(spending_f.m_grid["Expense"]["Foreign"][ReportAccount(acJpyCash)][eActual][2] == (moJpyTransaction*moJpyPrice)); QVERIFY(spending_f.m_grid["Expense"]["Foreign"][ReportAccount(acJpyCash)][eActual][3] == (moJpyTransaction*moJpyPrice)); QVERIFY(spending_f.m_grid["Expense"]["Foreign"].m_total[eActual][1] == (moJpyTransaction*moJpyPrice + moCanTransaction*moCanPrice)); QVERIFY(spending_f.m_grid["Expense"]["Foreign"].m_total[eActual].m_total == (moJpyTransaction*moJpyPrice + moCanTransaction*moCanPrice + moJpyTransaction*moJpyPrice + moCanTransaction*moCanPrice + moJpyTransaction*moJpyPrice + moCanTransaction*moCanPrice)); // Test the report type where we DO NOT convert the currency filter.setConvertCurrency(false); filter.setDetailLevel(MyMoneyReport::eDetailAll); filter.setName("Multiple Currency Spending Report (WITHOUT currency conversion)"); XMLandback(filter); PivotTable spending_fnc(filter); writeTabletoCSV(spending_fnc); QVERIFY(spending_fnc.m_grid["Expense"]["Foreign"][ReportAccount(acCanCash)][eActual][1] == (moCanTransaction)); QVERIFY(spending_fnc.m_grid["Expense"]["Foreign"][ReportAccount(acCanCash)][eActual][2] == (moCanTransaction)); QVERIFY(spending_fnc.m_grid["Expense"]["Foreign"][ReportAccount(acCanCash)][eActual][3] == (moCanTransaction)); QVERIFY(spending_fnc.m_grid["Expense"]["Foreign"][ReportAccount(acJpyCash)][eActual][1] == (moJpyTransaction)); QVERIFY(spending_fnc.m_grid["Expense"]["Foreign"][ReportAccount(acJpyCash)][eActual][2] == (moJpyTransaction)); QVERIFY(spending_fnc.m_grid["Expense"]["Foreign"][ReportAccount(acJpyCash)][eActual][3] == (moJpyTransaction)); filter.setConvertCurrency(true); filter.clearTransactionFilter(); filter.setName("Multiple currency net worth"); filter.setRowType(MyMoneyReport::eAssetLiability); filter.setDateFilter(QDate(2004, 1, 1), QDate(2005, 1, 1).addDays(-1)); XMLandback(filter); PivotTable networth_f(filter); writeTabletoCSV(networth_f); // test single foreign currency QVERIFY(networth_f.m_grid["Asset"]["Canadian Checking"][ReportAccount(acCanChecking)][eActual][1] == (moCanOpening*moCanPrice)); QVERIFY(networth_f.m_grid["Asset"]["Canadian Checking"][ReportAccount(acCanChecking)][eActual][2] == ((moCanOpening - moCanTransaction)*moCanPrice)); QVERIFY(networth_f.m_grid["Asset"]["Canadian Checking"][ReportAccount(acCanChecking)][eActual][3] == ((moCanOpening - moCanTransaction - moCanTransaction)*moCanPrice)); QVERIFY(networth_f.m_grid["Asset"]["Canadian Checking"][ReportAccount(acCanChecking)][eActual][4] == ((moCanOpening - moCanTransaction - moCanTransaction - moCanTransaction)*moCanPrice)); QVERIFY(networth_f.m_grid["Asset"]["Canadian Checking"][ReportAccount(acCanChecking)][eActual][12] == ((moCanOpening - moCanTransaction - moCanTransaction - moCanTransaction)*moCanPrice)); // test Stable currency price, fluctuating account balance QVERIFY(networth_f.m_grid["Asset"]["Japanese Checking"][ReportAccount(acJpyChecking)][eActual][1] == (moJpyOpening*moJpyPrice)); QVERIFY(networth_f.m_grid["Asset"]["Japanese Checking"][ReportAccount(acJpyChecking)][eActual][2] == ((moJpyOpening - moJpyTransaction)*moJpyPrice)); QVERIFY(networth_f.m_grid["Asset"]["Japanese Checking"][ReportAccount(acJpyChecking)][eActual][3] == ((moJpyOpening - moJpyTransaction - moJpyTransaction)*moJpyPrice)); QVERIFY(networth_f.m_grid["Asset"]["Japanese Checking"][ReportAccount(acJpyChecking)][eActual][4] == ((moJpyOpening - moJpyTransaction - moJpyTransaction - moJpyTransaction)*moJpyPrice)); // test Fluctuating currency price, stable account balance QVERIFY(networth_f.m_grid["Asset"]["Japanese Checking"][ReportAccount(acJpyChecking)][eActual][5] == ((moJpyOpening - moJpyTransaction - moJpyTransaction - moJpyTransaction)*moJpyPrice2)); QVERIFY(networth_f.m_grid["Asset"]["Japanese Checking"][ReportAccount(acJpyChecking)][eActual][6] == ((moJpyOpening - moJpyTransaction - moJpyTransaction - moJpyTransaction)*moJpyPrice3)); QVERIFY(networth_f.m_grid["Asset"]["Japanese Checking"][ReportAccount(acJpyChecking)][eActual][7] == ((moJpyOpening - moJpyTransaction - moJpyTransaction - moJpyTransaction)*moJpyPrice4)); // test multiple currencies totalled up QVERIFY(networth_f.m_grid["Asset"].m_total[eActual][4] == ((moCanOpening - moCanTransaction - moCanTransaction - moCanTransaction)*moCanPrice) + ((moJpyOpening - moJpyTransaction - moJpyTransaction - moJpyTransaction)*moJpyPrice)); QVERIFY(networth_f.m_grid["Asset"].m_total[eActual][5] == ((moCanOpening - moCanTransaction - moCanTransaction - moCanTransaction)*moCanPrice) + ((moJpyOpening - moJpyTransaction - moJpyTransaction - moJpyTransaction)*moJpyPrice2) + moCheckingOpen); } void PivotTableTest::testAdvancedFilter() { // test more advanced filtering capabilities // amount { TransactionHelper t1(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent1, acCredit, acParent); TransactionHelper t2(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moChild, acCredit, acChild); MyMoneyReport filter; filter.setRowType(MyMoneyReport::eExpenseIncome); filter.setDateFilter(QDate(2004, 1, 1), QDate(2005, 1, 1).addDays(-1)); filter.setAmountFilter(moChild, moChild); XMLandback(filter); PivotTable spending_f(filter); QVERIFY(spending_f.m_grid.m_total[eActual].m_total == -moChild); } // payee (specific) { TransactionHelper t1(QDate(2004, 10, 31), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moSolo, acChecking, acSolo); TransactionHelper t2(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent1, acCredit, acParent); TransactionHelper t3(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moChild, acCredit, acChild); TransactionHelper t4(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moThomas, acCredit, acParent, QString(), "Thomas Baumgart"); MyMoneyReport filter; filter.setRowType(MyMoneyReport::eExpenseIncome); filter.setDateFilter(QDate(2004, 1, 1), QDate(2005, 1, 1).addDays(-1)); filter.addPayee(MyMoneyFile::instance()->payeeByName("Thomas Baumgart").id()); filter.setName("Spending with Payee Filter"); XMLandback(filter); PivotTable spending_f(filter); writeTabletoHTML(spending_f, "Spending with Payee Filter.html"); QVERIFY(spending_f.m_grid["Expense"]["Parent"][ReportAccount(acParent)][eActual][10] == moThomas); QVERIFY(spending_f.m_grid.m_total[eActual].m_total == -moThomas); } // payee (no payee) { TransactionHelper t1(QDate(2004, 10, 31), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moSolo, acChecking, acSolo); TransactionHelper t2(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent1, acCredit, acParent); TransactionHelper t3(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moChild, acCredit, acChild); TransactionHelper t4(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moNoPayee, acCredit, acParent, QString(), QString()); MyMoneyReport filter; filter.setRowType(MyMoneyReport::eExpenseIncome); filter.setDateFilter(QDate(2004, 1, 1), QDate(2005, 1, 1).addDays(-1)); filter.addPayee(QString()); XMLandback(filter); PivotTable spending_f(filter); QVERIFY(spending_f.m_grid["Expense"]["Parent"][ReportAccount(acParent)][eActual][10] == moNoPayee); QVERIFY(spending_f.m_grid.m_total[eActual].m_total == -moNoPayee); } // text { TransactionHelper t1(QDate(2004, 10, 31), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moSolo, acChecking, acSolo); TransactionHelper t2(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent1, acCredit, acParent); TransactionHelper t3(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moChild, acCredit, acChild); TransactionHelper t4(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moThomas, acCredit, acParent, QString(), "Thomas Baumgart"); MyMoneyReport filter; filter.setRowType(MyMoneyReport::eExpenseIncome); filter.setDateFilter(QDate(2004, 1, 1), QDate(2005, 1, 1).addDays(-1)); filter.setTextFilter(QRegExp("Thomas")); XMLandback(filter); PivotTable spending_f(filter); } // type (payment, deposit, transfer) { TransactionHelper t1(QDate(2004, 1, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moSolo, acChecking, acSolo); TransactionHelper t2(QDate(2004, 2, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit), -moParent1, acCredit, acParent); TransactionHelper t3(QDate(2004, 11, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Transfer), moChild, acCredit, acChecking); MyMoneyReport filter; filter.setRowType(MyMoneyReport::eExpenseIncome); filter.addType((int)eMyMoney::TransactionFilter::Type::Payments); XMLandback(filter); PivotTable spending_f(filter); QVERIFY(spending_f.m_grid.m_total[eActual].m_total == -moSolo); filter.clearTransactionFilter(); filter.addType((int)eMyMoney::TransactionFilter::Type::Deposits); XMLandback(filter); PivotTable spending_f2(filter); QVERIFY(spending_f2.m_grid.m_total[eActual].m_total == moParent1); filter.clearTransactionFilter(); filter.addType((int)eMyMoney::TransactionFilter::Type::Transfers); XMLandback(filter); PivotTable spending_f3(filter); QVERIFY(spending_f3.m_grid.m_total[eActual].m_total == moZero); filter.setRowType(MyMoneyReport::eAssetLiability); filter.setDateFilter(QDate(2004, 1, 1), QDate(2004, 12, 31)); XMLandback(filter); PivotTable networth_f4(filter); QVERIFY(networth_f4.m_grid["Asset"].m_total[eActual][11] == moCheckingOpen + moChild); QVERIFY(networth_f4.m_grid["Liability"].m_total[eActual][11] == - moCreditOpen + moChild); QVERIFY(networth_f4.m_grid.m_total[eActual][9] == moCheckingOpen + moCreditOpen); QVERIFY(networth_f4.m_grid.m_total[eActual][10] == moCheckingOpen + moCreditOpen); } // state (reconciled, cleared, not) { TransactionHelper t1(QDate(2004, 1, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moSolo, acChecking, acSolo); TransactionHelper t2(QDate(2004, 2, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent1, acCredit, acParent); TransactionHelper t3(QDate(2004, 3, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent2, acCredit, acParent); TransactionHelper t4(QDate(2004, 4, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moChild, acCredit, acChild); QList splits = t1.splits(); splits[0].setReconcileFlag(eMyMoney::Split::State::Cleared); splits[1].setReconcileFlag(eMyMoney::Split::State::Cleared); t1.modifySplit(splits[0]); t1.modifySplit(splits[1]); t1.update(); splits.clear(); splits = t2.splits(); splits[0].setReconcileFlag(eMyMoney::Split::State::Reconciled); splits[1].setReconcileFlag(eMyMoney::Split::State::Reconciled); t2.modifySplit(splits[0]); t2.modifySplit(splits[1]); t2.update(); MyMoneyReport filter; filter.setRowType(MyMoneyReport::eExpenseIncome); filter.setDateFilter(QDate(2004, 1, 1), QDate(2005, 1, 1).addDays(-1)); filter.addState((int)eMyMoney::TransactionFilter::State::Cleared); XMLandback(filter); PivotTable spending_f(filter); QVERIFY(spending_f.m_grid.m_total[eActual].m_total == -moSolo); filter.addState((int)eMyMoney::TransactionFilter::State::Reconciled); XMLandback(filter); PivotTable spending_f2(filter); QVERIFY(spending_f2.m_grid.m_total[eActual].m_total == -moSolo - moParent1); filter.clearTransactionFilter(); filter.addState((int)eMyMoney::TransactionFilter::State::NotReconciled); XMLandback(filter); PivotTable spending_f3(filter); QVERIFY(spending_f3.m_grid.m_total[eActual].m_total == -moChild - moParent2); } // number { TransactionHelper t1(QDate(2004, 10, 31), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moSolo, acChecking, acSolo); TransactionHelper t2(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent1, acCredit, acParent); TransactionHelper t3(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent2, acCredit, acParent); TransactionHelper t4(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moChild, acCredit, acChild); QList splits = t1.splits(); splits[0].setNumber("1"); splits[1].setNumber("1"); t1.modifySplit(splits[0]); t1.modifySplit(splits[1]); t1.update(); splits.clear(); splits = t2.splits(); splits[0].setNumber("2"); splits[1].setNumber("2"); t2.modifySplit(splits[0]); t2.modifySplit(splits[1]); t2.update(); splits.clear(); splits = t3.splits(); splits[0].setNumber("3"); splits[1].setNumber("3"); t3.modifySplit(splits[0]); t3.modifySplit(splits[1]); t3.update(); splits.clear(); splits = t2.splits(); splits[0].setNumber("4"); splits[1].setNumber("4"); t4.modifySplit(splits[0]); t4.modifySplit(splits[1]); t4.update(); MyMoneyReport filter; filter.setRowType(MyMoneyReport::eExpenseIncome); filter.setDateFilter(QDate(2004, 1, 1), QDate(2005, 1, 1).addDays(-1)); filter.setNumberFilter("1", "3"); XMLandback(filter); PivotTable spending_f(filter); QVERIFY(spending_f.m_grid.m_total[eActual].m_total == -moSolo - moParent1 - moParent2); } // blank dates { TransactionHelper t1y1(QDate(2003, 10, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moSolo, acChecking, acSolo); TransactionHelper t2y1(QDate(2003, 11, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent1, acCredit, acParent); TransactionHelper t3y1(QDate(2003, 12, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent2, acCredit, acParent); TransactionHelper t1y2(QDate(2004, 4, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moSolo, acChecking, acSolo); TransactionHelper t2y2(QDate(2004, 5, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent1, acCredit, acParent); TransactionHelper t3y2(QDate(2004, 6, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent2, acCredit, acParent); TransactionHelper t1y3(QDate(2005, 1, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moSolo, acChecking, acSolo); TransactionHelper t2y3(QDate(2005, 5, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent1, acCredit, acParent); TransactionHelper t3y3(QDate(2005, 9, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent2, acCredit, acParent); MyMoneyReport filter; filter.setRowType(MyMoneyReport::eExpenseIncome); filter.setDateFilter(QDate(), QDate(2004, 7, 1)); XMLandback(filter); PivotTable spending_f(filter); QVERIFY(spending_f.m_grid.m_total[eActual].m_total == -moSolo - moParent1 - moParent2 - moSolo - moParent1 - moParent2); filter.clearTransactionFilter(); XMLandback(filter); PivotTable spending_f2(filter); QVERIFY(spending_f2.m_grid.m_total[eActual].m_total == -moSolo - moParent1 - moParent2 - moSolo - moParent1 - moParent2 - moSolo - moParent1 - moParent2); } } void PivotTableTest::testColumnType() { // test column type values of other than 'month' TransactionHelper t1q1(QDate(2004, 1, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moSolo, acChecking, acSolo); TransactionHelper t2q1(QDate(2004, 2, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent1, acCredit, acParent); TransactionHelper t3q1(QDate(2004, 3, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent2, acCredit, acParent); TransactionHelper t1q2(QDate(2004, 4, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moSolo, acChecking, acSolo); TransactionHelper t2q2(QDate(2004, 5, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent1, acCredit, acParent); TransactionHelper t3q2(QDate(2004, 6, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent2, acCredit, acParent); TransactionHelper t1y2(QDate(2005, 1, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moSolo, acChecking, acSolo); TransactionHelper t2y2(QDate(2005, 5, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent1, acCredit, acParent); TransactionHelper t3y2(QDate(2005, 9, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent2, acCredit, acParent); MyMoneyReport filter; filter.setRowType(MyMoneyReport::eExpenseIncome); filter.setDateFilter(QDate(2003, 12, 31), QDate(2005, 12, 31)); filter.setRowType(MyMoneyReport::eExpenseIncome); filter.setColumnType(MyMoneyReport::eBiMonths); XMLandback(filter); PivotTable spending_b(filter); QVERIFY(spending_b.m_grid.m_total[eActual][0] == moZero); QVERIFY(spending_b.m_grid.m_total[eActual][1] == -moParent1 - moSolo); QVERIFY(spending_b.m_grid.m_total[eActual][2] == -moParent2 - moSolo); QVERIFY(spending_b.m_grid.m_total[eActual][3] == -moParent); QVERIFY(spending_b.m_grid.m_total[eActual][4] == moZero); QVERIFY(spending_b.m_grid.m_total[eActual][5] == moZero); QVERIFY(spending_b.m_grid.m_total[eActual][6] == moZero); QVERIFY(spending_b.m_grid.m_total[eActual][7] == -moSolo); QVERIFY(spending_b.m_grid.m_total[eActual][8] == moZero); QVERIFY(spending_b.m_grid.m_total[eActual][9] == -moParent1); QVERIFY(spending_b.m_grid.m_total[eActual][10] == moZero); QVERIFY(spending_b.m_grid.m_total[eActual][11] == -moParent2); QVERIFY(spending_b.m_grid.m_total[eActual][12] == moZero); filter.setColumnType(MyMoneyReport::eQuarters); XMLandback(filter); PivotTable spending_q(filter); QVERIFY(spending_q.m_grid.m_total[eActual][0] == moZero); QVERIFY(spending_q.m_grid.m_total[eActual][1] == -moSolo - moParent); QVERIFY(spending_q.m_grid.m_total[eActual][2] == -moSolo - moParent); QVERIFY(spending_q.m_grid.m_total[eActual][3] == moZero); QVERIFY(spending_q.m_grid.m_total[eActual][4] == moZero); QVERIFY(spending_q.m_grid.m_total[eActual][5] == -moSolo); QVERIFY(spending_q.m_grid.m_total[eActual][6] == -moParent1); QVERIFY(spending_q.m_grid.m_total[eActual][7] == -moParent2); QVERIFY(spending_q.m_grid.m_total[eActual][8] == moZero); filter.setRowType(MyMoneyReport::eAssetLiability); filter.setName("Net Worth by Quarter"); XMLandback(filter); PivotTable networth_q(filter); writeTabletoHTML(networth_q, "Net Worth by Quarter.html"); QVERIFY(networth_q.m_grid.m_total[eActual][1] == moZero); QVERIFY(networth_q.m_grid.m_total[eActual][2] == -moSolo - moParent); QVERIFY(networth_q.m_grid.m_total[eActual][3] == -moSolo - moParent - moSolo - moParent + moCheckingOpen); QVERIFY(networth_q.m_grid.m_total[eActual][4] == -moSolo - moParent - moSolo - moParent + moCheckingOpen + moCreditOpen); QVERIFY(networth_q.m_grid.m_total[eActual][5] == -moSolo - moParent - moSolo - moParent + moCheckingOpen + moCreditOpen); QVERIFY(networth_q.m_grid.m_total[eActual][6] == -moSolo - moSolo - moParent - moSolo - moParent + moCheckingOpen + moCreditOpen); QVERIFY(networth_q.m_grid.m_total[eActual][7] == -moParent1 - moSolo - moSolo - moParent - moSolo - moParent + moCheckingOpen + moCreditOpen); QVERIFY(networth_q.m_grid.m_total[eActual][8] == -moParent2 - moParent1 - moSolo - moSolo - moParent - moSolo - moParent + moCheckingOpen + moCreditOpen); QVERIFY(networth_q.m_grid.m_total[eActual][9] == -moParent2 - moParent1 - moSolo - moSolo - moParent - moSolo - moParent + moCheckingOpen + moCreditOpen); filter.setRowType(MyMoneyReport::eExpenseIncome); filter.setColumnType(MyMoneyReport::eYears); XMLandback(filter); PivotTable spending_y(filter); QVERIFY(spending_y.m_grid.m_total[eActual][0] == moZero); QVERIFY(spending_y.m_grid.m_total[eActual][1] == -moSolo - moParent - moSolo - moParent); QVERIFY(spending_y.m_grid.m_total[eActual][2] == -moSolo - moParent); QVERIFY(spending_y.m_grid.m_total[eActual].m_total == -moSolo - moParent - moSolo - moParent - moSolo - moParent); filter.setRowType(MyMoneyReport::eAssetLiability); XMLandback(filter); PivotTable networth_y(filter); QVERIFY(networth_y.m_grid.m_total[eActual][1] == moZero); QVERIFY(networth_y.m_grid.m_total[eActual][2] == -moSolo - moParent - moSolo - moParent + moCheckingOpen + moCreditOpen); QVERIFY(networth_y.m_grid.m_total[eActual][3] == -moSolo - moParent - moSolo - moParent - moSolo - moParent + moCheckingOpen + moCreditOpen); // Test days-based reports TransactionHelper t1d1(QDate(2004, 7, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moSolo, acChecking, acSolo); TransactionHelper t2d1(QDate(2004, 7, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent1, acCredit, acParent); TransactionHelper t3d1(QDate(2004, 7, 4), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent2, acCredit, acParent); TransactionHelper t1d2(QDate(2004, 7, 14), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moSolo, acChecking, acSolo); TransactionHelper t2d2(QDate(2004, 7, 15), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent1, acCredit, acParent); TransactionHelper t3d2(QDate(2004, 7, 20), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent2, acCredit, acParent); TransactionHelper t1d3(QDate(2004, 8, 2), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moSolo, acChecking, acSolo); TransactionHelper t2d3(QDate(2004, 8, 3), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent1, acCredit, acParent); TransactionHelper t3d3(QDate(2004, 8, 4), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent2, acCredit, acParent); filter.setDateFilter(QDate(2004, 7, 2), QDate(2004, 7, 14)); filter.setRowType(MyMoneyReport::eExpenseIncome); filter.setColumnType(MyMoneyReport::eMonths); filter.setColumnsAreDays(true); filter.setName("Spending by Days"); XMLandback(filter); PivotTable spending_days(filter); writeTabletoHTML(spending_days, "Spending by Days.html"); QVERIFY(spending_days.m_grid.m_total[eActual][2] == -moParent2); QVERIFY(spending_days.m_grid.m_total[eActual][12] == -moSolo); QVERIFY(spending_days.m_grid.m_total[eActual].m_total == -moSolo - moParent2); // set the first day of the week to 1 QLocale::setDefault(QLocale(QLocale::English, QLocale::UnitedKingdom)); filter.setDateFilter(QDate(2004, 7, 2), QDate(2004, 8, 1)); filter.setRowType(MyMoneyReport::eExpenseIncome); filter.setColumnType(static_cast(7)); filter.setColumnsAreDays(true); filter.setName("Spending by Weeks"); XMLandback(filter); PivotTable spending_weeks(filter); writeTabletoHTML(spending_weeks, "Spending by Weeks.html"); // restore the locale QLocale::setDefault(QLocale::system()); QVERIFY(spending_weeks.m_grid.m_total[eActual][0] == -moParent2); QVERIFY(spending_weeks.m_grid.m_total[eActual][1] == moZero); QVERIFY(spending_weeks.m_grid.m_total[eActual][2] == -moSolo - moParent1); QVERIFY(spending_weeks.m_grid.m_total[eActual][3] == -moParent2); QVERIFY(spending_weeks.m_grid.m_total[eActual][4] == moZero); QVERIFY(spending_weeks.m_grid.m_total[eActual].m_total == -moSolo - moParent - moParent2); } void PivotTableTest::testInvestment() { try { // Equities eqStock1 = makeEquity("Stock1", "STK1"); eqStock2 = makeEquity("Stock2", "STK2"); // Accounts acInvestment = makeAccount("Investment", eMyMoney::Account::Type::Investment, moZero, QDate(2004, 1, 1), acAsset); acStock1 = makeAccount("Stock 1", eMyMoney::Account::Type::Stock, moZero, QDate(2004, 1, 1), acInvestment, eqStock1); acStock2 = makeAccount("Stock 2", eMyMoney::Account::Type::Stock, moZero, QDate(2004, 1, 1), acInvestment, eqStock2); acDividends = makeAccount("Dividends", eMyMoney::Account::Type::Income, moZero, QDate(2004, 1, 1), acIncome); // Transactions // Date Action Shares Price Stock Asset Income InvTransactionHelper s1b1(QDate(2004, 2, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::BuyShares), MyMoneyMoney(1000.00), MyMoneyMoney(100.00), acStock1, acChecking, QString()); InvTransactionHelper s1b2(QDate(2004, 3, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::BuyShares), MyMoneyMoney(1000.00), MyMoneyMoney(110.00), acStock1, acChecking, QString()); InvTransactionHelper s1s1(QDate(2004, 4, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::BuyShares), MyMoneyMoney(-200.00), MyMoneyMoney(120.00), acStock1, acChecking, QString()); InvTransactionHelper s1s2(QDate(2004, 5, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::BuyShares), MyMoneyMoney(-200.00), MyMoneyMoney(100.00), acStock1, acChecking, QString()); InvTransactionHelper s1r1(QDate(2004, 6, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::ReinvestDividend), MyMoneyMoney(50.00), MyMoneyMoney(100.00), acStock1, QString(), acDividends); InvTransactionHelper s1r2(QDate(2004, 7, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::ReinvestDividend), MyMoneyMoney(50.00), MyMoneyMoney(80.00), acStock1, QString(), acDividends); InvTransactionHelper s1c1(QDate(2004, 8, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Dividend), MyMoneyMoney(10.00), MyMoneyMoney(100.00), acStock1, acChecking, acDividends); InvTransactionHelper s1c2(QDate(2004, 9, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Dividend), MyMoneyMoney(10.00), MyMoneyMoney(120.00), acStock1, acChecking, acDividends); makeEquityPrice(eqStock1, QDate(2004, 10, 1), MyMoneyMoney(100.00)); // // Net Worth Report (with investments) // MyMoneyReport networth_r; networth_r.setRowType(MyMoneyReport::eAssetLiability); networth_r.setDateFilter(QDate(2004, 1, 1), QDate(2004, 12, 31).addDays(-1)); XMLandback(networth_r); PivotTable networth(networth_r); networth.dump("networth_i.html"); QVERIFY(networth.m_grid["Asset"]["Investment"].m_total[eActual][1] == moZero); // 1000 shares @ $100.00 QVERIFY(networth.m_grid["Asset"]["Investment"].m_total[eActual][2] == MyMoneyMoney(100000.0)); // 2000 shares @ $110.00 QVERIFY(networth.m_grid["Asset"]["Investment"].m_total[eActual][3] == MyMoneyMoney(220000.0)); // 1800 shares @ $120.00 QVERIFY(networth.m_grid["Asset"]["Investment"].m_total[eActual][4] == MyMoneyMoney(216000.0)); // 1600 shares @ $100.00 QVERIFY(networth.m_grid["Asset"]["Investment"].m_total[eActual][5] == MyMoneyMoney(160000.0)); // 1650 shares @ $100.00 QVERIFY(networth.m_grid["Asset"]["Investment"].m_total[eActual][6] == MyMoneyMoney(165000.0)); // 1700 shares @ $ 80.00 QVERIFY(networth.m_grid["Asset"]["Investment"].m_total[eActual][7] == MyMoneyMoney(136000.0)); // 1700 shares @ $100.00 QVERIFY(networth.m_grid["Asset"]["Investment"].m_total[eActual][8] == MyMoneyMoney(170000.0)); // 1700 shares @ $120.00 QVERIFY(networth.m_grid["Asset"]["Investment"].m_total[eActual][9] == MyMoneyMoney(204000.0)); // 1700 shares @ $100.00 QVERIFY(networth.m_grid["Asset"]["Investment"].m_total[eActual][10] == MyMoneyMoney(170000.0)); #if 0 // Dump file & reports QFile g("investmentkmy.xml"); g.open(QIODevice::WriteOnly); MyMoneyStorageXML xml; IMyMoneyOperationsFormat& interface = xml; interface.writeFile(&g, dynamic_cast(MyMoneyFile::instance()->storage())); g.close(); invtran.dump("invtran.html", "%1"); invhold.dump("invhold.html", "%1"); #endif } catch (const MyMoneyException &e) { QFAIL(e.what()); } } void PivotTableTest::testBudget() { // 1. Budget on A, transactions on A { BudgetHelper budget; budget += BudgetEntryHelper(QDate(2006, 1, 1), acSolo, false, MyMoneyMoney(100.0)); MyMoneyReport report(MyMoneyReport::eBudgetActual, MyMoneyReport::eMonths, eMyMoney::TransactionFilter::Date::YearToDate, MyMoneyReport::eDetailTop, "Yearly Budgeted vs. Actual", "Default Report"); PivotTable table(report); } // 2. Budget on B, not applying to sub accounts, transactions on B and B:1 { BudgetHelper budget; budget += BudgetEntryHelper(QDate(2006, 1, 1), acParent, false, MyMoneyMoney(100.0)); MyMoneyReport report(MyMoneyReport::eBudgetActual, MyMoneyReport::eMonths, eMyMoney::TransactionFilter::Date::YearToDate, MyMoneyReport::eDetailTop, "Yearly Budgeted vs. Actual", "Default Report"); PivotTable table(report); } // - Both B and B:1 totals should show up // - B actuals compare against B budget // - B:1 actuals compare against 0 // 3. Budget on C, applying to sub accounts, transactions on C and C:1 and C:1:a { BudgetHelper budget; budget += BudgetEntryHelper(QDate(2006, 1, 1), acParent, true, MyMoneyMoney(100.0)); MyMoneyReport report(MyMoneyReport::eBudgetActual, MyMoneyReport::eMonths, eMyMoney::TransactionFilter::Date::YearToDate, MyMoneyReport::eDetailTop , "Yearly Budgeted vs. Actual", "Default Report"); PivotTable table(report); } // - Only C totals show up, not C:1 or C:1:a totals // - C + C:1 totals compare against C budget // 4. Budget on D, not applying to sub accounts, budget on D:1 not applying, budget on D:2 applying. Transactions on D, D:1, D:2, D:2:a, D:2:b { BudgetHelper budget; budget += BudgetEntryHelper(QDate(2006, 1, 1), acParent, false, MyMoneyMoney(100.0)); budget += BudgetEntryHelper(QDate(2006, 1, 1), acChild, false, MyMoneyMoney(100.0)); budget += BudgetEntryHelper(QDate(2006, 1, 1), acSecondChild, true, MyMoneyMoney(100.0)); MyMoneyReport report(MyMoneyReport::eBudgetActual, MyMoneyReport::eMonths, eMyMoney::TransactionFilter::Date::YearToDate, MyMoneyReport::eDetailTop, "Yearly Budgeted vs. Actual", "Default Report"); PivotTable table(report); } // - Totals for D, D:1, D:2 show up. D:2:a and D:2:b do not // - D actuals (only) compare against D budget // - Ditto for D:1 // - D:2 acutals and children compare against D:2 budget // 5. Budget on E, no transactions on E { BudgetHelper budget; budget += BudgetEntryHelper(QDate(2006, 1, 1), acSolo, false, MyMoneyMoney(100.0)); MyMoneyReport report(MyMoneyReport::eBudgetActual, MyMoneyReport::eMonths, eMyMoney::TransactionFilter::Date::YearToDate, MyMoneyReport::eDetailTop, "Yearly Budgeted vs. Actual", "Default Report"); PivotTable table(report); } } void PivotTableTest::testHtmlEncoding() { MyMoneyReport filter; filter.setRowType(MyMoneyReport::eAssetLiability); filter.setDateFilter(QDate(2004, 1, 1), QDate(2005, 1, 1).addDays(-1)); XMLandback(filter); PivotTable networth_f(filter); QByteArray encoding = QTextCodec::codecForLocale()->name(); QString html = networth_f.renderReport(QLatin1String("html"), encoding, filter.name(), false); QRegExp rx(QString::fromLatin1("**")); rx.setPatternSyntax(QRegExp::Wildcard); rx.setCaseSensitivity(Qt::CaseInsensitive); QVERIFY(rx.exactMatch(html)); } diff --git a/kmymoney/reports/tests/querytable-test.cpp b/kmymoney/reports/tests/querytable-test.cpp index 9944c03d3..989eb51e9 100644 --- a/kmymoney/reports/tests/querytable-test.cpp +++ b/kmymoney/reports/tests/querytable-test.cpp @@ -1,904 +1,903 @@ /*************************************************************************** querytabletest.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 "querytable-test.h" #include #include #include #include "reportstestcommon.h" #include "querytable.h" #include "mymoneyinstitution.h" #include "mymoneyaccount.h" #include "mymoneysecurity.h" #include "mymoneyprice.h" #include "mymoneystoragedump.h" #include "mymoneyreport.h" #include "mymoneysplit.h" #include "mymoneypayee.h" #include "mymoneystatement.h" -#include "mymoneystoragexml.h" #include "mymoneyexception.h" #include "kmymoneysettings.h" using namespace reports; using namespace test; QTEST_GUILESS_MAIN(QueryTableTest) void QueryTableTest::setup() { } void QueryTableTest::init() { storage = new MyMoneyStorageMgr; file = MyMoneyFile::instance(); file->attachStorage(storage); MyMoneyFileTransaction ft; file->addCurrency(MyMoneySecurity("CAD", "Canadian Dollar", "C$")); file->addCurrency(MyMoneySecurity("USD", "US Dollar", "$")); file->addCurrency(MyMoneySecurity("JPY", "Japanese Yen", QChar(0x00A5), 1)); file->addCurrency(MyMoneySecurity("GBP", "British Pound", "#")); file->setBaseCurrency(file->currency("USD")); MyMoneyPayee payeeTest("Test Payee"); file->addPayee(payeeTest); MyMoneyPayee payeeTest2("Thomas Baumgart"); file->addPayee(payeeTest2); acAsset = (MyMoneyFile::instance()->asset().id()); acLiability = (MyMoneyFile::instance()->liability().id()); acExpense = (MyMoneyFile::instance()->expense().id()); acIncome = (MyMoneyFile::instance()->income().id()); acChecking = makeAccount(QString("Checking Account"), eMyMoney::Account::Type::Checkings, moCheckingOpen, QDate(2004, 5, 15), acAsset); acCredit = makeAccount(QString("Credit Card"), eMyMoney::Account::Type::CreditCard, moCreditOpen, QDate(2004, 7, 15), acLiability); acSolo = makeAccount(QString("Solo"), eMyMoney::Account::Type::Expense, MyMoneyMoney(), QDate(2004, 1, 11), acExpense); acParent = makeAccount(QString("Parent"), eMyMoney::Account::Type::Expense, MyMoneyMoney(), QDate(2004, 1, 11), acExpense); acChild = makeAccount(QString("Child"), eMyMoney::Account::Type::Expense, MyMoneyMoney(), QDate(2004, 2, 11), acParent); acForeign = makeAccount(QString("Foreign"), eMyMoney::Account::Type::Expense, MyMoneyMoney(), QDate(2004, 1, 11), acExpense); acTax = makeAccount(QString("Tax"), eMyMoney::Account::Type::Expense, MyMoneyMoney(), QDate(2005, 1, 11), acExpense, "", true); MyMoneyInstitution i("Bank of the World", "", "", "", "", "", ""); file->addInstitution(i); inBank = i.id(); ft.commit(); } void QueryTableTest::cleanup() { file->detachStorage(storage); delete storage; } void QueryTableTest::testQueryBasics() { try { TransactionHelper t1q1(QDate(2004, 1, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moSolo, acChecking, acSolo); TransactionHelper t2q1(QDate(2004, 2, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent1, acCredit, acParent); TransactionHelper t3q1(QDate(2004, 3, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent2, acCredit, acParent); TransactionHelper t4y1(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moChild, acCredit, acChild); TransactionHelper t1q2(QDate(2004, 4, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moSolo, acChecking, acSolo); TransactionHelper t2q2(QDate(2004, 5, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent1, acCredit, acParent); TransactionHelper t3q2(QDate(2004, 6, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent2, acCredit, acParent); TransactionHelper t4q2(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moChild, acCredit, acChild); TransactionHelper t1y2(QDate(2005, 1, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moSolo, acChecking, acSolo); TransactionHelper t2y2(QDate(2005, 5, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent1, acCredit, acParent); TransactionHelper t3y2(QDate(2005, 9, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent2, acCredit, acParent); TransactionHelper t4y2(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moChild, acCredit, acChild); unsigned cols; MyMoneyReport filter; filter.setRowType(MyMoneyReport::eCategory); cols = MyMoneyReport::eQCnumber | MyMoneyReport::eQCpayee | MyMoneyReport::eQCaccount; filter.setQueryColumns(static_cast(cols)); // filter.setName("Transactions by Category"); XMLandback(filter); QueryTable qtbl_1(filter); writeTabletoHTML(qtbl_1, "Transactions by Category.html"); QList rows = qtbl_1.rows(); QVERIFY(rows.count() == 19); QVERIFY(rows[0][ListTable::ctCategoryType] == "Expense"); QVERIFY(rows[0][ListTable::ctCategory] == "Parent"); QVERIFY(rows[0][ListTable::ctPostDate] == "2004-02-01"); QVERIFY(rows[14][ListTable::ctCategoryType] == "Expense"); QVERIFY(rows[14][ListTable::ctCategory] == "Solo"); QVERIFY(rows[14][ListTable::ctPostDate] == "2005-01-01"); QVERIFY(MyMoneyMoney(rows[6][ListTable::ctValue]) == -(moParent1 + moParent2) * 3); QVERIFY(MyMoneyMoney(rows[10][ListTable::ctValue]) == -(moChild) * 3); QVERIFY(MyMoneyMoney(rows[16][ListTable::ctValue]) == -(moSolo) * 3); QVERIFY(MyMoneyMoney(rows[17][ListTable::ctValue]) == -(moParent1 + moParent2 + moSolo + moChild) * 3); QVERIFY(MyMoneyMoney(rows[18][ListTable::ctValue]) == -(moParent1 + moParent2 + moSolo + moChild) * 3 + moCheckingOpen + moCreditOpen); filter.setRowType(MyMoneyReport::eTopCategory); cols = MyMoneyReport::eQCnumber | MyMoneyReport::eQCpayee | MyMoneyReport::eQCaccount; filter.setQueryColumns(static_cast(cols)); // filter.setName("Transactions by Top Category"); XMLandback(filter); QueryTable qtbl_2(filter); writeTabletoHTML(qtbl_2, "Transactions by Top Category.html"); rows = qtbl_2.rows(); QVERIFY(rows.count() == 16); QVERIFY(rows[0][ListTable::ctCategoryType] == "Expense"); QVERIFY(rows[0][ListTable::ctTopCategory] == "Parent"); QVERIFY(rows[0][ListTable::ctPostDate] == "2004-02-01"); QVERIFY(rows[8][ListTable::ctCategoryType] == "Expense"); QVERIFY(rows[8][ListTable::ctTopCategory] == "Parent"); QVERIFY(rows[8][ListTable::ctPostDate] == "2005-09-01"); QVERIFY(rows[12][ListTable::ctCategoryType] == "Expense"); QVERIFY(rows[12][ListTable::ctTopCategory] == "Solo"); QVERIFY(rows[12][ListTable::ctPostDate] == "2005-01-01"); QVERIFY(MyMoneyMoney(rows[9][ListTable::ctValue]) == -(moParent1 + moParent2 + moChild) * 3); QVERIFY(MyMoneyMoney(rows[13][ListTable::ctValue]) == -(moSolo) * 3); QVERIFY(MyMoneyMoney(rows[14][ListTable::ctValue]) == -(moParent1 + moParent2 + moSolo + moChild) * 3); QVERIFY(MyMoneyMoney(rows[15][ListTable::ctValue]) == -(moParent1 + moParent2 + moSolo + moChild) * 3 + moCheckingOpen + moCreditOpen); filter.setRowType(MyMoneyReport::eAccount); filter.setName("Transactions by Account"); cols = MyMoneyReport::eQCnumber | MyMoneyReport::eQCpayee | MyMoneyReport::eQCcategory; filter.setQueryColumns(static_cast(cols)); // XMLandback(filter); QueryTable qtbl_3(filter); writeTabletoHTML(qtbl_3, "Transactions by Account.html"); rows = qtbl_3.rows(); #if 1 QVERIFY(rows.count() == 19); QVERIFY(rows[1][ListTable::ctAccount] == "Checking Account"); QVERIFY(rows[1][ListTable::ctCategory] == "Solo"); QVERIFY(rows[1][ListTable::ctPostDate] == "2004-01-01"); QVERIFY(rows[15][ListTable::ctAccount] == "Credit Card"); QVERIFY(rows[15][ListTable::ctCategory] == "Parent"); QVERIFY(rows[15][ListTable::ctPostDate] == "2005-09-01"); #else QVERIFY(rows.count() == 12); QVERIFY(rows[0][ListTable::ctAccount] == "Checking Account"); QVERIFY(rows[0][ListTable::ctCategory] == "Solo"); QVERIFY(rows[0][ListTable::ctPostDate] == "2004-01-01"); QVERIFY(rows[11][ListTable::ctAccount] == "Credit Card"); QVERIFY(rows[11][ListTable::ctCategory] == "Parent"); QVERIFY(rows[11][ListTable::ctPostDate] == "2005-09-01"); #endif QVERIFY(MyMoneyMoney(rows[5][ListTable::ctValue]) == -(moSolo) * 3 + moCheckingOpen); QVERIFY(MyMoneyMoney(rows[17][ListTable::ctValue]) == -(moParent1 + moParent2 + moChild) * 3 + moCreditOpen); QVERIFY(MyMoneyMoney(rows[18][ListTable::ctValue]) == -(moParent1 + moParent2 + moSolo + moChild) * 3 + moCheckingOpen + moCreditOpen); filter.setRowType(MyMoneyReport::ePayee); filter.setName("Transactions by Payee"); cols = MyMoneyReport::eQCnumber | MyMoneyReport::eQCmemo | MyMoneyReport::eQCcategory; filter.setQueryColumns(static_cast(cols)); // XMLandback(filter); QueryTable qtbl_4(filter); writeTabletoHTML(qtbl_4, "Transactions by Payee.html"); rows = qtbl_4.rows(); QVERIFY(rows.count() == 14); QVERIFY(rows[0][ListTable::ctPayee] == "Test Payee"); QVERIFY(rows[0][ListTable::ctCategory] == "Solo"); QVERIFY(rows[0][ListTable::ctPostDate] == "2004-01-01"); QVERIFY(rows[7][ListTable::ctPayee] == "Test Payee"); QVERIFY(rows[7][ListTable::ctCategory] == "Parent: Child"); QVERIFY(rows[7][ListTable::ctPostDate] == "2004-11-07"); QVERIFY(rows[11][ListTable::ctPayee] == "Test Payee"); QVERIFY(rows[11][ListTable::ctCategory] == "Parent"); QVERIFY(rows[11][ListTable::ctPostDate] == "2005-09-01"); QVERIFY(MyMoneyMoney(rows[12][ListTable::ctValue]) == -(moParent1 + moParent2 + moSolo + moChild) * 3); QVERIFY(MyMoneyMoney(rows[13][ListTable::ctValue]) == -(moParent1 + moParent2 + moSolo + moChild) * 3 + moCheckingOpen + moCreditOpen); filter.setRowType(MyMoneyReport::eMonth); filter.setName("Transactions by Month"); cols = MyMoneyReport::eQCnumber | MyMoneyReport::eQCpayee | MyMoneyReport::eQCcategory; filter.setQueryColumns(static_cast(cols)); // XMLandback(filter); QueryTable qtbl_5(filter); writeTabletoHTML(qtbl_5, "Transactions by Month.html"); rows = qtbl_5.rows(); QVERIFY(rows.count() == 23); QVERIFY(rows[0][ListTable::ctPayee] == "Test Payee"); QVERIFY(rows[0][ListTable::ctCategory] == "Solo"); QVERIFY(rows[0][ListTable::ctPostDate] == "2004-01-01"); QVERIFY(rows[12][ListTable::ctPayee] == "Test Payee"); QVERIFY(rows[12][ListTable::ctCategory] == "Parent: Child"); QVERIFY(rows[12][ListTable::ctPostDate] == "2004-11-07"); QVERIFY(rows[20][ListTable::ctPayee] == "Test Payee"); QVERIFY(rows[20][ListTable::ctCategory] == "Parent"); QVERIFY(rows[20][ListTable::ctPostDate] == "2005-09-01"); QVERIFY(MyMoneyMoney(rows[1][ListTable::ctValue]) == -moSolo); QVERIFY(MyMoneyMoney(rows[15][ListTable::ctValue]) == -(moChild) * 3); QVERIFY(MyMoneyMoney(rows[9][ListTable::ctValue]) == -moParent1 + moCheckingOpen); QVERIFY(MyMoneyMoney(rows[22][ListTable::ctValue]) == -(moParent1 + moParent2 + moSolo + moChild) * 3 + moCheckingOpen + moCreditOpen); filter.setRowType(MyMoneyReport::eWeek); filter.setName("Transactions by Week"); cols = MyMoneyReport::eQCnumber | MyMoneyReport::eQCpayee | MyMoneyReport::eQCcategory; filter.setQueryColumns(static_cast(cols)); // XMLandback(filter); QueryTable qtbl_6(filter); writeTabletoHTML(qtbl_6, "Transactions by Week.html"); rows = qtbl_6.rows(); QVERIFY(rows.count() == 23); QVERIFY(rows[0][ListTable::ctPayee] == "Test Payee"); QVERIFY(rows[0][ListTable::ctCategory] == "Solo"); QVERIFY(rows[0][ListTable::ctPostDate] == "2004-01-01"); QVERIFY(rows[20][ListTable::ctPayee] == "Test Payee"); QVERIFY(rows[20][ListTable::ctCategory] == "Parent"); QVERIFY(rows[20][ListTable::ctPostDate] == "2005-09-01"); QVERIFY(MyMoneyMoney(rows[1][ListTable::ctValue]) == -moSolo); QVERIFY(MyMoneyMoney(rows[15][ListTable::ctValue]) == -(moChild) * 3); QVERIFY(MyMoneyMoney(rows[21][ListTable::ctValue]) == -moParent2); QVERIFY(MyMoneyMoney(rows[22][ListTable::ctValue]) == -(moParent1 + moParent2 + moSolo + moChild) * 3 + moCheckingOpen + moCreditOpen); } catch (const MyMoneyException &e) { QFAIL(e.what()); } // Test querytable::TableRow::operator> and operator== QueryTable::TableRow low; low[ListTable::ctPrice] = 'A'; low[ListTable::ctLastPrice] = 'B'; low[ListTable::ctBuyPrice] = 'C'; QueryTable::TableRow high; high[ListTable::ctPrice] = 'A'; high[ListTable::ctLastPrice] = 'C'; high[ListTable::ctBuyPrice] = 'B'; QueryTable::TableRow::setSortCriteria({ListTable::ctPrice, ListTable::ctLastPrice, ListTable::ctBuyPrice}); QVERIFY(low < high); QVERIFY(low <= high); QVERIFY(high > low); QVERIFY(high <= high); QVERIFY(high == high); } void QueryTableTest::testCashFlowAnalysis() { // // Test IRR calculations // CashFlowList list; list += CashFlowListItem(QDate(2004, 5, 3), MyMoneyMoney(1000.0)); list += CashFlowListItem(QDate(2004, 5, 20), MyMoneyMoney(59.0)); list += CashFlowListItem(QDate(2004, 6, 3), MyMoneyMoney(14.0)); list += CashFlowListItem(QDate(2004, 6, 24), MyMoneyMoney(92.0)); list += CashFlowListItem(QDate(2004, 7, 6), MyMoneyMoney(63.0)); list += CashFlowListItem(QDate(2004, 7, 25), MyMoneyMoney(15.0)); list += CashFlowListItem(QDate(2004, 8, 5), MyMoneyMoney(92.0)); list += CashFlowListItem(QDate(2004, 9, 2), MyMoneyMoney(18.0)); list += CashFlowListItem(QDate(2004, 9, 21), MyMoneyMoney(5.0)); list += CashFlowListItem(QDate(2004, 10, 16), MyMoneyMoney(-2037.0)); MyMoneyMoney IRR(list.IRR(), 1000); QVERIFY(IRR == MyMoneyMoney(1676, 1000)); list.pop_back(); list += CashFlowListItem(QDate(2004, 10, 16), MyMoneyMoney(-1358.0)); IRR = MyMoneyMoney(list.IRR(), 1000); QVERIFY(IRR.isZero()); } void QueryTableTest::testAccountQuery() { try { QString htmlcontext = QString("\n\n%1\n\n"); // // No transactions, opening balances only // MyMoneyReport filter; filter.setRowType(MyMoneyReport::eInstitution); filter.setName("Accounts by Institution (No transactions)"); XMLandback(filter); QueryTable qtbl_1(filter); writeTabletoHTML(qtbl_1, "Accounts by Institution (No transactions).html"); QList rows = qtbl_1.rows(); QVERIFY(rows.count() == 6); QVERIFY(rows[0][ListTable::ctAccount] == "Checking Account"); QVERIFY(MyMoneyMoney(rows[0][ListTable::ctValue]) == moCheckingOpen); QVERIFY(rows[0][ListTable::ctEquityType].isEmpty()); QVERIFY(rows[2][ListTable::ctAccount] == "Credit Card"); QVERIFY(MyMoneyMoney(rows[1][ListTable::ctValue]) == moCreditOpen); QVERIFY(rows[2][ListTable::ctEquityType].isEmpty()); QVERIFY(MyMoneyMoney(rows[4][ListTable::ctValue]) == moCheckingOpen + moCreditOpen); QVERIFY(MyMoneyMoney(rows[5][ListTable::ctValue]) == moCheckingOpen + moCreditOpen); // // Adding in transactions // TransactionHelper t1q1(QDate(2004, 1, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moSolo, acChecking, acSolo); TransactionHelper t2q1(QDate(2004, 2, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent1, acCredit, acParent); TransactionHelper t3q1(QDate(2004, 3, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent2, acCredit, acParent); TransactionHelper t4y1(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moChild, acCredit, acChild); TransactionHelper t1q2(QDate(2004, 4, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moSolo, acChecking, acSolo); TransactionHelper t2q2(QDate(2004, 5, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent1, acCredit, acParent); TransactionHelper t3q2(QDate(2004, 6, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent2, acCredit, acParent); TransactionHelper t4q2(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moChild, acCredit, acChild); TransactionHelper t1y2(QDate(2005, 1, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moSolo, acChecking, acSolo); TransactionHelper t2y2(QDate(2005, 5, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent1, acCredit, acParent); TransactionHelper t3y2(QDate(2005, 9, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent2, acCredit, acParent); TransactionHelper t4y2(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moChild, acCredit, acChild); filter.setRowType(MyMoneyReport::eInstitution); filter.setName("Accounts by Institution (With Transactions)"); XMLandback(filter); QueryTable qtbl_2(filter); rows = qtbl_2.rows(); QVERIFY(rows.count() == 6); QVERIFY(rows[0][ListTable::ctAccount] == "Checking Account"); QVERIFY(MyMoneyMoney(rows[0][ListTable::ctValue]) == (moCheckingOpen - moSolo*3)); QVERIFY(rows[2][ListTable::ctAccount] == "Credit Card"); QVERIFY(MyMoneyMoney(rows[2][ListTable::ctValue]) == (moCreditOpen - (moParent1 + moParent2 + moChild) * 3)); QVERIFY(MyMoneyMoney(rows[5][ListTable::ctValue]) == moCheckingOpen + moCreditOpen - (moParent1 + moParent2 + moSolo + moChild) * 3); // // Account TYPES // filter.setRowType(MyMoneyReport::eAccountType); filter.setName("Accounts by Type"); XMLandback(filter); QueryTable qtbl_3(filter); rows = qtbl_3.rows(); QVERIFY(rows.count() == 5); QVERIFY(rows[0][ListTable::ctAccount] == "Checking Account"); QVERIFY(MyMoneyMoney(rows[0][ListTable::ctValue]) == (moCheckingOpen - moSolo * 3)); QVERIFY(rows[2][ListTable::ctAccount] == "Credit Card"); QVERIFY(MyMoneyMoney(rows[2][ListTable::ctValue]) == (moCreditOpen - (moParent1 + moParent2 + moChild) * 3)); QVERIFY(MyMoneyMoney(rows[1][ListTable::ctValue]) == moCheckingOpen - moSolo * 3); QVERIFY(MyMoneyMoney(rows[3][ListTable::ctValue]) == moCreditOpen - (moParent1 + moParent2 + moChild) * 3); QVERIFY(MyMoneyMoney(rows[4][ListTable::ctValue]) == moCheckingOpen + moCreditOpen - (moParent1 + moParent2 + moSolo + moChild) * 3); } catch (const MyMoneyException &e) { QFAIL(e.what()); } } void QueryTableTest::testInvestment() { try { // Equities eqStock1 = makeEquity("Stock1", "STK1"); eqStock2 = makeEquity("Stock2", "STK2"); eqStock3 = makeEquity("Stock3", "STK3"); eqStock4 = makeEquity("Stock4", "STK4"); // Accounts acInvestment = makeAccount("Investment", eMyMoney::Account::Type::Investment, moZero, QDate(2003, 11, 1), acAsset); acStock1 = makeAccount("Stock 1", eMyMoney::Account::Type::Stock, moZero, QDate(2004, 1, 1), acInvestment, eqStock1); acStock2 = makeAccount("Stock 2", eMyMoney::Account::Type::Stock, moZero, QDate(2004, 1, 1), acInvestment, eqStock2); acStock3 = makeAccount("Stock 3", eMyMoney::Account::Type::Stock, moZero, QDate(2003, 11, 1), acInvestment, eqStock3); acStock4 = makeAccount("Stock 4", eMyMoney::Account::Type::Stock, moZero, QDate(2004, 1, 1), acInvestment, eqStock4); acDividends = makeAccount("Dividends", eMyMoney::Account::Type::Income, moZero, QDate(2004, 1, 1), acIncome); acInterest = makeAccount("Interest", eMyMoney::Account::Type::Income, moZero, QDate(2004, 1, 1), acIncome); acFees = makeAccount("Fees", eMyMoney::Account::Type::Expense, moZero, QDate(2003, 11, 1), acExpense); // Transactions // Date Action Shares Price Stock Asset Income InvTransactionHelper s1b1(QDate(2003, 12, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::BuyShares), MyMoneyMoney(1000.00), MyMoneyMoney(100.00), acStock3, acChecking, QString()); InvTransactionHelper s1b2(QDate(2004, 1, 30), MyMoneySplit::actionName(eMyMoney::Split::Action::BuyShares), MyMoneyMoney(500.00), MyMoneyMoney(100.00), acStock4, acChecking, acFees, MyMoneyMoney(100.00)); InvTransactionHelper s1b3(QDate(2004, 1, 30), MyMoneySplit::actionName(eMyMoney::Split::Action::BuyShares), MyMoneyMoney(500.00), MyMoneyMoney(90.00), acStock4, acChecking, acFees, MyMoneyMoney(100.00)); InvTransactionHelper s1b4(QDate(2004, 2, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::BuyShares), MyMoneyMoney(1000.00), MyMoneyMoney(100.00), acStock1, acChecking, QString()); InvTransactionHelper s1b5(QDate(2004, 3, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::BuyShares), MyMoneyMoney(1000.00), MyMoneyMoney(110.00), acStock1, acChecking, QString()); InvTransactionHelper s1s1(QDate(2004, 4, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::BuyShares), MyMoneyMoney(-200.00), MyMoneyMoney(120.00), acStock1, acChecking, QString()); InvTransactionHelper s1s2(QDate(2004, 5, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::BuyShares), MyMoneyMoney(-200.00), MyMoneyMoney(100.00), acStock1, acChecking, QString()); InvTransactionHelper s1s3(QDate(2004, 5, 30), MyMoneySplit::actionName(eMyMoney::Split::Action::BuyShares), MyMoneyMoney(-1000.00), MyMoneyMoney(120.00), acStock4, acChecking, acFees, MyMoneyMoney(200.00)); InvTransactionHelper s1r1(QDate(2004, 6, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::ReinvestDividend), MyMoneyMoney(50.00), MyMoneyMoney(100.00), acStock1, QString(), acDividends); InvTransactionHelper s1r2(QDate(2004, 7, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::ReinvestDividend), MyMoneyMoney(50.00), MyMoneyMoney(80.00), acStock1, QString(), acDividends); InvTransactionHelper s1c1(QDate(2004, 8, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Dividend), MyMoneyMoney(10.00), MyMoneyMoney(100.00), acStock1, acChecking, acDividends); InvTransactionHelper s1c2(QDate(2004, 9, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Dividend), MyMoneyMoney(10.00), MyMoneyMoney(120.00), acStock1, acChecking, acDividends); InvTransactionHelper s1y1(QDate(2004, 9, 15), MyMoneySplit::actionName(eMyMoney::Split::Action::Yield), MyMoneyMoney(10.00), MyMoneyMoney(110.00), acStock1, acChecking, acInterest); makeEquityPrice(eqStock1, QDate(2004, 10, 1), MyMoneyMoney(100.00)); makeEquityPrice(eqStock3, QDate(2004, 10, 1), MyMoneyMoney(110.00)); makeEquityPrice(eqStock4, QDate(2004, 10, 1), MyMoneyMoney(110.00)); // // Investment Transactions Report // MyMoneyReport invtran_r( MyMoneyReport::eTopAccount, MyMoneyReport::eQCaction | MyMoneyReport::eQCshares | MyMoneyReport::eQCprice, eMyMoney::TransactionFilter::Date::UserDefined, MyMoneyReport::eDetailAll, i18n("Investment Transactions"), i18n("Test Report") ); invtran_r.setDateFilter(QDate(2004, 1, 1), QDate(2004, 12, 31)); invtran_r.setInvestmentsOnly(true); XMLandback(invtran_r); QueryTable invtran(invtran_r); #if 1 writeTabletoHTML(invtran, "investment_transactions_test.html"); QList rows = invtran.rows(); QVERIFY(rows.count() == 32); QVERIFY(MyMoneyMoney(rows[1][ListTable::ctValue]) == MyMoneyMoney(-100000.00)); QVERIFY(MyMoneyMoney(rows[2][ListTable::ctValue]) == MyMoneyMoney(-110000.00)); QVERIFY(MyMoneyMoney(rows[3][ListTable::ctValue]) == MyMoneyMoney(24000.00)); QVERIFY(MyMoneyMoney(rows[4][ListTable::ctValue]) == MyMoneyMoney(20000.00)); QVERIFY(MyMoneyMoney(rows[5][ListTable::ctValue]) == MyMoneyMoney(5000.00)); QVERIFY(MyMoneyMoney(rows[6][ListTable::ctValue]) == MyMoneyMoney(4000.00)); QVERIFY(MyMoneyMoney(rows[19][ListTable::ctValue]) == MyMoneyMoney(-50100.00)); QVERIFY(MyMoneyMoney(rows[22][ListTable::ctValue]) == MyMoneyMoney(-45100.00)); // need to fix these... fundamentally different from the original test //QVERIFY(MyMoneyMoney(invtran.m_rows[8][ListTable::ctValue])==MyMoneyMoney( -1000.00)); //QVERIFY(MyMoneyMoney(invtran.m_rows[11][ListTable::ctValue])==MyMoneyMoney( -1200.00)); //QVERIFY(MyMoneyMoney(invtran.m_rows[14][ListTable::ctValue])==MyMoneyMoney( -1100.00)); QVERIFY(MyMoneyMoney(rows[1][ListTable::ctPrice]) == MyMoneyMoney(100.00)); QVERIFY(MyMoneyMoney(rows[3][ListTable::ctPrice]) == MyMoneyMoney(120.00)); QVERIFY(MyMoneyMoney(rows[5][ListTable::ctPrice]) == MyMoneyMoney(100.00)); QVERIFY(MyMoneyMoney(rows[7][ListTable::ctPrice]) == MyMoneyMoney()); QVERIFY(MyMoneyMoney(rows[10][ListTable::ctPrice]) == MyMoneyMoney()); QVERIFY(MyMoneyMoney(rows[19][ListTable::ctPrice]) == MyMoneyMoney(100.00)); QVERIFY(MyMoneyMoney(rows[22][ListTable::ctPrice]) == MyMoneyMoney(90.00)); QVERIFY(MyMoneyMoney(rows[2][ListTable::ctShares]) == MyMoneyMoney(1000.00)); QVERIFY(MyMoneyMoney(rows[4][ListTable::ctShares]) == MyMoneyMoney(-200.00)); QVERIFY(MyMoneyMoney(rows[6][ListTable::ctShares]) == MyMoneyMoney(50.00)); QVERIFY(MyMoneyMoney(rows[8][ListTable::ctShares]) == MyMoneyMoney(0.00)); QVERIFY(MyMoneyMoney(rows[11][ListTable::ctShares]) == MyMoneyMoney(0.00)); QVERIFY(MyMoneyMoney(rows[19][ListTable::ctShares]) == MyMoneyMoney(500.00)); QVERIFY(MyMoneyMoney(rows[22][ListTable::ctShares]) == MyMoneyMoney(500.00)); QVERIFY(rows[1][ListTable::ctAction] == "Buy"); QVERIFY(rows[3][ListTable::ctAction] == "Sell"); QVERIFY(rows[5][ListTable::ctAction] == "Reinvest"); QVERIFY(rows[7][ListTable::ctAction] == "Dividend"); QVERIFY(rows[13][ListTable::ctAction] == "Yield"); QVERIFY(rows[19][ListTable::ctAction] == "Buy"); QVERIFY(rows[22][ListTable::ctAction] == "Buy"); #else QVERIFY(rows.count() == 9); QVERIFY(MyMoneyMoney(rows[0][ListTable::ctValue]) == MyMoneyMoney(100000.00)); QVERIFY(MyMoneyMoney(rows[1][ListTable::ctValue]) == MyMoneyMoney(110000.00)); QVERIFY(MyMoneyMoney(rows[2][ListTable::ctValue]) == MyMoneyMoney(-24000.00)); QVERIFY(MyMoneyMoney(rows[3][ListTable::ctValue]) == MyMoneyMoney(-20000.00)); QVERIFY(MyMoneyMoney(rows[4][ListTable::ctValue]) == MyMoneyMoney(5000.00)); QVERIFY(MyMoneyMoney(rows[5][ListTable::ctValue]) == MyMoneyMoney(4000.00)); QVERIFY(MyMoneyMoney(rows[6][ListTable::ctValue]) == MyMoneyMoney(-1000.00)); QVERIFY(MyMoneyMoney(rows[7][ListTable::ctValue]) == MyMoneyMoney(-1200.00)); QVERIFY(MyMoneyMoney(rows[8][ListTable::ctValue]) == MyMoneyMoney(-1100.00)); QVERIFY(MyMoneyMoney(rows[0][ListTable::ctPrice]) == MyMoneyMoney(100.00)); QVERIFY(MyMoneyMoney(rows[2][ListTable::ctPrice]) == MyMoneyMoney(120.00)); QVERIFY(MyMoneyMoney(rows[4][ListTable::ctPrice]) == MyMoneyMoney(100.00)); QVERIFY(MyMoneyMoney(rows[6][ListTable::ctPrice]) == MyMoneyMoney(0.00)); QVERIFY(MyMoneyMoney(rows[8][ListTable::ctPrice]) == MyMoneyMoney(0.00)); QVERIFY(MyMoneyMoney(rows[1][ListTable::ctShares]) == MyMoneyMoney(1000.00)); QVERIFY(MyMoneyMoney(rows[3][ListTable::ctShares]) == MyMoneyMoney(-200.00)); QVERIFY(MyMoneyMoney(rows[5][ListTable::ctShares]) == MyMoneyMoney(50.00)); QVERIFY(MyMoneyMoney(rows[7][ListTable::ctShares]) == MyMoneyMoney(0.00)); QVERIFY(MyMoneyMoney(rows[8][ListTable::ctShares]) == MyMoneyMoney(0.00)); QVERIFY(rows[0][ListTable::ctAction] == "Buy"); QVERIFY(rows[2][ListTable::ctAction] == "Sell"); QVERIFY(rows[4][ListTable::ctAction] == "Reinvest"); QVERIFY(rows[6][ListTable::ctAction] == "Dividend"); QVERIFY(rows[8][ListTable::ctAction] == "Yield"); #endif #if 1 // i think this is the correct amount. different treatment of dividend and yield QVERIFY(MyMoneyMoney(rows[17][ListTable::ctValue]) == MyMoneyMoney(-153700.00)); QVERIFY(MyMoneyMoney(rows[29][ListTable::ctValue]) == MyMoneyMoney(24600.00)); QVERIFY(MyMoneyMoney(rows[31][ListTable::ctValue]) == MyMoneyMoney(-129100.00)); #else QVERIFY(searchHTML(html, i18n("Total Stock 1")) == MyMoneyMoney(171700.00)); QVERIFY(searchHTML(html, i18n("Grand Total")) == MyMoneyMoney(171700.00)); #endif // // Investment Performance Report // MyMoneyReport invhold_r( MyMoneyReport::eAccountByTopAccount, MyMoneyReport::eQCperformance, eMyMoney::TransactionFilter::Date::UserDefined, MyMoneyReport::eDetailAll, i18n("Investment Performance by Account"), i18n("Test Report") ); invhold_r.setDateFilter(QDate(2004, 1, 1), QDate(2004, 10, 1)); invhold_r.setInvestmentsOnly(true); XMLandback(invhold_r); QueryTable invhold(invhold_r); writeTabletoHTML(invhold, "Investment Performance by Account.html"); rows = invhold.rows(); QVERIFY(rows.count() == 5); QVERIFY(MyMoneyMoney(rows[0][ListTable::ctReturn]) == MyMoneyMoney("669/10000")); QVERIFY(MyMoneyMoney(rows[0][ListTable::ctReturnInvestment]) == MyMoneyMoney("-39/5000")); QVERIFY(MyMoneyMoney(rows[0][ListTable::ctBuys]) == MyMoneyMoney(-210000.00)); QVERIFY(MyMoneyMoney(rows[0][ListTable::ctSells]) == MyMoneyMoney(44000.00)); QVERIFY(MyMoneyMoney(rows[0][ListTable::ctReinvestIncome]) == MyMoneyMoney(9000.00)); QVERIFY(MyMoneyMoney(rows[0][ListTable::ctCashIncome]) == MyMoneyMoney(3300.00)); QVERIFY(MyMoneyMoney(rows[1][ListTable::ctReturn]) == MyMoneyMoney("1349/10000")); QVERIFY(MyMoneyMoney(rows[1][ListTable::ctReturnInvestment]) == MyMoneyMoney("1/10")); QVERIFY(MyMoneyMoney(rows[1][ListTable::ctStartingBalance]) == MyMoneyMoney(100000.00)); // this should stay non-zero to check if investment performance is calculated at non-zero starting balance QVERIFY(MyMoneyMoney(rows[2][ListTable::ctReturn]) == MyMoneyMoney("2501/2500")); QVERIFY(MyMoneyMoney(rows[2][ListTable::ctReturnInvestment]) == MyMoneyMoney("323/1250")); QVERIFY(MyMoneyMoney(rows[2][ListTable::ctBuys]) == MyMoneyMoney(-95200.00)); QVERIFY(MyMoneyMoney(rows[2][ListTable::ctSells]) == MyMoneyMoney(119800.00)); QVERIFY(MyMoneyMoney(rows[2][ListTable::ctEndingBalance]) == MyMoneyMoney(0.00)); // this should stay zero to check if investment performance is calculated at zero ending balance QVERIFY(MyMoneyMoney(rows[4][ListTable::ctEndingBalance]) == MyMoneyMoney(280000.00)); #if 0 // Dump file & reports QFile g("investmentkmy.xml"); g.open(QIODevice::WriteOnly); MyMoneyStorageXML xml; IMyMoneyOperationsFormat& interface = xml; interface.writeFile(&g, dynamic_cast(MyMoneyFile::instance()->storage())); g.close(); invtran.dump("invtran.html", "%1"); invhold.dump("invhold.html", "%1"); #endif } catch (const MyMoneyException &e) { QFAIL(e.what()); } } // prevents bug #312135 void QueryTableTest::testSplitShares() { try { MyMoneyMoney firstSharesPurchase(16); MyMoneyMoney splitFactor(2); MyMoneyMoney secondSharesPurchase(1); MyMoneyMoney sharesAtTheEnd = firstSharesPurchase / splitFactor + secondSharesPurchase; MyMoneyMoney priceBeforeSplit(74.99, 100); MyMoneyMoney priceAfterSplit = splitFactor * priceBeforeSplit; // Equities eqStock1 = makeEquity("Stock1", "STK1"); // Accounts acInvestment = makeAccount("Investment", eMyMoney::Account::Type::Investment, moZero, QDate(2017, 8, 1), acAsset); acStock1 = makeAccount("Stock 1", eMyMoney::Account::Type::Stock, moZero, QDate(2017, 8, 1), acInvestment, eqStock1); // Transactions // Date Action Shares Price Stock Asset Income InvTransactionHelper s1b1(QDate(2017, 8, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::BuyShares), firstSharesPurchase, priceBeforeSplit, acStock1, acChecking, QString()); InvTransactionHelper s1s1(QDate(2017, 8, 2), MyMoneySplit::actionName(eMyMoney::Split::Action::SplitShares), splitFactor, MyMoneyMoney(), acStock1, QString(), QString()); InvTransactionHelper s1b2(QDate(2017, 8, 3), MyMoneySplit::actionName(eMyMoney::Split::Action::BuyShares), secondSharesPurchase, priceAfterSplit, acStock1, acChecking, QString()); // // Investment Performance Report // MyMoneyReport invhold_r( MyMoneyReport::eAccountByTopAccount, MyMoneyReport::eQCperformance, eMyMoney::TransactionFilter::Date::UserDefined, MyMoneyReport::eDetailAll, i18n("Investment Performance by Account"), i18n("Test Report") ); invhold_r.setDateFilter(QDate(2017, 8, 1), QDate(2017, 8, 3)); invhold_r.setInvestmentsOnly(true); XMLandback(invhold_r); QueryTable invhold(invhold_r); writeTabletoHTML(invhold, "Investment Performance by Account (with stock split).html"); const auto rows = invhold.rows(); QVERIFY(rows.count() == 3); QVERIFY(MyMoneyMoney(rows[0][ListTable::ctBuys]) == sharesAtTheEnd * priceAfterSplit * MyMoneyMoney(-1)); } catch (const MyMoneyException &e) { QFAIL(e.what()); } } // prevents bug #118159 void QueryTableTest::testConversionRate() { try { MyMoneyMoney firsConversionRate(1.1800, 10000); MyMoneyMoney secondConversionRate(1.1567, 10000); MyMoneyMoney amountWithdrawn(100); const auto acCadChecking = makeAccount(QString("Canadian Checking"), eMyMoney::Account::Type::Checkings, moZero, QDate(2017, 8, 1), acAsset, "CAD"); makePrice("CAD", QDate(2017, 8, 1), firsConversionRate); makePrice("CAD", QDate(2017, 8, 2), secondConversionRate); TransactionHelper t1(QDate(2017, 8, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), amountWithdrawn, acCadChecking, acSolo, "CAD"); MyMoneyReport filter; filter.setRowType(MyMoneyReport::eAccount); filter.setDateFilter(QDate(2017, 8, 1), QDate(2017, 8, 2)); filter.setName("Transactions by Account"); auto cols = MyMoneyReport::eQCnumber | MyMoneyReport::eQCpayee | MyMoneyReport::eQCcategory | MyMoneyReport::eQCbalance; filter.setQueryColumns(static_cast(cols)); // XMLandback(filter); QueryTable qtbl(filter); writeTabletoHTML(qtbl, "Transactions by Account (conversion rate).html"); const auto rows = qtbl.rows(); QVERIFY(rows.count() == 5); QVERIFY(MyMoneyMoney(rows[1][ListTable::ctValue]) == amountWithdrawn * firsConversionRate * MyMoneyMoney(-1)); QVERIFY(MyMoneyMoney(rows[1][ListTable::ctPrice]) == firsConversionRate); QVERIFY(MyMoneyMoney(rows[2][ListTable::ctBalance]) == amountWithdrawn * secondConversionRate * MyMoneyMoney(-1)); QVERIFY(MyMoneyMoney(rows[2][ListTable::ctPrice]) == secondConversionRate); } catch (const MyMoneyException &e) { QFAIL(e.what()); } } //this is to prevent me from making mistakes again when modifying balances - asoliverez //this case tests only the opening and ending balance of the accounts void QueryTableTest::testBalanceColumn() { try { TransactionHelper t1q1(QDate(2004, 1, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moSolo, acChecking, acSolo); TransactionHelper t2q1(QDate(2004, 2, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent1, acCredit, acParent); TransactionHelper t3q1(QDate(2004, 3, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent2, acCredit, acParent); TransactionHelper t4y1(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moChild, acCredit, acChild); TransactionHelper t1q2(QDate(2004, 4, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moSolo, acChecking, acSolo); TransactionHelper t2q2(QDate(2004, 5, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent1, acCredit, acParent); TransactionHelper t3q2(QDate(2004, 6, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent2, acCredit, acParent); TransactionHelper t4q2(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moChild, acCredit, acChild); TransactionHelper t1y2(QDate(2005, 1, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moSolo, acChecking, acSolo); TransactionHelper t2y2(QDate(2005, 5, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent1, acCredit, acParent); TransactionHelper t3y2(QDate(2005, 9, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent2, acCredit, acParent); TransactionHelper t4y2(QDate(2004, 11, 7), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moChild, acCredit, acChild); unsigned cols; MyMoneyReport filter; filter.setRowType(MyMoneyReport::eAccount); filter.setName("Transactions by Account"); cols = MyMoneyReport::eQCnumber | MyMoneyReport::eQCpayee | MyMoneyReport::eQCcategory | MyMoneyReport::eQCbalance; filter.setQueryColumns(static_cast(cols)); // XMLandback(filter); QueryTable qtbl_3(filter); writeTabletoHTML(qtbl_3, "Transactions by Account.html"); QString html = qtbl_3.renderHTML(); QList rows = qtbl_3.rows(); QVERIFY(rows.count() == 19); //this is to make sure that the dates of closing and opening balances and the balance numbers are ok QString openingDate = QLocale().toString(QDate(2004, 1, 1), QLocale::ShortFormat); QString closingDate = QLocale().toString(QDate(2005, 9, 1), QLocale::ShortFormat); QVERIFY(html.indexOf(openingDate + "" + i18n("Opening Balance")) > 0); QVERIFY(html.indexOf(closingDate + "" + i18n("Closing Balance") + " -702.36") > 0); QVERIFY(html.indexOf(closingDate + "" + i18n("Closing Balance") + " -705.69") > 0); } catch (const MyMoneyException &e) { QFAIL(e.what()); } } void QueryTableTest::testBalanceColumnWithMultipleCurrencies() { try { MyMoneyMoney moJpyOpening(0.0, 1); MyMoneyMoney moJpyPrice(0.010, 100); MyMoneyMoney moJpyPrice2(0.011, 100); MyMoneyMoney moJpyPrice3(0.024, 100); MyMoneyMoney moTransaction(100, 1); MyMoneyMoney moJpyTransaction(100, 1); QString acJpyChecking = makeAccount(QString("Japanese Checking"), eMyMoney::Account::Type::Checkings, moJpyOpening, QDate(2003, 11, 15), acAsset, "JPY"); makePrice("JPY", QDate(2004, 1, 1), MyMoneyMoney(moJpyPrice)); makePrice("JPY", QDate(2004, 5, 1), MyMoneyMoney(moJpyPrice2)); makePrice("JPY", QDate(2004, 6, 30), MyMoneyMoney(moJpyPrice3)); QDate openingDate(2004, 2, 20); QDate intermediateDate(2004, 5, 20); QDate closingDate(2004, 7, 20); TransactionHelper t1(openingDate, MyMoneySplit::actionName(eMyMoney::Split::Action::Transfer), MyMoneyMoney(moJpyTransaction), acJpyChecking, acChecking, "JPY"); TransactionHelper t4(openingDate, MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit), MyMoneyMoney(moTransaction), acCredit, acChecking); TransactionHelper t2(intermediateDate, MyMoneySplit::actionName(eMyMoney::Split::Action::Transfer), MyMoneyMoney(moJpyTransaction), acJpyChecking, acChecking, "JPY"); TransactionHelper t5(intermediateDate, MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit), MyMoneyMoney(moTransaction), acCredit, acChecking); TransactionHelper t3(closingDate, MyMoneySplit::actionName(eMyMoney::Split::Action::Transfer), MyMoneyMoney(moJpyTransaction), acJpyChecking, acChecking, "JPY"); TransactionHelper t6(closingDate, MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit), MyMoneyMoney(moTransaction), acCredit, acChecking); // test that an income/expense transaction that involves a currency exchange is properly reported TransactionHelper t7(intermediateDate, MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), MyMoneyMoney(moJpyTransaction), acJpyChecking, acSolo, "JPY"); unsigned cols; MyMoneyReport filter; filter.setRowType(MyMoneyReport::eAccount); filter.setName("Transactions by Account"); cols = MyMoneyReport::eQCnumber | MyMoneyReport::eQCpayee | MyMoneyReport::eQCcategory | MyMoneyReport::eQCbalance; filter.setQueryColumns(static_cast(cols)); // don't convert values to the default currency filter.setConvertCurrency(false); XMLandback(filter); QueryTable qtbl_3(filter); writeTabletoHTML(qtbl_3, "Transactions by Account (multiple currencies).html"); QString html = qtbl_3.renderHTML(); QList rows = qtbl_3.rows(); QVERIFY(rows.count() == 24); //this is to make sure that the dates of closing and opening balances and the balance numbers are ok QString openingDateString = QLocale().toString(openingDate, QLocale::ShortFormat); QString intermediateDateString = QLocale().toString(intermediateDate, QLocale::ShortFormat); QString closingDateString = QLocale().toString(closingDate, QLocale::ShortFormat); // check the opening and closing balances QVERIFY(html.indexOf(openingDateString + "" + i18n("Opening Balance") + "USD 0.00") > 0); QVERIFY(html.indexOf(closingDateString + "" + i18n("Closing Balance") + "USD 304.00") > 0); QVERIFY(html.indexOf(closingDateString + "" + i18n("Closing Balance") + "USD -300.00") > 0); QVERIFY(html.indexOf(closingDateString + "" + i18n("Closing Balance") + "JPY -400.00") > 0); // after a transfer of 100 JPY the balance should be 1.00 - price is 0.010 (precision of 2) QVERIFY(html.indexOf("" + openingDateString + "Test PayeeTransfer from Japanese CheckingUSD 1.00USD 1.00") > 0); // after a transfer of 100 the balance should be 101.00 QVERIFY(html.indexOf("" + openingDateString + "Test PayeeTransfer from Credit CardUSD 100.00USD 101.00") > 0); // after a transfer of 100 JPY the balance should be 102.00 - price is 0.011 (precision of 2) QVERIFY(html.indexOf("" + intermediateDateString + "Test PayeeTransfer from Japanese CheckingUSD 1.00USD 102.00") > 0); // after a transfer of 100 the balance should be 202.00 QVERIFY(html.indexOf("" + intermediateDateString + "Test PayeeTransfer from Credit CardUSD 100.00USD 202.00") > 0); // after a transfer of 100 JPY the balance should be 204.00 - price is 0.024 (precision of 2) QVERIFY(html.indexOf("" + closingDateString + "Test PayeeTransfer from Japanese CheckingUSD 2.00USD 204.00") > 0); // after a transfer of 100 the balance should be 304.00 QVERIFY(html.indexOf("" + closingDateString + "Test PayeeTransfer from Credit CardUSD 100.00USD 304.00") > 0); // a 100.00 JPY withdrawal should be displayed as such even if the expense account uses another currency QVERIFY(html.indexOf("" + intermediateDateString + "Test PayeeSoloJPY -100.00JPY -300.00") > 0); // now run the same report again but this time convert all values to the base currency and make sure the values are correct filter.setConvertCurrency(true); XMLandback(filter); QueryTable qtbl_4(filter); writeTabletoHTML(qtbl_4, "Transactions by Account (multiple currencies converted to base).html"); html = qtbl_4.renderHTML(); rows = qtbl_4.rows(); QVERIFY(rows.count() == 23); // check the opening and closing balances QVERIFY(html.indexOf(openingDateString + "" + i18n("Opening Balance") + " 0.00") > 0); QVERIFY(html.indexOf(closingDateString + "" + i18n("Closing Balance") + " 304.00") > 0); QVERIFY(html.indexOf(closingDateString + "" + i18n("Closing Balance") + " -300.00") > 0); // although the balance should be -5.00 it's -8.00 because the foreign currency balance is converted using the closing date price (0.024) QVERIFY(html.indexOf(closingDateString + "" + i18n("Closing Balance") + " -8.00") > 0); // a 100.00 JPY transfer should be displayed as -1.00 when converted to the base currency using the opening date price QVERIFY(html.indexOf("" + openingDateString + "Test PayeeTransfer to Checking Account -1.00 -1.00") > 0); // a 100.00 JPY transfer should be displayed as -1.00 when converted to the base currency using the intermediate date price QVERIFY(html.indexOf("" + intermediateDateString + "Test PayeeTransfer to Checking Account -1.00 -2.00") > 0); // a 100.00 JPY transfer should be displayed as -2.00 when converted to the base currency using the closing date price (notice the balance is -5.00) QVERIFY(html.indexOf("" + closingDateString + "Test PayeeTransfer to Checking Account -2.00 -5.00") > 0); // a 100.00 JPY withdrawal should be displayed as -1.00 when converted to the base currency using the intermediate date price QVERIFY(html.indexOf("" + intermediateDateString + "Test PayeeSolo -1.00 -3.00") > 0); } catch (const MyMoneyException &e) { QFAIL(e.what()); } } void QueryTableTest::testTaxReport() { try { TransactionHelper t1q1(QDate(2004, 1, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moSolo, acChecking, acSolo); TransactionHelper t2q1(QDate(2004, 2, 1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moParent1, acChecking, acTax); unsigned cols; MyMoneyReport filter; filter.setRowType(MyMoneyReport::eCategory); filter.setName("Tax Transactions"); cols = MyMoneyReport::eQCnumber | MyMoneyReport::eQCpayee | MyMoneyReport::eQCaccount; filter.setQueryColumns(static_cast(cols)); filter.setTax(true); XMLandback(filter); QueryTable qtbl_3(filter); writeTabletoHTML(qtbl_3, "Tax Transactions.html"); QList rows = qtbl_3.rows(); QString html = qtbl_3.renderHTML(); QVERIFY(rows.count() == 5); } catch (const MyMoneyException &e) { QFAIL(e.what()); } } diff --git a/kmymoney/reports/tests/reportstestcommon.cpp b/kmymoney/reports/tests/reportstestcommon.cpp index 739bad75c..2306b3683 100644 --- a/kmymoney/reports/tests/reportstestcommon.cpp +++ b/kmymoney/reports/tests/reportstestcommon.cpp @@ -1,497 +1,496 @@ /*************************************************************************** reportstestcommon.cpp ------------------- copyright : (C) 2002-2005 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 "reportstestcommon.h" #include "kreportsview-test.h" #include #include #include #include #include #include #include "pivottable.h" #include "querytable.h" using namespace reports; #include "mymoneyexception.h" #include "mymoneymoney.h" #include "mymoneysecurity.h" #include "mymoneyprice.h" #include "mymoneysplit.h" #include "mymoneystoragedump.h" #include "mymoneyreport.h" #include "mymoneypayee.h" #include "mymoneystatement.h" -#include "mymoneystoragexml.h" namespace test { const MyMoneyMoney moCheckingOpen(0.0); const MyMoneyMoney moCreditOpen(-0.0); const MyMoneyMoney moConverterCheckingOpen(1418.0); const MyMoneyMoney moConverterCreditOpen(-418.0); const MyMoneyMoney moZero(0.0); const MyMoneyMoney moSolo(234.12); const MyMoneyMoney moParent1(88.01); const MyMoneyMoney moParent2(133.22); const MyMoneyMoney moParent(moParent1 + moParent2); const MyMoneyMoney moChild(14.00); const MyMoneyMoney moThomas(5.11); const MyMoneyMoney moNoPayee(8944.70); QString acAsset; QString acLiability; QString acExpense; QString acIncome; QString acChecking; QString acCredit; QString acSolo; QString acParent; QString acChild; QString acSecondChild; QString acGrandChild1; QString acGrandChild2; QString acForeign; QString acCanChecking; QString acJpyChecking; QString acCanCash; QString acJpyCash; QString inBank; QString eqStock1; QString eqStock2; QString eqStock3; QString eqStock4; QString acInvestment; QString acStock1; QString acStock2; QString acStock3; QString acStock4; QString acDividends; QString acInterest; QString acFees; QString acTax; QString acCash; TransactionHelper::TransactionHelper(const QDate& _date, const QString& _action, MyMoneyMoney _value, const QString& _accountid, const QString& _categoryid, const QString& _currencyid, const QString& _payee) { // _currencyid is the currency of the transaction, and of the _value // both the account and category can have their own currency (athough the category having // a foreign currency is not yet supported by the program, the reports will still allow it, // so it must be tested.) MyMoneyFile* file = MyMoneyFile::instance(); bool haspayee = ! _payee.isEmpty(); MyMoneyPayee payeeTest = file->payeeByName(_payee); MyMoneyFileTransaction ft; setPostDate(_date); QString currencyid = _currencyid; if (currencyid.isEmpty()) currencyid = MyMoneyFile::instance()->baseCurrency().id(); setCommodity(currencyid); MyMoneyMoney price; MyMoneySplit splitLeft; if (haspayee) splitLeft.setPayeeId(payeeTest.id()); splitLeft.setAction(_action); splitLeft.setValue(-_value); price = MyMoneyFile::instance()->price(currencyid, file->account(_accountid).currencyId(), _date).rate(file->account(_accountid).currencyId()); splitLeft.setShares(-_value * price); splitLeft.setAccountId(_accountid); addSplit(splitLeft); MyMoneySplit splitRight; if (haspayee) splitRight.setPayeeId(payeeTest.id()); splitRight.setAction(_action); splitRight.setValue(_value); price = MyMoneyFile::instance()->price(currencyid, file->account(_categoryid).currencyId(), _date).rate(file->account(_categoryid).currencyId()); splitRight.setShares(_value * price); splitRight.setAccountId(_categoryid); addSplit(splitRight); MyMoneyFile::instance()->addTransaction(*this); ft.commit(); } TransactionHelper::~TransactionHelper() { MyMoneyFileTransaction ft; try { MyMoneyFile::instance()->removeTransaction(*this); ft.commit(); } catch (const MyMoneyException &e) { qDebug() << e.what(); } } void TransactionHelper::update() { MyMoneyFileTransaction ft; MyMoneyFile::instance()->modifyTransaction(*this); ft.commit(); } InvTransactionHelper::InvTransactionHelper(const QDate& _date, const QString& _action, MyMoneyMoney _shares, MyMoneyMoney _price, const QString& _stockaccountid, const QString& _transferid, const QString& _categoryid, MyMoneyMoney _fee) { init(_date, _action, _shares, _price, _fee, _stockaccountid, _transferid, _categoryid); } void InvTransactionHelper::init(const QDate& _date, const QString& _action, MyMoneyMoney _shares, MyMoneyMoney _price, MyMoneyMoney _fee, const QString& _stockaccountid, const QString& _transferid, const QString& _categoryid) { MyMoneyFile* file = MyMoneyFile::instance(); MyMoneyAccount stockaccount = file->account(_stockaccountid); MyMoneyMoney value = _shares * _price; setPostDate(_date); setCommodity("USD"); MyMoneySplit s1; s1.setValue(value); s1.setAccountId(_stockaccountid); if (_action == MyMoneySplit::actionName(eMyMoney::Split::Action::ReinvestDividend)) { s1.setShares(_shares); s1.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::ReinvestDividend)); MyMoneySplit s2; s2.setAccountId(_categoryid); s2.setShares(-value); s2.setValue(-value); addSplit(s2); } else if (_action == MyMoneySplit::actionName(eMyMoney::Split::Action::Dividend) || _action == MyMoneySplit::actionName(eMyMoney::Split::Action::Yield)) { s1.setAccountId(_categoryid); s1.setShares(-value); s1.setValue(-value); // Split 2 will be the zero-amount investment split that serves to // mark this transaction as a cash dividend and note which stock account // it belongs to. MyMoneySplit s2; s2.setValue(MyMoneyMoney()); s2.setShares(MyMoneyMoney()); s2.setAction(_action); s2.setAccountId(_stockaccountid); addSplit(s2); MyMoneySplit s3; s3.setAccountId(_transferid); s3.setShares(value); s3.setValue(value); addSplit(s3); } else if (_action == MyMoneySplit::actionName(eMyMoney::Split::Action::BuyShares)) { s1.setShares(_shares); s1.setValue(value); s1.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::BuyShares)); MyMoneySplit s3; s3.setAccountId(_transferid); s3.setShares(-value - _fee); s3.setValue(-value - _fee); addSplit(s3); if (!_categoryid.isEmpty() && !_fee.isZero()) { MyMoneySplit s2; s2.setAccountId(_categoryid); s2.setValue(_fee); s2.setShares(_fee); addSplit(s2); } } else if (_action == MyMoneySplit::actionName(eMyMoney::Split::Action::SplitShares)) { s1.setShares(_shares.abs()); s1.setValue(MyMoneyMoney()); s1.setPrice(MyMoneyMoney()); } addSplit(s1); //qDebug() << "created transaction, now adding..."; MyMoneyFileTransaction ft; file->addTransaction(*this); //qDebug() << "updating price..."; // update the price, while we're here if (_action != MyMoneySplit::actionName(eMyMoney::Split::Action::SplitShares)) { QString stockid = stockaccount.currencyId(); QString basecurrencyid = file->baseCurrency().id(); MyMoneyPrice price = file->price(stockid, basecurrencyid, _date, true); if (!price.isValid()) { MyMoneyPrice newprice(stockid, basecurrencyid, _date, _price, "test"); file->addPrice(newprice); } } ft.commit(); //qDebug() << "successfully added " << id(); } QString makeAccount(const QString& _name, eMyMoney::Account::Type _type, MyMoneyMoney _balance, const QDate& _open, const QString& _parent, QString _currency, bool _taxReport, bool _openingBalance) { MyMoneyAccount info; MyMoneyFileTransaction ft; info.setName(_name); info.setAccountType(_type); info.setOpeningDate(_open); if (!_currency.isEmpty()) info.setCurrencyId(_currency); else info.setCurrencyId(MyMoneyFile::instance()->baseCurrency().id()); if (_taxReport) info.setValue("Tax", "Yes"); if (_openingBalance) info.setValue("OpeningBalanceAccount", "Yes"); MyMoneyAccount parent = MyMoneyFile::instance()->account(_parent); MyMoneyFile::instance()->addAccount(info, parent); // create the opening balance transaction if any if (!_balance.isZero()) { MyMoneySecurity sec = MyMoneyFile::instance()->currency(info.currencyId()); MyMoneyFile::instance()->openingBalanceAccount(sec); MyMoneyFile::instance()->createOpeningBalanceTransaction(info, _balance); } ft.commit(); return info.id(); } void makePrice(const QString& _currencyid, const QDate& _date, const MyMoneyMoney& _price) { MyMoneyFileTransaction ft; MyMoneyFile* file = MyMoneyFile::instance(); MyMoneySecurity curr = file->currency(_currencyid); MyMoneyPrice price(_currencyid, file->baseCurrency().id(), _date, _price, "test"); file->addPrice(price); ft.commit(); } QString makeEquity(const QString& _name, const QString& _symbol) { MyMoneySecurity equity; MyMoneyFileTransaction ft; equity.setName(_name); equity.setTradingSymbol(_symbol); equity.setSmallestAccountFraction(1000); equity.setSecurityType(eMyMoney::Security::Type::None/*MyMoneyEquity::ETYPE_STOCK*/); MyMoneyFile::instance()->addSecurity(equity); ft.commit(); return equity.id(); } void makeEquityPrice(const QString& _id, const QDate& _date, const MyMoneyMoney& _price) { MyMoneyFile* file = MyMoneyFile::instance(); MyMoneyFileTransaction ft; QString basecurrencyid = file->baseCurrency().id(); MyMoneyPrice price = file->price(_id, basecurrencyid, _date, true); if (!price.isValid()) { MyMoneyPrice newprice(_id, basecurrencyid, _date, _price, "test"); file->addPrice(newprice); } ft.commit(); } void writeRCFtoXMLDoc(const MyMoneyReport& filter, QDomDocument* doc) { QDomProcessingInstruction instruct = doc->createProcessingInstruction(QString("xml"), QString("version=\"1.0\" encoding=\"utf-8\"")); doc->appendChild(instruct); QDomElement root = doc->createElement("KMYMONEY-FILE"); doc->appendChild(root); QDomElement reports = doc->createElement("REPORTS"); root.appendChild(reports); QDomElement report = doc->createElement("REPORT"); filter.write(report, doc); reports.appendChild(report); } void writeTabletoHTML(const PivotTable& table, const QString& _filename) { static unsigned filenumber = 1; QString filename = _filename; if (filename.isEmpty()) { filename = QString("report-%1%2.html").arg((filenumber < 10) ? "0" : "").arg(filenumber); ++filenumber; } QFile g(filename); g.open(QIODevice::WriteOnly); QTextStream(&g) << table.renderHTML(); g.close(); } void writeTabletoHTML(const QueryTable& table, const QString& _filename) { static unsigned filenumber = 1; QString filename = _filename; if (filename.isEmpty()) { filename = QString("report-%1%2.html").arg((filenumber < 10) ? "0" : "").arg(filenumber); ++filenumber; } QFile g(filename); g.open(QIODevice::WriteOnly); QTextStream(&g) << table.renderHTML(); g.close(); } void writeTabletoCSV(const PivotTable& table, const QString& _filename) { static unsigned filenumber = 1; QString filename = _filename; if (filename.isEmpty()) { filename = QString("report-%1%2.csv").arg((filenumber < 10) ? "0" : "").arg(filenumber); ++filenumber; } QFile g(filename); g.open(QIODevice::WriteOnly); QTextStream(&g) << table.renderCSV(); g.close(); } void writeTabletoCSV(const QueryTable& table, const QString& _filename) { static unsigned filenumber = 1; QString filename = _filename; if (filename.isEmpty()) { filename = QString("qreport-%1%2.csv").arg((filenumber < 10) ? "0" : "").arg(filenumber); ++filenumber; } QFile g(filename); g.open(QIODevice::WriteOnly); QTextStream(&g) << table.renderCSV(); g.close(); } void writeRCFtoXML(const MyMoneyReport& filter, const QString& _filename) { static unsigned filenum = 1; QString filename = _filename; if (filename.isEmpty()) { filename = QString("report-%1%2.xml").arg(QString::number(filenum).rightJustified(2, '0')); ++filenum; } QDomDocument* doc = new QDomDocument("KMYMONEY-FILE"); Q_CHECK_PTR(doc); writeRCFtoXMLDoc(filter, doc); QFile g(filename); g.open(QIODevice::WriteOnly); QTextStream stream(&g); stream.setCodec("UTF-8"); stream << doc->toString(); g.close(); delete doc; } bool readRCFfromXMLDoc(QList& list, QDomDocument* doc) { bool result = false; QDomElement rootElement = doc->documentElement(); if (!rootElement.isNull()) { QDomNode child = rootElement.firstChild(); while (!child.isNull() && child.isElement()) { QDomElement childElement = child.toElement(); if ("REPORTS" == childElement.tagName()) { result = true; QDomNode subchild = child.firstChild(); while (!subchild.isNull() && subchild.isElement()) { MyMoneyReport filter; if (filter.read(subchild.toElement())) { list += filter; } subchild = subchild.nextSibling(); } } child = child.nextSibling(); } } return result; } bool readRCFfromXML(QList& list, const QString& filename) { int result = false; QFile f(filename); f.open(QIODevice::ReadOnly); QDomDocument* doc = new QDomDocument; if (doc->setContent(&f, false)) { result = readRCFfromXMLDoc(list, doc); } delete doc; return result; } void XMLandback(MyMoneyReport& filter) { // this function writes the filter to XML, and then reads // it back from XML overwriting the original filter; // in all cases, the result should be the same if the read // & write methods are working correctly. QDomDocument* doc = new QDomDocument("KMYMONEY-FILE"); Q_CHECK_PTR(doc); writeRCFtoXMLDoc(filter, doc); QList list; if (readRCFfromXMLDoc(list, doc) && !list.isEmpty()) filter = list[0]; else throw MYMONEYEXCEPTION_CSTRING("Failed to load report from XML"); delete doc; } MyMoneyMoney searchHTML(const QString& _html, const QString& _search) { Q_UNUSED(_html) QRegExp re(QString("%1[<>/td]*([\\-.0-9,]*)").arg(_search)); if (re.indexIn(_html) > -1) { QString found = re.cap(1); found.remove(','); return MyMoneyMoney(found.toDouble()); } return MyMoneyMoney(); } } // end namespace test diff --git a/kmymoney/views/kmymoneyview.cpp b/kmymoney/views/kmymoneyview.cpp index e77a63d71..49a12a602 100644 --- a/kmymoney/views/kmymoneyview.cpp +++ b/kmymoney/views/kmymoneyview.cpp @@ -1,857 +1,852 @@ /*************************************************************************** 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 #include "kcurrencyeditdlg.h" -#include "mymoneystoragemgr.h" -#include "mymoneystoragebin.h" #include "mymoneyexception.h" -#include "mymoneystoragexml.h" -#include "mymoneystorageanon.h" #include "khomeview.h" #include "kaccountsview.h" #include "kcategoriesview.h" #include "kinstitutionsview.h" #include "kpayeesview.h" #include "ktagsview.h" #include "kscheduledview.h" #include "kgloballedgerview.h" #include "kinvestmentview.h" #include "models.h" #include "accountsmodel.h" #include "equitiesmodel.h" #include "securitiesmodel.h" #include "icons.h" #include "amountedit.h" #include "kmymoneyaccounttreeview.h" #include "accountsviewproxymodel.h" #include "mymoneyprice.h" #include "mymoneyschedule.h" #include "mymoneysplit.h" #include "mymoneyaccount.h" #include "mymoneyinstitution.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), m_lastViewSelected(0), m_storagePlugins(nullptr) { // 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() { KMyMoneySettings::setLastViewSelected(m_lastViewSelected); } 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(); } void KMyMoneyView::slotFileClosed() { 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 slotShowHomePage(); } 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); } void KMyMoneyView::setStoragePlugins(QMap& plugins) { m_storagePlugins = &plugins; } 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]); pActions[eMenu::Action::Print]->setEnabled(canPrint()); emit aboutToChangeView(); } bool KMyMoneyView::canPrint() { return ((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 for (auto i = (int)View::Home; 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::slotSetBaseCurrency(const MyMoneySecurity& baseCurrency) { if (!baseCurrency.id().isEmpty()) { QString baseId; try { baseId = MyMoneyFile::instance()->baseCurrency().id(); } catch (const MyMoneyException &e) { qDebug("%s", e.what()); } if (baseCurrency.id() != baseId) { MyMoneyFileTransaction ft; try { MyMoneyFile::instance()->setBaseCurrency(baseCurrency); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::sorry(this, i18n("Cannot set %1 as base currency: %2", baseCurrency.name(), QString::fromLatin1(e.what())), i18n("Set base currency")); } } AmountEdit::setStandardPrecision(MyMoneyMoney::denomToPrec(MyMoneyFile::instance()->baseCurrency().smallestAccountFraction())); KMyMoneyEdit::setStandardPrecision(MyMoneyMoney::denomToPrec(MyMoneyFile::instance()->baseCurrency().smallestAccountFraction())); } } 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) { // remember the current page m_lastViewSelected = current.row(); // set the current page's title in the header if (m_header) m_header->setText(m_model->data(current, KPageModel::HeaderRole).toString()); } 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(const View) { emit aboutToChangeView(); } 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 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: emit aboutToChangeView(); break; case eView::Action::SwitchView: showPage(view); 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); } }