feat(malaysia): 창고 랙 보기 페이지

최근(취소 제외) 재고조사의 랙 입력을 칸별로 한눈에 보는 읽기전용 메뉴.
각 칸에 상품명·박스수×입수량=총개수, 칸 합계(∑) 표시.

- nav: '창고 랙 보기' 탭 추가(active_tab='rack')
- router: GET /malaysia/rack — latest_stocktake → list_rack_entries 칸별 그룹
- 템플릿 rack_view.html — Side A/B 세로 적층, 칸별 아이템 목록
- i18n 키 추가(Rack View 등)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-14 16:48:07 +09:00
parent 4c7eb9467f
commit 1e0e69fcbe
5 changed files with 125 additions and 1 deletions
+34
View File
@@ -174,6 +174,40 @@ async def index(request: Request) -> HTMLResponse:
return render_template(request, "malaysia/index.html", ctx)
@router.get("/rack", response_class=HTMLResponse)
async def rack_view(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)
latest = st.latest_stocktake(warehouse_code=wh)
rack_by_cell: dict[str, list[dict[str, Any]]] = {}
names = st.item_name_map()
if latest:
for e in st.list_rack_entries(stocktake_id=latest["id"]):
e["item_name"] = names.get(e["sku_code"], e["sku_code"])
e["total"] = int(e["units_per_box"]) * int(e["box_count"])
rack_by_cell.setdefault(e["cell_code"], []).append(e)
ctx = _base_ctx(request, user, st, wh=wh)
ctx.update(
{
"page_title": "말레이시아 재고관리 — 창고 랙",
"page_subtitle": f"{wh} · 랙 보기",
"rack_layout": store.rack_layout(),
"rack_by_cell": rack_by_cell,
"stocktake": latest,
}
)
return render_template(request, "malaysia/rack_view.html", ctx)
@router.post("/reset")
async def reset_data(
request: Request, user: dict[str, Any] = Depends(_require_user)
@@ -2,4 +2,4 @@
<div style="display:flex;justify-content:flex-end;margin-bottom:8px;">
<button type="button" id="mys-lang-toggle" class="erp-btn erp-btn-outline">ENG</button>
</div>
<script src="/static/malaysia.js?v=20260614a"></script>
<script src="/static/malaysia.js?v=20260614b"></script>
@@ -3,6 +3,7 @@
<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=='rack' %}erp-btn-primary{% else %}erp-btn-outline{% endif %}" href="/malaysia/rack?wh={{ selected_wh }}">창고 랙 보기</a>
<span style="flex:1 1 auto"></span>
@@ -0,0 +1,85 @@
{% extends "erp_base.html" %}
{% block head_extra %}
<style>
.rv-board { display:flex; flex-direction:column; gap:18px; }
.rv-side { width:100%; }
.rv-side h3 { text-align:center; margin:0 0 8px; }
.rv-row { display:grid; grid-template-columns:repeat(6, 1fr); gap:6px; margin-bottom:6px; }
.rv-cell {
border:1px solid #cbd5e1; border-radius:6px; padding:5px;
background:#f8fafc; min-height:80px; display:flex; flex-direction:column; min-width:0;
}
.rv-cell.is-empty { background:#fff; }
.rv-cell-head {
display:flex; align-items:center; justify-content:space-between;
font-size:11px; font-weight:600; color:#475569; margin-bottom:4px;
border-bottom:1px solid #e2e8f0; padding-bottom:3px;
}
.rv-cell-head .rv-total { color:#0f172a; white-space:nowrap; }
.rv-items { display:flex; flex-direction:column; gap:3px; }
.rv-item { font-size:11px; line-height:1.25; }
.rv-item .rv-name { font-weight:600; color:#0f172a; }
.rv-item .rv-calc { color:#475569; }
.rv-item .rv-qty { color:#2563eb; font-weight:600; }
.rv-empty-note { font-size:11px; color:#cbd5e1; }
</style>
{% endblock %}
{% block content %}
<section class="mys mys-compact">
{% include "malaysia/_i18n.html" %}
{% set active_tab = 'rack' %}
{% include "malaysia/_nav.html" %}
<div class="erp-card">
<div class="cpg-card-head" style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:8px;">
<h2>창고 랙 보기</h2>
<span class="erp-muted">
{% if stocktake %}
<span class="i18n">기준 재고조사</span>: #{{ stocktake.id }} · {{ stocktake.stocktake_date }}
{% if stocktake.status=='finalized' %}<span class="erp-badge erp-badge-success">확정</span>
{% elif stocktake.status=='cancelled' %}<span class="erp-badge erp-badge-neutral">취소</span>
{% else %}<span class="erp-badge erp-badge-inverse">작성중</span>{% endif %}
{% else %}
<span class="i18n">표시할 재고조사가 없습니다.</span>
{% endif %}
</span>
</div>
<div class="rv-board" style="margin-top:10px;">
{% for side in rack_layout %}
<div class="rv-side">
<h3>Side {{ side.side }}</h3>
{% for row in side.rows %}
<div class="rv-row">
{% for col in row.cols %}
{% for cell in col %}
{% set entries = rack_by_cell.get(cell, []) %}
{% set ns = namespace(total=0) %}
{% for e in entries %}{% set ns.total = ns.total + e.total %}{% endfor %}
<div class="rv-cell {% if not entries %}is-empty{% endif %}">
<div class="rv-cell-head">
<span>{{ cell }}</span>
<span class="rv-total">∑ {{ ns.total }}</span>
</div>
<div class="rv-items">
{% for e in entries %}
<div class="rv-item">
<span class="rv-name" data-en="{{ e.item_name }}" data-ko="{{ ko_names.get(e.sku_code, e.item_name) }}">{{ e.item_name }}</span><br>
<span class="rv-calc">{{ e.box_count }}<span class="i18n">박스</span> × {{ e.units_per_box }} = <span class="rv-qty">{{ e.total }}</span></span>
</div>
{% endfor %}
{% if not entries %}<span class="rv-empty-note"></span>{% endif %}
</div>
</div>
{% endfor %}
{% endfor %}
</div>
{% endfor %}
</div>
{% endfor %}
</div>
</div>
</section>
{% endblock %}
+4
View File
@@ -17,6 +17,10 @@
"재고 현황": "Stock Status",
"입출고": "In / Out",
"일일 재고조사": "Daily Stocktake",
"창고 랙 보기": "Rack View",
"기준 재고조사": "Based on stocktake",
"표시할 재고조사가 없습니다.": "No stocktake to display.",
"박스": "box",
"창고": "Warehouse",
"날짜": "Date",
"메모": "Memo",