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..8e11212e0a4 --- /dev/null +++ b/base_exception/models/ir_http.py @@ -0,0 +1,45 @@ +# 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.fields import Command +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_remove.items(): + 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