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