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 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'),
|
('draft', 'Draft'),
|
||||||
('posted', 'Posted'),
|
('posted', 'Posted'),
|
||||||
], string='Status', default='draft', required=True)
|
], 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)
|
# Fields for report compatibility (account.report_payment_receipt)
|
||||||
ref = fields.Char(string='Report Reference', related='memo')
|
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')
|
reconciled_bill_ids = fields.Many2many('account.move', compute='_compute_dummy_fields')
|
||||||
journal_id = fields.Many2one('account.journal', 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')
|
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')
|
deduction_line_ids = fields.Many2many('account.move', compute='_compute_dummy_fields')
|
||||||
ref = fields.Char(related='memo')
|
|
||||||
amount_total = fields.Monetary(related='amount')
|
amount_total = fields.Monetary(related='amount')
|
||||||
final_payment_amount = 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."))
|
raise UserError(_("Please configure the Outstanding Payments Account on the source journal or company."))
|
||||||
|
|
||||||
# 2. Create the Bank Statement Line for the Source Journal
|
# 2. Create the Bank Statement Line for the Source Journal
|
||||||
# This generates the `account.move` with Credit Bank, Debit Suspense
|
|
||||||
stmt_line_vals = {
|
stmt_line_vals = {
|
||||||
'journal_id': transfer.source_journal_id.id,
|
'journal_id': transfer.source_journal_id.id,
|
||||||
'date': transfer.date,
|
'date': transfer.date,
|
||||||
'payment_ref': transfer.memo or transfer.name,
|
'payment_ref': transfer.memo or transfer.name,
|
||||||
'amount': -transfer.amount, # Outgoing money
|
'amount': -transfer.amount, # Outgoing money
|
||||||
|
'bank_internal_transfer_id': transfer.id,
|
||||||
}
|
}
|
||||||
stmt_line = self.env['account.bank.statement.line'].create(stmt_line_vals)
|
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
|
# 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
|
suspense_account = transfer.source_journal_id.suspense_account_id
|
||||||
if not suspense_account:
|
if not suspense_account:
|
||||||
raise UserError(_("Please configure the Suspense Account on the source journal."))
|
raise UserError(_("Please configure the Suspense Account on the source journal."))
|
||||||
|
|
||||||
move = stmt_line.move_id
|
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)
|
suspense_line = move.line_ids.filtered(lambda l: l.account_id == suspense_account)
|
||||||
if not suspense_line:
|
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)
|
suspense_line = move.line_ids.filtered(lambda l: l.debit > 0)
|
||||||
|
|
||||||
if suspense_line:
|
if suspense_line:
|
||||||
# We use check_move_validity=False to bypass balance checks while updating
|
|
||||||
suspense_line.with_context(check_move_validity=False).write({
|
suspense_line.with_context(check_move_validity=False).write({
|
||||||
'account_id': outstanding_account.id,
|
'account_id': outstanding_account.id,
|
||||||
})
|
})
|
||||||
@ -124,18 +120,73 @@ class BankInternalTransfer(models.Model):
|
|||||||
|
|
||||||
# Link and update state
|
# Link and update state
|
||||||
transfer.write({
|
transfer.write({
|
||||||
'statement_line_id': stmt_line.id,
|
'source_statement_line_id': stmt_line.id,
|
||||||
'state': 'posted',
|
'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):
|
def action_draft(self):
|
||||||
for transfer in self:
|
for transfer in self:
|
||||||
if transfer.statement_line_id:
|
if transfer.source_statement_line_id:
|
||||||
# Check if it's already reconciled on the destination side
|
if transfer.source_statement_line_id.is_reconciled and transfer.destination_statement_line_id:
|
||||||
for line in transfer.statement_line_id.move_id.line_ids:
|
raise UserError(_("You cannot reset a transfer that has already been processed on the destination side."))
|
||||||
if line.reconciled:
|
|
||||||
raise UserError(_("You cannot reset a transfer that has already been reconciled 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'
|
transfer.state = 'draft'
|
||||||
|
|
||||||
|
|||||||
@ -1,20 +1,12 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<odoo>
|
<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 -->
|
<!-- Tree View -->
|
||||||
<record id="view_bank_internal_transfer_tree" model="ir.ui.view">
|
<record id="view_bank_internal_transfer_tree" model="ir.ui.view">
|
||||||
<field name="name">bank.internal.transfer.tree</field>
|
<field name="name">bank.internal.transfer.tree</field>
|
||||||
<field name="model">bank.internal.transfer</field>
|
<field name="model">bank.internal.transfer</field>
|
||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<tree string="Bank Internal Transfers">
|
<list string="Bank Internal Transfers">
|
||||||
<field name="name"/>
|
<field name="name"/>
|
||||||
<field name="date"/>
|
<field name="date"/>
|
||||||
<field name="source_journal_id"/>
|
<field name="source_journal_id"/>
|
||||||
@ -22,7 +14,7 @@
|
|||||||
<field name="amount" sum="Total"/>
|
<field name="amount" sum="Total"/>
|
||||||
<field name="currency_id" column_invisible="True"/>
|
<field name="currency_id" column_invisible="True"/>
|
||||||
<field name="state" widget="badge" decoration-info="state == 'draft'" decoration-success="state == 'posted'"/>
|
<field name="state" widget="badge" decoration-info="state == 'draft'" decoration-success="state == 'posted'"/>
|
||||||
</tree>
|
</list>
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
@ -55,7 +47,8 @@
|
|||||||
<field name="currency_id" invisible="1"/>
|
<field name="currency_id" invisible="1"/>
|
||||||
<field name="memo" readonly="state != 'draft'"/>
|
<field name="memo" readonly="state != 'draft'"/>
|
||||||
<field name="company_id" invisible="1"/>
|
<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>
|
||||||
</group>
|
</group>
|
||||||
</sheet>
|
</sheet>
|
||||||
@ -67,7 +60,7 @@
|
|||||||
<record id="action_bank_internal_transfer" model="ir.actions.act_window">
|
<record id="action_bank_internal_transfer" model="ir.actions.act_window">
|
||||||
<field name="name">Bank Internal Transfers</field>
|
<field name="name">Bank Internal Transfers</field>
|
||||||
<field name="res_model">bank.internal.transfer</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">
|
<field name="help" type="html">
|
||||||
<p class="o_view_nocontent_smiling_face">
|
<p class="o_view_nocontent_smiling_face">
|
||||||
Create your first Bank Internal Transfer
|
Create your first Bank Internal Transfer
|
||||||
@ -103,7 +96,7 @@
|
|||||||
<!-- Menu Item in Accounting App Top Level -->
|
<!-- Menu Item in Accounting App Top Level -->
|
||||||
<menuitem id="menu_bank_internal_transfer"
|
<menuitem id="menu_bank_internal_transfer"
|
||||||
name="Bank Internal Transfer"
|
name="Bank Internal Transfer"
|
||||||
parent="account_accountant.menu_accounting"
|
parent="account.menu_finance_entries"
|
||||||
action="action_bank_internal_transfer"
|
action="action_bank_internal_transfer"
|
||||||
sequence="25"/>
|
sequence="25"/>
|
||||||
</odoo>
|
</odoo>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user