refactor(fusion_accounting_ai): route legacy reconcile tools through engine
When fusion_accounting_bank_rec is installed, match_bank_line_to_payments and auto_reconcile_bank_lines now use fusion.reconcile.engine via the BankRecAdapter, gaining precedent recording, AI suggestion superseding, and shared validation. Legacy paths preserved for Enterprise/Community- only installs (engine model absent -> fall back to set_line_bank_statement_line and _try_auto_reconcile_statement_lines). Also wraps engine.reconcile_batch's per-line loop in a savepoint so a single bad line's DB error (e.g. check-constraint violation) no longer poisons the whole batch transaction; the existing per-line try/except now isolates failures as originally intended. Made-with: Cursor
This commit is contained in:
@@ -67,7 +67,16 @@ def match_bank_line_to_payments(env, params):
|
|||||||
st_line = env['account.bank.statement.line'].browse(st_line_id)
|
st_line = env['account.bank.statement.line'].browse(st_line_id)
|
||||||
if not st_line.exists():
|
if not st_line.exists():
|
||||||
return {'error': 'Statement line not found'}
|
return {'error': 'Statement line not found'}
|
||||||
st_line.set_line_bank_statement_line(move_line_ids)
|
# Phase 1 Task 23: route through engine when available
|
||||||
|
if 'fusion.reconcile.engine' in env.registry:
|
||||||
|
cands = env['account.move.line'].browse(move_line_ids).exists()
|
||||||
|
if not cands:
|
||||||
|
return {'error': 'No valid move_line_ids'}
|
||||||
|
env['fusion.reconcile.engine'].reconcile_one(
|
||||||
|
st_line, against_lines=cands)
|
||||||
|
st_line.invalidate_recordset(['is_reconciled'])
|
||||||
|
else:
|
||||||
|
st_line.set_line_bank_statement_line(move_line_ids)
|
||||||
return {
|
return {
|
||||||
'status': 'matched',
|
'status': 'matched',
|
||||||
'statement_line_id': st_line_id,
|
'statement_line_id': st_line_id,
|
||||||
@@ -83,7 +92,12 @@ def auto_reconcile_bank_lines(env, params):
|
|||||||
('company_id', '=', int(company_id)),
|
('company_id', '=', int(company_id)),
|
||||||
])
|
])
|
||||||
before_count = len(lines)
|
before_count = len(lines)
|
||||||
lines._try_auto_reconcile_statement_lines(company_id=int(company_id))
|
# Phase 1 Task 23: route through engine when available
|
||||||
|
if 'fusion.reconcile.engine' in env.registry:
|
||||||
|
env['fusion.reconcile.engine'].reconcile_batch(
|
||||||
|
lines, strategy='auto')
|
||||||
|
else:
|
||||||
|
lines._try_auto_reconcile_statement_lines(company_id=int(company_id))
|
||||||
still_unreconciled = env['account.bank.statement.line'].search([
|
still_unreconciled = env['account.bank.statement.line'].search([
|
||||||
('is_reconciled', '=', False),
|
('is_reconciled', '=', False),
|
||||||
('company_id', '=', int(company_id)),
|
('company_id', '=', int(company_id)),
|
||||||
|
|||||||
@@ -177,14 +177,19 @@ class FusionReconcileEngine(models.AbstractModel):
|
|||||||
if line.is_reconciled:
|
if line.is_reconciled:
|
||||||
skipped += 1
|
skipped += 1
|
||||||
continue
|
continue
|
||||||
|
# Per-line savepoint so a single DB-level failure (e.g. a
|
||||||
|
# check-constraint violation on one bad line) doesn't poison
|
||||||
|
# the whole batch's transaction.
|
||||||
try:
|
try:
|
||||||
candidates = self._fetch_candidates(line)
|
with self.env.cr.savepoint():
|
||||||
picked = self._apply_strategy(line, candidates, strategy)
|
candidates = self._fetch_candidates(line)
|
||||||
if picked:
|
picked = self._apply_strategy(
|
||||||
self.reconcile_one(line, against_lines=picked)
|
line, candidates, strategy)
|
||||||
reconciled += 1
|
if picked:
|
||||||
else:
|
self.reconcile_one(line, against_lines=picked)
|
||||||
skipped += 1
|
reconciled += 1
|
||||||
|
else:
|
||||||
|
skipped += 1
|
||||||
except Exception as e: # noqa: BLE001
|
except Exception as e: # noqa: BLE001
|
||||||
errors.append({'line_id': line.id, 'error': str(e)})
|
errors.append({'line_id': line.id, 'error': str(e)})
|
||||||
_logger.warning(
|
_logger.warning(
|
||||||
|
|||||||
@@ -12,3 +12,4 @@ from . import test_reconcile_engine_integration
|
|||||||
from . import test_bank_rec_prompt
|
from . import test_bank_rec_prompt
|
||||||
from . import test_bank_rec_adapter
|
from . import test_bank_rec_adapter
|
||||||
from . import test_bank_rec_tools
|
from . import test_bank_rec_tools
|
||||||
|
from . import test_legacy_tools_refactor
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
"""Tests verifying legacy tools route through fusion.reconcile.engine when present.
|
||||||
|
|
||||||
|
These tests run in the fusion_accounting_bank_rec context where the engine IS
|
||||||
|
available, so they assert the engine path is taken and produces correct
|
||||||
|
results. The fallback path is exercised by the existing fusion_accounting_ai
|
||||||
|
tests when fusion_accounting_bank_rec is not installed."""
|
||||||
|
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from odoo.tests.common import TransactionCase, tagged
|
||||||
|
from odoo.addons.fusion_accounting_ai.services.tools import bank_reconciliation as tools
|
||||||
|
from . import _factories as f
|
||||||
|
|
||||||
|
|
||||||
|
@tagged('post_install', '-at_install')
|
||||||
|
class TestLegacyToolsRefactor(TransactionCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
self.partner = self.env['res.partner'].create({'name': 'Refactor Test Partner'})
|
||||||
|
|
||||||
|
def test_match_bank_line_to_payments_uses_engine(self):
|
||||||
|
"""When engine is present, match_bank_line_to_payments must produce
|
||||||
|
a partial reconcile via the engine, not via set_line_bank_statement_line."""
|
||||||
|
bank_line, recv_lines = f.make_reconcileable_pair(
|
||||||
|
self.env, amount=180.00, partner=self.partner)
|
||||||
|
result = tools.match_bank_line_to_payments(self.env, {
|
||||||
|
'statement_line_id': bank_line.id,
|
||||||
|
'move_line_ids': recv_lines.ids,
|
||||||
|
})
|
||||||
|
self.assertEqual(result.get('status'), 'matched')
|
||||||
|
bank_line.invalidate_recordset(['is_reconciled'])
|
||||||
|
self.assertTrue(bank_line.is_reconciled)
|
||||||
|
# Verify a precedent was recorded - engine-only behaviour
|
||||||
|
Precedent = self.env['fusion.reconcile.precedent']
|
||||||
|
precedents = Precedent.search([('partner_id', '=', self.partner.id)])
|
||||||
|
self.assertGreater(len(precedents), 0,
|
||||||
|
"Engine path should record a precedent; legacy path would not")
|
||||||
|
|
||||||
|
def test_auto_reconcile_bank_lines_uses_engine(self):
|
||||||
|
"""When engine is present, auto_reconcile_bank_lines must call
|
||||||
|
fusion.reconcile.engine.reconcile_batch (not the Enterprise-only
|
||||||
|
_try_auto_reconcile_statement_lines fallback). We patch
|
||||||
|
reconcile_batch to verify routing without running the real engine
|
||||||
|
across every legacy unreconciled line in the test DB."""
|
||||||
|
Engine = type(self.env['fusion.reconcile.engine'])
|
||||||
|
with patch.object(
|
||||||
|
Engine, 'reconcile_batch', autospec=True,
|
||||||
|
return_value={'reconciled_count': 2, 'skipped': 0, 'errors': []},
|
||||||
|
) as engine_call:
|
||||||
|
result = tools.auto_reconcile_bank_lines(self.env, {
|
||||||
|
'company_id': self.env.company.id,
|
||||||
|
})
|
||||||
|
self.assertEqual(result['status'], 'completed')
|
||||||
|
self.assertTrue(engine_call.called,
|
||||||
|
"Engine path must invoke fusion.reconcile.engine.reconcile_batch")
|
||||||
|
# Verify the engine was passed the strategy='auto' kwarg per spec
|
||||||
|
_self, _lines = engine_call.call_args.args[0], engine_call.call_args.args[1]
|
||||||
|
self.assertEqual(engine_call.call_args.kwargs.get('strategy'), 'auto')
|
||||||
Reference in New Issue
Block a user