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:
@@ -3,7 +3,6 @@
|
||||
<a class="erp-btn {% if active_tab=='status' %}erp-btn-primary{% else %}erp-btn-outline{% endif %}" href="/malaysia/?wh={{ selected_wh }}">재고 현황</a>
|
||||
<a class="erp-btn {% if active_tab=='move' %}erp-btn-primary{% else %}erp-btn-outline{% endif %}" href="/malaysia/movements?wh={{ selected_wh }}">입출고</a>
|
||||
<a class="erp-btn {% if active_tab=='stocktake' %}erp-btn-primary{% else %}erp-btn-outline{% endif %}" href="/malaysia/stocktakes?wh={{ selected_wh }}">일일 재고조사</a>
|
||||
<a class="erp-btn {% if active_tab=='sets' %}erp-btn-primary{% else %}erp-btn-outline{% endif %}" href="/malaysia/sets?wh={{ selected_wh }}">세트 구성</a>
|
||||
|
||||
<span style="flex:1 1 auto"></span>
|
||||
|
||||
|
||||
@@ -5,65 +5,76 @@
|
||||
{% set active_tab = 'move' %}
|
||||
{% include "malaysia/_nav.html" %}
|
||||
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;align-items:start;">
|
||||
|
||||
<!-- 입고/출고 등록 (낱개) -->
|
||||
<div class="erp-card">
|
||||
<div class="cpg-card-head"><h2>입고 / 출고 등록 (낱개)</h2></div>
|
||||
<span class="erp-muted">허용: MT-/MX-/MZ- · 차단: MY-/MD-</span>
|
||||
<form method="post" action="/malaysia/movements" style="margin-top:10px;display:flex;flex-direction:column;gap:8px;">
|
||||
<input type="hidden" name="warehouse_code" value="{{ selected_wh }}" />
|
||||
<!-- 낱개 입출고 그리드 일괄 -->
|
||||
<div class="erp-card">
|
||||
<div class="cpg-card-head"><h2>입고 / 출고 / 조정 — 낱개 일괄</h2></div>
|
||||
<span class="erp-muted">전체 낱개 아이템에 수량만 입력. 빈칸/0은 제외. IN/OUT 양수, ADJUST는 ±. (세트는 아래에서 출고)</span>
|
||||
<form method="post" action="/malaysia/movements/bulk" style="margin-top:10px;">
|
||||
<input type="hidden" name="warehouse_code" value="{{ selected_wh }}" />
|
||||
<div style="display:flex;gap:10px;flex-wrap:wrap;align-items:end;margin-bottom:10px;">
|
||||
<label class="erp-field"><span>이동 종류</span>
|
||||
<select class="erp-select" name="movement_type" required>
|
||||
<option value="IN">IN (입고)</option>
|
||||
<option value="OUT">OUT (출고)</option>
|
||||
<option value="ADJUST">ADJUST (조정, ± 가능)</option>
|
||||
<option value="ADJUST">ADJUST (조정 ±)</option>
|
||||
</select></label>
|
||||
<label class="erp-field"><span>날짜</span>
|
||||
<input class="erp-input" type="date" name="movement_date" value="{{ today }}" required /></label>
|
||||
<label class="erp-field"><span>아이템</span>
|
||||
<select class="erp-select" name="item_code" required>
|
||||
<option value="">— 선택 —</option>
|
||||
{% for it in individual_items %}
|
||||
<option value="{{ it.item_code }}">{{ it.item_code }} · {{ it.item_name }}</option>
|
||||
{% endfor %}
|
||||
</select></label>
|
||||
<label class="erp-field"><span>수량</span>
|
||||
<input class="erp-input" type="number" name="qty" required placeholder="IN/OUT 은 양수, ADJUST 는 ±" /></label>
|
||||
<label class="erp-field"><span>Ref Type</span>
|
||||
<input class="erp-input" type="text" name="ref_type" placeholder="manual / shopee / lazada ..." /></label>
|
||||
<input class="erp-input" type="text" name="ref_type" placeholder="manual / shopee / lazada" /></label>
|
||||
<label class="erp-field"><span>Ref No</span>
|
||||
<input class="erp-input" type="text" name="ref_no" placeholder="주문번호 등" /></label>
|
||||
<label class="erp-field"><span>메모</span>
|
||||
<input class="erp-input" type="text" name="ref_no" /></label>
|
||||
<label class="erp-field" style="flex:1 1 200px"><span>메모</span>
|
||||
<input class="erp-input" type="text" name="memo" /></label>
|
||||
<div class="erp-page-actions"><button type="submit" class="erp-btn erp-btn-primary">등록</button></div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="erp-table-wrap">
|
||||
<table class="erp-table">
|
||||
<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 class="erp-card">
|
||||
<div class="cpg-card-head"><h2>세트 출고 (BOM 분해)</h2></div>
|
||||
<span class="erp-muted">MY- 세트 1건 → set_bom 기준으로 구성품별 OUT 자동 생성</span>
|
||||
<form method="post" action="/malaysia/movements/set-out" style="margin-top:10px;display:flex;flex-direction:column;gap:8px;">
|
||||
<input type="hidden" name="warehouse_code" value="{{ selected_wh }}" />
|
||||
<!-- 세트 출고 그리드 (BOM 분해) -->
|
||||
<div class="erp-card" style="margin-top:16px;">
|
||||
<div class="cpg-card-head"><h2>세트 출고 — BOM 분해 (itemcode_db)</h2></div>
|
||||
<span class="erp-muted">세트 수량 입력 시 set_components 기준으로 구성품별 OUT 자동 생성. 빈칸/0 제외.</span>
|
||||
<form method="post" action="/malaysia/movements/set-out-bulk" style="margin-top:10px;">
|
||||
<input type="hidden" name="warehouse_code" value="{{ selected_wh }}" />
|
||||
<div style="display:flex;gap:10px;flex-wrap:wrap;align-items:end;margin-bottom:10px;">
|
||||
<label class="erp-field"><span>날짜</span>
|
||||
<input class="erp-input" type="date" name="movement_date" value="{{ today }}" required /></label>
|
||||
<label class="erp-field"><span>세트</span>
|
||||
<select class="erp-select" name="set_code" required>
|
||||
<option value="">— 선택 —</option>
|
||||
{% for it in set_items %}
|
||||
<option value="{{ it.item_code }}">{{ it.item_code }} · {{ it.item_name }}</option>
|
||||
{% endfor %}
|
||||
</select></label>
|
||||
<label class="erp-field"><span>세트 수량</span>
|
||||
<input class="erp-input" type="number" name="set_qty" min="1" required /></label>
|
||||
<label class="erp-field"><span>Ref No</span>
|
||||
<input class="erp-input" type="text" name="ref_no" placeholder="주문번호 등" /></label>
|
||||
<label class="erp-field"><span>메모</span>
|
||||
<label class="erp-field" style="flex:1 1 200px"><span>메모</span>
|
||||
<input class="erp-input" type="text" name="memo" /></label>
|
||||
<div class="erp-page-actions"><button type="submit" class="erp-btn erp-btn-primary">세트 출고</button></div>
|
||||
</form>
|
||||
</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>
|
||||
<tbody>
|
||||
{% for it in set_items %}
|
||||
<tr>
|
||||
<td>{{ it.item_code }}</td>
|
||||
<td>{{ it.item_name }}</td>
|
||||
<td><input class="erp-input" type="number" min="0" 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>
|
||||
|
||||
<!-- 이동 이력 -->
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
{% extends "erp_base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<section class="mys">
|
||||
{% set active_tab = 'sets' %}
|
||||
{% include "malaysia/_nav.html" %}
|
||||
|
||||
<div style="display:grid;grid-template-columns:340px 1fr;gap:16px;align-items:start;">
|
||||
|
||||
<!-- 구성품 추가 -->
|
||||
<div class="erp-card">
|
||||
<div class="cpg-card-head"><h2>세트 구성 추가/수정</h2></div>
|
||||
<span class="erp-muted">같은 세트+구성품은 덮어씁니다.</span>
|
||||
<form method="post" action="/malaysia/sets/bom" style="margin-top:10px;display:flex;flex-direction:column;gap:8px;">
|
||||
<label class="erp-field"><span>세트 (MY-)</span>
|
||||
<select class="erp-select" name="set_code" required>
|
||||
<option value="">— 선택 —</option>
|
||||
{% for it in set_items %}
|
||||
<option value="{{ it.item_code }}">{{ it.item_code }} · {{ it.item_name }}</option>
|
||||
{% endfor %}
|
||||
</select></label>
|
||||
<label class="erp-field"><span>구성품 (MT-/MX-/MZ-)</span>
|
||||
<select class="erp-select" name="component_code" required>
|
||||
<option value="">— 선택 —</option>
|
||||
{% for it in individual_items %}
|
||||
<option value="{{ it.item_code }}">{{ it.item_code }} · {{ it.item_name }}</option>
|
||||
{% endfor %}
|
||||
</select></label>
|
||||
<label class="erp-field"><span>구성 수량</span>
|
||||
<input class="erp-input" type="number" name="component_qty" min="1" required /></label>
|
||||
<div class="erp-page-actions"><button type="submit" class="erp-btn erp-btn-primary">저장</button></div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- 세트별 구성 목록 -->
|
||||
<div class="erp-card">
|
||||
<div class="cpg-card-head"><h2>세트 구성표 (set_bom)</h2></div>
|
||||
{% for it in set_items %}
|
||||
<div style="margin-bottom:18px;">
|
||||
<h3 style="margin:6px 0;">{{ it.item_code }} · {{ it.item_name }}</h3>
|
||||
<div class="erp-table-wrap">
|
||||
<table class="erp-table">
|
||||
<thead><tr><th>Component</th><th>Name</th><th style="text-align:right">Qty</th><th>Active</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
{% for b in by_set.get(it.item_code, []) %}
|
||||
<tr>
|
||||
<td>{{ b.component_code }}</td>
|
||||
<td>{{ names.get(b.component_code, '—') }}</td>
|
||||
<td style="text-align:right">{{ b.component_qty }}</td>
|
||||
<td>{% if b.active %}<span class="erp-badge erp-badge-success">활성</span>{% else %}<span class="erp-badge erp-badge-neutral">비활성</span>{% endif %}</td>
|
||||
<td>
|
||||
<form method="post" action="/malaysia/sets/bom/{{ b.id }}/delete" onsubmit="return confirm('이 구성품을 삭제합니다. 계속?');">
|
||||
<button type="submit" class="erp-btn erp-btn-danger">삭제</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% if not by_set.get(it.item_code) %}
|
||||
<tr><td colspan="5" class="erp-muted">구성품이 없습니다. (재고조사에 입력 시 경고)</td></tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -15,73 +15,58 @@
|
||||
{% else %}<span class="erp-badge erp-badge-inverse">작성중</span>{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{% if missing_bom %}
|
||||
<p style="color:#b91c1c;font-weight:600;">⚠ BOM 구성이 없는 세트: {{ missing_bom|join(', ') }} — 세트 구성 등록 전에는 확정할 수 없습니다.</p>
|
||||
<p style="color:#b91c1c;font-weight:600;">⚠ BOM 구성이 없는 세트: {{ missing_bom|join(', ') }} — itemcode_db set_components 확인 필요. 확정 불가.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if is_draft %}
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;align-items:start;margin-top:16px;">
|
||||
<!-- 낱개 입력 -->
|
||||
<div class="erp-card">
|
||||
<div class="cpg-card-head"><h2>① 낱개 아이템 입력</h2></div>
|
||||
<span class="erp-muted">세트 안에 든 건 빼고, 낱개로 보관 중인 수량만 입력</span>
|
||||
<form method="post" action="/malaysia/stocktakes/{{ stocktake.id }}/lines" style="margin-top:10px;display:flex;gap:8px;flex-wrap:wrap;align-items:end;">
|
||||
<label class="erp-field" style="flex:2 1 180px"><span>아이템</span>
|
||||
<select class="erp-select" name="sku_code" required>
|
||||
<option value="">— 선택 —</option>
|
||||
{% for it in individual_items %}<option value="{{ it.item_code }}">{{ it.item_code }} · {{ it.item_name }}</option>{% endfor %}
|
||||
</select></label>
|
||||
<label class="erp-field" style="flex:1 1 90px"><span>수량</span>
|
||||
<input class="erp-input" type="number" name="qty" min="0" required /></label>
|
||||
<button type="submit" class="erp-btn erp-btn-primary">입력</button>
|
||||
</form>
|
||||
<!-- 그리드 일괄 입력: 낱개 + 세트 한 폼에서 -->
|
||||
<form method="post" action="/malaysia/stocktakes/{{ stocktake.id }}/lines/bulk" style="margin-top:16px;">
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;align-items:start;">
|
||||
<div class="erp-card">
|
||||
<div class="cpg-card-head"><h2>① 낱개 아이템</h2></div>
|
||||
<span class="erp-muted">세트 안에 든 건 빼고, 낱개 보관분만. 빈칸=미조사, 0 입력 가능.</span>
|
||||
<div class="erp-table-wrap" style="margin-top:8px;">
|
||||
<table class="erp-table">
|
||||
<thead><tr><th>Code</th><th>Name</th><th style="width:120px">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" min="0" name="qty_{{ it.item_code }}"
|
||||
value="{{ entered.get(it.item_code, '') }}" placeholder="—" style="width:100px" /></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="erp-card">
|
||||
<div class="cpg-card-head"><h2>② 세트 아이템</h2></div>
|
||||
<span class="erp-muted">미리 포장된 세트 재고. BOM(itemcode_db)으로 낱개 분해됨.</span>
|
||||
<div class="erp-table-wrap" style="margin-top:8px;">
|
||||
<table class="erp-table">
|
||||
<thead><tr><th>Set Code</th><th>Set Name</th><th style="width:120px">Qty</th></tr></thead>
|
||||
<tbody>
|
||||
{% for it in set_items %}
|
||||
<tr>
|
||||
<td>{{ it.item_code }}</td>
|
||||
<td>{{ it.item_name }}</td>
|
||||
<td><input class="erp-input" type="number" min="0" name="qty_{{ it.item_code }}"
|
||||
value="{{ entered.get(it.item_code, '') }}" placeholder="—" style="width:100px" /></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 세트 입력 -->
|
||||
<div class="erp-card">
|
||||
<div class="cpg-card-head"><h2>② 세트 아이템 입력</h2></div>
|
||||
<span class="erp-muted">미리 포장된 세트 재고 수량 (BOM 으로 낱개 분해됨)</span>
|
||||
<form method="post" action="/malaysia/stocktakes/{{ stocktake.id }}/lines" style="margin-top:10px;display:flex;gap:8px;flex-wrap:wrap;align-items:end;">
|
||||
<label class="erp-field" style="flex:2 1 180px"><span>세트</span>
|
||||
<select class="erp-select" name="sku_code" required>
|
||||
<option value="">— 선택 —</option>
|
||||
{% for it in set_items %}<option value="{{ it.item_code }}">{{ it.item_code }} · {{ it.item_name }}</option>{% endfor %}
|
||||
</select></label>
|
||||
<label class="erp-field" style="flex:1 1 90px"><span>세트 수량</span>
|
||||
<input class="erp-input" type="number" name="qty" min="0" required /></label>
|
||||
<button type="submit" class="erp-btn erp-btn-primary">입력</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="erp-page-actions" style="margin-top:12px;"><button type="submit" class="erp-btn erp-btn-primary">입력 저장</button></div>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
<!-- 입력된 라인 -->
|
||||
<div class="erp-card" style="margin-top:16px;">
|
||||
<div class="cpg-card-head"><h2>입력 라인 ({{ stocktake.lines|length }})</h2></div>
|
||||
<div class="erp-table-wrap">
|
||||
<table class="erp-table">
|
||||
<thead><tr><th>SKU</th><th style="text-align:right">Qty</th><th>Memo</th>{% if is_draft %}<th></th>{% endif %}</tr></thead>
|
||||
<tbody>
|
||||
{% for ln in stocktake.lines %}
|
||||
<tr>
|
||||
<td>{{ ln.sku_code }}</td>
|
||||
<td style="text-align:right">{{ ln.qty }}</td>
|
||||
<td>{{ ln.memo or '—' }}</td>
|
||||
{% if is_draft %}
|
||||
<td><form method="post" action="/malaysia/stocktakes/{{ stocktake.id }}/lines/{{ ln.id }}/delete" onsubmit="return confirm('삭제?');"><button class="erp-btn erp-btn-danger">삭제</button></form></td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% if not stocktake.lines %}
|
||||
<tr><td colspan="{% if is_draft %}4{% else %}3{% endif %}" class="erp-muted">입력된 라인이 없습니다.</td></tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 최종 계산 결과 -->
|
||||
<div class="erp-card" style="margin-top:16px;">
|
||||
<div class="cpg-card-head"><h2>최종 계산 (낱개 기준)</h2></div>
|
||||
@@ -100,7 +85,7 @@
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% if not result_rows %}
|
||||
<tr><td colspan="5" class="erp-muted">계산 결과가 없습니다.</td></tr>
|
||||
<tr><td colspan="5" class="erp-muted">입력된 수량이 없습니다.</td></tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -110,7 +95,7 @@
|
||||
{% if is_draft %}
|
||||
<div class="erp-page-actions" style="margin-top:16px;display:flex;gap:8px;">
|
||||
<form method="post" action="/malaysia/stocktakes/{{ stocktake.id }}/finalize"
|
||||
onsubmit="return confirm('확정하면 시스템 재고와의 차이가 STOCKTAKE 이동으로 기록되고, 이후 라인 수정이 불가합니다. 계속?');">
|
||||
onsubmit="return confirm('확정하면 시스템 재고와의 차이가 STOCKTAKE 이동으로 기록되고, 이후 수정이 불가합니다. 계속?');">
|
||||
<button type="submit" class="erp-btn erp-btn-primary" {% if missing_bom %}disabled title="BOM 누락 세트 해결 필요"{% endif %}>확정 (재고 반영)</button>
|
||||
</form>
|
||||
<form method="post" action="/malaysia/stocktakes/{{ stocktake.id }}/cancel" onsubmit="return confirm('이 조사를 취소합니다. 계속?');">
|
||||
|
||||
Reference in New Issue
Block a user