结论先行
在 Odoo 标准源码里,客户把产品寄回维修这件事,并不是由 repair.order 直接发起的。
标准链路其实分成两段:
- 先走库存退货链路:从原始交货单创建
Return Picking - 再走维修链路:从退货单进入
Repair Order
也就是说,Odoo 的设计不是“维修单同时代表收货、维修、返还客户”三件事,而是把它拆成:
- 客户退回物流 →
stock.return.picking - 维修过程管理 →
repair.order - 维修完成后的库存落账 →
repair.order.action_repair_done()生成最终stock.move - 寄回客户 → 标准版需要再走独立出库链路
这也是很多项目里会觉得标准 Repair “差一口气”的根本原因:
它把“客户寄回维修”拆得很清楚,但不会默认帮你把售后闭环一键串到底。
先把业务过程翻成一句人话
一个最常见的售后返修场景,可以概括成下面这条线:
原始销售出库完成
→ 客户把产品寄回
→ 仓库创建退货单并收货
→ 从退货单创建维修单
→ 维修中记录加件 / 拆件 / 回收件
→ 维修完成,库存落账
→ 如需返还客户,再单独创建出库单
这里最容易误解的点有两个:
误解 1:Repair Order 就是客户寄回单
不是。
repair.order 代表的是维修过程,不是“客户退回物流单”。
误解 2:Repair 完工后会自动发回客户
也不是。
标准源码只负责把维修结果在库存层面落账,不会默认自动生成返还客户的出库单。
第一段:客户发回产品,为什么先走 Return Picking
标准入口在库存模块的退货向导 stock.return.picking。
当你在一张已经 done 的交货单上点击 Return,Odoo 会:
- 创建新的退货 picking
- 复制原来的 move 形成 return move
- 把这些 move 用
origin_returned_move_id、move_orig_ids、move_dest_ids串回原始流转链
这一步的核心不是“修”,而是:
先把客户手里的货,合法地收回库存系统。
也就是说,在标准设计里,“客户寄回待维修产品”首先是一个库存事实,然后才是一个维修事实。
这套设计的好处是追溯清楚:
- 原来从哪张交货单出去的
- 现在从哪张退货单退回来的
- 后面维修单接的是哪次退回
如果你做 lot / serial 追踪,这种拆分非常必要。
第二段:退货单如何进入 Repair Order
repair 模块在 stock.picking 上加了一个动作:action_repair_return()。
这个动作不是直接改库存,而是做三件非常关键的事情:
- 把当前退货单 ID 放进上下文
default_repair_picking_id - 根据当前仓库带出默认维修操作类型
warehouse.repair_type_id - 把客户
partner_id一起带进维修单
所以 Repair Order 不是凭空创建的,而是从“这次客户退回”这个上下文里长出来的。
随后,repair.order.default_get() 会读取上下文中的:
default_repair_picking_iddefault_repair_lot_id
并把这些值默认带入维修单表单。
这一步的设计很值得注意:
Odoo 没有把退货单和维修单强耦合成一个对象,而是通过“上下文 + 字段绑定”的方式,把两个业务对象接起来。
这样一来:
- 退货还是库存模块的职责
- 维修还是 repair 模块的职责
- 但两者可以无缝衔接
Repair Order 里到底绑定了什么
维修单上最关键的字段是这些:
picking_id:这张维修单来自哪张 transferproduct_id:待维修产品lot_id:待维修的批次 / 序列号partner_id:客户product_qty:维修数量
其中 picking_id 的 help 就很直接:
Transfer from which the product to be repaired is picked
翻译成人话就是:
这张维修单,是从哪一张“退回来的单”里接到待维修产品的。
如果维修单绑定了退货单,系统还会进一步自动推导:
1)客户默认来自退货单
partner_id 默认取 picking.partner_id
2)维修数量可从退货 move 自动推导
如果 picking 里有对应产品 / lot,product_qty 会跟着算
3)lot / serial 只能选退货单里真正退回来的那批货
这点很关键,避免维修单乱选一个不属于本次退回的序列号。
这也是为什么 repair 模块并不只是“登记一个维修申请”,它其实有明确的库存来源约束。
维修过程中,系统怎样表达“加件 / 拆件 / 回收”
Repair Order 的部件行本质上不是一套独立表,而是 stock.move,只是多了一个字段:
repair_line_type = addrepair_line_type = removerepair_line_type = recycle
这个设计很 Odoo:
能复用 stock.move,就尽量不要重新发明一套“维修部件流水”。
于是维修过程被拆成三种库存动作:
1)add:新增零件
表示从零件来源库位取料,投入维修过程。
常见流向是:
库存位 → 维修目标位(通常是 production location)
2)remove:拆下坏件
表示从维修位置拆出一个部件,送去拆下件位。
常见流向是:
维修位置 → 拆下件目的位
3)recycle:回收件
表示拆下来的部件不是报废,而是转去可回收位置。
常见流向是:
维修位置 → 回收库位
所以 Repair 并不是一句“修好了”,而是把整个维修过程拆成一组受库存规则约束的 move。
这也解释了为什么标准 Repair 能支持:
- 零件可用量检查
- 缺料补货触发
- 已用数量和计划数量对比
- lot / serial 追溯
为什么确认维修单时会检查库存
action_validate() 的逻辑可以概括成两层。
第一层:先检查维修部件配置是不是合法
例如部件需求不能是负数。
第二层:检查“待维修产品”是否真的在维修来源位置里存在
这一点很多人第一次看源码会意外。
系统会去查 stock.quant,而且查两类库存:
owner_id = partner_idowner_id = False
这意味着标准 Odoo 默认兼容两种售后场景:
场景 A:客户所有权库存
产品虽然在你仓里,但所有权仍然属于客户。
场景 B:公司库存
产品一旦退回,就按普通公司库存处理。
这也是 Repair 模块比表面上更强的一点:
它不只是关心“有没有这件货”,还关心“这件货是谁的”。
真正的关键点:维修完工时,到底发生了什么
很多人以为“End Repair”只是把状态改成 done。
其实不是。
真正的核心动作在 action_repair_done()。
它做了下面几件事:
1)把没有实际使用的部件行取消掉
如果某条部件 line 的实际 quantity = 0,它会被取消。
2)如果待修产品是追踪产品,必须有 lot / serial
否则直接报错,不能完工。
3)创建一条最终的 repaired product move
这条 move 的关键字段非常值得注意:
product_id = 待维修产品location_id = product_location_src_idlocation_dest_id = product_location_dest_idrepair_id = 当前维修单picking_id = False
而且 move line 上还会通过 consume_line_ids 关联到维修过程中用过的部件 move line。
翻成人话就是:
系统会明确登记:这台修好的产品,是由这张维修单产出的,它消耗过哪些零件流水。
4)把“维修部件 move + 最终产品 move”一起 done
最后 Repair 状态才会变成 done。
一个非常关键但经常被忽略的边界
维修完工后生成的最终 product move,没有 picking。
这意味着什么?
意味着标准 Repair 完工表示的是:
维修结果已经在库存里落账
而不是:
修好的东西已经寄回客户
所以标准设计天然把下面两件事分开了:
- 维修完成
- 返还客户
这是标准 Repair 最重要的业务边界之一。
如果项目上要求:
- 客户寄回后自动进维修单
- 修完后自动回寄客户
- 全链路在一个单据视角里闭环
那么就一定会进入二开。
官方测试已经把这条链路说得很清楚
repair 模块里有一个很关键的测试:test_repair_from_return()。
它验证的就是这条链:
- 先创建并完成 delivery picking
- 再创建 return picking 并完成
- 从 return picking 调
action_repair_return() - 创建 repair order
- 在 repair 里加一条部件 line
- 开始维修并完工
- 断言 repair 成功 done
- 同时断言 return picking 本身不会被 repair 的部件 move 污染
测试最后还特别验证了三件事:
repair.location_id == return_picking.location_dest_idrepair.partner_id == return_picking.partner_idrepair.picking_type_id == warehouse.repair_type_id
这三个断言其实就是标准设计的缩影:
维修单应该接住退货单的“库位、客户、维修操作类型”上下文,但维修过程本身不应该把退货单改造成另一张混合单据。
为什么很多项目会觉得标准 Repair 不够用
因为标准版的关注点是:
- 库存逻辑正确
- 追溯链正确
- 维修过程可记录
而业务方真正想要的,往往是:
- 一个界面里看完整个售后返修过程
- 客户退回、维修中、返还客户全部自动推进
- 售后工单、物流单、费用单、收款单彼此联动
这就会出现一个典型落差:
标准版回答的是
“库存和维修在系统里怎么建模才严谨?”
项目现场问的是
“客服和仓库怎么少点几次按钮?”
所以二开需求几乎是必然的。
实战二开方案:推荐怎么改
下面给你一个比较稳的方案,不求炫技,只求后期可维护。
方案一:保守型二开 —— 保持标准对象,补自动化串联
这是我最推荐的做法。
目标
不改标准 Repair 的核心建模,只补“串流程”的能力。
做法
1)在退货单完成后提供“一键创建维修单”
实际上标准已经有 action_repair_return(),你可以做的是:
- 增加更显眼的按钮
- 自动带入更多默认值
- 限制只有特定售后退货类型才能创建 repair
2)在维修单上补“返还客户”动作
当 repair.order 完工后,新增一个按钮,例如:
action_create_redelivery()
它去自动创建一张 outgoing picking,把修好的产品从返修库存位发回客户。
3)在 repair.order 上增加返还状态字段
例如:
redelivery_picking_idredelivery_stateis_returned_to_customer
这样前台人员看 repair 单就能知道:
- 货退回来没
- 修完没
- 已发回没
优点
- 对标准模型侵入小
- 升级风险低
- 物流和维修边界仍然清楚
- 适合大多数项目
风险点
- repair 完工和返还客户还是两步,只是系统帮你串起来
- 如果想做非常强的一体化售后台账,还得继续扩展
方案二:工单型二开 —— 建统一售后单,再驱动库存 / 维修
如果业务更重视服务流程,而不是库存对象本身,可以再上一个抽象层。
核心思路
新建一个自己的主模型,比如:
ylhc_after_sales_order
由它统一串:
- 原始销售单 / 交货单
- 退货 picking
- repair order
- redelivery picking
- 费用报价 / 收款 / 退款
Repair Order 退回到一个“子对象”的角色。
适合场景
- 售后流程复杂
- 需要客服统一看板
- 要做审批、责任判定、故障分类、配件报价、签收确认
优点
- 业务视角非常完整
- 前台体验最好
代价
- 开发量更大
- 需要长期维护映射关系
- 容易把标准对象包得太厚,后续升级成本高
如果你的目标只是把“客户返修”做顺一点,我反而不建议一上来就走这条路。
方案三:极简型二开 —— 只补“完工自动生成返还客户出库”
如果你们当前最大的痛点是:
修好了以后还要手工再建一张回寄客户的单
那就直接做一个极简增强:
触发点
在 action_repair_done() 之后补一个钩子。
自动动作
- 按维修单客户创建 outgoing picking
- move 只带 repaired product
- 来源库位取维修完成后的产品所在位置
- 目的地取客户位置
要注意的边界
1)不是所有 repair 完工都要自动返还
有些场景可能是:
- 客户暂不取回
- 转备用机
- 直接报废
- 留仓待确认
所以建议做成:
- picking type 上开关
- 或 repair 单上布尔字段控制
2)要处理序列号 / lot
如果 repair 完工产品有追踪,自动 redelivery 时 lot 不能丢。
3)要处理 owner / company / warehouse
尤其是多仓、多公司、客户寄存所有权场景,别把 owner 逻辑抹掉。
我建议你二开时重点盯这几个落点
1)退货单 → 维修单入口
stock.picking.action_repair_return()- 适合补默认值、按钮、流程校验
2)维修单默认值
repair.order.default_get()- 适合补 lot、故障类型、售后来源、责任人等默认字段
3)维修部件和库位映射
stock.move._get_repair_locations()- 适合做维修区、拆件区、回收区的精细化配置
4)维修完工
repair.order.action_repair_done()- 这是最关键的二开挂点
- 适合补自动建返还出库、自动通知、自动写售后台账
5)售后单据聚合视图
如果前台需要“一个页面看全链路”,建议通过:
- smart button
- computed counters
- timeline / chatter
- related stat fields
来做轻聚合,而不是第一天就重写标准主流程。
最后给一个推荐落地路径
如果让我在项目里落,我会按下面顺序做:
第一阶段:轻改标准
- 退货单一键创建维修单
- 维修单显示原始交货单 / 退货单 / lot / 客户
- 维修完工后可一键创建返还客户出库
第二阶段:补售后视图
- repair 上聚合显示返修状态
- 增加 smart button 跳转 return / redelivery
- 增加客服可看的简单售后跟踪页
第三阶段:再决定是否上统一售后主单
只有当你们业务真的存在下面这些强需求时再做:
- 多环节审批
- 多次返修
- 多轮报价
- 检测费 / 材料费 / 人工费拆分
- 售后 SLA / 服务台联动
这样做的好处是:
先把标准 Repair 用到极致,再决定是否值得为业务流程再包一层。
这比一开始就造一个巨大的售后主单更稳。
一句话总结
Odoo 标准 Repair 对“客户发回待维修产品”的处理逻辑是:先用 Return Picking 把货收回,再由 Repair Order 接手维修过程,完工时只负责库存落账,不默认负责返还客户;项目里的主要二开价值,通常就落在“退货单到维修单的自动串联”和“维修完工后的自动返还客户”这两段。
DISCUSSION
评论区