from flask import Blueprint, request, jsonify from app.services.outbound_service import OutboundService from flask_jwt_extended import jwt_required, get_jwt_identity, get_jwt from app.utils.decorators import permission_required, audit_log from app.services.auth_service import AuthService import traceback outbound_bp = Blueprint('outbound', __name__, url_prefix='/outbound') # ============================================================================== # 辅助函数:获取当前用户的完整权限列表(基于角色查询) # ============================================================================== def get_current_user_permissions(): """ 返回当前用户拥有的所有权限码列表(包括菜单和元素) 此函数根据角色查询数据库得到权限。 """ from flask_jwt_extended import get_jwt from app.services.auth_service import AuthService claims = get_jwt() user_role = claims.get('role') if not user_role: return [] # 超级管理员返回所有字段权限 (忽略大小写) if user_role.upper() == 'SUPER_ADMIN': return ['outbound_list:*'] perm_dict = AuthService.get_user_permissions(user_role) # 合并菜单和元素权限 perms = perm_dict.get('menus', []) + perm_dict.get('elements', []) return perms def filter_item_by_permissions(item_dict, user_permissions): """ 根据用户权限过滤 item 字典,无权限的字段值置为 None """ # 字段名到权限码的映射(与前端 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', 'subtotal': 'outbound_list:subtotal', } # 如果用户是超级管理员且有 'outbound_list:*',则不过滤 if 'outbound_list:*' in user_permissions: return item_dict for field, perm_code in field_to_perm.items(): if field in item_dict and perm_code not in user_permissions: item_dict[field] = None # 如果 item_dict 中包含 items 列表,递归处理每个子项 if 'items' in item_dict and isinstance(item_dict['items'], list): for sub_item in item_dict['items']: filter_item_by_permissions(sub_item, user_permissions) return item_dict # -------------------------------------------------------- # 1. 扫码查询库存接口 (关联三个库存表) # GET /api/v1/outbound/scan?barcode=... # -------------------------------------------------------- @outbound_bp.route('/scan', methods=['GET']) @jwt_required() @permission_required('outbound_selection') def scan_barcode(): barcode = request.args.get('barcode') if not barcode: return jsonify({'code': 400, 'msg': '请提供条码'}), 400 try: # 调用 Service 层去三个表中查找 (Service已更新,会返回价格) result = OutboundService.get_stock_by_barcode(barcode) if result: return jsonify({ 'code': 200, 'msg': '扫描成功', 'data': result }) else: return jsonify({ 'code': 404, 'msg': '未找到对应的库存记录,请确认条码是否正确' }), 404 except Exception as e: traceback.print_exc() return jsonify({'code': 500, 'msg': f'扫描查询出错: {str(e)}'}), 500 # -------------------------------------------------------- # 2. 提交出库单接口 (批量) # POST /api/v1/outbound # -------------------------------------------------------- @outbound_bp.route('', methods=['POST']) @jwt_required() @audit_log( module='出库管理', action='新增', get_target_name_fn=lambda: request.get_json().get('order_no') if request.get_json() else None ) def create_outbound(): # 权限检查:需要 outbound_create:operation 或 outbound_selection:operation 之一 claims = get_jwt() user_role = claims.get('role') if not user_role: return jsonify({'code': 403, 'msg': '未授权'}), 403 # 超级管理员直接放行 if user_role.upper() != 'SUPER_ADMIN': perm_dict = AuthService.get_user_permissions(user_role) perms = perm_dict.get('menus', []) + perm_dict.get('elements', []) if ('outbound_create:operation' not in perms) and ('outbound_selection:operation' not in perms): return jsonify({'code': 403, 'msg': '权限不足'}), 403 data = request.get_json() if not data: return jsonify({'code': 400, 'msg': '无有效数据'}), 400 # 获取当前登录用户名 (JWT identity) current_user_name = get_jwt_identity() if not current_user_name: current_user_name = 'Unknown' # 获取最终的操作员名称 final_operator = data.get('operator_name') if not final_operator: final_operator = current_user_name # 必填校验 (针对整个单据) # items 必须是列表且不为空,consumer_name 和 signature_path 必填 if 'items' not in data or not data['items']: return jsonify({'code': 400, 'msg': '出库商品列表不能为空'}), 400 if not data.get('consumer_name') or not data.get('signature_path'): return jsonify({'code': 400, 'msg': '领用人及签名信息缺失'}), 400 try: # ★ [修改] 调用批量创建服务 outbound_no = OutboundService.create_outbound_batch(data, operator_name=final_operator) return jsonify({ 'code': 200, 'msg': '出库成功', 'data': {'outbound_no': outbound_no} }) 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/outbound # -------------------------------------------------------- @outbound_bp.route('', methods=['GET']) @jwt_required() @permission_required('outbound_list') def get_outbound_list(): try: page = int(request.args.get('page', 1)) limit = int(request.args.get('limit', 10)) keyword = request.args.get('keyword', '') search_type = request.args.get('search_type', 'all') # 如果前端传了日期范围,可以解析处理,这里暂略 # ★ [修改] 调用分组查询服务,支持搜索类型 result = OutboundService.get_grouped_list(page, limit, keyword, search_type=search_type) # 字段级脱敏 user_permissions = get_current_user_permissions() if result.get('items'): result['items'] = [filter_item_by_permissions(item, user_permissions) for item in result['items']] return jsonify({ 'code': 200, 'msg': '获取成功', 'data': result }) 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//approve # -------------------------------------------------------- @outbound_bp.route('/request//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/ # -------------------------------------------------------- @outbound_bp.route('/request/', 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