From 7ceadd9fc642f6cee18149dddb73708d1dbe610d Mon Sep 17 00:00:00 2001 From: "Laurent Mignon (ACSONE)" Date: Tue, 4 Jul 2023 13:38:04 +0200 Subject: [PATCH 1/4] [IMP] base_exception: One model per python file --- base_exception/models/__init__.py | 2 + base_exception/models/base_exception.py | 195 +----------------- .../models/base_exception_method.py | 149 +++++++++++++ base_exception/models/exception_rule.py | 72 +++++++ 4 files changed, 224 insertions(+), 194 deletions(-) create mode 100644 base_exception/models/base_exception_method.py create mode 100644 base_exception/models/exception_rule.py diff --git a/base_exception/models/__init__.py b/base_exception/models/__init__.py index 495e1fe24..644c12a52 100644 --- a/base_exception/models/__init__.py +++ b/base_exception/models/__init__.py @@ -1 +1,3 @@ +from . import exception_rule +from . import base_exception_method from . import base_exception diff --git a/base_exception/models/base_exception.py b/base_exception/models/base_exception.py index 0ea2a31bd..bd418d7ba 100644 --- a/base_exception/models/base_exception.py +++ b/base_exception/models/base_exception.py @@ -2,6 +2,7 @@ # Copyright 2017 Akretion (http://www.akretion.com) # Mourad EL HADJ MIMOUNE # Copyright 2020 Hibou Corp. +# Copyright 2023 ACSONE SA/NV (http://acsone.eu) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). import html @@ -9,204 +10,10 @@ import logging from odoo import _, api, fields, models from odoo.exceptions import UserError, ValidationError -from odoo.osv import expression -from odoo.tools.safe_eval import safe_eval _logger = logging.getLogger(__name__) -class ExceptionRule(models.Model): - _name = "exception.rule" - _description = "Exception Rule" - _order = "active desc, sequence asc" - - name = fields.Char("Exception Name", required=True, translate=True) - description = fields.Text(translate=True) - sequence = fields.Integer(help="Gives the sequence order when applying the test") - model = fields.Selection(selection=[], string="Apply on", required=True) - - exception_type = fields.Selection( - selection=[ - ("by_domain", "By domain"), - ("by_py_code", "By python code"), - ("by_method", "By method"), - ], - required=True, - default="by_py_code", - help="By python code: allow to define any arbitrary check\n" - "By domain: limited to a selection by an odoo domain:\n" - " performance can be better when exceptions" - " are evaluated with several records\n" - "By method: allow to select an existing check method", - ) - domain = fields.Char() - method = fields.Selection(selection=[], readonly=True) - active = fields.Boolean(default=True) - code = fields.Text( - "Python Code", - help="Python code executed to check if the exception apply or " - "not. Use failed = True to block the exception", - ) - is_blocking = fields.Boolean( - help="When checked the exception can not be ignored", - ) - - @api.constrains("exception_type", "domain", "code", "model") - def check_exception_type_consistency(self): - for rule in self: - if ( - (rule.exception_type == "by_py_code" and not rule.code) - or (rule.exception_type == "by_domain" and not rule.domain) - or (rule.exception_type == "by_method" and not rule.method) - ): - raise ValidationError( - _( - "There is a problem of configuration, python code, " - "domain or method is missing to match the exception " - "type." - ) - ) - - def _get_domain(self): - """override me to customize domains according exceptions cases""" - self.ensure_one() - return safe_eval(self.domain) - - -class BaseExceptionMethod(models.AbstractModel): - _name = "base.exception.method" - _description = "Exception Rule Methods" - - def _get_main_records(self): - """ - Used in case we check exceptions on a record but write these - exceptions on a parent record. Typical example is with - sale.order.line. We check exceptions on some sale order lines but - write these exceptions on the sale order, so they are visible. - """ - return self - - def _reverse_field(self): - raise NotImplementedError() - - def _rule_domain(self): - """Filter exception.rules. - By default, only the rules with the correct model - will be used. - """ - return [("model", "=", self._name), ("active", "=", True)] - - def detect_exceptions(self): - """List all exception_ids applied on self - Exception ids are also written on records - """ - rules = self.env["exception.rule"].sudo().search(self._rule_domain()) - all_exception_ids = [] - rules_to_remove = {} - rules_to_add = {} - for rule in rules: - records_with_exception = self._detect_exceptions(rule) - reverse_field = self._reverse_field() - main_records = self._get_main_records() - commons = main_records & rule[reverse_field] - to_remove = commons - records_with_exception - to_add = records_with_exception - commons - # we expect to always work on the same model type - if rule.id not in rules_to_remove: - rules_to_remove[rule.id] = main_records.browse() - rules_to_remove[rule.id] |= to_remove - if rule.id not in rules_to_add: - rules_to_add[rule.id] = main_records.browse() - rules_to_add[rule.id] |= to_add - if records_with_exception: - all_exception_ids.append(rule.id) - # Cumulate all the records to attach to the rule - # before linking. We don't want to call "rule.write()" - # which would: - # * write on write_date so lock the exception.rule - # * trigger the recomputation of "main_exception_id" on - # all the sale orders related to the rule, locking them all - # and preventing concurrent writes - # Reversing the write by writing on SaleOrder instead of - # ExceptionRule fixes the 2 kinds of unexpected locks. - # It should not result in more queries than writing on ExceptionRule: - # 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)]}) - return all_exception_ids - - @api.model - def _exception_rule_eval_context(self, rec): - return { - "self": rec, - "object": rec, - "obj": rec, - } - - @api.model - def _rule_eval(self, rule, rec): - expr = rule.code - space = self._exception_rule_eval_context(rec) - try: - safe_eval( - expr, space, mode="exec", nocopy=True - ) # nocopy allows to return 'result' - except Exception as e: - _logger.exception(e) - raise UserError( - _( - "Error when evaluating the exception.rule" - " rule:\n %(rule_name)s \n(%(error)s)" - ) - % {"rule_name": rule.name, "error": e} - ) from e - return space.get("failed", False) - - def _detect_exceptions(self, rule): - if rule.exception_type == "by_py_code": - return self._detect_exceptions_by_py_code(rule) - elif rule.exception_type == "by_domain": - return self._detect_exceptions_by_domain(rule) - elif rule.exception_type == "by_method": - return self._detect_exceptions_by_method(rule) - - def _get_base_domain(self): - return [("ignore_exception", "=", False), ("id", "in", self.ids)] - - def _detect_exceptions_by_py_code(self, rule): - """ - Find exceptions found on self. - """ - domain = self._get_base_domain() - records = self.search(domain) - records_with_exception = self.env[self._name] - for record in records: - if self._rule_eval(rule, record): - records_with_exception |= record - return records_with_exception - - def _detect_exceptions_by_domain(self, rule): - """ - Find exceptions found on self. - """ - base_domain = self._get_base_domain() - rule_domain = rule._get_domain() - domain = expression.AND([base_domain, rule_domain]) - return self.search(domain) - - def _detect_exceptions_by_method(self, rule): - """ - Find exceptions found on self. - """ - base_domain = self._get_base_domain() - records = self.search(base_domain) - return getattr(records, rule.method)() - - class BaseExceptionModel(models.AbstractModel): _inherit = "base.exception.method" _name = "base.exception" diff --git a/base_exception/models/base_exception_method.py b/base_exception/models/base_exception_method.py new file mode 100644 index 000000000..7926f8776 --- /dev/null +++ b/base_exception/models/base_exception_method.py @@ -0,0 +1,149 @@ +# Copyright 2011 Raphaël Valyi, Renato Lima, Guewen Baconnier, Sodexis +# Copyright 2017 Akretion (http://www.akretion.com) +# Mourad EL HADJ MIMOUNE +# Copyright 2020 Hibou Corp. +# Copyright 2023 ACSONE SA/NV (http://acsone.eu) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +import logging + +from odoo import _, api, models +from odoo.exceptions import UserError +from odoo.osv import expression +from odoo.tools.safe_eval import safe_eval + +_logger = logging.getLogger(__name__) + + +class BaseExceptionMethod(models.AbstractModel): + _name = "base.exception.method" + _description = "Exception Rule Methods" + + def _get_main_records(self): + """ + Used in case we check exceptions on a record but write these + exceptions on a parent record. Typical example is with + sale.order.line. We check exceptions on some sale order lines but + write these exceptions on the sale order, so they are visible. + """ + return self + + def _reverse_field(self): + raise NotImplementedError() + + def _rule_domain(self): + """Filter exception.rules. + By default, only the rules with the correct model + will be used. + """ + return [("model", "=", self._name), ("active", "=", True)] + + def detect_exceptions(self): + """List all exception_ids applied on self + Exception ids are also written on records + """ + rules = self.env["exception.rule"].sudo().search(self._rule_domain()) + all_exception_ids = [] + rules_to_remove = {} + rules_to_add = {} + for rule in rules: + records_with_exception = self._detect_exceptions(rule) + reverse_field = self._reverse_field() + main_records = self._get_main_records() + commons = main_records & rule[reverse_field] + to_remove = commons - records_with_exception + to_add = records_with_exception - commons + # we expect to always work on the same model type + if rule.id not in rules_to_remove: + rules_to_remove[rule.id] = main_records.browse() + rules_to_remove[rule.id] |= to_remove + if rule.id not in rules_to_add: + rules_to_add[rule.id] = main_records.browse() + rules_to_add[rule.id] |= to_add + if records_with_exception: + all_exception_ids.append(rule.id) + # Cumulate all the records to attach to the rule + # before linking. We don't want to call "rule.write()" + # which would: + # * write on write_date so lock the exception.rule + # * trigger the recomputation of "main_exception_id" on + # all the sale orders related to the rule, locking them all + # and preventing concurrent writes + # Reversing the write by writing on SaleOrder instead of + # ExceptionRule fixes the 2 kinds of unexpected locks. + # It should not result in more queries than writing on ExceptionRule: + # 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)]}) + return all_exception_ids + + @api.model + def _exception_rule_eval_context(self, rec): + return { + "self": rec, + "object": rec, + "obj": rec, + } + + @api.model + def _rule_eval(self, rule, rec): + expr = rule.code + space = self._exception_rule_eval_context(rec) + try: + safe_eval( + expr, space, mode="exec", nocopy=True + ) # nocopy allows to return 'result' + except Exception as e: + _logger.exception(e) + raise UserError( + _( + "Error when evaluating the exception.rule" + " rule:\n %(rule_name)s \n(%(error)s)" + ) + % {"rule_name": rule.name, "error": e} + ) from e + return space.get("failed", False) + + def _detect_exceptions(self, rule): + if rule.exception_type == "by_py_code": + return self._detect_exceptions_by_py_code(rule) + elif rule.exception_type == "by_domain": + return self._detect_exceptions_by_domain(rule) + elif rule.exception_type == "by_method": + return self._detect_exceptions_by_method(rule) + + def _get_base_domain(self): + return [("ignore_exception", "=", False), ("id", "in", self.ids)] + + def _detect_exceptions_by_py_code(self, rule): + """ + Find exceptions found on self. + """ + domain = self._get_base_domain() + records = self.search(domain) + records_with_exception = self.env[self._name] + for record in records: + if self._rule_eval(rule, record): + records_with_exception |= record + return records_with_exception + + def _detect_exceptions_by_domain(self, rule): + """ + Find exceptions found on self. + """ + base_domain = self._get_base_domain() + rule_domain = rule._get_domain() + domain = expression.AND([base_domain, rule_domain]) + return self.search(domain) + + def _detect_exceptions_by_method(self, rule): + """ + Find exceptions found on self. + """ + base_domain = self._get_base_domain() + records = self.search(base_domain) + return getattr(records, rule.method)() diff --git a/base_exception/models/exception_rule.py b/base_exception/models/exception_rule.py new file mode 100644 index 000000000..8cc463cd1 --- /dev/null +++ b/base_exception/models/exception_rule.py @@ -0,0 +1,72 @@ +# Copyright 2011 Raphaël Valyi, Renato Lima, Guewen Baconnier, Sodexis +# Copyright 2017 Akretion (http://www.akretion.com) +# Mourad EL HADJ MIMOUNE +# Copyright 2020 Hibou Corp. +# Copyright 2023 ACSONE SA/NV (http://acsone.eu) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +import logging + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError +from odoo.tools.safe_eval import safe_eval + +_logger = logging.getLogger(__name__) + + +class ExceptionRule(models.Model): + _name = "exception.rule" + _description = "Exception Rule" + _order = "active desc, sequence asc" + + name = fields.Char("Exception Name", required=True, translate=True) + description = fields.Text(translate=True) + sequence = fields.Integer(help="Gives the sequence order when applying the test") + model = fields.Selection(selection=[], string="Apply on", required=True) + + exception_type = fields.Selection( + selection=[ + ("by_domain", "By domain"), + ("by_py_code", "By python code"), + ("by_method", "By method"), + ], + required=True, + default="by_py_code", + help="By python code: allow to define any arbitrary check\n" + "By domain: limited to a selection by an odoo domain:\n" + " performance can be better when exceptions" + " are evaluated with several records\n" + "By method: allow to select an existing check method", + ) + domain = fields.Char() + method = fields.Selection(selection=[], readonly=True) + active = fields.Boolean(default=True) + code = fields.Text( + "Python Code", + help="Python code executed to check if the exception apply or " + "not. Use failed = True to block the exception", + ) + is_blocking = fields.Boolean( + help="When checked the exception can not be ignored", + ) + + @api.constrains("exception_type", "domain", "code", "model") + def check_exception_type_consistency(self): + for rule in self: + if ( + (rule.exception_type == "by_py_code" and not rule.code) + or (rule.exception_type == "by_domain" and not rule.domain) + or (rule.exception_type == "by_method" and not rule.method) + ): + raise ValidationError( + _( + "There is a problem of configuration, python code, " + "domain or method is missing to match the exception " + "type." + ) + ) + + def _get_domain(self): + """override me to customize domains according exceptions cases""" + self.ensure_one() + return safe_eval(self.domain) From b5099bc51ea53592cefd5162b6ce72cc5e6edeee Mon Sep 17 00:00:00 2001 From: "Laurent Mignon (ACSONE)" Date: Tue, 4 Jul 2023 15:32:33 +0200 Subject: [PATCH 2/4] [IMP] base_exception: Improves performances Before this change, the inverse relation from the exception to the records linked to the exception was read to determine if the rule should be added or removed from the rule detected for the current records. On large database with non blocking exceptions this lead to a performance issues since this issues a read of all the SO where the exception has been applied. With this change, we only read the information from the current records --- base_exception/models/base_exception_method.py | 14 ++++++-------- base_exception/tests/purchase_test.py | 3 --- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/base_exception/models/base_exception_method.py b/base_exception/models/base_exception_method.py index 7926f8776..4d51f7bcb 100644 --- a/base_exception/models/base_exception_method.py +++ b/base_exception/models/base_exception_method.py @@ -28,9 +28,6 @@ class BaseExceptionMethod(models.AbstractModel): """ return self - def _reverse_field(self): - raise NotImplementedError() - def _rule_domain(self): """Filter exception.rules. By default, only the rules with the correct model @@ -47,12 +44,13 @@ class BaseExceptionMethod(models.AbstractModel): rules_to_remove = {} rules_to_add = {} for rule in rules: - records_with_exception = self._detect_exceptions(rule) - reverse_field = self._reverse_field() main_records = self._get_main_records() - commons = main_records & rule[reverse_field] - to_remove = commons - records_with_exception - to_add = records_with_exception - commons + records_with_rule_in_exceptions = main_records.filtered( + lambda r, rule_id=rule.id: rule_id in r.exception_ids.ids + ) + records_with_exception = self._detect_exceptions(rule) + to_remove = records_with_rule_in_exceptions - records_with_exception + to_add = records_with_exception - records_with_rule_in_exceptions # we expect to always work on the same model type if rule.id not in rules_to_remove: rules_to_remove[rule.id] = main_records.browse() diff --git a/base_exception/tests/purchase_test.py b/base_exception/tests/purchase_test.py index b1bc61ae6..65dac0ce7 100644 --- a/base_exception/tests/purchase_test.py +++ b/base_exception/tests/purchase_test.py @@ -69,9 +69,6 @@ class PurchaseTest(models.Model): def button_cancel(self): self.write({"state": "cancel"}) - def _reverse_field(self): - return "test_purchase_ids" - def exception_method_no_zip(self): records_fail = self.env["base.exception.test.purchase"] for rec in self: From 254c5684e3712cdc9b110f028cd05d18df5cdc5d Mon Sep 17 00:00:00 2001 From: "Laurent Mignon (ACSONE)" Date: Tue, 4 Jul 2023 15:34:32 +0200 Subject: [PATCH 3/4] [IMP] base_exception: Improves performances Keeps rule information into cache to avoid to always call the ORM for static informations. This is change allows a boost into the evaluation of exception rules by avoiding useless SQL queries to retrieve rules definitions --- base_exception/__manifest__.py | 2 +- .../models/base_exception_method.py | 58 ++++++++-------- base_exception/models/exception_rule.py | 68 ++++++++++++++++++- base_exception/readme/CONTRIBUTORS.rst | 1 + 4 files changed, 100 insertions(+), 29 deletions(-) diff --git a/base_exception/__manifest__.py b/base_exception/__manifest__.py index 95c9078be..a9d7d96f0 100644 --- a/base_exception/__manifest__.py +++ b/base_exception/__manifest__.py @@ -11,7 +11,7 @@ "summary": """ This module provide an abstract model to manage customizable exceptions to be applied on different models (sale order, invoice, ...)""", - "author": "Akretion, Sodexis, Camptocamp, Odoo Community Association (OCA)", + "author": "Akretion, Sodexis, Camptocamp, ACSONE SA/NV, Odoo Community Association (OCA)", "website": "https://github.com/OCA/server-tools", "depends": ["base_setup"], "maintainers": ["hparfr", "sebastienbeau"], diff --git a/base_exception/models/base_exception_method.py b/base_exception/models/base_exception_method.py index 4d51f7bcb..6fa493b9c 100644 --- a/base_exception/models/base_exception_method.py +++ b/base_exception/models/base_exception_method.py @@ -39,27 +39,31 @@ class BaseExceptionMethod(models.AbstractModel): """List all exception_ids applied on self Exception ids are also written on records """ - rules = self.env["exception.rule"].sudo().search(self._rule_domain()) + rules_info = ( + self.env["exception.rule"] + .sudo() + ._get_rules_info_for_domain(self._rule_domain()) + ) all_exception_ids = [] rules_to_remove = {} rules_to_add = {} - for rule in rules: + for rule_info in rules_info: main_records = self._get_main_records() records_with_rule_in_exceptions = main_records.filtered( - lambda r, rule_id=rule.id: rule_id in r.exception_ids.ids + lambda r, rule_id=rule_info.id: rule_id in r.exception_ids.ids ) - records_with_exception = self._detect_exceptions(rule) + records_with_exception = self._detect_exceptions(rule_info) to_remove = records_with_rule_in_exceptions - records_with_exception to_add = records_with_exception - records_with_rule_in_exceptions # we expect to always work on the same model type - if rule.id not in rules_to_remove: - rules_to_remove[rule.id] = main_records.browse() - rules_to_remove[rule.id] |= to_remove - if rule.id not in rules_to_add: - rules_to_add[rule.id] = main_records.browse() - rules_to_add[rule.id] |= to_add + if rule_info.id not in rules_to_remove: + rules_to_remove[rule_info.id] = main_records.browse() + rules_to_remove[rule_info.id] |= to_remove + if rule_info.id not in rules_to_add: + rules_to_add[rule_info.id] = main_records.browse() + rules_to_add[rule_info.id] |= to_add if records_with_exception: - all_exception_ids.append(rule.id) + all_exception_ids.append(rule_info.id) # Cumulate all the records to attach to the rule # before linking. We don't want to call "rule.write()" # which would: @@ -88,8 +92,8 @@ class BaseExceptionMethod(models.AbstractModel): } @api.model - def _rule_eval(self, rule, rec): - expr = rule.code + def _rule_eval(self, rule_info, rec): + expr = rule_info.code space = self._exception_rule_eval_context(rec) try: safe_eval( @@ -102,22 +106,22 @@ class BaseExceptionMethod(models.AbstractModel): "Error when evaluating the exception.rule" " rule:\n %(rule_name)s \n(%(error)s)" ) - % {"rule_name": rule.name, "error": e} + % {"rule_name": rule_info.name, "error": e} ) from e return space.get("failed", False) - def _detect_exceptions(self, rule): - if rule.exception_type == "by_py_code": - return self._detect_exceptions_by_py_code(rule) - elif rule.exception_type == "by_domain": - return self._detect_exceptions_by_domain(rule) - elif rule.exception_type == "by_method": - return self._detect_exceptions_by_method(rule) + def _detect_exceptions(self, rule_info): + if rule_info.exception_type == "by_py_code": + return self._detect_exceptions_by_py_code(rule_info) + elif rule_info.exception_type == "by_domain": + return self._detect_exceptions_by_domain(rule_info) + elif rule_info.exception_type == "by_method": + return self._detect_exceptions_by_method(rule_info) def _get_base_domain(self): return [("ignore_exception", "=", False), ("id", "in", self.ids)] - def _detect_exceptions_by_py_code(self, rule): + def _detect_exceptions_by_py_code(self, rule_info): """ Find exceptions found on self. """ @@ -125,23 +129,23 @@ class BaseExceptionMethod(models.AbstractModel): records = self.search(domain) records_with_exception = self.env[self._name] for record in records: - if self._rule_eval(rule, record): + if self._rule_eval(rule_info, record): records_with_exception |= record return records_with_exception - def _detect_exceptions_by_domain(self, rule): + def _detect_exceptions_by_domain(self, rule_info): """ Find exceptions found on self. """ base_domain = self._get_base_domain() - rule_domain = rule._get_domain() + rule_domain = rule_info.domain domain = expression.AND([base_domain, rule_domain]) return self.search(domain) - def _detect_exceptions_by_method(self, rule): + def _detect_exceptions_by_method(self, rule_info): """ Find exceptions found on self. """ base_domain = self._get_base_domain() records = self.search(base_domain) - return getattr(records, rule.method)() + return getattr(records, rule_info.method)() diff --git a/base_exception/models/exception_rule.py b/base_exception/models/exception_rule.py index 8cc463cd1..1419eb916 100644 --- a/base_exception/models/exception_rule.py +++ b/base_exception/models/exception_rule.py @@ -7,7 +7,7 @@ import logging -from odoo import _, api, fields, models +from odoo import _, api, fields, models, tools from odoo.exceptions import ValidationError from odoo.tools.safe_eval import safe_eval @@ -70,3 +70,69 @@ class ExceptionRule(models.Model): """override me to customize domains according exceptions cases""" self.ensure_one() return safe_eval(self.domain) + + def _get_rules_info_for_domain(self, domain): + """returns the rules that match the domain + + This method will call _get_cached_rules_for_domain to get the rules + that match the domain. This is required to transform the domain + into a tuple to be used as a key in the cache. + """ + return self._get_cached_rules_for_domain(tuple(domain)) + + @api.model + @tools.ormcache_context("domain", keys=("lang",)) + def _get_cached_rules_for_domain(self, domain): + """This method is used to get the rules that match the domain. + + The result is cached to avoid to have to loockup the database every + time the method is called for rules that never change. + + Recordset are transformed into a dict and then into an object that have + the same attributes as the exception.rule model. If you need to add + new attributes to the exception.rule model, you need to add them to + the dict returned by _to_cache_entry method. + """ + return [ + type("RuleInfo", (), r._to_cache_entry()) for r in self.search(list(domain)) + ] + + def _to_cache_entry(self): + """ + This method is used to extract information from the rule to be put + in cache. It's used by _get_cached_rules_for_domain to avoid to put + the recordset in cache. The goal is to avoid to have to loockup + the database to get the information required to apply the rule + each time the rule is applied. + """ + self.ensure_one() + return { + "id": self.id, + "name": self.name, + "description": self.description, + "sequence": self.sequence, + "model": self.model, + "exception_type": self.exception_type, + "domain": self._get_domain() + if self.exception_type == "by_domain" + else None, + "method": self.method, + "code": self.code, + "is_blocking": self.is_blocking, + } + + @api.model_create_multi + def create(self, vals_list): + res = super().create(vals_list) + self._get_cached_rules_for_domain.clear_cache(self) + return res + + def write(self, vals): + res = super().write(vals) + self._get_cached_rules_for_domain.clear_cache(self) + return res + + def unlink(self): + res = super().unlink() + self._get_cached_rules_for_domain.clear_cache(self) + return res diff --git a/base_exception/readme/CONTRIBUTORS.rst b/base_exception/readme/CONTRIBUTORS.rst index 2786adc47..456dc3af8 100644 --- a/base_exception/readme/CONTRIBUTORS.rst +++ b/base_exception/readme/CONTRIBUTORS.rst @@ -13,3 +13,4 @@ * João Marques * Kevin Khao +* Laurent Mignon From 31c074cdc3b052918f4a8d64b63ef778440e97c9 Mon Sep 17 00:00:00 2001 From: "Laurent Mignon (ACSONE)" Date: Tue, 4 Jul 2023 15:41:47 +0200 Subject: [PATCH 4/4] [IMP] base_exception: Improves performances Don't call the ORM to filter current recordset. Replace call to the orm by a call to the filtered_domain method. --- base_exception/models/base_exception_method.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/base_exception/models/base_exception_method.py b/base_exception/models/base_exception_method.py index 6fa493b9c..9261cfbc9 100644 --- a/base_exception/models/base_exception_method.py +++ b/base_exception/models/base_exception_method.py @@ -119,14 +119,14 @@ class BaseExceptionMethod(models.AbstractModel): return self._detect_exceptions_by_method(rule_info) def _get_base_domain(self): - return [("ignore_exception", "=", False), ("id", "in", self.ids)] + return [("ignore_exception", "=", False)] def _detect_exceptions_by_py_code(self, rule_info): """ Find exceptions found on self. """ domain = self._get_base_domain() - records = self.search(domain) + records = self.filtered_domain(domain) records_with_exception = self.env[self._name] for record in records: if self._rule_eval(rule_info, record): @@ -140,12 +140,12 @@ class BaseExceptionMethod(models.AbstractModel): base_domain = self._get_base_domain() rule_domain = rule_info.domain domain = expression.AND([base_domain, rule_domain]) - return self.search(domain) + return self.filtered_domain(domain) def _detect_exceptions_by_method(self, rule_info): """ Find exceptions found on self. """ base_domain = self._get_base_domain() - records = self.search(base_domain) + records = self.filtered_domain(base_domain) return getattr(records, rule_info.method)()