feat(dispatch): SPX 로고(svg) + 출고 엑셀 상품명 DB 조회
- 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>
This commit is contained in:
@@ -10,6 +10,7 @@ SKU 가 있으면 SKU 당 1행으로 펼치고 박스 단위(받는 사람/주
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import io
|
import io
|
||||||
|
import re
|
||||||
from datetime import date, datetime
|
from datetime import date, datetime
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
@@ -46,11 +47,36 @@ def export_filename(*, dispatch_date: str, platform: str) -> str:
|
|||||||
return f"{stamp}_{suffix}.xlsx"
|
return f"{stamp}_{suffix}.xlsx"
|
||||||
|
|
||||||
|
|
||||||
def build_workbook_bytes(*, batch: dict[str, Any], parcels: list[dict[str, Any]]) -> bytes:
|
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 바이트.
|
"""배치 + 박스(상품 포함) → xlsx 바이트.
|
||||||
|
|
||||||
parcels 각 항목은 db.list_parcels 결과(items 포함, recipient_* 포함).
|
parcels 각 항목은 db.list_parcels 결과(items 포함, recipient_* 포함).
|
||||||
|
name_map: itemcode_db 의 {코드(대문자): 상품명}. 있으면 아이템코드로 상품명을
|
||||||
|
조회해 채운다(없으면 파싱된 상품명 사용).
|
||||||
"""
|
"""
|
||||||
|
name_map = name_map or {}
|
||||||
from openpyxl import Workbook # noqa: WPS433 — 지연 import
|
from openpyxl import Workbook # noqa: WPS433 — 지연 import
|
||||||
from openpyxl.styles import Font
|
from openpyxl.styles import Font
|
||||||
|
|
||||||
@@ -80,8 +106,11 @@ def build_workbook_bytes(*, batch: dict[str, Any], parcels: list[dict[str, Any]]
|
|||||||
items = p.get("items") or [{}]
|
items = p.get("items") or [{}]
|
||||||
for it in items:
|
for it in items:
|
||||||
row_data = dict(box)
|
row_data = dict(box)
|
||||||
row_data["product_name"] = it.get("product_name", "") or ""
|
sku = it.get("seller_sku", "") or ""
|
||||||
row_data["seller_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 ""
|
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])
|
ws.append([_cell(row_data.get(key, "")) for _, key in _COLUMNS])
|
||||||
|
|
||||||
|
|||||||
@@ -61,6 +61,21 @@ def _data_dir(request: Request) -> Path:
|
|||||||
return Path(getattr(request.app.state, "data_dir", Path("app/data")))
|
return Path(getattr(request.app.state, "data_dir", Path("app/data")))
|
||||||
|
|
||||||
|
|
||||||
|
def _itemcode_name_map(request: Request) -> dict[str, str]:
|
||||||
|
"""itemcode_db {코드(대문자): 상품명}. 미설정/실패 시 빈 dict.
|
||||||
|
|
||||||
|
malaysia 모듈의 읽기 전용 리더(single_items + set_items)를 재사용한다.
|
||||||
|
"""
|
||||||
|
reader = getattr(request.app.state, "malaysia_itemcode", None)
|
||||||
|
if reader is None or not getattr(reader, "enabled", False):
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
return reader.name_map()
|
||||||
|
except Exception: # noqa: BLE001 — 다운로드를 막지 않는다.
|
||||||
|
logger.exception("itemcode name_map 조회 실패")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
def _require_user(request: Request) -> dict[str, Any]:
|
def _require_user(request: Request) -> dict[str, Any]:
|
||||||
from app.main import get_current_user_record # noqa: WPS433
|
from app.main import get_current_user_record # noqa: WPS433
|
||||||
from app.store import has_module # noqa: WPS433
|
from app.store import has_module # noqa: WPS433
|
||||||
@@ -335,13 +350,17 @@ async def batch_download(
|
|||||||
count += 1
|
count += 1
|
||||||
|
|
||||||
# 취합 출고 엑셀(발송날짜/고객/주소/상품/택배 등)을 생성해 함께 넣는다.
|
# 취합 출고 엑셀(발송날짜/고객/주소/상품/택배 등)을 생성해 함께 넣는다.
|
||||||
|
# 상품이름은 아이템코드로 itemcode_db 에서 조회해 채운다.
|
||||||
parcels = st.list_parcels(batch_id=batch_id)
|
parcels = st.list_parcels(batch_id=batch_id)
|
||||||
|
name_map = _itemcode_name_map(request)
|
||||||
xlsx_name = export.export_filename(
|
xlsx_name = export.export_filename(
|
||||||
dispatch_date=batch.get("dispatch_date", ""),
|
dispatch_date=batch.get("dispatch_date", ""),
|
||||||
platform=batch.get("platform", ""),
|
platform=batch.get("platform", ""),
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
xlsx_bytes = export.build_workbook_bytes(batch=batch, parcels=parcels)
|
xlsx_bytes = export.build_workbook_bytes(
|
||||||
|
batch=batch, parcels=parcels, name_map=name_map
|
||||||
|
)
|
||||||
zf.writestr(xlsx_name, xlsx_bytes)
|
zf.writestr(xlsx_name, xlsx_bytes)
|
||||||
count += 1
|
count += 1
|
||||||
except Exception: # noqa: BLE001 — 엑셀 생성 실패해도 원본 다운로드는 살린다.
|
except Exception: # noqa: BLE001 — 엑셀 생성 실패해도 원본 다운로드는 살린다.
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 22.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 512 301" style="enable-background:new 0 0 512 301;" xml:space="preserve">
|
||||||
|
<style type="text/css">
|
||||||
|
.st0{fill:#FFFFFF;}
|
||||||
|
.st1{fill:#EE4D2D;}
|
||||||
|
</style>
|
||||||
|
<rect class="st0" width="512" height="301"/>
|
||||||
|
<path class="st1" d="M119.1,46h6.9c17,0.9,33.8,5.9,46.9,17.1c0.2,0.2,0.3,0.5,0.1,0.7l-20.4,26c-0.2,0.2-0.4,0.3-0.7,0.1
|
||||||
|
c-13.5-8.4-37.6-15.3-51.6-6.6c-9.8,6.1-2.8,16.6,4.8,20.1c25.8,11.8,65.1,23.4,60,61c-2.2,16.5-11.8,29.5-26.1,37.8
|
||||||
|
c-15.4,8.9-35,10.3-52.2,8.1c-17.6-2.2-32.9-9.8-45.7-22.7c-0.3-0.3-0.3-0.6,0-0.9L61.3,161c0.3-0.3,0.8-0.4,1.1-0.1c0,0,0,0,0,0
|
||||||
|
c6.1,5.3,13.1,10.1,20.6,12.8c12.2,4.4,34.5,6.1,43.9-5.2c7.2-8.6-4.1-17.2-10.8-20.3C94,138.1,62.3,129.9,59.5,101
|
||||||
|
c-0.8-8.5,1-16.9,5.6-25.5C75.1,56.7,97.5,46.8,119.1,46z"/>
|
||||||
|
<path class="st1" d="M206.6,46h59.2c27.9,1.2,58.2,14.5,56.5,47.8c-2,38.7-34.3,61.3-70.7,62.1c-8.6,0.2-17.1,0.2-25.5,0
|
||||||
|
c-0.3,0-0.6,0.2-0.6,0.5l-8.1,54.3c0,0.3-0.2,0.5-0.6,0.5l-35.5-0.3c-0.3,0-0.4-0.2-0.4-0.5L206.6,46z M277.6,108.3
|
||||||
|
c6-9.9,6.8-25.7-7.6-29.3c-10.5-2.6-21-2.4-31.5-1.9c-0.4,0-0.6,0.2-0.7,0.6l-6.3,44.8c-0.1,0.4,0.1,0.6,0.6,0.7
|
||||||
|
C247.6,123.7,268.3,123.7,277.6,108.3z"/>
|
||||||
|
<path class="st1" d="M330.5,46h45.1l36,75.6c0.1,0.3,0.1,0.6-0.1,0.9L311.3,250.3c-0.2,0.3-0.5,0.4-0.9,0.4h-40.2
|
||||||
|
c-0.6,0-0.7-0.2-0.4-0.7c32.2-40.9,64.5-81.9,96.9-122.9c2.2-2.7,2.5-2.4,0.8-5.8C355.2,96.4,342.9,71.3,330.5,46z"/>
|
||||||
|
<path class="st1" d="M430.6,46H473l-55.3,68.8c-0.2,0.2-0.5,0.3-0.7,0.1c-0.1,0-0.1-0.1-0.1-0.2L402,83.5c-0.2-0.5-0.2-0.9,0.1-1.3
|
||||||
|
L430.6,46z"/>
|
||||||
|
<path class="st1" d="M415.2,130.3C415.2,130.2,415.3,130.2,415.2,130.3c0.3,0.1,0.5,0.4,0.6,0.6c12.7,26.6,25.4,53.1,38.1,79.5
|
||||||
|
c0.1,0.2,0,0.4-0.2,0.5c-0.1,0-0.1,0-0.2,0h-41.3c-0.4,0-0.6-0.2-0.8-0.5l-22.6-46c-0.1-0.3-0.1-0.6,0.1-0.8L415.2,130.3z"/>
|
||||||
|
<path class="st1" d="M22.8,222.5l36.4,0c0.2,0,0.3,0.2,0.3,0.3c0,0.1,0,0.1-0.1,0.2l-21.6,27.6c-0.1,0.1-0.2,0.1-0.3,0.1H0.7
|
||||||
|
c-0.2,0-0.3-0.1-0.3-0.3c0-0.1,0-0.2,0.1-0.2l22.1-27.6C22.6,222.6,22.7,222.5,22.8,222.5z"/>
|
||||||
|
<path class="st1" d="M128.6,222.9L107,250.6c-0.1,0.1-0.2,0.1-0.3,0.1H47.9c-0.2,0-0.3-0.2-0.3-0.3c0-0.1,0-0.1,0.1-0.2l21.6-27.6
|
||||||
|
c0.1-0.1,0.2-0.1,0.3-0.1l58.8-0.1c0.2,0,0.3,0.2,0.3,0.3C128.7,222.8,128.7,222.9,128.6,222.9z"/>
|
||||||
|
<path class="st1" d="M258.7,250.8H117.1c-0.2,0-0.4-0.1-0.4-0.3c0-0.1,0-0.2,0.1-0.2l21.5-27.5c0.1-0.1,0.2-0.1,0.3-0.1l141.7,0
|
||||||
|
c0.2,0,0.4,0.1,0.4,0.3c0,0.1,0,0.2-0.1,0.2L259,250.6C258.9,250.7,258.8,250.8,258.7,250.8z"/>
|
||||||
|
<path class="st1" d="M349.1,228.9l-0.6,5c0,0.2,0.1,0.5,0.4,0.5c0,0,0,0,0.1,0l11.7-0.3c0.2,0,0.4,0.2,0.4,0.4c0,0,0,0,0,0.1
|
||||||
|
l-0.7,5.1c0,0.2-0.2,0.4-0.4,0.4h-12.2c-0.2,0-0.4,0.2-0.4,0.4l-0.5,3.9c0,0.2,0.1,0.4,0.4,0.5c0,0,0,0,0,0h12.1
|
||||||
|
c0.2,0,0.4,0.2,0.4,0.4c0,0,0,0,0,0.1l-0.9,5.1c0,0.2-0.2,0.4-0.4,0.4H340c-0.2,0-0.4-0.2-0.4-0.4c0,0,0,0,0-0.1l4.3-27.4
|
||||||
|
c0-0.2,0.2-0.4,0.4-0.4l18.6-0.1c0.2,0,0.4,0.2,0.4,0.4c0,0,0,0.1,0,0.1l-0.9,5.2c0,0.2-0.2,0.4-0.4,0.4h-12.4
|
||||||
|
C349.3,228.5,349.2,228.7,349.1,228.9z"/>
|
||||||
|
<path class="st1" d="M376,242.9l-6.3,7.8c-0.1,0.1-0.2,0.1-0.3,0.1h-7c-0.2,0-0.4-0.2-0.4-0.4c0-0.1,0-0.2,0.1-0.2l11.1-13.6
|
||||||
|
c0.1-0.1,0.1-0.3,0-0.4l-6.6-13.2c-0.1-0.2,0-0.4,0.2-0.5c0.1,0,0.1,0,0.2,0l7.3,0.2c0.2,0,0.3,0.1,0.4,0.2l3.3,6.9
|
||||||
|
c0.1,0.2,0.3,0.3,0.5,0.2c0.1,0,0.1-0.1,0.1-0.1l5.8-7c0.1-0.1,0.2-0.1,0.3-0.1l7.2-0.4c0.2,0,0.4,0.2,0.4,0.4c0,0.1,0,0.2-0.1,0.3
|
||||||
|
l-10.8,13.3c-0.1,0.1-0.1,0.3,0,0.4l7,13.6c0.1,0.2,0,0.4-0.2,0.5c-0.1,0-0.1,0-0.2,0h-7.3c-0.2,0-0.3-0.1-0.4-0.2l-3.8-7.6
|
||||||
|
c-0.1-0.2-0.3-0.3-0.5-0.2C376.1,242.8,376.1,242.8,376,242.9z"/>
|
||||||
|
<path class="st1" d="M450.3,244.8h12.2c0.2,0,0.4,0.2,0.4,0.4c0,0,0,0,0,0.1l-0.8,5.2c0,0.2-0.2,0.3-0.4,0.3H443
|
||||||
|
c-0.2,0-0.4-0.2-0.4-0.4c0,0,0,0,0-0.1l4.2-27.4c0-0.2,0.2-0.3,0.4-0.3l18.7-0.2c0.2,0,0.4,0.2,0.4,0.4c0,0,0,0.1,0,0.1l-0.9,5.4
|
||||||
|
c0,0.2-0.2,0.3-0.4,0.3h-12.4c-0.2,0-0.3,0.1-0.4,0.3l-0.8,5c0,0.2,0.1,0.4,0.3,0.4c0,0,0,0,0.1,0l11.9-0.2c0.2,0,0.4,0.2,0.4,0.4
|
||||||
|
c0,0,0,0,0,0l-0.6,5.2c0,0.2-0.2,0.3-0.4,0.3H451c-0.2,0-0.3,0.1-0.4,0.3l-0.7,4C449.9,244.5,450,244.7,450.3,244.8
|
||||||
|
C450.3,244.7,450.3,244.7,450.3,244.8z"/>
|
||||||
|
<path class="st1" d="M399.3,243.8l-0.9,6.3c0,0.3-0.3,0.6-0.7,0.6h-5.3c-0.3,0-0.5-0.2-0.4-0.5l4.3-27.2c0.1-0.4,0.3-0.5,0.6-0.5
|
||||||
|
c8.1-0.1,20.2-2.4,19.7,8.9c-0.4,10.3-8.2,11.7-16.7,11.8C399.6,243.3,399.3,243.5,399.3,243.8z M401.6,229.2l-1.1,7.3
|
||||||
|
c-0.1,0.3,0.1,0.5,0.4,0.5c4.8,0.1,11.3,0.3,9.2-7c-0.2-0.6-0.7-1.1-1.4-1.1l-6.1-0.5C402,228.3,401.7,228.6,401.6,229.2z"/>
|
||||||
|
<path class="st1" d="M421.5,222.7c0-0.1,0.1-0.2,0.2-0.2c6.1-0.1,18.6-2.2,19.9,6.3c0.9,5.8-1.2,10.2-6.4,13.3
|
||||||
|
c-0.3,0.2-0.3,0.4-0.2,0.6l4.2,7.5c0.2,0.3,0.1,0.5-0.3,0.5h-6c-0.4,0-0.6-0.2-0.8-0.5l-3.5-6.3c-0.6-1.1-2.1-1.6-3.2-0.9
|
||||||
|
c-0.6,0.4-1.1,1-1.2,1.7l-0.8,5.4c-0.1,0.4-0.3,0.6-0.7,0.6h-5c-0.5,0-0.7-0.2-0.6-0.7L421.5,222.7z M426.7,229l-1.2,7.7
|
||||||
|
c0,0.3,0.1,0.5,0.5,0.4c3.8-0.2,11.2,0.3,9.5-6.2c-0.8-2.9-5.9-2.6-8.2-2.4C427,228.6,426.8,228.7,426.7,229z"/>
|
||||||
|
<path class="st1" d="M467.3,244.7l1.7-2.1c0.2-0.2,0.4-0.3,0.6-0.1c2.8,1.6,5.8,3.2,9,1.8c2-0.9,2.1-3.1,0-4
|
||||||
|
c-5.3-2.5-12.7-5.2-8.4-12.8c3.6-6.3,12.8-6.1,17.9-2.4c0.3,0.2,0.4,0.5,0.1,0.8l-3,3.6c-0.2,0.3-0.5,0.3-0.8,0.2
|
||||||
|
c-2.8-1.2-6-2.8-8.6-0.3c-0.6,0.6-0.6,1.6-0.1,2.2c0.1,0.1,0.3,0.2,0.4,0.3c5.2,2.6,14.1,5.6,9.5,13.7c-4,7-14.5,6.6-19.8,1.4
|
||||||
|
c-0.2-0.2-0.3-0.5-0.1-0.7c0.2-0.5,0.5-0.9,1-1.1C467.1,244.9,467.2,244.8,467.3,244.7z"/>
|
||||||
|
<path class="st1" d="M493.9,226.9c3.6-5.8,12.6-5.4,17.6-1.9c0.3,0.2,0.4,0.5,0.1,0.8l-3.2,3.8c-0.3,0.3-0.6,0.3-0.9,0.1
|
||||||
|
c-2.2-1.5-4.5-2-7.1-1.5c-1.1,0.2-1.9,1.3-1.7,2.5c0,0.1,0,0.1,0.1,0.2c0.1,0.5,0.7,0.9,1.5,1.3c2.6,1.2,4.8,2.4,6.7,3.6
|
||||||
|
c10.8,7.4-6.6,21.7-17.8,11.2c-0.3-0.3-0.3-0.7-0.1-1l3-3.5c0.3-0.3,0.6-0.4,0.9-0.1c2.2,1.7,8.3,3.9,10,0.4c0.5-1,0.1-2.2-0.9-2.6
|
||||||
|
c0,0-0.1,0-0.1-0.1C496.5,237.7,488.9,235,493.9,226.9z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 5.8 KiB |
Reference in New Issue
Block a user