feat(cupang): 박스 계산 화면 추가
달력 상단 "박스 계산" 버튼 → /cupang/box-calc. 제품명+수량을 여러 행 입력하면 제품별 박스 수·남은 낱개·총 필요 박스와 박스명별 합계를 계산. 수량/제품을 고치고 "계산 / 재계산" 으로 다시 계산할 수 있다(수량 칸에서 Enter 도 동일). 저장하지 않는 계산 전용 화면. 계산은 POST /cupang/api/box-calc 에서 store.compute_boxes 로 수행 — 클라이언트 계산을 신뢰하지 않는다. 입수량 규칙이 없는 제품은 "미설정" 으로 표시하고 합계에서 제외. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -568,6 +568,102 @@ async def box_rule_delete(
|
||||
return RedirectResponse(url="/cupang/box-rules", status_code=303)
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════════
|
||||
# 박스 계산기 — 제품명 + 수량 → 박스 수 / 남은 낱개
|
||||
# 저장하지 않는 계산 전용 화면. 규칙은 cupang_box_rules 를 그대로 사용한다.
|
||||
# ════════════════════════════════════════════════════════════
|
||||
@router.get("/box-calc", response_class=HTMLResponse)
|
||||
async def box_calc_page(request: Request) -> HTMLResponse:
|
||||
from app.main import build_erp_nav, render_template # noqa: WPS433
|
||||
from app.store import is_admin # noqa: WPS433
|
||||
|
||||
guard = _guard(request)
|
||||
if not isinstance(guard, tuple):
|
||||
return guard
|
||||
store, user = guard
|
||||
return render_template(
|
||||
request,
|
||||
"cupang/box_calc.html",
|
||||
{
|
||||
"user": user,
|
||||
"is_admin": is_admin(user),
|
||||
"nav_items": build_erp_nav(user, active="cupang"),
|
||||
"page_title": "쿠팡 밀크런 — 박스 계산",
|
||||
"page_subtitle": "제품명과 수량을 넣으면 박스 수와 남은 낱개를 계산합니다.",
|
||||
"box_rules": store.list_box_rules(),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@router.post("/api/box-calc")
|
||||
async def box_calc_api(
|
||||
request: Request,
|
||||
payload: dict[str, Any] = Body(...),
|
||||
user: dict[str, Any] = Depends(_require_user),
|
||||
) -> JSONResponse:
|
||||
"""[{product_code, quantity}] → 제품별 박스 계산 + 박스명별 합계.
|
||||
|
||||
클라이언트 계산을 신뢰하지 않고 store.compute_boxes 로 서버에서 계산한다.
|
||||
"""
|
||||
from .store import compute_boxes # noqa: WPS433
|
||||
|
||||
store = _store(request)
|
||||
if store is None:
|
||||
raise HTTPException(status_code=503, detail="cupang_db 미설정")
|
||||
|
||||
raw_items = payload.get("items")
|
||||
if not isinstance(raw_items, list):
|
||||
raise HTTPException(status_code=400, detail="items 는 배열이어야 합니다.")
|
||||
|
||||
rules = {r["product_code"]: r for r in store.list_box_rules()}
|
||||
|
||||
results: list[dict[str, Any]] = []
|
||||
totals: dict[str, dict[str, Any]] = {}
|
||||
for raw in raw_items:
|
||||
if not isinstance(raw, dict):
|
||||
continue
|
||||
code = str(raw.get("product_code") or "").strip()
|
||||
if not code:
|
||||
continue
|
||||
try:
|
||||
qty = int(raw.get("quantity") or 0)
|
||||
except (TypeError, ValueError):
|
||||
qty = 0
|
||||
qty = max(qty, 0)
|
||||
|
||||
rule = rules.get(code)
|
||||
upb = rule["units_per_box"] if rule else None
|
||||
calc = compute_boxes(qty, upb)
|
||||
box_name = (rule or {}).get("box_name") or ""
|
||||
results.append(
|
||||
{
|
||||
"product_code": code,
|
||||
"product_name": (rule or {}).get("product_name_snapshot") or code,
|
||||
"box_name": box_name,
|
||||
"units_per_box": calc["units_per_box"],
|
||||
"quantity": qty,
|
||||
"configured": calc["configured"],
|
||||
"full_boxes": calc["full_boxes"],
|
||||
"remainder_units": calc["remainder_units"],
|
||||
"required_boxes": calc["required_boxes"],
|
||||
}
|
||||
)
|
||||
if calc["configured"]:
|
||||
agg = totals.setdefault(
|
||||
box_name, {"box_name": box_name, "full_boxes": 0, "required_boxes": 0, "remainder_units": 0}
|
||||
)
|
||||
agg["full_boxes"] += calc["full_boxes"]
|
||||
agg["required_boxes"] += calc["required_boxes"]
|
||||
agg["remainder_units"] += calc["remainder_units"]
|
||||
|
||||
return JSONResponse(
|
||||
{
|
||||
"results": results,
|
||||
"totals": sorted(totals.values(), key=lambda t: t["box_name"]),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════════
|
||||
# 설정 — 제품명 카탈로그 관리 (itemcode_db 에서 등록)
|
||||
# ════════════════════════════════════════════════════════════
|
||||
|
||||
Reference in New Issue
Block a user