feat(dispatch): 말레이시아 재고 차감 버튼(날짜 1회성)

- 달력 다운로드 아래 '말레이시아 재고 차감' 버튼. 선택 날짜 출고를
  말레이시아 재고관리에 OUT 으로 반영(movement_date=해당 날짜)
  · 낱개(MT/MX/MZ)=낱개 OUT, 콤보(MY-)=세트 OUT(재고관리가 BOM 분해)
  · _N 배수 반영, MD-(뚜껑) 제외
- 날짜당 1회만: dispatch_stock_deductions 마커로 재차감 차단(이중 출고 방지)
  · 마커 선점 후 반영, 콤보 BOM 누락은 사전검증으로 차단
- export.split_singles_combos, db 차감 가드 메서드, 라우터 POST /dispatch/stock-deduct
- 스키마: dispatch_stock_deductions (init + 마이그레이션 dispatch_add_deductions.sql)
- 이미 차감된 날짜는 버튼 비활성 + 안내

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-22 18:07:37 +09:00
parent 27e2c17606
commit b583627bd3
6 changed files with 259 additions and 3 deletions
+26
View File
@@ -139,6 +139,32 @@ def build_workbook_bytes(
# ── 사방넷 재고 업로드 엑셀(낱개 분해) ──
_STOCK_HEADERS: tuple[str, ...] = ("상품코드[필수]", "가용수량", "불용수량", "바코드")
_LID_PREFIX = "MD-" # 뚜껑 — 재고 집계 제외
_SET_PREFIX = "MY-" # 콤보(세트)
def split_singles_combos(
rows: list[dict[str, Any]],
) -> tuple[dict[str, int], dict[str, int]]:
"""SKU별 합산 행 → (낱개 {code: qty}, 콤보 {code: qty}).
_N 배수 반영, MD-(뚜껑) 제외. 콤보는 MY- 접두사. 분해는 하지 않는다
(말레이시아 재고관리가 세트 OUT 시 BOM 으로 분해하므로 콤보 그대로 넘긴다).
"""
singles: dict[str, int] = {}
combos: dict[str, int] = {}
for r in rows:
base, mult = split_sku(r.get("seller_sku", ""))
base = base.upper()
if not base:
continue
pieces = int(r.get("qty", 0)) * mult
if pieces <= 0 or base.startswith(_LID_PREFIX):
continue
if base.startswith(_SET_PREFIX):
combos[base] = combos.get(base, 0) + pieces
else:
singles[base] = singles.get(base, 0) + pieces
return singles, combos
def explode_to_singles(