很多团队以为现场服务里的材料面板只是一个“销售单快捷改数量”的 UI:少了就减一点,多了就加一点,反正最后库存会自己对齐。企业版并不是这么放任的。
在 Odoo 企业版 FSM 里,材料数量的增减、批次/序列号的绑定、仓库上下文、qty_delivered 的推进,全部都跟 stock move 链路绑在一起。你可以补料,但不能把已经交付出去的数量当成普通销售行那样直接回改;要回头,必须走库存退货。
关键源码主要在:
enterprise/industry_fsm_stock/models/project_task.pyenterprise/industry_fsm_stock/models/product.pyenterprise/industry_fsm_stock/models/sale_order_line.pyenterprise/industry_fsm_stock/models/stock_move.py
一、为什么 FSM 材料面板不是“想改多少就改多少”
最直接的一刀在 product.product.write()。
这里如果前端试图通过 fsm_quantity 把数量减到低于 quantity_decreasable_sum,系统会直接抛错:
已订购数量不能减少到低于已交付数量;请改为在库存里创建退货。
这句限制非常关键。它说明企业版的口径是:
- 未交付的部分,可以在 FSM 面板里继续调;
- 已经交付的部分,不能靠改销售数量“冲掉”;
- 真正的回退动作,要进入 inventory return 语义,而不是假装它从未交付过。
所以 Odoo 明确把“补料”和“退料”分成两类业务:
- 补料:还在现有 move / SOL 上继续补;
- 退料:必须生成库存反向动作,不允许只改前台数字。
二、系统怎么判断一项材料还能不能往下减
这个判断不是看销售行原始数量,而是 product.product._compute_quantity_decreasable() 里动态算出来的。
它会分两步做:
- 先按当前用户默认仓库,统计该 task 对应、且仍未 done/cancel 的
stock.move数量; - 如果没有 move,再回头看还没生成 move 的
sale.order.line,用product_uom_qty - qty_delivered去补上可减数量。
因此,前端看到的“可减少数量”并不是一个简单字段,而是当前仓库上下文 + 现存 move + 销售行剩余量三者共同决定的结果。
这也解释了为什么同一个 task,换个默认仓库用户去看,材料面板的可编辑边界可能不同——企业版就是这么设计的。
三、为什么 lot / serial 不是最后出库时再随便补
industry_fsm_stock 对 lot / serial 的处理非常前置。
1)销售行上先有 fsm_lot_id
sale.order.line 增加了 fsm_lot_id,让一条 FSM 材料销售行从一开始就能记住“客户现场到底用了哪一批/哪一个序列号”。
2)补料时 move line 会跟着修
在 sale.order.line._action_launch_stock_rule() 里:
- 如果 move 还没有 move line,就自动创建,并把
lot_id带上; - 如果数量增加,就把差额补到现有 move line;
- 如果数量减少,就尽量沿着同一 lot 的 move line 往回减;
- 如果整条 move 数量归零,则删掉 move line。
也就是说,企业版不是“先改 SOL,库存以后再说”,而是尽量在 stock rule 触发时就把 move line 跟上。
3)保留量分配也会优先照顾 FSM lot
stock.move._update_reserved_quantity() 又进一步把 lot 绑定推到保留阶段:
- 如果 sale line 上已有
fsm_lot_id,就优先用那个 lot 去保留; - 对 serial 追踪产品,还会检查同级 move line 已经占用了多少;
- 只有 FSM lot 的需求满足不了,才回退到普通保留逻辑。
这保证了现场录入的序列号,不会在真正拣货时被系统随手换成别的 lot。
四、任务完工时,为什么 delivered quantity 会被“推平”
project.task._validate_stock() 是 FSM 完工时最核心的一步。
它会遍历 sale order 上与当前 task/FSM 项目相关的销售行,做几件事:
- 计算还没交付的数量
product_uom_qty - qty_delivered; - 找出尚未完成的 move 与上游 move_orig_ids;
- 没有 move line 的就自动补一条;
- 已有 move line 但数量不够的,就补齐缺口;
- 对 lot/serial 产品把
fsm_lot_id回写进 move line; - 对非
delivered_timesheet、非delivered_milestones的 task 材料/服务行,直接把qty_delivered推到等于订购数量。
最后这一步尤其重要:
对很多 FSM 材料行来说,任务完成本身就是“可以认定已交付”的触发点。
所以 Odoo 不会等你事后慢慢调 qty_delivered,而是在 stock 校验链和任务状态都满足时,直接把交付数量推到位。
五、为什么“退一点数量”不能靠改 task 面板完成
很多人会问:既然系统能自动补 move line,也能往回减 move line,为什么不允许把已交付数量一并改回来?
答案是:因为那会破坏业务语义。
一旦某部分材料已经被视为 delivered:
- 销售交付口径已经成立;
- 可能已经影响利润分析;
- lot / serial 的实际去向已经被记录;
- 对应 pickings 甚至可能已经 done。
这时候如果只通过 FSM 面板把数量减掉,等于把“已发生的库存动作”伪装成“从未发生”。企业版选择的做法更严格:
- UI 可以阻止你减破已交付底线;
- 真要回退,请走库存退货,让反向 move 明确留痕。
这正是企业系统该有的边界。
六、仓库为什么总跟当前用户有关
project.task._fsm_ensure_sale_order()、_fsm_create_sale_order() 和 action_fsm_view_material() 都在强调同一件事:FSM 材料不是脱离仓库上下文的。
系统会尽量确保:
- 先确认 SO,避免后续不同用户追加材料时继承错仓库;
- 在材料面板中显式塞入当前用户默认仓库;
- tracked 产品的 wizard 也按 task/SO/company 的仓库语义来组织数据。
因此,很多“为什么同一任务不同人看起来不一样”的现象,根本原因并不是 bug,而是仓库上下文不同。
七、实战建议
- 如果客户经常发生现场退料,务必把库存退货流程培训清楚,不要让工程师试图用减销售数量代替退货。
- tracked 产品一定要实测 lot / serial 流程,尤其是“先补料、再完工、再回看 move line”的完整链路。
- 多仓库场景下,要先统一默认仓库策略,再放开一线工程师自行补料。
- 做利润或交付报表时,不要只盯销售行数量,还要核对 done move、move line 和
qty_delivered是否已经同步推进。
八、结论
Odoo 企业版 FSM 的材料逻辑,本质不是一个“可自由改数”的前台表单,而是销售行、库存 move、lot/serial 追踪和 delivered quantity 的联动边界。补料可以沿现有链路继续追加;但一旦材料已经交付,要回头就必须走正式库存退货。这不是麻烦,而是企业版用来保护库存与计费一致性的核心设计。
DISCUSSION
评论区