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