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 }