先说结论
Odoo 的 Blanket Order 会做三件很重要的事:
- 确认协议时要求每行必须有价格和数量
- 把协议行写入
product.supplierinfo - 统计同一协议下、同一产品已经落成
purchase状态的已下单数量
但它不会天然把自己做成一个“合同余额强锁系统”。
更准确地说:
Odoo 会告诉你协议数量、已采购数量和供应商条件,但不会默认阻止你超出协议数量继续下单。
这正是很多实施团队对 Blanket Order 最容易产生期待错位的地方。
这篇为什么不是已有“Blanket Order 与 supplierinfo 边界”重写
站里已有文章已经讲过:
- Blanket Order 会在确认时创建
supplierinfo - PO 通过
requisition_id参与协议上下文过滤 - 协议结束时相关
supplierinfo会被清理
那篇重点在协议与供应商主数据的边界。
这篇改讲一个现场更常见的问题:
- 协议数量到底是不是“余额池”
qty_ordered到底在统计什么- 改价时 supplierinfo 会被回写到什么程度
- 为什么这条链能辅助释放,但不等于严格合同执行
第一关键点:Blanket Order 确认前就要求数量和价格都完整
在 addons/purchase_requisition/models/purchase_requisition.py 的 action_confirm() 里,blanket_order 会逐行检查:
price_unit > 0product_qty > 0
缺一不可。
这一步很有意思。
它说明官方并不把 Blanket Order 仅仅当成一个“价格意向”文档,而是认为:
- 协议行必须达到足以支撑后续采购引用的完整度
也就是说,数量从一开始就有意义。
但这个“有意义”,不代表“强制余额锁单”。
第二关键点:系统会统计已下单数量,但统计口径是“已采购”,不是“剩余额度控制”
同一文件里的 purchase.requisition.line 定义了:
qty_ordered
它在 _compute_ordered_qty() 里按这条逻辑计算:
- 只统计当前 agreement 下
state == 'purchase'的采购单 - 只累计同产品的采购行数量
- 必要时做 UoM 换算
- 同一 requisition 里同产品只让一条协议行显示累计值,避免重复展示
这意味着什么?
意味着 Odoo 非常关心“这份协议下,这个产品已经真正下了多少采购”。
但源码并没有在确认 PO 或新增 PO 行时写出类似:
- 如果
qty_ordered + 本次数量 > agreement_qty就禁止
也没有天然生成“remaining_qty”强约束链路。
所以 qty_ordered 更像:
- 释放监控指标
- 执行进度提示
- 协议消费观察窗
而不是合同余额闸门。
第三关键点:supplierinfo 的回写边界,是“协议价同步”,不是“完整释放台账”
Blanket Order 的 write() 里有个很重要的实现:
- 如果改了
price_unit - 且是已确认、未关闭/未取消的 blanket order
- 系统会把对应
supplier_info_ids的price同步更新
这就是典型的“writeback”。
但你仔细看会发现,回写内容非常克制,主要围绕:
- 价格
- 供应商
- 产品
- UoM
- 币种
- 协议行关联
它没有把 Blanket Order 做成完整释放台账,例如:
- 每次释放一张 PO 就写剩余额
- 每次改 PO 数量就重算合同可用余额并阻断超量
- 每次分批执行都记录 release schedule 明细
所以这条 writeback 链的本质是:
让协议价格持续影响采购主数据,而不是让协议数量变成严格会计账。
第四关键点:协议结束或取消时,相关 supplierinfo 会被清理
action_done() 和 action_cancel() 都会把协议行相关的 supplier_info_ids 删除。
这意味着 Blanket Order 注入的供应商条件是:
- 有生命周期的
- 与当前协议绑定的
- 不是永久静态主数据
这也再次说明,Odoo 的重点是“协议在有效期内参与采购引擎”,而不是“永久维护一份合同余额总账”。
如果它真想把 Blanket Order 做成刚性合同控制器,通常会更强调:
- 可追溯释放记录
- 剩余额冻结
- 超量审批
- 合同偏离日志
但原生实现并没有把力度放在这里。
第五关键点:新加行、改价、删行都围绕 supplierinfo 生命周期,而不是 release governance
create()、write()、unlink() 这几段代码的行为很能说明问题:
- 已确认的 Blanket Order 新增行,必要时会创建
supplierinfo - 已确认状态改价,会同步更新相关
supplierinfo.price - 删除有效协议行,会把关联 supplierinfo 一起删掉
整条链都在围绕一个中心:
- 协议行如何映射为可消费的供应商条件,并在生命周期内保持同步
而不是:
- 每一次 release 如何被严格审计和强制扣减
这就是 Odoo Blanket Order 的真正边界。
现场最容易误解的 5 个点
1)以为 agreement quantity 就是系统自动管控的合同余额
源码只统计 qty_ordered,没有默认把它做成硬阻断。
2)以为每次下 PO 都会自动回写 release 余额
原生没有这套完整余额台账。
3)以为 supplierinfo writeback 代表“合同正在被精确执行”
它更多代表价格条件同步,不代表完整合同治理。
4)以为关闭协议只是改状态
实际上还会清理掉这份协议注入的 supplierinfo。
5)以为 Blanket Order 天然等于采购合同模块
它更像“带数量参考的采购框架 + 供应商条件生命周期管理”。
什么时候需要自定义
如果你的业务真的要求:
- 每个协议行必须有剩余额
- PO 超量必须阻断或审批
- 分批释放要留 release 明细
- 按月/按期次消耗协议量
那就不要把责任强压给原生 Blanket Order。
更合适的做法通常是:
- 在协议行上增加
remaining_qty - 在 PO 确认或行保存时校验超量
- 单独做 release log 或 release schedule
- 把超量、改价、越期释放纳入审批链
这样你的扩展会顺着 Odoo 的原始边界走,而不是误解它已经提供了完整合同释放系统。
一句话记忆
Odoo Blanket Order 会统计已下单数量、回写 supplierinfo、管理协议生命周期,但不会原生把协议数量做成严格的合同余额扣减引擎。
DISCUSSION
评论区