@ -0,0 +1,138 @@ |
|||
# Odoo Access Role |
|||
|
|||
[](https://www.odoo.com) |
|||
[](https://opensource.org/licenses/MIT) |
|||
[](https://github.com/cybrosystech/Odoo-Access-Role) |
|||
|
|||
## Overview |
|||
|
|||
Odoo Access Role is an open-source module which enable access roles for users. |
|||
|
|||
## Features |
|||
|
|||
- 🔐 Easily assign predefined access rights using custom roles. |
|||
|
|||
- 🧩 Simplifies user access management by grouping permissions. |
|||
|
|||
- 🔄 Automatically updates views when roles or permissions change. |
|||
|
|||
- ✅ Ensures users get the correct permissions based on selected roles. |
|||
- |
|||
## Screenshots |
|||
|
|||
Here are some glimpses of Odoo Access Role in action: |
|||
|
|||
### 1. Access Role |
|||
|
|||
<div> |
|||
<tr> |
|||
<td align="center"> |
|||
<img src="static/description/assets/screenshots/access_roles-01.png" alt="Access Role1" width="500" style="border: none;"/> |
|||
</td> |
|||
</tr> |
|||
</div> |
|||
|
|||
### 2. Sales module |
|||
|
|||
<div> |
|||
<tr> |
|||
<td align="center"> |
|||
<img src="static/description/assets/screenshots/access_roles-02.png" alt="Role1" width="500" style="border: none;"/> |
|||
</td> |
|||
</tr> |
|||
</div> |
|||
<div> |
|||
<tr> |
|||
<td align="center"> |
|||
<img src="static/description/assets/screenshots/access_roles-03.png" alt="Role2" width="500" style="border: none;"/> |
|||
</td> |
|||
</tr> |
|||
</div> |
|||
<div> |
|||
<tr> |
|||
<td align="center"> |
|||
<img src="static/description/assets/screenshots/access_roles-04.png" alt="Role3" width="500" style="border: none;"/> |
|||
</td> |
|||
</tr> |
|||
</div> |
|||
<div> |
|||
<tr> |
|||
<td align="center"> |
|||
<img src="static/description/assets/screenshots/access_roles-05.png" alt="Role4" width="500" style="border: none;"/> |
|||
</td> |
|||
</tr> |
|||
</div> |
|||
<div> |
|||
<tr> |
|||
<td align="center"> |
|||
<img src="static/description/assets/screenshots/access_roles-06.png" alt="Role5" width="500" style="border: none;"/> |
|||
</td> |
|||
</tr> |
|||
</div> |
|||
<div> |
|||
<tr> |
|||
<td align="center"> |
|||
<img src="static/description/assets/screenshots/access_roles-07.png" alt="Role6" width="500" style="border: none;"/> |
|||
</td> |
|||
</tr> |
|||
</div> |
|||
<div> |
|||
<tr> |
|||
<td align="center"> |
|||
<img src="static/description/assets/screenshots/access_roles-08.png" alt="Role7" width="500" style="border: none;"/> |
|||
</td> |
|||
</tr> |
|||
</div> |
|||
<div> |
|||
<tr> |
|||
<td align="center"> |
|||
<img src="static/description/assets/screenshots/access_roles-09.png" alt="Role8" width="500" style="border: none;"/> |
|||
</td> |
|||
</tr> |
|||
</div> |
|||
|
|||
|
|||
## Configuration |
|||
|
|||
* No additional configurations needed. |
|||
|
|||
## Installation |
|||
|
|||
Follow these steps to set up and run the app: |
|||
|
|||
1. **Clone the Repository** |
|||
```bash |
|||
git clone https://github.com/cybrosystech/Odoo-Access-Role.git |
|||
cd Odoo-Access-Role |
|||
|
|||
|
|||
## Contributing |
|||
|
|||
We welcome contributions! Feel free to contribute and enhance the functionality. To get started: |
|||
|
|||
1. Fork the repository. |
|||
|
|||
2. Create a new branch: |
|||
``` |
|||
git checkout -b feature/your-feature-name |
|||
``` |
|||
3. Make changes and commit: |
|||
``` |
|||
git commit -m "Add your message here" |
|||
``` |
|||
4. Push your changes: |
|||
``` |
|||
git push origin feature/your-feature-name |
|||
``` |
|||
5. Create a Pull Request on GitHub. |
|||
|
|||
--- |
|||
- Submit a pull request with a clear description of your changes. |
|||
|
|||
## License |
|||
|
|||
This project is licensed under the AGPL-3. Feel free to use, modify, and distribute it as needed. |
|||
|
|||
## Contact |
|||
|
|||
For questions or support, reach out to the maintainers at info@cybrosys.com or open an issue on GitHub. |
|||
@ -0,0 +1,22 @@ |
|||
# -*- 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 . import models |
|||
@ -0,0 +1,59 @@ |
|||
# -*- 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/>. |
|||
# |
|||
############################################################################# |
|||
{ |
|||
'name': 'Access Roles', |
|||
'version': '18.0.1.0.0', |
|||
'category':'Security', |
|||
'sequence': 1, |
|||
'summary': 'Access Roles for users', |
|||
'description': """Access Roles for users""", |
|||
'author': 'Cybrosys Techno Solutions', |
|||
'company': 'Cybrosys Techno Solutions', |
|||
'maintainer': 'Cybrosys Techno Solutions', |
|||
'website': 'https://www.cybrosys.com', |
|||
'depends': ['base', 'mail', 'web'], |
|||
'data': [ |
|||
'security/access_roles_security.xml', |
|||
'security/ir.model.access.csv', |
|||
'views/access_role_views.xml', |
|||
'views/role_management_views.xml', |
|||
'views/res_users_views.xml', |
|||
'views/domain_model_views.xml', |
|||
'views/access_role_menus.xml' |
|||
], |
|||
'assets': { |
|||
'web.assets_backend': [ |
|||
'access_roles/static/src/js/chatter.js', |
|||
'access_roles/static/src/js/debug.js', |
|||
'access_roles/static/src/js/views/list_controller.js', |
|||
'access_roles/static/src/js/views/form_controller.js', |
|||
'access_roles/static/src/js/x2many.js', |
|||
'access_roles/static/src/js/form_cog_menu.js', |
|||
], |
|||
}, |
|||
'images': ['static/description/banner.jpg'], |
|||
'license': 'AGPL-3', |
|||
'installable': True, |
|||
'auto_install': False, |
|||
'application': True, |
|||
} |
|||
|
|||
@ -0,0 +1,6 @@ |
|||
## Module <access_roles> |
|||
|
|||
#### 21.07.2025 |
|||
#### Version 18.0.1.0.0 |
|||
##### ADD |
|||
- Initial commit for Access Roles. |
|||
@ -0,0 +1,34 @@ |
|||
# -*- 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 . import access_role |
|||
from . import button_registry |
|||
from . import domain_model |
|||
from . import field_access |
|||
from . import filter_registry |
|||
from . import groupby_registry |
|||
from . import ir_ui_menu |
|||
from . import ir_ui_view |
|||
from . import ir_rule |
|||
from . import res_users |
|||
from . import res_groups |
|||
from . import role_management |
|||
from . import tab_registry |
|||
@ -0,0 +1,388 @@ |
|||
# -*- 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 |
|||
@ -0,0 +1,98 @@ |
|||
# -*- 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 re |
|||
from odoo import api, Command, fields, models |
|||
|
|||
|
|||
class ButtonRegistry(models.Model): |
|||
"""Class for representing buttons""" |
|||
_name = 'button.registry' |
|||
_description = 'Button Registry' |
|||
|
|||
name = fields.Char(string='Button Name', required=True) |
|||
action_name = fields.Char(string='Action/Method Name') |
|||
model_id = fields.Many2one('ir.model', string='Model', ondelete='cascade') |
|||
view_ids = fields.Many2many('ir.ui.view', string='View') |
|||
|
|||
@api.model |
|||
def _register_hook(self): |
|||
""" |
|||
Hook that runs when the module is loaded. |
|||
Calls `get_all_buttons` to populate the button registry. |
|||
""" |
|||
super()._register_hook() |
|||
self.get_all_buttons() |
|||
return True |
|||
|
|||
def get_all_buttons(self): |
|||
"""Finds buttons from views and stores them.""" |
|||
views = self.env['ir.ui.view'].search([]) |
|||
button_model = {} |
|||
# Collect buttons per model, along with the view IDs where they appear. |
|||
for view in views: |
|||
if view.arch: |
|||
model_name = view.model |
|||
if model_name not in button_model: |
|||
button_model[model_name] = {} |
|||
# Find all buttons in the view XML with their attributes |
|||
button_pattern = r'<button\s+([^>]*)>' |
|||
buttons = re.findall(button_pattern, view.arch) |
|||
for button_attrs in buttons: |
|||
name_match = re.search(r'name="([^"]*)"', button_attrs) |
|||
if not name_match: |
|||
continue |
|||
button_name = name_match.group(1) |
|||
display_name = button_name |
|||
string_match = re.search(r'string="([^"]*)"', button_attrs) |
|||
if string_match: |
|||
display_name = string_match.group(1) |
|||
else: |
|||
title_match = re.search(r'title="([^"]*)"', button_attrs) |
|||
if title_match: |
|||
display_name = title_match.group(1) |
|||
button_key = (button_name, display_name) |
|||
if button_key not in button_model[model_name]: |
|||
button_model[model_name][button_key] = [] |
|||
if view.id not in button_model[model_name][button_key]: |
|||
button_model[model_name][button_key].append(view.id) |
|||
for model_name, buttons in button_model.items(): |
|||
if not model_name or not buttons: |
|||
continue |
|||
model_record = self.env['ir.model'].search( |
|||
[('model', '=', model_name)], limit=1) |
|||
if not model_record: |
|||
continue |
|||
for button_info, view_ids in buttons.items(): |
|||
button_name, display_name = button_info |
|||
existing_button = self.search([ |
|||
('name', '=', display_name), |
|||
('action_name', '=', button_name), |
|||
('model_id', '=', model_record.id) |
|||
], limit=1) |
|||
if not existing_button: |
|||
self.create({ |
|||
'name': display_name, |
|||
'action_name': button_name, |
|||
'model_id': model_record.id, |
|||
'view_ids': [Command.link(view) for view in view_ids], |
|||
}) |
|||
return button_model |
|||
@ -0,0 +1,38 @@ |
|||
# -*- 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 odoo import api, fields, models |
|||
|
|||
|
|||
class DomainModel(models.Model): |
|||
"""Class for storing domain rules for models.""" |
|||
_name = 'domain.model' |
|||
_description = 'Model Domain' |
|||
|
|||
name = fields.Text(string='Domain', default='[]', required=True) |
|||
domain_model_name = fields.Char(string='Model', compute='_compute_domain_model_name') |
|||
domain_model_id = fields.Many2one('ir.model', string='Model', domain="[('model', '!=', 'access.role')]") |
|||
|
|||
@api.depends('domain_model_id') |
|||
def _compute_domain_model_name(self): |
|||
"""Computes the technical name of the selected model.""" |
|||
for record in self: |
|||
record.domain_model_name = record.domain_model_id.model if record.domain_model_id else None |
|||
@ -0,0 +1,73 @@ |
|||
# -*- 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 odoo import api, fields, models |
|||
|
|||
|
|||
class FieldAccess(models.Model): |
|||
"""Manages access control for fields, buttons, tabs, and models.""" |
|||
_name = 'field.access' |
|||
|
|||
model_id = fields.Many2one('ir.model', domain="[('model', '!=', 'access.role')]") |
|||
button_invisible = fields.Char(help='Field for setting button visibility') |
|||
is_field_readonly = fields.Boolean(string='Readonly') |
|||
is_field_invisible = fields.Boolean(string='Invisible') |
|||
is_field_required = fields.Boolean(string='Required') |
|||
is_remove_link = fields.Boolean(string='Remove External link') |
|||
is_model_readonly = fields.Boolean(string='Readonly') |
|||
is_hide_create = fields.Boolean(string='Hide Create') |
|||
is_hide_delete = fields.Boolean(string='Hide Delete') |
|||
is_hide_duplicate = fields.Boolean(string='Hide Duplicate') |
|||
is_hide_archive = fields.Boolean(string='Hide Archive/UnArchive') |
|||
is_hide_export = fields.Boolean(string='Hide Export') |
|||
button_access_id = fields.Many2one('role.management') |
|||
button_ids = fields.Many2many( |
|||
'button.registry', |
|||
string='Model Button', |
|||
domain="[('model_id', '=', model_id)]") |
|||
tab_ids = fields.Many2many('tab.registry', string='Tab', |
|||
domain="[('model_id', '=', model_id)]") |
|||
access_field_id = fields.Many2one('role.management') |
|||
fields_ids = fields.Many2many('ir.model.fields', |
|||
domain="[('model_id', '=', model_id)]") |
|||
access_model_id = fields.Many2one('role.management') |
|||
hide_report_ids = fields.Many2many('ir.actions.report', |
|||
domain="[('model_id', '=', model_id)]") |
|||
hide_actions_ids = fields.Many2many('ir.actions.server', |
|||
domain="[('model_id', '=', model_id)]") |
|||
filter_access_id = fields.Many2one('role.management') |
|||
filter_ids = fields.Many2many('filter.registry', string='Filters', |
|||
domain="[('model_id', '=', model_id)]") |
|||
group_ids = fields.Many2many('groupby.registry', string='GroupBy', |
|||
domain="[('model_id', '=', model_id)]") |
|||
domain_access_id = fields.Many2one('role.management') |
|||
domain_id = fields.Many2one('domain.model') |
|||
|
|||
@api.onchange('model_id') |
|||
def _onchange_model_id(self): |
|||
"""Clears related fields when the model is changed.""" |
|||
self.fields_ids = False |
|||
self.filter_ids = False |
|||
self.group_ids = False |
|||
self.button_ids = False |
|||
self.hide_report_ids = False |
|||
self.hide_actions_ids = False |
|||
self.tab_ids = False |
|||
@ -0,0 +1,129 @@ |
|||
# -*- 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 xml.etree.ElementTree as ET |
|||
from odoo import api, Command, fields, models |
|||
|
|||
|
|||
class FilterRegistry(models.Model): |
|||
"""Stores and manages filters from search views.""" |
|||
_name = 'filter.registry' |
|||
_description = 'Filter Registry' |
|||
|
|||
name = fields.Char(string='Filter Name', required=True) |
|||
domain = fields.Char(string='Domain') |
|||
string = fields.Char(string='Display Name') |
|||
active = fields.Boolean(default=True) |
|||
model_id = fields.Many2one('ir.model', string='Model', ondelete='cascade') |
|||
view_ids = fields.Many2many('ir.ui.view', string='View') |
|||
|
|||
@api.model |
|||
def _register_hook(self): |
|||
"""Triggers filter extraction during module initialization.""" |
|||
super()._register_hook() |
|||
self.get_all_filters() |
|||
return True |
|||
|
|||
def _get_filter_elements_from_arch(self, arch): |
|||
"""Parse the XML arch and return a list of <filter> elements |
|||
that do not have 'group_by' in their context attribute.""" |
|||
try: |
|||
root = ET.fromstring(arch) |
|||
except ET.ParseError: |
|||
return [] |
|||
filter_elements = [] |
|||
for filter_el in root.iter("filter"): |
|||
context = filter_el.get("context", "") |
|||
if "group_by" in context: |
|||
continue |
|||
filter_elements.append(filter_el) |
|||
return filter_elements |
|||
|
|||
def _extract_filter_attributes_from_el(self, filter_el): |
|||
"""Extract attributes from a filter element.""" |
|||
return { |
|||
'name': filter_el.get('name') or '', |
|||
'domain': filter_el.get('domain') or '', |
|||
'string': filter_el.get('string') or '', |
|||
} |
|||
|
|||
def get_all_filters(self): |
|||
"""Collect all filters defined in search views.""" |
|||
search_views = self.env['ir.ui.view'].search([('type', '=', 'search')]) |
|||
filter_model = {} |
|||
for view in search_views: |
|||
if not view.arch: |
|||
continue |
|||
model_name = view.model |
|||
if model_name not in filter_model: |
|||
filter_model[model_name] = { |
|||
'filters': [], |
|||
'view_ids': [] |
|||
} |
|||
filter_elements = self._get_filter_elements_from_arch(view.arch) |
|||
for filter_el in filter_elements: |
|||
attributes = self._extract_filter_attributes_from_el(filter_el) |
|||
if not attributes['name']: |
|||
continue |
|||
filter_info = { |
|||
'name': attributes['name'], |
|||
'domain': attributes['domain'], |
|||
'string': attributes['string'] |
|||
} |
|||
if filter_info not in filter_model[model_name]['filters']: |
|||
filter_model[model_name]['filters'].append(filter_info) |
|||
if view.id not in filter_model[model_name]['view_ids']: |
|||
filter_model[model_name]['view_ids'].append(view.id) |
|||
for model_name, data in filter_model.items(): |
|||
if not model_name: |
|||
continue |
|||
model_record = self.env['ir.model'].search( |
|||
[('model', '=', model_name)], limit=1) |
|||
if not model_record: |
|||
continue |
|||
for filter_info in data['filters']: |
|||
self._create_or_update_filter( |
|||
filter_info['name'], |
|||
model_record.id, |
|||
data['view_ids'], |
|||
filter_info['domain'], |
|||
filter_info['string'] |
|||
) |
|||
return filter_model |
|||
|
|||
def _create_or_update_filter(self, name, model_id, view_ids, domain, string): |
|||
"""Create or update a filter registry record.""" |
|||
display_name = string if string else name |
|||
existing_filter = self.search([ |
|||
('name', '=', name), |
|||
('model_id', '=', model_id) |
|||
], limit=1) |
|||
vals = { |
|||
'name': display_name, |
|||
'model_id': model_id, |
|||
'view_ids': [Command.link(view) for view in view_ids], |
|||
'domain': domain, |
|||
'string': string |
|||
} |
|||
if existing_filter: |
|||
existing_filter.write(vals) |
|||
else: |
|||
self.create(vals) |
|||
@ -0,0 +1,122 @@ |
|||
# -*- 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 re |
|||
from odoo import api, Command, fields, models |
|||
|
|||
|
|||
class GroupByRegistry(models.Model): |
|||
"""Stores and manages group_by filters from search views.""" |
|||
_name = 'groupby.registry' |
|||
_description = 'GroupBy Registry' |
|||
|
|||
name = fields.Char(string='GroupBy Name', required=True) |
|||
context = fields.Char(string='Context') |
|||
string = fields.Char(string='Display Name') |
|||
active = fields.Boolean(default=True) |
|||
model_id = fields.Many2one('ir.model', string='Model', ondelete='cascade') |
|||
view_ids = fields.Many2many('ir.ui.view', string='View') |
|||
|
|||
@api.model |
|||
def _register_hook(self): |
|||
"""Triggers group_by extraction during module initialization.""" |
|||
super()._register_hook() |
|||
self.get_all_groupby() |
|||
return True |
|||
|
|||
def _extract_groupby_attributes(self, filter_tag): |
|||
"""Extract attributes from a group_by filter tag.""" |
|||
name_match = re.search(r'name="([^"]*)"', filter_tag) |
|||
context_match = re.search(r'context="([^"]*)"', filter_tag) |
|||
string_match = re.search(r'string="([^"]*)"', filter_tag) |
|||
|
|||
technical_name = name_match.group(1) if name_match else '' |
|||
display_name = string_match.group(1) if string_match else '' |
|||
|
|||
return { |
|||
'name': technical_name, |
|||
'context': context_match.group(1) if context_match else '', |
|||
'string': display_name |
|||
} |
|||
|
|||
def get_all_groupby(self): |
|||
"""Collect all group_by filters defined in search views.""" |
|||
search_views = self.env['ir.ui.view'].search([('type', '=', 'search')]) |
|||
groupby_model = {} |
|||
for view in search_views: |
|||
if not view.arch: |
|||
continue |
|||
model_name = view.model |
|||
if model_name not in groupby_model: |
|||
groupby_model[model_name] = { |
|||
'groupby': [], |
|||
'view_ids': [] |
|||
} |
|||
groupby_tags = re.findall( |
|||
r'<filter[^>]+?context="[^"]*group_by[^"]*"[^>]*?/>', view.arch) |
|||
for groupby_tag in groupby_tags: |
|||
attributes = self._extract_groupby_attributes(groupby_tag) |
|||
if not attributes['name']: |
|||
continue |
|||
groupby_info = { |
|||
'name': attributes['name'], |
|||
'context': attributes['context'], |
|||
'string': attributes['string'] |
|||
} |
|||
if groupby_info not in groupby_model[model_name]['groupby']: |
|||
groupby_model[model_name]['groupby'].append(groupby_info) |
|||
if view.id not in groupby_model[model_name]['view_ids']: |
|||
groupby_model[model_name]['view_ids'].append(view.id) |
|||
for model_name, data in groupby_model.items(): |
|||
if not model_name: |
|||
continue |
|||
model_record = self.env['ir.model'].search([('model', '=', model_name)], |
|||
limit=1) |
|||
if not model_record: |
|||
continue |
|||
for groupby_info in data['groupby']: |
|||
self._create_or_update_groupby( |
|||
groupby_info['name'], |
|||
model_record.id, |
|||
data['view_ids'], |
|||
groupby_info['context'], |
|||
groupby_info['string'] |
|||
) |
|||
return groupby_model |
|||
|
|||
def _create_or_update_groupby(self, name, model_id, view_ids, context, string): |
|||
"""Create or update a groupby registry record.""" |
|||
display_name = string if string else name |
|||
existing_groupby = self.search([ |
|||
('name', '=', name), |
|||
('model_id', '=', model_id) |
|||
], limit=1) |
|||
vals = { |
|||
'name': display_name, |
|||
'model_id': model_id, |
|||
'view_ids': [Command.link(view) for view in view_ids], |
|||
'context': context, |
|||
'string': string |
|||
} |
|||
if existing_groupby: |
|||
existing_groupby.write(vals) |
|||
else: |
|||
self.create(vals) |
|||
@ -0,0 +1,47 @@ |
|||
# -*- 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 odoo import models |
|||
from odoo.osv import expression |
|||
from odoo.tools.safe_eval import safe_eval |
|||
|
|||
|
|||
class IrRule(models.Model): |
|||
"""Extend ir.rule to apply role-based UI restrictions.""" |
|||
_inherit = "ir.rule" |
|||
|
|||
def _compute_domain(self, model_name, mode="read"): |
|||
""" |
|||
Override _compute_domain to include role-based domain restrictions. |
|||
""" |
|||
user = self.env.user |
|||
user_roles = user.access_role_id.role_management_id |
|||
role_domains = [] |
|||
for role in user_roles: |
|||
for access in role.domain_ids: |
|||
if access.domain_model_name == model_name and access.name: |
|||
role_domains.append(safe_eval(access.name)) |
|||
base_domain = super()._compute_domain(model_name, mode=mode) |
|||
if role_domains: |
|||
role_domain_combined = expression.OR(role_domains) |
|||
return expression.AND([base_domain, |
|||
role_domain_combined]) if base_domain else role_domain_combined |
|||
return base_domain |
|||
@ -0,0 +1,44 @@ |
|||
# -*- 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 odoo import api, models, registry |
|||
|
|||
|
|||
class IrUiMenu(models.Model): |
|||
"""Extend ir.ui.menu to apply role-based UI restrictions.""" |
|||
_inherit = 'ir.ui.menu' |
|||
|
|||
@api.model |
|||
def _visible_menu_ids(self, debug=False): |
|||
"""Override to dynamically hide menus based on role management.""" |
|||
visible_menu_ids = super()._visible_menu_ids(debug=debug) |
|||
role = self.env.user.access_role_id |
|||
hidden_menu_ids = set() |
|||
if role.role_management_id and role.role_management_id.menu_ids: |
|||
hidden_menu_ids.update(role.role_management_id.menu_ids.ids) |
|||
self.clear_caches() |
|||
return visible_menu_ids - hidden_menu_ids |
|||
|
|||
@api.model |
|||
def load_menus(self, debug=False): |
|||
"""Override to ensure menus are always fresh when loaded.""" |
|||
self.clear_caches() |
|||
return super().load_menus(debug=debug) |
|||
@ -0,0 +1,173 @@ |
|||
# -*- 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 odoo import models |
|||
|
|||
|
|||
class IrUiView(models.Model): |
|||
"""Extend ir.ui.view to apply role-based UI restrictions.""" |
|||
_inherit = 'ir.ui.view' |
|||
|
|||
def _postprocess_access_rights(self, tree): |
|||
"""Modify the view tree, list, kanban based on role-based access rules.""" |
|||
current_model = tree.get('model_access_rights') |
|||
tree = super()._postprocess_access_rights(tree) |
|||
current_user_access_role = self.env.user.access_role_id |
|||
if not current_user_access_role: |
|||
return tree |
|||
role_management = current_user_access_role.role_management_id |
|||
if not role_management: |
|||
return tree |
|||
button_access_records = role_management.button_access_ids |
|||
filter_access_records = role_management.filter_access_ids |
|||
field_access_records = role_management.field_access_ids |
|||
model_access_records = role_management.model_access_ids |
|||
tree = self._process_button_access(tree, button_access_records, current_model) |
|||
tree = self._process_tab_access(tree, button_access_records, current_model) |
|||
tree = self._process_filter_access(tree, filter_access_records, current_model) |
|||
tree = self._process_field_access(tree, field_access_records, current_model) |
|||
tree = self._process_model_access(tree, model_access_records, role_management, |
|||
current_model) |
|||
return tree |
|||
|
|||
def _process_button_access(self, tree, button_access_records, current_model): |
|||
"""Process button access restrictions.""" |
|||
for button in button_access_records: |
|||
for button_node in tree.xpath('//button'): |
|||
button_name = button_node.get('name') |
|||
for record in button.button_ids: |
|||
if current_model == record.model_id.model and button_name == record.action_name: |
|||
button_node.set('invisible', 'True') |
|||
return tree |
|||
|
|||
def _process_tab_access(self, tree, button_access_records, current_model): |
|||
"""Process tab access restrictions.""" |
|||
for tab in button_access_records: |
|||
for tab_node in tree.xpath('//page'): |
|||
tab_name = tab_node.get('string') |
|||
for record in tab.tab_ids: |
|||
if current_model == record.model_id.model and tab_name == record.name: |
|||
tab_node.set('invisible', 'True') |
|||
return tree |
|||
|
|||
def _process_filter_access(self, tree, filter_access_records, current_model): |
|||
"""Process filter and groupBy access restrictions.""" |
|||
for groupby in filter_access_records: |
|||
for filter_node in tree.xpath('//filter'): |
|||
filter_name = filter_node.get('string') |
|||
for record in groupby.filter_ids: |
|||
if current_model == record.model_id.model and filter_name == record.name: |
|||
filter_node.set('invisible', 'True') |
|||
for record in groupby.group_ids: |
|||
if current_model == record.model_id.model and filter_name == record.name: |
|||
filter_node.set('invisible', 'True') |
|||
return tree |
|||
|
|||
def _process_field_access(self, tree, field_access_records, current_model): |
|||
"""Process field access restrictions.""" |
|||
for field_model in field_access_records.fields_ids: |
|||
for field_node in tree.xpath('//field'): |
|||
field_name = field_node.get('name') |
|||
if field_model.model_id.model == current_model and field_name == field_model.name: |
|||
field_access = field_access_records.filtered( |
|||
lambda f: field_name in f.fields_ids.mapped("name")) |
|||
if field_access: |
|||
self._apply_field_attributes(field_node, field_access) |
|||
return tree |
|||
|
|||
def _apply_field_attributes(self, field_node, field_access): |
|||
"""Apply attributes to field nodes based on access rights.""" |
|||
if any(field_access.mapped("is_field_required")): |
|||
field_node.set("required", "1") |
|||
if any(field_access.mapped("is_field_invisible")): |
|||
field_node.set("invisible", "1") |
|||
if any(field_access.mapped("is_field_readonly")): |
|||
field_node.set("readonly", "1") |
|||
if any(field_access.mapped("is_remove_link")): |
|||
field_node.set("options", '{"no_open": true}') |
|||
|
|||
def _process_model_access(self, tree, model_access_records, role_management, |
|||
current_model): |
|||
"""Process model access restrictions for form, list, and kanban views.""" |
|||
tree = self._process_form_access(tree, model_access_records, role_management, |
|||
current_model) |
|||
tree = self._process_list_access(tree, model_access_records, role_management, |
|||
current_model) |
|||
tree = self._process_kanban_access(tree, model_access_records, |
|||
role_management, current_model) |
|||
return tree |
|||
|
|||
def _process_form_access(self, tree, model_access_records, role_management, |
|||
current_model): |
|||
"""Process form view access restrictions.""" |
|||
for model_node in tree.xpath('//form'): |
|||
if role_management.is_readonly: |
|||
model_node.set("edit", "false") |
|||
model_node.set("create", "false") |
|||
for model in model_access_records: |
|||
if current_model == model.model_id.model: |
|||
if model.is_model_readonly: |
|||
model_node.set("edit", "false") |
|||
model_node.set("create", "false") |
|||
for button_node in model_node.xpath('//button'): |
|||
button_node.set('invisible', 'True') |
|||
if model.is_hide_create: |
|||
model_node.set("create", "false") |
|||
if model.is_hide_delete: |
|||
model_node.set("delete", "false") |
|||
if model.is_hide_duplicate: |
|||
model_node.set("duplicate", "false") |
|||
return tree |
|||
|
|||
def _process_list_access(self, tree, model_access_records, role_management, |
|||
current_model): |
|||
"""Process list view access restrictions.""" |
|||
for list_node in tree.xpath('//list'): |
|||
if role_management.is_readonly: |
|||
list_node.set("edit", "false") |
|||
list_node.set("create", "false") |
|||
for model in model_access_records: |
|||
if current_model == model.model_id.model: |
|||
if model.is_model_readonly: |
|||
list_node.set("create", "false") |
|||
if model.is_hide_create: |
|||
list_node.set("create", "false") |
|||
if model.is_hide_delete: |
|||
list_node.set("delete", "false") |
|||
if model.is_hide_duplicate: |
|||
list_node.set("duplicate", "false") |
|||
return tree |
|||
|
|||
def _process_kanban_access(self, tree, model_access_records, role_management, |
|||
current_model): |
|||
"""Process kanban view access restrictions.""" |
|||
for kanban_node in tree.xpath('//kanban'): |
|||
if role_management.is_readonly: |
|||
kanban_node.set("edit", "false") |
|||
kanban_node.set("create", "false") |
|||
for model in model_access_records: |
|||
if current_model == model.model_id.model: |
|||
if model.is_model_readonly: |
|||
kanban_node.set("edit", "false") |
|||
kanban_node.set("create", "false") |
|||
if model.is_hide_create: |
|||
kanban_node.set("create", "false") |
|||
return tree |
|||
@ -0,0 +1,202 @@ |
|||
# -*- 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)]) |
|||
@ -0,0 +1,93 @@ |
|||
# -*- 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 odoo import _, api, Command, fields, models |
|||
|
|||
|
|||
class ResUsers(models.Model): |
|||
"""Extends res.users to integrate access roles and role-based permissions.""" |
|||
_inherit = 'res.users' |
|||
|
|||
access_role_id = fields.Many2one('access.role', string='Access Role', |
|||
help='Select the role of the user') |
|||
|
|||
@api.onchange('access_role_id') |
|||
def _onchange_access_role_id(self): |
|||
"""Warn users to save before assigning a role and update user-role relationships.""" |
|||
if not self._origin.id and self.access_role_id: # Record is new (not saved) |
|||
self.access_role_id = False |
|||
return { |
|||
'warning': { |
|||
'title': _("Warning"), |
|||
'message': _("Please save the user before assigning an Access Role."), |
|||
} |
|||
} |
|||
if not self.user_id: |
|||
if self._origin.access_role_id: |
|||
self._origin.access_role_id.write( |
|||
{'user_ids': [(fields.Command.unlink(self._origin.id))]}) |
|||
# Assign user to the new role |
|||
if self.access_role_id: |
|||
self.access_role_id.write( |
|||
{'user_ids': [(fields.Command.link(self._origin.id))]}) |
|||
else: |
|||
# Remove user from the previous role |
|||
if self._origin.access_role_id: |
|||
self._origin.access_role_id.write( |
|||
{'user_ids': [(fields.Command.unlink(self.user_id.id))]}) |
|||
# Assign user to the new role |
|||
if self.access_role_id: |
|||
self.access_role_id.write( |
|||
{'user_ids': [(fields.Command.link(self.user_id.id))]}) |
|||
|
|||
@api.model_create_multi |
|||
def create(self, vals_list): |
|||
"""Override create to assign groups based on the selected access role and creation of new users.""" |
|||
users = super(ResUsers, self).create(vals_list) |
|||
for user in users: |
|||
if user.access_role_id: |
|||
user.write({ |
|||
'groups_ids': [Command.set(user.access_role_id.groups_id.ids)] |
|||
}) |
|||
return users |
|||
|
|||
def write(self, vals): |
|||
"""Override write to manage role-based group assignment""" |
|||
groups_to_remove = None |
|||
# Handle role removal |
|||
if 'access_role_id' in vals and not vals['access_role_id']: |
|||
if self.access_role_id: |
|||
groups_to_remove = self.access_role_id.groups_ids |
|||
result = super(ResUsers, self).write(vals) |
|||
if 'access_role_id' in vals: |
|||
if vals['access_role_id']: |
|||
new_role = self.env['access.role'].browse(vals['access_role_id']) |
|||
self.write({ |
|||
'groups_id': [Command.set(new_role.groups_ids.ids)] |
|||
}) |
|||
elif groups_to_remove: |
|||
groups_list = groups_to_remove.ids |
|||
if 1 in groups_list: |
|||
groups_list.remove(1) |
|||
self.write({ |
|||
'groups_id': [Command.unlink(gid) for gid in groups_list] |
|||
}) |
|||
return result |
|||
@ -0,0 +1,138 @@ |
|||
# -*- 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 odoo import api, fields, models |
|||
|
|||
|
|||
class RoleManagement(models.Model): |
|||
"""Manages access roles, permissions, and system restrictions for users.""" |
|||
_name = 'role.management' |
|||
_description = 'Role Management' |
|||
_inherit = ['mail.thread'] |
|||
|
|||
name = fields.Char(string='Name', required=True) |
|||
domain_ids = fields.Many2many('domain.model') |
|||
is_debug = fields.Boolean( |
|||
string='Disable Debug Mode', |
|||
help='Disable the option to enable debug mode for the user') |
|||
is_chatter = fields.Boolean( |
|||
string='Disable chatter', |
|||
help='Disable chatter for the role' |
|||
) |
|||
is_readonly = fields.Boolean( |
|||
string='Make System ReadOnly', |
|||
help='Make system readonly' |
|||
) |
|||
role_ids = fields.Many2many('access.role', string='Roles', |
|||
domain="[('id','not in', selected_role_ids)]") |
|||
menu_ids = fields.Many2many('ir.ui.menu', domain="[('id','not in', access_role_menu_ids)]") |
|||
access_role_menu_ids = fields.Many2many('ir.ui.menu', compute='_compute_access_role_menu_ids') |
|||
selected_role_ids = fields.Many2many('access.role', |
|||
compute='_compute_selected_role_ids') |
|||
field_access_ids = fields.One2many('field.access', 'access_field_id') |
|||
model_access_ids = fields.One2many('field.access', 'access_model_id') |
|||
button_access_ids = fields.One2many('field.access', 'button_access_id') |
|||
domain_access_ids = fields.One2many('field.access', 'domain_access_id') |
|||
filter_access_ids = fields.One2many('field.access', 'filter_access_id') |
|||
|
|||
@api.depends('role_ids') |
|||
def _compute_selected_role_ids(self): |
|||
"""Compute selected roles to prevent duplicate role assignments.""" |
|||
for record in self: |
|||
record.selected_role_ids = self.search([]).mapped('role_ids').ids |
|||
for role in record.role_ids: |
|||
role.role_management_id = record.id |
|||
|
|||
@api.depends('menu_ids') |
|||
def _compute_access_role_menu_ids(self): |
|||
"""Compute access roles menu id to prevent the menu from hiding.""" |
|||
menu_id = self.env.ref('access_roles.access_role_menu_root').id |
|||
self.access_role_menu_ids = [fields.Command.link(menu_id)] |
|||
|
|||
@api.model |
|||
def get_role_restrictions(self, user_id): |
|||
user = self.env.user |
|||
role_management = user.access_role_id.role_management_id |
|||
if role_management: |
|||
return {'is_debug':role_management.is_debug, 'is_chatter': role_management.is_chatter} |
|||
|
|||
@api.model |
|||
def get_export_restrictions(self, user_id): |
|||
"""Retrieve model-based restrictions for a user. |
|||
This function is used to determine which export, archive, reports, |
|||
and actions should be hidden for the user based on their assigned |
|||
access role. The data is passed to the JavaScript file for frontend |
|||
enforcement. |
|||
:param int user_id: The ID of the user for whom restrictions are being retrieved. |
|||
:return: A list of dictionaries containing model-specific restrictions. |
|||
:rtype: list of dict |
|||
""" |
|||
user = self.env.user |
|||
if not user.access_role_id or not user.access_role_id.role_management_id: |
|||
return [] |
|||
return user.access_role_id.role_management_id.model_access_ids.mapped( |
|||
lambda r: {"model": r.model_id.model, "is_hide_export": r.is_hide_export, |
|||
"is_hide_archive": r.is_hide_archive, |
|||
"report_id": r.hide_report_ids.ids, |
|||
"action_id": r.hide_actions_ids.ids} |
|||
) |
|||
|
|||
@api.model |
|||
def check_model_access_restrictions(self, user_id, model_name): |
|||
""" |
|||
Check if create access and readonly mode are restricted for the user on a specific model. |
|||
:param user_id: ID of the user to check |
|||
:param model_name: Technical name of the model to check |
|||
:return: Dictionary containing is_hide_create and is_model_readonly |
|||
""" |
|||
user = self.env.user |
|||
role_management = user.access_role_id.role_management_id |
|||
access_data = {} |
|||
if role_management: |
|||
model_access = role_management.model_access_ids.filtered( |
|||
lambda r: r.model_id.model == model_name |
|||
) |
|||
if model_access: |
|||
access_data['is_hide_create'] = any(model_access.mapped('is_hide_create')) |
|||
access_data['is_model_readonly'] = any( |
|||
model_access.mapped('is_model_readonly')) |
|||
return access_data |
|||
|
|||
@api.model_create_multi |
|||
def create(self, vals_list): |
|||
"""Override create to automatically link roles with role management records.""" |
|||
records = super(RoleManagement, self).create(vals_list) |
|||
for record in records: |
|||
for role in record.role_ids: |
|||
if not role.role_management_id: |
|||
role.write({'role_management_id': record.id}) |
|||
return records |
|||
|
|||
def action_open_domain_form(self): |
|||
"""Opens the domain form when clicking on domain_id""" |
|||
return { |
|||
'type': 'ir.actions.act_window', |
|||
'name': 'Domain Configuration', |
|||
'res_model': 'domain.model', |
|||
'view_mode': 'form', |
|||
'res_id': self.domain_access_ids.domain_id.id, |
|||
'target': 'new' |
|||
} |
|||
@ -0,0 +1,77 @@ |
|||
# -*- 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 re |
|||
from odoo import api, Command, fields, models |
|||
|
|||
|
|||
class TabRegistry(models.Model): |
|||
"""Class for representing tabs""" |
|||
_name = 'tab.registry' |
|||
_description = 'Tab Registry' |
|||
|
|||
name = fields.Char(string='Tab Name', required=True) |
|||
view_ids = fields.Many2many('ir.ui.view', string='View') |
|||
model_id = fields.Many2one('ir.model', string='Model', ondelete='cascade') |
|||
|
|||
@api.model |
|||
def _register_hook(self): |
|||
"""Triggers filter extraction during module initialization.""" |
|||
super()._register_hook() |
|||
self.get_all_tabs() |
|||
return True |
|||
|
|||
def get_all_tabs(self): |
|||
"""Finds buttons from views and stores them.""" |
|||
views = self.env['ir.ui.view'].search([]) |
|||
tab_model = {} |
|||
# Collect tabs per model, along with the view IDs where they appear. |
|||
for view in views: |
|||
if view.arch: |
|||
model_name = view.model |
|||
if model_name not in tab_model: |
|||
tab_model[model_name] = {} |
|||
found_tabs = re.findall(r'<page[^>]*string="([^"]*)"', view.arch) |
|||
for tab_name in found_tabs: |
|||
if tab_name not in tab_model[model_name]: |
|||
tab_model[model_name][tab_name] = [] |
|||
if view.id not in tab_model[model_name][tab_name]: |
|||
tab_model[model_name][tab_name].append(view.id) |
|||
# Create registry records for each tab. |
|||
for model_name, tabs in tab_model.items(): |
|||
if not model_name or not tabs: |
|||
continue |
|||
model_record = self.env['ir.model'].search([('model', '=', model_name)], |
|||
limit=1) |
|||
if not model_record: |
|||
continue |
|||
for tab_name, view_ids in tabs.items(): |
|||
existing_tab = self.search([ |
|||
('name', '=', tab_name), |
|||
('model_id', '=', model_record.id) |
|||
], limit=1) |
|||
if not existing_tab: |
|||
self.create({ |
|||
'name': tab_name, |
|||
'model_id': model_record.id, |
|||
'view_ids': [Command.link(view) for view in view_ids], |
|||
}) |
|||
return tab_model |
|||
@ -0,0 +1,21 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<odoo> |
|||
<record id="module_access_roles_category" model="ir.module.category"> |
|||
<field name="name">Access Role</field> |
|||
<field name="sequence">10</field> |
|||
</record> |
|||
<record id="access_role_group_user" model="res.groups"> |
|||
<field name="name">Access Role User</field> |
|||
<field name="category_id" ref="module_access_roles_category"/> |
|||
<field name="implied_ids" eval="[(4, ref('base.group_user'))]"/> |
|||
<field name="comment">Access to access roles</field> |
|||
</record> |
|||
<record id="access_role_group_administrator" model="res.groups"> |
|||
<field name="name">Administrator</field> |
|||
<field name="category_id" ref="module_access_roles_category"/> |
|||
<field name="implied_ids" eval="[(4, ref('access_roles.access_role_group_user'))]"/> |
|||
<field name="users" eval="[(4, ref('base.user_root')), (4, ref('base.user_admin'))]"/> |
|||
</record> |
|||
</odoo> |
|||
|
|||
|
|||
|
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 628 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 210 KiB |
|
After Width: | Height: | Size: 209 KiB |
|
After Width: | Height: | Size: 109 KiB |
|
After Width: | Height: | Size: 495 B |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 624 B |
|
After Width: | Height: | Size: 136 KiB |
|
After Width: | Height: | Size: 214 KiB |
|
After Width: | Height: | Size: 841 B |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 310 B |
|
After Width: | Height: | Size: 929 B |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 8.4 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 542 B |
|
After Width: | Height: | Size: 576 B |
|
After Width: | Height: | Size: 733 B |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 738 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 911 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 600 B |
|
After Width: | Height: | Size: 673 B |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 462 B |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 926 B |
|
After Width: | Height: | Size: 9.0 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 878 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 653 B |
|
After Width: | Height: | Size: 800 B |
|
After Width: | Height: | Size: 905 B |
|
After Width: | Height: | Size: 189 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 839 B |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 5.9 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 427 B |
|
After Width: | Height: | Size: 627 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 988 B |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 875 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 619 B |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 4.4 KiB |