-- ============================================================================= -- 生产导致报废: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 是 ':' 而不是裸的单号 -- 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 '外部系统唯一引用,格式 :<外部单号>。用于幂等与追溯'; -- 唯一索引:同一外部单据不得重复受理(部分索引,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 '外部系统唯一引用,格式 :<外部单号>。外部接口据此判重(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;