@ -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 |