# inventory-backend/app/api/v1/scrap.py from flask import Blueprint, request, jsonify 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 from app.extensions import db from app.models.transaction import TransScrap from app.models.inbound.buy import StockBuy from app.models.inbound.semi import StockSemi from app.models.inbound.product import StockProduct import traceback import math scrap_bp = Blueprint('scrap', __name__, url_prefix='/scrap') # ============================================================================== # 辅助函数:获取当前用户的完整权限列表(基于角色查询) # ============================================================================== 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 ['scrap_list:*'] perm_dict = AuthService.get_user_permissions(user_role) perms = perm_dict.get('menus', []) + perm_dict.get('elements', []) return perms # -------------------------------------------------------- # 1. 扫码查询库存接口 (关联三个库存表) # GET /api/v1/scrap/scan?barcode=... # -------------------------------------------------------- @scrap_bp.route('/scan', methods=['GET']) @jwt_required() @permission_required('scrap_selection') def scan_barcode(): barcode = request.args.get('barcode') if not barcode: return jsonify({'code': 400, 'msg': '请提供条码'}), 400 try: result = ScrapService.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/scrap # -------------------------------------------------------- @scrap_bp.route('', methods=['POST']) @jwt_required() @audit_log( module='报废管理', action='报废出库', get_target_name_fn=lambda: request.get_json().get('items')[0].get('sku') if request.get_json() and request.get_json().get('items') else None ) def create_scrap(): 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 'scrap_create:operation' not in perms: return jsonify({'code': 403, 'msg': '权限不足'}), 403 data = request.get_json() if not data: return jsonify({'code': 400, 'msg': '无有效数据'}), 400 current_user_name = get_jwt_identity() or 'Unknown' # items 必填 if 'items' not in data or not data['items']: return jsonify({'code': 400, 'msg': '报废商品列表不能为空'}), 400 try: result = ScrapService.process_scrap(data, operator_name=current_user_name) return jsonify({'code': 200, 'msg': '报废成功', 'data': result}) except Exception as e: traceback.print_exc() db.session.rollback() return jsonify({'code': 400, 'msg': str(e)}), 400 # -------------------------------------------------------- # 3. 报废记录查询接口 # GET /api/v1/scrap/records # -------------------------------------------------------- @scrap_bp.route('/records', methods=['GET']) @jwt_required() @permission_required('scrap_list') def get_scrap_records(): page = request.args.get('page', 1, type=int) page_size = request.args.get('pageSize', 50, type=int) sku = request.args.get('sku', '') start_date = request.args.get('start_date', '') end_date = request.args.get('end_date', '') try: result = ScrapService.query_records( page=page, page_size=page_size, sku=sku, start_date=start_date, end_date=end_date ) return jsonify({'code': 200, 'msg': 'success', 'data': result}) except Exception as e: traceback.print_exc() return jsonify({'code': 500, 'msg': str(e)}), 500 # ============================================================ # Service 层:报废核心逻辑 # ============================================================ class ScrapService: @staticmethod def get_stock_by_barcode(barcode): """根据条码查找库存""" if not barcode: return None clean_code = barcode.strip() def get_price(item, table_type): if table_type == 'stock_product': return float(item.sale_price) if item.sale_price else 0 elif table_type == 'stock_buy': return float(item.pre_tax_unit_price) if item.pre_tax_unit_price else 0 return 0 # 1. 查询成品 prod = StockProduct.query.filter( db.or_(StockProduct.barcode == clean_code, StockProduct.sku == clean_code) ).first() if prod: res = ScrapService._format_stock(prod, 'stock_product') res['price'] = get_price(prod, 'stock_product') return res # 2. 查询半成品 semi = StockSemi.query.filter( db.or_(StockSemi.barcode == clean_code, StockSemi.sku == clean_code) ).first() if semi: res = ScrapService._format_stock(semi, 'stock_semi') res['price'] = 0 return res # 3. 查询原材料 buy = StockBuy.query.filter( db.or_(StockBuy.barcode == clean_code, StockBuy.sku == clean_code) ).first() if buy: res = ScrapService._format_stock(buy, 'stock_buy') res['price'] = get_price(buy, 'stock_buy') return res return None @staticmethod def _format_stock(item, table_type): """格式化库存查询结果""" stock_qty = float(item.stock_quantity) if item.stock_quantity else 0 avail_qty = float(item.available_quantity) if item.available_quantity else 0 return { 'id': item.id, 'sku': item.sku, 'barcode': item.barcode, 'name': item.material_base.name if item.material_base else '', 'spec': item.material_base.spec_model if item.material_base else '', 'category': item.material_base.category if item.material_base else '', 'material_type': item.material_base.material_type if item.material_base else '', 'warehouse_loc': item.warehouse_loc or '', 'stock_quantity': stock_qty, 'available_quantity': avail_qty, 'source_table': table_type, } @staticmethod def process_scrap(data, operator_name='System'): """处理报废:扣减库存并记录报废单""" items = data.get('items', []) reason = data.get('reason', '') if not reason: raise ValueError('请填写报废原因') created_records = [] for item in items: stock_id = item.get('id') source_table = item.get('source_table') scrap_qty = float(item.get('quantity', 0)) if not stock_id or not source_table or scrap_qty <= 0: continue # 获取库存记录 stock_record = None if source_table == 'stock_product': stock_record = StockProduct.query.get(stock_id) elif source_table == 'stock_semi': stock_record = StockSemi.query.get(stock_id) elif source_table == 'stock_buy': stock_record = StockBuy.query.get(stock_id) if not stock_record: raise ValueError(f'库存记录不存在: ID={stock_id}') # 检查可用数量 avail_qty = float(stock_record.available_quantity) if stock_record.available_quantity else 0 if avail_qty < scrap_qty: raise ValueError(f"SKU {stock_record.sku} 可用库存不足,当前可用: {avail_qty}") # 计算损失金额 unit_price = 0.0 if source_table == 'stock_product': unit_price = float(stock_record.sale_price) if stock_record.sale_price else 0 elif source_table == 'stock_buy': unit_price = float(stock_record.pre_tax_unit_price) if stock_record.pre_tax_unit_price else 0 total_loss = round(unit_price * scrap_qty, 2) # 扣减库存 stock_record.stock_quantity = float(stock_record.stock_quantity) - scrap_qty stock_record.available_quantity = float(stock_record.available_quantity) - scrap_qty # 创建报废记录 scrap_record = TransScrap( sku=stock_record.sku, source_table=source_table, stock_id=stock_id, quantity=scrap_qty, reason=reason, operator_name=operator_name, approval_status='approved', cost_at_scrap=unit_price, total_loss=total_loss ) db.session.add(scrap_record) created_records.append(scrap_record) db.session.commit() return {'count': len(created_records)} @staticmethod def query_records(page=1, page_size=50, sku='', start_date='', end_date=''): """分页查询报废记录""" query = TransScrap.query if sku: query = query.filter(TransScrap.sku.like(f'%{sku}%')) if start_date: query = query.filter(TransScrap.operation_time >= start_date) if end_date: query = query.filter(TransScrap.operation_time <= end_date + ' 23:59:59') # 按时间倒序 query = query.order_by(TransScrap.operation_time.desc()) total = query.count() records = query.offset((page - 1) * page_size).limit(page_size).all() return { 'list': [r.to_dict() for r in records], 'total': total, 'page': page, 'pageSize': page_size }