1 /*
2  * Copyright 2002       Michael Edwardes <mte@users.sourceforge.net>
3  * Copyright 2002-2011  Thomas Baumgart <tbaumgart@kde.org>
4  * Copyright 2017-2018  Łukasz Wojniłowicz <lukasz.wojnilowicz@gmail.com>
5  *
6  * This program is free software; you can redistribute it and/or
7  * modify it under the terms of the GNU General Public License as
8  * published by the Free Software Foundation; either version 2 of
9  * the License, or (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
18  */
19 
20 #include "ksplittransactiondlg.h"
21 
22 // ----------------------------------------------------------------------------
23 // QT Includes
24 
25 #include <QPushButton>
26 #include <QLabel>
27 #include <QTimer>
28 #include <QRadioButton>
29 #include <QList>
30 #include <QIcon>
31 #include <QDialogButtonBox>
32 #include <QPointer>
33 
34 // ----------------------------------------------------------------------------
35 // KDE Includes
36 
37 #include <KConfig>
38 #include <KMessageBox>
39 #include <KSharedConfig>
40 #include <KConfigGroup>
41 #include <KLocalizedString>
42 
43 // ----------------------------------------------------------------------------
44 // Project Includes
45 
46 #include "ui_ksplittransactiondlg.h"
47 #include "ui_ksplitcorrectiondlg.h"
48 
49 #include "kmymoneyutils.h"
50 #include "mymoneyfile.h"
51 #include "kmymoneysplittable.h"
52 #include "mymoneymoney.h"
53 #include "mymoneyexception.h"
54 #include "mymoneyaccount.h"
55 #include "mymoneysecurity.h"
56 #include "mymoneysplit.h"
57 #include "mymoneytransaction.h"
58 #include "icons/icons.h"
59 
60 using namespace Icons;
61 
KSplitCorrectionDlg(QWidget * parent)62 KSplitCorrectionDlg::KSplitCorrectionDlg(QWidget *parent) :
63   QDialog(parent),
64   ui(new Ui::KSplitCorrectionDlg)
65 {
66   ui->setupUi(this);
67 }
68 
~KSplitCorrectionDlg()69 KSplitCorrectionDlg::~KSplitCorrectionDlg()
70 {
71   delete ui;
72 }
73 
74 class KSplitTransactionDlgPrivate
75 {
76   Q_DISABLE_COPY(KSplitTransactionDlgPrivate)
77   Q_DECLARE_PUBLIC(KSplitTransactionDlg)
78 
79 public:
KSplitTransactionDlgPrivate(KSplitTransactionDlg * qq)80   explicit KSplitTransactionDlgPrivate(KSplitTransactionDlg *qq) :
81     q_ptr(qq),
82     ui(new Ui::KSplitTransactionDlg),
83     m_buttonBox(nullptr),
84     m_precision(2),
85     m_amountValid(false),
86     m_isDeposit(false)
87   {
88   }
89 
~KSplitTransactionDlgPrivate()90   ~KSplitTransactionDlgPrivate()
91   {
92     delete ui;
93   }
94 
init(const MyMoneyTransaction & t,const QMap<QString,MyMoneyMoney> & priceInfo)95   void init(const MyMoneyTransaction& t, const QMap<QString, MyMoneyMoney>& priceInfo)
96   {
97     Q_Q(KSplitTransactionDlg);
98     ui->setupUi(q);
99     q->setModal(true);
100 
101     auto okButton = ui->buttonBox->button(QDialogButtonBox::Ok);
102     okButton->setDefault(true);
103     okButton->setShortcut(Qt::CTRL | Qt::Key_Return);
104     auto user1Button = new QPushButton;
105     ui->buttonBox->addButton(user1Button, QDialogButtonBox::ActionRole);
106     auto user2Button = new QPushButton;
107     ui->buttonBox->addButton(user2Button, QDialogButtonBox::ActionRole);
108     auto user3Button = new QPushButton;
109     ui->buttonBox->addButton(user3Button, QDialogButtonBox::ActionRole);
110 
111     //set custom buttons
112     //clearAll button
113     user1Button->setText(i18n("Clear &All"));
114     user1Button->setToolTip(i18n("Clear all splits"));
115     user1Button->setWhatsThis(i18n("Use this to clear all splits of this transaction"));
116     user1Button->setIcon(Icons::get(Icon::EditClear));
117 
118     //clearZero button
119     user2Button->setText(i18n("Clear &Zero"));
120     user2Button->setToolTip(i18n("Removes all splits that have a value of zero"));
121     user2Button->setIcon(Icons::get(Icon::EditClear));
122 
123     //merge button
124     user3Button->setText(i18n("&Merge"));
125     user3Button->setToolTip(i18n("Merges splits with the same category to one split"));
126     user3Button->setWhatsThis(i18n("In case you have multiple split entries to the same category and you like to keep them as a single split"));
127 
128     // make finish the default
129     ui->buttonBox->button(QDialogButtonBox::Cancel)->setDefault(true);
130 
131     // setup the focus
132     ui->buttonBox->button(QDialogButtonBox::Cancel)->setFocusPolicy(Qt::NoFocus);
133     okButton->setFocusPolicy(Qt::NoFocus);
134     user1Button->setFocusPolicy(Qt::NoFocus);
135 
136     // q->connect signals with slots
137     q->connect(ui->transactionsTable, &KMyMoneySplitTable::transactionChanged,
138             q, &KSplitTransactionDlg::slotSetTransaction);
139     q->connect(ui->transactionsTable, &KMyMoneySplitTable::createCategory, q, &KSplitTransactionDlg::slotCreateCategory);
140     q->connect(ui->transactionsTable, &KMyMoneySplitTable::createTag, q, &KSplitTransactionDlg::slotCreateTag);
141     q->connect(ui->transactionsTable, &KMyMoneySplitTable::objectCreation, q, &KSplitTransactionDlg::objectCreation);
142 
143     q->connect(ui->transactionsTable, &KMyMoneySplitTable::returnPressed, q, &KSplitTransactionDlg::accept);
144     q->connect(ui->transactionsTable, &KMyMoneySplitTable::escapePressed, q, &KSplitTransactionDlg::reject);
145     q->connect(ui->transactionsTable, &KMyMoneySplitTable::editStarted, q, &KSplitTransactionDlg::slotEditStarted);
146     q->connect(ui->transactionsTable, &KMyMoneySplitTable::editFinished, q, &KSplitTransactionDlg::slotUpdateButtons);
147 
148     q->connect(ui->buttonBox->button(QDialogButtonBox::Cancel), &QAbstractButton::clicked, q, &KSplitTransactionDlg::reject);
149     q->connect(ui->buttonBox->button(QDialogButtonBox::Ok), &QAbstractButton::clicked, q, &KSplitTransactionDlg::accept);
150     q->connect(user1Button, &QAbstractButton::clicked, q, &KSplitTransactionDlg::slotClearAllSplits);
151     q->connect(user3Button, &QAbstractButton::clicked, q, &KSplitTransactionDlg::slotMergeSplits);
152     q->connect(user2Button, &QAbstractButton::clicked, q, &KSplitTransactionDlg::slotClearUnusedSplits);
153 
154     // setup the precision
155     try {
156       auto currency = MyMoneyFile::instance()->currency(t.commodity());
157       m_precision = MyMoneyMoney::denomToPrec(m_account.fraction(currency));
158     } catch (const MyMoneyException &) {
159     }
160 
161     q->slotSetTransaction(t);
162 
163     // pass on those vars
164     ui->transactionsTable->setup(priceInfo, m_precision);
165 
166     QSize size(q->width(), q->height());
167     KConfigGroup grp = KSharedConfig::openConfig()->group("SplitTransactionEditor");
168     size = grp.readEntry("Geometry", size);
169     size.setHeight(size.height() - 1);
170     q->resize(size.expandedTo(q->minimumSizeHint()));
171 
172     // Trick: it seems, that the initial sizing of the dialog does
173     // not work correctly. At least, the columns do not get displayed
174     // correct. Reason: the return value of ui->transactionsTable->visibleWidth()
175     // is incorrect. If the widget is visible, resizing works correctly.
176     // So, we let the dialog show up and resize it then. It's not really
177     // clean, but the only way I got the damned thing working.
178     QTimer::singleShot(10, q, SLOT(initSize()));
179 
180   }
181 
182   /**
183     * This method updates the display of the sums below the register
184     */
updateSums()185   void updateSums()
186   {
187     Q_Q(KSplitTransactionDlg);
188     MyMoneyMoney splits(q->splitsValue());
189 
190     if (m_amountValid == false) {
191       m_split.setValue(-splits);
192       m_transaction.modifySplit(m_split);
193     }
194 
195     ui->splitSum->setText("<b>" + splits.formatMoney(QString(), m_precision) + ' ');
196     ui->splitUnassigned->setText("<b>" + q->diffAmount().formatMoney(QString(), m_precision) + ' ');
197     ui->transactionAmount->setText("<b>" + (-m_split.value()).formatMoney(QString(), m_precision) + ' ');
198   }
199 
200   KSplitTransactionDlg      *q_ptr;
201   Ui::KSplitTransactionDlg  *ui;
202   QDialogButtonBox          *m_buttonBox;
203   /**
204     * This member keeps a copy of the current selected transaction
205     */
206   MyMoneyTransaction     m_transaction;
207 
208   /**
209     * This member keeps a copy of the currently selected account
210     */
211   MyMoneyAccount         m_account;
212 
213   /**
214     * This member keeps a copy of the currently selected split
215     */
216   MyMoneySplit           m_split;
217 
218   /**
219     * This member keeps the precision for the values
220     */
221   int                    m_precision;
222 
223   /**
224     * flag that shows that the amount specified in the constructor
225     * should be used as fix value (true) or if it can be changed (false)
226     */
227   bool                   m_amountValid;
228 
229   /**
230     * This member keeps track if the current transaction is of type
231     * deposit (true) or withdrawal (false).
232     */
233   bool                   m_isDeposit;
234 
235   /**
236     * This member keeps the amount that will be assigned to all the
237     * splits that are marked 'will be calculated'.
238     */
239   MyMoneyMoney           m_calculatedValue;
240 };
241 
KSplitTransactionDlg(const MyMoneyTransaction & t,const MyMoneySplit & s,const MyMoneyAccount & acc,const bool amountValid,const bool deposit,const MyMoneyMoney & calculatedValue,const QMap<QString,MyMoneyMoney> & priceInfo,QWidget * parent)242 KSplitTransactionDlg::KSplitTransactionDlg(const MyMoneyTransaction& t,
243     const MyMoneySplit& s,
244     const MyMoneyAccount& acc,
245     const bool amountValid,
246     const bool deposit,
247     const MyMoneyMoney& calculatedValue,
248     const QMap<QString, MyMoneyMoney>& priceInfo,
249     QWidget* parent) :
250   QDialog(parent),
251   d_ptr(new KSplitTransactionDlgPrivate(this))
252 {
253   Q_D(KSplitTransactionDlg);
254   d->ui->buttonBox = nullptr;
255   d->m_account = acc;
256   d->m_split = s;
257   d->m_precision = 2;
258   d->m_amountValid = amountValid;
259   d->m_isDeposit = deposit;
260   d->m_calculatedValue = calculatedValue;
261   d->init(t, priceInfo);
262 }
263 
~KSplitTransactionDlg()264 KSplitTransactionDlg::~KSplitTransactionDlg()
265 {
266   Q_D(KSplitTransactionDlg);
267   auto grp =  KSharedConfig::openConfig()->group("SplitTransactionEditor");
268   grp.writeEntry("Geometry", size());
269   delete d;
270 }
271 
exec()272 int KSplitTransactionDlg::exec()
273 {
274   Q_D(KSplitTransactionDlg);
275   // for deposits, we invert the sign of all splits.
276   // don't forget to revert when we're done ;-)
277   if (d->m_isDeposit) {
278     for (auto i = 0; i < d->m_transaction.splits().count(); ++i) {
279       MyMoneySplit split = d->m_transaction.splits()[i];
280       split.setValue(-split.value());
281       split.setShares(-split.shares());
282       d->m_transaction.modifySplit(split);
283     }
284   }
285 
286   int rc;
287   do {
288     d->ui->transactionsTable->setFocus();
289 
290     // initialize the display
291     d->ui->transactionsTable->setTransaction(d->m_transaction, d->m_split, d->m_account);
292     d->updateSums();
293 
294     rc = QDialog::exec();
295 
296     if (rc == Accepted) {
297       if (!diffAmount().isZero()) {
298         QPointer<KSplitCorrectionDlg> corrDlg = new KSplitCorrectionDlg(this);
299         connect(corrDlg->ui->buttonBox, &QDialogButtonBox::accepted, corrDlg.data(), &QDialog::accept);
300         connect(corrDlg->ui->buttonBox, &QDialogButtonBox::rejected, corrDlg.data(), &QDialog::reject);
301         corrDlg->ui->buttonGroup->setId(corrDlg->ui->continueBtn, 0);
302         corrDlg->ui->buttonGroup->setId(corrDlg->ui->changeBtn, 1);
303         corrDlg->ui->buttonGroup->setId(corrDlg->ui->distributeBtn, 2);
304         corrDlg->ui->buttonGroup->setId(corrDlg->ui->leaveBtn, 3);
305 
306         MyMoneySplit split = d->m_transaction.splits()[0];
307         QString total = (-split.value()).formatMoney(QString(), d->m_precision);
308         QString sums = splitsValue().formatMoney(QString(), d->m_precision);
309         QString diff = diffAmount().formatMoney(QString(), d->m_precision);
310 
311         // now modify the text items of the dialog to contain the correct values
312         QString q = i18n("The total amount of this transaction is %1 while "
313                          "the sum of the splits is %2. The remaining %3 are "
314                          "unassigned.", total, sums, diff);
315         corrDlg->ui->explanation->setText(q);
316 
317         q = i18n("Change &total amount of transaction to %1.", sums);
318         corrDlg->ui->changeBtn->setText(q);
319 
320         q = i18n("&Distribute difference of %1 among all splits.", diff);
321         corrDlg->ui->distributeBtn->setText(q);
322         // FIXME remove the following line once distribution among
323         //       all splits is implemented
324         corrDlg->ui->distributeBtn->hide();
325 
326 
327         // if we have only two splits left, we don't allow leaving sth. unassigned.
328         if (d->m_transaction.splitCount() < 3) {
329           q = i18n("&Leave total amount of transaction at %1.", total);
330         } else {
331           q = i18n("&Leave %1 unassigned.", diff);
332         }
333         corrDlg->ui->leaveBtn->setText(q);
334 
335         if ((rc = corrDlg->exec()) == Accepted) {
336           switch (corrDlg->ui->buttonGroup->checkedId()) {
337             case 0:       // continue to edit
338               rc = Rejected;
339               break;
340 
341             case 1:       // modify total
342               split.setValue(-splitsValue());
343               split.setShares(-splitsValue());
344               d->m_transaction.modifySplit(split);
345               break;
346 
347             case 2:       // distribute difference
348               qDebug("distribution of difference not yet supported in KSplitTransactionDlg::slotFinishClicked()");
349               break;
350 
351             case 3:       // leave unassigned
352               break;
353           }
354         }
355         delete corrDlg;
356       }
357     } else
358       break;
359 
360   } while (rc != Accepted);
361 
362   // for deposits, we inverted the sign of all splits.
363   // now we revert it back, so that things are left correct
364   if (d->m_isDeposit) {
365     for (auto i = 0; i < d->m_transaction.splits().count(); ++i) {
366       auto split = d->m_transaction.splits()[i];
367       split.setValue(-split.value());
368       split.setShares(-split.shares());
369       d->m_transaction.modifySplit(split);
370     }
371   }
372 
373   return rc;
374 }
375 
initSize()376 void KSplitTransactionDlg::initSize()
377 {
378   QDialog::resize(width(), height() + 1);
379 }
380 
accept()381 void KSplitTransactionDlg::accept()
382 {
383   Q_D(KSplitTransactionDlg);
384   d->ui->transactionsTable->slotCancelEdit();
385   QDialog::accept();
386 }
387 
reject()388 void KSplitTransactionDlg::reject()
389 {
390   Q_D(KSplitTransactionDlg);
391   // cancel any edit activity in the split register
392   d->ui->transactionsTable->slotCancelEdit();
393   QDialog::reject();
394 }
395 
slotClearAllSplits()396 void KSplitTransactionDlg::slotClearAllSplits()
397 {
398   Q_D(KSplitTransactionDlg);
399   int answer;
400   answer = KMessageBox::warningContinueCancel(this,
401            i18n("You are about to delete all splits of this transaction. "
402                 "Do you really want to continue?"),
403            i18n("KMyMoney"));
404 
405   if (answer == KMessageBox::Continue) {
406     d->ui->transactionsTable->slotCancelEdit();
407     QList<MyMoneySplit> list = d->ui->transactionsTable->getSplits(d->m_transaction);
408     QList<MyMoneySplit>::ConstIterator it;
409 
410     // clear all but the one referencing the account
411     for (it = list.constBegin(); it != list.constEnd(); ++it) {
412       d->m_transaction.removeSplit(*it);
413     }
414 
415     d->ui->transactionsTable->setTransaction(d->m_transaction, d->m_split, d->m_account);
416     slotSetTransaction(d->m_transaction);
417   }
418 }
419 
slotClearUnusedSplits()420 void KSplitTransactionDlg::slotClearUnusedSplits()
421 {
422   Q_D(KSplitTransactionDlg);
423   QList<MyMoneySplit> list = d->ui->transactionsTable->getSplits(d->m_transaction);
424   QList<MyMoneySplit>::ConstIterator it;
425 
426   try {
427     // remove all splits that don't have a value assigned
428     for (it = list.constBegin(); it != list.constEnd(); ++it) {
429       if ((*it).shares().isZero()) {
430         d->m_transaction.removeSplit(*it);
431       }
432     }
433 
434     d->ui->transactionsTable->setTransaction(d->m_transaction, d->m_split, d->m_account);
435     slotSetTransaction(d->m_transaction);
436   } catch (const MyMoneyException &) {
437   }
438 }
439 
slotMergeSplits()440 void KSplitTransactionDlg::slotMergeSplits()
441 {
442   Q_D(KSplitTransactionDlg);
443 
444   try {
445     // collect all splits, merge them if needed and remove from transaction
446     QList<MyMoneySplit> splits;
447     foreach (const auto lsplit, d->ui->transactionsTable->getSplits(d->m_transaction)) {
448       auto found = false;
449       for (auto& split : splits) {
450         if (split.accountId() == lsplit.accountId()
451             && split.memo().isEmpty() && lsplit.memo().isEmpty()) {
452           split.setShares(lsplit.shares() + split.shares());
453           split.setValue(lsplit.value() + split.value());
454           found = true;
455           break;
456         }
457       }
458       if (!found)
459         splits << lsplit;
460 
461       d->m_transaction.removeSplit(lsplit);
462     }
463 
464     // now add them back to the transaction
465     for (auto& split : splits) {
466       split.clearId();
467       d->m_transaction.addSplit(split);
468     }
469 
470     d->ui->transactionsTable->setTransaction(d->m_transaction, d->m_split, d->m_account);
471     slotSetTransaction(d->m_transaction);
472   } catch (const MyMoneyException &) {
473   }
474 }
475 
slotSetTransaction(const MyMoneyTransaction & t)476 void KSplitTransactionDlg::slotSetTransaction(const MyMoneyTransaction& t)
477 {
478   Q_D(KSplitTransactionDlg);
479   d->m_transaction = t;
480   slotUpdateButtons();
481   d->updateSums();
482 }
483 
slotUpdateButtons()484 void KSplitTransactionDlg::slotUpdateButtons()
485 {
486   Q_D(KSplitTransactionDlg);
487   QList<MyMoneySplit> list = d->ui->transactionsTable->getSplits(d->m_transaction);
488   // check if we can merge splits or not, have zero splits or not
489   QMap<QString, int> splits;
490   bool haveZeroSplit = false;
491   for (QList<MyMoneySplit>::const_iterator it = list.constBegin(); it != list.constEnd(); ++it) {
492     splits[(*it).accountId()]++;
493     if (((*it).id() != d->m_split.id()) && ((*it).shares().isZero()))
494       haveZeroSplit = true;
495   }
496   QMap<QString, int>::const_iterator it_s;
497   for (it_s = splits.constBegin(); it_s != splits.constEnd(); ++it_s) {
498     if ((*it_s) > 1)
499       break;
500   }
501   d->ui->buttonBox->buttons().at(4)->setEnabled(it_s != splits.constEnd());
502   d->ui->buttonBox->buttons().at(3)->setEnabled(haveZeroSplit);
503 }
504 
slotEditStarted()505 void KSplitTransactionDlg::slotEditStarted()
506 {
507   Q_D(KSplitTransactionDlg);
508   d->ui->buttonBox->buttons().at(4)->setEnabled(false);
509   d->ui->buttonBox->buttons().at(3)->setEnabled(false);
510 }
511 
splitsValue()512 MyMoneyMoney KSplitTransactionDlg::splitsValue()
513 {
514   Q_D(KSplitTransactionDlg);
515   MyMoneyMoney splitsValue(d->m_calculatedValue);
516   QList<MyMoneySplit> list = d->ui->transactionsTable->getSplits(d->m_transaction);
517   QList<MyMoneySplit>::ConstIterator it;
518 
519   // calculate the current sum of all split parts
520   for (it = list.constBegin(); it != list.constEnd(); ++it) {
521     if ((*it).value() != MyMoneyMoney::autoCalc)
522       splitsValue += (*it).value();
523   }
524 
525   return splitsValue;
526 }
527 
transaction() const528 MyMoneyTransaction KSplitTransactionDlg::transaction() const
529 {
530   Q_D(const KSplitTransactionDlg);
531   return d->m_transaction;
532 }
533 
diffAmount()534 MyMoneyMoney KSplitTransactionDlg::diffAmount()
535 {
536   Q_D(KSplitTransactionDlg);
537   MyMoneyMoney diff;
538 
539   // if there is an amount specified in the transaction, we need to calculate the
540   // difference, otherwise we display the difference as 0 and display the same sum.
541   if (d->m_amountValid) {
542     MyMoneySplit split = d->m_transaction.splits()[0];
543 
544     diff = -(splitsValue() + split.value());
545   }
546   return diff;
547 }
548 
slotCreateCategory(const QString & name,QString & id)549 void KSplitTransactionDlg::slotCreateCategory(const QString& name, QString& id)
550 {
551   Q_D(KSplitTransactionDlg);
552   MyMoneyAccount acc, parent;
553   acc.setName(name);
554 
555   if (d->m_isDeposit)
556     parent = MyMoneyFile::instance()->income();
557   else
558     parent = MyMoneyFile::instance()->expense();
559 
560   // TODO extract possible first part of a hierarchy and check if it is one
561   // of our top categories. If so, remove it and select the parent
562   // according to this information.
563 
564   emit createCategory(acc, parent);
565 
566   // return id
567   id = acc.id();
568 }
569 
slotCreateTag(const QString & txt,QString & id)570 void KSplitTransactionDlg::slotCreateTag(const QString& txt, QString& id)
571 {
572   KMyMoneyUtils::newTag(txt, id);
573   emit createTag(txt, id);
574 }
575