""" 구매 관리 API - 구매 품목 생성/조회 - 구매 수량 계산 - 리비전 비교 """ from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session from sqlalchemy import text from typing import List, Optional import json from ..database import get_db from ..services.purchase_calculator import ( generate_purchase_items_from_materials, save_purchase_items_to_db, calculate_pipe_purchase_quantity, calculate_standard_purchase_quantity ) router = APIRouter(prefix="/purchase", tags=["purchase"]) @router.get("/items/calculate") async def calculate_purchase_items( job_no: str = Query(..., description="Job 번호"), revision: str = Query("Rev.0", description="리비전"), file_id: Optional[int] = Query(None, description="파일 ID (선택사항)"), db: Session = Depends(get_db) ): """ 구매 품목 계산 (실시간) - 자재 데이터로부터 구매 품목 생성 - 수량 계산 (파이프 절단손실 포함) """ try: # 1. 파일 ID 조회 (job_no, revision으로) if not file_id: file_query = text(""" SELECT id FROM files WHERE job_no = :job_no AND revision = :revision AND is_active = TRUE ORDER BY created_at DESC LIMIT 1 """) file_result = db.execute(file_query, {"job_no": job_no, "revision": revision}).fetchone() if not file_result: raise HTTPException(status_code=404, detail=f"Job {job_no} {revision}에 해당하는 파일을 찾을 수 없습니다") file_id = file_result[0] # 2. 구매 품목 생성 purchase_items = generate_purchase_items_from_materials(db, file_id, job_no, revision) return { "success": True, "job_no": job_no, "revision": revision, "file_id": file_id, "items": purchase_items, "total_items": len(purchase_items) } except Exception as e: raise HTTPException(status_code=500, detail=f"구매 품목 계산 실패: {str(e)}") @router.post("/items/save") async def save_purchase_items( job_no: str, revision: str, file_id: int, db: Session = Depends(get_db) ): """ 구매 품목을 데이터베이스에 저장 """ try: # 1. 구매 품목 생성 purchase_items = generate_purchase_items_from_materials(db, file_id, job_no, revision) # 2. 데이터베이스에 저장 saved_ids = save_purchase_items_to_db(db, purchase_items) return { "success": True, "message": f"{len(saved_ids)}개 구매 품목이 저장되었습니다", "saved_items": len(saved_ids), "item_ids": saved_ids } except Exception as e: db.rollback() raise HTTPException(status_code=500, detail=f"구매 품목 저장 실패: {str(e)}") @router.get("/items") async def get_purchase_items( job_no: str = Query(..., description="Job 번호"), revision: str = Query("Rev.0", description="리비전"), category: Optional[str] = Query(None, description="카테고리 필터"), db: Session = Depends(get_db) ): """ 저장된 구매 품목 조회 """ try: query = text(""" SELECT pi.*, COUNT(mpm.material_id) as material_count, SUM(m.quantity) as total_material_quantity FROM purchase_items pi LEFT JOIN material_purchase_mapping mpm ON pi.id = mpm.purchase_item_id LEFT JOIN materials m ON mpm.material_id = m.id WHERE pi.job_no = :job_no AND pi.revision = :revision AND pi.is_active = TRUE """) params = {"job_no": job_no, "revision": revision} if category: query = text(str(query) + " AND pi.category = :category") params["category"] = category query = text(str(query) + """ GROUP BY pi.id ORDER BY pi.category, pi.specification """) result = db.execute(query, params) items = result.fetchall() return { "success": True, "job_no": job_no, "revision": revision, "items": [dict(item) for item in items], "total_items": len(items) } except Exception as e: raise HTTPException(status_code=500, detail=f"구매 품목 조회 실패: {str(e)}") @router.patch("/items/{item_id}") async def update_purchase_item( item_id: int, safety_factor: Optional[float] = None, calculated_qty: Optional[float] = None, minimum_order_qty: Optional[float] = None, db: Session = Depends(get_db) ): """ 구매 품목 수정 (수량 조정) """ try: update_fields = [] params = {"item_id": item_id} if safety_factor is not None: update_fields.append("safety_factor = :safety_factor") params["safety_factor"] = safety_factor if calculated_qty is not None: update_fields.append("calculated_qty = :calculated_qty") params["calculated_qty"] = calculated_qty if minimum_order_qty is not None: update_fields.append("minimum_order_qty = :minimum_order_qty") params["minimum_order_qty"] = minimum_order_qty if not update_fields: raise HTTPException(status_code=400, detail="수정할 필드가 없습니다") update_fields.append("updated_at = CURRENT_TIMESTAMP") query = text(f""" UPDATE purchase_items SET {', '.join(update_fields)} WHERE id = :item_id RETURNING id, calculated_qty, safety_factor """) result = db.execute(query, params) updated_item = result.fetchone() if not updated_item: raise HTTPException(status_code=404, detail="구매 품목을 찾을 수 없습니다") db.commit() return { "success": True, "message": "구매 품목이 수정되었습니다", "item": dict(updated_item) } except Exception as e: db.rollback() raise HTTPException(status_code=500, detail=f"구매 품목 수정 실패: {str(e)}") @router.get("/revision-diff") async def get_revision_diff( job_no: str = Query(..., description="Job 번호"), current_revision: str = Query(..., description="현재 리비전"), previous_revision: str = Query(..., description="이전 리비전"), db: Session = Depends(get_db) ): """ 리비전간 구매 수량 차이 계산 """ try: # 1. 이전 리비전 구매 품목 조회 prev_query = text(""" SELECT item_code, category, specification, calculated_qty, bom_quantity FROM purchase_items WHERE job_no = :job_no AND revision = :prev_revision AND is_active = TRUE """) prev_items = db.execute(prev_query, { "job_no": job_no, "prev_revision": previous_revision }).fetchall() # 2. 현재 리비전 구매 품목 조회 curr_query = text(""" SELECT item_code, category, specification, calculated_qty, bom_quantity FROM purchase_items WHERE job_no = :job_no AND revision = :curr_revision AND is_active = TRUE """) curr_items = db.execute(curr_query, { "job_no": job_no, "curr_revision": current_revision }).fetchall() # 3. 차이 계산 prev_dict = {item.item_code: dict(item) for item in prev_items} curr_dict = {item.item_code: dict(item) for item in curr_items} changes = [] added_items = 0 modified_items = 0 # 현재 리비전에서 추가되거나 변경된 항목 for item_code, curr_item in curr_dict.items(): if item_code not in prev_dict: # 새로 추가된 품목 changes.append({ "item_code": item_code, "change_type": "ADDED", "specification": curr_item["specification"], "previous_qty": 0, "current_qty": curr_item["calculated_qty"], "qty_diff": curr_item["calculated_qty"], "additional_needed": curr_item["calculated_qty"] }) added_items += 1 else: prev_item = prev_dict[item_code] qty_diff = curr_item["calculated_qty"] - prev_item["calculated_qty"] if abs(qty_diff) > 0.001: # 수량 변경 changes.append({ "item_code": item_code, "change_type": "MODIFIED", "specification": curr_item["specification"], "previous_qty": prev_item["calculated_qty"], "current_qty": curr_item["calculated_qty"], "qty_diff": qty_diff, "additional_needed": max(qty_diff, 0) # 증가한 경우만 추가 구매 }) modified_items += 1 # 삭제된 품목 (현재 리비전에 없는 항목) removed_items = 0 for item_code, prev_item in prev_dict.items(): if item_code not in curr_dict: changes.append({ "item_code": item_code, "change_type": "REMOVED", "specification": prev_item["specification"], "previous_qty": prev_item["calculated_qty"], "current_qty": 0, "qty_diff": -prev_item["calculated_qty"], "additional_needed": 0 }) removed_items += 1 # 요약 정보 total_additional_needed = sum( change["additional_needed"] for change in changes if change["additional_needed"] > 0 ) has_changes = len(changes) > 0 summary = f"추가: {added_items}개, 변경: {modified_items}개, 삭제: {removed_items}개" if total_additional_needed > 0: summary += f" (추가 구매 필요: {total_additional_needed:.1f})" return { "success": True, "job_no": job_no, "previous_revision": previous_revision, "current_revision": current_revision, "comparison": { "has_changes": has_changes, "summary": summary, "added_items": added_items, "modified_items": modified_items, "removed_items": removed_items, "total_additional_needed": total_additional_needed, "changes": changes } } except Exception as e: raise HTTPException(status_code=500, detail=f"리비전 비교 실패: {str(e)}") @router.post("/orders/create") async def create_purchase_order( job_no: str, revision: str, items: List[dict], supplier_name: Optional[str] = None, required_date: Optional[str] = None, db: Session = Depends(get_db) ): """ 구매 주문 생성 """ try: from datetime import datetime, date # 1. 주문 번호 생성 order_no = f"PO-{job_no}-{revision}-{datetime.now().strftime('%Y%m%d')}" # 2. 구매 주문 생성 order_query = text(""" INSERT INTO purchase_orders ( order_no, job_no, revision, status, order_date, required_date, supplier_name, created_by ) VALUES ( :order_no, :job_no, :revision, 'DRAFT', CURRENT_DATE, :required_date, :supplier_name, 'system' ) RETURNING id """) order_result = db.execute(order_query, { "order_no": order_no, "job_no": job_no, "revision": revision, "required_date": required_date, "supplier_name": supplier_name }) order_id = order_result.fetchone()[0] # 3. 주문 상세 항목 생성 total_amount = 0 for item in items: item_query = text(""" INSERT INTO purchase_order_items ( purchase_order_id, purchase_item_id, ordered_quantity, required_date ) VALUES ( :order_id, :item_id, :quantity, :required_date ) """) db.execute(item_query, { "order_id": order_id, "item_id": item["purchase_item_id"], "quantity": item["ordered_quantity"], "required_date": required_date }) db.commit() return { "success": True, "message": "구매 주문이 생성되었습니다", "order_no": order_no, "order_id": order_id, "items_count": len(items) } except Exception as e: db.rollback() raise HTTPException(status_code=500, detail=f"구매 주문 생성 실패: {str(e)}") @router.get("/orders") async def get_purchase_orders( job_no: Optional[str] = Query(None, description="Job 번호"), status: Optional[str] = Query(None, description="주문 상태"), db: Session = Depends(get_db) ): """ 구매 주문 목록 조회 """ try: query = text(""" SELECT po.*, COUNT(poi.id) as items_count, SUM(poi.ordered_quantity) as total_quantity FROM purchase_orders po LEFT JOIN purchase_order_items poi ON po.id = poi.purchase_order_id WHERE 1=1 """) params = {} if job_no: query = text(str(query) + " AND po.job_no = :job_no") params["job_no"] = job_no if status: query = text(str(query) + " AND po.status = :status") params["status"] = status query = text(str(query) + """ GROUP BY po.id ORDER BY po.created_at DESC """) result = db.execute(query, params) orders = result.fetchall() return { "success": True, "orders": [dict(order) for order in orders], "total_orders": len(orders) } except Exception as e: raise HTTPException(status_code=500, detail=f"구매 주문 조회 실패: {str(e)}")