feat(cupang): 상자 목록을 번호·제품명·제품코드·수량 표로, 엑셀 다운로드 추가

- ② 상자 목록: 카드 대신 표. 한 상자에 여러 제품이면 상자번호 칸 rowspan 병합
- GET /cupang/{id}/boxes.xlsx — 같은 표를 xlsx 로 (export.build_box_list_workbook)
- box_list 항목에 product_code 포함

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-01 19:55:06 +09:00
parent 15947e73ed
commit 3926b6a431
9 changed files with 174 additions and 61 deletions
+53 -2
View File
@@ -431,6 +431,47 @@ async def detail(request: Request, shipment_id: int) -> HTMLResponse:
)
@router.get("/{shipment_id:int}/boxes.xlsx")
async def export_box_list_xlsx(request: Request, shipment_id: int) -> Any:
"""상자 목록(상자번호·제품명·제품코드·수량) 엑셀 다운로드."""
from io import BytesIO # noqa: WPS433
from fastapi.responses import StreamingResponse # noqa: WPS433
from .export import build_box_list_workbook # noqa: WPS433
guard = _guard(request)
if not isinstance(guard, tuple):
return guard
store, _user = guard
ship = store.get_shipment(shipment_id=shipment_id)
if not ship:
raise HTTPException(status_code=404, detail="출고 묶음을 찾을 수 없습니다.")
items = [
{
"product_code": ln.get("product_code"),
"product_name": ln.get("product_name_snapshot") or ln.get("product_code"),
"quantity": int(ln.get("quantity") or 0),
}
for ln in (ship.get("lines") or [])
]
box_list = _expand_box_list(ship, _compute_boxes(store, items), items)
wb = build_box_list_workbook(ship, box_list)
buf = BytesIO()
wb.save(buf)
buf.seek(0)
stamp = str(ship.get("ship_date") or "").replace("-", "")
filename = f"{stamp}_cupang_boxes_{shipment_id}.xlsx"
return StreamingResponse(
buf,
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
headers={"Content-Disposition": f'attachment; filename="{filename}"'},
)
def _safe_next(raw: str, fallback: str) -> str:
"""열린 리다이렉트 방지 — /cupang/ 안쪽 경로만 허용."""
nxt = (raw or "").strip()
@@ -1077,6 +1118,7 @@ def _expand_box_list(
contents = [
{
"product_name": it.get("product_name") or it.get("product_code") or "",
"product_code": it.get("product_code") or "",
"quantity": int(it.get("quantity") or 0),
}
for it in (entry.get("items") or [])
@@ -1087,7 +1129,11 @@ def _expand_box_list(
else:
code = str(entry.get("product_code") or "")
contents = [
{"product_name": entry.get("name") or code, "quantity": units}
{
"product_name": entry.get("name") or code,
"product_code": code,
"quantity": units,
}
]
box_name = box_name_by_code.get(code, "")
for _ in range(count):
@@ -1102,7 +1148,11 @@ def _expand_box_list(
"kind": "product",
"box_name": r.get("box_name") or "",
"items": [
{"product_name": r["product_name"], "quantity": r["units_per_box"]}
{
"product_name": r["product_name"],
"product_code": r["product_code"],
"quantity": r["units_per_box"],
}
],
}
)
@@ -1115,6 +1165,7 @@ def _expand_box_list(
"items": [
{
"product_name": it.get("product_name") or "",
"product_code": it.get("product_code") or "",
"quantity": int(it.get("quantity") or 0),
}
for it in (box.get("items") or [])