fix(outbound+trans): 修复POST接口错误数据清洗导致的sku/quantity字段被清除Bug,并新增出库审批工作流全链路
This commit is contained in:
@ -318,6 +318,41 @@ def get_my_permissions():
|
||||
return jsonify({'msg': f'获取权限失败: {str(e)}'}), 500
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# 获取可指定审批人列表(SUPERVISOR / SUPER_ADMIN 且 status=active)
|
||||
# ==============================================================================
|
||||
@auth_bp.route('/users/approvers', methods=['GET'])
|
||||
@jwt_required()
|
||||
def get_approvers():
|
||||
"""
|
||||
查询角色为 SUPER_ADMIN 或 SUPERVISOR 且状态为活跃的用户列表
|
||||
返回: [{id, username, email, role}]
|
||||
"""
|
||||
try:
|
||||
from app.models.system import SysUser
|
||||
|
||||
users = SysUser.query.filter(
|
||||
SysUser.role.in_(['SUPER_ADMIN', 'SUPERVISOR']),
|
||||
SysUser.status == 'active'
|
||||
).all()
|
||||
|
||||
return jsonify({
|
||||
'msg': '获取成功',
|
||||
'data': [
|
||||
{
|
||||
'id': u.id,
|
||||
'username': u.username,
|
||||
'email': u.email or '',
|
||||
'role': u.role
|
||||
} for u in users
|
||||
]
|
||||
}), 200
|
||||
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"Get Approvers Failed: {str(e)}")
|
||||
return jsonify({'msg': f'获取审批人列表失败: {str(e)}'}), 500
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# 获取当前用户个人资料(自我查看)
|
||||
# ==============================================================================
|
||||
|
||||
@ -148,44 +148,6 @@ def create_outbound():
|
||||
if not data.get('consumer_name') or not data.get('signature_path'):
|
||||
return jsonify({'code': 400, 'msg': '领用人及签名信息缺失'}), 400
|
||||
|
||||
# 数据清洗:移除用户没有权限的字段
|
||||
user_permissions = get_current_user_permissions()
|
||||
# 超级管理员不过滤
|
||||
if 'outbound_list:*' not in user_permissions:
|
||||
# 字段名到权限码的映射(与前端 permissionMap 保持一致)
|
||||
field_to_perm = {
|
||||
'outbound_no': 'outbound_list:outbound_no',
|
||||
'outbound_time': 'outbound_list:outbound_time',
|
||||
'outbound_type': 'outbound_list:outbound_type',
|
||||
'total_amount': 'outbound_list:total_amount',
|
||||
'consumer_name': 'outbound_list:consumer_name',
|
||||
'operator_name': 'outbound_list:operator_name',
|
||||
'remark': 'outbound_list:remark',
|
||||
'signature_path': 'outbound_list:signature_path',
|
||||
# 明细字段
|
||||
'sku': 'outbound_list:sku',
|
||||
'name': 'outbound_list:name',
|
||||
'material_type': 'outbound_list:material_type',
|
||||
'category': 'outbound_list:category',
|
||||
'spec_model': 'outbound_list:spec_model',
|
||||
'quantity': 'outbound_list:quantity',
|
||||
'unit_price': 'outbound_list:unit_price',
|
||||
'price': 'outbound_list:unit_price', # 兼容 price 字段
|
||||
'subtotal': 'outbound_list:subtotal',
|
||||
}
|
||||
# 清洗顶层字段
|
||||
for field in list(data.keys()):
|
||||
perm_code = field_to_perm.get(field)
|
||||
if perm_code and perm_code not in user_permissions:
|
||||
data.pop(field, None)
|
||||
# 清洗 items 中的每个商品字段
|
||||
if 'items' in data and isinstance(data['items'], list):
|
||||
for item in data['items']:
|
||||
for field in list(item.keys()):
|
||||
perm_code = field_to_perm.get(field)
|
||||
if perm_code and perm_code not in user_permissions:
|
||||
item.pop(field, None)
|
||||
|
||||
try:
|
||||
# ★ [修改] 调用批量创建服务
|
||||
outbound_no = OutboundService.create_outbound_batch(data, operator_name=final_operator)
|
||||
@ -233,3 +195,244 @@ def get_outbound_list():
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
return jsonify({'code': 500, 'msg': str(e)}), 500
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# 出库审批相关接口
|
||||
# ==============================================================================
|
||||
|
||||
from app.services.outbound_service import OutboundApprovalService
|
||||
|
||||
|
||||
def get_current_user_id():
|
||||
"""获取当前用户ID"""
|
||||
from app.models.system import SysUser
|
||||
identity = get_jwt_identity()
|
||||
if not identity:
|
||||
return None
|
||||
# JWT identity 是数据库主键整数,直接用 .get() 查询
|
||||
user = SysUser.query.get(identity)
|
||||
return user.id if user else None
|
||||
|
||||
|
||||
def get_current_user_info():
|
||||
"""获取当前用户信息和角色"""
|
||||
from app.models.system import SysUser
|
||||
identity = get_jwt_identity()
|
||||
if not identity:
|
||||
return None, None
|
||||
# JWT identity 是数据库主键整数,直接用 .get() 查询
|
||||
user = SysUser.query.get(identity)
|
||||
return user.id if user else None, user.role if user else None
|
||||
|
||||
|
||||
# --------------------------------------------------------
|
||||
# 4. 创建出库审批单
|
||||
# POST /api/v1/outbound/request
|
||||
# --------------------------------------------------------
|
||||
@outbound_bp.route('/request', methods=['POST'])
|
||||
@jwt_required()
|
||||
def create_outbound_request():
|
||||
"""
|
||||
创建出库审批单(申请阶段,用户只需提交宏观物料信息,无需关联具体库存记录)
|
||||
|
||||
请求体示例:
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"name": "物料A", // 物料名称 (必填)
|
||||
"spec_model": "规格1", // 规格型号 (必填)
|
||||
"quantity": 10, // 计划出库数量 (必填)
|
||||
"warehouse_location": "A区-01-01", // 库位 (可选)
|
||||
"remark": "备注信息" // 物品备注 (可选)
|
||||
}
|
||||
],
|
||||
"allowed_approvers": [
|
||||
{"type": "role", "value": "SUPERVISOR"},
|
||||
{"type": "role", "value": "SUPER_ADMIN"}
|
||||
],
|
||||
"remark": "紧急出库申请"
|
||||
}
|
||||
"""
|
||||
try:
|
||||
user_id, user_role = get_current_user_info()
|
||||
if not user_id:
|
||||
return jsonify({'code': 401, 'msg': '用户未登录'}), 401
|
||||
|
||||
data = request.get_json()
|
||||
if not data:
|
||||
return jsonify({'code': 400, 'msg': '无有效数据'}), 400
|
||||
|
||||
items = data.get('items', [])
|
||||
if not items:
|
||||
return jsonify({'code': 400, 'msg': '出库物品列表不能为空'}), 400
|
||||
|
||||
# ★ 申请阶段仅校验宏观字段:名称、规格、数量
|
||||
required_fields = ['name', 'spec_model', 'quantity']
|
||||
for idx, item in enumerate(items):
|
||||
missing = [f for f in required_fields if f not in item or item.get(f) is None or str(item.get(f)).strip() == '']
|
||||
if missing:
|
||||
return jsonify({
|
||||
'code': 400,
|
||||
'msg': f'第{idx + 1}条物品缺少必填字段: {", ".join(missing)}。'
|
||||
f'必须包含: name(名称), spec_model(规格), quantity(数量)'
|
||||
}), 400
|
||||
try:
|
||||
qty = float(item.get('quantity', 0))
|
||||
if qty <= 0:
|
||||
return jsonify({'code': 400, 'msg': f'第{idx + 1}条物品的出库数量必须大于0'}), 400
|
||||
except (TypeError, ValueError):
|
||||
return jsonify({'code': 400, 'msg': f'第{idx + 1}条物品的 quantity 格式无效'}), 400
|
||||
|
||||
# ★ 指定审批人:前端传 approver_id 则精准通知,否则用默认角色规则
|
||||
approver_id = data.get('approver_id')
|
||||
_default_approvers = [
|
||||
{"type": "role", "value": "SUPERVISOR"},
|
||||
{"type": "role", "value": "SUPER_ADMIN"}
|
||||
]
|
||||
allowed_approvers = data.get('allowed_approvers') or _default_approvers
|
||||
|
||||
# 创建审批单(直接存储前端传来的宏观信息快照,不查询库存)
|
||||
approval = OutboundApprovalService.create_request(
|
||||
applicant_id=user_id,
|
||||
items=items,
|
||||
allowed_approvers=allowed_approvers,
|
||||
remark=data.get('remark'),
|
||||
approver_id=approver_id
|
||||
)
|
||||
|
||||
return jsonify({
|
||||
'code': 200,
|
||||
'msg': '审批单创建成功',
|
||||
'data': approval.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
|
||||
|
||||
|
||||
# --------------------------------------------------------
|
||||
# 5. 审批出库申请
|
||||
# PATCH /api/v1/outbound/request/<id>/approve
|
||||
# --------------------------------------------------------
|
||||
@outbound_bp.route('/request/<int:request_id>/approve', methods=['PATCH'])
|
||||
@jwt_required()
|
||||
def approve_outbound_request(request_id):
|
||||
"""
|
||||
审批出库申请
|
||||
|
||||
请求体示例:
|
||||
{
|
||||
"action": "approve", // "approve" 通过, "reject" 驳回
|
||||
"reject_reason": "库存不足" // 仅在驳回时需要
|
||||
}
|
||||
"""
|
||||
try:
|
||||
user_id, user_role = get_current_user_info()
|
||||
if not user_id:
|
||||
return jsonify({'code': 401, 'msg': '用户未登录'}), 401
|
||||
|
||||
data = request.get_json() or {}
|
||||
action = data.get('action', 'approve')
|
||||
reject_reason = data.get('reject_reason')
|
||||
|
||||
if action not in ('approve', 'reject'):
|
||||
return jsonify({'code': 400, 'msg': '无效的审批操作,仅支持 approve 或 reject'}), 400
|
||||
|
||||
if action == 'reject' and not reject_reason:
|
||||
return jsonify({'code': 400, 'msg': '驳回时必须提供原因'}), 400
|
||||
|
||||
success, message, approval = OutboundApprovalService.approve(
|
||||
request_id=request_id,
|
||||
user_id=user_id,
|
||||
user_role=user_role,
|
||||
action=action,
|
||||
reject_reason=reject_reason
|
||||
)
|
||||
|
||||
if not success:
|
||||
return jsonify({'code': 400, 'msg': message}), 400
|
||||
|
||||
return jsonify({
|
||||
'code': 200,
|
||||
'msg': message,
|
||||
'data': approval.to_dict() if approval else None
|
||||
}), 200
|
||||
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
return jsonify({'code': 500, 'msg': f'服务器内部错误: {str(e)}'}), 500
|
||||
|
||||
|
||||
# --------------------------------------------------------
|
||||
# 6. 获取审批单列表
|
||||
# GET /api/v1/outbound/request
|
||||
# --------------------------------------------------------
|
||||
@outbound_bp.route('/request', methods=['GET'])
|
||||
@jwt_required()
|
||||
def get_outbound_request_list():
|
||||
"""
|
||||
获取出库审批单列表
|
||||
|
||||
Query参数:
|
||||
- page: 页码 (默认1)
|
||||
- limit: 每页数量 (默认10)
|
||||
- applicant_id: 按申请人筛选 (可选)
|
||||
- status: 按状态筛选 (0待审/1通过/2驳回/3完成, 可选)
|
||||
"""
|
||||
try:
|
||||
page = int(request.args.get('page', 1))
|
||||
limit = int(request.args.get('limit', 10))
|
||||
|
||||
applicant_id = request.args.get('applicant_id')
|
||||
if applicant_id:
|
||||
applicant_id = int(applicant_id)
|
||||
|
||||
status = request.args.get('status')
|
||||
if status is not None:
|
||||
status = int(status)
|
||||
|
||||
result = OutboundApprovalService.get_request_list(
|
||||
page=page,
|
||||
per_page=limit,
|
||||
applicant_id=applicant_id,
|
||||
status=status
|
||||
)
|
||||
|
||||
return jsonify({
|
||||
'code': 200,
|
||||
'msg': '获取成功',
|
||||
'data': result
|
||||
}), 200
|
||||
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
return jsonify({'code': 500, 'msg': str(e)}), 500
|
||||
|
||||
|
||||
# --------------------------------------------------------
|
||||
# 7. 获取单个审批单详情
|
||||
# GET /api/v1/outbound/request/<id>
|
||||
# --------------------------------------------------------
|
||||
@outbound_bp.route('/request/<int:request_id>', methods=['GET'])
|
||||
@jwt_required()
|
||||
def get_outbound_request_detail(request_id):
|
||||
"""获取出库审批单详情"""
|
||||
try:
|
||||
approval = OutboundApprovalService.get_request_by_id(request_id)
|
||||
|
||||
if not approval:
|
||||
return jsonify({'code': 404, 'msg': '审批单不存在'}), 404
|
||||
|
||||
return jsonify({
|
||||
'code': 200,
|
||||
'msg': '获取成功',
|
||||
'data': approval.to_dict()
|
||||
}), 200
|
||||
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
return jsonify({'code': 500, 'msg': str(e)}), 500
|
||||
|
||||
@ -66,26 +66,6 @@ def filter_item_by_permissions(item_dict, user_permissions, prefix='op_records')
|
||||
)
|
||||
def create_borrow():
|
||||
data = request.get_json()
|
||||
# 数据清洗:移除用户没有权限的字段
|
||||
user_permissions = get_current_user_permissions()
|
||||
# 超级管理员不过滤
|
||||
if '*' not in user_permissions:
|
||||
field_to_perm = {
|
||||
'borrow_no': 'op_records:borrow_no',
|
||||
'borrower_name': 'op_records:borrower_name',
|
||||
'sku': 'op_records:sku',
|
||||
'borrow_time': 'op_records:borrow_time',
|
||||
'return_time': 'op_records:return_time',
|
||||
'status': 'op_records:status',
|
||||
'expected_return_time': 'op_records:expected_return_time',
|
||||
'return_location': 'op_records:return_location',
|
||||
'borrow_signature': 'op_records:borrow_signature',
|
||||
'return_signature': 'op_records:return_signature',
|
||||
}
|
||||
for field in list(data.keys()):
|
||||
perm_code = field_to_perm.get(field)
|
||||
if perm_code and perm_code not in user_permissions:
|
||||
data.pop(field, None)
|
||||
try:
|
||||
no = TransService.create_borrow(data)
|
||||
return jsonify({'code': 200, 'msg': '借用成功', 'data': {'borrow_no': no}})
|
||||
@ -120,26 +100,6 @@ def scan_borrowed_item():
|
||||
)
|
||||
def submit_return():
|
||||
data = request.get_json()
|
||||
# 数据清洗:移除用户没有权限的字段
|
||||
user_permissions = get_current_user_permissions()
|
||||
# 超级管理员不过滤
|
||||
if '*' not in user_permissions:
|
||||
field_to_perm = {
|
||||
'borrow_no': 'op_records:borrow_no',
|
||||
'borrower_name': 'op_records:borrower_name',
|
||||
'sku': 'op_records:sku',
|
||||
'borrow_time': 'op_records:borrow_time',
|
||||
'return_time': 'op_records:return_time',
|
||||
'status': 'op_records:status',
|
||||
'expected_return_time': 'op_records:expected_return_time',
|
||||
'return_location': 'op_records:return_location',
|
||||
'borrow_signature': 'op_records:borrow_signature',
|
||||
'return_signature': 'op_records:return_signature',
|
||||
}
|
||||
for field in list(data.keys()):
|
||||
perm_code = field_to_perm.get(field)
|
||||
if perm_code and perm_code not in user_permissions:
|
||||
data.pop(field, None)
|
||||
user = get_jwt_identity() # 库管
|
||||
try:
|
||||
TransService.process_return(data, operator_name=user)
|
||||
|
||||
Reference in New Issue
Block a user