[FIX] web_field_required_invisible_manager: reset on change

pull/2573/head
Ilyas 2023-07-26 10:58:34 +02:00
parent 5f98b52439
commit 245f1f1d88
7 changed files with 258 additions and 49 deletions

View File

@ -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",

View File

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

View File

@ -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 name='%s' invisible='1'/>"
field_node_mod = bytes(
'{"invisible": true,"column_invisible": true}', "utf-8"
)
if view_type == "tree":
field_node_str = (
"<field name='%s' column_invisible='1' optional='hide'/>"
)
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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,3 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="view_model_form_inherit" model="ir.ui.view">
@ -33,6 +32,7 @@
/>
<field name="condition_domain" />
<field name="group_ids" widget="many2many_tags" />
<field name="required_field_id" optional="hide" />
</tree>
</field>
</page>
@ -59,6 +59,35 @@
<field name="field_invisible" />
<field name="condition_domain" />
<field name="group_ids" widget="many2many_tags" />
<field name="visibility_field_id" optional="hide" />
</tree>
</field>
</page>
<page
string="Custom Readonly Fields"
name="custom_readonly_restriction"
>
<field
name="custom_readonly_restriction_ids"
nolabel="1"
context="{'default_readonly_model_id': model}"
>
<tree string="Fields">
<field name="required_model_id" invisible="1" />
<field name="invisible_model_id" invisible="1" />
<field name="readonly_model_id" invisible="1" />
<field name="model_name" invisible="1" />
<field
name="field_id"
context="{'search_by_technical_name': True, 'display_technical_name': True}"
domain="[('model_id.model', '=', model_name)]"
options="{'create': False, 'create_edit': False}"
/>
<field name="default_required" invisible="1" />
<field name="field_readonly" />
<field name="condition_domain" />
<field name="group_ids" widget="many2many_tags" />
<field name="readonly_field_id" optional="hide" />
</tree>
</field>
</page>
@ -76,6 +105,7 @@
<field name="required_model_id" invisible="1" />
<field name="invisible_model_id" invisible="1" />
<field name="readonly_model_id" invisible="1" />
<field name="model_name" invisible="1" />
<field
name="field_id"
@ -91,8 +121,11 @@
/>
<field
name="field_invisible"
attrs="{'readonly':[('default_required', '=', True)],
'invisible':[('invisible_model_id', '=', False)]}"
attrs="{'invisible':[('invisible_model_id', '=', False)]}"
/>
<field
name="field_readonly"
attrs="{'invisible':[('readonly_model_id', '=', False)]}"
/>
<field
name="condition_domain"