File indexing completed on 2024-05-19 05:08:28
0001 /* 0002 SPDX-FileCopyrightText: 2015-2020 Thomas Baumgart <tbaumgart@kde.org> 0003 SPDX-License-Identifier: GPL-2.0-or-later 0004 */ 0005 0006 #include "splitdialog.h" 0007 0008 // ---------------------------------------------------------------------------- 0009 // QT Includes 0010 0011 #include <QDebug> 0012 #include <QHeaderView> 0013 #include <QPointer> 0014 0015 // ---------------------------------------------------------------------------- 0016 // KDE Includes 0017 0018 #include <KColorScheme> 0019 #include <KConfigGroup> 0020 #include <KLocalizedString> 0021 #include <KSharedConfig> 0022 0023 // ---------------------------------------------------------------------------- 0024 // Project Includes 0025 0026 #include "icons.h" 0027 #include "mymoneysecurity.h" 0028 #include "splitadjustdialog.h" 0029 #include "splitmodel.h" 0030 #include "ui_splitdialog.h" 0031 0032 using namespace Icons; 0033 0034 class SplitDialog::Private 0035 { 0036 Q_DISABLE_COPY_MOVE(Private) 0037 0038 public: 0039 Private(SplitDialog* p) 0040 : parent(p) 0041 , ui(new Ui_SplitDialog) 0042 , transactionEditor(nullptr) 0043 , splitModel(nullptr) 0044 , fraction(100) 0045 , readOnly(false) 0046 { 0047 } 0048 0049 ~Private() 0050 { 0051 delete ui; 0052 } 0053 0054 void deleteSplits(QModelIndexList indexList); 0055 void blockEditorStart(bool blocked); 0056 void blockImmediateEditor(); 0057 void selectRow(int row); 0058 0059 SplitDialog* parent; 0060 Ui_SplitDialog* ui; 0061 0062 /** 0063 * The parent transaction editor which opened the split editor 0064 */ 0065 QWidget* transactionEditor; 0066 0067 SplitModel* splitModel; 0068 0069 /** 0070 * The fraction of the account for which this split editor was opened 0071 */ 0072 int fraction; 0073 0074 MyMoneyMoney transactionTotal; 0075 MyMoneyMoney splitsTotal; 0076 MyMoneyMoney inversionFactor; 0077 QString transactionPayeeId; 0078 QString commoditySymbol; 0079 bool readOnly; 0080 }; 0081 0082 static const int SumRow = 0; 0083 static const int DiffRow = 1; 0084 static const int AmountRow = 2; 0085 static const int HeaderCol = 0; 0086 static const int ValueCol = 1; 0087 static const int SummaryRows = 3; 0088 static const int SummaryCols = 2; 0089 0090 void SplitDialog::Private::deleteSplits(QModelIndexList indexList) 0091 { 0092 if (indexList.isEmpty()) { 0093 return; 0094 } 0095 0096 // remove from the end so that the row information stays 0097 // consistent and is not changed due to index changes 0098 QMap<int, int> sortedList; 0099 for (const auto& index : indexList) { 0100 sortedList[index.row()] = index.row(); 0101 } 0102 0103 blockEditorStart(true); 0104 const auto model = ui->splitView->model(); 0105 QMap<int, int>::const_iterator it = sortedList.constEnd(); 0106 do { 0107 --it; 0108 const auto idx = model->index(*it, 0); 0109 const auto id = idx.data(eMyMoney::Model::IdRole).toString(); 0110 if (!(id.isEmpty() || id.endsWith('-'))) { 0111 model->removeRow(*it); 0112 } 0113 } while (it != sortedList.constBegin()); 0114 blockEditorStart(false); 0115 } 0116 0117 void SplitDialog::Private::blockEditorStart(bool blocked) 0118 { 0119 ui->splitView->blockEditorStart(blocked); 0120 } 0121 0122 void SplitDialog::Private::blockImmediateEditor() 0123 { 0124 if (ui->splitView->model()->rowCount() <= 1) { 0125 ui->splitView->skipStartEditing(); 0126 } 0127 } 0128 0129 void SplitDialog::Private::selectRow(int row) 0130 { 0131 if (row >= ui->splitView->model()->rowCount()) 0132 row = ui->splitView->model()->rowCount() - 1; 0133 if (row >= 0) { 0134 blockEditorStart(true); 0135 ui->splitView->selectRow(row); 0136 blockEditorStart(false); 0137 } 0138 } 0139 0140 SplitDialog::SplitDialog(const MyMoneySecurity& commodity, 0141 const MyMoneyMoney& amount, 0142 int fraction, 0143 const MyMoneyMoney& inversionFactor, 0144 QWidget* parent, 0145 Qt::WindowFlags f) 0146 : QDialog(parent, f) 0147 , d(new Private(this)) 0148 { 0149 d->transactionEditor = parent; 0150 d->fraction = fraction; 0151 d->transactionTotal = amount; 0152 d->inversionFactor = inversionFactor; 0153 d->commoditySymbol = commodity.tradingSymbol(); 0154 d->ui->setupUi(this); 0155 0156 d->ui->splitView->setSelectionMode(QAbstractItemView::ExtendedSelection); 0157 d->ui->splitView->setSelectionBehavior(QAbstractItemView::SelectRows); 0158 d->ui->splitView->setCommodity(commodity); 0159 d->ui->splitView->setTotalTransactionValue(amount); 0160 0161 d->ui->okButton->setIcon(Icons::get(Icon::DialogOK)); 0162 d->ui->cancelButton->setIcon(Icons::get(Icon::DialogCancel)); 0163 0164 // setup some connections 0165 connect(d->ui->splitView, &SplitView::aboutToStartEdit, this, &SplitDialog::disableButtons); 0166 connect(d->ui->splitView, &SplitView::aboutToFinishEdit, this, &SplitDialog::enableButtons); 0167 connect(d->ui->splitView, &SplitView::deleteSelectedSplits, this, &SplitDialog::deleteSelectedSplits); 0168 0169 connect(d->ui->deleteAllButton, &QAbstractButton::pressed, this, &SplitDialog::deleteAllSplits); 0170 connect(d->ui->deleteButton, &QAbstractButton::pressed, this, &SplitDialog::deleteSelectedSplits); 0171 connect(d->ui->deleteZeroButton, &QAbstractButton::pressed, this, &SplitDialog::deleteZeroSplits); 0172 connect(d->ui->adjustUnassigned, &QAbstractButton::pressed, this, &SplitDialog::adjustUnassigned); 0173 connect(d->ui->mergeButton, &QAbstractButton::pressed, this, &SplitDialog::mergeSplits); 0174 connect(d->ui->newSplitButton, &QAbstractButton::pressed, this, &SplitDialog::newSplit); 0175 0176 ensurePolished(); 0177 0178 QSize size(width(), height()); 0179 KConfigGroup grp = KSharedConfig::openConfig()->group("SplitTransactionEditor"); 0180 size = grp.readEntry("Geometry", size); 0181 size.setHeight(size.height() - 1); 0182 resize(size.expandedTo(minimumSizeHint())); 0183 0184 // m_unassigned_over = KColorScheme(QPalette::Normal).foreground(KColorScheme::PositiveText); 0185 // m_unassigned_under = KColorScheme(QPalette::Normal).foreground(KColorScheme::NegativeText); 0186 m_unassigned_error = KColorScheme(QPalette::Normal).foreground(KColorScheme::NegativeText); 0187 m_unassigned_normal = KColorScheme(QPalette::Normal).foreground(KColorScheme::NormalText); 0188 0189 const int rowHeight = d->ui->summaryView->verticalHeader()->fontMetrics().lineSpacing() + 2; 0190 d->ui->summaryView->verticalHeader()->setMinimumSectionSize(20); 0191 d->ui->summaryView->verticalHeader()->setDefaultSectionSize(rowHeight); 0192 d->ui->summaryView->setMinimumHeight((d->ui->summaryView->model()->rowCount() * rowHeight) + 4); 0193 0194 // finish polishing the widgets 0195 QMetaObject::invokeMethod(this, "adjustSummary", Qt::QueuedConnection); 0196 } 0197 0198 SplitDialog::~SplitDialog() 0199 { 0200 auto grp = KSharedConfig::openConfig()->group("SplitTransactionEditor"); 0201 grp.writeEntry("Geometry", size()); 0202 } 0203 0204 int SplitDialog::exec() 0205 { 0206 if (!d->ui->splitView->model()) { 0207 qWarning() << "SplitDialog::exec() executed without a model. Use setModel() before calling exec()."; 0208 return QDialog::Rejected; 0209 } 0210 return QDialog::exec(); 0211 } 0212 0213 void SplitDialog::accept() 0214 { 0215 adjustSummary(); 0216 bool accept = true; 0217 if (d->transactionTotal.isAutoCalc()) { 0218 d->transactionTotal = d->splitsTotal; 0219 0220 } else if (d->transactionTotal != d->splitsTotal) { 0221 QPointer<SplitAdjustDialog> dlg = new SplitAdjustDialog(this); 0222 dlg->setValues(d->ui->summaryView->item(AmountRow, ValueCol)->data(Qt::DisplayRole).toString(), 0223 d->ui->summaryView->item(SumRow, ValueCol)->data(Qt::DisplayRole).toString(), 0224 d->ui->summaryView->item(DiffRow, ValueCol)->data(Qt::DisplayRole).toString(), 0225 d->ui->splitView->model()->rowCount()); 0226 accept = false; 0227 if (dlg->exec() == QDialog::Accepted && dlg) { 0228 switch (dlg->selectedOption()) { 0229 case SplitAdjustDialog::SplitAdjustContinue: 0230 break; 0231 case SplitAdjustDialog::SplitAdjustChange: 0232 d->transactionTotal = d->splitsTotal; 0233 accept = true; 0234 break; 0235 case SplitAdjustDialog::SplitAdjustDistribute: 0236 qWarning() << "SplitDialog::accept needs to implement the case SplitAdjustDialog::SplitAdjustDistribute"; 0237 accept = true; 0238 break; 0239 case SplitAdjustDialog::SplitAdjustLeaveAsIs: 0240 accept = true; 0241 break; 0242 } 0243 } 0244 delete dlg; 0245 updateButtonState(); 0246 } 0247 if (accept) 0248 QDialog::accept(); 0249 } 0250 0251 void SplitDialog::enableButtons() 0252 { 0253 d->ui->buttonContainer->setEnabled(true); 0254 } 0255 0256 void SplitDialog::disableButtons() 0257 { 0258 d->ui->buttonContainer->setEnabled(false); 0259 } 0260 0261 void SplitDialog::setModel(SplitModel* model) 0262 { 0263 d->splitModel = model; 0264 d->ui->splitView->setModel(model); 0265 0266 if (model->rowCount() > 0) { 0267 QModelIndex index = model->index(0, 0); 0268 d->ui->splitView->setCurrentIndex(index); 0269 } 0270 0271 adjustSummary(); 0272 0273 // force an update of the summary if data changes in the model 0274 connect(model, &QAbstractItemModel::dataChanged, this, &SplitDialog::adjustSummary); 0275 connect(d->ui->splitView->selectionModel(), &QItemSelectionModel::selectionChanged, this, &SplitDialog::selectionChanged); 0276 } 0277 0278 void SplitDialog::adjustSummary() 0279 { 0280 // Apply color scheme to the summary panel 0281 for (int row = 0; row < SummaryRows; row++) { 0282 for (int col = 0; col < SummaryCols; col++) { 0283 if (row == DiffRow && col == ValueCol) 0284 continue; 0285 d->ui->summaryView->item(row, col)->setForeground(m_unassigned_normal); 0286 } 0287 } 0288 0289 // Only show the currency symbol when multiple currencies are involved 0290 QString currencySymbol = d->commoditySymbol; 0291 if (!d->splitModel->hasMultiCurrencySplits()) { 0292 currencySymbol.clear(); 0293 } 0294 0295 d->splitsTotal = 0; 0296 const auto model = d->ui->splitView->model(); 0297 for (int row = 0; row < model->rowCount(); ++row) { 0298 const auto index = model->index(row, 0); 0299 if (index.isValid() && !index.data(eMyMoney::Model::SplitIsNewRole).toBool()) { 0300 d->splitsTotal += index.data(eMyMoney::Model::SplitValueRole).value<MyMoneyMoney>(); 0301 } 0302 } 0303 0304 const int denom = MyMoneyMoney::denomToPrec(d->fraction); 0305 QString formattedValue = (d->splitsTotal * d->inversionFactor).formatMoney(currencySymbol, denom); 0306 d->ui->summaryView->item(SumRow, ValueCol)->setData(Qt::DisplayRole, formattedValue); 0307 0308 if (d->transactionEditor) { 0309 if (d->transactionTotal.isAutoCalc()) { 0310 formattedValue = (d->splitsTotal * d->inversionFactor).formatMoney(currencySymbol, denom); 0311 } else { 0312 formattedValue = (d->transactionTotal * d->inversionFactor).formatMoney(currencySymbol, denom); 0313 } 0314 d->ui->summaryView->item(AmountRow, ValueCol)->setData(Qt::DisplayRole, formattedValue); 0315 0316 if (!d->transactionTotal.isAutoCalc()) { 0317 auto diff = d->transactionTotal.abs() - d->splitsTotal.abs(); 0318 if (diff.isNegative()) { 0319 d->ui->summaryView->item(DiffRow, HeaderCol)->setData(Qt::DisplayRole, i18nc("Split editor summary", "Overassigned")); 0320 d->ui->summaryView->item(DiffRow, ValueCol)->setForeground(m_unassigned_error); 0321 } else { 0322 d->ui->summaryView->item(DiffRow, HeaderCol)->setData(Qt::DisplayRole, i18nc("Split editor summary", "Unassigned")); 0323 if (diff.isZero()) { 0324 d->ui->summaryView->item(DiffRow, ValueCol)->setForeground(m_unassigned_normal); 0325 } else { 0326 d->ui->summaryView->item(DiffRow, ValueCol)->setForeground(m_unassigned_error); 0327 } 0328 } 0329 formattedValue = (d->transactionTotal - d->splitsTotal).abs().formatMoney(currencySymbol, denom); 0330 d->ui->summaryView->item(DiffRow, ValueCol)->setData(Qt::DisplayRole, formattedValue); 0331 } else { 0332 d->ui->summaryView->item(DiffRow, HeaderCol)->setData(Qt::DisplayRole, QString()); 0333 d->ui->summaryView->item(DiffRow, ValueCol)->setData(Qt::DisplayRole, QString()); 0334 } 0335 } else { 0336 d->ui->summaryView->item(SumRow, ValueCol)->setData(Qt::DisplayRole, QString()); 0337 d->ui->summaryView->item(AmountRow, ValueCol)->setData(Qt::DisplayRole, QString()); 0338 } 0339 0340 adjustSummaryWidth(); 0341 updateButtonState(); 0342 } 0343 0344 void SplitDialog::resizeEvent(QResizeEvent* ev) 0345 { 0346 QDialog::resizeEvent(ev); 0347 adjustSummaryWidth(); 0348 } 0349 0350 void SplitDialog::adjustSummaryWidth() 0351 { 0352 d->ui->summaryView->resizeColumnToContents(1); 0353 d->ui->summaryView->horizontalHeader()->resizeSection(0, d->ui->summaryView->width() - d->ui->summaryView->horizontalHeader()->sectionSize(1) - 10); 0354 } 0355 0356 void SplitDialog::newSplit() 0357 { 0358 // creating a new split is easy, because we simply 0359 // need to select the last entry in the view. If we 0360 // are on this row already with the editor closed things 0361 // are a bit more complicated. 0362 QModelIndex index = d->ui->splitView->currentIndex(); 0363 if (index.isValid()) { 0364 int row = index.row(); 0365 if (row != d->ui->splitView->model()->rowCount() - 1) { 0366 d->ui->splitView->selectRow(d->ui->splitView->model()->rowCount() - 1); 0367 } else { 0368 d->ui->splitView->edit(index); 0369 } 0370 } else { 0371 d->ui->splitView->selectRow(d->ui->splitView->model()->rowCount() - 1); 0372 } 0373 } 0374 0375 MyMoneyMoney SplitDialog::transactionAmount() const 0376 { 0377 return d->transactionTotal; 0378 } 0379 0380 void SplitDialog::selectionChanged() 0381 { 0382 updateButtonState(); 0383 } 0384 0385 void SplitDialog::updateButtonState() 0386 { 0387 d->ui->deleteButton->setEnabled(false); 0388 d->ui->deleteAllButton->setEnabled(false); 0389 d->ui->mergeButton->setEnabled(false); 0390 d->ui->deleteZeroButton->setEnabled(false); 0391 d->ui->adjustUnassigned->setEnabled(false); 0392 0393 if (!d->readOnly) { 0394 if (d->ui->splitView->selectionModel()->selectedRows().count() > 0) { 0395 d->ui->deleteButton->setEnabled(true); 0396 } 0397 0398 if (d->ui->splitView->model()->rowCount() > 2) { 0399 d->ui->deleteAllButton->setEnabled(true); 0400 } 0401 0402 if (d->ui->splitView->selectionModel()->selectedRows().count() == 1 0403 && !d->ui->splitView->selectionModel()->selectedIndexes().at(0).data(eMyMoney::Model::IdRole).toString().isEmpty()) { 0404 if (!d->transactionTotal.isAutoCalc()) { 0405 d->ui->adjustUnassigned->setDisabled((d->transactionTotal.abs() - d->splitsTotal.abs()).isZero()); 0406 } 0407 } 0408 0409 QAbstractItemModel* model = d->ui->splitView->model(); 0410 QSet<QString> accountIDs; 0411 const auto rows = model->rowCount(); 0412 for (int row = 0; row < rows; ++row) { 0413 const auto idx = model->index(row, 0); 0414 // don't check the empty line at the end 0415 if (idx.data(eMyMoney::Model::IdRole).toString().isEmpty()) 0416 continue; 0417 0418 const auto accountID = idx.data(eMyMoney::Model::SplitAccountIdRole).toString(); 0419 const auto amount = idx.data(eMyMoney::Model::SplitSharesRole).value<MyMoneyMoney>(); 0420 if (accountIDs.contains(accountID)) { 0421 d->ui->mergeButton->setEnabled(true); 0422 } 0423 if (amount.isZero()) { 0424 d->ui->deleteZeroButton->setEnabled(true); 0425 } 0426 } 0427 } 0428 } 0429 0430 void SplitDialog::deleteSelectedSplits() 0431 { 0432 if (!d->ui->splitView->selectionModel()->selectedRows().isEmpty()) { 0433 const auto row = d->ui->splitView->selectionModel()->selectedRows().first().row(); 0434 d->deleteSplits(d->ui->splitView->selectionModel()->selectedRows()); 0435 adjustSummary(); 0436 d->selectRow(row); 0437 } 0438 } 0439 0440 void SplitDialog::deleteAllSplits() 0441 { 0442 QAbstractItemModel* model = d->ui->splitView->model(); 0443 QModelIndexList list = model->match(model->index(0, 0), eMyMoney::Model::IdRole, QLatin1String(".+"), -1, Qt::MatchRegularExpression); 0444 const auto row = d->ui->splitView->selectionModel()->selectedRows().first().row(); 0445 d->deleteSplits(list); 0446 adjustSummary(); 0447 d->selectRow(row); 0448 } 0449 0450 void SplitDialog::adjustUnassigned() 0451 { 0452 QModelIndex index = d->ui->splitView->currentIndex(); 0453 if (index.isValid()) { 0454 // extract current values ... 0455 auto shares = index.data(eMyMoney::Model::SplitSharesRole).value<MyMoneyMoney>(); 0456 auto value = index.data(eMyMoney::Model::SplitValueRole).value<MyMoneyMoney>(); 0457 const auto price = value / shares; 0458 const auto diff = d->transactionTotal - d->splitsTotal; 0459 // ... and adjust shares and value ... 0460 value += diff; 0461 shares = value / price; 0462 // ... and update the model 0463 auto model = d->ui->splitView->model(); 0464 model->setData(index, QVariant::fromValue<MyMoneyMoney>(shares), eMyMoney::Model::SplitSharesRole); 0465 model->setData(index, QVariant::fromValue<MyMoneyMoney>(value), eMyMoney::Model::SplitValueRole); 0466 0467 adjustSummary(); 0468 } 0469 } 0470 0471 void SplitDialog::deleteZeroSplits() 0472 { 0473 QAbstractItemModel* model = d->ui->splitView->model(); 0474 QModelIndexList list = model->match(model->index(0, 0), eMyMoney::Model::IdRole, QLatin1String(".+"), -1, Qt::MatchRegularExpression); 0475 0476 for (int row = 0; row < list.count();) { 0477 const auto idx = list.at(row); 0478 if (!idx.data(eMyMoney::Model::SplitSharesRole).value<MyMoneyMoney>().isZero()) { 0479 list.removeAt(row); 0480 } else { 0481 ++row; 0482 } 0483 } 0484 const auto row = d->ui->splitView->selectionModel()->selectedRows().first().row(); 0485 d->deleteSplits(list); 0486 adjustSummary(); 0487 d->selectRow(row); 0488 } 0489 0490 void SplitDialog::mergeSplits() 0491 { 0492 auto row = d->ui->splitView->selectionModel()->selectedRows().first().row(); 0493 qDebug() << "Merge splits not yet implemented."; 0494 adjustSummary(); 0495 d->selectRow(row); 0496 } 0497 0498 void SplitDialog::setTransactionPayeeId(const QString& id) 0499 { 0500 d->ui->splitView->setTransactionPayeeId(id); 0501 } 0502 0503 void SplitDialog::setReadOnly(bool readOnly) 0504 { 0505 d->readOnly = readOnly; 0506 d->ui->okButton->setDisabled(readOnly); 0507 d->ui->newSplitButton->setDisabled(readOnly); 0508 d->ui->splitView->setReadOnlyMode(readOnly); 0509 updateButtonState(); 0510 }