feat(scrap): 报废申请审批流 + 按单报废(后端)
- ScrapApproval 模型 + ScrapApprovalService:提交/列表/审批/执行(锁库存扣 available_quantity 写 TransScrap) - 新路由 POST /scrap/request、/request/check-approval、GET /request、PATCH /request/<id>/approve、POST /request/<id>/execute;权限码 scrap_apply/scrap_approval/scrap_execute(无角色硬编码) - 旧 /scrap 直接报废入口保持不变 - /inbound/stock/list 每项返回 source_table,供按单流程精准选库存
This commit is contained in:
@ -466,3 +466,159 @@ class ScrapService:
|
||||
'page': page,
|
||||
'pageSize': page_size
|
||||
}
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# 报废审批流(申请 → 审批 → 按单执行)—— 镜像 借库/出库 审批框架
|
||||
# 与旧 /scrap(直接报废)并存,旧入口不动
|
||||
# ==============================================================================
|
||||
|
||||
def _current_user_role():
|
||||
try:
|
||||
claims = get_jwt()
|
||||
return (claims.get('role') or '').upper()
|
||||
except Exception:
|
||||
return ''
|
||||
|
||||
|
||||
@scrap_bp.route('/request/check-approval', methods=['POST'])
|
||||
@jwt_required()
|
||||
@permission_required('scrap_apply')
|
||||
def scrap_check_approval():
|
||||
"""提交前预检:判断所选库存(物料)是否命中“需审批”"""
|
||||
try:
|
||||
from app.services.approval_control import resolve_approval_control
|
||||
data = request.get_json() or {}
|
||||
items = data.get('items', []) or []
|
||||
need, flagged = resolve_approval_control(items)
|
||||
return jsonify({'code': 200, 'msg': 'success',
|
||||
'data': {'need_approval': need, 'materials': flagged}}), 200
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
return jsonify({'code': 500, 'msg': f'预检失败: {str(e)}'}), 500
|
||||
|
||||
|
||||
@scrap_bp.route('/request', methods=['POST'])
|
||||
@jwt_required()
|
||||
@permission_required('scrap_apply')
|
||||
def create_scrap_request():
|
||||
"""提交报废申请(不扣库存;扣减在库管执行时)"""
|
||||
try:
|
||||
from app.services.scrap_approval_service import ScrapApprovalService
|
||||
identity = get_jwt_identity()
|
||||
if not identity:
|
||||
return jsonify({'code': 401, 'msg': '用户未登录'}), 401
|
||||
data = request.get_json() or {}
|
||||
req = ScrapApprovalService.submit_approval(
|
||||
applicant_id=int(identity),
|
||||
items=data.get('items', []),
|
||||
allowed_approvers=data.get('allowed_approvers'),
|
||||
remark=data.get('remark'),
|
||||
approver_id=data.get('approver_id'),
|
||||
force_approval=(_current_user_role() == 'WAREHOUSE_MGR'),
|
||||
)
|
||||
return jsonify({'code': 200, 'msg': '报废申请已提交', 'data': req.to_dict()}), 200
|
||||
except ValueError as e:
|
||||
return jsonify({'code': 400, 'msg': str(e)}), 400
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
return jsonify({'code': 500, 'msg': f'提交报废申请失败: {str(e)}'}), 500
|
||||
|
||||
|
||||
@scrap_bp.route('/request', methods=['GET'])
|
||||
@jwt_required()
|
||||
@permission_required('scrap_approval')
|
||||
def list_scrap_requests():
|
||||
"""
|
||||
报废申请列表。
|
||||
scope: mine(我提交的,默认) / pending(待我审批) / executable(待执行) / all(管理者)
|
||||
Query: page, limit, status(可选覆盖)
|
||||
"""
|
||||
try:
|
||||
from app.services.scrap_approval_service import ScrapApprovalService
|
||||
from app.utils.decorators import is_privileged_viewer
|
||||
identity = int(get_jwt_identity())
|
||||
|
||||
scope = (request.args.get('scope') or 'mine').lower()
|
||||
page = int(request.args.get('page', 1))
|
||||
limit = int(request.args.get('limit', 10))
|
||||
status = request.args.get('status')
|
||||
status = int(status) if status not in (None, '', 'all') else None
|
||||
priv = is_privileged_viewer()
|
||||
|
||||
kwargs = {'page': page, 'limit': limit, 'status': status}
|
||||
if scope == 'pending':
|
||||
kwargs['status'] = 0
|
||||
kwargs['approver_id'] = identity
|
||||
elif scope == 'executable':
|
||||
kwargs['status'] = 1
|
||||
elif scope == 'all':
|
||||
if not priv:
|
||||
kwargs['applicant_id'] = identity
|
||||
else: # mine
|
||||
kwargs['applicant_id'] = identity
|
||||
|
||||
data = ScrapApprovalService.get_list(**kwargs)
|
||||
return jsonify({'code': 200, 'msg': '获取成功', 'data': data}), 200
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
return jsonify({'code': 500, 'msg': f'获取报废申请列表失败: {str(e)}'}), 500
|
||||
|
||||
|
||||
@scrap_bp.route('/request/<int:request_id>', methods=['GET'])
|
||||
@jwt_required()
|
||||
@permission_required('scrap_approval')
|
||||
def get_scrap_request_detail(request_id):
|
||||
try:
|
||||
from app.models.scrap_approval import ScrapApproval
|
||||
from app.utils.decorators import is_privileged_viewer
|
||||
req = db.session.get(ScrapApproval, request_id)
|
||||
if not req:
|
||||
return jsonify({'code': 404, 'msg': '报废申请不存在'}), 404
|
||||
if not is_privileged_viewer() and int(req.applicant_id) != int(get_jwt_identity()):
|
||||
# 被指定审批人也可查看
|
||||
allowed = req.get_allowed_approvers() or []
|
||||
if str(get_jwt_identity()) not in [str(a.get('value')) for a in allowed if a.get('type') == 'user']:
|
||||
return jsonify({'code': 403, 'msg': '无权查看该报废申请'}), 403
|
||||
return jsonify({'code': 200, 'msg': '获取成功', 'data': req.to_dict()}), 200
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
return jsonify({'code': 500, 'msg': f'获取报废申请详情失败: {str(e)}'}), 500
|
||||
|
||||
|
||||
@scrap_bp.route('/request/<int:request_id>/approve', methods=['PATCH'])
|
||||
@jwt_required()
|
||||
@permission_required('scrap_approval')
|
||||
def approve_scrap_request(request_id):
|
||||
"""审批报废申请(仅被指定审批人)"""
|
||||
try:
|
||||
from app.services.scrap_approval_service import ScrapApprovalService
|
||||
data = request.get_json() or {}
|
||||
req = ScrapApprovalService.approve(
|
||||
request_id, int(get_jwt_identity()),
|
||||
data.get('action'), data.get('reject_reason', '')
|
||||
)
|
||||
return jsonify({'code': 200, 'msg': '操作成功', 'data': req.to_dict()}), 200
|
||||
except ValueError as e:
|
||||
return jsonify({'code': 400, 'msg': str(e)}), 400
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
return jsonify({'code': 500, 'msg': f'审批报废申请失败: {str(e)}'}), 500
|
||||
|
||||
|
||||
@scrap_bp.route('/request/<int:request_id>/execute', methods=['POST'])
|
||||
@jwt_required()
|
||||
@permission_required('scrap_execute')
|
||||
def execute_scrap_request(request_id):
|
||||
"""按单报废:有 scrap_execute 权限者执行,扣减实物库存并写报废流水"""
|
||||
try:
|
||||
from app.services.scrap_approval_service import ScrapApprovalService
|
||||
identity = int(get_jwt_identity())
|
||||
u = SysUser.query.get(identity)
|
||||
req = ScrapApprovalService.execute(request_id, operator_name=(u.username if u else str(identity)))
|
||||
return jsonify({'code': 200, 'msg': '已执行报废并扣减库存', 'data': req.to_dict()}), 200
|
||||
except ValueError as e:
|
||||
return jsonify({'code': 400, 'msg': str(e)}), 400
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
return jsonify({'code': 500, 'msg': f'执行报废失败: {str(e)}'}), 500
|
||||
|
||||
Reference in New Issue
Block a user