[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) # 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", "category": "Web",
"version": "14.0.1.0.0", "version": "14.0.1.0.0",
"license": "AGPL-3", "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) # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
from odoo import api, fields, models from odoo import api, fields, models
@ -13,13 +13,11 @@ class CustomFieldRestriction(models.Model):
required=True, required=True,
string="Field", string="Field",
) )
field_name = fields.Char( field_name = fields.Char(
related="field_id.name", related="field_id.name",
store=True, store=True,
string="Field Name", string="Field Name",
) )
required_model_id = fields.Many2one( required_model_id = fields.Many2one(
"ir.model", "ir.model",
ondelete="cascade", ondelete="cascade",
@ -32,7 +30,12 @@ class CustomFieldRestriction(models.Model):
string="invisible_model_id", string="invisible_model_id",
index=True, index=True,
) )
readonly_model_id = fields.Many2one(
"ir.model",
ondelete="cascade",
string="readonly_model_id",
index=True,
)
model_name = fields.Char( model_name = fields.Char(
compute="_compute_model_name", compute="_compute_model_name",
store=True, store=True,
@ -41,18 +44,77 @@ class CustomFieldRestriction(models.Model):
) )
condition_domain = fields.Char() condition_domain = fields.Char()
group_ids = fields.Many2many("res.groups", required=True) group_ids = fields.Many2many("res.groups", required=True)
required = fields.Boolean()
default_required = fields.Boolean(related="field_id.required") default_required = fields.Boolean(related="field_id.required")
required = fields.Boolean()
field_invisible = 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") @api.onchange("field_id")
def onchange_field_id(self): 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): def _compute_model_name(self):
for rec in self: for rec in self:
if rec.required_model_id: if rec.required_model_id:
rec.model_name = rec.required_model_id.model rec.model_name = rec.required_model_id.model
elif rec.invisible_model_id: elif rec.invisible_model_id:
rec.model_name = rec.invisible_model_id.model 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) # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
import json as simplejson import json as simplejson
@ -19,6 +19,10 @@ class IrModel(models.Model):
"custom.field.restriction", "custom.field.restriction",
"invisible_model_id", "invisible_model_id",
) )
custom_readonly_restriction_ids = fields.One2many(
"custom.field.restriction",
"readonly_model_id",
)
class Base(models.AbstractModel): class Base(models.AbstractModel):
@ -28,52 +32,98 @@ class Base(models.AbstractModel):
def fields_view_get( def fields_view_get(
self, view_id=None, view_type=False, toolbar=False, submenu=False 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 view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu
) )
if view_type not in ["form", "tree", "kanban"]: if view_type not in ["form", "tree"]:
return res return arch
# TODO speed up somehow # TODO speed up somehow
restrictions = self.env["custom.field.restriction"].search( restrictions = self.env["custom.field.restriction"].search(
[ [
"|",
("model_name", "=", self._name), ("model_name", "=", self._name),
("group_ids", "in", self.env.user.groups_id.ids), ("group_ids", "in", self.env.user.groups_id.ids),
] ]
) )
if not restrictions: if not restrictions:
return res return arch
doc = etree.XML(res["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"): for node in doc.xpath("//field"):
name = node.attrib.get("name") name = node.attrib.get("name")
restrictions_filtered = restrictions.filtered( restrictions_filtered = restrictions.filtered(
lambda x: x.field_id.name == name lambda x: x.field_id.name == name
) )
if not restrictions_filtered:
continue
for r in restrictions_filtered: for r in restrictions_filtered:
if ( field_node_str = "<field name='%s' invisible='1'/>"
view_type == "form" field_node_mod = bytes(
and self.env.context.get("params") '{"invisible": true,"column_invisible": true}', "utf-8"
and self.env.context["params"].get("id") )
): if view_type == "tree":
rec_id = self.env[r.model_name].browse( field_node_str = (
self.env.context["params"]["id"] "<field name='%s' column_invisible='1' optional='hide'/>"
) )
if r.condition_domain: if r.field_invisible and r.invisible_model_id:
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")
modifiers = simplejson.loads(node.get("modifiers")) 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)) node.set("modifiers", simplejson.dumps(modifiers))
res["arch"] = etree.tostring(doc) new_node = etree.fromstring(field_node_str % visibility_field_name)
if r.field_invisible: new_node.set("invisible", "1")
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 = 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)) node.set("modifiers", simplejson.dumps(modifiers))
res["arch"] = etree.tostring(doc) new_node = etree.fromstring(field_node_str % required_field_name)
return res 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 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")] [("model", "=", "res.partner"), ("name", "=", "name")]
) )
cls.partner_id_field_id = cls.env["ir.model.fields"].search( 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_model_id = cls.partner_name_field_id.model_id
cls.partner_title_model_id = cls.env["ir.model"].search( 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( cls.partner_title_name_field_id = cls.env["ir.model.fields"].search(
[("model", "=", "res.partner.title"), ("name", "=", "name")] [("model", "=", "res.partner.title"), ("name", "=", "name")]
@ -50,10 +50,29 @@ class TestFieldRequiredIvisibleManager(common.SavepointCase):
"condition_domain": "[('id', '>', 0)]", "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.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): 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() # onchange_field_id()
self.assertFalse(self.invisible_title_rec_id.required) self.assertFalse(self.invisible_title_rec_id.required)
self.invisible_title_rec_id.field_id = self.partner_title_name_field_id 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.required_rec_id._compute_model_name()
self.assertEqual(self.required_rec_id.model_name, "res.partner") self.assertEqual(self.required_rec_id.model_name, "res.partner")
# fields_view_get() # 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> <odoo>
<record id="view_model_form_inherit" model="ir.ui.view"> <record id="view_model_form_inherit" model="ir.ui.view">
@ -33,6 +32,7 @@
/> />
<field name="condition_domain" /> <field name="condition_domain" />
<field name="group_ids" widget="many2many_tags" /> <field name="group_ids" widget="many2many_tags" />
<field name="required_field_id" optional="hide" />
</tree> </tree>
</field> </field>
</page> </page>
@ -59,6 +59,35 @@
<field name="field_invisible" /> <field name="field_invisible" />
<field name="condition_domain" /> <field name="condition_domain" />
<field name="group_ids" widget="many2many_tags" /> <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> </tree>
</field> </field>
</page> </page>
@ -76,6 +105,7 @@
<field name="required_model_id" invisible="1" /> <field name="required_model_id" invisible="1" />
<field name="invisible_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="model_name" invisible="1" />
<field <field
name="field_id" name="field_id"
@ -91,8 +121,11 @@
/> />
<field <field
name="field_invisible" name="field_invisible"
attrs="{'readonly':[('default_required', '=', True)], attrs="{'invisible':[('invisible_model_id', '=', False)]}"
'invisible':[('invisible_model_id', '=', False)]}" />
<field
name="field_readonly"
attrs="{'invisible':[('readonly_model_id', '=', False)]}"
/> />
<field <field
name="condition_domain" name="condition_domain"