feat(outbound): 原单退回后可自动生成补发单

背景
----
原单退回只做两件事:良品加回库存 / 不良品转在管台账。但**申请人的需求并没有
被满足** —— 东西交回来了(甚至还是坏的),系统却不提醒任何人、无单据承载
「我要重新领一份」,退回与后续再出库之间也毫无关联。现场只能靠人记住再手建
一张出库申请,而那张单与原单看不出任何关系。

改动
----
· outbound_approval 新增 source_return_id(非空 = 补发单),把「退回 → 补发」
  串成闭环。
· POST /inbound/stock/return-from-outbound 新增 need_reissue / reissue_qty:
  勾选即自动生成一张**免审批**出库单(status=1,直接进入待执行),
  沿用原单出库类型,并关联回本笔退回。

★ 为什么只存退回单 ID,不加 is_reissue 布尔列
  「是不是补发」完全由来源是否存在决定,再加一列就是同一事实的两处存储,
  必然有不同步的一天。也不冗余存原出库单 ID:trans_return 已有 outbound_id。

★ 库存不足 → 整笔回滚(关键取舍)
  补发走 reserve_for_items(strict=True),与出库申请同一口径。不足时抛错,
  退回也一并回滚 —— 若只让补发静默失败,「需要补发」的意图就丢了,
  那正是本功能要解决的问题。库管看到提示后取消勾选即可只做退回过账。

★ 一个被发现的数据约束(改变了原设计)
  原打算把补发单的申请人设为「原出库单的申请人」,但 **trans_outbound 既没有
  申请人字段,也没有指回原审批单的关联**(扫码出库时只把审批单状态置为 3)。
  按 consumer_name 反查会重蹈「重名错绑」的覆辙。故申请人取**当前操作人**,
  原领用人写入备注供人工追溯。
  ⚠ 若业务要求补发单挂在原领用人名下,需要前端在退回弹窗里加一个「补发给谁」
    的人员选择 —— 请确认是否需要。

验证(打桩 JWT 直连真实接口,22 项断言全通过)
  勾选补发 → 免审批单生成、关联退回、预占库存、原领用人入备注;
  不勾选 → 不生成补发单、良品正常回库;
  ★ 库存不足 → 接口拒绝且**退回流水/补发单/退回额度全部未落库**(整笔回滚);
  补发量 > 退回量被拒;库存与数据零残留。
This commit is contained in:
yueli
2026-09-17 11:56:38 +08:00
parent 483fb5336a
commit 7071c89305
3 changed files with 169 additions and 2 deletions

View File

@ -0,0 +1,65 @@
-- =============================================================================
-- 出库 · 原单退回后的「补发单」关联
--
-- 背景
-- 原单退回目前只做两件事:良品加回库存 / 不良品转在管台账。
-- 但**申请人的需求并没有被满足** —— 东西交回来了(甚至还是坏的),
-- 系统却:不提醒任何人、无单据承载「我要重新领一份」、退回与后续再出库
-- 之间毫无关联。现场只能靠人记住再手动新建一张出库申请,且那张单与原单
-- 看不出任何关系。
--
-- 本次改动
-- outbound_approval 新增 source_return_id:非空即表示这是一张由「原单退回」
-- 自动生成的**补发单**,从而把「退回 → 补发」串成闭环,出库记录里也能
-- 一眼看出哪张单是补发的。
--
-- ---------------------------------------------------------------------------
-- ★ 为什么不加 is_reissue 布尔列
-- 「是不是补发」完全由「来源退回单是否存在」决定,再加一个布尔列就是
-- 同一事实的两处存储,必然有不同步的一天(改了 A 忘了 B)。
-- 单列既表达事实,又能直接 join 回原单。
--
-- ★ 为什么只存退回单 ID,不另存原出库单 ID
-- trans_return 本身已有 outbound_id,顺着 source_return_id 一跳即可拿到
-- 原出库明细。冗余存储只会带来「两张单对不上」的风险。
--
-- 幂等:带 IF NOT EXISTS,可重复执行。不含 psql 元命令,DataGrip 可直接执行。
-- =============================================================================
BEGIN;
ALTER TABLE outbound_approval
ADD COLUMN IF NOT EXISTS source_return_id integer;
COMMENT ON COLUMN outbound_approval.source_return_id IS
'补发单来源:trans_return.id。非空即表示本单是由「原单退回」自动生成的补发单';
-- 支撑「这批补发是从哪张退回单来的」与列表标识
CREATE INDEX IF NOT EXISTS ix_outbound_approval_source_return
ON outbound_approval (source_return_id);
COMMIT;
-- =============================================================================
-- 执行后核对
-- =============================================================================
SELECT '=== 1) 新列已就位 ===' AS "核对项";
SELECT column_name, data_type FROM information_schema.columns
WHERE table_name = 'outbound_approval' AND column_name = 'source_return_id';
SELECT '=== 2) 索引已就位 ===' AS "核对项";
SELECT indexname FROM pg_indexes
WHERE tablename = 'outbound_approval' AND indexname = 'ix_outbound_approval_source_return';
SELECT '=== 3) 存量补发单(新功能上线前应为 0)===' AS "核对项";
SELECT count(*) AS 补发单数 FROM outbound_approval WHERE source_return_id IS NOT NULL;
-- =============================================================================
-- 回滚段
-- =============================================================================
-- BEGIN;
-- DROP INDEX IF EXISTS ix_outbound_approval_source_return;
-- ALTER TABLE outbound_approval DROP COLUMN IF EXISTS source_return_id;
-- COMMIT;