From fe923aab8def77ccb8d446bd4646173be2cb7d62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20BEAU?= Date: Fri, 15 May 2020 10:54:27 +0200 Subject: [PATCH 1/5] [12.0][IMP] add by_method exception --- base_exception/models/base_exception.py | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/base_exception/models/base_exception.py b/base_exception/models/base_exception.py index fee5f5674..41f6357e8 100644 --- a/base_exception/models/base_exception.py +++ b/base_exception/models/base_exception.py @@ -26,14 +26,16 @@ class ExceptionRule(models.Model): exception_type = fields.Selection( selection=[('by_domain', 'By domain'), - ('by_py_code', 'By python code')], + ('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') - + method = fields.Char('Method', readonly=True) active = fields.Boolean('Active', default=True) code = fields.Text( 'Python Code', @@ -45,11 +47,13 @@ class ExceptionRule(models.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)): + (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.") ) + # TODO in case of by_method exception test that the method exist with hasattr @api.multi def _get_domain(self): @@ -163,6 +167,8 @@ class BaseExceptionMethod(models.AbstractModel): 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) @api.multi def _get_base_domain(self): @@ -191,6 +197,15 @@ 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' From 6498d43a33561d5d32995224384f41d720ac854c Mon Sep 17 00:00:00 2001 From: KevinKhao Date: Sat, 16 May 2020 07:24:40 +0200 Subject: [PATCH 2/5] [IMP] black, flake8 nitpick --- base_exception/__manifest__.py | 32 ++-- base_exception/models/__init__.py | 1 - base_exception/models/base_exception.py | 171 ++++++++++-------- base_exception/tests/__init__.py | 1 - base_exception/tests/common.py | 5 +- base_exception/tests/purchase_test.py | 48 ++--- base_exception/tests/test_base_exception.py | 87 +++++---- base_exception/wizard/__init__.py | 1 - .../wizard/base_exception_confirm.py | 25 ++- 9 files changed, 196 insertions(+), 175 deletions(-) diff --git a/base_exception/__manifest__.py b/base_exception/__manifest__.py index eaef9d7f9..6becd9fb6 100644 --- a/base_exception/__manifest__.py +++ b/base_exception/__manifest__.py @@ -3,24 +3,22 @@ # Mourad EL HADJ MIMOUNE # 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"], + "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, } diff --git a/base_exception/models/__init__.py b/base_exception/models/__init__.py index ae49ca9d5..495e1fe24 100644 --- a/base_exception/models/__init__.py +++ b/base_exception/models/__init__.py @@ -1,2 +1 @@ - from . import base_exception diff --git a/base_exception/models/base_exception.py b/base_exception/models/base_exception.py index 41f6357e8..1e29af359 100644 --- a/base_exception/models/base_exception.py +++ b/base_exception/models/base_exception.py @@ -12,48 +12,56 @@ 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'), - ('by_method', 'By method'), - ], - 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') - method = fields.Char('Method', readonly=True) - 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.Char("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") 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)): + 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.") + _( + "There is a problem of configuration, python code, " + "domain or method is missing to match the exception " + "type." + ) ) - # TODO in case of by_method exception test that the method exist with hasattr @api.multi def _get_domain(self): @@ -63,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): @@ -85,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 = {} @@ -127,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 @@ -151,28 +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': + 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): @@ -208,34 +218,31 @@ class BaseExceptionMethod(models.AbstractModel): 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: @@ -243,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 = '
    %s
' % ''.join([ - '
  • %s: %s
  • ' % tuple(map(html.escape, ( - e.name, e.description))) for e in rec.exception_ids]) + rec.exceptions_summary = "
      %s
    " % "".join( + [ + "
  • %s: %s
  • " + % 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): @@ -281,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"))) diff --git a/base_exception/tests/__init__.py b/base_exception/tests/__init__.py index 874f27874..9a972aeab 100644 --- a/base_exception/tests/__init__.py +++ b/base_exception/tests/__init__.py @@ -1,4 +1,3 @@ - from . import common from . import purchase_test from . import test_base_exception diff --git a/base_exception/tests/common.py b/base_exception/tests/common.py index a66b65238..5721e6353 100644 --- a/base_exception/tests/common.py +++ b/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), ) diff --git a/base_exception/tests/purchase_test.py b/base_exception/tests/purchase_test.py index 5c2c7ad11..3b5d806ed 100644 --- a/base_exception/tests/purchase_test.py +++ b/base_exception/tests/purchase_test.py @@ -4,58 +4,63 @@ from odoo import api, fields, models 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" class LineTest(models.Model): @@ -63,7 +68,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() diff --git a/base_exception/tests/test_base_exception.py b/base_exception/tests/test_base_exception.py index a20e2a881..0a73f0250 100644 --- a/base_exception/tests/test_base_exception.py +++ b/base_exception/tests/test_base_exception.py @@ -14,57 +14,64 @@ _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]) - 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.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.exception_rule._fields["model"].selection.append( + ("base.exception.test.purchase.line", "Purchase Order Line") + ) - 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", - }) + 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", + } + ) - 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", - }) + 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", + } + ) - 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" - }) + 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_purchase_order_exception(self): - partner = self.env.ref('base.res_partner_1') + 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, - })], - }) + 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}) + ], + } + ) # Block because of exception during validation with self.assertRaises(ValidationError): potest1.button_confirm() @@ -73,4 +80,4 @@ class TestBaseException(common.SavepointCase): # Test ignore exeception make possible for the po to validate potest1.ignore_exception = True potest1.button_confirm() - self.assertTrue(potest1.state == 'purchase') + self.assertTrue(potest1.state == "purchase") diff --git a/base_exception/wizard/__init__.py b/base_exception/wizard/__init__.py index 6987f7c00..a31f679e5 100644 --- a/base_exception/wizard/__init__.py +++ b/base_exception/wizard/__init__.py @@ -1,2 +1 @@ - from . import base_exception_confirm diff --git a/base_exception/wizard/base_exception_confirm.py b/base_exception/wizard/base_exception_confirm.py index ecdd63b49..b9bb533c9 100644 --- a/base_exception/wizard/base_exception_confirm.py +++ b/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"} From 694fc82710189a1fed9e8bce5deaea79f2108471 Mon Sep 17 00:00:00 2001 From: KevinKhao Date: Sat, 16 May 2020 07:29:06 +0200 Subject: [PATCH 3/5] [IMP] Added tests, fixed logic on rule validity testing --- base_exception/models/base_exception.py | 14 +++- base_exception/tests/purchase_test.py | 7 ++ base_exception/tests/test_base_exception.py | 78 ++++++++++++++------- 3 files changed, 73 insertions(+), 26 deletions(-) diff --git a/base_exception/models/base_exception.py b/base_exception/models/base_exception.py index 1e29af359..45c153276 100644 --- a/base_exception/models/base_exception.py +++ b/base_exception/models/base_exception.py @@ -47,13 +47,16 @@ class ExceptionRule(models.Model): "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) - or (rule.exception_type == "by_method" and not rule.method) + or ( + rule.exception_type == "by_method" + and not self._check_method_valid(rule.method, rule.model) + ) ): raise ValidationError( _( @@ -63,6 +66,13 @@ class ExceptionRule(models.Model): ) ) + def _check_method_valid(self, method_name, model_name): + model = self.env[model_name] + method = getattr(model, method_name) + if method and callable(method): + return True + return False + @api.multi def _get_domain(self): """ override me to customize domains according exceptions cases """ diff --git a/base_exception/tests/purchase_test.py b/base_exception/tests/purchase_test.py index 3b5d806ed..c7c4dfe4d 100644 --- a/base_exception/tests/purchase_test.py +++ b/base_exception/tests/purchase_test.py @@ -62,6 +62,13 @@ class PurchaseTest(models.Model): 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: + if not rec.partner_id.zip: + records_fail += rec + return records_fail + class LineTest(models.Model): _name = "base.exception.test.purchase.line" diff --git a/base_exception/tests/test_base_exception.py b/base_exception/tests/test_base_exception.py index 0a73f0250..e67f05e16 100644 --- a/base_exception/tests/test_base_exception.py +++ b/base_exception/tests/test_base_exception.py @@ -33,51 +33,81 @@ class TestBaseException(common.SavepointCase): ("base.exception.test.purchase.line", "Purchase Order Line") ) - cls.exceptionnozip = cls.env["exception.rule"].create( + cls.partner = cls.env.ref("base.res_partner_1") + cls.partner.zip = False + cls.potest1 = cls.env["base.exception.test.purchase"].create( { - "name": "No ZIP code on destination", - "sequence": 10, - "model": "base.exception.test.purchase", - "code": "if not obj.partner_id.zip: failed=True", + "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.exceptionno_minorder = cls.env["exception.rule"].create( + def test_purchase_order_valid(self): + self.potest1.button_confirm() + self.assertFalse(self.potest1.exception_ids) + + def test_purchase_order_exception_invalid_amt_total(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_lineqty = cls.env["exception.rule"].create( + def test_purchase_order_exception_invalid_partner_zip(self): + self.exception_partner_no_zip = self.env["exception.rule"].create( { - "name": "Qty > 0", + "name": "No ZIP code on destination", "sequence": 10, - "model": "base.exception.test.purchase.line", - "code": "if obj.qty <= 0: failed=True", + "model": "base.exception.test.purchase", + "code": "if not obj.partner_id.zip: failed=True", + "exception_type": "by_py_code", } ) + 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( + def test_purchase_order_exception_name(self): + self.exception_no_name = self.env["exception.rule"].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}) - ], + "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_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.ignore_exception = True + self.potest1.button_confirm() + self.assertTrue(self.potest1.state == "purchase") From 3044d1db2e905efbe3a43cb8d8e73fd203d2f437 Mon Sep 17 00:00:00 2001 From: KevinKhao Date: Mon, 18 May 2020 11:50:40 +0200 Subject: [PATCH 4/5] [IMP] Code coverage --- base_exception/__manifest__.py | 2 +- base_exception/tests/test_base_exception.py | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/base_exception/__manifest__.py b/base_exception/__manifest__.py index 6becd9fb6..e8c448b4b 100644 --- a/base_exception/__manifest__.py +++ b/base_exception/__manifest__.py @@ -10,7 +10,7 @@ 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)", + "Odoo Community Association (OCA)", "website": "https://github.com/OCA/server-tools", "depends": ["base_setup"], "license": "AGPL-3", diff --git a/base_exception/tests/test_base_exception.py b/base_exception/tests/test_base_exception.py index e67f05e16..55c215d29 100644 --- a/base_exception/tests/test_base_exception.py +++ b/base_exception/tests/test_base_exception.py @@ -45,11 +45,11 @@ class TestBaseException(common.SavepointCase): } ) - def test_purchase_order_valid(self): + def test_valid(self): self.potest1.button_confirm() self.assertFalse(self.potest1.exception_ids) - def test_purchase_order_exception_invalid_amt_total(self): + def test_fail_by_py(self): self.exception_amount_total = self.env["exception.rule"].create( { "name": "Min order except", @@ -63,21 +63,21 @@ class TestBaseException(common.SavepointCase): self.potest1.button_confirm() self.assertTrue(self.potest1.exception_ids) - def test_purchase_order_exception_invalid_partner_zip(self): + 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", - "code": "if not obj.partner_id.zip: failed=True", - "exception_type": "by_py_code", + "domain": "[('partner_id.zip', '=', False)]", + "exception_type": "by_domain", } ) with self.assertRaises(ValidationError): self.potest1.button_confirm() self.assertTrue(self.potest1.exception_ids) - def test_purchase_order_exception_name(self): + def test_fail_by_method(self): self.exception_no_name = self.env["exception.rule"].create( { "name": "No name", @@ -108,6 +108,6 @@ class TestBaseException(common.SavepointCase): # Test that we have linked exceptions self.assertTrue(self.potest1.exception_ids) # Test ignore exeception make possible for the po to validate - self.potest1.ignore_exception = True + self.potest1.action_ignore_exceptions() self.potest1.button_confirm() self.assertTrue(self.potest1.state == "purchase") From 0f0576424c58bb377c522add9029b462f330ed52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20BEAU?= Date: Tue, 11 Aug 2020 10:14:27 +0200 Subject: [PATCH 5/5] [IMP] use selection field to be safer, add maintainers --- base_exception/__manifest__.py | 1 + base_exception/models/base_exception.py | 14 ++------------ base_exception/tests/purchase_test.py | 9 +++++++++ base_exception/tests/test_base_exception.py | 4 ++-- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/base_exception/__manifest__.py b/base_exception/__manifest__.py index e8c448b4b..0833fbf05 100644 --- a/base_exception/__manifest__.py +++ b/base_exception/__manifest__.py @@ -13,6 +13,7 @@ "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", diff --git a/base_exception/models/base_exception.py b/base_exception/models/base_exception.py index 45c153276..acf8f667d 100644 --- a/base_exception/models/base_exception.py +++ b/base_exception/models/base_exception.py @@ -39,7 +39,7 @@ class ExceptionRule(models.Model): " are evaluated with several records", ) domain = fields.Char("Domain") - method = fields.Char("Method", readonly=True) + method = fields.Selection(selection=[], string="Method", readonly=True) active = fields.Boolean("Active", default=True) code = fields.Text( "Python Code", @@ -53,10 +53,7 @@ class ExceptionRule(models.Model): 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 self._check_method_valid(rule.method, rule.model) - ) + or (rule.exception_type == "by_method" and not rule.method) ): raise ValidationError( _( @@ -66,13 +63,6 @@ class ExceptionRule(models.Model): ) ) - def _check_method_valid(self, method_name, model_name): - model = self.env[model_name] - method = getattr(model, method_name) - if method and callable(method): - return True - return False - @api.multi def _get_domain(self): """ override me to customize domains according exceptions cases """ diff --git a/base_exception/tests/purchase_test.py b/base_exception/tests/purchase_test.py index c7c4dfe4d..ace5342ce 100644 --- a/base_exception/tests/purchase_test.py +++ b/base_exception/tests/purchase_test.py @@ -3,6 +3,15 @@ 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" _name = "base.exception.test.purchase" diff --git a/base_exception/tests/test_base_exception.py b/base_exception/tests/test_base_exception.py index 55c215d29..d56df7ed6 100644 --- a/base_exception/tests/test_base_exception.py +++ b/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__) @@ -17,7 +17,7 @@ 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"]