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
+101 -10
View File
@@ -176,11 +176,21 @@ let _userNames=null; // lazy-loaded [{username,fullName}], only fetched if CAN
let _co=window.__DEFAULT_COMPANY__;let _companies=[{value:_co,label:_co}];
let _rows=[],_loading=false,_hasSig=false,_curPO=null;
let _viewSearch='',_viewStatus='',_viewPage=0; // _viewStatus: '' | 'ISSUED' | 'RECEIVED' | 'VERIFIED' — every row here is an approved Work Order already, so the filter is about which sign-off is still outstanding, not WO approval status
let _viewDateFrom='',_viewDateTo=''; // filters by the Work Order's Created date
let _sapBatchMatches=[], _sapBatchTimer=null; // cross-reference results from /sap/lookup/batch-in-sap — same rationale as work-order.html's identical function
const VF_PAGE_SIZE=24;
// This user's allowed Item Groups for issuing (Admin → user → Issue Items).
// Empty = no restriction. Same rule the real SAP issuance enforces.
let _issueGroups=[];
let _balanceIds=[]; // Admin → System Settings — allowed Weighing Balance IDs; empty = free text
// Admin → System Settings → "Raw Material Qty Issued Source". Only under
// 'issue_for_production' (the default) does Qty Issued get set BEFORE this
// row's own "Mark Issued" stamp (derived from the real SAP posting) — so
// only then does it make sense to require that qty first. Under
// 'mark_issued', THIS stamp is what sets Qty Issued to the full required
// amount in the first place, so gating the button on qty already being
// there would make it permanently unreachable.
let _rawQtyIssuedSource='issue_for_production';
function esc(v){return(v==null?'':String(v)).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');}
function q(s){return document.querySelector(s);}
@@ -299,7 +309,7 @@ const ROW_STAMP_PREREQ={received:'issued',verified:'received'};
// A row's Issued/Received/Verified cell: signer+date if already stamped, a
// one-click "Mark …" button if this user may stamp it AND its prerequisite
// stamp already exists, a "waiting on …" note if not, else a dash.
function stampCell(row,canMark,which,label){
function stampCell(row,canMark,which,label,blockedMsg){
const sig=rowSig(row,which);
if(sig){
// Already signed — normally final. A woVerifyOverride/admin user who
@@ -312,16 +322,18 @@ function stampCell(row,canMark,which,label){
return issSigDisp(sig);
}
const prereq=ROW_STAMP_PREREQ[which];
// A woVerifyOverride/admin user may sign a stage out of the normal
// Issued→Received→Verified order (server allows it — see canOverrideStamp
// in routes/workOrders.js) — everyone else still sees "Waiting on …" and
// can't act until the prior stage is done.
if(prereq&&!row[prereq+'At']&&!(canMark&&CAN_VERIFY_OVERRIDE))return `<span style="color:var(--text3);font-size:9.5px">Waiting on ${prereq}</span>`;
// The NEXT-action button only ever appears once the prior stage is
// genuinely done — no exception, for anyone, including a woVerifyOverride/
// admin user. Override is only ever for RE-SIGNING an already-completed
// stamp (see the `sig` branch above — correcting who signed/when), never
// for skipping straight past a stage that hasn't happened yet.
if(prereq&&!row[prereq+'At'])return `<span style="color:var(--text3);font-size:9.5px">Waiting on ${prereq}</span>`;
if(canMark){
if(CAN_VERIFY_OVERRIDE)
return `<button class="btn-mini" onclick="openVerifyOverride(${row.__woId},'${row.__section}',${row.__idx},'${which}')">${esc(label)}</button>`;
return `<button class="btn-mini" onclick="markRow(${row.__woId},'${row.__section}',${row.__idx},'${which}',this)">${esc(label)}</button>`;
}
if(blockedMsg)return `<span style="color:var(--text3);font-size:9.5px">${esc(blockedMsg)}</span>`;
return '<span style="color:var(--text3)">—</span>';
}
// Weighing Balance ID / AR No. — editable inline by whoever holds 'edit' on
@@ -352,8 +364,42 @@ function issCells(row,iss,groupMap){
// and workOrderPdf.js already do.
const qtyVal=row.__section==='raw'?row.qtyIssued:(iss.qtyByItem&&iss.qtyByItem[(row.itemCode||'').trim()]);
const qty=`<td class="wo-td">${num3wo(qtyVal)}</td>`;
const canIssue=CAN_MARK_ISSUED&&itemGroupAllowed(row.itemCode,groupMap);
return qty+`<td class="wo-td">${stampCell(row,canIssue,'issued','Mark Issued')}</td><td class="wo-td">${stampCell(row,CAN_MARK_RECEIVED,'received','Mark Received')}</td><td class="wo-td">${stampCell(row,CAN_MARK_VERIFIED,'verified','Mark Verified')}</td>`;
// "Mark Issued" only appears once this row's FULL required quantity has
// actually been issued (in SAP for Packing/live off the PO; persisted on
// the row itself for Raw Material — see the comment above) — not merely
// whenever some partial quantity has gone out. A 0.001 tolerance absorbs
// float rounding from the prorated Raw Material figure.
const reqQty=parseFloat(row.__section==='raw'?row.qtyReq:row.qtyReqUnits)||0;
// The 'mark_issued' bypass ONLY applies to Raw Material — its displayed
// Qty Issued is the persisted, prorated value this exact stamp writes
// under that mode (see the server route), so checking it first would be
// circular. Packing Material's displayed Qty Issued always comes straight
// off the live SAP posting (qtyByItem above) no matter which mode is set —
// it's never written by this stamp — so it must always be checked for
// real, or a Work Order with no Production Order/issuance yet (qtyVal
// empty) would wrongly show "Mark Issued" as available.
// Even under the 'mark_issued' bypass, nothing is issuable before a
// Production Order actually exists for this Work Order — there's no
// production event at all yet for the raw-material stamp to be recording.
// This is what actually closes the reported bug: a fresh WO with "No
// Production Order created yet" was still showing every raw-material row
// as markable, because the bypass alone had no precondition of its own.
const rawBypass=row.__section==='raw'&&_rawQtyIssuedSource==='mark_issued'&&iss.hasPO;
const fullyIssued=rawBypass||(iss.hasPO&&(reqQty<=0||(parseFloat(qtyVal)||0)+0.001>=reqQty));
const permittedToIssue=CAN_MARK_ISSUED&&itemGroupAllowed(row.itemCode,groupMap);
const canIssue=permittedToIssue&&fullyIssued;
const issuedBlockedMsg=(permittedToIssue&&!fullyIssued)?(iss.hasPO?'Awaiting full qty issued':'No Production Order yet'):undefined;
// Received/Verified must also require a Production Order to exist — not
// just because Issued isn't stamped yet (the ordinary prereq check below
// already covers that for everyone), but specifically because a
// woVerifyOverride/admin user is otherwise allowed to sign OUT of the
// normal order to catch up genuine paperwork, and that bypass alone has no
// "did production even start" precondition of its own. Without gating it
// here too, that same user could stamp Received/Verified on a Work Order
// with no Production Order at all — nothing to actually be catching up on.
const canReceive=CAN_MARK_RECEIVED&&iss.hasPO;
const canVerify=CAN_MARK_VERIFIED&&iss.hasPO;
return qty+`<td class="wo-td">${stampCell(row,canIssue,'issued','Mark Issued',issuedBlockedMsg)}</td><td class="wo-td">${stampCell(row,canReceive,'received','Mark Received')}</td><td class="wo-td">${stampCell(row,canVerify,'verified','Mark Verified')}</td>`;
}
function buildWoMaterials(w,iss,groupMap){
@@ -493,6 +539,13 @@ function filteredRows(){
(w.itemName||'').toUpperCase().includes(s)||
(w.batchNumber||'').toUpperCase().includes(s)||
(w.intimationDocNo||'').toUpperCase().includes(s));
if(_viewDateFrom||_viewDateTo)list=list.filter(w=>{
if(!w.createdAt)return false;
const d=w.createdAt.slice(0,10);
if(_viewDateFrom&&d<_viewDateFrom)return false;
if(_viewDateTo&&d>_viewDateTo)return false;
return true;
});
return list;
}
function woCard(w){
@@ -513,6 +566,7 @@ function woCard(w){
<span><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="flex-shrink:0"><line x1="4" y1="9" x2="20" y2="9"/><line x1="4" y1="15" x2="20" y2="15"/><line x1="10" y1="3" x2="8" y2="21"/><line x1="16" y1="3" x2="14" y2="21"/></svg> Batch ${esc(w.batchNumber||'—')}</span>
<span><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="flex-shrink:0"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg> Intimation ${esc(w.intimationDocNo||'—')}</span>
<span><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="flex-shrink:0"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg> ${esc(w.createdByName||w.createdBy||'—')}</span>
${w.createdAt?`<span>${new Date(w.createdAt).toLocaleDateString('en-IN',{day:'2-digit',month:'short',year:'2-digit'})}</span>`:''}
<span style="color:var(--text3)">${esc(poChip)}</span>
</div>
</div>`;
@@ -540,9 +594,19 @@ function render(){
!_rows.length?`<div class="empty-msg">No approved work orders yet.</div>`:`
<div class="vf-bar">
<div class="vf-search"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="position:absolute;left:11px;top:50%;transform:translateY(-50%);color:var(--text3)"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg><input id="vf-search" placeholder="Search WO No, product, batch, intimation…" value="${esc(_viewSearch)}" style="padding-left:32px"/></div>
<input class="fi" id="vf-datefrom" type="date" value="${esc(_viewDateFrom)}" style="width:140px"/>
<span style="font-size:11px;color:var(--text3)">to</span>
<input class="fi" id="vf-dateto" type="date" value="${esc(_viewDateTo)}" style="width:140px"/>
<div class="vf-chips">${chips.map(([v,lbl])=>`<button class="vf-chip ${_viewStatus===v?'on':''}" data-vfstatus="${v}" title="${v?'Still pending '+lbl:'All approved Work Orders'}">${lbl}</button>`).join('')}</div>
<span class="vf-count">${list.length} of ${_rows.length}</span>
</div>
${_sapBatchMatches.length?`<div style="margin:0 0 12px;padding:10px 12px;background:var(--surface2,#f5f7fa);border:1px solid var(--border,#dfe4ea);border-radius:10px">
<div style="font-size:11px;font-weight:700;color:var(--text2);margin-bottom:6px">Also found in SAP (not one of this portal's own Work Orders):</div>
${_sapBatchMatches.map(m=>m.source==='productionOrder'
?`<div style="font-size:12px;padding:3px 0"><a href="/production#order=${m.absEntry}&company=${encodeURIComponent(_co)}" style="color:#2563eb" target="_blank">Production Order ${esc(String(m.docNum))}</a> — ${esc(m.itemCode)} ${esc(m.itemName||'')} · Batch (SAP Comments) <span style="font-family:monospace">${esc(m.batch)}</span></div>`
:`<div style="font-size:12px;padding:3px 0">SAP Batch Master — ${esc(m.itemCode)} ${esc(m.itemName||'')} · <span style="font-family:monospace">${esc(m.batch)}</span></div>`
).join('')}
</div>`:''}
${pageList.length?`<div class="wo-grid">${pageList.map(woCard).join('')}</div>`:'<div class="empty-msg">No work orders match this search/filter.</div>'}
${list.length>VF_PAGE_SIZE?`<div class="vf-pager">
<button id="vf-prev" ${_viewPage===0?'disabled':''}>← Prev</button>
@@ -556,8 +620,11 @@ function render(){
q('#vf-search')&&(q('#vf-search').oninput=function(){
const pos=this.selectionStart;_viewSearch=this.value;_viewPage=0;render();
const el=q('#vf-search');if(el){el.focus();try{el.setSelectionRange(pos,pos);}catch(_e){}}
scheduleSapBatchLookup();
});
document.querySelectorAll('[data-vfstatus]').forEach(b=>b.onclick=function(){_viewStatus=this.dataset.vfstatus;_viewPage=0;render();});
q('#vf-datefrom')&&(q('#vf-datefrom').onchange=function(){_viewDateFrom=this.value;_viewPage=0;render();});
q('#vf-dateto')&&(q('#vf-dateto').onchange=function(){_viewDateTo=this.value;_viewPage=0;render();});
if(q('#vf-prev'))q('#vf-prev').onclick=function(){_viewPage--;render();};
if(q('#vf-next'))q('#vf-next').onclick=function(){_viewPage++;render();};
document.querySelectorAll('[data-open]').forEach(el=>el.onclick=function(){
@@ -565,15 +632,39 @@ function render(){
});
}
// Same cross-reference lookup as Batch Issuance/Work Order's own search
// boxes — see batch-issuance.html's identical function for the full
// rationale (SAP Production Orders created directly in SAP B1 store their
// batch number as free text in Comments, invisible to every local search).
function scheduleSapBatchLookup(){
clearTimeout(_sapBatchTimer);
const term=_viewSearch.trim();
if(term.length<4){
if(_sapBatchMatches.length){_sapBatchMatches=[];render();}
return;
}
_sapBatchTimer=setTimeout(async()=>{
if(_viewSearch.trim()!==term)return;
try{
const r=await api('GET',`/sap/lookup/batch-in-sap?batch=${encodeURIComponent(term)}&company=${encodeURIComponent(_co)}`);
if(_viewSearch.trim()!==term)return;
_sapBatchMatches=r.data||[];
}catch(_e){_sapBatchMatches=[];}
const pos=q('#vf-search')?.selectionStart;
render();
const s2=q('#vf-search');if(s2&&pos!=null){s2.focus();try{s2.setSelectionRange(pos,pos);}catch(_e){}}
},450);
}
async function init(){
try{const list=await window.fetchCompanyList();if(list.length)_companies=list;}catch(_e){}
try{const r=await api('GET','/auth/signature');_hasSig=!!(r&&r.signature);}catch(_e){}
try{const pr=await api('GET','/auth/profile');_issueGroups=Array.isArray(pr?.user?.issueItemGroups)?pr.user.issueItemGroups.map(String):[];}catch(_e){}
try{const cfg=await api('GET','/config');if(Array.isArray(cfg?.woWeighingBalanceIds))_balanceIds=cfg.woWeighingBalanceIds;}catch(_e){}
try{const cfg=await api('GET','/config');if(Array.isArray(cfg?.woWeighingBalanceIds))_balanceIds=cfg.woWeighingBalanceIds;if(cfg?.woRawQtyIssuedSource)_rawQtyIssuedSource=cfg.woRawQtyIssuedSource;}catch(_e){}
if(CAN_VIEW)load();else render();
}
init();
</script>
<script src="/sidebar.js?v=25"></script>
<script src="/sidebar.js?v=31"></script>
</body>
</html>