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