change(cupang): 화면 문구 "박스" → "상자" 통일
템플릿·라우터 메시지·페이지 제목과 seed/DDL 기본값(쿠팡박스→쿠팡상자)까지 표기를 맞췄다. URL(/box-calc)과 코드 식별자(box_name 등)는 그대로. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -154,7 +154,7 @@ async def index(request: Request) -> HTMLResponse:
|
||||
# 선택일의 묶음 — 달력과 같은 기준(출고일)
|
||||
sel_shipments = [s for s in shipments if s.get("ship_date") == sel]
|
||||
|
||||
# 오른쪽 상세: 센터별 상품 목록·수량·박스 수·출고방식
|
||||
# 오른쪽 상세: 센터별 상품 목록·수량·상자 수·출고방식
|
||||
for s in sel_shipments:
|
||||
full = store.get_shipment(shipment_id=s["id"])
|
||||
lines = (full.get("lines") if full else []) or []
|
||||
@@ -334,16 +334,16 @@ async def export_shipments_xlsx(request: Request, date: str = "") -> Any:
|
||||
# ════════════════════════════════════════════════════════════
|
||||
@router.get("/new")
|
||||
async def new_form(request: Request) -> RedirectResponse:
|
||||
"""신규 등록 폼은 없앴다. 출고 묶음은 박스 계산의 [분배 확정] 으로만 만든다.
|
||||
"""신규 등록 폼은 없앴다. 출고 묶음은 상자 계산의 [분배 확정] 으로만 만든다.
|
||||
|
||||
예전 링크/북마크가 404 나지 않도록 박스 계산으로 보낸다.
|
||||
예전 링크/북마크가 404 나지 않도록 상자 계산으로 보낸다.
|
||||
"""
|
||||
return RedirectResponse(url="/cupang/box-calc", status_code=303)
|
||||
|
||||
|
||||
@router.get("/{shipment_id:int}", response_class=HTMLResponse)
|
||||
async def detail(request: Request, shipment_id: int) -> HTMLResponse:
|
||||
"""출고 묶음 보기 — 박스 계산 화면과 같은 3열 구성(읽기 전용).
|
||||
"""출고 묶음 보기 — 상자 계산 화면과 같은 3열 구성(읽기 전용).
|
||||
|
||||
수정 기능은 없앴다. 잘못 만들었으면 달력에서 삭제하고 다시 확정한다.
|
||||
"""
|
||||
@@ -611,7 +611,7 @@ async def center_delete(
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════════
|
||||
# 박스 입수량 관리
|
||||
# 상자 입수량 관리
|
||||
# ════════════════════════════════════════════════════════════
|
||||
@router.get("/box-rules", response_class=HTMLResponse)
|
||||
async def box_rules_page(request: Request) -> HTMLResponse:
|
||||
@@ -629,8 +629,8 @@ async def box_rules_page(request: Request) -> HTMLResponse:
|
||||
"user": user,
|
||||
"is_admin": is_admin(user),
|
||||
"nav_items": build_erp_nav(user, active="cupang"),
|
||||
"page_title": "쿠팡 밀크런 — 박스 입수량",
|
||||
"page_subtitle": "제품코드별 쿠팡박스 1박스당 입수량 설정",
|
||||
"page_title": "쿠팡 밀크런 — 상자 입수량",
|
||||
"page_subtitle": "제품코드별 쿠팡상자 1상자당 입수량 설정",
|
||||
"box_rules": store.list_box_rules(include_inactive=True),
|
||||
"products": store.list_products(),
|
||||
"search_enabled": bool((_itemcode(request)) and _itemcode(request).enabled),
|
||||
@@ -644,7 +644,7 @@ async def box_rule_upsert(
|
||||
product_code: str = Form(...),
|
||||
units_per_box: int = Form(...),
|
||||
product_name_snapshot: str = Form(""),
|
||||
box_name: str = Form("쿠팡박스"),
|
||||
box_name: str = Form("쿠팡상자"),
|
||||
memo: str = Form(""),
|
||||
user: dict[str, Any] = Depends(_require_user),
|
||||
) -> RedirectResponse:
|
||||
@@ -681,7 +681,7 @@ async def box_rule_delete(
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════════
|
||||
# 박스 계산기 — 제품명 + 수량 → 박스 수 / 남은 낱개
|
||||
# 상자 계산기 — 제품명 + 수량 → 상자 수 / 남은 낱개
|
||||
# 저장하지 않는 계산 전용 화면. 규칙은 cupang_box_rules 를 그대로 사용한다.
|
||||
# ════════════════════════════════════════════════════════════
|
||||
@router.get("/box-calc", response_class=HTMLResponse)
|
||||
@@ -701,7 +701,7 @@ async def box_calc_page(request: Request) -> HTMLResponse:
|
||||
"is_admin": is_admin(user),
|
||||
"nav_items": build_erp_nav(user, active="cupang"),
|
||||
"page_title": "쿠팡 밀크런 — 쿠팡 발주 업로드",
|
||||
"page_subtitle": "쿠팡 발주 엑셀을 올리면 센터별 수량을 합산해 박스를 계산합니다.",
|
||||
"page_subtitle": "쿠팡 발주 엑셀을 올리면 센터별 수량을 합산해 상자를 계산합니다.",
|
||||
"box_rules": store.list_box_rules(),
|
||||
# 센터 선택 드롭다운은 가나다순 (한글 음절은 코드포인트 순 = 가나다순)
|
||||
"centers": sorted(store.list_centers(), key=lambda c: (c.get("name") or "")),
|
||||
@@ -866,7 +866,7 @@ async def box_calc_upload(
|
||||
if unknown_centers:
|
||||
warnings.append("등록되지 않은 센터: " + ", ".join(sorted(unknown_centers)))
|
||||
if no_rule:
|
||||
warnings.append("박스 입수량 미설정: " + ", ".join(sorted(no_rule)))
|
||||
warnings.append("상자 입수량 미설정: " + ", ".join(sorted(no_rule)))
|
||||
|
||||
uniq_dates = sorted(set(ship_dates))
|
||||
|
||||
@@ -927,7 +927,7 @@ async def box_calc_api(
|
||||
payload: dict[str, Any] = Body(...),
|
||||
user: dict[str, Any] = Depends(_require_user),
|
||||
) -> JSONResponse:
|
||||
"""[{product_code, quantity}] → 제품별 박스 계산 + 박스명별 합계.
|
||||
"""[{product_code, quantity}] → 제품별 상자 계산 + 상자명별 합계.
|
||||
|
||||
클라이언트 계산을 신뢰하지 않고 store.compute_boxes 로 서버에서 계산한다.
|
||||
"""
|
||||
@@ -943,9 +943,9 @@ async def box_calc_api(
|
||||
|
||||
|
||||
def _compute_boxes(store: Any, raw_items: list[Any]) -> dict[str, Any]:
|
||||
"""[{product_code, quantity}] → 제품별 박스 + 자투리 혼합 박스 + 합계.
|
||||
"""[{product_code, quantity}] → 제품별 상자 + 자투리 혼합 상자 + 합계.
|
||||
|
||||
박스 계산 화면(API)과 출고 상세 보기가 같은 결과를 쓰도록 한 곳에 둔다.
|
||||
상자 계산 화면(API)과 출고 상세 보기가 같은 결과를 쓰도록 한 곳에 둔다.
|
||||
"""
|
||||
from .store import compute_boxes # noqa: WPS433
|
||||
|
||||
@@ -1003,12 +1003,12 @@ def _compute_boxes(store: Any, raw_items: list[Any]) -> dict[str, Any]:
|
||||
|
||||
|
||||
def _pack_leftovers(results: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
"""제품별 자투리(remainder_units)를 같은 박스명끼리 모아 혼합 박스에 담는다.
|
||||
"""제품별 자투리(remainder_units)를 같은 상자명끼리 모아 혼합 상자에 담는다.
|
||||
|
||||
- 한 박스의 용량을 1 로 두고, 제품 1개가 차지하는 부피를 1/units_per_box 로 본다.
|
||||
(예: 쿠팡 2호 = 8개들이 → 1개 = 1/8 박스)
|
||||
- 같은 박스명끼리만 섞는다. 서로 다른 입수량이 섞여도 부피 합으로 정확히 계산된다.
|
||||
- 한 제품의 자투리가 두 박스에 나뉘어 담기는 것은 허용(그래야 박스 수가 최소).
|
||||
- 한 상자의 용량을 1 로 두고, 제품 1개가 차지하는 부피를 1/units_per_box 로 본다.
|
||||
(예: 쿠팡 2호 = 8개들이 → 1개 = 1/8 상자)
|
||||
- 같은 상자명끼리만 섞는다. 서로 다른 입수량이 섞여도 부피 합으로 정확히 계산된다.
|
||||
- 한 제품의 자투리가 두 상자에 나뉘어 담기는 것은 허용(그래야 상자 수가 최소).
|
||||
- 오차 없이 계산하려고 float 대신 Fraction 을 쓴다.
|
||||
"""
|
||||
groups: dict[str, list[dict[str, Any]]] = {}
|
||||
@@ -1019,7 +1019,7 @@ def _pack_leftovers(results: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
|
||||
out: list[dict[str, Any]] = []
|
||||
for box_name in sorted(groups):
|
||||
# 자투리가 많은 제품부터 담아 박스 안 품목 수를 줄인다.
|
||||
# 자투리가 많은 제품부터 담아 상자 안 품목 수를 줄인다.
|
||||
items = sorted(groups[box_name], key=lambda r: -r["remainder_units"])
|
||||
boxes: list[dict[str, Any]] = []
|
||||
cur: list[dict[str, Any]] = []
|
||||
@@ -1030,7 +1030,7 @@ def _pack_leftovers(results: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
left = int(r["remainder_units"])
|
||||
while left > 0:
|
||||
take = min(left, int(free / unit))
|
||||
if take == 0: # 남은 자리 없음 → 새 박스
|
||||
if take == 0: # 남은 자리 없음 → 새 상자
|
||||
boxes.append({"items": cur, "fill_percent": float(round((1 - free) * 100, 1))})
|
||||
cur, free = [], Fraction(1)
|
||||
continue
|
||||
@@ -1245,9 +1245,9 @@ async def product_delete(
|
||||
# 분배 확정 — 센터별 출고 묶음 생성
|
||||
# ════════════════════════════════════════════════════════════
|
||||
def _clean_box_plan(raw: Any) -> list[dict[str, Any]]:
|
||||
"""화면이 보낸 박스 구성을 표시용으로만 정리해서 저장한다.
|
||||
"""화면이 보낸 상자 구성을 표시용으로만 정리해서 저장한다.
|
||||
|
||||
수량·박스 수는 라인(제품 합계)이 정답이고, 이 값은 "어느 상자에 무엇이
|
||||
수량·상자 수는 라인(제품 합계)이 정답이고, 이 값은 "어느 상자에 무엇이
|
||||
담겼는지"를 보여주기 위한 스냅샷이다.
|
||||
"""
|
||||
if not isinstance(raw, list):
|
||||
@@ -1305,7 +1305,7 @@ async def box_calc_confirm(
|
||||
) -> JSONResponse:
|
||||
"""{ship_date, centers:[{center_id, ship_method, boxes, items:[...]}]} → 센터마다 출고 묶음 1건 생성.
|
||||
|
||||
화면에서 보낸 박스 수는 요약 표시용이고, 라인의 박스 수는 저장 시
|
||||
화면에서 보낸 상자 수는 요약 표시용이고, 라인의 상자 수는 저장 시
|
||||
서버(store.compute_boxes)가 수량과 입수량으로 다시 계산한다.
|
||||
"""
|
||||
from .store import SHIP_METHODS as _METHODS # noqa: WPS433
|
||||
@@ -1345,7 +1345,7 @@ async def box_calc_confirm(
|
||||
status_code=400, detail=f"{center['name']} 의 출고방식을 선택하세요."
|
||||
)
|
||||
|
||||
# 같은 제품이 여러 박스로 나뉘어 담겼을 수 있으므로 제품코드로 합친다.
|
||||
# 같은 제품이 여러 상자로 나뉘어 담겼을 수 있으므로 제품코드로 합친다.
|
||||
merged: dict[str, int] = {}
|
||||
for it in raw.get("items") or []:
|
||||
if not isinstance(it, dict):
|
||||
@@ -1382,10 +1382,10 @@ async def box_calc_confirm(
|
||||
except (TypeError, ValueError):
|
||||
boxes = 0
|
||||
pieces = sum(merged.values())
|
||||
# 화면에서 고른 상자 종류별 박스 수(예: "쿠팡상자41"). 출고리스트 엑셀의 "출고" 칸.
|
||||
# 화면에서 고른 상자 종류별 상자 수(예: "쿠팡상자41"). 출고리스트 엑셀의 "출고" 칸.
|
||||
summary = str(raw.get("box_summary") or "").strip()
|
||||
if not summary:
|
||||
summary = f"{boxes}박스 · {pieces}개" if boxes else f"{pieces}개"
|
||||
summary = f"{boxes}상자 · {pieces}개" if boxes else f"{pieces}개"
|
||||
|
||||
plans.append(
|
||||
{
|
||||
@@ -1417,7 +1417,7 @@ async def box_calc_confirm(
|
||||
"outbound_summary": plan["summary"],
|
||||
"worker": worker,
|
||||
"status": "출고준비",
|
||||
"memo": "박스 계산에서 분배 확정",
|
||||
"memo": "상자 계산에서 분배 확정",
|
||||
"box_plan": plan["box_plan"],
|
||||
},
|
||||
lines=plan["lines"],
|
||||
@@ -1438,7 +1438,7 @@ async def box_calc_confirm(
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════════
|
||||
# 박스 계산 임시 저장 (화면 상태 스냅샷)
|
||||
# 상자 계산 임시 저장 (화면 상태 스냅샷)
|
||||
# ════════════════════════════════════════════════════════════
|
||||
@router.get("/api/box-calc/drafts")
|
||||
async def box_calc_drafts_list(
|
||||
|
||||
Reference in New Issue
Block a user