""" 부품 재고 API (모바일 기능 #62). GET /api/inventory/parts — 부품 목록 (tenant 필터) GET /api/inventory/parts/{id} — 부품 상세 POST /api/inventory/parts — 부품 등록 POST /api/inventory/parts/{id}/request — 부품 요청 → SR 자동 생성 """ from __future__ import annotations from datetime import datetime from typing import List, Optional from uuid import uuid4 from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel, ConfigDict from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from core.auth import get_current_user from database import get_db from models import Institution, InventoryPart, SRRequest, SRStatus, SRType, User router = APIRouter(prefix="/api/inventory", tags=["Inventory"]) def _tenant_of(user: User) -> str: return user.inst_code or f"user:{user.username}" class PartCreate(BaseModel): name: str model: Optional[str] = None quantity: int = 0 min_quantity: int = 1 location: Optional[str] = None class PartOut(BaseModel): model_config = ConfigDict(from_attributes=True) id: int name: str model: Optional[str] quantity: int min_quantity: int location: Optional[str] low_stock: bool = False class PartRequest(BaseModel): quantity: int = 1 reason: Optional[str] = None target_server: Optional[str] = None def _to_out(p: InventoryPart) -> dict: return { "id": p.id, "name": p.name, "model": p.model, "quantity": p.quantity, "min_quantity": p.min_quantity, "location": p.location, "low_stock": (p.quantity or 0) <= (p.min_quantity or 0), } @router.get("/parts", response_model=List[PartOut]) async def list_parts( low_stock_only: bool = False, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): """내 테넌트의 부품 목록.""" q = select(InventoryPart).where( InventoryPart.tenant_id == _tenant_of(current_user) ).order_by(InventoryPart.name) rows = (await db.execute(q)).scalars().all() out = [_to_out(p) for p in rows] if low_stock_only: out = [p for p in out if p["low_stock"]] return out @router.post("/parts", response_model=PartOut, status_code=201) async def create_part( payload: PartCreate, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): part = InventoryPart( tenant_id=_tenant_of(current_user), name=payload.name, model=payload.model, quantity=payload.quantity, min_quantity=payload.min_quantity, location=payload.location, ) db.add(part) await db.commit() await db.refresh(part) return _to_out(part) async def _get_owned_part(part_id: int, db: AsyncSession, user: User) -> InventoryPart: part = await db.get(InventoryPart, part_id) if not part or part.tenant_id != _tenant_of(user): raise HTTPException(404, "부품을 찾을 수 없습니다.") return part @router.get("/parts/{part_id}", response_model=PartOut) async def get_part( part_id: int, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): part = await _get_owned_part(part_id, db, current_user) return _to_out(part) @router.post("/parts/{part_id}/request", status_code=201) async def request_part( part_id: int, payload: PartRequest, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): """부품 요청 → SR 자동 생성.""" part = await _get_owned_part(part_id, db, current_user) if payload.quantity < 1: raise HTTPException(422, "요청 수량은 1 이상이어야 합니다.") # 소속 기관 id 매핑 (있으면) inst_id = None if current_user.inst_code: inst = (await db.execute( select(Institution).where(Institution.inst_code == current_user.inst_code) )).scalars().first() if inst: inst_id = inst.id sr_id = f"SR-{datetime.now().strftime('%Y%m%d')}-{str(uuid4())[:6].upper()}" desc = ( f"부품 요청\n" f"- 부품명: {part.name}\n" f"- 모델: {part.model or '-'}\n" f"- 요청수량: {payload.quantity}\n" f"- 보관위치: {part.location or '-'}\n" f"- 사유: {payload.reason or '-'}" ) sr = SRRequest( sr_id=sr_id, inst_id=inst_id, sr_type=SRType.OTHER, title=f"[부품요청] {part.name} x{payload.quantity}", description=desc, status=SRStatus.RECEIVED, requested_by=current_user.username, target_server=payload.target_server, ) db.add(sr) await db.commit() return { "sr_id": sr_id, "part_id": part.id, "part_name": part.name, "requested_quantity": payload.quantity, "message": "부품 요청 SR이 생성되었습니다.", }