1 /***************************************************************************
2                           kaccountsview.cpp
3                              -------------------
4     copyright            : (C) 2007 by Thomas Baumgart <ipwizard@users.sourceforge.net>
5                            (C) 2017 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 #ifndef KACCOUNTSVIEW_P_H
18 #define KACCOUNTSVIEW_P_H
19 
20 #include "kaccountsview.h"
21 
22 // ----------------------------------------------------------------------------
23 // QT Includes
24 
25 #include <QAction>
26 #include <QPointer>
27 
28 // ----------------------------------------------------------------------------
29 // KDE Includes
30 
31 #include <KMessageBox>
32 
33 // ----------------------------------------------------------------------------
34 // Project Includes
35 
36 #include "ui_kaccountsview.h"
37 #include "kmymoneyaccountsviewbase_p.h"
38 
39 #include "mymoneyexception.h"
40 #include "mymoneysplit.h"
41 #include "mymoneyschedule.h"
42 #include "mymoneytransaction.h"
43 #include "knewaccountdlg.h"
44 #include "keditloanwizard.h"
45 #include "mymoneyaccount.h"
46 #include "mymoneymoney.h"
47 #include "mymoneyfile.h"
48 #include "accountsviewproxymodel.h"
49 #include "kmymoneyplugin.h"
50 #include "icons.h"
51 #include "mymoneyenums.h"
52 #include "menuenums.h"
53 #include "mymoneystatementreader.h"
54 #include "kmymoneyutils.h"
55 
56 using namespace Icons;
57 
58 class KAccountsViewPrivate : public KMyMoneyAccountsViewBasePrivate
59 {
Q_DECLARE_PUBLIC(KAccountsView)60   Q_DECLARE_PUBLIC(KAccountsView)
61 
62 public:
63   explicit KAccountsViewPrivate(KAccountsView *qq) :
64     q_ptr(qq),
65     ui(new Ui::KAccountsView),
66     m_haveUnusedCategories(false),
67     m_onlinePlugins(nullptr)
68   {
69   }
70 
~KAccountsViewPrivate()71   ~KAccountsViewPrivate()
72   {
73     delete ui;
74   }
75 
init()76   void init()
77   {
78     Q_Q(KAccountsView);
79     m_accountTree = &ui->m_accountTree;
80 
81     // setup icons for collapse and expand button
82     ui->m_collapseButton->setIcon(Icons::get(Icon::ListCollapse));
83     ui->m_expandButton->setIcon(Icons::get(Icon::ListExpand));
84 
85     m_proxyModel = ui->m_accountTree->init(View::Accounts);
86     q->connect(m_proxyModel, &AccountsProxyModel::unusedIncomeExpenseAccountHidden, q, &KAccountsView::slotUnusedIncomeExpenseAccountHidden);
87     q->connect(ui->m_searchWidget, &QLineEdit::textChanged, m_proxyModel, &QSortFilterProxyModel::setFilterFixedString);
88     q->connect(ui->m_accountTree, &KMyMoneyAccountTreeView::selectByObject, q, &KAccountsView::selectByObject);
89     q->connect(ui->m_accountTree, &KMyMoneyAccountTreeView::selectByVariant, q, &KAccountsView::selectByVariant);
90 
91     q->connect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, q, &KAccountsView::refresh);
92   }
93 
editLoan()94   void editLoan()
95   {
96     Q_Q(KAccountsView);
97     if (m_currentAccount.id().isEmpty())
98       return;
99 
100     const auto file = MyMoneyFile::instance();
101     if (file->isStandardAccount(m_currentAccount.id()))
102       return;
103 
104     QPointer<KEditLoanWizard> wizard = new KEditLoanWizard(m_currentAccount);
105     q->connect(wizard, &KEditLoanWizard::newCategory, q, &KAccountsView::slotNewCategory);
106     q->connect(wizard, &KEditLoanWizard::createPayee, q, &KAccountsView::slotNewPayee);
107     if (wizard->exec() == QDialog::Accepted && wizard != 0) {
108       MyMoneySchedule sch;
109       try {
110         sch = file->schedule(m_currentAccount.value("schedule").toLatin1());
111       } catch (const MyMoneyException &) {
112         qDebug() << "schedule" << m_currentAccount.value("schedule").toLatin1() << "not found";
113       }
114       if (!(m_currentAccount == wizard->account())
115           || !(sch == wizard->schedule())) {
116         MyMoneyFileTransaction ft;
117         try {
118           file->modifyAccount(wizard->account());
119           if (!sch.id().isEmpty()) {
120             sch = wizard->schedule();
121           }
122           try {
123             file->schedule(sch.id());
124             file->modifySchedule(sch);
125             ft.commit();
126           } catch (const MyMoneyException &) {
127             try {
128               if(sch.transaction().splitCount() >= 2) {
129                 file->addSchedule(sch);
130               }
131               ft.commit();
132             } catch (const MyMoneyException &e) {
133               qDebug("Cannot add schedule: '%s'", e.what());
134             }
135           }
136         } catch (const MyMoneyException &e) {
137           qDebug("Unable to modify account %s: '%s'", qPrintable(m_currentAccount.name()),
138                  e.what());
139         }
140       }
141     }
142     delete wizard;
143   }
144 
editAccount()145   void editAccount()
146   {
147     if (m_currentAccount.id().isEmpty())
148       return;
149 
150     const auto file = MyMoneyFile::instance();
151     if (file->isStandardAccount(m_currentAccount.id()))
152       return;
153 
154 
155     // set a status message so that the application can't be closed until the editing is done
156     //        slotStatusMsg(caption);
157 
158     auto tid = file->openingBalanceTransaction(m_currentAccount);
159     MyMoneyTransaction t;
160     MyMoneySplit s0, s1;
161     QPointer<KNewAccountDlg> dlg =
162         new KNewAccountDlg(m_currentAccount, true, false, 0, i18n("Edit account '%1'", m_currentAccount.name()));
163 
164     if (!tid.isEmpty()) {
165       try {
166         t = file->transaction(tid);
167         s0 = t.splitByAccount(m_currentAccount.id());
168         s1 = t.splitByAccount(m_currentAccount.id(), false);
169         dlg->setOpeningBalance(s0.shares());
170         if (m_currentAccount.accountGroup() == eMyMoney::Account::Type::Liability) {
171           dlg->setOpeningBalance(-s0.shares());
172         }
173       } catch (const MyMoneyException &e) {
174         qDebug() << "Error retrieving opening balance transaction " << tid << ": " << e.what() << "\n";
175         tid.clear();
176       }
177     }
178 
179     // check for online modules
180     QMap<QString, KMyMoneyPlugin::OnlinePlugin *>::const_iterator it_plugin;
181     if (m_onlinePlugins) {
182       it_plugin = m_onlinePlugins->constEnd();
183       const auto& kvp = m_currentAccount.onlineBankingSettings();
184       if (!kvp["provider"].isEmpty()) {
185         // if we have an online provider for this account, we need to check
186         // that we have the corresponding plugin. If that exists, we ask it
187         // to provide an additional tab for the account editor.
188         it_plugin = m_onlinePlugins->constFind(kvp["provider"].toLower());
189         if (it_plugin != m_onlinePlugins->constEnd()) {
190           QString name;
191           auto w = (*it_plugin)->accountConfigTab(m_currentAccount, name);
192           dlg->addTab(w, name);
193         }
194       }
195     }
196 
197     if (dlg != 0 && dlg->exec() == QDialog::Accepted) {
198       try {
199         MyMoneyFileTransaction ft;
200 
201         auto account = dlg->account();
202         auto parent = dlg->parentAccount();
203         if (m_onlinePlugins && it_plugin != m_onlinePlugins->constEnd()) {
204           account.setOnlineBankingSettings((*it_plugin)->onlineBankingSettings(account.onlineBankingSettings()));
205         }
206         auto bal = dlg->openingBalance();
207         if (m_currentAccount.accountGroup() == eMyMoney::Account::Type::Liability) {
208           bal = -bal;
209         }
210 
211         // we need to modify first, as reparent would override all other changes
212         file->modifyAccount(account);
213         if (account.parentAccountId() != parent.id()) {
214           file->reparentAccount(account, parent);
215         }
216         if (!tid.isEmpty() && dlg->openingBalance().isZero()) {
217           file->removeTransaction(t);
218 
219         } else if (!tid.isEmpty() && !dlg->openingBalance().isZero()) {
220           // update transaction only if changed
221           if ((s0.shares() != bal) || (t.postDate() != account.openingDate())) {
222             s0.setShares(bal);
223             s0.setValue(bal);
224             t.modifySplit(s0);
225             s1.setShares(-bal);
226             s1.setValue(-bal);
227             t.modifySplit(s1);
228             t.setPostDate(account.openingDate());
229             file->modifyTransaction(t);
230           }
231         } else if (tid.isEmpty() && !dlg->openingBalance().isZero()) {
232           file->createOpeningBalanceTransaction(m_currentAccount, bal);
233         }
234 
235         ft.commit();
236 
237         // reload the account object as it might have changed in the meantime
238         //            slotSelectAccount(file->account(account.id()));
239 
240       } catch (const MyMoneyException &e) {
241         Q_Q(KAccountsView);
242         KMessageBox::error(q, i18n("Unable to modify account '%1'. Cause: %2", m_currentAccount.name(), e.what()));
243       }
244     }
245 
246     delete dlg;
247     //        ready();
248 
249   }
250 
251   enum CanCloseAccountCodeE {
252     AccountCanClose = 0,        // can close the account
253     AccountBalanceNonZero,      // balance is non zero
254     AccountChildrenOpen,        // account has open children account
255     AccountScheduleReference,   // account is referenced in a schedule
256     AccountHasOnlineMapping,    // account has an online mapping
257   };
258 
259   /**
260     * This method checks, if an account can be closed or not. An account
261     * can be closed if:
262     *
263     * - the balance is zero and
264     * - all children are already closed and
265     * - there is no unfinished schedule referencing the account
266     * - and no online mapping is setup
267     *
268     * @param acc reference to MyMoneyAccount object in question
269     * @retval true account can be closed
270     * @retval false account cannot be closed
271     */
canCloseAccount(const MyMoneyAccount & acc)272   CanCloseAccountCodeE canCloseAccount(const MyMoneyAccount& acc)
273   {
274     // balance must be zero
275     if (!acc.balance().isZero())
276       return AccountBalanceNonZero;
277     if (acc.hasOnlineMapping())
278       return AccountHasOnlineMapping;
279 
280     // all children must be already closed
281     foreach (const auto sAccount, acc.accountList()) {
282       if (!MyMoneyFile::instance()->account(sAccount).isClosed()) {
283         return AccountChildrenOpen;
284       }
285     }
286 
287     // there must be no unfinished schedule referencing the account
288     QList<MyMoneySchedule> list = MyMoneyFile::instance()->scheduleList();
289     QList<MyMoneySchedule>::const_iterator it_l;
290     for (it_l = list.constBegin(); it_l != list.constEnd(); ++it_l) {
291       if ((*it_l).isFinished())
292         continue;
293       if ((*it_l).hasReferenceTo(acc.id()))
294         return AccountScheduleReference;
295     }
296     return AccountCanClose;
297   }
298 
299   /**
300    * This method checks if an account can be closed and enables/disables
301    * the close account action
302    * If disabled, it sets a tooltip explaining why it cannot be closed
303    * @brief enableCloseAccountAction
304    * @param acc reference to MyMoneyAccount object in question
305    */
hintCloseAccountAction(const MyMoneyAccount & acc,QAction * a)306   void hintCloseAccountAction(const MyMoneyAccount& acc, QAction* a)
307   {
308     switch (canCloseAccount(acc)) {
309       case AccountCanClose:
310         a->setToolTip(QString());
311         break;
312       case AccountBalanceNonZero:
313         a->setToolTip(i18n("The balance of the account must be zero before the account can be closed"));
314         break;
315       case AccountChildrenOpen:
316         a->setToolTip(i18n("All subaccounts must be closed before the account can be closed"));
317         break;
318       case AccountScheduleReference:
319         a->setToolTip(i18n("This account is still included in an active schedule"));
320         break;
321       case AccountHasOnlineMapping:
322         a->setToolTip(i18n("This account is still mapped to an online account"));
323         break;
324     }
325   }
326 
accountsUpdateOnline(const QList<MyMoneyAccount> & accList)327   void accountsUpdateOnline(const QList<MyMoneyAccount>& accList)
328   {
329     Q_Q(KAccountsView);
330 
331     // block the update account actions for now so that we don't get here twice
332     const QVector<eMenu::Action> disabledActions {eMenu::Action::UpdateAccount, eMenu::Action::UpdateAllAccounts};
333     for (const auto& a : disabledActions)
334       pActions[a]->setEnabled(false);
335 
336     // clear global message list
337     MyMoneyStatementReader::clearResultMessages();
338 
339     // process all entries that have a mapped account and the 'provider' is available
340     // we need to make sure that only the very last entry that matches sets the
341     // 'moreAccounts' parameter in the call to updateAccount() to false
342     auto processedAccounts = 0;
343 
344     for (auto it_provider = m_onlinePlugins->constBegin(); it_provider != m_onlinePlugins->constEnd(); ++it_provider) {
345       auto nextAccount = accList.cend();
346       for (auto it_a = accList.cbegin(); it_a != accList.cend(); ++it_a) {
347         if ((*it_a).hasOnlineMapping()
348         && (it_provider == m_onlinePlugins->constFind((*it_a).onlineBankingSettings().value("provider").toLower()))) {
349           if (nextAccount != accList.cend()) {
350             (*it_provider)->updateAccount(*nextAccount, true);
351           }
352           nextAccount = it_a;
353           ++processedAccounts;
354         }
355       }
356       // process a possible pending entry
357       if (nextAccount != accList.cend()) {
358         (*it_provider)->updateAccount(*nextAccount, false);
359       }
360     }
361 
362     // re-enable the disabled actions
363     q->updateActions(m_currentAccount);
364 
365     KMyMoneyUtils::showStatementImportResult(MyMoneyStatementReader::resultMessages(), processedAccounts);
366   }
367 
368   KAccountsView       *q_ptr;
369   Ui::KAccountsView   *ui;
370   bool                m_haveUnusedCategories;
371   MyMoneyAccount      m_currentAccount;
372   QMap<QString, KMyMoneyPlugin::OnlinePlugin*>* m_onlinePlugins;
373 };
374 
375 #endif
376