2026-05-12 16:33:18 +08:00
|
|
|
|
import traceback
|
|
|
|
|
|
from flask import Blueprint, request, jsonify, current_app
|
|
|
|
|
|
from flask_jwt_extended import jwt_required, get_jwt_identity
|
|
|
|
|
|
from app.services.purchase_service import PurchaseService
|
|
|
|
|
|
from app.utils.decorators import permission_required
|
|
|
|
|
|
|
|
|
|
|
|
purchase_bp = Blueprint('purchase', __name__, url_prefix='/api/v1/purchase')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_current_user_id():
|
|
|
|
|
|
"""获取当前登录用户ID"""
|
|
|
|
|
|
identity = get_jwt_identity()
|
|
|
|
|
|
return identity
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_current_user_role():
|
|
|
|
|
|
"""获取当前用户角色"""
|
|
|
|
|
|
from flask_jwt_extended import get_jwt
|
|
|
|
|
|
claims = get_jwt()
|
|
|
|
|
|
return claims.get('role')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# --------------------------------------------------------
|
|
|
|
|
|
# 1. 采购申请列表
|
|
|
|
|
|
# GET /api/v1/purchase
|
|
|
|
|
|
# --------------------------------------------------------
|
|
|
|
|
|
@purchase_bp.route('', methods=['GET'])
|
|
|
|
|
|
@jwt_required()
|
|
|
|
|
|
def get_purchase_list():
|
|
|
|
|
|
"""获取采购申请列表"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
page = int(request.args.get('page', 1))
|
|
|
|
|
|
per_page = int(request.args.get('limit', 20))
|
|
|
|
|
|
status = request.args.get('status')
|
|
|
|
|
|
status = int(status) if status is not None else None
|
|
|
|
|
|
|
|
|
|
|
|
user_id = get_current_user_id()
|
|
|
|
|
|
role = get_current_user_role()
|
|
|
|
|
|
|
|
|
|
|
|
# 普通用户(SUPERVISOR 和 SUPER_ADMIN 除外)只看自己提交的
|
|
|
|
|
|
is_admin = role in ('SUPERVISOR', 'SUPER_ADMIN')
|
|
|
|
|
|
|
|
|
|
|
|
result = PurchaseService.get_purchase_list(
|
|
|
|
|
|
page=page,
|
|
|
|
|
|
per_page=per_page,
|
|
|
|
|
|
requester_id=None if is_admin else user_id,
|
|
|
|
|
|
status=status
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
return jsonify({'code': 200, 'msg': '获取成功', 'data': result})
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
traceback.print_exc()
|
|
|
|
|
|
return jsonify({'code': 500, 'msg': f'获取失败: {str(e)}'}), 500
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# --------------------------------------------------------
|
|
|
|
|
|
# 2. 创建采购申请
|
|
|
|
|
|
# POST /api/v1/purchase
|
|
|
|
|
|
# --------------------------------------------------------
|
|
|
|
|
|
@purchase_bp.route('', methods=['POST'])
|
|
|
|
|
|
@jwt_required()
|
|
|
|
|
|
def create_purchase_request():
|
|
|
|
|
|
"""创建采购申请"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
data = request.get_json()
|
|
|
|
|
|
if not data:
|
|
|
|
|
|
return jsonify({'code': 400, 'msg': '无有效数据'}), 400
|
|
|
|
|
|
|
|
|
|
|
|
user_id = get_current_user_id()
|
|
|
|
|
|
|
|
|
|
|
|
# 必填校验
|
|
|
|
|
|
required = ['name', 'quantity', 'purchase_date', 'approver_id']
|
|
|
|
|
|
for field in required:
|
|
|
|
|
|
if field not in data or str(data.get(field, '')).strip() == '':
|
|
|
|
|
|
return jsonify({'code': 400, 'msg': f'缺少必填字段: {field}'}), 400
|
|
|
|
|
|
|
2026-05-12 17:48:29 +08:00
|
|
|
|
# 图片必填强校验
|
|
|
|
|
|
images = data.get('images')
|
|
|
|
|
|
if not images or (isinstance(images, list) and len(images) == 0):
|
|
|
|
|
|
return jsonify({'code': 400, 'msg': '请上传采购凭证/物品图片'}), 400
|
|
|
|
|
|
|
2026-05-12 16:33:18 +08:00
|
|
|
|
purchase = PurchaseService.create_purchase_request(data, requester_id=user_id)
|
|
|
|
|
|
|
|
|
|
|
|
return jsonify({
|
|
|
|
|
|
'code': 200,
|
|
|
|
|
|
'msg': '创建成功',
|
|
|
|
|
|
'data': purchase.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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# --------------------------------------------------------
|
|
|
|
|
|
# 3. 获取采购申请详情
|
|
|
|
|
|
# GET /api/v1/purchase/<id>
|
|
|
|
|
|
# --------------------------------------------------------
|
|
|
|
|
|
@purchase_bp.route('/<int:purchase_id>', methods=['GET'])
|
|
|
|
|
|
@jwt_required()
|
|
|
|
|
|
def get_purchase_detail(purchase_id):
|
|
|
|
|
|
"""获取采购申请详情"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
purchase = PurchaseService.get_purchase_by_id(purchase_id)
|
|
|
|
|
|
if not purchase:
|
|
|
|
|
|
return jsonify({'code': 404, 'msg': '采购申请不存在'}), 404
|
|
|
|
|
|
|
|
|
|
|
|
# 普通用户只能看自己的
|
|
|
|
|
|
user_id = get_current_user_id()
|
|
|
|
|
|
role = get_current_user_role()
|
|
|
|
|
|
is_admin = role in ('SUPERVISOR', 'SUPER_ADMIN')
|
|
|
|
|
|
if not is_admin and purchase['requester_id'] != user_id:
|
|
|
|
|
|
return jsonify({'code': 403, 'msg': '无权查看此申请'}), 403
|
|
|
|
|
|
|
|
|
|
|
|
return jsonify({'code': 200, 'msg': '获取成功', 'data': purchase}), 200
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
return jsonify({'code': 500, 'msg': str(e)}), 500
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# --------------------------------------------------------
|
|
|
|
|
|
# 4. 审批采购申请
|
|
|
|
|
|
# PATCH /api/v1/purchase/<id>/approve
|
|
|
|
|
|
# --------------------------------------------------------
|
|
|
|
|
|
@purchase_bp.route('/<int:purchase_id>/approve', methods=['PATCH'])
|
|
|
|
|
|
@jwt_required()
|
|
|
|
|
|
def approve_purchase_request(purchase_id):
|
|
|
|
|
|
"""审批采购申请"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
user_id = get_current_user_id()
|
|
|
|
|
|
role = get_current_user_role()
|
|
|
|
|
|
|
|
|
|
|
|
if role not in ('SUPERVISOR', 'SUPER_ADMIN'):
|
|
|
|
|
|
return jsonify({'code': 403, 'msg': '只有主管或超级管理员可以审批'}), 403
|
|
|
|
|
|
|
|
|
|
|
|
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': '无效的审批操作'}), 400
|
|
|
|
|
|
|
|
|
|
|
|
if action == 'reject' and not reject_reason:
|
|
|
|
|
|
return jsonify({'code': 400, 'msg': '驳回时必须提供原因'}), 400
|
|
|
|
|
|
|
|
|
|
|
|
purchase = PurchaseService.approve_purchase_request(
|
|
|
|
|
|
purchase_id=purchase_id,
|
|
|
|
|
|
user_id=user_id,
|
|
|
|
|
|
action=action,
|
|
|
|
|
|
reject_reason=reject_reason
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
msg = '审批通过' if action == 'approve' else '已驳回'
|
|
|
|
|
|
return jsonify({'code': 200, 'msg': msg, 'data': purchase.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. 获取可选审批人列表
|
|
|
|
|
|
# GET /api/v1/purchase/approvers
|
|
|
|
|
|
# --------------------------------------------------------
|
|
|
|
|
|
@purchase_bp.route('/approvers', methods=['GET'])
|
|
|
|
|
|
@jwt_required()
|
|
|
|
|
|
def get_purchase_approvers():
|
|
|
|
|
|
"""获取可选审批人列表(主管+超管)"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
from app.models.system import SysUser
|
|
|
|
|
|
users = SysUser.query.filter(
|
|
|
|
|
|
SysUser.role.in_(['SUPER_ADMIN', 'SUPERVISOR']),
|
|
|
|
|
|
SysUser.status == 'active'
|
|
|
|
|
|
).all()
|
|
|
|
|
|
|
|
|
|
|
|
return jsonify({
|
|
|
|
|
|
'code': 200,
|
|
|
|
|
|
'msg': '获取成功',
|
|
|
|
|
|
'data': [
|
|
|
|
|
|
{'id': u.id, 'username': u.username, 'email': u.email or '', 'role': u.role}
|
|
|
|
|
|
for u in users
|
|
|
|
|
|
]
|
|
|
|
|
|
}), 200
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
return jsonify({'code': 500, 'msg': str(e)}), 500
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# --------------------------------------------------------
|
|
|
|
|
|
# 6. 根据名称/规格自动补全
|
|
|
|
|
|
# GET /api/v1/purchase/auto-fill?keyword=xxx
|
|
|
|
|
|
# --------------------------------------------------------
|
|
|
|
|
|
@purchase_bp.route('/auto-fill', methods=['GET'])
|
|
|
|
|
|
@jwt_required()
|
|
|
|
|
|
def auto_fill_purchase():
|
|
|
|
|
|
"""根据名称或规格自动补全另一个字段"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
keyword = request.args.get('keyword', '').strip()
|
|
|
|
|
|
if not keyword:
|
|
|
|
|
|
return jsonify({'code': 200, 'msg': 'ok', 'data': None}), 200
|
|
|
|
|
|
|
|
|
|
|
|
result = PurchaseService.auto_fill_from_material(keyword)
|
|
|
|
|
|
return jsonify({'code': 200, 'msg': 'ok', 'data': result}), 200
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
return jsonify({'code': 500, 'msg': str(e)}), 500
|
2026-05-12 17:48:29 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# --------------------------------------------------------
|
2026-07-14 15:25:00 +08:00
|
|
|
|
# 7. 已审批且未入库的采购单列表(供库管按单入库使用)
|
|
|
|
|
|
# GET /api/v1/purchase/approved-unstocked?page=1&keyword=xxx
|
|
|
|
|
|
# --------------------------------------------------------
|
|
|
|
|
|
@purchase_bp.route('/approved-unstocked', methods=['GET'])
|
|
|
|
|
|
@jwt_required()
|
2026-07-14 17:02:07 +08:00
|
|
|
|
@permission_required('inbound_buy')
|
2026-07-14 15:25:00 +08:00
|
|
|
|
def get_approved_unstocked_requests():
|
|
|
|
|
|
"""获取已审批通过且未入库的采购申请列表"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
page = int(request.args.get('page', 1))
|
|
|
|
|
|
per_page = int(request.args.get('limit', 20))
|
|
|
|
|
|
keyword = request.args.get('keyword', '').strip() or None
|
|
|
|
|
|
|
|
|
|
|
|
result = PurchaseService.get_approved_requests(
|
|
|
|
|
|
page=page, per_page=per_page, keyword=keyword
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
return jsonify({'code': 200, 'msg': '获取成功', 'data': result}), 200
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
traceback.print_exc()
|
|
|
|
|
|
return jsonify({'code': 500, 'msg': f'获取失败: {str(e)}'}), 500
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# --------------------------------------------------------
|
|
|
|
|
|
# 8. 物料基础信息搜索(分页)
|
2026-05-12 17:48:29 +08:00
|
|
|
|
# GET /api/v1/purchase/search-material?keyword=xxx&page=1
|
|
|
|
|
|
# --------------------------------------------------------
|
|
|
|
|
|
@purchase_bp.route('/search-material', methods=['GET'])
|
|
|
|
|
|
@jwt_required()
|
|
|
|
|
|
def search_material_for_purchase():
|
|
|
|
|
|
"""物料基础信息搜索接口,支持分页,用于采购申请弹窗"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
keyword = request.args.get('keyword', '')
|
|
|
|
|
|
page = request.args.get('page', 1, type=int)
|
|
|
|
|
|
limit = 20
|
|
|
|
|
|
|
|
|
|
|
|
result = PurchaseService.search_base_material(keyword, page, limit)
|
|
|
|
|
|
return jsonify({
|
|
|
|
|
|
'code': 200,
|
|
|
|
|
|
'msg': 'success',
|
|
|
|
|
|
'data': result['items'],
|
|
|
|
|
|
'total': result['total'],
|
|
|
|
|
|
'has_next': result['has_next']
|
|
|
|
|
|
}), 200
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
traceback.print_exc()
|
|
|
|
|
|
return jsonify({'code': 500, 'msg': str(e)}), 500
|