""" 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