feat: BOM과 구매관리 페이지 엑셀 통합 및 완전 동일화
Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled

엑셀 내보내기 통합:
- BOM 페이지: 구매신청 시 백엔드에서 엑셀 파일 생성 및 저장
- 구매관리 페이지: 저장된 엑셀 파일 직접 다운로드 (재생성 안 함)
- 두 페이지에서 완전히 동일한 엑셀 파일 제공

백엔드 엑셀 생성:
- openpyxl 사용하여 서버에서 엑셀 생성
- 카테고리별 시트 구성
- 헤더 스타일링 (연파랑 배경)
- 컬럼 너비 자동 조정

FLANGE 품목명 개선:
- 품목명: FLANGE (간단)
- 상세내역: WELD NECK RF, SLIP-ON RF 등 (전체 이름)
- 특수 플랜지: ORIFICE FLANGE, SPECTACLE BLIND 등 구분

구매신청 관리 API 개선:
- 상세 정보 포함 (pipe_details, fitting_details, flange_details 등)
- BOM 형식과 동일한 데이터 구조
- 수량 정수 변환 (3.000 → 3)

에러 수정:
- fileName 중복 선언 해결
- flange_details.connection_method 컬럼 제거 (존재하지 않음)
- Python 문법 오류 수정 (new Date() → datetime.now())

DB 스키마 개선:
- revision_status 컬럼 추가 및 크기 조정 (VARCHAR(30))
- 리비전 변경사항 추적 지원
This commit is contained in:
Hyungi Ahn
2025-10-14 15:59:33 +09:00
parent 72126ef78d
commit 8f5330a008
9 changed files with 334 additions and 4535 deletions

View File

@@ -51,9 +51,13 @@ async def create_purchase_request(
count = db.execute(count_query, {"pattern": f"PR-{today}%"}).fetchone().count
request_no = f"PR-{today}-{str(count + 1).zfill(3)}"
# 자재 데이터를 JSON 파일로 저장 (나중에 재다운로드 시 사용)
# 자재 데이터를 JSON과 엑셀 파일로 저장
json_filename = f"{request_no}.json"
excel_filename = f"{request_no}.xlsx"
json_path = os.path.join(EXCEL_DIR, json_filename)
excel_path = os.path.join(EXCEL_DIR, excel_filename)
# JSON 저장
save_materials_data(
request_data.materials_data,
json_path,
@@ -62,6 +66,14 @@ async def create_purchase_request(
request_data.grouped_materials # 그룹화 정보 추가
)
# 엑셀 파일 생성 및 저장
create_excel_file(
request_data.grouped_materials or request_data.materials_data,
excel_path,
request_no,
request_data.job_no
)
# 구매신청 레코드 생성
insert_request = text("""
INSERT INTO purchase_requests (
@@ -79,7 +91,7 @@ async def create_purchase_request(
"job_no": request_data.job_no,
"category": request_data.category,
"material_count": len(request_data.material_ids),
"excel_path": json_filename,
"excel_path": excel_filename, # 엑셀 파일명 저장 (JSON 대신)
"requested_by": 1 # current_user.get("user_id")
})
request_id = result.fetchone().request_id
@@ -276,12 +288,30 @@ async def get_request_materials(
m.original_description,
m.classified_category,
m.size_spec,
m.main_nom,
m.red_nom,
m.schedule,
m.material_grade,
m.full_material_grade,
m.quantity as original_quantity,
m.unit as original_unit
m.unit as original_unit,
m.classification_details,
pd.outer_diameter, pd.schedule as pipe_schedule, pd.material_spec, pd.manufacturing_method,
pd.end_preparation, pd.length_mm,
fd.fitting_type, fd.fitting_subtype, fd.connection_method as fitting_connection,
fd.pressure_rating as fitting_pressure, fd.schedule as fitting_schedule,
fld.flange_type, fld.facing_type,
fld.pressure_rating as flange_pressure,
gd.gasket_type, gd.gasket_subtype, gd.material_type as gasket_material,
gd.filler_material, gd.pressure_rating as gasket_pressure, gd.thickness as gasket_thickness,
bd.bolt_type, bd.material_standard as bolt_material, bd.length as bolt_length
FROM purchase_request_items pri
JOIN materials m ON pri.material_id = m.id
LEFT JOIN pipe_details pd ON m.id = pd.material_id
LEFT JOIN fitting_details fd ON m.id = fd.material_id
LEFT JOIN flange_details fld ON m.id = fld.material_id
LEFT JOIN gasket_details gd ON m.id = gd.material_id
LEFT JOIN bolt_details bd ON m.id = bd.material_id
WHERE pri.request_id = :request_id
ORDER BY m.classified_category, m.original_description
""")
@@ -296,21 +326,69 @@ async def get_request_materials(
qty_int = int(float(qty)) if qty else 0
except (ValueError, TypeError):
qty_int = 0
materials.append({
# BOM 페이지와 동일한 형식으로 데이터 구성
material_dict = {
"item_id": row.item_id,
"material_id": row.material_id,
"description": row.original_description,
"category": row.classified_category,
"size": row.size_spec,
"id": row.material_id,
"original_description": row.original_description,
"classified_category": row.classified_category,
"size_spec": row.size_spec,
"size_inch": row.main_nom,
"main_nom": row.main_nom,
"red_nom": row.red_nom,
"schedule": row.schedule,
"material_grade": row.material_grade,
"quantity": qty_int, # 정수로 변환
"full_material_grade": row.full_material_grade,
"quantity": qty_int,
"unit": row.requested_unit or row.original_unit,
"user_requirement": row.user_requirement,
"is_ordered": row.is_ordered,
"is_received": row.is_received
})
"is_received": row.is_received,
"classification_details": row.classification_details
}
# 카테고리별 상세 정보 추가
if row.classified_category == 'PIPE' and row.manufacturing_method:
material_dict["pipe_details"] = {
"manufacturing_method": row.manufacturing_method,
"schedule": row.pipe_schedule,
"material_spec": row.material_spec,
"end_preparation": row.end_preparation,
"length_mm": row.length_mm
}
elif row.classified_category == 'FITTING' and row.fitting_type:
material_dict["fitting_details"] = {
"fitting_type": row.fitting_type,
"fitting_subtype": row.fitting_subtype,
"connection_method": row.fitting_connection,
"pressure_rating": row.fitting_pressure,
"schedule": row.fitting_schedule
}
elif row.classified_category == 'FLANGE' and row.flange_type:
material_dict["flange_details"] = {
"flange_type": row.flange_type,
"facing_type": row.facing_type,
"pressure_rating": row.flange_pressure
}
elif row.classified_category == 'GASKET' and row.gasket_type:
material_dict["gasket_details"] = {
"gasket_type": row.gasket_type,
"gasket_subtype": row.gasket_subtype,
"material_type": row.gasket_material,
"filler_material": row.filler_material,
"pressure_rating": row.gasket_pressure,
"thickness": row.gasket_thickness
}
elif row.classified_category == 'BOLT' and row.bolt_type:
material_dict["bolt_details"] = {
"bolt_type": row.bolt_type,
"material_standard": row.bolt_material,
"length": row.bolt_length
}
materials.append(material_dict)
return {
"success": True,
@@ -334,8 +412,10 @@ async def download_request_excel(
db: Session = Depends(get_db)
):
"""
구매신청 자재 데이터 조회 (프론트엔드에서 엑셀 생성용)
구매신청 엑셀 파일 직접 다운로드 (BOM 페이지에서 생성한 파일 그대로)
"""
from fastapi.responses import FileResponse
try:
# 구매신청 정보 조회
query = text("""
@@ -351,25 +431,20 @@ async def download_request_excel(
detail="구매신청을 찾을 수 없습니다"
)
file_path = os.path.join(EXCEL_DIR, result.excel_file_path)
excel_file_path = os.path.join(EXCEL_DIR, result.excel_file_path)
if not os.path.exists(file_path):
if not os.path.exists(excel_file_path):
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="데이터 파일을 찾을 수 없습니다"
detail="엑셀 파일을 찾을 수 없습니다"
)
# JSON 파일 읽기
with open(file_path, 'r', encoding='utf-8') as f:
data = json.load(f)
return {
"success": True,
"request_no": result.request_no,
"job_no": result.job_no,
"materials": data.get("materials", []),
"grouped_materials": data.get("grouped_materials", []) # 그룹화 정보도 반환
}
# 엑셀 파일 직접 다운로드
return FileResponse(
path=excel_file_path,
filename=f"{result.job_no}_{result.request_no}.xlsx",
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
)
except HTTPException:
raise
@@ -418,3 +493,85 @@ def save_materials_data(materials_data: List[Dict], file_path: str, request_no:
with open(file_path, 'w', encoding='utf-8') as f:
json.dump(data_to_save, f, ensure_ascii=False, indent=2)
def create_excel_file(materials_data: List[Dict], file_path: str, request_no: str, job_no: str):
"""
자재 데이터로 엑셀 파일 생성 (BOM 페이지와 동일한 형식)
"""
import openpyxl
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
# 새 워크북 생성
wb = openpyxl.Workbook()
wb.remove(wb.active) # 기본 시트 제거
# 카테고리별 그룹화
category_groups = {}
for material in materials_data:
category = material.get('category', 'UNKNOWN')
if category not in category_groups:
category_groups[category] = []
category_groups[category].append(material)
# 각 카테고리별 시트 생성
for category, items in category_groups.items():
if not items:
continue
ws = wb.create_sheet(title=category)
# 헤더 정의
headers = ['TAGNO', '품목명', '수량', '통화구분', '단가', '크기', '압력등급', '스케줄',
'재질', '상세내역', '사용자요구', '관리항목1', '관리항목7', '관리항목8',
'관리항목9', '관리항목10', '납기일(YYYY-MM-DD)']
# 헤더 작성
for col, header in enumerate(headers, 1):
cell = ws.cell(row=1, column=col, value=header)
cell.font = Font(bold=True, color="000000", size=12, name="맑은 고딕")
cell.fill = PatternFill(start_color="B3D9FF", end_color="B3D9FF", fill_type="solid")
cell.alignment = Alignment(horizontal="center", vertical="center")
cell.border = Border(
top=Side(style="thin", color="666666"),
bottom=Side(style="thin", color="666666"),
left=Side(style="thin", color="666666"),
right=Side(style="thin", color="666666")
)
# 데이터 작성
for row_idx, material in enumerate(items, 2):
data = [
'', # TAGNO
category, # 품목명
material.get('quantity', 0), # 수량
'KRW', # 통화구분
1, # 단가
material.get('size', '-'), # 크기
'-', # 압력등급 (추후 개선)
material.get('schedule', '-'), # 스케줄
material.get('material_grade', '-'), # 재질
'-', # 상세내역 (추후 개선)
material.get('user_requirement', ''), # 사용자요구
'', '', '', '', '', # 관리항목들
datetime.now().strftime('%Y-%m-%d') # 납기일
]
for col, value in enumerate(data, 1):
ws.cell(row=row_idx, column=col, value=value)
# 컬럼 너비 자동 조정
for column in ws.columns:
max_length = 0
column_letter = column[0].column_letter
for cell in column:
try:
if len(str(cell.value)) > max_length:
max_length = len(str(cell.value))
except:
pass
adjusted_width = min(max(max_length + 2, 10), 50)
ws.column_dimensions[column_letter].width = adjusted_width
# 파일 저장
wb.save(file_path)