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:
2026-06-12 10:36:10 +09:00
parent 03929ff517
commit d56365b338
6 changed files with 123 additions and 5 deletions
+55 -1
View File
@@ -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}"'},
)
# ════════════════════════════════════════════════════════════
# 일일 재고조사
# ════════════════════════════════════════════════════════════