feat: implement automated destination leg reconciliation and refactor internal transfer models to support linked bank statement lines.

This commit is contained in:
Suherdy Yacob 2026-04-29 11:49:25 +07:00
parent 4fbc0f17d8
commit 8417038900
5 changed files with 105 additions and 30 deletions

View File

@ -1 +1,3 @@
from . import bank_internal_transfer
from . import account_bank_statement_line
from . import account_move

View File

@ -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'
)

18
models/account_move.py Normal file
View File

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

View File

@ -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.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.statement_line_id.move_id.button_draft()
transfer.statement_line_id.unlink()
transfer.state = 'draft'

View File

@ -1,20 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Sequence for the custom internal transfer -->
<record id="seq_bank_internal_transfer" model="ir.sequence">
<field name="name">Bank Internal Transfer Sequence</field>
<field name="code">bank.internal.transfer</field>
<field name="prefix">INT/TRANS/</field>
<field name="padding">4</field>
<field name="company_id" eval="False"/>
</record>
<!-- Tree View -->
<record id="view_bank_internal_transfer_tree" model="ir.ui.view">
<field name="name">bank.internal.transfer.tree</field>
<field name="model">bank.internal.transfer</field>
<field name="arch" type="xml">
<tree string="Bank Internal Transfers">
<list string="Bank Internal Transfers">
<field name="name"/>
<field name="date"/>
<field name="source_journal_id"/>
@ -22,7 +14,7 @@
<field name="amount" sum="Total"/>
<field name="currency_id" column_invisible="True"/>
<field name="state" widget="badge" decoration-info="state == 'draft'" decoration-success="state == 'posted'"/>
</tree>
</list>
</field>
</record>
@ -55,7 +47,8 @@
<field name="currency_id" invisible="1"/>
<field name="memo" readonly="state != 'draft'"/>
<field name="company_id" invisible="1"/>
<field name="statement_line_id" invisible="not statement_line_id"/>
<field name="source_statement_line_id" invisible="not source_statement_line_id"/>
<field name="destination_statement_line_id" invisible="not destination_statement_line_id"/>
</group>
</group>
</sheet>
@ -67,7 +60,7 @@
<record id="action_bank_internal_transfer" model="ir.actions.act_window">
<field name="name">Bank Internal Transfers</field>
<field name="res_model">bank.internal.transfer</field>
<field name="view_mode">tree,form</field>
<field name="view_mode">list,form</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create your first Bank Internal Transfer
@ -103,7 +96,7 @@
<!-- Menu Item in Accounting App Top Level -->
<menuitem id="menu_bank_internal_transfer"
name="Bank Internal Transfer"
parent="account_accountant.menu_accounting"
parent="account.menu_finance_entries"
action="action_bank_internal_transfer"
sequence="25"/>
</odoo>