""" 리비전 자재 처리 전용 서비스 구매 상태별 자재 처리 로직 """ from sqlalchemy.orm import Session from sqlalchemy import text from typing import List, Dict, Any, Optional from decimal import Decimal 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 RevisionMaterialService: """리비전 자재 처리 전용 서비스""" def __init__(self, db: Session): self.db = db self.db_service = DatabaseService(db) def process_material_by_purchase_status( self, prev_material: Dict, curr_material: Dict, category: str ) -> Dict[str, Any]: """ 구매 상태별 자재 처리 Args: prev_material: 이전 리비전 자재 curr_material: 현재 리비전 자재 category: 자재 카테고리 Returns: 처리 결과 """ # 수량 변화 계산 quantity_change = self._calculate_quantity_change(prev_material, curr_material, category) # 구매 완료 자재 처리 if prev_material.get('purchase_confirmed', False): return self._process_purchased_material(prev_material, curr_material, quantity_change, category) else: # 구매 미완료 자재 처리 return self._process_unpurchased_material(prev_material, curr_material, quantity_change, category) def process_new_material(self, material: Dict, category: str) -> Dict[str, Any]: """신규 자재 처리""" return { 'material_id': material['id'], 'category': category, 'action': 'new_material', 'status': 'needs_purchase', 'quantity': material.get('quantity', 0), 'description': material.get('original_description', ''), 'processing_note': '신규 자재 - 구매 필요', 'ui_display': { 'show_in_revision_page': True, 'highlight_color': 'green', 'action_required': '구매 신청', 'badge': 'NEW' } } def process_removed_material(self, material: Dict, category: str) -> Dict[str, Any]: """제거된 자재 처리""" if material.get('purchase_confirmed', False): # 구매 완료된 자재가 제거됨 → 재고로 분류 return { 'material_id': material['id'], 'category': category, 'action': 'move_to_inventory', 'status': 'excess_inventory', 'quantity': material.get('quantity', 0), 'description': material.get('original_description', ''), 'processing_note': '구매 완료 후 리비전에서 제거됨 - 재고 보관', 'ui_display': { 'show_in_revision_page': True, 'highlight_color': 'orange', 'action_required': '재고 관리', 'badge': 'INVENTORY' } } else: # 구매 미완료 자재가 제거됨 → 완전 삭제 return { 'material_id': material['id'], 'category': category, 'action': 'delete', 'status': 'deleted', 'quantity': material.get('quantity', 0), 'description': material.get('original_description', ''), 'processing_note': '리비전에서 제거됨 - 구매 불필요', 'ui_display': { 'show_in_revision_page': False, # 삭제된 자재는 표시 안함 'highlight_color': 'red', 'action_required': '삭제 완료', 'badge': 'DELETED' } } def _process_purchased_material( self, prev_material: Dict, curr_material: Dict, quantity_change: Dict, category: str ) -> Dict[str, Any]: """구매 완료 자재 처리""" if quantity_change['change_type'] == 'no_change': # 변동 없음 → 구매 완료 상태 유지 return { 'material_id': curr_material['id'], 'category': category, 'action': 'maintain_status', 'status': 'purchased_completed', 'quantity': curr_material.get('quantity', 0), 'description': curr_material.get('original_description', ''), 'processing_note': '구매 완료 - 변동 없음', 'ui_display': { 'show_in_revision_page': False, # 변동 없는 구매완료 자재는 숨김 'highlight_color': 'gray', 'action_required': '관리 불필요', 'badge': 'COMPLETED' } } elif quantity_change['change_type'] == 'decreased': # 수량 감소 → 재고 자재로 분류 excess_quantity = abs(quantity_change['quantity_change']) return { 'material_id': curr_material['id'], 'category': category, 'action': 'partial_inventory', 'status': 'excess_inventory', 'quantity': curr_material.get('quantity', 0), 'excess_quantity': excess_quantity, 'purchased_quantity': prev_material.get('quantity', 0), 'description': curr_material.get('original_description', ''), 'processing_note': f'구매 완료 후 수량 감소 - 잉여 {excess_quantity}개 재고 보관', 'ui_display': { 'show_in_revision_page': True, 'highlight_color': 'orange', 'action_required': '잉여 재고 관리', 'badge': 'EXCESS' } } else: # increased # 수량 부족 → 추가 구매 필요 additional_needed = quantity_change['quantity_change'] return { 'material_id': curr_material['id'], 'category': category, 'action': 'additional_purchase', 'status': 'needs_additional_purchase', 'quantity': curr_material.get('quantity', 0), 'additional_needed': additional_needed, 'already_purchased': prev_material.get('quantity', 0), 'description': curr_material.get('original_description', ''), 'processing_note': f'구매 완료 후 수량 부족 - 추가 {additional_needed}개 구매 필요', 'ui_display': { 'show_in_revision_page': True, 'highlight_color': 'red', 'action_required': '추가 구매 신청', 'badge': 'ADDITIONAL' } } def _process_unpurchased_material( self, prev_material: Dict, curr_material: Dict, quantity_change: Dict, category: str ) -> Dict[str, Any]: """구매 미완료 자재 처리""" if quantity_change['change_type'] == 'no_change': # 수량 동일 → 구매 관리 계속 return { 'material_id': curr_material['id'], 'category': category, 'action': 'continue_purchase', 'status': 'pending_purchase', 'quantity': curr_material.get('quantity', 0), 'description': curr_material.get('original_description', ''), 'processing_note': '수량 변동 없음 - 구매 진행', 'ui_display': { 'show_in_revision_page': True, 'highlight_color': 'blue', 'action_required': '구매 신청', 'badge': 'PENDING' } } else: # 수량 변경 → 수량 업데이트 후 구매 관리 return { 'material_id': curr_material['id'], 'category': category, 'action': 'update_quantity', 'status': 'quantity_updated', 'quantity': curr_material.get('quantity', 0), 'previous_quantity': prev_material.get('quantity', 0), 'quantity_change': quantity_change['quantity_change'], 'description': curr_material.get('original_description', ''), 'processing_note': f'수량 변경: {prev_material.get("quantity", 0)} → {curr_material.get("quantity", 0)}', 'ui_display': { 'show_in_revision_page': True, 'highlight_color': 'yellow', 'action_required': '수량 확인 후 구매 신청', 'badge': 'UPDATED' } } def _calculate_quantity_change( self, prev_material: Dict, curr_material: Dict, category: str ) -> Dict[str, Any]: """수량 변화 계산""" # GASKET, BOLT는 규칙 적용 전 수량으로 비교 if category in ['GASKET', 'BOLT']: prev_qty = float(prev_material.get('original_quantity', prev_material.get('quantity', 0))) curr_qty = float(curr_material.get('original_quantity', curr_material.get('quantity', 0))) else: prev_qty = float(prev_material.get('quantity', 0)) curr_qty = float(curr_material.get('quantity', 0)) quantity_change = curr_qty - prev_qty if abs(quantity_change) < 0.001: # 부동소수점 오차 고려 change_type = 'no_change' elif quantity_change > 0: change_type = 'increased' else: change_type = 'decreased' return { 'previous_quantity': prev_qty, 'current_quantity': curr_qty, 'quantity_change': quantity_change, 'change_type': change_type, 'is_gasket_bolt': category in ['GASKET', 'BOLT'] } def get_category_materials_for_revision( self, file_id: int, category: str, include_processing_info: bool = True ) -> List[Dict[str, Any]]: """ 리비전 페이지용 카테고리별 자재 조회 Args: file_id: 파일 ID category: 카테고리 include_processing_info: 처리 정보 포함 여부 Returns: 자재 목록 (처리 정보 포함) """ 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, m.brand, m.user_requirement, m.line_number, m.is_active, m.notes, -- 추가 정보 COALESCE(m.purchase_confirmed_at, m.created_at) as status_date, COALESCE(m.purchase_confirmed_by, 'system') as status_by FROM materials m WHERE m.file_id = :file_id AND m.classified_category = :category AND m.classified_category != 'PIPE' AND m.is_active = true ORDER BY m.line_number """ result = self.db_service.execute_query(query, { "file_id": file_id, "category": category }) materials = [] for row in result.fetchall(): material_dict = dict(row._mapping) if include_processing_info: # 처리 정보 추가 material_dict['processing_info'] = self._get_material_processing_info(material_dict) materials.append(material_dict) return materials def _get_material_processing_info(self, material: Dict) -> Dict[str, Any]: """자재 처리 정보 생성""" revision_status = material.get('revision_status', '') purchase_confirmed = material.get('purchase_confirmed', False) if revision_status == 'new_in_revision': return { 'display_status': 'NEW', 'color': 'green', 'action': '신규 구매 필요', 'priority': 'high' } elif revision_status == 'additional_purchase_needed': return { 'display_status': 'ADDITIONAL', 'color': 'red', 'action': '추가 구매 필요', 'priority': 'high' } elif revision_status == 'excess_inventory': return { 'display_status': 'EXCESS', 'color': 'orange', 'action': '재고 관리', 'priority': 'medium' } elif revision_status == 'quantity_updated': return { 'display_status': 'UPDATED', 'color': 'yellow', 'action': '수량 확인', 'priority': 'medium' } elif purchase_confirmed: return { 'display_status': 'COMPLETED', 'color': 'gray', 'action': '완료', 'priority': 'low' } else: return { 'display_status': 'PENDING', 'color': 'blue', 'action': '구매 대기', 'priority': 'medium' } def apply_material_processing_results( self, processing_results: List[Dict[str, Any]] ) -> Dict[str, Any]: """자재 처리 결과를 DB에 적용""" try: applied_count = 0 error_count = 0 for result in processing_results: try: material_id = result['material_id'] action = result['action'] status = result['status'] if action == 'delete': # 자재 비활성화 update_query = """ UPDATE materials SET is_active = false, revision_status = 'deleted', notes = CONCAT(COALESCE(notes, ''), '\n', :note) WHERE id = :material_id """ else: # 자재 상태 업데이트 update_query = """ UPDATE materials SET revision_status = :status, notes = CONCAT(COALESCE(notes, ''), '\n', :note) WHERE id = :material_id """ self.db_service.execute_query(update_query, { "material_id": material_id, "status": status, "note": result.get('processing_note', '') }) applied_count += 1 except Exception as e: logger.error(f"Failed to apply processing result for material {result.get('material_id')}: {e}") error_count += 1 self.db.commit() return { "success": True, "applied_count": applied_count, "error_count": error_count, "message": f"자재 처리 완료: {applied_count}개 적용, {error_count}개 오류" } except Exception as e: self.db.rollback() logger.error(f"Failed to apply material processing results: {e}") return { "success": False, "error": str(e), "message": "자재 처리 적용 중 오류 발생" }