diff --git a/base_exception/models/base_exception.py b/base_exception/models/base_exception.py index b6b0d6fb0..d3619e104 100644 --- a/base_exception/models/base_exception.py +++ b/base_exception/models/base_exception.py @@ -41,6 +41,17 @@ class ExceptionRule(models.Model): required=True, ) model = fields.Selection(selection=[], string='Apply on', required=True) + + exception_type = fields.Selection( + selection=[('by_domain', 'By domain'), + ('by_py_code', 'By python code')], + string='Exception Type', 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") + domain = fields.Char('Domain') + active = fields.Boolean('Active') next_state = fields.Char( 'Next state', @@ -76,6 +87,12 @@ class ExceptionRule(models.Model): select_vals )) + @api.multi + def _get_domain(self): + """ override me to customize domains according exceptions cases """ + self.ensure_one() + return safe_eval(self.domain) + class BaseException(models.AbstractModel): _name = 'base.exception' @@ -142,29 +159,83 @@ class BaseException(models.AbstractModel): return False return True + @api.multi + def _reverse_field(self): + """Name of the many2many field from exception rule to self. + + In order to take advantage of domain optimisation, exception rule + model should have a many2many field to inherited object. + The opposit relation already exists in the name of exception_ids + + Example: + class ExceptionRule(models.Model): + _inherit = 'exception.rule' + + model = fields.Selection( + selection_add=[ + ('sale.order', 'Sale order'), + [...] + ]) + sale_ids = fields.Many2many( + 'sale.order', + string='Sales') + [...] + """ + exception_obj = self.env['exception.rule'] + reverse_fields = self.env['ir.model.fields'].search([ + ['model', '=', 'exception.rule'], + ['ttype', '=', 'many2many'], + ['relation', '=', self[0]._name], + ]) + # ir.model.fields may contain old variable name + # so we check if the field exists on exception rule + return ([ + field.name for field in reverse_fields + if hasattr(exception_obj, field.name) + ] or [None])[0] + + def _rule_domain(self): + """Filter exception.rules. + + By default, only the rules with the correct rule group + will be used. + """ + # TODO fix self[0] : it may not be the same on all ids in self + return [('rule_group', '=', self[0].rule_group)] + @api.multi def detect_exceptions(self): - """returns the list of exception_ids for all the considered base.exceptions + """List all exception_ids applied on self + Exception ids are also written on records """ if not self: return [] exception_obj = self.env['exception.rule'] all_exceptions = exception_obj.sudo().search( - [('rule_group', '=', self[0].rule_group)]) + self._rule_domain()) model_exceptions = all_exceptions.filtered( lambda ex: ex.model == self._name) sub_exceptions = all_exceptions.filtered( lambda ex: ex.model != self._name) + reverse_field = self._reverse_field() + if reverse_field: + optimize = True + else: + optimize = False + + exception_by_rec, exception_by_rule = self._detect_exceptions( + model_exceptions, sub_exceptions, optimize) + all_exception_ids = [] - for obj in self: - if obj.ignore_exception: - continue - exception_ids = obj._detect_exceptions( - model_exceptions, sub_exceptions) + for obj, exception_ids in exception_by_rec.iteritems(): obj.exception_ids = [(6, 0, exception_ids)] all_exception_ids += exception_ids - return all_exception_ids + for rule, exception_ids in exception_by_rule.iteritems(): + rule[reverse_field] = [(6, 0, exception_ids.ids)] + if exception_ids: + all_exception_ids += [rule.id] + return list(set(all_exception_ids)) @api.model def _exception_rule_eval_context(self, obj_name, rec): @@ -193,32 +264,96 @@ class BaseException(models.AbstractModel): return eval_ctx.get('failed', False) @api.multi - def _detect_exceptions(self, model_exceptions, sub_exceptions): - self.ensure_one() - exception_ids = [] - next_state_rule = False + def _detect_exceptions( + self, model_exceptions, sub_exceptions, + optimize=False, + ): + """Find exceptions found on self. + + @returns + exception_by_rec: {record_id: exception_ids} + exception_by_rule: {rule_id: record_ids} + """ + exception_by_rec = {} + exception_by_rule = {} + exception_set = set() + python_rules = [] + dom_rules = [] + optim_rules = [] + for rule in model_exceptions: - if self._rule_eval(rule, self.rule_group, self): - exception_ids.append(rule.id) - if rule.next_state: - if not next_state_rule or \ - rule.sequence < next_state_rule.sequence: - next_state_rule = rule - if sub_exceptions: - for obj_line in self._get_lines(): - for rule in sub_exceptions: - if rule.id in exception_ids: - # we do not matter if the exception as already been - # found for an line of this object - # (ex sale order line if obj is sale order) - continue - group_line = self.rule_group + '_line' - if self._rule_eval(rule, group_line, obj_line): - exception_ids.append(rule.id) + if rule.exception_type == 'by_py_code': + python_rules.append(rule) + elif rule.exception_type == 'by_domain' and rule.domain: + if optimize: + optim_rules.append(rule) + else: + dom_rules.append(rule) + + for rule in optim_rules: + domain = rule._get_domain() + domain.append(['ignore_exception', '=', False]) + domain.append(['id', 'in', self.ids]) + records_with_exception = self.search(domain) + exception_by_rule[rule] = records_with_exception + if records_with_exception: + exception_set.add(rule.id) + + if len(python_rules) or len(dom_rules) or sub_exceptions: + for rec in self: + for rule in python_rules: + if ( + not rec.ignore_exception and + self._rule_eval(rule, rec.rule_group, rec) + ): + exception_by_rec.setdefault(rec, []).append(rule.id) + exception_set.add(rule.id) + for rule in dom_rules: + # there is no reverse many2many, so this rule + # can't be optimized, see _reverse_field + domain = rule._get_domain() + domain.append(['ignore_exception', '=', False]) + domain.append(['id', '=', rec.id]) + if self.search_count(domain): + exception_by_rec.setdefault( + rec, []).append(rule.id) + exception_set.add(rule.id) + if sub_exceptions: + group_line = rec.rule_group + '_line' + for obj_line in rec._get_lines(): + for rule in sub_exceptions: + if rule.id in exception_set: + # we do not matter if the exception as + # already been + # found for an line of this object + # (ex sale order line if obj is sale order) + continue + if rule.exception_type == 'by_py_code': + if self._rule_eval( + rule, group_line, obj_line + ): + exception_by_rec.setdefault( + rec, []).append(rule.id) + elif ( + rule.exception_type == 'by_domain' and + rule.domain + ): + # sub_exception are currently not optimizable + domain = rule._get_domain() + domain.append(('id', '=', obj_line.id)) + if obj_line.search_count(domain): + exception_by_rec.setdefault( + rec, []).append(rule.id) + # set object to next state - if next_state_rule: - self.state = next_state_rule.next_state - return exception_ids + # find exception that raised error and has next_state + next_state_exception_ids = model_exceptions.filtered( + lambda r: r.id in exception_set and r.next_state) + + if next_state_exception_ids: + self.state = next_state_exception_ids[0].next_state + + return exception_by_rec, exception_by_rule @implemented_by_base_exception def _get_lines(self): diff --git a/base_exception/readme/CONTRIBUTORS.rst b/base_exception/readme/CONTRIBUTORS.rst index f1fe41bf8..6a3fc650e 100644 --- a/base_exception/readme/CONTRIBUTORS.rst +++ b/base_exception/readme/CONTRIBUTORS.rst @@ -5,4 +5,5 @@ * Yannick Vaucher * SodexisTeam * Mourad EL HADJ MIMOUNE +* Raphaël Reverdy * Iván Todorovich diff --git a/base_exception/readme/ROADMAP.rst b/base_exception/readme/ROADMAP.rst index 846343f9c..1f4739717 100644 --- a/base_exception/readme/ROADMAP.rst +++ b/base_exception/readme/ROADMAP.rst @@ -1 +1,3 @@ Terms used in old api like `pool`, `cr`, `uid` must be removed porting this module in version 12. + +This module execute user provided code though a safe_eval, it's unsecure? How mitigate risks should be adressed in future versions of this module. diff --git a/base_exception/views/base_exception_view.xml b/base_exception/views/base_exception_view.xml index 9526db774..3c6a7b0db 100644 --- a/base_exception/views/base_exception_view.xml +++ b/base_exception/views/base_exception_view.xml @@ -40,13 +40,15 @@ + + - + - +

Help with Python expressions