fix(outbound): 补发申请人取原申请人,绝不再回退为库管

问题
----
上一版在库管未指定「补发给谁」时,把补发单申请人回退成了**当前操作人(库管)**。
但补发是**原申请人的需求**,挂到库管名下逻辑不通 —— 那张单会出现在库管的
「我的申请」里,而真正该拿东西的人什么也看不到。

根因
----
trans_outbound 只有 consumer_name(**扫码时前端自由填写**的领用人/客户名,
既不可靠也可能是外部客户),没有任何指回原审批单的关联,所以当时只能退而求其次。

根治
----
一、trans_outbound 新增 applicant_id,**创建出库时从关联审批单带出**
    (request_id 已强制必填、approval 恒非 None,故新单据必然有值)。
    落这一列后,「退回 → 补发」即可自动找回真正的原申请人。
    ⚠ 存量行为 NULL —— 存量出库与其来源审批单之间没有任何可用关联,无从回填。
      刻意留 NULL 而不是按姓名猜(consumer_name 是自由文本,会重名错绑),
      与 dispatch_operator 同一取舍:宁可留空,也不猜。

二、退回接口的申请人优先级改为:
      ① 前端显式指定 reissue_applicant_id
      ② 原出库明细记录的 applicant_id(真实原申请人)
      ③ 都没有 → **报错要求指定**
    ★ 彻底移除「回退为当前操作人」—— 那正是本次要修的逻辑错误。

三、出库列表明细返回 applicant_id,供前端精确预填。

验证(9 项断言全通过)
  · 新单据 → 申请人 = 原申请人(12),绝不是库管(7)
  · 显式指定优先于原申请人
  ★ 历史单据未指定 → 接口拒绝、要求选择「补发给谁」、整笔回滚、
    且**未生成任何挂在库管名下的补发单**
  · 历史单据 + 显式指定 → 正常
  库存与数据零残留。
This commit is contained in:
yueli
2026-09-17 12:07:21 +08:00
parent 05524c988a
commit 0005a689dc
4 changed files with 98 additions and 11 deletions

View File

@ -0,0 +1,67 @@
-- =============================================================================
-- 出库明细补记「申请人」,让退回补发能找回真正该拿东西的人
--
-- 问题
-- 退回后勾选补发时,补发单的申请人无从确定 —— trans_outbound 只有
-- consumer_name**扫码时前端自由填写**的领用人/客户名,既不可靠也可能是
-- 客户),没有任何指回原审批单的关联。原先只能回退成「当前操作人(库管)」,
-- 但**补发是原申请人的需求**,挂在库管名下逻辑不通。
--
-- 本次改动
-- trans_outbound 新增 applicant_id创建出库时从审批单带出。
-- 创建出库时 approval 恒非 Nonerequest_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;

View File

@ -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(),

View File

@ -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)) # 操作员

View File

@ -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,