|
|
|
@@ -1,6 +1,6 @@
|
|
|
|
|
{% extends "erp_base.html" %}
|
|
|
|
|
|
|
|
|
|
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260901i" />{% endblock %}
|
|
|
|
|
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260901j" />{% endblock %}
|
|
|
|
|
|
|
|
|
|
{% block content %}
|
|
|
|
|
<section class="cpg">
|
|
|
|
@@ -22,6 +22,12 @@
|
|
|
|
|
</div>
|
|
|
|
|
{% else %}
|
|
|
|
|
|
|
|
|
|
<!-- 업로드한 발주서의 출고일이 여러 개면 날짜별로 작업 영역을 나눈다 -->
|
|
|
|
|
<div class="cpg-date-tabs" id="cpg-date-tabs" hidden>
|
|
|
|
|
<span class="cpg-date-tabs-label">출고일</span>
|
|
|
|
|
<div class="cpg-date-tabs-list" id="cpg-date-tabs-list"></div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="cpg-calc3">
|
|
|
|
|
|
|
|
|
|
<!-- ① 박스 계산 — 제품명 + 수량만 입력 -->
|
|
|
|
@@ -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 '<button type="button" class="cpg-date-tab' + (d === activeDate ? " is-active" : "") + '"' +
|
|
|
|
|
' data-date="' + esc(d) + '">' +
|
|
|
|
|
'<strong>' + esc(d || "날짜 미확인") + "</strong>" +
|
|
|
|
|
'<span>센터 ' + cn + "곳 · " + bx + "박스</span>" +
|
|
|
|
|
"</button>";
|
|
|
|
|
}).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 @@
|
|
|
|
|
'<span class="erp-badge erp-badge-neutral">파일 ' + ((data.files || []).length) + "개</span>" +
|
|
|
|
|
'<span class="erp-badge erp-badge-neutral">센터 ' + ((data.centers || []).length) + "곳</span>" +
|
|
|
|
|
"</div>";
|
|
|
|
|
if ((data.groups || []).length > 1) {
|
|
|
|
|
html += '<ul class="cpg-po-dates">' + data.groups.map(function (g) {
|
|
|
|
|
return "<li><b>" + esc(g.ship_date || "날짜 미확인") + "</b>" +
|
|
|
|
|
"<span>센터 " + ((g.centers || []).length) + "곳 · " + (g.quantity || 0) + "개</span></li>";
|
|
|
|
|
}).join("") + "</ul>";
|
|
|
|
|
}
|
|
|
|
|
html += '<ul class="cpg-po-files">';
|
|
|
|
|
(data.files || []).forEach(function (f) {
|
|
|
|
|
html += "<li><span>" + esc(f.filename) + "</span>" +
|
|
|
|
@@ -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;
|
|
|
|
|