File indexing completed on 2024-05-19 05:07:27

0001 /*
0002     SPDX-FileCopyrightText: 2019 Thomas Baumgart <tbaumgart@kde.org>
0003     SPDX-License-Identifier: GPL-2.0-or-later
0004 */
0005 
0006 #include "splitmodel.h"
0007 
0008 // ----------------------------------------------------------------------------
0009 // QT Includes
0010 
0011 #include <QStandardItem>
0012 
0013 // ----------------------------------------------------------------------------
0014 // KDE Includes
0015 
0016 #include <KLocalizedString>
0017 
0018 // ----------------------------------------------------------------------------
0019 // Project Includes
0020 
0021 #include "accountsmodel.h"
0022 #include "mymoneyenums.h"
0023 #include "mymoneyexception.h"
0024 #include "mymoneyfile.h"
0025 #include "mymoneymoney.h"
0026 #include "mymoneysecurity.h"
0027 #include "mymoneytransaction.h"
0028 #include "payeesmodel.h"
0029 #include "securitiesmodel.h"
0030 #include "tagsmodel.h"
0031 
0032 struct SplitModel::Private
0033 {
0034     Private(SplitModel* qq)
0035         : q(qq)
0036         , headerData(QHash<Column, QString>({
0037               {Category, i18nc("Split header", "Category")},
0038               {Memo, i18nc("Split header", "Memo")},
0039               {Tags, i18nc("Split header", "Tags")},
0040               {Payment, i18nc("Split header", "Payment")},
0041               {Deposit, i18nc("Split header", "Deposit")},
0042           }))
0043         , currentSplitCount(-1)
0044         , showCurrencies(false)
0045     {
0046     }
0047 
0048     void copyFrom(const SplitModel& right)
0049     {
0050         // suppress emission of dataChanged signal
0051         QSignalBlocker blocker(q);
0052         q->unload();
0053         headerData = right.d->headerData;
0054         const auto rows = right.rowCount();
0055         for (int row = 0; row < rows; ++row) {
0056             const auto idx = right.index(row, 0);
0057             const auto split = right.itemByIndex(idx);
0058             q->appendSplit(split);
0059         }
0060         blocker.unblock();
0061         // send out a combined dataChanged signal
0062         QModelIndex start(q->index(0, 0));
0063         QModelIndex end(q->index(rows - 1, q->columnCount() - 1));
0064         Q_EMIT q->dataChanged(start, end);
0065 
0066         updateItemCount();
0067     }
0068 
0069     int splitCount() const
0070     {
0071         int count = 0;
0072         const auto rows = q->rowCount();
0073         for (auto row = 0; row < rows; ++row) {
0074             const auto idx = q->index(row, 0);
0075             if (!idx.data(eMyMoney::Model::SplitAccountIdRole).toString().isEmpty()) {
0076                 ++count;
0077             }
0078         }
0079         return count;
0080     }
0081 
0082     void updateItemCount()
0083     {
0084         const auto count = splitCount();
0085         if (count != currentSplitCount) {
0086             currentSplitCount = count;
0087             Q_EMIT q->itemCountChanged(currentSplitCount);
0088         }
0089     }
0090 
0091 
0092     QString counterAccount() const
0093     {
0094         // A transaction can have more than 2 splits ...
0095         if(splitCount() > 1) {
0096             return i18n("Split transaction");
0097 
0098             // ... exactly two splits ...
0099         } else if(splitCount() == 1) {
0100             // we have to check which one is filled and which one
0101             // could be an empty (new) split
0102             const auto rows = q->rowCount();
0103             for (auto row = 0; row < rows; ++row) {
0104                 const auto idx = q->index(row, 0);
0105                 const auto accountId = idx.data(eMyMoney::Model::SplitAccountIdRole).toString();
0106                 if (!accountId.isEmpty()) {
0107                     return MyMoneyFile::instance()->accountsModel()->accountIdToHierarchicalName(accountId);
0108                 }
0109             }
0110 
0111             // ... or a single split
0112 #if 0
0113         } else if(!idx.data(eMyMoney::Model::SplitSharesRole).value<MyMoneyMoney>().isZero()) {
0114             return i18n("*** UNASSIGNED ***");
0115 #endif
0116         }
0117         return QString();
0118     }
0119 
0120     int currencyPrecision(const MyMoneySplit& split) const
0121     {
0122         const auto file = MyMoneyFile::instance();
0123         const auto account = file->accountsModel()->itemById(split.accountId());
0124         if (!account.id().isEmpty()) {
0125             try {
0126                 const auto currency = file->currency(account.currencyId());
0127                 if (Q_UNLIKELY(account.accountType() == eMyMoney::Account::Type::Cash)) {
0128                     return MyMoneyMoney::denomToPrec(currency.smallestCashFraction());
0129                 }
0130                 return MyMoneyMoney::denomToPrec(currency.smallestAccountFraction());
0131             } catch (MyMoneyException&) {
0132             }
0133         }
0134         return 2; // the default precision is 2 digits
0135     }
0136 
0137     QString splitCurrencySymbol(const MyMoneySplit& split) const
0138     {
0139         QString currencySymbol;
0140         if (showCurrencies) {
0141             const auto file = MyMoneyFile::instance();
0142             const auto accountIdx = file->accountsModel()->indexById(split.accountId());
0143             if (accountIdx.isValid()) {
0144                 const auto currencyId = accountIdx.data(eMyMoney::Model::AccountCurrencyIdRole).toString();
0145                 const auto securityIdx = file->currenciesModel()->indexById(currencyId);
0146                 currencySymbol = securityIdx.data(eMyMoney::Model::SecuritySymbolRole).toString();
0147             }
0148         }
0149         return currencySymbol;
0150     }
0151 
0152     QString tags(const QStringList& tagIdList) const
0153     {
0154         const auto file = MyMoneyFile::instance();
0155         QStringList splitTagList = tagIdList;
0156         if (!splitTagList.isEmpty()) {
0157             std::transform(splitTagList.begin(), splitTagList.end(), splitTagList.begin(), [file](const QString& tagId) {
0158                 return file->tagsModel()->itemById(tagId).name();
0159             });
0160             return splitTagList.join(", ");
0161         }
0162         return {};
0163     }
0164 
0165     SplitModel* q;
0166     QHash<Column, QString> headerData;
0167     QString transactionCommodity;
0168     int currentSplitCount;
0169     bool showCurrencies;
0170 };
0171 
0172 SplitModel::SplitModel(QObject* parent, QUndoStack* undoStack)
0173     : MyMoneyModel<MyMoneySplit>(parent, QStringLiteral("S"), 4, undoStack)
0174     , d(new Private(this))
0175 {
0176     // new splits in the split model start with 2 instead of 1
0177     // since the first split id is assigned by the transaction
0178     // editor when the transaction is created. (see
0179     // NewTransactionEditor::saveTransaction() )
0180     ++m_nextId;
0181     connect(this, &SplitModel::modelReset, this, [&] { d->updateItemCount(); });
0182     connect(this, &SplitModel::dataChanged, this, &SplitModel::checkForForeignCurrency);
0183 }
0184 
0185 SplitModel::SplitModel(QObject* parent, QUndoStack* undoStack, const SplitModel& right)
0186     : MyMoneyModel<MyMoneySplit>(parent, QStringLiteral("S"), 4, undoStack)
0187     , d(new Private(this))
0188 {
0189     d->copyFrom(right);
0190     connect(this, &SplitModel::dataChanged, this, &SplitModel::checkForForeignCurrency);
0191 }
0192 
0193 SplitModel& SplitModel::operator=(const SplitModel& right)
0194 {
0195     d->copyFrom(right);
0196     return *this;
0197 }
0198 
0199 SplitModel::~SplitModel()
0200 {
0201 }
0202 
0203 int SplitModel::columnCount(const QModelIndex& parent) const
0204 {
0205     Q_UNUSED(parent);
0206     Q_ASSERT(d->headerData.count() == MaxColumns);
0207     return MaxColumns;
0208 }
0209 
0210 QString SplitModel::newSplitId()
0211 {
0212     return QStringLiteral("New-ID");
0213 }
0214 
0215 bool SplitModel::isNewSplitId(const QString& id)
0216 {
0217     return id.compare(newSplitId()) == 0;
0218 }
0219 
0220 QVariant SplitModel::headerData(int section, Qt::Orientation orientation, int role) const
0221 {
0222     if (orientation == Qt::Horizontal) {
0223         switch (role) {
0224         case Qt::DisplayRole:
0225         case eMyMoney::Model::LongDisplayRole:
0226             return d->headerData.value(static_cast<Column>(section));
0227 
0228         case Qt::SizeHintRole:
0229             return QSize(20, 20);
0230         }
0231         return {};
0232     }
0233     if (orientation == Qt::Vertical && role == Qt::SizeHintRole) {
0234         return QSize(10, 10);
0235     }
0236 
0237     return MyMoneyModelBase::headerData(section, orientation, role);
0238 }
0239 
0240 Qt::ItemFlags SplitModel::flags(const QModelIndex& index) const
0241 {
0242     if (index.isValid()) {
0243         return (Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable);
0244     }
0245     return Qt::NoItemFlags;
0246 }
0247 
0248 QVariant SplitModel::data(const QModelIndex& idx, int role) const
0249 {
0250     if (!idx.isValid())
0251         return QVariant();
0252     if (idx.row() < 0 || idx.row() >= rowCount(idx.parent()))
0253         return QVariant();
0254 
0255     const MyMoneySplit& split = static_cast<TreeItem<MyMoneySplit>*>(idx.internalPointer())->constDataRef();
0256     const auto file = MyMoneyFile::instance();
0257     switch(role) {
0258     case Qt::DisplayRole:
0259     case Qt::EditRole:
0260         switch(idx.column()) {
0261         case Column::Category:
0262             return file->accountsModel()->accountIdToHierarchicalName(split.accountId());
0263 
0264         case Column::Memo:
0265         {
0266             QString rc(split.memo());
0267             // remove empty lines
0268             rc.replace(QStringLiteral("\n\n"), QStringLiteral("\n"));
0269             // replace '\n' with ", "
0270             rc.replace(QStringLiteral("\n"), QStringLiteral(", "));
0271             return rc;
0272         }
0273 
0274         case Column::Payment:
0275             if (!split.id().isEmpty()) {
0276                 const auto value = split.shares();
0277                 if (value.isPositive()) {
0278                     return value.formatMoney(d->splitCurrencySymbol(split), d->currencyPrecision(split));
0279                 }
0280             }
0281             return {};
0282 
0283         case Column::Deposit:
0284             if (!split.id().isEmpty()) {
0285                 const auto value = split.shares();
0286                 if (value.isNegative() || value.isZero()) {
0287                     return (-value).formatMoney(d->splitCurrencySymbol(split), d->currencyPrecision(split));
0288                 }
0289             }
0290             return {};
0291 
0292         case Tags:
0293             return d->tags(split.tagIdList());
0294 
0295         default:
0296             break;
0297         }
0298         break;
0299 
0300     case Qt::TextAlignmentRole:
0301         switch (idx.column()) {
0302         case Payment:
0303         case Deposit:
0304             return QVariant(Qt::AlignRight | Qt::AlignTop);
0305 
0306         default:
0307             break;
0308         }
0309         return QVariant(Qt::AlignLeft | Qt::AlignTop);
0310 
0311     case eMyMoney::Model::IdRole:
0312         return split.id();
0313 
0314     case eMyMoney::Model::SplitSingleLineMemoRole:
0315     case eMyMoney::Model::SplitMemoRole: {
0316         QString rc(split.memo());
0317         if (role == eMyMoney::Model::SplitSingleLineMemoRole) {
0318             // remove empty lines
0319             rc.replace(QStringLiteral("\n\n"), QStringLiteral("\n"));
0320             // replace '\n' with ", "
0321             rc.replace(QStringLiteral("\n"), QStringLiteral(", "));
0322         }
0323         return rc;
0324     }
0325 
0326     case eMyMoney::Model::SplitAccountIdRole:
0327         return split.accountId();
0328 
0329     case eMyMoney::Model::AccountFullNameRole:
0330         return file->accountsModel()->accountIdToHierarchicalName(split.accountId());
0331 
0332     case eMyMoney::Model::SplitSharesRole:
0333         return QVariant::fromValue<MyMoneyMoney>(split.shares());
0334 
0335     case eMyMoney::Model::SplitValueRole:
0336         return QVariant::fromValue<MyMoneyMoney>(split.value());
0337 
0338     case eMyMoney::Model::SplitCostCenterIdRole:
0339         return split.costCenterId();
0340 
0341     case eMyMoney::Model::SplitNumberRole:
0342         return split.number();
0343 
0344     case eMyMoney::Model::SplitPayeeIdRole:
0345         return split.payeeId();
0346 
0347     case eMyMoney::Model::SplitPayeeRole:
0348         return file->payeesModel()->itemById(split.payeeId()).name();
0349 
0350     case eMyMoney::Model::SplitTagIdRole:
0351         return QVariant::fromValue<QStringList>(split.tagIdList());
0352 
0353     case eMyMoney::Model::TransactionCounterAccountRole:
0354         break;
0355 
0356     case eMyMoney::Model::SplitIsNewRole:
0357         return split.id().isEmpty() || split.id().endsWith(QLatin1Char('-'));
0358 
0359     case eMyMoney::Model::SplitActionRole:
0360         return split.action();
0361 
0362     default:
0363         break;
0364     }
0365     return {};
0366 }
0367 
0368 bool SplitModel::setData(const QModelIndex& idx, const QVariant& value, int role)
0369 {
0370     if(!idx.isValid()) {
0371         return false;
0372     }
0373     if (idx.row() < 0 || idx.row() >= rowCount(idx.parent())) {
0374         return false;
0375     }
0376 
0377     const auto startIdx = idx.model()->index(idx.row(), 0);
0378     const auto endIdx = idx.model()->index(idx.row(), idx.model()->columnCount()-1);
0379     MyMoneySplit& split = static_cast<TreeItem<MyMoneySplit>*>(idx.internalPointer())->dataRef();
0380 
0381     // in case we modify the data of a new split, we need to setup an id
0382     // this will be updated once we add the split to the transaction
0383     // we do this only when the category is set since this is a required
0384     // field
0385     if ((role == eMyMoney::Model::SplitAccountIdRole) && split.id().isEmpty()) {
0386         split = MyMoneySplit(newSplitId(), split);
0387     }
0388 
0389     switch(role) {
0390     case Qt::DisplayRole:
0391     case Qt::EditRole:
0392         break;
0393 
0394     case eMyMoney::Model::SplitNumberRole:
0395         split.setNumber(value.toString());
0396         Q_EMIT dataChanged(startIdx, endIdx);
0397         return true;
0398 
0399     case eMyMoney::Model::SplitMemoRole:
0400         split.setMemo(value.toString());
0401         Q_EMIT dataChanged(startIdx, endIdx);
0402         return true;
0403 
0404     case eMyMoney::Model::SplitAccountIdRole:
0405         split.setAccountId(value.toString());
0406         Q_EMIT dataChanged(startIdx, endIdx);
0407         return true;
0408 
0409     case eMyMoney::Model::SplitCostCenterIdRole:
0410         split.setCostCenterId(value.toString());
0411         Q_EMIT dataChanged(startIdx, endIdx);
0412         return true;
0413 
0414     case eMyMoney::Model::SplitSharesRole:
0415         split.setShares(value.value<MyMoneyMoney>());
0416         Q_EMIT dataChanged(startIdx, endIdx);
0417         return true;
0418 
0419     case eMyMoney::Model::SplitValueRole:
0420         split.setValue(value.value<MyMoneyMoney>());
0421         Q_EMIT dataChanged(startIdx, endIdx);
0422         return true;
0423 
0424     case eMyMoney::Model::SplitPayeeIdRole:
0425         split.setPayeeId(value.toString());
0426         Q_EMIT dataChanged(startIdx, endIdx);
0427         return true;
0428 
0429     case eMyMoney::Model::SplitTagIdRole:
0430         split.setTagIdList(value.toStringList());
0431         Q_EMIT dataChanged(startIdx, endIdx);
0432         return true;
0433 
0434     case eMyMoney::Model::SplitBankIdRole:
0435         split.setBankID(value.toString());
0436         Q_EMIT dataChanged(startIdx, endIdx);
0437         return true;
0438 
0439     case eMyMoney::Model::SplitActionRole:
0440         split.setAction(value.toString());
0441         Q_EMIT dataChanged(startIdx, endIdx);
0442         return true;
0443 
0444     default:
0445         break;
0446     }
0447     return QAbstractItemModel::setData(idx, value, role);
0448 }
0449 
0450 void SplitModel::appendSplit(const MyMoneySplit& split)
0451 {
0452     doAddItem(split);
0453 }
0454 
0455 void SplitModel::appendEmptySplit()
0456 {
0457     const QModelIndexList list = match(index(0, 0), eMyMoney::Model::IdRole, QString(), -1, Qt::MatchExactly);
0458     if(list.isEmpty()) {
0459         doAddItem(MyMoneySplit());
0460     }
0461 }
0462 
0463 void SplitModel::removeEmptySplit()
0464 {
0465     const QModelIndexList list = match(index(0, 0), eMyMoney::Model::IdRole, QString(), -1, Qt::MatchExactly);
0466     if(!list.isEmpty()) {
0467         removeRow(list.first().row(), list.first().parent());
0468     }
0469 }
0470 
0471 QModelIndex SplitModel::emptySplit() const
0472 {
0473     const QModelIndexList list = match(index(0, 0), eMyMoney::Model::IdRole, QString(), -1, Qt::MatchExactly);
0474     if (!list.isEmpty()) {
0475         return list.first();
0476     }
0477     return {};
0478 }
0479 
0480 void SplitModel::doAddItem(const MyMoneySplit& item, const QModelIndex& parentIdx)
0481 {
0482     MyMoneyModel::doAddItem(item, parentIdx);
0483     d->updateItemCount();
0484 }
0485 
0486 void SplitModel::doRemoveItem(const MyMoneySplit& before)
0487 {
0488     MyMoneyModel::doRemoveItem(before);
0489     d->updateItemCount();
0490 }
0491 
0492 MyMoneyMoney SplitModel::valueSum() const
0493 {
0494     MyMoneyMoney sum;
0495     const auto rows = rowCount();
0496     for (int row = 0; row < rows; ++row) {
0497         const auto idx = index(row, 0);
0498         sum += idx.data(eMyMoney::Model::SplitValueRole).value<MyMoneyMoney>();
0499     }
0500     return sum;
0501 }
0502 
0503 void SplitModel::addSplitsToTransaction(MyMoneyTransaction& t) const
0504 {
0505     // now update and add what we have in the model
0506     const auto rows = rowCount();
0507     for (int row = 0; row < rows; ++row) {
0508         const auto idx = index(row, 0);
0509         MyMoneySplit s;
0510         const QString splitId = idx.data(eMyMoney::Model::IdRole).toString();
0511         // Extract the split from the transaction if
0512         // it already exists. Otherwise it remains
0513         // an empty split and will be added later.
0514         try {
0515             s = t.splitById(splitId);
0516         } catch (const MyMoneyException&) {
0517         }
0518         s.setNumber(idx.data(eMyMoney::Model::SplitNumberRole).toString());
0519         s.setMemo(idx.data(eMyMoney::Model::SplitMemoRole).toString());
0520         s.setAccountId(idx.data(eMyMoney::Model::SplitAccountIdRole).toString());
0521         s.setShares(idx.data(eMyMoney::Model::SplitSharesRole).value<MyMoneyMoney>());
0522         s.setValue(idx.data(eMyMoney::Model::SplitValueRole).value<MyMoneyMoney>());
0523         s.setCostCenterId(idx.data(eMyMoney::Model::SplitCostCenterIdRole).toString());
0524         s.setPayeeId(idx.data(eMyMoney::Model::SplitPayeeIdRole).toString());
0525         s.setTagIdList(idx.data(eMyMoney::Model::SplitTagIdRole).toStringList());
0526 
0527         // update the price information. setting the price to zero
0528         // will cause MyMoneySplit::price to recalculate the price
0529         // based on value and shares of the split
0530         s.setPrice(MyMoneyMoney());
0531         s.setPrice(s.price());
0532 
0533         if (s.id().isEmpty()) {
0534             t.addSplit(s);
0535         } else {
0536             t.modifySplit(s);
0537         }
0538     }
0539 }
0540 
0541 QList<MyMoneySplit> SplitModel::splitList() const
0542 {
0543     QList<MyMoneySplit> splits;
0544     const auto rows = rowCount();
0545     for (int row = 0; row < rows; ++row) {
0546         const auto idx = index(row, 0);
0547         MyMoneySplit s;
0548         s.setNumber(idx.data(eMyMoney::Model::SplitNumberRole).toString());
0549         s.setMemo(idx.data(eMyMoney::Model::SplitMemoRole).toString());
0550         s.setAccountId(idx.data(eMyMoney::Model::SplitAccountIdRole).toString());
0551         s.setShares(idx.data(eMyMoney::Model::SplitSharesRole).value<MyMoneyMoney>());
0552         s.setValue(idx.data(eMyMoney::Model::SplitValueRole).value<MyMoneyMoney>());
0553         s.setCostCenterId(idx.data(eMyMoney::Model::SplitCostCenterIdRole).toString());
0554         s.setPayeeId(idx.data(eMyMoney::Model::SplitPayeeIdRole).toString());
0555         s.setTagIdList(idx.data(eMyMoney::Model::SplitTagIdRole).toStringList());
0556 
0557         // update the price information. setting the price to zero
0558         // will cause MyMoneySplit::price to recalculate the price
0559         // based on value and shares of the split
0560         s.setPrice(MyMoneyMoney());
0561         s.setPrice(s.price());
0562 
0563         splits.append(s);
0564     }
0565     return splits;
0566 }
0567 
0568 void SplitModel::setTransactionCommodity(const QString& commodity)
0569 {
0570     d->transactionCommodity = commodity;
0571     checkForForeignCurrency();
0572 }
0573 
0574 void SplitModel::checkForForeignCurrency() const
0575 {
0576     d->showCurrencies = false;
0577     const auto file = MyMoneyFile::instance();
0578     const auto rows = rowCount();
0579     for (int row = 0; row < rows; ++row) {
0580         const auto idx = index(row, 0);
0581         const auto accountId = idx.data(eMyMoney::Model::SplitAccountIdRole).toString();
0582         if (!accountId.isEmpty()) {
0583             const auto accountIdx = file->accountsModel()->indexById(accountId);
0584             if (accountIdx.data(eMyMoney::Model::AccountCurrencyIdRole).toString() != d->transactionCommodity) {
0585                 d->showCurrencies = true;
0586                 break;
0587             }
0588         }
0589     }
0590 }
0591 
0592 bool SplitModel::hasMultiCurrencySplits() const
0593 {
0594     return d->showCurrencies;
0595 }
0596 
0597 void SplitModel::resetAllSplitIds()
0598 {
0599     const auto startIdx = index(0, 0);
0600     const auto endIdx = index(rowCount() - 1, columnCount() - 1);
0601     for (int row = 0; row <= endIdx.row(); ++row) {
0602         const auto idx = index(row, 0);
0603         MyMoneySplit& split = static_cast<TreeItem<MyMoneySplit>*>(idx.internalPointer())->dataRef();
0604         split = MyMoneySplit(newSplitId(), split);
0605     }
0606     Q_EMIT dataChanged(startIdx, endIdx);
0607 }