File indexing completed on 2024-05-12 16:42:10

0001 /*
0002     SPDX-FileCopyrightText: 2002 Michael Edwardes <mte@users.sourceforge.net>
0003     SPDX-FileCopyrightText: 2002-2011 Thomas Baumgart <tbaumgart@kde.org>
0004     SPDX-FileCopyrightText: 2017-2018 Łukasz Wojniłowicz <lukasz.wojnilowicz@gmail.com>
0005     SPDX-License-Identifier: GPL-2.0-or-later
0006 */
0007 
0008 #include "ksplittransactiondlg.h"
0009 
0010 // ----------------------------------------------------------------------------
0011 // QT Includes
0012 
0013 #include <QPushButton>
0014 #include <QLabel>
0015 #include <QTimer>
0016 #include <QRadioButton>
0017 #include <QList>
0018 #include <QIcon>
0019 #include <QDialogButtonBox>
0020 #include <QPointer>
0021 
0022 // ----------------------------------------------------------------------------
0023 // KDE Includes
0024 
0025 #include <KConfig>
0026 #include <KMessageBox>
0027 #include <KSharedConfig>
0028 #include <KConfigGroup>
0029 #include <KLocalizedString>
0030 
0031 // ----------------------------------------------------------------------------
0032 // Project Includes
0033 
0034 #include "ui_ksplittransactiondlg.h"
0035 #include "ui_ksplitcorrectiondlg.h"
0036 
0037 #include "kmymoneyutils.h"
0038 #include "mymoneyfile.h"
0039 #include "kmymoneysplittable.h"
0040 #include "mymoneymoney.h"
0041 #include "mymoneyexception.h"
0042 #include "mymoneyaccount.h"
0043 #include "mymoneysecurity.h"
0044 #include "mymoneysplit.h"
0045 #include "mymoneytransaction.h"
0046 #include "icons/icons.h"
0047 
0048 using namespace Icons;
0049 
0050 KSplitCorrectionDlg::KSplitCorrectionDlg(QWidget *parent) :
0051     QDialog(parent),
0052     ui(new Ui::KSplitCorrectionDlg)
0053 {
0054     ui->setupUi(this);
0055 }
0056 
0057 KSplitCorrectionDlg::~KSplitCorrectionDlg()
0058 {
0059     delete ui;
0060 }
0061 
0062 class KSplitTransactionDlgPrivate
0063 {
0064     Q_DISABLE_COPY(KSplitTransactionDlgPrivate)
0065     Q_DECLARE_PUBLIC(KSplitTransactionDlg)
0066 
0067 public:
0068     explicit KSplitTransactionDlgPrivate(KSplitTransactionDlg* qq)
0069         : q_ptr(qq)
0070         , ui(new Ui::KSplitTransactionDlg)
0071         , m_buttonBox(nullptr)
0072         , m_precision(2)
0073         , m_amountValid(false)
0074         , m_isDeposit(false)
0075         , m_readOnly(false)
0076     {
0077     }
0078 
0079     ~KSplitTransactionDlgPrivate()
0080     {
0081         delete ui;
0082     }
0083 
0084     void init(const MyMoneyTransaction& t, const QMap<QString, MyMoneyMoney>& priceInfo)
0085     {
0086         Q_Q(KSplitTransactionDlg);
0087         ui->setupUi(q);
0088         q->setModal(true);
0089 
0090         auto okButton = ui->buttonBox->button(QDialogButtonBox::Ok);
0091         okButton->setDefault(true);
0092         okButton->setShortcut(Qt::CTRL | Qt::Key_Return);
0093         auto user1Button = new QPushButton;
0094         ui->buttonBox->addButton(user1Button, QDialogButtonBox::ActionRole);
0095         auto user2Button = new QPushButton;
0096         ui->buttonBox->addButton(user2Button, QDialogButtonBox::ActionRole);
0097         auto user3Button = new QPushButton;
0098         ui->buttonBox->addButton(user3Button, QDialogButtonBox::ActionRole);
0099 
0100         //set custom buttons
0101         //clearAll button
0102         user1Button->setText(i18n("Clear &All"));
0103         user1Button->setToolTip(i18n("Clear all splits"));
0104         user1Button->setWhatsThis(i18n("Use this to clear all splits of this transaction"));
0105         user1Button->setIcon(Icons::get(Icon::EditClear));
0106 
0107         //clearZero button
0108         user2Button->setText(i18n("Clear &Zero"));
0109         user2Button->setToolTip(i18n("Removes all splits that have a value of zero"));
0110         user2Button->setIcon(Icons::get(Icon::EditClear));
0111 
0112         //merge button
0113         user3Button->setText(i18n("&Merge"));
0114         user3Button->setToolTip(i18n("Merges splits with the same category to one split"));
0115         user3Button->setWhatsThis(i18n("In case you have multiple split entries to the same category and you like to keep them as a single split"));
0116 
0117         // make finish the default
0118         ui->buttonBox->button(QDialogButtonBox::Cancel)->setDefault(true);
0119 
0120         // setup the focus
0121         ui->buttonBox->button(QDialogButtonBox::Cancel)->setFocusPolicy(Qt::NoFocus);
0122         okButton->setFocusPolicy(Qt::NoFocus);
0123         user1Button->setFocusPolicy(Qt::NoFocus);
0124 
0125         // q->connect signals with slots
0126         q->connect(ui->transactionsTable, &KMyMoneySplitTable::transactionChanged,
0127                    q, &KSplitTransactionDlg::slotSetTransaction);
0128         q->connect(ui->transactionsTable, &KMyMoneySplitTable::createCategory, q, &KSplitTransactionDlg::slotCreateCategory);
0129         q->connect(ui->transactionsTable, &KMyMoneySplitTable::createTag, q, &KSplitTransactionDlg::slotCreateTag);
0130         q->connect(ui->transactionsTable, &KMyMoneySplitTable::objectCreation, q, &KSplitTransactionDlg::objectCreation);
0131 
0132         q->connect(ui->transactionsTable, &KMyMoneySplitTable::returnPressed, q, &KSplitTransactionDlg::accept);
0133         q->connect(ui->transactionsTable, &KMyMoneySplitTable::escapePressed, q, &KSplitTransactionDlg::reject);
0134         q->connect(ui->transactionsTable, &KMyMoneySplitTable::editStarted, q, &KSplitTransactionDlg::slotEditStarted);
0135         q->connect(ui->transactionsTable, &KMyMoneySplitTable::editFinished, q, &KSplitTransactionDlg::slotUpdateButtons);
0136 
0137         q->connect(ui->buttonBox->button(QDialogButtonBox::Cancel), &QAbstractButton::clicked, q, &KSplitTransactionDlg::reject);
0138         q->connect(ui->buttonBox->button(QDialogButtonBox::Ok), &QAbstractButton::clicked, q, &KSplitTransactionDlg::accept);
0139         q->connect(user1Button, &QAbstractButton::clicked, q, &KSplitTransactionDlg::slotClearAllSplits);
0140         q->connect(user3Button, &QAbstractButton::clicked, q, &KSplitTransactionDlg::slotMergeSplits);
0141         q->connect(user2Button, &QAbstractButton::clicked, q, &KSplitTransactionDlg::slotClearUnusedSplits);
0142 
0143         // setup the precision
0144         try {
0145             auto currency = MyMoneyFile::instance()->currency(t.commodity());
0146             m_precision = MyMoneyMoney::denomToPrec(m_account.fraction(currency));
0147         } catch (const MyMoneyException &) {
0148         }
0149 
0150         q->slotSetTransaction(t);
0151 
0152         // pass on those vars
0153         ui->transactionsTable->setup(priceInfo, m_precision);
0154 
0155         QSize size(q->width(), q->height());
0156         KConfigGroup grp = KSharedConfig::openConfig()->group("SplitTransactionEditor");
0157         size = grp.readEntry("Geometry", size);
0158         size.setHeight(size.height() - 1);
0159         q->resize(size.expandedTo(q->minimumSizeHint()));
0160 
0161         // Trick: it seems, that the initial sizing of the dialog does
0162         // not work correctly. At least, the columns do not get displayed
0163         // correct. Reason: the return value of ui->transactionsTable->visibleWidth()
0164         // is incorrect. If the widget is visible, resizing works correctly.
0165         // So, we let the dialog show up and resize it then. It's not really
0166         // clean, but the only way I got the damned thing working.
0167         QTimer::singleShot(10, q, SLOT(initSize()));
0168 
0169     }
0170 
0171     /**
0172       * This method updates the display of the sums below the register
0173       */
0174     void updateSums()
0175     {
0176         Q_Q(KSplitTransactionDlg);
0177         MyMoneyMoney splits(q->splitsValue());
0178 
0179         if (m_amountValid == false) {
0180             m_split.setValue(-splits);
0181             m_transaction.modifySplit(m_split);
0182         }
0183 
0184         ui->splitSum->setText("<b>" + splits.formatMoney(QString(), m_precision) + ' ');
0185         ui->splitUnassigned->setText("<b>" + q->diffAmount().formatMoney(QString(), m_precision) + ' ');
0186         ui->transactionAmount->setText("<b>" + (-m_split.value()).formatMoney(QString(), m_precision) + ' ');
0187     }
0188 
0189     KSplitTransactionDlg* q_ptr;
0190     Ui::KSplitTransactionDlg* ui;
0191     QDialogButtonBox* m_buttonBox;
0192     /**
0193       * This member keeps a copy of the current selected transaction
0194       */
0195     MyMoneyTransaction m_transaction;
0196 
0197     /**
0198       * This member keeps a copy of the currently selected account
0199       */
0200     MyMoneyAccount m_account;
0201 
0202     /**
0203       * This member keeps a copy of the currently selected split
0204       */
0205     MyMoneySplit m_split;
0206 
0207     /**
0208       * This member keeps the precision for the values
0209       */
0210     int m_precision;
0211 
0212     /**
0213       * flag that shows that the amount specified in the constructor
0214       * should be used as fix value (true) or if it can be changed (false)
0215       */
0216     bool m_amountValid;
0217 
0218     /**
0219       * This member keeps track if the current transaction is of type
0220       * deposit (true) or withdrawal (false).
0221       */
0222     bool m_isDeposit;
0223 
0224     /**
0225      * This member keeps track of the readonly mode
0226      */
0227     bool m_readOnly;
0228 
0229     /**
0230       * This member keeps the amount that will be assigned to all the
0231       * splits that are marked 'will be calculated'.
0232       */
0233     MyMoneyMoney m_calculatedValue;
0234 };
0235 
0236 KSplitTransactionDlg::KSplitTransactionDlg(const MyMoneyTransaction& t,
0237         const MyMoneySplit& s,
0238         const MyMoneyAccount& acc,
0239         const bool amountValid,
0240         const bool deposit,
0241         const MyMoneyMoney& calculatedValue,
0242         const QMap<QString, MyMoneyMoney>& priceInfo,
0243         QWidget* parent) :
0244     QDialog(parent),
0245     d_ptr(new KSplitTransactionDlgPrivate(this))
0246 {
0247     Q_D(KSplitTransactionDlg);
0248     d->ui->buttonBox = nullptr;
0249     d->m_account = acc;
0250     d->m_split = s;
0251     d->m_precision = 2;
0252     d->m_amountValid = amountValid;
0253     d->m_isDeposit = deposit;
0254     d->m_calculatedValue = calculatedValue;
0255     d->init(t, priceInfo);
0256 }
0257 
0258 KSplitTransactionDlg::~KSplitTransactionDlg()
0259 {
0260     Q_D(KSplitTransactionDlg);
0261     auto grp =  KSharedConfig::openConfig()->group("SplitTransactionEditor");
0262     grp.writeEntry("Geometry", size());
0263     delete d;
0264 }
0265 
0266 int KSplitTransactionDlg::exec()
0267 {
0268     Q_D(KSplitTransactionDlg);
0269     // for deposits, we invert the sign of all splits.
0270     // don't forget to revert when we're done ;-)
0271     if (d->m_isDeposit) {
0272         for (auto i = 0; i < d->m_transaction.splits().count(); ++i) {
0273             MyMoneySplit split = d->m_transaction.splits()[i];
0274             split.setValue(-split.value());
0275             split.setShares(-split.shares());
0276             d->m_transaction.modifySplit(split);
0277         }
0278     }
0279 
0280     int rc;
0281     do {
0282         d->ui->transactionsTable->setFocus();
0283 
0284         // initialize the display
0285         d->ui->transactionsTable->setTransaction(d->m_transaction, d->m_split, d->m_account);
0286         d->updateSums();
0287 
0288         rc = QDialog::exec();
0289 
0290         if (rc == Accepted) {
0291             if (!diffAmount().isZero()) {
0292                 QPointer<KSplitCorrectionDlg> corrDlg = new KSplitCorrectionDlg(this);
0293                 connect(corrDlg->ui->buttonBox, &QDialogButtonBox::accepted, corrDlg.data(), &QDialog::accept);
0294                 connect(corrDlg->ui->buttonBox, &QDialogButtonBox::rejected, corrDlg.data(), &QDialog::reject);
0295                 corrDlg->ui->buttonGroup->setId(corrDlg->ui->continueBtn, 0);
0296                 corrDlg->ui->buttonGroup->setId(corrDlg->ui->changeBtn, 1);
0297                 corrDlg->ui->buttonGroup->setId(corrDlg->ui->distributeBtn, 2);
0298                 corrDlg->ui->buttonGroup->setId(corrDlg->ui->leaveBtn, 3);
0299 
0300                 MyMoneySplit split = d->m_transaction.splits()[0];
0301                 QString total = (-split.value()).formatMoney(QString(), d->m_precision);
0302                 QString sums = splitsValue().formatMoney(QString(), d->m_precision);
0303                 QString diff = diffAmount().formatMoney(QString(), d->m_precision);
0304 
0305                 // now modify the text items of the dialog to contain the correct values
0306                 QString q = i18n("The total amount of this transaction is %1 while "
0307                                  "the sum of the splits is %2. The remaining %3 are "
0308                                  "unassigned.", total, sums, diff);
0309                 corrDlg->ui->explanation->setText(q);
0310 
0311                 q = i18n("Change &total amount of transaction to %1.", sums);
0312                 corrDlg->ui->changeBtn->setText(q);
0313 
0314                 q = i18n("&Distribute difference of %1 among all splits.", diff);
0315                 corrDlg->ui->distributeBtn->setText(q);
0316                 // FIXME remove the following line once distribution among
0317                 //       all splits is implemented
0318                 corrDlg->ui->distributeBtn->hide();
0319 
0320 
0321                 // if we have only two splits left, we don't allow leaving sth. unassigned.
0322                 if (d->m_transaction.splitCount() < 3) {
0323                     q = i18n("&Leave total amount of transaction at %1.", total);
0324                 } else {
0325                     q = i18n("&Leave %1 unassigned.", diff);
0326                 }
0327                 corrDlg->ui->leaveBtn->setText(q);
0328 
0329                 if ((rc = corrDlg->exec()) == Accepted) {
0330                     switch (corrDlg->ui->buttonGroup->checkedId()) {
0331                     case 0:       // continue to edit
0332                         rc = Rejected;
0333                         break;
0334 
0335                     case 1:       // modify total
0336                         split.setValue(-splitsValue());
0337                         split.setShares(-splitsValue());
0338                         d->m_transaction.modifySplit(split);
0339                         break;
0340 
0341                     case 2:       // distribute difference
0342                         qDebug("distribution of difference not yet supported in KSplitTransactionDlg::slotFinishClicked()");
0343                         break;
0344 
0345                     case 3:       // leave unassigned
0346                         break;
0347                     }
0348                 }
0349                 delete corrDlg;
0350             }
0351         } else
0352             break;
0353 
0354     } while (rc != Accepted);
0355 
0356     // for deposits, we inverted the sign of all splits.
0357     // now we revert it back, so that things are left correct
0358     if (d->m_isDeposit) {
0359         for (auto i = 0; i < d->m_transaction.splits().count(); ++i) {
0360             auto split = d->m_transaction.splits()[i];
0361             split.setValue(-split.value());
0362             split.setShares(-split.shares());
0363             d->m_transaction.modifySplit(split);
0364         }
0365     }
0366 
0367     return rc;
0368 }
0369 
0370 void KSplitTransactionDlg::initSize()
0371 {
0372     QDialog::resize(width(), height() + 1);
0373 }
0374 
0375 void KSplitTransactionDlg::accept()
0376 {
0377     Q_D(KSplitTransactionDlg);
0378     d->ui->transactionsTable->slotCancelEdit();
0379     QDialog::accept();
0380 }
0381 
0382 void KSplitTransactionDlg::reject()
0383 {
0384     Q_D(KSplitTransactionDlg);
0385     // cancel any edit activity in the split register
0386     d->ui->transactionsTable->slotCancelEdit();
0387     QDialog::reject();
0388 }
0389 
0390 void KSplitTransactionDlg::slotClearAllSplits()
0391 {
0392     Q_D(KSplitTransactionDlg);
0393     int answer;
0394     answer = KMessageBox::warningContinueCancel(this,
0395              i18n("You are about to delete all splits of this transaction. "
0396                   "Do you really want to continue?"),
0397              i18n("KMyMoney"));
0398 
0399     if (answer == KMessageBox::Continue) {
0400         d->ui->transactionsTable->slotCancelEdit();
0401         QList<MyMoneySplit> list = d->ui->transactionsTable->getSplits(d->m_transaction);
0402         QList<MyMoneySplit>::ConstIterator it;
0403 
0404         // clear all but the one referencing the account
0405         for (it = list.constBegin(); it != list.constEnd(); ++it) {
0406             d->m_transaction.removeSplit(*it);
0407         }
0408 
0409         d->ui->transactionsTable->setTransaction(d->m_transaction, d->m_split, d->m_account);
0410         slotSetTransaction(d->m_transaction);
0411     }
0412 }
0413 
0414 void KSplitTransactionDlg::slotClearUnusedSplits()
0415 {
0416     Q_D(KSplitTransactionDlg);
0417     QList<MyMoneySplit> list = d->ui->transactionsTable->getSplits(d->m_transaction);
0418     QList<MyMoneySplit>::ConstIterator it;
0419 
0420     try {
0421         // remove all splits that don't have a value assigned
0422         for (it = list.constBegin(); it != list.constEnd(); ++it) {
0423             if ((*it).shares().isZero()) {
0424                 d->m_transaction.removeSplit(*it);
0425             }
0426         }
0427 
0428         d->ui->transactionsTable->setTransaction(d->m_transaction, d->m_split, d->m_account);
0429         slotSetTransaction(d->m_transaction);
0430     } catch (const MyMoneyException &) {
0431     }
0432 }
0433 
0434 void KSplitTransactionDlg::slotMergeSplits()
0435 {
0436     Q_D(KSplitTransactionDlg);
0437 
0438     try {
0439         // collect all splits, merge them if needed and remove from transaction
0440         QList<MyMoneySplit> splits;
0441         foreach (const auto lsplit, d->ui->transactionsTable->getSplits(d->m_transaction)) {
0442             auto found = false;
0443             for (auto& split : splits) {
0444                 if (split.accountId() == lsplit.accountId()
0445                         && split.memo().isEmpty() && lsplit.memo().isEmpty()) {
0446                     split.setShares(lsplit.shares() + split.shares());
0447                     split.setValue(lsplit.value() + split.value());
0448                     found = true;
0449                     break;
0450                 }
0451             }
0452             if (!found)
0453                 splits << lsplit;
0454 
0455             d->m_transaction.removeSplit(lsplit);
0456         }
0457 
0458         // now add them back to the transaction
0459         for (auto& split : splits) {
0460             split.clearId();
0461             d->m_transaction.addSplit(split);
0462         }
0463 
0464         d->ui->transactionsTable->setTransaction(d->m_transaction, d->m_split, d->m_account);
0465         slotSetTransaction(d->m_transaction);
0466     } catch (const MyMoneyException &) {
0467     }
0468 }
0469 
0470 void KSplitTransactionDlg::slotSetTransaction(const MyMoneyTransaction& t)
0471 {
0472     Q_D(KSplitTransactionDlg);
0473     d->m_transaction = t;
0474     slotUpdateButtons();
0475     d->updateSums();
0476 }
0477 
0478 void KSplitTransactionDlg::slotUpdateButtons()
0479 {
0480     Q_D(KSplitTransactionDlg);
0481     QList<MyMoneySplit> list = d->ui->transactionsTable->getSplits(d->m_transaction);
0482     // check if we can merge splits or not, have zero splits or not
0483     QMap<QString, int> splits;
0484     bool haveZeroSplit = false;
0485     for (QList<MyMoneySplit>::const_iterator it = list.constBegin(); it != list.constEnd(); ++it) {
0486         splits[(*it).accountId()]++;
0487         if (((*it).id() != d->m_split.id()) && ((*it).shares().isZero()))
0488             haveZeroSplit = true;
0489     }
0490     QMap<QString, int>::const_iterator it_s;
0491     for (it_s = splits.constBegin(); it_s != splits.constEnd(); ++it_s) {
0492         if ((*it_s) > 1)
0493             break;
0494     }
0495     d->ui->buttonBox->buttons().at(4)->setEnabled(it_s != splits.constEnd());
0496     d->ui->buttonBox->buttons().at(3)->setEnabled(haveZeroSplit);
0497 
0498     // manage readonly mode
0499     if (d->m_readOnly) {
0500         d->ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
0501         d->ui->buttonBox->buttons().at(2)->setEnabled(false); // clear all splits
0502         d->ui->buttonBox->buttons().at(3)->setEnabled(false); // clear zero splits
0503         d->ui->buttonBox->buttons().at(4)->setEnabled(false); // merge splits
0504     }
0505 }
0506 
0507 void KSplitTransactionDlg::slotEditStarted()
0508 {
0509     Q_D(KSplitTransactionDlg);
0510     d->ui->buttonBox->buttons().at(4)->setEnabled(false);
0511     d->ui->buttonBox->buttons().at(3)->setEnabled(false);
0512 }
0513 
0514 MyMoneyMoney KSplitTransactionDlg::splitsValue()
0515 {
0516     Q_D(KSplitTransactionDlg);
0517     MyMoneyMoney splitsValue(d->m_calculatedValue);
0518     QList<MyMoneySplit> list = d->ui->transactionsTable->getSplits(d->m_transaction);
0519     QList<MyMoneySplit>::ConstIterator it;
0520 
0521     // calculate the current sum of all split parts
0522     for (it = list.constBegin(); it != list.constEnd(); ++it) {
0523         if ((*it).value() != MyMoneyMoney::autoCalc)
0524             splitsValue += (*it).value();
0525     }
0526 
0527     return splitsValue;
0528 }
0529 
0530 MyMoneyTransaction KSplitTransactionDlg::transaction() const
0531 {
0532     Q_D(const KSplitTransactionDlg);
0533     return d->m_transaction;
0534 }
0535 
0536 MyMoneyMoney KSplitTransactionDlg::diffAmount()
0537 {
0538     Q_D(KSplitTransactionDlg);
0539     MyMoneyMoney diff;
0540 
0541     // if there is an amount specified in the transaction, we need to calculate the
0542     // difference, otherwise we display the difference as 0 and display the same sum.
0543     if (d->m_amountValid) {
0544         MyMoneySplit split = d->m_transaction.splits()[0];
0545 
0546         diff = -(splitsValue() + split.value());
0547     }
0548     return diff;
0549 }
0550 
0551 void KSplitTransactionDlg::slotCreateCategory(const QString& name, QString& id)
0552 {
0553     Q_D(KSplitTransactionDlg);
0554     MyMoneyAccount acc, parent;
0555     acc.setName(name);
0556 
0557     if (d->m_isDeposit)
0558         parent = MyMoneyFile::instance()->income();
0559     else
0560         parent = MyMoneyFile::instance()->expense();
0561 
0562     // TODO extract possible first part of a hierarchy and check if it is one
0563     // of our top categories. If so, remove it and select the parent
0564     // according to this information.
0565 
0566     emit createCategory(acc, parent);
0567 
0568     // return id
0569     id = acc.id();
0570 }
0571 
0572 void KSplitTransactionDlg::slotCreateTag(const QString& txt, QString& id)
0573 {
0574     KMyMoneyUtils::newTag(txt, id);
0575     emit createTag(txt, id);
0576 }
0577 
0578 void KSplitTransactionDlg::setReadOnlyMode(bool readOnly)
0579 {
0580     Q_D(KSplitTransactionDlg);
0581     d->m_readOnly = readOnly;
0582     d->ui->transactionsTable->setReadOnlyMode(readOnly);
0583     slotUpdateButtons();
0584 }