diff --git a/base_exception/__init__.py b/base_exception/__init__.py index 3c4dc4909..6aa85e87f 100644 --- a/base_exception/__init__.py +++ b/base_exception/__init__.py @@ -3,3 +3,4 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). from . import wizard, models +from .tests import test_tmp_model diff --git a/base_exception/__manifest__.py b/base_exception/__manifest__.py index 6a42b0b1b..2bece70df 100644 --- a/base_exception/__manifest__.py +++ b/base_exception/__manifest__.py @@ -1,21 +1,26 @@ # -*- coding: utf-8 -*- # © 2011 Raphaël Valyi, Renato Lima, Guewen Baconnier, Sodexis +# © 2017 Akretion (http://www.akretion.com) +# Mourad EL HADJ MIMOUNE # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -{'name': 'Exception Rule', - 'version': '10.0.1.0.0', - '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': 'http://www.akretion.com', - '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', - ], - 'installable': True, - } +{ + 'name': 'Exception Rule', + 'version': '11.0.1.0.0', + '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': 'http://www.akretion.com', + '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', + ], + 'installable': True, +} diff --git a/base_exception/models/base_exception.py b/base_exception/models/base_exception.py index c7922cbfe..d3179d943 100644 --- a/base_exception/models/base_exception.py +++ b/base_exception/models/base_exception.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- # © 2011 Raphaël Valyi, Renato Lima, Guewen Baconnier, Sodexis +# © 2017 Akretion (http://www.akretion.com) +# Mourad EL HADJ MIMOUNE # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). import time @@ -42,6 +44,12 @@ class ExceptionRule(models.Model): selection=[], string='Apply on', required=True) active = fields.Boolean('Active') + next_state = fields.Char( + 'Next state', + help="If we detect exception we set de state of object (ex purchase) " + "to the next_state (ex 'to approve'). If there are more than one " + "exception detected and all have a value for next_state, we use" + "the exception having the smallest sequence value") code = fields.Text( 'Python Code', help="Python code executed to check if the exception apply or " @@ -63,6 +71,26 @@ class ExceptionRule(models.Model): # - context: current context """) + @api.constrains('next_state') + def _check_next_state_value(self): + """ Ensure that the next_state value is in the state values of + destination model """ + for rule in self: + if rule.next_state: + select_vals = self.env[ + rule.model].fields_get()[ + 'state']['selection'] + if rule.next_state\ + not in [s[0] for s in select_vals]: + raise ValidationError( + _('The value "%s" you chose for the "next state" ' + 'field state of "%s" is wrong.' + ' Value must be in this list %s') + % (rule.next_state, + rule.model, + select_vals) + ) + class BaseException(models.AbstractModel): _name = 'base.exception' @@ -182,7 +210,7 @@ class BaseException(models.AbstractModel): space, mode='exec', nocopy=True) # nocopy allows to return 'result' - except Exception, e: + except Exception as e: raise UserError( _('Error when evaluating the exception.rule ' 'rule:\n %s \n(%s)') % (rule.name, e)) @@ -193,9 +221,16 @@ class BaseException(models.AbstractModel): sub_exceptions): self.ensure_one() exception_ids = [] + next_state_rule = False 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: + next_state_rule = rule + elif next_state_rule and\ + rule.sequence < next_state_rule.sequence: + next_state_rule = rule if sub_exceptions: for obj_line in self._get_lines(): for rule in sub_exceptions: @@ -207,6 +242,9 @@ class BaseException(models.AbstractModel): group_line = self.rule_group + '_line' if self._rule_eval(rule, group_line, obj_line): exception_ids.append(rule.id) + # set object to next state + if next_state_rule: + self.state = next_state_rule.next_state return exception_ids @implemented_by_base_exception diff --git a/base_exception/security/ir.model.access.csv b/base_exception/security/ir.model.access.csv index ee49a23e8..ad0cb33f9 100644 --- a/base_exception/security/ir.model.access.csv +++ b/base_exception/security/ir.model.access.csv @@ -1,5 +1,7 @@ -"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink" -"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 +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +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 +access_base_exception_test_purchase,access_base_exception_test_purchase,model_base_exception_test_purchase,base.group_system,1,1,1,1 +access_base_exception_test_model_line,access_base_exception_test_model_line,model_base_exception_test_model_line,base.group_system,1,1,1,1 \ No newline at end of file diff --git a/base_exception/tests/__init__.py b/base_exception/tests/__init__.py new file mode 100644 index 000000000..82b394b93 --- /dev/null +++ b/base_exception/tests/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- + +from . import test_tmp_model +from . import test_base_exception diff --git a/base_exception/tests/test_base_exception.py b/base_exception/tests/test_base_exception.py new file mode 100644 index 000000000..d99f435a2 --- /dev/null +++ b/base_exception/tests/test_base_exception.py @@ -0,0 +1,84 @@ +# -*- coding: utf-8 -*- +# © 2016 Akretion Mourad EL HADJ MIMOUNE +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo.tests import common + +import logging + +_logger = logging.getLogger(__name__) + + +# @common.at_install(False) +# @common.post_install(True) +class TestBaseException(common.TransactionCase): + + def setUp(self): + super(TestBaseException, self).setUp() + + self.base_exception = self.env['base.exception'] + self.exception_rule = self.env['exception.rule'] + self.exception_confirm = self.env['exception.rule.confirm'] + + self.exception_rule._fields['rule_group'].selection.append( + ('test_base', 'test base exception') + ) + self.exception_rule._fields['model'].selection.append( + ('base.exception.test.purchase', + 'base.exception.test.purchase') + ) + self.exception_rule._fields['model'].selection.append( + ('base.exception.test.purchase.line', + 'base.exception.test.purchase.line') + ) + self.exceptionnozip = self.env['exception.rule'].create({ + 'name': "No ZIP code on destination", + 'sequence': 10, + 'rule_group': "test_base", + 'model': "base.exception.test.purchase", + 'code': """if not test_base.partner_id.zip: + failed=True""", + }) + self.exceptionno_minorder = self.env['exception.rule'].create({ + 'name': "Min order except", + 'sequence': 10, + 'rule_group': "test_base", + 'model': "base.exception.test.purchase", + 'code': """if test_base.amount_total <= 200.0: + failed=True""", + }) + + self.exceptionno_lineqty = self.env['exception.rule'].create({ + 'name': "Qty > 0", + 'sequence': 10, + 'rule_group': "test_base", + 'model': "base.exception.test.purchase.line", + 'code': """if test_base_line.qty <= 0: + failed=True"""}) + + def test_sale_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})], + }) + + potest1.button_confirm() + # Set ignore_exception flag (Done after ignore is selected at wizard) + potest1.ignore_exception = True + potest1.button_confirm() + self.assertTrue(potest1.state == 'purchase') + # Simulation the opening of the wizard exception_confirm and + # set ignore_exception to True + except_confirm = self.exception_confirm.with_context( + { + 'active_id': potest1.id, + 'active_ids': [potest1.id], + 'active_model': potest1._name + }).new({'ignore': True}) + except_confirm.action_confirm() + self.assertTrue(potest1.ignore_exception) diff --git a/base_exception/tests/test_tmp_model.py b/base_exception/tests/test_tmp_model.py new file mode 100644 index 000000000..40c1b7c38 --- /dev/null +++ b/base_exception/tests/test_tmp_model.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- +# © 2017 Akretion (http://www.akretion.com) +# Mourad EL HADJ MIMOUNE + +from odoo import fields, models, api + + +class PurchaseTest(models.Model): + _inherit = 'base.exception' + _name = "base.exception.test.purchase" + _description = "Base Ecxeption Test Model" + + rule_group = fields.Selection( + selection_add=[('test_base', 'test')], + default='test_base', + ) + name = fields.Char(required=True) + 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') + active = fields.Boolean(default=True) + partner_id = fields.Many2one('res.partner', string='Partner') + line_ids = fields.One2many('base.exception.test.model.line', 'lead_id') + amount_total = fields.Float(compute='_compute_amount_total', store=True) + + @api.depends('line_ids') + def _compute_amount_total(self): + for record in self: + for line in record.line_ids: + record.amount_total += line.amount * line.qty + + @api.constrains('ignore_exception', 'line_ids', 'state') + def test_purchase_check_exception(self): + orders = self.filtered(lambda s: s.state == 'purchase') + if orders: + orders._check_exception() + + @api.multi + def button_approve(self, force=False): + self.write({'state': 'to approve'}) + return {} + + @api.multi + def button_draft(self): + self.write({'state': 'draft'}) + return {} + + @api.multi + def button_confirm(self): + self.write({'state': 'purchase'}) + return True + + @api.multi + def button_cancel(self): + self.write({'state': 'cancel'}) + + def test_base_get_lines(self): + self.ensure_one() + return self.line_ids + + +class LineTest(models.Model): + _name = "base.exception.test.model.line" + _description = "Base Ecxeption Test Model Line" + + name = fields.Char() + lead_id = fields.Many2one('base.exception.test.model', ondelete='cascade') + qty = fields.Float() + amount = fields.Float() diff --git a/base_exception/views/base_exception_view.xml b/base_exception/views/base_exception_view.xml index a44e376e5..d4c411644 100644 --- a/base_exception/views/base_exception_view.xml +++ b/base_exception/views/base_exception_view.xml @@ -1,51 +1,50 @@ + + exception.rule.tree + exception.rule + + + + + + + + + + - - exception.rule.tree - exception.rule - - - + + exception.rule.form + exception.rule + +
+ - + + + - - - - - - exception.rule.form - exception.rule - - - - - - - - - - - - - - - - - - - - - Exception Rules - exception.rule - form - tree,form - - {'active_test': False} - +
+ + + + + + + +
+
- + + Exception Rules + exception.rule + form + tree,form + + {'active_test': False} + +
diff --git a/base_exception/wizard/base_exception_confirm.py b/base_exception/wizard/base_exception_confirm.py index d9a7b6844..2a8365a4c 100644 --- a/base_exception/wizard/base_exception_confirm.py +++ b/base_exception/wizard/base_exception_confirm.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- # © 2011 Raphaël Valyi, Renato Lima, Guewen Baconnier, Sodexis +# © 2017 Akretion (http://www.akretion.com) +# Mourad EL HADJ MIMOUNE # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). from odoo import api, fields, models diff --git a/base_exception/wizard/base_exception_confirm_view.xml b/base_exception/wizard/base_exception_confirm_view.xml index daffb29d5..76dfffa81 100644 --- a/base_exception/wizard/base_exception_confirm_view.xml +++ b/base_exception/wizard/base_exception_confirm_view.xml @@ -1,39 +1,35 @@ - + + Exceptions Rules + exception.rule.confirm + +
+ + + + + + + + + + +
+
+
+
+
- - Exceptions Rules - exception.rule.confirm - -
- - - - - - - - - - -
-
-
-
+ + Blocked in draft due to exceptions + ir.actions.act_window + exception.rule.confirm + form + form + + new - - - Blocked in draft due to exceptions - ir.actions.act_window - exception.rule.confirm - form - form - - new - - -