售后返修源码

Odoo Repair 源码解析:客户发回待维修产品的完整链路与二开方案

从交货后的 Reverse Transfer、退货单、Repair Order 绑定、维修部件 stock.move、完工落库存,到返还客户的标准边界与二开设计,讲清 Odoo 维修场景里“客户发回待维修产品”到底是怎么流转的。

Odoo 开发 库存
进阶 开发者 4 分钟阅读
0 评论 0 点赞 0 收藏 20 阅读

结论先行

在 Odoo 标准源码里,客户把产品寄回维修这件事,并不是由 repair.order 直接发起的。

标准链路其实分成两段:

  1. 先走库存退货链路:从原始交货单创建 Return Picking
  2. 再走维修链路:从退货单进入 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 会:

  1. 创建新的退货 picking
  2. 复制原来的 move 形成 return move
  3. 把这些 move 用 origin_returned_move_idmove_orig_idsmove_dest_ids 串回原始流转链

这一步的核心不是“修”,而是:

先把客户手里的货,合法地收回库存系统。

也就是说,在标准设计里,“客户寄回待维修产品”首先是一个库存事实,然后才是一个维修事实

这套设计的好处是追溯清楚:

  • 原来从哪张交货单出去的
  • 现在从哪张退货单退回来的
  • 后面维修单接的是哪次退回

如果你做 lot / serial 追踪,这种拆分非常必要。


第二段:退货单如何进入 Repair Order

repair 模块在 stock.picking 上加了一个动作:action_repair_return()

这个动作不是直接改库存,而是做三件非常关键的事情:

  1. 把当前退货单 ID 放进上下文 default_repair_picking_id
  2. 根据当前仓库带出默认维修操作类型 warehouse.repair_type_id
  3. 把客户 partner_id 一起带进维修单

所以 Repair Order 不是凭空创建的,而是从“这次客户退回”这个上下文里长出来的

随后,repair.order.default_get() 会读取上下文中的:

  • default_repair_picking_id
  • default_repair_lot_id

并把这些值默认带入维修单表单。

这一步的设计很值得注意:

Odoo 没有把退货单和维修单强耦合成一个对象,而是通过“上下文 + 字段绑定”的方式,把两个业务对象接起来。

这样一来:

  • 退货还是库存模块的职责
  • 维修还是 repair 模块的职责
  • 但两者可以无缝衔接

Repair Order 里到底绑定了什么

维修单上最关键的字段是这些:

  • picking_id:这张维修单来自哪张 transfer
  • product_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 = add
  • repair_line_type = remove
  • repair_line_type = recycle

这个设计很 Odoo:

能复用 stock.move,就尽量不要重新发明一套“维修部件流水”。

于是维修过程被拆成三种库存动作:

1)add:新增零件

表示从零件来源库位取料,投入维修过程。

常见流向是:

库存位 → 维修目标位(通常是 production location)

2)remove:拆下坏件

表示从维修位置拆出一个部件,送去拆下件位。

常见流向是:

维修位置 → 拆下件目的位

3)recycle:回收件

表示拆下来的部件不是报废,而是转去可回收位置。

常见流向是:

维修位置 → 回收库位

所以 Repair 并不是一句“修好了”,而是把整个维修过程拆成一组受库存规则约束的 move。

这也解释了为什么标准 Repair 能支持:

  • 零件可用量检查
  • 缺料补货触发
  • 已用数量和计划数量对比
  • lot / serial 追溯

为什么确认维修单时会检查库存

action_validate() 的逻辑可以概括成两层。

第一层:先检查维修部件配置是不是合法

例如部件需求不能是负数。

第二层:检查“待维修产品”是否真的在维修来源位置里存在

这一点很多人第一次看源码会意外。

系统会去查 stock.quant,而且查两类库存:

  1. owner_id = partner_id
  2. owner_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_id
  • location_dest_id = product_location_dest_id
  • repair_id = 当前维修单
  • picking_id = False

而且 move line 上还会通过 consume_line_ids 关联到维修过程中用过的部件 move line。

翻成人话就是:

系统会明确登记:这台修好的产品,是由这张维修单产出的,它消耗过哪些零件流水。

4)把“维修部件 move + 最终产品 move”一起 done

最后 Repair 状态才会变成 done


一个非常关键但经常被忽略的边界

维修完工后生成的最终 product move,没有 picking

这意味着什么?

意味着标准 Repair 完工表示的是:

维修结果已经在库存里落账

而不是:

修好的东西已经寄回客户

所以标准设计天然把下面两件事分开了:

  1. 维修完成
  2. 返还客户

这是标准 Repair 最重要的业务边界之一。

如果项目上要求:

  • 客户寄回后自动进维修单
  • 修完后自动回寄客户
  • 全链路在一个单据视角里闭环

那么就一定会进入二开。


官方测试已经把这条链路说得很清楚

repair 模块里有一个很关键的测试:test_repair_from_return()

它验证的就是这条链:

  1. 先创建并完成 delivery picking
  2. 再创建 return picking 并完成
  3. 从 return picking 调 action_repair_return()
  4. 创建 repair order
  5. 在 repair 里加一条部件 line
  6. 开始维修并完工
  7. 断言 repair 成功 done
  8. 同时断言 return picking 本身不会被 repair 的部件 move 污染

测试最后还特别验证了三件事:

  • repair.location_id == return_picking.location_dest_id
  • repair.partner_id == return_picking.partner_id
  • repair.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_id
  • redelivery_state
  • is_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

评论区

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