协议边界

Odoo Blanket Order 为什么不像真正的“采购合同”:supplierinfo 注入、PO 引用与协议边界讲透

很多人把 Blanket Order 当成长周期采购合同,但 Odoo 的实现更像“把协议条件临时灌进供应商价目与后续 RFQ/PO 流程”。本文专讲协议、supplierinfo 与实际采购单之间的边界,不再停留在概念层。

采购
进阶 开发者 2 分钟阅读
0 评论 0 点赞 0 收藏 5 阅读

先说结论

很多团队看到 Odoo 的 Blanket Order,会自然把它理解成:

  • 已经签好的采购合同
  • 后续 PO 只是按合同执行

这个理解并不完全对。

从官方源码看,Odoo 更接近这样做:

  1. purchase.requisition 表达一段采购协议
  2. 在确认 Blanket Order 时,把行信息写进 product.supplierinfo
  3. 后续 RFQ / PO 再通过这些供应商条件参与取价和关联

所以 Blanket Order 在 Odoo 里不是“合同系统的硬约束投影”,而更像:

采购协议 + 供应商条件注入 + 后续 PO 引用框架

这也是为什么很多实施团队会产生错觉:

  • 明明有协议,为什么 PO 还能改?
  • 明明是长期协议,为什么表现得像供应商价目增强版?

因为源码设计本来就不是把它做成不可偏离的法务合同引擎。


这篇为什么不是已有采购协议文章的重复

站里已有文章讲 Blanket Order 和招标在采购治理上的分工。

那篇重点是:

  • Agreement 是什么
  • Blanket Order 和比价招标有什么不同

这篇更落到实现边界:

  • Blanket Order 如何落地成 supplierinfo
  • 后续 PO 到底是“继承协议”还是“继续正常采购”
  • 为什么它更像系统内采购框架,而不是严格合同执行器

也就是说,这篇讲的是 实现语义边界,不是产品概念介绍。


源码第一关键点:确认 Blanket Order 时会创建 supplierinfo

/home/ubuntu/odoo-temp/addons/purchase_requisition/models/purchase_requisition.py 里,action_confirm()blanket_order 做了几步很关键的校验:

  • 行上必须有价格
  • 行上必须有数量
  • 然后逐行执行 _create_supplier_info()

这一步意义巨大。

它说明 Odoo 并没有把 Blanket Order 只当作一份静态协议文档,而是主动把协议行转成供应商条件记录。

_create_supplier_info() 中,系统会创建:

  • partner_id
  • product_id
  • product_uom_id
  • product_tmpl_id
  • price
  • currency_id
  • purchase_requisition_line_id

也就是说,协议一确认,它就被灌进了供应商选价体系。


为什么这很像“把协议写进采购主数据”

supplierinfo 本来就是 Odoo 采购选供应商、取采购价、比起订量和交期的重要主数据结构。

Blanket Order 确认后把协议行写进去,意味着系统的设计不是:

  • 每次下 PO 都单独回头查协议文本

而是:

  • 把协议结果转译成日常采购引擎也能直接使用的供应商条件。

这非常实用,但也埋下了边界问题:

  • 如果你把 Blanket Order 当“合同本身”
  • 就会低估 supplierinfo 在后续实际采购中的影响力

第二关键点:后续卖方准备时,会按 requisition 过滤 supplierinfo

/home/ubuntu/odoo-temp/addons/purchase_requisition/models/product.py 中,product.product._prepare_sellers() 被扩展了。

逻辑是:

  • 如果当前上下文里带 order_id
  • 而这个订单有 requisition_id
  • 那就只保留:
  • 没有 purchase_requisition_id 的 seller
  • 或者属于当前这张 requisition 的 seller

这句非常关键。

它说明 Odoo 在协议场景里,并不是把所有供应商记录平铺混用,而是:

当 PO 明确关联某个协议时,允许该协议注入的 supplierinfo 进入候选集,并把别的协议信息隔开。

这就是“协议影响后续采购”的真正技术落点。


为什么这仍然不等于“PO 完全被合同锁死”

这是现场最常见的误会。

虽然协议行会生成 supplierinfo,也会影响后续 PO,但从源码看,PO 依然是独立业务对象。

/home/ubuntu/odoo-temp/addons/purchase_requisition/models/purchase.py 里,purchase.order 仍然有自己的一套逻辑:

  • requisition_id
  • date_order
  • currency_id
  • note
  • 采购行生成
  • 后续修改

而且协议行 _prepare_purchase_order_line() 也只是准备初始采购行数据:

  • name
  • product_id
  • product_qty
  • price_unit
  • date_planned

这更像“按协议起草采购”,而不是“合同系统直接锁单”。

所以 Blanket Order 与 PO 的关系更贴近:

  • 协议提供约束框架和默认条件
  • PO 依然是实际执行与变更载体

第三个关键点:协议关闭时,还会把相关 supplierinfo 清掉

action_done() 和取消逻辑里,Odoo 会把关联的 supplier_info_ids 删除。

这说明协议生成的供应商条件不是永久主数据,而是:

  • 带生命周期的、与协议关联的供应商条件注入

这个设计非常值得注意。

因为它再次说明 Blanket Order 更像一套“有效期内的采购框架机制”,而不是永远写死在产品主数据里的合同价格。


为什么说它不像传统采购合同

传统采购合同在企业语义里,往往强调的是:

  • 法律约束
  • 偏离审批
  • 合同版本
  • 违约边界
  • 强制执行口径

而 Odoo 这里的 Blanket Order,更偏系统执行层:

  • 确认前校验价格和数量
  • 确认后生成 supplierinfo
  • 通过 requisition_id 影响后续 RFQ/PO
  • 协议结束时清理注入数据

这当然非常有用,但它解决的是“系统内采购框架与取价协作”,而不是完整法务合同管理。

如果实施时把它硬当合同系统,后面一定会在审批、锁价、偏离控制上提出超过源码边界的期待。


业务上最容易误解的 5 个点

1. 以为确认 Blanket Order 后,后续 PO 就绝对不能改

源码里更像是带默认条件与约束框架,而不是绝对锁死。

2. 以为协议只是文档,不会影响选价

实际上它会直接生成 supplierinfo 并进入采购主链路。

3. 以为协议结束后,价格条件会永久留在产品上

相关 supplierinfo 是可以在协议结束时被清理掉的。

4. 以为所有协议供应商条件会和普通供应商条件混在一起

当 PO 带 requisition_id 时,源码会做定向过滤。

5. 以为 Blanket Order 等同法务合同模块

它更像采购执行框架,不是完整合同治理引擎。


开发和实施时最该注意什么

1. 如果客户要求“合同不可偏离”,不要只靠 Blanket Order 原生逻辑

你大概率还需要补:

  • 价格偏离审批
  • 数量偏离阈值
  • 合同版本留痕
  • 锁字段或锁状态机制

2. 定制取价逻辑时,要意识到协议生成的 supplierinfo 会参与日常采购选价

如果你忘了这层,后面很容易出现:

  • 平时采购价怎么突然变了
  • 为什么某个协议一确认,PO 选价表现就不同了

3. 协议关闭/取消的生命周期要测试

因为 supplierinfo 的创建与清理,直接影响后续采购候选集。

4. 报表设计时,最好同时展示 requisition_idpurchase_requisition_line_id

这样才能把“协议框架”与“实际采购执行”连起来看清楚。


一句话记忆法

Odoo 的 Blanket Order 更像把协议条件注入 supplierinfo 并引导后续 PO,而不是把 PO 完全锁死成一份不可偏离的采购合同。

DISCUSSION

评论区

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