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, }