diff --git a/base_tier_validation/README.rst b/base_tier_validation/README.rst new file mode 100644 index 000000000..8e27d1d46 --- /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 000000000..b44d76594 --- /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 000000000..ce34496ec --- /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 000000000..d7c418aa8 --- /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 000000000..21b7dbc03 --- /dev/null +++ b/base_tier_validation/models/tier_definition.py @@ -0,0 +1,53 @@ +# -*- 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 = [] + for n, m in self.env.registry.models.iteritems(): + if hasattr(m, '_inherit') and 'tier.validation' in m._inherit: + res.append(n) + return res + + model_id = fields.Many2one( + comodel_name="ir.model", + string="Referenced Model", + domain=lambda self: [ + ('model', 'in', self._get_tier_validation_model_names())], + ) + 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"), + ) diff --git a/base_tier_validation/models/tier_review.py b/base_tier_validation/models/tier_review.py new file mode 100644 index 000000000..e7afe7a72 --- /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 000000000..027ceffa7 --- /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 000000000..c35d91e73 --- /dev/null +++ b/base_tier_validation/security/ir.model.access.csv @@ -0,0 +1,3 @@ +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,user.tier.definition,model_tier_definition,,1,0,0,0 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 000000000..5511b38b5 --- /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 000000000..df0509f70 --- /dev/null +++ b/base_tier_validation/views/tier_review_view.xml @@ -0,0 +1,22 @@ + + + + + + tier.review.tree + tier.review + + + + + + + + + + + +