172 lines
4.9 KiB
Python
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이 생성되었습니다.",
|
|
}
|