From 27e5589a5ef6c7a067f320d5863817d333274246 Mon Sep 17 00:00:00 2001 From: yueli Date: Thu, 17 Sep 2026 12:01:11 +0800 Subject: [PATCH] =?UTF-8?q?feat(outbound,common):=20=E8=A1=A5=E5=8F=91?= =?UTF-8?q?=E5=8F=AF=E6=8C=87=E5=AE=9A=E3=80=8C=E8=A1=A5=E5=8F=91=E7=BB=99?= =?UTF-8?q?=E8=B0=81=E3=80=8D+=20=E6=8A=BD=E5=87=BA=E9=80=9A=E7=94=A8?= =?UTF-8?q?=E4=BA=BA=E5=91=98=E5=90=8D=E5=8D=95=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 一、补发申请人可选择(原单退回) 退回接口新增 reissue_applicant_id: ① 前端指定 → 校验用户存在后落库; ② 未指定 → 回退为**当前操作人**(原行为不变,向后兼容)。 为何不自动推断原申请人:trans_outbound **没有申请人字段,也没有指回原审批单 的关联**(扫码出库时只把审批单状态置为 3),按 consumer_name 反查会重蹈 「重名错绑」的覆辙(借用人姓名回填那轮刚踩过)。故把选择权交给现场,不猜。 二、抽出中性人员名单 GET /api/v1/common/active-users 实现抽到 common.active_user_options(),借库的 /transactions/borrow/users 改为调同一函数 —— 实现只有一份,但出库补发走**中性路径**,不再出现 「出库为什么在调借库的接口」这种跨模块语义错位。 仅要求登录、只返回 id 与姓名(与 /auth/users/approvers 同一处理)。 ★ 本次无需 DB 迁移:未新增任何列,补发申请人是复用已有的 outbound_approval.applicant_id。 验证(打桩/真实 token 直连接口,12 项断言全通过) · 名单只含 id/name,无邮箱/角色/部门;借库原路径返回值与新路径完全一致 · 指定「补发给谁」→ 补发单申请人 = 指定的人;备注仍含原领用人 · 不指定 → 回退为当前操作人 ★ 指定不存在的用户 → 被拒,且整笔退回回滚(流水未落库) 库存与数据零残留。 --- .../app/api/v1/common/__init__.py | 1 + inventory-backend/app/api/v1/common/users.py | 46 +++++++++++++++++++ inventory-backend/app/api/v1/inbound/stock.py | 37 ++++++++++----- inventory-backend/app/api/v1/transactions.py | 19 ++------ 4 files changed, 77 insertions(+), 26 deletions(-) create mode 100644 inventory-backend/app/api/v1/common/users.py diff --git a/inventory-backend/app/api/v1/common/__init__.py b/inventory-backend/app/api/v1/common/__init__.py index 408784d..a77ac55 100644 --- a/inventory-backend/app/api/v1/common/__init__.py +++ b/inventory-backend/app/api/v1/common/__init__.py @@ -5,3 +5,4 @@ common_bp = Blueprint('common', __name__) # 导入子模块,使其路由装饰器注册到 common_bp from . import search +from . import users # noqa: F401 「选择某人」类下拉框的共用人员名单 diff --git a/inventory-backend/app/api/v1/common/users.py b/inventory-backend/app/api/v1/common/users.py new file mode 100644 index 0000000..79dea76 --- /dev/null +++ b/inventory-backend/app/api/v1/common/users.py @@ -0,0 +1,46 @@ +# inventory-backend/app/api/v1/common/users.py +from flask import jsonify +from flask_jwt_extended import jwt_required + +from . import common_bp + + +def active_user_options(): + """ + 在职人员名单(id + 姓名)——「选择某人」类下拉框的**共用实现**。 + + ★ 为什么单独开一条中性路径,而不是复用 /transactions/borrow/users: + 那条路径在语义上属于借库模块,出库补发、报废执行等处若直接复用, + 后人读代码时会困惑「出库为什么在调借库的接口」。这里提供统一入口, + 借库那条路径改为调本函数,实现只有一份。 + + ★ 公司隔离与业务台账同口径(get_current_company_filter): + 否则 A 公司的人能在选择器里看到 B 公司人员。 + + ★ 只返回 id 与姓名:不含邮箱 / 角色 / 部门,最小披露。 + """ + from app.utils.decorators import get_current_company_filter + from app.models.system import SysUser + from app.services.trans_service import user_display_name + + company_limit = get_current_company_filter() + query = SysUser.query.filter(SysUser.status == 'active') + if company_limit is not None: + # 与 borrow_service.get_request_list 一致:SysUser.department 即公司维度 + query = query.filter(SysUser.department == company_limit) + + return [{'id': u.id, 'name': user_display_name(u)} + for u in query.order_by(SysUser.username).all()] + + +@common_bp.route('/active-users', methods=['GET']) +@jwt_required() +def get_active_users(): + """ + 在职人员名单,供借出 / 转交 / 归还 / 出库补发等多个页面的选择器共用。 + + ★ 无 permission_required,仅要求登录: + 同一份名单要被多个页面共用,绑定其中任一权限码都会让其他页面 403; + 且只暴露 id 与姓名(与 /auth/users/approvers 同一处理方式)。 + """ + return jsonify({'code': 200, 'msg': 'success', 'data': active_user_options()}) diff --git a/inventory-backend/app/api/v1/inbound/stock.py b/inventory-backend/app/api/v1/inbound/stock.py index b516717..d32412d 100644 --- a/inventory-backend/app/api/v1/inbound/stock.py +++ b/inventory-backend/app/api/v1/inbound/stock.py @@ -2768,7 +2768,8 @@ def return_from_outbound(): "is_defective": false, # 必填,true=不良品退回,false=良品退回 "reason": "错领退回", # 可选 "need_reissue": true, # 可选,退回后是否自动生成补发单 - "reissue_qty": 2 # 可选,补发数量,默认 = return_qty + "reissue_qty": 2, # 可选,补发数量,默认 = return_qty + "reissue_applicant_id": 12 # 可选,补发给谁;不传则=当前操作人 } 两条分支的差异: @@ -2790,6 +2791,8 @@ def return_from_outbound(): # 补发(可选):退回后申请人往往仍需这件东西。勾选则自动生成一张免审批出库单。 need_reissue = bool(data.get('need_reissue')) reissue_qty = data.get('reissue_qty') + # 补发给谁:不传则回退为当前操作人(见下方补发块) + reissue_applicant_id = data.get('reissue_applicant_id') # ---- 1. 入参校验(脏值一律挡在入口)---- if not outbound_id: @@ -2940,17 +2943,27 @@ def return_from_outbound(): strict=True, ) - # ★ 申请人取**当前操作人**(办理退回的库管)。 - # 为什么不是「原申请人」:trans_outbound **没有申请人字段,也没有 - # 指回原审批单的关联**(扫码出库时只把审批单状态置为 3),因此 - # 无法可靠判定原申请人是谁 —— 按 consumer_name 反查会重蹈「重名 - # 错绑」的覆辙(借用人姓名回填那轮刚踩过)。 - # 原领用人写入备注,便于人工追溯。 - _applicant = get_jwt_identity() - try: - _applicant = int(_applicant) - except (TypeError, ValueError): - raise ValueError('无法确定补发单申请人:当前登录用户缺失') + # ★ 申请人(补发给谁)优先级: + # ① 前端显式指定 reissue_applicant_id —— 现场最清楚该给谁; + # ② 未指定则回退到**当前操作人**(办理退回的库管)。 + # 为什么不自动推断成「原申请人」:trans_outbound **没有申请人字段, + # 也没有指回原审批单的关联**(扫码出库时只把审批单状态置为 3), + # 按 consumer_name 反查会重蹈「重名错绑」的覆辙(借用人姓名回填 + # 那轮刚踩过)。故把选择权交给现场,而不是猜。 + if reissue_applicant_id: + try: + _applicant = int(reissue_applicant_id) + except (TypeError, ValueError): + raise ValueError('补发申请人ID格式无效') + from app.models.system import SysUser + if not SysUser.query.get(_applicant): + raise ValueError(f'补发申请人不存在(ID:{reissue_applicant_id})') + else: + _applicant = get_jwt_identity() + try: + _applicant = int(_applicant) + except (TypeError, ValueError): + raise ValueError('无法确定补发单申请人:当前登录用户缺失') reissue = OutboundApproval( request_no=OutboundApprovalService.generate_request_no(), diff --git a/inventory-backend/app/api/v1/transactions.py b/inventory-backend/app/api/v1/transactions.py index c6b8d53..e1b26c5 100644 --- a/inventory-backend/app/api/v1/transactions.py +++ b/inventory-backend/app/api/v1/transactions.py @@ -596,22 +596,13 @@ def get_borrow_user_options(): ★ 公司隔离与借用台账同口径(get_current_company_filter): 否则 A 公司库管能在选择器里看到 B 公司人员,虽转交时会被 company 校验二次拦截,但名单本身已属越权披露。 + + ★ 实现已抽到 common.active_user_options(),与 /common/active-users 共用同一份 + 逻辑 —— 出库补发等场景走那条中性路径,避免跨模块引用借库接口。 """ - from app.utils.decorators import get_current_company_filter - from app.models.system import SysUser + from app.api.v1.common.users import active_user_options - company_limit = get_current_company_filter() - query = SysUser.query.filter(SysUser.status == 'active') - if company_limit is not None: - # 与 borrow_service.get_request_list 一致:SysUser.department 即公司维度 - query = query.filter(SysUser.department == company_limit) - - users = query.order_by(SysUser.username).all() - return jsonify({ - 'code': 200, - 'msg': 'success', - 'data': [{'id': u.id, 'name': user_display_name(u)} for u in users], - }) + return jsonify({'code': 200, 'msg': 'success', 'data': active_user_options()}) # --- 发起借库转交(双向握手第一步)---