diff --git a/base_tier_validation/README.rst b/base_tier_validation/README.rst index 6d5a4ff..4cdcd67 100644 --- a/base_tier_validation/README.rst +++ b/base_tier_validation/README.rst @@ -76,6 +76,7 @@ Contributors ~~~~~~~~~~~~ * Lois Rilo +* Adrià Gil Sorribes Maintainers ~~~~~~~~~~~ diff --git a/base_tier_validation/__manifest__.py b/base_tier_validation/__manifest__.py index 667ae85..3d4b40a 100644 --- a/base_tier_validation/__manifest__.py +++ b/base_tier_validation/__manifest__.py @@ -3,7 +3,7 @@ { "name": "Base Tier Validation", "summary": "Implement a validation process based on tiers.", - "version": "11.0.1.1.0", + "version": "11.0.2.0.0", "development_status": "Mature", "maintainers": ['lreficent'], "category": "Tools", @@ -13,11 +13,17 @@ "application": False, "installable": True, "depends": [ - "base", + "web", + "bus", ], "data": [ "security/ir.model.access.csv", "views/tier_definition_view.xml", "views/tier_review_view.xml", + "views/assets_backend.xml", + ], + 'qweb': [ + 'static/src/xml/systray.xml', + 'static/src/xml/tier_review_template.xml', ], } diff --git a/base_tier_validation/migrations/11.0.1.2.0/post-migrate.py b/base_tier_validation/migrations/11.0.1.2.0/post-migrate.py new file mode 100644 index 0000000..32d5222 --- /dev/null +++ b/base_tier_validation/migrations/11.0.1.2.0/post-migrate.py @@ -0,0 +1,16 @@ +# Copyright 2019 Creu Blanca +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from openupgradelib.openupgrade import migrate + + +@migrate() +def migrate(env, version): + module_ids = env['ir.module.module'].search([ + ('name', '=', 'base_tier_validation_formula'), + ('state', '=', 'uninstalled') + ]) + if module_ids: + module_ids.sudo().button_install() + cr = env.cr + cr.execute("UPDATE tier_definition SET definition_type = 'formula'") diff --git a/base_tier_validation/models/__init__.py b/base_tier_validation/models/__init__.py index 2c43513..4bfc7ac 100644 --- a/base_tier_validation/models/__init__.py +++ b/base_tier_validation/models/__init__.py @@ -3,3 +3,4 @@ from . import tier_definition from . import tier_review from . import tier_validation +from . import res_users diff --git a/base_tier_validation/models/res_users.py b/base_tier_validation/models/res_users.py new file mode 100644 index 0000000..930845d --- /dev/null +++ b/base_tier_validation/models/res_users.py @@ -0,0 +1,38 @@ +# Copyright 2019 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, modules + + +class Users(models.Model): + _inherit = 'res.users' + + review_ids = fields.Many2many( + string="Reviews", comodel_name="tier.review" + ) + + @api.model + def review_user_count(self): + user_reviews = {} + to_review_docs = {} + for review in self.env.user.review_ids.filtered( + lambda r: r.status == 'pending'): + record = review.env[review.model].browse(review.res_id) + if not user_reviews.get(review['model']): + user_reviews[review.model] = { + 'name': record._description, + 'model': review.model, + 'icon': modules.module.get_module_icon( + self.env[review.model]._original_module), + 'pending_count': 0 + } + docs = to_review_docs.get(review.model) + if (docs and record not in docs) or not docs: + user_reviews[review.model]['pending_count'] += 1 + to_review_docs.setdefault(review.model, []).append(record) + return list(user_reviews.values()) + + @api.model + def get_reviews(self, data): + return self.env['tier.review'].search_read( + [('id', 'in', data.get('res_ids'))]) diff --git a/base_tier_validation/models/tier_definition.py b/base_tier_validation/models/tier_definition.py index 1a027c0..95e0b1d 100644 --- a/base_tier_validation/models/tier_definition.py +++ b/base_tier_validation/models/tier_definition.py @@ -6,13 +6,19 @@ from odoo import api, fields, models class TierDefinition(models.Model): _name = "tier.definition" - _rec_name = "model_id" + _rec_name = "name" + + @api.model + def _get_default_name(self): + return "New Tier Validation" @api.model def _get_tier_validation_model_names(self): res = [] return res + name = fields.Char( + 'Description', required=True, default=_get_default_name) model_id = fields.Many2one( comodel_name="ir.model", string="Referenced Model", @@ -22,8 +28,10 @@ class TierDefinition(models.Model): ) review_type = fields.Selection( string="Validated by", default="individual", - selection=[("individual", "Specific user"), - ("group", "Any user in a specific group.")] + selection=[ + ("individual", "Specific user"), + ("group", "Any user in a specific group."), + ] ) reviewer_id = fields.Many2one( comodel_name="res.users", string="Reviewer", @@ -31,13 +39,14 @@ class TierDefinition(models.Model): 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""", + definition_type = fields.Selection( + string="Definition", + selection=[ + ('domain', 'Domain'), + ], + default='domain', ) + definition_domain = fields.Char() active = fields.Boolean(default=True) sequence = fields.Integer(default=30) company_id = fields.Many2one( @@ -51,3 +60,8 @@ class TierDefinition(models.Model): return {'domain': { 'model_id': [ ('model', 'in', self._get_tier_validation_model_names())]}} + + @api.onchange('review_type') + def onchange_review_type(self): + self.reviewer_id = None + self.reviewer_group_id = None diff --git a/base_tier_validation/models/tier_review.py b/base_tier_validation/models/tier_review.py index 5e1c9c2..8eb8777 100644 --- a/base_tier_validation/models/tier_review.py +++ b/base_tier_validation/models/tier_review.py @@ -7,6 +7,7 @@ from odoo import api, fields, models class TierReview(models.Model): _name = "tier.review" + name = fields.Char(related="definition_id.name", readonly=True) status = fields.Selection( selection=[("pending", "Pending"), ("rejected", "Rejected"), @@ -38,9 +39,17 @@ class TierReview(models.Model): requested_by = fields.Many2one( comodel_name="res.users", ) + reviewed_date = fields.Datetime(string='Validation Date') + + @api.model + def _get_reviewer_fields(self): + return ['reviewer_id', 'reviewer_group_id', 'reviewer_group_id.users'] @api.multi - @api.depends('reviewer_id', 'reviewer_group_id', 'reviewer_group_id.users') + @api.depends(lambda self: self._get_reviewer_fields()) def _compute_reviewer_ids(self): for rec in self: - rec.reviewer_ids = rec.reviewer_id + rec.reviewer_group_id.users + rec.reviewer_ids = rec._get_reviewers() + + def _get_reviewers(self): + return self.reviewer_id + self.reviewer_group_id.users diff --git a/base_tier_validation/models/tier_validation.py b/base_tier_validation/models/tier_validation.py index 0150bf4..3b474b9 100644 --- a/base_tier_validation/models/tier_validation.py +++ b/base_tier_validation/models/tier_validation.py @@ -2,8 +2,8 @@ # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). from odoo import api, fields, models, _ -from odoo.exceptions import ValidationError, UserError -from odoo.tools.safe_eval import safe_eval +from odoo.exceptions import ValidationError +from ast import literal_eval class TierValidation(models.AbstractModel): @@ -22,6 +22,10 @@ class TierValidation(models.AbstractModel): domain=lambda self: [('model', '=', self._name)], auto_join=True, ) + review_ids_dropdown = fields.One2many( + related='review_ids', + help="Field needed to display the dropdown menu correctly" + ) validated = fields.Boolean( compute="_compute_validated_rejected", search="_search_validated", @@ -93,12 +97,10 @@ class TierValidation(models.AbstractModel): @api.multi def evaluate_tier(self, tier): - try: - res = safe_eval(tier.python_code, globals_dict={'rec': self}) - except Exception as error: - raise UserError(_( - "Error evaluating tier validation conditions.\n %s") % error) - return res + domain = [] + if tier.definition_domain: + domain = literal_eval(tier.definition_domain) + return self.search([('id', '=', self.id)] + domain) @api.model def _get_under_validation_exceptions(self): @@ -146,12 +148,13 @@ class TierValidation(models.AbstractModel): 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)) + (self.env.user in r.reviewer_ids)) user_reviews.write({ 'status': 'approved', 'done_by': self.env.user.id, + 'reviewed_date': fields.Datetime.now(), }) + # TODO: add message_post @api.multi def validate_tier(self): @@ -168,7 +171,9 @@ class TierValidation(models.AbstractModel): user_reviews.write({ 'status': 'rejected', 'done_by': self.env.user.id, + 'reviewed_date': fields.Datetime.now(), }) + # TODO: Add Message_post @api.multi def request_validation(self): @@ -190,7 +195,7 @@ class TierValidation(models.AbstractModel): 'sequence': sequence, 'requested_by': self.env.uid, }) - # TODO: notify? post some msg in chatter? + self._update_counter() return created_trs @api.multi @@ -198,3 +203,10 @@ class TierValidation(models.AbstractModel): for rec in self: if getattr(rec, self._state_field) in self._state_from: rec.mapped('review_ids').unlink() + self._update_counter() + + def _update_counter(self): + notifications = [] + channel = 'base.tier.validation' + notifications.append([channel, {}]) + self.env['bus.bus'].sendmany(notifications) diff --git a/base_tier_validation/readme/CONTRIBUTORS.rst b/base_tier_validation/readme/CONTRIBUTORS.rst index 4b57463..df81656 100644 --- a/base_tier_validation/readme/CONTRIBUTORS.rst +++ b/base_tier_validation/readme/CONTRIBUTORS.rst @@ -1 +1,2 @@ * Lois Rilo +* Adrià Gil Sorribes diff --git a/base_tier_validation/static/description/index.html b/base_tier_validation/static/description/index.html index ff0ae9b..b51096f 100644 --- a/base_tier_validation/static/description/index.html +++ b/base_tier_validation/static/description/index.html @@ -3,13 +3,13 @@ - + Base Tier Validation + + +
+

Base Tier Validation Formula

+ + +

Beta License: AGPL-3 OCA/server-ux Translate me on Weblate Try me on Runbot

+

This module includes the ability to define the tier definition domain +and the tier reviewers using python code.

+

Table of contents

+ +
+

Usage

+

To define the domain by python code choose the Formula option in the Definition field. +To define the reviewers by python code choose Python Expression option in the Validated by field.

+
+
+

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.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Creu Blanca
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

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.

+

This module is part of the OCA/server-ux project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/base_tier_validation_formula/tests/__init__.py b/base_tier_validation_formula/tests/__init__.py new file mode 100644 index 0000000..c5d19b1 --- /dev/null +++ b/base_tier_validation_formula/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_formula/tests/common.py b/base_tier_validation_formula/tests/common.py new file mode 100644 index 0000000..79b92d0 --- /dev/null +++ b/base_tier_validation_formula/tests/common.py @@ -0,0 +1,19 @@ +# 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) + ) + + +def teardown_test_model(env, model_clses): + for model_cls in model_clses: + del env.registry.models[model_cls._name] + env.registry.setup_models(env.cr) diff --git a/base_tier_validation_formula/tests/test_tier_validation.py b/base_tier_validation_formula/tests/test_tier_validation.py new file mode 100644 index 0000000..e94bed9 --- /dev/null +++ b/base_tier_validation_formula/tests/test_tier_validation.py @@ -0,0 +1,112 @@ +# 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 UserError +from .common import setup_test_model, teardown_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', + }) + cls.test_user_3 = cls.env['res.users'].create({ + 'name': 'Mary', + 'login': 'test3', + }) + + # Create tier definitions: + 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, + 'definition_domain': "[('test_field', '>', 1.0)]", + }) + + cls.test_record = cls.test_model.create({ + 'test_field': 2.5, + }) + + @classmethod + def tearDownClass(cls): + teardown_test_model(cls.env, + [TierValidationTester]) + super(TierTierValidation, cls).tearDownClass() + + def test_01_reviewer_from_python_expression(self): + tier_definition = self.tier_def_obj.create({ + 'model_id': self.tester_model.id, + 'review_type': 'individual', + 'reviewer_id': self.test_user_1.id, + 'definition_type': 'formula', + 'python_code': 'rec.test_field > 1.0', + }) + tier_definition.write({ + 'model_id': self.tester_model.id, + 'review_type': 'expression', + 'python_code': 'rec.test_field > 3.0', + }) + tier_definition.onchange_review_type() + tier_definition.write({ + 'reviewer_expression': 'rec.user_id', + }) + self.test_record.write({ + 'test_field': 3.5, + 'user_id': self.test_user_2.id, + }) + reviews = self.test_record.sudo( + self.test_user_3.id).request_validation() + self.assertTrue(reviews) + self.assertEqual(len(reviews), 2) + record = self.test_record.sudo(self.test_user_1.id) + self.assertIn(self.test_user_1, record.reviewer_ids) + self.assertIn(self.test_user_2, record.reviewer_ids) + res = self.test_model.search( + [('reviewer_ids', 'in', self.test_user_2.id)]) + self.assertTrue(res) + + def test_02_wrong_reviewer_expression(self): + """Error should raise with incorrect python expresions on + tier definitions.""" + self.tier_def_obj.create({ + 'model_id': self.tester_model.id, + 'review_type': 'expression', + 'reviewer_expression': 'rec.test_field', + 'python_code': 'rec.test_field > 1.0', + }) + with self.assertRaises(UserError): + self.test_record.sudo(self.test_user_3.id).request_validation() diff --git a/base_tier_validation_formula/tests/tier_validation_tester.py b/base_tier_validation_formula/tests/tier_validation_tester.py new file mode 100644 index 0000000..d0136be --- /dev/null +++ b/base_tier_validation_formula/tests/tier_validation_tester.py @@ -0,0 +1,23 @@ +# 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() + user_id = fields.Many2one(string="Assigned to:", + comodel_name="res.users") + + @api.multi + def action_confirm(self): + self.write({'state': 'confirmed'}) diff --git a/base_tier_validation_formula/views/tier_definition_view.xml b/base_tier_validation_formula/views/tier_definition_view.xml new file mode 100644 index 0000000..8498b08 --- /dev/null +++ b/base_tier_validation_formula/views/tier_definition_view.xml @@ -0,0 +1,25 @@ + + + + + + tier.definition.form + tier.definition + + + + + + + + + + + + + +