在 Odoo 上做二次开发,最常见的两个需求:
- 批量生成一套 ERP 菜单结构(复刻鼎捷 E10、用友、SAP、K3 之类的菜单树);
- 菜单点不开,点一下就报「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"
三条都过,再去浏览器点。
核心原则:能写进命令的断言,就不要用眼睛看。
六、几个能省时间的实践
- 结构定义放 Python,XML 当产物——这条值多少小时省多少小时。
- 每次部署后 grep 日志——静默失败是 Odoo 开发最大的时间黑洞。
-i和-u分清——模块首次安装和后续更新用错参数,现象会很迷惑。- Git Bash 加
MSYS_NO_PATHCONV=1——否则路径被转换,报错信息完全指不到点上。 - 出问题时先注释菜单——二分法定位比逐行读日志快。
结语
Odoo 二次开发的核心难点不在技术,在它是元数据驱动的。
你写的不是业务逻辑,是"往数据库里塞元数据"。一旦 XML 有一点错,它不会明确报错,而是静默地部分失败,然后以"点不开""白屏"的形式暴露出来。
把调试思路从"改代码看效果"换成"写断言验证数据",效率会有质的差别。
本文基于在 Docker 环境 Odoo 实例上的自定义模块开发实践总结。环境:Docker + Odoo(addons 挂载至 /mnt/extra-addons)。