feat(cupang): 달력 월 표기 확대, 출고 보기 ②를 상자 번호별 내용물 목록으로
- 달력 월 이름/연도 글자 크기 2배·굵게 - 출고 상세 ② 상자 요약 → ② 상자 목록: 전체 상자에 1번부터 번호를 붙이고 상자마다 담긴 제품·수량을 표시. box_plan 스냅샷 우선, 없으면 계산 결과로 대체 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -425,6 +425,7 @@ async def detail(request: Request, shipment_id: int) -> HTMLResponse:
|
|||||||
"shipment": ship,
|
"shipment": ship,
|
||||||
"items": items,
|
"items": items,
|
||||||
"calc": calc,
|
"calc": calc,
|
||||||
|
"box_list": _expand_box_list(ship, calc, items),
|
||||||
"total_qty": total_qty,
|
"total_qty": total_qty,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -1052,6 +1053,81 @@ def _compute_boxes(store: Any, raw_items: list[Any]) -> dict[str, Any]:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _expand_box_list(
|
||||||
|
shipment: dict[str, Any], calc: dict[str, Any], items: list[dict[str, Any]]
|
||||||
|
) -> list[dict[str, Any]]:
|
||||||
|
"""전체 상자를 1번부터 번호 붙여 펼친다 — 상자마다 무엇이 몇 개 들었는지.
|
||||||
|
|
||||||
|
확정 당시 스냅샷(`box_plan`)이 있으면 그것을 그대로 펼치고,
|
||||||
|
없는 예전 데이터는 계산 결과(`calc`)로 대신 만든다.
|
||||||
|
"""
|
||||||
|
box_name_by_code = {
|
||||||
|
r["product_code"]: (r.get("box_name") or "") for r in calc.get("results", [])
|
||||||
|
}
|
||||||
|
out: list[dict[str, Any]] = []
|
||||||
|
|
||||||
|
plan = shipment.get("box_plan") or []
|
||||||
|
if plan:
|
||||||
|
for entry in plan:
|
||||||
|
count = int(entry.get("count") or 0)
|
||||||
|
units = int(entry.get("units") or 0)
|
||||||
|
if count <= 0:
|
||||||
|
continue
|
||||||
|
if entry.get("kind") == "mix":
|
||||||
|
contents = [
|
||||||
|
{
|
||||||
|
"product_name": it.get("product_name") or it.get("product_code") or "",
|
||||||
|
"quantity": int(it.get("quantity") or 0),
|
||||||
|
}
|
||||||
|
for it in (entry.get("items") or [])
|
||||||
|
]
|
||||||
|
box_name = entry.get("box_type") or ""
|
||||||
|
for _ in range(count):
|
||||||
|
out.append({"kind": "mix", "box_name": box_name, "items": contents})
|
||||||
|
else:
|
||||||
|
code = str(entry.get("product_code") or "")
|
||||||
|
contents = [
|
||||||
|
{"product_name": entry.get("name") or code, "quantity": units}
|
||||||
|
]
|
||||||
|
box_name = box_name_by_code.get(code, "")
|
||||||
|
for _ in range(count):
|
||||||
|
out.append({"kind": "product", "box_name": box_name, "items": contents})
|
||||||
|
else:
|
||||||
|
for r in calc.get("results", []):
|
||||||
|
if not r.get("configured"):
|
||||||
|
continue
|
||||||
|
for _ in range(int(r.get("full_boxes") or 0)):
|
||||||
|
out.append(
|
||||||
|
{
|
||||||
|
"kind": "product",
|
||||||
|
"box_name": r.get("box_name") or "",
|
||||||
|
"items": [
|
||||||
|
{"product_name": r["product_name"], "quantity": r["units_per_box"]}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
for mix in calc.get("mixes", []):
|
||||||
|
for box in mix.get("boxes", []):
|
||||||
|
out.append(
|
||||||
|
{
|
||||||
|
"kind": "mix",
|
||||||
|
"box_name": mix.get("box_name") or "",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"product_name": it.get("product_name") or "",
|
||||||
|
"quantity": int(it.get("quantity") or 0),
|
||||||
|
}
|
||||||
|
for it in (box.get("items") or [])
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
for idx, box in enumerate(out, start=1):
|
||||||
|
box["no"] = idx
|
||||||
|
box["quantity"] = sum(int(it["quantity"]) for it in box["items"])
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
def _pack_leftovers(results: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
def _pack_leftovers(results: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||||
"""제품별 자투리(remainder_units)를 같은 상자명끼리 모아 혼합 상자에 담는다.
|
"""제품별 자투리(remainder_units)를 같은 상자명끼리 모아 혼합 상자에 담는다.
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{% extends "erp_base.html" %}
|
{% extends "erp_base.html" %}
|
||||||
|
|
||||||
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260902b" />{% endblock %}
|
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260903a" />{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<section class="cpg">
|
<section class="cpg">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{% extends "erp_base.html" %}
|
{% extends "erp_base.html" %}
|
||||||
|
|
||||||
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260902b" />{% endblock %}
|
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260903a" />{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<section class="cpg">
|
<section class="cpg">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{% extends "erp_base.html" %}
|
{% extends "erp_base.html" %}
|
||||||
|
|
||||||
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260902b" />{% endblock %}
|
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260903a" />{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<section class="cpg">
|
<section class="cpg">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{% extends "erp_base.html" %}
|
{% extends "erp_base.html" %}
|
||||||
|
|
||||||
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260902b" />{% endblock %}
|
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260903a" />{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<section class="cpg">
|
<section class="cpg">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{% extends "erp_base.html" %}
|
{% extends "erp_base.html" %}
|
||||||
|
|
||||||
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260902b" />{% endblock %}
|
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260903a" />{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<section class="cpg">
|
<section class="cpg">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{% extends "erp_base.html" %}
|
{% extends "erp_base.html" %}
|
||||||
|
|
||||||
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260902b" />{% endblock %}
|
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260903a" />{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{# 출고 묶음 보기 — 상자 계산 화면과 같은 3열 구성. 읽기 전용(수정 없음). #}
|
{# 출고 묶음 보기 — 상자 계산 화면과 같은 3열 구성. 읽기 전용(수정 없음). #}
|
||||||
@@ -44,18 +44,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ② 상자 요약 -->
|
<!-- ② 상자 목록 -->
|
||||||
<div class="erp-card cpg-form-card cpg-calc-sum">
|
<div class="erp-card cpg-form-card cpg-calc-sum">
|
||||||
<div class="cpg-card-head">
|
<div class="cpg-card-head">
|
||||||
<h2>② 상자 요약</h2>
|
<h2>② 상자 목록</h2>
|
||||||
<span class="erp-muted">확정 당시 수량 기준으로 다시 계산한 값</span>
|
<span class="erp-muted">상자마다 담긴 제품과 수량</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="cpg-sum-body">
|
<div id="cpg-sum-body">
|
||||||
<div class="cpg-sum-kpis">
|
<div class="cpg-sum-kpis">
|
||||||
<div class="cpg-kpi">
|
<div class="cpg-kpi">
|
||||||
<span class="cpg-kpi-label">총 상자</span>
|
<span class="cpg-kpi-label">총 상자</span>
|
||||||
<strong class="cpg-kpi-value">{{ calc.grand_total_boxes }}상자</strong>
|
<strong class="cpg-kpi-value">{{ box_list|length }}상자</strong>
|
||||||
<span class="cpg-kpi-hint">제품별 + 혼합</span>
|
<span class="cpg-kpi-hint">제품별 + 혼합</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="cpg-kpi">
|
<div class="cpg-kpi">
|
||||||
@@ -71,53 +71,26 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="cpg-sum-scroll">
|
<div class="cpg-sum-scroll">
|
||||||
<h3 class="cpg-sum-h3">제품별 상자</h3>
|
<ul class="cpg-boxno-list">
|
||||||
<div class="cpg-sum-prods">
|
{% for b in box_list %}
|
||||||
{% for r in calc.results if r.configured %}
|
<li class="cpg-boxno{% if b.kind == 'mix' %} is-mix{% endif %}">
|
||||||
<div class="cpg-sum-prod is-done">
|
<div class="cpg-boxno-head">
|
||||||
<div class="cpg-sum-prod-head">
|
<span class="cpg-boxno-no">{{ b.no }}</span>
|
||||||
<span class="cpg-sum-prod-name">{{ r.product_name }}</span>
|
<span class="cpg-boxno-name">{{ b.box_name or '상자' }}</span>
|
||||||
|
{% if b.kind == 'mix' %}<span class="cpg-boxno-tag">혼합</span>{% endif %}
|
||||||
|
<span class="cpg-boxno-qty">{{ b.quantity }}개</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="cpg-sum-prod-nums">
|
<ul class="cpg-boxno-items">
|
||||||
<strong>{{ r.full_boxes }}상자</strong>
|
{% for it in b['items'] %}
|
||||||
<span class="erp-muted">{{ r.quantity }}개 · 입수량 {{ r.units_per_box }}</span>
|
<li><span>{{ it.product_name }}</span><b>{{ it.quantity }}개</b></li>
|
||||||
{% if r.remainder_units %}
|
{% endfor %}
|
||||||
<span class="cpg-sum-left">자투리 {{ r.remainder_units }}개</span>
|
</ul>
|
||||||
{% else %}
|
</li>
|
||||||
<span class="cpg-sum-exact">딱 맞음</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% set unconfigured = calc.results | rejectattr('configured') | list %}
|
{% if not box_list %}
|
||||||
{% if unconfigured %}
|
<li class="erp-muted">상자 정보가 없습니다.</li>
|
||||||
<p class="erp-muted">상자 입수량 미설정:
|
|
||||||
{% for r in unconfigured %}{{ r.product_name }}{% if not loop.last %}, {% endif %}{% endfor %}
|
|
||||||
</p>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</ul>
|
||||||
|
|
||||||
<h3 class="cpg-sum-h3">자투리 혼합 상자
|
|
||||||
<span class="erp-muted">{% if not calc.mixes %}— 자투리 없음{% else %}— 1장 = 1상자{% endif %}</span>
|
|
||||||
</h3>
|
|
||||||
<div class="cpg-sum-boxes">
|
|
||||||
{% for m in calc.mixes %}
|
|
||||||
{% for b in m.boxes %}
|
|
||||||
<div class="cpg-box-card is-done">
|
|
||||||
<div class="cpg-box-info">
|
|
||||||
<div class="cpg-box-title">{{ m.box_name }} 혼합 #{{ loop.index }}</div>
|
|
||||||
<ul class="cpg-box-items">
|
|
||||||
{% for it in b['items'] %}
|
|
||||||
<li><span>{{ it.product_name }}</span><b>{{ it.quantity }}개</b></li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
<div class="cpg-box-bar"><span style="width:{{ b.fill_percent }}%"></span></div>
|
|
||||||
<div class="cpg-box-fillnum">{{ b.fill_percent }}% 채움</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+40
-2
@@ -1181,8 +1181,8 @@ body.cpg-dragging { cursor: grabbing; user-select: none; }
|
|||||||
display: flex; align-items: baseline; gap: 6px;
|
display: flex; align-items: baseline; gap: 6px;
|
||||||
padding: 0 2px 8px;
|
padding: 0 2px 8px;
|
||||||
}
|
}
|
||||||
.cpg-mcal-mname { font-size: 15px; font-weight: 700; letter-spacing: -0.2px; }
|
.cpg-mcal-mname { font-size: 30px; font-weight: 800; letter-spacing: -0.5px; }
|
||||||
.cpg-mcal-myear { font-size: 12px; color: var(--color-midtone-gray); font-family: var(--font-geist-mono); }
|
.cpg-mcal-myear { font-size: 24px; font-weight: 800; color: var(--color-midtone-gray); font-family: var(--font-geist-mono); }
|
||||||
.cpg-mcal-total {
|
.cpg-mcal-total {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
font-size: 11px; padding: 2px 8px; border-radius: 999px;
|
font-size: 11px; padding: 2px 8px; border-radius: 999px;
|
||||||
@@ -1399,6 +1399,44 @@ body.cpg-dragging { cursor: grabbing; user-select: none; }
|
|||||||
.cpg-calc-card .cpg-calc-table .cpg-calc-qty { width: 70px; max-width: 70px; margin-left: auto; }
|
.cpg-calc-card .cpg-calc-table .cpg-calc-qty { width: 70px; max-width: 70px; margin-left: auto; }
|
||||||
|
|
||||||
/* 출고 보기(읽기 전용) — ③ 하단 메타 정보 */
|
/* 출고 보기(읽기 전용) — ③ 하단 메타 정보 */
|
||||||
|
/* ── 출고 보기 ② 상자 목록 — 상자마다 번호 + 내용물 ── */
|
||||||
|
.cpg-boxno-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; }
|
||||||
|
.cpg-boxno {
|
||||||
|
border: 1px solid var(--color-cloud-gray);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
.cpg-boxno.is-mix { border-color: var(--color-deep-black); }
|
||||||
|
.cpg-boxno-head { display: flex; align-items: center; gap: 6px; }
|
||||||
|
.cpg-boxno-no {
|
||||||
|
display: inline-flex; align-items: center; justify-content: center;
|
||||||
|
min-width: 24px; height: 24px; padding: 0 6px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--color-deep-black); color: #fff;
|
||||||
|
font-size: 12px; font-weight: 700; font-family: var(--font-geist-mono);
|
||||||
|
}
|
||||||
|
.cpg-boxno-name { font-size: 13px; font-weight: 600; }
|
||||||
|
.cpg-boxno-tag {
|
||||||
|
font-size: 10px; padding: 1px 6px; border-radius: 999px;
|
||||||
|
background: var(--color-ghost-gray); color: var(--color-midtone-gray);
|
||||||
|
}
|
||||||
|
.cpg-boxno-qty {
|
||||||
|
margin-left: auto;
|
||||||
|
font-size: 12px; color: var(--color-midtone-gray);
|
||||||
|
font-family: var(--font-geist-mono);
|
||||||
|
}
|
||||||
|
.cpg-boxno-items { list-style: none; margin: 6px 0 0; padding: 0 0 0 30px; }
|
||||||
|
.cpg-boxno-items li {
|
||||||
|
display: flex; align-items: baseline; gap: 8px;
|
||||||
|
padding: 3px 0;
|
||||||
|
font-size: 12px;
|
||||||
|
border-bottom: 1px dashed var(--color-cloud-gray);
|
||||||
|
}
|
||||||
|
.cpg-boxno-items li:last-child { border-bottom: 0; }
|
||||||
|
.cpg-boxno-items li span { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.cpg-boxno-items li b { flex: 0 0 auto; font-family: var(--font-geist-mono); }
|
||||||
|
|
||||||
.cpg-view-meta {
|
.cpg-view-meta {
|
||||||
margin: 12px 0 0; padding: 10px 12px;
|
margin: 12px 0 0; padding: 10px 12px;
|
||||||
border: 1px solid var(--color-subtle-ash); border-radius: 10px;
|
border: 1px solid var(--color-subtle-ash); border-radius: 10px;
|
||||||
|
|||||||
Reference in New Issue
Block a user