feat(dispatch): 말레이시아 재고 차감 버튼(날짜 1회성)

- 달력 다운로드 아래 '말레이시아 재고 차감' 버튼. 선택 날짜 출고를
  말레이시아 재고관리에 OUT 으로 반영(movement_date=해당 날짜)
  · 낱개(MT/MX/MZ)=낱개 OUT, 콤보(MY-)=세트 OUT(재고관리가 BOM 분해)
  · _N 배수 반영, MD-(뚜껑) 제외
- 날짜당 1회만: dispatch_stock_deductions 마커로 재차감 차단(이중 출고 방지)
  · 마커 선점 후 반영, 콤보 BOM 누락은 사전검증으로 차단
- export.split_singles_combos, db 차감 가드 메서드, 라우터 POST /dispatch/stock-deduct
- 스키마: dispatch_stock_deductions (init + 마이그레이션 dispatch_add_deductions.sql)
- 이미 차감된 날짜는 버튼 비활성 + 안내

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-22 18:07:37 +09:00
parent 27e2c17606
commit b583627bd3
6 changed files with 259 additions and 3 deletions
@@ -109,6 +109,10 @@
<a id="dsp-cal-dl" class="erp-btn erp-btn-primary" href="#"
data-ko="📥 사방넷 출고 엑셀 다운로드" data-en="📥 Download Sabangnet Outbound Excel"
style="pointer-events:none;opacity:.5;">📥 사방넷 출고 엑셀 다운로드</a>
<button id="dsp-cal-deduct" type="button" class="erp-btn erp-btn-outline" onclick="dspDeduct()"
data-ko="📦 말레이시아 재고 차감" data-en="📦 Deduct Malaysia Stock"
style="pointer-events:none;opacity:.5;">📦 말레이시아 재고 차감</button>
<div id="dsp-cal-deduct-note" class="erp-muted" style="font-size:12px;"></div>
</div>
</div>
</div>
@@ -120,6 +124,8 @@
(function () {
// 배치가 있는 날짜(YYYY-MM-DD)
var BATCH_DATES = {{ batch_dates|tojson }};
var DEDUCTED = {};
({{ deducted_dates|tojson }}).forEach(function (d) { DEDUCTED[d] = true; });
var dateSet = {};
BATCH_DATES.forEach(function (d) { dateSet[d] = true; });
@@ -127,6 +133,8 @@
var title = document.getElementById('dsp-cal-title');
var selLabel = document.getElementById('dsp-cal-sel');
var dlBtn = document.getElementById('dsp-cal-dl');
var dedBtn = document.getElementById('dsp-cal-deduct');
var dedNote = document.getElementById('dsp-cal-deduct-note');
var selected = null;
// 초기 표시 월: 가장 최근 배치 날짜 기준(없으면 오늘)
@@ -169,16 +177,62 @@
dlBtn.href = '/dispatch/stock-export?date=' + encodeURIComponent(iso);
dlBtn.style.pointerEvents = '';
dlBtn.style.opacity = '';
updateDeductBtn();
render();
}
function updateDeductBtn() {
var en = (window.dspLang && window.dspLang() === 'en');
if (!selected) {
dedBtn.style.pointerEvents = 'none'; dedBtn.style.opacity = '.5';
dedNote.textContent = '';
return;
}
if (DEDUCTED[selected]) {
dedBtn.style.pointerEvents = 'none'; dedBtn.style.opacity = '.5';
dedNote.textContent = en ? 'Already deducted (cannot run again).' : '이미 차감됨 (재실행 불가).';
} else {
dedBtn.style.pointerEvents = ''; dedBtn.style.opacity = '';
dedNote.textContent = '';
}
}
window.dspDeduct = function () {
if (!selected || DEDUCTED[selected]) return;
var en = (window.dspLang && window.dspLang() === 'en');
var msg = en
? ('Deduct Malaysia stock for ' + selected + '? This runs ONCE only and cannot be undone.')
: (selected + ' 출고를 말레이시아 재고에서 차감할까요?\n한 번만 실행되며 되돌릴 수 없습니다.');
if (!confirm(msg)) return;
dedBtn.style.pointerEvents = 'none'; dedBtn.style.opacity = '.5';
dedNote.textContent = en ? 'Processing…' : '처리 중…';
var body = new URLSearchParams(); body.set('date', selected);
fetch('/dispatch/stock-deduct', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: body.toString(),
})
.then(function (r) { return r.json().then(function (j) { return { ok: r.ok, j: j }; }); })
.then(function (res) {
if (!res.ok) throw new Error((res.j && res.j.detail) || '실패');
DEDUCTED[selected] = true;
updateDeductBtn();
alert((en ? 'Done. singles ' : '완료. 낱개 ') + res.j.singles + (en ? ', combos ' : '종, 콤보 ') + res.j.combos + (en ? '' : '종 반영'));
})
.catch(function (e) {
dedNote.textContent = (e && e.message) || '실패';
updateDeductBtn();
alert((en ? 'Failed: ' : '실패: ') + ((e && e.message) || ''));
});
};
document.getElementById('dsp-cal-prev').onclick = function () {
viewM--; if (viewM < 0) { viewM = 11; viewY--; } render();
};
document.getElementById('dsp-cal-next').onclick = function () {
viewM++; if (viewM > 11) { viewM = 0; viewY++; } render();
};
document.addEventListener('dsp:lang', render);
document.addEventListener('dsp:lang', function () { render(); updateDeductBtn(); });
render();
})();
</script>