很多人第一次看 Odoo 企业版 Approvals,会以为它只是个统一审批壳:
- 建一个请求;
- 加几个人;
- 别人点批准或拒绝;
- 结束。
这只能解释界面,解释不了它为什么能承接企业里的真实审批规则。
把 enterprise/approvals/models/approval_request.py、approval_approver.py 和测试一起看,你会发现官方真正设计的是一套 “状态不是由单个按钮决定,而是由审批拓扑决定” 的机制。
它至少同时处理四层东西:
- 最低审批人数:不是有人点了就算过;
- 必审人:有些人必须签,不可被人数规则替代;
- 顺序审批:后面的人可能根本还没轮到;
- 待办活动队列:谁当前该处理,不是前端猜的,而是
mail.activity真正排出来的。
所以 Approvals 的本质不是“记录几个按钮结果”,而是“把审批门槛、审批顺序和待办责任同步到同一套状态机里”。
一、真正的核心不是 approver 列表,而是 request + approver 双层状态
请求本体 approval.request 有一个总状态:
newpendingapprovedrefusedcancel
但每个审批人 approval.approver 也有自己的状态:
newpendingwaitingapprovedrefusedcancel
这两个状态不是重复的。
request_status 回答:整单现在处于什么阶段
也就是业务上:
- 还没提交;
- 已提交流转中;
- 已通过;
- 已拒绝;
- 已取消。
approver.status 回答:这个审批人在流程里的位置
也就是:
- 还没开始;
- 轮到你处理;
- 还在排队;
- 你已通过;
- 你已拒绝;
- 这个请求已取消。
因此一个请求能是
pending,但当前用户看到的自己状态可能是waiting。
这正是“流程状态”和“个人位置状态”的区别。
二、为什么最低审批人数不是“凑够票数”这么简单
approval_minimum 是第一层门槛。
action_confirm() 在提交时会先检查:
- 当前 approver 数量是否至少达到最小值;
- 不够就直接报错,不让进入待审批。
但更关键的是后面的 _compute_request_status() 语义:
- 并不是某一个审批人点了
approved,整单就过; - 要看累计已批准人数是否达到最小值;
- 同时还得看必审人是否都满足。
测试里也明确覆盖了:
approval_minimum = 1时,一个人过就可能整体通过;approval_minimum = 2时,先过一个仍然只是pending。
这说明官方把审批门槛理解成:
请求是否通过,由“全局通过条件”决定,而不是由“最近那个点击动作”决定。
三、为什么 required approver 比 minimum 更硬
approval.approver.required=True 是第二层门槛。
这是很多人最容易误判的地方。
有时会直觉认为:
- 最低审批人数是 1;
- 已经有 1 个人通过;
- 那就应该通过。
但测试明确证明:
- 如果那个必审人还没批,整单仍然不能过;
- 即使你已经“凑够人数”,也只是
pending。
这说明 Odoo 把 required 理解成一种 结构性约束:
- 它不是“额外加一票”;
- 它是“这条链上必须经过这个节点”。
所以真正的通过条件不是:
approved_count >= minimum
而是更接近:
approved_count >= minimum- 且
所有 required approver 已批准
required approver 不是数量规则的一部分,而是数量规则之外的强制拓扑。
四、顺序审批为什么要引入 waiting
如果 category 开启 approver_sequence,审批人不是一起生效,而是按顺序推进。
action_confirm() 里会做这件事:
- 当前第一个可处理 approver →
pending - 后续 approver →
waiting
这说明系统不是简单把所有人一起丢到待处理队列,而是显式表达:
- 谁现在该审;
- 谁只是未来候选。
为什么不能都给 pending?
因为一旦都给 pending,会立刻带来两个问题:
- 责任混乱:到底现在谁该动?
- 顺序失真:后面的审批人可能抢先批准,破坏顺序规则。
所以 action_approve() 里还有一个保护:
- 如果当前用户状态是
waiting,直接抛错,不能越级审批。
waiting不是 UI 灰色标签,而是防止越序审批的业务锁。
五、为什么审批流要和 mail.activity 绑在一起
Approvals 不只改状态,还会调 _create_activity() 给当前 approver 建待办。
这点很关键。
很多轻审批系统把“待审提醒”当成附属通知。 但 Odoo 把它做成流程主体的一部分:
- 当前该审的人变成
pending; - 同时收到对应 activity;
- 如果拒绝/取消/切回草稿,相关活动也会被清理或反馈完成。
这意味着什么?
意味着审批不是“状态存在数据库里,用户自己想起来去看”。
而是:
- 状态 决定当前责任;
- activity 把责任派到人头上。
Approvals 真正联动的是状态机 + 待办队列,而不是状态机 + 邮件提示。
这也是它更像企业流程工具的原因。
六、为什么“撤回”不是简单改回 pending/new
action_withdraw() 做的不是一键回到初始,而是:
- 把后续 approver 重新打回
waiting; - 当前 approver 写回
pending; - 同时配合取消/重建活动链。
这表示 Odoo 把 withdraw 理解成:
从当前审批断点往回收,而不是重置整单历史。
这很重要,因为在企业审批里:
- 有时只是某个审批人撤回自己的批准;
- 并不代表前面发生过的节点都要被抹掉。
所以它不是“清空所有记录”,而是“按顺序回退流程位置”。
七、为什么拒绝会向后传播
action_refuse() 之后,_update_next_approvers() 会把后续相关 approver 也更新成 refused,并取消活动。
这说明官方的判断是:
- 一旦链路上某个关键节点拒绝;
- 后面未执行的审批也没有继续意义。
因此拒绝不是局部事件,而是终止事件。
这和批准不一样:
- 批准只会推进到下一个;
- 拒绝会把后续等待链一起压死。
这就是审批流里“正向推进”和“反向终止”的不对称性。
八、为什么确认时要校验经理、附件、最少人数
action_confirm() 一次性检查了很多前置条件:
- 若 category 要求经理审批,则 request owner 的员工档案必须有 manager,且 manager 还必须有 user;
- 若要求文档,必须至少有一个附件;
- 若 approver 不足最小人数,不让提交流转。
这说明 Odoo 把 Approvals 看成:
- 不是先放进流程再慢慢补资料;
- 而是提交瞬间就应满足进入流转的基本合法性。
confirm 在这里不是“保存”,而是“准入校验 + 流程启动”。
九、为什么 approved 请求不能删,只能 archive
源码明确限制:
- 已批准请求不能删除,只能归档。
这是很典型的企业审计边界。
审批通过后,记录已经不再只是草稿内容,而是:
- 一个发生过的业务决策;
- 可能关联附件、产品行、消息、活动反馈。
如果允许随手删,审批系统就失去最基本的可追踪性。
所以官方选择的是:
draft/pending可以更自由处理;approved进入“保留痕迹”模式。
十、实战里最容易误解的 4 个点
误区 1:审批通过只看批准人数
不对。
还要看必审人是否满足。
误区 2:顺序审批只是前端显示顺序
不是。
waiting 会阻止后面的审批人抢先通过。
误区 3:activity 只是提醒,不影响流程
也不对。
activity 是当前责任链的落地形式。
误区 4:withdraw 就是全单回到草稿
不是。
它更像从当前断点往回收缩流程位置。
总结
把 approvals 源码串起来以后,你会发现 Odoo 企业版做的并不是一个“同意/拒绝按钮集合”。
它真正做的是一套审批拓扑控制:
- 用 approval_minimum 约束通过门槛;
- 用 required approver 保证关键节点不可跳过;
- 用 waiting / pending 表达顺序审批位置;
- 用 mail.activity 把当前责任真正派发出去;
- 用 withdraw / refuse / cancel 控制流程回退与终止;
- 用 archive 代替 delete 保留已批准决策痕迹。
所以 Odoo 企业版 Approvals 的本质,不是“审批按钮”,而是“把审批门槛、顺序和责任队列压进同一套状态机”。
这才是它值得学的地方。
参考源码
- enterprise/approvals/models/approval_request.py
- enterprise/approvals/models/approval_approver.py
- enterprise/approvals/tests/test_approvals.py
DISCUSSION
评论区