""" 리비전 비교 및 변경 처리 서비스 - 자재 비교 로직 (구매된/미구매 자재 구분) - 리비전 액션 결정 (추가구매, 재고이관, 수량변경 등) - GASKET/BOLT 특별 처리 (BOM 원본 수량 기준) """ import logging from typing import Dict, List, Optional, Any, Tuple from decimal import Decimal from sqlalchemy.orm import Session from sqlalchemy import text, and_, or_ from ..models import Material logger = logging.getLogger(__name__) class RevisionComparisonService: """리비전 비교 및 변경 처리 서비스""" def __init__(self, db: Session): self.db = db def compare_materials_by_category( self, current_file_id: int, previous_file_id: int, category: str, session_id: int ) -> Dict[str, Any]: """카테고리별 자재 비교 및 변경사항 기록""" try: logger.info(f"카테고리 {category} 자재 비교 시작 (현재: {current_file_id}, 이전: {previous_file_id})") # 현재 파일의 자재 조회 current_materials = self._get_materials_by_category(current_file_id, category) previous_materials = self._get_materials_by_category(previous_file_id, category) logger.info(f"현재 자재: {len(current_materials)}개, 이전 자재: {len(previous_materials)}개") # 자재 그룹화 (동일 자재 식별) current_grouped = self._group_materials_by_key(current_materials, category) previous_grouped = self._group_materials_by_key(previous_materials, category) # 비교 결과 저장 comparison_results = { "added": [], "removed": [], "changed": [], "unchanged": [] } # 현재 자재 기준으로 비교 for key, current_group in current_grouped.items(): if key in previous_grouped: previous_group = previous_grouped[key] # 수량 비교 (GASKET/BOLT는 BOM 원본 수량 기준) current_qty = self._get_comparison_quantity(current_group, category) previous_qty = self._get_comparison_quantity(previous_group, category) if current_qty != previous_qty: # 수량 변경됨 change_record = self._create_change_record( current_group, previous_group, "quantity_changed", current_qty, previous_qty, category, session_id ) comparison_results["changed"].append(change_record) else: # 수량 동일 unchanged_record = self._create_change_record( current_group, previous_group, "unchanged", current_qty, previous_qty, category, session_id ) comparison_results["unchanged"].append(unchanged_record) else: # 새로 추가된 자재 current_qty = self._get_comparison_quantity(current_group, category) added_record = self._create_change_record( current_group, None, "added", current_qty, 0, category, session_id ) comparison_results["added"].append(added_record) # 제거된 자재 확인 for key, previous_group in previous_grouped.items(): if key not in current_grouped: previous_qty = self._get_comparison_quantity(previous_group, category) removed_record = self._create_change_record( None, previous_group, "removed", 0, previous_qty, category, session_id ) comparison_results["removed"].append(removed_record) # DB에 변경사항 저장 self._save_material_changes(comparison_results, session_id) # 통계 정보 summary = { "category": category, "added_count": len(comparison_results["added"]), "removed_count": len(comparison_results["removed"]), "changed_count": len(comparison_results["changed"]), "unchanged_count": len(comparison_results["unchanged"]), "total_changes": len(comparison_results["added"]) + len(comparison_results["removed"]) + len(comparison_results["changed"]) } logger.info(f"카테고리 {category} 비교 완료: {summary}") return { "summary": summary, "changes": comparison_results } except Exception as e: logger.error(f"카테고리 {category} 자재 비교 실패: {e}") raise def _get_materials_by_category(self, file_id: int, category: str) -> List[Material]: """파일의 특정 카테고리 자재 조회""" return self.db.query(Material).filter( and_( Material.file_id == file_id, Material.classified_category == category, Material.is_active == True ) ).all() def _group_materials_by_key(self, materials: List[Material], category: str) -> Dict[str, Dict]: """자재를 고유 키로 그룹화""" grouped = {} for material in materials: # 카테고리별 고유 키 생성 전략 if category == "PIPE": # PIPE: description + material_grade + main_nom key_parts = [ material.original_description.strip().upper(), material.material_grade or '', material.main_nom or '' ] elif category in ["GASKET", "BOLT"]: # GASKET/BOLT: description + main_nom (집계 공식 적용 전 원본 기준) key_parts = [ material.original_description.strip().upper(), material.main_nom or '' ] else: # 기타: description + drawing + main_nom + red_nom key_parts = [ material.original_description.strip().upper(), material.drawing_name or '', material.main_nom or '', material.red_nom or '' ] key = "|".join(key_parts) if key in grouped: # 동일한 자재가 있으면 수량 합산 grouped[key]['total_quantity'] += float(material.quantity) grouped[key]['materials'].append(material) # 구매 확정 상태 확인 (하나라도 구매 확정되면 전체가 구매 확정) if getattr(material, 'purchase_confirmed', False): grouped[key]['purchase_confirmed'] = True grouped[key]['purchase_confirmed_at'] = getattr(material, 'purchase_confirmed_at', None) else: grouped[key] = { 'key': key, 'representative_material': material, 'materials': [material], 'total_quantity': float(material.quantity), 'purchase_confirmed': getattr(material, 'purchase_confirmed', False), 'purchase_confirmed_at': getattr(material, 'purchase_confirmed_at', None), 'category': category } return grouped def _get_comparison_quantity(self, material_group: Dict, category: str) -> Decimal: """비교용 수량 계산 (GASKET/BOLT는 집계 공식 적용 전 원본 수량)""" if category in ["GASKET", "BOLT"]: # GASKET/BOLT: BOM 원본 수량 기준 (집계 공식 적용 전) # 실제 BOM에서 읽은 원본 수량을 사용 original_quantity = 0 for material in material_group['materials']: # classification_details에서 원본 수량 추출 시도 details = getattr(material, 'classification_details', {}) if isinstance(details, dict) and 'original_quantity' in details: original_quantity += float(details['original_quantity']) else: # 원본 수량 정보가 없으면 현재 수량 사용 original_quantity += float(material.quantity) return Decimal(str(original_quantity)) else: # 기타 카테고리: 현재 수량 사용 return Decimal(str(material_group['total_quantity'])) def _create_change_record( self, current_group: Optional[Dict], previous_group: Optional[Dict], change_type: str, current_qty: Decimal, previous_qty: Decimal, category: str, session_id: int ) -> Dict[str, Any]: """변경 기록 생성""" # 대표 자재 정보 if current_group: material = current_group['representative_material'] material_id = material.id description = material.original_description purchase_status = 'purchased' if current_group['purchase_confirmed'] else 'not_purchased' purchase_confirmed_at = current_group.get('purchase_confirmed_at') else: material = previous_group['representative_material'] material_id = None # 제거된 자재는 현재 material_id가 없음 description = material.original_description purchase_status = 'purchased' if previous_group['purchase_confirmed'] else 'not_purchased' purchase_confirmed_at = previous_group.get('purchase_confirmed_at') # 리비전 액션 결정 revision_action = self._determine_revision_action( change_type, current_qty, previous_qty, purchase_status, category ) return { "session_id": session_id, "material_id": material_id, "previous_material_id": material.id if previous_group else None, "material_description": description, "category": category, "change_type": change_type, "current_quantity": float(current_qty), "previous_quantity": float(previous_qty), "quantity_difference": float(current_qty - previous_qty), "purchase_status": purchase_status, "purchase_confirmed_at": purchase_confirmed_at, "revision_action": revision_action } def _determine_revision_action( self, change_type: str, current_qty: Decimal, previous_qty: Decimal, purchase_status: str, category: str ) -> str: """리비전 액션 결정 로직""" if change_type == "added": return "new_material" elif change_type == "removed": if purchase_status == "purchased": return "inventory_transfer" # 구매된 자재 → 재고 이관 else: return "purchase_cancel" # 미구매 자재 → 구매 취소 elif change_type == "quantity_changed": quantity_diff = current_qty - previous_qty if purchase_status == "purchased": if quantity_diff > 0: return "additional_purchase" # 구매된 자재 수량 증가 → 추가 구매 else: return "inventory_transfer" # 구매된 자재 수량 감소 → 재고 이관 else: return "quantity_update" # 미구매 자재 → 수량 업데이트 else: return "maintain" # 변경 없음 def _save_material_changes(self, comparison_results: Dict, session_id: int): """변경사항을 DB에 저장""" try: all_changes = [] for change_type, changes in comparison_results.items(): all_changes.extend(changes) if not all_changes: return # 배치 삽입 insert_query = """ INSERT INTO revision_material_changes ( session_id, material_id, previous_material_id, material_description, category, change_type, current_quantity, previous_quantity, quantity_difference, purchase_status, purchase_confirmed_at, revision_action ) VALUES ( :session_id, :material_id, :previous_material_id, :material_description, :category, :change_type, :current_quantity, :previous_quantity, :quantity_difference, :purchase_status, :purchase_confirmed_at, :revision_action ) """ self.db.execute(text(insert_query), all_changes) self.db.commit() logger.info(f"변경사항 {len(all_changes)}건 저장 완료 (세션: {session_id})") except Exception as e: self.db.rollback() logger.error(f"변경사항 저장 실패: {e}") raise def get_session_changes(self, session_id: int, category: str = None) -> List[Dict[str, Any]]: """세션의 변경사항 조회""" try: query = """ SELECT id, material_id, material_description, category, change_type, current_quantity, previous_quantity, quantity_difference, purchase_status, revision_action, action_status, processed_by, processed_at, processing_notes FROM revision_material_changes WHERE session_id = :session_id """ params = {"session_id": session_id} if category: query += " AND category = :category" params["category"] = category query += " ORDER BY category, material_description" changes = self.db.execute(text(query), params).fetchall() return [dict(change._mapping) for change in changes] except Exception as e: logger.error(f"세션 변경사항 조회 실패: {e}") raise def process_revision_action( self, change_id: int, action: str, username: str, notes: str = None ) -> Dict[str, Any]: """리비전 액션 처리""" try: # 변경사항 조회 change = self.db.execute(text(""" SELECT * FROM revision_material_changes WHERE id = :change_id """), {"change_id": change_id}).fetchone() if not change: raise ValueError(f"변경사항을 찾을 수 없습니다: {change_id}") result = {"success": False, "message": ""} # 액션별 처리 if action == "additional_purchase": result = self._process_additional_purchase(change, username, notes) elif action == "inventory_transfer": result = self._process_inventory_transfer(change, username, notes) elif action == "purchase_cancel": result = self._process_purchase_cancel(change, username, notes) elif action == "quantity_update": result = self._process_quantity_update(change, username, notes) else: result = {"success": True, "message": "처리 완료"} # 처리 상태 업데이트 status = "completed" if result["success"] else "failed" self.db.execute(text(""" UPDATE revision_material_changes SET action_status = :status, processed_by = :username, processed_at = CURRENT_TIMESTAMP, processing_notes = :notes WHERE id = :change_id """), { "change_id": change_id, "status": status, "username": username, "notes": notes or result["message"] }) # 액션 로그 기록 self.db.execute(text(""" INSERT INTO revision_action_logs ( session_id, revision_change_id, action_type, action_description, executed_by, result, result_message ) VALUES ( :session_id, :change_id, :action, :description, :username, :result, :message ) """), { "session_id": change.session_id, "change_id": change_id, "action": action, "description": f"{change.material_description} - {action}", "username": username, "result": "success" if result["success"] else "failed", "message": result["message"] }) self.db.commit() return result except Exception as e: self.db.rollback() logger.error(f"리비전 액션 처리 실패: {e}") raise def _process_additional_purchase(self, change, username: str, notes: str) -> Dict[str, Any]: """추가 구매 처리""" # 구매 요청 생성 로직 구현 return {"success": True, "message": f"추가 구매 요청 생성: {change.quantity_difference}개"} def _process_inventory_transfer(self, change, username: str, notes: str) -> Dict[str, Any]: """재고 이관 처리""" # 재고 이관 로직 구현 try: self.db.execute(text(""" INSERT INTO inventory_transfers ( revision_change_id, material_description, category, quantity, unit, transferred_by, storage_notes ) VALUES ( :change_id, :description, :category, :quantity, 'EA', :username, :notes ) """), { "change_id": change.id, "description": change.material_description, "category": change.category, "quantity": abs(change.quantity_difference), "username": username, "notes": notes }) return {"success": True, "message": f"재고 이관 완료: {abs(change.quantity_difference)}개"} except Exception as e: return {"success": False, "message": f"재고 이관 실패: {str(e)}"} def _process_purchase_cancel(self, change, username: str, notes: str) -> Dict[str, Any]: """구매 취소 처리""" return {"success": True, "message": "구매 취소 완료"} def _process_quantity_update(self, change, username: str, notes: str) -> Dict[str, Any]: """수량 업데이트 처리""" return {"success": True, "message": f"수량 업데이트 완료: {change.current_quantity}개"}