feat: implement automated destination leg reconciliation and refactor internal transfer models to support linked bank statement lines.
This commit is contained in:
parent
4fbc0f17d8
commit
8417038900
@ -1 +1,3 @@
|
||||
from . import bank_internal_transfer
|
||||
from . import account_bank_statement_line
|
||||
from . import account_move
|
||||
|
||||
11
models/account_bank_statement_line.py
Normal file
11
models/account_bank_statement_line.py
Normal 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
18
models/account_move.py
Normal 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
|
||||
@ -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'
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user