File indexing completed on 2024-05-12 05:06:15

0001 /*
0002     SPDX-FileCopyrightText: 2017-2018 Łukasz Wojniłowicz <lukasz.wojnilowicz@gmail.com>
0003     SPDX-FileCopyrightText: 2019 Thomas Baumgart <tbaumgart@kde.org>
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "equitiesmodel.h"
0008 #include "accountsmodel.h"
0009 #include "mymoneyfile.h"
0010 #include "securitiesmodel.h"
0011 
0012 // ----------------------------------------------------------------------------
0013 // QT Includes
0014 #include <QDate>
0015 
0016 // ----------------------------------------------------------------------------
0017 // KDE Includes
0018 
0019 #include <KLocalizedString>
0020 
0021 // ----------------------------------------------------------------------------
0022 // Project Includes
0023 
0024 #include "mymoneyprice.h"
0025 #include "mymoneymoney.h"
0026 #include "mymoneyenums.h"
0027 #include "mymoneyutils.h"
0028 
0029 class EquitiesModelPrivate
0030 {
0031     Q_DECLARE_PUBLIC(EquitiesModel);
0032     EquitiesModel* q_ptr;
0033 
0034 public:
0035     EquitiesModelPrivate(EquitiesModel* qq)
0036         : q_ptr(qq)
0037     {
0038     }
0039 
0040     ~EquitiesModelPrivate() {}
0041 
0042 #if 0
0043     void loadInvestmentAccount(QStandardItem *node, const MyMoneyAccount &invAcc)
0044     {
0045         auto itInvAcc = new QStandardItem(invAcc.name());
0046         node->appendRow(itInvAcc);                                  // investment account is meant to be added under root item
0047         itInvAcc->setEditable(false);
0048         itInvAcc->setColumnCount(m_columns.count());
0049         setAccountData(node, itInvAcc->row(), invAcc, m_columns);
0050 
0051         for (const auto strStkAcc : invAcc.accountList()) { // only stock or bond accounts are expected here
0052             auto stkAcc = m_file->account(strStkAcc);
0053             auto itStkAcc = new QStandardItem(strStkAcc);
0054             itStkAcc->setEditable(false);
0055             itInvAcc->appendRow(itStkAcc);
0056             setAccountData(itInvAcc, itStkAcc->row(), stkAcc, m_columns);
0057         }
0058     }
0059 
0060     void setAccountData(QStandardItem *node, const int row, const MyMoneyAccount &account, const QList<Column> &columns)
0061     {
0062         QStandardItem *cell;
0063 
0064         auto getCell = [&, row](const auto column) {
0065             cell = node->child(row, column);      // try to get QStandardItem
0066             if (!cell) {                          // it may be uninitialized
0067                 cell = new QStandardItem;           // so create one
0068                 node->setChild(row, column, cell);  // and add it under the node
0069                 cell->setEditable(false);           // and don't forget that it's non-editable
0070             }
0071         };
0072 
0073         auto colNum = m_columns.indexOf(Column::Equity);
0074         if (colNum == -1)
0075             return;
0076 
0077         // Equity
0078         getCell(colNum);
0079         if (columns.contains(Column::Equity)) {
0080             cell->setData(account.name(), Qt::DisplayRole);
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->currency(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                 } else {
0141                     cell->setData(QVariant("---"), Qt::DisplayRole);
0142                 }
0143                 cell->setData(QVariant(Qt::AlignRight | Qt::AlignVCenter), Qt::TextAlignmentRole);
0144             }
0145         }
0146 
0147         // Quantity
0148         if (columns.contains(Column::Quantity)) {
0149             colNum = m_columns.indexOf(Column::Quantity);
0150             if (colNum != -1) {
0151                 getCell(colNum);
0152                 auto prec = MyMoneyMoney::denomToPrec(security.smallestAccountFraction());
0153                 auto strQuantity = QVariant(balance.formatMoney(QString(), prec));
0154                 cell->setData(strQuantity, Qt::DisplayRole);
0155                 cell->setData(QVariant(Qt::AlignRight | Qt::AlignVCenter), Qt::TextAlignmentRole);
0156             }
0157         }
0158 
0159         // Price
0160         if (columns.contains(Column::Price)) {
0161             colNum = m_columns.indexOf(Column::Price);
0162             if (colNum != -1) {
0163                 getCell(colNum);
0164                 if (price.isValid()) {
0165                     auto prec = security.pricePrecision();
0166                     auto strPrice = QVariant(price.rate(tradingCurrency.id()).formatMoney(tradingCurrency.tradingSymbol(), prec));
0167                     cell->setData(strPrice, Qt::DisplayRole);
0168                 } else {
0169                     cell->setData(QVariant("---"), Qt::DisplayRole);
0170                 }
0171                 cell->setData(QVariant(Qt::AlignRight | Qt::AlignVCenter), Qt::TextAlignmentRole);
0172             }
0173         }
0174     }
0175 
0176     QStandardItem *itemFromId(QStandardItemModel *model, const QString &id, const Role role)
0177     {
0178         const auto itemList = model->match(model->index(0, 0), role, QVariant(id), 1, Qt::MatchFlags(Qt::MatchExactly | Qt::MatchCaseSensitive | Qt::MatchRecursive));
0179         if (!itemList.isEmpty())
0180             return model->itemFromIndex(itemList.first());
0181         return nullptr;
0182     }
0183 
0184     MyMoneyFile *m_file;
0185     QList<EquitiesModel::Column> m_columns;
0186 #endif
0187 };
0188 
0189 EquitiesModel::EquitiesModel(QObject *parent)
0190     : KExtraColumnsProxyModel(parent)
0191     , d_ptr(new EquitiesModelPrivate(this))
0192 {
0193     appendColumn(i18nc("@title stock symbol column", "Symbol"));
0194     appendColumn(i18n("Quantity"));
0195     appendColumn(i18n("Price"));
0196     appendColumn(i18n("Value"));
0197     appendColumn(i18n("Last Price Update"));
0198 }
0199 
0200 EquitiesModel::~EquitiesModel()
0201 {
0202     Q_D(EquitiesModel);
0203     delete d;
0204 }
0205 
0206 QVariant EquitiesModel::extraColumnData(const QModelIndex& parent, int row, int extraColumn, int role) const
0207 {
0208     const auto file = MyMoneyFile::instance();
0209 
0210     const auto idx = index(row, 0, parent);
0211     const auto baseIdx = MyMoneyFile::baseModel()->mapToBaseSource(idx);
0212     auto model = qobject_cast<const AccountsModel*>(baseIdx.model());
0213 
0214     const auto acc = model->itemByIndex(baseIdx);
0215 
0216     if (role == Qt::DisplayRole || role == Qt::EditRole) {
0217         const auto securityIdx = file->securitiesModel()->indexById(acc.currencyId());
0218         const auto tradingCurrencyIdx = securityIdx.data(eMyMoney::Model::SecurityTradingCurrencyIndexRole).value<QModelIndex>();
0219 
0220         switch (extraColumn) {
0221         case Symbol:
0222             return securityIdx.data(eMyMoney::Model::SecuritySymbolRole);
0223 
0224         case Value: {
0225             const auto balance = baseIdx.data(eMyMoney::Model::AccountBalanceRole).value<MyMoneyMoney>();
0226             const auto tradingCurrencyId = tradingCurrencyIdx.data(eMyMoney::Model::IdRole).toString();
0227             const auto prec = MyMoneyMoney::denomToPrec(tradingCurrencyIdx.data(eMyMoney::Model::SecuritySmallestAccountFractionRole).toInt());
0228             const auto tradingCurrencySymbol = tradingCurrencyIdx.data(eMyMoney::Model::SecuritySymbolRole).toString();
0229 
0230             const auto value = (file->price(acc.currencyId(), tradingCurrencyId).rate(tradingCurrencyId) * balance);
0231 
0232             if(role == Qt::EditRole) {
0233                 return value.toDouble();
0234             } else {
0235                 return value.formatMoney(tradingCurrencySymbol, prec);
0236             }
0237         }
0238 
0239         case Quantity: {
0240             const auto balance = baseIdx.data(eMyMoney::Model::AccountBalanceRole).value<MyMoneyMoney>();
0241             const auto prec = MyMoneyMoney::denomToPrec(securityIdx.data(eMyMoney::Model::SecuritySmallestAccountFractionRole).toInt());
0242 
0243             if(role == Qt::EditRole) {
0244                 return balance.toDouble();
0245             } else {
0246                 return balance.formatMoney(QString(), prec);
0247             }
0248         }
0249 
0250         case Price: {
0251             const auto tradingCurrencyId = tradingCurrencyIdx.data(eMyMoney::Model::IdRole).toString();
0252             const auto prec = securityIdx.data(eMyMoney::Model::SecurityPricePrecisionRole).toInt();
0253             const auto tradingCurrencySymbol = tradingCurrencyIdx.data(eMyMoney::Model::SecuritySymbolRole).toString();
0254 
0255             const auto value = file->price(acc.currencyId(), tradingCurrencyId).rate(tradingCurrencyId);
0256 
0257             if(role == Qt::EditRole) {
0258                 return value.toDouble();
0259             } else {
0260                 return value.formatMoney(tradingCurrencySymbol, prec);
0261             }
0262         }
0263 
0264         case LastPriceUpdate: {
0265             const auto tradingCurrencyId = tradingCurrencyIdx.data(eMyMoney::Model::IdRole).toString();
0266             const auto priceDate = MyMoneyUtils::formatDate(file->price(acc.currencyId(), tradingCurrencyId).date());
0267             return priceDate;
0268         }
0269 
0270         default:
0271             break;
0272         }
0273     } else {
0274         switch(role) {
0275         case Qt::DecorationRole:
0276             return QVariant();
0277 
0278         case Qt::TextAlignmentRole:
0279             switch(extraColumn) {
0280             case Symbol:
0281                 return QVariant(Qt::AlignLeft | Qt::AlignVCenter);
0282             default:
0283                 return QVariant(Qt::AlignRight | Qt::AlignVCenter);
0284             }
0285             break;
0286 
0287         default:
0288             return idx.data(role);
0289         }
0290     }
0291     return QVariant();
0292 }
0293 
0294 QVariant EquitiesModel::headerData(int section, Qt::Orientation orientation, int role) const
0295 {
0296     // KExtraColumnsProxyModel only supports Qt::DisplayRole
0297     if (role == eMyMoney::Model::LongDisplayRole) {
0298         role = Qt::DisplayRole;
0299     }
0300     return KExtraColumnsProxyModel::headerData(section, orientation, role);
0301 }
0302 
0303 #if 0
0304 /**
0305   * Notify the model that an object has been added. An action is performed only if the object is an account.
0306   */
0307 void EquitiesModel::slotObjectAdded(eMyMoney::File::Object objType, const QString& id)
0308 {
0309     // check whether change is about accounts
0310     if (objType != eMyMoney::File::Object::Account)
0311         return;
0312 
0313     // check whether change is about either investment or stock account
0314     const auto acc = MyMoneyFile::instance()->account(id);
0315     if (acc.accountType() != eMyMoney::Account::Type::Investment &&
0316             acc.accountType() != eMyMoney::Account::Type::Stock)
0317         return;
0318     auto itAcc = d->itemFromId(this, id, Role::EquityID);
0319 
0320     QStandardItem *itParentAcc;
0321     if (acc.accountType() == eMyMoney::Account::Type::Investment) // if it's investment account then its parent is root item
0322         itParentAcc = invisibleRootItem();
0323     else                                                  // otherwise it's stock account and its parent is investment account
0324         itParentAcc = d->itemFromId(this, acc.parentAccountId(), Role::InvestmentID);
0325 
0326     // if account doesn't exist in model then add it
0327     if (!itAcc) {
0328         itAcc = new QStandardItem(acc.name());
0329         itParentAcc->appendRow(itAcc);
0330         itAcc->setEditable(false);
0331     }
0332 
0333     d->setAccountData(itParentAcc, itAcc->row(), acc, d->m_columns);
0334 }
0335 
0336 /**
0337   * Notify the model that an object has been modified. An action is performed only if the object is an account.
0338   */
0339 void EquitiesModel::slotObjectModified(eMyMoney::File::Object objType, const QString& id)
0340 {
0341     MyMoneyAccount acc;
0342     QStandardItem  *itAcc;
0343     switch (objType) {
0344     case eMyMoney::File::Object::Account:
0345     {
0346         auto tmpAcc = MyMoneyFile::instance()->account(id);
0347         if (tmpAcc.accountType() != eMyMoney::Account::Type::Stock)
0348             return;
0349         acc = MyMoneyAccount(tmpAcc);
0350         itAcc = d->itemFromId(this, acc.id(), Role::EquityID);
0351         break;
0352     }
0353     case eMyMoney::File::Object::Security:
0354     {
0355         auto sec = MyMoneyFile::instance()->security(id);
0356         // in case we hit a currency, we simply bail out here
0357         // as there is nothing to do for us
0358         if(sec.isCurrency())
0359             return;
0360         itAcc = d->itemFromId(this, sec.id(), Role::SecurityID);
0361         if (!itAcc)
0362             return;
0363         const auto idAcc = itAcc->data(Role::EquityID).toString();
0364         acc = d->m_file->account(idAcc);
0365         break;
0366     }
0367     default:
0368         return;
0369     }
0370 
0371     auto itParentAcc = d->itemFromId(this, acc.parentAccountId(), Role::InvestmentID);
0372     // in case something went wrong, we bail out
0373     if(itParentAcc == nullptr) {
0374         qWarning() << "EquitiesModel::slotObjectModified: itParentAcc == 0";
0375         return;
0376     }
0377 
0378     auto modelID = itParentAcc->data(Role::InvestmentID).toString();      // get parent account from model
0379     if (modelID == acc.parentAccountId()) {                              // and if it matches with those from file then modify only
0380         d->setAccountData(itParentAcc, itAcc->row(), acc, d->m_columns);
0381     } else {                                                              // and if not then reparent
0382         slotObjectRemoved(eMyMoney::File::Object::Account, acc.id());
0383         slotObjectAdded(eMyMoney::File::Object::Account, id);
0384     }
0385 }
0386 
0387 /**
0388   * Notify the model that an object has been removed. An action is performed only if the object is an account.
0389   *
0390   */
0391 void EquitiesModel::slotObjectRemoved(eMyMoney::File::Object objType, const QString& id)
0392 {
0393     if (objType != eMyMoney::File::Object::Account)
0394         return;
0395 
0396     const auto indexList = match(index(0, 0), Role::EquityID, id, -1, Qt::MatchFlags(Qt::MatchExactly | Qt::MatchRecursive));
0397     for (const auto& index : indexList)
0398         removeRow(index.row(), index.parent());
0399 }
0400 
0401 /**
0402   * Notify the model that the account balance has been changed.
0403   */
0404 void EquitiesModel::slotBalanceOrValueChanged(const MyMoneyAccount &account)
0405 {
0406     if (account.accountType() != eMyMoney::Account::Type::Stock)
0407         return;
0408 
0409     const auto itAcc = d->itemFromId(this, account.id(), Role::EquityID);
0410     if (!itAcc)
0411         return;
0412     d->setAccountBalanceAndValue(itAcc->parent(), itAcc->row(), account, d->m_columns);
0413 }
0414 
0415 auto EquitiesModel::getColumns()
0416 {
0417     return &d->m_columns;
0418 }
0419 
0420 QString EquitiesModel::getHeaderName(const Column column)
0421 {
0422     switch(column) {
0423     case Equity:
0424         return i18n("Equity");
0425     case Symbol:
0426         return i18nc("@title stock symbol column", "Symbol");
0427     case Value:
0428         return i18n("Value");
0429     case Quantity:
0430         return i18n("Quantity");
0431     case Price:
0432         return i18n("Price");
0433     case LastPriceUpdate:
0434         return i18n("Last Price Update");
0435     default:
0436         return QString();
0437     }
0438 }
0439 
0440 class EquitiesFilterProxyModel::Private
0441 {
0442 public:
0443     Private() :
0444         m_mdlColumns(nullptr),
0445         m_file(MyMoneyFile::instance()),
0446         m_hideClosedAccounts(false),
0447         m_hideZeroBalanceAccounts(false)
0448     {}
0449 
0450     ~Private() {}
0451 
0452     QList<EquitiesModel::Column> *m_mdlColumns;
0453     QList<EquitiesModel::Column> m_visColumns;
0454 
0455     MyMoneyFile *m_file;
0456 
0457     bool m_hideClosedAccounts;
0458     bool m_hideZeroBalanceAccounts;
0459 };
0460 
0461 #if QT_VERSION < QT_VERSION_CHECK(5,10,0)
0462 #define QSortFilterProxyModel KRecursiveFilterProxyModel
0463 #endif
0464 EquitiesFilterProxyModel::EquitiesFilterProxyModel(QObject *parent, EquitiesModel *model, const QList<EquitiesModel::Column> &columns)
0465     : QSortFilterProxyModel(parent), d(new Private)
0466 {
0467     setRecursiveFilteringEnabled(true);
0468     setDynamicSortFilter(true);
0469     setFilterKeyColumn(-1);
0470     setSortLocaleAware(true);
0471     setFilterCaseSensitivity(Qt::CaseInsensitive);
0472     setSourceModel(model);
0473     d->m_mdlColumns = model->getColumns();
0474     d->m_visColumns.append(columns);
0475 }
0476 #undef QSortFilterProxyModel
0477 
0478 EquitiesFilterProxyModel::~EquitiesFilterProxyModel()
0479 {
0480     delete d;
0481 }
0482 
0483 /**
0484   * Set if closed accounts should be hidden or not.
0485   * @param hideClosedAccounts
0486   */
0487 void EquitiesFilterProxyModel::setHideClosedAccounts(const bool hideClosedAccounts)
0488 {
0489     d->m_hideClosedAccounts = hideClosedAccounts;
0490 }
0491 
0492 /**
0493   * Set if zero balance accounts should be hidden or not.
0494   * @param hideZeroBalanceAccounts
0495   */
0496 void EquitiesFilterProxyModel::setHideZeroBalanceAccounts(const bool hideZeroBalanceAccounts)
0497 {
0498     d->m_hideZeroBalanceAccounts = hideZeroBalanceAccounts;
0499 }
0500 
0501 bool EquitiesFilterProxyModel::filterAcceptsColumn(int source_column, const QModelIndex &source_parent) const
0502 {
0503     Q_UNUSED(source_parent)
0504     if (d->m_visColumns.isEmpty() || d->m_visColumns.contains(d->m_mdlColumns->at(source_column)))
0505         return true;
0506     return false;
0507 }
0508 
0509 bool EquitiesFilterProxyModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const
0510 {
0511     if (d->m_hideClosedAccounts || d->m_hideZeroBalanceAccounts) {
0512         const auto ixRow = sourceModel()->index(source_row, EquitiesModel::Equity, source_parent);
0513         const auto idAcc = sourceModel()->data(ixRow, EquitiesModel::EquityID).toString();
0514         const auto acc = d->m_file->account(idAcc);
0515 
0516         if (d->m_hideClosedAccounts &&
0517                 acc.isClosed())
0518             return false;
0519         if (d->m_hideZeroBalanceAccounts &&
0520                 acc.accountType() != eMyMoney::Account::Type::Investment && acc.balance().isZero())  // we should never hide investment account because all underlaying stocks will be hidden as well
0521             return false;
0522     }
0523     return true;
0524 }
0525 
0526 QList<EquitiesModel::Column> &EquitiesFilterProxyModel::getVisibleColumns()
0527 {
0528     return d->m_visColumns;
0529 }
0530 
0531 void EquitiesFilterProxyModel::slotColumnsMenu(const QPoint)
0532 {
0533     // construct all hideable columns list
0534     const QList<EquitiesModel::Column> idColumns {
0535         EquitiesModel::Symbol, EquitiesModel::Value,
0536         EquitiesModel::Quantity, EquitiesModel::Price
0537     };
0538 
0539     // create menu
0540     QMenu menu(i18n("Displayed columns"));
0541     QList<QAction *> actions;
0542     for (const auto idColumn : idColumns) {
0543         auto a = new QAction(nullptr);
0544         a->setObjectName(QString::number(idColumn));
0545         a->setText(EquitiesModel::getHeaderName(idColumn));
0546         a->setCheckable(true);
0547         a->setChecked(d->m_visColumns.contains(idColumn));
0548         actions.append(a);
0549     }
0550     menu.addActions(actions);
0551 
0552     // execute menu and get result
0553     const auto retAction = menu.exec(QCursor::pos());
0554     if (retAction) {
0555         const auto idColumn = static_cast<EquitiesModel::Column>(retAction->objectName().toInt());
0556         const auto isChecked = retAction->isChecked();
0557         const auto contains = d->m_visColumns.contains(idColumn);
0558         if (isChecked && !contains) {           // column has just been enabled
0559             d->m_visColumns.append(idColumn);     // change filtering variable
0560             Q_EMIT columnToggled(idColumn, true);   // Q_EMIT signal for method to add column to model
0561             invalidate();                         // refresh model to reflect recent changes
0562         } else if (!isChecked && contains) {    // column has just been disabled
0563             d->m_visColumns.removeOne(idColumn);
0564             Q_EMIT columnToggled(idColumn, false);
0565             invalidate();
0566         }
0567     }
0568 }
0569 #endif