Files
dbx-main/app/modules/dispatch/parser.py
T
king 99eea01fc1 feat(dispatch): 말레이시아 TikTok 출고관리 모듈 추가
03_TikTok_Order_Export.xlsx 업로드 → 1박스=1카드 출고 작업 리스트,
SKU 피킹 요약, Kagayaku 전달표(A4 인쇄)를 자동 생성.

- 1박스 묶음 기준: Package ID > Tracking ID > Order ID, 같은 박스 같은 SKU 합산
- 작업 상태 5단계 토글(AJAX 즉시 저장) + dispatch_logs 기록
- 고객 이름/주소/전화 미저장(파싱 시 폐기, 스키마에 컬럼 없음)
- 엑셀은 openpyxl 파싱(pandas 미사용), DB는 dispatch_db 전용
- 인증은 기존 Google OAuth + 신규 권한키 dispatch 재사용
- scripts/sql/dispatch_db_init.sql, docs/DISPATCH_MODULE.md 동봉

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 15:11:43 +09:00

71 lines
2.7 KiB
Python

"""TikTok Order Export XLSX 파서 (openpyxl).
요구사항 2/11 구현. pandas 대신 레포에 이미 있는 openpyxl 로 읽어 의존성을
가볍게 유지한다(동작/결과는 동일: 헤더 정규화 → 표준키 매핑 → 박스 묶기).
- 첫 시트의 첫 행을 헤더로 본다. 헤더 앞뒤 공백 제거 후 표준키로 매핑한다.
- 매핑되지 않는 열(고객 이름/주소/전화 등)은 읽기 단계에서 버린다 → 개인정보
미보관(요구사항 11).
- Tracking ID 는 숫자로 읽혀도 문자열로 보존(store.clean_text).
- Quantity 가 비면 1, NaN/None 은 빈 문자열로 처리.
- 필수 컬럼이 없으면 어떤 컬럼이 없는지 ParseError 로 알린다.
"""
from __future__ import annotations
from typing import Any
from . import store
class ParseError(Exception):
"""엑셀 파싱 실패. message 는 사용자에게 그대로 보여줄 한국어 메시지."""
def parse_order_export(path: str) -> dict[str, Any]:
"""XLSX 경로 → {"parcels": [...], "row_count": int}.
parcels 는 store.group_parcels 결과(1박스=1항목). 실패 시 ParseError.
"""
try:
from openpyxl import load_workbook # noqa: WPS433 — 지연 import
except ImportError as exc: # pragma: no cover
raise ParseError("openpyxl 이 설치되어 있지 않습니다.") from exc
try:
wb = load_workbook(path, read_only=True, data_only=True)
except Exception as exc: # noqa: BLE001
raise ParseError(f"엑셀 파일을 열 수 없습니다: {type(exc).__name__}") from exc
try:
ws = wb.worksheets[0]
rows_iter = ws.iter_rows(values_only=True)
try:
header = next(rows_iter)
except StopIteration:
raise ParseError("엑셀에 데이터가 없습니다(빈 파일).")
mapping = store.resolve_columns(header)
missing = store.missing_required(mapping)
if missing:
raise ParseError("엑셀에서 필요한 컬럼을 찾지 못했습니다: " + ", ".join(missing))
norm_rows: list[dict[str, str]] = []
for raw in rows_iter:
if raw is None:
continue
# 표준키만 추출(매핑 안 된 열 = 개인정보 등은 버린다).
record = {
key: store.clean_text(raw[idx]) if idx < len(raw) else ""
for key, idx in mapping.items()
}
# 완전 빈 행 스킵
if not any(record.get(k) for k in ("order_id", "package_id", "tracking_id", "seller_sku")):
continue
norm_rows.append(record)
finally:
wb.close()
parcels = store.group_parcels(norm_rows)
return {"parcels": parcels, "row_count": len(norm_rows)}