Files
dbx-main/app/modules/cupang/templates/cupang/sales.html
T
king 35f849ba0c feat(cupang): 매출 표에 그룹 줄무늬 — 입고센터(센터 검색 시 출고일) 기준
연속된 같은 그룹은 같은 배경, 그룹이 바뀌면 배경을 번갈아 칠한다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 16:34:11 +09:00

297 lines
13 KiB
HTML

{% extends "erp_base.html" %}
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260904e" />{% endblock %}
{% block content %}
{# 쿠팡 로켓 매출 — 발주 라인별 공급가·원가·물류비·마진 #}
<section class="cpg cpg-sales">
<div class="erp-page-actions">
<a class="erp-btn erp-btn-primary" href="/cupang/">◀◀ 달력</a>
<label class="erp-btn erp-btn-outline cpg-sales-pick" for="cpg-sales-file">시트 업로드</label>
<input class="cpg-po-file" type="file" id="cpg-sales-file"
accept=".xlsx,.xlsm,.csv" multiple aria-label="매출 시트 선택" />
<button type="button" class="erp-btn erp-btn-outline" id="cpg-sales-add">+ 행 추가</button>
<a class="erp-btn cpg-boxno-dl" id="cpg-sales-dl" href="/cupang/sales/export.xlsx">엑셀 다운로드</a>
<span class="erp-muted" id="cpg-sales-msg"></span>
</div>
<!-- 조회 조건 -->
<form class="erp-card cpg-form-card cpg-sales-filter" method="get" action="/cupang/sales">
<label class="erp-field"><span>출고일 from</span>
<input class="erp-input" type="date" name="from" value="{{ filters.date_from }}" /></label>
<label class="erp-field"><span>to</span>
<input class="erp-input" type="date" name="to" value="{{ filters.date_to }}" /></label>
<label class="erp-field"><span>입고센터</span>
<select class="erp-select" name="center">
<option value="">전체</option>
{% for c in centers %}
<option value="{{ c }}" {% if c == filters.center_name %}selected{% endif %}>{{ c }}</option>
{% endfor %}
</select></label>
<label class="erp-field"><span>주차</span>
<select class="erp-select" name="week">
<option value="">전체</option>
{% if not weeks %}<option value="" disabled>등록된 주차 없음</option>{% endif %}
{% for w in weeks %}
<option value="{{ w }}" {% if w == filters.week_label %}selected{% endif %}>{{ w }}</option>
{% endfor %}
</select></label>
<label class="erp-field"><span>발주유형</span>
<select class="erp-select" name="type">
<option value="">전체</option>
{% for t in po_types %}
<option value="{{ t }}" {% if t == filters.po_type %}selected{% endif %}>{{ t }}</option>
{% endfor %}
</select></label>
<label class="erp-field cpg-sales-q"><span>검색</span>
<input class="erp-input" type="search" name="q" value="{{ filters.keyword }}"
placeholder="품목 · SKUID · 발주번호 · 바코드" /></label>
<div class="cpg-sales-filter-act">
<button type="submit" class="erp-btn erp-btn-primary">조회</button>
<a class="erp-btn erp-btn-outline" href="/cupang/sales">초기화</a>
</div>
</form>
<!---->
<div class="erp-card cpg-form-card cpg-sales-card">
<div class="erp-table-wrap cpg-sales-wrap">
<table class="erp-table cpg-sales-table">
<thead>
<tr>
<th>주차</th><th>순번</th><th>발주번호</th><th>유형</th>
<th>발주일</th><th>출고일</th><th>센터입고일</th>
<th>SKUID</th><th>바코드</th><th>품목</th><th>수량</th><th>입고센터</th>
<th>공급단가</th><th>공급가</th><th>원가단가</th><th>원가</th>
<th>피킹비</th><th>밀크런</th><th>물류비</th><th>물류비중</th>
<th>마진</th><th>마진율</th><th></th>
</tr>
</thead>
<tbody id="cpg-sales-rows">
{% for r in rows %}
<tr data-id="{{ r.id }}" data-row='{{ r | tojson }}'
class="{% if r.band %}is-band{% endif %}">
<td class="cpg-sales-week" title="{{ r.week_label }}">{{ r.week_label }}</td>
<td class="cpg-calc-num">{{ r.seq or '' }}</td>
<td class="cpg-sales-mono">{{ r.po_no }}</td>
<td>{{ r.po_type }}</td>
<td class="cpg-sales-mono">{{ r.order_date }}</td>
<td class="cpg-sales-mono">{{ r.ship_date }}</td>
<td class="cpg-sales-mono">{{ r.center_arrival_date }}</td>
<td class="cpg-sales-mono">{{ r.sku_id }}</td>
<td class="cpg-sales-mono">{{ r.barcode }}</td>
<td title="{{ r.item_name }}">{{ r.item_name }}</td>
<td class="cpg-calc-num">{{ "{:,}".format(r.quantity) }}</td>
<td>{{ r.center_name }}</td>
<td class="cpg-calc-num">{{ "{:,.0f}".format(r.supply_unit_price) }}</td>
<td class="cpg-calc-num">{{ "{:,.0f}".format(r.supply_amount) }}</td>
<td class="cpg-calc-num">{{ "{:,.0f}".format(r.cost_unit_price) }}</td>
<td class="cpg-calc-num">{{ "{:,.0f}".format(r.cost_amount) }}</td>
<td class="cpg-calc-num">{{ "{:,.0f}".format(r.picking_amount) }}</td>
<td class="cpg-calc-num">{{ "{:,.0f}".format(r.milkrun_amount) }}</td>
<td class="cpg-calc-num">{{ "{:,.0f}".format(r.logistics_total) }}</td>
<td class="cpg-calc-num">{{ r.logistics_ratio }}%</td>
<td class="cpg-calc-num">{{ "{:,.0f}".format(r.margin) }}</td>
<td class="cpg-calc-num">{{ r.margin_rate }}%</td>
<td class="cpg-sales-act">
<button type="button" class="cpg-icon-btn" data-edit="{{ r.id }}" title="수정"></button>
<button type="button" class="cpg-icon-btn is-danger" data-del="{{ r.id }}" title="삭제"></button>
</td>
</tr>
{% endfor %}
{% if not rows %}
<tr><td colspan="23" class="erp-muted">
{% if filters.week_label %}{{ filters.week_label }} 주차에 등록된 매출 자료가 없습니다.
{% else %}조회된 매출 자료가 없습니다. 시트를 업로드하거나 행을 추가하세요.{% endif %}
</td></tr>
{% endif %}
</tbody>
</table>
</div>
</div>
<!-- 행 추가/수정 -->
<div class="cpg-modal" id="cpg-sales-dlg" hidden>
<div class="cpg-modal-back" data-sales-close></div>
<div class="cpg-modal-box cpg-sales-box" role="dialog" aria-modal="true" aria-labelledby="cpg-sales-title">
<h3 id="cpg-sales-title">매출 행</h3>
<div class="cpg-sales-form" id="cpg-sales-form"></div>
<div class="cpg-dlg-actions">
<span class="erp-muted" id="cpg-sales-dlgmsg"></span>
<button type="button" class="erp-btn erp-btn-primary" id="cpg-sales-save">저장</button>
<button type="button" class="erp-btn erp-btn-outline" data-sales-close>취소</button>
</div>
</div>
</div>
<!-- 저장 중 -->
<div class="cpg-modal cpg-saving" id="cpg-sales-saving" hidden>
<div class="cpg-modal-back"></div>
<div class="cpg-modal-box cpg-saving-box" role="alertdialog" aria-live="assertive">
<div class="cpg-spinner" aria-hidden="true"></div>
<strong>처리 중…</strong>
<span class="erp-muted" id="cpg-sales-savingnote"></span>
</div>
</div>
<script>
// 매출 표 — 행 추가/수정/삭제, 시트 업로드.
(function () {
var msg = document.getElementById("cpg-sales-msg");
var dlg = document.getElementById("cpg-sales-dlg");
var form = document.getElementById("cpg-sales-form");
var dlgMsg = document.getElementById("cpg-sales-dlgmsg");
var saving = document.getElementById("cpg-sales-saving");
var savingNote = document.getElementById("cpg-sales-savingnote");
var tbody = document.getElementById("cpg-sales-rows");
if (!tbody) return;
// 입력 칸 정의 — [키, 라벨, 형식]
var FIELDS = [
["week_label", "주차", "text"],
["seq", "순번", "number"],
["po_no", "발주번호", "text"],
["po_type", "발주유형", "text"],
["order_date", "발주일", "date"],
["ship_date", "출고일", "date"],
["center_arrival_date", "센터입고일", "date"],
["sku_id", "SKUID", "text"],
["barcode", "바코드", "text"],
["item_name", "품목", "text"],
["quantity", "수량", "number"],
["center_name", "입고센터", "text"],
["supply_unit_price", "공급단가", "number"],
["supply_amount", "공급가", "number"],
["cost_unit_price", "원가(단가)", "number"],
["cost_amount", "원가(발주량)", "number"],
["picking_unit_price", "피킹비 단가", "number"],
["picking_amount", "피킹비 합계", "number"],
["milkrun_amount", "밀크런/쉽먼트", "number"],
["logistics_total", "물류비 합계", "number"],
["logistics_ratio", "물류비중(%)", "number"],
["margin", "마진", "number"],
["margin_rate", "마진율(%)", "number"],
["stock_deduct_memo", "재고차감 메모", "text"],
["memo", "메모", "text"]
];
var editing = null; // 수정 중인 id (없으면 신규)
function esc(s) { var d = document.createElement("div"); d.textContent = s == null ? "" : s; return d.innerHTML; }
function openDialog(row) {
editing = row && row.id ? row.id : null;
document.getElementById("cpg-sales-title").textContent = editing ? "매출 행 수정" : "매출 행 추가";
dlgMsg.textContent = "";
form.innerHTML = FIELDS.map(function (f) {
var v = row ? (row[f[0]] == null ? "" : row[f[0]]) : "";
return '<label class="erp-field"><span>' + esc(f[1]) + "</span>" +
'<input class="erp-input" type="' + f[2] + '" data-key="' + f[0] + '"' +
(f[2] === "number" ? ' step="any"' : "") +
' value="' + esc(v) + '" /></label>';
}).join("");
dlg.hidden = false;
}
function closeDialog() { dlg.hidden = true; }
function collect() {
var out = {};
form.querySelectorAll("[data-key]").forEach(function (el) {
out[el.getAttribute("data-key")] = el.value;
});
return out;
}
function showSaving(text) { savingNote.textContent = text || ""; saving.hidden = false; }
function hideSaving() { saving.hidden = true; }
document.getElementById("cpg-sales-add").addEventListener("click", function () {
openDialog(null);
});
tbody.addEventListener("click", function (e) {
var ed = e.target.closest("[data-edit]");
if (ed) {
var tr = ed.closest("tr");
var data = {};
try { data = JSON.parse(tr.getAttribute("data-row") || "{}"); } catch (err) { data = {}; }
openDialog(data);
return;
}
var del = e.target.closest("[data-del]");
if (del) {
var id = del.getAttribute("data-del");
if (!window.confirm("이 행을 삭제할까요?")) return;
showSaving("삭제 중");
fetch("/cupang/sales/api/" + id + "/delete", { method: "POST" })
.then(function (r) { if (!r.ok) throw new Error("http " + r.status); return r.json(); })
.then(function () { window.location.reload(); })
.catch(function () { hideSaving(); msg.textContent = "삭제 실패"; });
}
});
document.getElementById("cpg-sales-save").addEventListener("click", function () {
var payload = collect();
var url = editing ? "/cupang/sales/api/" + editing : "/cupang/sales/api";
dlgMsg.textContent = "저장 중…";
fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
})
.then(function (r) {
if (!r.ok) {
return r.json().catch(function () { return {}; }).then(function (e) {
throw new Error(e.detail || "http " + r.status);
});
}
return r.json();
})
.then(function () { window.location.reload(); })
.catch(function (err) { dlgMsg.textContent = (err && err.message) || "저장 실패"; });
});
Array.prototype.forEach.call(dlg.querySelectorAll("[data-sales-close]"), function (el) {
el.addEventListener("click", closeDialog);
});
document.addEventListener("keydown", function (e) {
if (e.key === "Escape" && !dlg.hidden) closeDialog();
});
// 시트 업로드 — 파일을 고르면 바로 올린다.
var file = document.getElementById("cpg-sales-file");
if (file) {
file.addEventListener("change", function () {
if (!file.files || !file.files.length) return;
var fd = new FormData();
Array.prototype.forEach.call(file.files, function (f) { fd.append("files", f); });
showSaving(file.files.length + "개 파일 읽는 중");
fetch("/cupang/sales/upload", { method: "POST", body: fd })
.then(function (r) {
return r.json().catch(function () { return {}; }).then(function (d) {
if (!r.ok) throw new Error(d.detail || "업로드 실패 (http " + r.status + ")");
return d;
});
})
.then(function (d) {
showSaving("저장 " + d.saved + "건 · 주차 " + d.weekly + "건 — 새로고침");
window.location.reload();
})
.catch(function (err) {
hideSaving();
msg.textContent = (err && err.message) || "업로드 실패";
});
});
}
// 엑셀 다운로드는 현재 조회 조건 그대로
var dl = document.getElementById("cpg-sales-dl");
if (dl && window.location.search) {
dl.href = "/cupang/sales/export.xlsx" + window.location.search;
}
})();
</script>
</section>
{% endblock %}