""" 전자조달 계약·검수·납품 이력 관리 나라장터 계약을 ITSM SR과 연계하여 IT 자산 도입 프로세스를 통합 관리. 엔드포인트: GET /api/eprocure/contracts — 계약 목록 POST /api/eprocure/contracts — 계약 등록 PUT /api/eprocure/contracts/{id} — 계약 수정 POST /api/eprocure/inspect/{id} — 납품 검수 처리 GET /api/eprocure/stats — 조달 통계 GET /api/eprocure/expiring — 계약 만료 예정 """ from __future__ import annotations from datetime import date, datetime, timedelta from typing import Optional from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel from sqlalchemy import select, and_, desc from sqlalchemy.ext.asyncio import AsyncSession from core.auth import get_current_user, require_admin_role from database import get_db from models import User, ProcurementRecord router = APIRouter(prefix="/api/eprocure", tags=["전자조달 관리"]) class ContractCreate(BaseModel): contract_no: str contract_name: str supplier: str amount: int start_date: str end_date: str category: str = "IT장비" linked_sr_ids: list[int] = [] note: Optional[str] = None class InspectRequest(BaseModel): inspected_by: str note: Optional[str] = None result: str = "PASS" # PASS | FAIL | PARTIAL @router.post("/contracts") async def create_contract(req: ContractCreate, db: AsyncSession = Depends(get_db), user: User = Depends(require_admin_role)): record = ProcurementRecord( tenant_id=user.tenant_id, contract_no=req.contract_no, contract_name=req.contract_name, supplier=req.supplier, amount=req.amount, start_date=date.fromisoformat(req.start_date), end_date=date.fromisoformat(req.end_date), category=req.category, linked_sr_ids=req.linked_sr_ids, status="ACTIVE", created_at=datetime.utcnow(), ) db.add(record); await db.commit(); await db.refresh(record) return {"ok": True, "id": record.id} @router.get("/contracts") async def list_contracts(db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)): rows = await db.execute( select(ProcurementRecord).where(ProcurementRecord.tenant_id == user.tenant_id) .order_by(desc(ProcurementRecord.end_date)).limit(100) ) records = rows.scalars().all() return [ {"id": r.id, "no": r.contract_no, "name": r.contract_name, "supplier": r.supplier, "amount": r.amount, "period": f"{r.start_date} ~ {r.end_date}", "status": r.status, "category": r.category, "linked_sr": r.linked_sr_ids or []} for r in records ] @router.post("/inspect/{contract_id}") async def inspect_delivery(contract_id: int, req: InspectRequest, db: AsyncSession = Depends(get_db), user: User = Depends(require_admin_role)): row = await db.execute(select(ProcurementRecord).where(ProcurementRecord.id == contract_id, ProcurementRecord.tenant_id == user.tenant_id)) record = row.scalar_one_or_none() if not record: raise HTTPException(404) record.inspection_result = req.result record.inspection_date = date.today() record.inspection_by = req.inspected_by if req.result == "PASS": record.status = "COMPLETED" await db.commit() return {"ok": True, "result": req.result} @router.get("/expiring") async def expiring_contracts(days: int = 30, db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)): cutoff = date.today() + timedelta(days=days) rows = await db.execute( select(ProcurementRecord).where( ProcurementRecord.tenant_id == user.tenant_id, ProcurementRecord.end_date <= cutoff, ProcurementRecord.end_date >= date.today(), ProcurementRecord.status == "ACTIVE", ).order_by(ProcurementRecord.end_date) ) records = rows.scalars().all() return [ {"id": r.id, "name": r.contract_name, "end_date": r.end_date, "days_left": (r.end_date - date.today()).days, "amount": r.amount} for r in records ] @router.get("/stats") async def procurement_stats(db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)): from sqlalchemy import func rows = await db.execute(select(ProcurementRecord).where(ProcurementRecord.tenant_id == user.tenant_id)) records = rows.scalars().all() total_amount = sum(r.amount or 0 for r in records) active = sum(1 for r in records if r.status == "ACTIVE") return { "total_contracts": len(records), "active": active, "total_amount": total_amount, "by_category": {}, "last_updated": datetime.utcnow(), }