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;
|
||||
@ -310,7 +310,14 @@ def create_app():
|
||||
# 系统与业务模型 (SysRolePermission 等在 models.system 中)
|
||||
from app.models.system import SysUser, SysLog, SysMenu, SysElement, SysRolePermission, SysWarehouseLocation
|
||||
# 确保借还模型被加载
|
||||
from app.models.transaction import TransBorrow, TransRepair, TransScrap
|
||||
# ★ TransReturn / TransDefectiveGoods 为二期逆向物流新增,必须在此
|
||||
# 预加载:审计监听器按表名从 db.metadata 取模型,未预加载的表在
|
||||
# create_app() 完成时尚未映射,会漏绑审计(虽然后续惰性补绑能兜底,
|
||||
# 但预加载更可靠)。
|
||||
from app.models.transaction import (
|
||||
TransBorrow, TransRepair, TransScrap,
|
||||
TransReturn, TransDefectiveGoods,
|
||||
)
|
||||
# ★ 审批单模型(原仅在函数体内延迟导入,会导致审计监听器漏绑)
|
||||
from app.models.outbound import OutboundApproval
|
||||
from app.models.borrow import BorrowApproval
|
||||
|
||||
@ -41,6 +41,8 @@ WHITELIST_TABLES = {
|
||||
'trans_borrow',
|
||||
'trans_scrap',
|
||||
'trans_repair',
|
||||
'trans_return', # 原单退回流水(二期)
|
||||
'trans_defective_goods', # 不良品在管台账(二期)
|
||||
# --- 库存(三表 + 库存调整)---
|
||||
'stock_buy',
|
||||
'stock_semi',
|
||||
@ -66,6 +68,8 @@ TABLE_LABELS = {
|
||||
'trans_borrow': '借还流水',
|
||||
'trans_scrap': '报废流水',
|
||||
'trans_repair': '维修单',
|
||||
'trans_return': '退回流水',
|
||||
'trans_defective_goods': '不良品在管',
|
||||
'stock_buy': '采购库存',
|
||||
'stock_semi': '半成品库存',
|
||||
'stock_product': '成品库存',
|
||||
@ -185,6 +189,8 @@ def _get_module_name(mapper):
|
||||
return '报废管理'
|
||||
if tablename == 'trans_repair':
|
||||
return '维修管理'
|
||||
if tablename in ('trans_return', 'trans_defective_goods'):
|
||||
return '退回管理'
|
||||
if tablename in ('stock_buy', 'stock_semi', 'stock_product'):
|
||||
return '库存管理'
|
||||
if tablename == 'stock_adjustment':
|
||||
|
||||
@ -139,16 +139,28 @@ class TransOutbound(db.Model):
|
||||
# [新增] 出库时的库位快照(从源库存记录带出,便于历史追溯)
|
||||
warehouse_location = db.Column(db.String(100))
|
||||
|
||||
# [新增] 累计已退回数量(良品 + 不良品口径合并),用于原单退回的额度校验。
|
||||
# ★ 用 numeric(19,4) 而非 float:本系统所有数量列一律 numeric(19,4),
|
||||
# 且该值要参与 `return_qty <= quantity - returned_quantity` 的判等比较,
|
||||
# 浮点误差会让反复部分退回后出现「已退满却判定未退满」的错判。
|
||||
# DDL 见 db_migrations/phase2_return_and_defective_goods.sql
|
||||
returned_quantity = db.Column(db.Numeric(19, 4), nullable=False, default=0)
|
||||
|
||||
remark = db.Column(db.Text)
|
||||
|
||||
def to_dict(self):
|
||||
qty = float(self.quantity) if self.quantity else 0
|
||||
returned = float(self.returned_quantity) if self.returned_quantity is not None else 0
|
||||
return {
|
||||
'id': self.id,
|
||||
'outbound_no': self.outbound_no,
|
||||
'sku': self.sku,
|
||||
'source_table': self.source_table,
|
||||
'outbound_type': self.outbound_type,
|
||||
'quantity': float(self.quantity) if self.quantity else 0,
|
||||
'quantity': qty,
|
||||
# [新增] 退回额度三件套,供前端判断该明细还能退多少
|
||||
'returned_quantity': returned,
|
||||
'returnable_quantity': qty - returned,
|
||||
'unit_price': float(self.unit_price) if self.unit_price else 0,
|
||||
'consumer_name': self.consumer_name,
|
||||
'signature_path': self.signature_path,
|
||||
|
||||
@ -217,3 +217,198 @@ class TransScrap(db.Model):
|
||||
'cost_at_scrap': float(self.cost_at_scrap) if self.cost_at_scrap is not None else None,
|
||||
'total_loss': float(self.total_loss) if self.total_loss is not None else None,
|
||||
}
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# 原单退回(逆向物流)
|
||||
# =============================================================================
|
||||
# 设计要点
|
||||
# --------
|
||||
# 1. **流水不覆盖**:每一次退回写一条 trans_return,而不是在原记录上累加覆盖。
|
||||
# 这是刻意与 trans_borrow 划清界限 —— 后者在部分归还时会把
|
||||
# return_time / return_operator / return_signature 逐次覆盖,导致
|
||||
# 「谁在什么时候还了多少」永久丢失。退回流水不能再犯同样的错。
|
||||
#
|
||||
# 2. **不良品不入库存表**:库存表的 status 是**行级**属性,而质量是**件级**
|
||||
# 属性。把坏件加回原行,会让一行同时含良品与坏件 —— 只能整行打不良,
|
||||
# 而实测 stock_buy 单行最大 4789 件(中位 8 件),整行打不良等于凭空
|
||||
# 损失大量良品。故坏件全程存放在独立的 trans_defective_goods 台账里,
|
||||
# 只有修好回库那一刻才回到原库存行。
|
||||
#
|
||||
# 3. **与维修模块解耦**:trans_repair 是 SN 单台粒度、且没有任何数量列,
|
||||
# 承载不了「一批坏件」(实测 50.6% 的出库是多件,中位 2、最大 186)。
|
||||
# 故坏件台账独立建表,不复用 trans_repair。
|
||||
|
||||
RETURN_TYPE_GOOD = '良品'
|
||||
RETURN_TYPE_DEFECTIVE = '不良品'
|
||||
|
||||
VALID_RETURN_TYPES = (RETURN_TYPE_GOOD, RETURN_TYPE_DEFECTIVE)
|
||||
|
||||
|
||||
class TransReturn(db.Model):
|
||||
"""
|
||||
原单退回流水。
|
||||
|
||||
一行 = 一次退回动作(不做覆盖式更新)。累计退回量另存于
|
||||
trans_outbound.returned_quantity,本表负责回答「每一次是谁、何时、
|
||||
退了多少、良品还是不良品」。
|
||||
"""
|
||||
__tablename__ = 'trans_return'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
|
||||
outbound_id = db.Column(db.Integer, nullable=False, index=True) # 原出库明细 trans_outbound.id
|
||||
stock_id = db.Column(db.Integer) # 原库存行 id(快照)
|
||||
source_table = db.Column(db.String(50)) # 原库存表名(快照)
|
||||
sku = db.Column(db.String(100)) # 冗余,避免列表页联表
|
||||
|
||||
return_qty = db.Column(db.Numeric(19, 4), nullable=False, default=0)
|
||||
return_type = db.Column(db.String(20), nullable=False) # '良品' | '不良品'
|
||||
reason = db.Column(db.Text)
|
||||
operator = db.Column(db.String(100))
|
||||
return_time = db.Column(db.DateTime, default=beijing_time)
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'id': self.id,
|
||||
'outbound_id': self.outbound_id,
|
||||
'stock_id': self.stock_id,
|
||||
'source_table': self.source_table,
|
||||
'sku': self.sku,
|
||||
'return_qty': float(self.return_qty) if self.return_qty is not None else 0,
|
||||
'return_type': self.return_type,
|
||||
'reason': self.reason,
|
||||
'operator': self.operator,
|
||||
'return_time': self.return_time.strftime('%Y-%m-%d %H:%M:%S') if self.return_time else None,
|
||||
}
|
||||
|
||||
|
||||
# 状态机(三期升级):
|
||||
# 待处理 ──┬─→ 处理中 ──┬─→ 已回库(全部回库,无报废)
|
||||
# │ ├─→ 已报废(全部报废,无回库)
|
||||
# │ └─→ 已闭环(回库与报废混合,在管量归零)
|
||||
# └─(一次处置即为终态时,直接跳到对应的终态)
|
||||
#
|
||||
# ★ 三种终态而非单一「已闭环」,是为了让工作台一眼看出这批坏件的**去向**:
|
||||
# 修好回到库存了、还是被销毁了。混合处置无法用单一去向描述,才归到「已闭环」。
|
||||
DEFECTIVE_STATUS_PENDING = '待处理' # 刚退回,尚未做任何处置
|
||||
DEFECTIVE_STATUS_IN_PROGRESS = '处理中' # 已部分回库/部分报废,仍有在管量
|
||||
DEFECTIVE_STATUS_RESTOCKED = '已回库' # 全部回库,无报废
|
||||
DEFECTIVE_STATUS_SCRAPPED = '已报废' # 全部报废,无回库
|
||||
DEFECTIVE_STATUS_CLOSED = '已闭环' # 回库与报废混合,在管量归零
|
||||
|
||||
VALID_DEFECTIVE_STATUSES = (
|
||||
DEFECTIVE_STATUS_PENDING,
|
||||
DEFECTIVE_STATUS_IN_PROGRESS,
|
||||
DEFECTIVE_STATUS_RESTOCKED,
|
||||
DEFECTIVE_STATUS_SCRAPPED,
|
||||
DEFECTIVE_STATUS_CLOSED,
|
||||
)
|
||||
|
||||
# ★ 可继续处置的状态白名单(Fail-Closed:未列出的一律拒绝回库/报废)。
|
||||
# 三种终态都不可再动:已回库→再回库就是凭空多一份库存;已报废→实物已销毁;
|
||||
# 已闭环→在管量已归零。
|
||||
OPEN_DEFECTIVE_STATUSES = (
|
||||
DEFECTIVE_STATUS_PENDING,
|
||||
DEFECTIVE_STATUS_IN_PROGRESS,
|
||||
)
|
||||
|
||||
# 语义别名:回库与报废的准入白名单是同一组「未结案」状态
|
||||
RESTOCKABLE_DEFECTIVE_STATUSES = OPEN_DEFECTIVE_STATUSES
|
||||
SCRAPPABLE_DEFECTIVE_STATUSES = OPEN_DEFECTIVE_STATUSES
|
||||
|
||||
|
||||
def defective_close_status(restocked_qty, scrapped_qty):
|
||||
"""
|
||||
在管量归零时,由累计去向推导终态。
|
||||
|
||||
★ 为什么需要它:一次坏件批次可能既回库了一部分、又报废了剩余部分。
|
||||
此时 remaining_qty 归零,但既不是「已回库」也不是「已报废」——
|
||||
按最后一次动作定状态会产生误导(最后报废 ≠ 整批报废)。
|
||||
故由累计量推导,语义稳定且与动作顺序无关。
|
||||
"""
|
||||
restocked = float(restocked_qty or 0)
|
||||
scrapped = float(scrapped_qty or 0)
|
||||
if restocked > 0 and scrapped > 0:
|
||||
return DEFECTIVE_STATUS_CLOSED
|
||||
if scrapped > 0:
|
||||
return DEFECTIVE_STATUS_SCRAPPED
|
||||
return DEFECTIVE_STATUS_RESTOCKED
|
||||
|
||||
|
||||
class TransDefectiveGoods(db.Model):
|
||||
"""
|
||||
不良品在管台账。
|
||||
|
||||
一行 = 一批同源坏件。支持**部分回库**:remaining_qty 随每次回库递减,
|
||||
归零才算整批回库完成。
|
||||
|
||||
本表与库存表解耦:坏件在管期间不占用任何库存行的数量,也不改其 status。
|
||||
回库时才按 source_table + stock_id 回到原库存行。
|
||||
"""
|
||||
__tablename__ = 'trans_defective_goods'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
|
||||
# --- 来源追溯 ---
|
||||
return_id = db.Column(db.Integer, index=True) # trans_return.id
|
||||
outbound_id = db.Column(db.Integer, index=True) # trans_outbound.id
|
||||
|
||||
# --- 回库目标(原库存行)---
|
||||
source_table = db.Column(db.String(50), nullable=False)
|
||||
stock_id = db.Column(db.Integer, nullable=False)
|
||||
|
||||
# --- 物料快照(回库目标行可能被删,此处保留可读信息)---
|
||||
base_id = db.Column(db.Integer)
|
||||
sku = db.Column(db.String(100))
|
||||
material_name = db.Column(db.String(200))
|
||||
spec_model = db.Column(db.String(255))
|
||||
|
||||
# --- 数量 ---
|
||||
# ★ 不变式:restocked_qty + scrapped_qty + remaining_qty = quantity
|
||||
# 三个去向列相互独立,不可互推 —— 二期曾用 quantity - remaining_qty
|
||||
# 反推回库量,三期加入报废出口后该反推即失效。
|
||||
quantity = db.Column(db.Numeric(19, 4), nullable=False, default=0) # 进入在管时的原始数量
|
||||
remaining_qty = db.Column(db.Numeric(19, 4), nullable=False, default=0) # 仍在管数量
|
||||
restocked_qty = db.Column(db.Numeric(19, 4), nullable=False, default=0) # [三期] 累计已回库
|
||||
scrapped_qty = db.Column(db.Numeric(19, 4), nullable=False, default=0) # [三期] 累计已报废
|
||||
|
||||
# --- 状态与归属 ---
|
||||
status = db.Column(db.String(20), nullable=False, default=DEFECTIVE_STATUS_PENDING)
|
||||
company_name = db.Column(db.String(255), index=True) # 行级隔离:本表承载实物,按库存表口径存公司
|
||||
reason = db.Column(db.Text)
|
||||
operator = db.Column(db.String(100))
|
||||
remark = db.Column(db.Text)
|
||||
|
||||
created_at = db.Column(db.DateTime, default=beijing_time)
|
||||
updated_at = db.Column(db.DateTime, default=beijing_time, onupdate=beijing_time)
|
||||
|
||||
def to_dict(self):
|
||||
qty = float(self.quantity) if self.quantity is not None else 0
|
||||
remain = float(self.remaining_qty) if self.remaining_qty is not None else 0
|
||||
restocked = float(self.restocked_qty) if self.restocked_qty is not None else 0
|
||||
scrapped = float(self.scrapped_qty) if self.scrapped_qty is not None else 0
|
||||
return {
|
||||
'id': self.id,
|
||||
'return_id': self.return_id,
|
||||
'outbound_id': self.outbound_id,
|
||||
'source_table': self.source_table,
|
||||
'stock_id': self.stock_id,
|
||||
'base_id': self.base_id,
|
||||
'sku': self.sku,
|
||||
'material_name': self.material_name,
|
||||
'spec_model': self.spec_model,
|
||||
'quantity': qty,
|
||||
'remaining_qty': remain,
|
||||
# ★ 三个去向列各自独立取值。改造前 restocked_qty 由
|
||||
# quantity - remaining_qty 反推,三期加入报废出口后会算错。
|
||||
'restocked_qty': restocked,
|
||||
'scrapped_qty': scrapped,
|
||||
'status': self.status,
|
||||
'company_name': self.company_name,
|
||||
'reason': self.reason,
|
||||
'operator': self.operator,
|
||||
'remark': self.remark,
|
||||
'created_at': self.created_at.strftime('%Y-%m-%d %H:%M:%S') if self.created_at else None,
|
||||
'updated_at': self.updated_at.strftime('%Y-%m-%d %H:%M:%S') if self.updated_at else None,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user