报废要回答「这笔损失出在哪个环节」,并让主管审批不再依赖逐个指定人。
- 新增「报废原因分类」字段(scrap_approval + trans_scrap 各一列),
只有两个互斥口径:生产报废(走 Track 的)/ 库存报废(MOM 自身流程的)。
不传即库存报废 —— 这条二分法在写入那一刻就成立,不依赖任何推导。
⚠️ 不能从 source_table 推导:Track 的生产报废与手工的不良品退回共用
同一张 trans_defective_goods 表,推导会把生产损失算成库存损失。
- scrap_approval 加 company_name / source_ref:前者是公司隔离快照,
后者是外部单据的幂等锚点(Redis 未部署,prevent_double_submit 全程
fail-open,唯一索引是唯一防线)。
- 审批从「只认 type=user」放宽到「user 或 role」——主管角色都能审,
谁审就记谁。★ 空名单依然拒绝所有人(Fail-Closed),这是历史
「名单为空则人人可审」漏洞的修复点,不得改回 fail-open。
- 角色级放行必须配公司隔离:6 个主管里 IRIS 5 个、LICA 1 个,
不隔离就是跨公司审批通道。
- trans_return 加 source_ref(幂等锚点)。
迁移:db_migrations/phase12(建列)+ phase13(存量空分类回填为库存报废)。
156 lines
8.2 KiB
PL/PgSQL
156 lines
8.2 KiB
PL/PgSQL
-- =============================================================================
|
||
-- 生产导致报废:Track → MOM 复用逆向物流 + 报废审批
|
||
--
|
||
-- 问题
|
||
-- Track 要把「生产领用的物料已经领到产线、之后在生产中报废」提交进 MOM,
|
||
-- 并复用 MOM 现有的报废流程(申请→审批→执行),最终能统计报废金额。
|
||
-- 但料**已经出库**,那条库存行的 available_quantity / stock_quantity 在出库
|
||
-- 时就已扣掉,走不了标准库存行报废(cap=可用库存)。MOM 自己的答案是走
|
||
-- 「从出库单退回(不良品)」→ trans_defective_goods 在管台账(库存表分毫不动)。
|
||
--
|
||
-- 本次改动(全部为可空列,纯增量)
|
||
-- 1) scrap_approval.company_name —— 公司隔离快照。角色级审批的前提:
|
||
-- 6 个主管里 IRIS 5 个、LICA 1 个,不隔离公司就是跨公司审批通道。
|
||
-- 2) scrap_approval.reason_category —— 报废原因分类,标记「生产导致」。
|
||
-- 3) scrap_approval.source_ref —— 外部系统唯一引用(幂等 + 追溯)。
|
||
-- 4) trans_scrap.reason_category —— 台账侧同名字段,供报废金额分组统计。
|
||
-- 5) trans_return.source_ref —— 幂等锚点。
|
||
--
|
||
-- ---------------------------------------------------------------------------
|
||
-- ★ 为什么 source_ref 是 '<company>:<track_ref>' 而不是裸的单号
|
||
-- IRIS 与 LICA 各自独立跑一套 Track,工单号可能重号。唯一索引必须带公司维度,
|
||
-- 否则两家会互相挡住对方的首次受理。
|
||
--
|
||
-- ★ 为什么幂等要靠唯一索引,而不是靠 prevent_double_submit
|
||
-- 该装饰器依赖 Redis,而 docker-compose 里**没有 redis 服务** ——
|
||
-- app/extensions.py 的 redis_client 恒为 None,装饰器全程 fail-open、
|
||
-- 实际不生效(已实测)。所以 DB 唯一索引是唯一的并发防线。
|
||
--
|
||
-- ★ 为什么不回填 reason_category
|
||
-- 本库 scrap_approval / trans_scrap 均为 0 行,无存量;生产库若有历史行,
|
||
-- 留 NULL 表示「产生于本列上线之前」,不猜。company_name 可安全回填
|
||
-- (申请人所属部门即公司,是确定性映射)。
|
||
--
|
||
-- ★ 时区提醒
|
||
-- 本文件不新增时间列。注意 scrap_approval 的时间列在**本库**仍是 timestamptz
|
||
-- (unify_approval_timezone.sql 尚未在本库执行,实测库 TimeZone=Etc/UTC),
|
||
-- 与该脚本的预期不一致。日后再加时间列时请先确认该脚本已执行,
|
||
-- 不要照抄 add_scrap_approval.sql 的 timestamptz 写法。
|
||
--
|
||
-- 幂等:带 IF NOT EXISTS,可重复执行。不含 psql 元命令,DataGrip 可直接执行。
|
||
-- 执行: docker exec -i inventory_db psql -U test -d inventory_system < 本文件
|
||
-- =============================================================================
|
||
|
||
BEGIN;
|
||
|
||
-- ---------------------------------------------------------------------------
|
||
-- 1) 报废申请单:公司快照 + 原因分类 + 外部引用
|
||
-- ---------------------------------------------------------------------------
|
||
ALTER TABLE scrap_approval
|
||
ADD COLUMN IF NOT EXISTS company_name varchar(255);
|
||
|
||
COMMENT ON COLUMN scrap_approval.company_name IS
|
||
'申请所属公司(= sys_user.department)。角色级审批据此做公司隔离,防止跨公司审批';
|
||
|
||
CREATE INDEX IF NOT EXISTS ix_scrap_approval_company
|
||
ON scrap_approval (company_name);
|
||
|
||
ALTER TABLE scrap_approval
|
||
ADD COLUMN IF NOT EXISTS reason_category varchar(50);
|
||
|
||
COMMENT ON COLUMN scrap_approval.reason_category IS
|
||
'报废原因分类码:PRODUCTION(生产损耗=好料领用后变坏) / STOCK(库存/采购=在库或买来就有问题) / OTHER(其他)。'
|
||
'★必须显式记录,不能从 source_table 推导:Track 的生产报废与 MOM 手工报的不良品退回共用 trans_defective_goods,'
|
||
'推导会把生产损失静默算成库存损失。中文名见 scrap_approval.SCRAP_CATEGORY_LABELS';
|
||
|
||
ALTER TABLE scrap_approval
|
||
ADD COLUMN IF NOT EXISTS source_ref varchar(100);
|
||
|
||
COMMENT ON COLUMN scrap_approval.source_ref IS
|
||
'外部系统唯一引用,格式 <company>:<外部单号>。用于幂等与追溯';
|
||
|
||
-- 唯一索引:同一外部单据不得重复受理(部分索引,NULL 不参与唯一性)
|
||
CREATE UNIQUE INDEX IF NOT EXISTS uk_scrap_approval_source_ref
|
||
ON scrap_approval (source_ref) WHERE source_ref IS NOT NULL;
|
||
|
||
-- ---------------------------------------------------------------------------
|
||
-- 2) 报废流水台账:原因分类(统计按它分组)
|
||
-- ---------------------------------------------------------------------------
|
||
ALTER TABLE trans_scrap
|
||
ADD COLUMN IF NOT EXISTS reason_category varchar(50);
|
||
|
||
COMMENT ON COLUMN trans_scrap.reason_category IS
|
||
'报废原因分类码,执行报废时从 scrap_approval.reason_category 带出。统计「生产报废金额」按此分组';
|
||
|
||
CREATE INDEX IF NOT EXISTS ix_trans_scrap_category
|
||
ON trans_scrap (reason_category);
|
||
|
||
-- ---------------------------------------------------------------------------
|
||
-- 3) 退回流水:幂等锚点
|
||
-- ---------------------------------------------------------------------------
|
||
ALTER TABLE trans_return
|
||
ADD COLUMN IF NOT EXISTS source_ref varchar(100);
|
||
|
||
COMMENT ON COLUMN trans_return.source_ref IS
|
||
'外部系统唯一引用,格式 <company>:<外部单号>。外部接口据此判重(Redis 未部署,唯一索引是唯一防线)';
|
||
|
||
CREATE UNIQUE INDEX IF NOT EXISTS uk_trans_return_source_ref
|
||
ON trans_return (source_ref) WHERE source_ref IS NOT NULL;
|
||
|
||
-- ---------------------------------------------------------------------------
|
||
-- 4) 存量回填:申请人所属部门即公司(确定性映射)
|
||
-- 查不到(申请人已删)保持 NULL —— 应用层对 company_name 为空的旧单
|
||
-- **跳过**公司校验而非拒绝,避免把历史在途单卡死。
|
||
-- ---------------------------------------------------------------------------
|
||
UPDATE scrap_approval sa
|
||
SET company_name = u.department
|
||
FROM sys_user u
|
||
WHERE u.id = sa.applicant_id
|
||
AND sa.company_name IS NULL
|
||
AND COALESCE(u.department, '') <> '';
|
||
|
||
COMMIT;
|
||
|
||
|
||
-- =============================================================================
|
||
-- 执行后核对
|
||
-- =============================================================================
|
||
SELECT '=== 1) scrap_approval 三个新列已就位 ===' AS "核对项";
|
||
SELECT column_name, data_type FROM information_schema.columns
|
||
WHERE table_name = 'scrap_approval'
|
||
AND column_name IN ('company_name', 'reason_category', 'source_ref')
|
||
ORDER BY column_name;
|
||
|
||
SELECT '=== 2) trans_scrap / trans_return 新列已就位 ===' AS "核对项";
|
||
SELECT table_name, column_name, data_type FROM information_schema.columns
|
||
WHERE (table_name = 'trans_scrap' AND column_name = 'reason_category')
|
||
OR (table_name = 'trans_return' AND column_name = 'source_ref')
|
||
ORDER BY table_name;
|
||
|
||
SELECT '=== 3) 索引已就位(三个普通 + 两个唯一)===' AS "核对项";
|
||
SELECT tablename, indexname FROM pg_indexes
|
||
WHERE indexname IN ('ix_scrap_approval_company', 'ix_trans_scrap_category',
|
||
'uk_scrap_approval_source_ref', 'uk_trans_return_source_ref')
|
||
ORDER BY indexname;
|
||
|
||
SELECT '=== 4) 存量:本库应均为 0 行 ===' AS "核对项";
|
||
SELECT (SELECT count(*) FROM scrap_approval) AS 报废申请单数,
|
||
(SELECT count(*) FROM trans_scrap) AS 报废流水数,
|
||
(SELECT count(*) FROM scrap_approval WHERE company_name IS NULL) AS 公司为空的申请单;
|
||
|
||
|
||
-- =============================================================================
|
||
-- 回滚段
|
||
-- =============================================================================
|
||
-- BEGIN;
|
||
-- DROP INDEX IF EXISTS uk_trans_return_source_ref;
|
||
-- ALTER TABLE trans_return DROP COLUMN IF EXISTS source_ref;
|
||
-- DROP INDEX IF EXISTS ix_trans_scrap_category;
|
||
-- ALTER TABLE trans_scrap DROP COLUMN IF EXISTS reason_category;
|
||
-- DROP INDEX IF EXISTS uk_scrap_approval_source_ref;
|
||
-- DROP INDEX IF EXISTS ix_scrap_approval_company;
|
||
-- ALTER TABLE scrap_approval DROP COLUMN IF EXISTS source_ref;
|
||
-- ALTER TABLE scrap_approval DROP COLUMN IF EXISTS reason_category;
|
||
-- ALTER TABLE scrap_approval DROP COLUMN IF EXISTS company_name;
|
||
-- COMMIT;
|