feat(dispatch): 날짜별 사방넷 재고 엑셀 다운로드 + 달력 UI

- 배치 목록을 좌(리스트)/우(달력) 2단 배치
- 달력에서 배치 있는 날짜 클릭 → 그날 전 배치 출고를 낱개로 분해해 다운로드
  · _N 배수 × 주문수량 × 콤보 BOM 구성수량, MD-(뚜껑) 제외
  · 사방넷 4열 템플릿(A 상품코드[필수]=사방넷코드 / B 가용수량=수량 /
    C 불용수량=0 / D 바코드=아이템코드), 주황 헤더, xlsx
- db.stock_aggregate_for_date, export.explode_to_singles/build_stock_xlsx
- 라우터 GET /dispatch/stock-export?date=YYYY-MM-DD

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-22 17:25:05 +09:00
parent 2c7f98ef30
commit 9221efe925
4 changed files with 274 additions and 2 deletions
+57 -1
View File
@@ -76,6 +76,24 @@ def _itemcode_name_map(request: Request) -> dict[str, str]:
return {}
def _itemcode_bom_and_sabangnet(request: Request) -> tuple[dict[str, Any], dict[str, str]]:
"""(세트 BOM map, item_code→사방넷코드 map). 미설정/실패 시 빈 dict."""
reader = getattr(request.app.state, "malaysia_itemcode", None)
if reader is None or not getattr(reader, "enabled", False):
return {}, {}
bom: dict[str, Any] = {}
sab: dict[str, str] = {}
try:
bom = reader.set_bom_map()
except Exception: # noqa: BLE001
logger.exception("itemcode set_bom_map 조회 실패")
try:
sab = reader.sabangnet_code_map()
except Exception: # noqa: BLE001
logger.exception("itemcode sabangnet_code_map 조회 실패")
return bom, sab
def _require_user(request: Request) -> dict[str, Any]:
from app.main import get_current_user_record # noqa: WPS433
from app.store import has_module # noqa: WPS433
@@ -180,12 +198,14 @@ async def batches_index(request: Request) -> HTMLResponse:
if not isinstance(guard, tuple):
return guard
st, user = guard
batches = st.list_batches()
ctx = _base_ctx(request, user)
ctx.update(
{
"page_title": "말레이시아 배송 — 출고 배치",
"page_subtitle": "TikTok 일일 출고 배치 목록",
"batches": st.list_batches(),
"batches": batches,
"batch_dates": sorted({b["dispatch_date"] for b in batches if b.get("dispatch_date")}),
"active_tab": "batches",
}
)
@@ -382,6 +402,42 @@ async def batch_download(
)
@router.get("/stock-export")
async def stock_export(
request: Request, date: str, user: dict[str, Any] = Depends(_require_user)
) -> Response:
"""선택한 날짜의 전 배치 출고 수량을 낱개로 분해해 사방넷 재고 업로드 xlsx 다운로드.
콤보(세트)는 BOM 으로 낱개 분해, _N 배수 반영, MD-(뚜껑) 제외.
A=사방넷코드 / B=수량 / C=불용수량(0) / D=아이템코드.
"""
st = _store(request)
if st is None:
raise HTTPException(status_code=503, detail="dispatch_db 미설정")
d = (date or "").strip()
if not d:
raise HTTPException(status_code=400, detail="날짜가 필요합니다.")
rows = st.stock_aggregate_for_date(dispatch_date=d)
if not rows:
raise HTTPException(status_code=404, detail="해당 날짜의 출고 데이터가 없습니다.")
set_bom, sabangnet_map = _itemcode_bom_and_sabangnet(request)
xlsx_bytes = export.build_stock_xlsx(rows=rows, set_bom=set_bom, sabangnet_map=sabangnet_map)
fname = export.stock_filename(d)
headers = {
"Content-Disposition": (
f"attachment; filename=\"stock_{d}.xlsx\"; "
f"filename*=UTF-8''{quote(fname)}"
)
}
return Response(
content=xlsx_bytes,
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
headers=headers,
)
# ════════════════════════════════════════════════════════════
# 출고 작업 리스트 (1박스 = 1카드)
# ════════════════════════════════════════════════════════════