From d56365b3383546eb332f27dcf5bceaf782ead031 Mon Sep 17 00:00:00 2001 From: king Date: Fri, 12 Jun 2026 10:36:10 +0900 Subject: [PATCH] =?UTF-8?q?feat(malaysia):=20=EC=9E=85=EC=B6=9C=EA=B3=A0?= =?UTF-8?q?=20=EC=9D=BC=EC=9E=90=EB=B3=84=20=EB=82=B1=EA=B0=9C=20=EC=88=98?= =?UTF-8?q?=EB=9F=89=20=EC=97=91=EC=85=80=20=EB=8B=A4=EC=9A=B4=EB=A1=9C?= =?UTF-8?q?=EB=93=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - '일괄 적용' 버튼명 '입력'으로 변경 - 입력 버튼 우측 '다운로드' 버튼 추가 - 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 --- app/modules/malaysia/db.py | 25 +++++++++ app/modules/malaysia/itemcode.py | 19 +++++++ app/modules/malaysia/router.py | 56 ++++++++++++++++++- .../malaysia/templates/malaysia/_i18n.html | 2 +- .../templates/malaysia/movements.html | 23 +++++++- app/static/malaysia.js | 3 +- 6 files changed, 123 insertions(+), 5 deletions(-) diff --git a/app/modules/malaysia/db.py b/app/modules/malaysia/db.py index 2f6a758..34316dd 100644 --- a/app/modules/malaysia/db.py +++ b/app/modules/malaysia/db.py @@ -304,6 +304,31 @@ class MalaysiaStockStore: ).fetchall() return [self._serialize(r) for r in rows] + def daily_movement_totals( + self, *, warehouse_code: str, movement_date: str, movement_type: str + ) -> dict[str, int]: + """특정 일자/종류의 낱개 아이템별 합계 수량. + + 세트 OUT 은 생성 시 이미 구성품 OUT 으로 분해 저장되므로 그대로 합산된다. + 반환: { item_code: 합계수량 } + """ + if movement_type not in store.MOVEMENT_TYPES: + raise ValueError(f"허용되지 않는 이동 종류: {movement_type}") + with self._pool.connection() as conn: + rows = conn.execute( + """ + SELECT item_code, SUM(qty) AS qty + FROM stock_movement + WHERE warehouse_code = %s + AND movement_date = %s + AND movement_type = %s + GROUP BY item_code + ORDER BY item_code + """, + (warehouse_code, movement_date, movement_type), + ).fetchall() + return {r["item_code"]: int(r["qty"] or 0) for r in rows} + def current_stock(self, *, warehouse_code: str) -> dict[str, int]: """창고별 현재고(낱개 아이템 기준). diff --git a/app/modules/malaysia/itemcode.py b/app/modules/malaysia/itemcode.py index 0676c97..8ef8d76 100644 --- a/app/modules/malaysia/itemcode.py +++ b/app/modules/malaysia/itemcode.py @@ -115,6 +115,25 @@ class MalaysiaItemcodeReader: for r in rows } + def sabangnet_code_map(self) -> dict[str, str]: + """낱개+세트 item_code → 사방넷 코드(itemcode_db). 엑셀 내보내기용.""" + if not self.enabled or not self._pool: + return {} + try: + with self._pool.connection() as conn: + rows = conn.execute( + "SELECT item_code, sabangnet_code FROM single_items " + "UNION ALL SELECT item_code, sabangnet_code FROM set_items" + ).fetchall() + self.last_error = "" + except Exception as exc: # noqa: BLE001 + self.last_error = f"{type(exc).__name__}: {exc}" + return {} + return { + str(r["item_code"]).strip().upper(): str(r.get("sabangnet_code") or "").strip() + for r in rows + } + def close(self) -> None: if self._pool is not None: self._pool.close() diff --git a/app/modules/malaysia/router.py b/app/modules/malaysia/router.py index 2607899..d7a800d 100644 --- a/app/modules/malaysia/router.py +++ b/app/modules/malaysia/router.py @@ -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}"'}, + ) + + # ════════════════════════════════════════════════════════════ # 일일 재고조사 # ════════════════════════════════════════════════════════════ diff --git a/app/modules/malaysia/templates/malaysia/_i18n.html b/app/modules/malaysia/templates/malaysia/_i18n.html index 014fbe5..701f4c9 100644 --- a/app/modules/malaysia/templates/malaysia/_i18n.html +++ b/app/modules/malaysia/templates/malaysia/_i18n.html @@ -2,4 +2,4 @@
- + diff --git a/app/modules/malaysia/templates/malaysia/movements.html b/app/modules/malaysia/templates/malaysia/movements.html index 533ad29..851b15a 100644 --- a/app/modules/malaysia/templates/malaysia/movements.html +++ b/app/modules/malaysia/templates/malaysia/movements.html @@ -42,8 +42,9 @@ -
- +
+ +
@@ -87,6 +88,24 @@ + +
diff --git a/app/static/malaysia.js b/app/static/malaysia.js index 1d3cd6e..53cbd23 100644 --- a/app/static/malaysia.js +++ b/app/static/malaysia.js @@ -38,7 +38,8 @@ "IN (입고)": "IN (In)", "OUT (출고)": "OUT (Out)", "ADJUST (조정 ±)": "ADJUST (±)", - "일괄 적용": "Apply All", + "입력": "Input", + "다운로드": "Download", "낱개 상품": "Single Products", "콤보 상품": "Combo Products", "이동 이력": "Movement History",