odoo_17.0.1/odoo/addons/base/models/ir_ui_menu.py

339 lines
14 KiB
Python
Raw Normal View History

# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import base64
from collections import defaultdict
from os.path import join as opj
import operator
import re
from odoo import api, fields, models, tools, _
from odoo.exceptions import ValidationError
from odoo.http import request
from odoo.osv import expression
MENU_ITEM_SEPARATOR = "/"
NUMBER_PARENS = re.compile(r"\(([0-9]+)\)")
class IrUiMenu(models.Model):
_name = 'ir.ui.menu'
_description = 'Menu'
_order = "sequence,id"
_parent_store = True
_allow_sudo_commands = False
name = fields.Char(string='Menu', required=True, translate=True)
active = fields.Boolean(default=True)
sequence = fields.Integer(default=10)
child_id = fields.One2many('ir.ui.menu', 'parent_id', string='Child IDs')
parent_id = fields.Many2one('ir.ui.menu', string='Parent Menu', index=True, ondelete="restrict")
parent_path = fields.Char(index=True, unaccent=False)
groups_id = fields.Many2many('res.groups', 'ir_ui_menu_group_rel',
'menu_id', 'gid', string='Groups',
help="If you have groups, the visibility of this menu will be based on these groups. "\
"If this field is empty, Odoo will compute visibility based on the related object's read access.")
complete_name = fields.Char(string='Full Path', compute='_compute_complete_name', recursive=True)
web_icon = fields.Char(string='Web Icon File')
action = fields.Reference(selection=[('ir.actions.report', 'ir.actions.report'),
('ir.actions.act_window', 'ir.actions.act_window'),
('ir.actions.act_url', 'ir.actions.act_url'),
('ir.actions.server', 'ir.actions.server'),
('ir.actions.client', 'ir.actions.client')])
web_icon_data = fields.Binary(string='Web Icon Image', attachment=True)
@api.depends('name', 'parent_id.complete_name')
def _compute_complete_name(self):
for menu in self:
menu.complete_name = menu._get_full_name()
def _get_full_name(self, level=6):
""" Return the full name of ``self`` (up to a certain level). """
if level <= 0:
return '...'
if self.parent_id:
return self.parent_id._get_full_name(level - 1) + MENU_ITEM_SEPARATOR + (self.name or "")
else:
return self.name
def _read_image(self, path):
if not path:
return False
path_info = path.split(',')
icon_path = opj(path_info[0], path_info[1])
try:
with tools.file_open(icon_path, 'rb', filter_ext=('.png',)) as icon_file:
return base64.encodebytes(icon_file.read())
except FileNotFoundError:
return False
@api.constrains('parent_id')
def _check_parent_id(self):
if not self._check_recursion():
raise ValidationError(_('Error! You cannot create recursive menus.'))
@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()
groups = self.env.user.groups_id
if not debug:
groups = groups - self.env.ref('base.group_no_one')
# first discard all menus with groups the user does not have
menus = menus.filtered(
lambda menu: not menu.groups_id or menu.groups_id & groups)
# 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)
@api.returns('self')
def _filter_visible_menus(self):
""" Filter `self` to only keep the menu items that should be visible in
the menu hierarchy of the current user.
Uses a cache for speeding up the computation.
"""
visible_ids = self._visible_menu_ids(request.session.debug if request else False)
return self.filtered(lambda menu: menu.id in visible_ids)
@api.model
def search_fetch(self, domain, field_names, offset=0, limit=None, order=None):
menus = super().search_fetch(domain, field_names, order=order)
if menus:
# menu filtering is done only on main menu tree, not other menu lists
if not self._context.get('ir.ui.menu.full_list'):
menus = menus._filter_visible_menus()
if offset:
menus = menus[offset:]
if limit:
menus = menus[:limit]
return menus
@api.model
def search_count(self, domain, limit=None):
# to be consistent with search() above
return len(self.search(domain, limit=limit))
@api.depends('parent_id')
def _compute_display_name(self):
for menu in self:
menu.display_name = menu._get_full_name()
@api.model_create_multi
def create(self, vals_list):
self.env.registry.clear_cache()
for values in vals_list:
if 'web_icon' in values:
values['web_icon_data'] = self._compute_web_icon_data(values.get('web_icon'))
return super(IrUiMenu, self).create(vals_list)
def write(self, values):
self.env.registry.clear_cache()
if 'web_icon' in values:
values['web_icon_data'] = self._compute_web_icon_data(values.get('web_icon'))
return super(IrUiMenu, self).write(values)
def _compute_web_icon_data(self, web_icon):
""" Returns the image associated to `web_icon`.
`web_icon` can either be:
- an image icon [module, path]
- a built icon [icon_class, icon_color, background_color]
and it only has to call `_read_image` if it's an image.
"""
if web_icon and len(web_icon.split(',')) == 2:
return self._read_image(web_icon)
def unlink(self):
# Detach children and promote them to top-level, because it would be unwise to
# cascade-delete submenus blindly. We also can't use ondelete=set null because
# that is not supported when _parent_store is used (would silently corrupt it).
# TODO: ideally we should move them under a generic "Orphans" menu somewhere?
extra = {'ir.ui.menu.full_list': True,
'active_test': False}
direct_children = self.with_context(**extra).search([('parent_id', 'in', self.ids)])
direct_children.write({'parent_id': False})
self.env.registry.clear_cache()
return super(IrUiMenu, self).unlink()
def copy(self, default=None):
record = super(IrUiMenu, self).copy(default=default)
match = NUMBER_PARENS.search(record.name)
if match:
next_num = int(match.group(1)) + 1
record.name = NUMBER_PARENS.sub('(%d)' % next_num, record.name)
else:
record.name = record.name + '(1)'
return record
@api.model
@api.returns('self')
def get_user_roots(self):
""" Return all root menu ids visible for the user.
:return: the root menu ids
:rtype: list(int)
"""
return self.search([('parent_id', '=', False)])
def _load_menus_blacklist(self):
return []
@api.model
@tools.ormcache_context('self._uid', keys=('lang',))
def load_menus_root(self):
fields = ['name', 'sequence', 'parent_id', 'action', 'web_icon_data']
menu_roots = self.get_user_roots()
menu_roots_data = menu_roots.read(fields) if menu_roots else []
menu_root = {
'id': False,
'name': 'root',
'parent_id': [-1, ''],
'children': menu_roots_data,
'all_menu_ids': menu_roots.ids,
}
xmlids = menu_roots._get_menuitems_xmlids()
for menu in menu_roots_data:
menu['xmlid'] = xmlids.get(menu['id'], '')
return menu_root
@api.model
@tools.ormcache_context('self._uid', 'debug', keys=('lang',))
def load_menus(self, debug):
""" Loads all menu items (all applications and their sub-menus).
:return: the menu root
:rtype: dict('children': menu_nodes)
"""
fields = ['name', 'sequence', 'parent_id', 'action', 'web_icon']
menu_roots = self.get_user_roots()
menu_roots_data = menu_roots.read(fields) if menu_roots else []
menu_root = {
'id': False,
'name': 'root',
'parent_id': [-1, ''],
'children': [menu['id'] for menu in menu_roots_data],
}
all_menus = {'root': menu_root}
if not menu_roots_data:
return all_menus
# menus are loaded fully unlike a regular tree view, cause there are a
# limited number of items (752 when all 6.1 addons are installed)
menus_domain = [('id', 'child_of', menu_roots.ids)]
blacklisted_menu_ids = self._load_menus_blacklist()
if blacklisted_menu_ids:
menus_domain = expression.AND([menus_domain, [('id', 'not in', blacklisted_menu_ids)]])
menus = self.search(menus_domain)
menu_items = menus.read(fields)
xmlids = (menu_roots + menus)._get_menuitems_xmlids()
# add roots at the end of the sequence, so that they will overwrite
# equivalent menu items from full menu read when put into id:item
# mapping, resulting in children being correctly set on the roots.
menu_items.extend(menu_roots_data)
mi_attachments = self.env['ir.attachment'].sudo().search_read(
domain=[('res_model', '=', 'ir.ui.menu'),
('res_id', 'in', [menu_item['id'] for menu_item in menu_items if menu_item['id']]),
('res_field', '=', 'web_icon_data')],
fields=['res_id', 'datas', 'mimetype'])
mi_attachment_by_res_id = {attachment['res_id']: attachment for attachment in mi_attachments}
# set children ids and xmlids
menu_items_map = {menu_item["id"]: menu_item for menu_item in menu_items}
for menu_item in menu_items:
menu_item.setdefault('children', [])
parent = menu_item['parent_id'] and menu_item['parent_id'][0]
menu_item['xmlid'] = xmlids.get(menu_item['id'], "")
if parent in menu_items_map:
menu_items_map[parent].setdefault(
'children', []).append(menu_item['id'])
attachment = mi_attachment_by_res_id.get(menu_item['id'])
if attachment:
menu_item['web_icon_data'] = attachment['datas']
menu_item['web_icon_data_mimetype'] = attachment['mimetype']
else:
menu_item['web_icon_data'] = False
menu_item['web_icon_data_mimetype'] = False
all_menus.update(menu_items_map)
# sort by sequence
for menu_id in all_menus:
all_menus[menu_id]['children'].sort(key=lambda id: all_menus[id]['sequence'])
# recursively set app ids to related children
def _set_app_id(app_id, menu):
menu['app_id'] = app_id
for child_id in menu['children']:
_set_app_id(app_id, all_menus[child_id])
for app in menu_roots_data:
app_id = app['id']
_set_app_id(app_id, all_menus[app_id])
# filter out menus not related to an app (+ keep root menu)
all_menus = {menu['id']: menu for menu in all_menus.values() if menu.get('app_id')}
all_menus['root'] = menu_root
return all_menus
def _get_menuitems_xmlids(self):
menuitems = self.env['ir.model.data'].sudo().search([
('res_id', 'in', self.ids),
('model', '=', 'ir.ui.menu')
])
return {
menu.res_id: menu.complete_name
for menu in menuitems
}