sale order
SAP-ERP Portal CI/CD / build (push) Successful in 3m57s

This commit is contained in:
John
2026-10-05 18:45:17 +05:30
parent e725a6571b
commit eead8f5ffd
129 changed files with 14252 additions and 452 deletions
+59 -7
View File
@@ -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>