diff --git a/kmymoney/dialogs/CMakeLists.txt b/kmymoney/dialogs/CMakeLists.txt --- a/kmymoney/dialogs/CMakeLists.txt +++ b/kmymoney/dialogs/CMakeLists.txt @@ -41,6 +41,7 @@ stdtransactioneditor.cpp transactionmatcher.cpp ksaveasquestion.cpp + kmymoneydocstoragesettingsdlg.cpp ) set(dialogs_HEADERS @@ -68,6 +69,7 @@ ktemplateexportdlg.ui kupdatestockpricedlg.ui ksaveasquestion.ui + kmymoneydocstoragesettingsdlg.ui ) ki18n_wrap_ui(libdialogs_a_SOURCES ${dialogs_UI} ) diff --git a/kmymoney/dialogs/kmymoneydocstoragesettingsdlg.h b/kmymoney/dialogs/kmymoneydocstoragesettingsdlg.h new file mode 100644 --- /dev/null +++ b/kmymoney/dialogs/kmymoneydocstoragesettingsdlg.h @@ -0,0 +1,64 @@ +/* + * Copyright 2018 Marc Hübner + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + + +#ifndef KMYMONEYDOCSTORAGESETTINGSDLG_H +#define KMYMONEYDOCSTORAGESETTINGSDLG_H + +// ---------------------------------------------------------------------------- +// QT Includes + +#include + +// ---------------------------------------------------------------------------- +// KDE Includes + +// ---------------------------------------------------------------------------- +// Project Includes + + +// This dialog lets the user create/edit a file. +// Use the second constructor to edit a file. +class KMyMoneyDocStorageSettingsDlgPrivate; +class KMyMoneyDocStorageSettingsDlg : public QDialog +{ + Q_OBJECT + Q_DISABLE_COPY(KMyMoneyDocStorageSettingsDlg) + +public: + explicit KMyMoneyDocStorageSettingsDlg(QWidget *parent, const QString& title); + explicit KMyMoneyDocStorageSettingsDlg(bool storeCopies, + const QString& docStoragePath, + QWidget *parent, + const QString& title); + ~KMyMoneyDocStorageSettingsDlg(); + + bool storeCopies() const; + QString documentStoragePath() const; + +protected Q_SLOTS: + void okClicked(); + void slotToggleFileSelection(); + void slotSelectFolder(); + void selectedPathChanged(); + +private: + KMyMoneyDocStorageSettingsDlgPrivate * const d_ptr; + Q_DECLARE_PRIVATE(KMyMoneyDocStorageSettingsDlg) +}; + +#endif//KMYMONEYDOCSTORAGESETTINGSDLG_H diff --git a/kmymoney/dialogs/kmymoneydocstoragesettingsdlg.cpp b/kmymoney/dialogs/kmymoneydocstoragesettingsdlg.cpp new file mode 100644 --- /dev/null +++ b/kmymoney/dialogs/kmymoneydocstoragesettingsdlg.cpp @@ -0,0 +1,144 @@ +/* + * Copyright 2018 Marc Hübner + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + + +// ---------------------------------------------------------------------------- +// QT Includes + +#include +#include + +// ---------------------------------------------------------------------------- +// KDE Headers + +#include +#include + +// ---------------------------------------------------------------------------- +// Project Includes +#include "kmymoneydocstoragesettingsdlg.h" +#include "ui_kmymoneydocstoragesettingsdlg.h" + +class KMyMoneyDocStorageSettingsDlgPrivate +{ + Q_DISABLE_COPY(KMyMoneyDocStorageSettingsDlgPrivate) + Q_DECLARE_PUBLIC(KMyMoneyDocStorageSettingsDlg) + +public: + explicit KMyMoneyDocStorageSettingsDlgPrivate(KMyMoneyDocStorageSettingsDlg *qq) : + q_ptr(qq), + ui(new Ui::KMyMoneyDocStorageSettingsDlg) + { + } + + ~KMyMoneyDocStorageSettingsDlgPrivate() + { + delete ui; + } + + void init(const QString& title) + { + Q_Q(KMyMoneyDocStorageSettingsDlg); + ui->setupUi(q); + q->setModal(true); + + if (!title.isEmpty()) + q->setWindowTitle(title); + + q->connect(ui->buttonBox, &QDialogButtonBox::rejected, q, &QDialog::reject); + q->connect(ui->buttonBox, &QDialogButtonBox::accepted, q, &KMyMoneyDocStorageSettingsDlg::okClicked); + q->connect(ui->m_storeCopies, &QCheckBox::clicked, q, &KMyMoneyDocStorageSettingsDlg::slotToggleFileSelection); + q->connect(ui->m_selectFolderButton, &QAbstractButton::clicked, q, &KMyMoneyDocStorageSettingsDlg::slotSelectFolder); + q->connect(ui->m_storagePath, &KLineEdit::textChanged, q, &KMyMoneyDocStorageSettingsDlg::selectedPathChanged); + q->connect(ui->m_storeCopies, &QCheckBox::clicked, q, &KMyMoneyDocStorageSettingsDlg::selectedPathChanged); + } + + KMyMoneyDocStorageSettingsDlg *q_ptr; + Ui::KMyMoneyDocStorageSettingsDlg *ui; + bool storeCopies; + QString documentStoragePath; +}; + + +KMyMoneyDocStorageSettingsDlg::KMyMoneyDocStorageSettingsDlg(QWidget *parent, const QString& title) : + QDialog(parent), + d_ptr(new KMyMoneyDocStorageSettingsDlgPrivate(this)) +{ + Q_D(KMyMoneyDocStorageSettingsDlg); + d->init(title); +} + +KMyMoneyDocStorageSettingsDlg::KMyMoneyDocStorageSettingsDlg(bool storeCopies, + const QString& documentStoragePath, + QWidget *parent, + const QString& title) : + QDialog(parent), + d_ptr(new KMyMoneyDocStorageSettingsDlgPrivate(this)) +{ + Q_D(KMyMoneyDocStorageSettingsDlg); + d->init(title); + d->ui->m_storeCopies->setChecked(storeCopies); + d->ui->m_storagePath->setText(documentStoragePath); + d->ui->m_storagePath->setEnabled(storeCopies); + d->ui->m_selectFolderButton->setEnabled(storeCopies); +} + +bool KMyMoneyDocStorageSettingsDlg::storeCopies() const +{ + Q_D(const KMyMoneyDocStorageSettingsDlg); + return d->storeCopies; +} + +QString KMyMoneyDocStorageSettingsDlg::documentStoragePath() const +{ + Q_D(const KMyMoneyDocStorageSettingsDlg); + return d->documentStoragePath; +} + +KMyMoneyDocStorageSettingsDlg::~KMyMoneyDocStorageSettingsDlg() +{ + Q_D(KMyMoneyDocStorageSettingsDlg); + delete d; +} + +void KMyMoneyDocStorageSettingsDlg::okClicked() +{ + Q_D(KMyMoneyDocStorageSettingsDlg); + d->storeCopies = d->ui->m_storeCopies->isChecked(); + d->documentStoragePath = d->ui->m_storagePath->text(); + accept(); +} + +void KMyMoneyDocStorageSettingsDlg::slotToggleFileSelection() { + Q_D(KMyMoneyDocStorageSettingsDlg); + d->ui->m_storagePath->setEnabled(!d->ui->m_storagePath->isEnabled()); + d->ui->m_selectFolderButton->setEnabled(!d->ui->m_selectFolderButton->isEnabled()); +} + +void KMyMoneyDocStorageSettingsDlg::slotSelectFolder() +{ + Q_D(const KMyMoneyDocStorageSettingsDlg); + auto fileName = QFileDialog::getExistingDirectory(this, i18n("Select directory to store copies of documents"), d->ui->m_storagePath->text()); + d->ui->m_storagePath->setText(fileName); +} + +void KMyMoneyDocStorageSettingsDlg::selectedPathChanged() { + Q_D(const KMyMoneyDocStorageSettingsDlg); + const auto selectedPath = d->ui->m_storagePath->text(); + bool selectionOk = !d->ui->m_storeCopies->isChecked() || (!selectedPath.isEmpty() && QDir(selectedPath).exists()); + d->ui->buttonBox->button(QDialogButtonBox::StandardButton::Ok)->setEnabled(selectionOk); +} \ No newline at end of file diff --git a/kmymoney/dialogs/kmymoneydocstoragesettingsdlg.ui b/kmymoney/dialogs/kmymoneydocstoragesettingsdlg.ui new file mode 100644 --- /dev/null +++ b/kmymoney/dialogs/kmymoneydocstoragesettingsdlg.ui @@ -0,0 +1,128 @@ + + + + + + KMyMoneyDocStorageSettingsDlg + + + + 0 + 0 + 600 + 415 + + + + + + + + KMyMoney allows you to store arbitrary documents supporting each transaction.<br><br> + By default, only <b>references</b> to (external) documents are stored. However, you can choose to store <b>copies</b> of attached documents instead. + + + + true + + + + + + + + + + 25 + 0 + + + + QFrame::NoFrame + + + QFrame::Plain + + + + + + + + + + + + 100 + 0 + + + + Store copies of attached documents + + + + + + + + + + + Provide storage path + + + false + + + + + + + ... + + + false + + + + + + + + + + + + 25 + 0 + + + + QFrame::NoFrame + + + QFrame::Plain + + + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + KLineEdit + QLineEdit +
klineedit.h
+
+
+
diff --git a/kmymoney/icons/icons.h b/kmymoney/icons/icons.h --- a/kmymoney/icons/icons.h +++ b/kmymoney/icons/icons.h @@ -98,7 +98,8 @@ InvestmentNew, InvestmentEdit, InvestmentDelete, InvestmentOnlinePrice, BudgetNew, BudgetRename, BudgetDelete, BudgetCopy, - PriceUpdate, ToolUpdatePrices + PriceUpdate, ToolUpdatePrices, + ViewDocuments, ListAddDocument, ListRemoveDocument }; KMM_ICONS_EXPORT void setIconThemeNames(const QString &_themeName); diff --git a/kmymoney/icons/icons.cpp b/kmymoney/icons/icons.cpp --- a/kmymoney/icons/icons.cpp +++ b/kmymoney/icons/icons.cpp @@ -206,7 +206,8 @@ {Icon::PreferencesFont, QStringLiteral("preferences-desktop-font")}, {Icon::PreferencesIcon, QStringLiteral("preferences-desktop-icon")}, {Icon::NetworkDisconect, QStringLiteral("network-disconnect")}, - {Icon::Folder, QStringLiteral("folder")} + {Icon::Folder, QStringLiteral("folder")}, + {Icon::ViewDocuments, QStringLiteral("document-new")} }; } diff --git a/kmymoney/kmymoney.h b/kmymoney/kmymoney.h --- a/kmymoney/kmymoney.h +++ b/kmymoney/kmymoney.h @@ -58,6 +58,8 @@ class MyMoneyTag; class MyMoneySplit; class MyMoneyTransaction; +class MyMoneyDocumentType; +class MyMoneyDocument; class WebConnect; class creditTransfer; class IMyMoneyOperationsFormat; @@ -156,6 +158,10 @@ */ void slotFileViewPersonal(); + /** + * Called when the user asks for the document storage settings. + */ + void slotFileViewDocumentSettings(); void slotLoadAccountTemplates(); void slotSaveAccountTemplates(); diff --git a/kmymoney/kmymoney.cpp b/kmymoney/kmymoney.cpp --- a/kmymoney/kmymoney.cpp +++ b/kmymoney/kmymoney.cpp @@ -4,6 +4,7 @@ copyright : (C) 2000 by Michael Edwardes (C) 2007 by Thomas Baumgart (C) 2017, 2018 by Łukasz Wojniłowicz + (C) 2018 by Marc Hübner ****************************************************************************/ @@ -95,6 +96,7 @@ #include "wizards/newinvestmentwizard/knewinvestmentwizard.h" #include "dialogs/knewaccountdlg.h" #include "dialogs/editpersonaldatadlg.h" +#include "dialogs/kmymoneydocstoragesettingsdlg.h" #include "dialogs/kcurrencycalculator.h" #include "dialogs/keditscheduledlg.h" #include "wizards/newloanwizard/keditloanwizard.h" @@ -138,6 +140,8 @@ #include "mymoney/mymoneystatement.h" #include "mymoney/mymoneyforecast.h" #include "mymoney/mymoneytransactionfilter.h" +#include "mymoney/mymoneydocumenttype.h" +#include "mymoney/mymoneydocument.h" #include "mymoneyexception.h" @@ -1347,7 +1351,8 @@ {Menu::MoveTransaction, QStringLiteral("transaction_move_menu")}, {Menu::MarkTransaction, QStringLiteral("transaction_mark_menu")}, {Menu::MarkTransactionContext, QStringLiteral("transaction_context_mark_menu")}, - {Menu::OnlineJob, QStringLiteral("onlinejob_context_menu")} + {Menu::OnlineJob, QStringLiteral("onlinejob_context_menu")}, + {Menu::Document, QStringLiteral("document_context_menu")} }; for (auto it = menuNames.cbegin(); it != menuNames.cend(); ++it) @@ -1400,6 +1405,7 @@ {Action::FileImportTemplate, QStringLiteral("file_import_template"), i18n("Account Template..."), Icon::Empty}, {Action::FileExportTemplate, QStringLiteral("file_export_template"), i18n("Account Template..."), Icon::Empty}, {Action::FilePersonalData, QStringLiteral("view_personal_data"), i18n("Personal Data..."), Icon::UserProperties}, + {Action::FileDocumentSettings, QStringLiteral("file_view_document_setings"), i18n("Document Storage Settings"), Icon::Empty}, #ifdef KMM_DEBUG {Action::FileDump, QStringLiteral("file_dump"), i18n("Dump Memory"), Icon::Empty}, #endif @@ -1486,7 +1492,8 @@ {Action::AssignTransactionsNumber, QStringLiteral("transaction_assign_number"), i18n("Assign next number"), Icon::Empty}, {Action::CombineTransactions, QStringLiteral("transaction_combine"), i18nc("Combine transactions", "Combine"), Icon::Empty}, {Action::CopySplits, QStringLiteral("transaction_copy_splits"), i18n("Copy splits"), Icon::Empty}, - //Investment + {Action::AssignDocuments, QStringLiteral("transaction_assign_documents"), i18n("Assign documents"), Icon::DocumentNew}, + //Investment {Action::NewInvestment, QStringLiteral("investment_new"), i18n("New investment..."), Icon::InvestmentNew}, {Action::EditInvestment, QStringLiteral("investment_edit"), i18n("Edit investment..."), Icon::InvestmentEdit}, {Action::DeleteInvestment, QStringLiteral("investment_delete"), i18n("Delete investment..."), Icon::InvestmentDelete}, @@ -1508,7 +1515,11 @@ {Action::NewTag, QStringLiteral("tag_new"), i18n("New tag"), Icon::ListAddTag}, {Action::RenameTag, QStringLiteral("tag_rename"), i18n("Rename tag"), Icon::TagRename}, {Action::DeleteTag, QStringLiteral("tag_delete"), i18n("Delete tag"), Icon::ListRemoveTag}, - //debug actions + //Documents + {Action::NewDocument, QStringLiteral("document_new"), i18n("New document"), Icon::Empty}, + {Action::RenameDocument, QStringLiteral("document_rename"), i18n("Rename document"), Icon::Empty}, + {Action::DeleteDocument, QStringLiteral("document_delete"), i18n("Delete document"), Icon::Empty}, + //debug actions #ifdef KMM_DEBUG {Action::WizardNewUser, QStringLiteral("new_user_wizard"), i18n("Test new feature"), Icon::Empty}, {Action::DebugTraces, QStringLiteral("debug_traces"), i18n("Debug Traces"), Icon::Empty}, @@ -1546,6 +1557,7 @@ {Action::FileImportTemplate, &KMyMoneyApp::slotLoadAccountTemplates}, {Action::FileExportTemplate, &KMyMoneyApp::slotSaveAccountTemplates}, {Action::FilePersonalData, &KMyMoneyApp::slotFileViewPersonal}, + {Action::FileDocumentSettings, &KMyMoneyApp::slotFileViewDocumentSettings}, #ifdef KMM_DEBUG {Action::FileDump, &KMyMoneyApp::slotFileFileInfo}, #endif @@ -1650,7 +1662,7 @@ for(const auto& it : actionShortcuts) aC->setDefaultShortcut(lutActions[it.first], it.second); - } +} // ************* // Misc settings @@ -2131,6 +2143,34 @@ delete editPersonalDataDlg; } +void KMyMoneyApp::slotFileViewDocumentSettings() { + if (!d->m_storageInfo.isOpened) { + KMessageBox::information(this, i18n("No KMyMoneyFile open")); + return; + } + + KMSTATUS(i18n("Viewing document storage settings...")); + + MyMoneyFile* file = MyMoneyFile::instance(); + + QPointer settingsdlg = new KMyMoneyDocStorageSettingsDlg(file->storeDocumentCopies(), file->documentStorageLocation(), this, i18n("Edit Document Storage Settings")); + + if (settingsdlg->exec() == QDialog::Accepted && settingsdlg != 0) { + MyMoneyFileTransaction ft; + try { + file->storage()->setDirty(); + file->setStoreDocumentCopies(settingsdlg->storeCopies()); + file->setDocumentStorageLocation(settingsdlg->documentStoragePath()); + ft.commit(); + d->fileAction(eKMyMoney::FileAction::Changed); + } catch (const MyMoneyException &e) { + KMessageBox::information(this, i18n("Unable to save document storage settings: %1", QString::fromLatin1(e.what()))); + } + } + delete settingsdlg; +} + + void KMyMoneyApp::slotLoadAccountTemplates() { KMSTATUS(i18n("Importing account templates.")); @@ -2829,7 +2869,7 @@ { const QVector actions { - Action::FilePersonalData, Action::FileInformation, Action::FileImportTemplate, Action::FileExportTemplate, + Action::FilePersonalData, Action::FileDocumentSettings, Action::FileInformation, Action::FileImportTemplate, Action::FileExportTemplate, #ifdef KMM_DEBUG Action::FileDump, #endif @@ -3408,6 +3448,12 @@ for (auto &tmpl : wizard.templates()) tmpl.importTemplate(progressCallback); + // set document storage settings + auto storeDocumentCopies = wizard.storeDocumentCopies(); + file->setStoreDocumentCopies(storeDocumentCopies); + if (storeDocumentCopies) + file->setDocumentStorageLocation(wizard.documentStoragePath()); + ft.commit(); KMyMoneySettings::setFirstTimeRun(false); diff --git a/kmymoney/kmymoneyui.rc b/kmymoney/kmymoneyui.rc --- a/kmymoney/kmymoneyui.rc +++ b/kmymoney/kmymoneyui.rc @@ -1,5 +1,5 @@ - + @@ -20,6 +20,7 @@ + @@ -89,6 +90,8 @@ + + @@ -227,6 +230,12 @@ + + Document options + + + + diff --git a/kmymoney/kmymoneyutils.h b/kmymoney/kmymoneyutils.h --- a/kmymoney/kmymoneyutils.h +++ b/kmymoney/kmymoneyutils.h @@ -9,6 +9,7 @@ John C Thomas Baumgart Kevin Tambascio + Marc Hübner ***************************************************************************/ /*************************************************************************** @@ -374,15 +375,31 @@ static bool newPayee(const QString& newnameBase, QString& id); static void newTag(const QString& newnameBase, QString& id); - + /** * Creates a new institution entry in the MyMoneyFile engine * * @param institution MyMoneyInstitution object containing the data of * the institution to be created. */ static void newInstitution(MyMoneyInstitution& institution); + /** + * Creates a new document type entry in the MyMoneyFile engine + * + * @param institution MyMoneyDocumentType object containing the data of + * the document type to be created. + */ + static void newDocumentType(const QString& newnameBase, QString& id); + + /** + * Creates a new document entry in the MyMoneyFile engine + * + * @param institution MyMoneyDocument object containing the data of + * the document to be created. + */ + static void newDocument(const QString& newnameBase, QString& id); + static QDebug debug(); static MyMoneyForecast forecast(); diff --git a/kmymoney/kmymoneyutils.cpp b/kmymoney/kmymoneyutils.cpp --- a/kmymoney/kmymoneyutils.cpp +++ b/kmymoney/kmymoneyutils.cpp @@ -10,21 +10,22 @@ Thomas Baumgart Kevin Tambascio (C) 2017 by Łukasz Wojniłowicz + (C) 2018 by Marc Hübner ***************************************************************************/ -/*************************************************************************** - * * - * This program is free software; you can redistribute it and/or modify * - * it under the terms of the GNU General Public License as published by * - * the Free Software Foundation; either version 2 of the License, or * - * (at your option) any later version. * - * * - ***************************************************************************/ + /*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ #include "kmymoneyutils.h" -// ---------------------------------------------------------------------------- -// QT Includes + // ---------------------------------------------------------------------------- + // QT Includes #include #include @@ -73,6 +74,8 @@ #include "storageenums.h" #include "mymoneyenums.h" #include "kmymoneyplugin.h" +#include "mymoneydocumenttype.h" +#include "mymoneydocument.h" using namespace Icons; @@ -107,19 +110,19 @@ KGuiItem KMyMoneyUtils::scheduleNewGuiItem() { KGuiItem splitGuiItem(i18n("&New Schedule..."), - Icons::get(Icon::DocumentNew), - i18n("Create a new schedule."), - i18n("Use this to create a new schedule.")); + Icons::get(Icon::DocumentNew), + i18n("Create a new schedule."), + i18n("Use this to create a new schedule.")); return splitGuiItem; } KGuiItem KMyMoneyUtils::accountsFilterGuiItem() { KGuiItem splitGuiItem(i18n("&Filter"), - Icons::get(Icon::ViewFilter), - i18n("Filter out accounts"), - i18n("Use this to filter out accounts")); + Icons::get(Icon::ViewFilter), + i18n("Filter out accounts"), + i18n("Use this to filter out accounts")); return splitGuiItem; } @@ -207,9 +210,9 @@ QString css; css += "\n"; return css; @@ -242,15 +245,15 @@ // search the given resource if (rc.isEmpty()) { - mask = filename.arg("_%1"); - rc = QStandardPaths::locate(type, mask.arg(language)); + mask = filename.arg("_%1"); + rc = QStandardPaths::locate(type, mask.arg(language)); } if (rc.isEmpty()) { - // qDebug(QString("html/home_%1.html not found").arg(country).toLatin1()); - rc = QStandardPaths::locate(type, mask.arg(country)); + // qDebug(QString("html/home_%1.html not found").arg(country).toLatin1()); + rc = QStandardPaths::locate(type, mask.arg(country)); } if (rc.isEmpty()) { - rc = QStandardPaths::locate(type, filename.arg("")); + rc = QStandardPaths::locate(type, filename.arg("")); } } else { rc = QStandardPaths::locate(type, filename); @@ -308,9 +311,9 @@ a = MyMoneyFile::instance()->account(ida); b = MyMoneyFile::instance()->account(idb); if ((a.accountGroup() == eMyMoney::Account::Type::Asset - || a.accountGroup() == eMyMoney::Account::Type::Liability) - && (b.accountGroup() == eMyMoney::Account::Type::Asset - || b.accountGroup() == eMyMoney::Account::Type::Liability)) + || a.accountGroup() == eMyMoney::Account::Type::Liability) + && (b.accountGroup() == eMyMoney::Account::Type::Asset + || b.accountGroup() == eMyMoney::Account::Type::Liability)) return Transfer; return Normal; } @@ -425,38 +428,38 @@ QString txt; if (text) { switch (flag) { - case eMyMoney::Split::State::NotReconciled: - txt = i18nc("Reconciliation state 'Not reconciled'", "Not reconciled"); - break; - case eMyMoney::Split::State::Cleared: - txt = i18nc("Reconciliation state 'Cleared'", "Cleared"); - break; - case eMyMoney::Split::State::Reconciled: - txt = i18nc("Reconciliation state 'Reconciled'", "Reconciled"); - break; - case eMyMoney::Split::State::Frozen: - txt = i18nc("Reconciliation state 'Frozen'", "Frozen"); - break; - default: - txt = i18nc("Unknown reconciliation state", "Unknown"); - break; + case eMyMoney::Split::State::NotReconciled: + txt = i18nc("Reconciliation state 'Not reconciled'", "Not reconciled"); + break; + case eMyMoney::Split::State::Cleared: + txt = i18nc("Reconciliation state 'Cleared'", "Cleared"); + break; + case eMyMoney::Split::State::Reconciled: + txt = i18nc("Reconciliation state 'Reconciled'", "Reconciled"); + break; + case eMyMoney::Split::State::Frozen: + txt = i18nc("Reconciliation state 'Frozen'", "Frozen"); + break; + default: + txt = i18nc("Unknown reconciliation state", "Unknown"); + break; } } else { switch (flag) { - case eMyMoney::Split::State::NotReconciled: - break; - case eMyMoney::Split::State::Cleared: - txt = i18nc("Reconciliation flag C", "C"); - break; - case eMyMoney::Split::State::Reconciled: - txt = i18nc("Reconciliation flag R", "R"); - break; - case eMyMoney::Split::State::Frozen: - txt = i18nc("Reconciliation flag F", "F"); - break; - default: - txt = i18nc("Flag for unknown reconciliation state", "?"); - break; + case eMyMoney::Split::State::NotReconciled: + break; + case eMyMoney::Split::State::Cleared: + txt = i18nc("Reconciliation flag C", "C"); + break; + case eMyMoney::Split::State::Reconciled: + txt = i18nc("Reconciliation flag R", "R"); + break; + case eMyMoney::Split::State::Frozen: + txt = i18nc("Reconciliation flag F", "F"); + break; + default: + txt = i18nc("Flag for unknown reconciliation state", "?"); + break; } } return txt; @@ -583,9 +586,9 @@ return; MyMoneyPrice price(security, - currency, - stPrice.m_date, - stPrice.m_amount, stPrice.m_sourceName.isEmpty() ? i18n("Prices Importer") : stPrice.m_sourceName); + currency, + stPrice.m_date, + stPrice.m_amount, stPrice.m_sourceName.isEmpty() ? i18n("Prices Importer") : stPrice.m_sourceName); file->addPrice(price); } } @@ -643,31 +646,31 @@ bool KMyMoneyUtils::fileExists(const QUrl &url) { - bool fileExists = false; - if (url.isValid()) { - short int detailLevel = 0; // Lowest level: file/dir/symlink/none - KIO::StatJob* statjob = KIO::stat(url, KIO::StatJob::SourceSide, detailLevel); - bool noerror = statjob->exec(); - if (noerror) { - // We want a file - fileExists = !statjob->statResult().isDir(); - } - statjob->kill(); + bool fileExists = false; + if (url.isValid()) { + short int detailLevel = 0; // Lowest level: file/dir/symlink/none + KIO::StatJob* statjob = KIO::stat(url, KIO::StatJob::SourceSide, detailLevel); + bool noerror = statjob->exec(); + if (noerror) { + // We want a file + fileExists = !statjob->statResult().isDir(); } - return fileExists; + statjob->kill(); + } + return fileExists; } QString KMyMoneyUtils::downloadFile(const QUrl &url) { QString filename; KIO::StoredTransferJob *transferjob = KIO::storedGet (url); -// KJobWidgets::setWindow(transferjob, this); + // 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; + KMessageBox::detailedError(nullptr, + i18n("Error while loading file '%1'.", url.url()), + transferjob->errorString(), + i18n("File access error")); + return filename; } QTemporaryFile file; @@ -841,11 +844,97 @@ void KMyMoneyUtils::showStatementImportResult(const QStringList& resultMessages, uint statementCount) { KMessageBox::informationList(nullptr, - i18np("One statement has been processed with the following results:", - "%1 statements have been processed with the following results:", - statementCount), - !resultMessages.isEmpty() ? - resultMessages : - QStringList { i18np("No new transaction has been imported.", "No new transactions have been imported.", statementCount) }, - i18n("Statement import statistics")); + i18np("One statement has been processed with the following results:", + "%1 statements have been processed with the following results:", + statementCount), + !resultMessages.isEmpty() ? + resultMessages : + QStringList { i18np("No new transaction has been imported.", "No new transactions have been imported.", statementCount) }, + i18n("Statement import statistics")); +} + +void KMyMoneyUtils::newDocument(const QString& newnameBase, QString& id) +{ + bool doit = true; + + if (newnameBase != i18n("New Document")) { + // Ask the user if that is what he intended to do? + const auto msg = i18n("Do you want to add %1 as document?", newnameBase); + + if (KMessageBox::questionYesNo(nullptr, msg, i18n("New document"), KStandardGuiItem::yes(), KStandardGuiItem::no(), "NewDocument") == KMessageBox::No) { + doit = false; + KSharedConfigPtr kconfig = KSharedConfig::openConfig(); + if (kconfig) { + kconfig->group(QLatin1String("Notification Messages")).deleteEntry(QLatin1String("NewDocument")); + } + } + } + + if (doit) { + MyMoneyFileTransaction ft; + try { + QString newname(newnameBase); + // adjust name until a unique name has been created + int count = 0; + for (;;) { + try { + MyMoneyFile::instance()->documentByName(newname); + newname = QString::fromLatin1("%1 [%2]").arg(newnameBase).arg(++count); + } catch (const MyMoneyException &e) { + break; + } + } + + MyMoneyDocument doc; + doc.setName(newname); + MyMoneyFile::instance()->addDocument(doc); + id = doc.id(); + ft.commit(); + } catch (const MyMoneyException &e) { + KMessageBox::detailedSorry(nullptr, i18n("Unable to add document"), QString::fromLatin1(e.what())); + } + } +} + +void KMyMoneyUtils::newDocumentType(const QString& newnameBase, QString& id) +{ + bool doit = true; + + if (newnameBase != i18n("New Document type")) { + // Ask the user if that is what he intended to do? + const auto msg = i18n("Do you want to add %1 as document type?", newnameBase); + + if (KMessageBox::questionYesNo(nullptr, msg, i18n("New document type"), KStandardGuiItem::yes(), KStandardGuiItem::no(), "NewDocumentType") == KMessageBox::No) { + doit = false; + KSharedConfigPtr kconfig = KSharedConfig::openConfig(); + if (kconfig) { + kconfig->group(QLatin1String("Notification Messages")).deleteEntry(QLatin1String("NewDocumentType")); + } + } + } + + if (doit) { + MyMoneyFileTransaction ft; + try { + QString newname(newnameBase); + // adjust name until a unique name has been created + int count = 0; + for (;;) { + try { + MyMoneyFile::instance()->documentTypeByName(newname); + newname = QString::fromLatin1("%1 [%2]").arg(newnameBase).arg(++count); + } catch (const MyMoneyException &e) { + break; + } + } + + MyMoneyDocumentType doctype; + doctype.setName(newname); + MyMoneyFile::instance()->addDocumentType(doctype); + id = doctype.id(); + ft.commit(); + } catch (const MyMoneyException &e) { + KMessageBox::detailedSorry(nullptr, i18n("Unable to add document type"), QString::fromLatin1(e.what())); + } + } } diff --git a/kmymoney/menus/menuenums.h b/kmymoney/menus/menuenums.h --- a/kmymoney/menus/menuenums.h +++ b/kmymoney/menus/menuenums.h @@ -2,6 +2,7 @@ menuenums.h ------------------- copyright : (C) 2017, 2018 by Łukasz Wojniłowicz + 2018 by Marc Hübner ***************************************************************************/ @@ -37,6 +38,7 @@ FileDump, #endif FilePersonalData, FileInformation, + FileDocumentSettings, // ************* // The edit menu // ************* @@ -81,6 +83,7 @@ GoToAccount, GoToPayee, NewScheduledTransaction, AssignTransactionsNumber, CombineTransactions, + AssignDocuments, // ************* // The tools menu // ************* @@ -113,6 +116,10 @@ // The tag menu // ************* NewTag, RenameTag, DeleteTag, + // ************* + // The document menu + // ************* + NewDocument, RenameDocument, DeleteDocument, // ************* // The budget menu // ************* @@ -125,7 +132,7 @@ ShowSchedulesView, ShowCategoriesView, ShowTagsView, ShowPayeesView, ShowLedgersView, ShowInvestmentsView, ShowReportsView, ShowBudgetView, ShowForecastView, - ShowOnlineJobOutboxView, + ShowOnlineJobOutboxView, ShowDocumentsView, // ************* // The misc actions // ************* @@ -144,6 +151,7 @@ Schedule, Category, Tag, + Document, Payee, Investment, Transaction, diff --git a/kmymoney/mymoney/CMakeLists.txt b/kmymoney/mymoney/CMakeLists.txt --- a/kmymoney/mymoney/CMakeLists.txt +++ b/kmymoney/mymoney/CMakeLists.txt @@ -26,6 +26,8 @@ onlinejobfolder.cpp mymoneycontact.cpp payeeidentifiermodel.cpp + mymoneydocumenttype.cpp + mymoneydocument.cpp ) # storage_a_SOURCES cannot be set in storage directory on MS Windows @@ -74,6 +76,8 @@ onlinejobtyped.h onlinejobmessage.h onlinejobfolder.h + mymoneydocumenttype.h + mymoneydocument.h ) add_library(kmm_mymoney SHARED ${kmm_mymoney_LIB_SRCS}) diff --git a/kmymoney/mymoney/mymoneydocument.h b/kmymoney/mymoney/mymoneydocument.h new file mode 100644 --- /dev/null +++ b/kmymoney/mymoney/mymoneydocument.h @@ -0,0 +1,107 @@ +/* + * Copyright 2018 Marc Hübner + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef MYMONEYDOCUMENT_H +#define MYMONEYDOCUMENT_H + + // ---------------------------------------------------------------------------- + // QT Includes +#include +#include +#include + +// ---------------------------------------------------------------------------- +// Project Includes +#include "kmm_mymoney_export.h" +#include "mymoneyobject.h" + +class QStringList; +class MyMoneyDocumentPrivate; + +/** + * This class represents a Document within the MyMoney engine. + */ +class KMM_MYMONEY_EXPORT MyMoneyDocument : public MyMoneyObject +{ + Q_DECLARE_PRIVATE(MyMoneyDocument) + KMM_MYMONEY_UNIT_TESTABLE + +public: + MyMoneyDocument(); + explicit MyMoneyDocument(const QString& name); + explicit MyMoneyDocument(const QString& name, const QDate& documentDate, const QString& documentTypeId, const QString& hash, const QString& description, const QString& storagepath); + + MyMoneyDocument(const QString& id, const MyMoneyDocument& document); + MyMoneyDocument(const MyMoneyDocument& other); + MyMoneyDocument(MyMoneyDocument&& other); + MyMoneyDocument & operator=(MyMoneyDocument other); + friend void swap(MyMoneyDocument& first, MyMoneyDocument& second); + ~MyMoneyDocument(); + + QString name() const; + QDate documentDate() const; + QString documentTypeId() const; + QString description() const; + QString storagePath() const; + QString hash() const; + + void setName(const QString& val); + void setDocumentDate(const QDate& val); + void setDocumentTypeId(const QString& val); + void setHash(const QString& val); + void setDescription(const QString& val); + void setStoragePath(const QString& val); + + bool operator == (const MyMoneyDocument &) const; + bool operator <(const MyMoneyDocument& right) const; + + /** + * This method checks if a reference to the given object exists. It returns, + * a @p true if the object is referencing the one requested by the + * parameter @p id. If it does not, this method returns @p false. + * + * @param id id of the object to be checked for references + * @retval true This object references object with id @p id. + * @retval false This object does not reference the object with id @p id. + */ + virtual bool hasReferenceTo(const QString& id) const override; + + static MyMoneyDocument null; +}; +inline void swap(MyMoneyDocument& first, MyMoneyDocument& second) // krazy:exclude=inline +{ + using std::swap; + swap(first.d_ptr, second.d_ptr); +} + +inline MyMoneyDocument::MyMoneyDocument(MyMoneyDocument && other) : MyMoneyDocument() // krazy:exclude=inline +{ + swap(*this, other); +} + +inline MyMoneyDocument & MyMoneyDocument::operator=(MyMoneyDocument other) // krazy:exclude=inline +{ + swap(*this, other); + return *this; +} + +/** + * Make it possible to hold @ref MyMoneyDocument objects inside @ref QVariant objects. + */ +Q_DECLARE_METATYPE(MyMoneyDocument) + +#endif diff --git a/kmymoney/mymoney/mymoneydocument.cpp b/kmymoney/mymoney/mymoneydocument.cpp new file mode 100644 --- /dev/null +++ b/kmymoney/mymoney/mymoneydocument.cpp @@ -0,0 +1,165 @@ +/* + * Copyright 2018 Marc Hübner + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + + // ---------------------------------------------------------------------------- + // QT Includes + +#include +// ---------------------------------------------------------------------------- +// Project Includes + +#include "mymoneyutils.h" +#include "mymoneyexception.h" +#include "mymoneydocument.h" +#include "mymoneydocument_p.h" + +MyMoneyDocument MyMoneyDocument::null; + +MyMoneyDocument::MyMoneyDocument() : + MyMoneyObject(*new MyMoneyDocumentPrivate) +{ + Q_D(MyMoneyDocument); + d->m_documentDate = QDate::currentDate(); +} + +MyMoneyDocument::MyMoneyDocument(const QString& id) : + MyMoneyObject(*new MyMoneyDocumentPrivate, id) +{ + Q_D(MyMoneyDocument); + d->m_documentDate = QDate::currentDate(); +} + +MyMoneyDocument::MyMoneyDocument(const QString& name, const QDate& documentDate, + const QString& documentTypeId, const QString& hash, + const QString& description, const QString& storagepath) : + MyMoneyObject(*new MyMoneyDocumentPrivate) +{ + Q_D(MyMoneyDocument); + d->m_name = name; + d->m_documentDate = documentDate; + d->m_documentTypeId = documentTypeId; + d->m_hash = hash; + d->m_description = description; + d->m_storagePath = storagepath; +} + +MyMoneyDocument::MyMoneyDocument(const MyMoneyDocument& other) : + MyMoneyObject(*new MyMoneyDocumentPrivate(*other.d_func()), other.id()) +{ +} + +MyMoneyDocument::MyMoneyDocument(const QString& id, const MyMoneyDocument& other) : + MyMoneyObject(*new MyMoneyDocumentPrivate(*other.d_func()), id) +{ +} + +MyMoneyDocument::~MyMoneyDocument() +{ +} + +bool MyMoneyDocument::operator == (const MyMoneyDocument& right) const +{ + Q_D(const MyMoneyDocument); + return (MyMoneyObject::operator==(right) && + ((d->m_id.length() == 0 && right.id().length() == 0) || (d->m_id == right.id())) && + (d->m_documentDate == right.documentDate()) && + ((d->m_documentTypeId.length() == 0 && right.documentTypeId().length() == 0) || (d->m_documentTypeId == right.documentTypeId())) && + ((d->m_hash.length() == 0 && right.hash().length() == 0) || (d->m_hash == right.hash())) && + ((d->m_description.length() == 0 && right.description().length() == 0) || (d->m_description == right.description())) && + ((d->m_storagePath.length() == 0 && right.storagePath().length() == 0) || (d->m_storagePath == right.storagePath()))); +} + +bool MyMoneyDocument::operator < (const MyMoneyDocument& right) const +{ + Q_D(const MyMoneyDocument); + return d->m_id < right.name(); +} + +bool MyMoneyDocument::hasReferenceTo(const QString& /*id*/) const +{ + return false; +} + +QString MyMoneyDocument::name() const +{ + Q_D(const MyMoneyDocument); + return d->m_name; +} + +QDate MyMoneyDocument::documentDate() const +{ + Q_D(const MyMoneyDocument); + return d->m_documentDate; +} + +QString MyMoneyDocument::documentTypeId() const +{ + Q_D(const MyMoneyDocument); + return d->m_documentTypeId; +} +QString MyMoneyDocument::hash() const +{ + Q_D(const MyMoneyDocument); + return d->m_hash; +} + +QString MyMoneyDocument::description() const +{ + Q_D(const MyMoneyDocument); + return d->m_description; +} + +QString MyMoneyDocument::storagePath() const +{ + Q_D(const MyMoneyDocument); + return d->m_storagePath; +} + +void MyMoneyDocument::setName(const QString& val) +{ + Q_D(MyMoneyDocument); + d->m_name = val; +} + +void MyMoneyDocument::setDocumentDate(const QDate& val) +{ + Q_D(MyMoneyDocument); + d->m_documentDate = val; +} + +void MyMoneyDocument::setDocumentTypeId(const QString& val) +{ + Q_D(MyMoneyDocument); + d->m_documentTypeId = val; +} +void MyMoneyDocument::setHash(const QString& val) +{ + Q_D(MyMoneyDocument); + d->m_hash = val; +} + +void MyMoneyDocument::setDescription(const QString& val) +{ + Q_D(MyMoneyDocument); + d->m_description = val; +} + +void MyMoneyDocument::setStoragePath(const QString& val) +{ + Q_D(MyMoneyDocument); + d->m_storagePath = val; +} \ No newline at end of file diff --git a/kmymoney/mymoney/mymoneydocument_p.h b/kmymoney/mymoney/mymoneydocument_p.h new file mode 100644 --- /dev/null +++ b/kmymoney/mymoney/mymoneydocument_p.h @@ -0,0 +1,62 @@ +/* + * Copyright 2018 Marc Hübner + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef MYMONEYDOCUMENT_P_H +#define MYMONEYDOCUMENT_P_H + + // ---------------------------------------------------------------------------- + // QT Includes +#include +#include +#include + +// ---------------------------------------------------------------------------- +// Project Includes +#include "mymoneyobject_p.h" + +namespace Document +{ + enum class Attribute { + Name = 0, + DocumentDate, + DocumentTypeId, + Checksum, + Description, + StoragePath, + // insert new entries above this line + LastAttribute + }; + uint qHash(const Attribute key, uint seed) { return ::qHash(static_cast(key), seed); } +} + +class MyMoneyDocumentPrivate : public MyMoneyObjectPrivate +{ +public: + + MyMoneyDocumentPrivate() : + m_name("") + { + } + + QString m_name; + QDate m_documentDate; + QString m_documentTypeId; + QString m_hash; + QString m_description; + QString m_storagePath; +}; +#endif //MYMONEYDOCUMENT_P_H \ No newline at end of file diff --git a/kmymoney/mymoney/mymoneydocumenttype.h b/kmymoney/mymoney/mymoneydocumenttype.h new file mode 100644 --- /dev/null +++ b/kmymoney/mymoney/mymoneydocumenttype.h @@ -0,0 +1,99 @@ +/* + * Copyright 2018 Marc Hübner + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef MYMONEYDOCUMENTTYPE_H +#define MYMONEYDOCUMENTTYPE_H + + // ---------------------------------------------------------------------------- + // QT Includes +#include + +// ---------------------------------------------------------------------------- +// Project Includes +#include "kmm_mymoney_export.h" +#include "mymoneyobject.h" + +class QStringList; +class QString; + +/** + * This class represents a Document Type within the MyMoney engine. + */ +class MyMoneyDocumentTypePrivate; +class KMM_MYMONEY_EXPORT MyMoneyDocumentType : public MyMoneyObject +{ + Q_DECLARE_PRIVATE(MyMoneyDocumentType) + KMM_MYMONEY_UNIT_TESTABLE + +public: + MyMoneyDocumentType(); + explicit MyMoneyDocumentType(const QString& name); + explicit MyMoneyDocumentType(const QString& name, const QString& description); + + MyMoneyDocumentType(const QString& id, const MyMoneyDocumentType& doctype); + MyMoneyDocumentType(const MyMoneyDocumentType& other); + MyMoneyDocumentType(MyMoneyDocumentType&& other); + MyMoneyDocumentType & operator=(MyMoneyDocumentType other); + friend void swap(MyMoneyDocumentType& first, MyMoneyDocumentType& second); + ~MyMoneyDocumentType(); + + const QString& name() const; + const QString& description() const; + + void setName(const QString& val); + void setDescription(const QString& val); + + bool operator == (const MyMoneyDocumentType &) const; + bool operator <(const MyMoneyDocumentType& right) const; + + /** + * This method checks if a reference to the given object exists. It returns, + * a @p true if the object is referencing the one requested by the + * parameter @p id. If it does not, this method returns @p false. + * + * @param id id of the object to be checked for references + * @retval true This object references object with id @p id. + * @retval false This object does not reference the object with id @p id. + */ + virtual bool hasReferenceTo(const QString& id) const; + + static MyMoneyDocumentType null; +}; + +inline void swap(MyMoneyDocumentType& first, MyMoneyDocumentType& second) // krazy:exclude=inline +{ + using std::swap; + swap(first.d_ptr, second.d_ptr); +} + +inline MyMoneyDocumentType::MyMoneyDocumentType(MyMoneyDocumentType && other) : MyMoneyDocumentType() // krazy:exclude=inline +{ + swap(*this, other); +} + +inline MyMoneyDocumentType & MyMoneyDocumentType::operator=(MyMoneyDocumentType other) // krazy:exclude=inline +{ + swap(*this, other); + return *this; +} + +/** + * Make it possible to hold @ref MyMoneyDocumentType objects inside @ref QVariant objects. + */ +Q_DECLARE_METATYPE(MyMoneyDocumentType) + +#endif diff --git a/kmymoney/mymoney/mymoneydocumenttype.cpp b/kmymoney/mymoney/mymoneydocumenttype.cpp new file mode 100644 --- /dev/null +++ b/kmymoney/mymoney/mymoneydocumenttype.cpp @@ -0,0 +1,105 @@ +/* + * Copyright 2018 Marc Hübner + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + + // ---------------------------------------------------------------------------- + // QT Includes +#include +#include + +// ---------------------------------------------------------------------------- +// Project Includes +#include "mymoneyutils.h" +#include "mymoneyexception.h" +#include "mymoneydocumenttype.h" +#include "mymoneydocumenttype_p.h" + +MyMoneyDocumentType MyMoneyDocumentType::null; + +MyMoneyDocumentType::MyMoneyDocumentType() : + MyMoneyObject(*new MyMoneyDocumentTypePrivate) +{ +} + +MyMoneyDocumentType::MyMoneyDocumentType(const QString &id) : + MyMoneyObject(*new MyMoneyDocumentTypePrivate, id) +{ +} + +MyMoneyDocumentType::MyMoneyDocumentType(const QString& name, const QString& description) : + MyMoneyObject(*new MyMoneyDocumentTypePrivate) +{ + Q_D(MyMoneyDocumentType); + d->m_name = name; + d->m_description = description; +} + +MyMoneyDocumentType::MyMoneyDocumentType(const MyMoneyDocumentType& other) : + MyMoneyObject(*new MyMoneyDocumentTypePrivate(*other.d_func()), other.id()) +{ +} + +MyMoneyDocumentType::MyMoneyDocumentType(const QString& id, const MyMoneyDocumentType& other) : + MyMoneyObject(*new MyMoneyDocumentTypePrivate(*other.d_func()), id) +{ +} + +MyMoneyDocumentType::~MyMoneyDocumentType() +{ +} + +bool MyMoneyDocumentType::operator < (const MyMoneyDocumentType& right) const +{ + Q_D(const MyMoneyDocumentType); + return d->m_name < right.name(); +} + +bool MyMoneyDocumentType::hasReferenceTo(const QString& /*id*/) const +{ + return false; +} + +bool MyMoneyDocumentType::operator == (const MyMoneyDocumentType& right) const +{ + Q_D(const MyMoneyDocumentType); + auto d2 = static_cast(right.d_func()); + return (MyMoneyObject::operator==(right) && + ((d->m_name.length() == 0 && d2->m_name.length() == 0) || (d->m_name == d2->m_name)) && + ((d->m_description.length() == 0 && d2->m_description.length() == 0) || (d->m_description == d2->m_description))); +} + +const QString& MyMoneyDocumentType::name() const +{ + Q_D(const MyMoneyDocumentType); + return d->m_name; +} + +const QString& MyMoneyDocumentType::description() const +{ + Q_D(const MyMoneyDocumentType); + return d->m_description; +} + +void MyMoneyDocumentType::setName(const QString& val) +{ + Q_D(MyMoneyDocumentType); + d->m_name = val; +} +void MyMoneyDocumentType::setDescription(const QString& val) +{ + Q_D(MyMoneyDocumentType); + d->m_description = val; +}; \ No newline at end of file diff --git a/kmymoney/mymoney/storage/storageenums.h b/kmymoney/mymoney/mymoneydocumenttype_p.h copy from kmymoney/mymoney/storage/storageenums.h copy to kmymoney/mymoney/mymoneydocumenttype_p.h --- a/kmymoney/mymoney/storage/storageenums.h +++ b/kmymoney/mymoney/mymoneydocumenttype_p.h @@ -1,5 +1,5 @@ /* - * Copyright 2017 Łukasz Wojniłowicz + * Copyright 2018 Marc Hübner * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -15,28 +15,30 @@ * along with this program. If not, see . */ -#ifndef STORAGEENUMS_H -#define STORAGEENUMS_H - -#include - -namespace eStorage { - enum class Reference { - Account = 0, - Institution, - Payee, - Transaction, - Report, - Budget, - Schedule, - Security, - Currency, - Price, - Tag, - // insert new entries above this line - Count - }; - - inline uint qHash(const Reference key, uint seed) { return ::qHash(static_cast(key), seed); } -} -#endif +#ifndef MYMONEYDOCUMENTTYPE_P_H +#define MYMONEYDOCUMENTTYPE_P_H + +// ---------------------------------------------------------------------------- +// QT Includes + +#include +#include + +// ---------------------------------------------------------------------------- +// Project Includes + +#include "mymoneyobject_p.h" + +class MyMoneyDocumentTypePrivate : public MyMoneyObjectPrivate +{ +public: + + MyMoneyDocumentTypePrivate() : + m_name("") + { + } + + QString m_name; + QString m_description; +}; +#endif // MYMONEYDOCUMENTTYPE_P_H \ No newline at end of file diff --git a/kmymoney/mymoney/mymoneyenums.h b/kmymoney/mymoney/mymoneyenums.h --- a/kmymoney/mymoney/mymoneyenums.h +++ b/kmymoney/mymoney/mymoneyenums.h @@ -328,7 +328,9 @@ Schedule, Security, OnlineJob, - CostCenter + CostCenter, + DocumentType, + Document }; /** diff --git a/kmymoney/mymoney/mymoneyfile.h b/kmymoney/mymoney/mymoneyfile.h --- a/kmymoney/mymoney/mymoneyfile.h +++ b/kmymoney/mymoney/mymoneyfile.h @@ -6,6 +6,7 @@ * Copyright 2006-2018 Thomas Baumgart * Copyright 2006 Darren Gould * Copyright 2017-2018 Łukasz Wojniłowicz + * Copyright 2018 Marc Hübner * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -105,6 +106,16 @@ * the currency selected by the user as base currency. If a currency * reference is empty, it will usually be interpreted as baseCurrency(). * + * The methods addDocumentType(), modifyDocumentType() and removeDocumentType() + * implement the general document type maintenance functions. + * documentType() and documentTypeList() are used to retrieve a single instance + * or a QList of MyMoneyDocumentType objects + * + * The methods addDocument(), modifyDocument() and removeDocument() + * implement the general document maintenenace functions. + * doument() and documentList() are used to retrieve a single instance + * or a QList of MyMoneyDocument objects. + * * The methods liability(), asset(), expense(), income() and equity() are * used to retrieve the five standard accounts. isStandardAccount() * checks if a given accountId references one of the or not. @@ -146,6 +157,8 @@ class MyMoneyObject; class MyMoneyTransaction; class MyMoneyTransactionFilter; +class MyMoneyDocumentType; +class MyMoneyDocument; class onlineJob; namespace eMyMoney { namespace Account { enum class Type; } @@ -919,6 +932,115 @@ */ QList tagList() const; + /** + * This method is used to create a new document type + * An exception will be thrown upon error conditions + * + * @param doctype MyMoneyDocumentType reference to document type information + */ + void addDocumentType(MyMoneyDocumentType& doctype); + + /** + * This method is used to retrieve information about a document type + * An exception will be thrown upon error conditions. + * + * @param id QString reference to id of document type + * + * @return MyMoneyDocumentType object of document type + */ + const MyMoneyDocumentType& documentType(const QString& id) const; + + /** + * This method is used to retrieve the id to a corresponding + * name of a document type. + * An exception will be thrown upon error conditions. + * + * @param doctype QString reference to name of document type + * + * @return MyMoneyDocumentType object of document type + */ + const MyMoneyDocumentType& documentTypeByName(const QString& doctype) const; + + /** + * This method is used to modify an existing document type + * An exception will be thrown upon error conditions + * + * @param doctype MyMoneyDocumentType reference to document type information + */ + void modifyDocumentType(const MyMoneyDocumentType& doctype); + + /** + * This method is used to remove an existing document type. + * An error condition occurs, if the document type is still referenced. + * An exception will be thrown upon error conditions + * + * @param doctype MyMoneyDocumentType reference to document type information + */ + void removeDocumentType(const MyMoneyDocumentType& doctype); + + /** + * This method returns a list of the document types + * inside a MyMoneyStorage object + * + * @return QList containing the document type information + */ + const QList documentTypeList() const; + + /** + * This method is used to create a new document + * An exception will be thrown upon error conditions + * + * @param document MyMoneyDocument reference to document information + */ + void addDocument(MyMoneyDocument& document); + + /** + * This method is used to retrieve information about a document + * An exception will be thrown upon error conditions. + * + * @param id QString reference to id of document + * + * @return MyMoneyDocument object of document + */ + const MyMoneyDocument document(const QString& id) const; + + /** + * This method is used to retrieve the id to a corresponding + * name of a document. + * An exception will be thrown upon error conditions. + * + * @param name QString reference to name of document + * + * @return MyMoneyDocument object of document + */ + const MyMoneyDocument documentByName(const QString& name) const; + + /** + * This method is used to modify an existing document + * An exception will be thrown upon error conditions + * + * @param document MyMoneyDocument reference to document information + */ + void modifyDocument(const MyMoneyDocument& document); + + /** + * This method is used to remove an existing document. + * An error condition occurs, if the document is still referenced. + * An exception will be thrown upon error conditions + * + * @param document MyMoneyDocument reference to document information + */ + void removeDocument(const MyMoneyDocument& document); + + /** + * This method returns a list of the documents + * inside a MyMoneyStorage object + * + * @return QList containing the document information + */ + const QList documentList() const; + + /** * This method returns a list of the cost centers * inside a MyMoneyStorage object @@ -1579,6 +1701,27 @@ */ void removeOnlineJob(const QStringList onlineJobIds); + /** + * Returns true if copies of documents assigned to splits in this file are supposed to be stored, false otherwise + */ + bool storeDocumentCopies() const; + + /** + * This method sets the document storage mode. Documents that are assigned to splits can either be copied to a kmymoney-specific location - or references to existing documents can be stored. + * @param storecopies true if copies are to be stored, false if references are to be stored + */ + void setStoreDocumentCopies(bool storecopies); + + /** + * This method returns the location kmymoney stores the documents assigned to splits in if copies of documents are supposed to be stored; empty QString otherwise. + */ + QString documentStorageLocation() const; + + /** + * This method sets the storage location for documents that are assigned to splits. + * @param path The path to store the documents in. + */ + void setDocumentStorageLocation(QString path); protected: /** * This is the constructor for a new empty file description diff --git a/kmymoney/mymoney/mymoneyfile.cpp b/kmymoney/mymoney/mymoneyfile.cpp --- a/kmymoney/mymoney/mymoneyfile.cpp +++ b/kmymoney/mymoney/mymoneyfile.cpp @@ -6,6 +6,7 @@ * Copyright 2006-2018 Thomas Baumgart * Copyright 2006 Darren Gould * Copyright 2017-2018 Łukasz Wojniłowicz + * Copyright 2018 Marc Hübner * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -62,7 +63,8 @@ #include "onlinejob.h" #include "storageenums.h" #include "mymoneyenums.h" - +#include "mymoneydocumenttype.h" +#include "mymoneydocument.h" // include the following line to get a 'cout' for debug purposes // #include @@ -127,6 +129,18 @@ m_id(job.id()) { } + MyMoneyNotification(File::Mode mode, const MyMoneyDocumentType& doctype) : + m_objType(File::Object::DocumentType), + m_notificationMode(mode), + m_id(doctype.id()) { + } + + MyMoneyNotification(File::Mode mode, const MyMoneyDocument& document) : + m_objType(File::Object::Document), + m_notificationMode(mode), + m_id(document.id()) { + } + File::Object objectType() const { return m_objType; } @@ -1351,6 +1365,76 @@ d->m_changeSet += MyMoneyNotification(File::Mode::Remove, tag); } +void MyMoneyFile::addDocumentType(MyMoneyDocumentType& doctype) +{ + d->checkTransaction(Q_FUNC_INFO); + d->m_storage->addDocumentType(doctype); + d->m_changeSet += MyMoneyNotification(File::Mode::Add, doctype); +} + +const MyMoneyDocumentType& MyMoneyFile::documentType(const QString& id) const +{ + d->checkStorage(); + return d->m_storage->documentType(id); +} + +const MyMoneyDocumentType& MyMoneyFile::documentTypeByName(const QString& name) const +{ + d->checkStorage(); + return d->m_storage->documentTypeByName(name); +} + +void MyMoneyFile::modifyDocumentType(const MyMoneyDocumentType& doctype) +{ + d->checkTransaction(Q_FUNC_INFO); + d->m_storage->modifyDocumentType(doctype); + d->m_changeSet += MyMoneyNotification(File::Mode::Modify, doctype); +} + +void MyMoneyFile::removeDocumentType(const MyMoneyDocumentType& doctype) +{ + d->checkTransaction(Q_FUNC_INFO); + for (const MyMoneyDocument& doc: d->m_storage->documentList()) { + if (doc.documentTypeId() == doctype.id()) + throw MYMONEYEXCEPTION("Cannot delete document type that still has documents assigned"); + } + + d->m_storage->removeDocumentType(doctype); + d->m_changeSet += MyMoneyNotification(File::Mode::Remove, doctype); +} + +void MyMoneyFile::addDocument(MyMoneyDocument& document) +{ + d->checkTransaction(Q_FUNC_INFO); + d->m_storage->addDocument(document); + d->m_changeSet += MyMoneyNotification(File::Mode::Add, document); +} + +const MyMoneyDocument MyMoneyFile::document(const QString& id) const +{ + return d->m_storage->document(id); +} + +const MyMoneyDocument MyMoneyFile::documentByName(const QString& name) const +{ + d->checkStorage(); + return d->m_storage->documentByName(name); +} + +void MyMoneyFile::modifyDocument(const MyMoneyDocument& document) +{ + d->checkTransaction(Q_FUNC_INFO); + d->m_storage->modifyDocument(document); + d->m_changeSet += MyMoneyNotification(File::Mode::Modify, document); +} + +void MyMoneyFile::removeDocument(const MyMoneyDocument& document) +{ + d->checkTransaction(Q_FUNC_INFO); + d->m_storage->removeDocument(document); + d->m_changeSet += MyMoneyNotification(File::Mode::Remove, document); +} + void MyMoneyFile::accountList(QList& list, const QStringList& idlist, const bool recursive) const { d->checkStorage(); @@ -1612,6 +1696,18 @@ return d->m_storage->tagList(); } +const QList MyMoneyFile::documentTypeList() const +{ + return d->m_storage->documentTypeList(); +} + +const QList MyMoneyFile::documentList() const +{ + QList list; + list = d->m_storage->documentList(); + return list; +} + QString MyMoneyFile::accountToCategory(const QString& accountId, bool includeStandardAccounts) const { MyMoneyAccount acc; @@ -3446,6 +3542,22 @@ } } +bool MyMoneyFile::storeDocumentCopies() const { + return this->value(QStringLiteral("kmm_storeDocumentCopies")) == QStringLiteral("true"); +} + +void MyMoneyFile::setStoreDocumentCopies(bool storecopies) { + this->setValue(QStringLiteral("kmm_storeDocumentCopies"), storecopies ? QStringLiteral("true") : QStringLiteral("false")); +} + +QString MyMoneyFile::documentStorageLocation() const { + return this->value(QStringLiteral("kmm_documentStorageLocation")); +} + +void MyMoneyFile::setDocumentStorageLocation(QString path) { + this->setValue(QStringLiteral("kmm_documentStorageLocation"), path); +} + class MyMoneyFileTransactionPrivate { Q_DISABLE_COPY(MyMoneyFileTransactionPrivate) diff --git a/kmymoney/mymoney/mymoneysplit.h b/kmymoney/mymoney/mymoneysplit.h --- a/kmymoney/mymoney/mymoneysplit.h +++ b/kmymoney/mymoney/mymoneysplit.h @@ -179,6 +179,9 @@ QString transactionId() const; void setTransactionId(const QString& id); + QList documentIdList() const; + void setDocumentIdList(const QList& docList); + /** * returns @a true if this its a transaction matched against an imported * transaction. The imported and matched transaction can be extracted diff --git a/kmymoney/mymoney/mymoneysplit.cpp b/kmymoney/mymoney/mymoneysplit.cpp --- a/kmymoney/mymoney/mymoneysplit.cpp +++ b/kmymoney/mymoney/mymoneysplit.cpp @@ -3,6 +3,7 @@ * Copyright 2004 Kevin Tambascio * Copyright 2005-2006 Ace Jones * Copyright 2017-2018 Łukasz Wojniłowicz + * Copyright 2018 Marc Hübner * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -80,7 +81,8 @@ d->m_shares == d2->m_shares && d->m_value == d2->m_value && d->m_price == d2->m_price && - d->m_transactionId == d2->m_transactionId; + d->m_transactionId == d2->m_transactionId && + d->m_documentList == d2->m_documentList; } MyMoneySplit MyMoneySplit::operator-() const @@ -353,6 +355,10 @@ for (int i = 0; i < d->m_tagList.size(); i++) if (id == d->m_tagList[i]) return true; + + if (d->m_documentList.contains(id)) + return true; + return rc || (id == d->m_account) || (id == d->m_payee) || (id == d->m_costCenter); } @@ -435,3 +441,16 @@ }; return actionNames[action]; } + +QList MyMoneySplit::documentIdList() const +{ + Q_D(const MyMoneySplit); + return d->m_documentList; +} + +void MyMoneySplit::setDocumentIdList(const QList& docList) +{ + Q_D(MyMoneySplit); + d->m_documentList = docList; +} + diff --git a/kmymoney/mymoney/mymoneysplit_p.h b/kmymoney/mymoney/mymoneysplit_p.h --- a/kmymoney/mymoney/mymoneysplit_p.h +++ b/kmymoney/mymoney/mymoneysplit_p.h @@ -62,6 +62,11 @@ */ QList m_tagList; + /* + * This member contains a list of the IDs of documents assigned to this split. + */ + QList m_documentList; + /** * This member contains the ID of the account */ diff --git a/kmymoney/mymoney/mymoneytransactionfilter.h b/kmymoney/mymoney/mymoneytransactionfilter.h --- a/kmymoney/mymoney/mymoneytransactionfilter.h +++ b/kmymoney/mymoney/mymoneytransactionfilter.h @@ -3,6 +3,7 @@ * Copyright 2004 Ace Jones * Copyright 2008-2010 Alvaro Soliverez * Copyright 2017-2018 Łukasz Wojniłowicz + * Copyright 2018 Marc Hübner * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -75,6 +76,7 @@ unsigned typeFilter : 1; unsigned stateFilter : 1; unsigned validityFilter : 1; + unsigned documentFilter : 1; } singleFilter; } FilterSet; @@ -129,6 +131,7 @@ * - Payee * - Tag * - Category + * - Document * - Shares / Value * - Number * @@ -220,6 +223,14 @@ */ void addState(const int state); + /** + * This method will add the document with id to the list of matching documents. + * If the list is empty, any transaction will match. + * + * @param id internal id of the document + */ + void addDocument(const QString& id); + /** * This method sets the number filter to match only transactions with * a number in the range specified by @p from and @p to. @@ -384,6 +395,17 @@ */ bool includesTag(const QString& tag) const; + /** + * This method is used to return information about the + * presence of a specific document in the account filter. + * The document in question is included in the filter set, + * if it has been set or no account filter is set. + * + * @param doc id of document in question + * @return true if document is in filter set, false otherwise + */ + bool includesDocument(const QString& doc) const; + /** * This method is used to return information about the * presence of a date filter. @@ -432,6 +454,15 @@ */ bool tags(QStringList& list) const; + /** + * This method returns whether a document filter has been set, + * and if so, it returns all the documents set in the filter. + * + * @param list list to append documents into + * @return return true if a document filter has been set + */ + bool documents(QStringList& list) const; + /** * This method returns whether an account filter has been set, * and if so, it returns all the accounts set in the filter. diff --git a/kmymoney/mymoney/mymoneytransactionfilter.cpp b/kmymoney/mymoney/mymoneytransactionfilter.cpp --- a/kmymoney/mymoney/mymoneytransactionfilter.cpp +++ b/kmymoney/mymoney/mymoneytransactionfilter.cpp @@ -3,6 +3,7 @@ * Copyright 2004 Ace Jones * Copyright 2008-2010 Alvaro Soliverez * Copyright 2017-2018 Łukasz Wojniłowicz + * Copyright 2018 Marc Hübner * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -41,6 +42,7 @@ #include "mymoneytransaction.h" #include "mymoneysplit.h" #include "mymoneyenums.h" +#include "mymoneydocument.h" class MyMoneyTransactionFilterPrivate { @@ -68,6 +70,7 @@ QHash m_payees; QHash m_tags; QHash m_categories; + QHash m_documents; QHash m_states; QHash m_types; QHash m_validity; @@ -110,6 +113,7 @@ d->m_categories.clear(); d->m_payees.clear(); d->m_tags.clear(); + d->m_documents.clear(); d->m_types.clear(); d->m_states.clear(); d->m_validity.clear(); @@ -218,6 +222,17 @@ d->m_tags.insert(id, QString()); } +void MyMoneyTransactionFilter::addDocument(const QString& id) +{ + Q_D(MyMoneyTransactionFilter); + if (!d->m_documents.isEmpty() && !id.isEmpty() && d->m_documents.contains(id)) + return; + + d->m_filterSet.singleFilter.documentFilter = 1; + if (!id.isEmpty()) + d->m_documents.insert(id, QString()); +} + void MyMoneyTransactionFilter::addType(const int type) { Q_D(MyMoneyTransactionFilter); @@ -436,6 +451,29 @@ continue; } + // check the document list + if (filter.documentFilter) { + const auto documents = s.documentIdList(); + if (!d->m_documents.isEmpty()) { + if (documents.isEmpty()) { + continue; + } + else { + auto found = false; + for (const auto& doc : documents) { + if (d->m_documents.contains(doc)) { + found = true; + break; + } + } + + if (!found) + continue; + } + } else if (!documents.isEmpty()) + continue; + } + // check the type list if (filter.typeFilter && !d->m_types.isEmpty() && @@ -647,6 +685,12 @@ return !d->m_filterSet.singleFilter.tagFilter || d->m_tags.contains(tag); } +bool MyMoneyTransactionFilter::includesDocument(const QString& doc) const +{ + Q_D(const MyMoneyTransactionFilter); + return (! d->m_filterSet.singleFilter.documentFilter) || d->m_documents.end() != d->m_documents.find(doc); +} + bool MyMoneyTransactionFilter::dateFilter(QDate& from, QDate& to) const { Q_D(const MyMoneyTransactionFilter); @@ -701,6 +745,21 @@ return result; } +bool MyMoneyTransactionFilter::documents(QStringList& list) const +{ + Q_D(const MyMoneyTransactionFilter); + bool result = d->m_filterSet.singleFilter.documentFilter; + + if (result) { + QHashIterator it_document(d->m_documents); + while (it_document.hasNext()) { + it_document.next(); + list += it_document.key(); + } + } + return result; +} + bool MyMoneyTransactionFilter::accounts(QStringList& list) const { Q_D(const MyMoneyTransactionFilter); @@ -978,5 +1037,8 @@ } else if (d->m_tags.end() != d->m_tags.find(id)) { qDebug("%s", qPrintable(QString("Remove tag '%1' from report").arg(id))); d->m_tags.remove(id); + } else if (d->m_documents.end() != d->m_documents.find(id)) { + qDebug("%s", qPrintable(QString("Remove document '%1' from report").arg(id))); + d->m_documents.remove(id); } } diff --git a/kmymoney/mymoney/storage/mymoneystoragedump.cpp b/kmymoney/mymoney/storage/mymoneystoragedump.cpp --- a/kmymoney/mymoney/storage/mymoneystoragedump.cpp +++ b/kmymoney/mymoney/storage/mymoneystoragedump.cpp @@ -1,6 +1,7 @@ /* * Copyright 2002-2018 Thomas Baumgart * Copyright 2004 Ace Jones + * Copyright 2018 Marc Hübner * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -52,6 +53,8 @@ #include "mymoneysplit.h" #include "mymoneytransaction.h" #include "mymoneyenums.h" +#include "mymoneydocumenttype.h" +#include "mymoneydocument.h" MyMoneyStorageDump::MyMoneyStorageDump() { @@ -116,6 +119,9 @@ s << "payees = " << _storage->payeeList().count() << ", next id = " << _storage->d_func()->m_nextPayeeID << "\n"; s << "tags = " << _storage->tagList().count() << ", next id = " << _storage->d_func()->m_nextTagID << "\n"; + s << "documentTypes = " << _storage->documentTypeList().count() << ", next id = " << _storage->d_func()->m_nextDocumentTypeID << "\n"; + s << "documents = " << _storage->documentList().count() << ", next id = " << _storage->d_func()->m_nextDocumentID << "\n"; + s << "institutions = " << _storage->institutionList().count() << ", next id = " << _storage->d_func()->m_nextInstitutionID << "\n"; s << "schedules = " << _storage->scheduleList(QString(), eMyMoney::Schedule::Type::Any, eMyMoney::Schedule::Occurrence::Any, eMyMoney::Schedule::PaymentType::Any, QDate(), QDate(), false).count() << ", next id = " << _storage->d_func()->m_nextScheduleID << "\n"; @@ -167,6 +173,32 @@ } s << "\n"; + s << "DocumentTypes" << "\n"; + s << "------" << "\n"; + + for (const MyMoneyDocumentType& dt : storage->documentTypeList()) { + s << " ID = " << dt.id() << "\n"; + s << " Name = " << dt.name() << "\n"; + s << " Description = " << dt.description() << "\n"; + s << "\n"; + } + s << "\n"; + + s << "Documents" << "\n"; + s << "------" << "\n"; + + for (const MyMoneyDocument& doc : storage->documentList()) { + s << " ID = " << doc.id() << "\n"; + s << " Name = " << doc.name() << "\n"; + s << " DocumentDate = " << doc.documentDate().toString(Qt::ISODate) << "\n"; + s << " DocumentType = " << doc.documentTypeId() << "\n"; + s << " Hash = " << doc.hash() << "\n"; + s << " Description = " << doc.description() << "\n"; + s << " StoragePath = " << doc.storagePath() << "\n"; + s << "\n"; + } + s << "\n"; + s << "Accounts" << "\n"; s << "--------" << "\n"; @@ -424,6 +456,10 @@ } else s << " ()\n"; } + + for (const QString& docId : split.documentIdList()) { + s << " Document = " << docId << "\n"; + } s << " Account = " << split.accountId(); MyMoneyAccount acc; try { diff --git a/kmymoney/mymoney/storage/mymoneystoragemgr.h b/kmymoney/mymoney/storage/mymoneystoragemgr.h --- a/kmymoney/mymoney/storage/mymoneystoragemgr.h +++ b/kmymoney/mymoney/storage/mymoneystoragemgr.h @@ -5,6 +5,7 @@ * Copyright 2004-2006 Ace Jones * Copyright 2006 Darren Gould * Copyright 2017-2018 Łukasz Wojniłowicz + * Copyright 2018 Marc Hübner * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -54,6 +55,8 @@ class MyMoneyTransaction; class MyMoneyTransactionFilter; class MyMoneyCostCenter; +class MyMoneyDocumentType; +class MyMoneyDocument; class onlineJob; class MyMoneyStorageSql; @@ -286,6 +289,118 @@ */ QList tagList() const; + /** + * This method is used to create a new document type + * + * An exception will be thrown upon error conditions + * + * @param doctype MyMoneyDocumentType reference to document type information + */ + void addDocumentType(MyMoneyDocumentType& doctype); + + /** + * This method is used to retrieve information about a document type + * An exception will be thrown upon error conditions. + * + * @param id QString reference to id of document type + * + * @return MyMoneyDocumentType object of document type + */ + MyMoneyDocumentType documentType(const QString& id) const; + + /** + * This method is used to retrieve the id to a corresponding + * name of a document type. + * An exception will be thrown upon error conditions. + * + * @param doctype QString reference to name of document type + * + * @return MyMoneyDocumentType object of document type + */ + MyMoneyDocumentType documentTypeByName(const QString& doctype) const; + + /** + * This method is used to modify an existing document type + * + * An exception will be thrown upon error conditions + * + * @param doctype MyMoneyDocumentType reference to document type information + */ + void modifyDocumentType(const MyMoneyDocumentType& doctype); + + /** + * This method is used to remove an existing document type + * + * An exception will be thrown upon error conditions + * + * @param doctype MyMoneyDocumentType reference to document type information + */ + void removeDocumentType(const MyMoneyDocumentType& doctype); + + /** + * This method returns a list of the document types + * inside a MyMoneyStorage object + * + * @return QList containing the document type information + */ + QList documentTypeList() const; + + /** + * This method is used to create a new document + * + * An exception will be thrown upon error conditions + * + * @param document MyMoneyDocument reference to document information + */ + void addDocument(MyMoneyDocument& document); + + /** + * This method is used to retrieve information about a document + * An exception will be thrown upon error conditions. + * + * @param id QString reference to id of document + * + * @return MyMoneyDocument object of document + */ + const MyMoneyDocument document(const QString& id) const; + + /** + * This method is used to retrieve the id to a corresponding + * name of a document. + * An exception will be thrown upon error conditions. + * + * @param name QString reference to name of document + * + * @return MyMoneyDocument object of document + */ + const MyMoneyDocument documentByName(const QString& name) const; + + /** + * This method is used to modify an existing document + * + * An exception will be thrown upon error conditions + * + * @param document MyMoneyDocument reference to document information + */ + void modifyDocument(const MyMoneyDocument& document); + + /** + * This method is used to remove an existing document + * + * An exception will be thrown upon error conditions + * + * @param document MyMoneyDocument reference to document information + */ + void removeDocument(const MyMoneyDocument& document); + + /** + * This method returns a list of the documents + * inside a MyMoneyStorage object + * + * @return QList containing the document information + */ + const QList documentList() const; + /** * This method is used to add one account as sub-ordinate to another * (parent) account. The objects passed as arguments will be modified @@ -633,6 +748,8 @@ void loadPrices(const MyMoneyPriceList& list); void loadOnlineJobs(const QMap& onlineJobs); void loadCostCenters(const QMap& costCenters); + void loadDocumentTypes(const QMap& map); + void loadDocuments(const QMap& map); /** * This method is used to set a value in the diff --git a/kmymoney/mymoney/storage/mymoneystoragemgr.cpp b/kmymoney/mymoney/storage/mymoneystoragemgr.cpp --- a/kmymoney/mymoney/storage/mymoneystoragemgr.cpp +++ b/kmymoney/mymoney/storage/mymoneystoragemgr.cpp @@ -5,6 +5,7 @@ * Copyright 2004-2006 Ace Jones * Copyright 2006 Darren Gould * Copyright 2017-2018 Łukasz Wojniłowicz + * Copyright 2018 Marc Hübner * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -396,6 +397,135 @@ return d->m_tagList.values(); } +void MyMoneyStorageMgr::addDocumentType(MyMoneyDocumentType& doctype) +{ + Q_D(MyMoneyStorageMgr); + // create the document type + MyMoneyDocumentType newDocumentType(d->nextDocumentTypeID(), doctype); + d->m_documentTypeList.insert(newDocumentType.id(), newDocumentType); + doctype = newDocumentType; +} + +MyMoneyDocumentType MyMoneyStorageMgr::documentType(const QString& id) const +{ + Q_D(const MyMoneyStorageMgr); + if (id.isEmpty()) + return MyMoneyDocumentType::null; + else if (!d->m_documentTypeList.contains(id)) + throw MYMONEYEXCEPTION(QString::fromLatin1("Unknown document type '%1'").arg(id)); + + return d->m_documentTypeList[id]; +} + +MyMoneyDocumentType MyMoneyStorageMgr::documentTypeByName(const QString& doctype) const +{ + Q_D(const MyMoneyStorageMgr); + if (doctype.isEmpty()) + return MyMoneyDocumentType::null; + + for (const MyMoneyDocumentType& dt : d->m_documentTypeList) { + if (dt.name() == doctype) + return dt; + } + + throw MYMONEYEXCEPTION(QString::fromLatin1("Unknown document type '%1'").arg(doctype)); +} + +void MyMoneyStorageMgr::modifyDocumentType(const MyMoneyDocumentType& doctype) +{ + Q_D(MyMoneyStorageMgr); + if (!doctype.id().isEmpty() && !d->m_documentTypeList.contains(doctype.id())) + throw MYMONEYEXCEPTION(QString::fromLatin1("Unknown document type '%1'").arg(doctype.id())); + + d->m_documentTypeList.modify(doctype.id(), doctype); +} + +void MyMoneyStorageMgr::removeDocumentType(const MyMoneyDocumentType& doctype) +{ + Q_D(MyMoneyStorageMgr); + if (!d->m_documentTypeList.contains(doctype.id())) + throw MYMONEYEXCEPTION(QString::fromLatin1("Unknown document type '%1'").arg(doctype.id())); + + // scan all documents to check if the document type is still referenced + for (const MyMoneyDocument& doc : d->m_documentList) { + if (doc.documentTypeId() == doctype.id()) + throw MYMONEYEXCEPTION(QString::fromLatin1("Cannot remove document type that is still referenced by a %1").arg("document")); + } + + d->m_documentTypeList.remove(doctype.id()); +} + +QList MyMoneyStorageMgr::documentTypeList() const +{ + Q_D(const MyMoneyStorageMgr); + return d->m_documentTypeList.values(); +} + +void MyMoneyStorageMgr::addDocument(MyMoneyDocument& document) +{ + // create the document + Q_D(MyMoneyStorageMgr); + MyMoneyDocument newDocument(d->nextDocumentID(), document); + d->m_documentList.insert(newDocument.id(), newDocument); + document = newDocument; +} + +const MyMoneyDocument MyMoneyStorageMgr::document(const QString& id) const +{ + Q_D(const MyMoneyStorageMgr); + if (!d->m_documentList.contains(id)) + throw MYMONEYEXCEPTION(QString::fromLatin1("Unknown document '%1'").arg(id)); + + return d->m_documentList[id]; +} + +const MyMoneyDocument MyMoneyStorageMgr::documentByName(const QString& name) const +{ + Q_D(const MyMoneyStorageMgr); + if (name.isEmpty()) + return MyMoneyDocument::null; + + for (const MyMoneyDocument& doc : d->m_documentList) { + if (doc.name() == name) + return doc; + } + throw MYMONEYEXCEPTION(QString::fromLatin1("Unknown document '%1'").arg(name)); +} + +void MyMoneyStorageMgr::modifyDocument(const MyMoneyDocument& document) +{ + Q_D(MyMoneyStorageMgr); + if (!d->m_documentList.contains(document.id())) + throw MYMONEYEXCEPTION(QString::fromLatin1("Unknown document '%1'").arg(document.id())); + + //throw exception in case we're referencing a doctype that doesn't exist + documentType(document.documentTypeId()); + + d->m_documentList.modify(document.id(), document); +} + +void MyMoneyStorageMgr::removeDocument(const MyMoneyDocument& document) //todo +{ + Q_D(MyMoneyStorageMgr); + if (!d->m_documentList.contains(document.id())) + throw MYMONEYEXCEPTION(QString::fromLatin1("Unknown document '%1'").arg(document.id())); + + // scan all splits of all transactions to check if the document is still referenced + for (const MyMoneyTransaction& transaction : d->m_transactionList) { + for (const MyMoneySplit& split : transaction.splits()) { + if (split.hasReferenceTo(document.id())) + throw MYMONEYEXCEPTION(QString::fromLatin1("Cannot remove document that is still referenced to a %1").arg("split")); + } + } + d->m_documentList.remove(document.id()); +} + +const QList MyMoneyStorageMgr::documentList() const +{ + Q_D(const MyMoneyStorageMgr); + return d->m_documentList.values(); +} + void MyMoneyStorageMgr::addAccount(MyMoneyAccount& parent, MyMoneyAccount& account) { Q_D(MyMoneyStorageMgr); @@ -644,14 +774,20 @@ // now check the splits foreach (const auto split, transaction.splits()) { // the following lines will throw an exception if the - // account or payee do not exist + // referenced account/payee/tag/document does not exist account(split.accountId()); if (!split.payeeId().isEmpty()) payee(split.payeeId()); foreach (const auto tagId, split.tagIdList()) { if (!tagId.isEmpty()) tag(tagId); } + + for (const auto docId : split.documentIdList()) { + if (!docId.isEmpty()) { + document(docId); + } + } } // new data seems to be ok. find old version of transaction @@ -1144,6 +1280,38 @@ } } +void MyMoneyStorageMgr::loadDocumentTypes(const QMap& map) +{ + Q_D(MyMoneyStorageMgr); + d->m_documentTypeList = map; + + // scan the map to identify the last used id + d->m_nextDocumentTypeID = 0; + const QRegularExpression idExp("DT(\\d+)$"); + auto end = map.constEnd(); + for (auto iter = map.constBegin(); iter != end; ++iter) { + const auto id = d->extractId(idExp, (*iter).id()); + if (id > d->m_nextDocumentTypeID) + d->m_nextDocumentTypeID = id; + } +} + +void MyMoneyStorageMgr::loadDocuments(const QMap& map) +{ + Q_D(MyMoneyStorageMgr); + d->m_documentList = map; + + // scan the map to identify the last used id + d->m_nextDocumentID = 0; + const QRegularExpression idExp("DOC(\\d+)$"); + auto end = map.constEnd(); + for (auto iter = map.constBegin(); iter != end; ++iter) { + const auto id = d->extractId(idExp, (*iter).id()); + if (id > d->m_nextDocumentID) + d->m_nextDocumentID = id; + } +} + void MyMoneyStorageMgr::setValue(const QString& key, const QString& val) { Q_D(MyMoneyStorageMgr); @@ -1822,6 +1990,16 @@ if (it.hasReferenceTo(id)) return true; + if (!skipCheck.testBit((int)Reference::DocumentType)) + foreach (const auto it, d->m_documentTypeList) + if (it.hasReferenceTo(id)) + return true; + + if (!skipCheck.testBit((int)Reference::Document)) + foreach (const auto it, d->m_documentList) + if (it.hasReferenceTo(id)) + return true; + // within the pricelist we don't have to scan each entry. Checking the QPair // members of the MyMoneySecurityPair is enough as they are identical to the // two security ids @@ -1851,6 +2029,8 @@ d->m_budgetList.startTransaction(&d->m_nextBudgetID); d->m_priceList.startTransaction(); d->m_onlineJobList.startTransaction(&d->m_nextOnlineJobID); + d->m_documentTypeList.startTransaction(&d->m_nextDocumentTypeID); + d->m_documentList.startTransaction(&d->m_nextDocumentID); } bool MyMoneyStorageMgr::commitTransaction() @@ -1870,7 +2050,8 @@ rc |= d->m_budgetList.commitTransaction(); rc |= d->m_priceList.commitTransaction(); rc |= d->m_onlineJobList.commitTransaction(); - + rc |= d->m_documentTypeList.commitTransaction(); + rc |= d->m_documentList.commitTransaction(); // if there was a change, touch the whole storage object if (rc) d->touch(); @@ -1894,4 +2075,6 @@ d->m_budgetList.rollbackTransaction(); d->m_priceList.rollbackTransaction(); d->m_onlineJobList.rollbackTransaction(); + d->m_documentTypeList.rollbackTransaction(); + d->m_documentList.rollbackTransaction(); } diff --git a/kmymoney/mymoney/storage/mymoneystoragemgr_p.h b/kmymoney/mymoney/storage/mymoneystoragemgr_p.h --- a/kmymoney/mymoney/storage/mymoneystoragemgr_p.h +++ b/kmymoney/mymoney/storage/mymoneystoragemgr_p.h @@ -5,6 +5,7 @@ * Copyright 2004-2006 Ace Jones * Copyright 2006 Darren Gould * Copyright 2017-2018 Łukasz Wojniłowicz + * Copyright 2018 Marc Hübner * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -57,6 +58,8 @@ #include "mymoneytransaction.h" #include "mymoneytransactionfilter.h" #include "mymoneycostcenter.h" +#include "mymoneydocumenttype.h" +#include "mymoneydocument.h" #include "mymoneymap.h" #include "onlinejob.h" #include "mymoneyenums.h" @@ -74,6 +77,8 @@ const int BUDGET_ID_SIZE = 6; const int ONLINE_JOB_ID_SIZE = 6; const int COSTCENTER_ID_SIZE = 6; +const int DOCUMENTTYPE_ID_SIZE = 6; +const int DOCUMENT_ID_SIZE = 18; class MyMoneyStorageMgrPrivate { @@ -94,6 +99,8 @@ m_nextBudgetID(0), m_nextOnlineJobID(0), m_nextCostCenterID(0), + m_nextDocumentTypeID(0), + m_nextDocumentID(0), m_dirty(false), m_creationDate(QDate::currentDate()), // initialize for file fixes (see kmymoneyview.cpp) @@ -420,6 +427,32 @@ return id; } + /** + * The member variable m_nextDocumentTypeID keeps the number that will be + * assigned to the next document type created. It is maintained by + * nextDocumentTypeID() + */ + QString nextDocumentTypeID() + { + QString id; + id.setNum(++m_nextDocumentTypeID); + id = "DT" + id.rightJustified(DOCUMENTTYPE_ID_SIZE, '0'); + return id; + } + + /** + * The member variable m_nextDocumentID keeps the number that will be + * assigned to the next document created. It is maintained by + * nextDocumentID() + */ + QString nextDocumentID() + { + QString id; + id.setNum(++m_nextDocumentID); + id = "DOC" + id.rightJustified(DOCUMENT_ID_SIZE, '0'); + return id; + } + ulong extractId(const QRegularExpression& exp, const QString& txtId) const { const auto match = exp.match(txtId, 0, QRegularExpression::NormalMatch, QRegularExpression::AnchoredMatchOption); @@ -507,6 +540,20 @@ */ ulong m_nextCostCenterID; + /** + * The member variable m_nextDocumentTypeID keeps the number that will be + * assigned to the next document type created. It is maintained by + * nextDocumentTypeID() + */ + ulong m_nextDocumentTypeID; + + /** + * The member variable m_nextDocumentID keeps the number that will be + * assigned to the next document created. It is maintained by + * nextDocumentID() + */ + ulong m_nextDocumentID; + /** * The member variable m_institutionList is the container for the * institutions known within this file. @@ -581,6 +628,16 @@ */ MyMoneyMap m_costCenterList; + /** + * A list containing all the document types that have been used + */ + MyMoneyMap m_documentTypeList; + + /** + * A list containing all the documents that have been used + */ + MyMoneyMap m_documentList; + /** * This member signals if the file has been modified or not */ diff --git a/kmymoney/mymoney/storage/storageenums.h b/kmymoney/mymoney/storage/storageenums.h --- a/kmymoney/mymoney/storage/storageenums.h +++ b/kmymoney/mymoney/storage/storageenums.h @@ -33,6 +33,8 @@ Currency, Price, Tag, + DocumentType, + Document, // insert new entries above this line Count }; diff --git a/kmymoney/mymoney/storage/tests/mymoneystoragemgr-test.h b/kmymoney/mymoney/storage/tests/mymoneystoragemgr-test.h --- a/kmymoney/mymoney/storage/tests/mymoneystoragemgr-test.h +++ b/kmymoney/mymoney/storage/tests/mymoneystoragemgr-test.h @@ -73,6 +73,14 @@ void testSchedule(); void testModifySchedule(); void testRemoveSchedule(); + void testAddDocumentType(); + void testModifyDocumentType(); + void testDocumentTypeName(); + void testRemoveDocumentType(); + void testAddDocument(); + void testModifyDocument(); + void testDocumentName(); + void testRemoveDocument(); void testSupportFunctions(); void testScheduleList(); void testAddCurrency(); diff --git a/kmymoney/mymoney/storage/tests/mymoneystoragemgr-test.cpp b/kmymoney/mymoney/storage/tests/mymoneystoragemgr-test.cpp --- a/kmymoney/mymoney/storage/tests/mymoneystoragemgr-test.cpp +++ b/kmymoney/mymoney/storage/tests/mymoneystoragemgr-test.cpp @@ -1,5 +1,6 @@ /* * Copyright 2009-2012 Thomas Baumgart + * Copyright 2018 Marc Hübner * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -38,6 +39,8 @@ #include "mymoneytransaction.h" #include "mymoneybudget.h" #include "mymoneyprice.h" +#include "mymoneydocumenttype.h" +#include "mymoneydocument.h" #include "onlinejob.h" #include "onlinetasks/dummy/tasks/dummytask.h" @@ -88,6 +91,8 @@ QCOMPARE(m->d_func()->m_payeeList.count(), 0); QCOMPARE(m->d_func()->m_tagList.count(), 0); QCOMPARE(m->d_func()->m_scheduleList.count(), 0); + QCOMPARE(m->d_func()->m_documentTypeList.count(), 0); + QCOMPARE(m->d_func()->m_documentList.count(), 0); QCOMPARE(m->d_func()->m_dirty, false); QCOMPARE(m->creationDate(), QDate::currentDate()); @@ -186,6 +191,11 @@ QCOMPARE(m->d_func()->m_nextReportID, 1ul); QCOMPARE(m->d_func()->nextOnlineJobID(), QLatin1String("O000001")); QCOMPARE(m->d_func()->m_nextOnlineJobID, 1ul); + QCOMPARE(m->d_func()->nextDocumentTypeID(), QLatin1String("DT000001")); + QCOMPARE(m->d_func()->m_nextDocumentTypeID, 1ul); + QCOMPARE(m->d_func()->nextDocumentID(), QLatin1String("DOC000000000000000001")); + QCOMPARE(m->d_func()->m_nextDocumentID, 1ul); + } void MyMoneyStorageMgrTest::testIsStandardAccount() @@ -1120,6 +1130,202 @@ QCOMPARE(m->d_func()->m_tagList.count(), 1); } +void MyMoneyStorageMgrTest::testAddDocumentType() +{ + MyMoneyDocumentType dt; + + dt.setName("Hue"); + m->d_func()->m_dirty = false; + try { + QCOMPARE(m->d_func()->m_nextDocumentTypeID, 0ul); + m->addDocumentType(dt); + m->commitTransaction(); + m->startTransaction(); + QCOMPARE(m->dirty(), true); + QCOMPARE(m->d_func()->m_nextDocumentTypeID, 1ul); + } catch (const MyMoneyException &) { + QFAIL("Unexpected exception"); + } +} + +void MyMoneyStorageMgrTest::testModifyDocumentType() +{ + MyMoneyDocumentType dt; + + testAddDocumentType(); + + dt = m->documentType("DT000001"); + dt.setName("New name"); + m->d_func()->m_dirty = false; + try { + m->modifyDocumentType(dt); + m->commitTransaction(); + m->startTransaction(); + dt = m->documentType("DT000001"); + QCOMPARE(dt.name(), QLatin1String("New name")); + QCOMPARE(m->dirty(), true); + } catch (const MyMoneyException &) { + QFAIL("Unexpected exception"); + } +} + +void MyMoneyStorageMgrTest::testRemoveDocumentType() +{ + testAddDocumentType(); + m->d_func()->m_dirty = false; + + // check that we can remove an unreferenced document type + MyMoneyDocumentType dt = m->documentType("DT000001"); + try { + QCOMPARE(m->d_func()->m_documentTypeList.count(), 1); + m->removeDocumentType(dt); + m->commitTransaction(); + m->startTransaction(); + QCOMPARE(m->d_func()->m_documentTypeList.count(), 0); + QCOMPARE(m->dirty(), true); + } catch (const MyMoneyException &e) { + qDebug(e.what()); + QFAIL("Unexpected exception"); + } + + // add transaction + testAddDocument(); + + MyMoneyDocument doc = m->document("DOC000000000000000001"); + doc.setDocumentTypeId("DT000001"); + + // check that we cannot add a document referencing an unknown document type + try { + m->modifyDocument(doc); + QFAIL("Expected exception"); + } catch (const MyMoneyException &) { + } + + m->d_func()->m_nextDocumentTypeID = 0; // reset here, so that the testAddDocumentType will not fail + testAddDocumentType(); + + // check that it works when the document type exists + try { + m->modifyDocument(doc); + } catch (const MyMoneyException &e) { + qDebug("Error: %s", e.what()); + QFAIL("Unexpected exception"); + } + + m->d_func()->m_dirty = false; + + // now check, that we cannot remove the document type + try { + m->removeDocumentType(dt); + QFAIL("Expected exception"); + } catch (const MyMoneyException &) { + } + QCOMPARE(m->d_func()->m_documentTypeList.count(), 1); +} + +void MyMoneyStorageMgrTest::testAddDocument() +{ + MyMoneyDocument doc; + + doc.setName("Hue"); + m->d_func()->m_dirty = false; + try { + QCOMPARE(m->d_func()->m_nextDocumentID, 0ul); + m->addDocument(doc); + m->commitTransaction(); + m->startTransaction(); + QCOMPARE(m->dirty(), true); + QCOMPARE(m->d_func()->m_nextDocumentID, 1ul); + } catch (const MyMoneyException &e) { + qDebug("Error: %s", e.what()); + QFAIL("Unexpected exception"); + } +} + +void MyMoneyStorageMgrTest::testModifyDocument() +{ + MyMoneyDocument doc; + + testAddDocument(); + testAddDocumentType(); + doc = m->document("DOC000000000000000001"); + doc.setName("New name"); + doc.setDocumentTypeId("DT000001"); + m->d_func()->m_dirty = false; + try { + m->modifyDocument(doc); + m->commitTransaction(); + m->startTransaction(); + doc = m->document("DOC000000000000000001"); + QCOMPARE(doc.name(), QLatin1String("New name")); + QCOMPARE(m->dirty(), true); + } catch (const MyMoneyException &e) { + qDebug("Error: %s", e.what()); + QFAIL("Unexpected exception"); + } +} + +void MyMoneyStorageMgrTest::testRemoveDocument() +{ + testAddDocument(); + m->d_func()->m_dirty = false; + + // check that we can remove an unreferenced document + MyMoneyDocument doc = m->document("DOC000000000000000001"); + try { + QCOMPARE(m->d_func()->m_documentList.count(), 1); + m->removeDocument(doc); + m->commitTransaction(); + m->startTransaction(); + QCOMPARE(m->d_func()->m_documentList.count(), 0); + QCOMPARE(m->dirty(), true); + } catch (const MyMoneyException &e) { + qDebug(e.what()); + QFAIL("Unexpected exception"); + } + + // add transaction + testAddTransactions(); + + MyMoneyTransaction tr = m->transaction("T000000000000000001"); + MyMoneySplit sp; + sp = tr.splits()[0]; + QList documentIdList; + documentIdList << "DOC000000000000000001"; + sp.setDocumentIdList(documentIdList); + tr.modifySplit(sp); + + // check that we cannot add a transaction referencing + // an unknown document + try { + m->modifyTransaction(tr); + QFAIL("Expected exception"); + } catch (const MyMoneyException &) { + } + + m->d_func()->m_nextDocumentID = 0; // reset here, so that the + // testAddDocument will not fail + testAddDocument(); + + // check that it works when the document exists + try { + m->modifyTransaction(tr); + } catch (const MyMoneyException &e) { + qDebug(e.what()); + QFAIL("Unexpected exception"); + } + + m->d_func()->m_dirty = false; + + // now check, that we cannot remove the document + try { + m->removeDocument(doc); + QFAIL("Expected exception"); + } catch (const MyMoneyException &) { + } + QCOMPARE(m->d_func()->m_documentList.count(), 1); +} + void MyMoneyStorageMgrTest::testRemoveAccountFromTree() { MyMoneyAccount a, b, c; @@ -1216,6 +1422,56 @@ } } +void MyMoneyStorageMgrTest::testDocumentTypeName() +{ + testAddDocumentType(); + + MyMoneyDocumentType dt; + QString name("Hue"); + + // OK case + try { + dt = m->documentTypeByName(name); + QCOMPARE(dt.name(), QLatin1String("Hue")); + QCOMPARE(dt.id(), QLatin1String("DT000001")); + } catch (const MyMoneyException &e) { + unexpectedException(e); + } + + // Not OK case + name = "HUE"; + try { + dt = m->documentTypeByName(name); + QFAIL("Exception expected"); + } catch (const MyMoneyException &) { + } +} + +void MyMoneyStorageMgrTest::testDocumentName() +{ + testAddDocument(); + + MyMoneyDocument doc; + QString name("Hue"); + + // OK case + try { + doc = m->documentByName(name); + QCOMPARE(doc.name(), QLatin1String("Hue")); + QCOMPARE(doc.id(), QLatin1String("DOC000000000000000001")); + } catch (const MyMoneyException &e) { + unexpectedException(e); + } + + // Not OK case + name = "HUE"; + try { + doc = m->documentByName(name); + QFAIL("Exception expected"); + } catch (const MyMoneyException &) { + } +} + // disabled because of no real world use case //void MyMoneyStorageMgrTest::testAssignment() //{ @@ -1244,6 +1500,8 @@ QCOMPARE(m->d_func()->m_nextPayeeID, t->d_func()->m_nextPayeeID); QCOMPARE(m->d_func()->m_nextTagID, t->d_func()->m_nextTagID); QCOMPARE(m->d_func()->m_nextScheduleID, t->d_func()->m_nextScheduleID); + QCOMPARE(m->d_func()->m_nextDocumentTypeID, t->d_func()->m_nextDocumentTypeID); + QCOMPARE(m->d_func()->m_nextDocumentID, t->d_func()->m_nextDocumentID); QCOMPARE(m->dirty(), t->dirty()); QCOMPARE(m->creationDate(), t->creationDate()); QCOMPARE(m->lastModificationDate(), t->lastModificationDate()); @@ -1265,8 +1523,13 @@ QCOMPARE(m->d_func()->m_transactionList.keys(), t->d_func()->m_transactionList.keys()); QCOMPARE(m->d_func()->m_transactionList.values(), t->d_func()->m_transactionList.values()); -// QCOMPARE(m->d_func()->m_scheduleList.keys(), t->m_scheduleList.keys()); -// QCOMPARE(m->d_func()->m_scheduleList.values(), t->m_scheduleList.values()); + QCOMPARE(m->d_func()->m_documentTypeList.keys(), t->d_func()->m_documentTypeList.keys()); + QCOMPARE(m->d_func()->m_documentTypeList.values(), t->d_func()->m_documentTypeList.values()); + QCOMPARE(m->d_func()->m_documentList.keys(), t->d_func()->m_documentList.keys()); + QCOMPARE(m->d_func()->m_documentList.values(), t->d_func()->m_documentList.values()); + + // QCOMPARE(m->d_func()->m_scheduleList.keys(), t->m_scheduleList.keys()); + // QCOMPARE(m->d_func()->m_scheduleList.values(), t->m_scheduleList.values()); } // disabled because of no real world use case @@ -1305,13 +1568,13 @@ m->addAccount(a2); MyMoneySchedule schedule("Sched-Name", - Schedule::Type::Deposit, - Schedule::Occurrence::Daily, 1, - Schedule::PaymentType::ManualDeposit, - QDate(), - QDate(), - true, - false); + Schedule::Type::Deposit, + Schedule::Occurrence::Daily, 1, + Schedule::PaymentType::ManualDeposit, + QDate(), + QDate(), + true, + false); t1.setPostDate(QDate(2003, 7, 10)); schedule.setTransaction(t1); m->addSchedule(schedule); @@ -1325,13 +1588,13 @@ try { MyMoneySchedule schedule("Sched-Name", - Schedule::Type::Deposit, - Schedule::Occurrence::Daily, 1, - Schedule::PaymentType::ManualDeposit, - QDate(), - QDate(), - true, - false); + Schedule::Type::Deposit, + Schedule::Occurrence::Daily, 1, + Schedule::PaymentType::ManualDeposit, + QDate(), + QDate(), + true, + false); m->addSchedule(schedule); QFAIL("Exception expected"); } catch (const MyMoneyException &) { @@ -1429,13 +1692,13 @@ m->addAccount(a1); m->addAccount(a2); MyMoneySchedule schedule1("Schedule 1", - Schedule::Type::Bill, - Schedule::Occurrence::Once, 1, - Schedule::PaymentType::DirectDebit, - QDate(), - QDate(), - false, - false); + Schedule::Type::Bill, + Schedule::Occurrence::Once, 1, + Schedule::PaymentType::DirectDebit, + QDate(), + QDate(), + false, + false); t1.setPostDate(notOverdue); schedule1.setTransaction(t1); schedule1.setLastPayment(notOverdue); @@ -1449,13 +1712,13 @@ MyMoneyAccount a3("A000003", MyMoneyAccount()); m->addAccount(a3); MyMoneySchedule schedule2("Schedule 2", - Schedule::Type::Deposit, - Schedule::Occurrence::Daily, 1, - Schedule::PaymentType::DirectDeposit, - QDate(), - QDate(), - false, - false); + Schedule::Type::Deposit, + Schedule::Occurrence::Daily, 1, + Schedule::PaymentType::DirectDeposit, + QDate(), + QDate(), + false, + false); t2.setPostDate(notOverdue.addDays(1)); schedule2.setTransaction(t2); schedule2.setLastPayment(notOverdue.addDays(1)); @@ -1473,13 +1736,13 @@ m->addAccount(a6); m->addAccount(a7); MyMoneySchedule schedule3("Schedule 3", - Schedule::Type::Transfer, - Schedule::Occurrence::Weekly, 1, - Schedule::PaymentType::Other, - QDate(), - QDate(), - false, - false); + Schedule::Type::Transfer, + Schedule::Occurrence::Weekly, 1, + Schedule::PaymentType::Other, + QDate(), + QDate(), + false, + false); t3.setPostDate(notOverdue.addDays(2)); schedule3.setTransaction(t3); schedule3.setLastPayment(notOverdue.addDays(2)); @@ -1491,13 +1754,13 @@ s8.setAccountId("A000006"); t4.addSplit(s8); MyMoneySchedule schedule4("Schedule 4", - Schedule::Type::Bill, - Schedule::Occurrence::Weekly, 1, - Schedule::PaymentType::WriteChecque, - QDate(), - notOverdue.addDays(31), - false, - false); + Schedule::Type::Bill, + Schedule::Occurrence::Weekly, 1, + Schedule::PaymentType::WriteChecque, + QDate(), + notOverdue.addDays(31), + false, + false); t4.setPostDate(overdue.addDays(-7)); schedule4.setTransaction(t4); @@ -1515,83 +1778,83 @@ // no filter list = m->scheduleList(QString(), Schedule::Type::Any, Schedule::Occurrence::Any, Schedule::PaymentType::Any, - QDate(), QDate(), false); + QDate(), QDate(), false); QCOMPARE(list.count(), 4); // filter by type list = m->scheduleList("", Schedule::Type::Bill, Schedule::Occurrence::Any, Schedule::PaymentType::Any, - QDate(), QDate(), false); + QDate(), QDate(), false); QCOMPARE(list.count(), 2); QCOMPARE(list[0].name(), QLatin1String("Schedule 1")); QCOMPARE(list[1].name(), QLatin1String("Schedule 4")); // filter by occurrence list = m->scheduleList("", Schedule::Type::Any, - Schedule::Occurrence::Daily, Schedule::PaymentType::Any, - QDate(), QDate(), false); + Schedule::Occurrence::Daily, Schedule::PaymentType::Any, + QDate(), QDate(), false); QCOMPARE(list.count(), 1); QCOMPARE(list[0].name(), QLatin1String("Schedule 2")); // filter by payment type list = m->scheduleList("", Schedule::Type::Any, - Schedule::Occurrence::Any, - Schedule::PaymentType::DirectDeposit, - QDate(), QDate(), false); + Schedule::Occurrence::Any, + Schedule::PaymentType::DirectDeposit, + QDate(), QDate(), false); QCOMPARE(list.count(), 1); QCOMPARE(list[0].name(), QLatin1String("Schedule 2")); // filter by account list = m->scheduleList("A01", Schedule::Type::Any, Schedule::Occurrence::Any, Schedule::PaymentType::Any, - QDate(), QDate(), false); + QDate(), QDate(), false); QCOMPARE(list.count(), 0); list = m->scheduleList("A000001", Schedule::Type::Any, Schedule::Occurrence::Any, Schedule::PaymentType::Any, - QDate(), QDate(), false); + QDate(), QDate(), false); QCOMPARE(list.count(), 2); list = m->scheduleList("A000002", Schedule::Type::Any, Schedule::Occurrence::Any, Schedule::PaymentType::Any, - QDate(), QDate(), false); + QDate(), QDate(), false); QCOMPARE(list.count(), 1); // filter by start date list = m->scheduleList("", Schedule::Type::Any, - Schedule::Occurrence::Any, - Schedule::PaymentType::Any, - notOverdue.addDays(31), - QDate(), false); + Schedule::Occurrence::Any, + Schedule::PaymentType::Any, + notOverdue.addDays(31), + QDate(), false); QCOMPARE(list.count(), 3); QCOMPARE(list[0].name(), QLatin1String("Schedule 2")); QCOMPARE(list[1].name(), QLatin1String("Schedule 3")); QCOMPARE(list[2].name(), QLatin1String("Schedule 4")); // filter by end date list = m->scheduleList("", Schedule::Type::Any, - Schedule::Occurrence::Any, - Schedule::PaymentType::Any, - QDate(), - notOverdue.addDays(1), - false); + Schedule::Occurrence::Any, + Schedule::PaymentType::Any, + QDate(), + notOverdue.addDays(1), + false); QCOMPARE(list.count(), 3); QCOMPARE(list[0].name(), QLatin1String("Schedule 1")); QCOMPARE(list[1].name(), QLatin1String("Schedule 2")); QCOMPARE(list[2].name(), QLatin1String("Schedule 4")); // filter by start and end date list = m->scheduleList("", Schedule::Type::Any, - Schedule::Occurrence::Any, - Schedule::PaymentType::Any, - notOverdue.addDays(-1), - notOverdue.addDays(1), - false); + Schedule::Occurrence::Any, + Schedule::PaymentType::Any, + notOverdue.addDays(-1), + notOverdue.addDays(1), + false); QCOMPARE(list.count(), 2); QCOMPARE(list[0].name(), QLatin1String("Schedule 1")); QCOMPARE(list[1].name(), QLatin1String("Schedule 2")); // filter by overdue status list = m->scheduleList("", Schedule::Type::Any, - Schedule::Occurrence::Any, - Schedule::PaymentType::Any, - QDate(), - QDate(), - true); + Schedule::Occurrence::Any, + Schedule::PaymentType::Any, + QDate(), + QDate(), + true); QCOMPARE(list.count(), 1); QCOMPARE(list[0].name(), QLatin1String("Schedule 4")); } @@ -1795,6 +2058,28 @@ QCOMPARE(m->d_func()->m_tagList.keys(), tamap.keys()); QCOMPARE(m->d_func()->m_nextTagID, 1234ul); + // document type loader + QMap dtmap; + MyMoneyDocumentType dt("DT54231", MyMoneyDocumentType()); + dtmap[dt.id()] = dt; + m->loadDocumentTypes(dtmap); + QCOMPARE(m->d_func()->m_documentTypeList.values(), dtmap.values()); + QCOMPARE(m->d_func()->m_documentTypeList.keys(), dtmap.keys()); + QCOMPARE(m->d_func()->m_nextDocumentTypeID, 54231ul); + + // document loader + QMap docmap; + MyMoneyDocument doc("DOC123123123", MyMoneyDocument()); + docmap[doc.id()] = doc; + + auto docid = "id is " + doc.id(); + + qDebug(doc.id().toStdString().c_str()); + m->loadDocuments(docmap); + QCOMPARE(m->d_func()->m_documentList.values(), docmap.values()); + QCOMPARE(m->d_func()->m_documentList.keys(), docmap.keys()); + QCOMPARE(m->d_func()->m_nextDocumentID, 123123123ul); + // security loader QMap smap; MyMoneySecurity s("E54321", MyMoneySecurity()); diff --git a/kmymoney/plugins/sql/mymoneydbdef.h b/kmymoney/plugins/sql/mymoneydbdef.h --- a/kmymoney/plugins/sql/mymoneydbdef.h +++ b/kmymoney/plugins/sql/mymoneydbdef.h @@ -467,6 +467,9 @@ TABLE(PayeeIdentifier) TABLE(PluginInfo) TABLE(CostCenter) + TABLE(DocumentTypes); + TABLE(Documents); + TABLE(DocumentSplits); VIEW(Balances) protected: diff --git a/kmymoney/plugins/sql/mymoneydbdef.cpp b/kmymoney/plugins/sql/mymoneydbdef.cpp --- a/kmymoney/plugins/sql/mymoneydbdef.cpp +++ b/kmymoney/plugins/sql/mymoneydbdef.cpp @@ -5,6 +5,7 @@ copyright : (C) 2010 by Fernando Vilas email : tonybloom@users.sourceforge.net : Fernando Vilas + : Marc Hübner ***************************************************************************/ /*************************************************************************** @@ -64,6 +65,9 @@ OnlineJobs(); PayeeIdentifier(); CostCenter(); + DocumentTypes(); + Documents(); + DocumentSplits(); } /* PRIMARYKEY - these fields combine to form a unique key field on which the db will create an index @@ -96,6 +100,8 @@ appendField(MyMoneyDbIntColumn("schedules", MyMoneyDbIntColumn::BIG, UNSIGNED)); appendField(MyMoneyDbIntColumn("reports", MyMoneyDbIntColumn::BIG, UNSIGNED)); appendField(MyMoneyDbIntColumn("kvps", MyMoneyDbIntColumn::BIG, UNSIGNED)); + appendField(MyMoneyDbIntColumn("documentTypes", MyMoneyDbIntColumn::BIG, UNSIGNED, false, false, 12)); + appendField(MyMoneyDbIntColumn("documents", MyMoneyDbIntColumn::BIG, UNSIGNED, false, false, 12)); appendField(MyMoneyDbColumn("dateRangeStart", "date")); appendField(MyMoneyDbColumn("dateRangeEnd", "date")); appendField(MyMoneyDbIntColumn("hiInstitutionId", MyMoneyDbIntColumn::BIG, UNSIGNED)); @@ -112,6 +118,8 @@ appendField(MyMoneyDbIntColumn("hiBudgetId", MyMoneyDbIntColumn::BIG, UNSIGNED, false, false, 1)); appendField(MyMoneyDbIntColumn("hiOnlineJobId", MyMoneyDbIntColumn::BIG, UNSIGNED, false, false, 8)); appendField(MyMoneyDbIntColumn("hiPayeeIdentifierId", MyMoneyDbIntColumn::BIG, UNSIGNED, false, false, 8)); + appendField(MyMoneyDbIntColumn("hiDocumentTypeId", MyMoneyDbIntColumn::BIG, UNSIGNED, false, false, 12)); + appendField(MyMoneyDbIntColumn("hiDocumentId", MyMoneyDbIntColumn::BIG, UNSIGNED, false, false, 12)); appendField(MyMoneyDbColumn("logonUser", "varchar(255)", false, false, 1)); appendField(MyMoneyDbDatetimeColumn("logonAt", false, false, 1)); appendField(MyMoneyDbIntColumn("fixLevel", @@ -196,6 +204,44 @@ m_tables[t.name()] = t; } + +void MyMoneyDbDef::DocumentTypes() +{ + QList > fields; + appendField(MyMoneyDbColumn("id", "varchar(32)", PRIMARYKEY, NOTNULL)); + appendField(MyMoneyDbTextColumn("name")); + appendField(MyMoneyDbTextColumn("description")); + MyMoneyDbTable t("kmmDocumentTypes", fields); + t.buildSQLStrings(); + m_tables[t.name()] = t; +} + +void MyMoneyDbDef::Documents() +{ + QList > fields; + appendField(MyMoneyDbColumn("id", "varchar(32)", PRIMARYKEY, NOTNULL)); + appendField(MyMoneyDbTextColumn("name")); + appendField(MyMoneyDbDatetimeColumn("documentdate")); + appendField(MyMoneyDbTextColumn("documenttype")); + appendField(MyMoneyDbTextColumn("hash")); + appendField(MyMoneyDbTextColumn("description")); + appendField(MyMoneyDbTextColumn("storagepath")); + MyMoneyDbTable t("kmmDocuments", fields); + t.buildSQLStrings(); + m_tables[t.name()] = t; +} + +void MyMoneyDbDef::DocumentSplits() +{ + QList > fields; + appendField(MyMoneyDbColumn("transactionId", "varchar(32)", PRIMARYKEY, NOTNULL)); + appendField(MyMoneyDbColumn("documentId", "varchar(32)", PRIMARYKEY, NOTNULL)); + appendField(MyMoneyDbIntColumn("splitId", MyMoneyDbIntColumn::SMALL, UNSIGNED, PRIMARYKEY, NOTNULL)); + MyMoneyDbTable t("kmmDocumentSplits", fields); + t.buildSQLStrings(); + m_tables[t.name()] = t; +} + void MyMoneyDbDef::Accounts() { QList > fields; diff --git a/kmymoney/plugins/sql/mymoneystoragesql.h b/kmymoney/plugins/sql/mymoneystoragesql.h --- a/kmymoney/plugins/sql/mymoneystoragesql.h +++ b/kmymoney/plugins/sql/mymoneystoragesql.h @@ -4,6 +4,7 @@ * Copyright (C) Fernando Vilas * Copyright (C) 2014 Christian Dávid * (C) 2017 by Łukasz Wojniłowicz + * (C) 2018 by Marc Hübner * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -50,6 +51,8 @@ class MyMoneyBudget; class MyMoneyReport; class MyMoneyPrice; +class MyMoneyDocumentType; +class MyMoneyDocument; class payeeIdentifier; class onlineJob; class MyMoneyStorageSql; @@ -177,6 +180,12 @@ void addPayeeIdentifier(payeeIdentifier& ident); void modifyPayeeIdentifier(const payeeIdentifier& ident); void removePayeeIdentifier(const payeeIdentifier& ident); + void addDocumentType(const MyMoneyDocumentType& dt); + void modifyDocumentType(const MyMoneyDocumentType& dt); + void removeDocumentType(const MyMoneyDocumentType& dt); + void addDocument(const MyMoneyDocument& dt); + void modifyDocument(const MyMoneyDocument& dt); + void removeDocument(const MyMoneyDocument& dt); ulong transactionCount(const QString& aid) const; QHash transactionCountMap() const; @@ -242,6 +251,12 @@ QMap fetchPayeeIdentifiers(const QStringList& idList) const; QMap fetchPayeeIdentifiers() const; + QMap fetchDocumentTypes(const QStringList& idList, bool forUpdate = false) const; + QMap fetchDocumentTypes() const; + + QMap fetchDocuments(const QStringList& idList, bool forUpdate = false) const; + QMap fetchDocuments() const; + bool isReferencedByTransaction(const QString& id) const; void readPayees(const QString&); @@ -252,6 +267,14 @@ void readTags(const QList& tagList); void readTags(); + void readDocumentTypes(const QString&); + void readDocumentTypes(const QList& dtList); + void readDocumentTypes(); + + void readDocuments(const QString&); + void readDocuments(const QList& docList); + void readDocuments(); + void readTransactions(const MyMoneyTransactionFilter& filter); void setProgressCallback(void(*callback)(int, int, const QString&)) override; @@ -275,6 +298,8 @@ ulong getNextSecurityId() const; ulong getNextTransactionId() const; ulong getNextCostCenterId() const; + ulong getNextDocumentTypeId() const; + ulong getNextDocumentId() const; ulong incrementBudgetId(); ulong incrementAccountId(); @@ -288,6 +313,8 @@ ulong incrementOnlineJobId(); ulong incrementPayeeIdentfierId(); ulong incrementCostCenterId(); + ulong incrementDocumentTypeId(); + ulong incrementDocumentId(); void loadAccountId(ulong id); void loadTransactionId(ulong id); @@ -301,6 +328,8 @@ void loadOnlineJobId(ulong id); void loadPayeeIdentifierId(ulong id); void loadCostCenterId(ulong id); + void loadDocumentTypeId(ulong id); + void loadDocumentId(ulong id); /** * This method allows to modify the precision with which prices diff --git a/kmymoney/plugins/sql/mymoneystoragesql.cpp b/kmymoney/plugins/sql/mymoneystoragesql.cpp --- a/kmymoney/plugins/sql/mymoneystoragesql.cpp +++ b/kmymoney/plugins/sql/mymoneystoragesql.cpp @@ -7,6 +7,7 @@ : Fernando Vilas : Christian Dávid (C) 2017 by Łukasz Wojniłowicz + (C) 2018 by Marc Hübner ***************************************************************************/ /*************************************************************************** @@ -275,6 +276,8 @@ d->readReports(); d->readBudgets(); d->readOnlineJobs(); + readDocumentTypes(); + readDocuments(); //FIXME - ?? if (m_mode == 0) //m_storage->rebuildAccountBalances(); // this seems to be nonsense, but it clears the dirty flag @@ -299,9 +302,9 @@ Q_D(MyMoneyStorageSql); // initialize record counts and hi ids d->m_institutions = d->m_accounts = d->m_payees = d->m_tags = d->m_transactions = d->m_splits - = d->m_securities = d->m_prices = d->m_currencies = d->m_schedules = d->m_reports = d->m_kvps = d->m_budgets = 0; + = d->m_securities = d->m_prices = d->m_currencies = d->m_schedules = d->m_reports = d->m_kvps = d->m_budgets = d->m_documentTypes = d->m_documents = 0; d->m_hiIdInstitutions = d->m_hiIdPayees = d->m_hiIdTags = d->m_hiIdAccounts = d->m_hiIdTransactions = - d->m_hiIdSchedules = d->m_hiIdSecurities = d->m_hiIdReports = d->m_hiIdBudgets = 0; + d->m_hiIdSchedules = d->m_hiIdSecurities = d->m_hiIdReports = d->m_hiIdBudgets = d->m_hiIdDocumentTypes = d->m_hiIdDocuments = 0; d->m_onlineJobs = d->m_payeeIdentifier = 0; d->m_displayStatus = true; try { @@ -325,6 +328,8 @@ d->writeReports(); d->writeBudgets(); d->writeOnlineJobs(); + d->writeDocumentTypes(); + d->writeDocuments(); d->writeFileInfo(); // this seems to be nonsense, but it clears the dirty flag // as a side-effect. @@ -650,6 +655,74 @@ d->writeFileInfo(); } +// **** Document Types **** +void MyMoneyStorageSql::addDocumentType(const MyMoneyDocumentType& dt) +{ + Q_D(MyMoneyStorageSql); + MyMoneyDbTransaction t(*this, Q_FUNC_INFO); + QSqlQuery q(*this); + q.prepare(d->m_db.m_tables["kmmDocumentTypes"].insertString()); + d->writeDocumentType(dt, q); + ++d->m_documentTypes; + d->writeFileInfo(); +} + +void MyMoneyStorageSql::modifyDocumentType(const MyMoneyDocumentType& dt) +{ + Q_D(MyMoneyStorageSql); + MyMoneyDbTransaction t(*this, Q_FUNC_INFO); + QSqlQuery q(*this); + q.prepare(d->m_db.m_tables["kmmDocumentTypes"].updateString()); + d->writeDocumentType(dt, q); + d->writeFileInfo(); +} + +void MyMoneyStorageSql::removeDocumentType(const MyMoneyDocumentType& dt) +{ + Q_D(MyMoneyStorageSql); + MyMoneyDbTransaction t(*this, Q_FUNC_INFO); + QSqlQuery query(*this); + query.prepare(d->m_db.m_tables["kmmDocumentTypes"].deleteString()); + query.bindValue(":id", dt.id()); + if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("deleting Document type")); // krazy:exclude=crashy + --d->m_documentTypes; + d->writeFileInfo(); +} + +// **** Documents **** +void MyMoneyStorageSql::addDocument(const MyMoneyDocument& doc) +{ + Q_D(MyMoneyStorageSql); + MyMoneyDbTransaction t(*this, Q_FUNC_INFO); + QSqlQuery q(*this); + q.prepare(d->m_db.m_tables["kmmDocuments"].insertString()); + d->writeDocument(doc, q); + ++d->m_documents; + d->writeFileInfo(); +} + +void MyMoneyStorageSql::modifyDocument(const MyMoneyDocument& doc) +{ + Q_D(MyMoneyStorageSql); + MyMoneyDbTransaction t(*this, Q_FUNC_INFO); + QSqlQuery q(*this); + q.prepare(d->m_db.m_tables["kmmDocuments"].updateString()); + d->writeDocument(doc, q); + d->writeFileInfo(); +} + +void MyMoneyStorageSql::removeDocument(const MyMoneyDocument& doc) +{ + Q_D(MyMoneyStorageSql); + MyMoneyDbTransaction t(*this, Q_FUNC_INFO); + QSqlQuery query(*this); + query.prepare(d->m_db.m_tables["kmmDocuments"].deleteString()); + query.bindValue(":id", doc.id()); + if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("deleting Document")); // krazy:exclude=crashy + --d->m_documents; + d->writeFileInfo(); +} + // **** Accounts **** void MyMoneyStorageSql::addAccount(const MyMoneyAccount& acc) { @@ -1399,6 +1472,49 @@ readTags(QList()); } +void MyMoneyStorageSql::readDocumentTypes(const QString& id) +{ + QList list; + list.append(id); + readDocumentTypes(list); +} + +void MyMoneyStorageSql::readDocumentTypes(const QList& pid) +{ + Q_D(MyMoneyStorageSql); + try { + d->m_storage->loadDocumentTypes(fetchDocumentTypes(pid)); + } catch (const MyMoneyException &) { + } +} + +void MyMoneyStorageSql::readDocumentTypes() +{ + readDocumentTypes(QList()); +} + +void MyMoneyStorageSql::readDocuments(const QString& id) +{ + QList list; + list.append(id); + readDocuments(list); +} + +void MyMoneyStorageSql::readDocuments(const QList& pid) +{ + Q_D(MyMoneyStorageSql); + try { + d->m_storage->loadDocuments(fetchDocuments(pid)); + } catch (const MyMoneyException &e) { + throw e; + } +} + +void MyMoneyStorageSql::readDocuments() +{ + readDocuments(QList()); +} + QMap MyMoneyStorageSql::fetchOnlineJobs(const QStringList& idList, bool forUpdate) const { Q_D(const MyMoneyStorageSql); @@ -1559,6 +1675,114 @@ return fetchTags(QStringList(), false); } +QMap MyMoneyStorageSql::fetchDocumentTypes(const QStringList& idList, bool /*forUpdate*/) const +{ + Q_D(const MyMoneyStorageSql); + MyMoneyDbTransaction trans(const_cast (*this), Q_FUNC_INFO); + if (d->m_displayStatus) { + int doctypesNb = (idList.isEmpty() ? d->m_documentTypes : idList.size()); + d->signalProgress(0, doctypesNb, QObject::tr("Loading document types...")); + } else { + // if (m_documentTypeListRead) return; + } + int progress = 0; + QMap dtList; + //ulong lastId; + const MyMoneyDbTable& t = d->m_db.m_tables["kmmDocumentTypes"]; + QSqlQuery query(*const_cast (this)); + if (idList.isEmpty()) { + query.prepare(t.selectAllString()); + } else { + QString whereClause = " where ("; + QString itemConnector = ""; + foreach (const QString& it, idList) { + whereClause.append(QString("%1id = '%2'").arg(itemConnector).arg(it)); + itemConnector = " or "; + } + whereClause += ')'; + query.prepare(t.selectAllString(false) + whereClause); + } + if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("reading Document type")); // krazy:exclude=crashy + int idCol = t.fieldNumber("id"); + int nameCol = t.fieldNumber("name"); + int descriptionCol = t.fieldNumber("description"); + + while (query.next()) { + QString pid; + QString boolChar; + MyMoneyDocumentType doctype; + pid = GETSTRING(idCol); + doctype.setName(GETSTRING(nameCol)); + doctype.setDescription(GETSTRING(descriptionCol)); + dtList[pid] = MyMoneyDocumentType(pid, doctype); + if (d->m_displayStatus) d->signalProgress(++progress, 0); + } + return dtList; +} + +QMap MyMoneyStorageSql::fetchDocumentTypes() const +{ + return fetchDocumentTypes(QStringList(), false); +} + +QMap MyMoneyStorageSql::fetchDocuments(const QStringList& idList, bool /*forUpdate*/) const +{ + Q_D(const MyMoneyStorageSql); + MyMoneyDbTransaction trans(const_cast (*this), Q_FUNC_INFO); + if (d->m_displayStatus) { + int documentsNb = (idList.isEmpty() ? d->m_documents : idList.size()); + d->signalProgress(0, documentsNb, QObject::tr("Loading documents...")); + } else { + // if (m_documentListRead) return; + } + int progress = 0; + QMap docList; + //ulong lastId; + const MyMoneyDbTable& t = d->m_db.m_tables["kmmDocuments"]; + QSqlQuery query(*const_cast (this)); + if (idList.isEmpty()) { + query.prepare(t.selectAllString()); + } else { + QString whereClause = " where ("; + QString itemConnector = ""; + foreach (const QString& it, idList) { + whereClause.append(QString("%1id = '%2'").arg(itemConnector).arg(it)); + itemConnector = " or "; + } + whereClause += ')'; + query.prepare(t.selectAllString(false) + whereClause); + } + if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("reading Document")); // krazy:exclude=crashy + int idCol = t.fieldNumber("id"); + int nameCol = t.fieldNumber("name"); + int descriptionCol = t.fieldNumber("description"); + int docDateCol = t.fieldNumber("documentdate"); + int docTypeCol = t.fieldNumber("documenttype"); + int storagePathCol = t.fieldNumber("storagepath"); + int checksumCol = t.fieldNumber("hash"); + + while (query.next()) { + QString pid; + QString boolChar; + MyMoneyDocument document; + pid = GETSTRING(idCol); + document.setName(GETSTRING(nameCol)); + document.setDescription(GETSTRING(descriptionCol)); + document.setDocumentDate(QDate::fromString(GETSTRING(docDateCol))); + document.setDocumentTypeId(GETSTRING(docTypeCol)); + document.setStoragePath(GETSTRING(storagePathCol)); + document.setHash(GETSTRING(checksumCol)); + docList[pid] = MyMoneyDocument(pid, document); + if (d->m_displayStatus) d->signalProgress(++progress, 0); + } + return docList; +} + +QMap MyMoneyStorageSql::fetchDocuments() const +{ + return fetchDocuments(QStringList(), false); +} + QMap MyMoneyStorageSql::fetchAccounts(const QStringList& idList, bool forUpdate) const { Q_D(const MyMoneyStorageSql); @@ -1997,6 +2221,23 @@ splitFilterActive = true; } + //documents + QStringList documents; + if (filter.documents(documents)) { + QString itemConnector = "splitId in ( SELECT splitId from kmmDocumentSplits where kmmDocumentSplits.transactionId = kmmSplits.transactionId and documentId in ("; + QString documentsClause = ""; + foreach (const QString& it, documents) { + documentsClause.append(QString("%1'%2'") + .arg(itemConnector).arg(it)); + itemConnector = ", "; + } + if (!documentsClause.isEmpty()) { + whereClause += subClauseconnector + documentsClause + ')'; + subClauseconnector = " and "; + } + splitFilterActive = true; + } + // accounts and categories if (!accounts.isEmpty()) { splitFilterActive = true; @@ -2622,6 +2863,19 @@ return d->getNextId<&MyMoneyStorageSqlPrivate::m_hiIdTags>(QLatin1String("kmmTags"), QLatin1String("id"), 1); } +ulong MyMoneyStorageSql::getNextDocumentTypeId() const +{ + Q_D(const MyMoneyStorageSql); + return d->getNextId<&MyMoneyStorageSqlPrivate::m_hiIdDocumentTypes>(QLatin1String("kmmDocumentTypes"), QLatin1String("id"), 1); +} + +ulong MyMoneyStorageSql::getNextDocumentId() const +{ + Q_D(const MyMoneyStorageSql); + return d->getNextId<&MyMoneyStorageSqlPrivate::m_hiIdDocuments>(QLatin1String("kmmDocuments"), QLatin1String("id"), 1); +} + + ulong MyMoneyStorageSql::getNextReportId() const { Q_D(const MyMoneyStorageSql); @@ -2705,6 +2959,20 @@ return (d->m_hiIdTags - 1); } +ulong MyMoneyStorageSql::incrementDocumentTypeId() +{ + Q_D(MyMoneyStorageSql); + d->m_hiIdDocumentTypes = getNextDocumentTypeId() + 1; + return (d->m_hiIdDocumentTypes - 1); +} + +ulong MyMoneyStorageSql::incrementDocumentId() +{ + Q_D(MyMoneyStorageSql); + d->m_hiIdDocuments = getNextDocumentId() + 1; + return (d->m_hiIdDocuments - 1); +} + ulong MyMoneyStorageSql::incrementReportId() { Q_D(MyMoneyStorageSql); @@ -2782,6 +3050,20 @@ d->writeFileInfo(); } +void MyMoneyStorageSql::loadDocumentTypeId(ulong id) +{ + Q_D(MyMoneyStorageSql); + d->m_hiIdDocumentTypes = id; + d->writeFileInfo(); +} + +void MyMoneyStorageSql::loadDocumentId(ulong id) +{ + Q_D(MyMoneyStorageSql); + d->m_hiIdDocuments = id; + d->writeFileInfo(); +} + void MyMoneyStorageSql::loadInstitutionId(ulong id) { Q_D(MyMoneyStorageSql); diff --git a/kmymoney/plugins/sql/mymoneystoragesql_p.h b/kmymoney/plugins/sql/mymoneystoragesql_p.h --- a/kmymoney/plugins/sql/mymoneystoragesql_p.h +++ b/kmymoney/plugins/sql/mymoneystoragesql_p.h @@ -7,6 +7,7 @@ : Fernando Vilas : Christian Dávid (C) 2017 by Łukasz Wojniłowicz + (C) 2018 by Marc Hübner ***************************************************************************/ /*************************************************************************** @@ -83,6 +84,8 @@ #include "onlinetasks/sepa/sepaonlinetransferimpl.h" #include "xmlstoragehelper.h" #include "mymoneyenums.h" +#include "mymoneydocumenttype.h" +#include "mymoneydocument.h" using namespace eMyMoney; @@ -215,6 +218,8 @@ m_budgets(0), m_onlineJobs(0), m_payeeIdentifier(0), + m_documentTypes(0), + m_documents(0), m_hiIdInstitutions(0), m_hiIdPayees(0), m_hiIdTags(0), @@ -227,6 +232,8 @@ m_hiIdOnlineJobs(0), m_hiIdPayeeIdentifier(0), m_hiIdCostCenter(0), + m_hiIdDocumentTypes(0), + m_hiIdDocuments(0), m_displayStatus(false), m_readingPrices(false), m_newDatabase(false), @@ -399,6 +406,82 @@ } } + void writeDocumentTypes() + { + Q_Q(MyMoneyStorageSql); + // first, get a list of what's on the database (see writeInstitutions) + QList dbList; + QSqlQuery query(*q); + query.prepare("SELECT id FROM kmmDocumentTypes;"); + if (!query.exec()) throw MYMONEYEXCEPTIONSQL("building document type list"); // krazy:exclude=crashy + while (query.next()) dbList.append(query.value(0).toString()); + + QList list = m_storage->documentTypeList(); + signalProgress(0, list.count(), "Writing Document types..."); + QSqlQuery query2(*q); + query.prepare(m_db.m_tables["kmmDocumentTypes"].updateString()); + query2.prepare(m_db.m_tables["kmmDocumentTypes"].insertString()); + foreach (const MyMoneyDocumentType& it, list) { + if (dbList.contains(it.id())) { + dbList.removeAll(it.id()); + writeDocumentType(it, query); + } else { + writeDocumentType(it, query2); + } + signalProgress(++m_documentTypes, 0); + } + + if (!dbList.isEmpty()) { + QVariantList deleteList; + // qCopy segfaults here, so do it with a hand-rolled loop + foreach (const QString& it, dbList) { + deleteList << it; + } + query.prepare(m_db.m_tables["kmmDocumentTypes"].deleteString()); + query.bindValue(":id", deleteList); + if (!query.execBatch()) throw MYMONEYEXCEPTIONSQL("deleting Document type"); + m_documentTypes -= query.numRowsAffected(); + } + } + + void writeDocuments() + { + Q_Q(MyMoneyStorageSql); + // first, get a list of what's on the database (see writeInstitutions) + QList dbList; + QSqlQuery query(*q); + query.prepare("SELECT id FROM kmmDocuments;"); + if (!query.exec()) throw MYMONEYEXCEPTIONSQL("building document list"); // krazy:exclude=crashy + while (query.next()) dbList.append(query.value(0).toString()); + + QList list = m_storage->documentList(); + signalProgress(0, list.count(), "Writing Documents..."); + QSqlQuery query2(*q); + query.prepare(m_db.m_tables["kmmDocuments"].updateString()); + query2.prepare(m_db.m_tables["kmmDocuments"].insertString()); + foreach (const MyMoneyDocument& it, list) { + if (dbList.contains(it.id())) { + dbList.removeAll(it.id()); + writeDocument(it, query); + } else { + writeDocument(it, query2); + } + signalProgress(++m_documents, 0); + } + + if (!dbList.isEmpty()) { + QVariantList deleteList; + // qCopy segfaults here, so do it with a hand-rolled loop + foreach (const QString& it, dbList) { + deleteList << it; + } + query.prepare(m_db.m_tables["kmmDocuments"].deleteString()); + query.bindValue(":id", deleteList); + if (!query.execBatch()) throw MYMONEYEXCEPTIONSQL("deleting Document"); + m_documents -= query.numRowsAffected(); + } + } + void writeAccounts() { Q_Q(MyMoneyStorageSql); @@ -720,6 +803,8 @@ "hiBudgetId = :hiBudgetId, " "hiOnlineJobId = :hiOnlineJobId, " "hiPayeeIdentifierId = :hiPayeeIdentifierId, " + "hiDocumentTypeId = :hiDocumentTypeId, " + "hiDocumentId = :hiDocumentId, " "encryptData = :encryptData, " "updateInProgress = :updateInProgress, " "logonUser = :logonUser, " @@ -738,7 +823,9 @@ "schedules = :schedules, " "reports = :reports, " "kvps = :kvps, " - "budgets = :budgets; " + "budgets = :budgets, " + "documentTypes = :documentTypes, " + "documents = :documents;" ) ); @@ -776,6 +863,8 @@ query.bindValue(":hiBudgetId", QVariant::fromValue(q->getNextBudgetId())); query.bindValue(":hiOnlineJobId", QVariant::fromValue(q->getNextOnlineJobId())); query.bindValue(":hiPayeeIdentifierId", QVariant::fromValue(q->getNextPayeeIdentifierId())); + query.bindValue(":hiDocumentTypeId", QVariant::fromValue(q->getNextDocumentTypeId())); + query.bindValue(":hiDocumentId", QVariant::fromValue(q->getNextDocumentId())); query.bindValue(":encryptData", m_encryptData); query.bindValue(":updateInProgress", "N"); @@ -797,6 +886,8 @@ query.bindValue(":reports", (unsigned long long) m_reports); query.bindValue(":kvps", (unsigned long long) m_kvps); query.bindValue(":budgets", (unsigned long long) m_budgets); + query.bindValue(":documentTypes", (unsigned long long) m_documentTypes); + query.bindValue(":documents", (unsigned long long) m_documents); if (!query.exec()) throw MYMONEYEXCEPTIONSQL("writing FileInfo"); // krazy:exclude=crashy @@ -1005,6 +1096,28 @@ m_hiIdTags = 0; } + void writeDocumentType(const MyMoneyDocumentType& dt, QSqlQuery& query) + { + query.bindValue(":id", dt.id()); + query.bindValue(":name", dt.name()); + query.bindValue(":description", dt.description()); + if (!query.exec()) throw MYMONEYEXCEPTIONSQL("writing Document type"); // krazy:exclude=crashy + m_hiIdDocumentTypes = 0; + } + + void writeDocument(const MyMoneyDocument& doc, QSqlQuery& query) + { + query.bindValue(":id", doc.id()); + query.bindValue(":name", doc.name()); + query.bindValue(":description", doc.description()); + query.bindValue(":documentdate", doc.documentDate().toString()); + query.bindValue(":documenttype", doc.documentTypeId()); + query.bindValue(":storagepath", doc.storagePath()); + query.bindValue(":hash", doc.hash()); + if (!query.exec()) throw MYMONEYEXCEPTIONSQL("writing Document"); // krazy:exclude=crashy + m_hiIdDocuments = 0; + } + void writeAccountList(const QList& accList, QSqlQuery& query) { //MyMoneyMoney balance = m_storagePtr->balance(acc.id(), QDate()); @@ -1162,12 +1275,16 @@ if (!insertList.isEmpty()) { writeSplitList(txId, insertList, type, insertIdList, query2); writeTagSplitsList(txId, insertList, insertIdList); + writeDocumentSplitsList(txId, insertList, insertIdList); } if (!updateList.isEmpty()) { writeSplitList(txId, updateList, type, updateIdList, query); deleteTagSplitsList(txId, updateIdList); writeTagSplitsList(txId, updateList, updateIdList); + + deleteDocumentSplitsList(txId, updateIdList); + writeDocumentSplitsList(txId, updateList, updateIdList); } if (!dbList.isEmpty()) { @@ -1213,6 +1330,32 @@ if (!query.execBatch()) throw MYMONEYEXCEPTIONSQL("writing tagSplits"); } + void writeDocumentSplitsList(const QString& txId, const QList& splitList, const QList& splitIdList) + { + Q_Q(MyMoneyStorageSql); + MyMoneyDbTransaction t(*q, Q_FUNC_INFO); + QVariantList docIdList; + QVariantList txIdList; + QVariantList splitIdList_DocumentSplits; + QVariantList docSplitsIdList; + + int i = 0, l = 0; + foreach (const MyMoneySplit& s, splitList) { + for (l = 0; l < s.documentIdList().size(); ++l) { + docIdList << s.documentIdList()[l]; + splitIdList_DocumentSplits << splitIdList[i]; + txIdList << txId; + } + i++; + } + QSqlQuery query(*q); + query.prepare(m_db.m_tables["kmmDocumentSplits"].insertString()); + query.bindValue(":documentId", docIdList); + query.bindValue(":splitId", splitIdList_DocumentSplits); + query.bindValue(":transactionId", txIdList); + if (!query.execBatch()) throw MYMONEYEXCEPTIONSQL("writing documentSplits"); + } + void writeSplitList (const QString& txId, const QList& splitList, @@ -1564,7 +1707,9 @@ " (SELECT count(*) FROM kmmReportConfig) AS reports, " " (SELECT count(*) FROM kmmBudgetConfig) AS budgets, " " (SELECT count(*) FROM kmmOnlineJobs) AS onlineJobs, " - " (SELECT count(*) FROM kmmPayeeIdentifier) AS payeeIdentifier " + " (SELECT count(*) FROM kmmPayeeIdentifier) AS payeeIdentifier, " + " (SELECT count(*) FROM kmmDocumentTypes) AS documentTypes, " + " (SELECT count(*) FROM kmmDocuments) AS documents " "FROM kmmFileInfo;" ); @@ -1592,7 +1737,9 @@ m_budgets = (ulong) GETULL(rec.indexOf("budgets")); m_onlineJobs = (ulong) GETULL(rec.indexOf("onlineJobs")); m_payeeIdentifier = (ulong) GETULL(rec.indexOf("payeeIdentifier")); - + m_documentTypes = (ulong)GETULL(rec.indexOf("documentTypes")); + m_documents = (ulong)GETULL(rec.indexOf("documents")); + m_encryptData = GETSTRING(rec.indexOf("encryptData")); m_logonUser = GETSTRING(rec.indexOf("logonUser")); m_logonAt = GETDATETIME(rec.indexOf("logonAt")); @@ -1662,15 +1809,25 @@ MyMoneySplit s; QList tagIdList; - QSqlQuery query1(*const_cast (q)); - query1.prepare("SELECT tagId from kmmTagSplits where splitId = :id and transactionId = :transactionId"); - query1.bindValue(":id", GETSTRING(splitIdCol)); - query1.bindValue(":transactionId", GETSTRING(transactionIdCol)); - if (!query1.exec()) throw MYMONEYEXCEPTIONSQL("reading tagId in Split"); // krazy:exclude=crashy - while (query1.next()) - tagIdList << query1.value(0).toString(); + QSqlQuery tagSplitsQuery(*const_cast (q)); + tagSplitsQuery.prepare("SELECT tagId from kmmTagSplits where splitId = :id and transactionId = :transactionId"); + tagSplitsQuery.bindValue(":id", GETSTRING(splitIdCol)); + tagSplitsQuery.bindValue(":transactionId", GETSTRING(transactionIdCol)); + if (!tagSplitsQuery.exec()) throw MYMONEYEXCEPTIONSQL("reading tagId in Split"); // krazy:exclude=crashy + while (tagSplitsQuery.next()) + tagIdList << tagSplitsQuery.value(0).toString(); + + QList docIdList; + QSqlQuery docSplitsQuery(*const_cast (q)); + docSplitsQuery.prepare("SELECT documentId from kmmDocumentSplits where splitId = :id and transactionId = :transactionId"); + docSplitsQuery.bindValue(":id", GETSTRING(splitIdCol)); + docSplitsQuery.bindValue(":transactionId", GETSTRING(transactionIdCol)); + if (!docSplitsQuery.exec()) throw MYMONEYEXCEPTIONSQL("reading documentId in Split"); // krazy:exclude=crashy + while (docSplitsQuery.next()) + docIdList << docSplitsQuery.value(0).toString(); s.setTagIdList(tagIdList); + s.setDocumentIdList(docIdList); s.setPayeeId(GETSTRING(payeeIdCol)); s.setReconcileDate(GETDATE(reconcileDateCol)); s.setAction(GETSTRING(actionCol)); @@ -1857,6 +2014,25 @@ if (!query.execBatch()) throw MYMONEYEXCEPTIONSQL("deleting tagSplits"); } + void deleteDocumentSplitsList(const QString& txId, const QList& splitIdList) + { + Q_Q(MyMoneyStorageSql); + MyMoneyDbTransaction t(*q, Q_FUNC_INFO); + QVariantList iList; + QVariantList transactionIdList; + + // qCopy segfaults here, so do it with a hand-rolled loop + foreach (int it_s, splitIdList) { + iList << it_s; + transactionIdList << txId; + } + QSqlQuery query(*q); + query.prepare("DELETE FROM kmmDocumentSplits WHERE transactionId = :transactionId AND splitId = :splitId"); + query.bindValue(":splitId", iList); + query.bindValue(":transactionId", transactionIdList); + if (!query.execBatch()) throw MYMONEYEXCEPTIONSQL("deleting documentSplits"); + } + void deleteSchedule(const QString& id) { Q_Q(MyMoneyStorageSql); @@ -2134,6 +2310,10 @@ if ((rc = upgradeToV12()) != 0) return (1); ++m_dbVersion; break; + case 12: + if ((rc = upgradeToV13()) != 0) return (1); + ++m_dbVersion; + break; default: qWarning("Unknown version number in database - %d", m_dbVersion); } @@ -2498,6 +2678,24 @@ return 0; } + int upgradeToV13() + { + Q_Q(MyMoneyStorageSql); + MyMoneyDbTransaction dbtrans(*q, Q_FUNC_INFO); + + + QSqlQuery query(*q); + + // add column roundingMethodCol to kmmSecurities + if (!alterTable(m_db.m_tables["kmmDocumentTypes"], m_dbVersion)) + return 1; + // add column pricePrecision to kmmCurrencies + if (!alterTable(m_db.m_tables["kmmDocuments"], m_dbVersion)) + return 1; + + return 0; + } + int createTables() { Q_Q(MyMoneyStorageSql); @@ -3212,7 +3410,8 @@ ulong m_budgets; ulong m_onlineJobs; ulong m_payeeIdentifier; - + ulong m_documentTypes; + ulong m_documents; // Cache for next id to use // value 0 means data is not available and has to be loaded from the database ulong m_hiIdInstitutions; @@ -3227,6 +3426,8 @@ ulong m_hiIdOnlineJobs; ulong m_hiIdPayeeIdentifier; ulong m_hiIdCostCenter; + ulong m_hiIdDocumentTypes; + ulong m_hiIdDocuments; // encrypt option - usage TBD QString m_encryptData; diff --git a/kmymoney/plugins/sql/tests/mymoneydatabasemgr-test.h b/kmymoney/plugins/sql/tests/mymoneydatabasemgr-test.h --- a/kmymoney/plugins/sql/tests/mymoneydatabasemgr-test.h +++ b/kmymoney/plugins/sql/tests/mymoneydatabasemgr-test.h @@ -116,6 +116,14 @@ void testAddOnlineJob(); void testModifyOnlineJob(); void testRemoveOnlineJob(); + void testAddDocumentType(); + void testModifyDocumentType(); + void testDocumentTypeName(); + void testRemoveDocumentType(); + void testAddDocument(); + void testModifyDocument(); + void testDocumentName(); + void testRemoveDocument(); void testHighestNumberFromIdString(); }; diff --git a/kmymoney/plugins/sql/tests/mymoneydatabasemgr-test.cpp b/kmymoney/plugins/sql/tests/mymoneydatabasemgr-test.cpp --- a/kmymoney/plugins/sql/tests/mymoneydatabasemgr-test.cpp +++ b/kmymoney/plugins/sql/tests/mymoneydatabasemgr-test.cpp @@ -3,6 +3,8 @@ ------------------- copyright : (C) 2008 by Fernando Vilas email : fvilas@iname.com + : (C) 2018 by Marc Hübner + ***************************************************************************/ /*************************************************************************** @@ -37,6 +39,8 @@ #include "mymoneysplit_p.h" #include "mymoneytransaction.h" #include "mymoneybudget.h" +#include "mymoneydocumenttype.h" +#include "mymoneydocument.h" #include "onlinetasks/dummy/tasks/dummytask.h" #include "misc/platformtools.h" @@ -124,6 +128,8 @@ QCOMPARE(m->tagList().count(), 0); QCOMPARE(m->scheduleList(QString(), Schedule::Type::Any, Schedule::Occurrence::Any, Schedule::PaymentType::Any, QDate(), QDate(), false).count(), 0); + QCOMPARE(m->documentTypeList().count(), 0); + QCOMPARE(m->documentList().count(), 0); QCOMPARE(m->creationDate(), QDate::currentDate()); } @@ -295,6 +301,8 @@ QCOMPARE(m->d_func()->nextTransactionID(), QLatin1String("T000000000000000001")); QCOMPARE(m->d_func()->nextPayeeID(), QLatin1String("P000001")); QCOMPARE(m->d_func()->nextTagID(), QLatin1String("G000001")); + QCOMPARE(m->d_func()->nextDocumentTypeID(), QLatin1String("DT000001")); + QCOMPARE(m->d_func()->nextDocumentID(), QLatin1String("DOC000000000000000001")); QCOMPARE(m->d_func()->nextScheduleID(), QLatin1String("SCH000001")); QCOMPARE(m->d_func()->nextReportID(), QLatin1String("R000001")); QCOMPARE(m->d_func()->nextOnlineJobID(), QLatin1String("O000001")); @@ -1679,6 +1687,241 @@ QVERIFY(m->tagList().count() == 1); } +void MyMoneyStorageMgrTest::testAddDocumentType() +{ + testAttachDb(); + + if (!m_canOpen) + QSKIP("Database test skipped because no database could be opened.", SkipAll); + + MyMoneyDocumentType dt; + + dt.setName("Hue"); + m->setDirty(); + try { + QVERIFY(m->d_func()->m_nextDocumentTypeID == 0); + MyMoneyFileTransaction ft; + m->addDocumentType(dt); + ft.commit(); + QVERIFY(m->d_func()->m_nextDocumentTypeID == 1); + MyMoneyDocumentType dt1 = m->documentTypeByName("Hue"); + QVERIFY(dt.id() == dt1.id()); + QVERIFY(dt.name() == dt1.name()); + QVERIFY(dt.description() == dt1.description()); + QVERIFY(dt == dt1); + } catch (const MyMoneyException &e) { + unexpectedException(e); + } +} + +void MyMoneyStorageMgrTest::testModifyDocumentType() +{ + testAttachDb(); + + if (!m_canOpen) + QSKIP("Database test skipped because no database could be opened.", SkipAll); + + MyMoneyDocumentType dt; + + testAddDocumentType(); + + dt = m->documentType("DT000001"); + dt.setName("New name"); + m->setDirty(); + try { + MyMoneyFileTransaction ft; + m->modifyDocumentType(dt); + ft.commit(); + dt = m->documentType("DT000001"); + QVERIFY(dt.name() == "New name"); + } catch (const MyMoneyException &e) { + unexpectedException(e); + } +} + +void MyMoneyStorageMgrTest::testRemoveDocumentType() +{ + testAttachDb(); + + if (!m_canOpen) + QSKIP("Database test skipped because no database could be opened.", SkipAll); + + testAddDocumentType(); + m->setDirty(); + + // check that we can remove an unreferenced document type + MyMoneyDocumentType dt = m->documentType("DT000001"); + try { + QVERIFY(m->documentTypeList().count() == 1); + MyMoneyFileTransaction ft; + m->removeDocumentType(dt); + ft.commit(); + QVERIFY(m->documentTypeList().count() == 0); + } catch (const MyMoneyException &e) { + unexpectedException(e); + } + + // add transaction + testAddDocument(); + + MyMoneyDocument doc = m->document("DOC000000000000000001"); + doc.setDocumentTypeId("DT000001"); + + // check that we cannot add a document referencing an unknown document type + try { + m->modifyDocument(doc); + QFAIL("Expected exception"); + } catch (const MyMoneyException &) { + } + + m->d_func()->m_nextDocumentTypeID = 0; // reset here, so that the testAddDocumentType will not fail + testAddDocumentType(); + + // check that it works when the document type exists + try { + MyMoneyFileTransaction ft; + m->modifyDocument(doc); + ft.commit(); + } catch (const MyMoneyException &e) { + qDebug(e.what()); + QFAIL("Unexpected exception"); + } + + m->d_func()->m_dirty = false; + + // now check, that we cannot remove the document type + try { + MyMoneyFileTransaction ft; + m->removeDocumentType(dt); + ft.commit(); + QFAIL("Expected exception"); + } catch (const MyMoneyException &) { + } + QVERIFY(m->documentTypeList().count() == 1); +} + +void MyMoneyStorageMgrTest::testAddDocument() +{ + testAttachDb(); + + if (!m_canOpen) + QSKIP("Database test skipped because no database could be opened.", SkipAll); + + MyMoneyDocument doc; + + doc.setName("Hue"); + m->setDirty(); + try { + QVERIFY(m->d_func()->m_nextDocumentID == 0); + MyMoneyFileTransaction ft; + m->addDocument(doc); + ft.commit(); + QVERIFY(m->d_func()->m_nextDocumentID == 1); + MyMoneyDocument doc1 = m->documentByName("Hue"); + QVERIFY(doc.id() == doc1.id()); + QVERIFY(doc.name() == doc1.name()); + QVERIFY(doc == doc1); + } catch (const MyMoneyException &e) { + unexpectedException(e); + } +} + +void MyMoneyStorageMgrTest::testModifyDocument() +{ + testAttachDb(); + + if (!m_canOpen) + QSKIP("Database test skipped because no database could be opened.", SkipAll); + + MyMoneyDocument doc; + + testAddDocument(); + testAddDocumentType(); + doc = m->document("DOC000000000000000001"); + doc.setName("New name"); + doc.setDocumentTypeId("DT000001"); + m->setDirty(); + try { + MyMoneyFileTransaction ft; + m->modifyDocument(doc); + ft.commit(); + doc = m->document("DOC000000000000000001"); + QVERIFY(doc.name() == "New name"); + } catch (const MyMoneyException &e) { + unexpectedException(e); + } +} + +void MyMoneyStorageMgrTest::testRemoveDocument() +{ + testAttachDb(); + + if (!m_canOpen) + QSKIP("Database test skipped because no database could be opened.", SkipAll); + + testAddDocument(); + m->setDirty(); + + // check that we can remove an unreferenced document + MyMoneyDocument doc= m->document("DOC000000000000000001"); + try { + QVERIFY(m->documentList().count() == 1); + MyMoneyFileTransaction ft; + m->removeDocument(doc); + ft.commit(); + QVERIFY(m->documentList().count() == 0); + } catch (const MyMoneyException &e) { + unexpectedException(e); + } + + // add transaction + testAddTransactions(); + + MyMoneyTransaction tr = m->transaction("T000000000000000001"); + MyMoneySplit sp; + sp = tr.splits()[0]; + QList documentIdList; + documentIdList << "DOC000000000000000001"; + sp.setDocumentIdList(documentIdList); + tr.modifySplit(sp); + + // check that we cannot add a transaction referencing + // an unknown document + try { + MyMoneyFileTransaction ft; + m->modifyTransaction(tr); + ft.commit(); + QFAIL("Expected exception"); + } catch (const MyMoneyException &) { + } + + // reset here, so that the + // testAddDocument will not fail + m->d_func()->m_nextDocumentID = 0; + testAddDocument(); + + // check that it works when the document exists + try { + MyMoneyFileTransaction ft; + m->modifyTransaction(tr); + ft.commit(); + } catch (const MyMoneyException &e) { + unexpectedException(e); + } + + m->setDirty(); + + // now check, that we cannot remove the document + try { + MyMoneyFileTransaction ft; + m->removeDocument(doc); + ft.commit(); + QFAIL("Expected exception"); + } catch (const MyMoneyException &) { + } + QVERIFY(m->documentList().count() == 1); +} + void MyMoneyStorageMgrTest::testRemoveAccountFromTree() { testAttachDb(); @@ -1794,6 +2037,66 @@ } } +void MyMoneyStorageMgrTest::testDocumentTypeName() +{ + testAttachDb(); + + if (!m_canOpen) + QSKIP("Database test skipped because no database could be opened.", SkipAll); + + testAddDocumentType(); + + MyMoneyDocumentType dt; + QString name("Hue"); + + // OK case + try { + dt = m->documentTypeByName(name); + QVERIFY(dt.name() == "Hue"); + QVERIFY(dt.id() == "DT000001"); + } catch (const MyMoneyException &e) { + unexpectedException(e); + } + + // Not OK case + name = "HUE"; + try { + dt = m->documentTypeByName(name); + QFAIL("Exception expected"); + } catch (const MyMoneyException &) { + } +} + +void MyMoneyStorageMgrTest::testDocumentName() +{ + testAttachDb(); + + if (!m_canOpen) + QSKIP("Database test skipped because no database could be opened.", SkipAll); + + testAddDocument(); + + MyMoneyDocument doc; + QString name("Hue"); + + // OK case + try { + doc = m->documentByName(name); + QVERIFY(doc.name() == "Hue"); + QVERIFY(doc.id() == "DOC000000000000000001"); + } catch (const MyMoneyException &e) { + unexpectedException(e); + } + + // Not OK case + name = "HUE"; + try { + doc = m->documentByName(name); + QFAIL("Exception expected"); + } catch (const MyMoneyException &) { + } +} + // disabled because of no real world use case //void MyMoneyStorageMgrTest::testAssignment() //{ diff --git a/kmymoney/plugins/xml/mymoneystorageanon.h b/kmymoney/plugins/xml/mymoneystorageanon.h --- a/kmymoney/plugins/xml/mymoneystorageanon.h +++ b/kmymoney/plugins/xml/mymoneystorageanon.h @@ -1,6 +1,7 @@ /* * Copyright 2004-2006 Ace Jones * Copyright 2005-2017 Thomas Baumgart + * Copyright 2018 Marc Hübner * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -81,6 +82,10 @@ void writeReport(QDomElement& reports, const MyMoneyReport& r) final override; + void writeDocumentType(QDomElement& doctype, const MyMoneyDocumentType& dt) final override; + + void writeDocument(QDomElement& document, const MyMoneyDocument& doc) final override; + void readFile(QIODevice* s, MyMoneyStorageMgr* storage) final override; void writeSecurity(QDomElement& securityElement, const MyMoneySecurity& security) final override; diff --git a/kmymoney/plugins/xml/mymoneystorageanon.cpp b/kmymoney/plugins/xml/mymoneystorageanon.cpp --- a/kmymoney/plugins/xml/mymoneystorageanon.cpp +++ b/kmymoney/plugins/xml/mymoneystorageanon.cpp @@ -1,7 +1,8 @@ /* * Copyright 2004-2006 Ace Jones * Copyright 2005-2017 Thomas Baumgart - * + * Copyright 2018 Marc Hübner + * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of @@ -45,6 +46,8 @@ #include "mymoneysecurity.h" #include "mymoneypayee.h" #include "mymoneytag.h" +#include "mymoneydocumenttype.h" +#include "mymoneydocument.h" #include "mymoneykeyvaluecontainer.h" #include "mymoneyexception.h" @@ -321,3 +324,22 @@ Q_UNUSED(onlineJobs); Q_UNUSED(job); } + +void MyMoneyStorageANON::writeDocumentType(QDomElement& doctype, const MyMoneyDocumentType& _dt) +{ + MyMoneyDocumentType dt(_dt); + + dt.setName(dt.id()); + dt.setDescription(hideString(dt.description())); + MyMoneyStorageXML::writeDocumentType(doctype, dt); +} + +void MyMoneyStorageANON::writeDocument(QDomElement& document, const MyMoneyDocument& _doc) +{ + MyMoneyDocument doc(_doc); + + doc.setName(doc.id()); + doc.setDescription(hideString(doc.description())); + doc.setStoragePath(hideString(doc.storagePath())); + MyMoneyStorageXML::writeDocument(document, doc); +} diff --git a/kmymoney/plugins/xml/mymoneystoragenames.h b/kmymoney/plugins/xml/mymoneystoragenames.h --- a/kmymoney/plugins/xml/mymoneystoragenames.h +++ b/kmymoney/plugins/xml/mymoneystoragenames.h @@ -1,5 +1,6 @@ /* * Copyright 2017-2018 Łukasz Wojniłowicz + * Copyright 2018 Marc Hübner * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -40,6 +41,8 @@ Reports, Budgets, OnlineJobs, + DocumentTypes, + Documents, KMMFile, FileInfo, User @@ -61,6 +64,8 @@ Report, Budget, OnlineJob, + DocumentType, + Document, KeyValuePairs, Equity }; @@ -85,7 +90,8 @@ Tag, Match, Container, - KeyValuePairs + KeyValuePairs, + Document }; enum class Account { @@ -230,7 +236,7 @@ // insert new entries above this line LastAttribute }; - + enum class Security { ID = 0, Name, @@ -305,6 +311,26 @@ // insert new entries above this line LastAttribute }; + + enum class DocumentType { + ID = 0, + Name, + Description, + // insert new entries above this line + LastAttribute + }; + + enum class Document { + ID = 0, + Name, + DocumentDate, + DocumentTypeId, + Checksum, + Description, + StoragePath, + // insert new entries above this line + LastAttribute + }; } QString elementName(Element::General elementID); @@ -340,6 +366,10 @@ QString attributeName(Attribute::CostCenter attributeID); +QString attributeName(Attribute::DocumentType attributeID); + +QString attributeName(Attribute::Document attributeID); + QString tagName(Tag tagID); QString nodeName(Node nodeID); diff --git a/kmymoney/plugins/xml/mymoneystoragenames.cpp b/kmymoney/plugins/xml/mymoneystoragenames.cpp --- a/kmymoney/plugins/xml/mymoneystoragenames.cpp +++ b/kmymoney/plugins/xml/mymoneystoragenames.cpp @@ -37,6 +37,8 @@ {Tag::Reports, QStringLiteral("REPORTS")}, {Tag::Budgets, QStringLiteral("BUDGETS")}, {Tag::OnlineJobs, QStringLiteral("ONLINEJOBS")}, + {Tag::DocumentTypes,QStringLiteral("DOCUMENTTYPES")}, + {Tag::Documents, QStringLiteral("DOCUMENTS")}, {Tag::KMMFile, QStringLiteral("KMYMONEY-FILE")}, {Tag::FileInfo, QStringLiteral("FILEINFO")}, {Tag::User, QStringLiteral("USER")} @@ -64,6 +66,8 @@ {Node::Report, QStringLiteral("REPORT")}, {Node::Budget, QStringLiteral("BUDGET")}, {Node::OnlineJob, QStringLiteral("ONLINEJOB")}, + {Node::DocumentType, QStringLiteral("DOCUMENTTYPE")}, + {Node::Document, QStringLiteral("DOCUMENT")}, {Node::KeyValuePairs, QStringLiteral("KEYVALUEPAIRS")}, {Node::Equity, QStringLiteral("EQUITY")}, }; @@ -94,6 +98,8 @@ uint qHash(const Institution key, uint seed) { return ::qHash(static_cast(key), seed); } uint qHash(const Schedule key, uint seed) { return ::qHash(static_cast(key), seed); } uint qHash(const OnlineJob key, uint seed) { return ::qHash(static_cast(key), seed); } + uint qHash(const DocumentType key, uint seed) { return ::qHash(static_cast(key), seed); } + uint qHash(const Document key, uint seed) { return ::qHash(static_cast(key), seed); } } QString elementName(Element::General elementID) @@ -162,7 +168,8 @@ {Element::Split::Tag, QStringLiteral("TAG")}, {Element::Split::Match, QStringLiteral("MATCH")}, {Element::Split::Container, QStringLiteral("CONTAINER")}, - {Element::Split::KeyValuePairs, QStringLiteral("KEYVALUEPAIRS")} + {Element::Split::KeyValuePairs, QStringLiteral("KEYVALUEPAIRS")}, + {Element::Split::Document, QStringLiteral("DOCUMENT")} }; return elementNames.value(elementID); } @@ -389,3 +396,25 @@ }; return attributeNames.value(attributeID); } + +QString attributeName(Attribute::DocumentType attributeID) +{ + static const QMap attributeNames{ + {Attribute::DocumentType::Name, QStringLiteral("name")}, + {Attribute::DocumentType::Description, QStringLiteral("description")}, + }; + return attributeNames.value(attributeID); +} + +QString attributeName(Attribute::Document attributeID) +{ + static const QMap attributeNames{ + {Attribute::Document::Name, QStringLiteral("name")}, + {Attribute::Document::DocumentDate, QStringLiteral("documentdate")}, + {Attribute::Document::DocumentTypeId, QStringLiteral("documenttypeid")}, + {Attribute::Document::Checksum, QStringLiteral("checksum")}, + {Attribute::Document::Description, QStringLiteral("description")}, + {Attribute::Document::StoragePath, QStringLiteral("storagePath")}, + }; + return attributeNames.value(attributeID); +} diff --git a/kmymoney/plugins/xml/mymoneystoragexml.h b/kmymoney/plugins/xml/mymoneystoragexml.h --- a/kmymoney/plugins/xml/mymoneystoragexml.h +++ b/kmymoney/plugins/xml/mymoneystoragexml.h @@ -4,6 +4,7 @@ * Copyright 2004-2005 Ace Jones * Copyright 2006 Darren Gould * Copyright 2017-2018 Łukasz Wojniłowicz + * Copyright 2018 Marc Hübner * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -58,6 +59,8 @@ class MyMoneyPrice; class MyMoneyTransaction; class MyMoneyCostCenter; +class MyMoneyDocumentType; +class MyMoneyDocument; class onlineJob; template class QList; @@ -147,6 +150,12 @@ virtual QDomElement writeKeyValuePairs(const QMap pairs); + virtual void writeDocumentTypes(QDomElement& documentTypes); + virtual void writeDocumentType(QDomElement& documentType, const MyMoneyDocumentType& docType); + + virtual void writeDocuments(QDomElement& documents); + virtual void writeDocument(QDomElement& document, const MyMoneyDocument& doc); + bool readUserInformation(const QDomElement& userElement); void readPricePair(const QDomElement& pricePair); diff --git a/kmymoney/plugins/xml/mymoneystoragexml.cpp b/kmymoney/plugins/xml/mymoneystoragexml.cpp --- a/kmymoney/plugins/xml/mymoneystoragexml.cpp +++ b/kmymoney/plugins/xml/mymoneystoragexml.cpp @@ -4,6 +4,7 @@ * Copyright 2004-2005 Ace Jones * Copyright 2006 Darren Gould * Copyright 2017-2018 Łukasz Wojniłowicz + * Copyright 2018 Marc Hübner * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -68,6 +69,8 @@ #include "payeeidentifier/nationalaccount/nationalaccount.h" #include "plugins/xmlhelper/xmlstoragehelper.h" #include "mymoneyenums.h" +#include "mymoneydocumenttype.h" +#include "mymoneydocument.h" using namespace eMyMoney; @@ -93,6 +96,8 @@ QMap onlineJobList; QMap prList; QMap ccList; + QMap dtList; + QMap docList; QString m_fromSecurity; QString m_toSecurity; @@ -181,6 +186,10 @@ static void writeOnlineJob(const onlineJob &job, QDomDocument &document, QDomElement &parent); static MyMoneyCostCenter readCostCenter(const QDomElement &node); static void writeCostCenter(const MyMoneyCostCenter &costCenter, QDomDocument &document, QDomElement &parent); + static MyMoneyDocumentType readDocumentType(const QDomElement &node); + static void writeDocumentType(const MyMoneyDocumentType &doctype, QDomDocument &document, QDomElement &parent); + static MyMoneyDocument readDocument(const QDomElement &node); + static void writeDocument(const MyMoneyDocument &mymoneydoc, QDomDocument &document, QDomElement &parent); }; MyMoneyXmlContentHandler::MyMoneyXmlContentHandler(MyMoneyStorageXML* reader) : @@ -240,7 +249,9 @@ || s == tagName(Tag::FileInfo) || s == tagName(Tag::User) || s == nodeName(Node::ScheduleTX) - || s == nodeName(Node::OnlineJob)) { + || s == nodeName(Node::OnlineJob) + || s == nodeName(Node::DocumentType) + || s == nodeName(Node::Document)) { m_baseNode = m_doc.createElement(qName); for (int i = 0; i < atts.count(); ++i) { m_baseNode.setAttribute(atts.qName(i), atts.value(i)); @@ -391,6 +402,16 @@ m_reader->d->ccList[c.id()] = c; } m_reader->signalProgress(++m_elementCount, 0); + } else if (s == nodeName(Node::DocumentType)) { + auto dt = readDocumentType(m_baseNode); + if (!dt.id().isEmpty()) + m_reader->d->dtList[dt.id()] = dt; + m_reader->signalProgress(++m_elementCount, 0); + } else if (s == nodeName(Node::Document)) { + auto doc = readDocument(m_baseNode); + if (!doc.id().isEmpty()) + m_reader->d->docList[doc.id()] = doc; + m_reader->signalProgress(++m_elementCount, 0); } else { m_errMsg = i18n("Unknown XML tag %1 found in line %2", qName, m_loc->lineNumber()); qWarning() << m_errMsg; @@ -461,6 +482,14 @@ m_reader->m_storage->loadCostCenters(m_reader->d->ccList); m_reader->d->ccList.clear(); m_reader->signalProgress(-1, -1); + } else if (s == tagName(Tag::DocumentTypes)) { + // last document type read, now dump them into the engine + m_reader->m_storage->loadDocumentTypes(m_reader->d->dtList); + m_reader->d->dtList.clear(); + } else if (s == tagName(Tag::Documents)) { + // last document read, now dump them into the engine + m_reader->m_storage->loadDocuments(m_reader->d->docList); + m_reader->d->docList.clear(); } } return rc; @@ -614,6 +643,12 @@ tagList << nodeList.item(i).toElement().attribute(attributeName(Attribute::Split::ID)); split.setTagIdList(tagList); + QList documentList; + QDomNodeList docNodeList = node.elementsByTagName(elementName(Element::Split::Document)); + for (auto i = 0; i < docNodeList.count(); ++i) + documentList << docNodeList.item(i).toElement().attribute(attributeName(Attribute::Split::ID)); + split.setDocumentIdList(documentList); + split.setReconcileDate(QDate::fromString(node.attribute(attributeName(Attribute::Split::ReconcileDate)), Qt::ISODate)); split.setAction(node.attribute(attributeName(Attribute::Split::Action))); split.setReconcileFlag(static_cast(node.attribute(attributeName(Attribute::Split::ReconcileFlag)).toInt())); @@ -671,6 +706,12 @@ el.appendChild(sel); } + for (const QString& documentId : split.documentIdList()) { + QDomElement sel = document.createElement(elementName(Element::Split::Document)); + sel.setAttribute(attributeName(Attribute::Split::ID), documentId); + el.appendChild(sel); + } + if (split.isMatched()) { QDomDocument docMatchedTransaction(elementName(Element::Split::Match)); QDomElement elMatchedTransaction = docMatchedTransaction.createElement(elementName(Element::Split::Container)); @@ -1339,10 +1380,69 @@ parent.appendChild(el); } +MyMoneyDocumentType MyMoneyXmlContentHandler::readDocumentType(const QDomElement& node) +{ + if (nodeName(Node::DocumentType) != node.tagName()) + throw MYMONEYEXCEPTION_CSTRING("Node was not DOCUMENTTYPE"); + MyMoneyDocumentType doctype(node.attribute(attributeName(Attribute::Account::ID))); + doctype.setName(node.attribute(attributeName(Attribute::DocumentType::Name))); + if (node.hasAttribute(attributeName(Attribute::DocumentType::Description))) + doctype.setDescription(node.attribute(attributeName(Attribute::DocumentType::Description))); + + return doctype; +} +void MyMoneyXmlContentHandler::writeDocumentType(const MyMoneyDocumentType &doctype, QDomDocument &document, QDomElement& parent) +{ + auto el = document.createElement(nodeName(Node::DocumentType)); + writeBaseXML(doctype.id(), document, el); + el.setAttribute(attributeName(Attribute::DocumentType::Name), doctype.name()); + el.setAttribute(attributeName(Attribute::DocumentType::Description), doctype.description()); + parent.appendChild(el); +} + +MyMoneyDocument MyMoneyXmlContentHandler::readDocument(const QDomElement& node) +{ + if (nodeName(Node::Document) != node.tagName()) + throw MYMONEYEXCEPTION_CSTRING("Node was not DOCUMENT"); + + MyMoneyDocument doc(node.attribute(attributeName(Attribute::Account::ID))); + + doc.setName(node.attribute(attributeName(Attribute::Document::Name))); + + if (node.hasAttribute(attributeName(Attribute::Document::DocumentDate))) + doc.setDocumentDate(QDate::fromString(node.attribute(attributeName(Attribute::Document::DocumentDate)), Qt::ISODate)); + + if (node.hasAttribute(attributeName(Attribute::Document::DocumentTypeId))) + doc.setDocumentTypeId(node.attribute(attributeName(Attribute::Document::DocumentTypeId))); + + if (node.hasAttribute(attributeName(Attribute::Document::Checksum))) + doc.setHash(node.attribute(attributeName(Attribute::Document::Checksum))); + + if (node.hasAttribute(attributeName(Attribute::Document::Description))) + doc.setDescription(node.attribute(attributeName(Attribute::Document::Description))); + + if (node.hasAttribute(attributeName(Attribute::Document::StoragePath))) + doc.setStoragePath(node.attribute(attributeName(Attribute::Document::StoragePath))); + + return doc; +} + +void MyMoneyXmlContentHandler::writeDocument(const MyMoneyDocument &mymoneydoc, QDomDocument& document, QDomElement& parent) +{ + auto el = document.createElement(nodeName(Node::Document)); + writeBaseXML(mymoneydoc.id(), document, el); + el.setAttribute(attributeName(Attribute::Document::Name), mymoneydoc.name()); + el.setAttribute(attributeName(Attribute::Document::DocumentDate), mymoneydoc.documentDate().toString(Qt::ISODate)); + el.setAttribute(attributeName(Attribute::Document::DocumentTypeId), mymoneydoc.documentTypeId()); + el.setAttribute(attributeName(Attribute::Document::Checksum), mymoneydoc.hash()); + el.setAttribute(attributeName(Attribute::Document::Description), mymoneydoc.description()); + el.setAttribute(attributeName(Attribute::Document::StoragePath), mymoneydoc.storagePath()); + parent.appendChild(el); +} MyMoneyStorageXML::MyMoneyStorageXML() : m_progressCallback(0), @@ -1487,6 +1587,14 @@ writeOnlineJobs(onlineJobs); mainElement.appendChild(onlineJobs); + QDomElement documentTypes = m_doc->createElement(tagName(Tag::DocumentTypes)); + writeDocumentTypes(documentTypes); + mainElement.appendChild(documentTypes); + + QDomElement documents = m_doc->createElement(tagName(Tag::Documents)); + writeDocuments(documents); + mainElement.appendChild(documents); + QTextStream stream(qf); stream.setCodec("UTF-8"); stream << m_doc->toString(); @@ -1888,6 +1996,32 @@ m_progressCallback = callback; } +void MyMoneyStorageXML::writeDocumentTypes(QDomElement& documentTypes) +{ + documentTypes.setAttribute(attributeName(Attribute::General::Count), m_storage->documentTypeList().count()); + for (const MyMoneyDocumentType& dt : m_storage->documentTypeList()) { + writeDocumentType(documentTypes, dt); + } +} + +void MyMoneyStorageXML::writeDocuments(QDomElement& documents) +{ + documents.setAttribute(attributeName(Attribute::General::Count), m_storage->documentList().count()); + for (const MyMoneyDocument& doc : m_storage->documentList()) { + writeDocument(documents, doc); + } +} + +void MyMoneyStorageXML::writeDocument(QDomElement& document, const MyMoneyDocument& doc) +{ + MyMoneyXmlContentHandler::writeDocument(doc, *m_doc, document); +} + +void MyMoneyStorageXML::writeDocumentType(QDomElement& documentType, const MyMoneyDocumentType& dt) +{ + MyMoneyXmlContentHandler::writeDocumentType(dt, *m_doc, documentType); +} + void MyMoneyStorageXML::signalProgress(int current, int total, const QString& msg) { if (m_progressCallback != 0) diff --git a/kmymoney/tips b/kmymoney/tips --- a/kmymoney/tips +++ b/kmymoney/tips @@ -136,3 +136,14 @@ + + +

+ ... that you can assign arbitrary documents (think invoices or bank statements) to transactions? + Simply create the document in the "Documents" view first; + then assign it to your transaction in the ledger using the "assign documents" button. +

+

Provided by Jose Jorge

+ +
+ diff --git a/kmymoney/views/CMakeLists.txt b/kmymoney/views/CMakeLists.txt --- a/kmymoney/views/CMakeLists.txt +++ b/kmymoney/views/CMakeLists.txt @@ -16,6 +16,7 @@ kpayeeidentifierview.cpp payeeidentifierselectiondelegate.cpp kmymoneywebpage.cpp + kdocumentsview.cpp ) if(ENABLE_UNFINISHEDFEATURES) @@ -42,6 +43,7 @@ kscheduledview.ui ktagsview.ui kpayeeidentifierview.ui + kdocumentsview.ui ) if(ENABLE_UNFINISHEDFEATURES) diff --git a/kmymoney/views/kdocumentsview.h b/kmymoney/views/kdocumentsview.h new file mode 100644 --- /dev/null +++ b/kmymoney/views/kdocumentsview.h @@ -0,0 +1,137 @@ +/* + * Copyright 2018 Marc Hübner + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + + +#ifndef KDOCUMENTSVIEW_H +#define KDOCUMENTSVIEW_H + + // ---------------------------------------------------------------------------- + // QT Includes +#include + // ---------------------------------------------------------------------------- + // KDE Includes + + // ---------------------------------------------------------------------------- + // Project Includes +#include "kmymoneyviewbase.h" + +class QListWidgetItem; +class KListWidgetSearchLine; +class MyMoneyObject; +class MyMoneyDocument; + +template class QList; + +class KDocumentsViewPrivate; +class KDocumentsView : public KMyMoneyViewBase +{ + Q_OBJECT + +public: + explicit KDocumentsView(QWidget *parent = nullptr); + ~KDocumentsView(); + + void updateDocumentActions(const QList& documents); + void executeCustomAction(eView::Action action) override; + +public Q_SLOTS: + void slotSelectDocumentAndTransaction(const QString& docId, const QString& accountId, const QString& transactionId); + void slotSelectDocumentAndTransaction(const QString& docId); + void slotStartRename(QListWidgetItem*); + void slotRenameButtonCliked(); + void slotHelp(); + + void refresh(); +protected: + void showEvent(QShowEvent* event) override; + void loadDocuments(); + void selectedDocuments(QList& documentsList) const; + void ensureDocumentVisible(const QString& id); + void clearItemData(); + +protected Q_SLOTS: + /** + * This method loads the m_transactionList, clears + * the m_TransactionPtrVector and rebuilds and sorts + * it according to the current settings. Then it + * loads the m_transactionView with the transaction data. + */ + void showTransactions(); + + /** + * This slot is called whenever the selection in m_documentsList + * is about to change. + */ + void slotSelectDocument(QListWidgetItem* cur, QListWidgetItem* prev); + + /** + * This slot is called whenever the selection in m_documentsList + * has been changed. + */ + void slotSelectDocument(); + + /** + * This slot marks the current selected document as modified (dirty). + */ + void slotDocumentDataChanged(); + + /** + * This slot is called when the name of a document is changed inside + * the document list view and only a single document is selected. + */ + void slotRenameSingleDocument(QListWidgetItem *doc); + + /** + * Updates the document data in m_document from the information in the + * document information widget. + */ + void slotUpdateDocument(); + + void slotSelectTransaction(); + + void slotChangeFilter(int index); + +Q_SIGNALS: + void transactionSelected(const QString& accountId, const QString& transactionId); + +private: + Q_DISABLE_COPY(KDocumentsView); + Q_DECLARE_PRIVATE(KDocumentsView); + void fillDoctypeList(); + +private Q_SLOTS: + /** + * This slot receives the signal from the listview control that an item was right-clicked, + * If @p points to a real document item, emits openContextMenu(). + * + * @param p position of the pointer device + */ + void slotShowDocumentsMenu(const QPoint& p); + + + void slotSelectDocuments(const QList& list); + + void slotNewDocument(); + void slotRenameDocument(); + void slotDeleteDocument(); + void slotOpenExternal(); + void slotChooseFile(); + QString getFileChecksum(const QString &file, const QCryptographicHash::Algorithm& hashAlgorithm); + void slotNewDocumentType(const QString& newnameBase, QString& id); +}; + +#endif diff --git a/kmymoney/views/kdocumentsview.cpp b/kmymoney/views/kdocumentsview.cpp new file mode 100644 --- /dev/null +++ b/kmymoney/views/kdocumentsview.cpp @@ -0,0 +1,741 @@ +/* + * Copyright 2018 Marc Hübner + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + + +// ---------------------------------------------------------------------------- +// QT Includes +#include +#include +#include +#include +#include +#include +#include +#include +#include +// ---------------------------------------------------------------------------- +// KDE Includes +#include +#include + +// ---------------------------------------------------------------------------- +// Project Includes +#include "mymoneyexception.h" +#include "mymoneyprice.h" +#include "kmymoneysettings.h" +#include "mymoneysecurity.h" +#include "mymoneysplit.h" +#include "mymoneytransaction.h" +#include "transaction.h" +#include "menuenums.h" +#include "mymoneydocumenttype.h" +#include "mymoneymoney.h" +#include "kmymoneyutils.h" +#include "kdocumentsview.h" +#include "kdocumentsview_p.h" + +using namespace Icons; + +/* -------------------------------------------------------------------------*/ +/* KTransactionPtrVector */ +/* -------------------------------------------------------------------------*/ + + +// *** KDocumentsView Implementation *** +KDocumentsView::KDocumentsView(QWidget *parent) : + KMyMoneyViewBase(*new KDocumentsViewPrivate(this), parent) +{ + typedef void(KDocumentsView::*KDocumentsViewFunc)(); + const QHash actionConnections{ + { eMenu::Action::NewDocument, &KDocumentsView::slotNewDocument }, + { eMenu::Action::RenameDocument, &KDocumentsView::slotRenameDocument }, + { eMenu::Action::DeleteDocument, &KDocumentsView::slotDeleteDocument } + }; + + for (auto a = actionConnections.cbegin(); a != actionConnections.cend(); ++a) + connect(pActions[a.key()], &QAction::triggered, this, a.value()); +} + +KDocumentsView::~KDocumentsView() +{ +} + +void KDocumentsView::executeCustomAction(eView::Action action) +{ + switch (action) { + case eView::Action::Refresh: + refresh(); + break; + + case eView::Action::SetDefaultFocus: + { + Q_D(KDocumentsView); + QTimer::singleShot(0, d->m_searchWidget, SLOT(setFocus())); + } + break; + + default: + break; + } +} + +void KDocumentsView::refresh() +{ + Q_D(KDocumentsView); + if (isVisible()) { + if (d->m_inSelection) + QTimer::singleShot(0, this, SLOT(refresh())); + else + loadDocuments(); + + d->m_needsRefresh = false; + } + else { + d->m_needsRefresh = true; + } +} + +void KDocumentsView::slotStartRename(QListWidgetItem* item) +{ + Q_D(KDocumentsView); + d->m_allowEditing = true; + d->ui->m_documentsList->editItem(item); +} + +// This variant is only called when a single document is selected and renamed. +void KDocumentsView::slotRenameSingleDocument(QListWidgetItem* doc) +{ + Q_D(KDocumentsView); + //if there is no current item selected, exit + if (d->m_allowEditing == false || !d->ui->m_documentsList->currentItem() || doc != d->ui->m_documentsList->currentItem()) { + return; + } + + // create a copy of the new name without appended whitespaces + auto new_name = doc->text(); + if (d->m_document.name() != new_name) { + MyMoneyFileTransaction ft; + try { + // check if we already have a document with the new name + try { + // this function call will throw an exception, if the document hasn't been found. + MyMoneyFile::instance()->documentByName(new_name); + // the name already exists, ask the user whether he's sure to keep the name + if (KMessageBox::questionYesNo(this, + i18n("A document with the name '%1' already exists. It is not advisable to have " + "multiple documents with the same name. Are you sure you would like " + "to rename the document?", new_name)) != KMessageBox::Yes) { + doc->setText(d->m_document.name()); + return; + } + } + catch (const MyMoneyException &) { + // all ok, the name is unique + } + + d->m_document.setName(new_name); + d->m_newName = new_name; + MyMoneyFile::instance()->modifyDocument(d->m_document); + + // the above call to modifyDocument will reload the view so + // all references and pointers to the view have to be + // re-established. + + // make sure, that the record is visible even if it moved + // out of sight due to the rename operation + ensureDocumentVisible(d->m_document.id()); + + ft.commit(); + + } + catch (const MyMoneyException &e) { + KMessageBox::detailedSorry(this, i18n("Unable to modify document"), QString::fromLatin1(e.what())); + } + } + else { + doc->setText(new_name); + } +} + +void KDocumentsView::ensureDocumentVisible(const QString& id) +{ + Q_D(KDocumentsView); + for (int i = 0; i < d->ui->m_documentsList->count(); ++i) { + KDocumentListItem* doc = dynamic_cast(d->ui->m_documentsList->item(0)); + if (doc && doc->document().id() == id) { + d->ui->m_documentsList->scrollToItem(doc, QAbstractItemView::PositionAtCenter); + d->ui->m_documentsList->setCurrentItem(doc); // active item and deselect all others + d->ui->m_documentsList->setCurrentRow(i, QItemSelectionModel::ClearAndSelect); // and select it + break; + } + } +} + +void KDocumentsView::selectedDocuments(QList& documentsList) const +{ + Q_D(const KDocumentsView); + for (QListWidgetItem* widgetItem : d->ui->m_documentsList->selectedItems()) { + KDocumentListItem* docItem = dynamic_cast(widgetItem); + if (docItem) { + documentsList << docItem->document(); + } + } +} + +void KDocumentsView::slotSelectDocument(QListWidgetItem* cur, QListWidgetItem* prev) +{ + Q_D(KDocumentsView); + Q_UNUSED(cur); + Q_UNUSED(prev); + + d->m_allowEditing = false; +} + +void KDocumentsView::slotSelectDocument() +{ + Q_D(KDocumentsView); + // check if the content of a currently selected document was modified + // and ask to store the data + if (d->ui->m_updateButton->isEnabled()) { + if (KMessageBox::questionYesNo(this, QString("%1").arg( + i18n("Do you want to save the changes for %1?", d->m_newName)), + i18n("Save changes")) == KMessageBox::Yes) { + d->m_inSelection = true; + slotUpdateDocument(); + d->m_inSelection = false; + } + } + // loop over all docments and count the number of documents, also obtain last selected document + QList documentsList; + selectedDocuments(documentsList); + slotSelectDocuments(documentsList); + + if (documentsList.isEmpty()) { + d->ui->m_tabWidget->setEnabled(false); // disable tab widget + d->ui->m_balanceLabel->hide(); + d->ui->m_deleteButton->setEnabled(false); //disable delete and rename button + d->ui->m_renameButton->setEnabled(false); + clearItemData(); + d->m_document = MyMoneyDocument(); + return; // make sure we don't access an undefined document + } + + d->ui->m_deleteButton->setEnabled(true); //re-enable delete button + + // if we have multiple documents selected, clear and disable the document information + if (documentsList.count() > 1) { + d->ui->m_tabWidget->setEnabled(false); // disable tab widget + d->ui->m_renameButton->setEnabled(false); // disable also the rename button + d->ui->m_balanceLabel->hide(); + clearItemData(); + } + else d->ui->m_renameButton->setEnabled(true); + + // otherwise we have just one selected, enable document information widget and renameButton + d->ui->m_tabWidget->setEnabled(true); + d->ui->m_balanceLabel->show(); + + // as of now we are updating only the last selected document, and until + // selection mode of the QListView has been changed to Extended, this + // will also be the only selection and behave exactly as before - Andreas + try { + d->m_document = documentsList[0]; + + MyMoneyFile *file = MyMoneyFile::instance(); + qDebug("doc type id '%s'", d->m_document.documentTypeId()); + + fillDoctypeList(); + + d->m_newName = d->m_document.name(); + d->ui->m_documentDate->setDate(d->m_document.documentDate()); + + d->ui->doctypeCombo->setCurrentIndex(d->ui->doctypeCombo->findData(d->m_document.documentTypeId())); + + d->ui->m_hash->setText(d->m_document.hash()); + d->ui->m_description->setPlainText(d->m_document.description()); + d->ui->m_storagePath->setText(d->m_document.storagePath()); + + slotDocumentDataChanged(); + + showTransactions(); + + } + catch (const MyMoneyException &e) { + qDebug("exception during display of document: %s", e.what()); + d->ui->m_register->clear(); + d->m_document = MyMoneyDocument(); + } + d->m_allowEditing = true; +} + +void KDocumentsView::clearItemData() +{ + Q_D(KDocumentsView); + d->ui->m_documentDate->clear(); + fillDoctypeList(); + d->ui->doctypeCombo->clear(); + d->ui->m_hash->setText(QString()); + d->ui->m_storagePath->setText(QString()); + d->ui->m_description->setPlainText(QString()); + showTransactions(); +} + +void KDocumentsView::showTransactions() +{ + Q_D(KDocumentsView); + MyMoneyMoney balance; + auto file = MyMoneyFile::instance(); + MyMoneySecurity base = file->baseCurrency(); + + // setup sort order + d->ui->m_register->setSortOrder(KMyMoneySettings::sortSearchView()); + + // clear the register + d->ui->m_register->clear(); + + if (d->m_document.id().isEmpty() || !d->ui->m_tabWidget->isEnabled()) { + d->ui->m_balanceLabel->setText(i18n("Balance: %1", balance.formatMoney(file->baseCurrency().smallestAccountFraction()))); + return; + } + + // setup the list and the pointer vector + MyMoneyTransactionFilter filter; + filter.addDocument(d->m_document.id()); + filter.setDateFilter(KMyMoneySettings::startDate().date(), QDate()); + + // retrieve the list from the engine + file->transactionList(d->m_transactionList, filter); + + // create the elements for the register + QList >::const_iterator it; + QMap uniqueMap; + MyMoneyMoney deposit, payment; + + int splitCount = 0; + bool balanceAccurate = true; + for (it = d->m_transactionList.constBegin(); it != d->m_transactionList.constEnd(); ++it) { + const MyMoneySplit& split = (*it).second; + MyMoneyAccount acc = file->account(split.accountId()); + ++splitCount; + uniqueMap[(*it).first.id()]++; + + KMyMoneyRegister::Register::transactionFactory(d->ui->m_register, (*it).first, (*it).second, uniqueMap[(*it).first.id()]); + + // take care of foreign currencies + MyMoneyMoney val = split.shares().abs(); + if (acc.currencyId() != base.id()) { + const MyMoneyPrice &price = file->price(acc.currencyId(), base.id()); + // in case the price is valid, we use it. Otherwise, we keep + // a flag that tells us that the balance is somewhat inaccurate + if (price.isValid()) { + val *= price.rate(base.id()); + } + else { + balanceAccurate = false; + } + } + + if (split.shares().isNegative()) { + payment += val; + } + else { + deposit += val; + } + } + balance = deposit - payment; + + // add the group markers + d->ui->m_register->addGroupMarkers(); + + // sort the transactions according to the sort setting + d->ui->m_register->sortItems(); + + // remove trailing and adjacent markers + d->ui->m_register->removeUnwantedGroupMarkers(); + + d->ui->m_register->updateRegister(true); + + // we might end up here with updates disabled on the register so + // make sure that we enable updates here + d->ui->m_register->setUpdatesEnabled(true); + d->ui->m_balanceLabel->setText(i18n("Balance: %1%2", + balanceAccurate ? "" : "~", + balance.formatMoney(file->baseCurrency().smallestAccountFraction()))); +} + +void KDocumentsView::slotDocumentDataChanged() +{ + Q_D(KDocumentsView); + bool rc = false; + // bool rc1, rc2, rc3, rc4, rc5, rc6 = false; + if (d->ui->m_tabWidget->isEnabled()) + { + const QDate& date = d->ui->m_documentDate->date(); + const QString& type = d->ui->doctypeCombo->currentData().toString(); + const QString& hash = d->ui->m_hash->text(); + const QString& storage = d->ui->m_storagePath->text(); + const QString& desc = d->ui->m_description->toPlainText(); + + + rc = (d->m_document.documentDate() != date) || + (d->m_document.documentTypeId() != type) || + (d->m_document.hash() != hash) || + (d->m_document.storagePath() != storage) || + (d->m_document.description() != desc); + } + d->ui->m_updateButton->setEnabled(rc); +} + +void KDocumentsView::slotNewDocumentType(const QString& newnameBase, QString& id) +{ + KMyMoneyUtils::newDocumentType(newnameBase, id); +} + +void KDocumentsView::slotUpdateDocument() +{ + Q_D(KDocumentsView); + if (d->ui->m_updateButton->isEnabled()) { + MyMoneyFileTransaction ft; + d->ui->m_updateButton->setEnabled(false); + try { + d->m_document.setName(d->m_newName); + d->m_document.setDocumentDate(d->ui->m_documentDate->date()); + d->m_document.setDocumentTypeId(d->ui->doctypeCombo->currentData().value()); + d->m_document.setHash(d->ui->m_hash->text()); + d->m_document.setDescription(d->ui->m_description->toPlainText()); + + if (MyMoneyFile::instance()->storeDocumentCopies() && !d->ui->m_storagePath->text().isEmpty()) { + auto storagePath = MyMoneyFile::instance()->documentStorageLocation(); + auto sourceFile = QFileInfo(d->ui->m_storagePath->text()); + + int suffix = 0; + QFileInfo targetFile; + do { + targetFile = QFileInfo(storagePath, d->m_document.id() + "_" + QString::number(suffix++) + "_" + sourceFile.fileName()); + } while (targetFile.exists()); + if (QFile::copy(sourceFile.absoluteFilePath(), targetFile.absoluteFilePath())) { + d->m_document.setStoragePath(targetFile.absoluteFilePath()); + d->ui->m_storagePath->setText(targetFile.absoluteFilePath()); + } else { + throw MYMONEYEXCEPTION(QString::fromLatin1("Cannot copy file '%1'").arg(sourceFile.absoluteFilePath())); + } + } else { + d->m_document.setStoragePath(d->ui->m_storagePath->text()); + } + + MyMoneyFile::instance()->modifyDocument(d->m_document); + ft.commit(); + } + catch (const MyMoneyException &e) { + KMessageBox::detailedSorry(0, i18n("Unable to modify document"), QString::fromLatin1(e.what())); + } + } +} + +void KDocumentsView::showEvent(QShowEvent* event) +{ + Q_D(KDocumentsView); + if (d->m_needLoad) { + d->init(); + } + + emit customActionRequested(View::Documents, eView::Action::AboutToShow); + + if (d->m_needsRefresh) { + refresh(); + } + + // don't forget base class implementation + QWidget::showEvent(event); + + QList list; + selectedDocuments(list); + slotSelectDocuments(list); +} + +void KDocumentsView::updateDocumentActions(const QList& docs) +{ + pActions[eMenu::Action::NewDocument]->setEnabled(true); + const auto documentsCount = docs.count(); + auto b = documentsCount == 1 ? true : false; + pActions[eMenu::Action::RenameDocument]->setEnabled(b); + b = documentsCount >= 1 ? true : false; + pActions[eMenu::Action::DeleteDocument]->setEnabled(b); +} + +void KDocumentsView::loadDocuments() +{ + Q_D(KDocumentsView); + if (d->m_inSelection) { + return; + } + + QMap isSelected; + QString id; + MyMoneyFile* file = MyMoneyFile::instance(); + + // remember which items are selected in the list + for (QListWidgetItem* item : d->ui->m_documentsList->selectedItems()) { + KDocumentListItem* docListItem = dynamic_cast(item); + isSelected[docListItem->document().id()] = true; + } + + // keep current selected item + KDocumentListItem *currentItem = static_cast(d->ui->m_documentsList->currentItem()); + if (currentItem) { + id = currentItem->document().id(); + } + + d->m_allowEditing = false; + // clear the list + d->m_searchWidget->clear(); + d->m_searchWidget->updateSearch(); + d->ui->m_documentsList->clear(); + d->ui->m_register->clear(); + currentItem = 0; + + for (const MyMoneyDocument& doc : file->documentList()) { + if (d->m_documentFilterType == (int)eView::Document::All || + (d->m_documentFilterType == (int)eView::Document::Referenced && file->isReferenced(doc)) || + (d->m_documentFilterType == (int)eView::Document::Unused && !file->isReferenced(doc))) { + KDocumentListItem* item = new KDocumentListItem(d->ui->m_documentsList, doc); + if (item->document().id() == id) + currentItem = item; + if (isSelected[item->document().id()]) + item->setSelected(true); + } + } + d->ui->m_documentsList->sortItems(); + + if (currentItem) { + d->ui->m_documentsList->setCurrentItem(currentItem); + d->ui->m_documentsList->scrollToItem(currentItem); + } + + slotSelectDocument(0, 0); + d->m_allowEditing = true; +} + +void KDocumentsView::slotSelectTransaction() +{ + Q_D(KDocumentsView); + QList list = d->ui->m_register->selectedItems(); + if (!list.isEmpty()) { + KMyMoneyRegister::Transaction* t = dynamic_cast(list[0]); + if (t) { + emit selectByVariant(QVariantList{ QVariant(t->split().accountId()), QVariant(t->transaction().id()) }, eView::Intent::ShowTransaction); + } + } +} + +void KDocumentsView::slotSelectDocumentAndTransaction(const QString& documentId, const QString& accountId, const QString& transactionId) +{ + if (!isVisible()) + return; + + Q_D(KDocumentsView); + try { + // clear filter + d->m_searchWidget->clear(); + d->m_searchWidget->updateSearch(); + + // deselect all other selected items + QList selectedItems = d->ui->m_documentsList->selectedItems(); + QList::const_iterator docsIt = selectedItems.constBegin(); + while (docsIt != selectedItems.constEnd()) { + KDocumentListItem* item = dynamic_cast(*docsIt); + if (item) + item->setSelected(false); + ++docsIt; + } + + // find the document in the list + QListWidgetItem* it; + for (int i = 0; i < d->ui->m_documentsList->count(); ++i) { + it = d->ui->m_documentsList->item(i); + KDocumentListItem* item = dynamic_cast(it); + if (item && item->document().id() == documentId) { + d->ui->m_documentsList->scrollToItem(it, QAbstractItemView::PositionAtCenter); + + d->ui->m_documentsList->setCurrentItem(it); // active item and deselect all others + d->ui->m_documentsList->setCurrentRow(i, QItemSelectionModel::ClearAndSelect); // and select it + + //make sure the document selection is updated and transactions are updated accordingly + slotSelectDocument(); + + KMyMoneyRegister::RegisterItem *registerItem = 0; + for (i = 0; i < d->ui->m_register->rowCount(); ++i) { + registerItem = d->ui->m_register->itemAtRow(i); + KMyMoneyRegister::Transaction* t = dynamic_cast(registerItem); + if (t) { + if (t->transaction().id() == transactionId && t->transaction().accountReferenced(accountId)) { + d->ui->m_register->selectItem(registerItem); + d->ui->m_register->ensureItemVisible(registerItem); + break; + } + } + } + // quit out of outer for() loop + break; + } + } + } + catch (const MyMoneyException &e) { + qWarning("Unexpected exception in KDocumentsView::slotSelectDocumentAndTransaction %s", e.what()); + } +} + +void KDocumentsView::slotSelectDocumentAndTransaction(const QString& documentId) +{ + slotSelectDocumentAndTransaction(documentId, QString(), QString()); +} + +void KDocumentsView::slotShowDocumentsMenu(const QPoint&) +{ + Q_D(KDocumentsView); + auto item = dynamic_cast(d->ui->m_documentsList->currentItem()); + if (item) { + slotSelectDocument(); + pMenus[eMenu::Menu::Document]->exec(QCursor::pos()); + } +} + +void KDocumentsView::slotHelp() +{ + KHelpClient::invokeHelp("details.documents.attributes"); +} + +void KDocumentsView::slotChangeFilter(int index) +{ + Q_D(KDocumentsView); + //update the filter type then reload the documents list + d->m_documentFilterType = index; + loadDocuments(); +} + +void KDocumentsView::slotRenameButtonCliked() +{ + Q_D(KDocumentsView); + if (d->ui->m_documentsList->currentItem() && d->ui->m_documentsList->selectedItems().count() == 1) { + slotStartRename(d->ui->m_documentsList->currentItem()); + } +} + +void KDocumentsView::slotSelectDocuments(const QList& list) +{ + Q_D(KDocumentsView); + d->m_selectedDocuments = list; + updateDocumentActions(list); +} + +void KDocumentsView::slotNewDocument() +{ + QString id; + KMyMoneyUtils::newDocument(i18n("New Document"), id); + slotSelectDocumentAndTransaction(id); +} + +void KDocumentsView::slotRenameDocument() +{ + Q_D(KDocumentsView); + if (d->ui->m_documentsList->currentItem() && d->ui->m_documentsList->selectedItems().count() == 1) { + slotStartRename(d->ui->m_documentsList->currentItem()); + } +} + +void KDocumentsView::slotDeleteDocument() +{ + Q_D(KDocumentsView); + if (d->m_selectedDocuments.isEmpty()) + return; // shouldn't happen + + const auto file = MyMoneyFile::instance(); + + // get confirmation from user + QString prompt; + if (d->m_selectedDocuments.size() == 1) + prompt = i18n("

Do you really want to remove the document %1?

", d->m_selectedDocuments.front().name()); + else + prompt = i18n("Do you really want to remove all selected documnets?"); + + if (KMessageBox::questionYesNo(this, prompt, i18n("Remove Document")) == KMessageBox::No) + return; + + bool deleteFiles = file->storeDocumentCopies() && KMessageBox::questionYesNo(this, i18n("KMyMoney is set up to store local copies of attached documents. Do you want those files to be deleted as well?")) == KMessageBox::Yes; + + MyMoneyFileTransaction ft; + try { + // create a transaction filter that contains all documents selected for removal + MyMoneyTransactionFilter f = MyMoneyTransactionFilter(); + for (const MyMoneyDocument& doc : d->m_selectedDocuments) { + f.addDocument(doc.id()); + } + + // request a list of all transactions that still use the document(s) in question + auto translist = file->transactionList(f); + + if (translist.count() > 0) { + throw MYMONEYEXCEPTION(QString::fromLatin1("Cannot delete a document that is still referenced by transactions")); + } + + for (const MyMoneyDocument& doc : d->m_selectedDocuments) { + if (deleteFiles && !doc.storagePath().isEmpty() && !QFile(doc.storagePath()).remove()) { + KMessageBox::information(this, i18n("Could not delete file '%1'.", doc.storagePath())); + } + file->removeDocument(doc); + } + ft.commit(); + slotSelectDocuments(QList()); + } catch (const MyMoneyException &e) { + KMessageBox::detailedSorry(this, i18n("Unable to remove document(s)"), QString::fromLatin1(e.what())); + } +} + +void KDocumentsView::fillDoctypeList() +{ + Q_D(const KDocumentsView); + d->ui->doctypeCombo->loadDocumentTypes(MyMoneyFile::instance()->documentTypeList()); +} + +void KDocumentsView::slotOpenExternal() { + Q_D(const KDocumentsView); + const QString& path = d->ui->m_storagePath->text(); + QDesktopServices::openUrl(QUrl::fromLocalFile(path)); +} + +void KDocumentsView::slotChooseFile() { + Q_D(KDocumentsView); + const QString& selectedFile = QFileDialog::getOpenFileName(this, i18n("Find Files")); + + if (QFileInfo(selectedFile).exists()) { + d->ui->m_storagePath->setText(selectedFile); + QString checksum = getFileChecksum(selectedFile, QCryptographicHash::Algorithm::Sha1); + d->ui->m_hash->setText(checksum); + } +} + +QString KDocumentsView::getFileChecksum(const QString &filename, const QCryptographicHash::Algorithm& hashAlgorithm) +{ + QFile f(filename); + if (f.open(QFile::ReadOnly)) { + QCryptographicHash hash(hashAlgorithm); + if (hash.addData(&f)) { + return QString(hash.result().toHex().toStdString().c_str()); + } + } + return QString(); +} diff --git a/kmymoney/views/kdocumentsview.ui b/kmymoney/views/kdocumentsview.ui new file mode 100644 --- /dev/null +++ b/kmymoney/views/kdocumentsview.ui @@ -0,0 +1,447 @@ + + + KDocumentsView + + + + 0 + 0 + 719 + 404 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + Qt::Horizontal + + + false + + + + + 1 + 1 + + + + Your documents + + + + + + + + New + + + + + + + Rename + + + + + + + Delete + + + + + + + + + + + + + + + + + + + + 1 + 1 + + + + true + + + QAbstractItemView::ExtendedSelection + + + + + + + + + 2 + 1 + + + + 0 + + + + Details + + + + + 10 + 50 + 118 + 32 + + + + Document date + + + + + + 10 + 90 + 118 + 32 + + + + Document type + + + + + + 130 + 130 + 251 + 32 + + + + + + + 10 + 130 + 118 + 32 + + + + Storage Path + + + + + + 130 + 50 + 291 + 32 + + + + + + + 130 + 90 + 291 + 32 + + + + true + + + QComboBox::InsertAtBottom + + + + + + 10 + 160 + 118 + 32 + + + + Hash + + + + + + 10 + 190 + 118 + 32 + + + + Description + + + + + + 130 + 190 + 291 + 61 + + + + + + + 130 + 160 + 291 + 32 + + + + Hash + + + + + + 130 + 260 + 291 + 34 + + + + Open in external aplication + + + + + + 380 + 130 + 41 + 34 + + + + ... + + + label + m_name + label_2 + label_3 + m_storagePath + label_4 + m_documentDate + m_documentType + label_5 + m_hash + label_6 + m_description + m_btnOpenExternal + m_selectFileBtn + + + + Transactions + + + + + + Transactions + + + + 6 + + + 11 + + + 11 + + + 11 + + + 11 + + + + + 6 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + + + + + + + + + + + + + Balance: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + false + + + + + + + + + + + + + + + + + + + + 0 + 0 + + + + Help + + + + + + + Qt::Horizontal + + + QSizePolicy::Expanding + + + + 260 + 21 + + + + + + + + + 0 + 0 + + + + + + + + + + + + + + + + + KComboBox + QComboBox +
kcombobox.h
+
+ + KMyMoneyRegister::Register + QTableWidget +
register.h
+
+
+ + m_newButton + m_renameButton + m_deleteButton + m_filterBox + m_documentsList + m_name + m_documentDate + m_documentType + m_storagePath + m_selectFileBtn + m_description + m_btnOpenExternal + m_updateButton + m_helpButton + m_tabWidget + m_register + + + ../widgets/kmymoneyaccountcombo.h + + + +
diff --git a/kmymoney/views/kdocumentsview_p.h b/kmymoney/views/kdocumentsview_p.h new file mode 100644 --- /dev/null +++ b/kmymoney/views/kdocumentsview_p.h @@ -0,0 +1,221 @@ +/* + * Copyright 2018 Marc Hübner + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + + +#ifndef KDOCUMENTSVIEW_P_H +#define KDOCUMENTSVIEW_P_H + +// ---------------------------------------------------------------------------- +// QT Includes + +#include + +// ---------------------------------------------------------------------------- +// KDE Includes + +#include +#include +#include +#include + +// ---------------------------------------------------------------------------- +// Project Includes +#include "kdocumentsview.h" +#include "ui_kdocumentsview.h" +#include "kmymoneyviewbase_p.h" +#include "mymoneyaccount.h" +#include "mymoneyfile.h" +#include "mymoneydocument.h" +#include "mymoneytransactionfilter.h" +#include "icons.h" +#include "viewenums.h" +#include "widgetenums.h" +#include "kdocumentlistitem.h" + +using namespace Icons; +namespace Ui { class KDocumentsView; } + +class KDocumentsViewPrivate : public KMyMoneyViewBasePrivate +{ + Q_DECLARE_PUBLIC(KDocumentsView) + +public: + explicit KDocumentsViewPrivate(KDocumentsView *qq) : + q_ptr(qq), + ui(new Ui::KDocumentsView), + m_needLoad(true), + m_searchWidget(nullptr), + m_inSelection(false), + m_allowEditing(true), + m_documentFilterType(0) + { + } + + ~KDocumentsViewPrivate() override + { + if (!m_needLoad) { + // remember the splitter settings for startup + KConfigGroup grp = KSharedConfig::openConfig()->group("Last Use Settings"); + grp.writeEntry("KDocumentsViewSplitterSize", ui->m_splitter->saveState()); + grp.sync(); + } + delete ui; + } + + void init() + { + Q_Q(KDocumentsView); + m_needLoad = false; + ui->setupUi(q); + + // create the searchline widget + // and insert it into the existing layout + m_searchWidget = new KListWidgetSearchLine(q, ui->m_documentsList); + m_searchWidget->setSizePolicy(QSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed)); + ui->m_documentsList->setContextMenuPolicy(Qt::CustomContextMenu); + ui->m_listTopHLayout->insertWidget(0, m_searchWidget); + + //load the filter type + ui->m_filterBox->addItem(i18nc("@item Show all documents", "All")); + ui->m_filterBox->addItem(i18nc("@item Show only used documents", "Used")); + ui->m_filterBox->addItem(i18nc("@item Show only unused documents", "Unused")); + ui->m_filterBox->setSizeAdjustPolicy(QComboBox::AdjustToContents); + + ui->m_newButton->setIcon(Icons::get(Icon::ListAddDocument)); + ui->m_renameButton->setIcon(Icons::get(Icon::EditRename)); + ui->m_deleteButton->setIcon(Icons::get(Icon::ListRemoveDocument)); + ui->m_updateButton->setIcon(Icons::get(Icon::DialogOK)); + ui->m_updateButton->setEnabled(false); + + ui->m_register->setupRegister(MyMoneyAccount(), + QList { eWidgets::eTransaction::Column::Date, + eWidgets::eTransaction::Column::Account, + eWidgets::eTransaction::Column::Detail, + eWidgets::eTransaction::Column::ReconcileFlag, + eWidgets::eTransaction::Column::Payment, + eWidgets::eTransaction::Column::Deposit + }); + ui->m_register->setSelectionMode(QTableWidget::SingleSelection); + ui->m_register->setDetailsColumnType(eWidgets::eRegister::DetailColumn::AccountFirst); + ui->m_balanceLabel->hide(); + + q->connect(ui->m_documentsList, &QListWidget::currentItemChanged, q, static_cast(&KDocumentsView::slotSelectDocument)); + q->connect(ui->m_documentsList, &QListWidget::itemSelectionChanged, q, static_cast(&KDocumentsView::slotSelectDocument)); + q->connect(ui->m_documentsList, &QListWidget::itemDoubleClicked, q, &KDocumentsView::slotStartRename); + q->connect(ui->m_documentsList, &QListWidget::itemChanged, q, &KDocumentsView::slotRenameSingleDocument); + q->connect(ui->m_documentsList, &QWidget::customContextMenuRequested, q, &KDocumentsView::slotShowDocumentsMenu); + + + q->connect(ui->m_newButton, &QAbstractButton::clicked, q, &KDocumentsView::slotNewDocument); + q->connect(ui->m_renameButton, &QAbstractButton::clicked, q, &KDocumentsView::slotRenameDocument); + q->connect(ui->m_deleteButton, &QAbstractButton::clicked, q, &KDocumentsView::slotDeleteDocument); + + + q->connect(ui->m_documentDate, &QDateEdit::dateChanged, q, &KDocumentsView::slotDocumentDataChanged); + q->connect(ui->doctypeCombo, &KMyMoneyDocumentTypeCombo::currentTextChanged, q, &KDocumentsView::slotDocumentDataChanged); + q->connect(ui->m_storagePath, &QLineEdit::textChanged, q, &KDocumentsView::slotDocumentDataChanged); + q->connect(ui->m_selectFileBtn, &QPushButton::clicked, q, &KDocumentsView::slotChooseFile); + q->connect(ui->m_description, &QPlainTextEdit::textChanged, q, &KDocumentsView::slotDocumentDataChanged); + + q->connect(ui->doctypeCombo, &KMyMoneyMVCCombo::createItem, q, &KDocumentsView::slotNewDocumentType); + + + q->connect(ui->m_updateButton, &QAbstractButton::clicked, q, &KDocumentsView::slotUpdateDocument); + q->connect(ui->m_btnOpenExternal, &QAbstractButton::clicked, q, &KDocumentsView::slotOpenExternal); + q->connect(ui->m_helpButton, &QAbstractButton::clicked, q, &KDocumentsView::slotHelp); + + q->connect(ui->m_register, &KMyMoneyRegister::Register::editTransaction, q, &KDocumentsView::slotSelectTransaction); + + q->connect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, q, &KDocumentsView::refresh); + + q->connect(ui->m_filterBox, static_cast(&QComboBox::currentIndexChanged), q, &KDocumentsView::slotChangeFilter); + + // use the size settings of the last run (if any) + auto grp = KSharedConfig::openConfig()->group("Last Use Settings"); + ui->m_splitter->restoreState(grp.readEntry("KDocumentsViewSplitterSize", QByteArray())); + ui->m_splitter->setChildrenCollapsible(false); + + // At start we haven't any document selected + ui->m_tabWidget->setEnabled(false); // disable tab widget + ui->m_deleteButton->setEnabled(false); // disable delete and rename button + ui->m_renameButton->setEnabled(false); + m_document = MyMoneyDocument(); // make sure we don't access an undefined document + q->clearItemData(); + } + + /** + * Check if a list contains a document with a given id + * + * @param list const reference to value list + * @param id const reference to id + * + * @retval true object has been found + * @retval false object is not in list + */ + bool documentInList(const QList& list, const QString& id) const + { + bool rc = false; + for (const MyMoneyDocument& currentDocument : list) { + if (currentDocument.id() == id) { + rc = true; + break; + } + } + return rc; + } + + + KDocumentsView *q_ptr; + Ui::KDocumentsView *ui; + + MyMoneyDocument m_document; + QString m_newName; + + /** + * This member holds a list of all transactions + */ + QList > m_transactionList; + + /** + * This member holds the load state of page + */ + bool m_needLoad; + + /** + * Search widget for the list + */ + KListWidgetSearchLine* m_searchWidget; + + /** + * Semaphore to suppress loading during selection + */ + bool m_inSelection; + + /** + * This signals whether a document can be edited + **/ + bool m_allowEditing; + + /** + * This holds the filter type + */ + int m_documentFilterType; + + QList m_selectedDocuments; +}; + +#endif //KDOCUMENTSVIEW_P_H \ No newline at end of file diff --git a/kmymoney/views/kgloballedgerview.h b/kmymoney/views/kgloballedgerview.h --- a/kmymoney/views/kgloballedgerview.h +++ b/kmymoney/views/kgloballedgerview.h @@ -263,6 +263,8 @@ void slotCancelTransaction(); void slotEditSplits(); + void slotAssignDocuments(); + /** * This method takes the selected splits and checks that only one transaction (src) * has more than one split and all others have only a single one. It then copies the diff --git a/kmymoney/views/kgloballedgerview.cpp b/kmymoney/views/kgloballedgerview.cpp --- a/kmymoney/views/kgloballedgerview.cpp +++ b/kmymoney/views/kgloballedgerview.cpp @@ -30,6 +30,7 @@ #include #include #include +#include // ---------------------------------------------------------------------------- // KDE Includes @@ -100,6 +101,7 @@ {Action::PostponeReconciliation, &KGlobalLedgerView::slotPostponeReconciliation}, {Action::OpenAccount, &KGlobalLedgerView::slotOpenAccount}, {Action::EditFindTransaction, &KGlobalLedgerView::slotFindTransaction}, + {Action::AssignDocuments, &KGlobalLedgerView::slotAssignDocuments}, }; for (auto a = actionConnections.cbegin(); a != actionConnections.cend(); ++a) @@ -276,7 +278,7 @@ Action::CancelTransaction, Action::DeleteTransaction, Action::MatchTransaction, Action::AcceptTransaction, Action::DuplicateTransaction, Action::ToggleReconciliationFlag, Action::MarkCleared, Action::GoToAccount, Action::GoToPayee, Action::AssignTransactionsNumber, Action::NewScheduledTransaction, - Action::CombineTransactions, Action::CopySplits, + Action::CombineTransactions, Action::CopySplits, Action::AssignDocuments, }; for (const auto& a : actionsToBeDisabled) @@ -357,6 +359,10 @@ pActions[Action::MatchTransaction]->setIcon(QIcon("process-stop")); } + if (d->m_selectedTransactions.count() == 1) { + pActions[Action::AssignDocuments]->setEnabled(true); + } + if (d->m_selectedTransactions.count() > 1) { pActions[Action::CombineTransactions]->setEnabled(true); } @@ -1601,6 +1607,11 @@ } } +void KGlobalLedgerView::slotAssignDocuments() { + Q_D(KGlobalLedgerView); + d->showDocumentAssignmentFrame(d->m_selectedTransactions.first().transaction(), d->m_currentAccount.id()); +} + void KGlobalLedgerView::slotCancelTransaction() { Q_D(KGlobalLedgerView); diff --git a/kmymoney/views/kgloballedgerview_p.h b/kmymoney/views/kgloballedgerview_p.h --- a/kmymoney/views/kgloballedgerview_p.h +++ b/kmymoney/views/kgloballedgerview_p.h @@ -5,6 +5,7 @@ copyright : (C) 2006 by Thomas Baumgart email : Thomas Baumgart (C) 2017 by Łukasz Wojniłowicz + (C) 2018 by Marc Hübner ***************************************************************************/ /*************************************************************************** @@ -34,7 +35,8 @@ #include #include #include - +#include +#include // ---------------------------------------------------------------------------- // KDE Includes @@ -82,6 +84,7 @@ #include "mymoneyenums.h" #include "modelenums.h" #include "menuenums.h" +#include "kmymoneydocumentassignmentwidget.h" #include #ifdef KMM_DEBUG @@ -256,7 +259,8 @@ m_balanceWarning(nullptr), m_moveToAccountSelector(nullptr), m_endingBalanceDlg(nullptr), - m_searchDlg(nullptr) + m_searchDlg(nullptr), + m_documentAssignmentFrame(nullptr) { } @@ -348,13 +352,21 @@ m_buttonbar->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); buttonLayout->addWidget(m_buttonbar); + // create the document assignment frame (popup) + m_documentAssignmentFrame = new QFrame(nullptr); + m_documentAssignmentFrame->setWindowFlags(Qt::Popup); + m_documentAssignmentFrame->setFrameStyle(QFrame::Panel | QFrame::Raised); + m_documentManagementWidget = new KMyMoneyDocumentAssignmentWidget(m_documentAssignmentFrame); + m_documentAssignmentFrame->hide(); + m_buttonbar->addAction(pActions[eMenu::Action::NewTransaction]); m_buttonbar->addAction(pActions[eMenu::Action::DeleteTransaction]); m_buttonbar->addAction(pActions[eMenu::Action::EditTransaction]); m_buttonbar->addAction(pActions[eMenu::Action::EnterTransaction]); m_buttonbar->addAction(pActions[eMenu::Action::CancelTransaction]); m_buttonbar->addAction(pActions[eMenu::Action::AcceptTransaction]); m_buttonbar->addAction(pActions[eMenu::Action::MatchTransaction]); + m_buttonbar->addAction(pActions[eMenu::Action::AssignDocuments]); // create the transaction form frame m_formFrame = new QFrame(q); @@ -393,6 +405,33 @@ m_tooltipPosn = QPoint(); } + void showDocumentAssignmentFrame(const MyMoneyTransaction& transaction, const QString& accountId) { + Q_Q(KGlobalLedgerView); + + auto h = m_documentManagementWidget->height(); + auto w = m_documentManagementWidget->width(); + + // usually, the document assignment widget is shown underneath the corresponding button frame + // if it does not fit on the screen, we show it above instead + auto p = q->mapToGlobal(QPoint(m_buttonFrame->x(), m_buttonFrame->y())); + if (p.y() + q->height() + h > QApplication::desktop()->height()) + p.setY(p.y() - h + 0); + else + p.setY(p.y() + q->height()); + + // usually, it is shown left aligned. If it does not fit, we align it + // to the right edge of the widget + if (p.x() + w > QApplication::desktop()->width()) + p.setX(p.x() + q->width() - w); + + QRect r = m_documentManagementWidget->geometry(); + r.moveTopLeft(p); + m_documentAssignmentFrame->setGeometry(r); + m_documentAssignmentFrame->show(); + m_documentManagementWidget->setFocus(); + m_documentManagementWidget->refresh(transaction, accountId); + } + /** * This method reloads the account selection combo box of the * view with all asset and liability accounts from the engine. @@ -1592,10 +1631,12 @@ QFrame* m_buttonFrame; QFrame* m_formFrame; QFrame* m_summaryFrame; + QFrame* m_documentAssignmentFrame; // widgets - KMyMoneyRegister::Register* m_register; - KToolBar* m_buttonbar; + KMyMoneyRegister::Register* m_register; + KToolBar* m_buttonbar; + KMyMoneyDocumentAssignmentWidget* m_documentManagementWidget; /** * This member holds the currently selected account diff --git a/kmymoney/views/kmymoneyview.h b/kmymoney/views/kmymoneyview.h --- a/kmymoney/views/kmymoneyview.h +++ b/kmymoney/views/kmymoneyview.h @@ -4,6 +4,7 @@ copyright : (C) 2000-2001 by Michael Edwardes 2004 by Thomas Baumgart (C) 2017, 2018 by Łukasz Wojniłowicz + (C) 2018 by Marc Hübner ***************************************************************************/ /*************************************************************************** @@ -75,6 +76,7 @@ class MyMoneyObject; class QLabel; class KMyMoneyViewBase; +class KDocumentsView; /** * This class represents the view of the MyMoneyFile which contains @@ -212,6 +214,16 @@ */ void slotTagSelected(const QString& tagId, const QString& accountId, const QString& transactionId); + /** + * Called, whenever the documents view should pop up and a specific + * transaction in an account should be shown. + * + * @param tagId The ID of the document to be shown + * @param accountId The ID of the account to be shown + * @param transactionId The ID of the transaction to be selected + */ + void slotDocumentSelected(const QString& documentId, const QString& accountId, const QString& transactionId); + /** * This slot prints the current view. */ @@ -257,6 +269,7 @@ void slotShowForecastPage(); void slotShowOutboxPage(); void switchToDefaultView(); + void slotShowDocumentsPage(); /** * Opens object in ledgers or edits in case of institution diff --git a/kmymoney/views/kmymoneyview.cpp b/kmymoney/views/kmymoneyview.cpp --- a/kmymoney/views/kmymoneyview.cpp +++ b/kmymoney/views/kmymoneyview.cpp @@ -4,6 +4,7 @@ copyright : (C) 2000-2001 by Michael Edwardes 2004 by Thomas Baumgart (C) 2017, 2018 by Łukasz Wojniłowicz + (C) 2018 by Marc Hübner ***************************************************************************/ @@ -69,6 +70,7 @@ #include "kscheduledview.h" #include "kgloballedgerview.h" #include "kinvestmentview.h" +#include "kdocumentsview.h" #include "models.h" #include "accountsmodel.h" #include "equitiesmodel.h" @@ -135,6 +137,7 @@ viewBases[View::Payees] = new KPayeesView; viewBases[View::Ledgers] = new KGlobalLedgerView; viewBases[View::Investments] = new KInvestmentView; + viewBases[View::Documents] = new KDocumentsView; #ifdef ENABLE_UNFINISHEDFEATURES viewBases[View::NewLedgers] = new SimpleLedgerView; #endif @@ -157,6 +160,7 @@ {View::Payees, i18n("Payees"), Icon::ViewPayees}, {View::Ledgers, i18n("Ledgers"), Icon::ViewLedgers}, {View::Investments, i18n("Investments"), Icon::ViewInvestment}, + {View::Documents, i18n("Documents"), Icon::ViewDocuments}, #ifdef ENABLE_UNFINISHEDFEATURES {View::NewLedgers, i18n("New ledger"), Icon::DocumentProperties}, #endif @@ -295,6 +299,11 @@ showPageAndFocus(View::OnlineJobOutbox); } +void KMyMoneyView::slotShowDocumentsPage() +{ + showPageAndFocus(View::Documents); +} + void KMyMoneyView::showTitleBar(bool show) { if (m_header) @@ -468,7 +477,8 @@ {Action::ShowReportsView, &KMyMoneyView::slotShowReportsPage, i18n("Show reports page"), Qt::CTRL + Qt::Key_9}, {Action::ShowBudgetView, &KMyMoneyView::slotShowBudgetPage, i18n("Show budget page"), }, {Action::ShowForecastView, &KMyMoneyView::slotShowForecastPage, i18n("Show forecast page"), }, - {Action::ShowOnlineJobOutboxView, &KMyMoneyView::slotShowOutboxPage, i18n("Show outbox page") } + {Action::ShowOnlineJobOutboxView, &KMyMoneyView::slotShowOutboxPage, i18n("Show outbox page") }, + {Action::ShowDocumentsView, &KMyMoneyView::slotShowDocumentsPage, i18n("Show documents page"), }, }; QHash lutActions; @@ -553,6 +563,12 @@ static_cast(viewBases[View::Tags])->slotSelectTagAndTransaction(tag, account, transaction); } +void KMyMoneyView::slotDocumentSelected(const QString& document, const QString& account, const QString& transaction) +{ + showPage(View::Documents); + static_cast(viewBases[View::Documents])->slotSelectDocumentAndTransaction(document, account, transaction); +} + void KMyMoneyView::finishReconciliation(const MyMoneyAccount& /* account */) { Models::instance()->accountsModel()->slotReconcileAccount(MyMoneyAccount(), QDate(), MyMoneyMoney()); diff --git a/kmymoney/views/viewenums.h b/kmymoney/views/viewenums.h --- a/kmymoney/views/viewenums.h +++ b/kmymoney/views/viewenums.h @@ -20,7 +20,7 @@ #include enum class View { Home = 0, Institutions, Accounts, Schedules, Categories, Tags, - Payees, Ledgers, Investments, Reports, Budget, Forecast, OnlineJobOutbox, NewLedgers, None }; + Payees, Ledgers, Investments, Reports, Budget, Forecast, OnlineJobOutbox, NewLedgers, Documents, None }; inline uint qHash(const View key, uint seed) { return ::qHash(static_cast(key), seed); } @@ -70,6 +70,11 @@ ShowBalanceChart }; + enum class Document { + All = 0, + Referenced, + Unused + }; } #endif diff --git a/kmymoney/widgets/CMakeLists.txt b/kmymoney/widgets/CMakeLists.txt --- a/kmymoney/widgets/CMakeLists.txt +++ b/kmymoney/widgets/CMakeLists.txt @@ -59,6 +59,8 @@ budgetviewproxymodel.cpp kmymoneyviewbase.cpp kmymoneyaccountsviewbase.cpp + kmymoneydocumenttypecombo.cpp + kmymoneydocumentassignmentwidget.cpp ) set(nationalAccountWidget_SOURCES @@ -101,6 +103,7 @@ ktransactionfilter.ui ./payeeidentifier/nationalaccount/nationalaccountedit.ui ./payeeidentifier/ibanbic/ibanbicitemedit.ui + kmymoneydocumentassignmentwidget.ui ) add_library(kmm_widgets SHARED ${kmm_widgets_sources}) diff --git a/kmymoney/widgets/kdocumentlistitem.h b/kmymoney/widgets/kdocumentlistitem.h new file mode 100644 --- /dev/null +++ b/kmymoney/widgets/kdocumentlistitem.h @@ -0,0 +1,57 @@ +/* + * Copyright 2018 Marc Hübner + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef KDOCUMENTLISTITEM_H +#define KDOCUMENTLISTITEM_H + +class MyMoneyDocument; + +class KDocumentListItem : public QListWidgetItem +{ +public: + /** + * Constructor to be used to construct a document entry object. + * + * @param parent pointer to the QListWidget object this entry should be + * added to. + * @param doc const reference to MyMoneyDocument for which + * the QListWidget entry is constructed + */ + explicit KDocumentListItem(QListWidget *parent, const MyMoneyDocument& doc) : + QListWidgetItem(parent, QListWidgetItem::UserType), + m_document(doc) + { + setText(doc.name()); + // allow in column rename + setFlags(Qt::ItemIsEditable | Qt::ItemIsSelectable | Qt::ItemIsEnabled); + } + + ~KDocumentListItem() {} + + MyMoneyDocument document() const { + return m_document; + } + + MyMoneyDocument data(int role) { + return m_document; + } + +private: + MyMoneyDocument m_document; +}; + +#endif //KDOCUMENTLISTITEM_H \ No newline at end of file diff --git a/kmymoney/widgets/kmymoney.widgets b/kmymoney/widgets/kmymoney.widgets --- a/kmymoney/widgets/kmymoney.widgets +++ b/kmymoney/widgets/kmymoney.widgets @@ -73,3 +73,7 @@ [KMyMoneyAccountCombo] ToolTip=The KMyMoney widget that displays accounts + +[KMyMoneyDocumentTypeCombo] +ToolTip=The KMyMoney widget that displays document types +IncludeFile=kmymoneymvccombo.h diff --git a/kmymoney/widgets/kmymoneydocumentassignmentwidget.h b/kmymoney/widgets/kmymoneydocumentassignmentwidget.h new file mode 100644 --- /dev/null +++ b/kmymoney/widgets/kmymoneydocumentassignmentwidget.h @@ -0,0 +1,60 @@ +/* + * Copyright 2018 Marc Hübner + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef KMYMONEYDOCUMENTASSIGNMENTWIDGET_H +#define KMYMONEYDOCUMENTASSIGNMENTWIDGET_H + + // ---------------------------------------------------------------------------- + // QT Includes +#include + +// ---------------------------------------------------------------------------- +// Project Includes +#include "kmm_widgets_export.h" + +class KMyMoneyDocumentAssignmentWidgetPrivate; +class MyMoneyTransaction; +class MyMoneySplit; + +class KMM_WIDGETS_EXPORT KMyMoneyDocumentAssignmentWidget : public QFrame +{ + Q_OBJECT + Q_DISABLE_COPY(KMyMoneyDocumentAssignmentWidget) + +protected Q_SLOTS: + void slotAssignSelectedDocuments(); + void slotUnassignSelectedDocuments(); + void slotRevertDocumentAssignment(); + void slotConfirmDocumentAssignment(); + void slotOpenAvailableDocument(); + void slotOpenAssignedDocument(); + +public: + explicit KMyMoneyDocumentAssignmentWidget(QFrame* parent = nullptr); + ~KMyMoneyDocumentAssignmentWidget(); + + void refresh(const MyMoneyTransaction& transaction, const QString& accountId); + +private: + KMyMoneyDocumentAssignmentWidgetPrivate * const d_ptr; + Q_DECLARE_PRIVATE(KMyMoneyDocumentAssignmentWidget); + + void loadDocuments(const MyMoneyTransaction& transaction, const QString& accountId); + void openInExternalViewer(const QString& path) const; + +}; +#endif //KMYMONEYDOCUMENTASSIGNMENTWIDGET_H \ No newline at end of file diff --git a/kmymoney/widgets/kmymoneydocumentassignmentwidget.cpp b/kmymoney/widgets/kmymoneydocumentassignmentwidget.cpp new file mode 100644 --- /dev/null +++ b/kmymoney/widgets/kmymoneydocumentassignmentwidget.cpp @@ -0,0 +1,190 @@ +/* + * Copyright 2018 Marc Hübner + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + + + // ---------------------------------------------------------------------------- + // QT Includes +#include +#include +#include + +// ---------------------------------------------------------------------------- +// Project Includes +#include "mymoneykeyvaluecontainer.h" +#include "mymoneyobject.h" +#include "kmm_mymoney_export.h" +#include "mymoneyunittestable.h" +#include "kmymoneydocumentassignmentwidget.h" +#include "mymoneyfile.h" +#include "ui_kmymoneydocumentassignmentwidget.h" +#include "mymoneydocument.h" +#include "kdocumentlistitem.h" +#include "mymoneysplit.h" +#include "mymoneytransaction.h" + +class MyMoneyTransactionPrivate; + +class KMyMoneyDocumentAssignmentWidgetPrivate +{ + Q_DISABLE_COPY(KMyMoneyDocumentAssignmentWidgetPrivate) + Q_DECLARE_PUBLIC(KMyMoneyDocumentAssignmentWidget) +public: + explicit KMyMoneyDocumentAssignmentWidgetPrivate(KMyMoneyDocumentAssignmentWidget *qq) : + q_ptr(qq), + ui(new Ui::KMyMoneyDocumentAssignmentWidget) + { + } + + ~KMyMoneyDocumentAssignmentWidgetPrivate() + { + delete ui; + } + + void init() + { + Q_Q(KMyMoneyDocumentAssignmentWidget); + + q->connect(ui->m_assignDocumentButton, &QAbstractButton::clicked, q, &KMyMoneyDocumentAssignmentWidget::slotAssignSelectedDocuments); + q->connect(ui->m_unassignDocumentButton, &QAbstractButton::clicked, q, &KMyMoneyDocumentAssignmentWidget::slotUnassignSelectedDocuments); + q->connect(ui->m_cancelButton, &QAbstractButton::clicked, q, &KMyMoneyDocumentAssignmentWidget::slotRevertDocumentAssignment); + + m_searchWidget = new KListWidgetSearchLine(q, ui->m_availableDocuments); + m_searchWidget->setSizePolicy(QSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed)); + ui->m_listTopHLayout->insertWidget(0, m_searchWidget); + + q->connect(ui->m_okButton, &QAbstractButton::clicked, q, &KMyMoneyDocumentAssignmentWidget::slotConfirmDocumentAssignment); + + q->connect(ui->m_availableDocuments, &QListWidget::doubleClicked, q, &KMyMoneyDocumentAssignmentWidget::slotOpenAvailableDocument); + q->connect(ui->m_assignedDocuments, &QListWidget::doubleClicked, q, &KMyMoneyDocumentAssignmentWidget::slotOpenAssignedDocument); + } + + KMyMoneyDocumentAssignmentWidget *q_ptr; + Ui::KMyMoneyDocumentAssignmentWidget *ui; + + MyMoneyTransaction m_transaction; + QString m_accountId; + + /** + * Search widget for the list + */ + KListWidgetSearchLine* m_searchWidget; +}; + + +void KMyMoneyDocumentAssignmentWidget::slotConfirmDocumentAssignment() { + Q_D(KMyMoneyDocumentAssignmentWidget); + + auto transaction = d->m_transaction; + auto split = transaction.splitByAccount(d->m_accountId, true); + + QList docIdList; + + for (QListWidgetItem* item : d->ui->m_assignedDocuments->findItems(QStringLiteral("*"), Qt::MatchWildcard)) { + auto docId = dynamic_cast(item)->data(0).id(); + docIdList.append(docId); + } + split.setDocumentIdList(docIdList); + transaction.modifySplit(split); + MyMoneyFileTransaction ft; + const auto file = MyMoneyFile::instance(); + file->modifyTransaction(transaction); + file->commitTransaction(); + this->parentWidget()->hide(); +} + +void KMyMoneyDocumentAssignmentWidget::slotOpenAvailableDocument() { + Q_D(KMyMoneyDocumentAssignmentWidget); + + const auto path = (dynamic_cast(d->ui->m_availableDocuments->selectedItems().first())->data(0)).storagePath(); + openInExternalViewer(path); +} + +void KMyMoneyDocumentAssignmentWidget::slotOpenAssignedDocument() { + Q_D(KMyMoneyDocumentAssignmentWidget); + + const auto path = (dynamic_cast(d->ui->m_assignedDocuments->selectedItems().first())->data(0)).storagePath(); + openInExternalViewer(path); +} + +void KMyMoneyDocumentAssignmentWidget::slotAssignSelectedDocuments() { + Q_D(KMyMoneyDocumentAssignmentWidget); + + for (QListWidgetItem* item : d->ui->m_availableDocuments->selectedItems()) { + KDocumentListItem* item2 = new KDocumentListItem(d->ui->m_assignedDocuments, dynamic_cast(item)->data(0)); + delete(d->ui->m_availableDocuments->takeItem(d->ui->m_availableDocuments->row(item))); + item2->setSelected(true); + } + d->ui->m_assignedDocuments->sortItems(); +} + +void KMyMoneyDocumentAssignmentWidget::slotUnassignSelectedDocuments() { + Q_D(KMyMoneyDocumentAssignmentWidget); + for (QListWidgetItem* item : d->ui->m_assignedDocuments->selectedItems()) { + KDocumentListItem* item2 = new KDocumentListItem(d->ui->m_availableDocuments, dynamic_cast(item)->data(0)); + delete(d->ui->m_assignedDocuments->takeItem(d->ui->m_assignedDocuments->row(item))); + item2->setSelected(true); + } + d->ui->m_availableDocuments->sortItems(); +} + +void KMyMoneyDocumentAssignmentWidget::slotRevertDocumentAssignment() { + Q_D(KMyMoneyDocumentAssignmentWidget); + loadDocuments(d->m_transaction, d->m_accountId); + this->parentWidget()->hide(); +} + +void KMyMoneyDocumentAssignmentWidget::refresh(const MyMoneyTransaction& transcation, const QString& accountId) { + loadDocuments(transcation, accountId); +} + +KMyMoneyDocumentAssignmentWidget::KMyMoneyDocumentAssignmentWidget(QFrame *parent) : + QFrame(parent), + d_ptr(new KMyMoneyDocumentAssignmentWidgetPrivate(this)) +{ + Q_D(KMyMoneyDocumentAssignmentWidget); + d->ui->setupUi(this); + d->init(); +} + +KMyMoneyDocumentAssignmentWidget::~KMyMoneyDocumentAssignmentWidget() +{ + Q_D(KMyMoneyDocumentAssignmentWidget); + delete d; +} + +void KMyMoneyDocumentAssignmentWidget::loadDocuments(const MyMoneyTransaction& transaction, const QString& accountId) +{ + Q_D(KMyMoneyDocumentAssignmentWidget); + + d->m_transaction = transaction; + d->m_accountId = accountId; + + d->ui->m_assignedDocuments->clear(); + d->ui->m_availableDocuments->clear(); + + // load contents of assigned documents and available documents list widgets + for (const MyMoneyDocument& doc : MyMoneyFile::instance()->documentList()) { + if (transaction.splitByAccount(accountId, true).documentIdList().contains(doc.id())) + KDocumentListItem* item = new KDocumentListItem(d->ui->m_assignedDocuments, doc); + else + KDocumentListItem* item = new KDocumentListItem(d->ui->m_availableDocuments, doc); + } +} + +void KMyMoneyDocumentAssignmentWidget::openInExternalViewer(const QString& path) const { + QDesktopServices::openUrl(QUrl::fromLocalFile(path)); +} \ No newline at end of file diff --git a/kmymoney/widgets/kmymoneydocumentassignmentwidget.ui b/kmymoney/widgets/kmymoneydocumentassignmentwidget.ui new file mode 100644 --- /dev/null +++ b/kmymoney/widgets/kmymoneydocumentassignmentwidget.ui @@ -0,0 +1,132 @@ + + + KMyMoneyDocumentAssignmentWidget + + + + 0 + 0 + 1081 + 717 + + + + Frame + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + + QAbstractItemView::NoEditTriggers + + + QListView::Static + + + QListView::Fixed + + + QListView::ListMode + + + + + + + + + + 10 + + + 10 + + + + + Qt::Vertical + + + + + + + < + + + + + + + > + + + + + + + Qt::Vertical + + + + + + + + + QAbstractItemView::NoEditTriggers + + + + + + + + + Qt::Vertical + + + + + + + &Okay + + + + + + + &Cancel + + + + + + + Qt::Vertical + + + + + + + + + + KComboBox + QWidget +
kcombobox.h
+
+
+ + +
diff --git a/kmymoney/widgets/kmymoneydocumenttypecombo.h b/kmymoney/widgets/kmymoneydocumenttypecombo.h new file mode 100644 --- /dev/null +++ b/kmymoney/widgets/kmymoneydocumenttypecombo.h @@ -0,0 +1,67 @@ +/* + * Copyright 2018 Marc Hübner + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef KMYMONEYDOCUMENTTYPECOMBO_H +#define KMYMONEYDOCUMENTTYPECOMBO_H + +// ---------------------------------------------------------------------------- +// QT Includes + +// ---------------------------------------------------------------------------- +// KDE Includes + +// ---------------------------------------------------------------------------- +// Project Includes + +#include "kmymoneymvccombo.h" + +class MyMoneyDocumentType; + +/** + * This class implements a text based document type selector. + * The widget has the functionality of a KMyMoneyPayeeCombo object. + * Whenever a key is pressed, the set of loaded document types is searched for + * document type names which match the currently entered text. + * + * @author Alessandro Russo + */ +class KMyMoneyDocumentTypeComboPrivate; +class KMM_WIDGETS_EXPORT KMyMoneyDocumentTypeCombo : public KMyMoneyMVCCombo +{ + Q_OBJECT + Q_DISABLE_COPY(KMyMoneyDocumentTypeCombo) + +public: + explicit KMyMoneyDocumentTypeCombo(QWidget* parent = nullptr); + ~KMyMoneyDocumentTypeCombo() override; + + void loadDocumentTypes(const QList& list); + /** ids in usedIdList are excluded from the internal list + * you should call loadDocumentTypes before calling setUsedDocumentTypeList because it doesn't readd + * document types which have been removed in a previous call */ + void setUsedDocumentTypeList(QList& usedIdList, QList& usedDocumentTypeNameList); + +protected: + /** + * check if the current text is contained in the internal list, if not ask the user if want to create a new item. + */ + virtual void checkCurrentText() override; +private: + Q_DECLARE_PRIVATE(KMyMoneyDocumentTypeCombo) +}; + +#endif //KMYMONEYDOCUMENTTYPECOMBO_H diff --git a/kmymoney/widgets/kmymoneydocumenttypecombo.cpp b/kmymoney/widgets/kmymoneydocumenttypecombo.cpp new file mode 100644 --- /dev/null +++ b/kmymoney/widgets/kmymoneydocumenttypecombo.cpp @@ -0,0 +1,114 @@ +/* + * Copyright 2018 Marc Hübner + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "kmymoneydocumenttypecombo.h" +#include "kmymoneymvccombo_p.h" + +// ---------------------------------------------------------------------------- +// QT Includes + +// ---------------------------------------------------------------------------- +// KDE Includes + +#include +#include + +// ---------------------------------------------------------------------------- +// Project Includes + +#include "mymoneydocumenttype.h" + +class KMyMoneyDocumentTypeComboPrivate : public KMyMoneyMVCComboPrivate +{ + Q_DISABLE_COPY(KMyMoneyDocumentTypeComboPrivate) + +public: + KMyMoneyDocumentTypeComboPrivate() : + KMyMoneyMVCComboPrivate() + { + } + + QList m_usedIdList; + QList m_usedDocumentTypeNameList; +}; + +KMyMoneyDocumentTypeCombo::KMyMoneyDocumentTypeCombo(QWidget* parent) : + KMyMoneyMVCCombo(*new KMyMoneyDocumentTypeComboPrivate, true, parent) +{ + setPlaceholderText(i18n("Select document type")); +} + +KMyMoneyDocumentTypeCombo::~KMyMoneyDocumentTypeCombo() +{ +} + +void KMyMoneyDocumentTypeCombo::loadDocumentTypes(const QList& list) +{ + Q_D(KMyMoneyDocumentTypeCombo); + clear(); + + for (const MyMoneyDocumentType dt : list) { + addItem(dt.name(), QVariant(dt.id())); + } + + //sort the model, which will sort the list in the combo + model()->sort(Qt::DisplayRole, Qt::AscendingOrder); + + //set the text to empty and the index to the first item on the list + setCurrentIndex(0); + clearEditText(); + setPlaceholderText(i18n("Select document type")); +} + +void KMyMoneyDocumentTypeCombo::setUsedDocumentTypeList(QList& usedIdList, QList& usedDocumentTypeNameList) +{ + Q_D(KMyMoneyDocumentTypeCombo); + d->m_usedIdList = usedIdList; + d->m_usedDocumentTypeNameList = usedDocumentTypeNameList; + for (auto i = 0; i < d->m_usedIdList.size(); ++i) { + int index = findData(QVariant(d->m_usedIdList.at(i)), Qt::UserRole, Qt::MatchExactly); + if (index != -1) removeItem(index); + } +} + +void KMyMoneyDocumentTypeCombo::checkCurrentText() +{ + Q_D(KMyMoneyDocumentTypeCombo); + if (!contains(currentText())) { + if (d->m_usedDocumentTypeNameList.contains(currentText())) { + // Tell the user what happened + QString msg = QString("") + i18n("The document type is already present.") + QString(""); + KMessageBox::information(this, msg, i18n("Duplicate document type"), "Duplicate document type"); + setCurrentText(); + return; + } + QString id; + // annouce that we go into a possible dialog to create an object + // This can be used by upstream widgets to disable filters etc. + emit objectCreation(true); + + emit createItem(currentText(), id); + + // Announce that we return from object creation + emit objectCreation(false); + + // update the field to a possibly created object + //m_id = id; + addEntry(currentText(), id); + setCurrentTextById(id); + } +} diff --git a/kmymoney/wizards/newuserwizard/CMakeLists.txt b/kmymoney/wizards/newuserwizard/CMakeLists.txt --- a/kmymoney/wizards/newuserwizard/CMakeLists.txt +++ b/kmymoney/wizards/newuserwizard/CMakeLists.txt @@ -8,13 +8,15 @@ kgeneralpage.cpp kintropage.cpp kpreferencepage.cpp + kdocumentpage.cpp ) set (libnewuserwizard_a_UI kaccountpage.ui kcurrencypage.ui kgeneralpage.ui kintropage.ui kpreferencepage.ui kpasswordpage.ui + kdocumentpage.ui ) # The handling of these ui files depends diff --git a/kmymoney/wizards/newuserwizard/kcategoriespage.cpp b/kmymoney/wizards/newuserwizard/kcategoriespage.cpp --- a/kmymoney/wizards/newuserwizard/kcategoriespage.cpp +++ b/kmymoney/wizards/newuserwizard/kcategoriespage.cpp @@ -34,7 +34,7 @@ #include "kaccounttemplateselector.h" #include "knewuserwizard.h" #include "knewuserwizard_p.h" -#include "kpreferencepage.h" +#include "kdocumentpage.h" #include "wizardpage_p.h" #include "mymoneytemplate.h" @@ -64,7 +64,7 @@ KMyMoneyWizardPage* CategoriesPage::nextPage() const { Q_D(const CategoriesPage); - return d->m_wizard->d_func()->m_preferencePage; + return d->m_wizard->d_func()->m_documentPage; } QList CategoriesPage::selectedTemplates() const diff --git a/kmymoney/wizards/newuserwizard/kdocumentpage.h b/kmymoney/wizards/newuserwizard/kdocumentpage.h new file mode 100644 --- /dev/null +++ b/kmymoney/wizards/newuserwizard/kdocumentpage.h @@ -0,0 +1,64 @@ +/* + * Copyright 2018 Marc Hübner + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef KDOCUMENTPAGE_H +#define KDOCUMENTPAGE_H + + // ---------------------------------------------------------------------------- + // QT Includes + +#include + +// ---------------------------------------------------------------------------- +// Project Includes + +#include "wizardpage.h" + +class KMyMoneyWizardPage; + +namespace NewUserWizard +{ + class Wizard; + + /** + * Wizard page collecting information about document management settings + */ + class DocumentPagePrivate; + class DocumentPage : public QWidget, public WizardPage + { + Q_OBJECT + Q_DISABLE_COPY(DocumentPage) + + public: + explicit DocumentPage(Wizard* parent); + ~DocumentPage() override; + + KMyMoneyWizardPage* nextPage() const override; + + bool isComplete() const override; + + protected Q_SLOTS: + void slotSelectFolder(); + void slotToggleFolderSelection(); + + private: + Q_DECLARE_PRIVATE_D(WizardPage::d_ptr, DocumentPage) + friend class Wizard; + }; +} // namespace + +#endif //KDOCUMENTPAGE_H diff --git a/kmymoney/wizards/newuserwizard/kdocumentpage.cpp b/kmymoney/wizards/newuserwizard/kdocumentpage.cpp new file mode 100644 --- /dev/null +++ b/kmymoney/wizards/newuserwizard/kdocumentpage.cpp @@ -0,0 +1,87 @@ +/* + * Copyright 2018 Marc Hübner + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "kdocumentpage.h" +#include "kdocumentpage_p.h" + + // ---------------------------------------------------------------------------- + // QT Includes + +#include +#include + +// ---------------------------------------------------------------------------- +// KDE Includes + +#include + +// ---------------------------------------------------------------------------- +// Project Includes + +#include "ui_kdocumentpage.h" + +#include "knewuserwizard.h" +#include "knewuserwizard_p.h" +#include "kpreferencepage.h" +#include "wizardpage.h" +#include "kguiutils.h" + +namespace NewUserWizard +{ + DocumentPage::DocumentPage(Wizard* wizard) : + QWidget(wizard), + WizardPage(*new DocumentPagePrivate(wizard), stepCount++, this, wizard) + { + Q_D(DocumentPage); + d->ui->setupUi(this); + connect(d->ui->m_selectFolderButton, &QAbstractButton::clicked, this, &DocumentPage::slotSelectFolder); + connect(d->ui->m_storeCopies, &QCheckBox::clicked, this, &DocumentPage::slotToggleFolderSelection); + d->ui->m_storagePath->setText(QStandardPaths::writableLocation(QStandardPaths::StandardLocation::AppLocalDataLocation)); + d->m_mandatoryGroup->add(d->ui->m_storeCopies); + } + + DocumentPage::~DocumentPage() + { + } + + KMyMoneyWizardPage* DocumentPage::nextPage() const + { + Q_D(const DocumentPage); + return d->m_wizard->d_func()->m_preferencePage; + } + + bool DocumentPage::isComplete() const + { + Q_D(const DocumentPage); + const auto selectedPath = d->ui->m_storagePath->text(); + return !d->ui->m_storeCopies->isChecked() || !selectedPath.isEmpty() && QDir(selectedPath).exists(); + } + + void DocumentPage::slotSelectFolder() + { + Q_D(const DocumentPage); + auto fileName = QFileDialog::getExistingDirectory(this, i18n("Select directory to store copies of documents"), d->ui->m_storagePath->text()); + d->ui->m_storagePath->setText(fileName); + } + + void DocumentPage::slotToggleFolderSelection() + { + Q_D(const DocumentPage); + d->ui->m_selectFolderButton->setEnabled(!d->ui->m_selectFolderButton->isEnabled()); + d->ui->m_storagePath->setEnabled(!d->ui->m_storagePath->isEnabled()); + } +} diff --git a/kmymoney/wizards/newuserwizard/kdocumentpage.ui b/kmymoney/wizards/newuserwizard/kdocumentpage.ui new file mode 100644 --- /dev/null +++ b/kmymoney/wizards/newuserwizard/kdocumentpage.ui @@ -0,0 +1,121 @@ + + + + + + KDocumentPage + + + + 0 + 0 + 600 + 415 + + + + + + + + KMyMoney allows you to store arbitrary documents supporting each transaction.<br><br> + By default, only <b>references</b> to (external) documents are stored. However, you can choose to store <b>copies</b> of attached documents instead. + + + + true + + + + + + + + + + 25 + 0 + + + + QFrame::NoFrame + + + QFrame::Plain + + + + + + + + + + + + 100 + 0 + + + + Store copies of attached documents + + + + + + + + + + + Provide storage path + + + false + + + + + + + ... + + + false + + + + + + + + + + + + 25 + 0 + + + + QFrame::NoFrame + + + QFrame::Plain + + + + + + + + + + + KLineEdit + QLineEdit +
klineedit.h
+
+
+
diff --git a/kmymoney/wizards/newuserwizard/kdocumentpage_p.h b/kmymoney/wizards/newuserwizard/kdocumentpage_p.h new file mode 100644 --- /dev/null +++ b/kmymoney/wizards/newuserwizard/kdocumentpage_p.h @@ -0,0 +1,56 @@ +/* + * Copyright 2018 Marc Hübner + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef KDOCUMENTPAGE_P_H +#define KDOCUMENTPAGE_P_H + + // ---------------------------------------------------------------------------- + // QT Includes + + // ---------------------------------------------------------------------------- + // KDE Includes + + // ---------------------------------------------------------------------------- + // Project Includes + +#include "ui_kdocumentpage.h" +#include "wizardpage_p.h" + +namespace NewUserWizard +{ + class Wizard; + + class DocumentPagePrivate : public WizardPagePrivate + { + Q_DISABLE_COPY(DocumentPagePrivate) + + public: + explicit DocumentPagePrivate(QObject* parent) : + WizardPagePrivate(parent), + ui(new Ui::KDocumentPage) + { + } + + ~DocumentPagePrivate() + { + delete ui; + } + + Ui::KDocumentPage *ui; + }; +} +#endif //KDOCUMENTPAGE_P_H diff --git a/kmymoney/wizards/newuserwizard/knewuserwizard.h b/kmymoney/wizards/newuserwizard/knewuserwizard.h --- a/kmymoney/wizards/newuserwizard/knewuserwizard.h +++ b/kmymoney/wizards/newuserwizard/knewuserwizard.h @@ -5,6 +5,7 @@ copyright : (C) 2006 Thomas Baumgart email : Thomas Baumgart (C) 2017 by Łukasz Wojniłowicz + (C) 2018 by Marc Hübner ***************************************************************************/ /*************************************************************************** @@ -58,6 +59,7 @@ friend class AccountPage; friend class CategoriesPage; friend class PreferencePage; + friend class DocumentPage; Q_OBJECT Q_DISABLE_COPY(Wizard) @@ -104,6 +106,17 @@ */ bool startSettingsAfterFinished() const; + /** + * Returns true if the user chose to store copies of documents instead of references, false otherwise + */ + bool storeDocumentCopies() const; + + /** + * Returns the path to store document copies in + */ + QString documentStoragePath() const; + + private: Q_DECLARE_PRIVATE(Wizard) }; diff --git a/kmymoney/wizards/newuserwizard/knewuserwizard.cpp b/kmymoney/wizards/newuserwizard/knewuserwizard.cpp --- a/kmymoney/wizards/newuserwizard/knewuserwizard.cpp +++ b/kmymoney/wizards/newuserwizard/knewuserwizard.cpp @@ -5,6 +5,7 @@ copyright : (C) 2006 Thomas Baumgart email : Thomas Baumgart (C) 2017 by Łukasz Wojniłowicz + (C) 2018 by Marc Hübner ***************************************************************************/ /*************************************************************************** @@ -41,6 +42,8 @@ #include "kintropage.h" #include "kpreferencepage.h" #include "kpreferencepage_p.h" +#include "kdocumentpage.h" +#include "kdocumentpage_p.h" #include "mymoneysecurity.h" #include "mymoneypayee.h" @@ -71,14 +74,16 @@ addStep(i18n("Personal Data")); addStep(i18n("Select Currency")); addStep(i18n("Select Accounts")); + addStep(i18n("Document storage")); addStep(i18nc("Finish the wizard", "Finish")); if (isFirstTime) d->m_introPage = new IntroPage(this); d->m_generalPage = new GeneralPage(this); d->m_currencyPage = new CurrencyPage(this); d->m_accountPage = new AccountPage(this); d->m_categoriesPage = new CategoriesPage(this); + d->m_documentPage = new DocumentPage(this); d->m_preferencePage = new PreferencePage(this); d->m_accountPage->d_func()->ui->m_haveCheckingAccountButton->setChecked(true); @@ -152,4 +157,16 @@ Q_D(const Wizard); return d->m_preferencePage->d_func()->ui->m_openConfigAfterFinished->checkState() == Qt::Checked; } + + bool Wizard::storeDocumentCopies() const + { + Q_D(const Wizard); + return d->m_documentPage->d_func()->ui->m_storeCopies->isChecked(); + } + + QString Wizard::documentStoragePath() const + { + Q_D(const Wizard); + return d->m_documentPage->d_func()->ui->m_storagePath->text(); + } } diff --git a/kmymoney/wizards/newuserwizard/knewuserwizard_p.h b/kmymoney/wizards/newuserwizard/knewuserwizard_p.h --- a/kmymoney/wizards/newuserwizard/knewuserwizard_p.h +++ b/kmymoney/wizards/newuserwizard/knewuserwizard_p.h @@ -35,6 +35,7 @@ class CurrencyPage; class AccountPage; class CategoriesPage; + class DocumentPage; class PreferencePage; class WizardPrivate : public KMyMoneyWizardPrivate @@ -49,6 +50,7 @@ m_currencyPage(nullptr), m_accountPage(nullptr), m_categoriesPage(nullptr), + m_documentPage(nullptr), m_preferencePage(nullptr) { } @@ -63,6 +65,7 @@ CurrencyPage* m_currencyPage; AccountPage* m_accountPage; CategoriesPage* m_categoriesPage; + DocumentPage* m_documentPage; PreferencePage* m_preferencePage; };