You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

388 lines
15 KiB

# -*- coding: utf-8 -*-
#############################################################################
#
# Cybrosys Technologies Pvt. Ltd.
#
# Copyright (C) 2025-TODAY Cybrosys Technologies(<https://www.cybrosys.com>).
# Author: Cybrosys Techno Solutions(<https://www.cybrosys.com>)
#
# You can modify it under the terms of the GNU AFFERO
# GENERAL PUBLIC LICENSE (AGPL v3), Version 3.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details.
#
# You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
# (AGPL v3) along with this program.
# If not, see <http://www.gnu.org/licenses/>.
#
#############################################################################
import itertools
from itertools import repeat
from odoo import api, Command, fields, models
from odoo.tools import partition
def is_boolean_group(name):
return name.startswith('in_group_')
def is_selection_groups(name):
return name.startswith('sel_groups_')
def is_reified_group(name):
return is_boolean_group(name) or is_selection_groups(name)
def get_selection_groups(name):
return [int(v) for v in name[11:].split('_')]
def get_boolean_group(name):
return int(name[9:])
def parse_m2m(commands):
"""return a list of ids corresponding to a Many2Many value"""
ids = []
for command in commands:
if isinstance(command, (tuple, list)):
if command[0] in (Command.UPDATE, Command.LINK):
ids.append(command[1])
elif command[0] == Command.CLEAR:
ids = []
elif command[0] == Command.SET:
ids = list(command[2])
else:
ids.append(command)
return ids
class AccessRole(models.Model):
"""Class for representing access role"""
_name = 'access.role'
_description = 'Access Role'
_inherit = ['mail.thread']
name = fields.Char(string='Name', required=True)
user_ids = fields.Many2many('res.users')
role_management_id = fields.Many2one('role.management')
groups_ids = fields.Many2many('res.groups',
string='Groups')
accesses_count = fields.Integer('# Access Rights',
compute='_compute_accesses_count', compute_sudo=True)
rules_count = fields.Integer('# Record Rules',
compute='_compute_accesses_count', compute_sudo=True)
groups_count = fields.Integer('# Groups',
compute='_compute_accesses_count', compute_sudo=True)
@api.depends('groups_ids')
def _compute_accesses_count(self):
"""Compute access counts"""
for user in self:
groups = user.groups_ids
user.accesses_count = len(groups.model_access)
user.rules_count = len(groups.rule_groups)
user.groups_count = len(groups)
@api.model
def default_get(self, fields):
"""
Override default_get to manage reified group fields properly and
set 'Internal User' as default for sel_groups_1_10_11.
"""
group_fields, fields = partition(is_reified_group, fields)
fields1 = (fields + ['groups_ids']) if group_fields else fields
values = super(AccessRole, self).default_get(fields1)
if 'groups_ids' not in values:
values['groups_ids'] = [
Command.set([1])]
elif isinstance(values['groups_ids'], list):
current_ids = parse_m2m(values['groups_ids'])
if 1 not in current_ids:
values['groups_ids'] = [Command.set(current_ids + [1])]
self._add_reified_groups(group_fields, values)
return values
def onchange(self, values, field_names, fields_spec):
"""
Handles onchange events by removing reified group fields from values and processing groups_ids.
Adds back the reified group values after calling the super method.
"""
reified_fnames = [fname for fname in fields_spec if is_reified_group(fname)]
if reified_fnames:
values = {key: val for key, val in values.items() if key != 'groups_ids'}
values = self._remove_reified_groups(values)
if any(is_reified_group(fname) for fname in field_names):
field_names = [fname for fname in field_names if
not is_reified_group(fname)]
field_names.append('groups_ids')
fields_spec = {
field_name: field_spec
for field_name, field_spec in fields_spec.items()
if not is_reified_group(field_name)
}
fields_spec['groups_ids'] = {}
result = super().onchange(values, field_names, fields_spec)
if reified_fnames and 'groups_ids' in result.get('value', {}):
self._add_reified_groups(reified_fnames, result['value'])
result['value'].pop('groups_ids', None)
return result
@property
def SELF_READABLE_FIELDS(self):
""" The list of fields a user can read on their own user record.
In order to add fields, please override this property on model extensions.
"""
return [
'signature', 'company_id', 'login', 'email', 'name', 'image_1920',
'image_1024', 'image_512', 'image_256', 'image_128', 'lang', 'tz',
'tz_offset', 'groups_ids', 'partner_id', 'write_date', 'action_id',
'avatar_1920', 'avatar_1024', 'avatar_512', 'avatar_256', 'avatar_128',
'share', 'device_ids',
]
@property
def SELF_WRITEABLE_FIELDS(self):
""" The list of fields a user can write on their own user record.
In order to add fields, please override this property on model extensions.
"""
return ['signature', 'action_id', 'company_id', 'email', 'name', 'image_1920',
'lang', 'tz']
@api.model_create_multi
def create(self, vals_list):
"""Set default group if none provided and clean up group data"""
new_vals_list = []
for values in vals_list:
if 'sel_groups_1_10_11' not in values and 'groups_ids' not in values:
values['groups_ids'] = [Command.set([1])]
elif 'sel_groups_1_10_11' in values and values['sel_groups_1_10_11'] is False:
values['groups_ids'] = [
Command.set([])]
new_vals_list.append(self._remove_reified_groups(values))
return super(AccessRole, self).create(new_vals_list)
def write(self, values):
"""Clean up group data and update user groups if changed"""
values = self._remove_reified_groups(values)
result = super(AccessRole, self).write(values)
if 'groups_ids' in values:
self._update_users_groups()
return result
def read(self, fields=None, load='_classic_read'):
"""
Reads records while handling reified group fields properly.
Ensures group fields are retrieved and processed after reading.
"""
fields1 = fields or list(self.fields_get())
group_fields, other_fields = partition(is_reified_group, fields1)
drop_groups_id = False
if group_fields and fields:
if 'groups_ids' not in other_fields:
other_fields.append('groups_ids')
drop_groups_id = True
else:
other_fields = fields
res = super(AccessRole, self).read(other_fields, load=load)
if group_fields:
for values in res:
self._add_reified_groups(group_fields, values)
if drop_groups_id:
values.pop('groups_ids', None)
return res
def action_get_groups(self):
"""Get the list of groups configured for the role"""
self.ensure_one()
return {
'name': 'Groups',
'view_mode': 'list,form',
'res_model': 'res.groups',
'type': 'ir.actions.act_window',
'context': {'create': False, 'delete': False},
'domain': [('id', 'in', self.groups_ids.ids)],
'target': 'current',
}
def action_get_accesses(self):
"""Get the list of access rights configured for the role"""
self.ensure_one()
return {
'name': 'Access Rights',
'view_mode': 'list,form',
'res_model': 'ir.model.access',
'type': 'ir.actions.act_window',
'context': {'create': False, 'delete': False},
'domain': [('id', 'in', self.groups_ids.model_access.ids)],
'target': 'current',
}
def action_get_rules(self):
"""Get the list of rules configured for the role"""
self.ensure_one()
return {
'name': 'Record Rules',
'view_mode': 'list,form',
'res_model': 'ir.rule',
'type': 'ir.actions.act_window',
'context': {'create': False, 'delete': False},
'domain': [('id', 'in', self.groups_ids.rule_groups.ids)],
'target': 'current',
}
def _remove_reified_groups(self, values):
""" return `values` without reified group fields """
add, rem = [], []
values1 = {}
for key, val in values.items():
if is_boolean_group(key):
(add if val else rem).append(get_boolean_group(key))
elif is_selection_groups(key):
rem += get_selection_groups(key)
if val:
add.append(val)
else:
values1[key] = val
if 'groups_ids' not in values and (add or rem):
added = self.env['res.groups'].sudo().browse(add)
added |= added.mapped('trans_implied_ids')
added_ids = added._ids
values1['groups_ids'] = list(itertools.chain(
zip(repeat(3), [gid for gid in rem if gid not in added_ids]),
zip(repeat(4), add)
))
return values1
def _update_users_groups(self):
"""Update groups for all users associated with this role"""
for role in self:
if role.user_ids:
role.user_ids.write({
'groups_id': [Command.set(role.groups_ids.ids)]
})
def _determine_fields_to_fetch(self, field_names, ignore_when_in_cache=False):
"""
Filters out reified group fields from the list of fields to fetch,
ensuring only valid fields are passed to the super method.
"""
valid_fields = partition(is_reified_group, field_names)[1]
return super()._determine_fields_to_fetch(valid_fields, ignore_when_in_cache)
def _read_format(self, fnames, load='_classic_read'):
"""
Ensures that only valid fields (excluding reified groups) are passed to the read function.
"""
valid_fields = partition(is_reified_group, fnames)[1]
return super()._read_format(valid_fields, load)
def _add_reified_groups(self, fields, values):
""" add the given reified group fields into `values` """
gids = set(parse_m2m(values.get('groups_ids') or []))
for f in fields:
if is_boolean_group(f):
values[f] = get_boolean_group(f) in gids
elif is_selection_groups(f):
sel_groups = self.env['res.groups'].sudo().browse(get_selection_groups(f))
sel_order = {g: len(g.trans_implied_ids & sel_groups) for g in sel_groups}
sel_groups = sel_groups.sorted(key=sel_order.get)
selected = [gid for gid in sel_groups.ids if gid in gids]
if self.env.ref('base.group_user').id in selected:
values[f] = self.env.ref('base.group_user').id
else:
values[f] = selected and selected[-1] or False
def fields_get(self, allfields=None, attributes=None):
"""
Retrieves field metadata and includes reified group fields dynamically.
"""
self.env['res.groups']._update_role_groups_view()
res = super(AccessRole, self).fields_get(allfields, attributes=attributes)
for app, kind, gs, category_name in self.env[
'res.groups'].sudo().get_groups_by_application():
if kind == 'selection':
selection_vals = [(False, '')]
if app.xml_id == 'base.module_category_user_type':
selection_vals = [(1, 'Internal User')]
field_name = self.env['res.groups'].name_selection_groups(gs.ids)
if allfields and field_name not in allfields:
continue
tips = []
if app.description:
tips.append(app.description + '\n')
tips.extend('%s: %s' % (g.name, g.comment) for g in gs if g.comment)
if field_name == 'sel_groups_1_10_11':
res[field_name] = {
'type': 'selection',
'string': app.name or 'Other',
'selection': selection_vals,
'help': '\n'.join(tips),
'exportable': False,
'invisible': True,
}
else:
res[field_name] = {
'type': 'selection',
'string': app.name or 'Other',
'selection': selection_vals + [(g.id, g.name) for g in gs],
'help': '\n'.join(tips),
'exportable': False,
'selectable': False,
}
else:
for g in gs:
field_name = self.env['res.groups'].name_boolean_group(g.id)
if allfields and field_name not in allfields:
continue
res[field_name] = {
'type': 'boolean',
'string': g.name,
'help': g.comment,
'exportable': False,
'selectable': False,
}
missing = set(self.SELF_WRITEABLE_FIELDS).union(
self.SELF_READABLE_FIELDS).difference(res.keys())
if allfields:
missing = missing.intersection(allfields)
if missing:
res.update({
key: dict(values, readonly=key not in self.SELF_WRITEABLE_FIELDS,
searchable=False)
for key, values in
super(AccessRole, self.sudo()).fields_get(missing, attributes).items()
})
return res
class IrModuleCategoryView(models.Model):
"""Inherits the ir.module.category model to track changes that
affect role group views."""
_inherit = "ir.module.category"
def write(self, values):
"""
Updates the module category and triggers a role group view update if the name changes.
"""
res = super().write(values)
if "name" in values:
self.env["res.groups"]._update_role_groups_view()
return res
def unlink(self):
"""
Deletes the module category and updates the role groups view accordingly.
"""
res = super().unlink()
self.env["res.groups"]._update_role_groups_view()
return res