From cda3f3357a41dbb4883bc144ecee1a25961f2a36 Mon Sep 17 00:00:00 2001 From: Akim Juillerat Date: Wed, 24 Dec 2025 18:12:45 +0100 Subject: [PATCH 1/3] [POC] base_exception: Rollback transaction triggering exception rule In case a implementation module, eg sale_exception, breaks a function override, ie does not call super on sale.order.action_confirm in case an exception rule applies, it is still possible that another module that is not in the implementation module's dependency, does modify existing the same object through the same function's override that is called before through MRO. As the exception rule is only written in base_exception module, such modifications would be still be committed although the exception was triggered. The only way to rollback everything that happened is to raise an exception that is going to rollback the transaction. However, we still want to commit the write of the exception rule and not to propagate the exception back to the webclient. Since we cannot alter the env in the retrying function (that handles rollback), we do not have any other choice than to use a dedicated cursor in the dispatching function to process the request, since it could trigger the exception rule, and to use the original env to write the exception rule, and have the webclient being refreshed to display the exception. This might break the latest feature to pop up the wizard, but since this is still a POC, we could fix that afterwards in case this POC is accepted. --- base_exception/exceptions.py | 11 +++++ base_exception/models/__init__.py | 1 + .../models/base_exception_method.py | 19 +++++++-- base_exception/models/ir_http.py | 40 +++++++++++++++++++ 4 files changed, 67 insertions(+), 4 deletions(-) create mode 100644 base_exception/exceptions.py create mode 100644 base_exception/models/ir_http.py diff --git a/base_exception/exceptions.py b/base_exception/exceptions.py new file mode 100644 index 00000000000..47082ad91b4 --- /dev/null +++ b/base_exception/exceptions.py @@ -0,0 +1,11 @@ +# Copyright 2025 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) + +from odoo.exceptions import ValidationError + + +class BaseExceptionError(ValidationError): + def __init__(self, msg, rules_to_add, rules_to_remove): + super().__init__(msg) + self.rules_to_add = rules_to_add + self.rules_to_remove = rules_to_remove diff --git a/base_exception/models/__init__.py b/base_exception/models/__init__.py index 644c12a5295..1e12b85c3d7 100644 --- a/base_exception/models/__init__.py +++ b/base_exception/models/__init__.py @@ -1,3 +1,4 @@ from . import exception_rule from . import base_exception_method from . import base_exception +from . import ir_http diff --git a/base_exception/models/base_exception_method.py b/base_exception/models/base_exception_method.py index 61923098aaf..dc09db6d036 100644 --- a/base_exception/models/base_exception_method.py +++ b/base_exception/models/base_exception_method.py @@ -12,6 +12,8 @@ from odoo.osv import expression from odoo.tools.safe_eval import safe_eval +from ..exceptions import BaseExceptionError + _logger = logging.getLogger(__name__) @@ -71,6 +73,7 @@ def detect_exceptions(self): Exception ids are also written on records """ all_exception_ids, rules_to_remove, rules_to_add = self._get_exceptions() + # TODO: Remove outdated comment? # Cumulate all the records to attach to the rule # before linking. We don't want to call "rule.write()" # which would: @@ -84,10 +87,18 @@ def detect_exceptions(self): # the "to remove" part generates one DELETE per rule on the relation # table # and the "to add" part generates one INSERT (with unnest) per rule. - for rule_id, records in rules_to_remove.items(): - records.write({"exception_ids": [(3, rule_id)]}) - for rule_id, records in rules_to_add.items(): - records.write({"exception_ids": [(4, rule_id)]}) + if rules_to_add or rules_to_remove: + raise BaseExceptionError( + "Exception on records", + { + rule_id: (records._name, records.ids) + for rule_id, records in rules_to_add.items() + }, + { + rule_id: (records._name, records.ids) + for rule_id, records in rules_to_remove.items() + }, + ) return all_exception_ids @api.model diff --git a/base_exception/models/ir_http.py b/base_exception/models/ir_http.py new file mode 100644 index 00000000000..063bc223347 --- /dev/null +++ b/base_exception/models/ir_http.py @@ -0,0 +1,40 @@ +# Copyright 2025 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) + +from odoo import models +from odoo.api import Environment +from odoo.http import request +from odoo.modules.registry import Registry + +from ..exceptions import BaseExceptionError + + +class IrHttp(models.AbstractModel): + _inherit = "ir.http" + + @classmethod + def _dispatch(cls, endpoint): + res = None + # FIXME: Find a way to condition the creation of new transaction + # only for requests that may trigger an exception rule + # ie exclude whatever goes to bus, websocket, getting views, etc + old_env = request.env + to_add = {} + to_remove = {} + with Registry(old_env.cr.dbname).cursor() as new_cr: + new_env = Environment(new_cr, old_env.uid, old_env.context) + request.env = new_env + try: + res = super()._dispatch(endpoint) + except BaseExceptionError as err: + to_add = err.rules_to_add + to_remove = err.rules_to_remove + new_env.cr.rollback() + + for rule_id, (model, res_ids) in to_add.items(): + old_env[model].browse(res_ids).write({"exception_ids": [(4, rule_id)]}) + for rule_id, (model, res_ids) in to_remove.items(): + old_env[model].browse(res_ids).write({"exception_ids": [(4, rule_id)]}) + + request.env = old_env + return res From 365e9cb4f6f3a1d2ed69c558e0e3dfaed7b5b354 Mon Sep 17 00:00:00 2001 From: Akim Juillerat Date: Fri, 2 Jan 2026 14:00:00 +0100 Subject: [PATCH 2/3] fixup! [POC] base_exception: Rollback transaction triggering exception rule --- base_exception/models/ir_http.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/base_exception/models/ir_http.py b/base_exception/models/ir_http.py index 063bc223347..ad1f36ba116 100644 --- a/base_exception/models/ir_http.py +++ b/base_exception/models/ir_http.py @@ -3,6 +3,7 @@ from odoo import models from odoo.api import Environment +from odoo.fields import Command from odoo.http import request from odoo.modules.registry import Registry @@ -31,10 +32,10 @@ def _dispatch(cls, endpoint): to_remove = err.rules_to_remove new_env.cr.rollback() - for rule_id, (model, res_ids) in to_add.items(): - old_env[model].browse(res_ids).write({"exception_ids": [(4, rule_id)]}) for rule_id, (model, res_ids) in to_remove.items(): - old_env[model].browse(res_ids).write({"exception_ids": [(4, rule_id)]}) + old_env[model].browse(res_ids).write({"exception_ids": [Command.unlink(rule_id)]}) + for rule_id, (model, res_ids) in to_add.items(): + old_env[model].browse(res_ids).write({"exception_ids": [Command.link(rule_id)]}) request.env = old_env return res From 7aa6c4e192f48efb5abb8583c9d603e6a60cf7b0 Mon Sep 17 00:00:00 2001 From: Akim Juillerat Date: Fri, 2 Jan 2026 16:53:03 +0100 Subject: [PATCH 3/3] fixup! fixup! [POC] base_exception: Rollback transaction triggering exception rule --- base_exception/models/ir_http.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/base_exception/models/ir_http.py b/base_exception/models/ir_http.py index ad1f36ba116..8e11212e0a4 100644 --- a/base_exception/models/ir_http.py +++ b/base_exception/models/ir_http.py @@ -33,9 +33,13 @@ def _dispatch(cls, endpoint): new_env.cr.rollback() for rule_id, (model, res_ids) in to_remove.items(): - old_env[model].browse(res_ids).write({"exception_ids": [Command.unlink(rule_id)]}) + old_env[model].browse(res_ids).write( + {"exception_ids": [Command.unlink(rule_id)]} + ) for rule_id, (model, res_ids) in to_add.items(): - old_env[model].browse(res_ids).write({"exception_ids": [Command.link(rule_id)]}) + old_env[model].browse(res_ids).write( + {"exception_ids": [Command.link(rule_id)]} + ) request.env = old_env return res