diff --git a/app/modules/cupang/router.py b/app/modules/cupang/router.py index 5c9f553..92fee16 100644 --- a/app/modules/cupang/router.py +++ b/app/modules/cupang/router.py @@ -16,6 +16,8 @@ from fractions import Fraction from typing import Any from urllib.parse import quote +from datetime import date as _date + from app.timezone import today_kst from fastapi import APIRouter, Body, Depends, Form, HTTPException, Request @@ -908,6 +910,128 @@ async def product_delete( return RedirectResponse(url="/cupang/products", status_code=303) +# ════════════════════════════════════════════════════════════ +# 분배 확정 — 센터별 출고 묶음 생성 +# ════════════════════════════════════════════════════════════ +@router.post("/api/box-calc/confirm") +async def box_calc_confirm( + request: Request, + payload: dict[str, Any] = Body(...), + user: dict[str, Any] = Depends(_require_user), +) -> JSONResponse: + """{ship_date, centers:[{center_id, ship_method, boxes, items:[...]}]} → 센터마다 출고 묶음 1건 생성. + + 화면에서 보낸 박스 수는 요약 표시용이고, 라인의 박스 수는 저장 시 + 서버(store.compute_boxes)가 수량과 입수량으로 다시 계산한다. + """ + from .store import SHIP_METHODS as _METHODS # noqa: WPS433 + + store = _store(request) + if store is None: + raise HTTPException(status_code=503, detail="cupang_db 미설정") + + ship_date = str(payload.get("ship_date") or "").strip() + try: + _date.fromisoformat(ship_date) + except ValueError: + raise HTTPException(status_code=400, detail="출고일자를 올바르게 선택하세요.") + + raw_centers = payload.get("centers") + if not isinstance(raw_centers, list) or not raw_centers: + raise HTTPException(status_code=400, detail="확정할 센터가 없습니다.") + + rules = {r["product_code"]: r for r in store.list_box_rules()} + known_centers = {str(c["id"]): c for c in store.list_centers(include_inactive=True)} + today = today_kst().isoformat() + worker = str(user.get("name") or user.get("email") or "") + + plans: list[dict[str, Any]] = [] + for raw in raw_centers: + if not isinstance(raw, dict): + continue + cid = str(raw.get("center_id") or "").strip() + center = known_centers.get(cid) + if center is None: + raise HTTPException(status_code=400, detail=f"알 수 없는 센터입니다: {cid}") + + method = str(raw.get("ship_method") or "").strip() + if method not in _METHODS: + raise HTTPException( + status_code=400, detail=f"{center['name']} 의 출고방식을 선택하세요." + ) + + # 같은 제품이 여러 박스로 나뉘어 담겼을 수 있으므로 제품코드로 합친다. + merged: dict[str, int] = {} + for it in raw.get("items") or []: + if not isinstance(it, dict): + continue + code = str(it.get("product_code") or "").strip() + if not code: + continue + try: + qty = int(it.get("quantity") or 0) + except (TypeError, ValueError): + qty = 0 + if qty <= 0: + continue + merged[code] = merged.get(code, 0) + qty + + if not merged: + continue + + lines = [] + for code, qty in merged.items(): + rule = rules.get(code) + lines.append( + { + "product_code": code, + "product_name_snapshot": (rule or {}).get("product_name_snapshot") or code, + "quantity": qty, + "units_per_box": (rule or {}).get("units_per_box"), + "box_rule_id": (rule or {}).get("id"), + } + ) + + try: + boxes = max(int(raw.get("boxes") or 0), 0) + except (TypeError, ValueError): + boxes = 0 + pieces = sum(merged.values()) + summary = f"{boxes}박스 · {pieces}개" if boxes else f"{pieces}개" + + plans.append({"center": center, "method": method, "lines": lines, "summary": summary}) + + if not plans: + raise HTTPException(status_code=400, detail="담긴 품목이 없습니다.") + + created: list[dict[str, Any]] = [] + for plan in plans: + center = plan["center"] + try: + ship = store.create_shipment( + created_by=str(user.get("email") or ""), + header={ + "document_date": today, + "ship_date": ship_date, + # 센터입고일은 출고일과 같게 두고, 필요하면 출고 상세에서 고친다. + "center_arrival_date": ship_date, + "center_id": center["id"], + "center_name_snapshot": center["name"], + "ship_method": plan["method"], + "outbound_summary": plan["summary"], + "worker": worker, + "status": "출고준비", + "memo": "박스 계산에서 분배 확정", + }, + lines=plan["lines"], + ) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) + created.append({"id": ship["id"], "center_name": center["name"]}) + + return JSONResponse({"created": created, "ship_date": ship_date}) + + # ════════════════════════════════════════════════════════════ # 박스 계산 임시 저장 (화면 상태 스냅샷) # ════════════════════════════════════════════════════════════ diff --git a/app/modules/cupang/templates/cupang/box_calc.html b/app/modules/cupang/templates/cupang/box_calc.html index e6fe466..df28782 100644 --- a/app/modules/cupang/templates/cupang/box_calc.html +++ b/app/modules/cupang/templates/cupang/box_calc.html @@ -1,6 +1,6 @@ {% extends "erp_base.html" %} -{% block head_extra %}{% endblock %} +{% block head_extra %}{% endblock %} {% block content %}
@@ -79,9 +79,11 @@
-
+

③ 센터 분배

담을 센터를 골라 추가하세요. +
{% if not centers %} @@ -123,6 +125,36 @@
+ + +