先说结论
很多人把 Odoo 视图继承理解成:
- 找到基视图
- 写一个 XPath
- 系统把改动贴上去
这只说对了一半。
真正麻烦的地方在于:一个页面往往不是只有你这一层继承。同一个 form/tree/search 视图,可能同时被多个模块、多个自定义模块、多个 Studio 扩展一起修改。
所以你真正要问的,不是“我的 XPath 对不对”,而是:
当很多继承视图同时存在时,Odoo 到底按什么顺序把它们组合成最终视图?
这件事在 base/models/ir_ui_view.py 里写得很清楚,核心在这几个点:
priority决定同层扩展的大体先后mode决定一个视图是“扩展层”还是“新的主根”_combine()用的是带特殊规则的深度优先遍历primary视图不会像普通 extension 一样立刻插进去,而是会在当前主树的 extension 处理完后再继续展开
理解了这几件事,你调试“为什么 XPath 有时生效、有时被别人盖掉”就会快很多。
先把两个角色分清:extension 和 primary
在 Odoo 里,不是所有带 inherit_id 的视图都扮演同一种角色。
extension:在父视图上继续打补丁
这是最常见的情况。
你继承某个 view,然后写 XPath 去插字段、改属性、替换节点。
这种视图的作用是:
在已有主视图上继续叠加修改。
它不是新的根,只是修改链中的一个补丁层。
primary:成为一棵新的“主视图根”
primary 更像是:
我承认自己来源于某个父视图,但我希望最终对外被当作一棵新的主视图来继续挂子扩展。
这也是为什么某些继承链里,你会看到“同样 inherit 某个父视图”,但最终组合顺序明显和普通扩展不一样。
因为它不是简单补一刀,而是把自己提升成后续组合的主干节点。
源码里真正的关键:_combine()
在 ir_ui_view.py 里,_combine() 的注释几乎直接把设计意图写出来了。
它要做的是:
- 先从当前
primary视图的arch出发 - 再把整棵继承树按顺序遍历
- 每走到一个子视图,就调用
apply_inheritance_specs()把修改应用到当前组合结果上
源码里最值得注意的一句不是 XPath 本身,而是这句思路:
对当前 primary view,先遍历它的各种扩展;如果途中遇到子 primary,则把那个 primary 放到队列另一端,等当前分支的 extension 处理完后再进入它。
这意味着:
- extension 更像“立刻叠加”
- primary 更像“下一层主树入口”
所以最终顺序不是“看到谁先处理谁”,而是一个带优先规则的深度优先过程。
为什么官方说是“特殊的深度优先遍历”
源码里用 collections.deque 来维护遍历队列。
规则大意是:
- 普通 extension 子视图:往左边压,尽快处理
primary子视图:往右边放,延后处理
这个设计非常像:
- 先把当前主树应该叠上的补丁都叠完
- 再进入下一棵从它分叉出去的主树
如果你把它想成“分支代码合并”会比较容易理解:
- extension 是在当前分支继续 commit
- primary 是从某个时点切出去,形成后面还会继续长孩子的新主分支
这也解释了为什么某些视图问题,光看 inherit_id 还不够,你还得看它是不是 primary,以及它在树里的层级位置。
priority 真正影响什么
同一个父视图下面往往会挂很多子视图。
源码在构造队列时会先做 sorted(hierarchy[self], key=lambda v: v.mode),而实际子视图集合本身又是按数据库查询顺序和 priority 参与组合。你在业务上真正感受到的是:
- priority 小的,通常更早参与组合
- priority 大的,通常更晚叠上去
而“更晚叠上去”意味着什么?
1. 后来的 attributes 可能覆盖前面的属性修改
比如前一个扩展把字段设成 readonly="1",后一个又改回动态表达式,那么最终你看到的是后者。
2. 前面的结构改动会影响后面 XPath 能不能找到节点
这是最常见的坑。
你以为自己的 XPath 没写错,实际上是:
- 前一个扩展把节点搬走了
- 或者已经
replace了 - 或者外层结构被重组了
于是你的 XPath 匹配路径在“最终待处理的中间 arch”里已经不存在。
3. 不是所有“后执行”都等于“更强”
有些人会把 priority 理解成 CSS 权重,这是不对的。
因为后面的视图必须先找得到目标节点,才有资格覆盖前面。
如果节点都被改没了,priority 再大也没用。
为什么“我继承的是同一个父视图”,结果却和别人的扩展相互影响
因为 Odoo 不是给每个扩展一份独立父视图副本。
它是把所有扩展叠到同一条组合链里。
也就是说:
- 你的 XPath 面向的是“当前已组合到这一步的中间结果”
- 不是最初那份官方原始 XML
这点非常关键。
很多人调试失败,是因为脑子里看的还是原模块 XML;但程序实际操作的,已经是:
原始视图 + 若干前置继承之后的组合 arch
所以在复杂项目里,调视图的正确方式不是只看基视图,而是看:
- 基视图是谁
- 当前链上还有哪些继承视图
- 它们的 priority 怎么排
- 哪些是 extension,哪些是 primary
- 你自己的 XPath 是打在“原图”上,还是打在“别人改过的图”上
apply_inheritance_specs() 为什么是调试核心
_combine() 每处理一个子视图,最终都落到 apply_inheritance_specs()。
这个方法本身不神秘,它做的就是:
- 定位 spec 节点
- 根据
position执行 after / before / inside / replace / attributes / move 等修改 - 如果定位失败,抛出视图错误
所以很多视图问题本质上不是“前端没刷新”,而是:
- 组合顺序让你的目标节点已经变形
- 于是 spec 应用点变化了
- 或者直接定位失败
当你把问题归因从“XPath 写错”升级到“继承树顺序导致 XPath 对象变化”,思路会立刻清晰很多。
最容易误解的 4 个点
误解 1:inherit_id 一样,作用面就一样
不对。
即使两个视图都继承 sale.view_order_form,只要:
- priority 不同
- 中间还夹着别的 extension
- 或者其中一个是
primary
它们对最终视图的影响位置就可能完全不同。
误解 2:priority 只是在“冲突时”才重要
不对。
priority 不只是冲突解决,它直接决定了:
- 谁先改结构
- 谁后面基于谁来匹配 XPath
换句话说,它影响的不是最终一行配置,而是整个继承链的中间态。
误解 3:换个更长的 XPath 就能解决一切
也不对。
更长的 XPath 只是让定位更具体,不会改变组合顺序。如果前置扩展把你依赖的层级结构整体换掉,长 XPath 反而更脆。
误解 4:primary 只是少见配置,可以忽略
复杂系统里不能忽略。
特别是多套后端界面、不同业务入口或平台扩展共存时,primary 会改变视图树组织方式。你忽略它,就很容易看错“谁才是当前主根”。
实战里怎么避免把自己写进死胡同
1. 尽量锚定稳定节点
优先找:
- 语义稳定的字段
- 官方长期不太会改名的容器
- 而不是第几个 group、第几个 page
因为前者更能穿越前置扩展带来的结构漂移。
2. 能做属性增量,就别轻易 replace 大块节点
replace 很爽,但破坏面最大。
你一旦整块替掉,后面别人原本打在旧节点上的 XPath 很可能一起失效。
3. 改优先级前先判断自己是在“覆盖结果”还是“依赖前置结构”
如果你只是想最后改个属性,提高 priority 可能有效。
但如果你依赖另一个扩展先把节点插进来,priority 提太高反而会让你先执行,结果找不到节点。
4. 调试时看“整棵链”,不要只看一份 XML
复杂项目里,视图问题几乎从来不是单文件问题。
你需要把它当成:
一个由多个模块共同维护的组合树
而不是“我这一段 XML 为什么没生效”。
一句话记住
如果只记一条,就记这句:
Odoo 视图继承不是把每个扩展分别贴到原视图上,而是沿着继承树按特定顺序不断修改同一份组合结果。
所以:
priority决定谁先参与primary决定谁是后续主树- 前置扩展会改变后置 XPath 的操作对象
这就是为什么在 Odoo 里,视图问题经常不是“单点错误”,而是“组合顺序错误”。
你一旦用这个视角看问题,很多诡异现象都会突然合理起来。
DISCUSSION
评论区