看板空列机制

Odoo 看板为什么能显示空列:group_expand、read_group 与阶段补全机制

很多人以为 Odoo 看板上的空列只是前端自己补出来的占位 UI,但 Odoo 19 实际把这件事做进了字段定义与 read_group 结果整形层:字段声明 group_expand 后,系统会主动把“当前没有记录的组”补进结果里。

Odoo 开发 前端
进阶 开发者 1 分钟阅读
0 评论 0 点赞 0 收藏 6 阅读

先说结论

Odoo 看板里那些“当前一张卡都没有、但仍然显示出来”的列,很多时候不是前端随便补的。

它们通常来自后端的 group_expand 机制。

也就是说,系统不是先把数据库真实分组结果原样扔给前端,再让前端凭空画空列; 而是:

  1. 后端先做 read_group / formatted_read_group 聚合;
  2. 判断当前分组字段有没有声明 group_expand
  3. 如果有,就额外求出“应该展示的全部组”;
  4. 把空组补进结果里;
  5. 再让前端按这个结果渲染列。

所以真正的结论是:

空列不是纯 UI 幻觉,而是 ORM 分组结果被有意识扩展后的产物。


一、group_expand 根本不是“看板专属小技巧”

odoo/orm/fields.py 里,字段对象有 group_expand 属性,determine_group_expand() 会去解析它。

这说明 group_expand 从设计上就不是前端 patch,而是字段元数据的一部分。

这点很重要。

因为它意味着:

  • 哪些组应该显示;
  • 空组是否值得保留;
  • 保留顺序是什么;
  • 是按 selection 值补,还是按 many2one 记录补;

这些都被视为数据分组语义的一部分,而不是某个具体页面的临时展示技巧。


二、Selection 字段为什么只写 group_expand=True 就够了

odoo/orm/fields_selection.py 有一段非常关键:

if attrs.get('group_expand') is True:
    attrs['group_expand'] = self._default_group_expand

这意味着对 Selection 字段来说,group_expand=True 不是布尔开关那么简单。

它会被自动替换成默认实现 _default_group_expand()

而这个默认实现做的事也非常干脆:

def _default_group_expand(self, records, groups, domain):
    return self.get_values(records.env)

意思就是:

  • 不关心当前库里有没有数据;
  • 直接把这个字段“定义上允许的全部选项值”拿出来;
  • 再让后续分组结果整形逻辑补全。

所以对 Selection 字段来说,空列之所以能出现,不是因为前端知道“还缺哪个状态”, 而是因为字段自己就声明了:

这些状态即使暂时没数据,也属于应展示的合法组。


三、Many2one 的 group_expand 又是另一套逻辑

到了 odoo/orm/models.py_read_group_fill_results(),Odoo 会区分:

  • relational 字段;
  • 非 relational 字段。

如果是 relational 字段,比如 many2one:

values = group_expand(self, groups, domain).sudo()

这句很关键。

它表示 many2one 分组扩展不是返回简单值列表,而是返回一个记录集

默认实现 _read_group_expand_full() 更直接:

def _read_group_expand_full(self, groups, domain):
    return groups.search([])

也就是说,一个 many2one 字段如果挂了这条默认扩展逻辑,系统会倾向于把目标模型里“应该显示的记录组”一并取回来。

这时看板里的“空列”就不再只是状态值,而可能是:

  • 还没有卡片的阶段;
  • 还没有任务的负责人组;
  • 还没有单据的某个业务分类对象。

所以 Selection 的补全更像“补枚举值”,Many2one 的补全更像“补记录对象”。


四、测试把行为差异写得非常清楚

test_web_group_expand.py 这组测试很适合当教材。

1)test_static_group_expand()

它验证了三件事:

  • 返回顺序要遵守字段定义顺序;
  • 没有记录的组也要出现;
  • False 组放最后。

这非常关键,因为它说明 group_expand 不是“只多补几行数据”这么简单,而是连组顺序语义都一起定义了。

2)test_no_group_expand()

没有 group_expand 时,结果只保留必要组,空组不会出现。

这就能直接证明:

空列不是默认行为,而是显式声明出来的行为。

3)limit / offset 测试

测试还证明了一个很多人没意识到的细节:

  • offset 存在时,可能不做 group_expand;
  • limit 太小、会把补全结果挤爆时,也可能不做完整扩展;
  • limit 足够时,才会把空组补回来。

这说明 Odoo 在这里并不是“功能优先不计代价”,而是在:

  • 展示完整列语义;
  • 控制查询与返回规模;

之间做平衡。


五、为什么这套设计比前端硬补列更好

如果让前端自己补空列,会遇到几个问题:

1)前端不知道合法组全集

Selection 还好说,Many2one 根本不可能只靠前端猜出“该展示哪几条记录”。

2)顺序与折叠信息不稳定

测试里 many2one 结果还会带:

  • display_name
  • fold
  • __extra_domain

这些都不是前端凭空能补对的。

3)不同视图会失去一致性

如果每个视图自己补,kanban、list、graph 之类的分组行为就会逐渐分叉。

而把它放到 read_group 结果层,统一性会更强。

所以 Odoo 的思路其实很成熟:

让字段声明“应显示哪些组”,让 ORM 负责把结果补齐,让前端只负责渲染。


六、开发时最容易误解的 3 个点

1)把 group_expand 理解成“显示全部分组结果”

并不完全对。

它显示的是“应该展示的组”,不是数据库里所有可能对象都无限铺开。

尤其碰到 limit / offset,行为会收敛。

2)以为它只影响看板

虽然最典型感知发生在 kanban 列,但根子在 formatted_read_group()_read_group_fill_results(),是后端分组结果层。

3)以为没数据的列是前端空壳

错。它们通常带着真实的 __extra_domain,点进去之后是能继续 drill-down 的。

也就是说,这不是装饰,而是可操作的空组结果


七、什么时候该考虑自定义 group_expand

如果你的业务对象存在下面这些需求,就值得考虑:

  • 即使当前没单据,也要展示完整阶段;
  • 某些候选组必须按固定业务顺序出现;
  • 分组列不是“数据库自然聚合结果”,而是“业务允许集合”;
  • 你需要让空列也能成为一个明确的投放或拖拽目标。

这时不要在前端硬写假列,更稳妥的做法是:

  • 在字段层声明 group_expand
  • read_group 结果自然长出这些列;
  • 保持 domain、顺序、fold、权限语义都一致。

结语

理解 group_expand 之后,你会发现 Odoo 看板空列的本质并不是“前端为了好看多画几列”。

它真正做的是:

  • 先承认“分组展示”不是数据库现状的机械投影;
  • 再允许字段和模型声明“哪些组具有展示资格”;
  • 最后由 ORM 把这些语义化空组补进结果。

所以一句话总结就是:

Odoo 的空看板列,本质上是后端分组语义的一部分,不是前端视觉补丁。

DISCUSSION

评论区

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