1. 新增预警表添加工单号字段,支持按工单号检索预警 2. 添加工单过滤在制品/绩效报表/预警中心查询 3. 补全WMS台账字段口径与权限配置 4. 修复备料单与数量不符处理流程 5. 新增配送进度/库位联动/自动出库功能 6. 修复AGV配送状态更新逻辑 7. 优化退料与库存管理细节
23 KiB
出库与配送闭环 · 最终方案(2026-09-19 定稿)
本文为唯一版本,覆盖此前所有草稿;外部建议(DeepSeek 两轮)仅作参考,逐条给了采纳/否决理由。 全部结论基于真实代码逐行核实,位置标注
文件:行。 本文是方案,未改任何代码。全部改动需授权后执行。
0. 结论先行
- 现状不是"缺功能",而是"同一份料的库存被扣两次、同一个字段被两个语义写"。 这是数据正确性缺陷,必须先于补功能修 —— 否则功能越全,账越乱。
- 外部方案描述的「四条链路」约 70% 已在代码里跑着(工单物料台账、AGV 任务、接料确认、退库、手动出库、接驳台主数据、叫料、分批出库、工位级拆单、工序-物料绑定全部已实现)。照它重建会推翻现有可用结构。
- 唯一真空白:数量不符的处理侧(上报有、处理无)。已确认
material_qty_report.resolved_at是从未被写入的死字段。 - 1 个真 bug:叫料单不写工位号 → 工位终端「待接料」查不到自己叫的料(本轮逐行复核再次确认)。
- 改进方向:修 1 处重复扣减、拆 2 个被污染的字段、补 1 处空白、修 1 个 bug、语义化 1 个枚举。
- 已定调:库存扣减走 A(出库扣、反冲只核销)。但 A 有三个前提必须先落,否则会从"双扣"变成"漏扣"(§3.2)。
- 已否决:在 MES 建「线边库存」实体表(客户从未提出、可完全派生、会引出第三本账)。
1. 根因:三个"唯一性"缺失
1.1 inventories.quantity 的全部写入点(事实核查)
| # | 位置 | 触发动作 | 上限校验 |
|---|---|---|---|
| 1 | bj_power_wms/internal/handler/outbound.go:107 工单出库 |
仓管发料 | ✅ 409 |
| 2 | bj_power_wms/internal/handler/outbound.go:436 通用出库 |
仓管发料 | ✅ 409 |
| 3 | bj_power_wms/internal/handler/outbound.go:deductStockHandler(/api/internal/stock/deduct) |
MES 备料自动出库 | ✅ 409 |
| 4 | bj_power_wms/internal/handler/backflush.go:192 fifoConsume |
报工反冲 | ❌ 无任何上限 |
第 3 条是关键:MES logic/material.go:231 的备料自动出库走的正是 /api/internal/stock/deduct,已经扣过一次库存;随后 MES 报工 logic/workpiece.go:379 调 /api/internal/material/backflush,同一份料又被 fifoConsume 扣一次。
料只出一次门,库存扣两次。 且反冲按 FIFO 从"当前在库任意合格批次"扣,不认工位实领的那批 —— 领料后若有新批次入库,还会扣错批。
1.2 out_qty / sent_qty 的多写者
WMS order_material_ledger.out_qty(字段注释"累计已出库数量")有 3 个写者,其中 1 个语义完全不同:
| 写者 | 语义 | 校验 |
|---|---|---|
outbound.go 工单出库 |
已发出 | ✅ |
outbound.go:deductStockHandler |
已发出 | ✅ |
backflush.go:100 newOut := ledger.OutQty + consumed |
已消耗 | ❌ 无上限 → 可 > total_qty |
MES material_request.sent_qty 有 2 个写者,语义互斥:
| 写者 | 语义 | 后果 |
|---|---|---|
logic/material.go:241 AddSentQty(sent)(自动出库) |
已发出 | 同时是 :192 need = ReqQty − SentQty 的幂等基准 |
logic/material.go:383 SetSentQty(newSent)(接料) |
已接料 | 把 sent_qty 顶到 ReqQty → need 恒为 0 → 后续补发永久失效;且 add<=0 时算出的实数不落库 → 实收量根本没记 |
1.3 三处结构性缺陷
| # | 缺陷 | 证据 |
|---|---|---|
| A | 反冲被实现成了"第二次出库" | backflush.go:63 创建 outbound_type=backflush 出库单 + :192 扣库存 + :100 累加 out_qty。已核实该出库单无任何消费方(列表/导出按 type 过滤,导出 outbound.go:641 只认 workorder,反冲单被归入"通用出库")→ 纯账目污染 |
| B | 退库不回收缺口 | return_order.go:59/72 建单 PENDING → :137 收货 RECEIVED → :158 AddQuantity(ro.Qty) 回库存,但不回写台账。来源确认是工位退料(MES logic/station_ops.go:128 调 wmsclient.CreateReturnOrder,注释「工位退料回库房」) |
| C | 台账缺字段 | schema/order_material_ledger.go 仅有 total_qty / out_qty / status / target_station / completed_at / remark,无 consumed_qty、无 returned_qty → 消耗与退回无处安放,只能挤进 out_qty |
1.4 "退库 → 补发"死锁(本轮新发现,外部建议与上一版方案均未点到)
outbound.go 与 deductStockHandler 的超量判定都是 newOutQty > TotalQty → 409,而退库不回写台账:
| 步骤 | total_qty | out_qty | 结果 |
|---|---|---|---|
| 出库 100 | 100 | 100 | 领料完结 |
| 工位退料 20(多发) | 100 | 100 | 库存 +20,台账缺口仍为 0 |
| 工位再叫料 20 | 100 | 120 > 100 | 409 拒绝 → 补料永远发不出去 |
→ 必须引入 returned_qty,缺口与完结判定改净额。
1.5 其他
- 补料与备料同表同状态无来源标记(无
source),AGV 待发料列表无法按"工位急件优先"排序。 - 接料实收量不落库,与"数量不符"无法自动对账。
- 出库单无
need_agv,分不清"这批走没走车"。 - 数据现状(硬证据,查库):
order_material_ledgers0 行、outbound_orders0 行 → 隐患尚未触发,库里无坏数据。属"上线即坏",非"已经坏了"。
2. 目标态:三条铁律
铁律 1 —— 一次物理位移 = 一次库存账变
inventories.quantity 只在两处变:出库(−)、退库收货(+)。消耗不动库存。
铁律 2 —— 一个字段只有一个写者
| 字段 | 唯一写者 | 语义 |
|---|---|---|
inventories.quantity |
出库单 / 退库收货 | 仓库实物 |
order_material_ledger.out_qty |
出库(含自动出库) | 已发出 |
order_material_ledger.returned_qty 🆕 |
退库收货(仅挂工单的) | 已退回 |
order_material_ledger.consumed_qty 🆕 |
报工反冲 | 已消耗 |
material_request.sent_qty |
自动出库 | 已发出 |
material_request.received_qty 🆕 |
工位接料 | 已接料 |
铁律 3 —— 不新增账本,在途/线边全部派生
缺口 gap = total_qty − (out_qty − returned_qty) ← 净额,修 1.4 死锁
在途 in_transit = out_qty − received_qty
线边 line_side = received_qty − consumed_qty − 工位退回量
恒等式:入库总量 = 在库 + 在途 + 线边 + 已消耗 + 退库回仓 + 盘点差异
派生依据全部现成:在途 → agv_task(已有 DISPATCHED/DELIVERING/DONE 与 arrived_at);线边 → material_request;已消耗 → ledger.consumed_qty。不需要新表。
四时点口径表(对外沟通用)
| 时点 | 触发 | 写什么 | 动库存 |
|---|---|---|---|
| T1 出库提交 | 仓管 / MES 自动出库 | ledger.out_qty += n、mr.sent_qty += n |
✅ 唯一减点 |
| T2 AGV 送达 | RCS 回调 | agv_task.status / arrived_at |
❌ |
| T3 工位接料 | 工位终端扫码 | mr.received_qty += n |
❌ |
| T4 报工消耗 | MES 报工 | ledger.consumed_qty += n |
❌ |
| T5 退库收货 | WMS 退库确认 | ledger.returned_qty += n |
✅ 加点 |
3. 方案
3.1 P0(必须先修)—— 数据正确性:不改,库存与台账本身就是错的
0-1 反冲停扣库存、停写 out_qty、停产反冲出库单
文件:bj_power_wms/internal/handler/backflush.go
- 删除
:63-77反冲出库单创建(已核实无任何消费方,删除零影响);outboundNo随之去掉。 - 删除
:86对fifoConsume的调用;:145fifoConsume整体删除;其中写inventories(:174SetStatus("出库")、:192AddQuantity(-take))与OutboundDetail.Create(:180/:200)的分支全部删除。 - 台账改写新字段:
SetConsumedQty(ledger.ConsumedQty + n);完结判定改净额(见 0-2)。 - 新增核销上限:
可核销量 = (out_qty − returned_qty) − consumed_qty,实际核销min(应核销, 可核销量);超出部分不写台账,落差异记录 + 预警(material_over_consume),不阻断报工(生产线不能被系统卡死)。响应体保留consumed(实际核销)与shortfall(未核销额)。
0-2 台账加两列 + 退库回收缺口
文件:bj_power_wms/schema/order_material_ledger.go、internal/handler/return_order.go、internal/db/db.go
- schema 加
consumed_qty(默认 0,「累计已消耗数量(报工反冲)」)、returned_qty(默认 0,「累计已退回数量」)。 - ent 重生成:
cd bj_power_wms && go run -tags entgenerate ./tools。 internal/db/db.go的applyColumnPatches登记两列(带默认值 → 无需进applyPreMigratePatches,不阻断启动)。return_order.go收货(:137RECEIVED)事务内:若退库单挂工单 →ledger.returned_qty += qty;不挂工单(仓库自身退料)不动台账。
0-3 所有超量判定改净额
文件:bj_power_wms/internal/handler/outbound.go
deductStockHandler与工单出库的 409 判定:newOutQty > TotalQty→newOutQty > TotalQty + ReturnedQty。- 「领料完结」判定同步改净额。
- 错误文案中文化:
出库数量超过工单需求(需求 X,已退 Y,累计将达 Z)。
0-4 MES 接料改写 received_qty
文件:bj_power_mes/schema/material_request.go、internal/logic/material.go:366-395
- schema 加
received_qty(默认 0)、received_at(可选)、receive_by(可选)。 ReceiveMaterialRequest::383-387改为SetReceivedQty + SetReceivedAt + SetReceiveBy(operator),删掉SetSentQty;add<=0的兜底改用ReqQty − ReceivedQty计算。sent_qty语义锁定为「已发出」(唯一写者 = 自动出库:241),need = ReqQty − SentQty保持成立。- 「DONE」推进改由
received_qty >= req_qty触发。 - MES 侧
internal/db/db.go的schemaPatchSQL登记三个新列。
0-5 出库类型语义化
outbound_type最终三值:workorder(备料/工单领料)/refill(补料)/general(手动出库),移除backflush。- 导出与列表类型文案同步(
outbound.go:641的if ob.OutboundType == "workorder"分支补refill)。
P0 验收:连续跑「备料自动出库 → 报工反冲」,inventories.quantity 只减一次;out_qty 不因反冲变化;out_qty − returned_qty 与 consumed_qty 均可解释。
3.2 A 方案的三个前提(已采纳 A,但前提不落则变"漏扣")
| # | 前提 | 不做的后果 |
|---|---|---|
| 1 | 反冲核销有上限 = net_out_qty − consumed_qty |
工位自取的料(辅料/胶水/现场替换)被全额核销,但库存从没扣过 → 库存永不下降 |
| 2 | 退库必须回收缺口(returned_qty) |
§1.4 死锁:退料后补料被 409 永久拦死 |
| 3 | 出库扣到实领批次/SN | 只解决"扣几次",不解决"扣哪批"(现有出库本就按批次/SN 指定,已满足) |
3.3 P1(该做,但不动现有数据)—— 功能空白与真 bug
1-1 修 bug:叫料单补工位号(零成本,必做)
文件:bj_power_mes/internal/logic/line.go CallMaterial
- 已逐行复核:建单只有
SetRequestNo/OrderNo/PlanDate/MaterialCode/MaterialName/Unit/ManageMode/ReqQty/Status/TargetDock/Operator,无SetStationNo、无SetProcessCode→station_no=0。 - 而工位终端「待接料」按
stationNo=本工位查询 → 工位看不见自己叫的料(扫码接料直查备料单号,不受影响,所以隐蔽)。 - 修法:
SetStationNo(...)+SetProcessCode(...)(按该物料在本工位的装配工序取,取不到则 0)+SetSource("REFILL")。 - 一次性数据修复:历史
stationNo=0的叫料单按target_dock → dock.station_no映射回填(幂等)。
1-2 补空白:数量不符处理侧 —— 真源留在 MES,处理页也放 MES
理由(关键,与上一版方案的结论相反):
- 上报链路、表、预警全在 MES:工位终端
InspectPanel ⑥→/api/qty-report→ 代理 MES/api/internal/station/qty-report→logic.CreateQtyReport(logic/station_ops.go:64)落material_qty_report+ 预警type=qty_diff。 - 处理动作在 MES 侧可完全闭环:补发 = 生成
source=REFILL备料单 → 走已有自动出库链路;退库 = 走已有wmsclient.CreateReturnOrder(MES→WMS 方向已存在);调整 = 本地记账。 - 仓管本来就在 MES 操作备料(
MaterialRequest.vue已含「按日排产生成备料」「一键自动出库」)。 - 放 WMS 虽技术可行(
bj_power_wms/internal/mesclient/client.go已具备反向调用能力),但需新增 3 个接口,且补发要绕WMS→MES→WMS一圈,没有理由多绕。
实施:
- 表:
material_qty_report已有status(PENDING/RESOLVED)、resolved_at(当前死字段)→ 只需补handle_type(REFILL/RETURN/ADJUST)、handle_note、handled_by、handle_ref_no。 - 接口:
GET /api/v1/qty-diffs(状态/工位/物料/时间筛选 + 后端 SQL 真分页)、GET /api/v1/qty-diffs/:id(明细,配el-drawer懒加载)、POST /api/v1/qty-diffs/:id/handle。 - 处理分支:
REFILL→ 生成source=REFILL备料单(可走自动出库)→ 回写handle_ref_noRETURN→wmsclient.CreateReturnOrder→ 回写退库单号ADJUST→ 仅记账关单
- 照片:走统一附件体系
biz_type=QTY_DIFF+biz_id=差异记录ID,不加列。 - 权限:新增
produce.qtydiff:view/produce.qtydiff:handle,同时登记进logic/seed.go的 menus 与 buttons 及handler/perm.go的pathPermMap(本项目栽过一次"菜单码未登记 → 任何角色永远授不了权")。 - 前端:MES 新增「数量不符处理」页 +
MainLayout菜单 +help.js。 - 客户 DOC 同步:
seed.go:165 migrateLegacyQtyReport留的「明细由 WMS 处理」改为「在 MES 数量不符处理页处理」。
1-3 补料来源标记
material_request加source(默认PLAN);叫料与差异补发写REFILL。AutoOutboundRequests与 WMS 待发料列表按source=REFILL优先排序(不新建单据类型、不新建优先级字段)。- 补料出库默认叫 AGV(沿用现有 AGV 下发条件,不新增
need_agv字段)。
1-4 只读「配送进度」聚合
- 不给出库单加五态(状态分散在
agv_task/material_request,硬塞会互斗)。改一条只读接口按出库单号聚合:出库单(已出库)+ AGV 任务(配送中/已送达)+ 备料单(已接料/完结)。
3.4 P2(可有可无)—— 可选增强
| 项 | 说明 |
|---|---|
| 「物料去向」只读对账页 | 按物料展示 在库/在途/线边/已消耗,直接验 §2 恒等式 |
| 在途对账预警 | out_qty − received_qty 长期 > 0(滞留在途)或 < 0(未发先接)→ 预警 |
| 消耗超接料预警 | consumed_qty > received_qty → 提示"未接料先消耗",不阻断 |
4. 明确不做(含理由)
| 不做 | 理由 |
|---|---|
| MES「线边库存」实体表 | ① 客户《软件系统问题-hd.doc》全文检索「线边/工位库存/工位上的料/余料」命中 0,只提过仓库盘点(第 102 段)→ 为未提出的需求建表即过度设计;② 会在 WMS 库存 + WMS 台账 + MES 备料单 + MES 线边四处同步,本项目已栽过一次跨系统对不上;③ 线边量可由 received_qty − consumed_qty 完全派生。唯一例外:客户明确要求"盘工位上的料"时再建,并接受同步成本(列入待确认) |
| 出库单五态(待出库→已出库→已送达→已接料→已完成) | 状态分散在 agv_task / material_request,硬塞进出库单会产生三份互相打架的状态副本。改只读聚合(1-4) |
新建 difference_report 表 |
material_qty_report 已存在且字段齐全(含 resolved_at 死字段),补 4 列即可,不造同义表 |
| 双写两份账本 | 违反单一真源。sent_qty 的教训已经很清楚 |
| 接料"可选"、AGV"可选" | 接料确认是消耗核销的唯一依据,保持强校验 |
| 补料另建单据类型 | 用 source 字段标记即可,避免两套单据各自维护缺口 |
| OA 对接 | 客户已定:永久不做,不再提 |
5. 影响面与风险
| 改动 | 影响 | 处置 |
|---|---|---|
| 反冲单不再产生 | 出库列表/导出少掉 backflush 类单据 |
已核实无消费方;导出文案补 refill |
| 反冲不再扣库存 | 若存在"从未出库的料被消耗"(工位自取/辅料) | 由 0-1 的核销上限兜住:超出部分落差异+预警,不写台账、不阻断报工 |
sent_qty 语义锁定 |
MES 页面/看板若把 sent_qty 当"已接料"展示 |
一并改为读 received_qty |
| 台账新增两列 | 存量 0 行,无回填 | 直接加列 |
material_request 新增列 |
存量可能有数据 | 均带默认值,安全 |
6. 验收标准
- 库存账:
备料自动出库 → 报工反冲连续两轮,inventories.quantity只在出库时减一次;反冲前后库存不变。 - 台账账:
out_qty只由出库写;反冲只增consumed_qty;超量报工不使out_qty > total_qty。 - 死锁解除:出库满额 → 工位退料 → 再补料,不再被 409 拦住。
- 接料账:
received_qty落库;接料后need = ReqQty − SentQty不被污染,补发仍可执行。 - 叫料 bug:工位终端叫料后,该工位「待接料」列表能看到自己叫的料。
- 数量不符闭环:工位上报 → 处理页可见 → 补发/退库/调整 → 差异关闭,生成单据可追溯。
- 负向验证:非 admin 角色在缺权限时看不到/点不动处理按钮(防"菜单码未登记"老坑)。
7. 待确认
refill补料是否允许超出工单 BOM 需求?现规则是硬 409(含 0-3 净额修正)。补料语义上可能确实需要超额(损耗/报废),那就必须走"数量不符/额外需求"通道而非工单领料通道 —— 需业务确认。本方案默认仍受净额上限约束。- 是否要求「工位线边盘点」?要 → 建
line_stock实体表并接受四处同步成本;不要 → 线边量走派生视图(默认)。 - 数量不符处理页放 MES 与客户 DOC 原写「明细由 WMS 处理」不一致 → 建议改 DOC(理由见 1-2)。
outbound_type由三值变四值(新增refill)是否影响历史统计口径(现存量workorder中是否混有旧叫料出库)。- 环境依赖:AGV 真机联调需海康 RCS 地址与 appKey/appSecret(现
Agv.EnableMock: true);附件归档需外部备份路径(现ArchiveDir为空)。
附:实施顺序(一次性做完,不分阶段交付)
【编号说明】P0=必须先修(P0-1..P0-6);P1=该做(P1-1..P1-4);P2=可选。
P0-1 WMS schema: ledger 加 consumed_qty / returned_qty → ent 重生成 → applyColumnPatches 登记
P0-2 MES schema: material_request 加 received_qty/received_at/receive_by → ent 重生成 → schemaPatchSQL 登记
P0-3 WMS backflush.go: 删出库单 + 删 fifoConsume + 改 consumed_qty + 加核销上限
P0-4 WMS return_order.go: 收货回写 returned_qty
P0-5 WMS outbound.go: 两处 409 与完结判定改净额
P0-6 MES material.go: ReceiveMaterialRequest 改写 received_qty
P1-1 MES line.go: CallMaterial 补 station_no/process_code/source + 历史回填
P1-2 MES: material_qty_report 加 4 列 + 处理接口(真分页) + 权限(菜单+按钮) + 页面 + help.js
P1-3 MES: material_request 加 source + 排序优先
P1-4 MES: 只读配送进度聚合接口
P2 只读「物料去向」对账页 + 两条对账预警(可选)
DOC 同步客户问题 DOC;改「明细由 WMS 处理」措辞
增强⑧「物料去向对账」最优方案(2026-09-19 定稿,待授权实施)
0. 结论
- 对账后端已经 100% 存在:上一轮实施时顺手落了
GET /api/v1/material-requests/progress(MES 聚合 WMSledger/progress+ 本地已接料),返回字段正是对账表全部列——需求/已发出/已退回/已接料/已消耗/在途/线边/待发缺口/台账状态。备料单页已有「配送进度」弹窗在读它。 - DeepSeek 方案定位对(只读/不建表/不做自动修正),但四个硬伤:①"差异=需求−已出、非0标红"是错的(在产工单缺口天然非0,照做满屏假红);②示例第3行自己算错符号;③需求总量重算 BOM 是重复建设(total_qty 已随台账同步,重算必与备料时点不一致);④工位余公式漏了退库。
- 剩余工作量 = 纯前端:把已有进度弹窗升级为对账视图(异常标记 + 明细展开 + 导出)。后端 0 行新代码(明细过滤
/event-logs已支持 orderNo+eventType)。
1. 公式(纠正版,与后端派生完全一致)
待发缺口 gap = totalQty − (outQty − returnedQty) ← 生产中非0是常态,不标红
在途 inTransit = outQty − receivedQty
工位余 lineSide = receivedQty − consumedQty − returnedQty ← DeepSeek 漏了退库
恒等式:入库总量 = 在库 + 在途 + 工位余 + 已消耗 + 退库回仓 + 盘点差异
2. 异常判定(三条硬异常,全部可从现有字段判定,前端现算)
| 异常 | 判定 | 含义 | 标记 |
|---|---|---|---|
| 超领 | netOut > totalQty | 实领超过 BOM(超领原因在 WMS 留痕 material.over) | 红 |
| 送达未接 | inTransit > 0 且该工单物料存在 status=已送达 的出库单 | 料到了工位没人扫码确认 | 红 |
| 完结剩料 | 台账状态=领料完结 且 lineSide > 0 | 工单做完了工位还压着料,没消耗也没退库 | 红 |
| 在途等待 | inTransit > 0 且无已送达出库单 | AGV 正在送,正常 | 黄(提醒) |
3. 页面形态(升级既有弹窗,不新建菜单)
- 位置:MES 备料单页「配送进度」弹窗 → 改名「物料去向对账」。
- 列:现有 9 列 + 「异常」列(红/黄/无,hover 显示判定原因)。
- 行展开明细(全部现成数据,无后端改动):
- 出库明细 → 行内备料单已含出库单号;超领原因在 WMS 出库记录页(按工单筛)查看,不跨系统重复建设;
- 接料明细 → material_request 行(receivedQty/receiveBy/receivedAt);
- 消耗明细 →
/event-logs?orderNo=xxx&eventType=material.backflush(含工位/数量/缺口)。
- 导出:xlsx,复用 exportXlsxFetch。
- 权限:沿用 produce.material(只读,不新增权限码)。
4. 明确不做
- 不重算 BOM 需求量(读 total_qty);
- 不做"差异=需求−已出"列与标红(就是 gap 列,完结后必为 0,出现非 0 即数据错,属可发现的脏数据而非业务异常);
- 不做跨系统明细钻取(WMS 出库/核销明细由 WMS 出库记录页承担,已可按工单筛选);
- 不做自动对账修正、不建新表、不加权限码。
5. 工作量与顺序
前端单文件(MaterialRequest.vue)+ help.js 一条说明,约半天。排在"①~⑦ 编译验证"之后做。