diff --git a/app/modules/cupang/router.py b/app/modules/cupang/router.py index 7165e2e..9e1e465 100644 --- a/app/modules/cupang/router.py +++ b/app/modules/cupang/router.py @@ -769,8 +769,8 @@ async def box_calc_upload( warnings: list[str] = [] file_infos: list[dict[str, Any]] = [] ship_dates: list[str] = [] - # (센터명, 제품코드) → 수량 - agg: dict[tuple[str, str], dict[str, Any]] = {} + # (출고일, 센터명, 제품코드) → 수량 + agg: dict[tuple[str, str, str], dict[str, Any]] = {} unknown_codes: set[str] = set() unknown_centers: set[str] = set() no_rule: set[str] = set() @@ -793,6 +793,7 @@ async def box_calc_upload( else: warnings.append(f"{name}: F13 입고예정일을 읽지 못했습니다.") + ship_iso = ship.isoformat() if ship else "" used = 0 for row in parsed["rows"]: prod = by_coupang.get(row["coupang_item_code"]) @@ -805,10 +806,11 @@ async def box_calc_upload( code = prod["product_code"] if code not in rules: no_rule.add(f'{prod["product_name"]}({code})') - key = (cname, code) + key = (ship_iso, cname, code) cell = agg.setdefault( key, { + "ship_date": ship_iso, "center_name": cname, "product_code": code, "product_name": prod["product_name"], @@ -823,7 +825,7 @@ async def box_calc_upload( { "filename": name, "arrival_date": arrival.isoformat() if arrival else "", - "ship_date": ship.isoformat() if ship else "", + "ship_date": ship_iso, "rows": len(parsed["rows"]), "quantity": used, } @@ -841,12 +843,12 @@ async def box_calc_upload( warnings.append("박스 입수량 미설정: " + ", ".join(sorted(no_rule))) uniq_dates = sorted(set(ship_dates)) - if len(uniq_dates) > 1: - warnings.append("파일마다 출고일이 다릅니다: " + ", ".join(uniq_dates)) - grouped: dict[str, dict[str, Any]] = {} - for (cname, _code), cell in agg.items(): - g = grouped.setdefault( + # 출고일 → 센터 → 품목. 업로드한 파일의 출고일이 다르면 날짜별로 나뉜다. + by_date: dict[str, dict[str, dict[str, Any]]] = {} + for (ship_iso, cname, _code), cell in agg.items(): + centers_of_date = by_date.setdefault(ship_iso, {}) + g = centers_of_date.setdefault( cname, { "center_name": cname, @@ -862,8 +864,24 @@ async def box_calc_upload( "quantity": cell["quantity"], } ) - for g in grouped.values(): - g["items"].sort(key=lambda it: it["product_code"]) + + groups: list[dict[str, Any]] = [] + for ship_iso in sorted(by_date): + centers_of_date = by_date[ship_iso] + for g in centers_of_date.values(): + g["items"].sort(key=lambda it: it["product_code"]) + groups.append( + { + "ship_date": ship_iso, + "centers": sorted(centers_of_date.values(), key=lambda g: g["center_name"]), + "files": [f for f in file_infos if f["ship_date"] == ship_iso], + "quantity": sum( + it["quantity"] + for g in centers_of_date.values() + for it in g["items"] + ), + } + ) return JSONResponse( { @@ -871,7 +889,7 @@ async def box_calc_upload( "ship_date": uniq_dates[0] if uniq_dates else "", "ship_dates": uniq_dates, "files": file_infos, - "centers": sorted(grouped.values(), key=lambda g: g["center_name"]), + "groups": groups, "warnings": warnings, } ) diff --git a/app/modules/cupang/templates/cupang/box_calc.html b/app/modules/cupang/templates/cupang/box_calc.html index 5ea9aad..e8eb600 100644 --- a/app/modules/cupang/templates/cupang/box_calc.html +++ b/app/modules/cupang/templates/cupang/box_calc.html @@ -1,6 +1,6 @@ {% extends "erp_base.html" %} -{% block head_extra %}{% endblock %} +{% block head_extra %}{% endblock %} {% block content %}
@@ -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 += '"; + } html += '