Role Based Group Management

This commit is contained in:
karuna 2026-07-07 11:56:58 +05:30
parent d0ed9e4e9d
commit b44c591082
9 changed files with 366 additions and 0 deletions

View File

@ -0,0 +1 @@
from . import models

View File

@ -0,0 +1,17 @@
{
"name": "Role-Based Group Management",
"summary": "Manage reusable security-group roles for users",
"version": "18.0.1.4.0",
"category": "Administration/Access Rights",
"license": "LGPL-3",
"author": "Custom",
"depends": ["base"],
"data": [
"security/ir.model.access.csv",
"views/user_role_views.xml",
"views/res_users_views.xml",
"views/user_role_menus.xml",
],
"installable": True,
"application": False,
}

View File

@ -0,0 +1,2 @@
from . import res_users
from . import user_role

View File

@ -0,0 +1,109 @@
from odoo import _, api, Command, fields, models
from odoo.exceptions import AccessError, UserError
class ResUsers(models.Model):
_inherit = "res.users"
role_id = fields.Many2one(
comodel_name="res.users.role",
string="Role",
groups="base.group_system",
ondelete="set null",
copy=False,
help="Reusable role whose groups can be synchronized to this user.",
)
role_managed_group_ids = fields.Many2many(
comodel_name="res.groups",
relation="res_users_role_managed_group_rel",
column1="user_id",
column2="group_id",
string="Role-Managed Groups",
groups="base.group_system",
copy=False,
readonly=True,
help="Technical record of groups controlled by role synchronization.",
)
@api.model_create_multi
def create(self, vals_list):
users = super().create(vals_list)
users.filtered("role_id")._sync_groups_from_role()
return users
def write(self, values):
role_changed = "role_id" in values
result = super().write(values)
if role_changed:
self._sync_groups_from_role()
return result
def _get_protected_role_group_ids(self):
"""Return user-type groups that role synchronization must never revoke."""
user_type_category = self.env.ref(
"base.module_category_user_type", raise_if_not_found=False
)
if not user_type_category:
return self.env["res.groups"]
return self.env["res.groups"].search(
[("category_id", "=", user_type_category.id)]
)
def _sync_groups_from_role(self):
"""Synchronize access groups from the selected role.
When a role is selected it becomes the source of truth for application
access groups: groups not present in the role are removed. The only
groups protected from role synchronization are user-type groups
(Internal User, Portal, Public), because changing them through a role
can lock users out or convert an internal user into a portal user.
"""
protected_groups = self._get_protected_role_group_ids()
for user in self:
role_groups = user.role_id.group_ids
desired_groups = role_groups | role_groups.trans_implied_ids
desired_groups -= protected_groups
current_groups = user.groups_id
previously_managed = user.role_managed_group_ids
if user.role_id:
groups_to_remove = current_groups - desired_groups - protected_groups
direct_groups_to_add = role_groups - current_groups - protected_groups
managed_groups = desired_groups
else:
groups_to_remove = previously_managed - protected_groups
direct_groups_to_add = self.env["res.groups"]
managed_groups = self.env["res.groups"]
commands = [Command.unlink(group.id) for group in groups_to_remove]
commands += [Command.link(group.id) for group in direct_groups_to_add]
if commands:
user.write({"groups_id": commands})
user.write({
"role_managed_group_ids": [
Command.set((managed_groups & user.groups_id).ids)
],
})
return True
def action_fetch_role_groups(self):
self.ensure_one()
if not self.env.user.has_group("base.group_system"):
raise AccessError(_("Only Settings administrators can synchronize roles."))
if not self.role_id:
raise UserError(_("Select a role before fetching groups."))
self._sync_groups_from_role()
return {
"type": "ir.actions.client",
"tag": "display_notification",
"params": {
"title": _("Groups Fetched"),
"message": _("The role groups were synchronized successfully."),
"type": "success",
"sticky": False,
},
}

View File

@ -0,0 +1,126 @@
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
class ResUsersRole(models.Model):
_name = "res.users.role"
_description = "User Role"
_order = "name"
name = fields.Char(required=True, index=True)
group_ids = fields.Many2many(
comodel_name="res.groups",
relation="res_users_role_group_rel",
column1="role_id",
column2="group_id",
string="Groups",
domain=lambda self: self._get_allowed_group_domain(),
help="Security groups granted by this role. User-type groups are intentionally excluded.",
)
user_ids = fields.One2many(
comodel_name="res.users",
inverse_name="role_id",
string="Users",
readonly=True,
)
assigned_user_ids = fields.Many2many(
comodel_name="res.users",
string="Assigned Users",
compute="_compute_assigned_user_ids",
inverse="_inverse_assigned_user_ids",
domain=[("share", "=", False)],
help="Select internal users to assign this role. Their groups are synchronized automatically.",
)
user_count = fields.Integer(compute="_compute_user_count")
_sql_constraints = [
("name_unique", "UNIQUE(name)", "The role name must be unique."),
]
@api.model
def _get_allowed_group_domain(self):
"""Roles must not change whether an account is internal, portal, or public."""
user_type_category = self.env.ref(
"base.module_category_user_type", raise_if_not_found=False
)
return (
[("category_id", "!=", user_type_category.id)]
if user_type_category
else []
)
@api.depends("user_ids")
def _compute_user_count(self):
counts = self.env["res.users"]._read_group(
[("role_id", "in", self.ids)], ["role_id"], ["__count"]
)
count_by_role = {role.id: count for role, count in counts}
for role in self:
role.user_count = count_by_role.get(role.id, 0)
def _compute_assigned_user_ids(self):
users = self.env["res.users"].with_context(active_test=False).search(
[("role_id", "in", self.ids)]
)
for role in self:
role.assigned_user_ids = users.filtered(
lambda user: user.role_id == role
)
def _inverse_assigned_user_ids(self):
"""Apply additions/removals made through the role's user selector."""
Users = self.env["res.users"].with_context(active_test=False)
for role in self.filtered("id"):
current_users = Users.search([("role_id", "=", role.id)])
selected_users = role.assigned_user_ids
(current_users - selected_users).write({"role_id": False})
(selected_users - current_users).write({"role_id": role.id})
@api.constrains("group_ids")
def _check_group_ids_do_not_include_user_types(self):
user_type_category = self.env.ref(
"base.module_category_user_type", raise_if_not_found=False
)
if not user_type_category:
return
for role in self:
invalid_groups = role.group_ids.filtered(
lambda group: group.category_id == user_type_category
)
if invalid_groups:
raise ValidationError(
_(
"User-type groups cannot be assigned through roles: %(groups)s. "
"Keep the Internal User, Portal, or Public user type managed directly on the user.",
groups=", ".join(invalid_groups.mapped("display_name")),
)
)
def write(self, values):
groups_changed = "group_ids" in values
result = super().write(values)
if groups_changed:
users = self.env["res.users"].with_context(active_test=False).search(
[("role_id", "in", self.ids)]
)
users._sync_groups_from_role()
return result
def action_update_users(self):
self.ensure_one()
users = self.env["res.users"].with_context(active_test=False).search(
[("role_id", "=", self.id)]
)
users._sync_groups_from_role()
return {
"type": "ir.actions.client",
"tag": "display_notification",
"params": {
"title": _("Role Updated"),
"message": _(
"Groups were synchronized for %(count)s user(s).", count=len(users)
),
"type": "success",
"sticky": False,
},
}

View File

@ -0,0 +1,2 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_res_users_role_system,res.users.role system,model_res_users_role,base.group_system,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_res_users_role_system res.users.role system model_res_users_role base.group_system 1 1 1 1

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_users_form_role_management" model="ir.ui.view">
<field name="name">res.users.form.role.management</field>
<field name="model">res.users</field>
<field name="inherit_id" ref="base.view_users_form"/>
<field name="priority">15</field>
<field name="arch" type="xml">
<!-- Insert before Odoo's generated groups block so the role selector
appears at the top of Access Rights, just above User Type. -->
<xpath expr="//page[@name='access_rights']/field[@name='groups_id']" position="before">
<group string="Role Access" groups="base.group_system">
<field name="role_id"
string="Assigned Role"
options="{'no_create': True, 'no_create_edit': True}"/>
<button name="action_fetch_role_groups"
string="Fetch Groups"
type="object"
class="btn-secondary"
invisible="not role_id"/>
</group>
</xpath>
</field>
</record>
</odoo>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<menuitem id="menu_res_users_role"
name="Roles"
parent="base.menu_users"
action="action_res_users_role"
sequence="2"
groups="base.group_system"/>
</odoo>

View File

@ -0,0 +1,75 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_res_users_role_list" model="ir.ui.view">
<field name="name">res.users.role.list</field>
<field name="model">res.users.role</field>
<field name="arch" type="xml">
<list string="Roles">
<field name="name"/>
<field name="group_ids" widget="many2many_tags" options="{'no_create': True}"/>
<field name="user_count" string="Users"/>
</list>
</field>
</record>
<record id="view_res_users_role_form" model="ir.ui.view">
<field name="name">res.users.role.form</field>
<field name="model">res.users.role</field>
<field name="arch" type="xml">
<form string="Role">
<header>
<button name="action_update_users"
string="Update Users"
type="object"
class="btn-primary"
groups="base.group_system"
invisible="not id"/>
</header>
<sheet>
<div class="oe_title">
<label for="name"/>
<h1>
<field name="name" placeholder="e.g. Developer"/>
</h1>
</div>
<notebook>
<page string="Users" name="users">
<field name="assigned_user_ids"
string="Assigned Users"
context="{'active_test': False}"
options="{'no_create': True, 'no_create_edit': True}">
<list string="Assigned Users" editable="bottom" create="true" delete="true">
<field name="name"/>
<field name="login"/>
<field name="company_id" groups="base.group_multi_company"/>
<field name="active" column_invisible="True"/>
</list>
</field>
</page>
<page string="Groups" name="groups">
<field name="group_ids"
string="Groups"
options="{'no_create': True, 'no_create_edit': True}"
placeholder="Select security groups...">
<list string="Groups" editable="bottom" create="true" delete="true">
<field name="full_name" string="Group"/>
<field name="category_id" string="Application"/>
</list>
</field>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<record id="action_res_users_role" model="ir.actions.act_window">
<field name="name">Roles</field>
<field name="res_model">res.users.role</field>
<field name="view_mode">list,form</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">Create a reusable user role</p>
<p>Choose the security groups that administrators can synchronize to users.</p>
</field>
</record>
</odoo>