很多人第一次排查 Odoo 表单默认值时,脑子里只有一句话:
这个字段不是写了
default=吗,为什么页面上没有?
但真正打开 /home/ubuntu/odoo-temp/odoo/orm/models.py 里的 default_get(),你会发现 Odoo 对“默认值”这件事的理解,比单个字段属性复杂得多。
它并不是只看字段定义,而是按一条很明确的优先链路去找值:
context里的default_xxxir.default里给当前用户 / 当前公司配置的默认值- 字段本身的
field.default company_dependent字段的 fallback 默认值- 如果字段是 inherited 字段,再委托父模型继续做
default_get
所以新手最容易踩的坑不是“不会写 default”,而是:
把默认值来源想得太单一,结果在错误的层次上排查。
一、default_get() 不是随便拼字典,而是按优先级逐层截胡
源码里 default_get(self, fields) 的主循环非常清楚。对每个字段名,它会按顺序判断:
1)先看 context
只要上下文里有 default_<field_name>,这个值就直接拿走,并 continue。
这意味着:
- action context 里塞的默认值,优先级最高;
- 向导打开表单时带入的
default_partner_id、default_company_id,通常会压过模型里自己写的默认值; - 你在 Python 里测试
field.default明明正常,但界面里看到别的值,很可能不是字段错了,而是 action 或 button 的 context 先截胡了。
这是 Odoo 的一个重要设计:
“当前操作意图”优先于“模型通用默认值”。
因为用户此刻是从某个入口打开表单,入口上下文本来就应该最懂这次创建动作想要什么。
二、ir.default 比字段 default= 更像“用户偏好层”
如果 context 没给值,源码第二步会读取:
self.env['ir.default']._get_model_defaults(self._name)
而 /home/ubuntu/odoo-temp/odoo/addons/base/models/ir_default.py 里的 _get_model_defaults() 也很值得看。
它会按当前:
uidcompany_idmodel_name- 可选条件
condition
去取默认值,并按 SQL 结果顺序保留最高优先级项。
这说明 ir.default 的定位不是“模型代码逻辑”,而更像:
- 用户习惯
- 公司习惯
- 后台配置出来的默认表单行为
所以如果你遇到这些现象:
- 同一个模型,不同用户新建时默认值不同;
- 同一个用户,切公司后默认值变了;
- 字段没有写
default=,但界面总是自己带某个值;
第一反应就不该只去搜字段定义,而该去看是不是有 ir.default 在生效。
三、为什么 field.default 反而排在后面
很多人会惊讶:字段定义里的 default=,竟然不是最前面。
源码里它排在:
- context 之后
- 非
company_dependent的ir.default之后
这其实非常合理。
field.default 更像“模型作者给出的通用兜底建议”;
而 context 和 ir.default 更像“这次操作 / 这个用户 / 这家公司当前真实想要的默认值”。
也就是说,Odoo 把优先级排成了:
具体场景 > 用户/公司偏好 > 模型通用默认
这就是为什么很多业务系统里,你应该把“本次动作专属默认值”放进 action context,而不是指望一个通用字段 default 统治所有入口。
四、company_dependent 字段为什么要单独走 fallback
default_get() 里有个很容易被忽略的分叉:
- 非
company_dependent字段,先查ir.default company_dependent字段,字段默认值之后再查 fallback
这不是多余,而是在尊重 company-dependent 字段的含义。
这类字段本来就带“按公司分层”的语义。Odoo 不希望太早把它们当成普通字段默认值处理,而是让:
- 显式上下文先说了算
- 字段自己定义的默认逻辑先试一次
- 最后再回到公司维度 fallback
这能避免一种常见误判:
- 你以为是字段默认值没跑;
- 实际上是公司依赖字段最后用 fallback 接住了;
- 或者你以为切公司不影响新建默认;
- 实际上 company-dependent 默认本来就是按公司语义取值。
五、inherited 字段为什么有时像“晚一步才出现默认值”
default_get() 的最后一段会把 inherited 字段收集到 parent_fields,然后再:
defaults.update(self.env[model].default_get(names))
这说明通过 _inherits 暴露出来的字段,不是和当前模型字段完全平铺等价处理的。
Odoo 会先完成当前模型能直接确定的默认值,再把 inherited 字段委托给父模型去算。
这带来两个实战结论:
1)父模型默认值不会自动“抢在最前面”
如果你是在子模型上打开表单,当前模型的 context / ir.default / 字段默认值先参与;
父模型 inherited 字段默认值是后续委托补上的。
2)排查 inherited 字段默认异常时,不要只盯当前模型
你可能在子模型上看到一个 inherited 字段没默认值,但真正逻辑写在父模型:
- 父模型
default_get - 父模型字段
default= - 父模型自己的
ir.default
如果只在子模型找,往往会越找越乱。
六、源码还顺手处理了一个前端常见坑:x2many 默认值格式
default_get() 里还有一段特别实用的注释:
- 它故意不直接用
_convert_to_write()处理 x2many 默认值; - 因为像
[(Command.LINK, 2), (Command.LINK, 3)]这种结构,web client 作为默认值并不稳定; - Odoo 会先走 cache,再转成更适合前端消费的写法,比如归一成
Command.SET风格。
这背后的意思是:
默认值不仅要“逻辑上对”,还要“前端能吃得下”。
所以有时候你在 shell 里看一个默认值像是合法的 ORM 命令,前端却没有按预期展示,并不一定是字段定义错了,而可能是默认值格式没有被规范到 web client 能稳定处理的形态。
七、实战里怎么排查“默认值不对”
推荐按这个顺序查:
- 先看入口 action / button / wizard 有没有
default_xxx - 再看后台有没有
ir.default - 再看字段
default=或模型自定义default_get - 如果字段是
company_dependent,切公司复测 - 如果字段来自
_inherits,继续追父模型 - 如果是 x2many,顺手检查默认值命令格式是否适合前端
这个顺序的好处是,你是在沿着 Odoo 的源码顺序排查,而不是按自己的主观猜测乱跳。
八、最容易误解的一点:默认值不是“模型真相”,而是“创建起点”
最后一定要分清:默认值解决的是“新记录创建时先带什么”,不是“字段以后永远应该是什么”。
所以以下几件事不要混为一谈:
default_get()给的初始值onchange后界面动态改出来的值create()/write()最终真正落库的值- 约束和权限把某些值挡掉后的结果
很多“默认值失效”的 bug,最终根本不是默认值问题,而是:
- onchange 又覆盖了一次;
- create 里又重算了一次;
- 权限或多公司过滤让候选值不可用;
- inherited 父模型逻辑在别处补值。
结语
default_get() 这段源码看起来不长,但它把 Odoo 一个很核心的产品观表达得很清楚:
- 当前入口上下文最重要;
- 用户 / 公司偏好其次;
- 模型作者给的字段默认再其次;
- inherited 字段仍由真正拥有它的父模型负责;
- 前端能否稳定消费默认值,也被纳入设计范围。
所以以后再看到“这个字段明明写了 default 为什么没生效”,更准确的问题应该是:
这次新建动作里,到底是谁在默认值链路上先拿到了发言权?
DISCUSSION
评论区