Files
KCGL/db_migrations/fill_missing_stock_status.sql
yueli fbc9296056 feat(stock): status 硬隔离与出库通道封堵
激活库存表长期「只写不读」的 status 列,使其成为分配准入的硬门槛。

- inventory_reservation: 新增状态语义单一事实来源(STOCK_STATUS_*/
  allocatable_filter/is_allocatable);verify_scanned 增加状态准入校验
  (覆盖出库与借库两条提交路径)
- outbound: _allocate_bom_requirements 过滤条件加 allocatable_filter,
  非「在库」一律不进入候选集;扫码路由把 ValueError 转为 400
- outbound_service: create_outbound_batch 强制 request_id 必填。原先无单
  时会跳到所谓「散单」分支,而该分支的扣减逻辑从未落地(注释点名的
  _apply_reservation_override 全仓不存在),实测只写台账不扣库存,
  同一批货可被反复出库;get_stock_by_barcode 接入扫码准入;维修单扫码
  补排除「报废转出」;预占再平衡块补 rollback 兜底
- fill_missing_stock_status.sql: 把存量行刷回「在库」。该过滤是
  Fail-Closed 的,不先洗刷会让全库无法出库

破坏性变更:不传 request_id 的出库请求现在会被拒绝。
2026-09-16 15:45:02 +08:00

141 lines
7.6 KiB
PL/PgSQL
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

-- =============================================================================
-- 一次性迁移:库存状态列存量洗刷(配合 status 硬隔离上线)
--
-- 背景
-- 三张库存表stock_buy / stock_semi / stock_product的 status 列自建表起
-- 就是「只写不读」的:入库时写一次 '在库',此后全仓再无任何代码更新它,
-- 分配器也从不看它。现分配器已改为「仅 status = '在库' 才可被分配」
-- (见 app/services/inventory_reservation.py::allocatable_filter
--
-- 该过滤是 Fail-Closed 的status 为 NULL、空串或任何非 '在库' 值的行
-- 都会被排除在候选集之外。因此上线该过滤**之前**必须先跑本脚本,把
-- 本该可用的存量行刷回 '在库',否则它们将集体无法出库。
--
-- 洗刷范围(刻意收敛,只动 status 一列)
-- (status IS NULL OR status <> '在库')
-- AND (stock_quantity > 0 OR available_quantity > 0)
--
-- · 为什么带 stock_quantity 条件,而不只是 available_quantity
-- 借出未还的行 available_quantity 可能为 0 而 stock_quantity > 0。
-- 归还时 process_return() 会给该行加回 available_quantity届时若
-- status 仍为 NULL这行就永远出不去了。此处一并覆盖堵住这个未来缺口。
--
-- · 为什么排除「无实物的行」stock_quantity / available_quantity 均为空或 0
-- 这类行多为历史脏数据(实测 stock_buy id=807 是整行全空的幽灵记录)。
-- 把它们刷成 '在库' 等于凭空声明一批并不存在的合格库存,是有害的。
-- 它们本来就不会被分配(分配器要求 available_quantity > 0保持原样无害。
-- 可用文末「0-b)」的查询把它们捞出来人工核对。
--
-- ● 为什么**不**刷 quality_status / inspection_status对原始需求的偏离务必知悉
-- 1) stock_buy **没有** quality_status 列,只有 inspection_status。
-- 统一写 `SET quality_status='合格'` 会直接报 column does not exist
-- 脚本根本跑不完。
-- 2) 更要命的是语义stock_semi 有 24 行 quality_status='待检'、
-- stock_buy 有 2042 行 inspection_status='未检'(其中 1766 行可用量 > 0
-- 把它们刷成 '合格' 属于**伪造质检结论** —— 等于把从未检验过的货
-- 当成合格品放行,与本次改造「不让坏件流出」的目的正好相反。
-- 因此本脚本只洗 status。如确有补空需求见文末「3) 可选:质量列补空」,
-- 那里只补 NULL绝不覆盖任何已有取值。
--
-- 幂等性
-- WHERE 条件保证可重复执行:第二次执行影响 0 行。
--
-- 执行
-- docker exec -i inventory_db psql -U test -d inventory_system < 本文件
-- =============================================================================
BEGIN;
-- ---------------------------------------------------------------------------
-- 0) 执行前预览:将被洗刷的行数(建议先单独跑这一段确认无误再跑 1
--
-- ★ 用 COALESCE 而非裸比较:库存表允许数量列为 NULL而 `NULL > 0` 求值为
-- NULL非真裸写会让「数量为 NULL」的脏行既不进洗刷、也不进 0-b 诊断,
-- 在两次查询里凭空消失。COALESCE 让两个条件互为补集,口径严密。
-- ---------------------------------------------------------------------------
SELECT 'stock_buy' AS tbl, count(*) AS will_update
FROM stock_buy
WHERE (status IS NULL OR status <> '在库')
AND (COALESCE(stock_quantity, 0) > 0 OR COALESCE(available_quantity, 0) > 0)
UNION ALL
SELECT 'stock_semi', count(*)
FROM stock_semi
WHERE (status IS NULL OR status <> '在库')
AND (COALESCE(stock_quantity, 0) > 0 OR COALESCE(available_quantity, 0) > 0)
UNION ALL
SELECT 'stock_product', count(*)
FROM stock_product
WHERE (status IS NULL OR status <> '在库')
AND (COALESCE(stock_quantity, 0) > 0 OR COALESCE(available_quantity, 0) > 0);
-- ---------------------------------------------------------------------------
-- 0-b) 被跳过的行(无实物但状态异常,多为历史脏数据)—— 仅诊断,不修改
-- 与 0) 严格互补,两者行数之和 = 该表 status 非「在库」的总行数
-- ---------------------------------------------------------------------------
SELECT 'stock_buy' AS tbl, id, sku, barcode, status, stock_quantity, available_quantity
FROM stock_buy
WHERE (status IS NULL OR status <> '在库')
AND COALESCE(stock_quantity, 0) <= 0 AND COALESCE(available_quantity, 0) <= 0
UNION ALL
SELECT 'stock_semi', id, sku, barcode, status, stock_quantity, available_quantity
FROM stock_semi
WHERE (status IS NULL OR status <> '在库')
AND COALESCE(stock_quantity, 0) <= 0 AND COALESCE(available_quantity, 0) <= 0
UNION ALL
SELECT 'stock_product', id, sku, barcode, status, stock_quantity, available_quantity
FROM stock_product
WHERE (status IS NULL OR status <> '在库')
AND COALESCE(stock_quantity, 0) <= 0 AND COALESCE(available_quantity, 0) <= 0;
-- ---------------------------------------------------------------------------
-- 1) 洗刷 status三表口径一致stock_buy 无 quality_status故不涉及
-- ---------------------------------------------------------------------------
UPDATE stock_buy
SET status = '在库'
WHERE (status IS NULL OR status <> '在库')
AND (COALESCE(stock_quantity, 0) > 0 OR COALESCE(available_quantity, 0) > 0);
UPDATE stock_semi
SET status = '在库'
WHERE (status IS NULL OR status <> '在库')
AND (COALESCE(stock_quantity, 0) > 0 OR COALESCE(available_quantity, 0) > 0);
UPDATE stock_product
SET status = '在库'
WHERE (status IS NULL OR status <> '在库')
AND (COALESCE(stock_quantity, 0) > 0 OR COALESCE(available_quantity, 0) > 0);
-- ---------------------------------------------------------------------------
-- 2) 执行后核对:三行结果应全部为 0
-- (统计口径与 1) 的 WHERE 完全一致,故「有实物且状态非在库」的行应为 0
-- ---------------------------------------------------------------------------
SELECT 'stock_buy' AS tbl, count(*) AS remaining
FROM stock_buy
WHERE (status IS NULL OR status <> '在库')
AND (COALESCE(stock_quantity, 0) > 0 OR COALESCE(available_quantity, 0) > 0)
UNION ALL
SELECT 'stock_semi', count(*)
FROM stock_semi
WHERE (status IS NULL OR status <> '在库')
AND (COALESCE(stock_quantity, 0) > 0 OR COALESCE(available_quantity, 0) > 0)
UNION ALL
SELECT 'stock_product', count(*)
FROM stock_product
WHERE (status IS NULL OR status <> '在库')
AND (COALESCE(stock_quantity, 0) > 0 OR COALESCE(available_quantity, 0) > 0);
-- ---------------------------------------------------------------------------
-- 3) 可选:质量列补空 —— 默认**注释掉**,需要时人工确认后再放开
--
-- ★ 只补 NULL不覆盖任何已有取值。
-- '待检' / '未检' 是真实业务状态,绝不能被本脚本抹成 '合格'。
-- 实测当前三表均无 NULL 的质量列stock_buy.inspection_status 有 2042 行为
-- '未检',是有值状态,不在补空范围内),故本段在当前数据上是空操作。
-- ---------------------------------------------------------------------------
-- UPDATE stock_semi SET quality_status = '合格' WHERE quality_status IS NULL;
-- UPDATE stock_product SET quality_status = '合格' WHERE quality_status IS NULL;
-- UPDATE stock_buy SET inspection_status = '未检' WHERE inspection_status IS NULL;
-- ↑ 采购件补的是「未检」而非「合格」:默认未检验是保守且诚实的取值。
COMMIT;