157 lines
6.2 KiB
Python
157 lines
6.2 KiB
Python
from dateutil.relativedelta import relativedelta
|
|
|
|
from odoo import models, fields, _, api
|
|
from odoo.tools.misc import format_date
|
|
from odoo.exceptions import ValidationError
|
|
|
|
from ..lib import pyloan
|
|
|
|
|
|
class AccountLoanComputeWizard(models.TransientModel):
|
|
_name = 'account.loan.compute.wizard'
|
|
_description = 'Loan Compute Wizard'
|
|
|
|
loan_id = fields.Many2one(
|
|
comodel_name='account.loan',
|
|
string='Loan',
|
|
required=True,
|
|
)
|
|
currency_id = fields.Many2one(related='loan_id.currency_id')
|
|
loan_amount = fields.Monetary(
|
|
string='Loan Amount',
|
|
required=True,
|
|
)
|
|
interest_rate = fields.Float(
|
|
string='Interest Rate',
|
|
default=1.0,
|
|
required=True,
|
|
digits=(12, 10),
|
|
min_display_digits=2,
|
|
)
|
|
loan_term = fields.Integer(
|
|
string='Loan Term',
|
|
default=1,
|
|
required=True,
|
|
)
|
|
start_date = fields.Date(
|
|
string='Start Date',
|
|
required=True,
|
|
default=fields.Date.context_today,
|
|
)
|
|
first_payment_date = fields.Date(
|
|
string='First Payment',
|
|
required=True,
|
|
default=lambda self: fields.Date.context_today(self).replace(day=1) + relativedelta(months=1), # first day of next month
|
|
)
|
|
payment_end_of_month = fields.Selection(
|
|
string='Payment',
|
|
selection=[
|
|
('end_of_month', 'End of Month'),
|
|
('at_anniversary', 'At Anniversary'),
|
|
],
|
|
default='end_of_month',
|
|
required=True,
|
|
)
|
|
compounding_method = fields.Selection(
|
|
string='Compounding Method',
|
|
selection=[
|
|
('30A/360', '30A/360'),
|
|
('30U/360', '30U/360'),
|
|
('30E/360', '30E/360'),
|
|
('30E/360 ISDA', '30E/360 ISDA'),
|
|
('A/360', 'A/360'),
|
|
('A/365F', 'A/365F'),
|
|
('A/A ISDA', 'A/A ISDA'),
|
|
('A/A AFB', 'A/A AFB'),
|
|
],
|
|
default='30E/360',
|
|
required=True,
|
|
)
|
|
preview = fields.Text(compute='_compute_preview')
|
|
|
|
# Onchange
|
|
@api.onchange('loan_amount', 'interest_rate', 'loan_term', 'start_date', 'first_payment_date')
|
|
def _onchange_preview(self):
|
|
if self.loan_amount < 0:
|
|
raise ValidationError(_("Loan Amount must be positive"))
|
|
if self.interest_rate < 0 or self.interest_rate > 100:
|
|
raise ValidationError(_("Interest Rate must be between 0 and 100"))
|
|
if self.loan_term < 0:
|
|
raise ValidationError(_("Loan Term must be positive"))
|
|
if self.first_payment_date and self.start_date and self.start_date + relativedelta(years=self.loan_term) < self.first_payment_date:
|
|
raise ValidationError(_("The First Payment Date must be before the end of the loan."))
|
|
|
|
@api.onchange('start_date')
|
|
def _onchange_start_date(self):
|
|
self.first_payment_date = self.start_date and self.start_date.replace(day=1) + relativedelta(months=1) # first day of next month
|
|
|
|
# Compute
|
|
def _get_loan_payment_schedule(self):
|
|
self.ensure_one()
|
|
loan = pyloan.Loan(
|
|
loan_amount=self.loan_amount,
|
|
interest_rate=self.interest_rate,
|
|
loan_term=self.loan_term,
|
|
start_date=format_date(self.env, self.start_date, date_format='yyyy-MM-dd'),
|
|
first_payment_date=format_date(self.env, self.first_payment_date, date_format='yyyy-MM-dd') if self.first_payment_date and self.payment_end_of_month == 'at_anniversary' else None,
|
|
payment_end_of_month=self.payment_end_of_month == 'end_of_month',
|
|
compounding_method=self.compounding_method,
|
|
loan_type='annuity' if self.interest_rate else 'linear',
|
|
)
|
|
if schedule := loan.get_payment_schedule():
|
|
return schedule[1:] # Skip first line which is always 0 (simply the start of the loan)
|
|
return []
|
|
|
|
@api.depends('loan_amount', 'interest_rate', 'loan_term', 'start_date', 'first_payment_date', 'payment_end_of_month', 'compounding_method')
|
|
def _compute_preview(self):
|
|
def get_preview_row(payment):
|
|
return (
|
|
f"{format_date(self.env, payment.date): <12} "
|
|
f"{wizard.currency_id.format(float(payment.principal_amount)):>15} "
|
|
f"{wizard.currency_id.format(float(payment.interest_amount)):>15} "
|
|
f"{wizard.currency_id.format(float(payment.payment_amount)):>15} "
|
|
f"{wizard.currency_id.format(float(payment.loan_balance_amount)):>15}\n"
|
|
)
|
|
for wizard in self:
|
|
if wizard.loan_amount and wizard.loan_term and wizard.start_date:
|
|
schedule = self._get_loan_payment_schedule()
|
|
if not schedule:
|
|
wizard.preview = ''
|
|
continue
|
|
preview = "{: <12} {:>15} {:>15} {:>15} {:>15}\n".format(_('Date'), _('Principal'), _('Interest'), _('Payment'), _('Balance'))
|
|
for payment in schedule[:5]:
|
|
preview += get_preview_row(payment)
|
|
preview += "{: <12} {:>15} {:>15} {:>15} {:>15}\n".format("...", "...", "...", "...", "...")
|
|
for payment in schedule[-5:]:
|
|
preview += get_preview_row(payment)
|
|
wizard.preview = preview
|
|
else:
|
|
wizard.preview = ''
|
|
|
|
# Actions
|
|
def action_save(self):
|
|
loan_lines_values = []
|
|
for payment in self._get_loan_payment_schedule():
|
|
loan_lines_values.append({
|
|
'loan_id': self.loan_id.id,
|
|
'date': payment.date,
|
|
'principal': float(payment.principal_amount),
|
|
'interest': float(payment.interest_amount),
|
|
})
|
|
self.env['account.loan.line'].create(loan_lines_values)
|
|
self.loan_id.write({
|
|
'date': self.start_date,
|
|
'amount_borrowed': self.loan_amount,
|
|
'interest': sum(self.loan_id.line_ids.mapped('interest')),
|
|
'duration': len(self.loan_id.line_ids),
|
|
})
|
|
return {
|
|
'name': self.loan_id.name,
|
|
'res_id': self.loan_id.id,
|
|
'type': 'ir.actions.act_window',
|
|
'res_model': self.loan_id._name,
|
|
'target': 'self',
|
|
'views': [[False, 'form']],
|
|
'context': self.env.context,
|
|
}
|