feat(fusion_accounting_bank_rec): auto-reconcile wizard

TransientModel that filters unreconciled bank lines by journal +
date range + strategy and runs engine.reconcile_batch. Shows
reconciled_count / skipped_count / error_summary in result view.

Made-with: Cursor
This commit is contained in:
gsinghpal
2026-04-19 13:16:06 -04:00
parent 6cbb5f85fe
commit 75850aad73
7 changed files with 172 additions and 1 deletions

View File

@@ -1,6 +1,6 @@
{
'name': 'Fusion Accounting — Bank Reconciliation',
'version': '19.0.1.0.18',
'version': '19.0.1.0.19',
'category': 'Accounting/Accounting',
'sequence': 28,
'summary': 'Native V19 bank reconciliation widget with AI confidence scoring + behavioural learning.',
@@ -31,6 +31,7 @@ Built by Nexa Systems Inc.
'data': [
'security/ir.model.access.csv',
'data/cron.xml',
'wizards/auto_reconcile_wizard_views.xml',
],
'assets': {
'web.assets_backend': [

View File

@@ -8,3 +8,4 @@ access_fusion_reconcile_suggestion_admin,suggestion admin,model_fusion_reconcile
access_fusion_bank_rec_widget_user,bank rec widget user,model_fusion_bank_rec_widget,fusion_accounting_core.group_fusion_accounting_user,1,1,1,1
access_fusion_unreconciled_bank_line_mv_user,unreconciled bank line mv user,model_fusion_unreconciled_bank_line_mv,fusion_accounting_core.group_fusion_accounting_user,1,0,0,0
access_fusion_unreconciled_bank_line_mv_admin,unreconciled bank line mv admin,model_fusion_unreconciled_bank_line_mv,fusion_accounting_core.group_fusion_accounting_admin,1,0,0,0
access_fusion_auto_reconcile_wizard_user,fusion.auto.reconcile.wizard.user,model_fusion_auto_reconcile_wizard,base.group_user,1,1,1,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
8 access_fusion_bank_rec_widget_user bank rec widget user model_fusion_bank_rec_widget fusion_accounting_core.group_fusion_accounting_user 1 1 1 1
9 access_fusion_unreconciled_bank_line_mv_user unreconciled bank line mv user model_fusion_unreconciled_bank_line_mv fusion_accounting_core.group_fusion_accounting_user 1 0 0 0
10 access_fusion_unreconciled_bank_line_mv_admin unreconciled bank line mv admin model_fusion_unreconciled_bank_line_mv fusion_accounting_core.group_fusion_accounting_admin 1 0 0 0
11 access_fusion_auto_reconcile_wizard_user fusion.auto.reconcile.wizard.user model_fusion_auto_reconcile_wizard base.group_user 1 1 1 0

View File

@@ -16,3 +16,4 @@ from . import test_legacy_tools_refactor
from . import test_mv_unreconciled
from . import test_cron_methods
from . import test_controller
from . import test_auto_reconcile_wizard

View File

@@ -0,0 +1,50 @@
"""Tests for fusion.auto.reconcile.wizard."""
from odoo.tests.common import TransactionCase, tagged
from . import _factories as f
@tagged('post_install', '-at_install')
class TestAutoReconcileWizard(TransactionCase):
def setUp(self):
super().setUp()
self.partner = self.env['res.partner'].create({'name': 'Auto Wizard Partner'})
self.journal = f.make_bank_journal(self.env, name='Auto Bank', code='AUBK')
def test_wizard_runs_and_reconciles_matchable_lines(self):
statement = f.make_bank_statement(self.env, journal=self.journal)
for amount in [100.00, 200.00]:
f.make_invoice(self.env, partner=self.partner, amount=amount)
f.make_bank_line(
self.env, statement=statement, amount=amount, partner=self.partner)
wizard = self.env['fusion.auto.reconcile.wizard'].create({
'journal_id': self.journal.id,
'strategy': 'auto',
'only_with_partner': True,
})
wizard.action_run()
self.assertEqual(wizard.state, 'done')
self.assertGreaterEqual(wizard.reconciled_count, 2)
def test_wizard_filters_by_date_range(self):
wizard = self.env['fusion.auto.reconcile.wizard'].create({
'journal_id': self.journal.id,
'date_from': '2099-01-01',
'date_to': '2099-12-31',
'strategy': 'auto',
})
wizard.action_run()
self.assertEqual(wizard.reconciled_count, 0)
def test_wizard_skips_when_only_with_partner_excludes_orphans(self):
statement = f.make_bank_statement(self.env, journal=self.journal)
f.make_bank_line(self.env, statement=statement, amount=999, partner=None)
wizard = self.env['fusion.auto.reconcile.wizard'].create({
'journal_id': self.journal.id,
'strategy': 'auto',
'only_with_partner': True,
})
wizard.action_run()
self.assertEqual(wizard.reconciled_count, 0)

View File

@@ -0,0 +1 @@
from . import auto_reconcile_wizard

View File

@@ -0,0 +1,78 @@
"""Auto-reconcile wizard.
Lets the user pick filters (journal, date range, strategy) and runs
fusion.reconcile.engine.reconcile_batch on all matching unreconciled
bank lines. Shows summary of results.
"""
from odoo import _, fields, models
class FusionAutoReconcileWizard(models.TransientModel):
_name = "fusion.auto.reconcile.wizard"
_description = "Auto-Reconcile Bank Statement Lines Wizard"
journal_id = fields.Many2one(
'account.journal', string="Bank Journal",
domain=[('type', '=', 'bank')], required=True)
date_from = fields.Date(string="Date From")
date_to = fields.Date(string="Date To", default=fields.Date.today)
strategy = fields.Selection([
('auto', 'Auto (try amount-exact, then multi-invoice, then FIFO)'),
('amount_exact', 'Amount Exact only'),
('fifo', 'FIFO only'),
('multi_invoice', 'Multi-invoice combination only'),
], default='auto', required=True)
only_with_partner = fields.Boolean(
string="Only lines with a partner",
default=True,
help="Most safer matches require a known partner. Untick to attempt "
"matching for orphan lines too (uses memo tokenization).")
state = fields.Selection([
('draft', 'Draft'),
('done', 'Done'),
], default='draft')
reconciled_count = fields.Integer(readonly=True)
skipped_count = fields.Integer(readonly=True)
error_count = fields.Integer(readonly=True)
error_summary = fields.Text(readonly=True)
def _build_domain(self):
self.ensure_one()
domain = [
('journal_id', '=', self.journal_id.id),
('is_reconciled', '=', False),
]
if self.date_from:
domain.append(('date', '>=', self.date_from))
if self.date_to:
domain.append(('date', '<=', self.date_to))
if self.only_with_partner:
domain.append(('partner_id', '!=', False))
return domain
def action_run(self):
self.ensure_one()
Line = self.env['account.bank.statement.line']
lines = Line.search(self._build_domain(), limit=1000)
result = self.env['fusion.reconcile.engine'].reconcile_batch(
lines, strategy=self.strategy)
errors = result.get('errors', [])
self.write({
'state': 'done',
'reconciled_count': result.get('reconciled_count', 0),
'skipped_count': result.get('skipped', 0),
'error_count': len(errors),
'error_summary': '\n'.join(
f"Line {e['line_id']}: {e['error']}" for e in errors[:20]
) or False,
})
return {
'type': 'ir.actions.act_window',
'res_model': self._name,
'res_id': self.id,
'view_mode': 'form',
'target': 'new',
'context': self.env.context,
}

View File

@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_fusion_auto_reconcile_wizard_form" model="ir.ui.view">
<field name="name">fusion.auto.reconcile.wizard.form</field>
<field name="model">fusion.auto.reconcile.wizard</field>
<field name="arch" type="xml">
<form string="Auto-Reconcile Bank Lines">
<group invisible="state == 'done'">
<field name="journal_id" options="{'no_create': True}"/>
<field name="date_from"/>
<field name="date_to"/>
<field name="strategy"/>
<field name="only_with_partner"/>
</group>
<group invisible="state != 'done'" string="Results">
<field name="reconciled_count"/>
<field name="skipped_count"/>
<field name="error_count"/>
<field name="error_summary"/>
</group>
<field name="state" invisible="1"/>
<footer>
<button name="action_run" type="object" string="Run"
class="btn-primary" invisible="state == 'done'"/>
<button special="cancel" string="Close"/>
</footer>
</form>
</field>
</record>
<record id="action_fusion_auto_reconcile_wizard" model="ir.actions.act_window">
<field name="name">Auto-Reconcile</field>
<field name="res_model">fusion.auto.reconcile.wizard</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
</odoo>