cee0c65a01
- spx.svg 추가(택배사 SPX 로고). courier 로고 슬러그 spx 가 사용 - 출고 엑셀 상품이름을 아이템코드(seller_sku)로 itemcode_db 에서 조회해 채움 · malaysia_itemcode.name_map(single_items+set_items) 재사용 · 변형 접미사(_숫자) 제거 후 재조회, 못 찾으면 파싱된 상품명 폴백 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
155 lines
5.3 KiB
Python
155 lines
5.3 KiB
Python
"""출고 배치 → 취합 엑셀(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
|