feat: 리비전 관리 시스템 완전 개편
변동이력 관리로 전환: - 도면번호 기준 변경 추적 - 리비전 업로드 시 전체 자재 저장 (차이분만 저장 방식 폐지) - 구매신청 정보 수량 기반 상속 리비전 변경 감지: - 수량/재질/크기/카테고리 변경 감지 - 변경 유형: specification_changed, quantity_changed, added, removed - 도면별 변경사항 추적 누락 도면 처리: - 리비전 업로드 시 누락된 도면 자동 감지 - 3가지 선택 옵션: 일부 업로드 / 도면 삭제 / 취소 - 구매신청 여부에 따라 다른 처리 (재고품 vs 숨김) 자재 상태 관리: - revision_status 컬럼 추가 (active/inventory/deleted_not_purchased/changed) - 재고품: 연노랑색 배경, '재고품' 배지 - 변경됨: 파란색 테두리, '변경됨' 배지 - 삭제됨: 자동 숨김 구매신청 정보 상속: - 수량 기반 상속 (그룹별 개수만큼만) - Rev.0에서 3개 구매 → Rev.1에서 처음 3개만 상속, 추가분은 미구매 - 도면번호 정확히 일치하는 경우에만 상속 기타 개선: - 구매신청 관리 페이지 수량 표시 개선 (3 EA, 소수점 제거) - 도면번호/라인번호 파싱 및 저장 (DWG_NAME, LINE_NUM 컬럼) - SPECIAL 카테고리 도면번호 표시 - 마이그레이션 스크립트 추가 (29_add_revision_status.sql)
This commit is contained in:
@@ -533,72 +533,67 @@ async def upload_file(
|
||||
})
|
||||
materials_inserted += 1
|
||||
|
||||
# 리비전 업로드인 경우 차이분 계산
|
||||
materials_diff = []
|
||||
original_materials_to_classify = materials_to_classify.copy() # 원본 보존
|
||||
# 리비전 업로드의 경우 변경사항 추적
|
||||
changed_materials_keys = set()
|
||||
new_materials_keys = set()
|
||||
|
||||
if parent_file_id is not None:
|
||||
# 새 파일의 자재들을 수량별로 그룹화
|
||||
new_materials_grouped = {}
|
||||
for material_data in original_materials_to_classify:
|
||||
description = material_data["original_description"]
|
||||
size_spec = material_data["size_spec"]
|
||||
quantity = float(material_data.get("quantity", 0))
|
||||
|
||||
material_key = f"{description}|{size_spec or ''}"
|
||||
if material_key in new_materials_grouped:
|
||||
new_materials_grouped[material_key]["quantity"] += quantity
|
||||
new_materials_grouped[material_key]["items"].append(material_data)
|
||||
print(f"🔄 리비전 업로드: 전체 {len(materials_to_classify)}개 자재 저장 (변경사항 추적 포함)")
|
||||
|
||||
# 이전 리비전의 자재 조회 (도면번호 기준)
|
||||
prev_materials_query = text("""
|
||||
SELECT original_description, size_spec, material_grade, main_nom,
|
||||
drawing_name, line_no, quantity
|
||||
FROM materials
|
||||
WHERE file_id = :parent_file_id
|
||||
""")
|
||||
prev_materials_result = db.execute(prev_materials_query, {"parent_file_id": parent_file_id}).fetchall()
|
||||
|
||||
# 이전 자재를 딕셔너리로 변환 (도면번호 + 설명 + 크기 + 재질로 키 생성)
|
||||
prev_materials_dict = {}
|
||||
for prev_mat in prev_materials_result:
|
||||
if prev_mat.drawing_name:
|
||||
key = f"{prev_mat.drawing_name}|{prev_mat.original_description}|{prev_mat.size_spec or ''}|{prev_mat.material_grade or ''}"
|
||||
elif prev_mat.line_no:
|
||||
key = f"{prev_mat.line_no}|{prev_mat.original_description}|{prev_mat.size_spec or ''}|{prev_mat.material_grade or ''}"
|
||||
else:
|
||||
new_materials_grouped[material_key] = {
|
||||
"quantity": quantity,
|
||||
"items": [material_data],
|
||||
"description": description,
|
||||
"size_spec": size_spec
|
||||
}
|
||||
|
||||
# 차이분 계산
|
||||
print(f"🔍 차이분 계산 시작: 신규 {len(new_materials_grouped)}개 vs 기존 {len(existing_materials_with_quantity)}개")
|
||||
|
||||
for material_key, new_data in new_materials_grouped.items():
|
||||
existing_quantity = existing_materials_with_quantity.get(material_key, 0)
|
||||
new_quantity = new_data["quantity"]
|
||||
key = f"{prev_mat.original_description}|{prev_mat.size_spec or ''}|{prev_mat.material_grade or ''}"
|
||||
|
||||
# 디버깅: 첫 10개 키 매칭 상태 출력
|
||||
if len(materials_diff) < 10:
|
||||
print(f"🔑 키 매칭: '{material_key}' → 기존:{existing_quantity}, 신규:{new_quantity}")
|
||||
prev_materials_dict[key] = {
|
||||
"quantity": float(prev_mat.quantity) if prev_mat.quantity else 0,
|
||||
"description": prev_mat.original_description
|
||||
}
|
||||
|
||||
# 새 자재와 비교하여 변경사항 감지
|
||||
for material_data in materials_to_classify:
|
||||
desc = material_data["original_description"]
|
||||
size_spec = material_data.get("size_spec", "")
|
||||
material_grade = material_data.get("material_grade", "")
|
||||
dwg_name = material_data.get("dwg_name")
|
||||
line_num = material_data.get("line_num")
|
||||
|
||||
if new_quantity > existing_quantity:
|
||||
# 증가분이 있는 경우
|
||||
diff_quantity = new_quantity - existing_quantity
|
||||
print(f"차이분 발견: {new_data['description'][:50]}... (증가: {diff_quantity})")
|
||||
if dwg_name:
|
||||
new_key = f"{dwg_name}|{desc}|{size_spec}|{material_grade}"
|
||||
elif line_num:
|
||||
new_key = f"{line_num}|{desc}|{size_spec}|{material_grade}"
|
||||
else:
|
||||
new_key = f"{desc}|{size_spec}|{material_grade}"
|
||||
|
||||
if new_key in prev_materials_dict:
|
||||
# 기존에 있던 자재 - 수량 변경 확인
|
||||
prev_qty = prev_materials_dict[new_key]["quantity"]
|
||||
new_qty = float(material_data.get("quantity", 0))
|
||||
|
||||
# 증가분만큼 자재 데이터 생성
|
||||
for item in new_data["items"]:
|
||||
if diff_quantity <= 0:
|
||||
break
|
||||
|
||||
item_quantity = float(item.get("quantity", 0))
|
||||
if item_quantity <= diff_quantity:
|
||||
# 이 아이템 전체를 포함
|
||||
materials_diff.append(item)
|
||||
diff_quantity -= item_quantity
|
||||
new_materials_count += 1
|
||||
else:
|
||||
# 이 아이템의 일부만 포함
|
||||
item_copy = item.copy()
|
||||
item_copy["quantity"] = diff_quantity
|
||||
materials_diff.append(item_copy)
|
||||
new_materials_count += 1
|
||||
break
|
||||
if abs(prev_qty - new_qty) > 0.001:
|
||||
# 수량이 변경됨
|
||||
changed_materials_keys.add(new_key)
|
||||
print(f"🔄 변경 감지: {desc[:40]}... (수량: {prev_qty} → {new_qty})")
|
||||
else:
|
||||
print(f"수량 감소/동일: {new_data['description'][:50]}... (기존:{existing_quantity} 새:{new_quantity})")
|
||||
# 새로 추가된 자재
|
||||
new_materials_keys.add(new_key)
|
||||
print(f"➕ 신규 감지: {desc[:40]}...")
|
||||
|
||||
# 차이분만 처리하도록 materials_to_classify 교체
|
||||
materials_to_classify = materials_diff
|
||||
print(f"차이분 자재 개수: {len(materials_to_classify)}")
|
||||
print(f"🔄 리비전 업로드: 차이분 {len(materials_diff)}개 라인 아이템 분류 처리")
|
||||
print(f"📊 차이분 요약: {len(new_materials_grouped)}개 자재 그룹 → {len(materials_diff)}개 라인 아이템")
|
||||
print(f"📊 변경사항 요약: 변경 {len(changed_materials_keys)}개, 신규 {len(new_materials_keys)}개")
|
||||
else:
|
||||
print(f"🆕 신규 업로드: 전체 {len(materials_to_classify)}개 분류 처리")
|
||||
|
||||
@@ -728,6 +723,8 @@ async def upload_file(
|
||||
print(f" size_spec: '{material_data['size_spec']}'")
|
||||
print(f" original_description: {material_data['original_description']}")
|
||||
print(f" category: {classification_result.get('category', 'UNKNOWN')}")
|
||||
print(f" drawing_name: {material_data.get('dwg_name')}")
|
||||
print(f" line_no: {material_data.get('line_num')}")
|
||||
|
||||
material_result = db.execute(material_insert_query, {
|
||||
"file_id": file_id,
|
||||
@@ -752,7 +749,37 @@ async def upload_file(
|
||||
material_id = material_result.fetchone()[0]
|
||||
materials_inserted += 1
|
||||
|
||||
# 리비전 업로드인 경우 신규 자재 카운트는 이미 위에서 처리됨
|
||||
# 리비전 업로드인 경우 변경/신규 자재 표시 및 구매신청 정보 상속
|
||||
if parent_file_id is not None:
|
||||
# 현재 자재의 키 생성
|
||||
dwg_name = material_data.get("dwg_name")
|
||||
line_num = material_data.get("line_num")
|
||||
|
||||
if dwg_name:
|
||||
current_key = f"{dwg_name}|{description}|{size_spec}|{material_data.get('material_grade', '')}"
|
||||
elif line_num:
|
||||
current_key = f"{line_num}|{description}|{size_spec}|{material_data.get('material_grade', '')}"
|
||||
else:
|
||||
current_key = f"{description}|{size_spec}|{material_data.get('material_grade', '')}"
|
||||
|
||||
# 변경 또는 신규 자재에 상태 표시
|
||||
if current_key in changed_materials_keys:
|
||||
# 변경된 자재
|
||||
update_status_query = text("""
|
||||
UPDATE materials SET revision_status = 'changed'
|
||||
WHERE id = :material_id
|
||||
""")
|
||||
db.execute(update_status_query, {"material_id": material_id})
|
||||
elif current_key in new_materials_keys:
|
||||
# 신규 자재 (추가됨)
|
||||
update_status_query = text("""
|
||||
UPDATE materials SET revision_status = 'active'
|
||||
WHERE id = :material_id
|
||||
""")
|
||||
db.execute(update_status_query, {"material_id": material_id})
|
||||
|
||||
# 구매신청 정보 상속은 전체 자재 저장 후 일괄 처리하도록 변경
|
||||
# (개별 처리는 비효율적이고 수량 계산이 복잡함)
|
||||
|
||||
# PIPE 분류 결과인 경우 상세 정보 저장
|
||||
if classification_result.get("category") == "PIPE":
|
||||
@@ -1432,6 +1459,83 @@ async def upload_file(
|
||||
db.commit()
|
||||
print(f"자재 저장 완료: {materials_inserted}개")
|
||||
|
||||
# 리비전 업로드인 경우 구매신청 정보 수량 기반 상속
|
||||
if parent_file_id is not None:
|
||||
try:
|
||||
print(f"🔄 구매신청 정보 상속 처리 시작...")
|
||||
|
||||
# 1. 이전 리비전에서 그룹별 구매신청 수량 집계
|
||||
prev_purchase_summary = text("""
|
||||
SELECT
|
||||
m.original_description,
|
||||
m.size_spec,
|
||||
m.material_grade,
|
||||
m.drawing_name,
|
||||
COUNT(DISTINCT pri.material_id) as purchased_count,
|
||||
SUM(pri.quantity) as total_purchased_qty,
|
||||
MIN(pri.request_id) as request_id
|
||||
FROM materials m
|
||||
JOIN purchase_request_items pri ON m.id = pri.material_id
|
||||
WHERE m.file_id = :parent_file_id
|
||||
GROUP BY m.original_description, m.size_spec, m.material_grade, m.drawing_name
|
||||
""")
|
||||
|
||||
prev_purchases = db.execute(prev_purchase_summary, {"parent_file_id": parent_file_id}).fetchall()
|
||||
|
||||
# 2. 새 리비전에서 같은 그룹의 자재에 수량만큼 구매신청 상속
|
||||
for prev_purchase in prev_purchases:
|
||||
purchased_count = prev_purchase.purchased_count
|
||||
|
||||
# 새 리비전에서 같은 그룹의 자재 조회 (순서대로)
|
||||
new_group_materials = text("""
|
||||
SELECT id, quantity
|
||||
FROM materials
|
||||
WHERE file_id = :file_id
|
||||
AND original_description = :description
|
||||
AND COALESCE(size_spec, '') = :size_spec
|
||||
AND COALESCE(material_grade, '') = :material_grade
|
||||
AND COALESCE(drawing_name, '') = :drawing_name
|
||||
ORDER BY id
|
||||
LIMIT :limit
|
||||
""")
|
||||
|
||||
new_materials = db.execute(new_group_materials, {
|
||||
"file_id": file_id,
|
||||
"description": prev_purchase.original_description,
|
||||
"size_spec": prev_purchase.size_spec or '',
|
||||
"material_grade": prev_purchase.material_grade or '',
|
||||
"drawing_name": prev_purchase.drawing_name or '',
|
||||
"limit": purchased_count
|
||||
}).fetchall()
|
||||
|
||||
# 구매신청 수량만큼만 상속
|
||||
for new_mat in new_materials:
|
||||
inherit_query = text("""
|
||||
INSERT INTO purchase_request_items (
|
||||
request_id, material_id, quantity, unit, user_requirement
|
||||
) VALUES (
|
||||
:request_id, :material_id, :quantity, 'EA', ''
|
||||
)
|
||||
ON CONFLICT DO NOTHING
|
||||
""")
|
||||
db.execute(inherit_query, {
|
||||
"request_id": prev_purchase.request_id,
|
||||
"material_id": new_mat.id,
|
||||
"quantity": new_mat.quantity
|
||||
})
|
||||
|
||||
inherited_count = len(new_materials)
|
||||
if inherited_count > 0:
|
||||
print(f" ✅ {prev_purchase.original_description[:30]}... (도면: {prev_purchase.drawing_name or 'N/A'}) → {inherited_count}/{purchased_count}개 상속")
|
||||
|
||||
db.commit()
|
||||
print(f"✅ 구매신청 정보 상속 완료")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 구매신청 정보 상속 실패: {str(e)}")
|
||||
db.rollback()
|
||||
# 상속 실패는 업로드 성공에 영향 없음
|
||||
|
||||
# 활동 로그 기록
|
||||
try:
|
||||
activity_logger = ActivityLogger(db)
|
||||
@@ -1451,25 +1555,136 @@ async def upload_file(
|
||||
print(f"활동 로그 기록 실패: {str(e)}")
|
||||
# 로그 실패는 업로드 성공에 영향을 주지 않음
|
||||
|
||||
# 리비전 업로드인 경우 누락된 도면 감지 및 구매신청 여부 확인
|
||||
missing_drawings_info = None
|
||||
has_previous_purchase = False
|
||||
|
||||
if parent_file_id is not None:
|
||||
try:
|
||||
# 이전 리비전의 구매신청 여부 확인
|
||||
purchase_check_query = text("""
|
||||
SELECT COUNT(*) as purchase_count
|
||||
FROM purchase_request_items pri
|
||||
JOIN materials m ON pri.material_id = m.id
|
||||
WHERE m.file_id = :parent_file_id
|
||||
""")
|
||||
purchase_result = db.execute(purchase_check_query, {"parent_file_id": parent_file_id}).fetchone()
|
||||
has_previous_purchase = purchase_result.purchase_count > 0
|
||||
|
||||
print(f"📦 이전 리비전 구매신청 여부: {has_previous_purchase} ({purchase_result.purchase_count}개 자재)")
|
||||
print(f"📂 parent_file_id: {parent_file_id}, new file_id: {file_id}")
|
||||
|
||||
# 이전 리비전의 도면 목록 조회
|
||||
prev_drawings_query = text("""
|
||||
SELECT DISTINCT drawing_name, line_no, COUNT(*) as material_count
|
||||
FROM materials
|
||||
WHERE file_id = :parent_file_id
|
||||
AND (drawing_name IS NOT NULL OR line_no IS NOT NULL)
|
||||
GROUP BY drawing_name, line_no
|
||||
""")
|
||||
prev_drawings_result = db.execute(prev_drawings_query, {"parent_file_id": parent_file_id}).fetchall()
|
||||
print(f"📋 이전 리비전 도면 수: {len(prev_drawings_result)}")
|
||||
|
||||
# 새 리비전의 도면 목록 조회
|
||||
new_drawings_query = text("""
|
||||
SELECT DISTINCT drawing_name, line_no
|
||||
FROM materials
|
||||
WHERE file_id = :file_id
|
||||
AND (drawing_name IS NOT NULL OR line_no IS NOT NULL)
|
||||
""")
|
||||
new_drawings_result = db.execute(new_drawings_query, {"file_id": file_id}).fetchall()
|
||||
print(f"📋 새 리비전 도면 수: {len(new_drawings_result)}")
|
||||
|
||||
prev_drawings = set()
|
||||
for row in prev_drawings_result:
|
||||
if row.drawing_name:
|
||||
prev_drawings.add(row.drawing_name)
|
||||
elif row.line_no:
|
||||
prev_drawings.add(row.line_no)
|
||||
|
||||
new_drawings = set()
|
||||
for row in new_drawings_result:
|
||||
if row.drawing_name:
|
||||
new_drawings.add(row.drawing_name)
|
||||
elif row.line_no:
|
||||
new_drawings.add(row.line_no)
|
||||
|
||||
missing_drawings = prev_drawings - new_drawings
|
||||
|
||||
print(f"📊 이전 도면: {list(prev_drawings)[:5]}")
|
||||
print(f"📊 새 도면: {list(new_drawings)[:5]}")
|
||||
print(f"❌ 누락 도면: {len(missing_drawings)}개")
|
||||
|
||||
if missing_drawings:
|
||||
# 누락된 도면의 자재 상세 정보
|
||||
missing_materials = []
|
||||
for row in prev_drawings_result:
|
||||
drawing = row.drawing_name or row.line_no
|
||||
if drawing in missing_drawings:
|
||||
missing_materials.append({
|
||||
"drawing_name": drawing,
|
||||
"material_count": row.material_count
|
||||
})
|
||||
|
||||
missing_drawings_info = {
|
||||
"drawings": list(missing_drawings),
|
||||
"materials": missing_materials,
|
||||
"count": len(missing_drawings),
|
||||
"requires_confirmation": True,
|
||||
"has_previous_purchase": has_previous_purchase,
|
||||
"action": "mark_as_inventory" if has_previous_purchase else "hide_materials"
|
||||
}
|
||||
|
||||
# 누락된 도면의 자재 상태 업데이트
|
||||
if missing_drawings:
|
||||
for drawing in missing_drawings:
|
||||
status_to_set = 'inventory' if has_previous_purchase else 'deleted_not_purchased'
|
||||
|
||||
update_status_query = text("""
|
||||
UPDATE materials
|
||||
SET revision_status = :status
|
||||
WHERE file_id = :parent_file_id
|
||||
AND (drawing_name = :drawing OR line_no = :drawing)
|
||||
""")
|
||||
|
||||
db.execute(update_status_query, {
|
||||
"status": status_to_set,
|
||||
"parent_file_id": parent_file_id,
|
||||
"drawing": drawing
|
||||
})
|
||||
|
||||
db.commit()
|
||||
print(f"✅ 누락 도면 자재 상태 업데이트: {len(missing_drawings)}개 도면 → {status_to_set}")
|
||||
except Exception as e:
|
||||
print(f"누락 도면 감지 실패: {str(e)}")
|
||||
db.rollback()
|
||||
# 감지 실패는 업로드 성공에 영향 없음
|
||||
|
||||
# 리비전 업로드인 경우 메시지 다르게 표시
|
||||
if parent_file_id is not None:
|
||||
message = f"리비전 업로드 성공! {new_materials_count}개의 신규 자재가 추가되었습니다."
|
||||
else:
|
||||
message = f"업로드 성공! {materials_inserted}개 자재가 분류되었습니다."
|
||||
|
||||
return {
|
||||
response_data = {
|
||||
"success": True,
|
||||
"message": message,
|
||||
"original_filename": file.filename,
|
||||
"file_id": file_id,
|
||||
"materials_count": materials_inserted,
|
||||
"saved_materials_count": materials_inserted,
|
||||
"new_materials_count": new_materials_count if parent_file_id is not None else None, # 신규 자재 수
|
||||
"revision": revision, # 생성된 리비전 정보 추가
|
||||
"uploaded_by": username, # 업로드한 사용자 정보 추가
|
||||
"new_materials_count": new_materials_count if parent_file_id is not None else None,
|
||||
"revision": revision,
|
||||
"uploaded_by": username,
|
||||
"parsed_count": parsed_count
|
||||
}
|
||||
|
||||
# 누락된 도면 정보 추가
|
||||
if missing_drawings_info:
|
||||
response_data["missing_drawings"] = missing_drawings_info
|
||||
|
||||
return response_data
|
||||
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
if os.path.exists(file_path):
|
||||
@@ -1612,7 +1827,7 @@ async def get_materials(
|
||||
query = """
|
||||
SELECT m.id, m.file_id, m.original_description, m.quantity, m.unit,
|
||||
m.size_spec, m.main_nom, m.red_nom, m.material_grade, m.full_material_grade, m.line_number, m.row_number,
|
||||
m.drawing_name, m.line_no,
|
||||
m.drawing_name, m.line_no, m.revision_status,
|
||||
m.created_at, m.classified_category, m.classification_confidence,
|
||||
m.classification_details,
|
||||
m.is_verified, m.verified_by, m.verified_at,
|
||||
@@ -1821,6 +2036,7 @@ async def get_materials(
|
||||
"full_material_grade": m.full_material_grade,
|
||||
"drawing_name": m.drawing_name,
|
||||
"line_no": m.line_no,
|
||||
"revision_status": m.revision_status or 'active',
|
||||
"line_number": m.line_number,
|
||||
"row_number": m.row_number,
|
||||
# 구매수량 계산에서 분류된 정보를 우선 사용
|
||||
@@ -2358,7 +2574,8 @@ async def compare_revisions(
|
||||
# 기존 리비전 자재 조회
|
||||
old_materials_query = text("""
|
||||
SELECT m.original_description, m.quantity, m.unit, m.size_spec,
|
||||
m.material_grade, m.classified_category, m.classification_confidence
|
||||
m.material_grade, m.classified_category, m.classification_confidence,
|
||||
m.main_nom, m.red_nom, m.drawing_name, m.line_no
|
||||
FROM materials m
|
||||
JOIN files f ON m.file_id = f.id
|
||||
WHERE f.job_no = :job_no
|
||||
@@ -2376,7 +2593,8 @@ async def compare_revisions(
|
||||
# 새 리비전 자재 조회
|
||||
new_materials_query = text("""
|
||||
SELECT m.original_description, m.quantity, m.unit, m.size_spec,
|
||||
m.material_grade, m.classified_category, m.classification_confidence
|
||||
m.material_grade, m.classified_category, m.classification_confidence,
|
||||
m.main_nom, m.red_nom, m.drawing_name, m.line_no
|
||||
FROM materials m
|
||||
JOIN files f ON m.file_id = f.id
|
||||
WHERE f.job_no = :job_no
|
||||
@@ -2391,9 +2609,17 @@ async def compare_revisions(
|
||||
})
|
||||
new_materials = new_result.fetchall()
|
||||
|
||||
# 자재 키 생성 함수 (전체 수량 기준)
|
||||
# 자재 키 생성 함수 (도면번호 + 자재 정보)
|
||||
def create_material_key(material):
|
||||
return f"{material.original_description}|{material.size_spec or ''}|{material.material_grade or ''}"
|
||||
# 도면번호가 있으면 도면번호 + 자재 설명으로 고유 키 생성
|
||||
# (같은 도면에 여러 자재가 있을 수 있으므로)
|
||||
if hasattr(material, 'drawing_name') and material.drawing_name:
|
||||
return f"{material.drawing_name}|{material.original_description}|{material.size_spec or ''}|{material.material_grade or ''}"
|
||||
elif hasattr(material, 'line_no') and material.line_no:
|
||||
return f"{material.line_no}|{material.original_description}|{material.size_spec or ''}|{material.material_grade or ''}"
|
||||
else:
|
||||
# 도면번호 없으면 기존 방식 (설명 + 크기 + 재질)
|
||||
return f"{material.original_description}|{material.size_spec or ''}|{material.material_grade or ''}"
|
||||
|
||||
# 기존 자재를 딕셔너리로 변환 (수량 합산)
|
||||
old_materials_dict = {}
|
||||
@@ -2410,7 +2636,11 @@ async def compare_revisions(
|
||||
"size_spec": material.size_spec,
|
||||
"material_grade": material.material_grade,
|
||||
"classified_category": material.classified_category,
|
||||
"classification_confidence": material.classification_confidence
|
||||
"classification_confidence": material.classification_confidence,
|
||||
"main_nom": material.main_nom,
|
||||
"red_nom": material.red_nom,
|
||||
"drawing_name": material.drawing_name,
|
||||
"line_no": material.line_no
|
||||
}
|
||||
|
||||
# 새 자재를 딕셔너리로 변환 (수량 합산)
|
||||
@@ -2428,7 +2658,11 @@ async def compare_revisions(
|
||||
"size_spec": material.size_spec,
|
||||
"material_grade": material.material_grade,
|
||||
"classified_category": material.classified_category,
|
||||
"classification_confidence": material.classification_confidence
|
||||
"classification_confidence": material.classification_confidence,
|
||||
"main_nom": material.main_nom,
|
||||
"red_nom": material.red_nom,
|
||||
"drawing_name": material.drawing_name,
|
||||
"line_no": material.line_no
|
||||
}
|
||||
|
||||
# 변경 사항 분석
|
||||
@@ -2457,22 +2691,68 @@ async def compare_revisions(
|
||||
"change_type": "added"
|
||||
})
|
||||
elif old_item and new_item:
|
||||
# 수량 변경 확인 (전체 수량 기준)
|
||||
# 변경 사항 감지: 수량, 재질, 크기, 카테고리 등
|
||||
changes_detected = []
|
||||
|
||||
old_qty = old_item["quantity"]
|
||||
new_qty = new_item["quantity"]
|
||||
qty_diff = new_qty - old_qty
|
||||
|
||||
# 수량 차이가 있으면 변경된 것으로 간주 (소수점 오차 고려)
|
||||
# 1. 수량 변경 확인
|
||||
if abs(qty_diff) > 0.001:
|
||||
change_type = "quantity_increased" if qty_diff > 0 else "quantity_decreased"
|
||||
changes_detected.append({
|
||||
"type": "quantity",
|
||||
"old_value": old_qty,
|
||||
"new_value": new_qty,
|
||||
"diff": qty_diff
|
||||
})
|
||||
|
||||
# 2. 재질 변경 확인
|
||||
if old_item.get("material_grade") != new_item.get("material_grade"):
|
||||
changes_detected.append({
|
||||
"type": "material",
|
||||
"old_value": old_item.get("material_grade", "-"),
|
||||
"new_value": new_item.get("material_grade", "-")
|
||||
})
|
||||
|
||||
# 3. 크기 변경 확인
|
||||
if old_item.get("main_nom") != new_item.get("main_nom") or old_item.get("size_spec") != new_item.get("size_spec"):
|
||||
changes_detected.append({
|
||||
"type": "size",
|
||||
"old_value": old_item.get("main_nom") or old_item.get("size_spec", "-"),
|
||||
"new_value": new_item.get("main_nom") or new_item.get("size_spec", "-")
|
||||
})
|
||||
|
||||
# 4. 카테고리 변경 확인 (자재 종류가 바뀜)
|
||||
if old_item.get("classified_category") != new_item.get("classified_category"):
|
||||
changes_detected.append({
|
||||
"type": "category",
|
||||
"old_value": old_item.get("classified_category", "-"),
|
||||
"new_value": new_item.get("classified_category", "-")
|
||||
})
|
||||
|
||||
# 변경사항이 있으면 changed_items에 추가
|
||||
if changes_detected:
|
||||
# 변경 유형 결정
|
||||
has_qty_change = any(c["type"] == "quantity" for c in changes_detected)
|
||||
has_spec_change = any(c["type"] in ["material", "size", "category"] for c in changes_detected)
|
||||
|
||||
if has_spec_change:
|
||||
change_type = "specification_changed" # 자재 사양 변경
|
||||
elif has_qty_change:
|
||||
change_type = "quantity_changed" # 수량만 변경
|
||||
else:
|
||||
change_type = "modified"
|
||||
|
||||
changed_items.append({
|
||||
"key": key,
|
||||
"old_item": old_item,
|
||||
"new_item": new_item,
|
||||
"quantity_change": qty_diff,
|
||||
"quantity_change_abs": abs(qty_diff),
|
||||
"changes": changes_detected,
|
||||
"change_type": change_type,
|
||||
"change_percentage": (qty_diff / old_qty * 100) if old_qty > 0 else 0
|
||||
"quantity_change": qty_diff if has_qty_change else 0,
|
||||
"drawing_name": new_item.get("drawing_name") or old_item.get("drawing_name"),
|
||||
"line_no": new_item.get("line_no") or old_item.get("line_no")
|
||||
})
|
||||
|
||||
# 분류별 통계
|
||||
@@ -2490,6 +2770,40 @@ async def compare_revisions(
|
||||
removed_stats = calculate_category_stats(removed_items)
|
||||
changed_stats = calculate_category_stats(changed_items)
|
||||
|
||||
# 누락된 도면 감지
|
||||
old_drawings = set()
|
||||
new_drawings = set()
|
||||
|
||||
for material in old_materials:
|
||||
if hasattr(material, 'drawing_name') and material.drawing_name:
|
||||
old_drawings.add(material.drawing_name)
|
||||
elif hasattr(material, 'line_no') and material.line_no:
|
||||
old_drawings.add(material.line_no)
|
||||
|
||||
for material in new_materials:
|
||||
if hasattr(material, 'drawing_name') and material.drawing_name:
|
||||
new_drawings.add(material.drawing_name)
|
||||
elif hasattr(material, 'line_no') and material.line_no:
|
||||
new_drawings.add(material.line_no)
|
||||
|
||||
missing_drawings = old_drawings - new_drawings # 이전에는 있었는데 새 파일에 없는 도면
|
||||
new_only_drawings = new_drawings - old_drawings # 새로 추가된 도면
|
||||
|
||||
# 누락된 도면의 자재 목록
|
||||
missing_drawing_materials = []
|
||||
if missing_drawings:
|
||||
for material in old_materials:
|
||||
drawing = getattr(material, 'drawing_name', None) or getattr(material, 'line_no', None)
|
||||
if drawing in missing_drawings:
|
||||
missing_drawing_materials.append({
|
||||
"drawing_name": drawing,
|
||||
"description": material.original_description,
|
||||
"quantity": float(material.quantity) if material.quantity else 0,
|
||||
"category": material.classified_category,
|
||||
"size": material.main_nom or material.size_spec,
|
||||
"material_grade": material.material_grade
|
||||
})
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"comparison": {
|
||||
@@ -2501,7 +2815,15 @@ async def compare_revisions(
|
||||
"added_count": len(added_items),
|
||||
"removed_count": len(removed_items),
|
||||
"changed_count": len(changed_items),
|
||||
"total_changes": len(added_items) + len(removed_items) + len(changed_items)
|
||||
"total_changes": len(added_items) + len(removed_items) + len(changed_items),
|
||||
"missing_drawings_count": len(missing_drawings),
|
||||
"new_drawings_count": len(new_only_drawings)
|
||||
},
|
||||
"missing_drawings": {
|
||||
"drawings": list(missing_drawings),
|
||||
"materials": missing_drawing_materials,
|
||||
"count": len(missing_drawings),
|
||||
"requires_confirmation": len(missing_drawings) > 0
|
||||
},
|
||||
"changes": {
|
||||
"added": added_items,
|
||||
@@ -3291,4 +3613,91 @@ async def log_materials_view(
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"조회 로그 기록 실패: {str(e)}")
|
||||
return {"message": "조회 로그 기록 실패"}
|
||||
return {"message": "조회 로그 기록 실패"}
|
||||
|
||||
|
||||
@router.post("/{file_id}/process-missing-drawings")
|
||||
async def process_missing_drawings(
|
||||
file_id: int,
|
||||
action: str = "delete",
|
||||
drawings: List[str] = [],
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
누락된 도면 처리
|
||||
- action='delete': 누락된 도면의 이전 리비전 자재 삭제 처리
|
||||
- action='keep': 누락된 도면의 이전 리비전 자재 유지
|
||||
"""
|
||||
try:
|
||||
# 현재 파일 정보 확인
|
||||
from pydantic import BaseModel
|
||||
|
||||
class MissingDrawingsRequest(BaseModel):
|
||||
action: str
|
||||
drawings: List[str]
|
||||
|
||||
# parent_file_id 조회는 files 테이블에 없을 수 있으므로 revision으로 판단
|
||||
file_query = text("""
|
||||
SELECT f.id, f.job_no, f.revision, f.original_filename
|
||||
FROM files f
|
||||
WHERE f.id = :file_id
|
||||
""")
|
||||
file_result = db.execute(file_query, {"file_id": file_id}).fetchone()
|
||||
|
||||
if not file_result:
|
||||
raise HTTPException(status_code=404, detail="파일을 찾을 수 없습니다")
|
||||
|
||||
# 이전 리비전 파일 찾기
|
||||
prev_revision_query = text("""
|
||||
SELECT id FROM files
|
||||
WHERE job_no = :job_no
|
||||
AND original_filename = :filename
|
||||
AND revision < :current_revision
|
||||
ORDER BY revision DESC
|
||||
LIMIT 1
|
||||
""")
|
||||
prev_result = db.execute(prev_revision_query, {
|
||||
"job_no": file_result.job_no,
|
||||
"filename": file_result.original_filename,
|
||||
"current_revision": file_result.revision
|
||||
}).fetchone()
|
||||
|
||||
if not prev_result:
|
||||
raise HTTPException(status_code=400, detail="이전 리비전을 찾을 수 없습니다")
|
||||
|
||||
parent_file_id = prev_result.id
|
||||
|
||||
if action == "delete":
|
||||
# 누락된 도면의 자재를 deleted_not_purchased 상태로 변경
|
||||
for drawing in drawings:
|
||||
update_query = text("""
|
||||
UPDATE materials
|
||||
SET revision_status = 'deleted_not_purchased'
|
||||
WHERE file_id = :parent_file_id
|
||||
AND (drawing_name = :drawing OR line_no = :drawing)
|
||||
""")
|
||||
db.execute(update_query, {
|
||||
"parent_file_id": parent_file_id,
|
||||
"drawing": drawing
|
||||
})
|
||||
|
||||
db.commit()
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"{len(drawings)}개 도면의 자재가 삭제 처리되었습니다",
|
||||
"action": "deleted",
|
||||
"drawings_count": len(drawings)
|
||||
}
|
||||
else:
|
||||
# keep - 이미 처리됨 (inventory 또는 active 상태 유지)
|
||||
return {
|
||||
"success": True,
|
||||
"message": "누락된 도면의 자재가 유지됩니다",
|
||||
"action": "kept",
|
||||
"drawings_count": len(drawings)
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=500, detail=f"도면 처리 실패: {str(e)}")
|
||||
@@ -290,6 +290,13 @@ async def get_request_materials(
|
||||
|
||||
materials = []
|
||||
for row in results:
|
||||
# quantity를 정수로 변환 (소수점 제거)
|
||||
qty = row.requested_quantity or row.original_quantity
|
||||
try:
|
||||
qty_int = int(float(qty)) if qty else 0
|
||||
except (ValueError, TypeError):
|
||||
qty_int = 0
|
||||
|
||||
materials.append({
|
||||
"item_id": row.item_id,
|
||||
"material_id": row.material_id,
|
||||
@@ -298,7 +305,7 @@ async def get_request_materials(
|
||||
"size": row.size_spec,
|
||||
"schedule": row.schedule,
|
||||
"material_grade": row.material_grade,
|
||||
"quantity": row.requested_quantity or row.original_quantity,
|
||||
"quantity": qty_int, # 정수로 변환
|
||||
"unit": row.requested_unit or row.original_unit,
|
||||
"user_requirement": row.user_requirement,
|
||||
"is_ordered": row.is_ordered,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -1,90 +0,0 @@
|
||||
{
|
||||
"request_no": "PR-20251014-004",
|
||||
"job_no": "TKG-25000P",
|
||||
"created_at": "2025-10-14T02:10:22.262092",
|
||||
"materials": [
|
||||
{
|
||||
"material_id": 77528,
|
||||
"description": "PIPE, SMLS, SCH 40S, ASTM A312 TP304",
|
||||
"category": "PIPE",
|
||||
"size": "1/2\"",
|
||||
"material_grade": "ASTM A312 TP304",
|
||||
"quantity": 11,
|
||||
"unit": "EA",
|
||||
"user_requirement": "열처리?"
|
||||
}
|
||||
],
|
||||
"grouped_materials": [
|
||||
{
|
||||
"group_key": "PIPE, SMLS, SCH 40S, ASTM A312 TP304|1/2\"|undefined|ASTM A312 TP304",
|
||||
"material_ids": [
|
||||
77528
|
||||
],
|
||||
"description": "PIPE, SMLS, SCH 40S, ASTM A312 TP304",
|
||||
"category": "PIPE",
|
||||
"size": "1/2\"",
|
||||
"material_grade": "ASTM A312 TP304",
|
||||
"quantity": 11,
|
||||
"unit": "m",
|
||||
"total_length": 1395.1,
|
||||
"pipe_lengths": [
|
||||
{
|
||||
"length": 70,
|
||||
"quantity": 1,
|
||||
"totalLength": 70
|
||||
},
|
||||
{
|
||||
"length": 70,
|
||||
"quantity": 1,
|
||||
"totalLength": 70
|
||||
},
|
||||
{
|
||||
"length": 100,
|
||||
"quantity": 1,
|
||||
"totalLength": 100
|
||||
},
|
||||
{
|
||||
"length": 100,
|
||||
"quantity": 1,
|
||||
"totalLength": 100
|
||||
},
|
||||
{
|
||||
"length": 155,
|
||||
"quantity": 1,
|
||||
"totalLength": 155
|
||||
},
|
||||
{
|
||||
"length": 155,
|
||||
"quantity": 1,
|
||||
"totalLength": 155
|
||||
},
|
||||
{
|
||||
"length": 200,
|
||||
"quantity": 1,
|
||||
"totalLength": 200
|
||||
},
|
||||
{
|
||||
"length": 245.1,
|
||||
"quantity": 1,
|
||||
"totalLength": 245.1
|
||||
},
|
||||
{
|
||||
"length": 100,
|
||||
"quantity": 1,
|
||||
"totalLength": 100
|
||||
},
|
||||
{
|
||||
"length": 100,
|
||||
"quantity": 1,
|
||||
"totalLength": 100
|
||||
},
|
||||
{
|
||||
"length": 100,
|
||||
"quantity": 1,
|
||||
"totalLength": 100
|
||||
}
|
||||
],
|
||||
"user_requirement": "열처리?"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
{
|
||||
"request_no": "PR-20251014-005",
|
||||
"job_no": "TKG-25000P",
|
||||
"created_at": "2025-10-14T02:14:05.318457",
|
||||
"materials": [
|
||||
{
|
||||
"material_id": 78247,
|
||||
"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": ""
|
||||
}
|
||||
],
|
||||
"grouped_materials": [
|
||||
{
|
||||
"group_key": "FLG WELD NECK RF SCH 40S, 150LB, ASTM A182 F304|10\"|undefined|ASTM A182 F304",
|
||||
"material_ids": [
|
||||
78247
|
||||
],
|
||||
"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": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
{
|
||||
"request_no": "PR-20251014-006",
|
||||
"job_no": "TKG-25000P",
|
||||
"created_at": "2025-10-14T02:17:13.397257",
|
||||
"materials": [
|
||||
{
|
||||
"material_id": 78599,
|
||||
"description": "(4) 0.5, 75 LG, 150LB, ASTM A193/A194 GR B7/2H, ELEC.GALV",
|
||||
"category": "BOLT",
|
||||
"size": "1\"",
|
||||
"material_grade": "ASTM A193/A194 GR B7/2H",
|
||||
"quantity": 51,
|
||||
"unit": "EA",
|
||||
"user_requirement": ""
|
||||
}
|
||||
],
|
||||
"grouped_materials": [
|
||||
{
|
||||
"group_key": "(4) 0.5, 75 LG, 150LB, ASTM A193/A194 GR B7/2H, ELEC.GALV|1\"|undefined|ASTM A193/A194 GR B7/2H",
|
||||
"material_ids": [
|
||||
78599
|
||||
],
|
||||
"description": "(4) 0.5, 75 LG, 150LB, ASTM A193/A194 GR B7/2H, ELEC.GALV",
|
||||
"category": "BOLT",
|
||||
"size": "1\"",
|
||||
"material_grade": "ASTM A193/A194 GR B7/2H",
|
||||
"quantity": 51,
|
||||
"unit": "EA",
|
||||
"user_requirement": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
{
|
||||
"request_no": "PR-20251014-007",
|
||||
"job_no": "TKG-25000P",
|
||||
"created_at": "2025-10-14T02:17:26.376309",
|
||||
"materials": [
|
||||
{
|
||||
"material_id": 78599,
|
||||
"description": "(4) 0.5, 75 LG, 150LB, ASTM A193/A194 GR B7/2H, ELEC.GALV",
|
||||
"category": "BOLT",
|
||||
"size": "1\"",
|
||||
"material_grade": "ASTM A193/A194 GR B7/2H",
|
||||
"quantity": 51,
|
||||
"unit": "EA",
|
||||
"user_requirement": ""
|
||||
}
|
||||
],
|
||||
"grouped_materials": [
|
||||
{
|
||||
"group_key": "(4) 0.5, 75 LG, 150LB, ASTM A193/A194 GR B7/2H, ELEC.GALV|1\"|undefined|ASTM A193/A194 GR B7/2H",
|
||||
"material_ids": [
|
||||
78599
|
||||
],
|
||||
"description": "(4) 0.5, 75 LG, 150LB, ASTM A193/A194 GR B7/2H, ELEC.GALV",
|
||||
"category": "BOLT",
|
||||
"size": "1\"",
|
||||
"material_grade": "ASTM A193/A194 GR B7/2H",
|
||||
"quantity": 51,
|
||||
"unit": "EA",
|
||||
"user_requirement": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,495 +0,0 @@
|
||||
{
|
||||
"request_no": "PR-20251014-008",
|
||||
"job_no": "TKG-25000P",
|
||||
"created_at": "2025-10-14T02:17:50.004262",
|
||||
"materials": [
|
||||
{
|
||||
"material_id": 77536,
|
||||
"description": "PIPE, SMLS, SCH 80, ASTM A106 B",
|
||||
"category": "PIPE",
|
||||
"size": "3/4\"",
|
||||
"material_grade": "ASTM A106 B",
|
||||
"quantity": 92,
|
||||
"unit": "EA",
|
||||
"user_requirement": ""
|
||||
}
|
||||
],
|
||||
"grouped_materials": [
|
||||
{
|
||||
"group_key": "PIPE, SMLS, SCH 80, ASTM A106 B|3/4\"|undefined|ASTM A106 B",
|
||||
"material_ids": [
|
||||
77536
|
||||
],
|
||||
"description": "PIPE, SMLS, SCH 80, ASTM A106 B",
|
||||
"category": "PIPE",
|
||||
"size": "3/4\"",
|
||||
"material_grade": "ASTM A106 B",
|
||||
"quantity": 92,
|
||||
"unit": "m",
|
||||
"total_length": 7920.2,
|
||||
"pipe_lengths": [
|
||||
{
|
||||
"length": 60,
|
||||
"quantity": 1,
|
||||
"totalLength": 60
|
||||
},
|
||||
{
|
||||
"length": 60,
|
||||
"quantity": 1,
|
||||
"totalLength": 60
|
||||
},
|
||||
{
|
||||
"length": 60,
|
||||
"quantity": 1,
|
||||
"totalLength": 60
|
||||
},
|
||||
{
|
||||
"length": 60,
|
||||
"quantity": 1,
|
||||
"totalLength": 60
|
||||
},
|
||||
{
|
||||
"length": 43.3,
|
||||
"quantity": 1,
|
||||
"totalLength": 43.3
|
||||
},
|
||||
{
|
||||
"length": 43.3,
|
||||
"quantity": 1,
|
||||
"totalLength": 43.3
|
||||
},
|
||||
{
|
||||
"length": 43.3,
|
||||
"quantity": 1,
|
||||
"totalLength": 43.3
|
||||
},
|
||||
{
|
||||
"length": 43.3,
|
||||
"quantity": 1,
|
||||
"totalLength": 43.3
|
||||
},
|
||||
{
|
||||
"length": 43.3,
|
||||
"quantity": 1,
|
||||
"totalLength": 43.3
|
||||
},
|
||||
{
|
||||
"length": 43.3,
|
||||
"quantity": 1,
|
||||
"totalLength": 43.3
|
||||
},
|
||||
{
|
||||
"length": 50,
|
||||
"quantity": 1,
|
||||
"totalLength": 50
|
||||
},
|
||||
{
|
||||
"length": 50,
|
||||
"quantity": 1,
|
||||
"totalLength": 50
|
||||
},
|
||||
{
|
||||
"length": 50,
|
||||
"quantity": 1,
|
||||
"totalLength": 50
|
||||
},
|
||||
{
|
||||
"length": 50,
|
||||
"quantity": 1,
|
||||
"totalLength": 50
|
||||
},
|
||||
{
|
||||
"length": 50,
|
||||
"quantity": 1,
|
||||
"totalLength": 50
|
||||
},
|
||||
{
|
||||
"length": 50,
|
||||
"quantity": 1,
|
||||
"totalLength": 50
|
||||
},
|
||||
{
|
||||
"length": 50,
|
||||
"quantity": 1,
|
||||
"totalLength": 50
|
||||
},
|
||||
{
|
||||
"length": 70,
|
||||
"quantity": 1,
|
||||
"totalLength": 70
|
||||
},
|
||||
{
|
||||
"length": 70,
|
||||
"quantity": 1,
|
||||
"totalLength": 70
|
||||
},
|
||||
{
|
||||
"length": 70,
|
||||
"quantity": 1,
|
||||
"totalLength": 70
|
||||
},
|
||||
{
|
||||
"length": 70,
|
||||
"quantity": 1,
|
||||
"totalLength": 70
|
||||
},
|
||||
{
|
||||
"length": 70,
|
||||
"quantity": 1,
|
||||
"totalLength": 70
|
||||
},
|
||||
{
|
||||
"length": 70,
|
||||
"quantity": 1,
|
||||
"totalLength": 70
|
||||
},
|
||||
{
|
||||
"length": 70,
|
||||
"quantity": 1,
|
||||
"totalLength": 70
|
||||
},
|
||||
{
|
||||
"length": 70,
|
||||
"quantity": 1,
|
||||
"totalLength": 70
|
||||
},
|
||||
{
|
||||
"length": 70,
|
||||
"quantity": 1,
|
||||
"totalLength": 70
|
||||
},
|
||||
{
|
||||
"length": 70,
|
||||
"quantity": 1,
|
||||
"totalLength": 70
|
||||
},
|
||||
{
|
||||
"length": 70,
|
||||
"quantity": 1,
|
||||
"totalLength": 70
|
||||
},
|
||||
{
|
||||
"length": 70,
|
||||
"quantity": 1,
|
||||
"totalLength": 70
|
||||
},
|
||||
{
|
||||
"length": 76.2,
|
||||
"quantity": 1,
|
||||
"totalLength": 76.2
|
||||
},
|
||||
{
|
||||
"length": 76.2,
|
||||
"quantity": 1,
|
||||
"totalLength": 76.2
|
||||
},
|
||||
{
|
||||
"length": 76.2,
|
||||
"quantity": 1,
|
||||
"totalLength": 76.2
|
||||
},
|
||||
{
|
||||
"length": 76.2,
|
||||
"quantity": 1,
|
||||
"totalLength": 76.2
|
||||
},
|
||||
{
|
||||
"length": 76.2,
|
||||
"quantity": 1,
|
||||
"totalLength": 76.2
|
||||
},
|
||||
{
|
||||
"length": 76.2,
|
||||
"quantity": 1,
|
||||
"totalLength": 76.2
|
||||
},
|
||||
{
|
||||
"length": 77.6,
|
||||
"quantity": 1,
|
||||
"totalLength": 77.6
|
||||
},
|
||||
{
|
||||
"length": 77.6,
|
||||
"quantity": 1,
|
||||
"totalLength": 77.6
|
||||
},
|
||||
{
|
||||
"length": 77.6,
|
||||
"quantity": 1,
|
||||
"totalLength": 77.6
|
||||
},
|
||||
{
|
||||
"length": 77.6,
|
||||
"quantity": 1,
|
||||
"totalLength": 77.6
|
||||
},
|
||||
{
|
||||
"length": 77.6,
|
||||
"quantity": 1,
|
||||
"totalLength": 77.6
|
||||
},
|
||||
{
|
||||
"length": 77.6,
|
||||
"quantity": 1,
|
||||
"totalLength": 77.6
|
||||
},
|
||||
{
|
||||
"length": 80,
|
||||
"quantity": 1,
|
||||
"totalLength": 80
|
||||
},
|
||||
{
|
||||
"length": 80,
|
||||
"quantity": 1,
|
||||
"totalLength": 80
|
||||
},
|
||||
{
|
||||
"length": 80,
|
||||
"quantity": 1,
|
||||
"totalLength": 80
|
||||
},
|
||||
{
|
||||
"length": 80,
|
||||
"quantity": 1,
|
||||
"totalLength": 80
|
||||
},
|
||||
{
|
||||
"length": 80,
|
||||
"quantity": 1,
|
||||
"totalLength": 80
|
||||
},
|
||||
{
|
||||
"length": 80,
|
||||
"quantity": 1,
|
||||
"totalLength": 80
|
||||
},
|
||||
{
|
||||
"length": 80,
|
||||
"quantity": 1,
|
||||
"totalLength": 80
|
||||
},
|
||||
{
|
||||
"length": 80,
|
||||
"quantity": 1,
|
||||
"totalLength": 80
|
||||
},
|
||||
{
|
||||
"length": 80,
|
||||
"quantity": 1,
|
||||
"totalLength": 80
|
||||
},
|
||||
{
|
||||
"length": 88.6,
|
||||
"quantity": 1,
|
||||
"totalLength": 88.6
|
||||
},
|
||||
{
|
||||
"length": 88.6,
|
||||
"quantity": 1,
|
||||
"totalLength": 88.6
|
||||
},
|
||||
{
|
||||
"length": 98.4,
|
||||
"quantity": 1,
|
||||
"totalLength": 98.4
|
||||
},
|
||||
{
|
||||
"length": 98.4,
|
||||
"quantity": 1,
|
||||
"totalLength": 98.4
|
||||
},
|
||||
{
|
||||
"length": 100,
|
||||
"quantity": 1,
|
||||
"totalLength": 100
|
||||
},
|
||||
{
|
||||
"length": 100,
|
||||
"quantity": 1,
|
||||
"totalLength": 100
|
||||
},
|
||||
{
|
||||
"length": 100,
|
||||
"quantity": 1,
|
||||
"totalLength": 100
|
||||
},
|
||||
{
|
||||
"length": 100,
|
||||
"quantity": 1,
|
||||
"totalLength": 100
|
||||
},
|
||||
{
|
||||
"length": 100,
|
||||
"quantity": 1,
|
||||
"totalLength": 100
|
||||
},
|
||||
{
|
||||
"length": 100,
|
||||
"quantity": 1,
|
||||
"totalLength": 100
|
||||
},
|
||||
{
|
||||
"length": 100,
|
||||
"quantity": 1,
|
||||
"totalLength": 100
|
||||
},
|
||||
{
|
||||
"length": 100,
|
||||
"quantity": 1,
|
||||
"totalLength": 100
|
||||
},
|
||||
{
|
||||
"length": 100,
|
||||
"quantity": 1,
|
||||
"totalLength": 100
|
||||
},
|
||||
{
|
||||
"length": 100,
|
||||
"quantity": 1,
|
||||
"totalLength": 100
|
||||
},
|
||||
{
|
||||
"length": 100,
|
||||
"quantity": 1,
|
||||
"totalLength": 100
|
||||
},
|
||||
{
|
||||
"length": 100,
|
||||
"quantity": 1,
|
||||
"totalLength": 100
|
||||
},
|
||||
{
|
||||
"length": 100,
|
||||
"quantity": 1,
|
||||
"totalLength": 100
|
||||
},
|
||||
{
|
||||
"length": 100,
|
||||
"quantity": 1,
|
||||
"totalLength": 100
|
||||
},
|
||||
{
|
||||
"length": 100,
|
||||
"quantity": 1,
|
||||
"totalLength": 100
|
||||
},
|
||||
{
|
||||
"length": 100,
|
||||
"quantity": 1,
|
||||
"totalLength": 100
|
||||
},
|
||||
{
|
||||
"length": 100,
|
||||
"quantity": 1,
|
||||
"totalLength": 100
|
||||
},
|
||||
{
|
||||
"length": 100,
|
||||
"quantity": 1,
|
||||
"totalLength": 100
|
||||
},
|
||||
{
|
||||
"length": 100,
|
||||
"quantity": 1,
|
||||
"totalLength": 100
|
||||
},
|
||||
{
|
||||
"length": 100,
|
||||
"quantity": 1,
|
||||
"totalLength": 100
|
||||
},
|
||||
{
|
||||
"length": 100,
|
||||
"quantity": 1,
|
||||
"totalLength": 100
|
||||
},
|
||||
{
|
||||
"length": 100,
|
||||
"quantity": 1,
|
||||
"totalLength": 100
|
||||
},
|
||||
{
|
||||
"length": 100,
|
||||
"quantity": 1,
|
||||
"totalLength": 100
|
||||
},
|
||||
{
|
||||
"length": 100,
|
||||
"quantity": 1,
|
||||
"totalLength": 100
|
||||
},
|
||||
{
|
||||
"length": 100,
|
||||
"quantity": 1,
|
||||
"totalLength": 100
|
||||
},
|
||||
{
|
||||
"length": 100,
|
||||
"quantity": 1,
|
||||
"totalLength": 100
|
||||
},
|
||||
{
|
||||
"length": 100,
|
||||
"quantity": 1,
|
||||
"totalLength": 100
|
||||
},
|
||||
{
|
||||
"length": 100,
|
||||
"quantity": 1,
|
||||
"totalLength": 100
|
||||
},
|
||||
{
|
||||
"length": 100,
|
||||
"quantity": 1,
|
||||
"totalLength": 100
|
||||
},
|
||||
{
|
||||
"length": 100,
|
||||
"quantity": 1,
|
||||
"totalLength": 100
|
||||
},
|
||||
{
|
||||
"length": 120,
|
||||
"quantity": 1,
|
||||
"totalLength": 120
|
||||
},
|
||||
{
|
||||
"length": 120,
|
||||
"quantity": 1,
|
||||
"totalLength": 120
|
||||
},
|
||||
{
|
||||
"length": 150,
|
||||
"quantity": 1,
|
||||
"totalLength": 150
|
||||
},
|
||||
{
|
||||
"length": 150,
|
||||
"quantity": 1,
|
||||
"totalLength": 150
|
||||
},
|
||||
{
|
||||
"length": 150,
|
||||
"quantity": 1,
|
||||
"totalLength": 150
|
||||
},
|
||||
{
|
||||
"length": 150,
|
||||
"quantity": 1,
|
||||
"totalLength": 150
|
||||
},
|
||||
{
|
||||
"length": 150,
|
||||
"quantity": 1,
|
||||
"totalLength": 150
|
||||
},
|
||||
{
|
||||
"length": 223.6,
|
||||
"quantity": 1,
|
||||
"totalLength": 223.6
|
||||
}
|
||||
],
|
||||
"user_requirement": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
{
|
||||
"request_no": "PR-20251014-009",
|
||||
"job_no": "TK-MP-TEST-001",
|
||||
"created_at": "2025-10-14T02:24:08.046686",
|
||||
"materials": [
|
||||
{
|
||||
"material_id": 88864,
|
||||
"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": 88869,
|
||||
"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": ""
|
||||
}
|
||||
],
|
||||
"grouped_materials": [
|
||||
{
|
||||
"group_key": "FLG WELD NECK RF SCH 40S, 150LB, ASTM A182 F304|10\"|undefined|ASTM A182 F304",
|
||||
"material_ids": [
|
||||
88864
|
||||
],
|
||||
"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": ""
|
||||
},
|
||||
{
|
||||
"group_key": "FLG WELD NECK RF SCH 40S, 150LB, ASTM A182 F304|12\"|undefined|ASTM A182 F304",
|
||||
"material_ids": [
|
||||
88869
|
||||
],
|
||||
"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": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
{
|
||||
"request_no": "PR-20251014-010",
|
||||
"job_no": "TK-MP-TEST-001",
|
||||
"created_at": "2025-10-14T02:24:14.814790",
|
||||
"materials": [
|
||||
{
|
||||
"material_id": 90052,
|
||||
"description": "ON/OFF VALVE, FLG, 600LB",
|
||||
"category": "VALVE",
|
||||
"size": "1\"",
|
||||
"material_grade": "-",
|
||||
"quantity": 1,
|
||||
"unit": "EA",
|
||||
"user_requirement": ""
|
||||
},
|
||||
{
|
||||
"material_id": 90053,
|
||||
"description": "CHECK VALVE, LIFT, SW, 800LB",
|
||||
"category": "VALVE",
|
||||
"size": "1 1/2\"",
|
||||
"material_grade": "-",
|
||||
"quantity": 2,
|
||||
"unit": "EA",
|
||||
"user_requirement": ""
|
||||
}
|
||||
],
|
||||
"grouped_materials": [
|
||||
{
|
||||
"group_key": "ON/OFF VALVE, FLG, 600LB|1\"|undefined|-",
|
||||
"material_ids": [
|
||||
90052
|
||||
],
|
||||
"description": "ON/OFF VALVE, FLG, 600LB",
|
||||
"category": "VALVE",
|
||||
"size": "1\"",
|
||||
"material_grade": "-",
|
||||
"quantity": 1,
|
||||
"unit": "EA",
|
||||
"user_requirement": ""
|
||||
},
|
||||
{
|
||||
"group_key": "CHECK VALVE, LIFT, SW, 800LB|1 1/2\"|undefined|-",
|
||||
"material_ids": [
|
||||
90053
|
||||
],
|
||||
"description": "CHECK VALVE, LIFT, SW, 800LB",
|
||||
"category": "VALVE",
|
||||
"size": "1 1/2\"",
|
||||
"material_grade": "-",
|
||||
"quantity": 2,
|
||||
"unit": "EA",
|
||||
"user_requirement": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
{
|
||||
"request_no": "PR-20251014-011",
|
||||
"job_no": "TK-MP-TEST-001",
|
||||
"created_at": "2025-10-14T02:24:21.733349",
|
||||
"materials": [
|
||||
{
|
||||
"material_id": 89216,
|
||||
"description": "(4) 0.5, 75 LG, 150LB, ASTM A193/A194 GR B7/2H, ELEC.GALV",
|
||||
"category": "BOLT",
|
||||
"size": "1\"",
|
||||
"material_grade": "ASTM A193/A194 GR B7/2H",
|
||||
"quantity": 51,
|
||||
"unit": "EA",
|
||||
"user_requirement": ""
|
||||
}
|
||||
],
|
||||
"grouped_materials": [
|
||||
{
|
||||
"group_key": "(4) 0.5, 75 LG, 150LB, ASTM A193/A194 GR B7/2H, ELEC.GALV|1\"|undefined|ASTM A193/A194 GR B7/2H",
|
||||
"material_ids": [
|
||||
89216
|
||||
],
|
||||
"description": "(4) 0.5, 75 LG, 150LB, ASTM A193/A194 GR B7/2H, ELEC.GALV",
|
||||
"category": "BOLT",
|
||||
"size": "1\"",
|
||||
"material_grade": "ASTM A193/A194 GR B7/2H",
|
||||
"quantity": 51,
|
||||
"unit": "EA",
|
||||
"user_requirement": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
{
|
||||
"request_no": "PR-20251014-012",
|
||||
"job_no": "TK-MP-TEST-001",
|
||||
"created_at": "2025-10-14T02:42:08.351432",
|
||||
"materials": [
|
||||
{
|
||||
"material_id": 89220,
|
||||
"description": "(4) 0.5, 80 LG, 150LB, ASTM A193/A194 GR B7/2H, ELEC.GALV",
|
||||
"category": "BOLT",
|
||||
"size": "1 1/2\"",
|
||||
"material_grade": "ASTM A193/A194 GR B7/2H",
|
||||
"quantity": 32,
|
||||
"unit": "EA",
|
||||
"user_requirement": ""
|
||||
}
|
||||
],
|
||||
"grouped_materials": [
|
||||
{
|
||||
"group_key": "(4) 0.5, 80 LG, 150LB, ASTM A193/A194 GR B7/2H, ELEC.GALV|1 1/2\"|undefined|ASTM A193/A194 GR B7/2H",
|
||||
"material_ids": [
|
||||
89220
|
||||
],
|
||||
"description": "(4) 0.5, 80 LG, 150LB, ASTM A193/A194 GR B7/2H, ELEC.GALV",
|
||||
"category": "BOLT",
|
||||
"size": "1 1/2\"",
|
||||
"material_grade": "ASTM A193/A194 GR B7/2H",
|
||||
"quantity": 32,
|
||||
"unit": "EA",
|
||||
"user_requirement": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
{
|
||||
"request_no": "PR-20251014-013",
|
||||
"job_no": "TK-MP-TEST-001",
|
||||
"created_at": "2025-10-14T02:47:17.256790",
|
||||
"materials": [
|
||||
{
|
||||
"material_id": 89465,
|
||||
"description": "SWG, 150LB, H/F/I/O SS304/GRAPHITE/SS304/SS304, 4.5mm",
|
||||
"category": "GASKET",
|
||||
"size": "1/2\"",
|
||||
"material_grade": "SS304",
|
||||
"quantity": 44,
|
||||
"unit": "EA",
|
||||
"user_requirement": ""
|
||||
}
|
||||
],
|
||||
"grouped_materials": [
|
||||
{
|
||||
"group_key": "SWG, 150LB, H/F/I/O SS304/GRAPHITE/SS304/SS304, 4.5mm|1/2\"|undefined|SS304",
|
||||
"material_ids": [
|
||||
89465
|
||||
],
|
||||
"description": "SWG, 150LB, H/F/I/O SS304/GRAPHITE/SS304/SS304, 4.5mm",
|
||||
"category": "GASKET",
|
||||
"size": "1/2\"",
|
||||
"material_grade": "SS304",
|
||||
"quantity": 44,
|
||||
"unit": "EA",
|
||||
"user_requirement": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"request_no": "PR-20251014-015",
|
||||
"job_no": "TK-MP-TEST-001",
|
||||
"created_at": "2025-10-14T02:54:15.899037",
|
||||
"materials": [],
|
||||
"grouped_materials": []
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
{
|
||||
"request_no": "PR-20251014-016",
|
||||
"job_no": "TK-MP-TEST-001",
|
||||
"created_at": "2025-10-14T02:54:33.149908",
|
||||
"materials": [
|
||||
{
|
||||
"material_id": 88142,
|
||||
"description": "HALF NIPPLE, SMLS, SCH 80S, ASTM A312 TP304 SW X NPT",
|
||||
"category": "FITTING",
|
||||
"size": "1/2\"",
|
||||
"material_grade": "ASTM A312 TP304",
|
||||
"quantity": "3.000",
|
||||
"unit": "EA",
|
||||
"user_requirement": ""
|
||||
},
|
||||
{
|
||||
"material_id": 88144,
|
||||
"description": "HALF NIPPLE, SMLS, SCH 80S, ASTM A312 TP304 SW X NPT",
|
||||
"category": "FITTING",
|
||||
"size": "1/2\"",
|
||||
"material_grade": "ASTM A312 TP304",
|
||||
"quantity": "3.000",
|
||||
"unit": "EA",
|
||||
"user_requirement": ""
|
||||
}
|
||||
],
|
||||
"grouped_materials": [
|
||||
{
|
||||
"group_key": "HALF NIPPLE, SMLS, SCH 80S, ASTM A312 TP304 SW X NPT|1/2\"|undefined|ASTM A312 TP304",
|
||||
"material_ids": [
|
||||
88142,
|
||||
88144
|
||||
],
|
||||
"description": "HALF NIPPLE, SMLS, SCH 80S, ASTM A312 TP304 SW X NPT",
|
||||
"category": "FITTING",
|
||||
"size": "1/2\"",
|
||||
"material_grade": "ASTM A312 TP304",
|
||||
"quantity": "3.000",
|
||||
"unit": "EA",
|
||||
"user_requirement": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,132 +0,0 @@
|
||||
{
|
||||
"request_no": "PR-20251014-017",
|
||||
"job_no": "TK-MP-TEST-001",
|
||||
"created_at": "2025-10-14T02:54:45.118843",
|
||||
"materials": [
|
||||
{
|
||||
"material_id": 91515,
|
||||
"description": "CLAMP CL-1",
|
||||
"category": "SUPPORT",
|
||||
"size": "1\"",
|
||||
"material_grade": "-",
|
||||
"quantity": "8.000",
|
||||
"unit": "EA",
|
||||
"user_requirement": ""
|
||||
},
|
||||
{
|
||||
"material_id": 91516,
|
||||
"description": "CLAMP CL-1",
|
||||
"category": "SUPPORT",
|
||||
"size": "1\"",
|
||||
"material_grade": "-",
|
||||
"quantity": "8.000",
|
||||
"unit": "EA",
|
||||
"user_requirement": ""
|
||||
},
|
||||
{
|
||||
"material_id": 91517,
|
||||
"description": "CLAMP CL-1",
|
||||
"category": "SUPPORT",
|
||||
"size": "1\"",
|
||||
"material_grade": "-",
|
||||
"quantity": "8.000",
|
||||
"unit": "EA",
|
||||
"user_requirement": ""
|
||||
},
|
||||
{
|
||||
"material_id": 91518,
|
||||
"description": "CLAMP CL-1",
|
||||
"category": "SUPPORT",
|
||||
"size": "1\"",
|
||||
"material_grade": "-",
|
||||
"quantity": "8.000",
|
||||
"unit": "EA",
|
||||
"user_requirement": ""
|
||||
},
|
||||
{
|
||||
"material_id": 91552,
|
||||
"description": "CLAMP CL-1",
|
||||
"category": "SUPPORT",
|
||||
"size": "1\"",
|
||||
"material_grade": "-",
|
||||
"quantity": "8.000",
|
||||
"unit": "EA",
|
||||
"user_requirement": ""
|
||||
},
|
||||
{
|
||||
"material_id": 91553,
|
||||
"description": "CLAMP CL-1",
|
||||
"category": "SUPPORT",
|
||||
"size": "1\"",
|
||||
"material_grade": "-",
|
||||
"quantity": "8.000",
|
||||
"unit": "EA",
|
||||
"user_requirement": ""
|
||||
},
|
||||
{
|
||||
"material_id": 91554,
|
||||
"description": "CLAMP CL-1",
|
||||
"category": "SUPPORT",
|
||||
"size": "1\"",
|
||||
"material_grade": "-",
|
||||
"quantity": "8.000",
|
||||
"unit": "EA",
|
||||
"user_requirement": ""
|
||||
},
|
||||
{
|
||||
"material_id": 91555,
|
||||
"description": "CLAMP CL-1",
|
||||
"category": "SUPPORT",
|
||||
"size": "1\"",
|
||||
"material_grade": "-",
|
||||
"quantity": "8.000",
|
||||
"unit": "EA",
|
||||
"user_requirement": ""
|
||||
},
|
||||
{
|
||||
"material_id": 89471,
|
||||
"description": "SWG, 150LB, H/F/I/O SS304/GRAPHITE/SS304/SS304, 4.5mm",
|
||||
"category": "GASKET",
|
||||
"size": "3/4\"",
|
||||
"material_grade": "SS304",
|
||||
"quantity": 18,
|
||||
"unit": "EA",
|
||||
"user_requirement": ""
|
||||
}
|
||||
],
|
||||
"grouped_materials": [
|
||||
{
|
||||
"group_key": "CLAMP CL-1|1\"|undefined|-",
|
||||
"material_ids": [
|
||||
91515,
|
||||
91516,
|
||||
91517,
|
||||
91518,
|
||||
91552,
|
||||
91553,
|
||||
91554,
|
||||
91555
|
||||
],
|
||||
"description": "CLAMP CL-1",
|
||||
"category": "SUPPORT",
|
||||
"size": "1\"",
|
||||
"material_grade": "-",
|
||||
"quantity": "8.000",
|
||||
"unit": "EA",
|
||||
"user_requirement": ""
|
||||
},
|
||||
{
|
||||
"group_key": "SWG, 150LB, H/F/I/O SS304/GRAPHITE/SS304/SS304, 4.5mm|3/4\"|undefined|SS304",
|
||||
"material_ids": [
|
||||
89471
|
||||
],
|
||||
"description": "SWG, 150LB, H/F/I/O SS304/GRAPHITE/SS304/SS304, 4.5mm",
|
||||
"category": "GASKET",
|
||||
"size": "3/4\"",
|
||||
"material_grade": "SS304",
|
||||
"quantity": 18,
|
||||
"unit": "EA",
|
||||
"user_requirement": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
{
|
||||
"request_no": "PR-20251014-018",
|
||||
"job_no": "TK-MP-TEST-001",
|
||||
"created_at": "2025-10-14T02:54:50.900910",
|
||||
"materials": [
|
||||
{
|
||||
"material_id": 91515,
|
||||
"description": "CLAMP CL-1",
|
||||
"category": "SUPPORT",
|
||||
"size": "1\"",
|
||||
"material_grade": "-",
|
||||
"quantity": "8.000",
|
||||
"unit": "EA",
|
||||
"user_requirement": ""
|
||||
},
|
||||
{
|
||||
"material_id": 91516,
|
||||
"description": "CLAMP CL-1",
|
||||
"category": "SUPPORT",
|
||||
"size": "1\"",
|
||||
"material_grade": "-",
|
||||
"quantity": "8.000",
|
||||
"unit": "EA",
|
||||
"user_requirement": ""
|
||||
},
|
||||
{
|
||||
"material_id": 91517,
|
||||
"description": "CLAMP CL-1",
|
||||
"category": "SUPPORT",
|
||||
"size": "1\"",
|
||||
"material_grade": "-",
|
||||
"quantity": "8.000",
|
||||
"unit": "EA",
|
||||
"user_requirement": ""
|
||||
},
|
||||
{
|
||||
"material_id": 91518,
|
||||
"description": "CLAMP CL-1",
|
||||
"category": "SUPPORT",
|
||||
"size": "1\"",
|
||||
"material_grade": "-",
|
||||
"quantity": "8.000",
|
||||
"unit": "EA",
|
||||
"user_requirement": ""
|
||||
},
|
||||
{
|
||||
"material_id": 91552,
|
||||
"description": "CLAMP CL-1",
|
||||
"category": "SUPPORT",
|
||||
"size": "1\"",
|
||||
"material_grade": "-",
|
||||
"quantity": "8.000",
|
||||
"unit": "EA",
|
||||
"user_requirement": ""
|
||||
},
|
||||
{
|
||||
"material_id": 91553,
|
||||
"description": "CLAMP CL-1",
|
||||
"category": "SUPPORT",
|
||||
"size": "1\"",
|
||||
"material_grade": "-",
|
||||
"quantity": "8.000",
|
||||
"unit": "EA",
|
||||
"user_requirement": ""
|
||||
},
|
||||
{
|
||||
"material_id": 91554,
|
||||
"description": "CLAMP CL-1",
|
||||
"category": "SUPPORT",
|
||||
"size": "1\"",
|
||||
"material_grade": "-",
|
||||
"quantity": "8.000",
|
||||
"unit": "EA",
|
||||
"user_requirement": ""
|
||||
},
|
||||
{
|
||||
"material_id": 91555,
|
||||
"description": "CLAMP CL-1",
|
||||
"category": "SUPPORT",
|
||||
"size": "1\"",
|
||||
"material_grade": "-",
|
||||
"quantity": "8.000",
|
||||
"unit": "EA",
|
||||
"user_requirement": ""
|
||||
}
|
||||
],
|
||||
"grouped_materials": [
|
||||
{
|
||||
"group_key": "CLAMP CL-1|1\"|undefined|-",
|
||||
"material_ids": [
|
||||
91515,
|
||||
91516,
|
||||
91517,
|
||||
91518,
|
||||
91552,
|
||||
91553,
|
||||
91554,
|
||||
91555
|
||||
],
|
||||
"description": "CLAMP CL-1",
|
||||
"category": "SUPPORT",
|
||||
"size": "1\"",
|
||||
"material_grade": "-",
|
||||
"quantity": "8.000",
|
||||
"unit": "EA",
|
||||
"user_requirement": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
20
backend/scripts/29_add_revision_status.sql
Normal file
20
backend/scripts/29_add_revision_status.sql
Normal file
@@ -0,0 +1,20 @@
|
||||
-- 리비전 관리 개선: 자재 상태 추적
|
||||
-- 리비전 업로드 시 삭제된 자재의 상태를 추적
|
||||
|
||||
-- materials 테이블에 revision_status 컬럼 추가
|
||||
ALTER TABLE materials ADD COLUMN IF NOT EXISTS revision_status VARCHAR(20) DEFAULT 'active';
|
||||
-- 가능한 값: 'active', 'inventory', 'deleted_not_purchased', 'changed'
|
||||
|
||||
-- revision_status 설명:
|
||||
-- 'active': 정상 활성 자재 (기본값)
|
||||
-- 'inventory': 재고품 (구매신청 후 리비전에서 삭제됨 - 연노랑색 표시)
|
||||
-- 'deleted_not_purchased': 구매신청 전 삭제됨 (숨김 처리)
|
||||
-- 'changed': 변경된 자재 (추가 구매 필요)
|
||||
|
||||
-- 인덱스 추가 (성능 최적화)
|
||||
CREATE INDEX IF NOT EXISTS idx_materials_revision_status ON materials(revision_status);
|
||||
CREATE INDEX IF NOT EXISTS idx_materials_drawing_name ON materials(drawing_name);
|
||||
CREATE INDEX IF NOT EXISTS idx_materials_line_no ON materials(line_no);
|
||||
|
||||
COMMENT ON COLUMN materials.revision_status IS '리비전 자재 상태: active(활성), inventory(재고품), deleted_not_purchased(삭제됨), changed(변경됨)';
|
||||
|
||||
@@ -299,6 +299,72 @@ const BOMWorkspacePage = ({ project, onNavigate, onBack }) => {
|
||||
});
|
||||
|
||||
if (response.data?.success) {
|
||||
// 누락된 도면 확인
|
||||
if (response.data.missing_drawings && response.data.missing_drawings.requires_confirmation) {
|
||||
const missingDrawings = response.data.missing_drawings.drawings || [];
|
||||
const materialCount = response.data.missing_drawings.materials?.length || 0;
|
||||
const hasPreviousPurchase = response.data.missing_drawings.has_previous_purchase;
|
||||
const fileId = response.data.file_id;
|
||||
|
||||
// 사용자 선택을 위한 프롬프트 메시지
|
||||
let alertMessage = `⚠️ 리비전 업로드 확인\n\n` +
|
||||
`다음 도면이 새 파일에 없습니다:\n` +
|
||||
`${missingDrawings.slice(0, 5).join('\n')}` +
|
||||
`${missingDrawings.length > 5 ? `\n...외 ${missingDrawings.length - 5}개` : ''}\n\n` +
|
||||
`관련 자재: ${materialCount}개\n\n`;
|
||||
|
||||
if (hasPreviousPurchase) {
|
||||
// 케이스 1: 이미 구매신청된 경우
|
||||
alertMessage += `✅ 이전 리비전에서 구매신청이 진행되었습니다.\n\n` +
|
||||
`다음 중 선택하세요:\n\n` +
|
||||
`1️⃣ "일부만 업로드" - 변경된 도면만 업로드, 기존 도면 유지\n` +
|
||||
` → 누락된 도면의 자재는 "재고품"으로 표시 (연노랑색)\n\n` +
|
||||
`2️⃣ "도면 삭제됨" - 누락된 도면 완전 삭제\n` +
|
||||
` → 해당 자재 제거 및 재고품 처리\n\n` +
|
||||
`3️⃣ "취소" - 업로드 취소\n\n` +
|
||||
`숫자를 입력하세요 (1, 2, 3):`;
|
||||
} else {
|
||||
// 케이스 2: 구매신청 전인 경우
|
||||
alertMessage += `⚠️ 아직 구매신청이 진행되지 않았습니다.\n\n` +
|
||||
`다음 중 선택하세요:\n\n` +
|
||||
`1️⃣ "일부만 업로드" - 변경된 도면만 업로드, 기존 도면 유지\n` +
|
||||
` → 누락된 도면의 자재는 그대로 유지\n\n` +
|
||||
`2️⃣ "도면 삭제됨" - 누락된 도면 완전 삭제\n` +
|
||||
` → 해당 자재 완전 제거 (숨김 처리)\n\n` +
|
||||
`3️⃣ "취소" - 업로드 취소\n\n` +
|
||||
`숫자를 입력하세요 (1, 2, 3):`;
|
||||
}
|
||||
|
||||
const userChoice = prompt(alertMessage);
|
||||
|
||||
if (userChoice === '3' || userChoice === null) {
|
||||
// 취소 선택
|
||||
await api.delete(`/files/${fileId}`);
|
||||
alert('업로드가 취소되었습니다.');
|
||||
return;
|
||||
} else if (userChoice === '2') {
|
||||
// 도면 삭제됨 - 백엔드에 삭제 처리 요청
|
||||
try {
|
||||
await api.post(`/files/${fileId}/process-missing-drawings`, {
|
||||
action: 'delete',
|
||||
drawings: missingDrawings
|
||||
});
|
||||
alert(`✅ ${missingDrawings.length}개 도면이 삭제 처리되었습니다.`);
|
||||
} catch (err) {
|
||||
console.error('도면 삭제 처리 실패:', err);
|
||||
alert('도면 삭제 처리에 실패했습니다.');
|
||||
}
|
||||
} else if (userChoice === '1') {
|
||||
// 일부만 업로드 - 이미 처리됨 (기본 동작)
|
||||
alert(`✅ 일부 업로드로 처리되었습니다.\n누락된 ${missingDrawings.length}개 도면은 기존 상태를 유지합니다.`);
|
||||
} else {
|
||||
// 잘못된 입력
|
||||
await api.delete(`/files/${fileId}`);
|
||||
alert('잘못된 입력입니다. 업로드가 취소되었습니다.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
alert(`리비전 ${response.data.revision} 업로드 성공!\n신규 자재: ${response.data.new_materials_count || 0}개`);
|
||||
await loadFiles();
|
||||
}
|
||||
|
||||
@@ -1168,4 +1168,24 @@
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #9ca3af;
|
||||
}
|
||||
|
||||
/* ================================
|
||||
리비전 상태별 스타일
|
||||
================================ */
|
||||
|
||||
/* 재고품 스타일 (구매신청 후 리비전에서 삭제됨) */
|
||||
.detailed-material-row.inventory {
|
||||
background-color: #fef3c7 !important; /* 연노랑색 배경 */
|
||||
border-left: 4px solid #f59e0b !important; /* 주황색 왼쪽 테두리 */
|
||||
}
|
||||
|
||||
/* 삭제된 자재 (구매신청 전) - 숨김 처리 */
|
||||
.detailed-material-row.deleted-not-purchased {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* 변경된 자재 (추가 구매 필요) */
|
||||
.detailed-material-row.changed {
|
||||
border-left: 4px solid #3b82f6 !important; /* 파란색 왼쪽 테두리 */
|
||||
}
|
||||
@@ -1145,6 +1145,71 @@ const NewMaterialsPage = ({
|
||||
setSelectedMaterials(newSelection);
|
||||
};
|
||||
|
||||
// 자재 상태별 CSS 클래스 생성
|
||||
const getMaterialRowClasses = (material, baseClass) => {
|
||||
const classes = [baseClass];
|
||||
|
||||
if (selectedMaterials.has(material.id)) {
|
||||
classes.push('selected');
|
||||
}
|
||||
|
||||
// 구매신청 여부 확인
|
||||
const isPurchased = material.grouped_ids ?
|
||||
material.grouped_ids.some(id => purchasedMaterials.has(id)) :
|
||||
purchasedMaterials.has(material.id);
|
||||
|
||||
if (isPurchased) {
|
||||
classes.push('purchased');
|
||||
}
|
||||
|
||||
// 리비전 상태
|
||||
if (material.revision_status === 'inventory') {
|
||||
classes.push('inventory');
|
||||
} else if (material.revision_status === 'deleted_not_purchased') {
|
||||
classes.push('deleted-not-purchased');
|
||||
} else if (material.revision_status === 'changed') {
|
||||
classes.push('changed');
|
||||
}
|
||||
|
||||
return classes.join(' ');
|
||||
};
|
||||
|
||||
// 리비전 상태 배지 렌더링
|
||||
const renderRevisionBadge = (material) => {
|
||||
if (material.revision_status === 'inventory') {
|
||||
return (
|
||||
<span style={{
|
||||
display: 'inline-block',
|
||||
padding: '2px 6px',
|
||||
backgroundColor: '#f59e0b',
|
||||
color: 'white',
|
||||
fontSize: '10px',
|
||||
borderRadius: '3px',
|
||||
marginBottom: '4px',
|
||||
marginLeft: '4px'
|
||||
}}>
|
||||
재고품
|
||||
</span>
|
||||
);
|
||||
} else if (material.revision_status === 'changed') {
|
||||
return (
|
||||
<span style={{
|
||||
display: 'inline-block',
|
||||
padding: '2px 6px',
|
||||
backgroundColor: '#3b82f6',
|
||||
color: 'white',
|
||||
fontSize: '10px',
|
||||
borderRadius: '3px',
|
||||
marginBottom: '4px',
|
||||
marginLeft: '4px'
|
||||
}}>
|
||||
변경됨
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// 필터 헤더 컴포넌트
|
||||
const FilterableHeader = ({ children, sortKey, filterKey, className = "" }) => {
|
||||
const uniqueValues = React.useMemo(() => {
|
||||
@@ -1856,7 +1921,7 @@ const NewMaterialsPage = ({
|
||||
return (
|
||||
<div
|
||||
key={material.id}
|
||||
className={`detailed-material-row pipe-row ${selectedMaterials.has(material.id) ? 'selected' : ''} ${isPurchased ? 'purchased' : ''}`}
|
||||
className={getMaterialRowClasses(material, 'detailed-material-row pipe-row')}
|
||||
style={isPurchased ? { opacity: 0.6, backgroundColor: '#f0f0f0' } : {}}
|
||||
>
|
||||
{/* 선택 */}
|
||||
|
||||
Reference in New Issue
Block a user