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 }