销售驱动制造

Odoo 销售驱动制造为什么不是“确认订单自动建工单”这么简单:sale_line_id 如何穿过补货、MO 与 kit 组件追踪讲透

很多人把销售驱动制造理解成“销售单确认以后就建 manufacturing order”;但标准 Odoo 真正重视的是把 sale_line_id、截止日期、仓库、路线和变体上下文沿着 procurement 一路传下去,直到 MO 和 stock move 都还能回指销售行,kit 场景还会额外补 bom_line_id。本文把这条可追踪链讲透。

制造 销售
进阶 开发者 2 分钟阅读
0 评论 0 点赞 0 收藏 5 阅读

先抓主线

“销售驱动制造”最容易被误解成一句大白话:

  • 销售单确认
  • 系统自动建 MO
  • 然后车间开工

但标准 Odoo 的源码重点,其实不在“自动建”这三个字,而在可追踪地建

更准确的主链路是:

  1. 销售行先准备 procurement values
  2. sale_line_id、deadline、warehouse、route、shipping partner 等销售上下文塞进去
  3. stock rule 决定这次需求走 pull / buy / manufacture 哪条路
  4. 如果走 manufacture,sale_mrp 再把 sale_line_id 写进 MO
  5. 后续生成 stock move 时,还尽量保住这条销售来源关系
  6. 如果销售行卖的是 kit,而 move 对应的是组件,还会补 bom_line_id 让组件追踪不丢

所以这条链路真正解决的是:

下游生产和库存动作,如何持续知道自己是替哪一条销售行服务。

没有这层追踪,你后面很多问题都没法答:

  • 这张 MO 是哪张销售单触发的?
  • 这个组件 move 为什么会挂到这条销售行?
  • 客户承诺日期为什么会传进生产计划?
  • kit 展开后为什么还需要知道对应哪个 BOM line?

这篇文章主要参考哪些源码

核心参考包括:

  • /home/ubuntu/odoo-temp/addons/sale_stock/models/sale_order_line.py
  • /home/ubuntu/odoo-temp/addons/sale_stock/models/stock.py
  • /home/ubuntu/odoo-temp/addons/sale_mrp/models/stock_rule.py

最关键的方法是:

  • sale.order.line._prepare_procurement_values()
  • stock.move._prepare_procurement_values()
  • stock.rule._prepare_mo_vals()
  • stock.rule._get_stock_move_values()

这几个点连起来看,才会发现标准 Odoo 一直在尽量保住销售来源,而不是只关心“数量有没有流下去”。


第一层:销售行不是只算数量,还要准备整包下游上下文

sale_stock 里,sale.order.line._prepare_procurement_values() 往下游塞的值远不止数量。

标准会准备:

  • origin
  • reference_ids
  • sale_line_id
  • date_planned
  • date_deadline
  • route_ids
  • warehouse_id
  • partner_id
  • location_final_id
  • product_description_variants
  • company_id
  • sequence
  • never_product_template_attribute_value_ids
  • packaging_uom_id

这说明 procurement 从来不是“补货一条数量消息”,而是一条带业务语义的任务。

尤其是 sale_line_id,它不是装饰字段,而是整条追踪链的锚点。


第二层:销售侧日期为什么会影响制造计划

同一个方法里还有两个容易忽略的字段:

  • date_deadline
  • date_planned

其中:

  • date_deadline 优先取订单 commitment_date,否则退回 _expected_date()
  • date_planned 则会再减去公司 security_lead

这意味着生产端看到的计划时间,并不是车间自己拍脑袋算的,而是销售承诺和公司安全提前量共同推出来的。

所以“客户承诺 4 月 20 日交货,为什么车间计划更早开始”这种现象,并不是多余保守,而是销售链路已经把 deadline 约束往下游传了。


第三层:sale_line_id 先从销售行进 procurement,再从 move 继续往下传

标准链路的关键不是单点注入,而是层层续传

sale.order.line._prepare_procurement_values() 里,销售行先把:

  • sale_line_id = self.id

放进 procurement values。

然后在 sale_stock/models/stock.pystock.move._prepare_procurement_values() 里,如果 move 本身已经挂着 sale_line_id,它又会把这层关系继续传给下游。

源码注释写得很直白:

  • to pass sale_line_id from SO to MO in mto

也就是说,move 并不是终点,而是“把销售来源继续带下去的中继站”。

这就是为什么在 MTO + Manufacture 的场景里,MO 最终还能知道自己从哪条销售行来。


第四层:真正把销售来源写进 MO 的,是 sale_mrp_prepare_mo_vals()

到了 sale_mrp/models/stock_rule.py_prepare_mo_vals() 会在父类结果基础上追加:

  • 如果 values 里有 sale_line_id
  • 就把它写到 MO 的 vals 里

这一步非常关键,因为它代表标准 Odoo 不是只让 procurement 临时带一带销售来源,而是把这层来源关系真正落库到生产单上。

业务意义也很明确:

  • 生产单可回溯到销售单行
  • 销售、计划、仓储、实施在跨模块排错时有公共锚点
  • 交付争议时,更容易分辨“这张生产单是为谁做的”

所以“销售触发制造”真正重要的不是自动,而是来源关系进了 MO 以后,才有后续协同价值


第五层:kit 场景为什么还要额外补 bom_line_id

_get_stock_move_values() 是这篇文章最值得看的细节。

它在处理 sale_line_id 时,会先看:

  • values.get('sale_line_id') 在不在
  • move_values 里是不是有 product_id

然后它特别处理一种情况:

  • 销售行卖的是一个 kit
  • 但下游 move 的 product_id 不是销售行产品本身,而是 kit 组件

这时,标准会去找销售行现有活跃 move 对应的 bom_line_id,再把那个 bom_line_id 塞回新 move。

这说明什么?

说明在 kit 场景里,仅仅知道“这个 move 来自这条销售行”还不够;你还得进一步知道:

它来自这条销售行展开后的哪一个 BOM 组件。

否则组件级追踪会断层,后面你看补货、退货、替换件、成本分摊都会很痛苦。


第六层:所以标准 Odoo 更在意“链路语义完整”,不是“建单够快”

把这几个方法连起来看,会发现标准设计有一个很鲜明的取向:

  • 数量要传
  • 但业务来源也要传
  • 时间约束要传
  • 仓库和路线要传
  • 销售行和组件映射也尽量别丢

这就是为什么它不是简单地“确认订单 -> create mrp.production”。

如果只是建一张 MO,当然很容易;真正难的是:

  • 建出来以后,下游对象还能保留足够语义,支撑后续追踪、对账、解释与排错

新手最容易误解的 5 件事

1. 以为销售驱动制造只和数量有关

不是。标准还会传日期、路线、仓库、客户地址与销售来源。

2. 以为 sale_line_id 只是给界面显示用

不是。它是销售、库存、制造之间追踪锚点。

3. 以为 move 有了 sale_line_id 就够了

不够。MO 侧也需要落这层关系,排错和协同才顺。

4. 以为 kit 展开后组件 move 天然知道自己对应哪条 BOM line

不是。标准要显式补 bom_line_id

5. 以为“自动建 MO”就是这条功能的核心价值

真正的价值是把销售来源一路保住。


实战调试顺序

如果你排查“为什么这张 MO 没挂到销售行”,建议按这个顺序看:

  1. 销售行是否真的走了 _action_launch_stock_rule()
  2. sale.order.line._prepare_procurement_values() 有没有带出 sale_line_id
  3. stock rule 是不是实际走到了 manufacture 路线
  4. stock.move._prepare_procurement_values() 有没有继续续传 sale_line_id
  5. sale_mrp._prepare_mo_vals() 是否被继承或自定义覆盖
  6. kit 场景下 move 产品与销售产品是否不同,从而需要补 bom_line_id

如果你排查“为什么组件追踪乱了”,重点看:

  • 销售的是 kit 还是普通产品
  • active moves 上有没有可匹配的 bom_line_id
  • 自定义代码是否把 sale_line_id / bom_line_id 清掉了

一句话记忆法

Odoo 的销售驱动制造,不是“销售单确认后自动建个 MO”而已,而是把 sale_line_id、时间约束和组件映射沿 procurement 一路带到 MO 与 move,确保下游始终知道自己在替哪条销售行履约。

DISCUSSION

评论区

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