# inventory-backend/app/api/v1/stock/adjustment.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 from app.extensions import db from app.models.stock.adjustment import StockAdjustment from app.models.base import MaterialBase from app.models.inbound.buy import StockBuy from app.models.inbound.semi import StockSemi from app.models.inbound.product import StockProduct from app.models.inbound.stocktake import StocktakeDraft from datetime import datetime import random import string adjustment_bp = Blueprint('adjustment', __name__, url_prefix='/stock/adjustment') def generate_order_no(): """生成单号 ADJ-YYYYMMDD-XXXX""" today = datetime.now().strftime('%Y%m%d') suffix = ''.join(random.choices(string.digits, k=4)) return f'ADJ-{today}-{suffix}' def get_stock_model(source_table): """根据source_table获取对应的库存模型""" if source_table == 'stock_buy': return StockBuy elif source_table == 'stock_semi': return StockSemi elif source_table == 'stock_product': return StockProduct return None # -------------------------------------------------------- # 1. 获取调整单列表 # GET /api/v1/stock/adjustment/list # -------------------------------------------------------- @adjustment_bp.route('/list', methods=['GET']) @jwt_required() @permission_required('stock_adjustment:list') def get_list(): page = int(request.args.get('page', 1)) limit = int(request.args.get('limit', 10)) keyword = request.args.get('keyword', '') adjust_type = request.args.get('adjust_type', '') status = request.args.get('status', '') query = StockAdjustment.query if keyword: query = query.filter( db.or_( StockAdjustment.order_no.ilike(f'%{keyword}%'), StockAdjustment.sku.ilike(f'%{keyword}%'), StockAdjustment.material_name.ilike(f'%{keyword}%') ) ) if adjust_type: query = query.filter(StockAdjustment.adjust_type == adjust_type) if status: query = query.filter(StockAdjustment.status == status) # 按创建时间降序 query = query.order_by(StockAdjustment.create_time.desc()) pagination = query.paginate(page=page, per_page=limit, error_out=False) return jsonify({ 'code': 200, 'data': { 'items': [item.to_dict() for item in pagination.items], 'total': pagination.total, 'page': page, 'limit': limit } }) # -------------------------------------------------------- # 2. 创建调整单 # POST /api/v1/stock/adjustment/create # -------------------------------------------------------- @adjustment_bp.route('/create', methods=['POST']) @jwt_required() @permission_required('stock_adjustment:operation') def create(): data = request.get_json() if not data: return jsonify({'code': 400, 'msg': '请求参数不能为空'}), 400 # 必填字段验证 required_fields = ['source_table', 'stock_id', 'adjust_type', 'adjust_quantity', 'reason'] for field in required_fields: if field not in data or not data.get(field): return jsonify({'code': 400, 'msg': f'{field} 为必填项'}), 400 source_table = data['source_table'] stock_id = int(data['stock_id']) adjust_type = data['adjust_type'] # 'profit' or 'loss' adjust_quantity = float(data['adjust_quantity']) reason = data['reason'] operator = get_jwt_identity() or 'system' # 获取库存记录 StockModel = get_stock_model(source_table) if not StockModel: return jsonify({'code': 400, 'msg': '无效的库存类型'}), 400 stock = StockModel.query.get(stock_id) if not stock: return jsonify({'code': 404, 'msg': '库存记录不存在'}), 404 # 获取物料信息 base_id = getattr(stock, 'base_id', None) material = MaterialBase.query.get(base_id) if base_id else None # 计算库存变动 if adjust_type == 'profit': # 盘盈:增加库存 new_stock_qty = float(stock.stock_quantity or 0) + adjust_quantity new_avail_qty = float(stock.available_quantity or 0) + adjust_quantity elif adjust_type == 'loss': # 盘亏:减少库存 new_stock_qty = float(stock.stock_quantity or 0) - adjust_quantity new_avail_qty = float(stock.available_quantity or 0) - adjust_quantity if new_stock_qty < 0 or new_avail_qty < 0: return jsonify({'code': 400, 'msg': '库存不足,无法盘亏'}), 400 else: return jsonify({'code': 400, 'msg': '无效的调整类型'}), 400 try: # 创建调整单 adjustment = StockAdjustment( order_no=generate_order_no(), base_id=base_id, stock_id=stock_id, source_table=source_table, sku=getattr(stock, 'sku', None) or getattr(stock, 'SKU', None), material_name=material.name if material else getattr(stock, 'sku', '未知'), spec_model=getattr(material, 'spec_model', None) if material else None, warehouse_location=getattr(stock, 'warehouse_location', None), adjust_type=adjust_type, adjust_quantity=adjust_quantity, reason=reason, status='completed', operator=operator ) db.session.add(adjustment) # 更新库存 stock.stock_quantity = new_stock_qty stock.available_quantity = new_avail_qty db.session.commit() return jsonify({ 'code': 200, 'msg': '调整成功', 'data': adjustment.to_dict() }) except Exception as e: db.session.rollback() return jsonify({'code': 500, 'msg': f'调整失败: {str(e)}'}), 500 # -------------------------------------------------------- # 3. 获取库存列表(用于选择物料) # GET /api/v1/stock/adjustment/stocks # -------------------------------------------------------- @adjustment_bp.route('/stocks', methods=['GET']) @jwt_required() @permission_required('stock_adjustment:list') def get_stocks(): """获取可用于调整的库存列表""" source_table = request.args.get('source_table', 'stock_buy') keyword = request.args.get('keyword', '') page = int(request.args.get('page', 1)) limit = int(request.args.get('limit', 20)) StockModel = get_stock_model(source_table) if not StockModel: return jsonify({'code': 400, 'msg': '无效的库存类型'}), 400 query = StockModel.query.filter(StockModel.stock_quantity > 0) if keyword: query = query.filter( db.or_( StockModel.sku.ilike(f'%{keyword}%'), StockModel.barcode.ilike(f'%{keyword}%') ) ) pagination = query.paginate(page=page, per_page=limit, error_out=False) items = [] for stock in pagination.items: base_id = getattr(stock, 'base_id', None) material = MaterialBase.query.get(base_id) if base_id else None items.append({ 'stock_id': stock.id, 'source_table': source_table, 'sku': getattr(stock, 'sku', None) or getattr(stock, 'SKU', None), 'material_name': material.name if material else getattr(stock, 'sku', '未知'), 'spec_model': getattr(material, 'spec_model', None) if material else None, 'stock_quantity': float(stock.stock_quantity or 0), 'available_quantity': float(stock.available_quantity or 0), 'warehouse_location': getattr(stock, 'warehouse_location', None), }) return jsonify({ 'code': 200, 'data': { 'items': items, 'total': pagination.total, 'page': page, 'limit': limit } }) # -------------------------------------------------------- # 5. 一键引入盘点差异 # POST /api/v1/stock/adjustment/import-from-stocktake # -------------------------------------------------------- @adjustment_bp.route('/import-from-stocktake', methods=['POST']) @jwt_required() @permission_required('stock_adjustment:operation') def import_from_stocktake(): """从盘点差异记录导入为盘盈盘亏单""" identity = get_jwt_identity() operator = identity.get('username', 'system') if isinstance(identity, dict) else str(identity) try: # 查询所有有差异的盘点记录 drafts = StocktakeDraft.query.filter(StocktakeDraft.diff_qty != 0).all() if not drafts: return jsonify({'code': 200, 'msg': '暂无盘点差异记录', 'data': {'count': 0}}) count = 0 for draft in drafts: # 判断盘盈/盘亏 diff = float(draft.diff_qty or 0) if diff == 0: continue adjust_type = 'profit' if diff > 0 else 'loss' adjust_quantity = abs(diff) # 获取物料基础信息 base_id = None sku = '' warehouse_location = '' # 根据source_table获取对应的库存记录 stock_model = get_stock_model(draft.source_table) if stock_model and draft.stock_id: stock = stock_model.query.get(draft.stock_id) if stock: base_id = getattr(stock, 'base_id', None) sku = getattr(stock, 'sku', None) or getattr(stock, 'SKU', '') warehouse_location = getattr(stock, 'warehouse_location', '') # 生成调整单号 order_no = generate_order_no() # 使用备注或默认原因 reason = draft.remark if draft.remark else '盘点差异自动生成' # 创建调整单 adjustment = StockAdjustment( order_no=order_no, base_id=base_id, stock_id=draft.stock_id, source_table=draft.source_table, sku=sku, warehouse_location=warehouse_location, adjust_type=adjust_type, adjust_quantity=adjust_quantity, reason=reason, status='pending', operator=operator ) db.session.add(adjustment) count += 1 db.session.commit() return jsonify({'code': 200, 'msg': '导入成功', 'data': {'count': count}}) except Exception as e: db.session.rollback() return jsonify({'code': 500, 'msg': f'导入失败: {str(e)}'}), 500