@@ -1,6 +1,6 @@
{% extends "erp_base.html" %}
{% block head_extra %}< link rel = "stylesheet" href = "/static/cupang.css?v=20260901p " / > {% endblock %}
{% block head_extra %}< link rel = "stylesheet" href = "/static/cupang.css?v=20260901q " / > {% endblock %}
{% block content %}
< section class = "cpg" >
@@ -351,7 +351,6 @@
var had = Object . keys ( alloc ) . some ( function ( k ) { return alloc [ k ] . length ; } ) ;
alloc = { } ;
stamped = { } ;
pool = { } ;
allStamped = false ; // 계산이 바뀌면 이전 배분은 근거가 사라지므로 비운다.
var skipped = restore ? applyRestore ( restore ) : 0 ;
render ( ) ;
@@ -433,7 +432,7 @@
units : units , labels : labels , contents : contents ,
openCenters : openCenters . slice ( ) , alloc : alloc , methods : methods ,
stamped : stamped , allStamped : allStamped ,
mixType : mixType , mixOwner : mixOwner , pool : pool
mixType : mixType , mixOwner : mixOwner
} ;
}
@@ -450,7 +449,6 @@
alloc = st . alloc ; methods = st . methods ;
stamped = st . stamped ; allStamped = st . allStamped ;
mixType = st . mixType || { } ; mixOwner = st . mixOwner || { } ;
pool = st . pool || { } ;
tbody . innerHTML = "" ;
( st . items || [ ] ) . forEach ( function ( it ) { addRow ( it . product _code , it . quantity ) ; } ) ;
@@ -482,7 +480,7 @@
units : { } , labels : { } , contents : { } ,
openCenters : [ ] , alloc : { } , methods : { } ,
stamped : { } , allStamped : false ,
mixType : { } , mixOwner : { } , pool : { }
mixType : { } , mixOwner : { }
} ;
function put ( cid , key , count ) {
@@ -691,8 +689,6 @@
// ── 혼합 상자 편집 ────────────────────────────────
// 상자 용량은 부피 합으로 본다: 제품 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 ;
@@ -766,36 +762,6 @@
. 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 ] ;
}
@@ -812,19 +778,48 @@
return put ;
}
// 상자에서 빼기 — 뺀 수량은 그 센터의 미배치 풀로 .
function mixTake ( key , code , qty ) {
// 상자에서 덜어내기(내부용) — 호출한 쪽이 곧바로 다른 상자에 담는다 .
function mixPull ( 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 boxWithRoom ( cid , code , exceptKey ) {
var found = "" ;
mixKeysOf ( cid ) . forEach ( function ( k ) {
if ( found || k === exceptKey ) return ;
if ( mixRoomFor ( k , code ) > 0 ) found = k ;
} ) ;
return found ;
}
// 그 제품이 담겨 있는 다른 상자 찾기 (없으면 "")
function boxHaving ( cid , code , exceptKey ) {
var found = "" ;
mixKeysOf ( cid ) . forEach ( function ( k ) {
if ( found || k === exceptKey ) return ;
if ( mixItem ( k , code ) ) found = k ;
} ) ;
return found ;
}
// 상자 → 상자 이동. 실제로 옮긴 수량을 돌려준다(자리만큼만).
function moveUnits ( fromKey , toKey , code , name , qty ) {
var room = mixRoomFor ( toKey , code ) ;
var move = Math . min ( qty , room ) ;
if ( move <= 0 ) return 0 ;
var pulled = mixPull ( fromKey , code , move ) ;
if ( ! pulled ) return 0 ;
mixPut ( toKey , code , name , pulled ) ;
return pulled ;
}
// 새 혼합 상자 (같은 센터, 비어 있는 상태로 추가)
function newMixBox ( cid , boxName ) {
var name = boxName || ( calc . mixes [ 0 ] && calc . mixes [ 0 ] . box _name ) || MIX _TYPES [ 0 ] ;
@@ -983,7 +978,6 @@
esc ( labels [ k ] || k ) + ( room ? " (여유 " + room + "개)" : " (가득)" ) + "</option>" ;
} ) ;
opts += '<option value="__new__">+ 새 상자로</option>' ;
opts += '<option value="__pool__">미배치로 빼기</option>' ;
return opts ;
}
@@ -1000,15 +994,16 @@
( entry . count > 1 ? '<span class="cpg-tree-per">(' + it . quantity + "개 × " + entry . count + "상자)</span>" : "" ) +
"</li>" ;
}
var canAdd = mixRoomFor ( entry . key , it . product _code ) > 0 ;
var canAdd = mixRoomFor ( entry . key , it . product _code ) > 0 &&
! ! boxHaving ( cid , it . product _code , entry . key ) ;
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>' +
'" 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>' +
' 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 ) +
@@ -1017,34 +1012,6 @@
} ) . 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 ( ) {
if ( ! distList ) return ;
@@ -1090,7 +1057,6 @@
( rows . length
? '<ul class="cpg-dist-items">' + items + "</ul>"
: '<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="' +
@@ -1292,33 +1258,10 @@
} ) ;
// ── 혼합 상자 내용물 편집 이벤트 ──────────────────
// 수량은 상자 사이에서만 오간다(총량 보존). 갈 곳이 없으면 새 상자를 만든다.
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" ) ;
@@ -1330,95 +1273,81 @@
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 ) ;
var left = it . quantity ;
while ( left > 0 ) {
var dest = boxWithRoom ( dcid , it . product _code , entry . key ) || newMixBox ( dcid ) ;
var moved = moveUnits ( entry . key , dest , it . product _code , it . product _name , left ) ;
if ( ! moved ) break ;
left -= moved ;
}
} ) ;
contents [ entry . key ] = [ ] ;
syncMix ( entry . key ) ;
render ( ) ;
msg . textContent = "혼합 상자를 비우고 미배치 로 옮겼습니다." ;
msg . textContent = "혼합 상자를 비우고 내용물을 다른 상자 로 옮겼습니다." ;
}
return ;
}
var dec = e . target . closest ( "[data-mix-dec]" ) ;
if ( dec ) {
var key = dec . getAttribute ( "data-key" ) ;
var code = dec . getAttribute ( "data-code" ) ;
var cid = centerOfKey ( key ) ;
var item = mixItem ( key , code ) ;
if ( ! item ) return ;
var to = boxWithRoom ( cid , code , key ) || newMixBox ( cid ) ;
var n = moveUnits ( key , to , code , item . product _name , 1 ) ;
if ( ! n ) { msg . textContent = "옮길 자리가 없습니다." ; return ; }
render ( ) ;
msg . textContent = "1개를 " + ( labels [ to ] || "다른 상자" ) + " 로 옮겼습니다." ;
return ;
}
var inc = e . target . closest ( "[data-mix-inc]" ) ;
if ( inc ) {
var ikey = inc . getAttribute ( "data-key" ) ;
var icode = inc . getAttribute ( "data-code" ) ;
var icid = centerOfKey ( ikey ) ;
if ( mixRoomFor ( ikey , icode ) <= 0 ) {
msg . textContent = "이 상자에 더 들어갈 자리가 없습니다." ; return ;
}
var from = boxHaving ( icid , icode , ikey ) ;
if ( ! from ) { msg . textContent = "다른 상자에 이 제품이 없습니다." ; return ; }
var src = mixItem ( from , icode ) ;
var got = moveUnits ( from , ikey , icode , src . product _name , 1 ) ;
if ( ! got ) { msg . textContent = "가져올 수 없습니다." ; return ; }
render ( ) ;
msg . textContent = "1개를 " + ( labels [ from ] || "다른 상자" ) + " 에서 가져왔습니다." ;
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 ( ! mv ) return ;
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 ) ;
var toKey = target === "__new__" ? newMixBox ( cid ) : target ;
var moved = moveUnits ( fromKey , toKey , code , name , qty ) ;
if ( ! moved ) { msg . textContent = "옮길 자리가 없습니다." ; render ( ) ; return ; }
render ( ) ;
msg . textContent = name + " " + moved + "개를 " + ( labels [ toKey ] || "새 상자" ) + " 로 옮겼습니다." +
( moved < qty ? " (자리가 부족해 " + ( qty - moved ) + "개는 남음)" : "" ) ;
} ) ;
}
@@ -1573,9 +1502,6 @@
if ( ! cids . length ) return "센터에 담긴 상자가 없습니다." ;
var missing = cids . filter ( function ( cid ) { return ! methods [ String ( cid ) ] ; } ) ;
if ( missing . length ) return "출고방식 미선택: " + missing . map ( centerName ) . join ( ", " ) ;
var leftover = 0 ;
openCenters . forEach ( function ( cid ) { leftover += poolTotal ( cid ) ; } ) ;
if ( leftover ) return "미배치 자투리 " + leftover + "개가 남았습니다." ;
return "" ;
}