1 /***************************************************************************
2                           kaccountsview.cpp
3                              -------------------
4     copyright            : (C) 2007 by Thomas Baumgart <ipwizard@users.sourceforge.net>
5                            (C) 2017, 2018 by Łukasz Wojniłowicz <lukasz.wojnilowicz@gmail.com>
6  ***************************************************************************/
7 
8 /***************************************************************************
9  *                                                                         *
10  *   This program is free software; you can redistribute it and/or modify  *
11  *   it under the terms of the GNU General Public License as published by  *
12  *   the Free Software Foundation; either version 2 of the License, or     *
13  *   (at your option) any later version.                                   *
14  *                                                                         *
15  ***************************************************************************/
16 
17 #include "kaccountsview_p.h"
18 
19 #include <typeinfo>
20 
21 // ----------------------------------------------------------------------------
22 // QT Includes
23 
24 #include <QTimer>
25 #include <QMenu>
26 #include <QAction>
27 #include <QBitArray>
28 
29 // ----------------------------------------------------------------------------
30 // KDE Includes
31 
32 #include <KMessageBox>
33 
34 // ----------------------------------------------------------------------------
35 // Project Includes
36 
37 #include "onlinejobadministration.h"
38 #include "knewaccountwizard.h"
39 #include "kmymoneyutils.h"
40 #include "kmymoneysettings.h"
41 #include "storageenums.h"
42 #include "menuenums.h"
43 
44 using namespace Icons;
45 
KAccountsView(QWidget * parent)46 KAccountsView::KAccountsView(QWidget *parent) :
47     KMyMoneyAccountsViewBase(*new KAccountsViewPrivate(this), parent)
48 {
49   Q_D(KAccountsView);
50   d->ui->setupUi(this);
51 
52   connect(pActions[eMenu::Action::NewAccount],          &QAction::triggered, this, &KAccountsView::slotNewAccount);
53   connect(pActions[eMenu::Action::EditAccount],         &QAction::triggered, this, &KAccountsView::slotEditAccount);
54   connect(pActions[eMenu::Action::DeleteAccount],       &QAction::triggered, this, &KAccountsView::slotDeleteAccount);
55   connect(pActions[eMenu::Action::CloseAccount],        &QAction::triggered, this, &KAccountsView::slotCloseAccount);
56   connect(pActions[eMenu::Action::ReopenAccount],       &QAction::triggered, this, &KAccountsView::slotReopenAccount);
57   connect(pActions[eMenu::Action::ChartAccountBalance], &QAction::triggered, this, &KAccountsView::slotChartAccountBalance);
58   connect(pActions[eMenu::Action::MapOnlineAccount],    &QAction::triggered, this, &KAccountsView::slotAccountMapOnline);
59   connect(pActions[eMenu::Action::UnmapOnlineAccount],  &QAction::triggered, this, &KAccountsView::slotAccountUnmapOnline);
60   connect(pActions[eMenu::Action::UpdateAccount],       &QAction::triggered, this, &KAccountsView::slotAccountUpdateOnline);
61   connect(pActions[eMenu::Action::UpdateAllAccounts],   &QAction::triggered, this, &KAccountsView::slotAccountUpdateOnlineAll);
62 }
63 
~KAccountsView()64 KAccountsView::~KAccountsView()
65 {
66 }
67 
executeCustomAction(eView::Action action)68 void KAccountsView::executeCustomAction(eView::Action action)
69 {
70   switch(action) {
71     case eView::Action::Refresh:
72       refresh();
73       break;
74 
75     case eView::Action::SetDefaultFocus:
76       {
77         Q_D(KAccountsView);
78         QTimer::singleShot(0, d->ui->m_accountTree, SLOT(setFocus()));
79       }
80       break;
81 
82     default:
83       break;
84   }
85 }
86 
refresh()87 void KAccountsView::refresh()
88 {
89   Q_D(KAccountsView);
90   if (!isVisible()) {
91     d->m_needsRefresh = true;
92     return;
93   }
94   d->m_needsRefresh = false;
95   // TODO: check why the invalidate is needed here
96   d->m_proxyModel->invalidate();
97   d->m_proxyModel->setHideClosedAccounts(KMyMoneySettings::hideClosedAccounts() && !KMyMoneySettings::showAllAccounts());
98   d->m_proxyModel->setHideEquityAccounts(!KMyMoneySettings::expertMode());
99   if (KMyMoneySettings::showCategoriesInAccountsView()) {
100     d->m_proxyModel->addAccountGroup(QVector<eMyMoney::Account::Type> {eMyMoney::Account::Type::Income, eMyMoney::Account::Type::Expense});
101   } else {
102     d->m_proxyModel->removeAccountType(eMyMoney::Account::Type::Income);
103     d->m_proxyModel->removeAccountType(eMyMoney::Account::Type::Expense);
104   }
105 
106   // reinitialize the default state of the hidden categories label
107   d->m_haveUnusedCategories = false;
108   d->ui->m_hiddenCategories->hide();  // hides label
109   d->m_proxyModel->setHideUnusedIncomeExpenseAccounts(KMyMoneySettings::hideUnusedCategory());
110 }
111 
showEvent(QShowEvent * event)112 void KAccountsView::showEvent(QShowEvent * event)
113 {
114   Q_D(KAccountsView);
115   if (!d->m_proxyModel)
116     d->init();
117 
118   emit customActionRequested(View::Accounts, eView::Action::AboutToShow);
119 
120   if (d->m_needsRefresh)
121     refresh();
122 
123   // don't forget base class implementation
124   QWidget::showEvent(event);
125 }
126 
updateActions(const MyMoneyObject & obj)127 void KAccountsView::updateActions(const MyMoneyObject& obj)
128 {
129   Q_D(KAccountsView);
130 
131   const auto file = MyMoneyFile::instance();
132 
133   if (typeid(obj) != typeid(MyMoneyAccount) &&
134       (obj.id().isEmpty() && d->m_currentAccount.id().isEmpty())) // do not disable actions that were already disabled)
135     return;
136 
137   const auto& acc = static_cast<const MyMoneyAccount&>(obj);
138 
139   const QVector<eMenu::Action> actionsToBeDisabled {
140         eMenu::Action::NewAccount, eMenu::Action::EditAccount, eMenu::Action::DeleteAccount,
141         eMenu::Action::CloseAccount, eMenu::Action::ReopenAccount,
142         eMenu::Action::ChartAccountBalance,
143         eMenu::Action::UnmapOnlineAccount, eMenu::Action::MapOnlineAccount,
144         eMenu::Action::UpdateAccount
145   };
146 
147   for (const auto& a : actionsToBeDisabled)
148     pActions[a]->setEnabled(false);
149 
150   pActions[eMenu::Action::NewAccount]->setEnabled(true);
151   pActions[eMenu::Action::UpdateAllAccounts]->setEnabled(KMyMoneyUtils::canUpdateAllAccounts());
152 
153   if (acc.id().isEmpty()) {
154     d->m_currentAccount = MyMoneyAccount();
155     return;
156   } else if (file->isStandardAccount(acc.id())) {
157     d->m_currentAccount = acc;
158     return;
159   }
160   d->m_currentAccount = acc;
161 
162   switch (acc.accountGroup()) {
163     case eMyMoney::Account::Type::Asset:
164     case eMyMoney::Account::Type::Liability:
165     case eMyMoney::Account::Type::Equity:
166     {
167       pActions[eMenu::Action::EditAccount]->setEnabled(true);
168       pActions[eMenu::Action::DeleteAccount]->setEnabled(!file->isReferenced(acc));
169 
170       auto b = acc.isClosed() ? true : false;
171       pActions[eMenu::Action::ReopenAccount]->setEnabled(b);
172       pActions[eMenu::Action::CloseAccount]->setEnabled(!b);
173 
174       if (!acc.isClosed()) {
175         b = (d->canCloseAccount(acc) == KAccountsViewPrivate::AccountCanClose) ? true : false;
176         pActions[eMenu::Action::CloseAccount]->setEnabled(b);
177         d->hintCloseAccountAction(acc, pActions[eMenu::Action::CloseAccount]);
178       }
179 
180       pActions[eMenu::Action::ChartAccountBalance]->setEnabled(true);
181 
182       if (d->m_currentAccount.hasOnlineMapping()) {
183         pActions[eMenu::Action::UnmapOnlineAccount]->setEnabled(true);
184 
185         if (d->m_onlinePlugins) {
186           // check if provider is available
187           QMap<QString, KMyMoneyPlugin::OnlinePlugin*>::const_iterator it_p;
188           it_p = d->m_onlinePlugins->constFind(d->m_currentAccount.onlineBankingSettings().value(QLatin1String("provider")).toLower());
189           if (it_p != d->m_onlinePlugins->constEnd()) {
190             QStringList protocols;
191             (*it_p)->protocols(protocols);
192             if (protocols.count() > 0) {
193               pActions[eMenu::Action::UpdateAccount]->setEnabled(true);
194             }
195           }
196         }
197 
198       } else {
199         pActions[eMenu::Action::MapOnlineAccount]->setEnabled(!acc.isClosed() && d->m_onlinePlugins && !d->m_onlinePlugins->isEmpty());
200       }
201 
202       break;
203     }
204     default:
205       break;
206   }
207 
208 #if 0
209   // It is unclear to me, what the following code should do
210   // as it just does nothing
211   QBitArray skip((int)eStorage::Reference::Count);
212   if (!d->m_currentAccount.id().isEmpty()) {
213     if (!file->isStandardAccount(d->m_currentAccount.id())) {
214       switch (d->m_currentAccount.accountGroup()) {
215         case eMyMoney::Account::Type::Asset:
216         case eMyMoney::Account::Type::Liability:
217         case eMyMoney::Account::Type::Equity:
218 
219           break;
220 
221         default:
222           break;
223       }
224     }
225   }
226 #endif
227 
228 }
229 
230 /**
231   * The view is notified that an unused income expense account has been hidden.
232   */
slotUnusedIncomeExpenseAccountHidden()233 void KAccountsView::slotUnusedIncomeExpenseAccountHidden()
234 {
235   Q_D(KAccountsView);
236   d->m_haveUnusedCategories = true;
237   d->ui->m_hiddenCategories->setVisible(d->m_haveUnusedCategories);
238 }
239 
slotNetWorthChanged(const MyMoneyMoney & netWorth)240 void KAccountsView::slotNetWorthChanged(const MyMoneyMoney &netWorth)
241 {
242   Q_D(KAccountsView);
243   d->netBalProChanged(netWorth, d->ui->m_totalProfitsLabel, View::Accounts);
244 }
245 
slotShowAccountMenu(const MyMoneyAccount & acc)246 void KAccountsView::slotShowAccountMenu(const MyMoneyAccount& acc)
247 {
248   Q_UNUSED(acc);
249   pMenus[eMenu::Menu::Account]->exec(QCursor::pos());
250 }
251 
slotSelectByObject(const MyMoneyObject & obj,eView::Intent intent)252 void KAccountsView::slotSelectByObject(const MyMoneyObject& obj, eView::Intent intent)
253 {
254   switch(intent) {
255     case eView::Intent::UpdateActions:
256       updateActions(obj);
257       break;
258 
259     case eView::Intent::OpenContextMenu:
260       slotShowAccountMenu(static_cast<const MyMoneyAccount&>(obj));
261       break;
262 
263     default:
264       break;
265   }
266 }
267 
slotSelectByVariant(const QVariantList & variant,eView::Intent intent)268 void KAccountsView::slotSelectByVariant(const QVariantList& variant, eView::Intent intent)
269 {
270   Q_D(KAccountsView);
271   switch (intent) {
272     case eView::Intent::UpdateNetWorth:
273       if (variant.count() == 1)
274         slotNetWorthChanged(variant.first().value<MyMoneyMoney>());
275       break;
276 
277     case eView::Intent::SetOnlinePlugins:
278       if (variant.count() == 1)
279         d->m_onlinePlugins = static_cast<QMap<QString, KMyMoneyPlugin::OnlinePlugin*>*>(variant.first().value<void*>());
280       break;
281 
282     default:
283       break;
284   }
285 }
286 
slotNewAccount()287 void KAccountsView::slotNewAccount()
288 {
289   MyMoneyAccount account;
290   account.setOpeningDate(KMyMoneySettings::firstFiscalDate());
291   NewAccountWizard::Wizard::newAccount(account);
292 }
293 
slotEditAccount()294 void KAccountsView::slotEditAccount()
295 {
296   Q_D(KAccountsView);
297 
298   switch (d->m_currentAccount.accountType()) {
299     case eMyMoney::Account::Type::Loan:
300     case eMyMoney::Account::Type::AssetLoan:
301       d->editLoan();
302       break;
303     default:
304       d->editAccount();
305       break;
306   }
307   emit selectByObject(d->m_currentAccount, eView::Intent::None);
308 }
309 
slotDeleteAccount()310 void KAccountsView::slotDeleteAccount()
311 {
312   Q_D(KAccountsView);
313   if (d->m_currentAccount.id().isEmpty())
314     return;  // need an account ID
315 
316   const auto file = MyMoneyFile::instance();
317   // can't delete standard accounts or account which still have transactions assigned
318   if (file->isStandardAccount(d->m_currentAccount.id()))
319     return;
320 
321   // check if the account is referenced by a transaction or schedule
322   QBitArray skip((int)eStorage::Reference::Count);
323   skip.fill(false);
324   skip.setBit((int)eStorage::Reference::Account);
325   skip.setBit((int)eStorage::Reference::Institution);
326   skip.setBit((int)eStorage::Reference::Payee);
327   skip.setBit((int)eStorage::Reference::Tag);
328   skip.setBit((int)eStorage::Reference::Security);
329   skip.setBit((int)eStorage::Reference::Currency);
330   skip.setBit((int)eStorage::Reference::Price);
331   if (file->isReferenced(d->m_currentAccount, skip))
332     return;
333 
334   MyMoneyFileTransaction ft;
335 
336   // retain the account name for a possible later usage in the error message box
337   // since the account removal notifies the views the selected account can be changed
338   // so we make sure by doing this that we display the correct name in the error message
339   auto selectedAccountName = d->m_currentAccount.name();
340 
341   try {
342     file->removeAccount(d->m_currentAccount);
343     d->m_currentAccount.clearId();
344     emit selectByObject(MyMoneyAccount(), eView::Intent::None);
345     ft.commit();
346   } catch (const MyMoneyException &e) {
347     KMessageBox::error(this, i18n("Unable to delete account '%1'. Cause: %2", selectedAccountName, QString::fromLatin1(e.what())));
348   }
349 }
350 
slotCloseAccount()351 void KAccountsView::slotCloseAccount()
352 {
353   Q_D(KAccountsView);
354   MyMoneyFileTransaction ft;
355   try {
356     d->m_currentAccount.setClosed(true);
357     MyMoneyFile::instance()->modifyAccount(d->m_currentAccount);
358     emit selectByObject(d->m_currentAccount, eView::Intent::None);
359     ft.commit();
360     if (KMyMoneySettings::hideClosedAccounts())
361       KMessageBox::information(this, i18n("<qt>You have closed this account. It remains in the system because you have transactions which still refer to it, but it is not shown in the views. You can make it visible again by going to the View menu and selecting <b>Show all accounts</b> or by deselecting the <b>Do not show closed accounts</b> setting.</qt>"), i18n("Information"), "CloseAccountInfo");
362   } catch (const MyMoneyException &) {
363   }
364 }
365 
slotReopenAccount()366 void KAccountsView::slotReopenAccount()
367 {
368   Q_D(KAccountsView);
369   const auto file = MyMoneyFile::instance();
370   MyMoneyFileTransaction ft;
371   try {
372     auto& acc = d->m_currentAccount;
373     while (acc.isClosed()) {
374       acc.setClosed(false);
375       file->modifyAccount(acc);
376       acc = file->account(acc.parentAccountId());
377     }
378     emit selectByObject(d->m_currentAccount, eView::Intent::None);
379     ft.commit();
380   } catch (const MyMoneyException &) {
381   }
382 }
383 
slotChartAccountBalance()384 void KAccountsView::slotChartAccountBalance()
385 {
386   Q_D(KAccountsView);
387   if (!d->m_currentAccount.id().isEmpty()) {
388     emit customActionRequested(View::Accounts, eView::Action::ShowBalanceChart);
389   }
390 }
391 
slotNewCategory()392 void KAccountsView::slotNewCategory()
393 {
394   Q_D(KAccountsView);
395   KNewAccountDlg::newCategory(d->m_currentAccount, MyMoneyAccount());
396 }
397 
slotNewPayee(const QString & nameBase,QString & id)398 void KAccountsView::slotNewPayee(const QString& nameBase, QString& id)
399 {
400   KMyMoneyUtils::newPayee(nameBase, id);
401 }
402 
slotAccountUnmapOnline()403 void KAccountsView::slotAccountUnmapOnline()
404 {
405   Q_D(KAccountsView);
406   // no account selected
407   if (d->m_currentAccount.id().isEmpty())
408     return;
409 
410   // not a mapped account
411   if (!d->m_currentAccount.hasOnlineMapping())
412     return;
413 
414   if (KMessageBox::warningYesNo(this, QString("<qt>%1</qt>").arg(i18n("Do you really want to remove the mapping of account <b>%1</b> to an online account? Depending on the details of the online banking method used, this action cannot be reverted.", d->m_currentAccount.name())), i18n("Remove mapping to online account")) == KMessageBox::Yes) {
415     MyMoneyFileTransaction ft;
416     try {
417       d->m_currentAccount.setOnlineBankingSettings(MyMoneyKeyValueContainer());
418       // Avoid showing an oline balance
419       d->m_currentAccount.deletePair(QStringLiteral("lastStatementBalance"));
420       // delete the kvp that is used in MyMoneyStatementReader too
421       // we should really get rid of it, but since I don't know what it
422       // is good for, I'll keep it around. (ipwizard)
423       d->m_currentAccount.deletePair(QStringLiteral("StatementKey"));
424       MyMoneyFile::instance()->modifyAccount(d->m_currentAccount);
425       ft.commit();
426       // The mapping could disable the online task system
427       onlineJobAdministration::instance()->updateOnlineTaskProperties();
428     } catch (const MyMoneyException &e) {
429       KMessageBox::error(this, i18n("Unable to unmap account from online account: %1", QString::fromLatin1(e.what())));
430     }
431   }
432   updateActions(d->m_currentAccount);
433 }
434 
slotAccountMapOnline()435 void KAccountsView::slotAccountMapOnline()
436 {
437   Q_D(KAccountsView);
438   // no account selected
439   if (d->m_currentAccount.id().isEmpty())
440     return;
441 
442   // already an account mapped
443   if (d->m_currentAccount.hasOnlineMapping())
444     return;
445 
446   // check if user tries to map a brokerageAccount
447   if (d->m_currentAccount.name().contains(i18n(" (Brokerage)"))) {
448     if (KMessageBox::warningContinueCancel(this, i18n("You try to map a brokerage account to an online account. This is usually not advisable. In general, the investment account should be mapped to the online account. Please cancel if you intended to map the investment account, continue otherwise"), i18n("Mapping brokerage account")) == KMessageBox::Cancel) {
449       return;
450     }
451   }
452   if (!d->m_onlinePlugins)
453     return;
454 
455   // if we have more than one provider let the user select the current provider
456   QString provider;
457   QMap<QString, KMyMoneyPlugin::OnlinePlugin*>::const_iterator it_p;
458   switch (d->m_onlinePlugins->count()) {
459     case 0:
460       break;
461     case 1:
462       provider = d->m_onlinePlugins->begin().key();
463       break;
464     default: {
465         QMenu popup(this);
466         popup.setTitle(i18n("Select online banking plugin"));
467 
468         // Populate the pick list with all the provider
469         for (it_p = d->m_onlinePlugins->constBegin(); it_p != d->m_onlinePlugins->constEnd(); ++it_p) {
470           popup.addAction(it_p.key())->setData(it_p.key());
471         }
472 
473         QAction *item = popup.actions()[0];
474         if (item) {
475           popup.setActiveAction(item);
476         }
477 
478         // cancelled
479         if ((item = popup.exec(QCursor::pos(), item)) == 0) {
480           return;
481         }
482 
483         provider = item->data().toString();
484       }
485       break;
486   }
487 
488   if (provider.isEmpty())
489     return;
490 
491   // find the provider
492   it_p = d->m_onlinePlugins->constFind(provider.toLower());
493   if (it_p != d->m_onlinePlugins->constEnd()) {
494     // plugin found, call it
495     MyMoneyKeyValueContainer settings;
496     if ((*it_p)->mapAccount(d->m_currentAccount, settings)) {
497       settings["provider"] = provider.toLower();
498       MyMoneyAccount acc(d->m_currentAccount);
499       acc.setOnlineBankingSettings(settings);
500       MyMoneyFileTransaction ft;
501       try {
502         MyMoneyFile::instance()->modifyAccount(acc);
503         ft.commit();
504         // The mapping could enable the online task system
505         onlineJobAdministration::instance()->updateOnlineTaskProperties();
506       } catch (const MyMoneyException &e) {
507         KMessageBox::error(this, i18n("Unable to map account to online account: %1", QString::fromLatin1(e.what())));
508       }
509     }
510   }
511   updateActions(d->m_currentAccount);
512 }
513 
slotAccountUpdateOnlineAll()514 void KAccountsView::slotAccountUpdateOnlineAll()
515 {
516   Q_D(KAccountsView);
517 
518   QList<MyMoneyAccount> accList;
519   MyMoneyFile::instance()->accountList(accList);
520 
521   QList<MyMoneyAccount> mappedAccList;
522   Q_FOREACH(auto account, accList) {
523     if (account.hasOnlineMapping())
524       mappedAccList += account;
525   }
526 
527   d->accountsUpdateOnline(mappedAccList);
528 }
529 
slotAccountUpdateOnline()530 void KAccountsView::slotAccountUpdateOnline()
531 {
532   Q_D(KAccountsView);
533   // no account selected
534   if (d->m_currentAccount.id().isEmpty())
535     return;
536 
537   // no online account mapped
538   if (!d->m_currentAccount.hasOnlineMapping())
539     return;
540 
541   d->accountsUpdateOnline(QList<MyMoneyAccount> { d->m_currentAccount } );
542 }
543