Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions account_reconcile_bg/README.rst
Original file line number Diff line number Diff line change
@@ -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
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

La documentación técnica indica que el job ejecuta bank.rec.widget._do_validate(), pero el código en realidad encola account.bank.statement.line._bg_validate_reconciliation() y no usa _do_validate. Esto hace que el README quede inconsistente con la implementación (y también con la guía de tests). Sugerencia: actualizar el README para reflejar el método/modelo real del job, o cambiar el código para que el job invoque efectivamente _do_validate si ese era el diseño.

Suggested change
* ``_do_validate()``: Executes the actual validation in background
* ``account.bank.statement.line._bg_validate_reconciliation()``: Executes the actual validation in background

Copilot uses AI. Check for mistakes.

Credits
=======

Authors
-------

* ADHOC SA

Contributors
------------

* ADHOC SA

Maintainers
-----------

This module is maintained by ADHOC SA.
2 changes: 2 additions & 0 deletions account_reconcile_bg/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from . import models
37 changes: 37 additions & 0 deletions account_reconcile_bg/__manifest__.py
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
#
##############################################################################
{
"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,
}
10 changes: 10 additions & 0 deletions account_reconcile_bg/data/ir_config_parameter_data.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>

<!-- Parámetro de configuración para el umbral de líneas -->
<record id="config_lines_threshold" model="ir.config_parameter">
<field name="key">account_reconcile_bg.lines_threshold</field>
<field name="value">100</field>
</record>

</odoo>
5 changes: 5 additions & 0 deletions account_reconcile_bg/models/__init__.py
Original file line number Diff line number Diff line change
@@ -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
84 changes: 84 additions & 0 deletions account_reconcile_bg/models/account_bank_statement_line.py
Original file line number Diff line number Diff line change
@@ -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()
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

base_bg solo notifica al usuario si el método del job devuelve un resultado (ver bg.job.run: solo llama a _notify_user si result es truthy). _bg_validate_reconciliation() no retorna nada, por lo que no habrá notificación de finalización, contradiciendo la descripción/README del módulo. Sugerencia: devolver un string/markup con un mensaje (p.ej. link a la línea conciliada) o implementar una notificación explícita al finalizar.

Suggested change
wizard._action_validate()
wizard._action_validate()
return self.env._(
"Reconciliation completed for statement line %s."
) % self.display_name

Copilot uses AI. Check for mistakes.

# Retornar mensaje de éxito
return Markup(
_("Bank reconciliation completed successfully:<br><a href='%s' target='_blank'>%s</a>")
% (st_line_url, st_line_name)
)
except Exception as e:
return Markup(
_("Bank reconciliation failed:<br><a href='%s' target='_blank'>%s</a><br><br>Error: %s")
% (st_line_url, st_line_name, str(e))
)
finally:
self.write({"reconciliation_in_background": False})
Comment on lines +21 to +63
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

El job en background solo reconstruye el widget con default_st_line_id, pero no persiste ni reinyecta la selección del usuario (AMLs elegidas en el widget) antes de validar. Esto puede hacer que el job concilie algo distinto, no concilie nada, o falle, porque no está reproduciendo el estado con el que el usuario presionó "Validar". Recomendación: al encolar, guardar/pasar los selected_aml_ids (o el payload necesario) y en _bg_validate_reconciliation reconstruir line_ids/selección antes de ejecutar _action_validate().

Suggested change
def _bg_validate_reconciliation(self):
"""
Método ejecutado en background para validar la conciliación.
Se llama desde el job de base_bg.
"""
self.ensure_one()
try:
# Crear el widget de conciliación
wizard = self.env["bank.rec.widget"].with_context(default_st_line_id=self.id).new({})
# Ejecutar la validación
with wizard._action_validate_method():
wizard._action_validate()
finally:
# Siempre limpiar el flag al terminar (éxito o error)
self.write({"reconciliation_in_background": False})
reconciliation_selected_aml_ids = fields.Json(
readonly=True,
help="Technical field used to preserve the AML selection for background reconciliation",
)
def _bg_get_selected_aml_ids(self, selected_aml_ids=None):
self.ensure_one()
aml_ids = (
selected_aml_ids
if selected_aml_ids is not None
else self.reconciliation_selected_aml_ids
) or []
return [int(aml_id) for aml_id in aml_ids if aml_id]
def _bg_apply_selected_aml_ids_to_widget(self, wizard, selected_aml_ids):
self.ensure_one()
if not selected_aml_ids or not wizard.line_ids:
return
selected_aml_ids = set(selected_aml_ids)
matched_lines = self.env[wizard.line_ids._name]
for line in wizard.line_ids:
aml_id = False
for field_name in ("aml_id", "move_line_id", "account_move_line_id"):
if field_name in line._fields and line[field_name]:
aml_id = line[field_name].id
break
if aml_id and aml_id in selected_aml_ids:
matched_lines |= line
if not matched_lines:
return
if "is_selected" in matched_lines._fields:
(wizard.line_ids - matched_lines).write({"is_selected": False})
matched_lines.write({"is_selected": True})
else:
wizard.line_ids = matched_lines
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.
"""
self.ensure_one()
selected_aml_ids = self._bg_get_selected_aml_ids(selected_aml_ids)
try:
# Crear el widget de conciliación
wizard = self.env["bank.rec.widget"].with_context(
default_st_line_id=self.id
).new({})
# Reinyectar la selección original del usuario antes de validar
self._bg_apply_selected_aml_ids_to_widget(wizard, selected_aml_ids)
# Ejecutar la validación
with wizard._action_validate_method():
wizard._action_validate()
finally:
# Siempre limpiar el flag al terminar (éxito o error)
self.write(
{
"reconciliation_in_background": False,
"reconciliation_selected_aml_ids": False,
}
)

Copilot uses AI. Check for mistakes.

@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,
)
)
16 changes: 16 additions & 0 deletions account_reconcile_bg/models/account_move_line.py
Original file line number Diff line number Diff line change
@@ -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",
)
120 changes: 120 additions & 0 deletions account_reconcile_bg/models/bank_rec_widget.py
Original file line number Diff line number Diff line change
@@ -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)
Comment on lines +41 to +45
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

El parámetro account_reconcile_bg.lines_threshold se convierte con int(...) sin validar. Si el sysparam queda con un valor no numérico (o vacío), esto va a lanzar ValueError y romper la validación del widget. Sugerencia: capturar la excepción y usar fallback seguro (p.ej. 50) o levantar UserError indicando que el parámetro debe ser un entero.

Copilot uses AI. Check for mistakes.

# 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()
Comment on lines +47 to +51
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Se está importando logging dentro del método y se están escribiendo logs a nivel INFO en cada validación (incluyendo mensajes marcados como "DEBUG"). Esto puede generar ruido significativo en producción. Recomendación: declarar _logger = logging.getLogger(__name__) a nivel de módulo y bajar estos mensajes a debug (o condicionarlos).

Copilot uses AI. Check for mistakes.

# 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()
Loading
Loading