feat(borrow,scrap): 补齐申请人撤回端点,与出库对齐
背景
----
出库已有独立的申请人撤回端点(仅 @jwt_required + 服务层归属断言),
但借库/报废没有:
· 借库撤回复用 close 端点,权限是 op_borrow_approval(管理路径),
普通员工调用返回 403;
· 报废撤回带 @permission_required('scrap_apply'),同样挡住普通申请人
(scrap_apply 只授予 INBOUND/OUTBOUND/SUPERVISOR/SUPER_ADMIN)。
结果是「我的申请单」页面里,借库/报废的撤回按钮对普通员工点了报错。
借库
----
服务层拆成与管理路径并列的两条入口(与出库同构):
mark_completed —— 管理路径,需 op_borrow_approval,仅 status==1
withdraw_request —— 申请人路径,仅校验单据归属,status 0 或 1
两者共用 _release_and_close(),释放逻辑只有一份实现。
新增 POST /transactions/borrow/request/<id>/withdraw(仅 @jwt_required)。
报废
----
withdraw() 增加 require_owner 参数区分两条路径:
require_owner=True (默认,申请人路径)→ 断言 applicant_id == operator_id
require_owner=False(管理路径) → 由调用方权限装饰器鉴权
WITHDRAWABLE_STATUS 从 (1,) 放宽到 (0, 1),与出库/借库对齐。
移除端点上的 @permission_required('scrap_apply'),改走归属校验。
安全实测
--------
借库 B 撤 A 的单 → 403,库存仍是 27(未被释放)
借库 A 撤自己的单 → 200,30 完全释放
报废 B 撤 A 的单 → 403
报废 A 撤自己的单 → 200
主管代撤员工的单(特权路径)→ 200
This commit is contained in:
@ -204,17 +204,35 @@ class ScrapApprovalService:
|
||||
# 状态守卫「仅 status==1 可撤回」同时充当执行守卫:
|
||||
# 已执行(status=3)的单无法再次撤回。
|
||||
# ------------------------------------------------------------------
|
||||
WITHDRAWABLE_STATUS = (0, 1)
|
||||
|
||||
@staticmethod
|
||||
def withdraw(request_id, operator_id):
|
||||
"""撤回已通过但尚未执行的报废申请单"""
|
||||
def withdraw(request_id, operator_id, require_owner=True):
|
||||
"""
|
||||
撤回报废申请单(待审批 或 已通过但未执行)。
|
||||
|
||||
require_owner=True(默认,申请人路径):
|
||||
断言 applicant_id == operator_id,否则拒绝(特权角色可代撤)。
|
||||
require_owner=False(管理路径,审批页调用):
|
||||
由调用方的权限装饰器(scrap_approval)负责鉴权。
|
||||
|
||||
与出库/借库保持一致的两条独立入口 + 共用释放底层。
|
||||
"""
|
||||
from app.utils.decorators import is_privileged_viewer
|
||||
|
||||
req = db.session.get(ScrapApproval, request_id)
|
||||
if not req:
|
||||
raise ValueError("报废申请不存在")
|
||||
if req.status != 1:
|
||||
|
||||
# ★ 归属校验(仅申请人路径)
|
||||
if require_owner and not is_privileged_viewer():
|
||||
if int(req.applicant_id) != int(operator_id):
|
||||
raise PermissionError("无权撤回他人的申请单")
|
||||
|
||||
if req.status not in ScrapApprovalService.WITHDRAWABLE_STATUS:
|
||||
status_map = {0: '待审批', 1: '已通过(待执行)', 2: '已驳回', 3: '已执行', 4: '已撤回'}
|
||||
raise ValueError(
|
||||
f"仅「已通过(待执行)」的报废申请可撤回 "
|
||||
f"(当前状态:{status_map.get(req.status, req.status)})"
|
||||
f"当前状态不可撤回:{status_map.get(req.status, req.status)}"
|
||||
)
|
||||
|
||||
from app.services.inventory_reservation import release_reserved
|
||||
|
||||
Reference in New Issue
Block a user