结论先行
domain_force 不是“用户搜索时额外附加的一段 domain”那么简单。
在 Odoo 里,它真正扮演的是:
- 记录规则的原始表达式来源
- 会先经过
safe_eval() - 再按 全局规则 AND、分组规则 OR、最后整体再 AND 的方式拼装
- 还可能叠加
_inherits父模型的规则 - 并且结果会按用户、sudo 状态、模型、操作类型和公司上下文做缓存
所以很多人感觉“这条 rule 明明写了,怎么像没生效”,通常不是 domain_force 没执行,而是 被别的规则一起组合之后,结果已经不是你脑子里那条单独 domain 的语义了。
先看源码:domain_force 在哪里被消费
在 /home/ubuntu/odoo-temp/odoo/addons/base/models/ir_rule.py 里,ir.rule 定义很直接:
domain_force = fields.Text(string='Domain')
关键不在字段定义,而在这几个方法:
_eval_context()_get_rules()_compute_domain()_check_domain()
其中 _eval_context() 给 safe_eval() 提供的上下文非常关键:
return {
'user': self.env.user.with_context({}),
'company_ids': self.env.companies.ids,
'company_id': self.env.company.id,
}
这意味着你在 XML 或后台里写的:
[('company_id', 'in', company_ids)]
[('user_id', '=', user.id)]
并不是“魔法变量”,而是 Odoo 在求规则时专门喂进去的上下文对象。
第一步:先挑出“哪些规则有资格参与本次计算”
_get_rules(model_name, mode='read') 会先按模型和操作类型筛一遍:
- 当前模型是否匹配
- 规则是否激活
- 当前操作是否命中
perm_read / perm_write / perm_create / perm_unlink - 规则是不是全局规则
- 如果不是全局规则,当前用户是否在该规则的 group 里
这一步非常重要。
因为很多人误以为:
“我给某个组写了一条 rule,它就会和所有规则一起 AND。”
其实不是。Odoo 先筛出本次真的适用的规则,后面才谈组合逻辑。
第二步:全局规则 AND,分组规则 OR
真正的核心在 _compute_domain():
for rule in rules.sudo():
dom = Domain(safe_eval(rule.domain_force, eval_context)) if rule.domain_force else Domain.TRUE
if rule.groups:
group_domains.append(dom)
else:
global_domains.append(dom)
if group_domains:
global_domains.append(Domain.OR(group_domains))
return Domain.AND(global_domains).optimize(model)
翻译成人话就是:
- 没有 group 的规则,算全局规则,彼此之间是 AND
- 有 group 的规则,只把当前用户命中的那些拿出来,彼此之间是 OR
- 如果存在分组规则,先把它们 OR 成一大块,再跟全局规则整体 AND
这就是为什么:
- 多条全局 rule 往往越加越“窄”
- 多条组内 rule 往往是在“放行多个入口”
- 但这些被放行的入口,仍然逃不过全局 rule 的总闸门
一个非常常见的误解是:
“我给销售组加了一条能看自己单据的规则,为什么还是看不到?”
答案常常是:还有一条全局规则先把范围锁死了。
第三步:_inherits 父模型规则也会被带进来
_compute_domain() 前面还有一段很容易被忽略的逻辑:
for parent_model_name, parent_field_name in model._inherits.items():
if domain := self._compute_domain(parent_model_name, mode):
global_domains.append(Domain(parent_field_name, 'any', domain))
意思是:如果当前模型用了 _inherits,父模型上的记录规则也会被递归计算,再通过父字段折进来。
这会带来两个很实战的现象:
- 你明明只给子模型配了 rule,却还是被父模型卡住
- 你以为自己在调一个业务模型,实际命中的是底层父模型安全边界
所以,排查 _inherits 模型权限时,只盯当前模型的 ir.rule 往往不够。
第四步:domain_force 不是原样 SQL,而是 safe_eval + Domain.validate
_check_domain() 会先做校验:
domain = safe_eval(rule.domain_force, eval_context)
Domain(domain).validate(model)
这说明两件事:
1)domain_force 本质是 Python 表达的 domain 结构
它不是数据库 SQL 片段,也不是字符串拼接搜索条件。
2)就算 safe_eval 通过,也还要过 Domain 结构校验
也就是说,下面两种问题都可能报错:
- 语法能跑,但字段路径不合法
- 字段存在,但运算符或值结构不合法
因此写记录规则时,最稳的思路不是“先把表达式写复杂”,而是:
- 先写最小可用 domain
- 再一点点加条件
- 每次都验证真实生效范围
第五步:为什么切公司后结果会变
_compute_domain() 被 ormcache 缓存,但缓存 key 里专门包含:
'allowed_company_ids'
这意味着同一个用户、同一个模型、同一个操作,只要切换了激活公司集合,记录规则结果就可能不同。
这也解释了很多多公司环境下的“玄学”问题:
- 同一个账号上午能看,下午切了公司就不能看
- 不是 rule 被删了,而是 缓存命中条件和 eval context 里的公司集合变了
实战里最容易误解的 5 件事
1)把分组规则当成 AND
错。分组规则之间默认是 OR。
2)以为没写 group 就是“默认所有员工都能看”
不只是“所有员工都能看”,而是 所有命中此模型与操作的用户都要被它约束。这通常比想象中更强。
3)以为 sudo 还能继续测试规则
_get_rules() 里如果 self.env.su,直接返回空规则集。也就是说:
sudo()经常不是“我想看看规则算出来啥”- 而是“我直接绕过这层规则了”
4)只看搜索结果,不看 create/write/unlink 权限位
记录规则不是只管 read。你可能 read 没问题,但 write 被另一套 rule 卡住。
5)把前端 domain 和记录规则混为一谈
字段 domain、窗口 action domain、search view filter、record rule 都会影响“你最终看见什么”,但它们不在同一层。
其中 domain_force 属于 安全边界层,优先级和后果都比普通 UI 过滤重。
一套够用的排查顺序
如果你怀疑 domain_force 没生效,建议按这个顺序查:
- 当前操作是什么:read、write、create 还是 unlink
- 当前用户命中了哪些 group rule
- 是否还有全局 rule 在更外层做 AND
- 模型是否带
_inherits,父模型 rule 是否一起进来了 - 当前激活公司集合是不是变了
- 是不是用了 sudo,导致你根本没在测记录规则
总结
真正理解 domain_force,关键不是背“它是记录规则的 domain”。
关键是记住这张心智图:
domain_force提供原始规则表达式safe_eval()用user / company_id / company_ids求值- 全局规则 AND
- 分组规则 OR
_inherits父模型规则继续叠加- 结果按用户与公司上下文缓存
一旦把这套链路看清,很多权限问题就不再玄学,而会变成很具体的组合逻辑问题。
DISCUSSION
评论区