35f849ba0c
연속된 같은 그룹은 같은 배경, 그룹이 바뀌면 배경을 번갈아 칠한다. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
297 lines
13 KiB
HTML
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 %}
|