from passlib.apps import master_context from odoo import models, fields, api, tools, _ from collections import defaultdict from odoo.http import request 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 menu items visible to the current user based on permissions and active master.""" # Clear existing cache to ensure fresh menu visibility calculation self.env['ir.ui.menu'].sudo().clear_caches() # Retrieve all menus with required fields context = {'ir.ui.menu.full_list': True} menus = self.with_context(context).search_fetch([], ['action', 'parent_id']).sudo() # Get active master and control configuration active_master = request.session.get('active_master') master_control = False control_unit = False if active_master: master_control = self.env['master.control'].sudo().search( [('code', '=', active_master)], limit=1 ) if master_control: control_unit = self.env['menu.access.control'].sudo().search([ ('user_ids', 'ilike', self.env.user.id), ('master_control', '=', master_control.id) ]) # Get user groups and exclude technical group in non-debug mode group_ids = set(self.env.user._get_group_ids()) if not debug: group_ids -= { self.env['ir.model.data']._xmlid_to_res_id( 'base.group_no_one', raise_if_not_found=False ) } # Filter menus by group permissions menus = menus.filtered( lambda menu: not (menu.groups_id and group_ids.isdisjoint(menu.groups_id._ids)) ) # Determine menus to hide based on access control hide_menus_list = self._get_hidden_menu_ids(control_unit, master_control, debug) menus = menus.filtered(lambda menu: menu.id not in hide_menus_list) # Process menus with actions visible = self._process_action_menus(menus) return set(visible.ids) def _get_hidden_menu_ids(self, control_unit, master_control, debug): """Helper method to determine menu IDs that should be hidden from the user.""" if debug and control_unit: # In debug mode with control unit, use its specific menu restrictions parent_menus = control_unit.access_menu_line_ids.filtered( lambda menu: not menu.is_main_menu ).menu_id.ids sub_menus = control_unit.access_sub_menu_line_ids.filtered( lambda menu: not menu.is_main_menu ).menu_id.ids elif not debug: # In non-debug mode, determine menus to hide based on control configuration domain = [('user_ids', 'ilike', self.env.user.id)] if master_control: domain.append(('master_control', '=', master_control.id)) access_controls = self.env['menu.access.control'].sudo().search(domain) parent_menus = access_controls.access_menu_line_ids.filtered( lambda menu: not menu.is_main_menu ).menu_id.ids sub_menus = access_controls.access_sub_menu_line_ids.filtered( lambda menu: not menu.is_main_menu ).menu_id.ids else: # Default case: no menus to hide return [] return list(set(parent_menus + sub_menus)) def _process_action_menus(self, menus): """Process menus with actions and determine visibility based on model access.""" # Separate menus with actions from folder menus 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() # Model access check configuration access = self.env['ir.model.access'] MODEL_BY_TYPE = { 'ir.actions.act_window': 'res_model', 'ir.actions.report': 'model', 'ir.actions.server': 'model_name', } # Prefetch action data for performance prefetch_ids = defaultdict(list) for action in action_menus.mapped('action'): prefetch_ids[action._name].append(action.id) # Check access for each action menu for menu in action_menus: action = menu.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 visible += menu parent = menu.parent_id while parent and parent in folder_menus and parent not in visible: visible += parent parent = parent.parent_id return visible