From c3883f6a1845154584748646edd420a00e4ea0ab Mon Sep 17 00:00:00 2001 From: pranay Date: Wed, 1 Oct 2025 12:49:15 +0530 Subject: [PATCH] new module department/user wise menu control center --- .../menu_control_center/__init__.py | 1 + .../menu_control_center/__manifest__.py | 28 +++++ .../menu_control_center/data/data.xml | 28 +++++ .../menu_control_center/models/__init__.py | 2 + .../menu_control_center/models/menu.py | 67 ++++++++++++ .../menu_control_center/models/models.py | 65 ++++++++++++ .../security/ir.model.access.csv | 4 + .../static/src/js/menu_service.js | 44 ++++++++ .../views/menu_access_control_views.xml | 100 ++++++++++++++++++ 9 files changed, 339 insertions(+) create mode 100644 addons_extensions/menu_control_center/__init__.py create mode 100644 addons_extensions/menu_control_center/__manifest__.py create mode 100644 addons_extensions/menu_control_center/data/data.xml create mode 100644 addons_extensions/menu_control_center/models/__init__.py create mode 100644 addons_extensions/menu_control_center/models/menu.py create mode 100644 addons_extensions/menu_control_center/models/models.py create mode 100644 addons_extensions/menu_control_center/security/ir.model.access.csv create mode 100644 addons_extensions/menu_control_center/static/src/js/menu_service.js create mode 100644 addons_extensions/menu_control_center/views/menu_access_control_views.xml diff --git a/addons_extensions/menu_control_center/__init__.py b/addons_extensions/menu_control_center/__init__.py new file mode 100644 index 000000000..9a7e03ede --- /dev/null +++ b/addons_extensions/menu_control_center/__init__.py @@ -0,0 +1 @@ +from . import models \ No newline at end of file diff --git a/addons_extensions/menu_control_center/__manifest__.py b/addons_extensions/menu_control_center/__manifest__.py new file mode 100644 index 000000000..385729848 --- /dev/null +++ b/addons_extensions/menu_control_center/__manifest__.py @@ -0,0 +1,28 @@ +{ + 'name': 'Menu Access Control', + 'version': '1.0', + 'summary': 'Control menu visibility based on users or companies', + 'description': """ + This module allows administrators to configure menu visibility + based on selected users or companies. Active parent menus can + be generated and assigned for access control. + """, + 'category': 'Tools', + 'author': 'PRANAY', + 'website': 'https://ftprotech.in', + 'depends': ['base','hr'], + 'data': [ + 'security/ir.model.access.csv', + 'data/data.xml', + 'views/menu_access_control_views.xml', + ], + # 'assets': { + # 'web.assets_backend': [ + # 'menu_control_center/static/src/js/menu_service.js', + # ], + # }, + 'installable': True, + 'application': True, + 'auto_install': False, + 'license': 'LGPL-3', +} diff --git a/addons_extensions/menu_control_center/data/data.xml b/addons_extensions/menu_control_center/data/data.xml new file mode 100644 index 000000000..4cc52ea05 --- /dev/null +++ b/addons_extensions/menu_control_center/data/data.xml @@ -0,0 +1,28 @@ + + + + + Human Resources + + + + Administration + + + + Development + + + + Quality Assurance + + + IT Support + + + + FINANCE + + + + \ No newline at end of file diff --git a/addons_extensions/menu_control_center/models/__init__.py b/addons_extensions/menu_control_center/models/__init__.py new file mode 100644 index 000000000..52e35f001 --- /dev/null +++ b/addons_extensions/menu_control_center/models/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import menu \ No newline at end of file diff --git a/addons_extensions/menu_control_center/models/menu.py b/addons_extensions/menu_control_center/models/menu.py new file mode 100644 index 000000000..c82f101c0 --- /dev/null +++ b/addons_extensions/menu_control_center/models/menu.py @@ -0,0 +1,67 @@ +from odoo import models, fields, api, tools, _ +from collections import defaultdict + + +class IrUiMenu(models.Model): + _inherit = 'ir.ui.menu' + + @api.model + @tools.ormcache('frozenset(self.env.user.groups_id.ids)', 'debug') + def _visible_menu_ids(self, debug=False): + """ Return the ids of the menu items visible to the user. """ + # retrieve all menus, and determine which ones are visible + context = {'ir.ui.menu.full_list': True} + menus = self.with_context(context).search_fetch([], ['action', 'parent_id']).sudo() + + # first discard all menus with groups the user does not have + + group_ids = set(self.env.user._get_group_ids()) + if not debug: + hide_menus_list = self.env['menu.access.control'].sudo().search([('user_ids','ilike',self.env.user.id)]).access_menu_line_ids.filtered(lambda menu: not(menu.is_main_menu)).menu_id.ids + + menus = menus.filtered(lambda menu: (menu.id not in hide_menus_list)) + group_ids = group_ids - { + self.env['ir.model.data']._xmlid_to_res_id('base.group_no_one', raise_if_not_found=False)} + menus = menus.filtered( + lambda menu: not (menu.groups_id and group_ids.isdisjoint(menu.groups_id._ids))) + + # take apart menus that have an action + actions_by_model = defaultdict(set) + for action in menus.mapped('action'): + if action: + actions_by_model[action._name].add(action.id) + existing_actions = { + action + for model_name, action_ids in actions_by_model.items() + for action in self.env[model_name].browse(action_ids).exists() + } + action_menus = menus.filtered(lambda m: m.action and m.action in existing_actions) + folder_menus = menus - action_menus + visible = self.browse() + + # process action menus, check whether their action is allowed + access = self.env['ir.model.access'] + MODEL_BY_TYPE = { + 'ir.actions.act_window': 'res_model', + 'ir.actions.report': 'model', + 'ir.actions.server': 'model_name', + } + + # performance trick: determine the ids to prefetch by type + prefetch_ids = defaultdict(list) + for action in action_menus.mapped('action'): + prefetch_ids[action._name].append(action.id) + + for menu in action_menus: + action = menu.action + action = action.with_prefetch(prefetch_ids[action._name]) + model_name = action._name in MODEL_BY_TYPE and action[MODEL_BY_TYPE[action._name]] + if not model_name or access.check(model_name, 'read', False): + # make menu visible, and its folder ancestors, too + visible += menu + menu = menu.parent_id + while menu and menu in folder_menus and menu not in visible: + visible += menu + menu = menu.parent_id + + return set(visible.ids) \ No newline at end of file diff --git a/addons_extensions/menu_control_center/models/models.py b/addons_extensions/menu_control_center/models/models.py new file mode 100644 index 000000000..a1f11067e --- /dev/null +++ b/addons_extensions/menu_control_center/models/models.py @@ -0,0 +1,65 @@ +# models/models.py +from odoo import models, fields, api, tools, _ +from collections import defaultdict + +class MenuControlUnits(models.Model): + _name = 'menu.control.units' + _rec_name = 'unit_name' + + _sql_constraints = [ + ('unique_unit_name', 'UNIQUE(unit_name)', "'Unit Name' already defined. Please don't confuse me 😤.") + ] + + unit_name = fields.Char(string='Unit Name',required=True) + department_ids = fields.Many2many('hr.department') + + user_ids = fields.Many2many('res.users') + + def generate_department_user_ids(self): + for rec in self: + user_ids = self.env['hr.employee'].sudo().search([('department_id','in',rec.department_ids.ids)]).user_id.ids + self.write({ + 'user_ids': [(6, 0, user_ids)] + }) + +class MenuAccessControl(models.Model): + _name = 'menu.access.control' + _description = 'Menu Access Control' + _rec_name = 'control_unit' + + _sql_constraints = [ + ('unique_control_unit', 'UNIQUE(control_unit)', "Only one service can exist with a specific control_unit. Please don't confuse me 🤪.") + ] + + control_unit = fields.Many2one('menu.control.units',required=True) + user_ids = fields.Many2many('res.users', string="Users", related='control_unit.user_ids') + + + access_menu_line_ids = fields.One2many( + 'menu.access.line', 'access_control_id', + string="Accessible Menus" + ) + + def action_generate_menus(self): + """Button to fetch active top-level menus and populate access lines.""" + menu_lines = [] + active_menus = self.env['ir.ui.menu'].search([ + ('parent_id', '=', False), # top-level menus + ('active', '=', True) + ]) + for menu in active_menus: + menu_lines.append((0, 0, { + 'menu_id': menu.id, + 'is_main_menu': True + })) + self.access_menu_line_ids = menu_lines + + +class MenuAccessLine(models.Model): + _name = 'menu.access.line' + _description = 'Menu Access Line' + _rec_name = 'menu_id' + + access_control_id = fields.Many2one('menu.access.control', ondelete='cascade') + menu_id = fields.Many2one('ir.ui.menu', string="Menu") + is_main_menu = fields.Boolean(string="Is Main Menu", default=True) diff --git a/addons_extensions/menu_control_center/security/ir.model.access.csv b/addons_extensions/menu_control_center/security/ir.model.access.csv new file mode 100644 index 000000000..d7edab9d1 --- /dev/null +++ b/addons_extensions/menu_control_center/security/ir.model.access.csv @@ -0,0 +1,4 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_menu_access_control,access.menu.access.control,model_menu_access_control,hr.group_hr_manager,1,1,1,1 +access_menu_access_line,access.menu.access.line,model_menu_access_line,hr.group_hr_manager,1,1,1,1 +access_menu_control_units,access.menu.control.units,model_menu_control_units,hr.group_hr_manager,1,1,1,1 \ No newline at end of file diff --git a/addons_extensions/menu_control_center/static/src/js/menu_service.js b/addons_extensions/menu_control_center/static/src/js/menu_service.js new file mode 100644 index 000000000..fb647a3ec --- /dev/null +++ b/addons_extensions/menu_control_center/static/src/js/menu_service.js @@ -0,0 +1,44 @@ +///** @odoo-module **/ +// +//import { browser } from "@web/core/browser/browser"; +//import { makeEnv, startServices } from "@web/env"; +//import { session } from "@web/session"; +//import { _t } from "@web/core/l10n/translation"; +//import { browser } from "@web/core/browser/browser"; +//import { MenuService } from "@web/services/menu_service"; +// +//export const menuService = { +// dependencies: MenuService.dependencies, +// start(env, deps) { +// const menu = super.start(env, deps); +// +// // Override the getApps method +// const originalGetApps = menu.getApps; +// menu.getApps = async function() { +// // Get the original apps +// let apps = await originalGetApps.call(this); +// +// // Fetch your custom menu access data +// const accessData = await this.orm.call( +// 'menu.access.control', +// 'get_user_access_menus', +// [] +// ); +// +// // If access data exists, filter the apps +// if (accessData && accessData.allowed_menu_ids) { +// apps = apps.filter(app => +// accessData.allowed_menu_ids.includes(app.id) +// ); +// } +// +// return apps; +// }; +// +// return menu; +// }, +//}; +// +//// Register the service +//import { registry } from "@web/core/registry"; +//registry.category("services").add("menu", menuService, { force: true }); \ No newline at end of file diff --git a/addons_extensions/menu_control_center/views/menu_access_control_views.xml b/addons_extensions/menu_control_center/views/menu_access_control_views.xml new file mode 100644 index 000000000..7dd71421d --- /dev/null +++ b/addons_extensions/menu_control_center/views/menu_access_control_views.xml @@ -0,0 +1,100 @@ + + + + menu.control.units.list + menu.control.units + + + + + + + + + + menu.control.units.form + menu.control.units + +
+ + + + +