diff --git a/kmymoney/kmymoney.cpp b/kmymoney/kmymoney.cpp index f15b13ac2..72c38f73b 100644 --- a/kmymoney/kmymoney.cpp +++ b/kmymoney/kmymoney.cpp @@ -1,3331 +1,4588 @@ /*************************************************************************** kmymoney.cpp ------------------- copyright : (C) 2000 by Michael Edwardes (C) 2007 by Thomas Baumgart (C) 2017 by Łukasz Wojniłowicz ****************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include #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 // ---------------------------------------------------------------------------- // 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/kfindtransactiondlg.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/onlinejobmessagesview.h" - +#include "widgets/amountedit.h" +#include "widgets/kmymoneyedit.h" #include "widgets/kmymoneymvccombo.h" #include "views/kmymoneyview.h" #include "views/konlinejoboutbox.h" #include "models/onlinejobmessagesmodel.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 "mymoney/onlinejobmessage.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 "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 #include "transactioneditor.h" #include "konlinetransferform.h" #include #include #include "kmymoneyutils.h" #include "kcreditswindow.h" #include "ledgerdelegate.h" #include "storageenums.h" #include "mymoneyenums.h" #include "dialogenums.h" #include "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 char recoveryKeyId[] = "59B0F826D2B08440"; +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 QHash pActions; QHash pMenus; enum backupStateE { BACKUP_IDLE = 0, BACKUP_MOUNTING, BACKUP_COPYING, BACKUP_UNMOUNTING }; class KMyMoneyApp::Private { public: Private(KMyMoneyApp *app) : q(app), m_ft(0), m_moveToAccountSelector(0), m_statementXMLindex(0), m_balanceWarning(0), m_collectingStatements(false), m_backupResult(0), m_backupMount(0), m_ignoreBackupExitCode(false), + m_fileOpen(false), + m_fmode(QFileDevice::ReadUser | QFileDevice::WriteUser), m_myMoneyView(0), m_progressBar(0), m_smtReader(0), m_searchDlg(0), m_autoSaveTimer(0), m_progressTimer(0), m_inAutoSaving(false), m_transactionEditor(0), m_endingBalanceDlg(0), m_saveEncrypted(0), m_additionalKeyLabel(0), m_additionalKeyButton(0), m_recentFiles(0), #ifdef KF5Holidays_FOUND m_holidayRegion(0), #endif m_applicationIsReady(true), m_webConnect(new WebConnect(app)) { // since the days of the week are from 1 to 7, // and a day of the week is used to index this bit array, // resize the array to 8 elements (element 0 is left unused) m_processingDays.resize(8); } void closeFile(); void unlinkStatementXML(); void moveInvestmentTransaction(const QString& fromId, const QString& toId, const MyMoneyTransaction& t); QList > automaticReconciliation(const MyMoneyAccount &account, const QList > &transactions, const MyMoneyMoney &amount); /** * The public interface. */ KMyMoneyApp * const q; MyMoneyFileTransaction* m_ft; KMyMoneyAccountSelector* m_moveToAccountSelector; int m_statementXMLindex; KBalanceWarning* m_balanceWarning; bool m_collectingStatements; QStringList m_statementResults; QString m_lastPayeeEnteredId; /** the configuration object of the application */ KSharedConfigPtr m_config; /** * @brief Structure of plugins objects by their interfaces */ KMyMoneyPlugin::Container m_plugins; /** * 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; MyMoneyStatementReader* m_smtReader; // allows multiple imports to be launched trough web connect and to be executed sequentially QQueue m_importUrlsQueue; KFindTransactionDlg* m_searchDlg; MyMoneyAccount m_selectedAccount; MyMoneyAccount m_reconciliationAccount; MyMoneySchedule m_selectedSchedule; KMyMoneyRegister::SelectedTransactions m_selectedTransactions; // This is Auto Saving related bool m_autoSaveEnabled; QTimer* m_autoSaveTimer; QTimer* m_progressTimer; int m_autoSavePeriod; bool m_inAutoSaving; // pointer to the current transaction editor TransactionEditor* m_transactionEditor; // Reconciliation dialog KEndingBalanceDlg* m_endingBalanceDlg; // Pointer to the combo box used for key selection during // File/Save as KComboBox* m_saveEncrypted; // id's that need to be remembered QString m_accountGoto, m_payeeGoto; QStringList m_additionalGpgKeys; QLabel* m_additionalKeyLabel; QPushButton* m_additionalKeyButton; KRecentFilesAction* m_recentFiles; #ifdef KF5Holidays_FOUND // used by the calendar interface for schedules KHolidays::HolidayRegion* m_holidayRegion; #endif QBitArray m_processingDays; QMap m_holidayMap; QStringList m_consistencyCheckResult; bool m_applicationIsReady; WebConnect* m_webConnect; // methods void consistencyCheck(bool alwaysDisplayResults); static void setThemedCSS(); void copyConsistencyCheckResults(); void saveConsistencyCheckResults(); + + 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", qPrintable(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(i18n("Unknown fix level in input file")); + } + } + ft.commit(); + } catch (const MyMoneyException &) { + MyMoneyFile::instance()->blockSignals(blocked); + return false; + } + } else { + qDebug("Skipping automatic transaction fix!"); + } + MyMoneyFile::instance()->blockSignals(blocked); + 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(); + 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", qPrintable(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", qPrintable(e.what())); + } + + if (!baseId.isEmpty()) { + // check that all accounts have a currency + QList list; + file->accountList(list); + QList::Iterator it; + + // don't forget those standard accounts + list << file->asset(); + list << file->liability(); + list << file->income(); + list << file->expense(); + list << file->equity(); + + + for (it = list.begin(); it != list.end(); ++it) { + QString cid; + try { + if (!(*it).currencyId().isEmpty() || (*it).currencyId().length() != 0) + cid = MyMoneyFile::instance()->currency((*it).currencyId()).id(); + } catch (const MyMoneyException& 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()), qPrintable(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) + { + 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; + + emit q->kmmFilePlugin(preOpen); + + 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("%1").arg(i18n("GPG is not available for decryption of file %1", 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. '?%1").arg(i18n("File %1 contains the old binary format used by KMyMoney. Please use an older version of KMyMoney (0.8.x) that still supports this format to convert it to the new XML based format.", fileName))); + } + + // 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(""); + 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; + } + m_fileType = KMyMoneyApp::GncXML; + } 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(); + + auto storage = new MyMoneyStorageMgr; + pReader->setProgressCallback(&KMyMoneyApp::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); + + // things are finished, now we connect the storage to the engine + // which forces a reload of the cache in the engine with those + // objects that are cached + MyMoneyFile::instance()->attachStorage(storage); + + // 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 : m_plugins.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(); + delete pStorage; + return false; + } + + if (pStorage) { + MyMoneyFile::instance()->detachStorage(); + MyMoneyFile::instance()->attachStorage(pStorage); + } + 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 + bool rc = true; + try { + if (! url.isValid()) { + throw MYMONEYEXCEPTION(i18n("Malformed URL '%1'", url.url())); + } + + if (url.isLocalFile()) { + filename = url.toLocalFile(); + try { + const unsigned int nbak = KMyMoneySettings::autoBackupCopies(); + if (nbak) { + KBackup::numberedBackupFile(filename, QString(), QStringLiteral("~"), nbak); + } + saveToLocalFile(filename, storageWriter.get(), plaintext, keyList); + } catch (const MyMoneyException &) { + throw MYMONEYEXCEPTION(i18n("Unable to write changes to '%1'", filename)); + } + } else { + QTemporaryFile tmpfile; + tmpfile.open(); // to obtain the name + tmpfile.close(); + saveToLocalFile(tmpfile.fileName(), storageWriter.get(), plaintext, keyList); + + Q_CONSTEXPR int permission = -1; + QFile file(tmpfile.fileName()); + file.open(QIODevice::ReadOnly); + KIO::StoredTransferJob *putjob = KIO::storedPut(file.readAll(), url, permission, KIO::JobFlag::Overwrite); + if (!putjob->exec()) { + throw MYMONEYEXCEPTION(i18n("Unable to upload to '%1'.
%2", url.toDisplayString(), putjob->errorString())); + } + file.close(); + } + m_fileType = KMyMoneyApp::KmmXML; + } catch (const MyMoneyException &e) { + KMessageBox::error(q, 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(i18n("Unable to open file '%1' for writing.", 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(i18n("Failure while writing to '%1'", localFile)); + } + + if (writeFile != localFile) { + // This simple comparison is possible because the strings are equal if no temporary file was created. + // If a temporary file was created, it is made in a way that the name is definitely different. So no + // symlinks etc. have to be evaluated. + if (!QFile::remove(localFile) || !QFile::rename(writeFile, localFile)) + throw MYMONEYEXCEPTION(i18n("Failure while writing to '%1'", localFile)); + } + QFile::setPermissions(localFile, m_fmode); + pWriter->setProgressCallback(0); + } + + /** + * 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", qPrintable(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("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", 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); { #ifdef Q_OS_WIN 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(this/*the global variable kmymoney is not yet assigned. So we pass it here*/); layout->addWidget(d->m_myMoneyView, 10); connect(d->m_myMoneyView, &KMyMoneyView::aboutToChangeView, this, &KMyMoneyApp::slotResetSelections); connect(d->m_myMoneyView, SIGNAL(currentPageChanged(KPageWidgetItem*,KPageWidgetItem*)), this, SLOT(slotUpdateActions())); connect(d->m_myMoneyView, &KMyMoneyView::statusMsg, this, &KMyMoneyApp::slotStatusMsg); connect(d->m_myMoneyView, &KMyMoneyView::statusProgress, this, &KMyMoneyApp::slotStatusProgressBar); /////////////////////////////////////////////////////////////////// // call inits to invoke all other construction parts readOptions(); // now initialize the plugin structure createInterfaces(); KMyMoneyPlugin::pluginHandling(KMyMoneyPlugin::Action::Load, d->m_plugins, this, guiFactory()); onlineJobAdministration::instance()->setOnlinePlugins(d->m_plugins.extended); d->m_myMoneyView->setOnlinePlugins(d->m_plugins.online); d->m_myMoneyView->setStoragePlugins(d->m_plugins.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; int weekStart = locale.firstDayOfWeek(); int weekEnd = weekStart-1; if (weekEnd < Qt::Monday) { weekEnd = Qt::Sunday; } bool startFirst = (weekStart < weekEnd); for (int i = 0; i < 8; ++i) { if (startFirst) d->m_processingDays.setBit(i, (i >= weekStart && i <= weekEnd)); else d->m_processingDays.setBit(i, (i >= weekStart || i <= weekEnd)); } d->m_autoSaveTimer = new QTimer(this); d->m_progressTimer = new QTimer(this); connect(d->m_autoSaveTimer, SIGNAL(timeout()), this, SLOT(slotAutoSave())); connect(d->m_progressTimer, SIGNAL(timeout()), this, SLOT(slotStatusProgressDone())); // make sure, we get a note when the engine changes state connect(MyMoneyFile::instance(), SIGNAL(dataChanged()), this, SLOT(slotDataChanged())); // connect the WebConnect server connect(d->m_webConnect, SIGNAL(gotUrl(QUrl)), this, SLOT(webConnect(QUrl))); // make sure we have a balance warning object d->m_balanceWarning = new KBalanceWarning(this); // setup the initial configuration slotUpdateConfiguration(QString()); // kickstart date change timer slotDateChanged(); connect(this, SIGNAL(fileLoaded(QUrl)), onlineJobAdministration::instance(), SLOT(updateOnlineTaskProperties())); } KMyMoneyApp::~KMyMoneyApp() { + d->removeStorage(); // delete cached objects since the are in the way // when unloading the plugins onlineJobAdministration::instance()->clearCaches(); // we need to unload all plugins before we destroy anything else KMyMoneyPlugin::pluginHandling(KMyMoneyPlugin::Action::Unload, d->m_plugins, this, guiFactory()); delete d->m_searchDlg; delete d->m_transactionEditor; delete d->m_endingBalanceDlg; delete d->m_moveToAccountSelector; #ifdef KF5Holidays_FOUND delete d->m_holidayRegion; #endif delete d; } QUrl KMyMoneyApp::lastOpenedURL() { QUrl url = d->m_startDialog ? QUrl() : d->m_fileName; if (!url.isValid()) { url = QUrl::fromUserInput(readLastUsedFile()); } ready(); return url; } void KMyMoneyApp::slotObjectDestroyed(QObject* o) { if (o == d->m_moveToAccountSelector) { d->m_moveToAccountSelector = 0; } } void KMyMoneyApp::slotInstallConsistencyCheckContextMenu() { // this code relies on the implementation of KMessageBox::informationList to add a context menu to that list, // please adjust it if it's necessary or rewrite the way the consistency check results are displayed if (QWidget* dialog = QApplication::activeModalWidget()) { if (QListWidget* widget = dialog->findChild()) { // give the user a hint that the data can be saved widget->setToolTip(i18n("This is the consistency check log, use the context menu to copy or save it.")); widget->setWhatsThis(widget->toolTip()); widget->setContextMenuPolicy(Qt::CustomContextMenu); connect(widget, SIGNAL(customContextMenuRequested(QPoint)), SLOT(slotShowContextMenuForConsistencyCheck(QPoint))); } } } void KMyMoneyApp::slotShowContextMenuForConsistencyCheck(const QPoint &pos) { // allow the user to save the consistency check results if (QWidget* widget = qobject_cast< QWidget* >(sender())) { QMenu contextMenu(widget); QAction* copy = new QAction(i18n("Copy to clipboard"), widget); QAction* save = new QAction(i18n("Save to file"), widget); contextMenu.addAction(copy); contextMenu.addAction(save); QAction *result = contextMenu.exec(widget->mapToGlobal(pos)); if (result == copy) { // copy the consistency check results to the clipboard d->copyConsistencyCheckResults(); } else if (result == save) { // save the consistency check results to a file d->saveConsistencyCheckResults(); } } } 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(); // ************* // Adding standard actions // ************* KStandardAction::openNew(this, &KMyMoneyApp::slotFileNew, aC); KStandardAction::open(this, &KMyMoneyApp::slotFileOpen, aC); d->m_recentFiles = KStandardAction::openRecent(this, &KMyMoneyApp::slotFileOpenRecent, aC); KStandardAction::save(this, &KMyMoneyApp::slotFileSave, aC); KStandardAction::saveAs(this, &KMyMoneyApp::slotFileSaveAs, aC); KStandardAction::close(this, &KMyMoneyApp::slotFileClose, aC); KStandardAction::quit(this, &KMyMoneyApp::slotFileQuit, aC); KStandardAction::print(this, &KMyMoneyApp::slotPrintView, aC); KStandardAction::preferences(this, &KMyMoneyApp::slotSettings, aC); /* Look-up table for all custom actions. It's required for: 1) building QList with QActions to be added to ActionCollection 2) adding custom features to QActions like e.g. keyboard shortcut */ QHash lutActions; // ************* // 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 Edit menu // ************* {Action::EditFindTransaction, &KMyMoneyApp::slotFindTransaction}, // ************* // The View menu // ************* {Action::ViewTransactionDetail, &KMyMoneyApp::slotShowTransactionDetail}, {Action::ViewHideReconciled, &KMyMoneyApp::slotHideReconciledTransactions}, {Action::ViewHideCategories, &KMyMoneyApp::slotHideUnusedCategories}, {Action::ViewShowAll, &KMyMoneyApp::slotShowAllAccounts}, // ***************** // The accounts menu // ***************** {Action::MapOnlineAccount, &KMyMoneyApp::slotAccountMapOnline}, {Action::UnmapOnlineAccount, &KMyMoneyApp::slotAccountUnmapOnline}, {Action::UpdateAccount, &KMyMoneyApp::slotAccountUpdateOnline}, {Action::UpdateAllAccounts, &KMyMoneyApp::slotAccountUpdateOnlineAll}, // ************** // 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->m_myMoneyView->dirty()) { + 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_myMoneyView->fileOpen()) { + if (!d->m_fileOpen) { // next line required until we move all file handling out of KMyMoneyView - d->m_myMoneyView->newFile(); + 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(); - d->m_myMoneyView->readFile(d->m_fileName); + if (d->openNondatabase(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->m_myMoneyView->closeFile(); + d->closeFile(); } if (wizard->startSettingsAfterFinished()) slotSettings(); } else { // next line required until we move all file handling out of KMyMoneyView - d->m_myMoneyView->closeFile(); + 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; +} + // General open void KMyMoneyApp::slotFileOpen() { KMSTATUS(i18n("Open a file.")); QString prevDir = readLastUsedDir(); - QPointer dialog = new QFileDialog(this, QString(), prevDir, - i18n("KMyMoney files (*.kmy *.xml);;All files")); + QString fileExtensions; + fileExtensions.append(i18n("KMyMoney files (*.kmy *.xml)")); + fileExtensions.append(QLatin1String(";;")); + + for (const auto& plugin : d->m_plugins.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; } -void KMyMoneyApp::slotOpenDatabase() -{ -// KMSTATUS(i18n("Open a file.")); -// QPointer dialog = new KSelectDatabaseDlg(QIODevice::ReadWrite); -// if (!dialog->checkDrivers()) { -// delete dialog; -// return; -// } - -// if (dialog->exec() == QDialog::Accepted && dialog != 0) { -// slotFileOpenRecent(dialog->selectedURL()); -// } -// delete dialog; -} - bool KMyMoneyApp::isImportableFile(const QUrl &url) { bool result = false; // Iterate through the plugins and see if there's a loaded plugin who can handle it QMap::const_iterator it_plugin = d->m_plugins.importer.constBegin(); while (it_plugin != d->m_plugins.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 == d->m_plugins.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; } #endif return false; } void KMyMoneyApp::slotFileOpenRecent(const QUrl &url) { KMSTATUS(i18n("Loading file...")); - if (!isFileOpenedInAnotherInstance(url)) { - QUrl newurl = url; - if (newurl.scheme() == QLatin1String("sql") || KMyMoneyUtils::fileExists(newurl)) { - slotFileClose(); - if (!d->m_myMoneyView->fileOpen()) { - try { - if (d->m_myMoneyView->readFile(newurl)) { - if ((d->m_myMoneyView->isNativeFile())) { - d->m_fileName = newurl; - updateCaption(); - d->m_recentFiles->addUrl(newurl); - writeLastUsedFile(newurl.toDisplayString(QUrl::PreferLocalFile)); - } else { - d->m_fileName = QUrl(); // imported files have no filename - } - // Check the schedules - slotCheckSchedules(); - } - } catch (const MyMoneyException &e) { - KMessageBox::sorry(this, i18n("Cannot open file as requested. Error was: %1", e.what())); - } - updateCaption(); - emit fileLoaded(d->m_fileName); - } else { - /*fileOpen failed - should we do something - or maybe fileOpen puts out the message... - it does for database*/ - } - } else { // newurl invalid - slotFileClose(); - KMessageBox::sorry(this, i18n("

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

", url.toDisplayString(QUrl::PreferLocalFile)), i18n("File not found")); - } - } else { // isDuplicate + 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 + isOpened = d->openNondatabase(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)); + /* Dont'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", 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->m_myMoneyView->dirty()) + if (!d->dirty()) return true; bool rc = false; KMSTATUS(i18n("Saving file...")); if (d->m_fileName.isEmpty()) return slotFileSaveAs(); d->consistencyCheck(false); setEnabled(false); - if (d->m_myMoneyView->isDatabase()) { + if (isDatabase()) { auto pluginFound = false; for (const auto& plugin : d->m_plugins.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->m_myMoneyView->saveFile(d->m_fileName, MyMoneyFile::instance()->value("kmm-encryption-key")); + 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()); const int rc = dlg->exec(); if ((rc == QDialog::Accepted) && (dlg != 0)) { d->m_additionalGpgKeys = dlg->additionalKeys(); selectedKeyName = dlg->secretKey(); } delete dlg; if ((rc != QDialog::Accepted) || (dlg == 0)) { return false; } } QString prevDir; // don't prompt file name if not a native file - if (d->m_myMoneyView->isNativeFile()) + 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->m_myMoneyView->saveFile(newURL); + 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->m_myMoneyView->saveFile(d->m_fileName, encryptionKeys); + 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); } } delete dlg; updateCaption(); return rc; } -void KMyMoneyApp::slotSaveAsDatabase() -{ - saveAsDatabase(); -} - -bool KMyMoneyApp::saveAsDatabase() -{ -// bool rc = false; -// QUrl oldUrl; -// // in event of it being a database, ensure that all data is read into storage for saveas -// if (d->m_myMoneyView->isDatabase()) -// oldUrl = d->m_fileName.isEmpty() ? lastOpenedURL() : d->m_fileName; - -// KMSTATUS(i18n("Saving file to database...")); -// QPointer dialog = new KSelectDatabaseDlg(QIODevice::WriteOnly); -// QUrl url = oldUrl; -// if (!dialog->checkDrivers()) { -// delete dialog; -// return (false); -// } - -// while (oldUrl == url && dialog->exec() == QDialog::Accepted && dialog != 0) { -// url = dialog->selectedURL(); -// // If the protocol is SQL for the old and new, and the hostname and database names match -// // Let the user know that the current database cannot be saved on top of itself. -// if (url.scheme() == "sql" && oldUrl.scheme() == "sql" -// && oldUrl.host() == url.host() -// && QUrlQuery(oldUrl).queryItemValue("driver") == QUrlQuery(url).queryItemValue("driver") -// && oldUrl.path().right(oldUrl.path().length() - 1) == url.path().right(url.path().length() - 1)) { -// KMessageBox::sorry(this, i18n("Cannot save to current database.")); -// } else { -// try { -// rc = d->m_myMoneyView->saveAsDatabase(url); -// } catch (const MyMoneyException &e) { -// KMessageBox::sorry(this, i18n("Cannot save to current database: %1", e.what())); -// } -// } -// } -// delete dialog; - -// if (rc) { -// //KRecentFilesAction *p = dynamic_cast(action("file_open_recent")); -// //if(p) -// d->m_recentFiles->addUrl(url); -// writeLastUsedFile(url.toDisplayString(QUrl::PreferLocalFile)); -// } -// d->m_autoSaveTimer->stop(); -// updateCaption(); -// return rc; - return false; -} - void KMyMoneyApp::slotFileCloseWindow() { KMSTATUS(i18n("Closing window...")); - if (d->m_myMoneyView->dirty()) { + 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->m_myMoneyView->dirty()) { + 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_myMoneyView->fileOpen()) { + 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_myMoneyView->fileOpen()) { + 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", 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(0, i18n("Error"), i18n("Unable to import template(s): %1, thrown in %2:%3", e.what(), e.file(), e.line())); } } delete dlg; } void KMyMoneyApp::slotSaveAccountTemplates() { KMSTATUS(i18n("Exporting account templates.")); QString savePath = QStandardPaths::writableLocation(QStandardPaths::DataLocation) + "/templates/" + QLocale().name(); QDir d(savePath); if (!d.exists()) d.mkpath(savePath); QString newName = QFileDialog::getSaveFileName(this, i18n("Save as..."), savePath, i18n("KMyMoney template files (*.kmt);;All files (*)")); // // If there is no file extension, then append a .kmt at the end of the file name. // If there is a file extension, make sure it is .kmt, delete any others. // if (!newName.isEmpty()) { // find last . delimiter int nLoc = newName.lastIndexOf('.'); if (nLoc != -1) { QString strExt, strTemp; strTemp = newName.left(nLoc + 1); strExt = newName.right(newName.length() - (nLoc + 1)); if ((strExt.indexOf("kmt", 0, Qt::CaseInsensitive) == -1)) { strTemp.append("kmt"); //append to make complete file name newName = strTemp; } } else { newName.append(".kmt"); } if (okToWriteFile(QUrl::fromLocalFile(newName))) { QPointer dlg = new KTemplateExportDlg(this); if (dlg->exec() == QDialog::Accepted && dlg) { MyMoneyTemplate templ; templ.setTitle(dlg->title()); templ.setShortDescription(dlg->shortDescription()); templ.setLongDescription(dlg->longDescription()); templ.exportTemplate(&progressCallback); templ.saveTemplate(QUrl::fromLocalFile(newName)); } delete dlg; } } } 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, d->m_plugins, this, guiFactory()); onlineJobAdministration::instance()->updateActions(); 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->m_myMoneyView->dirty()) { + 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(recoveryKeyId)); + 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->m_myMoneyView->dirty()) + 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::slotFindTransaction() { if (d->m_searchDlg == 0) { d->m_searchDlg = new KFindTransactionDlg(this); connect(d->m_searchDlg, SIGNAL(destroyed()), this, SLOT(slotCloseSearchDialog())); connect(d->m_searchDlg, SIGNAL(transactionSelected(QString,QString)), d->m_myMoneyView, SLOT(slotLedgerSelected(QString,QString))); } d->m_searchDlg->show(); d->m_searchDlg->raise(); d->m_searchDlg->activateWindow(); } void KMyMoneyApp::slotCloseSearchDialog() { if (d->m_searchDlg) d->m_searchDlg->deleteLater(); d->m_searchDlg = 0; } void KMyMoneyApp::createAccount(MyMoneyAccount& newAccount, MyMoneyAccount& parentAccount, MyMoneyAccount& brokerageAccount, MyMoneyMoney openingBal) { MyMoneyFile *file = MyMoneyFile::instance(); try { const MyMoneySecurity& sec = file->security(newAccount.currencyId()); // Check the opening balance if (openingBal.isPositive() && newAccount.accountGroup() == eMyMoney::Account::Type::Liability) { QString message = i18n("This account is a liability and if the " "opening balance represents money owed, then it should be negative. " "Negate the amount?\n\n" "Please click Yes to change the opening balance to %1,\n" "Please click No to leave the amount as %2,\n" "Please click Cancel to abort the account creation." , MyMoneyUtils::formatMoney(-openingBal, newAccount, sec) , MyMoneyUtils::formatMoney(openingBal, newAccount, sec)); int ans = KMessageBox::questionYesNoCancel(this, message); if (ans == KMessageBox::Yes) { openingBal = -openingBal; } else if (ans == KMessageBox::Cancel) return; } file->createAccount(newAccount, parentAccount, brokerageAccount, openingBal); } catch (const MyMoneyException &e) { KMessageBox::information(this, i18n("Unable to add account: %1", e.what())); } } void KMyMoneyApp::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("Transaction for schedule has less than 2 splits!"); } MyMoneyFileTransaction ft; try { file->addSchedule(newSchedule); // in case of a loan account, we keep a reference to this // schedule in the account if (newAccount.accountType() == eMyMoney::Account::Type::Loan || newAccount.accountType() == eMyMoney::Account::Type::AssetLoan) { newAccount.setValue("schedule", newSchedule.id()); file->modifyAccount(newAccount); } ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::information(this, i18n("Unable to add scheduled transaction: %1", e.what())); } } catch (const MyMoneyException &e) { KMessageBox::information(this, i18n("Unable to add scheduled transaction: %1", e.what())); } } } QList > KMyMoneyApp::Private::automaticReconciliation(const MyMoneyAccount &account, const QList > &transactions, const MyMoneyMoney &amount) { static const int NR_OF_STEPS_LIMIT = 300000; static const int PROGRESSBAR_STEPS = 1000; QList > result = transactions; KMSTATUS(i18n("Running automatic reconciliation")); int progressBarIndex = 0; kmymoney->slotStatusProgressBar(progressBarIndex, NR_OF_STEPS_LIMIT / PROGRESSBAR_STEPS); // optimize the most common case - all transactions should be cleared QListIterator > itTransactionSplitResult(result); MyMoneyMoney transactionsBalance; while (itTransactionSplitResult.hasNext()) { const QPair &transactionSplit = itTransactionSplitResult.next(); transactionsBalance += transactionSplit.second.shares(); } if (amount == transactionsBalance) { result = transactions; return result; } kmymoney->slotStatusProgressBar(progressBarIndex++, 0); // only one transaction is uncleared itTransactionSplitResult.toFront(); int index = 0; while (itTransactionSplitResult.hasNext()) { const QPair &transactionSplit = itTransactionSplitResult.next(); if (transactionsBalance - transactionSplit.second.shares() == amount) { result.removeAt(index); return result; } index++; } kmymoney->slotStatusProgressBar(progressBarIndex++, 0); // more than one transaction is uncleared - apply the algorithm result.clear(); const MyMoneySecurity &security = MyMoneyFile::instance()->security(account.currencyId()); double precision = 0.1 / account.fraction(security); QList sumList; sumList << MyMoneyMoney(); QMap > > sumToComponentsMap; // compute the possible matches QListIterator > itTransactionSplit(transactions); while (itTransactionSplit.hasNext()) { const QPair &transactionSplit = itTransactionSplit.next(); QListIterator itSum(sumList); QList tempList; while (itSum.hasNext()) { const MyMoneyMoney &sum = itSum.next(); QList > splitIds; splitIds << qMakePair(transactionSplit.first.id(), transactionSplit.second.id()); if (sumToComponentsMap.contains(sum)) { if (sumToComponentsMap.value(sum).contains(qMakePair(transactionSplit.first.id(), transactionSplit.second.id()))) { continue; } splitIds.append(sumToComponentsMap.value(sum)); } tempList << transactionSplit.second.shares() + sum; sumToComponentsMap[transactionSplit.second.shares() + sum] = splitIds; int size = sumToComponentsMap.size(); if (size % PROGRESSBAR_STEPS == 0) { kmymoney->slotStatusProgressBar(progressBarIndex++, 0); } if (size > NR_OF_STEPS_LIMIT) { return result; // it's taking too much resources abort the algorithm } } QList unionList; unionList.append(tempList); unionList.append(sumList); qSort(unionList); sumList.clear(); MyMoneyMoney smallestSumFromUnion = unionList.first(); sumList.append(smallestSumFromUnion); QListIterator itUnion(unionList); while (itUnion.hasNext()) { MyMoneyMoney sumFromUnion = itUnion.next(); if (smallestSumFromUnion < MyMoneyMoney(1 - precision / transactions.size())*sumFromUnion) { smallestSumFromUnion = sumFromUnion; sumList.append(sumFromUnion); } } } kmymoney->slotStatusProgressBar(NR_OF_STEPS_LIMIT / PROGRESSBAR_STEPS, 0); if (sumToComponentsMap.contains(amount)) { QListIterator > itTransactionSplit(transactions); while (itTransactionSplit.hasNext()) { const QPair &transactionSplit = itTransactionSplit.next(); const QList > &splitIds = sumToComponentsMap.value(amount); if (splitIds.contains(qMakePair(transactionSplit.first.id(), transactionSplit.second.id()))) { result.append(transactionSplit); } } } #ifdef KMM_DEBUG qDebug("For the amount %s a number of %d possible sums where computed from the set of %d transactions: ", qPrintable(MyMoneyUtils::formatMoney(amount, security)), sumToComponentsMap.size(), transactions.size()); #endif kmymoney->slotStatusProgressBar(-1, -1); return result; } void KMyMoneyApp::slotReparentAccount(const MyMoneyAccount& _src, const MyMoneyInstitution& _dst) { MyMoneyAccount src(_src); src.setInstitutionId(_dst.id()); MyMoneyFileTransaction ft; try { MyMoneyFile::instance()->modifyAccount(src); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::sorry(this, i18n("

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

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

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

", src.name(), dst.name(), e.what())); } } void KMyMoneyApp::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); QMenu *menu = dynamic_cast(w); if (menu) menu->exec(QCursor::pos()); else qDebug("menu '%s' not found: w = %p, menu = %p", qPrintable(containerName), w, menu); } void KMyMoneyApp::slotPrintView() { d->m_myMoneyView->slotPrintView(); } void KMyMoneyApp::updateCaption(bool skipActions) { QString caption; caption = d->m_fileName.fileName(); - if (caption.isEmpty() && d->m_myMoneyView && d->m_myMoneyView->fileOpen()) + 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_myMoneyView->enableViewsIfFileOpen(d->m_fileOpen); slotUpdateActions(); } } void KMyMoneyApp::slotUpdateActions() { const auto file = MyMoneyFile::instance(); - const bool fileOpen = d->m_myMoneyView->fileOpen(); + const bool fileOpen = d->m_fileOpen; const bool modified = file->dirty(); // const bool importRunning = (d->m_smtReader != 0); auto aC = actionCollection(); // ************* // Disabling actions to be disabled at this point // ************* { static const QVector disabledActions { Action::UpdateAllAccounts }; for (const auto& a : disabledActions) pActions[a]->setEnabled(false); } // ************* // Disabling actions based on conditions // ************* { QString tooltip = i18n("Create a new transaction"); const QVector> actionStates { // {qMakePair(Action::FileOpenDatabase, true)}, // {qMakePair(Action::FileSaveAsDatabase, fileOpen)}, {qMakePair(Action::FilePersonalData, fileOpen)}, - {qMakePair(Action::FileBackup, (fileOpen && !d->m_myMoneyView->isDatabase()))}, + {qMakePair(Action::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::Close)))->setEnabled(fileOpen); aC->action(QString::fromLatin1(KStandardAction::name(KStandardAction::Print)))->setEnabled(fileOpen && d->m_myMoneyView->canPrint()); // ************* // Enabling actions based on conditions // ************* QList accList; file->accountList(accList); QList::const_iterator it_a; QMap::const_iterator it_p = d->m_plugins.online.constEnd(); for (it_a = accList.constBegin(); (it_p == d->m_plugins.online.constEnd()) && (it_a != accList.constEnd()); ++it_a) { if ((*it_a).hasOnlineMapping()) { // check if provider is available it_p = d->m_plugins.online.constFind((*it_a).onlineBankingSettings().value("provider").toLower()); if (it_p != d->m_plugins.online.constEnd()) { QStringList protocols; (*it_p)->protocols(protocols); if (protocols.count() > 0) { pActions[Action::UpdateAllAccounts]->setEnabled(true); } } } } } void KMyMoneyApp::slotResetSelections() { slotSelectAccount(MyMoneyAccount()); d->m_myMoneyView->slotObjectSelected(MyMoneyAccount()); d->m_myMoneyView->slotObjectSelected(MyMoneyInstitution()); d->m_myMoneyView->slotObjectSelected(MyMoneySchedule()); d->m_myMoneyView->slotObjectSelected(MyMoneyTag()); d->m_myMoneyView->slotTransactionsSelected(KMyMoneyRegister::SelectedTransactions()); slotUpdateActions(); } void KMyMoneyApp::slotSelectAccount(const MyMoneyObject& obj) { if (typeid(obj) != typeid(MyMoneyAccount)) return; d->m_selectedAccount = MyMoneyAccount(); const MyMoneyAccount& acc = dynamic_cast(obj); if (!acc.isInvest()) d->m_selectedAccount = acc; // qDebug("slotSelectAccount('%s')", d->m_selectedAccount.name().data()); slotUpdateActions(); emit accountSelected(d->m_selectedAccount); } void KMyMoneyApp::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::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_myMoneyView->fileOpen() && + if (! d->m_fileOpen && KMessageBox::warningContinueCancel(kmymoney, i18n("You must first select a KMyMoney file before you can import a statement.")) == KMessageBox::Continue) kmymoney->slotFileOpen(); // only continue if the user really did open a file. - if (d->m_myMoneyView->fileOpen()) { + if (d->m_fileOpen) { KMSTATUS(i18n("Importing a statement via Web Connect")); // remove the statement files d->unlinkStatementXML(); QMap::const_iterator it_plugin = d->m_plugins.importer.constBegin(); while (it_plugin != d->m_plugins.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 == d->m_plugins.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->m_myMoneyView->dirty() && d->m_autoSaveEnabled) { + 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::slotOnlineAccountRequested(const MyMoneyAccount& acc, eMenu::Action action) { d->m_selectedAccount = acc; switch (action) { case eMenu::Action::UnmapOnlineAccount: slotAccountUnmapOnline(); break; case eMenu::Action::MapOnlineAccount: slotAccountMapOnline(); break; case eMenu::Action::UpdateAccount: slotAccountUpdateOnline(); break; case eMenu::Action::UpdateAllAccounts: slotAccountUpdateOnlineAll(); break; default: break; } } void KMyMoneyApp::slotAccountUnmapOnline() { // no account selected if (d->m_selectedAccount.id().isEmpty()) return; // not a mapped account if (!d->m_selectedAccount.hasOnlineMapping()) return; if (KMessageBox::warningYesNo(this, QString("%1").arg(i18n("Do you really want to remove the mapping of account %1 to an online account? Depending on the details of the online banking method used, this action cannot be reverted.", d->m_selectedAccount.name())), i18n("Remove mapping to online account")) == KMessageBox::Yes) { MyMoneyFileTransaction ft; try { d->m_selectedAccount.setOnlineBankingSettings(MyMoneyKeyValueContainer()); // delete the kvp that is used in MyMoneyStatementReader too // we should really get rid of it, but since I don't know what it // is good for, I'll keep it around. (ipwizard) d->m_selectedAccount.deletePair("StatementKey"); MyMoneyFile::instance()->modifyAccount(d->m_selectedAccount); ft.commit(); // The mapping could disable the online task system onlineJobAdministration::instance()->updateOnlineTaskProperties(); } catch (const MyMoneyException &e) { KMessageBox::error(this, i18n("Unable to unmap account from online account: %1", e.what())); } } } void KMyMoneyApp::slotAccountMapOnline() { // no account selected if (d->m_selectedAccount.id().isEmpty()) return; // already an account mapped if (d->m_selectedAccount.hasOnlineMapping()) return; // check if user tries to map a brokerageAccount if (d->m_selectedAccount.name().contains(i18n(" (Brokerage)"))) { if (KMessageBox::warningContinueCancel(this, i18n("You try to map a brokerage account to an online account. This is usually not advisable. In general, the investment account should be mapped to the online account. Please cancel if you intended to map the investment account, continue otherwise"), i18n("Mapping brokerage account")) == KMessageBox::Cancel) { return; } } // if we have more than one provider let the user select the current provider QString provider; QMap::const_iterator it_p; switch (d->m_plugins.online.count()) { case 0: break; case 1: provider = d->m_plugins.online.begin().key(); break; default: { QMenu popup(this); popup.setTitle(i18n("Select online banking plugin")); // Populate the pick list with all the provider for (it_p = d->m_plugins.online.constBegin(); it_p != d->m_plugins.online.constEnd(); ++it_p) { popup.addAction(it_p.key())->setData(it_p.key()); } QAction *item = popup.actions()[0]; if (item) { popup.setActiveAction(item); } // cancelled if ((item = popup.exec(QCursor::pos(), item)) == 0) { return; } provider = item->data().toString(); } break; } if (provider.isEmpty()) return; // find the provider it_p = d->m_plugins.online.constFind(provider.toLower()); if (it_p != d->m_plugins.online.constEnd()) { // plugin found, call it MyMoneyKeyValueContainer settings; if ((*it_p)->mapAccount(d->m_selectedAccount, settings)) { settings["provider"] = provider.toLower(); MyMoneyAccount acc(d->m_selectedAccount); acc.setOnlineBankingSettings(settings); MyMoneyFileTransaction ft; try { MyMoneyFile::instance()->modifyAccount(acc); ft.commit(); // The mapping could enable the online task system onlineJobAdministration::instance()->updateOnlineTaskProperties(); } catch (const MyMoneyException &e) { KMessageBox::error(this, i18n("Unable to map account to online account: %1", e.what())); } } } } void KMyMoneyApp::slotAccountUpdateOnlineAll() { QList accList; MyMoneyFile::instance()->accountList(accList); QList::iterator it_a; QMap::const_iterator it_p; d->m_statementResults.clear(); d->m_collectingStatements = true; // remove all those from the list, that don't have a 'provider' or the // provider is not currently present for (it_a = accList.begin(); it_a != accList.end();) { if (!(*it_a).hasOnlineMapping() || d->m_plugins.online.find((*it_a).onlineBankingSettings().value("provider").toLower()) == d->m_plugins.online.end()) { it_a = accList.erase(it_a); } else ++it_a; } const QVector disabledActions {Action::UpdateAccount, Action::UpdateAllAccounts}; for (const auto& a : disabledActions) pActions[a]->setEnabled(false); // now work on the remaining list of accounts int cnt = accList.count() - 1; for (it_a = accList.begin(); it_a != accList.end(); ++it_a) { it_p = d->m_plugins.online.constFind((*it_a).onlineBankingSettings().value("provider").toLower()); (*it_p)->updateAccount(*it_a, cnt != 0); --cnt; } d->m_collectingStatements = false; if (!d->m_statementResults.isEmpty()) KMessageBox::informationList(this, i18n("The statements have been processed with the following results:"), d->m_statementResults, i18n("Statement stats")); // re-enable the disabled actions slotUpdateActions(); } void KMyMoneyApp::slotAccountUpdateOnline() { // no account selected if (d->m_selectedAccount.id().isEmpty()) return; // no online account mapped if (!d->m_selectedAccount.hasOnlineMapping()) return; const QVector disabledActions {Action::UpdateAccount, Action::UpdateAllAccounts}; for (const auto& a : disabledActions) pActions[a]->setEnabled(false); // find the provider QMap::const_iterator it_p; it_p = d->m_plugins.online.constFind(d->m_selectedAccount.onlineBankingSettings().value("provider").toLower()); if (it_p != d->m_plugins.online.constEnd()) { // plugin found, call it d->m_collectingStatements = true; d->m_statementResults.clear(); (*it_p)->updateAccount(d->m_selectedAccount); d->m_collectingStatements = false; if (!d->m_statementResults.isEmpty()) KMessageBox::informationList(this, i18n("The statements have been processed with the following results:"), d->m_statementResults, i18n("Statement stats")); } // re-enable the disabled actions slotUpdateActions(); } void KMyMoneyApp::setHolidayRegion(const QString& holidayRegion) { #ifdef KF5Holidays_FOUND //since the cost of updating the cache is now not negligible //check whether the region has been modified if (!d->m_holidayRegion || d->m_holidayRegion->regionCode() != holidayRegion) { // Delete the previous holidayRegion before creating a new one. delete d->m_holidayRegion; // Create a new holidayRegion. d->m_holidayRegion = new KHolidays::HolidayRegion(holidayRegion); //clear and update the holiday cache preloadHolidays(); } #else Q_UNUSED(holidayRegion); #endif } bool KMyMoneyApp::isProcessingDate(const QDate& date) const { #ifdef KF5Holidays_FOUND if (!d->m_processingDays.testBit(date.dayOfWeek())) return false; if (!d->m_holidayRegion || !d->m_holidayRegion->isValid()) return true; //check first whether it's already in cache if (d->m_holidayMap.contains(date)) { return d->m_holidayMap.value(date, true); } else { bool processingDay = !d->m_holidayRegion->isHoliday(date); d->m_holidayMap.insert(date, processingDay); return processingDay; } #else Q_UNUSED(date); return true; #endif } void KMyMoneyApp::preloadHolidays() { #ifdef KF5Holidays_FOUND //clear the cache before loading d->m_holidayMap.clear(); //only do this if it is a valid region if (d->m_holidayRegion && d->m_holidayRegion->isValid()) { //load holidays for the forecast days plus 1 cycle, to be on the safe side int forecastDays = 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() { q->slotSelectAccount(MyMoneyAccount()); q->d->m_myMoneyView->slotObjectSelected(MyMoneyAccount()); q->d->m_myMoneyView->slotObjectSelected(MyMoneyInstitution()); q->d->m_myMoneyView->slotObjectSelected(MyMoneySchedule()); q->d->m_myMoneyView->slotObjectSelected(MyMoneyTag()); q->d->m_myMoneyView->slotTransactionsSelected(KMyMoneyRegister::SelectedTransactions()); // q->slotSelectTransactions(KMyMoneyRegister::SelectedTransactions()); m_reconciliationAccount = MyMoneyAccount(); m_myMoneyView->finishReconciliation(MyMoneyAccount()); - m_myMoneyView->closeFile(); + 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 f7905538c..0bc07b751 100644 --- a/kmymoney/kmymoney.h +++ b/kmymoney/kmymoney.h @@ -1,707 +1,731 @@ /*************************************************************************** kmymoney.h ------------------- copyright : (C) 2000-2001 by Michael Edwardes (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 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; 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: /** * Keep track of objects that are destroyed by external events */ void slotObjectDestroyed(QObject* o); /** * 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(); /** * Brings up a dialog to let the user search for specific transaction(s). It then * opens a results window to display those transactions. */ void slotFindTransaction(); /** * Destroys a possibly open the search dialog */ void slotCloseSearchDialog(); /** * 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 slotAccountMapOnline(); void slotAccountUnmapOnline(); void slotAccountUpdateOnline(); void slotAccountUpdateOnlineAll(); 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 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; + 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 */ virtual bool queryClose(); void slotCheckSchedules(); virtual void resizeEvent(QResizeEvent*); 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); - /** open a SQL database */ - void slotOpenDatabase(); - /** * 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(); - /** - * ask the user to select a database and save the current document - * - * @retval false save operation failed - * @retval true save operation was successful - */ - bool saveAsDatabase(); - void slotSaveAsDatabase(); - /** 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(); void slotOnlineAccountRequested(const MyMoneyAccount& acc, eMenu::Action action); /** * 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(); void slotSelectAccount(const MyMoneyObject& account); /** * 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; /** * 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 sent out, when the user presses Ctrl+A or activates * the Select all transactions action. */ void selectAllTransactions(); /** * This signal is emitted when a new account has been selected by * the GUI. If no account is selected or the selection is removed, * @a account is identical to MyMoneyAccount(). This signal is used * by plugins to get information about changes. */ void accountSelected(const MyMoneyAccount& account); /** * 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/kmymoneyutils.cpp b/kmymoney/kmymoneyutils.cpp index 0204ca7ec..a409854f9 100644 --- a/kmymoney/kmymoneyutils.cpp +++ b/kmymoney/kmymoneyutils.cpp @@ -1,788 +1,812 @@ /*************************************************************************** kmymoneyutils.cpp - description ------------------- begin : Wed Feb 5 2003 copyright : (C) 2000-2003 by Michael Edwardes email : mte@users.sourceforge.net Javier Campos Morales Felix Rodriguez John C Thomas Baumgart Kevin Tambascio (C) 2017 by Łukasz Wojniłowicz ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "kmymoneyutils.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include #include #include #include #include #include +#include // ---------------------------------------------------------------------------- // KDE Headers #include #include #include #include #include #include #include +#include // ---------------------------------------------------------------------------- // Project Includes #include "mymoneymoney.h" #include "mymoneyexception.h" #include "mymoneyfile.h" #include "mymoneyaccount.h" #include "mymoneysecurity.h" #include "mymoneyschedule.h" #include "mymoneypayee.h" #include "mymoneytag.h" #include "mymoneyprice.h" #include "mymoneystatement.h" #include "mymoneyforecast.h" #include "mymoneysplit.h" #include "mymoneytransaction.h" #include "kmymoneysettings.h" #include "icons.h" #include "storageenums.h" #include "mymoneyenums.h" using namespace Icons; KMyMoneyUtils::KMyMoneyUtils() { } KMyMoneyUtils::~KMyMoneyUtils() { } const QString KMyMoneyUtils::occurrenceToString(const eMyMoney::Schedule::Occurrence occurrence) { return i18nc("Frequency of schedule", MyMoneySchedule::occurrenceToString(occurrence).toLatin1()); } const QString KMyMoneyUtils::paymentMethodToString(eMyMoney::Schedule::PaymentType paymentType) { return i18nc("Scheduled Transaction payment type", MyMoneySchedule::paymentMethodToString(paymentType).toLatin1()); } const QString KMyMoneyUtils::weekendOptionToString(eMyMoney::Schedule::WeekendOption weekendOption) { return i18n(MyMoneySchedule::weekendOptionToString(weekendOption).toLatin1()); } const QString KMyMoneyUtils::scheduleTypeToString(eMyMoney::Schedule::Type type) { return i18nc("Scheduled transaction type", MyMoneySchedule::scheduleTypeToString(type).toLatin1()); } KGuiItem KMyMoneyUtils::scheduleNewGuiItem() { KGuiItem splitGuiItem(i18n("&New Schedule..."), Icons::get(Icon::DocumentNew), i18n("Create a new schedule."), i18n("Use this to create a new schedule.")); return splitGuiItem; } KGuiItem KMyMoneyUtils::accountsFilterGuiItem() { KGuiItem splitGuiItem(i18n("&Filter"), Icons::get(Icon::ViewFilter), i18n("Filter out accounts"), i18n("Use this to filter out accounts")); return splitGuiItem; } const char* homePageItems[] = { I18N_NOOP("Payments"), I18N_NOOP("Preferred accounts"), I18N_NOOP("Payment accounts"), I18N_NOOP("Favorite reports"), I18N_NOOP("Forecast (schedule)"), I18N_NOOP("Net worth forecast"), I18N_NOOP("Forecast (history)"), I18N_NOOP("Assets and Liabilities"), I18N_NOOP("Budget"), I18N_NOOP("CashFlow"), // insert new items above this comment 0 }; const QString KMyMoneyUtils::homePageItemToString(const int idx) { QString rc; if (abs(idx) > 0 && abs(idx) < static_cast(sizeof(homePageItems) / sizeof(homePageItems[0]))) { rc = i18n(homePageItems[abs(idx-1)]); } return rc; } int KMyMoneyUtils::stringToHomePageItem(const QString& txt) { int idx = 0; for (idx = 0; homePageItems[idx] != 0; ++idx) { if (txt == i18n(homePageItems[idx])) return idx + 1; } return 0; } bool KMyMoneyUtils::appendCorrectFileExt(QString& str, const QString& strExtToUse) { bool rc = false; if (!str.isEmpty()) { //find last . delminator int nLoc = str.lastIndexOf('.'); if (nLoc != -1) { QString strExt, strTemp; strTemp = str.left(nLoc + 1); strExt = str.right(str.length() - (nLoc + 1)); if (strExt.indexOf(strExtToUse, 0, Qt::CaseInsensitive) == -1) { // if the extension given contains a period, we remove our's if (strExtToUse.indexOf('.') != -1) strTemp = strTemp.left(strTemp.length() - 1); //append extension to make complete file name strTemp.append(strExtToUse); str = strTemp; rc = true; } } else { str.append("."); str.append(strExtToUse); rc = true; } } return rc; } void KMyMoneyUtils::checkConstants() { // TODO: port to kf5 #if 0 Q_ASSERT(static_cast(KLocale::ParensAround) == static_cast(MyMoneyMoney::ParensAround)); Q_ASSERT(static_cast(KLocale::BeforeQuantityMoney) == static_cast(MyMoneyMoney::BeforeQuantityMoney)); Q_ASSERT(static_cast(KLocale::AfterQuantityMoney) == static_cast(MyMoneyMoney::AfterQuantityMoney)); Q_ASSERT(static_cast(KLocale::BeforeMoney) == static_cast(MyMoneyMoney::BeforeMoney)); Q_ASSERT(static_cast(KLocale::AfterMoney) == static_cast(MyMoneyMoney::AfterMoney)); #endif } QString KMyMoneyUtils::variableCSS() { QColor tcolor = KColorScheme(QPalette::Active).foreground(KColorScheme::NormalText).color(); QColor link = KColorScheme(QPalette::Active).foreground(KColorScheme::LinkText).color(); QString css; css += "\n"; return css; } QString KMyMoneyUtils::findResource(QStandardPaths::StandardLocation type, const QString& filename) { QLocale locale; QString country; QString localeName = locale.bcp47Name(); QString language = localeName; // extract language and country from the bcp47name QRegularExpression regExp(QLatin1String("(\\w+)_(\\w+)")); QRegularExpressionMatch match = regExp.match(localeName); if (match.hasMatch()) { language = match.captured(1); country = match.captured(2); } QString rc; // check that the placeholder is present and set things up if (filename.indexOf("%1") != -1) { /// @fixme somehow I have the impression, that language and country /// mappings to the filename are not correct. This certainly must /// be overhauled at some point in time (ipwizard, 2017-10-22) QString mask = filename.arg("_%1.%2"); rc = QStandardPaths::locate(type, mask.arg(country).arg(language)); // search the given resource if (rc.isEmpty()) { mask = filename.arg("_%1"); rc = QStandardPaths::locate(type, mask.arg(language)); } if (rc.isEmpty()) { // qDebug(QString("html/home_%1.html not found").arg(country).toLatin1()); rc = QStandardPaths::locate(type, mask.arg(country)); } if (rc.isEmpty()) { rc = QStandardPaths::locate(type, filename.arg("")); } } else { rc = QStandardPaths::locate(type, filename); } if (rc.isEmpty()) { qWarning("No resource found for (%s,%s)", qPrintable(QStandardPaths::displayName(type)), qPrintable(filename)); } return rc; } const MyMoneySplit KMyMoneyUtils::stockSplit(const MyMoneyTransaction& t) { MyMoneySplit investmentAccountSplit; foreach (const auto split, t.splits()) { if (!split.accountId().isEmpty()) { auto acc = MyMoneyFile::instance()->account(split.accountId()); if (acc.isInvest()) { return split; } // if we have a reference to an investment account, we remember it here if (acc.accountType() == eMyMoney::Account::Type::Investment) investmentAccountSplit = split; } } // if we haven't found a stock split, we see if we've seen // an investment account on the way. If so, we return it. if (!investmentAccountSplit.id().isEmpty()) return investmentAccountSplit; // if none was found, we return an empty split. return MyMoneySplit(); } KMyMoneyUtils::transactionTypeE KMyMoneyUtils::transactionType(const MyMoneyTransaction& t) { if (!stockSplit(t).id().isEmpty()) return InvestmentTransaction; if (t.splitCount() < 2) { return Unknown; } else if (t.splitCount() > 2) { // FIXME check for loan transaction here return SplitTransaction; } QString ida, idb; if (t.splits().size() > 0) ida = t.splits()[0].accountId(); if (t.splits().size() > 1) idb = t.splits()[1].accountId(); if (ida.isEmpty() || idb.isEmpty()) return Unknown; MyMoneyAccount a, b; a = MyMoneyFile::instance()->account(ida); b = MyMoneyFile::instance()->account(idb); if ((a.accountGroup() == eMyMoney::Account::Type::Asset || a.accountGroup() == eMyMoney::Account::Type::Liability) && (b.accountGroup() == eMyMoney::Account::Type::Asset || b.accountGroup() == eMyMoney::Account::Type::Liability)) return Transfer; return Normal; } void KMyMoneyUtils::calculateAutoLoan(const MyMoneySchedule& schedule, MyMoneyTransaction& transaction, const QMap& balances) { try { MyMoneyForecast::calculateAutoLoan(schedule, transaction, balances); } catch (const MyMoneyException &e) { KMessageBox::detailedError(0, i18n("Unable to load schedule details"), e.what()); } } QString KMyMoneyUtils::nextCheckNumber(const MyMoneyAccount& acc) { QString number; // +-#1--+ +#2++-#3-++-#4--+ QRegExp exp(QString("(.*\\D)?(0*)(\\d+)(\\D.*)?")); if (exp.indexIn(acc.value("lastNumberUsed")) != -1) { setLastNumberUsed(acc.value("lastNumberUsed")); QString arg1 = exp.cap(1); QString arg2 = exp.cap(2); QString arg3 = QString::number(exp.cap(3).toULong() + 1); QString arg4 = exp.cap(4); number = QString("%1%2%3%4").arg(arg1).arg(arg2).arg(arg3).arg(arg4); // if new number is longer than previous one and we identified // preceding 0s, then remove one of the preceding zeros if (arg2.length() > 0 && (number.length() != acc.value("lastNumberUsed").length())) { arg2 = arg2.mid(1); number = QString("%1%2%3%4").arg(arg1).arg(arg2).arg(arg3).arg(arg4); } } else { number = '1'; } return number; } void KMyMoneyUtils::updateLastNumberUsed(const MyMoneyAccount& acc, const QString& number) { MyMoneyAccount accnt = acc; QString num = number; // now check if this number has been used already auto file = MyMoneyFile::instance(); if (file->checkNoUsed(accnt.id(), num)) { // if a number has been entered which is immediately prior to // an existing number, the next new number produced would clash // so need to look ahead for free next number bool free = false; for (int i = 0; i < 10; i++) { // find next unused number - 10 tries (arbitrary) if (file->checkNoUsed(accnt.id(), num)) { // increment and try again num = getAdjacentNumber(num); } else { // found a free number free = true; break; } } if (!free) { qDebug() << "No free number found - set to '1'"; num = '1'; } setLastNumberUsed(getAdjacentNumber(num, - 1)); } } void KMyMoneyUtils::setLastNumberUsed(const QString& num) { m_lastNumberUsed = num; } QString KMyMoneyUtils::lastNumberUsed() { return m_lastNumberUsed; } QString KMyMoneyUtils::getAdjacentNumber(const QString& number, int offset) { QString num = number; // +-#1--+ +#2++-#3-++-#4--+ QRegExp exp(QString("(.*\\D)?(0*)(\\d+)(\\D.*)?")); if (exp.indexIn(num) != -1) { QString arg1 = exp.cap(1); QString arg2 = exp.cap(2); QString arg3 = QString::number(exp.cap(3).toULong() + offset); QString arg4 = exp.cap(4); num = QString("%1%2%3%4").arg(arg1).arg(arg2).arg(arg3).arg(arg4); } else { num = '1'; } // next free number return num; } quint64 KMyMoneyUtils::numericPart(const QString & num) { quint64 num64 = 0; QRegExp exp(QString("(.*\\D)?(0*)(\\d+)(\\D.*)?")); if (exp.indexIn(num) != -1) { QString arg1 = exp.cap(1); QString arg2 = exp.cap(2); QString arg3 = QString::number(exp.cap(3).toULongLong()); QString arg4 = exp.cap(4); num64 = QString("%2%3").arg(arg2).arg(arg3).toULongLong(); } return num64; } QString KMyMoneyUtils::reconcileStateToString(eMyMoney::Split::State flag, bool text) { QString txt; if (text) { switch (flag) { case eMyMoney::Split::State::NotReconciled: txt = i18nc("Reconciliation state 'Not reconciled'", "Not reconciled"); break; case eMyMoney::Split::State::Cleared: txt = i18nc("Reconciliation state 'Cleared'", "Cleared"); break; case eMyMoney::Split::State::Reconciled: txt = i18nc("Reconciliation state 'Reconciled'", "Reconciled"); break; case eMyMoney::Split::State::Frozen: txt = i18nc("Reconciliation state 'Frozen'", "Frozen"); break; default: txt = i18nc("Unknown reconciliation state", "Unknown"); break; } } else { switch (flag) { case eMyMoney::Split::State::NotReconciled: break; case eMyMoney::Split::State::Cleared: txt = i18nc("Reconciliation flag C", "C"); break; case eMyMoney::Split::State::Reconciled: txt = i18nc("Reconciliation flag R", "R"); break; case eMyMoney::Split::State::Frozen: txt = i18nc("Reconciliation flag F", "F"); break; default: txt = i18nc("Flag for unknown reconciliation state", "?"); break; } } return txt; } MyMoneyTransaction KMyMoneyUtils::scheduledTransaction(const MyMoneySchedule& schedule) { MyMoneyTransaction t = schedule.transaction(); try { if (schedule.type() == eMyMoney::Schedule::Type::LoanPayment) { calculateAutoLoan(schedule, t, QMap()); } } catch (const MyMoneyException &e) { qDebug("Unable to load schedule details for '%s' during transaction match: %s", qPrintable(schedule.name()), qPrintable(e.what())); } t.clearId(); t.setEntryDate(QDate()); return t; } KXmlGuiWindow* KMyMoneyUtils::mainWindow() { foreach (QWidget *widget, QApplication::topLevelWidgets()) { KXmlGuiWindow* result = dynamic_cast(widget); if (result) return result; } return 0; } void KMyMoneyUtils::updateWizardButtons(QWizard* wizard) { // setup text on buttons wizard->setButtonText(QWizard::NextButton, i18nc("Go to next page of the wizard", "&Next")); wizard->setButtonText(QWizard::BackButton, KStandardGuiItem::back().text()); // setup icons wizard->button(QWizard::FinishButton)->setIcon(KStandardGuiItem::ok().icon()); wizard->button(QWizard::CancelButton)->setIcon(KStandardGuiItem::cancel().icon()); wizard->button(QWizard::NextButton)->setIcon(KStandardGuiItem::forward(KStandardGuiItem::UseRTL).icon()); wizard->button(QWizard::BackButton)->setIcon(KStandardGuiItem::back(KStandardGuiItem::UseRTL).icon()); } void KMyMoneyUtils::dissectTransaction(const MyMoneyTransaction& transaction, const MyMoneySplit& split, MyMoneySplit& assetAccountSplit, QList& feeSplits, QList& interestSplits, MyMoneySecurity& security, MyMoneySecurity& currency, eMyMoney::Split::InvestmentTransactionType& transactionType) { // collect the splits. split references the stock account and should already // be set up. assetAccountSplit references the corresponding asset account (maybe // empty), feeSplits is the list of all expenses and interestSplits // the list of all incomes assetAccountSplit = MyMoneySplit(); // set to none to check later if it was assigned auto file = MyMoneyFile::instance(); foreach (const auto tsplit, transaction.splits()) { auto acc = file->account(tsplit.accountId()); if (tsplit.id() == split.id()) { security = file->security(acc.currencyId()); } else if (acc.accountGroup() == eMyMoney::Account::Type::Expense) { feeSplits.append(tsplit); // feeAmount += tsplit.value(); } else if (acc.accountGroup() == eMyMoney::Account::Type::Income) { interestSplits.append(tsplit); // interestAmount += tsplit.value(); } else { if (assetAccountSplit == MyMoneySplit()) // first asset Account should be our requested brokerage account assetAccountSplit = tsplit; else if (tsplit.value().isNegative()) // the rest (if present) is handled as fee or interest feeSplits.append(tsplit); // and shouldn't be allowed to override assetAccountSplit else if (tsplit.value().isPositive()) interestSplits.append(tsplit); } } // determine transaction type if (split.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::AddShares)) { transactionType = (!split.shares().isNegative()) ? eMyMoney::Split::InvestmentTransactionType::AddShares : eMyMoney::Split::InvestmentTransactionType::RemoveShares; } else if (split.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::BuyShares)) { transactionType = (!split.value().isNegative()) ? eMyMoney::Split::InvestmentTransactionType::BuyShares : eMyMoney::Split::InvestmentTransactionType::SellShares; } else if (split.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::Dividend)) { transactionType = eMyMoney::Split::InvestmentTransactionType::Dividend; } else if (split.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::ReinvestDividend)) { transactionType = eMyMoney::Split::InvestmentTransactionType::ReinvestDividend; } else if (split.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::Yield)) { transactionType = eMyMoney::Split::InvestmentTransactionType::Yield; } else if (split.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::SplitShares)) { transactionType = eMyMoney::Split::InvestmentTransactionType::SplitShares; } else if (split.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::InterestIncome)) { transactionType = eMyMoney::Split::InvestmentTransactionType::InterestIncome; } else transactionType = eMyMoney::Split::InvestmentTransactionType::BuyShares; currency.setTradingSymbol("???"); try { currency = file->security(transaction.commodity()); } catch (const MyMoneyException &) { } } void KMyMoneyUtils::processPriceList(const MyMoneyStatement &st) { auto file = MyMoneyFile::instance(); QHash secBySymbol; QHash secByName; for (const auto& sec : file->securityList()) { secBySymbol[sec.tradingSymbol()] = sec; secByName[sec.name()] = sec; } for (const auto& stPrice : st.m_listPrices) { auto currency = file->baseCurrency().id(); QString security; if (!stPrice.m_strCurrency.isEmpty()) { security = stPrice.m_strSecurity; currency = stPrice.m_strCurrency; } else if (secBySymbol.contains(stPrice.m_strSecurity)) { security = secBySymbol[stPrice.m_strSecurity].id(); currency = file->security(file->security(security).tradingCurrency()).id(); } else if (secByName.contains(stPrice.m_strSecurity)) { security = secByName[stPrice.m_strSecurity].id(); currency = file->security(file->security(security).tradingCurrency()).id(); } else return; MyMoneyPrice price(security, currency, stPrice.m_date, stPrice.m_amount, stPrice.m_sourceName.isEmpty() ? i18n("Prices Importer") : stPrice.m_sourceName); file->addPrice(price); } } void KMyMoneyUtils::deleteSecurity(const MyMoneySecurity& security, QWidget* parent) { QString msg, msg2; QString dontAsk, dontAsk2; if (security.isCurrency()) { msg = i18n("

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

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

All exchange rates for currency %1 will be lost.

Do you still want to continue?

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

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

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

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

Do you still want to continue?

", MyMoneySecurity::securityTypeToString(security.securityType()), security.name()); dontAsk = "DeleteSecurity"; dontAsk2 = "DeleteSecurityPrices"; } if (KMessageBox::questionYesNo(parent, msg, i18n("Delete security"), KStandardGuiItem::yes(), KStandardGuiItem::no(), dontAsk) == KMessageBox::Yes) { MyMoneyFileTransaction ft; auto file = MyMoneyFile::instance(); QBitArray skip((int)eStorage::Reference::Count); skip.fill(true); skip.clearBit((int)eStorage::Reference::Price); if (file->isReferenced(security, skip)) { if (KMessageBox::questionYesNo(parent, msg2, i18n("Delete prices"), KStandardGuiItem::yes(), KStandardGuiItem::no(), dontAsk2) == KMessageBox::Yes) { try { QString secID = security.id(); foreach (auto priceEntry, file->priceList()) { const MyMoneyPrice& price = priceEntry.first(); if (price.from() == secID || price.to() == secID) file->removePrice(price); } ft.commit(); ft.restart(); } catch (const MyMoneyException &) { qDebug("Cannot delete price"); return; } } else return; } try { if (security.isCurrency()) file->removeCurrency(security); else file->removeSecurity(security); ft.commit(); } catch (const MyMoneyException &) { } } } bool KMyMoneyUtils::fileExists(const QUrl &url) { bool fileExists = false; if (url.isValid()) { short int detailLevel = 0; // Lowest level: file/dir/symlink/none KIO::StatJob* statjob = KIO::stat(url, KIO::StatJob::SourceSide, detailLevel); bool noerror = statjob->exec(); if (noerror) { // We want a file fileExists = !statjob->statResult().isDir(); } statjob->kill(); } return fileExists; } +QString KMyMoneyUtils::downloadFile(const QUrl &url) +{ + QString filename; + KIO::StoredTransferJob *transferjob = KIO::storedGet (url); +// KJobWidgets::setWindow(transferjob, this); + if (! transferjob->exec()) { + KMessageBox::detailedError(nullptr, + i18n("Error while loading file '%1'.", url.url()), + transferjob->errorString(), + i18n("File access error")); + return filename; + } + + QTemporaryFile file; + file.setAutoRemove(false); + file.open(); + file.write(transferjob->data()); + filename = file.fileName(); + file.close(); + return filename; +} + bool KMyMoneyUtils::newPayee(const QString& newnameBase, QString& id) { bool doit = true; if (newnameBase != i18n("New Payee")) { // Ask the user if that is what he intended to do? const auto msg = i18n("Do you want to add %1 as payer/receiver?", newnameBase); if (KMessageBox::questionYesNo(nullptr, msg, i18n("New payee/receiver"), KStandardGuiItem::yes(), KStandardGuiItem::no(), "NewPayee") == KMessageBox::No) { doit = false; // we should not keep the 'no' setting because that can confuse people like // I have seen in some usability tests. So we just delete it right away. KSharedConfigPtr kconfig = KSharedConfig::openConfig(); if (kconfig) { kconfig->group(QLatin1String("Notification Messages")).deleteEntry(QLatin1String("NewPayee")); } } } if (doit) { MyMoneyFileTransaction ft; try { QString newname(newnameBase); // adjust name until a unique name has been created int count = 0; for (;;) { try { MyMoneyFile::instance()->payeeByName(newname); newname = QString::fromLatin1("%1 [%2]").arg(newnameBase).arg(++count); } catch (const MyMoneyException &) { break; } } MyMoneyPayee p; p.setName(newname); MyMoneyFile::instance()->addPayee(p); id = p.id(); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(nullptr, i18n("Unable to add payee"), i18n("%1 thrown in %2:%3", e.what(), e.file(), e.line())); doit = false; } } return doit; } void KMyMoneyUtils::newTag(const QString& newnameBase, QString& id) { bool doit = true; if (newnameBase != i18n("New Tag")) { // Ask the user if that is what he intended to do? const auto msg = i18n("Do you want to add %1 as tag?", newnameBase); if (KMessageBox::questionYesNo(nullptr, msg, i18n("New tag"), KStandardGuiItem::yes(), KStandardGuiItem::no(), "NewTag") == KMessageBox::No) { doit = false; // we should not keep the 'no' setting because that can confuse people like // I have seen in some usability tests. So we just delete it right away. KSharedConfigPtr kconfig = KSharedConfig::openConfig(); if (kconfig) { kconfig->group(QLatin1String("Notification Messages")).deleteEntry(QLatin1String("NewTag")); } } } if (doit) { MyMoneyFileTransaction ft; try { QString newname(newnameBase); // adjust name until a unique name has been created int count = 0; for (;;) { try { MyMoneyFile::instance()->tagByName(newname); newname = QString::fromLatin1("%1 [%2]").arg(newnameBase, ++count); } catch (const MyMoneyException &) { break; } } MyMoneyTag ta; ta.setName(newname); MyMoneyFile::instance()->addTag(ta); id = ta.id(); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(nullptr, i18n("Unable to add tag"), i18n("%1 thrown in %2:%3", e.what(), e.file(), e.line())); } } } void KMyMoneyUtils::newInstitution(MyMoneyInstitution& institution) { auto file = MyMoneyFile::instance(); MyMoneyFileTransaction ft; try { file->addInstitution(institution); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::information(nullptr, i18n("Cannot add institution: %1", e.what())); } } QDebug KMyMoneyUtils::debug() { return qDebug() << QDateTime::currentDateTime().toString(QStringLiteral("HH:mm:ss.zzz")); } MyMoneyForecast KMyMoneyUtils::forecast() { MyMoneyForecast forecast; // override object defaults with those of the application forecast.setForecastCycles(KMyMoneySettings::forecastCycles()); forecast.setAccountsCycle(KMyMoneySettings::forecastAccountCycle()); forecast.setHistoryStartDate(QDate::currentDate().addDays(-forecast.forecastCycles()*forecast.accountsCycle())); forecast.setHistoryEndDate(QDate::currentDate().addDays(-1)); forecast.setForecastDays(KMyMoneySettings::forecastDays()); forecast.setBeginForecastDay(KMyMoneySettings::beginForecastDay()); forecast.setForecastMethod(KMyMoneySettings::forecastMethod()); forecast.setHistoryMethod(KMyMoneySettings::historyMethod()); forecast.setIncludeFutureTransactions(KMyMoneySettings::includeFutureTransactions()); forecast.setIncludeScheduledTransactions(KMyMoneySettings::includeScheduledTransactions()); return forecast; } diff --git a/kmymoney/kmymoneyutils.h b/kmymoney/kmymoneyutils.h index 30b7c75f2..748be16fa 100644 --- a/kmymoney/kmymoneyutils.h +++ b/kmymoney/kmymoneyutils.h @@ -1,391 +1,393 @@ /*************************************************************************** kmymoneyutils.h - description ------------------- begin : Wed Feb 5 2003 copyright : (C) 2000-2003 by Michael Edwardes email : mte@users.sourceforge.net Javier Campos Morales Felix Rodriguez John C Thomas Baumgart Kevin Tambascio ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #ifndef KMYMONEYUTILS_H #define KMYMONEYUTILS_H // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include // ---------------------------------------------------------------------------- // KDE Headers // ---------------------------------------------------------------------------- // Project Includes #include "selectedtransactions.h" class QIcon; /** * @author Thomas Baumgart */ static QString m_lastNumberUsed; class QPixmap; class QWizard; class QWidget; class KGuiItem; class KXmlGuiWindow; class MyMoneyMoney; class MyMoneyAccount; class MyMoneySecurity; class MyMoneySchedule; class MyMoneySplit; class MyMoneyTransaction; class MyMoneyStatement; class MyMoneyInstitution; class MyMoneyForecast; namespace eMyMoney { namespace Schedule { enum class Occurrence; enum class PaymentType; enum class WeekendOption; enum class Type; } namespace Split { enum class State; enum class InvestmentTransactionType; } } class KMyMoneyUtils { public: enum transactionTypeE { /** * Unknown transaction type (e.g. used for a transaction with only * a single split) */ Unknown, /** * A 'normal' transaction is one that consists out two splits: one * referencing an income/expense account, the other referencing * an asset/liability account. */ Normal, /** * A transfer denotes a transaction consisting of two splits. * Both of the splits reference an asset/liability * account. */ Transfer, /** * Whenever a transaction consists of more than 2 splits, * it is treated as 'split transaction'. */ SplitTransaction, /** * This transaction denotes a specific transaction where * a loan account is involved. Usually, a special dialog * is used to modify this transaction. */ LoanPayment, /** * This transaction denotes a specific transaction where * an investment is involved. Usually, a special dialog * is used to modify this transaction. */ InvestmentTransaction }; static const int maxHomePageItems = 5; KMyMoneyUtils(); ~KMyMoneyUtils(); /** * This method is used to convert the occurrence type from its * internal representation into a human readable format. * * @param occurrence numerical representation of the MyMoneySchedule * occurrence type * * @return QString representing the human readable format translated according to the language cataglogue * * @sa MyMoneySchedule::occurrenceToString() * * @deprecated Use i18n(MyMoneySchedule::occurrenceToString(occurrence)) instead */ static const QString occurrenceToString(const eMyMoney::Schedule::Occurrence occurrence); /** * This method is used to convert the payment type from its * internal representation into a human readable format. * * @param paymentType numerical representation of the MyMoneySchedule * payment type * * @return QString representing the human readable format translated according to the language cataglogue * * @sa MyMoneySchedule::paymentMethodToString() */ static const QString paymentMethodToString(eMyMoney::Schedule::PaymentType paymentType); /** * This method is used to convert the schedule weekend option from its * internal representation into a human readable format. * * @param weekendOption numerical representation of the MyMoneySchedule * weekend option * * @return QString representing the human readable format translated according to the language cataglogue * * @sa MyMoneySchedule::weekendOptionToString() */ static const QString weekendOptionToString(eMyMoney::Schedule::WeekendOption weekendOption); /** * This method is used to convert the schedule type from its * internal representation into a human readable format. * * @param type numerical representation of the MyMoneySchedule * schedule type * * @return QString representing the human readable format translated according to the language cataglogue * * @sa MyMoneySchedule::scheduleTypeToString() */ static const QString scheduleTypeToString(eMyMoney::Schedule::Type type); /** * This method is used to convert a numeric index of an item * represented on the home page into its string form. * * @param idx numeric index of item * * @return QString with text of this item */ static const QString homePageItemToString(const int idx); /** * This method is used to convert the name of a home page item * to its internal numerical representation * * @param txt QString reference of the items name * * @retval 0 @p txt is unknown * @retval >0 numeric value for @p txt */ static int stringToHomePageItem(const QString& txt); /** * Retrieve a KDE KGuiItem for the new schedule button. * * @return The KGuiItem that can be used to display the icon and text */ static KGuiItem scheduleNewGuiItem(); /** * Retrieve a KDE KGuiItem for the account filter button * * @return The KGuiItem that can be used to display the icon and text */ static KGuiItem accountsFilterGuiItem(); /** * This method adds the file extension passed as argument @p extension * to the end of the file name passed as argument @p name if it is not present. * If @p name contains an extension it will be removed. * * @param name filename to be checked * @param extension extension to be added (w/o the dot) * * @retval true if @p name was changed * @retval false if @p name remained unchanged */ static bool appendCorrectFileExt(QString& name, const QString& extension); /** * Check that internal MyMoney engine constants use the same * values as the KDE constants. */ static void checkConstants(); static QString variableCSS(); /** * This method searches a KDE specific resource and applies country and * language settings during the search. Therefore, the parameter @p filename must contain * the characters '%1' which gets replaced with the language/country values. * * The search is performed in the following order (stopped immediately if a file was found): * - @c \%1 is replaced with _\.\ * - @c \%1 is replaced with _\ * - @c \%1 is replaced with _\ * - @c \%1 is replaced with the empty string * * @c \ and @c \ denote the respective KDE settings. * * Example: The KDE settings for country is Spain (es) and language is set * to Galician (gl). The code for looking up a file looks like this: * * @code * * : * QString fname = KMyMoneyUtils::findResource("appdata", "html/home%1.html") * : * * @endcode * * The method calls KStandardDirs::findResource() with the following values for the * parameter @p filename: * * - html/home_es.gl.html * - html/home_gl.html * - html/home_es.html * - html/home.html * * @note See KStandardDirs::findResource() for details on the parameters */ static QString findResource(QStandardPaths::StandardLocation type, const QString& filename); /** * This method returns the split referencing a stock account if * one exists in the transaction passed as @p t. If none is present * in @p t, an empty MyMoneySplit() object will be returned. * * @param t transaction to be checked for a stock account * @return MyMoneySplit object referencing a stock account or an * empty MyMoneySplit object. */ static const MyMoneySplit stockSplit(const MyMoneyTransaction& t); /** * This method analyses the splits of a transaction and returns * the type of transaction. Possible values are defined by the * KMyMoneyUtils::transactionTypeE enum. * * @param t const reference to the transaction * * @return KMyMoneyUtils::transactionTypeE value of the action */ static transactionTypeE transactionType(const MyMoneyTransaction& t); /** * This method modifies a scheduled loan transaction such that all * references to automatic calculated values are resolved to actual values. * * @param schedule const reference to the schedule the transaction is based on * @param transaction reference to the transaction to be checked and modified * @param balances QMap of (account-id,balance) pairs to be used as current balance * for the calculation of interest. If map is empty, the engine * will be interrogated for current balances. */ static void calculateAutoLoan(const MyMoneySchedule& schedule, MyMoneyTransaction& transaction, const QMap& balances); /** * Return next check number for account @a acc. */ static QString nextCheckNumber(const MyMoneyAccount& acc); static void updateLastNumberUsed(const MyMoneyAccount& acc, const QString& number); static void setLastNumberUsed(const QString& num); static QString lastNumberUsed(); /** * Returns previous number if offset is -1 or * the following number if offset is 1. */ static QString getAdjacentNumber(const QString& number, int offset = 1); /** * remove any non-numeric characters from check number * to allow validity check */ static quint64 numericPart(const QString & num); /** * Returns the text representing the reconcile flag. If @a text is @p true * then the full text will be returned otherwise a short form (usually one character). */ static QString reconcileStateToString(eMyMoney::Split::State flag, bool text = false); /** * Returns the transaction for @a schedule. In case of a loan payment the * transaction will be modified by calculateAutoLoan(). * The ID of the transaction as well as the entryDate will be reset. * * @returns adjusted transaction */ static MyMoneyTransaction scheduledTransaction(const MyMoneySchedule& schedule); /** * This method replaces the deprecated QApplication::mainWidget() from Qt 3.x. * It assumes that there is only one KXmlGuiWindow in the application, and * returns it. * * @return the first KXmlGuiWindow found in QApplication::topLevelWidgets() */ static KXmlGuiWindow* mainWindow(); /** * This method sets the button text and icons to the KDE standard ones * for the QWizard passed as argument. */ static void updateWizardButtons(QWizard *); static void dissectTransaction(const MyMoneyTransaction& transaction, const MyMoneySplit& split, MyMoneySplit& assetAccountSplit, QList& feeSplits, QList& interestSplits, MyMoneySecurity& security, MyMoneySecurity& currency, eMyMoney::Split::InvestmentTransactionType& transactionType); static void processPriceList(const MyMoneyStatement& st); /** * This method deletes security and associated price list but asks beforehand. */ static void deleteSecurity(const MyMoneySecurity &security, QWidget *parent = nullptr); /** * Check whether the url links to an existing file or not * @returns whether the file exists or not */ static bool fileExists(const QUrl &url); + static QString downloadFile(const QUrl &url); + static bool newPayee(const QString& newnameBase, QString& id); static void newTag(const QString& newnameBase, QString& id); /** * Creates a new institution entry in the MyMoneyFile engine * * @param institution MyMoneyInstitution object containing the data of * the institution to be created. */ static void newInstitution(MyMoneyInstitution& institution); static QDebug debug(); static MyMoneyForecast forecast(); }; #endif diff --git a/kmymoney/plugins/appinterface.h b/kmymoney/plugins/appinterface.h index 6d564adb8..ff4faaacf 100644 --- a/kmymoney/plugins/appinterface.h +++ b/kmymoney/plugins/appinterface.h @@ -1,56 +1,69 @@ /*************************************************************************** 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; + 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 QUrl filenameURL() const = 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; + + Q_SIGNALS: + void kmmFilePlugin(unsigned int); }; } #endif diff --git a/kmymoney/plugins/gnc/import/CMakeLists.txt b/kmymoney/plugins/gnc/import/CMakeLists.txt index 7d97e30a3..1bd6ddc80 100644 --- a/kmymoney/plugins/gnc/import/CMakeLists.txt +++ b/kmymoney/plugins/gnc/import/CMakeLists.txt @@ -1,34 +1,31 @@ # patch the version with the version defined in the build system configure_file(${CMAKE_CURRENT_SOURCE_DIR}/gncimporter.json.in ${CMAKE_CURRENT_BINARY_DIR}/gncimporter.json @ONLY) ########### next target ############### set(gncimporter_PART_SRCS gncimporter.cpp kgncimportoptionsdlg.cpp kgncpricesourcedlg.cpp ../../../widgets/kmymoneymoneyvalidator.cpp mymoneygncreader.cpp ) set(gncimporter_PART_UI kgncimportoptionsdlg.ui kgncpricesourcedlg.ui ) ki18n_wrap_ui(gncimporter_PART_SRCS ${gncimporter_PART_UI}) add_library(gncimporter MODULE ${gncimporter_PART_SRCS}) target_link_libraries(gncimporter kmm_plugin Alkimia::alkimia ) - -########### install files ############### -install(FILES gncimporter.rc - DESTINATION "${KXMLGUI_INSTALL_DIR}/gncimporter") +########### install files ############### install(TARGETS gncimporter DESTINATION "${KDE_INSTALL_PLUGINDIR}/kmymoney/") diff --git a/kmymoney/plugins/gnc/import/gncimporter.cpp b/kmymoney/plugins/gnc/import/gncimporter.cpp index c9a8db23e..3fe1a7b90 100644 --- a/kmymoney/plugins/gnc/import/gncimporter.cpp +++ b/kmymoney/plugins/gnc/import/gncimporter.cpp @@ -1,89 +1,89 @@ /*************************************************************************** gncimporter.cpp ------------------- copyright : (C) 2017 by Łukasz Wojniłowicz email : lukasz.wojnilowicz@gmail.com ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "gncimporter.h" // ---------------------------------------------------------------------------- // QT Includes #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "mymoneygncreader.h" #include "viewinterface.h" +#include "appinterface.h" #include "mymoneyfile.h" #include "mymoneyexception.h" #include "mymoneystoragemgr.h" class MyMoneyStatement; GNCImporter::GNCImporter(QObject *parent, const QVariantList &args) : KMyMoneyPlugin::Plugin(parent, "gncimporter"/*must be the same as X-KDE-PluginInfo-Name*/) { Q_UNUSED(args) setComponentName("gncimporter", i18n("GnuCash importer")); - setXMLFile("gncimporter.rc"); - createActions(); // For information, announce that we have been loaded. qDebug("Plugins: gncimporter loaded"); } GNCImporter::~GNCImporter() { qDebug("Plugins: gncimporter unloaded"); } -void GNCImporter::createActions() +bool GNCImporter::open(MyMoneyStorageMgr *storage, const QUrl &url) { - m_action = actionCollection()->addAction("file_import_gnc"); - m_action->setText(i18n("GnuCash...")); - connect(m_action, &QAction::triggered, this, &GNCImporter::slotGNCImport); + Q_UNUSED(url) + Q_UNUSED(storage) + return false; } -void GNCImporter::slotGNCImport() +bool GNCImporter::save(const QUrl &url) { - m_action->setEnabled(false); - - if (viewInterface()->fileOpen()) { - KMessageBox::information(nullptr, i18n("You cannot import GnuCash data into an existing file. Please close it.")); - m_action->setEnabled(true); - return; - } - - auto url = QFileDialog::getOpenFileUrl(nullptr, QString(), QUrl(), i18n("GnuCash files (*.gnucash *.xac *.gnc);;All files (*)")); - if (url.isLocalFile()) { - auto pReader = new MyMoneyGncReader; - if (viewInterface()->readFile(url, pReader)) - viewInterface()->slotRefreshViews(); - } - - m_action->setEnabled(true); + Q_UNUSED(url) + return false; +} + +IMyMoneyOperationsFormat* GNCImporter::reader() +{ + return new MyMoneyGncReader; +} + +QString GNCImporter::formatName() const +{ + return QStringLiteral("GNC"); +} + +QString GNCImporter::fileExtension() const +{ + return i18n("GnuCash files (*.gnucash *.xac *.gnc)"); } K_PLUGIN_FACTORY_WITH_JSON(GNCImporterFactory, "gncimporter.json", registerPlugin();) #include "gncimporter.moc" diff --git a/kmymoney/plugins/gnc/import/gncimporter.h b/kmymoney/plugins/gnc/import/gncimporter.h index 9f076756f..53d382cd9 100644 --- a/kmymoney/plugins/gnc/import/gncimporter.h +++ b/kmymoney/plugins/gnc/import/gncimporter.h @@ -1,60 +1,48 @@ /*************************************************************************** gncimporter.h ------------------- copyright : (C) 2017 by Łukasz Wojniłowicz email : lukasz.wojnilowicz@gmail.com ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #ifndef GNCIMPORTER_H #define GNCIMPORTER_H // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // QT Includes // Project Includes #include "kmymoneyplugin.h" class MyMoneyGncReader; -class GNCImporter : public KMyMoneyPlugin::Plugin +class GNCImporter : public KMyMoneyPlugin::Plugin, public KMyMoneyPlugin::StoragePlugin { Q_OBJECT + Q_INTERFACES(KMyMoneyPlugin::StoragePlugin) public: explicit GNCImporter(QObject *parent, const QVariantList &args); ~GNCImporter() override; - QAction *m_action; - -private: - MyMoneyGncReader *m_gncReader; - -private Q_SLOTS: - - /** - * Called when the user wishes to import tab delimeted transactions - * into the current account. An account must be open for this to - * work. Calls KMyMoneyView::slotAccountImportAscii. - * - * @see MyMoneyAccount - */ - void slotGNCImport(); - -protected: - void createActions(); + bool open(MyMoneyStorageMgr *storage, const QUrl &url) override; + bool save(const QUrl &url) override; + IMyMoneyOperationsFormat* reader() override; + QString formatName() const override; + QString fileExtension() const override; }; #endif diff --git a/kmymoney/plugins/gnc/import/gncimporter.rc b/kmymoney/plugins/gnc/import/gncimporter.rc deleted file mode 100644 index f1765fb14..000000000 --- a/kmymoney/plugins/gnc/import/gncimporter.rc +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/kmymoney/plugins/gnc/import/mymoneygncreader.cpp b/kmymoney/plugins/gnc/import/mymoneygncreader.cpp index 7e527001d..d919ee88a 100644 --- a/kmymoney/plugins/gnc/import/mymoneygncreader.cpp +++ b/kmymoney/plugins/gnc/import/mymoneygncreader.cpp @@ -1,2655 +1,2676 @@ /*************************************************************************** mymoneygncreader - description ------------------- begin : Wed Mar 3 2004 copyright : (C) 2000-2004 by Michael Edwardes email : mte@users.sourceforge.net Javier Campos Morales Felix Rodriguez John C Thomas Baumgart Kevin Tambascio ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "mymoneygncreader.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #ifndef _GNCFILEANON #include #include #endif #include // ---------------------------------------------------------------------------- // Third party Includes // ------------------------------------------------------------Box21---------------- // Project Includes #include #include "mymoneystoragemgr.h" #ifndef _GNCFILEANON #include "kmymoneyutils.h" #include "mymoneyfile.h" #include "mymoneyaccount.h" #include "mymoneysecurity.h" #include "mymoneyschedule.h" #include "mymoneyprice.h" #include "mymoneypayee.h" #include "mymoneysplit.h" #include "mymoneytransaction.h" #include "mymoneyexception.h" #include "kgncimportoptionsdlg.h" #include "kgncpricesourcedlg.h" #include "keditscheduledlg.h" #include "kmymoneyedit.h" #include "kmymoneymoneyvalidator.h" #define TRY try #define CATCH catch (const MyMoneyException &) #define PASS catch (const MyMoneyException &) { throw; } #else #include "mymoneymoney.h" #include // #define i18n QObject::tr #define TRY #define CATCH #define PASS #define MYMONEYEXCEPTION QString #define MyMoneyException QString #define PACKAGE "KMyMoney" #endif // _GNCFILEANON #include "mymoneyenums.h" using namespace eMyMoney; // init static variables double MyMoneyGncReader::m_fileHideFactor = 0.0; double GncObject::m_moneyHideFactor; // user options void MyMoneyGncReader::setOptions() { #ifndef _GNCFILEANON KGncImportOptionsDlg dlg; // display the dialog to allow the user to set own options if (dlg.exec()) { // set users input options m_dropSuspectSchedules = dlg.scheduleOption(); m_investmentOption = dlg.investmentOption(); m_useFinanceQuote = dlg.quoteOption(); m_useTxNotes = dlg.txNotesOption(); m_decoder = dlg.decodeOption(); gncdebug = dlg.generalDebugOption(); xmldebug = dlg.xmlDebugOption(); bAnonymize = dlg.anonymizeOption(); } else { // user declined, so set some sensible defaults m_dropSuspectSchedules = false; // investment option - 0, create investment a/c per stock a/c, 1 = single new investment account, 2 = prompt for each stock // option 2 doesn't really work too well at present m_investmentOption = 0; m_useFinanceQuote = false; m_useTxNotes = false; m_decoder = 0; gncdebug = false; // general debug messages xmldebug = false; // xml trace bAnonymize = false; // anonymize input } // no dialog option for the following; it will set base currency, and print actual XML data developerDebug = false; // set your fave currency here to save getting that enormous dialog each time you run a test // especially if you have to scroll down to USD... if (developerDebug) m_storage->setValue("kmm-baseCurrency", "GBP"); #endif // _GNCFILEANON } GncObject::GncObject() : pMain(0), m_subElementList(0), m_subElementListCount(0), m_dataElementList(0), m_dataElementListCount(0), m_dataPtr(0), m_state(0), m_anonClassList(0), m_anonClass(0) { } // Check that the current element is of a version we are coded for void GncObject::checkVersion(const QString& elName, const QXmlAttributes& elAttrs, const map_elementVersions& map) { TRY { if (map.contains(elName)) { // if it's not in the map, there's nothing to check if (!map[elName].contains(elAttrs.value("version"))) { QString em = Q_FUNC_INFO + i18n(": Sorry. This importer cannot handle version %1 of element %2" , elAttrs.value("version"), elName); throw MYMONEYEXCEPTION(em); } } return ; } PASS } // Check if this element is in the current object's sub element list GncObject *GncObject::isSubElement(const QString& elName, const QXmlAttributes& elAttrs) { TRY { uint i; GncObject *next = 0; for (i = 0; i < m_subElementListCount; i++) { if (elName == m_subElementList[i]) { m_state = i; next = startSubEl(); // go create the sub object if (next != 0) { next->initiate(elName, elAttrs); // initialize it next->m_elementName = elName; // save it's name so we can identify the end } break; } } return (next); } PASS } // Check if this element is in the current object's data element list bool GncObject::isDataElement(const QString &elName, const QXmlAttributes& elAttrs) { TRY { uint i; for (i = 0; i < m_dataElementListCount; i++) { if (elName == m_dataElementList[i]) { m_state = i; dataEl(elAttrs); // go set the pointer so the data can be stored return (true); } } m_dataPtr = 0; // we don't need this, so make sure we don't store extraneous data return (false); } PASS } // return the variable string, decoded if required QString GncObject::var(int i) const { /* This code was needed because the Qt3 XML reader apparently did not process the encoding parameter in the m_decoder == 0 ? m_v[i] : pMain->m_decoder->toUnicode(m_v[i].toUtf8())); } const QString GncObject::getKvpValue(const QString& key, const QString& type) const { QList::const_iterator it; // first check for exact match for (it = m_kvpList.begin(); it != m_kvpList.end(); ++it) { if (((*it).key() == key) && ((type.isEmpty()) || ((*it).type() == type))) return (*it).value(); } // then for partial match for (it = m_kvpList.begin(); it != m_kvpList.end(); ++it) { if (((*it).key().contains(key)) && ((type.isEmpty()) || ((*it).type() == type))) return (*it).value(); } return (QString()); } void GncObject::adjustHideFactor() { m_moneyHideFactor = pMain->m_fileHideFactor * (1.0 + (int)(200.0 * rand() / (RAND_MAX + 1.0))) / 100.0; } // data anonymizer QString GncObject::hide(QString data, unsigned int anonClass) { TRY { if (!pMain->bAnonymize) return (data); // no anonymizing required // counters used to generate names for anonymizer static int nextAccount; static int nextEquity; static int nextPayee; static int nextSched; static QMap anonPayees; // to check for duplicate payee names static QMap anonStocks; // for reference to equities QString result(data); QMap::const_iterator it; MyMoneyMoney in, mresult; switch (anonClass) { case ASIS: // this is not personal data break; case SUPPRESS: // this is personal and is not essential result = ""; break; case NXTACC: // generate account name result = ki18n("Account%1").subs(++nextAccount, -6).toString(); break; case NXTEQU: // generate/return an equity name it = anonStocks.constFind(data); if (it == anonStocks.constEnd()) { result = ki18n("Stock%1").subs(++nextEquity, -6).toString(); anonStocks.insert(data, result); } else { result = (*it); } break; case NXTPAY: // generate/return a payee name it = anonPayees.constFind(data); if (it == anonPayees.constEnd()) { result = ki18n("Payee%1").subs(++nextPayee, -6).toString(); anonPayees.insert(data, result); } else { result = (*it); } break; case NXTSCHD: // generate a schedule name result = ki18n("Schedule%1").subs(++nextSched, -6).toString(); break; case MONEY1: in = MyMoneyMoney(data); if (data == "-1/0") in = MyMoneyMoney(); // spurious gnucash data - causes a crash sometimes mresult = MyMoneyMoney(m_moneyHideFactor) * in; mresult.convert(10000); result = mresult.toString(); break; case MONEY2: in = MyMoneyMoney(data); if (data == "-1/0") in = MyMoneyMoney(); mresult = MyMoneyMoney(m_moneyHideFactor) * in; mresult.convert(10000); mresult.setThousandSeparator(' '); result = mresult.formatMoney("", 2); break; } return (result); } PASS } // dump current object data values // only called if gncdebug set void GncObject::debugDump() { uint i; qDebug() << "Object" << m_elementName; for (i = 0; i < m_dataElementListCount; i++) { qDebug() << m_dataElementList[i] << "=" << m_v[i]; } } //***************************************************************** GncFile::GncFile() { static const QString subEls[] = {"gnc:book", "gnc:count-data", "gnc:commodity", "price", "gnc:account", "gnc:transaction", "gnc:template-transactions", "gnc:schedxaction" }; m_subElementList = subEls; m_subElementListCount = END_FILE_SELS; m_dataElementListCount = 0; m_processingTemplates = false; m_bookFound = false; } GncFile::~GncFile() {} GncObject *GncFile::startSubEl() { TRY { if (pMain->xmldebug) qDebug("File start subel m_state %d", m_state); GncObject *next = 0; switch (m_state) { case BOOK: if (m_bookFound) throw MYMONEYEXCEPTION(i18n("This version of the importer cannot handle multi-book files.")); m_bookFound = true; break; case COUNT: next = new GncCountData; break; case CMDTY: next = new GncCommodity; break; case PRICE: next = new GncPrice; break; case ACCT: // accounts within the template section are ignored if (!m_processingTemplates) next = new GncAccount; break; case TX: next = new GncTransaction(m_processingTemplates); break; case TEMPLATES: m_processingTemplates = true; break; case SCHEDULES: m_processingTemplates = false; next = new GncSchedule; break; default: throw MYMONEYEXCEPTION("GncFile rcvd invalid state"); } return (next); } PASS } void GncFile::endSubEl(GncObject *subObj) { if (pMain->xmldebug) qDebug("File end subel"); if (!m_processingTemplates) delete subObj; // template txs must be saved awaiting schedules m_dataPtr = 0; return ; } //****************************************** GncDate ********************************************* GncDate::GncDate() { m_subElementListCount = 0; static const QString dEls[] = {"ts:date", "gdate"}; m_dataElementList = dEls; m_dataElementListCount = END_Date_DELS; static const unsigned int anonClasses[] = {ASIS, ASIS}; m_anonClassList = anonClasses; for (uint i = 0; i < m_dataElementListCount; i++) m_v.append(QString()); } GncDate::~GncDate() {} //*************************************GncCmdtySpec*************************************** GncCmdtySpec::GncCmdtySpec() { m_subElementListCount = 0; static const QString dEls[] = {"cmdty:space", "cmdty:id"}; m_dataElementList = dEls; m_dataElementListCount = END_CmdtySpec_DELS; static const unsigned int anonClasses[] = {ASIS, ASIS}; m_anonClassList = anonClasses; for (uint i = 0; i < m_dataElementListCount; i++) m_v.append(QString()); } GncCmdtySpec::~GncCmdtySpec() {} QString GncCmdtySpec::hide(QString data, unsigned int) { // hide equity names, but not currency names unsigned int newClass = ASIS; switch (m_state) { case CMDTYID: if (!isCurrency()) newClass = NXTEQU; } return (GncObject::hide(data, newClass)); } //************* GncKvp******************************************** GncKvp::GncKvp() { m_subElementListCount = END_Kvp_SELS; static const QString subEls[] = {"slot"}; // kvp's may be nested m_subElementList = subEls; m_dataElementListCount = END_Kvp_DELS; static const QString dataEls[] = {"slot:key", "slot:value"}; m_dataElementList = dataEls; static const unsigned int anonClasses[] = {ASIS, ASIS}; m_anonClassList = anonClasses; for (uint i = 0; i < m_dataElementListCount; i++) m_v.append(QString()); } GncKvp::~GncKvp() {} void GncKvp::dataEl(const QXmlAttributes& elAttrs) { switch (m_state) { case VALUE: m_kvpType = elAttrs.value("type"); } m_dataPtr = &(m_v[m_state]); if (key().contains("formula")) { m_anonClass = MONEY2; } else { m_anonClass = ASIS; } return ; } GncObject *GncKvp::startSubEl() { if (pMain->xmldebug) qDebug("Kvp start subel m_state %d", m_state); TRY { GncObject *next = 0; switch (m_state) { case KVP: next = new GncKvp; break; default: throw MYMONEYEXCEPTION("GncKvp rcvd invalid m_state "); } return (next); } PASS } void GncKvp::endSubEl(GncObject *subObj) { if (pMain->xmldebug) qDebug("Kvp end subel"); m_kvpList.append(*(static_cast (subObj))); m_dataPtr = 0; return ; } //*********************************GncLot********************************************* GncLot::GncLot() { m_subElementListCount = 0; m_dataElementListCount = 0; } GncLot::~GncLot() {} //*********************************GncCountData*************************************** GncCountData::GncCountData() { m_subElementListCount = 0; m_dataElementListCount = 0; m_v.append(QString()); // only 1 data item } GncCountData::~GncCountData() {} void GncCountData::initiate(const QString&, const QXmlAttributes& elAttrs) { m_countType = elAttrs.value("cd:type"); m_dataPtr = &(m_v[0]); return ; } void GncCountData::terminate() { int i = m_v[0].toInt(); if (m_countType == "commodity") { pMain->setGncCommodityCount(i); return ; } if (m_countType == "account") { pMain->setGncAccountCount(i); return ; } if (m_countType == "transaction") { pMain->setGncTransactionCount(i); return ; } if (m_countType == "schedxaction") { pMain->setGncScheduleCount(i); return ; } if (i != 0) { if (m_countType == "budget") pMain->setBudgetsFound(true); else if (m_countType.left(7) == "gnc:Gnc") pMain->setSmallBusinessFound(true); else if (pMain->xmldebug) qDebug() << "Unknown count type" << m_countType; } return ; } //*********************************GncCommodity*************************************** GncCommodity::GncCommodity() { m_subElementListCount = 0; static const QString dEls[] = {"cmdty:space", "cmdty:id", "cmdty:name", "cmdty:fraction"}; m_dataElementList = dEls; m_dataElementListCount = END_Commodity_DELS; static const unsigned int anonClasses[] = {ASIS, NXTEQU, SUPPRESS, ASIS}; m_anonClassList = anonClasses; for (uint i = 0; i < m_dataElementListCount; i++) m_v.append(QString()); } GncCommodity::~GncCommodity() {} void GncCommodity::terminate() { TRY { pMain->convertCommodity(this); return ; } PASS } //************* GncPrice******************************************** GncPrice::GncPrice() { static const QString subEls[] = {"price:commodity", "price:currency", "price:time"}; m_subElementList = subEls; m_subElementListCount = END_Price_SELS; m_dataElementListCount = END_Price_DELS; static const QString dataEls[] = {"price:value"}; m_dataElementList = dataEls; static const unsigned int anonClasses[] = {ASIS}; m_anonClassList = anonClasses; for (uint i = 0; i < m_dataElementListCount; i++) m_v.append(QString()); m_vpCommodity = 0; m_vpCurrency = 0; m_vpPriceDate = 0; } GncPrice::~GncPrice() { delete m_vpCommodity; delete m_vpCurrency; delete m_vpPriceDate; } GncObject *GncPrice::startSubEl() { TRY { GncObject *next = 0; switch (m_state) { case CMDTY: next = new GncCmdtySpec; break; case CURR: next = new GncCmdtySpec; break; case PRICEDATE: next = new GncDate; break; default: throw MYMONEYEXCEPTION("GncPrice rcvd invalid m_state"); } return (next); } PASS } void GncPrice::endSubEl(GncObject *subObj) { TRY { switch (m_state) { case CMDTY: m_vpCommodity = static_cast(subObj); break; case CURR: m_vpCurrency = static_cast(subObj); break; case PRICEDATE: m_vpPriceDate = static_cast(subObj); break; default: throw MYMONEYEXCEPTION("GncPrice rcvd invalid m_state"); } return; } PASS } void GncPrice::terminate() { TRY { pMain->convertPrice(this); return ; } PASS } //************* GncAccount******************************************** GncAccount::GncAccount() { m_subElementListCount = END_Account_SELS; static const QString subEls[] = {"act:commodity", "slot", "act:lots"}; m_subElementList = subEls; m_dataElementListCount = END_Account_DELS; static const QString dataEls[] = {"act:id", "act:name", "act:description", "act:type", "act:parent" }; m_dataElementList = dataEls; static const unsigned int anonClasses[] = {ASIS, NXTACC, SUPPRESS, ASIS, ASIS}; m_anonClassList = anonClasses; for (uint i = 0; i < m_dataElementListCount; i++) m_v.append(QString()); m_vpCommodity = 0; } GncAccount::~GncAccount() { delete m_vpCommodity; } GncObject *GncAccount::startSubEl() { TRY { if (pMain->xmldebug) qDebug("Account start subel m_state %d", m_state); GncObject *next = 0; switch (m_state) { case CMDTY: next = new GncCmdtySpec; break; case KVP: next = new GncKvp; break; case LOTS: next = new GncLot(); pMain->setLotsFound(true); // we don't handle lots; just set flag to report break; default: throw MYMONEYEXCEPTION("GncAccount rcvd invalid m_state"); } return (next); } PASS } void GncAccount::endSubEl(GncObject *subObj) { if (pMain->xmldebug) qDebug("Account end subel"); switch (m_state) { case CMDTY: m_vpCommodity = static_cast(subObj); break; case KVP: m_kvpList.append(*(static_cast (subObj))); } return ; } void GncAccount::terminate() { TRY { pMain->convertAccount(this); return ; } PASS } //************* GncTransaction******************************************** GncTransaction::GncTransaction(bool processingTemplates) { m_subElementListCount = END_Transaction_SELS; static const QString subEls[] = {"trn:currency", "trn:date-posted", "trn:date-entered", "trn:split", "slot" }; m_subElementList = subEls; m_dataElementListCount = END_Transaction_DELS; static const QString dataEls[] = {"trn:id", "trn:num", "trn:description"}; m_dataElementList = dataEls; static const unsigned int anonClasses[] = {ASIS, SUPPRESS, NXTPAY}; m_anonClassList = anonClasses; adjustHideFactor(); m_template = processingTemplates; for (uint i = 0; i < m_dataElementListCount; i++) m_v.append(QString()); m_vpCurrency = 0; m_vpDateEntered = m_vpDatePosted = 0; } GncTransaction::~GncTransaction() { delete m_vpCurrency; delete m_vpDatePosted; delete m_vpDateEntered; } GncObject *GncTransaction::startSubEl() { TRY { if (pMain->xmldebug) qDebug("Transaction start subel m_state %d", m_state); GncObject *next = 0; switch (m_state) { case CURRCY: next = new GncCmdtySpec; break; case POSTED: case ENTERED: next = new GncDate; break; case SPLIT: if (isTemplate()) { next = new GncTemplateSplit; } else { next = new GncSplit; } break; case KVP: next = new GncKvp; break; default: throw MYMONEYEXCEPTION("GncTransaction rcvd invalid m_state"); } return (next); } PASS } void GncTransaction::endSubEl(GncObject *subObj) { if (pMain->xmldebug) qDebug("Transaction end subel"); switch (m_state) { case CURRCY: m_vpCurrency = static_cast(subObj); break; case POSTED: m_vpDatePosted = static_cast(subObj); break; case ENTERED: m_vpDateEntered = static_cast(subObj); break; case SPLIT: m_splitList.append(subObj); break; case KVP: m_kvpList.append(*(static_cast (subObj))); } return ; } void GncTransaction::terminate() { TRY { if (isTemplate()) { pMain->saveTemplateTransaction(this); } else { pMain->convertTransaction(this); } return ; } PASS } //************* GncSplit******************************************** GncSplit::GncSplit() { m_subElementListCount = END_Split_SELS; static const QString subEls[] = {"split:reconcile-date"}; m_subElementList = subEls; m_dataElementListCount = END_Split_DELS; static const QString dataEls[] = {"split:id", "split:memo", "split:reconciled-state", "split:value", "split:quantity", "split:account" }; m_dataElementList = dataEls; static const unsigned int anonClasses[] = {ASIS, SUPPRESS, ASIS, MONEY1, MONEY1, ASIS}; m_anonClassList = anonClasses; for (uint i = 0; i < m_dataElementListCount; i++) m_v.append(QString()); m_vpDateReconciled = 0; } GncSplit::~GncSplit() { delete m_vpDateReconciled; } GncObject *GncSplit::startSubEl() { TRY { GncObject *next = 0; switch (m_state) { case RECDATE: next = new GncDate; break; default: throw MYMONEYEXCEPTION("GncTemplateSplit rcvd invalid m_state "); } return (next); } PASS } void GncSplit::endSubEl(GncObject *subObj) { if (pMain->xmldebug) qDebug("Split end subel"); switch (m_state) { case RECDATE: m_vpDateReconciled = static_cast(subObj); break; } return ; } //************* GncTemplateSplit******************************************** GncTemplateSplit::GncTemplateSplit() { m_subElementListCount = END_TemplateSplit_SELS; static const QString subEls[] = {"slot"}; m_subElementList = subEls; m_dataElementListCount = END_TemplateSplit_DELS; static const QString dataEls[] = {"split:id", "split:memo", "split:reconciled-state", "split:value", "split:quantity", "split:account" }; m_dataElementList = dataEls; static const unsigned int anonClasses[] = {ASIS, SUPPRESS, ASIS, MONEY1, MONEY1, ASIS}; m_anonClassList = anonClasses; for (uint i = 0; i < m_dataElementListCount; i++) m_v.append(QString()); } GncTemplateSplit::~GncTemplateSplit() {} GncObject *GncTemplateSplit::startSubEl() { if (pMain->xmldebug) qDebug("TemplateSplit start subel m_state %d", m_state); TRY { GncObject *next = 0; switch (m_state) { case KVP: next = new GncKvp; break; default: throw MYMONEYEXCEPTION("GncTemplateSplit rcvd invalid m_state"); } return (next); } PASS } void GncTemplateSplit::endSubEl(GncObject *subObj) { if (pMain->xmldebug) qDebug("TemplateSplit end subel"); m_kvpList.append(*(static_cast (subObj))); m_dataPtr = 0; return ; } //************* GncSchedule******************************************** GncSchedule::GncSchedule() { m_subElementListCount = END_Schedule_SELS; static const QString subEls[] = {"sx:start", "sx:last", "sx:end", "gnc:freqspec", "gnc:recurrence", "sx:deferredInstance"}; m_subElementList = subEls; m_dataElementListCount = END_Schedule_DELS; static const QString dataEls[] = {"sx:name", "sx:enabled", "sx:autoCreate", "sx:autoCreateNotify", "sx:autoCreateDays", "sx:advanceCreateDays", "sx:advanceRemindDays", "sx:instanceCount", "sx:num-occur", "sx:rem-occur", "sx:templ-acct" }; m_dataElementList = dataEls; static const unsigned int anonClasses[] = {NXTSCHD, ASIS, ASIS, ASIS, ASIS, ASIS, ASIS, ASIS, ASIS, ASIS, ASIS}; m_anonClassList = anonClasses; for (uint i = 0; i < m_dataElementListCount; i++) m_v.append(QString()); m_vpStartDate = m_vpLastDate = m_vpEndDate = 0; m_vpFreqSpec = 0; m_vpRecurrence.clear(); m_vpSchedDef = 0; } GncSchedule::~GncSchedule() { delete m_vpStartDate; delete m_vpLastDate; delete m_vpEndDate; delete m_vpFreqSpec; delete m_vpSchedDef; } GncObject *GncSchedule::startSubEl() { if (pMain->xmldebug) qDebug("Schedule start subel m_state %d", m_state); TRY { GncObject *next = 0; switch (m_state) { case STARTDATE: case LASTDATE: case ENDDATE: next = new GncDate; break; case FREQ: next = new GncFreqSpec; break; case RECURRENCE: next = new GncRecurrence; break; case DEFINST: next = new GncSchedDef; break; default: throw MYMONEYEXCEPTION("GncSchedule rcvd invalid m_state"); } return (next); } PASS } void GncSchedule::endSubEl(GncObject *subObj) { if (pMain->xmldebug) qDebug("Schedule end subel"); switch (m_state) { case STARTDATE: m_vpStartDate = static_cast(subObj); break; case LASTDATE: m_vpLastDate = static_cast(subObj); break; case ENDDATE: m_vpEndDate = static_cast(subObj); break; case FREQ: m_vpFreqSpec = static_cast(subObj); break; case RECURRENCE: m_vpRecurrence.append(static_cast(subObj)); break; case DEFINST: m_vpSchedDef = static_cast(subObj); break; } return ; } void GncSchedule::terminate() { TRY { pMain->convertSchedule(this); return ; } PASS } //************* GncFreqSpec******************************************** GncFreqSpec::GncFreqSpec() { m_subElementListCount = END_FreqSpec_SELS; static const QString subEls[] = {"gnc:freqspec"}; m_subElementList = subEls; m_dataElementListCount = END_FreqSpec_DELS; static const QString dataEls[] = {"fs:ui_type", "fs:monthly", "fs:daily", "fs:weekly", "fs:interval", "fs:offset", "fs:day" }; m_dataElementList = dataEls; static const unsigned int anonClasses[] = {ASIS, ASIS, ASIS, ASIS, ASIS, ASIS, ASIS }; m_anonClassList = anonClasses; for (uint i = 0; i < m_dataElementListCount; i++) m_v.append(QString()); } GncFreqSpec::~GncFreqSpec() {} GncObject *GncFreqSpec::startSubEl() { TRY { if (pMain->xmldebug) qDebug("FreqSpec start subel m_state %d", m_state); GncObject *next = 0; switch (m_state) { case COMPO: next = new GncFreqSpec; break; default: throw MYMONEYEXCEPTION("GncFreqSpec rcvd invalid m_state"); } return (next); } PASS } void GncFreqSpec::endSubEl(GncObject *subObj) { if (pMain->xmldebug) qDebug("FreqSpec end subel"); switch (m_state) { case COMPO: m_fsList.append(subObj); break; } m_dataPtr = 0; return ; } void GncFreqSpec::terminate() { pMain->convertFreqSpec(this); return ; } //************* GncRecurrence******************************************** GncRecurrence::GncRecurrence() : m_vpStartDate(0) { m_subElementListCount = END_Recurrence_SELS; static const QString subEls[] = {"recurrence:start"}; m_subElementList = subEls; m_dataElementListCount = END_Recurrence_DELS; static const QString dataEls[] = {"recurrence:mult", "recurrence:period_type"}; m_dataElementList = dataEls; static const unsigned int anonClasses[] = {ASIS, ASIS}; m_anonClassList = anonClasses; for (uint i = 0; i < m_dataElementListCount; i++) m_v.append(QString()); } GncRecurrence::~GncRecurrence() { delete m_vpStartDate; } GncObject *GncRecurrence::startSubEl() { TRY { if (pMain->xmldebug) qDebug("Recurrence start subel m_state %d", m_state); GncObject *next = 0; switch (m_state) { case STARTDATE: next = new GncDate; break; default: throw MYMONEYEXCEPTION("GncRecurrence rcvd invalid m_state"); } return (next); } PASS } void GncRecurrence::endSubEl(GncObject *subObj) { if (pMain->xmldebug) qDebug("Recurrence end subel"); switch (m_state) { case STARTDATE: m_vpStartDate = static_cast(subObj); break; } m_dataPtr = 0; return ; } void GncRecurrence::terminate() { pMain->convertRecurrence(this); return ; } QString GncRecurrence::getFrequency() const { // This function converts a gnucash 2.2 recurrence specification into it's previous equivalent // This will all need re-writing when MTE finishes the schedule re-write if (periodType() == "once") return("once"); if ((periodType() == "day") && (mult() == "1")) return("daily"); if (periodType() == "week") { if (mult() == "1") return ("weekly"); if (mult() == "2") return ("bi_weekly"); if (mult() == "4") return ("four-weekly"); } if (periodType() == "month") { if (mult() == "1") return ("monthly"); if (mult() == "2") return ("two-monthly"); if (mult() == "3") return ("quarterly"); if (mult() == "4") return ("tri_annually"); if (mult() == "6") return ("semi_yearly"); if (mult() == "12") return ("yearly"); if (mult() == "24") return ("two-yearly"); } return ("unknown"); } //************* GncSchedDef******************************************** GncSchedDef::GncSchedDef() { // process ing for this sub-object is undefined at the present time m_subElementListCount = 0; m_dataElementListCount = 0; } GncSchedDef::~GncSchedDef() {} /************************************************************************************************ XML Reader ************************************************************************************************/ XmlReader::XmlReader(MyMoneyGncReader *pM) : m_source(0), m_reader(0), m_co(0), pMain(pM), m_headerFound(false) { } void XmlReader::processFile(QIODevice* pDevice) { m_source = new QXmlInputSource(pDevice); // set up the Qt XML reader m_reader = new QXmlSimpleReader; m_reader->setContentHandler(this); // go read the file if (!m_reader->parse(m_source)) { throw MYMONEYEXCEPTION(i18n("Input file cannot be parsed; may be corrupt\n%1", errorString())); } delete m_reader; delete m_source; return ; } // XML handling routines bool XmlReader::startDocument() { m_co = new GncFile; // create initial object, push to stack , pass it the 'main' pointer m_os.push(m_co); m_co->setPm(pMain); m_headerFound = false; #ifdef _GNCFILEANON pMain->oStream << ""; lastType = -1; indentCount = 0; #endif // _GNCFILEANON return (true); } bool XmlReader::startElement(const QString&, const QString&, const QString& elName , const QXmlAttributes& elAttrs) { try { if (pMain->gncdebug) qDebug() << "XML start -" << elName; #ifdef _GNCFILEANON int i; QString spaces; // anonymizer - write data if (elName == "gnc:book" || elName == "gnc:count-data" || elName == "book:id") lastType = -1; pMain->oStream << endl; switch (lastType) { case 0: indentCount += 2; // tricky fall through here case 2: spaces.fill(' ', indentCount); pMain->oStream << spaces.toLatin1(); break; } pMain->oStream << '<' << elName; for (i = 0; i < elAttrs.count(); ++i) { pMain->oStream << ' ' << elAttrs.qName(i) << '=' << '"' << elAttrs.value(i) << '"'; } pMain->oStream << '>'; lastType = 0; #else if ((!m_headerFound) && (elName != "gnc-v2")) throw MYMONEYEXCEPTION(i18n("Invalid header for file. Should be 'gnc-v2'")); m_headerFound = true; #endif // _GNCFILEANON m_co->checkVersion(elName, elAttrs, pMain->m_versionList); // check if this is a sub object element; if so, push stack and initialize GncObject *temp = m_co->isSubElement(elName, elAttrs); if (temp != 0) { m_os.push(temp); m_co = m_os.top(); m_co->setVersion(elAttrs.value("version")); m_co->setPm(pMain); // pass the 'main' pointer to the sub object // return true; // removed, as we hit a return true anyway } #if 0 // check for a data element if (m_co->isDataElement(elName, elAttrs)) return (true); #endif else { // reduced the above to m_co->isDataElement(elName, elAttrs); } } catch (const MyMoneyException &e) { #ifndef _GNCFILEANON // we can't pass on exceptions here coz the XML reader won't catch them and we just abort KMessageBox::error(0, i18n("Import failed:\n\n%1", e.what()), PACKAGE); qWarning("%s", qPrintable(e.what())); #else qWarning("%s", e->toLatin1()); #endif // _GNCFILEANON } return true; // to keep compiler happy } bool XmlReader::endElement(const QString&, const QString&, const QString&elName) { try { if (pMain->xmldebug) qDebug() << "XML end -" << elName; #ifdef _GNCFILEANON QString spaces; switch (lastType) { case 2: indentCount -= 2; spaces.fill(' ', indentCount); pMain->oStream << endl << spaces.toLatin1(); break; } pMain->oStream << "' ; lastType = 2; #endif // _GNCFILEANON m_co->resetDataPtr(); // so we don't get extraneous data loaded into the variables if (elName == m_co->getElName()) { // check if this is the end of the current object if (pMain->gncdebug) m_co->debugDump(); // dump the object data (temp) // call the terminate routine, pop the stack, and advise the parent that it's done m_co->terminate(); GncObject *temp = m_co; m_os.pop(); m_co = m_os.top(); m_co->endSubEl(temp); } return (true); } catch (const MyMoneyException &e) { #ifndef _GNCFILEANON // we can't pass on exceptions here coz the XML reader won't catch them and we just abort KMessageBox::error(0, i18n("Import failed:\n\n%1", e.what()), PACKAGE); qWarning("%s", qPrintable(e.what())); #else qWarning("%s", e->toLatin1()); #endif // _GNCFILEANON } return (true); // to keep compiler happy } bool XmlReader::characters(const QString &data) { if (pMain->xmldebug) qDebug("XML Data received - %d bytes", data.length()); QString pData = data.trimmed(); // data may contain line feeds and indentation spaces if (!pData.isEmpty()) { if (pMain->developerDebug) qDebug() << "XML Data -" << pData; m_co->storeData(pData); //go store it #ifdef _GNCFILEANON QString anonData = m_co->getData(); if (anonData.isEmpty()) anonData = pData; // there must be a Qt standard way of doing the following but I can't ... find it anonData.replace('<', "<"); anonData.replace('>', ">"); anonData.replace('&', "&"); pMain->oStream << anonData; // write original data lastType = 1; #endif // _GNCFILEANON } return (true); } bool XmlReader::endDocument() { #ifdef _GNCFILEANON pMain->oStream << endl << endl; pMain->oStream << "" << endl; pMain->oStream << "" << endl; pMain->oStream << "" << endl; #endif // _GNCFILEANON return (true); } /******************************************************************************************* Main class for this module Controls overall operation of the importer ********************************************************************************************/ //***************** Constructor *********************** MyMoneyGncReader::MyMoneyGncReader() : m_dropSuspectSchedules(0), m_investmentOption(0), m_useFinanceQuote(0), m_useTxNotes(0), gncdebug(0), xmldebug(0), bAnonymize(0), developerDebug(0), m_xr(0), m_progressCallback(0), m_ccCount(0), m_orCount(0), m_scCount(0), m_potentialTransfer(0), m_suspectSchedule(false) { #ifndef _GNCFILEANON m_storage = 0; #endif // _GNCFILEANON // to hold gnucash count data (only used for progress bar) m_gncCommodityCount = m_gncAccountCount = m_gncTransactionCount = m_gncScheduleCount = 0; m_smallBusinessFound = m_budgetsFound = m_lotsFound = false; m_commodityCount = m_priceCount = m_accountCount = m_transactionCount = m_templateCount = m_scheduleCount = 0; m_decoder = 0; // build a list of valid versions static const QString versionList[] = {"gnc:book 2.0.0", "gnc:commodity 2.0.0", "gnc:pricedb 1", "gnc:account 2.0.0", "gnc:transaction 2.0.0", "gnc:schedxaction 1.0.0", "gnc:schedxaction 2.0.0", // for gnucash 2.2 onward "gnc:freqspec 1.0.0", "zzz" // zzz = stopper }; unsigned int i; for (i = 0; versionList[i] != "zzz"; ++i) m_versionList[versionList[i].section(' ', 0, 0)].append(versionList[i].section(' ', 1, 1)); } //***************** Destructor ************************* MyMoneyGncReader::~MyMoneyGncReader() {} //**************************** Main Entry Point ************************************ #ifndef _GNCFILEANON void MyMoneyGncReader::readFile(QIODevice* pDevice, MyMoneyStorageMgr* storage) { Q_CHECK_PTR(pDevice); Q_CHECK_PTR(storage); m_storage = storage; qDebug("Entering gnucash importer"); setOptions(); // get a file anonymization factor from the user if (bAnonymize) setFileHideFactor(); //m_defaultPayee = createPayee (i18n("Unknown payee")); MyMoneyFile::instance()->attachStorage(m_storage); + loadAllCurrencies(); MyMoneyFileTransaction ft; m_xr = new XmlReader(this); bool blocked = MyMoneyFile::instance()->signalsBlocked(); MyMoneyFile::instance()->blockSignals(true); try { m_xr->processFile(pDevice); terminate(); // do all the wind-up things ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::error(0, i18n("Import failed:\n\n%1", e.what()), PACKAGE); qWarning("%s", qPrintable(e.what())); } // end catch MyMoneyFile::instance()->blockSignals(blocked); MyMoneyFile::instance()->detachStorage(m_storage); signalProgress(0, 1, i18n("Import complete")); // switch off progress bar delete m_xr; signalProgress(0, 1, i18nc("Application is ready to use", "Ready.")); // application is ready for input qDebug("Exiting gnucash importer"); } #else // Control code for the file anonymizer void MyMoneyGncReader::readFile(QString in, QString out) { QFile pDevice(in); if (!pDevice.open(QIODevice::ReadOnly)) qWarning("Can't open input file"); QFile outFile(out); if (!outFile.open(QIODevice::WriteOnly)) qWarning("Can't open output file"); oStream.setDevice(&outFile); bAnonymize = true; // get a file anonymization factor from the user setFileHideFactor(); m_xr = new XmlReader(this); try { m_xr->processFile(&pDevice); } catch (const MyMoneyException &e) { qWarning("%s", e->toLatin1()); } // end catch delete m_xr; pDevice.close(); outFile.close(); return ; } #include int main(int argc, char ** argv) { QApplication a(argc, argv); MyMoneyGncReader m; QString inFile, outFile; if (argc > 0) inFile = a.argv()[1]; if (argc > 1) outFile = a.argv()[2]; if (inFile.isEmpty()) { inFile = KFileDialog::getOpenFileName("", "Gnucash files(*.nc *)", 0); } if (inFile.isEmpty()) qWarning("Input file required"); if (outFile.isEmpty()) outFile = inFile + ".anon"; m.readFile(inFile, outFile); exit(0); } #endif // _GNCFILEANON void MyMoneyGncReader::setFileHideFactor() { #define MINFILEHIDEF 0.01 #define MAXFILEHIDEF 99.99 srand(QTime::currentTime().second()); // seed randomizer for anonymize m_fileHideFactor = 0.0; while (m_fileHideFactor == 0.0) { m_fileHideFactor = QInputDialog::getDouble(0, i18n("Disguise your wealth"), i18n("Each monetary value on your file will be multiplied by a random number between 0.01 and 1.99\n" "with a different value used for each transaction. In addition, to further disguise the true\n" "values, you may enter a number between %1 and %2 which will be applied to all values.\n" "These numbers will not be stored in the file.", MINFILEHIDEF, MAXFILEHIDEF), (1.0 + (int)(1000.0 * rand() / (RAND_MAX + 1.0))) / 100.0, MINFILEHIDEF, MAXFILEHIDEF, 2); } } #ifndef _GNCFILEANON //********************************* convertCommodity ******************************************* void MyMoneyGncReader::convertCommodity(const GncCommodity *gcm) { Q_CHECK_PTR(gcm); MyMoneySecurity equ; if (m_commodityCount == 0) signalProgress(0, m_gncCommodityCount, i18n("Loading commodities...")); if (!gcm->isCurrency()) { // currencies should not be present here but... equ.setName(gcm->name()); equ.setTradingSymbol(gcm->id()); equ.setTradingMarket(gcm->space()); // the 'space' may be market or quote source, dep on what the user did // don't set the source here since he may not want quotes //equ.setValue ("kmm-online-source", gcm->space()); // we don't know, so use it as both equ.setTradingCurrency(""); // not available here, will set from pricedb or transaction equ.setSecurityType(Security::Type::Stock); // default to it being a stock //tell the storage objects we have a new equity object. equ.setSmallestAccountFraction(gcm->fraction().toInt()); m_storage->addSecurity(equ); //assign the gnucash id as the key into the map to find our id if (gncdebug) qDebug() << "mapping, key =" << gcm->id() << "id =" << equ.id(); m_mapEquities[gcm->id().toUtf8()] = equ.id(); } signalProgress(++m_commodityCount, 0); return ; } //******************************* convertPrice ************************************************ void MyMoneyGncReader::convertPrice(const GncPrice *gpr) { Q_CHECK_PTR(gpr); // add this to our price history if (m_priceCount == 0) signalProgress(0, 1, i18n("Loading prices...")); MyMoneyMoney rate(convBadValue(gpr->value())); if (gpr->commodity()->isCurrency()) { MyMoneyPrice exchangeRate(gpr->commodity()->id().toUtf8(), gpr->currency()->id().toUtf8(), gpr->priceDate(), rate, i18n("Imported History")); if (!exchangeRate.rate(QString()).isZero()) m_storage->addPrice(exchangeRate); } else { MyMoneySecurity e = m_storage->security(m_mapEquities[gpr->commodity()->id().toUtf8()]); if (gncdebug) qDebug() << "Searching map, key = " << gpr->commodity()->id() << ", found id =" << e.id().data(); e.setTradingCurrency(gpr->currency()->id().toUtf8()); MyMoneyPrice stockPrice(e.id(), gpr->currency()->id().toUtf8(), gpr->priceDate(), rate, i18n("Imported History")); if (!stockPrice.rate(QString()).isZero()) m_storage->addPrice(stockPrice); m_storage->modifySecurity(e); } signalProgress(++m_priceCount, 0); return ; } //*********************************convertAccount **************************************** void MyMoneyGncReader::convertAccount(const GncAccount* gac) { Q_CHECK_PTR(gac); TRY { // we don't care about the GNC root account if ("ROOT" == gac->type()) { m_rootId = gac->id().toUtf8(); return; } MyMoneyAccount acc; if (m_accountCount == 0) signalProgress(0, m_gncAccountCount, i18n("Loading accounts...")); acc.setName(gac->name()); acc.setDescription(gac->desc()); QDate currentDate = QDate::currentDate(); acc.setOpeningDate(currentDate); acc.setLastModified(currentDate); acc.setLastReconciliationDate(currentDate); if (gac->commodity()->isCurrency()) { acc.setCurrencyId(gac->commodity()->id().toUtf8()); m_currencyCount[gac->commodity()->id()]++; } acc.setParentAccountId(gac->parent().toUtf8()); // now determine the account type and its parent id /* This list taken from # Feb 2006: A RELAX NG Compact schema for gnucash "v2" XML files. # Copyright (C) 2006 Joshua Sled "NO_TYPE" "BANK" "CASH" "CREDIT" "ASSET" "LIABILITY" "STOCK" "MUTUAL" "CURRENCY" "INCOME" "EXPENSE" "EQUITY" "RECEIVABLE" "PAYABLE" "CHECKING" "SAVINGS" "MONEYMRKT" "CREDITLINE" Some don't seem to be used in practice. Not sure what CREDITLINE s/be converted as. */ if ("BANK" == gac->type() || "CHECKING" == gac->type()) { acc.setAccountType(Account::Type::Checkings); } else if ("SAVINGS" == gac->type()) { acc.setAccountType(Account::Type::Savings); } else if ("ASSET" == gac->type()) { acc.setAccountType(Account::Type::Asset); } else if ("CASH" == gac->type()) { acc.setAccountType(Account::Type::Cash); } else if ("CURRENCY" == gac->type()) { acc.setAccountType(Account::Type::Cash); } else if ("STOCK" == gac->type() || "MUTUAL" == gac->type()) { // gnucash allows a 'broker' account to be denominated as type STOCK, but with // a currency balance. We do not need to create a stock account for this // actually, the latest version of gnc (1.8.8) doesn't seem to allow you to do // this any more, though I do have one in my own account... if (gac->commodity()->isCurrency()) { acc.setAccountType(Account::Type::Investment); } else { acc.setAccountType(Account::Type::Stock); } } else if ("EQUITY" == gac->type()) { acc.setAccountType(Account::Type::Equity); } else if ("LIABILITY" == gac->type()) { acc.setAccountType(Account::Type::Liability); } else if ("CREDIT" == gac->type()) { acc.setAccountType(Account::Type::CreditCard); } else if ("INCOME" == gac->type()) { acc.setAccountType(Account::Type::Income); } else if ("EXPENSE" == gac->type()) { acc.setAccountType(Account::Type::Expense); } else if ("RECEIVABLE" == gac->type()) { acc.setAccountType(Account::Type::Asset); } else if ("PAYABLE" == gac->type()) { acc.setAccountType(Account::Type::Liability); } else if ("MONEYMRKT" == gac->type()) { acc.setAccountType(Account::Type::MoneyMarket); } else { // we have here an account type we can't currently handle QString em = i18n("Current importer does not recognize GnuCash account type %1", gac->type()); throw MYMONEYEXCEPTION(em); } // if no parent account is present, assign to one of our standard accounts if ((acc.parentAccountId().isEmpty()) || (acc.parentAccountId() == m_rootId)) { switch (acc.accountGroup()) { case Account::Type::Asset: acc.setParentAccountId(m_storage->asset().id()); break; case Account::Type::Liability: acc.setParentAccountId(m_storage->liability().id()); break; case Account::Type::Income: acc.setParentAccountId(m_storage->income().id()); break; case Account::Type::Expense: acc.setParentAccountId(m_storage->expense().id()); break; case Account::Type::Equity: acc.setParentAccountId(m_storage->equity().id()); break; default: break; // not necessary but avoids compiler warnings } } // extra processing for a stock account if (acc.accountType() == Account::Type::Stock) { // save the id for later linking to investment account m_stockList.append(gac->id()); // set the equity type MyMoneySecurity e = m_storage->security(m_mapEquities[gac->commodity()->id().toUtf8()]); if (gncdebug) qDebug() << "Acct equity search, key =" << gac->commodity()->id() << "found id =" << e.id(); acc.setCurrencyId(e.id()); // actually, the security id if ("MUTUAL" == gac->type()) { e.setSecurityType(Security::Type::MutualFund); if (gncdebug) qDebug() << "Setting" << e.name() << "to mutual"; m_storage->modifySecurity(e); } QString priceSource = gac->getKvpValue("price-source", "string"); if (!priceSource.isEmpty()) getPriceSource(e, priceSource); } if (gac->getKvpValue("tax-related", "integer") == QChar('1')) acc.setValue("Tax", "Yes"); // all the details from the file about the account should be known by now. // calling addAccount will automatically fill in the account ID. m_storage->addAccount(acc); m_mapIds[gac->id().toUtf8()] = acc.id(); // to link gnucash id to ours for tx posting if (gncdebug) qDebug() << "Gnucash account" << gac->id() << "has id of" << acc.id() << ", type of" << MyMoneyAccount::accountTypeToString(acc.accountType()) << "parent is" << acc.parentAccountId(); signalProgress(++m_accountCount, 0); return ; } PASS } //********************************************** convertTransaction ***************************** void MyMoneyGncReader::convertTransaction(const GncTransaction *gtx) { Q_CHECK_PTR(gtx); MyMoneyTransaction tx; MyMoneySplit split; unsigned int i; if (m_transactionCount == 0) signalProgress(0, m_gncTransactionCount, i18n("Loading transactions...")); // initialize class variables related to transactions m_txCommodity = ""; m_txPayeeId = ""; m_potentialTransfer = true; m_splitList.clear(); m_liabilitySplitList.clear(); m_otherSplitList.clear(); // payee, dates, commodity if (!gtx->desc().isEmpty()) m_txPayeeId = createPayee(gtx->desc()); tx.setEntryDate(gtx->dateEntered()); tx.setPostDate(gtx->datePosted()); m_txDatePosted = tx.postDate(); // save for use in splits m_txChequeNo = gtx->no(); // ditto tx.setCommodity(gtx->currency().toUtf8()); m_txCommodity = tx.commodity(); // save in storage, maybe needed for Orphan accounts // process splits for (i = 0; i < gtx->splitCount(); i++) { convertSplit(static_cast(gtx->getSplit(i))); } // handle the odd case of just one split, which gnc allows, // by just duplicating the split // of course, we should change the sign but this case has only ever been seen // when the balance is zero, and can cause kmm to crash, so... if (gtx->splitCount() == 1) { convertSplit(static_cast(gtx->getSplit(0))); } m_splitList += m_liabilitySplitList += m_otherSplitList; // the splits are in order in splitList. Link them to the tx. also, determine the // action type, and fill in some fields which gnc holds at transaction level // first off, is it a transfer (can only have 2 splits?) // also, a tx with just 2 splits is shown by GnuCash as non-split bool nonSplitTx = true; if (m_splitList.count() != 2) { m_potentialTransfer = false; nonSplitTx = false; } QString slotMemo = gtx->getKvpValue(QString("notes")); if (!slotMemo.isEmpty()) tx.setMemo(slotMemo); QList::iterator it = m_splitList.begin(); while (!m_splitList.isEmpty()) { split = *it; // at this point, if m_potentialTransfer is still true, it is actually one! if (m_potentialTransfer) split.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::Transfer)); if ((m_useTxNotes) // if use txnotes option is set && (nonSplitTx) // and it's a (GnuCash) non-split transaction && (!tx.memo().isEmpty())) // and tx notes are present split.setMemo(tx.memo()); // use the tx notes as memo tx.addSplit(split); it = m_splitList.erase(it); } m_storage->addTransaction(tx, true); // all done, add the transaction to storage signalProgress(++m_transactionCount, 0); return ; } //******************************************convertSplit******************************** void MyMoneyGncReader::convertSplit(const GncSplit *gsp) { Q_CHECK_PTR(gsp); MyMoneySplit split; MyMoneyAccount splitAccount; // find the kmm account id corresponding to the gnc id QString kmmAccountId; map_accountIds::const_iterator id = m_mapIds.constFind(gsp->acct().toUtf8()); if (id != m_mapIds.constEnd()) { kmmAccountId = id.value(); } else { // for the case where the acs not found (which shouldn't happen?), create an account with gnc name kmmAccountId = createOrphanAccount(gsp->acct()); } // find the account pointer and save for later splitAccount = m_storage->account(kmmAccountId); // print some data so we can maybe identify this split later // TODO : prints personal data //if (gncdebug) qDebug ("Split data - gncid %s, kmmid %s, memo %s, value %s, recon state %s", // gsp->acct().toLatin1(), kmmAccountId.data(), gsp->memo().toLatin1(), gsp->value().toLatin1(), // gsp->recon().toLatin1()); // payee id split.setPayeeId(m_txPayeeId.toUtf8()); // reconciled state and date switch (gsp->recon().at(0).toLatin1()) { case 'n': split.setReconcileFlag(eMyMoney::Split::State::NotReconciled); break; case 'c': split.setReconcileFlag(eMyMoney::Split::State::Cleared); break; case 'y': split.setReconcileFlag(eMyMoney::Split::State::Reconciled); break; } split.setReconcileDate(gsp->reconDate()); // memo split.setMemo(gsp->memo()); // accountId split.setAccountId(kmmAccountId); // cheque no split.setNumber(m_txChequeNo); // value and quantity MyMoneyMoney splitValue(convBadValue(gsp->value())); if (gsp->value() == "-1/0") { // treat gnc invalid value as zero // it's not quite a consistency check, but easier to treat it as such m_messageList["CC"].append (i18n("Account or Category %1, transaction date %2; split contains invalid value; please check", splitAccount.name(), m_txDatePosted.toString(Qt::ISODate))); } MyMoneyMoney splitQuantity(convBadValue(gsp->qty())); split.setValue(splitValue); // if split currency = tx currency, set shares = value (14/10/05) if (splitAccount.currencyId() == m_txCommodity) { split.setShares(splitValue); } else { split.setShares(splitQuantity); } // in kmm, the first split is important. in this routine we will // save the splits in our split list with the priority: // 1. assets // 2. liabilities // 3. others (categories) // but keeping each in same order as gnucash switch (splitAccount.accountGroup()) { case Account::Type::Asset: if (splitAccount.accountType() == Account::Type::Stock) { split.value().isZero() ? split.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::AddShares)) : // free shares? split.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::BuyShares)); m_potentialTransfer = false; // ? // add a price history entry MyMoneySecurity e = m_storage->security(splitAccount.currencyId()); MyMoneyMoney price; if (!split.shares().isZero()) { static const signed64 NEW_DENOM = 10000; price = split.value() / split.shares(); price = MyMoneyMoney(price.toDouble(), NEW_DENOM); } if (!price.isZero()) { TRY { // we can't use m_storage->security coz security list is not built yet m_storage->currency(m_txCommodity); // will throw exception if not currency e.setTradingCurrency(m_txCommodity); if (gncdebug) qDebug() << "added price for" << e.name() << price.toString() << "date" << m_txDatePosted.toString(Qt::ISODate); m_storage->modifySecurity(e); MyMoneyPrice dealPrice(e.id(), m_txCommodity, m_txDatePosted, price, i18n("Imported Transaction")); m_storage->addPrice(dealPrice); } CATCH { // stock transfer; treat like free shares? split.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::AddShares)); } } } else { // not stock if (split.value().isNegative()) { bool isNumeric = false; if (!split.number().isEmpty()) { split.number().toLong(&isNumeric); // No QString.isNumeric()?? } if (isNumeric) { split.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::Check)); } else { split.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal)); } } else { split.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit)); } } m_splitList.append(split); break; case Account::Type::Liability: split.value().isNegative() ? split.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal)) : split.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit)); m_liabilitySplitList.append(split); break; default: m_potentialTransfer = false; m_otherSplitList.append(split); } // backdate the account opening date if necessary if (m_txDatePosted < splitAccount.openingDate()) { splitAccount.setOpeningDate(m_txDatePosted); m_storage->modifyAccount(splitAccount); } return ; } //********************************* convertTemplateTransaction ********************************************** MyMoneyTransaction MyMoneyGncReader::convertTemplateTransaction(const QString& schedName, const GncTransaction *gtx) { Q_CHECK_PTR(gtx); MyMoneyTransaction tx; MyMoneySplit split; unsigned int i; if (m_templateCount == 0) signalProgress(0, 1, i18n("Loading templates...")); // initialize class variables related to transactions m_txCommodity = ""; m_txPayeeId = ""; m_potentialTransfer = true; m_splitList.clear(); m_liabilitySplitList.clear(); m_otherSplitList.clear(); // payee, dates, commodity if (!gtx->desc().isEmpty()) { m_txPayeeId = createPayee(gtx->desc()); } else { m_txPayeeId = createPayee(i18n("Unknown payee")); // schedules require a payee tho normal tx's don't. not sure why... } tx.setEntryDate(gtx->dateEntered()); tx.setPostDate(gtx->datePosted()); m_txDatePosted = tx.postDate(); tx.setCommodity(gtx->currency().toUtf8()); m_txCommodity = tx.commodity(); // save for possible use in orphan account // process splits for (i = 0; i < gtx->splitCount(); i++) { convertTemplateSplit(schedName, static_cast(gtx->getSplit(i))); } // determine the action type for the splits and link them to the template tx if (!m_otherSplitList.isEmpty()) m_potentialTransfer = false; // tfrs can occur only between assets and asset/liabilities m_splitList += m_liabilitySplitList += m_otherSplitList; // the splits are in order in splitList. Transfer them to the tx // also, determine the action type. first off, is it a transfer (can only have 2 splits?) if (m_splitList.count() != 2) m_potentialTransfer = false; // at this point, if m_potentialTransfer is still true, it is actually one! QString txMemo = ""; QList::iterator it = m_splitList.begin(); while (!m_splitList.isEmpty()) { split = *it; if (m_potentialTransfer) { split.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::Transfer)); } else { if (split.value().isNegative()) { //split.setAction (negativeActionType); split.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal)); } else { //split.setAction (positiveActionType); split.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit)); } } split.setNumber(gtx->no()); // set cheque no (or equivalent description) // Arbitrarily, save the first non-null split memo as the memo for the whole tx // I think this is necessary because txs with just 2 splits (the majority) // are not viewable as split transactions in kmm so the split memo is not seen if ((txMemo.isEmpty()) && (!split.memo().isEmpty())) txMemo = split.memo(); tx.addSplit(split); it = m_splitList.erase(it); } // memo - set from split tx.setMemo(txMemo); signalProgress(++m_templateCount, 0); return (tx); } //********************************* convertTemplateSplit **************************************************** void MyMoneyGncReader::convertTemplateSplit(const QString& schedName, const GncTemplateSplit *gsp) { Q_CHECK_PTR(gsp); // convertTemplateSplit MyMoneySplit split; MyMoneyAccount splitAccount; unsigned int i, j; bool nonNumericFormula = false; // action, value and account will be set from slots // reconcile state, always Not since it hasn't even been posted yet (?) split.setReconcileFlag(eMyMoney::Split::State::NotReconciled); // memo split.setMemo(gsp->memo()); // payee id split.setPayeeId(m_txPayeeId.toUtf8()); // read split slots (KVPs) int xactionCount = 0; int validSlotCount = 0; QString gncAccountId; for (i = 0; i < gsp->kvpCount(); i++) { const GncKvp& slot = gsp->getKvp(i); if ((slot.key() == "sched-xaction") && (slot.type() == "frame")) { bool bFoundStringCreditFormula = false; bool bFoundStringDebitFormula = false; bool bFoundGuidAccountId = false; QString gncCreditFormula, gncDebitFormula; for (j = 0; j < slot.kvpCount(); j++) { const GncKvp& subSlot = slot.getKvp(j); // again, see comments above. when we have a full specification // of all the options available to us, we can no doubt improve on this if ((subSlot.key() == "credit-formula") && (subSlot.type() == "string")) { gncCreditFormula = subSlot.value(); bFoundStringCreditFormula = true; } if ((subSlot.key() == "debit-formula") && (subSlot.type() == "string")) { gncDebitFormula = subSlot.value(); bFoundStringDebitFormula = true; } if ((subSlot.key() == "account") && (subSlot.type() == "guid")) { gncAccountId = subSlot.value(); bFoundGuidAccountId = true; } } // all data read, now check we have everything if ((bFoundStringCreditFormula) && (bFoundStringDebitFormula) && (bFoundGuidAccountId)) { if (gncdebug) qDebug() << "Found valid slot; credit" << gncCreditFormula << "debit" << gncDebitFormula << "acct" << gncAccountId; validSlotCount++; } // validate numeric, work out sign MyMoneyMoney exFormula; exFormula.setNegativeMonetarySignPosition(eMyMoney::Money::BeforeQuantityMoney); QString numericTest; char crdr = 0 ; if (!gncCreditFormula.isEmpty()) { crdr = 'C'; numericTest = gncCreditFormula; } else if (!gncDebitFormula.isEmpty()) { crdr = 'D'; numericTest = gncDebitFormula; } KMyMoneyMoneyValidator v(0); int pos; // useless, but required for validator if (v.validate(numericTest, pos) == QValidator::Acceptable) { switch (crdr) { case 'C': exFormula = QString("-" + numericTest); break; case 'D': exFormula = numericTest; } } else { if (gncdebug) qDebug() << numericTest << "is not numeric"; nonNumericFormula = true; } split.setValue(exFormula); xactionCount++; } else { m_messageList["SC"].append( i18n("Schedule %1 contains unknown action (key = %2, type = %3)", schedName, slot.key(), slot.type())); m_suspectSchedule = true; } } // report this as untranslatable tx if (xactionCount > 1) { m_messageList["SC"].append( i18n("Schedule %1 contains multiple actions; only one has been imported", schedName)); m_suspectSchedule = true; } if (validSlotCount == 0) { m_messageList["SC"].append( i18n("Schedule %1 contains no valid splits", schedName)); m_suspectSchedule = true; } if (nonNumericFormula) { m_messageList["SC"].append( i18n("Schedule %1 appears to contain a formula. GnuCash formulae are not convertible", schedName)); m_suspectSchedule = true; } // find the kmm account id corresponding to the gnc id QString kmmAccountId; map_accountIds::const_iterator id = m_mapIds.constFind(gncAccountId.toUtf8()); if (id != m_mapIds.constEnd()) { kmmAccountId = id.value(); } else { // for the case where the acs not found (which shouldn't happen?), create an account with gnc name kmmAccountId = createOrphanAccount(gncAccountId); } splitAccount = m_storage->account(kmmAccountId); split.setAccountId(kmmAccountId); // if split currency = tx currency, set shares = value (14/10/05) if (splitAccount.currencyId() == m_txCommodity) { split.setShares(split.value()); } /* else { //FIXME: scheduled currency or investment tx needs to be investigated split.setShares (splitQuantity); } */ // add the split to one of the lists switch (splitAccount.accountGroup()) { case Account::Type::Asset: m_splitList.append(split); break; case Account::Type::Liability: m_liabilitySplitList.append(split); break; default: m_otherSplitList.append(split); } // backdate the account opening date if necessary if (m_txDatePosted < splitAccount.openingDate()) { splitAccount.setOpeningDate(m_txDatePosted); m_storage->modifyAccount(splitAccount); } return ; } //********************************* convertSchedule ******************************************************** void MyMoneyGncReader::convertSchedule(const GncSchedule *gsc) { TRY { Q_CHECK_PTR(gsc); MyMoneySchedule sc; MyMoneyTransaction tx; m_suspectSchedule = false; QDate startDate, nextDate, lastDate, endDate; // for date calculations QDate today = QDate::currentDate(); int numOccurs, remOccurs; if (m_scheduleCount == 0) signalProgress(0, m_gncScheduleCount, i18n("Loading schedules...")); // schedule name sc.setName(gsc->name()); // find the transaction template as stored earlier QList::const_iterator itt; for (itt = m_templateList.constBegin(); itt != m_templateList.constEnd(); ++itt) { // the id to match against is the split:account value in the splits if (static_cast((*itt)->getSplit(0))->acct() == gsc->templId()) break; } if (itt == m_templateList.constEnd()) { throw MYMONEYEXCEPTION(i18n("Cannot find template transaction for schedule %1", sc.name())); } else { tx = convertTemplateTransaction(sc.name(), *itt); } tx.clearId(); // define the conversion table for intervals struct convIntvl { QString gncType; // the gnucash name unsigned char interval; // for date calculation unsigned int intervalCount; Schedule::Occurrence occ; // equivalent occurrence code Schedule::WeekendOption wo; }; /* other intervals supported by gnc according to Josh Sled's schema (see above) "none" "semi_monthly" */ /* some of these type names do not appear in gnucash and are difficult to generate for pre 2.2 files.They can be generated for 2.2 however, by GncRecurrence::getFrequency() */ static convIntvl vi [] = { {"once", 'o', 1, Schedule::Occurrence::Once, Schedule::WeekendOption::MoveNothing }, {"daily" , 'd', 1, Schedule::Occurrence::Daily, Schedule::WeekendOption::MoveNothing }, //{"daily_mf", 'd', 1, Schedule::Occurrence::Daily, Schedule::WeekendOption::MoveAfter }, doesn't work, need new freq in kmm {"30-days" , 'd', 30, Schedule::Occurrence::EveryThirtyDays, Schedule::WeekendOption::MoveNothing }, {"weekly", 'w', 1, Schedule::Occurrence::Weekly, Schedule::WeekendOption::MoveNothing }, {"bi_weekly", 'w', 2, Schedule::Occurrence::EveryOtherWeek, Schedule::WeekendOption::MoveNothing }, {"three-weekly", 'w', 3, Schedule::Occurrence::EveryThreeWeeks, Schedule::WeekendOption::MoveNothing }, {"four-weekly", 'w', 4, Schedule::Occurrence::EveryFourWeeks, Schedule::WeekendOption::MoveNothing }, {"eight-weekly", 'w', 8, Schedule::Occurrence::EveryEightWeeks, Schedule::WeekendOption::MoveNothing }, {"monthly", 'm', 1, Schedule::Occurrence::Monthly, Schedule::WeekendOption::MoveNothing }, {"two-monthly", 'm', 2, Schedule::Occurrence::EveryOtherMonth, Schedule::WeekendOption::MoveNothing }, {"quarterly", 'm', 3, Schedule::Occurrence::Quarterly, Schedule::WeekendOption::MoveNothing }, {"tri_annually", 'm', 4, Schedule::Occurrence::EveryFourMonths, Schedule::WeekendOption::MoveNothing }, {"semi_yearly", 'm', 6, Schedule::Occurrence::TwiceYearly, Schedule::WeekendOption::MoveNothing }, {"yearly", 'y', 1, Schedule::Occurrence::Yearly, Schedule::WeekendOption::MoveNothing }, {"two-yearly", 'y', 2, Schedule::Occurrence::EveryOtherYear, Schedule::WeekendOption::MoveNothing }, {"zzz", 'y', 1, Schedule::Occurrence::Yearly, Schedule::WeekendOption::MoveNothing} // zzz = stopper, may cause problems. what else can we do? }; QString frequency = "unknown"; // set default to unknown frequency bool unknownOccurs = false; // may have zero, or more than one frequency/recurrence spec QString schedEnabled; if (gsc->version() == "2.0.0") { if (gsc->m_vpRecurrence.count() != 1) { unknownOccurs = true; } else { const GncRecurrence *gre = gsc->m_vpRecurrence.first(); //qDebug (QString("Sched %1, pt %2, mu %3, sd %4").arg(gsc->name()).arg(gre->periodType()) // .arg(gre->mult()).arg(gre->startDate().toString(Qt::ISODate))); frequency = gre->getFrequency(); schedEnabled = gsc->enabled(); } sc.setOccurrence(Schedule::Occurrence::Once); // FIXME - how to convert } else { // find this interval const GncFreqSpec *fs = gsc->getFreqSpec(); if (fs == 0) { unknownOccurs = true; } else { frequency = fs->intervalType(); if (!fs->m_fsList.isEmpty()) unknownOccurs = true; // nested freqspec } schedEnabled = 'y'; // earlier versions did not have an enable flag } int i; for (i = 0; vi[i].gncType != "zzz"; i++) { if (frequency == vi[i].gncType) break; } if (vi[i].gncType == "zzz") { m_messageList["SC"].append( i18n("Schedule %1 has interval of %2 which is not currently available", sc.name(), frequency)); i = 0; // treat as single occurrence m_suspectSchedule = true; } if (unknownOccurs) { m_messageList["SC"].append( i18n("Schedule %1 contains unknown interval specification; please check for correct operation", sc.name())); m_suspectSchedule = true; } // set the occurrence interval, weekend option, start date sc.setOccurrence(vi[i].occ); sc.setWeekendOption(vi[i].wo); sc.setStartDate(gsc->startDate()); // if a last date was specified, use it, otherwise try to work out the last date sc.setLastPayment(gsc->lastDate()); numOccurs = gsc->numOccurs().toInt(); if (sc.lastPayment() == QDate()) { nextDate = lastDate = gsc->startDate(); while ((nextDate < today) && (numOccurs-- != 0)) { lastDate = nextDate; nextDate = incrDate(lastDate, vi[i].interval, vi[i].intervalCount); } sc.setLastPayment(lastDate); } // under Tom's new regime, the tx dates are the next due date (I think) tx.setPostDate(incrDate(sc.lastPayment(), vi[i].interval, vi[i].intervalCount)); tx.setEntryDate(incrDate(sc.lastPayment(), vi[i].interval, vi[i].intervalCount)); // if an end date was specified, use it, otherwise if the input file had a number // of occurs remaining, work out the end date sc.setEndDate(gsc->endDate()); numOccurs = gsc->numOccurs().toInt(); remOccurs = gsc->remOccurs().toInt(); if ((sc.endDate() == QDate()) && (remOccurs > 0)) { endDate = sc.lastPayment(); while (remOccurs-- > 0) { endDate = incrDate(endDate, vi[i].interval, vi[i].intervalCount); } sc.setEndDate(endDate); } // Check for sched deferred interval. Don't know how/if we can handle it, or even what it means... if (gsc->getSchedDef() != 0) { m_messageList["SC"].append( i18n("Schedule %1 contains a deferred interval specification; please check for correct operation", sc.name())); m_suspectSchedule = true; } // payment type, options sc.setPaymentType((Schedule::PaymentType)Schedule::PaymentType::Other); sc.setFixed(!m_suspectSchedule); // if any probs were found, set it as variable so user will always be prompted // we don't currently have a 'disable' option, but just make sure auto-enter is off if not enabled //qDebug(QString("%1 and %2").arg(gsc->autoCreate()).arg(schedEnabled)); sc.setAutoEnter((gsc->autoCreate() == QChar('y')) && (schedEnabled == QChar('y'))); //qDebug(QString("autoEnter set to %1").arg(sc.autoEnter())); // type QString actionType = tx.splits().first().action(); if (actionType == MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit)) { sc.setType((Schedule::Type)Schedule::Type::Deposit); } else if (actionType == MyMoneySplit::actionName(eMyMoney::Split::Action::Transfer)) { sc.setType((Schedule::Type)Schedule::Type::Transfer); } else { sc.setType((Schedule::Type)Schedule::Type::Bill); } // finally, set the transaction pointer sc.setTransaction(tx); //tell the storage objects we have a new schedule object. if (m_suspectSchedule && m_dropSuspectSchedules) { m_messageList["SC"].append( i18n("Schedule %1 dropped at user request", sc.name())); } else { m_storage->addSchedule(sc); if (m_suspectSchedule) m_suspectList.append(sc.id()); } signalProgress(++m_scheduleCount, 0); return ; } PASS } //********************************* convertFreqSpec ******************************************************** void MyMoneyGncReader::convertFreqSpec(const GncFreqSpec *) { // Nowt to do here at the moment, convertSched only retrieves the interval type // but we will probably need to look into the nested freqspec when we properly implement semi-monthly and stuff return ; } //********************************* convertRecurrence ******************************************************** void MyMoneyGncReader::convertRecurrence(const GncRecurrence *) { return ; } //********************************************************************************************************** //************************************* terminate ********************************************************** void MyMoneyGncReader::terminate() { TRY { // All data has been converted and added to storage // this code is just temporary to show us what is in the file. if (gncdebug) qDebug("%d accounts found in the GnuCash file", (unsigned int)m_mapIds.count()); for (map_accountIds::const_iterator it = m_mapIds.constBegin(); it != m_mapIds.constEnd(); ++it) { if (gncdebug) qDebug() << "key =" << it.key() << "value =" << it.value(); } // first step is to implement the users investment option, now we // have all the accounts available QList::iterator stocks; for (stocks = m_stockList.begin(); stocks != m_stockList.end(); ++stocks) { checkInvestmentOption(*stocks); } // Next step is to walk the list and assign the parent/child relationship between the objects. unsigned int i = 0; signalProgress(0, m_accountCount, i18n("Reorganizing accounts...")); QList list; QList::iterator acc; m_storage->accountList(list); for (acc = list.begin(); acc != list.end(); ++acc) { if ((*acc).parentAccountId() == m_storage->asset().id()) { MyMoneyAccount assets = m_storage->asset(); m_storage->addAccount(assets, (*acc)); if (gncdebug) qDebug() << "Account id" << (*acc).id() << "is a child of the main asset account"; } else if ((*acc).parentAccountId() == m_storage->liability().id()) { MyMoneyAccount liabilities = m_storage->liability(); m_storage->addAccount(liabilities, (*acc)); if (gncdebug) qDebug() << "Account id" << (*acc).id() << "is a child of the main liability account"; } else if ((*acc).parentAccountId() == m_storage->income().id()) { MyMoneyAccount incomes = m_storage->income(); m_storage->addAccount(incomes, (*acc)); if (gncdebug) qDebug() << "Account id" << (*acc).id() << "is a child of the main income account"; } else if ((*acc).parentAccountId() == m_storage->expense().id()) { MyMoneyAccount expenses = m_storage->expense(); m_storage->addAccount(expenses, (*acc)); if (gncdebug) qDebug() << "Account id" << (*acc).id() << "is a child of the main expense account"; } else if ((*acc).parentAccountId() == m_storage->equity().id()) { MyMoneyAccount equity = m_storage->equity(); m_storage->addAccount(equity, (*acc)); if (gncdebug) qDebug() << "Account id" << (*acc).id() << "is a child of the main equity account"; } else if ((*acc).parentAccountId() == m_rootId) { if (gncdebug) qDebug() << "Account id" << (*acc).id() << "is a child of the main root account"; } else { // it is not under one of the main accounts, so find gnucash parent QString parentKey = (*acc).parentAccountId(); if (gncdebug) qDebug() << "Account id" << (*acc).id() << "is a child of " << (*acc).parentAccountId(); map_accountIds::const_iterator id = m_mapIds.constFind(parentKey); if (id != m_mapIds.constEnd()) { if (gncdebug) qDebug() << "Setting account id" << (*acc).id() << "parent account id to" << id.value(); MyMoneyAccount parent = m_storage->account(id.value()); parent = checkConsistency(parent, (*acc)); m_storage->addAccount(parent, (*acc)); } else { throw MYMONEYEXCEPTION("terminate() could not find account id"); } } signalProgress(++i, 0); } // end for account signalProgress(0, 1, (".")); // debug - get rid of reorg message // offer the most common account currency as a default QString mainCurrency = ""; unsigned int maxCount = 0; QMap::ConstIterator it; for (it = m_currencyCount.constBegin(); it != m_currencyCount.constEnd(); ++it) { if (it.value() > maxCount) { maxCount = it.value(); mainCurrency = it.key(); } } if (mainCurrency != "") { QString question = i18n("Your main currency seems to be %1 (%2); do you want to set this as your base currency?", mainCurrency, m_storage->currency(mainCurrency.toUtf8()).name()); if (KMessageBox::questionYesNo(0, question, PACKAGE) == KMessageBox::Yes) { m_storage->setValue("kmm-baseCurrency", mainCurrency); } } // now produce the end of job reports - first, work out which ones are required QList sectionsToReport; // list of sections needing report sectionsToReport.append("MN"); // always build the main section if ((m_ccCount = m_messageList["CC"].count()) > 0) sectionsToReport.append("CC"); if ((m_orCount = m_messageList["OR"].count()) > 0) sectionsToReport.append("OR"); if ((m_scCount = m_messageList["SC"].count()) > 0) sectionsToReport.append("SC"); // produce the sections in separate message boxes bool exit = false; int si; for (si = 0; (si < sectionsToReport.count()) && !exit; ++si) { QString button0Text = i18nc("Button to show more detailed data", "More"); if (si + 1 == sectionsToReport.count()) button0Text = i18nc("Button to close the current dialog", "Done"); // last section KGuiItem yesItem(button0Text, QIcon(), "", ""); KGuiItem noItem(i18n("Save Report"), QIcon(), "", ""); switch (KMessageBox::questionYesNoCancel(0, buildReportSection(sectionsToReport[si]), PACKAGE, yesItem, noItem)) { case KMessageBox::Yes: break; case KMessageBox::No: exit = writeReportToFile(sectionsToReport); break; default: exit = true; break; } } for (si = 0; si < m_suspectList.count(); ++si) { auto sc = m_storage->schedule(m_suspectList[si]); KMessageBox::information(0, i18n("Problems were encountered in converting schedule '%1'.", sc.name()), PACKAGE); // TODO: return this feature // switch (KMessageBox::warningYesNo(0, i18n("Problems were encountered in converting schedule '%1'.\nDo you want to review or edit it now?", sc.name()), PACKAGE)) { // case KMessageBox::Yes: // auto s = new KEditScheduleDlg(sc); // if (s->exec()) // m_storage->modifySchedule(s->schedule()); // delete s; // break; // default: // break; // } } } PASS } //************************************ buildReportSection************************************ QString MyMoneyGncReader::buildReportSection(const QString& source) { TRY { QString s = ""; bool more = false; if (source == "MN") { s.append(i18n("Found:\n\n")); s.append(i18np("%1 commodity (equity)\n", "%1 commodities (equities)\n", m_commodityCount)); s.append(i18np("%1 price\n", "%1 prices\n", m_priceCount)); s.append(i18np("%1 account\n", "%1 accounts\n", m_accountCount)); s.append(i18np("%1 transaction\n", "%1 transactions\n", m_transactionCount)); s.append(i18np("%1 schedule\n", "%1 schedules\n", m_scheduleCount)); s.append("\n\n"); if (m_ccCount == 0) { s.append(i18n("No inconsistencies were detected\n")); } else { s.append(i18np("%1 inconsistency was detected and corrected\n", "%1 inconsistencies were detected and corrected\n", m_ccCount)); more = true; } if (m_orCount > 0) { s.append("\n\n"); s.append(i18np("%1 orphan account was created\n", "%1 orphan accounts were created\n", m_orCount)); more = true; } if (m_scCount > 0) { s.append("\n\n"); s.append(i18np("%1 possible schedule problem was noted\n", "%1 possible schedule problems were noted\n", m_scCount)); more = true; } QString unsupported(""); QString lineSep("\n - "); if (m_smallBusinessFound) unsupported.append(lineSep + i18n("Small Business Features (Customers, Invoices, etc.)")); if (m_budgetsFound) unsupported.append(lineSep + i18n("Budgets")); if (m_lotsFound) unsupported.append(lineSep + i18n("Lots")); if (!unsupported.isEmpty()) { unsupported.prepend(i18n("The following features found in your file are not currently supported:")); s.append(unsupported); } if (more) s.append(i18n("\n\nPress More for further information")); } else { s = m_messageList[source].join(QChar('\n')); } if (gncdebug) qDebug() << s; return (static_cast(s)); } PASS } //************************ writeReportToFile********************************* bool MyMoneyGncReader::writeReportToFile(const QList& sectionsToReport) { TRY { int i; QString fd = QFileDialog::getSaveFileName(0, QString(), QString(), i18n("Save report as")); if (fd.isEmpty()) return (false); QFile reportFile(fd); if (!reportFile.open(QIODevice::WriteOnly)) { return (false); } QTextStream stream(&reportFile); for (i = 0; i < sectionsToReport.count(); i++) stream << buildReportSection(sectionsToReport[i]) << endl; reportFile.close(); return (true); } PASS } /**************************************************************************** Utility routines *****************************************************************************/ //************************ createPayee *************************** QString MyMoneyGncReader::createPayee(const QString& gncDescription) { MyMoneyPayee payee; TRY { payee = m_storage->payeeByName(gncDescription); } CATCH { // payee not found, create one payee.setName(gncDescription); m_storage->addPayee(payee); } return (payee.id()); } //************************************** createOrphanAccount ******************************* QString MyMoneyGncReader::createOrphanAccount(const QString& gncName) { MyMoneyAccount acc; acc.setName("orphan_" + gncName); acc.setDescription(i18n("Orphan created from unknown GnuCash account")); QDate today = QDate::currentDate(); acc.setOpeningDate(today); acc.setLastModified(today); acc.setLastReconciliationDate(today); acc.setCurrencyId(m_txCommodity); acc.setAccountType(Account::Type::Asset); acc.setParentAccountId(m_storage->asset().id()); m_storage->addAccount(acc); // assign the gnucash id as the key into the map to find our id m_mapIds[gncName.toUtf8()] = acc.id(); m_messageList["OR"].append( i18n("One or more transactions contain a reference to an otherwise unknown account\n" "An asset account with the name %1 has been created to hold the data", acc.name())); return (acc.id()); } //****************************** incrDate ********************************************* QDate MyMoneyGncReader::incrDate(QDate lastDate, unsigned char interval, unsigned int intervalCount) { TRY { switch (interval) { case 'd': return (lastDate.addDays(intervalCount)); case 'w': return (lastDate.addDays(intervalCount * 7)); case 'm': return (lastDate.addMonths(intervalCount)); case 'y': return (lastDate.addYears(intervalCount)); case 'o': // once-only return (lastDate); } throw MYMONEYEXCEPTION(i18n("Internal error - invalid interval char in incrDate")); QDate r = QDate(); return (r); // to keep compiler happy } PASS } //********************************* checkConsistency ********************************** MyMoneyAccount MyMoneyGncReader::checkConsistency(MyMoneyAccount& parent, MyMoneyAccount& child) { TRY { // gnucash is flexible/weird enough to allow various inconsistencies // these are a couple I found in my file, no doubt more will be discovered if ((child.accountType() == Account::Type::Investment) && (parent.accountType() != Account::Type::Asset)) { m_messageList["CC"].append( i18n("An Investment account must be a child of an Asset account\n" "Account %1 will be stored under the main Asset account", child.name())); return m_storage->asset(); } if ((child.accountType() == Account::Type::Income) && (parent.accountType() != Account::Type::Income)) { m_messageList["CC"].append( i18n("An Income account must be a child of an Income account\n" "Account %1 will be stored under the main Income account", child.name())); return m_storage->income(); } if ((child.accountType() == Account::Type::Expense) && (parent.accountType() != Account::Type::Expense)) { m_messageList["CC"].append( i18n("An Expense account must be a child of an Expense account\n" "Account %1 will be stored under the main Expense account", child.name())); return m_storage->expense(); } return (parent); } PASS } //*********************************** checkInvestmentOption ************************* void MyMoneyGncReader::checkInvestmentOption(QString stockId) { // implement the investment option for stock accounts // first check whether the parent account (gnucash id) is actually an // investment account. if it is, no further action is needed MyMoneyAccount stockAcc = m_storage->account(m_mapIds[stockId.toUtf8()]); MyMoneyAccount parent; QString parentKey = stockAcc.parentAccountId(); map_accountIds::const_iterator id = m_mapIds.constFind(parentKey); if (id != m_mapIds.constEnd()) { parent = m_storage->account(id.value()); if (parent.accountType() == Account::Type::Investment) return ; } // so now, check the investment option requested by the user // option 0 creates a separate investment account for each stock account if (m_investmentOption == 0) { MyMoneyAccount invAcc(stockAcc); invAcc.setAccountType(Account::Type::Investment); invAcc.setCurrencyId(QString("")); // we don't know what currency it is!! invAcc.setParentAccountId(parentKey); // intersperse it between old parent and child stock acct m_storage->addAccount(invAcc); m_mapIds [invAcc.id()] = invAcc.id(); // so stock account gets parented (again) to investment account later if (gncdebug) qDebug() << "Created investment account" << invAcc.name() << "as id" << invAcc.id() << "parent" << invAcc.parentAccountId(); if (gncdebug) qDebug() << "Setting stock" << stockAcc.name() << "id" << stockAcc.id() << "as child of" << invAcc.id(); stockAcc.setParentAccountId(invAcc.id()); m_storage->addAccount(invAcc, stockAcc); // investment option 1 creates a single investment account for all stocks } else if (m_investmentOption == 1) { static QString singleInvAccId = ""; MyMoneyAccount singleInvAcc; bool ok = false; if (singleInvAccId.isEmpty()) { // if the account has not yet been created QString invAccName; while (!ok) { invAccName = QInputDialog::getText(0, QStringLiteral(PACKAGE), i18n("Enter the investment account name "), QLineEdit::Normal, i18n("My Investments"), &ok); } singleInvAcc.setName(invAccName); singleInvAcc.setAccountType(Account::Type::Investment); singleInvAcc.setCurrencyId(QString("")); singleInvAcc.setParentAccountId(m_storage->asset().id()); m_storage->addAccount(singleInvAcc); m_mapIds [singleInvAcc.id()] = singleInvAcc.id(); // so stock account gets parented (again) to investment account later if (gncdebug) qDebug() << "Created investment account" << singleInvAcc.name() << "as id" << singleInvAcc.id() << "parent" << singleInvAcc.parentAccountId() << "reparenting stock"; singleInvAccId = singleInvAcc.id(); } else { // the account has already been created singleInvAcc = m_storage->account(singleInvAccId); } m_storage->addAccount(singleInvAcc, stockAcc); // add stock as child // the original intention of option 2 was to allow any asset account to be converted to an investment (broker) account // however, since we have already stored the accounts as asset, we have no way at present of changing their type // the only alternative would be to hold all the gnucash data in memory, then implement this option, then convert all the data // that would mean a major overhaul of the code. Perhaps I'll think of another way... } else if (m_investmentOption == 2) { static int lastSelected = 0; MyMoneyAccount invAcc(stockAcc); QStringList accList; QList list; QList::iterator acc; m_storage->accountList(list); // build a list of candidates for the input box for (acc = list.begin(); acc != list.end(); ++acc) { // if (((*acc).accountGroup() == Account::Type::Asset) && ((*acc).accountType() != Account::Type::Stock)) accList.append ((*acc).name()); if ((*acc).accountType() == Account::Type::Investment) accList.append((*acc).name()); } //if (accList.isEmpty()) qWarning ("No available accounts"); bool ok = false; while (!ok) { // keep going till we have a valid investment parent QString invAccName = QInputDialog::getItem(0, PACKAGE, i18n("Select parent investment account or enter new name. Stock %1", stockAcc.name()), accList, lastSelected, true, &ok); if (ok) { lastSelected = accList.indexOf(invAccName); // preserve selection for next time for (acc = list.begin(); acc != list.end(); ++acc) { if ((*acc).name() == invAccName) break; } if (acc != list.end()) { // an account was selected invAcc = *acc; } else { // a new account name was entered invAcc.setAccountType(Account::Type::Investment); invAcc.setName(invAccName); invAcc.setCurrencyId(QString("")); invAcc.setParentAccountId(m_storage->asset().id()); m_storage->addAccount(invAcc); ok = true; } if (invAcc.accountType() == Account::Type::Investment) { ok = true; } else { // this code is probably not going to be implemented coz we can't change account types (??) #if 0 QMessageBox mb(PACKAGE, i18n("%1 is not an Investment Account. Do you wish to make it one?", invAcc.name()), QMessageBox::Question, QMessageBox::Yes | QMessageBox::Default, QMessageBox::No | QMessageBox::Escape, Qt::NoButton); switch (mb.exec()) { case QMessageBox::No : ok = false; break; default: // convert it - but what if it has splits??? qWarning("Not yet implemented"); ok = true; break; } #endif switch (KMessageBox::questionYesNo(0, i18n("%1 is not an Investment Account. Do you wish to make it one?", invAcc.name()), PACKAGE)) { case KMessageBox::Yes: // convert it - but what if it has splits??? qWarning("Not yet implemented"); ok = true; break; default: ok = false; break; } } } // end if ok - user pressed Cancel } // end while !ok m_mapIds [invAcc.id()] = invAcc.id(); // so stock account gets parented (again) to investment account later m_storage->addAccount(invAcc, stockAcc); } else { // investment option != 0, 1, 2 qWarning("Invalid investment option %d", m_investmentOption); } } // get the price source for a stock (gnc account) where online quotes are requested void MyMoneyGncReader::getPriceSource(MyMoneySecurity stock, QString gncSource) { // if he wants to use Finance::Quote, no conversion of source name is needed if (m_useFinanceQuote) { stock.setValue("kmm-online-quote-system", "Finance::Quote"); stock.setValue("kmm-online-source", gncSource.toLower()); m_storage->modifySecurity(stock); return; } // first check if we have already asked about this source // (mapSources is initialy empty. We may be able to pre-fill it with some equivalent // sources, if such things do exist. User feedback may help here.) QMap::const_iterator it; for (it = m_mapSources.constBegin(); it != m_mapSources.constEnd(); ++it) { if (it.key() == gncSource) { stock.setValue("kmm-online-source", it.value()); m_storage->modifySecurity(stock); return; } } // not found in map, so ask the user QPointer dlg = new KGncPriceSourceDlg(stock.name(), gncSource); dlg->exec(); QString s = dlg->selectedSource(); if (!s.isEmpty()) { stock.setValue("kmm-online-source", s); m_storage->modifySecurity(stock); } if (dlg->alwaysUse()) m_mapSources[gncSource] = s; delete dlg; return; } +void MyMoneyGncReader::loadAllCurrencies() +{ + auto file = MyMoneyFile::instance(); + MyMoneyFileTransaction ft; + if (!file->currencyList().isEmpty()) + return; + auto ancientCurrencies = file->ancientCurrencies(); + try { + foreach (auto currency, file->availableCurrencyList()) { + file->addCurrency(currency); + MyMoneyPrice price = ancientCurrencies.value(currency, MyMoneyPrice()); + if (price != MyMoneyPrice()) + file->addPrice(price); + } + ft.commit(); + } catch (const MyMoneyException &e) { + qDebug("Error %s loading currency", qPrintable(e.what())); + } +} + // functions to control the progress bar //*********************** setProgressCallback ***************************** void MyMoneyGncReader::setProgressCallback(void(*callback)(int, int, const QString&)) { m_progressCallback = callback; return ; } //************************** signalProgress ******************************* void MyMoneyGncReader::signalProgress(int current, int total, const QString& msg) { if (m_progressCallback != 0) (*m_progressCallback)(current, total, msg); return ; } #endif // _GNCFILEANON diff --git a/kmymoney/plugins/gnc/import/mymoneygncreader.h b/kmymoney/plugins/gnc/import/mymoneygncreader.h index 78e7bb0df..9c99dfc0d 100644 --- a/kmymoney/plugins/gnc/import/mymoneygncreader.h +++ b/kmymoney/plugins/gnc/import/mymoneygncreader.h @@ -1,1087 +1,1091 @@ /*************************************************************************** mymoneygncreader - description ------------------- begin : Wed Mar 3 2004 copyright : (C) 2000-2004 by Michael Edwardes email : mte@users.sourceforge.net Javier Campos Morales Felix Rodriguez John C Thomas Baumgart Kevin Tambascio ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ /* The main class of this module, MyMoneyGncReader, contains only a readFile() function, which controls the import of data from an XML file created by the current GnuCash version (1.8.8). The XML is processed in class XmlReader, which is an implementation of the Qt SAX2 reader class. Data in the input file is processed as a set of objects which fortunately, though perhaps not surprisingly, have almost a one-for-one correspondence with KMyMoney objects. These objects are bounded by start and end XML elements, and may contain both nested objects (described as sub objects in the code), and data items, also delimited by start and end elements. For example: * start of sub object within file Account Name * data string with start and end elements ... * end of sub objects A GnuCash file may consist of more than one 'book', or set of data. It is not clear how we could currently implement this, so only the first book in a file is processed. This should satisfy most user situations. GnuCash is somewhat inconsistent in its division of the major sections of the file. For example, multiple price history entries are delimited by elements, while each account starts with its own top-level element. In general, the 'container' elements are ignored. XmlReader This is an implementation of the Qt QXmlDefaultHandler class, which provides three main function calls in addition to start and end of document. The startElement() and endElement() calls are self-explanatory, the characters() function provides data strings. Thus in the above example, the sequence of calls would be startElement() for gnc:account startElement() for act:name characters() for 'Account Name' endElement() for act:name ... endElement() for gnc:account Objects Since the processing requirements of XML for most elements are very similar, the common code is implemented in a GncObject class, from which the others are derived, with virtual function calls to cater for any differences. The 'grandfather' object, GncFile representing the file (or more correctly, 'book') as a whole, is created in the startDocument() function call. The constructor function of each object is responsible for providing two lists for the XmlReader to scan, a list of element names which represent sub objects (called sub elements in the code), and a similar list of names representing data elements. In addition, an array of variables (m_v) is provided and initialized, to contain the actual data strings. Implementation Since objects may be nested, a stack is used, with the top element pointing to the 'current object'. The startDocument() call creates the first, GncFile, object at the top of the stack. As each startElement() call occurs, the two#include "mymoneygncreader.h" element lists created by the current object are scanned. If this element represents the start of a sub object, the current object's subEl() function is called to create an instance of the appropriate type. This is then pushed to the top of the stack, and the new object's initiate() function is called. This is used to process any XML attributes attached to the element; GnuCash makes little use of these. If this represents the start of a data element, a pointer (m_dataPointer) is set to point to an entry in the array (m_v) in which a subsequent characters() call can store the actual data. When an endElement() call occurs, a check is made to see if it matches the element name which started the current object. If so, the object's terminate() function is called. If the object represents a similar KMM object, this will normally result in a call to a conversion routine in the main (MyMoneyGncReader) class to convert the data to native format and place it in storage. The stack is then popped, and the parent (now current) object notified by a call to its endSubEl() function. Again depending on the type of object, this will either delete the instance, or save it in its own storage for later processing. For example, a GncSplit object makes little sense outside the context of its transaction, so will be saved by the transaction. A GncTransaction object on the other hand will be converted, along with its attendant splits, and then deleted by its parent. Since at any one time an object will only be processing either a subobject or a data element, a single object variable, m_state, is used to determine the actual type. In effect, it acts as the current index into either the subElement or dataElement list. As an object variable, it will be saved on the stack across subobject processing. Exceptions and Problems Fatal exceptions are processed via the standard MyMoneyException method. Due to differences in implementation between GnuCash and KMM, it is not always possible to provide an absolutely correct conversion. When such a problem situation is recognized, a message, along with any relevant variable data, is passed to the main class, and used to produce a report when processing terminates. Anonymizer When debugging problems, it is often useful to have a trace of what is happening within the module. However, in view of the sensitive nature of personal finance data, most users will be reluctant to provide this. Accordingly, an anonymize (hide()) function is provided to handle data strings. These may either be passed through asis (non-personal data), blanked out (non-critical but possibly personal data), replaced with a generated version (required, but possibly personal), or randomized (monetary amounts). The action for each data item is determined in the object's constructor function along with the creation of the data element list. This module will later be used as the basis of a file anonymizer, which will enable users to safely provide us with a copy of their GnuCash files, and will allow us to test the structure, if not the data content, of the file. */ #ifndef MYMONEYGNCREADER_H #define MYMONEYGNCREADER_H // system includes #include // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #ifndef _GNCFILEANON #include "storage/imymoneystorageformat.h" #endif // _GNCFILEANON // not sure what these are for, but leave them in #define VERSION_0_60_XML 0x10000010 // Version 0.5 file version info #define VERSION_0_61_XML 0x10000011 // use 8 bytes for MyMoneyMoney objects #define GNUCASH_ID_KEY "GNUCASH_ID" class MyMoneyAccount; class MyMoneySecurity; class MyMoneyTransaction; class MyMoneySplit; typedef QMap map_accountIds; typedef map_accountIds::iterator map_accountIds_iter; typedef map_accountIds::const_iterator map_accountIds_citer; typedef QMap map_elementVersions; class MyMoneyGncReader; class QIODevice; class QDate; class QTextCodec; class MyMoneyStorageMgr; class QXmlAttributes; class QXmlInputSource; class QXmlSimpleReader; /** GncObject is the base class for the various objects in the gnucash file Beyond the first level XML objects, elements will be of one of three types: 1. Sub object elements, which require creation of another object to process 2. Data object elements, which are only followed by data to be stored in a variable (m_v array) 3. Ignored objects, data not needed and not included herein */ class GncKvp; class GncObject { public: GncObject(); virtual ~GncObject() {} // make sure to have impl of all virtual rtns to avoid vtable errors? protected: friend class XmlReader; friend class MyMoneyGncReader; // check for sub object element; if it is, create the object GncObject *isSubElement(const QString &elName, const QXmlAttributes& elAttrs); // check for data element; if so, set data pointer bool isDataElement(const QString &elName, const QXmlAttributes& elAttrs); // process start element for 'this'; normally for attribute checking; other initialization done in constructor virtual void initiate(const QString&, const QXmlAttributes&) { return ; }; // a sub object has completed; process the data it gathered virtual void endSubEl(GncObject *) { m_dataPtr = 0; return ; }; // store data for data element void storeData(const QString& pData) { // NB - data MAY come in chunks, and may need to be anonymized if (m_dataPtr != 0) m_dataPtr->append(hide(pData, m_anonClass)); } // following is provided only for a future file anonymizer QString getData() const { return ((m_dataPtr != 0) ? *m_dataPtr : ""); }; void resetDataPtr() { m_dataPtr = 0; }; // process end element for 'this'; usually to convert to KMM format virtual void terminate() { return ; }; void setVersion(const QString& v) { m_version = v; return; }; QString version() const { return (m_version); }; // some gnucash elements have version attribute; check it void checkVersion(const QString&, const QXmlAttributes&, const map_elementVersions&); // get name of element processed by 'this' QString getElName() const { return (m_elementName); }; // pass 'main' pointer to object void setPm(MyMoneyGncReader *pM) { pMain = pM; }; const QString getKvpValue(const QString& key, const QString& type = QString()) const; // debug only void debugDump(); // called by isSubElement to create appropriate sub object virtual GncObject *startSubEl() { return (0); }; // called by isDataElement to set variable pointer virtual void dataEl(const QXmlAttributes&) { m_dataPtr = &(m_v[m_state]); m_anonClass = m_anonClassList[m_state]; }; // return gnucash data string variable pointer virtual QString var(int i) const; // anonymize data virtual QString hide(QString, unsigned int); unsigned int kvpCount() const { return (m_kvpList.count()); }; //! MyMoneyGncReader *pMain; // pointer to 'main' class // used at start of each transaction so same money hide factor is applied to all splits void adjustHideFactor(); QString m_elementName; // save 'this' element's name QString m_version; // and it's gnucash version const QString *m_subElementList; // list of sub object element names for 'this' unsigned int m_subElementListCount; // count of above const QString *m_dataElementList; // ditto for data elements unsigned int m_dataElementListCount; QString *m_dataPtr; // pointer to m_v variable for current data item mutable QList m_v; // storage for variable pointers unsigned int m_state; // effectively, the index to subElementList or dataElementList, whichever is currently in use const unsigned int *m_anonClassList; enum anonActions {ASIS, SUPPRESS, NXTACC, NXTEQU, NXTPAY, NXTSCHD, MAYBEQ, MONEY1, MONEY2}; // anonymize actions - see hide() unsigned int m_anonClass; // class of current data item for anonymizer static double m_moneyHideFactor; // a per-transaction factor QList m_kvpList; //! }; // ***************************************************************************** // This is the 'grandfather' object representing the gnucash file as a whole class GncFile : public GncObject { public: GncFile(); ~GncFile(); private: enum iSubEls {BOOK, COUNT, CMDTY, PRICE, ACCT, TX, TEMPLATES, SCHEDULES, END_FILE_SELS }; virtual GncObject *startSubEl(); virtual void endSubEl(GncObject *); bool m_processingTemplates; // gnc uses same transaction element for ordinary and template tx's; this will distinguish bool m_bookFound; // to detect multi-book files }; // The following are 'utility' objects, which occur within several other object types // ************* GncKvp******************************************** // Key/value pairs, which are introduced by the 'slot' element // Consist of slot:key (the 'name' of the kvp), and slot:value (the data value) // the slot value also contains a slot type (string, integer, etc) implemented as an XML attribute // kvp's may be nested class GncKvp : public GncObject { public: GncKvp(); ~GncKvp(); //protected: friend class MyMoneyGncReader; QString key() const { return (var(KEY)); }; QString value() const { return (var(VALUE)); }; QString type() const { return (m_kvpType); }; const GncKvp getKvp(unsigned int i) const { return (m_kvpList[i]); }; private: // subsidiary objects/elements enum KvpSubEls {KVP, END_Kvp_SELS }; virtual GncObject *startSubEl(); virtual void endSubEl(GncObject *); // data elements enum KvpDataEls {KEY, VALUE, END_Kvp_DELS }; virtual void dataEl(const QXmlAttributes&); QString m_kvpType; // type is an XML attribute }; // ************* GncLot******************************************** // KMM doesn't have support for lots as yet class GncLot : public GncObject { public: GncLot(); ~GncLot(); protected: friend class MyMoneyGncReader; private: }; // **************************************************************************** // commodity specification. consists of // cmdty:space - either ISO4217 if this cmdty is a currency, or, usually, the name of a stock exchange // cmdty:id - ISO4217 currency symbol, or 'ticker symbol' class GncCmdtySpec : public GncObject { public: GncCmdtySpec(); ~GncCmdtySpec(); protected: friend class MyMoneyGncReader; friend class GncTransaction; bool isCurrency() const { return (m_v[CMDTYSPC] == QString("ISO4217")); }; QString id() const { return (m_v[CMDTYID]); }; QString space() const { return (m_v[CMDTYSPC]); }; private: // data elements enum CmdtySpecDataEls {CMDTYSPC, CMDTYID, END_CmdtySpec_DELS}; virtual QString hide(QString, unsigned int); }; // ********************************************************************* // date; maybe one of two types, ts:date which is date/time, gdate which is date only // we do not preserve time data (at present) class GncDate : public GncObject { public: GncDate(); ~GncDate(); protected: friend class MyMoneyGncReader; friend class GncPrice; friend class GncTransaction; friend class GncSplit; friend class GncSchedule; friend class GncRecurrence; const QDate date() const { return (QDate::fromString(m_v[TSDATE].section(' ', 0, 0), Qt::ISODate)); }; private: // data elements enum DateDataEls {TSDATE, GDATE, END_Date_DELS}; virtual void dataEl(const QXmlAttributes&) { m_dataPtr = &(m_v[TSDATE]); m_anonClass = GncObject::ASIS; } ; // treat both date types the same }; /** Following are the main objects within the gnucash file, which correspond largely one-for-one with similar objects in the kmymoney structure, apart from schedules which gnc splits between template (transaction data) and schedule (date data) */ //******************************************************************** class GncCountData : public GncObject { public: GncCountData(); ~GncCountData(); private: virtual void initiate(const QString&, const QXmlAttributes&); virtual void terminate(); QString m_countType; // type of element being counted }; //******************************************************************** class GncCommodity : public GncObject { public: GncCommodity(); ~GncCommodity(); protected: friend class MyMoneyGncReader; // access data values bool isCurrency() const { return (var(SPACE) == QString("ISO4217")); }; QString space() const { return (var(SPACE)); }; QString id() const { return (var(ID)); }; QString name() const { return (var(NAME)); }; QString fraction() const { return (var(FRACTION)); }; private: virtual void terminate(); // data elements enum {SPACE, ID, NAME, FRACTION, END_Commodity_DELS}; }; // ************* GncPrice******************************************** class GncPrice : public GncObject { public: GncPrice(); ~GncPrice(); protected: friend class MyMoneyGncReader; // access data values const GncCmdtySpec *commodity() const { return (m_vpCommodity); }; const GncCmdtySpec *currency() const { return (m_vpCurrency); }; QString value() const { return (var(VALUE)); }; QDate priceDate() const { return (m_vpPriceDate->date()); }; private: virtual void terminate(); // sub object elements enum PriceSubEls {CMDTY, CURR, PRICEDATE, END_Price_SELS }; virtual GncObject *startSubEl(); virtual void endSubEl(GncObject *); // data elements enum PriceDataEls {VALUE, END_Price_DELS }; GncCmdtySpec *m_vpCommodity, *m_vpCurrency; GncDate *m_vpPriceDate; }; // ************* GncAccount******************************************** class GncAccount : public GncObject { public: GncAccount(); ~GncAccount(); protected: friend class MyMoneyGncReader; // access data values GncCmdtySpec *commodity() const { return (m_vpCommodity); }; QString id() const { return (var(ID)); }; QString name() const { return (var(NAME)); }; QString desc() const { return (var(DESC)); }; QString type() const { return (var(TYPE)); }; QString parent() const { return (var(PARENT)); }; private: // subsidiary objects/elements enum AccountSubEls {CMDTY, KVP, LOTS, END_Account_SELS }; virtual GncObject *startSubEl(); virtual void endSubEl(GncObject *); virtual void terminate(); // data elements enum AccountDataEls {ID, NAME, DESC, TYPE, PARENT, END_Account_DELS }; GncCmdtySpec *m_vpCommodity; }; // ************* GncSplit******************************************** class GncSplit : public GncObject { public: GncSplit(); ~GncSplit(); protected: friend class MyMoneyGncReader; // access data values QString id() const { return (var(ID)); }; QString memo() const { return (var(MEMO)); }; QString recon() const { return (var(RECON)); }; QString value() const { return (var(VALUE)); }; QString qty() const { return (var(QTY)); }; QString acct() const { return (var(ACCT)); }; const QDate reconDate() const { QDate x = QDate(); return (m_vpDateReconciled == NULL ? x : m_vpDateReconciled->date()); }; private: // subsidiary objects/elements enum TransactionSubEls {RECDATE, END_Split_SELS }; virtual GncObject *startSubEl(); virtual void endSubEl(GncObject *); // data elements enum SplitDataEls {ID, MEMO, RECON, VALUE, QTY, ACCT, END_Split_DELS }; GncDate *m_vpDateReconciled; }; // ************* GncTransaction******************************************** class GncTransaction : public GncObject { public: GncTransaction(bool processingTemplates); ~GncTransaction(); protected: friend class MyMoneyGncReader; // access data values QString id() const { return (var(ID)); }; QString no() const { return (var(NO)); }; QString desc() const { return (var(DESC)); }; QString currency() const { return (m_vpCurrency == NULL ? QString() : m_vpCurrency->id()); }; QDate dateEntered() const { return (m_vpDateEntered->date()); }; QDate datePosted() const { return (m_vpDatePosted->date()); }; bool isTemplate() const { return (m_template); }; unsigned int splitCount() const { return (m_splitList.count()); }; const GncObject *getSplit(unsigned int i) const { return (m_splitList.at(i)); }; private: // subsidiary objects/elements enum TransactionSubEls {CURRCY, POSTED, ENTERED, SPLIT, KVP, END_Transaction_SELS }; virtual GncObject *startSubEl(); virtual void endSubEl(GncObject *); virtual void terminate(); const GncKvp getKvp(unsigned int i) const { return (m_kvpList.at(i)); }; // data elements enum TransactionDataEls {ID, NO, DESC, END_Transaction_DELS }; GncCmdtySpec *m_vpCurrency; GncDate *m_vpDateEntered, *m_vpDatePosted; mutable QList m_splitList; bool m_template; // true if this is a template for scheduled transaction }; // ************* GncTemplateSplit******************************************** class GncTemplateSplit : public GncObject { public: GncTemplateSplit(); ~GncTemplateSplit(); protected: friend class MyMoneyGncReader; // access data values QString id() const { return (var(ID)); }; QString memo() const { return (var(MEMO)); }; QString recon() const { return (var(RECON)); }; QString value() const { return (var(VALUE)); }; QString qty() const { return (var(QTY)); }; QString acct() const { return (var(ACCT)); }; private: const GncKvp getKvp(unsigned int i) const { return (m_kvpList[i]); }; // subsidiary objects/elements enum TemplateSplitSubEls {KVP, END_TemplateSplit_SELS }; virtual GncObject *startSubEl(); virtual void endSubEl(GncObject *); // data elements enum TemplateSplitDataEls {ID, MEMO, RECON, VALUE, QTY, ACCT, END_TemplateSplit_DELS }; }; // ************* GncSchedule******************************************** class GncFreqSpec; class GncRecurrence; class GncSchedDef; class GncSchedule : public GncObject { public: GncSchedule(); ~GncSchedule(); protected: friend class MyMoneyGncReader; // access data values QString name() const { return (var(NAME)); }; QString enabled() const { return var(ENABLED); }; QString autoCreate() const { return (var(AUTOC)); }; QString autoCrNotify() const { return (var(AUTOCN)); }; QString autoCrDays() const { return (var(AUTOCD)); }; QString advCrDays() const { return (var(ADVCD)); }; QString advCrRemindDays() const { return (var(ADVRD)); }; QString instanceCount() const { return (var(INSTC)); }; QString numOccurs() const { return (var(NUMOCC)); }; QString remOccurs() const { return (var(REMOCC)); }; QString templId() const { return (var(TEMPLID)); }; QDate startDate() const { QDate x = QDate(); return (m_vpStartDate == NULL ? x : m_vpStartDate->date()); }; QDate lastDate() const { QDate x = QDate(); return (m_vpLastDate == NULL ? x : m_vpLastDate->date()); }; QDate endDate() const { QDate x = QDate(); return (m_vpEndDate == NULL ? x : m_vpEndDate->date()); }; const GncFreqSpec *getFreqSpec() const { return (m_vpFreqSpec); }; const GncSchedDef *getSchedDef() const { return (m_vpSchedDef); }; private: // subsidiary objects/elements enum ScheduleSubEls {STARTDATE, LASTDATE, ENDDATE, FREQ, RECURRENCE, DEFINST, END_Schedule_SELS }; virtual GncObject *startSubEl(); virtual void endSubEl(GncObject *); virtual void terminate(); // data elements enum ScheduleDataEls {NAME, ENABLED, AUTOC, AUTOCN, AUTOCD, ADVCD, ADVRD, INSTC, NUMOCC, REMOCC, TEMPLID, END_Schedule_DELS }; GncDate *m_vpStartDate, *m_vpLastDate, *m_vpEndDate; GncFreqSpec *m_vpFreqSpec; mutable QList m_vpRecurrence; // gnc handles multiple occurrences GncSchedDef *m_vpSchedDef; }; // ************* GncFreqSpec******************************************** class GncFreqSpec : public GncObject { public: GncFreqSpec(); ~GncFreqSpec(); protected: friend class MyMoneyGncReader; // access data values (only interval type used at present) QString intervalType() const { return (var(INTVT)); }; private: // subsidiary objects/elements enum FreqSpecSubEls {COMPO, END_FreqSpec_SELS }; virtual GncObject *startSubEl(); virtual void endSubEl(GncObject *); // data elements enum FreqSpecDataEls {INTVT, MONTHLY, DAILY, WEEKLY, INTVI, INTVO, INTVD, END_FreqSpec_DELS}; virtual void terminate(); mutable QList m_fsList; }; // ************* GncRecurrence******************************************** // this object replaces GncFreqSpec from Gnucash 2.2 onwards class GncRecurrence : public GncObject { public: GncRecurrence(); ~GncRecurrence(); protected: friend class MyMoneyGncReader; // access data values QDate startDate() const { QDate x = QDate(); return (m_vpStartDate == NULL ? x : m_vpStartDate->date()); }; QString mult() const { return (var(MULT)); }; QString periodType() const { return (var(PERIODTYPE)); }; QString getFrequency() const; private: // subsidiary objects/elements enum RecurrenceSubEls {STARTDATE, END_Recurrence_SELS }; virtual GncObject *startSubEl(); virtual void endSubEl(GncObject *); // data elements enum RecurrenceDataEls {MULT, PERIODTYPE, END_Recurrence_DELS}; virtual void terminate(); GncDate *m_vpStartDate; }; // ************* GncSchedDef******************************************** // This is a sub-object of GncSchedule, (sx:deferredInstance) function currently unknown class GncSchedDef : public GncObject { public: GncSchedDef(); ~GncSchedDef(); protected: friend class MyMoneyGncReader; private: // subsidiary objects/elements }; // **************************************************************************************** /** XML Reader The XML reader is an implementation of the Qt SAX2 XML parser. It determines the type of object represented by the XMl, and calls the appropriate object functions */ // ***************************************************************************************** class XmlReader : public QXmlDefaultHandler { protected: friend class MyMoneyGncReader; XmlReader(MyMoneyGncReader *pM); // keep pointer to 'main' void processFile(QIODevice*); // main entry point of reader // define xml content handler functions bool startDocument(); bool startElement(const QString&, const QString&, const QString&, const QXmlAttributes&); bool endElement(const QString&, const QString&, const QString&); bool characters(const QString &); bool endDocument(); private: QXmlInputSource *m_source; QXmlSimpleReader *m_reader; QStack m_os; // stack of sub objects GncObject *m_co; // current object, for ease of coding (=== m_os.top) MyMoneyGncReader *pMain; // the 'main' pointer, to pass on to objects bool m_headerFound; // check for gnc-v2 header #ifdef _GNCFILEANON int lastType; // 0 = start element, 1 = data, 2 = end element int indentCount; #endif // _GNCFILEANON }; /** MyMoneyGncReader - Main class for this module Controls overall operation of the importer */ #ifndef _GNCFILEANON class MyMoneyGncReader : public IMyMoneyOperationsFormat { #else class MyMoneyGncReader { #endif // _GNCFILEANON public: MyMoneyGncReader(); virtual ~MyMoneyGncReader(); /** * Import a GnuCash XML file * * @param pDevice : pointer to GnuCash file * @param storage : pointer to MyMoneySerialize storage * * @return void * */ #ifndef _GNCFILEANON void readFile(QIODevice* pDevice, MyMoneyStorageMgr *storage); // main entry point, IODevice is gnucash file void writeFile(QIODevice*, MyMoneyStorageMgr*) { return ; }; // dummy entry needed by kmymoneywiew. we will not be writing #else void readFile(QString, QString); #endif // _GNCFILEANON QTextCodec *m_decoder; protected: friend class GncObject; // pity we can't just say GncObject. And compiler doesn't like multiple friends on one line... friend class GncFile; // there must be a better way... friend class GncDate; friend class GncCmdtySpec; friend class GncKvp; friend class GncLot; friend class GncCountData; friend class GncCommodity; friend class GncPrice; friend class GncAccount; friend class GncTransaction; friend class GncSplit; friend class GncTemplateTransaction; friend class GncTemplateSplit; friend class GncSchedule; friend class GncFreqSpec; friend class GncRecurrence; friend class XmlReader; #ifndef _GNCFILEANON /** functions to convert gnc objects to our equivalent */ void convertCommodity(const GncCommodity *); void convertPrice(const GncPrice *); void convertAccount(const GncAccount *); void convertTransaction(const GncTransaction *); void convertSplit(const GncSplit *); void saveTemplateTransaction(GncTransaction *t) { m_templateList.append(t); }; void convertSchedule(const GncSchedule *); void convertFreqSpec(const GncFreqSpec *); void convertRecurrence(const GncRecurrence *); #else /** functions to convert gnc objects to our equivalent */ void convertCommodity(const GncCommodity *) { return; }; void convertPrice(const GncPrice *) { return; }; void convertAccount(const GncAccount *) { return; }; void convertTransaction(const GncTransaction *) { return; }; void convertSplit(const GncSplit *) { return; }; void saveTemplateTransaction(GncTransaction *t) { return; }; void convertSchedule(const GncSchedule *) { return; }; void convertFreqSpec(const GncFreqSpec *) { return; }; #endif // _GNCFILEANON /** to post messages for final report */ void postMessage(const QString&, const unsigned int, const char *); void postMessage(const QString&, const unsigned int, const char *, const char *); void postMessage(const QString&, const unsigned int, const char *, const char *, const char *); void postMessage(const QString&, const unsigned int, const QStringList&); void setProgressCallback(void(*callback)(int, int, const QString&)); void signalProgress(int current, int total, const QString& = ""); /** user options */ /** Scheduled Transactions Due to differences in implementation, it is not always possible to import scheduled transactions correctly. Though best efforts are made, it may be that some imported transactions cause problems within kmymoney. An attempt is made within the importer to identify potential problem transactions, and setting this option will cause them to be dropped from the file. A report of which were dropped, and why, will be produced. m_dropSuspectSchedules - drop suspect scheduled transactions */ bool m_dropSuspectSchedules; /** Investments In kmymoney, all accounts representing investments (stocks, shares, bonds, etc.) must have an associated investment account (e.g. a broker account). The stock account holds the share balance, the investment account a money balance. Gnucash does not do this, so we cannot automate this function. If you have investments, you must select one of the following options. 0 - create a separate investment account for each stock with the same name as the stock 1 - create a single investment account to hold all stocks - you will be asked for a name 2 - create multiple investment accounts - you will be asked for a name for each stock N.B. :- option 2 doesn't really work quite as desired at present */ unsigned int m_investmentOption; /** Online quotes The user has the option to use the Finance::Quote system, as used by GnuCash, to retrieve online share price quotes */ bool m_useFinanceQuote; /** Tx Notes handling Under some usage conditions, non-split GnuCash transactions may contain residual, usually incorrect, memo data which is not normally visible to the user. When imported into KMyMoney however, due to display differences, this data can become visible. Often, these transactions will have a Notes field describing the real purpose of the transaction. If this option is selected, these notes, if present, will be used to override the extraneous memo data." */ bool m_useTxNotes; // set gnucash counts (not always accurate!) void setGncCommodityCount(int i) { m_gncCommodityCount = i; }; void setGncAccountCount(int i) { m_gncAccountCount = i; }; void setGncTransactionCount(int i) { m_gncTransactionCount = i; }; void setGncScheduleCount(int i) { m_gncScheduleCount = i; }; void setSmallBusinessFound(bool b) { m_smallBusinessFound = b; }; void setBudgetsFound(bool b) { m_budgetsFound = b; }; void setLotsFound(bool b) { m_lotsFound = b; }; /* Debug Options If you don't know what these are, best leave them alone. gncdebug - produce general debug messages xmldebug - produce a trace of the gnucash file XML bAnonymize - hide personal data (account names, payees, etc., randomize money amounts) */ bool gncdebug; // general debug messages bool xmldebug; // xml trace bool bAnonymize; // anonymize input static double m_fileHideFactor; // an overall anonymization factor to be applied to all items bool developerDebug; private: void setOptions(); // to set user options from dialog void setFileHideFactor(); // the following handles the gnucash indicator for a bad value (-1/0) which causes us probs QString convBadValue(QString gncValue) const { return (gncValue == "-1/0" ? "0/1" : gncValue); }; #ifndef _GNCFILEANON MyMoneyTransaction convertTemplateTransaction(const QString&, const GncTransaction *); void convertTemplateSplit(const QString&, const GncTemplateSplit *); #endif // _GNCFILEANON // wind up when all done void terminate(); QString buildReportSection(const QString&); bool writeReportToFile(const QList&); // main storage #ifndef _GNCFILEANON MyMoneyStorageMgr *m_storage; #else QTextStream oStream; #endif // _GNCFILEANON XmlReader *m_xr; /** to hold the callback pointer for the progress bar */ void (*m_progressCallback)(int, int, const QString&); // a map of which versions of the various elements (objects) we can import map_elementVersions m_versionList; // counters holding count data from the Gnc 'count-data' section int m_gncCommodityCount; int m_gncAccountCount; int m_gncTransactionCount; int m_gncScheduleCount; // flags indicating detection of features not (yet?) supported bool m_smallBusinessFound; bool m_budgetsFound; bool m_lotsFound; /** counters for reporting */ int m_commodityCount; int m_priceCount; int m_accountCount; int m_transactionCount; int m_templateCount; int m_scheduleCount; #ifndef _GNCFILEANON // counters for error reporting int m_ccCount, m_orCount, m_scCount; // currency counter QMap m_currencyCount; /** * Map gnucash vs. Kmm ids for accounts, equities, schedules, price sources */ QMap m_mapIds; QString m_rootId; // save the root id for terminate() QMap m_mapEquities; QMap m_mapSchedules; QMap m_mapSources; /** * A list of stock accounts (gnc ids) which will be held till the end so we can implement the user's investment option */ QList m_stockList; /** * Temporary storage areas for transaction processing */ QString m_txCommodity; // save commodity for current transaction QString m_txPayeeId; // gnc has payee at tx level, we need it at split level QDate m_txDatePosted; // ditto for post date QString m_txChequeNo; // ditto for cheque number /** In kmm, the order of splits is critical to some operations. These * areas will hold the splits until we've read them all */ QList m_splitList, m_liabilitySplitList, m_otherSplitList; bool m_potentialTransfer; // to determine whether this might be a transfer /** Schedules are processed through 3 different functions, any of which may set this flag */ bool m_suspectSchedule; /** * A holding area for template txs while we're waiting for the schedules */ QList m_templateList; /** Hold a list of suspect schedule ids for later processing? */ QList m_suspectList; /** * To hold message data till final report */ QMap m_messageList; /** * Internal utility functions */ QString createPayee(const QString&); // create a payee and return it's id QString createOrphanAccount(const QString&); // create unknown account and return the id QDate incrDate(QDate lastDate, unsigned char interval, unsigned int intervalCount); // for date calculations MyMoneyAccount checkConsistency(MyMoneyAccount& parent, MyMoneyAccount& child); // gnucash is sometimes TOO flexible void checkInvestmentOption(QString stockId); // implement user investment option void getPriceSource(MyMoneySecurity stock, QString gncSource); + /** + * This method loads all known currencies and saves them to the storage + */ + void loadAllCurrencies(); #endif // _GNCFILEANON }; #endif // MYMONEYGNCREADER_H diff --git a/kmymoney/plugins/interfaces/kmmappinterface.cpp b/kmymoney/plugins/interfaces/kmmappinterface.cpp index b324a59d9..6ef774828 100644 --- a/kmymoney/plugins/interfaces/kmmappinterface.cpp +++ b/kmymoney/plugins/interfaces/kmmappinterface.cpp @@ -1,71 +1,82 @@ /*************************************************************************** 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(); } QUrl KMyMoneyPlugin::KMMAppInterface::filenameURL() const { return m_app->filenameURL(); } 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(); } diff --git a/kmymoney/plugins/interfaces/kmmappinterface.h b/kmymoney/plugins/interfaces/kmmappinterface.h index 522170e8b..f059f4834 100644 --- a/kmymoney/plugins/interfaces/kmmappinterface.h +++ b/kmymoney/plugins/interfaces/kmmappinterface.h @@ -1,65 +1,73 @@ /*************************************************************************** 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; 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; }; } #endif diff --git a/kmymoney/plugins/interfaces/kmmviewinterface.cpp b/kmymoney/plugins/interfaces/kmmviewinterface.cpp index 4d113f645..3944d4a93 100644 --- a/kmymoney/plugins/interfaces/kmmviewinterface.cpp +++ b/kmymoney/plugins/interfaces/kmmviewinterface.cpp @@ -1,90 +1,73 @@ /*************************************************************************** viewinterface.cpp ------------------- begin : Wed Jan 5 2005 copyright : (C) 2005 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 "kmmviewinterface.h" // ---------------------------------------------------------------------------- // QT Includes #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes #include "kmymoneyview.h" #include "selectedtransactions.h" KMyMoneyPlugin::KMMViewInterface::KMMViewInterface(KMyMoneyView* view, QObject* parent, const char* name) : ViewInterface(parent, name), m_view(view) { connect(m_view, &KMyMoneyView::accountSelected, this, &ViewInterface::accountSelected); connect(m_view, &KMyMoneyView::transactionsSelected, this, &ViewInterface::transactionsSelected); connect(m_view, &KMyMoneyView::accountReconciled, this, &ViewInterface::accountReconciled); // connect(app, &KMyMoneyApp::institutionSelected, this, &ViewInterface::institutionSelected); connect(m_view, &KMyMoneyView::viewStateChanged, this, &ViewInterface::viewStateChanged); - connect(m_view, &KMyMoneyView::kmmFilePlugin, this, &ViewInterface::kmmFilePlugin); -} - -bool KMyMoneyPlugin::KMMViewInterface::readFile(const QUrl &url, IMyMoneyOperationsFormat *pExtReader) -{ - return m_view->readFile(url, pExtReader); } void KMyMoneyPlugin::KMMViewInterface::slotRefreshViews() { m_view->slotRefreshViews(); } void KMyMoneyPlugin::KMMViewInterface::addView(KMyMoneyViewBase* view, const QString& name, View idView) { m_view->addView(view, name, idView); } void KMyMoneyPlugin::KMMViewInterface::removeView(View idView) { m_view->removeView(idView); } -bool KMyMoneyPlugin::KMMViewInterface::fileOpen() -{ - return m_view->fileOpen(); -} - -bool KMyMoneyPlugin::KMMViewInterface::isDatabase() -{ - return m_view->isDatabase(); -} - - //KMyMoneyViewBase* KMyMoneyPlugin::KMMViewInterface::addPage(const QString& item, const QString& icon) //{ // return m_view->addBasePage(item, icon); //} //void KMyMoneyPlugin::KMMViewInterface::addWidget(KMyMoneyViewBase* view, QWidget* w) //{ // if (view && w) // view->addWidget(w); //} diff --git a/kmymoney/plugins/interfaces/kmmviewinterface.h b/kmymoney/plugins/interfaces/kmmviewinterface.h index 7c44eca9f..01db6976e 100644 --- a/kmymoney/plugins/interfaces/kmmviewinterface.h +++ b/kmymoney/plugins/interfaces/kmmviewinterface.h @@ -1,108 +1,87 @@ /*************************************************************************** kmmviewinterface.h ------------------- begin : Wed Jan 5 2005 copyright : (C) 2005 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 KMMVIEWINTERFACE_H #define KMMVIEWINTERFACE_H // ---------------------------------------------------------------------------- // QT Includes // ---------------------------------------------------------------------------- // KDE Includes class KMyMoneyView; // ---------------------------------------------------------------------------- // Project Includes #include "viewinterface.h" namespace KMyMoneyPlugin { /** * This class represents the implementation of the * ViewInterface. */ class KMMViewInterface : public ViewInterface { Q_OBJECT public: KMMViewInterface(KMyMoneyView* view, QObject* parent, const char* name = 0); ~KMMViewInterface() {} /** * This method returns a pointer to a newly created view * with title @p item and icon @p pixmap. * * @param item Name of view * @param icon name for the icon to be used for the view * * @return pointer to KMyMoneyViewBase object */ // KMyMoneyViewBase* addPage(const QString& item, const QString& icon); /** * This method allows to add a widget to the view * created with addPage() * * @param view pointer to view object * @param w pointer to widget */ // void addWidget(KMyMoneyViewBase* view, QWidget* w); - /** - * 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 readFile(const QUrl &url, IMyMoneyOperationsFormat *pExtReader = nullptr) override; - - /** - * 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; - /** * Brings up a dialog to change the list(s) settings and saves them into the * class KMyMoneySettings (a singleton). * * @see KListSettingsDlg * Refreshes all views. Used e.g. after settings have been changed or * data has been loaded from external sources (QIF import). **/ void slotRefreshViews() override; void addView(KMyMoneyViewBase* view, const QString& name, View idView) override; void removeView(View idView) override; private: KMyMoneyView* m_view; }; } // namespace #endif diff --git a/kmymoney/plugins/kmymoneyplugin.h b/kmymoney/plugins/kmymoneyplugin.h index bf759e1e2..2e4fda96d 100644 --- a/kmymoney/plugins/kmymoneyplugin.h +++ b/kmymoney/plugins/kmymoneyplugin.h @@ -1,323 +1,332 @@ /* * This file is part of KMyMoney, A Personal Finance Manager by KDE * Copyright (C) 2005 Thomas Baumgart * Copyright (C) 2015 Christian Dávid * * 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 KMYMONEYPLUGIN_H #define KMYMONEYPLUGIN_H #include // ---------------------------------------------------------------------------- // QT Includes #include // ---------------------------------------------------------------------------- // KDE Includes #include class KToggleAction; // ---------------------------------------------------------------------------- // Project Includes #include "mymoneykeyvaluecontainer.h" class MyMoneyStorageMgr; class MyMoneyAccount; class KMyMoneySettings; +class IMyMoneyOperationsFormat; namespace KMyMoneyPlugin { class AppInterface; } namespace KMyMoneyPlugin { class ImportInterface; } namespace KMyMoneyPlugin { class StatementInterface; } namespace KMyMoneyPlugin { class ViewInterface; } /** * @defgroup KMyMoneyPlugin * * KMyMoney knows several types of plugins. The most common and generic one is KMyMoneyPlugin::Plugin. * * Another group of plugins are just loaded on demand and offer special functions with a tight integration into KMyMoney. Whenever possible you should use this kind of plugins. * At the moment this are the onlineTask and payeeIdentifierData. * * @{ */ namespace KMyMoneyPlugin { /** * This class describes the interface between KMyMoney and it's plugins. * * The plugins are based on Qt 5's plugin system. So you must compile json information into the plugin. * KMyMoney looks into the folder "${PLUGIN_INSTALL_DIR}/kmymoney/" and loads all plugins found there (if the user did not deactivate the plugin). * * The json header of the plugin must comply with the requirements of KCoreAddon's KPluginMetaData class. * To load the plugin at start up the service type "KMyMoney/Plugin" must be set. * * @warning The plugin system for KMyMoney 5 is still in development. Especially the loading of the on-demand plugins (mainly undocumented :( ) will change. * * A basic json header is shown below. * @code{.json} { "KPlugin": { "Authors": [ { "Name": "Author's Names, Second Author", "Email": "E-Mail 1, E-Mail 2" } ], "Description": "Short description for plugin list (translateable)", "EnabledByDefault": true, "Icon": "icon to be shown in plugin list", "Id": "a unique identifier", "License": "see KPluginMetaData for list of predefined licenses (translateable)", "Name": "Name of the plugin (translateable)", "ServiceTypes": [ "KMyMoney/Plugin" ], "Version": "@PROJECT_VERSION@@PROJECT_VERSION_SUFFIX@", } } * @endcode * * This example assumes you are using * @code{.cmake} configure_file(${CMAKE_CURRENT_SOURCE_DIR}/... ${CMAKE_CURRENT_BINARY_DIR}/... @ONLY) @endcode * to replace the version variables using cmake. * * @see http://doc.qt.io/qt-5/plugins-howto.html * @see https://api.kde.org/frameworks/kcoreaddons/html/classKPluginMetaData.html * */ class KMM_PLUGIN_EXPORT Plugin : public QObject, public KXMLGUIClient { Q_OBJECT public: explicit Plugin(QObject* parent = nullptr, const char* name = ""); virtual ~Plugin(); public Q_SLOTS: /** * @brief Called during plug in process */ virtual void plug(); /** * @brief Called before unloading */ virtual void unplug(); /** * @brief Called if the configuration of the plugin was changed * @todo Implement */ virtual void configurationChanged() ; protected: /** See KMyMoneyApp::toggleAction() for a description */ KToggleAction* toggleAction(const QString& name) const; // define interface classes here. The interface classes provide a mechanism // for the plugin to interact with KMyMoney // they are defined in the following form for an interface // named Xxx: // // XxxInterface* xxxInterface(); AppInterface* appInterface() const; ViewInterface* viewInterface() const; StatementInterface* statementInterface() const; ImportInterface* importInterface() const; }; /** * This class describes the interface between the KMyMoney * application and it's ONLINE-BANKING plugins. All online banking plugins * must provide this interface. * * A good tutorial on how to design and develop a plugin * structure for a KDE application (e.g. KMyMoney) can be found at * http://developer.kde.org/documentation/tutorials/developing-a-plugin-structure/index.html * */ class KMM_PLUGIN_EXPORT OnlinePlugin { public: OnlinePlugin(); virtual ~OnlinePlugin(); virtual void protocols(QStringList& protocolList) const = 0; /** * This method returns a pointer to a widget representing an additional * tab that will be added to the KNewAccountDlg. The string referenced * with @a tabName will be filled with the text that should be placed * on the tab. It should return 0 if no additional tab is needed. * * Information about the account can be taken out of @a account. * * Once the pointer to the widget is returned to KMyMoney, it takes care * of destruction of all included widgets when the dialog is closed. The plugin * can access the widgets created after the call to storeConfigParameters() * happened. */ virtual QWidget* accountConfigTab(const MyMoneyAccount& account, QString& tabName) = 0; /** * This method is called by the framework whenever it is time to store * the configuration data maintained by the plugin. The plugin should use * the widgets created in accountConfigTab() to extract the current values. * * @param current The @a current container contains the current settings */ virtual MyMoneyKeyValueContainer onlineBankingSettings(const MyMoneyKeyValueContainer& current) = 0; /** * This method is called by the framework when the user wants to map * a KMyMoney account onto an online account. The KMyMoney account is identified * by @a acc and the online provider should store its data in @a onlineBankingSettings * upon success. * * @retval true if account is mapped * @retval false if account is not mapped */ virtual bool mapAccount(const MyMoneyAccount& acc, MyMoneyKeyValueContainer& onlineBankingSettings) = 0; /** * This method is called by the framework when the user wants to update * a KMyMoney account with data from an online account. The KMyMoney account is identified * by @a acc. The online provider should read its data from acc.onlineBankingSettings(). * @a true is returned upon success. The plugin might consider to stack the requests * in case @a moreAccounts is @p true. @a moreAccounts defaults to @p false. * * @retval true if account is updated * @retval false if account is not updated */ virtual bool updateAccount(const MyMoneyAccount& acc, bool moreAccounts = false) = 0; }; /** * This class describes the interface between the KMyMoney * application and it's IMPORTER plugins. All importer plugins * must provide this interface. * * A good tutorial on how to design and develop a plugin * structure for a KDE application (e.g. KMyMoney) can be found at * http://developer.kde.org/documentation/tutorials/developing-a-plugin-structure/index.html * */ class KMM_PLUGIN_EXPORT ImporterPlugin { public: ImporterPlugin(); virtual ~ImporterPlugin(); /** * This method returns the english-language name of the format * this plugin imports, e.g. "OFX" * * @return QString Name of the format */ virtual QString formatName() const = 0; /** * This method returns the filename filter suitable for passing to * KFileDialog::setFilter(), e.g. "*.ofx *.qfx" which describes how * files of this format are likely to be named in the file system * * @return QString Filename filter string */ virtual QString formatFilenameFilter() const = 0; /** * This method returns whether this plugin is able to import * a particular file. * * @param filename Fully-qualified pathname to a file * * @return bool Whether the indicated file is importable by this plugin */ virtual bool isMyFormat(const QString& filename) const = 0; /** * Import a file * * @param filename File to import * * @return bool Whether the import was successful. */ virtual bool import(const QString& filename) = 0; /** * Returns the error result of the last import * * @return QString English-language name of the error encountered in the * last import, or QString() if it was successful. * */ virtual QString lastError() const = 0; }; /** * This class describes the interface between the KMyMoney * application and it's STORAGE plugins. All storage plugins * must provide this interface. * */ class KMM_PLUGIN_EXPORT StoragePlugin { public: StoragePlugin() = default; virtual ~StoragePlugin() = default; /** * @brief Loads file into storage * @param storage Storage manager for the file * @param url URL of the file * @return true if successfully opened */ virtual bool open(MyMoneyStorageMgr *storage, const QUrl &url) = 0; /** * @brief Saves storage into file * @param url URL of the file * @return true if successfully saved */ virtual bool save(const QUrl &url) = 0; + /** + * @brief Returns storage reader + * @return storage reader + */ + virtual IMyMoneyOperationsFormat* reader(){ return nullptr; } + /** * @brief Storage identifier * @return Storage identifier */ virtual QString formatName() const = 0; + + virtual QString fileExtension() const = 0; }; } // end of namespace Q_DECLARE_INTERFACE(KMyMoneyPlugin::OnlinePlugin, "org.kmymoney.plugin.onlineplugin") Q_DECLARE_INTERFACE(KMyMoneyPlugin::ImporterPlugin, "org.kmymoney.plugin.importerplugin") Q_DECLARE_INTERFACE(KMyMoneyPlugin::StoragePlugin, "org.kmymoney.plugin.storageplugin") /** @} */ #endif diff --git a/kmymoney/plugins/sql/sqlstorage.cpp b/kmymoney/plugins/sql/sqlstorage.cpp index 6d998fc9b..9672f8c45 100644 --- a/kmymoney/plugins/sql/sqlstorage.cpp +++ b/kmymoney/plugins/sql/sqlstorage.cpp @@ -1,312 +1,317 @@ /*************************************************************************** sqlstorage.cpp ------------------- copyright : (C) 2018 by Łukasz Wojniłowicz email : lukasz.wojnilowicz@gmail.com ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "sqlstorage.h" #include #include // ---------------------------------------------------------------------------- // QT Includes #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "appinterface.h" #include "viewinterface.h" #include "kselectdatabasedlg.h" #include "kgeneratesqldlg.h" #include "mymoneyfile.h" #include "mymoneystoragesql.h" #include "mymoneyexception.h" //#include "mymoneystoragemgr.h" #include "icons.h" #include "kmymoneysettings.h" using namespace Icons; SQLStorage::SQLStorage(QObject *parent, const QVariantList &args) : KMyMoneyPlugin::Plugin(parent, "sqlstorage"/*must be the same as X-KDE-PluginInfo-Name*/) { Q_UNUSED(args) setComponentName("sqlstorage", i18n("SQL storage")); setXMLFile("sqlstorage.rc"); createActions(); // For information, announce that we have been loaded. qDebug("Plugins: sqlstorage loaded"); } SQLStorage::~SQLStorage() { qDebug("Plugins: sqlstorage unloaded"); } bool SQLStorage::open(MyMoneyStorageMgr *storage, const QUrl &url) { auto reader = std::make_unique(storage, url); QUrl dbURL(url); bool retry = true; while (retry) { switch (reader->open(dbURL, QIODevice::ReadWrite)) { case 0: // opened okay retry = false; break; case 1: // permanent error KMessageBox::detailedError(nullptr, i18n("Cannot open database %1\n", dbURL.toDisplayString()), reader->lastError()); return false; case -1: // retryable error if (KMessageBox::warningYesNo(nullptr, reader->lastError(), PACKAGE) == KMessageBox::No) { return false; } else { QUrlQuery query(dbURL); const QString optionKey = QLatin1String("options"); QString options = query.queryItemValue(optionKey); if(!options.isEmpty()) { options += QLatin1Char(','); } options += QLatin1String("override"); query.removeQueryItem(QLatin1String("mode")); query.removeQueryItem(optionKey); query.addQueryItem(optionKey, options); dbURL.setQuery(query); } } } // single user mode; read some of the data into memory // FIXME - readFile no longer relevant? // tried removing it but then got no indication that loading was complete // also, didn't show home page // reader->setProgressCallback(&KMyMoneyView::progressCallback); if (!reader->readFile()) { KMessageBox::detailedError(nullptr, i18n("An unrecoverable error occurred while reading the database"), reader->lastError().toLatin1(), i18n("Database malfunction")); return false; } // reader->setProgressCallback(0); return true; } bool SQLStorage::save(const QUrl &url) { return saveDatabase(url); } QString SQLStorage::formatName() const { return QStringLiteral("SQL"); } +QString SQLStorage::fileExtension() const +{ + return QString(); +} + void SQLStorage::createActions() { m_openDBaction = actionCollection()->addAction("open_database"); m_openDBaction->setText(i18n("Open database...")); m_openDBaction->setIcon(Icons::get(Icon::SVNUpdate)); connect(m_openDBaction, &QAction::triggered, this, &SQLStorage::slotOpenDatabase); m_saveAsDBaction = actionCollection()->addAction("saveas_database"); m_saveAsDBaction->setText(i18n("Save as database...")); m_saveAsDBaction->setIcon(Icons::get(Icon::FileArchiver)); connect(m_saveAsDBaction, &QAction::triggered, this, &SQLStorage::slotSaveAsDatabase); m_generateDB = actionCollection()->addAction("tools_generate_sql"); m_generateDB->setText(i18n("Generate Database SQL")); connect(m_generateDB, &QAction::triggered, this, &SQLStorage::slotGenerateSql); } void SQLStorage::slotOpenDatabase() { QPointer dialog = new KSelectDatabaseDlg(QIODevice::ReadWrite); if (!dialog->checkDrivers()) { delete dialog; return; } if (dialog->exec() == QDialog::Accepted && dialog != 0) { auto url = dialog->selectedURL(); QUrl newurl = url; if ((newurl.scheme() == QLatin1String("sql"))) { const QString key = QLatin1String("driver"); // take care and convert some old url to their new counterpart QUrlQuery query(newurl); if (query.queryItemValue(key) == QLatin1String("QMYSQL3")) { // fix any old urls query.removeQueryItem(key); query.addQueryItem(key, QLatin1String("QMYSQL")); } if (query.queryItemValue(key) == QLatin1String("QSQLITE3")) { query.removeQueryItem(key); query.addQueryItem(key, QLatin1String("QSQLITE")); } newurl.setQuery(query); if (query.queryItemValue(key) == QLatin1String("QSQLITE")) { newurl.setUserInfo(QString()); newurl.setHost(QString()); } // check if a password is needed. it may be if the URL came from the last/recent file list QPointer dialog = new KSelectDatabaseDlg(QIODevice::ReadWrite, newurl); if (!dialog->checkDrivers()) { delete dialog; return; } // if we need to supply a password, then show the dialog // otherwise it isn't needed if ((query.queryItemValue("secure").toLower() == QLatin1String("yes")) && newurl.password().isEmpty()) { if (dialog->exec() == QDialog::Accepted && dialog != nullptr) { newurl = dialog->selectedURL(); } else { delete dialog; return; } } delete dialog; } appInterface()->slotFileOpenRecent(newurl); } delete dialog; } void SQLStorage::slotSaveAsDatabase() { bool rc = false; QUrl oldUrl; // in event of it being a database, ensure that all data is read into storage for saveas - if (viewInterface()->isDatabase()) + if (appInterface()->isDatabase()) oldUrl = appInterface()->filenameURL().isEmpty() ? appInterface()->lastOpenedURL() : appInterface()->filenameURL(); QPointer dialog = new KSelectDatabaseDlg(QIODevice::WriteOnly); QUrl url = oldUrl; if (!dialog->checkDrivers()) { delete dialog; return; } while (oldUrl == url && dialog->exec() == QDialog::Accepted && dialog != 0) { url = dialog->selectedURL(); // If the protocol is SQL for the old and new, and the hostname and database names match // Let the user know that the current database cannot be saved on top of itself. if (url.scheme() == "sql" && oldUrl.scheme() == "sql" && oldUrl.host() == url.host() && QUrlQuery(oldUrl).queryItemValue("driver") == QUrlQuery(url).queryItemValue("driver") && oldUrl.path().right(oldUrl.path().length() - 1) == url.path().right(url.path().length() - 1)) { KMessageBox::sorry(nullptr, i18n("Cannot save to current database.")); } else { try { rc = saveAsDatabase(url); } catch (const MyMoneyException &e) { KMessageBox::sorry(nullptr, i18n("Cannot save to current database: %1", e.what())); } } } delete dialog; if (rc) { //KRecentFilesAction *p = dynamic_cast(action("file_open_recent")); //if(p) appInterface()->addToRecentFiles(url); appInterface()->writeLastUsedFile(url.toDisplayString(QUrl::PreferLocalFile)); } appInterface()->autosaveTimer()->stop(); appInterface()->updateCaption(); return; } void SQLStorage::slotGenerateSql() { QPointer editor = new KGenerateSqlDlg(nullptr); editor->setObjectName("Generate Database SQL"); editor->exec(); delete editor; } bool SQLStorage::saveAsDatabase(const QUrl &url) { auto writer = new MyMoneyStorageSql(MyMoneyFile::instance()->storage(), url); auto canWrite = false; switch (writer->open(url, QIODevice::WriteOnly)) { case 0: canWrite = true; break; case -1: // dbase already has data, see if he wants to clear it out if (KMessageBox::warningContinueCancel(nullptr, i18n("Database contains data which must be removed before using Save As.\n" "Do you wish to continue?"), "Database not empty") == KMessageBox::Continue) { if (writer->open(url, QIODevice::WriteOnly, true) == 0) canWrite = true; } else { delete writer; return false; } break; } delete writer; if (canWrite) { saveDatabase(url); return true; } else { KMessageBox::detailedError(nullptr, i18n("Cannot open or create database %1.\n" "Retry Save As Database and click Help" " for further info.", url.toDisplayString()), writer->lastError()); return false; } } bool SQLStorage::saveDatabase(const QUrl &url) { auto rc = false; - if (!viewInterface()->fileOpen()) { + if (!appInterface()->fileOpen()) { KMessageBox::error(nullptr, i18n("Tried to access a file when it has not been opened")); return (rc); } auto writer = new MyMoneyStorageSql(MyMoneyFile::instance()->storage(), url); writer->open(url, QIODevice::WriteOnly); // writer->setProgressCallback(&KMyMoneyView::progressCallback); if (!writer->writeFile()) { KMessageBox::detailedError(nullptr, i18n("An unrecoverable error occurred while writing to the database.\n" "It may well be corrupt."), writer->lastError().toLatin1(), i18n("Database malfunction")); rc = false; } else { rc = true; } writer->setProgressCallback(0); delete writer; return rc; } K_PLUGIN_FACTORY_WITH_JSON(SQLStorageFactory, "sqlstorage.json", registerPlugin();) #include "sqlstorage.moc" diff --git a/kmymoney/plugins/sql/sqlstorage.h b/kmymoney/plugins/sql/sqlstorage.h index e9c465e17..bfc656191 100644 --- a/kmymoney/plugins/sql/sqlstorage.h +++ b/kmymoney/plugins/sql/sqlstorage.h @@ -1,70 +1,71 @@ /*************************************************************************** sqlstorage.h ------------------- copyright : (C) 2018 by Łukasz Wojniłowicz email : lukasz.wojnilowicz@gmail.com ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #ifndef SQLSTORAGE_H #define SQLSTORAGE_H // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // QT Includes // Project Includes #include "kmymoneyplugin.h" class MyMoneyStorageMgr; class SQLStorage : public KMyMoneyPlugin::Plugin, public KMyMoneyPlugin::StoragePlugin { Q_OBJECT Q_INTERFACES(KMyMoneyPlugin::StoragePlugin) public: explicit SQLStorage(QObject *parent, const QVariantList &args); ~SQLStorage() override; QAction *m_openDBaction; QAction *m_saveAsDBaction; QAction *m_generateDB; bool open(MyMoneyStorageMgr *storage, const QUrl &url) override; bool save(const QUrl &url) override; QString formatName() const override; + QString fileExtension() const override; protected: void createActions(); private: /** * Saves the data into permanent storage on a new or empty SQL database. * * @param url The pseudo URL of the database * * @retval false save operation failed * @retval true save operation was successful */ bool saveAsDatabase(const QUrl &url); bool saveDatabase(const QUrl &url); private Q_SLOTS: void slotOpenDatabase(); void slotSaveAsDatabase(); void slotGenerateSql(); }; #endif diff --git a/kmymoney/plugins/viewinterface.h b/kmymoney/plugins/viewinterface.h index dbf7fa2f0..9dbd7a34f 100644 --- a/kmymoney/plugins/viewinterface.h +++ b/kmymoney/plugins/viewinterface.h @@ -1,162 +1,140 @@ /*************************************************************************** viewinterface.h ------------------- begin : Wed Jan 5 2005 copyright : (C) 2005 Thomas Baumgart email : ipwizard@users.sourceforge.net ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ // krazy:excludeall=dpointer #ifndef VIEWINTERFACE_H #define VIEWINTERFACE_H // ---------------------------------------------------------------------------- // QT Includes #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes #include "mymoneymoney.h" #include "mymoneytransaction.h" #include "mymoneysplit.h" #include namespace KMyMoneyRegister { class SelectedTransactions; } enum class View; class MyMoneyInstitution; class MyMoneyAccount; class MyMoneySplit; class MyMoneyTransaction; -class IMyMoneyOperationsFormat; class KMyMoneyViewBase; namespace KMyMoneyPlugin { /** * This abstract class represents the ViewInterface to * add new view pages to the JanusWidget of KMyMoney. It * also gives access to the account context menu. */ class KMM_PLUGIN_EXPORT ViewInterface : public QObject { Q_OBJECT public: explicit ViewInterface(QObject* parent, const char* name = 0); virtual ~ViewInterface(); - /** - * 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. - */ - virtual bool readFile(const QUrl &url, IMyMoneyOperationsFormat *pExtReader = nullptr) = 0; - - /** - * 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; /** * Brings up a dialog to change the list(s) settings and saves them into the * class KMyMoneySettings (a singleton). * * @see KListSettingsDlg * Refreshes all views. Used e.g. after settings have been changed or * data has been loaded from external sources (QIF import). **/ virtual void slotRefreshViews() = 0; /** * This method creates a new page in the application. * See KPageWidget::addPage() for details. */ // virtual KMyMoneyViewBase* addPage(const QString& item, const QString& icon) = 0; /** * This method adds a widget to the layout of the view * created with addPage() * * @param view pointer to view widget * @param w widget to be added to @p page */ // virtual void addWidget(KMyMoneyViewBase* view, QWidget* w) = 0; virtual void addView(KMyMoneyViewBase* view, const QString& name, View idView) = 0; virtual void removeView(View idView) = 0; Q_SIGNALS: /** * This signal is emitted when a new account has been selected by * the GUI. If no account is selected or the selection is removed, * @a account is identical to MyMoneyAccount(). This signal is used * by plugins to get information about changes. */ void accountSelected(const MyMoneyAccount& acc); /** * 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 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 an account has been successfully reconciled * and all transactions are updated in the engine. It can be used by plugins * to create reconciliation reports. * * @param account the account data * @param date the reconciliation date as provided through the dialog * @param startingBalance the starting balance as provided through the dialog * @param endingBalance the ending balance as provided through the dialog * @param transactionList reference to QList of QPair containing all * transaction/split pairs processed by the reconciliation. */ void accountReconciled(const MyMoneyAccount& account, const QDate& date, const MyMoneyMoney& startingBalance, const MyMoneyMoney& endingBalance, const QList >& transactionList); void viewStateChanged(bool); - void kmmFilePlugin(unsigned int); }; } // namespace #endif diff --git a/kmymoney/views/kmymoneyview.cpp b/kmymoney/views/kmymoneyview.cpp index 4e13a8c96..859ec01ba 100644 --- a/kmymoney/views/kmymoneyview.cpp +++ b/kmymoney/views/kmymoneyview.cpp @@ -1,2253 +1,957 @@ /*************************************************************************** kmymoneyview.cpp ------------------- copyright : (C) 2000-2001 by Michael Edwardes 2004 by Thomas Baumgart 2017 by Łukasz Wojniłowicz ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include #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 #ifdef KF5Activities_FOUND #include #endif // ---------------------------------------------------------------------------- // 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 "kreportsview.h" #include "kbudgetview.h" #include "konlinejoboutbox.h" #include "kmymoney.h" #include "models.h" #include "accountsmodel.h" #include "equitiesmodel.h" #include "securitiesmodel.h" #include "icons.h" #include "amountedit.h" #include "kmymoneyaccounttreeview.h" #include "accountsviewproxymodel.h" #include "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" using namespace Icons; using namespace eMyMoney; -static constexpr KCompressionDevice::CompressionType const& COMPRESSION_TYPE = KCompressionDevice::GZip; -static constexpr char recoveryKeyId[] = "0xD2B08440"; - typedef void(KMyMoneyView::*KMyMoneyViewFunc)(); KMyMoneyView::KMyMoneyView(KMyMoneyApp *kmymoney) : KPageWidget(nullptr), m_header(0), m_inConstructor(true), - m_fileOpen(false), - m_fmode(QFileDevice::ReadUser | QFileDevice::WriteUser), m_lastViewSelected(0), m_storagePlugins(nullptr) #ifdef KF5Activities_FOUND , m_activityResourceInstance(0) #endif { // this is a workaround for the bug in KPageWidget that causes the header to be shown // for a short while during page switch which causes a kind of bouncing of the page's // content and if the page's content is at it's minimum size then during a page switch // the main window's size is also increased to fit the header that is shown for a sort // period - reading the code in kpagewidget.cpp we know that the header should be at (1,1) // in a grid layout so if we find it there remove it for good to avoid the described issues QGridLayout* gridLayout = qobject_cast(layout()); if (gridLayout) { QLayoutItem* headerItem = gridLayout->itemAtPosition(1, 1); // make sure that we remove only the header - we avoid surprises if the header is not at (1,1) in the layout if (headerItem && qobject_cast(headerItem->widget()) != NULL) { gridLayout->removeItem(headerItem); // after we remove the KPageWidget standard header replace it with our own title label m_header = new KMyMoneyTitleLabel(this); m_header->setObjectName("titleLabel"); m_header->setMinimumSize(QSize(100, 30)); m_header->setRightImageFile("pics/titlelabel_background.png"); m_header->setVisible(KMyMoneySettings::showTitleBar()); gridLayout->addWidget(m_header, 1, 1); } } - newStorage(); +// newStorage(); m_model = new KPageWidgetModel(this); // cannot be parentless, otherwise segfaults at exit connect(kmymoney, &KMyMoneyApp::fileLoaded, this, &KMyMoneyView::slotRefreshViews); // Page 0 m_homeView = new KHomeView; viewFrames[View::Home] = m_model->addPage(m_homeView, i18n("Home")); viewFrames[View::Home]->setIcon(Icons::get(Icon::ViewHome)); connect(m_homeView, &KMyMoneyViewBase::aboutToShow, this, &KMyMoneyView::connectView); connect(m_homeView, &KMyMoneyViewBase::aboutToShow, this, &KMyMoneyView::resetViewSelection); // Page 1 m_institutionsView = new KInstitutionsView; viewFrames[View::Institutions] = m_model->addPage(m_institutionsView, i18n("Institutions")); viewFrames[View::Institutions]->setIcon(Icons::get(Icon::ViewInstitutions)); connect(m_institutionsView, &KMyMoneyViewBase::aboutToShow, this, &KMyMoneyView::connectView); connect(m_institutionsView, &KMyMoneyViewBase::aboutToShow, this, &KMyMoneyView::resetViewSelection); // Page 2 m_accountsView = new KAccountsView; viewFrames[View::Accounts] = m_model->addPage(m_accountsView, i18n("Accounts")); viewFrames[View::Accounts]->setIcon(Icons::get(Icon::ViewAccounts)); connect(m_accountsView, &KMyMoneyViewBase::aboutToShow, this, &KMyMoneyView::connectView); connect(m_accountsView, &KMyMoneyViewBase::aboutToShow, this, &KMyMoneyView::resetViewSelection); // Page 3 m_scheduledView = new KScheduledView; //this is to solve the way long strings are handled differently among versions of KPageWidget viewFrames[View::Schedules] = m_model->addPage(m_scheduledView, i18nc("use \u2028 as line break", "Scheduled\u2028transactions")); viewFrames[View::Schedules]->setIcon(Icons::get(Icon::ViewSchedules)); connect(m_scheduledView, &KMyMoneyViewBase::aboutToShow, this, &KMyMoneyView::connectView); connect(m_scheduledView, &KMyMoneyViewBase::aboutToShow, this, &KMyMoneyView::resetViewSelection); // Page 4 m_categoriesView = new KCategoriesView; viewFrames[View::Categories] = m_model->addPage(m_categoriesView, i18n("Categories")); viewFrames[View::Categories]->setIcon(Icons::get(Icon::ViewCategories)); connect(m_categoriesView, &KMyMoneyViewBase::aboutToShow, this, &KMyMoneyView::connectView); connect(m_categoriesView, &KMyMoneyViewBase::aboutToShow, this, &KMyMoneyView::resetViewSelection); // Page 5 m_tagsView = new KTagsView; viewFrames[View::Tags] = m_model->addPage(m_tagsView, i18n("Tags")); viewFrames[View::Tags]->setIcon(Icons::get(Icon::ViewTags)); connect(m_tagsView, &KMyMoneyViewBase::aboutToShow, this, &KMyMoneyView::connectView); connect(m_tagsView, &KMyMoneyViewBase::aboutToShow, this, &KMyMoneyView::resetViewSelection); // Page 6 m_payeesView = new KPayeesView; viewFrames[View::Payees] = m_model->addPage(m_payeesView, i18n("Payees")); viewFrames[View::Payees]->setIcon(Icons::get(Icon::ViewPayees)); connect(m_payeesView, &KMyMoneyViewBase::aboutToShow, this, &KMyMoneyView::connectView); connect(m_payeesView, &KMyMoneyViewBase::aboutToShow, this, &KMyMoneyView::resetViewSelection); // Page 7 m_ledgerView = new KGlobalLedgerView; viewFrames[View::Ledgers] = m_model->addPage(m_ledgerView, i18n("Ledgers")); viewFrames[View::Ledgers]->setIcon(Icons::get(Icon::ViewLedgers)); connect(m_ledgerView, &KMyMoneyViewBase::aboutToShow, this, &KMyMoneyView::connectView); connect(m_ledgerView, &KMyMoneyViewBase::aboutToShow, this, &KMyMoneyView::resetViewSelection); // Page 8 m_investmentView = new KInvestmentView; viewFrames[View::Investments] = m_model->addPage(m_investmentView, i18n("Investments")); viewFrames[View::Investments]->setIcon(Icons::get(Icon::ViewInvestment)); connect(m_investmentView, &KMyMoneyViewBase::aboutToShow, this, &KMyMoneyView::connectView); connect(m_investmentView, &KMyMoneyViewBase::aboutToShow, this, &KMyMoneyView::resetViewSelection); // Page 9 m_reportsView = new KReportsView; viewFrames[View::Reports] = m_model->addPage(m_reportsView, i18n("Reports")); viewFrames[View::Reports]->setIcon(Icons::get(Icon::ViewReports)); connect(m_reportsView, &KMyMoneyViewBase::aboutToShow, this, &KMyMoneyView::connectView); connect(m_reportsView, &KMyMoneyViewBase::aboutToShow, this, &KMyMoneyView::resetViewSelection); // Page 10 m_budgetView = new KBudgetView; viewFrames[View::Budget] = m_model->addPage(m_budgetView, i18n("Budgets")); viewFrames[View::Budget]->setIcon(Icons::get(Icon::ViewBudgets)); connect(m_budgetView, &KMyMoneyViewBase::aboutToShow, this, &KMyMoneyView::connectView); connect(m_budgetView, &KMyMoneyViewBase::aboutToShow, this, &KMyMoneyView::resetViewSelection); // Page 11 // KForecastView // Page 12 m_onlineJobOutboxView = new KOnlineJobOutbox; addView(m_onlineJobOutboxView, i18n("Outbox"), View::OnlineJobOutbox); connect(m_onlineJobOutboxView, &KMyMoneyViewBase::aboutToShow, this, &KMyMoneyView::connectView); connect(m_onlineJobOutboxView, &KMyMoneyViewBase::aboutToShow, this, &KMyMoneyView::resetViewSelection); connect(m_reportsView, &KReportsView::switchViewRequested, this, &KMyMoneyView::slotSwitchView); connect(m_ledgerView, &KGlobalLedgerView::switchViewRequested, this, &KMyMoneyView::slotSwitchView); connect(m_homeView, &KHomeView::ledgerSelected, m_ledgerView, &KGlobalLedgerView::slotLedgerSelected); #ifdef ENABLE_UNFINISHEDFEATURES - SimpleLedgerView* view = new SimpleLedgerView(kmymoney, this); - KPageWidgetItem* frame = m_model->addPage(view, i18n("New ledger")); + m_simpleLedgerView = new SimpleLedgerView(kmymoney, this); + KPageWidgetItem* frame = m_model->addPage(m_simpleLedgerView, i18n("New ledger")); frame->setIcon(Icons::get(Icon::DocumentProperties)); #endif //set the model setModel(m_model); setCurrentPage(viewFrames[View::Home]); connect(this, SIGNAL(currentPageChanged(QModelIndex,QModelIndex)), this, SLOT(slotCurrentPageChanged(QModelIndex,QModelIndex))); updateViewType(); m_inConstructor = false; // add fast switching of main views through Ctrl + NUM_X struct pageInfo { View view; KMyMoneyViewFunc callback; QString text; QKeySequence shortcut = QKeySequence(); }; const QVector pageInfos { {View::Home, &KMyMoneyView::slotShowHomePage, i18n("Show home page"), Qt::CTRL + Qt::Key_1}, {View::Institutions, &KMyMoneyView::slotShowInstitutionsPage, i18n("Show institutions page"), Qt::CTRL + Qt::Key_2}, {View::Accounts, &KMyMoneyView::slotShowAccountsPage, i18n("Show accounts page"), Qt::CTRL + Qt::Key_3}, {View::Schedules, &KMyMoneyView::slotShowSchedulesPage, i18n("Show scheduled transactions page"), Qt::CTRL + Qt::Key_4}, {View::Categories, &KMyMoneyView::slotShowCategoriesPage, i18n("Show categories page"), Qt::CTRL + Qt::Key_5}, {View::Tags, &KMyMoneyView::slotShowTagsPage, i18n("Show tags page"), }, {View::Payees, &KMyMoneyView::slotShowPayeesPage, i18n("Show payees page"), Qt::CTRL + Qt::Key_6}, {View::Ledgers, &KMyMoneyView::slotShowLedgersPage, i18n("Show ledgers page"), Qt::CTRL + Qt::Key_7}, {View::Investments, &KMyMoneyView::slotShowInvestmentsPage, i18n("Show investments page"), Qt::CTRL + Qt::Key_8}, {View::Reports, &KMyMoneyView::slotShowReportsPage, i18n("Show reports page"), Qt::CTRL + Qt::Key_9}, {View::Budget, &KMyMoneyView::slotShowBudgetPage, i18n("Show budget page"), }, {View::Forecast, &KMyMoneyView::slotShowForecastPage, i18n("Show forecast page"), }, {View::OnlineJobOutbox, &KMyMoneyView::slotShowOutboxPage, i18n("Show outbox page") } }; QHash lutActions; auto aC = kmymoney->actionCollection(); auto pageCount = 0; foreach (const pageInfo info, pageInfos) { auto a = new QAction(this); // KActionCollection::addAction by name sets object name anyways, // so, as better alternative, set it here right from the start a->setObjectName(QLatin1String("ShowPage") + QString::number(pageCount++)); a->setText(info.text); connect(a, &QAction::triggered, this, info.callback); lutActions.insert(info.view, a); // store QAction's pointer for later processing if (!info.shortcut.isEmpty()) aC->setDefaultShortcut(a, info.shortcut); } aC->addActions(lutActions.values()); // Initialize kactivities resource instance #ifdef KF5Activities_FOUND m_activityResourceInstance = new KActivities::ResourceInstance(window()->winId(), this); connect(kmymoney, SIGNAL(fileLoaded(QUrl)), m_activityResourceInstance, SLOT(setUri(QUrl))); #endif } KMyMoneyView::~KMyMoneyView() { KMyMoneySettings::setLastViewSelected(m_lastViewSelected); #ifdef KF5Activities_FOUND delete m_activityResourceInstance; #endif - removeStorage(); +// removeStorage(); +} + +void KMyMoneyView::slotFileOpened() +{ + #ifdef ENABLE_UNFINISHEDFEATURES + m_simpleLedgerView->openFavoriteLedgers(); + #endif + switchToDefaultView(); +} + +void KMyMoneyView::slotFileClosed() +{ + if (m_reportsView) + m_reportsView->slotCloseAll(); + #ifdef ENABLE_UNFINISHEDFEATURES + m_simpleLedgerView->closeLedgers(); + #endif + slotShowHomePage(); } void KMyMoneyView::slotShowHomePage() { showPage(viewFrames[View::Home]); } void KMyMoneyView::slotShowInstitutionsPage() { showPage(viewFrames[View::Institutions]); m_institutionsView->setDefaultFocus(); } void KMyMoneyView::slotShowAccountsPage() { showPage(viewFrames[View::Accounts]); m_accountsView->setDefaultFocus(); } void KMyMoneyView::slotShowSchedulesPage() { showPage(viewFrames[View::Schedules]); m_scheduledView->setDefaultFocus(); } void KMyMoneyView::slotShowCategoriesPage() { showPage(viewFrames[View::Categories]); m_categoriesView->setDefaultFocus(); } void KMyMoneyView::slotShowTagsPage() { showPage(viewFrames[View::Tags]); m_tagsView->setDefaultFocus(); } void KMyMoneyView::slotShowPayeesPage() { showPage(viewFrames[View::Payees]); m_payeesView->setDefaultFocus(); } void KMyMoneyView::slotShowLedgersPage() { showPage(viewFrames[View::Ledgers]); m_ledgerView->setDefaultFocus(); } void KMyMoneyView::slotShowInvestmentsPage() { showPage(viewFrames[View::Investments]); m_investmentView->setDefaultFocus(); } void KMyMoneyView::slotShowReportsPage() { showPage(viewFrames[View::Reports]); m_reportsView->setDefaultFocus(); } void KMyMoneyView::slotShowBudgetPage() { showPage(viewFrames[View::Budget]); m_budgetView->setDefaultFocus(); } void KMyMoneyView::slotShowForecastPage() { if (viewFrames.contains(View::Forecast)) { showPage(viewFrames[View::Forecast]); viewBases[View::Forecast]->setDefaultFocus(); } } void KMyMoneyView::slotShowOutboxPage() { if (viewFrames[View::OnlineJobOutbox]) { showPage(viewFrames[View::OnlineJobOutbox]); viewBases[View::OnlineJobOutbox]->setDefaultFocus(); } } void KMyMoneyView::showTitleBar(bool show) { if (m_header) m_header->setVisible(show); } void KMyMoneyView::updateViewType() { // set the face type KPageView::FaceType faceType = KPageView::List; switch (KMyMoneySettings::viewType()) { case 0: faceType = KPageView::List; break; case 1: faceType = KPageView::Tree; break; case 2: faceType = KPageView::Tabbed; break; } if (faceType != KMyMoneyView::faceType()) { setFaceType(faceType); if (faceType == KPageView::Tree) { QList views = findChildren(); foreach (QTreeView * view, views) { if (view && (view->parent() == this)) { view->setRootIsDecorated(false); break; } } } } } void KMyMoneyView::slotAccountTreeViewChanged(const eAccountsModel::Column column, const bool show) { QVector proxyModels {m_institutionsView->getProxyModel(), m_accountsView->getProxyModel(), m_categoriesView->getProxyModel(), m_budgetView->getProxyModel()}; 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) { m_accountsView->setOnlinePlugins(plugins); if (m_onlineJobOutboxView) { m_onlineJobOutboxView->setOnlinePlugins(plugins); } if (plugins.isEmpty()) { removeView(View::OnlineJobOutbox); m_onlineJobOutboxView = nullptr; } } void KMyMoneyView::setStoragePlugins(QMap& plugins) { m_storagePlugins = &plugins; } eDialogs::ScheduleResultCode KMyMoneyView::enterSchedule(MyMoneySchedule& schedule, bool autoEnter, bool extendedKeys) { return m_scheduledView->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); viewBases[idView] = view; isViewInserted = true; break; } } if (!isViewInserted) viewFrames[idView] = m_model->addPage(view, name); auto icon = Icon::ViewForecast; switch (idView) { 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) { m_model->removePage(viewFrames[idView]); viewFrames.remove(idView); viewBases.remove(idView); } bool KMyMoneyView::showPageHeader() const { return false; } void KMyMoneyView::slotSwitchView(View view) { showPage(viewFrames[view]); } void KMyMoneyView::showPage(KPageWidgetItem* pageItem) { // reset all selected items before showing the selected view // but not while we're in our own constructor if (!m_inConstructor && pageItem != currentPage()) { kmymoney->slotResetSelections(); } // pretend we're in the constructor to avoid calling the // above resets. For some reason which I don't know the details // of, KJanusWidget::showPage() calls itself recursively. This // screws up the action handling, as items could have been selected // in the meantime. We prevent this by setting the m_inConstructor // to true and reset it to the previos value when we leave this method. bool prevConstructor = m_inConstructor; m_inConstructor = true; setCurrentPage(pageItem); m_inConstructor = prevConstructor; if (!m_inConstructor) { // fixup some actions that are dependant on the view // this does not work during construction kmymoney->slotUpdateActions(); } } bool KMyMoneyView::canPrint() { bool rc = ( viewFrames[View::Reports] == currentPage() || viewFrames[View::Home] == currentPage() ); return rc; } -void KMyMoneyView::newStorage() -{ - removeStorage(); - auto file = MyMoneyFile::instance(); - file->attachStorage(new MyMoneyStorageMgr); -} - -void KMyMoneyView::removeStorage() -{ - auto file = MyMoneyFile::instance(); - auto p = file->storage(); - if (p) { - file->detachStorage(p); - delete p; - } -} - -void KMyMoneyView::enableViewsIfFileOpen() +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() != m_fileOpen) - viewFrames[View(i)]->setEnabled(m_fileOpen); + if (viewFrames[View(i)]->isEnabled() != fileOpen) + viewFrames[View(i)]->setEnabled(fileOpen); - emit viewStateChanged(m_fileOpen); + emit viewStateChanged(fileOpen); } -void KMyMoneyView::slotPayeeSelected(const QString& payee, const QString& account, const QString& transaction) +void KMyMoneyView::switchToDefaultView() { - showPage(viewFrames[View::Payees]); - m_payeesView->slotSelectPayeeAndTransaction(payee, account, transaction); -} - -void KMyMoneyView::slotTagSelected(const QString& tag, const QString& account, const QString& transaction) -{ - showPage(viewFrames[View::Tags]); - m_tagsView->slotSelectTagAndTransaction(tag, account, transaction); -} - -bool KMyMoneyView::fileOpen() -{ - return m_fileOpen; -} - -void KMyMoneyView::closeFile() -{ - if (m_reportsView) - m_reportsView->slotCloseAll(); - - // disconnect the signals - disconnect(MyMoneyFile::instance(), &MyMoneyFile::objectAdded, - Models::instance()->accountsModel(), &AccountsModel::slotObjectAdded); - disconnect(MyMoneyFile::instance(), &MyMoneyFile::objectModified, - Models::instance()->accountsModel(), &AccountsModel::slotObjectModified); - disconnect(MyMoneyFile::instance(), &MyMoneyFile::objectRemoved, - Models::instance()->accountsModel(), &AccountsModel::slotObjectRemoved); - disconnect(MyMoneyFile::instance(), &MyMoneyFile::balanceChanged, - Models::instance()->accountsModel(), &AccountsModel::slotBalanceOrValueChanged); - disconnect(MyMoneyFile::instance(), &MyMoneyFile::valueChanged, - Models::instance()->accountsModel(), &AccountsModel::slotBalanceOrValueChanged); - - disconnect(MyMoneyFile::instance(), &MyMoneyFile::objectAdded, - Models::instance()->institutionsModel(), &InstitutionsModel::slotObjectAdded); - disconnect(MyMoneyFile::instance(), &MyMoneyFile::objectModified, - Models::instance()->institutionsModel(), &InstitutionsModel::slotObjectModified); - disconnect(MyMoneyFile::instance(), &MyMoneyFile::objectRemoved, - Models::instance()->institutionsModel(), &InstitutionsModel::slotObjectRemoved); - disconnect(MyMoneyFile::instance(), &MyMoneyFile::balanceChanged, - Models::instance()->institutionsModel(), &AccountsModel::slotBalanceOrValueChanged); - disconnect(MyMoneyFile::instance(), &MyMoneyFile::valueChanged, - Models::instance()->institutionsModel(), &AccountsModel::slotBalanceOrValueChanged); - - disconnect(MyMoneyFile::instance(), &MyMoneyFile::objectAdded, - Models::instance()->equitiesModel(), &EquitiesModel::slotObjectAdded); - disconnect(MyMoneyFile::instance(), &MyMoneyFile::objectModified, - Models::instance()->equitiesModel(), &EquitiesModel::slotObjectModified); - disconnect(MyMoneyFile::instance(), &MyMoneyFile::objectRemoved, - Models::instance()->equitiesModel(), &EquitiesModel::slotObjectRemoved); - disconnect(MyMoneyFile::instance(), &MyMoneyFile::balanceChanged, - Models::instance()->equitiesModel(), &EquitiesModel::slotBalanceOrValueChanged); - disconnect(MyMoneyFile::instance(), &MyMoneyFile::valueChanged, - Models::instance()->equitiesModel(), &EquitiesModel::slotBalanceOrValueChanged); - - disconnect(MyMoneyFile::instance(), &MyMoneyFile::objectAdded, - Models::instance()->securitiesModel(), &SecuritiesModel::slotObjectAdded); - disconnect(MyMoneyFile::instance(), &MyMoneyFile::objectModified, - Models::instance()->securitiesModel(), &SecuritiesModel::slotObjectModified); - disconnect(MyMoneyFile::instance(), &MyMoneyFile::objectRemoved, - Models::instance()->securitiesModel(), &SecuritiesModel::slotObjectRemoved); - - disconnect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, m_homeView, &KHomeView::refresh); - - // notify the models that the file is going to be closed (we should have something like dataChanged that reaches the models first) - Models::instance()->fileClosed(); - - emit kmmFilePlugin(preClose); - if (isDatabase()) - MyMoneyFile::instance()->storage()->close(); // to log off a database user - newStorage(); - - slotShowHomePage(); - - emit kmmFilePlugin(postClose); - m_fileOpen = false; - - emit fileClosed(); -} - -void KMyMoneyView::ungetString(QIODevice *qfile, char *buf, int len) -{ - buf = &buf[len-1]; - while (len--) { - qfile->ungetChar(*buf--); - } -} - -bool KMyMoneyView::readFile(const QUrl &url, IMyMoneyOperationsFormat* pExtReader) -{ - QString filename; - bool downloadedFile = false; - m_fileOpen = false; - bool isEncrypted = false; - - IMyMoneyOperationsFormat* pReader = 0; - - if (!url.isValid()) { - qDebug("Invalid URL '%s'", qPrintable(url.url())); - return false; - } - - // disconnect the current storga manager from the engine - MyMoneyFile::instance()->detachStorage(); - - if (url.scheme() == QLatin1String("sql")) { // handle reading of database - m_fileType = KmmDb; - // get rid of the mode parameter which is now redundant - QUrl newUrl(url); - QUrlQuery query(url); - query.removeQueryItem("mode"); - newUrl.setQuery(query); - auto rc = openDatabase(newUrl); // on error, any message will have been displayed - if (!rc) - MyMoneyFile::instance()->attachStorage(new MyMoneyStorageMgr); - return rc; - } - - auto storage = new MyMoneyStorageMgr; - - if (url.isLocalFile()) { - filename = url.toLocalFile(); - } else { - downloadedFile = true; - KIO::StoredTransferJob *transferjob = KIO::storedGet (url); - KJobWidgets::setWindow(transferjob, this); - if (! transferjob->exec()) { - KMessageBox::detailedError(this, - i18n("Error while loading file '%1'.", url.url()), - transferjob->errorString(), - i18n("File access error")); - return false; - } - QTemporaryFile file; - file.setAutoRemove(false); - file.open(); - file.write(transferjob->data()); - filename = file.fileName(); - file.close(); - } - - // let's glimps into the file to figure out, if it's one - // of the old (uncompressed) or new (compressed) files. - QFile file(filename); - QFileInfo info(file); - if (!info.isFile()) { - QString msg = i18n("

%1 is not a KMyMoney file.

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

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

%1

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

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

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

%1.

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

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

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

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

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

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

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

" - , acc.name()), - i18n("Account problem")); - - throw MYMONEYEXCEPTION("Fix LoanAccount0 not supported anymore"); - } -} - void KMyMoneyView::createSchedule(MyMoneySchedule newSchedule, MyMoneyAccount& newAccount) { // Add the schedule only if one exists // // Remember to modify the first split to reference the newly created account if (!newSchedule.name().isEmpty()) { MyMoneyFileTransaction ft; try { // We assume at least 2 splits in the transaction MyMoneyTransaction t = newSchedule.transaction(); if (t.splitCount() < 2) { throw MYMONEYEXCEPTION("Transaction for schedule has less than 2 splits!"); } // now search the split that does not have an account reference // and set it up to be the one of the account we just added // to the account pool. Note: the schedule code used to leave // this always the first split, but the loan code leaves it as // the second one. So I thought, searching is a good alternative .... foreach (const auto split, t.splits()) { if (split.accountId().isEmpty()) { MyMoneySplit s = split; s.setAccountId(newAccount.id()); t.modifySplit(s); break; } } newSchedule.setTransaction(t); MyMoneyFile::instance()->addSchedule(newSchedule); // in case of a loan account, we keep a reference to this // schedule in the account if (newAccount.isLoan()) { newAccount.setValue("schedule", newSchedule.id()); MyMoneyFile::instance()->modifyAccount(newAccount); } ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::information(this, i18n("Unable to add schedule: %1", e.what())); } } } -void KMyMoneyView::fixTransactions_0() -{ - auto file = MyMoneyFile::instance(); - - QList scheduleList = file->scheduleList(); - MyMoneyTransactionFilter filter; - filter.setReportAllSplits(false); - QList transactionList; - file->transactionList(transactionList, filter); - - QList::Iterator it_x; - QStringList interestAccounts; - - KMSTATUS(i18n("Fix transactions")); - kmymoney->slotStatusProgressBar(0, scheduleList.count() + transactionList.count()); - - int cnt = 0; - // scan the schedules to find interest accounts - for (it_x = scheduleList.begin(); it_x != scheduleList.end(); ++it_x) { - MyMoneyTransaction t = (*it_x).transaction(); - QList::ConstIterator it_s; - QStringList accounts; - bool hasDuplicateAccounts = false; - - foreach (const auto split, t.splits()) { - if (accounts.contains(split.accountId())) { - hasDuplicateAccounts = true; - qDebug() << Q_FUNC_INFO << " " << t.id() << " has multiple splits with account " << split.accountId(); - } else { - accounts << split.accountId(); - } - - if (split.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::Interest)) { - if (interestAccounts.contains(split.accountId()) == 0) { - interestAccounts << split.accountId(); - } - } - } - if (hasDuplicateAccounts) { - fixDuplicateAccounts_0(t); - } - ++cnt; - if (!(cnt % 10)) - kmymoney->slotStatusProgressBar(cnt); - } - - // scan the transactions and modify loan transactions - for (auto& transaction : transactionList) { - 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() == Account::Type::Asset - || acc.accountGroup() == Account::Type::Liability) { - val = split.value(); - accountCount++; - if (acc.accountType() == Account::Type::Loan - || acc.accountType() == Account::Type::AssetLoan) - isLoan = true; - } else - break; - } - if (accountCount == 2) { - if (isLoan) - defaultAction = MyMoneySplit::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() == Account::Type::Asset - || acc.accountGroup() == 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)) - kmymoney->slotStatusProgressBar(cnt); - } - - kmymoney->slotStatusProgressBar(-1, -1); -} - -void KMyMoneyView::fixDuplicateAccounts_0(MyMoneyTransaction& t) -{ - qDebug("Duplicate account in transaction %s", qPrintable(t.id())); -} - void KMyMoneyView::slotPrintView() { if (viewFrames[View::Reports] == currentPage()) m_reportsView->slotPrintView(); else if (viewFrames[View::Home] == currentPage()) m_homeView->slotPrintView(); } void KMyMoneyView::resetViewSelection(const View) { emit aboutToChangeView(); } void KMyMoneyView::connectView(const View view) { KMyMoneyAccountTreeView *treeView; switch (view) { case View::Home: disconnect(m_homeView, &KHomeView::aboutToShow, this, &KMyMoneyView::connectView); connect(m_homeView, &KHomeView::objectSelected, this, &KMyMoneyView::slotObjectSelected); connect(m_homeView, &KHomeView::openObjectRequested, this, &KMyMoneyView::slotOpenObjectRequested); + // views can wait since they are going to be refresed in slotRefreshViews + connect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, m_homeView, &KHomeView::refresh); break; case View::Accounts: disconnect(m_accountsView, &KAccountsView::aboutToShow, this, &KMyMoneyView::connectView); treeView = m_accountsView->getTreeView(); connect(treeView, &KMyMoneyAccountTreeView::openObjectRequested, this, &KMyMoneyView::slotOpenObjectRequested); connect(treeView, &KMyMoneyAccountTreeView::contextMenuRequested, this, &KMyMoneyView::slotContextMenuRequested); connect(treeView, &KMyMoneyAccountTreeView::columnToggled, this, &KMyMoneyView::slotAccountTreeViewChanged); connect(m_accountsView, &KAccountsView::objectSelected, this, &KMyMoneyView::slotObjectSelected); connect(Models::instance()->accountsModel(), &AccountsModel::netWorthChanged, m_accountsView, &KAccountsView::slotNetWorthChanged); break; case View::Schedules: disconnect(m_scheduledView, &KScheduledView::aboutToShow, this, &KMyMoneyView::connectView); connect(m_scheduledView, &KScheduledView::openObjectRequested, this, &KMyMoneyView::slotOpenObjectRequested); connect(m_scheduledView, &KScheduledView::objectSelected, this, &KMyMoneyView::slotObjectSelected); connect(m_scheduledView, &KScheduledView::contextMenuRequested, this, &KMyMoneyView::slotContextMenuRequested); break; case View::Institutions: disconnect(m_institutionsView, &KInstitutionsView::aboutToShow, this, &KMyMoneyView::connectView); treeView = m_institutionsView->getTreeView(); connect(treeView, &KMyMoneyAccountTreeView::openObjectRequested, this, &KMyMoneyView::slotOpenObjectRequested); connect(treeView, &KMyMoneyAccountTreeView::contextMenuRequested, this, &KMyMoneyView::slotContextMenuRequested); connect(treeView, &KMyMoneyAccountTreeView::columnToggled, this, &KMyMoneyView::slotAccountTreeViewChanged); connect(m_institutionsView, &KInstitutionsView::objectSelected, this, &KMyMoneyView::slotObjectSelected); connect(Models::instance()->institutionsModel(), &AccountsModel::netWorthChanged, m_institutionsView, &KInstitutionsView::slotNetWorthChanged); break; case View::Categories: disconnect(m_categoriesView, &KCategoriesView::aboutToShow, this, &KMyMoneyView::connectView); treeView = m_categoriesView->getTreeView(); connect(treeView, &KMyMoneyAccountTreeView::openObjectRequested, this, &KMyMoneyView::slotOpenObjectRequested); connect(treeView, &KMyMoneyAccountTreeView::objectSelected, this, &KMyMoneyView::slotObjectSelected); connect(treeView, &KMyMoneyAccountTreeView::contextMenuRequested, this, &KMyMoneyView::slotContextMenuRequested); connect(treeView, &KMyMoneyAccountTreeView::columnToggled, this, &KMyMoneyView::slotAccountTreeViewChanged); connect(m_categoriesView, &KCategoriesView::objectSelected, this, &KMyMoneyView::slotObjectSelected); connect(Models::instance()->institutionsModel(), &AccountsModel::profitChanged, m_categoriesView, &KCategoriesView::slotProfitChanged); break; case View::Tags: disconnect(m_tagsView, &KTagsView::aboutToShow, this, &KMyMoneyView::connectView); connect(m_tagsView, &KTagsView::transactionSelected, m_ledgerView, &KGlobalLedgerView::slotLedgerSelected); break; case View::Payees: disconnect(m_payeesView, &KTagsView::aboutToShow, this, &KMyMoneyView::connectView); connect(m_payeesView, &KPayeesView::transactionSelected, m_ledgerView, &KGlobalLedgerView::slotLedgerSelected); break; case View::Ledgers: disconnect(m_ledgerView, &KGlobalLedgerView::aboutToShow, this, &KMyMoneyView::connectView); connect(m_ledgerView, &KGlobalLedgerView::openObjectRequested, this, &KMyMoneyView::slotOpenObjectRequested); connect(m_ledgerView, &KGlobalLedgerView::openPayeeRequested, this, &KMyMoneyView::slotPayeeSelected); connect(m_ledgerView, &KGlobalLedgerView::objectSelected, this, &KMyMoneyView::slotObjectSelected); connect(m_ledgerView, &KGlobalLedgerView::transactionsSelected, this, &KMyMoneyView::slotTransactionsSelected); connect(m_ledgerView, &KGlobalLedgerView::contextMenuRequested, this, &KMyMoneyView::slotContextMenuRequested); connect(m_ledgerView, &KGlobalLedgerView::transactionsContextMenuRequested, this, &KMyMoneyView::slotTransactionsMenuRequested); connect(m_ledgerView, &KGlobalLedgerView::statusProgress, this, &KMyMoneyView::statusProgress); connect(m_ledgerView, &KGlobalLedgerView::statusMsg, this, &KMyMoneyView::statusMsg); connect(m_ledgerView, &KGlobalLedgerView::accountReconciled, this, &KMyMoneyView::accountReconciled); connect(m_ledgerView, &KGlobalLedgerView::enterOverdueSchedulesRequested, m_scheduledView, &KScheduledView::slotEnterOverdueSchedules); connect(m_scheduledView, &KScheduledView::enterOverdueSchedulesFinished, m_ledgerView, &KGlobalLedgerView::slotContinueReconciliation); break; case View::Budget: disconnect(m_budgetView, &KBudgetView::aboutToShow, this, &KMyMoneyView::connectView); treeView = m_budgetView->getTreeView(); connect(treeView, &KMyMoneyAccountTreeView::openObjectRequested, this, &KMyMoneyView::slotOpenObjectRequested); connect(treeView, &KMyMoneyAccountTreeView::contextMenuRequested, this, &KMyMoneyView::slotContextMenuRequested); connect(treeView, &KMyMoneyAccountTreeView::columnToggled, this, &KMyMoneyView::slotAccountTreeViewChanged); connect(m_budgetView, &KBudgetView::objectSelected, this, &KMyMoneyView::slotObjectSelected); break; case View::Investments: disconnect(m_investmentView, &KInvestmentView::aboutToShow, this, &KMyMoneyView::connectView); connect(m_investmentView, &KInvestmentView::accountSelected, kmymoney, &KMyMoneyApp::slotSelectAccount); connect(m_investmentView, &KInvestmentView::objectSelected, this, &KMyMoneyView::slotObjectSelected); connect(m_investmentView, &KInvestmentView::contextMenuRequested, this, &KMyMoneyView::slotContextMenuRequested); break; case View::Reports: disconnect(m_reportsView, &KReportsView::aboutToShow, this, &KMyMoneyView::connectView); connect(m_reportsView, &KReportsView::transactionSelected, m_ledgerView, &KGlobalLedgerView::slotLedgerSelected); break; case View::OnlineJobOutbox: disconnect(m_onlineJobOutboxView, &KOnlineJobOutbox::aboutToShow, this, &KMyMoneyView::connectView); break; default: break; } } 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())) m_ledgerView->slotLedgerSelected(acc.id(), QString()); } else if (typeid(obj) == typeid(MyMoneyInstitution)) { // const auto& inst = static_cast(obj); m_institutionsView->slotEditInstitution(); } else if (typeid(obj) == typeid(MyMoneySchedule)) { m_scheduledView->slotEditSchedule(); } else if (typeid(obj) == typeid(MyMoneyReport)) { const auto& rep = static_cast(obj); m_reportsView->slotOpenReport(rep); } } 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)) { kmymoney->slotSelectAccount(obj); m_investmentView->updateActions(obj); m_categoriesView->updateActions(obj); m_accountsView->updateActions(obj); m_ledgerView->updateActions(obj); m_reportsView->updateActions(obj); if (m_onlineJobOutboxView) { m_onlineJobOutboxView->updateActions(obj); } // 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)) { m_institutionsView->updateActions(obj); } else if (typeid(obj) == typeid(MyMoneySchedule)) { kmymoney->slotUpdateActions(); m_scheduledView->updateActions(obj); } } void KMyMoneyView::slotContextMenuRequested(const MyMoneyObject& obj) { if (typeid(obj) == typeid(MyMoneyAccount)) { const auto& acc = static_cast(obj); if (acc.isInvest()) { m_investmentView->slotShowInvestmentMenu(acc); return; } else if (acc.isIncomeExpense()) { m_categoriesView->slotShowCategoriesMenu(acc); } else { m_accountsView->slotShowAccountMenu(acc); } } else if (typeid(obj) == typeid(MyMoneyInstitution)) { const auto& inst = static_cast(obj); m_institutionsView->slotShowInstitutionsMenu(inst); } else if (typeid(obj) == typeid(MyMoneySchedule)) { const auto& sch = static_cast(obj); m_scheduledView->slotShowScheduleMenu(sch); } } void KMyMoneyView::slotTransactionsMenuRequested(const KMyMoneyRegister::SelectedTransactions& list) { Q_UNUSED(list) m_ledgerView->slotShowTransactionMenu(MyMoneySplit()); } void KMyMoneyView::slotTransactionsSelected(const KMyMoneyRegister::SelectedTransactions& list) { m_ledgerView->updateLedgerActions(list); emit transactionsSelected(list); // for plugins } diff --git a/kmymoney/views/kmymoneyview.h b/kmymoney/views/kmymoneyview.h index 0ef62427c..ead90e6b2 100644 --- a/kmymoney/views/kmymoneyview.h +++ b/kmymoney/views/kmymoneyview.h @@ -1,560 +1,385 @@ /*************************************************************************** kmymoneyview.h ------------------- copyright : (C) 2000-2001 by Michael Edwardes 2004 by Thomas Baumgart 2017 by Łukasz Wojniłowicz ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #ifndef KMYMONEYVIEW_H #define KMYMONEYVIEW_H #include // ---------------------------------------------------------------------------- // QT Includes #include // ---------------------------------------------------------------------------- // KDE Includes #include // ---------------------------------------------------------------------------- // Project Includes #include "selectedtransactions.h" #ifdef KF5Activities_FOUND namespace KActivities { class ResourceInstance; } #endif namespace eAccountsModel { enum class Column; } namespace eMenu { enum class Action; } namespace KMyMoneyPlugin { class OnlinePlugin; } namespace KMyMoneyPlugin { class StoragePlugin; } namespace eDialogs { enum class ScheduleResultCode; } namespace Icons { enum class Icon; } class KMyMoneyApp; class KHomeView; class KAccountsView; class KCategoriesView; class KInstitutionsView; class KPayeesView; class KTagsView; class KBudgetView; class KScheduledView; class KGlobalLedgerView; class IMyMoneyOperationsFormat; class MyMoneyTransaction; class KInvestmentView; class KReportsView; +class SimpleLedgerView; class MyMoneySchedule; class MyMoneySecurity; class MyMoneyReport; class TransactionEditor; class KOnlineJobOutbox; class KMyMoneyTitleLabel; class MyMoneyAccount; class MyMoneyMoney; class MyMoneyObject; class QLabel; class KMyMoneyViewBase; /** * This class represents the view of the MyMoneyFile which contains * Banks/Accounts/Transactions, Recurring transactions (or Bills & Deposits) * and scripts (yet to be implemented). Each different aspect of the file * is represented by a tab within the view. * * @author Michael Edwardes 2001 Copyright 2000-2001 * * @short Handles the view of the MyMoneyFile. */ enum class View; class KMyMoneyView : public KPageWidget { Q_OBJECT public: // file actions for plugin enum fileActions { preOpen, postOpen, preSave, postSave, preClose, postClose }; private: enum menuID { AccountNew = 1, AccountOpen, AccountReconcile, AccountEdit, AccountDelete, AccountOnlineMap, AccountOnlineUpdate, AccountOfxConnect, CategoryNew }; enum storageTypeE { Memory = 0, Database } _storageType; KPageWidgetModel* m_model; KHomeView *m_homeView; KAccountsView *m_accountsView; KInstitutionsView *m_institutionsView; KCategoriesView *m_categoriesView; KPayeesView *m_payeesView; KTagsView *m_tagsView; KBudgetView *m_budgetView; KScheduledView *m_scheduledView; KGlobalLedgerView *m_ledgerView; KInvestmentView *m_investmentView; KReportsView* m_reportsView; KOnlineJobOutbox* m_onlineJobOutboxView; + #ifdef ENABLE_UNFINISHEDFEATURES + SimpleLedgerView* m_simpleLedgerView; + #endif + QHash viewFrames; QHash viewBases; KMyMoneyTitleLabel* m_header; bool m_inConstructor; - bool m_fileOpen; - QFileDevice::Permissions m_fmode; int m_lastViewSelected; QMap* m_storagePlugins; - // 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; - fileTypeE m_fileType; - #ifdef KF5Activities_FOUND private: KActivities::ResourceInstance * m_activityResourceInstance; #endif private: - void ungetString(QIODevice *qfile, char * buf, int len); - - /** - * if no base currency is defined, start the dialog and force it to be set - */ - void selectBaseCurrency(); - - /** - * This method attaches an empty storage object to the MyMoneyFile - * object. It calls removeStorage() to remove a possibly attached - * storage object. - */ - void newStorage(); - - /** - * This method removes an attached storage from the MyMoneyFile - * object. - */ - void removeStorage(); - void viewAccountList(const QString& selectAccount); // Show the accounts view - static void progressCallback(int current, int total, const QString&); - - /** - */ - void fixFile_0(); - void fixFile_1(); - void fixFile_2(); - void fixFile_3(); - - /** - */ - void fixLoanAccount_0(MyMoneyAccount acc); - - /** - */ - void fixTransactions_0(); - void fixSchedule_0(MyMoneySchedule sched); - void fixDuplicateAccounts_0(MyMoneyTransaction& t); - void createSchedule(MyMoneySchedule s, MyMoneyAccount& a); - void checkAccountName(const MyMoneyAccount& acc, const QString& name) const; - public: /** * The constructor for KMyMoneyView. Just creates all the tabs for the * different aspects of the MyMoneyFile. */ explicit KMyMoneyView(KMyMoneyApp *kmymoney); /** * Destructor */ ~KMyMoneyView(); - /** - * Makes sure that a MyMoneyFile is open and has been created successfully. - * - * @return Whether the file is open and initialised - */ - bool fileOpen(); - - /** - * Closes the open MyMoneyFile and frees all the allocated memory, I hope ! - */ - void closeFile(); - - - /** - * 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 readFile(const QUrl &url, IMyMoneyOperationsFormat *pExtReader = nullptr); - - /** - * 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()); - - /** - * 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() { - return (m_fileOpen && (m_fileType < MaxNativeFileType)); - } - - /** - * 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() { - return (m_fileOpen && ((m_fileType == KmmDb))); - } - - /** - * 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(); - - /** - * Close the currently opened file and create an empty new file. - * - * @see MyMoneyFile - */ - void newFile(); - /** * This method enables the state of all views (except home view) according * to an open file. */ - void enableViewsIfFileOpen(); + void enableViewsIfFileOpen(bool fileOpen); + void switchToDefaultView(); + void switchToHomeView(); void addWidget(QWidget* w); void showPage(KPageWidgetItem* pageItem); /** * check if the current view allows to print something * * @retval true Yes, view allows to print * @retval false No, view cannot print */ bool canPrint(); void finishReconciliation(const MyMoneyAccount& account); - /** - * This method updates names of currencies from file to localized names - */ - void updateCurrencyNames(); - - /** - * This method loads all known currencies and saves them to the storage - */ - void loadAllCurrencies(); - void showTitleBar(bool show); /** * This method changes the view type according to the settings. */ void updateViewType(); void slotAccountTreeViewChanged(const eAccountsModel::Column column, const bool show); void setOnlinePlugins(QMap& plugins); void setStoragePlugins(QMap& plugins); // TODO: remove that function /** * ugly proxy function */ eDialogs::ScheduleResultCode enterSchedule(MyMoneySchedule& schedule, bool autoEnter, bool extendedKeys); void addView(KMyMoneyViewBase* view, const QString& name, View idView); void removeView(View idView); protected: /** * Overwritten because KMyMoney has it's custom header. */ virtual bool showPageHeader() const; public Q_SLOTS: /** * This slot writes information about the page passed as argument @a current * in the kmymoney.rc file so that it can be selected automatically when * the application is started again. * * @param current QModelIndex of the current page item * @param previous QModelIndex of the previous page item */ void slotCurrentPageChanged(const QModelIndex current, const QModelIndex previous); /** * Brings up a dialog to change the list(s) settings and saves them into the * class KMyMoneySettings (a singleton). * * @see KListSettingsDlg * Refreshes all views. Used e.g. after settings have been changed or * data has been loaded from external sources (QIF import). **/ void slotRefreshViews(); /** * Called, whenever the payees view should pop up and a specific * transaction in an account should be shown. * * @param payeeId The ID of the payee to be shown * @param accountId The ID of the account to be shown * @param transactionId The ID of the transaction to be selected */ void slotPayeeSelected(const QString& payeeId, const QString& accountId, const QString& transactionId); /** * Called, whenever the tags view should pop up and a specific * transaction in an account should be shown. * * @param tagId The ID of the tag to be shown * @param accountId The ID of the account to be shown * @param transactionId The ID of the transaction to be selected */ void slotTagSelected(const QString& tagId, const QString& accountId, const QString& transactionId); /** * This slot prints the current view. */ void slotPrintView(); /** * Called when the user changes the detail * setting of the transaction register * * @param detailed if true, the register is shown with all details */ void slotShowTransactionDetail(bool detailed); - - /** * Informs respective views about selected object, so they can * update action states and current object. * @param obj Account, Category, Investment, Stock, Institution */ void slotObjectSelected(const MyMoneyObject& obj); void slotTransactionsSelected(const KMyMoneyRegister::SelectedTransactions& list); + void slotFileOpened(); + void slotFileClosed(); + private Q_SLOTS: /** * This slots switches the view to the specific page */ void slotShowHomePage(); void slotShowInstitutionsPage(); void slotShowAccountsPage(); void slotShowSchedulesPage(); void slotShowCategoriesPage(); void slotShowTagsPage(); void slotShowPayeesPage(); void slotShowLedgersPage(); void slotShowInvestmentsPage(); void slotShowReportsPage(); void slotShowBudgetPage(); void slotShowForecastPage(); void slotShowOutboxPage(); /** * Opens object in ledgers or edits in case of institution * @param obj Account, Category, Investment, Stock, Institution */ void slotOpenObjectRequested(const MyMoneyObject& obj); /** * Opens context menu based on objects's type * @param obj Account, Category, Investment, Stock, Institution */ void slotContextMenuRequested(const MyMoneyObject& obj); void slotTransactionsMenuRequested(const KMyMoneyRegister::SelectedTransactions& list); void slotSwitchView(View view); protected Q_SLOTS: /** * eventually replace this with KMyMoneyApp::slotCurrencySetBase(). * it contains the same code * * @deprecated */ void slotSetBaseCurrency(const MyMoneySecurity& baseCurrency); private: - /** - * 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 &dbaseURL); - /** - * 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(); - /** - * 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* writer, bool plaintext = false, const QString& keyList = QString()); /** * Internal method used by slotAccountNew() and slotAccountCategory(). */ void accountNew(const bool createCategory); void resetViewSelection(const View); void connectView(const View); Q_SIGNALS: /** * This signal is emitted whenever a view is selected. * The parameter @p view is identified as one of KMyMoneyView::viewID. */ void viewActivated(int view); /** * This signal is emitted whenever a new view is about to be selected. */ void aboutToChangeView(); void accountSelectedForContextMenu(const MyMoneyAccount& acc); void viewStateChanged(bool enabled); /** * This signal is emitted to inform the kmmFile plugin when various file actions * occur. The Action parameter distinguishes between them. */ void kmmFilePlugin(unsigned int action); - /** - * This signal is emitted after a data source has been closed - */ - void fileClosed(); - - /** - * This signal is emitted after a data source has been opened - */ - void fileOpened(); - /** * @brief proxy signal */ void statusMsg(const QString& txt); /** * @brief proxy signal */ void statusProgress(int cnt, int base); void accountReconciled(const MyMoneyAccount& account, const QDate& date, const MyMoneyMoney& startingBalance, const MyMoneyMoney& endingBalance, const QList >& transactionList); /** * 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 when a new account has been selected by * the GUI. If no account is selected or the selection is removed, * @a account is identical to MyMoneyAccount(). This signal is used * by plugins to get information about changes. */ void accountSelected(const MyMoneyAccount& account); }; #endif diff --git a/kmymoney/views/simpleledgerview.cpp b/kmymoney/views/simpleledgerview.cpp index 588abeab2..64fee888b 100644 --- a/kmymoney/views/simpleledgerview.cpp +++ b/kmymoney/views/simpleledgerview.cpp @@ -1,246 +1,249 @@ /*************************************************************************** simpleledgerview.cpp ------------------- begin : Sat Aug 8 2015 copyright : (C) 2015 by Thomas Baumgart email : Thomas Baumgart ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "simpleledgerview.h" // ---------------------------------------------------------------------------- // QT Includes #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes #include "ledgerviewpage.h" #include "models.h" #include "accountsmodel.h" #include "kmymoneyaccountcombo.h" #include "ui_simpleledgerview.h" #include "icons/icons.h" #include "kmymoneyview.h" #include "mymoneyfile.h" #include "mymoneyaccount.h" #include "mymoneyenums.h" #include "modelenums.h" using namespace Icons; class SimpleLedgerView::Private { public: Private(SimpleLedgerView* p) : parent(p) , ui(new Ui_SimpleLedgerView) , accountsModel(new AccountNamesFilterProxyModel(parent)) , newTabWidget(0) , lastIdx(-1) , inModelUpdate(false) , m_needLoad(true) {} ~Private() {} SimpleLedgerView* parent; Ui_SimpleLedgerView* ui; AccountNamesFilterProxyModel* accountsModel; QWidget* newTabWidget; int lastIdx; bool inModelUpdate; bool m_needLoad; }; SimpleLedgerView::SimpleLedgerView(KMyMoneyApp *kmymoney, KMyMoneyView *kmymoneyview) : QWidget(nullptr) , d(new Private(this)) , m_kmymoney(kmymoney) , m_kmymoneyview(kmymoneyview) { } SimpleLedgerView::~SimpleLedgerView() { if (!d->m_needLoad) delete d->ui; delete d; } void SimpleLedgerView::init() { d->m_needLoad = false; d->ui->setupUi(this); d->ui->ledgerTab->setTabIcon(0, Icons::get(Icon::ListAdd)); d->ui->ledgerTab->setTabText(0, QString()); d->newTabWidget = d->ui->ledgerTab->widget(0); // remove close button from new page QTabBar* bar = d->ui->ledgerTab->findChild(); if(bar) { QTabBar::ButtonPosition closeSide = (QTabBar::ButtonPosition)style()->styleHint(QStyle::SH_TabBar_CloseButtonPosition, 0, d->newTabWidget); QWidget *w = bar->tabButton(0, closeSide); bar->setTabButton(0, closeSide, 0); w->deleteLater(); connect(bar, SIGNAL(tabMoved(int,int)), this, SLOT(checkTabOrder(int,int))); } connect(d->ui->accountCombo, SIGNAL(accountSelected(QString)), this, SLOT(openNewLedger(QString))); connect(d->ui->ledgerTab, &QTabWidget::currentChanged, this, &SimpleLedgerView::tabSelected); connect(Models::instance(), &Models::modelsLoaded, this, &SimpleLedgerView::updateModels); connect(d->ui->ledgerTab, &QTabWidget::tabCloseRequested, this, &SimpleLedgerView::closeLedger); - connect(m_kmymoneyview, &KMyMoneyView::fileClosed, this, &SimpleLedgerView::closeLedgers); - connect(m_kmymoneyview, &KMyMoneyView::fileOpened, this, &SimpleLedgerView::openFavoriteLedgers); d->accountsModel->addAccountGroup(QVector {eMyMoney::Account::Type::Asset, eMyMoney::Account::Type::Liability, eMyMoney::Account::Type::Equity}); d->accountsModel->setHideEquityAccounts(false); auto const model = Models::instance()->accountsModel(); d->accountsModel->setSourceModel(model); d->accountsModel->setSourceColumns(model->getColumns()); d->accountsModel->sort((int)eAccountsModel::Column::Account); d->ui->accountCombo->setModel(d->accountsModel); tabSelected(0); updateModels(); openFavoriteLedgers(); } void SimpleLedgerView::openNewLedger(QString accountId) { if(d->inModelUpdate || accountId.isEmpty()) return; LedgerViewPage* view = 0; // check if ledger is already opened for(int idx=0; idx < d->ui->ledgerTab->count()-1; ++idx) { view = qobject_cast(d->ui->ledgerTab->widget(idx)); if(view) { if(accountId == view->accountId()) { d->ui->ledgerTab->setCurrentIndex(idx); return; } } } // need a new tab, we insert it before the rightmost one QModelIndex index = Models::instance()->accountsModel()->accountById(accountId); if(index.isValid()) { // create new ledger view page MyMoneyAccount acc = Models::instance()->accountsModel()->data(index, (int)eAccountsModel::Role::Account).value(); view = new LedgerViewPage(this); view->setShowEntryForNewTransaction(); view->setAccount(acc); /// @todo setup current global setting for form visibility // view->showTransactionForm(...); // insert new ledger view page in tab view int newIdx = d->ui->ledgerTab->insertTab(d->ui->ledgerTab->count()-1, view, acc.name()); d->ui->ledgerTab->setCurrentIndex(d->ui->ledgerTab->count()-1); d->ui->ledgerTab->setCurrentIndex(newIdx); } } void SimpleLedgerView::tabSelected(int idx) { // qDebug() << "tabSelected" << idx << (d->ui->ledgerTab->count()-1); if(idx != (d->ui->ledgerTab->count()-1)) { d->lastIdx = idx; } } void SimpleLedgerView::updateModels() { d->inModelUpdate = true; // d->ui->accountCombo-> d->ui->accountCombo->expandAll(); d->ui->accountCombo->setSelected(MyMoneyFile::instance()->asset().id()); d->inModelUpdate = false; } void SimpleLedgerView::closeLedger(int idx) { // don't react on the close request for the new ledger function if(idx != (d->ui->ledgerTab->count()-1)) { d->ui->ledgerTab->removeTab(idx); } } void SimpleLedgerView::checkTabOrder(int from, int to) { if(d->inModelUpdate) return; QTabBar* bar = d->ui->ledgerTab->findChild(); if(bar) { const int rightMostIdx = d->ui->ledgerTab->count()-1; if(from == rightMostIdx) { // someone tries to move the new account tab away from the rightmost position d->inModelUpdate = true; bar->moveTab(to, from); d->inModelUpdate = false; } } } void SimpleLedgerView::showTransactionForm(bool show) { emit showForms(show); } void SimpleLedgerView::closeLedgers() { - int tabCount = d->ui->ledgerTab->count(); + if (d->m_needLoad) + return; + auto tabCount = d->ui->ledgerTab->count(); // check that we have a least one tab that can be closed if(tabCount > 1) { // we keep the tab with the selector open at all times // which is located in the right most position --tabCount; do { --tabCount; closeLedger(tabCount); } while(tabCount > 0); } } void SimpleLedgerView::openFavoriteLedgers() { + if (d->m_needLoad) + return; + AccountsModel* model = Models::instance()->accountsModel(); QModelIndex start = model->index(0, 0); QModelIndexList indexes = model->match(start, (int)eAccountsModel::Role::Favorite, QVariant(true), -1, Qt::MatchRecursive); // indexes now has a list of favorite accounts but two entries for each. // that doesn't matter here, since openNewLedger() can handle duplicates Q_FOREACH(QModelIndex index, indexes) { openNewLedger(model->data(index, (int)eAccountsModel::Role::ID).toString()); } d->ui->ledgerTab->setCurrentIndex(0); } void SimpleLedgerView::showEvent(QShowEvent* event) { if (d->m_needLoad) init(); // don't forget base class implementation QWidget::showEvent(event); }