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 }