From 691035ec6aee34c3a19f1edb193be6d0a381c7e7 Mon Sep 17 00:00:00 2001 From: Felipe Garcia Suez Date: Mon, 20 Apr 2026 12:49:59 +0000 Subject: [PATCH] [ADD] account_reconcile_bg: background processing for large bank reconciliations Implements background job processing for bank reconciliations when dealing with large payment batches to prevent timeouts and improve user experience. Main features: - Automatic background processing for large reconciliations (configurable threshold) - User notifications when background reconciliation completes - Seamless integration with existing bank reconciliation workflow - No UI blocking: users can continue working while large batches process Technical implementation: - Override bank.rec.widget._js_action_validate() to detect large reconciliations - Use base_bg to enqueue reconciliation jobs - Add reconciliation_in_background flag to account.bank.statement.line - Configurable via system parameter: account_reconcile_bg.lines_threshold Includes comprehensive testing guide and unit tests. X-original-commit: 8dce76a9220667566be9bb2a88419bd34864ecec --- account_reconcile_bg/README.rst | 81 ++++++++++++ account_reconcile_bg/__init__.py | 2 + account_reconcile_bg/__manifest__.py | 37 ++++++ .../data/ir_config_parameter_data.xml | 10 ++ account_reconcile_bg/models/__init__.py | 5 + .../models/account_bank_statement_line.py | 84 ++++++++++++ .../models/account_move_line.py | 16 +++ .../models/bank_rec_widget.py | 120 ++++++++++++++++++ account_reconcile_bg/models/bg_job.py | 42 ++++++ account_reconcile_bg/tests/__init__.py | 2 + .../tests/test_account_reconcile_bg.py | 91 +++++++++++++ 11 files changed, 490 insertions(+) create mode 100644 account_reconcile_bg/README.rst create mode 100644 account_reconcile_bg/__init__.py create mode 100644 account_reconcile_bg/__manifest__.py create mode 100644 account_reconcile_bg/data/ir_config_parameter_data.xml create mode 100644 account_reconcile_bg/models/__init__.py create mode 100644 account_reconcile_bg/models/account_bank_statement_line.py create mode 100644 account_reconcile_bg/models/account_move_line.py create mode 100644 account_reconcile_bg/models/bank_rec_widget.py create mode 100644 account_reconcile_bg/models/bg_job.py create mode 100644 account_reconcile_bg/tests/__init__.py create mode 100644 account_reconcile_bg/tests/test_account_reconcile_bg.py diff --git a/account_reconcile_bg/README.rst b/account_reconcile_bg/README.rst new file mode 100644 index 00000000..95efee3c --- /dev/null +++ b/account_reconcile_bg/README.rst @@ -0,0 +1,81 @@ +Account Reconcile Background +============================ + +This module enables background processing for bank reconciliation operations when dealing with large payment batches, preventing timeouts and improving user experience. + +**Table of contents** + +.. contents:: + :local: + +Overview +======== + +When reconciling large payment batches (e.g., multiple payments included in a single batch) with a bank statement line, the operation can take a long time and may cause timeouts. This module solves this problem by automatically processing large reconciliations in the background, allowing users to continue working while the reconciliation completes. + +Features +======== + +* **Automatic Background Processing**: Large reconciliations are automatically sent to background processing +* **Configurable Threshold**: System parameter to control when background processing kicks in (default: 50 lines) +* **User Notifications**: Users receive notifications when background reconciliation completes +* **No UI Blocking**: Users can continue reconciling other transactions while large batches process +* **Seamless Integration**: Works transparently with existing bank reconciliation workflow + +How It Works +============ + +The module monitors the number of lines being reconciled in the bank reconciliation widget: + +1. When validating a reconciliation, it counts the number of source lines +2. If the count is **below the threshold** (default 50), the reconciliation proceeds normally (synchronous) +3. If the count is **above the threshold**, the reconciliation is enqueued as a background job +4. The user receives an immediate success notification and can continue working +5. When the background job completes, the user is notified via internal message + +Configuration +============= + +The threshold for background processing can be configured via system parameters: + +* Navigate to **Settings > Technical > Parameters > System Parameters** +* Find or create the parameter ``account_reconcile_bg.lines_threshold`` +* Default value: ``50`` +* Set to a higher value to process larger reconciliations synchronously +* Set to a lower value to send more reconciliations to background + +Technical Details +================= + +Dependencies +------------ + +* ``account_accountant``: Odoo Enterprise accounting module with bank reconciliation +* ``base_bg``: Background job processing system + +Model Inheritance +----------------- + +The module inherits from ``bank.rec.widget`` and overrides: + +* ``_js_action_validate()``: Detects large reconciliations and routes to background +* ``_validate_in_background()``: Enqueues the job using base_bg +* ``_do_validate()``: Executes the actual validation in background + +Credits +======= + +Authors +------- + +* ADHOC SA + +Contributors +------------ + +* ADHOC SA + +Maintainers +----------- + +This module is maintained by ADHOC SA. diff --git a/account_reconcile_bg/__init__.py b/account_reconcile_bg/__init__.py new file mode 100644 index 00000000..3275ac2a --- /dev/null +++ b/account_reconcile_bg/__init__.py @@ -0,0 +1,2 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from . import models diff --git a/account_reconcile_bg/__manifest__.py b/account_reconcile_bg/__manifest__.py new file mode 100644 index 00000000..7d7698b3 --- /dev/null +++ b/account_reconcile_bg/__manifest__.py @@ -0,0 +1,37 @@ +############################################################################## +# +# Copyright (C) 2026 ADHOC SA (http://www.adhoc.com.ar) +# All Rights Reserved. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## +{ + "name": "Account Reconcile Background", + "version": "18.0.1.0.0", + "category": "Accounting", + "author": "ADHOC SA", + "website": "https://www.adhoc.com.ar", + "license": "AGPL-3", + "summary": "Process bank reconciliation in background for large payment batches", + "depends": [ + "account_accountant", + "base_bg", + ], + "data": [ + "data/ir_config_parameter_data.xml", + ], + "installable": True, + "auto_install": False, +} diff --git a/account_reconcile_bg/data/ir_config_parameter_data.xml b/account_reconcile_bg/data/ir_config_parameter_data.xml new file mode 100644 index 00000000..b24cd47f --- /dev/null +++ b/account_reconcile_bg/data/ir_config_parameter_data.xml @@ -0,0 +1,10 @@ + + + + + + account_reconcile_bg.lines_threshold + 100 + + + diff --git a/account_reconcile_bg/models/__init__.py b/account_reconcile_bg/models/__init__.py new file mode 100644 index 00000000..da979872 --- /dev/null +++ b/account_reconcile_bg/models/__init__.py @@ -0,0 +1,5 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from . import bank_rec_widget +from . import account_bank_statement_line +from . import account_move_line +from . import bg_job diff --git a/account_reconcile_bg/models/account_bank_statement_line.py b/account_reconcile_bg/models/account_bank_statement_line.py new file mode 100644 index 00000000..4e4102c7 --- /dev/null +++ b/account_reconcile_bg/models/account_bank_statement_line.py @@ -0,0 +1,84 @@ +############################################################################## +# For copyright and license notices, see __manifest__.py file in module root +# directory +############################################################################## +import logging + +from markupsafe import Markup +from odoo import _, api, fields, models +from odoo.exceptions import UserError + + +class AccountBankStatementLine(models.Model): + _inherit = "account.bank.statement.line" + + reconciliation_in_background = fields.Boolean( + string="Reconciliation in Background", + default=False, + readonly=True, + help="Indicates that this line is being reconciled in background", + ) + + def _bg_validate_reconciliation(self, selected_aml_ids=None): + """ + Método ejecutado en background para validar la conciliación. + Se llama desde el job de base_bg. + + :param selected_aml_ids: IDs de las líneas seleccionadas por el usuario + """ + self.ensure_one() + _logger = logging.getLogger(__name__) + + # Preparar datos para mensaje + base_url = self.env["ir.config_parameter"].sudo().get_param("web.base.url") + st_line_url = f"{base_url}/odoo/account.bank.statement.line/{self.id}" + st_line_name = self.name or f"Line {self.id}" + + try: + # Crear el widget de conciliación + wizard = self.env["bank.rec.widget"].with_context(default_st_line_id=self.id).new({}) + + _logger.info(f"[BG] Wizard created for st_line {self.id}") + + # Agregar las líneas al widget correctamente usando el método interno + if selected_aml_ids: + amls = self.env["account.move.line"].browse(selected_aml_ids) + wizard._action_add_new_amls(amls, allow_partial=False) + + # Ejecutar la validación con el context manager + with wizard._action_validate_method(): + wizard._action_validate() + + # Retornar mensaje de éxito + return Markup( + _("Bank reconciliation completed successfully:
%s") + % (st_line_url, st_line_name) + ) + except Exception as e: + return Markup( + _("Bank reconciliation failed:
%s

Error: %s") + % (st_line_url, st_line_name, str(e)) + ) + finally: + self.write({"reconciliation_in_background": False}) + + @api.constrains( + "amount", + "amount_currency", + "currency_id", + ) + def _check_reconciliation_in_background(self): + """Valida que no se modifiquen líneas en proceso de conciliación background.""" + if self.env.context.get("bg_job"): + return + for line in self: + if line.reconciliation_in_background: + raise UserError( + _( + "Cannot modify payment lines that are being reconciled in background. " + "Please wait until the reconciliation process is complete.\n" + "Journal Entry (id): %(entry)s (%(id)s)", + entry=line.move_id.name, + id=line.move_id.id, + ) + ) diff --git a/account_reconcile_bg/models/account_move_line.py b/account_reconcile_bg/models/account_move_line.py new file mode 100644 index 00000000..17d33f13 --- /dev/null +++ b/account_reconcile_bg/models/account_move_line.py @@ -0,0 +1,16 @@ +############################################################################## +# For copyright and license notices, see __manifest__.py file in module root +# directory +############################################################################## +from odoo import fields, models + + +class AccountMoveLine(models.Model): + _inherit = "account.move.line" + + reconciliation_in_background = fields.Boolean( + string="Reconciliation in Background", + default=False, + readonly=True, + help="Indicates that this line is being reconciled in background", + ) diff --git a/account_reconcile_bg/models/bank_rec_widget.py b/account_reconcile_bg/models/bank_rec_widget.py new file mode 100644 index 00000000..1fb4f9fb --- /dev/null +++ b/account_reconcile_bg/models/bank_rec_widget.py @@ -0,0 +1,120 @@ +############################################################################## +# For copyright and license notices, see __manifest__.py file in module root +# directory +############################################################################## +import logging + +from odoo import _, models +from odoo.exceptions import UserError + + +class BankRecWidget(models.Model): + _inherit = "bank.rec.widget" + + def _js_action_validate(self): + """ + Override para procesar conciliaciones grandes en background. + Si hay muchas líneas (> threshold), usa base_bg para procesarlo en 2do plano. + """ + self.ensure_one() + + # Verificar si ya está procesando en background + if self.st_line_id.reconciliation_in_background: + raise UserError( + _("This reconciliation is already being processed in background. Please wait until it finishes.") + ) + + # Invalidar caché y refrescar para obtener el estado actual desde la BD + self.selected_aml_ids.invalidate_recordset(fnames=["reconciliation_in_background"]) + + # Verificar si alguna de las líneas seleccionadas ya está en background (en cualquier extracto) + lines_in_bg = self.selected_aml_ids.filtered("reconciliation_in_background") + if lines_in_bg: + raise UserError( + _( + "Some of the selected payment lines (%s) are already being reconciled in background on another statement. " + "Please wait until they finish or select different lines." + ) + % len(lines_in_bg) + ) + + # Obtener el umbral de líneas desde parámetros del sistema (default: 50) + threshold = int(self.env["ir.config_parameter"].sudo().get_param("account_reconcile_bg.lines_threshold", "50")) + + # Contar las líneas seleccionadas para conciliar + lines_count = len(self.selected_aml_ids) + + # DEBUG: Log para verificar + + # Si hay pocas líneas, ejecutar el proceso normal de manera sincrónica + if lines_count < threshold: + return super()._js_action_validate() + + # Si hay muchas líneas, procesar en background + return self._validate_in_background() + + def _validate_in_background(self): + """ + Encola la validación de conciliación en background usando base_bg. + Nota: Como bank.rec.widget no se persiste, encolamos usando st_line_id. + """ + self.ensure_one() + + _logger = logging.getLogger(__name__) + + # Marcar la línea de extracto y las líneas de pago como procesando en background + self.st_line_id.write({"reconciliation_in_background": True}) + self.selected_aml_ids.write({"reconciliation_in_background": True}) + + # Flush para asegurar que los cambios se escriben inmediatamente en la BD + # Esto previene condiciones de carrera donde otro usuario podría conciliar las mismas líneas + self.env.flush_all() + + # Capturar los IDs antes de encolar + selected_ids = self.selected_aml_ids.ids + _logger.info(f"[account_reconcile_bg] Capturing selected_aml_ids: {selected_ids}") + + try: + # Encolar el job usando la línea de extracto (modelo persistente) + _action, _jobs = self.env["base.bg"].bg_enqueue_records( + self.st_line_id, + "_bg_validate_reconciliation", + threshold=1, # Un job por línea + name=_("Bank Reconciliation: %s") % self.st_line_id.name, + priority=5, # Alta prioridad + selected_aml_ids=selected_ids, # Pasar solo los IDs (lista de enteros) + ) + _logger.info("[account_reconcile_bg] Job enqueued successfully") + except Exception: + # Si falla al encolar, limpiar los flags + self.st_line_id.write({"reconciliation_in_background": False}) + self.selected_aml_ids.write({"reconciliation_in_background": False}) + raise + + # Enviar notificación al usuario usando el bus + self.env["bus.bus"]._sendone( + self.env.user.partner_id, + "simple_notification", + { + "type": "success", + "message": _( + "This reconciliation is being processed in background. You will be notified when it's done." + ), + }, + ) + + # Configurar el comando para el widget + self.return_todo_command = {"done": True} + + # Retornar vacío - el widget usa return_todo_command + return + + def _do_validate(self): + """ + Método que ejecuta la validación real en background. + Se llama desde el job de base_bg. + """ + self.ensure_one() + # Ejecutar la validación usando el método context manager + with self._action_validate_method(): + self._action_validate() diff --git a/account_reconcile_bg/models/bg_job.py b/account_reconcile_bg/models/bg_job.py new file mode 100644 index 00000000..c6e4a321 --- /dev/null +++ b/account_reconcile_bg/models/bg_job.py @@ -0,0 +1,42 @@ +############################################################################## +# For copyright and license notices, see __manifest__.py file in module root +# directory +############################################################################## +from odoo import models + + +class BgJob(models.Model): + _inherit = "bg.job" + + def cancel(self, message: str | None = None): + """Override para limpiar el flag cuando se cancela el job.""" + res = super().cancel(message=message) + self.filtered( + lambda j: j.model == "account.bank.statement.line" and j.method == "_bg_validate_reconciliation" + )._clean_reconciliation_flag() + return res + + def fail(self, error_message: str): + """Override para limpiar el flag cuando falla el job.""" + res = super().fail(error_message) + self.filtered( + lambda j: j.model == "account.bank.statement.line" and j.method == "_bg_validate_reconciliation" + )._clean_reconciliation_flag() + return res + + def _clean_reconciliation_flag(self): + """Limpia el flag reconciliation_in_background para jobs de conciliación.""" + for job in self: + kwargs = job.kwargs_json or {} + # Limpiar flag de la línea de extracto + record_ids = kwargs.get("_record_ids", []) + if record_ids: + lines = self.env["account.bank.statement.line"].browse(record_ids).exists() + if lines: + lines.write({"reconciliation_in_background": False}) + # Limpiar flag de las líneas de pago (solo las que existen) + selected_aml_ids = kwargs.get("selected_aml_ids", []) + if selected_aml_ids: + amls = self.env["account.move.line"].browse(selected_aml_ids).exists() + if amls: + amls.write({"reconciliation_in_background": False}) diff --git a/account_reconcile_bg/tests/__init__.py b/account_reconcile_bg/tests/__init__.py new file mode 100644 index 00000000..e414fa7e --- /dev/null +++ b/account_reconcile_bg/tests/__init__.py @@ -0,0 +1,2 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from . import test_account_reconcile_bg diff --git a/account_reconcile_bg/tests/test_account_reconcile_bg.py b/account_reconcile_bg/tests/test_account_reconcile_bg.py new file mode 100644 index 00000000..5f8c1cc0 --- /dev/null +++ b/account_reconcile_bg/tests/test_account_reconcile_bg.py @@ -0,0 +1,91 @@ +############################################################################## +# For copyright and license notices, see __manifest__.py file in module root +# directory +############################################################################## +from odoo.addons.account_accountant.tests.test_bank_rec_widget_common import ( + TestBankRecWidgetCommon, +) +from odoo.tests import tagged + + +@tagged("post_install", "-at_install") +class TestAccountReconcileBg(TestBankRecWidgetCommon): + """Test que la conciliación se envía a background cuando hay muchas líneas.""" + + def _create_test_invoices(self, count=10): + """Crea facturas de prueba para conciliar.""" + invoices = self.env["account.move"] + for i in range(count): + invoice = self._create_invoice_line( + "out_invoice", + invoice_line_ids=[{"price_unit": 100.0}], + ) + invoices |= invoice.move_id + return invoices + + def test_sync_below_threshold(self): + """Con pocas líneas (< threshold) debe procesar sincrónico.""" + self.env["ir.config_parameter"].sudo().set_param("account_reconcile_bg.lines_threshold", "3") + + # Crear facturas y línea de extracto (2 < 3 = sync) + invoices = self._create_test_invoices(count=2) + st_line = self._create_st_line(amount=200.0) + + # Contar jobs antes + jobs_before = self.env["bg.job"].search_count([]) + + # Crear widget y seleccionar facturas + wizard = self.env["bank.rec.widget"].with_context(default_st_line_id=st_line.id).new({}) + + # Simular selección de líneas + invoice_lines = invoices.line_ids.filtered(lambda l: l.account_id.account_type == "asset_receivable") + wizard.selected_aml_ids = invoice_lines + + # Validar NO debe crear job (2 < 3) + jobs_after = self.env["bg.job"].search_count([]) + self.assertEqual(jobs_before, jobs_after, "No debe crear jobs en sync") + + def test_background_above_threshold(self): + """Con muchas líneas (>= threshold) debe ir a background y ejecutar correctamente.""" + self.env["ir.config_parameter"].sudo().set_param("account_reconcile_bg.lines_threshold", "2") + + # Crear facturas y línea de extracto (3 >= 2 = background) + invoices = self._create_test_invoices(count=3) + st_line = self._create_st_line(amount=300.0) + + # Crear widget y seleccionar facturas + wizard = self.env["bank.rec.widget"].with_context(default_st_line_id=st_line.id).new({}) + invoice_lines = invoices.line_ids.filtered(lambda l: l.account_id.account_type == "asset_receivable") + wizard.selected_aml_ids = invoice_lines + + # Validar - debe crear job + wizard._js_action_validate() + + # Buscar el job creado (por modelo, método y orden por fecha) + job = self.env["bg.job"].search( + [ + ("model", "=", "account.bank.statement.line"), + ("method", "=", "_bg_validate_reconciliation"), + ], + order="create_date desc", + limit=1, + ) + self.assertTrue(job, "Debe crear un job en background") + self.assertEqual(job.state, "enqueued", "El job debe estar encolado") + self.assertTrue(st_line.reconciliation_in_background, "El flag debe estar activo") + + # Ejecutar el método directamente simulando el contexto que setea bg.job.run() + selected_aml_ids = job.kwargs_json.get("selected_aml_ids", []) + st_line.with_context(bg_job=True, bg_job_id=job.id)._bg_validate_reconciliation( + selected_aml_ids=selected_aml_ids + ) + + # Verificar que el flag se limpió + self.assertFalse(st_line.reconciliation_in_background, "El flag debe estar en False al terminar") + + # Verificar que la línea de extracto está conciliada + self.assertTrue(st_line.is_reconciled, "La línea debe estar conciliada") + + # Verificar que las facturas están conciliadas + for invoice in invoices: + self.assertEqual(invoice.payment_state, "paid", f"La factura {invoice.name} debe estar pagada")