CMDB 자동 발견 (4개): - autodiscovery.py: SSH 네트워크 스캔 + CMDB 자동 등록 - snmp_discovery.py: SNMP v2c/v3 장비 자동 발견 - dependency_map.py: 서비스 의존성 자동 매핑 (netstat) - config_inventory.py: 서버 인벤토리 자동 수집 (SSH) NL 쿼리 엔진 (3개): - nlquery.py: Text-to-SQL (SELECT 전용, DML 차단) - op_assistant.py: Multi-turn 대화형 운영 어시스턴트 - query_history.py: 쿼리 이력·즐겨찾기·공유 구성 드리프트 (3개): - drift_detection.py: 골든 구성 vs 실제 비교·SR 자동 생성 - golden_config.py: 내장 CSAP 템플릿 + 버전 관리 - auto_remediation.py: 승인 기반 자동 교정 + 롤백 멀티클라우드 (4개): - multicloud.py: 통합 관제 (NCloud+AWS+KT) - aws_connector.py: AWS SigV4 직접 서명 연동 - cost_optimizer.py: AI 비용 최적화 권고 - cloud_migration.py: On-prem→K-Cloud 체크리스트 공공기관 특화 (6개): - narasajang.py: 나라장터 OpenAPI 연동 - public_api_hub.py: data.go.kr KISA·기상청 허브 - isp_support.py: ISP 수립 지원 + AI 보고서 - network_zone.py: 행정망/인터넷망 분리 관리 - k_cloud.py: 정부 K-Cloud 전환 자동화 - e_procurement.py: 전자조달 계약·검수·납품 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
128 lines
4.7 KiB
Python
128 lines
4.7 KiB
Python
"""
|
|
전자조달 계약·검수·납품 이력 관리
|
|
|
|
나라장터 계약을 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(),
|
|
}
|