feat(malaysia): 입출고 화면 통합 — 낱개(좌)·세트(우) 한 폼, 버튼 하나로 일괄 적용
- /movements/bulk + /movements/set-out-bulk → /movements/apply 단일 엔드포인트 - 낱개는 선택 종류(IN/OUT/ADJUST), 세트는 항상 OUT(BOM 분해) - 2컬럼 그리드 한 폼, "일괄 적용" 버튼 하나 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -170,12 +170,16 @@ async def movements_page(request: Request) -> HTMLResponse:
|
|||||||
return render_template(request, "malaysia/movements.html", ctx)
|
return render_template(request, "malaysia/movements.html", ctx)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/movements/bulk")
|
@router.post("/movements/apply")
|
||||||
async def movements_bulk(request: Request, user: dict[str, Any] = Depends(_require_user)) -> RedirectResponse:
|
async def movements_apply(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 아닌 값만 등록(±).
|
form: movement_type(낱개용 IN/OUT/ADJUST), movement_date, warehouse_code,
|
||||||
|
ref_type, ref_no, memo, qty_<CODE>...
|
||||||
|
|
||||||
|
- 낱개(individual): 선택한 movement_type 으로 등록. IN/OUT 양수만, ADJUST ±(0 스킵).
|
||||||
|
- 세트(set): 항상 OUT 으로 처리 — BOM(itemcode_db) 분해해 구성품별 OUT 생성.
|
||||||
|
빈칸/0 은 스킵. 코드별 에러는 모아서 부분 적용 후 안내.
|
||||||
"""
|
"""
|
||||||
st = _store(request)
|
st = _store(request)
|
||||||
if st is None:
|
if st is None:
|
||||||
@@ -189,9 +193,12 @@ async def movements_bulk(request: Request, user: dict[str, Any] = Depends(_requi
|
|||||||
ref_type = str(form.get("ref_type") or "").strip()
|
ref_type = str(form.get("ref_type") or "").strip()
|
||||||
ref_no = str(form.get("ref_no") or "").strip()
|
ref_no = str(form.get("ref_no") or "").strip()
|
||||||
memo = str(form.get("memo") or "").strip()
|
memo = str(form.get("memo") or "").strip()
|
||||||
|
bmap = _bom_map(request)
|
||||||
|
|
||||||
created = 0
|
applied = 0
|
||||||
errors: list[str] = []
|
errors: list[str] = []
|
||||||
|
|
||||||
|
# 낱개 — 선택한 종류로
|
||||||
for it in st.list_items(kind="individual"):
|
for it in st.list_items(kind="individual"):
|
||||||
code = it["item_code"]
|
code = it["item_code"]
|
||||||
raw = str(form.get(f"qty_{code}") or "").strip()
|
raw = str(form.get(f"qty_{code}") or "").strip()
|
||||||
@@ -210,31 +217,11 @@ async def movements_bulk(request: Request, user: dict[str, Any] = Depends(_requi
|
|||||||
movement_type=mtype, qty=n, ref_type=ref_type, ref_no=ref_no,
|
movement_type=mtype, qty=n, ref_type=ref_type, ref_no=ref_no,
|
||||||
memo=memo, created_by=user["email"],
|
memo=memo, created_by=user["email"],
|
||||||
)
|
)
|
||||||
created += 1
|
applied += 1
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
errors.append(f"{code}: {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)
|
|
||||||
|
|
||||||
|
# 세트 — 항상 OUT(BOM 분해)
|
||||||
@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 미설정")
|
|
||||||
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)
|
|
||||||
|
|
||||||
done = 0
|
|
||||||
errors: list[str] = []
|
|
||||||
for it in st.list_items(kind="set"):
|
for it in st.list_items(kind="set"):
|
||||||
code = it["item_code"]
|
code = it["item_code"]
|
||||||
raw = str(form.get(f"qty_{code}") or "").strip()
|
raw = str(form.get(f"qty_{code}") or "").strip()
|
||||||
@@ -243,6 +230,7 @@ async def set_out_bulk(request: Request, user: dict[str, Any] = Depends(_require
|
|||||||
try:
|
try:
|
||||||
n = int(raw)
|
n = int(raw)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
|
errors.append(f"{code}: 숫자 아님")
|
||||||
continue
|
continue
|
||||||
if n <= 0:
|
if n <= 0:
|
||||||
continue
|
continue
|
||||||
@@ -252,12 +240,13 @@ async def set_out_bulk(request: Request, user: dict[str, Any] = Depends(_require
|
|||||||
bom_map=bmap, ref_type="set", ref_no=ref_no, memo=memo,
|
bom_map=bmap, ref_type="set", ref_no=ref_no, memo=memo,
|
||||||
created_by=user["email"],
|
created_by=user["email"],
|
||||||
)
|
)
|
||||||
done += 1
|
applied += 1
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
errors.append(f"{code}: {exc}")
|
errors.append(f"{code}: {exc}")
|
||||||
if errors and done == 0:
|
|
||||||
|
if errors and applied == 0:
|
||||||
raise HTTPException(status_code=400, detail="; ".join(errors[:10]))
|
raise HTTPException(status_code=400, detail="; ".join(errors[:10]))
|
||||||
return RedirectResponse(url=f"/malaysia/movements?wh={wh}&type=OUT", status_code=303)
|
return RedirectResponse(url=f"/malaysia/movements?wh={wh}&type={mtype}", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
# ════════════════════════════════════════════════════════════
|
# ════════════════════════════════════════════════════════════
|
||||||
|
|||||||
@@ -5,14 +5,13 @@
|
|||||||
{% set active_tab = 'move' %}
|
{% set active_tab = 'move' %}
|
||||||
{% include "malaysia/_nav.html" %}
|
{% include "malaysia/_nav.html" %}
|
||||||
|
|
||||||
<!-- 낱개 입출고 그리드 일괄 -->
|
<!-- 낱개 + 세트 한 화면 일괄 (버튼 하나) -->
|
||||||
<div class="erp-card">
|
<form method="post" action="/malaysia/movements/apply">
|
||||||
<div class="cpg-card-head"><h2>입고 / 출고 / 조정 — 낱개 일괄</h2></div>
|
<input type="hidden" name="warehouse_code" value="{{ selected_wh }}" />
|
||||||
<span class="erp-muted">전체 낱개 아이템에 수량만 입력. 빈칸/0은 제외. IN/OUT 양수, ADJUST는 ±. (세트는 아래에서 출고)</span>
|
|
||||||
<form method="post" action="/malaysia/movements/bulk" style="margin-top:10px;">
|
<div class="erp-card">
|
||||||
<input type="hidden" name="warehouse_code" value="{{ selected_wh }}" />
|
<div style="display:flex;gap:10px;flex-wrap:wrap;align-items:end;">
|
||||||
<div style="display:flex;gap:10px;flex-wrap:wrap;align-items:end;margin-bottom:10px;">
|
<label class="erp-field"><span>이동 종류 (낱개)</span>
|
||||||
<label class="erp-field"><span>이동 종류</span>
|
|
||||||
<select class="erp-select" name="movement_type" required>
|
<select class="erp-select" name="movement_type" required>
|
||||||
<option value="IN">IN (입고)</option>
|
<option value="IN">IN (입고)</option>
|
||||||
<option value="OUT">OUT (출고)</option>
|
<option value="OUT">OUT (출고)</option>
|
||||||
@@ -27,55 +26,54 @@
|
|||||||
<label class="erp-field" style="flex:1 1 200px"><span>메모</span>
|
<label class="erp-field" style="flex:1 1 200px"><span>메모</span>
|
||||||
<input class="erp-input" type="text" name="memo" /></label>
|
<input class="erp-input" type="text" name="memo" /></label>
|
||||||
</div>
|
</div>
|
||||||
<div class="erp-table-wrap">
|
<p class="erp-muted" style="margin:8px 0 0;">낱개는 선택한 종류로, <b>세트는 항상 출고(OUT)</b>로 처리(BOM 분해). 빈칸/0 제외.</p>
|
||||||
<table class="erp-table">
|
</div>
|
||||||
<thead><tr><th>Item Code</th><th>Product Name</th><th style="width:140px">Qty</th></tr></thead>
|
|
||||||
<tbody>
|
|
||||||
{% for it in individual_items %}
|
|
||||||
<tr>
|
|
||||||
<td>{{ it.item_code }}</td>
|
|
||||||
<td>{{ it.item_name }}</td>
|
|
||||||
<td><input class="erp-input" type="number" name="qty_{{ it.item_code }}" placeholder="0" style="width:120px" /></td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<div class="erp-page-actions" style="margin-top:10px;"><button type="submit" class="erp-btn erp-btn-primary">일괄 등록</button></div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 세트 출고 그리드 (BOM 분해) -->
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;align-items:start;margin-top:16px;">
|
||||||
<div class="erp-card" style="margin-top:16px;">
|
<!-- 좌: 낱개 -->
|
||||||
<div class="cpg-card-head"><h2>세트 출고 — BOM 분해 (itemcode_db)</h2></div>
|
<div class="erp-card">
|
||||||
<span class="erp-muted">세트 수량 입력 시 set_components 기준으로 구성품별 OUT 자동 생성. 빈칸/0 제외.</span>
|
<div class="cpg-card-head"><h2>낱개 아이템</h2></div>
|
||||||
<form method="post" action="/malaysia/movements/set-out-bulk" style="margin-top:10px;">
|
<div class="erp-table-wrap" style="margin-top:8px;">
|
||||||
<input type="hidden" name="warehouse_code" value="{{ selected_wh }}" />
|
<table class="erp-table">
|
||||||
<div style="display:flex;gap:10px;flex-wrap:wrap;align-items:end;margin-bottom:10px;">
|
<thead><tr><th>Code</th><th>Name</th><th style="width:120px">Qty</th></tr></thead>
|
||||||
<label class="erp-field"><span>날짜</span>
|
<tbody>
|
||||||
<input class="erp-input" type="date" name="movement_date" value="{{ today }}" required /></label>
|
{% for it in individual_items %}
|
||||||
<label class="erp-field"><span>Ref No</span>
|
<tr>
|
||||||
<input class="erp-input" type="text" name="ref_no" placeholder="주문번호 등" /></label>
|
<td>{{ it.item_code }}</td>
|
||||||
<label class="erp-field" style="flex:1 1 200px"><span>메모</span>
|
<td>{{ it.item_name }}</td>
|
||||||
<input class="erp-input" type="text" name="memo" /></label>
|
<td><input class="erp-input" type="number" name="qty_{{ it.item_code }}" placeholder="—" style="width:100px" /></td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="erp-table-wrap">
|
|
||||||
<table class="erp-table">
|
<!-- 우: 세트 (출고 전용) -->
|
||||||
<thead><tr><th>Set Code</th><th>Set Name</th><th style="width:140px">Set Qty (OUT)</th></tr></thead>
|
<div class="erp-card">
|
||||||
<tbody>
|
<div class="cpg-card-head"><h2>세트 아이템 — 출고(OUT) · BOM 분해</h2></div>
|
||||||
{% for it in set_items %}
|
<span class="erp-muted">itemcode_db set_components 기준 구성품별 OUT 자동 생성</span>
|
||||||
<tr>
|
<div class="erp-table-wrap" style="margin-top:8px;">
|
||||||
<td>{{ it.item_code }}</td>
|
<table class="erp-table">
|
||||||
<td>{{ it.item_name }}</td>
|
<thead><tr><th>Set Code</th><th>Set Name</th><th style="width:120px">Qty (OUT)</th></tr></thead>
|
||||||
<td><input class="erp-input" type="number" min="0" name="qty_{{ it.item_code }}" placeholder="0" style="width:120px" /></td>
|
<tbody>
|
||||||
</tr>
|
{% for it in set_items %}
|
||||||
{% endfor %}
|
<tr>
|
||||||
</tbody>
|
<td>{{ it.item_code }}</td>
|
||||||
</table>
|
<td>{{ it.item_name }}</td>
|
||||||
|
<td><input class="erp-input" type="number" min="0" name="qty_{{ it.item_code }}" placeholder="—" style="width:100px" /></td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="erp-page-actions" style="margin-top:10px;"><button type="submit" class="erp-btn erp-btn-primary">세트 출고</button></div>
|
</div>
|
||||||
</form>
|
|
||||||
</div>
|
<div class="erp-page-actions" style="margin-top:12px;">
|
||||||
|
<button type="submit" class="erp-btn erp-btn-primary">일괄 적용</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
<!-- 이동 이력 -->
|
<!-- 이동 이력 -->
|
||||||
<div class="erp-card" style="margin-top:16px;">
|
<div class="erp-card" style="margin-top:16px;">
|
||||||
|
|||||||
Reference in New Issue
Block a user