feat(malaysia): 재고현황 조사수량·차이 표기, 확정 관리자 제한, 슈퍼관리자 조사삭제

- 재고현황 헤더 "전산 재고 현황", Last Stocktake 옆 조사수량+차이(±) 표기
  (차이 = 마지막 재고조사 낱개 total − 전산재고)
- 재고조사 확정(전산 반영)은 관리자(is_admin)만 가능, 비관리자 안내
- 슈퍼관리자만 재고조사 완전 삭제(목록/상세 버튼 + /delete 라우트)
- "◀◀ 조사 목록" 버튼 검정 바탕(primary)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-11 16:38:38 +09:00
parent 3d85ccf43e
commit c07d3301e4
5 changed files with 102 additions and 16 deletions
+38 -1
View File
@@ -124,6 +124,7 @@ def _base_ctx(request: Request, user: dict[str, Any], st: Any, *, wh: str) -> di
return {
"user": user,
"is_admin": is_admin(user),
"is_super": bool(user.get("is_super_admin")),
"nav_items": build_erp_nav(user, active="malaysia"),
"warehouses": st.list_warehouses(),
"selected_wh": wh,
@@ -143,12 +144,26 @@ async def index(request: Request) -> HTMLResponse:
st, user = guard
wh = _selected_wh(request, st)
ctx = _base_ctx(request, user, st, wh=wh)
rows = st.stock_status(warehouse_code=wh)
# 마지막 재고조사(취소 제외) 의 낱개 기준 total 과 전산재고 차이
latest = st.latest_stocktake(warehouse_code=wh)
last_date = latest["stocktake_date"] if latest else None
last_map: dict[str, int] = {}
if latest:
comp = st.compute_result(stocktake_id=latest["id"], bom_map=_bom_map(request))
last_map = {r["item_code"]: r["total_qty"] for r in comp["rows"]}
for r in rows:
code = r["item_code"]
lq = last_map.get(code)
r["last_stocktake_date"] = last_date
r["last_qty"] = lq
r["diff"] = (lq - r["current_qty"]) if lq is not None else None
set_stock = st.latest_set_quantities(warehouse_code=wh)
ctx.update(
{
"page_title": "말레이시아 재고관리",
"page_subtitle": f"{wh} · 재고 현황",
"rows": st.stock_status(warehouse_code=wh),
"rows": rows,
"set_rows": set_stock["rows"],
"set_stocktake_date": set_stock["stocktake_date"],
}
@@ -410,6 +425,10 @@ async def stocktake_line_delete(
async def stocktake_finalize(
request: Request, stocktake_id: int, user: dict[str, Any] = Depends(_require_user)
) -> RedirectResponse:
from app.store import is_admin # noqa: WPS433
if not is_admin(user):
raise HTTPException(status_code=403, detail="전산 재고 반영은 관리자만 가능합니다.")
st = _store(request)
if st is None:
raise HTTPException(status_code=503, detail="malaysia_stock_db 미설정")
@@ -436,6 +455,24 @@ async def stocktake_cancel(
return RedirectResponse(url=f"/malaysia/stocktakes/{stocktake_id}", status_code=303)
@router.post("/stocktakes/{stocktake_id:int}/delete")
async def stocktake_delete(
request: Request, stocktake_id: int, user: dict[str, Any] = Depends(_require_user)
) -> RedirectResponse:
"""재고조사 완전 삭제 — 슈퍼관리자 전용."""
if not user.get("is_super_admin"):
raise HTTPException(status_code=403, detail="재고조사 삭제는 슈퍼관리자만 가능합니다.")
st = _store(request)
if st is None:
raise HTTPException(status_code=503, detail="malaysia_stock_db 미설정")
wh = _selected_wh(request, st)
try:
st.delete_stocktake(stocktake_id=stocktake_id)
except KeyError:
raise HTTPException(status_code=404, detail="재고조사를 찾을 수 없습니다.")
return RedirectResponse(url=f"/malaysia/stocktakes?wh={wh}", status_code=303)
# ════════════════════════════════════════════════════════════
# JSON API
# ════════════════════════════════════════════════════════════