Files
Odoo-Modules/Fusion Accounting/models/loan_line.py
2026-02-22 01:22:18 -05:00

168 lines
5.6 KiB
Python

"""
Fusion Accounting - Loan Amortization Line
Each record represents a single installment in a loan's amortization
schedule, tracking principal, interest, remaining balance, and the
link to the corresponding journal entry once paid.
"""
from odoo import api, Command, fields, models, _
from odoo.exceptions import UserError
from odoo.tools import float_round
class FusionLoanLine(models.Model):
"""Single installment of a loan amortization schedule.
Created in bulk by :meth:`fusion.loan.compute_amortization_schedule`
and individually paid via :meth:`action_create_entry` which posts
a journal entry debiting the loan liability and interest expense,
and crediting the bank / payment account.
"""
_name = 'fusion.loan.line'
_description = 'Loan Amortization Line'
_order = 'sequence, id'
# ------------------------------------------------------------------
# Parent link
# ------------------------------------------------------------------
loan_id = fields.Many2one(
'fusion.loan',
string='Loan',
required=True,
ondelete='cascade',
index=True,
)
# ------------------------------------------------------------------
# Schedule fields
# ------------------------------------------------------------------
sequence = fields.Integer(
string='#',
required=True,
help="Installment number in the amortization schedule.",
)
date = fields.Date(
string='Due Date',
required=True,
)
principal_amount = fields.Monetary(
string='Principal',
currency_field='currency_id',
help="Portion of the payment that reduces the outstanding balance.",
)
interest_amount = fields.Monetary(
string='Interest',
currency_field='currency_id',
help="Interest charged for this period.",
)
total_payment = fields.Monetary(
string='Total Payment',
currency_field='currency_id',
help="Sum of principal and interest for this installment.",
)
remaining_balance = fields.Monetary(
string='Remaining Balance',
currency_field='currency_id',
help="Outstanding principal after this installment.",
)
# ------------------------------------------------------------------
# Status
# ------------------------------------------------------------------
is_paid = fields.Boolean(
string='Paid',
default=False,
copy=False,
)
move_id = fields.Many2one(
'account.move',
string='Journal Entry',
copy=False,
readonly=True,
help="The posted journal entry recording this installment payment.",
)
# ------------------------------------------------------------------
# Related / helper
# ------------------------------------------------------------------
currency_id = fields.Many2one(
related='loan_id.currency_id',
store=True,
)
company_id = fields.Many2one(
related='loan_id.company_id',
store=True,
)
loan_state = fields.Selection(
related='loan_id.state',
string='Loan Status',
)
# ==================== Business Methods ====================
def action_create_entry(self):
"""Create and post the journal entry for this loan installment.
Debits:
- Loan liability account (principal portion)
- Interest expense account (interest portion)
Credits:
- Journal default account / bank (total payment)
"""
self.ensure_one()
if self.is_paid:
raise UserError(
_("Installment #%s is already paid.", self.sequence)
)
if self.loan_id.state != 'running':
raise UserError(
_("Entries can only be created for running loans.")
)
loan = self.loan_id
move_vals = {
'journal_id': loan.journal_id.id,
'date': self.date,
'ref': _('%(loan)s - Installment #%(seq)s',
loan=loan.name, seq=self.sequence),
'fusion_loan_id': loan.id,
'line_ids': [
# Debit: reduce loan liability
Command.create({
'account_id': loan.loan_account_id.id,
'debit': self.principal_amount,
'credit': 0.0,
'partner_id': loan.partner_id.id,
'name': _('%(loan)s - Principal #%(seq)s',
loan=loan.name, seq=self.sequence),
}),
# Debit: interest expense
Command.create({
'account_id': loan.interest_account_id.id,
'debit': self.interest_amount,
'credit': 0.0,
'partner_id': loan.partner_id.id,
'name': _('%(loan)s - Interest #%(seq)s',
loan=loan.name, seq=self.sequence),
}),
# Credit: bank / payment
Command.create({
'account_id': loan.journal_id.default_account_id.id,
'debit': 0.0,
'credit': self.total_payment,
'partner_id': loan.partner_id.id,
'name': _('%(loan)s - Payment #%(seq)s',
loan=loan.name, seq=self.sequence),
}),
],
}
move = self.env['account.move'].create(move_vals)
move.action_post()
self.write({
'is_paid': True,
'move_id': move.id,
})
return True