160 lines
6.3 KiB
Python
160 lines
6.3 KiB
Python
"""
|
|
ERP / 그룹웨어 연동 커넥터
|
|
|
|
기능:
|
|
- 그룹웨어 전자결재 연동 (결재 요청 → GUARDiA SR 생성)
|
|
- ERP HR 데이터 동기화 (사용자 조직 정보)
|
|
- 범용 REST API 커넥터 (설정 기반)
|
|
|
|
엔드포인트:
|
|
POST /api/erp/config — ERP 연동 설정
|
|
GET /api/erp/config — 설정 조회
|
|
POST /api/erp/test — 연결 테스트
|
|
POST /api/erp/webhook — ERP 웹훅 수신 (결재 알림)
|
|
POST /api/erp/sync-users — HR 사용자 동기화
|
|
GET /api/erp/org-chart — 조직도 조회
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from datetime import datetime
|
|
from typing import Optional
|
|
|
|
import httpx
|
|
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Request
|
|
from pydantic import BaseModel, Field
|
|
from sqlalchemy import select
|
|
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, UserRole, SRRequest, SRStatus, ERPConfig # 신규
|
|
|
|
logger = logging.getLogger(__name__)
|
|
router = APIRouter(prefix="/api/erp", tags=["ERP 연동"])
|
|
|
|
|
|
class ERPConfigCreate(BaseModel):
|
|
name: str = Field(..., max_length=100, description="시스템명 (예: 나라장터, 그룹웨어)")
|
|
base_url: str
|
|
api_key: Optional[str] = None
|
|
username: Optional[str] = None
|
|
password: Optional[str] = None
|
|
erp_type: str = Field("generic", description="groupware | nara | hr | generic")
|
|
|
|
|
|
@router.post("/config")
|
|
async def save_erp_config(
|
|
req: ERPConfigCreate, db: AsyncSession = Depends(get_db),
|
|
user: User = Depends(require_admin_role),
|
|
):
|
|
cfg = ERPConfig(
|
|
tenant_id=user.tenant_id, name=req.name, base_url=req.base_url,
|
|
api_key_enc=req.api_key, username=req.username, password_enc=req.password,
|
|
erp_type=req.erp_type, is_active=True, created_at=datetime.utcnow()
|
|
)
|
|
db.add(cfg)
|
|
await db.commit()
|
|
await db.refresh(cfg)
|
|
return {"ok": True, "id": cfg.id}
|
|
|
|
|
|
@router.get("/config")
|
|
async def list_erp_configs(db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)):
|
|
rows = await db.execute(select(ERPConfig).where(ERPConfig.tenant_id == user.tenant_id, ERPConfig.is_active == True))
|
|
cfgs = rows.scalars().all()
|
|
return [{"id": c.id, "name": c.name, "erp_type": c.erp_type, "base_url": c.base_url[:30] + "..."} for c in cfgs]
|
|
|
|
|
|
@router.post("/test/{config_id}")
|
|
async def test_erp(config_id: int, db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)):
|
|
row = await db.execute(select(ERPConfig).where(ERPConfig.id == config_id, ERPConfig.tenant_id == user.tenant_id))
|
|
cfg = row.scalar_one_or_none()
|
|
if not cfg: raise HTTPException(404, "설정 없음")
|
|
try:
|
|
headers = {"Authorization": f"Bearer {cfg.api_key_enc}"} if cfg.api_key_enc else {}
|
|
async with httpx.AsyncClient(timeout=10, verify=False) as c:
|
|
r = await c.get(cfg.base_url, headers=headers)
|
|
return {"ok": r.status_code < 400, "status_code": r.status_code}
|
|
except Exception as e:
|
|
return {"ok": False, "error": str(e)}
|
|
|
|
|
|
@router.post("/webhook")
|
|
async def erp_webhook(request: Request, background_tasks: BackgroundTasks,
|
|
db: AsyncSession = Depends(get_db)):
|
|
"""ERP 웹훅 수신 — 결재 요청 → SR 자동 생성."""
|
|
body = await request.json()
|
|
event_type = body.get("event_type", "")
|
|
title = body.get("title") or body.get("subject") or "ERP 연동 요청"
|
|
description = body.get("description") or body.get("content") or json_to_str(body)
|
|
|
|
if event_type in ("APPROVAL_REQUEST", "WORK_ORDER", "MAINTENANCE_REQUEST"):
|
|
sr = SRRequest(
|
|
title=f"[ERP] {title[:100]}",
|
|
description=description[:1000],
|
|
category="ERP", priority="MEDIUM",
|
|
status=SRStatus.OPEN, created_at=datetime.utcnow(),
|
|
)
|
|
db.add(sr)
|
|
await db.commit()
|
|
return {"ok": True, "sr_id": sr.id}
|
|
|
|
return {"ok": True, "skipped": True}
|
|
|
|
|
|
@router.post("/sync-users/{config_id}")
|
|
async def sync_hr_users(
|
|
config_id: int, db: AsyncSession = Depends(get_db),
|
|
user: User = Depends(require_admin_role),
|
|
):
|
|
"""ERP HR → GUARDiA 사용자 동기화."""
|
|
row = await db.execute(select(ERPConfig).where(ERPConfig.id == config_id, ERPConfig.tenant_id == user.tenant_id))
|
|
cfg = row.scalar_one_or_none()
|
|
if not cfg: raise HTTPException(404, "설정 없음")
|
|
|
|
try:
|
|
headers = {"Authorization": f"Bearer {cfg.api_key_enc}"} if cfg.api_key_enc else {}
|
|
async with httpx.AsyncClient(timeout=15, verify=False) as c:
|
|
r = await c.get(f"{cfg.base_url}/users", headers=headers)
|
|
if r.status_code != 200:
|
|
raise HTTPException(400, "HR API 응답 오류")
|
|
hr_users = r.json().get("users", r.json() if isinstance(r.json(), list) else [])
|
|
except Exception as e:
|
|
raise HTTPException(500, f"HR 연결 실패: {e}")
|
|
|
|
synced = 0
|
|
for hr_user in hr_users:
|
|
email = hr_user.get("email") or hr_user.get("mail")
|
|
name = hr_user.get("name") or hr_user.get("displayName")
|
|
if not email: continue
|
|
existing = await db.execute(select(User).where(User.email == email))
|
|
u = existing.scalar_one_or_none()
|
|
if u:
|
|
if name: u.name = name
|
|
synced += 1
|
|
|
|
await db.commit()
|
|
return {"ok": True, "synced": synced, "total_hr": len(hr_users)}
|
|
|
|
|
|
@router.get("/org-chart/{config_id}")
|
|
async def get_org_chart(
|
|
config_id: int, db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user),
|
|
):
|
|
row = await db.execute(select(ERPConfig).where(ERPConfig.id == config_id, ERPConfig.tenant_id == user.tenant_id))
|
|
cfg = row.scalar_one_or_none()
|
|
if not cfg: raise HTTPException(404, "설정 없음")
|
|
try:
|
|
headers = {"Authorization": f"Bearer {cfg.api_key_enc}"} if cfg.api_key_enc else {}
|
|
async with httpx.AsyncClient(timeout=15, verify=False) as c:
|
|
r = await c.get(f"{cfg.base_url}/org-chart", headers=headers)
|
|
return r.json() if r.status_code == 200 else {"departments": []}
|
|
except Exception:
|
|
return {"departments": []}
|
|
|
|
|
|
def json_to_str(data: dict) -> str:
|
|
import json
|
|
return json.dumps(data, ensure_ascii=False)[:500]
|