File indexing completed on 2024-06-23 05:02:59

0001 /*
0002     SPDX-FileCopyrightText: 2017 Łukasz Wojniłowicz <lukasz.wojnilowicz@gmail.com>
0003     SPDX-License-Identifier: GPL-2.0-or-later
0004 */
0005 
0006 #ifndef KNEWLOANWIZARD_P_H
0007 #define KNEWLOANWIZARD_P_H
0008 
0009 #include "knewloanwizard.h"
0010 
0011 // ----------------------------------------------------------------------------
0012 // QT Includes
0013 
0014 #include <QBitArray>
0015 #include <qmath.h>
0016 
0017 // ----------------------------------------------------------------------------
0018 // KDE Includes
0019 
0020 #include <KLocalizedString>
0021 #include <KMessageBox>
0022 
0023 // ----------------------------------------------------------------------------
0024 // Project Includes
0025 
0026 #include "ui_knewloanwizard.h"
0027 #include "ui_namewizardpage.h"
0028 #include "ui_firstpaymentwizardpage.h"
0029 #include "ui_loanamountwizardpage.h"
0030 #include "ui_interestwizardpage.h"
0031 #include "ui_paymenteditwizardpage.h"
0032 #include "ui_finalpaymentwizardpage.h"
0033 #include "ui_interestcategorywizardpage.h"
0034 #include "ui_assetaccountwizardpage.h"
0035 #include "ui_schedulewizardpage.h"
0036 #include "ui_paymentwizardpage.h"
0037 
0038 #include "kmymoneysettings.h"
0039 #include "kmymoneyutils.h"
0040 #include "mymoneyaccountloan.h"
0041 #include "mymoneyenums.h"
0042 #include "mymoneyexception.h"
0043 #include "mymoneyfile.h"
0044 #include "mymoneyfinancialcalculator.h"
0045 #include "mymoneyschedule.h"
0046 #include "mymoneysecurity.h"
0047 #include "mymoneysplit.h"
0048 #include "mymoneytransaction.h"
0049 #include "splitmodel.h"
0050 
0051 namespace Ui {
0052 class KNewLoanWizard;
0053 }
0054 
0055 class KNewLoanWizard;
0056 class KNewLoanWizardPrivate
0057 {
0058     Q_DISABLE_COPY(KNewLoanWizardPrivate)
0059     Q_DECLARE_PUBLIC(KNewLoanWizard)
0060 
0061 public:
0062     explicit KNewLoanWizardPrivate(KNewLoanWizard *qq) :
0063         q_ptr(qq),
0064         ui(new Ui::KNewLoanWizard)
0065     {
0066     }
0067 
0068     ~KNewLoanWizardPrivate()
0069     {
0070         delete ui;
0071     }
0072 
0073     void init()
0074     {
0075         Q_Q(KNewLoanWizard);
0076         ui->setupUi(q);
0077         m_pages = QBitArray(KNewLoanWizard::Page_Summary + 1, true);
0078         q->setModal(true);
0079 
0080         KMyMoneyMVCCombo::setSubstringSearchForChildren(ui->m_namePage, !KMyMoneySettings::stringMatchFromStart());
0081 
0082         // make sure, the back button does not clear fields
0083         q->setOption(QWizard::IndependentPages, true);
0084 
0085         // connect(m_payeeEdit, SIGNAL(newPayee(QString)), this, SLOT(slotNewPayee(QString)));
0086         q->connect(ui->m_namePage->ui->m_payeeEdit, &KMyMoneyMVCCombo::createItem, q, &KNewLoanWizard::slotNewPayee);
0087 
0088         q->connect(ui->m_additionalFeesPage, &AdditionalFeesWizardPage::newCategory, q, &KNewLoanWizard::slotNewCategory);
0089 
0090         q->connect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, q, &KNewLoanWizard::slotReloadEditWidgets);
0091 
0092         resetCalculator();
0093 
0094         q->slotReloadEditWidgets();
0095 
0096         // As default we assume a liability loan, with fixed interest rate,
0097         // with a first payment due on the 30th of this month. All payments
0098         // should be recorded and none have been made so far.
0099 
0100         //FIXME: port
0101         ui->m_firstPaymentPage->ui->m_firstDueDateEdit->setDate(QDate(QDate::currentDate().year(), QDate::currentDate().month(), 30));
0102 
0103         // FIXME: we currently only support interest calculation on reception
0104         m_pages.clearBit(KNewLoanWizard::Page_InterestCalculation);
0105 
0106         // turn off all pages that are contained here for derived classes
0107         m_pages.clearBit(KNewLoanWizard::Page_EditIntro);
0108         m_pages.clearBit(KNewLoanWizard::Page_EditSelection);
0109         m_pages.clearBit(KNewLoanWizard::Page_EffectiveDate);
0110         m_pages.clearBit(KNewLoanWizard::Page_PaymentEdit);
0111         m_pages.clearBit(KNewLoanWizard::Page_InterestEdit);
0112         m_pages.clearBit(KNewLoanWizard::Page_SummaryEdit);
0113 
0114         // for now, we don't have online help :-(
0115         q->setOption(QWizard::HaveHelpButton, false);
0116 
0117         // setup a phony transaction for additional fee processing
0118         m_account = MyMoneyAccount("Phony-ID", MyMoneyAccount());
0119         m_phonySplit.setAccountId(m_account.id());
0120         m_phonySplit.setValue(MyMoneyMoney());
0121         m_additionalFeesTransaction.addSplit(m_phonySplit);
0122 
0123         KMyMoneyUtils::updateWizardButtons(q);
0124     }
0125 
0126     void resetCalculator()
0127     {
0128         Q_Q(KNewLoanWizard);
0129         ui->m_loanAmountPage->resetCalculator();
0130         ui->m_interestPage->resetCalculator();
0131         ui->m_durationPage->resetCalculator();
0132         ui->m_paymentPage->resetCalculator();
0133         ui->m_finalPaymentPage->resetCalculator();
0134 
0135         q->setField("additionalCost", MyMoneyMoney().formatMoney(m_account.fraction(MyMoneyFile::instance()->security(m_account.currencyId()))));
0136     }
0137 
0138     void updateLoanAmount()
0139     {
0140         Q_Q(KNewLoanWizard);
0141         QString txt;
0142         //FIXME: port
0143         if (! q->field("loanAmountEditValid").toBool()) {
0144             txt = QString("<") + i18n("calculate") + QString(">");
0145         } else {
0146             txt = q->field("loanAmountEdit").value<MyMoneyMoney>().formatMoney(m_account.fraction(MyMoneyFile::instance()->security(m_account.currencyId())));
0147         }
0148         q->setField("loanAmount1", txt);
0149         q->setField("loanAmount2", txt);
0150         q->setField("loanAmount3", txt);
0151         q->setField("loanAmount4", txt);
0152         q->setField("loanAmount5", txt);
0153     }
0154 
0155     void updateInterestRate()
0156     {
0157         Q_Q(KNewLoanWizard);
0158         QString txt;
0159         //FIXME: port
0160         if (! q->field("interestRateEditValid").toBool()) {
0161             txt = QString("<") + i18n("calculate") + QString(">");
0162         } else {
0163             txt = q->field("interestRateEdit").value<MyMoneyMoney>().formatMoney(QString(), 3) + QString("%");
0164         }
0165         q->setField("interestRate1", txt);
0166         q->setField("interestRate2", txt);
0167         q->setField("interestRate3", txt);
0168         q->setField("interestRate4", txt);
0169         q->setField("interestRate5", txt);
0170     }
0171 
0172     void updateDuration()
0173     {
0174         Q_Q(KNewLoanWizard);
0175         QString txt;
0176         //FIXME: port
0177         if (q->field("durationValueEdit").toInt() == 0) {
0178             txt = QString("<") + i18n("calculate") + QString(">");
0179         } else {
0180             txt = QStringLiteral("%1 %2").arg(q->field("durationValueEdit").toInt()).arg(q->field("durationUnitEdit").toString());
0181         }
0182         q->setField("duration1", txt);
0183         q->setField("duration2", txt);
0184         q->setField("duration3", txt);
0185         q->setField("duration4", txt);
0186         q->setField("duration5", txt);
0187     }
0188 
0189     void updatePayment()
0190     {
0191         Q_Q(KNewLoanWizard);
0192         QString txt;
0193         //FIXME: port
0194         if (! q->field("paymentEditValid").toBool()) {
0195             txt = QString("<") + i18n("calculate") + QString(">");
0196         } else {
0197             txt = q->field("paymentEdit").value<MyMoneyMoney>().formatMoney(m_account.fraction(MyMoneyFile::instance()->security(m_account.currencyId())));
0198         }
0199         q->setField("payment1", txt);
0200         q->setField("payment2", txt);
0201         q->setField("payment3", txt);
0202         q->setField("payment4", txt);
0203         q->setField("payment5", txt);
0204         q->setField("basePayment", txt);
0205     }
0206 
0207     void updateFinalPayment()
0208     {
0209         Q_Q(KNewLoanWizard);
0210         QString txt;
0211         //FIXME: port
0212         if (! q->field("finalPaymentEditValid").toBool()) {
0213             txt = QString("<") + i18n("calculate") + QString(">");
0214         } else {
0215             txt = q->field("finalPaymentEdit").value<MyMoneyMoney>().formatMoney(m_account.fraction(MyMoneyFile::instance()->security(m_account.currencyId())));
0216         }
0217         q->setField("balloon1", txt);
0218         q->setField("balloon2", txt);
0219         q->setField("balloon3", txt);
0220         q->setField("balloon4", txt);
0221         q->setField("balloon5", txt);
0222     }
0223 
0224     void updateLoanInfo()
0225     {
0226         Q_Q(KNewLoanWizard);
0227         updateLoanAmount();
0228         updateInterestRate();
0229         updateDuration();
0230         updatePayment();
0231         updateFinalPayment();
0232         ui->m_additionalFeesPage->updatePeriodicPayment(m_account);
0233 
0234         QString txt;
0235 
0236         int fraction = m_account.fraction(MyMoneyFile::instance()->security(m_account.currencyId()));
0237         q->setField("loanAmount6", q->field("loanAmountEdit").value<MyMoneyMoney>().formatMoney(fraction));
0238         q->setField("interestRate6", QString(q->field("interestRateEdit").value<MyMoneyMoney>().formatMoney("", 3) + QString("%")));
0239         txt = QStringLiteral("%1 %2").arg(q->field("durationValueEdit").toInt()).arg(q->field("durationUnitEdit").toString());
0240         q->setField("duration6", txt);
0241         q->setField("payment6", q->field("paymentEdit").value<MyMoneyMoney>().formatMoney(fraction));
0242         q->setField("balloon6", q->field("finalPaymentEdit").value<MyMoneyMoney>().formatMoney(fraction));
0243     }
0244 
0245     int calculateLoan()
0246     {
0247         Q_Q(KNewLoanWizard);
0248         MyMoneyFinancialCalculator calc;
0249         double val;
0250         int PF;
0251         QString result;
0252 
0253         // FIXME: for now, we only support interest calculation at the end of the period
0254         calc.setBep();
0255         // FIXME: for now, we only support periodic compounding
0256         calc.setDisc();
0257 
0258         PF = MyMoneySchedule::eventsPerYear(eMyMoney::Schedule::Occurrence(q->field("paymentFrequencyUnitEdit").toInt()));
0259         if (PF == 0)
0260             return 0;
0261         calc.setPF(PF);
0262 
0263         // FIXME: for now we only support compounding frequency == payment frequency
0264         calc.setCF(PF);
0265 
0266         if (q->field("loanAmountEditValid").toBool()) {
0267             val = q->field("loanAmountEdit").value<MyMoneyMoney>().abs().toDouble();
0268             if (q->field("borrowButton").toBool())
0269                 val = -val;
0270             calc.setPv(val);
0271         }
0272 
0273         if (q->field("interestRateEditValid").toBool()) {
0274             val = q->field("interestRateEdit").value<MyMoneyMoney>().abs().toDouble();
0275             calc.setIr(val);
0276         }
0277 
0278         if (q->field("paymentEditValid").toBool()) {
0279             val = q->field("paymentEdit").value<MyMoneyMoney>().abs().toDouble();
0280             if (q->field("lendButton").toBool())
0281                 val = -val;
0282             calc.setPmt(val);
0283         }
0284 
0285         if (q->field("finalPaymentEditValid").toBool()) {
0286             val = q->field("finalPaymentEditValid").value<MyMoneyMoney>().abs().toDouble();
0287             if (q->field("lendButton").toBool())
0288                 val = -val;
0289             calc.setFv(val);
0290         }
0291 
0292         if (q->field("durationValueEdit").toInt() != 0) {
0293             calc.setNpp(ui->m_durationPage->term());
0294         }
0295 
0296         int fraction = m_account.fraction(MyMoneyFile::instance()->security(m_account.currencyId()));
0297         // setup of parameters is done, now do the calculation
0298         try {
0299             //FIXME: port
0300             if (!q->field("loanAmountEditValid").toBool()) {
0301                 // calculate the amount of the loan out of the other information
0302                 val = calc.presentValue();
0303                 ui->m_loanAmountPage->ui->m_loanAmountEdit->setText(MyMoneyMoney(static_cast<double>(val)).abs().formatMoney(fraction));
0304                 result = i18n("KMyMoney has calculated the amount of the loan as %1.", ui->m_loanAmountPage->ui->m_loanAmountEdit->text());
0305 
0306             } else if (!q->field("interestRateEditValid").toBool()) {
0307                 // calculate the interest rate out of the other information
0308                 val = calc.interestRate();
0309 
0310                 ui->m_interestPage->ui->m_interestRateEdit->setText(MyMoneyMoney(static_cast<double>(val)).abs().formatMoney("", 3));
0311                 result = i18n("KMyMoney has calculated the interest rate to %1%.", ui->m_interestPage->ui->m_interestRateEdit->text());
0312 
0313             } else if (!q->field("paymentEditValid").toBool()) {
0314                 // calculate the periodical amount of the payment out of the other information
0315                 val = calc.payment();
0316                 q->setField("paymentEdit", QVariant::fromValue<MyMoneyMoney>(MyMoneyMoney(val).abs()));
0317                 // reset payment as it might have changed due to rounding
0318                 val = q->field("paymentEdit").value<MyMoneyMoney>().abs().toDouble();
0319                 if (q->field("lendButton").toBool())
0320                     val = -val;
0321                 calc.setPmt(val);
0322 
0323                 result = i18n("KMyMoney has calculated a periodic payment of %1 to cover principal and interest.", ui->m_paymentPage->ui->m_paymentEdit->text());
0324 
0325                 val = calc.futureValue();
0326                 if ((q->field("borrowButton").toBool() && val < 0 && qAbs(val) >= qAbs(calc.payment()))
0327                         || (q->field("lendButton").toBool() && val > 0 && qAbs(val) >= qAbs(calc.payment()))) {
0328                     calc.setNpp(calc.npp() - 1);
0329                     ui->m_durationPage->updateTermWidgets(calc.npp());
0330                     val = calc.futureValue();
0331                     MyMoneyMoney refVal(static_cast<double>(val));
0332                     ui->m_finalPaymentPage->ui->m_finalPaymentEdit->setText(refVal.abs().formatMoney(fraction));
0333                     result += QString(" ");
0334                     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());
0335                 } else if ((q->field("borrowButton").toBool() && val < 0 && qAbs(val) < qAbs(calc.payment()))
0336                            || (q->field("lendButton").toBool() && val > 0 && qAbs(val) < qAbs(calc.payment()))) {
0337                     ui->m_finalPaymentPage->ui->m_finalPaymentEdit->setText(MyMoneyMoney().formatMoney(fraction));
0338                 } else {
0339                     MyMoneyMoney refVal(static_cast<double>(val));
0340                     ui->m_finalPaymentPage->ui->m_finalPaymentEdit->setText(refVal.abs().formatMoney(fraction));
0341                     result += i18n("The final payment has been modified to %1.", ui->m_finalPaymentPage->ui->m_finalPaymentEdit->text());
0342                 }
0343 
0344             } else if (q->field("durationValueEdit").toInt() == 0) {
0345                 // calculate the number of payments out of the other information
0346                 val = calc.numPayments();
0347                 if (val == 0)
0348                     throw MYMONEYEXCEPTION_CSTRING("incorrect financial calculation");
0349 
0350                 // if the number of payments has a fractional part, then we
0351                 // round it to the smallest integer and calculate the balloon payment
0352                 result = i18n("KMyMoney has calculated the term of your loan as %1. ", ui->m_durationPage->updateTermWidgets(qFloor(val)));
0353 
0354                 if (val != qFloor(val)) {
0355                     calc.setNpp(qFloor(val));
0356                     val = calc.futureValue();
0357                     MyMoneyMoney refVal(static_cast<double>(val));
0358                     ui->m_finalPaymentPage->ui->m_finalPaymentEdit->setText(refVal.abs().formatMoney(fraction));
0359                     result += i18n("The final payment has been modified to %1.", ui->m_finalPaymentPage->ui->m_finalPaymentEdit->text());
0360                 }
0361 
0362             } else {
0363                 // calculate the future value of the loan out of the other information
0364                 val = calc.futureValue();
0365 
0366                 // we differentiate between the following cases:
0367                 // a) the future value is greater than a payment
0368                 // b) the future value is less than a payment or the loan is overpaid
0369                 // c) all other cases
0370                 //
0371                 // a) means, we have paid more than we owed. This can't be
0372                 // b) means, we paid more than we owed but the last payment is
0373                 //    less in value than regular payments. That means, that the
0374                 //    future value is to be treated as  (fully paid back)
0375                 // c) the loan is not paid back yet
0376 
0377                 if ((q->field("borrowButton").toBool() && val < 0 && qAbs(val) > qAbs(calc.payment()))
0378                         || (q->field("lendButton").toBool() && val > 0 && qAbs(val) > qAbs(calc.payment()))) {
0379                     // case a)
0380                     qDebug("Future Value is %f", val);
0381                     throw MYMONEYEXCEPTION_CSTRING("incorrect financial calculation");
0382 
0383                 } else if ((q->field("borrowButton").toBool() && val < 0 && qAbs(val) <= qAbs(calc.payment()))
0384                            || (q->field("lendButton").toBool() && val > 0 && qAbs(val) <= qAbs(calc.payment()))) {
0385                     // case b)
0386                     val = 0;
0387                 }
0388 
0389                 MyMoneyMoney refVal(static_cast<double>(val));
0390                 result = i18n("KMyMoney has calculated a final payment of %1 for this loan.", refVal.abs().formatMoney(fraction));
0391 
0392                 if (q->field("finalPaymentEditValid").toBool()) {
0393                     if ((q->field("finalPaymentEdit").value<MyMoneyMoney>().abs() - refVal.abs()).abs().toDouble() > 1) {
0394                         throw MYMONEYEXCEPTION_CSTRING("incorrect financial calculation");
0395                     }
0396                     result = i18n("KMyMoney has successfully verified your loan information.");
0397                 }
0398                 //FIXME: port
0399                 ui->m_finalPaymentPage->ui->m_finalPaymentEdit->setText(refVal.abs().formatMoney(fraction));
0400             }
0401 
0402         } catch (const MyMoneyException &) {
0403             KMessageBox::error(0,
0404                                i18n("You have entered mis-matching information. Please backup to the "
0405                                     "appropriate page and update your figures or leave one value empty "
0406                                     "to let KMyMoney calculate it for you"),
0407                                i18n("Calculation error"));
0408             return 0;
0409         }
0410 
0411         result += i18n("\n\nAccept this or modify the loan information and recalculate.");
0412 
0413         KMessageBox::information(0, result, i18n("Calculation successful"));
0414         return 1;
0415     }
0416 
0417     /**
0418       * This method returns the transaction that is stored within
0419       * the schedule. See schedule().
0420       *
0421       * @return MyMoneyTransaction object to be used within the schedule
0422       */
0423     MyMoneyTransaction transaction() const
0424     {
0425         Q_Q(const KNewLoanWizard);
0426         MyMoneyTransaction t;
0427         bool hasInterest = !q->field("interestRateEdit").value<MyMoneyMoney>().isZero();
0428 
0429         MyMoneySplit sPayment, sInterest, sAmortization;
0430         // setup accounts. at this point, we cannot fill in the id of the
0431         // account that the amortization will be performed on, because we
0432         // create the account. So the id is yet unknown. But all others
0433         // must exist, otherwise we cannot create a schedule.
0434         sPayment.setAccountId(q->field("paymentAccountEdit").toStringList().first());
0435         if (!sPayment.accountId().isEmpty()) {
0436 
0437             //Only create the interest split if not zero
0438             if (hasInterest) {
0439                 sInterest.setAccountId(q->field("interestAccountEdit").toStringList().first());
0440                 sInterest.setValue(MyMoneyMoney::autoCalc);
0441                 sInterest.setShares(sInterest.value());
0442                 sInterest.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::Interest));
0443             }
0444 
0445             // values
0446             if (q->field("borrowButton").toBool()) {
0447                 sPayment.setValue(-q->field("paymentEdit").value<MyMoneyMoney>());
0448             } else {
0449                 sPayment.setValue(q->field("paymentEdit").value<MyMoneyMoney>());
0450             }
0451 
0452             sAmortization.setValue(MyMoneyMoney::autoCalc);
0453             // don't forget the shares
0454             sPayment.setShares(sPayment.value());
0455 
0456             sAmortization.setShares(sAmortization.value());
0457 
0458             // setup the commodity
0459             MyMoneyAccount acc = MyMoneyFile::instance()->account(sPayment.accountId());
0460             t.setCommodity(acc.currencyId());
0461 
0462             // actions
0463             sPayment.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::Amortization));
0464             sAmortization.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::Amortization));
0465 
0466             // payee
0467             QString payeeId = q->field("payeeEdit").toString();
0468             sPayment.setPayeeId(payeeId);
0469             sAmortization.setPayeeId(payeeId);
0470 
0471             sAmortization.setAccountId(QStringLiteral("Phony-ID"));
0472 
0473             // IMPORTANT: Payment split must be the first one, because
0474             //            the schedule view expects it this way during display
0475             t.addSplit(sPayment);
0476             t.addSplit(sAmortization);
0477 
0478             if (hasInterest) {
0479                 t.addSplit(sInterest);
0480             }
0481 
0482             // copy the splits from the other costs and update the payment split
0483             m_feeSplitModel.addSplitsToTransaction(t);
0484 
0485             // and subtract the sum from the payment value
0486             sPayment.setValue(sPayment.value() - m_feeSplitModel.valueSum());
0487             sPayment.setShares(sPayment.value());
0488             t.modifySplit(sPayment);
0489         }
0490         return t;
0491     }
0492 
0493     void loadAccountList()
0494     {
0495         Q_Q(KNewLoanWizard);
0496         AccountSet interestSet, assetSet;
0497 
0498         if (q->field("borrowButton").toBool()) {
0499             interestSet.addAccountType(eMyMoney::Account::Type::Expense);
0500         } else {
0501             interestSet.addAccountType(eMyMoney::Account::Type::Income);
0502         }
0503         if (ui->m_interestCategoryPage)
0504             interestSet.load(ui->m_interestCategoryPage->ui->m_interestAccountEdit);
0505 
0506         assetSet.addAccountType(eMyMoney::Account::Type::Checkings);
0507         assetSet.addAccountType(eMyMoney::Account::Type::Savings);
0508         assetSet.addAccountType(eMyMoney::Account::Type::Cash);
0509         assetSet.addAccountType(eMyMoney::Account::Type::Asset);
0510         assetSet.addAccountType(eMyMoney::Account::Type::Currency);
0511         if (ui->m_assetAccountPage)
0512             assetSet.load(ui->m_assetAccountPage->ui->m_assetAccountEdit);
0513 
0514         assetSet.addAccountType(eMyMoney::Account::Type::CreditCard);
0515         assetSet.addAccountType(eMyMoney::Account::Type::Liability);
0516         if (ui->m_schedulePage)
0517             assetSet.load(ui->m_schedulePage->ui->m_paymentAccountEdit);
0518     }
0519 
0520     KNewLoanWizard* q_ptr;
0521     Ui::KNewLoanWizard* ui;
0522     SplitModel m_feeSplitModel;
0523     MyMoneyAccountLoan m_account;
0524     MyMoneyTransaction m_additionalFeesTransaction;
0525     MyMoneySplit m_phonySplit;
0526     QBitArray m_pages;
0527 };
0528 
0529 #endif