diff --git a/access_roles/README.md b/access_roles/README.md new file mode 100644 index 000000000..5813b2748 --- /dev/null +++ b/access_roles/README.md @@ -0,0 +1,138 @@ +# Odoo Access Role + +[![Odoo](https://img.shields.io/badge/Odoo-%23A24689.svg?style=for-the-badge&logo=Odoo&logoColor=white)](https://www.odoo.com) +[![License](https://img.shields.io/badge/License-MIT-green.svg?style=for-the-badge)](https://opensource.org/licenses/MIT) +[![GitHub Stars](https://img.shields.io/github/stars/cybrosystech/Odoo-Access-Role?style=for-the-badge)](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 + +
+ + + Access Role1 + + +
+ +### 2. Sales module + +
+ + + Role1 + + +
+
+ + + Role2 + + +
+
+ + + Role3 + + +
+
+ + + Role4 + + +
+
+ + + Role5 + + +
+
+ + + Role6 + + +
+
+ + + Role7 + + +
+
+ + + Role8 + + +
+ + +## 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. \ No newline at end of file diff --git a/access_roles/__init__.py b/access_roles/__init__.py new file mode 100644 index 000000000..94dda1816 --- /dev/null +++ b/access_roles/__init__.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +############################################################################# +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2025-TODAY Cybrosys Technologies(). +# Author: Cybrosys Techno Solutions() +# +# 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 . +# +############################################################################# +from . import models diff --git a/access_roles/__manifest__.py b/access_roles/__manifest__.py new file mode 100644 index 000000000..4a2fb0fa0 --- /dev/null +++ b/access_roles/__manifest__.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +############################################################################# +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2025-TODAY Cybrosys Technologies(). +# Author: Cybrosys Techno Solutions() +# +# 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 . +# +############################################################################# +{ + '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, +} + diff --git a/access_roles/doc/RELEASE_NOTES.md b/access_roles/doc/RELEASE_NOTES.md new file mode 100644 index 000000000..ca6dbd466 --- /dev/null +++ b/access_roles/doc/RELEASE_NOTES.md @@ -0,0 +1,6 @@ +## Module + +#### 21.07.2025 +#### Version 18.0.1.0.0 +##### ADD +- Initial commit for Access Roles. diff --git a/access_roles/models/__init__.py b/access_roles/models/__init__.py new file mode 100644 index 000000000..fb27ffffc --- /dev/null +++ b/access_roles/models/__init__.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +############################################################################# +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2025-TODAY Cybrosys Technologies(). +# Author: Cybrosys Techno Solutions() +# +# 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 . +# +############################################################################# +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 diff --git a/access_roles/models/access_role.py b/access_roles/models/access_role.py new file mode 100644 index 000000000..ba3c50545 --- /dev/null +++ b/access_roles/models/access_role.py @@ -0,0 +1,388 @@ +# -*- coding: utf-8 -*- +############################################################################# +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2025-TODAY Cybrosys Technologies(). +# Author: Cybrosys Techno Solutions() +# +# 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 . +# +############################################################################# +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 diff --git a/access_roles/models/button_registry.py b/access_roles/models/button_registry.py new file mode 100644 index 000000000..cc76752a6 --- /dev/null +++ b/access_roles/models/button_registry.py @@ -0,0 +1,98 @@ +# -*- coding: utf-8 -*- +############################################################################# +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2025-TODAY Cybrosys Technologies(). +# Author: Cybrosys Techno Solutions() +# +# 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 . +# +############################################################################# +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']*)>' + 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 diff --git a/access_roles/models/domain_model.py b/access_roles/models/domain_model.py new file mode 100644 index 000000000..37e293808 --- /dev/null +++ b/access_roles/models/domain_model.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +############################################################################# +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2025-TODAY Cybrosys Technologies(). +# Author: Cybrosys Techno Solutions() +# +# 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 . +# +############################################################################# +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 diff --git a/access_roles/models/field_access.py b/access_roles/models/field_access.py new file mode 100644 index 000000000..75a58def8 --- /dev/null +++ b/access_roles/models/field_access.py @@ -0,0 +1,73 @@ +# -*- coding: utf-8 -*- +############################################################################# +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2025-TODAY Cybrosys Technologies(). +# Author: Cybrosys Techno Solutions() +# +# 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 . +# +############################################################################# +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 diff --git a/access_roles/models/filter_registry.py b/access_roles/models/filter_registry.py new file mode 100644 index 000000000..e715f375d --- /dev/null +++ b/access_roles/models/filter_registry.py @@ -0,0 +1,129 @@ +# -*- coding: utf-8 -*- +############################################################################# +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2025-TODAY Cybrosys Technologies(). +# Author: Cybrosys Techno Solutions() +# +# 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 . +# +############################################################################# +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 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) diff --git a/access_roles/models/groupby_registry.py b/access_roles/models/groupby_registry.py new file mode 100644 index 000000000..9445318d4 --- /dev/null +++ b/access_roles/models/groupby_registry.py @@ -0,0 +1,122 @@ +# -*- coding: utf-8 -*- +############################################################################# +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2025-TODAY Cybrosys Technologies(). +# Author: Cybrosys Techno Solutions() +# +# 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 . +# +############################################################################# +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']+?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) diff --git a/access_roles/models/ir_rule.py b/access_roles/models/ir_rule.py new file mode 100644 index 000000000..4194d8cb0 --- /dev/null +++ b/access_roles/models/ir_rule.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +############################################################################# +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2025-TODAY Cybrosys Technologies(). +# Author: Cybrosys Techno Solutions() +# +# 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 . +# +############################################################################# +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 diff --git a/access_roles/models/ir_ui_menu.py b/access_roles/models/ir_ui_menu.py new file mode 100644 index 000000000..9da4ad896 --- /dev/null +++ b/access_roles/models/ir_ui_menu.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +############################################################################# +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2025-TODAY Cybrosys Technologies(). +# Author: Cybrosys Techno Solutions() +# +# 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 . +# +############################################################################# +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) diff --git a/access_roles/models/ir_ui_view.py b/access_roles/models/ir_ui_view.py new file mode 100644 index 000000000..6f16525b4 --- /dev/null +++ b/access_roles/models/ir_ui_view.py @@ -0,0 +1,173 @@ +# -*- coding: utf-8 -*- +############################################################################# +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2025-TODAY Cybrosys Technologies(). +# Author: Cybrosys Techno Solutions() +# +# 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 . +# +############################################################################# +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 diff --git a/access_roles/models/res_groups.py b/access_roles/models/res_groups.py new file mode 100644 index 000000000..ea670c23b --- /dev/null +++ b/access_roles/models/res_groups.py @@ -0,0 +1,202 @@ +# -*- coding: utf-8 -*- +############################################################################# +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2025-TODAY Cybrosys Technologies(). +# Author: Cybrosys Techno Solutions() +# +# 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 . +# +############################################################################# +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)]) diff --git a/access_roles/models/res_users.py b/access_roles/models/res_users.py new file mode 100644 index 000000000..b5a73101d --- /dev/null +++ b/access_roles/models/res_users.py @@ -0,0 +1,93 @@ +# -*- coding: utf-8 -*- +############################################################################# +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2025-TODAY Cybrosys Technologies(). +# Author: Cybrosys Techno Solutions() +# +# 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 . +# +############################################################################# +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 diff --git a/access_roles/models/role_management.py b/access_roles/models/role_management.py new file mode 100644 index 000000000..7608dc611 --- /dev/null +++ b/access_roles/models/role_management.py @@ -0,0 +1,138 @@ +# -*- coding: utf-8 -*- +############################################################################# +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2025-TODAY Cybrosys Technologies(). +# Author: Cybrosys Techno Solutions() +# +# 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 . +# +############################################################################# +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' + } \ No newline at end of file diff --git a/access_roles/models/tab_registry.py b/access_roles/models/tab_registry.py new file mode 100644 index 000000000..ed8e423ce --- /dev/null +++ b/access_roles/models/tab_registry.py @@ -0,0 +1,77 @@ +# -*- coding: utf-8 -*- +############################################################################# +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2025-TODAY Cybrosys Technologies(). +# Author: Cybrosys Techno Solutions() +# +# 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 . +# +############################################################################# +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']*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 diff --git a/access_roles/security/access_roles_security.xml b/access_roles/security/access_roles_security.xml new file mode 100644 index 000000000..17b5d727b --- /dev/null +++ b/access_roles/security/access_roles_security.xml @@ -0,0 +1,21 @@ + + + + Access Role + 10 + + + Access Role User + + + Access to access roles + + + Administrator + + + + + + + diff --git a/access_roles/security/ir.model.access.csv b/access_roles/security/ir.model.access.csv new file mode 100644 index 000000000..ebf3517cd --- /dev/null +++ b/access_roles/security/ir.model.access.csv @@ -0,0 +1,9 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_access_role,access.access.role,model_access_role,base.group_user,1,1,1,1 +access_field_access,access.field.access,model_field_access,base.group_user,1,1,1,1 +access_role_management,access.role.management,model_role_management,base.group_user,1,1,1,1 +access_button_registry,access.button.registry,model_button_registry,base.group_user,1,1,1,1 +access_tab_registry,access.tab.registry,model_tab_registry,base.group_user,1,1,1,1 +access_filter_registry,access.filter.registry,model_filter_registry,base.group_user,1,1,1,1 +access_groupby_registry,access.groupby.registry,model_groupby_registry,base.group_user,1,1,1,1 +access_domain_model,access.domain.model,model_domain_model,base.group_user,1,1,1,1 diff --git a/access_roles/static/description/assets/cybro-icon.png b/access_roles/static/description/assets/cybro-icon.png new file mode 100644 index 000000000..06e73e11d Binary files /dev/null and b/access_roles/static/description/assets/cybro-icon.png differ diff --git a/access_roles/static/description/assets/cybro-odoo.png b/access_roles/static/description/assets/cybro-odoo.png new file mode 100644 index 000000000..ed02e07a4 Binary files /dev/null and b/access_roles/static/description/assets/cybro-odoo.png differ diff --git a/access_roles/static/description/assets/h2.png b/access_roles/static/description/assets/h2.png new file mode 100644 index 000000000..0bfc4707d Binary files /dev/null and b/access_roles/static/description/assets/h2.png differ diff --git a/access_roles/static/description/assets/icons/arrows-repeat.svg b/access_roles/static/description/assets/icons/arrows-repeat.svg new file mode 100644 index 000000000..1d7efabc5 --- /dev/null +++ b/access_roles/static/description/assets/icons/arrows-repeat.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/access_roles/static/description/assets/icons/banner-1.png b/access_roles/static/description/assets/icons/banner-1.png new file mode 100644 index 000000000..c180db172 Binary files /dev/null and b/access_roles/static/description/assets/icons/banner-1.png differ diff --git a/access_roles/static/description/assets/icons/banner-2.svg b/access_roles/static/description/assets/icons/banner-2.svg new file mode 100644 index 000000000..e606d97d9 --- /dev/null +++ b/access_roles/static/description/assets/icons/banner-2.svg @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/access_roles/static/description/assets/icons/banner-bg.png b/access_roles/static/description/assets/icons/banner-bg.png new file mode 100644 index 000000000..a8238d3c0 Binary files /dev/null and b/access_roles/static/description/assets/icons/banner-bg.png differ diff --git a/access_roles/static/description/assets/icons/banner-bg.svg b/access_roles/static/description/assets/icons/banner-bg.svg new file mode 100644 index 000000000..b1378103e --- /dev/null +++ b/access_roles/static/description/assets/icons/banner-bg.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/access_roles/static/description/assets/icons/banner-call.svg b/access_roles/static/description/assets/icons/banner-call.svg new file mode 100644 index 000000000..96c687e81 --- /dev/null +++ b/access_roles/static/description/assets/icons/banner-call.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/access_roles/static/description/assets/icons/banner-mail.svg b/access_roles/static/description/assets/icons/banner-mail.svg new file mode 100644 index 000000000..cbf0d158d --- /dev/null +++ b/access_roles/static/description/assets/icons/banner-mail.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/access_roles/static/description/assets/icons/banner-pattern.svg b/access_roles/static/description/assets/icons/banner-pattern.svg new file mode 100644 index 000000000..9c1c7e101 --- /dev/null +++ b/access_roles/static/description/assets/icons/banner-pattern.svg @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/access_roles/static/description/assets/icons/banner-promo.svg b/access_roles/static/description/assets/icons/banner-promo.svg new file mode 100644 index 000000000..d52791b11 --- /dev/null +++ b/access_roles/static/description/assets/icons/banner-promo.svg @@ -0,0 +1,147 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/access_roles/static/description/assets/icons/blog-icon.png b/access_roles/static/description/assets/icons/blog-icon.png new file mode 100644 index 000000000..ba4c7c366 Binary files /dev/null and b/access_roles/static/description/assets/icons/blog-icon.png differ diff --git a/access_roles/static/description/assets/icons/brand-pair.svg b/access_roles/static/description/assets/icons/brand-pair.svg new file mode 100644 index 000000000..d8db7fc1e --- /dev/null +++ b/access_roles/static/description/assets/icons/brand-pair.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/access_roles/static/description/assets/icons/check.png b/access_roles/static/description/assets/icons/check.png new file mode 100644 index 000000000..c8e85f51d Binary files /dev/null and b/access_roles/static/description/assets/icons/check.png differ diff --git a/access_roles/static/description/assets/icons/chevron.png b/access_roles/static/description/assets/icons/chevron.png new file mode 100644 index 000000000..2089293d6 Binary files /dev/null and b/access_roles/static/description/assets/icons/chevron.png differ diff --git a/access_roles/static/description/assets/icons/close-icon.svg b/access_roles/static/description/assets/icons/close-icon.svg new file mode 100644 index 000000000..df8cce37a --- /dev/null +++ b/access_roles/static/description/assets/icons/close-icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/access_roles/static/description/assets/icons/cogs.png b/access_roles/static/description/assets/icons/cogs.png new file mode 100644 index 000000000..95d0bad62 Binary files /dev/null and b/access_roles/static/description/assets/icons/cogs.png differ diff --git a/access_roles/static/description/assets/icons/collabarate-icon.svg b/access_roles/static/description/assets/icons/collabarate-icon.svg new file mode 100644 index 000000000..dd4e10518 --- /dev/null +++ b/access_roles/static/description/assets/icons/collabarate-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/access_roles/static/description/assets/icons/consultation.png b/access_roles/static/description/assets/icons/consultation.png new file mode 100644 index 000000000..8319d4baa Binary files /dev/null and b/access_roles/static/description/assets/icons/consultation.png differ diff --git a/access_roles/static/description/assets/icons/copylink.svg b/access_roles/static/description/assets/icons/copylink.svg new file mode 100644 index 000000000..3b67f60e0 --- /dev/null +++ b/access_roles/static/description/assets/icons/copylink.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/access_roles/static/description/assets/icons/cybro-logo.png b/access_roles/static/description/assets/icons/cybro-logo.png new file mode 100644 index 000000000..ff4b78220 Binary files /dev/null and b/access_roles/static/description/assets/icons/cybro-logo.png differ diff --git a/access_roles/static/description/assets/icons/down.svg b/access_roles/static/description/assets/icons/down.svg new file mode 100644 index 000000000..f21c36271 --- /dev/null +++ b/access_roles/static/description/assets/icons/down.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/access_roles/static/description/assets/icons/ecom-black.png b/access_roles/static/description/assets/icons/ecom-black.png new file mode 100644 index 000000000..a9385ff13 Binary files /dev/null and b/access_roles/static/description/assets/icons/ecom-black.png differ diff --git a/access_roles/static/description/assets/icons/education-black.png b/access_roles/static/description/assets/icons/education-black.png new file mode 100644 index 000000000..3eb09b27b Binary files /dev/null and b/access_roles/static/description/assets/icons/education-black.png differ diff --git a/access_roles/static/description/assets/icons/faq.png b/access_roles/static/description/assets/icons/faq.png new file mode 100644 index 000000000..4250b5b81 Binary files /dev/null and b/access_roles/static/description/assets/icons/faq.png differ diff --git a/access_roles/static/description/assets/icons/feature-icon.svg b/access_roles/static/description/assets/icons/feature-icon.svg new file mode 100644 index 000000000..fa0ea6850 --- /dev/null +++ b/access_roles/static/description/assets/icons/feature-icon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/access_roles/static/description/assets/icons/feature.png b/access_roles/static/description/assets/icons/feature.png new file mode 100644 index 000000000..ac7a785c0 Binary files /dev/null and b/access_roles/static/description/assets/icons/feature.png differ diff --git a/access_roles/static/description/assets/icons/gear.svg b/access_roles/static/description/assets/icons/gear.svg new file mode 100644 index 000000000..0cc66b6ea --- /dev/null +++ b/access_roles/static/description/assets/icons/gear.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/access_roles/static/description/assets/icons/hero.gif b/access_roles/static/description/assets/icons/hero.gif new file mode 100644 index 000000000..380654dfe Binary files /dev/null and b/access_roles/static/description/assets/icons/hero.gif differ diff --git a/access_roles/static/description/assets/icons/hire-odoo.svg b/access_roles/static/description/assets/icons/hire-odoo.svg new file mode 100644 index 000000000..e1ac089b0 --- /dev/null +++ b/access_roles/static/description/assets/icons/hire-odoo.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/access_roles/static/description/assets/icons/hotel-black.png b/access_roles/static/description/assets/icons/hotel-black.png new file mode 100644 index 000000000..130f613be Binary files /dev/null and b/access_roles/static/description/assets/icons/hotel-black.png differ diff --git a/access_roles/static/description/assets/icons/license.png b/access_roles/static/description/assets/icons/license.png new file mode 100644 index 000000000..a5869797e Binary files /dev/null and b/access_roles/static/description/assets/icons/license.png differ diff --git a/access_roles/static/description/assets/icons/life-ring-icon.svg b/access_roles/static/description/assets/icons/life-ring-icon.svg new file mode 100644 index 000000000..3ae6e1d89 --- /dev/null +++ b/access_roles/static/description/assets/icons/life-ring-icon.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/access_roles/static/description/assets/icons/lifebuoy.png b/access_roles/static/description/assets/icons/lifebuoy.png new file mode 100644 index 000000000..658d56ccc Binary files /dev/null and b/access_roles/static/description/assets/icons/lifebuoy.png differ diff --git a/access_roles/static/description/assets/icons/logo.png b/access_roles/static/description/assets/icons/logo.png new file mode 100644 index 000000000..478462d3e Binary files /dev/null and b/access_roles/static/description/assets/icons/logo.png differ diff --git a/access_roles/static/description/assets/icons/mail.svg b/access_roles/static/description/assets/icons/mail.svg new file mode 100644 index 000000000..1eedde695 --- /dev/null +++ b/access_roles/static/description/assets/icons/mail.svg @@ -0,0 +1,3 @@ + + + diff --git a/access_roles/static/description/assets/icons/manufacturing-black.png b/access_roles/static/description/assets/icons/manufacturing-black.png new file mode 100644 index 000000000..697eb0e9f Binary files /dev/null and b/access_roles/static/description/assets/icons/manufacturing-black.png differ diff --git a/access_roles/static/description/assets/icons/notes.png b/access_roles/static/description/assets/icons/notes.png new file mode 100644 index 000000000..ee5e95404 Binary files /dev/null and b/access_roles/static/description/assets/icons/notes.png differ diff --git a/access_roles/static/description/assets/icons/notification icon.svg b/access_roles/static/description/assets/icons/notification icon.svg new file mode 100644 index 000000000..053189973 --- /dev/null +++ b/access_roles/static/description/assets/icons/notification icon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/access_roles/static/description/assets/icons/odoo-consultancy.svg b/access_roles/static/description/assets/icons/odoo-consultancy.svg new file mode 100644 index 000000000..e05f65bde --- /dev/null +++ b/access_roles/static/description/assets/icons/odoo-consultancy.svg @@ -0,0 +1,4 @@ + + + + diff --git a/access_roles/static/description/assets/icons/odoo-licencing.svg b/access_roles/static/description/assets/icons/odoo-licencing.svg new file mode 100644 index 000000000..2606c88b0 --- /dev/null +++ b/access_roles/static/description/assets/icons/odoo-licencing.svg @@ -0,0 +1,3 @@ + + + diff --git a/access_roles/static/description/assets/icons/odoo-logo.png b/access_roles/static/description/assets/icons/odoo-logo.png new file mode 100644 index 000000000..0e4d0eb5a Binary files /dev/null and b/access_roles/static/description/assets/icons/odoo-logo.png differ diff --git a/access_roles/static/description/assets/icons/patter.svg b/access_roles/static/description/assets/icons/patter.svg new file mode 100644 index 000000000..25c9c0a8f --- /dev/null +++ b/access_roles/static/description/assets/icons/patter.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/access_roles/static/description/assets/icons/pattern1.png b/access_roles/static/description/assets/icons/pattern1.png new file mode 100644 index 000000000..09ab0fb2d Binary files /dev/null and b/access_roles/static/description/assets/icons/pattern1.png differ diff --git a/access_roles/static/description/assets/icons/pos-black.png b/access_roles/static/description/assets/icons/pos-black.png new file mode 100644 index 000000000..97c0f90c1 Binary files /dev/null and b/access_roles/static/description/assets/icons/pos-black.png differ diff --git a/access_roles/static/description/assets/icons/puzzle-piece-icon.svg b/access_roles/static/description/assets/icons/puzzle-piece-icon.svg new file mode 100644 index 000000000..3e9ad9373 --- /dev/null +++ b/access_roles/static/description/assets/icons/puzzle-piece-icon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/access_roles/static/description/assets/icons/puzzle.png b/access_roles/static/description/assets/icons/puzzle.png new file mode 100644 index 000000000..65cf854e7 Binary files /dev/null and b/access_roles/static/description/assets/icons/puzzle.png differ diff --git a/access_roles/static/description/assets/icons/replace-icon.svg b/access_roles/static/description/assets/icons/replace-icon.svg new file mode 100644 index 000000000..d0e3a7af1 --- /dev/null +++ b/access_roles/static/description/assets/icons/replace-icon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/access_roles/static/description/assets/icons/restaurant-black.png b/access_roles/static/description/assets/icons/restaurant-black.png new file mode 100644 index 000000000..4a35eb939 Binary files /dev/null and b/access_roles/static/description/assets/icons/restaurant-black.png differ diff --git a/access_roles/static/description/assets/icons/screenshot-main.png b/access_roles/static/description/assets/icons/screenshot-main.png new file mode 100644 index 000000000..575f8e676 Binary files /dev/null and b/access_roles/static/description/assets/icons/screenshot-main.png differ diff --git a/access_roles/static/description/assets/icons/screenshot.png b/access_roles/static/description/assets/icons/screenshot.png new file mode 100644 index 000000000..cef272529 Binary files /dev/null and b/access_roles/static/description/assets/icons/screenshot.png differ diff --git a/access_roles/static/description/assets/icons/service-black.png b/access_roles/static/description/assets/icons/service-black.png new file mode 100644 index 000000000..301ab51cb Binary files /dev/null and b/access_roles/static/description/assets/icons/service-black.png differ diff --git a/access_roles/static/description/assets/icons/skype-fill.svg b/access_roles/static/description/assets/icons/skype-fill.svg new file mode 100644 index 000000000..c17423639 --- /dev/null +++ b/access_roles/static/description/assets/icons/skype-fill.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/access_roles/static/description/assets/icons/skype.png b/access_roles/static/description/assets/icons/skype.png new file mode 100644 index 000000000..51b409fb3 Binary files /dev/null and b/access_roles/static/description/assets/icons/skype.png differ diff --git a/access_roles/static/description/assets/icons/skype.svg b/access_roles/static/description/assets/icons/skype.svg new file mode 100644 index 000000000..df3dad39b --- /dev/null +++ b/access_roles/static/description/assets/icons/skype.svg @@ -0,0 +1,3 @@ + + + diff --git a/access_roles/static/description/assets/icons/star-1.svg b/access_roles/static/description/assets/icons/star-1.svg new file mode 100644 index 000000000..7e55ab162 --- /dev/null +++ b/access_roles/static/description/assets/icons/star-1.svg @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/access_roles/static/description/assets/icons/star-2.svg b/access_roles/static/description/assets/icons/star-2.svg new file mode 100644 index 000000000..5ae9f507a --- /dev/null +++ b/access_roles/static/description/assets/icons/star-2.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/access_roles/static/description/assets/icons/support.png b/access_roles/static/description/assets/icons/support.png new file mode 100644 index 000000000..4f18b8b82 Binary files /dev/null and b/access_roles/static/description/assets/icons/support.png differ diff --git a/access_roles/static/description/assets/icons/test-1 - Copy.png b/access_roles/static/description/assets/icons/test-1 - Copy.png new file mode 100644 index 000000000..f6a902663 Binary files /dev/null and b/access_roles/static/description/assets/icons/test-1 - Copy.png differ diff --git a/access_roles/static/description/assets/icons/test-1.png b/access_roles/static/description/assets/icons/test-1.png new file mode 100644 index 000000000..0908add2b Binary files /dev/null and b/access_roles/static/description/assets/icons/test-1.png differ diff --git a/access_roles/static/description/assets/icons/test-2.png b/access_roles/static/description/assets/icons/test-2.png new file mode 100644 index 000000000..4671fe91e Binary files /dev/null and b/access_roles/static/description/assets/icons/test-2.png differ diff --git a/access_roles/static/description/assets/icons/tms.png b/access_roles/static/description/assets/icons/tms.png new file mode 100644 index 000000000..f87428ec5 Binary files /dev/null and b/access_roles/static/description/assets/icons/tms.png differ diff --git a/access_roles/static/description/assets/icons/trading-black.png b/access_roles/static/description/assets/icons/trading-black.png new file mode 100644 index 000000000..9398ba2f1 Binary files /dev/null and b/access_roles/static/description/assets/icons/trading-black.png differ diff --git a/access_roles/static/description/assets/icons/training.png b/access_roles/static/description/assets/icons/training.png new file mode 100644 index 000000000..884ca024d Binary files /dev/null and b/access_roles/static/description/assets/icons/training.png differ diff --git a/access_roles/static/description/assets/icons/translate.svg b/access_roles/static/description/assets/icons/translate.svg new file mode 100644 index 000000000..af9c8a1aa --- /dev/null +++ b/access_roles/static/description/assets/icons/translate.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/access_roles/static/description/assets/icons/update.png b/access_roles/static/description/assets/icons/update.png new file mode 100644 index 000000000..ecbc5a01a Binary files /dev/null and b/access_roles/static/description/assets/icons/update.png differ diff --git a/access_roles/static/description/assets/icons/user.png b/access_roles/static/description/assets/icons/user.png new file mode 100644 index 000000000..6ffb23d9f Binary files /dev/null and b/access_roles/static/description/assets/icons/user.png differ diff --git a/access_roles/static/description/assets/icons/video.png b/access_roles/static/description/assets/icons/video.png new file mode 100644 index 000000000..576705b17 Binary files /dev/null and b/access_roles/static/description/assets/icons/video.png differ diff --git a/access_roles/static/description/assets/icons/whatsapp.png b/access_roles/static/description/assets/icons/whatsapp.png new file mode 100644 index 000000000..d513a5356 Binary files /dev/null and b/access_roles/static/description/assets/icons/whatsapp.png differ diff --git a/access_roles/static/description/assets/icons/whatsapp.svg b/access_roles/static/description/assets/icons/whatsapp.svg new file mode 100644 index 000000000..bba9ca395 --- /dev/null +++ b/access_roles/static/description/assets/icons/whatsapp.svg @@ -0,0 +1,14 @@ + + + + + + + + + + \ No newline at end of file diff --git a/access_roles/static/description/assets/icons/wrench-icon.svg b/access_roles/static/description/assets/icons/wrench-icon.svg new file mode 100644 index 000000000..174b5a465 --- /dev/null +++ b/access_roles/static/description/assets/icons/wrench-icon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/access_roles/static/description/assets/icons/wrench.png b/access_roles/static/description/assets/icons/wrench.png new file mode 100644 index 000000000..6c04dea0f Binary files /dev/null and b/access_roles/static/description/assets/icons/wrench.png differ diff --git a/access_roles/static/description/assets/icons/youtube-icon.png b/access_roles/static/description/assets/icons/youtube-icon.png new file mode 100644 index 000000000..f206560dc Binary files /dev/null and b/access_roles/static/description/assets/icons/youtube-icon.png differ diff --git a/access_roles/static/description/assets/misc/categories.png b/access_roles/static/description/assets/misc/categories.png new file mode 100644 index 000000000..bedf1e0b1 Binary files /dev/null and b/access_roles/static/description/assets/misc/categories.png differ diff --git a/access_roles/static/description/assets/misc/check-box.png b/access_roles/static/description/assets/misc/check-box.png new file mode 100644 index 000000000..42caf24b9 Binary files /dev/null and b/access_roles/static/description/assets/misc/check-box.png differ diff --git a/access_roles/static/description/assets/misc/compass.png b/access_roles/static/description/assets/misc/compass.png new file mode 100644 index 000000000..d5fed8faa Binary files /dev/null and b/access_roles/static/description/assets/misc/compass.png differ diff --git a/access_roles/static/description/assets/misc/corporate.png b/access_roles/static/description/assets/misc/corporate.png new file mode 100644 index 000000000..2eb13edbf Binary files /dev/null and b/access_roles/static/description/assets/misc/corporate.png differ diff --git a/access_roles/static/description/assets/misc/customer-support.png b/access_roles/static/description/assets/misc/customer-support.png new file mode 100644 index 000000000..79efc72ed Binary files /dev/null and b/access_roles/static/description/assets/misc/customer-support.png differ diff --git a/access_roles/static/description/assets/misc/cybrosys-logo.png b/access_roles/static/description/assets/misc/cybrosys-logo.png new file mode 100644 index 000000000..cc3cc0ccf Binary files /dev/null and b/access_roles/static/description/assets/misc/cybrosys-logo.png differ diff --git a/access_roles/static/description/assets/misc/features.png b/access_roles/static/description/assets/misc/features.png new file mode 100644 index 000000000..b41769f77 Binary files /dev/null and b/access_roles/static/description/assets/misc/features.png differ diff --git a/access_roles/static/description/assets/misc/logo.png b/access_roles/static/description/assets/misc/logo.png new file mode 100644 index 000000000..478462d3e Binary files /dev/null and b/access_roles/static/description/assets/misc/logo.png differ diff --git a/access_roles/static/description/assets/misc/pictures.png b/access_roles/static/description/assets/misc/pictures.png new file mode 100644 index 000000000..56d255fe9 Binary files /dev/null and b/access_roles/static/description/assets/misc/pictures.png differ diff --git a/access_roles/static/description/assets/misc/pie-chart.png b/access_roles/static/description/assets/misc/pie-chart.png new file mode 100644 index 000000000..426e05244 Binary files /dev/null and b/access_roles/static/description/assets/misc/pie-chart.png differ diff --git a/access_roles/static/description/assets/misc/right-arrow.png b/access_roles/static/description/assets/misc/right-arrow.png new file mode 100644 index 000000000..730984a06 Binary files /dev/null and b/access_roles/static/description/assets/misc/right-arrow.png differ diff --git a/access_roles/static/description/assets/misc/star.png b/access_roles/static/description/assets/misc/star.png new file mode 100644 index 000000000..2eb9ab29f Binary files /dev/null and b/access_roles/static/description/assets/misc/star.png differ diff --git a/access_roles/static/description/assets/misc/support.png b/access_roles/static/description/assets/misc/support.png new file mode 100644 index 000000000..4f18b8b82 Binary files /dev/null and b/access_roles/static/description/assets/misc/support.png differ diff --git a/access_roles/static/description/assets/misc/whatsapp.png b/access_roles/static/description/assets/misc/whatsapp.png new file mode 100644 index 000000000..d513a5356 Binary files /dev/null and b/access_roles/static/description/assets/misc/whatsapp.png differ diff --git a/access_roles/static/description/assets/modules/1.png b/access_roles/static/description/assets/modules/1.png new file mode 100644 index 000000000..e0b09a5a0 Binary files /dev/null and b/access_roles/static/description/assets/modules/1.png differ diff --git a/access_roles/static/description/assets/modules/2.png b/access_roles/static/description/assets/modules/2.png new file mode 100644 index 000000000..ecea68d98 Binary files /dev/null and b/access_roles/static/description/assets/modules/2.png differ diff --git a/access_roles/static/description/assets/modules/3.jpg b/access_roles/static/description/assets/modules/3.jpg new file mode 100644 index 000000000..a83c58aac Binary files /dev/null and b/access_roles/static/description/assets/modules/3.jpg differ diff --git a/access_roles/static/description/assets/modules/4.jpg b/access_roles/static/description/assets/modules/4.jpg new file mode 100644 index 000000000..31a56b08c Binary files /dev/null and b/access_roles/static/description/assets/modules/4.jpg differ diff --git a/access_roles/static/description/assets/modules/5.jpg b/access_roles/static/description/assets/modules/5.jpg new file mode 100644 index 000000000..3b40aa4e4 Binary files /dev/null and b/access_roles/static/description/assets/modules/5.jpg differ diff --git a/access_roles/static/description/assets/modules/6.jpg b/access_roles/static/description/assets/modules/6.jpg new file mode 100644 index 000000000..5d071f8ae Binary files /dev/null and b/access_roles/static/description/assets/modules/6.jpg differ diff --git a/access_roles/static/description/assets/modules/budget_image.png b/access_roles/static/description/assets/modules/budget_image.png new file mode 100644 index 000000000..b50130c7d Binary files /dev/null and b/access_roles/static/description/assets/modules/budget_image.png differ diff --git a/access_roles/static/description/assets/modules/credit_image.png b/access_roles/static/description/assets/modules/credit_image.png new file mode 100644 index 000000000..3ad04ecfd Binary files /dev/null and b/access_roles/static/description/assets/modules/credit_image.png differ diff --git a/access_roles/static/description/assets/modules/employee_image.png b/access_roles/static/description/assets/modules/employee_image.png new file mode 100644 index 000000000..30ad58232 Binary files /dev/null and b/access_roles/static/description/assets/modules/employee_image.png differ diff --git a/access_roles/static/description/assets/modules/export_image.png b/access_roles/static/description/assets/modules/export_image.png new file mode 100644 index 000000000..492980ad0 Binary files /dev/null and b/access_roles/static/description/assets/modules/export_image.png differ diff --git a/access_roles/static/description/assets/modules/gantt_image.png b/access_roles/static/description/assets/modules/gantt_image.png new file mode 100644 index 000000000..1ae7cfe3b Binary files /dev/null and b/access_roles/static/description/assets/modules/gantt_image.png differ diff --git a/access_roles/static/description/assets/modules/quotation_image.png b/access_roles/static/description/assets/modules/quotation_image.png new file mode 100644 index 000000000..499b1a72f Binary files /dev/null and b/access_roles/static/description/assets/modules/quotation_image.png differ diff --git a/access_roles/static/description/assets/screenshots/access_roles-01.png b/access_roles/static/description/assets/screenshots/access_roles-01.png new file mode 100644 index 000000000..20ad75f15 Binary files /dev/null and b/access_roles/static/description/assets/screenshots/access_roles-01.png differ diff --git a/access_roles/static/description/assets/screenshots/access_roles-02.png b/access_roles/static/description/assets/screenshots/access_roles-02.png new file mode 100644 index 000000000..fcb6db3bc Binary files /dev/null and b/access_roles/static/description/assets/screenshots/access_roles-02.png differ diff --git a/access_roles/static/description/assets/screenshots/access_roles-03.png b/access_roles/static/description/assets/screenshots/access_roles-03.png new file mode 100644 index 000000000..6805706f6 Binary files /dev/null and b/access_roles/static/description/assets/screenshots/access_roles-03.png differ diff --git a/access_roles/static/description/assets/screenshots/access_roles-04.png b/access_roles/static/description/assets/screenshots/access_roles-04.png new file mode 100644 index 000000000..35eb689ec Binary files /dev/null and b/access_roles/static/description/assets/screenshots/access_roles-04.png differ diff --git a/access_roles/static/description/assets/screenshots/access_roles-05.png b/access_roles/static/description/assets/screenshots/access_roles-05.png new file mode 100644 index 000000000..21911aca2 Binary files /dev/null and b/access_roles/static/description/assets/screenshots/access_roles-05.png differ diff --git a/access_roles/static/description/assets/screenshots/access_roles-06.png b/access_roles/static/description/assets/screenshots/access_roles-06.png new file mode 100644 index 000000000..7d8952a63 Binary files /dev/null and b/access_roles/static/description/assets/screenshots/access_roles-06.png differ diff --git a/access_roles/static/description/assets/screenshots/access_roles-07.png b/access_roles/static/description/assets/screenshots/access_roles-07.png new file mode 100644 index 000000000..4b94cb3f0 Binary files /dev/null and b/access_roles/static/description/assets/screenshots/access_roles-07.png differ diff --git a/access_roles/static/description/assets/screenshots/access_roles-08.png b/access_roles/static/description/assets/screenshots/access_roles-08.png new file mode 100644 index 000000000..36315cbad Binary files /dev/null and b/access_roles/static/description/assets/screenshots/access_roles-08.png differ diff --git a/access_roles/static/description/assets/screenshots/access_roles-09.png b/access_roles/static/description/assets/screenshots/access_roles-09.png new file mode 100644 index 000000000..df14e80f0 Binary files /dev/null and b/access_roles/static/description/assets/screenshots/access_roles-09.png differ diff --git a/access_roles/static/description/assets/screenshots/hero.gif b/access_roles/static/description/assets/screenshots/hero.gif new file mode 100644 index 000000000..948c0e472 Binary files /dev/null and b/access_roles/static/description/assets/screenshots/hero.gif differ diff --git a/access_roles/static/description/assets/y18.jpg b/access_roles/static/description/assets/y18.jpg new file mode 100644 index 000000000..eea1714f2 Binary files /dev/null and b/access_roles/static/description/assets/y18.jpg differ diff --git a/access_roles/static/description/banner.jpg b/access_roles/static/description/banner.jpg new file mode 100644 index 000000000..26f18a73d Binary files /dev/null and b/access_roles/static/description/banner.jpg differ diff --git a/access_roles/static/description/hero.gif b/access_roles/static/description/hero.gif new file mode 100644 index 000000000..d7079485c Binary files /dev/null and b/access_roles/static/description/hero.gif differ diff --git a/access_roles/static/description/icon.png b/access_roles/static/description/icon.png new file mode 100644 index 000000000..4db869570 Binary files /dev/null and b/access_roles/static/description/icon.png differ diff --git a/access_roles/static/description/index.html b/access_roles/static/description/index.html new file mode 100644 index 000000000..af277c5f9 --- /dev/null +++ b/access_roles/static/description/index.html @@ -0,0 +1,1089 @@ + + + + + + Odoo Access Roles + + + + + + + + + + +
+
+ + + +
+
+
+
+
+
+ +
+ Supports: +
+ Community +
+
+ Enterprise +
+
+
+
+
+ Availability: +
+ On Premise +
+
+ Odoo.sh +
+
+
+
+
+
+
+ +
+
+
+
+

+ This module helps manage user access rights and role-based group assignments in Odoo. +

+

Odoo Access Roles +

+
+
+ +
+ + + + + +
+ +
+
+
+ +
+
+ +
+
+ + + +
+
+
+ Odoo Acces Roles +

+ Are you ready to make your business more + organized? +
Improve now! +

+ +
+
+ +
+
+
+ + + + +
+
+
+

Key + Highlights

+
+
+
+
+ +
+
+ Compatible +
+

+ Available in Odoo 18.0 Community and + Enterprise.

+
+
+
+
+
+ +
+
+ Easiness +
+

+ Easily assign predefined access rights + using custom roles. +

+
+
+
+
+ + + + +
+
+ +
+
+
+
+ acc_bg +
+ +
+
+
+
+

+ Access Role +

+
+
+

+ From the access role module, + we can create a new role and + also add necessary access rights. +

+
+
+
+ +
+
+
+
+
+
+
+
+
+

+ Role Management + +

+
+
+

+ From the Role management menu, + users can manage the roles + easily with access rights. +

+
+
+
+ +
+
+
+
+
+
+
+
+
+

+ Select Menu + +

+
+
+

+ Also we can add the roles and + then Click on 'Add a line' and select + the menus that needs to be hidden for this role. +

+
+
+
+ +
+
+
+
+
+
+
+
+
+

+ Hide Button or Tab + +

+
+
+

+ We can easily hide any button + or tabs from specific users + having this role directly from here. +

+
+
+
+ +
+
+
+
+
+
+
+
+
+

+ Hide Filter or Groupby +

+
+
+

+ We can hide specific filters + or groupby from the users in a model. +

+
+
+
+ +
+
+
+
+
+
+
+
+
+

+ Edit Field Access +

+
+
+

+ We can easily change any field + access in a model to readonly, + invisible, required etc. +

+
+
+
+ +
+
+
+
+
+
+
+
+
+

+ Edit Model Access +

+
+
+

+ Allows to control and customize + access to models based on user + roles and rules. +

+
+
+
+ +
+
+
+
+
+
+
+
+
+

+ Edit Domain Access +

+
+
+

+ Allows to manage and + customize record-level access + using dynamic domain rules. +

+
+
+
+ +
+
+
+
+
+
+
+
+
+

+ Assign Role +

+
+
+

+ We can assign role to the user + from the settings and choose + the role for that particular user. +

+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+ +
+

+ Easily assign predefined + access rights using custom roles.

+
+ +
+
+ iv class="col-md-6 col-sm-12 p-3"> +
+
+
+ +
+

+ Automatically updates views + when roles or permissions change.

+
+ +
+
+
+
+
+
+ +
+

+ Simplifies user access + management by grouping permissions.

+
+
+
+
+
+
+
+ +
+
+

+ Latest Release 18.0.1.0.0 +

+ + 16th July, 2025 + +
+
+
+
+
+ Add +
+
+
+
    +
  • + Initial Commit +
  • + +
+
+
+
+
+
+
+
+
+
+ + + + + + +
+

+ Our Services

+ +
+
+ +
+
+ .... +
+
+ +
+ + + + + + + + + + diff --git a/access_roles/static/src/js/chatter.js b/access_roles/static/src/js/chatter.js new file mode 100644 index 000000000..86e97f196 --- /dev/null +++ b/access_roles/static/src/js/chatter.js @@ -0,0 +1,36 @@ +/** @odoo-module **/ + +import { patch } from "@web/core/utils/patch"; +import { useService } from "@web/core/utils/hooks"; +import { Chatter } from "@mail/chatter/web_portal/chatter"; +import { session } from "@web/session"; +import { onRendered, useRef } from "@odoo/owl"; + +const ChatterPatch = { + setup() { + super.setup(...arguments); + this.orm = useService("orm"); + this.chatterRef = useRef("chatter"); + // Once the chatter component is mounted, check user preferences + onRendered(async () => { + try { + const userId = session.storeData.Store.settings.user_id.id; + const userData = await this.orm.call( + "role.management", + "get_role_restrictions", + [userId] + ); + if (userData.is_chatter === true && this.rootRef?.el) { + setTimeout(() => { + if (this.rootRef?.el) { + this.rootRef.el.classList.add("d-none"); + } + }, 0); + } + } catch (error) { + console.error("Failed to check chatter visibility preference:", error); + } + }); + }, +}; +patch(Chatter.prototype, ChatterPatch); diff --git a/access_roles/static/src/js/debug.js b/access_roles/static/src/js/debug.js new file mode 100644 index 000000000..b4459a356 --- /dev/null +++ b/access_roles/static/src/js/debug.js @@ -0,0 +1,35 @@ +/** @odoo-module */ + +import { patch } from "@web/core/utils/patch"; +import { LoadingIndicator } from "@web/webclient/loading_indicator/loading_indicator"; +import { useService } from "@web/core/utils/hooks"; +import { router } from "@web/core/browser/router"; +import { session } from "@web/session"; +import { onWillStart} from "@odoo/owl"; + +patch(LoadingIndicator.prototype, { + setup() { + this.orm = useService("orm"); + if (odoo.debug) { + onWillStart(async () => { + try { + const userId = session.storeData.Store.settings.user_id.id; + const result = await this.orm.call( + "role.management", + "get_role_restrictions", + [userId] + ); + if (result.is_debug) { + alert('You are not allowed to enter debug mode. Please contact Administration.'); + router.pushState({ debug: 0 }, { reload: true }); + } + } catch (error) { + console.error("Error checking debug permission:", error); + } + }); + } + super.setup(); + }, +}); + + diff --git a/access_roles/static/src/js/form_cog_menu.js b/access_roles/static/src/js/form_cog_menu.js new file mode 100644 index 000000000..8c05e1f99 --- /dev/null +++ b/access_roles/static/src/js/form_cog_menu.js @@ -0,0 +1,65 @@ +/** @odoo-module **/ + +import { patch } from "@web/core/utils/patch"; +import { ActionMenus } from "@web/search/action_menus/action_menus"; +import { session } from "@web/session"; +import { useState, onMounted } from "@odoo/owl"; + +patch(ActionMenus.prototype, { + setup() { + super.setup(...arguments); + this.state = useState({ restrictedReportIds: [], + restrictedActionIds: [], + printItems: []}); + this.fetchRestrictedReports = this.fetchRestrictedReports.bind(this); + onMounted(async () => { + await this.fetchRestrictedReports(); + }); + }, + async fetchRestrictedReports() { + if (this.state.restrictedReportIds.length || this.state.restrictedActionIds.length) { + return; + } + try { + const userId = session.uid || session.storeData?.Store?.settings?.user_id?.id; + const restrictions = await this.orm.call( + "role.management", + "get_export_restrictions", + [userId] + ); + this.state.restrictedReportIds = [ + ...new Set(restrictions + .filter(r => r.model === this.props.resModel) + .flatMap(r => r.report_id) + )]; + this.state.restrictedActionIds = [ + ...new Set(restrictions + .filter(r => r.model === this.props.resModel) + .flatMap(r => r.action_id) + ) + ]; + } catch (error) { + console.error("Error fetching restrictions:", error); + } + }, + async getActionItems(props) { + const originalActionItems = await super.getActionItems(props);; + if (!this.state.restrictedActionIds.length) { + await this.fetchRestrictedReports(); + } + return originalActionItems.filter(item => { + const isRestricted = this.state.restrictedActionIds.includes(parseInt(item.key, 10)); + return !isRestricted; + }); + }, + async loadAvailablePrintItems() { + if (!this.state.restrictedReportIds.length) { + await this.fetchRestrictedReports(); + } + const printActions = await super.loadAvailablePrintItems(); + return printActions.filter(action => { + return !this.state.restrictedReportIds.includes(parseInt(action.key, 10)); + }); + } +}); + diff --git a/access_roles/static/src/js/views/form_controller.js b/access_roles/static/src/js/views/form_controller.js new file mode 100644 index 000000000..fe5ab458d --- /dev/null +++ b/access_roles/static/src/js/views/form_controller.js @@ -0,0 +1,42 @@ +/** @odoo-module */ + +import { FormController } from '@web/views/form/form_controller'; +import { patch } from "@web/core/utils/patch"; +import { session } from "@web/session"; +import { onWillStart } from "@odoo/owl"; +import { useService } from "@web/core/utils/hooks"; + +patch(FormController.prototype, { + setup() { + super.setup(...arguments); + this.orm = useService("orm"); + + // Custom flag + this.isArchiveEnable = true; + + onWillStart(async () => { + const userId = session.storeData.Store.settings.user_id.id; + const restrictions = await this.orm.call("role.management", "get_export_restrictions", [userId]); + restrictions.forEach(result => { + if (this.props.resModel === result.model) { + if (result.is_hide_archive) { + this.isArchiveEnable = false; + } + } + }); + }); + // Patch getStaticActionMenuItems to conditionally exclude archive/unarchive + if (this.getStaticActionMenuItems) { + const originalGetStaticActionMenuItems = this.getStaticActionMenuItems.bind(this); + this.getStaticActionMenuItems = (...args) => { + const items = originalGetStaticActionMenuItems(...args); + if (!this.isArchiveEnable) { + // Remove archive and unarchive from the static items + delete items.archive; + delete items.unarchive; + } + return items; + }; + } + } +}); \ No newline at end of file diff --git a/access_roles/static/src/js/views/list_controller.js b/access_roles/static/src/js/views/list_controller.js new file mode 100644 index 000000000..58360e173 --- /dev/null +++ b/access_roles/static/src/js/views/list_controller.js @@ -0,0 +1,26 @@ +/** @odoo-module */ + +import { ListController} from '@web/views/list/list_controller'; +import { patch } from "@web/core/utils/patch"; +import { session } from "@web/session"; +import { onWillStart } from "@odoo/owl"; +import { useService } from "@web/core/utils/hooks"; + +patch(ListController.prototype, { + setup() { + super.setup(...arguments); + this.orm = useService("orm"); + onWillStart(async () => { + const userId = session.storeData.Store.settings.user_id.id; + const restrictions = await this.orm.call("role.management", "get_export_restrictions", [userId]); + restrictions.forEach(result => { + if (this.props.resModel === result.model && result.is_hide_export) { + this.isExportEnable = false; + } + if (this.props.resModel === result.model && result.is_hide_archive) { + this.archiveEnabled = false; + } + }); + }); + } +}); \ No newline at end of file diff --git a/access_roles/static/src/js/x2many.js b/access_roles/static/src/js/x2many.js new file mode 100644 index 000000000..8ca6d1d05 --- /dev/null +++ b/access_roles/static/src/js/x2many.js @@ -0,0 +1,43 @@ +/** @odoo-module **/ + +import { patch } from "@web/core/utils/patch"; +import { Many2XAutocomplete } from "@web/views/fields/relational_utils"; +import { session } from "@web/session"; + +patch(Many2XAutocomplete.prototype, { + setup() { + super.setup(); + this.orm = this.env.services.orm; + this._checkCreateAccess(); + // Ensure _checkCreateAccess runs whenever input changes + const originalOnInput = this.onInput; + this.onInput = async (...args) => { + await this._checkCreateAccess(); + originalOnInput.apply(this, args); + }; + }, + async _checkCreateAccess() { + try { + const targetModel = this.props.resModel; + const userId = session.storeData.Store.settings.user_id.id; + const result = await this.orm.call( + "role.management", + "check_model_access_restrictions", + [userId, targetModel] + ); + if (result) { + if (result.is_hide_create) { + this.props.quickCreate = null; + if (this.props.activeActions) { + this.props.activeActions.createEdit = false; + } + } + if (result.is_model_readonly && this.props.activeActions) { + this.props.activeActions.createEdit = false; + } + } + } catch (error) { + console.error("Error checking model access restrictions:", error); + } + }, +}); diff --git a/access_roles/views/access_role_menus.xml b/access_roles/views/access_role_menus.xml new file mode 100644 index 000000000..2b95ac8d4 --- /dev/null +++ b/access_roles/views/access_role_menus.xml @@ -0,0 +1,10 @@ + + + + + + + diff --git a/access_roles/views/access_role_views.xml b/access_roles/views/access_role_views.xml new file mode 100644 index 000000000..b82e23bb8 --- /dev/null +++ b/access_roles/views/access_role_views.xml @@ -0,0 +1,74 @@ + + + + access.role.view.form + access.role + +
+ +
+ + + +
+ + +
+
+
+
+ + + + + +
+ + +
+
+ + access.role.view.form.groups + access.role + + + + + + + + access.role + + + + + + + + + Access Roles + access.role + list,form + +
+ diff --git a/access_roles/views/domain_model_views.xml b/access_roles/views/domain_model_views.xml new file mode 100644 index 000000000..e7df75c24 --- /dev/null +++ b/access_roles/views/domain_model_views.xml @@ -0,0 +1,27 @@ + + + + domain.model.view.form + domain.model + +
+ + + + + + + +
+
+
+ + domain.model + + + + + + + +
diff --git a/access_roles/views/res_users_views.xml b/access_roles/views/res_users_views.xml new file mode 100644 index 000000000..74d74d7c0 --- /dev/null +++ b/access_roles/views/res_users_views.xml @@ -0,0 +1,15 @@ + + + + res.users.view.form.inherit.access.role + res.users + + + + + + + + + + diff --git a/access_roles/views/role_management_views.xml b/access_roles/views/role_management_views.xml new file mode 100644 index 000000000..fd2a899c8 --- /dev/null +++ b/access_roles/views/role_management_views.xml @@ -0,0 +1,99 @@ + + + + role.management.view.form + role.management + +
+ + + +
+
+ + +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ + Role Management + role.management + list,form + +