[WIP] sale_exception_by_domain
parent
fac14db84f
commit
f6b72003ff
|
@ -41,6 +41,17 @@ class ExceptionRule(models.Model):
|
||||||
required=True,
|
required=True,
|
||||||
)
|
)
|
||||||
model = fields.Selection(selection=[], string='Apply on', 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')
|
active = fields.Boolean('Active')
|
||||||
next_state = fields.Char(
|
next_state = fields.Char(
|
||||||
'Next state',
|
'Next state',
|
||||||
|
@ -76,6 +87,12 @@ class ExceptionRule(models.Model):
|
||||||
select_vals
|
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):
|
class BaseException(models.AbstractModel):
|
||||||
_name = 'base.exception'
|
_name = 'base.exception'
|
||||||
|
@ -142,29 +159,83 @@ class BaseException(models.AbstractModel):
|
||||||
return False
|
return False
|
||||||
return True
|
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
|
@api.multi
|
||||||
def detect_exceptions(self):
|
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:
|
if not self:
|
||||||
return []
|
return []
|
||||||
exception_obj = self.env['exception.rule']
|
exception_obj = self.env['exception.rule']
|
||||||
all_exceptions = exception_obj.sudo().search(
|
all_exceptions = exception_obj.sudo().search(
|
||||||
[('rule_group', '=', self[0].rule_group)])
|
self._rule_domain())
|
||||||
model_exceptions = all_exceptions.filtered(
|
model_exceptions = all_exceptions.filtered(
|
||||||
lambda ex: ex.model == self._name)
|
lambda ex: ex.model == self._name)
|
||||||
sub_exceptions = all_exceptions.filtered(
|
sub_exceptions = all_exceptions.filtered(
|
||||||
lambda ex: ex.model != self._name)
|
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 = []
|
all_exception_ids = []
|
||||||
for obj in self:
|
for obj, exception_ids in exception_by_rec.iteritems():
|
||||||
if obj.ignore_exception:
|
|
||||||
continue
|
|
||||||
exception_ids = obj._detect_exceptions(
|
|
||||||
model_exceptions, sub_exceptions)
|
|
||||||
obj.exception_ids = [(6, 0, exception_ids)]
|
obj.exception_ids = [(6, 0, exception_ids)]
|
||||||
all_exception_ids += 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
|
@api.model
|
||||||
def _exception_rule_eval_context(self, obj_name, rec):
|
def _exception_rule_eval_context(self, obj_name, rec):
|
||||||
|
@ -193,32 +264,96 @@ class BaseException(models.AbstractModel):
|
||||||
return eval_ctx.get('failed', False)
|
return eval_ctx.get('failed', False)
|
||||||
|
|
||||||
@api.multi
|
@api.multi
|
||||||
def _detect_exceptions(self, model_exceptions, sub_exceptions):
|
def _detect_exceptions(
|
||||||
self.ensure_one()
|
self, model_exceptions, sub_exceptions,
|
||||||
exception_ids = []
|
optimize=False,
|
||||||
next_state_rule = 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:
|
for rule in model_exceptions:
|
||||||
if self._rule_eval(rule, self.rule_group, self):
|
if rule.exception_type == 'by_py_code':
|
||||||
exception_ids.append(rule.id)
|
python_rules.append(rule)
|
||||||
if rule.next_state:
|
elif rule.exception_type == 'by_domain' and rule.domain:
|
||||||
if not next_state_rule or \
|
if optimize:
|
||||||
rule.sequence < next_state_rule.sequence:
|
optim_rules.append(rule)
|
||||||
next_state_rule = rule
|
else:
|
||||||
if sub_exceptions:
|
dom_rules.append(rule)
|
||||||
for obj_line in self._get_lines():
|
|
||||||
for rule in sub_exceptions:
|
for rule in optim_rules:
|
||||||
if rule.id in exception_ids:
|
domain = rule._get_domain()
|
||||||
# we do not matter if the exception as already been
|
domain.append(['ignore_exception', '=', False])
|
||||||
# found for an line of this object
|
domain.append(['id', 'in', self.ids])
|
||||||
# (ex sale order line if obj is sale order)
|
records_with_exception = self.search(domain)
|
||||||
continue
|
exception_by_rule[rule] = records_with_exception
|
||||||
group_line = self.rule_group + '_line'
|
if records_with_exception:
|
||||||
if self._rule_eval(rule, group_line, obj_line):
|
exception_set.add(rule.id)
|
||||||
exception_ids.append(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
|
# set object to next state
|
||||||
if next_state_rule:
|
# find exception that raised error and has next_state
|
||||||
self.state = next_state_rule.next_state
|
next_state_exception_ids = model_exceptions.filtered(
|
||||||
return exception_ids
|
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
|
@implemented_by_base_exception
|
||||||
def _get_lines(self):
|
def _get_lines(self):
|
||||||
|
|
|
@ -5,4 +5,5 @@
|
||||||
* Yannick Vaucher <yannick.vaucher@camptocamp.com>
|
* Yannick Vaucher <yannick.vaucher@camptocamp.com>
|
||||||
* SodexisTeam <dev@sodexis.com>
|
* SodexisTeam <dev@sodexis.com>
|
||||||
* Mourad EL HADJ MIMOUNE <mourad.elhadj.mimoune@akretion.com>
|
* Mourad EL HADJ MIMOUNE <mourad.elhadj.mimoune@akretion.com>
|
||||||
|
* Raphaël Reverdy <raphael.reverdy@akretion.com>
|
||||||
* Iván Todorovich <ivan.todorovich@gmail.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.
|
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="rule_group"/>
|
||||||
<field name="model"/>
|
<field name="model"/>
|
||||||
<field name="next_state"/>
|
<field name="next_state"/>
|
||||||
|
<field name="exception_type" widget="radio"/>
|
||||||
|
<field name="domain" attrs="{'invisible': [('exception_type','!=','by_domain')]}"/>
|
||||||
</group>
|
</group>
|
||||||
</group>
|
</group>
|
||||||
<notebook>
|
<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."/>
|
<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>
|
||||||
<page name="help" string="Help">
|
<page name="help" string="Help" attrs="{'invisible': [('exception_type','!=','by_py_code')]}">
|
||||||
<group>
|
<group>
|
||||||
<div style="margin-top: 4px;">
|
<div style="margin-top: 4px;">
|
||||||
<h3>Help with Python expressions</h3>
|
<h3>Help with Python expressions</h3>
|
||||||
|
|
Loading…
Reference in New Issue