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 }