""" 구매 추적 및 관리 API 엑셀 내보내기 이력 및 구매 상태 관리 """ from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy import text from sqlalchemy.orm import Session from typing import Optional, List, Dict, Any from datetime import datetime, date import json from ..database import get_db from ..auth.jwt_service import get_current_user from ..utils.logger import logger router = APIRouter(prefix="/purchase", tags=["Purchase Tracking"]) @router.post("/export-history") async def create_export_history( file_id: int, job_no: Optional[str] = None, export_type: str = "full", category: Optional[str] = None, material_ids: List[int] = [], filters_applied: Optional[Dict] = None, current_user: dict = Depends(get_current_user), db: Session = Depends(get_db) ): """ 엑셀 내보내기 이력 생성 및 자재 추적 """ try: # 내보내기 이력 생성 insert_history = text(""" INSERT INTO excel_export_history ( file_id, job_no, exported_by, export_type, category, material_count, filters_applied ) VALUES ( :file_id, :job_no, :exported_by, :export_type, :category, :material_count, :filters_applied ) RETURNING export_id """) result = db.execute(insert_history, { "file_id": file_id, "job_no": job_no, "exported_by": current_user.get("user_id"), "export_type": export_type, "category": category, "material_count": len(material_ids), "filters_applied": json.dumps(filters_applied) if filters_applied else None }) export_id = result.fetchone().export_id # 내보낸 자재들 기록 if material_ids: for material_id in material_ids: insert_material = text(""" INSERT INTO exported_materials ( export_id, material_id, purchase_status ) VALUES ( :export_id, :material_id, 'pending' ) """) db.execute(insert_material, { "export_id": export_id, "material_id": material_id }) db.commit() logger.info(f"Export history created: {export_id} with {len(material_ids)} materials") return { "success": True, "export_id": export_id, "message": f"{len(material_ids)}개 자재의 내보내기 이력이 저장되었습니다" } except Exception as e: db.rollback() logger.error(f"Failed to create export history: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"내보내기 이력 생성 실패: {str(e)}" ) @router.get("/export-history") async def get_export_history( file_id: Optional[int] = None, job_no: Optional[str] = None, limit: int = 50, current_user: dict = Depends(get_current_user), db: Session = Depends(get_db) ): """ 엑셀 내보내기 이력 조회 """ try: query = text(""" SELECT eeh.export_id, eeh.file_id, eeh.job_no, eeh.export_date, eeh.export_type, eeh.category, eeh.material_count, u.name as exported_by_name, f.original_filename, COUNT(DISTINCT em.material_id) as actual_material_count, COUNT(DISTINCT CASE WHEN em.purchase_status = 'pending' THEN em.material_id END) as pending_count, COUNT(DISTINCT CASE WHEN em.purchase_status = 'requested' THEN em.material_id END) as requested_count, COUNT(DISTINCT CASE WHEN em.purchase_status = 'ordered' THEN em.material_id END) as ordered_count, COUNT(DISTINCT CASE WHEN em.purchase_status = 'received' THEN em.material_id END) as received_count FROM excel_export_history eeh LEFT JOIN users u ON eeh.exported_by = u.user_id LEFT JOIN files f ON eeh.file_id = f.id LEFT JOIN exported_materials em ON eeh.export_id = em.export_id WHERE 1=1 AND (:file_id IS NULL OR eeh.file_id = :file_id) AND (:job_no IS NULL OR eeh.job_no = :job_no) GROUP BY eeh.export_id, eeh.file_id, eeh.job_no, eeh.export_date, eeh.export_type, eeh.category, eeh.material_count, u.name, f.original_filename ORDER BY eeh.export_date DESC LIMIT :limit """) results = db.execute(query, { "file_id": file_id, "job_no": job_no, "limit": limit }).fetchall() history = [] for row in results: history.append({ "export_id": row.export_id, "file_id": row.file_id, "job_no": row.job_no, "export_date": row.export_date.isoformat() if row.export_date else None, "export_type": row.export_type, "category": row.category, "material_count": row.material_count, "exported_by": row.exported_by_name, "file_name": row.original_filename, "status_summary": { "total": row.actual_material_count, "pending": row.pending_count, "requested": row.requested_count, "ordered": row.ordered_count, "received": row.received_count } }) return { "success": True, "history": history, "count": len(history) } except Exception as e: logger.error(f"Failed to get export history: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"내보내기 이력 조회 실패: {str(e)}" ) @router.get("/materials/status") async def get_materials_by_status( status: Optional[str] = None, export_id: Optional[int] = None, file_id: Optional[int] = None, current_user: dict = Depends(get_current_user), db: Session = Depends(get_db) ): """ 구매 상태별 자재 목록 조회 """ try: query = text(""" SELECT em.id as exported_material_id, em.material_id, m.original_description, m.classified_category, m.quantity, m.unit, em.purchase_status, em.purchase_request_no, em.purchase_order_no, em.vendor_name, em.expected_date, em.quantity_ordered, em.quantity_received, em.unit_price, em.total_price, em.notes, em.updated_at, eeh.export_date, f.original_filename as file_name, j.job_no, j.job_name FROM exported_materials em JOIN materials m ON em.material_id = m.id JOIN excel_export_history eeh ON em.export_id = eeh.export_id LEFT JOIN files f ON eeh.file_id = f.id LEFT JOIN jobs j ON eeh.job_no = j.job_no WHERE 1=1 AND (:status IS NULL OR em.purchase_status = :status) AND (:export_id IS NULL OR em.export_id = :export_id) AND (:file_id IS NULL OR eeh.file_id = :file_id) ORDER BY em.updated_at DESC """) results = db.execute(query, { "status": status, "export_id": export_id, "file_id": file_id }).fetchall() materials = [] for row in results: materials.append({ "exported_material_id": row.exported_material_id, "material_id": row.material_id, "description": row.original_description, "category": row.classified_category, "quantity": row.quantity, "unit": row.unit, "purchase_status": row.purchase_status, "purchase_request_no": row.purchase_request_no, "purchase_order_no": row.purchase_order_no, "vendor_name": row.vendor_name, "expected_date": row.expected_date.isoformat() if row.expected_date else None, "quantity_ordered": row.quantity_ordered, "quantity_received": row.quantity_received, "unit_price": float(row.unit_price) if row.unit_price else None, "total_price": float(row.total_price) if row.total_price else None, "notes": row.notes, "updated_at": row.updated_at.isoformat() if row.updated_at else None, "export_date": row.export_date.isoformat() if row.export_date else None, "file_name": row.file_name, "job_no": row.job_no, "job_name": row.job_name }) return { "success": True, "materials": materials, "count": len(materials) } except Exception as e: logger.error(f"Failed to get materials by status: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"구매 상태별 자재 조회 실패: {str(e)}" ) @router.patch("/materials/{exported_material_id}/status") async def update_purchase_status( exported_material_id: int, new_status: str, purchase_request_no: Optional[str] = None, purchase_order_no: Optional[str] = None, vendor_name: Optional[str] = None, expected_date: Optional[date] = None, quantity_ordered: Optional[int] = None, quantity_received: Optional[int] = None, unit_price: Optional[float] = None, notes: Optional[str] = None, current_user: dict = Depends(get_current_user), db: Session = Depends(get_db) ): """ 자재 구매 상태 업데이트 """ try: # 현재 상태 조회 get_current = text(""" SELECT purchase_status, material_id FROM exported_materials WHERE id = :id """) current = db.execute(get_current, {"id": exported_material_id}).fetchone() if not current: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="해당 자재를 찾을 수 없습니다" ) # 상태 업데이트 update_query = text(""" UPDATE exported_materials SET purchase_status = :new_status, purchase_request_no = COALESCE(:pr_no, purchase_request_no), purchase_order_no = COALESCE(:po_no, purchase_order_no), vendor_name = COALESCE(:vendor, vendor_name), expected_date = COALESCE(:expected_date, expected_date), quantity_ordered = COALESCE(:qty_ordered, quantity_ordered), quantity_received = COALESCE(:qty_received, quantity_received), unit_price = COALESCE(:unit_price, unit_price), total_price = CASE WHEN :unit_price IS NOT NULL AND :qty_ordered IS NOT NULL THEN :unit_price * :qty_ordered WHEN :unit_price IS NOT NULL AND quantity_ordered IS NOT NULL THEN :unit_price * quantity_ordered ELSE total_price END, notes = COALESCE(:notes, notes), updated_by = :updated_by, requested_date = CASE WHEN :new_status = 'requested' THEN CURRENT_TIMESTAMP ELSE requested_date END, ordered_date = CASE WHEN :new_status = 'ordered' THEN CURRENT_TIMESTAMP ELSE ordered_date END, received_date = CASE WHEN :new_status = 'received' THEN CURRENT_TIMESTAMP ELSE received_date END WHERE id = :id """) db.execute(update_query, { "id": exported_material_id, "new_status": new_status, "pr_no": purchase_request_no, "po_no": purchase_order_no, "vendor": vendor_name, "expected_date": expected_date, "qty_ordered": quantity_ordered, "qty_received": quantity_received, "unit_price": unit_price, "notes": notes, "updated_by": current_user.get("user_id") }) # 이력 기록 insert_history = text(""" INSERT INTO purchase_status_history ( exported_material_id, material_id, previous_status, new_status, changed_by, reason ) VALUES ( :em_id, :material_id, :prev_status, :new_status, :changed_by, :reason ) """) db.execute(insert_history, { "em_id": exported_material_id, "material_id": current.material_id, "prev_status": current.purchase_status, "new_status": new_status, "changed_by": current_user.get("user_id"), "reason": notes }) db.commit() logger.info(f"Purchase status updated: {exported_material_id} from {current.purchase_status} to {new_status}") return { "success": True, "message": f"구매 상태가 {new_status}로 변경되었습니다" } except HTTPException: raise except Exception as e: db.rollback() logger.error(f"Failed to update purchase status: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"구매 상태 업데이트 실패: {str(e)}" ) @router.get("/status-summary") async def get_status_summary( file_id: Optional[int] = None, job_no: Optional[str] = None, current_user: dict = Depends(get_current_user), db: Session = Depends(get_db) ): """ 구매 상태 요약 통계 """ try: query = text(""" SELECT em.purchase_status, COUNT(DISTINCT em.material_id) as material_count, SUM(em.quantity_exported) as total_quantity, SUM(em.total_price) as total_amount, COUNT(DISTINCT em.export_id) as export_count FROM exported_materials em JOIN excel_export_history eeh ON em.export_id = eeh.export_id WHERE 1=1 AND (:file_id IS NULL OR eeh.file_id = :file_id) AND (:job_no IS NULL OR eeh.job_no = :job_no) GROUP BY em.purchase_status """) results = db.execute(query, { "file_id": file_id, "job_no": job_no }).fetchall() summary = {} total_materials = 0 total_amount = 0 for row in results: summary[row.purchase_status] = { "material_count": row.material_count, "total_quantity": row.total_quantity, "total_amount": float(row.total_amount) if row.total_amount else 0, "export_count": row.export_count } total_materials += row.material_count if row.total_amount: total_amount += float(row.total_amount) # 기본 상태들 추가 (없는 경우 0으로) for status in ['pending', 'requested', 'ordered', 'received', 'cancelled']: if status not in summary: summary[status] = { "material_count": 0, "total_quantity": 0, "total_amount": 0, "export_count": 0 } return { "success": True, "summary": summary, "total_materials": total_materials, "total_amount": total_amount } except Exception as e: logger.error(f"Failed to get status summary: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"구매 상태 요약 조회 실패: {str(e)}" )