feat(malaysia): 세트 BOM을 itemcode_db에서 읽고 입력을 그리드 일괄로 전환
- 세트구성(set_bom) 수동 관리 메뉴/라우트/API/템플릿 제거 - 세트 BOM은 itemcode_db set_components(set_code/single_code/quantity)에서 읽음 (MalaysiaItemcodeReader, ITEMCODE_DB_URL 재사용) - db.py: create_set_out/compute_result/finalize_stocktake 에 bom_map 주입식 - 입출고: 낱개 전체 그리드 일괄 등록 + 세트 출고 그리드(BOM 분해) - 일일 재고조사: 낱개+세트 전체 그리드에 수량만 키인 → 일괄 저장 - malaysia_stock_db.set_bom 테이블은 레거시(미사용) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+140
-216
@@ -4,9 +4,9 @@
|
||||
- 권한: 로그인 + `malaysia` 모듈 권한 (관리자는 항상 통과). 서버 측 검사.
|
||||
- 데이터: MalaysiaStockStore (malaysia_stock_db / PostgreSQL) 전용.
|
||||
MALAYSIA_STOCK_DB_URL 미설정 시 store 가 None 이며, 각 페이지는 "설정 필요" 안내.
|
||||
- 상품명: 전역 itemcode_reader(itemcode_db 읽기 전용). 미설정 시 등록 스냅샷 사용.
|
||||
- 세트 BOM: itemcode_db(set_components) 읽기 전용(MalaysiaItemcodeReader). 별도 관리 메뉴 없음.
|
||||
|
||||
화면(HTML) + JSON API 를 함께 제공한다. JSON API 는 /malaysia/api/* 아래.
|
||||
입력 방식: 입출고/재고조사 모두 전체 아이템(+세트) 그리드에 수량만 키인 → 일괄 등록.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -30,8 +30,14 @@ def _store(request: Request) -> Any:
|
||||
return getattr(request.app.state, "malaysia_store", None)
|
||||
|
||||
|
||||
def _itemcode(request: Request) -> Any:
|
||||
return getattr(request.app.state, "itemcode_reader", None)
|
||||
def _reader(request: Request) -> Any:
|
||||
return getattr(request.app.state, "malaysia_itemcode", None)
|
||||
|
||||
|
||||
def _bom_map(request: Request) -> dict[str, list[dict[str, Any]]]:
|
||||
"""itemcode_db(set_components)에서 MY- 세트 BOM 을 읽어 반환. 리더 없으면 {}."""
|
||||
r = _reader(request)
|
||||
return r.set_bom_map() if r else {}
|
||||
|
||||
|
||||
def _require_user(request: Request) -> dict[str, Any]:
|
||||
@@ -90,8 +96,7 @@ def _guard(request: Request) -> tuple[Any, dict[str, Any]] | HTMLResponse | Redi
|
||||
def _selected_wh(request: Request, st: Any) -> str:
|
||||
"""쿼리 ?wh= 또는 기본(첫 활성 창고)."""
|
||||
wh = (request.query_params.get("wh") or "").strip()
|
||||
warehouses = st.list_warehouses()
|
||||
codes = [w["warehouse_code"] for w in warehouses]
|
||||
codes = [w["warehouse_code"] for w in st.list_warehouses()]
|
||||
if wh in codes:
|
||||
return wh
|
||||
return codes[0] if codes else "MY-WH-01"
|
||||
@@ -134,7 +139,7 @@ async def index(request: Request) -> HTMLResponse:
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════════
|
||||
# 입고 / 출고 등록
|
||||
# 입고 / 출고 등록 (그리드 일괄)
|
||||
# ════════════════════════════════════════════════════════════
|
||||
@router.get("/movements", response_class=HTMLResponse)
|
||||
async def movements_page(request: Request) -> HTMLResponse:
|
||||
@@ -150,7 +155,7 @@ async def movements_page(request: Request) -> HTMLResponse:
|
||||
ctx.update(
|
||||
{
|
||||
"page_title": "말레이시아 재고관리 — 입출고",
|
||||
"page_subtitle": f"{wh} · 입고/출고 등록",
|
||||
"page_subtitle": f"{wh} · 입고/출고/조정 일괄 등록",
|
||||
"individual_items": st.list_items(kind="individual"),
|
||||
"set_items": st.list_items(kind="set"),
|
||||
"movements": st.list_movements(
|
||||
@@ -165,138 +170,94 @@ async def movements_page(request: Request) -> HTMLResponse:
|
||||
return render_template(request, "malaysia/movements.html", ctx)
|
||||
|
||||
|
||||
@router.post("/movements")
|
||||
async def movement_create(
|
||||
request: Request,
|
||||
movement_date: str = Form(...),
|
||||
warehouse_code: str = Form(...),
|
||||
item_code: str = Form(...),
|
||||
movement_type: str = Form(...),
|
||||
qty: int = Form(...),
|
||||
ref_type: str = Form(""),
|
||||
ref_no: str = Form(""),
|
||||
memo: str = Form(""),
|
||||
user: dict[str, Any] = Depends(_require_user),
|
||||
) -> RedirectResponse:
|
||||
@router.post("/movements/bulk")
|
||||
async def movements_bulk(request: Request, user: dict[str, Any] = Depends(_require_user)) -> RedirectResponse:
|
||||
"""낱개 아이템 그리드 일괄 등록. form: movement_type, movement_date,
|
||||
warehouse_code, ref_type, ref_no, memo, qty_<ITEMCODE>...
|
||||
|
||||
IN/OUT 은 양수만 등록(0/빈칸 스킵), ADJUST 는 0 아닌 값만 등록(±).
|
||||
"""
|
||||
st = _store(request)
|
||||
if st is None:
|
||||
raise HTTPException(status_code=503, detail="malaysia_stock_db 미설정")
|
||||
try:
|
||||
st.create_movement(
|
||||
movement_date=movement_date,
|
||||
warehouse_code=warehouse_code,
|
||||
item_code=item_code,
|
||||
movement_type=movement_type,
|
||||
qty=qty,
|
||||
ref_type=ref_type,
|
||||
ref_no=ref_no,
|
||||
memo=memo,
|
||||
created_by=user["email"],
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc))
|
||||
return RedirectResponse(url=f"/malaysia/movements?wh={warehouse_code}", status_code=303)
|
||||
form = await request.form()
|
||||
mtype = str(form.get("movement_type") or "").strip().upper()
|
||||
if mtype not in ("IN", "OUT", "ADJUST"):
|
||||
raise HTTPException(status_code=400, detail="이동 종류 오류")
|
||||
wh = str(form.get("warehouse_code") or "").strip()
|
||||
mdate = str(form.get("movement_date") or "").strip()
|
||||
ref_type = str(form.get("ref_type") or "").strip()
|
||||
ref_no = str(form.get("ref_no") or "").strip()
|
||||
memo = str(form.get("memo") or "").strip()
|
||||
|
||||
created = 0
|
||||
errors: list[str] = []
|
||||
for it in st.list_items(kind="individual"):
|
||||
code = it["item_code"]
|
||||
raw = str(form.get(f"qty_{code}") or "").strip()
|
||||
if raw == "":
|
||||
continue
|
||||
try:
|
||||
n = int(raw)
|
||||
except ValueError:
|
||||
errors.append(f"{code}: 숫자 아님")
|
||||
continue
|
||||
if n == 0:
|
||||
continue
|
||||
try:
|
||||
st.create_movement(
|
||||
movement_date=mdate, warehouse_code=wh, item_code=code,
|
||||
movement_type=mtype, qty=n, ref_type=ref_type, ref_no=ref_no,
|
||||
memo=memo, created_by=user["email"],
|
||||
)
|
||||
created += 1
|
||||
except ValueError as exc:
|
||||
errors.append(f"{code}: {exc}")
|
||||
if errors and created == 0:
|
||||
raise HTTPException(status_code=400, detail="; ".join(errors[:10]))
|
||||
return RedirectResponse(url=f"/malaysia/movements?wh={wh}&type={mtype}", status_code=303)
|
||||
|
||||
|
||||
@router.post("/movements/set-out")
|
||||
async def movement_set_out(
|
||||
request: Request,
|
||||
movement_date: str = Form(...),
|
||||
warehouse_code: str = Form(...),
|
||||
set_code: str = Form(...),
|
||||
set_qty: int = Form(...),
|
||||
ref_type: str = Form("set"),
|
||||
ref_no: str = Form(""),
|
||||
memo: str = Form(""),
|
||||
user: dict[str, Any] = Depends(_require_user),
|
||||
) -> RedirectResponse:
|
||||
"""세트 주문 출고 → BOM 분해 후 구성품별 OUT 생성."""
|
||||
@router.post("/movements/set-out-bulk")
|
||||
async def set_out_bulk(request: Request, user: dict[str, Any] = Depends(_require_user)) -> RedirectResponse:
|
||||
"""세트 그리드 일괄 출고. form: movement_date, warehouse_code, ref_no, memo,
|
||||
qty_<SETCODE>... → 각 세트를 BOM 분해해 구성품별 OUT 생성.
|
||||
"""
|
||||
st = _store(request)
|
||||
if st is None:
|
||||
raise HTTPException(status_code=503, detail="malaysia_stock_db 미설정")
|
||||
try:
|
||||
st.create_set_out(
|
||||
movement_date=movement_date,
|
||||
warehouse_code=warehouse_code,
|
||||
set_code=set_code,
|
||||
set_qty=set_qty,
|
||||
ref_type=ref_type,
|
||||
ref_no=ref_no,
|
||||
memo=memo,
|
||||
created_by=user["email"],
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc))
|
||||
return RedirectResponse(url=f"/malaysia/movements?wh={warehouse_code}", status_code=303)
|
||||
form = await request.form()
|
||||
wh = str(form.get("warehouse_code") or "").strip()
|
||||
mdate = str(form.get("movement_date") or "").strip()
|
||||
ref_no = str(form.get("ref_no") or "").strip()
|
||||
memo = str(form.get("memo") or "").strip()
|
||||
bmap = _bom_map(request)
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════════
|
||||
# 세트 구성 관리 (set_bom)
|
||||
# ════════════════════════════════════════════════════════════
|
||||
@router.get("/sets", response_class=HTMLResponse)
|
||||
async def sets_page(request: Request) -> HTMLResponse:
|
||||
from app.main import render_template # noqa: WPS433
|
||||
|
||||
guard = _guard(request)
|
||||
if not isinstance(guard, tuple):
|
||||
return guard
|
||||
st, user = guard
|
||||
wh = _selected_wh(request, st)
|
||||
ctx = _base_ctx(request, user, st, wh=wh)
|
||||
# 세트별로 묶어서 표시
|
||||
set_items = st.list_items(kind="set")
|
||||
bom = st.list_bom(include_inactive=True)
|
||||
by_set: dict[str, list[dict[str, Any]]] = {}
|
||||
for b in bom:
|
||||
by_set.setdefault(b["set_code"], []).append(b)
|
||||
names = st.item_name_map()
|
||||
ctx.update(
|
||||
{
|
||||
"page_title": "말레이시아 재고관리 — 세트 구성",
|
||||
"page_subtitle": "MY- 세트 BOM 관리 (구성품은 MT-/MX-/MZ-)",
|
||||
"set_items": set_items,
|
||||
"individual_items": st.list_items(kind="individual"),
|
||||
"by_set": by_set,
|
||||
"names": names,
|
||||
}
|
||||
)
|
||||
return render_template(request, "malaysia/sets.html", ctx)
|
||||
|
||||
|
||||
@router.post("/sets/bom")
|
||||
async def bom_upsert(
|
||||
request: Request,
|
||||
set_code: str = Form(...),
|
||||
component_code: str = Form(...),
|
||||
component_qty: int = Form(...),
|
||||
user: dict[str, Any] = Depends(_require_user),
|
||||
) -> RedirectResponse:
|
||||
st = _store(request)
|
||||
if st is None:
|
||||
raise HTTPException(status_code=503, detail="malaysia_stock_db 미설정")
|
||||
try:
|
||||
st.upsert_bom_line(
|
||||
set_code=set_code, component_code=component_code, component_qty=component_qty
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc))
|
||||
return RedirectResponse(url="/malaysia/sets", status_code=303)
|
||||
|
||||
|
||||
@router.post("/sets/bom/{bom_id:int}/delete")
|
||||
async def bom_delete(
|
||||
request: Request,
|
||||
bom_id: int,
|
||||
user: dict[str, Any] = Depends(_require_user),
|
||||
) -> RedirectResponse:
|
||||
st = _store(request)
|
||||
if st is None:
|
||||
raise HTTPException(status_code=503, detail="malaysia_stock_db 미설정")
|
||||
try:
|
||||
st.delete_bom_line(bom_id=bom_id)
|
||||
except KeyError:
|
||||
raise HTTPException(status_code=404, detail="BOM 라인을 찾을 수 없습니다.")
|
||||
return RedirectResponse(url="/malaysia/sets", status_code=303)
|
||||
done = 0
|
||||
errors: list[str] = []
|
||||
for it in st.list_items(kind="set"):
|
||||
code = it["item_code"]
|
||||
raw = str(form.get(f"qty_{code}") or "").strip()
|
||||
if raw == "":
|
||||
continue
|
||||
try:
|
||||
n = int(raw)
|
||||
except ValueError:
|
||||
continue
|
||||
if n <= 0:
|
||||
continue
|
||||
try:
|
||||
st.create_set_out(
|
||||
movement_date=mdate, warehouse_code=wh, set_code=code, set_qty=n,
|
||||
bom_map=bmap, ref_type="set", ref_no=ref_no, memo=memo,
|
||||
created_by=user["email"],
|
||||
)
|
||||
done += 1
|
||||
except ValueError as exc:
|
||||
errors.append(f"{code}: {exc}")
|
||||
if errors and done == 0:
|
||||
raise HTTPException(status_code=400, detail="; ".join(errors[:10]))
|
||||
return RedirectResponse(url=f"/malaysia/movements?wh={wh}&type=OUT", status_code=303)
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════════
|
||||
@@ -336,10 +297,8 @@ async def stocktake_create(
|
||||
raise HTTPException(status_code=503, detail="malaysia_stock_db 미설정")
|
||||
try:
|
||||
head = st.create_stocktake(
|
||||
stocktake_date=stocktake_date,
|
||||
warehouse_code=warehouse_code,
|
||||
memo=memo,
|
||||
created_by=user["email"],
|
||||
stocktake_date=stocktake_date, warehouse_code=warehouse_code,
|
||||
memo=memo, created_by=user["email"],
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc))
|
||||
@@ -356,7 +315,7 @@ async def stocktake_detail(request: Request, stocktake_id: int) -> HTMLResponse:
|
||||
return guard
|
||||
st, user = guard
|
||||
try:
|
||||
computed = st.compute_result(stocktake_id=stocktake_id)
|
||||
computed = st.compute_result(stocktake_id=stocktake_id, bom_map=_bom_map(request))
|
||||
except KeyError:
|
||||
return render_template(
|
||||
request, "denied.html",
|
||||
@@ -365,6 +324,8 @@ async def stocktake_detail(request: Request, stocktake_id: int) -> HTMLResponse:
|
||||
)
|
||||
head = computed["stocktake"]
|
||||
wh = head["warehouse_code"]
|
||||
# 입력된 라인 qty 를 코드별 dict 로 (그리드 기본값)
|
||||
entered = {ln["sku_code"]: ln["qty"] for ln in head.get("lines", [])}
|
||||
ctx = _base_ctx(request, user, st, wh=wh)
|
||||
ctx.update(
|
||||
{
|
||||
@@ -373,6 +334,7 @@ async def stocktake_detail(request: Request, stocktake_id: int) -> HTMLResponse:
|
||||
"stocktake": head,
|
||||
"individual_items": st.list_items(kind="individual"),
|
||||
"set_items": st.list_items(kind="set"),
|
||||
"entered": entered,
|
||||
"result_rows": computed["rows"],
|
||||
"missing_bom": computed["missing_bom"],
|
||||
"is_draft": head["status"] == "draft",
|
||||
@@ -381,32 +343,41 @@ async def stocktake_detail(request: Request, stocktake_id: int) -> HTMLResponse:
|
||||
return render_template(request, "malaysia/stocktake_detail.html", ctx)
|
||||
|
||||
|
||||
@router.post("/stocktakes/{stocktake_id:int}/lines")
|
||||
async def stocktake_line_upsert(
|
||||
request: Request,
|
||||
stocktake_id: int,
|
||||
sku_code: str = Form(...),
|
||||
qty: int = Form(...),
|
||||
memo: str = Form(""),
|
||||
user: dict[str, Any] = Depends(_require_user),
|
||||
@router.post("/stocktakes/{stocktake_id:int}/lines/bulk")
|
||||
async def stocktake_lines_bulk(
|
||||
request: Request, stocktake_id: int, user: dict[str, Any] = Depends(_require_user)
|
||||
) -> RedirectResponse:
|
||||
"""그리드 일괄 저장. form: qty_<SKUCODE>... (낱개+세트). 빈칸은 스킵, 0 허용."""
|
||||
st = _store(request)
|
||||
if st is None:
|
||||
raise HTTPException(status_code=503, detail="malaysia_stock_db 미설정")
|
||||
try:
|
||||
st.upsert_line(stocktake_id=stocktake_id, sku_code=sku_code, qty=qty, memo=memo)
|
||||
except KeyError:
|
||||
raise HTTPException(status_code=404, detail="재고조사를 찾을 수 없습니다.")
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc))
|
||||
form = await request.form()
|
||||
codes = [i["item_code"] for i in st.list_items(kind="individual")]
|
||||
codes += [i["item_code"] for i in st.list_items(kind="set")]
|
||||
errors: list[str] = []
|
||||
for code in codes:
|
||||
raw = str(form.get(f"qty_{code}") or "").strip()
|
||||
if raw == "":
|
||||
continue
|
||||
try:
|
||||
n = int(raw)
|
||||
except ValueError:
|
||||
errors.append(f"{code}: 숫자 아님")
|
||||
continue
|
||||
try:
|
||||
st.upsert_line(stocktake_id=stocktake_id, sku_code=code, qty=n)
|
||||
except KeyError:
|
||||
raise HTTPException(status_code=404, detail="재고조사를 찾을 수 없습니다.")
|
||||
except ValueError as exc:
|
||||
errors.append(f"{code}: {exc}")
|
||||
if errors:
|
||||
raise HTTPException(status_code=400, detail="; ".join(errors[:10]))
|
||||
return RedirectResponse(url=f"/malaysia/stocktakes/{stocktake_id}", status_code=303)
|
||||
|
||||
|
||||
@router.post("/stocktakes/{stocktake_id:int}/lines/{line_id:int}/delete")
|
||||
async def stocktake_line_delete(
|
||||
request: Request,
|
||||
stocktake_id: int,
|
||||
line_id: int,
|
||||
request: Request, stocktake_id: int, line_id: int,
|
||||
user: dict[str, Any] = Depends(_require_user),
|
||||
) -> RedirectResponse:
|
||||
st = _store(request)
|
||||
@@ -423,15 +394,13 @@ async def stocktake_line_delete(
|
||||
|
||||
@router.post("/stocktakes/{stocktake_id:int}/finalize")
|
||||
async def stocktake_finalize(
|
||||
request: Request,
|
||||
stocktake_id: int,
|
||||
user: dict[str, Any] = Depends(_require_user),
|
||||
request: Request, stocktake_id: int, user: dict[str, Any] = Depends(_require_user)
|
||||
) -> RedirectResponse:
|
||||
st = _store(request)
|
||||
if st is None:
|
||||
raise HTTPException(status_code=503, detail="malaysia_stock_db 미설정")
|
||||
try:
|
||||
st.finalize_stocktake(stocktake_id=stocktake_id)
|
||||
st.finalize_stocktake(stocktake_id=stocktake_id, bom_map=_bom_map(request))
|
||||
except KeyError:
|
||||
raise HTTPException(status_code=404, detail="재고조사를 찾을 수 없습니다.")
|
||||
except ValueError as exc:
|
||||
@@ -441,9 +410,7 @@ async def stocktake_finalize(
|
||||
|
||||
@router.post("/stocktakes/{stocktake_id:int}/cancel")
|
||||
async def stocktake_cancel(
|
||||
request: Request,
|
||||
stocktake_id: int,
|
||||
user: dict[str, Any] = Depends(_require_user),
|
||||
request: Request, stocktake_id: int, user: dict[str, Any] = Depends(_require_user)
|
||||
) -> RedirectResponse:
|
||||
st = _store(request)
|
||||
if st is None:
|
||||
@@ -456,12 +423,10 @@ async def stocktake_cancel(
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════════
|
||||
# JSON API (요구사항 6) — /malaysia/api/*
|
||||
# JSON API
|
||||
# ════════════════════════════════════════════════════════════
|
||||
@router.get("/api/warehouses")
|
||||
async def api_warehouses(
|
||||
request: Request, _: dict[str, Any] = Depends(_require_user)
|
||||
) -> JSONResponse:
|
||||
async def api_warehouses(request: Request, _: dict[str, Any] = Depends(_require_user)) -> JSONResponse:
|
||||
st = _store(request)
|
||||
if st is None:
|
||||
raise HTTPException(status_code=503, detail="malaysia_stock_db 미설정")
|
||||
@@ -469,10 +434,7 @@ async def api_warehouses(
|
||||
|
||||
|
||||
@router.get("/api/items")
|
||||
async def api_items(
|
||||
request: Request, kind: str = "", _: dict[str, Any] = Depends(_require_user)
|
||||
) -> JSONResponse:
|
||||
"""kind=individual → MT/MX/MZ, kind=set → MY, 미지정 → 전체."""
|
||||
async def api_items(request: Request, kind: str = "", _: dict[str, Any] = Depends(_require_user)) -> JSONResponse:
|
||||
st = _store(request)
|
||||
if st is None:
|
||||
raise HTTPException(status_code=503, detail="malaysia_stock_db 미설정")
|
||||
@@ -481,39 +443,13 @@ async def api_items(
|
||||
|
||||
|
||||
@router.get("/api/bom")
|
||||
async def api_bom(
|
||||
request: Request, _: dict[str, Any] = Depends(_require_user)
|
||||
) -> JSONResponse:
|
||||
st = _store(request)
|
||||
if st is None:
|
||||
raise HTTPException(status_code=503, detail="malaysia_stock_db 미설정")
|
||||
return JSONResponse({"bom": st.list_bom(include_inactive=True)})
|
||||
|
||||
|
||||
@router.post("/api/bom")
|
||||
async def api_bom_upsert(
|
||||
request: Request,
|
||||
payload: dict[str, Any] = Body(...),
|
||||
_: dict[str, Any] = Depends(_require_user),
|
||||
) -> JSONResponse:
|
||||
st = _store(request)
|
||||
if st is None:
|
||||
raise HTTPException(status_code=503, detail="malaysia_stock_db 미설정")
|
||||
try:
|
||||
row = st.upsert_bom_line(
|
||||
set_code=str(payload.get("set_code") or ""),
|
||||
component_code=str(payload.get("component_code") or ""),
|
||||
component_qty=int(payload.get("component_qty") or 0),
|
||||
)
|
||||
except (ValueError, TypeError) as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc))
|
||||
return JSONResponse({"ok": True, "bom": row})
|
||||
async def api_bom(request: Request, _: dict[str, Any] = Depends(_require_user)) -> JSONResponse:
|
||||
"""itemcode_db(set_components)에서 읽은 MY- 세트 BOM (읽기 전용)."""
|
||||
return JSONResponse({"bom": _bom_map(request)})
|
||||
|
||||
|
||||
@router.get("/api/stock")
|
||||
async def api_stock(
|
||||
request: Request, wh: str = "", _: dict[str, Any] = Depends(_require_user)
|
||||
) -> JSONResponse:
|
||||
async def api_stock(request: Request, wh: str = "", _: dict[str, Any] = Depends(_require_user)) -> JSONResponse:
|
||||
st = _store(request)
|
||||
if st is None:
|
||||
raise HTTPException(status_code=503, detail="malaysia_stock_db 미설정")
|
||||
@@ -523,11 +459,8 @@ async def api_stock(
|
||||
|
||||
@router.post("/api/movements")
|
||||
async def api_movement_create(
|
||||
request: Request,
|
||||
payload: dict[str, Any] = Body(...),
|
||||
user: dict[str, Any] = Depends(_require_user),
|
||||
request: Request, payload: dict[str, Any] = Body(...), user: dict[str, Any] = Depends(_require_user)
|
||||
) -> JSONResponse:
|
||||
"""단일 낱개 movement 생성. body: {movement_date,warehouse_code,item_code,movement_type,qty,...}."""
|
||||
st = _store(request)
|
||||
if st is None:
|
||||
raise HTTPException(status_code=503, detail="malaysia_stock_db 미설정")
|
||||
@@ -550,11 +483,8 @@ async def api_movement_create(
|
||||
|
||||
@router.post("/api/movements/set-out")
|
||||
async def api_set_out(
|
||||
request: Request,
|
||||
payload: dict[str, Any] = Body(...),
|
||||
user: dict[str, Any] = Depends(_require_user),
|
||||
request: Request, payload: dict[str, Any] = Body(...), user: dict[str, Any] = Depends(_require_user)
|
||||
) -> JSONResponse:
|
||||
"""세트 출고(BOM 분해). body: {movement_date,warehouse_code,set_code,set_qty,...}."""
|
||||
st = _store(request)
|
||||
if st is None:
|
||||
raise HTTPException(status_code=503, detail="malaysia_stock_db 미설정")
|
||||
@@ -564,6 +494,7 @@ async def api_set_out(
|
||||
warehouse_code=str(payload.get("warehouse_code") or ""),
|
||||
set_code=str(payload.get("set_code") or ""),
|
||||
set_qty=int(payload.get("set_qty") or 0),
|
||||
bom_map=_bom_map(request),
|
||||
ref_type=str(payload.get("ref_type") or "set"),
|
||||
ref_no=str(payload.get("ref_no") or ""),
|
||||
memo=str(payload.get("memo") or ""),
|
||||
@@ -576,24 +507,17 @@ async def api_set_out(
|
||||
|
||||
@router.get("/api/stocktakes/{stocktake_id:int}/result")
|
||||
async def api_stocktake_result(
|
||||
request: Request,
|
||||
stocktake_id: int,
|
||||
_: dict[str, Any] = Depends(_require_user),
|
||||
request: Request, stocktake_id: int, _: dict[str, Any] = Depends(_require_user)
|
||||
) -> JSONResponse:
|
||||
"""재고조사 최종 계산 결과(낱개 기준 direct/from_set/total)."""
|
||||
st = _store(request)
|
||||
if st is None:
|
||||
raise HTTPException(status_code=503, detail="malaysia_stock_db 미설정")
|
||||
try:
|
||||
computed = st.compute_result(stocktake_id=stocktake_id)
|
||||
computed = st.compute_result(stocktake_id=stocktake_id, bom_map=_bom_map(request))
|
||||
except KeyError:
|
||||
raise HTTPException(status_code=404, detail="재고조사를 찾을 수 없습니다.")
|
||||
return JSONResponse(
|
||||
{
|
||||
"stocktake": computed["stocktake"],
|
||||
"rows": computed["rows"],
|
||||
"missing_bom": computed["missing_bom"],
|
||||
}
|
||||
{"stocktake": computed["stocktake"], "rows": computed["rows"], "missing_bom": computed["missing_bom"]}
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user