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 }