File indexing completed on 2024-05-19 05:06:59
0001 /* 0002 SPDX-FileCopyrightText: 2019-2020 Thomas Baumgart <tbaumgart@kde.org> 0003 SPDX-License-Identifier: GPL-2.0-or-later 0004 */ 0005 0006 #include "specialledgeritemfilter.h" 0007 0008 // ---------------------------------------------------------------------------- 0009 // QT Includes 0010 0011 #include <QTimer> 0012 0013 // ---------------------------------------------------------------------------- 0014 // KDE Includes 0015 0016 // ---------------------------------------------------------------------------- 0017 // Project Includes 0018 0019 #include "journalmodel.h" 0020 #include "ledgerfilter.h" 0021 #include "ledgersortproxymodel_p.h" 0022 #include "mymoneyfile.h" 0023 #include "reconciliationmodel.h" 0024 #include "specialdatesmodel.h" 0025 0026 using namespace eMyMoney; 0027 0028 class SpecialLedgerItemFilterPrivate : public LedgerSortProxyModelPrivate 0029 { 0030 struct BalanceParameter { 0031 Qt::SortOrder sortOrder; 0032 int startRow = 0; 0033 int lastRow = 0; 0034 int firstRow = 0; 0035 bool valid = false; 0036 0037 void setFirstRow(int row) 0038 { 0039 firstRow = row; 0040 valid = true; 0041 } 0042 bool isValid() const 0043 { 0044 return valid; 0045 } 0046 }; 0047 0048 public: 0049 SpecialLedgerItemFilterPrivate(SpecialLedgerItemFilter* qq) 0050 : LedgerSortProxyModelPrivate(qq) 0051 , sourceModel(nullptr) 0052 , showReconciliationEntries(LedgerViewSettings::DontShowReconciliationHeader) 0053 , filterBalanceMode(SpecialLedgerItemFilter::FilterBalanceNormal) 0054 , lastWasReconciliationEntry(false) 0055 { 0056 updateDelayTimer.setSingleShot(true); 0057 updateDelayTimer.setInterval(20); 0058 } 0059 0060 bool isSortingByDateFirst() const 0061 { 0062 if (sourceModel) { 0063 const auto sourceLedgerSortOrder = sourceModel->ledgerSortOrder(); 0064 if (!sourceLedgerSortOrder.isEmpty()) { 0065 const auto role = sourceLedgerSortOrder.at(0).sortRole; 0066 return dateRoles.contains(role); 0067 } 0068 } 0069 return false; 0070 } 0071 /** 0072 * Returns true if the main sort key is a date (post or entry) 0073 * or second and the main sort key is security 0074 */ 0075 bool isSortingByDate() const 0076 { 0077 if (sourceModel) { 0078 const auto sourceLedgerSortOrder = sourceModel->ledgerSortOrder(); 0079 const int maxIndex = qMin(sourceLedgerSortOrder.count(), 2); 0080 for (int i = 0; i < maxIndex; ++i) { 0081 const auto role = sourceLedgerSortOrder.at(i).sortRole; 0082 if (dateRoles.contains(role)) { 0083 return true; 0084 } 0085 // if the first one is security then 0086 // check next sorting parameter too 0087 if ((i == 0) && (role == eMyMoney::Model::JournalSplitSecurityNameRole)) { 0088 continue; 0089 } 0090 break; 0091 } 0092 } 0093 return false; 0094 }; 0095 0096 bool isSortingBySecurity() const 0097 { 0098 if (sourceModel) { 0099 const auto sourceLedgerSortOrder = sourceModel->ledgerSortOrder(); 0100 if (!sourceLedgerSortOrder.isEmpty()) { 0101 return (sourceLedgerSortOrder.at(0).sortRole == eMyMoney::Model::JournalSplitSecurityNameRole); 0102 } 0103 } 0104 return false; 0105 } 0106 0107 bool showBalance() const 0108 { 0109 bool filterActive(false); 0110 0111 if (filterBalanceMode == SpecialLedgerItemFilter::FilterBalanceNormal) { 0112 filterActive = q->sourceModel()->data(QModelIndex(), eMyMoney::Model::ActiveFilterRole).toBool(); 0113 0114 } else if (filterBalanceMode == SpecialLedgerItemFilter::FilterBalanceReconciliation) { 0115 filterActive = q->sourceModel()->data(QModelIndex(), eMyMoney::Model::ActiveFilterTextRole).toBool(); 0116 filterActive |= (q->sourceModel()->data(QModelIndex(), eMyMoney::Model::ActiveFilterStateRole).value<LedgerFilter::State>() 0117 != LedgerFilter::State::NotReconciled); 0118 } 0119 return isSortingByDate() && !filterActive; 0120 } 0121 0122 /** 0123 * initializeBalanceCalculation sets up the BalanceParameter structure 0124 * with the row values to be visited for balance calculation. If sort order 0125 * is ascending, rows are positive. If sort order is descending, row values 0126 * are negative, so that startRow is always smaller than lastRow. This 0127 * simplifies the calculation of the balance. 0128 * 0129 * @Note One must pay attention to use @c qAbs(x) when creating the QModelIndex 0130 * to access the data. 0131 */ 0132 BalanceParameter initializeBalanceCalculation() const 0133 { 0134 BalanceParameter parameter; 0135 // we only display balances when sorted by a date field 0136 const auto order = sourceModel->ledgerSortOrder(); 0137 if (isSortingByDate()) { 0138 const auto rows = q->rowCount(); 0139 if (rows > 0) { 0140 parameter.sortOrder = order.first().sortOrder; 0141 parameter.startRow = (parameter.sortOrder == Qt::AscendingOrder) ? 0 : -(rows - 1); 0142 parameter.lastRow = (parameter.sortOrder == Qt::AscendingOrder) ? rows - 1 : 0; 0143 0144 for (int row = parameter.startRow; (row >= parameter.startRow) && (row <= parameter.lastRow); ++row) { 0145 const auto idx = q->index(qAbs(row), 0); 0146 if (isJournalModel(idx) || isSchedulesJournalModel(idx)) { 0147 parameter.setFirstRow(row); 0148 break; 0149 } 0150 } 0151 } 0152 } 0153 return parameter; 0154 } 0155 0156 void recalculateBalances() 0157 { 0158 QMap<QString, MyMoneyMoney> balances; 0159 0160 const auto parameter = initializeBalanceCalculation(); 0161 0162 if (parameter.isValid()) { 0163 auto idx = q->index(qAbs(parameter.firstRow), 0); 0164 const bool showValuesInverted(idx.data(eMyMoney::Model::ShowValueInvertedRole).toBool()); 0165 auto accountId = idx.data(eMyMoney::Model::JournalSplitAccountIdRole).toString(); 0166 const auto account = MyMoneyFile::instance()->accountsModel()->itemById(accountId); 0167 const bool isInvestmentAccount = (account.accountType() == eMyMoney::Account::Type::Investment) || account.isInvest(); 0168 0169 QDate startDate = idx.data(eMyMoney::Model::TransactionPostDateRole).toDate(); 0170 if (isSortingByDate()) { 0171 // if sorted by entry date or by security, we need to find the balance of the oldest 0172 // transaction in the set. This may not be the date of the first transaction displayed 0173 for (int row = parameter.firstRow; (row >= parameter.startRow) && (row <= parameter.lastRow); ++row) { 0174 idx = q->index(qAbs(row), 0); 0175 if (!idx.data(eMyMoney::Model::IdRole).toString().isEmpty() && isJournalModel(idx)) { 0176 if (idx.data(eMyMoney::Model::TransactionPostDateRole).toDate() < startDate) { 0177 startDate = idx.data(eMyMoney::Model::TransactionPostDateRole).toDate(); 0178 } 0179 } 0180 } 0181 } 0182 0183 // we need to get the balance of the day prior to the first day found 0184 startDate = startDate.addDays(-1); 0185 0186 // take care of 0187 for (int row = parameter.firstRow; (row >= parameter.startRow) && (row <= parameter.lastRow); ++row) { 0188 // check if we have the balance for this account 0189 idx = q->index(qAbs(row), 0); 0190 if (!(isJournalModel(idx) || isSchedulesJournalModel(idx))) { 0191 continue; 0192 } 0193 0194 accountId = idx.data(eMyMoney::Model::JournalSplitAccountIdRole).toString(); 0195 if (balances.constFind(accountId) == balances.constEnd()) { 0196 balances[accountId] = MyMoneyFile::instance()->balance(accountId, startDate); 0197 } 0198 0199 const auto shares = idx.data(eMyMoney::Model::SplitSharesRole).value<MyMoneyMoney>(); 0200 if (isInvestmentAccount) { 0201 if (idx.data(eMyMoney::Model::TransactionIsStockSplitRole).toBool()) { 0202 balances[accountId] = MyMoneyFile::instance()->journalModel()->stockSplitBalance(accountId, balances[accountId], shares); 0203 } else { 0204 balances[accountId] += shares; 0205 } 0206 0207 } else { 0208 if (showValuesInverted) { 0209 balances[accountId] -= shares; 0210 } else { 0211 balances[accountId] += shares; 0212 } 0213 } 0214 q->setData(idx, QVariant::fromValue(balances[accountId]), eMyMoney::Model::JournalBalanceRole); 0215 } 0216 } 0217 } 0218 0219 bool filterAcceptsRow(const QModelIndex& idx, const QModelIndex& source_parent, int rowCount) 0220 { 0221 switch (idx.data(eMyMoney::Model::BaseModelRole).value<eMyMoney::Model::Roles>()) { 0222 case eMyMoney::Model::SpecialDatesEntryRole: { 0223 // Don't show them if display is not sorted by date 0224 if (!isSortingByDateFirst()) 0225 return false; 0226 // make sure we don't show trailing special date entries 0227 int row = idx.row() + 1; 0228 bool visible = false; 0229 QModelIndex testIdx; 0230 for (; !visible && row < rowCount; ++row) { 0231 testIdx = q->sourceModel()->index(row, 0, source_parent); 0232 if (testIdx.data(eMyMoney::Model::IdRole).toString().isEmpty()) { 0233 // the empty id is the entry for the new transaction entry 0234 // we're done scanning 0235 break; 0236 } 0237 if (!isSpecialDatesModel(testIdx)) { 0238 // we did not hit a special date entry 0239 // now we need to check for a real transaction or the online balance one 0240 if (!testIdx.data(eMyMoney::Model::JournalTransactionIdRole).toString().isEmpty()) { 0241 visible = true; 0242 } 0243 break; 0244 } 0245 } 0246 0247 // in case this is not a trailing date entry, we need to check 0248 // if it is the last of a row of date entries. 0249 if (visible && ((idx.row() + 1) < rowCount)) { 0250 // check if the next is also a date entry 0251 testIdx = q->sourceModel()->index(idx.row() + 1, 0, source_parent); 0252 if (isSpecialDatesModel(testIdx)) { 0253 visible = false; 0254 } 0255 } 0256 return visible; 0257 } 0258 0259 case eMyMoney::Model::ReconciliationEntryRole: { 0260 // Don't show them if view is not sorted by date 0261 if (!isSortingByDate()) { 0262 return false; 0263 } 0264 // Depending on the setting we only show a subset 0265 if (showReconciliationEntries != LedgerViewSettings::ShowAllReconciliationHeader) { 0266 const auto filterHint = idx.data(eMyMoney::Model::ReconciliationFilterHintRole).value<eMyMoney::Model::ReconciliationFilterHint>(); 0267 switch (showReconciliationEntries) { 0268 case LedgerViewSettings::DontShowReconciliationHeader: 0269 if (filterHint != eMyMoney::Model::DontFilter) { 0270 return false; 0271 } 0272 break; 0273 0274 case LedgerViewSettings::ShowLastReconciliationHeader: 0275 if (filterHint == eMyMoney::Model::StdFilter) { 0276 return false; 0277 } 0278 // intentional fall through 0279 0280 case LedgerViewSettings::ShowAllReconciliationHeader: 0281 break; 0282 } 0283 } 0284 0285 // in case the source model is not sorting, we 0286 // can assume that the item is visible. Once it 0287 // is sorted, it is early enough to perform the 0288 // other checks for reconciliation entries. 0289 // Not suppressing this this on an unsorted model 0290 // may cause a hug performance penalty (looks like 0291 // the application hung up in certain scenarios) 0292 if (!sourceModel->inSorting()) { 0293 return true; 0294 } 0295 0296 // in case we get here recursively, we simply assume 0297 // that this entry will be shown, so the actual one 0298 // that is checked will be hidden 0299 if (lastWasReconciliationEntry) { 0300 return true; 0301 } 0302 0303 // make sure we only show reconciliation entries that are not followed by 0304 // another reconciliation entry. Only inspect visible items 0305 lastWasReconciliationEntry = true; 0306 int row = idx.row() + 1; 0307 while (row < rowCount) { 0308 const auto testIdx = q->sourceModel()->index(row, 0, source_parent); 0309 if (filterAcceptsRow(testIdx, source_parent, rowCount)) { 0310 lastWasReconciliationEntry = false; 0311 if (isReconciliationModel(testIdx)) { 0312 return false; 0313 } 0314 return true; 0315 } 0316 ++row; 0317 } 0318 return true; 0319 } 0320 0321 default: 0322 break; 0323 } 0324 return true; 0325 } 0326 0327 QSet<int> dateRoles = { 0328 eMyMoney::Model::TransactionPostDateRole, 0329 eMyMoney::Model::TransactionEntryDateRole, 0330 eMyMoney::Model::SplitReconcileDateRole, 0331 eMyMoney::Model::IdRole, 0332 }; 0333 0334 LedgerSortProxyModel* sourceModel; 0335 QTimer updateDelayTimer; 0336 LedgerViewSettings::ReconciliationHeader showReconciliationEntries; 0337 SpecialLedgerItemFilter::FilterBalanceMode filterBalanceMode; 0338 bool lastWasReconciliationEntry; 0339 }; 0340 0341 SpecialLedgerItemFilter::SpecialLedgerItemFilter(QObject* parent) 0342 : LedgerSortProxyModel(new SpecialLedgerItemFilterPrivate(this), parent) 0343 { 0344 Q_D(SpecialLedgerItemFilter); 0345 setObjectName("SpecialLedgerItemFilter"); 0346 connect(&d->updateDelayTimer, &QTimer::timeout, this, [&]() { 0347 // sort afresh in case some rows need to be resorted 0348 // doSort() inherits a call to invalidateFilter(). 0349 doSort(); 0350 }); 0351 0352 connect(MyMoneyFile::instance()->journalModel(), &JournalModel::balanceChanged, this, [&](const QString& accountId) { 0353 Q_D(SpecialLedgerItemFilter); 0354 const auto model = sourceModel(); 0355 // in case we have a model assigned and 0356 // then we check if accountId is referring this account 0357 if (model) { 0358 const auto rows = model->rowCount(); 0359 // we scan the rows for the first one containing a split 0360 // referencing an account to check if it is ours 0361 for (int row = 0; row < rows; ++row) { 0362 const auto idx = model->index(row, 0); 0363 if (!idx.data(eMyMoney::Model::IdRole).toString().isEmpty() && d->isJournalModel(idx)) { 0364 if (idx.data(eMyMoney::Model::JournalSplitAccountIdRole).toString() == accountId) { 0365 forceReload(); 0366 // we found an account id in the ledger 0367 // so we can stop scanning 0368 break; 0369 } 0370 } 0371 } 0372 } 0373 }); 0374 } 0375 0376 void SpecialLedgerItemFilter::setSourceModel(QAbstractItemModel* model) 0377 { 0378 Q_UNUSED(model) 0379 qDebug() << "This must never be called"; 0380 } 0381 0382 void SpecialLedgerItemFilter::setSourceModel(LedgerSortProxyModel* model) 0383 { 0384 Q_D(SpecialLedgerItemFilter); 0385 if (sourceModel()) { 0386 disconnect(sourceModel(), &QAbstractItemModel::rowsRemoved, this, &SpecialLedgerItemFilter::forceReload); 0387 disconnect(sourceModel(), &QAbstractItemModel::rowsInserted, this, &SpecialLedgerItemFilter::forceReload); 0388 disconnect(sourceModel(), &QAbstractItemModel::rowsMoved, this, &SpecialLedgerItemFilter::forceReload); 0389 disconnect(sourceModel(), &QAbstractItemModel::modelReset, this, &SpecialLedgerItemFilter::forceReload); 0390 } 0391 LedgerSortProxyModel::setSourceModel(model); 0392 if (model) { 0393 connect(model, &QAbstractItemModel::rowsRemoved, this, &SpecialLedgerItemFilter::forceReload); 0394 connect(model, &QAbstractItemModel::rowsInserted, this, &SpecialLedgerItemFilter::forceReload); 0395 connect(model, &QAbstractItemModel::rowsMoved, this, &SpecialLedgerItemFilter::forceReload); 0396 connect(model, &QAbstractItemModel::modelReset, this, &SpecialLedgerItemFilter::forceReload); 0397 } 0398 d->sourceModel = model; 0399 } 0400 0401 void SpecialLedgerItemFilter::setLedgerSortOrder(LedgerSortOrder sortOrder) 0402 { 0403 Q_D(SpecialLedgerItemFilter); 0404 if (d->sourceModel) { 0405 d->ledgerSortOrder = sortOrder; 0406 d->sourceModel->setLedgerSortOrder(sortOrder); 0407 } 0408 } 0409 0410 LedgerSortOrder SpecialLedgerItemFilter::ledgerSortOrder() const 0411 { 0412 Q_D(const SpecialLedgerItemFilter); 0413 if (d->sourceModel) { 0414 return d->sourceModel->ledgerSortOrder(); 0415 } 0416 return {}; 0417 } 0418 void SpecialLedgerItemFilter::sort(int column, Qt::SortOrder order) 0419 { 0420 Q_D(SpecialLedgerItemFilter); 0421 0422 if (column >= 0) { 0423 // propagate the sorting to the source model and 0424 // update balances 0425 d->sourceModel->sort(column, order); 0426 d->recalculateBalances(); 0427 } 0428 } 0429 0430 void SpecialLedgerItemFilter::setSortingEnabled(bool enable) 0431 { 0432 Q_D(SpecialLedgerItemFilter); 0433 0434 if (d->sortEnabled != enable) { 0435 d->sortEnabled = enable; 0436 // propagate setting to source model. This 0437 // will do the sorting if needed. We only 0438 // need to recalc the balance afterwards. 0439 d->sourceModel->setSortingEnabled(enable); 0440 if (enable) { 0441 invalidateFilter(); 0442 d->recalculateBalances(); 0443 } 0444 } 0445 } 0446 0447 void SpecialLedgerItemFilter::setHideReconciledTransactions(bool hide) 0448 { 0449 Q_D(SpecialLedgerItemFilter); 0450 if (d->hideReconciledTransactions != hide) { 0451 d->hideReconciledTransactions = hide; 0452 forceReload(); 0453 } 0454 } 0455 0456 void SpecialLedgerItemFilter::setShowReconciliationEntries(LedgerViewSettings::ReconciliationHeader show) 0457 { 0458 Q_D(SpecialLedgerItemFilter); 0459 0460 if (d->showReconciliationEntries != show) { 0461 d->showReconciliationEntries = show; 0462 forceReload(); 0463 } 0464 } 0465 0466 void SpecialLedgerItemFilter::doSortOnIdle() 0467 { 0468 Q_D(SpecialLedgerItemFilter); 0469 d->sourceModel->doSortOnIdle(); 0470 } 0471 0472 void SpecialLedgerItemFilter::setFilterBalanceMode(SpecialLedgerItemFilter::FilterBalanceMode mode) 0473 { 0474 Q_D(SpecialLedgerItemFilter); 0475 d->filterBalanceMode = mode; 0476 } 0477 0478 bool SpecialLedgerItemFilter::filterAcceptsRow(int source_row, const QModelIndex& source_parent) const 0479 { 0480 Q_D(const SpecialLedgerItemFilter); 0481 0482 const QModelIndex idx = sourceModel()->index(source_row, 0, source_parent); 0483 0484 switch (idx.data(eMyMoney::Model::BaseModelRole).value<eMyMoney::Model::Roles>()) { 0485 case eMyMoney::Model::SpecialDatesEntryRole: 0486 case eMyMoney::Model::ReconciliationEntryRole: 0487 return const_cast<SpecialLedgerItemFilterPrivate*>(d)->filterAcceptsRow(idx, source_parent, sourceModel()->rowCount(source_parent)); 0488 0489 case eMyMoney::Model::OnlineBalanceEntryRole: 0490 // Don't show online balance items if display is not sorted by date 0491 if (!d->isSortingByDate()) { 0492 return false; 0493 } 0494 break; 0495 0496 case eMyMoney::Model::SecurityAccountNameEntryRole: 0497 // Don't show online balance items if display is not sorted by date 0498 if (!d->isSortingBySecurity()) { 0499 return false; 0500 } 0501 break; 0502 0503 default: 0504 break; 0505 } 0506 return true; 0507 } 0508 0509 void SpecialLedgerItemFilter::forceReload() 0510 { 0511 Q_D(SpecialLedgerItemFilter); 0512 d->updateDelayTimer.start(); 0513 } 0514 0515 QVariant SpecialLedgerItemFilter::data(const QModelIndex& index, int role) const 0516 { 0517 Q_D(const SpecialLedgerItemFilter); 0518 if (index.column() == JournalModel::Balance) { 0519 switch (role) { 0520 case Qt::DisplayRole: 0521 if (!d->showBalance() && !index.data(eMyMoney::Model::IdRole).toString().isEmpty()) { 0522 return QLatin1String("---"); 0523 } 0524 break; 0525 case Qt::TextAlignmentRole: 0526 if (!d->showBalance()) { 0527 return QVariant(Qt::AlignHCenter | Qt::AlignTop); 0528 } 0529 break; 0530 default: 0531 break; 0532 } 0533 } 0534 return LedgerSortProxyModel::data(index, role); 0535 }