diff --git a/web_field_required_invisible_manager/__manifest__.py b/web_field_required_invisible_manager/__manifest__.py index 9c4e99b67..3f954ab1f 100644 --- a/web_field_required_invisible_manager/__manifest__.py +++ b/web_field_required_invisible_manager/__manifest__.py @@ -1,6 +1,6 @@ # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) { - "name": "Web Field Required Invisible Manager", + "name": "Web Field Required Invisible Readonly Managerr", "category": "Web", "version": "14.0.1.0.0", "license": "AGPL-3", diff --git a/web_field_required_invisible_manager/models/custom_field_restriction.py b/web_field_required_invisible_manager/models/custom_field_restriction.py index ba543e9c3..b924f1e9e 100644 --- a/web_field_required_invisible_manager/models/custom_field_restriction.py +++ b/web_field_required_invisible_manager/models/custom_field_restriction.py @@ -1,4 +1,4 @@ -# Copyright 2020 ooops404 +# Copyright 2023 ooops404 # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) from odoo import api, fields, models @@ -13,13 +13,11 @@ class CustomFieldRestriction(models.Model): required=True, string="Field", ) - field_name = fields.Char( related="field_id.name", store=True, string="Field Name", ) - required_model_id = fields.Many2one( "ir.model", ondelete="cascade", @@ -32,7 +30,12 @@ class CustomFieldRestriction(models.Model): string="invisible_model_id", index=True, ) - + readonly_model_id = fields.Many2one( + "ir.model", + ondelete="cascade", + string="readonly_model_id", + index=True, + ) model_name = fields.Char( compute="_compute_model_name", store=True, @@ -41,18 +44,77 @@ class CustomFieldRestriction(models.Model): ) condition_domain = fields.Char() group_ids = fields.Many2many("res.groups", required=True) - required = fields.Boolean() default_required = fields.Boolean(related="field_id.required") + required = fields.Boolean() field_invisible = fields.Boolean() + field_readonly = fields.Boolean() + # generated technical fields used in form attrs: + visibility_field_id = fields.Many2one("ir.model.fields", ondelete="cascade") + readonly_field_id = fields.Many2one("ir.model.fields", ondelete="cascade") + required_field_id = fields.Many2one("ir.model.fields", ondelete="cascade") @api.onchange("field_id") def onchange_field_id(self): - self.required = self.field_id.required + self.update( + { + "required": self.field_id.required, + "field_invisible": False, + "field_readonly": self.field_id.readonly, + } + ) - @api.depends("required_model_id", "invisible_model_id") + @api.depends("required_model_id", "invisible_model_id", "readonly_model_id") def _compute_model_name(self): for rec in self: if rec.required_model_id: rec.model_name = rec.required_model_id.model elif rec.invisible_model_id: rec.model_name = rec.invisible_model_id.model + elif rec.readonly_model_id: + rec.model_name = rec.readonly_model_id.model + + @api.model + def create(self, vals): + rec = super().create(vals) + if rec.invisible_model_id and rec.field_invisible: + rec.create_restriction_field("visibility") + elif rec.readonly_model_id and rec.field_readonly: + rec.create_restriction_field("readonly") + elif rec.required_model_id and rec.required: + rec.create_restriction_field("required") + return rec + + def create_restriction_field(self, f_type): + field_name = self.get_field_name(f_type) + field_id = self.env["ir.model.fields"].search([("name", "=", field_name)]) + if f_type == "required": + rec_model_id = self.required_model_id.id + rec_field_name = "required_field_id" + elif f_type == "readonly": + rec_model_id = self.readonly_model_id.id + rec_field_name = "readonly_field_id" + elif f_type == "visibility": + rec_model_id = self.invisible_model_id.id + rec_field_name = "visibility_field_id" + if not field_id: + field_id = self.env["ir.model.fields"].create( + { + "name": field_name, + "model_id": rec_model_id, + "state": "manual", + "field_description": "%s %s field" % (self.field_id.name, f_type), + "store": False, + "ttype": "boolean", + "compute": "for r in self: r._compute_restrictions_fields()", + } + ) + self[rec_field_name] = field_id + + def get_field_name(self, f_type): + # e.g. x_computed_res_partner_name_readonly + res = "x_computed_%s_%s_%s" % ( + self.field_id.model.replace(".", "_"), + self.field_id.name, + f_type, + ) + return res diff --git a/web_field_required_invisible_manager/models/models.py b/web_field_required_invisible_manager/models/models.py index 3b1975f2a..78555ee24 100644 --- a/web_field_required_invisible_manager/models/models.py +++ b/web_field_required_invisible_manager/models/models.py @@ -1,4 +1,4 @@ -# Copyright 2020 ooops404 +# Copyright 2023 ooops404 # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) import json as simplejson @@ -19,6 +19,10 @@ class IrModel(models.Model): "custom.field.restriction", "invisible_model_id", ) + custom_readonly_restriction_ids = fields.One2many( + "custom.field.restriction", + "readonly_model_id", + ) class Base(models.AbstractModel): @@ -28,52 +32,98 @@ class Base(models.AbstractModel): def fields_view_get( self, view_id=None, view_type=False, toolbar=False, submenu=False ): - res = super().fields_view_get( + arch = super().fields_view_get( view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu ) - if view_type not in ["form", "tree", "kanban"]: - return res + if view_type not in ["form", "tree"]: + return arch # TODO speed up somehow restrictions = self.env["custom.field.restriction"].search( [ - "|", ("model_name", "=", self._name), ("group_ids", "in", self.env.user.groups_id.ids), ] ) if not restrictions: - return res - doc = etree.XML(res["arch"]) + return arch + else: + return self.create_restrictions_fields(restrictions, view_type, arch) + return arch + + def create_restrictions_fields(self, restrictions, view_type, arch): + doc = etree.XML(arch["arch"]) for node in doc.xpath("//field"): name = node.attrib.get("name") restrictions_filtered = restrictions.filtered( lambda x: x.field_id.name == name ) + if not restrictions_filtered: + continue for r in restrictions_filtered: - if ( - view_type == "form" - and self.env.context.get("params") - and self.env.context["params"].get("id") - ): - rec_id = self.env[r.model_name].browse( - self.env.context["params"]["id"] + field_node_str = "" + field_node_mod = bytes( + '{"invisible": true,"column_invisible": true}', "utf-8" + ) + if view_type == "tree": + field_node_str = ( + "" ) - if r.condition_domain: - filtered_rec_id = rec_id.filtered_domain( - safe_eval(r.condition_domain) - ) - if not filtered_rec_id: - continue - if r.required: - node.set("required", "1") + if r.field_invisible and r.invisible_model_id: modifiers = simplejson.loads(node.get("modifiers")) - modifiers["required"] = True + visibility_field_name = r.get_field_name("visibility") + modifiers["invisible"] = ( + "[('%s', '=', True)]" % visibility_field_name + ) node.set("modifiers", simplejson.dumps(modifiers)) - res["arch"] = etree.tostring(doc) - if r.field_invisible: - node.set("invisible", "1") + new_node = etree.fromstring(field_node_str % visibility_field_name) + new_node.set("invisible", "1") + new_node.set("modifiers", field_node_mod) + node.getparent().append(new_node) + if r.required_field_id and r.required_model_id: modifiers = simplejson.loads(node.get("modifiers")) - modifiers["invisible"] = True + required_field_name = r.get_field_name("required") + modifiers["required"] = "[('%s', '=', True)]" % required_field_name node.set("modifiers", simplejson.dumps(modifiers)) - res["arch"] = etree.tostring(doc) - return res + new_node = etree.fromstring(field_node_str % required_field_name) + new_node.set("invisible", "1") + new_node.set("modifiers", field_node_mod) + node.getparent().append(new_node) + if r.readonly_field_id and r.readonly_model_id: + modifiers = simplejson.loads(node.get("modifiers")) + readonly_field_name = r.get_field_name("readonly") + modifiers["readonly"] = "[('%s', '=', True)]" % readonly_field_name + node.set("modifiers", simplejson.dumps(modifiers)) + new_node = etree.fromstring(field_node_str % readonly_field_name) + new_node.set("invisible", "1") + new_node.set("modifiers", field_node_mod) + node.getparent().append(new_node) + arch["arch"] = etree.tostring(doc) + return arch + + def _compute_restrictions_fields(self): + """Common compute method for all restrictions types""" + for record in self: + restrictions = self.env["custom.field.restriction"].search( + [("model_name", "=", self._name)] + ) + if not restrictions: + return + for r in restrictions: + if r.visibility_field_id: + field_name = r.visibility_field_id.name + record[field_name] = False + if r.required_field_id: + field_name = r.required_field_id.name + record[field_name] = False + if r.readonly_field_id: + field_name = r.readonly_field_id.name + record[field_name] = False + if r.condition_domain: + filtered_rec_id = record.filtered_domain( + safe_eval(r.condition_domain) + ) + if filtered_rec_id and r.group_ids & self.env.user.groups_id: + record[field_name] = True + elif r.group_ids: + if r.group_ids & self.env.user.groups_id: + record[field_name] = True diff --git a/web_field_required_invisible_manager/readme/DESCRIPTION.rst b/web_field_required_invisible_manager/readme/DESCRIPTION.rst index 865933451..2f712e503 100644 --- a/web_field_required_invisible_manager/readme/DESCRIPTION.rst +++ b/web_field_required_invisible_manager/readme/DESCRIPTION.rst @@ -1,3 +1,3 @@ -This module allows to set a field required or invisible for users belonging to a specific group. +This module allows to set a field required, invisible or readonly for users belonging to a specific group. -The field can be required or invisible in any case, or according to specific conditions. +The field can be required, invisible or readonly in any case, or according to specific conditions. diff --git a/web_field_required_invisible_manager/readme/USAGE.rst b/web_field_required_invisible_manager/readme/USAGE.rst index 9bf077834..28ff48091 100644 --- a/web_field_required_invisible_manager/readme/USAGE.rst +++ b/web_field_required_invisible_manager/readme/USAGE.rst @@ -1,7 +1,7 @@ Go to Settings > Technical > Models -Select a model > in tab "Custom required fields" or "Custom invisible fields" add a line +Select a model > in tab "Custom required fields", "Custom invisible fields" or "Custom readonly fields" add a line -Select a field, add one or more group and enable flag "Required" or "Invisible" +Select a field, add one or more group and enable flag "Required", "Invisible" or "Readonly" -If needed, set a condition for which the field should be required or invisible for users of those groups. +If needed, set a condition for which the field should be required, invisible or readonly for users of those groups. diff --git a/web_field_required_invisible_manager/tests/test_web_field_required_invisible_manager.py b/web_field_required_invisible_manager/tests/test_web_field_required_invisible_manager.py index 6dac9b59b..2d3113297 100644 --- a/web_field_required_invisible_manager/tests/test_web_field_required_invisible_manager.py +++ b/web_field_required_invisible_manager/tests/test_web_field_required_invisible_manager.py @@ -12,11 +12,11 @@ class TestFieldRequiredIvisibleManager(common.SavepointCase): [("model", "=", "res.partner"), ("name", "=", "name")] ) cls.partner_id_field_id = cls.env["ir.model.fields"].search( - [("model", "=", "res.partner"), ("name", "=", "id")] + [("model", "=", "res.partner"), ("name", "=", "name")] ) cls.partner_model_id = cls.partner_name_field_id.model_id cls.partner_title_model_id = cls.env["ir.model"].search( - [("name", "=", "res.partner.title")] + [("model", "=", "res.partner.title")] ) cls.partner_title_name_field_id = cls.env["ir.model.fields"].search( [("model", "=", "res.partner.title"), ("name", "=", "name")] @@ -50,10 +50,29 @@ class TestFieldRequiredIvisibleManager(common.SavepointCase): "condition_domain": "[('id', '>', 0)]", } ) + cls.readonly_rec_id = cls.env["custom.field.restriction"].create( + { + "readonly_model_id": cls.partner_model_id.id, + "field_id": cls.partner_id_field_id.id, + "group_ids": [(6, 0, cls.env.ref("base.group_user").ids)], + "field_readonly": True, + "condition_domain": "[('id', '>', 0)]", + } + ) + cls.res_partner_title_madam = cls.env.ref("base.res_partner_title_madam") cls.deco_addict = cls.env.ref("base.res_partner_2") - cls.partner_view = cls.env.ref("base.view_partner_simple_form") + cls.partner_form_view = cls.env.ref("base.view_partner_simple_form") + cls.partner_tree_view = cls.env.ref("base.view_partner_tree") + cls.view_partner_title_form = cls.env.ref("base.view_partner_title_form") + cls.view_partner_title_tree = cls.env.ref("base.view_partner_title_tree") + cls.view_users_form = cls.env.ref("base.view_users_form") + cls.view_users_tree = cls.env.ref("base.view_users_tree") def test_all_web_field_required_invisible_manager(self): + # related fields are created + self.assertTrue(self.invisible_rec_id.visibility_field_id) + self.assertTrue(self.required_rec_id.required_field_id) + self.assertTrue(self.readonly_rec_id.readonly_field_id) # onchange_field_id() self.assertFalse(self.invisible_title_rec_id.required) self.invisible_title_rec_id.field_id = self.partner_title_name_field_id @@ -65,4 +84,49 @@ class TestFieldRequiredIvisibleManager(common.SavepointCase): self.required_rec_id._compute_model_name() self.assertEqual(self.required_rec_id.model_name, "res.partner") # fields_view_get() - self.deco_addict.fields_view_get(view_id=self.partner_view.id, view_type="form") + self.deco_addict.fields_view_get( + view_id=self.partner_form_view.id, view_type="form" + ) + self.deco_addict.fields_view_get( + view_id=self.partner_tree_view.id, view_type="tree" + ) + self.res_partner_title_madam.fields_view_get( + view_id=self.view_partner_title_form.id, view_type="form" + ) + self.res_partner_title_madam.fields_view_get( + view_id=self.view_partner_title_tree.id, view_type="tree" + ) + self.env.user.fields_view_get(view_id=self.view_users_form.id, view_type="form") + self.env.user.fields_view_get(view_id=self.view_users_tree.id, view_type="tree") + # read + self.deco_addict.read( + [ + "id", + "name", + "x_computed_res_partner_name_readonly", + "x_computed_res_partner_name_required", + "x_computed_res_partner_name_visibility", + ] + ) + self.res_partner_title_madam.read( + ["id", "name", "x_computed_res_partner_title_shortcut_visibility"] + ) + self.env.user.read(["id", "name"]) + self.env.user._compute_restrictions_fields() + # computed value + self.assertTrue(self.deco_addict.x_computed_res_partner_name_readonly) + self.assertTrue(self.deco_addict.x_computed_res_partner_name_required) + self.assertFalse(self.deco_addict.x_computed_res_partner_name_visibility) + self.assertTrue( + self.res_partner_title_madam.x_computed_res_partner_title_shortcut_visibility + ) + # change domain, reset cache. Should be True now + self.invisible_rec_id.condition_domain = "[('id', '>', 0)]" + self.deco_addict.invalidate_cache() + self.deco_addict.read(["x_computed_res_partner_name_visibility"]) + self.assertTrue(self.deco_addict.x_computed_res_partner_name_visibility) + # unlink + field_name = self.invisible_title_rec_id.get_field_name("visibility") + self.invisible_title_rec_id.unlink() + field_id = self.env["ir.model.fields"].search([("name", "=", field_name)]) + self.assertFalse(field_id) diff --git a/web_field_required_invisible_manager/views/views.xml b/web_field_required_invisible_manager/views/views.xml index 636bb65b5..cfe6c90d1 100644 --- a/web_field_required_invisible_manager/views/views.xml +++ b/web_field_required_invisible_manager/views/views.xml @@ -1,4 +1,3 @@ - @@ -33,6 +32,7 @@ /> + @@ -59,6 +59,35 @@ + + + + + + + + + + + + + + + + + @@ -76,6 +105,7 @@ + +