File indexing completed on 2024-05-12 16:42:17

0001 /*
0002     SPDX-FileCopyrightText: 2017-2018 Łukasz Wojniłowicz <lukasz.wojnilowicz@gmail.com>
0003     SPDX-License-Identifier: GPL-2.0-or-later
0004 */
0005 
0006 #include "equitiesmodel.h"
0007 
0008 // ----------------------------------------------------------------------------
0009 // QT Includes
0010 
0011 #include <QMenu>
0012 #include <QDebug>
0013 
0014 // ----------------------------------------------------------------------------
0015 // KDE Includes
0016 
0017 #include <KLocalizedString>
0018 
0019 // ----------------------------------------------------------------------------
0020 // Project Includes
0021 
0022 #include "mymoneymoney.h"
0023 #include "mymoneyfile.h"
0024 #include "mymoneyaccount.h"
0025 #include "mymoneysecurity.h"
0026 #include "mymoneyprice.h"
0027 #include "mymoneyenums.h"
0028 
0029 class EquitiesModel::Private
0030 {
0031 public:
0032     Private() : m_file(MyMoneyFile::instance())
0033     {
0034         QVector<Column> columns {Column::Equity, Column::Symbol, Column::Value,
0035                                  Column::Quantity, Column::Price};
0036         foreach (auto const column, columns)
0037             m_columns.append(column);
0038     }
0039 
0040     ~Private() {}
0041 
0042     void loadInvestmentAccount(QStandardItem *node, const MyMoneyAccount &invAcc)
0043     {
0044         auto itInvAcc = new QStandardItem(invAcc.name());
0045         node->appendRow(itInvAcc);                                  // investment account is meant to be added under root item
0046         itInvAcc->setEditable(false);
0047         itInvAcc->setColumnCount(m_columns.count());
0048         setAccountData(node, itInvAcc->row(), invAcc, m_columns);
0049 
0050         foreach (const auto strStkAcc, invAcc.accountList()) { // only stock or bond accounts are expected here
0051             auto stkAcc = m_file->account(strStkAcc);
0052             auto itStkAcc = new QStandardItem(strStkAcc);
0053             itStkAcc->setEditable(false);
0054             itInvAcc->appendRow(itStkAcc);
0055             setAccountData(itInvAcc, itStkAcc->row(), stkAcc, m_columns);
0056         }
0057     }
0058 
0059     void setAccountData(QStandardItem *node, const int row, const MyMoneyAccount &account, const QList<Column> &columns)
0060     {
0061         QStandardItem *cell;
0062 
0063         auto getCell = [&, row](const auto column) {
0064             cell = node->child(row, column);      // try to get QStandardItem
0065             if (!cell) {                          // it may be uninitialized
0066                 cell = new QStandardItem;           // so create one
0067                 node->setChild(row, column, cell);  // and add it under the node
0068                 cell->setEditable(false);           // and don't forget that it's non-editable
0069             }
0070         };
0071 
0072         auto colNum = m_columns.indexOf(Column::Equity);
0073         if (colNum == -1)
0074             return;
0075 
0076         // Equity
0077         getCell(colNum);
0078         if (columns.contains(Column::Equity)) {
0079             cell->setData(account.name(), Qt::DisplayRole);
0080             cell->setData(account.name(), Role::SortRole);
0081             cell->setData(account.id(), Role::EquityID);
0082             cell->setData(account.currencyId(), Role::SecurityID);
0083         }
0084 
0085         if (account.accountType() == eMyMoney::Account::Type::Investment)  // investments accounts are not meant to be displayed, so stop here
0086             return;
0087 
0088         // Display the name of the equity with strikeout font in case it is closed
0089         auto font = cell->data(Qt::FontRole).value<QFont>();
0090         if (account.isClosed() != font.strikeOut()) {
0091             font.setStrikeOut(account.isClosed());
0092             cell->setData(font, Qt::FontRole);
0093         }
0094 
0095         // Symbol
0096         if (columns.contains(Column::Symbol)) {
0097             colNum = m_columns.indexOf(Column::Symbol);
0098             if (colNum != -1) {
0099                 auto security = m_file->security(account.currencyId());
0100                 getCell(colNum);
0101                 cell->setData(security.tradingSymbol(), Qt::DisplayRole);
0102             }
0103         }
0104 
0105         setAccountBalanceAndValue(node, row, account, columns);
0106     }
0107 
0108     void setAccountBalanceAndValue(QStandardItem *node, const int row, const MyMoneyAccount &account, const QList<Column> &columns)
0109     {
0110         QStandardItem *cell;
0111 
0112         auto getCell = [&, row](const auto column) {
0113             cell = node->child(row, column);      // try to get QStandardItem
0114             if (!cell) {                          // it may be uninitialized
0115                 cell = new QStandardItem;           // so create one
0116                 node->setChild(row, column, cell);  // and add it under the node
0117                 cell->setEditable(false);           // and don't forget that it's non-editable
0118             }
0119         };
0120 
0121         auto colNum = m_columns.indexOf(Column::Equity);
0122         if (colNum == -1)
0123             return;
0124 
0125         auto balance = m_file->balance(account.id());
0126         auto security = m_file->security(account.currencyId());
0127         auto tradingCurrency = m_file->security(security.tradingCurrency());
0128         auto price = m_file->price(account.currencyId(), tradingCurrency.id());
0129 
0130         // Value
0131         if (columns.contains(Column::Value)) {
0132             colNum = m_columns.indexOf(Column::Value);
0133             if (colNum != -1) {
0134                 getCell(colNum);
0135                 if (price.isValid()) {
0136                     auto prec = MyMoneyMoney::denomToPrec(tradingCurrency.smallestAccountFraction());
0137                     auto value = balance * price.rate(tradingCurrency.id());
0138                     auto strValue = QVariant(value.formatMoney(tradingCurrency.tradingSymbol(), prec));
0139                     cell->setData(strValue, Qt::DisplayRole);
0140                     cell->setData(QVariant::fromValue<MyMoneyMoney>(value), Role::SortRole);
0141                 } else {
0142                     cell->setData(QVariant("---"), Qt::DisplayRole);
0143                     cell->setData(QVariant::fromValue<MyMoneyMoney>(MyMoneyMoney()), Role::SortRole);
0144                 }
0145                 cell->setData(QVariant(Qt::AlignRight | Qt::AlignVCenter), Qt::TextAlignmentRole);
0146             }
0147         }
0148 
0149         // Quantity
0150         if (columns.contains(Column::Quantity)) {
0151             colNum = m_columns.indexOf(Column::Quantity);
0152             if (colNum != -1) {
0153                 getCell(colNum);
0154                 auto prec = MyMoneyMoney::denomToPrec(security.smallestAccountFraction());
0155                 auto strQuantity = QVariant(balance.formatMoney(QString(), prec));
0156                 cell->setData(strQuantity, Qt::DisplayRole);
0157                 cell->setData(QVariant::fromValue<MyMoneyMoney>(balance), Role::SortRole);
0158                 cell->setData(QVariant(Qt::AlignRight | Qt::AlignVCenter), Qt::TextAlignmentRole);
0159             }
0160         }
0161 
0162         // Price
0163         if (columns.contains(Column::Price)) {
0164             colNum = m_columns.indexOf(Column::Price);
0165             if (colNum != -1) {
0166                 getCell(colNum);
0167                 if (price.isValid()) {
0168                     auto prec = security.pricePrecision();
0169                     const auto rate = price.rate(tradingCurrency.id());
0170                     auto strPrice = QVariant(rate.formatMoney(tradingCurrency.tradingSymbol(), prec));
0171                     cell->setData(strPrice, Qt::DisplayRole);
0172                     cell->setData(QVariant::fromValue<MyMoneyMoney>(rate), Role::SortRole);
0173                 } else {
0174                     cell->setData(QVariant("---"), Qt::DisplayRole);
0175                     cell->setData(QVariant::fromValue<MyMoneyMoney>(MyMoneyMoney()), Role::SortRole);
0176                 }
0177                 cell->setData(QVariant(Qt::AlignRight | Qt::AlignVCenter), Qt::TextAlignmentRole);
0178             }
0179         }
0180     }
0181 
0182     QStandardItem *itemFromId(QStandardItemModel *model, const QString &id, const Role role)
0183     {
0184         const auto itemList = model->match(model->index(0, 0), role, QVariant(id), 1, Qt::MatchFlags(Qt::MatchExactly | Qt::MatchCaseSensitive | Qt::MatchRecursive));
0185         if (!itemList.isEmpty())
0186             return model->itemFromIndex(itemList.first());
0187         return nullptr;
0188     }
0189 
0190     MyMoneyFile *m_file;
0191     QList<EquitiesModel::Column> m_columns;
0192 };
0193 
0194 EquitiesModel::EquitiesModel(QObject *parent)
0195     : QStandardItemModel(parent), d(new Private)
0196 {
0197     init();
0198 }
0199 
0200 EquitiesModel::~EquitiesModel()
0201 {
0202     delete d;
0203 }
0204 
0205 void EquitiesModel::init()
0206 {
0207     QStringList headerLabels;
0208     foreach (const auto column, d->m_columns)
0209         headerLabels.append(getHeaderName(column));
0210     setHorizontalHeaderLabels(headerLabels);
0211     setSortRole(Role::SortRole);
0212 }
0213 
0214 void EquitiesModel::load()
0215 {
0216     this->blockSignals(true);
0217 
0218     auto rootItem = invisibleRootItem();
0219     QList<MyMoneyAccount> accList;
0220     d->m_file->accountList(accList);                        // get all available accounts
0221     foreach (const auto acc, accList)
0222         if (acc.accountType() == eMyMoney::Account::Type::Investment)  // but add only investment accounts (and its children) to the model
0223             d->loadInvestmentAccount(rootItem, acc);
0224 
0225     this->blockSignals(false);
0226 }
0227 
0228 /**
0229   * Notify the model that an object has been added. An action is performed only if the object is an account.
0230   */
0231 void EquitiesModel::slotObjectAdded(eMyMoney::File::Object objType, const QString& id)
0232 {
0233     // check whether change is about accounts
0234     if (objType != eMyMoney::File::Object::Account)
0235         return;
0236 
0237     // check whether change is about either investment or stock account
0238     const auto acc = MyMoneyFile::instance()->account(id);
0239     if (acc.accountType() != eMyMoney::Account::Type::Investment &&
0240             acc.accountType() != eMyMoney::Account::Type::Stock)
0241         return;
0242     auto itAcc = d->itemFromId(this, id, Role::EquityID);
0243 
0244     QStandardItem *itParentAcc;
0245     if (acc.accountType() == eMyMoney::Account::Type::Investment) // if it's investment account then its parent is root item
0246         itParentAcc = invisibleRootItem();
0247     else                                                  // otherwise it's stock account and its parent is investment account
0248         itParentAcc = d->itemFromId(this, acc.parentAccountId(), Role::InvestmentID);
0249 
0250     // if account doesn't exist in model then add it
0251     if (!itAcc) {
0252         itAcc = new QStandardItem(acc.name());
0253         itParentAcc->appendRow(itAcc);
0254         itAcc->setEditable(false);
0255     }
0256 
0257     d->setAccountData(itParentAcc, itAcc->row(), acc, d->m_columns);
0258 }
0259 
0260 /**
0261   * Notify the model that an object has been modified. An action is performed only if the object is an account.
0262   */
0263 void EquitiesModel::slotObjectModified(eMyMoney::File::Object objType, const QString& id)
0264 {
0265     MyMoneyAccount acc;
0266     QStandardItem  *itAcc;
0267     switch (objType) {
0268     case eMyMoney::File::Object::Account:
0269     {
0270         auto tmpAcc = MyMoneyFile::instance()->account(id);
0271         if (tmpAcc.accountType() != eMyMoney::Account::Type::Stock)
0272             return;
0273         acc = MyMoneyAccount(tmpAcc);
0274         itAcc = d->itemFromId(this, acc.id(), Role::EquityID);
0275         break;
0276     }
0277     case eMyMoney::File::Object::Security:
0278     {
0279         auto sec = MyMoneyFile::instance()->security(id);
0280         // in case we hit a currency, we simply bail out here
0281         // as there is nothing to do for us
0282         if(sec.isCurrency())
0283             return;
0284         itAcc = d->itemFromId(this, sec.id(), Role::SecurityID);
0285         if (!itAcc)
0286             return;
0287         const auto idAcc = itAcc->data(Role::EquityID).toString();
0288         acc = d->m_file->account(idAcc);
0289         break;
0290     }
0291     default:
0292         return;
0293     }
0294 
0295     auto itParentAcc = d->itemFromId(this, acc.parentAccountId(), Role::InvestmentID);
0296     // in case something went wrong, we bail out
0297     if(itParentAcc == nullptr) {
0298         qWarning() << "EquitiesModel::slotObjectModified: itParentAcc == 0";
0299         return;
0300     }
0301 
0302     auto modelID = itParentAcc->data(Role::InvestmentID).toString();      // get parent account from model
0303     if (modelID == acc.parentAccountId()) {                              // and if it matches with those from file then modify only
0304         d->setAccountData(itParentAcc, itAcc->row(), acc, d->m_columns);
0305     } else {                                                              // and if not then reparent
0306         slotObjectRemoved(eMyMoney::File::Object::Account, acc.id());
0307         slotObjectAdded(eMyMoney::File::Object::Account, id);
0308     }
0309 }
0310 
0311 /**
0312   * Notify the model that an object has been removed. An action is performed only if the object is an account.
0313   *
0314   */
0315 void EquitiesModel::slotObjectRemoved(eMyMoney::File::Object objType, const QString& id)
0316 {
0317     if (objType != eMyMoney::File::Object::Account)
0318         return;
0319 
0320     const auto indexList = match(index(0, 0), Role::EquityID, id, -1, Qt::MatchFlags(Qt::MatchExactly | Qt::MatchRecursive));
0321     foreach (const auto index, indexList)
0322         removeRow(index.row(), index.parent());
0323 }
0324 
0325 /**
0326   * Notify the model that the account balance has been changed.
0327   */
0328 void EquitiesModel::slotBalanceOrValueChanged(const MyMoneyAccount &account)
0329 {
0330     if (account.accountType() != eMyMoney::Account::Type::Stock)
0331         return;
0332 
0333     const auto itAcc = d->itemFromId(this, account.id(), Role::EquityID);
0334     if (!itAcc)
0335         return;
0336     d->setAccountBalanceAndValue(itAcc->parent(), itAcc->row(), account, d->m_columns);
0337 }
0338 
0339 auto EquitiesModel::getColumns()
0340 {
0341     return &d->m_columns;
0342 }
0343 
0344 QString EquitiesModel::getHeaderName(const Column column)
0345 {
0346     switch(column) {
0347     case Equity:
0348         return i18n("Equity");
0349     case Symbol:
0350         return i18nc("@title stock symbol column", "Symbol");
0351     case Value:
0352         return i18n("Value");
0353     case Quantity:
0354         return i18n("Quantity");
0355     case Price:
0356         return i18n("Price");
0357     default:
0358         return QString();
0359     }
0360 }
0361 
0362 class EquitiesFilterProxyModel::Private
0363 {
0364 public:
0365     Private() :
0366         m_mdlColumns(nullptr),
0367         m_file(MyMoneyFile::instance()),
0368         m_hideClosedAccounts(false),
0369         m_hideZeroBalanceAccounts(false)
0370     {}
0371 
0372     ~Private() {}
0373 
0374     QList<EquitiesModel::Column> *m_mdlColumns;
0375     QList<EquitiesModel::Column> m_visColumns;
0376 
0377     MyMoneyFile *m_file;
0378 
0379     bool m_hideClosedAccounts;
0380     bool m_hideZeroBalanceAccounts;
0381 };
0382 
0383 #if QT_VERSION < QT_VERSION_CHECK(5,10,0)
0384 #define QSortFilterProxyModel KRecursiveFilterProxyModel
0385 #endif
0386 EquitiesFilterProxyModel::EquitiesFilterProxyModel(QObject *parent, EquitiesModel *model, const QList<EquitiesModel::Column> &columns)
0387     : QSortFilterProxyModel(parent), d(new Private)
0388 {
0389     setRecursiveFilteringEnabled(true);
0390     setDynamicSortFilter(true);
0391     setFilterKeyColumn(-1);
0392     setSortLocaleAware(true);
0393     setFilterCaseSensitivity(Qt::CaseInsensitive);
0394     setSourceModel(model);
0395     d->m_mdlColumns = model->getColumns();
0396     d->m_visColumns.append(columns);
0397 }
0398 #undef QSortFilterProxyModel
0399 
0400 EquitiesFilterProxyModel::~EquitiesFilterProxyModel()
0401 {
0402     delete d;
0403 }
0404 
0405 /**
0406   * Set if closed accounts should be hidden or not.
0407   * @param hideClosedAccounts
0408   */
0409 void EquitiesFilterProxyModel::setHideClosedAccounts(const bool hideClosedAccounts)
0410 {
0411     d->m_hideClosedAccounts = hideClosedAccounts;
0412 }
0413 
0414 /**
0415   * Set if zero balance accounts should be hidden or not.
0416   * @param hideZeroBalanceAccounts
0417   */
0418 void EquitiesFilterProxyModel::setHideZeroBalanceAccounts(const bool hideZeroBalanceAccounts)
0419 {
0420     d->m_hideZeroBalanceAccounts = hideZeroBalanceAccounts;
0421 }
0422 
0423 bool EquitiesFilterProxyModel::filterAcceptsColumn(int source_column, const QModelIndex &source_parent) const
0424 {
0425     Q_UNUSED(source_parent)
0426     if (d->m_visColumns.isEmpty() || d->m_visColumns.contains(d->m_mdlColumns->at(source_column)))
0427         return true;
0428     return false;
0429 }
0430 
0431 bool EquitiesFilterProxyModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const
0432 {
0433     if (d->m_hideClosedAccounts || d->m_hideZeroBalanceAccounts) {
0434         const auto ixRow = sourceModel()->index(source_row, EquitiesModel::Equity, source_parent);
0435         const auto idAcc = sourceModel()->data(ixRow, EquitiesModel::EquityID).toString();
0436         const auto acc = d->m_file->account(idAcc);
0437 
0438         if (d->m_hideClosedAccounts &&
0439                 acc.isClosed())
0440             return false;
0441         if (d->m_hideZeroBalanceAccounts &&
0442                 acc.accountType() != eMyMoney::Account::Type::Investment && acc.balance().isZero())  // we should never hide investment account because all underlaying stocks will be hidden as well
0443             return false;
0444     }
0445     return true;
0446 }
0447 
0448 QList<EquitiesModel::Column> &EquitiesFilterProxyModel::getVisibleColumns()
0449 {
0450     return d->m_visColumns;
0451 }
0452 
0453 void EquitiesFilterProxyModel::slotColumnsMenu(const QPoint)
0454 {
0455     // construct all hideable columns list
0456     const QList<EquitiesModel::Column> idColumns {
0457         EquitiesModel::Symbol, EquitiesModel::Value,
0458         EquitiesModel::Quantity, EquitiesModel::Price
0459     };
0460 
0461     // create menu
0462     QMenu menu(i18n("Displayed columns"));
0463     QList<QAction *> actions;
0464     foreach (const auto idColumn, idColumns) {
0465         auto a = new QAction(nullptr);
0466         a->setObjectName(QString::number(idColumn));
0467         a->setText(EquitiesModel::getHeaderName(idColumn));
0468         a->setCheckable(true);
0469         a->setChecked(d->m_visColumns.contains(idColumn));
0470         actions.append(a);
0471     }
0472     menu.addActions(actions);
0473 
0474     // execute menu and get result
0475     const auto retAction = menu.exec(QCursor::pos());
0476     if (retAction) {
0477         const auto idColumn = static_cast<EquitiesModel::Column>(retAction->objectName().toInt());
0478         const auto isChecked = retAction->isChecked();
0479         const auto contains = d->m_visColumns.contains(idColumn);
0480         if (isChecked && !contains) {           // column has just been enabled
0481             d->m_visColumns.append(idColumn);     // change filtering variable
0482             emit columnToggled(idColumn, true);   // emit signal for method to add column to model
0483             invalidate();                         // refresh model to reflect recent changes
0484         } else if (!isChecked && contains) {    // column has just been disabled
0485             d->m_visColumns.removeOne(idColumn);
0486             emit columnToggled(idColumn, false);
0487             invalidate();
0488         }
0489     }
0490 }
0491 
0492 bool EquitiesFilterProxyModel::lessThan(const QModelIndex& left, const QModelIndex& right) const
0493 {
0494     switch (left.column()) {
0495     case EquitiesModel::Column::Value:
0496     case EquitiesModel::Column::Price:
0497     case EquitiesModel::Column::Quantity: {
0498         const auto leftValue = left.data(EquitiesModel::Role::SortRole).value<MyMoneyMoney>();
0499         const auto rightValue = right.data(EquitiesModel::Role::SortRole).value<MyMoneyMoney>();
0500         if (leftValue != rightValue) {
0501             return leftValue < rightValue;
0502         }
0503     }
0504         // intentional fall through
0505 
0506     default:
0507         return QSortFilterProxyModel::lessThan(left, right);
0508     }
0509 }