"""출고 배치 → 취합 엑셀(openpyxl) 생성. 다운로드 zip 에 업로드 원본과 함께 넣는 '출고 엑셀'을 만든다. 1박스 안에 여러 SKU 가 있으면 SKU 당 1행으로 펼치고 박스 단위(받는 사람/주문/택배) 정보는 반복한다. 파일명 형식: 2026.06.22(Mon)_tictoc.xlsx (Shopee 는 끝이 _shopee). - TikTok 은 운영 요청 철자에 맞춰 'tictoc' 을 쓴다. """ from __future__ import annotations import io import re from datetime import date, datetime from typing import Any # 엑셀 컬럼(요청 순서). (헤더, 박스/배치/상품 키) _COLUMNS: tuple[tuple[str, str], ...] = ( ("발송 날짜", "dispatch_date"), ("고객 이름", "recipient_name"), ("전화번호", "recipient_phone"), ("주소", "recipient_address"), ("상품이름", "product_name"), ("아이템 코드", "seller_sku"), ("주문 수량", "quantity"), ("오더번호", "order_id"), ("택배사", "shipping_provider"), ("택배송장번호", "tracking_id"), ("주문처", "platform"), ) # 플랫폼 → 파일명 접미사. _PLATFORM_SUFFIX: dict[str, str] = { "TikTok": "tictoc", "Shopee": "shopee", } def export_filename(*, dispatch_date: str, platform: str) -> str: """2026.06.22(Mon)_tictoc.xlsx 형식 파일명. 날짜 파싱 실패 시 원문 사용.""" d = _parse_date(dispatch_date) if d is not None: stamp = d.strftime("%Y.%m.%d(%a)") else: stamp = (dispatch_date or "").strip() or "dispatch" suffix = _PLATFORM_SUFFIX.get((platform or "").strip(), (platform or "dispatch").strip().lower()) return f"{stamp}_{suffix}.xlsx" def resolve_product_name(seller_sku: str, name_map: dict[str, str], fallback: str = "") -> str: """아이템코드(seller_sku)로 itemcode_db 상품명을 찾는다. seller_sku 는 변형 접미사가 붙을 수 있다(예: MT-0320_3 → DB 는 MT-0320). 정확 일치 → 접미사(_숫자) 제거 후 일치 → 없으면 fallback(파싱된 상품명). """ key = (seller_sku or "").strip().upper() if not key: return fallback if key in name_map: return name_map[key] base = re.sub(r"_\d+$", "", key) if base != key and base in name_map: return name_map[base] return fallback def build_workbook_bytes( *, batch: dict[str, Any], parcels: list[dict[str, Any]], name_map: dict[str, str] | None = None, ) -> bytes: """배치 + 박스(상품 포함) → xlsx 바이트. parcels 각 항목은 db.list_parcels 결과(items 포함, recipient_* 포함). name_map: itemcode_db 의 {코드(대문자): 상품명}. 있으면 아이템코드로 상품명을 조회해 채운다(없으면 파싱된 상품명 사용). """ name_map = name_map or {} from openpyxl import Workbook # noqa: WPS433 — 지연 import from openpyxl.styles import Font wb = Workbook() ws = wb.active ws.title = "출고" headers = [h for h, _ in _COLUMNS] ws.append(headers) for cell in ws[1]: cell.font = Font(bold=True) dispatch_date = batch.get("dispatch_date", "") or "" platform = batch.get("platform", "") or "" for p in parcels: box = { "dispatch_date": dispatch_date, "platform": platform, "recipient_name": p.get("recipient_name", "") or "", "recipient_phone": p.get("recipient_phone", "") or "", "recipient_address": p.get("recipient_address", "") or "", "order_id": p.get("order_id", "") or "", "shipping_provider": p.get("shipping_provider", "") or "", "tracking_id": p.get("tracking_id", "") or "", } items = p.get("items") or [{}] for it in items: row_data = dict(box) sku = it.get("seller_sku", "") or "" row_data["product_name"] = resolve_product_name( sku, name_map, fallback=it.get("product_name", "") or "" ) row_data["seller_sku"] = sku row_data["quantity"] = it.get("quantity", "") if it.get("quantity") is not None else "" ws.append([_cell(row_data.get(key, "")) for _, key in _COLUMNS]) _autosize(ws, headers) buf = io.BytesIO() wb.save(buf) return buf.getvalue() def _cell(value: Any) -> Any: """송장/전화번호 등이 숫자로 보이면 엑셀이 과학표기/반올림 하므로 문자열 보존.""" if isinstance(value, int): return value return "" if value is None else str(value) def _autosize(ws: Any, headers: list[str]) -> None: """대략적인 컬럼 폭. 주소는 길어 상한을 둔다.""" from openpyxl.utils import get_column_letter widths: list[int] = [len(h) for h in headers] for row in ws.iter_rows(min_row=2, values_only=True): for i, val in enumerate(row): ln = len(str(val)) if val is not None else 0 if ln > widths[i]: widths[i] = ln for i, w in enumerate(widths, start=1): ws.column_dimensions[get_column_letter(i)].width = min(max(w + 2, 8), 50) def _parse_date(value: str) -> date | None: s = (value or "").strip() if not s: return None for fmt in ("%Y-%m-%d", "%Y.%m.%d", "%Y/%m/%d"): try: return datetime.strptime(s, fmt).date() except ValueError: continue return None