Browse Source

Merge PR #1831 into 12.0

Signed-off-by sebastienbeau
12.0-mig-module_prototyper_last
OCA-git-bot 4 years ago
parent
commit
13771aaa72
  1. 33
      base_exception/__manifest__.py
  2. 1
      base_exception/models/__init__.py
  3. 176
      base_exception/models/base_exception.py
  4. 1
      base_exception/tests/__init__.py
  5. 5
      base_exception/tests/common.py
  6. 64
      base_exception/tests/purchase_test.py
  7. 133
      base_exception/tests/test_base_exception.py
  8. 1
      base_exception/wizard/__init__.py
  9. 25
      base_exception/wizard/base_exception_confirm.py

33
base_exception/__manifest__.py

@ -3,24 +3,23 @@
# Mourad EL HADJ MIMOUNE <mourad.elhadj.mimoune@akretion.com>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
{
'name': 'Exception Rule',
'version': '12.0.3.0.1',
'category': 'Generic Modules',
'summary': """
"name": "Exception Rule",
"version": "12.0.3.0.1",
"category": "Generic Modules",
"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)",
'website': 'https://github.com/OCA/server-tools',
'depends': [
'base_setup',
"author": "Akretion, Sodexis, Camptocamp, "
"Odoo Community Association (OCA)",
"website": "https://github.com/OCA/server-tools",
"depends": ["base_setup"],
"maintainers": ["hparfr", "sebastienbeau"],
"license": "AGPL-3",
"data": [
"security/base_exception_security.xml",
"security/ir.model.access.csv",
"wizard/base_exception_confirm_view.xml",
"views/base_exception_view.xml",
],
'license': 'AGPL-3',
'data': [
'security/base_exception_security.xml',
'security/ir.model.access.csv',
'wizard/base_exception_confirm_view.xml',
'views/base_exception_view.xml',
],
'installable': True,
"installable": True,
}

1
base_exception/models/__init__.py

@ -1,2 +1 @@
from . import base_exception

176
base_exception/models/base_exception.py

@ -12,43 +12,55 @@ from odoo import osv
class ExceptionRule(models.Model):
_name = 'exception.rule'
_description = 'Exception Rule'
_order = 'active desc, sequence asc'
_name = "exception.rule"
_description = "Exception Rule"
_order = "active desc, sequence asc"
name = fields.Char('Exception Name', required=True, translate=True)
description = fields.Text('Description', translate=True)
name = fields.Char("Exception Name", required=True, translate=True)
description = fields.Text("Description", translate=True)
sequence = fields.Integer(
string='Sequence',
string="Sequence",
help="Gives the sequence order when applying the test",
)
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',
selection=[
("by_domain", "By domain"),
("by_py_code", "By python code"),
("by_method", "By method"),
],
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', default=True)
"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")
method = fields.Selection(selection=[], string="Method", readonly=True)
active = fields.Boolean("Active", default=True)
code = fields.Text(
'Python Code',
"Python Code",
help="Python code executed to check if the exception apply or "
"not. Use failed = True to block the exception",
)
"not. Use failed = True to block the exception",
)
@api.constrains('exception_type', 'domain', 'code')
@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)):
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 or "
"domain is missing to match the exception type.")
_(
"There is a problem of configuration, python code, "
"domain or method is missing to match the exception "
"type."
)
)
@api.multi
@ -59,8 +71,8 @@ class ExceptionRule(models.Model):
class BaseExceptionMethod(models.AbstractModel):
_name = 'base.exception.method'
_description = 'Exception Rule Methods'
_name = "base.exception.method"
_description = "Exception Rule Methods"
@api.multi
def _get_main_records(self):
@ -81,15 +93,14 @@ class BaseExceptionMethod(models.AbstractModel):
By default, only the rules with the correct model
will be used.
"""
return [('model', '=', self._name)]
return [("model", "=", self._name)]
@api.multi
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())
rules = self.env["exception.rule"].sudo().search(self._rule_domain())
all_exception_ids = []
rules_to_remove = {}
rules_to_add = {}
@ -123,23 +134,23 @@ class BaseExceptionMethod(models.AbstractModel):
# 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,)]})
records.write({"exception_ids": [(3, rule_id)]})
for rule_id, records in rules_to_add.items():
records.write(({'exception_ids': [(4, rule_id,)]}))
records.write(({"exception_ids": [(4, rule_id)]}))
return all_exception_ids
@api.model
def _exception_rule_eval_context(self, rec):
return {
'time': time,
'self': rec,
"time": time,
"self": rec,
# object, obj: deprecated.
# should be removed in future migrations
'object': rec,
'obj': rec,
"object": rec,
"obj": rec,
# copy context to prevent side-effects of eval
# should be deprecated too, accesible through self.
'context': self.env.context.copy()
"context": self.env.context.copy(),
}
@api.model
@ -147,26 +158,31 @@ class BaseExceptionMethod(models.AbstractModel):
expr = rule.code
space = self._exception_rule_eval_context(rec)
try:
safe_eval(expr,
space,
mode='exec',
nocopy=True) # nocopy allows to return 'result'
safe_eval(
expr, space, mode="exec", nocopy=True
) # nocopy allows to return 'result'
except Exception as e:
raise UserError(
_('Error when evaluating the exception.rule '
'rule:\n %s \n(%s)') % (rule.name, e))
return space.get('failed', False)
_(
"Error when evaluating the exception.rule "
"rule:\n %s \n(%s)"
)
% (rule.name, e)
)
return space.get("failed", False)
@api.multi
def _detect_exceptions(self, rule):
if rule.exception_type == 'by_py_code':
if rule.exception_type == "by_py_code":
return self._detect_exceptions_by_py_code(rule)
elif rule.exception_type == 'by_domain':
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)
@api.multi
def _get_base_domain(self):
return [('ignore_exception', '=', False), ('id', 'in', self.ids)]
return [("ignore_exception", "=", False), ("id", "in", self.ids)]
@api.multi
def _detect_exceptions_by_py_code(self, rule):
@ -191,36 +207,42 @@ class BaseExceptionMethod(models.AbstractModel):
domain = osv.expression.AND([base_domain, rule_domain])
return self.search(domain)
@api.multi
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 BaseException(models.AbstractModel):
_inherit = 'base.exception.method'
_name = 'base.exception'
_order = 'main_exception_id asc'
_description = 'Exception'
_inherit = "base.exception.method"
_name = "base.exception"
_order = "main_exception_id asc"
_description = "Exception"
main_exception_id = fields.Many2one(
'exception.rule',
compute='_compute_main_error',
string='Main Exception',
"exception.rule",
compute="_compute_main_error",
string="Main Exception",
store=True,
)
exceptions_summary = fields.Html(
'Exceptions Summary',
compute='_compute_exceptions_summary',
"Exceptions Summary", compute="_compute_exceptions_summary"
)
exception_ids = fields.Many2many(
'exception.rule',
string='Exceptions',
copy=False,
"exception.rule", string="Exceptions", copy=False
)
ignore_exception = fields.Boolean('Ignore Exceptions', copy=False)
ignore_exception = fields.Boolean("Ignore Exceptions", copy=False)
@api.multi
def action_ignore_exceptions(self):
self.write({'ignore_exception': True})
self.write({"ignore_exception": True})
return True
@api.depends('exception_ids', 'ignore_exception')
@api.depends("exception_ids", "ignore_exception")
def _compute_main_error(self):
for rec in self:
if not rec.ignore_exception and rec.exception_ids:
@ -228,29 +250,35 @@ class BaseException(models.AbstractModel):
else:
rec.main_exception_id = False
@api.depends('exception_ids', 'ignore_exception')
@api.depends("exception_ids", "ignore_exception")
def _compute_exceptions_summary(self):
for rec in self:
if rec.exception_ids and not rec.ignore_exception:
rec.exceptions_summary = '<ul>%s</ul>' % ''.join([
'<li>%s: <i>%s</i></li>' % tuple(map(html.escape, (
e.name, e.description))) for e in rec.exception_ids])
rec.exceptions_summary = "<ul>%s</ul>" % "".join(
[
"<li>%s: <i>%s</i></li>"
% tuple(map(html.escape, (e.name, e.description)))
for e in rec.exception_ids
]
)
@api.multi
def _popup_exceptions(self):
action = self._get_popup_action().read()[0]
action.update({
'context': {
'active_id': self.ids[0],
'active_ids': self.ids,
'active_model': self._name,
action.update(
{
"context": {
"active_id": self.ids[0],
"active_ids": self.ids,
"active_model": self._name,
}
}
})
)
return action
@api.model
def _get_popup_action(self):
return self.env.ref('base_exception.action_exception_rule_confirm')
return self.env.ref("base_exception.action_exception_rule_confirm")
@api.multi
def _check_exception(self):
@ -266,5 +294,5 @@ class BaseException(models.AbstractModel):
"""
exception_ids = self.detect_exceptions()
if exception_ids:
exceptions = self.env['exception.rule'].browse(exception_ids)
raise ValidationError('\n'.join(exceptions.mapped('name')))
exceptions = self.env["exception.rule"].browse(exception_ids)
raise ValidationError("\n".join(exceptions.mapped("name")))

1
base_exception/tests/__init__.py

@ -1,4 +1,3 @@
from . import common
from . import purchase_test
from . import test_base_exception

5
base_exception/tests/common.py

@ -8,6 +8,7 @@ def setup_test_model(env, model_clses):
env.registry.setup_models(env.cr)
env.registry.init_models(
env.cr, [model_cls._name for model_cls in model_clses],
dict(env.context, update_custom_fields=True)
env.cr,
[model_cls._name for model_cls in model_clses],
dict(env.context, update_custom_fields=True),
)

64
base_exception/tests/purchase_test.py

@ -3,59 +3,80 @@
from odoo import api, fields, models
class ExceptionRule(models.Model):
_inherit = "exception.rule"
_name = "exception.rule"
method = fields.Selection(
selection_add=[('exception_method_no_zip', 'Purchase exception no zip')]
)
class PurchaseTest(models.Model):
_inherit = 'base.exception'
_inherit = "base.exception"
_name = "base.exception.test.purchase"
_description = "Base Ecxeption Test Model"
name = fields.Char(required=True)
user_id = fields.Many2one('res.users', string='Responsible')
user_id = fields.Many2one("res.users", string="Responsible")
state = fields.Selection(
[('draft', 'New'), ('cancel', 'Cancelled'),
('purchase', 'Purchase'),
('to approve', 'To approve'), ('done', 'Done')],
string="Status", readonly=True, default='draft')
[
("draft", "New"),
("cancel", "Cancelled"),
("purchase", "Purchase"),
("to approve", "To approve"),
("done", "Done"),
],
string="Status",
readonly=True,
default="draft",
)
active = fields.Boolean(default=True)
partner_id = fields.Many2one('res.partner', string='Partner')
line_ids = fields.One2many(
'base.exception.test.purchase.line', 'lead_id')
amount_total = fields.Float(
compute='_compute_amount_total', store=True)
partner_id = fields.Many2one("res.partner", string="Partner")
line_ids = fields.One2many("base.exception.test.purchase.line", "lead_id")
amount_total = fields.Float(compute="_compute_amount_total", store=True)
@api.depends('line_ids')
@api.depends("line_ids")
def _compute_amount_total(cls):
for record in cls:
for line in record.line_ids:
record.amount_total += line.amount * line.qty
@api.constrains('ignore_exception', 'line_ids', 'state')
@api.constrains("ignore_exception", "line_ids", "state")
def test_purchase_check_exception(cls):
orders = cls.filtered(lambda s: s.state == 'purchase')
orders = cls.filtered(lambda s: s.state == "purchase")
if orders:
orders._check_exception()
@api.multi
def button_approve(cls, force=False):
cls.write({'state': 'to approve'})
cls.write({"state": "to approve"})
return {}
@api.multi
def button_draft(cls):
cls.write({'state': 'draft'})
cls.write({"state": "draft"})
return {}
@api.multi
def button_confirm(cls):
cls.write({'state': 'purchase'})
cls.write({"state": "purchase"})
return True
@api.multi
def button_cancel(cls):
cls.write({'state': 'cancel'})
cls.write({"state": "cancel"})
@api.multi
def _reverse_field(self):
return 'test_purchase_ids'
return "test_purchase_ids"
def exception_method_no_zip(self):
records_fail = self.env["base.exception.test.purchase"]
for rec in self:
if not rec.partner_id.zip:
records_fail += rec
return records_fail
class LineTest(models.Model):
@ -63,7 +84,8 @@ class LineTest(models.Model):
_description = "Base Exception Test Model Line"
name = fields.Char()
lead_id = fields.Many2one('base.exception.test.purchase',
ondelete='cascade')
lead_id = fields.Many2one(
"base.exception.test.purchase", ondelete="cascade"
)
qty = fields.Float()
amount = fields.Float()

133
base_exception/tests/test_base_exception.py

@ -5,7 +5,7 @@ from odoo.tests import common
from odoo.exceptions import ValidationError
from odoo import fields
from .common import setup_test_model
from .purchase_test import PurchaseTest, LineTest
from .purchase_test import PurchaseTest, LineTest, ExceptionRule
import logging
_logger = logging.getLogger(__name__)
@ -14,63 +14,100 @@ _logger = logging.getLogger(__name__)
@common.at_install(False)
@common.post_install(True)
class TestBaseException(common.SavepointCase):
@classmethod
def setUpClass(cls):
super(TestBaseException, cls).setUpClass()
setup_test_model(cls.env, [PurchaseTest, LineTest])
setup_test_model(cls.env, [PurchaseTest, LineTest, ExceptionRule])
cls.base_exception = cls.env["base.exception"]
cls.exception_rule = cls.env["exception.rule"]
if "test_purchase_ids" not in cls.exception_rule._fields:
field = fields.Many2many("base.exception.test.purchase")
cls.exception_rule._add_field("test_purchase_ids", field)
cls.exception_confirm = cls.env["exception.rule.confirm"]
cls.exception_rule._fields["model"].selection.append(
("base.exception.test.purchase", "Purchase Order")
)
cls.exception_rule._fields["model"].selection.append(
("base.exception.test.purchase.line", "Purchase Order Line")
)
cls.base_exception = cls.env['base.exception']
cls.exception_rule = cls.env['exception.rule']
if 'test_purchase_ids' not in cls.exception_rule._fields:
field = fields.Many2many('base.exception.test.purchase')
cls.exception_rule._add_field('test_purchase_ids', field)
cls.exception_confirm = cls.env['exception.rule.confirm']
cls.exception_rule._fields['model'].selection.append(
('base.exception.test.purchase', 'Purchase Order'))
cls.partner = cls.env.ref("base.res_partner_1")
cls.partner.zip = False
cls.potest1 = cls.env["base.exception.test.purchase"].create(
{
"name": "Test base exception to basic purchase",
"partner_id": cls.partner.id,
"line_ids": [
(0, 0, {"name": "line test", "amount": 120.0, "qty": 1.5})
],
}
)
cls.exception_rule._fields['model'].selection.append(
('base.exception.test.purchase.line', 'Purchase Order Line'))
def test_valid(self):
self.potest1.button_confirm()
self.assertFalse(self.potest1.exception_ids)
cls.exceptionnozip = cls.env['exception.rule'].create({
'name': "No ZIP code on destination",
'sequence': 10,
'model': "base.exception.test.purchase",
'code': "if not obj.partner_id.zip: failed=True",
})
def test_fail_by_py(self):
self.exception_amount_total = self.env["exception.rule"].create(
{
"name": "Min order except",
"sequence": 10,
"model": "base.exception.test.purchase",
"code": "if obj.amount_total <= 200.0: failed=True",
"exception_type": "by_py_code",
}
)
with self.assertRaises(ValidationError):
self.potest1.button_confirm()
self.assertTrue(self.potest1.exception_ids)
cls.exceptionno_minorder = cls.env['exception.rule'].create({
'name': "Min order except",
'sequence': 10,
'model': "base.exception.test.purchase",
'code': "if obj.amount_total <= 200.0: failed=True",
})
def test_fail_by_domain(self):
self.exception_partner_no_zip = self.env["exception.rule"].create(
{
"name": "No ZIP code on destination",
"sequence": 10,
"model": "base.exception.test.purchase",
"domain": "[('partner_id.zip', '=', False)]",
"exception_type": "by_domain",
}
)
with self.assertRaises(ValidationError):
self.potest1.button_confirm()
self.assertTrue(self.potest1.exception_ids)
cls.exceptionno_lineqty = cls.env['exception.rule'].create({
'name': "Qty > 0",
'sequence': 10,
'model': "base.exception.test.purchase.line",
'code': "if obj.qty <= 0: failed=True"
})
def test_fail_by_method(self):
self.exception_no_name = self.env["exception.rule"].create(
{
"name": "No name",
"sequence": 10,
"model": "base.exception.test.purchase",
"method": "exception_method_no_zip",
"exception_type": "by_method",
}
)
with self.assertRaises(ValidationError):
self.potest1.button_confirm()
self.assertTrue(self.potest1.exception_ids)
def test_purchase_order_exception(self):
partner = self.env.ref('base.res_partner_1')
partner.zip = False
potest1 = self.env['base.exception.test.purchase'].create({
'name': 'Test base exception to basic purchase',
'partner_id': partner.id,
'line_ids': [(0, 0, {
'name': "line test",
'amount': 120.0,
'qty': 1.5,
})],
})
def test_ignore_exception(self):
# same as 1st test
self.exception_amount_total = self.env["exception.rule"].create(
{
"name": "Min order except",
"sequence": 10,
"model": "base.exception.test.purchase",
"code": "if obj.amount_total <= 200.0: failed=True",
"exception_type": "by_py_code",
}
)
# Block because of exception during validation
with self.assertRaises(ValidationError):
potest1.button_confirm()
self.potest1.button_confirm()
# Test that we have linked exceptions
self.assertTrue(potest1.exception_ids)
self.assertTrue(self.potest1.exception_ids)
# Test ignore exeception make possible for the po to validate
potest1.ignore_exception = True
potest1.button_confirm()
self.assertTrue(potest1.state == 'purchase')
self.potest1.action_ignore_exceptions()
self.potest1.button_confirm()
self.assertTrue(self.potest1.state == "purchase")

1
base_exception/wizard/__init__.py

@ -1,2 +1 @@
from . import base_exception_confirm

25
base_exception/wizard/base_exception_confirm.py

@ -7,34 +7,33 @@ from odoo.exceptions import ValidationError
class ExceptionRuleConfirm(models.AbstractModel):
_name = 'exception.rule.confirm'
_description = 'Exception Rule Confirm Wizard'
_name = "exception.rule.confirm"
_description = "Exception Rule Confirm Wizard"
related_model_id = fields.Many2one('base.exception',)
related_model_id = fields.Many2one("base.exception")
exception_ids = fields.Many2many(
'exception.rule',
string='Exceptions to resolve',
readonly=True,
"exception.rule", string="Exceptions to resolve", readonly=True
)
ignore = fields.Boolean('Ignore Exceptions')
ignore = fields.Boolean("Ignore Exceptions")
@api.model
def default_get(self, field_list):
res = super(ExceptionRuleConfirm, self).default_get(field_list)
current_model = self.env.context.get('active_model')
current_model = self.env.context.get("active_model")
model_except_obj = self.env[current_model]
active_ids = self.env.context.get('active_ids')
active_ids = self.env.context.get("active_ids")
if len(active_ids) > 1:
raise ValidationError(
_('Only 1 ID accepted, got %r.') % active_ids)
_("Only 1 ID accepted, got %r.") % active_ids
)
active_id = active_ids[0]
related_model_except = model_except_obj.browse(active_id)
exception_ids = related_model_except.exception_ids.ids
res.update({'exception_ids': [(6, 0, exception_ids)]})
res.update({'related_model_id': active_id})
res.update({"exception_ids": [(6, 0, exception_ids)]})
res.update({"related_model_id": active_id})
return res
@api.multi
def action_confirm(self):
self.ensure_one()
return {'type': 'ir.actions.act_window_close'}
return {"type": "ir.actions.act_window_close"}
Loading…
Cancel
Save