先抓主线
“销售驱动制造”最容易被误解成一句大白话:
- 销售单确认
- 系统自动建 MO
- 然后车间开工
但标准 Odoo 的源码重点,其实不在“自动建”这三个字,而在可追踪地建。
更准确的主链路是:
- 销售行先准备 procurement values
- 把
sale_line_id、deadline、warehouse、route、shipping partner 等销售上下文塞进去 - stock rule 决定这次需求走 pull / buy / manufacture 哪条路
- 如果走 manufacture,
sale_mrp再把sale_line_id写进 MO - 后续生成 stock move 时,还尽量保住这条销售来源关系
- 如果销售行卖的是 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() 往下游塞的值远不止数量。
标准会准备:
originreference_idssale_line_iddate_planneddate_deadlineroute_idswarehouse_idpartner_idlocation_final_idproduct_description_variantscompany_idsequencenever_product_template_attribute_value_idspackaging_uom_id
这说明 procurement 从来不是“补货一条数量消息”,而是一条带业务语义的任务。
尤其是 sale_line_id,它不是装饰字段,而是整条追踪链的锚点。
第二层:销售侧日期为什么会影响制造计划
同一个方法里还有两个容易忽略的字段:
date_deadlinedate_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.py 的 stock.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 没挂到销售行”,建议按这个顺序看:
- 销售行是否真的走了
_action_launch_stock_rule() sale.order.line._prepare_procurement_values()有没有带出sale_line_id- stock rule 是不是实际走到了 manufacture 路线
stock.move._prepare_procurement_values()有没有继续续传sale_line_idsale_mrp._prepare_mo_vals()是否被继承或自定义覆盖- 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
评论区