feat(malaysia): 뚜껑(MD-) 랙 위치확인 배치 허용, 재고는 미반영
뚜껑(MD-0000~0005)을 재고조사 랙 입력 드롭다운에 노출해 창고 랙 어디에 있는지 위치만 기록할 수 있게 함. 재고에는 반영하지 않음. - store.LID_ITEMS / lid_name_map 추가 - validate_rack_code 신설(개별/세트/뚜껑 허용) → replace_rack· reposition_rack 검증에 적용. 직접 라인 입력(validate_stocktake_code)은 여전히 MD- 차단해 재고 오염 방지. - aggregate_rack 에서 MD- 집계 제외 → daily_stocktake_line 미생성. - 랙 드롭다운 목록에 LID_ITEMS 추가, 랙 보기 이름맵에 뚜껑 이름 병합. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -33,7 +33,7 @@ Claude Code는 이 저장소에서 작업을 시작하기 전에 **반드시 아
|
|||||||
- 개인경비 (`app/modules/expense/`, `expense_db`)
|
- 개인경비 (`app/modules/expense/`, `expense_db`)
|
||||||
- 쿠팡 밀크런 (`app/modules/cupang/`, `cupang_db`) — 출고 달력/박스 입수량 계산/입고센터 관리, 상품은 `itemcode_db` 읽기 전용
|
- 쿠팡 밀크런 (`app/modules/cupang/`, `cupang_db`) — 출고 달력/박스 입수량 계산/입고센터 관리, 상품은 `itemcode_db` 읽기 전용
|
||||||
- 휴가 관리 (`app/modules/vacation/`, `vacation_db`) — 월간 달력(구글식 bar)/연차·반차 신청/승인 워크플로/공휴일·연차 설정. 권한키 `vacation`·`vacation_approver`
|
- 휴가 관리 (`app/modules/vacation/`, `vacation_db`) — 월간 달력(구글식 bar)/연차·반차 신청/승인 워크플로/공휴일·연차 설정. 권한키 `vacation`·`vacation_approver`
|
||||||
- 말레이시아 창고 재고관리 (`app/modules/malaysia/`, `malaysia_stock_db`) — 낱개(MT/MX/MZ) 입출고·조정, 세트(MY) BOM, 일일 재고조사(세트→낱개 자동 분해), 현재고 현황. 뚜껑(MD-) 제외. 상품은 `itemcode_db` 읽기 전용. 권한키 `malaysia`
|
- 말레이시아 창고 재고관리 (`app/modules/malaysia/`, `malaysia_stock_db`) — 낱개(MT/MX/MZ) 입출고·조정, 세트(MY) BOM, 일일 재고조사(세트→낱개 자동 분해), 현재고 현황. 뚜껑(MD-)은 재고 집계 제외 — 단, 창고 랙에는 위치 확인용으로 배치 가능(`store.LID_ITEMS`). 상품은 `itemcode_db` 읽기 전용. 권한키 `malaysia`
|
||||||
|
|
||||||
상세는 `docs/PROJECT_OVERVIEW.md`.
|
상세는 `docs/PROJECT_OVERVIEW.md`.
|
||||||
|
|
||||||
|
|||||||
@@ -476,7 +476,7 @@ class MalaysiaStockStore:
|
|||||||
cell = (e.get("cell_code") or "").strip().upper()
|
cell = (e.get("cell_code") or "").strip().upper()
|
||||||
if cell not in valid_cells:
|
if cell not in valid_cells:
|
||||||
raise ValueError(f"알 수 없는 랙 위치: {cell or '(빈값)'}")
|
raise ValueError(f"알 수 없는 랙 위치: {cell or '(빈값)'}")
|
||||||
code = store.validate_stocktake_code(str(e.get("sku_code") or ""))
|
code = store.validate_rack_code(str(e.get("sku_code") or ""))
|
||||||
upb = store.validate_qty(
|
upb = store.validate_qty(
|
||||||
e.get("units_per_box"), allow_zero=False, allow_negative=False
|
e.get("units_per_box"), allow_zero=False, allow_negative=False
|
||||||
)
|
)
|
||||||
@@ -754,7 +754,7 @@ class MalaysiaStockStore:
|
|||||||
cell = (e.get("cell_code") or "").strip().upper()
|
cell = (e.get("cell_code") or "").strip().upper()
|
||||||
if cell not in valid_cells:
|
if cell not in valid_cells:
|
||||||
raise ValueError(f"알 수 없는 랙 위치: {cell or '(빈값)'}")
|
raise ValueError(f"알 수 없는 랙 위치: {cell or '(빈값)'}")
|
||||||
code = store.validate_stocktake_code(str(e.get("sku_code") or ""))
|
code = store.validate_rack_code(str(e.get("sku_code") or ""))
|
||||||
upb = store.validate_qty(
|
upb = store.validate_qty(
|
||||||
e.get("units_per_box"), allow_zero=False, allow_negative=False
|
e.get("units_per_box"), allow_zero=False, allow_negative=False
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -190,7 +190,8 @@ async def rack_view(request: Request) -> HTMLResponse:
|
|||||||
# 랙보기는 '랙을 가장 최근에 저장한' 조사 기준(조사 날짜 아님).
|
# 랙보기는 '랙을 가장 최근에 저장한' 조사 기준(조사 날짜 아님).
|
||||||
latest = st.latest_rack_stocktake(warehouse_code=wh)
|
latest = st.latest_rack_stocktake(warehouse_code=wh)
|
||||||
rack_by_cell: dict[str, list[dict[str, Any]]] = {}
|
rack_by_cell: dict[str, list[dict[str, Any]]] = {}
|
||||||
names = st.item_name_map()
|
# 뚜껑(MD-)은 malaysia_items 에 없으므로 이름맵에 병합해 코드 대신 이름 표시.
|
||||||
|
names = {**st.item_name_map(), **store.lid_name_map()}
|
||||||
if latest:
|
if latest:
|
||||||
for e in st.list_rack_entries(stocktake_id=latest["id"]):
|
for e in st.list_rack_entries(stocktake_id=latest["id"]):
|
||||||
e["item_name"] = names.get(e["sku_code"], e["sku_code"])
|
e["item_name"] = names.get(e["sku_code"], e["sku_code"])
|
||||||
@@ -547,7 +548,8 @@ async def stocktake_detail(request: Request, stocktake_id: int) -> HTMLResponse:
|
|||||||
"stocktake": head,
|
"stocktake": head,
|
||||||
"rack_layout": store.rack_layout(),
|
"rack_layout": store.rack_layout(),
|
||||||
"rack_by_cell": rack_by_cell,
|
"rack_by_cell": rack_by_cell,
|
||||||
"rack_items": st.list_items(),
|
# 낱개·세트 + 뚜껑(MD-, 위치확인용). 뚜껑은 재고 집계엔 빠진다.
|
||||||
|
"rack_items": st.list_items() + list(store.LID_ITEMS),
|
||||||
"entered": entered,
|
"entered": entered,
|
||||||
"result_rows": computed["rows"],
|
"result_rows": computed["rows"],
|
||||||
"missing_bom": computed["missing_bom"],
|
"missing_bom": computed["missing_bom"],
|
||||||
|
|||||||
@@ -25,7 +25,22 @@ STOCKTAKE_STATUSES: tuple[str, ...] = ("draft", "finalized", "cancelled")
|
|||||||
# ── 코드 prefix ──
|
# ── 코드 prefix ──
|
||||||
INDIVIDUAL_PREFIXES: tuple[str, ...] = ("MT-", "MX-", "MZ-")
|
INDIVIDUAL_PREFIXES: tuple[str, ...] = ("MT-", "MX-", "MZ-")
|
||||||
SET_PREFIX: str = "MY-"
|
SET_PREFIX: str = "MY-"
|
||||||
BLOCKED_PREFIX: str = "MD-" # 뚜껑 — 재고관리 대상에서 제외
|
BLOCKED_PREFIX: str = "MD-" # 뚜껑 — 재고(입출고/조사 집계) 대상에서 제외
|
||||||
|
|
||||||
|
# 뚜껑(MD-) 아이템 목록 — 재고에는 반영하지 않고 '창고 랙 위치 확인' 용도로만
|
||||||
|
# 랙 칸에 배치할 수 있다(드롭다운 노출). malaysia_items 테이블엔 넣지 않는다.
|
||||||
|
LID_ITEMS: tuple[dict[str, str], ...] = (
|
||||||
|
{"item_code": "MD-0000", "item_name": "Lid XS", "kind": "lid"},
|
||||||
|
{"item_code": "MD-0001", "item_name": "Lid S", "kind": "lid"},
|
||||||
|
{"item_code": "MD-0002", "item_name": "Lid M", "kind": "lid"},
|
||||||
|
{"item_code": "MD-0003", "item_name": "Lid L", "kind": "lid"},
|
||||||
|
{"item_code": "MD-0005", "item_name": "Lid 5ℓ", "kind": "lid"},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def lid_name_map() -> dict[str, str]:
|
||||||
|
"""뚜껑 코드 → 이름(랙 보기 표시용)."""
|
||||||
|
return {it["item_code"]: it["item_name"] for it in LID_ITEMS}
|
||||||
|
|
||||||
# ── 창고 랙 레이아웃 (재고조사 랙 입력용) ──
|
# ── 창고 랙 레이아웃 (재고조사 랙 입력용) ──
|
||||||
# 그림 고정: A/B 두 면, 각 면 3행 × 3열, 각 열은 2분할 → 면당 18칸, 총 36칸.
|
# 그림 고정: A/B 두 면, 각 면 3행 × 3열, 각 열은 2분할 → 면당 18칸, 총 36칸.
|
||||||
@@ -87,6 +102,9 @@ def aggregate_rack(entries: Iterable[dict[str, Any]]) -> dict[str, int]:
|
|||||||
code = _norm(e.get("sku_code"))
|
code = _norm(e.get("sku_code"))
|
||||||
if not code:
|
if not code:
|
||||||
continue
|
continue
|
||||||
|
# 뚜껑(MD-)은 위치 확인용일 뿐 재고에 반영하지 않으므로 집계 제외.
|
||||||
|
if is_blocked_code(code):
|
||||||
|
continue
|
||||||
try:
|
try:
|
||||||
upb = int(e.get("units_per_box") or 0)
|
upb = int(e.get("units_per_box") or 0)
|
||||||
box = int(e.get("box_count") or 0)
|
box = int(e.get("box_count") or 0)
|
||||||
@@ -166,6 +184,22 @@ def validate_stocktake_code(code: str) -> str:
|
|||||||
return c
|
return c
|
||||||
|
|
||||||
|
|
||||||
|
def validate_rack_code(code: str) -> str:
|
||||||
|
"""창고 랙 칸에 배치할 수 있는 코드 검증.
|
||||||
|
|
||||||
|
낱개(MT-/MX-/MZ-)·세트(MY-)에 더해 뚜껑(MD-)도 허용한다.
|
||||||
|
뚜껑은 위치 확인 용도로만 랙에 두며, aggregate_rack 에서 집계 제외되어
|
||||||
|
재고(daily_stocktake_line)에는 반영되지 않는다.
|
||||||
|
"""
|
||||||
|
c = _norm(code)
|
||||||
|
if not c:
|
||||||
|
raise ValueError("SKU 코드가 비어 있습니다.")
|
||||||
|
kind = classify_code(c)
|
||||||
|
if kind not in ("individual", "set", "blocked"):
|
||||||
|
raise ValueError(f"허용되지 않는 코드입니다(MT-/MX-/MZ-/MY-/MD- 만 가능): {c}")
|
||||||
|
return c
|
||||||
|
|
||||||
|
|
||||||
def validate_bom_set_code(code: str) -> str:
|
def validate_bom_set_code(code: str) -> str:
|
||||||
"""set_bom 의 set_code 검증 — MY- 만 허용."""
|
"""set_bom 의 set_code 검증 — MY- 만 허용."""
|
||||||
c = _norm(code)
|
c = _norm(code)
|
||||||
|
|||||||
Reference in New Issue
Block a user