diff --git a/kmymoney/kmymoney.h b/kmymoney/kmymoney.h --- a/kmymoney/kmymoney.h +++ b/kmymoney/kmymoney.h @@ -59,6 +59,7 @@ class MyMoneyTransaction; class WebConnect; class creditTransfer; +class IMyMoneyOperationsFormat; template class onlineJobTyped; @@ -261,6 +262,21 @@ 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. @@ -352,6 +368,24 @@ */ 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 @@ -425,9 +459,6 @@ 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. @@ -445,15 +476,6 @@ */ 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(); @@ -674,6 +696,8 @@ void startMatchTransaction(const MyMoneyTransaction& t); void cancelMatchTransaction(); + void kmmFilePlugin(unsigned int); + public: bool isActionToggled(const eMenu::Action _a); diff --git a/kmymoney/kmymoney.cpp b/kmymoney/kmymoney.cpp --- a/kmymoney/kmymoney.cpp +++ b/kmymoney/kmymoney.cpp @@ -67,6 +67,10 @@ #include #include #include +#include +#include +#include +#include #ifdef KF5Holidays_FOUND #include #include @@ -108,20 +112,26 @@ #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" @@ -151,6 +161,9 @@ #include "misc/webconnect.h" #include "storage/mymoneystoragemgr.h" +#include "storage/mymoneystoragexml.h" +#include "storage/mymoneystoragebin.h" +#include "storage/mymoneystorageanon.h" #include @@ -178,7 +191,9 @@ 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 @@ -209,6 +224,8 @@ 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), @@ -293,6 +310,11 @@ */ 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. @@ -360,6 +382,1246 @@ 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) : @@ -408,6 +1670,7 @@ 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); @@ -476,6 +1739,7 @@ KMyMoneyApp::~KMyMoneyApp() { + d->removeStorage(); // delete cached objects since the are in the way // when unloading the plugins onlineJobAdministration::instance()->clearCaches(); @@ -1006,7 +2270,7 @@ if (!isReady()) return false; - if (d->m_myMoneyView->dirty()) { + if (d->dirty()) { int ans = askSaveOnClose(); if (ans == KMessageBox::Cancel) @@ -1157,9 +2421,9 @@ 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(); @@ -1212,7 +2476,10 @@ // 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 @@ -1223,29 +2490,56 @@ } 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); @@ -1255,21 +2549,6 @@ 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; @@ -1318,46 +2597,62 @@ 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; @@ -1370,7 +2665,7 @@ 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) { @@ -1382,7 +2677,7 @@ 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); @@ -1418,7 +2713,7 @@ } QString prevDir; // don't prompt file name if not a native file - if (d->m_myMoneyView->isNativeFile()) + if (isNativeFile()) prevDir = readLastUsedDir(); QPointer dlg = @@ -1446,7 +2741,7 @@ // 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; @@ -1459,7 +2754,7 @@ 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); @@ -1474,63 +2769,11 @@ 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; @@ -1551,7 +2794,7 @@ 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; @@ -1615,7 +2858,7 @@ #ifdef KMM_DEBUG void KMyMoneyApp::slotFileFileInfo() { - if (!d->m_myMoneyView->fileOpen()) { + if (!d->m_fileOpen) { KMessageBox::information(this, i18n("No KMyMoneyFile open")); return; } @@ -1710,7 +2953,7 @@ void KMyMoneyApp::slotFileViewPersonal() { - if (!d->m_myMoneyView->fileOpen()) { + if (!d->m_fileOpen) { KMessageBox::information(this, i18n("No KMyMoneyFile open")); return; } @@ -1880,7 +3123,7 @@ 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); } @@ -1892,7 +3135,7 @@ 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; @@ -1921,7 +3164,7 @@ 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 " @@ -2471,7 +3714,7 @@ 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 @@ -2493,15 +3736,15 @@ 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(); @@ -2527,7 +3770,7 @@ // {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*/)}, @@ -2925,12 +4168,12 @@ //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 @@ -2991,7 +4234,7 @@ //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); @@ -3319,7 +4562,21 @@ 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(); diff --git a/kmymoney/kmymoneyutils.h b/kmymoney/kmymoneyutils.h --- a/kmymoney/kmymoneyutils.h +++ b/kmymoney/kmymoneyutils.h @@ -371,6 +371,8 @@ */ 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); diff --git a/kmymoney/kmymoneyutils.cpp b/kmymoney/kmymoneyutils.cpp --- a/kmymoney/kmymoneyutils.cpp +++ b/kmymoney/kmymoneyutils.cpp @@ -38,6 +38,7 @@ #include #include #include +#include // ---------------------------------------------------------------------------- // KDE Headers @@ -49,6 +50,7 @@ #include #include #include +#include // ---------------------------------------------------------------------------- // Project Includes @@ -654,6 +656,28 @@ 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; diff --git a/kmymoney/plugins/appinterface.h b/kmymoney/plugins/appinterface.h --- a/kmymoney/plugins/appinterface.h +++ b/kmymoney/plugins/appinterface.h @@ -32,6 +32,8 @@ class QTimer; +class IMyMoneyOperationsFormat; + namespace KMyMoneyPlugin { class KMM_PLUGIN_EXPORT AppInterface : public QObject @@ -42,13 +44,24 @@ 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); }; } diff --git a/kmymoney/plugins/gnc/import/CMakeLists.txt b/kmymoney/plugins/gnc/import/CMakeLists.txt --- a/kmymoney/plugins/gnc/import/CMakeLists.txt +++ b/kmymoney/plugins/gnc/import/CMakeLists.txt @@ -24,11 +24,8 @@ 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.h b/kmymoney/plugins/gnc/import/gncimporter.h --- a/kmymoney/plugins/gnc/import/gncimporter.h +++ b/kmymoney/plugins/gnc/import/gncimporter.h @@ -29,32 +29,20 @@ 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.cpp b/kmymoney/plugins/gnc/import/gncimporter.cpp --- a/kmymoney/plugins/gnc/import/gncimporter.cpp +++ b/kmymoney/plugins/gnc/import/gncimporter.cpp @@ -35,6 +35,7 @@ #include "mymoneygncreader.h" #include "viewinterface.h" +#include "appinterface.h" #include "mymoneyfile.h" #include "mymoneyexception.h" #include "mymoneystoragemgr.h" @@ -46,8 +47,6 @@ { Q_UNUSED(args) setComponentName("gncimporter", i18n("GnuCash importer")); - setXMLFile("gncimporter.rc"); - createActions(); // For information, announce that we have been loaded. qDebug("Plugins: gncimporter loaded"); } @@ -57,31 +56,32 @@ 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();) diff --git a/kmymoney/plugins/gnc/import/gncimporter.rc b/kmymoney/plugins/gnc/import/gncimporter.rc deleted file mode 100644 --- a/kmymoney/plugins/gnc/import/gncimporter.rc +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/kmymoney/plugins/gnc/import/mymoneygncreader.h b/kmymoney/plugins/gnc/import/mymoneygncreader.h --- a/kmymoney/plugins/gnc/import/mymoneygncreader.h +++ b/kmymoney/plugins/gnc/import/mymoneygncreader.h @@ -1081,6 +1081,10 @@ 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 }; diff --git a/kmymoney/plugins/gnc/import/mymoneygncreader.cpp b/kmymoney/plugins/gnc/import/mymoneygncreader.cpp --- a/kmymoney/plugins/gnc/import/mymoneygncreader.cpp +++ b/kmymoney/plugins/gnc/import/mymoneygncreader.cpp @@ -1318,6 +1318,7 @@ //m_defaultPayee = createPayee (i18n("Unknown payee")); MyMoneyFile::instance()->attachStorage(m_storage); + loadAllCurrencies(); MyMoneyFileTransaction ft; m_xr = new XmlReader(this); bool blocked = MyMoneyFile::instance()->signalsBlocked(); @@ -2639,6 +2640,26 @@ 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&)) diff --git a/kmymoney/plugins/interfaces/kmmappinterface.h b/kmymoney/plugins/interfaces/kmmappinterface.h --- a/kmymoney/plugins/interfaces/kmmappinterface.h +++ b/kmymoney/plugins/interfaces/kmmappinterface.h @@ -48,6 +48,14 @@ 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; diff --git a/kmymoney/plugins/interfaces/kmmappinterface.cpp b/kmymoney/plugins/interfaces/kmmappinterface.cpp --- a/kmymoney/plugins/interfaces/kmmappinterface.cpp +++ b/kmymoney/plugins/interfaces/kmmappinterface.cpp @@ -33,6 +33,17 @@ 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 diff --git a/kmymoney/plugins/interfaces/kmmviewinterface.h b/kmymoney/plugins/interfaces/kmmviewinterface.h --- a/kmymoney/plugins/interfaces/kmmviewinterface.h +++ b/kmymoney/plugins/interfaces/kmmviewinterface.h @@ -67,27 +67,6 @@ */ // 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). diff --git a/kmymoney/plugins/interfaces/kmmviewinterface.cpp b/kmymoney/plugins/interfaces/kmmviewinterface.cpp --- a/kmymoney/plugins/interfaces/kmmviewinterface.cpp +++ b/kmymoney/plugins/interfaces/kmmviewinterface.cpp @@ -44,12 +44,6 @@ // 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() @@ -67,17 +61,6 @@ 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); diff --git a/kmymoney/plugins/kmymoneyplugin.h b/kmymoney/plugins/kmymoneyplugin.h --- a/kmymoney/plugins/kmymoneyplugin.h +++ b/kmymoney/plugins/kmymoneyplugin.h @@ -41,6 +41,7 @@ class MyMoneyStorageMgr; class MyMoneyAccount; class KMyMoneySettings; +class IMyMoneyOperationsFormat; namespace KMyMoneyPlugin { class AppInterface; } namespace KMyMoneyPlugin { class ImportInterface; } @@ -303,11 +304,19 @@ */ 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; }; diff --git a/kmymoney/plugins/sql/sqlstorage.h b/kmymoney/plugins/sql/sqlstorage.h --- a/kmymoney/plugins/sql/sqlstorage.h +++ b/kmymoney/plugins/sql/sqlstorage.h @@ -45,6 +45,7 @@ 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(); diff --git a/kmymoney/plugins/sql/sqlstorage.cpp b/kmymoney/plugins/sql/sqlstorage.cpp --- a/kmymoney/plugins/sql/sqlstorage.cpp +++ b/kmymoney/plugins/sql/sqlstorage.cpp @@ -126,6 +126,11 @@ return QStringLiteral("SQL"); } +QString SQLStorage::fileExtension() const +{ + return QString(); +} + void SQLStorage::createActions() { m_openDBaction = actionCollection()->addAction("open_database"); @@ -201,7 +206,7 @@ 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); @@ -285,7 +290,7 @@ 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); } diff --git a/kmymoney/plugins/viewinterface.h b/kmymoney/plugins/viewinterface.h --- a/kmymoney/plugins/viewinterface.h +++ b/kmymoney/plugins/viewinterface.h @@ -48,7 +48,6 @@ class MyMoneyAccount; class MyMoneySplit; class MyMoneyTransaction; -class IMyMoneyOperationsFormat; class KMyMoneyViewBase; namespace KMyMoneyPlugin @@ -67,26 +66,6 @@ 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). @@ -155,7 +134,6 @@ 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 diff --git a/kmymoney/views/kmymoneyview.h b/kmymoney/views/kmymoneyview.h --- a/kmymoney/views/kmymoneyview.h +++ b/kmymoney/views/kmymoneyview.h @@ -62,6 +62,7 @@ class MyMoneyTransaction; class KInvestmentView; class KReportsView; +class SimpleLedgerView; class MyMoneySchedule; class MyMoneySecurity; class MyMoneyReport; @@ -127,80 +128,29 @@ 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 @@ -213,85 +163,13 @@ */ ~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); @@ -307,16 +185,6 @@ 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); /** @@ -399,8 +267,6 @@ */ void slotShowTransactionDetail(bool detailed); - - /** * Informs respective views about selected object, so they can * update action states and current object. @@ -410,6 +276,9 @@ void slotTransactionsSelected(const KMyMoneyRegister::SelectedTransactions& list); + void slotFileOpened(); + void slotFileClosed(); + private Q_SLOTS: /** * This slots switches the view to the specific page @@ -454,40 +323,6 @@ 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(). @@ -518,16 +353,6 @@ */ 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 */ diff --git a/kmymoney/views/kmymoneyview.cpp b/kmymoney/views/kmymoneyview.cpp --- a/kmymoney/views/kmymoneyview.cpp +++ b/kmymoney/views/kmymoneyview.cpp @@ -106,17 +106,12 @@ 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 @@ -145,7 +140,7 @@ } } - newStorage(); +// newStorage(); m_model = new KPageWidgetModel(this); // cannot be parentless, otherwise segfaults at exit connect(kmymoney, &KMyMoneyApp::fileLoaded, this, &KMyMoneyView::slotRefreshViews); @@ -242,8 +237,8 @@ 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 @@ -311,7 +306,25 @@ #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() @@ -575,791 +588,47 @@ 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()) { @@ -1384,114 +653,6 @@ } } -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()) @@ -1538,12 +699,6 @@ 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 @@ -1553,253 +708,6 @@ 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 @@ -1843,212 +751,6 @@ } } -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()) @@ -2070,6 +772,8 @@ 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: diff --git a/kmymoney/views/simpleledgerview.cpp b/kmymoney/views/simpleledgerview.cpp --- a/kmymoney/views/simpleledgerview.cpp +++ b/kmymoney/views/simpleledgerview.cpp @@ -105,8 +105,6 @@ 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}); @@ -209,7 +207,9 @@ 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 @@ -224,6 +224,9 @@ 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);