1 /*
2  * Copyright 2010-2014  Cristian Oneț <onet.cristian@gmail.com>
3  * Copyright 2017-2018  Łukasz Wojniłowicz <lukasz.wojnilowicz@gmail.com>
4  * Copyright 2020       Robert Szczesiak <dev.rszczesiak@gmail.com>
5  *
6  * This program is free software; you can redistribute it and/or
7  * modify it under the terms of the GNU General Public License as
8  * published by the Free Software Foundation; either version 2 of
9  * the License, or (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
18  */
19 
20 #include "accountsmodel.h"
21 
22 // ----------------------------------------------------------------------------
23 // QT Includes
24 
25 #include <QIcon>
26 
27 // ----------------------------------------------------------------------------
28 // KDE Includes
29 
30 #include <KLocalizedString>
31 
32 // ----------------------------------------------------------------------------
33 // Project Includes
34 
35 #include "mymoneyutils.h"
36 #include "mymoneymoney.h"
37 #include "mymoneyexception.h"
38 #include "mymoneyfile.h"
39 #include "mymoneyinstitution.h"
40 #include "mymoneyaccount.h"
41 #include "mymoneysecurity.h"
42 #include "mymoneyprice.h"
43 #include "kmymoneysettings.h"
44 #include "icons.h"
45 #include "modelenums.h"
46 #include "mymoneyenums.h"
47 #include "viewenums.h"
48 
49 using namespace Icons;
50 using namespace eAccountsModel;
51 using namespace eMyMoney;
52 
53 class AccountsModelPrivate
54 {
55   Q_DECLARE_PUBLIC(AccountsModel)
56 
57 public:
58   /**
59     * The pimpl.
60     */
AccountsModelPrivate(AccountsModel * qq)61   AccountsModelPrivate(AccountsModel *qq) :
62     q_ptr(qq),
63     m_file(MyMoneyFile::instance())
64   {
65     m_columns.append(Column::Account);
66   }
67 
~AccountsModelPrivate()68   virtual ~AccountsModelPrivate()
69   {
70   }
71 
init()72   void init()
73   {
74     Q_Q(AccountsModel);
75     QStringList headerLabels;
76     for (const auto& column : qAsConst(m_columns))
77       headerLabels.append(q->getHeaderName(column));
78     q->setHorizontalHeaderLabels(headerLabels);
79   }
80 
loadPreferredAccount(const MyMoneyAccount & acc,QStandardItem * fromNode,const int row,QStandardItem * toNode)81   void loadPreferredAccount(const MyMoneyAccount &acc, QStandardItem *fromNode /*accounts' regular node*/, const int row, QStandardItem *toNode /*accounts' favourite node*/)
82   {
83     if (acc.value(QStringLiteral("PreferredAccount")) != QLatin1String("Yes"))
84       return;
85 
86     auto favRow = toNode->rowCount();
87     if (auto favItem = itemFromAccountId(toNode, acc.id())) {
88         favRow = favItem->row();
89         toNode->removeRow(favRow);
90     }
91 
92     auto itemToClone = fromNode->child(row);
93     if (itemToClone)
94         toNode->insertRow(favRow, itemToClone->clone());
95   }
96 
97   /**
98     * Load all the sub-accounts recursively.
99     *
100     * @param model The model in which to load the data.
101     * @param accountsItem The item from the model of the parent account of the sub-accounts which are being loaded.
102     * @param favoriteAccountsItem The item of the favorites accounts groups so favorite accounts can be added here also.
103     * @param list The list of the account id's of the sub-accounts which are being loaded.
104     *
105     */
loadSubaccounts(QStandardItem * node,QStandardItem * favoriteAccountsItem,const QStringList & subaccounts)106   void loadSubaccounts(QStandardItem *node, QStandardItem *favoriteAccountsItem, const QStringList& subaccounts)
107   {
108     for (const auto& subaccStr : subaccounts) {
109       const auto subacc = m_file->account(subaccStr);
110 
111       auto item = new QStandardItem(subacc.name());                         // initialize first column of subaccount
112       node->appendRow(item);                                                // add subaccount row to node
113       item->setEditable(false);
114 
115       item->setData(node->data((int)Role::DisplayOrder), (int)Role::DisplayOrder);        // inherit display order role from node
116 
117       loadSubaccounts(item, favoriteAccountsItem, subacc.accountList());    // subaccount may have subaccounts as well
118 
119       // set the account data after the children have been loaded
120       const auto row = item->row();
121       setAccountData(node, row, subacc, m_columns);                          // initialize rest of columns of subaccount
122       loadPreferredAccount(subacc, node, row, favoriteAccountsItem);         // add to favourites node if preferred
123     }
124   }
125 
126   /**
127     * Note: this functions should only be called after the child account data has been set.
128     */
setAccountData(QStandardItem * node,const int row,const MyMoneyAccount & account,const QList<Column> & columns)129   void setAccountData(QStandardItem *node, const int row, const MyMoneyAccount &account, const QList<Column> &columns)
130   {
131     QStandardItem *cell;
132 
133     auto getCell = [&, row](const auto column) {
134       cell = node->child(row, column);      // try to get QStandardItem
135       if (!cell) {                          // it may be uninitialized
136         cell = new QStandardItem;           // so create one
137         node->setChild(row, column, cell);  // and add it under the node
138       }
139     };
140 
141     auto colNum = m_columns.indexOf(Column::Account);
142     if (colNum == -1)
143       return;
144     getCell(colNum);
145     auto font = cell->data(Qt::FontRole).value<QFont>();
146     // display the names of closed accounts with strikeout font
147     if (account.isClosed() != font.strikeOut())
148       font.setStrikeOut(account.isClosed());
149 
150     if (columns.contains(Column::Account)) {
151       // setting account column
152       cell->setData(account.name(), Qt::DisplayRole);
153 //      cell->setData(QVariant::fromValue(account), (int)Role::Account); // is set in setAccountBalanceAndValue
154       cell->setData(QVariant(account.id()), (int)Role::ID);
155       cell->setData(QVariant(account.value("PreferredAccount") == QLatin1String("Yes")), (int)Role::Favorite);
156       cell->setData(QVariant(QIcon(account.accountPixmap(m_reconciledAccount.id().isEmpty() ? false : account.id() == m_reconciledAccount.id()))), Qt::DecorationRole);
157       cell->setData(MyMoneyFile::instance()->accountToCategory(account.id(), true), (int)Role::FullName);
158       cell->setData(font, Qt::FontRole);
159     }
160 
161     // Type
162     if (columns.contains(Column::Type)) {
163       colNum = m_columns.indexOf(Column::Type);
164       if (colNum != -1) {
165         getCell(colNum);
166         cell->setData(account.accountTypeToString(account.accountType()), Qt::DisplayRole);
167         cell->setData(font, Qt::FontRole);
168       }
169     }
170 
171     // Account's number
172     if (columns.contains(Column::AccountNumber)) {
173       colNum = m_columns.indexOf(Column::AccountNumber);
174       if (colNum != -1) {
175         getCell(colNum);
176         cell->setData(account.number(), Qt::DisplayRole);
177         cell->setData(font, Qt::FontRole);
178       }
179     }
180 
181     // Account's sort code
182     if (columns.contains(Column::AccountSortCode)) {
183       colNum = m_columns.indexOf(Column::AccountSortCode);
184       if (colNum != -1) {
185         getCell(colNum);
186         cell->setData(account.value("iban"), Qt::DisplayRole);
187         cell->setData(font, Qt::FontRole);
188       }
189     }
190 
191     const auto checkMark = Icons::get(Icon::DialogOK);
192     switch (account.accountType()) {
193       case Account::Type::Income:
194       case Account::Type::Expense:
195       case Account::Type::Asset:
196       case Account::Type::Liability:
197         // Tax
198         if (columns.contains(Column::Tax)) {
199           colNum = m_columns.indexOf(Column::Tax);
200           if (colNum != -1) {
201             getCell(colNum);
202             if (account.value("Tax").toLower() == "yes")
203               cell->setData(checkMark, Qt::DecorationRole);
204             else
205               cell->setData(QIcon(), Qt::DecorationRole);
206           }
207         }
208 
209         // VAT Account
210         if (columns.contains(Column::VAT)) {
211           colNum = m_columns.indexOf(Column::VAT);
212           if (colNum != -1) {
213             getCell(colNum);
214             if (!account.value("VatAccount").isEmpty()) {
215               const auto vatAccount = MyMoneyFile::instance()->account(account.value("VatAccount"));
216               cell->setData(vatAccount.name(), Qt::DisplayRole);
217               cell->setData(QVariant(Qt::AlignLeft | Qt::AlignVCenter), Qt::TextAlignmentRole);
218 
219               // VAT Rate
220             } else if (!account.value("VatRate").isEmpty()) {
221               const auto vatRate = MyMoneyMoney(account.value("VatRate")) * MyMoneyMoney(100, 1);
222               cell->setData(QString::fromLatin1("%1 %").arg(vatRate.formatMoney(QString(), 1)), Qt::DisplayRole);
223               cell->setData(QVariant(Qt::AlignRight | Qt::AlignVCenter), Qt::TextAlignmentRole);
224 
225             } else {
226               cell->setData(QString(), Qt::DisplayRole);
227             }
228           }
229         }
230 
231         // CostCenter
232         if (columns.contains(Column::CostCenter)) {
233           colNum = m_columns.indexOf(Column::CostCenter);
234           if (colNum != -1) {
235             getCell(colNum);
236             if (account.isCostCenterRequired())
237               cell->setData(checkMark, Qt::DecorationRole);
238             else
239               cell->setData(QIcon(), Qt::DecorationRole);
240           }
241         }
242         break;
243       default:
244         break;
245     }
246 
247     // balance and value
248     setAccountBalanceAndValue(node, row, account, columns);
249   }
250 
setInstitutionTotalValue(QStandardItem * node,const int row)251   void setInstitutionTotalValue(QStandardItem *node, const int row)
252   {
253     const auto colInstitution = m_columns.indexOf(Column::Account);
254     auto itInstitution = node->child(row, colInstitution);
255     const auto valInstitution = childrenTotalValue(itInstitution, true);
256     itInstitution->setData(QVariant::fromValue(valInstitution ), (int)Role::TotalValue);
257 
258     const auto colTotalValue = m_columns.indexOf(Column::TotalValue);
259     if (colTotalValue == -1)
260       return;
261     auto cell = node->child(row, colTotalValue);
262     if (!cell) {
263       cell = new QStandardItem;
264       node->setChild(row, colTotalValue, cell);
265     }
266     const auto fontColor = KMyMoneySettings::schemeColor(valInstitution.isNegative() ? SchemeColor::Negative : SchemeColor::Positive);
267     cell->setData(QVariant(fontColor),                                               Qt::ForegroundRole);
268     cell->setData(QVariant(itInstitution->data(Qt::FontRole).value<QFont>()),        Qt::FontRole);
269     cell->setData(QVariant(Qt::AlignRight | Qt::AlignVCenter),                       Qt::TextAlignmentRole);
270     cell->setData(MyMoneyUtils::formatMoney(valInstitution, m_file->baseCurrency()), Qt::DisplayRole);
271   }
272 
setAccountBalanceAndValue(QStandardItem * node,const int row,const MyMoneyAccount & account,const QList<Column> & columns)273   void setAccountBalanceAndValue(QStandardItem *node, const int row, const MyMoneyAccount &account, const QList<Column> &columns)
274   {
275     QStandardItem *cell;
276 
277     auto getCell = [&, row](auto column)
278     {
279       cell = node->child(row, column);
280       if (!cell) {
281         cell = new QStandardItem;
282         node->setChild(row, column, cell);
283       }
284     };
285 
286     // setting account column
287     auto colNum = m_columns.indexOf(Column::Account);
288     if (colNum == -1)
289       return;
290     getCell(colNum);
291 
292     MyMoneyMoney accountBalance, accountValue, accountTotalValue;
293     if (columns.contains(Column::Account)) { // update values only when requested
294       accountBalance    = balance(account);
295       accountValue      = value(account, accountBalance);
296       accountTotalValue = childrenTotalValue(cell) + accountValue;
297       cell->setData(QVariant::fromValue(account),           (int)Role::Account);
298       cell->setData(QVariant::fromValue(accountBalance),    (int)Role::Balance);
299       cell->setData(QVariant::fromValue(accountValue),      (int)Role::Value);
300       cell->setData(QVariant::fromValue(accountTotalValue), (int)Role::TotalValue);
301     } else {  // otherwise save up on tedious calculations
302       accountBalance    = cell->data((int)Role::Balance).value<MyMoneyMoney>();
303       accountValue      = cell->data((int)Role::Value).value<MyMoneyMoney>();
304       accountTotalValue = cell->data((int)Role::TotalValue).value<MyMoneyMoney>();
305     }
306 
307     const auto font = QVariant(cell->data(Qt::FontRole).value<QFont>());
308     const auto alignment = QVariant(Qt::AlignRight | Qt::AlignVCenter);
309 
310     // setting total balance column
311     if (columns.contains(Column::TotalBalance)) {
312       colNum = m_columns.indexOf(Column::TotalBalance);
313       if (colNum != -1) {
314         const auto accountBalanceStr = QVariant::fromValue(MyMoneyUtils::formatMoney(accountBalance, m_file->security(account.currencyId())));
315         getCell(colNum);
316         // only show the balance, if its a different security/currency
317         if (m_file->security(account.currencyId()) != m_file->baseCurrency()) {
318           cell->setData(accountBalanceStr, Qt::DisplayRole);
319         }
320         cell->setData(font,       Qt::FontRole);
321         cell->setData(alignment,  Qt::TextAlignmentRole);
322       }
323     }
324 
325     // setting posted value column
326     if (columns.contains(Column::PostedValue)) {
327       colNum = m_columns.indexOf(Column::PostedValue);
328       if (colNum != -1) {
329         const auto accountValueStr = QVariant::fromValue(MyMoneyUtils::formatMoney(accountValue, m_file->baseCurrency()));
330         getCell(colNum);
331         const auto fontColor = KMyMoneySettings::schemeColor(accountValue.isNegative() ? SchemeColor::Negative : SchemeColor::Positive);
332         cell->setData(QVariant(fontColor),  Qt::ForegroundRole);
333         cell->setData(accountValueStr,      Qt::DisplayRole);
334         cell->setData(font,                 Qt::FontRole);
335         cell->setData(alignment,            Qt::TextAlignmentRole);
336       }
337     }
338 
339     // setting total value column
340     if (columns.contains(Column::TotalValue)) {
341       colNum = m_columns.indexOf(Column::TotalValue);
342       if (colNum != -1) {
343         const auto accountTotalValueStr = QVariant::fromValue(MyMoneyUtils::formatMoney(accountTotalValue, m_file->baseCurrency()));
344         getCell(colNum);
345         const auto fontColor = KMyMoneySettings::schemeColor(accountTotalValue.isNegative() ? SchemeColor::Negative : SchemeColor::Positive);
346         cell->setData(accountTotalValueStr, Qt::DisplayRole);
347         cell->setData(font,                 Qt::FontRole);
348         cell->setData(QVariant(fontColor),  Qt::ForegroundRole);
349         cell->setData(alignment,            Qt::TextAlignmentRole);
350       }
351     }
352   }
353 
354   /**
355     * Compute the balance of the given account.
356     *
357     * @param account The account for which the balance is being computed.
358     */
balance(const MyMoneyAccount & account)359   MyMoneyMoney balance(const MyMoneyAccount &account)
360   {
361     MyMoneyMoney balance;
362     // a closed account has a zero balance by definition
363     if (!account.isClosed()) {
364       // account.balance() is not compatible with stock accounts
365       if (account.isInvest())
366         balance = m_file->balance(account.id());
367       else
368         balance = account.balance();
369     }
370 
371     // for income and liability accounts, we reverse the sign
372     switch (account.accountGroup()) {
373       case Account::Type::Income:
374       case Account::Type::Liability:
375       case Account::Type::Equity:
376         balance = -balance;
377         break;
378 
379       default:
380         break;
381     }
382 
383     return balance;
384   }
385 
386   /**
387     * Compute the value of the given account using the provided balance.
388     * The value is defined as the balance of the account converted to the base currency.
389     *
390     * @param account The account for which the value is being computed.
391     * @param balance The balance which should be used.
392     *
393     * @see balance
394     */
value(const MyMoneyAccount & account,const MyMoneyMoney & balance)395   MyMoneyMoney value(const MyMoneyAccount &account, const MyMoneyMoney &balance)
396   {
397     if (account.isClosed())
398       return MyMoneyMoney();
399 
400     QList<MyMoneyPrice> prices;
401     MyMoneySecurity security = m_file->baseCurrency();
402     try {
403       if (account.isInvest()) {
404         security = m_file->security(account.currencyId());
405         prices += m_file->price(account.currencyId(), security.tradingCurrency());
406         if (security.tradingCurrency() != m_file->baseCurrency().id()) {
407           MyMoneySecurity sec = m_file->security(security.tradingCurrency());
408           prices += m_file->price(sec.id(), m_file->baseCurrency().id());
409         }
410       } else if (account.currencyId() != m_file->baseCurrency().id()) {
411         security = m_file->security(account.currencyId());
412         prices += m_file->price(account.currencyId(), m_file->baseCurrency().id());
413       }
414 
415     } catch (const MyMoneyException &e) {
416       qDebug() << Q_FUNC_INFO << " caught exception while adding " << account.name() << "[" << account.id() << "]: " << e.what();
417     }
418 
419     MyMoneyMoney value = balance;
420     {
421       QList<MyMoneyPrice>::const_iterator it_p;
422       QString securityID = account.currencyId();
423       for (it_p = prices.constBegin(); it_p != prices.constEnd(); ++it_p) {
424         value = (value * (MyMoneyMoney::ONE / (*it_p).rate(securityID))).convertPrecision(m_file->security(securityID).pricePrecision());
425         if ((*it_p).from() == securityID)
426           securityID = (*it_p).to();
427         else
428           securityID = (*it_p).from();
429       }
430       value = value.convert(m_file->baseCurrency().smallestAccountFraction());
431     }
432 
433     return value;
434   }
435 
436   /**
437     * Compute the total value of the child accounts of the given account.
438     * Note that the value of the current account is not in this sum. Also,
439     * before calling this function, the caller must make sure that the values
440     * of all sub-account must be already in the model in the @ref Role::Value.
441     *
442     * @param index The index of the account in the model.
443     * @see value
444     */
childrenTotalValue(const QStandardItem * node,const bool isInstitutionsModel=false)445   MyMoneyMoney childrenTotalValue(const QStandardItem *node, const bool isInstitutionsModel = false)
446   {
447     MyMoneyMoney totalValue;
448     if (!node)
449       return totalValue;
450 
451     for (auto i = 0; i < node->rowCount(); ++i) {
452       const auto childNode = node->child(i, (int)Column::Account);
453       if (childNode->hasChildren())
454         totalValue += childrenTotalValue(childNode, isInstitutionsModel);
455       const auto data = childNode->data((int)Role::Value);
456       if (data.isValid()) {
457         auto value = data.value<MyMoneyMoney>();
458         if (isInstitutionsModel) {
459           const auto account = childNode->data((int)Role::Account).value<MyMoneyAccount>();
460           if (account.accountGroup() == Account::Type::Liability)
461             value = -value;
462         }
463       totalValue += value;
464       }
465     }
466     return totalValue;
467   }
468 
469   /**
470     * Function to get the item from an account id.
471     *
472     * @param parent The parent to localize the search in the child items of this parameter.
473     * @param accountId Search based on this parameter.
474     *
475     * @return The item corresponding to the given account id, NULL if the account was not found.
476     */
itemFromAccountId(QStandardItem * parent,const QString & accountId)477   QStandardItem *itemFromAccountId(QStandardItem *parent, const QString &accountId) {
478     auto const model = parent->model();
479     const auto list = model->match(model->index(0, 0, parent->index()), (int)Role::ID, QVariant(accountId), 1, Qt::MatchFlags(Qt::MatchExactly | Qt::MatchCaseSensitive));
480     if (!list.isEmpty())
481       return model->itemFromIndex(list.front());
482     // TODO: if not found at this item search for it in the model and if found reparent it.
483     return nullptr;
484   }
485 
486   /**
487     * Function to get the item from an account id without knowing it's parent item.
488     * Note that for the accounts which have two items in the model (favorite accounts)
489     * the account item which is not the child of the favorite accounts item is always returned.
490     *
491     * @param model The model in which to search.
492     * @param accountId Search based on this parameter.
493     *
494     * @return The item corresponding to the given account id, NULL if the account was not found.
495     */
itemFromAccountId(QStandardItemModel * model,const QString & accountId)496   QStandardItem *itemFromAccountId(QStandardItemModel *model, const QString &accountId)
497   {
498     const auto list = model->match(model->index(0, 0), (int)Role::ID, QVariant(accountId), -1, Qt::MatchFlags(Qt::MatchExactly | Qt::MatchCaseSensitive | Qt::MatchRecursive));
499     for (const auto& index : list) {
500       // always return the account which is not the child of the favorite accounts item
501       if (index.parent().data((int)Role::ID).toString() != AccountsModel::favoritesAccountId)
502         return model->itemFromIndex(index);
503     }
504     return nullptr;
505   }
506 
507   AccountsModel *q_ptr;
508 
509   /**
510     * Used to load the accounts data.
511     */
512   MyMoneyFile *m_file;
513   /**
514     * Used to emit the @ref netWorthChanged signal.
515     */
516   MyMoneyMoney m_lastNetWorth;
517   /**
518     * Used to emit the @ref profitChanged signal.
519     */
520   MyMoneyMoney m_lastProfit;
521   /**
522     * Used to set the reconciliation flag.
523     */
524   MyMoneyAccount m_reconciledAccount;
525 
526   QList<Column> m_columns;
527   static const QString m_accountsModelConfGroup;
528   static const QString m_accountsModelColumnSelection;
529 };
530 
531 const QString AccountsModelPrivate::m_accountsModelConfGroup = QStringLiteral("AccountsModel");
532 const QString AccountsModelPrivate::m_accountsModelColumnSelection = QStringLiteral("ColumnSelection");
533 
534 const QString AccountsModel::favoritesAccountId(QStringLiteral("Favorites"));
535 
536 /**
537   * The constructor is private so that only the @ref Models object can create such an object.
538   */
AccountsModel(QObject * parent)539 AccountsModel::AccountsModel(QObject *parent) :
540   QStandardItemModel(parent),
541   d_ptr(new AccountsModelPrivate(this))
542 {
543   Q_D(AccountsModel);
544   d->init();
545 }
546 
AccountsModel(AccountsModelPrivate & dd,QObject * parent)547 AccountsModel::AccountsModel(AccountsModelPrivate &dd, QObject *parent) :
548   QStandardItemModel(parent),
549   d_ptr(&dd)
550 {
551   Q_D(AccountsModel);
552   d->init();
553 }
554 
~AccountsModel()555 AccountsModel::~AccountsModel()
556 {
557   Q_D(AccountsModel);
558   delete d;
559 }
560 
561 /**
562   * Perform the initial load of the model data
563   * from the @ref MyMoneyFile.
564   *
565   */
load()566 void AccountsModel::load()
567 {
568   Q_D(AccountsModel);
569   blockSignals(true);
570   QStandardItem *rootItem = invisibleRootItem();
571 
572   QFont font;
573   font.setBold(true);
574 
575   // adding favourite accounts node
576   auto favoriteAccountsItem = new QStandardItem();
577   favoriteAccountsItem->setEditable(false);
578   rootItem->appendRow(favoriteAccountsItem);
579   {
580     QMap<int, QVariant> itemData;
581     itemData[Qt::DisplayRole] = itemData[Qt::EditRole] = itemData[(int)Role::FullName] = i18n("Favorites");
582     itemData[Qt::FontRole] = font;
583     itemData[Qt::DecorationRole] = Icons::get(Icon::BankAccount);
584     itemData[(int)Role::ID] = favoritesAccountId;
585     itemData[(int)Role::DisplayOrder] = 0;
586     this->setItemData(favoriteAccountsItem->index(), itemData);
587   }
588 
589   // adding account categories (asset, liability, etc.) node
590   const QVector <Account::Type> categories {
591     Account::Type::Asset, Account::Type::Liability,
592     Account::Type::Income, Account::Type::Expense,
593     Account::Type::Equity
594   };
595 
596   for (const auto category : categories) {
597     MyMoneyAccount account;
598     QString accountName;
599     int displayOrder;
600 
601     switch (category) {
602       case Account::Type::Asset:
603         // Asset accounts
604         account = d->m_file->asset();
605         accountName = i18n("Asset accounts");
606         displayOrder = 1;
607         break;
608       case Account::Type::Liability:
609         // Liability accounts
610         account = d->m_file->liability();
611         accountName = i18n("Liability accounts");
612         displayOrder = 2;
613         break;
614       case Account::Type::Income:
615         // Income categories
616         account = d->m_file->income();
617         accountName = i18n("Income categories");
618         displayOrder = 3;
619         break;
620       case Account::Type::Expense:
621         // Expense categories
622         account = d->m_file->expense();
623         accountName = i18n("Expense categories");
624         displayOrder = 4;
625         break;
626       case Account::Type::Equity:
627         // Equity accounts
628         account = d->m_file->equity();
629         accountName = i18n("Equity accounts");
630         displayOrder = 5;
631         break;
632       default:
633         continue;
634     }
635 
636     auto accountsItem = new QStandardItem(accountName);
637     accountsItem->setEditable(false);
638     rootItem->appendRow(accountsItem);
639 
640     {
641       QMap<int, QVariant> itemData;
642       itemData[Qt::DisplayRole] = accountName;
643       itemData[(int)Role::FullName] = itemData[Qt::EditRole] = QVariant::fromValue(MyMoneyFile::instance()->accountToCategory(account.id(), true));
644       itemData[Qt::FontRole] = font;
645       itemData[(int)Role::DisplayOrder] = displayOrder;
646       this->setItemData(accountsItem->index(), itemData);
647     }
648 
649     // adding accounts (specific bank/investment accounts) belonging to given accounts category
650     const auto&  accountList = account.accountList();
651     for (const auto& accStr : accountList) {
652       const auto acc = d->m_file->account(accStr);
653 
654       auto item = new QStandardItem(acc.name());
655       accountsItem->appendRow(item);
656       item->setEditable(false);
657       auto subaccountsStr = acc.accountList();
658       // filter out stocks with zero balance if requested by user
659       for (auto subaccStr = subaccountsStr.begin(); subaccStr != subaccountsStr.end();) {
660         const auto subacc = d->m_file->account(*subaccStr);
661         if (subacc.isInvest() && KMyMoneySettings::hideZeroBalanceEquities() && subacc.balance().isZero())
662           subaccStr = subaccountsStr.erase(subaccStr);
663         else
664           ++subaccStr;
665       }
666 
667       // adding subaccounts (e.g. stocks under given investment account) belonging to given account
668       d->loadSubaccounts(item, favoriteAccountsItem, subaccountsStr);
669       const auto row = item->row();
670       d->setAccountData(accountsItem, row, acc, d->m_columns);
671       d->loadPreferredAccount(acc, accountsItem, row, favoriteAccountsItem);
672     }
673 
674     d->setAccountData(rootItem, accountsItem->row(), account, d->m_columns);
675   }
676 
677   blockSignals(false);
678   checkNetWorth();
679   checkProfit();
680 }
681 
accountById(const QString & id) const682 QModelIndex AccountsModel::accountById(const QString& id) const
683 {
684   QModelIndexList accountList = match(index(0, 0),
685                                     (int)Role::ID,
686                                     id,
687                                     1,
688                                     Qt::MatchFlags(Qt::MatchExactly | Qt::MatchRecursive));
689 
690   if(accountList.count() == 1) {
691     return accountList.first();
692   }
693   return QModelIndex();
694 }
695 
getColumns()696 QList<Column> *AccountsModel::getColumns()
697 {
698   Q_D(AccountsModel);
699   return &d->m_columns;
700 }
701 
setColumnVisibility(const Column column,const bool show)702 void AccountsModel::setColumnVisibility(const Column column, const bool show)
703 {
704   Q_D(AccountsModel);
705   const auto ixCol = d->m_columns.indexOf(column);  // get column index in our column's map
706   if (!show && ixCol != -1) {                       // start removing column row by row from bottom to up
707     d->m_columns.removeOne(column);                 // remove it from our column's map
708     blockSignals(true);                             // block signals to not emit resources consuming dataChanged
709     for (auto i = 0; i < rowCount(); ++i) {
710       // recursive lambda function to remove cell belonging to unwanted column from all rows
711       auto removeCellFromRow = [=](auto &&self, QStandardItem *item) -> bool {
712         for(auto j = 0; j < item->rowCount(); ++j) {
713           auto childItem = item->child(j);
714           if (childItem->hasChildren())
715             self(self, childItem);
716           childItem->removeColumn(ixCol);
717         }
718         return true;
719       };
720 
721       auto topItem = item(i);
722       if (topItem->hasChildren())
723         removeCellFromRow(removeCellFromRow, topItem);
724       topItem->removeColumn(ixCol);
725     }
726     blockSignals(false);                           // unblock signals, so model can update itself with new column
727     removeColumn(ixCol);                           // remove column from invisible root item which triggers model's view update
728   } else if (show && ixCol == -1) {                // start inserting columns row by row  from up to bottom (otherwise columns will be inserted automatically)
729     auto model = qobject_cast<InstitutionsModel *>(this);
730     const auto isInstitutionsModel = model ? true : false;  // if it's institution's model, then don't set any data on institution nodes
731 
732     auto newColPos = 0;
733     for(; newColPos < d->m_columns.count(); ++newColPos) {
734       if (d->m_columns.at(newColPos) > column)
735         break;
736     }
737     d->m_columns.insert(newColPos, column);       // insert columns according to enum order for cleanliness
738 
739     insertColumn(newColPos);
740     setHorizontalHeaderItem(newColPos, new QStandardItem(getHeaderName(column)));
741     blockSignals(true);
742     for (auto i = 0; i < rowCount(); ++i) {
743       // recursive lambda function to remove cell belonging to unwanted column from all rows
744       auto addCellToRow = [&, newColPos](auto &&self, QStandardItem *item) -> bool {
745         for(auto j = 0; j < item->rowCount(); ++j) {
746           auto childItem = item->child(j);
747           childItem->insertColumns(newColPos, 1);
748           if (childItem->hasChildren())
749             self(self, childItem);
750           d->setAccountData(item, j, childItem->data((int)Role::Account).value<MyMoneyAccount>(), QList<Column> {column});
751         }
752         return true;
753       };
754 
755       auto topItem = item(i);
756       topItem->insertColumns(newColPos, 1);
757       if (topItem->hasChildren())
758         addCellToRow(addCellToRow, topItem);
759 
760       if (isInstitutionsModel)
761         d->setInstitutionTotalValue(invisibleRootItem(), i);
762       else if (i !=0)  // favourites node doesn't play well here, so exclude it from update
763         d->setAccountData(invisibleRootItem(), i, topItem->data((int)Role::Account).value<MyMoneyAccount>(), QList<Column> {column});
764     }
765     blockSignals(false);
766   }
767 }
768 
getHeaderName(const Column column)769 QString AccountsModel::getHeaderName(const Column column)
770 {
771   switch(column) {
772     case Column::Account:
773       return i18n("Account");
774     case Column::Type:
775       return i18n("Type");
776     case Column::Tax:
777       return i18nc("Column heading for category in tax report", "Tax");
778     case Column::VAT:
779       return i18nc("Column heading for VAT category", "VAT");
780     case Column::CostCenter:
781       return i18nc("Column heading for Cost Center", "CC");
782     case Column::TotalBalance:
783       return i18n("Total Balance");
784     case Column::PostedValue:
785       return i18n("Posted Value");
786     case Column::TotalValue:
787       return i18n("Total Value");
788     case Column::AccountNumber:
789       return i18n("Number");
790     case Column::AccountSortCode:
791       return i18nc("IBAN, SWIFT, etc.", "Sort Code");
792     default:
793       return QString();
794   }
795 }
796 
797 /**
798   * Check if netWorthChanged should be emitted.
799   */
checkNetWorth()800 void AccountsModel::checkNetWorth()
801 {
802   Q_D(AccountsModel);
803   // compute the net worth
804   QModelIndexList assetList = match(index(0, 0),
805                                     (int)Role::ID,
806                                     MyMoneyFile::instance()->asset().id(),
807                                     1,
808                                     Qt::MatchFlags(Qt::MatchExactly | Qt::MatchCaseSensitive));
809 
810   QModelIndexList liabilityList = match(index(0, 0),
811                                         (int)Role::ID,
812                                         MyMoneyFile::instance()->liability().id(),
813                                         1,
814                                         Qt::MatchFlags(Qt::MatchExactly | Qt::MatchCaseSensitive));
815 
816   MyMoneyMoney netWorth;
817   if (!assetList.isEmpty() && !liabilityList.isEmpty()) {
818     const auto  assetValue = data(assetList.front(), (int)Role::TotalValue);
819     const auto  liabilityValue = data(liabilityList.front(), (int)Role::TotalValue);
820 
821     if (assetValue.isValid() && liabilityValue.isValid())
822       netWorth = assetValue.value<MyMoneyMoney>() - liabilityValue.value<MyMoneyMoney>();
823   }
824   if (d->m_lastNetWorth != netWorth) {
825     d->m_lastNetWorth = netWorth;
826     emit netWorthChanged(QVariantList {QVariant::fromValue(d->m_lastNetWorth)}, eView::Intent::UpdateNetWorth);
827   }
828 }
829 
830 /**
831   * Check if profitChanged should be emitted.
832   */
checkProfit()833 void AccountsModel::checkProfit()
834 {
835   Q_D(AccountsModel);
836   // compute the profit
837   const auto incomeList = match(index(0, 0),
838                                 (int)Role::ID,
839                                 MyMoneyFile::instance()->income().id(),
840                                 1,
841                                 Qt::MatchFlags(Qt::MatchExactly | Qt::MatchCaseSensitive));
842 
843   const auto expenseList = match(index(0, 0),
844                                  (int)Role::ID,
845                                  MyMoneyFile::instance()->expense().id(),
846                                  1,
847                                  Qt::MatchFlags(Qt::MatchExactly | Qt::MatchCaseSensitive));
848 
849   MyMoneyMoney profit;
850   if (!incomeList.isEmpty() && !expenseList.isEmpty()) {
851     const auto incomeValue = data(incomeList.front(), (int)Role::TotalValue);
852     const auto expenseValue = data(expenseList.front(), (int)Role::TotalValue);
853 
854     if (incomeValue.isValid() && expenseValue.isValid())
855       profit = incomeValue.value<MyMoneyMoney>() - expenseValue.value<MyMoneyMoney>();
856   }
857   if (d->m_lastProfit != profit) {
858     d->m_lastProfit = profit;
859     emit profitChanged(QVariantList {QVariant::fromValue(d->m_lastProfit)}, eView::Intent::UpdateProfit);
860   }
861 }
862 
accountValue(const MyMoneyAccount & account,const MyMoneyMoney & balance)863 MyMoneyMoney AccountsModel::accountValue(const MyMoneyAccount &account, const MyMoneyMoney &balance)
864 {
865   Q_D(AccountsModel);
866   return d->value(account, balance);
867 }
868 
869 /**
870   * This slot should be connected so that the model will be notified which account is being reconciled.
871   */
slotReconcileAccount(const MyMoneyAccount & account,const QDate & reconciliationDate,const MyMoneyMoney & endingBalance)872 void AccountsModel::slotReconcileAccount(const MyMoneyAccount &account, const QDate &reconciliationDate, const MyMoneyMoney &endingBalance)
873 {
874   Q_D(AccountsModel);
875   Q_UNUSED(reconciliationDate)
876   Q_UNUSED(endingBalance)
877   if (d->m_reconciledAccount.id() != account.id()) {
878     // first clear the flag of the old reconciliation account
879     if (!d->m_reconciledAccount.id().isEmpty()) {
880       const auto list = match(index(0, 0), (int)Role::ID, QVariant(d->m_reconciledAccount.id()), -1, Qt::MatchFlags(Qt::MatchExactly | Qt::MatchCaseSensitive | Qt::MatchRecursive));
881       for (const auto& index : list)
882         setData(index, QVariant(QIcon(account.accountPixmap(false))), Qt::DecorationRole);
883     }
884 
885     // then set the reconciliation flag of the new reconciliation account
886     const auto list = match(index(0, 0), (int)Role::ID, QVariant(account.id()), -1, Qt::MatchFlags(Qt::MatchExactly | Qt::MatchCaseSensitive | Qt::MatchRecursive));
887     for (const auto& index : list)
888       setData(index, QVariant(QIcon(account.accountPixmap(true))), Qt::DecorationRole);
889     d->m_reconciledAccount = account;
890   }
891 }
892 
893 /**
894   * Notify the model that an object has been added. An action is performed only if the object is an account.
895   *
896   */
slotObjectAdded(File::Object objType,const QString & id)897 void AccountsModel::slotObjectAdded(File::Object objType, const QString& id)
898 {
899   Q_D(AccountsModel);
900   if (objType != File::Object::Account)
901     return;
902 
903   const auto account = MyMoneyFile::instance()->account(id);
904 
905   auto favoriteAccountsItem = d->itemFromAccountId(this, favoritesAccountId);
906   auto parentAccountItem = d->itemFromAccountId(this, account.parentAccountId());
907   auto item = d->itemFromAccountId(parentAccountItem, account.id());
908   if (!item) {
909     item = new QStandardItem(account.name());
910     parentAccountItem->appendRow(item);
911     item->setEditable(false);
912   }
913   // load the sub-accounts if there are any - there could be sub accounts if this is an add operation
914   // that was triggered in slotObjectModified on an already existing account which went trough a hierarchy change
915   d->loadSubaccounts(item, favoriteAccountsItem, account.accountList());
916 
917   const auto row = item->row();
918   d->setAccountData(parentAccountItem, row, account, d->m_columns);
919   d->loadPreferredAccount(account, parentAccountItem, row, favoriteAccountsItem);
920 
921   checkNetWorth();
922   checkProfit();
923 }
924 
925 /**
926   * Notify the model that an object has been modified. An action is performed only if the object is an account.
927   *
928   */
slotObjectModified(File::Object objType,const QString & id)929 void AccountsModel::slotObjectModified(File::Object objType, const QString& id)
930 {
931   Q_D(AccountsModel);
932   if (objType != File::Object::Account)
933     return;
934 
935   const auto account = MyMoneyFile::instance()->account(id);
936   auto accountItem = d->itemFromAccountId(this, id);
937   if (!accountItem) {
938     qDebug() << "Unexpected null accountItem in AccountsModel::slotObjectModified";
939     return;
940   }
941 
942   const auto oldAccount = accountItem->data((int)Role::Account).value<MyMoneyAccount>();
943   if (oldAccount.parentAccountId() == account.parentAccountId()) {
944     // the hierarchy did not change so update the account data
945     auto parentAccountItem = accountItem->parent();
946     if (!parentAccountItem)
947       parentAccountItem = this->invisibleRootItem();
948     const auto row = accountItem->row();
949     d->setAccountData(parentAccountItem, row, account, d->m_columns);
950     // and the child of the favorite item if the account is a favorite account or it's favorite status has just changed
951     if (auto favoriteAccountsItem = d->itemFromAccountId(this, favoritesAccountId)) {
952       if (account.value("PreferredAccount") == QLatin1String("Yes"))
953         d->loadPreferredAccount(account, parentAccountItem, row, favoriteAccountsItem);
954       else if (auto favItem = d->itemFromAccountId(favoriteAccountsItem, account.id()))
955         favoriteAccountsItem->removeRow(favItem->row()); // it's not favorite anymore
956     }
957   } else {
958     // this means that the hierarchy was changed - simulate this with a remove followed by and add operation
959     slotObjectRemoved(File::Object::Account, oldAccount.id());
960     slotObjectAdded(File::Object::Account, id);
961   }
962 
963   checkNetWorth();
964   checkProfit();
965 }
966 
967 /**
968   * Notify the model that an object has been removed. An action is performed only if the object is an account.
969   *
970   */
slotObjectRemoved(File::Object objType,const QString & id)971 void AccountsModel::slotObjectRemoved(File::Object objType, const QString& id)
972 {
973   if (objType != File::Object::Account)
974     return;
975 
976   const auto list = match(index(0, 0), (int)Role::ID, id, -1, Qt::MatchFlags(Qt::MatchExactly | Qt::MatchRecursive));
977   for (const auto& index : list)
978     removeRow(index.row(), index.parent());
979 
980   checkNetWorth();
981   checkProfit();
982 }
983 
984 /**
985   * Notify the model that the account balance has been changed.
986   */
slotBalanceOrValueChanged(const MyMoneyAccount & account)987 void AccountsModel::slotBalanceOrValueChanged(const MyMoneyAccount &account)
988 {
989   Q_D(AccountsModel);
990   auto itParent = d->itemFromAccountId(this, account.id()); // get node of account in model
991   auto isTopLevel = false;                                  // it could be top-level but we don't know it yet
992   while (itParent && !isTopLevel) {                         // loop in which we set total values and balances from the bottom to the top
993     auto itCurrent = itParent;
994     const auto accCurrent = d->m_file->account(itCurrent->data((int)Role::Account).value<MyMoneyAccount>().id());
995     if (accCurrent.id().isEmpty()) {   // this is institution
996       d->setInstitutionTotalValue(invisibleRootItem(), itCurrent->row());
997       break;                            // it's top-level node so nothing above that;
998     }
999     itParent = itCurrent->parent();
1000     if (!itParent) {
1001       itParent = this->invisibleRootItem();
1002       isTopLevel = true;
1003     }
1004     d->setAccountBalanceAndValue(itParent, itCurrent->row(), accCurrent, d->m_columns);
1005   }
1006   checkNetWorth();
1007   checkProfit();
1008 }
1009 
1010 /**
1011   * The pimpl of the @ref InstitutionsModel derived from the pimpl of the @ref AccountsModel.
1012   */
1013 class InstitutionsModelPrivate : public AccountsModelPrivate
1014 {
1015 public:
InstitutionsModelPrivate(InstitutionsModel * qq)1016   InstitutionsModelPrivate(InstitutionsModel *qq) :
1017     AccountsModelPrivate(qq)
1018   {
1019   }
1020 
~InstitutionsModelPrivate()1021   ~InstitutionsModelPrivate() override
1022   {
1023   }
1024 
1025   /**
1026     * Function to get the institution item from an institution id.
1027     *
1028     * @param model The model in which to look for the item.
1029     * @param institutionId Search based on this parameter.
1030     *
1031     * @return The item corresponding to the given institution id, NULL if the institution was not found.
1032     */
institutionItemFromId(QStandardItemModel * model,const QString & institutionId)1033   QStandardItem *institutionItemFromId(QStandardItemModel *model, const QString &institutionId) {
1034     const auto list = model->match(model->index(0, 0), (int)Role::ID, QVariant(institutionId), 1, Qt::MatchFlags(Qt::MatchExactly | Qt::MatchCaseSensitive));
1035     if (!list.isEmpty())
1036       return model->itemFromIndex(list.front());
1037     return nullptr; // this should rarely fail as we add all institutions early on
1038   }
1039 
1040   /**
1041     * Function to add the account item to it's corresponding institution item.
1042     *
1043     * @param model The model where to add the item.
1044     * @param account The account for which to create the item.
1045     *
1046     */
loadInstitution(QStandardItemModel * model,const MyMoneyAccount & account)1047   void loadInstitution(QStandardItemModel *model, const MyMoneyAccount &account) {
1048     if (!account.isAssetLiability() && !account.isInvest())
1049       return;
1050 
1051     // we've got account but don't know under which institution it should be added, so we find it out
1052     auto idInstitution = account.institutionId();
1053     if (account.isInvest()) {                                                 // if it's stock account then...
1054       const auto investmentAccount = m_file->account(account.parentAccountId());  // ...get investment account it's under and...
1055       idInstitution = investmentAccount.institutionId();                          // ...get institution from investment account
1056     }
1057 
1058     auto itInstitution = institutionItemFromId(model, idInstitution);
1059     auto itAccount = itemFromAccountId(itInstitution, account.id());  // check if account already exists under institution
1060     // only stock accounts are added to their parent in the institutions view
1061     // this makes hierarchy maintenance a lot easier since the stock accounts
1062     // are the only ones that always have the same institution as their parent
1063     auto itInvestmentAccount = account.isInvest() ? itemFromAccountId(itInstitution, account.parentAccountId()) : nullptr;
1064     if (!itAccount) {
1065       itAccount = new QStandardItem(account.name());
1066       if (itInvestmentAccount)                       // stock account nodes go under investment account nodes and...
1067         itInvestmentAccount->appendRow(itAccount);
1068       else if (itInstitution)                       // ...the rest goes under institution's node
1069         itInstitution->appendRow(itAccount);
1070       else
1071         return;
1072       itAccount->setEditable(false);
1073     }
1074     if (itInvestmentAccount) {
1075       setAccountData(itInvestmentAccount, itAccount->row(), account, m_columns);                                         // set data for stock account node
1076       setAccountData(itInstitution, itInvestmentAccount->row(), m_file->account(account.parentAccountId()), m_columns);  // set data for investment account node
1077     } else if (itInstitution) {
1078       setAccountData(itInstitution, itAccount->row(), account, m_columns);
1079     }
1080   }
1081 
1082   /**
1083     * Function to add an institution item to the model.
1084     *
1085     * @param model The model in which to add the item.
1086     * @param institution The institution object which should be represented by the item.
1087     *
1088     */
addInstitutionItem(QStandardItemModel * model,const MyMoneyInstitution & institution)1089   void addInstitutionItem(QStandardItemModel *model, const MyMoneyInstitution &institution) {
1090     QFont font;
1091     font.setBold(true);
1092     auto itInstitution = new QStandardItem(Icons::get(Icon::Institution), institution.name());
1093     itInstitution->setFont(font);
1094     itInstitution->setData(QVariant::fromValue(MyMoneyMoney()), (int)Role::TotalValue);
1095     itInstitution->setData(institution.id(), (int)Role::ID);
1096     itInstitution->setData(QVariant::fromValue(institution), (int)Role::Account);
1097     itInstitution->setData(6, (int)Role::DisplayOrder);
1098     itInstitution->setEditable(false);
1099     model->invisibleRootItem()->appendRow(itInstitution);
1100     setInstitutionTotalValue(model->invisibleRootItem(), itInstitution->row());
1101   }
1102 };
1103 
1104 /**
1105   * The institution model contains the accounts grouped by institution.
1106   *
1107   */
InstitutionsModel(QObject * parent)1108 InstitutionsModel::InstitutionsModel(QObject *parent) :
1109   AccountsModel(*new InstitutionsModelPrivate(this), parent)
1110 {
1111 }
1112 
~InstitutionsModel()1113 InstitutionsModel::~InstitutionsModel()
1114 {
1115 }
1116 
1117 /**
1118   * Perform the initial load of the model data
1119   * from the @ref MyMoneyFile.
1120   *
1121   */
load()1122 void InstitutionsModel::load()
1123 {
1124   Q_D(InstitutionsModel);
1125   // create items for all the institutions
1126   auto institutionList = d->m_file->institutionList();
1127   MyMoneyInstitution none;
1128   none.setName(i18n("Accounts with no institution assigned"));
1129   institutionList.append(none);
1130   for (const auto& institution : institutionList)   // add all known institutions as top-level nodes
1131     d->addInstitutionItem(this, institution);
1132 
1133   QList<MyMoneyAccount> accountsList;
1134   QList<MyMoneyAccount> stocksList;
1135   d->m_file->accountList(accountsList);
1136   for (const auto& account : accountsList) {  // add account nodes under institution nodes...
1137     if (account.isInvest())                     // ...but wait with stocks until investment accounts appear
1138       stocksList.append(account);
1139     else
1140       d->loadInstitution(this, account);
1141   }
1142 
1143   for (const auto& stock : stocksList) {
1144     if (!(KMyMoneySettings::hideZeroBalanceEquities() && stock.balance().isZero()))
1145       d->loadInstitution(this, stock);
1146   }
1147 
1148   for (auto i = 0 ; i < rowCount(); ++i)
1149     d->setInstitutionTotalValue(invisibleRootItem(), i);
1150 }
1151 
1152 /**
1153   * Notify the model that an object has been added. An action is performed only if the object is an account or an institution.
1154   *
1155   */
slotObjectAdded(File::Object objType,const QString & id)1156 void InstitutionsModel::slotObjectAdded(File::Object objType, const QString& id)
1157 {
1158   Q_D(InstitutionsModel);
1159   if (objType == File::Object::Institution) {
1160     // if an institution was added then add the item which will represent it
1161     const auto institution = MyMoneyFile::instance()->institution(id);
1162     d->addInstitutionItem(this, institution);
1163   }
1164 
1165   if (objType != File::Object::Account)
1166     return;
1167 
1168   // if an account was added then add the item which will represent it only for real accounts
1169   const auto account = MyMoneyFile::instance()->account(id);
1170   // nothing to do for root accounts and categories
1171   if (account.parentAccountId().isEmpty() || account.isIncomeExpense())
1172     return;
1173 
1174   // load the account into the institution
1175   d->loadInstitution(this, account);
1176 
1177   // load the investment sub-accounts if there are any - there could be sub-accounts if this is an add operation
1178   // that was triggered in slotObjectModified on an already existing account which went trough a hierarchy change
1179   const auto& sAccounts = account.accountList();
1180   if (!sAccounts.isEmpty()) {
1181     QList<MyMoneyAccount> subAccounts;
1182     d->m_file->accountList(subAccounts, sAccounts);
1183     for (const auto& subAccount : subAccounts) {
1184       if (subAccount.isInvest()) {
1185         d->loadInstitution(this, subAccount);
1186       }
1187     }
1188   }
1189 }
1190 
1191 /**
1192   * Notify the model that an object has been modified. An action is performed only if the object is an account or an institution.
1193   *
1194   */
slotObjectModified(File::Object objType,const QString & id)1195 void InstitutionsModel::slotObjectModified(File::Object objType, const QString& id)
1196 {
1197   Q_D(InstitutionsModel);
1198   if (objType == File::Object::Institution) {
1199     // if an institution was modified then modify the item which represents it
1200     const auto institution = MyMoneyFile::instance()->institution(id);
1201     if (auto institutionItem = d->institutionItemFromId(this, id)) {
1202       institutionItem->setData(institution.name(), Qt::DisplayRole);
1203       institutionItem->setData(QVariant::fromValue(institution), (int)Role::Account);
1204       institutionItem->setIcon(MyMoneyInstitution::pixmap());
1205     }
1206   }
1207 
1208   if (objType != File::Object::Account)
1209     return;
1210 
1211   // if an account was modified then modify the item which represents it
1212   const auto account = MyMoneyFile::instance()->account(id);
1213   // nothing to do for root accounts, categories and equity accounts since they don't have a representation in this model
1214   if (account.parentAccountId().isEmpty() || account.isIncomeExpense() || account.accountType() == Account::Type::Equity)
1215     return;
1216 
1217   auto accountItem = d->itemFromAccountId(this, account.id());
1218   const auto oldAccount = accountItem->data((int)Role::Account).value<MyMoneyAccount>();
1219   if (oldAccount.institutionId() == account.institutionId()) {
1220     // the hierarchy did not change so update the account data
1221     d->setAccountData(accountItem->parent(), accountItem->row(), account, d->m_columns);
1222   } else {
1223     // this means that the hierarchy was changed - simulate this with a remove followed by and add operation
1224     slotObjectRemoved(File::Object::Account, oldAccount.id());
1225     slotObjectAdded(File::Object::Account, id);
1226   }
1227 }
1228 
1229 /**
1230   * Notify the model that an object has been removed. An action is performed only if the object is an account or an institution.
1231   *
1232   */
slotObjectRemoved(File::Object objType,const QString & id)1233 void InstitutionsModel::slotObjectRemoved(File::Object objType, const QString& id)
1234 {
1235   Q_D(InstitutionsModel);
1236   if (objType == File::Object::Institution) {
1237     // if an institution was removed then remove the item which represents it
1238     if (auto itInstitution = d->institutionItemFromId(this, id))
1239       removeRow(itInstitution->row(), itInstitution->index().parent());
1240   }
1241 
1242   if (objType != File::Object::Account)
1243     return;
1244 
1245   // if an account was removed then remove the item which represents it and recompute the institution's value
1246   auto itAccount = d->itemFromAccountId(this, id);
1247   if (!itAccount)
1248     return; // this could happen if the account isIncomeExpense
1249 
1250   const auto account = itAccount->data((int)Role::Account).value<MyMoneyAccount>();
1251   if (auto itInstitution = d->itemFromAccountId(this, account.institutionId())) {
1252     AccountsModel::slotObjectRemoved(objType, id);
1253     d->setInstitutionTotalValue(invisibleRootItem(), itInstitution->row());
1254   }
1255 }
1256