先说结论
Odoo 里的制造报废,不是“库存模块里的 Scrap 按钮搬到制造里”而已。
一旦 Scrap 带上 production_id 或 workorder_id,它就不再是普通库存损耗记录,而是进入了制造语义:
- 它属于哪张 MO
- 是原料报废还是成品报废
- 应该从哪个库位扣
- 是否要保留对工单与追溯链的解释能力
所以真正的问题不是“能不能报废”,而是这次报废到底是在制造链上的哪个位置发生。
源码里最关键的两件事
在 stock.scrap 的 MRP 扩展里,有两个很重要的字段:
production_idworkorder_id
这已经说明 Odoo 不把制造报废看成无上下文动作,而是允许你把 Scrap 直接挂到:
- 整张制造单
- 某一道工单
这两种挂法的业务含义不一样:
- 挂 MO:更偏整单层面的原料或成品异常
- 挂工单:更偏具体工序现场发生的损耗
为什么库位会“自动变来变去”
_compute_location_id() 的逻辑很值得看:
- 如果 Scrap 关联
production_id,且 MO 还没 done,默认从location_src_id取货 - 如果 Scrap 关联
production_id,但 MO 已经 done,默认从location_dest_id取货 - 如果是
workorder_id,则走该工单所属 MO 的原料来源库位
这背后的业务语义非常清楚:
完工前报废,多数是在“待消耗原料”这一侧;完工后报废,多数已经站到“成品侧”了。
所以你看到同样是制造报废,却在不同时间点扣不同库位,不是系统乱跳,而是 Odoo 在替你判断“这次报废更像原料损耗还是成品异常”。
为什么有时报废 move 会挂到 production_id,有时挂 raw_material_production_id
_prepare_move_values() 里,Odoo 会区分报废的产品是不是这张 MO 的 finished product:
- 如果报废的是成品集合中的产品,move 记到
production_id - 否则记到
raw_material_production_id
这件事非常重要,因为它决定后续 traceability、统计、以及你回头查“这笔损耗到底属于原料端还是成品端”时,系统怎么归类。
批次 / 序列号为什么更敏感
如果报废的是 serial 跟踪产品,且 Scrap 关联了制造单,Odoo 还会借助 stock.quant._check_serial_number() 做额外检查,并可能给出推荐库位。
换句话说,制造报废并不是简单写一笔数量。对序列号产品,它还在防你做出一笔不符合真实存放位置或追溯逻辑的报废。
最容易误解的地方
1. 以为工单报废只是备注
不是。workorder_id 会让这笔损耗更靠近具体工序上下文。
2. 以为报废一定都从原料库扣
完工前后,默认源库位逻辑不同。
3. 以为制造报废不会影响补货
如果 MO 有 production_group_id,do_replenish() 还会把这层生产组上下文带过去,影响后续补货链。
4. 以为 Scrap 只影响库存数量
它也会影响追溯解释、MO 成本理解以及现场责任定位。
正确排错顺序
遇到制造报废“扣错库位”或“挂错对象”,按这个顺序查:
- Scrap 是挂 MO 还是挂工单
- 当前 MO 是否已经 done
- 报废的是成品还是原料
- 是否有 lot / serial 校验干预
- 是否触发了带
production_group_id的补货上下文
一句话记忆法
Odoo 制造报废不是单纯减库存,而是在回答:这次损耗发生在制造链的哪一段。
DISCUSSION
评论区