feat(dispatch): 출고 배치별 업로드 원본 zip 다운로드
배치 목록에 '다운로드' 버튼 추가. 피킹/라벨 PDF + 주문 엑셀 중 실제 존재하는 파일만 zip 으로 묶어 내려준다. 한글 파일명은 RFC5987 filename* 으로 인코딩. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -12,14 +12,17 @@ Order ID / Package ID / Tracking ID / Seller SKU / Quantity 뿐이다.
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import io
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import unicodedata
|
import unicodedata
|
||||||
|
import zipfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
from urllib.parse import quote
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, File, Form, HTTPException, Request, UploadFile
|
from fastapi import APIRouter, Depends, File, Form, HTTPException, Request, UploadFile
|
||||||
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
|
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse, Response
|
||||||
|
|
||||||
from app.timezone import now_kst, today_kst
|
from app.timezone import now_kst, today_kst
|
||||||
|
|
||||||
@@ -259,6 +262,57 @@ async def batch_delete(
|
|||||||
return RedirectResponse(url="/dispatch/", status_code=303)
|
return RedirectResponse(url="/dispatch/", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/batches/{batch_id:int}/download")
|
||||||
|
async def batch_download(
|
||||||
|
request: Request, batch_id: int, user: dict[str, Any] = Depends(_require_user)
|
||||||
|
) -> Response:
|
||||||
|
"""배치에 업로드된 원본 파일(피킹/라벨 PDF + 주문 엑셀)을 zip 으로 묶어 다운로드."""
|
||||||
|
st = _store(request)
|
||||||
|
if st is None:
|
||||||
|
raise HTTPException(status_code=503, detail="dispatch_db 미설정")
|
||||||
|
batch = st.get_batch(batch_id=batch_id)
|
||||||
|
if batch is None:
|
||||||
|
raise HTTPException(status_code=404, detail="배치를 찾을 수 없습니다.")
|
||||||
|
|
||||||
|
# 저장된 경로 중 실제로 존재하는 파일만 묶는다. 같은 파일명 충돌 시 번호 부여.
|
||||||
|
paths = [
|
||||||
|
batch.get("picking_pdf_path"),
|
||||||
|
batch.get("label_pdf_path"),
|
||||||
|
batch.get("order_export_path"),
|
||||||
|
]
|
||||||
|
buf = io.BytesIO()
|
||||||
|
used: set[str] = set()
|
||||||
|
count = 0
|
||||||
|
with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf:
|
||||||
|
for raw in paths:
|
||||||
|
p = Path(raw) if raw else None
|
||||||
|
if not p or not p.is_file():
|
||||||
|
continue
|
||||||
|
arcname = p.name
|
||||||
|
n = 1
|
||||||
|
while arcname in used:
|
||||||
|
arcname = f"{p.stem}_{n}{p.suffix}"
|
||||||
|
n += 1
|
||||||
|
used.add(arcname)
|
||||||
|
zf.write(p, arcname=arcname)
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
if count == 0:
|
||||||
|
raise HTTPException(status_code=404, detail="다운로드할 업로드 파일이 없습니다.")
|
||||||
|
|
||||||
|
slug = _slugify(batch.get("batch_name") or "dispatch")
|
||||||
|
fname = f"{batch.get('dispatch_date', '')}_{slug}.zip".lstrip("_")
|
||||||
|
headers = {
|
||||||
|
"Content-Disposition": (
|
||||||
|
f"attachment; filename=\"dispatch_{batch_id}.zip\"; "
|
||||||
|
f"filename*=UTF-8''{quote(fname)}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return Response(
|
||||||
|
content=buf.getvalue(), media_type="application/zip", headers=headers
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# ════════════════════════════════════════════════════════════
|
# ════════════════════════════════════════════════════════════
|
||||||
# 출고 작업 리스트 (1박스 = 1카드)
|
# 출고 작업 리스트 (1박스 = 1카드)
|
||||||
# ════════════════════════════════════════════════════════════
|
# ════════════════════════════════════════════════════════════
|
||||||
|
|||||||
@@ -36,6 +36,7 @@
|
|||||||
<a class="erp-btn erp-btn-outline" href="/dispatch/batches/{{ b.id }}">작업</a>
|
<a class="erp-btn erp-btn-outline" href="/dispatch/batches/{{ b.id }}">작업</a>
|
||||||
<a class="erp-btn erp-btn-outline" href="/dispatch/batches/{{ b.id }}/picking">피킹</a>
|
<a class="erp-btn erp-btn-outline" href="/dispatch/batches/{{ b.id }}/picking">피킹</a>
|
||||||
<a class="erp-btn erp-btn-outline" href="/dispatch/batches/{{ b.id }}/handover">전달</a>
|
<a class="erp-btn erp-btn-outline" href="/dispatch/batches/{{ b.id }}/handover">전달</a>
|
||||||
|
<a class="erp-btn erp-btn-outline" href="/dispatch/batches/{{ b.id }}/download">다운로드</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
Reference in New Issue
Block a user