diff --git a/base_exception/README.rst b/base_exception/README.rst index 9a34bdfed..6c09516d3 100644 --- a/base_exception/README.rst +++ b/base_exception/README.rst @@ -1,19 +1,24 @@ .. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg - :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :target: https://www.gnu.org/licenses/agpl-3.0-standalone.html :alt: License: AGPL-3 ============== Base Exception ============== -This module provide an abstract model to manage customizable exceptions to be applied on different models (sale order, invoice, ...). It is not usefull for itself. You can see an example of implementation in the 'sale_exception' module. (sale-workflow repository). +This module provide an abstract model to manage customizable +exceptions to be applied on different models (sale order, invoice, ...). + +It is not useful for itself. You can see an example of implementation +in the 'sale_exception' module. (sale-workflow repository) or +'purchase_exception' module (purchase-workflow repository). Usage ===== .. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas :alt: Try me on Runbot - :target: https://runbot.odoo-community.org/runbot/149/10.0 + :target: https://runbot.odoo-community.org/runbot/149/11.0 Bug Tracker @@ -22,11 +27,7 @@ Bug Tracker Bugs are tracked on `GitHub Issues `_. In case of trouble, please check there if your issue has already been reported. If you spotted it first, -help us smashing it by providing a detailed and welcomed `feedback -`_. +help us smash it by providing detailed and welcomed feedback. Images ------ diff --git a/base_exception/__init__.py b/base_exception/__init__.py index 3c4dc4909..7f16dfdc2 100644 --- a/base_exception/__init__.py +++ b/base_exception/__init__.py @@ -1,5 +1,5 @@ -# -*- 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). +# Copyright 2011 Raphaël Valyi, Renato Lima, Guewen Baconnier, Sodexis +# License AGPL-3.0 or later (https://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..35e2c0be2 100644 --- a/base_exception/__manifest__.py +++ b/base_exception/__manifest__.py @@ -1,21 +1,25 @@ -# -*- 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). +# Copyright 2011 Raphaël Valyi, Renato Lima, Guewen Baconnier, Sodexis +# Copyright 2017 Akretion (http://www.akretion.com) +# Mourad EL HADJ MIMOUNE +# License AGPL-3.0 or later (https://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/__init__.py b/base_exception/models/__init__.py index 5f94bb885..ae49ca9d5 100644 --- a/base_exception/models/__init__.py +++ b/base_exception/models/__init__.py @@ -1,5 +1,2 @@ -# -*- 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 . import base_exception diff --git a/base_exception/models/base_exception.py b/base_exception/models/base_exception.py index c7922cbfe..0eebb72cf 100644 --- a/base_exception/models/base_exception.py +++ b/base_exception/models/base_exception.py @@ -1,6 +1,7 @@ -# -*- 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). +# Copyright 2011 Raphaël Valyi, Renato Lima, Guewen Baconnier, Sodexis +# Copyright 2017 Akretion (http://www.akretion.com) +# Mourad EL HADJ MIMOUNE +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). import time from functools import wraps @@ -42,6 +43,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 +70,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 +209,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 +220,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 +241,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..e817e312c --- /dev/null +++ b/base_exception/tests/__init__.py @@ -0,0 +1,3 @@ + +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..e8f57d751 --- /dev/null +++ b/base_exception/tests/test_base_exception.py @@ -0,0 +1,83 @@ +# Copyright 2016 Akretion Mourad EL HADJ MIMOUNE +# License AGPL-3.0 or later (https://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..a07367878 --- /dev/null +++ b/base_exception/tests/test_tmp_model.py @@ -0,0 +1,71 @@ +# Copyright 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/__init__.py b/base_exception/wizard/__init__.py index 613ae2e30..6987f7c00 100644 --- a/base_exception/wizard/__init__.py +++ b/base_exception/wizard/__init__.py @@ -1,5 +1,2 @@ -# -*- 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 . import base_exception_confirm diff --git a/base_exception/wizard/base_exception_confirm.py b/base_exception/wizard/base_exception_confirm.py index d9a7b6844..9cb207f62 100644 --- a/base_exception/wizard/base_exception_confirm.py +++ b/base_exception/wizard/base_exception_confirm.py @@ -1,6 +1,7 @@ -# -*- 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). +# Copyright 2011 Raphaël Valyi, Renato Lima, Guewen Baconnier, Sodexis +# Copyright 2017 Akretion (http://www.akretion.com) +# Mourad EL HADJ MIMOUNE +# License AGPL-3.0 or later (https://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..e100c7ac6 100644 --- a/base_exception/wizard/base_exception_confirm_view.xml +++ b/base_exception/wizard/base_exception_confirm_view.xml @@ -1,39 +1,37 @@ - + + Exceptions Rules + exception.rule.confirm + +
+ + + + + + + + + + + + +
+
+
+
+
- - Exceptions Rules - exception.rule.confirm - -
- - - - - - - - - - -
-
-
-
+ + Outstanding exceptions to manage + 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 - - -