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