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 }