guardia-itsm/routers/inventory.py

172 lines
4.9 KiB
Python

"""
부품 재고 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이 생성되었습니다.",
}