From 25f033d0c87c87d473fc22f3876f0f345ecd8d16 Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Sun, 19 Apr 2026 13:17:58 -0400 Subject: [PATCH] feat(fusion_accounting_bank_rec): bulk reconcile wizard for selected lines TransientModel + view + binding action so users can select bank lines from any list view and bulk-apply either engine.reconcile_batch or a chosen reconcile model. Made-with: Cursor --- fusion_accounting_bank_rec/__manifest__.py | 3 +- .../security/ir.model.access.csv | 1 + fusion_accounting_bank_rec/tests/__init__.py | 1 + .../tests/test_bulk_reconcile_wizard.py | 42 +++++++++ .../wizards/__init__.py | 1 + .../wizards/bulk_reconcile_wizard.py | 93 +++++++++++++++++++ .../wizards/bulk_reconcile_wizard_views.xml | 43 +++++++++ 7 files changed, 183 insertions(+), 1 deletion(-) create mode 100644 fusion_accounting_bank_rec/tests/test_bulk_reconcile_wizard.py create mode 100644 fusion_accounting_bank_rec/wizards/bulk_reconcile_wizard.py create mode 100644 fusion_accounting_bank_rec/wizards/bulk_reconcile_wizard_views.xml diff --git a/fusion_accounting_bank_rec/__manifest__.py b/fusion_accounting_bank_rec/__manifest__.py index 64ce0b62..c4d62e60 100644 --- a/fusion_accounting_bank_rec/__manifest__.py +++ b/fusion_accounting_bank_rec/__manifest__.py @@ -1,6 +1,6 @@ { 'name': 'Fusion Accounting — Bank Reconciliation', - 'version': '19.0.1.0.19', + 'version': '19.0.1.0.20', 'category': 'Accounting/Accounting', 'sequence': 28, 'summary': 'Native V19 bank reconciliation widget with AI confidence scoring + behavioural learning.', @@ -32,6 +32,7 @@ Built by Nexa Systems Inc. 'security/ir.model.access.csv', 'data/cron.xml', 'wizards/auto_reconcile_wizard_views.xml', + 'wizards/bulk_reconcile_wizard_views.xml', ], 'assets': { 'web.assets_backend': [ diff --git a/fusion_accounting_bank_rec/security/ir.model.access.csv b/fusion_accounting_bank_rec/security/ir.model.access.csv index 5a8b64be..f4a6424b 100644 --- a/fusion_accounting_bank_rec/security/ir.model.access.csv +++ b/fusion_accounting_bank_rec/security/ir.model.access.csv @@ -9,3 +9,4 @@ access_fusion_bank_rec_widget_user,bank rec widget user,model_fusion_bank_rec_wi 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 +access_fusion_bulk_reconcile_wizard_user,fusion.bulk.reconcile.wizard.user,model_fusion_bulk_reconcile_wizard,base.group_user,1,1,1,0 diff --git a/fusion_accounting_bank_rec/tests/__init__.py b/fusion_accounting_bank_rec/tests/__init__.py index 271d7566..3e21e927 100644 --- a/fusion_accounting_bank_rec/tests/__init__.py +++ b/fusion_accounting_bank_rec/tests/__init__.py @@ -17,3 +17,4 @@ from . import test_mv_unreconciled from . import test_cron_methods from . import test_controller from . import test_auto_reconcile_wizard +from . import test_bulk_reconcile_wizard diff --git a/fusion_accounting_bank_rec/tests/test_bulk_reconcile_wizard.py b/fusion_accounting_bank_rec/tests/test_bulk_reconcile_wizard.py new file mode 100644 index 00000000..13efe42a --- /dev/null +++ b/fusion_accounting_bank_rec/tests/test_bulk_reconcile_wizard.py @@ -0,0 +1,42 @@ +"""Tests for fusion.bulk.reconcile.wizard.""" + +from odoo.tests.common import TransactionCase, tagged +from . import _factories as f + + +@tagged('post_install', '-at_install') +class TestBulkReconcileWizard(TransactionCase): + + def setUp(self): + super().setUp() + self.partner = self.env['res.partner'].create({'name': 'Bulk Wizard Partner'}) + self.journal = f.make_bank_journal(self.env, name='Bulk Bank', code='BLKBK') + self.statement = f.make_bank_statement(self.env, journal=self.journal) + + def test_wizard_default_picks_active_ids(self): + line1 = f.make_bank_line( + self.env, statement=self.statement, amount=100, partner=self.partner) + line2 = f.make_bank_line( + self.env, statement=self.statement, amount=200, partner=self.partner) + wizard = self.env['fusion.bulk.reconcile.wizard'].with_context( + active_model='account.bank.statement.line', + active_ids=[line1.id, line2.id], + ).create({}) + self.assertEqual(set(wizard.statement_line_ids.ids), {line1.id, line2.id}) + self.assertEqual(wizard.selected_count, 2) + + def test_wizard_auto_mode_runs_engine_batch(self): + line_ids = [] + for amount in [110.00, 220.00]: + f.make_invoice(self.env, partner=self.partner, amount=amount) + line = f.make_bank_line( + self.env, statement=self.statement, amount=amount, partner=self.partner) + line_ids.append(line.id) + wizard = self.env['fusion.bulk.reconcile.wizard'].create({ + 'statement_line_ids': [(6, 0, line_ids)], + 'mode': 'auto', + 'strategy': 'auto', + }) + wizard.action_run() + self.assertEqual(wizard.state, 'done') + self.assertGreaterEqual(wizard.reconciled_count, 2) diff --git a/fusion_accounting_bank_rec/wizards/__init__.py b/fusion_accounting_bank_rec/wizards/__init__.py index ace9d80b..76477f0d 100644 --- a/fusion_accounting_bank_rec/wizards/__init__.py +++ b/fusion_accounting_bank_rec/wizards/__init__.py @@ -1 +1,2 @@ from . import auto_reconcile_wizard +from . import bulk_reconcile_wizard diff --git a/fusion_accounting_bank_rec/wizards/bulk_reconcile_wizard.py b/fusion_accounting_bank_rec/wizards/bulk_reconcile_wizard.py new file mode 100644 index 00000000..87986fce --- /dev/null +++ b/fusion_accounting_bank_rec/wizards/bulk_reconcile_wizard.py @@ -0,0 +1,93 @@ +"""Bulk reconcile wizard — operates on user-selected records. + +Reads active_ids from context (selected bank lines). Two modes: +1. Auto (run engine on all selected with chosen strategy) +2. Apply reconcile model (apply a chosen account.reconcile.model to all) +""" + +from odoo import _, api, fields, models +from odoo.exceptions import UserError + + +class FusionBulkReconcileWizard(models.TransientModel): + _name = "fusion.bulk.reconcile.wizard" + _description = "Bulk Reconcile Selected Bank Lines Wizard" + + statement_line_ids = fields.Many2many( + 'account.bank.statement.line', + string="Selected Bank Lines", + default=lambda self: [(6, 0, self._default_line_ids())]) + selected_count = fields.Integer( + compute='_compute_selected_count', string="# Selected") + mode = fields.Selection([ + ('auto', 'Auto (engine reconcile_batch)'), + ('reconcile_model', 'Apply Reconcile Model'), + ], default='auto', required=True) + strategy = fields.Selection([ + ('auto', 'Auto'), + ('amount_exact', 'Amount Exact only'), + ('fifo', 'FIFO only'), + ('multi_invoice', 'Multi-invoice'), + ], default='auto') + reconcile_model_id = fields.Many2one( + 'account.reconcile.model', string="Reconcile Model", + domain=[('rule_type', '=', 'writeoff_button')]) + + 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) + + @api.model + def _default_line_ids(self): + ctx = self.env.context + if ctx.get('active_model') == 'account.bank.statement.line': + return ctx.get('active_ids', []) + return [] + + @api.depends('statement_line_ids') + def _compute_selected_count(self): + for w in self: + w.selected_count = len(w.statement_line_ids) + + def action_run(self): + self.ensure_one() + if self.mode == 'auto': + result = self.env['fusion.reconcile.engine'].reconcile_batch( + self.statement_line_ids, strategy=self.strategy) + elif self.mode == 'reconcile_model': + if not self.reconcile_model_id: + raise UserError(_("Pick a reconcile model first.")) + # Phase 1 fallback: apply the model line-by-line via the engine's + # write_off path (simplified — real reconcile-model semantics are + # more nuanced; full integration in Task 38 follow-up). + result = {'reconciled_count': 0, 'skipped': 0, 'errors': []} + for line in self.statement_line_ids: + try: + self.reconcile_model_id._apply_lines_for_bank_statement_line(line) + result['reconciled_count'] += 1 + except Exception as e: # noqa: BLE001 + result['errors'].append({'line_id': line.id, 'error': str(e)}) + else: + result = {'reconciled_count': 0, 'skipped': 0, 'errors': []} + + 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, + } diff --git a/fusion_accounting_bank_rec/wizards/bulk_reconcile_wizard_views.xml b/fusion_accounting_bank_rec/wizards/bulk_reconcile_wizard_views.xml new file mode 100644 index 00000000..ef2dbe4c --- /dev/null +++ b/fusion_accounting_bank_rec/wizards/bulk_reconcile_wizard_views.xml @@ -0,0 +1,43 @@ + + + + + fusion.bulk.reconcile.wizard.form + fusion.bulk.reconcile.wizard + +
+ + + + + + + + + + + + + + +
+
+ +
+
+ + + Bulk Reconcile Selected + fusion.bulk.reconcile.wizard + form + new + + list + + +