先说结论
Odoo 菜单是否显示,不是一个“有 group 就显示、没 group 就隐藏”的简单判断。
真正的链路是:
- 先找出用户理论上能看的菜单
- 再检查菜单挂的 action 是否存在
- 再检查 action 对应模型有没有读权限
- 最后把父级菜单一路补出来
所以有些菜单“理论上存在”,但用户实际看不到;也有些菜单看得到,却点进去才发现没有权限或动作失效。
_visible_menu_ids 在做什么
_visible_menu_ids() 是整个可见性判断的核心。
它先拿当前用户的 group 集合,再按这些 group 去查菜单。
这里有个很有意思的细节:
- 如果不是 debug 模式,会把
base.group_no_one排除掉 - 查询菜单时会批量把
parent_id和action一次读出来 - 然后再根据 action 类型去检查对应模型是否存在
这意味着菜单可见性不是一次查一个,而是尽量批量处理。
为什么要先预读 action
菜单本身可见,不代表它挂的 action 还活着。
所以 Odoo 会先把 action 分类成不同模型:
ir.actions.act_windowir.actions.reportir.actions.server- 其他 reference 类型
然后分别检查它们是否存在。
对于窗口动作、报表动作、服务器动作,Odoo 还会顺手读取它们对应的目标模型字段,再做读权限校验。
这一步特别重要,因为:
一个菜单能不能点,不只看菜单本身,还看它背后的 action 能不能真正执行。
为什么祖先菜单也会被保留下来
如果某个子菜单可见,Odoo 会沿着 parent_id 一路把它的祖先也加入可见集合。
这是为了保证菜单树不会断掉。
否则你可能会看到一个“孤立子菜单”挂在一个不可见父节点下面,用户根本没法导航。
load_menus_root 和 load_menus 的作用
这两个方法负责把菜单树喂给前端。
load_menus_root():给根节点和顶层菜单load_menus(debug):给完整菜单树,并结合黑名单和可见性过滤
其中 load_menus() 还会按 app 重新组织树结构,让前端拿到的是“用户真正能用的应用入口”。
常见误解
1)group_ids 空就是公开菜单
不完全是。即使菜单没有组限制,挂的 action 仍然可能因为模型权限而被挡掉。
2)菜单显示了就一定能进去
也不一定。action 可能存在,但目标模型没有读权限,最终还是会被过滤。
3)debug 模式和普通模式完全一样
不一样。debug 模式会保留 group_no_one 相关菜单,普通模式不会。
排查菜单“看不见”的顺序
如果一个菜单不显示,我会先看:
- 菜单的
group_ids - 菜单挂的
action是否存在 - action 对应模型是否能读
- 祖先菜单有没有被别的限制卡掉
- 是否处在 debug 模式
一句话总结
Odoo 菜单可见性是“菜单组权限 + action 存在性 + 目标模型读权限 + 祖先补全”的组合结果,不是单看菜单自己就能决定的。
DISCUSSION
评论区