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
+49
View File
@@ -309,6 +309,55 @@ class DispatchStore:
for r in rows
]
# ════════════════════════════════════════════════════════════
# 말레이시아 재고 차감 1회성 가드
# ════════════════════════════════════════════════════════════
def deduction_exists(self, *, dispatch_date: str) -> bool:
with self._pool.connection() as conn:
row = conn.execute(
"SELECT 1 FROM dispatch_stock_deductions WHERE dispatch_date = %s",
(dispatch_date,),
).fetchone()
return row is not None
def list_deducted_dates(self) -> list[str]:
with self._pool.connection() as conn:
rows = conn.execute(
"SELECT dispatch_date FROM dispatch_stock_deductions "
"ORDER BY dispatch_date DESC"
).fetchall()
out: list[str] = []
for r in rows:
d = r["dispatch_date"]
out.append(d.isoformat() if isinstance(d, date) else str(d))
return out
def record_deduction(
self,
*,
dispatch_date: str,
warehouse_code: str,
single_lines: int,
combo_lines: int,
worker_name: str = "",
) -> None:
"""차감 완료 기록. 이미 있으면 충돌(재차감 차단)."""
with self._pool.connection() as conn:
conn.execute(
"""
INSERT INTO dispatch_stock_deductions
(dispatch_date, warehouse_code, single_lines, combo_lines, worker_name)
VALUES (%s,%s,%s,%s,%s)
""",
(
dispatch_date,
(warehouse_code or "").strip(),
int(single_lines),
int(combo_lines),
(worker_name or "").lower().strip(),
),
)
def parcel_batch_id(self, *, parcel_id: int) -> int | None:
with self._pool.connection() as conn:
row = conn.execute(