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 @@
+
+