From 8417038900bc24d1847d80d0798e159b3d18e527 Mon Sep 17 00:00:00 2001 From: Suherdy Yacob Date: Wed, 29 Apr 2026 11:49:25 +0700 Subject: [PATCH] feat: implement automated destination leg reconciliation and refactor internal transfer models to support linked bank statement lines. --- models/__init__.py | 2 + models/account_bank_statement_line.py | 11 ++++ models/account_move.py | 18 ++++++ models/bank_internal_transfer.py | 85 ++++++++++++++++++++------ views/bank_internal_transfer_views.xml | 19 ++---- 5 files changed, 105 insertions(+), 30 deletions(-) create mode 100644 models/account_bank_statement_line.py create mode 100644 models/account_move.py diff --git a/models/__init__.py b/models/__init__.py index 7fbc3c4..3d024fa 100644 --- a/models/__init__.py +++ b/models/__init__.py @@ -1 +1,3 @@ from . import bank_internal_transfer +from . import account_bank_statement_line +from . import account_move diff --git a/models/account_bank_statement_line.py b/models/account_bank_statement_line.py new file mode 100644 index 0000000..4de2844 --- /dev/null +++ b/models/account_bank_statement_line.py @@ -0,0 +1,11 @@ +from odoo import models, fields + +class AccountBankStatementLine(models.Model): + _inherit = 'account.bank.statement.line' + + bank_internal_transfer_id = fields.Many2one( + 'bank.internal.transfer', + string='Bank Internal Transfer', + readonly=True, + ondelete='set null' + ) diff --git a/models/account_move.py b/models/account_move.py new file mode 100644 index 0000000..37c8591 --- /dev/null +++ b/models/account_move.py @@ -0,0 +1,18 @@ +from odoo import models, api + +class AccountMove(models.Model): + _inherit = 'account.move' + + def set_moves_checked(self, is_checked=True): + """ Hook into the 'Review' action of the bank reconciliation widget. + When a move is marked as checked, if it's linked to an internal transfer, + trigger the automation of the destination leg. + """ + res = super().set_moves_checked(is_checked=is_checked) + if is_checked: + for move in self: + # In Odoo 19, statement lines are moves or linked to moves via statement_line_id + st_line = move.statement_line_id + if st_line and st_line.bank_internal_transfer_id: + st_line.bank_internal_transfer_id._automate_destination_leg() + return res diff --git a/models/bank_internal_transfer.py b/models/bank_internal_transfer.py index b4bb745..9b6ef11 100644 --- a/models/bank_internal_transfer.py +++ b/models/bank_internal_transfer.py @@ -19,7 +19,8 @@ class BankInternalTransfer(models.Model): ('draft', 'Draft'), ('posted', 'Posted'), ], string='Status', default='draft', required=True) - statement_line_id = fields.Many2one('account.bank.statement.line', string='Source Statement Line', readonly=True) + source_statement_line_id = fields.Many2one('account.bank.statement.line', string='Source Statement Line', readonly=True) + destination_statement_line_id = fields.Many2one('account.bank.statement.line', string='Destination Statement Line', readonly=True) # Fields for report compatibility (account.report_payment_receipt) ref = fields.Char(string='Report Reference', related='memo') @@ -30,9 +31,8 @@ class BankInternalTransfer(models.Model): reconciled_bill_ids = fields.Many2many('account.move', compute='_compute_dummy_fields') journal_id = fields.Many2one('account.journal', compute='_compute_dummy_fields') partner_bank_id = fields.Many2one('res.partner.bank', compute='_compute_dummy_fields') - payment_ids = fields.Many2many('bank.internal.transfer', compute='_compute_dummy_fields') + payment_ids = fields.Many2many('bank.internal.transfer', compute='_compute_dummy_fields', string='Payments') deduction_line_ids = fields.Many2many('account.move', compute='_compute_dummy_fields') - ref = fields.Char(related='memo') amount_total = fields.Monetary(related='amount') final_payment_amount = fields.Monetary(related='amount') @@ -90,31 +90,27 @@ class BankInternalTransfer(models.Model): raise UserError(_("Please configure the Outstanding Payments Account on the source journal or company.")) # 2. Create the Bank Statement Line for the Source Journal - # This generates the `account.move` with Credit Bank, Debit Suspense stmt_line_vals = { 'journal_id': transfer.source_journal_id.id, 'date': transfer.date, 'payment_ref': transfer.memo or transfer.name, 'amount': -transfer.amount, # Outgoing money + 'bank_internal_transfer_id': transfer.id, } stmt_line = self.env['account.bank.statement.line'].create(stmt_line_vals) # 3. Modify the underlying move's suspense line to use the Outstanding Payments account + # This effectively "reconciles" the source line against the transfer suspense_account = transfer.source_journal_id.suspense_account_id if not suspense_account: raise UserError(_("Please configure the Suspense Account on the source journal.")) move = stmt_line.move_id - if move.state != 'draft': - move.button_draft() - suspense_line = move.line_ids.filtered(lambda l: l.account_id == suspense_account) if not suspense_line: - # Fallback if somehow it didn't use the suspense account suspense_line = move.line_ids.filtered(lambda l: l.debit > 0) if suspense_line: - # We use check_move_validity=False to bypass balance checks while updating suspense_line.with_context(check_move_validity=False).write({ 'account_id': outstanding_account.id, }) @@ -124,18 +120,73 @@ class BankInternalTransfer(models.Model): # Link and update state transfer.write({ - 'statement_line_id': stmt_line.id, + 'source_statement_line_id': stmt_line.id, 'state': 'posted', }) + def _automate_destination_leg(self): + """ Automatically create and reconcile the destination bank statement line. + This is called when the source statement line is 'Reviewed'. + """ + self.ensure_one() + if self.destination_statement_line_id: + return + + # 1. Determine the transit account + transit_account = self.source_journal_id.outbound_payment_method_line_ids.mapped('payment_account_id') + if transit_account: + transit_account = transit_account[0] + else: + # Fallback to default transit account code 218401 + transit_account = self.env['account.account'].search([ + ('code', '=', '218401'), + ('company_id', '=', self.company_id.id) + ], limit=1) + + if not transit_account: + transit_account = self.company_id.account_journal_payment_credit_account_id + + if not transit_account: + raise UserError(_("Could not find a suitable transit account (218401 or Outstanding Payments).")) + + # 2. Create the Bank Statement Line for the Destination Journal + dst_st_line_vals = { + 'journal_id': self.destination_journal_id.id, + 'date': self.date, + 'payment_ref': self.memo or self.name, + 'amount': self.amount, # Incoming money + 'bank_internal_transfer_id': self.id, + } + dst_st_line = self.env['account.bank.statement.line'].create(dst_st_line_vals) + self.destination_statement_line_id = dst_st_line + + # 3. Reconcile the destination line against the transit account + move = dst_st_line.move_id + suspense_account = self.destination_journal_id.suspense_account_id + suspense_line = move.line_ids.filtered(lambda l: l.account_id == suspense_account) + if not suspense_line: + suspense_line = move.line_ids.filtered(lambda l: l.credit > 0) + + if suspense_line: + suspense_line.with_context(check_move_validity=False).write({ + 'account_id': transit_account.id, + }) + + # 4. Mark as reviewed automatically + move.set_moves_checked(True) + def action_draft(self): for transfer in self: - if transfer.statement_line_id: - # Check if it's already reconciled on the destination side - for line in transfer.statement_line_id.move_id.line_ids: - if line.reconciled: - raise UserError(_("You cannot reset a transfer that has already been reconciled on the destination side.")) + if transfer.source_statement_line_id: + if transfer.source_statement_line_id.is_reconciled and transfer.destination_statement_line_id: + raise UserError(_("You cannot reset a transfer that has already been processed on the destination side.")) - transfer.statement_line_id.move_id.button_draft() - transfer.statement_line_id.unlink() + transfer.source_statement_line_id.move_id.button_draft() + transfer.source_statement_line_id.unlink() + + if transfer.destination_statement_line_id: + transfer.destination_statement_line_id.move_id.button_draft() + transfer.destination_statement_line_id.unlink() + transfer.state = 'draft' + diff --git a/views/bank_internal_transfer_views.xml b/views/bank_internal_transfer_views.xml index bca9ecd..986873d 100644 --- a/views/bank_internal_transfer_views.xml +++ b/views/bank_internal_transfer_views.xml @@ -1,20 +1,12 @@ - - - Bank Internal Transfer Sequence - bank.internal.transfer - INT/TRANS/ - 4 - - bank.internal.transfer.tree bank.internal.transfer - + @@ -22,7 +14,7 @@ - + @@ -55,7 +47,8 @@ - + + @@ -67,7 +60,7 @@ Bank Internal Transfers bank.internal.transfer - tree,form + list,form

Create your first Bank Internal Transfer @@ -103,7 +96,7 @@