File indexing completed on 2024-05-19 05:06:58

0001 /*
0002     SPDX-FileCopyrightText: 2022 Thomas Baumgart <tbaumgart@kde.org>
0003     SPDX-License-Identifier: GPL-2.0-or-later
0004 */
0005 
0006 #include "ledgersortproxymodel.h"
0007 #include "ledgersortproxymodel_p.h"
0008 
0009 // ----------------------------------------------------------------------------
0010 // QT Includes
0011 
0012 #include <QDate>
0013 
0014 // ----------------------------------------------------------------------------
0015 // KDE Includes
0016 
0017 // ----------------------------------------------------------------------------
0018 // Project Includes
0019 
0020 #include "accountsmodel.h"
0021 #include "journalmodel.h"
0022 #include "ledgerviewsettings.h"
0023 #include "mymoneyenums.h"
0024 #include "mymoneyfile.h"
0025 #include "specialdatesmodel.h"
0026 
0027 using namespace eMyMoney;
0028 
0029 LedgerSortProxyModel::LedgerSortProxyModel(LedgerSortProxyModelPrivate* dd, QObject* parent)
0030     : QSortFilterProxyModel(parent)
0031     , d_ptr(dd)
0032 {
0033     setDynamicSortFilter(false); // no automatic sorting
0034 }
0035 
0036 LedgerSortProxyModel::~LedgerSortProxyModel()
0037 {
0038 }
0039 
0040 void LedgerSortProxyModel::setSourceModel(QAbstractItemModel* model)
0041 {
0042     if (sourceModel()) {
0043         disconnect(model, &QAbstractItemModel::rowsInserted, this, &LedgerSortProxyModel::sortOnIdle);
0044     }
0045     if (model) {
0046         connect(model, &QAbstractItemModel::rowsInserted, this, &LedgerSortProxyModel::sortOnIdle);
0047     }
0048     QSortFilterProxyModel::setSourceModel(model);
0049 }
0050 
0051 bool LedgerSortProxyModel::lessThan(const QModelIndex& left, const QModelIndex& right) const
0052 {
0053     Q_D(const LedgerSortProxyModel);
0054 
0055     // make sure that the dummy transaction is shown last in any case
0056     if (left.data(eMyMoney::Model::IdRole).toString().isEmpty()) {
0057         return false;
0058 
0059     } else if (right.data(eMyMoney::Model::IdRole).toString().isEmpty()) {
0060         return true;
0061     }
0062 
0063     // make sure that the online balance is the last entry of a day
0064     // and the date headers are the first
0065     for (const auto sortOrderItem : d->ledgerSortOrder) {
0066         //                   SortOrder of item
0067         //                ascending    descending
0068         // trueValue        true         false
0069         // falseValue       false        true
0070         const auto trueValue = sortOrderItem.lessThanIs(true);
0071         const auto falseValue = sortOrderItem.lessThanIs(false);
0072 
0073         switch (sortOrderItem.sortRole) {
0074         case eMyMoney::Model::TransactionPostDateRole:
0075         case eMyMoney::Model::TransactionEntryDateRole:
0076         case eMyMoney::Model::SplitReconcileDateRole: {
0077             const auto leftDate = left.data(sortOrderItem.sortRole).toDate();
0078             const auto rightDate = right.data(sortOrderItem.sortRole).toDate();
0079 
0080             // in case of sorting by reconciliation date, the date
0081             // may be invalid and we have to react a bit different.
0082             if ((leftDate == rightDate) && leftDate.isValid()) {
0083                 const auto leftModel = left.data(eMyMoney::Model::BaseModelRole);
0084                 const auto rightModel = right.data(eMyMoney::Model::BaseModelRole);
0085                 if (leftModel != rightModel) {
0086                     // schedules will always be presented last on the same day
0087                     // before that the online balance is shown
0088                     // before that the reconciliation records are displayed
0089                     // special date records are shown on top
0090                     // account names are shown on top
0091                     if (d->isSchedulesJournalModel(left)) {
0092                         return falseValue;
0093                     } else if (d->isSchedulesJournalModel(right)) {
0094                         return trueValue;
0095                     } else if (d->isOnlineBalanceModel(left)) {
0096                         return falseValue;
0097                     } else if (d->isOnlineBalanceModel(right)) {
0098                         return trueValue;
0099                     } else if (d->isSpecialDatesModel(left)) {
0100                         return trueValue;
0101                     } else if (d->isSpecialDatesModel(right)) {
0102                         return falseValue;
0103                     } else if (d->isReconciliationModel(left)) {
0104                         return falseValue;
0105                     } else if (d->isReconciliationModel(right)) {
0106                         return trueValue;
0107                     }
0108                     // if we get here, both are transaction entries
0109                 }
0110 
0111                 // same date and same model means that the next item
0112                 // in the sortOrderList needs to be evaluated
0113                 break;
0114 
0115             } else if (sortOrderItem.sortRole == eMyMoney::Model::SplitReconcileDateRole) {
0116                 // special handling for reconciliation date because it
0117                 // might be invalid and has to be sorted to the end in
0118                 // this case
0119                 if (leftDate.isValid() && !rightDate.isValid()) {
0120                     return trueValue;
0121                 }
0122                 if (!leftDate.isValid() && rightDate.isValid()) {
0123                     return falseValue;
0124                 }
0125                 if (leftDate.isValid()) {
0126                     // actually, both dates are valid here but testing
0127                     // one for validity is enough
0128                     return sortOrderItem.lessThanIs(leftDate < rightDate);
0129                 }
0130 
0131                 // in case both are invalid, we continue with the
0132                 // next item in the sortOrderList
0133                 break;
0134             }
0135 
0136             return sortOrderItem.lessThanIs(leftDate < rightDate);
0137         }
0138         case eMyMoney::Model::SplitSharesRole: {
0139             const auto lValue = left.data(sortOrderItem.sortRole).value<MyMoneyMoney>();
0140             const auto rValue = right.data(sortOrderItem.sortRole).value<MyMoneyMoney>();
0141             if (lValue != rValue) {
0142                 return sortOrderItem.lessThanIs(lValue < rValue);
0143             }
0144             break;
0145         }
0146         case eMyMoney::Model::SplitPayeeRole:
0147         case eMyMoney::Model::TransactionCounterAccountRole:
0148         case eMyMoney::Model::SplitSharesSuffixRole:
0149         case eMyMoney::Model::IdRole: {
0150             const auto lValue = left.data(sortOrderItem.sortRole).toString();
0151             const auto rValue = right.data(sortOrderItem.sortRole).toString();
0152             if (lValue != rValue) {
0153                 return sortOrderItem.lessThanIs(QString::localeAwareCompare(lValue, rValue) == -1);
0154             }
0155             break;
0156         }
0157 
0158         case eMyMoney::Model::JournalSplitSecurityNameRole: {
0159             const auto leftSecurity = left.data(sortOrderItem.sortRole).toString();
0160             const auto rightSecurity = right.data(sortOrderItem.sortRole).toString();
0161             if (leftSecurity == rightSecurity) {
0162                 const auto leftModel = left.data(eMyMoney::Model::BaseModelRole);
0163                 const auto rightModel = right.data(eMyMoney::Model::BaseModelRole);
0164                 if (leftModel != rightModel) {
0165                     if (d->isSecurityAccountNameModel(left)) {
0166                         return trueValue;
0167                     } else if (d->isSecurityAccountNameModel(right)) {
0168                         return falseValue;
0169                     }
0170                     // if we get here, both are transaction entries
0171                 }
0172                 // same security and same model means that the next item
0173                 // in the sortOrderList needs to be evaluated
0174                 break;
0175             }
0176             return sortOrderItem.lessThanIs(leftSecurity < rightSecurity);
0177         }
0178 
0179         case eMyMoney::Model::SplitReconcileFlagRole: {
0180             const auto lValue = left.data(sortOrderItem.sortRole).toInt();
0181             const auto rValue = right.data(sortOrderItem.sortRole).toInt();
0182             if (lValue != rValue) {
0183                 return sortOrderItem.lessThanIs(lValue < rValue);
0184             }
0185             break;
0186         }
0187 
0188         case eMyMoney::Model::SplitNumberRole: {
0189             const auto lValue = left.data(sortOrderItem.sortRole).toString();
0190             const auto rValue = right.data(sortOrderItem.sortRole).toString();
0191             if (lValue != rValue) {
0192                 // convert both values to numbers
0193                 bool ok1(false);
0194                 bool ok2(false);
0195                 const auto n1 = lValue.toULongLong(&ok1);
0196                 const auto n2 = rValue.toULongLong(&ok2);
0197                 // the following four cases exist:
0198                 // a) both are converted correct
0199                 //    compare them directly
0200                 // b) n1 is numeric, n2 is not
0201                 //    numbers come first, so trueValue
0202                 // c) n1 is not numeric, n2 is
0203                 //    numbers come first, so falseValue
0204                 // d) both are non numbers
0205                 //    compare using localeAwareCompare
0206                 if (ok1 && ok2) { // case a)
0207                     return sortOrderItem.lessThanIs(n1 < n2);
0208 
0209                 } else if (ok1 && !ok2) { // case b)
0210                     return trueValue;
0211 
0212                 } else if (!ok1 && ok2) { // case c)
0213                     return falseValue;
0214 
0215                 } else { // case d)
0216                     return sortOrderItem.lessThanIs(QString::localeAwareCompare(lValue, rValue) == -1);
0217                 }
0218             }
0219             break;
0220         }
0221         default:
0222             break;
0223         }
0224     }
0225 
0226     // same everything, let the id decide
0227     return left.data(eMyMoney::Model::IdRole).toString() < right.data(eMyMoney::Model::IdRole).toString();
0228 }
0229 
0230 bool LedgerSortProxyModel::filterAcceptsRow(int source_row, const QModelIndex& source_parent) const
0231 {
0232     Q_D(const LedgerSortProxyModel);
0233 
0234     const auto idx = sourceModel()->index(source_row, 0, source_parent);
0235     // only check the start date if it's not the new transaction placeholder
0236     if (!idx.data(eMyMoney::Model::IdRole).toString().isEmpty()) {
0237         if (d->firstVisiblePostDate.isValid() && d->firstVisiblePostDate > idx.data(eMyMoney::Model::TransactionPostDateRole).toDate()) {
0238             return false;
0239         }
0240     }
0241 
0242     // in case it's a special date entry or reconciliation entry, we accept it
0243     if (d->isSpecialDatesModel(idx) || d->isReconciliationModel(idx)) {
0244         return true;
0245     }
0246 
0247     // now do the filtering
0248 
0249     if (d->hideReconciledTransactions
0250         && idx.data(eMyMoney::Model::SplitReconcileFlagRole).value<eMyMoney::Split::State>() >= eMyMoney::Split::State::Reconciled) {
0251         return false;
0252     }
0253 
0254     return true;
0255 }
0256 
0257 void LedgerSortProxyModel::setHideTransactionsBefore(const QDate& date)
0258 {
0259     Q_D(LedgerSortProxyModel);
0260     if (d->firstVisiblePostDate != date) {
0261         d->firstVisiblePostDate = date;
0262         invalidateFilter();
0263     }
0264 }
0265 
0266 void LedgerSortProxyModel::setHideReconciledTransactions(bool hide)
0267 {
0268     Q_D(LedgerSortProxyModel);
0269     if (d->hideReconciledTransactions != hide) {
0270         d->hideReconciledTransactions = hide;
0271         invalidateFilter();
0272     }
0273 }
0274 
0275 void LedgerSortProxyModel::setSortingEnabled(bool enable)
0276 {
0277     Q_D(LedgerSortProxyModel);
0278     if (d->sortEnabled != enable) {
0279         d->sortEnabled = enable;
0280         if (enable && d->sortPending) {
0281             doSort();
0282         }
0283     }
0284 }
0285 
0286 bool LedgerSortProxyModel::inSorting() const
0287 {
0288     Q_D(const LedgerSortProxyModel);
0289     return d->sorting;
0290 }
0291 
0292 void LedgerSortProxyModel::sort(int column, Qt::SortOrder order)
0293 {
0294     Q_UNUSED(column)
0295     Q_UNUSED(order)
0296     Q_D(LedgerSortProxyModel);
0297 
0298     // call the actual sorting only if we really need to sort
0299     if (sortRole() >= 0) {
0300         if (d->sortEnabled) {
0301             // LedgerSortProxyModel::lessThan takes care of the sort
0302             // order and is based on a general ascending order
0303             d->sorting = true;
0304             QSortFilterProxyModel::sort(0, Qt::AscendingOrder);
0305             d->sorting = false;
0306             d->sortPending = false;
0307         } else {
0308             d->sortPending = true;
0309         }
0310         d->sortPostponed = false;
0311     }
0312 }
0313 
0314 void LedgerSortProxyModel::sortOnIdle()
0315 {
0316     Q_D(LedgerSortProxyModel);
0317     if (!d->sortPostponed) {
0318         d->sortPostponed = true;
0319         // in case a recalc operation is pending, we turn it off
0320         // since we need to sort first. Once sorting is done,
0321         // the recalc will be triggered again
0322         d->balanceCalculationPending = false;
0323         QMetaObject::invokeMethod(this, &LedgerSortProxyModel::doSortOnIdle, Qt::QueuedConnection);
0324     }
0325 }
0326 
0327 void LedgerSortProxyModel::doSort()
0328 {
0329     sort(0, Qt::AscendingOrder);
0330 }
0331 
0332 void LedgerSortProxyModel::doSortOnIdle()
0333 {
0334     Q_D(LedgerSortProxyModel);
0335     if (d->sortPostponed) {
0336         doSort();
0337     }
0338 }
0339 
0340 void LedgerSortProxyModel::setLedgerSortOrder(LedgerSortOrder sortOrder)
0341 {
0342     Q_D(LedgerSortProxyModel);
0343     if (sortOrder != d->ledgerSortOrder) {
0344         // the next line will turn on sorting for this model
0345         // but is otherwise not used (see lessThan())
0346         d->ledgerSortOrder = sortOrder;
0347         setSortRole(eMyMoney::Model::TransactionPostDateRole);
0348         doSort();
0349     }
0350 }
0351 
0352 LedgerSortOrder LedgerSortProxyModel::ledgerSortOrder() const
0353 {
0354     Q_D(const LedgerSortProxyModel);
0355     return d->ledgerSortOrder;
0356 }
0357 
0358 void LedgerSortProxyModel::dumpSourceModel() const
0359 {
0360 #if 0
0361     qDebug() << objectName() << "Dump on model" << sourceModel()->metaObject()->className() << sourceModel()->objectName();
0362     int row = 0;
0363     for (;; ++row) {
0364         const auto idx = sourceModel()->index(row, 0);
0365         if (idx.isValid()) {
0366             qDebug() << "Row" << row << "ID" << idx.data(eMyMoney::Model::IdRole).toString() << idx.data(eMyMoney::Model::JournalSplitAccountIdRole).toString();
0367         } else {
0368             break;
0369         }
0370     }
0371 
0372 #if 0
0373     const auto qsfpm = qobject_cast<LedgerSortProxyModel*>(sourceModel());
0374     if (qsfpm) {
0375         qsfpm->dumpSourceModel();
0376     }
0377 #endif
0378 #endif
0379 }