[WIP] sale_exception_by_domain
parent
fac14db84f
commit
f6b72003ff
|
@ -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 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:
|
||||
for obj_line in self._get_lines():
|
||||
group_line = rec.rule_group + '_line'
|
||||
for obj_line in rec._get_lines():
|
||||
for rule in sub_exceptions:
|
||||
if rule.id in exception_ids:
|
||||
# we do not matter if the exception as already been
|
||||
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
|
||||
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':
|
||||
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):
|
||||
|
|
|
@ -5,4 +5,5 @@
|
|||
* Yannick Vaucher <yannick.vaucher@camptocamp.com>
|
||||
* SodexisTeam <dev@sodexis.com>
|
||||
* Mourad EL HADJ MIMOUNE <mourad.elhadj.mimoune@akretion.com>
|
||||
* Raphaël Reverdy <raphael.reverdy@akretion.com>
|
||||
* Iván Todorovich <ivan.todorovich@gmail.com>
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -40,13 +40,15 @@
|
|||
<field name="rule_group"/>
|
||||
<field name="model"/>
|
||||
<field name="next_state"/>
|
||||
<field name="exception_type" widget="radio"/>
|
||||
<field name="domain" attrs="{'invisible': [('exception_type','!=','by_domain')]}"/>
|
||||
</group>
|
||||
</group>
|
||||
<notebook>
|
||||
<page name="code" string="Python Code">
|
||||
<page name="code" string="Python Code" attrs="{'invisible': [('exception_type','!=','by_py_code')]}">
|
||||
<field name="code" widget="ace" options="{'mode': 'python'}" placeholder="Enter Python code here. Help about Python expression is available in the help tab of this document."/>
|
||||
</page>
|
||||
<page name="help" string="Help">
|
||||
<page name="help" string="Help" attrs="{'invisible': [('exception_type','!=','by_py_code')]}">
|
||||
<group>
|
||||
<div style="margin-top: 4px;">
|
||||
<h3>Help with Python expressions</h3>
|
||||
|
|
Loading…
Reference in New Issue