diff --git a/base_exception/__manifest__.py b/base_exception/__manifest__.py index 2aa04f143..0ba5165b9 100644 --- a/base_exception/__manifest__.py +++ b/base_exception/__manifest__.py @@ -2,20 +2,18 @@ # © 2011 Raphaël Valyi, Renato Lima, Guewen Baconnier, Sodexis # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -{'name': 'Sale Exception', - 'summary': 'Custom exceptions on sale order', - 'version': '9.0.1.0.0', +{'name': 'Exception Rule', + 'version': '10.0.1.0.0', 'category': 'Generic Modules/Sale', 'author': "Akretion, Sodexis, Odoo Community Association (OCA)", 'website': 'http://www.akretion.com', 'depends': ['sale'], 'license': 'AGPL-3', 'data': [ + 'security/base_exception_security.xml', 'security/ir.model.access.csv', - 'wizard/sale_exception_confirm_view.xml', - 'data/sale_exception_data.xml', - 'views/sale_view.xml', + 'wizard/base_exception_confirm_view.xml', + 'views/base_exception_view.xml', ], - 'images': [], - 'installable': False, + 'installable': True, } diff --git a/base_exception/data/sale_exception_data.xml b/base_exception/data/sale_exception_data.xml deleted file mode 100644 index 6db29dcf8..000000000 --- a/base_exception/data/sale_exception_data.xml +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - Test Draft Orders - - - 20 - minutes - -1 - - - - - - - - No ZIP code on destination - No ZIP code on destination - 50 - sale.order - if not order.partner_shipping_id.zip: - failed=True - - - - - Not Enough Virtual Stock - Not Enough Virtual Stock - 50 - sale.order.line - if line.product_id and line.product_id.type == 'product' and line.product_id.virtual_available < line.product_uom_qty: - failed=True - - - - - diff --git a/base_exception/models/__init__.py b/base_exception/models/__init__.py index bc7f66f92..5f94bb885 100644 --- a/base_exception/models/__init__.py +++ b/base_exception/models/__init__.py @@ -2,4 +2,4 @@ # © 2011 Raphaël Valyi, Renato Lima, Guewen Baconnier, Sodexis # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -from . import sale +from . import base_exception diff --git a/base_exception/models/base_exception.py b/base_exception/models/base_exception.py new file mode 100644 index 000000000..1503e40d9 --- /dev/null +++ b/base_exception/models/base_exception.py @@ -0,0 +1,215 @@ +# -*- coding: utf-8 -*- +# © 2011 Raphaël Valyi, Renato Lima, Guewen Baconnier, Sodexis +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +import time +from functools import wraps + +from odoo import api, models, fields, _ +from odoo.exceptions import UserError, ValidationError +from odoo.tools.safe_eval import safe_eval + + +def implemented_by_base_exception(func): + """Call a prefixed function based on 'namespace'.""" + @wraps(func) + def wrapper(cls, *args, **kwargs): + fun_name = func.__name__ + fun = '_%s%s' % (cls.rule_group, fun_name) + if not hasattr(cls, fun): + fun = '_default%s' % (fun_name) + return getattr(cls, fun)(*args, **kwargs) + return wrapper + + +class ExceptionRule(models.Model): + _name = 'exception.rule' + _description = "Exception Rules" + _order = 'active desc, sequence asc' + + name = fields.Char('Exception Name', required=True, translate=True) + description = fields.Text('Description', translate=True) + sequence = fields.Integer( + string='Sequence', + help="Gives the sequence order when applying the test") + rule_group = fields.Selection( + [], + help="Rule group is used the group the rules that must validated " + "at same time for a target object. Ex: " + "validate sale.order.line rules with sale order rules.", + required=True) + model = fields.Selection( + [], + string='Apply on', required=True) + active = fields.Boolean('Active') + code = fields.Text( + 'Python Code', + help="Python code executed to check if the exception apply or " + "not. The code must apply block = True to apply the " + "exception.", + default=""" +# Python code. Use failed = True to block the base.exception. +# You can use the following variables : +# - self: ORM model of the record which is checked +# - "rule_group" or "rule_group_"line: +# browse_record of the base.exception or +# base.exception line (ex rule_group = sale for sale order) +# - object: same as order or line, browse_record of the base.exception or +# base.exception line +# - pool: ORM model pool (i.e. self.pool) +# - time: Python time module +# - cr: database cursor +# - uid: current user id +# - context: current context +""") + + +class BaseException(models.AbstractModel): + _name = 'base.exception' + + _order = 'main_exception_id asc' + + main_exception_id = fields.Many2one( + 'exception.rule', + compute='_compute_main_error', + string='Main Exception', + store=True) + rule_group = fields.Selection( + [], + readonly=True, + ) + exception_ids = fields.Many2many( + 'exception.rule', + string='Exceptions') + ignore_exception = fields.Boolean('Ignore Exceptions', copy=False) + + @api.depends('exception_ids', 'ignore_exception') + def _compute_main_error(self): + for obj in self: + if not obj.ignore_exception and obj.exception_ids: + obj.main_exception_id = obj.exception_ids[0] + else: + obj.main_exception_id = False + + @api.multi + def _popup_exceptions(self): + action = self._get_popup_action() + action = action.read()[0] + action.update({ + 'context': { + 'active_id': self.ids[0], + 'active_ids': self.ids + } + }) + return action + + @api.model + def _get_popup_action(self): + action = self.env.ref('base_exception.action_exception_rule_confirm') + return action + + @api.model + def _check_exception(self): + """ + This method must be used in a constraint that must be created in the + object that inherits for base.exception. + for sale : + @api.constrains('ignore_exception',) + def sale_check_exception(self): + ... + ... + self._check_exception + """ + exception_ids = self.detect_exceptions() + if exception_ids: + exceptions = self.env['exception.rule'].browse(exception_ids) + raise ValidationError('\n'.join(exceptions.mapped('name'))) + + @api.multi + def test_exceptions(self): + """ + Condition method for the workflow from draft to confirm + """ + if self.detect_exceptions(): + return False + return True + + @api.multi + def detect_exceptions(self): + """returns the list of exception_ids for all the considered base.exceptions + """ + exception_obj = self.env['exception.rule'] + model_exceptions = exception_obj.sudo().search( + [('model', '=', self._name)]) + sub_exceptions = exception_obj.sudo().search( + [('rule_group', '=', self.rule_group), + ('id', 'not in', model_exceptions.ids), + ]) + + all_exception_ids = [] + for obj in self: + if obj.ignore_exception: + continue + exception_ids = obj._detect_exceptions( + model_exceptions, sub_exceptions) + obj.exception_ids = [(6, 0, exception_ids)] + all_exception_ids += exception_ids + return all_exception_ids + + @api.model + def _exception_rule_eval_context(self, obj_name, rec): + user = self.env['res.users'].browse(self._uid) + return {obj_name: rec, + 'self': self.pool.get(rec._name), + 'object': rec, + 'obj': rec, + 'pool': self.pool, + 'cr': self._cr, + 'uid': self._uid, + 'user': user, + 'time': time, + # copy context to prevent side-effects of eval + 'context': self._context.copy()} + + @api.model + def _rule_eval(self, rule, obj_name, rec): + expr = rule.code + space = self._exception_rule_eval_context(obj_name, rec) + try: + safe_eval(expr, + space, + mode='exec', + nocopy=True) # nocopy allows to return 'result' + except Exception, e: + raise UserError( + _('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, model_exceptions, + sub_exceptions): + self.ensure_one() + exception_ids = [] + for rule in model_exceptions: + if self._rule_eval(rule, self.rule_group, self): + exception_ids.append(rule.id) + 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) + return exception_ids + + @implemented_by_base_exception + def _get_lines(self): + pass + + def _default_get_lines(self): + return [] diff --git a/base_exception/models/sale.py b/base_exception/models/sale.py deleted file mode 100644 index 669b84c27..000000000 --- a/base_exception/models/sale.py +++ /dev/null @@ -1,201 +0,0 @@ -# -*- coding: utf-8 -*- -# © 2011 Raphaël Valyi, Renato Lima, Guewen Baconnier, Sodexis -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). - -import time - -from openerp import api, models, fields, _ -from openerp.exceptions import UserError, ValidationError -from openerp.tools.safe_eval import safe_eval - - -class SaleException(models.Model): - _name = 'sale.exception' - _description = "Sale Exceptions" - _order = 'active desc, sequence asc' - - name = fields.Char('Exception Name', required=True, translate=True) - description = fields.Text('Description', translate=True) - sequence = fields.Integer( - string='Sequence', - help="Gives the sequence order when applying the test") - model = fields.Selection( - [('sale.order', 'Sale Order'), - ('sale.order.line', 'Sale Order Line')], - string='Apply on', required=True) - active = fields.Boolean('Active') - code = fields.Text( - 'Python Code', - help="Python code executed to check if the exception apply or " - "not. The code must apply block = True to apply the " - "exception.", - default=""" -# Python code. Use failed = True to block the sale order. -# You can use the following variables : -# - self: ORM model of the record which is checked -# - order or line: browse_record of the sale order or sale order line -# - object: same as order or line, browse_record of the sale order or -# sale order line -# - pool: ORM model pool (i.e. self.pool) -# - time: Python time module -# - cr: database cursor -# - uid: current user id -# - context: current context -""") - sale_order_ids = fields.Many2many( - 'sale.order', - 'sale_order_exception_rel', 'exception_id', 'sale_order_id', - string='Sale Orders', - readonly=True) - - -class SaleOrder(models.Model): - _inherit = 'sale.order' - - _order = 'main_exception_id asc, date_order desc, name desc' - - main_exception_id = fields.Many2one( - 'sale.exception', - compute='_get_main_error', - string='Main Exception', - store=True) - exception_ids = fields.Many2many( - 'sale.exception', - 'sale_order_exception_rel', 'sale_order_id', 'exception_id', - string='Exceptions') - ignore_exception = fields.Boolean('Ignore Exceptions', copy=False) - - @api.one - @api.depends('exception_ids', 'ignore_exception') - def _get_main_error(self): - if not self.ignore_exception and self.exception_ids: - self.main_exception_id = self.exception_ids[0] - else: - self.main_exception_id = False - - @api.model - def test_all_draft_orders(self): - order_set = self.search([('state', '=', 'draft')]) - order_set.test_exceptions() - return True - - @api.multi - def _popup_exceptions(self): - action = self.env.ref('sale_exception.action_sale_exception_confirm') - action = action.read()[0] - action.update({ - 'context': { - 'active_id': self.ids[0], - 'active_ids': self.ids - } - }) - return action - - @api.one - @api.constrains('ignore_exception', 'order_line', 'state') - def check_sale_exception_constrains(self): - if self.state == 'sale': - exception_ids = self.detect_exceptions() - if exception_ids: - exceptions = self.env['sale.exception'].browse(exception_ids) - raise ValidationError('\n'.join(exceptions.mapped('name'))) - - @api.onchange('order_line') - def onchange_ignore_exception(self): - if self.state == 'sale': - self.ignore_exception = False - - @api.multi - def action_confirm(self): - if self.detect_exceptions(): - return self._popup_exceptions() - else: - return super(SaleOrder, self).action_confirm() - - @api.multi - def action_cancel(self): - for order in self: - if order.ignore_exception: - order.ignore_exception = False - return super(SaleOrder, self).action_cancel() - - @api.multi - def test_exceptions(self): - """ - Condition method for the workflow from draft to confirm - """ - if self.detect_exceptions(): - return False - return True - - @api.multi - def detect_exceptions(self): - """returns the list of exception_ids for all the considered sale orders - - as a side effect, the sale order's exception_ids column is updated with - the list of exceptions related to the SO - """ - exception_obj = self.env['sale.exception'] - order_exceptions = exception_obj.search( - [('model', '=', 'sale.order')]) - line_exceptions = exception_obj.search( - [('model', '=', 'sale.order.line')]) - - all_exception_ids = [] - for order in self: - if order.ignore_exception: - continue - exception_ids = order._detect_exceptions(order_exceptions, - line_exceptions) - order.exception_ids = [(6, 0, exception_ids)] - all_exception_ids += exception_ids - return all_exception_ids - - @api.model - def _exception_rule_eval_context(self, obj_name, rec): - user = self.env['res.users'].browse(self._uid) - return {obj_name: rec, - 'self': self.pool.get(rec._name), - 'object': rec, - 'obj': rec, - 'pool': self.pool, - 'cr': self._cr, - 'uid': self._uid, - 'user': user, - 'time': time, - # copy context to prevent side-effects of eval - 'context': self._context.copy()} - - @api.model - def _rule_eval(self, rule, obj_name, rec): - expr = rule.code - space = self._exception_rule_eval_context(obj_name, rec) - try: - safe_eval(expr, - space, - mode='exec', - nocopy=True) # nocopy allows to return 'result' - except Exception, e: - raise UserError( - _('Error when evaluating the sale exception ' - 'rule:\n %s \n(%s)') % (rule.name, e)) - return space.get('failed', False) - - @api.multi - def _detect_exceptions(self, order_exceptions, - line_exceptions): - self.ensure_one() - exception_ids = [] - for rule in order_exceptions: - if self._rule_eval(rule, 'order', self): - exception_ids.append(rule.id) - - for order_line in self.order_line: - for rule in line_exceptions: - if rule.id in exception_ids: - # we do not matter if the exception as already been - # found for an order line of this order - continue - if self._rule_eval(rule, 'line', order_line): - exception_ids.append(rule.id) - return exception_ids diff --git a/base_exception/security/base_exception_security.xml b/base_exception/security/base_exception_security.xml new file mode 100644 index 000000000..d69d3669b --- /dev/null +++ b/base_exception/security/base_exception_security.xml @@ -0,0 +1,9 @@ + + + + + Exception manager + + + + diff --git a/base_exception/security/ir.model.access.csv b/base_exception/security/ir.model.access.csv index 116e2c7b7..ee49a23e8 100644 --- a/base_exception/security/ir.model.access.csv +++ b/base_exception/security/ir.model.access.csv @@ -1,3 +1,5 @@ "id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink" -"access_sale_exception","sale.exception","model_sale_exception","base.group_user",1,0,0,0 -"access_sale_exception_manager","sale.exception","model_sale_exception","base.group_sale_manager",1,1,1,1 +"access_exception_rule","base.exception","model_exception_rule","base.group_user",1,0,0,0 +"access_exception_rule_manager","base.exception","model_exception_rule","base_exception.group_exception_rule_manager",1,1,1,1 +"access_base_exception","base.exception","model_base_exception","base.group_user",1,0,0,0 +"access_base_exception_manager","base.exception","model_base_exception","base_exception.group_exception_rule_manager",1,1,1,1 diff --git a/base_exception/tests/__init__.py b/base_exception/tests/__init__.py deleted file mode 100644 index a343538ef..000000000 --- a/base_exception/tests/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -# -*- coding: utf-8 -*- -# (c) 2015 Oihane Crucelaegui - AvanzOSC -# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html - -from . import test_sale_exception diff --git a/base_exception/tests/test_sale_exception.py b/base_exception/tests/test_sale_exception.py deleted file mode 100644 index 5fbce18ce..000000000 --- a/base_exception/tests/test_sale_exception.py +++ /dev/null @@ -1,62 +0,0 @@ -from openerp.exceptions import ValidationError -from openerp.addons.sale.tests.test_sale_order import TestSaleOrder - - -class TestSaleException(TestSaleOrder): - - def test_sale_order_exception(self): - exception = self.env.ref('sale_exception.excep_no_zip') - exception.active = True - partner = self.env.ref('base.res_partner_1') - partner.zip = False - p = self.env.ref('product.product_product_6') - so = self.env['sale.order'].create({ - 'partner_id': partner.id, - 'partner_invoice_id': partner.id, - 'partner_shipping_id': partner.id, - 'order_line': [(0, 0, {'name': p.name, - 'product_id': p.id, - 'product_uom_qty': 2, - 'product_uom': p.uom_id.id, - 'price_unit': p.list_price})], - 'pricelist_id': self.env.ref('product.list0').id, - }) - - # confirm quotation - so.action_confirm() - self.assertTrue(so.state == 'draft') - - # Set ignore_exception flag (Done after ignore is selected at wizard) - so.ignore_exception = True - so.action_confirm() - self.assertTrue(so.state == 'sale') - - # Add a order line to test after SO is confirmed - p = self.env.ref('product.product_product_7') - - # set ignore_exception = False (Done by onchange of order_line) - self.assertRaises( - ValidationError, - so.write, - { - 'ignore_exception': False, - 'order_line': [(0, 0, {'name': p.name, - 'product_id': p.id, - 'product_uom_qty': 2, - 'product_uom': p.uom_id.id, - 'price_unit': p.list_price})] - }, - ) - - p = self.env.ref('product.product_product_7') - - # Set ignore exception True (Done manually by user) - so.write({ - 'ignore_exception': True, - 'order_line': [(0, 0, {'name': p.name, - 'product_id': p.id, - 'product_uom_qty': 2, - 'product_uom': p.uom_id.id, - 'price_unit': p.list_price})] - }) - exception.active = False diff --git a/base_exception/views/base_exception_view.xml b/base_exception/views/base_exception_view.xml new file mode 100644 index 000000000..4f30a0f05 --- /dev/null +++ b/base_exception/views/base_exception_view.xml @@ -0,0 +1,51 @@ + + + + + exception.rule.tree + exception.rule + + + + + + + + + + + + + exception.rule.form + exception.rule + +
+ + + + + + + + + + + + + +
+
+
+ + + Exception Rules + exception.rule + form + tree,form + + {'active_test': False} + + + + +
diff --git a/base_exception/views/sale_view.xml b/base_exception/views/sale_view.xml deleted file mode 100644 index 59504d128..000000000 --- a/base_exception/views/sale_view.xml +++ /dev/null @@ -1,116 +0,0 @@ - - - - - sale.exception.tree - sale.exception - - - - - - - - - - - - - sale.exception.form - sale.exception - -
- - - - - - - - - - - - - - - - - -
-
-
- - - Exception Rules - sale.exception - form - tree,form - - {'active_test': False} - - - - - - - sale_exception.view_order_form - sale.order - - - - - - - - - - - - - - - - - - - - - - sale_exception.view_order_tree - sale.order - - - - - - - - - - sale_exception.view_order_tree - sale.order - - - - - - - - - - sale_exception.view_sales_order_filter - sale.order - - - - - - - - - -
diff --git a/base_exception/wizard/__init__.py b/base_exception/wizard/__init__.py index d2f4f0b07..613ae2e30 100644 --- a/base_exception/wizard/__init__.py +++ b/base_exception/wizard/__init__.py @@ -2,4 +2,4 @@ # © 2011 Raphaël Valyi, Renato Lima, Guewen Baconnier, Sodexis # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -from . import sale_exception_confirm +from . import base_exception_confirm diff --git a/base_exception/wizard/base_exception_confirm.py b/base_exception/wizard/base_exception_confirm.py new file mode 100644 index 000000000..d9a7b6844 --- /dev/null +++ b/base_exception/wizard/base_exception_confirm.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# © 2011 Raphaël Valyi, Renato Lima, Guewen Baconnier, Sodexis +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import api, fields, models + + +class ExceptionRuleConfirm(models.AbstractModel): + + _name = 'exception.rule.confirm' + + related_model_id = fields.Many2one('base.exception',) + exception_ids = fields.Many2many('exception.rule', + string='Exceptions to resolve', + readonly=True) + ignore = fields.Boolean('Ignore Exceptions') + + @api.model + def default_get(self, field_list): + res = super(ExceptionRuleConfirm, self).default_get(field_list) + current_model = self._context.get('active_model') + model_except_obj = self.env[current_model] + active_ids = self._context.get('active_ids') + assert len(active_ids) == 1, "Only 1 ID accepted, got %r" % active_ids + active_id = active_ids[0] + related_model_except = model_except_obj.browse(active_id) + exception_ids = [e.id for e in related_model_except.exception_ids] + 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'} diff --git a/base_exception/wizard/sale_exception_confirm_view.xml b/base_exception/wizard/base_exception_confirm_view.xml similarity index 67% rename from base_exception/wizard/sale_exception_confirm_view.xml rename to base_exception/wizard/base_exception_confirm_view.xml index b82bbbb2d..daffb29d5 100644 --- a/base_exception/wizard/sale_exception_confirm_view.xml +++ b/base_exception/wizard/base_exception_confirm_view.xml @@ -1,21 +1,21 @@ - + - - Sale Exceptions - sale.exception.confirm + + Exceptions Rules + exception.rule.confirm
- + - +