前端

Odoo Many2one 为什么不是“输几个字弹个下拉框”:自动补全、Quick Create 与 Search More 链路讲透

很多人把 Odoo 的 many2one 字段理解成一个带搜索的下拉框:输入关键字、调 `name_search`、选中一项、结束。但从 `many2one_field.js`、`many2one.js`、`record_autocomplete.js` 看,官方实际维护的是一条关系选择链路:字段选项决定是否允许创建/编辑/快速创建,动态 domain 与上下文在每次搜索前现算,候选结果会进入 name service 缓存,超出上限后再切到 Search More 对话框。本文讲清 many2one 为什么远比“输入框 + 下拉”复杂。

前端
进阶 开发者 2 分钟阅读
0 评论 0 点赞 0 收藏 5 阅读

如果只看表面,Odoo 里的 many2one 字段很容易被理解成:

  • 一个输入框;
  • 配一个下拉建议列表;
  • 选中后回填一条记录。

但只要你真正做过二开,就会很快发现 many2one 从来不只是“自动补全输入框”。

你会不断遇到这些问题:

  • 为什么有的字段能直接新建,有的只能搜不能建;
  • 为什么有时会出现 “Create” 和 “Create and Edit”,有时没有;
  • 为什么同一个字段在不同记录上搜索结果不一样;
  • 为什么一开始只给 7 条结果,后面又弹出 Search More;
  • 为什么某些结果选过一次后,别处又能秒显示 display_name;
  • 为什么扫码、只读、链接跳转也跟 many2one 放在一起。

如果去读 many2one_field.jsmany2one.jsrecord_autocomplete.js,你会看到 Odoo 的真实设计是:

many2one 不是一个 UI 小组件,而是一条关系记录选择与创建链路。

一、字段层先决定“允许做什么”,再决定“怎么搜”

many2one_field.js 最值得看的不是模板,而是 extractM2OFieldProps()

它会把字段静态信息、动态信息和 options 汇总成一组能力开关:

  • canCreate
  • canCreateEdit
  • canQuickCreate
  • canOpen
  • canWrite
  • canScanBarcode
  • searchThreshold
  • nameCreateField
  • placeholder

这一步非常关键,因为它说明 many2one 的第一层问题不是“候选怎么显示”,而是:

当前用户、当前字段、当前视图配置,到底被允许完成哪些关系动作。

例如:

  • no_create 直接关闭所有创建路径;
  • no_quick_create 禁掉基于输入文字直接创建;
  • no_create_edit 禁掉通过弹窗表单创建;
  • no_open 禁掉已选记录的打开能力。

也就是说,many2one 先是一个关系动作权限壳,然后才是搜索框。

二、computeM2OProps() 说明 domain 和 context 都是运行时现算

many2one.js 里的 computeM2OProps() 很值得细看。

它不是直接把 domain、context 从 field props 原样往下传,而是做了两层运行时计算:

1)domain 是函数,不是死值

domain: () => getFieldDomain(record, name, fieldProps.domain)

这说明 many2one 搜索条件不是一个固定数组,而是和当前 record 状态绑定。比如:

  • 当前公司;
  • 当前 partner;
  • 当前单据类型;
  • 甚至临时虚拟 ID 的 eval context。

这些都会影响“这一刻该搜哪些记录”。

2)openActionContext 也要合成

它会把:

  • openActionContext
  • 字段本身 context
  • record 当前 evalContext

组合成真正用于打开记录动作的上下文。

这说明 many2one 不是选完值就结束,它还要考虑:

  • 之后打开记录时进入什么上下文;
  • 创建表单默认值从哪来;
  • 不同业务单据下 relation 记录怎么继承环境。

三、Many2One 组件真正维护的是“选择 + 创建 + 打开”的统一体验

Many2One 组件里最能体现架构思路的是 activeActionsmany2XAutocompleteProps

它把当前关系字段能力整理成:

  • create
  • createEdit
  • write

然后传给自动补全层。

注意这意味着自动补全不是一个独立的“搜列表”,而是必须知道:

  • 这个字段是否允许 quick create;
  • 是否允许 Create and Edit;
  • 选中后如何更新 record;
  • 打开现有记录时走 action、dialog 还是 tab。

换句话说,many2one 的补全层从一开始就是服务于关系编辑工作流的,不是通用下拉菜单。

四、为什么选中值存的是 {id, display_name},而不是只存 id

源码里的 extractData(record) 很简单,却特别说明问题。

many2one 选中后并不只保留一个 id,而是希望立刻拿到:

  • id
  • display_name

原因很现实:

  • 字段渲染立刻要显示名字;
  • 某些 name 会有多行,需要拆 extraLines;
  • 之后打开记录、恢复状态、重新渲染时都需要稳定显示文本。

如果只存 id,界面到处都得额外补读一次 display_name,交互会变得很脏。

这也是记录保存后 onRecordSaved() 还会再 read(display_name) 一次的原因:

  • 新建或编辑完后,many2one 需要拿到最新、规范的显示名;
  • 不能只相信输入框当时那段文本。

record_autocomplete.js 里有几层很值得注意:

1)结果数量故意受限

默认 SEARCH_LIMIT = 7,多一条只是为了判断要不要显示 “Search More...”。

这说明官方有明确取舍:

  • 输入下拉只适合快速决策;
  • 不是给你把整库数据都塞进 dropdown。

2)结果会进入 nameService

addNames() 会把 [id, label] 写入 name service。

这很关键,因为 many2one 不是只在一个输入框里短暂显示结果。关系记录名称一旦查出来,系统后续很多地方都能复用,避免重复补读。

3)旧请求会被主动 abort

loadOptionsSource() 每次搜索前,会把上一次 lastProm.abort(false)

这和前面模型层讲的竞态控制是一个思路:

  • 用户输入很快;
  • 搜索请求很多;
  • 如果不取消旧请求,结果顺序就可能乱掉。

对于自动补全,这种问题尤其明显,因为用户眼睛盯着的是“当前输入对应的候选”,而不是某个历史关键词的结果。

六、为什么要有 Search More 对话框

当结果超过短列表承载范围时,Odoo 没继续往 dropdown 里硬塞,而是切到 SelectCreateDialog

onSearchMore() 里会:

  • 先基于当前 quick search 结果生成动态过滤;
  • 再把它作为 dialog 的 dynamicFilters
  • 同时保留当前 domain、context、multiSelect 等条件。

这说明 Search More 不是“打开另一个无关页面再搜一次”,而是:

把当前输入上下文升级成一个更完整的关系选择界面。

它承接的是同一条选择链路,只是从“轻量下拉”升级成“完整搜索对话框”。

七、Quick Create 为什么危险又必要

many2one 的 quick create 一直是最容易被误解的地方。

很多人喜欢把它理解成“没搜到就顺手建一个名字一样的新记录”。

但源码设计其实很谨慎:

  • 能不能 quick create,要先看字段级能力;
  • 与 Create and Edit 分开;
  • 真正保存后还会重新读记录并规范化显示名;
  • 某些场景只能弹完整表单,不能直接快建。

这背后的原因很好理解。

关系记录常常不只是一个名字,它可能还需要:

  • 公司;
  • 税号;
  • 联系方式;
  • 分类;
  • 权限或业务前提。

所以 quick create 适合“名字已足够建立最小记录”的关系,不适合所有模型无脑开启。

八、二开 many2one 时最容易犯的错

误区 1:把 many2one 当成普通 AutoComplete 复用

如果你忽略它背后的 domain/context/create/open 工作流,很快就会发现字段行为和标准系统不一致。

误区 2:把 domain 写成静态值

很多时候 many2one 的筛选依赖当前 record 实时状态,静态化后会出现“看上去能搜,但结果不对”。

误区 3:只保存 id,不维护 display_name

表面省事,后面渲染、恢复、链接显示、保存后刷新都会多出很多补丁代码。

误区 4:Search More 丢失当前上下文

如果弹出的完整搜索没有继承 quick search 的 domain/context,用户会觉得“下拉里和弹窗里像两个系统”。

九、结论

Odoo many2one 远不是“输入几个字弹个下拉框”。

从源码看,它真正维护的是一条完整的关系选择链路:

  • 字段层先确定创建、编辑、打开、扫码等能力;
  • 运行时基于当前 record 现算 domain 和 context;
  • 自动补全层做受限搜索、名称缓存与请求取消;
  • 结果过多时再升级到 Search More 对话框;
  • 必要时接上 Quick Create 或完整创建表单;
  • 最后把选中的关系记录规范成 {id, display_name} 回写页面。

所以更准确的理解应该是:

many2one 不是“搜索型下拉框”,而是 Odoo Web Client 里最常见的一条关系记录发现、选择、创建与回填工作流。

理解这条链路之后,你改 many2one 就不会只想着“下拉怎么渲染”,而会开始先问:

  • 当前关系动作权限是什么;
  • domain/context 是怎么在运行时形成的;
  • 结果列表何时该升级到 dialog;
  • quick create 到底是不是这个模型该开的路。

DISCUSSION

评论区

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