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