File indexing completed on 2024-06-09 05:03:29

0001 /*
0002     SPDX-FileCopyrightText: 2007 Thomas Baumgart <ipwizard@users.sourceforge.net>
0003     SPDX-FileCopyrightText: 2017 Łukasz Wojniłowicz <lukasz.wojnilowicz@gmail.com>
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "kloandetailspage.h"
0008 #include "kloandetailspage_p.h"
0009 
0010 #include <qmath.h>
0011 
0012 // ----------------------------------------------------------------------------
0013 // QT Includes
0014 
0015 #include <QPushButton>
0016 #include <QSpinBox>
0017 
0018 // ----------------------------------------------------------------------------
0019 // KDE Includes
0020 
0021 #include <KComboBox>
0022 #include <KLineEdit>
0023 #include <KLocalizedString>
0024 #include <KMessageBox>
0025 
0026 // ----------------------------------------------------------------------------
0027 // Project Includes
0028 
0029 #include "ui_kgeneralloaninfopage.h"
0030 #include "ui_kloandetailspage.h"
0031 
0032 #include "knewaccountwizard.h"
0033 #include "knewaccountwizard_p.h"
0034 #include "kgeneralloaninfopage.h"
0035 #include "kgeneralloaninfopage_p.h"
0036 #include "kloanpaymentpage.h"
0037 #include "mymoneyenums.h"
0038 #include "mymoneyexception.h"
0039 #include "mymoneyfinancialcalculator.h"
0040 #include "mymoneymoney.h"
0041 #include "wizardpage.h"
0042 #include "kguiutils.h"
0043 
0044 using namespace eMyMoney;
0045 
0046 namespace NewAccountWizard
0047 {
0048 LoanDetailsPage::LoanDetailsPage(Wizard* wizard) :
0049     QWidget(wizard),
0050     WizardPage<Wizard>(*new LoanDetailsPagePrivate(wizard), StepPayments, this, wizard)
0051 {
0052     Q_D(LoanDetailsPage);
0053     d->m_needCalculate = true;
0054     d->ui->setupUi(this);
0055     // force the balloon payment to zero (default)
0056     d->ui->m_balloonAmount->setValue(MyMoneyMoney());
0057     // allow any precision for the interest rate
0058     d->ui->m_interestRate->setPrecision(-1);
0059 
0060     connect(d->ui->m_paymentDue, static_cast<void (QComboBox::*)(int)>(&QComboBox::activated), this, &LoanDetailsPage::slotValuesChanged);
0061 
0062     connect(d->ui->m_termAmount, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), this, &LoanDetailsPage::slotValuesChanged);
0063     connect(d->ui->m_termUnit, static_cast<void (QComboBox::*)(int)>(&QComboBox::highlighted), this, &LoanDetailsPage::slotValuesChanged);
0064     connect(d->ui->m_loanAmount, &AmountEdit::textChanged, this, &LoanDetailsPage::slotValuesChanged);
0065     connect(d->ui->m_interestRate, &AmountEdit::textChanged, this, &LoanDetailsPage::slotValuesChanged);
0066     connect(d->ui->m_paymentAmount, &AmountEdit::textChanged, this, &LoanDetailsPage::slotValuesChanged);
0067     connect(d->ui->m_balloonAmount, &AmountEdit::textChanged, this, &LoanDetailsPage::slotValuesChanged);
0068 
0069     d->ui->m_loanAmount->setAllowEmpty();
0070     d->ui->m_interestRate->setAllowEmpty();
0071     d->ui->m_paymentAmount->setAllowEmpty();
0072     d->ui->m_balloonAmount->setAllowEmpty();
0073 
0074     connect(d->ui->m_calculateButton, &QAbstractButton::clicked, this, &LoanDetailsPage::slotCalculate);
0075 }
0076 
0077 LoanDetailsPage::~LoanDetailsPage()
0078 {
0079 }
0080 
0081 void LoanDetailsPage::enterPage()
0082 {
0083     Q_D(LoanDetailsPage);
0084     // we need to remove a bunch of entries of the payment frequencies
0085     d->ui->m_termUnit->clear();
0086 
0087     d->m_mandatoryGroup->clear();
0088     if (!d->m_wizard->openingBalance().isZero()) {
0089         d->m_mandatoryGroup->add(d->ui->m_loanAmount);
0090         if (d->ui->m_loanAmount->text().length() == 0) {
0091             d->ui->m_loanAmount->setValue(d->m_wizard->openingBalance().abs());
0092         }
0093     }
0094 
0095     switch (d->m_wizard->d_func()->m_generalLoanInfoPage->d_func()->ui->m_paymentFrequency->currentItem()) {
0096     default:
0097         d->ui->m_termUnit->insertItem(i18n("Payments"), (int)Schedule::Occurrence::Once);
0098         d->ui->m_termUnit->setCurrentItem((int)Schedule::Occurrence::Once);
0099         break;
0100     case Schedule::Occurrence::Monthly:
0101         d->ui->m_termUnit->insertItem(i18n("Months"), (int)Schedule::Occurrence::Monthly);
0102         d->ui->m_termUnit->insertItem(i18n("Years"), (int)Schedule::Occurrence::Yearly);
0103         d->ui->m_termUnit->setCurrentItem((int)Schedule::Occurrence::Monthly);
0104         break;
0105     case Schedule::Occurrence::Yearly:
0106         d->ui->m_termUnit->insertItem(i18n("Years"), (int)Schedule::Occurrence::Yearly);
0107         d->ui->m_termUnit->setCurrentItem((int)Schedule::Occurrence::Yearly);
0108         break;
0109     }
0110 }
0111 
0112 void LoanDetailsPage::slotValuesChanged()
0113 {
0114     Q_D(LoanDetailsPage);
0115     d->m_needCalculate = true;
0116     d->m_wizard->completeStateChanged();
0117 }
0118 
0119 void LoanDetailsPage::slotCalculate()
0120 {
0121     Q_D(LoanDetailsPage);
0122     MyMoneyFinancialCalculator calc;
0123     MyMoneyMoney val;
0124     int PF, CF;
0125     QString result;
0126     bool moneyBorrowed = d->m_wizard->moneyBorrowed();
0127     bool moneyLend = !moneyBorrowed;
0128 
0129     // FIXME: for now, we only support interest calculation at the end of the period
0130     calc.setBep();
0131     // FIXME: for now, we only support periodic compounding
0132     calc.setDisc();
0133 
0134     PF = d->m_wizard->d_func()->m_generalLoanInfoPage->d_func()->ui->m_paymentFrequency->eventsPerYear();
0135     CF = d->m_wizard->d_func()->m_generalLoanInfoPage->d_func()->ui->m_compoundFrequency->eventsPerYear();
0136 
0137     if (PF == 0 || CF == 0)
0138         return;
0139 
0140     calc.setPF(PF);
0141     calc.setCF(CF);
0142 
0143 
0144     if (!d->ui->m_loanAmount->text().isEmpty()) {
0145         val = d->ui->m_loanAmount->value().abs();
0146         if (moneyBorrowed)
0147             val = -val;
0148         calc.setPv(val.toDouble());
0149     }
0150 
0151     if (!d->ui->m_interestRate->text().isEmpty()) {
0152         val = d->ui->m_interestRate->value().abs();
0153         calc.setIr(val.toDouble());
0154     }
0155 
0156     if (!d->ui->m_paymentAmount->text().isEmpty()) {
0157         val = d->ui->m_paymentAmount->value().abs();
0158         if (moneyLend)
0159             val = -val;
0160         calc.setPmt(val.toDouble());
0161     }
0162 
0163     if (!d->ui->m_balloonAmount->text().isEmpty()) {
0164         val = d->ui->m_balloonAmount->value().abs();
0165         if (moneyLend)
0166             val = -val;
0167         calc.setFv(val.toDouble());
0168     }
0169 
0170     if (d->ui->m_termAmount->value() != 0) {
0171         calc.setNpp(term());
0172     }
0173 
0174     // setup of parameters is done, now do the calculation
0175     try {
0176         if (d->ui->m_loanAmount->text().isEmpty()) {
0177             // calculate the amount of the loan out of the other information
0178             val = MyMoneyMoney(calc.presentValue());
0179             d->ui->m_loanAmount->setValue(val);
0180             result = i18n("KMyMoney has calculated the amount of the loan as %1.", d->ui->m_loanAmount->text());
0181 
0182         } else if (d->ui->m_interestRate->text().isEmpty()) {
0183             // calculate the interest rate out of the other information
0184             val = MyMoneyMoney(calc.interestRate());
0185 
0186             d->ui->m_interestRate->setValue(val);
0187             result = i18n("KMyMoney has calculated the interest rate to %1%.", d->ui->m_interestRate->text());
0188 
0189         } else if (d->ui->m_paymentAmount->text().isEmpty()) {
0190             // calculate the periodical amount of the payment out of the other information
0191             val = MyMoneyMoney(calc.payment());
0192             d->ui->m_paymentAmount->setValue(val.abs());
0193             // reset payment as it might have changed due to rounding
0194             val = d->ui->m_paymentAmount->value().abs();
0195             if (moneyLend)
0196                 val = -val;
0197             calc.setPmt(val.toDouble());
0198 
0199             result = i18n("KMyMoney has calculated a periodic payment of %1 to cover principal and interest.", d->ui->m_paymentAmount->text());
0200 
0201             val = MyMoneyMoney(calc.futureValue());
0202             if ((moneyBorrowed && val < MyMoneyMoney() && qAbs(val.toDouble()) >= qAbs(calc.payment()))
0203                     || (moneyLend && val > MyMoneyMoney() && qAbs(val.toDouble()) >= qAbs(calc.payment()))) {
0204                 calc.setNpp(calc.npp() - 1);
0205                 // updateTermWidgets(calc.npp());
0206                 val = MyMoneyMoney(calc.futureValue());
0207                 MyMoneyMoney refVal(val);
0208                 d->ui->m_balloonAmount->setValue(refVal);
0209                 result += QString(" ");
0210                 result += i18n("The number of payments has been decremented and the balloon payment has been modified to %1.", d->ui->m_balloonAmount->text());
0211             } else if ((moneyBorrowed && val < MyMoneyMoney() && qAbs(val.toDouble()) < qAbs(calc.payment()))
0212                        || (moneyLend && val > MyMoneyMoney() && qAbs(val.toDouble()) < qAbs(calc.payment()))) {
0213                 d->ui->m_balloonAmount->setValue(MyMoneyMoney());
0214             } else {
0215                 MyMoneyMoney refVal(val);
0216                 d->ui->m_balloonAmount->setValue(refVal);
0217                 result += i18n("The balloon payment has been modified to %1.", d->ui->m_balloonAmount->text());
0218             }
0219 
0220         } else if (d->ui->m_termAmount->value() == 0) {
0221             // calculate the number of payments out of the other information
0222             val = MyMoneyMoney(calc.numPayments());
0223             if (val == 0)
0224                 throw MYMONEYEXCEPTION_CSTRING("incorrect financial calculation");
0225 
0226             // if the number of payments has a fractional part, then we
0227             // round it to the smallest integer and calculate the balloon payment
0228             result = i18n("KMyMoney has calculated the term of your loan as %1. ", updateTermWidgets(qFloor(val.toDouble())));
0229 
0230             if (val.toDouble() != qFloor(val.toDouble())) {
0231                 calc.setNpp(qFloor(val.toDouble()));
0232                 val = MyMoneyMoney(calc.futureValue());
0233                 d->ui->m_balloonAmount->setValue(val);
0234                 result += i18n("The balloon payment has been modified to %1.", d->ui->m_balloonAmount->text());
0235             }
0236 
0237         } else {
0238             // calculate the future value of the loan out of the other information
0239             val = MyMoneyMoney(calc.futureValue());
0240 
0241             // we differentiate between the following cases:
0242             // a) the future value is greater than a payment
0243             // b) the future value is less than a payment or the loan is overpaid
0244             // c) all other cases
0245             //
0246             // a) means, we have paid more than we owed. This can't be
0247             // b) means, we paid more than we owed but the last payment is
0248             //    less in value than regular payments. That means, that the
0249             //    future value is to be treated as  (fully paid back)
0250             // c) the loan is not paid back yet
0251             if ((moneyBorrowed && val < MyMoneyMoney() && qAbs(val.toDouble()) > qAbs(calc.payment()))
0252                     || (moneyLend && val > MyMoneyMoney() && qAbs(val.toDouble()) > qAbs(calc.payment()))) {
0253                 // case a)
0254                 qDebug("Future Value is %f", val.toDouble());
0255                 throw MYMONEYEXCEPTION_CSTRING("incorrect financial calculation");
0256 
0257             } else if ((moneyBorrowed && val < MyMoneyMoney() && qAbs(val.toDouble()) <= qAbs(calc.payment()))
0258                        || (moneyLend && val > MyMoneyMoney() && qAbs(val.toDouble()) <= qAbs(calc.payment()))) {
0259                 // case b)
0260                 val = 0;
0261             }
0262 
0263             result = i18n("KMyMoney has calculated a balloon payment of %1 for this loan.", val.abs().formatMoney(QString(), d->m_wizard->d_func()->precision()));
0264 
0265             if (!d->ui->m_balloonAmount->text().isEmpty()) {
0266                 if ((d->ui->m_balloonAmount->value().abs() - val.abs()).abs().toDouble() > 1) {
0267                     throw MYMONEYEXCEPTION_CSTRING("incorrect financial calculation");
0268                 }
0269                 result = i18n("KMyMoney has successfully verified your loan information.");
0270             }
0271             d->ui->m_balloonAmount->setValue(val);
0272         }
0273 
0274     } catch (const MyMoneyException &) {
0275         KMessageBox::error(0,
0276                            i18n("You have entered mis-matching information. Please modify "
0277                                 "your figures or leave one value empty "
0278                                 "to let KMyMoney calculate it for you"),
0279                            i18n("Calculation error"));
0280         return;
0281     }
0282 
0283     result += i18n("\n\nAccept this or modify the loan information and recalculate.");
0284 
0285     KMessageBox::information(0, result, i18n("Calculation successful"));
0286     d->m_needCalculate = false;
0287 
0288     // now update change
0289     d->m_wizard->completeStateChanged();
0290 }
0291 
0292 int LoanDetailsPage::term() const
0293 {
0294     Q_D(const LoanDetailsPage);
0295     int factor = 0;
0296 
0297     if (d->ui->m_termAmount->value() != 0) {
0298         factor = 1;
0299         switch (d->ui->m_termUnit->currentItem()) {
0300         case Schedule::Occurrence::Yearly: // years
0301             factor = 12;
0302         // intentional fall through
0303 
0304         case Schedule::Occurrence::Monthly: // months
0305             factor *= 30;
0306             factor *= d->ui->m_termAmount->value();
0307             // factor now is the duration in days. we divide this by the
0308             // payment frequency and get the number of payments
0309             factor /= d->m_wizard->d_func()->m_generalLoanInfoPage->d_func()->ui->m_paymentFrequency->daysBetweenEvents();
0310             break;
0311 
0312         default:
0313             qDebug("Unknown term unit %d in LoanDetailsPage::term(). Using payments.", (int)d->ui->m_termUnit->currentItem());
0314         // intentional fall through
0315 
0316         case Schedule::Occurrence::Once: // payments
0317             factor = d->ui->m_termAmount->value();
0318             break;
0319         }
0320     }
0321     return factor;
0322 }
0323 
0324 QString LoanDetailsPage::updateTermWidgets(const double val)
0325 {
0326     Q_D(LoanDetailsPage);
0327     long vl = qFloor(val);
0328 
0329     QString valString;
0330     Schedule::Occurrence unit = d->ui->m_termUnit->currentItem();
0331 
0332     if ((unit == Schedule::Occurrence::Monthly)
0333             && ((vl % 12) == 0)) {
0334         vl /= 12;
0335         unit = Schedule::Occurrence::Yearly;
0336     }
0337 
0338     switch (unit) {
0339     case Schedule::Occurrence::Monthly:
0340         valString = i18np("one month", "%1 months", vl);
0341         d->ui->m_termUnit->setCurrentItem((int)Schedule::Occurrence::Monthly);
0342         break;
0343     case Schedule::Occurrence::Yearly:
0344         valString = i18np("one year", "%1 years", vl);
0345         d->ui->m_termUnit->setCurrentItem((int)Schedule::Occurrence::Yearly);
0346         break;
0347     default:
0348         valString = i18np("one payment", "%1 payments", vl);
0349         d->ui->m_termUnit->setCurrentItem((int)Schedule::Occurrence::Once);
0350         break;
0351     }
0352     d->ui->m_termAmount->setValue(vl);
0353     return valString;
0354 }
0355 
0356 bool LoanDetailsPage::isComplete() const
0357 {
0358     Q_D(const LoanDetailsPage);
0359     // bool rc = KMyMoneyWizardPage::isComplete();
0360 
0361     int fieldCnt = 0;
0362 
0363     if (d->ui->m_loanAmount->text().length() > 0) {
0364         fieldCnt++;
0365     }
0366 
0367     if (d->ui->m_interestRate->text().length() > 0) {
0368         fieldCnt++;
0369     }
0370 
0371     if (d->ui->m_termAmount->value() != 0) {
0372         fieldCnt++;
0373     }
0374 
0375     if (d->ui->m_paymentAmount->text().length() > 0) {
0376         fieldCnt++;
0377     }
0378 
0379     if (d->ui->m_balloonAmount->text().length() > 0) {
0380         fieldCnt++;
0381     }
0382 
0383     d->ui->m_calculateButton->setEnabled(fieldCnt == 4 || (fieldCnt == 5 && d->m_needCalculate));
0384 
0385     d->ui->m_calculateButton->setAutoDefault(false);
0386     d->ui->m_calculateButton->setDefault(false);
0387     if (d->m_needCalculate && fieldCnt == 4) {
0388         d->m_wizard->d_func()->m_nextButton->setToolTip(i18n("Press Calculate to verify the values"));
0389         d->ui->m_calculateButton->setAutoDefault(true);
0390         d->ui->m_calculateButton->setDefault(true);
0391     } else if (fieldCnt != 5) {
0392         d->m_wizard->d_func()->m_nextButton->setToolTip(i18n("Not all details supplied"));
0393         d->ui->m_calculateButton->setAutoDefault(true);
0394         d->ui->m_calculateButton->setDefault(true);
0395     }
0396     d->m_wizard->d_func()->m_nextButton->setAutoDefault(!d->ui->m_calculateButton->autoDefault());
0397     d->m_wizard->d_func()->m_nextButton->setDefault(!d->ui->m_calculateButton->autoDefault());
0398 
0399     return (fieldCnt == 5) && !d->m_needCalculate;
0400 }
0401 
0402 QWidget* LoanDetailsPage::initialFocusWidget() const
0403 {
0404     Q_D(const LoanDetailsPage);
0405     return d->ui->m_paymentDue;
0406 }
0407 
0408 KMyMoneyWizardPage* LoanDetailsPage::nextPage() const
0409 {
0410     Q_D(const LoanDetailsPage);
0411     return d->m_wizard->d_func()->m_loanPaymentPage;
0412 }
0413 }