向导初始化

Odoo 向导一打开为什么就“自带答案”:default_get、context 与 active_ids 初始化链路讲透

从官方源码讲清 Odoo 打开向导或新建表单时,默认值为何会来自 context、ir.default、field.default,active_model/active_id/active_ids 又是怎样参与初始化的。

Odoo 开发 框架
进阶 开发者 3 分钟阅读
0 评论 0 点赞 0 收藏 16 阅读

先说结论

Odoo 里“新建一条记录”或“弹出一个向导”时,默认值并不是只来自某一个地方,而是按一条很明确的顺序合并出来的:

  1. context 里的 default_xxx
  2. ir.default 里的用户/公司默认值
  3. 字段自身的 field.default
  4. 公司相关字段(company_dependent)的 fallback 默认值
  5. 继承父模型的默认值

active_modelactive_idactive_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_modelactive_idactive_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() 里先:

  1. 读出 active_ids
  2. browse 出这些发票
  3. 检查是否同公司、同币种、同伙伴
  4. 再算出默认 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.defaultdefault_get() 还是 action context?

可以这么分工:

用字段 default=

适合稳定、模型自身就能决定的默认值。

例如:

  • 当前公司
  • 当前用户
  • 默认草稿状态
  • 默认布尔开关

ir.default

适合“用户习惯”或“公司习惯”。

例如:

  • 默认团队
  • 默认仓库
  • 默认日记账

用 action context 里的 default_xxx

适合“这次按钮点进来,我就是要强行预填成这个值”。

例如:

  • 从某个 partner 点“新建机会”,直接带入 partner
  • 从某张单据点“创建关联记录”,直接带 origin / company / currency

default_get()

适合需要根据上下文做判断和转换的情况。

例如:

  • 读取 active_ids 后聚合
  • 校验多条记录能否一起处理
  • 生成 x2many 默认行
  • 动态区分单条/批量模式

八、一个最实用的心智模型

把 Odoo 表单初始化想成两层:

第一层:提供线索

  • action context
  • default_xxx
  • active_model
  • active_id
  • active_ids

第二层:把线索翻译成可显示、可编辑的字段值

  • default_get()
  • field.default
  • ir.default
  • onchange

这样你调试时就不容易乱。

如果默认值不对,先问:

  1. context 里到底带了什么?
  2. default_get() 有没有把这些线索转成字段值?
  3. 是不是被 default_xxx 覆盖了字段 default?
  4. 是不是 ir.default 在偷偷生效?
  5. 是不是前端 form 初始化和你 shell 里的复现方式根本不一样?

结语

default_get() 的本质不是“给字段补几个默认值”这么简单,它其实是 Odoo 在“新建记录 / 打开向导”时,把上下文、用户偏好、字段定义和业务场景拼成一个初始状态的总装配点。

理解这条链路后,你会更容易判断:

  • 什么该放到 action context
  • 什么该放到字段 default
  • 什么必须留在 default_get()
  • 为什么 active_ids 看起来什么都没做,但其实决定了整个向导是怎么长出来的

DISCUSSION

评论区

想参与讨论?先 登录 再发表评论。
还没有评论,你可以成为第一个留言的人。