企业 现场服务

Odoo 企业版现场服务为什么不是“领料后就不能回头”:补料、退料限制、批次回写与 delivered quantity 重算讲透

基于 industry_fsm_stock 源码,讲清现场服务任务里材料数量为何不能随手回改、为什么必须走库存退货、以及 lot/serial 与 delivered quantity 如何一起被重建。

企业 项目
进阶 开发者 2 分钟阅读
0 评论 0 点赞 0 收藏 6 阅读

很多团队以为现场服务里的材料面板只是一个“销售单快捷改数量”的 UI:少了就减一点,多了就加一点,反正最后库存会自己对齐。企业版并不是这么放任的。

在 Odoo 企业版 FSM 里,材料数量的增减、批次/序列号的绑定、仓库上下文、qty_delivered 的推进,全部都跟 stock move 链路绑在一起。你可以补料,但不能把已经交付出去的数量当成普通销售行那样直接回改;要回头,必须走库存退货。

关键源码主要在:

  • enterprise/industry_fsm_stock/models/project_task.py
  • enterprise/industry_fsm_stock/models/product.py
  • enterprise/industry_fsm_stock/models/sale_order_line.py
  • enterprise/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() 里动态算出来的。

它会分两步做:

  1. 先按当前用户默认仓库,统计该 task 对应、且仍未 done/cancel 的 stock.move 数量;
  2. 如果没有 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 项目相关的销售行,做几件事:

  1. 计算还没交付的数量 product_uom_qty - qty_delivered
  2. 找出尚未完成的 move 与上游 move_orig_ids;
  3. 没有 move line 的就自动补一条;
  4. 已有 move line 但数量不够的,就补齐缺口;
  5. 对 lot/serial 产品把 fsm_lot_id 回写进 move line;
  6. 对非 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

评论区

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