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,
|
||||
|
||||
Reference in New Issue
Block a user