File indexing completed on 2024-05-12 16:42:18
0001 /* 0002 SPDX-FileCopyrightText: 2016-2018 Thomas Baumgart <tbaumgart@kde.org> 0003 SPDX-FileCopyrightText: 2017-2018 Łukasz Wojniłowicz <lukasz.wojnilowicz@gmail.com> 0004 SPDX-License-Identifier: GPL-2.0-or-later 0005 */ 0006 0007 #include "ledgermodel.h" 0008 0009 // ---------------------------------------------------------------------------- 0010 // QT Includes 0011 0012 #include <QDebug> 0013 #include <QString> 0014 0015 // ---------------------------------------------------------------------------- 0016 // KDE Includes 0017 0018 #include <KLocalizedString> 0019 0020 // ---------------------------------------------------------------------------- 0021 // Project Includes 0022 0023 #include "ledgerschedule.h" 0024 0025 #include "mymoneyschedule.h" 0026 #include "mymoneysplit.h" 0027 #include "mymoneytransaction.h" 0028 #include "mymoneytransactionfilter.h" 0029 #include "mymoneyfile.h" 0030 #include "mymoneymoney.h" 0031 #include "mymoneyexception.h" 0032 #include "kmymoneyutils.h" 0033 #include "kmymoneysettings.h" 0034 #include "mymoneyenums.h" 0035 #include "modelenums.h" 0036 0037 using namespace eLedgerModel; 0038 using namespace eMyMoney; 0039 0040 class LedgerModelPrivate 0041 { 0042 public: 0043 ~LedgerModelPrivate() { 0044 qDeleteAll(m_ledgerItems); 0045 m_ledgerItems.clear(); 0046 } 0047 MyMoneyTransaction m_lastTransactionStored; 0048 QVector<LedgerItem*> m_ledgerItems; 0049 }; 0050 0051 LedgerModel::LedgerModel(QObject* parent) : 0052 QAbstractTableModel(parent), 0053 d_ptr(new LedgerModelPrivate) 0054 { 0055 } 0056 0057 LedgerModel::~LedgerModel() 0058 { 0059 } 0060 0061 int LedgerModel::rowCount(const QModelIndex& parent) const 0062 { 0063 // since the ledger model is a simple table model, we only 0064 // return the rowCount for the hiddenRootItem. and zero otherwise 0065 if(parent.isValid()) { 0066 return 0; 0067 } 0068 0069 Q_D(const LedgerModel); 0070 return d->m_ledgerItems.count(); 0071 } 0072 0073 int LedgerModel::columnCount(const QModelIndex& parent) const 0074 { 0075 Q_UNUSED(parent); 0076 return (int)Column::LastColumn; 0077 } 0078 0079 Qt::ItemFlags LedgerModel::flags(const QModelIndex& index) const 0080 { 0081 Q_D(const LedgerModel); 0082 Qt::ItemFlags flags; 0083 0084 if(!index.isValid()) 0085 return flags; 0086 if(index.row() < 0 || index.row() >= d->m_ledgerItems.count()) 0087 return flags; 0088 0089 return d->m_ledgerItems[index.row()]->flags(); 0090 } 0091 0092 0093 QVariant LedgerModel::headerData(int section, Qt::Orientation orientation, int role) const 0094 { 0095 if(orientation == Qt::Horizontal && role == Qt::DisplayRole) { 0096 switch(section) { 0097 case (int)Column::Number: 0098 return i18nc("Cheque Number", "No."); 0099 case (int)Column::Date: 0100 return i18n("Date"); 0101 case (int)Column::Security: 0102 return i18n("Security"); 0103 case (int)Column::CostCenter: 0104 return i18n("CC"); 0105 case (int)Column::Detail: 0106 return i18n("Detail"); 0107 case (int)Column::Reconciliation: 0108 return i18n("C"); 0109 case (int)Column::Payment: 0110 return i18nc("Payment made from account", "Payment"); 0111 case (int)Column::Deposit: 0112 return i18nc("Deposit into account", "Deposit"); 0113 case (int)Column::Quantity: 0114 return i18n("Quantity"); 0115 case (int)Column::Price: 0116 return i18n("Price"); 0117 case (int)Column::Amount: 0118 return i18n("Amount"); 0119 case (int)Column::Value: 0120 return i18n("Value"); 0121 case (int)Column::Balance: 0122 return i18n("Balance"); 0123 } 0124 } 0125 else if(orientation == Qt::Vertical && role == Qt::SizeHintRole) { 0126 // as small as possible, so that the delegate has a chance 0127 // to override the information 0128 return QSize(10, 10); 0129 } 0130 return QAbstractItemModel::headerData(section, orientation, role); 0131 } 0132 0133 QVariant LedgerModel::data(const QModelIndex& index, int role) const 0134 { 0135 Q_D(const LedgerModel); 0136 if(!index.isValid()) 0137 return QVariant(); 0138 if(index.row() < 0 || index.row() >= d->m_ledgerItems.count()) 0139 return QVariant(); 0140 0141 QVariant rc; 0142 switch(role) { 0143 case Qt::DisplayRole: 0144 // make sure to never return any displayable text for the dummy entry 0145 if(!d->m_ledgerItems[index.row()]->transactionSplitId().isEmpty()) { 0146 switch(index.column()) { 0147 case (int)Column::Number: 0148 rc = d->m_ledgerItems[index.row()]->transactionNumber(); 0149 break; 0150 case (int)Column::Date: 0151 rc = QLocale().toString(d->m_ledgerItems[index.row()]->postDate(), QLocale::ShortFormat); 0152 break; 0153 case (int)Column::Detail: 0154 rc = d->m_ledgerItems[index.row()]->counterAccount(); 0155 break; 0156 case (int)Column::Reconciliation: 0157 rc = d->m_ledgerItems[index.row()]->reconciliationStateShort(); 0158 break; 0159 case (int)Column::Payment: 0160 rc = d->m_ledgerItems[index.row()]->payment(); 0161 break; 0162 case (int)Column::Deposit: 0163 rc = d->m_ledgerItems[index.row()]->deposit(); 0164 break; 0165 case (int)Column::Amount: 0166 rc = d->m_ledgerItems[index.row()]->signedSharesAmount(); 0167 break; 0168 case (int)Column::Balance: 0169 rc = d->m_ledgerItems[index.row()]->balance(); 0170 break; 0171 } 0172 } 0173 break; 0174 0175 case Qt::TextAlignmentRole: 0176 switch(index.column()) { 0177 case (int)Column::Payment: 0178 case (int)Column::Deposit: 0179 case (int)Column::Amount: 0180 case (int)Column::Balance: 0181 case (int)Column::Value: 0182 rc = QVariant(Qt::AlignRight| Qt::AlignTop); 0183 break; 0184 case (int)Column::Reconciliation: 0185 rc = QVariant(Qt::AlignHCenter | Qt::AlignTop); 0186 break; 0187 default: 0188 rc = QVariant(Qt::AlignLeft | Qt::AlignTop); 0189 break; 0190 } 0191 break; 0192 0193 case Qt::BackgroundColorRole: 0194 if(d->m_ledgerItems[index.row()]->isImported()) { 0195 return KMyMoneySettings::schemeColor(SchemeColor::TransactionImported); 0196 } 0197 break; 0198 0199 case (int)Role::CounterAccount: 0200 rc = d->m_ledgerItems[index.row()]->counterAccount(); 0201 break; 0202 0203 case (int)Role::SplitCount: 0204 rc = d->m_ledgerItems[index.row()]->splitCount(); 0205 break; 0206 0207 case (int)Role::CostCenterId: 0208 rc = d->m_ledgerItems[index.row()]->costCenterId(); 0209 break; 0210 0211 case (int)Role::PostDate: 0212 rc = d->m_ledgerItems[index.row()]->postDate(); 0213 break; 0214 0215 case (int)Role::PayeeName: 0216 rc = d->m_ledgerItems[index.row()]->payeeName(); 0217 break; 0218 0219 case (int)Role::PayeeId: 0220 rc = d->m_ledgerItems[index.row()]->payeeId(); 0221 break; 0222 0223 case (int)Role::AccountId: 0224 rc = d->m_ledgerItems[index.row()]->accountId(); 0225 break; 0226 0227 case Qt::EditRole: 0228 case (int)Role::TransactionSplitId: 0229 rc = d->m_ledgerItems[index.row()]->transactionSplitId(); 0230 break; 0231 0232 case (int)Role::TransactionId: 0233 rc = d->m_ledgerItems[index.row()]->transactionId(); 0234 break; 0235 0236 case (int)Role::Reconciliation: 0237 rc = (int)d->m_ledgerItems[index.row()]->reconciliationState(); 0238 break; 0239 0240 case (int)Role::ReconciliationShort: 0241 rc = d->m_ledgerItems[index.row()]->reconciliationStateShort(); 0242 break; 0243 0244 case (int)Role::ReconciliationLong: 0245 rc = d->m_ledgerItems[index.row()]->reconciliationStateLong(); 0246 break; 0247 0248 case (int)Role::SplitValue: 0249 rc.setValue(d->m_ledgerItems[index.row()]->value()); 0250 break; 0251 0252 case (int)Role::SplitShares: 0253 rc.setValue(d->m_ledgerItems[index.row()]->shares()); 0254 break; 0255 0256 case (int)Role::ShareAmount: 0257 rc.setValue(d->m_ledgerItems[index.row()]->sharesAmount()); 0258 break; 0259 0260 case (int)Role::ShareAmountSuffix: 0261 rc.setValue(d->m_ledgerItems[index.row()]->sharesSuffix()); 0262 break; 0263 0264 case (int)Role::ScheduleId: 0265 { 0266 LedgerSchedule* schedule = 0; 0267 schedule = dynamic_cast<LedgerSchedule*>(d->m_ledgerItems[index.row()]); 0268 if(schedule) { 0269 rc = schedule->scheduleId(); 0270 } 0271 break; 0272 } 0273 0274 case (int)Role::Memo: 0275 case (int)Role::SingleLineMemo: 0276 rc.setValue(d->m_ledgerItems[index.row()]->memo()); 0277 if(role == (int)Role::SingleLineMemo) { 0278 QString txt = rc.toString(); 0279 // remove empty lines 0280 txt.replace("\n\n", "\n"); 0281 // replace '\n' with ", " 0282 txt.replace('\n', ", "); 0283 rc.setValue(txt); 0284 } 0285 break; 0286 0287 case (int)Role::Number: 0288 rc = d->m_ledgerItems[index.row()]->transactionNumber(); 0289 break; 0290 0291 case (int)Role::Erroneous: 0292 rc = d->m_ledgerItems[index.row()]->isErroneous(); 0293 break; 0294 0295 case (int)Role::Import: 0296 rc = d->m_ledgerItems[index.row()]->isImported(); 0297 break; 0298 0299 case (int)Role::CounterAccountId: 0300 rc = d->m_ledgerItems[index.row()]->counterAccountId(); 0301 break; 0302 0303 case (int)Role::TransactionCommodity: 0304 rc = d->m_ledgerItems[index.row()]->transactionCommodity(); 0305 break; 0306 0307 case (int)Role::Transaction: 0308 rc.setValue(d->m_ledgerItems[index.row()]->transaction()); 0309 break; 0310 0311 case (int)Role::Split: 0312 rc.setValue(d->m_ledgerItems[index.row()]->split()); 0313 break; 0314 } 0315 return rc; 0316 } 0317 0318 bool LedgerModel::setData(const QModelIndex& index, const QVariant& value, int role) 0319 { 0320 Q_D(LedgerModel); 0321 if(!index.isValid()) { 0322 return false; 0323 } 0324 if(role == Qt::DisplayRole && index.column() == (int)Column::Balance) { 0325 d->m_ledgerItems[index.row()]->setBalance(value.toString()); 0326 return true; 0327 } 0328 qDebug() << "setData(" << index.row() << index.column() << ")" << value << role; 0329 return QAbstractItemModel::setData(index, value, role); 0330 } 0331 0332 0333 0334 void LedgerModel::unload() 0335 { 0336 Q_D(LedgerModel); 0337 if(rowCount() > 0) { 0338 beginRemoveRows(QModelIndex(), 0, rowCount() - 1); 0339 for(int i = 0; i < rowCount(); ++i) { 0340 delete d->m_ledgerItems[i]; 0341 } 0342 d->m_ledgerItems.clear(); 0343 endRemoveRows(); 0344 } 0345 } 0346 0347 void LedgerModel::addTransactions(const QList< QPair<MyMoneyTransaction, MyMoneySplit> >& list) 0348 { 0349 Q_D(LedgerModel); 0350 if(list.count() > 0) { 0351 beginInsertRows(QModelIndex(), rowCount(), rowCount() + list.count() - 1); 0352 QList< QPair<MyMoneyTransaction, MyMoneySplit> >::const_iterator it; 0353 for(it = list.constBegin(); it != list.constEnd(); ++it) { 0354 d->m_ledgerItems.append(new LedgerTransaction((*it).first, (*it).second)); 0355 } 0356 endInsertRows(); 0357 } 0358 } 0359 0360 void LedgerModel::addTransaction(const LedgerTransaction& t) 0361 { 0362 Q_D(LedgerModel); 0363 beginInsertRows(QModelIndex(), rowCount(), rowCount()); 0364 d->m_ledgerItems.append(new LedgerTransaction(t.transaction(), t.split())); 0365 endInsertRows(); 0366 } 0367 0368 void LedgerModel::addTransaction(const QString& transactionSplitId) 0369 { 0370 Q_D(LedgerModel); 0371 QRegExp transactionSplitIdExp("^(\\w+)-(\\w+)$"); 0372 if(transactionSplitIdExp.exactMatch(transactionSplitId)) { 0373 const QString transactionId = transactionSplitIdExp.cap(1); 0374 const QString splitId = transactionSplitIdExp.cap(2); 0375 if(transactionId != d->m_lastTransactionStored.id()) { 0376 try { 0377 d->m_lastTransactionStored = MyMoneyFile::instance()->transaction(transactionId); 0378 } catch (const MyMoneyException &) { 0379 d->m_lastTransactionStored = MyMoneyTransaction(); 0380 } 0381 } 0382 try { 0383 MyMoneySplit split = d->m_lastTransactionStored.splitById(splitId); 0384 beginInsertRows(QModelIndex(), rowCount(), rowCount()); 0385 d->m_ledgerItems.append(new LedgerTransaction(d->m_lastTransactionStored, split)); 0386 endInsertRows(); 0387 } catch (const MyMoneyException &) { 0388 d->m_lastTransactionStored = MyMoneyTransaction(); 0389 } 0390 } 0391 } 0392 0393 void LedgerModel::addSchedules(const QList<MyMoneySchedule> & list, int previewPeriod) 0394 { 0395 Q_D(LedgerModel); 0396 if(list.count() > 0) { 0397 QVector<LedgerItem*> newList; 0398 0399 // create dummy entries for the scheduled transactions if sorted by postdate 0400 // show scheduled transactions which have a scheduled postdate 0401 // within the next 'previewPeriod' days. In reconciliation mode, the 0402 // previewPeriod starts on the statement date. 0403 QDate endDate = QDate::currentDate().addDays(previewPeriod); 0404 0405 #if 0 0406 if (isReconciliationAccount()) 0407 endDate = reconciliationDate.addDays(previewPeriod); 0408 #endif 0409 0410 QList<MyMoneySchedule>::const_iterator it; 0411 for(it = list.constBegin(); it != list.constEnd(); ++it) { 0412 MyMoneySchedule schedule = *it; 0413 0414 // now create entries for this schedule until the endDate is reached 0415 for (;;) { 0416 if (schedule.isFinished() || schedule.adjustedNextDueDate() > endDate) { 0417 break; 0418 } 0419 0420 MyMoneyTransaction t(schedule.id(), KMyMoneyUtils::scheduledTransaction(schedule)); 0421 // if the transaction is scheduled and overdue, it can't 0422 // certainly be posted in the past. So we take today's date 0423 // as the alternative 0424 if (schedule.isOverdue()) { 0425 t.setPostDate(schedule.adjustedDate(QDate::currentDate(), schedule.weekendOption())); 0426 } else { 0427 t.setPostDate(schedule.adjustedNextDueDate()); 0428 } 0429 0430 // create a model entry for each split of the schedule 0431 foreach (const auto split, t.splits()) 0432 newList.append(new LedgerSchedule(schedule, t, split)); 0433 0434 // keep track of this payment locally (not in the engine) 0435 if (schedule.isOverdue()) { 0436 schedule.setLastPayment(QDate::currentDate()); 0437 } else { 0438 schedule.setLastPayment(schedule.nextDueDate()); 0439 } 0440 0441 // if this is a one time schedule, we can bail out here as we're done 0442 if (schedule.occurrence() == Schedule::Occurrence::Once) 0443 break; 0444 0445 // for all others, we check if the next payment date is still 'in range' 0446 QDate nextDueDate = schedule.nextPayment(schedule.nextDueDate()); 0447 if (nextDueDate.isValid()) { 0448 schedule.setNextDueDate(nextDueDate); 0449 } else { 0450 break; 0451 } 0452 } 0453 } 0454 if(!newList.isEmpty()) { 0455 beginInsertRows(QModelIndex(), rowCount(), rowCount() + newList.count() - 1); 0456 d->m_ledgerItems += newList; 0457 endInsertRows(); 0458 } 0459 } 0460 } 0461 0462 void LedgerModel::load() 0463 { 0464 qDebug() << "Start loading splits"; 0465 // load all transactions and splits into the model 0466 QList<QPair<MyMoneyTransaction, MyMoneySplit> > tList; 0467 MyMoneyTransactionFilter filter; 0468 MyMoneyFile::instance()->transactionList(tList, filter); 0469 addTransactions(tList); 0470 qDebug() << "Loaded" << rowCount() << "elements"; 0471 0472 // load all scheduled transactions and splits into the model 0473 const int splitCount = rowCount(); 0474 QList<MyMoneySchedule> sList = MyMoneyFile::instance()->scheduleList(); 0475 addSchedules(sList, KMyMoneySettings::schedulePreview()); 0476 qDebug() << "Loaded" << rowCount()-splitCount << "elements"; 0477 0478 // create a dummy entry for new transactions 0479 addTransaction(LedgerTransaction::newTransactionEntry()); 0480 0481 qDebug() << "Loaded" << rowCount() << "elements"; 0482 } 0483 0484 void LedgerModel::slotAddTransaction(File::Object objType, const QString& id) 0485 { 0486 if(objType != File::Object::Transaction) { 0487 return; 0488 } 0489 Q_D(LedgerModel); 0490 qDebug() << "Adding transaction" << id; 0491 0492 const auto t = MyMoneyFile::instance()->transaction(id); 0493 0494 beginInsertRows(QModelIndex(), rowCount(), rowCount() + t.splitCount() - 1); 0495 foreach (auto s, t.splits()) 0496 d->m_ledgerItems.append(new LedgerTransaction(t, s)); 0497 endInsertRows(); 0498 0499 // just make sure we're in sync 0500 Q_ASSERT(d->m_ledgerItems.count() == rowCount()); 0501 } 0502 0503 void LedgerModel::slotModifyTransaction(File::Object objType, const QString& id) 0504 { 0505 if(objType != File::Object::Transaction) { 0506 return; 0507 } 0508 0509 Q_D(LedgerModel); 0510 const auto t = MyMoneyFile::instance()->transaction(id); 0511 // get indexes of all existing splits for this transaction 0512 auto list = match(index(0, 0), (int)Role::TransactionId, id, -1); 0513 // get list of splits to be stored 0514 auto splits = t.splits(); 0515 0516 int lastRowUsed = -1; 0517 int firstRowUsed = 99999999; 0518 if(list.count()) { 0519 firstRowUsed = list.first().row(); 0520 lastRowUsed = list.last().row(); 0521 } 0522 0523 qDebug() << "first:" << firstRowUsed << "last:" << lastRowUsed; 0524 0525 while(!list.isEmpty() && !splits.isEmpty()) { 0526 QModelIndex index = list.takeFirst(); 0527 MyMoneySplit split = splits.takeFirst(); 0528 // get rid of the old split and store new split 0529 qDebug() << "Modify split in row:" << index.row() << t.id() << split.id(); 0530 delete d->m_ledgerItems[index.row()]; 0531 d->m_ledgerItems[index.row()] = new LedgerTransaction(t, split); 0532 } 0533 0534 // inform every one else about the changes 0535 if(lastRowUsed != -1) { 0536 qDebug() << "emit dataChanged from" << firstRowUsed << "to" << lastRowUsed; 0537 emit dataChanged(index(firstRowUsed, 0), index(lastRowUsed, columnCount()-1)); 0538 0539 } else { 0540 lastRowUsed = rowCount(); 0541 } 0542 0543 // now check if we need to add more splits ... 0544 if(!splits.isEmpty() && list.isEmpty()) { 0545 beginInsertRows(QModelIndex(), lastRowUsed, lastRowUsed + splits.count() - 1); 0546 d->m_ledgerItems.insert(lastRowUsed, splits.count(), 0); 0547 while(!splits.isEmpty()) { 0548 MyMoneySplit split = splits.takeFirst(); 0549 d->m_ledgerItems[lastRowUsed] = new LedgerTransaction(t, split); 0550 lastRowUsed++; 0551 } 0552 endInsertRows(); 0553 } 0554 0555 // ... or remove some leftovers 0556 if(splits.isEmpty() && !list.isEmpty()) { 0557 firstRowUsed = lastRowUsed - list.count() + 1; 0558 beginRemoveRows(QModelIndex(), firstRowUsed, lastRowUsed); 0559 int count = 0; 0560 while(!list.isEmpty()) { 0561 ++count; 0562 QModelIndex index = list.takeFirst(); 0563 // get rid of the old split and store new split 0564 qDebug() << "Delete split in row:" << index.row() << data(index, (int)Role::TransactionSplitId).toString(); 0565 delete d->m_ledgerItems[index.row()]; 0566 } 0567 d->m_ledgerItems.remove(firstRowUsed, count); 0568 endRemoveRows(); 0569 } 0570 0571 // just make sure we're in sync 0572 Q_ASSERT(d->m_ledgerItems.count() == rowCount()); 0573 } 0574 0575 void LedgerModel::slotRemoveTransaction(File::Object objType, const QString& id) 0576 { 0577 if(objType != File::Object::Transaction) { 0578 return; 0579 } 0580 Q_D(LedgerModel); 0581 0582 QModelIndexList list = match(index(0, 0), (int)Role::TransactionId, id, -1); 0583 0584 if(list.count()) { 0585 const int firstRowUsed = list[0].row(); 0586 beginRemoveRows(QModelIndex(), firstRowUsed, firstRowUsed + list.count() - 1); 0587 for(int row = firstRowUsed; row < firstRowUsed + list.count(); ++row) { 0588 delete d->m_ledgerItems[row]; 0589 } 0590 d->m_ledgerItems.remove(firstRowUsed, list.count()); 0591 endRemoveRows(); 0592 0593 // just make sure we're in sync 0594 Q_ASSERT(d->m_ledgerItems.count() == rowCount()); 0595 } 0596 } 0597 0598 void LedgerModel::slotAddSchedule(File::Object objType, const QString& id) 0599 { 0600 Q_UNUSED(id); 0601 if(objType != File::Object::Schedule) { 0602 return; 0603 } 0604 0605 /// @todo implement LedgerModel::addSchedule 0606 } 0607 0608 void LedgerModel::slotModifySchedule(File::Object objType, const QString& id) 0609 { 0610 Q_UNUSED(id); 0611 if(objType != File::Object::Schedule) { 0612 return; 0613 } 0614 0615 /// @todo implement LedgerModel::modifySchedule 0616 } 0617 0618 void LedgerModel::slotRemoveSchedule(File::Object objType, const QString& id) 0619 { 0620 Q_UNUSED(id); 0621 if(objType != File::Object::Schedule) { 0622 return; 0623 } 0624 0625 /// @todo implement LedgerModel::removeSchedule 0626 } 0627 0628 QString LedgerModel::transactionIdFromTransactionSplitId(const QString& transactionSplitId) const 0629 { 0630 QRegExp transactionSplitIdExp("^(\\w+)-\\w+$"); 0631 if(transactionSplitIdExp.exactMatch(transactionSplitId)) { 0632 return transactionSplitIdExp.cap(1); 0633 } 0634 return QString(); 0635 }