feat(cupang): 혼합 상자 내용물 편집 (수량 스테퍼 + 이동 + 미배치 풀)
- ③ 센터 분배의 혼합 상자 트리에서 항목마다 [−] n [+] 와 이동 드롭다운 (다른 혼합 상자 / 새 상자 / 미배치로 빼기). 용량(부피 합)을 넘지 않게 제한 - 센터마다 "미배치 자투리" 풀 + [자동 담기], [+ 새 혼합 상자] 버튼 - 혼합 상자 ✕ 는 내용물을 미배치로 옮기고 상자를 없앤다 - 상자가 비면 자동 제거, 채움률은 헤더에 실시간 표시 - 확정 조건에 "미배치 자투리 0" 추가 - 혼합 상자 키를 위치 index → 상자별 고유 id 로 바꿔 추가/삭제에도 배분 유지 - 미배치 풀은 출고일 탭 전환 시에도 함께 보관 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
{% extends "erp_base.html" %}
|
{% extends "erp_base.html" %}
|
||||||
|
|
||||||
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260901o" />{% endblock %}
|
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260901p" />{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<section class="cpg">
|
<section class="cpg">
|
||||||
@@ -253,7 +253,13 @@
|
|||||||
var pending = null; // 대화상자 대상 {key, centerId}
|
var pending = null; // 대화상자 대상 {key, centerId}
|
||||||
|
|
||||||
function prodKey(code) { return "p:" + code; }
|
function prodKey(code) { return "p:" + code; }
|
||||||
function mixKey(boxName, idx) { return "m:" + boxName + "#" + idx; }
|
// 혼합 상자 키는 위치가 아니라 상자마다 붙는 고유 id 로 잡는다.
|
||||||
|
// (상자를 새로 만들거나 지워도 이미 담긴 배분이 어긋나지 않게)
|
||||||
|
var mixSeq = 0;
|
||||||
|
function mixKeyFor(b) {
|
||||||
|
if (!b.uid) { mixSeq += 1; b.uid = "b" + mixSeq; }
|
||||||
|
return "m:" + b.uid;
|
||||||
|
}
|
||||||
|
|
||||||
// 자투리 혼합 상자에 쓸 상자 종류 — 요약 카드에서 고른다(표시용).
|
// 자투리 혼합 상자에 쓸 상자 종류 — 요약 카드에서 고른다(표시용).
|
||||||
var MIX_TYPES = ["쿠팡상자", "3호상자", "6호상자"];
|
var MIX_TYPES = ["쿠팡상자", "3호상자", "6호상자"];
|
||||||
@@ -332,7 +338,7 @@
|
|||||||
mixOwner = {};
|
mixOwner = {};
|
||||||
calc.mixes.forEach(function (m) {
|
calc.mixes.forEach(function (m) {
|
||||||
m.boxes.forEach(function (b, i) {
|
m.boxes.forEach(function (b, i) {
|
||||||
var k = mixKey(m.box_name, i);
|
var k = mixKeyFor(b);
|
||||||
var n = 0;
|
var n = 0;
|
||||||
b.items.forEach(function (it) { n += it.quantity; });
|
b.items.forEach(function (it) { n += it.quantity; });
|
||||||
units[k] = n;
|
units[k] = n;
|
||||||
@@ -345,6 +351,7 @@
|
|||||||
var had = Object.keys(alloc).some(function (k) { return alloc[k].length; });
|
var had = Object.keys(alloc).some(function (k) { return alloc[k].length; });
|
||||||
alloc = {};
|
alloc = {};
|
||||||
stamped = {};
|
stamped = {};
|
||||||
|
pool = {};
|
||||||
allStamped = false; // 계산이 바뀌면 이전 배분은 근거가 사라지므로 비운다.
|
allStamped = false; // 계산이 바뀌면 이전 배분은 근거가 사라지므로 비운다.
|
||||||
var skipped = restore ? applyRestore(restore) : 0;
|
var skipped = restore ? applyRestore(restore) : 0;
|
||||||
render();
|
render();
|
||||||
@@ -426,7 +433,7 @@
|
|||||||
units: units, labels: labels, contents: contents,
|
units: units, labels: labels, contents: contents,
|
||||||
openCenters: openCenters.slice(), alloc: alloc, methods: methods,
|
openCenters: openCenters.slice(), alloc: alloc, methods: methods,
|
||||||
stamped: stamped, allStamped: allStamped,
|
stamped: stamped, allStamped: allStamped,
|
||||||
mixType: mixType, mixOwner: mixOwner
|
mixType: mixType, mixOwner: mixOwner, pool: pool
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -443,6 +450,7 @@
|
|||||||
alloc = st.alloc; methods = st.methods;
|
alloc = st.alloc; methods = st.methods;
|
||||||
stamped = st.stamped; allStamped = st.allStamped;
|
stamped = st.stamped; allStamped = st.allStamped;
|
||||||
mixType = st.mixType || {}; mixOwner = st.mixOwner || {};
|
mixType = st.mixType || {}; mixOwner = st.mixOwner || {};
|
||||||
|
pool = st.pool || {};
|
||||||
|
|
||||||
tbody.innerHTML = "";
|
tbody.innerHTML = "";
|
||||||
(st.items || []).forEach(function (it) { addRow(it.product_code, it.quantity); });
|
(st.items || []).forEach(function (it) { addRow(it.product_code, it.quantity); });
|
||||||
@@ -474,7 +482,7 @@
|
|||||||
units: {}, labels: {}, contents: {},
|
units: {}, labels: {}, contents: {},
|
||||||
openCenters: [], alloc: {}, methods: {},
|
openCenters: [], alloc: {}, methods: {},
|
||||||
stamped: {}, allStamped: false,
|
stamped: {}, allStamped: false,
|
||||||
mixType: {}, mixOwner: {}
|
mixType: {}, mixOwner: {}, pool: {}
|
||||||
};
|
};
|
||||||
|
|
||||||
function put(cid, key, count) {
|
function put(cid, key, count) {
|
||||||
@@ -512,7 +520,7 @@
|
|||||||
mx.boxes.forEach(function (b) {
|
mx.boxes.forEach(function (b) {
|
||||||
var idx = bucket.boxes.length;
|
var idx = bucket.boxes.length;
|
||||||
bucket.boxes.push(b);
|
bucket.boxes.push(b);
|
||||||
var k = mixKey(mx.box_name, idx);
|
var k = mixKeyFor(b);
|
||||||
st.mixOwner[k] = g.center_name;
|
st.mixOwner[k] = g.center_name;
|
||||||
put(cid, k, 1);
|
put(cid, k, 1);
|
||||||
});
|
});
|
||||||
@@ -530,7 +538,7 @@
|
|||||||
m.boxes.forEach(function (b, i) {
|
m.boxes.forEach(function (b, i) {
|
||||||
var n = 0;
|
var n = 0;
|
||||||
b.items.forEach(function (it) { n += it.quantity; });
|
b.items.forEach(function (it) { n += it.quantity; });
|
||||||
var k = mixKey(m.box_name, i);
|
var k = mixKeyFor(b);
|
||||||
st.units[k] = n;
|
st.units[k] = n;
|
||||||
st.mixType[k] = MIX_TYPES[0];
|
st.mixType[k] = MIX_TYPES[0];
|
||||||
st.labels[k] = st.mixType[k] + " 혼합 #" + (i + 1) +
|
st.labels[k] = st.mixType[k] + " 혼합 #" + (i + 1) +
|
||||||
@@ -680,6 +688,161 @@
|
|||||||
return n;
|
return n;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── 혼합 상자 편집 ────────────────────────────────
|
||||||
|
// 상자 용량은 부피 합으로 본다: 제품 1개 = 1/입수량 상자.
|
||||||
|
// 빼낸 수량은 센터별 "미배치 자투리" 풀에 남고, 거기서 다시 담는다.
|
||||||
|
var pool = {}; // centerId -> [{product_code, product_name, quantity}]
|
||||||
|
|
||||||
|
function upbOf(code) {
|
||||||
|
var hit = calc.results.filter(function (r) { return r.product_code === code; })[0];
|
||||||
|
return hit ? (hit.units_per_box || 0) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mixRef(key) {
|
||||||
|
var found = null;
|
||||||
|
calc.mixes.forEach(function (m) {
|
||||||
|
m.boxes.forEach(function (b) { if (mixKeyFor(b) === key) found = { m: m, b: b }; });
|
||||||
|
});
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mixFill(key) {
|
||||||
|
var vol = 0;
|
||||||
|
(contents[key] || []).forEach(function (it) {
|
||||||
|
var u = upbOf(it.product_code);
|
||||||
|
if (u) vol += it.quantity / u;
|
||||||
|
});
|
||||||
|
return vol;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mixRoomFor(key, code) {
|
||||||
|
var u = upbOf(code);
|
||||||
|
if (!u) return 0;
|
||||||
|
return Math.max(0, Math.floor((1 - mixFill(key)) * u + 1e-9));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 내용물이 바뀐 상자를 ②(요약) 쪽 데이터와 맞춘다. 비면 상자를 없앤다.
|
||||||
|
function syncMix(key) {
|
||||||
|
var list = (contents[key] || []).filter(function (it) { return it.quantity > 0; });
|
||||||
|
contents[key] = list;
|
||||||
|
var ref = mixRef(key);
|
||||||
|
if (!list.length) {
|
||||||
|
if (ref) {
|
||||||
|
var idx = ref.m.boxes.indexOf(ref.b);
|
||||||
|
if (idx >= 0) ref.m.boxes.splice(idx, 1);
|
||||||
|
if (!ref.m.boxes.length) {
|
||||||
|
calc.mixes = calc.mixes.filter(function (m) { return m !== ref.m; });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Object.keys(alloc).forEach(function (cid) {
|
||||||
|
alloc[cid] = (alloc[cid] || []).filter(function (a) { return a.key !== key; });
|
||||||
|
});
|
||||||
|
delete units[key]; delete labels[key]; delete contents[key];
|
||||||
|
delete mixType[key]; delete mixOwner[key]; delete stamped[key];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var total = 0;
|
||||||
|
list.forEach(function (it) { total += it.quantity; });
|
||||||
|
units[key] = total;
|
||||||
|
if (ref) {
|
||||||
|
ref.b.items = list.map(function (it) {
|
||||||
|
return { product_code: it.product_code, product_name: it.product_name, quantity: it.quantity };
|
||||||
|
});
|
||||||
|
ref.b.fill_percent = Math.round(mixFill(key) * 1000) / 10;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function centerOfKey(key) {
|
||||||
|
var hit = "";
|
||||||
|
Object.keys(alloc).forEach(function (cid) {
|
||||||
|
(alloc[cid] || []).forEach(function (a) { if (a.key === key) hit = String(cid); });
|
||||||
|
});
|
||||||
|
return hit;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mixKeysOf(cid) {
|
||||||
|
return (alloc[String(cid)] || [])
|
||||||
|
.filter(function (a) { return a.key.indexOf("m:") === 0; })
|
||||||
|
.map(function (a) { return a.key; });
|
||||||
|
}
|
||||||
|
|
||||||
|
function poolOf(cid) {
|
||||||
|
cid = String(cid);
|
||||||
|
if (!pool[cid]) pool[cid] = [];
|
||||||
|
return pool[cid];
|
||||||
|
}
|
||||||
|
|
||||||
|
function poolAdd(cid, code, name, qty) {
|
||||||
|
if (qty <= 0) return;
|
||||||
|
var list = poolOf(cid);
|
||||||
|
var hit = list.filter(function (it) { return it.product_code === code; })[0];
|
||||||
|
if (hit) { hit.quantity += qty; return; }
|
||||||
|
list.push({ product_code: code, product_name: name, quantity: qty });
|
||||||
|
}
|
||||||
|
|
||||||
|
function poolTake(cid, code, qty) {
|
||||||
|
var list = poolOf(cid);
|
||||||
|
var hit = list.filter(function (it) { return it.product_code === code; })[0];
|
||||||
|
if (!hit) return 0;
|
||||||
|
var take = Math.min(hit.quantity, qty);
|
||||||
|
hit.quantity -= take;
|
||||||
|
pool[String(cid)] = list.filter(function (it) { return it.quantity > 0; });
|
||||||
|
return take;
|
||||||
|
}
|
||||||
|
|
||||||
|
function poolTotal(cid) {
|
||||||
|
var n = 0;
|
||||||
|
poolOf(cid).forEach(function (it) { n += it.quantity; });
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mixItem(key, code) {
|
||||||
|
return (contents[key] || []).filter(function (it) { return it.product_code === code; })[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 상자에 담기 — 용량만큼만 들어간다. 담은 수량을 돌려준다.
|
||||||
|
function mixPut(key, code, name, qty) {
|
||||||
|
var room = mixRoomFor(key, code);
|
||||||
|
var put = Math.min(room, qty);
|
||||||
|
if (put <= 0) return 0;
|
||||||
|
var hit = mixItem(key, code);
|
||||||
|
if (hit) { hit.quantity += put; }
|
||||||
|
else { (contents[key] = contents[key] || []).push({ product_code: code, product_name: name, quantity: put }); }
|
||||||
|
syncMix(key);
|
||||||
|
return put;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 상자에서 빼기 — 뺀 수량은 그 센터의 미배치 풀로.
|
||||||
|
function mixTake(key, code, qty) {
|
||||||
|
var hit = mixItem(key, code);
|
||||||
|
if (!hit) return 0;
|
||||||
|
var take = Math.min(hit.quantity, qty);
|
||||||
|
if (take <= 0) return 0;
|
||||||
|
var cid = centerOfKey(key);
|
||||||
|
hit.quantity -= take;
|
||||||
|
syncMix(key);
|
||||||
|
if (cid) poolAdd(cid, code, hit.product_name, take);
|
||||||
|
return take;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 새 혼합 상자 (같은 센터, 비어 있는 상태로 추가)
|
||||||
|
function newMixBox(cid, boxName) {
|
||||||
|
var name = boxName || (calc.mixes[0] && calc.mixes[0].box_name) || MIX_TYPES[0];
|
||||||
|
var bucket = calc.mixes.filter(function (m) { return m.box_name === name; })[0];
|
||||||
|
if (!bucket) { bucket = { box_name: name, boxes: [] }; calc.mixes.push(bucket); }
|
||||||
|
var b = { items: [], fill_percent: 0 };
|
||||||
|
bucket.boxes.push(b);
|
||||||
|
var key = mixKeyFor(b);
|
||||||
|
contents[key] = [];
|
||||||
|
units[key] = 0;
|
||||||
|
mixType[key] = MIX_TYPES[0];
|
||||||
|
mixOwner[key] = centerName(cid);
|
||||||
|
labels[key] = mixLabel(key, bucket.boxes.length - 1);
|
||||||
|
seq += 1;
|
||||||
|
(alloc[String(cid)] = alloc[String(cid)] || []).push({ id: seq, key: key, count: 1 });
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
// ── ② 요약 렌더 ───────────────────────────────────
|
// ── ② 요약 렌더 ───────────────────────────────────
|
||||||
function boxSvg(fill, label) {
|
function boxSvg(fill, label) {
|
||||||
var f = Math.max(0, Math.min(100, fill || 0));
|
var f = Math.max(0, Math.min(100, fill || 0));
|
||||||
@@ -731,7 +894,7 @@
|
|||||||
calc.mixes.forEach(function (m) {
|
calc.mixes.forEach(function (m) {
|
||||||
m.boxes.forEach(function (b, i) {
|
m.boxes.forEach(function (b, i) {
|
||||||
totalMix += 1;
|
totalMix += 1;
|
||||||
remainMix += remainFor(mixKey(m.box_name, i));
|
remainMix += remainFor(mixKeyFor(b));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
var assigned = (totalProd + totalMix) - (remainProd + remainMix);
|
var assigned = (totalProd + totalMix) - (remainProd + remainMix);
|
||||||
@@ -783,7 +946,7 @@
|
|||||||
var bh = "", n = 0;
|
var bh = "", n = 0;
|
||||||
calc.mixes.forEach(function (m) {
|
calc.mixes.forEach(function (m) {
|
||||||
m.boxes.forEach(function (b, i) {
|
m.boxes.forEach(function (b, i) {
|
||||||
var key = mixKey(m.box_name, i);
|
var key = mixKeyFor(b);
|
||||||
var remain = remainFor(key);
|
var remain = remainFor(key);
|
||||||
if (remain) delete stamped[key];
|
if (remain) delete stamped[key];
|
||||||
var items = b.items.map(function (it) {
|
var items = b.items.map(function (it) {
|
||||||
@@ -809,18 +972,79 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 혼합 상자는 안에 무엇이 몇 개 들어가는지 트리로 펼쳐 보여준다.
|
// 혼합 상자는 안에 무엇이 몇 개 들어가는지 트리로 펼치고, 그 자리에서
|
||||||
|
// 수량 조절(− +)과 다른 상자로 이동(⇄)을 할 수 있게 한다.
|
||||||
|
function moveOptions(fromKey, cid, code) {
|
||||||
|
var opts = '<option value="">이동…</option>';
|
||||||
|
mixKeysOf(cid).forEach(function (k) {
|
||||||
|
if (k === fromKey) return;
|
||||||
|
var room = mixRoomFor(k, code);
|
||||||
|
opts += '<option value="' + esc(k) + '"' + (room ? "" : " disabled") + ">" +
|
||||||
|
esc(labels[k] || k) + (room ? " (여유 " + room + "개)" : " (가득)") + "</option>";
|
||||||
|
});
|
||||||
|
opts += '<option value="__new__">+ 새 상자로</option>';
|
||||||
|
opts += '<option value="__pool__">미배치로 빼기</option>';
|
||||||
|
return opts;
|
||||||
|
}
|
||||||
|
|
||||||
function treeHtml(entry) {
|
function treeHtml(entry) {
|
||||||
var list = contents[entry.key];
|
var list = contents[entry.key];
|
||||||
if (!list || !list.length) return "";
|
if (!list || !list.length) return "";
|
||||||
|
var editable = entry.key.indexOf("m:") === 0 && entry.count === 1;
|
||||||
|
var cid = editable ? centerOfKey(entry.key) : "";
|
||||||
return '<ul class="cpg-dist-tree">' + list.map(function (it) {
|
return '<ul class="cpg-dist-tree">' + list.map(function (it) {
|
||||||
|
var qty = it.quantity * entry.count;
|
||||||
|
if (!editable) {
|
||||||
return '<li><span class="cpg-tree-name">' + esc(it.product_name) + "</span>" +
|
return '<li><span class="cpg-tree-name">' + esc(it.product_name) + "</span>" +
|
||||||
'<span class="cpg-tree-qty">' + (it.quantity * entry.count) + "개</span>" +
|
'<span class="cpg-tree-qty">' + qty + "개</span>" +
|
||||||
(entry.count > 1 ? '<span class="cpg-tree-per">(' + it.quantity + "개 × " + entry.count + "상자)</span>" : "") +
|
(entry.count > 1 ? '<span class="cpg-tree-per">(' + it.quantity + "개 × " + entry.count + "상자)</span>" : "") +
|
||||||
"</li>";
|
"</li>";
|
||||||
|
}
|
||||||
|
var canAdd = mixRoomFor(entry.key, it.product_code) > 0;
|
||||||
|
return '<li><span class="cpg-tree-name">' + esc(it.product_name) + "</span>" +
|
||||||
|
'<span class="cpg-mix-ctl">' +
|
||||||
|
'<button type="button" class="cpg-mix-step" data-mix-dec data-key="' + esc(entry.key) +
|
||||||
|
'" data-code="' + esc(it.product_code) + '" title="1개 빼기">−</button>' +
|
||||||
|
'<span class="cpg-tree-qty">' + qty + "개</span>" +
|
||||||
|
'<button type="button" class="cpg-mix-step" data-mix-inc data-key="' + esc(entry.key) +
|
||||||
|
'" data-code="' + esc(it.product_code) + '"' + (canAdd ? "" : " disabled") +
|
||||||
|
' title="미배치에서 1개 담기">+</button>' +
|
||||||
|
'<select class="erp-select cpg-mix-move" data-key="' + esc(entry.key) +
|
||||||
|
'" data-code="' + esc(it.product_code) + '" aria-label="이동">' +
|
||||||
|
moveOptions(entry.key, cid, it.product_code) +
|
||||||
|
"</select>" +
|
||||||
|
"</span></li>";
|
||||||
}).join("") + "</ul>";
|
}).join("") + "</ul>";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 센터별 미배치 자투리 — 상자에서 뺀 수량이 여기 모인다.
|
||||||
|
function poolHtml(cid) {
|
||||||
|
var list = poolOf(cid);
|
||||||
|
if (!list.length) return "";
|
||||||
|
var rows = list.map(function (it) {
|
||||||
|
var opts = '<option value="">담을 상자…</option>';
|
||||||
|
mixKeysOf(cid).forEach(function (k) {
|
||||||
|
var room = mixRoomFor(k, it.product_code);
|
||||||
|
opts += '<option value="' + esc(k) + '"' + (room ? "" : " disabled") + ">" +
|
||||||
|
esc(labels[k] || k) + (room ? " (여유 " + room + "개)" : " (가득)") + "</option>";
|
||||||
|
});
|
||||||
|
opts += '<option value="__new__">+ 새 상자에</option>';
|
||||||
|
return '<li><span class="cpg-tree-name">' + esc(it.product_name) + "</span>" +
|
||||||
|
'<span class="cpg-mix-ctl">' +
|
||||||
|
'<span class="cpg-tree-qty">' + it.quantity + "개</span>" +
|
||||||
|
'<select class="erp-select cpg-pool-move" data-center="' + esc(cid) +
|
||||||
|
'" data-code="' + esc(it.product_code) + '" aria-label="담을 상자">' + opts + "</select>" +
|
||||||
|
"</span></li>";
|
||||||
|
}).join("");
|
||||||
|
return '<div class="cpg-pool">' +
|
||||||
|
'<div class="cpg-pool-head">미배치 자투리 <b>' + poolTotal(cid) + "개</b>" +
|
||||||
|
'<button type="button" class="erp-btn erp-btn-outline cpg-pool-auto" data-center="' + esc(cid) +
|
||||||
|
'">자동 담기</button>' +
|
||||||
|
"</div>" +
|
||||||
|
'<ul class="cpg-dist-tree">' + rows + "</ul>" +
|
||||||
|
"</div>";
|
||||||
|
}
|
||||||
|
|
||||||
// ── ③ 센터 분배 렌더 ──────────────────────────────
|
// ── ③ 센터 분배 렌더 ──────────────────────────────
|
||||||
function renderDist() {
|
function renderDist() {
|
||||||
if (!distList) return;
|
if (!distList) return;
|
||||||
@@ -834,9 +1058,12 @@
|
|||||||
var per = units[a.key] || 0;
|
var per = units[a.key] || 0;
|
||||||
boxes += a.count;
|
boxes += a.count;
|
||||||
pieces += a.count * per;
|
pieces += a.count * per;
|
||||||
items += '<li class="cpg-dist-item" data-entry="' + a.id + '">' +
|
var isMix = a.key.indexOf("m:") === 0;
|
||||||
|
var fillTxt = isMix ? Math.round(mixFill(a.key) * 100) + "% 채움" : "";
|
||||||
|
items += '<li class="cpg-dist-item' + (isMix ? " is-mix" : "") + '" data-entry="' + a.id + '">' +
|
||||||
'<div class="cpg-dist-line">' +
|
'<div class="cpg-dist-line">' +
|
||||||
'<span class="cpg-dist-name">' + esc(labels[a.key] || a.key) + "</span>" +
|
'<span class="cpg-dist-name">' + esc(labels[a.key] || a.key) +
|
||||||
|
(fillTxt ? ' <em class="cpg-mix-fill">' + fillTxt + "</em>" : "") + "</span>" +
|
||||||
'<input class="erp-input cpg-dist-qty" type="number" min="1" max="999" step="1" inputmode="numeric" value="' + a.count + '" />' +
|
'<input class="erp-input cpg-dist-qty" type="number" min="1" max="999" step="1" inputmode="numeric" value="' + a.count + '" />' +
|
||||||
'<span class="cpg-dist-unit">상자 · ' + (a.count * per) + "개</span>" +
|
'<span class="cpg-dist-unit">상자 · ' + (a.count * per) + "개</span>" +
|
||||||
'<button type="button" class="erp-btn erp-btn-danger cpg-dist-del" title="빼기" aria-label="빼기">✕</button>' +
|
'<button type="button" class="erp-btn erp-btn-danger cpg-dist-del" title="빼기" aria-label="빼기">✕</button>' +
|
||||||
@@ -863,6 +1090,13 @@
|
|||||||
(rows.length
|
(rows.length
|
||||||
? '<ul class="cpg-dist-items">' + items + "</ul>"
|
? '<ul class="cpg-dist-items">' + items + "</ul>"
|
||||||
: '<p class="cpg-dist-hint">여기로 상자를 끌어다 놓기</p>') +
|
: '<p class="cpg-dist-hint">여기로 상자를 끌어다 놓기</p>') +
|
||||||
|
poolHtml(cid) +
|
||||||
|
(rows.length
|
||||||
|
? '<div class="cpg-dist-foot">' +
|
||||||
|
'<button type="button" class="erp-btn erp-btn-outline cpg-mix-new" data-center="' +
|
||||||
|
esc(cid) + '">+ 새 혼합 상자</button>' +
|
||||||
|
"</div>"
|
||||||
|
: "") +
|
||||||
"</div>";
|
"</div>";
|
||||||
});
|
});
|
||||||
distList.innerHTML = html;
|
distList.innerHTML = html;
|
||||||
@@ -1049,7 +1283,7 @@
|
|||||||
mixType[key] = sel.value;
|
mixType[key] = sel.value;
|
||||||
calc.mixes.forEach(function (m) {
|
calc.mixes.forEach(function (m) {
|
||||||
m.boxes.forEach(function (b, i) {
|
m.boxes.forEach(function (b, i) {
|
||||||
var k = mixKey(m.box_name, i);
|
var k = mixKeyFor(b);
|
||||||
if (k === key) labels[k] = mixLabel(k, i);
|
if (k === key) labels[k] = mixLabel(k, i);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1057,6 +1291,137 @@
|
|||||||
msg.textContent = "상자 종류를 " + sel.value + " 로 바꿨습니다.";
|
msg.textContent = "상자 종류를 " + sel.value + " 로 바꿨습니다.";
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── 혼합 상자 내용물 편집 이벤트 ──────────────────
|
||||||
|
if (distList) {
|
||||||
|
distList.addEventListener("click", function (e) {
|
||||||
|
var dec = e.target.closest("[data-mix-dec]");
|
||||||
|
if (dec) {
|
||||||
|
var moved = mixTake(dec.getAttribute("data-key"), dec.getAttribute("data-code"), 1);
|
||||||
|
if (moved) { render(); msg.textContent = "1개를 미배치로 뺐습니다."; }
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var inc = e.target.closest("[data-mix-inc]");
|
||||||
|
if (inc) {
|
||||||
|
var key = inc.getAttribute("data-key");
|
||||||
|
var code = inc.getAttribute("data-code");
|
||||||
|
var cid = centerOfKey(key);
|
||||||
|
var got = poolTake(cid, code, Math.min(1, mixRoomFor(key, code)));
|
||||||
|
if (!got) {
|
||||||
|
msg.textContent = mixRoomFor(key, code)
|
||||||
|
? "미배치 자투리에 남은 수량이 없습니다."
|
||||||
|
: "이 상자에 더 들어갈 자리가 없습니다.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var name = (mixItem(key, code) || {}).product_name || code;
|
||||||
|
mixPut(key, code, name, got);
|
||||||
|
render();
|
||||||
|
msg.textContent = "1개를 담았습니다.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 혼합 상자의 ✕ — 내용물을 미배치로 옮기고 상자를 없앤다.
|
||||||
|
var del = e.target.closest(".cpg-dist-del");
|
||||||
|
if (del) {
|
||||||
|
var li = del.closest(".cpg-dist-item");
|
||||||
|
var zone = del.closest("[data-drop-for]");
|
||||||
|
var dcid = zone && zone.getAttribute("data-drop-for");
|
||||||
|
var entry = (alloc[dcid] || []).filter(function (a) {
|
||||||
|
return String(a.id) === li.getAttribute("data-entry");
|
||||||
|
})[0];
|
||||||
|
if (entry && entry.key.indexOf("m:") === 0) {
|
||||||
|
e.stopImmediatePropagation(); // 뒤 핸들러가 또 지우지 않게
|
||||||
|
(contents[entry.key] || []).slice().forEach(function (it) {
|
||||||
|
poolAdd(dcid, it.product_code, it.product_name, it.quantity);
|
||||||
|
});
|
||||||
|
contents[entry.key] = [];
|
||||||
|
syncMix(entry.key);
|
||||||
|
render();
|
||||||
|
msg.textContent = "혼합 상자를 비우고 미배치로 옮겼습니다.";
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var neu = e.target.closest(".cpg-mix-new");
|
||||||
|
if (neu) {
|
||||||
|
newMixBox(neu.getAttribute("data-center"));
|
||||||
|
render();
|
||||||
|
msg.textContent = "빈 혼합 상자를 추가했습니다.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var auto = e.target.closest(".cpg-pool-auto");
|
||||||
|
if (auto) {
|
||||||
|
var acid = auto.getAttribute("data-center");
|
||||||
|
var left = 0;
|
||||||
|
poolOf(acid).slice().forEach(function (it) {
|
||||||
|
var need = it.quantity;
|
||||||
|
mixKeysOf(acid).forEach(function (k) {
|
||||||
|
if (need <= 0) return;
|
||||||
|
var put = mixPut(k, it.product_code, it.product_name, Math.min(need, mixRoomFor(k, it.product_code)));
|
||||||
|
if (put) { poolTake(acid, it.product_code, put); need -= put; }
|
||||||
|
});
|
||||||
|
if (need > 0) {
|
||||||
|
var nk = newMixBox(acid);
|
||||||
|
var put2 = mixPut(nk, it.product_code, it.product_name, Math.min(need, mixRoomFor(nk, it.product_code)));
|
||||||
|
if (put2) { poolTake(acid, it.product_code, put2); need -= put2; }
|
||||||
|
}
|
||||||
|
left += Math.max(need, 0);
|
||||||
|
});
|
||||||
|
render();
|
||||||
|
msg.textContent = left ? "일부는 담지 못했습니다 (" + left + "개)" : "미배치 자투리를 모두 담았습니다.";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
distList.addEventListener("change", function (e) {
|
||||||
|
var mv = e.target.closest(".cpg-mix-move");
|
||||||
|
if (mv) {
|
||||||
|
var fromKey = mv.getAttribute("data-key");
|
||||||
|
var code = mv.getAttribute("data-code");
|
||||||
|
var target = mv.value;
|
||||||
|
mv.value = "";
|
||||||
|
if (!target) return;
|
||||||
|
var src = mixItem(fromKey, code);
|
||||||
|
if (!src) return;
|
||||||
|
var qty = src.quantity;
|
||||||
|
var name = src.product_name;
|
||||||
|
var cid = centerOfKey(fromKey);
|
||||||
|
if (target === "__pool__") {
|
||||||
|
mixTake(fromKey, code, qty);
|
||||||
|
render();
|
||||||
|
msg.textContent = name + " " + qty + "개를 미배치로 뺐습니다.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var toKey = target === "__new__" ? newMixBox(cid) : target;
|
||||||
|
var room = mixRoomFor(toKey, code);
|
||||||
|
var move = Math.min(qty, room);
|
||||||
|
if (move <= 0) { msg.textContent = "옮길 자리가 없습니다."; render(); return; }
|
||||||
|
mixTake(fromKey, code, move); // 일단 미배치로
|
||||||
|
poolTake(cid, code, move); // 바로 꺼내서
|
||||||
|
mixPut(toKey, code, name, move); // 대상 상자에 담기
|
||||||
|
render();
|
||||||
|
msg.textContent = name + " " + move + "개를 " + (labels[toKey] || "새 상자") + " 로 옮겼습니다." +
|
||||||
|
(move < qty ? " (자리가 부족해 " + (qty - move) + "개는 남음)" : "");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var pm = e.target.closest(".cpg-pool-move");
|
||||||
|
if (pm) {
|
||||||
|
var pcid = pm.getAttribute("data-center");
|
||||||
|
var pcode = pm.getAttribute("data-code");
|
||||||
|
var dest = pm.value;
|
||||||
|
pm.value = "";
|
||||||
|
if (!dest) return;
|
||||||
|
var entry = poolOf(pcid).filter(function (it) { return it.product_code === pcode; })[0];
|
||||||
|
if (!entry) return;
|
||||||
|
var key2 = dest === "__new__" ? newMixBox(pcid) : dest;
|
||||||
|
var put = Math.min(entry.quantity, mixRoomFor(key2, pcode));
|
||||||
|
if (put <= 0) { msg.textContent = "그 상자에는 자리가 없습니다."; render(); return; }
|
||||||
|
poolTake(pcid, pcode, put);
|
||||||
|
mixPut(key2, pcode, entry.product_name, put);
|
||||||
|
render();
|
||||||
|
msg.textContent = entry.product_name + " " + put + "개를 담았습니다.";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (distList) {
|
if (distList) {
|
||||||
distList.addEventListener("input", function (e) {
|
distList.addEventListener("input", function (e) {
|
||||||
if (!e.target.classList.contains("cpg-dist-qty")) return;
|
if (!e.target.classList.contains("cpg-dist-qty")) return;
|
||||||
@@ -1189,7 +1554,7 @@
|
|||||||
calc.mixes.forEach(function (m) {
|
calc.mixes.forEach(function (m) {
|
||||||
m.boxes.forEach(function (b, i) {
|
m.boxes.forEach(function (b, i) {
|
||||||
total += 1;
|
total += 1;
|
||||||
remain += remainFor(mixKey(m.box_name, i));
|
remain += remainFor(mixKeyFor(b));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
return { total: total, remain: remain };
|
return { total: total, remain: remain };
|
||||||
@@ -1208,6 +1573,9 @@
|
|||||||
if (!cids.length) return "센터에 담긴 상자가 없습니다.";
|
if (!cids.length) return "센터에 담긴 상자가 없습니다.";
|
||||||
var missing = cids.filter(function (cid) { return !methods[String(cid)]; });
|
var missing = cids.filter(function (cid) { return !methods[String(cid)]; });
|
||||||
if (missing.length) return "출고방식 미선택: " + missing.map(centerName).join(", ");
|
if (missing.length) return "출고방식 미선택: " + missing.map(centerName).join(", ");
|
||||||
|
var leftover = 0;
|
||||||
|
openCenters.forEach(function (cid) { leftover += poolTotal(cid); });
|
||||||
|
if (leftover) return "미배치 자투리 " + leftover + "개가 남았습니다.";
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1478,7 +1846,7 @@
|
|||||||
});
|
});
|
||||||
calc.mixes.forEach(function (m) {
|
calc.mixes.forEach(function (m) {
|
||||||
m.boxes.forEach(function (b, i) {
|
m.boxes.forEach(function (b, i) {
|
||||||
var k = mixKey(m.box_name, i);
|
var k = mixKeyFor(b);
|
||||||
labels[k] = mixLabel(k, i);
|
labels[k] = mixLabel(k, i);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{% extends "erp_base.html" %}
|
{% extends "erp_base.html" %}
|
||||||
|
|
||||||
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260901o" />{% endblock %}
|
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260901p" />{% 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=20260901o" />{% endblock %}
|
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260901p" />{% 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=20260901o" />{% endblock %}
|
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260901p" />{% 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=20260901o" />{% endblock %}
|
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260901p" />{% 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=20260901o" />{% endblock %}
|
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260901p" />{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{# 출고 묶음 보기 — 상자 계산 화면과 같은 3열 구성. 읽기 전용(수정 없음). #}
|
{# 출고 묶음 보기 — 상자 계산 화면과 같은 3열 구성. 읽기 전용(수정 없음). #}
|
||||||
|
|||||||
@@ -1422,3 +1422,37 @@ body.cpg-dragging { cursor: grabbing; user-select: none; }
|
|||||||
.cpg-list .cpg-ship-meta { padding: 0 10px 8px; font-size: 11px; }
|
.cpg-list .cpg-ship-meta { padding: 0 10px 8px; font-size: 11px; }
|
||||||
/* 혼합 상자 줄은 굵게 — 어떤 상자인지 바로 보이게 */
|
/* 혼합 상자 줄은 굵게 — 어떤 상자인지 바로 보이게 */
|
||||||
.cpg-dist-item.is-mix .cpg-dist-name { font-weight: 600; }
|
.cpg-dist-item.is-mix .cpg-dist-name { font-weight: 600; }
|
||||||
|
|
||||||
|
/* ── ③ 혼합 상자 내용물 편집 (수량 스테퍼 / 이동 / 미배치 풀) ── */
|
||||||
|
.cpg-mix-fill { font-style: normal; font-size: 11px; color: var(--color-midtone-gray); }
|
||||||
|
.cpg-mix-ctl { display: inline-flex; align-items: center; gap: 4px; flex: 0 0 auto; }
|
||||||
|
.cpg-mix-step {
|
||||||
|
width: 20px; height: 20px; line-height: 1;
|
||||||
|
display: inline-flex; align-items: center; justify-content: center;
|
||||||
|
border: 1px solid var(--color-subtle-ash); border-radius: 6px;
|
||||||
|
background: #fff; cursor: pointer; font-size: 13px; padding: 0;
|
||||||
|
}
|
||||||
|
.cpg-mix-step:hover:not(:disabled) { background: var(--color-ghost-gray); border-color: var(--color-midtone-gray); }
|
||||||
|
.cpg-mix-step:disabled { opacity: .35; cursor: not-allowed; }
|
||||||
|
.cpg-mix-move, .cpg-pool-move {
|
||||||
|
width: auto; min-width: 74px; max-width: 132px;
|
||||||
|
padding: 1px 4px; font-size: 11px; line-height: 1.5; cursor: pointer;
|
||||||
|
}
|
||||||
|
.cpg-dist-tree .cpg-tree-qty { min-width: 38px; text-align: right; }
|
||||||
|
|
||||||
|
.cpg-pool {
|
||||||
|
margin: 6px 10px 8px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border: 1px dashed var(--color-midtone-gray); border-radius: 10px;
|
||||||
|
background: var(--color-ghost-gray);
|
||||||
|
}
|
||||||
|
.cpg-pool-head {
|
||||||
|
display: flex; align-items: center; gap: 8px;
|
||||||
|
font-size: 12px; font-weight: 600; margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
.cpg-pool-head > b { font-family: var(--font-geist-mono); }
|
||||||
|
.cpg-pool-auto { margin-left: auto; padding: 2px 8px; font-size: 11px; line-height: 1.5; }
|
||||||
|
.cpg-pool .cpg-dist-tree { border-left: 0; padding-left: 0; }
|
||||||
|
.cpg-pool .cpg-dist-tree li::before { content: ""; }
|
||||||
|
.cpg-dist-foot { padding: 0 10px 10px; }
|
||||||
|
.cpg-mix-new { padding: 3px 10px; font-size: 12px; line-height: 1.5; }
|
||||||
|
|||||||
Reference in New Issue
Block a user