Changeset View
Changeset View
Standalone View
Standalone View
kmymoney/models/equitiesmodel.cpp
- This file was added.
1 | /*************************************************************************** | ||||
---|---|---|---|---|---|
2 | equitiesmodel.cpp | ||||
3 | ------------------- | ||||
4 | copyright : (C) 2017 by Łukasz Wojniłowicz <lukasz.wojnilowicz@gmail.com> | ||||
5 | ***************************************************************************/ | ||||
6 | | ||||
7 | /*************************************************************************** | ||||
8 | * * | ||||
9 | * This program is free software; you can redistribute it and/or modify * | ||||
10 | * it under the terms of the GNU General Public License as published by * | ||||
11 | * the Free Software Foundation; either version 2 of the License, or * | ||||
12 | * (at your option) any later version. * | ||||
13 | * * | ||||
14 | ***************************************************************************/ | ||||
15 | | ||||
16 | #include "equitiesmodel.h" | ||||
17 | | ||||
18 | // ---------------------------------------------------------------------------- | ||||
19 | // QT Includes | ||||
20 | | ||||
21 | #include <QMenu> | ||||
22 | | ||||
23 | // ---------------------------------------------------------------------------- | ||||
24 | // KDE Includes | ||||
25 | | ||||
26 | #include <KLocalizedString> | ||||
27 | | ||||
28 | // ---------------------------------------------------------------------------- | ||||
29 | // Project Includes | ||||
30 | | ||||
31 | class EquitiesModel::Private | ||||
32 | { | ||||
33 | public: | ||||
34 | Private() : m_file(MyMoneyFile::instance()) | ||||
35 | { | ||||
36 | QVector<Column> columns {Column::Equity, Column::Symbol, Column::Value, | ||||
37 | Column::Quantity, Column::Price}; | ||||
38 | foreach (auto const column, columns) | ||||
39 | m_columns.append(column); | ||||
40 | } | ||||
41 | | ||||
42 | ~Private() {} | ||||
43 | | ||||
44 | void loadInvestmentAccount(QStandardItem *node, const MyMoneyAccount &invAcc) | ||||
45 | { | ||||
46 | auto itInvAcc = new QStandardItem(invAcc.name()); | ||||
47 | node->appendRow(itInvAcc); // investment account is meant to be added under root item | ||||
48 | itInvAcc->setEditable(false); | ||||
49 | itInvAcc->setColumnCount(m_columns.count()); | ||||
50 | setAccountData(node, itInvAcc->row(), invAcc, m_columns); | ||||
51 | | ||||
52 | const auto strStkAccList = invAcc.accountList(); // only stock or bond accounts are expected here | ||||
53 | foreach (const auto strStkAcc, strStkAccList) { | ||||
54 | auto stkAcc = m_file->account(strStkAcc); | ||||
55 | auto itStkAcc = new QStandardItem(strStkAcc); | ||||
56 | itStkAcc->setEditable(false); | ||||
57 | itInvAcc->appendRow(itStkAcc); | ||||
58 | setAccountData(itInvAcc, itStkAcc->row(), stkAcc, m_columns); | ||||
59 | } | ||||
60 | } | ||||
61 | | ||||
62 | void setAccountData(QStandardItem *node, const int row, const MyMoneyAccount &account, const QList<Column> &columns) | ||||
63 | { | ||||
64 | QStandardItem *cell; | ||||
65 | | ||||
66 | auto getCell = [&, row](const auto column) { | ||||
67 | cell = node->child(row, column); // try to get QStandardItem | ||||
68 | if (!cell) { // it may be uninitialized | ||||
69 | cell = new QStandardItem; // so create one | ||||
70 | node->setChild(row, column, cell); // and add it under the node | ||||
71 | cell->setEditable(false); // and don't forget that it's non-editable | ||||
72 | } | ||||
73 | }; | ||||
74 | | ||||
75 | auto colNum = m_columns.indexOf(Column::Equity); | ||||
76 | if (colNum == -1) | ||||
77 | return; | ||||
78 | | ||||
79 | // Equity | ||||
80 | getCell(colNum); | ||||
81 | if (columns.contains(Column::Equity)) { | ||||
82 | cell->setData(account.name(), Qt::DisplayRole); | ||||
83 | cell->setData(account.id(), Role::EquityID); | ||||
84 | cell->setData(account.currencyId(), Role::SecurityID); | ||||
85 | } | ||||
86 | | ||||
87 | if (account.accountType() == MyMoneyAccount::Investment) // investments accounts are not meant to be displayed, so stop here | ||||
88 | return; | ||||
89 | | ||||
90 | // Symbol | ||||
91 | if (columns.contains(Column::Symbol)) { | ||||
92 | colNum = m_columns.indexOf(Column::Symbol); | ||||
93 | if (colNum != -1) { | ||||
94 | auto security = m_file->security(account.currencyId()); | ||||
95 | getCell(colNum); | ||||
96 | cell->setData(security.tradingSymbol(), Qt::DisplayRole); | ||||
97 | } | ||||
98 | } | ||||
99 | | ||||
100 | setAccountBalanceAndValue(node, row, account, columns); | ||||
101 | } | ||||
102 | | ||||
103 | void setAccountBalanceAndValue(QStandardItem *node, const int row, const MyMoneyAccount &account, const QList<Column> &columns) | ||||
104 | { | ||||
105 | QStandardItem *cell; | ||||
106 | | ||||
107 | auto getCell = [&, row](const auto column) { | ||||
108 | cell = node->child(row, column); // try to get QStandardItem | ||||
109 | if (!cell) { // it may be uninitialized | ||||
110 | cell = new QStandardItem; // so create one | ||||
111 | node->setChild(row, column, cell); // and add it under the node | ||||
112 | cell->setEditable(false); // and don't forget that it's non-editable | ||||
113 | } | ||||
114 | }; | ||||
115 | | ||||
116 | auto colNum = m_columns.indexOf(Column::Equity); | ||||
117 | if (colNum == -1) | ||||
118 | return; | ||||
119 | | ||||
120 | auto balance = m_file->balance(account.id()); | ||||
121 | auto security = m_file->security(account.currencyId()); | ||||
122 | auto tradingCurrency = m_file->security(security.tradingCurrency()); | ||||
123 | auto price = m_file->price(account.currencyId(), tradingCurrency.id()); | ||||
124 | | ||||
125 | // Value | ||||
126 | if (columns.contains(Column::Value)) { | ||||
127 | colNum = m_columns.indexOf(Column::Value); | ||||
128 | if (colNum != -1) { | ||||
129 | getCell(colNum); | ||||
130 | if (price.isValid()) { | ||||
131 | auto prec = MyMoneyMoney::denomToPrec(tradingCurrency.smallestAccountFraction()); | ||||
132 | auto value = balance * price.rate(tradingCurrency.id()); | ||||
133 | auto strValue = QVariant(value.formatMoney(tradingCurrency.tradingSymbol(), prec)); | ||||
134 | cell->setData(strValue, Qt::DisplayRole); | ||||
135 | } else { | ||||
136 | cell->setData(QVariant("---"), Qt::DisplayRole); | ||||
137 | } | ||||
138 | cell->setData(QVariant(Qt::AlignRight | Qt::AlignVCenter), Qt::TextAlignmentRole); | ||||
139 | } | ||||
140 | } | ||||
141 | | ||||
142 | // Quantity | ||||
143 | if (columns.contains(Column::Quantity)) { | ||||
144 | colNum = m_columns.indexOf(Column::Quantity); | ||||
145 | if (colNum != -1) { | ||||
146 | getCell(colNum); | ||||
147 | auto prec = MyMoneyMoney::denomToPrec(security.smallestAccountFraction()); | ||||
148 | auto strQuantity = QVariant(balance.formatMoney(QString(), prec)); | ||||
149 | cell->setData(strQuantity, Qt::DisplayRole); | ||||
150 | } | ||||
151 | } | ||||
152 | | ||||
153 | // Price | ||||
154 | if (columns.contains(Column::Price)) { | ||||
155 | colNum = m_columns.indexOf(Column::Price); | ||||
156 | if (colNum != -1) { | ||||
157 | getCell(colNum); | ||||
158 | if (price.isValid()) { | ||||
159 | auto prec = security.pricePrecision(); | ||||
160 | auto strPrice = QVariant(price.rate(tradingCurrency.id()).formatMoney(tradingCurrency.tradingSymbol(), prec)); | ||||
161 | cell->setData(strPrice, Qt::DisplayRole); | ||||
162 | } else { | ||||
163 | cell->setData(QVariant("---"), Qt::DisplayRole); | ||||
164 | } | ||||
165 | cell->setData(QVariant(Qt::AlignRight | Qt::AlignVCenter), Qt::TextAlignmentRole); | ||||
166 | } | ||||
167 | } | ||||
168 | } | ||||
169 | | ||||
170 | QStandardItem *itemFromId(QStandardItemModel *model, const QString &id, const Role role) | ||||
171 | { | ||||
172 | const auto itemList = model->match(model->index(0, 0), role, QVariant(id), 1, Qt::MatchFlags(Qt::MatchExactly | Qt::MatchCaseSensitive | Qt::MatchRecursive)); | ||||
173 | if (!itemList.isEmpty()) | ||||
174 | return model->itemFromIndex(itemList.first()); | ||||
175 | return nullptr; | ||||
176 | } | ||||
177 | | ||||
178 | MyMoneyFile *m_file; | ||||
179 | QList<EquitiesModel::Column> m_columns; | ||||
180 | }; | ||||
181 | | ||||
182 | EquitiesModel::EquitiesModel(QObject *parent) | ||||
183 | : QStandardItemModel(parent), d(new Private) | ||||
184 | { | ||||
185 | init(); | ||||
186 | } | ||||
187 | | ||||
188 | EquitiesModel::~EquitiesModel() | ||||
189 | { | ||||
190 | delete d; | ||||
191 | } | ||||
192 | | ||||
193 | void EquitiesModel::init() | ||||
194 | { | ||||
195 | QStringList headerLabels; | ||||
196 | foreach (const auto column, d->m_columns) | ||||
197 | headerLabels.append(getHeaderName(column)); | ||||
198 | setHorizontalHeaderLabels(headerLabels); | ||||
199 | } | ||||
200 | | ||||
201 | void EquitiesModel::load() | ||||
202 | { | ||||
203 | this->blockSignals(true); | ||||
204 | | ||||
205 | auto rootItem = invisibleRootItem(); | ||||
206 | QList<MyMoneyAccount> accList; | ||||
207 | d->m_file->accountList(accList); // get all available accounts | ||||
208 | foreach (const auto acc, accList) | ||||
209 | if (acc.accountType() == MyMoneyAccount::Investment) // but add only investment accounts (and its children) to the model | ||||
210 | d->loadInvestmentAccount(rootItem, acc); | ||||
211 | | ||||
212 | this->blockSignals(false); | ||||
213 | } | ||||
214 | | ||||
215 | /** | ||||
216 | * Notify the model that an object has been added. An action is performed only if the object is an account. | ||||
217 | * | ||||
218 | */ | ||||
219 | void EquitiesModel::slotObjectAdded(MyMoneyFile::notificationObjectT objType, const MyMoneyObject * const obj) | ||||
220 | { | ||||
221 | // check whether change is about accounts | ||||
222 | if (objType != MyMoneyFile::notifyAccount) | ||||
223 | return; | ||||
224 | | ||||
225 | // check whether change is about either investment or stock account | ||||
226 | const auto acc = dynamic_cast<const MyMoneyAccount * const>(obj); | ||||
227 | if (!acc || | ||||
228 | (acc->accountType() != MyMoneyAccount::Investment && | ||||
229 | acc->accountType() != MyMoneyAccount::Stock)) | ||||
230 | return; | ||||
231 | auto itAcc = d->itemFromId(this, acc->id(), Role::EquityID); | ||||
232 | | ||||
233 | QStandardItem *itParentAcc; | ||||
234 | if (acc->accountType() == MyMoneyAccount::Investment) // if it's investment account then its parent is root item | ||||
235 | itParentAcc = invisibleRootItem(); | ||||
236 | else // otherwise it's stock account and its parent is investment account | ||||
237 | itParentAcc = d->itemFromId(this, acc->parentAccountId(), Role::InvestmentID); | ||||
238 | | ||||
239 | // if account doesn't exist in model then add it | ||||
240 | if (!itAcc) { | ||||
241 | itAcc = new QStandardItem(acc->name()); | ||||
242 | itParentAcc->appendRow(itAcc); | ||||
243 | itAcc->setEditable(false); | ||||
244 | } | ||||
245 | | ||||
246 | d->setAccountData(itParentAcc, itAcc->row(), *acc, d->m_columns); | ||||
247 | } | ||||
248 | | ||||
249 | /** | ||||
250 | * Notify the model that an object has been modified. An action is performed only if the object is an account. | ||||
251 | * | ||||
252 | */ | ||||
253 | void EquitiesModel::slotObjectModified(MyMoneyFile::notificationObjectT objType, const MyMoneyObject * const obj) | ||||
254 | { | ||||
255 | const MyMoneyAccount *acc; | ||||
256 | QStandardItem *itAcc; | ||||
257 | switch (objType) { | ||||
258 | case MyMoneyFile::notifyAccount: | ||||
259 | { | ||||
260 | auto tmpAcc = dynamic_cast<const MyMoneyAccount * const>(obj); | ||||
261 | if (!tmpAcc || tmpAcc->accountType() != MyMoneyAccount::Stock) | ||||
262 | return; | ||||
263 | acc = tmpAcc; | ||||
264 | itAcc = d->itemFromId(this, acc->id(), Role::EquityID); | ||||
265 | break; | ||||
266 | } | ||||
267 | case MyMoneyFile::notifySecurity: | ||||
268 | { | ||||
269 | auto sec = dynamic_cast<const MyMoneySecurity * const>(obj); | ||||
270 | itAcc = d->itemFromId(this, sec->id(), Role::SecurityID); | ||||
271 | if (!itAcc) | ||||
272 | return; | ||||
273 | const auto idAcc = itAcc->data(Role::EquityID).toString(); | ||||
274 | acc = &d->m_file->account(idAcc); | ||||
275 | break; | ||||
276 | } | ||||
277 | default: | ||||
278 | return; | ||||
279 | } | ||||
280 | | ||||
281 | auto itParentAcc = d->itemFromId(this, acc->parentAccountId(), Role::InvestmentID); | ||||
282 | | ||||
283 | auto modelID = itParentAcc->data(Role::InvestmentID).toString(); // get parent account from model | ||||
284 | if (modelID == acc->parentAccountId()) { // and if it matches with those from file then modify only | ||||
285 | d->setAccountData(itParentAcc, itAcc->row(), *acc, d->m_columns); | ||||
286 | } else { // and if not then reparent | ||||
287 | slotObjectRemoved(MyMoneyFile::notifyAccount, acc->id()); | ||||
288 | slotObjectAdded(MyMoneyFile::notifyAccount, obj); | ||||
289 | } | ||||
290 | } | ||||
291 | | ||||
292 | /** | ||||
293 | * Notify the model that an object has been removed. An action is performed only if the object is an account. | ||||
294 | * | ||||
295 | */ | ||||
296 | void EquitiesModel::slotObjectRemoved(MyMoneyFile::notificationObjectT objType, const QString& id) | ||||
297 | { | ||||
298 | if (objType != MyMoneyFile::notifyAccount) | ||||
299 | return; | ||||
300 | | ||||
301 | const auto indexList = match(index(0, 0), Role::EquityID, id, -1, Qt::MatchFlags(Qt::MatchExactly | Qt::MatchRecursive)); | ||||
302 | foreach (const auto index, indexList) | ||||
303 | removeRow(index.row(), index.parent()); | ||||
304 | } | ||||
305 | | ||||
306 | /** | ||||
307 | * Notify the model that the account balance has been changed. | ||||
308 | */ | ||||
309 | void EquitiesModel::slotBalanceOrValueChanged(const MyMoneyAccount &account) | ||||
310 | { | ||||
311 | if (account.accountType() != MyMoneyAccount::Stock) | ||||
312 | return; | ||||
313 | | ||||
314 | const auto itAcc = d->itemFromId(this, account.id(), Role::EquityID); | ||||
315 | if (!itAcc) | ||||
316 | return; | ||||
317 | d->setAccountBalanceAndValue(itAcc->parent(), itAcc->row(), account, d->m_columns); | ||||
318 | } | ||||
319 | | ||||
320 | auto EquitiesModel::getColumns() | ||||
321 | { | ||||
322 | return &d->m_columns; | ||||
323 | } | ||||
324 | | ||||
325 | QString EquitiesModel::getHeaderName(const Column column) | ||||
326 | { | ||||
327 | switch(column) { | ||||
328 | case Equity: | ||||
329 | return i18n("Equity"); | ||||
330 | case Symbol: | ||||
331 | return i18n("Symbol"); | ||||
332 | case Value: | ||||
333 | return i18n("Value"); | ||||
334 | case Quantity: | ||||
335 | return i18n("Quantity"); | ||||
336 | case Price: | ||||
337 | return i18n("Price"); | ||||
338 | default: | ||||
339 | return QString(); | ||||
340 | } | ||||
341 | } | ||||
342 | | ||||
343 | class EquitiesFilterProxyModel::Private | ||||
344 | { | ||||
345 | public: | ||||
346 | Private() : | ||||
347 | m_mdlColumns(nullptr), | ||||
348 | m_file(MyMoneyFile::instance()), | ||||
349 | m_hideClosedAccounts(false), | ||||
350 | m_hideZeroBalanceAccounts(false) | ||||
351 | {} | ||||
352 | | ||||
353 | ~Private() {} | ||||
354 | | ||||
355 | QList<EquitiesModel::Column> *m_mdlColumns; | ||||
356 | QList<EquitiesModel::Column> m_visColumns; | ||||
357 | | ||||
358 | MyMoneyFile *m_file; | ||||
359 | | ||||
360 | bool m_hideClosedAccounts; | ||||
361 | bool m_hideZeroBalanceAccounts; | ||||
362 | }; | ||||
363 | | ||||
364 | EquitiesFilterProxyModel::EquitiesFilterProxyModel(QObject *parent, EquitiesModel *model, const QList<EquitiesModel::Column> &columns) | ||||
365 | : KRecursiveFilterProxyModel(parent), d(new Private) | ||||
366 | { | ||||
367 | setDynamicSortFilter(true); | ||||
368 | setFilterKeyColumn(-1); | ||||
369 | setSortLocaleAware(true); | ||||
370 | setFilterCaseSensitivity(Qt::CaseInsensitive); | ||||
371 | setSourceModel(model); | ||||
372 | d->m_mdlColumns = model->getColumns(); | ||||
373 | d->m_visColumns.append(columns); | ||||
374 | } | ||||
375 | | ||||
376 | EquitiesFilterProxyModel::~EquitiesFilterProxyModel() | ||||
377 | { | ||||
378 | delete d; | ||||
379 | } | ||||
380 | | ||||
381 | /** | ||||
382 | * Set if closed accounts should be hidden or not. | ||||
383 | * @param hideClosedAccounts | ||||
384 | */ | ||||
385 | void EquitiesFilterProxyModel::setHideClosedAccounts(const bool hideClosedAccounts) | ||||
386 | { | ||||
387 | d->m_hideClosedAccounts = hideClosedAccounts; | ||||
388 | } | ||||
389 | | ||||
390 | /** | ||||
391 | * Set if zero balance accounts should be hidden or not. | ||||
392 | * @param hideZeroBalanceAccounts | ||||
393 | */ | ||||
394 | void EquitiesFilterProxyModel::setHideZeroBalanceAccounts(const bool hideZeroBalanceAccounts) | ||||
395 | { | ||||
396 | d->m_hideZeroBalanceAccounts = hideZeroBalanceAccounts; | ||||
397 | } | ||||
398 | | ||||
399 | bool EquitiesFilterProxyModel::filterAcceptsColumn(int source_column, const QModelIndex &source_parent) const | ||||
400 | { | ||||
401 | Q_UNUSED(source_parent) | ||||
402 | if (d->m_visColumns.isEmpty() || d->m_visColumns.contains(d->m_mdlColumns->at(source_column))) | ||||
403 | return true; | ||||
404 | return false; | ||||
405 | } | ||||
406 | | ||||
407 | bool EquitiesFilterProxyModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const | ||||
408 | { | ||||
409 | if (d->m_hideClosedAccounts || d->m_hideZeroBalanceAccounts) { | ||||
410 | const auto ixRow = sourceModel()->index(source_row, EquitiesModel::Equity, source_parent); | ||||
411 | const auto idAcc = sourceModel()->data(ixRow, EquitiesModel::EquityID).toString(); | ||||
412 | const auto acc = d->m_file->account(idAcc); | ||||
413 | | ||||
414 | if (d->m_hideClosedAccounts && | ||||
415 | acc.isClosed()) | ||||
416 | return false; | ||||
417 | if (d->m_hideZeroBalanceAccounts && | ||||
418 | acc.accountType() != MyMoneyAccount::Investment && acc.balance().isZero()) // we should never hide investment account because all underlaying stocks will be hidden as well | ||||
419 | return false; | ||||
420 | } | ||||
421 | return true; | ||||
422 | } | ||||
423 | | ||||
424 | QList<EquitiesModel::Column> &EquitiesFilterProxyModel::getVisibleColumns() | ||||
425 | { | ||||
426 | return d->m_visColumns; | ||||
427 | } | ||||
428 | | ||||
429 | void EquitiesFilterProxyModel::slotColumnsMenu(const QPoint) | ||||
430 | { | ||||
431 | // construct all hideable columns list | ||||
432 | const QList<EquitiesModel::Column> idColumns { | ||||
433 | EquitiesModel::Symbol, EquitiesModel::Value, | ||||
434 | EquitiesModel::Quantity, EquitiesModel::Price | ||||
435 | }; | ||||
436 | | ||||
437 | // create menu | ||||
438 | QMenu menu(i18n("Displayed columns")); | ||||
439 | QList<QAction *> actions; | ||||
440 | foreach (const auto idColumn, idColumns) { | ||||
441 | auto a = new QAction(nullptr); | ||||
442 | a->setObjectName(QString::number(idColumn)); | ||||
443 | a->setText(EquitiesModel::getHeaderName(idColumn)); | ||||
444 | a->setCheckable(true); | ||||
445 | a->setChecked(d->m_visColumns.contains(idColumn)); | ||||
446 | actions.append(a); | ||||
447 | } | ||||
448 | menu.addActions(actions); | ||||
449 | | ||||
450 | // execute menu and get result | ||||
451 | const auto retAction = menu.exec(QCursor::pos()); | ||||
452 | if (retAction) { | ||||
453 | const auto idColumn = static_cast<EquitiesModel::Column>(retAction->objectName().toInt()); | ||||
454 | const auto isChecked = retAction->isChecked(); | ||||
455 | const auto contains = d->m_visColumns.contains(idColumn); | ||||
456 | if (isChecked && !contains) { // column has just been enabled | ||||
457 | d->m_visColumns.append(idColumn); // change filtering variable | ||||
458 | emit columnToggled(idColumn, true); // emit signal for method to add column to model | ||||
459 | invalidate(); // refresh model to reflect recent changes | ||||
460 | } else if (!isChecked && contains) { // column has just been disabled | ||||
461 | d->m_visColumns.removeOne(idColumn); | ||||
462 | emit columnToggled(idColumn, false); | ||||
463 | invalidate(); | ||||
464 | } | ||||
465 | } | ||||
466 | } |