feat(malaysia): 재고조사 랙(rack) 박스 단위 입력
낱개/콤보 평면 수량 그리드를 창고 랙 그림(A/B 36칸) 입력으로 대체.
각 칸에 아이템·박스당 입수량·박스 수를 넣고 + 로 한 칸 여러 아이템 추가.
저장 시 SKU별 qty=SUM(입수량×박스수) 집계로 daily_stocktake_line 재생성.
이후 분해/확정 로직은 라인 기준 그대로 동작.
- DB: daily_stocktake_rack 테이블 + grant (멱등)
- store: rack_cell_codes/rack_layout/aggregate_rack 순수함수
- db: list_rack_entries/replace_rack(1 트랜잭션, draft만)
- router: rack 컨텍스트 + POST /stocktakes/{id}/rack/bulk
- UI: 랙 보드 템플릿 + malaysia_rack.js(행 추가/삭제·실시간 소계)
- 테스트 4건 추가(18 전부 통과)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -27,6 +27,66 @@ INDIVIDUAL_PREFIXES: tuple[str, ...] = ("MT-", "MX-", "MZ-")
|
||||
SET_PREFIX: str = "MY-"
|
||||
BLOCKED_PREFIX: str = "MD-" # 뚜껑 — 재고관리 대상에서 제외
|
||||
|
||||
# ── 창고 랙 레이아웃 (재고조사 랙 입력용) ──
|
||||
# 그림 고정: A/B 두 면, 각 면 3행 × 3열, 각 열은 2분할 → 면당 18칸, 총 36칸.
|
||||
# 행은 위→아래로 3,2,1 (그림과 동일). 칸 코드: '{면}{행}-{열}-{분할}' 예) A3-1-1.
|
||||
RACK_SIDES: tuple[str, ...] = ("A", "B")
|
||||
RACK_ROWS: tuple[int, ...] = (3, 2, 1)
|
||||
RACK_COLS: tuple[int, ...] = (1, 2, 3)
|
||||
RACK_SUBS: tuple[int, ...] = (1, 2)
|
||||
|
||||
|
||||
def rack_cell_codes() -> list[str]:
|
||||
"""모든 랙 칸 코드 목록(검증용). 예: 'A3-1-1' ... 'B1-3-2'."""
|
||||
return [
|
||||
f"{side}{row}-{col}-{sub}"
|
||||
for side in RACK_SIDES
|
||||
for row in RACK_ROWS
|
||||
for col in RACK_COLS
|
||||
for sub in RACK_SUBS
|
||||
]
|
||||
|
||||
|
||||
def rack_layout() -> list[dict[str, Any]]:
|
||||
"""템플릿 렌더용 중첩 구조.
|
||||
|
||||
[{"side":"A","rows":[{"row":3,"cols":[["A3-1-1","A3-1-2"], ...]}, ...]}, ...]
|
||||
"""
|
||||
out: list[dict[str, Any]] = []
|
||||
for side in RACK_SIDES:
|
||||
rows: list[dict[str, Any]] = []
|
||||
for row in RACK_ROWS:
|
||||
cols = [
|
||||
[f"{side}{row}-{col}-{sub}" for sub in RACK_SUBS]
|
||||
for col in RACK_COLS
|
||||
]
|
||||
rows.append({"row": row, "cols": cols})
|
||||
out.append({"side": side, "rows": rows})
|
||||
return out
|
||||
|
||||
|
||||
def aggregate_rack(entries: Iterable[dict[str, Any]]) -> dict[str, int]:
|
||||
"""랙 입력 항목을 SKU 코드별 총수량으로 집계.
|
||||
|
||||
각 항목: {"sku_code", "units_per_box", "box_count"}.
|
||||
qty = SUM(units_per_box * box_count). 입수량/박스수 ≤ 0 또는 코드 공백은 스킵.
|
||||
반환: { sku_code: total_qty } (낱개·세트 코드 모두 포함 가능)
|
||||
"""
|
||||
totals: dict[str, int] = {}
|
||||
for e in entries:
|
||||
code = _norm(e.get("sku_code"))
|
||||
if not code:
|
||||
continue
|
||||
try:
|
||||
upb = int(e.get("units_per_box") or 0)
|
||||
box = int(e.get("box_count") or 0)
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
if upb <= 0 or box <= 0:
|
||||
continue
|
||||
totals[code] = totals.get(code, 0) + upb * box
|
||||
return totals
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════════
|
||||
# 코드 분류 / 검증 (순수 함수)
|
||||
|
||||
Reference in New Issue
Block a user