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.
202 lines
9.2 KiB
202 lines
9.2 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/>.
|
|
#
|
|
#############################################################################
|
|
from collections import defaultdict
|
|
from lxml import etree
|
|
from lxml.builder import E
|
|
from odoo import api, models
|
|
from odoo.addons.base.models.ir_model import MODULE_UNINSTALL_FLAG
|
|
|
|
|
|
class ResGroups(models.Model):
|
|
"""Extends res.groups to dynamically generate role-based group views."""
|
|
_inherit = 'res.groups'
|
|
|
|
def name_boolean_group(self, id):
|
|
"""Generate boolean group field name."""
|
|
return 'in_group_' + str(id)
|
|
|
|
def name_selection_groups(self, ids):
|
|
"""Generate selection group field name."""
|
|
return 'sel_groups_' + '_'.join(str(it) for it in sorted(ids))
|
|
|
|
def is_boolean_group(name):
|
|
"""Check if the field name belongs to a boolean group."""
|
|
return name.startswith('in_group_')
|
|
|
|
def is_selection_groups(name):
|
|
"""Check if the field name belongs to a selection group."""
|
|
return name.startswith('sel_groups_')
|
|
|
|
def get_boolean_group(name):
|
|
"""Extract group ID from a boolean group field name."""
|
|
return int(name[9:])
|
|
|
|
def get_selection_groups(name):
|
|
"""Extract group IDs from a selection group field name."""
|
|
return [int(v) for v in name[11:].split('_')]
|
|
|
|
@api.model
|
|
def _register_hook(self):
|
|
"""Hook to update role-based group view after module installation."""
|
|
super()._register_hook()
|
|
self._update_role_groups_view()
|
|
return True
|
|
|
|
@api.model
|
|
def get_groups_by_application(self):
|
|
""" Return all groups classified by application (module category), as a list::
|
|
[(app, kind, groups), ...],
|
|
where ``app`` and ``groups`` are recordsets, and ``kind`` is either
|
|
``'boolean'`` or ``'selection'``. Applications are given in sequence
|
|
order. If ``kind`` is ``'selection'``, ``groups`` are given in
|
|
reverse implication order.
|
|
"""
|
|
def linearize(app, gs, category_name):
|
|
if app.xml_id == 'base.module_category_user_type':
|
|
return (app, 'selection', gs.sorted('id'), category_name)
|
|
order = {g: len(g.trans_implied_ids & gs) for g in gs}
|
|
if app.xml_id == 'base.module_category_accounting_accounting':
|
|
return (app, 'selection', gs.sorted(key=order.get), category_name)
|
|
if len(set(order.values())) == len(gs):
|
|
return (app, 'selection', gs.sorted(key=order.get), category_name)
|
|
else:
|
|
return (app, 'boolean', gs, (100, 'Other'))
|
|
|
|
by_app, others = defaultdict(self.browse), self.browse()
|
|
for g in self.get_application_groups([]):
|
|
if g.category_id:
|
|
by_app[g.category_id] += g
|
|
else:
|
|
others += g
|
|
res = []
|
|
for app, gs in sorted(by_app.items(), key=lambda it: it[0].sequence or 0):
|
|
if app.parent_id:
|
|
res.append(
|
|
linearize(app, gs, (app.parent_id.sequence, app.parent_id.name)))
|
|
else:
|
|
res.append(linearize(app, gs, (100, 'Other')))
|
|
if others:
|
|
res.append(
|
|
(self.env['ir.module.category'], 'boolean', others, (100, 'Other')))
|
|
return res
|
|
|
|
@api.model
|
|
def customize_role_group_fields(self, field_name, attrs, model_name):
|
|
"""
|
|
Customize group field attributes for specific models.
|
|
"""
|
|
if model_name == 'access.role' and field_name == 'sel_groups_1_10_11':
|
|
# Hide the field but keep it in the form data
|
|
attrs['invisible'] = '1'
|
|
attrs['class'] = 'd-none'
|
|
return attrs
|
|
|
|
@api.model
|
|
def _update_role_groups_view(self):
|
|
"""
|
|
Modify the view with xmlid ``base.user_groups_view`` or custom view,
|
|
introducing reified group fields with customizations.
|
|
"""
|
|
self = self.with_context(lang=None)
|
|
view = self.env.ref('access_roles.access_role_view_form_groups',
|
|
raise_if_not_found=False)
|
|
|
|
if not (view and view._name == 'ir.ui.view'):
|
|
return
|
|
|
|
model_name = self._context.get('model', 'access.role')
|
|
if self._context.get('install_filename') or self._context.get(
|
|
MODULE_UNINSTALL_FLAG):
|
|
xml = E.field(name="groups_ids", position="after")
|
|
else:
|
|
group_no_one = self.env.ref('base.group_no_one')
|
|
xml0, xml2, xml3, xml4 = [], [], [], []
|
|
xml_by_category = {}
|
|
sorted_tuples = sorted(self.get_groups_by_application(),
|
|
key=lambda t: t[0].xml_id != 'base.module_category_user_type')
|
|
|
|
invisible_information = (
|
|
"All fields linked to groups must be present in the view "
|
|
"due to the overwrite of create and write. "
|
|
"The implied groups are calculated using this values.")
|
|
for app, kind, gs, category_name in sorted_tuples:
|
|
attrs = {}
|
|
if kind == 'selection':
|
|
field_name = self.name_selection_groups(gs.ids)
|
|
attrs['on_change'] = '1'
|
|
attrs = self.customize_role_group_fields(field_name, attrs,
|
|
model_name)
|
|
if category_name not in xml_by_category:
|
|
xml_by_category[category_name] = []
|
|
xml_by_category[category_name].append(E.newline())
|
|
xml_by_category[category_name].append(
|
|
E.field(name=field_name, **attrs))
|
|
xml_by_category[category_name].append(E.newline())
|
|
if attrs.get('groups') == 'base.group_no_one':
|
|
xml0.append(E.field(name=field_name,
|
|
**dict(attrs, groups='!base.group_no_one')))
|
|
xml0.append(etree.Comment(invisible_information))
|
|
else:
|
|
app_name = app.name or 'Other'
|
|
xml4.append(E.separator(string=app_name, **attrs))
|
|
left_group, right_group = [], []
|
|
group_count = 0
|
|
for g in gs:
|
|
field_name = self.name_boolean_group(g.id)
|
|
dest_group = left_group if group_count % 2 == 0 else right_group
|
|
attrs = self.customize_role_group_fields(field_name, attrs,
|
|
model_name)
|
|
if g == group_no_one:
|
|
dest_group.append(
|
|
E.field(name=field_name, invisible="True", **attrs))
|
|
dest_group.append(etree.Comment(invisible_information))
|
|
else:
|
|
dest_group.append(E.field(name=field_name, **attrs))
|
|
xml0.append(E.field(name=field_name,
|
|
**dict(attrs, invisible="True",
|
|
groups='!base.group_no_one')))
|
|
xml0.append(etree.Comment(invisible_information))
|
|
group_count += 1
|
|
xml4.append(E.group(*left_group))
|
|
xml4.append(E.group(*right_group))
|
|
xml4.append({'class': "o_label_nowrap"})
|
|
for xml_cat in sorted(xml_by_category.keys(), key=lambda it: it[0]):
|
|
master_category_name = xml_cat[1]
|
|
xml3.append(
|
|
E.group(*(xml_by_category[xml_cat]), string=master_category_name))
|
|
xml = E.field(
|
|
*(xml0),
|
|
E.group(*(xml2)),
|
|
E.group(*(xml3)),
|
|
E.group(*(xml4), groups='base.group_no_one'),
|
|
name="groups_ids", position="replace")
|
|
xml.addprevious(etree.Comment("GENERATED AUTOMATICALLY BY GROUPS"))
|
|
xml_content = etree.tostring(xml, pretty_print=True, encoding="unicode")
|
|
if xml_content != view.arch:
|
|
new_context = dict(view._context)
|
|
new_context.pop('install_filename', None)
|
|
new_context['lang'] = None
|
|
view.with_context(new_context).write({'arch': xml_content})
|
|
|
|
def get_application_groups(self, domain):
|
|
"""Return the non-share groups that satisfy ``domain``."""
|
|
return self.search(domain + [('share', '=', False)])
|
|
|