feat(malaysia): 입출고 일자별 낱개 수량 엑셀 다운로드
- '일괄 적용' 버튼명 '입력'으로 변경 - 입력 버튼 우측 '다운로드' 버튼 추가 - GET /malaysia/movements/export: 해당 일자/종류(IN/OUT) 낱개 합계를 엑셀(A=사방넷코드, B=수량, C=이름)로 스트리밍 - db.daily_movement_totals(): 일자/종류별 낱개 합계 집계 - itemcode.sabangnet_code_map(): item_code → 사방넷 코드 매핑 - i18n 사전 '입력'/'다운로드' 추가, malaysia.js 캐시 버전 bump Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -14,7 +14,7 @@ from __future__ import annotations
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Body, Depends, Form, HTTPException, Request
|
||||
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
|
||||
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse, StreamingResponse
|
||||
|
||||
from app.timezone import today_kst
|
||||
|
||||
@@ -292,6 +292,60 @@ async def movements_apply(request: Request, user: dict[str, Any] = Depends(_requ
|
||||
return RedirectResponse(url=f"/malaysia/movements?wh={wh}&type={mtype}", status_code=303)
|
||||
|
||||
|
||||
@router.get("/movements/export")
|
||||
async def movements_export(request: Request) -> StreamingResponse:
|
||||
"""해당 일자/종류(IN/OUT)에 입출고된 낱개 수량을 엑셀로 내보낸다.
|
||||
|
||||
컬럼: A=사방넷 코드, B=수량, C=이름.
|
||||
세트 OUT 은 생성 시 구성품 OUT 으로 분해 저장되므로 낱개 기준으로 합산된다.
|
||||
"""
|
||||
import io # noqa: WPS433
|
||||
|
||||
guard = _guard(request)
|
||||
if not isinstance(guard, tuple):
|
||||
return guard # type: ignore[return-value]
|
||||
st, _user = guard
|
||||
wh = _selected_wh(request, st)
|
||||
mtype = (request.query_params.get("type") or "OUT").strip().upper()
|
||||
if mtype not in ("IN", "OUT"):
|
||||
raise HTTPException(status_code=400, detail="type 은 IN 또는 OUT 이어야 합니다.")
|
||||
mdate = (request.query_params.get("date") or today_kst().isoformat()).strip()
|
||||
|
||||
totals = st.daily_movement_totals(
|
||||
warehouse_code=wh, movement_date=mdate, movement_type=mtype
|
||||
)
|
||||
reader = _reader(request)
|
||||
sabang = reader.sabangnet_code_map() if reader else {}
|
||||
names = reader.name_map() if reader else {}
|
||||
item_names = st.item_name_map()
|
||||
|
||||
from openpyxl import Workbook # noqa: WPS433
|
||||
|
||||
wb = Workbook()
|
||||
ws = wb.active
|
||||
ws.title = f"{mtype}_{mdate}"
|
||||
ws.append(["사방넷코드", "수량", "이름"])
|
||||
for code in sorted(totals.keys()):
|
||||
qty = totals[code]
|
||||
if qty == 0:
|
||||
continue
|
||||
ws.append([
|
||||
sabang.get(code, ""),
|
||||
int(qty),
|
||||
names.get(code) or item_names.get(code) or code,
|
||||
])
|
||||
|
||||
buf = io.BytesIO()
|
||||
wb.save(buf)
|
||||
buf.seek(0)
|
||||
fname = f"malaysia_{mtype}_{mdate}.xlsx"
|
||||
return StreamingResponse(
|
||||
buf,
|
||||
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
headers={"Content-Disposition": f'attachment; filename="{fname}"'},
|
||||
)
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════════
|
||||
# 일일 재고조사
|
||||
# ════════════════════════════════════════════════════════════
|
||||
|
||||
Reference in New Issue
Block a user