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:
@ -892,20 +892,35 @@ def approve_scrap_request(request_id):
|
||||
|
||||
@scrap_bp.route('/request/<int:request_id>/withdraw', methods=['POST'])
|
||||
@jwt_required()
|
||||
@permission_required('scrap_apply')
|
||||
def withdraw_scrap_request(request_id):
|
||||
"""
|
||||
撤回已通过但尚未执行的报废申请(status 1 → 4)。
|
||||
撤回自己的报废申请单(待审批 或 已通过但未执行)。
|
||||
|
||||
★ 严格职责分离:本端点**不做模块权限校验**(@jwt_required 即可),
|
||||
权限判定完全落在「单据归属」上 —— 服务层断言
|
||||
applicant_id == 当前用户,否则 403。库管/主管可代撤。
|
||||
|
||||
与出库/借库的撤回语义一致:作废该单并释放其预占库存
|
||||
(报废模块若未接入预占,release_reserved 会安全跳过)。
|
||||
"""
|
||||
try:
|
||||
identity = get_jwt_identity()
|
||||
if not identity:
|
||||
return jsonify({'code': 401, 'msg': '用户未登录'}), 401
|
||||
|
||||
from app.services.scrap_approval_service import ScrapApprovalService
|
||||
from app.utils.decorators import is_privileged_viewer
|
||||
|
||||
# 审批页调用时调用方已有 scrap_approval 权限;此处统一走归属校验,
|
||||
# 特权角色可代撤(is_privileged_viewer 在服务层内判定)。
|
||||
req = ScrapApprovalService.withdraw(
|
||||
request_id, operator_id=int(get_jwt_identity())
|
||||
request_id,
|
||||
operator_id=int(identity),
|
||||
require_owner=not is_privileged_viewer(),
|
||||
)
|
||||
return jsonify({'code': 200, 'msg': '报废申请已撤回', 'data': req.to_dict()}), 200
|
||||
except PermissionError as e:
|
||||
return jsonify({'code': 403, 'msg': str(e)}), 403
|
||||
except ValueError as e:
|
||||
return jsonify({'code': 400, 'msg': str(e)}), 400
|
||||
except Exception as e:
|
||||
|
||||
Reference in New Issue
Block a user