fix(scrap): 补上遗漏的 phase14 迁移(不良品报废的库位/批次快照)

问题:前面几个报废提交里,inventory-backend/app/models/transaction.py 已经
引用了 db_migrations/phase14_defective_goods_location_snapshot.sql(两处注释
指向它),但**该文件本身没被提交** —— 代码与迁移对不上,别人拉下来跑不起来。

内容:为 trans_defective_goods 增加 warehouse_location / batch_number 两列,
并从源库存行回填存量。

为什么需要这两列:报废台账页与报废申请单明细上,凡是不良品来源的行,库位与
批次此前一律显示 "-" —— 因为应用层硬编码了空串,理由是「源库存行可能已被
物理删除」。该担忧对 material_name/spec_model 成立(所以它们有冗余快照),
但库位/批次当时**没有**快照列,一律置空等于放弃了「源行还在」这个绝大多数
情形。补上快照后,退回那一刻 stock_row 已 with_for_update() 锁在手里,
库位/批次随手可取,落库后即使源行日后被删除,报表依然有值。

口径:存「原库位」(这批货最初在哪),与三张库存表来源的报废记录一致;
不良品区的位置本系统未记录。batch_number 对 stock_product 来源存的是
serial_number(成品表无 batch_number 列),与 scrap.py 的 _from_stock 对齐。

安全性:纯追加式 DDL,无 NOT NULL、无 DEFAULT,幂等可重复执行。
已在本机执行验证:存量 1 行全部回填,源行已删取不到 0 行。
This commit is contained in:
yueli
2026-09-23 17:16:08 +08:00
parent 3e311048ef
commit c5356a27ed

View File

@ -0,0 +1,108 @@
-- =============================================================================
-- 不良品在管台账:补「原库位 / 批次(序列号)」快照列
--
-- 背景
-- trans_defective_goods 只冗余了 material_name / spec_model,却没有存库位与
-- 批次。于是报废台账页、报废申请单明细上,凡是不良品来源的行,库位与
-- 批次一律显示 "-" —— 而它们的源库存行往往**还在**,本该取得到。
--
-- 应用层原先的处置是在 _resolve_materials 与 DefectiveScrapAdapter.snapshot
-- 里直接硬编码空串,注释理由是「源库存行可能已被物理删除」。这个担忧对
-- material_name/spec_model 成立(所以它们有冗余快照),但库位/批次**没有**
-- 快照列,一律置空等于放弃了「源行还在」这个绝大多数情形。
--
-- 本次改动(A+B)
-- A. 应用层改为「快照优先 + 回查源库存行兜底」(本次只加列,逻辑在代码侧)
-- B. 本文件补两列,把「源行可能被删」这个担忧真正解决掉:退回那一刻
-- stock_row 已 with_for_update() 锁在手里,库位/批次随手可取,
-- 落库后即使源行日后被物理删除,报表依然有值。
--
-- 口径(2026-09-23 与业务确认)
-- 存的是「**原库位**」—— 这批货最初在哪,而不是坏件当前所在的不良品区。
-- 与三张库存表来源的报废记录口径一致。不良品区的位置本系统未记录。
--
-- ★ batch_number 列对 stock_product 来源存的是 serial_number:
-- 成品表没有 batch_number 列(见 information_schema),取值口径沿用
-- 应用层的 COALESCE(batch_number, serial_number),与 _from_stock 一致。
-- 故本列语义为「批次或序列号」,与前端列名「批次/序列号」对齐。
--
-- 安全性:纯追加式 DDL,无 NOT NULL、无 DEFAULT,既有行取值 NULL。
-- 幂等:IF NOT EXISTS + 带 WHERE 的 UPDATE,可重复执行。
--
-- 执行
-- docker exec -i inventory_db psql -U test -d inventory_system < 本文件
-- =============================================================================
BEGIN;
-- ---------------------------------------------------------------------------
-- 1) 快照列(长度与三张库存表对齐,均为 varchar(100))
-- ---------------------------------------------------------------------------
ALTER TABLE trans_defective_goods
ADD COLUMN IF NOT EXISTS warehouse_location varchar(100),
ADD COLUMN IF NOT EXISTS batch_number varchar(100);
COMMENT ON COLUMN trans_defective_goods.warehouse_location IS
'原库位快照(退回时取自源库存行)。源行被物理删除时此处仍有值,故报表优先用它';
COMMENT ON COLUMN trans_defective_goods.batch_number IS
'批次/序列号快照(退回时取自源库存行的 batch_number,成品表缺失时取 serial_number)';
-- ---------------------------------------------------------------------------
-- 2) 存量回填:按 source_table + stock_id 回查源库存行
-- 三张表分别 UPDATE(多态关联,无法用一条语句表达)。
-- WHERE warehouse_location IS NULL 保证幂等,且不覆盖已回填过的值。
--
-- 回填不到的(源行已被物理删除)保持 NULL —— 刻意不写成空串,
-- 这样下面的核对能数出「真正取不到」的行数,与「已确认无库位」区分开。
-- ---------------------------------------------------------------------------
UPDATE trans_defective_goods g
SET warehouse_location = COALESCE(b.warehouse_location, ''),
batch_number = COALESCE(NULLIF(b.batch_number, ''), b.serial_number, '')
FROM stock_buy b
WHERE g.source_table = 'stock_buy'
AND g.stock_id = b.id
AND g.warehouse_location IS NULL;
UPDATE trans_defective_goods g
SET warehouse_location = COALESCE(s.warehouse_location, ''),
batch_number = COALESCE(NULLIF(s.batch_number, ''), s.serial_number, '')
FROM stock_semi s
WHERE g.source_table = 'stock_semi'
AND g.stock_id = s.id
AND g.warehouse_location IS NULL;
-- ★ 成品表无 batch_number 列,只取 serial_number
UPDATE trans_defective_goods g
SET warehouse_location = COALESCE(p.warehouse_location, ''),
batch_number = COALESCE(p.serial_number, '')
FROM stock_product p
WHERE g.source_table = 'stock_product'
AND g.stock_id = p.id
AND g.warehouse_location IS NULL;
COMMIT;
-- =============================================================================
-- 执行后核对
-- =============================================================================
SELECT '=== 1) 回填结果:已回填 / 源行已删取不到 / 无需回填 ===' AS "核对项";
SELECT count(*) FILTER (WHERE warehouse_location IS NOT NULL) AS 已回填,
count(*) FILTER (WHERE warehouse_location IS NULL) AS 源行已删_取不到,
count(*) AS 台账总行数
FROM trans_defective_goods;
SELECT '=== 2) ★ 源行已删的明细(取不到库位属预期,应逐条确认是否为真)===' AS "核对项";
SELECT id, source_table, stock_id, sku, status
FROM trans_defective_goods
WHERE warehouse_location IS NULL
ORDER BY id;
-- =============================================================================
-- 回滚段(仅撤销本迁移;回填值与「原本就是空」无法区分)
-- =============================================================================
-- BEGIN;
-- ALTER TABLE trans_defective_goods DROP COLUMN IF EXISTS warehouse_location;
-- ALTER TABLE trans_defective_goods DROP COLUMN IF EXISTS batch_number;
-- COMMIT;