feat: 서포트 카테고리 전면 개선
Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled
Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled
- 서포트 카테고리 UI 개선: 좌우 스크롤, 헤더/본문 동기화, 가운데 정렬 - 동일 항목 합산 기능 구현 (Type + Size + Grade 기준) - 헤더 구조 변경: 압력/스케줄 제거, 구매수량 단일화, User Requirements 추가 - 우레탄 블럭슈 두께 정보(40t, 27t) Material Grade에 포함 - 서포트 수량 계산 수정: 취합된 숫자 그대로 표시 (4의 배수 계산 제거) - 서포트 분류 로직 개선: CLAMP, U-BOLT, URETHANE BLOCK SHOE 등 정확한 분류 - 백엔드 서포트 분류기에 User Requirements 추출 기능 추가 - 엑셀 내보내기에 서포트 카테고리 처리 로직 추가
This commit is contained in:
@@ -1,13 +1,14 @@
|
|||||||
"""
|
"""
|
||||||
구매신청 관리 API
|
구매신청 관리 API
|
||||||
"""
|
"""
|
||||||
from fastapi import APIRouter, Depends, HTTPException, status, Body
|
from fastapi import APIRouter, Depends, HTTPException, status, Body, File, UploadFile, Form
|
||||||
from fastapi.responses import FileResponse
|
from fastapi.responses import FileResponse
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from sqlalchemy import text
|
from sqlalchemy import text
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from typing import Optional, List, Dict
|
from typing import Optional, List, Dict
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
|
|
||||||
@@ -20,7 +21,7 @@ logger = get_logger(__name__)
|
|||||||
router = APIRouter(prefix="/purchase-request", tags=["Purchase Request"])
|
router = APIRouter(prefix="/purchase-request", tags=["Purchase Request"])
|
||||||
|
|
||||||
# 엑셀 파일 저장 경로
|
# 엑셀 파일 저장 경로
|
||||||
EXCEL_DIR = "exports"
|
EXCEL_DIR = "uploads/excel_exports"
|
||||||
os.makedirs(EXCEL_DIR, exist_ok=True)
|
os.makedirs(EXCEL_DIR, exist_ok=True)
|
||||||
|
|
||||||
class PurchaseRequestCreate(BaseModel):
|
class PurchaseRequestCreate(BaseModel):
|
||||||
@@ -705,3 +706,76 @@ def create_excel_file(materials_data: List[Dict], file_path: str, request_no: st
|
|||||||
|
|
||||||
# 파일 저장
|
# 파일 저장
|
||||||
wb.save(file_path)
|
wb.save(file_path)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/upload-excel")
|
||||||
|
async def upload_request_excel(
|
||||||
|
excel_file: UploadFile = File(...),
|
||||||
|
request_id: int = Form(...),
|
||||||
|
category: str = Form(...),
|
||||||
|
# current_user: dict = Depends(get_current_user),
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
구매신청에 대한 엑셀 파일 업로드 (BOM에서 생성한 원본 파일)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 구매신청 정보 조회
|
||||||
|
query = text("""
|
||||||
|
SELECT request_no, job_no
|
||||||
|
FROM purchase_requests
|
||||||
|
WHERE request_id = :request_id
|
||||||
|
""")
|
||||||
|
result = db.execute(query, {"request_id": request_id}).fetchone()
|
||||||
|
|
||||||
|
if not result:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="구매신청을 찾을 수 없습니다"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 엑셀 저장 디렉토리 생성
|
||||||
|
excel_dir = Path("uploads/excel_exports")
|
||||||
|
excel_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# 파일명 생성
|
||||||
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
|
safe_filename = f"{result.job_no}_{result.request_no}_{timestamp}.xlsx"
|
||||||
|
file_path = excel_dir / safe_filename
|
||||||
|
|
||||||
|
# 파일 저장
|
||||||
|
content = await excel_file.read()
|
||||||
|
with open(file_path, "wb") as f:
|
||||||
|
f.write(content)
|
||||||
|
|
||||||
|
# 구매신청 테이블에 엑셀 파일 경로 업데이트
|
||||||
|
update_query = text("""
|
||||||
|
UPDATE purchase_requests
|
||||||
|
SET excel_file_path = :excel_file_path
|
||||||
|
WHERE request_id = :request_id
|
||||||
|
""")
|
||||||
|
|
||||||
|
db.execute(update_query, {
|
||||||
|
"excel_file_path": safe_filename,
|
||||||
|
"request_id": request_id
|
||||||
|
})
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
logger.info(f"엑셀 파일 업로드 완료: {safe_filename}")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"message": "엑셀 파일이 성공적으로 업로드되었습니다",
|
||||||
|
"file_path": safe_filename
|
||||||
|
}
|
||||||
|
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
db.rollback()
|
||||||
|
logger.error(f"Failed to upload excel file: {str(e)}")
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=500,
|
||||||
|
detail=f"엑셀 파일 업로드 실패: {str(e)}"
|
||||||
|
)
|
||||||
|
|||||||
@@ -706,7 +706,8 @@ def classify_bolt(dat_file: str, description: str, main_nom: str, length: Option
|
|||||||
"nominal_size_fraction": dimensions_result.get('nominal_size_fraction', main_nom),
|
"nominal_size_fraction": dimensions_result.get('nominal_size_fraction', main_nom),
|
||||||
"length": dimensions_result.get('length', ''),
|
"length": dimensions_result.get('length', ''),
|
||||||
"diameter": dimensions_result.get('diameter', ''),
|
"diameter": dimensions_result.get('diameter', ''),
|
||||||
"dimension_description": dimensions_result.get('dimension_description', '')
|
"dimension_description": dimensions_result.get('dimension_description', ''),
|
||||||
|
"bolts_per_flange": dimensions_result.get('bolts_per_flange', 1)
|
||||||
},
|
},
|
||||||
|
|
||||||
"grade_strength": {
|
"grade_strength": {
|
||||||
@@ -966,12 +967,19 @@ def extract_bolt_dimensions(main_nom: str, description: str) -> Dict:
|
|||||||
except:
|
except:
|
||||||
nominal_size_fraction = actual_bolt_size
|
nominal_size_fraction = actual_bolt_size
|
||||||
|
|
||||||
|
# 플랜지당 볼트 세트 수 추출 (예: (8), (4))
|
||||||
|
bolts_per_flange = 1 # 기본값
|
||||||
|
flange_bolt_pattern = re.search(r'\((\d+)\)', description)
|
||||||
|
if flange_bolt_pattern:
|
||||||
|
bolts_per_flange = int(flange_bolt_pattern.group(1))
|
||||||
|
|
||||||
dimensions = {
|
dimensions = {
|
||||||
"nominal_size": actual_bolt_size, # 실제 볼트 사이즈
|
"nominal_size": actual_bolt_size, # 실제 볼트 사이즈
|
||||||
"nominal_size_fraction": nominal_size_fraction, # 분수 변환값
|
"nominal_size_fraction": nominal_size_fraction, # 분수 변환값
|
||||||
"length": "",
|
"length": "",
|
||||||
"diameter": "",
|
"diameter": "",
|
||||||
"dimension_description": nominal_size_fraction # 분수로 표시
|
"dimension_description": nominal_size_fraction, # 분수로 표시
|
||||||
|
"bolts_per_flange": bolts_per_flange # 플랜지당 볼트 세트 수
|
||||||
}
|
}
|
||||||
|
|
||||||
# 길이 정보 추출 (개선된 패턴)
|
# 길이 정보 추출 (개선된 패턴)
|
||||||
@@ -984,6 +992,8 @@ def extract_bolt_dimensions(main_nom: str, description: str) -> Dict:
|
|||||||
r'X\s*(\d+(?:\.\d+)?)\s*MM', # M8 X 20MM 형태
|
r'X\s*(\d+(?:\.\d+)?)\s*MM', # M8 X 20MM 형태
|
||||||
r',\s*(\d+(?:\.\d+)?)\s*LG', # ", 145.0000 LG" 형태 (PSV, LT 볼트용)
|
r',\s*(\d+(?:\.\d+)?)\s*LG', # ", 145.0000 LG" 형태 (PSV, LT 볼트용)
|
||||||
r',\s*(\d+(?:\.\d+)?)\s+(?:CK|PSV|LT)', # ", 140 CK" 형태 (PSV 볼트용)
|
r',\s*(\d+(?:\.\d+)?)\s+(?:CK|PSV|LT)', # ", 140 CK" 형태 (PSV 볼트용)
|
||||||
|
r'PSV\s+(\d+(?:\.\d+)?)', # PSV 140 형태 (PSV 볼트 전용)
|
||||||
|
r'(\d+(?:\.\d+)?)\s+PSV', # 140 PSV 형태 (PSV 볼트 전용)
|
||||||
r'(\d+(?:\.\d+)?)\s*MM(?:\s|,|$)', # 75MM 형태 (단독)
|
r'(\d+(?:\.\d+)?)\s*MM(?:\s|,|$)', # 75MM 형태 (단독)
|
||||||
r'(\d+(?:\.\d+)?)\s*mm(?:\s|,|$)' # 75mm 형태 (단독)
|
r'(\d+(?:\.\d+)?)\s*mm(?:\s|,|$)' # 75mm 형태 (단독)
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -182,6 +182,14 @@ def classify_flange(dat_file: str, description: str, main_nom: str,
|
|||||||
dat_upper = dat_file.upper()
|
dat_upper = dat_file.upper()
|
||||||
|
|
||||||
# 1. 플랜지 키워드 확인 (재질만 있어도 통합 분류기가 이미 플랜지로 분류했으므로 진행)
|
# 1. 플랜지 키워드 확인 (재질만 있어도 통합 분류기가 이미 플랜지로 분류했으므로 진행)
|
||||||
|
# 사이트 글라스와 스트레이너는 밸브로 분류되어야 함
|
||||||
|
if 'SIGHT GLASS' in desc_upper or 'STRAINER' in desc_upper or '사이트글라스' in desc_upper or '스트레이너' in desc_upper:
|
||||||
|
return {
|
||||||
|
"category": "VALVE",
|
||||||
|
"overall_confidence": 1.0,
|
||||||
|
"reason": "SIGHT GLASS 또는 STRAINER는 밸브로 분류"
|
||||||
|
}
|
||||||
|
|
||||||
flange_keywords = ['FLG', 'FLANGE', '플랜지', 'ORIFICE', 'SPECTACLE', 'PADDLE', 'SPACER']
|
flange_keywords = ['FLG', 'FLANGE', '플랜지', 'ORIFICE', 'SPECTACLE', 'PADDLE', 'SPACER']
|
||||||
has_flange_keyword = any(keyword in desc_upper or keyword in dat_upper for keyword in flange_keywords)
|
has_flange_keyword = any(keyword in desc_upper or keyword in dat_upper for keyword in flange_keywords)
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from .fitting_classifier import classify_fitting
|
|||||||
# Level 1: 명확한 타입 키워드 (최우선)
|
# Level 1: 명확한 타입 키워드 (최우선)
|
||||||
LEVEL1_TYPE_KEYWORDS = {
|
LEVEL1_TYPE_KEYWORDS = {
|
||||||
"BOLT": ["FLANGE BOLT", "U-BOLT", "U BOLT", "BOLT", "STUD", "NUT", "SCREW", "WASHER", "볼트", "너트", "스터드", "나사", "와셔", "유볼트"],
|
"BOLT": ["FLANGE BOLT", "U-BOLT", "U BOLT", "BOLT", "STUD", "NUT", "SCREW", "WASHER", "볼트", "너트", "스터드", "나사", "와셔", "유볼트"],
|
||||||
"VALVE": ["VALVE", "GATE", "BALL", "GLOBE", "CHECK", "BUTTERFLY", "NEEDLE", "RELIEF", "밸브", "게이트", "볼", "글로브", "체크", "버터플라이", "니들", "릴리프"],
|
"VALVE": ["VALVE", "GATE", "BALL", "GLOBE", "CHECK", "BUTTERFLY", "NEEDLE", "RELIEF", "SIGHT GLASS", "STRAINER", "밸브", "게이트", "볼", "글로브", "체크", "버터플라이", "니들", "릴리프", "사이트글라스", "스트레이너"],
|
||||||
"FLANGE": ["FLG", "FLANGE", "플랜지", "프랜지", "ORIFICE", "SPECTACLE", "PADDLE", "SPACER", "BLIND", "REDUCING FLANGE", "RED FLANGE"],
|
"FLANGE": ["FLG", "FLANGE", "플랜지", "프랜지", "ORIFICE", "SPECTACLE", "PADDLE", "SPACER", "BLIND", "REDUCING FLANGE", "RED FLANGE"],
|
||||||
"PIPE": ["PIPE", "TUBE", "파이프", "배관", "SMLS", "SEAMLESS"],
|
"PIPE": ["PIPE", "TUBE", "파이프", "배관", "SMLS", "SEAMLESS"],
|
||||||
"FITTING": ["SOCK-O-LET", "WELD-O-LET", "ELL-O-LET", "THREAD-O-LET", "ELB-O-LET", "NIP-O-LET", "COUP-O-LET", "SOCKOLET", "WELDOLET", "ELLOLET", "THREADOLET", "ELBOLET", "NIPOLET", "COUPOLET", "OLET", "ELBOW", "ELL", "TEE", "REDUCER", "CAP", "COUPLING", "NIPPLE", "SWAGE", "PLUG", "엘보", "티", "리듀서", "캡", "니플", "커플링", "플러그", "CONC", "ECC"],
|
"FITTING": ["SOCK-O-LET", "WELD-O-LET", "ELL-O-LET", "THREAD-O-LET", "ELB-O-LET", "NIP-O-LET", "COUP-O-LET", "SOCKOLET", "WELDOLET", "ELLOLET", "THREADOLET", "ELBOLET", "NIPOLET", "COUPOLET", "OLET", "ELBOW", "ELL", "TEE", "REDUCER", "CAP", "COUPLING", "NIPPLE", "SWAGE", "PLUG", "엘보", "티", "리듀서", "캡", "니플", "커플링", "플러그", "CONC", "ECC"],
|
||||||
@@ -110,6 +110,17 @@ def classify_material_integrated(description: str, main_nom: str = "",
|
|||||||
"reason": "스페셜 키워드 발견"
|
"reason": "스페셜 키워드 발견"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# VALVE 카테고리 우선 확인 (SIGHT GLASS, STRAINER)
|
||||||
|
if ('SIGHT GLASS' in desc_upper or 'STRAINER' in desc_upper or
|
||||||
|
'사이트글라스' in desc_upper or '스트레이너' in desc_upper):
|
||||||
|
return {
|
||||||
|
"category": "VALVE",
|
||||||
|
"confidence": 1.0,
|
||||||
|
"evidence": ["VALVE_SPECIAL_KEYWORD"],
|
||||||
|
"classification_level": "LEVEL0_VALVE",
|
||||||
|
"reason": "SIGHT GLASS 또는 STRAINER 키워드 발견"
|
||||||
|
}
|
||||||
|
|
||||||
# SUPPORT 카테고리 우선 확인 (BOLT 카테고리보다 먼저)
|
# SUPPORT 카테고리 우선 확인 (BOLT 카테고리보다 먼저)
|
||||||
# U-BOLT, CLAMP, URETHANE BLOCK 등
|
# U-BOLT, CLAMP, URETHANE BLOCK 등
|
||||||
if ('U-BOLT' in desc_upper or 'U BOLT' in desc_upper or '유볼트' in desc_upper or
|
if ('U-BOLT' in desc_upper or 'U BOLT' in desc_upper or '유볼트' in desc_upper or
|
||||||
|
|||||||
@@ -108,7 +108,22 @@ def classify_support(dat_file: str, description: str, main_nom: str,
|
|||||||
# 4. 사이즈 정보 추출
|
# 4. 사이즈 정보 추출
|
||||||
size_result = extract_support_size(description, main_nom)
|
size_result = extract_support_size(description, main_nom)
|
||||||
|
|
||||||
# 5. 최종 결과 조합
|
# 5. 사용자 요구사항 추출
|
||||||
|
user_requirements = extract_support_user_requirements(description)
|
||||||
|
|
||||||
|
# 6. 우레탄 블럭슈 두께 정보 추출 및 Material Grade 보강
|
||||||
|
enhanced_material_grade = material_result.get('grade', 'UNKNOWN')
|
||||||
|
if support_type_result.get("support_type") == "URETHANE_BLOCK":
|
||||||
|
# 두께 정보 추출 (40t, 27t 등)
|
||||||
|
thickness_match = re.search(r'(\d+)\s*[tT](?![oO])', description.upper())
|
||||||
|
if thickness_match:
|
||||||
|
thickness = f"{thickness_match.group(1)}t"
|
||||||
|
if enhanced_material_grade == 'UNKNOWN' or not enhanced_material_grade:
|
||||||
|
enhanced_material_grade = thickness
|
||||||
|
elif thickness not in enhanced_material_grade:
|
||||||
|
enhanced_material_grade = f"{enhanced_material_grade} {thickness}"
|
||||||
|
|
||||||
|
# 7. 최종 결과 조합
|
||||||
return {
|
return {
|
||||||
"category": "SUPPORT",
|
"category": "SUPPORT",
|
||||||
|
|
||||||
@@ -118,10 +133,10 @@ def classify_support(dat_file: str, description: str, main_nom: str,
|
|||||||
"load_rating": load_result.get("load_rating", ""),
|
"load_rating": load_result.get("load_rating", ""),
|
||||||
"load_capacity": load_result.get("capacity", ""),
|
"load_capacity": load_result.get("capacity", ""),
|
||||||
|
|
||||||
# 재질 정보 (공통 모듈)
|
# 재질 정보 (공통 모듈) - 우레탄 블럭슈 두께 정보 포함
|
||||||
"material": {
|
"material": {
|
||||||
"standard": material_result.get('standard', 'UNKNOWN'),
|
"standard": material_result.get('standard', 'UNKNOWN'),
|
||||||
"grade": material_result.get('grade', 'UNKNOWN'),
|
"grade": enhanced_material_grade,
|
||||||
"material_type": material_result.get('material_type', 'UNKNOWN'),
|
"material_type": material_result.get('material_type', 'UNKNOWN'),
|
||||||
"confidence": material_result.get('confidence', 0.0)
|
"confidence": material_result.get('confidence', 0.0)
|
||||||
},
|
},
|
||||||
@@ -129,6 +144,9 @@ def classify_support(dat_file: str, description: str, main_nom: str,
|
|||||||
# 사이즈 정보
|
# 사이즈 정보
|
||||||
"size_info": size_result,
|
"size_info": size_result,
|
||||||
|
|
||||||
|
# 사용자 요구사항
|
||||||
|
"user_requirements": user_requirements,
|
||||||
|
|
||||||
# 전체 신뢰도
|
# 전체 신뢰도
|
||||||
"overall_confidence": calculate_support_confidence({
|
"overall_confidence": calculate_support_confidence({
|
||||||
"type": support_type_result.get('confidence', 0),
|
"type": support_type_result.get('confidence', 0),
|
||||||
@@ -183,6 +201,34 @@ def classify_support_type(dat_file: str, description: str) -> Dict:
|
|||||||
"evidence": ["NO_SUPPORT_TYPE_FOUND"]
|
"evidence": ["NO_SUPPORT_TYPE_FOUND"]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def extract_support_user_requirements(description: str) -> List[str]:
|
||||||
|
"""서포트 사용자 요구사항 추출"""
|
||||||
|
|
||||||
|
desc_upper = description.upper()
|
||||||
|
requirements = []
|
||||||
|
|
||||||
|
# 표면처리 관련
|
||||||
|
if 'GALV' in desc_upper or 'GALVANIZED' in desc_upper:
|
||||||
|
requirements.append('GALVANIZED')
|
||||||
|
if 'HDG' in desc_upper or 'HOT DIP' in desc_upper:
|
||||||
|
requirements.append('HOT DIP GALVANIZED')
|
||||||
|
if 'PAINT' in desc_upper or 'PAINTED' in desc_upper:
|
||||||
|
requirements.append('PAINTED')
|
||||||
|
|
||||||
|
# 재질 관련
|
||||||
|
if 'SS' in desc_upper or 'STAINLESS' in desc_upper:
|
||||||
|
requirements.append('STAINLESS STEEL')
|
||||||
|
if 'CARBON' in desc_upper:
|
||||||
|
requirements.append('CARBON STEEL')
|
||||||
|
|
||||||
|
# 특수 요구사항
|
||||||
|
if 'FIRE SAFE' in desc_upper:
|
||||||
|
requirements.append('FIRE SAFE')
|
||||||
|
if 'SEISMIC' in desc_upper or '내진' in desc_upper:
|
||||||
|
requirements.append('SEISMIC')
|
||||||
|
|
||||||
|
return requirements
|
||||||
|
|
||||||
def classify_load_rating(description: str) -> Dict:
|
def classify_load_rating(description: str) -> Dict:
|
||||||
"""하중 등급 분류"""
|
"""하중 등급 분류"""
|
||||||
|
|
||||||
|
|||||||
@@ -89,6 +89,24 @@ VALVE_TYPES = {
|
|||||||
"typical_connections": ["FLANGED", "THREADED"],
|
"typical_connections": ["FLANGED", "THREADED"],
|
||||||
"pressure_range": "150LB ~ 600LB",
|
"pressure_range": "150LB ~ 600LB",
|
||||||
"special_features": ["LUBRICATED", "NON_LUBRICATED"]
|
"special_features": ["LUBRICATED", "NON_LUBRICATED"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"SIGHT_GLASS": {
|
||||||
|
"dat_file_patterns": ["SIGHT_", "SG_"],
|
||||||
|
"description_keywords": ["SIGHT GLASS", "SIGHT", "사이트글라스", "사이트 글라스"],
|
||||||
|
"characteristics": "유체 확인용 관찰창",
|
||||||
|
"typical_connections": ["FLANGED", "THREADED"],
|
||||||
|
"pressure_range": "150LB ~ 600LB",
|
||||||
|
"special_features": ["TRANSPARENT", "VISUAL_INSPECTION"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"STRAINER": {
|
||||||
|
"dat_file_patterns": ["STRAINER_", "STR_"],
|
||||||
|
"description_keywords": ["STRAINER", "스트레이너", "여과기"],
|
||||||
|
"characteristics": "이물질 여과용",
|
||||||
|
"typical_connections": ["FLANGED", "THREADED"],
|
||||||
|
"pressure_range": "150LB ~ 600LB",
|
||||||
|
"special_features": ["MESH_FILTER", "Y_TYPE", "BASKET_TYPE"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,8 +230,13 @@ def classify_valve(dat_file: str, description: str, main_nom: str, length: float
|
|||||||
desc_upper = description.upper()
|
desc_upper = description.upper()
|
||||||
dat_upper = dat_file.upper()
|
dat_upper = dat_file.upper()
|
||||||
|
|
||||||
# 1. 밸브 키워드 확인 (재질만 있어도 통합 분류기가 이미 밸브로 분류했으므로 진행)
|
# 1. 사이트 글라스와 스트레이너 우선 확인
|
||||||
valve_keywords = ['VALVE', 'GATE', 'BALL', 'GLOBE', 'CHECK', 'BUTTERFLY', 'NEEDLE', 'RELIEF', 'SOLENOID', '밸브', '게이트', '볼', '글로브', '체크', '버터플라이', '니들', '릴리프', '솔레노이드']
|
if 'SIGHT GLASS' in desc_upper or 'STRAINER' in desc_upper or '사이트글라스' in desc_upper or '스트레이너' in desc_upper:
|
||||||
|
# 사이트 글라스와 스트레이너는 항상 밸브로 분류
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 밸브 키워드 확인 (재질만 있어도 통합 분류기가 이미 밸브로 분류했으므로 진행)
|
||||||
|
valve_keywords = ['VALVE', 'GATE', 'BALL', 'GLOBE', 'CHECK', 'BUTTERFLY', 'NEEDLE', 'RELIEF', 'SOLENOID', 'SIGHT GLASS', 'STRAINER', '밸브', '게이트', '볼', '글로브', '체크', '버터플라이', '니들', '릴리프', '솔레노이드', '사이트글라스', '스트레이너']
|
||||||
has_valve_keyword = any(keyword in desc_upper or keyword in dat_upper for keyword in valve_keywords)
|
has_valve_keyword = any(keyword in desc_upper or keyword in dat_upper for keyword in valve_keywords)
|
||||||
|
|
||||||
# 밸브 재질 확인 (A216, A217, A351, A352)
|
# 밸브 재질 확인 (A216, A217, A351, A352)
|
||||||
|
|||||||
168
backend/exports/PR-20251016-001.json
Normal file
168
backend/exports/PR-20251016-001.json
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
{
|
||||||
|
"request_no": "PR-20251016-001",
|
||||||
|
"job_no": "1",
|
||||||
|
"created_at": "2025-10-16T05:40:46.947440",
|
||||||
|
"materials": [
|
||||||
|
{
|
||||||
|
"material_id": 3543,
|
||||||
|
"description": "PIPE, SMLS, SCH 40S, ASTM A312 TP304",
|
||||||
|
"category": "PIPE",
|
||||||
|
"size": "1/2\"",
|
||||||
|
"material_grade": "ASTM A312 TP304",
|
||||||
|
"quantity": 11,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 3551,
|
||||||
|
"description": "PIPE, SMLS, SCH 80, ASTM A106 B",
|
||||||
|
"category": "PIPE",
|
||||||
|
"size": "3/4\"",
|
||||||
|
"material_grade": "ASTM A106 B",
|
||||||
|
"quantity": 92,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 3555,
|
||||||
|
"description": "PIPE, SMLS, SCH 40S, ASTM A312 TP304",
|
||||||
|
"category": "PIPE",
|
||||||
|
"size": "1\"",
|
||||||
|
"material_grade": "ASTM A312 TP304",
|
||||||
|
"quantity": 23,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 3565,
|
||||||
|
"description": "PIPE, SMLS, SCH 80, ASTM A106 B",
|
||||||
|
"category": "PIPE",
|
||||||
|
"size": "1\"",
|
||||||
|
"material_grade": "ASTM A106 B",
|
||||||
|
"quantity": 139,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 3574,
|
||||||
|
"description": "PIPE, SMLS, SCH 40S, ASTM A312 TP304",
|
||||||
|
"category": "PIPE",
|
||||||
|
"size": "1 1/2\"",
|
||||||
|
"material_grade": "ASTM A312 TP304",
|
||||||
|
"quantity": 14,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 3588,
|
||||||
|
"description": "PIPE, SMLS, SCH 80, ASTM A106 B",
|
||||||
|
"category": "PIPE",
|
||||||
|
"size": "1 1/2\"",
|
||||||
|
"material_grade": "ASTM A106 B",
|
||||||
|
"quantity": 98,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 3844,
|
||||||
|
"description": "PIPE, SMLS, SCH 80, ASTM A106 B",
|
||||||
|
"category": "PIPE",
|
||||||
|
"size": "1/2\"",
|
||||||
|
"material_grade": "ASTM A106 B",
|
||||||
|
"quantity": 82,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 3926,
|
||||||
|
"description": "PIPE, SMLS, SCH 40S, ASTM A312 TP304",
|
||||||
|
"category": "PIPE",
|
||||||
|
"size": "10\"",
|
||||||
|
"material_grade": "ASTM A312 TP304",
|
||||||
|
"quantity": 4,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 3930,
|
||||||
|
"description": "PIPE, SMLS, SCH 40S, ASTM A312 TP304",
|
||||||
|
"category": "PIPE",
|
||||||
|
"size": "12\"",
|
||||||
|
"material_grade": "ASTM A312 TP304",
|
||||||
|
"quantity": 1,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 3931,
|
||||||
|
"description": "PIPE, SMLS, SCH 40, ASTM A106 B",
|
||||||
|
"category": "PIPE",
|
||||||
|
"size": "2\"",
|
||||||
|
"material_grade": "ASTM A106 B",
|
||||||
|
"quantity": 50,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 3981,
|
||||||
|
"description": "PIPE, SMLS, SCH 40S, ASTM A312 TP304",
|
||||||
|
"category": "PIPE",
|
||||||
|
"size": "2\"",
|
||||||
|
"material_grade": "ASTM A312 TP304",
|
||||||
|
"quantity": 9,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 3990,
|
||||||
|
"description": "PIPE, SMLS, SCH 40, ASTM A106 B",
|
||||||
|
"category": "PIPE",
|
||||||
|
"size": "3\"",
|
||||||
|
"material_grade": "ASTM A106 B",
|
||||||
|
"quantity": 25,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 3998,
|
||||||
|
"description": "PIPE, SMLS, SCH 40S, ASTM A312 TP304",
|
||||||
|
"category": "PIPE",
|
||||||
|
"size": "3\"",
|
||||||
|
"material_grade": "ASTM A312 TP304",
|
||||||
|
"quantity": 8,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 4023,
|
||||||
|
"description": "PIPE, SMLS, SCH 40S, ASTM A312 TP304",
|
||||||
|
"category": "PIPE",
|
||||||
|
"size": "3/4\"",
|
||||||
|
"material_grade": "ASTM A312 TP304",
|
||||||
|
"quantity": 15,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 4126,
|
||||||
|
"description": "PIPE, SMLS, SCH 40, ASTM A106 B",
|
||||||
|
"category": "PIPE",
|
||||||
|
"size": "4\"",
|
||||||
|
"material_grade": "ASTM A106 B",
|
||||||
|
"quantity": 12,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 4138,
|
||||||
|
"description": "PIPE, SMLS, SCH 40, ASTM A106 B",
|
||||||
|
"category": "PIPE",
|
||||||
|
"size": "6\"",
|
||||||
|
"material_grade": "ASTM A106 B",
|
||||||
|
"quantity": 13,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"grouped_materials": []
|
||||||
|
}
|
||||||
BIN
backend/exports/PR-20251016-001.xlsx
Normal file
BIN
backend/exports/PR-20251016-001.xlsx
Normal file
Binary file not shown.
778
backend/exports/PR-20251016-002.json
Normal file
778
backend/exports/PR-20251016-002.json
Normal file
@@ -0,0 +1,778 @@
|
|||||||
|
{
|
||||||
|
"request_no": "PR-20251016-002",
|
||||||
|
"job_no": "1",
|
||||||
|
"created_at": "2025-10-16T05:44:08.264221",
|
||||||
|
"materials": [
|
||||||
|
{
|
||||||
|
"material_id": 3540,
|
||||||
|
"description": "HALF NIPPLE, SMLS, SCH 80S, ASTM A312 TP304 SW X NPT",
|
||||||
|
"category": "FITTING",
|
||||||
|
"size": "1/2\"",
|
||||||
|
"material_grade": "ASTM A312 TP304",
|
||||||
|
"quantity": 2,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 3542,
|
||||||
|
"description": "HALF NIPPLE, SMLS, SCH 80S, ASTM A312 TP304 SW X NPT",
|
||||||
|
"category": "FITTING",
|
||||||
|
"size": "1/2\"",
|
||||||
|
"material_grade": "ASTM A312 TP304",
|
||||||
|
"quantity": 1,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 3682,
|
||||||
|
"description": "HALF NIPPLE, SMLS, SCH 160, ASTM A106 B SW * NPT",
|
||||||
|
"category": "FITTING",
|
||||||
|
"size": "1\"",
|
||||||
|
"material_grade": "ASTM A106 B",
|
||||||
|
"quantity": 2,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 3831,
|
||||||
|
"description": "HALF NIPPLE, SMLS, SCH 160, ASTM A106 B SW X NPT",
|
||||||
|
"category": "FITTING",
|
||||||
|
"size": "1/2\"",
|
||||||
|
"material_grade": "ASTM A106 B",
|
||||||
|
"quantity": 4,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 3835,
|
||||||
|
"description": "HALF NIPPLE, SMLS, SCH 160, ASTM A106 B SW X NPT",
|
||||||
|
"category": "FITTING",
|
||||||
|
"size": "1/2\"",
|
||||||
|
"material_grade": "ASTM A106 B",
|
||||||
|
"quantity": 3,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 3838,
|
||||||
|
"description": "HALF NIPPLE, SMLS, SCH 160, ASTM A106 B SW X NPT",
|
||||||
|
"category": "FITTING",
|
||||||
|
"size": "1/2\"",
|
||||||
|
"material_grade": "ASTM A106 B",
|
||||||
|
"quantity": 2,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 3840,
|
||||||
|
"description": "NIPPLE, SMLS, SCH 160, ASTM A106 B",
|
||||||
|
"category": "FITTING",
|
||||||
|
"size": "1/2\"",
|
||||||
|
"material_grade": "ASTM A106 B",
|
||||||
|
"quantity": 1,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 4151,
|
||||||
|
"description": "90 LR ELL, SMLS, SCH 40S, ASTM A403 WP304",
|
||||||
|
"category": "FITTING",
|
||||||
|
"size": "10\"",
|
||||||
|
"material_grade": "ASTM A403 WP304",
|
||||||
|
"quantity": 1,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 4152,
|
||||||
|
"description": "90 LR ELL, SMLS, SCH 40, ASTM A234 WPB",
|
||||||
|
"category": "FITTING",
|
||||||
|
"size": "2\"",
|
||||||
|
"material_grade": "ASTM A234 WPB",
|
||||||
|
"quantity": 25,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 4177,
|
||||||
|
"description": "90 LR ELL, SMLS, SCH 40S, ASTM A403 WP304",
|
||||||
|
"category": "FITTING",
|
||||||
|
"size": "2\"",
|
||||||
|
"material_grade": "ASTM A403 WP304",
|
||||||
|
"quantity": 6,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 4183,
|
||||||
|
"description": "90 LR ELL, SMLS, SCH 40, ASTM A234 WPB",
|
||||||
|
"category": "FITTING",
|
||||||
|
"size": "3\"",
|
||||||
|
"material_grade": "ASTM A234 WPB",
|
||||||
|
"quantity": 12,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 4195,
|
||||||
|
"description": "90 LR ELL, SMLS, SCH 40S, ASTM A403 WP304",
|
||||||
|
"category": "FITTING",
|
||||||
|
"size": "3\"",
|
||||||
|
"material_grade": "ASTM A403 WP304",
|
||||||
|
"quantity": 4,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 4199,
|
||||||
|
"description": "90 LR ELL, SMLS, SCH 40, ASTM A234 WPB",
|
||||||
|
"category": "FITTING",
|
||||||
|
"size": "4\"",
|
||||||
|
"material_grade": "ASTM A234 WPB",
|
||||||
|
"quantity": 7,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 4206,
|
||||||
|
"description": "90 SR ELL, SMLS, SCH 40, ASTM A234 WPB",
|
||||||
|
"category": "FITTING",
|
||||||
|
"size": "4\"",
|
||||||
|
"material_grade": "ASTM A234 WPB",
|
||||||
|
"quantity": 1,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 4207,
|
||||||
|
"description": "90 LR ELL, SMLS, SCH 40, ASTM A234 WPB",
|
||||||
|
"category": "FITTING",
|
||||||
|
"size": "6\"",
|
||||||
|
"material_grade": "ASTM A234 WPB",
|
||||||
|
"quantity": 7,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 4214,
|
||||||
|
"description": "45 ELL, SMLS, SCH 40, ASTM A234 WPB",
|
||||||
|
"category": "FITTING",
|
||||||
|
"size": "6\"",
|
||||||
|
"material_grade": "ASTM A234 WPB",
|
||||||
|
"quantity": 2,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 4216,
|
||||||
|
"description": "TEE, SMLS, SCH 40, ASTM A234 WPB",
|
||||||
|
"category": "FITTING",
|
||||||
|
"size": "2\"",
|
||||||
|
"material_grade": "ASTM A234 WPB",
|
||||||
|
"quantity": 4,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 4220,
|
||||||
|
"description": "TEE, SMLS, SCH 40S, ASTM A403 WP304",
|
||||||
|
"category": "FITTING",
|
||||||
|
"size": "2\"",
|
||||||
|
"material_grade": "ASTM A403 WP304",
|
||||||
|
"quantity": 1,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 4221,
|
||||||
|
"description": "TEE, SMLS, SCH 40, ASTM A234 WPB",
|
||||||
|
"category": "FITTING",
|
||||||
|
"size": "3\"",
|
||||||
|
"material_grade": "ASTM A234 WPB",
|
||||||
|
"quantity": 1,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 4222,
|
||||||
|
"description": "TEE, SMLS, SCH 40S, ASTM A403 WP304",
|
||||||
|
"category": "FITTING",
|
||||||
|
"size": "3\"",
|
||||||
|
"material_grade": "ASTM A403 WP304",
|
||||||
|
"quantity": 1,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 4223,
|
||||||
|
"description": "TEE RED, SMLS, SCH 40 X SCH 80, ASTM A234 WPB",
|
||||||
|
"category": "FITTING",
|
||||||
|
"size": "2\" x 1 1/2\"",
|
||||||
|
"material_grade": "ASTM A234 WPB",
|
||||||
|
"quantity": 3,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 4226,
|
||||||
|
"description": "TEE RED, SMLS, SCH 40S X SCH 40S, ASTM A403 WP304",
|
||||||
|
"category": "FITTING",
|
||||||
|
"size": "2\" x 1 1/2\"",
|
||||||
|
"material_grade": "ASTM A403 WP304",
|
||||||
|
"quantity": 2,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 4228,
|
||||||
|
"description": "TEE RED, SMLS, SCH 40 X SCH 80, ASTM A234 WPB",
|
||||||
|
"category": "FITTING",
|
||||||
|
"size": "3\" x 1 1/2\"",
|
||||||
|
"material_grade": "ASTM A234 WPB",
|
||||||
|
"quantity": 3,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 4231,
|
||||||
|
"description": "TEE RED, SMLS, SCH 40 X SCH 80, ASTM A234 WPB",
|
||||||
|
"category": "FITTING",
|
||||||
|
"size": "4\" x 1 1/2\"",
|
||||||
|
"material_grade": "ASTM A234 WPB",
|
||||||
|
"quantity": 2,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 4233,
|
||||||
|
"description": "RED CONC, SMLS, SCH 80 X SCH 80, ASTM A234 WPB",
|
||||||
|
"category": "FITTING",
|
||||||
|
"size": "1 1/2\" x 1\"",
|
||||||
|
"material_grade": "ASTM A234 WPB",
|
||||||
|
"quantity": 5,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 4238,
|
||||||
|
"description": "RED CONC, SMLS, SCH 80 X SCH 80, ASTM A234 WPB",
|
||||||
|
"category": "FITTING",
|
||||||
|
"size": "1 1/2\" x 3/4\"",
|
||||||
|
"material_grade": "ASTM A234 WPB",
|
||||||
|
"quantity": 2,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 4240,
|
||||||
|
"description": "RED CONC, SMLS, SCH 80 X SCH 80, ASTM A234 WPB",
|
||||||
|
"category": "FITTING",
|
||||||
|
"size": "1\" x 3/4\"",
|
||||||
|
"material_grade": "ASTM A234 WPB",
|
||||||
|
"quantity": 5,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 4245,
|
||||||
|
"description": "RED CONC, SMLS, SCH 40S X SCH 40S, ASTM A403 WP304",
|
||||||
|
"category": "FITTING",
|
||||||
|
"size": "12\" x 10\"",
|
||||||
|
"material_grade": "ASTM A403 WP304",
|
||||||
|
"quantity": 1,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 4246,
|
||||||
|
"description": "RED CONC, SMLS, SCH 40 X SCH 80, ASTM A234 WPB",
|
||||||
|
"category": "FITTING",
|
||||||
|
"size": "2\" x 1 1/2\"",
|
||||||
|
"material_grade": "ASTM A234 WPB",
|
||||||
|
"quantity": 6,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 4252,
|
||||||
|
"description": "RED CONC, SMLS, SCH 40S X SCH 40S, ASTM A403 WP304",
|
||||||
|
"category": "FITTING",
|
||||||
|
"size": "2\" x 1 1/2\"",
|
||||||
|
"material_grade": "ASTM A403 WP304",
|
||||||
|
"quantity": 1,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 4253,
|
||||||
|
"description": "RED CONC, SMLS, SCH 40 X SCH 80, ASTM A234 WPB",
|
||||||
|
"category": "FITTING",
|
||||||
|
"size": "2\" x 1\"",
|
||||||
|
"material_grade": "ASTM A234 WPB",
|
||||||
|
"quantity": 1,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 4254,
|
||||||
|
"description": "RED CONC, SMLS, SCH 40 X SCH 80, ASTM A234 WPB",
|
||||||
|
"category": "FITTING",
|
||||||
|
"size": "3\" x 1 1/2\"",
|
||||||
|
"material_grade": "ASTM A234 WPB",
|
||||||
|
"quantity": 2,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 4256,
|
||||||
|
"description": "RED CONC, SMLS, SCH 40S X SCH 40S, ASTM A403 WP304",
|
||||||
|
"category": "FITTING",
|
||||||
|
"size": "3\" x 1\"",
|
||||||
|
"material_grade": "ASTM A403 WP304",
|
||||||
|
"quantity": 1,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 4257,
|
||||||
|
"description": "RED CONC, SMLS, SCH 40 X SCH 40, ASTM A234 WPB",
|
||||||
|
"category": "FITTING",
|
||||||
|
"size": "3\" x 2\"",
|
||||||
|
"material_grade": "ASTM A234 WPB",
|
||||||
|
"quantity": 1,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 4258,
|
||||||
|
"description": "RED CONC, SMLS, SCH 40S X SCH 40S, ASTM A403 WP304",
|
||||||
|
"category": "FITTING",
|
||||||
|
"size": "3\" x 2\"",
|
||||||
|
"material_grade": "ASTM A403 WP304",
|
||||||
|
"quantity": 1,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 4259,
|
||||||
|
"description": "RED CONC, SMLS, SCH 80 X SCH 80, ASTM A234 WPB",
|
||||||
|
"category": "FITTING",
|
||||||
|
"size": "3/4\" x 1/2\"",
|
||||||
|
"material_grade": "ASTM A234 WPB",
|
||||||
|
"quantity": 2,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 5136,
|
||||||
|
"description": "90 ELL, SW, 3000LB, ASTM A182 F304",
|
||||||
|
"category": "FITTING",
|
||||||
|
"size": "1/2\"",
|
||||||
|
"material_grade": "ASTM A182 F304",
|
||||||
|
"quantity": 2,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 5138,
|
||||||
|
"description": "90 ELL, SW, 3000LB, ASTM A105",
|
||||||
|
"category": "FITTING",
|
||||||
|
"size": "1\"",
|
||||||
|
"material_grade": "ASTM A105",
|
||||||
|
"quantity": 57,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 5142,
|
||||||
|
"description": "90 ELL, SW, 3000LB, ASTM A182 F304",
|
||||||
|
"category": "FITTING",
|
||||||
|
"size": "1\"",
|
||||||
|
"material_grade": "ASTM A182 F304",
|
||||||
|
"quantity": 9,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 5146,
|
||||||
|
"description": "90 ELL, SW, 3000LB, ASTM A105",
|
||||||
|
"category": "FITTING",
|
||||||
|
"size": "1 1/2\"",
|
||||||
|
"material_grade": "ASTM A105",
|
||||||
|
"quantity": 32,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 5178,
|
||||||
|
"description": "90 ELL, SW, 3000LB, ASTM A182 F304",
|
||||||
|
"category": "FITTING",
|
||||||
|
"size": "1 1/2\"",
|
||||||
|
"material_grade": "ASTM A182 F304",
|
||||||
|
"quantity": 9,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 5245,
|
||||||
|
"description": "90 ELL, SW, 3000LB, ASTM A105",
|
||||||
|
"category": "FITTING",
|
||||||
|
"size": "1/2\"",
|
||||||
|
"material_grade": "ASTM A105",
|
||||||
|
"quantity": 32,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 5277,
|
||||||
|
"description": "90 ELL, SW, 3000LB, ASTM A105",
|
||||||
|
"category": "FITTING",
|
||||||
|
"size": "3/4\"",
|
||||||
|
"material_grade": "ASTM A105",
|
||||||
|
"quantity": 24,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 5301,
|
||||||
|
"description": "TEE, SW, 3000LB, ASTM A105",
|
||||||
|
"category": "FITTING",
|
||||||
|
"size": "1 1/2\"",
|
||||||
|
"material_grade": "ASTM A105",
|
||||||
|
"quantity": 7,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 5308,
|
||||||
|
"description": "TEE, SW, 3000LB, ASTM A105",
|
||||||
|
"category": "FITTING",
|
||||||
|
"size": "1\"",
|
||||||
|
"material_grade": "ASTM A105",
|
||||||
|
"quantity": 15,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 5323,
|
||||||
|
"description": "TEE, SW, 3000LB, ASTM A182 F304",
|
||||||
|
"category": "FITTING",
|
||||||
|
"size": "1\"",
|
||||||
|
"material_grade": "ASTM A182 F304",
|
||||||
|
"quantity": 1,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 5324,
|
||||||
|
"description": "TEE, SW, 3000LB, ASTM A105",
|
||||||
|
"category": "FITTING",
|
||||||
|
"size": "1/2\"",
|
||||||
|
"material_grade": "ASTM A105",
|
||||||
|
"quantity": 2,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 5326,
|
||||||
|
"description": "TEE RED, SW, 3000LB, ASTM A105",
|
||||||
|
"category": "FITTING",
|
||||||
|
"size": "1 1/2\" x 1\"",
|
||||||
|
"material_grade": "ASTM A105",
|
||||||
|
"quantity": 5,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 5331,
|
||||||
|
"description": "TEE RED, SW, 3000LB, ASTM A105",
|
||||||
|
"category": "FITTING",
|
||||||
|
"size": "1 1/2\" x 1/2\"",
|
||||||
|
"material_grade": "ASTM A105",
|
||||||
|
"quantity": 2,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 5333,
|
||||||
|
"description": "TEE RED, SW, 3000LB, ASTM A105",
|
||||||
|
"category": "FITTING",
|
||||||
|
"size": "1 1/2\" x 3/4\"",
|
||||||
|
"material_grade": "ASTM A105",
|
||||||
|
"quantity": 6,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 5339,
|
||||||
|
"description": "TEE RED, SW, 3000LB, ASTM A105",
|
||||||
|
"category": "FITTING",
|
||||||
|
"size": "1\" x 1/2\"",
|
||||||
|
"material_grade": "ASTM A105",
|
||||||
|
"quantity": 7,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 5346,
|
||||||
|
"description": "TEE RED, SW, 3000LB, ASTM A182 F304",
|
||||||
|
"category": "FITTING",
|
||||||
|
"size": "1\" x 1/2\"",
|
||||||
|
"material_grade": "ASTM A182 F304",
|
||||||
|
"quantity": 6,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 5349,
|
||||||
|
"description": "TEE RED, SW, 3000LB, ASTM A105",
|
||||||
|
"category": "FITTING",
|
||||||
|
"size": "1\" x 3/4\"",
|
||||||
|
"material_grade": "ASTM A105",
|
||||||
|
"quantity": 3,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 5355,
|
||||||
|
"description": "TEE RED, SW, 3000LB, ASTM A105",
|
||||||
|
"category": "FITTING",
|
||||||
|
"size": "2\" x 1\"",
|
||||||
|
"material_grade": "ASTM A105",
|
||||||
|
"quantity": 4,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 5359,
|
||||||
|
"description": "TEE RED, SW, 3000LB, ASTM A105",
|
||||||
|
"category": "FITTING",
|
||||||
|
"size": "3/4\" x 1/2\"",
|
||||||
|
"material_grade": "ASTM A105",
|
||||||
|
"quantity": 5,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 5364,
|
||||||
|
"description": "CAP, SW, 3000LB, ASTM A105",
|
||||||
|
"category": "FITTING",
|
||||||
|
"size": "1 1/2\"",
|
||||||
|
"material_grade": "ASTM A105",
|
||||||
|
"quantity": 1,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 5365,
|
||||||
|
"description": "SOLID HEX. PLUG, NPT(M), 3000LB, ASTM A105",
|
||||||
|
"category": "FITTING",
|
||||||
|
"size": "1 1/2\"",
|
||||||
|
"material_grade": "ASTM A105",
|
||||||
|
"quantity": 1,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 5366,
|
||||||
|
"description": "CAP, SW, 3000LB, ASTM A105",
|
||||||
|
"category": "FITTING",
|
||||||
|
"size": "1\"",
|
||||||
|
"material_grade": "ASTM A105",
|
||||||
|
"quantity": 1,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 5367,
|
||||||
|
"description": "CAP, SW, 3000LB, ASTM A182 F304",
|
||||||
|
"category": "FITTING",
|
||||||
|
"size": "1\"",
|
||||||
|
"material_grade": "ASTM A182 F304",
|
||||||
|
"quantity": 1,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 5368,
|
||||||
|
"description": "SOLID HEX. PLUG, NPT(M), 3000LB, ASTM A105",
|
||||||
|
"category": "FITTING",
|
||||||
|
"size": "1/2\"",
|
||||||
|
"material_grade": "ASTM A105",
|
||||||
|
"quantity": 2,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 5370,
|
||||||
|
"description": "SOLID HEX. PLUG, NPT(M), 3000LB, ASTM A182 F304",
|
||||||
|
"category": "FITTING",
|
||||||
|
"size": "1/2\"",
|
||||||
|
"material_grade": "ASTM A182 F304",
|
||||||
|
"quantity": 1,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 5371,
|
||||||
|
"description": "CAP, SMLS, SCH 40, BW, ASTM A234 WPB",
|
||||||
|
"category": "FITTING",
|
||||||
|
"size": "2\"",
|
||||||
|
"material_grade": "ASTM A234 WPB",
|
||||||
|
"quantity": 1,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 5372,
|
||||||
|
"description": "CAP, SW, 3000LB, ASTM A105",
|
||||||
|
"category": "FITTING",
|
||||||
|
"size": "3/4\"",
|
||||||
|
"material_grade": "ASTM A105",
|
||||||
|
"quantity": 1,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 5373,
|
||||||
|
"description": "SOLID HEX. PLUG, NPT(M), 3000LB, ASTM A105",
|
||||||
|
"category": "FITTING",
|
||||||
|
"size": "3/4\"",
|
||||||
|
"material_grade": "ASTM A105",
|
||||||
|
"quantity": 36,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 5409,
|
||||||
|
"description": "SOLID HEX. PLUG, NPT(M), 3000LB, ASTM A182 F304",
|
||||||
|
"category": "FITTING",
|
||||||
|
"size": "3/4\"",
|
||||||
|
"material_grade": "ASTM A182 F304",
|
||||||
|
"quantity": 2,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 5426,
|
||||||
|
"description": "SOCK O LET, SW, 3000LB, ASTM A182 F304",
|
||||||
|
"category": "FITTING",
|
||||||
|
"size": "10\" x 1 1/2\"",
|
||||||
|
"material_grade": "ASTM A182 F304",
|
||||||
|
"quantity": 1,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 5427,
|
||||||
|
"description": "SOCK O LET, SW, 3000LB, ASTM A182 F304",
|
||||||
|
"category": "FITTING",
|
||||||
|
"size": "10\" x 1\"",
|
||||||
|
"material_grade": "ASTM A182 F304",
|
||||||
|
"quantity": 1,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 5428,
|
||||||
|
"description": "SOCK O LET, SW, 3000LB, ASTM A182 F304",
|
||||||
|
"category": "FITTING",
|
||||||
|
"size": "10\" x 3/4\"",
|
||||||
|
"material_grade": "ASTM A182 F304",
|
||||||
|
"quantity": 1,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 5429,
|
||||||
|
"description": "SOCK O LET, SW, 3000LB, ASTM A105",
|
||||||
|
"category": "FITTING",
|
||||||
|
"size": "2\" x 1/2\"",
|
||||||
|
"material_grade": "ASTM A105",
|
||||||
|
"quantity": 2,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 5431,
|
||||||
|
"description": "ELL O LET, SW, 3000LB, ASTM A105",
|
||||||
|
"category": "FITTING",
|
||||||
|
"size": "2\" x 3/4\"",
|
||||||
|
"material_grade": "ASTM A105",
|
||||||
|
"quantity": 1,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 5432,
|
||||||
|
"description": "ELL O LET, SW, 3000LB, ASTM A105",
|
||||||
|
"category": "FITTING",
|
||||||
|
"size": "3\" x 3/4\"",
|
||||||
|
"material_grade": "ASTM A105",
|
||||||
|
"quantity": 1,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 5433,
|
||||||
|
"description": "SOCK O LET, SW, 3000LB, ASTM A105",
|
||||||
|
"category": "FITTING",
|
||||||
|
"size": "3\" x 3/4\"",
|
||||||
|
"material_grade": "ASTM A105",
|
||||||
|
"quantity": 9,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 5442,
|
||||||
|
"description": "SOCK O LET, SW, 3000LB, ASTM A182 F304",
|
||||||
|
"category": "FITTING",
|
||||||
|
"size": "3\" x 3/4\"",
|
||||||
|
"material_grade": "ASTM A182 F304",
|
||||||
|
"quantity": 3,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 5445,
|
||||||
|
"description": "SOCK O LET, SW, 3000LB, ASTM A105",
|
||||||
|
"category": "FITTING",
|
||||||
|
"size": "3\" x 1\"",
|
||||||
|
"material_grade": "ASTM A105",
|
||||||
|
"quantity": 1,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 5446,
|
||||||
|
"description": "SOCK O LET, SW, 3000LB, ASTM A105",
|
||||||
|
"category": "FITTING",
|
||||||
|
"size": "4\" x 3/4\"",
|
||||||
|
"material_grade": "ASTM A105",
|
||||||
|
"quantity": 1,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 5447,
|
||||||
|
"description": "SOCK O LET, SW, 3000LB, ASTM A105",
|
||||||
|
"category": "FITTING",
|
||||||
|
"size": "6\" x 1 1/2\"",
|
||||||
|
"material_grade": "ASTM A105",
|
||||||
|
"quantity": 2,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 5449,
|
||||||
|
"description": "SOCK O LET, SW, 3000LB, ASTM A105",
|
||||||
|
"category": "FITTING",
|
||||||
|
"size": "6\" x 3/4\"",
|
||||||
|
"material_grade": "ASTM A105",
|
||||||
|
"quantity": 1,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"grouped_materials": []
|
||||||
|
}
|
||||||
BIN
backend/exports/PR-20251016-002.xlsx
Normal file
BIN
backend/exports/PR-20251016-002.xlsx
Normal file
Binary file not shown.
168
backend/exports/PR-20251016-003.json
Normal file
168
backend/exports/PR-20251016-003.json
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
{
|
||||||
|
"request_no": "PR-20251016-003",
|
||||||
|
"job_no": "2",
|
||||||
|
"created_at": "2025-10-16T06:01:25.896639",
|
||||||
|
"materials": [
|
||||||
|
{
|
||||||
|
"material_id": 7082,
|
||||||
|
"description": "PIPE, SMLS, SCH 40S, ASTM A312 TP304",
|
||||||
|
"category": "PIPE",
|
||||||
|
"size": "1/2\"",
|
||||||
|
"material_grade": "ASTM A312 TP304",
|
||||||
|
"quantity": 11,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 7090,
|
||||||
|
"description": "PIPE, SMLS, SCH 80, ASTM A106 B",
|
||||||
|
"category": "PIPE",
|
||||||
|
"size": "3/4\"",
|
||||||
|
"material_grade": "ASTM A106 B",
|
||||||
|
"quantity": 92,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 7094,
|
||||||
|
"description": "PIPE, SMLS, SCH 40S, ASTM A312 TP304",
|
||||||
|
"category": "PIPE",
|
||||||
|
"size": "1\"",
|
||||||
|
"material_grade": "ASTM A312 TP304",
|
||||||
|
"quantity": 23,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 7104,
|
||||||
|
"description": "PIPE, SMLS, SCH 80, ASTM A106 B",
|
||||||
|
"category": "PIPE",
|
||||||
|
"size": "1\"",
|
||||||
|
"material_grade": "ASTM A106 B",
|
||||||
|
"quantity": 139,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 7113,
|
||||||
|
"description": "PIPE, SMLS, SCH 40S, ASTM A312 TP304",
|
||||||
|
"category": "PIPE",
|
||||||
|
"size": "1 1/2\"",
|
||||||
|
"material_grade": "ASTM A312 TP304",
|
||||||
|
"quantity": 14,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 7127,
|
||||||
|
"description": "PIPE, SMLS, SCH 80, ASTM A106 B",
|
||||||
|
"category": "PIPE",
|
||||||
|
"size": "1 1/2\"",
|
||||||
|
"material_grade": "ASTM A106 B",
|
||||||
|
"quantity": 98,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 7383,
|
||||||
|
"description": "PIPE, SMLS, SCH 80, ASTM A106 B",
|
||||||
|
"category": "PIPE",
|
||||||
|
"size": "1/2\"",
|
||||||
|
"material_grade": "ASTM A106 B",
|
||||||
|
"quantity": 82,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 7465,
|
||||||
|
"description": "PIPE, SMLS, SCH 40S, ASTM A312 TP304",
|
||||||
|
"category": "PIPE",
|
||||||
|
"size": "10\"",
|
||||||
|
"material_grade": "ASTM A312 TP304",
|
||||||
|
"quantity": 4,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 7469,
|
||||||
|
"description": "PIPE, SMLS, SCH 40S, ASTM A312 TP304",
|
||||||
|
"category": "PIPE",
|
||||||
|
"size": "12\"",
|
||||||
|
"material_grade": "ASTM A312 TP304",
|
||||||
|
"quantity": 1,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 7470,
|
||||||
|
"description": "PIPE, SMLS, SCH 40, ASTM A106 B",
|
||||||
|
"category": "PIPE",
|
||||||
|
"size": "2\"",
|
||||||
|
"material_grade": "ASTM A106 B",
|
||||||
|
"quantity": 50,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 7520,
|
||||||
|
"description": "PIPE, SMLS, SCH 40S, ASTM A312 TP304",
|
||||||
|
"category": "PIPE",
|
||||||
|
"size": "2\"",
|
||||||
|
"material_grade": "ASTM A312 TP304",
|
||||||
|
"quantity": 9,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 7529,
|
||||||
|
"description": "PIPE, SMLS, SCH 40, ASTM A106 B",
|
||||||
|
"category": "PIPE",
|
||||||
|
"size": "3\"",
|
||||||
|
"material_grade": "ASTM A106 B",
|
||||||
|
"quantity": 25,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 7537,
|
||||||
|
"description": "PIPE, SMLS, SCH 40S, ASTM A312 TP304",
|
||||||
|
"category": "PIPE",
|
||||||
|
"size": "3\"",
|
||||||
|
"material_grade": "ASTM A312 TP304",
|
||||||
|
"quantity": 8,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 7562,
|
||||||
|
"description": "PIPE, SMLS, SCH 40S, ASTM A312 TP304",
|
||||||
|
"category": "PIPE",
|
||||||
|
"size": "3/4\"",
|
||||||
|
"material_grade": "ASTM A312 TP304",
|
||||||
|
"quantity": 15,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 7665,
|
||||||
|
"description": "PIPE, SMLS, SCH 40, ASTM A106 B",
|
||||||
|
"category": "PIPE",
|
||||||
|
"size": "4\"",
|
||||||
|
"material_grade": "ASTM A106 B",
|
||||||
|
"quantity": 12,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 7677,
|
||||||
|
"description": "PIPE, SMLS, SCH 40, ASTM A106 B",
|
||||||
|
"category": "PIPE",
|
||||||
|
"size": "6\"",
|
||||||
|
"material_grade": "ASTM A106 B",
|
||||||
|
"quantity": 13,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"grouped_materials": []
|
||||||
|
}
|
||||||
BIN
backend/exports/PR-20251016-003.xlsx
Normal file
BIN
backend/exports/PR-20251016-003.xlsx
Normal file
Binary file not shown.
408
backend/exports/PR-20251016-004.json
Normal file
408
backend/exports/PR-20251016-004.json
Normal file
@@ -0,0 +1,408 @@
|
|||||||
|
{
|
||||||
|
"request_no": "PR-20251016-004",
|
||||||
|
"job_no": "5",
|
||||||
|
"created_at": "2025-10-16T05:24:45.921468",
|
||||||
|
"materials": [
|
||||||
|
{
|
||||||
|
"material_id": 118834,
|
||||||
|
"description": "FLG WELD NECK RF SCH 40S, 150LB, ASTM A182 F304",
|
||||||
|
"category": "FLANGE",
|
||||||
|
"size": "10\"",
|
||||||
|
"material_grade": "ASTM A182 F304",
|
||||||
|
"quantity": 5,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 118839,
|
||||||
|
"description": "FLG WELD NECK RF SCH 40S, 150LB, ASTM A182 F304",
|
||||||
|
"category": "FLANGE",
|
||||||
|
"size": "12\"",
|
||||||
|
"material_grade": "ASTM A182 F304",
|
||||||
|
"quantity": 1,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 118840,
|
||||||
|
"description": "FLG WELD NECK RF SCH 40, 150LB, ASTM A105",
|
||||||
|
"category": "FLANGE",
|
||||||
|
"size": "2\"",
|
||||||
|
"material_grade": "ASTM A105",
|
||||||
|
"quantity": 36,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 118876,
|
||||||
|
"description": "FLG WELD NECK RF SCH 40, 300LB, ASTM A105",
|
||||||
|
"category": "FLANGE",
|
||||||
|
"size": "2\"",
|
||||||
|
"material_grade": "ASTM A105",
|
||||||
|
"quantity": 6,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 118882,
|
||||||
|
"description": "FLG WELD NECK RF SCH 40S, 150LB, ASTM A182 F304",
|
||||||
|
"category": "FLANGE",
|
||||||
|
"size": "2\"",
|
||||||
|
"material_grade": "ASTM A182 F304",
|
||||||
|
"quantity": 7,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 118890,
|
||||||
|
"description": "FLG WELD NECK RF SCH 40, 600LB, ASTM A105",
|
||||||
|
"category": "FLANGE",
|
||||||
|
"size": "3\"",
|
||||||
|
"material_grade": "ASTM A105",
|
||||||
|
"quantity": 8,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 118895,
|
||||||
|
"description": "FLG WELD NECK RF SCH 40S, 600LB, ASTM A182 F304",
|
||||||
|
"category": "FLANGE",
|
||||||
|
"size": "3\"",
|
||||||
|
"material_grade": "ASTM A182 F304",
|
||||||
|
"quantity": 2,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 118897,
|
||||||
|
"description": "FLG WELD NECK RF SCH 40, 150LB, ASTM A105",
|
||||||
|
"category": "FLANGE",
|
||||||
|
"size": "3\"",
|
||||||
|
"material_grade": "ASTM A105",
|
||||||
|
"quantity": 12,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 118909,
|
||||||
|
"description": "FLG WELD NECK RF SCH 40, 300LB, ASTM A105",
|
||||||
|
"category": "FLANGE",
|
||||||
|
"size": "3\"",
|
||||||
|
"material_grade": "ASTM A105",
|
||||||
|
"quantity": 2,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 118914,
|
||||||
|
"description": "FLG WELD NECK RF SCH 40S, 150LB, ASTM A182 F304",
|
||||||
|
"category": "FLANGE",
|
||||||
|
"size": "3\"",
|
||||||
|
"material_grade": "ASTM A182 F304",
|
||||||
|
"quantity": 9,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 118923,
|
||||||
|
"description": "FLG WELD NECK RF SCH 40, 300LB, ASTM A105",
|
||||||
|
"category": "FLANGE",
|
||||||
|
"size": "4\"",
|
||||||
|
"material_grade": "ASTM A105",
|
||||||
|
"quantity": 3,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 118926,
|
||||||
|
"description": "FLG WELD NECK RF SCH 40, 150LB, ASTM A105",
|
||||||
|
"category": "FLANGE",
|
||||||
|
"size": "6\"",
|
||||||
|
"material_grade": "ASTM A105",
|
||||||
|
"quantity": 10,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 118936,
|
||||||
|
"description": "FLG SWRF SCH 40S, 150LB, ASTM A182 F304",
|
||||||
|
"category": "FLANGE",
|
||||||
|
"size": "1/2\"",
|
||||||
|
"material_grade": "ASTM A182 F304",
|
||||||
|
"quantity": 14,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 118948,
|
||||||
|
"description": "FLG SWRF SCH 80, 150LB, ASTM A105",
|
||||||
|
"category": "FLANGE",
|
||||||
|
"size": "3/4\"",
|
||||||
|
"material_grade": "ASTM A105",
|
||||||
|
"quantity": 15,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 118952,
|
||||||
|
"description": "FLG SWRF SCH 40S, 150LB, ASTM A182 F304",
|
||||||
|
"category": "FLANGE",
|
||||||
|
"size": "1\"",
|
||||||
|
"material_grade": "ASTM A182 F304",
|
||||||
|
"quantity": 9,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 118953,
|
||||||
|
"description": "FLG SWRF SCH 80, 150LB, ASTM A105",
|
||||||
|
"category": "FLANGE",
|
||||||
|
"size": "1\"",
|
||||||
|
"material_grade": "ASTM A105",
|
||||||
|
"quantity": 66,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 118957,
|
||||||
|
"description": "FLG SWRF SCH 80, 600LB, ASTM A105",
|
||||||
|
"category": "FLANGE",
|
||||||
|
"size": "1\"",
|
||||||
|
"material_grade": "ASTM A105",
|
||||||
|
"quantity": 6,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 118959,
|
||||||
|
"description": "FLG SWRF SCH 40S, 150LB, ASTM A182 F304",
|
||||||
|
"category": "FLANGE",
|
||||||
|
"size": "1 1/2\"",
|
||||||
|
"material_grade": "ASTM A182 F304",
|
||||||
|
"quantity": 8,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 118967,
|
||||||
|
"description": "FLG SWRF SCH 80, 150LB, ASTM A105",
|
||||||
|
"category": "FLANGE",
|
||||||
|
"size": "1 1/2\"",
|
||||||
|
"material_grade": "ASTM A105",
|
||||||
|
"quantity": 40,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 119003,
|
||||||
|
"description": "FLG SWRF SCH 80, 600LB, ASTM A105",
|
||||||
|
"category": "FLANGE",
|
||||||
|
"size": "1 1/2\"",
|
||||||
|
"material_grade": "ASTM A105",
|
||||||
|
"quantity": 5,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 119008,
|
||||||
|
"description": "RED. FLG SWRF SCH 40S, 150LB, ASTM A182 F304",
|
||||||
|
"category": "FLANGE",
|
||||||
|
"size": "1 1/2\" x 3/4\"",
|
||||||
|
"material_grade": "ASTM A182 F304",
|
||||||
|
"quantity": 2,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 119010,
|
||||||
|
"description": "RED. FLG SWRF SCH 80, 150LB, ASTM A105",
|
||||||
|
"category": "FLANGE",
|
||||||
|
"size": "1 1/2\" x 3/4\"",
|
||||||
|
"material_grade": "ASTM A105",
|
||||||
|
"quantity": 4,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 119014,
|
||||||
|
"description": "RED. FLG SWRF SCH 80, 600LB, ASTM A105",
|
||||||
|
"category": "FLANGE",
|
||||||
|
"size": "1 1/2\" x 3/4\"",
|
||||||
|
"material_grade": "ASTM A105",
|
||||||
|
"quantity": 2,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 119024,
|
||||||
|
"description": "FLG SWRF SCH 40S, 600LB, ASTM A182 F304",
|
||||||
|
"category": "FLANGE",
|
||||||
|
"size": "1\"",
|
||||||
|
"material_grade": "ASTM A182 F304",
|
||||||
|
"quantity": 1,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 119092,
|
||||||
|
"description": "RED. FLG SWRF SCH 80, 150LB, ASTM A105",
|
||||||
|
"category": "FLANGE",
|
||||||
|
"size": "1\" x 3/4\"",
|
||||||
|
"material_grade": "ASTM A105",
|
||||||
|
"quantity": 1,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 119097,
|
||||||
|
"description": "FLG SWRF SCH 80, 300LB, ASTM A105",
|
||||||
|
"category": "FLANGE",
|
||||||
|
"size": "1 1/2\"",
|
||||||
|
"material_grade": "ASTM A105",
|
||||||
|
"quantity": 3,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 119102,
|
||||||
|
"description": "FLG SWRF SCH 80, 150LB, ASTM A105",
|
||||||
|
"category": "FLANGE",
|
||||||
|
"size": "1/2\"",
|
||||||
|
"material_grade": "ASTM A105",
|
||||||
|
"quantity": 57,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 119159,
|
||||||
|
"description": "FLG SWRF SCH 40S, 150LB, ASTM A182 F304",
|
||||||
|
"category": "FLANGE",
|
||||||
|
"size": "3/4\"",
|
||||||
|
"material_grade": "ASTM A182 F304",
|
||||||
|
"quantity": 7,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 119166,
|
||||||
|
"description": "FLG SWRF SCH 40S, 300LB, ASTM A182 F304",
|
||||||
|
"category": "FLANGE",
|
||||||
|
"size": "3/4\"",
|
||||||
|
"material_grade": "ASTM A182 F304",
|
||||||
|
"quantity": 1,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 119167,
|
||||||
|
"description": "FLG SWRF SCH 40S, 600LB, ASTM A182 F304",
|
||||||
|
"category": "FLANGE",
|
||||||
|
"size": "3/4\"",
|
||||||
|
"material_grade": "ASTM A182 F304",
|
||||||
|
"quantity": 1,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 119179,
|
||||||
|
"description": "FLG SWRF SCH 80, 300LB, ASTM A105",
|
||||||
|
"category": "FLANGE",
|
||||||
|
"size": "3/4\"",
|
||||||
|
"material_grade": "ASTM A105",
|
||||||
|
"quantity": 2,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 119181,
|
||||||
|
"description": "FLG SWRF SCH 80, 600LB, ASTM A105",
|
||||||
|
"category": "FLANGE",
|
||||||
|
"size": "3/4\"",
|
||||||
|
"material_grade": "ASTM A105",
|
||||||
|
"quantity": 5,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 119985,
|
||||||
|
"description": "ORIFICE, 150LB",
|
||||||
|
"category": "FLANGE",
|
||||||
|
"size": "10\"",
|
||||||
|
"material_grade": "-",
|
||||||
|
"quantity": 1,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 119987,
|
||||||
|
"description": "WOOD ORIFICE, 300LB",
|
||||||
|
"category": "FLANGE",
|
||||||
|
"size": "10\"",
|
||||||
|
"material_grade": "-",
|
||||||
|
"quantity": 2,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 119989,
|
||||||
|
"description": "WOOD ORIFICE, 600LB",
|
||||||
|
"category": "FLANGE",
|
||||||
|
"size": "3\"",
|
||||||
|
"material_grade": "-",
|
||||||
|
"quantity": 1,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 119990,
|
||||||
|
"description": "WOOD ORIFICE, 300LB",
|
||||||
|
"category": "FLANGE",
|
||||||
|
"size": "4\"",
|
||||||
|
"material_grade": "-",
|
||||||
|
"quantity": 2,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 119992,
|
||||||
|
"description": "WOOD ORIFICE, 300LB",
|
||||||
|
"category": "FLANGE",
|
||||||
|
"size": "5\"",
|
||||||
|
"material_grade": "-",
|
||||||
|
"quantity": 1,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 119993,
|
||||||
|
"description": "WOOD ORIFICE, 600LB",
|
||||||
|
"category": "FLANGE",
|
||||||
|
"size": "5\"",
|
||||||
|
"material_grade": "-",
|
||||||
|
"quantity": 1,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 119994,
|
||||||
|
"description": "WOOD ORIFICE, 150LB",
|
||||||
|
"category": "FLANGE",
|
||||||
|
"size": "6\"",
|
||||||
|
"material_grade": "-",
|
||||||
|
"quantity": 2,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"material_id": 119996,
|
||||||
|
"description": "WOOD ORIFICE, 300LB",
|
||||||
|
"category": "FLANGE",
|
||||||
|
"size": "8\"",
|
||||||
|
"material_grade": "-",
|
||||||
|
"quantity": 2,
|
||||||
|
"unit": "EA",
|
||||||
|
"user_requirement": ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"grouped_materials": []
|
||||||
|
}
|
||||||
BIN
backend/exports/PR-20251016-004.xlsx
Normal file
BIN
backend/exports/PR-20251016-004.xlsx
Normal file
Binary file not shown.
@@ -10,6 +10,8 @@ const BoltMaterialsView = ({
|
|||||||
userRequirements,
|
userRequirements,
|
||||||
setUserRequirements,
|
setUserRequirements,
|
||||||
purchasedMaterials,
|
purchasedMaterials,
|
||||||
|
onPurchasedMaterialsUpdate,
|
||||||
|
jobNo,
|
||||||
fileId,
|
fileId,
|
||||||
user
|
user
|
||||||
}) => {
|
}) => {
|
||||||
@@ -45,11 +47,24 @@ const BoltMaterialsView = ({
|
|||||||
|
|
||||||
const parseBoltInfo = (material) => {
|
const parseBoltInfo = (material) => {
|
||||||
const qty = Math.round(material.quantity || 0);
|
const qty = Math.round(material.quantity || 0);
|
||||||
const safetyQty = Math.ceil(qty * 1.05); // 5% 여유율
|
|
||||||
const purchaseQty = Math.ceil(safetyQty / 4) * 4; // 4의 배수
|
|
||||||
|
|
||||||
// 볼트 상세 정보 우선 사용
|
// 플랜지당 볼트 세트 수 추출 (예: (8), (4))
|
||||||
const boltDetails = material.bolt_details || {};
|
const boltDetails = material.bolt_details || {};
|
||||||
|
let boltsPerFlange = boltDetails.dimensions?.bolts_per_flange || 1;
|
||||||
|
|
||||||
|
// 백엔드에서 정보가 없으면 원본 설명에서 직접 추출
|
||||||
|
if (boltsPerFlange === 1) {
|
||||||
|
const description = material.original_description || '';
|
||||||
|
const flangePattern = description.match(/\((\d+)\)/);
|
||||||
|
if (flangePattern) {
|
||||||
|
boltsPerFlange = parseInt(flangePattern[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 실제 필요한 볼트 수 = 플랜지 수 × 플랜지당 볼트 세트 수
|
||||||
|
const totalBoltsNeeded = qty * boltsPerFlange;
|
||||||
|
const safetyQty = Math.ceil(totalBoltsNeeded * 1.05); // 5% 여유율
|
||||||
|
const purchaseQty = Math.ceil(safetyQty / 4) * 4; // 4의 배수
|
||||||
|
|
||||||
// 길이 정보 (bolt_details 우선, 없으면 원본 설명에서 추출)
|
// 길이 정보 (bolt_details 우선, 없으면 원본 설명에서 추출)
|
||||||
let boltLength = '-';
|
let boltLength = '-';
|
||||||
@@ -113,19 +128,32 @@ const BoltMaterialsView = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 추가요구사항 추출 (ELEC.GALV 등)
|
// 압력 등급 추출 (150LB 등)
|
||||||
const additionalReq = extractBoltAdditionalRequirements(material.original_description || '');
|
let boltPressure = '-';
|
||||||
|
const description = material.original_description || '';
|
||||||
|
const pressureMatch = description.match(/(\d+)\s*LB/i);
|
||||||
|
if (pressureMatch) {
|
||||||
|
boltPressure = `${pressureMatch[1]}LB`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// User Requirements 추출 (ELEC.GALV 등)
|
||||||
|
const userRequirements = extractBoltAdditionalRequirements(material.original_description || '');
|
||||||
|
|
||||||
|
// Purchase Quantity (Quantity + Unit 통합) - 플랜지당 볼트 세트 수 정보 포함
|
||||||
|
const purchaseQuantity = boltsPerFlange > 1
|
||||||
|
? `${purchaseQty} SETS (${boltsPerFlange}/flange)`
|
||||||
|
: `${purchaseQty} SETS`;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: 'BOLT',
|
type: 'BOLT',
|
||||||
subtype: boltSubtype,
|
subtype: boltSubtype,
|
||||||
size: material.size_spec || material.main_nom || '-',
|
size: material.size_spec || material.main_nom || '-',
|
||||||
pressure: '-', // 볼트는 압력 등급 없음
|
pressure: boltPressure, // 압력 등급 (150LB 등)
|
||||||
schedule: boltLength, // 길이 정보
|
schedule: boltLength, // 길이 정보
|
||||||
grade: boltGrade,
|
grade: boltGrade,
|
||||||
additionalReq: additionalReq, // 추가요구사항
|
userRequirements: userRequirements, // User Requirements (ELEC.GALV 등)
|
||||||
quantity: purchaseQty,
|
additionalReq: '-', // 추가요구사항 (사용자 입력)
|
||||||
unit: 'SETS'
|
purchaseQuantity: purchaseQuantity // 구매수량 (통합)
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -228,29 +256,83 @@ const BoltMaterialsView = ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await api.post('/files/save-excel', {
|
console.log('🔄 볼트 엑셀 내보내기 시작 - 새로운 방식');
|
||||||
|
|
||||||
|
// 1. 먼저 클라이언트에서 엑셀 파일 생성
|
||||||
|
console.log('📊 엑셀 Blob 생성 중...', dataWithRequirements.length, '개 자료');
|
||||||
|
const excelBlob = await createExcelBlob(dataWithRequirements, excelFileName, {
|
||||||
|
category: 'BOLT',
|
||||||
|
filename: excelFileName,
|
||||||
|
uploadDate: new Date().toLocaleDateString()
|
||||||
|
});
|
||||||
|
console.log('✅ 엑셀 Blob 생성 완료:', excelBlob.size, 'bytes');
|
||||||
|
|
||||||
|
// 2. 구매신청 생성
|
||||||
|
const allMaterialIds = selectedMaterialsData.map(m => m.id);
|
||||||
|
const response = await api.post('/purchase-request/create', {
|
||||||
file_id: fileId,
|
file_id: fileId,
|
||||||
|
job_no: jobNo,
|
||||||
category: 'BOLT',
|
category: 'BOLT',
|
||||||
materials: dataWithRequirements,
|
material_ids: allMaterialIds,
|
||||||
filename: excelFileName,
|
materials_data: dataWithRequirements.map(m => ({
|
||||||
user_id: user?.id
|
material_id: m.id,
|
||||||
|
description: m.original_description,
|
||||||
|
category: m.classified_category,
|
||||||
|
size: m.size_inch || m.size_spec,
|
||||||
|
schedule: m.schedule,
|
||||||
|
material_grade: m.material_grade || m.full_material_grade,
|
||||||
|
quantity: m.quantity,
|
||||||
|
unit: m.unit,
|
||||||
|
user_requirement: userRequirements[m.id] || ''
|
||||||
|
}))
|
||||||
});
|
});
|
||||||
|
|
||||||
exportMaterialsToExcel(dataWithRequirements, excelFileName, {
|
if (response.data.success) {
|
||||||
category: 'BOLT',
|
console.log(`✅ 구매신청 완료: ${response.data.request_no}, request_id: ${response.data.request_id}`);
|
||||||
filename: excelFileName,
|
|
||||||
uploadDate: new Date().toLocaleDateString()
|
|
||||||
});
|
|
||||||
|
|
||||||
alert('엑셀 파일이 생성되고 서버에 저장되었습니다.');
|
// 3. 엑셀 파일을 서버에 업로드
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('excel_file', excelBlob, excelFileName);
|
||||||
|
formData.append('request_id', response.data.request_id);
|
||||||
|
formData.append('category', 'BOLT');
|
||||||
|
|
||||||
|
console.log('📤 엑셀 파일 서버 업로드 중...');
|
||||||
|
await api.post('/purchase-request/upload-excel', formData, {
|
||||||
|
headers: { 'Content-Type': 'multipart/form-data' }
|
||||||
|
});
|
||||||
|
console.log('✅ 엑셀 파일 서버 업로드 완료');
|
||||||
|
|
||||||
|
// 4. 구매된 자재 목록 업데이트 (비활성화)
|
||||||
|
onPurchasedMaterialsUpdate(allMaterialIds);
|
||||||
|
console.log('✅ 구매된 자재 목록 업데이트 완료');
|
||||||
|
|
||||||
|
// 5. 클라이언트에 파일 다운로드
|
||||||
|
const url = window.URL.createObjectURL(excelBlob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.download = excelFileName;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
|
||||||
|
alert(`구매신청 ${response.data?.request_no || ''}이 생성되고 엑셀 파일이 저장되었습니다.`);
|
||||||
|
} else {
|
||||||
|
throw new Error(response.data?.message || '구매신청 생성 실패');
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('엑셀 저장 실패:', error);
|
console.error('엑셀 저장 또는 구매신청 실패:', error);
|
||||||
|
// 실패 시에도 클라이언트 다운로드는 진행
|
||||||
exportMaterialsToExcel(dataWithRequirements, excelFileName, {
|
exportMaterialsToExcel(dataWithRequirements, excelFileName, {
|
||||||
category: 'BOLT',
|
category: 'BOLT',
|
||||||
filename: excelFileName,
|
filename: excelFileName,
|
||||||
uploadDate: new Date().toLocaleDateString()
|
uploadDate: new Date().toLocaleDateString()
|
||||||
});
|
});
|
||||||
|
alert('엑셀 파일은 다운로드되었지만 구매신청 생성에 실패했습니다.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 선택 해제
|
||||||
|
setSelectedMaterials(new Set());
|
||||||
};
|
};
|
||||||
|
|
||||||
const filteredMaterials = getFilteredAndSortedMaterials();
|
const filteredMaterials = getFilteredAndSortedMaterials();
|
||||||
@@ -317,21 +399,24 @@ const BoltMaterialsView = ({
|
|||||||
<div style={{
|
<div style={{
|
||||||
background: 'white',
|
background: 'white',
|
||||||
borderRadius: '12px',
|
borderRadius: '12px',
|
||||||
overflow: 'hidden',
|
overflow: 'auto',
|
||||||
|
maxHeight: '600px',
|
||||||
boxShadow: '0 1px 3px rgba(0, 0, 0, 0.1)'
|
boxShadow: '0 1px 3px rgba(0, 0, 0, 0.1)'
|
||||||
}}>
|
}}>
|
||||||
{/* 헤더 */}
|
<div style={{ minWidth: '1500px' }}>
|
||||||
<div style={{
|
{/* 헤더 */}
|
||||||
display: 'grid',
|
<div style={{
|
||||||
gridTemplateColumns: '50px 200px 120px 100px 120px 150px 80px 80px 200px',
|
display: 'grid',
|
||||||
gap: '16px',
|
gridTemplateColumns: '50px 200px 120px 100px 120px 150px 180px 150px 200px',
|
||||||
padding: '16px',
|
gap: '16px',
|
||||||
background: '#f8fafc',
|
padding: '16px',
|
||||||
borderBottom: '1px solid #e2e8f0',
|
background: '#f8fafc',
|
||||||
fontSize: '14px',
|
borderBottom: '1px solid #e2e8f0',
|
||||||
fontWeight: '600',
|
fontSize: '14px',
|
||||||
color: '#374151'
|
fontWeight: '600',
|
||||||
}}>
|
color: '#374151',
|
||||||
|
textAlign: 'center'
|
||||||
|
}}>
|
||||||
<div>
|
<div>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@@ -343,18 +428,83 @@ const BoltMaterialsView = ({
|
|||||||
style={{ cursor: 'pointer' }}
|
style={{ cursor: 'pointer' }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<FilterableHeader sortKey="subtype" filterKey="subtype">Type</FilterableHeader>
|
<FilterableHeader
|
||||||
<FilterableHeader sortKey="size" filterKey="size">Size</FilterableHeader>
|
sortKey="subtype"
|
||||||
<FilterableHeader sortKey="pressure" filterKey="pressure">Pressure</FilterableHeader>
|
filterKey="subtype"
|
||||||
<FilterableHeader sortKey="schedule" filterKey="schedule">Length</FilterableHeader>
|
sortConfig={sortConfig}
|
||||||
<FilterableHeader sortKey="grade" filterKey="grade">Material Grade</FilterableHeader>
|
onSort={handleSort}
|
||||||
<FilterableHeader sortKey="quantity" filterKey="quantity">Quantity</FilterableHeader>
|
columnFilters={columnFilters}
|
||||||
<div>Unit</div>
|
onFilterChange={setColumnFilters}
|
||||||
<div>User Requirement</div>
|
showFilterDropdown={showFilterDropdown}
|
||||||
</div>
|
setShowFilterDropdown={setShowFilterDropdown}
|
||||||
|
>
|
||||||
|
Type
|
||||||
|
</FilterableHeader>
|
||||||
|
<FilterableHeader
|
||||||
|
sortKey="size"
|
||||||
|
filterKey="size"
|
||||||
|
sortConfig={sortConfig}
|
||||||
|
onSort={handleSort}
|
||||||
|
columnFilters={columnFilters}
|
||||||
|
onFilterChange={setColumnFilters}
|
||||||
|
showFilterDropdown={showFilterDropdown}
|
||||||
|
setShowFilterDropdown={setShowFilterDropdown}
|
||||||
|
>
|
||||||
|
Size
|
||||||
|
</FilterableHeader>
|
||||||
|
<FilterableHeader
|
||||||
|
sortKey="pressure"
|
||||||
|
filterKey="pressure"
|
||||||
|
sortConfig={sortConfig}
|
||||||
|
onSort={handleSort}
|
||||||
|
columnFilters={columnFilters}
|
||||||
|
onFilterChange={setColumnFilters}
|
||||||
|
showFilterDropdown={showFilterDropdown}
|
||||||
|
setShowFilterDropdown={setShowFilterDropdown}
|
||||||
|
>
|
||||||
|
Pressure
|
||||||
|
</FilterableHeader>
|
||||||
|
<FilterableHeader
|
||||||
|
sortKey="schedule"
|
||||||
|
filterKey="schedule"
|
||||||
|
sortConfig={sortConfig}
|
||||||
|
onSort={handleSort}
|
||||||
|
columnFilters={columnFilters}
|
||||||
|
onFilterChange={setColumnFilters}
|
||||||
|
showFilterDropdown={showFilterDropdown}
|
||||||
|
setShowFilterDropdown={setShowFilterDropdown}
|
||||||
|
>
|
||||||
|
Length
|
||||||
|
</FilterableHeader>
|
||||||
|
<FilterableHeader
|
||||||
|
sortKey="grade"
|
||||||
|
filterKey="grade"
|
||||||
|
sortConfig={sortConfig}
|
||||||
|
onSort={handleSort}
|
||||||
|
columnFilters={columnFilters}
|
||||||
|
onFilterChange={setColumnFilters}
|
||||||
|
showFilterDropdown={showFilterDropdown}
|
||||||
|
setShowFilterDropdown={setShowFilterDropdown}
|
||||||
|
>
|
||||||
|
Material Grade
|
||||||
|
</FilterableHeader>
|
||||||
|
<div>User Requirements</div>
|
||||||
|
<div>Additional Request</div>
|
||||||
|
<FilterableHeader
|
||||||
|
sortKey="purchaseQuantity"
|
||||||
|
filterKey="purchaseQuantity"
|
||||||
|
sortConfig={sortConfig}
|
||||||
|
onSort={handleSort}
|
||||||
|
columnFilters={columnFilters}
|
||||||
|
onFilterChange={setColumnFilters}
|
||||||
|
showFilterDropdown={showFilterDropdown}
|
||||||
|
setShowFilterDropdown={setShowFilterDropdown}
|
||||||
|
>
|
||||||
|
Purchase Quantity
|
||||||
|
</FilterableHeader>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* 데이터 행들 */}
|
{/* 데이터 행들 */}
|
||||||
<div style={{ maxHeight: '600px', overflowY: 'auto' }}>
|
|
||||||
{filteredMaterials.map((material, index) => {
|
{filteredMaterials.map((material, index) => {
|
||||||
const info = parseBoltInfo(material);
|
const info = parseBoltInfo(material);
|
||||||
const isSelected = selectedMaterials.has(material.id);
|
const isSelected = selectedMaterials.has(material.id);
|
||||||
@@ -365,7 +515,7 @@ const BoltMaterialsView = ({
|
|||||||
key={material.id}
|
key={material.id}
|
||||||
style={{
|
style={{
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
gridTemplateColumns: '50px 200px 120px 100px 120px 150px 80px 80px 200px',
|
gridTemplateColumns: '50px 200px 120px 100px 120px 150px 180px 150px 200px',
|
||||||
gap: '16px',
|
gap: '16px',
|
||||||
padding: '16px',
|
padding: '16px',
|
||||||
borderBottom: index < filteredMaterials.length - 1 ? '1px solid #f1f5f9' : 'none',
|
borderBottom: index < filteredMaterials.length - 1 ? '1px solid #f1f5f9' : 'none',
|
||||||
@@ -411,23 +561,24 @@ const BoltMaterialsView = ({
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: '14px', color: '#1f2937' }}>
|
<div style={{ fontSize: '14px', color: '#1f2937', textAlign: 'center' }}>
|
||||||
{info.size}
|
{info.size}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: '14px', color: '#1f2937' }}>
|
<div style={{ fontSize: '14px', color: '#1f2937', textAlign: 'center' }}>
|
||||||
{info.pressure}
|
{info.pressure}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: '14px', color: '#1f2937' }}>
|
<div style={{ fontSize: '14px', color: '#1f2937', textAlign: 'center' }}>
|
||||||
{info.schedule}
|
{info.schedule}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: '14px', color: '#1f2937' }}>
|
<div style={{ fontSize: '14px', color: '#1f2937', textAlign: 'center' }}>
|
||||||
{info.grade}
|
{info.grade}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: '14px', color: '#1f2937', fontWeight: '600', textAlign: 'right' }}>
|
<div style={{
|
||||||
{info.quantity}
|
fontSize: '14px',
|
||||||
</div>
|
color: '#1f2937',
|
||||||
<div style={{ fontSize: '14px', color: '#6b7280' }}>
|
textAlign: 'center'
|
||||||
{info.unit}
|
}}>
|
||||||
|
{info.userRequirements}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<input
|
<input
|
||||||
@@ -437,7 +588,7 @@ const BoltMaterialsView = ({
|
|||||||
...userRequirements,
|
...userRequirements,
|
||||||
[material.id]: e.target.value
|
[material.id]: e.target.value
|
||||||
})}
|
})}
|
||||||
placeholder="Enter requirement..."
|
placeholder="Enter additional request..."
|
||||||
style={{
|
style={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
padding: '6px 8px',
|
padding: '6px 8px',
|
||||||
@@ -447,6 +598,9 @@ const BoltMaterialsView = ({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div style={{ fontSize: '14px', color: '#1f2937', textAlign: 'center' }}>
|
||||||
|
{info.purchaseQuantity}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ const SupportMaterialsView = ({
|
|||||||
userRequirements,
|
userRequirements,
|
||||||
setUserRequirements,
|
setUserRequirements,
|
||||||
purchasedMaterials,
|
purchasedMaterials,
|
||||||
|
onPurchasedMaterialsUpdate,
|
||||||
|
jobNo,
|
||||||
fileId,
|
fileId,
|
||||||
user
|
user
|
||||||
}) => {
|
}) => {
|
||||||
@@ -19,33 +21,96 @@ const SupportMaterialsView = ({
|
|||||||
|
|
||||||
const parseSupportInfo = (material) => {
|
const parseSupportInfo = (material) => {
|
||||||
const desc = material.original_description || '';
|
const desc = material.original_description || '';
|
||||||
const isUrethaneBlock = desc.includes('URETHANE') || desc.includes('BLOCK SHOE') || desc.includes('우레탄');
|
const descUpper = desc.toUpperCase();
|
||||||
const isClamp = desc.includes('CLAMP') || desc.includes('클램프');
|
|
||||||
|
|
||||||
let subtypeText = '';
|
// 서포트 타입 분류 (백엔드 분류기와 동일한 로직)
|
||||||
if (isUrethaneBlock) {
|
let supportType = 'U-BOLT'; // 기본값
|
||||||
subtypeText = '우레탄블럭슈';
|
|
||||||
} else if (isClamp) {
|
if (descUpper.includes('URETHANE') || descUpper.includes('BLOCK SHOE') || descUpper.includes('우레탄')) {
|
||||||
subtypeText = '클램프';
|
supportType = 'URETHANE BLOCK SHOE';
|
||||||
} else {
|
} else if (descUpper.includes('CLAMP') || descUpper.includes('클램프')) {
|
||||||
subtypeText = '유볼트';
|
supportType = 'CLAMP';
|
||||||
|
} else if (descUpper.includes('HANGER') || descUpper.includes('행거')) {
|
||||||
|
supportType = 'HANGER';
|
||||||
|
} else if (descUpper.includes('SPRING') || descUpper.includes('스프링')) {
|
||||||
|
supportType = 'SPRING HANGER';
|
||||||
|
} else if (descUpper.includes('GUIDE') || descUpper.includes('가이드')) {
|
||||||
|
supportType = 'GUIDE';
|
||||||
|
} else if (descUpper.includes('ANCHOR') || descUpper.includes('앵커')) {
|
||||||
|
supportType = 'ANCHOR';
|
||||||
|
}
|
||||||
|
|
||||||
|
// User Requirements 추출 (분류기에서 제공된 것 우선)
|
||||||
|
const userRequirements = material.user_requirements || [];
|
||||||
|
|
||||||
|
// 구매 수량 계산 (서포트는 취합된 숫자 그대로)
|
||||||
|
const qty = Math.round(material.quantity || 0);
|
||||||
|
const purchaseQty = qty;
|
||||||
|
|
||||||
|
// Material Grade 처리 - 우레탄 블럭슈의 경우 두께 정보 포함
|
||||||
|
let materialGrade = material.full_material_grade || material.material_grade || '-';
|
||||||
|
|
||||||
|
if (supportType === 'URETHANE BLOCK SHOE') {
|
||||||
|
// 두께 정보 추출 (40t, 27t 등 - 여기서 t는 thickness를 의미)
|
||||||
|
const thicknessMatch = desc.match(/(\d+)\s*[tT]/);
|
||||||
|
if (thicknessMatch) {
|
||||||
|
const thickness = `${thicknessMatch[1]}t`;
|
||||||
|
if (materialGrade === '-' || !materialGrade) {
|
||||||
|
materialGrade = thickness;
|
||||||
|
} else if (!materialGrade.includes(thickness)) {
|
||||||
|
materialGrade = `${materialGrade} ${thickness}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: 'SUPPORT',
|
type: supportType,
|
||||||
subtype: subtypeText,
|
|
||||||
size: material.main_nom || material.size_inch || material.size_spec || '-',
|
size: material.main_nom || material.size_inch || material.size_spec || '-',
|
||||||
pressure: '-', // 서포트는 압력 등급 없음
|
grade: materialGrade,
|
||||||
schedule: '-', // 서포트는 스케줄 없음
|
userRequirements: userRequirements.join(', ') || '-',
|
||||||
description: material.original_description || '-',
|
|
||||||
grade: material.full_material_grade || material.material_grade || '-',
|
|
||||||
additionalReq: '-',
|
additionalReq: '-',
|
||||||
quantity: Math.round(material.quantity || 0),
|
purchaseQuantity: `${purchaseQty} EA`,
|
||||||
unit: '개',
|
originalQuantity: qty,
|
||||||
isSupport: true
|
isSupport: true
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 동일한 서포트 항목 합산
|
||||||
|
const consolidateSupportMaterials = (materials) => {
|
||||||
|
const consolidated = {};
|
||||||
|
|
||||||
|
materials.forEach(material => {
|
||||||
|
const info = parseSupportInfo(material);
|
||||||
|
const key = `${info.type}|${info.size}|${info.grade}`;
|
||||||
|
|
||||||
|
if (!consolidated[key]) {
|
||||||
|
consolidated[key] = {
|
||||||
|
...material,
|
||||||
|
consolidatedQuantity: info.originalQuantity,
|
||||||
|
consolidatedIds: [material.id],
|
||||||
|
parsedInfo: info
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
consolidated[key].consolidatedQuantity += info.originalQuantity;
|
||||||
|
consolidated[key].consolidatedIds.push(material.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 합산된 수량으로 구매 수량 재계산 (서포트는 취합된 숫자 그대로)
|
||||||
|
return Object.values(consolidated).map(item => {
|
||||||
|
const purchaseQty = item.consolidatedQuantity;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
parsedInfo: {
|
||||||
|
...item.parsedInfo,
|
||||||
|
originalQuantity: item.consolidatedQuantity,
|
||||||
|
purchaseQuantity: `${purchaseQty} EA`
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// 정렬 처리
|
// 정렬 처리
|
||||||
const handleSort = (key) => {
|
const handleSort = (key) => {
|
||||||
let direction = 'asc';
|
let direction = 'asc';
|
||||||
@@ -57,19 +122,24 @@ const SupportMaterialsView = ({
|
|||||||
|
|
||||||
// 필터링된 및 정렬된 자재 목록
|
// 필터링된 및 정렬된 자재 목록
|
||||||
const getFilteredAndSortedMaterials = () => {
|
const getFilteredAndSortedMaterials = () => {
|
||||||
let filtered = materials.filter(material => {
|
// 먼저 합산 처리
|
||||||
|
let consolidated = consolidateSupportMaterials(materials);
|
||||||
|
|
||||||
|
// 필터링
|
||||||
|
let filtered = consolidated.filter(material => {
|
||||||
return Object.entries(columnFilters).every(([key, filterValue]) => {
|
return Object.entries(columnFilters).every(([key, filterValue]) => {
|
||||||
if (!filterValue) return true;
|
if (!filterValue) return true;
|
||||||
const info = parseSupportInfo(material);
|
const info = material.parsedInfo;
|
||||||
const value = info[key]?.toString().toLowerCase() || '';
|
const value = info[key]?.toString().toLowerCase() || '';
|
||||||
return value.includes(filterValue.toLowerCase());
|
return value.includes(filterValue.toLowerCase());
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 정렬
|
||||||
if (sortConfig && sortConfig.key) {
|
if (sortConfig && sortConfig.key) {
|
||||||
filtered.sort((a, b) => {
|
filtered.sort((a, b) => {
|
||||||
const aInfo = parseSupportInfo(a);
|
const aInfo = a.parsedInfo;
|
||||||
const bInfo = parseSupportInfo(b);
|
const bInfo = b.parsedInfo;
|
||||||
|
|
||||||
if (!aInfo || !bInfo) return 0;
|
if (!aInfo || !bInfo) return 0;
|
||||||
|
|
||||||
@@ -104,26 +174,34 @@ const SupportMaterialsView = ({
|
|||||||
// 전체 선택/해제 (구매신청된 자재 제외)
|
// 전체 선택/해제 (구매신청된 자재 제외)
|
||||||
const handleSelectAll = () => {
|
const handleSelectAll = () => {
|
||||||
const filteredMaterials = getFilteredAndSortedMaterials();
|
const filteredMaterials = getFilteredAndSortedMaterials();
|
||||||
const selectableMaterials = filteredMaterials.filter(m => !purchasedMaterials.has(m.id));
|
const selectableMaterials = filteredMaterials.filter(material =>
|
||||||
|
!material.consolidatedIds.some(id => purchasedMaterials.has(id))
|
||||||
|
);
|
||||||
|
|
||||||
if (selectedMaterials.size === selectableMaterials.length) {
|
if (selectedMaterials.size === selectableMaterials.flatMap(m => m.consolidatedIds).length) {
|
||||||
setSelectedMaterials(new Set());
|
setSelectedMaterials(new Set());
|
||||||
} else {
|
} else {
|
||||||
setSelectedMaterials(new Set(selectableMaterials.map(m => m.id)));
|
const allIds = selectableMaterials.flatMap(m => m.consolidatedIds);
|
||||||
|
setSelectedMaterials(new Set(allIds));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 개별 선택 (구매신청된 자재는 선택 불가)
|
// 개별 선택 (구매신청된 자재는 선택 불가)
|
||||||
const handleMaterialSelect = (materialId) => {
|
const handleMaterialSelect = (consolidatedMaterial) => {
|
||||||
if (purchasedMaterials.has(materialId)) {
|
const hasAnyPurchased = consolidatedMaterial.consolidatedIds.some(id => purchasedMaterials.has(id));
|
||||||
return; // 구매신청된 자재는 선택 불가
|
if (hasAnyPurchased) {
|
||||||
|
return; // 구매신청된 자재가 포함된 경우 선택 불가
|
||||||
}
|
}
|
||||||
|
|
||||||
const newSelected = new Set(selectedMaterials);
|
const newSelected = new Set(selectedMaterials);
|
||||||
if (newSelected.has(materialId)) {
|
const allSelected = consolidatedMaterial.consolidatedIds.every(id => newSelected.has(id));
|
||||||
newSelected.delete(materialId);
|
|
||||||
|
if (allSelected) {
|
||||||
|
// 모두 선택된 경우 모두 해제
|
||||||
|
consolidatedMaterial.consolidatedIds.forEach(id => newSelected.delete(id));
|
||||||
} else {
|
} else {
|
||||||
newSelected.add(materialId);
|
// 일부 또는 전체 미선택인 경우 모두 선택
|
||||||
|
consolidatedMaterial.consolidatedIds.forEach(id => newSelected.add(id));
|
||||||
}
|
}
|
||||||
setSelectedMaterials(newSelected);
|
setSelectedMaterials(newSelected);
|
||||||
};
|
};
|
||||||
@@ -145,29 +223,83 @@ const SupportMaterialsView = ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await api.post('/files/save-excel', {
|
console.log('🔄 서포트 엑셀 내보내기 시작 - 새로운 방식');
|
||||||
|
|
||||||
|
// 1. 먼저 클라이언트에서 엑셀 파일 생성
|
||||||
|
console.log('📊 엑셀 Blob 생성 중...', dataWithRequirements.length, '개 자료');
|
||||||
|
const excelBlob = await createExcelBlob(dataWithRequirements, excelFileName, {
|
||||||
|
category: 'SUPPORT',
|
||||||
|
filename: excelFileName,
|
||||||
|
uploadDate: new Date().toLocaleDateString()
|
||||||
|
});
|
||||||
|
console.log('✅ 엑셀 Blob 생성 완료:', excelBlob.size, 'bytes');
|
||||||
|
|
||||||
|
// 2. 구매신청 생성
|
||||||
|
const allMaterialIds = selectedMaterialsData.map(m => m.id);
|
||||||
|
const response = await api.post('/purchase-request/create', {
|
||||||
file_id: fileId,
|
file_id: fileId,
|
||||||
|
job_no: jobNo,
|
||||||
category: 'SUPPORT',
|
category: 'SUPPORT',
|
||||||
materials: dataWithRequirements,
|
material_ids: allMaterialIds,
|
||||||
filename: excelFileName,
|
materials_data: dataWithRequirements.map(m => ({
|
||||||
user_id: user?.id
|
material_id: m.id,
|
||||||
|
description: m.original_description,
|
||||||
|
category: m.classified_category,
|
||||||
|
size: m.size_inch || m.size_spec,
|
||||||
|
schedule: m.schedule,
|
||||||
|
material_grade: m.material_grade || m.full_material_grade,
|
||||||
|
quantity: m.quantity,
|
||||||
|
unit: m.unit,
|
||||||
|
user_requirement: userRequirements[m.id] || ''
|
||||||
|
}))
|
||||||
});
|
});
|
||||||
|
|
||||||
exportMaterialsToExcel(dataWithRequirements, excelFileName, {
|
if (response.data.success) {
|
||||||
category: 'SUPPORT',
|
console.log(`✅ 구매신청 완료: ${response.data.request_no}, request_id: ${response.data.request_id}`);
|
||||||
filename: excelFileName,
|
|
||||||
uploadDate: new Date().toLocaleDateString()
|
|
||||||
});
|
|
||||||
|
|
||||||
alert('엑셀 파일이 생성되고 서버에 저장되었습니다.');
|
// 3. 엑셀 파일을 서버에 업로드
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('excel_file', excelBlob, excelFileName);
|
||||||
|
formData.append('request_id', response.data.request_id);
|
||||||
|
formData.append('category', 'SUPPORT');
|
||||||
|
|
||||||
|
console.log('📤 엑셀 파일 서버 업로드 중...');
|
||||||
|
await api.post('/purchase-request/upload-excel', formData, {
|
||||||
|
headers: { 'Content-Type': 'multipart/form-data' }
|
||||||
|
});
|
||||||
|
console.log('✅ 엑셀 파일 서버 업로드 완료');
|
||||||
|
|
||||||
|
// 4. 구매된 자재 목록 업데이트 (비활성화)
|
||||||
|
onPurchasedMaterialsUpdate(allMaterialIds);
|
||||||
|
console.log('✅ 구매된 자재 목록 업데이트 완료');
|
||||||
|
|
||||||
|
// 5. 클라이언트에 파일 다운로드
|
||||||
|
const url = window.URL.createObjectURL(excelBlob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.download = excelFileName;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
|
||||||
|
alert(`구매신청 ${response.data?.request_no || ''}이 생성되고 엑셀 파일이 저장되었습니다.`);
|
||||||
|
} else {
|
||||||
|
throw new Error(response.data?.message || '구매신청 생성 실패');
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('엑셀 저장 실패:', error);
|
console.error('엑셀 저장 또는 구매신청 실패:', error);
|
||||||
|
// 실패 시에도 클라이언트 다운로드는 진행
|
||||||
exportMaterialsToExcel(dataWithRequirements, excelFileName, {
|
exportMaterialsToExcel(dataWithRequirements, excelFileName, {
|
||||||
category: 'SUPPORT',
|
category: 'SUPPORT',
|
||||||
filename: excelFileName,
|
filename: excelFileName,
|
||||||
uploadDate: new Date().toLocaleDateString()
|
uploadDate: new Date().toLocaleDateString()
|
||||||
});
|
});
|
||||||
|
alert('엑셀 파일은 다운로드되었지만 구매신청 생성에 실패했습니다.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 선택 해제
|
||||||
|
setSelectedMaterials(new Set());
|
||||||
};
|
};
|
||||||
|
|
||||||
const filteredMaterials = getFilteredAndSortedMaterials();
|
const filteredMaterials = getFilteredAndSortedMaterials();
|
||||||
@@ -234,138 +366,171 @@ const SupportMaterialsView = ({
|
|||||||
<div style={{
|
<div style={{
|
||||||
background: 'white',
|
background: 'white',
|
||||||
borderRadius: '12px',
|
borderRadius: '12px',
|
||||||
overflow: 'hidden',
|
overflow: 'auto',
|
||||||
boxShadow: '0 1px 3px rgba(0, 0, 0, 0.1)'
|
boxShadow: '0 1px 3px rgba(0, 0, 0, 0.1)',
|
||||||
|
maxHeight: '600px'
|
||||||
}}>
|
}}>
|
||||||
{/* 헤더 */}
|
<div style={{ minWidth: '1200px' }}>
|
||||||
<div style={{
|
{/* 헤더 */}
|
||||||
display: 'grid',
|
<div style={{
|
||||||
gridTemplateColumns: '50px 200px 120px 100px 120px 150px 80px 80px 200px',
|
display: 'grid',
|
||||||
gap: '16px',
|
gridTemplateColumns: '50px 200px 120px 150px 180px 150px 200px',
|
||||||
padding: '16px',
|
gap: '16px',
|
||||||
background: '#f8fafc',
|
padding: '16px',
|
||||||
borderBottom: '1px solid #e2e8f0',
|
background: '#f8fafc',
|
||||||
fontSize: '14px',
|
borderBottom: '1px solid #e2e8f0',
|
||||||
fontWeight: '600',
|
fontSize: '14px',
|
||||||
color: '#374151'
|
fontWeight: '600',
|
||||||
}}>
|
color: '#374151',
|
||||||
|
textAlign: 'center'
|
||||||
|
}}>
|
||||||
<div>
|
<div>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={(() => {
|
checked={(() => {
|
||||||
const selectableMaterials = filteredMaterials.filter(m => !purchasedMaterials.has(m.id));
|
const selectableMaterials = filteredMaterials.filter(material =>
|
||||||
return selectedMaterials.size === selectableMaterials.length && selectableMaterials.length > 0;
|
!material.consolidatedIds.some(id => purchasedMaterials.has(id))
|
||||||
|
);
|
||||||
|
return selectedMaterials.size === selectableMaterials.flatMap(m => m.consolidatedIds).length && selectableMaterials.length > 0;
|
||||||
})()}
|
})()}
|
||||||
onChange={handleSelectAll}
|
onChange={handleSelectAll}
|
||||||
style={{ cursor: 'pointer' }}
|
style={{ cursor: 'pointer' }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<FilterableHeader sortKey="subtype" filterKey="subtype">Type</FilterableHeader>
|
<FilterableHeader
|
||||||
<FilterableHeader sortKey="size" filterKey="size">Size</FilterableHeader>
|
sortKey="type"
|
||||||
<FilterableHeader sortKey="pressure" filterKey="pressure">Pressure</FilterableHeader>
|
filterKey="type"
|
||||||
<FilterableHeader sortKey="schedule" filterKey="schedule">Schedule</FilterableHeader>
|
sortConfig={sortConfig}
|
||||||
<FilterableHeader sortKey="grade" filterKey="grade">Material Grade</FilterableHeader>
|
onSort={handleSort}
|
||||||
<FilterableHeader sortKey="quantity" filterKey="quantity">Quantity</FilterableHeader>
|
columnFilters={columnFilters}
|
||||||
<div>Unit</div>
|
onFilterChange={setColumnFilters}
|
||||||
<div>User Requirement</div>
|
showFilterDropdown={showFilterDropdown}
|
||||||
|
setShowFilterDropdown={setShowFilterDropdown}
|
||||||
|
>
|
||||||
|
Type
|
||||||
|
</FilterableHeader>
|
||||||
|
<FilterableHeader
|
||||||
|
sortKey="size"
|
||||||
|
filterKey="size"
|
||||||
|
sortConfig={sortConfig}
|
||||||
|
onSort={handleSort}
|
||||||
|
columnFilters={columnFilters}
|
||||||
|
onFilterChange={setColumnFilters}
|
||||||
|
showFilterDropdown={showFilterDropdown}
|
||||||
|
setShowFilterDropdown={setShowFilterDropdown}
|
||||||
|
>
|
||||||
|
Size
|
||||||
|
</FilterableHeader>
|
||||||
|
<FilterableHeader
|
||||||
|
sortKey="grade"
|
||||||
|
filterKey="grade"
|
||||||
|
sortConfig={sortConfig}
|
||||||
|
onSort={handleSort}
|
||||||
|
columnFilters={columnFilters}
|
||||||
|
onFilterChange={setColumnFilters}
|
||||||
|
showFilterDropdown={showFilterDropdown}
|
||||||
|
setShowFilterDropdown={setShowFilterDropdown}
|
||||||
|
>
|
||||||
|
Material Grade
|
||||||
|
</FilterableHeader>
|
||||||
|
<div>User Requirements</div>
|
||||||
|
<div>Additional Request</div>
|
||||||
|
<FilterableHeader
|
||||||
|
sortKey="purchaseQuantity"
|
||||||
|
filterKey="purchaseQuantity"
|
||||||
|
sortConfig={sortConfig}
|
||||||
|
onSort={handleSort}
|
||||||
|
columnFilters={columnFilters}
|
||||||
|
onFilterChange={setColumnFilters}
|
||||||
|
showFilterDropdown={showFilterDropdown}
|
||||||
|
setShowFilterDropdown={setShowFilterDropdown}
|
||||||
|
>
|
||||||
|
Purchase Quantity
|
||||||
|
</FilterableHeader>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 데이터 행들 */}
|
{/* 데이터 행들 */}
|
||||||
<div style={{ maxHeight: '600px', overflowY: 'auto' }}>
|
{filteredMaterials.map((consolidatedMaterial, index) => {
|
||||||
{filteredMaterials.map((material, index) => {
|
const info = consolidatedMaterial.parsedInfo;
|
||||||
const info = parseSupportInfo(material);
|
const hasAnyPurchased = consolidatedMaterial.consolidatedIds.some(id => purchasedMaterials.has(id));
|
||||||
const isSelected = selectedMaterials.has(material.id);
|
const allSelected = consolidatedMaterial.consolidatedIds.every(id => selectedMaterials.has(id));
|
||||||
const isPurchased = purchasedMaterials.has(material.id);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={material.id}
|
key={`consolidated-${index}`}
|
||||||
style={{
|
style={{
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
gridTemplateColumns: '50px 200px 120px 100px 120px 150px 80px 80px 200px',
|
gridTemplateColumns: '50px 200px 120px 150px 180px 150px 200px',
|
||||||
gap: '16px',
|
gap: '16px',
|
||||||
padding: '16px',
|
padding: '16px',
|
||||||
borderBottom: index < filteredMaterials.length - 1 ? '1px solid #f1f5f9' : 'none',
|
borderBottom: index < filteredMaterials.length - 1 ? '1px solid #f1f5f9' : 'none',
|
||||||
background: isSelected ? '#eff6ff' : (isPurchased ? '#fef3c7' : 'white'),
|
background: allSelected ? '#eff6ff' : (hasAnyPurchased ? '#fef3c7' : 'white'),
|
||||||
transition: 'background 0.15s ease'
|
transition: 'background 0.15s ease',
|
||||||
}}
|
textAlign: 'center'
|
||||||
onMouseEnter={(e) => {
|
}}
|
||||||
if (!isSelected && !isPurchased) {
|
onMouseEnter={(e) => {
|
||||||
e.target.style.background = '#f8fafc';
|
if (!allSelected && !hasAnyPurchased) {
|
||||||
}
|
e.target.style.background = '#f8fafc';
|
||||||
}}
|
}
|
||||||
onMouseLeave={(e) => {
|
}}
|
||||||
if (!isSelected && !isPurchased) {
|
onMouseLeave={(e) => {
|
||||||
e.target.style.background = 'white';
|
if (!allSelected && !hasAnyPurchased) {
|
||||||
}
|
e.target.style.background = 'white';
|
||||||
}}
|
}
|
||||||
>
|
}}
|
||||||
<div>
|
>
|
||||||
<input
|
<div>
|
||||||
type="checkbox"
|
<input
|
||||||
checked={isSelected}
|
type="checkbox"
|
||||||
onChange={() => handleMaterialSelect(material.id)}
|
checked={allSelected}
|
||||||
disabled={isPurchased}
|
onChange={() => handleMaterialSelect(consolidatedMaterial)}
|
||||||
style={{
|
disabled={hasAnyPurchased}
|
||||||
cursor: isPurchased ? 'not-allowed' : 'pointer',
|
style={{
|
||||||
opacity: isPurchased ? 0.5 : 1
|
cursor: hasAnyPurchased ? 'not-allowed' : 'pointer',
|
||||||
}}
|
opacity: hasAnyPurchased ? 0.5 : 1
|
||||||
/>
|
}}
|
||||||
</div>
|
/>
|
||||||
<div style={{ fontSize: '14px', color: '#1f2937', fontWeight: '500' }}>
|
|
||||||
{info.subtype}
|
|
||||||
{isPurchased && (
|
|
||||||
<span style={{
|
|
||||||
marginLeft: '8px',
|
|
||||||
padding: '2px 6px',
|
|
||||||
background: '#fbbf24',
|
|
||||||
color: '#92400e',
|
|
||||||
borderRadius: '4px',
|
|
||||||
fontSize: '10px',
|
|
||||||
fontWeight: '500'
|
|
||||||
}}>
|
|
||||||
PURCHASED
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div style={{ fontSize: '14px', color: '#1f2937' }}>
|
|
||||||
{info.size}
|
|
||||||
</div>
|
|
||||||
<div style={{ fontSize: '14px', color: '#1f2937' }}>
|
|
||||||
{info.pressure}
|
|
||||||
</div>
|
|
||||||
<div style={{ fontSize: '14px', color: '#1f2937' }}>
|
|
||||||
{info.schedule}
|
|
||||||
</div>
|
|
||||||
<div style={{ fontSize: '14px', color: '#1f2937' }}>
|
|
||||||
{info.grade}
|
|
||||||
</div>
|
|
||||||
<div style={{ fontSize: '14px', color: '#1f2937', fontWeight: '600', textAlign: 'right' }}>
|
|
||||||
{info.quantity}
|
|
||||||
</div>
|
|
||||||
<div style={{ fontSize: '14px', color: '#6b7280' }}>
|
|
||||||
{info.unit}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={userRequirements[material.id] || ''}
|
|
||||||
onChange={(e) => setUserRequirements({
|
|
||||||
...userRequirements,
|
|
||||||
[material.id]: e.target.value
|
|
||||||
})}
|
|
||||||
placeholder="Enter requirement..."
|
|
||||||
style={{
|
|
||||||
width: '100%',
|
|
||||||
padding: '6px 8px',
|
|
||||||
border: '1px solid #d1d5db',
|
|
||||||
borderRadius: '4px',
|
|
||||||
fontSize: '12px'
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
<div style={{ fontSize: '14px', color: '#1f2937', fontWeight: '500' }}>
|
||||||
|
{info.type}
|
||||||
|
{hasAnyPurchased && (
|
||||||
|
<span style={{
|
||||||
|
marginLeft: '8px',
|
||||||
|
padding: '2px 6px',
|
||||||
|
background: '#fbbf24',
|
||||||
|
color: '#92400e',
|
||||||
|
borderRadius: '4px',
|
||||||
|
fontSize: '10px',
|
||||||
|
fontWeight: '500'
|
||||||
|
}}>
|
||||||
|
PURCHASED
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: '14px', color: '#1f2937' }}>{info.size}</div>
|
||||||
|
<div style={{ fontSize: '14px', color: '#1f2937' }}>{info.grade}</div>
|
||||||
|
<div style={{ fontSize: '14px', color: '#1f2937' }}>{info.userRequirements}</div>
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={userRequirements[consolidatedMaterial.id] || ''}
|
||||||
|
onChange={(e) => setUserRequirements({
|
||||||
|
...userRequirements,
|
||||||
|
[consolidatedMaterial.id]: e.target.value
|
||||||
|
})}
|
||||||
|
placeholder="Enter additional request..."
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
padding: '8px',
|
||||||
|
border: '1px solid #d1d5db',
|
||||||
|
borderRadius: '4px',
|
||||||
|
fontSize: '12px'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: '14px', color: '#1f2937' }}>{info.purchaseQuantity}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -457,60 +457,57 @@ const formatMaterialForExcel = (material, includeComparison = false) => {
|
|||||||
itemName = gasketTypeMap[gasketType];
|
itemName = gasketTypeMap[gasketType];
|
||||||
} else if (extractedType && gasketTypeMap[extractedType]) {
|
} else if (extractedType && gasketTypeMap[extractedType]) {
|
||||||
itemName = gasketTypeMap[extractedType];
|
itemName = gasketTypeMap[extractedType];
|
||||||
} else {
|
} else {
|
||||||
itemName = 'GASKET';
|
itemName = 'GASKET';
|
||||||
}
|
}
|
||||||
} else if (category === 'BOLT') {
|
} else if (category === 'BOLT') {
|
||||||
// 볼트 상세 타입 표시
|
// 볼트 상세 타입 표시
|
||||||
const boltDetails = material.bolt_details || {};
|
const boltDetails = material.bolt_details || {};
|
||||||
const boltType = boltDetails.bolt_type || '';
|
const boltType = boltDetails.bolt_type || '';
|
||||||
|
|
||||||
if (boltType === 'HEX_BOLT') {
|
// BOM 페이지의 타입을 따라가도록 - 프론트엔드 parseBoltInfo와 동일한 로직
|
||||||
itemName = '육각 볼트';
|
let boltSubtype = 'BOLT_GENERAL';
|
||||||
} else if (boltType === 'STUD_BOLT') {
|
|
||||||
itemName = '스터드 볼트';
|
if (boltType && boltType !== 'UNKNOWN') {
|
||||||
} else if (boltType === 'U_BOLT') {
|
boltSubtype = boltType;
|
||||||
itemName = '유볼트';
|
|
||||||
} else if (boltType === 'FLANGE_BOLT') {
|
|
||||||
itemName = '플랜지 볼트';
|
|
||||||
} else if (boltType === 'PSV_BOLT') {
|
|
||||||
itemName = 'PSV 볼트';
|
|
||||||
} else if (boltType === 'LT_BOLT') {
|
|
||||||
itemName = '저온용 볼트';
|
|
||||||
} else if (boltType === 'CK_BOLT') {
|
|
||||||
itemName = '체크밸브용 볼트';
|
|
||||||
} else {
|
} else {
|
||||||
// description에서 추출
|
// 원본 설명에서 특수 볼트 타입 추출 (프론트엔드와 동일)
|
||||||
const desc = cleanDescription.toUpperCase();
|
const desc = cleanDescription.toUpperCase();
|
||||||
if (desc.includes('PSV')) {
|
if (desc.includes('PSV')) {
|
||||||
itemName = 'PSV 볼트';
|
boltSubtype = 'PSV_BOLT';
|
||||||
} else if (desc.includes('LT')) {
|
} else if (desc.includes('LT')) {
|
||||||
itemName = '저온용 볼트';
|
boltSubtype = 'LT_BOLT';
|
||||||
} else if (desc.includes('CK')) {
|
} else if (desc.includes('CK')) {
|
||||||
itemName = '체크밸브용 볼트';
|
boltSubtype = 'CK_BOLT';
|
||||||
} else if (desc.includes('STUD')) {
|
|
||||||
itemName = '스터드 볼트';
|
|
||||||
} else if (desc.includes('U-BOLT') || desc.includes('U BOLT')) {
|
|
||||||
itemName = '유볼트';
|
|
||||||
} else {
|
|
||||||
itemName = '볼트';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BOM 페이지와 동일한 타입명 사용
|
||||||
|
itemName = boltSubtype;
|
||||||
} else if (category === 'SUPPORT' || category === 'U_BOLT') {
|
} else if (category === 'SUPPORT' || category === 'U_BOLT') {
|
||||||
// 서포트 상세 타입 표시
|
// 서포트 상세 타입 표시
|
||||||
const desc = cleanDescription.toUpperCase();
|
const desc = cleanDescription.toUpperCase();
|
||||||
if (desc.includes('URETHANE') || desc.includes('BLOCK SHOE')) {
|
if (desc.includes('URETHANE') || desc.includes('BLOCK SHOE')) {
|
||||||
itemName = '우레탄 블록 슈';
|
itemName = 'URETHANE BLOCK SHOE';
|
||||||
|
// 우레탄 블럭슈의 경우 두께 정보 추가
|
||||||
|
const thicknessMatch = desc.match(/(\d+)\s*[tT](?![oO])/);
|
||||||
|
if (thicknessMatch) {
|
||||||
|
itemName += ` ${thicknessMatch[1]}t`;
|
||||||
|
}
|
||||||
} else if (desc.includes('CLAMP')) {
|
} else if (desc.includes('CLAMP')) {
|
||||||
itemName = '클램프';
|
itemName = 'CLAMP';
|
||||||
} else if (desc.includes('U-BOLT') || desc.includes('U BOLT')) {
|
} else if (desc.includes('U-BOLT') || desc.includes('U BOLT')) {
|
||||||
itemName = '유볼트';
|
itemName = 'U-BOLT';
|
||||||
} else if (desc.includes('HANGER')) {
|
} else if (desc.includes('HANGER')) {
|
||||||
itemName = '행거';
|
itemName = 'HANGER';
|
||||||
} else if (desc.includes('SPRING')) {
|
} else if (desc.includes('SPRING')) {
|
||||||
itemName = '스프링 서포트';
|
itemName = 'SPRING HANGER';
|
||||||
|
} else if (desc.includes('GUIDE')) {
|
||||||
|
itemName = 'GUIDE';
|
||||||
|
} else if (desc.includes('ANCHOR')) {
|
||||||
|
itemName = 'ANCHOR';
|
||||||
} else {
|
} else {
|
||||||
itemName = '서포트';
|
itemName = 'SUPPORT';
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
itemName = category || 'UNKNOWN';
|
itemName = category || 'UNKNOWN';
|
||||||
@@ -542,12 +539,22 @@ const formatMaterialForExcel = (material, includeComparison = false) => {
|
|||||||
let schedule = '-';
|
let schedule = '-';
|
||||||
|
|
||||||
if (category === 'BOLT') {
|
if (category === 'BOLT') {
|
||||||
// 볼트의 경우 길이 정보 추출
|
// 볼트의 경우 길이 정보 추출 (백엔드와 동일한 패턴 사용)
|
||||||
const lengthPatterns = [
|
const lengthPatterns = [
|
||||||
/(\d+(?:\.\d+)?)\s*LG/i, // 145.0000 LG, 75 LG
|
/(\d+(?:\.\d+)?)\s*LG/i, // 145.0000 LG, 75 LG (최우선)
|
||||||
|
/(\d+(?:\.\d+)?)\s*MM\s*LG/i, // 70MM LG 형태
|
||||||
|
/L\s*(\d+(?:\.\d+)?)\s*MM/i,
|
||||||
|
/LENGTH\s*(\d+(?:\.\d+)?)\s*MM/i,
|
||||||
|
/(\d+(?:\.\d+)?)\s*MM\s*LONG/i,
|
||||||
|
/X\s*(\d+(?:\.\d+)?)\s*MM/i, // M8 X 20MM 형태
|
||||||
|
/,\s*(\d+(?:\.\d+)?)\s*LG/i, // ", 145.0000 LG" 형태 (PSV, LT 볼트용)
|
||||||
|
/,\s*(\d+(?:\.\d+)?)\s+(?:CK|PSV|LT)/i, // ", 140 CK" 형태 (PSV 볼트용)
|
||||||
|
/PSV\s+(\d+(?:\.\d+)?)/i, // PSV 140 형태 (PSV 볼트 전용)
|
||||||
|
/(\d+(?:\.\d+)?)\s+PSV/i, // 140 PSV 형태 (PSV 볼트 전용)
|
||||||
|
/(\d+(?:\.\d+)?)\s*CK/i, // 140CK 형태 (체크밸브용 볼트)
|
||||||
|
/(\d+(?:\.\d+)?)\s*LT/i, // 140LT 형태 (저온용 볼트)
|
||||||
/(\d+(?:\.\d+)?)\s*mm/i, // 50mm
|
/(\d+(?:\.\d+)?)\s*mm/i, // 50mm
|
||||||
/(\d+(?:\.\d+)?)\s*MM/i, // 50MM
|
/(\d+(?:\.\d+)?)\s*MM/i, // 50MM
|
||||||
/,\s*(\d+(?:\.\d+)?)\s*LG/i, // ", 145.0000 LG" 형태
|
|
||||||
/LG[,\s]*(\d+(?:\.\d+)?)/i // LG, 75 형태
|
/LG[,\s]*(\d+(?:\.\d+)?)/i // LG, 75 형태
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -678,20 +685,20 @@ const formatMaterialForExcel = (material, includeComparison = false) => {
|
|||||||
gasketMaterial = `${fourMaterialMatch[1]}/${fourMaterialMatch[2]}/${fourMaterialMatch[3]}/${fourMaterialMatch[4]}`;
|
gasketMaterial = `${fourMaterialMatch[1]}/${fourMaterialMatch[2]}/${fourMaterialMatch[3]}/${fourMaterialMatch[4]}`;
|
||||||
} else {
|
} else {
|
||||||
// DB에서 가져온 정보로 구성 (fallback)
|
// DB에서 가져온 정보로 구성 (fallback)
|
||||||
if (material.gasket_details) {
|
if (material.gasket_details) {
|
||||||
const materialType = material.gasket_details.material_type || '';
|
const materialType = material.gasket_details.material_type || '';
|
||||||
const fillerMaterial = material.gasket_details.filler_material || '';
|
const fillerMaterial = material.gasket_details.filler_material || '';
|
||||||
|
|
||||||
if (materialType && fillerMaterial) {
|
if (materialType && fillerMaterial) {
|
||||||
gasketMaterial = `${materialType}/${fillerMaterial}`;
|
gasketMaterial = `${materialType}/${fillerMaterial}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 마지막으로 간단한 패턴
|
// 마지막으로 간단한 패턴
|
||||||
if (!gasketMaterial) {
|
if (!gasketMaterial) {
|
||||||
const simpleMaterialMatch = cleanDescription.match(/(SS\d+|304|316)\s*[\/+]\s*(GRAPHITE|PTFE|VITON|EPDM)/i);
|
const simpleMaterialMatch = cleanDescription.match(/(SS\d+|304|316)\s*[\/+]\s*(GRAPHITE|PTFE|VITON|EPDM)/i);
|
||||||
if (simpleMaterialMatch) {
|
if (simpleMaterialMatch) {
|
||||||
gasketMaterial = `${simpleMaterialMatch[1]}/${simpleMaterialMatch[2]}`;
|
gasketMaterial = `${simpleMaterialMatch[1]}/${simpleMaterialMatch[2]}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -730,6 +737,29 @@ const formatMaterialForExcel = (material, includeComparison = false) => {
|
|||||||
} else if (material.pipe_count) {
|
} else if (material.pipe_count) {
|
||||||
quantity = material.pipe_count;
|
quantity = material.pipe_count;
|
||||||
}
|
}
|
||||||
|
} else if (category === 'BOLT') {
|
||||||
|
// BOLT의 경우 플랜지당 볼트 세트 수를 고려한 수량 계산 (BOM 페이지와 동일)
|
||||||
|
const qty = Math.round(material.quantity || 0);
|
||||||
|
|
||||||
|
// 플랜지당 볼트 세트 수 추출 (예: (8), (4))
|
||||||
|
const boltDetails = material.bolt_details || {};
|
||||||
|
let boltsPerFlange = boltDetails.dimensions?.bolts_per_flange || 1;
|
||||||
|
|
||||||
|
// 백엔드에서 정보가 없으면 원본 설명에서 직접 추출
|
||||||
|
if (boltsPerFlange === 1) {
|
||||||
|
const description = material.original_description || '';
|
||||||
|
const flangePattern = description.match(/\((\d+)\)/);
|
||||||
|
if (flangePattern) {
|
||||||
|
boltsPerFlange = parseInt(flangePattern[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 실제 필요한 볼트 수 = 플랜지 수 × 플랜지당 볼트 세트 수
|
||||||
|
const totalBoltsNeeded = qty * boltsPerFlange;
|
||||||
|
const safetyQty = Math.ceil(totalBoltsNeeded * 1.05); // 5% 여유율
|
||||||
|
const purchaseQty = Math.ceil(safetyQty / 4) * 4; // 4의 배수
|
||||||
|
|
||||||
|
quantity = purchaseQty;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 새로운 엑셀 양식: A~E 고정, F~O 카테고리별, P 납기일
|
// 새로운 엑셀 양식: A~E 고정, F~O 카테고리별, P 납기일
|
||||||
@@ -840,14 +870,26 @@ const formatMaterialForExcel = (material, includeComparison = false) => {
|
|||||||
base['관리항목1'] = ''; // M열
|
base['관리항목1'] = ''; // M열
|
||||||
base['관리항목2'] = ''; // N열
|
base['관리항목2'] = ''; // N열
|
||||||
base['관리항목3'] = ''; // O열
|
base['관리항목3'] = ''; // O열
|
||||||
} else if (category === 'BOLT') {
|
} else if (category === 'BOLT') {
|
||||||
// 볼트 전용 컬럼 (F~O) - 타입 제거, 품목명에 포함됨
|
// 볼트 전용 컬럼 (F~O) - User Requirements와 Additional Request 분리
|
||||||
base['크기'] = material.size_spec || '-'; // F열
|
base['크기'] = material.size_spec || '-'; // F열
|
||||||
base['압력등급'] = pressure; // G열
|
base['압력등급'] = pressure; // G열
|
||||||
base['길이'] = schedule; // H열: 볼트는 길이 정보
|
base['길이'] = schedule; // H열: 볼트는 길이 정보
|
||||||
base['재질'] = grade; // I열
|
base['재질'] = grade; // I열
|
||||||
base['추가요구'] = detailInfo || '-'; // J열: 표면처리 등
|
base['사용자요구'] = detailInfo || '-'; // J열: ELEC.GALV 등 (분류기 추출)
|
||||||
base['사용자요구'] = material.user_requirement || ''; // K열
|
base['추가요청사항'] = material.user_requirement || ''; // K열: 사용자 입력
|
||||||
|
base['관리항목1'] = ''; // L열
|
||||||
|
base['관리항목2'] = ''; // M열
|
||||||
|
base['관리항목3'] = ''; // N열
|
||||||
|
base['관리항목4'] = ''; // O열
|
||||||
|
} else if (category === 'SUPPORT') {
|
||||||
|
// 서포트 전용 컬럼 (F~O)
|
||||||
|
base['크기'] = material.size_spec || material.main_nom || '-'; // F열
|
||||||
|
base['압력등급'] = pressure; // G열
|
||||||
|
base['재질'] = grade; // H열
|
||||||
|
base['상세내역'] = material.original_description || '-'; // I열
|
||||||
|
base['사용자요구'] = material.user_requirements?.join(', ') || ''; // J열 (분류기에서 추출)
|
||||||
|
base['추가요청사항'] = material.user_requirement || ''; // K열 (사용자 입력)
|
||||||
base['관리항목1'] = ''; // L열
|
base['관리항목1'] = ''; // L열
|
||||||
base['관리항목2'] = ''; // M열
|
base['관리항목2'] = ''; // M열
|
||||||
base['관리항목3'] = ''; // N열
|
base['관리항목3'] = ''; // N열
|
||||||
@@ -1113,7 +1155,7 @@ export const createExcelBlob = async (materials, filename, options = {}) => {
|
|||||||
'FLANGE': ['크기', '페이싱', '압력등급', '스케줄', '재질', '추가요구사항', '관리항목1', '관리항목2', '관리항목3', '관리항목4'],
|
'FLANGE': ['크기', '페이싱', '압력등급', '스케줄', '재질', '추가요구사항', '관리항목1', '관리항목2', '관리항목3', '관리항목4'],
|
||||||
'VALVE': ['크기', '압력등급', '재질', '상세내역', '사용자요구', '관리항목1', '관리항목2', '관리항목3', '관리항목4'],
|
'VALVE': ['크기', '압력등급', '재질', '상세내역', '사용자요구', '관리항목1', '관리항목2', '관리항목3', '관리항목4'],
|
||||||
'GASKET': ['크기', '압력등급', '구조', '재질1', '재질2', '두께', '사용자요구', '관리항목1', '관리항목2', '관리항목3'],
|
'GASKET': ['크기', '압력등급', '구조', '재질1', '재질2', '두께', '사용자요구', '관리항목1', '관리항목2', '관리항목3'],
|
||||||
'BOLT': ['크기', '압력등급', '길이', '재질', '추가요구', '사용자요구', '관리항목1', '관리항목2', '관리항목3', '관리항목4'],
|
'BOLT': ['크기', '압력등급', '길이', '재질', '사용자요구', '추가요청사항', '관리항목1', '관리항목2', '관리항목3', '관리항목4'],
|
||||||
'SUPPORT': ['크기', '압력등급', '재질', '상세내역', '사용자요구', '관리항목1', '관리항목2', '관리항목3', '관리항목4'],
|
'SUPPORT': ['크기', '압력등급', '재질', '상세내역', '사용자요구', '관리항목1', '관리항목2', '관리항목3', '관리항목4'],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -126,6 +126,15 @@ export const calculatePurchaseQuantity = (material) => {
|
|||||||
unit: 'EA'
|
unit: 'EA'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
case 'SUPPORT':
|
||||||
|
// 서포트는 취합된 숫자 그대로
|
||||||
|
return {
|
||||||
|
purchaseQuantity: bomQuantity,
|
||||||
|
calculation: `${bomQuantity} EA (취합된 수량 그대로)`,
|
||||||
|
category: 'SUPPORT',
|
||||||
|
unit: 'EA'
|
||||||
|
};
|
||||||
|
|
||||||
case 'FITTING':
|
case 'FITTING':
|
||||||
case 'INSTRUMENT':
|
case 'INSTRUMENT':
|
||||||
case 'VALVE':
|
case 'VALVE':
|
||||||
|
|||||||
110
update_excel_exports.py
Normal file
110
update_excel_exports.py
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
|
# 수정할 파일들과 카테고리
|
||||||
|
files_categories = [
|
||||||
|
('frontend/src/components/bom/materials/ValveMaterialsView.jsx', 'VALVE'),
|
||||||
|
('frontend/src/components/bom/materials/GasketMaterialsView.jsx', 'GASKET'),
|
||||||
|
('frontend/src/components/bom/materials/BoltMaterialsView.jsx', 'BOLT'),
|
||||||
|
('frontend/src/components/bom/materials/SupportMaterialsView.jsx', 'SUPPORT')
|
||||||
|
]
|
||||||
|
|
||||||
|
for file_path, category in files_categories:
|
||||||
|
if os.path.exists(file_path):
|
||||||
|
with open(file_path, 'r', encoding='utf-8') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
# 기존 try 블록을 새로운 방식으로 교체
|
||||||
|
old_pattern = r'try\s*\{\s*//\s*1\.\s*구매신청\s*생성.*?alert\(\'엑셀\s*파일은\s*다운로드되었지만\s*구매신청\s*생성에\s*실패했습니다\.\'\);\s*\}'
|
||||||
|
|
||||||
|
new_code = f'''try {{
|
||||||
|
console.log('🔄 {category.lower()} 엑셀 내보내기 시작 - 새로운 방식');
|
||||||
|
|
||||||
|
// 1. 먼저 클라이언트에서 엑셀 파일 생성
|
||||||
|
console.log('📊 엑셀 Blob 생성 중...', dataWithRequirements.length, '개 자료');
|
||||||
|
const excelBlob = await createExcelBlob(dataWithRequirements, excelFileName, {{
|
||||||
|
category: '{category}',
|
||||||
|
filename: excelFileName,
|
||||||
|
uploadDate: new Date().toLocaleDateString()
|
||||||
|
}});
|
||||||
|
console.log('✅ 엑셀 Blob 생성 완료:', excelBlob.size, 'bytes');
|
||||||
|
|
||||||
|
// 2. 구매신청 생성
|
||||||
|
const allMaterialIds = selectedMaterialsData.map(m => m.id);
|
||||||
|
const response = await api.post('/purchase-request/create', {{
|
||||||
|
file_id: fileId,
|
||||||
|
job_no: jobNo,
|
||||||
|
category: '{category}',
|
||||||
|
material_ids: allMaterialIds,
|
||||||
|
materials_data: dataWithRequirements.map(m => ({{
|
||||||
|
material_id: m.id,
|
||||||
|
description: m.original_description,
|
||||||
|
category: m.classified_category,
|
||||||
|
size: m.size_inch || m.size_spec,
|
||||||
|
schedule: m.schedule,
|
||||||
|
material_grade: m.material_grade || m.full_material_grade,
|
||||||
|
quantity: m.quantity,
|
||||||
|
unit: m.unit,
|
||||||
|
user_requirement: userRequirements[m.id] || ''
|
||||||
|
}}))
|
||||||
|
}});
|
||||||
|
|
||||||
|
if (response.data.success) {{
|
||||||
|
console.log(`✅ 구매신청 완료: ${{response.data.request_no}}, request_id: ${{response.data.request_id}}`);
|
||||||
|
|
||||||
|
// 3. 생성된 엑셀 파일을 서버에 업로드
|
||||||
|
console.log('📤 서버에 엑셀 파일 업로드 중...');
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('excel_file', excelBlob, excelFileName);
|
||||||
|
formData.append('request_id', response.data.request_id);
|
||||||
|
formData.append('category', '{category}');
|
||||||
|
|
||||||
|
const uploadResponse = await api.post('/purchase-request/upload-excel', formData, {{
|
||||||
|
headers: {{
|
||||||
|
'Content-Type': 'multipart/form-data',
|
||||||
|
}},
|
||||||
|
}});
|
||||||
|
console.log('✅ 엑셀 업로드 완료:', uploadResponse.data);
|
||||||
|
|
||||||
|
if (onPurchasedMaterialsUpdate) {{
|
||||||
|
onPurchasedMaterialsUpdate(allMaterialIds);
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
|
||||||
|
// 4. 클라이언트 다운로드
|
||||||
|
const url = window.URL.createObjectURL(excelBlob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.download = excelFileName;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
|
||||||
|
alert(`구매신청 ${{response.data?.request_no || ''}}이 생성되고 엑셀 파일이 저장되었습니다.`);
|
||||||
|
}} catch (error) {{
|
||||||
|
console.error('엑셀 저장 또는 구매신청 실패:', error);
|
||||||
|
// 실패 시에도 클라이언트 다운로드는 진행
|
||||||
|
exportMaterialsToExcel(dataWithRequirements, excelFileName, {{
|
||||||
|
category: '{category}',
|
||||||
|
filename: excelFileName,
|
||||||
|
uploadDate: new Date().toLocaleDateString()
|
||||||
|
}});
|
||||||
|
alert('엑셀 파일은 다운로드되었지만 구매신청 생성에 실패했습니다.');
|
||||||
|
}}'''
|
||||||
|
|
||||||
|
# 간단한 패턴으로 기존 try 블록 찾기
|
||||||
|
try_pattern = r'try\s*\{[^}]*?구매신청\s*생성[^}]*?\}\s*catch[^}]*?\}'
|
||||||
|
|
||||||
|
if re.search(try_pattern, content, re.DOTALL):
|
||||||
|
content = re.sub(try_pattern, new_code, content, flags=re.DOTALL)
|
||||||
|
|
||||||
|
with open(file_path, 'w', encoding='utf-8') as f:
|
||||||
|
f.write(content)
|
||||||
|
print(f"✅ Updated {file_path}")
|
||||||
|
else:
|
||||||
|
print(f"❌ Pattern not found in {file_path}")
|
||||||
|
else:
|
||||||
|
print(f"❌ File not found: {file_path}")
|
||||||
|
|
||||||
|
print("✅ All files updated!")
|
||||||
Reference in New Issue
Block a user