feat(return): 逆向物流数据模型与迁移
新增原单退回与不良品在管的持久化结构。
- TransOutbound 增 returned_quantity(numeric(19,4),非 float):该值参与
「return_qty <= quantity - returned_quantity」判等,浮点误差会让反复部分
退回后出现「已退满却判定未退满」的错判
- 新增 TransReturn:退回流水,每次退回写一条而非覆盖式更新。刻意与
trans_borrow 划清界限——后者部分归还时会覆盖 return_time/operator,
导致归还历史永久丢失
- 新增 TransDefectiveGoods:不良品在管台账。坏件全程不入库存表,因为
status 是行级属性而质量是件级属性,把坏件加回原行只能整行打不良
(实测 stock_buy 单行最大 4789 件、中位 8 件,整行打不良会凭空损失良品)
- 状态机:待处理 → 处理中 → {已回库|已报废|已闭环}。终态由累计去向推导
而非「最后一次动作」——一批坏件可能既回库过又报废过,按最后动作定状态
会产生误导
- restocked_qty/scrapped_qty 两列:二期用 quantity-remaining_qty 反推回库量,
三期加入报废出口后该反推失效
- 审计白名单与模型预加载同步登记(监听器绑定 18 → 20 个模型)
迁移脚本均为纯追加式 DDL,含预检、回滚段与执行后核对。首个脚本用
COALESCE 包裹数量列——库存表允许数量为 NULL,而「NULL 大于 0」求值为
NULL 而非真,裸写会让脏行在预览与诊断两次查询里凭空消失。
This commit is contained in:
141
db_migrations/phase2_return_and_defective_goods.sql
Normal file
141
db_migrations/phase2_return_and_defective_goods.sql
Normal file
@ -0,0 +1,141 @@
|
||||
-- =============================================================================
|
||||
-- 二期迁移:通用原单退回 + 不良品在管台账
|
||||
--
|
||||
-- 背景
|
||||
-- 逆向物流二期。出库后的实物退回分两条路径:
|
||||
-- · 良品退回(错领/多领)—— 直接加回原库存行
|
||||
-- · 不良品退回 —— **不写库存表**,转入独立的坏件在管台账
|
||||
-- trans_defective_goods,修好后一键回库
|
||||
--
|
||||
-- 不良品不入库存表是本次的**核心架构决策**:库存表的 status 是行级属性,
|
||||
-- 而质量是件级属性。若把坏件加回原行,一行可能同时含良品与坏件,只能整行
|
||||
-- 打不良(实测 stock_buy 单行最大 4789 件,中位 8 件,整行打不良会凭空
|
||||
-- 损失大量良品)。故坏件全程独立于库存表,只在修好回库时才回到原行。
|
||||
--
|
||||
-- 与之配套:worklist 表 trans_defective_goods 与维修模块(trans_repair)
|
||||
-- **完全解耦** —— trans_repair 是 SN 单台粒度、无任何数量列,承载不了
|
||||
-- "一批坏件"(实测 50.6% 的出库是多件,中位 2、最大 186)。
|
||||
--
|
||||
-- 变更内容(全部为**追加式**,不改动/不删除任何既有列与行)
|
||||
-- 1) trans_outbound 新增 returned_quantity —— 每条出库明细的累计退回量
|
||||
-- 2) 新建 trans_return —— 退回流水(每一次退回一条)
|
||||
-- 3) 新建 trans_defective_goods —— 不良品在管台账(可部分回库)
|
||||
--
|
||||
-- ★ 类型选择说明:returned_quantity 用 numeric(19,4) 而非 float。
|
||||
-- 本系统所有数量列一律 numeric(19,4)(trans_outbound.quantity /
|
||||
-- trans_borrow.returned_quantity 等),且 returned_quantity 要参与
|
||||
-- `return_qty <= quantity - returned_quantity` 这种判等/比较运算。
|
||||
-- 用 float 会引入二进制浮点误差,反复部分退回后可能出现
|
||||
-- "已退满却仍判定为未退满"或反之的错判。
|
||||
--
|
||||
-- 安全性
|
||||
-- · 纯追加式 DDL:ADD COLUMN 带 DEFAULT 0,既有 1077 行出库记录自动补 0,
|
||||
-- 不改写任何业务数据;两张新表不影响现有查询。
|
||||
-- · 幂等:全部使用 IF NOT EXISTS,可重复执行。
|
||||
-- · 回滚:见文末「回滚段」(同样为纯 DDL,不触碰业务数据)。
|
||||
--
|
||||
-- 执行
|
||||
-- docker exec -i inventory_db psql -U test -d inventory_system < 本文件
|
||||
-- =============================================================================
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 1) 出库明细:累计退回量
|
||||
-- ---------------------------------------------------------------------------
|
||||
ALTER TABLE trans_outbound
|
||||
ADD COLUMN IF NOT EXISTS returned_quantity numeric(19,4) NOT NULL DEFAULT 0;
|
||||
|
||||
COMMENT ON COLUMN trans_outbound.returned_quantity IS '累计已退回数量(良品+不良品),不得超过 quantity';
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 2) 退回流水
|
||||
-- 每一次退回写一条,**不做覆盖式更新** —— 避免重演 trans_borrow 归还时
|
||||
-- 把 return_time/operator 覆盖掉、导致部分归还历史丢失的老问题。
|
||||
-- ---------------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS trans_return (
|
||||
id serial PRIMARY KEY,
|
||||
outbound_id integer NOT NULL, -- 原出库明细 trans_outbound.id
|
||||
stock_id integer, -- 原库存行 id(快照)
|
||||
source_table varchar(50), -- 原库存表名(快照)
|
||||
sku varchar(100), -- 冗余,便于列表展示免联表
|
||||
return_qty numeric(19,4) NOT NULL DEFAULT 0,
|
||||
return_type varchar(20) NOT NULL, -- '良品' | '不良品'
|
||||
reason text,
|
||||
operator varchar(100),
|
||||
return_time timestamp without time zone DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
COMMENT ON TABLE trans_return IS '原单退回流水(每次退回一条,不覆盖)';
|
||||
COMMENT ON COLUMN trans_return.return_type IS '良品 / 不良品';
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_trans_return_outbound
|
||||
ON trans_return (outbound_id);
|
||||
CREATE INDEX IF NOT EXISTS ix_trans_return_stock
|
||||
ON trans_return (source_table, stock_id);
|
||||
CREATE INDEX IF NOT EXISTS ix_trans_return_time
|
||||
ON trans_return (return_time);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 3) 不良品在管台账
|
||||
-- 一行 = 一批同源坏件。支持部分回库:remaining_qty 随回库递减。
|
||||
-- 状态机:待处理 → (部分回库) → 已回库
|
||||
-- ↘ 已报废
|
||||
-- ---------------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS trans_defective_goods (
|
||||
id serial PRIMARY KEY,
|
||||
return_id integer, -- 来源 trans_return.id(追溯)
|
||||
outbound_id integer, -- 来源 trans_outbound.id(追溯)
|
||||
source_table varchar(50) NOT NULL, -- 回库目标库存表
|
||||
stock_id integer NOT NULL, -- 回库目标库存行
|
||||
base_id integer, -- 物料主数据(联表展示用)
|
||||
sku varchar(100),
|
||||
material_name varchar(200),
|
||||
spec_model varchar(255),
|
||||
quantity numeric(19,4) NOT NULL DEFAULT 0, -- 进入在管时的原始数量
|
||||
remaining_qty numeric(19,4) NOT NULL DEFAULT 0, -- 当前仍在管数量
|
||||
status varchar(20) NOT NULL DEFAULT '待处理',
|
||||
company_name varchar(255), -- 行级隔离用(本表承载实物,按库存表口径存公司)
|
||||
reason text,
|
||||
operator varchar(100),
|
||||
remark text,
|
||||
created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
COMMENT ON TABLE trans_defective_goods IS '不良品在管台账;与库存表解耦,回库时才回到 source_table#stock_id';
|
||||
COMMENT ON COLUMN trans_defective_goods.remaining_qty IS '仍在管数量;每次回库递减,归零即整批回库完成';
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_tdg_status
|
||||
ON trans_defective_goods (status);
|
||||
CREATE INDEX IF NOT EXISTS ix_tdg_source
|
||||
ON trans_defective_goods (source_table, stock_id);
|
||||
CREATE INDEX IF NOT EXISTS ix_tdg_company
|
||||
ON trans_defective_goods (company_name);
|
||||
CREATE INDEX IF NOT EXISTS ix_tdg_outbound
|
||||
ON trans_defective_goods (outbound_id);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 4) 执行后核对
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo '--- 1) returned_quantity 列已就位,既有行补 0 ---'
|
||||
SELECT count(*) AS outbound_rows,
|
||||
count(*) FILTER (WHERE returned_quantity = 0) AS zero_filled
|
||||
FROM trans_outbound;
|
||||
|
||||
\echo '--- 2) 新表已建立 ---'
|
||||
SELECT tablename FROM pg_tables
|
||||
WHERE tablename IN ('trans_return', 'trans_defective_goods') ORDER BY 1;
|
||||
|
||||
COMMIT;
|
||||
|
||||
|
||||
-- =============================================================================
|
||||
-- 回滚段(仅在需要撤销本次迁移时执行;同样纯 DDL,不动业务数据)
|
||||
-- 注意:一旦已有退回/在管数据落库,回滚会一并丢弃这些数据。
|
||||
-- =============================================================================
|
||||
-- BEGIN;
|
||||
-- DROP TABLE IF EXISTS trans_defective_goods;
|
||||
-- DROP TABLE IF EXISTS trans_return;
|
||||
-- ALTER TABLE trans_outbound DROP COLUMN IF EXISTS returned_quantity;
|
||||
-- COMMIT;
|
||||
84
db_migrations/phase3_defective_qty_tracking.sql
Normal file
84
db_migrations/phase3_defective_qty_tracking.sql
Normal file
@ -0,0 +1,84 @@
|
||||
-- =============================================================================
|
||||
-- 三期迁移:坏件处置量跟踪(配合三期的「坏件报废」闭环)
|
||||
--
|
||||
-- 背景
|
||||
-- 二期只支持坏件的「回库」一种出口,故用 `quantity - remaining_qty` 就能
|
||||
-- 反推已回库量。三期补上「报废」出口后,这个反推就不再成立 ——
|
||||
-- 它会把报废掉的数量误算成已回库量。
|
||||
--
|
||||
-- 因此拆出两个独立累计列:restocked_qty(累计回库)与 scrapped_qty(累计报废)。
|
||||
--
|
||||
-- ★ 不变式(全表恒成立,测试中逐行校验):
|
||||
-- restocked_qty + scrapped_qty + remaining_qty = quantity
|
||||
-- 暂不加 DB 层 CHECK 约束:本系统库存数量普遍以 Python float 累加后再
|
||||
-- 落 numeric(19,4),极端小数下可能出现 4 位以外的舍入抖动,硬约束会把
|
||||
-- 一次合法操作变成 500。改为在接口层维护并由测试守住不变式。
|
||||
--
|
||||
-- 状态值归一
|
||||
-- 二期把「部分处置」命名为 '部分回库'。三期报废也会产生部分状态,旧名不再
|
||||
-- 准确,统一改为 '处理中'。新状态机(取值见 app/models/transaction.py):
|
||||
-- 待处理 → 处理中 → ┬ 已回库(全部回库)
|
||||
-- ├ 已报废(全部报废)
|
||||
-- └ 已闭环(回库与报废混合)
|
||||
--
|
||||
-- 安全性:纯追加式 DDL;新列带 DEFAULT 0,既有行自动补 0。
|
||||
-- 幂等:全部 IF NOT EXISTS / 带条件的 UPDATE,可重复执行。
|
||||
--
|
||||
-- 执行
|
||||
-- docker exec -i inventory_db psql -U test -d inventory_system < 本文件
|
||||
-- =============================================================================
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 1) 处置量跟踪列
|
||||
-- ---------------------------------------------------------------------------
|
||||
ALTER TABLE trans_defective_goods
|
||||
ADD COLUMN IF NOT EXISTS restocked_qty numeric(19,4) NOT NULL DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS scrapped_qty numeric(19,4) NOT NULL DEFAULT 0;
|
||||
|
||||
COMMENT ON COLUMN trans_defective_goods.restocked_qty IS '累计已回库数量';
|
||||
COMMENT ON COLUMN trans_defective_goods.scrapped_qty IS '累计已报废数量';
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 2) 存量回填
|
||||
-- 迁移前只有「回库」一种出口,故「已减少的在管量」全部归入 restocked_qty。
|
||||
-- 本表在写此迁移时为空,此段为幂等防御,防止在其它环境(已有数据)执行时
|
||||
-- 出现 restocked/scrapped 全 0 而 remaining 已减少的不一致状态。
|
||||
-- ---------------------------------------------------------------------------
|
||||
UPDATE trans_defective_goods
|
||||
SET restocked_qty = GREATEST(COALESCE(quantity, 0) - COALESCE(remaining_qty, 0), 0)
|
||||
WHERE restocked_qty = 0
|
||||
AND scrapped_qty = 0
|
||||
AND COALESCE(quantity, 0) > COALESCE(remaining_qty, 0);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 3) 状态值归一:'部分回库' → '处理中'
|
||||
-- ---------------------------------------------------------------------------
|
||||
UPDATE trans_defective_goods SET status = '处理中' WHERE status = '部分回库';
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 4) 执行后核对:不一致行数必须为 0
|
||||
-- ---------------------------------------------------------------------------
|
||||
\echo '--- 处置量不变式核对(inconsistent 应为 0)---'
|
||||
SELECT count(*) AS total,
|
||||
count(*) FILTER (
|
||||
WHERE COALESCE(restocked_qty,0) + COALESCE(scrapped_qty,0)
|
||||
+ COALESCE(remaining_qty,0) <> COALESCE(quantity,0)
|
||||
) AS inconsistent
|
||||
FROM trans_defective_goods;
|
||||
|
||||
\echo '--- 状态取值分布 ---'
|
||||
SELECT COALESCE(status, '<NULL>') AS status, count(*) FROM trans_defective_goods GROUP BY 1;
|
||||
|
||||
COMMIT;
|
||||
|
||||
|
||||
-- =============================================================================
|
||||
-- 回滚段(仅撤销三期变更;不动二期表结构)
|
||||
-- =============================================================================
|
||||
-- BEGIN;
|
||||
-- ALTER TABLE trans_defective_goods DROP COLUMN IF EXISTS restocked_qty;
|
||||
-- ALTER TABLE trans_defective_goods DROP COLUMN IF EXISTS scrapped_qty;
|
||||
-- UPDATE trans_defective_goods SET status = '部分回库' WHERE status = '处理中';
|
||||
-- COMMIT;
|
||||
Reference in New Issue
Block a user