feat(malaysia): 말레이시아 창고 재고관리 모듈 추가
- 신규 모듈 app/modules/malaysia (prefix /malaysia, 권한키 malaysia) - malaysia_stock_db: warehouses/malaysia_items/set_bom/stock_movement/ daily_stocktake/daily_stocktake_line (멱등 init SQL) - 낱개(MT/MX/MZ) 입출고·조정 movement, 세트(MY) BOM 관리, 세트 출고 BOM 분해 - 일일 재고조사: 세트→낱개 자동 분해(direct+from_set=total), 확정 시 차이만 STOCKTAKE - prefix 검증 이중화(DB CHECK + store.py 순수함수), MD- 전면 제외 - 상품명은 itemcode_db 읽기 전용 재사용(중복 마스터 없음) - 화면 4종 + JSON API, 순수로직 테스트 14건, README/docs 갱신 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
{% extends "erp_base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<section class="mys">
|
||||
<div class="erp-page-actions">
|
||||
<a class="erp-btn erp-btn-outline" href="/malaysia/stocktakes?wh={{ stocktake.warehouse_code }}">◀◀ 조사 목록</a>
|
||||
</div>
|
||||
|
||||
<div class="erp-card">
|
||||
<div class="cpg-card-head" style="display:flex;justify-content:space-between;align-items:center;">
|
||||
<h2>재고조사 #{{ stocktake.id }} — {{ stocktake.stocktake_date }} · {{ stocktake.warehouse_code }}</h2>
|
||||
<span>
|
||||
{% if stocktake.status=='finalized' %}<span class="erp-badge erp-badge-success">확정 ({{ stocktake.finalized_at }})</span>
|
||||
{% elif stocktake.status=='cancelled' %}<span class="erp-badge erp-badge-neutral">취소</span>
|
||||
{% 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>
|
||||
{% 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>
|
||||
</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>
|
||||
{% 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>
|
||||
<span class="erp-muted">total_qty = direct_qty(낱개 직접) + from_set_qty(세트 분해)</span>
|
||||
<div class="erp-table-wrap">
|
||||
<table class="erp-table">
|
||||
<thead><tr><th>Item Code</th><th>Product Name</th><th style="text-align:right">Direct</th><th style="text-align:right">From Set</th><th style="text-align:right">Total</th></tr></thead>
|
||||
<tbody>
|
||||
{% for r in result_rows %}
|
||||
<tr>
|
||||
<td>{{ r.item_code }}</td>
|
||||
<td>{{ r.item_name }}</td>
|
||||
<td style="text-align:right">{{ r.direct_qty }}</td>
|
||||
<td style="text-align:right">{{ r.from_set_qty }}</td>
|
||||
<td style="text-align:right;font-weight:600;">{{ r.total_qty }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% if not result_rows %}
|
||||
<tr><td colspan="5" class="erp-muted">계산 결과가 없습니다.</td></tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% 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 이동으로 기록되고, 이후 라인 수정이 불가합니다. 계속?');">
|
||||
<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('이 조사를 취소합니다. 계속?');">
|
||||
<button type="submit" class="erp-btn erp-btn-danger">취소</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
</section>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user