# inventory-backend/app/api/v1/inbound/semi.py from flask import Blueprint, request, jsonify from app.services.inbound.semi_service import SemiInboundService from app.utils.decorators import permission_required, audit_log import traceback # === 这一行非常关键,绝对不能丢!=== inbound_semi_bp = Blueprint('stock_semi', __name__) 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') user_company = claims.get('company_name', '') if not user_role: return [] from app.utils.constants import UserRole if str(user_role).strip().upper() == UserRole.SUPER_ADMIN: return ['inbound_semi:*'] perm_dict = AuthService.get_user_permissions(user_role, company_name=user_company) return perm_dict.get('menus', []) + perm_dict.get('elements', []) def filter_item_by_permissions(item_dict, user_permissions): """严格 Default Deny 字段过滤 (see app/utils/field_permissions.py)""" from app.utils.field_permissions import apply_strict_rbac return apply_strict_rbac(item_dict, 'StockSemi', user_permissions) @inbound_semi_bp.route('/search-base', methods=['GET']) @permission_required('inbound_semi') def search_base(): try: keyword = request.args.get('keyword', '') page = request.args.get('page', 1, type=int) result = SemiInboundService.search_base_material(keyword, page) return jsonify({"code": 200, "msg": "success", "data": result}) except Exception as e: traceback.print_exc() return jsonify({"code": 500, "msg": str(e)}), 500 @inbound_semi_bp.route('/search-bom', methods=['GET']) @permission_required('inbound_semi') def search_bom(): try: keyword = request.args.get('keyword', '') parent_spec = request.args.get('parent_spec', None) data = SemiInboundService.search_bom_options(keyword, parent_spec=parent_spec) return jsonify({"code": 200, "msg": "success", "data": data}) except Exception as e: traceback.print_exc() return jsonify({"code": 500, "msg": str(e)}), 500 @inbound_semi_bp.route('/list', methods=['GET']) @permission_required('inbound_semi') def get_list(): try: page = request.args.get('page', 1, type=int) limit = request.args.get('pageSize', 15, type=int) keyword = request.args.get('keyword', '') sku = request.args.get('sku', '') search_field = request.args.get('searchField', 'all') statuses_str = request.args.get('statuses', '') statuses = statuses_str.split(',') if statuses_str else [] company = request.args.get('company', '') category = request.args.get('category', '') material_type = request.args.get('material_type', '') order_by_column = request.args.get('orderByColumn', '') is_asc = request.args.get('isAsc', '') advanced_filters_str = request.args.get('advancedFilters', '') # 准备额外筛选字典 extra_filters = {} if company: extra_filters['company'] = company if category: extra_filters['category'] = category if material_type: extra_filters['material_type'] = material_type if order_by_column: extra_filters['order_by_column'] = order_by_column if is_asc: extra_filters['is_asc'] = is_asc if advanced_filters_str: try: import json advanced_filters = json.loads(advanced_filters_str) extra_filters['advanced_filters'] = advanced_filters except Exception: extra_filters['advanced_filters'] = [] # 调用服务,传入所有参数 result = SemiInboundService.get_list( page, limit, keyword, sku, search_field, statuses, **extra_filters ) 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": "success", "data": result}) except Exception as e: traceback.print_exc() return jsonify({"code": 500, "msg": str(e)}), 500 @inbound_semi_bp.route('/submit', methods=['POST']) @permission_required('inbound_semi:operation') @audit_log( module='半成品入库', action='新增', get_target_name_fn=lambda: request.get_json().get('material_name') if request.get_json() else None ) def submit(): try: data = request.get_json() if not data: return jsonify({"code": 400, "msg": "No data"}), 400 # 库位必填校验(安全兜底) location = data.get('warehouse_location', '').strip() if not location: return jsonify({"code": 400, "msg": "入库失败:库位为必填项,不能为空!"}), 400 user_permissions = get_current_user_permissions() if 'inbound_semi:*' not in user_permissions: for field in ('raw_material_cost', 'manual_cost', 'unit_total_cost', 'total_price'): perm_code = f'inbound_semi:{field}' if field in data and perm_code not in user_permissions: data.pop(field, None) new_stock = SemiInboundService.handle_inbound(data) # ★ Fail-Closed: 入库成功响应剥离成本字段 resp = new_stock.to_dict() for k in ('raw_material_cost', 'manual_cost', 'unit_total_cost', 'total_price'): resp.pop(k, None) return jsonify({"code": 200, "msg": "入库成功", "data": resp}) except Exception as e: traceback.print_exc() return jsonify({"code": 500, "msg": str(e)}), 500 @inbound_semi_bp.route('/', methods=['PUT']) @permission_required('inbound_semi:operation') @audit_log( module='半成品入库', action='修改', get_target_id_fn=lambda: request.view_args.get('id'), get_target_name_fn=lambda: request.get_json().get('material_name') if request.get_json() else None ) def update_semi(id): try: data = request.get_json() user_permissions = get_current_user_permissions() if 'inbound_semi:*' not in user_permissions: for field in ('raw_material_cost', 'manual_cost', 'unit_total_cost', 'total_price'): perm_code = f'inbound_semi:{field}' if field in data and perm_code not in user_permissions: data.pop(field, None) SemiInboundService.update_inbound(id, data) return jsonify({"code": 200, "msg": "更新成功"}) except Exception as e: traceback.print_exc() return jsonify({"code": 500, "msg": str(e)}), 500 @inbound_semi_bp.route('/', methods=['DELETE']) @permission_required('inbound_semi:operation') @audit_log( module='半成品入库', action='删除', get_target_id_fn=lambda: request.view_args.get('id') ) def delete_semi(id): try: material_name = SemiInboundService.delete_inbound(id) return jsonify({"code": 200, "msg": "删除成功", "material_name": material_name}) except ValueError as ve: return jsonify({"code": 400, "msg": str(ve)}) except Exception as e: import traceback traceback.print_exc() return jsonify({"code": 500, "msg": f"服务器内部错误详情: {str(e)}"}), 500 @inbound_semi_bp.route('//history', methods=['GET']) @permission_required('inbound_semi') def get_history(id): try: data = SemiInboundService.get_outbound_history(id) return jsonify({"code": 200, "msg": "success", "data": data}) except Exception as e: traceback.print_exc() return jsonify({"code": 500, "msg": str(e)}), 500 @inbound_semi_bp.route('/suggestions/users', methods=['GET']) @permission_required('inbound_semi') def get_user_suggestions(): keyword = request.args.get('keyword', '') data = SemiInboundService.search_system_users(keyword) return jsonify({"code": 200, "msg": "success", "data": data}) @inbound_semi_bp.route('/options', methods=['GET']) @permission_required('inbound_semi') def get_options(): try: data = SemiInboundService.get_filter_options() return jsonify({"code": 200, "msg": "success", "data": data}) except Exception as e: return jsonify({"code": 500, "msg": str(e)}), 500 @inbound_semi_bp.route('/suggestions/managers', methods=['GET']) @permission_required('inbound_semi') def get_manager_history(): keyword = request.args.get('keyword', '') try: data = SemiInboundService.get_history_managers(keyword) return jsonify({"code": 200, "msg": "success", "data": data}) except Exception as e: traceback.print_exc() return jsonify({"code": 500, "msg": str(e)}), 500 # ------------------------------------------------------------------ # 9. BOM 原材料成本自动核算 (新增) # ------------------------------------------------------------------ @inbound_semi_bp.route('/calculate-bom-cost', methods=['GET']) @permission_required('inbound_semi') def calculate_bom_cost(): try: bom_code = request.args.get('bom_code') bom_version = request.args.get('bom_version') if not bom_code or not bom_version: return jsonify({"code": 400, "msg": "bom_code和bom_version不能为空"}), 400 cost = SemiInboundService.calculate_bom_cost(bom_code, bom_version) return jsonify({"code": 200, "msg": "success", "data": cost}) except Exception as e: traceback.print_exc() return jsonify({"code": 500, "msg": str(e)}), 500 # ------------------------------------------------------------------ # 获取最近一次入库的库位(跨表查询) # ------------------------------------------------------------------ @inbound_semi_bp.route('/last-location', methods=['GET']) @permission_required('inbound_semi') def get_last_location(): """ 获取指定物料最近一次入库的库位 查询顺序:半成品入库 -> 采购入库 -> 成品入库,返回最新入库的库位 """ base_id = request.args.get('base_id', type=int) if not base_id: return jsonify({"code": 400, "msg": "base_id required"}), 400 location = SemiInboundService.get_last_location_by_base_id(base_id) return jsonify({"code": 200, "msg": "success", "data": {"location": location}})