1 /***************************************************************************
2                           knewloanwizard_p.cpp  -  description
3                              -------------------
4     copyright            : (C) 2017 by Łukasz Wojniłowicz <lukasz.wojnilowicz@gmail.com>
5 
6  ***************************************************************************/
7 
8 /***************************************************************************
9  *                                                                         *
10  *   This program is free software; you can redistribute it and/or modify  *
11  *   it under the terms of the GNU General Public License as published by  *
12  *   the Free Software Foundation; either version 2 of the License, or     *
13  *   (at your option) any later version.                                   *
14  *                                                                         *
15  ***************************************************************************/
16 
17 #ifndef KNEWLOANWIZARD_P_H
18 #define KNEWLOANWIZARD_P_H
19 
20 #include "knewloanwizard.h"
21 
22 // ----------------------------------------------------------------------------
23 // QT Includes
24 
25 #include <QBitArray>
26 #include <qmath.h>
27 
28 // ----------------------------------------------------------------------------
29 // KDE Includes
30 
31 #include <KLocalizedString>
32 #include <KMessageBox>
33 
34 // ----------------------------------------------------------------------------
35 // Project Includes
36 
37 #include "ui_knewloanwizard.h"
38 #include "ui_namewizardpage.h"
39 #include "ui_firstpaymentwizardpage.h"
40 #include "ui_loanamountwizardpage.h"
41 #include "ui_interestwizardpage.h"
42 #include "ui_paymenteditwizardpage.h"
43 #include "ui_finalpaymentwizardpage.h"
44 #include "ui_interestcategorywizardpage.h"
45 #include "ui_assetaccountwizardpage.h"
46 #include "ui_schedulewizardpage.h"
47 #include "ui_paymentwizardpage.h"
48 
49 #include "kmymoneyutils.h"
50 #include "kmymoneysettings.h"
51 
52 #include "mymoneyfinancialcalculator.h"
53 #include "mymoneyfile.h"
54 #include "mymoneyexception.h"
55 #include "mymoneysecurity.h"
56 #include "mymoneyaccountloan.h"
57 #include "mymoneyschedule.h"
58 #include "mymoneysplit.h"
59 #include "mymoneytransaction.h"
60 #include "mymoneyenums.h"
61 
62 namespace Ui { class KNewLoanWizard; }
63 
64 class KNewLoanWizard;
65 class KNewLoanWizardPrivate
66 {
67   Q_DISABLE_COPY(KNewLoanWizardPrivate)
Q_DECLARE_PUBLIC(KNewLoanWizard)68   Q_DECLARE_PUBLIC(KNewLoanWizard)
69 
70 public:
71   explicit KNewLoanWizardPrivate(KNewLoanWizard *qq) :
72     q_ptr(qq),
73     ui(new Ui::KNewLoanWizard)
74   {
75   }
76 
~KNewLoanWizardPrivate()77   ~KNewLoanWizardPrivate()
78   {
79     delete ui;
80   }
81 
init()82   void init()
83   {
84     Q_Q(KNewLoanWizard);
85     ui->setupUi(q);
86     m_pages = QBitArray(KNewLoanWizard::Page_Summary + 1, true);
87     q->setModal(true);
88 
89     KMyMoneyMVCCombo::setSubstringSearchForChildren(ui->m_namePage, !KMyMoneySettings::stringMatchFromStart());
90 
91     // make sure, the back button does not clear fields
92     q->setOption(QWizard::IndependentPages, true);
93 
94     // connect(m_payeeEdit, SIGNAL(newPayee(QString)), this, SLOT(slotNewPayee(QString)));
95     q->connect(ui->m_namePage->ui->m_payeeEdit, &KMyMoneyMVCCombo::createItem, q, &KNewLoanWizard::slotNewPayee);
96 
97     q->connect(ui->m_additionalFeesPage, &AdditionalFeesWizardPage::newCategory, q, &KNewLoanWizard::slotNewCategory);
98 
99     q->connect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, q, &KNewLoanWizard::slotReloadEditWidgets);
100 
101     resetCalculator();
102 
103     q->slotReloadEditWidgets();
104 
105     // As default we assume a liability loan, with fixed interest rate,
106     // with a first payment due on the 30th of this month. All payments
107     // should be recorded and none have been made so far.
108 
109     //FIXME: port
110     ui->m_firstPaymentPage->ui->m_firstDueDateEdit->loadDate(QDate(QDate::currentDate().year(), QDate::currentDate().month(), 30));
111 
112     // FIXME: we currently only support interest calculation on reception
113     m_pages.clearBit(KNewLoanWizard::Page_InterestCalculation);
114 
115     // turn off all pages that are contained here for derived classes
116     m_pages.clearBit(KNewLoanWizard::Page_EditIntro);
117     m_pages.clearBit(KNewLoanWizard::Page_EditSelection);
118     m_pages.clearBit(KNewLoanWizard::Page_EffectiveDate);
119     m_pages.clearBit(KNewLoanWizard::Page_PaymentEdit);
120     m_pages.clearBit(KNewLoanWizard::Page_InterestEdit);
121     m_pages.clearBit(KNewLoanWizard::Page_SummaryEdit);
122 
123     // for now, we don't have online help :-(
124     q->setOption(QWizard::HaveHelpButton, false);
125 
126     // setup a phony transaction for additional fee processing
127     m_account = MyMoneyAccount("Phony-ID", MyMoneyAccount());
128     m_split.setAccountId(m_account.id());
129     m_split.setValue(MyMoneyMoney());
130     m_transaction.addSplit(m_split);
131 
132     KMyMoneyUtils::updateWizardButtons(q);
133   }
134 
resetCalculator()135   void resetCalculator()
136   {
137     Q_Q(KNewLoanWizard);
138     ui->m_loanAmountPage->resetCalculator();
139     ui->m_interestPage->resetCalculator();
140     ui->m_durationPage->resetCalculator();
141     ui->m_paymentPage->resetCalculator();
142     ui->m_finalPaymentPage->resetCalculator();
143 
144     q->setField("additionalCost", MyMoneyMoney().formatMoney(m_account.fraction(MyMoneyFile::instance()->security(m_account.currencyId()))));
145   }
146 
updateLoanAmount()147   void updateLoanAmount()
148   {
149     Q_Q(KNewLoanWizard);
150     QString txt;
151     //FIXME: port
152     if (! q->field("loanAmountEditValid").toBool()) {
153       txt = QString("<") + i18n("calculate") + QString(">");
154     } else {
155       txt = q->field("loanAmountEdit").value<MyMoneyMoney>().formatMoney(m_account.fraction(MyMoneyFile::instance()->security(m_account.currencyId())));
156     }
157     q->setField("loanAmount1", txt);
158     q->setField("loanAmount2", txt);
159     q->setField("loanAmount3", txt);
160     q->setField("loanAmount4", txt);
161     q->setField("loanAmount5", txt);
162   }
163 
updateInterestRate()164   void updateInterestRate()
165   {
166     Q_Q(KNewLoanWizard);
167     QString txt;
168     //FIXME: port
169     if (! q->field("interestRateEditValid").toBool()) {
170       txt = QString("<") + i18n("calculate") + QString(">");
171     } else {
172       txt = q->field("interestRateEdit").value<MyMoneyMoney>().formatMoney(QString(), 3) + QString("%");
173     }
174     q->setField("interestRate1", txt);
175     q->setField("interestRate2", txt);
176     q->setField("interestRate3", txt);
177     q->setField("interestRate4", txt);
178     q->setField("interestRate5", txt);
179   }
180 
updateDuration()181   void updateDuration()
182   {
183     Q_Q(KNewLoanWizard);
184     QString txt;
185     //FIXME: port
186     if (q->field("durationValueEdit").toInt() == 0) {
187       txt = QString("<") + i18n("calculate") + QString(">");
188     } else {
189       txt = QString().sprintf("%d ", q->field("durationValueEdit").toInt())
190             + q->field("durationUnitEdit").toString();
191     }
192     q->setField("duration1", txt);
193     q->setField("duration2", txt);
194     q->setField("duration3", txt);
195     q->setField("duration4", txt);
196     q->setField("duration5", txt);
197   }
198 
updatePayment()199   void updatePayment()
200   {
201     Q_Q(KNewLoanWizard);
202     QString txt;
203     //FIXME: port
204     if (! q->field("paymentEditValid").toBool()) {
205       txt = QString("<") + i18n("calculate") + QString(">");
206     } else {
207       txt = q->field("paymentEdit").value<MyMoneyMoney>().formatMoney(m_account.fraction(MyMoneyFile::instance()->security(m_account.currencyId())));
208     }
209     q->setField("payment1", txt);
210     q->setField("payment2", txt);
211     q->setField("payment3", txt);
212     q->setField("payment4", txt);
213     q->setField("payment5", txt);
214     q->setField("basePayment", txt);
215   }
216 
updateFinalPayment()217   void updateFinalPayment()
218   {
219     Q_Q(KNewLoanWizard);
220     QString txt;
221     //FIXME: port
222     if (! q->field("finalPaymentEditValid").toBool()) {
223       txt = QString("<") + i18n("calculate") + QString(">");
224     } else {
225       txt = q->field("finalPaymentEdit").value<MyMoneyMoney>().formatMoney(m_account.fraction(MyMoneyFile::instance()->security(m_account.currencyId())));
226     }
227     q->setField("balloon1", txt);
228     q->setField("balloon2", txt);
229     q->setField("balloon3", txt);
230     q->setField("balloon4", txt);
231     q->setField("balloon5", txt);
232   }
233 
updateLoanInfo()234   void updateLoanInfo()
235   {
236     Q_Q(KNewLoanWizard);
237     updateLoanAmount();
238     updateInterestRate();
239     updateDuration();
240     updatePayment();
241     updateFinalPayment();
242     ui->m_additionalFeesPage->updatePeriodicPayment(m_account);
243 
244     QString txt;
245 
246     int fraction = m_account.fraction(MyMoneyFile::instance()->security(m_account.currencyId()));
247     q->setField("loanAmount6", q->field("loanAmountEdit").value<MyMoneyMoney>().formatMoney(fraction));
248     q->setField("interestRate6", QString(q->field("interestRateEdit").value<MyMoneyMoney>().formatMoney("", 3) + QString("%")));
249     txt = QString().sprintf("%d ", q->field("durationValueEdit").toInt())
250           + q->field("durationUnitEdit").toString();
251     q->setField("duration6", txt);
252     q->setField("payment6", q->field("paymentEdit").value<MyMoneyMoney>().formatMoney(fraction));
253     q->setField("balloon6", q->field("finalPaymentEdit").value<MyMoneyMoney>().formatMoney(fraction));
254   }
255 
calculateLoan()256   int calculateLoan()
257   {
258     Q_Q(KNewLoanWizard);
259     MyMoneyFinancialCalculator calc;
260     double val;
261     int PF;
262     QString result;
263 
264     // FIXME: for now, we only support interest calculation at the end of the period
265     calc.setBep();
266     // FIXME: for now, we only support periodic compounding
267     calc.setDisc();
268 
269     PF = MyMoneySchedule::eventsPerYear(eMyMoney::Schedule::Occurrence(q->field("paymentFrequencyUnitEdit").toInt()));
270     if (PF == 0)
271       return 0;
272     calc.setPF(PF);
273 
274     // FIXME: for now we only support compounding frequency == payment frequency
275     calc.setCF(PF);
276 
277     if (q->field("loanAmountEditValid").toBool()) {
278       val = q->field("loanAmountEdit").value<MyMoneyMoney>().abs().toDouble();
279       if (q->field("borrowButton").toBool())
280         val = -val;
281       calc.setPv(val);
282     }
283 
284     if (q->field("interestRateEditValid").toBool()) {
285       val = q->field("interestRateEdit").value<MyMoneyMoney>().abs().toDouble();
286       calc.setIr(val);
287     }
288 
289     if (q->field("paymentEditValid").toBool()) {
290       val = q->field("paymentEdit").value<MyMoneyMoney>().abs().toDouble();
291       if (q->field("lendButton").toBool())
292         val = -val;
293       calc.setPmt(val);
294     }
295 
296     if (q->field("finalPaymentEditValid").toBool()) {
297       val = q->field("finalPaymentEditValid").value<MyMoneyMoney>().abs().toDouble();
298       if (q->field("lendButton").toBool())
299         val = -val;
300       calc.setFv(val);
301     }
302 
303     if (q->field("durationValueEdit").toInt() != 0) {
304       calc.setNpp(ui->m_durationPage->term());
305     }
306 
307     int fraction = m_account.fraction(MyMoneyFile::instance()->security(m_account.currencyId()));
308     // setup of parameters is done, now do the calculation
309     try {
310       //FIXME: port
311       if (!q->field("loanAmountEditValid").toBool()) {
312         // calculate the amount of the loan out of the other information
313         val = calc.presentValue();
314         ui->m_loanAmountPage->ui->m_loanAmountEdit->setText(MyMoneyMoney(static_cast<double>(val)).abs().formatMoney(fraction));
315         result = i18n("KMyMoney has calculated the amount of the loan as %1.", ui->m_loanAmountPage->ui->m_loanAmountEdit->text());
316 
317       } else if (!q->field("interestRateEditValid").toBool()) {
318         // calculate the interest rate out of the other information
319         val = calc.interestRate();
320 
321         ui->m_interestPage->ui->m_interestRateEdit->setText(MyMoneyMoney(static_cast<double>(val)).abs().formatMoney("", 3));
322         result = i18n("KMyMoney has calculated the interest rate to %1%.", ui->m_interestPage->ui->m_interestRateEdit->text());
323 
324       } else if (!q->field("paymentEditValid").toBool()) {
325         // calculate the periodical amount of the payment out of the other information
326         val = calc.payment();
327         q->setField("paymentEdit", QVariant::fromValue<MyMoneyMoney>(MyMoneyMoney(val).abs()));
328         // reset payment as it might have changed due to rounding
329         val = q->field("paymentEdit").value<MyMoneyMoney>().abs().toDouble();
330         if (q->field("lendButton").toBool())
331           val = -val;
332         calc.setPmt(val);
333 
334         result = i18n("KMyMoney has calculated a periodic payment of %1 to cover principal and interest.", ui->m_paymentPage->ui->m_paymentEdit->text());
335 
336         val = calc.futureValue();
337         if ((q->field("borrowButton").toBool() && val < 0 && qAbs(val) >= qAbs(calc.payment()))
338             || (q->field("lendButton").toBool() && val > 0 && qAbs(val) >= qAbs(calc.payment()))) {
339           calc.setNpp(calc.npp() - 1);
340           ui->m_durationPage->updateTermWidgets(calc.npp());
341           val = calc.futureValue();
342           MyMoneyMoney refVal(static_cast<double>(val));
343           ui->m_finalPaymentPage->ui->m_finalPaymentEdit->setText(refVal.abs().formatMoney(fraction));
344           result += QString(" ");
345           result += i18n("The number of payments has been decremented and the final payment has been modified to %1.", ui->m_finalPaymentPage->ui->m_finalPaymentEdit->text());
346         } else if ((q->field("borrowButton").toBool() && val < 0 && qAbs(val) < qAbs(calc.payment()))
347                    || (q->field("lendButton").toBool() && val > 0 && qAbs(val) < qAbs(calc.payment()))) {
348           ui->m_finalPaymentPage->ui->m_finalPaymentEdit->setText(MyMoneyMoney().formatMoney(fraction));
349         } else {
350           MyMoneyMoney refVal(static_cast<double>(val));
351           ui->m_finalPaymentPage->ui->m_finalPaymentEdit->setText(refVal.abs().formatMoney(fraction));
352           result += i18n("The final payment has been modified to %1.", ui->m_finalPaymentPage->ui->m_finalPaymentEdit->text());
353         }
354 
355       } else if (q->field("durationValueEdit").toInt() == 0) {
356         // calculate the number of payments out of the other information
357         val = calc.numPayments();
358         if (val == 0)
359           throw MYMONEYEXCEPTION_CSTRING("incorrect financial calculation");
360 
361         // if the number of payments has a fractional part, then we
362         // round it to the smallest integer and calculate the balloon payment
363         result = i18n("KMyMoney has calculated the term of your loan as %1. ", ui->m_durationPage->updateTermWidgets(qFloor(val)));
364 
365         if (val != qFloor(val)) {
366           calc.setNpp(qFloor(val));
367           val = calc.futureValue();
368           MyMoneyMoney refVal(static_cast<double>(val));
369           ui->m_finalPaymentPage->ui->m_finalPaymentEdit->setText(refVal.abs().formatMoney(fraction));
370           result += i18n("The final payment has been modified to %1.", ui->m_finalPaymentPage->ui->m_finalPaymentEdit->text());
371         }
372 
373       } else {
374         // calculate the future value of the loan out of the other information
375         val = calc.futureValue();
376 
377         // we differentiate between the following cases:
378         // a) the future value is greater than a payment
379         // b) the future value is less than a payment or the loan is overpaid
380         // c) all other cases
381         //
382         // a) means, we have paid more than we owed. This can't be
383         // b) means, we paid more than we owed but the last payment is
384         //    less in value than regular payments. That means, that the
385         //    future value is to be treated as  (fully payed back)
386         // c) the loan is not payed back yet
387 
388         if ((q->field("borrowButton").toBool() && val < 0 && qAbs(val) > qAbs(calc.payment()))
389             || (q->field("lendButton").toBool() && val > 0 && qAbs(val) > qAbs(calc.payment()))) {
390           // case a)
391           qDebug("Future Value is %f", val);
392           throw MYMONEYEXCEPTION_CSTRING("incorrect financial calculation");
393 
394         } else if ((q->field("borrowButton").toBool() && val < 0 && qAbs(val) <= qAbs(calc.payment()))
395                    || (q->field("lendButton").toBool() && val > 0 && qAbs(val) <= qAbs(calc.payment()))) {
396           // case b)
397           val = 0;
398         }
399 
400         MyMoneyMoney refVal(static_cast<double>(val));
401         result = i18n("KMyMoney has calculated a final payment of %1 for this loan.", refVal.abs().formatMoney(fraction));
402 
403         if (q->field("finalPaymentEditValid").toBool()) {
404           if ((q->field("finalPaymentEdit").value<MyMoneyMoney>().abs() - refVal.abs()).abs().toDouble() > 1) {
405             throw MYMONEYEXCEPTION_CSTRING("incorrect financial calculation");
406           }
407           result = i18n("KMyMoney has successfully verified your loan information.");
408         }
409         //FIXME: port
410         ui->m_finalPaymentPage->ui->m_finalPaymentEdit->setText(refVal.abs().formatMoney(fraction));
411       }
412 
413     } catch (const MyMoneyException &) {
414       KMessageBox::error(0,
415                          i18n("You have entered mis-matching information. Please backup to the "
416                               "appropriate page and update your figures or leave one value empty "
417                               "to let KMyMoney calculate it for you"),
418                          i18n("Calculation error"));
419       return 0;
420     }
421 
422     result += i18n("\n\nAccept this or modify the loan information and recalculate.");
423 
424     KMessageBox::information(0, result, i18n("Calculation successful"));
425     return 1;
426   }
427 
428   /**
429     * This method returns the transaction that is stored within
430     * the schedule. See schedule().
431     *
432     * @return MyMoneyTransaction object to be used within the schedule
433     */
transaction()434   MyMoneyTransaction transaction() const
435   {
436     Q_Q(const KNewLoanWizard);
437     MyMoneyTransaction t;
438     bool hasInterest = !q->field("interestRateEdit").value<MyMoneyMoney>().isZero();
439 
440     MyMoneySplit sPayment, sInterest, sAmortization;
441     // setup accounts. at this point, we cannot fill in the id of the
442     // account that the amortization will be performed on, because we
443     // create the account. So the id is yet unknown. But all others
444     // must exist, otherwise we cannot create a schedule.
445     sPayment.setAccountId(q->field("paymentAccountEdit").toStringList().first());
446     if (!sPayment.accountId().isEmpty()) {
447 
448       //Only create the interest split if not zero
449       if (hasInterest) {
450         sInterest.setAccountId(q->field("interestAccountEdit").toStringList().first());
451         sInterest.setValue(MyMoneyMoney::autoCalc);
452         sInterest.setShares(sInterest.value());
453         sInterest.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::Interest));
454       }
455 
456       // values
457       if (q->field("borrowButton").toBool()) {
458         sPayment.setValue(-q->field("paymentEdit").value<MyMoneyMoney>());
459       } else {
460         sPayment.setValue(q->field("paymentEdit").value<MyMoneyMoney>());
461       }
462 
463       sAmortization.setValue(MyMoneyMoney::autoCalc);
464       // don't forget the shares
465       sPayment.setShares(sPayment.value());
466 
467       sAmortization.setShares(sAmortization.value());
468 
469       // setup the commodity
470       MyMoneyAccount acc = MyMoneyFile::instance()->account(sPayment.accountId());
471       t.setCommodity(acc.currencyId());
472 
473       // actions
474       sPayment.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::Amortization));
475       sAmortization.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::Amortization));
476 
477       // payee
478       QString payeeId = q->field("payeeEdit").toString();
479       sPayment.setPayeeId(payeeId);
480       sAmortization.setPayeeId(payeeId);
481 
482       sAmortization.setAccountId(QStringLiteral("Phony-ID"));
483 
484       // IMPORTANT: Payment split must be the first one, because
485       //            the schedule view expects it this way during display
486       t.addSplit(sPayment);
487       t.addSplit(sAmortization);
488 
489       if (hasInterest) {
490         t.addSplit(sInterest);
491       }
492 
493       // copy the splits from the other costs and update the payment split
494       foreach (const MyMoneySplit& it, m_transaction.splits()) {
495         if (it.accountId() != QStringLiteral("Phony-ID")) {
496           MyMoneySplit sp = it;
497           sp.clearId();
498           t.addSplit(sp);
499           sPayment.setValue(sPayment.value() - sp.value());
500           sPayment.setShares(sPayment.value());
501           t.modifySplit(sPayment);
502         }
503       }
504     }
505     return t;
506   }
507 
loadAccountList()508   void loadAccountList()
509   {
510     Q_Q(KNewLoanWizard);
511     AccountSet interestSet, assetSet;
512 
513     if (q->field("borrowButton").toBool()) {
514       interestSet.addAccountType(eMyMoney::Account::Type::Expense);
515     } else {
516       interestSet.addAccountType(eMyMoney::Account::Type::Income);
517     }
518     if (ui->m_interestCategoryPage)
519       interestSet.load(ui->m_interestCategoryPage->ui->m_interestAccountEdit);
520 
521     assetSet.addAccountType(eMyMoney::Account::Type::Checkings);
522     assetSet.addAccountType(eMyMoney::Account::Type::Savings);
523     assetSet.addAccountType(eMyMoney::Account::Type::Cash);
524     assetSet.addAccountType(eMyMoney::Account::Type::Asset);
525     assetSet.addAccountType(eMyMoney::Account::Type::Currency);
526     if (ui->m_assetAccountPage)
527       assetSet.load(ui->m_assetAccountPage->ui->m_assetAccountEdit);
528 
529     assetSet.addAccountType(eMyMoney::Account::Type::CreditCard);
530     assetSet.addAccountType(eMyMoney::Account::Type::Liability);
531     if (ui->m_schedulePage)
532       assetSet.load(ui->m_schedulePage->ui->m_paymentAccountEdit);
533   }
534 
535   KNewLoanWizard       *q_ptr;
536   Ui::KNewLoanWizard   *ui;
537   MyMoneyAccountLoan    m_account;
538   MyMoneyTransaction    m_transaction;
539   MyMoneySplit          m_split;
540   QBitArray             m_pages;
541 };
542 
543 #endif
544