@@ -66,6 +66,7 @@ select.fi option{background:var(--surface2)}
|
||||
.alert{padding:10px 12px;border-radius:8px;font-size:12px;margin-bottom:12px;line-height:1.5}
|
||||
.al-green{background:rgba(13,187,138,.08);border:1px solid rgba(13,187,138,.2);color:#6ee7b7}
|
||||
.al-amber{background:rgba(245,166,35,.08);border:1px solid rgba(245,166,35,.2);color:#fcd34d}
|
||||
.al-red{background:rgba(229,62,62,.1);border:1px solid rgba(229,62,62,.3);color:#fca5a5}
|
||||
.sp{width:14px;height:14px;border:2px solid rgba(255,255,255,.25);border-top-color:#fff;border-radius:50%;animation:spin .6s linear infinite;display:inline-block}
|
||||
@keyframes spin{to{transform:rotate(360deg)}}
|
||||
.toast-c{position:fixed;top:56px;right:10px;z-index:9999;display:flex;flex-direction:column;gap:6px;pointer-events:none}
|
||||
@@ -169,6 +170,7 @@ let _readyOrders=[];
|
||||
let _readyLoading=false;
|
||||
let _modalOpen=false;
|
||||
let _page=0,_pageSize=20,_total=null,_rawCount=0,_search=''; // ready-orders pagination + server search
|
||||
let _dateFrom='',_dateTo=''; // Due Date range, sent to the server (see loadReadyOrders())
|
||||
let _viewOrderType=''; // '' = all, 'consumable' = Consumable Orders only, 'regular' = everything else
|
||||
|
||||
// Receipt fields
|
||||
@@ -184,6 +186,8 @@ let _expDate='';
|
||||
let _datesLocked=false;
|
||||
let _variety='';
|
||||
let _postDate='';
|
||||
let _backflushWarnings=[]; // [{itemCode,itemName,warehouse,required,onHand,shortfall}] — insufficient Backflush-component stock, see checkBackflushStock()
|
||||
let _bfCheckTimer=null;
|
||||
|
||||
// Return Components (defective components back to stock after Issuance)
|
||||
let _retLines=[];
|
||||
@@ -237,9 +241,16 @@ async function loadReadyOrders(resetPage){
|
||||
_readyLoading=true;render();
|
||||
const searchQ=_search.trim()?`&search=${encodeURIComponent(_search.trim())}`:'';
|
||||
try{
|
||||
const r=await api('GET',`/sap/production-orders?status=Released&readyToReceive=1&top=${_pageSize}&skip=${_page*_pageSize}&company=${encodeURIComponent(_co)}${searchQ}${_viewOrderType?'&orderType='+_viewOrderType:''}`);
|
||||
const dateQ=(_dateFrom?`&dateFrom=${_dateFrom}`:'')+(_dateTo?`&dateTo=${_dateTo}`:'');
|
||||
const r=await api('GET',`/sap/production-orders?status=Released&readyToReceive=1&top=${_pageSize}&skip=${_page*_pageSize}&company=${encodeURIComponent(_co)}${searchQ}${dateQ}${_viewOrderType?'&orderType='+_viewOrderType:''}`);
|
||||
// Server-side WHERE (readyToReceive, routes/sap.js) now guarantees every
|
||||
// returned row is already "at least partially issued, not yet fully
|
||||
// received" — no client-side re-filter needed anymore. Re-filtering here
|
||||
// used to be able to shrink a full 20-row page down to zero visible
|
||||
// cards (an order not issued at all is still "Released"), while the
|
||||
// pager kept counting every Released order as the total regardless.
|
||||
_rawCount=(r.data||[]).length;
|
||||
_readyOrders=(r.data||[]).filter(o=>(o._issueStatus==='full'||o._issueStatus==='partial')&&o._receiptStatus!=='done');
|
||||
_readyOrders=r.data||[];
|
||||
_total=(typeof r.total==='number')?r.total:null;
|
||||
}catch(_e){_readyOrders=[];_rawCount=0;_total=null;}
|
||||
_readyLoading=false;render();
|
||||
@@ -299,10 +310,12 @@ async function loadOrder(absEntry){
|
||||
// FG group list. Non-blocking — the modal renders immediately.
|
||||
_autoclaveQty='';_isFgItem=false;
|
||||
}catch(e){toast(e.message,'err');}
|
||||
_backflushWarnings=[];
|
||||
_loadingOrder=false;render();
|
||||
if(_retLines.length) loadReturnBatches();
|
||||
if(_autoclaveEnabled&&_autoclaveGroups.length&&_order?.ItemNo) loadFgFlag(_order.ItemNo);
|
||||
loadRecvUoms();
|
||||
checkBackflushStock();
|
||||
}
|
||||
|
||||
// Unit (Inventory UOM) shown next to each Warehouse/Qty/Transaction Type
|
||||
@@ -375,6 +388,38 @@ function refreshRecvTotal(){
|
||||
const qtyMatches=!_requireFullQty||Math.abs(recvTotal()-remainingQty())<=0.0001;
|
||||
btn.disabled=_submitting||!qtyMatches;
|
||||
}
|
||||
// Backflush stock-shortage check depends on the total too — debounced so it
|
||||
// doesn't fire on every keystroke.
|
||||
clearTimeout(_bfCheckTimer);
|
||||
_bfCheckTimer=setTimeout(checkBackflushStock,400);
|
||||
}
|
||||
// Pre-flight warning for SAP's own "-5002 consumed quantity would cause
|
||||
// inventory to fall below zero": Backflush-type components are auto-consumed
|
||||
// by SAP when the Receipt posts (never manually issued), so nothing else in
|
||||
// this flow ever checks their stock. Purely informational — never blocks
|
||||
// Post, since the admin/user may still want to proceed (e.g. SAP's "allow
|
||||
// negative inventory" is enabled for that item) or fix stock afterwards.
|
||||
async function checkBackflushStock(){
|
||||
if(!_order?.AbsoluteEntry){_backflushWarnings=[];return;}
|
||||
const qty=recvTotal();
|
||||
if(!(qty>0)){_backflushWarnings=[];refreshBackflushWarn();return;}
|
||||
try{
|
||||
const r=await api('GET',`/sap/production-orders/${_order.AbsoluteEntry}/backflush-check?qty=${qty}&company=${encodeURIComponent(_co)}`);
|
||||
_backflushWarnings=r.warnings||[];
|
||||
}catch(_e){_backflushWarnings=[];}
|
||||
refreshBackflushWarn();
|
||||
}
|
||||
function backflushWarnHtml(){
|
||||
if(!_backflushWarnings.length)return'';
|
||||
return `<div class="alert al-red" id="backflush-warn-inner">
|
||||
<strong>⚠ Insufficient stock for ${_backflushWarnings.length} auto-consumed component${_backflushWarnings.length>1?'s':''}:</strong>
|
||||
${_backflushWarnings.map(w=>`<div style="margin-top:4px">${esc(w.itemCode)} ${esc(w.itemName?'('+w.itemName+')':'')} — needs <strong>${fmtQty(w.required)}</strong> at Whse ${esc(w.warehouse)}, only <strong>${fmtQty(w.onHand)}</strong> on hand (short by ${fmtQty(w.shortfall)}).</div>`).join('')}
|
||||
<div style="margin-top:6px;font-size:11px;opacity:.85">These items are Backflush-type (auto-consumed by SAP on Receipt, never manually issued). SAP will likely reject the post with error -5002 unless this stock is topped up first.</div>
|
||||
</div>`;
|
||||
}
|
||||
function refreshBackflushWarn(){
|
||||
const el=document.getElementById('backflush-warn');
|
||||
if(el)el.innerHTML=backflushWarnHtml();
|
||||
}
|
||||
// MFG/EXP — 6 selectable formats (2 full-date + 4 month-only). Entry is
|
||||
// picker-only (see rcptDateField()/openRcptDatePicker() below) — the field
|
||||
@@ -672,18 +717,22 @@ function _doRender(){
|
||||
<div class="po-top"><span class="po-code">${esc(o.ItemNo)}</span>${o._issueStatus==='partial'?'<span style="font-size:8px;font-weight:700;padding:1px 5px;border-radius:3px;background:rgba(245,166,35,.12);color:var(--amber)">PARTIAL</span>':''}${o.IsConsumable?'<span style="font-size:8px;font-weight:700;padding:1px 5px;border-radius:3px;background:rgba(159,90,253,.14);color:var(--purple)">CONSUMABLE</span>':''}</div>
|
||||
<div class="po-icon">📥</div>
|
||||
<div class="po-name" title="${esc(o.ProductDescription||'')}">${esc(o.ProductDescription||'-')}</div>
|
||||
<div class="po-meta"><span>PWO No#<b>${o.DocumentNumber||'-'}</b>${o.PostingDate?` · ${fmtDate(o.PostingDate)}`:''}</span><span>Qty <b>${o.PlannedQuantity}</b> · Done <b>${o.CompletedQuantity||0}</b> · WH <b>${esc(o.Warehouse||'-')}</b></span></div>
|
||||
<div class="po-meta"><span>PWO No#<b>${o.DocumentNumber||'-'}</b>${o.PostingDate?` · ${fmtDate(o.PostingDate)}`:''}</span><span>Qty <b>${o.PlannedQuantity}</b> · Done <b>${o.CompletedQuantity||0}</b> · WH <b>${esc(o.Warehouse||'-')}</b>${o.DueDate?` · Due <b>${fmtDate(o.DueDate)}</b>`:''}</span></div>
|
||||
${(o.WoNo||o.BatchNumber)?`<div class="po-meta"><span>${o.WoNo?`WO <b>${esc(o.WoNo)}</b>`:''}${o.WoNo&&o.BatchNumber?' · ':''}${o.BatchNumber?`Batch <b>${esc(o.BatchNumber)}</b>`:''}</span></div>`:''}
|
||||
</div>`).join('')}</div>`;
|
||||
const searchRow=`<div style="display:flex;gap:6px;margin-bottom:10px">
|
||||
<input class="fi fi-mono" id="rp-search" value="${esc(_search)}" placeholder="Search item code, description, document no, order no, WO no or batch no…" style="font-size:12px;flex:1"/>
|
||||
const searchRow=`<div style="display:flex;gap:6px;margin-bottom:10px;flex-wrap:wrap">
|
||||
<input class="fi fi-mono" id="rp-search" value="${esc(_search)}" placeholder="Search item code, description, document no, order no, WO no or batch no…" style="font-size:12px;flex:1;min-width:220px"/>
|
||||
<span style="font-size:10px;color:var(--text3);text-transform:uppercase;letter-spacing:.4px;align-self:center">Due</span>
|
||||
<input class="fi" id="rp-datefrom" type="date" value="${esc(_dateFrom)}" style="font-size:12px"/>
|
||||
<span style="font-size:11px;color:var(--text3);align-self:center">to</span>
|
||||
<input class="fi" id="rp-dateto" type="date" value="${esc(_dateTo)}" style="font-size:12px"/>
|
||||
<button class="btn" id="rp-load" style="padding:6px 16px;font-size:11px;background:var(--green);color:#fff;white-space:nowrap">Search</button>
|
||||
</div>`;
|
||||
const typeRow=CAN_CREATE_CONSUMABLE?`<div style="display:flex;gap:4px;margin-bottom:10px;flex-wrap:wrap;align-items:center">
|
||||
<span style="font-size:10px;color:var(--text3);text-transform:uppercase;letter-spacing:.4px;margin-right:2px">Type</span>
|
||||
${[['','All'],['regular','Regular PWO'],['consumable','Consumable']].map(([v,l])=>`<button class="btn" style="padding:5px 11px;font-size:11px;${_viewOrderType===v?'background:rgba(159,90,253,.15);color:var(--purple);border:1px solid rgba(159,90,253,.4)':'background:var(--surface2);color:var(--text3);border:1px solid var(--border2)'}" data-vot="${v}">${l}</button>`).join('')}
|
||||
</div>`:'';
|
||||
c2b.innerHTML=`<div class="card-hd"><span class="ico"></span> Orders Ready to Receive<span class="right">${_readyOrders.length}</span></div>
|
||||
c2b.innerHTML=`<div class="card-hd"><span class="ico"></span> Orders Ready to Receive<span class="right">${_total!=null?_total:_readyOrders.length}</span></div>
|
||||
<div class="card-bd">
|
||||
<div style="font-size:11px;color:var(--text3);margin-bottom:10px">Only Released orders that are fully issued (all components) are listed here, per Admin → System Settings → "Require full issuance before Receipt/Close". A partially-issued order isn't shown — issue the rest first, then it appears here.</div>
|
||||
${searchRow}${typeRow}${gridBody}${renderPager()}</div>`;
|
||||
@@ -697,6 +746,8 @@ function _doRender(){
|
||||
q('#rp-search')&&(q('#rp-search').oninput=function(){_search=this.value;});
|
||||
q('#rp-search')&&(q('#rp-search').onkeydown=function(e){if(e.key==='Enter'){e.preventDefault();loadReadyOrders(true);}});
|
||||
q('#rp-load')&&(q('#rp-load').onclick=()=>loadReadyOrders(true));
|
||||
q('#rp-datefrom')&&(q('#rp-datefrom').onchange=function(){_dateFrom=this.value;loadReadyOrders(true);});
|
||||
q('#rp-dateto')&&(q('#rp-dateto').onchange=function(){_dateTo=this.value;loadReadyOrders(true);});
|
||||
document.querySelectorAll('[data-vot]').forEach(b=>b.onclick=function(){_viewOrderType=this.dataset.vot;loadReadyOrders(true);});
|
||||
q('#rp-prev')&&(q('#rp-prev').onclick=()=>{if(_page>0){_page--;loadReadyOrders();}});
|
||||
q('#rp-next')&&(q('#rp-next').onclick=()=>{if(pagerHasNext()){_page++;loadReadyOrders();}});
|
||||
@@ -797,6 +848,7 @@ function renderModal(){
|
||||
</div>`;}).join('')}</div>
|
||||
<button type="button" class="btn b-cancel" id="btn-add-wh" style="margin-top:6px;padding:6px 14px;font-size:12px">+ Add Warehouse</button>
|
||||
<div id="recv-total" style="margin-top:6px;font-size:11px;color:var(--text3)">${recvTotalHtml()}</div>
|
||||
<div id="backflush-warn" style="margin-top:8px">${backflushWarnHtml()}</div>
|
||||
</div>
|
||||
<div class="fg" style="margin-top:4px;border-top:1px solid var(--border);padding-top:10px">
|
||||
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;font-size:12px;color:var(--text);font-weight:600">
|
||||
@@ -1005,6 +1057,6 @@ window.addEventListener('hashchange',()=>{
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<script src="/sidebar.js?v=25"></script>
|
||||
<script src="/sidebar.js?v=31"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user