先说结论
Odoo 里“新建一条记录”或“弹出一个向导”时,默认值并不是只来自某一个地方,而是按一条很明确的顺序合并出来的:
context里的default_xxxir.default里的用户/公司默认值- 字段自身的
field.default - 公司相关字段(
company_dependent)的 fallback 默认值 - 继承父模型的默认值
而 active_model、active_id、active_ids 本身不是默认值,它们更像是“初始化时给向导看的上下文线索”。很多向导会在 default_get() 里读取这些线索,再决定应该返回哪些真正要落到字段里的默认值。
所以你看到的现象通常是:
default_partner_id这种 直接塞值,优先级最高active_ids这种 上下文线索,要靠default_get()或 onchange 再翻译成字段值field.default往往只是兜底ir.default适合做“这个用户/公司平时默认选什么”
一、官方源码里,default_get() 到底按什么顺序找默认值
在 /home/ubuntu/odoo-temp/odoo/orm/models.py 里,default_get() 的实现非常直白:
def default_get(self, fields):
defaults = {}
ir_defaults = self.env['ir.default']._get_model_defaults(self._name)
for name in fields:
key = 'default_' + name
if key in self.env.context:
defaults[name] = self.env.context[key]
continue
field = self._fields.get(name)
if not field:
continue
if not field.company_dependent and name in ir_defaults:
defaults[name] = ir_defaults[name]
continue
if field.default:
defaults[name] = field.default(self)
continue
if field.company_dependent and name in ir_defaults:
defaults[name] = ir_defaults[name]
continue
这段代码其实已经把很多争议一次性讲清了。
1)context 里的 default_xxx 优先级最高
只要上下文里有 default_partner_id,那它就先赢。
这也是为什么很多按钮动作会写:
return {
'type': 'ir.actions.act_window',
'res_model': 'some.wizard',
'view_mode': 'form',
'target': 'new',
'context': {
'default_partner_id': self.partner_id.id,
},
}
这种写法不是“建议”,而是显式覆盖。
2)ir.default 是“用户/公司级偏好默认值”
ir.default._get_model_defaults() 会把当前用户、当前公司范围内可用的默认值读出来。
它的 SQL 顺序里,会综合:
- 当前用户 or 所有用户
- 当前公司 or 所有公司
- 条件是否匹配
因此 ir.default 更像“后台配置出来的习惯值”,例如:
- 某用户新建发票时默认某本日记账
- 某公司下默认某个仓库/币种/团队
3)field.default 只是字段级默认
字段自己也可以定义默认值,例如:
company_id = fields.Many2one(default=lambda self: self.env.company)
但它排在 context 和普通 ir.default 之后,所以很多人以为“我字段上已经写了 default,为什么没生效”,其实只是被更高优先级覆盖了。
4)继承字段还会继续向父模型要默认值
default_get() 最后会把 inherited field 收集起来,再委托父模型继续补默认值。
这意味着:
你打开的是当前模型的表单,但默认值链路可能会一路追到父模型。
二、active_model、active_id、active_ids 为什么总在向导里出现
很多开发者第一次写向导时,会以为:
active_id会自动写进某个字段active_ids会自动变成 one2many / many2many
其实不会自动写。
它们只是 action context 里的一组“当前操作对象”指针。
典型例子:
- 从销售单点“创建发票”
- 从多条记录点“批量处理”
- 从列表勾选若干条记录后弹出 wizard
系统通常会把这些上下文带进来:
{
'active_model': 'sale.order',
'active_id': 42,
'active_ids': [42, 43, 44],
}
然后由 wizard 在 default_get() 里自己解释:
- 我是不是从
sale.order打开的? - 是单条还是多条?
- 要不要把伙伴、公司、币种、明细行先带进来?
也就是说:
active_ids决定“你是从谁来的”,default_get()决定“我要把什么真正填进字段里”。
三、为什么很多向导默认值不是写在字段 default= 上,而是写在 default_get() 里
因为向导默认值往往依赖当前场景,而不是一个静态常量。
字段 default 适合:
- 当前公司
- 当前用户
- 固定布尔值
- 稳定的环境参数
但下面这些通常要进 default_get():
- 需要根据
active_model分支处理 - 需要读取
active_ids里的记录做聚合 - 需要做权限判断
- 需要根据多条记录是否同公司/同伙伴决定是否允许继续
- 需要预填 x2many 行
例如一个批量向导,如果你从 20 条发票打开,它可能会在 default_get() 里先:
- 读出
active_ids - browse 出这些发票
- 检查是否同公司、同币种、同伙伴
- 再算出默认 journal、payment date、amount 或待处理行
这类逻辑如果硬塞到字段 default=,很快就会失控。
四、Form.from_action() 也说明了 action context 才是向导初始化的起点
Odoo 的测试工具 odoo/tests/form.py 里有个很有代表性的入口:
record = env[action['res_model']]\
.with_context(context)\
.browse(action.get('res_id'))
return cls(record, view_id)
这段代码的含义很关键:
- action 上的
context会先灌进环境 - 然后才开始构造表单
- 表单初始化时再去跑默认值与 onchange
所以很多“为什么浏览器里点按钮有默认值,我在 shell 里 create({}) 却没有”的根因,其实就是:
浏览器打开 form / wizard 时走的是 action + context + default_get + onchange,而你手写
create()只走了创建链路,并没有模拟前端打开表单时那层初始化语义。
五、x2many 默认值为什么经常长得不像 write() 命令
default_get() 里还有一段很容易被忽略的注释:
它明确避免直接对 x2many 使用
_convert_to_write(),因为 web client 不支持某些原始写入格式,而是要把它们规范成客户端能吃的命令。
这点在测试里也能看到。
test_default_x2many() 里:
- context 传入的是
default_tags=[Command.set(tag.ids)] default_get(['tags'])返回的也是面向默认值初始化的命令格式- 但
onchange()结果里又可能进一步转成前端需要的表现形式
这意味着:
- 给数据库写值,和
- 给表单初始化默认值
虽然都长得像 command list,但它们不完全是同一种语义层。
实战里最容易踩坑的是:
- 你以为向导默认值里可以随便塞任意 x2many command
- 结果前端打开表单时报错,或者值不显示
稳妥做法是:
- 尽量使用
Command.set()/Command.create()这类规范命令 - 复杂默认值尽量通过
default_get()返回,而不是在按钮 action 里硬拼一大坨奇怪结构
六、最常见的误区
误区 1:active_id 会自动写进 res_id
不会。
res_id 是“编辑已有记录”,active_id 是“当前操作来源”。二者完全不同。
res_id:打开某条已有 wizard / 记录active_id:告诉新开的 wizard 你是从谁点进来的
误区 2:字段上写了 default=,就一定能盖过一切
也不会。
context 里的 default_xxx 先级更高,普通 ir.default 也可能先命中。
误区 3:active_ids 是字段值
不是。它只是 context 线索。你必须自己在 default_get() 里把它转成真正字段值。
误区 4:shell 里 create() 的表现应该和界面打开向导一致
不一致。
界面里还有 action context、form 初始化、onchange、视图字段规格等一整层逻辑。
七、开发时该怎么选:default=、ir.default、default_get() 还是 action context?
可以这么分工:
用字段 default=
适合稳定、模型自身就能决定的默认值。
例如:
- 当前公司
- 当前用户
- 默认草稿状态
- 默认布尔开关
用 ir.default
适合“用户习惯”或“公司习惯”。
例如:
- 默认团队
- 默认仓库
- 默认日记账
用 action context 里的 default_xxx
适合“这次按钮点进来,我就是要强行预填成这个值”。
例如:
- 从某个 partner 点“新建机会”,直接带入 partner
- 从某张单据点“创建关联记录”,直接带 origin / company / currency
用 default_get()
适合需要根据上下文做判断和转换的情况。
例如:
- 读取
active_ids后聚合 - 校验多条记录能否一起处理
- 生成 x2many 默认行
- 动态区分单条/批量模式
八、一个最实用的心智模型
把 Odoo 表单初始化想成两层:
第一层:提供线索
- action context
default_xxxactive_modelactive_idactive_ids
第二层:把线索翻译成可显示、可编辑的字段值
default_get()field.defaultir.default- onchange
这样你调试时就不容易乱。
如果默认值不对,先问:
- context 里到底带了什么?
default_get()有没有把这些线索转成字段值?- 是不是被
default_xxx覆盖了字段 default? - 是不是
ir.default在偷偷生效? - 是不是前端 form 初始化和你 shell 里的复现方式根本不一样?
结语
default_get() 的本质不是“给字段补几个默认值”这么简单,它其实是 Odoo 在“新建记录 / 打开向导”时,把上下文、用户偏好、字段定义和业务场景拼成一个初始状态的总装配点。
理解这条链路后,你会更容易判断:
- 什么该放到 action context
- 什么该放到字段 default
- 什么必须留在
default_get() - 为什么
active_ids看起来什么都没做,但其实决定了整个向导是怎么长出来的
DISCUSSION
评论区