From f658c721004134ea46afb8d9372d49bad1e7b0af Mon Sep 17 00:00:00 2001 From: lreficent Date: Fri, 1 Dec 2017 16:56:35 -0500 Subject: [PATCH 01/10] [9.0][ADD] base_tier_validation --- base_tier_validation/README.rst | 73 ++++++++++ base_tier_validation/__init__.py | 4 + base_tier_validation/__openerp__.py | 22 +++ base_tier_validation/models/__init__.py | 6 + .../models/tier_definition.py | 54 +++++++ base_tier_validation/models/tier_review.py | 41 ++++++ .../models/tier_validation.py | 134 ++++++++++++++++++ .../security/ir.model.access.csv | 4 + .../views/tier_definition_view.xml | 61 ++++++++ .../views/tier_review_view.xml | 22 +++ 10 files changed, 421 insertions(+) create mode 100644 base_tier_validation/README.rst create mode 100644 base_tier_validation/__init__.py create mode 100644 base_tier_validation/__openerp__.py create mode 100644 base_tier_validation/models/__init__.py create mode 100644 base_tier_validation/models/tier_definition.py create mode 100644 base_tier_validation/models/tier_review.py create mode 100644 base_tier_validation/models/tier_validation.py create mode 100644 base_tier_validation/security/ir.model.access.csv create mode 100644 base_tier_validation/views/tier_definition_view.xml create mode 100644 base_tier_validation/views/tier_review_view.xml diff --git a/base_tier_validation/README.rst b/base_tier_validation/README.rst new file mode 100644 index 0000000..8e27d1d --- /dev/null +++ b/base_tier_validation/README.rst @@ -0,0 +1,73 @@ +.. image:: https://img.shields.io/badge/license-AGPL--3-blue.png + :target: https://www.gnu.org/licenses/agpl + :alt: License: AGPL-3 + +==================== +Base Tier Validation +==================== + +This module does not provide a functionality by itself but an abstract model +to implement a validation process based on tiers on other models (e.g. +purchase orders, sales orders...). + +**Note:** To be able to use this module in a new model you will need some +development. + +See `purchase_tier_validation `_ as an example of implementation. + +Configuration +============= + +To configure this module, you need to: + +#. Go to *Settings > Technical > Tier Validations > Tier Definition*. +#. Create as many tiers as you want for any model having tier validation + functionality. + +.. 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/9.0 + +Known issues / Roadmap +====================== + +* In odoo v11 it would be interesting to try to take advantage of ``mail.activity.mixin``. + +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 smash it by providing detailed and welcomed feedback. + +Credits +======= + +Images +------ + +* Odoo Community Association: `Icon `_. + +Contributors +------------ + +* Lois Rilo + +Do not contact contributors directly about support or help with technical issues. + +Maintainer +---------- + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +This module is maintained by the OCA. + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +To contribute to this module, please visit https://odoo-community.org. diff --git a/base_tier_validation/__init__.py b/base_tier_validation/__init__.py new file mode 100644 index 0000000..b44d765 --- /dev/null +++ b/base_tier_validation/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import models diff --git a/base_tier_validation/__openerp__.py b/base_tier_validation/__openerp__.py new file mode 100644 index 0000000..ce34496 --- /dev/null +++ b/base_tier_validation/__openerp__.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Eficent Business and IT Consulting Services S.L. +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +{ + "name": "Base Tier Validation", + "summary": "Implement a validation process based on tiers.", + "version": "9.0.1.0.0", + "category": "Tools", + "website": "https://github.com/OCA/server-tools", + "author": "Eficent, Odoo Community Association (OCA)", + "license": "AGPL-3", + "application": False, + "installable": True, + "depends": [ + "base", + ], + "data": [ + "security/ir.model.access.csv", + "views/tier_definition_view.xml", + "views/tier_review_view.xml", + ], +} diff --git a/base_tier_validation/models/__init__.py b/base_tier_validation/models/__init__.py new file mode 100644 index 0000000..d7c418a --- /dev/null +++ b/base_tier_validation/models/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import tier_definition +from . import tier_review +from . import tier_validation diff --git a/base_tier_validation/models/tier_definition.py b/base_tier_validation/models/tier_definition.py new file mode 100644 index 0000000..ffa8144 --- /dev/null +++ b/base_tier_validation/models/tier_definition.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Eficent Business and IT Consulting Services S.L. +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from openerp import api, fields, models + + +class TierDefinition(models.Model): + _name = "tier.definition" + _rec_name = "model_id" + + @api.model + def _get_tier_validation_model_names(self): + res = [] + return res + + model_id = fields.Many2one( + comodel_name="ir.model", + string="Referenced Model", + ) + model = fields.Char( + related='model_id.model', index=True, store=True, + ) + review_type = fields.Selection( + string="Validated by", default="individual", + selection=[("individual", "Specific user"), + ("group", "Any user in a specific group.")] + ) + reviewer_id = fields.Many2one( + comodel_name="res.users", string="Reviewer", + ) + reviewer_group_id = fields.Many2one( + comodel_name="res.groups", string="Reviewer group", + ) + python_code = fields.Text( + string='Tier Definition Expression', + help="Write Python code that defines when this tier confirmation " + "will be needed. The result of executing the expresion must be " + "a boolean.", + default="""# Available locals:\n# - rec: current record""", + ) + active = fields.Boolean(default=True) + sequence = fields.Integer(default=30) + company_id = fields.Many2one( + comodel_name="res.company", string="Company", + default=lambda self: self.env["res.company"]._company_default_get( + "tier.definition"), + ) + + @api.onchange('model_id') + def onchange_model_id(self): + return {'domain': { + 'model_id': [ + ('model', 'in', self._get_tier_validation_model_names())]}} diff --git a/base_tier_validation/models/tier_review.py b/base_tier_validation/models/tier_review.py new file mode 100644 index 0000000..e7afe7a --- /dev/null +++ b/base_tier_validation/models/tier_review.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Eficent Business and IT Consulting Services S.L. +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from openerp import api, fields, models + + +class TierReview(models.Model): + _name = "tier.review" + + status = fields.Selection( + selection=[("pending", "Pending"), + ("rejected", "Rejected"), + ("approved", "Approved")], + default="pending", + ) + model = fields.Char(string='Related Document Model', index=True) + res_id = fields.Integer(string='Related Document ID', index=True) + definition_id = fields.Many2one( + comodel_name="tier.definition", + ) + review_type = fields.Selection( + related="definition_id.review_type", readonly=True, + ) + reviewer_id = fields.Many2one( + related="definition_id.reviewer_id", readonly=True, + ) + reviewer_group_id = fields.Many2one( + related="definition_id.reviewer_group_id", readonly=True, + ) + reviewer_ids = fields.Many2many( + string="Reviewers", comodel_name="res.users", + compute="_compute_reviewer_ids", store=True, + ) + sequence = fields.Integer(string="Tier") + + @api.multi + @api.depends('reviewer_id', 'reviewer_group_id', 'reviewer_group_id.users') + def _compute_reviewer_ids(self): + for rec in self: + rec.reviewer_ids = rec.reviewer_id + rec.reviewer_group_id.users diff --git a/base_tier_validation/models/tier_validation.py b/base_tier_validation/models/tier_validation.py new file mode 100644 index 0000000..027ceff --- /dev/null +++ b/base_tier_validation/models/tier_validation.py @@ -0,0 +1,134 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Eficent Business and IT Consulting Services S.L. +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from openerp import api, fields, models, _ +from openerp.exceptions import ValidationError, UserError +from openerp.tools.safe_eval import safe_eval + + +class TierValidation(models.AbstractModel): + _name = "tier.validation" + + _state_field = 'state' + _state_from = ['draft'] + _state_to = ['confirmed'] + _cancel_state = 'cancel' + + # TODO: reset validation? + # TODO: step by step validation? + + review_ids = fields.One2many( + comodel_name='tier.review', inverse_name='res_id', + string='Validations', + domain=lambda self: [('model', '=', self._name)], + auto_join=True, + ) + validated = fields.Boolean(compute="_compute_validated_rejected") + need_validation = fields.Boolean(compute="_compute_need_validation") + rejected = fields.Boolean(compute="_compute_validated_rejected") + reviewer_ids = fields.Many2many( + string="Reviewers", comodel_name="res.users", + compute="_compute_reviewer_ids", + search="_search_reviewer_ids", + ) + + @api.multi + @api.depends('review_ids') + def _compute_reviewer_ids(self): + for rec in self: + rec.reviewer_ids = rec.review_ids.filtered( + lambda r: r.status == 'pending').mapped('reviewer_ids') + + @api.model + def _search_reviewer_ids(self, operator, value): + reviews = self.env['tier.review'].search([ + ('model', '=', self._name), ('reviewer_ids', operator, value)]) + return [('id', 'in', list(set(reviews.mapped('res_id'))))] + + @api.multi + def _compute_validated_rejected(self): + """Override for different validation/rejection policy.""" + for rec in self: + # sort by tier + rec.validated = not any( + [s != 'approved' for s in self.review_ids.mapped('status')]) + rec.rejected = any( + [s == 'rejected' for s in self.review_ids.mapped('status')]) + + @api.multi + def _compute_need_validation(self): + for rec in self: + rec.need_validation = not self.review_ids and self.env[ + 'tier.definition'].search([('model', '=', self._name)]) and \ + getattr(rec, self._state_field) in self._state_from + + @api.multi + def evaluate_tier(self, tier): + try: + res = safe_eval(tier.python_code, globals_dict={'rec': self}) + except Exception, error: + raise UserError(_( + "Error evaluating tier validation conditions.\n %s") % error) + return res + + @api.multi + def write(self, vals): + for rec in self: + if (getattr(rec, self._state_field) in self._state_from and + vals.get(self._state_field) in self._state_to): + if rec.need_validation: + raise ValidationError(_( + "This action needs to be validated for at least one " + "record. \nPlease request a validation.")) + if not rec.validated: + raise ValidationError(_( + "A validation process is still open for at least " + "one record.")) + if (rec.review_ids and getattr(rec, self._state_field) in + self._state_from and not vals.get(self._state_field) in + (self._state_to + [self._cancel_state])): + raise ValidationError(_("The operation is under validation.")) + if vals.get(self._state_field) in self._state_from: + self.mapped('review_ids').sudo().unlink() + return super(TierValidation, self).write(vals) + + @api.multi + def validate_tier(self): + for rec in self: + user_reviews = rec.review_ids.filtered( + lambda r: r.status in ('pending', 'rejected') and + (r.reviewer_id == self.env.user or + r.reviewer_group_id in self.env.user.groups_id)) + user_reviews.write({'status': 'approved'}) + + @api.multi + def reject_tier(self): + for rec in self: + user_reviews = rec.review_ids.filtered( + lambda r: r.status in ('pending', 'approved') and + (r.reviewer_id == self.env.user or + r.reviewer_group_id in self.env.user.groups_id)) + user_reviews.write({'status': 'rejected'}) + + @api.multi + def request_validation(self): + td_obj = self.env['tier.definition'] + tr_obj = self.env['tier.review'] + for rec in self: + if getattr(rec, self._state_field) in self._state_from: + if rec.need_validation: + tier_definitions = td_obj.search([ + ('model', '=', self._name)], order="sequence desc") + sequence = 0 + for td in tier_definitions: + if self.evaluate_tier(td): + sequence += 1 + tr_obj.create({ + 'model': self._name, + 'res_id': rec.id, + 'definition_id': td.id, + 'sequence': sequence, + }) + # TODO: notify? post some msg in chatter? + return True diff --git a/base_tier_validation/security/ir.model.access.csv b/base_tier_validation/security/ir.model.access.csv new file mode 100644 index 0000000..8491017 --- /dev/null +++ b/base_tier_validation/security/ir.model.access.csv @@ -0,0 +1,4 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_tier_review,access.tier.review,model_tier_review,,1,1,1,1 +access_tier_definition_all,tier.definition.all,model_tier_definition,,1,0,0,0 +access_tier_definition_settings,tier.definition.settings,model_tier_definition,base.group_system,1,1,1,1 diff --git a/base_tier_validation/views/tier_definition_view.xml b/base_tier_validation/views/tier_definition_view.xml new file mode 100644 index 0000000..5511b38 --- /dev/null +++ b/base_tier_validation/views/tier_definition_view.xml @@ -0,0 +1,61 @@ + + + + + + tier.definition.tree + tier.definition + + + + + + + + + + + + tier.definition.form + tier.definition + +
+ + + + + + + + + + + + + + + + +
+
+
+ + + Tier Definition + ir.actions.act_window + tier.definition + form + tree,form + + + + + +
diff --git a/base_tier_validation/views/tier_review_view.xml b/base_tier_validation/views/tier_review_view.xml new file mode 100644 index 0000000..df0509f --- /dev/null +++ b/base_tier_validation/views/tier_review_view.xml @@ -0,0 +1,22 @@ + + + + + + tier.review.tree + tier.review + + + + + + + + + + + + From 373c0ac0ca2655575163ffafe07b37e505f24a66 Mon Sep 17 00:00:00 2001 From: Lois Rilo Date: Wed, 28 Feb 2018 18:20:06 +0100 Subject: [PATCH 02/10] fix: blocking unneded records --- base_tier_validation/models/tier_validation.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/base_tier_validation/models/tier_validation.py b/base_tier_validation/models/tier_validation.py index 027ceff..baf5361 100644 --- a/base_tier_validation/models/tier_validation.py +++ b/base_tier_validation/models/tier_validation.py @@ -59,8 +59,10 @@ class TierValidation(models.AbstractModel): @api.multi def _compute_need_validation(self): for rec in self: - rec.need_validation = not self.review_ids and self.env[ - 'tier.definition'].search([('model', '=', self._name)]) and \ + tiers = self.env[ + 'tier.definition'].search([('model', '=', self._name)]) + valid_tiers = any([self.evaluate_tier(tier) for tier in tiers]) + rec.need_validation = not self.review_ids and valid_tiers and \ getattr(rec, self._state_field) in self._state_from @api.multi From 7e769842df955287a30c3a904d3d932ff861003f Mon Sep 17 00:00:00 2001 From: Lois Rilo Date: Fri, 2 Mar 2018 16:11:18 +0100 Subject: [PATCH 03/10] [9.0][IMP] base_tier_validation: tries automatically request validation and validate if possible. --- .../models/tier_validation.py | 49 ++++++++++++------- 1 file changed, 32 insertions(+), 17 deletions(-) diff --git a/base_tier_validation/models/tier_validation.py b/base_tier_validation/models/tier_validation.py index baf5361..d08e90e 100644 --- a/base_tier_validation/models/tier_validation.py +++ b/base_tier_validation/models/tier_validation.py @@ -48,13 +48,19 @@ class TierValidation(models.AbstractModel): @api.multi def _compute_validated_rejected(self): - """Override for different validation/rejection policy.""" for rec in self: - # sort by tier - rec.validated = not any( - [s != 'approved' for s in self.review_ids.mapped('status')]) - rec.rejected = any( - [s == 'rejected' for s in self.review_ids.mapped('status')]) + rec.validated = self._calc_reviews_validated(rec.review_ids) + rec.rejected = self._calc_reviews_rejected(rec.review_ids) + + @api.model + def _calc_reviews_validated(self, reviews): + """Override for different validation policy.""" + return not any([s != 'approved' for s in reviews.mapped('status')]) + + @api.model + def _calc_reviews_rejected(self, reviews): + """Override for different rejection policy.""" + return any([s == 'rejected' for s in reviews.mapped('status')]) @api.multi def _compute_need_validation(self): @@ -80,9 +86,13 @@ class TierValidation(models.AbstractModel): if (getattr(rec, self._state_field) in self._state_from and vals.get(self._state_field) in self._state_to): if rec.need_validation: - raise ValidationError(_( - "This action needs to be validated for at least one " - "record. \nPlease request a validation.")) + # try to validate operation + reviews = rec.request_validation() + rec._validate_tier(reviews) + if not self._calc_reviews_validated(reviews): + raise ValidationError(_( + "This action needs to be validated for at least " + "one record. \nPlease request a validation.")) if not rec.validated: raise ValidationError(_( "A validation process is still open for at least " @@ -95,14 +105,19 @@ class TierValidation(models.AbstractModel): self.mapped('review_ids').sudo().unlink() return super(TierValidation, self).write(vals) + def _validate_tier(self, tiers=False): + self.ensure_one() + tier_reviews = tiers or self.review_ids + user_reviews = tier_reviews.filtered( + lambda r: r.status in ('pending', 'rejected') and + (r.reviewer_id == self.env.user or + r.reviewer_group_id in self.env.user.groups_id)) + user_reviews.write({'status': 'approved'}) + @api.multi def validate_tier(self): for rec in self: - user_reviews = rec.review_ids.filtered( - lambda r: r.status in ('pending', 'rejected') and - (r.reviewer_id == self.env.user or - r.reviewer_group_id in self.env.user.groups_id)) - user_reviews.write({'status': 'approved'}) + rec._validate_tier() @api.multi def reject_tier(self): @@ -116,7 +131,7 @@ class TierValidation(models.AbstractModel): @api.multi def request_validation(self): td_obj = self.env['tier.definition'] - tr_obj = self.env['tier.review'] + tr_obj = created_trs = self.env['tier.review'] for rec in self: if getattr(rec, self._state_field) in self._state_from: if rec.need_validation: @@ -126,11 +141,11 @@ class TierValidation(models.AbstractModel): for td in tier_definitions: if self.evaluate_tier(td): sequence += 1 - tr_obj.create({ + created_trs += tr_obj.create({ 'model': self._name, 'res_id': rec.id, 'definition_id': td.id, 'sequence': sequence, }) # TODO: notify? post some msg in chatter? - return True + return created_trs From beee538a64334001eb526f3f10a59e826ee050ec Mon Sep 17 00:00:00 2001 From: Lois Rilo Date: Fri, 2 Mar 2018 16:18:03 +0100 Subject: [PATCH 04/10] [9.0][IMP] base_tier_validation: filter out reviews not pending --- base_tier_validation/models/tier_validation.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/base_tier_validation/models/tier_validation.py b/base_tier_validation/models/tier_validation.py index d08e90e..fc97f09 100644 --- a/base_tier_validation/models/tier_validation.py +++ b/base_tier_validation/models/tier_validation.py @@ -43,7 +43,9 @@ class TierValidation(models.AbstractModel): @api.model def _search_reviewer_ids(self, operator, value): reviews = self.env['tier.review'].search([ - ('model', '=', self._name), ('reviewer_ids', operator, value)]) + ('model', '=', self._name), + ('reviewer_ids', operator, value), + ('status', '=', 'pending')]) return [('id', 'in', list(set(reviews.mapped('res_id'))))] @api.multi From 9c7b6ef7ee8a3907f57485ab6f2e53746fb117cd Mon Sep 17 00:00:00 2001 From: Lois Rilo Date: Fri, 2 Mar 2018 16:39:09 +0100 Subject: [PATCH 05/10] make possible to filter by validated records --- base_tier_validation/models/tier_validation.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/base_tier_validation/models/tier_validation.py b/base_tier_validation/models/tier_validation.py index fc97f09..ce9763d 100644 --- a/base_tier_validation/models/tier_validation.py +++ b/base_tier_validation/models/tier_validation.py @@ -24,7 +24,10 @@ class TierValidation(models.AbstractModel): domain=lambda self: [('model', '=', self._name)], auto_join=True, ) - validated = fields.Boolean(compute="_compute_validated_rejected") + validated = fields.Boolean( + compute="_compute_validated_rejected", + search="_search_validated", + ) need_validation = fields.Boolean(compute="_compute_need_validation") rejected = fields.Boolean(compute="_compute_validated_rejected") reviewer_ids = fields.Many2many( @@ -40,6 +43,15 @@ class TierValidation(models.AbstractModel): rec.reviewer_ids = rec.review_ids.filtered( lambda r: r.status == 'pending').mapped('reviewer_ids') + @api.model + def _search_validated(self, operator, value): + assert operator in ('=', '!='), 'Invalid domain operator' + assert value in (True, False), 'Invalid domain value' + pos = self.search([ + (self._state_field, 'in', self._state_from)]).filtered( + lambda r: r.review_ids and r.validated == value) + return [('id', 'in', pos.ids)] + @api.model def _search_reviewer_ids(self, operator, value): reviews = self.env['tier.review'].search([ From 950d6af5ac4cf25ebf3f173df58cb60f8dbd8e09 Mon Sep 17 00:00:00 2001 From: Lois Rilo Date: Thu, 15 Mar 2018 12:17:19 +0100 Subject: [PATCH 06/10] allow to add exceptions for fields that can be written on under validation records --- base_tier_validation/models/tier_validation.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/base_tier_validation/models/tier_validation.py b/base_tier_validation/models/tier_validation.py index ce9763d..e6cd559 100644 --- a/base_tier_validation/models/tier_validation.py +++ b/base_tier_validation/models/tier_validation.py @@ -94,6 +94,21 @@ class TierValidation(models.AbstractModel): "Error evaluating tier validation conditions.\n %s") % error) return res + @api.model + def _get_under_validation_exceptions(self): + """Extend for more field exceptions.""" + return ['message_follower_ids'] + + @api.multi + def _check_allow_write_under_validation(self, vals): + """Allow to add exceptions for fields that are allowed to be written + even when the record is under validation.""" + exceptions = self._get_under_validation_exceptions() + for val in vals: + if val not in exceptions: + return False + return True + @api.multi def write(self, vals): for rec in self: @@ -113,7 +128,8 @@ class TierValidation(models.AbstractModel): "one record.")) if (rec.review_ids and getattr(rec, self._state_field) in self._state_from and not vals.get(self._state_field) in - (self._state_to + [self._cancel_state])): + (self._state_to + [self._cancel_state]) and not + self._check_allow_write_under_validation(vals)): raise ValidationError(_("The operation is under validation.")) if vals.get(self._state_field) in self._state_from: self.mapped('review_ids').sudo().unlink() From cffdcfea0f275beb7f3f7aaf5e921f89844a4101 Mon Sep 17 00:00:00 2001 From: Lois Rilo Date: Fri, 23 Mar 2018 10:44:34 +0100 Subject: [PATCH 07/10] [9.0][IMP] base_tier_validation: * able to restart validation * sudo() not needed anymore --- base_tier_validation/__openerp__.py | 2 +- base_tier_validation/models/tier_validation.py | 13 ++++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/base_tier_validation/__openerp__.py b/base_tier_validation/__openerp__.py index ce34496..c9d97d9 100644 --- a/base_tier_validation/__openerp__.py +++ b/base_tier_validation/__openerp__.py @@ -4,7 +4,7 @@ { "name": "Base Tier Validation", "summary": "Implement a validation process based on tiers.", - "version": "9.0.1.0.0", + "version": "9.0.1.0.1", "category": "Tools", "website": "https://github.com/OCA/server-tools", "author": "Eficent, Odoo Community Association (OCA)", diff --git a/base_tier_validation/models/tier_validation.py b/base_tier_validation/models/tier_validation.py index e6cd559..739b2d6 100644 --- a/base_tier_validation/models/tier_validation.py +++ b/base_tier_validation/models/tier_validation.py @@ -15,7 +15,6 @@ class TierValidation(models.AbstractModel): _state_to = ['confirmed'] _cancel_state = 'cancel' - # TODO: reset validation? # TODO: step by step validation? review_ids = fields.One2many( @@ -69,6 +68,8 @@ class TierValidation(models.AbstractModel): @api.model def _calc_reviews_validated(self, reviews): """Override for different validation policy.""" + if not reviews: + return False return not any([s != 'approved' for s in reviews.mapped('status')]) @api.model @@ -122,7 +123,7 @@ class TierValidation(models.AbstractModel): raise ValidationError(_( "This action needs to be validated for at least " "one record. \nPlease request a validation.")) - if not rec.validated: + if rec.review_ids and not rec.validated: raise ValidationError(_( "A validation process is still open for at least " "one record.")) @@ -132,7 +133,7 @@ class TierValidation(models.AbstractModel): self._check_allow_write_under_validation(vals)): raise ValidationError(_("The operation is under validation.")) if vals.get(self._state_field) in self._state_from: - self.mapped('review_ids').sudo().unlink() + self.mapped('review_ids').unlink() return super(TierValidation, self).write(vals) def _validate_tier(self, tiers=False): @@ -179,3 +180,9 @@ class TierValidation(models.AbstractModel): }) # TODO: notify? post some msg in chatter? return created_trs + + @api.multi + def restart_validation(self): + for rec in self: + if getattr(rec, self._state_field) in self._state_from: + rec.mapped('review_ids').unlink() From 0b4abd1158d7064131e3f24c46a85881e6b7f7b5 Mon Sep 17 00:00:00 2001 From: Lois Rilo Date: Mon, 26 Mar 2018 13:23:15 +0200 Subject: [PATCH 08/10] [10.0][MIG] base_tier_validation --- base_tier_validation/README.rst | 2 +- base_tier_validation/{__openerp__.py => __manifest__.py} | 2 +- base_tier_validation/models/tier_definition.py | 2 +- base_tier_validation/models/tier_review.py | 2 +- base_tier_validation/models/tier_validation.py | 6 +++--- base_tier_validation/views/tier_definition_view.xml | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) rename base_tier_validation/{__openerp__.py => __manifest__.py} (95%) diff --git a/base_tier_validation/README.rst b/base_tier_validation/README.rst index 8e27d1d..76bf575 100644 --- a/base_tier_validation/README.rst +++ b/base_tier_validation/README.rst @@ -27,7 +27,7 @@ To configure this module, you need to: .. 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/9.0 + :target: https://runbot.odoo-community.org/runbot/149/10.0 Known issues / Roadmap ====================== diff --git a/base_tier_validation/__openerp__.py b/base_tier_validation/__manifest__.py similarity index 95% rename from base_tier_validation/__openerp__.py rename to base_tier_validation/__manifest__.py index c9d97d9..1f2df18 100644 --- a/base_tier_validation/__openerp__.py +++ b/base_tier_validation/__manifest__.py @@ -4,7 +4,7 @@ { "name": "Base Tier Validation", "summary": "Implement a validation process based on tiers.", - "version": "9.0.1.0.1", + "version": "10.0.1.0.0", "category": "Tools", "website": "https://github.com/OCA/server-tools", "author": "Eficent, Odoo Community Association (OCA)", diff --git a/base_tier_validation/models/tier_definition.py b/base_tier_validation/models/tier_definition.py index ffa8144..c1ef1f9 100644 --- a/base_tier_validation/models/tier_definition.py +++ b/base_tier_validation/models/tier_definition.py @@ -2,7 +2,7 @@ # Copyright 2017 Eficent Business and IT Consulting Services S.L. # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -from openerp import api, fields, models +from odoo import api, fields, models class TierDefinition(models.Model): diff --git a/base_tier_validation/models/tier_review.py b/base_tier_validation/models/tier_review.py index e7afe7a..e34e713 100644 --- a/base_tier_validation/models/tier_review.py +++ b/base_tier_validation/models/tier_review.py @@ -2,7 +2,7 @@ # Copyright 2017 Eficent Business and IT Consulting Services S.L. # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -from openerp import api, fields, models +from odoo import api, fields, models class TierReview(models.Model): diff --git a/base_tier_validation/models/tier_validation.py b/base_tier_validation/models/tier_validation.py index 739b2d6..ff7a4ea 100644 --- a/base_tier_validation/models/tier_validation.py +++ b/base_tier_validation/models/tier_validation.py @@ -2,9 +2,9 @@ # Copyright 2017 Eficent Business and IT Consulting Services S.L. # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -from openerp import api, fields, models, _ -from openerp.exceptions import ValidationError, UserError -from openerp.tools.safe_eval import safe_eval +from odoo import api, fields, models, _ +from odoo.exceptions import ValidationError, UserError +from odoo.tools.safe_eval import safe_eval class TierValidation(models.AbstractModel): diff --git a/base_tier_validation/views/tier_definition_view.xml b/base_tier_validation/views/tier_definition_view.xml index 5511b38..5aadc35 100644 --- a/base_tier_validation/views/tier_definition_view.xml +++ b/base_tier_validation/views/tier_definition_view.xml @@ -37,7 +37,7 @@ - + From b957d888a348b865ef3b818de810b67591740320 Mon Sep 17 00:00:00 2001 From: Lois Rilo Date: Wed, 9 May 2018 17:08:13 +0200 Subject: [PATCH 09/10] [11.0][MIG] base_tier_validation --- base_tier_validation/README.rst | 2 +- base_tier_validation/__init__.py | 1 - base_tier_validation/__manifest__.py | 5 ++--- base_tier_validation/models/__init__.py | 1 - base_tier_validation/models/tier_definition.py | 1 - base_tier_validation/models/tier_review.py | 1 - base_tier_validation/models/tier_validation.py | 3 +-- 7 files changed, 4 insertions(+), 10 deletions(-) diff --git a/base_tier_validation/README.rst b/base_tier_validation/README.rst index 76bf575..977eb44 100644 --- a/base_tier_validation/README.rst +++ b/base_tier_validation/README.rst @@ -27,7 +27,7 @@ To configure this module, you need to: .. 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/250/11.0 Known issues / Roadmap ====================== diff --git a/base_tier_validation/__init__.py b/base_tier_validation/__init__.py index b44d765..31660d6 100644 --- a/base_tier_validation/__init__.py +++ b/base_tier_validation/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). from . import models diff --git a/base_tier_validation/__manifest__.py b/base_tier_validation/__manifest__.py index 1f2df18..febb73f 100644 --- a/base_tier_validation/__manifest__.py +++ b/base_tier_validation/__manifest__.py @@ -1,12 +1,11 @@ -# -*- coding: utf-8 -*- # Copyright 2017 Eficent Business and IT Consulting Services S.L. # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). { "name": "Base Tier Validation", "summary": "Implement a validation process based on tiers.", - "version": "10.0.1.0.0", + "version": "11.0.1.0.0", "category": "Tools", - "website": "https://github.com/OCA/server-tools", + "website": "https://github.com/OCA/server-ux", "author": "Eficent, Odoo Community Association (OCA)", "license": "AGPL-3", "application": False, diff --git a/base_tier_validation/models/__init__.py b/base_tier_validation/models/__init__.py index d7c418a..2c43513 100644 --- a/base_tier_validation/models/__init__.py +++ b/base_tier_validation/models/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). from . import tier_definition diff --git a/base_tier_validation/models/tier_definition.py b/base_tier_validation/models/tier_definition.py index c1ef1f9..1a027c0 100644 --- a/base_tier_validation/models/tier_definition.py +++ b/base_tier_validation/models/tier_definition.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2017 Eficent Business and IT Consulting Services S.L. # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). diff --git a/base_tier_validation/models/tier_review.py b/base_tier_validation/models/tier_review.py index e34e713..c5f46dc 100644 --- a/base_tier_validation/models/tier_review.py +++ b/base_tier_validation/models/tier_review.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2017 Eficent Business and IT Consulting Services S.L. # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). diff --git a/base_tier_validation/models/tier_validation.py b/base_tier_validation/models/tier_validation.py index ff7a4ea..5b6ec5f 100644 --- a/base_tier_validation/models/tier_validation.py +++ b/base_tier_validation/models/tier_validation.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2017 Eficent Business and IT Consulting Services S.L. # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). @@ -90,7 +89,7 @@ class TierValidation(models.AbstractModel): def evaluate_tier(self, tier): try: res = safe_eval(tier.python_code, globals_dict={'rec': self}) - except Exception, error: + except Exception as error: raise UserError(_( "Error evaluating tier validation conditions.\n %s") % error) return res From e0fd8c841644d0d7b4ce67c2097fa8d3ded37170 Mon Sep 17 00:00:00 2001 From: Lois Rilo Date: Thu, 10 May 2018 11:28:45 +0200 Subject: [PATCH 10/10] [11.0][IMP] base_tier_validation: add tests --- base_tier_validation/tests/__init__.py | 4 + base_tier_validation/tests/common.py | 13 ++ .../tests/test_tier_validation.py | 150 ++++++++++++++++++ .../tests/tier_validation_tester.py | 21 +++ 4 files changed, 188 insertions(+) create mode 100644 base_tier_validation/tests/__init__.py create mode 100644 base_tier_validation/tests/common.py create mode 100644 base_tier_validation/tests/test_tier_validation.py create mode 100644 base_tier_validation/tests/tier_validation_tester.py diff --git a/base_tier_validation/tests/__init__.py b/base_tier_validation/tests/__init__.py new file mode 100644 index 0000000..c5d19b1 --- /dev/null +++ b/base_tier_validation/tests/__init__.py @@ -0,0 +1,4 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import common +from . import test_tier_validation diff --git a/base_tier_validation/tests/common.py b/base_tier_validation/tests/common.py new file mode 100644 index 0000000..b14a726 --- /dev/null +++ b/base_tier_validation/tests/common.py @@ -0,0 +1,13 @@ +# Copyright 2018 Eficent Business and IT Consulting Services S.L. +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + + +def setup_test_model(env, model_clses): + for model_cls in model_clses: + model_cls._build_model(env.registry, env.cr) + + 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) + ) diff --git a/base_tier_validation/tests/test_tier_validation.py b/base_tier_validation/tests/test_tier_validation.py new file mode 100644 index 0000000..a2d70fb --- /dev/null +++ b/base_tier_validation/tests/test_tier_validation.py @@ -0,0 +1,150 @@ +# Copyright 2018 Eficent Business and IT Consulting Services S.L. +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). + +from odoo.tests import common +from odoo.exceptions import ValidationError, UserError +from .common import setup_test_model +from .tier_validation_tester import TierValidationTester + + +@common.at_install(False) +@common.post_install(True) +class TierTierValidation(common.SavepointCase): + + @classmethod + def setUpClass(cls): + super(TierTierValidation, cls).setUpClass() + + setup_test_model(cls.env, [TierValidationTester]) + + cls.test_model = cls.env[TierValidationTester._name] + + cls.tester_model = cls.env['ir.model'].search([ + ('model', '=', 'tier.validation.tester')]) + + # Access record: + cls.env["ir.model.access"].create({ + 'name': "access.tester", + 'model_id': cls.tester_model.id, + 'perm_read': 1, + 'perm_write': 1, + 'perm_create': 1, + 'perm_unlink': 1, + }) + + # Create users: + group_ids = cls.env.ref('base.group_system').ids + cls.test_user_1 = cls.env['res.users'].create({ + 'name': 'John', + 'login': 'test1', + 'groups_id': [(6, 0, group_ids)], + }) + cls.test_user_2 = cls.env['res.users'].create({ + 'name': 'Mike', + 'login': 'test2', + }) + + # Create tier definition: + cls.tier_def_obj = cls.env['tier.definition'] + cls.tier_def_obj.create({ + 'model_id': cls.tester_model.id, + 'review_type': 'individual', + 'reviewer_id': cls.test_user_1.id, + 'python_code': 'rec.test_field > 1.0', + }) + + cls.test_record = cls.test_model.create({ + 'test_field': 2.5, + }) + + def test_01_auto_validation(self): + """When the user can validate all future reviews, it is not needed + to request a validation, the action can be done straight forward.""" + self.test_record.sudo(self.test_user_1.id).action_confirm() + self.assertEqual(self.test_record.state, 'confirmed') + + def test_02_no_auto_validation(self): + """User with no right to validate future reviews must request a + validation.""" + with self.assertRaises(ValidationError): + self.test_record.sudo(self.test_user_2.id).action_confirm() + + def test_03_request_validation_approved(self): + """User 2 request a validation and user 1 approves it.""" + self.assertFalse(self.test_record.review_ids) + reviews = self.test_record.sudo( + self.test_user_2.id).request_validation() + self.assertTrue(reviews) + record = self.test_record.sudo(self.test_user_1.id) + record.validate_tier() + self.assertTrue(record.validated) + + def test_04_request_validation_rejected(self): + """Request validation, rejection and reset.""" + self.assertFalse(self.test_record.review_ids) + reviews = self.test_record.sudo( + self.test_user_2.id).request_validation() + self.assertTrue(reviews) + record = self.test_record.sudo(self.test_user_1.id) + record.reject_tier() + self.assertTrue(record.review_ids) + self.assertTrue(record.rejected) + record.restart_validation() + self.assertFalse(record.review_ids) + + def test_05_under_validation(self): + """Write is forbidden in a record under validation.""" + self.assertFalse(self.test_record.review_ids) + reviews = self.test_record.sudo( + self.test_user_2.id).request_validation() + self.assertTrue(reviews) + record = self.test_record.sudo(self.test_user_1.id) + with self.assertRaises(ValidationError): + record.write({'test_field': 0.5}) + + def test_06_validation_process_open(self): + """Operation forbidden while a validation process is open.""" + self.assertFalse(self.test_record.review_ids) + reviews = self.test_record.sudo( + self.test_user_2.id).request_validation() + self.assertTrue(reviews) + record = self.test_record.sudo(self.test_user_1.id) + with self.assertRaises(ValidationError): + record.action_confirm() + + def test_07_search_reviewers(self): + """Test search methods.""" + reviews = self.test_record.sudo( + self.test_user_2.id).request_validation() + self.assertTrue(reviews) + record = self.test_record.sudo(self.test_user_1.id) + self.assertIn(self.test_user_1, record.reviewer_ids) + res = self.test_model.search( + [('reviewer_ids', 'in', self.test_user_1.id)]) + self.assertTrue(res) + + def test_08_search_validated(self): + """Test for the validated search method.""" + self.test_record.sudo(self.test_user_2.id).request_validation() + res = self.test_model.sudo(self.test_user_1.id).search( + [('validated', '=', False)]) + self.assertTrue(res) + + def test_09_wrong_tier_definition(self): + """Error should raise with incorrect python expresions on + tier definitions.""" + self.tier_def_obj.create({ + 'model_id': self.tester_model.id, + 'review_type': 'individual', + 'reviewer_id': self.test_user_1.id, + 'python_code': 'rec.not_existing_field > 1.0', + }) + with self.assertRaises(UserError): + self.test_record.sudo(self.test_user_1.id).action_confirm() + + def test_10_dummy_tier_definition(self): + """Test tier.definition methods.""" + res = self.tier_def_obj._get_tier_validation_model_names() + self.assertEqual(res, []) + res = self.tier_def_obj.onchange_model_id() + self.assertTrue(res) diff --git a/base_tier_validation/tests/tier_validation_tester.py b/base_tier_validation/tests/tier_validation_tester.py new file mode 100644 index 0000000..dc51caf --- /dev/null +++ b/base_tier_validation/tests/tier_validation_tester.py @@ -0,0 +1,21 @@ +# Copyright 2018 Eficent Business and IT Consulting Services S.L. +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models + + +class TierValidationTester(models.Model): + _name = 'tier.validation.tester' + _inherit = ['tier.validation'] + + state = fields.Selection( + selection=[('draft', 'Draft'), + ('confirmed', 'Confirmed'), + ('cancel', 'Cancel')], + default='draft', + ) + test_field = fields.Float() + + @api.multi + def action_confirm(self): + self.write({'state': 'confirmed'})