File indexing completed on 2024-05-12 05:07:38

0001 /*
0002     SPDX-FileCopyrightText: 2007-2019 Thomas Baumgart <tbaumgart@kde.org>
0003     SPDX-FileCopyrightText: 2017-2018 Łukasz Wojniłowicz <lukasz.wojnilowicz@gmail.com>
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #ifndef KACCOUNTSVIEW_P_H
0008 #define KACCOUNTSVIEW_P_H
0009 
0010 #include "kaccountsview.h"
0011 
0012 // ----------------------------------------------------------------------------
0013 // QT Includes
0014 
0015 #include <QAction>
0016 #include <QPointer>
0017 
0018 // ----------------------------------------------------------------------------
0019 // KDE Includes
0020 
0021 #include <KMessageBox>
0022 
0023 // ----------------------------------------------------------------------------
0024 // Project Includes
0025 
0026 #include "ui_kaccountsview.h"
0027 #include "kmymoneyviewbase_p.h"
0028 #include "mymoneyfile.h"
0029 #include "accountsmodel.h"
0030 
0031 #include "mymoneyexception.h"
0032 #include "mymoneysplit.h"
0033 #include "mymoneyschedule.h"
0034 #include "mymoneytransaction.h"
0035 #include "knewaccountdlg.h"
0036 #include "keditloanwizard.h"
0037 #include "mymoneyaccount.h"
0038 #include "mymoneymoney.h"
0039 #include "accountsproxymodel.h"
0040 #include "kmymoneyplugin.h"
0041 #include "icons.h"
0042 #include "mymoneyenums.h"
0043 #include "menuenums.h"
0044 #include "mymoneystatementreader.h"
0045 #include "kmymoneyutils.h"
0046 #include "columnselector.h"
0047 
0048 using namespace Icons;
0049 
0050 class KAccountsViewPrivate : public KMyMoneyViewBasePrivate
0051 {
0052     Q_DECLARE_PUBLIC(KAccountsView)
0053 
0054 public:
0055     explicit KAccountsViewPrivate(KAccountsView *qq)
0056         : KMyMoneyViewBasePrivate(qq)
0057         , ui(new Ui::KAccountsView)
0058         , m_haveUnusedCategories(false)
0059         , m_onlinePlugins(nullptr)
0060         , m_proxyModel(nullptr)
0061     {
0062     }
0063 
0064     ~KAccountsViewPrivate()
0065     {
0066         delete ui;
0067     }
0068 
0069     void init()
0070     {
0071         Q_Q(KAccountsView);
0072 
0073         ui->setupUi(q);
0074 
0075         // setup icons for collapse and expand button
0076         ui->m_collapseButton->setIcon(Icons::get(Icon::ListCollapse));
0077         ui->m_expandButton->setIcon(Icons::get(Icon::ListExpand));
0078 
0079         // setup filter
0080         m_proxyModel = ui->m_accountTree->proxyModel();
0081         q->connect(ui->m_searchWidget, &QLineEdit::textChanged, m_proxyModel, &QSortFilterProxyModel::setFilterFixedString);
0082 
0083         auto columnSelector = new ColumnSelector(ui->m_accountTree, q->metaObject()->className());
0084         columnSelector->setAlwaysVisible(QVector<int>({ AccountsModel::Column::AccountName }));
0085         columnSelector->setAlwaysHidden(QVector<int>({
0086             AccountsModel::Column::PostedValue,
0087             AccountsModel::Column::BankCode,
0088             AccountsModel::Column::Bic,
0089             AccountsModel::Column::CostCenter,
0090         }));
0091 
0092         ui->m_accountTree->setModel(MyMoneyFile::instance()->accountsModel());
0093         m_proxyModel->addAccountGroup(AccountsProxyModel::assetLiabilityEquity());
0094         m_proxyModel->setFilterComboBox(ui->m_filterBox);
0095         m_proxyModel->setClosedSelectable(true);
0096 
0097         columnSelector->setModel(m_proxyModel);
0098         q->slotSettingsChanged();
0099 
0100         // forward the widget requests
0101         q->connect(ui->m_accountTree, &KMyMoneyAccountTreeView::requestCustomContextMenu, q, &KAccountsView::requestCustomContextMenu);
0102         q->connect(ui->m_accountTree, &KMyMoneyAccountTreeView::requestSelectionChange, q, &KAccountsView::requestSelectionChange);
0103         q->connect(ui->m_accountTree, &KMyMoneyAccountTreeView::requestActionTrigger, q, &KAccountsView::requestActionTrigger);
0104 
0105         m_focusWidget = ui->m_accountTree;
0106 
0107         // make sure to update our local copy if the account data changes
0108         q->connect(ui->m_accountTree->model(), &QAbstractItemModel::dataChanged, q, [&](const QModelIndex& topLeft, const QModelIndex& bottomRight) {
0109             // it should be only one row, but we never know and so we loop over all of them
0110             for (int row = topLeft.row(); row <= bottomRight.row(); ++row) {
0111                 const auto idx = ui->m_accountTree->model()->index(row, 0, topLeft.parent());
0112                 const auto id = idx.data(eMyMoney::Model::IdRole).toString();
0113                 if (id == m_currentAccount.id()) {
0114                     m_currentAccount = MyMoneyFile::instance()->accountsModel()->itemById(id);
0115                     break;
0116                 }
0117             }
0118         });
0119     }
0120 
0121     void editLoan()
0122     {
0123         Q_Q(KAccountsView);
0124         if (m_currentAccount.id().isEmpty())
0125             return;
0126 
0127         const auto file = MyMoneyFile::instance();
0128         if (file->isStandardAccount(m_currentAccount.id()))
0129             return;
0130 
0131         QPointer<KEditLoanWizard> wizard = new KEditLoanWizard(m_currentAccount);
0132         q->connect(wizard, &KEditLoanWizard::newCategory, q, &KAccountsView::slotNewCategory);
0133         q->connect(wizard, &KEditLoanWizard::createPayee, q, &KAccountsView::slotNewPayee);
0134         if (wizard->exec() == QDialog::Accepted && wizard != 0) {
0135             MyMoneySchedule sch;
0136             try {
0137                 sch = file->schedule(m_currentAccount.value("schedule").toLatin1());
0138             } catch (const MyMoneyException &) {
0139                 qDebug() << "schedule" << m_currentAccount.value("schedule").toLatin1() << "not found";
0140             }
0141             if (!(m_currentAccount == wizard->account())
0142                     || !(sch == wizard->schedule())) {
0143                 MyMoneyFileTransaction ft;
0144                 try {
0145                     file->modifyAccount(wizard->account());
0146                     if (!sch.id().isEmpty()) {
0147                         sch = wizard->schedule();
0148                     }
0149                     try {
0150                         file->schedule(sch.id());
0151                         file->modifySchedule(sch);
0152                         ft.commit();
0153                     } catch (const MyMoneyException &) {
0154                         try {
0155                             if(sch.transaction().splitCount() >= 2) {
0156                                 file->addSchedule(sch);
0157                             }
0158                             ft.commit();
0159                         } catch (const MyMoneyException &e) {
0160                             qDebug("Cannot add schedule: '%s'", e.what());
0161                         }
0162                     }
0163                 } catch (const MyMoneyException &e) {
0164                     qDebug("Unable to modify account %s: '%s'", qPrintable(m_currentAccount.name()),
0165                            e.what());
0166                 }
0167             }
0168         }
0169         delete wizard;
0170     }
0171 
0172     void editAccount()
0173     {
0174         if (m_currentAccount.id().isEmpty())
0175             return;
0176 
0177         const auto file = MyMoneyFile::instance();
0178         if (file->isStandardAccount(m_currentAccount.id()))
0179             return;
0180 
0181 
0182         // set a status message so that the application can't be closed until the editing is done
0183         //        slotStatusMsg(caption);
0184 
0185         auto tid = file->openingBalanceTransaction(m_currentAccount);
0186         MyMoneyTransaction t;
0187         MyMoneySplit s0, s1;
0188         QPointer<KNewAccountDlg> dlg =
0189             new KNewAccountDlg(m_currentAccount, true, false, 0, i18n("Edit account '%1'", m_currentAccount.name()));
0190 
0191         if (!tid.isEmpty()) {
0192             try {
0193                 t = file->transaction(tid);
0194                 s0 = t.splitByAccount(m_currentAccount.id());
0195                 s1 = t.splitByAccount(m_currentAccount.id(), false);
0196                 dlg->setOpeningBalance(s0.shares());
0197                 if (m_currentAccount.accountGroup() == eMyMoney::Account::Type::Liability) {
0198                     dlg->setOpeningBalance(-s0.shares());
0199                 }
0200             } catch (const MyMoneyException &e) {
0201                 qDebug() << "Error retrieving opening balance transaction " << tid << ": " << e.what() << "\n";
0202                 tid.clear();
0203             }
0204         }
0205 
0206         // check for online modules
0207         QMap<QString, KMyMoneyPlugin::OnlinePlugin *>::const_iterator it_plugin;
0208         if (m_onlinePlugins) {
0209             it_plugin = m_onlinePlugins->constEnd();
0210             const auto& kvp = m_currentAccount.onlineBankingSettings();
0211             if (!kvp["provider"].isEmpty()) {
0212                 // if we have an online provider for this account, we need to check
0213                 // that we have the corresponding plugin. If that exists, we ask it
0214                 // to provide an additional tab for the account editor.
0215                 it_plugin = m_onlinePlugins->constFind(kvp["provider"].toLower());
0216                 if (it_plugin != m_onlinePlugins->constEnd()) {
0217                     QString name;
0218                     auto w = (*it_plugin)->accountConfigTab(m_currentAccount, name);
0219                     dlg->addTab(w, name);
0220                 }
0221             }
0222         }
0223 
0224         if (dlg->exec() == QDialog::Accepted) {
0225             if (dlg != nullptr) {
0226                 try {
0227                     auto account = dlg->account();
0228                     auto parent = dlg->parentAccount();
0229                     auto bal = dlg->openingBalance();
0230                     if (account.accountGroup() == eMyMoney::Account::Type::Liability) {
0231                         bal = -bal;
0232                     }
0233 
0234                     // determine if the opening balance transaction will change
0235 
0236                     // do we remove the transaction?
0237                     auto balanceTransactionChanges = (!tid.isEmpty() && bal.isZero());
0238                     // do we modify the transaction?
0239                     balanceTransactionChanges |= (!tid.isEmpty() && !bal.isZero() && ((s0.shares() != bal) || (t.postDate() != account.openingDate())));
0240                     // do we create the transaction?
0241                     balanceTransactionChanges |= (tid.isEmpty() && !bal.isZero());
0242                     // or do we reparent the account
0243                     balanceTransactionChanges |= (account.parentAccountId() != parent.id());
0244 
0245                     MyMoneyFileTransaction ft(i18nc("Undo action description", "Edit account"), balanceTransactionChanges);
0246 
0247                     if (m_onlinePlugins && it_plugin != m_onlinePlugins->constEnd()) {
0248                         account.setOnlineBankingSettings((*it_plugin)->onlineBankingSettings(account.onlineBankingSettings()));
0249                     }
0250                     // we need to modify first, as reparent would override all other changes
0251                     file->modifyAccount(account);
0252                     if (account.parentAccountId() != parent.id()) {
0253                         file->reparentAccount(account, parent);
0254                     }
0255                     if (!tid.isEmpty()) {
0256                         if (bal.isZero()) {
0257                             file->removeTransaction(t);
0258 
0259                         } else if ((s0.shares() != bal) || (t.postDate() != account.openingDate())) {
0260                             s0.setShares(bal);
0261                             s0.setValue(bal);
0262                             t.modifySplit(s0);
0263                             s1.setShares(-bal);
0264                             s1.setValue(-bal);
0265                             t.modifySplit(s1);
0266                             t.setPostDate(account.openingDate());
0267                             file->modifyTransaction(t);
0268                         }
0269 
0270                     } else if (!bal.isZero()) {
0271                         file->createOpeningBalanceTransaction(account, bal);
0272                     }
0273 
0274                     ft.commit();
0275                     m_currentAccount = account;
0276 
0277                 } catch (const MyMoneyException& e) {
0278                     Q_Q(KAccountsView);
0279                     KMessageBox::error(q, i18n("Unable to modify account '%1'. Cause: %2", m_currentAccount.name(), e.what()));
0280                 }
0281             }
0282         }
0283         delete dlg;
0284     }
0285 
0286     enum CanCloseAccountCodeE {
0287         AccountCanClose = 0,        // can close the account
0288         AccountBalanceNonZero,      // balance is non zero
0289         AccountChildrenOpen,        // account has open children account
0290         AccountScheduleReference,   // account is referenced in a schedule
0291         AccountHasOnlineMapping,    // account has an online mapping
0292     };
0293 
0294     /**
0295      * This method checks, if an account can be closed or not. An account
0296      * can be closed if:
0297      *
0298      * - the balance is zero and
0299      * - all children are already closed (or it is an investment account and all sub-accounts can be closed) and
0300      * - there is no unfinished schedule referencing the account
0301      * - and no online mapping is setup
0302      *
0303      * @param acc reference to MyMoneyAccount object in question
0304      * @retval true account can be closed
0305      * @retval false account cannot be closed
0306      */
0307     CanCloseAccountCodeE canCloseAccount(const MyMoneyAccount& acc)
0308     {
0309         // balance must be zero
0310         if (!acc.balance().isZero())
0311             return AccountBalanceNonZero;
0312         if (acc.hasOnlineMapping())
0313             return AccountHasOnlineMapping;
0314 
0315         // all children must be already closed
0316         const auto accList = acc.accountList();
0317         for (const auto& sAccount : accList) {
0318             const auto subAccount = MyMoneyFile::instance()->account(sAccount);
0319             if (acc.accountType() == eMyMoney::Account::Type::Investment) {
0320                 auto subAccountResult = canCloseAccount(subAccount);
0321                 if (subAccountResult != AccountCanClose)
0322                     return AccountChildrenOpen;
0323 
0324             } else if (!subAccount.isClosed()) {
0325                 return AccountChildrenOpen;
0326             }
0327         }
0328 
0329         // there must be no unfinished schedule referencing the account
0330         QList<MyMoneySchedule> list = MyMoneyFile::instance()->scheduleList();
0331         QList<MyMoneySchedule>::const_iterator it_l;
0332         for (it_l = list.constBegin(); it_l != list.constEnd(); ++it_l) {
0333             if ((*it_l).isFinished())
0334                 continue;
0335             if ((*it_l).hasReferenceTo(acc.id()))
0336                 return AccountScheduleReference;
0337         }
0338         return AccountCanClose;
0339     }
0340 
0341     /**
0342      * This method checks if an account can be closed and enables/disables
0343      * the close account action
0344      * If disabled, it sets a tooltip explaining why it cannot be closed
0345      * @brief enableCloseAccountAction
0346      * @param acc reference to MyMoneyAccount object in question
0347      */
0348     void hintCloseAccountAction(const MyMoneyAccount& acc, QAction* a)
0349     {
0350         switch (canCloseAccount(acc)) {
0351         case AccountCanClose:
0352             a->setToolTip(QString());
0353             break;
0354         case AccountBalanceNonZero:
0355             a->setToolTip(i18n("The balance of the account must be zero before the account can be closed"));
0356             break;
0357         case AccountChildrenOpen:
0358             a->setToolTip(i18n("All subaccounts must be closed before the account can be closed"));
0359             break;
0360         case AccountScheduleReference:
0361             a->setToolTip(i18n("This account is still included in an active schedule"));
0362             break;
0363         case AccountHasOnlineMapping:
0364             a->setToolTip(i18n("This account is still mapped to an online account"));
0365             break;
0366         }
0367     }
0368 
0369     void accountsUpdateOnline(const QList<MyMoneyAccount>& accList)
0370     {
0371         Q_Q(KAccountsView);
0372         // block the update account actions for now so that we don't get here twice
0373         const QVector<eMenu::Action> disabledActions {eMenu::Action::UpdateAccount, eMenu::Action::UpdateAllAccounts};
0374         for (const auto& a : disabledActions)
0375             pActions[a]->setEnabled(false);
0376 
0377         // clear global message list
0378         MyMoneyStatementReader::clearResultMessages();
0379 
0380         // process all entries that have a mapped account and the 'provider' is available
0381         // we need to make sure that only the very last entry that matches sets the
0382         // 'moreAccounts' parameter in the call to updateAccount() to false
0383         auto processedAccounts = 0;
0384 
0385         Q_EMIT q->beginImportingStatements();
0386         for (auto it_provider = m_onlinePlugins->constBegin(); it_provider != m_onlinePlugins->constEnd(); ++it_provider) {
0387             auto nextAccount = accList.cend();
0388             for (auto it_a = accList.cbegin(); it_a != accList.cend(); ++it_a) {
0389                 if ((*it_a).hasOnlineMapping()
0390                         && (it_provider == m_onlinePlugins->constFind((*it_a).onlineBankingSettings().value("provider").toLower()))) {
0391                     if (nextAccount != accList.cend()) {
0392                         (*it_provider)->updateAccount(*nextAccount, true);
0393                     }
0394                     nextAccount = it_a;
0395                     ++processedAccounts;
0396                 }
0397             }
0398             // process a possible pending entry
0399             if (nextAccount != accList.cend()) {
0400                 (*it_provider)->updateAccount(*nextAccount, false);
0401             }
0402         }
0403         Q_EMIT q->endImportingStatements();
0404 
0405         // re-enable the disabled actions
0406         updateActions(m_currentAccount);
0407 
0408         KMyMoneyUtils::showStatementImportResult(MyMoneyStatementReader::resultMessages(), processedAccounts);
0409     }
0410 
0411     void updateActions(const MyMoneyAccount& acc)
0412     {
0413         const auto file = MyMoneyFile::instance();
0414         switch (acc.accountGroup()) {
0415         case eMyMoney::Account::Type::Asset:
0416         case eMyMoney::Account::Type::Liability:
0417         case eMyMoney::Account::Type::Equity:
0418         {
0419             auto isClosed = acc.isClosed() ? true : false;
0420             pActions[eMenu::Action::EditAccount]->setEnabled(!isClosed);
0421             pActions[eMenu::Action::DeleteAccount]->setEnabled(!file->isReferenced(acc));
0422 
0423             pActions[eMenu::Action::ReopenAccount]->setEnabled(isClosed);
0424             pActions[eMenu::Action::CloseAccount]->setEnabled(!isClosed);
0425 
0426             if (!isClosed) {
0427                 const auto canClose = (canCloseAccount(acc) == KAccountsViewPrivate::AccountCanClose) ? true : false;
0428                 pActions[eMenu::Action::CloseAccount]->setEnabled(canClose);
0429                 hintCloseAccountAction(acc, pActions[eMenu::Action::CloseAccount]);
0430             }
0431 
0432             pActions[eMenu::Action::ChartAccountBalance]->setEnabled(true);
0433 
0434             if (acc.hasOnlineMapping()) {
0435                 pActions[eMenu::Action::MapOnlineAccount]->setEnabled(false);
0436                 pActions[eMenu::Action::UnmapOnlineAccount]->setEnabled(true);
0437 
0438                 if (m_onlinePlugins) {
0439                     // check if provider is available
0440                     QMap<QString, KMyMoneyPlugin::OnlinePlugin*>::const_iterator it_p;
0441                     it_p = m_onlinePlugins->constFind(acc.onlineBankingSettings().value(QLatin1String("provider")).toLower());
0442                     if (it_p != m_onlinePlugins->constEnd()) {
0443                         QStringList protocols;
0444                         (*it_p)->protocols(protocols);
0445                         if (protocols.count() > 0) {
0446                             pActions[eMenu::Action::UpdateAccount]->setEnabled(true);
0447                         }
0448                     }
0449                 }
0450 
0451             } else {
0452                 pActions[eMenu::Action::MapOnlineAccount]->setEnabled(!acc.isClosed() && m_onlinePlugins && !m_onlinePlugins->isEmpty());
0453             }
0454 
0455             break;
0456         }
0457         default:
0458             break;
0459         }
0460         pActions[eMenu::Action::UpdateAllAccounts]->setEnabled(KMyMoneyUtils::canUpdateAllAccounts());
0461     }
0462 
0463     Ui::KAccountsView   *ui;
0464     bool                m_haveUnusedCategories;
0465     MyMoneyAccount      m_currentAccount;
0466     QMap<QString, KMyMoneyPlugin::OnlinePlugin*>* m_onlinePlugins;
0467     AccountsProxyModel* m_proxyModel;
0468 };
0469 
0470 #endif