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 }