diff --git a/db_migrations/phase8_trans_outbound_applicant.sql b/db_migrations/phase8_trans_outbound_applicant.sql new file mode 100644 index 0000000..fdb1833 --- /dev/null +++ b/db_migrations/phase8_trans_outbound_applicant.sql @@ -0,0 +1,67 @@ +-- ============================================================================= +-- 出库明细补记「申请人」,让退回补发能找回真正该拿东西的人 +-- +-- 问题 +-- 退回后勾选补发时,补发单的申请人无从确定 —— trans_outbound 只有 +-- consumer_name(**扫码时前端自由填写**的领用人/客户名,既不可靠也可能是 +-- 客户),没有任何指回原审批单的关联。原先只能回退成「当前操作人(库管)」, +-- 但**补发是原申请人的需求**,挂在库管名下逻辑不通。 +-- +-- 本次改动 +-- trans_outbound 新增 applicant_id,创建出库时从审批单带出。 +-- 创建出库时 approval 恒非 None(request_id 已强制必填),故新单据必然有值。 +-- +-- --------------------------------------------------------------------------- +-- ★ 为什么从审批单带,而不是从 consumer_name 反查 +-- consumer_name 是自由文本、可能是客户名,按姓名反查会重蹈「重名错绑」 +-- 的覆辙(借用人姓名回填那轮刚踩过)。审批单的 applicant_id 是**主键**, +-- 没有歧义。 +-- +-- ★ 为什么不回填存量行 +-- 存量出库明细与其来源审批单之间**没有任何可用的关联**(创建时只把审批单 +-- 状态置为 3,没落任何外键),无从回填。刻意留 NULL 而不是按姓名猜 —— +-- NULL 表示「这张单产生于本列上线之前」,退回时由库管在选择器里明确指定。 +-- 与 dispatch_operator 的处理同一取舍:宁可留空,也不猜。 +-- +-- 幂等:带 IF NOT EXISTS,可重复执行。不含 psql 元命令,DataGrip 可直接执行。 +-- ============================================================================= + +BEGIN; + +ALTER TABLE trans_outbound + ADD COLUMN IF NOT EXISTS applicant_id integer; + +COMMENT ON COLUMN trans_outbound.applicant_id IS + '申请人ID,创建出库时从关联审批单带出。该列上线前的历史行为 NULL(无从回填)'; + +-- 支撑「某人申请过的出库」类查询与退回补发的申请人回查 +CREATE INDEX IF NOT EXISTS ix_trans_outbound_applicant + ON trans_outbound (applicant_id); + +COMMIT; + + +-- ============================================================================= +-- 执行后核对 +-- ============================================================================= +SELECT '=== 1) 新列已就位 ===' AS "核对项"; +SELECT column_name, data_type FROM information_schema.columns + WHERE table_name = 'trans_outbound' AND column_name = 'applicant_id'; + +SELECT '=== 2) 索引已就位 ===' AS "核对项"; +SELECT indexname FROM pg_indexes + WHERE tablename = 'trans_outbound' AND indexname = 'ix_trans_outbound_applicant'; + +SELECT '=== 3) 存量行应全部为 NULL(历史无从回填)===' AS "核对项"; +SELECT count(*) AS 出库明细总数, + count(applicant_id) AS 已记录申请人 + FROM trans_outbound; + + +-- ============================================================================= +-- 回滚段 +-- ============================================================================= +-- BEGIN; +-- DROP INDEX IF EXISTS ix_trans_outbound_applicant; +-- ALTER TABLE trans_outbound DROP COLUMN IF EXISTS applicant_id; +-- COMMIT; diff --git a/inventory-backend/app/api/v1/inbound/stock.py b/inventory-backend/app/api/v1/inbound/stock.py index d32412d..ec8a844 100644 --- a/inventory-backend/app/api/v1/inbound/stock.py +++ b/inventory-backend/app/api/v1/inbound/stock.py @@ -2945,11 +2945,15 @@ def return_from_outbound(): # ★ 申请人(补发给谁)优先级: # ① 前端显式指定 reissue_applicant_id —— 现场最清楚该给谁; - # ② 未指定则回退到**当前操作人**(办理退回的库管)。 - # 为什么不自动推断成「原申请人」:trans_outbound **没有申请人字段, - # 也没有指回原审批单的关联**(扫码出库时只把审批单状态置为 3), - # 按 consumer_name 反查会重蹈「重名错绑」的覆辙(借用人姓名回填 - # 那轮刚踩过)。故把选择权交给现场,而不是猜。 + # ② 回退到原出库明细记录的 applicant_id(创建出库时从审批单带出的 + # 真实原申请人); + # ③ 两者都没有 → **报错要求指定**。 + # + # ★ 绝不回退为「当前操作人」:补发是**原申请人的需求**,挂到办理 + # 退回的库管名下逻辑不通 —— 那张单会出现在库管的「我的申请」里, + # 而真正该拿东西的人什么也看不到。 + # ⚠ 存量出库明细的 applicant_id 为 NULL(历史无从回填),此时必须由 + # 库管在选择器里明确指定 —— 宁可多一步,也不猜错人。 if reissue_applicant_id: try: _applicant = int(reissue_applicant_id) @@ -2958,12 +2962,13 @@ def return_from_outbound(): from app.models.system import SysUser if not SysUser.query.get(_applicant): raise ValueError(f'补发申请人不存在(ID:{reissue_applicant_id})') + elif outbound.applicant_id: + _applicant = int(outbound.applicant_id) else: - _applicant = get_jwt_identity() - try: - _applicant = int(_applicant) - except (TypeError, ValueError): - raise ValueError('无法确定补发单申请人:当前登录用户缺失') + raise ValueError( + '无法确定补发单申请人:这张出库单产生于「申请人」字段上线之前,' + '请在上方选择「补发给谁」' + ) reissue = OutboundApproval( request_no=OutboundApprovalService.generate_request_no(), diff --git a/inventory-backend/app/models/outbound.py b/inventory-backend/app/models/outbound.py index 8b4697b..f06e591 100644 --- a/inventory-backend/app/models/outbound.py +++ b/inventory-backend/app/models/outbound.py @@ -141,6 +141,13 @@ class TransOutbound(db.Model): # 签字与追溯 consumer_name = db.Column(db.String(100)) # 领用人/客户 + # ★ 申请人ID:创建出库时从**关联审批单**带出(request_id 已强制必填, + # approval 恒非 None)。为什么需要它:退回后勾选补发时,补发单要挂回 + # 「真正该拿东西的人」名下;而 consumer_name 是扫码时自由填写的领用人/ + # 客户名,既不可靠也可能不是本系统用户,按姓名反查会重名错绑。 + # ⚠ 该列上线前的历史行为 NULL —— 存量出库与其来源审批单之间没有任何可用 + # 关联,无从回填;退回时由库管在选择器里明确指定。 + applicant_id = db.Column(db.Integer, index=True) signature_path = db.Column(db.Text) # 电子签名图片路径 outbound_time = db.Column(db.DateTime, default=beijing_time) operator_name = db.Column(db.String(100)) # 操作员 diff --git a/inventory-backend/app/services/outbound_service.py b/inventory-backend/app/services/outbound_service.py index 69c162c..1a14fe0 100644 --- a/inventory-backend/app/services/outbound_service.py +++ b/inventory-backend/app/services/outbound_service.py @@ -237,7 +237,11 @@ class OutboundService: 'outbound_type': data.get('outbound_type', 'SALES'), 'signature_path': data.get('signature_path'), 'operator_name': operator_name, - 'remark': data.get('remark') + 'remark': data.get('remark'), + # ★ 申请人从**关联审批单**带出。落这一列是为了让后续「退回 → 补发」 + # 能把补发单挂回真正该拿东西的人名下 —— consumer_name 是自由文本 + # (可能是客户名),不能作为依据。 + 'applicant_id': approval.applicant_id, } beijing_tz = timezone(timedelta(hours=8)) @@ -835,6 +839,10 @@ class OutboundService: # ★ 退回额度三件套:前端据此显示「已退 / 可退」并置灰已退满的行 'returned_quantity': returned, 'returnable_quantity': qty - returned, + # ★ 原申请人ID:退回弹窗勾选「需要补发」时据此**精确预填**「补发给谁」。 + # 为 NULL 表示该出库产生于本列上线之前(历史无从回填), + # 前端不预填,由库管在选择器里明确指定。 + 'applicant_id': d.applicant_id, 'unit_price': price, 'subtotal': subtotal, 'batch_sn': batch_sn,