Initial commit
This commit is contained in:
167
Fusion Accounting/models/loan_line.py
Normal file
167
Fusion Accounting/models/loan_line.py
Normal file
@@ -0,0 +1,167 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user