Odoo 二次开发实战:菜单点不开的四个根因与排查顺序

1 阅读6分钟

主稿 D|Odoo 模块开发与菜单点不开的排查

标题备选

  • D1(CSDN/掘金/阿里云):Odoo 自定义模块实战:批量生成菜单 XML,以及「菜单点不开」的根治办法
  • D2(知乎):在 Odoo 里复刻一套 ERP 菜单,我踩过的 4 个坑
  • D3(小红书/贴吧/B站):Odoo 二次开发避坑:菜单点不开怎么办

正文(长版·CSDN/掘金/阿里云/知乎)

在 Odoo 上做二次开发,最常见的两个需求:

  1. 批量生成一套 ERP 菜单结构(复刻鼎捷 E10、用友、SAP、K3 之类的菜单树);
  2. 菜单点不开,点一下就报「Odoo客户端错误」,或者直接 OwlError。

这两个问题看起来不同,其实根子是同一个:你没搞清楚 Odoo 的数据模型和运行环境就开始写 XML。

这篇记录我实际踩过的坑和现在的解法。

一、先摸环境,别急着写代码

这一步跳过的,后面全部要还。

docker ps --format "{{.Names}}\t{{.Image}}\t{{.Ports}}"   # 找 odoo 容器
docker inspect <容器名> --format '{{json .Mounts}}'          # 找 addons 挂载路径(关键!)
docker exec <db容器> psql -U odoo -lqt | head              # 列数据库
docker exec <db容器> psql -U odoo -d <库名> -tAc \
  "select name,state from ir_module_module where state='installed' order by name;"
docker exec <容器名> odoo -c /etc/odoo/odoo.conf -d <库名> --stop-after-init

最关键的一条是第二条:docker inspect 找 addons 挂载路径。

这个路径决定你文件写哪儿。常见情况是宿主机的 E:\developer-file\odoo\addons 挂到容器内的 /mnt/extra-addons。找错了,你会出现"我明明改了代码怎么没生效"的经典幻觉。

一个 Git Bash 的坑

在 Windows 上,Git Bash 会做路径转换:

# ❌ 错误:会被转换成 D:/.../etc/odoo/odoo.conf
docker exec <容器> odoo -c /etc/odoo/odoo.conf -d <库名> --stop-after-init

# ✅ 正确
MSYS_NO_PATHCONV=1 docker exec <容器> odoo -c /etc/odoo/odoo.conf -d <库名> --stop-after-init

所有 docker exec 前面加 MSYS_NO_PATHCONV=1。

二、菜单结构定义在 Python 里,不在 XML 里

这是最重要的一条工程实践。

当你需要复刻一套 ERP 菜单——那种几百上千个节点的树——绝对不要手写 XML。你会写到崩溃,而且改一个节点要把整个文件重新对一遍。

正确做法:写一个 menu_tree.py 定义嵌套结构,用生成器产出 XML。

# menu_tree.py
TREE = [
    ("#基础资料", "g"), [          # g = 目录
        ("客户管理", "p"),           # p = 流程
        ("物料管理", "r"),           # r = 报表
    ],
    ("#生产管理", "g"), [
        ("工单管理", "p"),
        ("单据", "f"),               # f = 单据
    ],
]

节点类型用一个字母标注:g=目录、p=流程、r=报表、f=单据,目录是 ("#目录名", "g"), [子项...],叶子默认 form。

然后:

cd <模块>/tools && python gen_xml.py
MSYS_NO_PATHCONV=1 docker exec <容器> odoo -c /etc/odoo/odoo.conf -d <库> --stop-after-init -u <模块名>
docker restart <容器>

首次安装用 -i <模块名>,后续更新用 -u <模块名>。这两个参数搞混是另一个常见错误源。

好处很明显:结构在 Python 里是可读的、能 diff 的、能被 IDE 提示的,XML 只是构建产物。

三、「菜单点不开」的四个根因

这个问题的排查顺序我从高频到低频排给你。

根因 1:res_id / integer ref 写错(最常见)

ir.actions.act_window 的 res_model 必须指向真实存在且已安装的模型。

<field name="res_model">sale.order</field>

如果这个模型所属模块没装,或者模型名拼错,菜单点下去就是 OwlError 或者空白。

排查方法:

docker exec <db容器> psql -U odoo -d <库名> -tAc \
  "select model from ir_model where model like '%sale%';"

先把模型名捞出来,再写进 XML。

根因 2:XML 语法错误导致部分数据没导入

Odoo 导入 XML 时,一条记录出错不影响其他记录,但错误只写到日志里。

所以你看到的现象往往是:「大部分菜单出来了,就某一个点不开」——因为那一条静默失败了。

docker logs <容器名> 2>&1 | grep -i "ERROR\|CRITICAL\|ValidationError"

必须养成每次部署后查日志的习惯。

根因 3:action 没有正确绑定

菜单项的 action 字段必须指向一个已存在的 action 的 xml id:

<field name="action">my_module.action_my_view</field>

这里用的是外部 ID(xml id),不是数据库 id。如果对应的 ir.actions.act_window 定义在另一个文件里,加载顺序错了就会解析不到。

根因 4:前端资源没重新加载

改完视图 XML 之后,如果 odoo worker 没重启或者资产没清,浏览器拿到的还是旧的 JS bundle。

docker restart <容器名>

然后浏览器强制刷新(Ctrl+F5)。

四、一个更隐蔽的坑:整站崩溃

Odoo 的 Web 客户端有个特性:一个坏掉的 action 可能让整个客户端页面挂掉,表现就是白屏或持续转圈。

所以排查时有个技巧:先临时把出问题的菜单项禁用,看其他功能是否恢复。

<!-- 注释掉可疑项,快速定位 -->
<!-- <menuitem id="broken_menu" name="问题菜单" action="broken_action"/> -->

如果禁用了就好了,范围立刻缩小到这一条。

五、验证不能靠肉眼

改完 Odoo 不能只看"菜单出现了",要实际点进去,而且要有可复现的验证手段。

我现在的做法是:

# 1. 确认菜单数据真的进库了
docker exec <db容器> psql -U odoo -d <库名> -tAc \
  "select name, action from ir_ui_menu m join ir_model i on m.action like '%'||i.model||'%' limit 20;"

# 2. 确认 action 指向的模型存在
docker exec <db容器> psql -U odoo -d <库名> -tAc \
  "select a.name, a.res_model from ir_act_window a limit 20;"

# 3. 后端无报错
docker logs <容器名> 2>&1 | grep -c "ERROR"

三条都过,再去浏览器点。

核心原则:能写进命令的断言,就不要用眼睛看。

六、几个能省时间的实践

  1. 结构定义放 Python,XML 当产物——这条值多少小时省多少小时。
  2. 每次部署后 grep 日志——静默失败是 Odoo 开发最大的时间黑洞。
  3. -i 和 -u 分清——模块首次安装和后续更新用错参数,现象会很迷惑。
  4. Git Bash 加 MSYS_NO_PATHCONV=1——否则路径被转换,报错信息完全指不到点上。
  5. 出问题时先注释菜单——二分法定位比逐行读日志快。

结语

Odoo 二次开发的核心难点不在技术,在它是元数据驱动的。

你写的不是业务逻辑,是"往数据库里塞元数据"。一旦 XML 有一点错,它不会明确报错,而是静默地部分失败,然后以"点不开""白屏"的形式暴露出来。

把调试思路从"改代码看效果"换成"写断言验证数据",效率会有质的差别。


本文基于在 Docker 环境 Odoo 实例上的自定义模块开发实践总结。环境:Docker + Odoo(addons 挂载至 /mnt/extra-addons)。