import traceback from flask import Blueprint, request, jsonify, current_app from flask_jwt_extended import jwt_required, get_jwt, get_jwt_identity from app.services.purchase_service import PurchaseService from app.utils.decorators import permission_required, prevent_double_submit 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(): """获取当前用户角色""" claims = get_jwt() return claims.get('role') def _user_has_purchase_perm(): """检查当前用户是否有 inbound_purchase 权限(菜单或任意操作权限)""" claims = get_jwt() role = claims.get('role', '') user_company = claims.get('company_name', '') if role.upper() in ('SUPER_ADMIN', 'SUPERVISOR'): return True from app.services.auth_service import AuthService perm_dict = AuthService.get_user_permissions(role, company_name=user_company) all_perms = perm_dict.get('menus', []) + perm_dict.get('elements', []) return 'inbound_purchase' in all_perms or any( p.startswith('inbound_purchase:') for p in all_perms ) # -------------------------------------------------------- # 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() can_view_all = _user_has_purchase_perm() result = PurchaseService.get_purchase_list( page=page, per_page=per_page, requester_id=None if can_view_all 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() @prevent_double_submit(lock_timeout=5) 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 # 图片必填强校验 images = data.get('images') if not images or (isinstance(images, list) and len(images) == 0): return jsonify({'code': 400, 'msg': '请上传采购凭证/物品图片'}), 400 purchase = PurchaseService.create_purchase_request(data, requester_id=user_id) # ★ Fail-Closed: 创建响应剥离价格字段 resp = purchase.to_dict() for k in ('unit_price', 'total_price', 'tax_rate'): resp.pop(k, None) return jsonify({ 'code': 200, 'msg': '创建成功', 'data': resp }), 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/ # -------------------------------------------------------- @purchase_bp.route('/', methods=['GET']) @jwt_required() def get_purchase_detail(purchase_id): """采购申请详情:自己的无需权限,别人的需要 inbound_purchase""" try: purchase = PurchaseService.get_purchase_by_id(purchase_id) if not purchase: return jsonify({'code': 404, 'msg': '采购申请不存在'}), 404 user_id = get_current_user_id() if purchase['requester_id'] != user_id and not _user_has_purchase_perm(): 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//approve # -------------------------------------------------------- @purchase_bp.route('//approve', methods=['PATCH']) @jwt_required() @prevent_double_submit(lock_timeout=5) @permission_required('inbound_purchase:operation') def approve_purchase_request(purchase_id): """审批采购申请:必须有操作权限(inbound_purchase:operation)""" try: user_id = get_current_user_id() 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 '已驳回' # ★ Fail-Closed: 审批响应剥离价格字段 resp = purchase.to_dict() for k in ('unit_price', 'total_price', 'tax_rate'): resp.pop(k, None) return jsonify({'code': 200, 'msg': msg, 'data': resp}), 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 # -------------------------------------------------------- # 7. 已审批且未入库的采购单列表(供库管按单入库使用) # GET /api/v1/purchase/approved-unstocked?page=1&keyword=xxx # -------------------------------------------------------- @purchase_bp.route('/approved-unstocked', methods=['GET']) @jwt_required() @permission_required('inbound_purchase') 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. 物料基础信息搜索(分页) # 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