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:
@@ -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카드)
|
||||
# ════════════════════════════════════════════════════════════
|
||||
|
||||
Reference in New Issue
Block a user