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:
2026-06-11 13:34:31 +09:00
parent 13ac11cc0a
commit f137f2a6a6
19 changed files with 2583 additions and 1 deletions
@@ -0,0 +1,103 @@
{% extends "erp_base.html" %}
{% block content %}
<section class="mys">
{% 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 }}" />
<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>
</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>
<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="memo" /></label>
<div class="erp-page-actions"><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 }}" />
<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>
<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-card" style="margin-top:16px;">
<div class="cpg-card-head" style="display:flex;justify-content:space-between;align-items:center;">
<h2>최근 이동 이력</h2>
<span>
<a class="erp-btn erp-btn-outline" href="/malaysia/movements?wh={{ selected_wh }}">전체</a>
{% for t in ['IN','OUT','ADJUST','STOCKTAKE'] %}
<a class="erp-btn {% if filter_type==t %}erp-btn-primary{% else %}erp-btn-outline{% endif %}" href="/malaysia/movements?wh={{ selected_wh }}&type={{ t }}">{{ t }}</a>
{% endfor %}
</span>
</div>
<div class="erp-table-wrap">
<table class="erp-table">
<thead><tr><th>Date</th><th>Type</th><th>Item</th><th style="text-align:right">Qty</th><th>Ref</th><th>Memo</th><th>By</th></tr></thead>
<tbody>
{% for m in movements %}
<tr>
<td>{{ m.movement_date }}</td>
<td>{{ m.movement_type }}</td>
<td>{{ m.item_code }}</td>
<td style="text-align:right">{{ m.qty }}</td>
<td>{{ m.ref_type }}{% if m.ref_no %} / {{ m.ref_no }}{% endif %}</td>
<td>{{ m.memo or '—' }}</td>
<td>{{ m.created_by or '—' }}</td>
</tr>
{% endfor %}
{% if not movements %}
<tr><td colspan="7" class="erp-muted">이동 이력이 없습니다.</td></tr>
{% endif %}
</tbody>
</table>
</div>
</div>
</section>
{% endblock %}