from datetime import datetime, timezone from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, func from pydantic import BaseModel from app.core.database import get_db from app.core.deps import get_current_user, require_roles from app.models.maintenance import SparePart, WarehouseTransaction from app.models.user import User router = APIRouter(prefix="/warehouse", tags=["仓库管理"]) # ── Pydantic Schemas ──────────────────────────────────────────────── class PartCreate(BaseModel): name: str code: str | None = None category: str | None = None unit: str | None = None current_stock: int = 0 min_stock: int = 0 location: str | None = None supplier: str | None = None unit_price: float | None = None specs: dict | None = None notes: str | None = None class PartUpdate(BaseModel): name: str | None = None code: str | None = None category: str | None = None unit: str | None = None min_stock: int | None = None location: str | None = None supplier: str | None = None unit_price: float | None = None specs: dict | None = None notes: str | None = None class TransactionCreate(BaseModel): spare_part_id: int type: str # "in" or "out" quantity: int reason: str | None = None related_order_id: int | None = None operator_id: int | None = None notes: str | None = None # ── Helpers ───────────────────────────────────────────────────────── def _part_to_dict(p: SparePart) -> dict: return { "id": p.id, "name": p.name, "code": p.code, "category": p.category, "unit": p.unit, "current_stock": p.current_stock, "min_stock": p.min_stock, "location": p.location, "supplier": p.supplier, "unit_price": p.unit_price, "specs": p.specs, "notes": p.notes, "created_at": str(p.created_at) if p.created_at else None, "updated_at": str(p.updated_at) if p.updated_at else None, } def _transaction_to_dict(t: WarehouseTransaction) -> dict: return { "id": t.id, "spare_part_id": t.spare_part_id, "type": t.type, "quantity": t.quantity, "reason": t.reason, "related_order_id": t.related_order_id, "operator_id": t.operator_id, "notes": t.notes, "created_at": str(t.created_at) if t.created_at else None, } # ── Spare Parts ──────────────────────────────────────────────────── @router.get("/parts") async def list_parts( category: str | None = None, search: str | None = None, low_stock_only: bool = False, page: int = Query(1, ge=1), page_size: int = Query(20, ge=1, le=100), db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user), ): query = select(SparePart) if category: query = query.where(SparePart.category == category) if search: query = query.where(SparePart.name.ilike(f"%{search}%")) if low_stock_only: query = query.where(SparePart.current_stock <= SparePart.min_stock) count_q = select(func.count()).select_from(query.subquery()) total = (await db.execute(count_q)).scalar() query = query.order_by(SparePart.id.desc()).offset((page - 1) * page_size).limit(page_size) result = await db.execute(query) return { "total": total, "items": [_part_to_dict(p) for p in result.scalars().all()], } @router.get("/parts/{part_id}") async def get_part( part_id: int, db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user), ): result = await db.execute(select(SparePart).where(SparePart.id == part_id)) part = result.scalar_one_or_none() if not part: raise HTTPException(status_code=404, detail="备件不存在") return _part_to_dict(part) @router.post("/parts") async def create_part( data: PartCreate, db: AsyncSession = Depends(get_db), user: User = Depends(require_roles("admin", "energy_manager")), ): part = SparePart(**data.model_dump()) db.add(part) await db.flush() return _part_to_dict(part) @router.put("/parts/{part_id}") async def update_part( part_id: int, data: PartUpdate, db: AsyncSession = Depends(get_db), user: User = Depends(require_roles("admin", "energy_manager")), ): result = await db.execute(select(SparePart).where(SparePart.id == part_id)) part = result.scalar_one_or_none() if not part: raise HTTPException(status_code=404, detail="备件不存在") for k, v in data.model_dump(exclude_unset=True).items(): setattr(part, k, v) return _part_to_dict(part) # ── Warehouse Transactions ───────────────────────────────────────── @router.get("/transactions") async def list_transactions( spare_part_id: int | None = None, type: str | None = None, start_date: str | None = None, end_date: str | None = None, page: int = Query(1, ge=1), page_size: int = Query(20, ge=1, le=100), db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user), ): query = select(WarehouseTransaction) if spare_part_id: query = query.where(WarehouseTransaction.spare_part_id == spare_part_id) if type: query = query.where(WarehouseTransaction.type == type) if start_date: query = query.where(WarehouseTransaction.created_at >= datetime.fromisoformat(start_date)) if end_date: query = query.where(WarehouseTransaction.created_at <= datetime.fromisoformat(end_date)) count_q = select(func.count()).select_from(query.subquery()) total = (await db.execute(count_q)).scalar() query = query.order_by(WarehouseTransaction.id.desc()).offset((page - 1) * page_size).limit(page_size) result = await db.execute(query) return { "total": total, "items": [_transaction_to_dict(t) for t in result.scalars().all()], } @router.post("/transactions") async def create_transaction( data: TransactionCreate, db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user), ): # Fetch the spare part result = await db.execute(select(SparePart).where(SparePart.id == data.spare_part_id)) part = result.scalar_one_or_none() if not part: raise HTTPException(status_code=404, detail="备件不存在") # Validate stock for outbound if data.type == "out" and part.current_stock < data.quantity: raise HTTPException(status_code=400, detail=f"库存不足,当前库存: {part.current_stock}") # Update stock if data.type == "in": part.current_stock += data.quantity elif data.type == "out": part.current_stock -= data.quantity else: raise HTTPException(status_code=400, detail="类型必须为 'in' 或 'out'") txn = WarehouseTransaction(**data.model_dump()) if not txn.operator_id: txn.operator_id = user.id db.add(txn) await db.flush() return _transaction_to_dict(txn) # ── Statistics ───────────────────────────────────────────────────── @router.get("/stats") async def warehouse_stats( db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user), ): # Total parts count total_q = select(func.count()).select_from(SparePart) total_parts = (await db.execute(total_q)).scalar() or 0 # Low stock alerts low_q = select(func.count()).select_from(SparePart).where( SparePart.current_stock <= SparePart.min_stock ) low_stock_count = (await db.execute(low_q)).scalar() or 0 # Recent transactions (last 30 days) from datetime import timedelta cutoff = datetime.now(timezone.utc) - timedelta(days=30) recent_q = select(func.count()).select_from(WarehouseTransaction).where( WarehouseTransaction.created_at >= cutoff ) recent_transactions = (await db.execute(recent_q)).scalar() or 0 return { "total_parts": total_parts, "low_stock_count": low_stock_count, "recent_transactions": recent_transactions, }