@@ -22,6 +22,12 @@
{% else %}
+
+
+
@@ -359,16 +365,116 @@
});
}
+ // ── 출고일별 작업 세트 ────────────────────────────
+ // 업로드한 발주서의 출고일이 다르면 날짜마다 ①②③ 상태를 따로 들고,
+ // 탭으로 전환한다. 확정은 현재 보고 있는 날짜만 대상으로 한다.
+ var sets = {}; // "YYYY-MM-DD" -> 상태 스냅샷
+ var dateOrder = []; // 탭 순서
+ var activeDate = ""; // "" = 업로드 전(수동 입력 모드)
+ var tabsBar = document.getElementById("cpg-date-tabs");
+ var tabsList = document.getElementById("cpg-date-tabs-list");
+
+ function centersOf(allocObj) {
+ return Object.keys(allocObj || {}).filter(function (cid) {
+ return (allocObj[cid] || []).length > 0;
+ });
+ }
+
+ function boxesOf(allocObj) {
+ var n = 0;
+ Object.keys(allocObj || {}).forEach(function (cid) {
+ (allocObj[cid] || []).forEach(function (a) { n += a.count; });
+ });
+ return n;
+ }
+
+ function renderTabs() {
+ if (!tabsBar) return;
+ if (dateOrder.length < 1) { tabsBar.hidden = true; tabsList.innerHTML = ""; return; }
+ tabsBar.hidden = false;
+ tabsList.innerHTML = dateOrder.map(function (d) {
+ var st = sets[d] || {};
+ var cn = centersOf(st.alloc).length;
+ var bx = boxesOf(st.alloc);
+ return '";
+ }).join("");
+ }
+
+ // 현재 화면 상태를 활성 날짜 세트에 저장
+ function saveActive() {
+ if (!activeDate) return;
+ sets[activeDate] = {
+ items: collect(),
+ calc: { results: calc.results, mixes: calc.mixes },
+ units: units, labels: labels, contents: contents,
+ openCenters: openCenters.slice(), alloc: alloc, methods: methods,
+ stamped: stamped, allStamped: allStamped
+ };
+ }
+
+ // 날짜 세트를 화면(①②③)에 올린다
+ function loadDate(d) {
+ var st = sets[d];
+ if (!st) return;
+ activeDate = d;
+ uploadShipDate = d;
+ calc.results = st.calc.results;
+ calc.mixes = st.calc.mixes;
+ units = st.units; labels = st.labels; contents = st.contents;
+ openCenters = st.openCenters.slice();
+ alloc = st.alloc; methods = st.methods;
+ stamped = st.stamped; allStamped = st.allStamped;
+
+ tbody.innerHTML = "";
+ (st.items || []).forEach(function (it) { addRow(it.product_code, it.quantity); });
+ if (!(st.items || []).length) addRow();
+
+ render();
+ renderTabs();
+ }
+
+ if (tabsList) {
+ tabsList.addEventListener("click", function (e) {
+ var btn = e.target.closest("[data-date]");
+ if (!btn) return;
+ var d = btn.getAttribute("data-date");
+ if (d === activeDate) return;
+ saveActive();
+ loadDate(d);
+ msg.textContent = "출고일 " + (d || "미확인") + " 작업으로 전환했습니다.";
+ });
+ }
+
// 센터별 계산 결과를 하나의 ② 요약으로 합치고, 각 박스를 그 센터에 자동 배분한다.
- function applyUploaded(groups, perCenter) {
+ // (한 출고일 안에서만 합친다 — 박스는 센터 단위로 포장)
+ function buildDataset(groups, perCenter) {
var mergedProds = {}; // code -> 합산 결과
var mergedMixes = {}; // box_name -> {box_name, boxes:[]}
- var autoAlloc = []; // [{cid, key, count}]
- var mixOwner = {}; // mixKey -> 센터명
+ var st = {
+ items: [], calc: { results: [], mixes: [] },
+ units: {}, labels: {}, contents: {},
+ openCenters: [], alloc: {}, methods: {},
+ stamped: {}, allStamped: false
+ };
+ var mixOwner = {};
+
+ function put(cid, key, count) {
+ if (count <= 0) return;
+ st.alloc[cid] = st.alloc[cid] || [];
+ var same = st.alloc[cid].filter(function (a) { return a.key === key; })[0];
+ if (same) { same.count += count; return; }
+ seq += 1;
+ st.alloc[cid].push({ id: seq, key: key, count: count });
+ }
groups.forEach(function (g, gi) {
var cid = String(g.center_id);
var data = perCenter[gi] || {};
+ if (st.openCenters.indexOf(cid) < 0) st.openCenters.push(cid);
(data.results || []).filter(function (r) { return r.configured; }).forEach(function (r) {
var m = mergedProds[r.product_code];
if (!m) {
@@ -383,9 +489,7 @@
m.full_boxes += r.full_boxes;
m.remainder_units += r.remainder_units;
m.required_boxes += r.required_boxes;
- if (r.full_boxes > 0) {
- autoAlloc.push({ cid: cid, key: prodKey(r.product_code), count: r.full_boxes });
- }
+ put(cid, prodKey(r.product_code), r.full_boxes);
});
(data.mixes || []).forEach(function (mx) {
var bucket = mergedMixes[mx.box_name];
@@ -395,50 +499,39 @@
bucket.boxes.push(b);
var k = mixKey(mx.box_name, idx);
mixOwner[k] = g.center_name;
- autoAlloc.push({ cid: cid, key: k, count: 1 });
+ put(cid, k, 1);
});
});
});
- calc.results = Object.keys(mergedProds).map(function (k) { return mergedProds[k]; });
- calc.mixes = Object.keys(mergedMixes).map(function (k) { return mergedMixes[k]; });
+ st.calc.results = Object.keys(mergedProds).map(function (k) { return mergedProds[k]; });
+ st.calc.mixes = Object.keys(mergedMixes).map(function (k) { return mergedMixes[k]; });
- units = {}; labels = {}; contents = {};
- calc.results.forEach(function (r) {
- units[prodKey(r.product_code)] = r.units_per_box;
- labels[prodKey(r.product_code)] = r.product_name;
+ st.calc.results.forEach(function (r) {
+ st.units[prodKey(r.product_code)] = r.units_per_box;
+ st.labels[prodKey(r.product_code)] = r.product_name;
});
- calc.mixes.forEach(function (m) {
+ st.calc.mixes.forEach(function (m) {
m.boxes.forEach(function (b, i) {
var n = 0;
b.items.forEach(function (it) { n += it.quantity; });
var k = mixKey(m.box_name, i);
- units[k] = n;
- labels[k] = m.box_name + " 혼합 #" + (i + 1) +
- (mixOwner[k] ? " · " + mixOwner[k] : "");
- contents[k] = b.items.map(function (it) {
+ st.units[k] = n;
+ st.labels[k] = m.box_name + " 혼합 #" + (i + 1) +
+ (mixOwner[k] ? " · " + mixOwner[k] : "");
+ st.contents[k] = b.items.map(function (it) {
return { product_code: it.product_code, product_name: it.product_name, quantity: it.quantity };
});
});
});
- // ③ 센터 열고 자동 배분
- alloc = {}; stamped = {}; allStamped = false;
- openCenters = [];
- groups.forEach(function (g) {
- var cid = String(g.center_id);
- if (openCenters.indexOf(cid) < 0) openCenters.push(cid);
- });
- autoAlloc.forEach(function (a) { addAlloc(a.cid, a.key, a.count); });
-
- // ① 표에는 제품별 합계를 채운다(수정 후 재계산 가능).
- tbody.innerHTML = "";
- calc.results.slice().sort(function (a, b) {
+ st.items = st.calc.results.slice().sort(function (a, b) {
return String(a.product_name).localeCompare(String(b.product_name), "ko");
- }).forEach(function (r) { addRow(r.product_code, r.quantity); });
- if (!calc.results.length) addRow();
+ }).map(function (r) {
+ return { product_code: r.product_code, quantity: r.quantity };
+ });
- render();
+ return st;
}
function renderPoResult(data, skipped) {
@@ -448,6 +541,12 @@
'파일 ' + ((data.files || []).length) + "개" +
'센터 ' + ((data.centers || []).length) + "곳" +
"
";
+ if ((data.groups || []).length > 1) {
+ html += '' + data.groups.map(function (g) {
+ return "- " + esc(g.ship_date || "날짜 미확인") + "" +
+ "센터 " + ((g.centers || []).length) + "곳 · " + (g.quantity || 0) + "개
";
+ }).join("") + "
";
+ }
html += '';
(data.files || []).forEach(function (f) {
html += "- " + esc(f.filename) + "" +
@@ -484,28 +583,63 @@
});
})
.then(function (data) {
- uploadShipDate = data.ship_date || "";
- var groups = (data.centers || []).filter(function (g) { return g.center_id != null; });
- var skipped = (data.centers || []).filter(function (g) { return g.center_id == null; })
- .map(function (g) { return g.center_name; });
- if (!groups.length) {
- renderPoResult(data, skipped);
+ var dateGroups = (data.groups || []).map(function (g) {
+ return {
+ ship_date: g.ship_date || "",
+ centers: (g.centers || []).filter(function (c) { return c.center_id != null; }),
+ skipped: (g.centers || []).filter(function (c) { return c.center_id == null; })
+ .map(function (c) { return c.center_name; })
+ };
+ }).filter(function (g) { return g.centers.length; });
+
+ var allSkipped = [];
+ (data.groups || []).forEach(function (g) {
+ (g.centers || []).forEach(function (c) {
+ if (c.center_id == null && allSkipped.indexOf(c.center_name) < 0) {
+ allSkipped.push(c.center_name);
+ }
+ });
+ });
+
+ if (!dateGroups.length) {
+ renderPoResult(data, allSkipped);
poMsg.textContent = "배분할 수 있는 센터가 없습니다.";
return;
}
- return Promise.all(groups.map(function (g) {
- return calcForItems(g.items.map(function (it) {
- return { product_code: it.product_code, quantity: it.quantity };
- }));
- })).then(function (perCenter) {
- applyUploaded(groups, perCenter);
- renderPoResult(data, skipped);
- var pieces = 0;
- groups.forEach(function (g) {
- g.items.forEach(function (it) { pieces += it.quantity; });
+
+ // 날짜 × 센터 단위로 박스 계산 (박스는 센터별로 포장되므로)
+ var jobs = [];
+ dateGroups.forEach(function (g) {
+ g.centers.forEach(function (c) {
+ jobs.push(calcForItems(c.items.map(function (it) {
+ return { product_code: it.product_code, quantity: it.quantity };
+ })));
});
- poMsg.textContent = "분석 완료 — 센터 " + groups.length + "곳 · " + pieces + "개 자동 배분";
- msg.textContent = "업로드 결과로 계산했습니다. 센터마다 출고방식을 고르면 확정할 수 있습니다.";
+ });
+
+ return Promise.all(jobs).then(function (all) {
+ var at = 0;
+ sets = {}; dateOrder = []; activeDate = "";
+ dateGroups.forEach(function (g) {
+ var per = all.slice(at, at + g.centers.length);
+ at += g.centers.length;
+ sets[g.ship_date] = buildDataset(g.centers, per);
+ dateOrder.push(g.ship_date);
+ });
+ loadDate(dateOrder[0]);
+ renderPoResult(data, allSkipped);
+ var pieces = 0, centerCount = 0;
+ dateGroups.forEach(function (g) {
+ centerCount += g.centers.length;
+ g.centers.forEach(function (c) {
+ c.items.forEach(function (it) { pieces += it.quantity; });
+ });
+ });
+ poMsg.textContent = "분석 완료 — 출고일 " + dateOrder.length + "개 · 센터 " +
+ centerCount + "곳 · " + pieces + "개 자동 배분";
+ msg.textContent = dateOrder.length > 1
+ ? "출고일이 " + dateOrder.length + "개입니다. 위 탭에서 날짜를 골라 확정하세요."
+ : "업로드 결과로 계산했습니다. 센터마다 출고방식을 고르면 확정할 수 있습니다.";
});
})
.catch(function (err) { poMsg.textContent = (err && err.message) || "업로드 실패"; })
@@ -1158,6 +1292,20 @@
})
.then(function (data) {
var d = (data && data.ship_date) || picked;
+ // 업로드한 출고일이 여러 개면 남은 날짜 작업을 이어서 한다.
+ var idx = dateOrder.indexOf(activeDate);
+ if (idx >= 0 && dateOrder.length > 1) {
+ var done = activeDate;
+ dateOrder.splice(idx, 1);
+ delete sets[done];
+ closeConfirm();
+ activeDate = "";
+ loadDate(dateOrder[Math.min(idx, dateOrder.length - 1)]);
+ msg.textContent = done + " 출고 확정 완료 — 남은 출고일 " + dateOrder.length + "개";
+ poMsg.textContent = done + " 확정됨. 남은 날짜: " + dateOrder.join(", ");
+ cfmOk.disabled = false;
+ return;
+ }
var parts = d.split("-");
// 달력으로 이동해 방금 만든 출고 묶음을 보여준다.
window.location.href = "/cupang/?year=" + parts[0] + "&month=" + parseInt(parts[1], 10) + "&date=" + d;
diff --git a/app/modules/cupang/templates/cupang/box_rules.html b/app/modules/cupang/templates/cupang/box_rules.html
index c4ee099..7703d05 100644
--- a/app/modules/cupang/templates/cupang/box_rules.html
+++ b/app/modules/cupang/templates/cupang/box_rules.html
@@ -1,6 +1,6 @@
{% extends "erp_base.html" %}
-{% block head_extra %}{% endblock %}
+{% block head_extra %}{% endblock %}
{% block content %}
diff --git a/app/modules/cupang/templates/cupang/centers.html b/app/modules/cupang/templates/cupang/centers.html
index b6be68e..aaf034b 100644
--- a/app/modules/cupang/templates/cupang/centers.html
+++ b/app/modules/cupang/templates/cupang/centers.html
@@ -1,6 +1,6 @@
{% extends "erp_base.html" %}
-{% block head_extra %}{% endblock %}
+{% block head_extra %}{% endblock %}
{% block content %}
diff --git a/app/modules/cupang/templates/cupang/detail.html b/app/modules/cupang/templates/cupang/detail.html
index ca0110f..7be3f8d 100644
--- a/app/modules/cupang/templates/cupang/detail.html
+++ b/app/modules/cupang/templates/cupang/detail.html
@@ -1,6 +1,6 @@
{% extends "erp_base.html" %}
-{% block head_extra %}{% endblock %}
+{% block head_extra %}{% endblock %}
{% block content %}
diff --git a/app/modules/cupang/templates/cupang/form.html b/app/modules/cupang/templates/cupang/form.html
index f192ddb..675582d 100644
--- a/app/modules/cupang/templates/cupang/form.html
+++ b/app/modules/cupang/templates/cupang/form.html
@@ -1,6 +1,6 @@
{% extends "erp_base.html" %}
-{% block head_extra %}{% endblock %}
+{% block head_extra %}{% endblock %}
{% block content %}
diff --git a/app/modules/cupang/templates/cupang/index.html b/app/modules/cupang/templates/cupang/index.html
index 901d8b3..cdf93f4 100644
--- a/app/modules/cupang/templates/cupang/index.html
+++ b/app/modules/cupang/templates/cupang/index.html
@@ -1,6 +1,6 @@
{% extends "erp_base.html" %}
-{% block head_extra %}{% endblock %}
+{% block head_extra %}{% endblock %}
{% block content %}
diff --git a/app/modules/cupang/templates/cupang/products.html b/app/modules/cupang/templates/cupang/products.html
index 77ebc1c..9a917f2 100644
--- a/app/modules/cupang/templates/cupang/products.html
+++ b/app/modules/cupang/templates/cupang/products.html
@@ -1,6 +1,6 @@
{% extends "erp_base.html" %}
-{% block head_extra %}{% endblock %}
+{% block head_extra %}{% endblock %}
{% block content %}
diff --git a/app/static/cupang.css b/app/static/cupang.css
index 230b990..c155254 100644
--- a/app/static/cupang.css
+++ b/app/static/cupang.css
@@ -1301,3 +1301,37 @@ body.cpg-dragging { cursor: grabbing; user-select: none; }
color: var(--color-callout-red);
}
.cpg-po-warn > li { padding: 2px 0; }
+
+/* ── 출고일 탭 (업로드한 발주서의 출고일이 여러 개일 때) ── */
+.cpg-date-tabs {
+ display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
+ margin-bottom: 12px;
+}
+.cpg-date-tabs[hidden] { display: none; }
+.cpg-date-tabs-label {
+ font-size: 12px; color: var(--color-midtone-gray); font-weight: 600;
+}
+.cpg-date-tabs-list { display: flex; gap: 8px; flex-wrap: wrap; }
+.cpg-date-tab {
+ display: flex; flex-direction: column; gap: 2px; align-items: flex-start;
+ padding: 6px 14px;
+ border: 1px solid var(--color-subtle-ash); border-radius: 10px;
+ background: #fff; cursor: pointer; font: inherit; text-align: left;
+ transition: background .12s, border-color .12s;
+}
+.cpg-date-tab:hover { border-color: var(--color-midtone-gray); background: var(--color-ghost-gray); }
+.cpg-date-tab > strong { font-size: 14px; font-family: var(--font-geist-mono); }
+.cpg-date-tab > span { font-size: 11px; color: var(--color-midtone-gray); }
+.cpg-date-tab.is-active {
+ background: var(--color-deep-black); border-color: var(--color-deep-black); color: #fff;
+}
+.cpg-date-tab.is-active > span { color: #c9c9c9; }
+
+/* 업로드 결과 — 출고일별 요약 */
+.cpg-po-dates { list-style: none; margin: 0 0 8px; padding: 0; font-size: 12px; }
+.cpg-po-dates > li {
+ display: flex; gap: 8px; align-items: baseline;
+ padding: 3px 0;
+}
+.cpg-po-dates > li > b { font-family: var(--font-geist-mono); }
+.cpg-po-dates > li > span { color: var(--color-midtone-gray); }