""" 강화된 리비전 관리 서비스 구매 상태 기반 리비전 비교 및 처리 """ from sqlalchemy.orm import Session from sqlalchemy import text, and_, or_ from typing import List, Dict, Any, Optional, Tuple from decimal import Decimal import hashlib from datetime import datetime from ..models import Material, File from ..utils.logger import get_logger from .database_service import DatabaseService logger = get_logger(__name__) class EnhancedRevisionService: """강화된 리비전 관리 서비스""" def __init__(self, db: Session): self.db = db self.db_service = DatabaseService(db) def compare_revisions_with_purchase_status( self, job_no: str, current_file_id: int, previous_file_id: Optional[int] = None ) -> Dict[str, Any]: """ 구매 상태를 고려한 리비전 비교 Args: job_no: 작업 번호 current_file_id: 현재 파일 ID previous_file_id: 이전 파일 ID (None이면 자동 탐지) Returns: 비교 결과 딕셔너리 """ if not previous_file_id: previous_file_id = self._get_previous_file_id(job_no, current_file_id) if not previous_file_id: return self._handle_first_revision(current_file_id) # 이전 리비전 자재 조회 (구매 상태 포함) previous_materials = self._get_materials_with_purchase_status(previous_file_id) # 현재 리비전 자재 조회 current_materials = self._get_materials_with_purchase_status(current_file_id) # 자재별 비교 수행 comparison_result = self._perform_detailed_comparison( previous_materials, current_materials, job_no ) return comparison_result def _get_materials_with_purchase_status(self, file_id: int) -> Dict[str, Dict]: """파일의 자재를 구매 상태와 함께 조회""" query = """ SELECT m.id, m.original_description, m.classified_category, m.material_grade, m.schedule, m.size_spec, m.main_nom, m.red_nom, m.quantity, m.unit, m.length, m.drawing_name, m.line_no, m.purchase_confirmed, m.confirmed_quantity, m.purchase_status, m.material_hash, m.revision_status, -- PIPE 자재 특별 키 생성 CASE WHEN m.classified_category = 'PIPE' THEN CONCAT(m.drawing_name, '|', m.line_no, '|', COALESCE(m.length, 0)) ELSE m.material_hash END as comparison_key FROM materials m WHERE m.file_id = :file_id ORDER BY m.line_number """ result = self.db_service.execute_query(query, {"file_id": file_id}) materials = {} for row in result.fetchall(): row_dict = dict(row._mapping) comparison_key = row_dict['comparison_key'] # PIPE 자재의 경우 도면-라인넘버별로 길이 합산 if row_dict['classified_category'] == 'PIPE': if comparison_key in materials: # 기존 자재에 길이 합산 materials[comparison_key]['quantity'] += row_dict['quantity'] materials[comparison_key]['total_length'] = ( materials[comparison_key].get('total_length', 0) + (row_dict['length'] or 0) * row_dict['quantity'] ) else: row_dict['total_length'] = (row_dict['length'] or 0) * row_dict['quantity'] materials[comparison_key] = row_dict else: materials[comparison_key] = row_dict return materials def _perform_detailed_comparison( self, previous_materials: Dict[str, Dict], current_materials: Dict[str, Dict], job_no: str ) -> Dict[str, Any]: """상세 비교 수행""" result = { "job_no": job_no, "comparison_date": datetime.now().isoformat(), "summary": { "total_previous": len(previous_materials), "total_current": len(current_materials), "purchased_maintained": 0, "purchased_increased": 0, "purchased_decreased": 0, "unpurchased_maintained": 0, "unpurchased_increased": 0, "unpurchased_decreased": 0, "new_materials": 0, "deleted_materials": 0 }, "changes": { "purchased_materials": { "maintained": [], "additional_purchase_needed": [], "excess_inventory": [] }, "unpurchased_materials": { "maintained": [], "quantity_updated": [], "quantity_reduced": [] }, "new_materials": [], "deleted_materials": [] } } # 이전 자재 기준으로 비교 for key, prev_material in previous_materials.items(): if key in current_materials: curr_material = current_materials[key] change_info = self._analyze_material_change(prev_material, curr_material) if prev_material.get('purchase_confirmed', False): # 구매 완료된 자재 처리 self._process_purchased_material_change(result, change_info, prev_material, curr_material) else: # 구매 미완료 자재 처리 self._process_unpurchased_material_change(result, change_info, prev_material, curr_material) else: # 삭제된 자재 result["changes"]["deleted_materials"].append({ "material": prev_material, "reason": "removed_from_new_revision" }) result["summary"]["deleted_materials"] += 1 # 신규 자재 처리 for key, curr_material in current_materials.items(): if key not in previous_materials: result["changes"]["new_materials"].append({ "material": curr_material, "action": "new_material_added" }) result["summary"]["new_materials"] += 1 return result def _analyze_material_change(self, prev_material: Dict, curr_material: Dict) -> Dict: """자재 변경 사항 분석""" prev_qty = float(prev_material.get('quantity', 0)) curr_qty = float(curr_material.get('quantity', 0)) # PIPE 자재의 경우 총 길이로 비교 if prev_material.get('classified_category') == 'PIPE': prev_total = prev_material.get('total_length', 0) curr_total = curr_material.get('total_length', 0) return { "quantity_change": curr_qty - prev_qty, "length_change": curr_total - prev_total, "change_type": "length_based" if abs(curr_total - prev_total) > 0.01 else "no_change" } else: return { "quantity_change": curr_qty - prev_qty, "change_type": "increased" if curr_qty > prev_qty else "decreased" if curr_qty < prev_qty else "no_change" } def _process_purchased_material_change( self, result: Dict, change_info: Dict, prev_material: Dict, curr_material: Dict ): """구매 완료 자재 변경 처리""" if change_info["change_type"] == "no_change": result["changes"]["purchased_materials"]["maintained"].append({ "material": curr_material, "action": "maintain_inventory" }) result["summary"]["purchased_maintained"] += 1 elif change_info["change_type"] == "increased": additional_qty = change_info["quantity_change"] result["changes"]["purchased_materials"]["additional_purchase_needed"].append({ "material": curr_material, "previous_quantity": prev_material.get('quantity'), "current_quantity": curr_material.get('quantity'), "additional_needed": additional_qty, "action": "additional_purchase_required" }) result["summary"]["purchased_increased"] += 1 else: # decreased excess_qty = abs(change_info["quantity_change"]) result["changes"]["purchased_materials"]["excess_inventory"].append({ "material": curr_material, "previous_quantity": prev_material.get('quantity'), "current_quantity": curr_material.get('quantity'), "excess_quantity": excess_qty, "action": "mark_as_excess_inventory" }) result["summary"]["purchased_decreased"] += 1 def _process_unpurchased_material_change( self, result: Dict, change_info: Dict, prev_material: Dict, curr_material: Dict ): """구매 미완료 자재 변경 처리""" if change_info["change_type"] == "no_change": result["changes"]["unpurchased_materials"]["maintained"].append({ "material": curr_material, "action": "maintain_purchase_pending" }) result["summary"]["unpurchased_maintained"] += 1 elif change_info["change_type"] == "increased": result["changes"]["unpurchased_materials"]["quantity_updated"].append({ "material": curr_material, "previous_quantity": prev_material.get('quantity'), "current_quantity": curr_material.get('quantity'), "quantity_change": change_info["quantity_change"], "action": "update_purchase_quantity" }) result["summary"]["unpurchased_increased"] += 1 else: # decreased result["changes"]["unpurchased_materials"]["quantity_reduced"].append({ "material": curr_material, "previous_quantity": prev_material.get('quantity'), "current_quantity": curr_material.get('quantity'), "quantity_change": change_info["quantity_change"], "action": "reduce_purchase_quantity" }) result["summary"]["unpurchased_decreased"] += 1 def _get_previous_file_id(self, job_no: str, current_file_id: int) -> Optional[int]: """이전 파일 ID 자동 탐지""" query = """ SELECT id, revision FROM files WHERE job_no = :job_no AND id != :current_file_id AND is_active = true ORDER BY upload_date DESC LIMIT 1 """ result = self.db_service.execute_query(query, { "job_no": job_no, "current_file_id": current_file_id }) row = result.fetchone() return row.id if row else None def _handle_first_revision(self, current_file_id: int) -> Dict[str, Any]: """첫 번째 리비전 처리""" materials = self._get_materials_with_purchase_status(current_file_id) return { "job_no": None, "comparison_date": datetime.now().isoformat(), "is_first_revision": True, "summary": { "total_materials": len(materials), "all_new": True }, "changes": { "new_materials": [{"material": mat, "action": "first_revision"} for mat in materials.values()] } } def apply_revision_changes(self, comparison_result: Dict, current_file_id: int) -> Dict[str, Any]: """리비전 변경사항을 DB에 적용""" try: # 각 변경사항별로 DB 업데이트 updates_applied = { "purchased_materials": 0, "unpurchased_materials": 0, "new_materials": 0, "deleted_materials": 0 } changes = comparison_result.get("changes", {}) # 구매 완료 자재 처리 purchased = changes.get("purchased_materials", {}) for category, materials in purchased.items(): for item in materials: material = item["material"] action = item["action"] if action == "additional_purchase_required": self._mark_additional_purchase_needed(material, item) elif action == "mark_as_excess_inventory": self._mark_excess_inventory(material, item) updates_applied["purchased_materials"] += 1 # 구매 미완료 자재 처리 unpurchased = changes.get("unpurchased_materials", {}) for category, materials in unpurchased.items(): for item in materials: material = item["material"] action = item["action"] if action == "update_purchase_quantity": self._update_purchase_quantity(material, item) elif action == "reduce_purchase_quantity": self._reduce_purchase_quantity(material, item) updates_applied["unpurchased_materials"] += 1 # 신규 자재 처리 for item in changes.get("new_materials", []): self._mark_new_material(item["material"]) updates_applied["new_materials"] += 1 # 삭제된 자재 처리 for item in changes.get("deleted_materials", []): self._mark_deleted_material(item["material"]) updates_applied["deleted_materials"] += 1 self.db.commit() return { "success": True, "updates_applied": updates_applied, "message": "리비전 변경사항이 성공적으로 적용되었습니다." } except Exception as e: self.db.rollback() logger.error(f"Failed to apply revision changes: {e}") return { "success": False, "error": str(e), "message": "리비전 변경사항 적용 중 오류가 발생했습니다." } def _mark_additional_purchase_needed(self, material: Dict, change_info: Dict): """추가 구매 필요 표시""" update_query = """ UPDATE materials SET revision_status = 'additional_purchase_needed', notes = CONCAT(COALESCE(notes, ''), '\n추가 구매 필요: ', :additional_qty, ' ', unit) WHERE id = :material_id """ self.db_service.execute_query(update_query, { "material_id": material["id"], "additional_qty": change_info["additional_needed"] }) def _mark_excess_inventory(self, material: Dict, change_info: Dict): """잉여 재고 표시""" update_query = """ UPDATE materials SET revision_status = 'excess_inventory', notes = CONCAT(COALESCE(notes, ''), '\n잉여 재고: ', :excess_qty, ' ', unit) WHERE id = :material_id """ self.db_service.execute_query(update_query, { "material_id": material["id"], "excess_qty": change_info["excess_quantity"] }) def _update_purchase_quantity(self, material: Dict, change_info: Dict): """구매 수량 업데이트""" update_query = """ UPDATE materials SET quantity = :new_quantity, revision_status = 'quantity_updated', notes = CONCAT(COALESCE(notes, ''), '\n수량 변경: ', :prev_qty, ' → ', :new_qty) WHERE id = :material_id """ self.db_service.execute_query(update_query, { "material_id": material["id"], "new_quantity": change_info["current_quantity"], "prev_qty": change_info["previous_quantity"], "new_qty": change_info["current_quantity"] }) def _reduce_purchase_quantity(self, material: Dict, change_info: Dict): """구매 수량 감소""" if change_info["current_quantity"] <= 0: # 수량이 0 이하면 삭제 표시 update_query = """ UPDATE materials SET revision_status = 'deleted', is_active = false, notes = CONCAT(COALESCE(notes, ''), '\n리비전에서 삭제됨') WHERE id = :material_id """ else: # 수량만 감소 update_query = """ UPDATE materials SET quantity = :new_quantity, revision_status = 'quantity_reduced', notes = CONCAT(COALESCE(notes, ''), '\n수량 감소: ', :prev_qty, ' → ', :new_qty) WHERE id = :material_id """ self.db_service.execute_query(update_query, { "material_id": material["id"], "new_quantity": change_info.get("current_quantity", 0), "prev_qty": change_info.get("previous_quantity", 0), "new_qty": change_info.get("current_quantity", 0) }) def _mark_new_material(self, material: Dict): """신규 자재 표시""" update_query = """ UPDATE materials SET revision_status = 'new_in_revision' WHERE id = :material_id """ self.db_service.execute_query(update_query, { "material_id": material["id"] }) def _mark_deleted_material(self, material: Dict): """삭제된 자재 표시 (이전 리비전에서)""" update_query = """ UPDATE materials SET revision_status = 'removed_in_new_revision', notes = CONCAT(COALESCE(notes, ''), '\n신규 리비전에서 제거됨') WHERE id = :material_id """ self.db_service.execute_query(update_query, { "material_id": material["id"] })