先把问题收窄
很多人一看到多公司校验,就会把它理解成一句话:
只允许当前公司记录,别家公司一律不行。
这句话只对了一半。
真正让新手最容易误判的,不是 _check_company_auto 有没有开,而是 Odoo 明明在做公司一致性校验,却又故意放行两类看起来像“例外”的记录:
company_id = False的共享记录res.company自身的一些特殊场景
如果你没把这两个口子看懂,项目里就会经常出现两种相反误判:
- 以为系统漏拦了,其实那是官方允许的共享主数据
- 以为某条关系一定能挂,结果 root company 约束又把你挡回来了
这篇只讲这一件事:_check_company_domain() 到底把谁放进“合法集合”,以及这个集合为什么不是你想象中的“只有当前公司”。
_check_company_domain() 真正返回的是什么
在 odoo/orm/models.py 里,_check_company_domain() 的默认逻辑非常短:
- 没有 company 时,只接受
company_id = False - 有 company 时,接受
company_id in allowed_companies + [False]
也就是说,Odoo 的默认答案从一开始就不是:
- 只能等于当前公司
而是:
- 当前允许公司可以
company_id=False也可以
这就是很多共享主数据能跨公司挂接的根本原因。
比如某些全局模板、公共配置、共享基础资料,看起来像“没公司”,实际上是 被框架明确定义成可跨公司引用。
所以你在调试时看到一条记录跨公司没报错,第一反应不该是“权限失效了”,而应该先问:
这条记录是不是本来就属于共享记录语义?
为什么 company_id=False 不是漏洞
很多实现者对 False 有天然警惕,觉得“没公司归属”就是脏数据。
但在 Odoo 多公司设计里,False 很多时候是一个明确业务语义:
- 这不是“丢了公司”
- 而是“对所有公司共享”
所以 _check_company_domain() 把 [False] 加到允许集合,不是放水,而是框架层面的折中:
- 既保护公司隔离
- 又不把公共主数据全部切碎
如果没有这条规则,很多跨公司共用的基础对象都会变得非常难用; 如果你把这条规则忘了,又会误把共享数据当成越权。
_check_company() 为什么仍然会把你拦住
虽然允许集合里有 [False],但 _check_company() 仍然会严格比较:
- 当前记录所属公司
- 关联字段实际指向的记录公司
- 以及该记录是否满足
_check_company_domain()
也就是说,Odoo 并不是“只要 related record 有 company_id 就随便放”。
真正的判定逻辑是:
- 先确定当前记录对应哪组公司
- 再对关联记录套
_check_company_domain() - 只有落在允许域里的记录才合法
因此下面两种情况会得到不同结果:
- 关联到别家公司记录 → 一般拦截
- 关联到共享记录(
company_id=False)→ 默认允许
看似都不是“本公司记录”,但语义完全不同。
res.company 为什么又是一个特殊分支
fields_relational.py 的 _description_domain() 里还有一个很容易被忽略的细节:
- 如果字段所在模型本身就是
res.company - 生成的 company domain 会直接用当前记录的
id
也就是源码注释写的那个意思:
- 在
res.company上使用check_company=True - company 不是来自
company_id字段 - 而是来自当前公司记录本身
这说明 Odoo 没有把 res.company 当成普通业务模型处理。
因为公司模型自己就是“公司边界”的定义者,它不能再机械地依赖一个额外的 company_id 字段来判定自己属于谁。
这也是为什么多公司开发里,公司主数据自身的关系字段 往往不能用普通业务单据那套直觉去理解。
前端 domain 为什么也会体现这个例外
很多人只在后端 _check_company() 里找答案,但 fields_relational.py 的 _description_domain() 其实会把相同思路提前注入到字段描述里。
如果模型有 company_id 或 company_ids,它会生成类似:
- 有公司时用
_check_company_domain(公司集合) - 没公司时退回
_check_company_domain('')
这个设计的意义是:
- 前端下拉候选尽量别把明显不合法的记录展示出来
- 但共享记录仍应出现在可选列表里
所以你在界面里看到“没有 company_id 的记录也能选”,不要立刻怀疑 domain 写错了。很多时候,这正是 Odoo 对共享记录的标准行为。
最容易踩的 3 个误判
1)把 company_id=False 当成脏数据
不一定。它可能是共享记录的正式语义。
2)把 res.company 上的关系字段当普通业务字段看
不行。公司模型自己就是边界定义者,源码里有单独分支。
3)看到前端 domain 放行共享记录,就以为后端不会校验
也不对。后端仍然会用 _check_company() 兜底,只是允许集合本来就包含共享记录。
一套更靠谱的排查顺序
如果你遇到“这条跨公司关系到底该不该过”,建议这样查:
- 关联字段是否
check_company=True - 关联对象的
company_id是当前公司、别家公司,还是False - 当前模型是不是
res.company - 字段描述里的默认 domain 有没有把共享记录放进候选
- 最终
_check_company()套上的 allowed companies 是哪一组
按这个顺序查,很多“看起来像特例”的行为都会变得很可解释。
总结
Odoo 多公司校验里最容易让人误判的,不是“为什么它会报错”,而是:
- 为什么有些跨公司关系居然没报错
- 为什么
company_id=False反而是合法候选 - 为什么
res.company自己又像走了另一套路
如果只记一句,就记这句:
_check_company_domain()从来就不是“只允许当前公司”,而是“允许当前公司 + 共享记录,并在公司模型上保留特殊语义”。
把这层边界看清以后,多公司关系里的很多“例外”,其实都不是例外,而是设计本意。
DISCUSSION
评论区